Files
arah2/.planning/phases/02-bot-skeleton-initdata-security/02-03-SUMMARY.md
T
araharah-executor c8f064f65a docs(02-03): complete command surface + Worker wiring plan (deploy checkpoint pending)
- SUMMARY for tasks 1-2 (b4393fb, df3b7d6): strings module + text-only
  command surface, Worker entry with webhook auth + /api/session gate,
  registration/deploy scripts, all suites green (27/27, dry-run exit 0)
- task 3 deploy gate returned to orchestrator: BOT_TOKEN + Cloudflare
  auth + KV namespace id required before the real-client walkthrough
- STATE.md / ROADMAP.md untouched (orchestrator-owned)
2026-10-22 11:50:53 +08:00

147 lines
12 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 and CI-verified — 27/27 tests, typecheck clean, wrangler dry-run exit 0; deploy (BOT_TOKEN + Cloudflare auth + KV id) now waits at the phase's D-16 human gate**
## Performance
- **Duration:** ~12 min
- **Started:** 2026-10-22T03:46Z
- **Completed:** 2026-10-22T04:0xZ
- **Tasks:** 2 of 3 complete (task 3 = blocking deploy checkpoint, returned to orchestrator/user)
- **Files created:** 9; **modified:** 2
## 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)** — **NOT EXECUTED — checkpoint returned**, no credentials present (`bot/.dev.vars` does not exist; no Cloudflare auth). See "User Setup Required".
## 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)
## 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
None - plan executed exactly as written. (One internal test-path fix in `commands.test.mjs` before its first green run — 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)
The phase's D-16 human gate is pending. Prepare, in order:
1. **@BotFather** → `/newbot` → copy the token
2. **Generate a webhook secret:** `openssl rand -hex 32`
3. **Create `bot/.dev.vars`** (gitignored, shape in `bot/.dev.vars.example`):
`BOT_TOKEN=<token>`, `WEBHOOK_SECRET=<hex>`, `WEBHOOK_URL=<workers.dev URL, set after deploy>`
4. **Authenticate Cloudflare:** `npx wrangler login` (browser flow) or `CLOUDFLARE_API_TOKEN`
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
- STATE.md / ROADMAP.md untouched (orchestrator-owned)
---
*Phase: 02-bot-skeleton-initdata-security*
*Completed: 2026-10-22 (tasks 1–2; task 3 checkpoint pending user credentials)*