@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.
- package/README.md +24 -18
- package/dist/index.js +318 -71
- 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,
|
|
96
|
-
by mixing sources. If `SUPACLOUD_ENV` is set without a complete process
|
|
97
|
-
it strictly selects `.env.supacloud.<value>`.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
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,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
6337
|
-
const
|
|
6338
|
-
if (!
|
|
6336
|
+
function canonicalApiOrigin(value) {
|
|
6337
|
+
const candidate = value.trim();
|
|
6338
|
+
if (!candidate)
|
|
6339
6339
|
return "";
|
|
6340
6340
|
try {
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6355
|
-
if (!normalized)
|
|
6356
|
+
if (!value)
|
|
6356
6357
|
return "";
|
|
6357
|
-
return new URL(
|
|
6358
|
+
return new URL(value).hostname.match(/^([a-z0-9-]+)\.api\./i)?.[1] ?? "";
|
|
6358
6359
|
}
|
|
6359
|
-
function
|
|
6360
|
-
const
|
|
6361
|
-
if (
|
|
6362
|
-
return "";
|
|
6363
|
-
|
|
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 =
|
|
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
|
|
6386
|
-
const
|
|
6387
|
-
|
|
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
|
|
10659
|
-
"Provide
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
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 &&
|
|
11672
|
+
return authentication.reachable && authentication.ok;
|
|
11466
11673
|
}
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
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,
|
|
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
|
|
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
|
|
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}
|
|
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.
|
|
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
|