d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
286 lines
8.7 KiB
TypeScript
286 lines
8.7 KiB
TypeScript
import { env, getEnv } from '../config/env'
|
|
import { isDev, isHosted, isReactGrabEnabled } from '../config/env-flags'
|
|
|
|
/**
|
|
* Content Security Policy (CSP) configuration builder
|
|
*
|
|
* NOTE: This file is loaded by next.config.ts at build time, before @/ path
|
|
* aliases are resolved. Do NOT import from ../utils/urls (which uses @/ imports).
|
|
* Keep all URL constants local to this file.
|
|
*/
|
|
|
|
const DEFAULT_SOCKET_URL = 'http://localhost:3002'
|
|
const DEFAULT_OLLAMA_URL = 'http://localhost:11434'
|
|
|
|
function toWebSocketUrl(httpUrl: string): string {
|
|
return httpUrl.replace('http://', 'ws://').replace('https://', 'wss://')
|
|
}
|
|
|
|
function getHostnameFromUrl(url: string | undefined): string[] {
|
|
if (!url) return []
|
|
try {
|
|
return [`https://${new URL(url).hostname}`]
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
export interface CSPDirectives {
|
|
'default-src'?: string[]
|
|
'script-src'?: string[]
|
|
'style-src'?: string[]
|
|
'img-src'?: string[]
|
|
'media-src'?: string[]
|
|
'font-src'?: string[]
|
|
'connect-src'?: string[]
|
|
'worker-src'?: string[]
|
|
'frame-src'?: string[]
|
|
'frame-ancestors'?: string[]
|
|
'form-action'?: string[]
|
|
'base-uri'?: string[]
|
|
'object-src'?: string[]
|
|
}
|
|
|
|
/**
|
|
* Static CSP sources shared between build-time and runtime.
|
|
* Add new domains here — both paths pick them up automatically.
|
|
*/
|
|
const STATIC_SCRIPT_SRC = [
|
|
"'self'",
|
|
"'unsafe-inline'",
|
|
...(isDev ? ["'unsafe-eval'"] : []),
|
|
'https://*.google.com',
|
|
'https://apis.google.com',
|
|
'https://challenges.cloudflare.com',
|
|
// Cal.com booking embed (landing /demo) — embed.js is served from app.cal.com
|
|
'https://app.cal.com',
|
|
...(isReactGrabEnabled ? ['https://unpkg.com'] : []),
|
|
...(isHosted
|
|
? [
|
|
'https://www.googletagmanager.com',
|
|
'https://www.google-analytics.com',
|
|
'https://analytics.ahrefs.com',
|
|
// HubSpot tracking (landing pages) — loader plus the
|
|
// analytics/form-tracking/banner scripts it injects as <script> tags
|
|
'https://*.hs-scripts.com',
|
|
'https://*.hs-analytics.net',
|
|
'https://*.hscollectedforms.net',
|
|
'https://*.hs-banner.com',
|
|
]
|
|
: []),
|
|
] as const
|
|
|
|
const STATIC_IMG_SRC = ["'self'", 'data:', 'blob:', 'https:'] as const
|
|
|
|
const STATIC_CONNECT_SRC = [
|
|
"'self'",
|
|
'https://api.browser-use.com',
|
|
'https://api.elevenlabs.io',
|
|
'wss://api.elevenlabs.io',
|
|
'https://api.exa.ai',
|
|
'https://api.firecrawl.dev',
|
|
'https://*.googleapis.com',
|
|
'https://*.amazonaws.com',
|
|
'https://*.s3.amazonaws.com',
|
|
'https://*.blob.core.windows.net',
|
|
'https://*.atlassian.com',
|
|
'https://*.supabase.co',
|
|
'https://api.github.com',
|
|
'https://github.com/*',
|
|
'https://challenges.cloudflare.com',
|
|
// Cal.com booking embed (landing /demo) — embed XHR/availability calls
|
|
'https://app.cal.com',
|
|
'https://cal.com',
|
|
...(isReactGrabEnabled ? ['https://www.react-grab.com'] : []),
|
|
...(isDev ? ['ws://localhost:4722'] : []),
|
|
...(isHosted
|
|
? [
|
|
'https://www.googletagmanager.com',
|
|
'https://*.google-analytics.com',
|
|
'https://*.analytics.google.com',
|
|
'https://analytics.google.com',
|
|
'https://www.google.com',
|
|
'https://analytics.ahrefs.com',
|
|
'https://*.g.doubleclick.net',
|
|
// HubSpot tracking — form-tracking API (hscollectedforms.js).
|
|
// The visitor beacon itself is an image pixel (img-src, already
|
|
// permitted below), not a connect-src request.
|
|
'https://*.hscollectedforms.net',
|
|
]
|
|
: []),
|
|
] as const
|
|
|
|
const STATIC_FRAME_SRC = [
|
|
"'self'",
|
|
'blob:',
|
|
'https://challenges.cloudflare.com',
|
|
// Cal.com booking embed (landing /demo) — the booking iframe
|
|
'https://app.cal.com',
|
|
'https://cal.com',
|
|
'https://drive.google.com',
|
|
'https://docs.google.com',
|
|
'https://*.google.com',
|
|
'https://www.youtube.com',
|
|
'https://player.vimeo.com',
|
|
'https://www.dailymotion.com',
|
|
'https://player.twitch.tv',
|
|
'https://clips.twitch.tv',
|
|
'https://streamable.com',
|
|
'https://fast.wistia.net',
|
|
'https://www.tiktok.com',
|
|
'https://w.soundcloud.com',
|
|
'https://open.spotify.com',
|
|
'https://embed.music.apple.com',
|
|
'https://www.loom.com',
|
|
'https://www.facebook.com',
|
|
'https://www.instagram.com',
|
|
'https://platform.twitter.com',
|
|
'https://rumble.com',
|
|
'https://play.vidyard.com',
|
|
'https://iframe.cloudflarestream.com',
|
|
'https://www.mixcloud.com',
|
|
'https://tenor.com',
|
|
'https://giphy.com',
|
|
...(isHosted ? ['https://www.googletagmanager.com'] : []),
|
|
] as const
|
|
|
|
// Build-time CSP directives (for next.config.ts)
|
|
export const buildTimeCSPDirectives: CSPDirectives = {
|
|
'default-src': ["'self'"],
|
|
'script-src': [...STATIC_SCRIPT_SRC],
|
|
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
|
|
|
'img-src': [...STATIC_IMG_SRC],
|
|
|
|
'media-src': ["'self'", 'blob:'],
|
|
'worker-src': ["'self'", 'blob:'],
|
|
'font-src': ["'self'", 'https://fonts.gstatic.com'],
|
|
|
|
'connect-src': [
|
|
...STATIC_CONNECT_SRC,
|
|
env.NEXT_PUBLIC_APP_URL || '',
|
|
...(env.OLLAMA_URL ? [env.OLLAMA_URL] : isDev ? [DEFAULT_OLLAMA_URL] : []),
|
|
...(env.NEXT_PUBLIC_SOCKET_URL
|
|
? [env.NEXT_PUBLIC_SOCKET_URL, toWebSocketUrl(env.NEXT_PUBLIC_SOCKET_URL)]
|
|
: isDev
|
|
? [DEFAULT_SOCKET_URL, toWebSocketUrl(DEFAULT_SOCKET_URL)]
|
|
: []),
|
|
...getHostnameFromUrl(env.NEXT_PUBLIC_BRAND_LOGO_URL),
|
|
...getHostnameFromUrl(env.NEXT_PUBLIC_PRIVACY_URL),
|
|
...getHostnameFromUrl(env.NEXT_PUBLIC_TERMS_URL),
|
|
],
|
|
|
|
'frame-src': [...STATIC_FRAME_SRC],
|
|
'frame-ancestors': ["'self'"],
|
|
'form-action': ["'self'"],
|
|
'base-uri': ["'self'"],
|
|
'object-src': ["'none'"],
|
|
}
|
|
|
|
/**
|
|
* Build CSP string from directives object
|
|
*/
|
|
export function buildCSPString(directives: CSPDirectives): string {
|
|
return Object.entries(directives)
|
|
.map(([directive, sources]) => {
|
|
if (!sources || sources.length === 0) return ''
|
|
const validSources = sources.filter((source: string) => source && source.trim() !== '')
|
|
if (validSources.length === 0) return ''
|
|
return `${directive} ${validSources.join(' ')}`
|
|
})
|
|
.filter(Boolean)
|
|
.join('; ')
|
|
}
|
|
|
|
/**
|
|
* Generate runtime CSP header with dynamic environment variables.
|
|
* Composes from the same STATIC_* constants as buildTimeCSPDirectives,
|
|
* but resolves env vars at request time via getEnv() to fix Docker
|
|
* deployments where build-time values may be stale placeholders.
|
|
*/
|
|
export function generateRuntimeCSP(): string {
|
|
const appUrl = getEnv('NEXT_PUBLIC_APP_URL') || ''
|
|
|
|
const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev ? DEFAULT_SOCKET_URL : '')
|
|
const socketWsUrl = socketUrl ? toWebSocketUrl(socketUrl) : ''
|
|
const ollamaUrl = getEnv('OLLAMA_URL') || (isDev ? DEFAULT_OLLAMA_URL : '')
|
|
|
|
const brandLogoDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_BRAND_LOGO_URL'))
|
|
const privacyDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_PRIVACY_URL'))
|
|
const termsDomains = getHostnameFromUrl(getEnv('NEXT_PUBLIC_TERMS_URL'))
|
|
|
|
const runtimeDirectives: CSPDirectives = {
|
|
...buildTimeCSPDirectives,
|
|
|
|
'img-src': [...STATIC_IMG_SRC],
|
|
|
|
'connect-src': [
|
|
...STATIC_CONNECT_SRC,
|
|
appUrl,
|
|
ollamaUrl,
|
|
socketUrl,
|
|
socketWsUrl,
|
|
...brandLogoDomains,
|
|
...privacyDomains,
|
|
...termsDomains,
|
|
],
|
|
}
|
|
|
|
return buildCSPString(runtimeDirectives)
|
|
}
|
|
|
|
/**
|
|
* Get the main CSP policy string (build-time)
|
|
*/
|
|
export function getMainCSPPolicy(): string {
|
|
return buildCSPString(buildTimeCSPDirectives)
|
|
}
|
|
|
|
/**
|
|
* Permissive CSP for workflow execution endpoints
|
|
*/
|
|
export function getWorkflowExecutionCSPPolicy(): string {
|
|
return "default-src * 'unsafe-inline' 'unsafe-eval'; connect-src *;"
|
|
}
|
|
|
|
/**
|
|
* CSP for embeddable chat pages.
|
|
* Extends the shared embed policy with Microsoft Office.js sources so the
|
|
* chat page can serve as an Office (Excel/Word/Outlook) add-in surface
|
|
* when loaded with `?embed=office`.
|
|
*/
|
|
export function getChatEmbedCSPPolicy(): string {
|
|
return buildCSPString({
|
|
...buildTimeCSPDirectives,
|
|
'script-src': [...STATIC_SCRIPT_SRC, 'https://appsforoffice.microsoft.com'],
|
|
'connect-src': [
|
|
...(buildTimeCSPDirectives['connect-src'] ?? []),
|
|
'https://appsforoffice.microsoft.com',
|
|
],
|
|
'frame-ancestors': ['*'],
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Add a source to a specific directive (modifies build-time directives)
|
|
*/
|
|
export function addCSPSource(directive: keyof CSPDirectives, source: string): void {
|
|
if (!buildTimeCSPDirectives[directive]) {
|
|
buildTimeCSPDirectives[directive] = []
|
|
}
|
|
if (!buildTimeCSPDirectives[directive]!.includes(source)) {
|
|
buildTimeCSPDirectives[directive]!.push(source)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove a source from a specific directive (modifies build-time directives)
|
|
*/
|
|
export function removeCSPSource(directive: keyof CSPDirectives, source: string): void {
|
|
if (buildTimeCSPDirectives[directive]) {
|
|
buildTimeCSPDirectives[directive] = buildTimeCSPDirectives[directive]!.filter(
|
|
(s: string) => s !== source
|
|
)
|
|
}
|
|
}
|