@supacloud/cli 0.16.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/README.md CHANGED
@@ -155,10 +155,11 @@ supacloud-cli frontend list --ref abc123
155
155
  supacloud-cli branch create --name feature-orders --data_mode schema_only
156
156
  supacloud-cli branch promotion_plan --branch_ref preview123
157
157
  supacloud-cli branch promote --branch_ref preview123 --plan_checksum <sha256>
158
- supacloud-cli edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
159
- supacloud-cli edge_functions deploy_bundle --ref abc123 --slug hello --files '{"index.ts":"export default { fetch: () => new Response(\"ok\") }"}'
160
- supacloud-cli edge_functions source --ref abc123 --slug hello --output ./hello.ts
161
- supacloud-cli edge_functions activate --ref abc123 --slug hello --version 3
158
+ supacloud-cli edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent
159
+ supacloud-cli edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
160
+ supacloud-cli edge_functions deploy_bundle --ref abc123 --slug hello --files '{"index.ts":"export default { fetch: () => new Response(\"ok\") }"}' --expected-active-version 7
161
+ supacloud-cli edge_functions source --ref abc123 --slug hello --version 7 --output ./hello-v7.ts
162
+ supacloud-cli edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 8
162
163
  supacloud-cli scheduled_functions list --ref abc123
163
164
  supacloud-cli secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
164
165
  supacloud-cli storage list_buckets --ref abc123
@@ -166,8 +167,10 @@ supacloud-cli storage get_bucket --ref abc123 --bucket reports
166
167
  supacloud-cli storage create_bucket --ref abc123 --bucket reports --public false \
167
168
  --file_size_limit 10485760 --allowed_mime_types "application/pdf,image/png"
168
169
  supacloud-cli storage update_bucket --ref abc123 --bucket reports \
170
+ --expected_revision <revision-from-get_bucket> \
169
171
  --allowed_mime_types '["application/pdf"]'
170
- supacloud-cli storage delete_bucket --ref abc123 --bucket reports
172
+ supacloud-cli storage delete_bucket --ref abc123 --bucket reports \
173
+ --expected_revision <revision-from-get_bucket> --require_empty true
171
174
  ```
172
175
 
173
176
  `database migration_inventory` reads the canonical migration ledger through the
@@ -178,14 +181,44 @@ drift, and statement-count mismatches instead of treating them as an empty
178
181
  ledger. `database list_migrations` remains available with its legacy SQL-backed,
179
182
  human-readable behavior.
180
183
 
184
+ Bucket list/get output includes the metadata `revision`. Update and delete reject
185
+ stale revisions with HTTP 409. Delete additionally requires `require_empty=true`;
186
+ it never empties a bucket. Mutation receipts bind `project_ref`, `bucket_id`,
187
+ `previous_revision`, and `new_revision` (`null` after delete).
188
+
189
+ `database push_migrations` rejects a remote row that reuses a local migration
190
+ name with another version, or a local version with another name, before either
191
+ dry-run reporting or apply can continue. This keeps the preview consistent with
192
+ the server conflict that would otherwise occur after deployment starts.
193
+
181
194
  `edge_functions deploy --path` bundles local TypeScript and dependencies with
182
195
  Bun and runs a local syntax check before upload. The Management API validates and
183
196
  normalizes the final server-side artifact against the multi-tenant Edge Runtime
184
197
  module policy consistently for CLI, Web Console, and direct API deployments.
198
+ For an artifact already built and validated by release automation, use
199
+ `deploy --prebundled-path <file> --expected-sha256 <64-lowercase-hex>`. The CLI
200
+ holds the opened regular file while reading it, rejects metadata drift, invalid
201
+ UTF-8, or a caller-hash mismatch before HTTP, and never passes the artifact in
202
+ the process argument list. The Management API validates the hash and runtime
203
+ policy again, rejects any normalization that would change the code, and stores
204
+ the submitted bytes unchanged as both immutable source and runtime artifact.
205
+ `--prebundled-path` is mutually exclusive with `--path`, `--code`, and
206
+ `--minify`.
185
207
  `deploy_bundle --files` accepts a JSON object in shell usage.
186
208
  Use `source --output <file>` for large Functions so terminal or automation output
187
209
  limits cannot truncate the original TS/JS source code. The destination must not
188
- already exist.
210
+ already exist. Add the positive version observed from `list` as
211
+ `source --version <N>` to read the immutable release instead of the moving active
212
+ pointer; this remains correct across an active-version A→B→A transition.
213
+
214
+ `deploy`, `deploy_bundle`, and `activate` require
215
+ `--expected-active-version <N|absent>`. Read the current positive integer
216
+ version from `edge_functions list`; use `absent` only when creating a slug that
217
+ does not yet exist. A stale value returns HTTP 409 without building, preheating,
218
+ or activating another version. List output remains a JSON array with string
219
+ `slug` and numeric `version` fields, while source output is exactly
220
+ `{ "code": "..." }`. Release automation must use `source --version <N>` for a
221
+ version-bound backup.
189
222
 
190
223
  `edge_functions activate` restores an existing immutable Function version and
191
224
  returns a machine-readable receipt containing the activated version and JWT
@@ -196,27 +229,50 @@ Mutation receipts use schema `supacloud.cli.release-control.v1`. An
196
229
  `OUTCOME_UNKNOWN` error means the server may have committed the mutation before
197
230
  the response was lost or failed validation; read back current state before any
198
231
  retry.
232
+ Version `0` is reserved for service-internal legacy recovery and cannot be used
233
+ as a public CLI/API activation target or expected active version.
234
+
235
+ ```json
236
+ {
237
+ "schema": "supacloud.cli.release-control.v1",
238
+ "ok": true,
239
+ "operation": "edge_functions.deploy_bundle",
240
+ "project_ref": "abc123",
241
+ "slug": "hello",
242
+ "previous_active_version": "7",
243
+ "active_version": "8",
244
+ "version": "8",
245
+ "verify_jwt": true
246
+ }
247
+ ```
199
248
 
200
249
  Scheduled Function lifecycle operations are also project-scoped:
201
250
 
202
251
  ```bash
