Frame modes: srcdoc and url
What matters in the frame document is its CSP, because the worker
inherits it. The frame option says where that document comes from.
srcdoc: no server
createSandbox({ frame: { srcdoc: true } });The library writes the document inline. Its <meta> CSP applies (policies
intersect, so even if your page allows connect-src to your API, nothing
inside the frame can reach it). The catch: a srcdoc document also
inherits your page's CSP in full. So:
- No CSP on your page: works.
- A CSP with
'unsafe-inline'or'unsafe-eval'missing fromscript-src, or a nonce-onlyscript-src: the frame cannot boot.runresolvesSandboxUnavailable; nothing falls back to your page. - A CSP that does allow inline scripts and eval but has no
worker-src blob:(orchild-src blob:): the frame boots but the worker cannot start;runresolvesSandboxUnavailablewith that cause.
url: a static document you serve
createSandbox({ frame: { url: '/sandbox' } });The same document, served over HTTP with its own headers. Its CSP comes
from the header and inherits nothing from your page, so it works on pages
with a strict CSP. Your page needs one line: frame-src to wherever you
serve it ('self' if it is on your origin). The document is static and
cacheable (immutable); the library appends ?v=<hash> and checks the
protocol version in the handshake.
Serving it:
// Next.js: app/sandbox/route.ts
import { frameDocument, frameHeaders } from '@faustine-ai/browser-sandbox/frame';
export function GET() {
return new Response(frameDocument(), { headers: frameHeaders() });
}// Express
import { frameDocument, frameHeaders } from '@faustine-ai/browser-sandbox/frame';
app.get('/sandbox', (_req, res) => res.set(frameHeaders()).send(frameDocument()));# A static file: write it once at build time…
node -e "import('@faustine-ai/browser-sandbox/frame').then(m => process.stdout.write(m.frameDocument()))" > public/sandbox.html# …and serve it with the headers (nginx)
location = /sandbox.html {
add_header Content-Security-Policy "default-src 'none'; connect-src 'none'; img-src 'none'; script-src 'sha256-<hash>' 'unsafe-eval'; worker-src blob:; base-uri 'none'; form-action 'none'; frame-ancestors *" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy no-referrer always;
add_header Cache-Control "public, max-age=31536000, immutable" always;
}The exact header values, including the hash, are what frameHeaders()
returns; print them once rather than copying from here. 'unsafe-eval'
is required by the worker's AsyncFunction and is harmless in a document
that has no network and no origin.
srcdoc |
url |
|
|---|---|---|
| Install | npm i |
npm i + one static route |
| Network blocked inside | yes (<meta> CSP) |
yes (header CSP) |
| Page without a CSP | works | works |
| Page with a strict CSP | SandboxUnavailable |
works, add frame-src |
| Page with a lax CSP | needs worker-src blob: |
works |
| Cache / version | n/a | immutable, ?v=, checked in the handshake |
The library never falls from one mode to the other on its own.