|
| 1 | +# Config Transfer API |
| 2 | + |
| 3 | +Telegram SMS uses a small Cloudflare Worker (`cf-kv-storage`) as a **short-lived, encrypted config-transfer relay**. It lets the web [Config Generator](https://config.telegram-sms.com/) and the Android app's *Transfer config* feature (`TransferConfigActivity`) exchange configuration without a persistent backend or user accounts. |
| 4 | + |
| 5 | +The relay never sees plaintext. The client encrypts the configuration locally and uploads only the opaque ciphertext; the server stores it under a short, human-shareable key and returns it exactly once. Security comes from three layers working together: |
| 6 | + |
| 7 | +1. **Client-side encryption** — the payload is encrypted before upload (libsodium SecretBox, the same `Crypto` scheme the app uses elsewhere). |
| 8 | +2. **Short TTL** — config entries expire after **1 hour**. |
| 9 | +3. **One-time read** — fetching a config deletes it immediately, so a key can only be redeemed once. |
| 10 | + |
| 11 | +The key itself is intentionally short and is *not* a secret; it is meant to be typed or scanned between two of your own devices within a few minutes. |
| 12 | + |
| 13 | +## Tech stack |
| 14 | + |
| 15 | +- **Cloudflare Workers** — serverless runtime |
| 16 | +- **itty-router** (`AutoRouter`) — routing + CORS (`preflight` / `corsify`) |
| 17 | +- **Workers KV** — storage for the encrypted blobs |
| 18 | +- **TypeScript**, tested with **Vitest** (`@cloudflare/vitest-pool-workers`), deployed with **Wrangler** |
| 19 | + |
| 20 | +The entire service is two source files: `src/index.ts` (routes) and `src/snowflake.ts` (key generator). |
| 21 | + |
| 22 | +## Endpoints |
| 23 | + |
| 24 | +| Method & path | KV namespace | TTL | Read behaviour | Used by | |
| 25 | +| -------------------- | ----------------- | ------ | --------------------- | -------------------------------- | |
| 26 | +| `GET /` | — | — | 302 → telegram-sms.com | — | |
| 27 | +| `PUT /config` | `telegram_config` | 1 hour | — | Upload main app config | |
| 28 | +| `GET /config?key=` | `telegram_config` | — | one-time (deletes) | Download main app config | |
| 29 | +| `PUT /cc-config` | `cc_config` | 1 hour | — | Upload Carbon Copy config | |
| 30 | +| `GET /cc-config?key=`| `cc_config` | — | one-time (deletes) | Download Carbon Copy config | |
| 31 | +| `POST /log` | `telegram_log` | none | no read route | Upload diagnostic log (write-only) | |
| 32 | + |
| 33 | +### Upload (`PUT /config`, `PUT /cc-config`) |
| 34 | + |
| 35 | +Body is a JSON object whose `encrypt` field holds the already-encrypted blob. The Worker generates a key, stores the blob with a 1-hour TTL, and returns the key. |
| 36 | + |
| 37 | +```bash |
| 38 | +curl -X PUT https://<worker-host>/config \ |
| 39 | + -H "Content-Type: application/json" \ |
| 40 | + -d '{"encrypt": "<client-encrypted-config>"}' |
| 41 | +# → {"key":"abc123xyz"} |
| 42 | +``` |
| 43 | + |
| 44 | +### Download (`GET /config`, `GET /cc-config`) |
| 45 | + |
| 46 | +Pass the key as the `key` query parameter. The Worker returns the stored blob **and deletes it**, so a second request with the same key returns `404 Value not found`. |
| 47 | + |
| 48 | +```bash |
| 49 | +curl "https://<worker-host>/config?key=abc123xyz" |
| 50 | +# → <client-encrypted-config> (then the key is gone) |
| 51 | +``` |
| 52 | + |
| 53 | +### Diagnostic log (`POST /log`) |
| 54 | + |
| 55 | +Stores an encrypted blob in `telegram_log` and returns a key. There is intentionally **no read route** and **no TTL** on this namespace — it is a write-only sink for diagnostics inspected out-of-band. |
| 56 | + |
| 57 | +## Key generation |
| 58 | + |
| 59 | +Keys come from `Snowflake.generateKey()` (`src/snowflake.ts`), a singleton that builds a base-36 string from a **21-bit timestamp + 13-bit sequence** (Snowflake-style), right-padded with random `[A-Za-z0-9]` characters to a fixed **9 characters**. The result is short enough to read aloud or scan, which is the whole point — it is a transient handle, not a credential. |
| 60 | + |
| 61 | +## CORS |
| 62 | + |
| 63 | +`cors()` is configured in `src/index.ts`: |
| 64 | + |
| 65 | +- **Allowed origins:** `http://localhost:5173` (local Config Generator dev) and `https://config.telegram-sms.com` |
| 66 | +- **Allowed methods:** `GET`, `PUT` |
| 67 | +- **Allowed headers:** `Content-Type` |
| 68 | + |
| 69 | +> Because `allowMethods` lists only `GET` and `PUT`, the `POST /log` route is **not** reachable cross-origin from the config site under the current configuration. |
| 70 | +
|
| 71 | +## KV bindings |
| 72 | + |
| 73 | +`wrangler.toml` declares two namespaces: |
| 74 | + |
| 75 | +```toml |
| 76 | +[[kv_namespaces]] |
| 77 | +binding = "telegram_config" |
| 78 | +id = "..." |
| 79 | + |
| 80 | +[[kv_namespaces]] |
| 81 | +binding = "cc_config" |
| 82 | +id = "..." |
| 83 | +``` |
| 84 | + |
| 85 | +> **Known gap:** `telegram_log` is referenced by `POST /log` but is **not** declared in `wrangler.toml`. The `/log` route will fail until a `telegram_log` binding is added. |
| 86 | +
|
| 87 | +## Develop, test, deploy |
| 88 | + |
| 89 | +```bash |
| 90 | +npm run dev # wrangler dev — local server |
| 91 | +npm test # vitest |
| 92 | +npm run deploy # wrangler deploy |
| 93 | +npm run cf-typegen # regenerate Worker type definitions |
| 94 | +``` |
| 95 | + |
| 96 | +> The checked-in `test/index.spec.ts` is still the `create-cloudflare` "Hello World!" template and does not cover the real routes; it needs to be rewritten. |
| 97 | +
|
| 98 | +## How it fits the app |
| 99 | + |
| 100 | +The Android *Transfer config* flow and the web Config Generator both encrypt the config locally, `PUT` it here, and surface the returned key (often as a QR code). The receiving device redeems the key once via `GET`, decrypts locally, and applies the config. The relay only ever holds opaque ciphertext for at most an hour, and only until it is read once. |
0 commit comments