forked from c-fu/arah2
- Deployment record: workers.dev URL, KV namespace id, secrets set-once, setWebhook + setMyCommands (en/ms) ok - Live smokes: webhook 401 (no/wrong secret), /api/session 401 malformed + 405, 404 fallback, KV baseline [] - Deviation: production secrets uploaded via wrangler secret put (deploy chain is set-once for secrets)
173 lines
15 KiB
Markdown
173 lines
15 KiB
Markdown
---
|
||
phase: 02-bot-skeleton-initdata-security
|
||
plan: 03
|
||
subsystem: bot-surface
|
||
tags: [telegram, grammy, setmycommands, cloudflare-worker, webhook, session-gate, i18n, malay, deploy-gate]
|
||
|
||
# Dependency graph
|
||
requires:
|
||
- plan 02-01: `bot/` workspace scaffold — Env contract (BOT_TOKEN, WEBHOOK_SECRET, chat_sessions), wrangler.jsonc (KV_ID_PLACEHOLDER), delegate scripts
|
||
- plan 02-02: `validateInitData` (rejection classes malformed/tampered/stale) + `mintSession` (TTL-only, rotate-on-new) + SESSION_TTL_SECONDS
|
||
provides:
|
||
- `bot/src/i18n/strings.ts` — single copy source of truth (en + ms identical key-sets), pickLocale fallback rule; clinical/honest start + privacy copy matching the code's actual KV writes (PRIV-01)
|
||
- `bot/src/bot/bot.ts` createBot(env) + command modules start/help/privacy — text-only handlers for /start /mula /help /privacy /privasi (aliases share handlers per D-04)
|
||
- `bot/src/webhook.ts` — Worker entry: POST /telegram/webhook (grammY cloudflare-mod, constant-time secret-token auth) + POST /api/session (validateInitData → 401 category-only → mintSession); only two routes, 404 otherwise
|
||
- `bot/scripts/register-commands.mjs` — one-time setMyCommands in default + ms scopes, descriptions derived from helpMenu (no inline copy, no stub entries)
|
||
- `bot/scripts/set-webhook.mjs` + chained `npm run deploy` — setWebhook with secret_token + allowed_updates:["message"]
|
||
- `bot/test/commands.test.mjs` — CS1–CS6 command-surface gates (locale parity, pickLocale matrix, PRIV-01 copy gate, D-02 structural grep, D-03 menu gap-freedom, D-04 alias completeness)
|
||
affects: [phase 3 mini-app shell (same Worker, session endpoint already live), phase 4 Mini App caller of /api/session, phase 5 guidance envelope]
|
||
|
||
# Tech tracking
|
||
tech-stack:
|
||
added: [] # zero added deps — grammy webhookCallback + node builtins only
|
||
patterns: [per-request Bot instantiation from env-bound token, webhookCallback(bot,"cloudflare-mod",undefined,undefined,secret) with constant-time header compare, descriptions derived from helpMenu lines (copy survives in one place), pure replyText builders + thin handlers, category-only rejection logging, KV namespace id placeholder until deploy checkpoint]
|
||
|
||
key-files:
|
||
created:
|
||
- bot/src/i18n/strings.ts
|
||
- bot/src/bot/bot.ts
|
||
- bot/src/bot/commands/start.ts
|
||
- bot/src/bot/commands/help.ts
|
||
- bot/src/bot/commands/privacy.ts
|
||
- bot/scripts/register-commands.mjs
|
||
- bot/scripts/set-webhook.mjs
|
||
- bot/test/commands.test.mjs
|
||
- bot/src/webhook.ts
|
||
modified:
|
||
- bot/package.json (deploy script now chains wrangler deploy + set-webhook)
|
||
- bot/wrangler.jsonc (workers_dev: true marker; KV id still KV_ID_PLACEHOLDER)
|
||
|
||
key-decisions:
|
||
- "setMyCommands descriptions derive from strings.helpMenu's '/cmd — text' lines, so the registered menu and the /help reply cannot drift (D-03/D-05 in one mechanism)"
|
||
- "pickLocale uses exact 'ms' or 'ms-' prefix (not everything-starting-with-ms) so unrelated codes can never select Malay"
|
||
- "/api/session adds no extra branch beyond plan-02's mintSession — replay closing is purely D-07 rotate-on-new; endpoint has zero additional KV writes (T-02-06)"
|
||
- "Bot is instantiated per request inside the webhook branch only (env-bound token cannot resolve at module scope; /api/session path skips instantiation entirely)"
|
||
|
||
patterns-established:
|
||
- "Command-surface structural gates live in test/commands.test.mjs: any new keyboard/button surface or menu stub fails CS4/CS5"
|
||
- "D-14 discipline wide-scope: the only console statement in the Worker entry is '[initdata] rejected: <category>'"
|
||
|
||
requirements-completed: [] # ONBD-01/PRIV-01 code paths shipped; req completion awaits the D-16 real-client deploy gate (plan task 3)
|
||
|
||
# Metrics
|
||
duration: ~12min
|
||
completed: 2026-10-22
|
||
---
|
||
|
||
# Phase 2 Plan 3: Command Surface + Worker Wiring + Deploy Gate Summary
|
||
|
||
**The full user-facing Telegram surface (en + ms, text-only, honest privacy copy) and the Worker entry (secret-token webhook + validateInitData→mintSession /api/session gate) are built, CI-verified (27/27), and LIVE on workers.dev — webhook secret-token auth and session gate 401/405 discipline confirmed against the deployed Worker; the real-client walkthrough (checks a–e) remains the phase's final human gate**
|
||
|
||
## Performance
|
||
|
||
- **Duration:** ~12 min (tasks 1–2) + deploy chain session
|
||
- **Started:** 2026-10-22T03:46Z
|
||
- **Completed:** 2026-10-22 (deploy chain through checkpoint)
|
||
- **Tasks:** 3 of 3 — task 3 deploy chain complete; final real-client walkthrough pending user verification
|
||
- **Files created:** 9; **modified:** 2 (+ KV namespace provisioned)
|
||
|
||
## Accomplishments
|
||
|
||
- **task 1 (`b4393fb`)** — command surface with aliases:
|
||
- `src/i18n/strings.ts` is the copy's tokens.css: en + ms with **identical key-sets** (CS1), clinical Rams-tone `/start` intro and `/privacy` stored/never-stored copy whose wording names exactly the D-08 claim fields (user id, chat id, language) and the 15-minute TTL (PRIV-01, matches `src/kv/sessions.ts` reads)
|
||
- Handlers send **plain text with link previews disabled and nothing else** — D-02 is pinned as a source grep gate (CS4: zero `keyboard`-shaped tokens in bot.ts + commands)
|
||
- `pickLocale`: `ms`/`ms-…` → ms; everything else (null, unknown, `id`) → en (CS2 matrix)
|
||
- `scripts/register-commands.mjs` registers exactly two `setMyCommands` calls — default scope + `language_code:"ms"` scope — descriptions **derived from `helpMenu` lines**, no inline copy, no future-command stubs (D-03/D-05); gap-freedom asserted by CS5
|
||
- Command stack registers exactly {start, mula, help, privacy, privasi} (CS6, D-04)
|
||
- **task 2 (`df3b7d6`)** — Worker entry + deploy plumbing:
|
||
- `src/webhook.ts` routes exactly `/telegram/webhook` (grammY `webhookCallback(bot, "cloudflare-mod", undefined, undefined, env.WEBHOOK_SECRET)` — constant-time secret-token header compare rejects garbage before bot processing or any KV touch, T-02-03) and `POST /api/session` (`validateInitData` → 401 `{error:<category>}` / `mintSession` one-time opaque token; single mint path, no extra branch — T-02-01/02/06)
|
||
- D-14: the only console statement in the Worker entry is the category-only `[initdata] rejected: <reason>` line
|
||
- `scripts/set-webhook.mjs` posts `setWebhook { url, secret_token, allowed_updates:["message"] }`; secrets read from gitignored `.dev.vars`; token never echoed (T-02-04)
|
||
- `npm run deploy -w bot` now chains `wrangler deploy && node scripts/set-webhook.mjs`; `wrangler.jsonc` marks `workers_dev: true` as the webhook URL target; KV id intentionally stays a placeholder for the checkpoint's `wrangler kv namespace create`
|
||
- **Verification:** `npm test -w bot` → `[runner] 27 passed, 0 failed` (all prior suites still green: initdata T1–T7, sessions, privacy PG1/PG2); `npm run bot:typecheck` exit 0; both scripts pass `node --check`; `wrangler deploy --dry-run --outdir /tmp/opencode/worker-dryrun` exit 0 bundling in 167.97 KiB with the chat_sessions binding
|
||
|
||
## task Commits
|
||
|
||
1. **task 1: strings module + command surface with aliases (D-01..D-05)** — `b4393fb` (feat)
|
||
2. **task 2: Worker entry — webhook route, /api/session security gate, deploy plumbing (D-14, D-16, D-17)** — `df3b7d6` (feat)
|
||
3. **task 3: deploy gate (checkpoint:human-verify)** — deploy chain executed after user provisioned credentials:
|
||
- KV namespace created + id wired: `492a97b` (fix)
|
||
- Worker deployed live; secrets set; setWebhook + setMyCommands registered — no code commits required (see Deployment Record)
|
||
|
||
## Files Created/Modified
|
||
|
||
- Created: `bot/src/i18n/strings.ts`, `bot/src/bot/bot.ts`, `bot/src/bot/commands/{start,help,privacy}.ts`, `bot/src/webhook.ts`, `bot/scripts/{register-commands,set-webhook}.mjs`, `bot/test/commands.test.mjs`
|
||
- Modified: `bot/package.json` (deploy chain), `bot/wrangler.jsonc` (workers_dev + real KV namespace id, `492a97b`)
|
||
|
||
## Deployment Record (task 3, D-16)
|
||
|
||
- **Worker URL (public, not a secret):** `https://araharah-bot.araharah-bot.workers.dev`
|
||
- `araharah-bot` workers.dev subdomain was already registered on the account; DNS went live within minutes of first deploy
|
||
- Account: adri.mcmc@gmail.com, account id `7b4510d8…51e9`
|
||
- **KV namespace:** `chat_sessions` → id `59f58a54bc44483e89872b20a005a5eb` — created via `wrangler kv namespace create`, id committed in `wrangler.jsonc` (`492a97b`; the id is account-scoped config, not a secret)
|
||
- **Runtime secrets:** `BOT_TOKEN` + `WEBHOOK_SECRET` uploaded via `wrangler secret put` (set-once; `.dev.vars` is local-only for `wrangler dev` — values never printed, never committed)
|
||
- **setWebhook:** ok:true → delivered updates restricted to `allowed_updates:["message"]`, secret-token header armed (T-02-03). Verified read-back via getWebhookInfo: url/secret-header/pending=0/no errors
|
||
- **setMyCommands:** default scope + `language_code:"ms"` scope both ok:true; read-back via getMyCommands shows exactly /help + /privacy with derived descriptions in both languages (D-03/D-05)
|
||
- **Live security smokes (automated against the deployed Worker):**
|
||
- webhook without secret header → 401 `"unauthorized"`
|
||
- webhook with wrong secret header → 401 `"unauthorized"`
|
||
- POST /api/session garbage body → 401 `{"error":"malformed"}` (category-only, T-02-01/D-14)
|
||
- GET /api/session → 405; unknown route → 404 "Not found"
|
||
- KV key list pre-/start → `[]` (PRIV-02 baseline)
|
||
|
||
## Awaiting: real-client walkthrough (final gate for ONBD-01/PRIV-01/PRIV-02 + SC1–SC4)
|
||
|
||
Human checks in Telegram against the bot (menu button → bot name: commands a–e as planned). KV spot-check command after a real /start:
|
||
`cd bot && npx wrangler kv key list --namespace-id 59f58a54bc44483e89872b20a005a5eb` — only `session:*`/`chat:*`-shaped keys, gone after ~15 min.
|
||
|
||
## Decisions Made
|
||
|
||
- **Menu descriptions derive from helpMenu:** registration imports `src/i18n/strings.ts` and slices the `/cmd — text` lines, making the persisted menu and the /help reply one artifact (D-03 gap-freedom enforced by construction + test)
|
||
- **pickLocale prefix semantics:** exact `ms` / `ms-` prefix only — an unrelated code can never enter the ms surface
|
||
- **/api/session is branch-free:** replay closing is entirely plan-02's rotate-on-new invariant; the endpoint introduces zero extra KV writes so the PG1/PG2 privacy gates hold automatically
|
||
- **Per-request Bot only on the webhook path:** the token cannot resolve at module scope (Workers env), and /api/session has no need for a Bot instance
|
||
|
||
## Deviations from Plan
|
||
|
||
**Fix applied during task 3 deploy chain ([Rule 1 - Bug]: production secrets not deployed):**
|
||
- **Found during:** task 3 live smoke test — first study: burning bright no-secret webhook probe returned 500 "Worker threw exception"; `wrangler tail` showed `Error: Empty token!` at `new Bot` — `.dev.vars` is local-only, so `env.BOT_TOKEN`/`env.WEBHOOK_SECRET` were undefined in production
|
||
- **Fix:** uploaded both runtime secrets via `wrangler secret put` (set-once, values from `.dev.vars`, never echoed); re-ran smokes → all green. **Noted for future deploys:** `npm run bot:deploy` deploys code + sets the webhook, but does NOT re-upload secrets — set-once is expected behavior; recreate only if the secret is rotated. Phase 3+ deploy docs should carry this note.
|
||
- **Files modified:** none (environment-only fix); Worker redeploy not needed
|
||
- **Commit:** n/a
|
||
|
||
Also: a first `wrangler secret put` attempt ran from the wrong working directory and uploaded empty values; corrected by re-uploading from `bot/` with full values and verifying via `wrangler secret list`.
|
||
|
||
(One internal test-path fix in `commands.test.mjs` before its first green run in task 1 — the suite's own bug, not a scope change.) ` ExecutionContext` parameter in `webhook.ts` is accepted-but-unused per the standard Workers fetch signature.
|
||
|
||
## Threat Model Dispositions Applied
|
||
|
||
- **T-02-03** (webhook flooding/DoS): secret-token header constant-time compare happens in grammY's adapter before bot processing; no KV touched pre-check
|
||
- **T-02-01** (spoofed identity → token): only the validateInitData→mintSession path mints; rejections return category-only 401s
|
||
- **T-02-02** (stale-but-valid replay): 900 s freshness window + rotate-on-new deletion in mintSession
|
||
- **T-02-04** (information disclosure): D-14 category-only logging; scripts print only ok/result/error_description — no token or secret values (grep-verified)
|
||
- **T-02-06** (KV persistence drift): endpoint adds no storage writes; PG1/PG2 gates automatically cover `src/webhook.ts` (no `.put(` present)
|
||
|
||
## Known Stubs
|
||
|
||
None. Every export is fully wired; no placeholder copy, no TODO/FIXME, no components without data sources. `KV_ID_PLACEHOLDER` in `wrangler.jsonc` is a deliberate deploy-checkpoint input (documented in plan task 3), not a code stub.
|
||
|
||
## User Setup Required (deploy checkpoint — plan task 3)
|
||
|
||
RESOLVED — credentials were provisioned by the user (@BotFather token + WEBHOOK_SECRET in `.dev.vars`, `wrangler login` active). Deploy chain complete — see "Deployment Record". Remaining: the real-client walkthrough a–e (above).
|
||
|
||
Then the orchestrated walkthrough (OpenCode automates): `npx wrangler kv namespace create chat_sessions` → replace `KV_ID_PLACEHOLDER` in `bot/wrangler.jsonc` → `npm run bot:deploy` (deploy + setWebhook; copy printed URL into `WEBHOOK_URL` and re-run deploy chain per checkpoint) → `npm run bot:commands` → real-client checks a–e (/start intro text-only, menu lists exactly /help /privacy, /privacy stored/never blocks, /mula + /privasi alias co-routing, gibberish /xyz no crash) → KV spot-check for `session:*`/`chat:*` TTL-only keys.
|
||
|
||
## Next Phase Readiness
|
||
|
||
- Phase 3's Worker inherits: mixed static-assets + existing routes are additive (`/telegram/webhook` and `/api/session` keep their paths); the chat_sessions binding and TTL conventions are already enforced by the green PG1/PG2 gates
|
||
- `/api/session` is live the moment the Worker deploys — the Phase 4 Mini App caller only needs to POST its verbatim `Telegram.WebApp.initData`
|
||
- Command-surface gates (CS1–CS6) guard all future copy/menu edits against drift from the strings module
|
||
|
||
## Self-Check: PASSED
|
||
|
||
- All 9 created files verified present on disk
|
||
- Commits verified in history: `b4393fb` (task 1), `df3b7d6` (task 2); no unintended file deletions in either commit
|
||
- `npm test -w bot` → `[runner] 27 passed, 0 failed`; `npm run bot:typecheck` exit 0
|
||
- `wrangler deploy --dry-run` exit 0; `node --check` clean on both scripts
|
||
- **task 3:** commits `492a97b` (KV id) verified in history; deployed Worker URL reachable (404 fallback), secrets in `wrangler secret list`, getWebhookInfo ok, getMyCommands read-back ok (both scopes), live 401/401/401/405/404 smokes green, KV list baseline `[]`
|
||
- STATE.md / ROADMAP.md untouched (orchestrator-owned)
|
||
|
||
---
|
||
*Phase: 02-bot-skeleton-initdata-security*
|
||
*Completed: 2026-10-22 (tasks 1–3; task 3 deploy chain done, real-client walkthrough pending user)*
|