@supacloud/cli 0.17.0 → 0.19.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
@@ -92,15 +92,19 @@ supacloud-cli status
92
92
 
93
93
  Project context is resolved from one atomic source: a named profile selected by
94
94
  `--env`, an explicit `--env-file`, a complete process environment, or the
95
- legacy `.env` fallback. Core URL, token, and project-ref values are not filled
96
- by mixing sources. If `SUPACLOUD_ENV` is set without a complete process context,
97
- it strictly selects `.env.supacloud.<value>`. For backward compatibility, when
98
- no selector and no core process variables are present, `supacloud-cli` still
99
- tries to auto-link from `.env` using:
100
-
101
- - `SUPABASE_URL` or `SUPACLOUD_API_URL`
102
- - `SUPABASE_SERVICE_ROLE_KEY` or `SUPACLOUD_API_TOKEN`
103
- - `SUPACLOUD_PROJECT_REF` when the project ref cannot be inferred from a managed `<ref>.api.*` hostname
95
+ legacy `.env` fallback. Core URL, credential, and project-ref values are not
96
+ filled by mixing sources. If `SUPACLOUD_ENV` is set without a complete process
97
+ context, it strictly selects `.env.supacloud.<value>`.
98
+
99
+ The two credential scopes are separate. Management-backed remote commands use
100
+ only `SUPACLOUD_API_URL` + `SUPACLOUD_API_TOKEN`. An application profile using
101
+ `SUPABASE_URL` + `SUPABASE_SERVICE_ROLE_KEY` can be auto-linked for `status`,
102
+ but its service-role key is never substituted for a Management token and cannot
103
+ enable Management-backed tools. Both URL types must be canonical HTTPS origins;
104
+ omit explicit default ports such as `:443`. HTTP is accepted only for literal
105
+ loopback development origins, with the default `:80` likewise omitted. Use
106
+ `SUPACLOUD_PROJECT_REF` when it cannot be inferred from a managed
107
+ `<ref>.api.*` application hostname.
104
108
 
105
109
  The legacy `.env` fallback is unclassified and therefore does not enable the
106
110
  production confirmation gate. Production automation must select a `prod` or
@@ -127,11 +131,13 @@ for one command. A production profile cannot target a different project with
127
131
  `--ref`; the requested ref and `--confirm-production` must both exactly match
128
132
  the profile's project ref.
129
133
 
130
- `status` checks configuration, Management API connectivity, and authentication;
131
- it exits non-zero when any required check fails. Its output includes
132
- `environment`, `source` (`kind` and `path`), `apiUrl`, `projectRef`, `readOnly`,
133
- `production`, and `hasApiToken`. It never prints the API token or service-role
134
- key.
134
+ `status` checks configuration, connectivity, and authentication against the
135
+ selected credential scope. Application profiles probe the project data API at
136
+ the exact configured origin; credential-bearing probes refuse redirects. The
137
+ command exits non-zero when any required check fails. Its output includes
138
+ `credentialScope`, `environment`, `source` (`kind` and `path`), `apiUrl`,
139
+ `projectRef`, `readOnly`, `production`, and `hasApiToken`. It never prints the
140
+ API token or service-role key.
135
141
 
136
142
  Examples:
137
143
 
@@ -212,10 +218,10 @@ already exist. Add the positive version observed from `list` as
212
218
  pointer; this remains correct across an active-version A→B→A transition.
213
219
 
214
220
  `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
221
+ `--expected-active-version <N|absent>`. Read the current non-negative integer
222
+ version from `edge_functions list`; use `0` for a listed legacy Function and
223
+ `absent` only when creating a slug that does not yet exist. A stale value returns
224
+ HTTP 409 without building, preheating, or activating another version. List output remains a JSON array with string
219
225
  `slug` and numeric `version` fields, while source output is exactly
220
226
  `{ "code": "..." }`. Release automation must use `source --version <N>` for a
221
227
  version-bound backup.
@@ -228,9 +234,13 @@ server response body.
228
234
  Mutation receipts use schema `supacloud.cli.release-control.v1`. An
229
235
  `OUTCOME_UNKNOWN` error means the server may have committed the mutation before
230
236
  the response was lost or failed validation; read back current state before any
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.
237
+ retry. For Function deploy, bundle deploy, and activation, the CLI applies a
238
+ separate 5-second, 64 KiB response-body boundary after receiving HTTP headers.
239
+ A stalled, oversized, truncated, unreadable, or malformed body is always
240
+ `OUTCOME_UNKNOWN` and its content is never included in CLI output.
241
+ Version `0` is reserved as the active-version CAS token for legacy Functions. It
242
+ can be passed only as `--expected-active-version`; immutable source reads and
243
+ activation targets still require a positive version.
234
244
 
235
245
  ```json
