You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The docs explain CSP setup (the webjs.csp config, the per-request nonce, cspNonce() for your own inline scripts) but say nothing about how the nonce behaves across a CLIENT-ROUTER SOFT NAVIGATION. That is the one genuinely subtle, security-relevant part of the story, and the exact "Turbo Drive nonce mismatch" that trips up developers on Rails/Turbo and Next. A user setting up strict CSP has no way, from the docs, to understand why a WebJs app does not break on soft nav, and might "fix" a non-problem (for example try to thread a per-request nonce into the client).
Two things are undocumented:
Enforcement vs carrier. WebJs enforces CSP via the HTTP Content-Security-Policy HEADER (so report-uri / frame-ancestors / report-only all work), and uses <meta name="csp-nonce"> purely as a client-side nonce CARRIER. It is NOT the flawed <meta http-equiv="Content-Security-Policy"> enforcement pattern. This distinction is what makes the meta-tag approach sound here.
Add a short "CSP and the client router" subsection to the security docs page (and a sentence to the built-ins reference), explaining: (a) enforcement is header-based, the meta is a nonce carrier; (b) on a soft nav the active document's original nonce is authoritative, and the router re-stamps dynamically-inserted scripts/preloads with it via getCspNonce(); (c) the server still mints a fresh nonce per request (including soft-nav responses), but the browser ignores a fetched response's CSP header for the live document, so the client owns "original nonce wins". Note this is the client-side analog of the Rails HTTP_X_TURBO_NONCE server-side workaround, and needs no user configuration.
Keep it accurate to the code, do NOT overstate: the meta is name=csp-nonce (carrier), the header is the enforcer.
Implementation notes (for the implementing agent)
Where to edit:
docs/app/docs/security/page.ts, the "Content-Security-Policy (opt-in, nonce-based)" section (around L47-49). Add a "CSP and the client router" subsection after it.
.agents/skills/webjs/references/built-ins.md, the "### CSP (opt-in, nonce)" section (around L135), add a sentence on soft-nav nonce preservation.
Grounding (cite these, do not re-derive): the enforcement header is built in packages/server/src/csp.js (buildCspHeader, cspHeaderName, mintNonce); the carrier meta is emitted in packages/server/src/ssr.js (around L1744, <meta name="csp-nonce" content=...>); the client reads it in packages/core/src/router-client.jsgetCspNonce() and re-stamps via cloneScriptWithCorrectNonce / cloneElementWithCorrectNonce; body scripts via reactivateScripts (around L4207); the head merges preserve the meta via the csp-nonce carve-outs added in fix: view-transition soft-nav meta leak and stuck Suspense skeleton #1049 (reconcileHeadMetas) and fix: preserve csp-nonce on mergeHead, warn on dropped streamed resolve #1052 (mergeHead); CSP pages are excluded from the HTML cache in packages/server/src/html-cache.js (if (guards.cspEnabled) return false).
Landmines: do not claim a WebJs app uses a <meta http-equiv> CSP (it does not); do not imply the soft-nav response's CSP header applies to the live document (it does not, that is the whole point). Keep the prose within invariant 11 (no em-dashes, no semicolon-as-pause; the block-prose-punctuation hook enforces this on new lines).
Invariants: docs-only change, no code. Follow the docs-site style (plain HTML in an html template, <h2> / <h3> / <p> / <code>). No sidebar entry needed (it is a subsection of an existing page).
Tests + docs surfaces: no test layer (docs-only). Both surfaces above must stay consistent; the docs-site /llms.txt regenerates automatically from the page.
Acceptance criteria
docs/app/docs/security/page.ts has a "CSP and the client router" subsection covering enforcement-vs-carrier and soft-nav nonce preservation
built-ins.md CSP section mentions the soft-nav nonce behavior
The prose is accurate to the code (the header enforces, name=csp-nonce carries, the original nonce wins on soft nav) and cites no behavior a WebJs app does not have
No em-dashes or banned pause punctuation (invariant 11)
Problem
The docs explain CSP setup (the
webjs.cspconfig, the per-request nonce,cspNonce()for your own inline scripts) but say nothing about how the nonce behaves across a CLIENT-ROUTER SOFT NAVIGATION. That is the one genuinely subtle, security-relevant part of the story, and the exact "Turbo Drive nonce mismatch" that trips up developers on Rails/Turbo and Next. A user setting up strict CSP has no way, from the docs, to understand why a WebJs app does not break on soft nav, and might "fix" a non-problem (for example try to thread a per-request nonce into the client).Two things are undocumented:
Content-Security-PolicyHEADER (soreport-uri/frame-ancestors/ report-only all work), and uses<meta name="csp-nonce">purely as a client-side nonce CARRIER. It is NOT the flawed<meta http-equiv="Content-Security-Policy">enforcement pattern. This distinction is what makes the meta-tag approach sound here.reconcileHeadMetas) and fix: preserve csp-nonce on mergeHead, warn on dropped streamed resolve #1052 (mergeHead).Design / approach
Add a short "CSP and the client router" subsection to the security docs page (and a sentence to the built-ins reference), explaining: (a) enforcement is header-based, the meta is a nonce carrier; (b) on a soft nav the active document's original nonce is authoritative, and the router re-stamps dynamically-inserted scripts/preloads with it via
getCspNonce(); (c) the server still mints a fresh nonce per request (including soft-nav responses), but the browser ignores a fetched response's CSP header for the live document, so the client owns "original nonce wins". Note this is the client-side analog of the RailsHTTP_X_TURBO_NONCEserver-side workaround, and needs no user configuration.Keep it accurate to the code, do NOT overstate: the meta is
name=csp-nonce(carrier), the header is the enforcer.Implementation notes (for the implementing agent)
docs/app/docs/security/page.ts, the "Content-Security-Policy (opt-in, nonce-based)" section (around L47-49). Add a "CSP and the client router" subsection after it..agents/skills/webjs/references/built-ins.md, the "### CSP (opt-in, nonce)" section (around L135), add a sentence on soft-nav nonce preservation.packages/server/src/csp.js(buildCspHeader,cspHeaderName,mintNonce); the carrier meta is emitted inpackages/server/src/ssr.js(around L1744,<meta name="csp-nonce" content=...>); the client reads it inpackages/core/src/router-client.jsgetCspNonce()and re-stamps viacloneScriptWithCorrectNonce/cloneElementWithCorrectNonce; body scripts viareactivateScripts(around L4207); the head merges preserve the meta via the csp-nonce carve-outs added in fix: view-transition soft-nav meta leak and stuck Suspense skeleton #1049 (reconcileHeadMetas) and fix: preserve csp-nonce on mergeHead, warn on dropped streamed resolve #1052 (mergeHead); CSP pages are excluded from the HTML cache inpackages/server/src/html-cache.js(if (guards.cspEnabled) return false).<meta http-equiv>CSP (it does not); do not imply the soft-nav response's CSP header applies to the live document (it does not, that is the whole point). Keep the prose within invariant 11 (no em-dashes, no semicolon-as-pause; the block-prose-punctuation hook enforces this on new lines).htmltemplate,<h2>/<h3>/<p>/<code>). No sidebar entry needed (it is a subsection of an existing page)./llms.txtregenerates automatically from the page.Acceptance criteria
docs/app/docs/security/page.tshas a "CSP and the client router" subsection covering enforcement-vs-carrier and soft-nav nonce preservationbuilt-ins.mdCSP section mentions the soft-nav nonce behaviorname=csp-noncecarries, the original nonce wins on soft nav) and cites no behavior a WebJs app does not have