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

15 KiB
Raw Blame History


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: '"

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)