/** * A single HTTP header, in the shape Next.js next.config headers() expects. */ export type SecurityHeader = { key: string; value: string } export type BuildSecurityHeadersArgs = { /** * Extra headers to append or override. Same-key entries replace the default, * so you can e.g. add your project's Content-Security-Policy here — CSP is * intentionally NOT a default because it depends on the project's own * domains (scripts, images, fonts, analytics). Keep CSP in your project. */ additional?: SecurityHeader[] /** * X-Frame-Options value. 'DENY' (default) blocks all framing; 'SAMEORIGIN' * allows same-origin framing. Note: CSP frame-ancestors supersedes this in * modern browsers, but X-Frame-Options is kept for older ones. Set to null * to omit (e.g. if you set frame-ancestors in your project CSP). */ frameOptions?: 'DENY' | 'SAMEORIGIN' | null /** * Enable HSTS (Strict-Transport-Security). Only takes effect over HTTPS, and * tells browsers to force HTTPS for `maxAge` seconds. Default true. Turn OFF * in local/dev over plain HTTP, or you may lock the browser to https on * localhost. Set the env guard in your next.config (see docs). */ hsts?: boolean /** Add includeSubDomains to HSTS. Default true. */ hstsIncludeSubDomains?: boolean /** HSTS max-age in seconds. Default 63072000 (2 years), the common baseline. */ hstsMaxAge?: number /** Add preload to HSTS (only if you'll submit to the preload list). Default false. */ hstsPreload?: boolean /** * Permissions-Policy. Default disables camera, microphone, geolocation. Pass * your own string to override, or null to omit. */ permissionsPolicy?: null | string /** Referrer-Policy. Default 'strict-origin-when-cross-origin' (browser default, explicit). */ referrerPolicy?: null | string } /** * Builds the generic, project-independent security headers every site should * send: HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, * Permissions-Policy. These are identical across projects, so the plugin owns * the boilerplate; the client spreads the result into next.config's headers(). * * Content-Security-Policy is deliberately excluded: a useful CSP enumerates the * exact domains a project loads from (its CDN, analytics, embeds), so it can't * be generic without being either too loose (useless) or too strict (breaks the * site). Add your project's CSP via `additional`. * * @example * // next.config.ts * import { buildSecurityHeaders } from '@intecion/ipal-kit' * const securityHeaders = buildSecurityHeaders({ * hsts: process.env.NODE_ENV === 'production', // off in dev over http * additional: [ * { key: 'Content-Security-Policy', value: "default-src 'self'; ..." }, * ], * }) * const nextConfig = { * async headers() { * return [{ source: '/:path*', headers: securityHeaders }] * }, * } */ export function buildSecurityHeaders(args: BuildSecurityHeadersArgs = {}): SecurityHeader[] { const { additional = [], frameOptions = 'DENY', hsts = true, hstsIncludeSubDomains = true, hstsMaxAge = 63072000, hstsPreload = false, permissionsPolicy = 'camera=(), microphone=(), geolocation=()', referrerPolicy = 'strict-origin-when-cross-origin', } = args const headers: SecurityHeader[] = [] if (hsts) { const parts = [`max-age=${hstsMaxAge}`] if (hstsIncludeSubDomains) {parts.push('includeSubDomains')} if (hstsPreload) {parts.push('preload')} headers.push({ key: 'Strict-Transport-Security', value: parts.join('; ') }) } if (frameOptions) { headers.push({ key: 'X-Frame-Options', value: frameOptions }) } // Prevents MIME-type sniffing — always safe, no project specifics. headers.push({ key: 'X-Content-Type-Options', value: 'nosniff' }) if (referrerPolicy) { headers.push({ key: 'Referrer-Policy', value: referrerPolicy }) } if (permissionsPolicy) { headers.push({ key: 'Permissions-Policy', value: permissionsPolicy }) } // Merge additional: same-key entries override the defaults above. for (const extra of additional) { const i = headers.findIndex((h) => h.key.toLowerCase() === extra.key.toLowerCase()) if (i >= 0) {headers[i] = extra} else {headers.push(extra)} } return headers }