203
252
  supacloud-cli scheduled_functions create --ref abc123 --name nightly \
204
253
  --slug cleanup --cron "0 2 * * *" --method POST
254
+ supacloud-cli scheduled_functions get --ref abc123 --schedule_id <id>
205
255
  supacloud-cli scheduled_functions update --ref abc123 --schedule_id <id> \
206
- --cron "0 3 * * *"
207
- supacloud-cli scheduled_functions delete --ref abc123 --schedule_id <id>
256
+ --expected_updated_at <updated_at-from-list> --cron "0 3 * * *"
257
+ supacloud-cli scheduled_functions delete --ref abc123 --schedule_id <id> \
258
+ --expected_updated_at <updated_at-from-list>
208
259
  ```
209
260
 
210
261
  Schedule IDs are canonical UUIDv4 values returned by create/list. Cron values
211
262
  use bounded numeric five-field syntax with wildcards, lists, ranges, and steps;
212
263
  out-of-range endpoints and steps are rejected before HTTP dispatch.
264
+ Update and delete require the exact canonical UTC `updated_at` returned by list.
265
+ A stale revision fails with HTTP 409 and performs no mutation; read the list
266
+ again before deciding whether to issue a new write.
213
267
 
214
268
  Use `--body_file ./payload.json` for a JSON-object request body. Header values
215
269
  must come from environment variables: pass a JSON name mapping such as
216
270
  `--header_env '{"x-schedule-token":"SCHEDULE_TOKEN"}'`. Platform-owned
217
271
  `authorization`, `apikey`, and `x-project-ref` headers cannot be overridden. Receipts never
218
272
  include header values or body content; list and mutation receipts report only
219
- whether the body is empty and the configured header names.
273
+ whether the body is empty and the configured header names. Update receipts bind
274
+ `previous_updated_at` to the requested revision and return a newer `updated_at`;
275
+ delete receipts return the matched revision as `deleted_updated_at`.
220
276
 
221
277
  For secret writes, `--from-env` accepts a comma-separated list of environment
222
278
  variable names. The CLI reads each non-empty value from its own process
package/dist/index.js CHANGED
@@ -6491,7 +6491,7 @@ var ACTION_POLICY = {
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"],
@@ -6964,6 +6964,28 @@ function migrationRows(data) {
6964
6964
  function migrationIdentityKey(version, name) {
6965
6965
  return `${version}\x00${name}`;
6966
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
+ }
6967
6989
  function nameBoundMigrationMarkerKeys(data) {
6968
6990
  const keys = new Set;
6969
6991
  for (const row of migrationRows(data)) {
@@ -7227,6 +7249,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7227
7249
  text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
7228
7250
  break;
7229
7251
  }
7252
+ assertNoMigrationIdentityConflicts(migrationsResult.data, migrationFiles);
7230
7253
  if (args.dry_run) {
7231
7254
  const appliedKeys = appliedMigrationKeys(migrationsResult.data);
7232
7255
  const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
@@ -7882,13 +7905,14 @@ var MAX_MIME_TYPE_LENGTH = 255;
7882
7905
  var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
7883
7906
  var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
7884
7907
  var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
7908
+ var BUCKET_REVISION_PATTERN = /^[0-9]{1,20}$/;
7885
7909
  var ACTION_ARGUMENTS = {
7886
7910
  status: new Set(["action"]),
7887
7911
  list_buckets: new Set(["action", "ref"]),
7888
7912
  get_bucket: new Set(["action", "ref", "bucket"]),
7889
7913
  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"]),
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"]),
7892
7916
  list_files: new Set(["action", "ref", "bucket"]),
7893
7917
  upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
7894
7918
  delete_file: new Set(["action", "ref", "bucket", "filename"])
@@ -7944,6 +7968,16 @@ function requiredBucketId(args) {
7944
7968
  throw new Error("'bucket' is invalid for Storage buckets");
7945
7969
  return bucket;
7946
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
+ }
7947
7981
  function assertActionArguments(action, args) {
7948
7982
  const allowedArguments = ACTION_ARGUMENTS[action];
7949
7983
  if (!allowedArguments)
@@ -7978,9 +8012,12 @@ function assertBucketSettings(args) {
7978
8012
  function isAllowedMimeTypes(candidate) {
7979
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));
7980
8014
  }
8015
+ function isBucketRevision(candidate) {
8016
+ return candidate === null || typeof candidate === "string" && BUCKET_REVISION_PATTERN.test(candidate);
8017
+ }
7981
8018
  function safeBucket(candidate, expectedBucket) {
7982
8019
  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) {
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) {
7984
8021
  return null;
7985
8022
  }
7986
8023
  return {
@@ -7988,7 +8025,8 @@ function safeBucket(candidate, expectedBucket) {
7988
8025
  name: bucket.name,
7989
8026
  public: bucket.public,
7990
8027
  file_size_limit: bucket.file_size_limit,
7991
- allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types]
8028
+ allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types],
8029
+ revision: bucket.revision
7992
8030
  };
7993
8031
  }
7994
8032
  function safeExactBucket(candidate, expectedBucket) {
@@ -8012,18 +8050,30 @@ function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
8012
8050
  return null;
8013
8051
  return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
8014
8052
  }
8015
- function safeDeletedBucket(candidate, expectedBucket) {
8053
+ function safeDeletedBucket(candidate, expectedBucket, expectedRevision) {
8016
8054
  const receipt = bucketRecord(candidate);
8017
- return receipt?.id === expectedBucket && receipt.deleted === true ? { bucket_id: expectedBucket, deleted: true } : null;
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;
8018
8062
  }
8019
8063
  function mutationReadbackResponse(expectation) {
8020
- const { operation, ref, response, expectedBucket, request } = expectation;
8064
+ const { operation, ref, response, expectedBucket, request, previousRevision, expectedNewRevision } = expectation;
8021
8065
  const readback = safeExactBucket(response.data, expectedBucket);
8022
- const validReadback = readback?.name === expectedBucket && bucketMatchesRequest(readback, request);
8066
+ const validReadback = readback?.name === expectedBucket && readback.revision !== null && (expectedNewRevision === undefined || readback.revision === expectedNewRevision) && bucketMatchesRequest(readback, request);
8023
8067
  if (!response.ok || !validReadback || !readback) {
8024
8068
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
8025
8069
  }
8026
- return releaseControlSuccess(operation, { project_ref: ref, bucket: readback });
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
+ });
8027
8077
  }
8028
8078
  function bucketResponse(expectation) {
8029
8079
  const { operation, ref, response, operationKind, safePayload } = expectation;
@@ -8049,10 +8099,10 @@ function createBucketRequest(args) {
8049
8099
  }
8050
8100
  function updateBucketRequest(args) {
8051
8101
  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)
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)
8054
8104
  throw new Error("Bucket update requires at least one field");
8055
- return request;
8105
+ return { expected_revision: requiredBucketRevision(args), ...settings };
8056
8106
  }
8057
8107
  function equalAllowedMimeTypes(candidate, expected) {
8058
8108
  if (!Array.isArray(expected))
@@ -8080,18 +8130,10 @@ async function createBucketMutationReceipt(http, ref, bucket, request) {
8080
8130
  safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
8081
8131
  });
8082
8132
  }
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
- });
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;
8095
8137
  }
8096
8138
  async function listBuckets(http, args) {
8097
8139
  const ref = requiredProjectRef(args);
@@ -8134,35 +8176,45 @@ async function createBucket(http, args) {
8134
8176
  ref,
8135
8177
  response: await http.get(bucketPath),
8136
8178
  expectedBucket: bucket,
8137
- request: expectedReadback
8179
+ request: expectedReadback,
8180
+ previousRevision: null
8138
8181
  });
8139
8182
  }
8140
8183
  async function updateBucket(http, args) {
8141
8184
  const ref = requiredProjectRef(args);
8142
8185
  const bucket = requiredBucketId(args);
8143
8186
  const request = updateBucketRequest(args);
8187
+ const expectedRevision = request.expected_revision;
8144
8188
  const bucketPath = storageBucketPath(ref, bucket);
8145
- const receipt = await updateBucketMutationReceipt({ http, ref, bucket, bucketPath, request });
8146
- if (receipt.isError)
8147
- return receipt;
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);
8148
8195
  return mutationReadbackResponse({
8149
8196
  operation: "storage.update_bucket",
8150
8197
  ref,
8151
8198
  response: await http.get(bucketPath),
8152
8199
  expectedBucket: bucket,
8153
- request
8200
+ request,
8201
+ previousRevision: expectedRevision,
8202
+ expectedNewRevision: updated.revision ?? undefined
8154
8203
  });
8155
8204
  }
8156
8205
  async function deleteBucket(http, args) {
8157
8206
  const ref = requiredProjectRef(args);
8158
8207
  const bucket = requiredBucketId(args);
8208
+ const expectedRevision = requiredBucketRevision(args);
8209
+ assertEmptyBucketDeletion(args);
8159
8210
  const bucketPath = storageBucketPath(ref, bucket);
8211
+ const deletePath = `${bucketPath}?expected_revision=${encodeURIComponent(expectedRevision)}&require_empty=true`;
8160
8212
  const receipt = bucketResponse({
8161
8213
  operation: "storage.delete_bucket",
8162
8214
  ref,
8163
- response: await http.delete(bucketPath),
8215
+ response: await http.delete(deletePath),
8164
8216
  operationKind: "mutation",
8165
- safePayload: (candidate) => safeDeletedBucket(candidate, bucket)
8217
+ safePayload: (candidate) => safeDeletedBucket(candidate, bucket, expectedRevision)
8166
8218
  });
8167
8219
  if (receipt.isError)
8168
8220
  return receipt;
@@ -8203,6 +8255,8 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
8203
8255
  public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
8204
8256
  file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
8205
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"),
8206
8260
  filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
8207
8261
  base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
8208
8262
  mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
@@ -8268,12 +8322,79 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
8268
8322
  }
8269
8323
 
8270
8324
  // src/shared/tools/advanced-tools.ts
8271
- 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";
8272
8338
  import { tmpdir } from "node:os";
8273
8339
  import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
8274
8340
  import { promisify } from "node:util";
8275
8341
  import { execFile } from "node:child_process";
8276
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
+ };
8355
+ }
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;
8358
+ }
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;
8372
+ }
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
+ }
8382
+ }
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
+ }
8397
+ }
8277
8398
  async function runBunBuild(args) {
8278
8399
  try {
8279
8400
  return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
@@ -8298,6 +8419,54 @@ async function bundleEdgeFunctionPath(pathArg) {
8298
8419
  rmSync(tmpDir, { recursive: true, force: true });
8299
8420
  }
8300
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
+ }
8301
8470
  function resolveEntrypoint(pathArg) {
8302
8471
  const resolved = resolve2(pathArg);
8303
8472
  const stat = statSync2(resolved);
@@ -8344,21 +8513,42 @@ function parseFunctionFiles(input) {
8344
8513
  }
8345
8514
  }
8346
8515
  var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
8347
- 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
+ }
8348
8520
  const version = String(input);
8349
- if (!CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
8350
- 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`);
8351
8523
  }
