173 lines · 8.0 KB
import { env, WorkerEntrypoint } from "cloudflare:workers";
import { repoDb } from "../scoped-db";
import { siteScope } from "./store";
import { decryptSecret } from "../crypto";
import { isForgeHostname, objectKey, SCRATCH_LIMITS, SITE_LIMITS, sitesDomain } from "./config";
import { isBlockedOutbound, sanitizeSetCookie, stripReservedCookies } from "./naming";
import { meterSiteCpu } from "./metering";
import { ensureSecurityHeaders, isUpgrade } from "./harden";
import { loadManifest, plain, serveStatic } from "./static";
import type { SiteManifest, SiteMeta } from "./config";

const COMPATIBILITY_DATE = "2026-10-01";
const MAX_BUNDLE_BYTES = 10 * 1024 * 1024;

type AssetProps = { deploymentId: string; spa: boolean };
type OutboundProps = { siteId: string };

/** `env.ASSETS` inside a dynamic site: static files of the same deployment, with no access to anything else. */
export class SiteAssets extends WorkerEntrypoint<Env, AssetProps> {
  override async fetch(request: Request): Promise<Response> {
    const manifest = await loadManifest(this.ctx.props.deploymentId);
    if (!manifest) return plain(503, "Deployment unavailable");
    return serveStatic(request, manifest, this.ctx.props.spa);
  }
}

/** The only network path out of a claimed site's isolate: no forge, no sites, no private or loopback addresses. */
export class SiteOutbound extends WorkerEntrypoint<Env, OutboundProps> {
  override async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const blocked = [new URL(env.PUBLIC_URL).hostname, sitesDomain()].filter((host): host is string => Boolean(host));
    if (isBlockedOutbound(url, blocked) || isForgeHostname(url.hostname)) {
      return plain(403, `Outbound request to ${url.hostname} is not allowed from sites`);
    }
    const headers = new Headers(request.headers);
    for (const name of [...headers.keys()]) {
      if (name.startsWith("cf-") || name.startsWith("x-forwarded-") || name === "x-real-ip") headers.delete(name);
    }
    headers.set("x-gitfor-site", this.ctx.props.siteId);
    return fetch(new Request(request, { headers, redirect: "manual" }));