@supacloud/cli 0.28.0 → 0.28.1

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
@@ -7971,22 +7971,20 @@ var safeAuthMutationCodes = new Set([
7971
7971
  var MAX_AUTH_READ_BYTES = 64 * 1024;
7972
7972
  var AUTH_READ_TIMEOUT_MS = 5000;
7973
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
- ];
7974
+ var AUTH_LINK_TYPES = ["magiclink", "recovery", "invite"];
7983
7975
  var SAFE_USER_FIELDS = [
7984
- "id",
7985
7976
  "email",
7986
7977
  "phone",
7987
7978
  "created_at",
7988
7979
  "last_sign_in_at"
7989
7980
  ];
7981
+ var MAX_AUTH_SEARCH_LENGTH = 256;
7982
+ var MAX_AUTH_EMAIL_LENGTH = 320;
7983
+ var MAX_AUTH_PHONE_LENGTH = 64;
7984
+ var MAX_AUTH_TIMESTAMP_LENGTH = 64;
7985
+ var MAX_AUTH_REDIRECT_LENGTH = 4096;
7986
+ var MAX_AUTH_ACTION_LINK_LENGTH = 8192;
7987
+ var INVALID_FIELD = Symbol("invalid-auth-field");
7990
7988
  function parseAuthConfig(input) {
7991
7989
  if (typeof input !== "string")
7992
7990
  return input;
@@ -8065,18 +8063,32 @@ function boundedPerPage(candidate) {
8065
8063
  }
8066
8064
  return candidate;
8067
8065
  }
8066
+ function boundedText(candidate, field, maxLength) {
8067
+ if (typeof candidate !== "string")
8068
+ throw new Error(`'${field}' must be a string`);
8069
+ const value = candidate.trim();
8070
+ if (!value || value.length > maxLength || /[\u0000-\u001f\u007f]/u.test(value)) {
8071
+ throw new Error(`'${field}' is invalid or exceeds ${maxLength} characters`);
8072
+ }
8073
+ return value;
8074
+ }
8075
+ function boundedSearch(candidate, field) {
8076
+ return boundedText(candidate, field, MAX_AUTH_SEARCH_LENGTH);
8077
+ }
8078
+ function requiredEmail(candidate) {
8079
+ return boundedText(candidate, "email", MAX_AUTH_EMAIL_LENGTH);
8080
+ }
8068
8081
  function safeRedirectTo(candidate) {
8069
8082
  if (candidate === undefined)
8070
8083
  return;
8071
- if (typeof candidate !== "string" || !candidate.trim())
8072
- throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL");
8084
+ const value = boundedText(candidate, "redirect_to", MAX_AUTH_REDIRECT_LENGTH);
8073
8085
  let uri;
8074
8086
  try {
8075
- uri = new URL(candidate.trim());
8087
+ uri = new URL(value);
8076
8088
  } catch {
8077
8089
  throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL");
8078
8090
  }
8079
- const loopback = uri.hostname === "127.0.0.1" || uri.hostname === "[::1]";
8091
+ const loopback = uri.hostname === "localhost" || uri.hostname.endsWith(".localhost") || uri.hostname === "127.0.0.1" || uri.hostname === "[::1]";
8080
8092
  const validProtocol = uri.protocol === "https:" || uri.protocol === "http:" && loopback && Boolean(uri.port);
8081
8093
  if (!validProtocol || uri.username || uri.password || uri.hash) {
8082
8094
  throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL without credentials or fragment");
@@ -8086,28 +8098,66 @@ function safeRedirectTo(candidate) {
8086
8098
  function isRecord(candidate) {
8087
8099
  return candidate !== null && typeof candidate === "object" && !Array.isArray(candidate);
8088
8100
  }
8101
+ function safeOptionalUserField(candidate, field, maxLength) {
8102
+ if (!(field in candidate))
8103
+ return;
8104
+ const value = candidate[field];
8105
+ if (value === null)
8106
+ return null;
8107
+ if (typeof value !== "string" || value.length > maxLength || /[\u0000-\u001f\u007f]/u.test(value))
8108
+ return INVALID_FIELD;
8109
+ return value;
8110
+ }
8089
8111
  function projectUser(candidate) {
8090
- if (!isRecord(candidate) || typeof candidate.id !== "string")
8112
+ if (!isRecord(candidate) || typeof candidate.id !== "string" || !USER_ID_PATTERN.test(candidate.id))
8091
8113
  return null;
8092
- const projectedUser = {};
8114
+ const projectedUser = { id: candidate.id.toLowerCase() };
8115
+ const fieldLimits = {
8116
+ email: MAX_AUTH_EMAIL_LENGTH,
8117
+ phone: MAX_AUTH_PHONE_LENGTH,
8118
+ created_at: MAX_AUTH_TIMESTAMP_LENGTH,
8119
+ last_sign_in_at: MAX_AUTH_TIMESTAMP_LENGTH
8120
+ };
8093
8121
  for (const field of SAFE_USER_FIELDS) {
8094
- if (field in candidate)
8095
- projectedUser[field] = candidate[field];
8122
+ const value = safeOptionalUserField(candidate, field, fieldLimits[field]);
8123
+ if (value === INVALID_FIELD)
8124
+ return null;
8125
+ if (value !== undefined)
8126
+ projectedUser[field] = value;
8096
8127
  }
8097
8128
  return projectedUser;
8098
8129
  }
8099
- function projectUserList(candidate) {
8100
- if (!isRecord(candidate) || !Array.isArray(candidate.users))
8130
+ function safePaginationField(candidate) {
8131
+ if (candidate === undefined)
8132
+ return;
8133
+ if (candidate === null)
8134
+ return null;
8135
+ return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0 ? candidate : INVALID_FIELD;
8136
+ }
8137
+ function projectUserList(candidate, expectedPage, expectedPerPage) {
8138
+ if (!isRecord(candidate) || !Array.isArray(candidate.users) || candidate.users.length > expectedPerPage)
8101
8139
  return null;
8102
8140
  const users = candidate.users.map(projectUser);
8103
8141
  if (users.some((user) => user === null))
8104
8142
  return null;
8105
- const projectedFields = { users };
8143
+ const projectedUsers = users;
8144
+ const userIds = projectedUsers.map((user) => user.id);
8145
+ if (new Set(userIds).size !== userIds.length)
8146
+ return null;
8147
+ const projectedFields = { users: projectedUsers };
8106
8148
  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
- }
8149
+ const value = safePaginationField(candidate[field]);
8150
+ if (value === INVALID_FIELD)
8151
+ return null;
8152
+ if (value !== undefined)
8153
+ projectedFields[field] = value;
8110
8154
  }
8155
+ if (projectedFields.page !== undefined && projectedFields.page !== expectedPage)
8156
+ return null;
8157
+ if (projectedFields.per_page !== undefined && projectedFields.per_page !== expectedPerPage)
8158
+ return null;
8159
+ if (typeof projectedFields.total === "number" && projectedFields.total < projectedUsers.length)
8160
+ return null;
8111
8161
  return projectedFields;
8112
8162
  }
8113
8163
  function actionLink(candidate) {
@@ -8117,10 +8167,17 @@ function actionLink(candidate) {
8117
8167
  if (isRecord(candidate.data))
8118
8168
  candidates.push(candidate.data.properties);
8119
8169
  }
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
- }
8170
+ for (const nested of candidates) {
8171
+ if (!isRecord(nested) || typeof nested.action_link !== "string")
8172
+ continue;
8173
+ const link = nested.action_link;
8174
+ if (!link || link.length > MAX_AUTH_ACTION_LINK_LENGTH || /[\u0000-\u001f\u007f]/u.test(link))
8175
+ continue;
8176
+ try {
8177
+ const uri = new URL(link);
8178
+ if ((uri.protocol === "https:" || uri.protocol === "http:") && !uri.username && !uri.password && !uri.hash && uri.toString() === link)
8179
+ return link;
8180
+ } catch {}
8124
8181
  }
8125
8182
  return null;
8126
8183
  }
@@ -8144,11 +8201,8 @@ async function listUsers(http, args) {
8144
8201
  const perPage = boundedPerPage(args.per_page);
8145
8202
  const params = new URLSearchParams({ page: String(page), per_page: String(perPage) });
8146
8203
  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
- }
8204
+ if (args[key] !== undefined)
8205
+ params.set(key, boundedSearch(args[key], key));
8152
8206
  }
8153
8207
  const response = await http.get(`/v1/projects/${ref}/auth/users?${params.toString()}`, {
8154
8208
  maxJsonBytes: MAX_AUTH_READ_BYTES,
@@ -8156,7 +8210,7 @@ async function listUsers(http, args) {
8156
8210
  });
8157
8211
  if (!response.ok)
8158
8212
  return safeAuthReadFailure("auth.list_users", response);
8159
- const users = projectUserList(response.data);
8213
+ const users = projectUserList(response.data, page, perPage);
8160
8214
  if (!users)
8161
8215
  return safeAuthReadFailure("auth.list_users", { ...response, responseReadError: true });
8162
8216
  return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.list_users", project_ref: ref, ...users }, null, 2) }] };
@@ -8171,27 +8225,43 @@ async function getUser(http, args) {
8171
8225
  if (!response.ok)
8172
8226
  return safeAuthReadFailure("auth.get_user", response);
8173
8227
  const user = projectUser(response.data);
8174
- if (!user)
8228
+ if (!user || user.id !== userId) {
8175
8229
  return safeAuthReadFailure("auth.get_user", { ...response, responseReadError: true });
8230
+ }
8176
8231
  return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.get_user", project_ref: ref, user }, null, 2) }] };
8177
8232
  }
8233
+ function generateLinkFailure(response) {
8234
+ const outcomeUnknown = response.responseReadError || response.transportError || response.status === 408 || response.status >= 500;
8235
+ return {
8236
+ isError: true,
8237
+ content: [{
8238
+ type: "text",
8239
+ text: JSON.stringify({
8240
+ ok: false,
8241
+ operation: "auth.generate_link",
8242
+ error: {
8243
+ code: outcomeUnknown ? "OUTCOME_UNKNOWN" : "HTTP_ERROR",
8244
+ http_status: response.transportError ? null : response.status
8245
+ }
8246
+ }, null, 2)
8247
+ }]
8248
+ };
8249
+ }
8178
8250
  async function generateLink(http, args) {
8179
8251
  const ref = requiredRef(args.ref);
8180
8252
  if (typeof args.type !== "string" || !AUTH_LINK_TYPES.includes(args.type)) {
8181
- throw new Error("'type' is invalid for 'generate_link'");
8253
+ throw new Error("'type' must be one of magiclink, recovery, or invite for 'generate_link'");
8182
8254
  }
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() };
8255
+ const body = { type: args.type, email: requiredEmail(args.email) };
8186
8256
  const redirectTo = safeRedirectTo(args.redirect_to);
8187
8257
  if (redirectTo)
8188
8258
  body.redirect_to = redirectTo;
8189
8259
  const response = await http.postReleaseMutation(`/v1/projects/${ref}/auth/generate_link`, body);
8190
8260
  if (!response.ok)
8191
- return safeAuthReadFailure("auth.generate_link", response);
8261
+ return generateLinkFailure(response);
8192
8262
  const link = actionLink(response.data);
8193
8263
  if (!link)
8194
- return safeAuthReadFailure("auth.generate_link", { ...response, responseReadError: true });
8264
+ return generateLinkFailure({ ...response, responseReadError: true });
8195
8265
  return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.generate_link", action_link: link }, null, 2) }] };
8196
8266
  }
8197
8267
  function formatProviders(data) {
@@ -8251,7 +8321,7 @@ Actions: list_users, get_user, generate_link, list_providers, get_provider, conf
8251
8321
  per_page: optional(Type.Integer({ minimum: 1, maximum: 100 }), "[list_users] Users per page (1-100)"),
8252
8322
  search: optional(Type.String(), "[list_users] Search user email, phone, or UUID"),
8253
8323
  email_like: optional(Type.String(), "[list_users] Search user email or phone"),
8254
- type: optional(stringEnum(AUTH_LINK_TYPES), "[generate_link] GoTrue link type"),
8324
+ type: optional(stringEnum(AUTH_LINK_TYPES), "[generate_link] magiclink, recovery, or invite"),
8255
8325
  email: optional(Type.String(), "[generate_link] User email"),
8256
8326
  redirect_to: optional(Type.String(), "[generate_link] Absolute HTTPS or loopback callback"),
8257
8327
  provider: optional(Type.String(), "[*_provider] Provider name (github, google, wechat, etc.)"),
@@ -10246,15 +10316,15 @@ function hasWellFormedUnicode(text) {
10246
10316
  }
10247
10317
  return true;
10248
10318
  }
10249
- function boundedText(candidate, maxLength) {
10319
+ function boundedText2(candidate, maxLength) {
10250
10320
  return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
10251
10321
  }
10252
10322
  function matchingText(candidate, maxLength, pattern) {
10253
- const candidateText = boundedText(candidate, maxLength);
10323
+ const candidateText = boundedText2(candidate, maxLength);
10254
10324
  return candidateText && pattern.test(candidateText) ? candidateText : null;
10255
10325
  }
10256
10326
  function canonicalTimestamp(candidate) {
10257
- const timestamp = boundedText(candidate, 64);
10327
+ const timestamp = boundedText2(candidate, 64);
10258
10328
  if (!timestamp)
10259
10329
  return null;
10260
10330
  const milliseconds = Date.parse(timestamp);
@@ -10266,7 +10336,7 @@ function projectedSummary(project) {
10266
10336
  ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN3),
10267
10337
  organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
10268
10338
  organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
10269
- name: boundedText(project.name, 100),
10339
+ name: boundedText2(project.name, 100),
10270
10340
  region: matchingText(project.region, 64, REGION_PATTERN),
10271
10341
  created_at: canonicalTimestamp(project.created_at),
10272
10342
  status: matchingText(project.status, 64, STATUS_PATTERN)
@@ -10274,7 +10344,7 @@ function projectedSummary(project) {
10274
10344
  return Object.values(summary).every((field) => field !== null) ? summary : null;
10275
10345
  }
10276
10346
  function databaseHost(candidate) {
10277
- const host = boundedText(candidate, 255);
10347
+ const host = boundedText2(candidate, 255);
10278
10348
  if (!host)
10279
10349
  return null;
10280
10350
  if (host.startsWith("[") && host.endsWith("]")) {
@@ -10314,7 +10384,7 @@ function projectEndpoint(candidate) {
10314
10384
  const endpoint = plainRecord(candidate);
10315
10385
  if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
10316
10386
  return null;
10317
- const endpointUrl = boundedText(endpoint.url, 2048);
10387
+ const endpointUrl = boundedText2(endpoint.url, 2048);
10318
10388
  if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
10319
10389
  return null;
10320
10390
  try {
@@ -10331,7 +10401,7 @@ function projectEndpoint(candidate) {
10331
10401
  function discardedDetailFieldsAreValid(project) {
10332
10402
  if (project.config !== undefined && plainRecord(project.config) === null)
10333
10403
  return false;
10334
- if (project.anon_key !== undefined && boundedText(project.anon_key, 16384) === null)
10404
+ if (project.anon_key !== undefined && boundedText2(project.anon_key, 16384) === null)
10335
10405
  return false;
10336
10406
  return project.services === undefined || Array.isArray(project.services);
10337
10407
  }
@@ -10410,11 +10480,11 @@ function plainRecord2(candidate) {
10410
10480
  function hasOnlyKeys2(record, allowedKeys) {
10411
10481
  return Object.keys(record).every((key) => allowedKeys.has(key));
10412
10482
  }
10413
- function boundedText2(candidate, maxLength) {
10483
+ function boundedText3(candidate, maxLength) {
10414
10484
  return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) ? candidate : null;
10415
10485
  }
10416
10486
  function canonicalHost(candidate, scheme) {
10417
- const host = boundedText2(candidate, 255);
10487
+ const host = boundedText3(candidate, 255);
10418
10488
  if (!host)
10419
10489
  return null;
10420
10490
  try {
@@ -10429,8 +10499,8 @@ function projectEndpoint2(candidate) {
10429
10499
  if (!endpoint || !hasOnlyKeys2(endpoint, ENDPOINT_KEYS))
10430
10500
  return null;
10431
10501
  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);
10502
+ const origin = boundedText3(endpoint.origin, 2048);
10503
+ const source = boundedText3(endpoint.source, 64);
10434
10504
  if (!scheme || !origin || !source || !PROJECT_ENDPOINT_SOURCES.has(source))
10435
10505
  return null;
10436
10506
  let parsedOrigin;
@@ -12995,7 +13065,7 @@ function registerReleaseTools(server, http, options = {}) {
12995
13065
  // package.json
12996
13066
  var package_default = {
12997
13067
  name: "@supacloud/cli",
12998
- version: "0.28.0",
13068
+ version: "0.28.1",
12999
13069
  description: "Project-scoped CLI for SupaCloud users",
13000
13070
  type: "module",
13001
13071
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/cli",
3
- "version": "0.28.0",
3
+ "version": "0.28.1",
4
4
  "description": "Project-scoped CLI for SupaCloud users",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@ Load this reference when selecting a command surface or when a user asks an AI t
17
17
  | Inspect or back up a remote database | `supabase db_pull`, `migration_list`, `db_dump`, `gen_types` | Requires explicit PostgreSQL DSN; redact it |
18
18
  | Preview/apply migrations remotely | `supabase push` | Always dry-run first; production needs explicit approval |
19
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 |
20
+ | Inspect auth users or generate a controlled login link | `auth list_users`, `auth get_user`, `auth generate_link` | User reads are bounded; generation supports only `magiclink`, `recovery`, and `invite`, requires production confirmation, and returns only a validated action URL |
21
21
  | Manage Auth/Storage/Edge Functions/frontend/secrets | Corresponding project module | Keep deployable config/code in version control |
22
22
  | Configure project gateway routes | `gateway` | Requires an admin-capable project token; inspect before write |
23
23
  | Install/upgrade/debug SupaCloud servers | `supacloud-admin` | Platform boundary; not a project CLI action |
@@ -32,7 +32,7 @@ until a project-scoped context is resolved.
32
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.
33
33
  - `database`: read/query, schema inspection, extensions, indexes, RLS, stats, migration push, controlled historical baseline, and SQL-file execution.
34
34
  - `supabase`: allowlisted official CLI adapter for migration authoring, local reset/diff, explicit-DSN inspection/backup/type generation, and SupaCloud-controlled migration push.
35
- - `auth`: provider/configuration plus bounded user lookup and production-confirmed login-link generation.
35
+ - `auth`: provider/configuration plus bounded user lookup and production-confirmed `magiclink`, `recovery`, or `invite` generation. Search/email/redirect inputs and returned action URLs are bounded and validated before use.
36
36
  - `storage`: buckets and object-management workflows.
37
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.
38
38
  - `frontend`: list, build/deploy, domain, and deployment workflows.