@supacloud/cli 0.16.0 → 0.18.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 non-negative integer
216
+ version from `edge_functions list`; use `0` for a listed legacy Function and
217
+ `absent` only when creating a slug that does not yet exist. A stale value returns
218
+ HTTP 409 without building, preheating, 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
@@ -195,28 +228,55 @@ server response body.
195
228
  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
- retry.
231
+ retry. For Function deploy, bundle deploy, and activation, the CLI applies a
232
+ separate 5-second, 64 KiB response-body boundary after receiving HTTP headers.
233
+ A stalled, oversized, truncated, unreadable, or malformed body is always
234
+ `OUTCOME_UNKNOWN` and its content is never included in CLI output.
235
+ Version `0` is reserved as the active-version CAS token for legacy Functions. It
236
+ can be passed only as `--expected-active-version`; immutable source reads and
237
+ activation targets still require a positive version.
238
+
239
+ ```json
240
+ {
241
+ "schema": "supacloud.cli.release-control.v1",
242
+ "ok": true,
243
+ "operation": "edge_functions.deploy_bundle",
244
+ "project_ref": "abc123",
245
+ "slug": "hello",
246
+ "previous_active_version": "7",
247
+ "active_version": "8",
248
+ "version": "8",
249
+ "verify_jwt": true
250
+ }
251
+ ```
199
252
 
200
253
  Scheduled Function lifecycle operations are also project-scoped:
201
254
 
202
255
  ```bash
203
256
  supacloud-cli scheduled_functions create --ref abc123 --name nightly \
204
257
  --slug cleanup --cron "0 2 * * *" --method POST
258
+ supacloud-cli scheduled_functions get --ref abc123 --schedule_id <id>
205
259
  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>
260
+ --expected_updated_at <updated_at-from-list> --cron "0 3 * * *"
261
+ supacloud-cli scheduled_functions delete --ref abc123 --schedule_id <id> \
262
+ --expected_updated_at <updated_at-from-list>
208
263
  ```
209
264
 
210
265
  Schedule IDs are canonical UUIDv4 values returned by create/list. Cron values
211
266
  use bounded numeric five-field syntax with wildcards, lists, ranges, and steps;
212
267
  out-of-range endpoints and steps are rejected before HTTP dispatch.
268
+ Update and delete require the exact canonical UTC `updated_at` returned by list.
269
+ A stale revision fails with HTTP 409 and performs no mutation; read the list
270
+ again before deciding whether to issue a new write.
213
271
 
214
272
  Use `--body_file ./payload.json` for a JSON-object request body. Header values
215
273
  must come from environment variables: pass a JSON name mapping such as
216
274
  `--header_env '{"x-schedule-token":"SCHEDULE_TOKEN"}'`. Platform-owned
217
275
  `authorization`, `apikey`, and `x-project-ref` headers cannot be overridden. Receipts never
218
276
  include header values or body content; list and mutation receipts report only
219
- whether the body is empty and the configured header names.
277
+ whether the body is empty and the configured header names. Update receipts bind
278
+ `previous_updated_at` to the requested revision and return a newer `updated_at`;
279
+ delete receipts return the matched revision as `deleted_updated_at`.
220
280
 
221
281
  For secret writes, `--from-env` accepts a comma-separated list of environment
222
282
  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"],
