All Docs
FeaturesCalmony Sanctions MonitorUpdated March 12, 2026

Nonce-based Content-Security-Policy (SEC-11)

Nonce-based Content-Security-Policy (SEC-11)

Released in v0.1.108

The Sanctions Monitor platform enforces a strict Content-Security-Policy (CSP) that removes 'unsafe-inline' and 'unsafe-eval' from script-src. This is achieved through a per-request cryptographic nonce that flows from the Edge middleware through every server component that emits an inline <script> tag.


Architecture overview

Middleware (Edge)
  └─ crypto.getRandomValues() → 128-bit nonce
  └─ CSP header: script-src 'self' 'nonce-<value>'  ← no unsafe-inline, no unsafe-eval
  └─ x-nonce request header forwarded to SSR

Server Components
  └─ headers().get("x-nonce")
  └─ <script nonce={nonce}> on all inline scripts
  └─ ThemeProvider nonce={nonce}   ← colour-scheme injection
  └─ JsonLd nonce={nonce}          ← structured data scripts

Step-by-step flow

  1. Nonce generation — On every incoming request the Edge middleware calls crypto.getRandomValues() to produce a 128-bit (16-byte) cryptographically random value, Base64-encoded to a 24-character string.
  2. CSP header — The nonce is embedded in the Content-Security-Policy response header as 'nonce-<value>' inside script-src. No 'unsafe-inline' or 'unsafe-eval' tokens are present.
  3. Header forwarding — The nonce is also set as the x-nonce request header so Next.js server components can read it during SSR.
  4. Server component propagation — Each server component root calls headers().get("x-nonce") and stamps the value onto every <script nonce={nonce}> tag it renders.
  5. Browser enforcement — The browser compares each script's nonce attribute against the value in the CSP header and blocks any script that does not match.

CSP directives

DirectiveValueNotes
default-src'self'Restrictive baseline
script-src'self' 'nonce-<value>'No unsafe-inline, no unsafe-eval
style-src'self' 'unsafe-inline'Required by Tailwind v4 runtime injection
img-src'self' data: blob: https:Allows Base64 and external images
font-src'self' data:Allows embedded fonts
connect-src'self' https:Allows HTTPS fetch/XHR
frame-ancestors'self' + trusted originsRestricts embedding to known hosts
upgrade-insecure-requestsUpgrades HTTP sub-resources to HTTPS

Trusted frame-ancestors origins

  • https://agent-os-saa-s-factory.vercel.app
  • https://jugg.ai
  • https://*.vercel.app
  • http://localhost:3000

Components that carry the nonce

JsonLd component

The JsonLd component accepts an optional nonce prop:

// src/components/json-ld.tsx
export function JsonLd({
  schema,
  nonce,
}: {
  schema: Record<string, unknown> | Record<string, unknown>[];
  nonce?: string;
}) {
  return (
    <script
      type="application/ld+json"
      nonce={nonce}
      dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
    />
  );
}

Although <script type="application/ld+json"> is technically structured data rather than executable script, the CSP specification is ambiguous on this point. Chrome 95+ and Firefox 93+ have begun enforcing script-src rules on these tags. Carrying the nonce is zero-cost and future-proofs the component.

Root layout

// src/app/layout.tsx
const nonce = headersList.get("x-nonce") ?? "";
// ...
<JsonLd schema={organizationSchema} nonce={nonce} />

Home page

// src/app/page.tsx
export default async function Home() {
  const headersList = await headers();
  const nonce = headersList.get("x-nonce") ?? "";
  // ...
  <JsonLd schema={softwareApplicationSchema} nonce={nonce} />
  <JsonLd schema={faqSchema} nonce={nonce} />
}

What is NOT covered (known limitations)

style-src 'unsafe-inline'

Tailwind v4 injects utility class styles at runtime, which requires 'unsafe-inline' to remain in style-src. Eliminating this requires either:

  • Switching to a hash-based style-src where each injected style block is pre-hashed at build time, or
  • Producing a fully static CSS output so no runtime injection occurs.

This is tracked as a separate future enhancement and is outside the scope of SEC-11.


Testing

The file tests/lib/csp.test.ts (141 lines, new in v0.1.108) provides a unit test suite covering:

TestDescription
Nonce embeddingbuildCsp() embeds the supplied nonce in script-src
No unsafe-evalscript-src does not contain 'unsafe-eval'
No unsafe-inlinescript-src does not contain 'unsafe-inline'
'self' presentscript-src always includes 'self'
default-srcSet to 'self'
upgrade-insecure-requestsPresent in all policies
frame-ancestorsContains trusted origins; no wildcard allow-all
connect-srcAllows 'self' and https:
img-srcAllows data: and blob: URIs
font-srcAllows data: URIs
Nonce entropy128-bit Base64-encoded, 24 characters
Nonce uniqueness100 consecutive calls all produce unique values

Tests use Vitest/Node and operate on pure functions — no Edge runtime or Next.js internals are required.


Why experimental.nonce is not used

Older Next.js documentation references an experimental.nonce config option. This option does not exist in the current @types/next. The equivalent behaviour is achieved by:

  1. Reading headers().get("x-nonce") in each server component root.
  2. Stamping the value onto <script nonce={nonce}> tags manually.

This is the approach used throughout the codebase and is documented in next.config.ts.