16 KiB
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 totheme/, 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""
- "D-09/D-10/D-11: backend lives in a
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.mjsFrom 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 + summaryN passed, M failed; exit 0 only when M = 0
-
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.
-
bot/tsconfig.json: strict, Workers-native, noEmit (wrangler/esbuild does the real build;
allowImportingTsExtensionspairs 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
.tsnatively under this constraint [VERIFIED in RESEARCH.md]. -
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" }] } -
bot/.gitignore: exactly
.dev.varsand.wrangler/(D-15/D-17 — secret hygiene from the first commit; Phase 1 precedent for self-hosted assets mirrors backend secrets). -
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.mjsin plan 03 reads it.) -
bot/src/config.ts (interface contract only — no implementation beyond types): the
Envinterface exactly as in<interfaces>above (D-10: grammY + workers-types first-class). -
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:typecheckexits 0 (src/config.ts type-checks against @cloudflare/workers-types)bot/src/config.tscontainsexport interface Envwith membersBOT_TOKEN,WEBHOOK_SECRET,chat_sessions: KVNamespace- root package.json contains
"workspaces": ["theme", "bot"](order irrelevant) and script keysbot:test,bot:deploy bot/package.jsoncontains"name": "@araharah/bot"and dependencygrammyin the ^1.46.0 rangebot/wrangler.jsonccontains"main": "src/webhook.ts"and bindingchat_sessionsbot/.gitignorecontains both.dev.varsand.wrangler/bot/.dev.vars.examplecontainsBOT_TOKEN=andWEBHOOK_SECRET=and no actual token valuegit check-ignore bot/.dev.varsexits 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).
- bot/test/runner.mjs (entry; D-12 shape from audit.test.mjs):
import { readdirSync } from "node:fs"+pathfromnode:path; resolve suites asreaddirSync(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 → printFAIL <name>+error.message/stack, keep going - End with /\n summary
\n[runner] N passed, M failed\nandprocess.exitCode = M > 0 ? 1 : 0
- bot/test/initdata.test.mjs: implement T1–T7 above as async cases via
import { test } from "./harness.mjs"andimport { 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 usingnode: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 botprints at least 7 FAIL lines matching the T1–T7 contract and exits 1 (documented RED)- The runner summary line matches
N passed, M failedwith 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.mjswith zero suite failures (temporarily verify bycd bot && node test/runner.mjsafter hiding suites) prints[runner] 0 passed, 0 failedand exits 0npm run bot:typecheckremains 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> |
<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.varsignored from the first commit </success_criteria>