8352
8524
  return version;
8353
8525
  }
8354
- var CANONICAL_FUNCTION_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)$/;
8355
- var SAFE_FUNCTION_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
8526
+ var POSITIVE_FUNCTION_VERSION_PATTERN = /^[1-9][0-9]*$/;
8356
8527
  var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
8357
- 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
+ ]);
8358
8535
  var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
8359
- Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
8360
- Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8361
- ]), 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));
8362
8552
  var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
8363
8553
  var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
8364
8554
  var MAX_SECRET_COUNT = 1024;
@@ -8476,10 +8666,35 @@ function secretsForUpsert(inlineSecrets, environmentNames, environment) {
8476
8666
  throw new Error("'secrets' array required");
8477
8667
  return inlineSecrets;
8478
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
+ }
8479
8694
  function confirmedFunctionConfig(payload, expected) {
8480
- if (!payload || typeof payload !== "object" || Array.isArray(payload))
8695
+ const response = objectRecord(payload);
8696
+ if (!response)
8481
8697
  return false;
8482
- const response = payload;
8483
8698
  if (expected.verify_jwt !== undefined && response.verify_jwt !== expected.verify_jwt)
8484
8699
  return false;
8485
8700
  if (expected.background_routes !== undefined) {
@@ -8490,28 +8705,87 @@ function confirmedFunctionConfig(payload, expected) {
8490
8705
  }
8491
8706
  return true;
8492
8707
  }
8493
- function functionSourceCode(payload) {
8708
+ function functionSourceCode(payload, field = "code") {
8494
8709
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
8495
8710
  return null;
8496
- const code = payload.code;
8711
+ const code = payload[field];
8497
8712
  return typeof code === "string" ? code : null;
8498
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
+ }
8499
8730
  function objectRecord(candidate) {
8500
8731
  return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
8501
8732
  }
8502
- function activationResponse(slug, version, response) {
8503
- 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);
8504
8747
  if (!response.ok)
8505
- return releaseControlMutationFailure(operation, response);
8506
- const receipt = objectRecord(response.data);
8507
- const config = objectRecord(receipt?.config);
8508
- if (receipt?.success !== true || receipt.version !== version || config?.version !== version || typeof config.verify_jwt !== "boolean") {
8509
- 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;
8510
8762
  }
8511
- return releaseControlSuccess(operation, {
8512
- slug,
8513
- version,
8514
- 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
8515
8789
  });
8516
8790
  }
8517
8791
  function readOnlyActivationResult() {
@@ -8523,15 +8797,22 @@ function readOnlyActivationResult() {
8523
8797
  function functionActivationTarget(args) {
8524
8798
  const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
8525
8799
  const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
8526
- const version = args.version;
8527
- if (!SAFE_FUNCTION_REF_PATTERN.test(projectRef))
8528
- throw new Error("'ref' is invalid for 'activate'");
8800
+ const version = positiveFunctionVersion(args.version, "Function activation version");
8801
+ projectRefPathSegment(projectRef, "Edge Function activation");
8529
8802
  if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
8530
8803
  throw new Error("'slug' is invalid for 'activate'");
8531
- if (typeof version !== "string" || !CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
8532
- 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}'`);
8533
8811
  }
8534
- 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;
8535
8816
  }
8536
8817
  async function activateFunctionVersion(http, args, readOnly = false) {
8537
8818
  if (readOnly)
@@ -8539,29 +8820,40 @@ async function activateFunctionVersion(http, args, readOnly = false) {
8539
8820
  const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
8540
8821
  if (unsupported.length > 0)
8541
8822
  throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
8542
- const { projectRef, functionSlug, version } = functionActivationTarget(args);
8543
- const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/functions/${encodeURIComponent(functionSlug)}` + `/versions/${encodeURIComponent(version)}/activate`;
8544
- 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 }));
8545
8832
  }
8546
8833
  function registerAdvancedTools(server, http, environment = process.env, options = {}) {
8547
- 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.
8548
8835
  Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
8549
8836
  action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
8550
8837
  ref: withDescription(Type.String(), "Project ref"),
8551
8838
  slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
8552
- version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
8839
+ version: withDescription(functionVersionSchema, "[source/activate] Existing immutable Function version; source requires a positive version"),
8553
8840
  code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
8554
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"),
8555
8844
  output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
8556
8845
  files: optional(functionFilesSchema, "[deploy_bundle] File map as a JSON object: { 'index.ts': '...', '_shared/x.ts': '...' }"),
8557
8846
  entrypoint: optional(Type.String(), "[deploy_bundle] Entrypoint file (default: index.ts)"),
8558
8847
  minify: optional(Type.Boolean(), "[deploy/deploy_bundle] Minify bundle"),
8559
8848
  verify_jwt: optional(Type.Boolean(), "[deploy/deploy_bundle/config] Set JWT verification for this function"),
8560
- 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")
8561
8851
  }, async (args) => {
8562
8852
  if (args.action === "activate")
8563
8853
  return activateFunctionVersion(http, args, options.readOnly);
8564
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;
8565
8857
  let code = args.code;
8566
8858
  const need = (f, v) => {
8567
8859
  if (!v)
@@ -8578,16 +8870,10 @@ Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`,
8578
8870
  if (!hasFunctionConfig()) {
8579
8871
  throw new Error("'verify_jwt' or 'background_routes' required for 'config'");
8580
8872
  }
8581
- const cr = await http.patch(`/v1/projects/${ref}/functions/${slug}/config`, functionConfig());
8873
+ const cr = await http.patch(`${edgeFunctionResourcePath(ref, slug)}/config`, functionConfig());
8582
8874
  return cr.ok ? `✅ Function ${slug} config updated
8583
8875
  ${JSON.stringify(cr.data, null, 2)}` : `❌ Config update failed (${cr.status}): ${JSON.stringify(cr.data)}`;
8584
8876
  };
8585
- const deploymentPolicyReceiptText = (successText, responsePayload) => {
8586
- if (!hasFunctionConfig() || confirmedFunctionConfig(responsePayload, functionConfig())) {
8587
- return successText;
8588
- }
8589
- 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.";
8590
- };
8591
8877
  const checkSyntax = async (sourceCode) => {
8592
8878
  const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-check-"));
8593
8879
  const tmpFile = join2(tmpDir, "index.ts");
@@ -8602,7 +8888,7 @@ ${e.stderr || e.message}` };
8602
8888
  rmSync(tmpDir, { recursive: true, force: true });
8603
8889
  }
8604
8890
  };
8605
- if (pathArg && !code) {
8891
+ if (action === "check" && pathArg && !code) {
8606
8892
  try {
8607
8893
  code = await bundleEdgeFunctionPath(pathArg);
8608
8894
  } catch (error) {
@@ -8612,8 +8898,7 @@ ${e.stderr || e.message}` };
8612
8898
  }
8613
8899
  switch (action) {
8614
8900
  case "list":
8615
- text = JSON.stringify((await http.get(`/v1/projects/${ref}/functions`)).data, null, 2);
8616
- break;
8901
+ return functionListResponse(await http.get(edgeFunctionResourcePath(ref)));
8617
8902
  case "check":
8618
8903
  need("code (or path)", code);
8619
8904
  const checkRes = await checkSyntax(code);
@@ -8626,65 +8911,59 @@ ${checkRes.err}`;
8626
8911
  break;
8627
8912
  case "deploy":
8628
8913
  need("slug", slug);
8629
- need("code", code);
8630
- const deployCheck = await checkSyntax(code);
8631
- if (!deployCheck.ok) {
8632
- 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:
8633
8919
  ${deployCheck.err}`;
8634
- break;
8920
+ break;
8921
+ }
8635
8922
  }
8636
- const dr = await http.post(`/v1/projects/${ref}/functions/${slug}`, {
8637
- code,
8638
- 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,
8639
8927
  ...functionConfig()
8640
8928
  });
8641
- if (!dr.ok) {
8642
- text = `❌ Failed (${dr.status}): ${JSON.stringify(dr.data)}`;
8643
- break;
8644
- }
8645
- text = deploymentPolicyReceiptText(`✅ Function ${slug} deployed`, dr.data);
8646
- break;
8929
+ return functionMutationResponse({
8930
+ operation: "edge_functions.deploy",
8931
+ projectRef: ref,
8932
+ slug,
8933
+ expectedActiveVersion,
8934
+ config: functionConfig()
8935
+ }, deploymentResponse);
8647
8936
  case "deploy_bundle":
8648
8937
  need("slug", slug);
8649
8938
  need("files", files);
8650
- const br = await http.post(`/v1/projects/${ref}/functions/${slug}/bundle`, {
8939
+ const bundleResponse = await http.post(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
8651
8940
  files,
8652
8941
  entrypoint,
8653
8942
  minify,
8943
+ expected_active_version: expectedActiveVersion,
8654
8944
  ...functionConfig()
8655
8945
  });
8656
- if (!br.ok) {
8657
- text = `❌ Failed (${br.status}): ${JSON.stringify(br.data)}`;
8658
- break;
8659
- }
8660
- text = deploymentPolicyReceiptText(`✅ Function ${slug} bundle deployed (${Object.keys(files).length} files)`, br.data);
8661
- break;
8946
+ return functionMutationResponse({
8947
+ operation: "edge_functions.deploy_bundle",
8948
+ projectRef: ref,
8949
+ slug,
8950
+ expectedActiveVersion,
8951
+ config: functionConfig()
8952
+ }, bundleResponse);
8662
8953
  case "config":
8663
8954
  text = await updateFunctionConfig();
8664
8955
  break;
8665
8956
  case "source":
8666
8957
  need("slug", slug);
8667
- const sr = await http.get(`/v1/projects/${ref}/functions/${slug}/source`);
8668
- if (!sr.ok) {
8669
- text = `❌ Not found (${sr.status})`;
8670
- break;
8671
- }
8672
- if (!output) {
8673
- text = JSON.stringify(sr.data, null, 2);
8674
- break;
8675
- }
8676
- const sourceCode = functionSourceCode(sr.data);
8677
- if (sourceCode === null) {
8678
- text = "❌ Source response did not contain a string code field";
8679
- break;
8680
- }
8681
- const outputPath = resolve2(output);
8682
- writeFileSync(outputPath, sourceCode, { flag: "wx" });
8683
- text = `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`;
8684
- break;
8958
+ return readFunctionSource(http, {
8959
+ projectRef: ref,
8960
+ slug,
8961
+ version: args.version,
8962
+ output
8963
+ });
8685
8964
  case "delete":
8686
8965
  need("slug", slug);
8687
- 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`;
8688
8967
  break;
8689
8968
  default:
8690
8969
  text = `❌ Unknown action`;
@@ -10605,12 +10884,14 @@ var MAX_BODY_FILE_BYTES = 1048576;
10605
10884
  var MAX_HEADER_COUNT = 64;
10606
10885
  var MAX_HEADER_VALUE_LENGTH = 8192;
10607
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$/;
10608
10888
  var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
10609
10889
  var ACTION_ARGUMENTS2 = {
10610
10890
  list: new Set(["action", "ref"]),
10891
+ get: new Set(["action", "ref", "schedule_id"]),
10611
10892
  create: new Set(["action", "ref", "name", "slug", "cron", "method", "body_file", "header_env"]),
10612
- update: new Set(["action", "ref", "schedule_id", "name", "cron", "method", "enabled", "body_file", "header_env"]),
10613
- 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"])
10614
10895
  };
10615
10896
  function parseHeaderEnvironment(input) {
10616
10897
  if (typeof input !== "string")
@@ -10730,7 +11011,13 @@ function validScheduleDefinition(schedule) {
10730
11011
  return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
10731
11012
  }
10732
11013
  function validScheduleMetadata(schedule) {
10733
- 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;
10734
11021
  }
10735
11022
  function safeSchedulePayload(schedule) {
10736
11023
  const headerNames = safeHeaderNames(schedule.header_names);
@@ -10803,6 +11090,17 @@ function listResponse(ref, response) {
10803
11090
  return releaseControlFailure(operation, "INVALID_RESPONSE", null);
10804
11091
  return releaseControlSuccess(operation, { project_ref: ref, schedules });
10805
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
+ }
10806
11104
  function mutationResponse(expectation, response) {
10807
11105
  const { action, ref, requestId, expectedFields } = expectation;
10808
11106
  const scheduleId = action === "update" ? expectation.scheduleId : undefined;
@@ -10812,22 +11110,29 @@ function mutationResponse(expectation, response) {
10812
11110
  const payload = objectRecord2(response.data);
10813
11111
  const schedule = safeSchedule(payload?.schedule);
10814
11112
  const confirmsRequest = schedule && Object.entries(expectedFields).every(([field, expected]) => isDeepStrictEqual(schedule[field], expected));
10815
- 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) {
10816
11115
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
10817
11116
  }
10818
- 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
+ });
10819
11123
  }
10820
- function deleteResponse(ref, scheduleId, response) {
11124
+ function deleteResponse(ref, scheduleId, expectedUpdatedAt, response) {
10821
11125
  const operation = "scheduled_functions.delete";
10822
11126
  if (!response.ok)
10823
11127
  return releaseControlMutationFailure(operation, response);
10824
11128
  const payload = objectRecord2(response.data);
10825
- 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) {
10826
11130
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
10827
11131
  }
10828
11132
  return releaseControlSuccess(operation, {
10829
11133
  project_ref: ref,
10830
11134
  schedule_id: scheduleId,
11135
+ deleted_updated_at: expectedUpdatedAt,
10831
11136
  deleted: true
10832
11137
  });
10833
11138
  }
@@ -10876,7 +11181,15 @@ function requiredCron(args, action) {
10876
11181
  throw new Error(`'cron' is invalid for '${action}'`);
10877
11182
  return cron;
10878
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
+ }
10879
11191
  function updateRequest(args, environment) {
11192
+ const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "update");
10880
11193
  const body = scheduleBody(args.body_file);
10881
11194
  const headers = scheduleHeaders(args.header_env, environment);
10882
11195
  const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
@@ -10892,11 +11205,18 @@ function updateRequest(args, environment) {
10892
11205
  if (Object.keys(mutationFields).length === 0) {
10893
11206
  throw new Error("Scheduled Function update requires at least one field");
10894
11207
  }
10895
- 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)}`;
10896
11216
  }
10897
11217
  async function executeScheduleAction(http, environment, args, readOnly = false) {
10898
11218
  const action = args.action;
10899
- if (readOnly && action !== "list")
11219
+ if (readOnly && action !== "list" && action !== "get")
10900
11220
  return readOnlyResult2();
10901
11221
  assertActionArguments2(action, args);
10902
11222
  const ref = requiredText2(args, "ref", action);
@@ -10908,27 +11228,34 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
10908
11228
  return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
10909
11229
  }
10910
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));
10911
11234
  if (action === "update") {
10912
11235
  const request = updateRequest(args, environment);
10913
11236
  const requestId = request.request_id;
11237
+ const expectedUpdatedAt2 = request.expected_updated_at;
10914
11238
  return mutationResponse({
10915
11239
  action,
10916
11240
  ref,
10917
11241
  scheduleId,
10918
11242
  requestId,
11243
+ expectedUpdatedAt: expectedUpdatedAt2,
10919
11244
  expectedFields: safeMutationFields(request)
10920
- }, await http.patch(schedulePath(ref, scheduleId), request));
11245
+ }, await http.patch(targetPath, request));
10921
11246
  }
