@supacloud/cli 0.15.0 → 0.17.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.
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,15 +6483,15 @@ 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"],
6491
6491
  local: ["check"],
6492
6492
  write: ["deploy", "deploy_bundle", "config", "activate", "delete"]
6493
6493
  },
6494
- scheduled_functions: { read: ["list"], write: ["create", "update", "delete"] },
6494
+ scheduled_functions: { read: ["list", "get"], write: ["create", "update", "delete"] },
6495
6495
  secrets: { read: ["list"], write: ["upsert", "delete"] },
6496
6496
  frontend: {
6497
6497
  read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
@@ -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 {
@@ -6854,6 +6964,28 @@ function migrationRows(data) {
6854
6964
  function migrationIdentityKey(version, name) {
6855
6965
  return `${version}\x00${name}`;
6856
6966
  }
6967
+ function migrationIdentities(data) {
6968
+ return migrationRows(data).flatMap((row) => row.version == null || row.name == null ? [] : [{ version: String(row.version), name: String(row.name) }]);
6969
+ }
6970
+ function identityConflicts(local, remote) {
6971
+ const reusesVersionOrName = local.version === remote.version || local.name === remote.name;
6972
+ const exactlyMatches = local.version === remote.version && local.name === remote.name;
6973
+ return reusesVersionOrName && !exactlyMatches;
6974
+ }
6975
+ function migrationIdentityConflicts(data, migrationFiles) {
6976
+ const remoteMigrations = migrationIdentities(data);
6977
+ return migrationFiles.flatMap((localMigration) => remoteMigrations.filter((remoteMigration) => identityConflicts(localMigration, remoteMigration)).map((remoteMigration) => ({ file: localMigration.file, local: localMigration, remote: remoteMigration })));
6978
+ }
6979
+ function assertNoMigrationIdentityConflicts(data, migrationFiles) {
6980
+ const conflicts = migrationIdentityConflicts(data, migrationFiles);
6981
+ if (!conflicts.length)
6982
+ return;
6983
+ throw new Error([
6984
+ "Migration identity conflicts:",
6985
+ ...conflicts.map(({ file, local, remote }) => `- ${file} (${local.version}) conflicts with remote ${remote.name} (${remote.version})`)
6986
+ ].join(`
6987
+ `));
6988
+ }
6857
6989
  function nameBoundMigrationMarkerKeys(data) {
6858
6990
  const keys = new Set;
6859
6991
  for (const row of migrationRows(data)) {
@@ -6939,6 +7071,7 @@ function registerDatabaseTools(server, http, config = {}) {
6939
7071
  "stats",
6940
7072
  "slow_queries",
6941
7073
  "list_migrations",
7074
+ "migration_inventory",
6942
7075
  "project_url",
6943
7076
  "generate_types"
6944
7077
  ];
@@ -7078,6 +7211,10 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7078
7211
  text = r.ok ? formatMigrations(r.data) : `❌ Failed (${r.status})`;
7079
7212
  break;
7080
7213
  }
7214
+ case "migration_inventory": {
7215
+ const response = await http.get(migrationInventoryPath(ref), { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
7216
+ return migrationInventoryResponse(response);
7217
+ }
7081
7218
  case "project_url": {
7082
7219
  const r = await http.get(`/v1/projects/${ref}`);
7083
7220
  text = r.ok ? JSON.stringify({ url: r.data.api?.url || `https://${ref}.supabase.co` }, null, 2) : `❌ Failed (${r.status})`;
@@ -7112,6 +7249,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7112
7249
  text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
7113
7250
  break;
7114
7251
  }
7252
+ assertNoMigrationIdentityConflicts(migrationsResult.data, migrationFiles);
7115
7253
  if (args.dry_run) {
7116
7254
  const appliedKeys = appliedMigrationKeys(migrationsResult.data);
7117
7255
  const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
@@ -7729,135 +7867,534 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7729
7867
  });
7730
7868
  }
7731
7869
 
7870
+ // src/shared/tools/release-control-response.ts
7871
+ var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
7872
+ function releaseControlSuccess(operation, payload) {
7873
+ return releaseControlResponse({
7874
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7875
+ ok: true,
7876
+ operation,
7877
+ ...payload
7878
+ });
7879
+ }
7880
+ function releaseControlFailure(operation, code, httpStatus) {
7881
+ return releaseControlErrorResponse({
7882
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7883
+ ok: false,
7884
+ operation,
7885
+ error: { code, http_status: httpStatus }
7886
+ });
7887
+ }
7888
+ function releaseControlMutationFailure(operation, response) {
7889
+ const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
7890
+ return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
7891
+ }
7892
+ function releaseControlResponse(payload) {
7893
+ return {
7894
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
7895
+ };
7896
+ }
7897
+ function releaseControlErrorResponse(payload) {
7898
+ return { ...releaseControlResponse(payload), isError: true };
7899
+ }
7900
+
7732
7901
  // src/shared/tools/storage-tools.ts
7902
+ var MAX_BUCKET_ID_LENGTH = 100;
7903
+ var MAX_MIME_TYPE_COUNT = 100;
7904
+ var MAX_MIME_TYPE_LENGTH = 255;
7905
+ var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
7906
+ var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
7907
+ var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
7908
+ var BUCKET_REVISION_PATTERN = /^[0-9]{1,20}$/;
7909
+ var ACTION_ARGUMENTS = {
7910
+ status: new Set(["action"]),
7911
+ list_buckets: new Set(["action", "ref"]),
7912
+ get_bucket: new Set(["action", "ref", "bucket"]),
7913
+ create_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
7914
+ update_bucket: new Set(["action", "ref", "bucket", "expected_revision", "public", "file_size_limit", "allowed_mime_types"]),
7915
+ delete_bucket: new Set(["action", "ref", "bucket", "expected_revision", "require_empty"]),
7916
+ list_files: new Set(["action", "ref", "bucket"]),
7917
+ upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
7918
+ delete_file: new Set(["action", "ref", "bucket", "filename"])
7919
+ };
7920
+ function normalizedMimeTypes(candidate) {
7921
+ return Array.isArray(candidate) ? candidate.map((mimeType) => typeof mimeType === "string" ? mimeType.trim() : mimeType) : candidate;
7922
+ }
7923
+ function parseAllowedMimeTypes(input) {
7924
+ if (Array.isArray(input))
7925
+ return normalizedMimeTypes(input);
7926
+ const trimmed = input.trim();
7927
+ if (!trimmed)
7928
+ return [];
7929
+ if (!trimmed.startsWith("[")) {
7930
+ return normalizedMimeTypes(trimmed.split(","));
7931
+ }
7932
+ try {
7933
+ return normalizedMimeTypes(JSON.parse(trimmed));
7934
+ } catch (error) {
7935
+ if (!(error instanceof SyntaxError))
7936
+ throw error;
7937
+ throw new Error("Invalid allowed_mime_types JSON array");
7938
+ }
7939
+ }
7940
+ var allowedMimeTypesSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), Type.Array(Type.String())]), Type.Array(Type.String({
7941
+ minLength: 1,
7942
+ maxLength: MAX_MIME_TYPE_LENGTH,
7943
+ pattern: MIME_TYPE_PATTERN.source
7944
+ }), { maxItems: MAX_MIME_TYPE_COUNT }), parseAllowedMimeTypes));
7945
+ var fileSizeLimitSchema = Type.Optional(Type.Integer({
7946
+ minimum: 1,
7947
+ maximum: Number.MAX_SAFE_INTEGER
7948
+ }));
7949
+ function requiredText(args, field) {
7950
+ const text = args[field];
7951
+ if (typeof text !== "string" || !text.trim()) {
7952
+ throw new Error(`'${field}' required for '${String(args.action)}'`);
7953
+ }
7954
+ return text.trim();
7955
+ }
7956
+ function validBucketId(bucket) {
7957
+ return BUCKET_ID_PATTERN.test(bucket);
7958
+ }
7959
+ function requiredProjectRef(args) {
7960
+ const ref = requiredText(args, "ref");
7961
+ if (!PROJECT_REF_PATTERN2.test(ref))
7962
+ throw new Error("'ref' is invalid for Storage buckets");
7963
+ return ref;
7964
+ }
7965
+ function requiredBucketId(args) {
7966
+ const bucket = requiredText(args, "bucket");
7967
+ if (!validBucketId(bucket))
7968
+ throw new Error("'bucket' is invalid for Storage buckets");
7969
+ return bucket;
7970
+ }
7971
+ function requiredBucketRevision(args) {
7972
+ const revision = requiredText(args, "expected_revision");
7973
+ if (!BUCKET_REVISION_PATTERN.test(revision))
7974
+ throw new Error("'expected_revision' is invalid for Storage buckets");
7975
+ return revision;
7976
+ }
7977
+ function assertEmptyBucketDeletion(args) {
7978
+ if (args.require_empty !== true)
7979
+ throw new Error("'require_empty=true' required for 'delete_bucket'");
7980
+ }
7981
+ function assertActionArguments(action, args) {
7982
+ const allowedArguments = ACTION_ARGUMENTS[action];
7983
+ if (!allowedArguments)
7984
+ throw new Error(`Unsupported Storage action '${action}'`);
7985
+ const unsupported = Object.keys(args).filter((field) => !allowedArguments.has(field));
7986
+ if (unsupported.length > 0)
7987
+ throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
7988
+ }
7989
+ function storageBucketPath(ref, bucket) {
7990
+ if (!PROJECT_REF_PATTERN2.test(ref))
7991
+ throw new Error("'ref' is invalid for Storage buckets");
7992
+ if (bucket !== undefined && !validBucketId(bucket)) {
7993
+ throw new Error("'bucket' is invalid for Storage buckets");
7994
+ }
7995
+ const root = `/v1/projects/${encodeURIComponent(ref)}/storage/buckets`;
7996
+ return bucket === undefined ? root : `${root}/${encodeURIComponent(bucket)}`;
7997
+ }
7998
+ function bucketRecord(candidate) {
7999
+ return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
8000
+ }
8001
+ function isFileSizeLimit(candidate) {
8002
+ return candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0;
8003
+ }
8004
+ function assertBucketSettings(args) {
8005
+ if (args.file_size_limit !== undefined && (args.file_size_limit === null || !isFileSizeLimit(args.file_size_limit))) {
8006
+ throw new Error("'file_size_limit' must be a positive safe integer");
8007
+ }
8008
+ if (args.allowed_mime_types !== undefined && (args.allowed_mime_types === null || !isAllowedMimeTypes(args.allowed_mime_types))) {
8009
+ throw new Error("'allowed_mime_types' is invalid");
8010
+ }
8011
+ }
8012
+ function isAllowedMimeTypes(candidate) {
8013
+ 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));
8014
+ }
8015
+ function isBucketRevision(candidate) {
8016
+ return candidate === null || typeof candidate === "string" && BUCKET_REVISION_PATTERN.test(candidate);
8017
+ }
8018
+ function safeBucket(candidate, expectedBucket) {
8019
+ const bucket = bucketRecord(candidate);
8020
+ 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) || !isBucketRevision(bucket.revision) || expectedBucket !== undefined && bucket.id !== expectedBucket && bucket.name !== expectedBucket) {
8021
+ return null;
8022
+ }
8023
+ return {
8024
+ id: bucket.id,
8025
+ name: bucket.name,
8026
+ public: bucket.public,
8027
+ file_size_limit: bucket.file_size_limit,
8028
+ allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types],
8029
+ revision: bucket.revision
8030
+ };
8031
+ }
8032
+ function safeExactBucket(candidate, expectedBucket) {
8033
+ const bucket = safeBucket(candidate);
8034
+ return bucket?.id === expectedBucket ? bucket : null;
8035
+ }
8036
+ function safeBucketList(candidate) {
8037
+ if (!Array.isArray(candidate))
8038
+ return null;
8039
+ const buckets = candidate.map((bucket) => safeBucket(bucket));
8040
+ if (buckets.some((bucket) => bucket === null))
8041
+ return null;
8042
+ const safeBuckets = buckets;
8043
+ const ids = safeBuckets.map((bucket) => bucket.id);
8044
+ const names = safeBuckets.map((bucket) => bucket.name);
8045
+ return new Set(ids).size === ids.length && new Set(names).size === names.length ? safeBuckets : null;
8046
+ }
8047
+ function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
8048
+ const bucket = bucketRecord(candidate);
8049
+ if (bucket?.id !== expectedBucket || bucket.name !== expectedBucket || bucket.public !== (request.public === true))
8050
+ return null;
8051
+ return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
8052
+ }
8053
+ function safeDeletedBucket(candidate, expectedBucket, expectedRevision) {
8054
+ const receipt = bucketRecord(candidate);
8055
+ return receipt?.id === expectedBucket && receipt.deleted === true && receipt.require_empty === true && receipt.previous_revision === expectedRevision && receipt.new_revision === null ? {
8056
+ bucket_id: expectedBucket,
8057
+ deleted: true,
8058
+ require_empty: true,
8059
+ previous_revision: expectedRevision,
8060
+ new_revision: null
8061
+ } : null;
8062
+ }
8063
+ function mutationReadbackResponse(expectation) {
8064
+ const { operation, ref, response, expectedBucket, request, previousRevision, expectedNewRevision } = expectation;
8065
+ const readback = safeExactBucket(response.data, expectedBucket);
8066
+ const validReadback = readback?.name === expectedBucket && readback.revision !== null && (expectedNewRevision === undefined || readback.revision === expectedNewRevision) && bucketMatchesRequest(readback, request);
8067
+ if (!response.ok || !validReadback || !readback) {
8068
+ return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
8069
+ }
8070
+ return releaseControlSuccess(operation, {
8071
+ project_ref: ref,
8072
+ bucket_id: expectedBucket,
8073
+ previous_revision: previousRevision,
8074
+ new_revision: readback.revision,
8075
+ bucket: readback
8076
+ });
8077
+ }
8078
+ function bucketResponse(expectation) {
8079
+ const { operation, ref, response, operationKind, safePayload } = expectation;
8080
+ if (!response.ok) {
8081
+ return operationKind === "mutation" ? releaseControlMutationFailure(operation, response) : releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
8082
+ }
8083
+ const payload = safePayload(response.data);
8084
+ if (!payload) {
8085
+ return operationKind === "mutation" ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status) : releaseControlFailure(operation, "INVALID_RESPONSE", null);
8086
+ }
8087
+ return releaseControlSuccess(operation, { project_ref: ref, ...payload });
8088
+ }
8089
+ function createBucketRequest(args) {
8090
+ assertBucketSettings(args);
8091
+ const request = { name: requiredText(args, "bucket") };
8092
+ if (args.public !== undefined)
8093
+ request.public = args.public;
8094
+ if (args.file_size_limit !== undefined)
8095
+ request.file_size_limit = args.file_size_limit;
8096
+ if (args.allowed_mime_types !== undefined)
8097
+ request.allowed_mime_types = args.allowed_mime_types;
8098
+ return request;
8099
+ }
8100
+ function updateBucketRequest(args) {
8101
+ assertBucketSettings(args);
8102
+ const settings = Object.fromEntries(["public", "file_size_limit", "allowed_mime_types"].filter((field) => args[field] !== undefined).map((field) => [field, args[field]]));
8103
+ if (Object.keys(settings).length === 0)
8104
+ throw new Error("Bucket update requires at least one field");
8105
+ return { expected_revision: requiredBucketRevision(args), ...settings };
8106
+ }
8107
+ function equalAllowedMimeTypes(candidate, expected) {
8108
+ if (!Array.isArray(expected))
8109
+ return false;
8110
+ if (expected.length === 0 && candidate === null)
8111
+ return true;
8112
+ return Array.isArray(candidate) && candidate.length === expected.length && candidate.every((mimeType, index) => mimeType === expected[index]);
8113
+ }
8114
+ function bucketMatchesRequest(candidate, request) {
8115
+ const bucket = bucketRecord(candidate);
8116
+ if (!bucket)
8117
+ return false;
8118
+ if (request.public !== undefined && bucket.public !== request.public)
8119
+ return false;
8120
+ if (request.file_size_limit !== undefined && bucket.file_size_limit !== request.file_size_limit)
8121
+ return false;
8122
+ return request.allowed_mime_types === undefined || equalAllowedMimeTypes(bucket.allowed_mime_types, request.allowed_mime_types);
8123
+ }
8124
+ async function createBucketMutationReceipt(http, ref, bucket, request) {
8125
+ return bucketResponse({
8126
+ operation: "storage.create_bucket",
8127
+ ref,
8128
+ response: await http.post(storageBucketPath(ref), request),
8129
+ operationKind: "mutation",
8130
+ safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
8131
+ });
8132
+ }
8133
+ function safeUpdatedBucket(candidate, expectedBucket, expectedRevision, request) {
8134
+ const receipt = bucketRecord(candidate);
8135
+ const bucket = safeExactBucket(candidate, expectedBucket);
8136
+ return bucket && bucket.name === expectedBucket && bucket.revision !== null && bucket.revision !== expectedRevision && receipt?.previous_revision === expectedRevision && receipt.new_revision === bucket.revision && bucketMatchesRequest(bucket, request) ? bucket : null;
8137
+ }
8138
+ async function listBuckets(http, args) {
8139
+ const ref = requiredProjectRef(args);
8140
+ return bucketResponse({
8141
+ operation: "storage.list_buckets",
8142
+ ref,
8143
+ response: await http.get(storageBucketPath(ref)),
8144
+ operationKind: "read",
8145
+ safePayload: (candidate) => {
8146
+ const buckets = safeBucketList(candidate);
8147
+ return buckets ? { buckets } : null;
8148
+ }
8149
+ });
8150
+ }
8151
+ async function getBucket(http, args) {
8152
+ const ref = requiredProjectRef(args);
8153
+ const bucket = requiredBucketId(args);
8154
+ return bucketResponse({
8155
+ operation: "storage.get_bucket",
8156
+ ref,
8157
+ response: await http.get(storageBucketPath(ref, bucket)),
8158
+ operationKind: "read",
8159
+ safePayload: (candidate) => {
8160
+ const safeReadback = safeBucket(candidate, bucket);
8161
+ return safeReadback ? { bucket: safeReadback } : null;
8162
+ }
8163
+ });
8164
+ }
8165
+ async function createBucket(http, args) {
8166
+ const ref = requiredProjectRef(args);
8167
+ const request = createBucketRequest(args);
8168
+ const bucket = request.name;
8169
+ const bucketPath = storageBucketPath(ref, bucket);
8170
+ const receipt = await createBucketMutationReceipt(http, ref, bucket, request);
8171
+ if (receipt.isError)
8172
+ return receipt;
8173
+ const expectedReadback = { ...request, public: request.public === true };
8174
+ return mutationReadbackResponse({
8175
+ operation: "storage.create_bucket",
8176
+ ref,
8177
+ response: await http.get(bucketPath),
8178
+ expectedBucket: bucket,
8179
+ request: expectedReadback,
8180
+ previousRevision: null
8181
+ });
8182
+ }
8183
+ async function updateBucket(http, args) {
8184
+ const ref = requiredProjectRef(args);
8185
+ const bucket = requiredBucketId(args);
8186
+ const request = updateBucketRequest(args);
8187
+ const expectedRevision = request.expected_revision;
8188
+ const bucketPath = storageBucketPath(ref, bucket);
8189
+ const mutation = await http.put(bucketPath, request);
8190
+ if (!mutation.ok)
8191
+ return releaseControlMutationFailure("storage.update_bucket", mutation);
8192
+ const updated = safeUpdatedBucket(mutation.data, bucket, expectedRevision, request);
8193
+ if (!updated)
8194
+ return releaseControlFailure("storage.update_bucket", "OUTCOME_UNKNOWN", mutation.status);
8195
+ return mutationReadbackResponse({
8196
+ operation: "storage.update_bucket",
8197
+ ref,
8198
+ response: await http.get(bucketPath),
8199
+ expectedBucket: bucket,
8200
+ request,
8201
+ previousRevision: expectedRevision,
8202
+ expectedNewRevision: updated.revision ?? undefined
8203
+ });
8204
+ }
8205
+ async function deleteBucket(http, args) {
8206
+ const ref = requiredProjectRef(args);
8207
+ const bucket = requiredBucketId(args);
8208
+ const expectedRevision = requiredBucketRevision(args);
8209
+ assertEmptyBucketDeletion(args);
8210
+ const bucketPath = storageBucketPath(ref, bucket);
8211
+ const deletePath = `${bucketPath}?expected_revision=${encodeURIComponent(expectedRevision)}&require_empty=true`;
8212
+ const receipt = bucketResponse({
8213
+ operation: "storage.delete_bucket",
8214
+ ref,
8215
+ response: await http.delete(deletePath),
8216
+ operationKind: "mutation",
8217
+ safePayload: (candidate) => safeDeletedBucket(candidate, bucket, expectedRevision)
8218
+ });
8219
+ if (receipt.isError)
8220
+ return receipt;
8221
+ const readback = await http.get(bucketPath);
8222
+ if (readback.ok || readback.transportError || readback.status !== 404) {
8223
+ return releaseControlFailure("storage.delete_bucket", "OUTCOME_UNKNOWN", readback.transportError ? null : readback.status);
8224
+ }
8225
+ return receipt;
8226
+ }
8227
+ var BUCKET_ACTION_HANDLERS = {
8228
+ list_buckets: listBuckets,
8229
+ get_bucket: getBucket,
8230
+ create_bucket: createBucket,
8231
+ update_bucket: updateBucket,
8232
+ delete_bucket: deleteBucket
8233
+ };
8234
+ function executeBucketAction(action, http, args) {
8235
+ if (!Object.hasOwn(BUCKET_ACTION_HANDLERS, action))
8236
+ return null;
8237
+ return BUCKET_ACTION_HANDLERS[action](http, args);
8238
+ }
7733
8239
  function registerStorageTools(server, http) {
7734
8240
  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"),
8241
+ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_bucket, list_files, upload_base64, delete_file`, {
8242
+ action: withDescription(stringEnum([
8243
+ "status",
8244
+ "list_buckets",
8245
+ "get_bucket",
8246
+ "create_bucket",
8247
+ "update_bucket",
8248
+ "delete_bucket",
8249
+ "list_files",
8250
+ "upload_base64",
8251
+ "delete_file"
8252
+ ]), "Action"),
8253
+ 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"),
8254
+ 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"),
8255
+ public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
8256
+ file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
8257
+ allowed_mime_types: withDescription(allowedMimeTypesSchema, "[create_bucket/update_bucket] MIME types as a comma-separated or JSON array"),
8258
+ expected_revision: optional(Type.String({ pattern: BUCKET_REVISION_PATTERN.source }), "[update_bucket/delete_bucket] Exact revision from list_buckets/get_bucket"),
8259
+ require_empty: optional(Type.Boolean(), "[delete_bucket] Must be true; deletion never empties a bucket"),
8260
+ filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
7740
8261
  base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
7741
8262
  mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
7742
8263
  }, 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
- };
8264
+ const action = String(args.action);
8265
+ assertActionArguments(action, args);
8266
+ const bucketAction = executeBucketAction(action, http, args);
8267
+ const bucketActionResponse = bucketAction ? await bucketAction : null;
8268
+ if (bucketActionResponse)
8269
+ return bucketActionResponse;
7751
8270
  let text;
7752
8271
  switch (action) {
7753
8272
  case "status":
7754
8273
  text = JSON.stringify((await http.get("/v1/storage/status")).data, null, 2);
7755
8274
  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
8275
  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})`;
8276
+ const ref = requiredProjectRef(args);
8277
+ const bucket = requiredBucketId(args);
8278
+ const response = await http.get(`/v1/storage/${ref}/buckets/${bucket}/files`);
8279
+ if (!response.ok) {
8280
+ text = `❌ Failed (${response.status})`;
7779
8281
  break;
7780
8282
  }
7781
- const files = res.data;
8283
+ const files = response.data;
7782
8284
  if (!Array.isArray(files) || !files.length) {
7783
8285
  text = "No files.";
7784
8286
  break;
7785
8287
  }
7786
8288
  text = `\uD83D\uDCC1 Files (${files.length}):
7787
- ` + files.map((f) => ` - ${f.name} (${f.size ? (f.size / 1024).toFixed(1) + "KB" : "?"})`).join(`
8289
+ ` + files.map((file) => ` - ${file.name} (${file.size ? (file.size / 1024).toFixed(1) + "KB" : "?"})`).join(`
7788
8290
  `);
7789
8291
  break;
7790
8292
  }
7791
8293
  case "upload_base64": {
7792
- need("ref", ref);
7793
- need("bucket", bucket);
7794
- need("filename", filename);
7795
- need("base64_content", base64_content);
8294
+ const ref = requiredProjectRef(args);
8295
+ const bucket = requiredBucketId(args);
8296
+ const filename = requiredText(args, "filename");
8297
+ const base64Content = requiredText(args, "base64_content");
7796
8298
  try {
7797
- const buffer = Buffer.from(base64_content, "base64");
7798
- const blob = new Blob([buffer], { type: mime_type || "application/octet-stream" });
8299
+ const buffer = Buffer.from(base64Content, "base64");
8300
+ const blob = new Blob([buffer], { type: typeof args.mime_type === "string" ? args.mime_type : "application/octet-stream" });
7799
8301
  const formData = new FormData;
7800
8302
  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}`;
8303
+ const response = await http.postMultipart(`/v1/storage/${ref}/buckets/${bucket}/upload`, formData);
8304
+ text = response.ok ? `✅ File ${filename} uploaded to ${bucket}` : `❌ Upload failed (${response.status})`;
8305
+ } catch (error) {
8306
+ text = `❌ Error: ${error instanceof Error ? error.message : String(error)}`;
7805
8307
  }
7806
8308
  break;
7807
8309
  }
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`;
8310
+ case "delete_file": {
8311
+ const ref = requiredProjectRef(args);
8312
+ const bucket = requiredBucketId(args);
8313
+ const filename = requiredText(args, "filename");
8314
+ text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` : "❌ Failed";
7813
8315
  break;
8316
+ }
7814
8317
  default:
7815
- text = `❌ Unknown action: ${action}`;
8318
+ return releaseControlFailure(`storage.${action}`, "INVALID_RESPONSE", null);
7816
8319
  }
7817
8320
  return { content: [{ type: "text", text }] };
7818
8321
  });
7819
8322
  }
7820
8323
 
7821
8324
  // src/shared/tools/advanced-tools.ts
7822
- import { existsSync as existsSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync } from "node:fs";
8325
+ import { createHash as createHash2, timingSafeEqual } from "node:crypto";
8326
+ import {
8327
+ closeSync,
8328
+ constants as fsConstants,
8329
+ existsSync as existsSync3,
8330
+ fstatSync,
8331
+ mkdtempSync,
8332
+ openSync,
8333
+ readFileSync as readFileSync3,
8334
+ rmSync,
8335
+ statSync as statSync2,
8336
+ writeFileSync
8337
+ } from "node:fs";
7823
8338
  import { tmpdir } from "node:os";
7824
8339
  import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
7825
8340
  import { promisify } from "node:util";
7826
8341
  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
- });
8342
+ var execFileAsync = promisify(execFile);
8343
+ var SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
8344
+ function openFileIdentity(descriptor) {
8345
+ const state = fstatSync(descriptor, { bigint: true });
8346
+ if (!state.isFile())
8347
+ throw new Error("Prebundled path must be a regular file");
8348
+ return {
8349
+ dev: state.dev,
8350
+ ino: state.ino,
8351
+ size: state.size,
8352
+ mtimeNs: state.mtimeNs,
8353
+ ctimeNs: state.ctimeNs
8354
+ };
7837
8355
  }
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
- });
8356
+ function sameOpenFile(left, right) {
8357
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
7845
8358
  }
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);
8359
+ function verifiedUtf8Code(bytes) {
8360
+ let code;
8361
+ try {
8362
+ code = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
8363
+ } catch (error) {
8364
+ if (error instanceof TypeError)
8365
+ throw new Error("Prebundled file is not valid UTF-8");
8366
+ throw error;
8367
+ }
8368
+ if (!Buffer.from(code, "utf8").equals(bytes)) {
8369
+ throw new Error("Prebundled file does not round-trip as UTF-8");
8370
+ }
8371
+ return code;
7849
8372
  }
7850
- function releaseControlResponse(payload) {
7851
- return {
7852
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
7853
- };
8373
+ function assertExpectedSha256(bytes, expectedSha256) {
8374
+ if (!SHA256_HEX_PATTERN.test(expectedSha256)) {
8375
+ throw new Error("'--expected-sha256' must be a lowercase 64-character SHA-256");
8376
+ }
8377
+ const actualDigest = createHash2("sha256").update(bytes).digest();
8378
+ const expectedDigest = Buffer.from(expectedSha256, "hex");
8379
+ if (!timingSafeEqual(actualDigest, expectedDigest)) {
8380
+ throw new Error("Prebundled file SHA-256 does not match --expected-sha256");
8381
+ }
7854
8382
  }
7855
- function releaseControlErrorResponse(payload) {
7856
- return { ...releaseControlResponse(payload), isError: true };
8383
+ function readVerifiedPrebundledCode(pathArg, expectedSha256) {
8384
+ const descriptor = openSync(resolve2(pathArg), fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
8385
+ try {
8386
+ const identityBeforeRead = openFileIdentity(descriptor);
8387
+ const bytes = readFileSync3(descriptor);
8388
+ const identityAfterRead = openFileIdentity(descriptor);
8389
+ if (BigInt(bytes.byteLength) !== identityBeforeRead.size || !sameOpenFile(identityBeforeRead, identityAfterRead)) {
8390
+ throw new Error("Prebundled file changed while it was being read");
8391
+ }
8392
+ assertExpectedSha256(bytes, expectedSha256);
8393
+ return verifiedUtf8Code(bytes);
8394
+ } finally {
8395
+ closeSync(descriptor);
8396
+ }
7857
8397
  }
7858
-
7859
- // src/shared/tools/advanced-tools.ts
7860
- var execFileAsync = promisify(execFile);
7861
8398
  async function runBunBuild(args) {
7862
8399
  try {
7863
8400
  return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
@@ -7882,6 +8419,54 @@ async function bundleEdgeFunctionPath(pathArg) {
7882
8419
  rmSync(tmpDir, { recursive: true, force: true });
7883
8420
  }
7884
8421
  }
8422
+ function prebundledDeployCode(pathArg, expectedSha256, minify) {
8423
+ if (typeof expectedSha256 !== "string") {
8424
+ throw new Error("'--expected-sha256' required with '--prebundled-path'");
8425
+ }
8426
+ if (minify !== undefined) {
8427
+ throw new Error("'--minify' cannot be combined with '--prebundled-path'");
8428
+ }
8429
+ return {
8430
+ code: readVerifiedPrebundledCode(pathArg, expectedSha256),
8431
+ prebundled: true,
8432
+ expectedSha256
8433
+ };
8434
+ }
8435
+ async function preparedDeployCode(args) {
8436
+ const codeArg = args.code;
8437
+ const pathArg = args.path;
8438
+ const prebundledPath = args["prebundled-path"];
8439
+ const sources = [codeArg, pathArg, prebundledPath].filter((source) => source !== undefined);
8440
+ if (sources.length !== 1) {
8441
+ throw new Error("Exactly one of '--code', '--path', or '--prebundled-path' is required for 'deploy'");
8442
+ }
8443
+ if (typeof prebundledPath === "string") {
8444
+ return prebundledDeployCode(prebundledPath, args["expected-sha256"], args.minify);
8445
+ }
8446
+ if (args["expected-sha256"] !== undefined) {
8447
+ throw new Error("'--expected-sha256' requires '--prebundled-path'");
8448
+ }
8449
+ if (typeof codeArg === "string")
8450
+ return { code: codeArg, prebundled: false };
8451
+ if (typeof pathArg !== "string")
8452
+ throw new Error("Function deploy source is invalid");
8453
+ return bundledDeployCode(pathArg);
8454
+ }
8455
+ async function bundledDeployCode(pathArg) {
8456
+ try {
8457
+ return { code: await bundleEdgeFunctionPath(pathArg), prebundled: false };
8458
+ } catch (error) {
8459
+ const message = error instanceof Error ? error.message : String(error);
8460
+ throw new Error(`Failed to bundle/read path ${pathArg}: ${message}`);
8461
+ }
8462
+ }
8463
+ function rejectPrebundledFlagsOutsideDeploy(action, args) {
8464
+ for (const flag of ["prebundled-path", "expected-sha256"]) {
8465
+ if (action !== "deploy" && args[flag] !== undefined) {
8466
+ throw new Error(`'--${flag}' is not supported for '${action}'`);
8467
+ }
8468
+ }
8469
+ }
7885
8470
  function resolveEntrypoint(pathArg) {
7886
8471
  const resolved = resolve2(pathArg);
7887
8472
  const stat = statSync2(resolved);
@@ -7928,21 +8513,42 @@ function parseFunctionFiles(input) {
7928
8513
  }
7929
8514
  }
7930
8515
  var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
7931
- function parseFunctionVersion(input) {
8516
+ function positiveFunctionVersion(input, label) {
8517
+ if (typeof input !== "string" && typeof input !== "number") {
8518
+ throw new Error(`${label} must be a canonical positive safe integer`);
8519
+ }
7932
8520
  const version = String(input);
7933
- if (!CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
7934
- throw new Error("Function version must be a canonical safe integer");
8521
+ if (!POSITIVE_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
8522
+ throw new Error(`${label} must be a canonical positive safe integer`);
7935
8523
  }
7936
8524
  return version;
7937
8525
  }
7938
- var CANONICAL_FUNCTION_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)$/;
7939
- var SAFE_FUNCTION_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
8526
+ var POSITIVE_FUNCTION_VERSION_PATTERN = /^[1-9][0-9]*$/;
7940
8527
  var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
7941
- var FUNCTION_ACTIVATION_ARGUMENTS = new Set(["action", "ref", "slug", "version"]);
8528
+ var FUNCTION_ACTIVATION_ARGUMENTS = new Set([
8529
+ "action",
8530
+ "ref",
8531
+ "slug",
8532
+ "version",
8533
+ "expected-active-version"
8534
+ ]);
7942
8535
  var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
7943
- Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
7944
- Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
7945
- ]), Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), parseFunctionVersion));
8536
+ Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
8537
+ Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8538
+ ]), Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), (input) => positiveFunctionVersion(input, "Function version")));
8539
+ function parseExpectedActiveVersion(input) {
8540
+ if (input === "absent")
8541
+ return input;
8542
+ return positiveFunctionVersion(input, "Expected active version");
8543
+ }
8544
+ var expectedActiveVersionSchema = Type.Optional(decodedSchema(Type.Union([
8545
+ Type.Literal("absent"),
8546
+ Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
8547
+ Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8548
+ ]), Type.Union([
8549
+ Type.Literal("absent"),
8550
+ Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8551
+ ]), parseExpectedActiveVersion));
7946
8552
  var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
7947
8553
  var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
7948
8554
  var MAX_SECRET_COUNT = 1024;
@@ -8060,10 +8666,35 @@ function secretsForUpsert(inlineSecrets, environmentNames, environment) {
8060
8666
  throw new Error("'secrets' array required");
8061
8667
  return inlineSecrets;
8062
8668
  }
8669
+ var INVALID_FUNCTION_LIST_RESPONSE = "❌ Edge Function list response is invalid";
8670
+ var INVALID_FUNCTION_SOURCE_RESPONSE = "❌ Edge Function source response is invalid";
8671
+ function invalidFunctionReadResponse(message) {
8672
+ return { isError: true, content: [{ type: "text", text: message }] };
8673
+ }
8674
+ function safeFunctionList(payload) {
8675
+ if (!Array.isArray(payload))
8676
+ return null;
8677
+ const functionSlugs = new Set;
8678
+ for (const candidate of payload) {
8679
+ const edgeFunction = objectRecord(candidate);
8680
+ const slug = edgeFunction?.slug;
8681
+ const version = edgeFunction?.version;
8682
+ if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug) || typeof version !== "number" || !Number.isSafeInteger(version) || version < 1 || functionSlugs.has(slug))
8683
+ return null;
8684
+ functionSlugs.add(slug);
8685
+ }
8686
+ return payload;
8687
+ }
8688
+ function functionListResponse(response) {
8689
+ if (!response.ok)
8690
+ return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
8691
+ const functions = safeFunctionList(response.data);
8692
+ return functions ? { content: [{ type: "text", text: JSON.stringify(functions, null, 2) }] } : invalidFunctionReadResponse(INVALID_FUNCTION_LIST_RESPONSE);
8693
+ }
8063
8694
  function confirmedFunctionConfig(payload, expected) {
8064
- if (!payload || typeof payload !== "object" || Array.isArray(payload))
8695
+ const response = objectRecord(payload);
8696
+ if (!response)
8065
8697
  return false;
8066
- const response = payload;
8067
8698
  if (expected.verify_jwt !== undefined && response.verify_jwt !== expected.verify_jwt)
8068
8699
  return false;
8069
8700
  if (expected.background_routes !== undefined) {
@@ -8074,28 +8705,87 @@ function confirmedFunctionConfig(payload, expected) {
8074
8705
  }
8075
8706
  return true;
8076
8707
  }
8077
- function functionSourceCode(payload) {
8708
+ function functionSourceCode(payload, field = "code") {
8078
8709
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
8079
8710
  return null;
8080
- const code = payload.code;
8711
+ const code = payload[field];
8081
8712
  return typeof code === "string" ? code : null;
8082
8713
  }
8714
+ function requestedSourceVersion(candidate) {
8715
+ return candidate === undefined ? undefined : positiveFunctionVersion(candidate, "Function source version");
8716
+ }
8717
+ function functionSourceOutput(slug, sourceCode, output) {
8718
+ if (!output) {
8719
+ return { content: [{ type: "text", text: JSON.stringify({ code: sourceCode }, null, 2) }] };
8720
+ }
8721
+ const outputPath = resolve2(output);
8722
+ writeFileSync(outputPath, sourceCode, { flag: "wx" });
8723
+ return {
8724
+ content: [{
8725
+ type: "text",
8726
+ text: `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`
8727
+ }]
8728
+ };
8729
+ }
8083
8730
  function objectRecord(candidate) {
8084
8731
  return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
8085
8732
  }
8086
- function activationResponse(slug, version, response) {
8087
- const operation = "edge_functions.activate";
8733
+ function edgeFunctionResourcePath(ref, slug) {
8734
+ const root = `/v1/projects/${projectRefPathSegment(ref, "Edge Functions")}/functions`;
8735
+ if (slug === undefined)
8736
+ return root;
8737
+ if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug)) {
8738
+ throw new Error("'slug' is invalid for Edge Functions");
8739
+ }
8740
+ return `${root}/${encodeURIComponent(slug)}`;
8741
+ }
8742
+ async function readFunctionSource(http, request) {
8743
+ const sourceVersion = requestedSourceVersion(request.version);
8744
+ const resourcePath = edgeFunctionResourcePath(request.projectRef, request.slug);
8745
+ const sourcePath = sourceVersion === undefined ? `${resourcePath}/source` : `${resourcePath}/versions/${encodeURIComponent(sourceVersion)}`;
8746
+ const response = await http.get(sourcePath);
8088
8747
  if (!response.ok)
8089
- return releaseControlMutationFailure(operation, response);
8090
- const receipt = objectRecord(response.data);
8091
- const config = objectRecord(receipt?.config);
8092
- if (receipt?.success !== true || receipt.version !== version || config?.version !== version || typeof config.verify_jwt !== "boolean") {
8093
- return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
8748
+ return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
8749
+ const sourceCode = functionSourceCode(response.data, sourceVersion === undefined ? "code" : "source_code");
8750
+ return sourceCode === null ? invalidFunctionReadResponse(INVALID_FUNCTION_SOURCE_RESPONSE) : functionSourceOutput(request.slug, sourceCode, request.output);
8751
+ }
8752
+ function mutationIdentityMatches(receipt, expectation) {
8753
+ return receipt.success === true && receipt.project_ref === expectation.projectRef && receipt.slug === expectation.slug && receipt.previous_active_version === expectation.expectedActiveVersion;
8754
+ }
8755
+ function validReceiptVersion(activeVersion) {
8756
+ return typeof activeVersion === "string" && POSITIVE_FUNCTION_VERSION_PATTERN.test(activeVersion) && Number.isSafeInteger(Number(activeVersion));
8757
+ }
8758
+ function confirmedMutationVersion(receipt, config, expectation) {
8759
+ const activeVersion = receipt.active_version;
8760
+ if (!validReceiptVersion(activeVersion) || receipt.version !== activeVersion || config.version !== activeVersion || expectation.targetVersion !== undefined && activeVersion !== expectation.targetVersion) {
8761
+ return null;
8094
8762
  }
8095
- return releaseControlSuccess(operation, {
8096
- slug,
8097
- version,
8098
- verify_jwt: config.verify_jwt
8763
+ return activeVersion;
8764
+ }
8765
+ function confirmedFunctionMutation(expectation, payload) {
8766
+ const receipt = objectRecord(payload);
8767
+ const config = objectRecord(receipt?.config);
8768
+ if (!receipt || !config || !mutationIdentityMatches(receipt, expectation))
8769
+ return null;
8770
+ const activeVersion = confirmedMutationVersion(receipt, config, expectation);
8771
+ if (activeVersion === null || typeof config.verify_jwt !== "boolean" || !confirmedFunctionConfig(config, expectation.config ?? {}))
8772
+ return null;
8773
+ return { activeVersion, verifyJwt: config.verify_jwt };
8774
+ }
8775
+ function functionMutationResponse(expectation, response) {
8776
+ if (!response.ok)
8777
+ return releaseControlMutationFailure(expectation.operation, response);
8778
+ const confirmed = confirmedFunctionMutation(expectation, response.data);
8779
+ if (!confirmed) {
8780
+ return releaseControlFailure(expectation.operation, "OUTCOME_UNKNOWN", response.status);
8781
+ }
8782
+ return releaseControlSuccess(expectation.operation, {
8783
+ project_ref: expectation.projectRef,
8784
+ slug: expectation.slug,
8785
+ previous_active_version: expectation.expectedActiveVersion,
8786
+ active_version: confirmed.activeVersion,
8787
+ version: confirmed.activeVersion,
8788
+ verify_jwt: confirmed.verifyJwt
8099
8789
  });
8100
8790
  }
8101
8791
  function readOnlyActivationResult() {
@@ -8107,15 +8797,22 @@ function readOnlyActivationResult() {
8107
8797
  function functionActivationTarget(args) {
8108
8798
  const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
8109
8799
  const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
8110
- const version = args.version;
8111
- if (!SAFE_FUNCTION_REF_PATTERN.test(projectRef))
8112
- throw new Error("'ref' is invalid for 'activate'");
8800
+ const version = positiveFunctionVersion(args.version, "Function activation version");
8801
+ projectRefPathSegment(projectRef, "Edge Function activation");
8113
8802
  if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
8114
8803
  throw new Error("'slug' is invalid for 'activate'");
8115
- if (typeof version !== "string" || !CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
8116
- throw new Error("'version' is invalid for 'activate'");
8804
+ const expectedActiveVersion = requiredExpectedActiveVersion(args, "activate");
8805
+ return { projectRef, functionSlug, version, expectedActiveVersion };
8806
+ }
8807
+ function requiredExpectedActiveVersion(args, action) {
8808
+ const expected = args["expected-active-version"];
8809
+ if (expected === undefined) {
8810
+ throw new Error(`'--expected-active-version' required for '${action}'`);
8117
8811
  }
8118
- return { projectRef, functionSlug, version };
8812
+ const parsed = parseExpectedActiveVersion(expected);
8813
+ if (typeof parsed !== "string")
8814
+ throw new Error("Expected active version is invalid");
8815
+ return parsed;
8119
8816
  }
8120
8817
  async function activateFunctionVersion(http, args, readOnly = false) {
8121
8818
  if (readOnly)
@@ -8123,29 +8820,40 @@ async function activateFunctionVersion(http, args, readOnly = false) {
8123
8820
  const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
8124
8821
  if (unsupported.length > 0)
8125
8822
  throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
8126
- const { projectRef, functionSlug, version } = functionActivationTarget(args);
8127
- const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/functions/${encodeURIComponent(functionSlug)}` + `/versions/${encodeURIComponent(version)}/activate`;
8128
- return activationResponse(functionSlug, version, await http.post(endpoint));
8823
+ const { projectRef, functionSlug, version, expectedActiveVersion } = functionActivationTarget(args);
8824
+ const endpoint = edgeFunctionResourcePath(projectRef, functionSlug) + `/versions/${encodeURIComponent(version)}/activate`;
8825
+ return functionMutationResponse({
8826
+ operation: "edge_functions.activate",
8827
+ projectRef,
8828
+ slug: functionSlug,
8829
+ expectedActiveVersion,
8830
+ targetVersion: version
8831
+ }, await http.post(endpoint, { expected_active_version: expectedActiveVersion }));
8129
8832
  }
8130
8833
  function registerAdvancedTools(server, http, environment = process.env, options = {}) {
8131
- server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Server auto-bundles dependencies.
8834
+ server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Source deploys are bundled; verified prebuilt artifacts stay byte-exact.
8132
8835
  Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
8133
8836
  action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
8134
8837
  ref: withDescription(Type.String(), "Project ref"),
8135
8838
  slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
8136
- version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
8839
+ version: withDescription(functionVersionSchema, "[source/activate] Existing immutable Function version; source requires a positive version"),
8137
8840
  code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
8138
8841
  path: optional(Type.String(), "[deploy/check] Local file path to read code from (alternative to code)"),
8842
+ "prebundled-path": optional(Type.String(), "[deploy] Prebuilt runtime bundle to upload without rebuilding; requires expected-sha256"),
8843
+ "expected-sha256": optional(Type.String({ pattern: SHA256_HEX_PATTERN.source, minLength: 64, maxLength: 64 }), "[deploy] Required lowercase SHA-256 of the exact prebundled-path bytes"),
8139
8844
  output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
8140
8845
  files: optional(functionFilesSchema, "[deploy_bundle] File map as a JSON object: { 'index.ts': '...', '_shared/x.ts': '...' }"),
8141
8846
  entrypoint: optional(Type.String(), "[deploy_bundle] Entrypoint file (default: index.ts)"),
8142
8847
  minify: optional(Type.Boolean(), "[deploy/deploy_bundle] Minify bundle"),
8143
8848
  verify_jwt: optional(Type.Boolean(), "[deploy/deploy_bundle/config] Set JWT verification for this function"),
8144
- background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI")
8849
+ background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI"),
8850
+ "expected-active-version": withDescription(expectedActiveVersionSchema, "[deploy/deploy_bundle/activate] Required current active version, or 'absent' when none exists")
8145
8851
  }, async (args) => {
8146
8852
  if (args.action === "activate")
8147
8853
  return activateFunctionVersion(http, args, options.readOnly);
8148
8854
  const { action, ref, slug, path: pathArg, output, files, entrypoint, minify, verify_jwt, background_routes } = args;
8855
+ rejectPrebundledFlagsOutsideDeploy(action, args);
8856
+ const expectedActiveVersion = action === "deploy" || action === "deploy_bundle" ? requiredExpectedActiveVersion(args, action) : undefined;
8149
8857
  let code = args.code;
8150
8858
  const need = (f, v) => {
8151
8859
  if (!v)
@@ -8162,16 +8870,10 @@ Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`,
8162
8870
  if (!hasFunctionConfig()) {
8163
8871
  throw new Error("'verify_jwt' or 'background_routes' required for 'config'");
8164
8872
  }
8165
- const cr = await http.patch(`/v1/projects/${ref}/functions/${slug}/config`, functionConfig());
8873
+ const cr = await http.patch(`${edgeFunctionResourcePath(ref, slug)}/config`, functionConfig());
8166
8874
  return cr.ok ? `✅ Function ${slug} config updated
8167
8875
  ${JSON.stringify(cr.data, null, 2)}` : `❌ Config update failed (${cr.status}): ${JSON.stringify(cr.data)}`;
8168
8876
  };
8169
- const deploymentPolicyReceiptText = (successText, responsePayload) => {
8170
- if (!hasFunctionConfig() || confirmedFunctionConfig(responsePayload, functionConfig())) {
8171
- return successText;
8172
- }
8173
- return "❌ Unsafe deployment receipt: POST succeeded but did not confirm the requested function policy. No follow-up PATCH was attempted because code and policy must be activated atomically.";
8174
- };
8175
8877
  const checkSyntax = async (sourceCode) => {
8176
8878
  const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-check-"));
8177
8879
  const tmpFile = join2(tmpDir, "index.ts");
@@ -8186,7 +8888,7 @@ ${e.stderr || e.message}` };
8186
8888
  rmSync(tmpDir, { recursive: true, force: true });
8187
8889
  }
8188
8890
  };
8189
- if (pathArg && !code) {
8891
+ if (action === "check" && pathArg && !code) {
8190
8892
  try {
8191
8893
  code = await bundleEdgeFunctionPath(pathArg);
8192
8894
  } catch (error) {
@@ -8196,8 +8898,7 @@ ${e.stderr || e.message}` };
8196
8898
  }
8197
8899
  switch (action) {
8198
8900
  case "list":
8199
- text = JSON.stringify((await http.get(`/v1/projects/${ref}/functions`)).data, null, 2);
8200
- break;
8901
+ return functionListResponse(await http.get(edgeFunctionResourcePath(ref)));
8201
8902
  case "check":
8202
8903
  need("code (or path)", code);
8203
8904
  const checkRes = await checkSyntax(code);
@@ -8210,65 +8911,59 @@ ${checkRes.err}`;
8210
8911
  break;
8211
8912
  case "deploy":
8212
8913
  need("slug", slug);
8213
- need("code", code);
8214
- const deployCheck = await checkSyntax(code);
8215
- if (!deployCheck.ok) {
8216
- text = `❌ Deployment aborted. Syntax check failed:
8914
+ const deployCode = await preparedDeployCode(args);
8915
+ if (!deployCode.prebundled) {
8916
+ const deployCheck = await checkSyntax(deployCode.code);
8917
+ if (!deployCheck.ok) {
8918
+ text = `❌ Deployment aborted. Syntax check failed:
8217
8919
  ${deployCheck.err}`;
8218
- break;
8920
+ break;
8921
+ }
8219
8922
  }
8220
- const dr = await http.post(`/v1/projects/${ref}/functions/${slug}`, {
8221
- code,
8222
- minify,
8923
+ const deploymentResponse = await http.post(edgeFunctionResourcePath(ref, slug), {
8924
+ code: deployCode.code,
8925
+ ...deployCode.prebundled ? { prebundled: true, expected_sha256: deployCode.expectedSha256 } : { minify },
8926
+ expected_active_version: expectedActiveVersion,
8223
8927
  ...functionConfig()
8224
8928
  });
8225
- if (!dr.ok) {
8226
- text = `❌ Failed (${dr.status}): ${JSON.stringify(dr.data)}`;
8227
- break;
8228
- }
8229
- text = deploymentPolicyReceiptText(`✅ Function ${slug} deployed`, dr.data);
8230
- break;
8929
+ return functionMutationResponse({
8930
+ operation: "edge_functions.deploy",
8931
+ projectRef: ref,
8932
+ slug,
8933
+ expectedActiveVersion,
8934
+ config: functionConfig()
8935
+ }, deploymentResponse);
8231
8936
  case "deploy_bundle":
8232
8937
  need("slug", slug);
8233
8938
  need("files", files);
8234
- const br = await http.post(`/v1/projects/${ref}/functions/${slug}/bundle`, {
8939
+ const bundleResponse = await http.post(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
8235
8940
  files,
8236
8941
  entrypoint,
8237
8942
  minify,
8943
+ expected_active_version: expectedActiveVersion,
8238
8944
  ...functionConfig()
8239
8945
  });
8240
- if (!br.ok) {
8241
- text = `❌ Failed (${br.status}): ${JSON.stringify(br.data)}`;
8242
- break;
8243
- }
8244
- text = deploymentPolicyReceiptText(`✅ Function ${slug} bundle deployed (${Object.keys(files).length} files)`, br.data);
8245
- break;
8946
+ return functionMutationResponse({
8947
+ operation: "edge_functions.deploy_bundle",
8948
+ projectRef: ref,
8949
+ slug,
8950
+ expectedActiveVersion,
8951
+ config: functionConfig()
8952
+ }, bundleResponse);
8246
8953
  case "config":
8247
8954
  text = await updateFunctionConfig();
8248
8955
  break;
8249
8956
  case "source":
8250
8957
  need("slug", slug);
8251
- const sr = await http.get(`/v1/projects/${ref}/functions/${slug}/source`);
8252
- if (!sr.ok) {
8253
- text = `❌ Not found (${sr.status})`;
8254
- break;
8255
- }
8256
- if (!output) {
8257
- text = JSON.stringify(sr.data, null, 2);
8258
- break;
8259
- }
8260
- const sourceCode = functionSourceCode(sr.data);
8261
- if (sourceCode === null) {
8262
- text = "❌ Source response did not contain a string code field";
8263
- break;
8264
- }
8265
- const outputPath = resolve2(output);
8266
- writeFileSync(outputPath, sourceCode, { flag: "wx" });
8267
- text = `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`;
8268
- break;
8958
+ return readFunctionSource(http, {
8959
+ projectRef: ref,
8960
+ slug,
8961
+ version: args.version,
8962
+ output
8963
+ });
8269
8964
  case "delete":
8270
8965
  need("slug", slug);
8271
- text = (await http.delete(`/v1/projects/${ref}/functions/${slug}`)).ok ? `✅ Function ${slug} deleted` : `❌ Failed`;
8966
+ text = (await http.delete(edgeFunctionResourcePath(ref, slug))).ok ? `✅ Function ${slug} deleted` : `❌ Failed`;
8272
8967
  break;
8273
8968
  default:
8274
8969
  text = `❌ Unknown action`;
@@ -10180,7 +10875,6 @@ var FORBIDDEN_HEADER_NAMES = new Set([
10180
10875
  "via",
10181
10876
  "x-project-ref"
10182
10877
  ]);
10183
- var PROJECT_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
10184
10878
  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
10879
  var SAFE_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
10186
10880
  var CRON_PART_PATTERN = /^(\*|([0-9]+)(?:-([0-9]+))?)(?:\/([0-9]+))?$/;
@@ -10190,12 +10884,14 @@ var MAX_BODY_FILE_BYTES = 1048576;
10190
10884
  var MAX_HEADER_COUNT = 64;
10191
10885
  var MAX_HEADER_VALUE_LENGTH = 8192;
10192
10886
  var MAX_SCHEDULE_NAME_LENGTH = 120;
10887
+ var CANONICAL_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
10193
10888
  var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
10194
- var ACTION_ARGUMENTS = {
10889
+ var ACTION_ARGUMENTS2 = {
10195
10890
  list: new Set(["action", "ref"]),
10891
+ get: new Set(["action", "ref", "schedule_id"]),
10196
10892
  create: new Set(["action", "ref", "name", "slug", "cron", "method", "body_file", "header_env"]),
10197
- update: new Set(["action", "ref", "schedule_id", "name", "cron", "method", "enabled", "body_file", "header_env"]),
10198
- delete: new Set(["action", "ref", "schedule_id"])
10893
+ update: new Set(["action", "ref", "schedule_id", "expected_updated_at", "name", "cron", "method", "enabled", "body_file", "header_env"]),
10894
+ delete: new Set(["action", "ref", "schedule_id", "expected_updated_at"])
10199
10895
  };
10200
10896
  function parseHeaderEnvironment(input) {
10201
10897
  if (typeof input !== "string")
@@ -10315,7 +11011,13 @@ function validScheduleDefinition(schedule) {
10315
11011
  return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
10316
11012
  }
10317
11013
  function validScheduleMetadata(schedule) {
10318
- return typeof schedule.created_at === "string" && typeof schedule.updated_at === "string";
11014
+ return typeof schedule.created_at === "string" && isCanonicalTimestamp(schedule.updated_at);
11015
+ }
11016
+ function isCanonicalTimestamp(candidate) {
11017
+ if (typeof candidate !== "string" || !CANONICAL_TIMESTAMP_PATTERN.test(candidate))
11018
+ return false;
11019
+ const milliseconds = Date.parse(candidate);
11020
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
10319
11021
  }
10320
11022
  function safeSchedulePayload(schedule) {
10321
11023
  const headerNames = safeHeaderNames(schedule.header_names);
@@ -10349,26 +11051,25 @@ function safeSchedule(candidate) {
10349
11051
  updated_at: schedule.updated_at
10350
11052
  };
10351
11053
  }
10352
- function requiredText(args, name, action) {
11054
+ function requiredText2(args, name, action) {
10353
11055
  const candidate = args[name];
10354
11056
  if (typeof candidate !== "string" || !candidate.trim()) {
10355
11057
  throw new Error(`'${name}' is required for '${action}'`);
10356
11058
  }
10357
11059
  return candidate.trim();
10358
11060
  }
10359
- function assertActionArguments(action, args) {
10360
- const unsupported = Object.keys(args).filter((name) => !ACTION_ARGUMENTS[action].has(name));
11061
+ function assertActionArguments2(action, args) {
11062
+ const unsupported = Object.keys(args).filter((name) => !ACTION_ARGUMENTS2[action].has(name));
10361
11063
  if (unsupported.length > 0) {
10362
11064
  throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
10363
11065
  }
10364
11066
  }
10365
11067
  function schedulePath(ref, scheduleId) {
10366
- if (!PROJECT_REF_PATTERN.test(ref))
10367
- throw new Error("'ref' is invalid for Scheduled Functions");
11068
+ const projectRefSegment = projectRefPathSegment(ref, "Scheduled Functions");
10368
11069
  if (scheduleId !== undefined && !SCHEDULE_ID_PATTERN.test(scheduleId)) {
10369
11070
  throw new Error("'schedule_id' is invalid");
10370
11071
  }
10371
- const root = `/v1/projects/${encodeURIComponent(ref)}/scheduled-functions`;
11072
+ const root = `/v1/projects/${projectRefSegment}/scheduled-functions`;
10372
11073
  return scheduleId ? `${root}/${encodeURIComponent(scheduleId)}` : root;
10373
11074
  }
10374
11075
  function scheduleFailure(operation, response) {
@@ -10389,6 +11090,17 @@ function listResponse(ref, response) {
10389
11090
  return releaseControlFailure(operation, "INVALID_RESPONSE", null);
10390
11091
  return releaseControlSuccess(operation, { project_ref: ref, schedules });
10391
11092
  }
11093
+ function getResponse(ref, scheduleId, response) {
11094
+ const operation = "scheduled_functions.get";
11095
+ if (!response.ok)
11096
+ return scheduleFailure(operation, response);
11097
+ const payload = objectRecord2(response.data);
11098
+ const schedule = safeSchedule(payload?.schedule);
11099
+ if (payload?.project_ref !== ref || !schedule || schedule.id !== scheduleId) {
11100
+ return releaseControlFailure(operation, "INVALID_RESPONSE", null);
11101
+ }
11102
+ return releaseControlSuccess(operation, { project_ref: ref, schedule });
11103
+ }
10392
11104
  function mutationResponse(expectation, response) {
10393
11105
  const { action, ref, requestId, expectedFields } = expectation;
10394
11106
  const scheduleId = action === "update" ? expectation.scheduleId : undefined;
@@ -10398,22 +11110,29 @@ function mutationResponse(expectation, response) {
10398
11110
  const payload = objectRecord2(response.data);
10399
11111
  const schedule = safeSchedule(payload?.schedule);
10400
11112
  const confirmsRequest = schedule && Object.entries(expectedFields).every(([field, expected]) => isDeepStrictEqual(schedule[field], expected));
10401
- if (payload?.project_ref !== ref || payload.request_id !== requestId || payload?.[action === "create" ? "created" : "updated"] !== true || !schedule || !confirmsRequest || scheduleId !== undefined && schedule.id !== scheduleId) {
11113
+ const confirmsRevision = action === "create" || payload?.previous_updated_at === expectation.expectedUpdatedAt && schedule !== null && schedule.updated_at > expectation.expectedUpdatedAt;
11114
+ if (payload?.project_ref !== ref || payload.request_id !== requestId || payload?.[action === "create" ? "created" : "updated"] !== true || !schedule || !confirmsRequest || !confirmsRevision || scheduleId !== undefined && schedule.id !== scheduleId) {
10402
11115
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
10403
11116
  }
10404
- return releaseControlSuccess(operation, { project_ref: ref, request_id: requestId, schedule });
11117
+ return releaseControlSuccess(operation, {
11118
+ project_ref: ref,
11119
+ request_id: requestId,
11120
+ ...action === "update" ? { previous_updated_at: expectation.expectedUpdatedAt } : {},
11121
+ schedule
11122
+ });
10405
11123
  }
10406
- function deleteResponse(ref, scheduleId, response) {
11124
+ function deleteResponse(ref, scheduleId, expectedUpdatedAt, response) {
10407
11125
  const operation = "scheduled_functions.delete";
10408
11126
  if (!response.ok)
10409
11127
  return releaseControlMutationFailure(operation, response);
10410
11128
  const payload = objectRecord2(response.data);
10411
- if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId) {
11129
+ if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId || payload.deleted_updated_at !== expectedUpdatedAt) {
10412
11130
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
10413
11131
  }
10414
11132
  return releaseControlSuccess(operation, {
10415
11133
  project_ref: ref,
10416
11134
  schedule_id: scheduleId,
11135
+ deleted_updated_at: expectedUpdatedAt,
10417
11136
  deleted: true
10418
11137
  });
10419
11138
  }
@@ -10429,7 +11148,7 @@ function createRequest(args, environment) {
10429
11148
  name: requiredName(args, "create"),
10430
11149
  slug: requiredSlug(args, "create"),
10431
11150
  cron: requiredCron(args, "create"),
10432
- method: requiredText(args, "method", "create"),
11151
+ method: requiredText2(args, "method", "create"),
10433
11152
  body: scheduleBody(args.body_file) ?? {},
10434
11153
  headers: scheduleHeaders(args.header_env, environment) ?? {}
10435
11154
  };
@@ -10445,24 +11164,32 @@ function safeMutationFields(request) {
10445
11164
  return safeFields;
10446
11165
  }
10447
11166
  function requiredName(args, action) {
10448
- const name = requiredText(args, "name", action);
11167
+ const name = requiredText2(args, "name", action);
10449
11168
  if (name.length > MAX_SCHEDULE_NAME_LENGTH)
10450
11169
  throw new Error(`'name' is too long for '${action}'`);
10451
11170
  return name;
10452
11171
  }
10453
11172
  function requiredSlug(args, action) {
10454
- const slug = requiredText(args, "slug", action);
11173
+ const slug = requiredText2(args, "slug", action);
10455
11174
  if (!SAFE_SLUG_PATTERN.test(slug))
10456
11175
  throw new Error(`'slug' is invalid for '${action}'`);
10457
11176
  return slug;
10458
11177
  }
10459
11178
  function requiredCron(args, action) {
10460
- const cron = requiredText(args, "cron", action);
11179
+ const cron = requiredText2(args, "cron", action);
10461
11180
  if (!validScheduledFunctionCron(cron))
10462
11181
  throw new Error(`'cron' is invalid for '${action}'`);
10463
11182
  return cron;
10464
11183
  }
11184
+ function requiredExpectedUpdatedAt(args, action) {
11185
+ const candidate = args.expected_updated_at;
11186
+ if (!isCanonicalTimestamp(candidate)) {
11187
+ throw new Error(`'expected_updated_at' must be a canonical UTC timestamp for '${action}'`);
11188
+ }
11189
+ return candidate;
11190
+ }
10465
11191
  function updateRequest(args, environment) {
11192
+ const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "update");
10466
11193
  const body = scheduleBody(args.body_file);
10467
11194
  const headers = scheduleHeaders(args.header_env, environment);
10468
11195
  const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
@@ -10478,14 +11205,21 @@ function updateRequest(args, environment) {
10478
11205
  if (Object.keys(mutationFields).length === 0) {
10479
11206
  throw new Error("Scheduled Function update requires at least one field");
10480
11207
  }
10481
- return { request_id: randomUUID(), ...mutationFields };
11208
+ return {
11209
+ request_id: randomUUID(),
11210
+ expected_updated_at: expectedUpdatedAt,
11211
+ ...mutationFields
11212
+ };
11213
+ }
11214
+ function deletePath(schedulePathname, expectedUpdatedAt) {
11215
+ return `${schedulePathname}?expected_updated_at=${encodeURIComponent(expectedUpdatedAt)}`;
10482
11216
  }
10483
11217
  async function executeScheduleAction(http, environment, args, readOnly = false) {
10484
11218
  const action = args.action;
10485
- if (readOnly && action !== "list")
11219
+ if (readOnly && action !== "list" && action !== "get")
10486
11220
  return readOnlyResult2();
10487
- assertActionArguments(action, args);
10488
- const ref = requiredText(args, "ref", action);
11221
+ assertActionArguments2(action, args);
11222
+ const ref = requiredText2(args, "ref", action);
10489
11223
  if (action === "list")
10490
11224
  return listResponse(ref, await http.get(schedulePath(ref)));
10491
11225
  if (action === "create") {
@@ -10493,28 +11227,35 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
10493
11227
  const requestId = request.request_id;
10494
11228
  return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
10495
11229
  }
10496
- const scheduleId = requiredText(args, "schedule_id", action);
11230
+ const scheduleId = requiredText2(args, "schedule_id", action);
11231
+ const targetPath = schedulePath(ref, scheduleId);
11232
+ if (action === "get")
11233
+ return getResponse(ref, scheduleId, await http.get(targetPath));
10497
11234
  if (action === "update") {
10498
11235
  const request = updateRequest(args, environment);
10499
11236
  const requestId = request.request_id;
11237
+ const expectedUpdatedAt2 = request.expected_updated_at;
10500
11238
  return mutationResponse({
10501
11239
  action,
10502
11240
  ref,
10503
11241
  scheduleId,
10504
11242
  requestId,
11243
+ expectedUpdatedAt: expectedUpdatedAt2,
10505
11244
  expectedFields: safeMutationFields(request)
10506
- }, await http.patch(schedulePath(ref, scheduleId), request));
11245
+ }, await http.patch(targetPath, request));
10507
11246
  }
10508
- return deleteResponse(ref, scheduleId, await http.delete(schedulePath(ref, scheduleId)));
11247
+ const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "delete");
11248
+ return deleteResponse(ref, scheduleId, expectedUpdatedAt, await http.delete(deletePath(targetPath, expectedUpdatedAt)));
10509
11249
  }
10510
11250
  function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
10511
11251
  server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
10512
11252
  }
10513
- var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, create, update, delete";
11253
+ var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, get, create, update, delete";
10514
11254
  var SCHEDULE_TOOL_SCHEMA = {
10515
- action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
11255
+ action: withDescription(stringEnum(["list", "get", "create", "update", "delete"]), "Action"),
10516
11256
  ref: withDescription(Type.String(), "Project ref"),
10517
- schedule_id: optional(Type.String(), "[update/delete] Schedule ID"),
11257
+ schedule_id: optional(Type.String(), "[get/update/delete] Schedule ID"),
11258
+ expected_updated_at: optional(Type.String(), "[update/delete] Canonical updated_at from list"),
10518
11259
  name: optional(Type.String(), "[create/update] Display name"),
10519
11260
  slug: optional(Type.String(), "[create] Edge Function slug"),
10520
11261
  cron: optional(Type.String(), "[create/update] Five-field cron expression"),
@@ -10526,7 +11267,7 @@ var SCHEDULE_TOOL_SCHEMA = {
10526
11267
  // package.json
10527
11268
  var package_default = {
10528
11269
  name: "@supacloud/cli",
10529
- version: "0.15.0",
11270
+ version: "0.17.0",
10530
11271
  description: "Project-scoped CLI for SupaCloud users",
10531
11272
  type: "module",
10532
11273
  main: "./dist/index.js",
@@ -10731,6 +11472,7 @@ EXAMPLES
10731
11472
  ${preferredCommand} frontend list --ref abc123
10732
11473
  ${preferredCommand} database query --sql "select now()"
10733
11474
  ${preferredCommand} database query --ref abc123 --file ./queries/vector-search.sql
11475
+ ${preferredCommand} database migration_inventory --ref abc123
10734
11476
  ${preferredCommand} database push_migrations --ref abc123 --dir supabase/migrations --dry_run
10735
11477
  ${preferredCommand} supabase migration_new --name add_accounts
10736
11478
  ${preferredCommand} supabase db_diff --schema public --name add_accounts
@@ -10741,8 +11483,9 @@ EXAMPLES
10741
11483
  ${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
10742
11484
  ${preferredCommand} ai show_skill
10743
11485
  ${preferredCommand} ai install_skill --dry_run
10744
- ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
10745
- ${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3
11486
+ ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent
11487
+ ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
11488
+ ${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4
10746
11489
  ${preferredCommand} scheduled_functions list --ref abc123
10747
11490
  ${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
10748
11491
  ${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
@@ -10824,6 +11567,11 @@ function createCliTools(context, confirmProduction) {
10824
11567
  })
10825
11568
  };
10826
11569
  }
11570
+ const storageContextCallback = tools.storage.callback;
11571
+ const storageHelpTool = captureTools((server) => registerStorageTools(server, {})).storage;
11572
+ if (storageHelpTool) {
11573
+ tools.storage = { schema: storageHelpTool.schema, callback: storageContextCallback };
11574
+ }
10827
11575
  const branchContextCallback = tools.branch.callback;
10828
11576
  const branchHelpTool = captureTools((server) => registerBranchTools(server, {}, {
10829
11577
  readOnly: true