1// What the sandbox a listener's song plays in may do (sandbox.mjs), as 2// headers: one source for every place that serves it. 3// 4// - `pages/jev/sandbox.astro` puts the policy in a meta tag, so it holds on 5// any static host. 6// - The build writes `_headers` (astro.config.mjs, `jevHeaders`), which 7// Cloudflare applies to the deploy's static files: the page gets the 8// policy as a header too, with `sandbox` (so even opened on its own, the 9// page has an opaque origin) and `frame-ancestors` (only this site may 10// frame it), which a meta tag cannot carry. 11// - The dev server sends the same headers (astro.config.mjs, 12// `jevSandboxDev`). 13// 14// The frame has an opaque origin, so everything it loads from the site is 15// a cross-origin request with `Origin: null`: its scripts (module scripts 16// and AudioWorklets are fetched with CORS) and the sample pack. Those 17// public files are served with `Access-Control-Allow-Origin: *`, which 18// sends no credentials. 19 20// Hosts songs load sounds from: Strudel's CDN (the default banks, 21// prebakeSounds.mjs), GitHub's raw files (`samples('github:…')`, as the 22// site's songs load tidalcycles/dirt-samples), and the soundfonts'. 23export const SAMPLE_HOSTS = [ 24 'https://strudel.b-cdn.net', 25 'https://raw.githubusercontent.com', 26 'https://felixroos.github.io/webaudiofontdata/', 27]; 28 29export const SANDBOX_PAGE = '/jev/sandbox/'; 30 31// `meta`: the policy as a meta tag may hold it (no `sandbox`, 32// `frame-ancestors`); otherwise the header's. 33export function sandboxCsp({ meta = false } = {}) { 34 const directives = [ 35 ...(meta ? [] : ['sandbox allow-scripts']), 36 "default-src 'none'", 37 // the site's own scripts; a song is code, evaluated (unsafe-eval), and 38 // AudioWorklets load from blob: (the meter) and data: (superdough's dough 39 // worklet in dev); their bundled file is 'self' in production 40 "script-src 'self' 'unsafe-eval' blob: data:", 41 `connect-src 'self' ${SAMPLE_HOSTS.join(' ')}`, 42 "style-src 'unsafe-inline'", 43 "img-src 'none'", 44 "media-src 'none'", 45 "font-src 'none'", 46 "object-src 'none'", 47 "frame-src 'none'", 48 "manifest-src 'none'", 49 // AudioWorklet module scripts are governed by worker-src 50 "worker-src 'self' blob: data:", 51 "form-action 'none'", 52 // the page sets <base href="/"> so a song's root-relative sample URL 53 // resolves to the site root, as it does on the main page; 'self' is the 54 // origin the frame was served from (the site), not its opaque runtime 55 // origin, so the tag is allowed but cannot be pointed off-site 56 "base-uri 'self'", 57 ...(meta ? [] : ["frame-ancestors 'self'"]), 58 ]; 59 return directives.join('; '); 60} 61 62export const sandboxHeaders = () => ({ 63 'Content-Security-Policy': sandboxCsp(), 64 'Referrer-Policy': 'no-referrer', 65 'X-Content-Type-Options': 'nosniff', 66}); 67 68// Paths the sandbox loads from the site, served to it with CORS. 69export const CORS_PATHS = ['/_astro/*', '/jev-samples/*']; 70 71// The deploy's `_headers` (Workers static assets). 72export function headersFile() { 73 const block = (path, headers) => [path, ...Object.entries(headers).map(([k, v]) => ` ${k}: ${v}`)].join('\n'); 74 return [ 75 '# Written by the build (website/src/jev/sandboxPolicy.mjs); do not edit.', 76 block(`${SANDBOX_PAGE}*`, sandboxHeaders()), 77 ...CORS_PATHS.map((path) => block(path, { 'Access-Control-Allow-Origin': '*' })), 78 '', 79 ].join('\n\n'); 80}