@supacloud/cli 0.26.0 → 0.28.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"],
|
|
@@ -6470,8 +6471,8 @@ var ACTION_POLICY = {
|
|
|
6470
6471
|
write: ["push"]
|
|
6471
6472
|
},
|
|
6472
6473
|
auth: {
|
|
6473
|
-
read: ["list_providers", "get_provider", "supported_providers", "get_settings", "get_config"],
|
|
6474
|
-
write: ["configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config"]
|
|
6474
|
+
read: ["list_users", "get_user", "list_providers", "get_provider", "supported_providers", "get_settings", "get_config"],
|
|
6475
|
+
write: ["generate_link", "configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config"]
|
|
6475
6476
|
},
|
|
6476
6477
|
oauth_clients: {
|
|
6477
6478
|
read: ["list", "get"],
|
|
@@ -7967,6 +7968,25 @@ var safeAuthMutationCodes = new Set([
|
|
|
7967
7968
|
"AUTH_RUNTIME_APPLY_FAILED",
|
|
7968
7969
|
"SUPAUTH_DEPENDENT_REFRESH_FAILED"
|
|
7969
7970
|
]);
|
|
7971
|
+
var MAX_AUTH_READ_BYTES = 64 * 1024;
|
|
7972
|
+
var AUTH_READ_TIMEOUT_MS = 5000;
|
|
7973
|
+
var USER_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7974
|
+
var AUTH_LINK_TYPES = [
|
|
7975
|
+
"signup",
|
|
7976
|
+
"magiclink",
|
|
7977
|
+
"recovery",
|
|
7978
|
+
"invite",
|
|
7979
|
+
"email_change",
|
|
7980
|
+
"email_change_current",
|
|
7981
|
+
"email_change_new"
|
|
7982
|
+
];
|
|
7983
|
+
var SAFE_USER_FIELDS = [
|
|
7984
|
+
"id",
|
|
7985
|
+
"email",
|
|
7986
|
+
"phone",
|
|
7987
|
+
"created_at",
|
|
7988
|
+
"last_sign_in_at"
|
|
7989
|
+
];
|
|
7970
7990
|
function parseAuthConfig(input) {
|
|
7971
7991
|
if (typeof input !== "string")
|
|
7972
7992
|
return input;
|
|
@@ -8018,6 +8038,162 @@ function authMutationResult(response, successMessage) {
|
|
|
8018
8038
|
}]
|
|
8019
8039
|
};
|
|
8020
8040
|
}
|
|
8041
|
+
function requiredRef(candidate) {
|
|
8042
|
+
if (typeof candidate !== "string" || !candidate.trim())
|
|
8043
|
+
throw new Error("'ref' is required");
|
|
8044
|
+
return projectRefPathSegment(candidate.trim(), "Auth");
|
|
8045
|
+
}
|
|
8046
|
+
function requiredUserId(candidate) {
|
|
8047
|
+
if (typeof candidate !== "string" || !USER_ID_PATTERN.test(candidate.trim())) {
|
|
8048
|
+
throw new Error("'user_id' must be a UUID");
|
|
8049
|
+
}
|
|
8050
|
+
return candidate.trim().toLowerCase();
|
|
8051
|
+
}
|
|
8052
|
+
function boundedPage(candidate) {
|
|
8053
|
+
if (candidate === undefined)
|
|
8054
|
+
return 1;
|
|
8055
|
+
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < 1) {
|
|
8056
|
+
throw new Error("'page' must be a positive integer");
|
|
8057
|
+
}
|
|
8058
|
+
return candidate;
|
|
8059
|
+
}
|
|
8060
|
+
function boundedPerPage(candidate) {
|
|
8061
|
+
if (candidate === undefined)
|
|
8062
|
+
return 50;
|
|
8063
|
+
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < 1 || candidate > 100) {
|
|
8064
|
+
throw new Error("'per_page' must be an integer between 1 and 100");
|
|
8065
|
+
}
|
|
8066
|
+
return candidate;
|
|
8067
|
+
}
|
|
8068
|
+
function safeRedirectTo(candidate) {
|
|
8069
|
+
if (candidate === undefined)
|
|
8070
|
+
return;
|
|
8071
|
+
if (typeof candidate !== "string" || !candidate.trim())
|
|
8072
|
+
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL");
|
|
8073
|
+
let uri;
|
|
8074
|
+
try {
|
|
8075
|
+
uri = new URL(candidate.trim());
|
|
8076
|
+
} catch {
|
|
8077
|
+
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL");
|
|
8078
|
+
}
|
|
8079
|
+
const loopback = uri.hostname === "127.0.0.1" || uri.hostname === "[::1]";
|
|
8080
|
+
const validProtocol = uri.protocol === "https:" || uri.protocol === "http:" && loopback && Boolean(uri.port);
|
|
8081
|
+
if (!validProtocol || uri.username || uri.password || uri.hash) {
|
|
8082
|
+
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL without credentials or fragment");
|
|
8083
|
+
}
|
|
8084
|
+
return uri.toString();
|
|
8085
|
+
}
|
|
8086
|
+
function isRecord(candidate) {
|
|
8087
|
+
return candidate !== null && typeof candidate === "object" && !Array.isArray(candidate);
|
|
8088
|
+
}
|
|
8089
|
+
function projectUser(candidate) {
|
|
8090
|
+
if (!isRecord(candidate) || typeof candidate.id !== "string")
|
|
8091
|
+
return null;
|
|
8092
|
+
const projectedUser = {};
|
|
8093
|
+
for (const field of SAFE_USER_FIELDS) {
|
|
8094
|
+
if (field in candidate)
|
|
8095
|
+
projectedUser[field] = candidate[field];
|
|
8096
|
+
}
|
|
8097
|
+
return projectedUser;
|
|
8098
|
+
}
|
|
8099
|
+
function projectUserList(candidate) {
|
|
8100
|
+
if (!isRecord(candidate) || !Array.isArray(candidate.users))
|
|
8101
|
+
return null;
|
|
8102
|
+
const users = candidate.users.map(projectUser);
|
|
8103
|
+
if (users.some((user) => user === null))
|
|
8104
|
+
return null;
|
|
8105
|
+
const projectedFields = { users };
|
|
8106
|
+
for (const field of ["total", "page", "per_page", "next_page", "last_page"]) {
|
|
8107
|
+
if (field in candidate && (typeof candidate[field] === "number" || candidate[field] === null)) {
|
|
8108
|
+
projectedFields[field] = candidate[field];
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8111
|
+
return projectedFields;
|
|
8112
|
+
}
|
|
8113
|
+
function actionLink(candidate) {
|
|
8114
|
+
const candidates = [candidate];
|
|
8115
|
+
if (isRecord(candidate)) {
|
|
8116
|
+
candidates.push(candidate.data, candidate.properties);
|
|
8117
|
+
if (isRecord(candidate.data))
|
|
8118
|
+
candidates.push(candidate.data.properties);
|
|
8119
|
+
}
|
|
8120
|
+
for (const candidate2 of candidates) {
|
|
8121
|
+
if (isRecord(candidate2) && typeof candidate2.action_link === "string" && candidate2.action_link.length > 0) {
|
|
8122
|
+
return candidate2.action_link;
|
|
8123
|
+
}
|
|
8124
|
+
}
|
|
8125
|
+
return null;
|
|
8126
|
+
}
|
|
8127
|
+
function safeAuthReadFailure(operation, response) {
|
|
8128
|
+
return {
|
|
8129
|
+
isError: true,
|
|
8130
|
+
content: [{
|
|
8131
|
+
type: "text",
|
|
8132
|
+
text: JSON.stringify({
|
|
8133
|
+
ok: false,
|
|
8134
|
+
operation,
|
|
8135
|
+
http_status: response.transportError || response.responseReadError ? null : response.status,
|
|
8136
|
+
error: response.responseReadError ? "INVALID_RESPONSE" : response.transportError ? "NETWORK_ERROR" : "HTTP_ERROR"
|
|
8137
|
+
}, null, 2)
|
|
8138
|
+
}]
|
|
8139
|
+
};
|
|
8140
|
+
}
|
|
8141
|
+
async function listUsers(http, args) {
|
|
8142
|
+
const ref = requiredRef(args.ref);
|
|
8143
|
+
const page = boundedPage(args.page);
|
|
8144
|
+
const perPage = boundedPerPage(args.per_page);
|
|
8145
|
+
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) });
|
|
8146
|
+
for (const key of ["search", "email_like"]) {
|
|
8147
|
+
if (args[key] !== undefined) {
|
|
8148
|
+
if (typeof args[key] !== "string" || !args[key].trim())
|
|
8149
|
+
throw new Error(`'${key}' must be a non-empty string`);
|
|
8150
|
+
params.set(key, args[key].trim());
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
const response = await http.get(`/v1/projects/${ref}/auth/users?${params.toString()}`, {
|
|
8154
|
+
maxJsonBytes: MAX_AUTH_READ_BYTES,
|
|
8155
|
+
responseTimeoutMs: AUTH_READ_TIMEOUT_MS
|
|
8156
|
+
});
|
|
8157
|
+
if (!response.ok)
|
|
8158
|
+
return safeAuthReadFailure("auth.list_users", response);
|
|
8159
|
+
const users = projectUserList(response.data);
|
|
8160
|
+
if (!users)
|
|
8161
|
+
return safeAuthReadFailure("auth.list_users", { ...response, responseReadError: true });
|
|
8162
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.list_users", project_ref: ref, ...users }, null, 2) }] };
|
|
8163
|
+
}
|
|
8164
|
+
async function getUser(http, args) {
|
|
8165
|
+
const ref = requiredRef(args.ref);
|
|
8166
|
+
const userId = requiredUserId(args.user_id);
|
|
8167
|
+
const response = await http.get(`/v1/projects/${ref}/auth/users/${encodeURIComponent(userId)}`, {
|
|
8168
|
+
maxJsonBytes: MAX_AUTH_READ_BYTES,
|
|
8169
|
+
responseTimeoutMs: AUTH_READ_TIMEOUT_MS
|
|
8170
|
+
});
|
|
8171
|
+
if (!response.ok)
|
|
8172
|
+
return safeAuthReadFailure("auth.get_user", response);
|
|
8173
|
+
const user = projectUser(response.data);
|
|
8174
|
+
if (!user)
|
|
8175
|
+
return safeAuthReadFailure("auth.get_user", { ...response, responseReadError: true });
|
|
8176
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.get_user", project_ref: ref, user }, null, 2) }] };
|
|
8177
|
+
}
|
|
8178
|
+
async function generateLink(http, args) {
|
|
8179
|
+
const ref = requiredRef(args.ref);
|
|
8180
|
+
if (typeof args.type !== "string" || !AUTH_LINK_TYPES.includes(args.type)) {
|
|
8181
|
+
throw new Error("'type' is invalid for 'generate_link'");
|
|
8182
|
+
}
|
|
8183
|
+
if (typeof args.email !== "string" || !args.email.trim())
|
|
8184
|
+
throw new Error("'email' is required for 'generate_link'");
|
|
8185
|
+
const body = { type: args.type, email: args.email.trim() };
|
|
8186
|
+
const redirectTo = safeRedirectTo(args.redirect_to);
|
|
8187
|
+
if (redirectTo)
|
|
8188
|
+
body.redirect_to = redirectTo;
|
|
8189
|
+
const response = await http.postReleaseMutation(`/v1/projects/${ref}/auth/generate_link`, body);
|
|
8190
|
+
if (!response.ok)
|
|
8191
|
+
return safeAuthReadFailure("auth.generate_link", response);
|
|
8192
|
+
const link = actionLink(response.data);
|
|
8193
|
+
if (!link)
|
|
8194
|
+
return safeAuthReadFailure("auth.generate_link", { ...response, responseReadError: true });
|
|
8195
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.generate_link", action_link: link }, null, 2) }] };
|
|
8196
|
+
}
|
|
8021
8197
|
function formatProviders(data) {
|
|
8022
8198
|
if (!data || typeof data !== "object")
|
|
8023
8199
|
return JSON.stringify(data, null, 2);
|
|
@@ -8050,9 +8226,12 @@ function formatProviders(data) {
|
|
|
8050
8226
|
return out;
|
|
8051
8227
|
}
|
|
8052
8228
|
function registerAuthTools(server, http) {
|
|
8053
|
-
server.tool("auth", `Auth & OAuth provider management.
|
|
8054
|
-
Actions: list_providers, get_provider, configure_provider, update_provider, disable_provider, supported_providers, wechat_mini, wechat_open, get_settings, update_settings, get_config, update_config`, {
|
|
8229
|
+
server.tool("auth", `Auth & OAuth provider management, controlled user lookup, and login-link generation.
|
|
8230
|
+
Actions: list_users, get_user, generate_link, list_providers, get_provider, configure_provider, update_provider, disable_provider, supported_providers, wechat_mini, wechat_open, get_settings, update_settings, get_config, update_config`, {
|
|
8055
8231
|
action: withDescription(stringEnum([
|
|
8232
|
+
"list_users",
|
|
8233
|
+
"get_user",
|
|
8234
|
+
"generate_link",
|
|
8056
8235
|
"list_providers",
|
|
8057
8236
|
"get_provider",
|
|
8058
8237
|
"configure_provider",
|
|
@@ -8067,6 +8246,14 @@ Actions: list_providers, get_provider, configure_provider, update_provider, disa
|
|
|
8067
8246
|
"update_config"
|
|
8068
8247
|
]), "Action to perform"),
|
|
8069
8248
|
ref: optional(Type.String(), "Project ref (required for most actions)"),
|
|
8249
|
+
user_id: optional(Type.String(), "[get_user] Exact auth user UUID"),
|
|
8250
|
+
page: optional(Type.Integer({ minimum: 1 }), "[list_users] 1-based page"),
|
|
8251
|
+
per_page: optional(Type.Integer({ minimum: 1, maximum: 100 }), "[list_users] Users per page (1-100)"),
|
|
8252
|
+
search: optional(Type.String(), "[list_users] Search user email, phone, or UUID"),
|
|
8253
|
+
email_like: optional(Type.String(), "[list_users] Search user email or phone"),
|
|
8254
|
+
type: optional(stringEnum(AUTH_LINK_TYPES), "[generate_link] GoTrue link type"),
|
|
8255
|
+
email: optional(Type.String(), "[generate_link] User email"),
|
|
8256
|
+
redirect_to: optional(Type.String(), "[generate_link] Absolute HTTPS or loopback callback"),
|
|
8070
8257
|
provider: optional(Type.String(), "[*_provider] Provider name (github, google, wechat, etc.)"),
|
|
8071
8258
|
client_id: optional(Type.String(), "[configure/update] OAuth Client ID"),
|
|
8072
8259
|
client_secret: optional(Type.String(), "[configure/update] OAuth Client Secret"),
|
|
@@ -8084,6 +8271,12 @@ Actions: list_providers, get_provider, configure_provider, update_provider, disa
|
|
|
8084
8271
|
const ok = (res) => res.ok ? JSON.stringify(res.data, null, 2) : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
|
|
8085
8272
|
let text;
|
|
8086
8273
|
switch (action) {
|
|
8274
|
+
case "list_users":
|
|
8275
|
+
return listUsers(http, args);
|
|
8276
|
+
case "get_user":
|
|
8277
|
+
return getUser(http, args);
|
|
8278
|
+
case "generate_link":
|
|
8279
|
+
return generateLink(http, args);
|
|
8087
8280
|
case "list_providers":
|
|
8088
8281
|
need("ref");
|
|
8089
8282
|
const lp = await http.get(`/v1/projects/${ref}/auth/providers`);
|
|
@@ -8179,7 +8372,7 @@ var RELEASE_CANARY_CLIENT_NAME = "supacloud-release-canary";
|
|
|
8179
8372
|
var CLIENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,256}$/;
|
|
8180
8373
|
var MAX_CLIENT_LIST_BYTES = 256 * 1024;
|
|
8181
8374
|
var READ_TIMEOUT_MS = 5000;
|
|
8182
|
-
function
|
|
8375
|
+
function isRecord2(value) {
|
|
8183
8376
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8184
8377
|
}
|
|
8185
8378
|
function releaseCanaryCallbackUri(value) {
|
|
@@ -8201,7 +8394,7 @@ function releaseCanaryCallbackUri(value) {
|
|
|
8201
8394
|
return uri.toString();
|
|
8202
8395
|
}
|
|
8203
8396
|
function createdClientId(value) {
|
|
8204
|
-
if (!
|
|
8397
|
+
if (!isRecord2(value))
|
|
8205
8398
|
return null;
|
|
8206
8399
|
try {
|
|
8207
8400
|
return clientId(value.client_id);
|
|
@@ -8224,7 +8417,7 @@ function oauthClientsPath(ref) {
|
|
|
8224
8417
|
return `/v1/projects/${encodeURIComponent(ref)}/auth/oauth-clients`;
|
|
8225
8418
|
}
|
|
8226
8419
|
function expectedClient(value, redirectUri) {
|
|
8227
|
-
if (!
|
|
8420
|
+
if (!isRecord2(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")
|
|
8228
8421
|
return null;
|
|
8229
8422
|
let callback;
|
|
8230
8423
|
try {
|
|
@@ -8245,9 +8438,9 @@ function expectedClient(value, redirectUri) {
|
|
|
8245
8438
|
};
|
|
8246
8439
|
}
|
|
8247
8440
|
function clientInventory(value) {
|
|
8248
|
-
if (!
|
|
8441
|
+
if (!isRecord2(value) || !Array.isArray(value.clients))
|
|
8249
8442
|
return null;
|
|
8250
|
-
const clients = value.clients.filter((client) =>
|
|
8443
|
+
const clients = value.clients.filter((client) => isRecord2(client) && client.client_name === RELEASE_CANARY_CLIENT_NAME).map((client) => expectedClient(client));
|
|
8251
8444
|
if (clients.some((client) => client === null))
|
|
8252
8445
|
return null;
|
|
8253
8446
|
const inventory = clients;
|
|
@@ -10191,6 +10384,115 @@ function projectGetRead(response, expectedRef) {
|
|
|
10191
10384
|
return project ? successfulResult(project) : failedResult("Invalid project response");
|
|
10192
10385
|
}
|
|
10193
10386
|
|
|
10387
|
+
// src/shared/tools/project-endpoint-read.ts
|
|
10388
|
+
var PROJECT_ENDPOINT_RESPONSE_MAX_BYTES = 256 * 1024;
|
|
10389
|
+
var PROJECT_ENDPOINT_LIST_RESPONSE_MAX_BYTES = 1024 * 1024;
|
|
10390
|
+
var PROJECT_REF_PATTERN4 = /^[a-z0-9-]{1,20}$/;
|
|
10391
|
+
var PROJECT_ENDPOINTS_SCHEMA = "supacloud.project-endpoints.v1";
|
|
10392
|
+
var PROJECT_ENDPOINT_SOURCES = new Set([
|
|
10393
|
+
"explicit_api_domain",
|
|
10394
|
+
"explicit_auth_domain",
|
|
10395
|
+
"explicit_studio_domain",
|
|
10396
|
+
"custom_domain",
|
|
10397
|
+
"derived_api_domain",
|
|
10398
|
+
"generated"
|
|
10399
|
+
]);
|
|
10400
|
+
var ROOT_KEYS = new Set(["schema", "project_ref", "endpoints"]);
|
|
10401
|
+
var ENDPOINTS_KEYS = new Set(["api", "auth", "studio"]);
|
|
10402
|
+
var ENDPOINT_KEYS = new Set(["origin", "host", "scheme", "source", "aliases"]);
|
|
10403
|
+
var MAX_ALIASES = 64;
|
|
10404
|
+
function plainRecord2(candidate) {
|
|
10405
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
10406
|
+
return null;
|
|
10407
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
10408
|
+
return prototype === Object.prototype || prototype === null ? candidate : null;
|
|
10409
|
+
}
|
|
10410
|
+
function hasOnlyKeys2(record, allowedKeys) {
|
|
10411
|
+
return Object.keys(record).every((key) => allowedKeys.has(key));
|
|
10412
|
+
}
|
|
10413
|
+
function boundedText2(candidate, maxLength) {
|
|
10414
|
+
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) ? candidate : null;
|
|
10415
|
+
}
|
|
10416
|
+
function canonicalHost(candidate, scheme) {
|
|
10417
|
+
const host = boundedText2(candidate, 255);
|
|
10418
|
+
if (!host)
|
|
10419
|
+
return null;
|
|
10420
|
+
try {
|
|
10421
|
+
const parsed = new URL(`${scheme}://${host}`);
|
|
10422
|
+
return parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.host !== host ? null : host;
|
|
10423
|
+
} catch {
|
|
10424
|
+
return null;
|
|
10425
|
+
}
|
|
10426
|
+
}
|
|
10427
|
+
function projectEndpoint2(candidate) {
|
|
10428
|
+
const endpoint = plainRecord2(candidate);
|
|
10429
|
+
if (!endpoint || !hasOnlyKeys2(endpoint, ENDPOINT_KEYS))
|
|
10430
|
+
return null;
|
|
10431
|
+
const scheme = endpoint.scheme === "http" || endpoint.scheme === "https" ? endpoint.scheme : null;
|
|
10432
|
+
const origin = boundedText2(endpoint.origin, 2048);
|
|
10433
|
+
const source = boundedText2(endpoint.source, 64);
|
|
10434
|
+
if (!scheme || !origin || !source || !PROJECT_ENDPOINT_SOURCES.has(source))
|
|
10435
|
+
return null;
|
|
10436
|
+
let parsedOrigin;
|
|
10437
|
+
try {
|
|
10438
|
+
parsedOrigin = new URL(origin);
|
|
10439
|
+
} catch {
|
|
10440
|
+
return null;
|
|
10441
|
+
}
|
|
10442
|
+
if (parsedOrigin.protocol !== `${scheme}:` || parsedOrigin.origin !== origin || parsedOrigin.username || parsedOrigin.password || parsedOrigin.pathname !== "/" || parsedOrigin.search || parsedOrigin.hash)
|
|
10443
|
+
return null;
|
|
10444
|
+
const host = canonicalHost(endpoint.host, scheme);
|
|
10445
|
+
if (!host || host !== parsedOrigin.host || !Array.isArray(endpoint.aliases) || endpoint.aliases.length > MAX_ALIASES)
|
|
10446
|
+
return null;
|
|
10447
|
+
const aliases = [];
|
|
10448
|
+
const seenAliases = new Set;
|
|
10449
|
+
for (const aliasCandidate of endpoint.aliases) {
|
|
10450
|
+
const alias = canonicalHost(aliasCandidate, scheme);
|
|
10451
|
+
if (!alias || alias === host || seenAliases.has(alias))
|
|
10452
|
+
return null;
|
|
10453
|
+
seenAliases.add(alias);
|
|
10454
|
+
aliases.push(alias);
|
|
10455
|
+
}
|
|
10456
|
+
return { origin, host, scheme, source, aliases };
|
|
10457
|
+
}
|
|
10458
|
+
function projectEndpointProjection(candidate) {
|
|
10459
|
+
const projection = plainRecord2(candidate);
|
|
10460
|
+
if (!projection || !hasOnlyKeys2(projection, ROOT_KEYS) || projection.schema !== PROJECT_ENDPOINTS_SCHEMA || typeof projection.project_ref !== "string" || !PROJECT_REF_PATTERN4.test(projection.project_ref))
|
|
10461
|
+
return null;
|
|
10462
|
+
const endpoints = plainRecord2(projection.endpoints);
|
|
10463
|
+
if (!endpoints || !hasOnlyKeys2(endpoints, ENDPOINTS_KEYS))
|
|
10464
|
+
return null;
|
|
10465
|
+
const api = projectEndpoint2(endpoints.api);
|
|
10466
|
+
const auth = projectEndpoint2(endpoints.auth);
|
|
10467
|
+
const studio = projectEndpoint2(endpoints.studio);
|
|
10468
|
+
return api && auth && studio ? {
|
|
10469
|
+
schema: PROJECT_ENDPOINTS_SCHEMA,
|
|
10470
|
+
project_ref: projection.project_ref,
|
|
10471
|
+
endpoints: { api, auth, studio }
|
|
10472
|
+
} : null;
|
|
10473
|
+
}
|
|
10474
|
+
function validHttpStatus2(status) {
|
|
10475
|
+
return Number.isSafeInteger(status) && status >= 100 && status <= 599;
|
|
10476
|
+
}
|
|
10477
|
+
function successfulResponse2(response) {
|
|
10478
|
+
return response.ok === true && validHttpStatus2(response.status) && response.status >= 200 && response.status <= 299;
|
|
10479
|
+
}
|
|
10480
|
+
function failedResult2(message) {
|
|
10481
|
+
return { text: `❌ ${message}`, isError: true };
|
|
10482
|
+
}
|
|
10483
|
+
function failedHttpResult2(label, status) {
|
|
10484
|
+
return failedResult2(validHttpStatus2(status) ? `${label} request failed (${status})` : `${label} request failed`);
|
|
10485
|
+
}
|
|
10486
|
+
function successfulResult2(payload) {
|
|
10487
|
+
return { text: JSON.stringify(payload, null, 2), isError: false };
|
|
10488
|
+
}
|
|
10489
|
+
function projectEndpointRead(response, expectedRef) {
|
|
10490
|
+
if (!successfulResponse2(response))
|
|
10491
|
+
return failedHttpResult2("Project endpoints", response.status);
|
|
10492
|
+
const projection = projectEndpointProjection(response.data);
|
|
10493
|
+
return projection && projection.project_ref === expectedRef ? successfulResult2(projection) : failedResult2("Invalid project endpoint response");
|
|
10494
|
+
}
|
|
10495
|
+
|
|
10194
10496
|
// src/shared/tools/project-cli-tools.ts
|
|
10195
10497
|
function projectReadResponse(readResult) {
|
|
10196
10498
|
return {
|
|
@@ -10324,12 +10626,17 @@ function resolveRef(refFromArgs, defaultRef) {
|
|
|
10324
10626
|
throw new Error("'ref' is required for this action");
|
|
10325
10627
|
return ref;
|
|
10326
10628
|
}
|
|
10629
|
+
function projectEndpointProjectionPath(ref) {
|
|
10630
|
+
return `/v1/projects/${encodeURIComponent(ref)}/endpoint/projection`;
|
|
10631
|
+
}
|
|
10327
10632
|
function registerUserProjectCliTools(server, http, options = {}) {
|
|
10328
10633
|
const { projectRef: projectRef2 } = options;
|
|
10329
10634
|
server.tool("project", `Project-scoped inspection and developer operations.
|
|
10330
|
-
Actions: get, pause, restore, health, logs, api_keys, settings, tasks, task_detail, task_cancel, task_retry, task_stats, dlq, background_settings, update_background_settings`, {
|
|
10635
|
+
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`, {
|
|
10331
10636
|
action: withDescription(stringEnum([
|
|
10637
|
+
"list",
|
|
10332
10638
|
"get",
|
|
10639
|
+
"endpoints",
|
|
10333
10640
|
"pause",
|
|
10334
10641
|
"restore",
|
|
10335
10642
|
"health",
|
|
@@ -10352,6 +10659,19 @@ Actions: get, pause, restore, health, logs, api_keys, settings, tasks, task_deta
|
|
|
10352
10659
|
concurrency: optional(Type.Number(), "[update_background_settings] Max concurrent background tasks"),
|
|
10353
10660
|
max_attempts: optional(Type.Number(), "[update_background_settings] Max attempts for background tasks")
|
|
10354
10661
|
}, async ({ action, ref, log_type, task_id, limit, concurrency, max_attempts }) => {
|
|
10662
|
+
if (action === "list") {
|
|
10663
|
+
return {
|
|
10664
|
+
isError: true,
|
|
10665
|
+
content: [{
|
|
10666
|
+
type: "text",
|
|
10667
|
+
text: [
|
|
10668
|
+
"⚠️ Project enumeration is a platform administration operation.",
|
|
10669
|
+
"Use `supacloud-admin project list` with an admin Management API context."
|
|
10670
|
+
].join(`
|
|
10671
|
+
`)
|
|
10672
|
+
}]
|
|
10673
|
+
};
|
|
10674
|
+
}
|
|
10355
10675
|
const resolvedRef = resolveRef(ref, projectRef2);
|
|
10356
10676
|
let text;
|
|
10357
10677
|
switch (action) {
|
|
@@ -10359,6 +10679,10 @@ Actions: get, pause, restore, health, logs, api_keys, settings, tasks, task_deta
|
|
|
10359
10679
|
return projectReadResponse(projectGetRead(await http.get(`/v1/projects/${resolvedRef}`, {
|
|
10360
10680
|
maxResponseBytes: PROJECT_READ_RESPONSE_MAX_BYTES
|
|
10361
10681
|
}), resolvedRef));
|
|
10682
|
+
case "endpoints":
|
|
10683
|
+
return projectReadResponse(projectEndpointRead(await http.get(projectEndpointProjectionPath(resolvedRef), {
|
|
10684
|
+
maxResponseBytes: PROJECT_ENDPOINT_RESPONSE_MAX_BYTES
|
|
10685
|
+
}), resolvedRef));
|
|
10362
10686
|
case "pause":
|
|
10363
10687
|
text = simple(await http.post(`/v1/projects/${resolvedRef}/pause`), `Project ${resolvedRef} paused`);
|
|
10364
10688
|
break;
|
|
@@ -12435,7 +12759,7 @@ var INVENTORY_MAX_BYTES = 1024 * 1024;
|
|
|
12435
12759
|
var MUTATION_MAX_BYTES = 64 * 1024;
|
|
12436
12760
|
var BACKUP_TIMEOUT_MS = 36 * 60000;
|
|
12437
12761
|
var RELEASE_READ_RESPONSE_TIMEOUT_MS = 5000;
|
|
12438
|
-
function
|
|
12762
|
+
function isRecord3(value) {
|
|
12439
12763
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12440
12764
|
}
|
|
12441
12765
|
function canonicalTimestamp3(value) {
|
|
@@ -12451,7 +12775,7 @@ function backupBelongsToProject(backupId, projectRef2) {
|
|
|
12451
12775
|
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef2}_`);
|
|
12452
12776
|
}
|
|
12453
12777
|
function verifiedBackup(value, projectRef2) {
|
|
12454
|
-
if (!
|
|
12778
|
+
if (!isRecord3(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))
|
|
12455
12779
|
return null;
|
|
12456
12780
|
return {
|
|
12457
12781
|
backup_id: value.backup_id,
|
|
@@ -12465,7 +12789,7 @@ function verifiedBackup(value, projectRef2) {
|
|
|
12465
12789
|
};
|
|
12466
12790
|
}
|
|
12467
12791
|
function backupInventory(value, projectRef2) {
|
|
12468
|
-
if (!
|
|
12792
|
+
if (!isRecord3(value) || !Array.isArray(value.backups))
|
|
12469
12793
|
return null;
|
|
12470
12794
|
const backups = value.backups.map((backup) => verifiedBackup(backup, projectRef2));
|
|
12471
12795
|
if (backups.some((backup) => backup === null))
|
|
@@ -12544,7 +12868,7 @@ function readInventoryFailure(operation, read) {
|
|
|
12544
12868
|
return null;
|
|
12545
12869
|
}
|
|
12546
12870
|
function postgrestStatus(value) {
|
|
12547
|
-
if (!
|
|
12871
|
+
if (!isRecord3(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)))
|
|
12548
12872
|
return null;
|
|
12549
12873
|
return {
|
|
12550
12874
|
desired: value.desired,
|
|
@@ -12565,7 +12889,7 @@ function readPostgrestFailure(operation, read) {
|
|
|
12565
12889
|
return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
|
|
12566
12890
|
}
|
|
12567
12891
|
function isRestartReceipt(value) {
|
|
12568
|
-
return
|
|
12892
|
+
return isRecord3(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
|
|
12569
12893
|
}
|
|
12570
12894
|
function registerReleaseTools(server, http, options = {}) {
|
|
12571
12895
|
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", {
|
|
@@ -12606,7 +12930,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12606
12930
|
if (!mutation2.ok || mutation2.status !== 200) {
|
|
12607
12931
|
return mutationFailure("release.logical_backup.create", mutation2);
|
|
12608
12932
|
}
|
|
12609
|
-
const responseBackup =
|
|
12933
|
+
const responseBackup = isRecord3(mutation2.data) ? verifiedBackup(mutation2.data.backup, projectRef2) : null;
|
|
12610
12934
|
const afterFailure = readInventoryFailure("release.logical_backup.create", after);
|
|
12611
12935
|
const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
|
|
12612
12936
|
if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
|
|
@@ -12631,7 +12955,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12631
12955
|
if (!mutation2.ok || mutation2.status !== 200) {
|
|
12632
12956
|
return mutationFailure("release.logical_backup.restore", mutation2);
|
|
12633
12957
|
}
|
|
12634
|
-
const responseBackup =
|
|
12958
|
+
const responseBackup = isRecord3(mutation2.data) ? verifiedBackup(mutation2.data.restored_backup, projectRef2) : null;
|
|
12635
12959
|
const after = await readInventory(http, projectRef2);
|
|
12636
12960
|
const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
|
|
12637
12961
|
const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
|
|
@@ -12671,7 +12995,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12671
12995
|
// package.json
|
|
12672
12996
|
var package_default = {
|
|
12673
12997
|
name: "@supacloud/cli",
|
|
12674
|
-
version: "0.
|
|
12998
|
+
version: "0.28.0",
|
|
12675
12999
|
description: "Project-scoped CLI for SupaCloud users",
|
|
12676
13000
|
type: "module",
|
|
12677
13001
|
main: "./dist/index.js",
|
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 |
|
|
@@ -15,6 +17,7 @@ Load this reference when selecting a command surface or when a user asks an AI t
|
|
|
15
17
|
| Inspect or back up a remote database | `supabase db_pull`, `migration_list`, `db_dump`, `gen_types` | Requires explicit PostgreSQL DSN; redact it |
|
|
16
18
|
| Preview/apply migrations remotely | `supabase push` | Always dry-run first; production needs explicit approval |
|
|
17
19
|
| Mark proven-equivalent historical migrations as applied | `database baseline_migrations` | Dry-run, schema-equivalence proof, backup, explicit approval |
|
|
20
|
+
| Inspect auth users or generate a controlled login link | `auth list_users`, `auth get_user`, `auth generate_link` | User reads are bounded; login-link generation is a production-confirmed write and action links stay in the calling process |
|
|
18
21
|
| Manage Auth/Storage/Edge Functions/frontend/secrets | Corresponding project module | Keep deployable config/code in version control |
|
|
19
22
|
| Configure project gateway routes | `gateway` | Requires an admin-capable project token; inspect before write |
|
|
20
23
|
| Install/upgrade/debug SupaCloud servers | `supacloud-admin` | Platform boundary; not a project CLI action |
|
|
@@ -26,10 +29,10 @@ until a project-scoped context is resolved.
|
|
|
26
29
|
## Command groups
|
|
27
30
|
|
|
28
31
|
- `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.
|
|
32
|
+
- `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
33
|
- `database`: read/query, schema inspection, extensions, indexes, RLS, stats, migration push, controlled historical baseline, and SQL-file execution.
|
|
31
34
|
- `supabase`: allowlisted official CLI adapter for migration authoring, local reset/diff, explicit-DSN inspection/backup/type generation, and SupaCloud-controlled migration push.
|
|
32
|
-
- `auth`: provider and
|
|
35
|
+
- `auth`: provider/configuration plus bounded user lookup and production-confirmed login-link generation.
|
|
33
36
|
- `storage`: buckets and object-management workflows.
|
|
34
37
|
- `edge_functions`: list, atomically read one active or deleted identity with `get_config`, read immutable source, deploy, activate, configure, and delete Edge Functions. For every mutation, pass the `activation_id` read from the same `list` or `get_config` snapshot as `--expected-activation-id`; use `legacy` only for a never-created or listed legacy Function, not for a deleted slug with a tombstone UUID. Deploy and activate actions also require the non-negative observed version as `--expected-active-version`; use `absent` for a never-created slug or a `get_config` tombstone. Version `0` is a legacy version token and cannot be used as a source or activation target.
|
|
35
38
|
- `frontend`: list, build/deploy, domain, and deployment workflows.
|
|
@@ -43,8 +46,14 @@ until a project-scoped context is resolved.
|
|
|
43
46
|
```bash
|
|
44
47
|
supacloud-cli status
|
|
45
48
|
supacloud-cli project get
|
|
49
|
+
supacloud-cli project endpoints
|
|
46
50
|
supacloud-cli project health
|
|
47
51
|
supacloud-cli supabase migration_list --db_url "$SUPACLOUD_DB_URL"
|
|
48
52
|
```
|
|
49
53
|
|
|
54
|
+
The endpoint projection returns bounded, credential-free API/Auth/Studio origins,
|
|
55
|
+
canonical hosts, URL schemes, configuration sources, and API aliases. It does
|
|
56
|
+
not assert DNS, certificate, or runtime readiness; use the relevant health and
|
|
57
|
+
gateway inspection commands for those checks.
|
|
58
|
+
|
|
50
59
|
Do not paste the DSN value into chat or commit it to shell scripts. Prefer an environment variable supplied outside the repository.
|