Skip to content

Commit d3772f3

Browse files
committed
fix(security): stop queued Twilio recordings fetches on cancel, type token-route auth mocks, bump chart
- Twilio list calls takes the run's signal: queued recordings requests stop once it aborts, and fetch receives it - Token route tests type the hybrid-auth mock through the real AuthResult and pass a route context - Chart version 1.11.13 for the README and helper comment changes
1 parent 52680e2 commit d3772f3

4 files changed

Lines changed: 47 additions & 17 deletions

File tree

‎apps/sim/app/api/auth/oauth/token/route.test.ts‎

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
createMockRequest,
88
hybridAuthMockFns,
99
} from '@sim/testing'
10+
import { createRouteContext } from '@sim/testing/helpers/http'
1011
import { NextRequest } from 'next/server'
1112
import { beforeEach, describe, expect, it, vi } from 'vitest'
1213

@@ -36,11 +37,13 @@ vi.mock('@/lib/credentials/application/resolve-managed-oauth-token', () => ({
3637
resolveManagedOAuthCredentialToken: { execute: mockResolveManagedOAuthCredentialToken },
3738
}))
3839

40+
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
3941
import { createQuickBooksAccountId } from '@/lib/oauth/quickbooks'
4042
import { GET, POST } from '@/app/api/auth/oauth/token/route'
4143
import { getToolMetadata } from '@/tools/metadata'
4244

4345
const mockGetToolMetadata = vi.mocked(getToolMetadata)
46+
const mockCheckSessionOrInternalAuth = vi.mocked(checkSessionOrInternalAuth)
4447

4548
describe('OAuth Token API Routes', () => {
4649
beforeEach(() => {
@@ -318,9 +321,9 @@ describe('OAuth Token API Routes', () => {
318321
})
319322

320323
it('refuses to release the secret to a session caller', async () => {
321-
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
324+
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
322325
success: true,
323-
authType: 'session',
326+
authType: AuthType.SESSION,
324327
userId: 'test-user-id',
325328
})
326329
mockAuthorizeCredentialUse.mockResolvedValueOnce(authorized)
@@ -329,7 +332,8 @@ describe('OAuth Token API Routes', () => {
329332
createMockRequest('POST', {
330333
credentialId: 'saved-credential-id',
331334
toolId: 'vanta_list_tests',
332-
})
335+
}),
336+
createRouteContext({})
333337
)
334338

335339
expect(response.status).toBe(403)
@@ -340,9 +344,9 @@ describe('OAuth Token API Routes', () => {
340344
})
341345

342346
it('resolves the secret for an internal-JWT execution caller', async () => {
343-
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
347+
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
344348
success: true,
345-
authType: 'internal_jwt',
349+
authType: AuthType.INTERNAL_JWT,
346350
userId: 'test-user-id',
347351
})
348352
mockAuthorizeCredentialUse.mockResolvedValueOnce(authorized)
@@ -352,7 +356,8 @@ describe('OAuth Token API Routes', () => {
352356
credentialId: 'saved-credential-id',
353357
toolId: 'vanta_list_tests',
354358
workflowId: 'workflow-id',
355-
})
359+
}),
360+
createRouteContext({})
356361
)
357362

358363
expect(response.status).toBe(200)
@@ -364,9 +369,9 @@ describe('OAuth Token API Routes', () => {
364369
})
365370

