From 36c8ea0ffaa41f39b54b92eeafc609f17ea811a8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 13:45:13 -0700 Subject: [PATCH 1/2] fix(account-deletion): list storage keys through the S3 client so Trigger runs can sweep The account storage sweep and Project file prefix cleanup listed S3 keys with `await import('@aws-sdk/client-s3')`. The Trigger.dev bundle resolves that package to its CommonJS build, and the dynamic import's namespace does not carry `ListObjectsV2Command`, so every listing threw "ListObjectsV2Command is not a constructor" and both outbox event types dead-lettered after 10 attempts. The listing now lives in `listS3ObjectKeyPage` beside the other S3 commands, which the S3 client module imports statically. --- apps/sim/lib/uploads/core/prefix-sweep.ts | 25 +++++++----------- apps/sim/lib/uploads/providers/s3/client.ts | 29 ++++++++++++++++++++- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/apps/sim/lib/uploads/core/prefix-sweep.ts b/apps/sim/lib/uploads/core/prefix-sweep.ts index 6786bcd8d55..662d65103b9 100644 --- a/apps/sim/lib/uploads/core/prefix-sweep.ts +++ b/apps/sim/lib/uploads/core/prefix-sweep.ts @@ -120,22 +120,17 @@ export async function listStorageKeyPage( ): Promise { signal.throwIfAborted() if (USE_S3_STORAGE) { - const { ListObjectsV2Command } = await import('@aws-sdk/client-s3') - const { getS3Client } = await import('@/lib/uploads/providers/s3/client') + const { listS3ObjectKeyPage } = await import('@/lib/uploads/providers/s3/client') const config = getStorageConfig(context) - const page = await getS3Client().send( - new ListObjectsV2Command({ - Bucket: config.bucket, - Prefix: prefix, - MaxKeys: STORAGE_KEY_PAGE_SIZE, - ...(pageToken ? { ContinuationToken: pageToken } : {}), - }), - { abortSignal: signal } - ) - return keyPage( - (page.Contents ?? []).flatMap((object) => (object.Key ? [object.Key] : [])), - page.IsTruncated ? page.NextContinuationToken : undefined - ) + if (!config.bucket) throw new Error(`Bucket missing for ${context} storage`) + const page = await listS3ObjectKeyPage({ + bucket: config.bucket, + prefix, + maxKeys: STORAGE_KEY_PAGE_SIZE, + continuationToken: pageToken, + signal, + }) + return keyPage(page.keys, page.nextContinuationToken) } if (USE_BLOB_STORAGE) { const { getBlobServiceClient } = await import('@/lib/uploads/providers/blob/client') diff --git a/apps/sim/lib/uploads/providers/s3/client.ts b/apps/sim/lib/uploads/providers/s3/client.ts index f072664d4cc..9c121703c88 100644 --- a/apps/sim/lib/uploads/providers/s3/client.ts +++ b/apps/sim/lib/uploads/providers/s3/client.ts @@ -7,6 +7,7 @@ import { DeleteObjectsCommand, GetObjectCommand, HeadObjectCommand, + ListObjectsV2Command, ListPartsCommand, PutObjectCommand, S3Client, @@ -325,7 +326,7 @@ export async function deleteS3ObjectVersion(params: { if (!params.etag) throw new Error('S3 upload object is missing its ETag') await getS3Client().send( new DeleteObjectCommand({ - Bucket: params.customConfig.bucket, + Bucket: params.bucket, Key: params.key, IfMatch: params.etag, }) @@ -413,6 +414,32 @@ export async function deleteManyFromS3( return { failed } } +/** + * Lists one page of object keys under `prefix` in `bucket`, resuming from `continuationToken` + * when given; `nextContinuationToken` is set while the listing has more. + */ +export async function listS3ObjectKeyPage(params: { + bucket: string + prefix: string + maxKeys: number + continuationToken?: string + signal: AbortSignal +}): Promise<{ keys: string[]; nextContinuationToken?: string }> { + const page = await getS3Client().send( + new ListObjectsV2Command({ + Bucket: params.bucket, + Prefix: params.prefix, + MaxKeys: params.maxKeys, + ...(params.continuationToken ? { ContinuationToken: params.continuationToken } : {}), + }), + { abortSignal: params.signal } + ) + const keys = (page.Contents ?? []).flatMap((object) => (object.Key ? [object.Key] : [])) + return page.IsTruncated && page.NextContinuationToken + ? { keys, nextContinuationToken: page.NextContinuationToken } + : { keys } +} + /** * Initiate a multipart upload for S3 */ From 6f410001c49ccce6811599dfb3da6fcef968d4c8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 18 Oct 2026 13:54:22 -0700 Subject: [PATCH 2/2] fix(uploads): keep deleteS3ObjectVersion reading its bucket from customConfig --- apps/sim/lib/uploads/providers/s3/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/uploads/providers/s3/client.ts b/apps/sim/lib/uploads/providers/s3/client.ts index 9c121703c88..226e439a9e4 100644 --- a/apps/sim/lib/uploads/providers/s3/client.ts +++ b/apps/sim/lib/uploads/providers/s3/client.ts @@ -326,7 +326,7 @@ export async function deleteS3ObjectVersion(params: { if (!params.etag) throw new Error('S3 upload object is missing its ETag') await getS3Client().send( new DeleteObjectCommand({ - Bucket: params.bucket, + Bucket: params.customConfig.bucket, Key: params.key, IfMatch: params.etag, })