Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123// The response headers a page that boots the runtime, and the runtime's own files, need.// COOP + COEP make the page cross-origin isolated (SharedArrayBuffer, which the sync// mailbox depends on); boot() refuses without them. COEP require-corp gates every// subresource, worker scripts included, on CORP. The runtime's Service Worker registers// at scope "/", so a worker script below the origin root needs Service-Worker-Allowed: /.//// Runs under Node, and as .ts directly (type stripping): erasable syntax only. The// request and response types are structural, so a consumer needs no @types/node, and// node:http, connect, Express and Vite's dev server all fit them.
export interface HeaderOptions { /** Send COOP/COEP for this path. Default: every path. */ isolate?: boolean | ((path: string) => boolean); /** Cross-Origin-Resource-Policy for every path, or false for none. A sandbox origin that * a coordinator on another site frames needs "cross-origin": the coordinator's COEP * require-corp refuses the frame otherwise. Default: "same-origin". */ resourcePolicy?: "same-origin" | "same-site" | "cross-origin" | false; /** Content-Security-Policy for this path, or false for none. Default: none, since the * site owns its policy; `contentSecurityPolicy()` builds one the runtime works under. */ csp?: string | false | ((path: string) => string | false); /** Paths of Service Worker scripts that register at scope "/" from below it (they get * Service-Worker-Allowed: /). Default: none. */ serviceWorkerAllowed?: (path: string) => boolean;}
export interface CspOptions { /** Allow inline scripts (an inline import map needs it). Default: false. */ inlineScripts?: boolean; /** Allow inline styles. Default: false. */ inlineStyles?: boolean; /** Let runtime.install() reach an npm registry from the page: `true` adds * https://registry.npmjs.org to connect-src, a string adds that origin instead. * Default: false, so a page that never installs packages keeps connect-src at 'self'. */ registry?: boolean | string; /** dev.nibling.npm.getClosure gateways the page's installs ask (boot's `npm.gateways`); * each one's origin joins connect-src. Default: none. */ npmGateways?: readonly string[]; /** Origins this page may frame besides its own, such as a coordinator's sandbox origins * (`https://*.sandbox.example`). Adds frame-src. Default: none, so frame-src falls back * to default-src 'self'. */ frames?: readonly string[]; /** Origins allowed to frame this page, such as the coordinator a sandbox origin serves. * Adds frame-ancestors; an empty list forbids framing. Default: none, so any origin may * frame the page (frame-ancestors has no default-src fallback). */ embedders?: readonly string[];}
/** A same-origin policy that leaves the runtime working: 'unsafe-eval' for CJS module * wrappers (new Function) and 'wasm-unsafe-eval' for compiling WebAssembly. connect-src * always carries data: and blob: — a guest's fetch("data:...") or fetch(blobUrl) never * reaches the network, but CSP still gates it on the scheme being allowed. The npm * registry joins connect-src only when `registry` asks for it, and framing in either * direction only when `frames` or `embedders` does. */export function contentSecurityPolicy(options: CspOptions = {}): string { const registry = options.registry === true ? "https://registry.npmjs.org" : options.registry || ""; const directives = [ "default-src 'self'", `script-src 'self'${options.inlineScripts ? " 'unsafe-inline'" : ""} 'unsafe-eval' 'wasm-unsafe-eval'`, options.inlineStyles ? "style-src 'self' 'unsafe-inline'" : "style-src 'self'", "worker-src 'self'", `connect-src ${sources(["'self'", "data:", "blob:", ...(registry ? [registry] : []), ...(options.npmGateways ?? []).map((g) => new URL(g).origin)])}`, ]; if (options.frames) directives.push(`frame-src ${sources(["'self'", ...options.frames])}`); if (options.embedders) directives.push(frameAncestors(options.embedders)); return directives.join("; ");}
/** The frame-ancestors directive alone: only `embedders` may frame the page, and an empty * list forbids framing. A page that sets no other policy can send it as its whole CSP. */export function frameAncestors(embedders: readonly string[]): string { return `frame-ancestors ${embedders.length ? sources(embedders) : "'none'"}`;}
/** A directive's source list. A source holding whitespace, `;` or `,` would end the * directive or the policy and smuggle in others, so it is refused. */function sources(list: readonly string[]): string { for (const source of list) { if (!source || /[\s;,]/.test(source)) throw new TypeError(`Not a CSP source: ${JSON.stringify(source)}`); } return list.join(" ");}
/** The headers for a response at `path` (a decoded URL pathname). */export function headersFor(path: string, options: HeaderOptions = {}): Record<string, string> { const headers: Record<string, string> = {}; const isolate = typeof options.isolate === "function" ? options.isolate(path) : options.isolate ?? true; if (isolate) { headers["Cross-Origin-Opener-Policy"] = "same-origin"; headers["Cross-Origin-Embedder-Policy"] = "require-corp"; } const resourcePolicy = options.resourcePolicy ?? "same-origin"; if (resourcePolicy) headers["Cross-Origin-Resource-Policy"] = resourcePolicy; const csp = typeof options.csp === "function" ? options.csp(path) : options.csp ?? false; if (csp) headers["Content-Security-Policy"] = csp; if (options.serviceWorkerAllowed?.(path)) headers["Service-Worker-Allowed"] = "/"; return headers;}
export interface RequestLike { url?: string;}
export interface ResponseLike { setHeader(name: string, value: string): unknown;}
export type Middleware = (req: RequestLike, res: ResponseLike, next: (err?: unknown) => void) => void;
/** Connect-style middleware that sets `headersFor()`'s headers on every response. */export function nibHeaders(options: HeaderOptions = {}): Middleware { return (req, res, next) => { let path: string; try { path = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname); } catch (err) { next(err); return; } for (const [name, value] of Object.entries(headersFor(path, options))) res.setHeader(name, value); next(); };}