import { HttpError } from "../errors"; import { handleScratchApi } from "../sites/scratch"; import { getSiteUsage } from "../sites/usage"; 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 { createRepo, deleteRepo, forkRepo, getCollaborator, getRepo, importRepo, listRepos, putCollaborator, updateRepo, } from "./repos"; import { approveRunApi, cancelRunApi, createOwnerRegistrationToken, createRegistrationToken, deleteOwnerRunnerApi, deleteRunnerApi, listOwnerRunnersApi, listRunnersApi, listRunsApi, putRunnerPolicy, } from "./runners"; import { createBranchRule, createProtection, deleteBranchRule, listBranchRules, listProtections, putContents, setPushProtection, updateBranchRule, } from "./settings"; import { createReview, dequeuePull, disableAutoMerge, dismissPullReview, enableAutoMerge, enqueuePull, patchPull, replyToComment, rerequestReviewer, resolveThread, setRequestedReviewers, } from "./pull-writes"; import { createLabel, createRepoDeployToken, createToken, listDeployTokens, listLabels, putCodeOwnerReview, removeDeployToken, removeLabel, updateLabel, } from "./admin-writes"; import { deleteOrg } from "./org-delete"; import { getLfsUsage, getUser, listTokens, revokeToken } from "./user"; import { getOrgUsage, getUserUsage } from "./usage"; export const API_V1_PREFIX = "/api/v1/"; /** Anonymous scratch deploys; listed as token-only in `src/server.ts` because it never reads cookies. */ export const SCRATCH_API_PATH = "/scratch"; type Handler = (ctx: ApiContext, params: string[]) => Response | Promise; type Route = { method: string; pattern: RegExp; handler: Handler }; const SEG = "([^/]+)"; const REPO = `/repos/${SEG}/${SEG}`; const PULL = `${REPO}/pulls/(\\d+)`; 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/usage", (ctx) => getUserUsage(ctx)), route("GET", `/orgs/${SEG}/usage`, (ctx, [org]) => getOrgUsage(ctx, org)), route("GET", "/user/site-usage", (ctx) => getSiteUsage(ctx.viewer)), route("GET", "/user/tokens", (ctx) => listTokens(ctx)), route("POST", "/user/tokens", (ctx) => createToken(ctx)), route("DELETE", `/orgs/${SEG}`, (ctx, [org]) => deleteOrg(ctx, org)), 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("PATCH", `${PULL}`, (ctx, [o, n, num]) => patchPull(ctx, o, n, pullNumber(num))), route("POST", `${PULL}/reviews`, (ctx, [o, n, num]) => createReview(ctx, o, n, pullNumber(num))), route("PUT", `${PULL}/reviews/${SEG}/dismissals`, (ctx, [o, n, num, id]) => dismissPullReview(ctx, o, n, pullNumber(num), id), ), route("POST", `${PULL}/comments/${SEG}/replies`, (ctx, [o, n, num, id]) => replyToComment(ctx, o, n, pullNumber(num), id), ), route("PUT", `${PULL}/threads/${SEG}`, (ctx, [o, n, num, id]) => resolveThread(ctx, o, n, pullNumber(num), id)), route("POST", `${PULL}/requested_reviewers`, (ctx, [o, n, num]) => setRequestedReviewers(ctx, o, n, pullNumber(num))), route("POST", `${PULL}/requested_reviewers/rerequest`, (ctx, [o, n, num]) => rerequestReviewer(ctx, o, n, pullNumber(num)), ), route("PUT", `${PULL}/auto_merge`, (ctx, [o, n, num]) => enableAutoMerge(ctx, o, n, pullNumber(num))), route("DELETE", `${PULL}/auto_merge`, (ctx, [o, n, num]) => disableAutoMerge(ctx, o, n, pullNumber(num))), route("PUT", `${PULL}/merge_queue`, (ctx, [o, n, num]) => enqueuePull(ctx, o, n, pullNumber(num))), route("DELETE", `${PULL}/merge_queue`, (ctx, [o, n, num]) => dequeuePull(ctx, o, n, pullNumber(num))), route("GET", `${REPO}/branch-protections`, (ctx, [o, n]) => listBranchRules(ctx, o, n)), route("POST", `${REPO}/branch-protections`, (ctx, [o, n]) => createBranchRule(ctx, o, n)), route("PUT", `${REPO}/branch-protections/${SEG}`, (ctx, [o, n, id]) => updateBranchRule(ctx, o, n, id)), route("DELETE", `${REPO}/branch-protections/${SEG}`, (ctx, [o, n, id]) => deleteBranchRule(ctx, o, n, id)), route("PUT", `${REPO}/branch-protections/${SEG}/code-owner-review`, (ctx, [o, n, id]) => putCodeOwnerReview(ctx, o, n, id), ), route("GET", `${REPO}/labels`, (ctx, [o, n]) => listLabels(ctx, o, n)), route("POST", `${REPO}/labels`, (ctx, [o, n]) => createLabel(ctx, o, n)), route("PATCH", `${REPO}/labels/${SEG}`, (ctx, [o, n, id]) => updateLabel(ctx, o, n, id)), route("DELETE", `${REPO}/labels/${SEG}`, (ctx, [o, n, id]) => removeLabel(ctx, o, n, id)), route("GET", `${REPO}/deploy-tokens`, (ctx, [o, n]) => listDeployTokens(ctx, o, n)), route("POST", `${REPO}/deploy-tokens`, (ctx, [o, n]) => createRepoDeployToken(ctx, o, n)), route("DELETE", `${REPO}/deploy-tokens/${SEG}`, (ctx, [o, n, id]) => removeDeployToken(ctx, o, n, id)), 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", `/orgs/${SEG}/actions/runners/registration-token`, (ctx, [o]) => createOwnerRegistrationToken(ctx, o)), route("GET", `/orgs/${SEG}/actions/runners`, (ctx, [o]) => listOwnerRunnersApi(ctx, o)), route("DELETE", `/orgs/${SEG}/actions/runners/([a-z0-9]+)`, (ctx, [o, id]) => deleteOwnerRunnerApi(ctx, o, id)), 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(/\/+$/, "") || "/"; if (path === SCRATCH_API_PATH) return handleScratchApi(request); 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) : []); }