@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.
- package/README.md +24 -18
- package/dist/index.js +568 -77
- 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"],
|
|
@@ -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 =
|
|
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
|
-
|
|
9562
|
-
|
|
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
|
|
10659
|
-
"Provide
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
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 &&
|
|
11916
|
+
return authentication.reachable && authentication.ok;
|
|
11466
11917
|
}
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
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,
|
|
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
|
|
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
|
|
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}
|
|
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.
|
|
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
|