@supacloud/cli 0.27.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
|
@@ -6471,8 +6471,8 @@ var ACTION_POLICY = {
|
|
|
6471
6471
|
write: ["push"]
|
|
6472
6472
|
},
|
|
6473
6473
|
auth: {
|
|
6474
|
-
read: ["list_providers", "get_provider", "supported_providers", "get_settings", "get_config"],
|
|
6475
|
-
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"]
|
|
6476
6476
|
},
|
|
6477
6477
|
oauth_clients: {
|
|
6478
6478
|
read: ["list", "get"],
|
|
@@ -7968,6 +7968,23 @@ var safeAuthMutationCodes = new Set([
|
|
|
7968
7968
|
"AUTH_RUNTIME_APPLY_FAILED",
|
|
7969
7969
|
"SUPAUTH_DEPENDENT_REFRESH_FAILED"
|
|
7970
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 = ["magiclink", "recovery", "invite"];
|
|
7975
|
+
var SAFE_USER_FIELDS = [
|
|
7976
|
+
"email",
|
|
7977
|
+
"phone",
|
|
7978
|
+
"created_at",
|
|
7979
|
+
"last_sign_in_at"
|
|
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");
|
|
7971
7988
|
function parseAuthConfig(input) {
|
|
7972
7989
|
if (typeof input !== "string")
|
|
7973
7990
|
return input;
|
|
@@ -8019,6 +8036,234 @@ function authMutationResult(response, successMessage) {
|
|
|
8019
8036
|
}]
|
|
8020
8037
|
};
|
|
8021
8038
|
}
|
|
8039
|
+
function requiredRef(candidate) {
|
|
8040
|
+
if (typeof candidate !== "string" || !candidate.trim())
|
|
8041
|
+
throw new Error("'ref' is required");
|
|
8042
|
+
return projectRefPathSegment(candidate.trim(), "Auth");
|
|
8043
|
+
}
|
|
8044
|
+
function requiredUserId(candidate) {
|
|
8045
|
+
if (typeof candidate !== "string" || !USER_ID_PATTERN.test(candidate.trim())) {
|
|
8046
|
+
throw new Error("'user_id' must be a UUID");
|
|
8047
|
+
}
|
|
8048
|
+
return candidate.trim().toLowerCase();
|
|
8049
|
+
}
|
|
8050
|
+
function boundedPage(candidate) {
|
|
8051
|
+
if (candidate === undefined)
|
|
8052
|
+
return 1;
|
|
8053
|
+
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < 1) {
|
|
8054
|
+
throw new Error("'page' must be a positive integer");
|
|
8055
|
+
}
|
|
8056
|
+
return candidate;
|
|
8057
|
+
}
|
|
8058
|
+
function boundedPerPage(candidate) {
|
|
8059
|
+
if (candidate === undefined)
|
|
8060
|
+
return 50;
|
|
8061
|
+
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < 1 || candidate > 100) {
|
|
8062
|
+
throw new Error("'per_page' must be an integer between 1 and 100");
|
|
8063
|
+
}
|
|
8064
|
+
return candidate;
|
|
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
|
+
}
|
|
8081
|
+
function safeRedirectTo(candidate) {
|
|
8082
|
+
if (candidate === undefined)
|
|
8083
|
+
return;
|
|
8084
|
+
const value = boundedText(candidate, "redirect_to", MAX_AUTH_REDIRECT_LENGTH);
|
|
8085
|
+
let uri;
|
|
8086
|
+
try {
|
|
8087
|
+
uri = new URL(value);
|
|
8088
|
+
} catch {
|
|
8089
|
+
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL");
|
|
8090
|
+
}
|
|
8091
|
+
const loopback = uri.hostname === "localhost" || uri.hostname.endsWith(".localhost") || uri.hostname === "127.0.0.1" || uri.hostname === "[::1]";
|
|
8092
|
+
const validProtocol = uri.protocol === "https:" || uri.protocol === "http:" && loopback && Boolean(uri.port);
|
|
8093
|
+
if (!validProtocol || uri.username || uri.password || uri.hash) {
|
|
8094
|
+
throw new Error("'redirect_to' must be an absolute HTTPS or loopback HTTP URL without credentials or fragment");
|
|
8095
|
+
}
|
|
8096
|
+
return uri.toString();
|
|
8097
|
+
}
|
|
8098
|
+
function isRecord(candidate) {
|
|
8099
|
+
return candidate !== null && typeof candidate === "object" && !Array.isArray(candidate);
|
|
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
|
+
}
|
|
8111
|
+
function projectUser(candidate) {
|
|
8112
|
+
if (!isRecord(candidate) || typeof candidate.id !== "string" || !USER_ID_PATTERN.test(candidate.id))
|
|
8113
|
+
return null;
|
|
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
|
+
};
|
|
8121
|
+
for (const field of SAFE_USER_FIELDS) {
|
|
8122
|
+
const value = safeOptionalUserField(candidate, field, fieldLimits[field]);
|
|
8123
|
+
if (value === INVALID_FIELD)
|
|
8124
|
+
return null;
|
|
8125
|
+
if (value !== undefined)
|
|
8126
|
+
projectedUser[field] = value;
|
|
8127
|
+
}
|
|
8128
|
+
return projectedUser;
|
|
8129
|
+
}
|
|
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)
|
|
8139
|
+
return null;
|
|
8140
|
+
const users = candidate.users.map(projectUser);
|
|
8141
|
+
if (users.some((user) => user === null))
|
|
8142
|
+
return null;
|
|
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 };
|
|
8148
|
+
for (const field of ["total", "page", "per_page", "next_page", "last_page"]) {
|
|
8149
|
+
const value = safePaginationField(candidate[field]);
|
|
8150
|
+
if (value === INVALID_FIELD)
|
|
8151
|
+
return null;
|
|
8152
|
+
if (value !== undefined)
|
|
8153
|
+
projectedFields[field] = value;
|
|
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;
|
|
8161
|
+
return projectedFields;
|
|
8162
|
+
}
|
|
8163
|
+
function actionLink(candidate) {
|
|
8164
|
+
const candidates = [candidate];
|
|
8165
|
+
if (isRecord(candidate)) {
|
|
8166
|
+
candidates.push(candidate.data, candidate.properties);
|
|
8167
|
+
if (isRecord(candidate.data))
|
|
8168
|
+
candidates.push(candidate.data.properties);
|
|
8169
|
+
}
|
|
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 {}
|
|
8181
|
+
}
|
|
8182
|
+
return null;
|
|
8183
|
+
}
|
|
8184
|
+
function safeAuthReadFailure(operation, response) {
|
|
8185
|
+
return {
|
|
8186
|
+
isError: true,
|
|
8187
|
+
content: [{
|
|
8188
|
+
type: "text",
|
|
8189
|
+
text: JSON.stringify({
|
|
8190
|
+
ok: false,
|
|
8191
|
+
operation,
|
|
8192
|
+
http_status: response.transportError || response.responseReadError ? null : response.status,
|
|
8193
|
+
error: response.responseReadError ? "INVALID_RESPONSE" : response.transportError ? "NETWORK_ERROR" : "HTTP_ERROR"
|
|
8194
|
+
}, null, 2)
|
|
8195
|
+
}]
|
|
8196
|
+
};
|
|
8197
|
+
}
|
|
8198
|
+
async function listUsers(http, args) {
|
|
8199
|
+
const ref = requiredRef(args.ref);
|
|
8200
|
+
const page = boundedPage(args.page);
|
|
8201
|
+
const perPage = boundedPerPage(args.per_page);
|
|
8202
|
+
const params = new URLSearchParams({ page: String(page), per_page: String(perPage) });
|
|
8203
|
+
for (const key of ["search", "email_like"]) {
|
|
8204
|
+
if (args[key] !== undefined)
|
|
8205
|
+
params.set(key, boundedSearch(args[key], key));
|
|
8206
|
+
}
|
|
8207
|
+
const response = await http.get(`/v1/projects/${ref}/auth/users?${params.toString()}`, {
|
|
8208
|
+
maxJsonBytes: MAX_AUTH_READ_BYTES,
|
|
8209
|
+
responseTimeoutMs: AUTH_READ_TIMEOUT_MS
|
|
8210
|
+
});
|
|
8211
|
+
if (!response.ok)
|
|
8212
|
+
return safeAuthReadFailure("auth.list_users", response);
|
|
8213
|
+
const users = projectUserList(response.data, page, perPage);
|
|
8214
|
+
if (!users)
|
|
8215
|
+
return safeAuthReadFailure("auth.list_users", { ...response, responseReadError: true });
|
|
8216
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.list_users", project_ref: ref, ...users }, null, 2) }] };
|
|
8217
|
+
}
|
|
8218
|
+
async function getUser(http, args) {
|
|
8219
|
+
const ref = requiredRef(args.ref);
|
|
8220
|
+
const userId = requiredUserId(args.user_id);
|
|
8221
|
+
const response = await http.get(`/v1/projects/${ref}/auth/users/${encodeURIComponent(userId)}`, {
|
|
8222
|
+
maxJsonBytes: MAX_AUTH_READ_BYTES,
|
|
8223
|
+
responseTimeoutMs: AUTH_READ_TIMEOUT_MS
|
|
8224
|
+
});
|
|
8225
|
+
if (!response.ok)
|
|
8226
|
+
return safeAuthReadFailure("auth.get_user", response);
|
|
8227
|
+
const user = projectUser(response.data);
|
|
8228
|
+
if (!user || user.id !== userId) {
|
|
8229
|
+
return safeAuthReadFailure("auth.get_user", { ...response, responseReadError: true });
|
|
8230
|
+
}
|
|
8231
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.get_user", project_ref: ref, user }, null, 2) }] };
|
|
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
|
+
}
|
|
8250
|
+
async function generateLink(http, args) {
|
|
8251
|
+
const ref = requiredRef(args.ref);
|
|
8252
|
+
if (typeof args.type !== "string" || !AUTH_LINK_TYPES.includes(args.type)) {
|
|
8253
|
+
throw new Error("'type' must be one of magiclink, recovery, or invite for 'generate_link'");
|
|
8254
|
+
}
|
|
8255
|
+
const body = { type: args.type, email: requiredEmail(args.email) };
|
|
8256
|
+
const redirectTo = safeRedirectTo(args.redirect_to);
|
|
8257
|
+
if (redirectTo)
|
|
8258
|
+
body.redirect_to = redirectTo;
|
|
8259
|
+
const response = await http.postReleaseMutation(`/v1/projects/${ref}/auth/generate_link`, body);
|
|
8260
|
+
if (!response.ok)
|
|
8261
|
+
return generateLinkFailure(response);
|
|
8262
|
+
const link = actionLink(response.data);
|
|
8263
|
+
if (!link)
|
|
8264
|
+
return generateLinkFailure({ ...response, responseReadError: true });
|
|
8265
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, operation: "auth.generate_link", action_link: link }, null, 2) }] };
|
|
8266
|
+
}
|
|
8022
8267
|
function formatProviders(data) {
|
|
8023
8268
|
if (!data || typeof data !== "object")
|
|
8024
8269
|
return JSON.stringify(data, null, 2);
|
|
@@ -8051,9 +8296,12 @@ function formatProviders(data) {
|
|
|
8051
8296
|
return out;
|
|
8052
8297
|
}
|
|
8053
8298
|
function registerAuthTools(server, http) {
|
|
8054
|
-
server.tool("auth", `Auth & OAuth provider management.
|
|
8055
|
-
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`, {
|
|
8299
|
+
server.tool("auth", `Auth & OAuth provider management, controlled user lookup, and login-link generation.
|
|
8300
|
+
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`, {
|
|
8056
8301
|
action: withDescription(stringEnum([
|
|
8302
|
+
"list_users",
|
|
8303
|
+
"get_user",
|
|
8304
|
+
"generate_link",
|
|
8057
8305
|
"list_providers",
|
|
8058
8306
|
"get_provider",
|
|
8059
8307
|
"configure_provider",
|
|
@@ -8068,6 +8316,14 @@ Actions: list_providers, get_provider, configure_provider, update_provider, disa
|
|
|
8068
8316
|
"update_config"
|
|
8069
8317
|
]), "Action to perform"),
|
|
8070
8318
|
ref: optional(Type.String(), "Project ref (required for most actions)"),
|
|
8319
|
+
user_id: optional(Type.String(), "[get_user] Exact auth user UUID"),
|
|
8320
|
+
page: optional(Type.Integer({ minimum: 1 }), "[list_users] 1-based page"),
|
|
8321
|
+
per_page: optional(Type.Integer({ minimum: 1, maximum: 100 }), "[list_users] Users per page (1-100)"),
|
|
8322
|
+
search: optional(Type.String(), "[list_users] Search user email, phone, or UUID"),
|
|
8323
|
+
email_like: optional(Type.String(), "[list_users] Search user email or phone"),
|
|
8324
|
+
type: optional(stringEnum(AUTH_LINK_TYPES), "[generate_link] magiclink, recovery, or invite"),
|
|
8325
|
+
email: optional(Type.String(), "[generate_link] User email"),
|
|
8326
|
+
redirect_to: optional(Type.String(), "[generate_link] Absolute HTTPS or loopback callback"),
|
|
8071
8327
|
provider: optional(Type.String(), "[*_provider] Provider name (github, google, wechat, etc.)"),
|
|
8072
8328
|
client_id: optional(Type.String(), "[configure/update] OAuth Client ID"),
|
|
8073
8329
|
client_secret: optional(Type.String(), "[configure/update] OAuth Client Secret"),
|
|
@@ -8085,6 +8341,12 @@ Actions: list_providers, get_provider, configure_provider, update_provider, disa
|
|
|
8085
8341
|
const ok = (res) => res.ok ? JSON.stringify(res.data, null, 2) : `❌ Failed (${res.status}): ${JSON.stringify(res.data)}`;
|
|
8086
8342
|
let text;
|
|
8087
8343
|
switch (action) {
|
|
8344
|
+
case "list_users":
|
|
8345
|
+
return listUsers(http, args);
|
|
8346
|
+
case "get_user":
|
|
8347
|
+
return getUser(http, args);
|
|
8348
|
+
case "generate_link":
|
|
8349
|
+
return generateLink(http, args);
|
|
8088
8350
|
case "list_providers":
|
|
8089
8351
|
need("ref");
|
|
8090
8352
|
const lp = await http.get(`/v1/projects/${ref}/auth/providers`);
|
|
@@ -8180,7 +8442,7 @@ var RELEASE_CANARY_CLIENT_NAME = "supacloud-release-canary";
|
|
|
8180
8442
|
var CLIENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,256}$/;
|
|
8181
8443
|
var MAX_CLIENT_LIST_BYTES = 256 * 1024;
|
|
8182
8444
|
var READ_TIMEOUT_MS = 5000;
|
|
8183
|
-
function
|
|
8445
|
+
function isRecord2(value) {
|
|
8184
8446
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8185
8447
|
}
|
|
8186
8448
|
function releaseCanaryCallbackUri(value) {
|
|
@@ -8202,7 +8464,7 @@ function releaseCanaryCallbackUri(value) {
|
|
|
8202
8464
|
return uri.toString();
|
|
8203
8465
|
}
|
|
8204
8466
|
function createdClientId(value) {
|
|
8205
|
-
if (!
|
|
8467
|
+
if (!isRecord2(value))
|
|
8206
8468
|
return null;
|
|
8207
8469
|
try {
|
|
8208
8470
|
return clientId(value.client_id);
|
|
@@ -8225,7 +8487,7 @@ function oauthClientsPath(ref) {
|
|
|
8225
8487
|
return `/v1/projects/${encodeURIComponent(ref)}/auth/oauth-clients`;
|
|
8226
8488
|
}
|
|
8227
8489
|
function expectedClient(value, redirectUri) {
|
|
8228
|
-
if (!
|
|
8490
|
+
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")
|
|
8229
8491
|
return null;
|
|
8230
8492
|
let callback;
|
|
8231
8493
|
try {
|
|
@@ -8246,9 +8508,9 @@ function expectedClient(value, redirectUri) {
|
|
|
8246
8508
|
};
|
|
8247
8509
|
}
|
|
8248
8510
|
function clientInventory(value) {
|
|
8249
|
-
if (!
|
|
8511
|
+
if (!isRecord2(value) || !Array.isArray(value.clients))
|
|
8250
8512
|
return null;
|
|
8251
|
-
const clients = value.clients.filter((client) =>
|
|
8513
|
+
const clients = value.clients.filter((client) => isRecord2(client) && client.client_name === RELEASE_CANARY_CLIENT_NAME).map((client) => expectedClient(client));
|
|
8252
8514
|
if (clients.some((client) => client === null))
|
|
8253
8515
|
return null;
|
|
8254
8516
|
const inventory = clients;
|
|
@@ -10054,15 +10316,15 @@ function hasWellFormedUnicode(text) {
|
|
|
10054
10316
|
}
|
|
10055
10317
|
return true;
|
|
10056
10318
|
}
|
|
10057
|
-
function
|
|
10319
|
+
function boundedText2(candidate, maxLength) {
|
|
10058
10320
|
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) && hasWellFormedUnicode(candidate) ? candidate : null;
|
|
10059
10321
|
}
|
|
10060
10322
|
function matchingText(candidate, maxLength, pattern) {
|
|
10061
|
-
const candidateText =
|
|
10323
|
+
const candidateText = boundedText2(candidate, maxLength);
|
|
10062
10324
|
return candidateText && pattern.test(candidateText) ? candidateText : null;
|
|
10063
10325
|
}
|
|
10064
10326
|
function canonicalTimestamp(candidate) {
|
|
10065
|
-
const timestamp =
|
|
10327
|
+
const timestamp = boundedText2(candidate, 64);
|
|
10066
10328
|
if (!timestamp)
|
|
10067
10329
|
return null;
|
|
10068
10330
|
const milliseconds = Date.parse(timestamp);
|
|
@@ -10074,7 +10336,7 @@ function projectedSummary(project) {
|
|
|
10074
10336
|
ref: matchingText(project.ref, 20, PROJECT_REF_PATTERN3),
|
|
10075
10337
|
organization_id: matchingText(project.organization_id, 128, SAFE_IDENTIFIER_PATTERN),
|
|
10076
10338
|
organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
|
|
10077
|
-
name:
|
|
10339
|
+
name: boundedText2(project.name, 100),
|
|
10078
10340
|
region: matchingText(project.region, 64, REGION_PATTERN),
|
|
10079
10341
|
created_at: canonicalTimestamp(project.created_at),
|
|
10080
10342
|
status: matchingText(project.status, 64, STATUS_PATTERN)
|
|
@@ -10082,7 +10344,7 @@ function projectedSummary(project) {
|
|
|
10082
10344
|
return Object.values(summary).every((field) => field !== null) ? summary : null;
|
|
10083
10345
|
}
|
|
10084
10346
|
function databaseHost(candidate) {
|
|
10085
|
-
const host =
|
|
10347
|
+
const host = boundedText2(candidate, 255);
|
|
10086
10348
|
if (!host)
|
|
10087
10349
|
return null;
|
|
10088
10350
|
if (host.startsWith("[") && host.endsWith("]")) {
|
|
@@ -10122,7 +10384,7 @@ function projectEndpoint(candidate) {
|
|
|
10122
10384
|
const endpoint = plainRecord(candidate);
|
|
10123
10385
|
if (!endpoint || !hasOnlyKeys(endpoint, PROJECT_ENDPOINT_KEYS))
|
|
10124
10386
|
return null;
|
|
10125
|
-
const endpointUrl =
|
|
10387
|
+
const endpointUrl = boundedText2(endpoint.url, 2048);
|
|
10126
10388
|
if (!endpointUrl || !rawUrlHasNoPath(endpointUrl))
|
|
10127
10389
|
return null;
|
|
10128
10390
|
try {
|
|
@@ -10139,7 +10401,7 @@ function projectEndpoint(candidate) {
|
|
|
10139
10401
|
function discardedDetailFieldsAreValid(project) {
|
|
10140
10402
|
if (project.config !== undefined && plainRecord(project.config) === null)
|
|
10141
10403
|
return false;
|
|
10142
|
-
if (project.anon_key !== undefined &&
|
|
10404
|
+
if (project.anon_key !== undefined && boundedText2(project.anon_key, 16384) === null)
|
|
10143
10405
|
return false;
|
|
10144
10406
|
return project.services === undefined || Array.isArray(project.services);
|
|
10145
10407
|
}
|
|
@@ -10218,11 +10480,11 @@ function plainRecord2(candidate) {
|
|
|
10218
10480
|
function hasOnlyKeys2(record, allowedKeys) {
|
|
10219
10481
|
return Object.keys(record).every((key) => allowedKeys.has(key));
|
|
10220
10482
|
}
|
|
10221
|
-
function
|
|
10483
|
+
function boundedText3(candidate, maxLength) {
|
|
10222
10484
|
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= maxLength && !/[\u0000-\u001f\u007f]/u.test(candidate) ? candidate : null;
|
|
10223
10485
|
}
|
|
10224
10486
|
function canonicalHost(candidate, scheme) {
|
|
10225
|
-
const host =
|
|
10487
|
+
const host = boundedText3(candidate, 255);
|
|
10226
10488
|
if (!host)
|
|
10227
10489
|
return null;
|
|
10228
10490
|
try {
|
|
@@ -10237,8 +10499,8 @@ function projectEndpoint2(candidate) {
|
|
|
10237
10499
|
if (!endpoint || !hasOnlyKeys2(endpoint, ENDPOINT_KEYS))
|
|
10238
10500
|
return null;
|
|
10239
10501
|
const scheme = endpoint.scheme === "http" || endpoint.scheme === "https" ? endpoint.scheme : null;
|
|
10240
|
-
const origin =
|
|
10241
|
-
const source =
|
|
10502
|
+
const origin = boundedText3(endpoint.origin, 2048);
|
|
10503
|
+
const source = boundedText3(endpoint.source, 64);
|
|
10242
10504
|
if (!scheme || !origin || !source || !PROJECT_ENDPOINT_SOURCES.has(source))
|
|
10243
10505
|
return null;
|
|
10244
10506
|
let parsedOrigin;
|
|
@@ -12567,7 +12829,7 @@ var INVENTORY_MAX_BYTES = 1024 * 1024;
|
|
|
12567
12829
|
var MUTATION_MAX_BYTES = 64 * 1024;
|
|
12568
12830
|
var BACKUP_TIMEOUT_MS = 36 * 60000;
|
|
12569
12831
|
var RELEASE_READ_RESPONSE_TIMEOUT_MS = 5000;
|
|
12570
|
-
function
|
|
12832
|
+
function isRecord3(value) {
|
|
12571
12833
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12572
12834
|
}
|
|
12573
12835
|
function canonicalTimestamp3(value) {
|
|
@@ -12583,7 +12845,7 @@ function backupBelongsToProject(backupId, projectRef2) {
|
|
|
12583
12845
|
return BACKUP_ID.test(backupId) && backupId.startsWith(`logical-full_${projectRef2}_`);
|
|
12584
12846
|
}
|
|
12585
12847
|
function verifiedBackup(value, projectRef2) {
|
|
12586
|
-
if (!
|
|
12848
|
+
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))
|
|
12587
12849
|
return null;
|
|
12588
12850
|
return {
|
|
12589
12851
|
backup_id: value.backup_id,
|
|
@@ -12597,7 +12859,7 @@ function verifiedBackup(value, projectRef2) {
|
|
|
12597
12859
|
};
|
|
12598
12860
|
}
|
|
12599
12861
|
function backupInventory(value, projectRef2) {
|
|
12600
|
-
if (!
|
|
12862
|
+
if (!isRecord3(value) || !Array.isArray(value.backups))
|
|
12601
12863
|
return null;
|
|
12602
12864
|
const backups = value.backups.map((backup) => verifiedBackup(backup, projectRef2));
|
|
12603
12865
|
if (backups.some((backup) => backup === null))
|
|
@@ -12676,7 +12938,7 @@ function readInventoryFailure(operation, read) {
|
|
|
12676
12938
|
return null;
|
|
12677
12939
|
}
|
|
12678
12940
|
function postgrestStatus(value) {
|
|
12679
|
-
if (!
|
|
12941
|
+
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)))
|
|
12680
12942
|
return null;
|
|
12681
12943
|
return {
|
|
12682
12944
|
desired: value.desired,
|
|
@@ -12697,7 +12959,7 @@ function readPostgrestFailure(operation, read) {
|
|
|
12697
12959
|
return read.response.status === 200 && read.status ? null : releaseControlFailure(operation, "INVALID_RESPONSE", read.response.status);
|
|
12698
12960
|
}
|
|
12699
12961
|
function isRestartReceipt(value) {
|
|
12700
|
-
return
|
|
12962
|
+
return isRecord3(value) && value.service === "postgrest" && value.action === "restart" && value.success === true;
|
|
12701
12963
|
}
|
|
12702
12964
|
function registerReleaseTools(server, http, options = {}) {
|
|
12703
12965
|
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", {
|
|
@@ -12738,7 +13000,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12738
13000
|
if (!mutation2.ok || mutation2.status !== 200) {
|
|
12739
13001
|
return mutationFailure("release.logical_backup.create", mutation2);
|
|
12740
13002
|
}
|
|
12741
|
-
const responseBackup =
|
|
13003
|
+
const responseBackup = isRecord3(mutation2.data) ? verifiedBackup(mutation2.data.backup, projectRef2) : null;
|
|
12742
13004
|
const afterFailure = readInventoryFailure("release.logical_backup.create", after);
|
|
12743
13005
|
const addedBackup = after.inventory && newlyCreatedBackup(before.inventory, after.inventory);
|
|
12744
13006
|
if (!responseBackup || afterFailure || !addedBackup || !equalBackup(responseBackup, addedBackup)) {
|
|
@@ -12763,7 +13025,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12763
13025
|
if (!mutation2.ok || mutation2.status !== 200) {
|
|
12764
13026
|
return mutationFailure("release.logical_backup.restore", mutation2);
|
|
12765
13027
|
}
|
|
12766
|
-
const responseBackup =
|
|
13028
|
+
const responseBackup = isRecord3(mutation2.data) ? verifiedBackup(mutation2.data.restored_backup, projectRef2) : null;
|
|
12767
13029
|
const after = await readInventory(http, projectRef2);
|
|
12768
13030
|
const afterFailure = readInventoryFailure("release.logical_backup.restore", after);
|
|
12769
13031
|
const restoredInventoryBackup = after.inventory?.find((backup) => backup.backup_id === request.backup_id);
|
|
@@ -12803,7 +13065,7 @@ function registerReleaseTools(server, http, options = {}) {
|
|
|
12803
13065
|
// package.json
|
|
12804
13066
|
var package_default = {
|
|
12805
13067
|
name: "@supacloud/cli",
|
|
12806
|
-
version: "0.
|
|
13068
|
+
version: "0.28.1",
|
|
12807
13069
|
description: "Project-scoped CLI for SupaCloud users",
|
|
12808
13070
|
type: "module",
|
|
12809
13071
|
main: "./dist/index.js",
|
package/package.json
CHANGED
|
@@ -17,6 +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; generation supports only `magiclink`, `recovery`, and `invite`, requires production confirmation, and returns only a validated action URL |
|
|
20
21
|
| Manage Auth/Storage/Edge Functions/frontend/secrets | Corresponding project module | Keep deployable config/code in version control |
|
|
21
22
|
| Configure project gateway routes | `gateway` | Requires an admin-capable project token; inspect before write |
|
|
22
23
|
| Install/upgrade/debug SupaCloud servers | `supacloud-admin` | Platform boundary; not a project CLI action |
|
|
@@ -31,7 +32,7 @@ until a project-scoped context is resolved.
|
|
|
31
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.
|
|
32
33
|
- `database`: read/query, schema inspection, extensions, indexes, RLS, stats, migration push, controlled historical baseline, and SQL-file execution.
|
|
33
34
|
- `supabase`: allowlisted official CLI adapter for migration authoring, local reset/diff, explicit-DSN inspection/backup/type generation, and SupaCloud-controlled migration push.
|
|
34
|
-
- `auth`: provider and
|
|
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.
|
|
35
36
|
- `storage`: buckets and object-management workflows.
|
|
36
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.
|
|
37
38
|
- `frontend`: list, build/deploy, domain, and deployment workflows.
|