Skip to content

Commit d9dd7a3

Browse files
waleedlatif1claude
andauthored
fix(cors): re-enable credentials on chat/form embed CORS policy (#4673)
* fix(cors): re-enable credentials on embed CORS policy Chat and form embeds authenticate via the chat_auth_<id> / 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 <noreply@anthropic.com> * chore(cors): trim verbose comments in proxy Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(cors): restore concise TSDoc on proxy CORS helpers Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f0311a6 commit d9dd7a3

2 files changed

Lines changed: 22 additions & 39 deletions

File tree

apps/sim/proxy.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ describe('resolveApiCorsPolicy', () => {
4444
expect(policy.headers).toContain('X-API-Key')
4545
})
4646

47-
it('reflects origin for chat and form embeds, never sets credentials', () => {
47+
it('reflects origin for chat and form embeds with credentials enabled', () => {
4848
const paths = [
4949
'/api/chat/abc',
5050
'/api/chat/abc/otp',
@@ -56,30 +56,32 @@ describe('resolveApiCorsPolicy', () => {
5656
const policy = resolveApiCorsPolicy(makeRequest(path, 'https://customer.example'))
5757
expect(policy).toEqual({
5858
origin: 'https://customer.example',
59-
credentials: false,
59+
credentials: true,
6060
methods: 'GET, POST, PUT, OPTIONS',
6161
headers: 'Content-Type, X-Requested-With',
6262
})
6363
}
6464
})
6565

66+
it('drops credentials on embed policy when Origin header is absent (CORS spec invariant)', () => {
67+
const policy = resolveApiCorsPolicy(makeRequest('/api/chat/abc'))
68+
expect(policy.origin).toBe('*')
69+
expect(policy.credentials).toBe(false)
70+
})
71+
6672
it('allows PUT on the embed policy (used by OTP verification on /[identifier]/otp)', () => {
6773
const policy = resolveApiCorsPolicy(
6874
makeRequest('/api/chat/abc/otp', 'https://customer.example')
6975
)
7076
expect(policy.methods).toContain('PUT')
7177
})
7278

73-
it('falls back to wildcard for chat/form embeds when no origin header is present', () => {
74-
expect(resolveApiCorsPolicy(makeRequest('/api/chat/abc')).origin).toBe('*')
75-
})
76-
7779
it('applies the embed policy to future identifier subroutes (not just /otp, /sso)', () => {
7880
const policy = resolveApiCorsPolicy(
7981
makeRequest('/api/chat/abc/transcript', 'https://customer.example')
8082
)
8183
expect(policy.origin).toBe('https://customer.example')
82-
expect(policy.credentials).toBe(false)
84+
expect(policy.credentials).toBe(true)
8385
})
8486

8587
it('uses the default credentialed policy for workspace-internal chat/form routes', () => {

apps/sim/proxy.ts

Lines changed: 13 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,10 @@ const DEFAULT_API_ALLOWED_HEADERS =
2222
const WORKFLOW_EXECUTE_HEADERS =
2323
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, X-API-Key'
2424

25-
/**
26-
* Workspace-internal segments under /api/{chat,form}/* that must NOT
27-
* receive the embed policy. They serve the workspace UI with session
28-
* cookies and need the default credentialed policy.
29-
*/
25+
/** Subpaths under /api/{chat,form}/* that serve the workspace UI, not embeds. */
3026
const EMBED_RESERVED_SEGMENTS = new Set(['manage', 'validate'])
3127

32-
/**
33-
* True for /api/{chat,form}/[identifier] and any deeper subroute
34-
* (e.g. /otp, /sso). The identifier segment is explicitly checked
35-
* against EMBED_RESERVED_SEGMENTS so workspace-internal routes fall
36-
* through to the default credentialed policy.
37-
*/
28+
/** True for /api/{chat,form}/[identifier] and any deeper subroute. */
3829
function isEmbedPath(pathname: string): boolean {
3930
const segments = pathname.split('/')
4031
if (segments.length < 4) return false
@@ -79,20 +70,16 @@ const CORS_RULES: readonly CorsRule[] = [
7970
}),
8071
},
8172
{
82-
// Embed endpoints: /api/chat/[identifier] and /api/form/[identifier]
83-
// (plus their /otp and /sso subroutes). These run on customer domains —
84-
// reflect the request origin and omit credentials (auth uses signed
85-
// tokens, not cookies). Workspace-internal subpaths (`manage`, `validate`,
86-
// and the bare collection routes) are deliberately excluded so they
87-
// continue to receive the default credentialed policy.
8873
match: (p) => isEmbedPath(p),
89-
policy: (request) => ({
90-
origin: request.headers.get('origin') || '*',
91-
credentials: false,
92-
// PUT is required for OTP verification on /[identifier]/otp.
93-
methods: 'GET, POST, PUT, OPTIONS',
94-
headers: 'Content-Type, X-Requested-With',
95-
}),
74+
policy: (request) => {
75+
const requestOrigin = request.headers.get('origin')
76+
return {
77+
origin: requestOrigin || '*',
78+
credentials: !!requestOrigin,
79+
methods: 'GET, POST, PUT, OPTIONS',
80+
headers: 'Content-Type, X-Requested-With',
81+
}
82+
},
9683
},
9784
{
9885
match: (p) => /^\/api\/workflows\/[^/]+\/execute$/.test(p),
@@ -105,10 +92,7 @@ const CORS_RULES: readonly CorsRule[] = [
10592
},
10693
]
10794

108-
/**
109-
* Single source of truth for CORS on /api/* — next.config.ts headers are
110-
* baked at build time and would freeze NEXT_PUBLIC_APP_URL into the image.
111-
*/
95+
/** Single source of truth for /api/* CORS — resolved at request time, not baked at build. */
11296
export function resolveApiCorsPolicy(request: NextRequest): CorsPolicy {
11397
const { pathname } = request.nextUrl
11498
for (const rule of CORS_RULES) {
@@ -134,10 +118,7 @@ function applyCorsHeaders(response: NextResponse, policy: CorsPolicy): void {
134118
}
135119
}
136120

137-
/**
138-
* Short-circuit preflight: Next's auto-OPTIONS for route handlers without
139-
* an explicit OPTIONS export does not carry middleware headers.
140-
*/
121+
/** Next's auto-OPTIONS doesn't carry middleware headers, so we answer preflight here. */
141122
function buildPreflightResponse(policy: CorsPolicy): NextResponse {
142123
const response = new NextResponse(null, { status: 204 })
143124
applyCorsHeaders(response, policy)

0 commit comments

Comments
 (0)