From 190593a7827f922aa511aa731b72a32091893539 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 10:15:57 -0700 Subject: [PATCH 1/3] fix(cors): re-enable credentials on embed CORS policy Chat and form embeds authenticate via the chat_auth_ / form auth cookie set by setDeploymentAuthCookie. The previous PR set Access-Control-Allow-Credentials: false on these routes, which made the browser drop the auth cookie and produce 401s on subsequent embed calls after login. Restore credentials: true (matching pre-consolidation behavior) while keeping reflected origin and Vary: Origin. The wildcard fallback when Origin is absent now also drops credentials to stay CORS-spec-compliant. Co-Authored-By: Claude Opus 4.7 --- apps/sim/proxy.test.ts | 16 +++++++++------- apps/sim/proxy.ts | 28 +++++++++++++++++----------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/apps/sim/proxy.test.ts b/apps/sim/proxy.test.ts index 0282d670cd..9311b14635 100644 --- a/apps/sim/proxy.test.ts +++ b/apps/sim/proxy.test.ts @@ -44,7 +44,7 @@ describe('resolveApiCorsPolicy', () => { expect(policy.headers).toContain('X-API-Key') }) - it('reflects origin for chat and form embeds, never sets credentials', () => { + it('reflects origin for chat and form embeds with credentials enabled', () => { const paths = [ '/api/chat/abc', '/api/chat/abc/otp', @@ -56,13 +56,19 @@ describe('resolveApiCorsPolicy', () => { const policy = resolveApiCorsPolicy(makeRequest(path, 'https://customer.example')) expect(policy).toEqual({ origin: 'https://customer.example', - credentials: false, + credentials: true, methods: 'GET, POST, PUT, OPTIONS', headers: 'Content-Type, X-Requested-With', }) } }) + it('drops credentials on embed policy when Origin header is absent (CORS spec invariant)', () => { + const policy = resolveApiCorsPolicy(makeRequest('/api/chat/abc')) + expect(policy.origin).toBe('*') + expect(policy.credentials).toBe(false) + }) + it('allows PUT on the embed policy (used by OTP verification on /[identifier]/otp)', () => { const policy = resolveApiCorsPolicy( makeRequest('/api/chat/abc/otp', 'https://customer.example') @@ -70,16 +76,12 @@ describe('resolveApiCorsPolicy', () => { expect(policy.methods).toContain('PUT') }) - it('falls back to wildcard for chat/form embeds when no origin header is present', () => { - expect(resolveApiCorsPolicy(makeRequest('/api/chat/abc')).origin).toBe('*') - }) - it('applies the embed policy to future identifier subroutes (not just /otp, /sso)', () => { const policy = resolveApiCorsPolicy( makeRequest('/api/chat/abc/transcript', 'https://customer.example') ) expect(policy.origin).toBe('https://customer.example') - expect(policy.credentials).toBe(false) + expect(policy.credentials).toBe(true) }) it('uses the default credentialed policy for workspace-internal chat/form routes', () => { diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index a5b8ea6df9..5eed2ad15e 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -80,19 +80,25 @@ const CORS_RULES: readonly CorsRule[] = [ }, { // Embed endpoints: /api/chat/[identifier] and /api/form/[identifier] - // (plus their /otp and /sso subroutes). These run on customer domains — - // reflect the request origin and omit credentials (auth uses signed - // tokens, not cookies). Workspace-internal subpaths (`manage`, `validate`, - // and the bare collection routes) are deliberately excluded so they + // (plus their /otp and /sso subroutes). These run on customer domains + // and authenticate via the `chat_auth_` / form auth cookie set by + // setDeploymentAuthCookie, so we must reflect the request origin AND + // allow credentials. Workspace-internal subpaths (`manage`, `validate`, + // and the bare collection routes) are excluded by isEmbedPath and // continue to receive the default credentialed policy. match: (p) => isEmbedPath(p), - policy: (request) => ({ - origin: request.headers.get('origin') || '*', - credentials: false, - // PUT is required for OTP verification on /[identifier]/otp. - methods: 'GET, POST, PUT, OPTIONS', - headers: 'Content-Type, X-Requested-With', - }), + policy: (request) => { + const requestOrigin = request.headers.get('origin') + return { + // Without an Origin header, fall back to '*' and drop credentials — + // the CORS spec rejects '*' paired with Allow-Credentials: true. + origin: requestOrigin || '*', + credentials: !!requestOrigin, + // PUT is required for OTP verification on /[identifier]/otp. + methods: 'GET, POST, PUT, OPTIONS', + headers: 'Content-Type, X-Requested-With', + } + }, }, { match: (p) => /^\/api\/workflows\/[^/]+\/execute$/.test(p), From 07f0957c66a59a18f9bf210c6ca06ccef4bc6700 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 10:20:02 -0700 Subject: [PATCH 2/3] chore(cors): trim verbose comments in proxy Co-Authored-By: Claude Opus 4.7 --- apps/sim/proxy.ts | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index 5eed2ad15e..e47a7bf5c8 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -22,19 +22,8 @@ const DEFAULT_API_ALLOWED_HEADERS = const WORKFLOW_EXECUTE_HEADERS = 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-API-Key' -/** - * Workspace-internal segments under /api/{chat,form}/* that must NOT - * receive the embed policy. They serve the workspace UI with session - * cookies and need the default credentialed policy. - */ const EMBED_RESERVED_SEGMENTS = new Set(['manage', 'validate']) -/** - * True for /api/{chat,form}/[identifier] and any deeper subroute - * (e.g. /otp, /sso). The identifier segment is explicitly checked - * against EMBED_RESERVED_SEGMENTS so workspace-internal routes fall - * through to the default credentialed policy. - */ function isEmbedPath(pathname: string): boolean { const segments = pathname.split('/') if (segments.length < 4) return false @@ -79,22 +68,12 @@ const CORS_RULES: readonly CorsRule[] = [ }), }, { - // Embed endpoints: /api/chat/[identifier] and /api/form/[identifier] - // (plus their /otp and /sso subroutes). These run on customer domains - // and authenticate via the `chat_auth_` / form auth cookie set by - // setDeploymentAuthCookie, so we must reflect the request origin AND - // allow credentials. Workspace-internal subpaths (`manage`, `validate`, - // and the bare collection routes) are excluded by isEmbedPath and - // continue to receive the default credentialed policy. match: (p) => isEmbedPath(p), policy: (request) => { const requestOrigin = request.headers.get('origin') return { - // Without an Origin header, fall back to '*' and drop credentials — - // the CORS spec rejects '*' paired with Allow-Credentials: true. origin: requestOrigin || '*', credentials: !!requestOrigin, - // PUT is required for OTP verification on /[identifier]/otp. methods: 'GET, POST, PUT, OPTIONS', headers: 'Content-Type, X-Requested-With', } @@ -111,10 +90,6 @@ const CORS_RULES: readonly CorsRule[] = [ }, ] -/** - * Single source of truth for CORS on /api/* — next.config.ts headers are - * baked at build time and would freeze NEXT_PUBLIC_APP_URL into the image. - */ export function resolveApiCorsPolicy(request: NextRequest): CorsPolicy { const { pathname } = request.nextUrl for (const rule of CORS_RULES) { @@ -140,10 +115,6 @@ function applyCorsHeaders(response: NextResponse, policy: CorsPolicy): void { } } -/** - * Short-circuit preflight: Next's auto-OPTIONS for route handlers without - * an explicit OPTIONS export does not carry middleware headers. - */ function buildPreflightResponse(policy: CorsPolicy): NextResponse { const response = new NextResponse(null, { status: 204 }) applyCorsHeaders(response, policy) From f8ac423fd524d43f8a512ebf54661070e3cca0b3 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 20 May 2026 10:26:22 -0700 Subject: [PATCH 3/3] chore(cors): restore concise TSDoc on proxy CORS helpers Co-Authored-By: Claude Opus 4.7 --- apps/sim/proxy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index e47a7bf5c8..1bf1ccac50 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -22,8 +22,10 @@ const DEFAULT_API_ALLOWED_HEADERS = const WORKFLOW_EXECUTE_HEADERS = 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-API-Key' +/** Subpaths under /api/{chat,form}/* that serve the workspace UI, not embeds. */ const EMBED_RESERVED_SEGMENTS = new Set(['manage', 'validate']) +/** True for /api/{chat,form}/[identifier] and any deeper subroute. */ function isEmbedPath(pathname: string): boolean { const segments = pathname.split('/') if (segments.length < 4) return false @@ -90,6 +92,7 @@ const CORS_RULES: readonly CorsRule[] = [ }, ] +/** Single source of truth for /api/* CORS — resolved at request time, not baked at build. */ export function resolveApiCorsPolicy(request: NextRequest): CorsPolicy { const { pathname } = request.nextUrl for (const rule of CORS_RULES) { @@ -115,6 +118,7 @@ function applyCorsHeaders(response: NextResponse, policy: CorsPolicy): void { } } +/** Next's auto-OPTIONS doesn't carry middleware headers, so we answer preflight here. */ function buildPreflightResponse(policy: CorsPolicy): NextResponse { const response = new NextResponse(null, { status: 204 }) applyCorsHeaders(response, policy)