import { HttpError } from "../errors"; import { createEditorSession } from "./editor"; import { apiContext } from "./http"; import type { ApiContext } from "./http"; import { createPull, getPull, listPulls, mergePull, updatePullBranch } from "./pulls"; import { getPullAiFix, startPullAiFix } from "./mergefix"; import { createHook, deleteHook, getLfsQuota, getNotificationSettingsApi, listHooks, listNotificationsApi, markNotificationsApi, pingHook, putNotificationSettingsApi, redeliverHook, } from "./platform"; import { createWebhookApi, deleteWebhookApi, listWebhookDeliveriesApi, listWebhooksApi, redeliverWebhookApi, } from "./webhooks"; import { createRepo, deleteRepo, forkRepo, getCollaborator, getRepo, importRepo, listRepos, putCollaborator, updateRepo, } from "./repos"; import { approveRunApi, cancelRunApi, createRegistrationToken, deleteRunnerApi, listRunnersApi, listRunsApi, putRunnerPolicy, } from "./runners"; import { createProtection, listProtections, putContents, setPushProtection } from "./settings"; import { getLfsUsage, getUser, listTokens, revokeToken } from "./user"; export const API_V1_PREFIX = "/api/v1/"; type Handler = (ctx: ApiContext, params: string[]) => Response | Promise; type Route = { method: string; pattern: RegExp; handler: Handler }; const SEG = "([^/]+)"; const REPO = `/repos/${SEG}/${SEG}`; function route(method: string, path: string, handler: Handler): Route { return { method, pattern: new RegExp(`^${path}$`), handler }; } function pullNumber(raw: string): number { const number = Number(raw); if (!Number.isSafeInteger(number) || number <= 0) throw new HttpError(404, "Pull request not found"); return number; } const ROUTES: Route[] = [ route("GET", "/user", (ctx) => getUser(ctx)), route("GET", "/user/lfs-usage", (ctx) => getLfsUsage(ctx)), route("GET", "/user/tokens", (ctx) => listTokens(ctx)), route("GET", "/user/lfs-quota", (ctx) => getLfsQuota(ctx)), route("GET", "/user/notification-settings", (ctx) => getNotificationSettingsApi(ctx)), route("PUT", "/user/notification-settings", (ctx) => putNotificationSettingsApi(ctx)), route("GET", "/notifications", (ctx) => listNotificationsApi(ctx)), route("PUT", "/notifications", (ctx) => markNotificationsApi(ctx)), route("GET", `/orgs/${SEG}/integrations`, (ctx, [o]) => listHooks(ctx, o, null)), route("POST", `/orgs/${SEG}/integrations`, (ctx, [o]) => createHook(ctx, o, null)), route("DELETE", `/orgs/${SEG}/integrations/${SEG}`, (ctx, [o, id]) => deleteHook(ctx, o, null, id)), route("POST", `/orgs/${SEG}/integrations/${SEG}/pings`, (ctx, [o, id]) => pingHook(ctx, o, null, id)), route("POST", `/orgs/${SEG}/integrations/${SEG}/deliveries/${SEG}/attempts`, (ctx, [o, id, d]) => redeliverHook(ctx, o, null, id, d), ), route("GET", `${REPO}/integrations`, (ctx, [o, n]) => listHooks(ctx, o, n)), route("POST", `${REPO}/integrations`, (ctx, [o, n]) => createHook(ctx, o, n)), route("DELETE", `${REPO}/integrations/${SEG}`, (ctx, [o, n, id]) => deleteHook(ctx, o, n, id)), route("POST", `${REPO}/integrations/${SEG}/pings`, (ctx, [o, n, id]) => pingHook(ctx, o, n, id)), route("POST", `${REPO}/integrations/${SEG}/deliveries/${SEG}/attempts`, (ctx, [o, n, id, d]) => redeliverHook(ctx, o, n, id, d), ), route("GET", `${REPO}/hooks`, (ctx, [o, n]) => listWebhooksApi(ctx, o, n)), route("POST", `${REPO}/hooks`, (ctx, [o, n]) => createWebhookApi(ctx, o, n)), route("DELETE", `${REPO}/hooks/${SEG}`, (ctx, [o, n, id]) => deleteWebhookApi(ctx, o, n, id)), route("GET", `${REPO}/hooks/${SEG}/deliveries`, (ctx, [o, n, id]) => listWebhookDeliveriesApi(ctx, o, n, id)), route("POST", `${REPO}/hooks/${SEG}/deliveries/${SEG}/attempts`, (ctx, [o, n, id, d]) => redeliverWebhookApi(ctx, o, n, id, d), ), route("DELETE", `/user/tokens/${SEG}`, (ctx, [id]) => revokeToken(ctx, id)), route("GET", "/repos", (ctx) => listRepos(ctx)), route("POST", "/repos", (ctx) => createRepo(ctx)), route("GET", REPO, (ctx, [o, n]) => getRepo(ctx, o, n)), route("PATCH", REPO, (ctx, [o, n]) => updateRepo(ctx, o, n)), route("DELETE", REPO, (ctx, [o, n]) => deleteRepo(ctx, o, n)), route("POST", `${REPO}/forks`, (ctx, [o, n]) => forkRepo(ctx, o, n)), route("GET", `${REPO}/collaborators/${SEG}`, (ctx, [o, n, u]) => getCollaborator(ctx, o, n, u)), route("PUT", `${REPO}/collaborators/${SEG}`, (ctx, [o, n, u]) => putCollaborator(ctx, o, n, u)), route("GET", `${REPO}/protections`, (ctx, [o, n]) => listProtections(ctx, o, n)), route("POST", `${REPO}/protections`, (ctx, [o, n]) => createProtection(ctx, o, n)), route("PUT", `${REPO}/settings/push-protection`, (ctx, [o, n]) => setPushProtection(ctx, o, n)), route("PUT", `${REPO}/contents/(.+)`, (ctx, [o, n, p]) => putContents(ctx, o, n, p)), route("GET", `${REPO}/pulls`, (ctx, [o, n]) => listPulls(ctx, o, n)), route("POST", `${REPO}/pulls`, (ctx, [o, n]) => createPull(ctx, o, n)), route("GET", `${REPO}/pulls/(\\d+)`, (ctx, [o, n, num]) => getPull(ctx, o, n, pullNumber(num))), route("POST", `${REPO}/pulls/(\\d+)/merge`, (ctx, [o, n, num]) => mergePull(ctx, o, n, pullNumber(num))), route("POST", `${REPO}/pulls/(\\d+)/update-branch`, (ctx, [o, n, num]) => updatePullBranch(ctx, o, n, pullNumber(num)), ), route("POST", `${REPO}/pulls/(\\d+)/ai-fix`, (ctx, [o, n, num]) => startPullAiFix(ctx, o, n, pullNumber(num))), route("GET", `${REPO}/pulls/(\\d+)/ai-fix`, (ctx, [o, n, num]) => getPullAiFix(ctx, o, n, pullNumber(num))), route("POST", `${REPO}/import`, (ctx, [o, n]) => importRepo(ctx, o, n)), route("POST", `${REPO}/actions/runners/registration-token`, (ctx, [o, n]) => createRegistrationToken(ctx, o, n)), route("GET", `${REPO}/actions/runners`, (ctx, [o, n]) => listRunnersApi(ctx, o, n)), route("PUT", `${REPO}/actions/runners/policy`, (ctx, [o, n]) => putRunnerPolicy(ctx, o, n)), route("DELETE", `${REPO}/actions/runners/([a-z0-9]+)`, (ctx, [o, n, id]) => deleteRunnerApi(ctx, o, n, id)), route("GET", `${REPO}/actions/runs`, (ctx, [o, n]) => listRunsApi(ctx, o, n)), route("POST", `${REPO}/actions/runs/(\\d+)/cancel`, (ctx, [o, n, num]) => cancelRunApi(ctx, o, n, pullNumber(num))), route("POST", `${REPO}/actions/runs/(\\d+)/approve`, (ctx, [o, n, num]) => approveRunApi(ctx, o, n, pullNumber(num))), route("POST", `${REPO}/editor-sessions`, (ctx, [o, n]) => createEditorSession(ctx, o, n)), ]; function decodeSegments(match: RegExpExecArray): string[] { try { return match.slice(1).map((part) => decodeURIComponent(part)); } catch { throw new HttpError(400, "Malformed path"); } } /** Dispatches `/api/v1/*`; errors are thrown as HttpError and rendered as `{error}` JSON by the Worker entry. */ export async function handleApiV1(request: Request, url: URL): Promise { const path = url.pathname.slice(API_V1_PREFIX.length - 1).replace(/\/+$/, "") || "/"; const matching = ROUTES.filter((candidate) => candidate.pattern.test(path)); if (matching.length === 0) return Response.json({ error: "Not found" }, { status: 404 }); const selected = matching.find((candidate) => candidate.method === request.method); if (!selected) { return Response.json( { error: "Method not allowed" }, { status: 405, headers: { allow: matching.map((candidate) => candidate.method).join(", ") } }, ); } const ctx = await apiContext(request, url); const match = selected.pattern.exec(path); return selected.handler(ctx, match ? decodeSegments(match) : []); }