Files
Arah2/.planning/phases/02-bot-skeleton-initdata-security/02-01-PLAN.md
T
2026-10-23 22:37:55 +08:00

16 KiB
Raw Blame History


phase: 02-bot-skeleton-initdata-security plan: 01 type: execute wave: 1 depends_on: [] files_modified:

  • package.json
  • bot/package.json
  • bot/tsconfig.json
  • bot/wrangler.jsonc
  • bot/.gitignore
  • bot/.dev.vars.example
  • bot/src/config.ts
  • bot/test/harness.mjs
  • bot/test/runner.mjs
  • bot/test/initdata.test.mjs autonomous: true requirements: [ONBD-01, PRIV-01, PRIV-02] must_haves: truths:
    • "D-09/D-10/D-11: backend lives in a bot/ TypeScript workspace sibling to theme/, with the explicit src/ tree contracted via bot/src/config.ts"
    • "D-12: npm test -w bot runs a zero-dependency hand-rolled harness (audit.test.mjs shape) reporting PASS/FAIL/exit codes — substrate for all phase REQ verification"
    • "D-15/D-17: the initdata vector suite RED-demonstrates the security-gate contract BEFORE implementation exists (TDD proof for RESEARCH Pitfalls 1–3), with no secret value ever in the repo"
    • "D-11/D-15 contracts reach later plans: Env interface + wrangler entry points exist before implementation" artifacts:
    • path: "bot/package.json" provides: "Workspace scripts/deps (grammy ^1.46.0, wrangler ^4.151.3, TS ^5.9.3)" contains: "@araharah/bot"
    • path: "bot/src/config.ts" provides: "Env contract consumed by plans 02/03" contains: "export interface Env"
    • path: "bot/test/runner.mjs" provides: "D-12 hand-rolled runner, auto-discovers *.test.mjs, suite-load failures become FAILs"
    • path: "bot/test/initdata.test.mjs" provides: "Vector suite T1–T7 (known-good/tamper/stale/boundary/order/malformed/RFC-4231 pin)"
    • path: "bot/wrangler.jsonc" provides: "Worker config: main src/webhook.ts + chat_sessions KV binding" contains: "chat_sessions" key_links:
    • from: "bot/test/initdata.test.mjs" to: "bot/src/security/initdata.ts" via: "import "../src/security/initdata.ts" (RED until plan 02)" pattern: "initdata\.ts"
    • from: "bot/wrangler.jsonc" to: "src/webhook.ts" via: "main entry binding" pattern: ""main": "src/webhook.ts""

Stand up the `bot/` npm workspace (D-09/D-10/D-11) with the hand-rolled test harness (D-12) and a RED initdata vector suite — the Wave-0 substrate every later verification run (`npm test -w bot`) executes on.

