forked from c-fu/arah2
feat(02-03): Worker entry — webhook route + /api/session security gate + deploy plumbing (D-11, D-14, D-16, D-17)
- src/webhook.ts: routes exactly /telegram/webhook (grammY cloudflare-mod adapter, constant-time secret-token auth against env.WEBHOOK_SECRET) and POST /api/session (validateInitData → 401 category-only rejections → mintSession one-time token; single mint path, no extra branch — T-02-01/02/06) - D-14: the only log statement is '[initdata] rejected: <category>' — no payload, ids, or timestamps - scripts/set-webhook.mjs: setWebhook with secret_token + allowed_updates [message], secrets read from gitignored .dev.vars, token never echoed - bot deploy script chains wrangler deploy + set-webhook; wrangler.jsonc gains workers_dev (webhook URL target); KV id stays placeholder for the deploy checkpoint - verified: 27/27 tests, typecheck clean, wrangler deploy --dry-run exit 0
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* webhook.ts — the Worker entry (D-11, D-14, D-16, D-17).
|
||||
* --------------------------------------------------------
|
||||
* Routes exactly two paths:
|
||||
* POST /telegram/webhook → grammY's webhook adapter (cloudflare-mod) with
|
||||
* the configured secret token — Telegram's updates must carry
|
||||
* X-Telegram-Bot-Api-Secret-Token = env.WEBHOOK_SECRET or they are 401'd
|
||||
* by a constant-time compare BEFORE any bot processing or KV touch
|
||||
* (T-02-03). The bot is instantiated per request because the token only
|
||||
* arrives with `env` (research-verified Workers pattern).
|
||||
* POST /api/session → the phase's security gate (T-02-01): the caller
|
||||
* posts a verbatim Telegram.WebApp.initData string; only an HMAC-validated,
|
||||
* fresh identity obtains a one-time opaque session token (D-06). Rotation
|
||||
* of any prior token for the chat is mintSession's own invariant (D-07) —
|
||||
* this endpoint adds no extra branch and no extra write path (T-02-06).
|
||||
* Anything else → 404.
|
||||
*
|
||||
* D-14: the ONLY log statement is category-only (malformed/tampered/stale) —
|
||||
* never the raw payload, never user ids, never timestamps. Rejection
|
||||
* responses carry the category only.
|
||||
* D-17: no secret is ever logged or echoed; the bot token / webhook secret
|
||||
* exist only inside `env` at runtime.
|
||||
*/
|
||||
import { webhookCallback } from "grammy";
|
||||
import type { Env } from "./config.ts";
|
||||
import { createBot } from "./bot/bot.ts";
|
||||
import { mintSession } from "./kv/sessions.ts";
|
||||
import { validateInitData } from "./security/initdata.ts";
|
||||
|
||||
export default {
|
||||
async fetch(req: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === "/telegram/webhook") {
|
||||
const bot = createBot(env); // env-bound token → per-request instantiation
|
||||
const cb = webhookCallback(bot, "cloudflare-mod", undefined, undefined, env.WEBHOOK_SECRET);
|
||||
return cb(req);
|
||||
}
|
||||
if (url.pathname === "/api/session") {
|
||||
return handleSession(req, env);
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
|
||||
/** Phase-2 security gate: initData in, one-time opaque session token out —
|
||||
* nothing else; unvalidated identities get a category-only 401 (T-02-01). */
|
||||
async function handleSession(req: Request, env: Env): Promise<Response> {
|
||||
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
|
||||
const raw = await req.text(); // the verbatim Telegram.WebApp.initData string
|
||||
const result = await validateInitData(raw, env.BOT_TOKEN);
|
||||
if (!result.ok) {
|
||||
console.log(`[initdata] rejected: ${result.reason}`); // D-14: category ONLY — no payload, no ids, no timestamps
|
||||
return new Response(JSON.stringify({ error: result.reason }), {
|
||||
status: 401,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
const token = await mintSession(env.chat_sessions, {
|
||||
user_id: result.user.id,
|
||||
chat_id: result.chat?.id ?? result.user.id, // private chat: Telegram conflates; claim surface unchanged (D-08)
|
||||
issued_at: Math.floor(Date.now() / 1000),
|
||||
language_code: result.languageCode,
|
||||
});
|
||||
return new Response(JSON.stringify({ token }), { status: 200, headers: { "content-type": "application/json" } });
|
||||
}
|
||||
Reference in New Issue
Block a user