@@ -6576,6 +6576,8 @@ function validateExecutionPolicyCoverage(tools) {
6576
6576
 
6577
6577
  // src/shared/transports/http.ts
6578
6578
  var DEFAULT_TIMEOUT = 30000;
6579
+ var RELEASE_MUTATION_RESPONSE_TIMEOUT = 5000;
6580
+ var RELEASE_MUTATION_RESPONSE_MAX_BYTES = 64 * 1024;
6579
6581
  var MAX_RETRIES = 2;
6580
6582
  var RETRY_BASE_DELAY = 500;
6581
6583
  function isRetryableMethod(method) {
@@ -6598,6 +6600,14 @@ function transportFailure(error) {
6598
6600
  transportError: true
6599
6601
  };
6600
6602
  }
6603
+ function responseReadFailure(status) {
6604
+ return {
6605
+ ok: false,
6606
+ status,
6607
+ data: { error: "Response body unavailable", code: "RESPONSE_READ_ERROR" },
6608
+ responseReadError: true
6609
+ };
6610
+ }
6601
6611
  async function fetchWithTimeout(url, options) {
6602
6612
  const controller = new AbortController;
6603
6613
  const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
@@ -6647,41 +6657,114 @@ function joinedResponseBytes(chunks, totalBytes) {
6647
6657
  }
6648
6658
  return responseBytes;
6649
6659
  }
6650
- async function responseBytesWithinLimit(response, maxBytes) {
6651
- if (declaredResponseTooLarge(response, maxBytes)) {
6652
- await response.body?.cancel();
6653
- return null;
6654
- }
6655
- if (!response.body)
6656
- return new Uint8Array;
6657
- const reader = response.body.getReader();
6660
+ function cancelResponseReader(reader) {
6661
+ reader.cancel().catch(() => {
6662
+ return;
6663
+ });
6664
+ }
6665
+ function serializedRequestBody(body) {
6666
+ return body ? JSON.stringify(body) : undefined;
6667
+ }
6668
+ async function responseBytesFromReader(reader, maxBytes, declaredBytes) {
6658
6669
  const chunks = [];
6659
6670
  let totalBytes = 0;
6660
6671
  while (true) {
6661
6672
  const { done, value } = await reader.read();
6662
- if (done)
6663
- return joinedResponseBytes(chunks, totalBytes);
6673
+ if (done) {
6674
+ if (declaredBytes !== null && totalBytes !== declaredBytes)
6675
+ return { ok: false };
6676
+ return { ok: true, bytes: joinedResponseBytes(chunks, totalBytes) };
6677
+ }
6664
6678
  totalBytes += value.byteLength;
6665
6679
  if (totalBytes > maxBytes) {
6666
- await reader.cancel();
6667
- return null;
6680
+ cancelResponseReader(reader);
6681
+ return { ok: false };
6668
6682
  }
6669
6683
  chunks.push(value);
6670
6684
  }
6671
6685
  }
6686
+ async function responseBytesWithinLimit(response, maxBytes) {
6687
+ if (declaredResponseTooLarge(response, maxBytes)) {
6688
+ await response.body?.cancel();
6689
+ return null;
6690
+ }
6691
+ if (!response.body)
6692
+ return new Uint8Array;
6693
+ const reader = response.body.getReader();
6694
+ const bodyRead = await responseBytesFromReader(reader, maxBytes, null);
6695
+ return bodyRead.ok ? bodyRead.bytes : null;
6696
+ }
6672
6697
  function parsedUtf8Json(responseBytes) {
6673
6698
  try {
6674
6699
  const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
6675
- return JSON.parse(responseText);
6700
+ return { ok: true, parsedJson: JSON.parse(responseText) };
6676
6701
  } catch (error) {
6677
6702
  if (error instanceof SyntaxError || error instanceof TypeError)
6678
- return null;
6703
+ return { ok: false };
6679
6704
  throw error;
6680
6705
  }
6681
6706
  }
6682
6707
  async function boundedResponseJson(response, maxBytes) {
6683
6708
  const responseBytes = await responseBytesWithinLimit(response, maxBytes);
6684
- return responseBytes === null ? null : parsedUtf8Json(responseBytes);
6709
+ if (responseBytes === null)
6710
+ return null;
6711
+ const parsed = parsedUtf8Json(responseBytes);
6712
+ return parsed.ok ? parsed.parsedJson : null;
6713
+ }
6714
+ function declaredIdentityResponseBytes(response) {
6715
+ const contentEncoding = response.headers.get("content-encoding");
6716
+ if (contentEncoding !== null && contentEncoding.toLowerCase() !== "identity")
6717
+ return null;
6718
+ const contentLength = response.headers.get("content-length");
6719
+ if (contentLength === null)
6720
+ return null;
6721
+ if (!/^\d+$/.test(contentLength))
6722
+ return "invalid";
6723
+ const declaredBytes = Number(contentLength);
6724
+ return Number.isSafeInteger(declaredBytes) ? declaredBytes : "invalid";
6725
+ }
6726
+ async function releaseMutationResponseBytes(response) {
6727
+ const declaredBytes = declaredIdentityResponseBytes(response);
6728
+ if (declaredBytes === "invalid" || declaredBytes !== null && declaredBytes > RELEASE_MUTATION_RESPONSE_MAX_BYTES) {
6729
+ response.body?.cancel().catch(() => {
6730
+ return;
6731
+ });
6732
+ return { ok: false };
6733
+ }
6734
+ if (!response.body) {
6735
+ return declaredBytes === null || declaredBytes === 0 ? { ok: true, bytes: new Uint8Array } : { ok: false };
6736
+ }
6737
+ return responseBytesBeforeDeadline(response.body.getReader(), declaredBytes);
6738
+ }
6739
+ async function responseBytesBeforeDeadline(reader, declaredBytes) {
6740
+ let deadlineTimer;
6741
+ const deadline = new Promise((resolve2) => {
6742
+ deadlineTimer = setTimeout(() => {
6743
+ cancelResponseReader(reader);
6744
+ resolve2({ ok: false });
6745
+ }, RELEASE_MUTATION_RESPONSE_TIMEOUT);
6746
+ });
6747
+ try {
6748
+ return await Promise.race([
6749
+ responseBytesFromReader(reader, RELEASE_MUTATION_RESPONSE_MAX_BYTES, declaredBytes),
6750
+ deadline
6751
+ ]);
6752
+ } catch {
6753
+ cancelResponseReader(reader);
6754
+ return { ok: false };
6755
+ } finally {
6756
+ clearTimeout(deadlineTimer);
6757
+ }
6758
+ }
6759
+ async function releaseMutationResponseJson(response) {
6760
+ const responseBytes = await releaseMutationResponseBytes(response);
6761
+ if (!responseBytes.ok)
6762
+ return { ok: false };
6763
+ const parsed = parsedUtf8Json(responseBytes.bytes);
6764
+ return parsed.ok ? { ok: true, parsedJson: parsed.parsedJson } : { ok: false };
6765
+ }
6766
+ async function responseJsonOrNull(response) {
6767
+ return { ok: true, parsedJson: await response.json().catch(() => null) };
6685
6768
  }
6686
6769
 
6687
6770
  class HttpTransport {
@@ -6697,6 +6780,19 @@ class HttpTransport {
6697
6780
  "Content-Type": "application/json"
6698
6781
  };
6699
6782
  }
6783
+ async postWithResponseReader(path, serializedBody, responseReader) {
6784
+ try {
6785
+ const response = await fetchWithRetry(`${this.baseUrl}${path}`, {
6786
+ method: "POST",
6787
+ headers: this.headers(),
6788
+ body: serializedBody
6789
+ });
6790
+ const responseBody = await responseReader(response);
6791
+ return responseBody.ok ? { ok: response.ok, status: response.status, data: responseBody.parsedJson } : responseReadFailure(response.status);
6792
+ } catch (error) {
6793
+ return transportFailure(error);
6794
+ }
6795
+ }
6700
6796
  async get(path, options = {}) {
6701
6797
  try {
6702
6798
  const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
@@ -6711,17 +6807,14 @@ class HttpTransport {
6711
6807
  }
6712
6808
  async post(path, body) {
6713
6809
  try {
6714
- const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
6715
- method: "POST",
6716
- headers: this.headers(),
6717
- body: body ? JSON.stringify(body) : undefined
6718
- });
6719
- const data = await res.json().catch(() => null);
6720
- return { ok: res.ok, status: res.status, data };
6810
+ return await this.postWithResponseReader(path, serializedRequestBody(body), responseJsonOrNull);
6721
6811
  } catch (error) {
6722
6812
  return transportFailure(error);
6723
6813
  }
6724
6814
  }
6815
+ async postReleaseMutation(path, body) {
6816
+ return this.postWithResponseReader(path, serializedRequestBody(body), releaseMutationResponseJson);
6817
+ }
6725
6818
  async postMultipart(path, formData) {
6726
6819
  try {
6727
6820
  const headers = { Authorization: `Bearer ${this.token}` };
@@ -6964,6 +7057,28 @@ function migrationRows(data) {
6964
7057
  function migrationIdentityKey(version, name) {
6965
7058
  return `${version}\x00${name}`;
6966
7059
  }
7060
+ function migrationIdentities(data) {
7061
+ return migrationRows(data).flatMap((row) => row.version == null || row.name == null ? [] : [{ version: String(row.version), name: String(row.name) }]);
7062
+ }
7063
+ function identityConflicts(local, remote) {
7064
+ const reusesVersionOrName = local.version === remote.version || local.name === remote.name;
7065
+ const exactlyMatches = local.version === remote.version && local.name === remote.name;
7066
+ return reusesVersionOrName && !exactlyMatches;
7067
+ }
7068
+ function migrationIdentityConflicts(data, migrationFiles) {
7069
+ const remoteMigrations = migrationIdentities(data);
7070
+ return migrationFiles.flatMap((localMigration) => remoteMigrations.filter((remoteMigration) => identityConflicts(localMigration, remoteMigration)).map((remoteMigration) => ({ file: localMigration.file, local: localMigration, remote: remoteMigration })));
7071
+ }
7072
+ function assertNoMigrationIdentityConflicts(data, migrationFiles) {
7073
+ const conflicts = migrationIdentityConflicts(data, migrationFiles);
7074
+ if (!conflicts.length)
7075
+ return;
7076
+ throw new Error([
7077
+ "Migration identity conflicts:",
7078
+ ...conflicts.map(({ file, local, remote }) => `- ${file} (${local.version}) conflicts with remote ${remote.name} (${remote.version})`)
7079
+ ].join(`
7080
+ `));
7081
+ }
6967
7082
  function nameBoundMigrationMarkerKeys(data) {
6968
7083
  const keys = new Set;
6969
7084
  for (const row of migrationRows(data)) {
@@ -7227,6 +7342,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
7227
7342
  text = `❌ Failed to load applied migrations (${migrationsResult.status}): ${JSON.stringify(migrationsResult.data)}`;
7228
7343
  break;
7229
7344
  }
7345
+ assertNoMigrationIdentityConflicts(migrationsResult.data, migrationFiles);
7230
7346
  if (args.dry_run) {
7231
7347
  const appliedKeys = appliedMigrationKeys(migrationsResult.data);
7232
7348
  const pending = migrationFiles.filter(({ name, version }) => !appliedKeys.has(name) && !appliedKeys.has(String(version)));
@@ -7863,7 +7979,7 @@ function releaseControlFailure(operation, code, httpStatus) {
7863
7979
  });
7864
7980
  }
7865
7981
  function releaseControlMutationFailure(operation, response) {
7866
- const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
7982
+ const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
7867
7983
  return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
7868
7984
  }
7869
7985
  function releaseControlResponse(payload) {
@@ -7882,13 +7998,14 @@ var MAX_MIME_TYPE_LENGTH = 255;
7882
7998
  var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
7883
7999
  var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
7884
8000
  var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
8001
+ var BUCKET_REVISION_PATTERN = /^[0-9]{1,20}$/;
7885
8002
  var ACTION_ARGUMENTS = {
7886
8003
  status: new Set(["action"]),
7887
8004
  list_buckets: new Set(["action", "ref"]),
7888
8005
  get_bucket: new Set(["action", "ref", "bucket"]),
7889
8006
  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"]),
8007
+ update_bucket: new Set(["action", "ref", "bucket", "expected_revision", "public", "file_size_limit", "allowed_mime_types"]),
8008
+ delete_bucket: new Set(["action", "ref", "bucket", "expected_revision", "require_empty"]),
7892
8009
  list_files: new Set(["action", "ref", "bucket"]),
7893
8010
  upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
7894
8011
  delete_file: new Set(["action", "ref", "bucket", "filename"])
@@ -7944,6 +8061,16 @@ function requiredBucketId(args) {
7944
8061
  throw new Error("'bucket' is invalid for Storage buckets");
7945
8062
  return bucket;
7946
8063
  }
8064
+ function requiredBucketRevision(args) {
8065
+ const revision = requiredText(args, "expected_revision");
8066
+ if (!BUCKET_REVISION_PATTERN.test(revision))
8067
+ throw new Error("'expected_revision' is invalid for Storage buckets");
8068
+ return revision;
8069
+ }
8070
+ function assertEmptyBucketDeletion(args) {
8071
+ if (args.require_empty !== true)
8072
+ throw new Error("'require_empty=true' required for 'delete_bucket'");
8073
+ }
7947
8074
  function assertActionArguments(action, args) {
7948
8075
  const allowedArguments = ACTION_ARGUMENTS[action];
7949
8076
  if (!allowedArguments)
@@ -7978,9 +8105,12 @@ function assertBucketSettings(args) {
7978
8105
  function isAllowedMimeTypes(candidate) {
7979
8106
  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
8107
  }
8108
+ function isBucketRevision(candidate) {
8109
+ return candidate === null || typeof candidate === "string" && BUCKET_REVISION_PATTERN.test(candidate);
8110
+ }
7981
8111
  function safeBucket(candidate, expectedBucket) {
7982
8112
  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) {
8113
+ 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
8114
  return null;
7985
8115
  }
7986
8116
  return {
@@ -7988,7 +8118,8 @@ function safeBucket(candidate, expectedBucket) {
7988
8118
  name: bucket.name,
7989
8119
  public: bucket.public,
7990
8120
  file_size_limit: bucket.file_size_limit,
7991
- allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types]
8121
+ allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types],
8122
+ revision: bucket.revision
7992
8123
  };
7993
8124
  }
7994
8125
  function safeExactBucket(candidate, expectedBucket) {
@@ -8012,18 +8143,30 @@ function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
8012
8143
  return null;
8013
8144
  return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
8014
8145
  }
8015
- function safeDeletedBucket(candidate, expectedBucket) {
8146
+ function safeDeletedBucket(candidate, expectedBucket, expectedRevision) {
8016
8147
  const receipt = bucketRecord(candidate);
8017
- return receipt?.id === expectedBucket && receipt.deleted === true ? { bucket_id: expectedBucket, deleted: true } : null;
8148
+ return receipt?.id === expectedBucket && receipt.deleted === true && receipt.require_empty === true && receipt.previous_revision === expectedRevision && receipt.new_revision === null ? {
8149
+ bucket_id: expectedBucket,
8150
+ deleted: true,
8151
+ require_empty: true,
8152
+ previous_revision: expectedRevision,
8153
+ new_revision: null
8154
+ } : null;
8018
8155
  }
8019
8156
  function mutationReadbackResponse(expectation) {
8020
- const { operation, ref, response, expectedBucket, request } = expectation;
8157
+ const { operation, ref, response, expectedBucket, request, previousRevision, expectedNewRevision } = expectation;
8021
8158
  const readback = safeExactBucket(response.data, expectedBucket);
8022
- const validReadback = readback?.name === expectedBucket && bucketMatchesRequest(readback, request);
8159
+ const validReadback = readback?.name === expectedBucket && readback.revision !== null && (expectedNewRevision === undefined || readback.revision === expectedNewRevision) && bucketMatchesRequest(readback, request);
8023
8160
  if (!response.ok || !validReadback || !readback) {
8024
8161
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
8025
8162
  }
8026
- return releaseControlSuccess(operation, { project_ref: ref, bucket: readback });
8163
+ return releaseControlSuccess(operation, {
8164
+ project_ref: ref,
8165
+ bucket_id: expectedBucket,
8166
+ previous_revision: previousRevision,
8167
+ new_revision: readback.revision,
8168
+ bucket: readback
8169
+ });
8027
8170
  }
8028
8171
  function bucketResponse(expectation) {
8029
8172
  const { operation, ref, response, operationKind, safePayload } = expectation;
@@ -8049,10 +8192,10 @@ function createBucketRequest(args) {
8049
8192
  }
8050
8193
  function updateBucketRequest(args) {
8051
8194
  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)
8195
+ const settings = Object.fromEntries(["public", "file_size_limit", "allowed_mime_types"].filter((field) => args[field] !== undefined).map((field) => [field, args[field]]));
8196
+ if (Object.keys(settings).length === 0)
8054
8197
  throw new Error("Bucket update requires at least one field");
8055
- return request;
8198
+ return { expected_revision: requiredBucketRevision(args), ...settings };
8056
8199
  }
8057
8200
  function equalAllowedMimeTypes(candidate, expected) {
8058
8201
  if (!Array.isArray(expected))
@@ -8080,18 +8223,10 @@ async function createBucketMutationReceipt(http, ref, bucket, request) {
8080
8223
  safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
8081
8224
  });
8082
8225
  }
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
- });
8226
+ function safeUpdatedBucket(candidate, expectedBucket, expectedRevision, request) {
8227
+ const receipt = bucketRecord(candidate);
8228
+ const bucket = safeExactBucket(candidate, expectedBucket);
8229
+ 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
8230
  }
8096
8231
  async function listBuckets(http, args) {
8097
8232
  const ref = requiredProjectRef(args);
@@ -8134,35 +8269,45 @@ async function createBucket(http, args) {
8134
8269
  ref,
8135
8270
  response: await http.get(bucketPath),
8136
8271
  expectedBucket: bucket,
8137
- request: expectedReadback
8272
+ request: expectedReadback,
8273
+ previousRevision: null
8138
8274
  });
8139
8275
  }
8140
8276
  async function updateBucket(http, args) {
8141
8277
  const ref = requiredProjectRef(args);
8142
8278
  const bucket = requiredBucketId(args);
8143
8279
  const request = updateBucketRequest(args);
8280
+ const expectedRevision = request.expected_revision;
8144
8281
  const bucketPath = storageBucketPath(ref, bucket);
8145
- const receipt = await updateBucketMutationReceipt({ http, ref, bucket, bucketPath, request });
8146
- if (receipt.isError)
8147
- return receipt;
8282
+ const mutation = await http.put(bucketPath, request);
8283
+ if (!mutation.ok)
8284
+ return releaseControlMutationFailure("storage.update_bucket", mutation);
8285
+ const updated = safeUpdatedBucket(mutation.data, bucket, expectedRevision, request);
8286
+ if (!updated)
8287
+ return releaseControlFailure("storage.update_bucket", "OUTCOME_UNKNOWN", mutation.status);
8148
8288
  return mutationReadbackResponse({
8149
8289
  operation: "storage.update_bucket",
8150
8290
  ref,
8151
8291
  response: await http.get(bucketPath),
8152
8292
  expectedBucket: bucket,
8153
- request
8293
+ request,
8294
+ previousRevision: expectedRevision,
8295
+ expectedNewRevision: updated.revision ?? undefined
8154
8296
  });
8155
8297
  }
8156
8298
  async function deleteBucket(http, args) {
8157
8299
  const ref = requiredProjectRef(args);
8158
8300
  const bucket = requiredBucketId(args);
8301
+ const expectedRevision = requiredBucketRevision(args);
8302
+ assertEmptyBucketDeletion(args);
8159
8303
  const bucketPath = storageBucketPath(ref, bucket);
8304
+ const deletePath = `${bucketPath}?expected_revision=${encodeURIComponent(expectedRevision)}&require_empty=true`;
8160
8305
  const receipt = bucketResponse({
8161
8306
  operation: "storage.delete_bucket",
8162
8307
  ref,
8163
- response: await http.delete(bucketPath),
8308
+ response: await http.delete(deletePath),
8164
8309
  operationKind: "mutation",
8165
- safePayload: (candidate) => safeDeletedBucket(candidate, bucket)
8310
+ safePayload: (candidate) => safeDeletedBucket(candidate, bucket, expectedRevision)
8166
8311
  });
8167
8312
  if (receipt.isError)
8168
8313
  return receipt;
@@ -8203,6 +8348,8 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
8203
8348
  public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
8204
8349
  file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
8205
8350
  allowed_mime_types: withDescription(allowedMimeTypesSchema, "[create_bucket/update_bucket] MIME types as a comma-separated or JSON array"),
8351
+ expected_revision: optional(Type.String({ pattern: BUCKET_REVISION_PATTERN.source }), "[update_bucket/delete_bucket] Exact revision from list_buckets/get_bucket"),
8352
+ require_empty: optional(Type.Boolean(), "[delete_bucket] Must be true; deletion never empties a bucket"),
8206
8353
  filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
8207
8354
  base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
8208
8355
  mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
@@ -8268,12 +8415,79 @@ Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_
8268
8415
  }
8269
8416
 
8270
8417
  // src/shared/tools/advanced-tools.ts
8271
- import { existsSync as existsSync3, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync as statSync2, writeFileSync } from "node:fs";
8418
+ import { createHash as createHash2, timingSafeEqual } from "node:crypto";
8419
+ import {
8420
+ closeSync,
8421
+ constants as fsConstants,
8422
+ existsSync as existsSync3,
8423
+ fstatSync,
8424
+ mkdtempSync,
8425
+ openSync,
8426
+ readFileSync as readFileSync3,
8427
+ rmSync,
8428
+ statSync as statSync2,
8429
+ writeFileSync
8430
+ } from "node:fs";
8272
8431
  import { tmpdir } from "node:os";
8273
8432
  import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
8274
8433
  import { promisify } from "node:util";
8275
8434
  import { execFile } from "node:child_process";
8276
8435
  var execFileAsync = promisify(execFile);
8436
+ var SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/;
8437
+ function openFileIdentity(descriptor) {
8438
+ const state = fstatSync(descriptor, { bigint: true });
8439
+ if (!state.isFile())
8440
+ throw new Error("Prebundled path must be a regular file");
8441
+ return {
8442
+ dev: state.dev,
8443
+ ino: state.ino,
8444
+ size: state.size,
8445
+ mtimeNs: state.mtimeNs,
8446
+ ctimeNs: state.ctimeNs
8447
+ };
8448
+ }
8449
+ function sameOpenFile(left, right) {
8450
+ return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
8451
+ }
8452
+ function verifiedUtf8Code(bytes) {
8453
+ let code;
8454
+ try {
8455
+ code = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
8456
+ } catch (error) {
8457
+ if (error instanceof TypeError)
8458
+ throw new Error("Prebundled file is not valid UTF-8");
8459
+ throw error;
8460
+ }
8461
+ if (!Buffer.from(code, "utf8").equals(bytes)) {
8462
+ throw new Error("Prebundled file does not round-trip as UTF-8");
8463
+ }
8464
+ return code;
8465
+ }
8466
+ function assertExpectedSha256(bytes, expectedSha256) {
8467
+ if (!SHA256_HEX_PATTERN.test(expectedSha256)) {
8468
+ throw new Error("'--expected-sha256' must be a lowercase 64-character SHA-256");
8469
+ }
8470
+ const actualDigest = createHash2("sha256").update(bytes).digest();
8471
+ const expectedDigest = Buffer.from(expectedSha256, "hex");
8472
+ if (!timingSafeEqual(actualDigest, expectedDigest)) {
8473
+ throw new Error("Prebundled file SHA-256 does not match --expected-sha256");
8474
+ }
8475
+ }
8476
+ function readVerifiedPrebundledCode(pathArg, expectedSha256) {
8477
+ const descriptor = openSync(resolve2(pathArg), fsConstants.O_RDONLY | fsConstants.O_NONBLOCK);
8478
+ try {
8479
+ const identityBeforeRead = openFileIdentity(descriptor);
8480
+ const bytes = readFileSync3(descriptor);
8481
+ const identityAfterRead = openFileIdentity(descriptor);
8482
+ if (BigInt(bytes.byteLength) !== identityBeforeRead.size || !sameOpenFile(identityBeforeRead, identityAfterRead)) {
8483
+ throw new Error("Prebundled file changed while it was being read");
8484
+ }
8485
+ assertExpectedSha256(bytes, expectedSha256);
8486
+ return verifiedUtf8Code(bytes);
8487
+ } finally {
8488
+ closeSync(descriptor);
8489
+ }
8490
+ }
8277
8491
  async function runBunBuild(args) {
8278
8492
  try {
8279
8493
  return await execFileAsync("bun", ["build", ...args], { maxBuffer: 10 * 1024 * 1024 });
@@ -8298,6 +8512,54 @@ async function bundleEdgeFunctionPath(pathArg) {
8298
8512
  rmSync(tmpDir, { recursive: true, force: true });
8299
8513
  }
8300
8514
  }
8515
+ function prebundledDeployCode(pathArg, expectedSha256, minify) {
8516
+ if (typeof expectedSha256 !== "string") {
8517
+ throw new Error("'--expected-sha256' required with '--prebundled-path'");
8518
+ }
8519
+ if (minify !== undefined) {
8520
+ throw new Error("'--minify' cannot be combined with '--prebundled-path'");
8521
+ }
8522
+ return {
8523
+ code: readVerifiedPrebundledCode(pathArg, expectedSha256),
8524
+ prebundled: true,
8525
+ expectedSha256
8526
+ };
8527
+ }
8528
+ async function preparedDeployCode(args) {
8529
+ const codeArg = args.code;
8530
+ const pathArg = args.path;
8531
+ const prebundledPath = args["prebundled-path"];
8532
+ const sources = [codeArg, pathArg, prebundledPath].filter((source) => source !== undefined);
8533
+ if (sources.length !== 1) {
8534
+ throw new Error("Exactly one of '--code', '--path', or '--prebundled-path' is required for 'deploy'");
8535
+ }
8536
+ if (typeof prebundledPath === "string") {
8537
+ return prebundledDeployCode(prebundledPath, args["expected-sha256"], args.minify);
8538
+ }
8539
+ if (args["expected-sha256"] !== undefined) {
8540
+ throw new Error("'--expected-sha256' requires '--prebundled-path'");
8541
+ }
8542
+ if (typeof codeArg === "string")
8543
+ return { code: codeArg, prebundled: false };
8544
+ if (typeof pathArg !== "string")
8545
+ throw new Error("Function deploy source is invalid");
8546
+ return bundledDeployCode(pathArg);
8547
+ }
8548
+ async function bundledDeployCode(pathArg) {
8549
+ try {
8550
+ return { code: await bundleEdgeFunctionPath(pathArg), prebundled: false };
8551
+ } catch (error) {
8552
+ const message = error instanceof Error ? error.message : String(error);
8553
+ throw new Error(`Failed to bundle/read path ${pathArg}: ${message}`);
8554
+ }
8555
+ }
8556
+ function rejectPrebundledFlagsOutsideDeploy(action, args) {
8557
+ for (const flag of ["prebundled-path", "expected-sha256"]) {
8558
+ if (action !== "deploy" && args[flag] !== undefined) {
8559
+ throw new Error(`'--${flag}' is not supported for '${action}'`);
8560
+ }
8561
+ }
8562
+ }
8301
8563
  function resolveEntrypoint(pathArg) {
8302
8564
  const resolved = resolve2(pathArg);
8303
8565
  const stat = statSync2(resolved);
@@ -8344,21 +8606,53 @@ function parseFunctionFiles(input) {
8344
8606
  }
8345
8607
  }
8346
8608
  var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
8347
- function parseFunctionVersion(input) {
8609
+ function positiveFunctionVersion(input, label) {
8610
+ if (typeof input !== "string" && typeof input !== "number") {
8611
+ throw new Error(`${label} must be a canonical positive safe integer`);
8612
+ }
8613
+ const version = String(input);
8614
+ if (!POSITIVE_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
8615
+ throw new Error(`${label} must be a canonical positive safe integer`);
8616
+ }
8617
+ return version;
8618
+ }
8619
+ function activeFunctionVersionToken(input) {
8620
+ if (typeof input !== "string" && typeof input !== "number") {
8621
+ throw new Error("Expected active version must be a canonical non-negative safe integer");
8622
+ }
8348
8623
  const version = String(input);
8349
8624
  if (!CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
8350
- throw new Error("Function version must be a canonical safe integer");
8625
+ throw new Error("Expected active version must be a canonical non-negative safe integer");
8351
8626
  }
8352
8627
  return version;
8353
8628
  }
8354
8629
  var CANONICAL_FUNCTION_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)$/;
8355
- var SAFE_FUNCTION_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
8630
+ var POSITIVE_FUNCTION_VERSION_PATTERN = /^[1-9][0-9]*$/;
8356
8631
  var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
8357
- var FUNCTION_ACTIVATION_ARGUMENTS = new Set(["action", "ref", "slug", "version"]);
8632
+ var FUNCTION_ACTIVATION_ARGUMENTS = new Set([
8633
+ "action",
8634
+ "ref",
8635
+ "slug",
8636
+ "version",
8637
+ "expected-active-version"
8638
+ ]);
8358
8639
  var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
8640
+ Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
8641
+ Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8642
+ ]), Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), (input) => positiveFunctionVersion(input, "Function version")));
8643
+ function parseExpectedActiveVersion(input) {
8644
+ if (input === "absent")
8645
+ return input;
8646
+ return activeFunctionVersionToken(input);
8647
+ }
8648
+ var expectedActiveVersionSchema = Type.Optional(decodedSchema(Type.Union([
8649
+ Type.Literal("absent"),
8359
8650
  Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
8360
8651
  Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8361
- ]), Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), parseFunctionVersion));
8652
+ ]), Type.Union([
8653
+ Type.Literal("absent"),
8654
+ Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8655
+ ]), parseExpectedActiveVersion));
8362
8656
  var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
