@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
|
-
|
|
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(
|
|
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
|
-
|
|
8095
|
-
|
|
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
|
|
8100
|
-
if (
|
|
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
|
|
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
|
-
|
|
8108
|
-
|
|
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
|
|
8121
|
-
if (isRecord(
|
|
8122
|
-
|
|
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
|
-
|
|
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'
|
|
8253
|
+
throw new Error("'type' must be one of magiclink, recovery, or invite for 'generate_link'");
|
|
8182
8254
|
}
|
|
8183
|
-
|
|
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
|
|
8261
|
+
return generateLinkFailure(response);
|
|
8192
8262
|
const link = actionLink(response.data);
|
|
8193
8263
|
if (!link)
|
|
8194
|
-
return
|
|
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]
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 &&
|
|
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
|
|
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 =
|
|
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 =
|
|
10433
|
-
const source =
|
|
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.
|
|
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
|
@@ -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;
|
|
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
|
|
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.
|