2026-04-24 17:06:45 +02:00
|
|
|
import { defineMiddleware } from 'astro:middleware';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Emits the site-wide security header set. Astro does not attach these to
|
|
|
|
|
* statically-built responses at runtime on a shared host — but `preview` uses
|
|
|
|
|
* them locally, and when Cloudflare Transform Rules are configured (per
|
|
|
|
|
* docs/superpowers/specs/2026-04-24-cameleer-website-design.md §5.3) the
|
|
|
|
|
* edge re-emits the same set for the prod origin. Having both is defense
|
|
|
|
|
* in depth.
|
|
|
|
|
*/
|
|
|
|
|
export function buildSecurityHeaders(): Record<string, string> {
|
|
|
|
|
const csp = [
|
|
|
|
|
"default-src 'self'",
|
|
|
|
|
"img-src 'self' data:",
|
2026-04-24 17:11:16 +02:00
|
|
|
// Astro's scoped-style system injects inline style attributes at build time;
|
|
|
|
|
// 'unsafe-inline' here is required until we migrate to a hash/nonce strategy.
|
2026-04-24 17:06:45 +02:00
|
|
|
"style-src 'self' 'unsafe-inline'",
|
|
|
|
|
"font-src 'self'",
|
|
|
|
|
"script-src 'self'",
|
|
|
|
|
"connect-src 'self'",
|
|
|
|
|
"frame-ancestors 'none'",
|
|
|
|
|
"base-uri 'self'",
|
2026-04-24 17:11:16 +02:00
|
|
|
// No forms on this marketing site today (all auth redirects go to auth.cameleer.io
|
|
|
|
|
// as plain <a> navigations). If a future form is added, relax to 'self' or an allow-list.
|
2026-04-24 17:06:45 +02:00
|
|
|
"form-action 'none'",
|
|
|
|
|
"object-src 'none'",
|
|
|
|
|
].join('; ');
|
|
|
|
|
|
2026-04-24 17:34:27 +02:00
|
|
|
// Must match .htaccess and the Cloudflare Transform Rule in OPERATOR-CHECKLIST.md.
|
2026-04-24 17:06:45 +02:00
|
|
|
const permissionsPolicy = [
|
|
|
|
|
'geolocation=()',
|
|
|
|
|
'microphone=()',
|
|
|
|
|
'camera=()',
|
|
|
|
|
'payment=()',
|
|
|
|
|
'usb=()',
|
|
|
|
|
].join(', ');
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
'Content-Security-Policy': csp,
|
|
|
|
|
'X-Frame-Options': 'DENY',
|
|
|
|
|
'X-Content-Type-Options': 'nosniff',
|
|
|
|
|
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
|
|
|
'Permissions-Policy': permissionsPolicy,
|
|
|
|
|
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const onRequest = defineMiddleware(async (_context, next) => {
|
|
|
|
|
const response = await next();
|
|
|
|
|
const headers = buildSecurityHeaders();
|
|
|
|
|
for (const [name, value] of Object.entries(headers)) {
|
|
|
|
|
response.headers.set(name, value);
|
|
|
|
|
}
|
|
|
|
|
return response;
|
|
|
|
|
});
|