8363
8657
  var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
8364
8658
  var MAX_SECRET_COUNT = 1024;
@@ -8476,10 +8770,35 @@ function secretsForUpsert(inlineSecrets, environmentNames, environment) {
8476
8770
  throw new Error("'secrets' array required");
8477
8771
  return inlineSecrets;
8478
8772
  }
8773
+ var INVALID_FUNCTION_LIST_RESPONSE = "❌ Edge Function list response is invalid";
8774
+ var INVALID_FUNCTION_SOURCE_RESPONSE = "❌ Edge Function source response is invalid";
8775
+ function invalidFunctionReadResponse(message) {
8776
+ return { isError: true, content: [{ type: "text", text: message }] };
8777
+ }
8778
+ function safeFunctionList(payload) {
8779
+ if (!Array.isArray(payload))
8780
+ return null;
8781
+ const functionSlugs = new Set;
8782
+ for (const candidate of payload) {
8783
+ const edgeFunction = objectRecord(candidate);
8784
+ const slug = edgeFunction?.slug;
8785
+ const version = edgeFunction?.version;
8786
+ if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug) || typeof version !== "number" || !Number.isSafeInteger(version) || version < 0 || functionSlugs.has(slug))
8787
+ return null;
8788
+ functionSlugs.add(slug);
8789
+ }
8790
+ return payload;
8791
+ }
8792
+ function functionListResponse(response) {
8793
+ if (!response.ok)
8794
+ return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
8795
+ const functions = safeFunctionList(response.data);
8796
+ return functions ? { content: [{ type: "text", text: JSON.stringify(functions, null, 2) }] } : invalidFunctionReadResponse(INVALID_FUNCTION_LIST_RESPONSE);
8797
+ }
8479
8798
  function confirmedFunctionConfig(payload, expected) {
8480
- if (!payload || typeof payload !== "object" || Array.isArray(payload))
8799
+ const response = objectRecord(payload);
8800
+ if (!response)
8481
8801
  return false;
8482
- const response = payload;
8483
8802
  if (expected.verify_jwt !== undefined && response.verify_jwt !== expected.verify_jwt)
8484
8803
  return false;
8485
8804
  if (expected.background_routes !== undefined) {
@@ -8490,28 +8809,87 @@ function confirmedFunctionConfig(payload, expected) {
8490
8809
  }
8491
8810
  return true;
8492
8811
  }
8493
- function functionSourceCode(payload) {
8812
+ function functionSourceCode(payload, field = "code") {
8494
8813
  if (!payload || typeof payload !== "object" || Array.isArray(payload))
8495
8814
  return null;
8496
- const code = payload.code;
8815
+ const code = payload[field];
8497
8816
  return typeof code === "string" ? code : null;
8498
8817
  }
8818
+ function requestedSourceVersion(candidate) {
8819
+ return candidate === undefined ? undefined : positiveFunctionVersion(candidate, "Function source version");
8820
+ }
8821
+ function functionSourceOutput(slug, sourceCode, output) {
8822
+ if (!output) {
8823
+ return { content: [{ type: "text", text: JSON.stringify({ code: sourceCode }, null, 2) }] };
8824
+ }
8825
+ const outputPath = resolve2(output);
8826
+ writeFileSync(outputPath, sourceCode, { flag: "wx" });
8827
+ return {
8828
+ content: [{
8829
+ type: "text",
8830
+ text: `✅ Function ${slug} source written to ${outputPath} (${Buffer.byteLength(sourceCode)} bytes)`
8831
+ }]
8832
+ };
8833
+ }
8499
8834
  function objectRecord(candidate) {
8500
8835
  return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
8501
8836
  }
8502
- function activationResponse(slug, version, response) {
8503
- const operation = "edge_functions.activate";
8837
+ function edgeFunctionResourcePath(ref, slug) {
8838
+ const root = `/v1/projects/${projectRefPathSegment(ref, "Edge Functions")}/functions`;
8839
+ if (slug === undefined)
8840
+ return root;
8841
+ if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug)) {
8842
+ throw new Error("'slug' is invalid for Edge Functions");
8843
+ }
8844
+ return `${root}/${encodeURIComponent(slug)}`;
8845
+ }
8846
+ async function readFunctionSource(http, request) {
8847
+ const sourceVersion = requestedSourceVersion(request.version);
8848
+ const resourcePath = edgeFunctionResourcePath(request.projectRef, request.slug);
8849
+ const sourcePath = sourceVersion === undefined ? `${resourcePath}/source` : `${resourcePath}/versions/${encodeURIComponent(sourceVersion)}`;
8850
+ const response = await http.get(sourcePath);
8504
8851
  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);
8852
+ return invalidFunctionReadResponse(`❌ Failed (${response.status})`);
8853
+ const sourceCode = functionSourceCode(response.data, sourceVersion === undefined ? "code" : "source_code");
8854
+ return sourceCode === null ? invalidFunctionReadResponse(INVALID_FUNCTION_SOURCE_RESPONSE) : functionSourceOutput(request.slug, sourceCode, request.output);
8855
+ }
8856
+ function mutationIdentityMatches(receipt, expectation) {
8857
+ return receipt.success === true && receipt.project_ref === expectation.projectRef && receipt.slug === expectation.slug && receipt.previous_active_version === expectation.expectedActiveVersion;
8858
+ }
8859
+ function validReceiptVersion(activeVersion) {
8860
+ return typeof activeVersion === "string" && POSITIVE_FUNCTION_VERSION_PATTERN.test(activeVersion) && Number.isSafeInteger(Number(activeVersion));
8861
+ }
8862
+ function confirmedMutationVersion(receipt, config, expectation) {
8863
+ const activeVersion = receipt.active_version;
8864
+ if (!validReceiptVersion(activeVersion) || receipt.version !== activeVersion || config.version !== activeVersion || expectation.targetVersion !== undefined && activeVersion !== expectation.targetVersion) {
8865
+ return null;
8510
8866
  }
8511
- return releaseControlSuccess(operation, {
8512
- slug,
8513
- version,
8514
- verify_jwt: config.verify_jwt
8867
+ return activeVersion;
8868
+ }
8869
+ function confirmedFunctionMutation(expectation, payload) {
8870
+ const receipt = objectRecord(payload);
8871
+ const config = objectRecord(receipt?.config);
8872
+ if (!receipt || !config || !mutationIdentityMatches(receipt, expectation))
8873
+ return null;
8874
+ const activeVersion = confirmedMutationVersion(receipt, config, expectation);
8875
+ if (activeVersion === null || typeof config.verify_jwt !== "boolean" || !confirmedFunctionConfig(config, expectation.config ?? {}))
8876
+ return null;
8877
+ return { activeVersion, verifyJwt: config.verify_jwt };
8878
+ }
8879
+ function functionMutationResponse(expectation, response) {
8880
+ if (!response.ok)
8881
+ return releaseControlMutationFailure(expectation.operation, response);
8882
+ const confirmed = confirmedFunctionMutation(expectation, response.data);
8883
+ if (!confirmed) {
8884
+ return releaseControlFailure(expectation.operation, "OUTCOME_UNKNOWN", response.status);
8885
+ }
8886
+ return releaseControlSuccess(expectation.operation, {
8887
+ project_ref: expectation.projectRef,
8888
+ slug: expectation.slug,
8889
+ previous_active_version: expectation.expectedActiveVersion,
8890
+ active_version: confirmed.activeVersion,
8891
+ version: confirmed.activeVersion,
8892
+ verify_jwt: confirmed.verifyJwt
8515
8893
  });
8516
8894
  }
8517
8895
  function readOnlyActivationResult() {
@@ -8523,15 +8901,22 @@ function readOnlyActivationResult() {
8523
8901
  function functionActivationTarget(args) {
8524
8902
  const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
8525
8903
  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'");
8904
+ const version = positiveFunctionVersion(args.version, "Function activation version");
8905
+ projectRefPathSegment(projectRef, "Edge Function activation");
8529
8906
  if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
8530
8907
  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'");
8908
+ const expectedActiveVersion = requiredExpectedActiveVersion(args, "activate");
8909
+ return { projectRef, functionSlug, version, expectedActiveVersion };
8910
+ }
8911
+ function requiredExpectedActiveVersion(args, action) {
8912
+ const expected = args["expected-active-version"];
8913
+ if (expected === undefined) {
8914
+ throw new Error(`'--expected-active-version' required for '${action}'`);
8533
8915
  }
8534
- return { projectRef, functionSlug, version };
8916
+ const parsed = parseExpectedActiveVersion(expected);
8917
+ if (typeof parsed !== "string")
8918
+ throw new Error("Expected active version is invalid");
8919
+ return parsed;
8535
8920
  }
8536
8921
  async function activateFunctionVersion(http, args, readOnly = false) {
8537
8922
  if (readOnly)
@@ -8539,29 +8924,40 @@ async function activateFunctionVersion(http, args, readOnly = false) {
8539
8924
  const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
8540
8925
  if (unsupported.length > 0)
8541
8926
  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));
8927
+ const { projectRef, functionSlug, version, expectedActiveVersion } = functionActivationTarget(args);
8928
+ const endpoint = edgeFunctionResourcePath(projectRef, functionSlug) + `/versions/${encodeURIComponent(version)}/activate`;
8929
+ return functionMutationResponse({
8930
+ operation: "edge_functions.activate",
8931
+ projectRef,
8932
+ slug: functionSlug,
8933
+ expectedActiveVersion,
8934
+ targetVersion: version
8935
+ }, await http.postReleaseMutation(endpoint, { expected_active_version: expectedActiveVersion }));
8545
8936
  }
8546
8937
  function registerAdvancedTools(server, http, environment = process.env, options = {}) {
8547
- server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Server auto-bundles dependencies.
8938
+ server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Source deploys are bundled; verified prebuilt artifacts stay byte-exact.
8548
8939
  Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
8549
8940
  action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
8550
8941
  ref: withDescription(Type.String(), "Project ref"),
8551
8942
  slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
8552
- version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
8943
+ version: withDescription(functionVersionSchema, "[source/activate] Existing immutable Function version; source requires a positive version"),
8553
8944
  code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
8554
8945
  path: optional(Type.String(), "[deploy/check] Local file path to read code from (alternative to code)"),
8946
+ "prebundled-path": optional(Type.String(), "[deploy] Prebuilt runtime bundle to upload without rebuilding; requires expected-sha256"),
8947
+ "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
8948
  output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
8556
8949
  files: optional(functionFilesSchema, "[deploy_bundle] File map as a JSON object: { 'index.ts': '...', '_shared/x.ts': '...' }"),
8557
8950
  entrypoint: optional(Type.String(), "[deploy_bundle] Entrypoint file (default: index.ts)"),
8558
8951
  minify: optional(Type.Boolean(), "[deploy/deploy_bundle] Minify bundle"),
8559
8952
  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")
8953
+ background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI"),
8954
+ "expected-active-version": withDescription(expectedActiveVersionSchema, "[deploy/deploy_bundle/activate] Required current active version, or 'absent' when none exists")
8561
8955
  }, async (args) => {
8562
8956
  if (args.action === "activate")
8563
8957
  return activateFunctionVersion(http, args, options.readOnly);
8564
8958
  const { action, ref, slug, path: pathArg, output, files, entrypoint, minify, verify_jwt, background_routes } = args;
8959
+ rejectPrebundledFlagsOutsideDeploy(action, args);
8960
+ const expectedActiveVersion = action === "deploy" || action === "deploy_bundle" ? requiredExpectedActiveVersion(args, action) : undefined;
8565
8961
  let code = args.code;
8566
8962
  const need = (f, v) => {
8567
8963
  if (!v)
@@ -8578,16 +8974,10 @@ Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`,
8578
8974
  if (!hasFunctionConfig()) {
8579
8975
  throw new Error("'verify_jwt' or 'background_routes' required for 'config'");
8580
8976
  }
8581
- const cr = await http.patch(`/v1/projects/${ref}/functions/${slug}/config`, functionConfig());
8977
+ const cr = await http.patch(`${edgeFunctionResourcePath(ref, slug)}/config`, functionConfig());
8582
8978
  return cr.ok ? `✅ Function ${slug} config updated
8583
8979
  ${JSON.stringify(cr.data, null, 2)}` : `❌ Config update failed (${cr.status}): ${JSON.stringify(cr.data)}`;
8584
8980
  };
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
8981
  const checkSyntax = async (sourceCode) => {
8592
8982
  const tmpDir = mkdtempSync(join2(tmpdir(), "supacloud-edge-check-"));
8593
8983
  const tmpFile = join2(tmpDir, "index.ts");
@@ -8602,7 +8992,7 @@ ${e.stderr || e.message}` };
8602
8992
  rmSync(tmpDir, { recursive: true, force: true });
8603
8993
  }
8604
8994
  };
8605
- if (pathArg && !code) {
8995
+ if (action === "check" && pathArg && !code) {
8606
8996
  try {
8607
8997
  code = await bundleEdgeFunctionPath(pathArg);
8608
8998
  } catch (error) {
@@ -8612,8 +9002,7 @@ ${e.stderr || e.message}` };
8612
9002
  }
8613
9003
  switch (action) {
8614
9004
  case "list":
8615
- text = JSON.stringify((await http.get(`/v1/projects/${ref}/functions`)).data, null, 2);
8616
- break;
9005
+ return functionListResponse(await http.get(edgeFunctionResourcePath(ref)));
8617
9006
  case "check":
8618
9007
  need("code (or path)", code);
8619
9008
  const checkRes = await checkSyntax(code);
@@ -8626,65 +9015,59 @@ ${checkRes.err}`;
8626
9015
  break;
8627
9016
  case "deploy":
8628
9017
  need("slug", slug);
8629
- need("code", code);
8630
- const deployCheck = await checkSyntax(code);
8631
- if (!deployCheck.ok) {
8632
- text = `❌ Deployment aborted. Syntax check failed:
9018
+ const deployCode = await preparedDeployCode(args);
9019
+ if (!deployCode.prebundled) {
9020
+ const deployCheck = await checkSyntax(deployCode.code);
9021
+ if (!deployCheck.ok) {
9022
+ text = `❌ Deployment aborted. Syntax check failed:
8633
9023
  ${deployCheck.err}`;
8634
- break;
9024
+ break;
9025
+ }
8635
9026
  }
8636
- const dr = await http.post(`/v1/projects/${ref}/functions/${slug}`, {
8637
- code,
8638
- minify,
9027
+ const deploymentResponse = await http.postReleaseMutation(edgeFunctionResourcePath(ref, slug), {
9028
+ code: deployCode.code,
9029
+ ...deployCode.prebundled ? { prebundled: true, expected_sha256: deployCode.expectedSha256 } : { minify },
9030
+ expected_active_version: expectedActiveVersion,
8639
9031
  ...functionConfig()
8640
9032
  });
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;
9033
+ return functionMutationResponse({
9034
+ operation: "edge_functions.deploy",
9035
+ projectRef: ref,
9036
+ slug,
9037
+ expectedActiveVersion,
9038
+ config: functionConfig()
9039
+ }, deploymentResponse);
8647
9040
  case "deploy_bundle":
8648
9041
  need("slug", slug);
8649
9042
  need("files", files);
8650
- const br = await http.post(`/v1/projects/${ref}/functions/${slug}/bundle`, {
9043
+ const bundleResponse = await http.postReleaseMutation(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
8651
9044
  files,
8652
9045
  entrypoint,
8653
9046
  minify,
9047
+ expected_active_version: expectedActiveVersion,
8654
9048
  ...functionConfig()
8655
9049
  });
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;
9050
+ return functionMutationResponse({
9051
+ operation: "edge_functions.deploy_bundle",
9052
+ projectRef: ref,
9053
+ slug,
9054
+ expectedActiveVersion,
9055
+ config: functionConfig()
9056
+ }, bundleResponse);
8662
9057
  case "config":
8663
9058
  text = await updateFunctionConfig();
8664
9059
  break;
8665
9060
  case "source":
8666
9061
  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;
9062
+ return readFunctionSource(http, {
9063
+ projectRef: ref,
9064
+ slug,
9065
+ version: args.version,
9066
+ output
9067
+ });
8685
9068
  case "delete":
8686
9069
  need("slug", slug);
8687
- text = (await http.delete(`/v1/projects/${ref}/functions/${slug}`)).ok ? `✅ Function ${slug} deleted` : `❌ Failed`;
9070
+ text = (await http.delete(edgeFunctionResourcePath(ref, slug))).ok ? `✅ Function ${slug} deleted` : `❌ Failed`;
8688
9071
  break;
8689
9072
  default:
8690
9073
  text = `❌ Unknown action`;
@@ -10605,12 +10988,14 @@ var MAX_BODY_FILE_BYTES = 1048576;
10605
10988
  var MAX_HEADER_COUNT = 64;
10606
10989
  var MAX_HEADER_VALUE_LENGTH = 8192;
10607
10990
  var MAX_SCHEDULE_NAME_LENGTH = 120;
10991
+ var CANONICAL_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
10608
10992
  var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
10609
10993
  var ACTION_ARGUMENTS2 = {
10610
10994
  list: new Set(["action", "ref"]),
10995
+ get: new Set(["action", "ref", "schedule_id"]),
10611
10996
  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"])
10997
+ update: new Set(["action", "ref", "schedule_id", "expected_updated_at", "name", "cron", "method", "enabled", "body_file", "header_env"]),
10998
+ delete: new Set(["action", "ref", "schedule_id", "expected_updated_at"])
10614
10999
  };
10615
11000
  function parseHeaderEnvironment(input) {
10616
11001
  if (typeof input !== "string")
@@ -10730,7 +11115,13 @@ function validScheduleDefinition(schedule) {
10730
11115
  return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
10731
11116
  }
10732
11117
  function validScheduleMetadata(schedule) {
10733
- return typeof schedule.created_at === "string" && typeof schedule.updated_at === "string";
11118
+ return typeof schedule.created_at === "string" && isCanonicalTimestamp(schedule.updated_at);
11119
+ }
11120
+ function isCanonicalTimestamp(candidate) {
11121
+ if (typeof candidate !== "string" || !CANONICAL_TIMESTAMP_PATTERN.test(candidate))
11122
+ return false;
11123
+ const milliseconds = Date.parse(candidate);
11124
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
10734
11125
  }
10735
11126
  function safeSchedulePayload(schedule) {
10736
11127
  const headerNames = safeHeaderNames(schedule.header_names);
@@ -10803,6 +11194,17 @@ function listResponse(ref, response) {
10803
11194
  return releaseControlFailure(operation, "INVALID_RESPONSE", null);
10804
11195
  return releaseControlSuccess(operation, { project_ref: ref, schedules });
10805
11196
  }
11197
+ function getResponse(ref, scheduleId, response) {
11198
+ const operation = "scheduled_functions.get";
11199
+ if (!response.ok)
11200
+ return scheduleFailure(operation, response);
11201
+ const payload = objectRecord2(response.data);
11202
+ const schedule = safeSchedule(payload?.schedule);
11203
+ if (payload?.project_ref !== ref || !schedule || schedule.id !== scheduleId) {
11204
+ return releaseControlFailure(operation, "INVALID_RESPONSE", null);
11205
+ }
11206
+ return releaseControlSuccess(operation, { project_ref: ref, schedule });
11207
+ }
10806
11208
  function mutationResponse(expectation, response) {
10807
11209
  const { action, ref, requestId, expectedFields } = expectation;
10808
11210
  const scheduleId = action === "update" ? expectation.scheduleId : undefined;
@@ -10812,22 +11214,29 @@ function mutationResponse(expectation, response) {
10812
11214
  const payload = objectRecord2(response.data);
10813
11215
  const schedule = safeSchedule(payload?.schedule);
10814
11216
  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) {
11217
+ const confirmsRevision = action === "create" || payload?.previous_updated_at === expectation.expectedUpdatedAt && schedule !== null && schedule.updated_at > expectation.expectedUpdatedAt;
11218
+ if (payload?.project_ref !== ref || payload.request_id !== requestId || payload?.[action === "create" ? "created" : "updated"] !== true || !schedule || !confirmsRequest || !confirmsRevision || scheduleId !== undefined && schedule.id !== scheduleId) {
10816
11219
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
10817
11220
  }
10818
- return releaseControlSuccess(operation, { project_ref: ref, request_id: requestId, schedule });
11221
+ return releaseControlSuccess(operation, {
11222
+ project_ref: ref,
11223
+ request_id: requestId,
11224
+ ...action === "update" ? { previous_updated_at: expectation.expectedUpdatedAt } : {},
11225
+ schedule
11226
+ });
10819
11227
  }
10820
- function deleteResponse(ref, scheduleId, response) {
11228
+ function deleteResponse(ref, scheduleId, expectedUpdatedAt, response) {
10821
11229
  const operation = "scheduled_functions.delete";
10822
11230
  if (!response.ok)
10823
11231
  return releaseControlMutationFailure(operation, response);
10824
11232
  const payload = objectRecord2(response.data);
10825
- if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId) {
11233
+ if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId || payload.deleted_updated_at !== expectedUpdatedAt) {
10826
11234
  return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
10827
11235
  }
10828
11236
  return releaseControlSuccess(operation, {
10829
11237
  project_ref: ref,
10830
11238
  schedule_id: scheduleId,
11239
+ deleted_updated_at: expectedUpdatedAt,
10831
11240
  deleted: true
10832
11241
  });
10833
11242
  }
@@ -10876,7 +11285,15 @@ function requiredCron(args, action) {
10876
11285
  throw new Error(`'cron' is invalid for '${action}'`);
10877
11286
  return cron;
10878
11287
  }
11288
+ function requiredExpectedUpdatedAt(args, action) {
11289
+ const candidate = args.expected_updated_at;
11290
+ if (!isCanonicalTimestamp(candidate)) {
11291
+ throw new Error(`'expected_updated_at' must be a canonical UTC timestamp for '${action}'`);
11292
+ }
11293
+ return candidate;
11294
+ }
10879
11295
  function updateRequest(args, environment) {
11296
+ const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "update");
10880
11297
  const body = scheduleBody(args.body_file);
10881
11298
  const headers = scheduleHeaders(args.header_env, environment);
10882
11299
  const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
@@ -10892,11 +11309,18 @@ function updateRequest(args, environment) {
10892
11309
  if (Object.keys(mutationFields).length === 0) {
10893
11310
  throw new Error("Scheduled Function update requires at least one field");
10894
11311
  }
10895
- return { request_id: randomUUID(), ...mutationFields };
11312
+ return {
11313
+ request_id: randomUUID(),
11314
+ expected_updated_at: expectedUpdatedAt,
11315
+ ...mutationFields
11316
+ };
11317
+ }
11318
+ function deletePath(schedulePathname, expectedUpdatedAt) {
11319
+ return `${schedulePathname}?expected_updated_at=${encodeURIComponent(expectedUpdatedAt)}`;
10896
11320
  }
10897
11321
  async function executeScheduleAction(http, environment, args, readOnly = false) {
10898
11322
  const action = args.action;
10899
- if (readOnly && action !== "list")
11323
+ if (readOnly && action !== "list" && action !== "get")
10900
11324
  return readOnlyResult2();
10901
11325
  assertActionArguments2(action, args);
10902
11326
  const ref = requiredText2(args, "ref", action);
@@ -10908,27 +11332,34 @@ async function executeScheduleAction(http, environment, args, readOnly = false)
10908
11332
  return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
10909
11333
  }
10910
11334
  const scheduleId = requiredText2(args, "schedule_id", action);
11335
+ const targetPath = schedulePath(ref, scheduleId);
11336
+ if (action === "get")
11337
+ return getResponse(ref, scheduleId, await http.get(targetPath));
10911
11338
  if (action === "update") {
10912
11339
  const request = updateRequest(args, environment);
10913
11340
  const requestId = request.request_id;
11341
+ const expectedUpdatedAt2 = request.expected_updated_at;
10914
11342
  return mutationResponse({
10915
11343
  action,
10916
11344
  ref,
10917
11345
  scheduleId,
10918
11346
  requestId,
11347
+ expectedUpdatedAt: expectedUpdatedAt2,
10919
11348
  expectedFields: safeMutationFields(request)
10920
- }, await http.patch(schedulePath(ref, scheduleId), request));
11349
+ }, await http.patch(targetPath, request));
10921
11350
  }
10922
- return deleteResponse(ref, scheduleId, await http.delete(schedulePath(ref, scheduleId)));
11351
+ const expectedUpdatedAt = requiredExpectedUpdatedAt(args, "delete");
11352
+ return deleteResponse(ref, scheduleId, expectedUpdatedAt, await http.delete(deletePath(targetPath, expectedUpdatedAt)));
10923
11353
  }
10924
11354
  function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
10925
11355
  server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
10926
11356
  }
10927
- var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, create, update, delete";
11357
+ var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, get, create, update, delete";
10928
11358
  var SCHEDULE_TOOL_SCHEMA = {
10929
- action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
11359
+ action: withDescription(stringEnum(["list", "get", "create", "update", "delete"]), "Action"),
10930
11360
  ref: withDescription(Type.String(), "Project ref"),
10931
- schedule_id: optional(Type.String(), "[update/delete] Schedule ID"),
11361
+ schedule_id: optional(Type.String(), "[get/update/delete] Schedule ID"),
11362
+ expected_updated_at: optional(Type.String(), "[update/delete] Canonical updated_at from list"),
10932
11363
  name: optional(Type.String(), "[create/update] Display name"),
10933
11364
  slug: optional(Type.String(), "[create] Edge Function slug"),
10934
11365
  cron: optional(Type.String(), "[create/update] Five-field cron expression"),
@@ -10940,7 +11371,7 @@ var SCHEDULE_TOOL_SCHEMA = {
10940
11371
  // package.json
10941
11372
  var package_default = {
10942
11373
  name: "@supacloud/cli",
10943
- version: "0.16.0",
11374
+ version: "0.18.0",
10944
11375
  description: "Project-scoped CLI for SupaCloud users",
10945
11376
  type: "module",
10946
11377
  main: "./dist/index.js",
@@ -11156,8 +11587,9 @@ EXAMPLES
11156
11587
  ${preferredCommand} branch promote --branch_ref preview123 --plan_checksum <sha256>
11157
11588
  ${preferredCommand} ai show_skill
11158
11589
  ${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
11590
+ ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello --expected-active-version absent
11591
+ ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
11592
+ ${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4
11161
11593
  ${preferredCommand} scheduled_functions list --ref abc123
11162
11594
  ${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
11163
11595
  ${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.18.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 non-negative version read from `list` as `--expected-active-version`; use `absent` only for a new slug. Version `0` is a legacy CAS token and cannot be used as a source or activation target.
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.