const API_URL = process.env.NEXT_PUBLIC_API_URL ? new URL(process.env.NEXT_PUBLIC_API_URL).origin : '' const SUPABASE_URL = process.env.SUPABASE_URL ? new URL(process.env.SUPABASE_URL).origin : '' const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL ? new URL(process.env.NEXT_PUBLIC_GOTRUE_URL).origin : '' const MARKETPLACE_API_URL = process.env.NEXT_PUBLIC_MARKETPLACE_API_URL ? new URL(process.env.NEXT_PUBLIC_MARKETPLACE_API_URL).origin : '' const SUPABASE_PROJECTS_URL = 'https://*.supabase.co https://*.storage.supabase.co' const SUPABASE_PROJECTS_URL_WS = 'wss://*.supabase.co' // construct the URL for the Websocket Local URLs let SUPABASE_LOCAL_PROJECTS_URL_WS = '' if (SUPABASE_URL) { const url = new URL(SUPABASE_URL) const wsUrl = `${url.hostname}:${url.port}` SUPABASE_LOCAL_PROJECTS_URL_WS = `ws://${wsUrl} wss://${wsUrl}` } // Needed to test docs search in local dev const SUPABASE_DOCS_PROJECT_URL = process.env.NEXT_PUBLIC_SUPABASE_URL ? new URL(process.env.NEXT_PUBLIC_SUPABASE_URL).origin : '' // Needed to test docs content API in local dev const SUPABASE_CONTENT_API_URL = process.env.NEXT_PUBLIC_CONTENT_API_URL ? new URL(process.env.NEXT_PUBLIC_CONTENT_API_URL).origin : '' const isDevOrStaging = process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview' || process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' || process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' const NIMBUS_STAGING_PROJECTS_URL = 'https://*.nmb-proj.com' const NIMBUS_STAGING_PROJECTS_URL_WS = 'wss://*.nmb-proj.com' const NIMBUS_PROD_PROJECTS_URL = process.env.NIMBUS_PROD_PROJECTS_URL || '' const NIMBUS_PROD_PROJECTS_URL_WS = process.env.NIMBUS_PROD_PROJECTS_URL_WS || '' const SUPABASE_STAGING_PROJECTS_URL = 'https://*.supabase.red https://*.storage.supabase.red' const SUPABASE_STAGING_PROJECTS_URL_WS = 'wss://*.supabase.red' const SUPABASE_COM_URL = 'https://supabase.com' const CLOUDFLARE_CDN_URL = 'https://cdnjs.cloudflare.com' const HCAPTCHA_SUBDOMAINS_URL = 'https://*.hcaptcha.com' const HCAPTCHA_ASSET_URL = 'https://newassets.hcaptcha.com' const HCAPTCHA_JS_URL = 'https://js.hcaptcha.com' const CONFIGCAT_URL = 'https://cdn-global.configcat.com' const CONFIGCAT_PROXY_URL = ['staging', 'local'].includes(process.env.NEXT_PUBLIC_ENVIRONMENT ?? '') ? 'https://configcat.supabase.green' : 'https://configcat.supabase.com' const STRIPE_SUBDOMAINS_URL = 'https://*.stripe.com' const STRIPE_JS_URL = 'https://js.stripe.com' const STRIPE_NETWORK_URL = 'https://*.stripe.network' const CLOUDFLARE_URL = 'https://www.cloudflare.com' const VERCEL_URL = 'https://vercel.com' const VERCEL_INSIGHTS_URL = 'https://*.vercel-insights.com' const GITHUB_URL = 'https://github.com' const GITHUB_API_URL = 'https://api.github.com' const GITHUB_USER_CONTENT_URL = 'https://raw.githubusercontent.com' const GITHUB_USER_AVATAR_URL = 'https://avatars.githubusercontent.com' const GOOGLE_USER_AVATAR_URL = 'https://lh3.googleusercontent.com' // This is a custom domain for Stape, which isused for GTM servers const STAPE_URL = 'https://ss.supabase.com' const VERCEL_LIVE_URL = 'https://vercel.live' const SENTRY_URL = 'https://*.ingest.sentry.io https://*.ingest.us.sentry.io https://*.ingest.de.sentry.io' const SUPABASE_ASSETS_URL = process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging' ? 'https://frontend-assets.supabase.green' : 'https://frontend-assets.supabase.com' const POSTHOG_URL = isDevOrStaging ? 'https://ph.supabase.green' : 'https://ph.supabase.com' const USERCENTRICS_URLS = 'https://*.usercentrics.eu' const USERCENTRICS_APP_URL = 'https://app.usercentrics.eu' // used by vercel live preview const PUSHER_URL = 'https://*.pusher.com' const PUSHER_URL_WS = 'wss://*.pusher.com' const GOOGLE_MAPS_API_URL = 'https://maps.googleapis.com' export function getCSP() { const DEFAULT_SRC_URLS = [ API_URL, SUPABASE_URL, GOTRUE_URL, MARKETPLACE_API_URL, SUPABASE_LOCAL_PROJECTS_URL_WS, SUPABASE_PROJECTS_URL, SUPABASE_PROJECTS_URL_WS, HCAPTCHA_SUBDOMAINS_URL, CONFIGCAT_URL, CONFIGCAT_PROXY_URL, STRIPE_SUBDOMAINS_URL, STRIPE_NETWORK_URL, CLOUDFLARE_URL, VERCEL_INSIGHTS_URL, GITHUB_API_URL, GITHUB_USER_CONTENT_URL, SUPABASE_ASSETS_URL, USERCENTRICS_URLS, STAPE_URL, GOOGLE_MAPS_API_URL, POSTHOG_URL, ...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []), CLOUDFLARE_CDN_URL, ] const SCRIPT_SRC_URLS = [ CLOUDFLARE_CDN_URL, HCAPTCHA_JS_URL, STRIPE_JS_URL, SUPABASE_ASSETS_URL, STAPE_URL, POSTHOG_URL, USERCENTRICS_URLS, ] const FRAME_SRC_URLS = [ HCAPTCHA_ASSET_URL, STRIPE_JS_URL, STAPE_URL, ...(isDevOrStaging ? [POSTHOG_URL] : []), ] // The TanStack build renders remote images as plain tags (no image // optimizer proxy), so every remote image origin must be listed here // explicitly — unlike the Next.js build, where the optimizer serves them // from same-origin /_next/image URLs covered by 'self'. const IMG_SRC_URLS = [ SUPABASE_URL, SUPABASE_COM_URL, SUPABASE_PROJECTS_URL, // GitHub profile avatars, e.g. https://github.com/.png GITHUB_URL, GITHUB_USER_AVATAR_URL, GOOGLE_USER_AVATAR_URL, // Vercel integration account avatars (https://vercel.com/api/www/avatar/...) VERCEL_URL, SUPABASE_ASSETS_URL, USERCENTRICS_APP_URL, STAPE_URL, USERCENTRICS_URLS, MARKETPLACE_API_URL, ...(!!NIMBUS_PROD_PROJECTS_URL ? [NIMBUS_PROD_PROJECTS_URL, NIMBUS_PROD_PROJECTS_URL_WS] : []), ] const STYLE_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL] const FONT_SRC_URLS = [CLOUDFLARE_CDN_URL, SUPABASE_ASSETS_URL] const defaultSrcDirective = [ `default-src 'self'`, ...DEFAULT_SRC_URLS, ...(isDevOrStaging ? [ SUPABASE_STAGING_PROJECTS_URL, SUPABASE_STAGING_PROJECTS_URL_WS, NIMBUS_STAGING_PROJECTS_URL, NIMBUS_STAGING_PROJECTS_URL_WS, VERCEL_LIVE_URL, SUPABASE_DOCS_PROJECT_URL, SUPABASE_CONTENT_API_URL, ] : []), PUSHER_URL_WS, SENTRY_URL, ].join(' ') const imgSrcDirective = [ `img-src 'self'`, `blob:`, `data:`, ...IMG_SRC_URLS, ...(isDevOrStaging ? [SUPABASE_STAGING_PROJECTS_URL, NIMBUS_STAGING_PROJECTS_URL] : []), ].join(' ') const scriptSrcDirective = [ `script-src 'self'`, `'unsafe-eval'`, `'unsafe-inline'`, ...SCRIPT_SRC_URLS, VERCEL_LIVE_URL, PUSHER_URL, GOOGLE_MAPS_API_URL, ].join(' ') const frameSrcDirective = [`frame-src 'self'`, ...FRAME_SRC_URLS, VERCEL_LIVE_URL].join(' ') const styleSrcDirective = [ `style-src 'self'`, `'unsafe-inline'`, ...STYLE_SRC_URLS, VERCEL_LIVE_URL, ].join(' ') // `data:` is required — GraphiQL's bundled Monaco ships the codicon icon font // as a data: URI, and Stripe's payment element inlines its fonts the same way. const fontSrcDirective = [`font-src 'self'`, `data:`, ...FONT_SRC_URLS, VERCEL_LIVE_URL].join(' ') const workerSrcDirective = [`worker-src 'self'`, `blob:`, `data:`].join(' ') const connectSrcDirective = [ `connect-src 'self'`, `data:`, `blob:`, ...DEFAULT_SRC_URLS, ...(isDevOrStaging ? [ SUPABASE_STAGING_PROJECTS_URL, SUPABASE_STAGING_PROJECTS_URL_WS, NIMBUS_STAGING_PROJECTS_URL, NIMBUS_STAGING_PROJECTS_URL_WS, VERCEL_LIVE_URL, SUPABASE_DOCS_PROJECT_URL, SUPABASE_CONTENT_API_URL, ] : []), PUSHER_URL_WS, SENTRY_URL, ].join(' ') const cspDirectives = [ connectSrcDirective, defaultSrcDirective, imgSrcDirective, scriptSrcDirective, frameSrcDirective, styleSrcDirective, fontSrcDirective, workerSrcDirective, `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`, `frame-ancestors 'none'`, `block-all-mixed-content`, ...(process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && process.env.NEXT_PUBLIC_ENVIRONMENT === 'prod' ? [`upgrade-insecure-requests`] : []), ] const csp = cspDirectives.join('; ') + ';' // Replace newline characters and spaces return csp.replace(/\s{2,}/g, ' ').trim() }