--- phase: 02-bot-skeleton-initdata-security verified: 2026-10-22T21:05:00Z status: passed score: 4/4 must-haves verified overrides_applied: 0 re_verification: previous_status: none previous_score: n/a gaps_closed: [] gaps_remaining: [] regressions: [] --- # Phase 2: Bot Skeleton + initData Security Verification Report **Phase Goal:** Users can reach araharah through Telegram with a working command surface, and the backend's identity + privacy architecture is provably sound from the first deploy — the initData validator is the first backend artifact. **Verified:** 2026-10-22T21:05Z **Status:** passed **Re-verification:** No — initial verification (codebase + live deployment) ## Goal Achievement ### Observable Truths | # | Truth (ROADMAP SC) | Status | Evidence | |---|--------------------|--------|----------| | 1 | SC1/ONBD-01: /start gives a concise explanation plus an always-available command menu (/help, /privacy) that answers correctly | ✓ VERIFIED | Handlers `bot.command("start"/"mula", …)` reply `strings.startIntro` (one clinical sentence, text-only, no keyboard — CS4 grep gate green); live Telegram read-backs: `getMyCommands` (default) and `getMyCommands?language_code=ms` both return **exactly** `help` + `privacy`, `getChatMenuButton` → `commands`; user real-client walkthrough a–e all green and APPROVED (`02-03-APPROVAL.md`) | | 2 | SC2/PRIV-01: /privacy gives an honest visible statement of what araharah stores / never stores | ✓ VERIFIED | `privacy.ts` builds `privacyStored` + `privacyNotStored` from `strings.ts`; copy names exactly user id, chat id, language, ~15-min TTL — matching `sessions.ts` `SessionClaim {user_id, chat_id, issued_at, language_code}` and `SESSION_TTL_SECONDS=900`; no 5th claim field in code; user observed block as written (approval check c) | | 3 | SC3: tampered, stale, and replayed initData rejected with CI test vectors; known-good HMAC passes; only validated identities get a one-time token | ✓ VERIFIED | `npm test -w bot` → **34 passed, 0 failed** (T1–T7 security vectors incl. tamper/stale/899/901 boundary/order/malformed + RFC-4231 pin; WK1–WK5; sessions SM1/SM2/SR1–SR3; privacy PG1/PG2). Mint path is *only* after `ok:true` in `webhook.ts` (single `mintSession` call). Replay closes via D-07 rotate-on-new (CONTEXT decision; re-mint cannot yield an extra token — accepted locked decision, WR-03 invariant documented honestly) | | 4 | SC4/PRIV-02: storage spot-check shows TTL-only KV, zero persistent coordinates or profiles | ✓ VERIFIED | Live `wrangler kv key list --namespace-id 59f58a54…` → `[]` (empty = TTL-expired privacy-by-construction); every `kv.put` in `sessions.ts` carries `expirationTtl: SESSION_TTL_SECONDS` on the call line (PG1 grep test green); `wrangler.jsonc` has exactly ONE kv binding (`chat_sessions`), no D1/R2/DO anywhere; claim key-set equals D-08 enforced by SM1 | **Score:** 4/4 truths verified ### Post-Review Hardening (WR-01/02/03) — Live Verification | Check | Status | Evidence | |-------|--------|----------| | Deployed version includes all 3 fix commits | ✓ | Commits `8995498`/`3ea1fdd`/`af691ae` at 12:17–12:21 UTC; HEAD = `af691ae`; latest active deployment `b6ba6e58-37dd-4aa1…` created 12:22:03 UTC (after final commit — `wrangler deployments list`) | | WR-01 live (4 KB body cap) | ✓ | `POST /api/session` with 5 KB body → **413**; tiny garbage → 401 `{"error":"malformed"}` | | WR-02 fail-closed secret guard | ✓ | Code verified at `webhook.ts:50-53` (constant 500 before grammY accept-all path); WK5 test green; not live-testable read-only (would require rotating secrets — timing evidence + 413 smoke confirm post-fix bundle deployed) | | WR-03 honest invariant | ✓ | `sessions.ts` header documents best-effort rotate + reconciliation sweep; SR3 asserts the honest KV-limits outcome; IN-01b dead branch removed | ### Deferred Items (Step 9b filter of review findings) | # | Item | Addressed In | Evidence | |---|------|-------------|----------| | 1 | Stale wrangler.jsonc comment (line 2: "filled in plan 03") contradicting the live id on line 9 (REVIEW IN-02, INFO) | Phase 7 | `02-REVIEW-FIX.md`: "Out of scope (Phase 7 audit): IN-01(a), IN-02, IN-03, IN-04, IN-05" — doc-comment hygiene, zero functional impact | ### Required Artifacts | Artifact | Expected | Status | Details | |----------|----------|--------|---------| | `bot/src/security/initdata.ts` | validateInitData + rejection classes, no grammy dep | ✓ VERIFIED | 215 lines; SubtleCrypto HMAC, constant-time hex compare, "WebAppData" salt; consumed by `webhook.ts` | | `bot/src/kv/sessions.ts` | TTL-only KV layer, rotate-on-new | ✓ VERIFIED | 122 lines; 2 puts, both with `expirationTtl`; read helpers noted as Phase 4 API (IN-01c) | | `bot/src/webhook.ts` | Worker entry: webhook route + /api/session gate | ✓ VERIFIED | Two routes only + 404; WR-01 cap; WR-02 fail-closed; D-14 category-only logs (4 console lines, all category-only) | | `bot/src/i18n/strings.ts` + `bot/src/bot/{bot.ts,commands/*}` | Copy source of truth, en+ms, text-only handlers | ✓ VERIFIED | WIRED: commands import strings; bot.ts registers 5 commands; live menu read-back matches helpMenu | | `bot/src/config.ts` | Env contract | ✓ VERIFIED | BOT_TOKEN/WEBHOOK_SECRET/chat_sessions; REQUIRED comment added in WR-02 fix | | `bot/test/{harness,runner}.mjs` | Zero-dep D-12 harness | ✓ VERIFIED | `test: "node test/runner.mjs"` — ran clean, 34/0, `devDependencies` carry zero test libs | | `bot/test/{initdata,sessions,privacy,commands,webhook}.test.mjs` | Vector suites | ✓ VERIFIED | 34 cases green incl. T1–T7, WK1–WK5 (webhook suite added by review fix `8995498`) | | `bot/scripts/{register-commands,set-webhook}.mjs` | Menu registration + setWebhook w/ secret_token | ✓ VERIFIED | Descriptions derived from helpMenu (no drift); secrets read from `.dev.vars`, never echoed | | `bot/wrangler.jsonc` | main src/webhook.ts + chat_sessions binding (live id) | ✓ VERIFIED | id `59f58a54…` live (matches APPROVAL record + deployment) | | `.dev.vars` hygiene | Gitignored, untracked | ✓ VERIFIED | `git check-ignore` ok; `git ls-files` shows only `.dev.vars.example` | ### Key Link Verification | From | To | Via | Status | Details | |------|----|----|--------|---------| | webhook.ts | initdata.ts | `validateInitData` call → 401/413 on rejection, mint on ok | ✓ WIRED | Lines 99–113; live 401 smoke confirms | | webhook.ts | sessions.ts | `mintSession` only after ok:true | ✓ WIRED | Single mint path; WK1 asserts exactly two 900s KV writes | | webhook.ts | bot.ts / grammy | `webhookCallback(bot,"cloudflare-mod",undefined,undefined,env.WEBHOOK_SECRET)` | ✓ WIRED | Live unsigned/wrong-secret webhook → 401 both | | commands/*.ts | strings.ts | copy imported, no inline copy | ✓ WIRED | start/help/privacy all import `strings`; reply builders pure | | register-commands.mjs | strings.ts | menu descriptions derived from helpMenu | ✓ WIRED | Live getMyCommands text matches strings exactly | | sessions.ts | KVNamespace.put | every put carries expirationTtl | ✓ WIRED | PG1/PG2/SM2 green; live KV list empty post-TTL | ### Data-Flow Trace (Level 4) | Artifact | Data Variable | Source | Produces Real Data | Status | |----------|--------------|--------|-------------------|--------| | webhook.ts handleSession | initData string | HTTP request body | Yes — validated against real bot-token HMAC | ✓ FLOWING | | privacy command | locale/strings | ctx.from.language_code → pickLocale | Yes (live user confirmed) | ✓ FLOWING | ### Behavioral Spot-Checks | Behavior | Command | Result | Status | |----------|---------|--------|--------| | Full test suite | `npm test -w bot` | `[runner] 34 passed, 0 failed`, exit 0 | ✓ PASS | | Typecheck | `npm run bot:typecheck` | exit 0 | ✓ PASS | | Webhook auth (unsigned / wrong header) | live POST /telegram/webhook | 401 / 401 | ✓ PASS | | /api/session garbage | live POST | 401 `{"error":"malformed"}` | ✓ PASS | | /api/session 5 KB body (WR-01) | live POST | 413 | ✓ PASS | | GET /api/session | live | 405 | ✓ PASS | | Unknown route | live | 404 | ✓ PASS | | Command menu (default + ms) | live getMyCommands ×2 | exactly /help + /privacy in both languages | ✓ PASS | | Menu button | live getChatMenuButton | type `commands` | ✓ PASS | | Webhook armed | live getWebhookInfo | url `/telegram/webhook`, pending_update_count 0, `allowed_updates:["message"]` | ✓ PASS | | KV TTL-only spot-check | live `wrangler kv key list` | `[]` (empty = all sessions expired ≤900s) | ✓ PASS | | Deployed = HEAD (post-fix bundle) | `wrangler deployments list` + git log | b6ba6e58 (12:22:03Z) created after af691ae (12:21:11Z); current deployments list shows b6ba6e58 active at 100% | ✓ PASS | ### Requirements Coverage | Requirement | Source Plan | Description | Status | Evidence | |-------------|------------|-------------|--------|----------| | ONBD-01 | 02-01, 02-03 | Open bot, /start gives concise explanation + always-available command menu | ✓ SATISFIED | Handlers + strings verified; live menu lists exactly /help /privacy both scopes; user-approved walkthrough (SC1) | | PRIV-01 | 02-01, 02-03 | Privacy promise visible in-product: /privacy command | ✓ SATISFIED (phase half) | /privacy + /privasi live-observed; noted: "trip ended, data deleted" messaging half completes in Phase 6 per REQUIREMENTS.md:127 | | PRIV-02 | 02-01, 02-02 | No persistent per-user history/profile — TTL-only ephemeral state | ✓ SATISFIED | Live KV `[]`; TTL-only on all write paths (PG1/PG2/SM tests); one binding in wrangler.jsonc; no other storage code in bot/src | All phase requirement IDs (ONBD-01, PRIV-01, PRIV-02) appear in plan frontmatter and are mapped to Phase 2 in REQUIREMENTS.md:97-99 — no orphaned requirements. Roadmap Phase 2 defines exactly 4 success criteria (device-matrix criterion belongs to Phase 3's SC5) — all 4 covered above. ### Anti-Patterns Found | File | Line | Pattern | Severity | Impact | |------|------|---------|----------|--------| | bot/wrangler.jsonc | 2 | Stale "KV_ID_PLACEHOLDER" comment vs live id on line 9 | ℹ️ Info | Doc-only, deferred to Phase 7 (REVIEW IN-02) | | bot/src/security/initdata.ts | 53-60 | `ParsedInitData` exported but unused | ℹ️ Info | Type-only export, deliberate; documented in REVIEW IN-01(a) | | — (everything else) | — | TODO/FIXME/placeholder/console-echo of secrets/empty implementations: full grep of src/, scripts/, test/ returned **zero** | — | — | The `getWebhookInfo` `last_error` timestamp (11:18:20 UTC) predates the final BOT_TOKEN secret re-upload (11:18:25 UTC deployment from the walkthrough remediation) and the user's subsequent live `/start` approval; `pending_update_count` is 0. Known resolved history, not a finding. ### Human Verification Required **None outstanding.** The human-only gates of this phase were already resolved and are recorded as evidence: - **Real-client walkthrough a–e** (D-16 blocking gate): user performed 2026-10-22 and said *"it works! Menu or / has /help and /privacy! approved!"* — `02-03-APPROVAL.md` (cross-checked live via Telegram API read-backs performed during this verification). - Secret provisioning (BotFather token, wrangler secrets): resolved during deploy chain; runtime behavior live-verified by smokes. ### Gaps Summary No gaps. All four roadmap success criteria verified against code, tests, and the live deployment; the code-review's 3 warnings are fixed and the fix bundle is what is live (timing + WR-01 live smoke). The one un-replicable human gate (real Telegram client) is user-approved with recorded evidence. --- _Verified: 2026-10-22T21:05Z_ _Verifier: OpenCode (gsd-verifier)_ _Verification basis: local code at af691ae + live deployment b6ba6e58; all Cloudflare/Telegram state touched read-only._