Skip to content

Commit aed98d1

Browse files
committed
Merge origin/staging into resource-views-canonical
Four conflicts, all from staging changing files this branch had moved. The one that mattered: staging added `assertOoxmlPreviewWithinLimits` — an OOXML zip-bomb guard — to the docx and xlsx preview paths. Both files moved into the file-view unit here, so the import came back as a conflict while the call sites auto-merged. Kept both; a security guard silently lost to a rename is exactly what these merges are for. `base-tags-modal` picked up staging's move of `FIELD_TYPE_LABELS` and `KNOWLEDGE_TAG_DISPLAY_NAME_MAX_LENGTH` into shared constants. Its new `getDocumentIcon` import pointed at the duplicate icons module this branch deleted as byte-identical to `@/components/icons/document-icons`; repointed there, which R3c would have required regardless. `bubble-menu-chrome.ts` was added by staging inside a renamed directory — placed at the new path. Suite: 21136 passed.
2 parents 2dffa1e + 86ac309 commit aed98d1

49 files changed

Lines changed: 907 additions & 344 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/sim/app/api/function/execute/route.ts

Lines changed: 51 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1453,14 +1453,11 @@ async function maybeExportSandboxFileToWorkspace(args: {
14531453
if (!outputSandboxPath) return null
14541454

14551455
if (!outputPath) {
1456-
return NextResponse.json(
1457-
{
1458-
success: false,
1459-
error:
1460-
'outputSandboxPath requires outputPath. Set outputPath to the destination workspace file, e.g. "files/result.csv".',
1461-
output: { result: null, stdout: cleanStdout(stdout), executionTime },
1462-
},
1463-
{ status: 400 }
1456+
return exportFailure(
1457+
'outputSandboxPath requires outputPath. Set outputPath to the destination workspace file, e.g. "files/result.csv".',
1458+
400,
1459+
stdout,
1460+
executionTime
14641461
)
14651462
}
14661463

@@ -1480,13 +1477,11 @@ async function maybeExportSandboxFileToWorkspace(args: {
14801477
if (!access) return exportFailure('Workspace access denied', 403, stdout, executionTime)
14811478

14821479
if (exportedFileContent === undefined) {
1483-
return NextResponse.json(
1484-
{
1485-
success: false,
1486-
error: `Sandbox file "${outputSandboxPath}" was not found or could not be read`,
1487-
output: { result: null, stdout: cleanStdout(stdout), executionTime },
1488-
},
1489-
{ status: 500 }
1480+
return exportFailure(
1481+
`Sandbox file "${outputSandboxPath}" was not found or could not be read`,
1482+
500,
1483+
stdout,
1484+
executionTime
14901485
)
14911486
}
14921487

@@ -1500,13 +1495,11 @@ async function maybeExportSandboxFileToWorkspace(args: {
15001495
const isBinary = !TEXT_MIMES.has(resolvedMimeType)
15011496
const outputBytes = Buffer.byteLength(exportedFileContent, isBinary ? 'base64' : 'utf-8')
15021497
if (outputBytes > MAX_SANDBOX_OUTPUT_BYTES) {
1503-
return NextResponse.json(
1504-
{
1505-
success: false,
1506-
error: `Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`,
1507-
output: { result: null, stdout: cleanStdout(stdout), executionTime },
1508-
},
1509-
{ status: 400 }
1498+
return exportFailure(
1499+
`Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`,
1500+
400,
1501+
stdout,
1502+
executionTime
15101503
)
15111504
}
15121505
const fileBuffer = isBinary
@@ -1579,13 +1572,11 @@ async function maybeExportSandboxFileToWorkspace(args: {
15791572
resources: [{ type: 'file', id: written.id, title: written.name, path: written.vfsPath }],
15801573
})
15811574
} catch (error) {
1582-
return NextResponse.json(
1583-
{
1584-
success: false,
1585-
error: getErrorMessage(error, 'Failed to export sandbox file'),
1586-
output: { result: null, stdout: cleanStdout(stdout), executionTime },
1587-
},
1588-
{ status: 400 }
1575+
return exportFailure(
1576+
getErrorMessage(error, 'Failed to export sandbox file'),
1577+
400,
1578+
stdout,
1579+
executionTime
15891580
)
15901581
}
15911582
}
@@ -1605,17 +1596,11 @@ async function maybeExportSandboxFilesToWorkspace(args: {
16051596
const sandboxFiles = args.outputFiles.filter((file) => file.sandboxPath)
16061597
if (sandboxFiles.length === 0) return null
16071598
if (sandboxFiles.length > MAX_SANDBOX_OUTPUT_FILES) {
1608-
return NextResponse.json(
1609-
{
1610-
success: false,
1611-
error: `Too many sandbox output files requested (${sandboxFiles.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`,
1612-
output: {
1613-
result: null,
1614-
stdout: cleanStdout(args.stdout),
1615-
executionTime: args.executionTime,
1616-
},
1617-
},
1618-
{ status: 400 }
1599+
return exportFailure(
1600+
`Too many sandbox output files requested (${sandboxFiles.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`,
1601+
400,
1602+
args.stdout,
1603+
args.executionTime
16191604
)
16201605
}
16211606

@@ -1667,17 +1652,11 @@ async function maybeExportSandboxFilesToWorkspace(args: {
16671652
const sandboxPath = file.sandboxPath!
16681653
const content = args.exportedFiles?.[sandboxPath]
16691654
if (content === undefined) {
1670-
return NextResponse.json(
1671-
{
1672-
success: false,
1673-
error: `Sandbox file "${sandboxPath}" was not found or could not be read`,
1674-
output: {
1675-
result: null,
1676-
stdout: cleanStdout(args.stdout),
1677-
executionTime: args.executionTime,
1678-
},
1679-
},
1680-
{ status: 500 }
1655+
return exportFailure(
1656+
`Sandbox file "${sandboxPath}" was not found or could not be read`,
1657+
500,
1658+
args.stdout,
1659+
args.executionTime
16811660
)
16821661
}
16831662
const outputPath = file.formatPath ?? file.path
@@ -1690,17 +1669,11 @@ async function maybeExportSandboxFilesToWorkspace(args: {
16901669
const size = Buffer.byteLength(content, isBinary ? 'base64' : 'utf-8')
16911670
totalOutputBytes += size
16921671
if (totalOutputBytes > MAX_SANDBOX_OUTPUT_BYTES) {
1693-
return NextResponse.json(
1694-
{
1695-
success: false,
1696-
error: `Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`,
1697-
output: {
1698-
result: null,
1699-
stdout: cleanStdout(args.stdout),
1700-
executionTime: args.executionTime,
1701-
},
1702-
},
1703-
{ status: 400 }
1672+
return exportFailure(
1673+
`Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`,
1674+
400,
1675+
args.stdout,
1676+
args.executionTime
17041677
)
17051678
}
17061679
const scanBuffer = isBinary ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf-8')
@@ -1740,34 +1713,22 @@ async function maybeExportSandboxFilesToWorkspace(args: {
17401713
)
17411714
validationPaths = validations.map((validation) => validation.vfsPath)
17421715
} catch (error) {
1743-
return NextResponse.json(
1744-
{
1745-
success: false,
1746-
error: getErrorMessage(error, 'Invalid sandbox output destination'),
1747-
output: {
1748-
result: null,
1749-
stdout: cleanStdout(args.stdout),
1750-
executionTime: args.executionTime,
1751-
},
1752-
},
1753-
{ status: 400 }
1716+
return exportFailure(
1717+
getErrorMessage(error, 'Invalid sandbox output destination'),
1718+
400,
1719+
args.stdout,
1720+
args.executionTime
17541721
)
17551722
}
17561723
const duplicateDestination = validationPaths.find(
17571724
(vfsPath, index) => validationPaths.indexOf(vfsPath) !== index
17581725
)
17591726
if (duplicateDestination) {
1760-
return NextResponse.json(
1761-
{
1762-
success: false,
1763-
error: `Duplicate sandbox output destination: ${duplicateDestination}`,
1764-
output: {
1765-
result: null,
1766-
stdout: cleanStdout(args.stdout),
1767-
executionTime: args.executionTime,
1768-
},
1769-
},
1770-
{ status: 400 }
1727+
return exportFailure(
1728+
`Duplicate sandbox output destination: ${duplicateDestination}`,
1729+
400,
1730+
args.stdout,
1731+
args.executionTime
17711732
)
17721733
}
17731734

@@ -1815,17 +1776,11 @@ async function maybeExportSandboxFilesToWorkspace(args: {
18151776
})
18161777
}
18171778
} catch (error) {
1818-
return NextResponse.json(
1819-
{
1820-
success: false,
1821-
error: getErrorMessage(error, 'Failed to export sandbox files'),
1822-
output: {
1823-
result: null,
1824-
stdout: cleanStdout(args.stdout),
1825-
executionTime: args.executionTime,
1826-
},
1827-
},
1828-
{ status: 400 }
1779+
return exportFailure(
1780+
getErrorMessage(error, 'Failed to export sandbox files'),
1781+
400,
1782+
args.stdout,
1783+
args.executionTime
18291784
)
18301785
}
18311786

@@ -1991,7 +1946,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
19911946
const workspaceAccess = workspaceId
19921947
? await checkWorkspaceAccess(workspaceId, auth.userId)
19931948
: undefined
1994-
if (workspaceAccess && !workspaceAccess.hasAccess) {
1949+
if (workspaceAccess && (!workspaceAccess.exists || !workspaceAccess.hasAccess)) {
19951950
logger.warn(`[${requestId}] Function execution denied for workspace`, {
19961951
workspaceId,
19971952
userId: auth.userId,

apps/sim/app/api/jobs/[jobId]/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { toError } from '@sim/utils/errors'
33
import { type NextRequest, NextResponse } from 'next/server'
44
import { getJobStatusContract } from '@/lib/api/contracts/common'
55
import { parseRequest } from '@/lib/api/server'
6+
import { WORKSPACE_KEY_SCOPE_DENIED } from '@/lib/api-key/policy-messages'
67
import { checkHybridAuth } from '@/lib/auth/hybrid'
78
import { getJobQueue } from '@/lib/core/async-jobs'
89
import { generateRequestId } from '@/lib/core/utils/request'
@@ -54,7 +55,7 @@ export const GET = withRouteHandler(
5455
const { getWorkflowById } = await import('@/lib/workflows/utils')
5556
const workflow = await getWorkflowById(metadataToCheck.workflowId as string)
5657
if (!workflow?.workspaceId || workflow.workspaceId !== authResult.workspaceId) {
57-
return createErrorResponse('API key is not authorized for this workspace', 403)
58+
return createErrorResponse(WORKSPACE_KEY_SCOPE_DENIED, 403)
5859
}
5960
}
6061
} else if (metadataToCheck?.userId && metadataToCheck.userId !== authenticatedUserId) {

apps/sim/app/api/knowledge/[id]/documents/[documentId]/tag-definitions/route.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { saveDocumentTagDefinitionsContract } from '@/lib/api/contracts/knowledg
55
import { parseRequest } from '@/lib/api/server'
66
import { getSession } from '@/lib/auth'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
8+
import { getFieldTypeForSlot, SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
99
import {
1010
cleanupUnusedTagDefinitions,
1111
createOrUpdateTagDefinitionsBulk,
@@ -114,6 +114,16 @@ export const POST = withRouteHandler(
114114
{ status: 400 }
115115
)
116116
}
117+
/**
118+
* Slot validity only, not slot/field-type agreement: this route also renames
119+
* existing definitions, which resend whatever pair is already stored.
120+
*/
121+
if (getFieldTypeForSlot(def.tagSlot) === null) {
122+
return NextResponse.json(
123+
{ error: 'Invalid request data', details: `Unsupported tag slot: ${def.tagSlot}` },
124+
{ status: 400 }
125+
)
126+
}
117127
}
118128

119129
const bulkData: BulkTagDefinitionsData = {

apps/sim/app/api/knowledge/[id]/tag-definitions/route.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ vi.mock('@/lib/knowledge/tags/service', () => ({
2323

2424
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
2525

26+
import { KNOWLEDGE_TAG_DISPLAY_NAME_MAX_LENGTH } from '@/lib/knowledge/constants'
2627
import { GET, POST } from '@/app/api/knowledge/[id]/tag-definitions/route'
2728

2829
const KB_ID = 'kb-victim'
@@ -140,5 +141,64 @@ describe('Knowledge Base Tag Definitions API Route', () => {
140141
expect(mockCheckKnowledgeBaseWriteAccess).not.toHaveBeenCalled()
141142
expect(mockCreateTagDefinition).not.toHaveBeenCalled()
142143
})
144+
145+
it('rejects a tag slot this schema has no column for', async () => {
146+
authenticateAs('user-1', 'session')
147+
mockCheckKnowledgeBaseWriteAccess.mockResolvedValue(granted)
148+
149+
const response = await POST(
150+
createMockRequest('POST', { ...CREATE_BODY, tagSlot: 'tag99' }),
151+
params()
152+
)
153+
154+
expect(response.status).toBe(400)
155+
expect(mockCreateTagDefinition).not.toHaveBeenCalled()
156+
})
157+
158+
it('rejects a slot that belongs to a different field type', async () => {
159+
authenticateAs('user-1', 'session')
160+
mockCheckKnowledgeBaseWriteAccess.mockResolvedValue(granted)
161+
162+
const response = await POST(
163+
createMockRequest('POST', {
164+
tagSlot: 'number1',
165+
displayName: 'Mismatch',
166+
fieldType: 'text',
167+
}),
168+
params()
169+
)
170+
171+
expect(response.status).toBe(400)
172+
expect(mockCreateTagDefinition).not.toHaveBeenCalled()
173+
})
174+
175+
it('rejects an unsupported field type', async () => {
176+
authenticateAs('user-1', 'session')
177+
mockCheckKnowledgeBaseWriteAccess.mockResolvedValue(granted)
178+
179+
const response = await POST(
180+
createMockRequest('POST', { ...CREATE_BODY, fieldType: 'nonsense' }),
181+
params()
182+
)
183+
184+
expect(response.status).toBe(400)
185+
expect(mockCreateTagDefinition).not.toHaveBeenCalled()
186+
})
187+
188+
it('rejects a display name longer than the shared limit', async () => {
189+
authenticateAs('user-1', 'session')
190+
mockCheckKnowledgeBaseWriteAccess.mockResolvedValue(granted)
191+
192+
const response = await POST(
193+
createMockRequest('POST', {
194+
...CREATE_BODY,
195+
displayName: 'a'.repeat(KNOWLEDGE_TAG_DISPLAY_NAME_MAX_LENGTH + 1),
196+
}),
197+
params()
198+
)
199+
200+
expect(response.status).toBe(400)
201+
expect(mockCreateTagDefinition).not.toHaveBeenCalled()
202+
})
143203
})
144204
})

apps/sim/app/api/knowledge/[id]/tag-definitions/route.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { createTagDefinitionContract } from '@/lib/api/contracts/knowledge'
55
import { parseRequest } from '@/lib/api/server'
66
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
77
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
8-
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
8+
import { isValidSlotForFieldType } from '@/lib/knowledge/constants'
99
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
1010
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
1111

@@ -76,9 +76,18 @@ export const POST = withRouteHandler(
7676
if (!parsed.success) return parsed.response
7777

7878
const validatedData = parsed.data.body
79-
if (!(SUPPORTED_FIELD_TYPES as readonly string[]).includes(validatedData.fieldType)) {
79+
/**
80+
* The contract types `tagSlot` and `fieldType` as plain strings because
81+
* tightening them to enums cascades into UI form state types, so the pair is
82+
* checked here. Nothing downstream enforces it: the slot column is `text`
83+
* (its Drizzle `enum` is types-only) and the service casts before inserting.
84+
*/
85+
if (!isValidSlotForFieldType(validatedData.tagSlot, validatedData.fieldType)) {
8086
return NextResponse.json(
81-
{ error: 'Invalid request data', details: 'Invalid field type' },
87+
{
88+
error: 'Invalid request data',
89+
details: `Tag slot "${validatedData.tagSlot}" is not valid for field type "${validatedData.fieldType}"`,
90+
},
8291
{ status: 400 }
8392
)
8493
}

0 commit comments

Comments
 (0)