Skip to content

Commit acece91

Browse files
authored
fix(supabase): remove non-functional SQL introspection, harden storage encoding, add missing endpoints (#5371)
* fix(supabase): remove non-functional SQL introspection path, harden storage URL encoding, add missing storage endpoints - introspect.ts no longer attempts raw SQL via a nonexistent PostgREST RPC endpoint (always failed); now uses the OpenAPI-spec path directly with honest heuristic/best-effort documentation for PK/FK/index fields - storage tools now trim + URL-encode bucket/path segments before building request URLs (download, list, get_public_url, create_signed_url, delete_bucket, delete, and the upload API route) - storage_create_signed_url guards against a missing signedURL field in the response instead of silently building a broken URL - add supabase_storage_create_signed_upload_url, supabase_storage_update_bucket, and supabase_storage_empty_bucket tools + block wiring - change insert/upsert `data` param type from 'array' to 'json' to match actual accepted shapes (array or single object) - bump tool versions to semver (1.0 -> 1.0.0) - rewrite a BlockMeta template that implied unsupported Supabase Auth Admin user-provisioning (cherry picked from commit 52b655a) * fix(supabase): address review feedback on update-bucket, signed upload url, and introspect - storage_update_bucket now fetches the bucket's current config first and only overrides isPublic/fileSizeLimit/allowedMimeTypes when the caller explicitly provides them, instead of silently forcing public: false on every update (the Storage API's PUT is a full replace, not a patch) - storage_create_signed_upload_url and storage_empty_bucket now check response.ok before surfacing an error, so a non-2xx response reports Supabase's actual error instead of a misleading parse-side message - introspect.ts drops the spurious ?select=* query param from the OpenAPI spec request (meaningless on the root spec endpoint) (cherry picked from commit 557e407) * fix(supabase): tri-state bucket visibility on update, empty-string param coercion, introspect schema header - introspect.ts now sends Accept-Profile when a schema param is given, matching the convention used by the other DB tools, so schema-scoped introspection actually reads from that schema's spec - storage_update_bucket.ts treats an empty-string param the same as "not provided" (e.g. an untouched file size limit input no longer coerces to 0) - the shared "Public Bucket" dropdown always sent an explicit true/false for storage_update_bucket (its default masked "not touched"), which could still flip a public bucket private; added a dedicated "Keep Current / True / False" control for the update operation so omitting a choice genuinely omits the isPublic override (cherry picked from commit c80567b) * fix(supabase): check response.ok before parsing storage_create_signed_url Matches the pattern already applied to the new signed-upload-url tool this session — a non-2xx response now surfaces Supabase's actual error message instead of the generic "did not return a signed URL path" one. (cherry picked from commit 9f40427) * fix(supabase): correct download param semantics, echoed path field, and schema hint mismatch Found by a second, per-tool parallel validation pass against live Supabase/PostgREST docs and source: - storage_get_public_url.ts and storage_create_signed_url.ts sent download=true literally, which Supabase's Storage API treats as a filename override (renaming the downloaded file to "true") rather than a boolean flag; forcing a download while keeping the original filename requires an empty download= value - storage_create_signed_url.ts also sent download in the POST body, which the sign endpoint ignores entirely — forcing download only works as a query param on the returned URL - storage_create_signed_upload_url.ts read a `path` field from the API response that doesn't exist there; it now echoes the caller-supplied path, matching the official storage-js client - introspect.ts's nullable heuristic didn't disclose that a NOT NULL column with a default is misreported as nullable (PostgREST omits it from the OpenAPI required list in that case); documented alongside the other already-disclosed heuristics, and removed a tautological schema-filter check left over from an earlier revision - insert.ts/upsert.ts's `data` param reverted from 'json' back to 'array': the 'json' type mapped to an LLM-facing JSON-schema type of 'object', which contradicted the param's own description ("array of objects or a single object")
1 parent 943ee27 commit acece91

32 files changed

Lines changed: 670 additions & 500 deletions

apps/sim/app/api/tools/supabase/storage-upload/route.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { processSingleFileToUserFile } from '@/lib/uploads/utils/file-utils'
1111
import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server'
1212
import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response'
1313
import { assertToolFileAccess } from '@/app/api/files/authorization'
14+
import { encodeStoragePath, encodeStorageSegment } from '@/tools/supabase/utils'
1415

1516
export const dynamic = 'force-dynamic'
1617

@@ -185,7 +186,9 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
185186
return NextResponse.json({ success: false, error: projectValidation.error }, { status: 400 })
186187
}
187188

188-
const supabaseUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/${validatedData.bucket}/${fullPath}`
189+
const encodedBucket = encodeStorageSegment(validatedData.bucket)
190+
const encodedPath = encodeStoragePath(fullPath)
191+
const supabaseUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/${encodedBucket}/${encodedPath}`
189192

