@block65/csp
v0.2.0
Published
Composable Content-Security-Policy builder with presets and deterministic output
Maintainers
Readme
@block65/csp
Content-Security-Policy builder: policies are plain objects, composeCsp merges
them and serialises the header. Node 26+, browsers with Uint8Array base64,
Cloudflare Workers.
Install
pnpm add @block65/cspUsage
composeCsp(...parts) unions each directive across parts, deduped and sorted.
import { composeCsp, sameOrigin, sentrySdk, strict } from "@block65/csp";
composeCsp(
strict(),
sameOrigin(),
sentrySdk({ dsn: "https://[email protected]/1", replay: true }),
);
// base-uri 'none'; connect-src 'self' https://o123.ingest.us.sentry.io; …Presets
Each returns Directives.
strict()sameOrigin()googleFonts()googleMaps({ nonce: string | true })orgoogleMaps({ nonce: false, allowUnsafeScripts: true })sentrySdk({ dsn: string, replay?: boolean })stripe({ paymentFields?: boolean, cardChallenges?: boolean, checkout?: boolean })cloudflareTurnstile()cloudflareInsights()maplibre({ tiles: readonly string[], worker: "blob" | { url: string } })viteDevServer()— empty
A string nonce puts the nonce on googleMaps script-src and style-src,
and 'strict-dynamic' on script-src. Non-nonce mode requires
googleMaps({ nonce: false, allowUnsafeScripts: true }) to acknowledge
'unsafe-inline' and 'unsafe-eval' permissions across the composed policy.
This is enforced by both TypeScript and runtime checks.
'none'
Dropped from any directive that also has a real source.
composeCsp({ scriptSrc: ["'none'"] }, { scriptSrc: ["https://example.com"] });
// script-src https://example.comremove
Deletes a directive an earlier part set; a later part can set it again.
import { composeCsp, remove, strict } from "@block65/csp";
composeCsp(strict(), {
upgradeInsecureRequests: remove,
frameAncestors: remove,
});MapLibre worker variants
Choose the mode that matches the MapLibre build and worker configuration:
// A build that creates blob workers.
maplibre({ tiles: ["https://tiles.example.com"], worker: "blob" });
// A same-origin worker configured with setWorkerUrl in the application.
maplibre({
tiles: ["https://tiles.example.com"],
worker: { url: "https://app.example.com/maplibre-gl-worker.mjs" },
});The worker choice is required. Neither variant adds child-src, so it does not
also permit blob frames. sameOrigin() adds 'self' to worker-src; omit or
remove that directive before composing URL mode if you want only the named worker.
CSP path restrictions do not constrain a redirect destination's path.
Nonces
generateNonce(byteLength = 16) per response, nonce(value) for the source.
import {
composeCsp,
generateNonce,
nonce,
sameOrigin,
strict,
} from "@block65/csp";
const value = generateNonce();
const policy = composeCsp(strict(), sameOrigin(), {
scriptSrc: [nonce(value)],
});Hashes
hash(content, algorithm = "SHA-256") takes the element's exact text content.
import { composeCsp, hash } from "@block65/csp";
composeCsp({ scriptSrc: [await hash('console.log("hi")')] });
// script-src 'sha256-…'Reporting
reportTo names an endpoint from the Reporting-Endpoints header; reportUri
covers browsers without report-to.
import { buildReportingEndpoints, composeCsp, strict } from "@block65/csp";
const endpoint = "https://example.com/csp-reports";
new Response(body, {
headers: {
"reporting-endpoints": buildReportingEndpoints({ csp: endpoint }),
"content-security-policy": composeCsp(strict(), {
reportTo: "csp",
reportUri: endpoint,
}),
},
});Send the policy as content-security-policy-report-only to collect violations
without enforcing it.
Documentation audit
Reviewed against the CSP specifications and vendor documentation on 2026-09-16.
Every directive in lib/types.ts and every preset has a documentation URL in its
JSDoc. The presets are additive building blocks; compose sameOrigin() when
local resources are needed.
| Preset | Reference and scope |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| strict() | CSP3: blocks resource loads, framing, forms and base URL changes; does not enable sandbox. |
| sameOrigin() | Source lists: allows the listed resource types from the document origin. |
| googleFonts() | Google Fonts: hosted CSS and font files. |
| googleMaps() | Maps CSP: nonce mode deliberately omits the example's unsafe-eval and legacy script fallbacks. Nonce the initial script and a style element; nonce: true requires supplying both policy nonces yourself. |
| sentrySdk() | Sentry CSP: bundled SDK and DSN ingestion; CDN loaders and tunnels need separate sources. |
| stripe() | Stripe CSP: Stripe.js and optional Checkout. Address Element with your own Maps key, Link, custom font stylesheets, Connect and crypto have additional requirements. |
| cloudflareTurnstile() | Turnstile CSP: pre-clearance also needs connect-src 'self'. |
| cloudflareInsights() | Web Analytics CSP: manual beacon; automatic injection also needs connect-src 'self'. |
| maplibre() | MapLibre CSP: explicit blob or URL worker mode. URL mode allows only the supplied script path. Include style, tile, glyph and sprite fetch origins in tiles. |
| viteDevServer() | Vite CSP: an empty extension point. Configure nonces, HMR connections and asset sources in the application. |
strict-dynamic affects the whole composed script policy: parser-inserted scripts
need nonces even when another preset allows their host. Explicit scriptSrcElem
or styleSrcElem directives also override the corresponding general directive.
Broad permissions that remain are intentional: Google's service wildcards follow
its Maps guide, and Stripe's *.js.stripe.com supports dynamically assigned
frame origins. Checkout's image wildcard follows Stripe's requirements and is
only added with checkout: true. Cloudflare Analytics is restricted to the beacon
script and ingestion paths. Blob worker permission requires an explicit choice
in MapLibre; Sentry adds it only with replay: true. Google Maps includes it as
part of its documented renderer requirements.
This review verifies documented rules, not live integration behavior. Source validation prevents header delimiter injection; it is not a complete CSP grammar or browser compatibility validator. Test the composed policy with the SDK versions and features your application uses.
License
MIT
