@supacloud/cli 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/dist/index.js +525 -105
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -146,6 +146,7 @@ supacloud-cli queue dlq --queue emails --limit 20
146
146
  supacloud-cli task_events inspect_webhook --ref abc123
147
147
  supacloud-cli database query --sql "select now()"
148
148
  supacloud-cli database query --ref abc123 --file ./queries/vector-search.sql
149
+ supacloud-cli database migration_inventory --ref abc123
149
150
  supacloud-cli database push_migrations --ref abc123 --dir supabase/migrations --dry_run
150
151
  supacloud-cli supabase migration_new --name add_accounts
151
152
  supacloud-cli supabase db_diff --schema public --name add_accounts
@@ -160,8 +161,23 @@ supacloud-cli edge_functions source --ref abc123 --slug hello --output ./hello.t
160
161
  supacloud-cli edge_functions activate --ref abc123 --slug hello --version 3
161
162
  supacloud-cli scheduled_functions list --ref abc123
162
163
  supacloud-cli secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
164
+ supacloud-cli storage list_buckets --ref abc123
165
+ supacloud-cli storage get_bucket --ref abc123 --bucket reports
166
+ supacloud-cli storage create_bucket --ref abc123 --bucket reports --public false \
167
+ --file_size_limit 10485760 --allowed_mime_types "application/pdf,image/png"
168
+ supacloud-cli storage update_bucket --ref abc123 --bucket reports \
169
+ --allowed_mime_types '["application/pdf"]'
170
+ supacloud-cli storage delete_bucket --ref abc123 --bucket reports
163
171
  ```
164
172
 
173
+ `database migration_inventory` reads the canonical migration ledger through the
174
+ project-scoped Management API and prints only a validated JSON array. It rejects
175
+ non-2xx responses, malformed entries, unsafe project refs, duplicate canonical
176
+ migration versions, checksum
177
+ drift, and statement-count mismatches instead of treating them as an empty
178
+ ledger. `database list_migrations` remains available with its legacy SQL-backed,
179
+ human-readable behavior.
180
+
165
181
  `edge_functions deploy --path` bundles local TypeScript and dependencies with
166
182
  Bun and runs a local syntax check before upload. The Management API validates and
167
183
  normalizes the final server-side artifact against the multi-tenant Edge Runtime
package/dist/index.js CHANGED
@@ -6471,7 +6471,7 @@ var ACTION_POLICY = {
6471
6471
  write: ["task_cancel", "task_retry", "update_background_settings"]
6472
6472
  },
6473
6473
  database: {
6474
- read: ["list_tables", "describe_columns", "list_indexes", "list_constraints", "list_extensions", "rls_status", "rls_policies", "list_auth_users", "get_auth_user", "connections", "stats", "slow_queries", "list_migrations", "project_url", "generate_types"],
6474
+ read: ["list_tables", "describe_columns", "list_indexes", "list_constraints", "list_extensions", "rls_status", "rls_policies", "list_auth_users", "get_auth_user", "connections", "stats", "slow_queries", "list_migrations", "migration_inventory", "project_url", "generate_types"],
6475
6475
  write: ["query", "apply_migration", "push_migrations", "baseline_migrations", "create_table_rls"]
6476
6476
  },
6477
6477
  supabase: {
@@ -6483,8 +6483,8 @@ var ACTION_POLICY = {
6483
6483
  write: ["configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config"]
6484
6484
  },
6485
6485
  storage: {
6486
- read: ["status", "list_buckets", "list_files"],
6487
- write: ["upload_base64", "delete_file"]
6486
+ read: ["status", "list_buckets", "get_bucket", "list_files"],
6487
+ write: ["create_bucket", "update_bucket", "delete_bucket", "upload_base64", "delete_file"]
6488
6488
  },
6489
6489
  edge_functions: {
6490
6490
  read: ["list", "source"],
@@ -6784,10 +6784,120 @@ class HttpTransport {
6784
6784
  import { createHash } from "node:crypto";
6785
6785
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
6786
6786
  import { basename, join } from "node:path";
6787
+
6788
+ // src/shared/project-ref.ts
6789
+ var PROJECT_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
6790
+ function projectRefPathSegment(ref, operation) {
6791
+ if (typeof ref !== "string" || !PROJECT_REF_PATTERN.test(ref)) {
6792
+ throw new Error(`'ref' is invalid for ${operation}`);
6793
+ }
6794
+ return encodeURIComponent(ref);
6795
+ }
6796
+
6797
+ // src/shared/tools/database-tools.ts
6787
6798
  var MAX_MIGRATION_VERSION = 9223372036854775807n;
6788
6799
  var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
6789
6800
  var FALLBACK_MIGRATION_VERSION_RANGE = 1000000000000000000n;
6790
6801
  var FALLBACK_MIGRATION_VERSION_LIMIT = FALLBACK_MIGRATION_VERSION_BASE + FALLBACK_MIGRATION_VERSION_RANGE;
6802
+ var MAX_MIGRATION_INVENTORY_BYTES = 64 * 1024 * 1024;
6803
+ function isMigrationInventoryVersion(version) {
6804
+ if (typeof version !== "string" || !/^\d{1,19}$/.test(version))
6805
+ return false;
6806
+ const numericVersion = BigInt(version);
6807
+ return numericVersion >= 1n && numericVersion <= MAX_MIGRATION_VERSION && numericVersion.toString() === version;
6808
+ }
6809
+ function isMigrationInventoryName(name) {
6810
+ return name === null || typeof name === "string" && name.length <= 255 && name.length > 0 && name.trim() === name;
6811
+ }
6812
+ function isMigrationInventoryStatements(statements) {
6813
+ return Array.isArray(statements) && statements.every((statement) => typeof statement === "string" && statement.length > 0 && statement.trim() === statement && !statement.includes("\r"));
6814
+ }
6815
+ function isMigrationInventoryAppliedAt(appliedAt) {
6816
+ return appliedAt === null || typeof appliedAt === "string" && appliedAt.trim() === appliedAt && appliedAt.length > 0 && !Number.isNaN(Date.parse(appliedAt));
6817
+ }
6818
+ function migrationInventoryChecksum(entry) {
6819
+ return createHash("sha256").update(JSON.stringify({
6820
+ version: entry.version,
6821
+ name: entry.name,
6822
+ statements: entry.statements
6823
+ })).digest("hex");
6824
+ }
6825
+ function migrationInventoryEntry(rawEntry) {
6826
+ if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry))
6827
+ return null;
6828
+ const entry = rawEntry;
6829
+ if (!isMigrationInventoryVersion(entry.version))
6830
+ return null;
6831
+ if (!isMigrationInventoryName(entry.name))
6832
+ return null;
6833
+ if (!isMigrationInventoryStatements(entry.statements))
6834
+ return null;
6835
+ if (typeof entry.statement_count !== "number" || !Number.isInteger(entry.statement_count))
6836
+ return null;
6837
+ if (entry.statement_count !== entry.statements.length)
6838
+ return null;
6839
+ if (typeof entry.checksum !== "string" || !/^[0-9a-f]{64}$/.test(entry.checksum))
6840
+ return null;
6841
+ if (!isMigrationInventoryAppliedAt(entry.applied_at))
6842
+ return null;
6843
+ const migration = {
6844
+ version: entry.version,
6845
+ name: entry.name,
6846
+ statements: entry.statements,
6847
+ statement_count: entry.statement_count,
6848
+ checksum: entry.checksum,
6849
+ applied_at: entry.applied_at
6850
+ };
6851
+ return migration.checksum === migrationInventoryChecksum(migration) ? migration : null;
6852
+ }
6853
+ function compareMigrationInventoryEntries(left, right) {
6854
+ const leftVersion = BigInt(left.version);
6855
+ const rightVersion = BigInt(right.version);
6856
+ if (leftVersion === rightVersion)
6857
+ return 0;
6858
+ return leftVersion < rightVersion ? -1 : 1;
6859
+ }
6860
+ function migrationInventory(payload) {
6861
+ if (!Array.isArray(payload))
6862
+ return null;
6863
+ const inventory = [];
6864
+ const versions = new Set;
6865
+ for (const rawEntry of payload) {
6866
+ const entry = migrationInventoryEntry(rawEntry);
6867
+ if (!entry)
6868
+ return null;
6869
+ if (versions.has(entry.version))
6870
+ return null;
6871
+ versions.add(entry.version);
6872
+ inventory.push(entry);
6873
+ }
6874
+ return inventory.sort(compareMigrationInventoryEntries);
6875
+ }
6876
+ function migrationInventoryPath(ref) {
6877
+ return `/v1/projects/${projectRefPathSegment(ref, "migration_inventory")}/database/migrations`;
6878
+ }
6879
+ function migrationInventoryFailure(code, httpStatus) {
6880
+ return {
6881
+ isError: true,
6882
+ content: [{
6883
+ type: "text",
6884
+ text: JSON.stringify({
6885
+ ok: false,
6886
+ operation: "database.migration_inventory",
6887
+ error: { code, http_status: httpStatus }
6888
+ }, null, 2)
6889
+ }]
6890
+ };
6891
+ }
6892
+ function migrationInventoryResponse(response) {
6893
+ if (!response.ok) {
6894
+ return migrationInventoryFailure("HTTP_ERROR", response.transportError ? null : response.status);
6895
+ }
6896
+ const inventory = migrationInventory(response.data);
6897
+ if (!inventory)
6898
+ return migrationInventoryFailure("INVALID_RESPONSE", response.status);
6899
+ return { content: [{ type: "text", text: JSON.stringify(inventory, null, 2) }] };
6900
+ }
6791
6901
  function readMigrationFile(dir, file) {
6792
6902
  const rawBytes = readFileSync2(join(dir, file));
6793
6903
  return {
@@ -6939,6 +7049,7 @@ function registerDatabaseTools(server, http, config = {}) {
6939
7049
  "stats",
6940
7050
  "slow_queries",
6941
7051
  "list_migrations",
7052
+ "migration_inventory",
6942
7053
  "project_url",
6943
7054
  "generate_types"
6944
7055
  ];
@@ -7078,6 +7189,10 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7078
7189
  text = r.ok ? formatMigrations(r.data) : `❌ Failed (${r.status})`;
7079
7190
  break;
7080
7191
  }
7192
+ case "migration_inventory": {
7193
+ const response = await http.get(migrationInventoryPath(ref), { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
7194
+ return migrationInventoryResponse(response);
7195
+ }
7081
7196
  case "project_url": {
7082
7197
  const r = await http.get(`/v1/projects/${ref}`);
7083
7198
  text = r.ok ? JSON.stringify({ url: r.data.api?.url || `https://${ref}.supabase.co` }, null, 2) : `❌ Failed (${r.status})`;
