Features

Routes that need a no-inline Content Security Policy can opt into FaceTheory’s strict CSP path: no inline scripts, no inline styles, no raw head HTML, with hydration data moved to a same-origin sidecar instead of an inline __FACETHEORY_DATA__ block.

Opting in

Set FaceRenderResult.csp to disable the inline channels:

return {
  html: '<h1>Hello</h1>',
  csp: {
    inlineScripts: false,
    inlineStyles: false,
    rawHead: false,
  },
  hydration: externalHydrationForEntry(
    manifest,
    'src/entry-client.ts',
    data,
    { dataUrl: '/hydration/hello.json' },
  ),
};

When csp.inlineScripts === false, FaceTheory refuses to emit inline <script> bodies. Hydration must be external already or be externalized through a configured framework-owned path (SSR hydration sidecars, SSG build sidecars, or ISR sidecars); otherwise inline or Vite hydration fails closed.

Building the CSP header

import { buildStrictCspHeader, createCspNonce } from '@theory-cloud/facetheory';

const nonce = createCspNonce();
const cspHeader = buildStrictCspHeader({ cspNonce: nonce });

buildStrictCspHeader emits the canonical strict directive set:

default-src 'self'
base-uri 'self'
object-src 'none'
frame-ancestors 'none'
script-src 'self'
style-src 'self'
img-src 'self' data:
font-src 'self'
connect-src 'self'
form-action 'self'

Nonces are unique per response and consistent within a response so that <script nonce> attributes match the Content-Security-Policy header.

Document validation

For defense in depth, FaceTheory can validate the rendered document against the policy before emitting it:

import {
  validateStrictCspDocument,
  requiresStrictCspDocumentValidation,
} from '@theory-cloud/facetheory';

if (requiresStrictCspDocumentValidation(policy)) {
  validateStrictCspDocument(html, { policy });
}

The validator throws on inline <script> bodies, inline style attributes, and raw head HTML that the policy forbids.

Sidecar hydration

Strict CSP requires hydration data, when present, to be external. Use one of:

  • Framework-owned same-origin sidecars — configure createFaceApp({ ssrHydrationSidecars }) and return normal viteHydrationForEntry() data from the SSR Face. FaceTheory writes the exact render-time payload once before emitting a same-origin /_facetheory/ssr-data/... link.
  • Caller-managed external sidecars — use externalHydrationForEntry() when the host owns the same-origin JSON URL.

See SSR hydration sidecars for the full path.

Examples in the repo

  • ts/examples/vite-strict-csp-svelte/ — strict CSP delivery with Svelte + Vite