@@ -14,6 +14,7 @@ import helmet from 'helmet';
1414import { LoggingInterceptor } from './telemetry/logging.interceptor' ;
1515import { OtelExceptionFilter } from './telemetry/exception.filter' ;
1616import { PostHogService } from './shared/application/posthog.service' ;
17+ import { PrismaService } from './prisma/prisma.service' ;
1718
1819async function bootstrap ( ) {
1920 const app = await NestFactory . create < NestExpressApplication > ( AppModule , {
@@ -38,12 +39,48 @@ async function bootstrap() {
3839 transform : true ,
3940 } ) ) ;
4041
41- const allowedOrigins = config . get < string > ( 'ALLOWED_ORIGINS' ) ?. split ( ',' ) . filter ( Boolean ) ;
42- if ( config . get ( 'NODE_ENV' ) === 'production' && ( ! allowedOrigins || allowedOrigins . length === 0 ) ) {
43- console . warn ( '⚠ ALLOWED_ORIGINS is not set — CORS will reject all cross-origin requests' ) ;
42+ // Static origins from env var (platform hosts, localhost, etc.)
43+ const staticOrigins = new Set (
44+ config . get < string > ( 'ALLOWED_ORIGINS' ) ?. split ( ',' ) . filter ( Boolean ) ?? [ ] ,
45+ ) ;
46+
47+ // Dynamic CORS: static origins are checked first, then brand domains from DB (cached 5 min)
48+ const prisma = app . get ( PrismaService ) ;
49+ let brandDomainCache : Set < string > = new Set ( ) ;
50+ let cacheExpiresAt = 0 ;
51+ const CACHE_TTL_MS = 5 * 60 * 1000 ;
52+
53+ async function loadBrandDomains ( ) : Promise < Set < string > > {
54+ const now = Date . now ( ) ;
55+ if ( now < cacheExpiresAt ) return brandDomainCache ;
56+ try {
57+ const brands = await prisma . brand . findMany ( {
58+ where : { isActive : true } ,
59+ select : { domain : true } ,
60+ } ) ;
61+ brandDomainCache = new Set (
62+ brands . map ( ( b ) => `https://${ b . domain } ` ) ,
63+ ) ;
64+ cacheExpiresAt = now + CACHE_TTL_MS ;
65+ } catch ( err ) {
66+ console . error ( 'Failed to load brand domains for CORS:' , err ) ;
67+ // Keep stale cache on error
68+ cacheExpiresAt = now + 30_000 ;
69+ }
70+ return brandDomainCache ;
4471 }
72+
4573 app . enableCors ( {
46- origin : allowedOrigins && allowedOrigins . length > 0 ? allowedOrigins : false ,
74+ origin : async ( origin , callback ) => {
75+ // Allow requests with no origin (server-to-server, curl, etc.)
76+ if ( ! origin ) return callback ( null , true ) ;
77+ if ( staticOrigins . has ( origin ) ) return callback ( null , true ) ;
78+
79+ const domains = await loadBrandDomains ( ) ;
80+ if ( domains . has ( origin ) ) return callback ( null , true ) ;
81+
82+ callback ( new Error ( `Origin ${ origin } not allowed by CORS` ) ) ;
83+ } ,
4784 credentials : true ,
4885 } ) ;
4986
0 commit comments