Files
carmanagement/config/nextSecurityHeaders.js
T
2026-06-11 11:57:03 -04:00

108 lines
3.0 KiB
JavaScript

function normalizeAssetPrefix(rawValue, basePath) {
if (!rawValue) return undefined
try {
const url = new URL(rawValue)
const pathname = url.pathname.replace(/\/$/, '')
if (!pathname.endsWith(basePath)) {
url.pathname = `${pathname}${basePath}`
}
return url.toString().replace(/\/$/, '')
} catch {
const trimmed = rawValue.replace(/\/$/, '')
return trimmed.endsWith(basePath) ? trimmed : `${trimmed}${basePath}`
}
}
function collectOrigins(assetSources) {
const origins = new Set()
for (const source of assetSources) {
if (!source) continue
try {
origins.add(new URL(source).origin)
} catch {
continue
}
}
return [...origins]
}
function collectWebSocketOrigins(assetSources) {
const origins = new Set()
for (const source of assetSources) {
if (!source) continue
try {
const url = new URL(source)
const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
origins.add(`${protocol}//${url.host}`)
} catch {
continue
}
}
return [...origins]
}
function collectBrowserApiSources() {
const sources = [process.env.NEXT_PUBLIC_API_URL, process.env.API_URL]
if (process.env.NODE_ENV !== 'production') {
sources.push('http://localhost:4000/api/v1')
}
return sources
}
function buildSecurityHeaders({ assetSources = [], connectSources = collectBrowserApiSources() } = {}) {
const assetOrigins = collectOrigins(assetSources)
const connectOrigins = collectOrigins(connectSources)
const websocketOrigins = [
...collectWebSocketOrigins(assetSources),
...collectWebSocketOrigins(connectSources),
]
const scriptSrc = ["'self'", "'unsafe-inline'"]
if (process.env.NODE_ENV !== 'production') {
scriptSrc.push("'unsafe-eval'")
}
scriptSrc.push(...assetOrigins)
const styleSrc = ["'self'", "'unsafe-inline'", ...assetOrigins]
const imgSrc = ["'self'", 'data:', 'blob:', 'https:', ...assetOrigins]
const fontSrc = ["'self'", 'data:', ...assetOrigins]
const connectSrc = [...new Set(["'self'", 'https:', 'wss:', ...assetOrigins, ...connectOrigins, ...websocketOrigins])]
return [
{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
`script-src ${scriptSrc.join(' ')}`,
`style-src ${styleSrc.join(' ')}`,
`img-src ${imgSrc.join(' ')}`,
`font-src ${fontSrc.join(' ')}`,
`connect-src ${connectSrc.join(' ')}`,
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
].join('; '),
},
]
}
module.exports = {
buildSecurityHeaders,
normalizeAssetPrefix,
}