Cross-origin isolation
Vivari needs SharedArrayBuffer, which browsers only expose on a cross-origin
isolated page. That means serving your HTML documents (and the sw.js script)
with:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Under require-corp, every cross-origin subresource must opt in with
Cross-Origin-Resource-Policy (or CORS). Same-origin subresources are fine,
which is why Vivari self-hosts all of its workers and Wasm.
Iframes are the exception to "same-origin is fine". A nested document does
not inherit its embedder's policy: it must send require-corp (or
credentialless) on its own response, even when it is same-origin. Miss it and
the browser blocks the frame and renders its "<host> refused to connect" error
page — the same page you get from X-Frame-Options, which sends you looking for a
CSP that isn't there. So if you scope the headers by path, make sure every
document you iframe is inside the scoped paths.
Embedding on a host that sends no headers (Chromium only). COOP and COEP have to come from the top-level page, so Vivari in an iframe normally needs the host site to send them. In Chromium the frame can isolate itself instead, by sending this on its own documents:
Document-Isolation-Policy: isolate-and-require-corp
Other browsers ignore it and still need the host's COOP and COEP. The preview Service
Worker copies your page's Document-Isolation-Policy onto the preview documents it
serves to that page (previews on a separate preview origin get none, being
cross-origin anyway), so the page can still read and reload its preview iframe. Tell
it the value when you boot:
await Vivari.boot({ documentIsolationPolicy: "isolate-and-require-corp" });
The declared value is used as given, so it has to be the header the page is really
served with: a value the page doesn't send breaks the preview the same way a missing
one does. The option needs the sw.js from the same @vivari/core version; an older
copy ignores it and falls back to asking your server.
If you don't declare it, the Service Worker asks your server: it sends a HEAD to the
page's URL (without its query string) and reads the header off the response. That needs
your server to answer HEAD with the same header as GET and without a redirect, and
it costs one extra request each time the Service Worker starts. The Service Worker
keeps one value per origin, so every page on that origin that boots Vivari should
declare (or be served with) the same value.
Check at runtime:
import { isCrossOriginIsolated } from "@vivari/core";
console.log(isCrossOriginIsolated()); // must be true before Vivari.boot()
You don't have to check first: Vivari.boot() performs the same test and rejects
with VivariError("ERR_NOT_ISOLATED") before starting any workers. Calling
isCrossOriginIsolated() yourself is useful when you want to render a
"headers missing" state without attempting a boot at all.
Header recipes
Vite (dev + preview)
import { defineConfig } from "vite";
const isolation = {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
};
export default defineConfig({
server: { headers: isolation },
preview: { headers: isolation },
});
Cloudflare Pages (_headers)
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Netlify (_headers) / Nginx / Express
# Netlify _headers
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
# Nginx
add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;
// Express
app.use((_req, res, next) => {
res.set("Cross-Origin-Opener-Policy", "same-origin");
res.set("Cross-Origin-Embedder-Policy", "require-corp");
next();
});
Cross-origin isolation is contagious: a page is only isolated if its top-level document sends the headers. If you host a marketing page and an embed on the same origin, you can scope the headers to just the embed's path so the rest of the site stays free of CORP constraints. That's exactly what this project does; see Deployment.