@supacloud/cli 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +24 -18
  2. package/dist/index.js +568 -77
  3. package/package.json +1 -1
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
 
@@ -325,10 +331,10 @@ supacloud-cli supabase push --ref abc123 --dir supabase/migrations --dry_run
325
331
  supacloud-cli supabase push --ref abc123 --dir supabase/migrations
326
332
  ```
327
333
 
328
- `push` uses `SUPABASE_SERVICE_ROLE_KEY` or `SUPACLOUD_API_TOKEN` only for the
329
- SupaCloud Management API. Those credentials, upstream access tokens, database
330
- passwords, and secret/key environment variables are removed from the official
331
- 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.
332
338
 
333
339
  `push` requires a resolved project ref; pass `--ref` explicitly or set
334
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"],
@@ -6580,6 +6572,15 @@ var RELEASE_MUTATION_RESPONSE_TIMEOUT = 5000;
6580
6572
  var RELEASE_MUTATION_RESPONSE_MAX_BYTES = 64 * 1024;
6581
6573
  var MAX_RETRIES = 2;
6582
6574
  var RETRY_BASE_DELAY = 500;
6575
+ function validatedGetResponseLimit(options) {
6576
+ const maxBytes = options.maxResponseBytes;
6577
+ if (maxBytes === undefined)
6578
+ return;
6579
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
6580
+ throw new RangeError("HTTP response limit must be a positive safe integer");
6581
+ }
6582
+ return maxBytes;
6583
+ }
6583
6584
  function isRetryableMethod(method) {
6584
6585
  const normalizedMethod = (method ?? "GET").toUpperCase();
6585
6586
  return normalizedMethod === "GET" || normalizedMethod === "HEAD";
@@ -6614,7 +6615,8 @@ async function fetchWithTimeout(url, options) {
6614
6615
  try {
6615
6616
  return await fetch(url, {
6616
6617
  ...options,
6617
- signal: controller.signal
6618
+ signal: controller.signal,
6619
+ redirect: "error"
6618
6620
  });
6619
6621
  } finally {
6620
6622
  clearTimeout(timeout);
@@ -6794,12 +6796,13 @@ class HttpTransport {
6794
6796
  }
6795
6797
  }
6796
6798
  async get(path, options = {}) {
6799
+ const maxResponseBytes = validatedGetResponseLimit(options);
6797
6800
  try {
6798
6801
  const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
6799
6802
  method: "GET",
6800
6803
  headers: this.headers()
6801
6804
  });
6802
- const data = options.maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, options.maxResponseBytes);
6805
+ const data = maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, maxResponseBytes);
6803
6806
  return { ok: res.ok, status: res.status, data };
6804
6807
  } catch (error) {
6805
6808
  return transportFailure(error);
@@ -7964,14 +7967,15 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
7964
7967
  var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
7965
7968
  function releaseControlSuccess(operation, payload) {
7966
7969
  return releaseControlResponse({
7970
+ ...payload,
7967
7971
  schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7968
7972
  ok: true,
7969
- operation,
7970
- ...payload
7973
+ operation
7971
7974
  });
7972
7975
  }
7973
- function releaseControlFailure(operation, code, httpStatus) {
7976
+ function releaseControlFailure(operation, code, httpStatus, safeState = {}) {
7974
7977
  return releaseControlErrorResponse({
7978
+ ...safeState,
7975
7979
  schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
7976
7980
  ok: false,
7977
7981
  operation,
@@ -9402,7 +9406,210 @@ Actions: list, get, create, update, delete, deploy_git, deploy_upload, redeploy,
9402
9406
  });
9403
9407
  }
9404
9408
 
9409
+ // src/shared/tools/project-read-projection.ts
9410
+ var PROJECT_READ_RESPONSE_MAX_BYTES = 1048576;
9411
+ var PROJECT_REF_PATTERN3 = /^[a-z0-9-]{1,20}$/;
9412
+ var SAFE_IDENTIFIER_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
9413
+ var REGION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
9414
+ var STATUS_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
9415
+ var DNS_LABEL_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/;
9416
+ var DATABASE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
9417
+ var PROJECT_SUMMARY_KEYS = new Set([
9418
+ "id",
9419
+ "ref",
9420
+ "organization_id",
9421
+ "organization_slug",
9422
+ "name",
9423
+ "region",
9424
+ "created_at",
9425
+ "status"
9426
+ ]);
9427
+ var PROJECT_DETAILS_KEYS = new Set([
9428
+ ...PROJECT_SUMMARY_KEYS,
9429
+ "database",
9430
+ "api",
9431
+ "studio",
9432
+ "config",
9433
+ "anon_key",
9434
+ "services"
9435
+ ]);
9436
+ var PROJECT_DATABASE_KEYS = new Set([
9437
+ "host",
9438
+ "version",
9439
+ "postgres_engine",
9440
+ "release_channel"
9441
+ ]);
9442
+ var PROJECT_ENDPOINT_KEYS = new Set(["url"]);
9443
+ function plainRecord(candidate) {
9444
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
9445
+ return null;
9446
+ const prototype = Object.getPrototypeOf(candidate);
9447
+ return prototype === Object.prototype || prototype === null ? candidate : null;
9448
+ }
9449
+ function hasOnlyKeys(record, allowedKeys) {
9450
+ return Object.keys(record).every((key) => allowedKeys.has(key));
9451
+ }
9452
+ function hasWellFormedUnicode(text) {
9453
+ for (let index = 0;index < text.length; index++) {
9454
+ const codeUnit = text.charCodeAt(index);
9455
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
9456
+ if (index + 1 >= text.length)
9457
+ return false;
9458
+ const lowSurrogate = text.charCodeAt(index + 1);
9459
+ if (lowSurrogate < 56320 || lowSurrogate > 57343)
9460
+ return false;
9461
+ index++;
9462
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
9463
+ return false;
9464
+ }
9465
+ }
9466
+ return true;
9467
+ }
9468
+ function boundedText(candidate, maxLength) {
9469
+ return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
9470
+ }
9471
+ function matchingText(candidate, maxLength, pattern) {
9472
+ const candidateText = boundedText(candidate, maxLength);
9473
+ return candidateText && pattern.test(candidateText) ? candidateText : null;
9474
+ }
9475
+ function canonicalTimestamp(candidate) {
9476
+ const timestamp = boundedText(candidate, 64);
9477
+ if (!timestamp)
9478
+ return null;
9479
+ const milliseconds = Date.parse(timestamp);
9480
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === timestamp ? timestamp : null;
9481
+ }
9482
+ function projectedSummary(project) {
9483
+ const summary = {
9484
+ id: matchingText(project.id, 128, SAFE_IDENTIFIER_PATTERN),
9485
+ ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN3),
9486
+ organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
9487
+ organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
9488
+ name: boundedText(project.name, 100),
9489
+ region: matchingText(project.region, 64, REGION_PATTERN),
9490
+ created_at: canonicalTimestamp(project.created_at),
9491
+ status: matchingText(project.status, 64, STATUS_PATTERN)
9492
+ };
9493
+ return Object.values(summary).every((field) => field !== null) ? summary : null;
9494
+ }
9495
+ function databaseHost(candidate) {
9496
+ const host = boundedText(candidate, 255);
9497
+ if (!host)
9498
+ return null;
9499
+ if (host.startsWith("[") && host.endsWith("]")) {
9500
+ try {
9501
+ const parsedHost = new URL(`http://${host}`);
9502
+ return parsedHost.host === host ? host : null;
9503
+ } catch (error) {
9504
+ if (error instanceof TypeError)
9505
+ return null;
9506
+ throw error;
9507
+ }
9508
+ }
9509
+ const ipv4Parts = host.split(".");
9510
+ if (ipv4Parts.length === 4 && ipv4Parts.every((part) => /^\d{1,3}$/u.test(part))) {
9511
+ return ipv4Parts.every((part) => Number(part) <= 255) ? host : null;
9512
+ }
9513
+ return ipv4Parts.every((label) => DNS_LABEL_PATTERN.test(label)) ? host : null;
9514
+ }
9515
+ function projectDatabase(candidate) {
9516
+ const database = plainRecord(candidate);
9517
+ if (!database || !hasOnlyKeys(database, PROJECT_DATABASE_KEYS))
9518
+ return null;
9519
+ const host = databaseHost(database.host);
9520
+ const version = matchingText(database.version, 64, DATABASE_VERSION_PATTERN);
9521
+ const postgresEngine = matchingText(database.postgres_engine, 64, DATABASE_VERSION_PATTERN);
9522
+ const releaseChannel = matchingText(database.release_channel, 64, DATABASE_VERSION_PATTERN);
9523
+ return host && version && postgresEngine && releaseChannel ? { host, version, postgres_engine: postgresEngine, release_channel: releaseChannel } : null;
9524
+ }
9525
+ function rawUrlHasNoPath(candidate) {
9526
+ if (candidate.trim() !== candidate || candidate.includes("\\"))
9527
+ return false;
9528
+ const schemeEnd = candidate.indexOf("://");
9529
+ const pathStart = candidate.indexOf("/", schemeEnd + 3);
9530
+ return pathStart === -1;
9531
+ }
9532
+ function projectEndpoint(candidate) {
9533
+ const endpoint = plainRecord(candidate);
9534
+ if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
9535
+ return null;
9536
+ const endpointUrl = boundedText(endpoint.url, 2048);
9537
+ if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
9538
+ return null;
9539
+ try {
9540
+ const url = new URL(endpointUrl);
9541
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.pathname !== "/")
9542
+ return null;
9543
+ return { url: url.origin };
9544
+ } catch (error) {
9545
+ if (error instanceof TypeError)
9546
+ return null;
9547
+ throw error;
9548
+ }
9549
+ }
9550
+ function discardedDetailFieldsAreValid(project) {
9551
+ if (project.config !== undefined && plainRecord(project.config) === null)
9552
+ return false;
9553
+ if (project.anon_key !== undefined && boundedText(project.anon_key, 16384) === null)
9554
+ return false;
9555
+ return project.services === undefined || Array.isArray(project.services);
9556
+ }
9557
+ function projectDetails(candidate, expectedRef) {
9558
+ const project = plainRecord(candidate);
9559
+ if (!project || !hasOnlyKeys(project, PROJECT_DETAILS_KEYS))
9560
+ return null;
9561
+ const summary = projectedSummary(project);
9562
+ const database = projectDatabase(project.database);
9563
+ const api = project.api === undefined ? undefined : projectEndpoint(project.api);
9564
+ const studio = project.studio === undefined ? undefined : projectEndpoint(project.studio);
9565
+ if (!summary || summary.ref !== expectedRef || !database || !discardedDetailFieldsAreValid(project) || project.api !== undefined && !api || project.studio !== undefined && !studio)
9566
+ return null;
9567
+ return {
9568
+ ...summary,
9569
+ database,
9570
+ ...api ? { api } : {},
9571
+ ...studio ? { studio } : {}
9572
+ };
9573
+ }
9574
+ function payloadWithinLimit(candidate) {
9575
+ try {
9576
+ const serializedPayload = JSON.stringify(candidate);
9577
+ return serializedPayload !== undefined && new TextEncoder().encode(serializedPayload).byteLength <= PROJECT_READ_RESPONSE_MAX_BYTES;
9578
+ } catch {
9579
+ return false;
9580
+ }
9581
+ }
9582
+ function validHttpStatus(status) {
9583
+ return Number.isSafeInteger(status) && status >= 100 && status <= 599;
9584
+ }
9585
+ function successfulResponse(response) {
9586
+ return response.ok === true && validHttpStatus(response.status) && response.status >= 200 && response.status <= 299;
9587
+ }
9588
+ function failedResult(message) {
9589
+ return { text: `❌ ${message}`, isError: true };
9590
+ }
9591
+ function failedHttpResult(label, status) {
9592
+ return failedResult(validHttpStatus(status) ? `${label} request failed (${status})` : `${label} request failed`);
9593
+ }
9594
+ function successfulResult(payload) {
9595
+ return { text: JSON.stringify(payload, null, 2), isError: false };
9596
+ }
9597
+ function projectGetRead(response, expectedRef) {
9598
+ if (!successfulResponse(response))
9599
+ return failedHttpResult("Project get", response.status);
9600
+ if (!payloadWithinLimit(response.data))
9601
+ return failedResult("Invalid project response");
9602
+ const project = projectDetails(response.data, expectedRef);
9603
+ return project ? successfulResult(project) : failedResult("Invalid project response");
9604
+ }
9605
+
9405
9606
  // src/shared/tools/project-cli-tools.ts
