import type { SiteConfig } from '../content/types.ts'; export const PERMISSIONS_POLICY = 'camera=(), microphone=(), geolocation=(), payment=(), usb=(), accelerometer=(), gyroscope=(), magnetometer=()' as const; const jsonLdHashPattern = /^sha256-[A-Za-z0-9+/]{43}=$/u; export interface SecurityHeaderMap { csp: string; contentTypeOptions: 'nosniff'; referrerPolicy: 'no-referrer'; permissionsPolicy: typeof PERMISSIONS_POLICY; frameOptions: 'DENY'; cacheControl: 'private, no-store' | 'public, max-age=0, must-revalidate'; hsts?: 'max-age=31536000'; xRobotsTag?: 'noindex, nofollow, noarchive'; } const assertJsonLdCspHash = (value: string): void => { if (!jsonLdHashPattern.test(value)) { throw new Error('JSON-LD CSP hash must be one canonical SHA-256 base64 expression.'); } }; export const buildContentSecurityPolicy = (jsonLdCspHash: string): string => { assertJsonLdCspHash(jsonLdCspHash); return [ "default-src 'self'", `script-src 'self' '${jsonLdCspHash}'`, "style-src 'self'", "img-src 'self' data:", "media-src 'self'", "font-src 'self'", "connect-src 'self'", "object-src 'none'", "base-uri 'none'", "frame-ancestors 'none'", "form-action 'self'", 'upgrade-insecure-requests', ].join('; '); }; export const createSecurityHeaders = (site: Readonly, jsonLdCspHash: string): SecurityHeaderMap => { const common = { csp: buildContentSecurityPolicy(jsonLdCspHash), contentTypeOptions: 'nosniff' as const, referrerPolicy: 'no-referrer' as const, permissionsPolicy: PERMISSIONS_POLICY, frameOptions: 'DENY' as const, }; return site.environment === 'preview' ? { ...common, cacheControl: 'private, no-store', xRobotsTag: 'noindex, nofollow, noarchive', } : { ...common, cacheControl: 'public, max-age=0, must-revalidate', hsts: 'max-age=31536000', }; }; export const serializeStaticHostHeaders = (site: Readonly, jsonLdCspHash: string): string => { const headers = createSecurityHeaders(site, jsonLdCspHash); return [ '/*', ` Content-Security-Policy: ${headers.csp}`, ` X-Content-Type-Options: ${headers.contentTypeOptions}`, ` Referrer-Policy: ${headers.referrerPolicy}`, ` Permissions-Policy: ${headers.permissionsPolicy}`, ` X-Frame-Options: ${headers.frameOptions}`, ` Cache-Control: ${headers.cacheControl}`, ...(headers.hsts === undefined ? [] : [` Strict-Transport-Security: ${headers.hsts}`]), ...(headers.xRobotsTag === undefined ? [] : [` X-Robots-Tag: ${headers.xRobotsTag}`]), '', ].join('\n'); };