/** 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 });
}