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 { 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("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}/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) : []); }