feat(security): add unified CSP headers to all 17 web apps

Create @manacore/shared-utils/security-headers with setSecurityHeaders()
utility that sets standard security headers (CSP, X-Frame-Options,
X-Content-Type-Options, Referrer-Policy, Permissions-Policy).

CSP includes stats.mana.how (Umami) and glitchtip.mana.how by default.
Each app passes its own connectSrc origins (auth URL, backend URL, etc.).

Previously only Calendar and Storage had CSP headers - now all 17 web
apps have consistent security headers via the shared utility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-22 18:53:40 +01:00
parent 79544160b7
commit f5ee3aae20
19 changed files with 246 additions and 58 deletions

View file

@ -8,7 +8,8 @@
"exports": {
".": "./src/index.ts",
"./analytics": "./src/analytics.ts",
"./analytics-server": "./src/analytics-server.ts"
"./analytics-server": "./src/analytics-server.ts",
"./security-headers": "./src/security-headers.ts"
},
"scripts": {
"type-check": "tsc --noEmit",

View file

@ -0,0 +1,66 @@
/**
* Shared security headers for SvelteKit web apps.
*
* Sets standard security headers (CSP, X-Frame-Options, etc.)
* with Umami analytics and GlitchTip error tracking pre-configured.
*
* @example
* ```typescript
* import { setSecurityHeaders } from '@manacore/shared-utils/security-headers';
*
* const response = await resolve(event, { transformPageChunk: ... });
* setSecurityHeaders(response, {
* connectSrc: [authUrl, backendUrl],
* });
* return response;
* ```
*/
interface SecurityHeadersOptions {
/** Additional connect-src origins (auth URL, backend URL, etc.) */
connectSrc?: string[];
/** Additional script-src origins */
scriptSrc?: string[];
/** Additional img-src origins */
imgSrc?: string[];
/** Additional font-src origins */
fontSrc?: string[];
/** Override frame-ancestors (default: 'none') */
frameAncestors?: string;
}
/**
* Set standard security headers on a Response object.
* Includes Umami (stats.mana.how) and GlitchTip (glitchtip.mana.how) by default.
*/
export function setSecurityHeaders(response: Response, options: SecurityHeadersOptions = {}): void {
const {
connectSrc = [],
scriptSrc = [],
imgSrc = [],
fontSrc = [],
frameAncestors = "'none'",
} = options;
// Standard security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
// Content Security Policy
const cspDirectives = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline' https://stats.mana.how https://glitchtip.mana.how ${scriptSrc.join(' ')}`.trim(),
"style-src 'self' 'unsafe-inline'",
`img-src 'self' data: https: ${imgSrc.join(' ')}`.trim(),
`connect-src 'self' https://stats.mana.how https://glitchtip.mana.how ${connectSrc.join(' ')}`.trim(),
`font-src 'self' ${fontSrc.join(' ')}`.trim(),
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
`frame-ancestors ${frameAncestors}`,
];
response.headers.set('Content-Security-Policy', cspDirectives.join('; '));
}