236
246
  {
@@ -321,10 +331,10 @@ supacloud-cli supabase push --ref abc123 --dir supabase/migrations --dry_run
321
331
  supacloud-cli supabase push --ref abc123 --dir supabase/migrations
322
332
  ```
323
333
 
324
- `push` uses `SUPABASE_SERVICE_ROLE_KEY` or `SUPACLOUD_API_TOKEN` only for the
325
- SupaCloud Management API. Those credentials, upstream access tokens, database
326
- passwords, and secret/key environment variables are removed from the official
327
- CLI child process, and command output is redacted.
334
+ `push` uses only `SUPACLOUD_API_TOKEN` for the SupaCloud Management API. That
335
+ token, upstream access tokens, database passwords, and secret/key environment
336
+ variables are removed from the official CLI child process, and command output
337
+ is redacted.
328
338
 
329
339
  `push` requires a resolved project ref; pass `--ref` explicitly or set
330
340
  `SUPACLOUD_PROJECT_REF`. Relative migration directories are resolved against
package/dist/index.js CHANGED
@@ -6333,58 +6333,45 @@ function readEnvFile(path, required) {
6333
6333
  throw new Error(`Failed to read SupaCloud environment file ${path}: ${message}`);
6334
6334
  }
6335
6335
  }
6336
- function normalizeUrl(value) {
6337
- const trimmed = value.trim().replace(/\/+$/, "");
6338
- if (!trimmed)
6336
+ function canonicalApiOrigin(value) {
6337
+ const candidate = value.trim();
6338
+ if (!candidate)
6339
6339
  return "";
6340
6340
  try {
6341
- return new URL(trimmed).toString().replace(/\/+$/, "");
6342
- } catch {
6343
- return "";
6341
+ const url = new URL(candidate);
6342
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
6343
+ const allowedProtocol = url.protocol === "https:" || url.protocol === "http:" && loopback;
6344
+ const exactOrigin = candidate === url.origin || candidate === `${url.origin}/`;
6345
+ return allowedProtocol && !url.username && !url.password && exactOrigin && url.pathname === "/" && !url.search && !url.hash ? url.origin : "";
6346
+ } catch (error) {
6347
+ if (error instanceof TypeError)
6348
+ return "";
6349
+ throw error;
6344
6350
  }
6345
6351
  }
6346
6352
  function hostFromUrl(value) {
6347
- try {
6348
- return new URL(value).hostname;
6349
- } catch {
6350
- return "";
6351
- }
6353
+ return value ? new URL(value).hostname : "";
6352
6354
  }
6353
6355
  function inferProjectRefFromSupabaseUrl(value) {
6354
- const normalized = normalizeUrl(value);
6355
- if (!normalized)
6356
+ if (!value)
6356
6357
  return "";
6357
- return new URL(normalized).hostname.match(/^([a-z0-9-]+)\.api\./i)?.[1] ?? "";
6358
+ return new URL(value).hostname.match(/^([a-z0-9-]+)\.api\./i)?.[1] ?? "";
6358
6359
  }
6359
- function inferManagementApiUrlFromSupabaseUrl(value, projectRef = "") {
6360
- const normalized = normalizeUrl(value);
6361
- if (!normalized)
6362
- return "";
6363
- const url = new URL(normalized);
6364
- const host = url.hostname;
6365
- if (host.startsWith("api.")) {
6366
- url.hostname = `studio.${host.slice("api.".length)}`;
6367
- return url.toString().replace(/\/+$/, "");
6368
- }
6369
- const ref = projectRef.trim();
6370
- if (ref && host.startsWith(`${ref}.api.`)) {
6371
- url.hostname = `studio-${ref}.${host.slice(`${ref}.api.`.length)}`;
6372
- return url.toString().replace(/\/+$/, "");
6373
- }
6374
- const managedHost = host.match(/^([a-z0-9-]+)\.api\.(.+)$/i);
6375
- if (managedHost) {
6376
- url.hostname = `studio-${managedHost[1]}.${managedHost[2]}`;
6377
- return url.toString().replace(/\/+$/, "");
6378
- }
6379
- return normalized;
6360
+ function sourceCredentialScope(values, explicitApiUrl, supabaseUrl) {
6361
+ const hasManagementContext = Boolean(explicitApiUrl.trim() || values.SUPACLOUD_API_TOKEN?.trim() || values.SUPACLOUD_HOST?.trim());
6362
+ if (hasManagementContext)
6363
+ return "management";
6364
+ return supabaseUrl || values.SUPABASE_SERVICE_ROLE_KEY?.trim() ? "project_application" : "incomplete";
6380
6365
  }
6381
6366
  function sourceProjectCore(values) {
6382
- const supabaseUrl = normalizeUrl(values.SUPABASE_URL || "");
6367
+ const supabaseUrl = canonicalApiOrigin(values.SUPABASE_URL || "");
6383
6368
  const projectRef = (values.SUPACLOUD_PROJECT_REF || values.X_PROJECT_REF || "").trim() || inferProjectRefFromSupabaseUrl(supabaseUrl);
6384
6369
  const explicitApiUrl = values.SUPACLOUD_API_URL || values.SUPACLOUD_MANAGEMENT_API_URL || values.MANAGEMENT_API_URL || "";
6385
- const apiUrl = normalizeUrl(explicitApiUrl) || inferManagementApiUrlFromSupabaseUrl(supabaseUrl, projectRef) || (values.SUPACLOUD_HOST ? `http://${values.SUPACLOUD_HOST}:9090` : "");
6386
- const apiToken = values.SUPACLOUD_API_TOKEN || values.SUPABASE_SERVICE_ROLE_KEY || "";
6387
- return { apiUrl, apiToken, projectRef, supabaseUrl };
6370
+ const credentialScope = sourceCredentialScope(values, explicitApiUrl, supabaseUrl);
6371
+ const managementUrl = explicitApiUrl || (values.SUPACLOUD_HOST ? `http://${values.SUPACLOUD_HOST}:9090` : "");
6372
+ const apiUrl = credentialScope === "management" ? canonicalApiOrigin(managementUrl) : "";
6373
+ const apiToken = credentialScope === "management" ? values.SUPACLOUD_API_TOKEN || "" : "";
6374
+ return { apiUrl, apiToken, projectRef, supabaseUrl, credentialScope };
6388
6375
  }
