@@ -29,38 +29,112 @@ 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+ 'https://cursor.com' ,
69+ ...( isHosted ? [ 'https://www.googletagmanager.com' , 'https://www.google-analytics.com' ] : [ ] ) ,
70+ ] as const
71+
72+ const STATIC_CONNECT_SRC = [
73+ "'self'" ,
74+ 'https://api.browser-use.com' ,
75+ 'https://api.elevenlabs.io' ,
76+ 'wss://api.elevenlabs.io' ,
77+ 'https://api.exa.ai' ,
78+ 'https://api.firecrawl.dev' ,
79+ 'https://*.googleapis.com' ,
80+ 'https://*.amazonaws.com' ,
81+ 'https://*.s3.amazonaws.com' ,
82+ 'https://*.blob.core.windows.net' ,
83+ 'https://*.atlassian.com' ,
84+ 'https://*.supabase.co' ,
85+ 'https://api.github.com' ,
86+ 'https://github.com/*' ,
87+ 'https://challenges.cloudflare.com' ,
88+ 'https://collector.onedollarstats.com' ,
89+ ...( isHosted
90+ ? [
91+ 'https://www.googletagmanager.com' ,
92+ 'https://*.google-analytics.com' ,
93+ 'https://*.analytics.google.com' ,
94+ 'https://analytics.google.com' ,
95+ 'https://www.google.com' ,
96+ ]
97+ : [ ] ) ,
98+ ] as const
99+
100+ const STATIC_FRAME_SRC = [
101+ "'self'" ,
102+ 'https://challenges.cloudflare.com' ,
103+ 'https://drive.google.com' ,
104+ 'https://docs.google.com' ,
105+ 'https://*.google.com' ,
106+ 'https://www.youtube.com' ,
107+ 'https://player.vimeo.com' ,
108+ 'https://www.dailymotion.com' ,
109+ 'https://player.twitch.tv' ,
110+ 'https://clips.twitch.tv' ,
111+ 'https://streamable.com' ,
112+ 'https://fast.wistia.net' ,
113+ 'https://www.tiktok.com' ,
114+ 'https://w.soundcloud.com' ,
115+ 'https://open.spotify.com' ,
116+ 'https://embed.music.apple.com' ,
117+ 'https://www.loom.com' ,
118+ 'https://www.facebook.com' ,
119+ 'https://www.instagram.com' ,
120+ 'https://platform.twitter.com' ,
121+ 'https://rumble.com' ,
122+ 'https://play.vidyard.com' ,
123+ 'https://iframe.cloudflarestream.com' ,
124+ 'https://www.mixcloud.com' ,
125+ 'https://tenor.com' ,
126+ 'https://giphy.com' ,
127+ ...( isHosted ? [ 'https://www.googletagmanager.com' ] : [ ] ) ,
128+ ] as const
129+
32130// Build-time CSP directives (for next.config.ts)
33131export const buildTimeCSPDirectives : CSPDirectives = {
34132 '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-
133+ 'script-src' : [ ...STATIC_SCRIPT_SRC ] ,
48134 'style-src' : [ "'self'" , "'unsafe-inline'" , 'https://fonts.googleapis.com' ] ,
49135
50136 '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' ] : [ ] ) ,
137+ ...STATIC_IMG_SRC ,
64138 ...( env . S3_BUCKET_NAME && env . AWS_REGION
65139 ? [ `https://${ env . S3_BUCKET_NAME } .s3.${ env . AWS_REGION } .amazonaws.com` ]
66140 : [ ] ) ,
@@ -70,21 +144,16 @@ export const buildTimeCSPDirectives: CSPDirectives = {
70144 ...( env . S3_CHAT_BUCKET_NAME && env . AWS_REGION
71145 ? [ `https://${ env . S3_CHAT_BUCKET_NAME } .s3.${ env . AWS_REGION } .amazonaws.com` ]
72146 : [ ] ) ,
73- 'https://*.amazonaws.com' ,
74- 'https://*.blob.core.windows.net' ,
75- 'https://github.com/*' ,
76147 ...getHostnameFromUrl ( env . NEXT_PUBLIC_BRAND_LOGO_URL ) ,
77148 ...getHostnameFromUrl ( env . NEXT_PUBLIC_BRAND_FAVICON_URL ) ,
78149 ] ,
79150
80151 'media-src' : [ "'self'" , 'blob:' ] ,
81-
82152 'font-src' : [ "'self'" , 'https://fonts.gstatic.com' ] ,
83153
84154 'connect-src' : [
85- "'self'" ,
155+ ... STATIC_CONNECT_SRC ,
86156 env . NEXT_PUBLIC_APP_URL || '' ,
87- // Only include localhost fallbacks in development mode
88157 ...( env . OLLAMA_URL ? [ env . OLLAMA_URL ] : isDev ? [ 'http://localhost:11434' ] : [ ] ) ,
89158 ...( env . NEXT_PUBLIC_SOCKET_URL
90159 ? [
@@ -94,42 +163,12 @@ export const buildTimeCSPDirectives: CSPDirectives = {
94163 : isDev
95164 ? [ 'http://localhost:3002' , 'ws://localhost:3002' ]
96165 : [ ] ) ,
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- : [ ] ) ,
119166 ...getHostnameFromUrl ( env . NEXT_PUBLIC_BRAND_LOGO_URL ) ,
120167 ...getHostnameFromUrl ( env . NEXT_PUBLIC_PRIVACY_URL ) ,
121168 ...getHostnameFromUrl ( env . NEXT_PUBLIC_TERMS_URL ) ,
122169 ] ,
123170
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-
171+ 'frame-src' : [ ...STATIC_FRAME_SRC ] ,
133172 'frame-ancestors' : [ "'self'" ] ,
134173 'form-action' : [ "'self'" ] ,
135174 'base-uri' : [ "'self'" ] ,
@@ -152,13 +191,14 @@ export function buildCSPString(directives: CSPDirectives): string {
152191}
153192
154193/**
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
194+ * Generate runtime CSP header with dynamic environment variables.
195+ * Composes from the same STATIC_* constants as buildTimeCSPDirectives,
196+ * but resolves env vars at request time via getEnv() to fix Docker
197+ * deployments where build-time values may be stale placeholders.
157198 */
158199export function generateRuntimeCSP ( ) : string {
159200 const appUrl = getEnv ( 'NEXT_PUBLIC_APP_URL' ) || ''
160201
161- // Only include localhost URLs in development or when explicitly configured
162202 const socketUrl = getEnv ( 'NEXT_PUBLIC_SOCKET_URL' ) || ( isDev ? 'http://localhost:3002' : '' )
163203 const socketWsUrl = socketUrl
164204 ? socketUrl . replace ( 'http://' , 'ws://' ) . replace ( 'https://' , 'wss://' )
@@ -172,42 +212,24 @@ export function generateRuntimeCSP(): string {
172212 const privacyDomains = getHostnameFromUrl ( getEnv ( 'NEXT_PUBLIC_PRIVACY_URL' ) )
173213 const termsDomains = getHostnameFromUrl ( getEnv ( 'NEXT_PUBLIC_TERMS_URL' ) )
174214
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' : ''
215+ const runtimeDirectives : CSPDirectives = {
216+ ...buildTimeCSPDirectives ,
217+
218+ 'img-src' : [ ...STATIC_IMG_SRC , ...brandLogoDomains , ...brandFaviconDomains ] ,
219+
220+ 'connect-src' : [
221+ ...STATIC_CONNECT_SRC ,
222+ appUrl ,
223+ ollamaUrl ,
224+ socketUrl ,
225+ socketWsUrl ,
226+ ...brandLogoDomains ,
227+ ...privacyDomains ,
228+ ...termsDomains ,
229+ ] ,
230+ }
194231
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 ( )
232+ return buildCSPString ( runtimeDirectives )
211233}
212234
213235/**
0 commit comments