import handler from "@tanstack/react-start/server-entry"; import { retryHiddenRefCleanups, sweepClosedPullRefs } from "./server/git/pull-bridge"; import { sweepOrphanedLfsBlobs } from "./server/lfs/objects"; import { handleGitRequest, matchGitRoute } from "./server/git/proxy"; import { handleJobApi, handleRunnerApi, reapStaleRunnerJobs } from "./server/ci/runner-api"; import { handleQueueBatch } from "./server/events/consumer"; import type { QueueMessage } from "./server/events/emit"; import { HttpError } from "./server/errors"; import { finishGithubLogin, startGithubLogin } from "./server/github-oauth"; import { handleRawRoutes } from "./server/raw"; import { handleLfsRequest, matchLfsRoute } from "./server/lfs/api"; import { handlePagesRequest } from "./server/pages/serve"; import { handleSitesRequest } from "./server/sites/dispatch"; import { handleSitesForgeRoutes } from "./server/sites/forge"; import { sweepScratchSites } from "./server/sites/scratch"; import { reconcileSites } from "./server/sites/sync"; import { runMeteringCron } from "./server/metering/cron"; import { handleSecurityApi } from "./server/security/api"; import { handleSecurityBatch, SECURITY_QUEUE } from "./server/security/queue"; import { handleAttachmentRoutes } from "./server/attachments"; import { handleProjectRoutes } from "./server/projects"; import { handleAvatarRoutes } from "./server/avatars"; import { purgeExpiredAuthTokens } from "./server/auth-tokens"; import { hasSessionCookie } from "./server/auth"; import { handleActionsRoutes } from "./server/ci/http"; import { runActionsCron } from "./server/ci/maintenance"; import { API_V1_PREFIX, handleApiV1 } from "./server/api/router"; import { handleEditorRoutes } from "./server/editor/api"; import { handleEditorOrigin } from "./server/editor/host"; import { handleIntegrationRoutes, INBOUND_HOOK_PREFIX } from "./server/integrations/http"; import { handleIntegrationBatch, INTEGRATIONS_DLQ, INTEGRATIONS_QUEUE, runIntegrationsCron, } from "./server/integrations/queue"; import type { IntegrationMessage } from "./server/integrations/queue"; import { handleMcpRoutes } from "./server/mcp/routes"; import { DEV_MAIL_MESSAGES_PATH, handleDevMailRoutes } from "./server/notify/dev-mail"; import { flushNotificationDigests } from "./server/notify/deliver"; import { sweepOfflineRunners } from "./server/notify/runners"; import { handleUnsubscribeRoute, UNSUBSCRIBE_PATH } from "./server/notify/unsubscribe"; export { Runner } from "./server/ci/runner-do"; export { RepoProvisionWorkflow } from "./server/repo-provisioning"; export { RunnerLarge, RunnerXLarge } from "./server/ci/runner-variants"; export { CiRunWorkflow } from "./server/ci/run-workflow"; export { PushWorkflow } from "./server/events/push-workflow"; export { PagesDeployWorkflow } from "./server/pages/workflow"; export { SitesDeployWorkflow } from "./server/sites/workflow"; export { SiteAssets, SiteOutbound, SiteTail } from "./server/sites/runtime"; export { MergeFixWorkflow } from "./server/mergefix/workflow"; export { ProjectRoom } from "./server/project-room"; export { EditorRoom } from "./server/editor/room"; export { EditorContainer } from "./server/editor/container-do"; export { MergeQueue } from "./server/merge-queue-do"; export { McpHub } from "./server/mcp/hub-do"; const RUNNER_API_PREFIX = "/api/runner/"; const JOB_API_PREFIX = "/api/"; async function route(request: Request, ctx: ExecutionContext): Promise { const editorHost = await handleEditorOrigin(request, new URL(request.url)); if (editorHost) return editorHost; const site = await handleSitesRequest(request, ctx); if (site) return site; const pages = await handlePagesRequest(request); if (pages) return pages; const url = new URL(request.url); const mcp = await handleMcpRoutes(request, url); if (mcp) return mcp; if (!isTokenOnlyPath(url) && !isSameOriginRequest(request)) { return errorResponse(request, 403, "Cross-origin request blocked"); } const gitRoute = matchGitRoute(url); if (gitRoute) return handleGitRequest(request, gitRoute, ctx); const lfsRoute = matchLfsRoute(url); if (lfsRoute) return handleLfsRequest(request, lfsRoute); if (url.pathname.startsWith(API_V1_PREFIX)) return handleApiV1(request, url); if (url.pathname.startsWith(RUNNER_API_PREFIX)) { return handleRunnerApi(request, url.pathname.slice(RUNNER_API_PREFIX.length)); } if (url.pathname.startsWith("/api/jobs/") || url.pathname.startsWith("/api/artifacts/")) { return handleJobApi(request, url.pathname.slice(JOB_API_PREFIX.length)); } const securityApi = await handleSecurityApi(request, url); if (securityApi) return securityApi; const actions = await handleActionsRoutes(request, url); if (actions) return actions; const integrations = await handleIntegrationRoutes(request, url); if (integrations) return integrations; const sites = await handleSitesForgeRoutes(request, url); if (sites) return sites; const notifyRoute = (await handleUnsubscribeRoute(request, url)) ?? (await handleDevMailRoutes(request, url)); if (notifyRoute) return notifyRoute; if (url.pathname === "/auth/github") return startGithubLogin(request); if (url.pathname === "/auth/github/callback") return finishGithubLogin(request); return ( (await handleEditorRoutes(request, url)) ?? (await handleAvatarRoutes(request, url)) ?? (await handleAttachmentRoutes(request, url)) ?? (await handleProjectRoutes(request, url)) ?? handleRawRoutes(request, url) ); } const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]); const TOKEN_ONLY_PREFIXES = [ RUNNER_API_PREFIX, "/api/jobs/", "/api/artifacts/", "/api/actions/", INBOUND_HOOK_PREFIX, API_V1_PREFIX, "/api/v1/scratch", UNSUBSCRIBE_PATH, DEV_MAIL_MESSAGES_PATH, ]; const SARIF_API_PATH = /^\/api\/repos\/[^/]+\/[^/]+\/code-scanning\/sarifs(\/|$)/; const BEARER_SCHEMES = new Set(["bearer", "token"]); /** Git, LFS, runner/job/actions and SARIF APIs never read the session cookie, so cross-site requests carry no ambient authority. */ function isTokenOnlyPath(url: URL): boolean { return ( matchGitRoute(url) !== null || matchLfsRoute(url) !== null || SARIF_API_PATH.test(url.pathname) || TOKEN_ONLY_PREFIXES.some((prefix) => url.pathname.startsWith(prefix)) ); } /** * State-changing requests must come from the forge's own pages. A bearer token can't be attached by a * cross-site form, and a cross-site fetch carrying one is preflighted, so token API clients pass. Basic is * only exempt without a session cookie, since browsers replay cached Basic credentials on their own. */ function isSameOriginRequest(request: Request): boolean { if (SAFE_METHODS.has(request.method)) return true; const scheme = (request.headers.get("authorization") ?? "").split(" ", 1)[0].toLowerCase(); if (BEARER_SCHEMES.has(scheme)) return true; if (scheme === "basic" && !hasSessionCookie(request)) return true; const fetchSite = request.headers.get("sec-fetch-site"); if (fetchSite) return fetchSite === "same-origin"; const origin = request.headers.get("origin"); return origin !== null && origin === new URL(request.url).origin; } /** API clients get JSON; browsers following a download or raw link get readable text. */ function errorResponse(request: Request, status: number, message: string): Response { const path = new URL(request.url).pathname; const wantsJson = path.startsWith("/api/") || (request.headers.get("accept") ?? "").includes("application/json"); if (wantsJson) return Response.json({ error: message }, { status }); return new Response(`${message}\n`, { status, headers: { "content-type": "text/plain; charset=utf-8" }, }); } export default { async fetch(request: Request, _env: Env, ctx: ExecutionContext): Promise { try { const response = await route(request, ctx); if (response) return response; } catch (error) { const status = error instanceof HttpError ? error.status : 500; const message = error instanceof HttpError ? error.message : "Internal error"; if (!(error instanceof HttpError)) console.error("Unhandled API error", error); return errorResponse(request, status, message); } return handler.fetch(request); }, async queue(batch: MessageBatch): Promise { if (batch.queue === SECURITY_QUEUE) return handleSecurityBatch(batch); if (batch.queue === INTEGRATIONS_QUEUE || batch.queue === INTEGRATIONS_DLQ) { return handleIntegrationBatch(batch as unknown as MessageBatch); } await handleQueueBatch(batch); }, async scheduled(_controller: ScheduledController, _env: Env, ctx: ExecutionContext): Promise { ctx.waitUntil(reapStaleRunnerJobs()); ctx.waitUntil(purgeExpiredAuthTokens()); ctx.waitUntil(runActionsCron()); ctx.waitUntil(retryHiddenRefCleanups()); ctx.waitUntil(sweepClosedPullRefs()); ctx.waitUntil(sweepOrphanedLfsBlobs()); ctx.waitUntil(runIntegrationsCron()); ctx.waitUntil(sweepScratchSites()); ctx.waitUntil(reconcileSites()); ctx.waitUntil(runMeteringCron()); ctx.waitUntil(flushNotificationDigests()); ctx.waitUntil(sweepOfflineRunners()); }, } satisfies ExportedHandler;