38 lines · 1.7 KB
/** 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 });
}