From f5ee3aae20c9a4c610db114547a273361c39cb7d Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 22 Mar 2026 18:53:40 +0100 Subject: [PATCH] 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) --- apps/calendar/apps/web/src/hooks.server.ts | 32 ++++----- apps/chat/apps/web/src/hooks.server.ts | 11 +++- apps/clock/apps/web/src/hooks.server.ts | 11 +++- apps/contacts/apps/web/src/hooks.server.ts | 15 ++++- apps/manacore/apps/web/src/hooks.server.ts | 17 ++++- apps/manadeck/apps/web/src/hooks.server.ts | 11 +++- apps/mukke/apps/web/src/hooks.server.ts | 11 +++- apps/nutriphi/apps/web/src/hooks.server.ts | 11 +++- apps/photos/apps/web/src/hooks.server.ts | 11 +++- apps/picture/apps/web/src/hooks.server.ts | 11 +++- apps/planta/apps/web/src/hooks.server.ts | 11 +++- apps/presi/apps/web/src/hooks.server.ts | 11 +++- apps/questions/apps/web/src/hooks.server.ts | 11 +++- apps/skilltree/apps/web/src/hooks.server.ts | 11 +++- apps/storage/apps/web/src/hooks.server.ts | 28 ++------ apps/todo/apps/web/src/hooks.server.ts | 11 +++- apps/zitare/apps/web/src/hooks.server.ts | 11 +++- packages/shared-utils/package.json | 3 +- packages/shared-utils/src/security-headers.ts | 66 +++++++++++++++++++ 19 files changed, 246 insertions(+), 58 deletions(-) create mode 100644 packages/shared-utils/src/security-headers.ts diff --git a/apps/calendar/apps/web/src/hooks.server.ts b/apps/calendar/apps/web/src/hooks.server.ts index 872eda7ea..39ea2c952 100644 --- a/apps/calendar/apps/web/src/hooks.server.ts +++ b/apps/calendar/apps/web/src/hooks.server.ts @@ -7,6 +7,7 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) // In dev mode, Vite exposes .env vars via import.meta.env, not process.env @@ -23,6 +24,7 @@ const PUBLIC_STT_URL = process.env.PUBLIC_STT_URL || 'https://stt-api.mana.how'; // Cross-app integration URLs (for todo and contacts APIs) const PUBLIC_TODO_BACKEND_URL = process.env.PUBLIC_TODO_BACKEND_URL || 'http://localhost:3018'; const PUBLIC_CONTACTS_API_URL = process.env.PUBLIC_CONTACTS_API_URL || 'http://localhost:3015'; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { const response = await resolve(event, { @@ -35,31 +37,21 @@ window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}"; window.__PUBLIC_STT_URL__ = "${PUBLIC_STT_URL}"; window.__PUBLIC_TODO_BACKEND_URL__ = "${PUBLIC_TODO_BACKEND_URL}"; window.__PUBLIC_CONTACTS_API_URL__ = "${PUBLIC_CONTACTS_API_URL}"; +window.__PUBLIC_GLITCHTIP_DSN__ = "${PUBLIC_GLITCHTIP_DSN}"; `; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); - // 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=()'); - response.headers.set( - 'Content-Security-Policy', - [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline' https://stats.mana.how", - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: https:", - `connect-src 'self' ${PUBLIC_MANA_CORE_AUTH_URL_CLIENT} ${PUBLIC_BACKEND_URL_CLIENT} ${PUBLIC_STT_URL} ${PUBLIC_TODO_BACKEND_URL} ${PUBLIC_CONTACTS_API_URL}`, - "font-src 'self'", - "object-src 'none'", - "base-uri 'self'", - "form-action 'self'", - "frame-ancestors 'none'", - ].join('; ') - ); + setSecurityHeaders(response, { + connectSrc: [ + PUBLIC_MANA_CORE_AUTH_URL_CLIENT, + PUBLIC_BACKEND_URL_CLIENT, + PUBLIC_STT_URL, + PUBLIC_TODO_BACKEND_URL, + PUBLIC_CONTACTS_API_URL, + ], + }); return response; }; diff --git a/apps/chat/apps/web/src/hooks.server.ts b/apps/chat/apps/web/src/hooks.server.ts index bbdabf731..086bbc41f 100644 --- a/apps/chat/apps/web/src/hooks.server.ts +++ b/apps/chat/apps/web/src/hooks.server.ts @@ -6,15 +6,17 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { // Inject runtime environment variables into the HTML // These will be available on window.__PUBLIC_*__ for client-side code @@ -22,8 +24,15 @@ export const handle: Handle = async ({ event, resolve }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/clock/apps/web/src/hooks.server.ts b/apps/clock/apps/web/src/hooks.server.ts index dd888a160..ad00e771f 100644 --- a/apps/clock/apps/web/src/hooks.server.ts +++ b/apps/clock/apps/web/src/hooks.server.ts @@ -1,19 +1,28 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/contacts/apps/web/src/hooks.server.ts b/apps/contacts/apps/web/src/hooks.server.ts index 7b148a6b5..9c833885f 100644 --- a/apps/contacts/apps/web/src/hooks.server.ts +++ b/apps/contacts/apps/web/src/hooks.server.ts @@ -6,6 +6,7 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = @@ -15,9 +16,10 @@ const PUBLIC_BACKEND_URL_CLIENT = // Cross-app integration URLs const PUBLIC_TODO_BACKEND_URL = process.env.PUBLIC_TODO_BACKEND_URL || 'http://localhost:3031'; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { // Inject runtime environment variables into the HTML // These will be available on window.__PUBLIC_*__ for client-side code @@ -25,8 +27,19 @@ export const handle: Handle = async ({ event, resolve }) => { window.__PUBLIC_MANA_CORE_AUTH_URL__ = "${PUBLIC_MANA_CORE_AUTH_URL_CLIENT}"; window.__PUBLIC_BACKEND_URL__ = "${PUBLIC_BACKEND_URL_CLIENT}"; window.__PUBLIC_TODO_BACKEND_URL__ = "${PUBLIC_TODO_BACKEND_URL}"; +window.__PUBLIC_GLITCHTIP_DSN__ = "${PUBLIC_GLITCHTIP_DSN}"; `; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [ + PUBLIC_MANA_CORE_AUTH_URL_CLIENT, + PUBLIC_BACKEND_URL_CLIENT, + PUBLIC_TODO_BACKEND_URL, + ], + }); + + return response; }; diff --git a/apps/manacore/apps/web/src/hooks.server.ts b/apps/manacore/apps/web/src/hooks.server.ts index 18e934331..e8393e804 100644 --- a/apps/manacore/apps/web/src/hooks.server.ts +++ b/apps/manacore/apps/web/src/hooks.server.ts @@ -1,5 +1,6 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; /** * Server hooks for ManaCore web app @@ -22,9 +23,10 @@ const PUBLIC_CLOCK_API_URL_CLIENT = process.env.PUBLIC_CLOCK_API_URL_CLIENT || process.env.PUBLIC_CLOCK_API_URL || ''; const PUBLIC_CONTACTS_API_URL_CLIENT = process.env.PUBLIC_CONTACTS_API_URL_CLIENT || process.env.PUBLIC_CONTACTS_API_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [ + PUBLIC_MANA_CORE_AUTH_URL_CLIENT, + PUBLIC_TODO_API_URL_CLIENT, + PUBLIC_CALENDAR_API_URL_CLIENT, + PUBLIC_CLOCK_API_URL_CLIENT, + PUBLIC_CONTACTS_API_URL_CLIENT, + ], + }); + + return response; }; diff --git a/apps/manadeck/apps/web/src/hooks.server.ts b/apps/manadeck/apps/web/src/hooks.server.ts index dd888a160..ad00e771f 100644 --- a/apps/manadeck/apps/web/src/hooks.server.ts +++ b/apps/manadeck/apps/web/src/hooks.server.ts @@ -1,19 +1,28 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/mukke/apps/web/src/hooks.server.ts b/apps/mukke/apps/web/src/hooks.server.ts index e637ac77c..034d07032 100644 --- a/apps/mukke/apps/web/src/hooks.server.ts +++ b/apps/mukke/apps/web/src/hooks.server.ts @@ -6,23 +6,32 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { // Inject runtime environment variables into the HTML // These will be available on window.__PUBLIC_*__ for client-side code const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/nutriphi/apps/web/src/hooks.server.ts b/apps/nutriphi/apps/web/src/hooks.server.ts index e637ac77c..034d07032 100644 --- a/apps/nutriphi/apps/web/src/hooks.server.ts +++ b/apps/nutriphi/apps/web/src/hooks.server.ts @@ -6,23 +6,32 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { // Inject runtime environment variables into the HTML // These will be available on window.__PUBLIC_*__ for client-side code const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/photos/apps/web/src/hooks.server.ts b/apps/photos/apps/web/src/hooks.server.ts index dd888a160..ad00e771f 100644 --- a/apps/photos/apps/web/src/hooks.server.ts +++ b/apps/photos/apps/web/src/hooks.server.ts @@ -1,19 +1,28 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/picture/apps/web/src/hooks.server.ts b/apps/picture/apps/web/src/hooks.server.ts index dd888a160..ad00e771f 100644 --- a/apps/picture/apps/web/src/hooks.server.ts +++ b/apps/picture/apps/web/src/hooks.server.ts @@ -1,19 +1,28 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/planta/apps/web/src/hooks.server.ts b/apps/planta/apps/web/src/hooks.server.ts index dd888a160..ad00e771f 100644 --- a/apps/planta/apps/web/src/hooks.server.ts +++ b/apps/planta/apps/web/src/hooks.server.ts @@ -1,19 +1,28 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/presi/apps/web/src/hooks.server.ts b/apps/presi/apps/web/src/hooks.server.ts index dd888a160..ad00e771f 100644 --- a/apps/presi/apps/web/src/hooks.server.ts +++ b/apps/presi/apps/web/src/hooks.server.ts @@ -1,19 +1,28 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/questions/apps/web/src/hooks.server.ts b/apps/questions/apps/web/src/hooks.server.ts index dd888a160..ad00e771f 100644 --- a/apps/questions/apps/web/src/hooks.server.ts +++ b/apps/questions/apps/web/src/hooks.server.ts @@ -1,19 +1,28 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/skilltree/apps/web/src/hooks.server.ts b/apps/skilltree/apps/web/src/hooks.server.ts index 0868fabf7..1a35656ea 100644 --- a/apps/skilltree/apps/web/src/hooks.server.ts +++ b/apps/skilltree/apps/web/src/hooks.server.ts @@ -6,20 +6,29 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/storage/apps/web/src/hooks.server.ts b/apps/storage/apps/web/src/hooks.server.ts index 8ce85e008..0cf53c8e9 100644 --- a/apps/storage/apps/web/src/hooks.server.ts +++ b/apps/storage/apps/web/src/hooks.server.ts @@ -7,6 +7,7 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = @@ -17,40 +18,23 @@ const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || 'http://localhost:3016'; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { const response = await resolve(event, { transformPageChunk: ({ html }) => { - // Inject runtime environment variables into the HTML - // These will be available on window.__PUBLIC_*__ for client-side code const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); - // 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=()'); - response.headers.set( - 'Content-Security-Policy', - [ - "default-src 'self'", - "script-src 'self' 'unsafe-inline' https://stats.mana.how", - "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: https:", - `connect-src 'self' ${PUBLIC_MANA_CORE_AUTH_URL_CLIENT} ${PUBLIC_BACKEND_URL_CLIENT} https://stats.mana.how`, - "font-src 'self'", - "object-src 'none'", - "base-uri 'self'", - "form-action 'self'", - "frame-ancestors 'none'", - ].join('; ') - ); + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); return response; }; diff --git a/apps/todo/apps/web/src/hooks.server.ts b/apps/todo/apps/web/src/hooks.server.ts index e637ac77c..034d07032 100644 --- a/apps/todo/apps/web/src/hooks.server.ts +++ b/apps/todo/apps/web/src/hooks.server.ts @@ -6,23 +6,32 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { // Inject runtime environment variables into the HTML // These will be available on window.__PUBLIC_*__ for client-side code const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/apps/zitare/apps/web/src/hooks.server.ts b/apps/zitare/apps/web/src/hooks.server.ts index 2b1537518..6d9ea842a 100644 --- a/apps/zitare/apps/web/src/hooks.server.ts +++ b/apps/zitare/apps/web/src/hooks.server.ts @@ -6,23 +6,32 @@ import type { Handle } from '@sveltejs/kit'; import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server'; +import { setSecurityHeaders } from '@manacore/shared-utils/security-headers'; // Get client-side URLs from environment (Docker runtime) const PUBLIC_MANA_CORE_AUTH_URL_CLIENT = process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || ''; const PUBLIC_BACKEND_URL_CLIENT = process.env.PUBLIC_ZITARE_API_URL_CLIENT || process.env.PUBLIC_ZITARE_API_URL || ''; +const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || ''; export const handle: Handle = async ({ event, resolve }) => { - return resolve(event, { + const response = await resolve(event, { transformPageChunk: ({ html }) => { // Inject runtime environment variables into the HTML // These will be available on window.__PUBLIC_*__ for client-side code const envScript = ``; return injectUmamiAnalytics(html.replace('', `${envScript}`)); }, }); + + setSecurityHeaders(response, { + connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT], + }); + + return response; }; diff --git a/packages/shared-utils/package.json b/packages/shared-utils/package.json index 1fe2490be..38b3d1b7b 100644 --- a/packages/shared-utils/package.json +++ b/packages/shared-utils/package.json @@ -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", diff --git a/packages/shared-utils/src/security-headers.ts b/packages/shared-utils/src/security-headers.ts new file mode 100644 index 000000000..3f9237edc --- /dev/null +++ b/packages/shared-utils/src/security-headers.ts @@ -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('; ')); +}