9607
+ function projectReadResponse(readResult) {
9608
+ return {
9609
+ content: [{ type: "text", text: readResult.text }],
9610
+ ...readResult.isError ? { isError: true } : {}
9611
+ };
9612
+ }
9406
9613
  var formatTasks = (data) => {
9407
9614
  if (!Array.isArray(data))
9408
9615
  return JSON.stringify(data, null, 2);
@@ -9558,8 +9765,9 @@ Actions: get, health, logs, api_keys, settings, tasks, task_detail, task_cancel,
9558
9765
  let text;
9559
9766
  switch (action) {
9560
9767
  case "get":
9561
- text = ok(await http.get(`/v1/projects/${resolvedRef}`));
9562
- break;
9768
+ return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
9769
+ maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
9770
+ }), resolvedRef));
9563
9771
  case "health":
9564
9772
  text = ok(await http.get(`/v1/projects/${resolvedRef}/health`));
9565
9773
  break;
@@ -10655,10 +10863,10 @@ function missingMigrationContextResult() {
10655
10863
  content: [{
10656
10864
  type: "text",
10657
10865
  text: [
10658
- "⚠️ Remote migration push requires SupaCloud project context.",
10659
- "Provide SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY, or SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN.",
10866
+ "⚠️ Remote migration push requires SupaCloud Management API context.",
10867
+ "Provide SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN.",
10660
10868
  "Also pass --ref or set SUPACLOUD_PROJECT_REF when the project ref cannot be inferred from the URL.",
10661
- "The service-role credential is sent only to the SupaCloud Management API and is never forwarded to the official CLI."
10869
+ "The Management token is sent only to the SupaCloud Management API and is never forwarded to the official CLI."
10662
10870
  ].join(`
10663
10871
  `)
10664
10872
  }]
@@ -10745,7 +10953,7 @@ function registerSupabaseCliTools(server, options = {}) {
10745
10953
  readOnly: options.readOnly ?? false,
10746
10954
  executeOfficialCli: options.executeOfficialCli || ((request) => executeOfficialSupabaseCli(request, environment))
10747
10955
  };
10748
- server.tool("supabase", "Controlled adapter for the official open-source Supabase CLI. Remote push stays on the SupaCloud service-role authenticated Management API.", {
10956
+ 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.", {
10749
10957
  action: withDescription(stringEnum([
10750
10958
  "version",
10751
10959
  "migration_new",
@@ -11368,10 +11576,245 @@ var SCHEDULE_TOOL_SCHEMA = {
11368
11576
  body_file: optional(Type.String(), "[create/update] Local JSON object file; content is never printed"),
11369
11577
  header_env: withDescription(headerEnvironmentSchema, "[create/update] JSON map of HTTP header names to environment variable names")
11370
11578
  };
11579
+
11580
+ // src/shared/mutation-protocol.ts
11581
+ 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}$/;
11582
+ var FINGERPRINT_PATTERN = /^[0-9a-f]{64}$/;
11583
+ var OPERATION_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/;
11584
+ var RESOURCE_KEY_PATTERN = /^v1\/(?:[a-z0-9][a-z0-9._-]{0,63})\/([A-Za-z0-9_-]{2,171})$/;
11585
+ var RESOURCE_ID_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
11586
+ var FAILURE_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,63}$/;
11587
+ var LEASE_OWNER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,254}$/;
11588
+ var TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
11589
+ var MAX_STATUS_RESPONSE_BYTES = 196608;
11590
+ var MAX_RESOURCE_ID_BYTES = 128;
11591
+ var FATAL_UTF8_DECODER = new TextDecoder("utf-8", { fatal: true });
11592
+ var MUTATION_STATUSES = new Set([
11593
+ "pending",
11594
+ "running",
11595
+ "succeeded",
11596
+ "failed_retryable",
11597
+ "failed_terminal",
11598
+ "outcome_unknown"
11599
+ ]);
11600
+ var MUTATION_RESPONSE_KEYS = ["project_ref", "mutation"];
11601
+ var MUTATION_KEYS = [
11602
+ "project_ref",
11603
+ "mutation_id",
11604
+ "operation",
11605
+ "resource_key",
11606
+ "request_fingerprint",
11607
+ "principal",
11608
+ "status",
11609
+ "checkpoint",
11610
+ "receipt",
11611
+ "response_status",
11612
+ "failure_code",
11613
+ "lease",
11614
+ "completed_at",
11615
+ "created_at",
11616
+ "updated_at"
11617
+ ];
11618
+ var PRINCIPAL_KEYS = ["type", "id"];
11619
+ var LEASE_KEYS = ["owner", "expires_at", "fencing_epoch"];
11620
+ function isMutationId(candidate) {
11621
+ return typeof candidate === "string" && MUTATION_ID_PATTERN.test(candidate);
11622
+ }
11623
+ function objectRecord3(candidate) {
11624
+ return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
11625
+ }
11626
+ function exactRecord(candidate, keys) {
11627
+ const record = objectRecord3(candidate);
11628
+ if (!record || Object.keys(record).length !== keys.length)
11629
+ return null;
11630
+ return keys.every((key) => Object.hasOwn(record, key)) ? record : null;
11631
+ }
11632
+ function emptyProjection(candidate) {
11633
+ const record = objectRecord3(candidate);
11634
+ return record && Object.keys(record).length === 0 ? record : null;
11635
+ }
11636
+ function canonicalTimestamp2(candidate) {
11637
+ if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
11638
+ return false;
11639
+ const milliseconds = Date.parse(candidate);
11640
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === candidate;
11641
+ }
11642
+ function nullableTimestamp(candidate) {
11643
+ return candidate === null || canonicalTimestamp2(candidate);
11644
+ }
11645
+ function safePrincipal(candidate) {
11646
+ const principal = exactRecord(candidate, PRINCIPAL_KEYS);
11647
+ if (!principal || !["master", "admin", "project"].includes(String(principal.type)))
11648
+ return null;
11649
+ if (typeof principal.id !== "string" || !principal.id || principal.id.length > 320 || principal.id.trim() !== principal.id || /[\u0000-\u001f\u007f]/u.test(principal.id))
11650
+ return null;
11651
+ return { type: principal.type, id: principal.id };
11652
+ }
11653
+ function safeLease(candidate) {
11654
+ const lease = exactRecord(candidate, LEASE_KEYS);
11655
+ if (!lease || lease.owner !== null && (typeof lease.owner !== "string" || !LEASE_OWNER_PATTERN.test(lease.owner)))
11656
+ return null;
11657
+ if (!nullableTimestamp(lease.expires_at) || !Number.isSafeInteger(lease.fencing_epoch) || Number(lease.fencing_epoch) < 0)
11658
+ return null;
11659
+ return {
11660
+ owner: lease.owner,
11661
+ expires_at: lease.expires_at,
11662
+ fencing_epoch: Number(lease.fencing_epoch)
11663
+ };
11664
+ }
11665
+ function safeResponseStatus(candidate) {
11666
+ if (candidate === null)
11667
+ return null;
11668
+ return Number.isInteger(candidate) && Number(candidate) >= 100 && Number(candidate) <= 599 ? Number(candidate) : undefined;
11669
+ }
11670
+ function validLeaseState(status, lease) {
11671
+ const running = status === "running";
11672
+ return running ? lease.owner !== null && lease.expires_at !== null && lease.fencing_epoch > 0 : lease.owner === null && lease.expires_at === null;
11673
+ }
11674
+ function validMutationLifecycle(mutation, receipt, responseStatus) {
11675
+ const terminal = ["succeeded", "failed_terminal", "outcome_unknown"].includes(String(mutation.status));
11676
+ if (mutation.completed_at !== null !== terminal)
11677
+ return false;
11678
+ if (mutation.status === "succeeded") {
11679
+ return receipt !== null && responseStatus !== null && responseStatus >= 200 && responseStatus < 300 && mutation.failure_code === null;
11680
+ }
11681
+ if (mutation.status === "failed_terminal") {
11682
+ return receipt !== null && typeof mutation.failure_code === "string";
11683
+ }
11684
+ if (mutation.status === "failed_retryable" || mutation.status === "outcome_unknown") {
11685
+ return receipt !== null && typeof mutation.failure_code === "string";
11686
+ }
11687
+ if (mutation.status === "pending") {
11688
+ return receipt === null && responseStatus === null && mutation.failure_code === null;
11689
+ }
11690
+ return true;
11691
+ }
11692
+ function canonicalMutationResourceKey(candidate) {
11693
+ if (typeof candidate !== "string")
11694
+ return false;
11695
+ const match = RESOURCE_KEY_PATTERN.exec(candidate);
11696
+ if (!match)
11697
+ return false;
11698
+ const encodedResourceId = match[1];
11699
+ const resourceIdBytes = Buffer.from(encodedResourceId, "base64url");
11700
+ if (resourceIdBytes.byteLength < 1 || resourceIdBytes.byteLength > MAX_RESOURCE_ID_BYTES || resourceIdBytes.toString("base64url") !== encodedResourceId)
11701
+ return false;
11702
+ let resourceId;
11703
+ try {
11704
+ resourceId = FATAL_UTF8_DECODER.decode(resourceIdBytes);
11705
+ } catch (decodeError) {
11706
+ if (decodeError instanceof TypeError)
11707
+ return false;
11708
+ throw decodeError;
11709
+ }
11710
+ return resourceId.trim() === resourceId && !RESOURCE_ID_CONTROL_PATTERN.test(resourceId) && Buffer.from(resourceId, "utf8").equals(resourceIdBytes);
11711
+ }
11712
+ function validMutationIdentity(mutation) {
11713
+ if (!isMutationId(mutation.mutation_id) || typeof mutation.project_ref !== "string")
11714
+ return false;
11715
+ if (typeof mutation.operation !== "string" || !OPERATION_PATTERN.test(mutation.operation))
11716
+ return false;
11717
+ if (mutation.resource_key !== null && !canonicalMutationResourceKey(mutation.resource_key))
11718
+ return false;
11719
+ return typeof mutation.request_fingerprint === "string" && FINGERPRINT_PATTERN.test(mutation.request_fingerprint);
11720
+ }
11721
+ function validMutationTerminalFields(mutation) {
11722
+ if (typeof mutation.status !== "string" || !MUTATION_STATUSES.has(mutation.status))
11723
+ return false;
11724
+ if (mutation.failure_code !== null && (typeof mutation.failure_code !== "string" || !FAILURE_CODE_PATTERN.test(mutation.failure_code)))
11725
+ return false;
11726
+ return nullableTimestamp(mutation.completed_at) && canonicalTimestamp2(mutation.created_at) && canonicalTimestamp2(mutation.updated_at);
11727
+ }
11728
+ function safeMutationStatus(candidate) {
11729
+ const mutation = exactRecord(candidate, MUTATION_KEYS);
11730
+ if (!mutation || !validMutationIdentity(mutation) || !validMutationTerminalFields(mutation))
11731
+ return null;
11732
+ const principal = safePrincipal(mutation.principal);
11733
+ const checkpoint = emptyProjection(mutation.checkpoint);
11734
+ const receipt = mutation.receipt === null ? null : emptyProjection(mutation.receipt);
11735
+ const responseStatus = safeResponseStatus(mutation.response_status);
11736
+ const lease = safeLease(mutation.lease);
11737
+ if (!principal || !checkpoint || mutation.receipt !== null && !receipt || responseStatus === undefined || !lease || !validLeaseState(String(mutation.status), lease) || !validMutationLifecycle(mutation, receipt, responseStatus))
11738
+ return null;
11739
+ return {
11740
+ project_ref: mutation.project_ref,
11741
+ mutation_id: mutation.mutation_id,
11742
+ operation: mutation.operation,
11743
+ resource_key: mutation.resource_key,
11744
+ request_fingerprint: mutation.request_fingerprint,
11745
+ principal,
11746
+ status: mutation.status,
11747
+ checkpoint,
11748
+ receipt,
11749
+ response_status: responseStatus,
11750
+ failure_code: mutation.failure_code,
11751
+ lease,
11752
+ completed_at: mutation.completed_at,
11753
+ created_at: mutation.created_at,
11754
+ updated_at: mutation.updated_at
11755
+ };
11756
+ }
11757
+ function mutationStatusPath(ref, mutationId) {
11758
+ if (!isMutationId(mutationId))
11759
+ throw new Error("'mutation_id' must be a UUIDv4");
11760
+ return `/v1/projects/${projectRefPathSegment(ref, "Mutations")}/mutations/${encodeURIComponent(mutationId)}`;
11761
+ }
11762
+ function mutationStatusResponse(ref, mutationId, payload) {
11763
+ const response = exactRecord(payload, MUTATION_RESPONSE_KEYS);
11764
+ const mutation = safeMutationStatus(response?.mutation);
11765
+ return response?.project_ref === ref && mutation?.project_ref === ref && mutation.mutation_id === mutationId ? mutation : null;
11766
+ }
11767
+ async function fetchMutationStatus(http, ref, mutationId) {
11768
+ const response = await http.get(mutationStatusPath(ref, mutationId), {
11769
+ maxResponseBytes: MAX_STATUS_RESPONSE_BYTES
11770
+ });
11771
+ if (!response.ok) {
11772
+ return { kind: "unavailable", httpStatus: response.transportError ? null : response.status };
11773
+ }
11774
+ const mutation = mutationStatusResponse(ref, mutationId, response.data);
11775
+ return mutation ? { kind: "available", mutation } : { kind: "invalid" };
11776
+ }
11777
+
11778
+ // src/shared/tools/mutation-tools.ts
11779
+ function requiredText3(args, name) {
11780
+ const candidate = args[name];
11781
+ if (typeof candidate !== "string" || !candidate.trim())
11782
+ throw new Error(`'${name}' is required for 'status'`);
11783
+ return candidate.trim();
11784
+ }
11785
+ async function mutationStatus(http, args) {
11786
+ const ref = requiredText3(args, "ref");
11787
+ projectRefPathSegment(ref, "Mutations");
11788
+ const mutationId = requiredText3(args, "mutation_id");
11789
+ if (!isMutationId(mutationId))
11790
+ throw new Error("'mutation_id' must be a UUIDv4");
11791
+ const readback = await fetchMutationStatus(http, ref, mutationId);
11792
+ if (readback.kind === "unavailable") {
11793
+ return releaseControlFailure("mutations.status", "HTTP_ERROR", readback.httpStatus);
11794
+ }
11795
+ if (readback.kind === "invalid") {
11796
+ return releaseControlFailure("mutations.status", "INVALID_RESPONSE", null);
11797
+ }
11798
+ if (readback.mutation.status !== "succeeded") {
11799
+ return releaseControlFailure("mutations.status", "MUTATION_NOT_SUCCEEDED", null, {
11800
+ project_ref: ref,
11801
+ mutation: readback.mutation
11802
+ });
11803
+ }
11804
+ return releaseControlSuccess("mutations.status", { project_ref: ref, mutation: readback.mutation });
11805
+ }
11806
+ function registerMutationTools(server, http) {
11807
+ server.tool("mutations", "Durable mutation status readback", MUTATION_TOOL_SCHEMA, (args) => mutationStatus(http, args));
11808
+ }
11809
+ var MUTATION_TOOL_SCHEMA = {
11810
+ action: withDescription(stringEnum(["status"]), "Action"),
11811
+ ref: withDescription(Type.String(), "[status] Project ref"),
11812
+ mutation_id: withDescription(Type.String(), "[status] Client mutation UUID")
11813
+ };
11371
11814
  // package.json
11372
11815
  var package_default = {
11373
11816
  name: "@supacloud/cli",
11374
- version: "0.18.0",
11817
+ version: "0.20.0",
11375
11818
  description: "Project-scoped CLI for SupaCloud users",
11376
11819
  type: "module",
11377
11820
  main: "./dist/index.js",
@@ -11436,13 +11879,14 @@ function failedEndpointProbe(error) {
11436
11879
  const timedOut = error instanceof Error && error.name === "AbortError";
11437
11880
  return { reachable: false, ok: false, httpStatus: null, error: timedOut ? "timeout" : "unreachable" };
11438
11881
  }
11439
- async function probeEndpoint(url, token) {
11882
+ async function probeEndpoint(url, headers) {
11440
11883
  const controller = new AbortController;
11441
11884
  const timeout = setTimeout(() => controller.abort(), 3000);
11442
11885
  try {
11443
11886
  const response = await fetch(url, {
11444
11887
  method: "GET",
11445
- headers: token ? { Authorization: `Bearer ${token}` } : undefined,
11888
+ headers,
11889
+ redirect: "error",
11446
11890
  signal: controller.signal
11447
11891
  });
11448
11892
  return successfulEndpointProbe(response);
@@ -11453,6 +11897,13 @@ async function probeEndpoint(url, token) {
11453
11897
  }
11454
11898
  }
11455
11899
  function missingProjectContextFields(context) {
11900
+ if (context.credentialScope === "project_application") {
11901
+ return [
11902
+ !context.inferredSupabaseUrl ? "secureSupabaseUrl" : null,
11903
+ !context.inferredServiceRoleKey ? "serviceRoleKey" : null,
11904
+ !context.projectRef ? "projectRef" : null
11905
+ ].filter((field) => Boolean(field));
11906
+ }
11456
11907
  return [
11457
11908
  !context.apiUrl ? "apiUrl" : null,
11458
11909
  !context.apiToken ? "apiToken" : null,
@@ -11462,16 +11913,42 @@ function missingProjectContextFields(context) {
11462
11913
  function authenticatedByProbe(authentication) {
11463
11914
  if (!authentication)
11464
11915
  return null;
11465
- return authentication.reachable && ![401, 403].includes(authentication.httpStatus ?? 0);
11916
+ return authentication.reachable && authentication.ok;
11466
11917
  }
11467
- async function collectProjectStatusChecks(context) {
11468
- const missing = missingProjectContextFields(context);
11469
- const connectivity = context.apiUrl ? await probeEndpoint(`${context.apiUrl}/health`) : null;
11470
- const authentication = missing.length === 0 && connectivity?.ok ? await probeEndpoint(`${context.apiUrl}/v1/projects/${encodeURIComponent(context.projectRef)}/health`, context.apiToken) : null;
11918
+ function connectivityProbeIsHealthy(scope, connectivity) {
11919
+ if (!connectivity)
11920
+ return null;
11921
+ if (scope !== "project_application")
11922
+ return connectivity.ok;
11923
+ return connectivity.reachable && (connectivity.ok || [401, 403].includes(connectivity.httpStatus ?? 0));
11924
+ }
11925
+ function projectApplicationHeaders(serviceRoleKey) {
11926
+ return { Authorization: `Bearer ${serviceRoleKey}`, apikey: serviceRoleKey };
11927
+ }
11928
+ function projectStatusApiUrl(context) {
11929
+ return context.credentialScope === "project_application" ? context.inferredSupabaseUrl : context.apiUrl;
11930
+ }
11931
+ function projectStatusProbePlan(context) {
11932
+ if (context.credentialScope === "project_application") {
11933
+ return {
11934
+ apiUrl: projectStatusApiUrl(context),
11935
+ connectivityPath: "/rest/v1/",
11936
+ authenticationPath: "/rest/v1/",
11937
+ authenticationHeaders: projectApplicationHeaders(context.inferredServiceRoleKey)
11938
+ };
11939
+ }
11940
+ return {
11941
+ apiUrl: projectStatusApiUrl(context),
11942
+ connectivityPath: "/health",
11943
+ authenticationPath: `/v1/projects/${encodeURIComponent(context.projectRef)}/health`,
11944
+ authenticationHeaders: { Authorization: `Bearer ${context.apiToken}` }
11945
+ };
11946
+ }
11947
+ function projectStatusChecks(missing, connectivity, authentication, connectivityOk) {
11471
11948
  return {
11472
11949
  configuration: { ok: missing.length === 0, missing },
11473
11950
  connectivity: {
11474
- ok: connectivity?.ok ?? null,
11951
+ ok: connectivityOk,
11475
11952
  reachable: connectivity?.reachable ?? null,
11476
11953
  httpStatus: connectivity?.httpStatus ?? null,
11477
11954
  error: connectivity?.error ?? null
@@ -11480,21 +11957,31 @@ async function collectProjectStatusChecks(context) {
11480
11957
  project: { ok: authentication?.ok ?? null }
11481
11958
  };
11482
11959
  }
11960
+ async function collectProjectStatusChecks(context) {
11961
+ const missing = missingProjectContextFields(context);
11962
+ const probePlan = projectStatusProbePlan(context);
11963
+ const connectivity = probePlan.apiUrl ? await probeEndpoint(`${probePlan.apiUrl}${probePlan.connectivityPath}`) : null;
11964
+ const connectivityOk = connectivityProbeIsHealthy(context.credentialScope, connectivity);
11965
+ const authentication = missing.length === 0 && connectivityOk ? await probeEndpoint(`${probePlan.apiUrl}${probePlan.authenticationPath}`, probePlan.authenticationHeaders) : null;
11966
+ return projectStatusChecks(missing, connectivity, authentication, connectivityOk);
11967
+ }
11483
11968
  function projectStatusIsHealthy(checks) {
11484
11969
  return checks.configuration.ok && checks.connectivity.ok === true && checks.authentication.ok === true && checks.project.ok === true;
11485
11970
  }
11486
11971
  async function createProjectStatusResult(context) {
11487
11972
  const checks = await collectProjectStatusChecks(context);
11973
+ const statusApiUrl = projectStatusApiUrl(context);
11488
11974
  const statusPayload = {
11489
11975
  mode: "project",
11976
+ credentialScope: context.credentialScope,
11490
11977
  environment: context.environment || null,
11491
11978
  source: { kind: context.source, path: context.sourcePath },
11492
11979
  projectRef: context.projectRef || null,
11493
- apiUrl: context.apiUrl || null,
11980
+ apiUrl: statusApiUrl || null,
11494
11981
  readOnly: context.readOnly,
11495
11982
  production: context.production,
11496
11983
  autoLinked: Boolean(context.inferredSupabaseUrl && context.inferredServiceRoleKey),
11497
- hasApiToken: Boolean(context.apiToken),
11984
+ hasApiToken: context.credentialScope === "project_application" ? Boolean(context.inferredServiceRoleKey) : Boolean(context.apiToken),
11498
11985
  checks
11499
11986
  };
11500
11987
  return {
@@ -11552,15 +12039,15 @@ GLOBAL FLAGS
11552
12039
  DEFAULT CONTEXT
11553
12040
 
11554
12041
  Without a selector or project variables, runs use the current project's legacy .env.
11555
- Supported auto-link variables:
11556
- SUPABASE_URL / SUPACLOUD_API_URL
11557
- SUPABASE_SERVICE_ROLE_KEY / SUPACLOUD_API_TOKEN
11558
- SUPACLOUD_PROJECT_REF (when it cannot be inferred from <ref>.api.*)
12042
+ Application status accepts SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY.
12043
+ Management-backed project commands require SUPACLOUD_API_URL +
12044
+ SUPACLOUD_API_TOKEN. These credential scopes are never mixed.
12045
+ SUPACLOUD_PROJECT_REF is required when it cannot be inferred from <ref>.api.*.
11559
12046
 
11560
12047
  SUPACLOUD_READ_ONLY=true blocks remote writes. Production writes require an
11561
12048
  exact --confirm-production value, and cannot override the selected project ref.
11562
12049
 
11563
- status checks configuration, Management API connectivity, and authentication.
12050
+ status checks configuration, the selected API scope, connectivity, and authentication.
11564
12051
  It exits non-zero when a required check fails.
11565
12052
 
11566
12053
  ${autoLink}
@@ -11591,6 +12078,7 @@ EXAMPLES
11591
12078
  ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
11592
12079
  ${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4
11593
12080
  ${preferredCommand} scheduled_functions list --ref abc123
12081
+ ${preferredCommand} mutations status --ref abc123 --mutation_id 00000000-0000-4000-8000-000000000001
11594
12082
  ${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
11595
12083
  ${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
11596
12084
  ${preferredCommand} gateway routes --ref abc123
@@ -11640,13 +12128,16 @@ function createCliTools(context, confirmProduction) {
11640
12128
  {
11641
12129
  type: "text",
11642
12130
  text: [
11643
- "⚠️ Project commands need a project-scoped API context.",
12131
+ "⚠️ Project commands need a Management API context.",
11644
12132
  "",
11645
12133
  "Provide one of these sources:",
11646
12134
  " - --env <name> for .env.supacloud.<name>",
11647
12135
  " - --env-file <path> for a file declaring SUPACLOUD_ENV",
11648
- " - .env with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY",
11649
12136
  " - SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN",
12137
+ " - SUPACLOUD_PROJECT_REF when the profile cannot infer it",
12138
+ "",
12139
+ "SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY application profiles",
12140
+ "are accepted only by status and local commands.",
11650
12141
  "",
11651
12142
  "Then retry commands such as:",
11652
12143
  ` ${preferredCommand} project get`,
@@ -11657,7 +12148,7 @@ function createCliTools(context, confirmProduction) {
11657
12148
  ]
11658
12149
  })
11659
12150
  };
11660
- for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "diagnostics", "gateway", "branch"]) {
12151
+ for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch"]) {
11661
12152
  tools[name] = {
11662
12153
  schema: { action: genericActionSchema },
11663
12154
  callback: async () => ({
@@ -11665,7 +12156,7 @@ function createCliTools(context, confirmProduction) {
11665
12156
  content: [
11666
12157
  {
11667
12158
  type: "text",
11668
- text: `⚠️ This command requires project-scoped API context. Run \`${preferredCommand} status\` to inspect current detection.`
12159
+ text: `⚠️ This command requires Management API context. Run \`${preferredCommand} status\` to inspect current detection.`
11669
12160
  }
11670
12161
  ]
11671
12162
  })
@@ -11684,7 +12175,7 @@ function createCliTools(context, confirmProduction) {
11684
12175
  tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
11685
12176
  }
11686
12177
  };
11687
- if (!context.apiUrl || !context.apiToken) {
12178
+ if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
11688
12179
  registerContextAwareHelp();
11689
12180
  tools.setup_help = {
11690
12181
  schema: {},
@@ -11696,19 +12187,18 @@ function createCliTools(context, confirmProduction) {
11696
12187
  text: [
11697
12188
  `⚠️ No project context found for ${preferredCommand}.`,
11698
12189
  "",
11699
- `${preferredCommand} expects project-scoped credentials by default.`,
12190
+ `${preferredCommand} remote tools require Management API credentials.`,
11700
12191
  "Provide one of these sources:",
11701
12192
  "",
11702
12193
  " 1. Named environment file",
11703
12194
  " supacloud-cli --env test status",
11704
12195
  "",
11705
- " 2. Current workspace .env",
11706
- " SUPABASE_URL=https://your-project.example.com",
11707
- " SUPABASE_SERVICE_ROLE_KEY=...",
11708
- "",
11709
- " 3. Explicit environment variables",
12196
+ " 2. Explicit environment variables",
11710
12197
  " SUPACLOUD_API_URL=https://your-project.example.com",
11711
12198
  " SUPACLOUD_API_TOKEN=...",
12199
+ " SUPACLOUD_PROJECT_REF=your-project-ref",
12200
+ "",
12201
+ "Application SUPABASE_* profiles remain available to status and local commands.",
11712
12202
  "",
11713
12203
  "For server installation and tenant management, use:",
11714
12204
  " supacloud-admin"
@@ -11742,6 +12232,7 @@ function createCliTools(context, confirmProduction) {
11742
12232
  assign(captureTools((server) => registerScheduledFunctionTools(server, http, process.env, {
11743
12233
  readOnly: context.readOnly
11744
12234
  })));
12235
+ assign(captureTools((server) => registerMutationTools(server, http)));
11745
12236
  assign(captureTools((server) => registerFrontendTools(server, http)));
11746
12237
  assign(captureTools((server) => registerGatewayTools(server, http, {
11747
12238
  projectRef: context.projectRef || undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",