@@ -29,38 +29,90 @@ export interface CSPDirectives {
2929 'object-src' ?: string [ ]
3030}
3131
32+ /**
33+ * Static CSP sources shared between build-time and runtime.
34+ * Add new domains here — both paths pick them up automatically.
35+ */
36+ const STATIC_SCRIPT_SRC = [
37+ "'self'" ,
38+ "'unsafe-inline'" ,
39+ 'https://*.google.com' ,
40+ 'https://apis.google.com' ,
41+ 'https://assets.onedollarstats.com' ,
42+ 'https://challenges.cloudflare.com' ,
43+ ...( isReactGrabEnabled ? [ 'https://unpkg.com' ] : [ ] ) ,
44+ ...( isHosted
45+ ? [
46+ 'https://www.googletagmanager.com' ,
47+ 'https://www.google-analytics.com' ,
48+ 'https://analytics.ahrefs.com' ,
49+ ]
50+ : [ ] ) ,
51+ ] as const
52+
53+ const STATIC_IMG_SRC = [
54+ "'self'" ,
55+ 'data:' ,
56+ 'blob:' ,
57+ 'https://*.googleusercontent.com' ,
58+ 'https://*.google.com' ,
59+ 'https://*.atlassian.com' ,
60+ 'https://cdn.discordapp.com' ,
61+ 'https://*.githubusercontent.com' ,
62+ 'https://*.s3.amazonaws.com' ,
63+ 'https://s3.amazonaws.com' ,
64+ 'https://*.amazonaws.com' ,
65+ 'https://*.blob.core.windows.net' ,
66+ 'https://github.com/*' ,
67+ 'https://collector.onedollarstats.com' ,
68+ ...( isHosted ? [ 'https://www.googletagmanager.com' , 'https://www.google-analytics.com' ] : [ ] ) ,
69+ ] as const
70+
71+ const STATIC_CONNECT_SRC = [
72+ "'self'" ,
73+ 'https://api.browser-use.com' ,
74+ 'https://api.elevenlabs.io' ,
75+ 'wss://api.elevenlabs.io' ,
76+ 'https://api.exa.ai' ,
77+ 'https://api.firecrawl.dev' ,
78+ 'https://*.googleapis.com' ,
79+ 'https://*.amazonaws.com' ,
80+ 'https://*.s3.amazonaws.com' ,
81+ 'https://*.blob.core.windows.net' ,
82+ 'https://*.atlassian.com' ,
83+ 'https://*.supabase.co' ,
84+ 'https://api.github.com' ,
85+ 'https://github.com/*' ,
86+ 'https://challenges.cloudflare.com' ,
87+ 'https://collector.onedollarstats.com' ,
88+ ...( isHosted
89+ ? [
90+ 'https://www.googletagmanager.com' ,
91+ 'https://*.google-analytics.com' ,
92+ 'https://*.analytics.google.com' ,
93+ 'https://analytics.google.com' ,
94+ 'https://www.google.com' ,
95+ ]
96+ : [ ] ) ,
97+ ] as const
98+
99+ const STATIC_FRAME_SRC = [
100+ "'self'" ,
101+ 'https://challenges.cloudflare.com' ,
102+ 'https://drive.google.com' ,
103+ 'https://docs.google.com' ,
104+ 'https://*.google.com' ,
105+ ...( isHosted ? [ 'https://www.googletagmanager.com' ] : [ ] ) ,
106+ ] as const
107+
32108// Build-time CSP directives (for next.config.ts)
33109export const buildTimeCSPDirectives : CSPDirectives = {
34110 'default-src' : [ "'self'" ] ,
35-
36- 'script-src' : [
37- "'self'" ,
38- "'unsafe-inline'" ,
39- "'unsafe-eval'" ,
40- 'https://*.google.com' ,
41- 'https://apis.google.com' ,
42- 'https://assets.onedollarstats.com' ,
43- 'https://challenges.cloudflare.com' ,
44- ...( isReactGrabEnabled ? [ 'https://unpkg.com' ] : [ ] ) ,
45- ...( isHosted ? [ 'https://www.googletagmanager.com' , 'https://www.google-analytics.com' ] : [ ] ) ,
46- ] ,
47-
111+ 'script-src' : [ ...STATIC_SCRIPT_SRC ] ,
48112 'style-src' : [ "'self'" , "'unsafe-inline'" , 'https://fonts.googleapis.com' ] ,
49113
50114 'img-src' : [
51- "'self'" ,
52- 'data:' ,
53- 'blob:' ,
54- 'https://*.googleusercontent.com' ,
55- 'https://*.google.com' ,
56- 'https://*.atlassian.com' ,
57- 'https://cdn.discordapp.com' ,
58- 'https://*.githubusercontent.com' ,
59- 'https://*.s3.amazonaws.com' ,
60- 'https://s3.amazonaws.com' ,
61- 'https://github.com/*' ,
62- 'https://collector.onedollarstats.com' ,
63- ...( isHosted ? [ 'https://www.googletagmanager.com' , 'https://www.google-analytics.com' ] : [ ] ) ,
115+ ...STATIC_IMG_SRC ,
64116 ...( env . S3_BUCKET_NAME && env . AWS_REGION
65117 ? [ `https://${ env . S3_BUCKET_NAME } .s3.${ env . AWS_REGION } .amazonaws.com` ]
66118 : [ ] ) ,
@@ -70,21 +122,16 @@ export const buildTimeCSPDirectives: CSPDirectives = {
70122 ...( env . S3_CHAT_BUCKET_NAME && env . AWS_REGION
71123 ? [ `https://${ env . S3_CHAT_BUCKET_NAME } .s3.${ env . AWS_REGION } .amazonaws.com` ]
72124 : [ ] ) ,
73- 'https://*.amazonaws.com' ,
74- 'https://*.blob.core.windows.net' ,
75- 'https://github.com/*' ,
76125 ...getHostnameFromUrl ( env . NEXT_PUBLIC_BRAND_LOGO_URL ) ,
77126 ...getHostnameFromUrl ( env . NEXT_PUBLIC_BRAND_FAVICON_URL ) ,
78127 ] ,
79128
80129 'media-src' : [ "'self'" , 'blob:' ] ,
81-
82130 'font-src' : [ "'self'" , 'https://fonts.gstatic.com' ] ,
83131
84132 'connect-src' : [
85- "'self'" ,
133+ ... STATIC_CONNECT_SRC ,
86134 env . NEXT_PUBLIC_APP_URL || '' ,
87- // Only include localhost fallbacks in development mode
88135 ...( env . OLLAMA_URL ? [ env . OLLAMA_URL ] : isDev ? [ 'http://localhost:11434' ] : [ ] ) ,
89136 ...( env . NEXT_PUBLIC_SOCKET_URL
90137 ? [
@@ -94,42 +141,12 @@ export const buildTimeCSPDirectives: CSPDirectives = {
94141 : isDev
95142 ? [ 'http://localhost:3002' , 'ws://localhost:3002' ]
96143 : [ ] ) ,
97- 'https://api.browser-use.com' ,
98- 'https://api.elevenlabs.io' ,
99- 'wss://api.elevenlabs.io' ,
100- 'https://api.exa.ai' ,
101- 'https://api.firecrawl.dev' ,
102- 'https://*.googleapis.com' ,
103- 'https://*.amazonaws.com' ,
104- 'https://*.s3.amazonaws.com' ,
105- 'https://*.blob.core.windows.net' ,
106- 'https://*.atlassian.com' ,
107- 'https://*.supabase.co' ,
108- 'https://api.github.com' ,
109- 'https://github.com/*' ,
110- 'https://challenges.cloudflare.com' ,
111- 'https://collector.onedollarstats.com' ,
112- ...( isHosted
113- ? [
114- 'https://www.googletagmanager.com' ,
115- 'https://*.google-analytics.com' ,
116- 'https://*.analytics.google.com' ,
117- ]
118- : [ ] ) ,
119144 ...getHostnameFromUrl ( env . NEXT_PUBLIC_BRAND_LOGO_URL ) ,
120145 ...getHostnameFromUrl ( env . NEXT_PUBLIC_PRIVACY_URL ) ,
121146 ...getHostnameFromUrl ( env . NEXT_PUBLIC_TERMS_URL ) ,
122147 ] ,
123148
124- 'frame-src' : [
125- "'self'" ,
126- 'https://challenges.cloudflare.com' ,
127- 'https://drive.google.com' ,
128- 'https://docs.google.com' ,
129- 'https://*.google.com' ,
130- ...( isHosted ? [ 'https://www.googletagmanager.com' ] : [ ] ) ,
131- ] ,
132-
149+ 'frame-src' : [ ...STATIC_FRAME_SRC ] ,
133150 'frame-ancestors' : [ "'self'" ] ,
134151 'form-action' : [ "'self'" ] ,
135152 'base-uri' : [ "'self'" ] ,
@@ -152,13 +169,14 @@ export function buildCSPString(directives: CSPDirectives): string {
152169}
153170
154171/**
155- * Generate runtime CSP header with dynamic environment variables (safer approach)
156- * This maintains compatibility with existing inline scripts while fixing Docker env var issues
172+ * Generate runtime CSP header with dynamic environment variables.
173+ * Composes from the same STATIC_* constants as buildTimeCSPDirectives,
174+ * but resolves env vars at request time via getEnv() to fix Docker
175+ * deployments where build-time values may be stale placeholders.
157176 */
158177export function generateRuntimeCSP ( ) : string {
159178 const appUrl = getEnv ( 'NEXT_PUBLIC_APP_URL' ) || ''
160179
161- // Only include localhost URLs in development or when explicitly configured
162180 const socketUrl = getEnv ( 'NEXT_PUBLIC_SOCKET_URL' ) || ( isDev ? 'http://localhost:3002' : '' )
163181 const socketWsUrl = socketUrl
164182 ? socketUrl . replace ( 'http://' , 'ws://' ) . replace ( 'https://' , 'wss://' )
@@ -172,42 +190,24 @@ export function generateRuntimeCSP(): string {
172190 const privacyDomains = getHostnameFromUrl ( getEnv ( 'NEXT_PUBLIC_PRIVACY_URL' ) )
173191 const termsDomains = getHostnameFromUrl ( getEnv ( 'NEXT_PUBLIC_TERMS_URL' ) )
174192
175- const allDynamicDomains = [
176- ...brandLogoDomains ,
177- ...brandFaviconDomains ,
178- ...privacyDomains ,
179- ...termsDomains ,
180- ]
181- const uniqueDynamicDomains = Array . from ( new Set ( allDynamicDomains ) )
182- const dynamicDomainsStr = uniqueDynamicDomains . join ( ' ' )
183- const brandLogoDomain = brandLogoDomains [ 0 ] || ''
184- const brandFaviconDomain = brandFaviconDomains [ 0 ] || ''
185- const reactGrabScript = isReactGrabEnabled ? 'https://unpkg.com' : ''
186- const gtmScript = isHosted
187- ? 'https://www.googletagmanager.com https://www.google-analytics.com'
188- : ''
189- const gtmConnect = isHosted
190- ? 'https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com'
191- : ''
192- const gtmImg = isHosted ? 'https://www.googletagmanager.com https://www.google-analytics.com' : ''
193- const gtmFrame = isHosted ? 'https://www.googletagmanager.com' : ''
193+ const runtimeDirectives : CSPDirectives = {
194+ ...buildTimeCSPDirectives ,
195+
196+ 'img-src' : [ ...STATIC_IMG_SRC , ...brandLogoDomains , ...brandFaviconDomains ] ,
197+
198+ 'connect-src' : [
199+ ...STATIC_CONNECT_SRC ,
200+ appUrl ,
201+ ollamaUrl ,
202+ socketUrl ,
203+ socketWsUrl ,
204+ ...brandLogoDomains ,
205+ ...privacyDomains ,
206+ ...termsDomains ,
207+ ] ,
208+ }
194209
195- return `
196- default-src 'self';
197- script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://assets.onedollarstats.com https://challenges.cloudflare.com ${ reactGrabScript } ${ gtmScript } ;
198- style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
199- img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com https://cdn.discordapp.com https://*.githubusercontent.com https://*.s3.amazonaws.com https://s3.amazonaws.com https://*.amazonaws.com https://*.blob.core.windows.net https://github.com/* https://collector.onedollarstats.com ${ gtmImg } ${ brandLogoDomain } ${ brandFaviconDomain } ;
200- media-src 'self' blob:;
201- font-src 'self' https://fonts.gstatic.com;
202- connect-src 'self' ${ appUrl } ${ ollamaUrl } ${ socketUrl } ${ socketWsUrl } 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://api.github.com https://github.com/* https://*.atlassian.com https://*.supabase.co https://challenges.cloudflare.com https://collector.onedollarstats.com ${ gtmConnect } ${ dynamicDomainsStr } ;
203- frame-src 'self' https://challenges.cloudflare.com https://drive.google.com https://docs.google.com https://*.google.com ${ gtmFrame } ;
204- frame-ancestors 'self';
205- form-action 'self';
206- base-uri 'self';
207- object-src 'none';
208- `
209- . replace ( / \s { 2 , } / g, ' ' )
210- . trim ( )
210+ return buildCSPString ( runtimeDirectives )
211211}
212212
213213/**
0 commit comments