import { httpRouter } from "convex/server" import type { Id } from "./_generated/dataModel" import { internal } from "./_generated/api" import { httpAction } from "./_generated/server" import { auth } from "./auth" const http = httpRouter() auth.addHttpRoutes(http) function verifyBearer(request: Request): boolean { const buildToken = process.env.CONVEX_BUILD_TOKEN if (!buildToken) return false const authHeader = request.headers.get("Authorization") if (!authHeader?.startsWith("Bearer ")) return false return authHeader.slice(7) === buildToken } function mapWebhookStatus(state: string): "running" | "succeeded" | "failed" { const s = state.toLowerCase() if (s === "running" || s === "queued") return "running" if (s === "succeeded" || s === "success" || s === "completed") return "succeeded" if (s === "failed" || s === "failure") return "failed" return "running" } http.route({ path: "/ingest-repo-build", method: "POST", handler: httpAction(async (ctx, request) => { if (!verifyBearer(request)) { return new Response("Unauthorized", { status: 401 }) } const payload = (await request.json()) as { repo_build_id?: string state?: string github_run_id?: string | number r2ObjectKey?: string errorSummary?: string } if (!payload.repo_build_id || !payload.state) { return new Response("Missing repo_build_id or state", { status: 400 }) } const runId = payload.github_run_id !== undefined ? Number(payload.github_run_id) : undefined await ctx.runMutation(internal.repoBuilds.patchFromWebhook, { buildId: payload.repo_build_id as Id<"repoBuilds">, status: mapWebhookStatus(payload.state), githubRunId: Number.isFinite(runId) ? runId : undefined, r2ObjectKey: payload.r2ObjectKey, errorSummary: payload.errorSummary, }) return new Response(null, { status: 200 }) }), }) http.route({ path: "/ingest-repo-build-progress", method: "POST", handler: httpAction(async (ctx, request) => { if (!verifyBearer(request)) { return new Response("Unauthorized", { status: 401 }) } const payload = (await request.json()) as { repo_build_id?: string step_index?: number step_total?: number label?: string } if ( !payload.repo_build_id || typeof payload.step_index !== "number" || typeof payload.step_total !== "number" || typeof payload.label !== "string" ) { return new Response("Missing repo_build_id, step_index, step_total, or label", { status: 400 }) } await ctx.runMutation(internal.repoBuilds.patchCiProgress, { buildId: payload.repo_build_id as Id<"repoBuilds">, stepIndex: payload.step_index, stepTotal: payload.step_total, label: payload.label, }) return new Response(null, { status: 200 }) }), }) /** Legacy: old workflows posting build_id + state */ http.route({ path: "/github-webhook", method: "POST", handler: httpAction(async (ctx, request) => { if (!verifyBearer(request)) { return new Response("Unauthorized", { status: 401 }) } const payload = (await request.json()) as { build_id?: string repo_build_id?: string state?: string github_run_id?: string | number firmwarePath?: string r2ObjectKey?: string errorSummary?: string } const id = payload.repo_build_id || payload.build_id if (!id || !payload.state) { return new Response("Missing build id or state", { status: 400 }) } const runId = payload.github_run_id !== undefined ? Number(payload.github_run_id) : undefined await ctx.runMutation(internal.repoBuilds.patchFromWebhook, { buildId: id as Id<"repoBuilds">, status: mapWebhookStatus(payload.state), githubRunId: Number.isFinite(runId) ? runId : undefined, r2ObjectKey: payload.r2ObjectKey || payload.firmwarePath, errorSummary: payload.errorSummary, }) return new Response(null, { status: 200 }) }), }) export default http