{ "$schema": "https://create-turbo-stack.dev/schema/package-registry.json", "name": "security", "type": "registry:package", "description": "OWASP security headers, nonce-based CSP builder, and origin/CSRF guard.", "dependencies": [], "devDependencies": [], "registryDependencies": [ "crypto" ], "envVars": {}, "exports": [ ".", "./headers", "./csp", "./origin" ], "lib": [ "ES2022", "WebWorker" ], "environment": "universal", "build": "none", "categories": [ "foundation", "security" ], "docs": "OWASP OSHP security response headers, nonce-based CSP builder with strict-dynamic, and origin/CSRF guard. Backed by the Fetch API and Web Crypto (via the crypto package) — no runtime dependencies. Does not include rate limiting, auth, session management, or input sanitization.", "files": [ { "path": "src/index.ts", "type": "registry:source", "content": "export type { HSTSOptions, SecurityHeadersOptions } from \"./headers\";\nexport {\n getSecurityHeaders,\n applySecurityHeaders,\n stripFingerprintHeaders,\n} from \"./headers\";\n\nexport type { CSPSource, CSPDirectives } from \"./csp\";\nexport {\n CSP_HEADER,\n CSP_REPORT_ONLY_HEADER,\n buildCSP,\n generateNonce,\n strictCspWithNonce,\n} from \"./csp\";\n\nexport { isTrustedOrigin, assertTrustedOrigin } from \"./origin\";\n" }, { "path": "src/headers.ts", "type": "registry:source", "content": "// HSTS: OSHP explicitly recommends omitting `preload` by default.\n// hstspreload.org warns against including it without understanding the permanent consequences.\nconst HSTS_ONE_YEAR = 31_536_000;\n\n// Permissions-Policy: deny the most commonly abused sensor and capture APIs.\n// fullscreen and picture-in-picture are left open (*) to avoid breaking common UX.\nconst DEFAULT_PERMISSIONS_POLICY = [\n \"accelerometer=()\",\n \"camera=()\",\n \"display-capture=()\",\n \"encrypted-media=()\",\n \"geolocation=()\",\n \"gyroscope=()\",\n \"magnetometer=()\",\n \"microphone=()\",\n \"midi=()\",\n \"payment=()\",\n \"publickey-credentials-get=()\",\n \"screen-wake-lock=()\",\n \"usb=()\",\n \"web-share=()\",\n \"xr-spatial-tracking=()\",\n].join(\", \");\n\n// Headers that leak implementation details and should be removed in production.\n// Source: OWASP OSHP \"Headers to Remove\" list.\nconst FINGERPRINT_HEADERS = [\n \"Server\",\n \"X-Powered-By\",\n \"X-AspNet-Version\",\n \"X-AspNetMvc-Version\",\n \"X-Generator\",\n] as const;\n\nexport interface HSTSOptions {\n /** Seconds the browser should remember HTTPS-only. Default: 31536000 (1 year). */\n maxAge?: number;\n /** Apply HSTS to all subdomains. Default: true. */\n includeSubDomains?: boolean;\n /**\n * Request inclusion in browser preload lists.\n * WARNING: Permanent and very difficult to undo. Requires 2-year max-age\n * and all subdomains on HTTPS. Do not set without understanding the consequences.\n */\n preload?: boolean;\n}\n\nexport interface SecurityHeadersOptions {\n /**\n * Strict-Transport-Security configuration.\n * Set to false to omit (e.g., non-HTTPS environments, local dev).\n */\n hsts?: HSTSOptions | false;\n /**\n * X-Frame-Options value. Default: \"DENY\".\n * frame-ancestors in CSP supersedes this for modern browsers;\n * include both for compatibility with older browsers.\n * Set to false to omit.\n */\n frameOptions?: \"DENY\" | \"SAMEORIGIN\" | false;\n /**\n * Permissions-Policy header value.\n * Pass a custom string to override the default restrictive policy.\n * Set to false to omit.\n */\n permissionsPolicy?: string | false;\n /**\n * Cross-Origin-Embedder-Policy: require-corp.\n * Enables SharedArrayBuffer but requires all cross-origin resources to opt in\n * via CORS or CORP. Off by default — audit cross-origin dependencies first.\n */\n coep?: boolean;\n /** Additional headers merged into the response verbatim. */\n extra?: Record;\n}\n\nfunction buildHSTS(opts: HSTSOptions): string {\n const maxAge = opts.maxAge ?? HSTS_ONE_YEAR;\n const parts = [`max-age=${maxAge}`];\n if (opts.includeSubDomains !== false) parts.push(\"includeSubDomains\");\n if (opts.preload) parts.push(\"preload\");\n return parts.join(\"; \");\n}\n\n/**\n * Returns the OWASP OSHP recommended security headers as a plain object.\n * Does not include Content-Security-Policy — generate that separately with buildCSP / strictCspWithNonce.\n */\nexport function getSecurityHeaders(\n options: SecurityHeadersOptions = {},\n): Record {\n const headers: Record = {};\n\n if (options.hsts !== false) {\n headers[\"Strict-Transport-Security\"] = buildHSTS(\n options.hsts && typeof options.hsts === \"object\" ? options.hsts : {},\n );\n }\n\n // Prevent MIME-type sniffing attacks.\n headers[\"X-Content-Type-Options\"] = \"nosniff\";\n\n // Clickjacking protection — still needed for browsers without frame-ancestors CSP support.\n if (options.frameOptions !== false) {\n headers[\"X-Frame-Options\"] = options.frameOptions ?? \"DENY\";\n }\n\n // Disable the legacy XSS Auditor. OSHP recommends setting this to 0:\n // the auditor causes information leakage and modern browsers have removed it.\n headers[\"X-XSS-Protection\"] = \"0\";\n\n // Strip path/query from the Referer header on cross-origin requests.\n headers[\"Referrer-Policy\"] = \"strict-origin-when-cross-origin\";\n\n // Isolates browsing context to same-origin. Mitigates window.opener attacks and XS-Leaks.\n headers[\"Cross-Origin-Opener-Policy\"] = \"same-origin\";\n\n // Prevents other origins from loading this resource (Spectre mitigation).\n headers[\"Cross-Origin-Resource-Policy\"] = \"same-origin\";\n\n // Restrict Adobe Flash / Acrobat cross-domain policy file access.\n headers[\"X-Permitted-Cross-Domain-Policies\"] = \"none\";\n\n if (options.permissionsPolicy !== false) {\n headers[\"Permissions-Policy\"] =\n options.permissionsPolicy ?? DEFAULT_PERMISSIONS_POLICY;\n }\n\n // COEP is opt-in: enabling it without auditing cross-origin dependencies breaks integrations.\n if (options.coep) {\n headers[\"Cross-Origin-Embedder-Policy\"] = \"require-corp\";\n }\n\n if (options.extra) {\n for (const [name, value] of Object.entries(options.extra)) {\n headers[name] = value;\n }\n }\n\n return headers;\n}\n\n/**\n * Applies OWASP security headers to a Response, returning a new Response.\n * The original Response is not mutated. Framework-agnostic: works in edge middleware,\n * route handlers, or service workers.\n */\nexport function applySecurityHeaders(\n response: Response,\n options?: SecurityHeadersOptions,\n): Response {\n const securityHeaders = getSecurityHeaders(options);\n const next = new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n for (const [name, value] of Object.entries(securityHeaders)) {\n next.headers.set(name, value);\n }\n return next;\n}\n\n/**\n * Removes server-fingerprinting headers from a Response, returning a new Response.\n * Strips: Server, X-Powered-By, X-AspNet-Version, X-AspNetMvc-Version, X-Generator.\n */\nexport function stripFingerprintHeaders(response: Response): Response {\n const next = new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers: response.headers,\n });\n for (const name of FINGERPRINT_HEADERS) {\n next.headers.delete(name);\n }\n return next;\n}\n" }, { "path": "src/csp.ts", "type": "registry:source", "content": "import { randomBytes, toBase64url } from \"{{scope}}/crypto\";\n\nexport const CSP_HEADER = \"Content-Security-Policy\";\nexport const CSP_REPORT_ONLY_HEADER = \"Content-Security-Policy-Report-Only\";\n\n/**\n * A single CSP source expression.\n * Examples: \"'self'\", \"'none'\", \"'strict-dynamic'\", \"'nonce-abc123'\",\n * \"https://cdn.example.com\", \"'sha256-xyz...'\".\n */\nexport type CSPSource = string;\n\n/**\n * Typed CSP directive map. Boolean directives (upgrade-insecure-requests,\n * block-all-mixed-content) are set with `true`; `false` or absence omits them.\n */\nexport interface CSPDirectives {\n \"default-src\"?: CSPSource[];\n \"script-src\"?: CSPSource[];\n \"style-src\"?: CSPSource[];\n \"img-src\"?: CSPSource[];\n \"connect-src\"?: CSPSource[];\n \"font-src\"?: CSPSource[];\n \"media-src\"?: CSPSource[];\n \"object-src\"?: CSPSource[];\n \"frame-src\"?: CSPSource[];\n \"worker-src\"?: CSPSource[];\n \"child-src\"?: CSPSource[];\n \"manifest-src\"?: CSPSource[];\n \"frame-ancestors\"?: CSPSource[];\n \"base-uri\"?: CSPSource[];\n \"form-action\"?: CSPSource[];\n sandbox?: CSPSource[];\n \"report-uri\"?: string[];\n \"report-to\"?: string[];\n \"upgrade-insecure-requests\"?: boolean;\n \"block-all-mixed-content\"?: boolean;\n}\n\n/**\n * Serializes a CSPDirectives object to a Content-Security-Policy header value string.\n * Directive ordering follows insertion order of the object.\n */\nexport function buildCSP(directives: CSPDirectives): string {\n const parts: string[] = [];\n\n for (const key of Object.keys(directives) as Array) {\n const value = directives[key];\n if (value === undefined) continue;\n if (typeof value === \"boolean\") {\n if (value) parts.push(key);\n continue;\n }\n if (value.length > 0) {\n parts.push(`${key} ${value.join(\" \")}`);\n }\n }\n\n return parts.join(\"; \");\n}\n\n/**\n * Generates a cryptographically strong nonce for use in a CSP script-src directive.\n * Returns a base64url-encoded string (no padding). Each page load should get a fresh nonce.\n *\n * Uses randomBytes from @scope/crypto (CSPRNG via getRandomValues) — 16 bytes = 128 bits entropy.\n */\nexport function generateNonce(): string {\n return toBase64url(randomBytes(16));\n}\n\n/**\n * Builds a strict nonce-based CSP policy following the OWASP CSP Cheat Sheet.\n *\n * Why nonce + strict-dynamic?\n * Static source allowlists are fragile: a single compromised CDN or inline script\n * bypasses the policy entirely. Nonces cryptographically bind each allowed script\n * to the server response that generated it. `strict-dynamic` propagates trust to\n * scripts loaded by nonce-trusted scripts, enabling module-based applications\n * without listing every source. `unsafe-inline` and `unsafe-eval` are excluded.\n *\n * @param nonce - The nonce generated by generateNonce() for this response.\n * @param extra - Optional overrides/additions merged on top of the strict default.\n * Passing a key overrides the default value for that directive.\n */\nexport function strictCspWithNonce(\n nonce: string,\n extra?: CSPDirectives,\n): string {\n const base: CSPDirectives = {\n \"default-src\": [\"'self'\"],\n \"script-src\": [`'nonce-${nonce}'`, \"'strict-dynamic'\"],\n \"style-src\": [\"'self'\"],\n \"img-src\": [\"'self'\", \"data:\"],\n \"connect-src\": [\"'self'\"],\n \"font-src\": [\"'self'\"],\n \"object-src\": [\"'none'\"],\n \"base-uri\": [\"'none'\"],\n \"form-action\": [\"'self'\"],\n \"frame-ancestors\": [\"'none'\"],\n \"upgrade-insecure-requests\": true,\n };\n\n return buildCSP({ ...base, ...extra });\n}\n" }, { "path": "src/origin.ts", "type": "registry:source", "content": "// Methods that mutate server state and require origin verification.\n// GET, HEAD, OPTIONS are excluded — they are safe and should not require CSRF protection.\nconst MUTATING_METHODS = new Set([\"POST\", \"PUT\", \"PATCH\", \"DELETE\"]);\n\n/**\n * Returns true if the request's origin is trusted.\n *\n * For non-mutating methods (GET, HEAD, OPTIONS) this always returns true —\n * CSRF attacks require state-changing requests.\n *\n * For mutating methods:\n * - A missing Origin header is rejected. Modern browsers always send Origin on\n * cross-site fetches; its absence on a mutating request is suspicious.\n * - Same-origin requests (Origin matches request URL's origin) are accepted.\n * - Origins in the explicit allowlist are accepted.\n * - Everything else is rejected.\n *\n * @param request - The incoming Fetch API Request.\n * @param allowedOrigins - Optional list of trusted cross-origins (e.g. [\"https://app.example.com\"]).\n */\nexport function isTrustedOrigin(\n request: Request,\n allowedOrigins?: string[],\n): boolean {\n if (!MUTATING_METHODS.has(request.method.toUpperCase())) return true;\n\n const origin = request.headers.get(\"Origin\");\n if (!origin) return false;\n\n const url = new URL(request.url);\n const requestOrigin = `${url.protocol}//${url.host}`;\n\n if (origin === requestOrigin) return true;\n if (allowedOrigins && allowedOrigins.includes(origin)) return true;\n\n return false;\n}\n\n/**\n * Returns a 403 Response if the request's origin is not trusted, null otherwise.\n * Designed for early-return guards at the top of route handlers or middleware:\n *\n * ```ts\n * const guard = assertTrustedOrigin(request);\n * if (guard) return guard;\n * ```\n *\n * @param request - The incoming Fetch API Request.\n * @param allowedOrigins - Optional list of trusted cross-origins.\n */\nexport function assertTrustedOrigin(\n request: Request,\n allowedOrigins?: string[],\n): Response | null {\n if (isTrustedOrigin(request, allowedOrigins)) return null;\n return new Response(\"Forbidden\", { status: 403 });\n}\n" } ], "checksum": "sha256-cfe00055060605fc27fea96405034816d18b9c77b0c4a774a07203a00e479357" }