import { verifyMirrorHook } from "./hook-auth"; import { enforceRateLimit, requestIp } from "../limits"; import { env } from "cloudflare:workers"; import { mirrorFlagKey, mirrorStub } from "./mirror-do"; export const MIRROR_HOOK_PATH = /^mirror\/([A-Za-z0-9_-]{1,64})$/; const MAX_HOOK_BYTES = 1024 * 1024; const HTTP_ACCEPTED = 202; const HTTP_UNAUTHORIZED = 401; const HTTP_TOO_LARGE = 413; async function readCapped(request: Request): Promise { if (Number(request.headers.get("content-length") ?? "0") > MAX_HOOK_BYTES) return null; if (!request.body) return ""; const reader = request.body.getReader(); const chunks: Uint8Array[] = []; let size = 0; for (;;) { const { value, done } = await reader.read(); if (done) break; size += value.byteLength; if (size > MAX_HOOK_BYTES) { await reader.cancel().catch(() => undefined); return null; } chunks.push(value); } const merged = new Uint8Array(size); let offset = 0; for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.byteLength; } return new TextDecoder().decode(merged); } /** `POST /integrations/hooks/mirror/`: a push on the remote queues a coalesced mirror sync. */ export async function handleMirrorHook(request: Request, repoId: string): Promise { await enforceRateLimit("GIT_LIMITER", `mirror-hook:${repoId}:${requestIp(request)}`); if (!(await env.KV.get(mirrorFlagKey(repoId)))) return new Response("Not found", { status: 404 }); const body = await readCapped(request); if (body === null) return new Response("Payload too large", { status: HTTP_TOO_LARGE }); const stub = mirrorStub(repoId); const secret = await stub.webhookSecret(); if (!secret || !(await verifyMirrorHook(secret, request, body))) { return new Response("Invalid signature", { status: HTTP_UNAUTHORIZED }); } const event = request.headers.get("x-github-event") ?? request.headers.get("x-gitea-event"); if (event === "ping") return new Response("pong"); const result = await stub.requestSync("webhook"); if (!result.ok) return Response.json({ queued: false, reason: result.reason }, { status: HTTP_ACCEPTED }); return Response.json({ queued: true, at: new Date(result.at).toISOString() }, { status: HTTP_ACCEPTED }); }