Skip to content

Commit 489f2d3

Browse files
v0.6.60: copilot security improvements, slack canvas ops, retention jobs fixes
2 parents 58a3ae2 + 65e17de commit 489f2d3

29 files changed

Lines changed: 1771 additions & 483 deletions

File tree

apps/sim/app/api/files/multipart/route.ts

Lines changed: 130 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,61 @@ import {
88
isUsingCloudStorage,
99
type StorageContext,
1010
} from '@/lib/uploads'
11+
import {
12+
signUploadToken,
13+
type UploadTokenPayload,
14+
verifyUploadToken,
15+
} from '@/lib/uploads/core/upload-token'
16+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1117

1218
const logger = createLogger('MultipartUploadAPI')
1319

20+
const ALLOWED_UPLOAD_CONTEXTS = new Set<StorageContext>([
21+
'knowledge-base',
22+
'chat',
23+
'copilot',
24+
'mothership',
25+
'execution',
26+
'workspace',
27+
'profile-pictures',
28+
'og-images',
29+
'logs',
30+
'workspace-logos',
31+
])
32+
1433
interface InitiateMultipartRequest {
1534
fileName: string
1635
contentType: string
1736
fileSize: number
37+
workspaceId: string
1838
context?: StorageContext
1939
}
2040

21-
interface GetPartUrlsRequest {
22-
uploadId: string
23-
key: string
41+
interface TokenBoundRequest {
42+
uploadToken: string
43+
}
44+
45+
interface GetPartUrlsRequest extends TokenBoundRequest {
2446
partNumbers: number[]
25-
context?: StorageContext
47+
}
48+
49+
interface CompleteSingleRequest extends TokenBoundRequest {
50+
parts: unknown
51+
}
52+
53+
interface CompleteBatchRequest {
54+
uploads: Array<TokenBoundRequest & { parts: unknown }>
55+
}
56+
57+
const verifyTokenForUser = (token: string | undefined, userId: string) => {
58+
if (!token || typeof token !== 'string') {
59+
return null
60+
}
61+
const result = verifyUploadToken(token)
62+
if (!result.valid || result.payload.userId !== userId) {
63+
return null
64+
}
65+
return result.payload
2666
}
2767

2868
export const POST = withRouteHandler(async (request: NextRequest) => {
@@ -31,6 +71,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3171
if (!session?.user?.id) {
3272
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
3373
}
74+
const userId = session.user.id
3475

3576
const action = request.nextUrl.searchParams.get('action')
3677

@@ -45,32 +86,34 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
4586

4687
switch (action) {
4788
case 'initiate': {
48-
const data: InitiateMultipartRequest = await request.json()
49-
const { fileName, contentType, fileSize, context = 'knowledge-base' } = data
89+
const data = (await request.json()) as InitiateMultipartRequest
90+
const { fileName, contentType, fileSize, workspaceId, context = 'knowledge-base' } = data
5091

51-
const config = getStorageConfig(context)
92+
if (!workspaceId || typeof workspaceId !== 'string') {
93+
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
94+
}
5295

53-
if (storageProvider === 's3') {
54-
const { initiateS3MultipartUpload } = await import('@/lib/uploads/providers/s3/client')
96+
if (!ALLOWED_UPLOAD_CONTEXTS.has(context)) {
97+
return NextResponse.json({ error: 'Invalid storage context' }, { status: 400 })
98+
}
5599

56-
const result = await initiateS3MultipartUpload({
57-
fileName,
58-
contentType,
59-
fileSize,
60-
})
100+
const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
101+
if (permission !== 'write' && permission !== 'admin') {
102+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
103+
}
61104

62-
logger.info(
63-
`Initiated S3 multipart upload for ${fileName} (context: ${context}): ${result.uploadId}`
64-
)
105+
const config = getStorageConfig(context)
65106

66-
return NextResponse.json({
67-
uploadId: result.uploadId,
68-
key: result.key,
69-
})
70-
}
71-
if (storageProvider === 'blob') {
72-
const { initiateMultipartUpload } = await import('@/lib/uploads/providers/blob/client')
107+
let uploadId: string
108+
let key: string
73109

110+
if (storageProvider === 's3') {
111+
const { initiateS3MultipartUpload } = await import('@/lib/uploads/providers/s3/client')
112+
const result = await initiateS3MultipartUpload({ fileName, contentType, fileSize })
113+
uploadId = result.uploadId
114+
key = result.key
115+
} else if (storageProvider === 'blob') {
116+
const { initiateMultipartUpload } = await import('@/lib/uploads/providers/blob/client')
74117
const result = await initiateMultipartUpload({
75118
fileName,
76119
contentType,
@@ -82,46 +125,55 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
82125
connectionString: config.connectionString,
83126
},
84127
})
85-
86-
logger.info(
87-
`Initiated Azure multipart upload for ${fileName} (context: ${context}): ${result.uploadId}`
128+
uploadId = result.uploadId
129+
key = result.key
130+
} else {
131+
return NextResponse.json(
132+
{ error: `Unsupported storage provider: ${storageProvider}` },
133+
{ status: 400 }
88134
)
89-
90-
return NextResponse.json({
91-
uploadId: result.uploadId,
92-
key: result.key,
93-
})
94135
}
95136

96-
return NextResponse.json(
97-
{ error: `Unsupported storage provider: ${storageProvider}` },
98-
{ status: 400 }
137+
const uploadToken = signUploadToken({
138+
uploadId,
139+
key,
140+
userId,
141+
workspaceId,
142+
context,
143+
})
144+
145+
logger.info(
146+
`Initiated ${storageProvider} multipart upload for ${fileName} (context: ${context}, workspace: ${workspaceId}): ${uploadId}`
99147
)
148+
149+
return NextResponse.json({ uploadId, key, uploadToken })
100150
}
101151

102152
case 'get-part-urls': {
103-
const data: GetPartUrlsRequest = await request.json()
104-
const { uploadId, key, partNumbers, context = 'knowledge-base' } = data
153+
const data = (await request.json()) as GetPartUrlsRequest
154+
const { partNumbers } = data
155+
156+
const tokenPayload = verifyTokenForUser(data.uploadToken, userId)
157+
if (!tokenPayload) {
158+
return NextResponse.json({ error: 'Invalid or expired upload token' }, { status: 403 })
159+
}
105160

161+
const { uploadId, key, context } = tokenPayload
106162
const config = getStorageConfig(context)
107163

108164
if (storageProvider === 's3') {
109165
const { getS3MultipartPartUrls } = await import('@/lib/uploads/providers/s3/client')
110-
111166
const presignedUrls = await getS3MultipartPartUrls(key, uploadId, partNumbers)
112-
113167
return NextResponse.json({ presignedUrls })
114168
}
115169
if (storageProvider === 'blob') {
116170
const { getMultipartPartUrls } = await import('@/lib/uploads/providers/blob/client')
117-
118171
const presignedUrls = await getMultipartPartUrls(key, partNumbers, {
119172
containerName: config.containerName!,
120173
accountName: config.accountName!,
121174
accountKey: config.accountKey,
122175
connectionString: config.connectionString,
123176
})
124-
125177
return NextResponse.json({ presignedUrls })
126178
}
127179

@@ -132,24 +184,32 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
132184
}
133185

134186
case 'complete': {
135-
const data = await request.json()
136-
const context: StorageContext = data.context || 'knowledge-base'
187+
const data = (await request.json()) as CompleteSingleRequest | CompleteBatchRequest
137188

138-
const config = getStorageConfig(context)
189+
if ('uploads' in data && Array.isArray(data.uploads)) {
190+
const verified = data.uploads.map((upload) => {
191+
const payload = verifyTokenForUser(upload.uploadToken, userId)
192+
return payload ? { payload, parts: upload.parts } : null
193+
})
194+
195+
if (verified.some((entry) => entry === null)) {
196+
return NextResponse.json({ error: 'Invalid or expired upload token' }, { status: 403 })
197+
}
198+
199+
const verifiedEntries = verified.filter(
200+
(entry): entry is { payload: UploadTokenPayload; parts: unknown } => entry !== null
201+
)
139202

140-
if ('uploads' in data) {
141203
const results = await Promise.all(
142-
data.uploads.map(async (upload: any) => {
143-
const { uploadId, key } = upload
204+
verifiedEntries.map(async ({ payload, parts }) => {
205+
const { uploadId, key, context } = payload
206+
const config = getStorageConfig(context)
144207

145208
if (storageProvider === 's3') {
146209
const { completeS3MultipartUpload } = await import(
147210
'@/lib/uploads/providers/s3/client'
148211
)
149-
const parts = upload.parts // S3 format: { ETag, PartNumber }
150-
151-
const result = await completeS3MultipartUpload(key, uploadId, parts)
152-
212+
const result = await completeS3MultipartUpload(key, uploadId, parts as any)
153213
return {
154214
success: true,
155215
location: result.location,
@@ -161,15 +221,12 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
161221
const { completeMultipartUpload } = await import(
162222
'@/lib/uploads/providers/blob/client'
163223
)
164-
const parts = upload.parts // Azure format: { blockId, partNumber }
165-
166-
const result = await completeMultipartUpload(key, parts, {
224+
const result = await completeMultipartUpload(key, parts as any, {
167225
containerName: config.containerName!,
168226
accountName: config.accountName!,
169227
accountKey: config.accountKey,
170228
connectionString: config.connectionString,
171229
})
172-
173230
return {
174231
success: true,
175232
location: result.location,
@@ -182,19 +239,23 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
182239
})
183240
)
184241

185-
logger.info(`Completed ${data.uploads.length} multipart uploads (context: ${context})`)
242+
logger.info(`Completed ${verifiedEntries.length} multipart uploads`)
186243
return NextResponse.json({ results })
187244
}
188245

189-
const { uploadId, key, parts } = data
246+
const single = data as CompleteSingleRequest
247+
const tokenPayload = verifyTokenForUser(single.uploadToken, userId)
248+
if (!tokenPayload) {
249+
return NextResponse.json({ error: 'Invalid or expired upload token' }, { status: 403 })
250+
}
251+
252+
const { uploadId, key, context } = tokenPayload
253+
const config = getStorageConfig(context)
190254

191255
if (storageProvider === 's3') {
192256
const { completeS3MultipartUpload } = await import('@/lib/uploads/providers/s3/client')
193-
194-
const result = await completeS3MultipartUpload(key, uploadId, parts)
195-
257+
const result = await completeS3MultipartUpload(key, uploadId, single.parts as any)
196258
logger.info(`Completed S3 multipart upload for key ${key} (context: ${context})`)
197-
198259
return NextResponse.json({
199260
success: true,
200261
location: result.location,
@@ -204,16 +265,13 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
204265
}
205266
if (storageProvider === 'blob') {
206267
const { completeMultipartUpload } = await import('@/lib/uploads/providers/blob/client')
207-
208-
const result = await completeMultipartUpload(key, parts, {
268+
const result = await completeMultipartUpload(key, single.parts as any, {
209269
containerName: config.containerName!,
210270
accountName: config.accountName!,
211271
accountKey: config.accountKey,
212272
connectionString: config.connectionString,
213273
})
214-
215274
logger.info(`Completed Azure multipart upload for key ${key} (context: ${context})`)
216-
217275
return NextResponse.json({
218276
success: true,
219277
location: result.location,
@@ -229,27 +287,27 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
229287
}
230288

231289
case 'abort': {
232-
const data = await request.json()
233-
const { uploadId, key, context = 'knowledge-base' } = data
290+
const data = (await request.json()) as TokenBoundRequest
291+
const tokenPayload = verifyTokenForUser(data.uploadToken, userId)
292+
if (!tokenPayload) {
293+
return NextResponse.json({ error: 'Invalid or expired upload token' }, { status: 403 })
294+
}
234295

235-
const config = getStorageConfig(context as StorageContext)
296+
const { uploadId, key, context } = tokenPayload
297+
const config = getStorageConfig(context)
236298

237299
if (storageProvider === 's3') {
238300
const { abortS3MultipartUpload } = await import('@/lib/uploads/providers/s3/client')
239-
240301
await abortS3MultipartUpload(key, uploadId)
241-
242302
logger.info(`Aborted S3 multipart upload for key ${key} (context: ${context})`)
243303
} else if (storageProvider === 'blob') {
244304
const { abortMultipartUpload } = await import('@/lib/uploads/providers/blob/client')
245-
246305
await abortMultipartUpload(key, {
247306
containerName: config.containerName!,
248307
accountName: config.accountName!,
249308
accountKey: config.accountKey,
250309
connectionString: config.connectionString,
251310
})
252-
253311
logger.info(`Aborted Azure multipart upload for key ${key} (context: ${context})`)
254312
} else {
255313
return NextResponse.json(

0 commit comments

Comments
 (0)