|
| 1 | +/** |
| 2 | + * GitHub automation event webhook route — internal endpoint that receives |
| 3 | + * pre-normalized GitHubAutomationEvents from the github-bot and proxies |
| 4 | + * them to the SchedulerDO for automation matching and session dispatch. |
| 5 | + */ |
| 6 | + |
| 7 | +import type { GitHubAutomationEvent } from "@open-inspect/shared"; |
| 8 | +import { verifyInternalToken } from "../auth/internal"; |
| 9 | +import type { Route, RequestContext } from "../routes/shared"; |
| 10 | +import { parsePattern, json, error } from "../routes/shared"; |
| 11 | +import type { Env } from "../types"; |
| 12 | + |
| 13 | +async function handleGitHubAutomationEvent( |
| 14 | + request: Request, |
| 15 | + env: Env, |
| 16 | + _match: RegExpMatchArray, |
| 17 | + _ctx: RequestContext |
| 18 | +): Promise<Response> { |
| 19 | + // 0. Authenticate — fail closed if secret is unconfigured or token is invalid |
| 20 | + if (!env.INTERNAL_CALLBACK_SECRET) { |
| 21 | + return error("Internal authentication not configured", 500); |
| 22 | + } |
| 23 | + const isValid = await verifyInternalToken( |
| 24 | + request.headers.get("Authorization"), |
| 25 | + env.INTERNAL_CALLBACK_SECRET |
| 26 | + ); |
| 27 | + if (!isValid) { |
| 28 | + return error("Unauthorized", 401); |
| 29 | + } |
| 30 | + |
| 31 | + // 1. Parse body |
| 32 | + let body: unknown; |
| 33 | + try { |
| 34 | + body = await request.json(); |
| 35 | + } catch { |
| 36 | + return error("Invalid JSON", 400); |
| 37 | + } |
| 38 | + |
| 39 | + // 2. Validate required fields |
| 40 | + const event = body as Partial<GitHubAutomationEvent>; |
| 41 | + if (event.source !== "github") { |
| 42 | + return error("Invalid event: source must be 'github'", 400); |
| 43 | + } |
| 44 | + if (!event.repoOwner || !event.repoName) { |
| 45 | + return error("Invalid event: repoOwner and repoName are required", 400); |
| 46 | + } |
| 47 | + if (!event.eventType || !event.triggerKey || !event.concurrencyKey) { |
| 48 | + return error("Invalid event: eventType, triggerKey, and concurrencyKey are required", 400); |
| 49 | + } |
| 50 | + |
| 51 | + // 3. Forward to SchedulerDO |
| 52 | + if (!env.SCHEDULER) { |
| 53 | + return error("Scheduler not configured", 503); |
| 54 | + } |
| 55 | + |
| 56 | + const doId = env.SCHEDULER.idFromName("global-scheduler"); |
| 57 | + const stub = env.SCHEDULER.get(doId); |
| 58 | + |
| 59 | + let response: Response; |
| 60 | + try { |
| 61 | + response = await stub.fetch("http://internal/internal/event", { |
| 62 | + method: "POST", |
| 63 | + headers: { "Content-Type": "application/json" }, |
| 64 | + body: JSON.stringify(event), |
| 65 | + }); |
| 66 | + } catch (_e) { |
| 67 | + return json({ ok: false, error: "Failed to reach scheduler" }, 502); |
| 68 | + } |
| 69 | + |
| 70 | + let result: { triggered: number; skipped: number }; |
| 71 | + try { |
| 72 | + result = await response.json<{ triggered: number; skipped: number }>(); |
| 73 | + } catch { |
| 74 | + return json({ ok: false, error: "Invalid response from scheduler" }, 502); |
| 75 | + } |
| 76 | + |
| 77 | + return json({ ok: true, ...result }, response.status); |
| 78 | +} |
| 79 | + |
| 80 | +export const githubAutomationEventRoute: Route = { |
| 81 | + method: "POST", |
| 82 | + pattern: parsePattern("/internal/github-event"), |
| 83 | + handler: handleGitHubAutomationEvent, |
| 84 | +}; |
0 commit comments