190193
const headers: Record<string, string> = {
191194
apikey: validatedData.apiKey,
@@ -248,7 +251,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
248251
path: fullPath,
249252
})
250253

251-
const publicUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/public/${validatedData.bucket}/${fullPath}`
254+
const publicUrl = `https://${projectValidation.sanitized}.supabase.co/storage/v1/object/public/${encodedBucket}/${encodedPath}`
252255

253256
return NextResponse.json({
254257
success: true,

apps/sim/blocks/blocks/supabase.ts

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ export const SupabaseBlock: BlockConfig<SupabaseResponse> = {
4545
{ label: 'Storage: Copy File', id: 'storage_copy' },
4646
{ label: 'Storage: Get Public URL', id: 'storage_get_public_url' },
4747
{ label: 'Storage: Create Signed URL', id: 'storage_create_signed_url' },
48+
{ label: 'Storage: Create Signed Upload URL', id: 'storage_create_signed_upload_url' },
4849
{ label: 'Storage: Create Bucket', id: 'storage_create_bucket' },
50+
{ label: 'Storage: Update Bucket', id: 'storage_update_bucket' },
51+
{ label: 'Storage: Empty Bucket', id: 'storage_empty_bucket' },
4952
{ label: 'Storage: List Buckets', id: 'storage_list_buckets' },
5053
{ label: 'Storage: Delete Bucket', id: 'storage_delete_bucket' },
5154
],
@@ -686,9 +689,12 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
686689
'storage_move',
687690
'storage_copy',
688691
'storage_create_bucket',
692+
'storage_update_bucket',
693+
'storage_empty_bucket',
689694
'storage_delete_bucket',
690695
'storage_get_public_url',
691696
'storage_create_signed_url',
697+
'storage_create_signed_upload_url',
692698
],
693699
},
694700
required: true,
@@ -919,19 +925,53 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
919925
value: () => 'false',
920926
condition: { field: 'operation', value: 'storage_create_bucket' },
921927
},
928+
{
929+
id: 'updateIsPublic',
930+
title: 'Public Bucket',
931+
type: 'dropdown',
932+
options: [
933+
{ label: 'Keep Current', id: '' },
934+
{ label: 'False (Private)', id: 'false' },
935+
{ label: 'True (Public)', id: 'true' },
936+
],
937+
value: () => '',
938+
condition: { field: 'operation', value: 'storage_update_bucket' },
939+
},
922940
{
923941
id: 'fileSizeLimit',
924942
title: 'File Size Limit (bytes)',
925943
type: 'short-input',
926944
placeholder: '52428800',
927-
condition: { field: 'operation', value: 'storage_create_bucket' },
945+
condition: { field: 'operation', value: ['storage_create_bucket', 'storage_update_bucket'] },
946+
mode: 'advanced',
928947
},
929948
{
930949
id: 'allowedMimeTypes',
931950
title: 'Allowed MIME Types (JSON array)',
932951
type: 'code',
933952
placeholder: '["image/png", "image/jpeg"]',
934-
condition: { field: 'operation', value: 'storage_create_bucket' },
953+
condition: { field: 'operation', value: ['storage_create_bucket', 'storage_update_bucket'] },
954+
mode: 'advanced',
955+
},
956+
{
957+
id: 'path',
958+
title: 'Destination Path',
959+
type: 'short-input',
960+
placeholder: 'folder/file.jpg',
961+
condition: { field: 'operation', value: 'storage_create_signed_upload_url' },
962+
required: true,
963+
},
964+
{
965+
id: 'upsert',
966+
title: 'Allow Overwrite',
967+
type: 'dropdown',
968+
options: [
969+
{ label: 'False', id: 'false' },
970+
{ label: 'True', id: 'true' },
971+
],
972+
value: () => 'false',
973+
condition: { field: 'operation', value: 'storage_create_signed_upload_url' },
974+
mode: 'advanced',
935975
},
936976
],
937977
tools: {
@@ -955,10 +995,13 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
955995
'supabase_storage_move',
956996
'supabase_storage_copy',
957997
'supabase_storage_create_bucket',
998+
'supabase_storage_update_bucket',
999+
'supabase_storage_empty_bucket',
9581000
'supabase_storage_list_buckets',
9591001
'supabase_storage_delete_bucket',
9601002
'supabase_storage_get_public_url',
9611003
'supabase_storage_create_signed_url',
1004+
'supabase_storage_create_signed_upload_url',
9621005
],
9631006
config: {
9641007
tool: (params) => {
@@ -1001,6 +1044,10 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
10011044
return 'supabase_storage_copy'
10021045
case 'storage_create_bucket':
10031046
return 'supabase_storage_create_bucket'
1047+
case 'storage_update_bucket':
1048+
return 'supabase_storage_update_bucket'
1049+
case 'storage_empty_bucket':
1050+
return 'supabase_storage_empty_bucket'
10041051
case 'storage_list_buckets':
10051052
return 'supabase_storage_list_buckets'
10061053
case 'storage_delete_bucket':
@@ -1009,6 +1056,8 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
10091056
return 'supabase_storage_get_public_url'
10101057
case 'storage_create_signed_url':
10111058
return 'supabase_storage_create_signed_url'
1059+
case 'storage_create_signed_upload_url':
1060+
return 'supabase_storage_create_signed_upload_url'
10121061
default:
10131062
throw new Error(`Invalid Supabase operation: ${params.operation}`)
10141063
}
@@ -1028,6 +1077,7 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
10281077
functionBody,
10291078
functionHeaders,
10301079
method,
1080+
updateIsPublic,
10311081
...rest
10321082
} = params
10331083

