Skip to content

Commit 092311e

Browse files
fix(api): restore migrated endpoint and SDK compatibility (#6564)
* fix(api): restore migrated endpoint compatibility * fix(api): close remaining migration regressions * fix(files): validate ensured folder paths * fix(files): project folder path validation * fix(files): align archive regression fixture * fix(tables): avoid partial bulk update failures * fix(auth): project legacy knowledge audits
1 parent cb28090 commit 092311e

80 files changed

Lines changed: 1924 additions & 478 deletions

File tree

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
@@ -4107,8 +4107,15 @@
41074107
"type": "object",
41084108
"properties": {
41094109
"contextId": {
4110-
"type": "string",
4111-
"description": "Resume context identifier for the earliest active pause point."
4110+
"anyOf": [
4111+
{
4112+
"type": "string"
4113+
},
4114+
{
4115+
"type": "null"
4116+
}
4117+
],
4118+
"description": "Resume context identifier, or null while every pause point is mid-resume."
41124119
},
41134120
"pausedAt": {
41144121
"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/[documentId]/chunks/[chunkId]/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ function resolveContentProvenance(
3131
request: NextRequest,
3232
principal: Principal,
3333
payload: unknown,
34-
workspaceId: string,
34+
workspaceId: string | undefined,
3535
includeContent: boolean
3636
) {
3737
const resolved = resolveKnowledgeWriteSecretProvenance({
3838
request,
3939
payload,
4040
authType: internalKnowledgeAuthType(principal),
4141
userId: internalKnowledgeActorUserId(principal),
42-
workspaceId,
42+
...(workspaceId ? { workspaceId } : {}),
4343
selectionKeys: includeContent ? ['chunk-content'] : [],
4444
})
4545
if (!resolved.success) {
@@ -93,7 +93,7 @@ export const PUT = defineInternalJsonRoute({
9393
chunkId: params.chunkId,
9494
content: body.content,
9595
enabled: body.enabled,
96-
resolveContentProvenance: ({ workspaceId }: { workspaceId: string }) =>
96+
resolveContentProvenance: ({ workspaceId }: { workspaceId?: string }) =>
9797
resolveContentProvenance(request, principal, body, workspaceId, body.content !== undefined),
9898
}),
9999
useCase: updateKnowledgeChunk,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ function resolveContentProvenance(
3232
request: NextRequest,
3333
principal: Principal,
3434
payload: unknown,
35-
workspaceId: string,
35+
workspaceId: string | undefined,
3636
includeContent: boolean
3737
) {
3838
const resolved = resolveKnowledgeWriteSecretProvenance({
3939
request,
4040
payload,
4141
authType: internalKnowledgeAuthType(principal),
4242
userId: internalKnowledgeActorUserId(principal),
43-
workspaceId,
43+
...(workspaceId ? { workspaceId } : {}),
4444
selectionKeys: includeContent ? ['chunk-content'] : [],
4545
})
4646
if (!resolved.success) {
@@ -95,7 +95,7 @@ export const POST = defineInternalJsonRoute({
9595
documentId: params.documentId,
9696
content: body.content,
9797
enabled: body.enabled,
98-
resolveContentProvenance: ({ workspaceId }: { workspaceId: string }) =>
98+
resolveContentProvenance: ({ workspaceId }: { workspaceId?: string }) =>
9999
resolveContentProvenance(request, principal, body, workspaceId, true),
100100
}),
101101
useCase: createKnowledgeChunk,

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/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 {
910
orchestrationErrorResponse,
@@ -54,9 +55,7 @@ describe('orchestrationErrorResponse', () => {
5455
})
5556

5657
it('answers the code the failure carries, not one derived from its wording', () => {
57-
expect(
58-
orchestrationErrorResponse(new OrchestrationError('not_found', 'Row not found'))?.status
59-
).toBe(404)
58+
expect(orchestrationErrorResponse(new TableRowNotFoundError())?.status).toBe(404)
6059
// The phrase that used to force a 400 no longer decides anything.
6160
expect(
6261
orchestrationErrorResponse(new OrchestrationError('conflict', 'Row 3: must be unique'))

0 commit comments

Comments
 (0)