Files
cameleer-website/src/middleware.ts
hsiegeln d98d73b14a Apply final-review cleanup: robots sitemap, CI guards, header parity
- Remove Sitemap line from robots.txt (no @astrojs/sitemap installed; was
  pointing to a 404 that would trip Google Search Console).
- Align Permissions-Policy across all three enforcement layers (middleware,
  .htaccess, Cloudflare Transform Rule in OPERATOR-CHECKLIST) by dropping the
  stray fullscreen=(self) from the middleware.
- Bump Lighthouse CI numberOfRuns from 1 to 3 to dampen CI-runner noise.
- Add CI guard that fails the build if any <TBD:...> marker survives into
  dist/ — prevents a legally incomplete imprint from shipping by accident.
- Add SFTP_* secret null-guard before the rsync --delete step so a missing
  secret fails loudly instead of targeting the SSH user's home root.
- Document the set:html compile-time-constant invariant in DualValueProps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:34:27 +02:00

56 lines
1.9 KiB
TypeScript

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:",
// 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.
"style-src 'self' 'unsafe-inline'",
"font-src 'self'",
"script-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
// 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.
"form-action 'none'",
"object-src 'none'",
].join('; ');
// Must match .htaccess and the Cloudflare Transform Rule in OPERATOR-CHECKLIST.md.
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;
});