10922
- 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)));
10923
11249
  }
10924
11250
  function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
10925
11251
  server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
10926
11252
  }
10927
- 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";
10928
11254
  var SCHEDULE_TOOL_SCHEMA = {
10929
- action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
11255
+ action: withDescription(stringEnum(["list", "get", "create", "update", "delete"]), "Action"),
10930
11256
  ref: withDescription(Type.String(), "Project ref"),
10931
- 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"),
10932
11259
  name: optional(Type.String(), "[create/update] Display name"),
10933
11260
  slug: optional(Type.String(), "[create] Edge Function slug"),
10934
11261
  cron: optional(Type.String(), "[create/update] Five-field cron expression"),
@@ -10940,7 +11267,7 @@ var SCHEDULE_TOOL_SCHEMA = {
10940
11267
  // package.json
10941
11268
  var package_default = {
10942
11269
  name: "@supacloud/cli",
10943
- version: "0.16.0",
11270
+ version: "0.17.0",
10944
11271
  description: "Project-scoped CLI for SupaCloud users",
10945
11272
  type: "module",
10946
11273
  main: "./dist/index.js",
@@ -11156,8 +11483,9 @@ EXAMPLES
11156
11483
  ${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
11157
11484
  ${preferredCommand} ai show_skill
11158
11485
  ${preferredCommand} ai install_skill --dry_run
11159
- ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
11160
- ${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
11161
11489
  ${preferredCommand} scheduled_functions list --ref abc123
11162
11490
  ${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
11163
11491
  ${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -31,7 +31,7 @@ until a project-scoped context is resolved.
31
31
  - `supabase`: allowlisted official CLI adapter for migration authoring, local reset/diff, explicit-DSN inspection/backup/type generation, and SupaCloud-controlled migration push.
32
32
  - `auth`: provider and authentication configuration.
33
33
  - `storage`: buckets and object-management workflows.
34
- - `edge_functions`: deploy and configure Edge Functions.
34
+ - `edge_functions`: list, read immutable source, deploy, activate, and configure Edge Functions. Pass the positive version read from `list` to `source --version` and as `--expected-active-version`; use `absent` only for a new slug. Version `0` is internal-only.
35
35
  - `frontend`: list, build/deploy, domain, and deployment workflows.
36
36
  - `secrets`: project secret management; never print values after write.
37
37
  - `queue`, `task_events`, `diagnostics`: asynchronous workload operations and bounded diagnostics.