57 lines · 2.4 KB
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:<port>` to
 * `usercontent.localhost:<port>` (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;