@supacloud/cli 0.25.0 → 0.27.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/dist/index.js
CHANGED
|
@@ -6458,8 +6458,9 @@ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selecti
|
|
|
6458
6458
|
// src/shared/execution-policy.ts
|
|
6459
6459
|
var ACTION_POLICY = {
|
|
6460
6460
|
project: {
|
|
6461
|
-
read: ["get", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
|
|
6462
|
-
write: ["pause", "restore", "task_cancel", "task_retry", "update_background_settings"]
|
|
6461
|
+
read: ["get", "endpoints", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
|
|
6462
|
+
write: ["pause", "restore", "task_cancel", "task_retry", "update_background_settings"],
|
|
6463
|
+
local: ["list"]
|
|
6463
6464
|
},
|
|
6464
6465
|
database: {
|
|
6465
6466
|
read: ["list_tables", "describe_columns", "list_indexes", "list_constraints", "list_extensions", "rls_status", "rls_policies", "list_auth_users", "get_auth_user", "connections", "stats", "slow_queries", "list_migrations", "migration_inventory", "project_url", "generate_types"],
|
|
@@ -6473,6 +6474,10 @@ var ACTION_POLICY = {
|
|
|
6473
6474
|
read: ["list_providers", "get_provider", "supported_providers", "get_settings", "get_config"],
|
|
6474
6475
|
write: ["configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config"]
|
|
6475
6476
|
},
|
|
6477
|
+
oauth_clients: {
|
|
6478
|
+
read: ["list", "get"],
|
|
6479
|
+
write: ["create", "delete"]
|
|
6480
|
+
},
|
|
6476
6481
|
storage: {
|
|
6477
6482
|
read: ["status", "list_buckets", "get_bucket", "list_files"],
|
|
6478
6483
|
write: ["create_bucket", "update_bucket", "delete_bucket", "upload_base64", "delete_file"]
|
|
@@ -8170,6 +8175,214 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
8170
8175
|
});
|
|
8171
8176
|
}
|
|
8172
8177
|
|
|
8178
|
+
// src/shared/tools/oauth-client-tools.ts
|
|
8179
|
+
var RELEASE_CANARY_CLIENT_NAME = "supacloud-release-canary";
|
|
8180
|
+
var CLIENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,256}$/;
|
|
8181
|
+
var MAX_CLIENT_LIST_BYTES = 256 * 1024;
|
|
8182
|
+
var READ_TIMEOUT_MS = 5000;
|
|
8183
|
+
function isRecord(value) {
|
|
8184
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8185
|
+
}
|
|
8186
|
+
function releaseCanaryCallbackUri(value) {
|
|
8187
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
8188
|
+
throw new Error("'redirect_uri' is required");
|
|
8189
|
+
}
|
|
8190
|
+
let uri;
|
|
8191
|
+
try {
|
|
8192
|
+
uri = new URL(value);
|
|
8193
|
+
} catch {
|
|
8194
|
+
throw new Error("'redirect_uri' must be an absolute HTTPS or loopback HTTP URL");
|
|
8195
|
+
}
|
|
8196
|
+
const loopback = uri.hostname === "127.0.0.1" || uri.hostname === "[::1]";
|
|
8197
|
+
const isHttps = uri.protocol === "https:";
|
|
8198
|
+
const isPortBoundLoopback = uri.protocol === "http:" && loopback && Boolean(uri.port);
|
|
8199
|
+
if (!isHttps && !isPortBoundLoopback || !uri.hostname || uri.username || uri.password || uri.search || uri.hash) {
|
|
8200
|
+
throw new Error("'redirect_uri' must be an exact HTTPS callback or port-bound loopback HTTP callback without credentials, query, or fragment");
|
|
8201
|
+
}
|
|
8202
|
+
return uri.toString();
|
|
8203
|
+
}
|
|
8204
|
+
function createdClientId(value) {
|
|
8205
|
+
if (!isRecord(value))
|
|
8206
|
+
return null;
|
|
8207
|
+
try {
|
|
8208
|
+
return clientId(value.client_id);
|
|
8209
|
+
} catch {
|
|
8210
|
+
return null;
|
|
8211
|
+
}
|
|
8212
|
+
}
|
|
8213
|
+
function clientId(value) {
|
|
8214
|
+
if (typeof value !== "string" || !CLIENT_ID_PATTERN.test(value)) {
|
|
8215
|
+
throw new Error("'client_id' is invalid");
|
|
8216
|
+
}
|
|
8217
|
+
return value;
|
|
8218
|
+
}
|
|
8219
|
+
function projectRef(value) {
|
|
8220
|
+
if (typeof value !== "string" || !value.trim())
|
|
8221
|
+
throw new Error("'ref' is required");
|
|
8222
|
+
return projectRefPathSegment(value.trim(), "OAuth client");
|
|
8223
|
+
}
|
|
8224
|
+
function oauthClientsPath(ref) {
|
|
8225
|
+
return `/v1/projects/${encodeURIComponent(ref)}/auth/oauth-clients`;
|
|
8226
|
+
}
|
|
8227
|
+
function expectedClient(value, redirectUri) {
|
|
8228
|
+
if (!isRecord(value) || typeof value.client_id !== "string" || !CLIENT_ID_PATTERN.test(value.client_id) || value.client_name !== RELEASE_CANARY_CLIENT_NAME || value.client_type !== "public" || value.token_endpoint_auth_method !== "none" || !Array.isArray(value.redirect_uris) || value.redirect_uris.length !== 1 || !Array.isArray(value.grant_types) || value.grant_types.length !== 1 || value.grant_types[0] !== "authorization_code" || !Array.isArray(value.response_types) || value.response_types.length !== 1 || value.response_types[0] !== "code")
|
|
8229
|
+
return null;
|
|
8230
|
+
let callback;
|
|
8231
|
+
try {
|
|
8232
|
+
callback = releaseCanaryCallbackUri(value.redirect_uris[0]);
|
|
8233
|
+
} catch {
|
|
8234
|
+
return null;
|
|
8235
|
+
}
|
|
8236
|
+
if (redirectUri !== undefined && callback !== redirectUri)
|
|
8237
|
+
return null;
|
|
8238
|
+
return {
|
|
8239
|
+
client_id: value.client_id,
|
|
8240
|
+
client_name: RELEASE_CANARY_CLIENT_NAME,
|
|
8241
|
+
client_type: "public",
|
|
8242
|
+
token_endpoint_auth_method: "none",
|
|
8243
|
+
redirect_uris: [callback],
|
|
8244
|
+
grant_types: ["authorization_code"],
|
|
8245
|
+
response_types: ["code"]
|
|
8246
|
+
};
|
|
8247
|
+
}
|
|
8248
|
+
function clientInventory(value) {
|
|
8249
|
+
if (!isRecord(value) || !Array.isArray(value.clients))
|
|
8250
|
+
return null;
|
|
8251
|
+
const clients = value.clients.filter((client) => isRecord(client) && client.client_name === RELEASE_CANARY_CLIENT_NAME).map((client) => expectedClient(client));
|
|
8252
|
+
if (clients.some((client) => client === null))
|
|
8253
|
+
return null;
|
|
8254
|
+
const inventory = clients;
|
|
8255
|
+
return new Set(inventory.map((client) => client.client_id)).size === inventory.length ? inventory : null;
|
|
8256
|
+
}
|
|
8257
|
+
function readFailure(operation, response) {
|
|
8258
|
+
if (!response.ok) {
|
|
8259
|
+
return releaseControlFailure(operation, response.responseReadError ? "INVALID_RESPONSE" : "HTTP_ERROR", response.transportError ? null : response.status);
|
|
8260
|
+
}
|
|
8261
|
+
return null;
|
|
8262
|
+
}
|
|
8263
|
+
async function listClients(http, ref) {
|
|
8264
|
+
const response = await http.get(oauthClientsPath(ref), {
|
|
8265
|
+
maxJsonBytes: MAX_CLIENT_LIST_BYTES,
|
|
8266
|
+
responseTimeoutMs: READ_TIMEOUT_MS
|
|
8267
|
+
});
|
|
8268
|
+
return { response, clients: response.ok && response.status === 200 ? clientInventory(response.data) : null };
|
|
8269
|
+
}
|
|
8270
|
+
async function getClient(http, ref, id) {
|
|
8271
|
+
const response = await http.get(`${oauthClientsPath(ref)}/${encodeURIComponent(id)}`, {
|
|
8272
|
+
maxJsonBytes: MAX_CLIENT_LIST_BYTES,
|
|
8273
|
+
responseTimeoutMs: READ_TIMEOUT_MS
|
|
8274
|
+
});
|
|
8275
|
+
return { response, client: response.ok && response.status === 200 ? expectedClient(response.data) : null };
|
|
8276
|
+
}
|
|
8277
|
+
function exactSingleClient(inventory, redirectUri) {
|
|
8278
|
+
return inventory.length === 1 && inventory[0]?.redirect_uris[0] === redirectUri ? inventory[0] : null;
|
|
8279
|
+
}
|
|
8280
|
+
async function listReleaseCanaryClients(http, ref) {
|
|
8281
|
+
const read = await listClients(http, ref);
|
|
8282
|
+
const failure = readFailure("oauth_clients.list", read.response);
|
|
8283
|
+
if (failure)
|
|
8284
|
+
return failure;
|
|
8285
|
+
if (!read.clients)
|
|
8286
|
+
return releaseControlFailure("oauth_clients.list", "INVALID_RESPONSE", read.response.status);
|
|
8287
|
+
return releaseControlSuccess("oauth_clients.list", { project_ref: ref, clients: read.clients });
|
|
8288
|
+
}
|
|
8289
|
+
async function getReleaseCanaryClient(http, ref, id) {
|
|
8290
|
+
const read = await getClient(http, ref, id);
|
|
8291
|
+
const failure = readFailure("oauth_clients.get", read.response);
|
|
8292
|
+
if (failure)
|
|
8293
|
+
return failure;
|
|
8294
|
+
if (!read.client || read.client.client_id !== id) {
|
|
8295
|
+
return releaseControlFailure("oauth_clients.get", "INVALID_RESPONSE", read.response.status);
|
|
8296
|
+
}
|
|
8297
|
+
return releaseControlSuccess("oauth_clients.get", { project_ref: ref, client: read.client });
|
|
8298
|
+
}
|
|
8299
|
+
async function createReleaseCanaryClient(http, ref, redirectUri) {
|
|
8300
|
+
const before = await listClients(http, ref);
|
|
8301
|
+
const beforeFailure = readFailure("oauth_clients.create", before.response);
|
|
8302
|
+
if (beforeFailure)
|
|
8303
|
+
return beforeFailure;
|
|
8304
|
+
if (!before.clients)
|
|
8305
|
+
return releaseControlFailure("oauth_clients.create", "INVALID_RESPONSE", before.response.status);
|
|
8306
|
+
const existing = exactSingleClient(before.clients, redirectUri);
|
|
8307
|
+
if (existing) {
|
|
8308
|
+
return releaseControlSuccess("oauth_clients.create", {
|
|
8309
|
+
project_ref: ref,
|
|
8310
|
+
client: existing,
|
|
8311
|
+
reused: true
|
|
8312
|
+
});
|
|
8313
|
+
}
|
|
8314
|
+
if (before.clients.length > 0) {
|
|
8315
|
+
return releaseControlFailure("oauth_clients.create", "MUTATION_NOT_SUCCEEDED", null, { project_ref: ref });
|
|
8316
|
+
}
|
|
8317
|
+
const mutation = await http.postReleaseMutation(oauthClientsPath(ref), {
|
|
8318
|
+
client_type: "public",
|
|
8319
|
+
token_endpoint_auth_method: "none",
|
|
8320
|
+
redirect_uris: [redirectUri],
|
|
8321
|
+
grant_types: ["authorization_code"],
|
|
8322
|
+
client_name: RELEASE_CANARY_CLIENT_NAME
|
|
8323
|
+
});
|
|
8324
|
+
if (!mutation.ok) {
|
|
8325
|
+
return releaseControlMutationFailure("oauth_clients.create", mutation, { project_ref: ref });
|
|
8326
|
+
}
|
|
8327
|
+
const createdId = createdClientId(mutation.data);
|
|
8328
|
+
if (!createdId)
|
|
8329
|
+
return releaseControlFailure("oauth_clients.create", "OUTCOME_UNKNOWN", mutation.status, { project_ref: ref });
|
|
8330
|
+
const read = await getClient(http, ref, createdId);
|
|
8331
|
+
const readFailureResult = readFailure("oauth_clients.create", read.response);
|
|
8332
|
+
if (readFailureResult || !read.client || read.client.client_id !== createdId || read.client.redirect_uris[0] !== redirectUri) {
|
|
8333
|
+
return releaseControlFailure("oauth_clients.create", "OUTCOME_UNKNOWN", mutation.status, { project_ref: ref });
|
|
8334
|
+
}
|
|
8335
|
+
return releaseControlSuccess("oauth_clients.create", {
|
|
8336
|
+
project_ref: ref,
|
|
8337
|
+
client: read.client,
|
|
8338
|
+
reused: false
|
|
8339
|
+
});
|
|
8340
|
+
}
|
|
8341
|
+
async function deleteReleaseCanaryClient(http, ref, id, redirectUri) {
|
|
8342
|
+
const before = await getClient(http, ref, id);
|
|
8343
|
+
const beforeFailure = readFailure("oauth_clients.delete", before.response);
|
|
8344
|
+
if (beforeFailure)
|
|
8345
|
+
return beforeFailure;
|
|
8346
|
+
if (!before.client || before.client.client_id !== id || before.client.redirect_uris[0] !== redirectUri) {
|
|
8347
|
+
return releaseControlFailure("oauth_clients.delete", "MUTATION_NOT_SUCCEEDED", null, { project_ref: ref });
|
|
8348
|
+
}
|
|
8349
|
+
const mutation = await http.deleteReleaseMutation(`${oauthClientsPath(ref)}/${encodeURIComponent(id)}`);
|
|
8350
|
+
if (!mutation.ok || ![200, 204].includes(mutation.status)) {
|
|
8351
|
+
return releaseControlMutationFailure("oauth_clients.delete", mutation, { project_ref: ref });
|
|
8352
|
+
}
|
|
8353
|
+
const after = await listClients(http, ref);
|
|
8354
|
+
const afterFailure = readFailure("oauth_clients.delete", after.response);
|
|
8355
|
+
if (afterFailure || !after.clients || after.clients.some((client) => client.client_id === id)) {
|
|
8356
|
+
return releaseControlFailure("oauth_clients.delete", "OUTCOME_UNKNOWN", mutation.status, { project_ref: ref });
|
|
8357
|
+
}
|
|
8358
|
+
return releaseControlSuccess("oauth_clients.delete", {
|
|
8359
|
+
project_ref: ref,
|
|
8360
|
+
client_id: id,
|
|
8361
|
+
deleted: true
|
|
8362
|
+
});
|
|
8363
|
+
}
|
|
8364
|
+
function registerOAuthClientTools(server, http) {
|
|
8365
|
+
server.tool("oauth_clients", "Dedicated release-canary public OAuth client lifecycle. It only manages the exact supacloud-release-canary public authorization-code client and never returns client secrets.", {
|
|
8366
|
+
action: withDescription(stringEnum(["list", "get", "create", "delete"]), "OAuth client action"),
|
|
8367
|
+
ref: withDescription(Type.String(), "Central SupAuth project ref"),
|
|
8368
|
+
client_id: optional(Type.String(), "[get/delete] Exact release-canary public OAuth client ID"),
|
|
8369
|
+
redirect_uri: optional(Type.String(), "[create/delete] Exact HTTPS or port-bound RFC 8252 loopback callback")
|
|
8370
|
+
}, async ({ action, ref, client_id, redirect_uri }) => {
|
|
8371
|
+
const targetRef = projectRef(ref);
|
|
8372
|
+
if (action === "list")
|
|
8373
|
+
return listReleaseCanaryClients(http, targetRef);
|
|
8374
|
+
if (action === "get")
|
|
8375
|
+
return getReleaseCanaryClient(http, targetRef, clientId(client_id));
|
|
8376
|
+
if (action === "create") {
|
|
8377
|
+
return createReleaseCanaryClient(http, targetRef, releaseCanaryCallbackUri(redirect_uri));
|
|
8378
|
+
}
|
|
8379
|
+
if (action === "delete") {
|
|
8380
|
+
return deleteReleaseCanaryClient(http, targetRef, clientId(client_id), releaseCanaryCallbackUri(redirect_uri));
|
|
8381
|
+
}
|
|
8382
|
+
throw new Error("Unknown OAuth client action");
|
|
8383
|
+
});
|
|
8384
|
+
}
|
|
8385
|
+
|
|
8173
8386
|
// src/shared/tools/storage-tools.ts
|
|
8174
8387
|
var MAX_BUCKET_ID_LENGTH = 100;
|
|
8175
8388
|
var MAX_MIME_TYPE_COUNT = 100;
|
|
@@ -9184,13 +9397,13 @@ async function readFunctionSource(http, request) {
|
|
|
9184
9397
|
function mutationIdentityMatches2(receipt, expectation) {
|
|
9185
9398
|
return receipt.success === true && receipt.project_ref === expectation.projectRef && receipt.slug === expectation.slug && receipt.previous_active_version === expectation.expectedActiveVersion && receipt.expected_activation_id === expectation.expectedActivationId;
|
|
9186
9399
|
}
|
|
9187
|
-
async function readFunctionIdentity(http,
|
|
9188
|
-
const resourcePath = edgeFunctionResourcePath(
|
|
9400
|
+
async function readFunctionIdentity(http, projectRef2, slug) {
|
|
9401
|
+
const resourcePath = edgeFunctionResourcePath(projectRef2, slug);
|
|
9189
9402
|
const response = await http.get(`${resourcePath}/config`);
|
|
9190
9403
|
if (!response.ok) {
|
|
9191
9404
|
return releaseControlFailure("edge_functions.get_config", "HTTP_ERROR", response.status);
|
|
9192
9405
|
}
|
|
9193
|
-
const identity = projectedFunctionIdentity(response.data,
|
|
9406
|
+
const identity = projectedFunctionIdentity(response.data, projectRef2, slug);
|
|
9194
9407
|
return identity ? { content: [{ type: "text", text: JSON.stringify(identity, null, 2) }] } : releaseControlFailure("edge_functions.get_config", "INVALID_RESPONSE", response.status);
|
|
9195
9408
|
}
|
|
9196
9409
|
async function updateFunctionConfiguration(http, request) {
|
|
@@ -9253,15 +9466,15 @@ function readOnlyActivationResult() {
|
|
|
9253
9466
|
};
|
|
9254
9467
|
}
|
|
9255
9468
|
function functionActivationTarget(args) {
|
|
9256
|
-
const
|
|
9469
|
+
const projectRef2 = typeof args.ref === "string" ? args.ref.trim() : "";
|
|
9257
9470
|
const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
9258
9471
|
const version = positiveFunctionVersion(args.version, "Function activation version");
|
|
9259
|
-
projectRefPathSegment(
|
|
9472
|
+
projectRefPathSegment(projectRef2, "Edge Function activation");
|
|
9260
9473
|
if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
|
|
9261
9474
|
throw new Error("'slug' is invalid for 'activate'");
|
|
9262
9475
|
const expectedActiveVersion = requiredExpectedActiveVersion(args, "activate");
|
|
9263
9476
|
const expectedActivationId = requiredExpectedActivationId(args, "activate");
|
|
9264
|
-
return { projectRef, functionSlug, version, expectedActiveVersion, expectedActivationId };
|
|
9477
|
+
return { projectRef: projectRef2, functionSlug, version, expectedActiveVersion, expectedActivationId };
|
|
9265
9478
|
}
|
|
9266
9479
|
function requiredExpectedActiveVersion(args, action) {
|
|
9267
9480
|
const expected = args["expected-active-version"];
|
|
@@ -9289,11 +9502,11 @@ async function activateFunctionVersion(http, args, readOnly = false) {
|
|
|
9289
9502
|
const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
|
|
9290
9503
|
if (unsupported.length > 0)
|
|
9291
9504
|
throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
|
|
9292
|
-
const { projectRef, functionSlug, version, expectedActiveVersion, expectedActivationId } = functionActivationTarget(args);
|
|
9293
|
-
const endpoint = edgeFunctionResourcePath(
|
|
9505
|
+
const { projectRef: projectRef2, functionSlug, version, expectedActiveVersion, expectedActivationId } = functionActivationTarget(args);
|
|
9506
|
+
const endpoint = edgeFunctionResourcePath(projectRef2, functionSlug) + `/versions/${encodeURIComponent(version)}/activate`;
|
|
9294
9507
|
return functionMutationResponse({
|
|
9295
9508
|
operation: "edge_functions.activate",
|
|
9296
|
-
projectRef,
|
|
9509
|
+
projectRef: projectRef2,
|
|
9297
9510
|
slug: functionSlug,
|
|
9298
9511
|
expectedActiveVersion,
|
|
9299
9512
|
expectedActivationId,
|
|
@@ -9979,6 +10192,115 @@ function projectGetRead(response, expectedRef) {
|
|
|
9979
10192
|
return project ? successfulResult(project) : failedResult("Invalid project response");
|
|
9980
10193
|
}
|
|
9981
10194
|
|
|
10195
|
+
// src/shared/tools/project-endpoint-read.ts
|
|
10196
|
+
var PROJECT_ENDPOINT_RESPONSE_MAX_BYTES = 256 * 1024;
|
|
10197
|
+
var PROJECT_ENDPOINT_LIST_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
10198
|
+
var PROJECT_REF_PATTERN4 = /^[a-z0-9-]{1,20}$/;
|
|
10199
|
+
var PROJECT_ENDPOINTS_SCHEMA = "supacloud.project-endpoints.v1";
|
|
10200
|
+
var PROJECT_ENDPOINT_SOURCES = new Set([
|
|
10201
|
+
"explicit_api_domain",
|
|
10202
|
+
"explicit_auth_domain",
|
|
10203
|
+
"explicit_studio_domain",
|
|
10204
|
+
"custom_domain",
|
|
10205
|
+
"derived_api_domain",
|
|
10206
|
+
"generated"
|
|
10207
|
+
]);
|
|
10208
|
+
var ROOT_KEYS = new Set(["schema", "project_ref", "endpoints"]);
|
|
10209
|
+
var ENDPOINTS_KEYS = new Set(["api", "auth", "studio"]);
|
|
10210
|
+
var ENDPOINT_KEYS = new Set(["origin", "host", "scheme", "source", "aliases"]);
|
|
10211
|
+
var MAX_ALIASES = 64;
|
|
10212
|
+
function plainRecord2(candidate) {
|
|
10213
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
10214
|
+
return null;
|
|
10215
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
10216
|
+
return prototype === Object.prototype || prototype === null ? candidate : null;
|
|
10217
|
+
}
|
|
10218
|
+
function hasOnlyKeys2(record, allowedKeys) {
|
|
10219
|
+
return Object.keys(record).every((key) => allowedKeys.has(key));
|
|
10220
|
+
}
|
|
10221
|
+
function boundedText2(candidate, maxLength) {
|
|
10222
|
+
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) ? candidate : null;
|
|
10223
|
+
}
|
|
10224
|
+
function canonicalHost(candidate, scheme) {
|
|
10225
|
+
const host = boundedText2(candidate, 255);
|
|
10226
|
+
if (!host)
|
|
10227
|
+
return null;
|
|
10228
|
+
try {
|
|
10229
|
+
const parsed = new URL(`${scheme}://${host}`);
|
|
10230
|
+
return parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.host !== host ? null : host;
|
|
10231
|
+
} catch {
|
|
10232
|
+
return null;
|
|
10233
|
+
}
|
|
10234
|
+
}
|
|
10235
|
+
function projectEndpoint2(candidate) {
|
|
10236
|
+
const endpoint = plainRecord2(candidate);
|
|
10237
|
+
if (!endpoint || !hasOnlyKeys2(endpoint, ENDPOINT_KEYS))
|
|
10238
|
+
return null;
|
|
10239
|
+
const scheme = endpoint.scheme === "http" || endpoint.scheme === "https" ? endpoint.scheme : null;
|
|
10240
|
+
const origin = boundedText2(endpoint.origin, 2048);
|
|
10241
|
+
const source = boundedText2(endpoint.source, 64);
|
|
10242
|
+
if (!scheme || !origin || !source || !PROJECT_ENDPOINT_SOURCES.has(source))
|
|
10243
|
+
return null;
|
|
10244
|
+
let parsedOrigin;
|
|
10245
|
+
try {
|
|
10246
|
+
parsedOrigin = new URL(origin);
|
|
10247
|
+
} catch {
|
|
10248
|
+
return null;
|
|
10249
|
+
}
|
|
10250
|
+
if (parsedOrigin.protocol !== `${scheme}:` || parsedOrigin.origin !== origin || parsedOrigin.username || parsedOrigin.password || parsedOrigin.pathname !== "/" || parsedOrigin.search || parsedOrigin.hash)
|
|
10251
|
+
return null;
|
|
10252
|
+
const host = canonicalHost(endpoint.host, scheme);
|
|
10253
|
+
if (!host || host !== parsedOrigin.host || !Array.isArray(endpoint.aliases) || endpoint.aliases.length > MAX_ALIASES)
|
|
10254
|
+
return null;
|
|
10255
|
+
const aliases = [];
|
|
10256
|
+
const seenAliases = new Set;
|
|
10257
|
+
for (const aliasCandidate of endpoint.aliases) {
|
|
10258
|
+
const alias = canonicalHost(aliasCandidate, scheme);
|
|
10259
|
+
if (!alias || alias === host || seenAliases.has(alias))
|
|
10260
|
+
return null;
|
|
10261
|
+
seenAliases.add(alias);
|
|
10262
|
+
aliases.push(alias);
|
|
10263
|
+
}
|
|
10264
|
+
return { origin, host, scheme, source, aliases };
|
|
10265
|
+
}
|
|
10266
|
+
function projectEndpointProjection(candidate) {
|
|
10267
|
+
const projection = plainRecord2(candidate);
|
|
10268
|
+
if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !PROJECT_REF_PATTERN4.test(projection.project_ref))
|
|
10269
|
+
return null;
|
|
10270
|
+
const endpoints = plainRecord2(projection.endpoints);
|
|
10271
|
+
if (!endpoints || !hasOnlyKeys2(endpoints, ENDPOINTS_KEYS))
|
|
10272
|
+
return null;
|
|
10273
|
+
const api = projectEndpoint2(endpoints.api);
|
|
10274
|
+
const auth = projectEndpoint2(endpoints.auth);
|
|
10275
|
+
const studio = projectEndpoint2(endpoints.studio);
|
|
10276
|
+
return api && auth && studio ? {
|
|
10277
|
+
schema: PROJECT_ENDPOINTS_SCHEMA,
|
|
10278
|
+
project_ref: projection.project_ref,
|
|
10279
|
+
endpoints: { api, auth, studio }
|
|
10280
|
+
} : null;
|
|
10281
|
+
}
|
|
10282
|
+
function validHttpStatus2(status) {
|
|
10283
|
+
return Number.isSafeInteger(status) && status >= 100 && status <= 599;
|
|
10284
|
+
}
|
|
10285
|
+
function successfulResponse2(response) {
|
|
10286
|
+
return response.ok === true && validHttpStatus2(response.status) && response.status >= 200 && response.status <= 299;
|
|
10287
|
+
}
|
|
10288
|
+
function failedResult2(message) {
|
|
10289
|
+
return { text: `❌ ${message}`, isError: true };
|
|
10290
|
+
}
|
|
10291
|
+
function failedHttpResult2(label, status) {
|
|
10292
|
+
return failedResult2(validHttpStatus2(status) ? `${label} request failed (${status})` : `${label} request failed`);
|
|
10293
|
+
}
|
|
10294
|
+
function successfulResult2(payload) {
|
|
10295
|
+
return { text: JSON.stringify(payload, null, 2), isError: false };
|
|
10296
|
+
}
|
|
10297
|
+
function projectEndpointRead(response, expectedRef) {
|
|
10298
|
+
if (!successfulResponse2(response))
|
|
10299
|
+
return failedHttpResult2("Project endpoints", response.status);
|
|
10300
|
+
const projection = projectEndpointProjection(response.data);
|
|
10301
|
+
return projection && projection.project_ref === expectedRef ? successfulResult2(projection) : failedResult2("Invalid project endpoint response");
|
|
10302
|
+
}
|
|
10303
|
+
|
|
9982
10304
|
// src/shared/tools/project-cli-tools.ts
|
|
9983
10305
|
function projectReadResponse(readResult) {
|
|
9984
10306
|
return {
|
|
@@ -10112,12 +10434,17 @@ function resolveRef(refFromArgs, defaultRef) {
|
|
|
10112
10434
|
throw new Error("'ref' is required for this action");
|
|
10113
10435
|
return ref;
|
|
10114
10436
|
}
|
|
10437
|
+
function projectEndpointProjectionPath(ref) {
|
|
10438
|
+
return `/v1/projects/${encodeURIComponent(ref)}/endpoint/projection`;
|
|
10439
|
+
}
|
|
10115
10440
|
function registerUserProjectCliTools(server, http, options = {}) {
|
|
10116
|
-
const { projectRef } = options;
|
|
10441
|
+
const { projectRef: projectRef2 } = options;
|
|
10117
10442
|
server.tool("project", `Project-scoped inspection and developer operations.
|
|
10118
|
-
Actions: get, pause, restore, health, logs, api_keys, settings, tasks, task_detail, task_cancel, task_retry, task_stats, dlq, background_settings, update_background_settings`, {
|
|
10443
|
+
Actions: list (Admin guidance), get, endpoints, pause, restore, health, logs, api_keys, settings, tasks, task_detail, task_cancel, task_retry, task_stats, dlq, background_settings, update_background_settings`, {
|
|
10119
10444
|
action: withDescription(stringEnum([
|
|
10445
|
+
"list",
|
|
10120
10446
|
"get",
|
|
10447
|
+
"endpoints",
|
|
10121
10448
|
"pause",
|
|
10122
10449
|
"restore",
|
|
10123
10450
|
"health",
|
|
@@ -10133,20 +10460,37 @@ Actions: get, pause, restore, health, logs, api_keys, settings, tasks, task_deta
|
|
|
10133
10460
|
"background_settings",
|
|
10134
10461
|
"update_background_settings"
|
|
10135
10462
|
]), "Action to perform"),
|
|
10136
|
-
ref: optional(Type.String(),
|
|
10463
|
+
ref: optional(Type.String(), projectRef2 ? "Optional override when not auto-linked" : "Project ref"),
|
|
10137
10464
|
log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service"),
|
|
10138
10465
|
task_id: optional(Type.String(), "[task_detail/task_cancel/task_retry] Task ID"),
|
|
10139
10466
|
limit: optional(Type.Number(), "[tasks/dlq] Max items to return"),
|
|
10140
10467
|
concurrency: optional(Type.Number(), "[update_background_settings] Max concurrent background tasks"),
|
|
10141
10468
|
max_attempts: optional(Type.Number(), "[update_background_settings] Max attempts for background tasks")
|
|
10142
10469
|
}, async ({ action, ref, log_type, task_id, limit, concurrency, max_attempts }) => {
|
|
10143
|
-
|
|
10470
|
+
if (action === "list") {
|
|
10471
|
+
return {
|
|
10472
|
+
isError: true,
|
|
10473
|
+
content: [{
|
|
10474
|
+
type: "text",
|
|
10475
|
+
text: [
|
|
10476
|
+
"⚠️ Project enumeration is a platform administration operation.",
|
|
10477
|
+
"Use `supacloud-admin project list` with an admin Management API context."
|
|
10478
|
+
].join(`
|
|
10479
|
+
`)
|
|
10480
|
+
}]
|
|
10481
|
+
};
|
|
10482
|
+
}
|
|
10483
|
+
const resolvedRef = resolveRef(ref, projectRef2);
|
|
10144
10484
|
let text;
|
|
10145
10485
|
switch (action) {
|
|
10146
10486
|
case "get":
|
|
10147
10487
|
return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
|
|
10148
10488
|
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
10149
10489
|
}), resolvedRef));
|
|
10490
|
+
case "endpoints":
|
|
10491
|
+
return projectReadResponse(projectEndpointRead(await http.get(projectEndpointProjectionPath(resolvedRef), {
|
|
10492
|
+
maxResponseBytes: PROJECT_ENDPOINT_RESPONSE_MAX_BYTES
|
|
10493
|
+
}), resolvedRef));
|
|
10150
10494
|
case "pause":
|
|
10151
10495
|
text = simple(await http.post(`/v1/projects/${resolvedRef}/pause`), `Project ${resolvedRef} paused`);
|
|
10152
10496
|
break;
|
|
@@ -10311,7 +10655,7 @@ function resolveRef2(refFromArgs, defaultRef) {
|
|
|
10311
10655
|
return ref;
|
|
10312
10656
|
}
|
|
10313
10657
|
function registerQueueTools(server, http, options = {}) {
|
|
10314
|
-
const { projectRef } = options;
|
|
10658
|
+
const { projectRef: projectRef2 } = options;
|
|
10315
10659
|
server.tool("queue", `Message queue operations for task-based messaging.
|
|
10316
10660
|
Actions: list, stats, list_messages, dlq, get_message, send, receive, ack, release, fail, retry, delete_message, get_settings, update_settings`, {
|
|
10317
10661
|
action: withDescription(stringEnum([
|
|
@@ -10351,7 +10695,7 @@ Actions: list, stats, list_messages, dlq, get_message, send, receive, ack, relea
|
|
|
10351
10695
|
max_attempts_setting: optional(Type.Number(), "[update_settings] Max delivery attempts"),
|
|
10352
10696
|
rate_limit: optional(Type.Number(), "[update_settings] Rate limit per minute")
|
|
10353
10697
|
}, async (args) => {
|
|
10354
|
-
const resolvedRef = resolveRef2(args.ref,
|
|
10698
|
+
const resolvedRef = resolveRef2(args.ref, projectRef2);
|
|
10355
10699
|
const q = args.queue;
|
|
10356
10700
|
const need = (fields) => {
|
|
10357
10701
|
for (const f of fields) {
|
|
@@ -10551,7 +10895,7 @@ var redirectStatus = Type.Optional(Type.Union([
|
|
|
10551
10895
|
var ok2 = (res) => res.ok ? JSON.stringify(res.data, null, 2) : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
|
|
10552
10896
|
var simple2 = (res, msg) => res.ok ? `✅ ${msg}` : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
|
|
10553
10897
|
function registerGatewayTools(server, http, options = {}) {
|
|
10554
|
-
const { projectRef } = options;
|
|
10898
|
+
const { projectRef: projectRef2 } = options;
|
|
10555
10899
|
server.tool("gateway", `Gateway / Caddy 配置(通过 JSON Admin API 注入)。要求 admin 权限。
|
|
10556
10900
|
Actions: routes, upsert_route, update_route, delete_route, config, get_certificate, update_certificate, issue_certificate, deploy_certificate, rebuild, custom_hostname, set_custom_hostname, delete_custom_hostname, verify_custom_hostname`, {
|
|
10557
10901
|
action: withDescription(stringEnum([
|
|
@@ -10570,7 +10914,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
10570
10914
|
"delete_custom_hostname",
|
|
10571
10915
|
"verify_custom_hostname"
|
|
10572
10916
|
]), "Action"),
|
|
10573
|
-
ref: optional(Type.String(),
|
|
10917
|
+
ref: optional(Type.String(), projectRef2 ? "可选:覆盖自动关联的项目 ref" : "项目 ref"),
|
|
10574
10918
|
route_id: optional(Type.String(), "[upsert_route/update_route/delete_route] 路由 ID(字母/数字/_/-,1-64)"),
|
|
10575
10919
|
hosts: withDescription(stringArray, "[upsert_route/update_route] 主机名列表,逗号分隔或 JSON 数组(1-20)"),
|
|
10576
10920
|
paths: withDescription(stringArray, "[upsert_route/update_route] 路径列表,逗号分隔或 JSON 数组(1-32)"),
|
|
@@ -10605,7 +10949,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
10605
10949
|
custom_hostname: optional(Type.String(), "[set_custom_hostname] 自定义域名")
|
|
10606
10950
|
}, async (args) => {
|
|
10607
10951
|
const resolveRef3 = (override) => {
|
|
10608
|
-
const ref2 = override ||
|
|
10952
|
+
const ref2 = override || projectRef2;
|
|
10609
10953
|
if (!ref2)
|
|
10610
10954
|
throw new Error("'ref' is required for this action");
|
|
10611
10955
|
return ref2;
|
|
@@ -10828,8 +11172,8 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
10828
11172
|
}
|
|
10829
11173
|
|
|
10830
11174
|
// src/shared/tools/branch-tools.ts
|
|
10831
|
-
function resolveProjectRef(ref,
|
|
10832
|
-
const resolved = typeof ref === "string" && ref.trim() ? ref.trim() :
|
|
11175
|
+
function resolveProjectRef(ref, projectRef2) {
|
|
11176
|
+
const resolved = typeof ref === "string" && ref.trim() ? ref.trim() : projectRef2 || "";
|
|
10833
11177
|
if (!resolved)
|
|
10834
11178
|
throw new Error("'ref' is required for this action");
|
|
10835
11179
|
return resolved;
|
|
@@ -11281,14 +11625,14 @@ async function executeMigrationPush(request, runtime) {
|
|
|
11281
11625
|
const pushMigrations = runtime.getPushMigrations?.();
|
|
11282
11626
|
if (!pushMigrations)
|
|
11283
11627
|
return missingMigrationContextResult();
|
|
11284
|
-
const
|
|
11285
|
-
if (!
|
|
11628
|
+
const projectRef2 = request.ref || runtime.projectRef;
|
|
11629
|
+
if (!projectRef2)
|
|
11286
11630
|
return missingProjectRefResult();
|
|
11287
11631
|
const workdir = resolveExistingWorkdir(request.workdir, runtime.fallbackWorkdir);
|
|
11288
11632
|
const migrationDirectory = resolve3(workdir, request.dir || "supabase/migrations");
|
|
11289
11633
|
const migrationResponse = await pushMigrations({
|
|
11290
11634
|
action: "push_migrations",
|
|
11291
|
-
ref:
|
|
11635
|
+
ref: projectRef2,
|
|
11292
11636
|
dir: migrationDirectory,
|
|
11293
11637
|
dry_run: request.dry_run
|
|
11294
11638
|
});
|
|
@@ -12223,7 +12567,7 @@ var INVENTORY_MAX_BYTES = 1024 * 1024;
|
|
|
12223
12567
|
var MUTATION_MAX_BYTES = 64 * 1024;
|
|
12224
12568
|
var BACKUP_TIMEOUT_MS = 36 * 60000;
|
|
12225
12569
|
var RELEASE_READ_RESPONSE_TIMEOUT_MS = 5000;
|
|
12226
|
-
function
|
|
12570
|
+
function isRecord2(value) {
|
|
12227
12571
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12228
12572
|
}
|
|
12229
12573
|
function canonicalTimestamp3(value) {
|
|
@@ -12235,15 +12579,15 @@ function canonicalTimestamp3(value) {
|
|
|
12235
12579
|
function validProjectRef(ref) {
|
|
12236
12580
|
return SAFE_PROJECT_REF.test(ref);
|
|
12237
12581
|
}
|
|
12238
|
-
function backupBelongsToProject(backupId,
|
|
12239
|
-
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${
|
|
12582
|
+
function backupBelongsToProject(backupId, projectRef2) {
|
|
12583
|
+
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef2}_`);
|
|
12240
12584
|
}
|
|
12241
|
-
function verifiedBackup(value,
|
|
12242
|
-
if (!
|
|
12585
|
+
function verifiedBackup(value, projectRef2) {
|
|
12586
|
+
if (!isRecord2(value) || typeof value.backup_id !== "string" || !backupBelongsToProject(value.backup_id, projectRef2) || value.project_ref !== projectRef2 || typeof value.database !== "string" || !SAFE_DATABASE.test(value.database) || value.kind !== "logical-full" || !canonicalTimestamp3(value.created_at) || !canonicalTimestamp3(value.completed_at) || new Date(value.completed_at).valueOf() < new Date(value.created_at).valueOf() || typeof value.bytes !== "number" || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || typeof value.sha256 !== "string" || !SHA256.test(value.sha256))
|
|
12243
12587
|
return null;
|
|
12244
12588
|
return {
|
|
12245
12589
|
backup_id: value.backup_id,
|
|
12246
|
-
project_ref:
|
|
12590
|
+
project_ref: projectRef2,
|
|
12247
12591
|
database: value.database,
|
|
12248
12592
|
kind: "logical-full",
|
|
12249
12593
|
created_at: value.created_at,
|
|
@@ -12252,10 +12596,10 @@ function verifiedBackup(value, projectRef) {
|
|
|
12252
12596
|
sha256: value.sha256
|
|
12253
12597
|
};
|
|
12254
12598
|
}
|
|
12255
|
-
function backupInventory(value,
|
|
12256
|
-
if (!
|
|
12599
|
+
function backupInventory(value, projectRef2) {
|
|
12600
|
+
if (!isRecord2(value) || !Array.isArray(value.backups))
|
|
12257
12601
|
return null;
|
|
12258
|
-
const backups = value.backups.map((backup) => verifiedBackup(backup,
|
|
12602
|
+
const backups = value.backups.map((backup) => verifiedBackup(backup, projectRef2));
|
|
12259
12603
|
if (backups.some((backup) => backup === null))
|
|
12260
12604
|
return null;
|
|
12261
12605
|
const inventory = backups;
|
|
@@ -12286,23 +12630,23 @@ function newlyCreatedBackup(before, after) {
|
|
|
12286
12630
|
const additions = after.filter((backup) => !known.has(backup.backup_id));
|
|
12287
12631
|
return additions.length === 1 ? additions[0] : null;
|
|
12288
12632
|
}
|
|
12289
|
-
function restoreRequest(
|
|
12290
|
-
if (typeof backupId !== "string" || !backupBelongsToProject(backupId,
|
|
12633
|
+
function restoreRequest(projectRef2, backupId, expectedSha256, restoreConfirmation) {
|
|
12634
|
+
if (typeof backupId !== "string" || !backupBelongsToProject(backupId, projectRef2)) {
|
|
12291
12635
|
throw new Error("'backup_id' must identify a logical-full backup for 'ref'");
|
|
12292
12636
|
}
|
|
12293
12637
|
if (typeof expectedSha256 !== "string" || !SHA256.test(expectedSha256)) {
|
|
12294
12638
|
throw new Error("'expected_sha256' must be a lowercase SHA-256 digest");
|
|
12295
12639
|
}
|
|
12296
|
-
const confirmation = `RESTORE_PROJECT:${
|
|
12640
|
+
const confirmation = `RESTORE_PROJECT:${projectRef2}:${backupId}:${expectedSha256}`;
|
|
12297
12641
|
if (restoreConfirmation !== confirmation) {
|
|
12298
12642
|
throw new Error("'restore_confirmation' must exactly confirm the selected logical backup restore");
|
|
12299
12643
|
}
|
|
12300
12644
|
return { backup_id: backupId, expected_sha256: expectedSha256, confirmation };
|
|
12301
12645
|
}
|
|
12302
|
-
function endpoint(
|
|
12303
|
-
if (!validProjectRef(
|
|
12646
|
+
function endpoint(projectRef2) {
|
|
12647
|
+
if (!validProjectRef(projectRef2))
|
|
12304
12648
|
throw new Error("'ref' is invalid for release controls");
|
|
12305
|
-
return `/v1/projects/${encodeURIComponent(
|
|
12649
|
+
return `/v1/projects/${encodeURIComponent(projectRef2)}`;
|
|
12306
12650
|
}
|
|
12307
12651
|
function httpFailure(operation, response) {
|
|
12308
12652
|
if (response.responseReadError) {
|
|
@@ -12316,12 +12660,12 @@ function mutationFailure(operation, response) {
|
|
|
12316
12660
|
}
|
|
12317
12661
|
return releaseControlFailure(operation, "HTTP_ERROR", response.status);
|
|
12318
12662
|
}
|
|
12319
|
-
async function readInventory(http,
|
|
12320
|
-
const response = await http.get(`${endpoint(
|
|
12663
|
+
async function readInventory(http, projectRef2) {
|
|
12664
|
+
const response = await http.get(`${endpoint(projectRef2)}/database/backups/logical`, {
|
|
12321
12665
|
maxJsonBytes: INVENTORY_MAX_BYTES,
|
|
12322
12666
|
responseTimeoutMs: RELEASE_READ_RESPONSE_TIMEOUT_MS
|
|
12323
12667
|
});
|
|
12324
|
-
return { response, inventory: response.ok && response.status === 200 ? backupInventory(response.data,
|
|
12668
|
+
return { response, inventory: response.ok && response.status === 200 ? backupInventory(response.data, projectRef2) : null };
|
|
12325
12669
|
}
|
|
12326
12670
|
function readInventoryFailure(operation, read) {
|
|
12327
12671
|
if (!read.response.ok)
|
|
@@ -12332,7 +12676,7 @@ function readInventoryFailure(operation, read) {
|
|
|
12332
12676
|
return null;
|
|
12333
12677
|
}
|
|
12334
12678
|
function postgrestStatus(value) {
|
|
12335
|
-
if (!
|
|
12679
|
+
if (!isRecord2(value) || value.component !== "postgrest" || !["running", "stopped"].includes(String(value.desired)) || !["running", "stopped", "starting", "error"].includes(String(value.actual)) || !["healthy", "unhealthy", "unknown"].includes(String(value.health)))
|
|
12336
12680
|
return null;
|
|
12337
12681
|
return {
|
|
12338
12682
|
desired: value.desired,
|
|
@@ -12340,8 +12684,8 @@ function postgrestStatus(value) {
|
|
|
12340
12684
|
health: value.health
|
|
12341
12685
|
};
|
|
12342
12686
|
}
|
|
12343
|
-
async function readPostgrestStatus(http,
|
|
12344
|
-
const response = await http.get(`${endpoint(
|
|
12687
|
+
async function readPostgrestStatus(http, projectRef2) {
|
|
12688
|
+
const response = await http.get(`${endpoint(projectRef2)}/services/postgrest/status`, {
|
|
12345
12689
|
maxJsonBytes: MUTATION_MAX_BYTES,
|
|
12346
12690
|
responseTimeoutMs: RELEASE_READ_RESPONSE_TIMEOUT_MS
|
|
12347
12691
|
});
|
|
@@ -12353,7 +12697,7 @@ function readPostgrestFailure(operation, read) {
|
|
|
12353
12697
|
return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
|
|
12354
12698
|
}
|
|
12355
12699
|
function isRestartReceipt(value) {
|
|
12356
|
-
return
|
|
12700
|
+
return isRecord2(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
|
|
12357
12701
|
}
|
|
12358
12702
|
function registerReleaseTools(server, http, options = {}) {
|
|
12359
12703
|
server.tool("release", "Verified release controls using a Management API credential. Actions: logical_backup_list, logical_backup_create, logical_backup_restore, postgrest_status, postgrest_restart", {
|
|
@@ -12369,45 +12713,45 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12369
12713
|
expected_sha256: optional(Type.String(), "[logical_backup_restore] Exact lowercase SHA-256 from the selected project inventory"),
|
|
12370
12714
|
restore_confirmation: optional(Type.String(), "[logical_backup_restore] Exact RESTORE_PROJECT:<ref>:<backup_id>:<sha256> confirmation")
|
|
12371
12715
|
}, async ({ action, ref, backup_id, expected_sha256, restore_confirmation }) => {
|
|
12372
|
-
const
|
|
12373
|
-
if (!
|
|
12716
|
+
const projectRef2 = typeof ref === "string" && ref || options.projectRef;
|
|
12717
|
+
if (!projectRef2)
|
|
12374
12718
|
throw new Error("'ref' is required for release controls");
|
|
12375
|
-
if (!validProjectRef(
|
|
12719
|
+
if (!validProjectRef(projectRef2))
|
|
12376
12720
|
throw new Error("'ref' is invalid for release controls");
|
|
12377
12721
|
if (action === "logical_backup_list") {
|
|
12378
|
-
const read2 = await readInventory(http,
|
|
12722
|
+
const read2 = await readInventory(http, projectRef2);
|
|
12379
12723
|
const failure = readInventoryFailure("release.logical_backup.list", read2);
|
|
12380
12724
|
return failure ?? releaseControlSuccess("release.logical_backup.list", {
|
|
12381
|
-
project_ref:
|
|
12725
|
+
project_ref: projectRef2,
|
|
12382
12726
|
backups: read2.inventory.map(publicBackup)
|
|
12383
12727
|
});
|
|
12384
12728
|
}
|
|
12385
12729
|
if (action === "logical_backup_create") {
|
|
12386
|
-
const before = await readInventory(http,
|
|
12730
|
+
const before = await readInventory(http, projectRef2);
|
|
12387
12731
|
const beforeFailure = readInventoryFailure("release.logical_backup.create", before);
|
|
12388
12732
|
if (beforeFailure)
|
|
12389
12733
|
return beforeFailure;
|
|
12390
|
-
const mutation2 = await http.postReleaseMutation(`${endpoint(
|
|
12734
|
+
const mutation2 = await http.postReleaseMutation(`${endpoint(projectRef2)}/database/backups/logical`, {}, {
|
|
12391
12735
|
timeoutMs: BACKUP_TIMEOUT_MS
|
|
12392
12736
|
});
|
|
12393
|
-
const after = await readInventory(http,
|
|
12737
|
+
const after = await readInventory(http, projectRef2);
|
|
12394
12738
|
if (!mutation2.ok || mutation2.status !== 200) {
|
|
12395
12739
|
return mutationFailure("release.logical_backup.create", mutation2);
|
|
12396
12740
|
}
|
|
12397
|
-
const responseBackup =
|
|
12741
|
+
const responseBackup = isRecord2(mutation2.data) ? verifiedBackup(mutation2.data.backup, projectRef2) : null;
|
|
12398
12742
|
const afterFailure = readInventoryFailure("release.logical_backup.create", after);
|
|
12399
12743
|
const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
|
|
12400
12744
|
if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
|
|
12401
12745
|
return releaseControlFailure("release.logical_backup.create", "OUTCOME_UNKNOWN", mutation2.status);
|
|
12402
12746
|
}
|
|
12403
12747
|
return releaseControlSuccess("release.logical_backup.create", {
|
|
12404
|
-
project_ref:
|
|
12748
|
+
project_ref: projectRef2,
|
|
12405
12749
|
backup: publicBackup(addedBackup)
|
|
12406
12750
|
});
|
|
12407
12751
|
}
|
|
12408
12752
|
if (action === "logical_backup_restore") {
|
|
12409
|
-
const request = restoreRequest(
|
|
12410
|
-
const before = await readInventory(http,
|
|
12753
|
+
const request = restoreRequest(projectRef2, backup_id, expected_sha256, restore_confirmation);
|
|
12754
|
+
const before = await readInventory(http, projectRef2);
|
|
12411
12755
|
const beforeFailure = readInventoryFailure("release.logical_backup.restore", before);
|
|
12412
12756
|
if (beforeFailure)
|
|
12413
12757
|
return beforeFailure;
|
|
@@ -12415,43 +12759,43 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12415
12759
|
if (!selectedBackup) {
|
|
12416
12760
|
return releaseControlFailure("release.logical_backup.restore", "MUTATION_NOT_SUCCEEDED", null);
|
|
12417
12761
|
}
|
|
12418
|
-
const mutation2 = await http.postReleaseMutation(`${endpoint(
|
|
12762
|
+
const mutation2 = await http.postReleaseMutation(`${endpoint(projectRef2)}/database/backups/logical/restore`, request, { timeoutMs: BACKUP_TIMEOUT_MS });
|
|
12419
12763
|
if (!mutation2.ok || mutation2.status !== 200) {
|
|
12420
12764
|
return mutationFailure("release.logical_backup.restore", mutation2);
|
|
12421
12765
|
}
|
|
12422
|
-
const responseBackup =
|
|
12423
|
-
const after = await readInventory(http,
|
|
12766
|
+
const responseBackup = isRecord2(mutation2.data) ? verifiedBackup(mutation2.data.restored_backup, projectRef2) : null;
|
|
12767
|
+
const after = await readInventory(http, projectRef2);
|
|
12424
12768
|
const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
|
|
12425
12769
|
const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
|
|
12426
12770
|
if (!responseBackup || !equalBackup(responseBackup, selectedBackup) || afterFailure || !restoredInventoryBackup || !equalBackup(restoredInventoryBackup, selectedBackup)) {
|
|
12427
12771
|
return releaseControlFailure("release.logical_backup.restore", "OUTCOME_UNKNOWN", mutation2.status);
|
|
12428
12772
|
}
|
|
12429
12773
|
return releaseControlSuccess("release.logical_backup.restore", {
|
|
12430
|
-
project_ref:
|
|
12774
|
+
project_ref: projectRef2,
|
|
12431
12775
|
backup: publicBackup(selectedBackup)
|
|
12432
12776
|
});
|
|
12433
12777
|
}
|
|
12434
12778
|
if (action === "postgrest_status") {
|
|
12435
|
-
const read2 = await readPostgrestStatus(http,
|
|
12779
|
+
const read2 = await readPostgrestStatus(http, projectRef2);
|
|
12436
12780
|
const failure = readPostgrestFailure("release.postgrest.status", read2);
|
|
12437
12781
|
return failure ?? releaseControlSuccess("release.postgrest.status", {
|
|
12438
|
-
project_ref:
|
|
12782
|
+
project_ref: projectRef2,
|
|
12439
12783
|
postgrest: read2.status
|
|
12440
12784
|
});
|
|
12441
12785
|
}
|
|
12442
12786
|
if (action !== "postgrest_restart")
|
|
12443
12787
|
throw new Error("Unknown release control action");
|
|
12444
|
-
const mutation = await http.postReleaseMutation(`${endpoint(
|
|
12445
|
-
const read = await readPostgrestStatus(http,
|
|
12788
|
+
const mutation = await http.postReleaseMutation(`${endpoint(projectRef2)}/services/postgrest/restart`);
|
|
12789
|
+
const read = await readPostgrestStatus(http, projectRef2);
|
|
12446
12790
|
if (!mutation.ok || mutation.status !== 200) {
|
|
12447
12791
|
return mutationFailure("release.postgrest.restart", mutation);
|
|
12448
12792
|
}
|
|
12449
|
-
const
|
|
12450
|
-
if (!isRestartReceipt(mutation.data) ||
|
|
12793
|
+
const readFailure2 = readPostgrestFailure("release.postgrest.restart", read);
|
|
12794
|
+
if (!isRestartReceipt(mutation.data) || readFailure2 || read.status.desired !== "running" || read.status.actual !== "running" || read.status.health !== "healthy") {
|
|
12451
12795
|
return releaseControlFailure("release.postgrest.restart", "OUTCOME_UNKNOWN", mutation.status);
|
|
12452
12796
|
}
|
|
12453
12797
|
return releaseControlSuccess("release.postgrest.restart", {
|
|
12454
|
-
project_ref:
|
|
12798
|
+
project_ref: projectRef2,
|
|
12455
12799
|
postgrest: read.status
|
|
12456
12800
|
});
|
|
12457
12801
|
});
|
|
@@ -12459,7 +12803,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12459
12803
|
// package.json
|
|
12460
12804
|
var package_default = {
|
|
12461
12805
|
name: "@supacloud/cli",
|
|
12462
|
-
version: "0.
|
|
12806
|
+
version: "0.27.0",
|
|
12463
12807
|
description: "Project-scoped CLI for SupaCloud users",
|
|
12464
12808
|
type: "module",
|
|
12465
12809
|
main: "./dist/index.js",
|
|
@@ -12801,7 +13145,7 @@ function createCliTools(context, confirmProduction) {
|
|
|
12801
13145
|
]
|
|
12802
13146
|
})
|
|
12803
13147
|
};
|
|
12804
|
-
for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch", "release"]) {
|
|
13148
|
+
for (const name of ["database", "auth", "oauth_clients", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "mutations", "diagnostics", "gateway", "branch", "release"]) {
|
|
12805
13149
|
tools[name] = {
|
|
12806
13150
|
schema: { action: genericActionSchema },
|
|
12807
13151
|
callback: async () => ({
|
|
@@ -12878,6 +13222,7 @@ function createCliTools(context, confirmProduction) {
|
|
|
12878
13222
|
pushMigrations = databaseTools.database?.callback;
|
|
12879
13223
|
assign(databaseTools);
|
|
12880
13224
|
assign(captureTools((server) => registerAuthTools(server, http)));
|
|
13225
|
+
assign(captureTools((server) => registerOAuthClientTools(server, http)));
|
|
12881
13226
|
assign(captureTools((server) => registerStorageTools(server, http)));
|
|
12882
13227
|
assign(captureTools((server) => registerAdvancedTools(server, http, process.env, {
|
|
12883
13228
|
readOnly: context.readOnly
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: supacloud-cli
|
|
3
|
-
description: Use when operating, implementing, diagnosing, deploying, or documenting a SupaCloud project through supacloud-cli, especially database schema, functions/RPC, triggers, RLS, indexes, grants, extensions, migrations, backups, auth, storage, Edge Functions, frontend, queues, task events, diagnostics, or gateway work. Also use when an AI might otherwise call SQL, psql, a database API, or the Management API directly.
|
|
3
|
+
description: Use when operating, implementing, diagnosing, deploying, or documenting a SupaCloud project through supacloud-cli, especially project endpoint discovery, database schema, functions/RPC, triggers, RLS, indexes, grants, extensions, migrations, backups, auth, storage, Edge Functions, frontend, queues, task events, diagnostics, or gateway work. Also use when an AI might otherwise call SQL, psql, a database API, or the Management API directly.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# SupaCloud CLI
|
|
@@ -24,6 +24,8 @@ Use `supacloud-cli` as the project-level control surface and keep durable change
|
|
|
24
24
|
5. Run a remote migration dry-run before apply. Production apply requires explicit user approval in the current task.
|
|
25
25
|
6. Do not edit `supabase_migrations.schema_migrations` through ordinary SQL. Migration history is an application ledger, not a schema backup or source of truth. For a proven-equivalent historical baseline, use the controlled `database baseline_migrations` action with dry-run and explicit approval.
|
|
26
26
|
7. Service-role credentials authenticate the SupaCloud Management API. Never reinterpret them as PostgreSQL passwords or forward them to the official Supabase CLI.
|
|
27
|
+
8. Read project domains through `supacloud-cli project endpoints`. Do not infer API, Auth, or Studio origins by concatenating project refs and base domains. The projection reports configuration, not DNS/certificate/runtime readiness.
|
|
28
|
+
9. Cross-project enumeration is an Admin boundary. Use `supacloud-admin project list` or `supacloud-admin project list_endpoints`; do not attempt to widen project credentials.
|
|
27
29
|
|
|
28
30
|
## Workflow
|
|
29
31
|
|
|
@@ -36,6 +38,20 @@ Use `supacloud-cli` as the project-level control surface and keep durable change
|
|
|
36
38
|
7. Apply only within the user-authorized environment and scope.
|
|
37
39
|
8. Read back migration history and affected resources; report exact evidence and any remaining drift.
|
|
38
40
|
|
|
41
|
+
## Project endpoint inspection
|
|
42
|
+
|
|
43
|
+
For the selected project:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
supacloud-cli status
|
|
47
|
+
supacloud-cli project endpoints
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The fixed projection contains credential-free API/Auth/Studio origins, hosts,
|
|
51
|
+
schemes, source classifications, and API aliases. Follow it with the relevant
|
|
52
|
+
health or gateway command before claiming that DNS, TLS, routing, or a runtime is
|
|
53
|
+
ready. Project-wide or fleet-wide inventories belong to `supacloud-admin`.
|
|
54
|
+
|
|
39
55
|
## Database default
|
|
40
56
|
|
|
41
57
|
For a new database change:
|
|
@@ -66,8 +82,8 @@ supacloud-cli database baseline_migrations \
|
|
|
66
82
|
|
|
67
83
|
## CLI boundaries
|
|
68
84
|
|
|
69
|
-
- `supacloud-cli`: project status, database, migrations, auth, storage, Edge Functions, frontend, queues, task events, diagnostics, and project gateway configuration.
|
|
70
|
-
- `supacloud-admin`: installation, upgrades, SSH diagnostics, platform-wide project lifecycle, tenant runtime, and server operations.
|
|
85
|
+
- `supacloud-cli`: selected-project status and endpoint projection, database, migrations, auth, storage, Edge Functions, frontend, queues, task events, diagnostics, and project gateway configuration.
|
|
86
|
+
- `supacloud-admin`: installation, upgrades, SSH diagnostics, platform-wide project/endpoint inventory, project lifecycle, tenant runtime, and server operations.
|
|
71
87
|
- Official `supabase` CLI: invoked only through the allowlisted `supacloud-cli supabase` adapter for supported local authoring or explicit-DSN inspection commands.
|
|
72
88
|
- Direct HTTP/SQL: read-only diagnosis or an explicitly approved break-glass path; never the default implementation path.
|
|
73
89
|
|
|
@@ -7,6 +7,8 @@ Load this reference when selecting a command surface or when a user asks an AI t
|
|
|
7
7
|
| Intent | Use | Guardrail |
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| Inspect current project binding | `supacloud-cli status` | Read-only; run first |
|
|
10
|
+
| Inspect selected project API/Auth/Studio origins | `supacloud-cli project endpoints` | Uses the Management API's authoritative projection; do not reconstruct domains locally |
|
|
11
|
+
| Enumerate projects or endpoint projections | `supacloud-admin project list` / `project list_endpoints` | Platform-wide read; never promote a project credential to Admin authority |
|
|
10
12
|
| Inspect project health/logs/tasks | `project`, `queue`, `task_events`, `diagnostics` | Prefer bounded reads |
|
|
11
13
|
| Read database rows or metadata | `database query` and database inspection actions | `SELECT`/read-only by default |
|
|
12
14
|
| Create schema/function/RPC/trigger/RLS/index/grant/extension | `supabase migration_new`, then edit SQL | Never direct remote DDL |
|
|
@@ -26,7 +28,7 @@ until a project-scoped context is resolved.
|
|
|
26
28
|
## Command groups
|
|
27
29
|
|
|
28
30
|
- `status`: resolved context, Management API connectivity, authentication, and project reachability.
|
|
29
|
-
- `project`: project metadata, health, logs, API keys/settings, background tasks, retry/cancel, DLQ, and background settings.
|
|
31
|
+
- `project`: selected-project metadata, authoritative endpoint projection, health, logs, API keys/settings, background tasks, retry/cancel, DLQ, and background settings. `project list` deliberately redirects to `supacloud-admin`; cross-project enumeration is not a project CLI capability.
|
|
30
32
|
- `database`: read/query, schema inspection, extensions, indexes, RLS, stats, migration push, controlled historical baseline, and SQL-file execution.
|
|
31
33
|
- `supabase`: allowlisted official CLI adapter for migration authoring, local reset/diff, explicit-DSN inspection/backup/type generation, and SupaCloud-controlled migration push.
|
|
32
34
|
- `auth`: provider and authentication configuration.
|
|
@@ -43,8 +45,14 @@ until a project-scoped context is resolved.
|
|
|
43
45
|
```bash
|
|
44
46
|
supacloud-cli status
|
|
45
47
|
supacloud-cli project get
|
|
48
|
+
supacloud-cli project endpoints
|
|
46
49
|
supacloud-cli project health
|
|
47
50
|
supacloud-cli supabase migration_list --db_url "$SUPACLOUD_DB_URL"
|
|
48
51
|
```
|
|
49
52
|
|
|
53
|
+
The endpoint projection returns bounded, credential-free API/Auth/Studio origins,
|
|
54
|
+
canonical hosts, URL schemes, configuration sources, and API aliases. It does
|
|
55
|
+
not assert DNS, certificate, or runtime readiness; use the relevant health and
|
|
56
|
+
gateway inspection commands for those checks.
|
|
57
|
+
|
|
50
58
|
Do not paste the DSN value into chat or commit it to shell scripts. Prefer an environment variable supplied outside the repository.
|