@@ -1200,6 +1250,16 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
12001250
result.isPublic = parsedIsPublic
12011251
}
12021252

1253+
// "Keep Current" (empty string) means the caller didn't choose to
1254+
// override visibility — omit `isPublic` entirely so the update
1255+
// tool preserves the bucket's existing public/private setting
1256+
// instead of defaulting to false.
1257+
if (operation === 'storage_update_bucket' && updateIsPublic !== undefined) {
1258+
if (updateIsPublic === 'true' || updateIsPublic === 'false') {
1259+
result.isPublic = updateIsPublic === 'true'
1260+
}
1261+
}
1262+
12031263
if (normalizedFileData !== undefined) {
12041264
result.fileData = normalizedFileData
12051265
}
@@ -1254,6 +1314,11 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
12541314
search: { type: 'string', description: 'Search term for filtering' },
12551315
expiresIn: { type: 'number', description: 'Expiration time in seconds for signed URL' },
12561316
isPublic: { type: 'boolean', description: 'Whether bucket should be public' },
1317+
updateIsPublic: {
1318+
type: 'string',
1319+
description:
1320+
'Visibility override for bucket update: "" keeps the current value, "true"/"false" overrides it',
1321+
},
12571322
fileSizeLimit: { type: 'number', description: 'Maximum file size in bytes' },
12581323
allowedMimeTypes: { type: 'array', description: 'Array of allowed MIME types' },
12591324
},
@@ -1281,7 +1346,15 @@ Return ONLY the PostgREST filter expression - no explanations, no markdown, no e
12811346
},
12821347
signedUrl: {
12831348
type: 'string',
1284-
description: 'Temporary signed URL for storage file',
1349+
description: 'Temporary signed URL for storage file (download or upload)',
1350+
},
1351+
token: {
1352+
type: 'string',
1353+
description: 'Upload token embedded in the signed upload URL',
1354+
},
1355+
path: {
1356+
type: 'string',
1357+
description: 'Destination object path for the signed upload URL',
12851358
},
12861359
tables: {
12871360
type: 'json',
@@ -1300,9 +1373,9 @@ export const SupabaseBlockMeta = {
13001373
templates: [
13011374
{
13021375
icon: SupabaseIcon,
1303-
title: 'Supabase user provisioning',
1376+
title: 'Supabase customer record sync',
13041377
prompt:
1305-
'Build a workflow that listens for Stripe new-customer events, provisions a Supabase user with the correct role and metadata, and emails the welcome login link.',
1378+
'Build a workflow that listens for Stripe new-customer events and upserts a row into a Supabase customers table with the correct plan and metadata, then emails a welcome message.',
13061379
modules: ['agent', 'workflows'],
13071380
category: 'operations',
13081381
tags: ['enterprise', 'automation'],

apps/sim/tools/registry.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3707,14 +3707,17 @@ import {
37073707
supabaseRpcTool,
37083708
supabaseStorageCopyTool,
37093709
supabaseStorageCreateBucketTool,
3710+
supabaseStorageCreateSignedUploadUrlTool,
37103711
supabaseStorageCreateSignedUrlTool,
37113712
supabaseStorageDeleteBucketTool,
37123713
supabaseStorageDeleteTool,
37133714
supabaseStorageDownloadTool,
3715+
supabaseStorageEmptyBucketTool,
37143716
supabaseStorageGetPublicUrlTool,
37153717
supabaseStorageListBucketsTool,
37163718
supabaseStorageListTool,
37173719
supabaseStorageMoveTool,
3720+
supabaseStorageUpdateBucketTool,
37183721
supabaseStorageUploadTool,
37193722
supabaseTextSearchTool,
37203723
supabaseUpdateTool,
@@ -5218,10 +5221,13 @@ export const tools: Record<string, ToolConfig> = {
52185221
supabase_storage_move: supabaseStorageMoveTool,
52195222
supabase_storage_copy: supabaseStorageCopyTool,
52205223
supabase_storage_create_bucket: supabaseStorageCreateBucketTool,
5224+
supabase_storage_update_bucket: supabaseStorageUpdateBucketTool,
5225+
supabase_storage_empty_bucket: supabaseStorageEmptyBucketTool,
52215226
supabase_storage_list_buckets: supabaseStorageListBucketsTool,
52225227
supabase_storage_delete_bucket: supabaseStorageDeleteBucketTool,
52235228
supabase_storage_get_public_url: supabaseStorageGetPublicUrlTool,
52245229
supabase_storage_create_signed_url: supabaseStorageCreateSignedUrlTool,
5230+
supabase_storage_create_signed_upload_url: supabaseStorageCreateSignedUploadUrlTool,
52255231
tailscale_list_devices: tailscaleListDevicesTool,
52265232
tailscale_get_device: tailscaleGetDeviceTool,
52275233
tailscale_delete_device: tailscaleDeleteDeviceTool,

apps/sim/tools/supabase/count.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const countTool: ToolConfig<SupabaseCountParams, SupabaseCountResponse> =
77
id: 'supabase_count',
88
name: 'Supabase Count',
99
description: 'Count rows in a Supabase table',
10-
version: '1.0',
10+
version: '1.0.0',
1111

1212
params: {
1313
projectId: {

apps/sim/tools/supabase/delete.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const deleteTool: ToolConfig<SupabaseDeleteParams, SupabaseDeleteResponse
77
id: 'supabase_delete',
88
name: 'Supabase Delete Row',
99
description: 'Delete rows from a Supabase table based on filter criteria',
10-
version: '1.0',
10+
version: '1.0.0',
1111

1212
params: {
1313
projectId: {

apps/sim/tools/supabase/get_row.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const getRowTool: ToolConfig<SupabaseGetRowParams, SupabaseGetRowResponse
77
id: 'supabase_get_row',
88
name: 'Supabase Get Row',
99
description: 'Get a single row from a Supabase table based on filter criteria',
10-
version: '1.0',
10+
version: '1.0.0',
1111

1212
params: {
1313
projectId: {

apps/sim/tools/supabase/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import { queryTool } from '@/tools/supabase/query'
88
import { rpcTool } from '@/tools/supabase/rpc'
99
import { storageCopyTool } from '@/tools/supabase/storage_copy'
1010
import { storageCreateBucketTool } from '@/tools/supabase/storage_create_bucket'
11+
import { storageCreateSignedUploadUrlTool } from '@/tools/supabase/storage_create_signed_upload_url'
1112
import { storageCreateSignedUrlTool } from '@/tools/supabase/storage_create_signed_url'
1213
import { storageDeleteTool } from '@/tools/supabase/storage_delete'
1314
import { storageDeleteBucketTool } from '@/tools/supabase/storage_delete_bucket'
1415
import { storageDownloadTool } from '@/tools/supabase/storage_download'
16+
import { storageEmptyBucketTool } from '@/tools/supabase/storage_empty_bucket'
1517
import { storageGetPublicUrlTool } from '@/tools/supabase/storage_get_public_url'
1618
import { storageListTool } from '@/tools/supabase/storage_list'
1719
import { storageListBucketsTool } from '@/tools/supabase/storage_list_buckets'
1820
import { storageMoveTool } from '@/tools/supabase/storage_move'
21+
import { storageUpdateBucketTool } from '@/tools/supabase/storage_update_bucket'
1922
import { storageUploadTool } from '@/tools/supabase/storage_upload'
2023
import { textSearchTool } from '@/tools/supabase/text_search'
2124
import { updateTool } from '@/tools/supabase/update'
@@ -45,3 +48,6 @@ export const supabaseStorageListBucketsTool = storageListBucketsTool
4548
export const supabaseStorageDeleteBucketTool = storageDeleteBucketTool
4649
export const supabaseStorageGetPublicUrlTool = storageGetPublicUrlTool
4750
export const supabaseStorageCreateSignedUrlTool = storageCreateSignedUrlTool
51+
export const supabaseStorageCreateSignedUploadUrlTool = storageCreateSignedUploadUrlTool
52+
export const supabaseStorageUpdateBucketTool = storageUpdateBucketTool
53+
export const supabaseStorageEmptyBucketTool = storageEmptyBucketTool

apps/sim/tools/supabase/insert.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const insertTool: ToolConfig<SupabaseInsertParams, SupabaseInsertResponse
77
id: 'supabase_insert',
88
name: 'Supabase Insert',
99
description: 'Insert data into a Supabase table',
10-
version: '1.0',
10+
version: '1.0.0',
1111

1212
params: {
1313
projectId: {

0 commit comments

Comments
 (0)