managarten/packages/shared-utils/src/security-headers.ts
Till JS e4e3360ca8 fix(csp): move jsdelivr allowlist to mana-web hooks (Vite SSR cache workaround)
The previous two attempts at allowlisting cdn.jsdelivr.net for
transformers.js's onnxruntime-web loader landed in shared-utils
security-headers.ts. The actual file change was correct (verified by
grep), the commits got pushed, the live security-headers.ts on disk
had the additions — but Vite's SSR module cache for cross-workspace-
package imports kept serving the OLD compiled shared-utils to
hooks.server.ts. Net effect: edits to hooks.server.ts hot-reloaded
fine (proven by the *.hf.co connect-src additions showing up
immediately) while edits to shared-utils/security-headers.ts did not.
A dev server restart should clear it but I'd rather not depend on
manual intervention every time we touch the shared CSP.

Move the jsdelivr allowlist out of the shared default and into
mana-web's hooks.server.ts via the existing scriptSrc + connectSrc
options. hooks.server.ts is in the SvelteKit app's own source tree so
it HMRs reliably, no SSR cache to fight. As a bonus this is also
architecturally cleaner: cdn.jsdelivr.net is only needed by mana-web
because mana-web is the only Mana app that bundles @mana/local-llm —
other apps get a slightly tighter CSP for free.

The pattern to remember: changes to packages/shared-utils that affect
SSR (response headers, server hooks) require either a dev server
restart OR a manual `rm -rf apps/.../node_modules/.vite` to take
effect. Client-side changes hot-reload fine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:03:38 +02:00

83 lines
3.1 KiB
TypeScript

/**
* 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 '@mana/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[];
/** Additional media-src origins (audio/video sources) */
mediaSrc?: 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 = [],
mediaSrc = [],
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');
// Permissions-Policy: allow microphone for `self` so dreams/memoro voice
// capture (getUserMedia) works on mana.how. `microphone=()` would block
// the API entirely — Chrome reports `[Violation] Permissions policy
// violation: microphone is not allowed in this document` and the
// permission dialog never appears, even if the user has explicitly
// granted access in OS + browser settings. Camera stays disallowed
// since no module needs it.
response.headers.set('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(self)');
// Content Security Policy
const cspDirectives = [
"default-src 'self'",
// 'wasm-unsafe-eval' is required by @mana/local-llm to instantiate
// browser inference WebGPU runtimes (both the old WebLLM/MLC path
// and the current transformers.js/ONNX path). It only permits
// WebAssembly compilation, NOT eval()/new Function() — much narrower
// than the legacy 'unsafe-eval' source. Supported by all evergreen
// browsers.
`script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://stats.mana.how https://glitchtip.mana.how ${scriptSrc.join(' ')}`.trim(),
"style-src 'self' 'unsafe-inline'",
`img-src 'self' data: blob: https: ${imgSrc.join(' ')}`.trim(),
`connect-src 'self' https://stats.mana.how https://glitchtip.mana.how ${connectSrc.join(' ')}`.trim(),
`font-src 'self' ${fontSrc.join(' ')}`.trim(),
mediaSrc.length > 0 ? `media-src 'self' ${mediaSrc.join(' ')}`.trim() : '',
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
`frame-ancestors ${frameAncestors}`,
];
response.headers.set('Content-Security-Policy', cspDirectives.filter(Boolean).join('; '));
}