366371
it('still returns an OAuth token to a session caller', async () => {
367-
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
372+
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
368373
success: true,
369-
authType: 'session',
374+
authType: AuthType.SESSION,
370375
userId: 'test-user-id',
371376
})
372377
mockAuthorizeCredentialUse.mockResolvedValueOnce({
@@ -386,7 +391,10 @@ describe('OAuth Token API Routes', () => {
386391
refreshed: false,
387392
})
388393

389-
const response = await POST(createMockRequest('POST', { credentialId: 'credential-id' }))
394+
const response = await POST(
395+
createMockRequest('POST', { credentialId: 'credential-id' }),
396+
createRouteContext({})
397+
)
390398

391399
expect(response.status).toBe(200)
392400
await expect(response.json()).resolves.toMatchObject({

‎apps/sim/tools/twilio_voice/list_calls.test.ts‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ function callsPage(recordingsPaths: string[]) {
2626
})
2727
}
2828

29-
async function listCalls(recordingsPaths: string[]) {
29+
async function listCalls(recordingsPaths: string[], signal?: AbortSignal) {
3030
if (!listCallsTool.transformResponse) throw new Error('Missing response transform')
31-
return listCallsTool.transformResponse(callsPage(recordingsPaths), {
32-
accountSid: 'AC123',
33-
authToken: 'secret',
34-
})
31+
return listCallsTool.transformResponse(
32+
callsPage(recordingsPaths),
33+
{ accountSid: 'AC123', authToken: 'secret' },
34+
{ signal }
35+
)
3536
}
3637

3738
describe('Twilio list calls recordings lookup', () => {
@@ -75,4 +76,22 @@ describe('Twilio list calls recordings lookup', () => {
7576
expect(fetchMock).toHaveBeenCalledTimes(35)
7677
expect(peak).toBe(10)
7778
})
79+
80+
it('starts no queued recordings requests once the run is cancelled', async () => {
81+
const controller = new AbortController()
82+
fetchMock.mockImplementation(async (_url: unknown, init?: RequestInit) => {
83+
controller.abort()
84+
expect(init?.signal).toBe(controller.signal)
85+
return jsonResponse({ recordings: [] })
86+
})
87+
88+
await expect(
89+
listCalls(
90+
Array.from({ length: 35 }, (_, i) => `/Calls/CA${i}/Recordings.json`),
91+
controller.signal
92+
)
93+
).rejects.toThrow()
94+
95+
expect(fetchMock.mock.calls.length).toBeLessThanOrEqual(10)
96+
})
7897
})

‎apps/sim/tools/twilio_voice/list_calls.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
22
import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
33
import { TWILIO_CREDENTIAL_OAUTH } from '@/tools/shared/saved-credentials'
44
import type { TwilioListCallsOutput, TwilioListCallsParams } from '@/tools/twilio_voice/types'
5-
import type { ToolConfig } from '@/tools/types'
5+
import type { ToolConfig, ToolResponseContext } from '@/tools/types'
66

77
const logger = createLogger('TwilioVoiceListCallsTool')
88

@@ -133,7 +133,7 @@ export const listCallsTool: ToolConfig<TwilioListCallsParams, TwilioListCallsOut
133133
},
134134
},
135135

136-
transformResponse: async (response, params) => {
136+
transformResponse: async (response, params, context?: ToolResponseContext) => {
137137
const data = await response.json()
138138

139139
logger.info('Twilio List Calls Response:', { total: data.calls?.length || 0 })
@@ -156,13 +156,15 @@ export const listCallsTool: ToolConfig<TwilioListCallsParams, TwilioListCallsOut
156156
(data.calls || []) as TwilioCallRecord[],
157157
RECORDINGS_FETCH_CONCURRENCY,
158158
async (call) => {
159+
context?.signal?.throwIfAborted()
159160
let recordingSids: string[] = []
160161
const recordingsUrl = twilioApiUrl(call.subresource_uris?.recordings)
161162
if (recordingsUrl) {
162163
try {
163164
const recordingsResponse = await fetch(recordingsUrl, {
164165
method: 'GET',
165166
headers: { Authorization: `Basic ${authToken}` },
167+
signal: context?.signal,
166168
})
167169

168170
if (recordingsResponse.ok) {
@@ -171,6 +173,7 @@ export const listCallsTool: ToolConfig<TwilioListCallsParams, TwilioListCallsOut
171173
recordingSids = (recordingsData.recordings || []).map((rec) => rec.sid)
172174
}
173175
} catch (error) {
176+
context?.signal?.throwIfAborted()
174177
logger.warn('Failed to fetch recordings for call', { callSid: call.sid, error })
175178
}
176179
}

‎helm/sim/Chart.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ apiVersion: v2
22
name: sim
33
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
44
type: application
5-
version: 1.11.12
5+
version: 1.11.13
66
appVersion: "v0.8.26"
77
kubeVersion: ">=1.25.0-0"
88
home: https://sim.ai

0 commit comments

Comments
 (0)