diff --git a/bot/src/security/initdata.ts b/bot/src/security/initdata.ts index de2efc1..b01b750 100644 --- a/bot/src/security/initdata.ts +++ b/bot/src/security/initdata.ts @@ -56,15 +56,6 @@ export interface WebAppInitChat { photo_url?: string; } -/** Shape of a fully parsed initData payload (hash/signature excluded from - * rawFields — they are auth metadata, not payload fields). */ -export interface ParsedInitData { - user: WebAppInitUser; - chat?: WebAppInitChat; - authDate: number; - rawFields: Map; -} - export interface ValidateInitDataOptions { /** "Now" in seconds-since-epoch — injectable for deterministic tests. */ nowSeconds?: number; @@ -95,8 +86,9 @@ function parseFields(raw: string): Map { try { fields.set(key, decodeURIComponent(value)); } catch { - // Undecodable percent-escape — keep raw so HMAC compare fails ("tampered") - // rather than silently dropping the field. + // Intent: an undecodable percent-escape keeps the RAW value so the HMAC + // compare fails ("tampered") rather than silently dropping the field — + // malformed-vs-tampered precedence (D-14). fields.set(key, value); } } @@ -160,6 +152,8 @@ export async function validateInitData( user = parsed as WebAppInitUser; } } catch { + // Intent: a malformed user payload is the "malformed" class, not a + // tampered one — the field never reaches the HMAC path. user = undefined; } } @@ -209,6 +203,8 @@ export async function validateInitData( chat = parsedChat as WebAppInitChat; } } catch { + // Intent: a malformed chat payload degrades to "no chat context" (a + // private chat) — it never fails an otherwise-valid private-chat payload. chat = undefined; } } diff --git a/bot/test/initdata.test.mjs b/bot/test/initdata.test.mjs index 728022c..4536b6a 100644 --- a/bot/test/initdata.test.mjs +++ b/bot/test/initdata.test.mjs @@ -188,6 +188,14 @@ test("T4 freshness boundary: age 901 s → reason stale", async () => { assert.equal(res.reason, "stale"); }); +test("T4 freshness boundary: age 900 s → ok:true (the boundary is INCLUSIVE)", async () => { + const validateInitData = await loadValidator(); + const now = Math.floor(Date.now() / 1000); + const raw = signedUser(now - 900); + const res = await validateInitData(raw, TEST_TOKEN); + assert.equal(res.ok, true, `expected ok:true at exactly 900 s, got ${JSON.stringify(res)}`); +}); + /* ————— T5 — field order independence (RESEARCH Pitfall 1) ————— */ test("T5 fields arriving out of alphabetical order still validate", async () => { diff --git a/bot/wrangler.jsonc b/bot/wrangler.jsonc index 00c7f76..3737bcd 100644 --- a/bot/wrangler.jsonc +++ b/bot/wrangler.jsonc @@ -1,5 +1,5 @@ { - // KV namespace id is filled in plan 03 task 1 after `npx wrangler kv namespace create chat_sessions` + // chat_sessions namespace created via `npx wrangler kv namespace create chat_sessions` — the id below is live. "name": "araharah-bot", "main": "src/webhook.ts", "compatibility_date": "2026-10-22",