import { env } from "cloudflare:workers"; import { userContentOriginProblem, userContentOriginWarning } from "./usercontent-policy"; const DEV_FORGE_HOST = "localhost"; const DEV_USERCONTENT_HOST = "usercontent.localhost"; let validated: { raw: string; url: URL | null } | null = null; /** The configured user-content origin, validated once per isolate; an unsafe value disables the split. */ function configured(): URL | null { if (!env.USERCONTENT_ORIGIN) return null; if (validated?.raw === env.USERCONTENT_ORIGIN) return validated.url; const problem = userContentOriginProblem(env.USERCONTENT_ORIGIN, new URL(env.PUBLIC_URL).hostname, { sites: env.SITES_DOMAIN, editor: env.EDITOR_ORIGIN, }); if (problem) console.error(`User-content origin disabled: ${problem}`); const warning = problem ? null : userContentOriginWarning(env.USERCONTENT_ORIGIN, new URL(env.PUBLIC_URL).hostname); if (warning) console.warn(warning); const url = problem ? null : new URL(env.USERCONTENT_ORIGIN); validated = { raw: env.USERCONTENT_ORIGIN, url }; return url; } function misconfigured(): boolean { return Boolean(env.USERCONTENT_ORIGIN) && configured() === null; } /** * User-supplied bytes are served from their own origin, like GitHub's `githubusercontent.com`, so a hostile * file, SVG or content-type confusion can never run with forge cookies or same-origin access. Production sets * USERCONTENT_ORIGIN on a separate registrable domain; local dev maps `localhost:` to * `usercontent.localhost:` (host-only cookies are never sent there). Null means "serve from the forge". */ export function userContentOriginFor(forgeUrl: URL): string | null { const explicit = configured(); if (explicit) return explicit.origin; if (misconfigured() || forgeUrl.hostname !== DEV_FORGE_HOST) return null; const url = new URL(forgeUrl.origin); url.hostname = DEV_USERCONTENT_HOST; return url.origin; } export function isUserContentOrigin(url: URL): boolean { const explicit = configured(); if (explicit) return url.origin === explicit.origin; return !misconfigured() && url.hostname === DEV_USERCONTENT_HOST; } /** The forge origin whose pages embed user content, for CORS on fetches that follow a redirect across. */ export function forgeOriginFor(contentUrl: URL): string { if (configured()) return new URL(env.PUBLIC_URL).origin; const url = new URL(contentUrl.origin); url.hostname = DEV_FORGE_HOST; return url.origin; }