Files
Arah2/.planning/phases/02-bot-skeleton-initdata-security/02-03-SUMMARY.md
T
araharah-executor 2ea7f6edc7 docs(02-03): finalize SUMMARY — deploy record, live security smokes, walkthrough gate pending
- 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)
2026-10-22 18:53:55 +08:00

173 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)*