--- 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. @$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md @$HOME/.config/opencode/get-shit-done/templates/summary.md @.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): ```typescript 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`): ```javascript 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: `), not a crash - Prints `PASS`/`FAIL ` 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. 2. **bot/package.json (new):** ```json { "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. 3. **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): ```json { "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]. 4. **bot/wrangler.jsonc (config, not secrets — D-17):** ```jsonc { // 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" }] } ``` 5. **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). 6. **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.) 7. **bot/src/config.ts (interface contract only — no implementation beyond types):** the `Env` interface exactly as in `` above (D-10: grammY + workers-types first-class). 8. 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 - `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) 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). 2. **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 `; throw → print `FAIL ` + `error.message`/stack, keep going - End with /\n summary `\n[runner] N passed, M failed\n` and `process.exitCode = M > 0 ? 1 : 0` 3. **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=$?" - `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) 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. ## 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 | - `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 - 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 After completion, create `.planning/phases/02-bot-skeleton-initdata-security/02-01-SUMMARY.md`