@supacloud/cli 0.18.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.
Files changed (3) hide show
  1. package/README.md +24 -18
  2. package/dist/index.js +318 -71
  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"],
@@ -6614,7 +6606,8 @@ async function fetchWithTimeout(url, options) {
6614
6606
  try {
6615
6607
  return await fetch(url, {
6616
6608
  ...options,
6617
- signal: controller.signal
6609
+ signal: controller.signal,
6610
+ redirect: "error"
6618
6611
  });
6619
6612
  } finally {
6620
6613
  clearTimeout(timeout);
@@ -10655,10 +10648,10 @@ function missingMigrationContextResult() {
10655
10648
  content: [{
10656
10649
  type: "text",
10657
10650
  text: [
10658
- "⚠️ Remote migration push requires SupaCloud project context.",
10659
- "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.",
10660
10653
  "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."
10654
+ "The Management token is sent only to the SupaCloud Management API and is never forwarded to the official CLI."
10662
10655
  ].join(`
10663
10656
  `)
10664
10657
  }]
@@ -10745,7 +10738,7 @@ function registerSupabaseCliTools(server, options = {}) {
10745
10738
  readOnly: options.readOnly ?? false,
10746
10739
  executeOfficialCli: options.executeOfficialCli || ((request) => executeOfficialSupabaseCli(request, environment))
10747
10740
  };
10748
- 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.", {
10749
10742
  action: withDescription(stringEnum([
10750
10743
  "version",
10751
10744
  "migration_new",
@@ -11368,10 +11361,216 @@ var SCHEDULE_TOOL_SCHEMA = {
11368
11361
  body_file: optional(Type.String(), "[create/update] Local JSON object file; content is never printed"),
11369
11362
  header_env: withDescription(headerEnvironmentSchema, "[create/update] JSON map of HTTP header names to environment variable names")
11370
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
+ };
11371
11570
  // package.json
11372
11571
  var package_default = {
11373
11572
  name: "@supacloud/cli",
11374
- version: "0.18.0",
11573
+ version: "0.19.0",
11375
11574
  description: "Project-scoped CLI for SupaCloud users",
11376
11575
  type: "module",
11377
11576
  main: "./dist/index.js",
@@ -11436,13 +11635,14 @@ function failedEndpointProbe(error) {
11436
11635
  const timedOut = error instanceof Error && error.name === "AbortError";
11437
11636
  return { reachable: false, ok: false, httpStatus: null, error: timedOut ? "timeout" : "unreachable" };
11438
11637
  }
11439
- async function probeEndpoint(url, token) {
11638
+ async function probeEndpoint(url, headers) {
11440
11639
  const controller = new AbortController;
11441
11640
  const timeout = setTimeout(() => controller.abort(), 3000);
11442
11641
  try {
11443
11642
  const response = await fetch(url, {
11444
11643
  method: "GET",
11445
- headers: token ? { Authorization: `Bearer ${token}` } : undefined,
11644
+ headers,
11645
+ redirect: "error",
11446
11646
  signal: controller.signal
11447
11647
  });
11448
11648
  return successfulEndpointProbe(response);
@@ -11453,6 +11653,13 @@ async function probeEndpoint(url, token) {
11453
11653
  }
11454
11654
  }
11455
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
+ }
11456
11663
  return [
11457
11664
  !context.apiUrl ? "apiUrl" : null,
11458
11665
  !context.apiToken ? "apiToken" : null,
@@ -11462,16 +11669,42 @@ function missingProjectContextFields(context) {
11462
11669
  function authenticatedByProbe(authentication) {
11463
11670
  if (!authentication)
11464
11671
  return null;
11465
- return authentication.reachable && ![401, 403].includes(authentication.httpStatus ?? 0);
11672
+ return authentication.reachable && authentication.ok;
11466
11673
  }
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;
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) {
11471
11704
  return {
11472
11705
  configuration: { ok: missing.length === 0, missing },
11473
11706
  connectivity: {
11474
- ok: connectivity?.ok ?? null,
11707
+ ok: connectivityOk,
11475
11708
  reachable: connectivity?.reachable ?? null,
11476
11709
  httpStatus: connectivity?.httpStatus ?? null,
11477
11710
  error: connectivity?.error ?? null
@@ -11480,21 +11713,31 @@ async function collectProjectStatusChecks(context) {
11480
11713
  project: { ok: authentication?.ok ?? null }
11481
11714
  };
11482
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
+ }
11483
11724
  function projectStatusIsHealthy(checks) {
11484
11725
  return checks.configuration.ok && checks.connectivity.ok === true && checks.authentication.ok === true && checks.project.ok === true;
11485
11726
  }
11486
11727
  async function createProjectStatusResult(context) {
11487
11728
  const checks = await collectProjectStatusChecks(context);
11729
+ const statusApiUrl = projectStatusApiUrl(context);
11488
11730
  const statusPayload = {
11489
11731
  mode: "project",
11732
+ credentialScope: context.credentialScope,
11490
11733
  environment: context.environment || null,
11491
11734
  source: { kind: context.source, path: context.sourcePath },
11492
11735
  projectRef: context.projectRef || null,
11493
- apiUrl: context.apiUrl || null,
11736
+ apiUrl: statusApiUrl || null,
11494
11737
  readOnly: context.readOnly,
11495
11738
  production: context.production,
11496
11739
  autoLinked: Boolean(context.inferredSupabaseUrl && context.inferredServiceRoleKey),
11497
- hasApiToken: Boolean(context.apiToken),
11740
+ hasApiToken: context.credentialScope === "project_application" ? Boolean(context.inferredServiceRoleKey) : Boolean(context.apiToken),
11498
11741
  checks
11499
11742
  };
11500
11743
  return {
@@ -11552,15 +11795,15 @@ GLOBAL FLAGS
11552
11795
  DEFAULT CONTEXT
11553
11796
 
11554
11797
  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.*)
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.*.
11559
11802
 
11560
11803
  SUPACLOUD_READ_ONLY=true blocks remote writes. Production writes require an
11561
11804
  exact --confirm-production value, and cannot override the selected project ref.
11562
11805
 
11563
- status checks configuration, Management API connectivity, and authentication.
11806
+ status checks configuration, the selected API scope, connectivity, and authentication.
11564
11807
  It exits non-zero when a required check fails.
11565
11808
 
11566
11809
  ${autoLink}
@@ -11591,6 +11834,7 @@ EXAMPLES
11591
11834
  ${preferredCommand} edge_functions deploy --ref abc123 --slug hello --prebundled-path ./dist/hello.js --expected-sha256 <sha256> --expected-active-version 4
11592
11835
  ${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3 --expected-active-version 4
11593
11836
  ${preferredCommand} scheduled_functions list --ref abc123
11837
+ ${preferredCommand} mutations status --ref abc123 --mutation_id 00000000-0000-4000-8000-000000000001
11594
11838
  ${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
11595
11839
  ${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
11596
11840
  ${preferredCommand} gateway routes --ref abc123
@@ -11640,13 +11884,16 @@ function createCliTools(context, confirmProduction) {
11640
11884
  {
11641
11885
  type: "text",
11642
11886
  text: [
11643
- "⚠️ Project commands need a project-scoped API context.",
11887
+ "⚠️ Project commands need a Management API context.",
11644
11888
  "",
11645
11889
  "Provide one of these sources:",
11646
11890
  " - --env <name> for .env.supacloud.<name>",
11647
11891
  " - --env-file <path> for a file declaring SUPACLOUD_ENV",
11648
- " - .env with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY",
11649
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.",
11650
11897
  "",
11651
11898
  "Then retry commands such as:",
11652
11899
  ` ${preferredCommand} project get`,
@@ -11657,7 +11904,7 @@ function createCliTools(context, confirmProduction) {
11657
11904
  ]
11658
11905
  })
11659
11906
  };
11660
- 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"]) {
11661
11908
  tools[name] = {
11662
11909
  schema: { action: genericActionSchema },
11663
11910
  callback: async () => ({
@@ -11665,7 +11912,7 @@ function createCliTools(context, confirmProduction) {
11665
11912
  content: [
11666
11913
  {
11667
11914
  type: "text",
11668
- 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.`
11669
11916
  }
11670
11917
  ]
11671
11918
  })
@@ -11684,7 +11931,7 @@ function createCliTools(context, confirmProduction) {
11684
11931
  tools.branch = { schema: branchHelpTool.schema, callback: branchContextCallback };
11685
11932
  }
11686
11933
  };
11687
- if (!context.apiUrl || !context.apiToken) {
11934
+ if (context.credentialScope !== "management" || !context.apiUrl || !context.apiToken) {
11688
11935
  registerContextAwareHelp();
11689
11936
  tools.setup_help = {
11690
11937
  schema: {},
@@ -11696,19 +11943,18 @@ function createCliTools(context, confirmProduction) {
11696
11943
  text: [
11697
11944
  `⚠️ No project context found for ${preferredCommand}.`,
11698
11945
  "",
11699
- `${preferredCommand} expects project-scoped credentials by default.`,
11946
+ `${preferredCommand} remote tools require Management API credentials.`,
11700
11947
  "Provide one of these sources:",
11701
11948
  "",
11702
11949
  " 1. Named environment file",
11703
11950
  " supacloud-cli --env test status",
11704
11951
  "",
11705
- " 2. Current workspace .env",
11706
- " SUPABASE_URL=https://your-project.example.com",
11707
- " SUPABASE_SERVICE_ROLE_KEY=...",
11708
- "",
11709
- " 3. Explicit environment variables",
11952
+ " 2. Explicit environment variables",
11710
11953
  " SUPACLOUD_API_URL=https://your-project.example.com",
11711
11954
  " SUPACLOUD_API_TOKEN=...",
11955
+ " SUPACLOUD_PROJECT_REF=your-project-ref",
11956
+ "",
11957
+ "Application SUPABASE_* profiles remain available to status and local commands.",
11712
11958
  "",
11713
11959
  "For server installation and tenant management, use:",
11714
11960
  " supacloud-admin"
@@ -11742,6 +11988,7 @@ function createCliTools(context, confirmProduction) {
11742
11988
  assign(captureTools((server) => registerScheduledFunctionTools(server, http, process.env, {
11743
11989
  readOnly: context.readOnly
11744
11990
  })));
11991
+ assign(captureTools((server) => registerMutationTools(server, http)));
11745
11992
  assign(captureTools((server) => registerFrontendTools(server, http)));
11746
11993
  assign(captureTools((server) => registerGatewayTools(server, http, {
11747
11994
  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.19.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",