Skip to content

Commit 33fcc36

Browse files
fix(api): restore migrated endpoint compatibility
1 parent 1dd85eb commit 33fcc36

61 files changed

Lines changed: 906 additions & 147 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/openapi-v2-workflows.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4012,8 +4012,15 @@
40124012
"type": "object",
40134013
"properties": {
40144014
"contextId": {
4015-
"type": "string",
4016-
"description": "Resume context identifier for the earliest active pause point."
4015+
"anyOf": [
4016+
{
4017+
"type": "string"
4018+
},
4019+
{
4020+
"type": "null"
4021+
}
4022+
],
4023+
"description": "Resume context identifier, or null while every pause point is mid-resume."
40174024
},
40184025
"pausedAt": {
40194026
"type": "string",

apps/sim/app/api/credentials/route.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,25 @@ describe('POST /api/credentials', () => {
143143
auditMetadata: { principalKind: 'tenant', principalId: 'acct_123' },
144144
principal: { kind: 'tenant', id: 'acct_123' },
145145
})
146+
queueTableRows(credential, [])
147+
queueTableRows(credential, [])
148+
queueTableRows(credential, [
149+
{
150+
id: 'credential-1',
151+
workspaceId: WORKSPACE_ID,
152+
type: 'service_account',
153+
displayName: 'Zoom account acct_123',
154+
description: null,
155+
providerId: 'zoom-service-account',
156+
accountId: null,
157+
envKey: null,
158+
envOwnerUserId: null,
159+
encryptedServiceAccountKey: 'encrypted-blob',
160+
createdBy: 'user-1',
161+
createdAt: new Date('2026-08-11T00:00:00.000Z'),
162+
updatedAt: new Date('2026-08-11T00:00:00.000Z'),
163+
},
164+
])
146165

147166
const req = createMockRequest('POST', {
148167
workspaceId: WORKSPACE_ID,
@@ -154,8 +173,10 @@ describe('POST /api/credentials', () => {
154173
})
155174

156175
const response = await POST(req)
176+
const body = await response.json()
157177

158178
expect(response.status).toBe(201)
179+
expect(body.credential).not.toHaveProperty('encryptedServiceAccountKey')
159180
expect(mockVerifyAndBuildServiceAccountSecret).toHaveBeenCalledTimes(1)
160181
expect(mockVerifyAndBuildServiceAccountSecret).toHaveBeenCalledWith(
161182
'zoom-service-account',

apps/sim/app/api/credentials/route.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,18 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
274274
)
275275
}
276276

277+
if (!result.credential) {
278+
throw new Error('Credential creation succeeded without a credential')
279+
}
280+
281+
const responseBody = createWorkspaceCredentialContract.response.schema.parse({
282+
credential: {
283+
...result.credential,
284+
createdAt: result.credential.createdAt.toISOString(),
285+
updatedAt: result.credential.updatedAt.toISOString(),
286+
},
287+
})
288+
277289
// An existing credential matched the source: an idempotent replay, not a create.
278-
return NextResponse.json(
279-
{ credential: result.credential },
280-
{ status: result.created ? 201 : 200 }
281-
)
290+
return NextResponse.json(responseBody, { status: result.created ? 201 : 200 })
282291
})

apps/sim/app/api/knowledge/[id]/documents/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export const POST = defineInternalJsonRoute({
8686
rateLimit: internalRateLimits.none({
8787
reason: 'Preserve existing internal document-create behavior',
8888
}),
89-
errorPolicy: internalKnowledgeErrorPolicies.documents,
89+
errorPolicy: internalKnowledgeErrorPolicies.uploads,
9090
mapInput: ({ params, body }, { principal, request }) => {
9191
const documents = body.bulk ? body.documents : [body]
9292
return {

apps/sim/app/api/knowledge/migrated-routes.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,15 @@ vi.mock('@/lib/core/telemetry', () => ({
114114

115115
vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: mocks.capture }))
116116

117+
import { OrchestrationError } from '@/lib/core/orchestration/types'
118+
import { KnowledgeUsageLimitExceededError } from '@/lib/knowledge/application/billing'
117119
import {
118120
GET as listConnectorDocuments,
119121
PATCH as updateConnectorDocuments,
120122
} from '@/app/api/knowledge/[id]/connectors/[connectorId]/documents/route'
121123
import { PUT as updateDocument } from '@/app/api/knowledge/[id]/documents/[documentId]/route'
122124
import {
125+
PATCH as bulkDocuments,
123126
POST as createDocuments,
124127
GET as listDocuments,
125128
} from '@/app/api/knowledge/[id]/documents/route'
@@ -360,6 +363,46 @@ describe('migrated internal Knowledge routes', () => {
360363
)
361364
})
362365

366+
it('preserves payment-required for document usage admission', async () => {
367+
mocks.createDocuments.mockRejectedValueOnce(
368+
new KnowledgeUsageLimitExceededError('Usage limit exceeded')
369+
)
370+
371+
const response = await createDocuments(
372+
createMockRequest('POST', {
373+
bulk: false,
374+
filename: document.filename,
375+
fileUrl: document.fileUrl,
376+
fileSize: document.fileSize,
377+
mimeType: document.mimeType,
378+
}),
379+
{ params: Promise.resolve({ id: 'knowledge-1' }) }
380+
)
381+
382+
expect(response.status).toBe(402)
383+
await expect(response.json()).resolves.toEqual({ error: 'Usage limit exceeded' })
384+
expect(mocks.capture).not.toHaveBeenCalled()
385+
})
386+
387+
it('returns not found when a bulk document selection has no active matches', async () => {
388+
mocks.bulkDocuments.mockRejectedValueOnce(
389+
new OrchestrationError('not_found', 'No valid documents found to update')
390+
)
391+
392+
const response = await bulkDocuments(
393+
createMockRequest('PATCH', {
394+
operation: 'disable',
395+
documentIds: ['document-1'],
396+
}),
397+
{ params: Promise.resolve({ id: 'knowledge-1' }) }
398+
)
399+
400+
expect(response.status).toBe(404)
401+
await expect(response.json()).resolves.toEqual({
402+
error: 'No valid documents found to update',
403+
})
404+
})
405+
363406
it('rejects oversized document-create arrays at the contract boundary', async () => {
364407
const response = await createDocuments(
365408
createMockRequest('POST', {

apps/sim/app/api/mcp/serve/[serverId]/route.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,49 @@ describe('MCP Serve Route', () => {
974974
expect(body.result.isError).toBe(false)
975975
})
976976

977+
it('reports a human-in-the-loop pause as a successful tool result', async () => {
978+
dbChainMockFns.limit
979+
.mockResolvedValueOnce([
980+
{
981+
id: 'server-1',
982+
name: 'Public Server',
983+
workspaceId: 'ws-1',
984+
isPublic: true,
985+
createdBy: 'owner-1',
986+
},
987+
])
988+
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
989+
.mockResolvedValueOnce([{ workspaceId: 'ws-1', deploymentVersionId: 'deployment-1' }])
990+
991+
mockExecuteWorkflowService.mockResolvedValueOnce({
992+
ok: true,
993+
executionId: 'exec-paused',
994+
workflowId: 'wf-1',
995+
status: 'paused',
996+
aborted: null,
997+
output: { approvalRequired: true },
998+
error: null,
999+
hasResponseBlock: false,
1000+
resolvedSecretTraceProvenance: createResolvedSecretTraceProvenance('owner-1'),
1001+
})
1002+
1003+
const req = new NextRequest('http://localhost:3000/api/mcp/serve/server-1', {
1004+
method: 'POST',
1005+
body: JSON.stringify({
1006+
jsonrpc: '2.0',
1007+
id: 1,
1008+
method: 'tools/call',
1009+
params: { name: 'tool_a', arguments: { q: 'test' } },
1010+
}),
1011+
})
1012+
const response = await POST(req, { params: Promise.resolve({ serverId: 'server-1' }) })
1013+
const body = await response.json()
1014+
1015+
expect(response.status).toBe(200)
1016+
expect(body.result.isError).toBe(false)
1017+
expect(body.result.content[0].text).toContain('approvalRequired')
1018+
})
1019+
9771020
it('serializes failed runs with the structured error and child executionId', async () => {
9781021
dbChainMockFns.limit
9791022
.mockResolvedValueOnce([

apps/sim/app/api/mcp/serve/[serverId]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -938,7 +938,7 @@ async function handleToolsCall(
938938
)
939939
}
940940

941-
const isError = serviceResult.status !== 'completed'
941+
const isError = serviceResult.status === 'failed' || serviceResult.status === 'cancelled'
942942
const toolOutput = isError
943943
? {
944944
success: false,

apps/sim/app/api/mcp/servers/route.test.ts

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
/**
22
* @vitest-environment node
33
*/
4-
import { resetDbChainMock } from '@sim/testing'
4+
import { mcpServers } from '@sim/db/schema'
5+
import { queueTableRows, resetDbChainMock } from '@sim/testing'
56
import type { NextRequest } from 'next/server'
67
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'
78

8-
const { mockPerformDeleteMcpServer } = vi.hoisted(() => ({
9+
const { mockCanWrite, mockPerformDeleteMcpServer } = vi.hoisted(() => ({
10+
mockCanWrite: vi.fn(),
911
mockPerformDeleteMcpServer: vi.fn(),
1012
}))
1113

@@ -21,6 +23,7 @@ vi.mock('@/lib/mcp/middleware', () => ({
2123
userName: string
2224
userEmail: string
2325
workspaceId: string
26+
canWrite: boolean
2427
requestId: string
2528
}
2629
) => Promise<Response>
@@ -31,6 +34,7 @@ vi.mock('@/lib/mcp/middleware', () => ({
3134
userName: 'Test User',
3235
userEmail: 'test@example.com',
3336
workspaceId: 'workspace-1',
37+
canWrite: mockCanWrite(),
3438
requestId: 'request-1',
3539
}),
3640
}))
@@ -40,7 +44,13 @@ vi.mock('@/lib/mcp/orchestration', () => ({
4044
performDeleteMcpServer: mockPerformDeleteMcpServer,
4145
}))
4246

43-
import { DELETE } from '@/app/api/mcp/servers/route'
47+
import { DELETE, GET } from '@/app/api/mcp/servers/route'
48+
49+
function createListRequest() {
50+
return new Request('http://localhost:3000/api/mcp/servers?workspaceId=workspace-1', {
51+
method: 'GET',
52+
}) as NextRequest
53+
}
4454

4555
function createDeleteRequest(serverId = 'server-1') {
4656
return new Request(
@@ -87,3 +97,44 @@ describe('MCP servers DELETE route', () => {
8797
expect(body).toEqual({ success: false, error: 'Failed to delete MCP server' })
8898
})
8999
})
100+
101+
describe('MCP servers GET route', () => {
102+
beforeEach(() => {
103+
vi.clearAllMocks()
104+
resetDbChainMock()
105+
mockCanWrite.mockReturnValue(false)
106+
queueTableRows(mcpServers, [
107+
{
108+
id: 'server-1',
109+
workspaceId: 'workspace-1',
110+
name: 'Private server',
111+
headers: { Authorization: 'Bearer secret-token' },
112+
oauthClientSecret: 'oauth-secret',
113+
},
114+
])
115+
})
116+
117+
afterAll(() => {
118+
resetDbChainMock()
119+
})
120+
121+
it('does not expose authentication header values to read-only users', async () => {
122+
const response = await GET(createListRequest())
123+
const body = await response.json()
124+
125+
expect(response.status).toBe(200)
126+
expect(body.data.servers[0]).not.toHaveProperty('headers')
127+
expect(body.data.servers[0]).not.toHaveProperty('oauthClientSecret')
128+
expect(body.data.servers[0].hasOauthClientSecret).toBe(true)
129+
})
130+
131+
it('retains configured headers for users who can manage the server', async () => {
132+
mockCanWrite.mockReturnValue(true)
133+
134+
const response = await GET(createListRequest())
135+
const body = await response.json()
136+
137+
expect(response.status).toBe(200)
138+
expect(body.data.servers[0].headers).toEqual({ Authorization: 'Bearer secret-token' })
139+
})
140+
})

apps/sim/app/api/mcp/servers/route.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const dynamic = 'force-dynamic'
2727
* GET - List all registered MCP servers for the workspace
2828
*/
2929
export const GET = withRouteHandler(
30-
withMcpAuth('read')(async (request: NextRequest, { userId, workspaceId, requestId }) => {
30+
withMcpAuth('read')(async (request: NextRequest, { workspaceId, canWrite, requestId }) => {
3131
try {
3232
logger.info(`[${requestId}] Listing MCP servers for workspace ${workspaceId}`)
3333

@@ -36,8 +36,9 @@ export const GET = withRouteHandler(
3636
.from(mcpServers)
3737
.where(and(eq(mcpServers.workspaceId, workspaceId), isNull(mcpServers.deletedAt)))
3838

39-
const servers = rows.map(({ oauthClientSecret: _secret, ...rest }) => ({
39+
const servers = rows.map(({ oauthClientSecret: _secret, headers, ...rest }) => ({
4040
...rest,
41+
...(canWrite ? { headers } : {}),
4142
hasOauthClientSecret: !!_secret,
4243
}))
4344

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { describe, expect, it } from 'vitest'
55
import { OrchestrationError } from '@/lib/core/orchestration/types'
66
import { TableRowLimitError } from '@/lib/table/billing'
7+
import { TableRowNotFoundError } from '@/lib/table/rows/errors'
78
import type { ColumnDefinition } from '@/lib/table/types'
89
import { rootErrorMessage, rowWriteErrorResponse, tableFilterError } from '@/app/api/table/utils'
910

@@ -49,9 +50,7 @@ describe('rowWriteErrorResponse', () => {
4950
})
5051

5152
it('answers the code the failure carries, not one derived from its wording', () => {
52-
expect(
53-
rowWriteErrorResponse(new OrchestrationError('not_found', 'Row not found'))?.status
54-
).toBe(404)
53+
expect(rowWriteErrorResponse(new TableRowNotFoundError())?.status).toBe(404)
5554
// The phrase that used to force a 400 no longer decides anything.
5655
expect(
5756
rowWriteErrorResponse(new OrchestrationError('conflict', 'Row 3: must be unique'))?.status

0 commit comments

Comments
 (0)