Files
ipal-kit/src/modules/security/buildSecurityHeaders.ts
T

114 lines
4.2 KiB
TypeScript

/**
* 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
}