import { env } from "cloudflare:workers"; import { MIN_SECRETS_KEY_LENGTH } from "./crypto"; import { bindingNameForShard, configuredShardCount } from "./git/artifacts-shards"; import { userContentOriginProblem } from "./usercontent-policy"; const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]); export type DeploymentVars = { GITFOR_LOCAL?: string; PUBLIC_URL?: string; USERCONTENT_ORIGIN?: string; SECRETS_KEY?: string; ARTIFACTS_SHARDS?: string; SITES_DOMAIN?: string; EDITOR_ORIGIN?: string; }; export function isLoopbackHost(host: string): boolean { const lower = host.toLowerCase(); return LOOPBACK_HOSTS.has(lower) || lower.endsWith(".localhost"); } /** True when PUBLIC_URL points at this machine; an unparsable value counts as local (never as a deployment). */ export function isLoopbackUrl(raw: string | undefined): boolean { try { return isLoopbackHost(new URL(raw ?? "").hostname); } catch { return true; } } /** * `vite dev` and `bun run preview` set GITFOR_LOCAL=true (vite.config.ts); `bun run deploy` never does. Dev-only * conveniences (first signup becomes admin, loopback integration sinks) also require a loopback PUBLIC_URL. */ export function isLocalDev(vars: DeploymentVars = env): boolean { return vars.GITFOR_LOCAL === "true" && isLoopbackUrl(vars.PUBLIC_URL); } /** * Why this worker must not serve, or an empty list. A deployed worker with a loopback or plain-http PUBLIC_URL, * no separate user-content origin or a missing SECRETS_KEY would quietly run with dev-grade security, so it * refuses instead (DEPLOY-P2). Shard bindings are checked everywhere (GIT-P8). */ export function deploymentProblems(vars: DeploymentVars, hasBinding: (name: string) => boolean): string[] { const problems: string[] = []; const shards = configuredShardCount(vars.ARTIFACTS_SHARDS); for (let shard = 0; shard < shards; shard++) { const name = bindingNameForShard(shard); if (!hasBinding(name)) problems.push(`ARTIFACTS_SHARDS=${shards} but the ${name} binding is missing`); } if (isLocalDev(vars)) return problems; if (vars.GITFOR_LOCAL === "true") problems.push("GITFOR_LOCAL=true is only valid with a loopback PUBLIC_URL"); let forge: URL | null = null; try { forge = new URL(vars.PUBLIC_URL ?? ""); } catch { problems.push("PUBLIC_URL is not set to a valid URL"); } if (forge && forge.protocol !== "https:") problems.push("PUBLIC_URL must use https"); if (forge && isLoopbackHost(forge.hostname)) problems.push("PUBLIC_URL must not be a loopback host"); const content = vars.USERCONTENT_ORIGIN?.trim() ?? ""; if (!content) problems.push("USERCONTENT_ORIGIN must be set to a separate origin"); else if (forge) { const problem = userContentOriginProblem(content, forge.hostname, { sites: vars.SITES_DOMAIN, editor: vars.EDITOR_ORIGIN, }); if (problem) problems.push(problem); } if ((vars.SECRETS_KEY ?? "").trim().length < MIN_SECRETS_KEY_LENGTH) { problems.push(`SECRETS_KEY must be set (at least ${MIN_SECRETS_KEY_LENGTH} characters)`); } return problems; } let cached: string[] | null = null; /** The isolate's deployment problems, computed and logged once (env is fixed for the isolate's lifetime). */ export function bootProblems(): string[] { if (cached) return cached; cached = deploymentProblems(env, (name) => Reflect.get(env, name) !== undefined); if (cached.length > 0) console.error("gitfor refuses to serve: misconfigured deployment", cached); return cached; } /** * A local-dev isolate serving a non-loopback host means a dev build (GITFOR_LOCAL baked in) was deployed, so it * gets the same refusal as a misconfigured deployment. */ export function requestProblems(request: Request, vars: DeploymentVars = env): string[] { if (!isLocalDev(vars) || isLoopbackHost(new URL(request.url).hostname)) return []; return ["a local-dev build (GITFOR_LOCAL=true) is serving a non-loopback host"]; } /** * The app itself answers only on the PUBLIC_URL host. Other hosts reach this Worker through the catch-all route on the * custom domains zone (a Cloudflare for SaaS hostname whose site is gone or unverified); they must not get the * forge. Local dev also answers on any loopback host. */ export function isForgeRequestHost(request: Request, vars: DeploymentVars = env): boolean { const host = new URL(request.url).hostname.toLowerCase(); if (isLocalDev(vars) && isLoopbackHost(host)) return true; try { return host === new URL(vars.PUBLIC_URL ?? "").hostname.toLowerCase(); } catch { return false; } } export function misconfiguredResponse(problems: string[]): Response { return new Response( `This gitfor deployment is misconfigured and is not serving requests.\n\n${problems.map((p) => `- ${p}`).join("\n")}\n`, { status: 503, headers: { "content-type": "text/plain; charset=utf-8", "retry-after": "300", "x-content-type-options": "nosniff", "cache-control": "no-store", }, }, ); }