79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
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<SiteConfig>, 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<SiteConfig>, 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');
|
|
};
|