/** Response headers the previewed (user-controlled) app may set on the editor origin; everything else is dropped. */ const PREVIEW_RESPONSE_HEADERS = new Set([ "content-type", "content-length", "content-encoding", "cache-control", "etag", "last-modified", "vary", ]); const HTTP_REDIRECT_MIN = 300; const HTTP_REDIRECT_MAX = 400; /** * Re-emits a container response with an allow-listed header set: no cookies, CORS, `Clear-Site-Data`, * `Link` preloads or service-worker scope can reach the editor origin, and redirects stay inside the * preview prefix (cross-origin `Location`s are dropped rather than followed by the browser). */ export function previewResponse(upstream: Response, prefix: string): Response { const headers = new Headers(); for (const [name, value] of upstream.headers) { if (PREVIEW_RESPONSE_HEADERS.has(name.toLowerCase())) headers.set(name, value); } const location = upstream.headers.get("location"); if (upstream.status >= HTTP_REDIRECT_MIN && upstream.status < HTTP_REDIRECT_MAX && location) { const resolved = new URL(location, "http://container/"); if (resolved.host === "container") { headers.set("location", `${prefix}${resolved.pathname.replace(/^\//, "")}${resolved.search}`); } } headers.set("content-security-policy", "sandbox allow-scripts allow-forms allow-popups allow-modals"); headers.set("cache-control", "no-store"); headers.set("x-content-type-options", "nosniff"); headers.set("referrer-policy", "no-referrer"); headers.set("cross-origin-resource-policy", "same-origin"); return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers }); }