@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 isRecord(value) {
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 (!isRecord(value))
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 (!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")
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 (!isRecord(value) || !Array.isArray(value.clients))
8441
+ if (!isRecord2(value) || !Array.isArray(value.clients))
8249
8442
  return null;
8250
- const clients = value.clients.filter((client) => isRecord(client) && client.client_name === RELEASE_CANARY_CLIENT_NAME).map((client) => expectedClient(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 isRecord2(value) {
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 (!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))
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 (!isRecord2(value) || !Array.isArray(value.backups))
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 (!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)))
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 isRecord2(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
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 = isRecord2(mutation2.data) ? verifiedBackup(mutation2.data.backup, projectRef2) : null;
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 = isRecord2(mutation2.data) ? verifiedBackup(mutation2.data.restored_backup, projectRef2) : null;
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.26.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
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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 authentication configuration.
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.