// c21 — handlers: Forgejo push-webhook entry point. HMAC-verified, fires // `judge()` in the background and acks immediately so the upstream push // hook doesn't time out while we're checking out commits. Extracted // from c21_app.ts per the SAMA Atomic rule — separate file from the // manual /api/judge trigger because the auth model (HMAC vs. bearer) // and the failure semantics (ack-and-fire vs. wait-for-verdict) are // genuinely different concepts. import { judge } from "./c14_judge.ts"; import { parseJson } from "./c14_request_parse.ts"; import { timingSafeEqual, hmacSha256Hex } from "./b32_session.ts"; export const forgejoWebhookHandler = async (req: Request): Promise => { if (req.method !== "POST") return new Response("POST only", { status: 405 }); const secret = process.env.WEBHOOK_SECRET; if (!secret) return new Response("webhook not configured", { status: 503 }); const body = await req.text(); const provided = req.headers.get("x-forgejo-signature") ?? req.headers.get("x-gitea-signature") ?? ""; const expected = await hmacSha256Hex(secret, body); if (provided.length !== expected.length || !timingSafeEqual(provided, expected)) { return new Response("invalid signature", { status: 401 }); } const parsed = parseJson<{ repository?: { owner?: { login?: string }; name?: string }; ref?: string }>(body); if (!parsed.ok) return new Response("invalid json", { status: 400 }); const payload = parsed.value; const owner = payload.repository?.owner?.login; const repo = payload.repository?.name; if (!owner || !repo) return new Response("missing owner/repo", { status: 400 }); // Fire the judge in the background; ack immediately so Forgejo // doesn't time out while we're checking out commits. void judge(owner, repo).catch((err) => { console.error(`judge failed for ${owner}/${repo}:`, err); }); return Response.json({ accepted: true, owner, repo }); };