Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

318 lines
10 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://')
}
/**
* Kept in sync with LOCALHOST_HOSTNAMES in ../utils/urls by hand: this module is
* loaded by next.config.ts before `@/` aliases resolve, so it cannot import from
* there (see the note above).
*/
const LOCALHOST_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])
/** Mirrors getSocketUrl's localhost check — those origins fall back to DEFAULT_SOCKET_URL. */
function isLocalhostUrl(url: string): boolean {
if (!url) return false
try {
return LOCALHOST_HOSTNAMES.has(new URL(url).hostname)
} catch {
return false
}
}
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',
// X (Twitter) conversion pixel (landing pages) — the base code injects
// uwt.js as a <script> tag from static.ads-twitter.com
'https://static.ads-twitter.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',
// X (Twitter) conversion pixel — uwt.js sends conversion beacons here
// via fetch/sendBeacon. The t.co image-pixel fallback is already
// covered by the `https:` wildcard in img-src.
'https://analytics.twitter.com',
]
: []),
] 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') || ''
// Must permit whatever getSocketUrl() actually connects to, or the browser
// blocks the handshake and Socket.IO retries forever. That helper falls back
// to DEFAULT_SOCKET_URL whenever the page is served from localhost — which
// includes a production build (docker compose sets NODE_ENV=production), so
// keying this on isDev alone left the bundled stack with a CSP that forbade
// its own realtime port. A non-localhost origin still resolves to the page
// origin, which appUrl already covers, so nothing is loosened there.
const socketUrl =
getEnv('NEXT_PUBLIC_SOCKET_URL') || (isDev || isLocalhostUrl(appUrl) ? 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
)
}
}