6389
6376
  function processValues(env) {
6390
6377
  return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
@@ -6394,6 +6381,9 @@ function hasProcessContext(env) {
6394
6381
  }
6395
6382
  function completeProjectContext(values) {
6396
6383
  const core = sourceProjectCore(values);
6384
+ if (core.credentialScope === "project_application") {
6385
+ return Boolean(core.supabaseUrl && values.SUPABASE_SERVICE_ROLE_KEY?.trim() && core.projectRef);
6386
+ }
6397
6387
  return Boolean(core.apiUrl && core.apiToken && core.projectRef);
6398
6388
  }
6399
6389
  function namedEnvironmentSource(cwd, selector) {
@@ -6459,6 +6449,7 @@ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selecti
6459
6449
  production: source.environment === "prod" || source.environment === "production",
6460
6450
  inferredSupabaseUrl: core.supabaseUrl,
6461
6451
  inferredServiceRoleKey: source.values.SUPABASE_SERVICE_ROLE_KEY || "",
6452
+ credentialScope: core.credentialScope,
6462
6453
  source: source.kind,
6463
6454
  sourcePath: source.path
6464
6455
  };
@@ -6492,6 +6483,7 @@ var ACTION_POLICY = {
6492
6483
  write: ["deploy", "deploy_bundle", "config", "activate", "delete"]
6493
6484
  },
6494
6485
  scheduled_functions: { read: ["list", "get"], write: ["create", "update", "delete"] },
6486
+ mutations: { read: ["status"] },
6495
6487
  secrets: { read: ["list"], write: ["upsert", "delete"] },
6496
6488
  frontend: {
6497
6489
  read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
@@ -6576,6 +6568,8 @@ function validateExecutionPolicyCoverage(tools) {
6576
6568
 
6577
6569
  // src/shared/transports/http.ts
6578
6570
  var DEFAULT_TIMEOUT = 30000;
6571
+ var RELEASE_MUTATION_RESPONSE_TIMEOUT = 5000;
6572
+ var RELEASE_MUTATION_RESPONSE_MAX_BYTES = 64 * 1024;
6579
6573
  var MAX_RETRIES = 2;
6580
6574
  var RETRY_BASE_DELAY = 500;
6581
6575
  function isRetryableMethod(method) {
@@ -6598,13 +6592,22 @@ function transportFailure(error) {
6598
6592
  transportError: true
6599
6593
  };
6600
6594
  }
6595
+ function responseReadFailure(status) {
6596
+ return {
6597
+ ok: false,
6598
+ status,
6599
+ data: { error: "Response body unavailable", code: "RESPONSE_READ_ERROR" },
6600
+ responseReadError: true
6601
+ };
6602
+ }
6601
6603
  async function fetchWithTimeout(url, options) {
6602
6604
  const controller = new AbortController;
6603
6605
  const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
6604
6606
  try {
6605
6607
  return await fetch(url, {
6606
6608
  ...options,
6607
- signal: controller.signal
6609
+ signal: controller.signal,
6610
+ redirect: "error"
6608
6611
  });
6609
6612
  } finally {
6610
6613
  clearTimeout(timeout);
@@ -6647,41 +6650,114 @@ function joinedResponseBytes(chunks, totalBytes) {
6647
6650
  }
6648
6651
  return responseBytes;
6649
6652
  }
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();
6653
+ function cancelResponseReader(reader) {
6654
+ reader.cancel().catch(() => {
6655
+ return;
6656
+ });
6657
+ }
6658
+ function serializedRequestBody(body) {
6659
+ return body ? JSON.stringify(body) : undefined;
6660
+ }
6661
+ async function responseBytesFromReader(reader, maxBytes, declaredBytes) {
6658
6662
  const chunks = [];
6659
6663
  let totalBytes = 0;
6660
6664
  while (true) {
6661
6665
  const { done, value } = await reader.read();
6662
- if (done)
6663
- return joinedResponseBytes(chunks, totalBytes);
6666
+ if (done) {
6667
+ if (declaredBytes !== null && totalBytes !== declaredBytes)
6668
+ return { ok: false };
6669
+ return { ok: true, bytes: joinedResponseBytes(chunks, totalBytes) };
6670
+ }
6664
6671
  totalBytes += value.byteLength;
6665
6672
  if (totalBytes > maxBytes) {
6666
- await reader.cancel();
6667
- return null;
6673
+ cancelResponseReader(reader);
6674
+ return { ok: false };
6668
6675
  }
6669
6676
  chunks.push(value);
6670
6677
  }
6671
6678
  }
6679
+ async function responseBytesWithinLimit(response, maxBytes) {
6680
+ if (declaredResponseTooLarge(response, maxBytes)) {
6681
+ await response.body?.cancel();
6682
+ return null;
6683
+ }
6684
+ if (!response.body)
6685
+ return new Uint8Array;
6686
+ const reader = response.body.getReader();
6687
+ const bodyRead = await responseBytesFromReader(reader, maxBytes, null);
6688
+ return bodyRead.ok ? bodyRead.bytes : null;
6689
+ }
6672
6690
  function parsedUtf8Json(responseBytes) {
6673
6691
  try {
6674
6692
  const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
6675
- return JSON.parse(responseText);
6693
+ return { ok: true, parsedJson: JSON.parse(responseText) };
6676
6694
  } catch (error) {
6677
6695
  if (error instanceof SyntaxError || error instanceof TypeError)
6678
- return null;
6696
+ return { ok: false };
6679
6697
  throw error;
6680
6698
  }
6681
6699
  }
6682
6700
  async function boundedResponseJson(response, maxBytes) {
6683
6701
  const responseBytes = await responseBytesWithinLimit(response, maxBytes);
6684
- return responseBytes === null ? null : parsedUtf8Json(responseBytes);
6702
+ if (responseBytes === null)
6703
+ return null;
6704
+ const parsed = parsedUtf8Json(responseBytes);
6705
+ return parsed.ok ? parsed.parsedJson : null;
6706
+ }
6707
+ function declaredIdentityResponseBytes(response) {
6708
+ const contentEncoding = response.headers.get("content-encoding");
6709
+ if (contentEncoding !== null && contentEncoding.toLowerCase() !== "identity")
6710
+ return null;
6711
+ const contentLength = response.headers.get("content-length");
6712
+ if (contentLength === null)
6713
+ return null;
6714
+ if (!/^\d+$/.test(contentLength))
6715
+ return "invalid";
6716
+ const declaredBytes = Number(contentLength);
6717
+ return Number.isSafeInteger(declaredBytes) ? declaredBytes : "invalid";
6718
+ }
6719
+ async function releaseMutationResponseBytes(response) {
6720
+ const declaredBytes = declaredIdentityResponseBytes(response);
6721
+ if (declaredBytes === "invalid" || declaredBytes !== null && declaredBytes > RELEASE_MUTATION_RESPONSE_MAX_BYTES) {
6722
+ response.body?.cancel().catch(() => {
6723
+ return;
6724
+ });
6725
+ return { ok: false };
6726
+ }
6727
+ if (!response.body) {
6728
+ return declaredBytes === null || declaredBytes === 0 ? { ok: true, bytes: new Uint8Array } : { ok: false };
6729
+ }
6730
+ return responseBytesBeforeDeadline(response.body.getReader(), declaredBytes);
6731
+ }
6732
+ async function responseBytesBeforeDeadline(reader, declaredBytes) {
6733
+ let deadlineTimer;
6734
+ const deadline = new Promise((resolve2) => {
6735
+ deadlineTimer = setTimeout(() => {
6736
+ cancelResponseReader(reader);
6737
+ resolve2({ ok: false });
6738
+ }, RELEASE_MUTATION_RESPONSE_TIMEOUT);
6739
+ });
6740
+ try {
6741
+ return await Promise.race([
6742
+ responseBytesFromReader(reader, RELEASE_MUTATION_RESPONSE_MAX_BYTES, declaredBytes),
6743
+ deadline
6744
+ ]);
6745
+ } catch {
6746
+ cancelResponseReader(reader);
6747
+ return { ok: false };
6748
+ } finally {
6749
+ clearTimeout(deadlineTimer);
6750
+ }
6751
+ }
6752
+ async function releaseMutationResponseJson(response) {
6753
+ const responseBytes = await releaseMutationResponseBytes(response);
6754
+ if (!responseBytes.ok)
6755
+ return { ok: false };
6756
+ const parsed = parsedUtf8Json(responseBytes.bytes);
6757
+ return parsed.ok ? { ok: true, parsedJson: parsed.parsedJson } : { ok: false };
6758
+ }
6759
+ async function responseJsonOrNull(response) {
6760
+ return { ok: true, parsedJson: await response.json().catch(() => null) };
6685
6761
  }
6686
6762
 
6687
6763
  class HttpTransport {
@@ -6697,6 +6773,19 @@ class HttpTransport {
6697
6773
  "Content-Type": "application/json"
6698
6774
  };
6699
6775
  }
6776
+ async postWithResponseReader(path, serializedBody, responseReader) {
6777
+ try {
6778
+ const response = await fetchWithRetry(`${this.baseUrl}${path}`, {
6779
+ method: "POST",
6780
+ headers: this.headers(),
6781
+ body: serializedBody
6782
+ });
6783
+ const responseBody = await responseReader(response);
6784
+ return responseBody.ok ? { ok: response.ok, status: response.status, data: responseBody.parsedJson } : responseReadFailure(response.status);
6785
+ } catch (error) {
6786
+ return transportFailure(error);
6787
+ }
6788
+ }
6700
6789
  async get(path, options = {}) {
6701
6790
  try {
6702
6791
  const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
@@ -6711,17 +6800,14 @@ class HttpTransport {
6711
6800
  }
6712
6801
  async post(path, body) {
6713
6802
  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 };
6803
+ return await this.postWithResponseReader(path, serializedRequestBody(body), responseJsonOrNull);
6721
6804
  } catch (error) {
6722
6805
  return transportFailure(error);
6723
6806
  }
6724
6807
  }
6808
+ async postReleaseMutation(path, body) {
6809
+ return this.postWithResponseReader(path, serializedRequestBody(body), releaseMutationResponseJson);
6810
+ }
6725
6811
  async postMultipart(path, formData) {
6726
6812
  try {
6727
6813
  const headers = { Authorization: `Bearer ${this.token}` };
@@ -7886,7 +7972,7 @@ function releaseControlFailure(operation, code, httpStatus) {
7886
7972
  });
7887
7973
  }
7888
7974
  function releaseControlMutationFailure(operation, response) {
7889
- const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
7975
+ const outcomeUnknown = response.transportError || response.responseReadError || response.status === 408 || response.status >= 500;
7890
7976
  return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
7891
7977
  }
7892
7978
  function releaseControlResponse(payload) {
@@ -8523,6 +8609,17 @@ function positiveFunctionVersion(input, label) {
8523
8609
  }
8524
8610
  return version;
8525
8611
  }
8612
+ function activeFunctionVersionToken(input) {
8613
+ if (typeof input !== "string" && typeof input !== "number") {
8614
+ throw new Error("Expected active version must be a canonical non-negative safe integer");
8615
+ }
8616
+ const version = String(input);
8617
+ if (!CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
8618
+ throw new Error("Expected active version must be a canonical non-negative safe integer");
8619
+ }
8620
+ return version;
8621
+ }
8622
+ var CANONICAL_FUNCTION_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)$/;
8526
8623
  var POSITIVE_FUNCTION_VERSION_PATTERN = /^[1-9][0-9]*$/;
8527
8624
  var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
8528
8625
  var FUNCTION_ACTIVATION_ARGUMENTS = new Set([
@@ -8539,15 +8636,15 @@ var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
8539
8636
  function parseExpectedActiveVersion(input) {
8540
8637
  if (input === "absent")
8541
8638
  return input;
8542
- return positiveFunctionVersion(input, "Expected active version");
8639
+ return activeFunctionVersionToken(input);
8543
8640
  }
8544
8641
  var expectedActiveVersionSchema = Type.Optional(decodedSchema(Type.Union([
8545
8642
  Type.Literal("absent"),
8546
- Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
8547
- Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8643
+ Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
8644
+ Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8548
8645
  ]), Type.Union([
8549
8646
  Type.Literal("absent"),
8550
- Type.String({ pattern: POSITIVE_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8647
+ Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
8551
8648
  ]), parseExpectedActiveVersion));
8552
8649
  var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
8553
8650
  var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
@@ -8679,7 +8776,7 @@ function safeFunctionList(payload) {
8679
8776
  const edgeFunction = objectRecord(candidate);
8680
8777
  const slug = edgeFunction?.slug;
8681
8778
  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))
8779
+ if (typeof slug !== "string" || !SAFE_FUNCTION_SLUG_PATTERN.test(slug) || typeof version !== "number" || !Number.isSafeInteger(version) || version < 0 || functionSlugs.has(slug))
8683
8780
  return null;
8684
8781
  functionSlugs.add(slug);
8685
8782
  }
@@ -8828,7 +8925,7 @@ async function activateFunctionVersion(http, args, readOnly = false) {
8828
8925
  slug: functionSlug,
8829
8926
  expectedActiveVersion,
8830
8927
  targetVersion: version
8831
- }, await http.post(endpoint, { expected_active_version: expectedActiveVersion }));
8928
+ }, await http.postReleaseMutation(endpoint, { expected_active_version: expectedActiveVersion }));
8832
8929
  }
8833
8930
  function registerAdvancedTools(server, http, environment = process.env, options = {}) {
8834
8931
  server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Source deploys are bundled; verified prebuilt artifacts stay byte-exact.
@@ -8920,7 +9017,7 @@ ${deployCheck.err}`;
8920
9017
  break;
8921
9018
  }
8922
9019
  }
8923
- const deploymentResponse = await http.post(edgeFunctionResourcePath(ref, slug), {
9020
+ const deploymentResponse = await http.postReleaseMutation(edgeFunctionResourcePath(ref, slug), {
8924
9021
  code: deployCode.code,
8925
9022
  ...deployCode.prebundled ? { prebundled: true, expected_sha256: deployCode.expectedSha256 } : { minify },
8926
9023
  expected_active_version: expectedActiveVersion,
@@ -8936,7 +9033,7 @@ ${deployCheck.err}`;
8936
9033
  case "deploy_bundle":
8937
9034
  need("slug", slug);
8938
9035
  need("files", files);
8939
- const bundleResponse = await http.post(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
9036
+ const bundleResponse = await http.postReleaseMutation(`${edgeFunctionResourcePath(ref, slug)}/bundle`, {
8940
9037
  files,
8941
9038
  entrypoint,
8942
9039
  minify,
@@ -10551,10 +10648,10 @@ function missingMigrationContextResult() {
10551
10648
  content: [{
10552
10649
  type: "text",
10553
10650
  text: [
10554
- "⚠️ Remote migration push requires SupaCloud project context.",
10555
- "Provide SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY, or SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN.",
10651
+ "⚠️ Remote migration push requires SupaCloud Management API context.",
10652
+ "Provide SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN.",
10556
10653
  "Also pass --ref or set SUPACLOUD_PROJECT_REF when the project ref cannot be inferred from the URL.",
10557
- "The service-role credential is sent only to the SupaCloud Management API and is never forwarded to the official CLI."
10654
+ "The Management token is sent only to the SupaCloud Management API and is never forwarded to the official CLI."
10558
10655
  ].join(`
10559
10656
  `)
10560
10657
  }]
@@ -10641,7 +10738,7 @@ function registerSupabaseCliTools(server, options = {}) {
10641
10738
  readOnly: options.readOnly ?? false,
10642
10739
  executeOfficialCli: options.executeOfficialCli || ((request) => executeOfficialSupabaseCli(request, environment))
10643
10740
  };
10644
- server.tool("supabase", "Controlled adapter for the official open-source Supabase CLI. Remote push stays on the SupaCloud service-role authenticated Management API.", {
10741
+ server.tool("supabase", "Controlled adapter for the official open-source Supabase CLI. Remote push stays on the SupaCloud Management API and requires explicit Management credentials.", {
10645
10742
  action: withDescription(stringEnum([
10646
10743
  "version",
10647
10744
  "migration_new",
@@ -11264,10 +11361,216 @@ var SCHEDULE_TOOL_SCHEMA = {
11264
11361
  body_file: optional(Type.String(), "[create/update] Local JSON object file; content is never printed"),
11265
11362
  header_env: withDescription(headerEnvironmentSchema, "[create/update] JSON map of HTTP header names to environment variable names")
11266
11363
  };
11364
+
11365
+ // src/shared/mutation-protocol.ts
11366
+ var MUTATION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
11367
+ var FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
11368
+ var OPERATION_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
11369
+ var RESOURCE_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
11370
+ var FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
11371
+ var LEASE_OWNER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,254}$/;
11372
+ var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
11373
+ var MAX_STATUS_RESPONSE_BYTES = 196608;
11374
+ var MUTATION_STATUSES = new Set([
11375
+ "pending",
11376
+ "running",
11377
+ "succeeded",
11378
+ "failed_retryable",
11379
+ "failed_terminal",
11380
+ "outcome_unknown"
11381
+ ]);
11382
+ var MUTATION_RESPONSE_KEYS = ["project_ref", "mutation"];
11383
+ var MUTATION_KEYS = [
11384
+ "project_ref",
11385
+ "mutation_id",
11386
+ "operation",
11387
+ "resource_key",
11388
+ "request_fingerprint",
11389
+ "principal",
11390
+ "status",
11391
+ "checkpoint",
11392
+ "receipt",
11393
+ "response_status",
11394
+ "failure_code",
11395
+ "lease",
11396
+ "completed_at",
11397
+ "created_at",
11398
+ "updated_at"
11399
+ ];
11400
+ var PRINCIPAL_KEYS = ["type", "id"];
11401
+ var LEASE_KEYS = ["owner", "expires_at", "fencing_epoch"];
11402
+ function isMutationId(candidate) {
11403
+ return typeof candidate === "string" && MUTATION_ID_PATTERN.test(candidate);
11404
+ }
11405
+ function objectRecord3(candidate) {
11406
+ return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
11407
+ }
11408
+ function exactRecord(candidate, keys) {
11409
+ const record = objectRecord3(candidate);
11410
+ if (!record || Object.keys(record).length !== keys.length)
11411
+ return null;
11412
+ return keys.every((key) => Object.hasOwn(record, key)) ? record : null;
11413
+ }
11414
+ function emptyProjection(candidate) {
11415
+ const record = objectRecord3(candidate);
11416
+ return record && Object.keys(record).length === 0 ? record : null;
11417
+ }
11418
+ function canonicalTimestamp(candidate) {
11419
+ if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
11420
+ return false;
11421
+ const milliseconds = Date.parse(candidate);
11422
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
11423
+ }
11424
+ function nullableTimestamp(candidate) {
11425
+ return candidate === null || canonicalTimestamp(candidate);
11426
+ }
11427
+ function safePrincipal(candidate) {
11428
+ const principal = exactRecord(candidate, PRINCIPAL_KEYS);
11429
+ if (!principal || !["master", "admin", "project"].includes(String(principal.type)))
11430
+ return null;
11431
+ if (typeof principal.id !== "string" || !principal.id || principal.id.length > 320 || principal.id.trim() !== principal.id || /[\u0000-\u001f\u007f]/u.test(principal.id))
11432
+ return null;
11433
+ return { type: principal.type, id: principal.id };
11434
+ }
11435
+ function safeLease(candidate) {
11436
+ const lease = exactRecord(candidate, LEASE_KEYS);
11437
+ if (!lease || lease.owner !== null && (typeof lease.owner !== "string" || !LEASE_OWNER_PATTERN.test(lease.owner)))
11438
+ return null;
11439
+ if (!nullableTimestamp(lease.expires_at) || !Number.isSafeInteger(lease.fencing_epoch) || Number(lease.fencing_epoch) < 0)
11440
+ return null;
11441
+ return {
11442
+ owner: lease.owner,
11443
+ expires_at: lease.expires_at,
11444
+ fencing_epoch: Number(lease.fencing_epoch)
11445
+ };
11446
+ }
11447
+ function safeResponseStatus(candidate) {
11448
+ if (candidate === null)
11449
+ return null;
11450
+ return Number.isInteger(candidate) && Number(candidate) >= 100 && Number(candidate) <= 599 ? Number(candidate) : undefined;
11451
+ }
11452
+ function validLeaseState(status, lease) {
11453
+ const running = status === "running";
11454
+ return running ? lease.owner !== null && lease.expires_at !== null && lease.fencing_epoch > 0 : lease.owner === null && lease.expires_at === null;
11455
+ }
11456
+ function validMutationLifecycle(mutation, receipt, responseStatus) {
11457
+ const terminal = ["succeeded", "failed_terminal", "outcome_unknown"].includes(String(mutation.status));
11458
+ if (mutation.completed_at !== null !== terminal)
11459
+ return false;
11460
+ if (mutation.status === "succeeded") {
11461
+ return receipt !== null && responseStatus !== null && responseStatus >= 200 && responseStatus < 300 && mutation.failure_code === null;
11462
+ }
11463
+ if (mutation.status === "failed_terminal") {
11464
+ return receipt !== null && typeof mutation.failure_code === "string";
11465
+ }
11466
+ if (mutation.status === "failed_retryable" || mutation.status === "outcome_unknown") {
11467
+ return receipt !== null && typeof mutation.failure_code === "string";
11468
+ }
11469
+ if (mutation.status === "pending") {
11470
+ return receipt === null && responseStatus === null && mutation.failure_code === null;
11471
+ }
11472
+ return true;
11473
+ }
11474
+ function validMutationIdentity(mutation) {
11475
+ if (!isMutationId(mutation.mutation_id) || typeof mutation.project_ref !== "string")
11476
+ return false;
11477
+ if (typeof mutation.operation !== "string" || !OPERATION_PATTERN.test(mutation.operation))
11478
+ return false;
11479
+ if (mutation.resource_key !== null && (typeof mutation.resource_key !== "string" || !RESOURCE_KEY_PATTERN.test(mutation.resource_key)))
11480
+ return false;
11481
+ return typeof mutation.request_fingerprint === "string" && FINGERPRINT_PATTERN.test(mutation.request_fingerprint);
11482
+ }
11483
+ function validMutationTerminalFields(mutation) {
11484
+ if (typeof mutation.status !== "string" || !MUTATION_STATUSES.has(mutation.status))
11485
+ return false;
11486
+ if (mutation.failure_code !== null && (typeof mutation.failure_code !== "string" || !FAILURE_CODE_PATTERN.test(mutation.failure_code)))
11487
+ return false;
11488
+ return nullableTimestamp(mutation.completed_at) && canonicalTimestamp(mutation.created_at) && canonicalTimestamp(mutation.updated_at);
11489
+ }
11490
+ function safeMutationStatus(candidate) {
11491
+ const mutation = exactRecord(candidate, MUTATION_KEYS);
11492
+ if (!mutation || !validMutationIdentity(mutation) || !validMutationTerminalFields(mutation))
11493
+ return null;
11494
+ const principal = safePrincipal(mutation.principal);
11495
+ const checkpoint = emptyProjection(mutation.checkpoint);
11496
+ const receipt = mutation.receipt === null ? null : emptyProjection(mutation.receipt);
11497
+ const responseStatus = safeResponseStatus(mutation.response_status);
11498
+ const lease = safeLease(mutation.lease);
11499
+ if (!principal || !checkpoint || mutation.receipt !== null && !receipt || responseStatus === undefined || !lease || !validLeaseState(String(mutation.status), lease) || !validMutationLifecycle(mutation, receipt, responseStatus))
11500
+ return null;
11501
+ return {
11502
+ project_ref: mutation.project_ref,
11503
+ mutation_id: mutation.mutation_id,
11504
+ operation: mutation.operation,
11505
+ resource_key: mutation.resource_key,
11506
+ request_fingerprint: mutation.request_fingerprint,
11507
+ principal,
11508
+ status: mutation.status,
11509
+ checkpoint,
11510
+ receipt,
11511
+ response_status: responseStatus,
11512
+ failure_code: mutation.failure_code,
11513
+ lease,
11514
+ completed_at: mutation.completed_at,
11515
+ created_at: mutation.created_at,
11516
+ updated_at: mutation.updated_at
11517
+ };
11518
+ }
11519
+ function mutationStatusPath(ref, mutationId) {
11520
+ if (!isMutationId(mutationId))
11521
+ throw new Error("'mutation_id' must be a UUIDv4");
11522
+ return `/v1/projects/${projectRefPathSegment(ref, "Mutations")}/mutations/${encodeURIComponent(mutationId)}`;
11523
+ }
11524
+ function mutationStatusResponse(ref, mutationId, payload) {
11525
+ const response = exactRecord(payload, MUTATION_RESPONSE_KEYS);
11526
+ const mutation = safeMutationStatus(response?.mutation);
11527
+ return response?.project_ref === ref && mutation?.project_ref === ref && mutation.mutation_id === mutationId ? mutation : null;
11528
+ }
11529
+ async function fetchMutationStatus(http, ref, mutationId) {
11530
+ const response = await http.get(mutationStatusPath(ref, mutationId), {
11531
+ maxResponseBytes: MAX_STATUS_RESPONSE_BYTES
11532
+ });
11533
+ if (!response.ok) {
11534
+ return { kind: "unavailable", httpStatus: response.transportError ? null : response.status };
11535
+ }
11536
+ const mutation = mutationStatusResponse(ref, mutationId, response.data);
11537
+ return mutation ? { kind: "available", mutation } : { kind: "invalid" };
11538
+ }
11539
+
11540
+ // src/shared/tools/mutation-tools.ts
11541
+ function requiredText3(args, name) {
11542
+ const candidate = args[name];
11543
+ if (typeof candidate !== "string" || !candidate.trim())
11544
+ throw new Error(`'${name}' is required for 'status'`);
11545
+ return candidate.trim();
11546
+ }
11547
+ async function mutationStatus(http, args) {
11548
+ const ref = requiredText3(args, "ref");
11549
+ projectRefPathSegment(ref, "Mutations");
11550
+ const mutationId = requiredText3(args, "mutation_id");
11551
+ if (!isMutationId(mutationId))
11552
+ throw new Error("'mutation_id' must be a UUIDv4");
11553
+ const readback = await fetchMutationStatus(http, ref, mutationId);
11554
+ if (readback.kind === "unavailable") {
11555
+ return releaseControlFailure("mutations.status", "HTTP_ERROR", readback.httpStatus);
11556
+ }
11557
+ if (readback.kind === "invalid") {
11558
+ return releaseControlFailure("mutations.status", "INVALID_RESPONSE", null);
11559
+ }
11560
+ return releaseControlSuccess("mutations.status", { project_ref: ref, mutation: readback.mutation });
11561
+ }
11562
+ function registerMutationTools(server, http) {
11563
+ server.tool("mutations", "Durable mutation status readback", MUTATION_TOOL_SCHEMA, (args) => mutationStatus(http, args));
11564
+ }
11565
+ var MUTATION_TOOL_SCHEMA = {
11566
+ action: withDescription(stringEnum(["status"]), "Action"),
11567
+ ref: withDescription(Type.String(), "[status] Project ref"),
11568
+ mutation_id: withDescription(Type.String(), "[status] Client mutation UUID")
11569
+ };
11267
11570
  // package.json
11268
11571
  var package_default = {
11269
11572
  name: "@supacloud/cli",
11270
- version: "0.17.0",
11573
+ version: "0.19.0",
11271
11574
  description: "Project-scoped CLI for SupaCloud users",
11272
11575
  type: "module",
11273
11576
  main: "./dist/index.js",
@@ -11332,13 +11635,14 @@ function failedEndpointProbe(error) {
11332
11635
  const timedOut = error instanceof Error && error.name === "AbortError";
11333
11636
  return { reachable: false, ok: false, httpStatus: null, error: timedOut ? "timeout" : "unreachable" };
11334
11637
  }
11335
- async function probeEndpoint(url, token) {
11638
+ async function probeEndpoint(url, headers) {
11336
11639
  const controller = new AbortController;
11337
11640
  const timeout = setTimeout(() => controller.abort(), 3000);
11338
11641
  try {
11339
11642
  const response = await fetch(url, {
11340
11643
  method: "GET",
11341
- headers: token ? { Authorization: `Bearer ${token}` } : undefined,
11644
+ headers,
11645
+ redirect: "error",
11342
11646
  signal: controller.signal
11343
11647
  });
11344
11648
  return successfulEndpointProbe(response);
@@ -11349,6 +11653,13 @@ async function probeEndpoint(url, token) {
11349
11653
  }
11350
11654
  }
11351
11655
  function missingProjectContextFields(context) {
11656
+ if (context.credentialScope === "project_application") {
11657
+ return [
11658
+ !context.inferredSupabaseUrl ? "secureSupabaseUrl" : null,
11659
+ !context.inferredServiceRoleKey ? "serviceRoleKey" : null,
11660
+ !context.projectRef ? "projectRef" : null
11661
+ ].filter((field) => Boolean(field));
11662
+ }
11352
11663
  return [
11353
11664
  !context.apiUrl ? "apiUrl" : null,
11354
11665
  !context.apiToken ? "apiToken" : null,
@@ -11358,16 +11669,42 @@ function missingProjectContextFields(context) {
11358
11669
  function authenticatedByProbe(authentication) {
11359
11670
  if (!authentication)
11360
11671
  return null;
11361
- return authentication.reachable && ![401, 403].includes(authentication.httpStatus ?? 0);
11672
+ return authentication.reachable && authentication.ok;
11362
11673
  }
11363
- async function collectProjectStatusChecks(context) {
11364
- const missing = missingProjectContextFields(context);
11365
- const connectivity = context.apiUrl ? await probeEndpoint(`${context.apiUrl}/health`) : null;
11366
- const authentication = missing.length === 0 && connectivity?.ok ? await probeEndpoint(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.projectRef)}/health`, context.apiToken) : null;
11674
+ function connectivityProbeIsHealthy(scope, connectivity) {
11675
+ if (!connectivity)
11676
+ return null;
11677
+ if (scope !== "project_application")
11678
+ return connectivity.ok;
11679
+ return connectivity.reachable && (connectivity.ok || [401, 403].includes(connectivity.httpStatus ?? 0));
11680
+ }
11681
+ function projectApplicationHeaders(serviceRoleKey) {
11682
+ return { Authorization: `Bearer ${serviceRoleKey}`, apikey: serviceRoleKey };
11683
+ }
11684
+ function projectStatusApiUrl(context) {
11685
+ return context.credentialScope === "project_application" ? context.inferredSupabaseUrl : context.apiUrl;
11686
+ }
11687
+ function projectStatusProbePlan(context) {
11688
+ if (context.credentialScope === "project_application") {
11689
+ return {
11690
+ apiUrl: projectStatusApiUrl(context),
11691
+ connectivityPath: "/rest/v1/",
11692
+ authenticationPath: "/rest/v1/",
11693
+ authenticationHeaders: projectApplicationHeaders(context.inferredServiceRoleKey)
11694
+ };
11695
+ }
11696
+ return {
11697
+ apiUrl: projectStatusApiUrl(context),
11698
+ connectivityPath: "/health",
11699
+ authenticationPath: `/v1/projects/${encodeURIComponent(context.projectRef)}/health`,
11700
+ authenticationHeaders: { Authorization: `Bearer ${context.apiToken}` }
11701
+ };
11702
+ }
11703
+ function projectStatusChecks(missing, connectivity, authentication, connectivityOk) {
11367
11704
  return {
11368
11705
  configuration: { ok: missing.length === 0, missing },
11369
11706
  connectivity: {
11370
- ok: connectivity?.ok ?? null,
11707
+ ok: connectivityOk,
11371
11708
  reachable: connectivity?.reachable ?? null,
11372
11709
  httpStatus: connectivity?.httpStatus ?? null,
11373
11710
  error: connectivity?.error ?? null
@@ -11376,21 +11713,31 @@ async function collectProjectStatusChecks(context) {
11376
11713
  project: { ok: authentication?.ok ?? null }
11377
11714
  };
11378
11715
  }
11716
+ async function collectProjectStatusChecks(context) {
11717
+ const missing = missingProjectContextFields(context);
11718
+ const probePlan = projectStatusProbePlan(context);
11719
+ const connectivity = probePlan.apiUrl ? await probeEndpoint(`${probePlan.apiUrl}${probePlan.connectivityPath}`) : null;
11720
+ const connectivityOk = connectivityProbeIsHealthy(context.credentialScope, connectivity);
11721
+ const authentication = missing.length === 0 && connectivityOk ? await probeEndpoint(`${probePlan.apiUrl}${probePlan.authenticationPath}`, probePlan.authenticationHeaders) : null;
11722
+ return projectStatusChecks(missing, connectivity, authentication, connectivityOk);
11723
+ }
11379
11724
  function projectStatusIsHealthy(checks) {
11380
11725
  return checks.configuration.ok && checks.connectivity.ok === true && checks.authentication.ok === true && checks.project.ok === true;
11381
11726
  }
11382
11727
  async function createProjectStatusResult(context) {
11383
11728
  const checks = await collectProjectStatusChecks(context);
11729
+ const statusApiUrl = projectStatusApiUrl(context);
11384
11730
  const statusPayload = {
11385
11731
  mode: "project",
11732
+ credentialScope: context.credentialScope,
11386
11733
  environment: context.environment || null,
11387
11734
  source: { kind: context.source, path: context.sourcePath },
11388
11735
  projectRef: context.projectRef || null,
11389
- apiUrl: context.apiUrl || null,
11736
+ apiUrl: statusApiUrl || null,
11390
11737
  readOnly: context.readOnly,
11391
11738
  production: context.production,
11392
11739
  autoLinked: Boolean(context.inferredSupabaseUrl && context.inferredServiceRoleKey),
11393
- hasApiToken: Boolean(context.apiToken),
11740
+ hasApiToken: context.credentialScope === "project_application" ? Boolean(context.inferredServiceRoleKey) : Boolean(context.apiToken),
11394
11741
  checks
11395
11742
  };
11396
11743
  return {
@@ -11448,15 +11795,15 @@ GLOBAL FLAGS
11448
11795
  DEFAULT CONTEXT
11449
11796
 
11450
11797
  Without a selector or project variables, runs use the current project's legacy .env.
11451
- Supported auto-link variables:
11452
- SUPABASE_URL / SUPACLOUD_API_URL
11453
- SUPABASE_SERVICE_ROLE_KEY / SUPACLOUD_API_TOKEN
11454
- SUPACLOUD_PROJECT_REF (when it cannot be inferred from <ref>.api.*)
11798
+ Application status accepts SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY.
11799
+ Management-backed project commands require SUPACLOUD_API_URL +
11800
+ SUPACLOUD_API_TOKEN. These credential scopes are never mixed.
11801
+ SUPACLOUD_PROJECT_REF is required when it cannot be inferred from <ref>.api.*.
11455
11802
 
11456
11803
  SUPACLOUD_READ_ONLY=true blocks remote writes. Production writes require an
11457
11804
  exact --confirm-production value, and cannot override the selected project ref.
11458
11805
 
11459
- status checks configuration, Management API connectivity, and authentication.
11806
+ status checks configuration, the selected API scope, connectivity, and authentication.
11460
11807
  It exits non-zero when a required check fails.
11461
11808
 
11462
11809
  ${autoLink}
@@ -11487,6 +11834,7 @@ EXAMPLES
11487
11834
  ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
11488
11835
  ${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4
11489
11836
  ${preferredCommand} scheduled_functions list --ref abc123
11837
+ ${preferredCommand} mutations status --ref abc123 --mutation_id 00000000-0000-4000-8000-000000000001
11490
11838
  ${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
11491
11839
  ${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
11492
11840
  ${preferredCommand} gateway routes --ref abc123
@@ -11536,13 +11884,16 @@ function createCliTools(context, confirmProduction) {
11536
11884
  {
11537
11885
  type: "text",
11538
11886
  text: [
11539
- "⚠️ Project commands need a project-scoped API context.",
11887
+ "⚠️ Project commands need a Management API context.",
11540
11888
  "",
11541
11889
  "Provide one of these sources:",
11542
11890
  " - --env <name> for .env.supacloud.<name>",
11543
11891
  " - --env-file <path> for a file declaring SUPACLOUD_ENV",
11544
- " - .env with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY",
11545
11892
  " - SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN",
11893
+ " - SUPACLOUD_PROJECT_REF when the profile cannot infer it",
11894
+ "",
11895
+ "SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY application profiles",
11896
+ "are accepted only by status and local commands.",
11546
11897
  "",
11547
11898
  "Then retry commands such as:",
11548
11899
  ` ${preferredCommand} project get`,
@@ -11553,7 +11904,7 @@ function createCliTools(context, confirmProduction) {
11553
11904
  ]
11554
11905
  })
11555
11906
  };
11556
- for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "diagnostics", "gateway", "branch"]) {
11907
+ for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch"]) {
11557
11908
  tools[name] = {
11558
11909
  schema: { action: genericActionSchema },
11559
11910
  callback: async () => ({
@@ -11561,7 +11912,7 @@ function createCliTools(context, confirmProduction) {
11561
11912
  content: [
11562
11913
  {
11563
11914
  type: "text",
11564
- text: `⚠️ This command requires project-scoped API context. Run \`${preferredCommand} status\` to inspect current detection.`
11915
+ text: `⚠️ This command requires Management API context. Run \`${preferredCommand} status\` to inspect current detection.`
11565
11916
  }
11566
11917
  ]
11567
11918
  })
@@ -11580,7 +11931,7 @@ function createCliTools(context, confirmProduction) {
11580
11931
  tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
11581
11932
  }
11582
11933
  };
11583
- if (!context.apiUrl || !context.apiToken) {
11934
+ if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
11584
11935
  registerContextAwareHelp();
11585
11936
  tools.setup_help = {
11586
11937
  schema: {},
@@ -11592,19 +11943,18 @@ function createCliTools(context, confirmProduction) {
11592
11943
  text: [
11593
11944
  `⚠️ No project context found for ${preferredCommand}.`,
11594
11945
  "",
11595
- `${preferredCommand} expects project-scoped credentials by default.`,
11946
+ `${preferredCommand} remote tools require Management API credentials.`,
11596
11947
  "Provide one of these sources:",
11597
11948
  "",
11598
11949
  " 1. Named environment file",
11599
11950
  " supacloud-cli --env test status",
11600
11951
  "",
11601
- " 2. Current workspace .env",
11602
- " SUPABASE_URL=https://your-project.example.com",
11603
- " SUPABASE_SERVICE_ROLE_KEY=...",
11604
- "",
11605
- " 3. Explicit environment variables",
11952
+ " 2. Explicit environment variables",
11606
11953
  " SUPACLOUD_API_URL=https://your-project.example.com",
11607
11954
  " SUPACLOUD_API_TOKEN=...",
11955
+ " SUPACLOUD_PROJECT_REF=your-project-ref",
11956
+ "",
11957
+ "Application SUPABASE_* profiles remain available to status and local commands.",
11608
11958
  "",
11609
11959
  "For server installation and tenant management, use:",
11610
11960
  " supacloud-admin"
@@ -11638,6 +11988,7 @@ function createCliTools(context, confirmProduction) {
11638
11988
  assign(captureTools((server) => registerScheduledFunctionTools(server, http, process.env, {
11639
11989
  readOnly: context.readOnly
11640
11990
  })));
11991
+ assign(captureTools((server) => registerMutationTools(server, http)));
11641
11992
  assign(captureTools((server) => registerFrontendTools(server, http)));
11642
11993
  assign(captureTools((server) => registerGatewayTools(server, http, {
11643
11994
  projectRef: context.projectRef || undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.17.0",
3
+ "version": "0.19.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`: 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.
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.