@@ -7729,90 +7844,424 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7729
7844
  });
7730
7845
  }
7731
7846
 
7847
+ // src/shared/tools/release-control-response.ts
7848
+ var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
7849
+ function releaseControlSuccess(operation, payload) {
7850
+ return releaseControlResponse({
7851
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7852
+ ok: true,
7853
+ operation,
7854
+ ...payload
7855
+ });
7856
+ }
7857
+ function releaseControlFailure(operation, code, httpStatus) {
7858
+ return releaseControlErrorResponse({
7859
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7860
+ ok: false,
7861
+ operation,
7862
+ error: { code, http_status: httpStatus }
7863
+ });
7864
+ }
7865
+ function releaseControlMutationFailure(operation, response) {
7866
+ const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
7867
+ return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
7868
+ }
7869
+ function releaseControlResponse(payload) {
7870
+ return {
7871
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
7872
+ };
7873
+ }
7874
+ function releaseControlErrorResponse(payload) {
7875
+ return { ...releaseControlResponse(payload), isError: true };
7876
+ }
7877
+
7732
7878
  // src/shared/tools/storage-tools.ts
7879
+ var MAX_BUCKET_ID_LENGTH = 100;
7880
+ var MAX_MIME_TYPE_COUNT = 100;
7881
+ var MAX_MIME_TYPE_LENGTH = 255;
7882
+ var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
7883
+ var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
7884
+ var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
7885
+ var ACTION_ARGUMENTS = {
7886
+ status: new Set(["action"]),
7887
+ list_buckets: new Set(["action", "ref"]),
7888
+ get_bucket: new Set(["action", "ref", "bucket"]),
7889
+ create_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
7890
+ update_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
7891
+ delete_bucket: new Set(["action", "ref", "bucket"]),
7892
+ list_files: new Set(["action", "ref", "bucket"]),
7893
+ upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
7894
+ delete_file: new Set(["action", "ref", "bucket", "filename"])
7895
+ };
7896
+ function normalizedMimeTypes(candidate) {
7897
+ return Array.isArray(candidate) ? candidate.map((mimeType) => typeof mimeType === "string" ? mimeType.trim() : mimeType) : candidate;
7898
+ }
7899
+ function parseAllowedMimeTypes(input) {
7900
+ if (Array.isArray(input))
7901
+ return normalizedMimeTypes(input);
7902
+ const trimmed = input.trim();
7903
+ if (!trimmed)
7904
+ return [];
7905
+ if (!trimmed.startsWith("[")) {
7906
+ return normalizedMimeTypes(trimmed.split(","));
7907
+ }
7908
+ try {
7909
+ return normalizedMimeTypes(JSON.parse(trimmed));
7910
+ } catch (error) {
7911
+ if (!(error instanceof SyntaxError))
7912
+ throw error;
7913
+ throw new Error("Invalid allowed_mime_types JSON array");
7914
+ }
7915
+ }
7916
+ var allowedMimeTypesSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), Type.Array(Type.String())]), Type.Array(Type.String({
7917
+ minLength: 1,
7918
+ maxLength: MAX_MIME_TYPE_LENGTH,
7919
+ pattern: MIME_TYPE_PATTERN.source
7920
+ }), { maxItems: MAX_MIME_TYPE_COUNT }), parseAllowedMimeTypes));
7921
+ var fileSizeLimitSchema = Type.Optional(Type.Integer({
7922
+ minimum: 1,
7923
+ maximum: Number.MAX_SAFE_INTEGER
7924
+ }));
7925
+ function requiredText(args, field) {
7926
+ const text = args[field];
7927
+ if (typeof text !== "string" || !text.trim()) {
7928
+ throw new Error(`'${field}' required for '${String(args.action)}'`);
7929
+ }
7930
+ return text.trim();
7931
+ }
7932
+ function validBucketId(bucket) {
7933
+ return BUCKET_ID_PATTERN.test(bucket);
7934
+ }
7935
+ function requiredProjectRef(args) {
7936
+ const ref = requiredText(args, "ref");
7937
+ if (!PROJECT_REF_PATTERN2.test(ref))
7938
+ throw new Error("'ref' is invalid for Storage buckets");
7939
+ return ref;
7940
+ }
7941
+ function requiredBucketId(args) {
7942
+ const bucket = requiredText(args, "bucket");
7943
+ if (!validBucketId(bucket))
7944
+ throw new Error("'bucket' is invalid for Storage buckets");
7945
+ return bucket;
7946
+ }
7947
+ function assertActionArguments(action, args) {
7948
+ const allowedArguments = ACTION_ARGUMENTS[action];
7949
+ if (!allowedArguments)
7950
+ throw new Error(`Unsupported Storage action '${action}'`);
7951
+ const unsupported = Object.keys(args).filter((field) => !allowedArguments.has(field));
7952
+ if (unsupported.length > 0)
7953
+ throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
7954
+ }
7955
+ function storageBucketPath(ref, bucket) {
7956
+ if (!PROJECT_REF_PATTERN2.test(ref))
7957
+ throw new Error("'ref' is invalid for Storage buckets");
7958
+ if (bucket !== undefined && !validBucketId(bucket)) {
7959
+ throw new Error("'bucket' is invalid for Storage buckets");
7960
+ }
7961
+ const root = `/v1/projects/${encodeURIComponent(ref)}/storage/buckets`;
7962
+ return bucket === undefined ? root : `${root}/${encodeURIComponent(bucket)}`;
7963
+ }
7964
+ function bucketRecord(candidate) {
7965
+ return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
7966
+ }
7967
+ function isFileSizeLimit(candidate) {
7968
+ return candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0;
7969
+ }
7970
+ function assertBucketSettings(args) {
7971
+ if (args.file_size_limit !== undefined && (args.file_size_limit === null || !isFileSizeLimit(args.file_size_limit))) {
7972
+ throw new Error("'file_size_limit' must be a positive safe integer");
7973
+ }
7974
+ if (args.allowed_mime_types !== undefined && (args.allowed_mime_types === null || !isAllowedMimeTypes(args.allowed_mime_types))) {
7975
+ throw new Error("'allowed_mime_types' is invalid");
7976
+ }
7977
+ }
7978
+ function isAllowedMimeTypes(candidate) {
7979
+ return candidate === null || Array.isArray(candidate) && candidate.length <= MAX_MIME_TYPE_COUNT && candidate.every((mimeType) => typeof mimeType === "string" && mimeType.length <= MAX_MIME_TYPE_LENGTH && MIME_TYPE_PATTERN.test(mimeType));
7980
+ }
7981
+ function safeBucket(candidate, expectedBucket) {
7982
+ const bucket = bucketRecord(candidate);
7983
+ if (bucket === null || typeof bucket.id !== "string" || !validBucketId(bucket.id) || typeof bucket.name !== "string" || !validBucketId(bucket.name) || typeof bucket.public !== "boolean" || !isFileSizeLimit(bucket.file_size_limit) || !isAllowedMimeTypes(bucket.allowed_mime_types) || expectedBucket !== undefined && bucket.id !== expectedBucket && bucket.name !== expectedBucket) {
7984
+ return null;
7985
+ }
7986
+ return {
7987
+ id: bucket.id,
7988
+ name: bucket.name,
7989
+ public: bucket.public,
7990
+ file_size_limit: bucket.file_size_limit,
7991
+ allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types]
7992
+ };
7993
+ }
7994
+ function safeExactBucket(candidate, expectedBucket) {
7995
+ const bucket = safeBucket(candidate);
7996
+ return bucket?.id === expectedBucket ? bucket : null;
7997
+ }
7998
+ function safeBucketList(candidate) {
7999
+ if (!Array.isArray(candidate))
8000
+ return null;
8001
+ const buckets = candidate.map((bucket) => safeBucket(bucket));
8002
+ if (buckets.some((bucket) => bucket === null))
8003
+ return null;
8004
+ const safeBuckets = buckets;
8005
+ const ids = safeBuckets.map((bucket) => bucket.id);
8006
+ const names = safeBuckets.map((bucket) => bucket.name);
8007
+ return new Set(ids).size === ids.length && new Set(names).size === names.length ? safeBuckets : null;
8008
+ }
8009
+ function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
8010
+ const bucket = bucketRecord(candidate);
8011
+ if (bucket?.id !== expectedBucket || bucket.name !== expectedBucket || bucket.public !== (request.public === true))
8012
+ return null;
8013
+ return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
8014
+ }
8015
+ function safeDeletedBucket(candidate, expectedBucket) {
8016
+ const receipt = bucketRecord(candidate);
8017
+ return receipt?.id === expectedBucket && receipt.deleted === true ? { bucket_id: expectedBucket, deleted: true } : null;
8018
+ }
8019
+ function mutationReadbackResponse(expectation) {
8020
+ const { operation, ref, response, expectedBucket, request } = expectation;
8021
+ const readback = safeExactBucket(response.data, expectedBucket);
8022
+ const validReadback = readback?.name === expectedBucket && bucketMatchesRequest(readback, request);
8023
+ if (!response.ok || !validReadback || !readback) {
8024
+ return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
8025
+ }
8026
+ return releaseControlSuccess(operation, { project_ref: ref, bucket: readback });
8027
+ }
8028
+ function bucketResponse(expectation) {
8029
+ const { operation, ref, response, operationKind, safePayload } = expectation;
8030
+ if (!response.ok) {
8031
+ return operationKind === "mutation" ? releaseControlMutationFailure(operation, response) : releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
8032
+ }
8033
+ const payload = safePayload(response.data);
8034
+ if (!payload) {
8035
+ return operationKind === "mutation" ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status) : releaseControlFailure(operation, "INVALID_RESPONSE", null);
8036
+ }
8037
+ return releaseControlSuccess(operation, { project_ref: ref, ...payload });
8038
+ }
8039
+ function createBucketRequest(args) {
8040
+ assertBucketSettings(args);
8041
+ const request = { name: requiredText(args, "bucket") };
8042
+ if (args.public !== undefined)
8043
+ request.public = args.public;
8044
+ if (args.file_size_limit !== undefined)
8045
+ request.file_size_limit = args.file_size_limit;
8046
+ if (args.allowed_mime_types !== undefined)
8047
+ request.allowed_mime_types = args.allowed_mime_types;
8048
+ return request;
8049
+ }
8050
+ function updateBucketRequest(args) {
8051
+ assertBucketSettings(args);
8052
+ const request = Object.fromEntries(["public", "file_size_limit", "allowed_mime_types"].filter((field) => args[field] !== undefined).map((field) => [field, args[field]]));
8053
+ if (Object.keys(request).length === 0)
8054
+ throw new Error("Bucket update requires at least one field");
8055
+ return request;
8056
+ }
8057
+ function equalAllowedMimeTypes(candidate, expected) {
8058
+ if (!Array.isArray(expected))
8059
+ return false;
8060
+ if (expected.length === 0 && candidate === null)
8061
+ return true;
8062
+ return Array.isArray(candidate) && candidate.length === expected.length && candidate.every((mimeType, index) => mimeType === expected[index]);
8063
+ }
8064
+ function bucketMatchesRequest(candidate, request) {
8065
+ const bucket = bucketRecord(candidate);
8066
+ if (!bucket)
8067
+ return false;
8068
+ if (request.public !== undefined && bucket.public !== request.public)
8069
+ return false;
8070
+ if (request.file_size_limit !== undefined && bucket.file_size_limit !== request.file_size_limit)
8071
+ return false;
8072
+ return request.allowed_mime_types === undefined || equalAllowedMimeTypes(bucket.allowed_mime_types, request.allowed_mime_types);
8073
+ }
8074
+ async function createBucketMutationReceipt(http, ref, bucket, request) {
8075
+ return bucketResponse({
8076
+ operation: "storage.create_bucket",
8077
+ ref,
8078
+ response: await http.post(storageBucketPath(ref), request),
8079
+ operationKind: "mutation",
8080
+ safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
8081
+ });
8082
+ }
8083
+ async function updateBucketMutationReceipt(expectation) {
8084
+ const { http, ref, bucket, bucketPath, request } = expectation;
8085
+ return bucketResponse({
8086
+ operation: "storage.update_bucket",
8087
+ ref,
8088
+ response: await http.put(bucketPath, request),
8089
+ operationKind: "mutation",
8090
+ safePayload: (apiPayload) => {
8091
+ const updated = safeExactBucket(apiPayload, bucket);
8092
+ return updated && bucketMatchesRequest(updated, request) ? { bucket: updated } : null;
8093
+ }
8094
+ });
8095
+ }
8096
+ async function listBuckets(http, args) {
8097
+ const ref = requiredProjectRef(args);
8098
+ return bucketResponse({
8099
+ operation: "storage.list_buckets",
8100
+ ref,
8101
+ response: await http.get(storageBucketPath(ref)),
8102
+ operationKind: "read",
8103
+ safePayload: (candidate) => {
8104
+ const buckets = safeBucketList(candidate);
8105
+ return buckets ? { buckets } : null;
8106
+ }
8107
+ });
8108
+ }
8109
+ async function getBucket(http, args) {
8110
+ const ref = requiredProjectRef(args);
8111
+ const bucket = requiredBucketId(args);
8112
+ return bucketResponse({
8113
+ operation: "storage.get_bucket",
8114
+ ref,
8115
+ response: await http.get(storageBucketPath(ref, bucket)),
8116
+ operationKind: "read",
8117
+ safePayload: (candidate) => {
8118
+ const safeReadback = safeBucket(candidate, bucket);
8119
+ return safeReadback ? { bucket: safeReadback } : null;
8120
+ }
8121
+ });
8122
+ }
8123
+ async function createBucket(http, args) {
8124
+ const ref = requiredProjectRef(args);
8125
+ const request = createBucketRequest(args);
8126
+ const bucket = request.name;
8127
+ const bucketPath = storageBucketPath(ref, bucket);
8128
+ const receipt = await createBucketMutationReceipt(http, ref, bucket, request);
8129
+ if (receipt.isError)
8130
+ return receipt;
8131
+ const expectedReadback = { ...request, public: request.public === true };
8132
+ return mutationReadbackResponse({
8133
+ operation: "storage.create_bucket",
8134
+ ref,
8135
+ response: await http.get(bucketPath),
8136
+ expectedBucket: bucket,
8137
+ request: expectedReadback
8138
+ });
8139
+ }
8140
+ async function updateBucket(http, args) {
8141
+ const ref = requiredProjectRef(args);
8142
+ const bucket = requiredBucketId(args);
8143
+ const request = updateBucketRequest(args);
8144
+ const bucketPath = storageBucketPath(ref, bucket);
8145
+ const receipt = await updateBucketMutationReceipt({ http, ref, bucket, bucketPath, request });
8146
+ if (receipt.isError)
8147
+ return receipt;
8148
+ return mutationReadbackResponse({
8149
+ operation: "storage.update_bucket",
8150
+ ref,
8151
+ response: await http.get(bucketPath),
8152
+ expectedBucket: bucket,
8153
+ request
8154
+ });
8155
+ }
8156
+ async function deleteBucket(http, args) {
8157
+ const ref = requiredProjectRef(args);
8158
+ const bucket = requiredBucketId(args);
8159
+ const bucketPath = storageBucketPath(ref, bucket);
8160
+ const receipt = bucketResponse({
8161
+ operation: "storage.delete_bucket",
8162
+ ref,
8163
+ response: await http.delete(bucketPath),
8164
+ operationKind: "mutation",
8165
+ safePayload: (candidate) => safeDeletedBucket(candidate, bucket)
8166
+ });
8167
+ if (receipt.isError)
8168
+ return receipt;
8169
+ const readback = await http.get(bucketPath);
8170
+ if (readback.ok || readback.transportError || readback.status !== 404) {
8171
+ return releaseControlFailure("storage.delete_bucket", "OUTCOME_UNKNOWN", readback.transportError ? null : readback.status);
8172
+ }
8173
+ return receipt;
8174
+ }
8175
+ var BUCKET_ACTION_HANDLERS = {
8176
+ list_buckets: listBuckets,
8177
+ get_bucket: getBucket,
8178
+ create_bucket: createBucket,
8179
+ update_bucket: updateBucket,
8180
+ delete_bucket: deleteBucket
8181
+ };
8182
+ function executeBucketAction(action, http, args) {
8183
+ if (!Object.hasOwn(BUCKET_ACTION_HANDLERS, action))
8184
+ return null;
8185
+ return BUCKET_ACTION_HANDLERS[action](http, args);
8186
+ }
7733
8187
  function registerStorageTools(server, http) {
7734
8188
  server.tool("storage", `S3/MinIO storage management.
7735
- Actions: status, list_buckets, list_files, upload_base64, delete_file`, {
7736
- action: withDescription(stringEnum(["status", "list_buckets", "list_files", "upload_base64", "delete_file"]), "Action"),
7737
- ref: optional(Type.String(), "Project ref (required except for 'status')"),
7738
- bucket: optional(Type.String(), "[list_files/upload/delete] Bucket name"),
7739
- filename: optional(Type.String(), "[upload/delete] File name/path"),
8189
+ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_bucket, list_files, upload_base64, delete_file`, {
8190
+ action: withDescription(stringEnum([
8191
+ "status",
8192
+ "list_buckets",
8193
+ "get_bucket",
8194
+ "create_bucket",
8195
+ "update_bucket",
8196
+ "delete_bucket",
8197
+ "list_files",
8198
+ "upload_base64",
8199
+ "delete_file"
8200
+ ]), "Action"),
8201
+ ref: optional(Type.String({ pattern: PROJECT_REF_PATTERN2.source }), "[list_buckets/get_bucket/create_bucket/update_bucket/delete_bucket/list_files/upload_base64/delete_file] Project ref"),
8202
+ bucket: optional(Type.String({ pattern: BUCKET_ID_PATTERN.source }), "[get_bucket/create_bucket/update_bucket/delete_bucket/list_files/upload_base64/delete_file] Bucket name or ID"),
8203
+ public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
8204
+ file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
8205
+ allowed_mime_types: withDescription(allowedMimeTypesSchema, "[create_bucket/update_bucket] MIME types as a comma-separated or JSON array"),
8206
+ filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
7740
8207
  base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
7741
8208
  mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
7742
8209
  }, async (args) => {
7743
- const { action, ref, bucket, filename, base64_content, mime_type } = args;
7744
- const need = (f, v) => {
7745
- if (!v)
7746
- throw new Error(`'${f}' required for '${action}'`);
7747
- };
7748
- const fmt = (data, label, fmtFn) => {
7749
- return Array.isArray(data) ? fmtFn(data) : JSON.stringify(data, null, 2);
7750
- };
8210
+ const action = String(args.action);
8211
+ assertActionArguments(action, args);
8212
+ const bucketAction = executeBucketAction(action, http, args);
8213
+ const bucketActionResponse = bucketAction ? await bucketAction : null;
8214
+ if (bucketActionResponse)
8215
+ return bucketActionResponse;
7751
8216
  let text;
7752
8217
  switch (action) {
7753
8218
  case "status":
7754
8219
  text = JSON.stringify((await http.get("/v1/storage/status")).data, null, 2);
7755
8220
  break;
7756
- case "list_buckets": {
7757
- need("ref", ref);
7758
- const res = await http.get(`/v1/storage/${ref}/buckets`);
7759
- if (!res.ok) {
7760
- text = `❌ Failed (${res.status})`;
7761
- break;
7762
- }
7763
- const buckets = res.data;
7764
- if (!Array.isArray(buckets) || !buckets.length) {
7765
- text = "No buckets found.";
7766
- break;
7767
- }
7768
- text = `\uD83D\uDCE6 Buckets (${buckets.length}):
7769
- ` + buckets.map((b) => ` - ${b.name} (${b.public ? "\uD83D\uDD13 public" : "\uD83D\uDD12 private"})`).join(`
7770
- `);
7771
- break;
7772
- }
7773
8221
  case "list_files": {
7774
- need("ref", ref);
7775
- need("bucket", bucket);
7776
- const res = await http.get(`/v1/storage/${ref}/buckets/${bucket}/files`);
7777
- if (!res.ok) {
7778
- text = `❌ Failed (${res.status})`;
8222
+ const ref = requiredProjectRef(args);
8223
+ const bucket = requiredBucketId(args);
8224
+ const response = await http.get(`/v1/storage/${ref}/buckets/${bucket}/files`);
8225
+ if (!response.ok) {
8226
+ text = `❌ Failed (${response.status})`;
7779
8227
  break;
7780
8228
  }
7781
- const files = res.data;
8229
+ const files = response.data;
7782
8230
  if (!Array.isArray(files) || !files.length) {
7783
8231
  text = "No files.";
7784
8232
  break;
7785
8233
  }
7786
8234
  text = `\uD83D\uDCC1 Files (${files.length}):
7787
- ` + files.map((f) => ` - ${f.name} (${f.size ? (f.size / 1024).toFixed(1) + "KB" : "?"})`).join(`
8235
+ ` + files.map((file) => ` - ${file.name} (${file.size ? (file.size / 1024).toFixed(1) + "KB" : "?"})`).join(`
7788
8236
  `);
7789
8237
  break;
7790
8238
  }
7791
8239
  case "upload_base64": {
7792
- need("ref", ref);
7793
- need("bucket", bucket);
7794
- need("filename", filename);
7795
- need("base64_content", base64_content);
8240
+ const ref = requiredProjectRef(args);
8241
+ const bucket = requiredBucketId(args);
8242
+ const filename = requiredText(args, "filename");
8243
+ const base64Content = requiredText(args, "base64_content");
7796
8244
  try {
7797
- const buffer = Buffer.from(base64_content, "base64");
7798
- const blob = new Blob([buffer], { type: mime_type || "application/octet-stream" });
8245
+ const buffer = Buffer.from(base64Content, "base64");
8246
+ const blob = new Blob([buffer], { type: typeof args.mime_type === "string" ? args.mime_type : "application/octet-stream" });
7799
8247
  const formData = new FormData;
7800
8248
  formData.append("file", blob, filename);
7801
- const res = await http.postMultipart(`/v1/storage/${ref}/buckets/${bucket}/upload`, formData);
7802
- text = res.ok ? `✅ File ${filename} uploaded to ${bucket}` : `❌ Upload failed (${res.status})`;
7803
- } catch (e) {
7804
- text = `❌ Error: ${e.message}`;
8249
+ const response = await http.postMultipart(`/v1/storage/${ref}/buckets/${bucket}/upload`, formData);
8250
+ text = response.ok ? `✅ File ${filename} uploaded to ${bucket}` : `❌ Upload failed (${response.status})`;
8251
+ } catch (error) {
8252
+ text = `❌ Error: ${error instanceof Error ? error.message : String(error)}`;
7805
8253
  }
7806
8254
  break;
7807
8255
  }
7808
- case "delete_file":
7809
- need("ref", ref);
7810
- need("bucket", bucket);
7811
- need("filename", filename);
7812
- text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` : `❌ Failed`;
8256
+ case "delete_file": {
8257
+ const ref = requiredProjectRef(args);
8258
+ const bucket = requiredBucketId(args);
8259
+ const filename = requiredText(args, "filename");
8260
+ text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` : "❌ Failed";
7813
8261
  break;
8262
+ }
7814
8263
  default:
7815
- text = `❌ Unknown action: ${action}`;
8264
+ return releaseControlFailure(`storage.${action}`, "INVALID_RESPONSE", null);
7816
8265
  }
7817
8266
  return { content: [{ type: "text", text }] };
7818
8267
  });
@@ -7824,39 +8273,6 @@ import { tmpdir } from "node:os";
7824
8273
  import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
7825
8274
  import { promisify } from "node:util";
7826
8275
  import { execFile } from "node:child_process";
7827
-
7828
- // src/shared/tools/release-control-response.ts
7829
- var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
7830
- function releaseControlSuccess(operation, payload) {
7831
- return releaseControlResponse({
7832
- schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7833
- ok: true,
7834
- operation,
7835
- ...payload
7836
- });
7837
- }
7838
- function releaseControlFailure(operation, code, httpStatus) {
7839
- return releaseControlErrorResponse({
7840
- schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7841
- ok: false,
7842
- operation,
7843
- error: { code, http_status: httpStatus }
7844
- });
7845
- }
7846
- function releaseControlMutationFailure(operation, response) {
7847
- const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
7848
- return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
7849
- }
7850
- function releaseControlResponse(payload) {
7851
- return {
7852
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
7853
- };
7854
- }
7855
- function releaseControlErrorResponse(payload) {
7856
- return { ...releaseControlResponse(payload), isError: true };
7857
- }
7858
-
7859
- // src/shared/tools/advanced-tools.ts
7860
8276
  var execFileAsync = promisify(execFile);
7861
8277
  async function runBunBuild(args) {
7862
8278
  try {
@@ -10180,7 +10596,6 @@ var FORBIDDEN_HEADER_NAMES = new Set([
10180
10596
  "via",
10181
10597
  "x-project-ref"
10182
10598
  ]);
10183
- var PROJECT_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
10184
10599
  var SCHEDULE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
10185
10600
  var SAFE_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
10186
10601
  var CRON_PART_PATTERN = /^(\*|([0-9]+)(?:-([0-9]+))?)(?:\/([0-9]+))?$/;
@@ -10191,7 +10606,7 @@ var MAX_HEADER_COUNT = 64;
10191
10606
  var MAX_HEADER_VALUE_LENGTH = 8192;
10192
10607
  var MAX_SCHEDULE_NAME_LENGTH = 120;
10193
10608
  var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
10194
- var ACTION_ARGUMENTS = {
10609
+ var ACTION_ARGUMENTS2 = {
10195
10610
  list: new Set(["action", "ref"]),
10196
10611
  create: new Set(["action", "ref", "name", "slug", "cron", "method", "body_file", "header_env"]),
10197
10612
  update: new Set(["action", "ref", "schedule_id", "name", "cron", "method", "enabled", "body_file", "header_env"]),
@@ -10349,26 +10764,25 @@ function safeSchedule(candidate) {
10349
10764
  updated_at: schedule.updated_at
10350
10765
  };
10351
10766
  }
10352
- function requiredText(args, name, action) {
10767
+ function requiredText2(args, name, action) {
10353
10768
  const candidate = args[name];
10354
10769
  if (typeof candidate !== "string" || !candidate.trim()) {
10355
10770
  throw new Error(`'${name}' is required for '${action}'`);
10356
10771
  }
10357
10772
  return candidate.trim();
10358
10773
  }
10359
- function assertActionArguments(action, args) {
10360
- const unsupported = Object.keys(args).filter((name) => !ACTION_ARGUMENTS[action].has(name));
10774
+ function assertActionArguments2(action, args) {
10775
+ const unsupported = Object.keys(args).filter((name) => !ACTION_ARGUMENTS2[action].has(name));
10361
10776
  if (unsupported.length > 0) {
10362
10777
  throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
10363
10778
  }
10364
10779
  }
10365
10780
  function schedulePath(ref, scheduleId) {
10366
- if (!PROJECT_REF_PATTERN.test(ref))
10367
- throw new Error("'ref' is invalid for Scheduled Functions");
10781
+ const projectRefSegment = projectRefPathSegment(ref, "Scheduled Functions");
10368
10782
  if (scheduleId !== undefined && !SCHEDULE_ID_PATTERN.test(scheduleId)) {
10369
10783
  throw new Error("'schedule_id' is invalid");
10370
10784
  }
10371
- const root = `/v1/projects/${encodeURIComponent(ref)}/scheduled-functions`;
10785
+ const root = `/v1/projects/${projectRefSegment}/scheduled-functions`;
10372
10786
  return scheduleId ? `${root}/${encodeURIComponent(scheduleId)}` : root;
10373
10787
  }
10374
10788
  function scheduleFailure(operation, response) {
@@ -10429,7 +10843,7 @@ function createRequest(args, environment) {
10429
10843
  name: requiredName(args, "create"),
10430
10844
  slug: requiredSlug(args, "create"),
10431
10845
  cron: requiredCron(args, "create"),
10432
- method: requiredText(args, "method", "create"),
10846
+ method: requiredText2(args, "method", "create"),
10433
10847
  body: scheduleBody(args.body_file) ?? {},
10434
10848
  headers: scheduleHeaders(args.header_env, environment) ?? {}
10435
10849
  };
@@ -10445,19 +10859,19 @@ function safeMutationFields(request) {
10445
10859
  return safeFields;
10446
10860
  }
10447
10861
  function requiredName(args, action) {
10448
- const name = requiredText(args, "name", action);
10862
+ const name = requiredText2(args, "name", action);
10449
10863
  if (name.length > MAX_SCHEDULE_NAME_LENGTH)
10450
10864
  throw new Error(`'name' is too long for '${action}'`);
10451
10865
  return name;
10452
10866
  }
10453
10867
  function requiredSlug(args, action) {
10454
- const slug = requiredText(args, "slug", action);
10868
+ const slug = requiredText2(args, "slug", action);
10455
10869
  if (!SAFE_SLUG_PATTERN.test(slug))
10456
10870
  throw new Error(`'slug' is invalid for '${action}'`);
10457
10871
  return slug;
10458
10872
  }
10459
10873
  function requiredCron(args, action) {
10460
- const cron = requiredText(args, "cron", action);
10874
+ const cron = requiredText2(args, "cron", action);
10461
10875
  if (!validScheduledFunctionCron(cron))
10462
10876
  throw new Error(`'cron' is invalid for '${action}'`);
10463
10877
  return cron;
@@ -10484,8 +10898,8 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
10484
10898
  const action = args.action;
10485
10899
  if (readOnly && action !== "list")
10486
10900
  return readOnlyResult2();
10487
- assertActionArguments(action, args);
10488
- const ref = requiredText(args, "ref", action);
10901
+ assertActionArguments2(action, args);
10902
+ const ref = requiredText2(args, "ref", action);
10489
10903
  if (action === "list")
10490
10904
  return listResponse(ref, await http.get(schedulePath(ref)));
10491
10905
  if (action === "create") {
@@ -10493,7 +10907,7 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
10493
10907
  const requestId = request.request_id;
10494
10908
  return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
10495
10909
  }
10496
- const scheduleId = requiredText(args, "schedule_id", action);
10910
+ const scheduleId = requiredText2(args, "schedule_id", action);
10497
10911
  if (action === "update") {
10498
10912
  const request = updateRequest(args, environment);
10499
10913
  const requestId = request.request_id;
@@ -10526,7 +10940,7 @@ var SCHEDULE_TOOL_SCHEMA = {
10526
10940
  // package.json
10527
10941
  var package_default = {
10528
10942
  name: "@supacloud/cli",
10529
- version: "0.15.0",
10943
+ version: "0.16.0",
10530
10944
  description: "Project-scoped CLI for SupaCloud users",
10531
10945
  type: "module",
10532
10946
  main: "./dist/index.js",
@@ -10731,6 +11145,7 @@ EXAMPLES
10731
11145
  ${preferredCommand} frontend list --ref abc123
10732
11146
  ${preferredCommand} database query --sql "select now()"
10733
11147
  ${preferredCommand} database query --ref abc123 --file ./queries/vector-search.sql
11148
+ ${preferredCommand} database migration_inventory --ref abc123
10734
11149
  ${preferredCommand} database push_migrations --ref abc123 --dir supabase/migrations --dry_run
10735
11150
  ${preferredCommand} supabase migration_new --name add_accounts
10736
11151
  ${preferredCommand} supabase db_diff --schema public --name add_accounts
@@ -10824,6 +11239,11 @@ function createCliTools(context, confirmProduction) {
10824
11239
  })
10825
11240
  };
10826
11241
  }
11242
+ const storageContextCallback = tools.storage.callback;
11243
+ const storageHelpTool = captureTools((server) => registerStorageTools(server, {})).storage;
11244
+ if (storageHelpTool) {
11245
+ tools.storage = { schema: storageHelpTool.schema, callback: storageContextCallback };
11246
+ }
10827
11247
  const branchContextCallback = tools.branch.callback;
10828
11248
  const branchHelpTool = captureTools((server) => registerBranchTools(server, {}, {
10829
11249
  readOnly: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",