Purpose: Plans 02 and 03 fill in the security core and command surface against this scaffold; the initdata suite written here goes RED now and goes GREEN in plan 02 (TDD proof of the phase's security exit criterion). Output: Installable workspace, type-checkable src/config.ts contract, runner + RED vector suite.

<execution_context> @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-bot-skeleton-initdata-security/02-CONTEXT.md @.planning/phases/02-bot-skeleton-initdata-security/02-RESEARCH.md @.planning/phases/02-bot-skeleton-initdata-security/02-VALIDATION.md @theme/package.json @theme/scripts/audit.test.mjs

From bot/src/config.ts (created here, consumed by plans 02/03):

export interface Env {
  BOT_TOKEN: string;        // wrangler secret (D-15) — Telegram Bot API token from @BotFather
  WEBHOOK_SECRET: string;   // wrangler secret (D-15) — value Telegram echoes in X-Telegram-Bot-Api-Secret-Token
  chat_sessions: KVNamespace; // TTL-only namespace — first durable KV namespace (PRIV-02)
}

Test harness API this plan creates (from bot/test/harness.mjs, consumed by every later *.test.mjs):

export function test(name, fn) // register one async case; fn() throws → FAIL

Runner contract (from bot/test/runner.mjs):

  • Scans bot/test/*.test.mjs, dynamic-imports each; a file that fails to import is recorded as one failing case (suite load failed: <file>), not a crash
  • Prints PASS/FAIL <name> per case + summary N passed, M failed; exit 0 only when M = 0
task 1: bot workspace scaffold with Env contract (D-09, D-10, D-11, D-15) package.json, bot/package.json, bot/tsconfig.json, bot/wrangler.jsonc, bot/.gitignore, bot/.dev.vars.example, bot/src/config.ts - ./package.json (current root — has workspaces ["theme"], delegate scripts pattern to extend) - ./theme/package.json (Phase 1 workspace precedent: scripts/deps shape) - ./theme/scripts/audit.test.mjs (runner shape D-12 builds on — read the header + structure) - .planning/phases/02-bot-skeleton-initdata-security/02-CONTEXT.md (D-09/D-10/D-11/D-15 exact wording) - .planning/phases/02-bot-skeleton-initdata-security/02-RESEARCH.md (Standard Stack versions + Recommended Project Structure) 1. **Root package.json (edit, don't replace):** workspaces becomes `["theme", "bot"]` (D-09). Add delegate scripts mirroring the Phase 1 pattern: ```json "bot:test": "npm test -w bot", "bot:typecheck": "npm run typecheck -w bot", "bot:dev": "npm run dev -w bot", "bot:deploy": "npm run deploy -w bot", "bot:commands": "npm run commands -w bot" ``` Keep existing theme scripts untouched.
  1. bot/package.json (new):

    {
      "name": "@araharah/bot",
      "private": true,
      "type": "module",
      "scripts": {
        "test": "node test/runner.mjs",
        "typecheck": "tsc --noEmit",
        "dev": "wrangler dev",
        "deploy": "wrangler deploy",
        "commands": "node scripts/register-commands.mjs"
      },
      "dependencies": { "grammy": "^1.46.0" },
      "devDependencies": {
        "wrangler": "^4.151.3",
        "typescript": "^5.9.3",
        "@cloudflare/workers-types": "^5.20261022.1",
        "@types/node": "^26.6.5"
      }
    }
    

    Versions are registry-verified in RESEARCH.md — use exactly these ranges.

  2. bot/tsconfig.json: strict, Workers-native, noEmit (wrangler/esbuild does the real build; allowImportingTsExtensions pairs with noEmit and matches Node 24 type-stripping for the scripts/ D-12 runner):

    {
      "compilerOptions": {
        "target": "ES2023",
        "lib": ["ES2023"],
        "module": "ESNext",
        "moduleResolution": "bundler",
        "types": ["@cloudflare/workers-types"],
        "strict": true,
        "noUncheckedIndexedAccess": true,
        "verbatimModuleSyntax": true,
        "allowImportingTsExtensions": true,
        "noEmit": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true
      },
      "include": ["src"]
    }
    

    Constraint for all bot/ TS written in this phase: erasable-only TypeScript (no enums, no parameter properties, no namespaces — only type stripping survives). Node 24 runs .ts natively under this constraint [VERIFIED in RESEARCH.md].

  3. bot/wrangler.jsonc (config, not secrets — D-17):

    {
      // KV namespace id is filled in plan 03 task 1 after `npx wrangler kv namespace create chat_sessions`
      "name": "araharah-bot",
      "main": "src/webhook.ts",
      "compatibility_date": "2026-10-22",
      "kv_namespaces": [{ "binding": "chat_sessions", "id": "KV_ID_PLACEHOLDER" }]
    }
    
  4. bot/.gitignore: exactly .dev.vars and .wrangler/ (D-15/D-17 — secret hygiene from the first commit; Phase 1 precedent for self-hosted assets mirrors backend secrets).

  5. bot/.dev.vars.example (shape only, no real values):

    # Copy to .dev.vars and fill for local wrangler dev (D-15). NEVER commit .dev.vars.
    BOT_TOKEN=
    WEBHOOK_SECRET=
    WEBHOOK_URL=
    

    (WEBHOOK_URL is set once after the first deploy; scripts/set-webhook.mjs in plan 03 reads it.)

  6. bot/src/config.ts (interface contract only — no implementation beyond types): the Env interface exactly as in <interfaces> above (D-10: grammY + workers-types first-class).

  7. Run npm i -w bot (creates/updates root package-lock.json; verify grammy@1.46.x and wrangler@4.15x land under node_modules/bot's tree). npm i -w bot && npm run bot:typecheck; test -f bot/src/config.ts && test -f bot/.dev.vars.example && test -f bot/.gitignore && git check-ignore bot/.dev.vars && echo SCAFFOLD_OK <acceptance_criteria>

  • npm run bot:typecheck exits 0 (src/config.ts type-checks against @cloudflare/workers-types)
  • bot/src/config.ts contains export interface Env with members BOT_TOKEN, WEBHOOK_SECRET, chat_sessions: KVNamespace
  • root package.json contains "workspaces": ["theme", "bot"] (order irrelevant) and script keys bot:test, bot:deploy
  • bot/package.json contains "name": "@araharah/bot" and dependency grammy in the ^1.46.0 range
  • bot/wrangler.jsonc contains "main": "src/webhook.ts" and binding chat_sessions
  • bot/.gitignore contains both .dev.vars and .wrangler/
  • bot/.dev.vars.example contains BOT_TOKEN= and WEBHOOK_SECRET= and no actual token value
  • git check-ignore bot/.dev.vars exits 0 (secret mirror is actually ignored) </acceptance_criteria> bot workspace installs, typechecks clean, wrangler entry points and secret-mirror files exist; nothing secret-value is committed (D-15/D-17).
task 2: Wave 0 harness + RED initdata vector suite (D-12, TDD RED phase) bot/test/harness.mjs, bot/test/runner.mjs, bot/test/initdata.test.mjs - ./theme/scripts/audit.test.mjs (the D-12 "same shape" precedent — node:assert, exit codes, /tmp/opencode-style out-of-repo fixtures) - {bot/.dev.vars.example path too} not needed here; read .planning/phases/02-bot-skeleton-initdata-security/02-RESEARCH.md § "Validation Architecture" and § Code Examples (vector contract) All cases begin failing (RED) because `../src/security/initdata.ts` does not exist yet — the runner must report the failed suite load as FAIL cases, not a crash. Plan 02 task 1 makes them GREEN. Vector contract (RFC-4231 cross-pinned in-suite): - T1 known-good: initData built with test token `12345:TESTTOKEN`, fields `user={"id":42,"first_name":"Zul","language_code":"ms"}`, `auth_date=`, `hash=` → validateInitData resolves `{ ok: true }` and exposes `user.id === 42`, `languageCode === "ms"` - T2 tampered: any char of `hash` flipped → `{ ok:false, reason:"tampered" }` - T3 stale: `auth_date` = now − 3600 with default 900s window → `reason:"stale"` (D-13) - T4 freshness boundary: age 899s → ok; age 901s → stale - T5 order-independence: fields inserted out of alphabetical order (`user` before `auth_date`) still validate (RESEARCH Pitfall 1) - T6 malformed: missing `hash` field → `reason:"malformed"`; `auth_date` non-numeric → `reason:"malformed"`; `user` JSON missing `id` → `reason:"malformed"` - T7 suite self-check: `node:crypto` HMAC(key="key", data="data") hex equals `5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0` (independence pin — src under test uses SubtleCrypto, so primitive cross-check is real) 1. **bot/test/harness.mjs** (the registry): ```javascript const tests = []; export function test(name, fn) { tests.push({ name, fn }); } export const registry = tests; ``` Zero dependencies (D-12).
  1. bot/test/runner.mjs (entry; D-12 shape from audit.test.mjs):
    • import { readdirSync } from "node:fs" + path from node:path; resolve suites as readdirSync(import.meta.dirname).filter(f => f.endsWith(".test.mjs")).sort()
    • For each suite: await import(./${file}) inside try/catch. On load error, push { name: "suite load failed: " + file, fn: () => { throw loadErr; } } into the registry (RED survives, never crashes the run)
    • Run all registered cases in order: pass → print PASS <name>; throw → print FAIL <name> + error.message/stack, keep going
    • End with /\n summary \n[runner] N passed, M failed\n and process.exitCode = M > 0 ? 1 : 0
  2. bot/test/initdata.test.mjs: implement T1–T7 above as async cases via import { test } from "./harness.mjs" and import { validateInitData } from "../src/security/initdata.ts" (missing import = the RED state). The helper that builds a signed initData string must live IN the test file using node:crypto (createHmac twice: secret = HMAC(key="WebAppData", data=botToken); hash = HMAC(key=secretHexBytes, data=dataCheckString)) — independent of the SubtleCrypto path under test (T7 pins both primitives). Fixtures stay in-process; nothing written into the repo. npm run bot:typecheck && npm test -w bot; echo "exit=$?" <acceptance_criteria>
  • npm test -w bot prints at least 7 FAIL lines matching the T1–T7 contract and exits 1 (documented RED)
  • The runner summary line matches N passed, M failed with M > 0
  • A suite-load failure prints as FAIL suite load failed: initdata.test.mjs (or per-case FAILs) — the runner exits with code 1 after printing all summaries, never crashes mid-run
  • node test/runner.mjs with zero suite failures (temporarily verify by cd bot && node test/runner.mjs after hiding suites) prints [runner] 0 passed, 0 failed and exits 0
  • npm run bot:typecheck remains exit 0 (tests are .mjs, outside tsc include — intentional; they run via Node type-stripping) </acceptance_criteria> Runner + harness exist with zero dependencies; the initdata vector suite is written to its final contract and demonstrably RED (plan 02 goes GREEN against it); D-12 replicated in a second workspace.

<threat_model>

Trust Boundaries

Boundary Description
repo → git history Secrets and local-env values cross into permanent history on any accidental git add
.dev.vars → local dev runtime Local secrets enter the dev process
npm registry → workspace tree Supply-chain surface when installing grammy/wrangler/typescript

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02-04 Information Disclosure bot/.gitignore, bot/.dev.vars.example mitigate .dev.vars ignored in the SAME commit that introduces the secret mirror (D-15/D-17); verify with git check-ignore in acceptance criteria
T-02-07 Tampering bot/wrangler.jsonc, bot/tsconfig.json mitigate Exact registry-verified versions (^1.46.0, ^4.151.3, ^5.9.3 …) and lockfile from npm i -w bot; dry-run + typecheck gates in later plans catch env/shape drift
T-02-08 DoS (supply chain) bot/package.json deps accept Only 1 runtime dep (grammy) for a single-person project; versions pinned; lockfile committed; no paid/obscure packages
</threat_model>
- `npm i` at repo root succeeds with the new bot workspace - `npm run bot:typecheck` exit 0 - `npm test -w bot` runs the full harness and exits 1 with the RED suite visible (this is the expected, documented state at end of this plan — plan 02 flips it) - `git check-ignore bot/.dev.vars` → ignored

<success_criteria>

  • bot/ workspace scaffolded per D-09/D-10/D-11 with confirmed-clean typecheck and a working zero-dep test runner
  • Wave 0 complete per 02-VALIDATION.md: harness + vector suite skeleton exist and FAIL (RED) before any implementation exists
  • No secret values in the repo; .dev.vars ignored from the first commit </success_criteria>
After completion, create `.planning/phases/02-bot-skeleton-initdata-security/02-01-SUMMARY.md`