| import { createFileRoute } from "@tanstack/react-router"; |
| import { createHash, timingSafeEqual } from "node:crypto"; |
| import { analyzeDiff } from "@/lib/branch-debug.functions"; |
|
|
| |
| |
| |
|
|
| const ALLOWED_ORIGINS = new Set<string>([ |
| |
| ]); |
|
|
| const MAX_BODY_BYTES = 256 * 1024; |
|
|
| function corsHeaders(origin: string | null): Record<string, string> { |
| const allow = origin && ALLOWED_ORIGINS.has(origin) ? origin : "null"; |
| return { |
| "Access-Control-Allow-Origin": allow, |
| "Access-Control-Allow-Methods": "POST, OPTIONS", |
| "Access-Control-Allow-Headers": "Content-Type, Authorization", |
| "Vary": "Origin", |
| }; |
| } |
|
|
| function timingSafeEqualStr(a: string, b: string): boolean { |
| |
| |
| const ha = createHash("sha256").update(a).digest(); |
| const hb = createHash("sha256").update(b).digest(); |
| return timingSafeEqual(ha, hb); |
| } |
|
|
| export const Route = createFileRoute("/api/public/branch-debug")({ |
| server: { |
| handlers: { |
| OPTIONS: async ({ request }) => |
| new Response(null, { status: 204, headers: corsHeaders(request.headers.get("origin")) }), |
| POST: async ({ request }) => { |
| const headers = corsHeaders(request.headers.get("origin")); |
|
|
| |
| const expected = process.env.BRANCH_DEBUG_TOKEN; |
| if (!expected) { |
| return Response.json({ error: "Endpoint disabled" }, { status: 503, headers }); |
| } |
| const auth = request.headers.get("authorization") ?? ""; |
| const provided = auth.startsWith("Bearer ") ? auth.slice(7) : ""; |
| if (!provided || !timingSafeEqualStr(provided, expected)) { |
| return Response.json({ error: "Unauthorized" }, { status: 401, headers }); |
| } |
|
|
| |
| |
| const advertised = Number(request.headers.get("content-length") ?? "0"); |
| if (advertised > MAX_BODY_BYTES) { |
| return Response.json({ error: "Payload too large" }, { status: 413, headers }); |
| } |
|
|
| try { |
| const buf = await request.arrayBuffer(); |
| if (buf.byteLength > MAX_BODY_BYTES) { |
| return Response.json({ error: "Payload too large" }, { status: 413, headers }); |
| } |
| let body: any = {}; |
| try { |
| body = JSON.parse(new TextDecoder().decode(buf)); |
| } catch { |
| return Response.json({ error: "Invalid JSON" }, { status: 400, headers }); |
| } |
| const diff = typeof body.diff === "string" ? body.diff : ""; |
| const failureDescription = typeof body.failureDescription === "string" ? body.failureDescription : ""; |
| const repoRoot = typeof body.repoRoot === "string" ? body.repoRoot : null; |
| const editor = body.editor === "cursor" ? "cursor" : "vscode"; |
|
|
| if (!diff || !failureDescription) { |
| return Response.json({ error: "diff and failureDescription are required" }, { status: 400, headers }); |
| } |
| if (diff.length > 200_000 || failureDescription.length > 5_000) { |
| return Response.json({ error: "Input too large" }, { status: 413, headers }); |
| } |
|
|
| const result = await analyzeDiff(diff, failureDescription); |
|
|
| const prefix = editor === "cursor" ? "cursor://file/" : "vscode://file/"; |
| const suspects = result.suspects.map((s) => ({ |
| ...s, |
| jumpUrl: repoRoot |
| ? `${prefix}${repoRoot.replace(/\/+$/, "")}/${s.filePath}:${s.lineStart}` |
| : null, |
| })); |
|
|
| return Response.json({ ...result, suspects }, { headers }); |
| } catch { |
| |
| return Response.json({ error: "Internal error" }, { status: 500, headers }); |
| } |
| }, |
| }, |
| }, |
| }); |
|
|