forked from c-fu/arah2
136 lines
12 KiB
Markdown
136 lines
12 KiB
Markdown
---
|
||
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._
|