Files
2026-10-23 22:37:55 +08:00

136 lines
12 KiB
Markdown
Raw Permalink 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
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._