Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .agents/skills/webjs/references/built-ins.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ On by default (`X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`,

Off by default. `{ "webjs": { "csp": true } }` enables a strict-dynamic + per-request nonce posture. An object form merges `directives` and supports `reportOnly`. Read the nonce with `cspNonce()` from `@webjsdev/core` to stamp your own inline `<script>`.

Enforcement is the HTTP `Content-Security-Policy` HEADER, never a `<meta http-equiv>` tag, so `frame-ancestors` / `report-uri` work. The emitted `<meta name="csp-nonce">` is only the client-side nonce CARRIER. Across a client-router soft navigation the ORIGINAL page-load nonce stays authoritative (the browser enforces the original document's CSP header, not the fetched response's fresh one), so the router preserves that meta and re-stamps every dynamically-inserted script / preload with the original nonce via `getCspNonce()`. The server still mints a fresh nonce per request, and CSP pages are excluded from the HTML cache so a nonce is never served stale. No client config is needed.

### Redirects, trailing-slash, basePath, allowed origins

```jsonc
Expand Down
4 changes: 4 additions & 0 deletions docs/app/docs/security/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export default function Security() {
<p>CSP is off by default and enabled with a <code>webjs.csp</code> key in <code>package.json</code> (<code>true</code> for a strict default policy, or an object to customize directives and toggle report-only). When enabled the server mints a fresh per-request CSPRNG nonce, stamps it on the inline boot script, the importmap, and the modulepreload hints, and emits a matching <code>Content-Security-Policy</code> header carrying that exact nonce. One value flows from mint to header, so there is no drift, and it changes every request.</p>
<p>To stamp the nonce on your own inline <code>&lt;script&gt;</code>, read it during SSR with <code>import { cspNonce } from '@webjsdev/core'</code>. For a strict <code>script-src 'self'</code> deploy with no CDN, pair CSP with <code>webjs vendor pin --download</code> so vendor bundles serve from your own origin. See <a href="/docs/configuration">Configuration</a> for the directive reference.</p>

<h3>CSP and the client router</h3>
<p>The policy is enforced by the <code>Content-Security-Policy</code> response HEADER, never by a <code>&lt;meta http-equiv&gt;</code> tag, so directives like <code>frame-ancestors</code> and <code>report-uri</code> that a meta-tag CSP cannot express work normally. The <code>&lt;meta name="csp-nonce"&gt;</code> tag WebJs emits is not an enforcement mechanism, it is a carrier: the client router reads the nonce from it to stamp any element it inserts.</p>
<p>This matters on a soft navigation. The browser keeps enforcing the ORIGINAL page load's CSP header, because a fetched response's CSP never binds to the already-live document, so the ORIGINAL nonce stays authoritative for the life of that document. The router therefore preserves that first <code>&lt;meta name="csp-nonce"&gt;</code> across every swap and re-stamps each script, module preload, and inline script it inserts with the original nonce (via <code>getCspNonce()</code>), even though the server minted a fresh nonce for the navigation response. A hard reload or a full page load is the harmless case: it creates a NEW document that enforces that response's own fresh nonce, so the nonce stays consistent either way. Nothing gets blocked, and this needs no configuration. It is the client-side equivalent of the Rails and Turbo reuse-the-original-nonce workaround. CSP pages are also excluded from the server HTML cache, so a nonce is never replayed stale.</p>
Comment thread
vivek7405 marked this conversation as resolved.

<h2>Secure headers and HSTS</h2>
<p>Every response carries a baseline of standard security headers, so a scaffolded app is not clickjackable or MIME-sniffable without a reverse proxy:</p>
<ul>
Expand Down
Loading