Skip to content

Commit a0af8b3

Browse files
merge staging into fix/chat-deploy
2 parents fe7d06a + 7e85965 commit a0af8b3

39 files changed

Lines changed: 891 additions & 556 deletions

File tree

apps/sim/app/api/chat/[identifier]/otp/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export const PUT = withRouteHandler(
228228
includeThinking: deployment.includeThinking ?? false,
229229
includeToolCalls: deployment.includeToolCalls ?? false,
230230
})
231-
setChatAuthCookie(response, deployment, email)
231+
await setChatAuthCookie(response, deployment, email)
232232

233233
return response
234234
} catch (error) {

apps/sim/app/api/chat/[identifier]/route.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,39 @@ describe('Chat Identifier API Route', () => {
416416
)
417417
}, 10000)
418418

419+
it('executes with the email proven by the chat authentication gate', async () => {
420+
mockValidateChatAuth.mockResolvedValueOnce({
421+
authorized: true,
422+
authenticatedEmail: 'person@example.com',
423+
})
424+
const req = createMockNextRequest('POST', { input: 'Hello world' })
425+
426+
const response = await POST(req, {
427+
params: Promise.resolve({ identifier: 'test-chat' }),
428+
})
429+
expect(response.status).toBe(200)
430+
431+
const streamOptions = vi.mocked(createStreamingResponse).mock.calls[0][0]
432+
await streamOptions.executeFn({
433+
onStream: vi.fn(),
434+
onBlockComplete: vi.fn(),
435+
abortSignal: new AbortController().signal,
436+
})
437+
438+
expect(vi.mocked(executeWorkflow).mock.calls[0][4]).toMatchObject({
439+
principal: {
440+
kind: 'system',
441+
serviceId: 'chat',
442+
workspaceId: 'test-workspace-id',
443+
workflowId: 'workflow-id',
444+
subject: {
445+
kind: 'authenticated_email',
446+
email: 'person@example.com',
447+
},
448+
},
449+
})
450+
}, 10000)
451+
419452
/**
420453
* A row predating the column has no tool policy, so it has not opted in.
421454
* Thinking must not drag tool frames along with it.

apps/sim/app/api/chat/[identifier]/route.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export const POST = withRouteHandler(
159159
const response = createSuccessResponse(toChatConfigResponse(deployment))
160160

161161
if (deployment.authType === 'password') {
162-
setChatAuthCookie(response, deployment)
162+
await setChatAuthCookie(response, deployment)
163163
}
164164

165165
return response
@@ -328,6 +328,14 @@ export const POST = withRouteHandler(
328328
serviceId: 'chat',
329329
workspaceId,
330330
workflowId: deployment.workflowId,
331+
...(authResult.authenticatedEmail
332+
? {
333+
subject: {
334+
kind: 'authenticated_email' as const,
335+
email: authResult.authenticatedEmail,
336+
},
337+
}
338+
: {}),
331339
},
332340
selectedOutputs,
333341
isSecureMode: true,

apps/sim/app/api/chat/utils.test.ts

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
1818
const {
1919
mockMergeSubblockStateWithValues,
2020
mockMergeSubBlockValues,
21-
mockValidateAuthToken,
21+
mockReadDeploymentAuthToken,
2222
mockSetDeploymentAuthCookie,
2323
mockIsEmailAllowed,
2424
mockCheckRateLimitDirect,
2525
} = vi.hoisted(() => ({
2626
mockMergeSubblockStateWithValues: vi.fn().mockReturnValue({}),
2727
mockMergeSubBlockValues: vi.fn().mockReturnValue({}),
28-
mockValidateAuthToken: vi.fn().mockReturnValue(false),
28+
mockReadDeploymentAuthToken: vi.fn().mockResolvedValue(null),
2929
mockSetDeploymentAuthCookie: vi.fn(),
3030
mockIsEmailAllowed: vi.fn(),
3131
mockCheckRateLimitDirect: vi.fn().mockResolvedValue({ allowed: true }),
@@ -57,7 +57,7 @@ vi.mock('@sim/workflow-persistence/subblocks', () => ({
5757
vi.mock('@/lib/core/security/encryption', () => encryptionMock)
5858

5959
vi.mock('@/lib/core/security/deployment', () => ({
60-
validateAuthToken: mockValidateAuthToken,
60+
readDeploymentAuthToken: mockReadDeploymentAuthToken,
6161
setDeploymentAuthCookie: mockSetDeploymentAuthCookie,
6262
isEmailAllowed: mockIsEmailAllowed,
6363
deploymentAuthCookieName: (prefix: string, id: string) => `${prefix}_auth_${id}`,
@@ -84,52 +84,65 @@ describe('Chat API Utils', () => {
8484

8585
describe('Auth token utils', () => {
8686
it('should accept valid auth cookie via validateChatAuth', async () => {
87-
mockValidateAuthToken.mockReturnValue(true)
87+
mockReadDeploymentAuthToken.mockResolvedValue({})
8888

8989
const deployment = {
9090
id: 'chat-id',
9191
authType: 'password',
9292
password: 'encrypted-password',
9393
}
9494

95-
const mockRequest = {
96-
method: 'POST',
97-
cookies: {
98-
get: vi.fn().mockReturnValue({ value: 'valid-token' }),
99-
},
100-
} as any
95+
const mockRequest = createMockRequest('POST', undefined, {
96+
cookie: 'chat_auth_chat-id=valid-token',
97+
})
10198

10299
const result = await validateChatAuth('request-id', deployment, mockRequest)
103-
expect(mockValidateAuthToken).toHaveBeenCalledWith({
100+
expect(mockReadDeploymentAuthToken).toHaveBeenCalledWith({
104101
token: 'valid-token',
105102
resource: deployment,
106103
})
107104
expect(result.authorized).toBe(true)
108105
})
109106

110107
it('should reject invalid auth cookie via validateChatAuth', async () => {
111-
mockValidateAuthToken.mockReturnValue(false)
108+
mockReadDeploymentAuthToken.mockResolvedValue(null)
112109

113110
const deployment = {
114111
id: 'chat-id',
115112
authType: 'password',
116113
password: 'encrypted-password',
117114
}
118115

119-
const mockRequest = {
120-
method: 'GET',
121-
cookies: {
122-
get: vi.fn().mockReturnValue({ value: 'invalid-token' }),
123-
},
124-
} as any
116+
const mockRequest = createMockRequest('GET', undefined, {
117+
cookie: 'chat_auth_chat-id=invalid-token',
118+
})
125119

126120
const result = await validateChatAuth('request-id', deployment, mockRequest)
127121
expect(result.authorized).toBe(false)
128122
})
123+
124+
it('returns the authenticated email carried by a valid email-auth cookie', async () => {
125+
mockReadDeploymentAuthToken.mockResolvedValue({
126+
authenticatedEmail: 'person@example.com',
127+
})
128+
129+
const deployment = {
130+
id: 'chat-id',
131+
authType: 'email',
132+
}
133+
const mockRequest = createMockRequest('POST', undefined, {
134+
cookie: 'chat_auth_chat-id=valid-token',
135+
})
136+
137+
await expect(validateChatAuth('request-id', deployment, mockRequest)).resolves.toEqual({
138+
authorized: true,
139+
authenticatedEmail: 'person@example.com',
140+
})
141+
})
129142
})
130143

131144
describe('Cookie handling', () => {
132-
it('should delegate to setDeploymentAuthCookie', () => {
145+
it('should delegate to setDeploymentAuthCookie', async () => {
133146
const mockResponse = {
134147
cookies: { set: vi.fn() },
135148
} as unknown as NextResponse
@@ -139,7 +152,7 @@ describe('Chat API Utils', () => {
139152
authType: 'password',
140153
password: 'encrypted-password',
141154
}
142-
setChatAuthCookie(mockResponse, deployment)
155+
await setChatAuthCookie(mockResponse, deployment)
143156

144157
expect(mockSetDeploymentAuthCookie).toHaveBeenCalledWith({
145158
response: mockResponse,
@@ -148,6 +161,26 @@ describe('Chat API Utils', () => {
148161
verifiedEmail: undefined,
149162
})
150163
})
164+
165+
it('forwards an authenticated email into the signed deployment cookie', async () => {
166+
const mockResponse = {
167+
cookies: { set: vi.fn() },
168+
} as unknown as NextResponse
169+
170+
const deployment = {
171+
id: 'test-chat-id',
172+
authType: 'email',
173+
allowedEmails: ['person@example.com'],
174+
}
175+
await setChatAuthCookie(mockResponse, deployment, 'person@example.com')
176+
177+
expect(mockSetDeploymentAuthCookie).toHaveBeenCalledWith({
178+
response: mockResponse,
179+
cookiePrefix: 'chat',
180+
resource: deployment,
181+
verifiedEmail: 'person@example.com',
182+
})
183+
})
151184
})
152185

153186
describe('Chat auth validation', () => {
@@ -429,14 +462,17 @@ describe('Chat API Utils', () => {
429462
})
430463

431464
it('authorizes execution when session email is allowlisted', async () => {
432-
mockGetSession.mockResolvedValue({ user: { email: 'user@example.com' } })
465+
mockGetSession.mockResolvedValue({ user: { email: 'User@Example.com' } })
433466
mockIsEmailAllowed.mockReturnValue(true)
434467

435468
const result = await validateChatAuth('request-id', ssoDeployment, postRequest, {
436469
input: 'hello',
437470
})
438471

439-
expect(result.authorized).toBe(true)
472+
expect(result).toEqual({
473+
authorized: true,
474+
authenticatedEmail: 'user@example.com',
475+
})
440476
})
441477

442478
it('rejects execution when session email is not allowlisted', async () => {

apps/sim/app/api/chat/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import {
1313
validateDeploymentAuth,
1414
} from '@/lib/core/security/deployment-auth'
1515

16-
export function setChatAuthCookie(
16+
export async function setChatAuthCookie(
1717
response: NextResponse,
1818
deployment: DeploymentAuthResource,
1919
verifiedEmail?: string
20-
): void {
21-
setDeploymentAuthCookie({
20+
): Promise<void> {
21+
await setDeploymentAuthCookie({
2222
response,
2323
cookiePrefix: 'chat',
2424
resource: deployment,

apps/sim/app/api/files/public/[token]/otp/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export const PUT = withRouteHandler(
200200
await deleteOTP('file', resolved.share.id, email)
201201

202202
const response = NextResponse.json({ authType: resolved.share.authType })
203-
setDeploymentAuthCookie({
203+
await setDeploymentAuthCookie({
204204
response,
205205
cookiePrefix: 'file',
206206
resource: resolved.share,

apps/sim/app/api/files/public/[token]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const POST = withRouteHandler(
124124
}
125125

126126
const response = NextResponse.json({ authType: resolved.share.authType })
127-
setDeploymentAuthCookie({
127+
await setDeploymentAuthCookie({
128128
response,
129129
cookiePrefix: 'file',
130130
resource: resolved.share,

apps/sim/app/f/[token]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ async function renderAuthGate(token: string, share: GateShare) {
9191

9292
const cookieStore = await cookies()
9393
const cookieValue = cookieStore.get(deploymentAuthCookieName('file', share.id))?.value
94-
if (validateAuthToken({ token: cookieValue ?? '', resource: share })) return null
94+
if (await validateAuthToken({ token: cookieValue ?? '', resource: share })) return null
9595

9696
return share.authType === 'email' ? (
9797
<PublicFileEmailAuth token={token} />

0 commit comments

Comments
 (0)