@supacloud/cli 0.14.6 → 0.16.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/README.md +164 -5
- package/dist/index.js +1606 -174
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6101,6 +6101,39 @@ function coerceCliValue(value) {
|
|
|
6101
6101
|
return Number(value);
|
|
6102
6102
|
return value;
|
|
6103
6103
|
}
|
|
6104
|
+
var DUPLICATE_CLI_FLAG_MESSAGE = "Duplicate CLI flag is not allowed";
|
|
6105
|
+
function cliFlagAt(args, index) {
|
|
6106
|
+
const argument = args[index];
|
|
6107
|
+
if (!argument.startsWith("--") || argument.length <= 2)
|
|
6108
|
+
return null;
|
|
6109
|
+
const rawFlag = argument.slice(2);
|
|
6110
|
+
const equalsIndex = rawFlag.indexOf("=");
|
|
6111
|
+
if (equalsIndex >= 0) {
|
|
6112
|
+
return { name: rawFlag.slice(0, equalsIndex), rawValue: rawFlag.slice(equalsIndex + 1), nextIndex: index };
|
|
6113
|
+
}
|
|
6114
|
+
const nextArgument = args[index + 1];
|
|
6115
|
+
if (nextArgument !== undefined && !nextArgument.startsWith("--")) {
|
|
6116
|
+
return { name: rawFlag, rawValue: nextArgument, nextIndex: index + 1 };
|
|
6117
|
+
}
|
|
6118
|
+
return { name: rawFlag, nextIndex: index };
|
|
6119
|
+
}
|
|
6120
|
+
function schemaCompatibleCliValue(field, rawValue) {
|
|
6121
|
+
return field && exports_value2.Check(field, rawValue) ? rawValue : coerceCliValue(rawValue);
|
|
6122
|
+
}
|
|
6123
|
+
function parseCliFlags(args, startIndex, schema) {
|
|
6124
|
+
const parsedFlags = Object.create(null);
|
|
6125
|
+
const fields = schemaProperties(schema);
|
|
6126
|
+
for (let index = startIndex;index < args.length; index++) {
|
|
6127
|
+
const flag = cliFlagAt(args, index);
|
|
6128
|
+
if (!flag)
|
|
6129
|
+
continue;
|
|
6130
|
+
index = flag.nextIndex;
|
|
6131
|
+
if (Object.hasOwn(parsedFlags, flag.name))
|
|
6132
|
+
throw new Error(DUPLICATE_CLI_FLAG_MESSAGE);
|
|
6133
|
+
parsedFlags[flag.name] = flag.rawValue === undefined ? true : schemaCompatibleCliValue(fields[flag.name], flag.rawValue);
|
|
6134
|
+
}
|
|
6135
|
+
return parsedFlags;
|
|
6136
|
+
}
|
|
6104
6137
|
async function runCli(cliTools, args, options = {}) {
|
|
6105
6138
|
const commandName = options.commandName || "supacloud";
|
|
6106
6139
|
const formatAvailableCommands = () => Object.keys(cliTools).filter((k) => !["setup_help", "deploy_web_console"].includes(k)).join(`
|
|
@@ -6168,28 +6201,14 @@ async function runCli(cliTools, args, options = {}) {
|
|
|
6168
6201
|
process.exitCode = 0;
|
|
6169
6202
|
return;
|
|
6170
6203
|
}
|
|
6171
|
-
const parsedArgs = {};
|
|
6172
6204
|
let startIdx = 1;
|
|
6173
6205
|
if (args.length > 1 && !args[1].startsWith("--")) {
|
|
6174
|
-
parsedArgs.action = args[1];
|
|
6175
6206
|
startIdx = 2;
|
|
6176
6207
|
}
|
|
6177
|
-
for (let i = startIdx;i < args.length; i++) {
|
|
6178
|
-
const arg = args[i];
|
|
6179
|
-
if (arg.startsWith("--") && arg.length > 2) {
|
|
6180
|
-
const rawFlag = arg.slice(2);
|
|
6181
|
-
const equalsIndex = rawFlag.indexOf("=");
|
|
6182
|
-
const key = equalsIndex >= 0 ? rawFlag.slice(0, equalsIndex) : rawFlag;
|
|
6183
|
-
let val = true;
|
|
6184
|
-
if (equalsIndex >= 0) {
|
|
6185
|
-
val = coerceCliValue(rawFlag.slice(equalsIndex + 1));
|
|
6186
|
-
} else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
|
|
6187
|
-
val = coerceCliValue(args[++i]);
|
|
6188
|
-
}
|
|
6189
|
-
parsedArgs[key] = val;
|
|
6190
|
-
}
|
|
6191
|
-
}
|
|
6192
6208
|
try {
|
|
6209
|
+
const parsedArgs = parseCliFlags(args, startIdx, tool.schema);
|
|
6210
|
+
if (startIdx === 2)
|
|
6211
|
+
parsedArgs.action = args[1];
|
|
6193
6212
|
const validatedArgs = parseToolArguments(tool.schema, parsedArgs);
|
|
6194
6213
|
const result = await tool.callback(validatedArgs);
|
|
6195
6214
|
if (result && result.content && Array.isArray(result.content)) {
|
|
@@ -6213,50 +6232,106 @@ async function runCli(cliTools, args, options = {}) {
|
|
|
6213
6232
|
}
|
|
6214
6233
|
|
|
6215
6234
|
// src/shared/context.ts
|
|
6235
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6216
6236
|
import { homedir } from "node:os";
|
|
6217
6237
|
import { resolve } from "node:path";
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6238
|
+
|
|
6239
|
+
// src/shared/global-options.ts
|
|
6240
|
+
var GLOBAL_FLAGS = {
|
|
6241
|
+
"--env": "environmentName",
|
|
6242
|
+
"--env-file": "envFile",
|
|
6243
|
+
"--confirm-production": "confirmProduction"
|
|
6244
|
+
};
|
|
6245
|
+
var ENVIRONMENT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
|
|
6246
|
+
function globalFlag(arg) {
|
|
6247
|
+
return Object.keys(GLOBAL_FLAGS).find((flag) => arg === flag || arg.startsWith(`${flag}=`)) ?? null;
|
|
6248
|
+
}
|
|
6249
|
+
function globalFlagValue(args, index, flag) {
|
|
6250
|
+
const inlineValue = args[index].slice(flag.length + 1);
|
|
6251
|
+
if (args[index].startsWith(`${flag}=`)) {
|
|
6252
|
+
if (!inlineValue)
|
|
6253
|
+
throw new Error(`${flag} requires a value`);
|
|
6254
|
+
return { value: inlineValue, consumed: 1 };
|
|
6255
|
+
}
|
|
6256
|
+
const value = args[index + 1];
|
|
6257
|
+
if (!value || value.startsWith("--"))
|
|
6258
|
+
throw new Error(`${flag} requires a value`);
|
|
6259
|
+
return { value, consumed: 2 };
|
|
6260
|
+
}
|
|
6261
|
+
function normalizeEnvironmentName(name) {
|
|
6262
|
+
if (!ENVIRONMENT_NAME_PATTERN.test(name)) {
|
|
6263
|
+
throw new Error("--env and SUPACLOUD_ENV must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$");
|
|
6264
|
+
}
|
|
6265
|
+
const normalized = name.toLowerCase();
|
|
6266
|
+
return normalized === "prod" || normalized === "production" ? "production" : normalized;
|
|
6267
|
+
}
|
|
6268
|
+
function parseGlobalOptions(args) {
|
|
6223
6269
|
const values = {};
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
const key = match[1].trim();
|
|
6232
|
-
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
6233
|
-
values[key] = value;
|
|
6270
|
+
const remainingArgs = [];
|
|
6271
|
+
for (let index = 0;index < args.length; ) {
|
|
6272
|
+
const flag = globalFlag(args[index]);
|
|
6273
|
+
if (!flag) {
|
|
6274
|
+
remainingArgs.push(args[index]);
|
|
6275
|
+
index += 1;
|
|
6276
|
+
continue;
|
|
6234
6277
|
}
|
|
6235
|
-
|
|
6236
|
-
|
|
6278
|
+
const key = GLOBAL_FLAGS[flag];
|
|
6279
|
+
if (values[key] !== undefined)
|
|
6280
|
+
throw new Error(`${flag} may be provided only once`);
|
|
6281
|
+
const parsed = globalFlagValue(args, index, flag);
|
|
6282
|
+
values[key] = parsed.value;
|
|
6283
|
+
index += parsed.consumed;
|
|
6284
|
+
}
|
|
6285
|
+
if (values.environmentName && values.envFile) {
|
|
6286
|
+
throw new Error("--env and --env-file are mutually exclusive");
|
|
6287
|
+
}
|
|
6288
|
+
if (values.environmentName)
|
|
6289
|
+
normalizeEnvironmentName(values.environmentName);
|
|
6290
|
+
return { ...values, args: remainingArgs };
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
// src/shared/context.ts
|
|
6294
|
+
var CORE_CONTEXT_KEYS = [
|
|
6295
|
+
"SUPABASE_URL",
|
|
6296
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
6297
|
+
"SUPACLOUD_API_URL",
|
|
6298
|
+
"SUPACLOUD_MANAGEMENT_API_URL",
|
|
6299
|
+
"MANAGEMENT_API_URL",
|
|
6300
|
+
"SUPACLOUD_API_TOKEN",
|
|
6301
|
+
"SUPACLOUD_PROJECT_REF",
|
|
6302
|
+
"X_PROJECT_REF",
|
|
6303
|
+
"SUPACLOUD_HOST"
|
|
6304
|
+
];
|
|
6305
|
+
function unquotedEnvValue(rawValue) {
|
|
6306
|
+
const value = rawValue.trim();
|
|
6307
|
+
const quote = value[0];
|
|
6308
|
+
return quote && (quote === '"' || quote === "'") && value.endsWith(quote) ? value.slice(1, -1) : value;
|
|
6309
|
+
}
|
|
6310
|
+
function parseEnvFile(contents) {
|
|
6311
|
+
const values = {};
|
|
6312
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
6313
|
+
const line = rawLine.trim();
|
|
6314
|
+
if (!line || line.startsWith("#"))
|
|
6315
|
+
continue;
|
|
6316
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
6317
|
+
if (!match)
|
|
6318
|
+
continue;
|
|
6319
|
+
values[match[1]] = unquotedEnvValue(match[2]);
|
|
6237
6320
|
}
|
|
6238
6321
|
return values;
|
|
6239
6322
|
}
|
|
6240
|
-
function
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6323
|
+
function readEnvFile(path, required) {
|
|
6324
|
+
if (!existsSync(path)) {
|
|
6325
|
+
if (required)
|
|
6326
|
+
throw new Error(`SupaCloud environment file not found: ${path}`);
|
|
6327
|
+
return {};
|
|
6245
6328
|
}
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6329
|
+
try {
|
|
6330
|
+
return parseEnvFile(readFileSync(path, "utf8"));
|
|
6331
|
+
} catch (error) {
|
|
6332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6333
|
+
throw new Error(`Failed to read SupaCloud environment file ${path}: ${message}`);
|
|
6250
6334
|
}
|
|
6251
|
-
return { value: "", source: "env" };
|
|
6252
|
-
}
|
|
6253
|
-
function detectSource(sources) {
|
|
6254
|
-
const present = new Set(sources.filter((value) => value !== "none"));
|
|
6255
|
-
if (present.size === 0)
|
|
6256
|
-
return "none";
|
|
6257
|
-
if (present.size === 1)
|
|
6258
|
-
return present.has("env") ? "env" : "dotenv";
|
|
6259
|
-
return "mixed";
|
|
6260
6335
|
}
|
|
6261
6336
|
function normalizeUrl(value) {
|
|
6262
6337
|
const trimmed = value.trim().replace(/\/+$/, "");
|
|
@@ -6279,8 +6354,7 @@ function inferProjectRefFromSupabaseUrl(value) {
|
|
|
6279
6354
|
const normalized = normalizeUrl(value);
|
|
6280
6355
|
if (!normalized)
|
|
6281
6356
|
return "";
|
|
6282
|
-
|
|
6283
|
-
return match?.[1] ?? "";
|
|
6357
|
+
return new URL(normalized).hostname.match(/^([a-z0-9-]+)\.api\./i)?.[1] ?? "";
|
|
6284
6358
|
}
|
|
6285
6359
|
function inferManagementApiUrlFromSupabaseUrl(value, projectRef = "") {
|
|
6286
6360
|
const normalized = normalizeUrl(value);
|
|
@@ -6297,43 +6371,209 @@ function inferManagementApiUrlFromSupabaseUrl(value, projectRef = "") {
|
|
|
6297
6371
|
url.hostname = `studio-${ref}.${host.slice(`${ref}.api.`.length)}`;
|
|
6298
6372
|
return url.toString().replace(/\/+$/, "");
|
|
6299
6373
|
}
|
|
6300
|
-
const
|
|
6301
|
-
if (
|
|
6302
|
-
url.hostname = `studio-${
|
|
6374
|
+
const managedHost = host.match(/^([a-z0-9-]+)\.api\.(.+)$/i);
|
|
6375
|
+
if (managedHost) {
|
|
6376
|
+
url.hostname = `studio-${managedHost[1]}.${managedHost[2]}`;
|
|
6303
6377
|
return url.toString().replace(/\/+$/, "");
|
|
6304
6378
|
}
|
|
6305
6379
|
return normalized;
|
|
6306
6380
|
}
|
|
6307
|
-
function
|
|
6308
|
-
const
|
|
6309
|
-
const
|
|
6310
|
-
const explicitApiUrl =
|
|
6311
|
-
const
|
|
6312
|
-
const
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6381
|
+
function sourceProjectCore(values) {
|
|
6382
|
+
const supabaseUrl = normalizeUrl(values.SUPABASE_URL || "");
|
|
6383
|
+
const projectRef = (values.SUPACLOUD_PROJECT_REF || values.X_PROJECT_REF || "").trim() || inferProjectRefFromSupabaseUrl(supabaseUrl);
|
|
6384
|
+
const explicitApiUrl = values.SUPACLOUD_API_URL || values.SUPACLOUD_MANAGEMENT_API_URL || values.MANAGEMENT_API_URL || "";
|
|
6385
|
+
const apiUrl = normalizeUrl(explicitApiUrl) || inferManagementApiUrlFromSupabaseUrl(supabaseUrl, projectRef) || (values.SUPACLOUD_HOST ? `http://${values.SUPACLOUD_HOST}:9090` : "");
|
|
6386
|
+
const apiToken = values.SUPACLOUD_API_TOKEN || values.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
6387
|
+
return { apiUrl, apiToken, projectRef, supabaseUrl };
|
|
6388
|
+
}
|
|
6389
|
+
function processValues(env) {
|
|
6390
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
|
|
6391
|
+
}
|
|
6392
|
+
function hasProcessContext(env) {
|
|
6393
|
+
return CORE_CONTEXT_KEYS.some((key) => Object.hasOwn(env, key));
|
|
6394
|
+
}
|
|
6395
|
+
function completeProjectContext(values) {
|
|
6396
|
+
const core = sourceProjectCore(values);
|
|
6397
|
+
return Boolean(core.apiUrl && core.apiToken && core.projectRef);
|
|
6398
|
+
}
|
|
6399
|
+
function namedEnvironmentSource(cwd, selector) {
|
|
6400
|
+
const environment = normalizeEnvironmentName(selector);
|
|
6401
|
+
const path = resolve(cwd, `.env.supacloud.${selector}`);
|
|
6402
|
+
const values = readEnvFile(path, true);
|
|
6403
|
+
if (values.SUPACLOUD_ENV && normalizeEnvironmentName(values.SUPACLOUD_ENV) !== environment) {
|
|
6404
|
+
throw new Error(`SUPACLOUD_ENV in ${path} does not match selector ${selector}`);
|
|
6405
|
+
}
|
|
6406
|
+
return { values, kind: "named_env_file", path, environment };
|
|
6407
|
+
}
|
|
6408
|
+
function explicitEnvironmentSource(cwd, envFile) {
|
|
6409
|
+
const path = resolve(cwd, envFile);
|
|
6410
|
+
const values = readEnvFile(path, true);
|
|
6411
|
+
if (!values.SUPACLOUD_ENV)
|
|
6412
|
+
throw new Error(`SUPACLOUD_ENV is required in ${path}`);
|
|
6318
6413
|
return {
|
|
6319
|
-
|
|
6414
|
+
values,
|
|
6415
|
+
kind: "explicit_env_file",
|
|
6416
|
+
path,
|
|
6417
|
+
environment: normalizeEnvironmentName(values.SUPACLOUD_ENV)
|
|
6418
|
+
};
|
|
6419
|
+
}
|
|
6420
|
+
function contextSource(env, cwd, selection) {
|
|
6421
|
+
if (selection.environmentName && selection.envFile)
|
|
6422
|
+
throw new Error("Environment selectors are mutually exclusive");
|
|
6423
|
+
if (selection.environmentName)
|
|
6424
|
+
return namedEnvironmentSource(cwd, selection.environmentName);
|
|
6425
|
+
if (selection.envFile)
|
|
6426
|
+
return explicitEnvironmentSource(cwd, selection.envFile);
|
|
6427
|
+
if (env.SUPACLOUD_ENV) {
|
|
6428
|
+
const environment2 = normalizeEnvironmentName(env.SUPACLOUD_ENV);
|
|
6429
|
+
const values2 = processValues(env);
|
|
6430
|
+
if (completeProjectContext(values2)) {
|
|
6431
|
+
return { values: values2, kind: "process_env", path: null, environment: environment2 };
|
|
6432
|
+
}
|
|
6433
|
+
return namedEnvironmentSource(cwd, env.SUPACLOUD_ENV);
|
|
6434
|
+
}
|
|
6435
|
+
if (hasProcessContext(env)) {
|
|
6436
|
+
return { values: processValues(env), kind: "process_env", path: null, environment: "" };
|
|
6437
|
+
}
|
|
6438
|
+
const path = resolve(cwd, ".env");
|
|
6439
|
+
const values = readEnvFile(path, false);
|
|
6440
|
+
const environment = values.SUPACLOUD_ENV ? normalizeEnvironmentName(values.SUPACLOUD_ENV) : "";
|
|
6441
|
+
return { values, kind: Object.keys(values).length ? "legacy_dotenv" : "none", path: existsSync(path) ? path : null, environment };
|
|
6442
|
+
}
|
|
6443
|
+
function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selection = {}) {
|
|
6444
|
+
const source = contextSource(env, cwd, selection);
|
|
6445
|
+
const core = sourceProjectCore(source.values);
|
|
6446
|
+
const host = source.values.SUPACLOUD_HOST || hostFromUrl(core.apiUrl || core.supabaseUrl);
|
|
6447
|
+
const readOnly = env.SUPACLOUD_READ_ONLY === "true" || source.values.SUPACLOUD_READ_ONLY === "true";
|
|
6448
|
+
return {
|
|
6449
|
+
host,
|
|
6320
6450
|
sshUser: env.SUPACLOUD_SSH_USER ?? "root",
|
|
6321
6451
|
sshPort: parseInt(env.SUPACLOUD_SSH_PORT ?? "22", 10),
|
|
6322
6452
|
sshKey: env.SUPACLOUD_SSH_KEY ?? resolve(homedir(), ".ssh", "id_rsa"),
|
|
6323
6453
|
sshPass: env.SUPACLOUD_SSH_PASS ?? "",
|
|
6324
|
-
apiUrl,
|
|
6325
|
-
apiToken:
|
|
6326
|
-
projectRef,
|
|
6327
|
-
readOnly
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6454
|
+
apiUrl: core.apiUrl,
|
|
6455
|
+
apiToken: core.apiToken,
|
|
6456
|
+
projectRef: core.projectRef,
|
|
6457
|
+
readOnly,
|
|
6458
|
+
environment: source.environment,
|
|
6459
|
+
production: source.environment === "prod" || source.environment === "production",
|
|
6460
|
+
inferredSupabaseUrl: core.supabaseUrl,
|
|
6461
|
+
inferredServiceRoleKey: source.values.SUPABASE_SERVICE_ROLE_KEY || "",
|
|
6462
|
+
source: source.kind,
|
|
6463
|
+
sourcePath: source.path
|
|
6334
6464
|
};
|
|
6335
6465
|
}
|
|
6336
6466
|
|
|
6467
|
+
// src/shared/execution-policy.ts
|
|
6468
|
+
var ACTION_POLICY = {
|
|
6469
|
+
project: {
|
|
6470
|
+
read: ["get", "health", "logs", "api_keys", "settings", "tasks", "task_detail", "task_stats", "dlq", "background_settings"],
|
|
6471
|
+
write: ["task_cancel", "task_retry", "update_background_settings"]
|
|
6472
|
+
},
|
|
6473
|
+
database: {
|
|
6474
|
+
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"],
|
|
6475
|
+
write: ["query", "apply_migration", "push_migrations", "baseline_migrations", "create_table_rls"]
|
|
6476
|
+
},
|
|
6477
|
+
supabase: {
|
|
6478
|
+
local: ["version", "migration_new", "db_diff", "db_reset", "db_pull", "db_dump", "migration_list", "gen_types"],
|
|
6479
|
+
write: ["push"]
|
|
6480
|
+
},
|
|
6481
|
+
auth: {
|
|
6482
|
+
read: ["list_providers", "get_provider", "supported_providers", "get_settings", "get_config"],
|
|
6483
|
+
write: ["configure_provider", "update_provider", "disable_provider", "wechat_mini", "wechat_open", "update_settings", "update_config"]
|
|
6484
|
+
},
|
|
6485
|
+
storage: {
|
|
6486
|
+
read: ["status", "list_buckets", "get_bucket", "list_files"],
|
|
6487
|
+
write: ["create_bucket", "update_bucket", "delete_bucket", "upload_base64", "delete_file"]
|
|
6488
|
+
},
|
|
6489
|
+
edge_functions: {
|
|
6490
|
+
read: ["list", "source"],
|
|
6491
|
+
local: ["check"],
|
|
6492
|
+
write: ["deploy", "deploy_bundle", "config", "activate", "delete"]
|
|
6493
|
+
},
|
|
6494
|
+
scheduled_functions: { read: ["list"], write: ["create", "update", "delete"] },
|
|
6495
|
+
secrets: { read: ["list"], write: ["upsert", "delete"] },
|
|
6496
|
+
frontend: {
|
|
6497
|
+
read: ["list", "get", "build_logs", "list_frameworks", "list_records"],
|
|
6498
|
+
write: ["create", "update", "delete", "deploy_git", "deploy_upload", "redeploy", "add_domain", "remove_domain", "set_env"]
|
|
6499
|
+
},
|
|
6500
|
+
task_events: { read: ["inspect_webhook"], write: ["register_webhook", "unregister_webhook"] },
|
|
6501
|
+
diagnostics: { read: ["list_checks", "get_run"], write: ["run_checks", "repair"] },
|
|
6502
|
+
gateway: {
|
|
6503
|
+
read: ["routes", "get_certificate", "custom_hostname"],
|
|
6504
|
+
write: ["upsert_route", "update_route", "delete_route", "config", "update_certificate", "issue_certificate", "deploy_certificate", "rebuild", "set_custom_hostname", "delete_custom_hostname", "verify_custom_hostname"]
|
|
6505
|
+
},
|
|
6506
|
+
branch: { read: ["list", "promotion_plan"], write: ["create", "delete", "promote"] },
|
|
6507
|
+
queue: {
|
|
6508
|
+
read: ["list", "stats", "list_messages", "dlq", "get_message", "get_settings"],
|
|
6509
|
+
write: ["send", "receive", "ack", "release", "fail", "retry", "delete_message", "update_settings"]
|
|
6510
|
+
},
|
|
6511
|
+
ai: { local: ["show_skill", "install_skill"] }
|
|
6512
|
+
};
|
|
6513
|
+
function declaredMode(moduleName, action) {
|
|
6514
|
+
const policy = ACTION_POLICY[moduleName];
|
|
6515
|
+
if (!policy)
|
|
6516
|
+
return;
|
|
6517
|
+
for (const mode of ["read", "write", "local"]) {
|
|
6518
|
+
if (policy[mode]?.includes(action))
|
|
6519
|
+
return mode;
|
|
6520
|
+
}
|
|
6521
|
+
return;
|
|
6522
|
+
}
|
|
6523
|
+
function executionMode(moduleName, action, args) {
|
|
6524
|
+
if (moduleName === "database" && ["push_migrations", "baseline_migrations"].includes(action) && args.dry_run === true)
|
|
6525
|
+
return "read";
|
|
6526
|
+
if (moduleName === "supabase" && action === "push" && args.dry_run === true)
|
|
6527
|
+
return "read";
|
|
6528
|
+
return declaredMode(moduleName, action);
|
|
6529
|
+
}
|
|
6530
|
+
function authorizeExecution(moduleName, args, authorization) {
|
|
6531
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
6532
|
+
if (!action)
|
|
6533
|
+
return;
|
|
6534
|
+
const mode = executionMode(moduleName, action, args);
|
|
6535
|
+
const { context, confirmProduction } = authorization;
|
|
6536
|
+
if (!mode && (context.production || context.readOnly)) {
|
|
6537
|
+
throw new Error(`Execution policy has no classification for ${moduleName}.${action}`);
|
|
6538
|
+
}
|
|
6539
|
+
if (context.production && mode === "read" && typeof args.ref === "string" && args.ref && args.ref !== context.projectRef) {
|
|
6540
|
+
throw new Error("Production profiles cannot target a different project with --ref");
|
|
6541
|
+
}
|
|
6542
|
+
if (mode !== "write")
|
|
6543
|
+
return;
|
|
6544
|
+
if (context.production && moduleName === "diagnostics" && action === "repair") {
|
|
6545
|
+
throw new Error("diagnostics repair is forbidden in production environments");
|
|
6546
|
+
}
|
|
6547
|
+
if (context.readOnly) {
|
|
6548
|
+
throw new Error(`Remote write ${moduleName}.${action} is blocked in read-only mode (SUPACLOUD_READ_ONLY=true)`);
|
|
6549
|
+
}
|
|
6550
|
+
if (!context.production)
|
|
6551
|
+
return;
|
|
6552
|
+
const requestedRef = args.ref ?? context.projectRef;
|
|
6553
|
+
if (typeof requestedRef !== "string" || !requestedRef) {
|
|
6554
|
+
throw new Error(`Production write ${moduleName}.${action} requires a project ref`);
|
|
6555
|
+
}
|
|
6556
|
+
if (requestedRef !== context.projectRef) {
|
|
6557
|
+
throw new Error("Production profiles cannot target a different project with --ref");
|
|
6558
|
+
}
|
|
6559
|
+
if (confirmProduction !== context.projectRef || confirmProduction !== requestedRef) {
|
|
6560
|
+
throw new Error(`Production write requires --confirm-production ${context.projectRef}`);
|
|
6561
|
+
}
|
|
6562
|
+
}
|
|
6563
|
+
function validateExecutionPolicyCoverage(tools) {
|
|
6564
|
+
for (const [moduleName, tool] of Object.entries(tools)) {
|
|
6565
|
+
const actionSchema = schemaProperties(tool.schema).action;
|
|
6566
|
+
if (!actionSchema)
|
|
6567
|
+
continue;
|
|
6568
|
+
const actions = schemaEnumValues(actionSchema);
|
|
6569
|
+
for (const action of actions) {
|
|
6570
|
+
if (!declaredMode(moduleName, String(action))) {
|
|
6571
|
+
throw new Error(`Execution policy has no classification for ${moduleName}.${String(action)}`);
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
}
|
|
6576
|
+
|
|
6337
6577
|
// src/shared/transports/http.ts
|
|
6338
6578
|
var DEFAULT_TIMEOUT = 30000;
|
|
6339
6579
|
var MAX_RETRIES = 2;
|
|
@@ -6348,6 +6588,16 @@ function isRetryableError(error) {
|
|
|
6348
6588
|
const networkError = error;
|
|
6349
6589
|
return networkError.name === "AbortError" || networkError.code === "ECONNREFUSED" || networkError.code === "ECONNRESET";
|
|
6350
6590
|
}
|
|
6591
|
+
function transportFailure(error) {
|
|
6592
|
+
const networkError = error instanceof Error ? error : null;
|
|
6593
|
+
const code = networkError?.name === "AbortError" ? "TIMEOUT" : networkError?.code === "ECONNRESET" ? "CONNECTION_RESET" : "NETWORK_ERROR";
|
|
6594
|
+
return {
|
|
6595
|
+
ok: false,
|
|
6596
|
+
status: 500,
|
|
6597
|
+
data: { error: "Network Error", code },
|
|
6598
|
+
transportError: true
|
|
6599
|
+
};
|
|
6600
|
+
}
|
|
6351
6601
|
async function fetchWithTimeout(url, options) {
|
|
6352
6602
|
const controller = new AbortController;
|
|
6353
6603
|
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
@@ -6382,6 +6632,57 @@ async function fetchWithRetry(url, options) {
|
|
|
6382
6632
|
}
|
|
6383
6633
|
throw new Error("Unreachable");
|
|
6384
6634
|
}
|
|
6635
|
+
function declaredResponseTooLarge(response, maxBytes) {
|
|
6636
|
+
const contentLength = response.headers.get("content-length");
|
|
6637
|
+
if (contentLength === null || !/^\d+$/.test(contentLength))
|
|
6638
|
+
return false;
|
|
6639
|
+
return Number(contentLength) > maxBytes;
|
|
6640
|
+
}
|
|
6641
|
+
function joinedResponseBytes(chunks, totalBytes) {
|
|
6642
|
+
const responseBytes = new Uint8Array(totalBytes);
|
|
6643
|
+
let offset = 0;
|
|
6644
|
+
for (const chunk of chunks) {
|
|
6645
|
+
responseBytes.set(chunk, offset);
|
|
6646
|
+
offset += chunk.byteLength;
|
|
6647
|
+
}
|
|
6648
|
+
return responseBytes;
|
|
6649
|
+
}
|
|
6650
|
+
async function responseBytesWithinLimit(response, maxBytes) {
|
|
6651
|
+
if (declaredResponseTooLarge(response, maxBytes)) {
|
|
6652
|
+
await response.body?.cancel();
|
|
6653
|
+
return null;
|
|
6654
|
+
}
|
|
6655
|
+
if (!response.body)
|
|
6656
|
+
return new Uint8Array;
|
|
6657
|
+
const reader = response.body.getReader();
|
|
6658
|
+
const chunks = [];
|
|
6659
|
+
let totalBytes = 0;
|
|
6660
|
+
while (true) {
|
|
6661
|
+
const { done, value } = await reader.read();
|
|
6662
|
+
if (done)
|
|
6663
|
+
return joinedResponseBytes(chunks, totalBytes);
|
|
6664
|
+
totalBytes += value.byteLength;
|
|
6665
|
+
if (totalBytes > maxBytes) {
|
|
6666
|
+
await reader.cancel();
|
|
6667
|
+
return null;
|
|
6668
|
+
}
|
|
6669
|
+
chunks.push(value);
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
function parsedUtf8Json(responseBytes) {
|
|
6673
|
+
try {
|
|
6674
|
+
const responseText = new TextDecoder("utf-8", { fatal: true }).decode(responseBytes);
|
|
6675
|
+
return JSON.parse(responseText);
|
|
6676
|
+
} catch (error) {
|
|
6677
|
+
if (error instanceof SyntaxError || error instanceof TypeError)
|
|
6678
|
+
return null;
|
|
6679
|
+
throw error;
|
|
6680
|
+
}
|
|
6681
|
+
}
|
|
6682
|
+
async function boundedResponseJson(response, maxBytes) {
|
|
6683
|
+
const responseBytes = await responseBytesWithinLimit(response, maxBytes);
|
|
6684
|
+
return responseBytes === null ? null : parsedUtf8Json(responseBytes);
|
|
6685
|
+
}
|
|
6385
6686
|
|
|
6386
6687
|
class HttpTransport {
|
|
6387
6688
|
baseUrl;
|
|
@@ -6396,16 +6697,16 @@ class HttpTransport {
|
|
|
6396
6697
|
"Content-Type": "application/json"
|
|
6397
6698
|
};
|
|
6398
6699
|
}
|
|
6399
|
-
async get(path) {
|
|
6700
|
+
async get(path, options = {}) {
|
|
6400
6701
|
try {
|
|
6401
6702
|
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
6402
6703
|
method: "GET",
|
|
6403
6704
|
headers: this.headers()
|
|
6404
6705
|
});
|
|
6405
|
-
const data = await res.json().catch(() => null);
|
|
6706
|
+
const data = options.maxResponseBytes === undefined ? await res.json().catch(() => null) : await boundedResponseJson(res, options.maxResponseBytes);
|
|
6406
6707
|
return { ok: res.ok, status: res.status, data };
|
|
6407
6708
|
} catch (error) {
|
|
6408
|
-
return
|
|
6709
|
+
return transportFailure(error);
|
|
6409
6710
|
}
|
|
6410
6711
|
}
|
|
6411
6712
|
async post(path, body) {
|
|
@@ -6418,7 +6719,7 @@ class HttpTransport {
|
|
|
6418
6719
|
const data = await res.json().catch(() => null);
|
|
6419
6720
|
return { ok: res.ok, status: res.status, data };
|
|
6420
6721
|
} catch (error) {
|
|
6421
|
-
return
|
|
6722
|
+
return transportFailure(error);
|
|
6422
6723
|
}
|
|
6423
6724
|
}
|
|
6424
6725
|
async postMultipart(path, formData) {
|
|
@@ -6432,7 +6733,7 @@ class HttpTransport {
|
|
|
6432
6733
|
const data = await res.json().catch(() => null);
|
|
6433
6734
|
return { ok: res.ok, status: res.status, data };
|
|
6434
6735
|
} catch (error) {
|
|
6435
|
-
return
|
|
6736
|
+
return transportFailure(error);
|
|
6436
6737
|
}
|
|
6437
6738
|
}
|
|
6438
6739
|
async patch(path, body) {
|
|
@@ -6445,7 +6746,7 @@ class HttpTransport {
|
|
|
6445
6746
|
const data = await res.json().catch(() => null);
|
|
6446
6747
|
return { ok: res.ok, status: res.status, data };
|
|
6447
6748
|
} catch (error) {
|
|
6448
|
-
return
|
|
6749
|
+
return transportFailure(error);
|
|
6449
6750
|
}
|
|
6450
6751
|
}
|
|
6451
6752
|
async put(path, body) {
|
|
@@ -6458,7 +6759,7 @@ class HttpTransport {
|
|
|
6458
6759
|
const data = await res.json().catch(() => null);
|
|
6459
6760
|
return { ok: res.ok, status: res.status, data };
|
|
6460
6761
|
} catch (error) {
|
|
6461
|
-
return
|
|
6762
|
+
return transportFailure(error);
|
|
6462
6763
|
}
|
|
6463
6764
|
}
|
|
6464
6765
|
async delete(path) {
|
|
@@ -6470,7 +6771,7 @@ class HttpTransport {
|
|
|
6470
6771
|
const data = await res.json().catch(() => null);
|
|
6471
6772
|
return { ok: res.ok, status: res.status, data };
|
|
6472
6773
|
} catch (error) {
|
|
6473
|
-
return
|
|
6774
|
+
return transportFailure(error);
|
|
6474
6775
|
}
|
|
6475
6776
|
}
|
|
6476
6777
|
async ping() {
|
|
@@ -6483,10 +6784,120 @@ class HttpTransport {
|
|
|
6483
6784
|
import { createHash } from "node:crypto";
|
|
6484
6785
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
6485
6786
|
import { basename, join } from "node:path";
|
|
6787
|
+
|
|
6788
|
+
// src/shared/project-ref.ts
|
|
6789
|
+
var PROJECT_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
6790
|
+
function projectRefPathSegment(ref, operation) {
|
|
6791
|
+
if (typeof ref !== "string" || !PROJECT_REF_PATTERN.test(ref)) {
|
|
6792
|
+
throw new Error(`'ref' is invalid for ${operation}`);
|
|
6793
|
+
}
|
|
6794
|
+
return encodeURIComponent(ref);
|
|
6795
|
+
}
|
|
6796
|
+
|
|
6797
|
+
// src/shared/tools/database-tools.ts
|
|
6486
6798
|
var MAX_MIGRATION_VERSION = 9223372036854775807n;
|
|
6487
6799
|
var FALLBACK_MIGRATION_VERSION_BASE = 8000000000000000000n;
|
|
6488
6800
|
var FALLBACK_MIGRATION_VERSION_RANGE = 1000000000000000000n;
|
|
6489
6801
|
var FALLBACK_MIGRATION_VERSION_LIMIT = FALLBACK_MIGRATION_VERSION_BASE + FALLBACK_MIGRATION_VERSION_RANGE;
|
|
6802
|
+
var MAX_MIGRATION_INVENTORY_BYTES = 64 * 1024 * 1024;
|
|
6803
|
+
function isMigrationInventoryVersion(version) {
|
|
6804
|
+
if (typeof version !== "string" || !/^\d{1,19}$/.test(version))
|
|
6805
|
+
return false;
|
|
6806
|
+
const numericVersion = BigInt(version);
|
|
6807
|
+
return numericVersion >= 1n && numericVersion <= MAX_MIGRATION_VERSION && numericVersion.toString() === version;
|
|
6808
|
+
}
|
|
6809
|
+
function isMigrationInventoryName(name) {
|
|
6810
|
+
return name === null || typeof name === "string" && name.length <= 255 && name.length > 0 && name.trim() === name;
|
|
6811
|
+
}
|
|
6812
|
+
function isMigrationInventoryStatements(statements) {
|
|
6813
|
+
return Array.isArray(statements) && statements.every((statement) => typeof statement === "string" && statement.length > 0 && statement.trim() === statement && !statement.includes("\r"));
|
|
6814
|
+
}
|
|
6815
|
+
function isMigrationInventoryAppliedAt(appliedAt) {
|
|
6816
|
+
return appliedAt === null || typeof appliedAt === "string" && appliedAt.trim() === appliedAt && appliedAt.length > 0 && !Number.isNaN(Date.parse(appliedAt));
|
|
6817
|
+
}
|
|
6818
|
+
function migrationInventoryChecksum(entry) {
|
|
6819
|
+
return createHash("sha256").update(JSON.stringify({
|
|
6820
|
+
version: entry.version,
|
|
6821
|
+
name: entry.name,
|
|
6822
|
+
statements: entry.statements
|
|
6823
|
+
})).digest("hex");
|
|
6824
|
+
}
|
|
6825
|
+
function migrationInventoryEntry(rawEntry) {
|
|
6826
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry))
|
|
6827
|
+
return null;
|
|
6828
|
+
const entry = rawEntry;
|
|
6829
|
+
if (!isMigrationInventoryVersion(entry.version))
|
|
6830
|
+
return null;
|
|
6831
|
+
if (!isMigrationInventoryName(entry.name))
|
|
6832
|
+
return null;
|
|
6833
|
+
if (!isMigrationInventoryStatements(entry.statements))
|
|
6834
|
+
return null;
|
|
6835
|
+
if (typeof entry.statement_count !== "number" || !Number.isInteger(entry.statement_count))
|
|
6836
|
+
return null;
|
|
6837
|
+
if (entry.statement_count !== entry.statements.length)
|
|
6838
|
+
return null;
|
|
6839
|
+
if (typeof entry.checksum !== "string" || !/^[0-9a-f]{64}$/.test(entry.checksum))
|
|
6840
|
+
return null;
|
|
6841
|
+
if (!isMigrationInventoryAppliedAt(entry.applied_at))
|
|
6842
|
+
return null;
|
|
6843
|
+
const migration = {
|
|
6844
|
+
version: entry.version,
|
|
6845
|
+
name: entry.name,
|
|
6846
|
+
statements: entry.statements,
|
|
6847
|
+
statement_count: entry.statement_count,
|
|
6848
|
+
checksum: entry.checksum,
|
|
6849
|
+
applied_at: entry.applied_at
|
|
6850
|
+
};
|
|
6851
|
+
return migration.checksum === migrationInventoryChecksum(migration) ? migration : null;
|
|
6852
|
+
}
|
|
6853
|
+
function compareMigrationInventoryEntries(left, right) {
|
|
6854
|
+
const leftVersion = BigInt(left.version);
|
|
6855
|
+
const rightVersion = BigInt(right.version);
|
|
6856
|
+
if (leftVersion === rightVersion)
|
|
6857
|
+
return 0;
|
|
6858
|
+
return leftVersion < rightVersion ? -1 : 1;
|
|
6859
|
+
}
|
|
6860
|
+
function migrationInventory(payload) {
|
|
6861
|
+
if (!Array.isArray(payload))
|
|
6862
|
+
return null;
|
|
6863
|
+
const inventory = [];
|
|
6864
|
+
const versions = new Set;
|
|
6865
|
+
for (const rawEntry of payload) {
|
|
6866
|
+
const entry = migrationInventoryEntry(rawEntry);
|
|
6867
|
+
if (!entry)
|
|
6868
|
+
return null;
|
|
6869
|
+
if (versions.has(entry.version))
|
|
6870
|
+
return null;
|
|
6871
|
+
versions.add(entry.version);
|
|
6872
|
+
inventory.push(entry);
|
|
6873
|
+
}
|
|
6874
|
+
return inventory.sort(compareMigrationInventoryEntries);
|
|
6875
|
+
}
|
|
6876
|
+
function migrationInventoryPath(ref) {
|
|
6877
|
+
return `/v1/projects/${projectRefPathSegment(ref, "migration_inventory")}/database/migrations`;
|
|
6878
|
+
}
|
|
6879
|
+
function migrationInventoryFailure(code, httpStatus) {
|
|
6880
|
+
return {
|
|
6881
|
+
isError: true,
|
|
6882
|
+
content: [{
|
|
6883
|
+
type: "text",
|
|
6884
|
+
text: JSON.stringify({
|
|
6885
|
+
ok: false,
|
|
6886
|
+
operation: "database.migration_inventory",
|
|
6887
|
+
error: { code, http_status: httpStatus }
|
|
6888
|
+
}, null, 2)
|
|
6889
|
+
}]
|
|
6890
|
+
};
|
|
6891
|
+
}
|
|
6892
|
+
function migrationInventoryResponse(response) {
|
|
6893
|
+
if (!response.ok) {
|
|
6894
|
+
return migrationInventoryFailure("HTTP_ERROR", response.transportError ? null : response.status);
|
|
6895
|
+
}
|
|
6896
|
+
const inventory = migrationInventory(response.data);
|
|
6897
|
+
if (!inventory)
|
|
6898
|
+
return migrationInventoryFailure("INVALID_RESPONSE", response.status);
|
|
6899
|
+
return { content: [{ type: "text", text: JSON.stringify(inventory, null, 2) }] };
|
|
6900
|
+
}
|
|
6490
6901
|
function readMigrationFile(dir, file) {
|
|
6491
6902
|
const rawBytes = readFileSync2(join(dir, file));
|
|
6492
6903
|
return {
|
|
@@ -6638,6 +7049,7 @@ function registerDatabaseTools(server, http, config = {}) {
|
|
|
6638
7049
|
"stats",
|
|
6639
7050
|
"slow_queries",
|
|
6640
7051
|
"list_migrations",
|
|
7052
|
+
"migration_inventory",
|
|
6641
7053
|
"project_url",
|
|
6642
7054
|
"generate_types"
|
|
6643
7055
|
];
|
|
@@ -6662,7 +7074,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6662
7074
|
owner_column: optional(Type.String(), "[create_table_rls owner] UUID owner column matched to auth.uid()")
|
|
6663
7075
|
}, async (args) => {
|
|
6664
7076
|
const { action } = args;
|
|
6665
|
-
const ref =
|
|
7077
|
+
const ref = args.ref || projectRef;
|
|
6666
7078
|
const schema = args.schema || "public";
|
|
6667
7079
|
const schemas = args.schemas || ["public"];
|
|
6668
7080
|
if (args.file && !args.sql) {
|
|
@@ -6777,6 +7189,10 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6777
7189
|
text = r.ok ? formatMigrations(r.data) : `❌ Failed (${r.status})`;
|
|
6778
7190
|
break;
|
|
6779
7191
|
}
|
|
7192
|
+
case "migration_inventory": {
|
|
7193
|
+
const response = await http.get(migrationInventoryPath(ref), { maxResponseBytes: MAX_MIGRATION_INVENTORY_BYTES });
|
|
7194
|
+
return migrationInventoryResponse(response);
|
|
7195
|
+
}
|
|
6780
7196
|
case "project_url": {
|
|
6781
7197
|
const r = await http.get(`/v1/projects/${ref}`);
|
|
6782
7198
|
text = r.ok ? JSON.stringify({ url: r.data.api?.url || `https://${ref}.supabase.co` }, null, 2) : `❌ Failed (${r.status})`;
|
|
@@ -7216,6 +7632,62 @@ function pgToTs(t) {
|
|
|
7216
7632
|
}
|
|
7217
7633
|
|
|
7218
7634
|
// src/shared/tools/auth-tools.ts
|
|
7635
|
+
var authConfigRecord = Type.Record(Type.String(), Type.Unknown());
|
|
7636
|
+
var safeAuthMutationCodes = new Set([
|
|
7637
|
+
"AUTH_RUNTIME_APPLY_FAILED",
|
|
7638
|
+
"SUPAUTH_DEPENDENT_REFRESH_FAILED"
|
|
7639
|
+
]);
|
|
7640
|
+
function parseAuthConfig(input) {
|
|
7641
|
+
if (typeof input !== "string")
|
|
7642
|
+
return input;
|
|
7643
|
+
try {
|
|
7644
|
+
return JSON.parse(input);
|
|
7645
|
+
} catch (error) {
|
|
7646
|
+
if (!(error instanceof SyntaxError))
|
|
7647
|
+
throw error;
|
|
7648
|
+
throw new Error("Invalid auth config JSON object");
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
var authConfigSchema = decodedSchema(Type.Union([Type.String(), authConfigRecord]), authConfigRecord, parseAuthConfig);
|
|
7652
|
+
function safeAuthFailureFields(payload) {
|
|
7653
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
7654
|
+
return {};
|
|
7655
|
+
const body = payload;
|
|
7656
|
+
const fields = {};
|
|
7657
|
+
if (typeof body.code === "string" && safeAuthMutationCodes.has(body.code)) {
|
|
7658
|
+
fields.code = body.code;
|
|
7659
|
+
}
|
|
7660
|
+
for (const field of ["persisted", "runtime_applied", "dependents_applied"]) {
|
|
7661
|
+
if (typeof body[field] === "boolean")
|
|
7662
|
+
fields[field] = body[field];
|
|
7663
|
+
}
|
|
7664
|
+
if (body.dependent_status === "failed" || body.dependent_status === "unknown") {
|
|
7665
|
+
fields.dependent_status = body.dependent_status;
|
|
7666
|
+
}
|
|
7667
|
+
if (body.runtime_mode === "local" || body.runtime_mode === "owner" || body.runtime_mode === "shared") {
|
|
7668
|
+
fields.runtime_mode = body.runtime_mode;
|
|
7669
|
+
}
|
|
7670
|
+
return fields;
|
|
7671
|
+
}
|
|
7672
|
+
function safeAuthMutationFailure(response) {
|
|
7673
|
+
return {
|
|
7674
|
+
ok: false,
|
|
7675
|
+
http_status: response.status,
|
|
7676
|
+
...safeAuthFailureFields(response.data)
|
|
7677
|
+
};
|
|
7678
|
+
}
|
|
7679
|
+
function authMutationResult(response, successMessage) {
|
|
7680
|
+
if (response.ok) {
|
|
7681
|
+
return { content: [{ type: "text", text: successMessage }] };
|
|
7682
|
+
}
|
|
7683
|
+
return {
|
|
7684
|
+
isError: true,
|
|
7685
|
+
content: [{
|
|
7686
|
+
type: "text",
|
|
7687
|
+
text: JSON.stringify(safeAuthMutationFailure(response), null, 2)
|
|
7688
|
+
}]
|
|
7689
|
+
};
|
|
7690
|
+
}
|
|
7219
7691
|
function formatProviders(data) {
|
|
7220
7692
|
if (!data || typeof data !== "object")
|
|
7221
7693
|
return JSON.stringify(data, null, 2);
|
|
@@ -7272,7 +7744,7 @@ Actions: list_providers, get_provider, configure_provider, update_provider, disa
|
|
|
7272
7744
|
url: optional(Type.String(), "[configure] Custom OAuth URL"),
|
|
7273
7745
|
app_id: optional(Type.String(), "[wechat_*] WeChat App ID"),
|
|
7274
7746
|
app_secret: optional(Type.String(), "[wechat_*] WeChat App Secret"),
|
|
7275
|
-
config: optional(
|
|
7747
|
+
config: optional(authConfigSchema, "[update_settings/update_config] Config fields as a JSON object")
|
|
7276
7748
|
}, async (args) => {
|
|
7277
7749
|
const { action, ref, provider, client_id, client_secret, redirect_uri, url, app_id, app_secret, config } = args;
|
|
7278
7750
|
const need = (f) => {
|
|
@@ -7355,8 +7827,7 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
7355
7827
|
need("ref");
|
|
7356
7828
|
if (!config)
|
|
7357
7829
|
throw new Error("'config' required");
|
|
7358
|
-
|
|
7359
|
-
break;
|
|
7830
|
+
return authMutationResult(await http.patch(`/v1/projects/${ref}/auth/config`, config), "✅ Auth settings updated");
|
|
7360
7831
|
case "get_config":
|
|
7361
7832
|
need("ref");
|
|
7362
7833
|
text = ok(await http.get(`/v1/projects/${ref}/config/auth`));
|
|
@@ -7365,8 +7836,7 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
7365
7836
|
need("ref");
|
|
7366
7837
|
if (!config)
|
|
7367
7838
|
throw new Error("'config' required");
|
|
7368
|
-
|
|
7369
|
-
break;
|
|
7839
|
+
return authMutationResult(await http.patch(`/v1/projects/${ref}/config/auth`, config), "✅ Auth config updated");
|
|
7370
7840
|
default:
|
|
7371
7841
|
text = `❌ Unknown action: ${action}`;
|
|
7372
7842
|
}
|
|
@@ -7374,90 +7844,424 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
7374
7844
|
});
|
|
7375
7845
|
}
|
|
7376
7846
|
|
|
7847
|
+
// src/shared/tools/release-control-response.ts
|
|
7848
|
+
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
7849
|
+
function releaseControlSuccess(operation, payload) {
|
|
7850
|
+
return releaseControlResponse({
|
|
7851
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7852
|
+
ok: true,
|
|
7853
|
+
operation,
|
|
7854
|
+
...payload
|
|
7855
|
+
});
|
|
7856
|
+
}
|
|
7857
|
+
function releaseControlFailure(operation, code, httpStatus) {
|
|
7858
|
+
return releaseControlErrorResponse({
|
|
7859
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7860
|
+
ok: false,
|
|
7861
|
+
operation,
|
|
7862
|
+
error: { code, http_status: httpStatus }
|
|
7863
|
+
});
|
|
7864
|
+
}
|
|
7865
|
+
function releaseControlMutationFailure(operation, response) {
|
|
7866
|
+
const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
|
|
7867
|
+
return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
|
|
7868
|
+
}
|
|
7869
|
+
function releaseControlResponse(payload) {
|
|
7870
|
+
return {
|
|
7871
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
7872
|
+
};
|
|
7873
|
+
}
|
|
7874
|
+
function releaseControlErrorResponse(payload) {
|
|
7875
|
+
return { ...releaseControlResponse(payload), isError: true };
|
|
7876
|
+
}
|
|
7877
|
+
|
|
7377
7878
|
// src/shared/tools/storage-tools.ts
|
|
7879
|
+
var MAX_BUCKET_ID_LENGTH = 100;
|
|
7880
|
+
var MAX_MIME_TYPE_COUNT = 100;
|
|
7881
|
+
var MAX_MIME_TYPE_LENGTH = 255;
|
|
7882
|
+
var PROJECT_REF_PATTERN2 = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7883
|
+
var BUCKET_ID_PATTERN = new RegExp(`^(?!\\.+$)[A-Za-z0-9._-]{1,${MAX_BUCKET_ID_LENGTH}}$`);
|
|
7884
|
+
var MIME_TYPE_PATTERN = /^(?=\S)(?=.*\S$)[^\u0000-\u001f\u007f]+$/;
|
|
7885
|
+
var ACTION_ARGUMENTS = {
|
|
7886
|
+
status: new Set(["action"]),
|
|
7887
|
+
list_buckets: new Set(["action", "ref"]),
|
|
7888
|
+
get_bucket: new Set(["action", "ref", "bucket"]),
|
|
7889
|
+
create_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
|
|
7890
|
+
update_bucket: new Set(["action", "ref", "bucket", "public", "file_size_limit", "allowed_mime_types"]),
|
|
7891
|
+
delete_bucket: new Set(["action", "ref", "bucket"]),
|
|
7892
|
+
list_files: new Set(["action", "ref", "bucket"]),
|
|
7893
|
+
upload_base64: new Set(["action", "ref", "bucket", "filename", "base64_content", "mime_type"]),
|
|
7894
|
+
delete_file: new Set(["action", "ref", "bucket", "filename"])
|
|
7895
|
+
};
|
|
7896
|
+
function normalizedMimeTypes(candidate) {
|
|
7897
|
+
return Array.isArray(candidate) ? candidate.map((mimeType) => typeof mimeType === "string" ? mimeType.trim() : mimeType) : candidate;
|
|
7898
|
+
}
|
|
7899
|
+
function parseAllowedMimeTypes(input) {
|
|
7900
|
+
if (Array.isArray(input))
|
|
7901
|
+
return normalizedMimeTypes(input);
|
|
7902
|
+
const trimmed = input.trim();
|
|
7903
|
+
if (!trimmed)
|
|
7904
|
+
return [];
|
|
7905
|
+
if (!trimmed.startsWith("[")) {
|
|
7906
|
+
return normalizedMimeTypes(trimmed.split(","));
|
|
7907
|
+
}
|
|
7908
|
+
try {
|
|
7909
|
+
return normalizedMimeTypes(JSON.parse(trimmed));
|
|
7910
|
+
} catch (error) {
|
|
7911
|
+
if (!(error instanceof SyntaxError))
|
|
7912
|
+
throw error;
|
|
7913
|
+
throw new Error("Invalid allowed_mime_types JSON array");
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
var allowedMimeTypesSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), Type.Array(Type.String())]), Type.Array(Type.String({
|
|
7917
|
+
minLength: 1,
|
|
7918
|
+
maxLength: MAX_MIME_TYPE_LENGTH,
|
|
7919
|
+
pattern: MIME_TYPE_PATTERN.source
|
|
7920
|
+
}), { maxItems: MAX_MIME_TYPE_COUNT }), parseAllowedMimeTypes));
|
|
7921
|
+
var fileSizeLimitSchema = Type.Optional(Type.Integer({
|
|
7922
|
+
minimum: 1,
|
|
7923
|
+
maximum: Number.MAX_SAFE_INTEGER
|
|
7924
|
+
}));
|
|
7925
|
+
function requiredText(args, field) {
|
|
7926
|
+
const text = args[field];
|
|
7927
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
7928
|
+
throw new Error(`'${field}' required for '${String(args.action)}'`);
|
|
7929
|
+
}
|
|
7930
|
+
return text.trim();
|
|
7931
|
+
}
|
|
7932
|
+
function validBucketId(bucket) {
|
|
7933
|
+
return BUCKET_ID_PATTERN.test(bucket);
|
|
7934
|
+
}
|
|
7935
|
+
function requiredProjectRef(args) {
|
|
7936
|
+
const ref = requiredText(args, "ref");
|
|
7937
|
+
if (!PROJECT_REF_PATTERN2.test(ref))
|
|
7938
|
+
throw new Error("'ref' is invalid for Storage buckets");
|
|
7939
|
+
return ref;
|
|
7940
|
+
}
|
|
7941
|
+
function requiredBucketId(args) {
|
|
7942
|
+
const bucket = requiredText(args, "bucket");
|
|
7943
|
+
if (!validBucketId(bucket))
|
|
7944
|
+
throw new Error("'bucket' is invalid for Storage buckets");
|
|
7945
|
+
return bucket;
|
|
7946
|
+
}
|
|
7947
|
+
function assertActionArguments(action, args) {
|
|
7948
|
+
const allowedArguments = ACTION_ARGUMENTS[action];
|
|
7949
|
+
if (!allowedArguments)
|
|
7950
|
+
throw new Error(`Unsupported Storage action '${action}'`);
|
|
7951
|
+
const unsupported = Object.keys(args).filter((field) => !allowedArguments.has(field));
|
|
7952
|
+
if (unsupported.length > 0)
|
|
7953
|
+
throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
|
|
7954
|
+
}
|
|
7955
|
+
function storageBucketPath(ref, bucket) {
|
|
7956
|
+
if (!PROJECT_REF_PATTERN2.test(ref))
|
|
7957
|
+
throw new Error("'ref' is invalid for Storage buckets");
|
|
7958
|
+
if (bucket !== undefined && !validBucketId(bucket)) {
|
|
7959
|
+
throw new Error("'bucket' is invalid for Storage buckets");
|
|
7960
|
+
}
|
|
7961
|
+
const root = `/v1/projects/${encodeURIComponent(ref)}/storage/buckets`;
|
|
7962
|
+
return bucket === undefined ? root : `${root}/${encodeURIComponent(bucket)}`;
|
|
7963
|
+
}
|
|
7964
|
+
function bucketRecord(candidate) {
|
|
7965
|
+
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
7966
|
+
}
|
|
7967
|
+
function isFileSizeLimit(candidate) {
|
|
7968
|
+
return candidate === null || typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0;
|
|
7969
|
+
}
|
|
7970
|
+
function assertBucketSettings(args) {
|
|
7971
|
+
if (args.file_size_limit !== undefined && (args.file_size_limit === null || !isFileSizeLimit(args.file_size_limit))) {
|
|
7972
|
+
throw new Error("'file_size_limit' must be a positive safe integer");
|
|
7973
|
+
}
|
|
7974
|
+
if (args.allowed_mime_types !== undefined && (args.allowed_mime_types === null || !isAllowedMimeTypes(args.allowed_mime_types))) {
|
|
7975
|
+
throw new Error("'allowed_mime_types' is invalid");
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
function isAllowedMimeTypes(candidate) {
|
|
7979
|
+
return candidate === null || Array.isArray(candidate) && candidate.length <= MAX_MIME_TYPE_COUNT && candidate.every((mimeType) => typeof mimeType === "string" && mimeType.length <= MAX_MIME_TYPE_LENGTH && MIME_TYPE_PATTERN.test(mimeType));
|
|
7980
|
+
}
|
|
7981
|
+
function safeBucket(candidate, expectedBucket) {
|
|
7982
|
+
const bucket = bucketRecord(candidate);
|
|
7983
|
+
if (bucket === null || typeof bucket.id !== "string" || !validBucketId(bucket.id) || typeof bucket.name !== "string" || !validBucketId(bucket.name) || typeof bucket.public !== "boolean" || !isFileSizeLimit(bucket.file_size_limit) || !isAllowedMimeTypes(bucket.allowed_mime_types) || expectedBucket !== undefined && bucket.id !== expectedBucket && bucket.name !== expectedBucket) {
|
|
7984
|
+
return null;
|
|
7985
|
+
}
|
|
7986
|
+
return {
|
|
7987
|
+
id: bucket.id,
|
|
7988
|
+
name: bucket.name,
|
|
7989
|
+
public: bucket.public,
|
|
7990
|
+
file_size_limit: bucket.file_size_limit,
|
|
7991
|
+
allowed_mime_types: bucket.allowed_mime_types === null ? null : [...bucket.allowed_mime_types]
|
|
7992
|
+
};
|
|
7993
|
+
}
|
|
7994
|
+
function safeExactBucket(candidate, expectedBucket) {
|
|
7995
|
+
const bucket = safeBucket(candidate);
|
|
7996
|
+
return bucket?.id === expectedBucket ? bucket : null;
|
|
7997
|
+
}
|
|
7998
|
+
function safeBucketList(candidate) {
|
|
7999
|
+
if (!Array.isArray(candidate))
|
|
8000
|
+
return null;
|
|
8001
|
+
const buckets = candidate.map((bucket) => safeBucket(bucket));
|
|
8002
|
+
if (buckets.some((bucket) => bucket === null))
|
|
8003
|
+
return null;
|
|
8004
|
+
const safeBuckets = buckets;
|
|
8005
|
+
const ids = safeBuckets.map((bucket) => bucket.id);
|
|
8006
|
+
const names = safeBuckets.map((bucket) => bucket.name);
|
|
8007
|
+
return new Set(ids).size === ids.length && new Set(names).size === names.length ? safeBuckets : null;
|
|
8008
|
+
}
|
|
8009
|
+
function safeCreatedBucketReceipt(candidate, expectedBucket, request) {
|
|
8010
|
+
const bucket = bucketRecord(candidate);
|
|
8011
|
+
if (bucket?.id !== expectedBucket || bucket.name !== expectedBucket || bucket.public !== (request.public === true))
|
|
8012
|
+
return null;
|
|
8013
|
+
return { bucket: { id: expectedBucket, name: expectedBucket, public: request.public === true } };
|
|
8014
|
+
}
|
|
8015
|
+
function safeDeletedBucket(candidate, expectedBucket) {
|
|
8016
|
+
const receipt = bucketRecord(candidate);
|
|
8017
|
+
return receipt?.id === expectedBucket && receipt.deleted === true ? { bucket_id: expectedBucket, deleted: true } : null;
|
|
8018
|
+
}
|
|
8019
|
+
function mutationReadbackResponse(expectation) {
|
|
8020
|
+
const { operation, ref, response, expectedBucket, request } = expectation;
|
|
8021
|
+
const readback = safeExactBucket(response.data, expectedBucket);
|
|
8022
|
+
const validReadback = readback?.name === expectedBucket && bucketMatchesRequest(readback, request);
|
|
8023
|
+
if (!response.ok || !validReadback || !readback) {
|
|
8024
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status);
|
|
8025
|
+
}
|
|
8026
|
+
return releaseControlSuccess(operation, { project_ref: ref, bucket: readback });
|
|
8027
|
+
}
|
|
8028
|
+
function bucketResponse(expectation) {
|
|
8029
|
+
const { operation, ref, response, operationKind, safePayload } = expectation;
|
|
8030
|
+
if (!response.ok) {
|
|
8031
|
+
return operationKind === "mutation" ? releaseControlMutationFailure(operation, response) : releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
|
|
8032
|
+
}
|
|
8033
|
+
const payload = safePayload(response.data);
|
|
8034
|
+
if (!payload) {
|
|
8035
|
+
return operationKind === "mutation" ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status) : releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
8036
|
+
}
|
|
8037
|
+
return releaseControlSuccess(operation, { project_ref: ref, ...payload });
|
|
8038
|
+
}
|
|
8039
|
+
function createBucketRequest(args) {
|
|
8040
|
+
assertBucketSettings(args);
|
|
8041
|
+
const request = { name: requiredText(args, "bucket") };
|
|
8042
|
+
if (args.public !== undefined)
|
|
8043
|
+
request.public = args.public;
|
|
8044
|
+
if (args.file_size_limit !== undefined)
|
|
8045
|
+
request.file_size_limit = args.file_size_limit;
|
|
8046
|
+
if (args.allowed_mime_types !== undefined)
|
|
8047
|
+
request.allowed_mime_types = args.allowed_mime_types;
|
|
8048
|
+
return request;
|
|
8049
|
+
}
|
|
8050
|
+
function updateBucketRequest(args) {
|
|
8051
|
+
assertBucketSettings(args);
|
|
8052
|
+
const request = Object.fromEntries(["public", "file_size_limit", "allowed_mime_types"].filter((field) => args[field] !== undefined).map((field) => [field, args[field]]));
|
|
8053
|
+
if (Object.keys(request).length === 0)
|
|
8054
|
+
throw new Error("Bucket update requires at least one field");
|
|
8055
|
+
return request;
|
|
8056
|
+
}
|
|
8057
|
+
function equalAllowedMimeTypes(candidate, expected) {
|
|
8058
|
+
if (!Array.isArray(expected))
|
|
8059
|
+
return false;
|
|
8060
|
+
if (expected.length === 0 && candidate === null)
|
|
8061
|
+
return true;
|
|
8062
|
+
return Array.isArray(candidate) && candidate.length === expected.length && candidate.every((mimeType, index) => mimeType === expected[index]);
|
|
8063
|
+
}
|
|
8064
|
+
function bucketMatchesRequest(candidate, request) {
|
|
8065
|
+
const bucket = bucketRecord(candidate);
|
|
8066
|
+
if (!bucket)
|
|
8067
|
+
return false;
|
|
8068
|
+
if (request.public !== undefined && bucket.public !== request.public)
|
|
8069
|
+
return false;
|
|
8070
|
+
if (request.file_size_limit !== undefined && bucket.file_size_limit !== request.file_size_limit)
|
|
8071
|
+
return false;
|
|
8072
|
+
return request.allowed_mime_types === undefined || equalAllowedMimeTypes(bucket.allowed_mime_types, request.allowed_mime_types);
|
|
8073
|
+
}
|
|
8074
|
+
async function createBucketMutationReceipt(http, ref, bucket, request) {
|
|
8075
|
+
return bucketResponse({
|
|
8076
|
+
operation: "storage.create_bucket",
|
|
8077
|
+
ref,
|
|
8078
|
+
response: await http.post(storageBucketPath(ref), request),
|
|
8079
|
+
operationKind: "mutation",
|
|
8080
|
+
safePayload: (candidate) => safeCreatedBucketReceipt(candidate, bucket, request)
|
|
8081
|
+
});
|
|
8082
|
+
}
|
|
8083
|
+
async function updateBucketMutationReceipt(expectation) {
|
|
8084
|
+
const { http, ref, bucket, bucketPath, request } = expectation;
|
|
8085
|
+
return bucketResponse({
|
|
8086
|
+
operation: "storage.update_bucket",
|
|
8087
|
+
ref,
|
|
8088
|
+
response: await http.put(bucketPath, request),
|
|
8089
|
+
operationKind: "mutation",
|
|
8090
|
+
safePayload: (apiPayload) => {
|
|
8091
|
+
const updated = safeExactBucket(apiPayload, bucket);
|
|
8092
|
+
return updated && bucketMatchesRequest(updated, request) ? { bucket: updated } : null;
|
|
8093
|
+
}
|
|
8094
|
+
});
|
|
8095
|
+
}
|
|
8096
|
+
async function listBuckets(http, args) {
|
|
8097
|
+
const ref = requiredProjectRef(args);
|
|
8098
|
+
return bucketResponse({
|
|
8099
|
+
operation: "storage.list_buckets",
|
|
8100
|
+
ref,
|
|
8101
|
+
response: await http.get(storageBucketPath(ref)),
|
|
8102
|
+
operationKind: "read",
|
|
8103
|
+
safePayload: (candidate) => {
|
|
8104
|
+
const buckets = safeBucketList(candidate);
|
|
8105
|
+
return buckets ? { buckets } : null;
|
|
8106
|
+
}
|
|
8107
|
+
});
|
|
8108
|
+
}
|
|
8109
|
+
async function getBucket(http, args) {
|
|
8110
|
+
const ref = requiredProjectRef(args);
|
|
8111
|
+
const bucket = requiredBucketId(args);
|
|
8112
|
+
return bucketResponse({
|
|
8113
|
+
operation: "storage.get_bucket",
|
|
8114
|
+
ref,
|
|
8115
|
+
response: await http.get(storageBucketPath(ref, bucket)),
|
|
8116
|
+
operationKind: "read",
|
|
8117
|
+
safePayload: (candidate) => {
|
|
8118
|
+
const safeReadback = safeBucket(candidate, bucket);
|
|
8119
|
+
return safeReadback ? { bucket: safeReadback } : null;
|
|
8120
|
+
}
|
|
8121
|
+
});
|
|
8122
|
+
}
|
|
8123
|
+
async function createBucket(http, args) {
|
|
8124
|
+
const ref = requiredProjectRef(args);
|
|
8125
|
+
const request = createBucketRequest(args);
|
|
8126
|
+
const bucket = request.name;
|
|
8127
|
+
const bucketPath = storageBucketPath(ref, bucket);
|
|
8128
|
+
const receipt = await createBucketMutationReceipt(http, ref, bucket, request);
|
|
8129
|
+
if (receipt.isError)
|
|
8130
|
+
return receipt;
|
|
8131
|
+
const expectedReadback = { ...request, public: request.public === true };
|
|
8132
|
+
return mutationReadbackResponse({
|
|
8133
|
+
operation: "storage.create_bucket",
|
|
8134
|
+
ref,
|
|
8135
|
+
response: await http.get(bucketPath),
|
|
8136
|
+
expectedBucket: bucket,
|
|
8137
|
+
request: expectedReadback
|
|
8138
|
+
});
|
|
8139
|
+
}
|
|
8140
|
+
async function updateBucket(http, args) {
|
|
8141
|
+
const ref = requiredProjectRef(args);
|
|
8142
|
+
const bucket = requiredBucketId(args);
|
|
8143
|
+
const request = updateBucketRequest(args);
|
|
8144
|
+
const bucketPath = storageBucketPath(ref, bucket);
|
|
8145
|
+
const receipt = await updateBucketMutationReceipt({ http, ref, bucket, bucketPath, request });
|
|
8146
|
+
if (receipt.isError)
|
|
8147
|
+
return receipt;
|
|
8148
|
+
return mutationReadbackResponse({
|
|
8149
|
+
operation: "storage.update_bucket",
|
|
8150
|
+
ref,
|
|
8151
|
+
response: await http.get(bucketPath),
|
|
8152
|
+
expectedBucket: bucket,
|
|
8153
|
+
request
|
|
8154
|
+
});
|
|
8155
|
+
}
|
|
8156
|
+
async function deleteBucket(http, args) {
|
|
8157
|
+
const ref = requiredProjectRef(args);
|
|
8158
|
+
const bucket = requiredBucketId(args);
|
|
8159
|
+
const bucketPath = storageBucketPath(ref, bucket);
|
|
8160
|
+
const receipt = bucketResponse({
|
|
8161
|
+
operation: "storage.delete_bucket",
|
|
8162
|
+
ref,
|
|
8163
|
+
response: await http.delete(bucketPath),
|
|
8164
|
+
operationKind: "mutation",
|
|
8165
|
+
safePayload: (candidate) => safeDeletedBucket(candidate, bucket)
|
|
8166
|
+
});
|
|
8167
|
+
if (receipt.isError)
|
|
8168
|
+
return receipt;
|
|
8169
|
+
const readback = await http.get(bucketPath);
|
|
8170
|
+
if (readback.ok || readback.transportError || readback.status !== 404) {
|
|
8171
|
+
return releaseControlFailure("storage.delete_bucket", "OUTCOME_UNKNOWN", readback.transportError ? null : readback.status);
|
|
8172
|
+
}
|
|
8173
|
+
return receipt;
|
|
8174
|
+
}
|
|
8175
|
+
var BUCKET_ACTION_HANDLERS = {
|
|
8176
|
+
list_buckets: listBuckets,
|
|
8177
|
+
get_bucket: getBucket,
|
|
8178
|
+
create_bucket: createBucket,
|
|
8179
|
+
update_bucket: updateBucket,
|
|
8180
|
+
delete_bucket: deleteBucket
|
|
8181
|
+
};
|
|
8182
|
+
function executeBucketAction(action, http, args) {
|
|
8183
|
+
if (!Object.hasOwn(BUCKET_ACTION_HANDLERS, action))
|
|
8184
|
+
return null;
|
|
8185
|
+
return BUCKET_ACTION_HANDLERS[action](http, args);
|
|
8186
|
+
}
|
|
7378
8187
|
function registerStorageTools(server, http) {
|
|
7379
8188
|
server.tool("storage", `S3/MinIO storage management.
|
|
7380
|
-
Actions: status, list_buckets, list_files, upload_base64, delete_file`, {
|
|
7381
|
-
action: withDescription(stringEnum([
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
8189
|
+
Actions: status, list_buckets, get_bucket, create_bucket, update_bucket, delete_bucket, list_files, upload_base64, delete_file`, {
|
|
8190
|
+
action: withDescription(stringEnum([
|
|
8191
|
+
"status",
|
|
8192
|
+
"list_buckets",
|
|
8193
|
+
"get_bucket",
|
|
8194
|
+
"create_bucket",
|
|
8195
|
+
"update_bucket",
|
|
8196
|
+
"delete_bucket",
|
|
8197
|
+
"list_files",
|
|
8198
|
+
"upload_base64",
|
|
8199
|
+
"delete_file"
|
|
8200
|
+
]), "Action"),
|
|
8201
|
+
ref: optional(Type.String({ pattern: PROJECT_REF_PATTERN2.source }), "[list_buckets/get_bucket/create_bucket/update_bucket/delete_bucket/list_files/upload_base64/delete_file] Project ref"),
|
|
8202
|
+
bucket: optional(Type.String({ pattern: BUCKET_ID_PATTERN.source }), "[get_bucket/create_bucket/update_bucket/delete_bucket/list_files/upload_base64/delete_file] Bucket name or ID"),
|
|
8203
|
+
public: optional(Type.Boolean(), "[create_bucket/update_bucket] Public bucket access"),
|
|
8204
|
+
file_size_limit: withDescription(fileSizeLimitSchema, "[create_bucket/update_bucket] Positive safe-integer per-file size limit in bytes"),
|
|
8205
|
+
allowed_mime_types: withDescription(allowedMimeTypesSchema, "[create_bucket/update_bucket] MIME types as a comma-separated or JSON array"),
|
|
8206
|
+
filename: optional(Type.String(), "[upload_base64/delete_file] File name/path"),
|
|
7385
8207
|
base64_content: optional(Type.String(), "[upload_base64] Base64 encoded content"),
|
|
7386
8208
|
mime_type: optional(Type.String(), "[upload_base64] MIME type (default: application/octet-stream)")
|
|
7387
8209
|
}, async (args) => {
|
|
7388
|
-
const
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
return Array.isArray(data) ? fmtFn(data) : JSON.stringify(data, null, 2);
|
|
7395
|
-
};
|
|
8210
|
+
const action = String(args.action);
|
|
8211
|
+
assertActionArguments(action, args);
|
|
8212
|
+
const bucketAction = executeBucketAction(action, http, args);
|
|
8213
|
+
const bucketActionResponse = bucketAction ? await bucketAction : null;
|
|
8214
|
+
if (bucketActionResponse)
|
|
8215
|
+
return bucketActionResponse;
|
|
7396
8216
|
let text;
|
|
7397
8217
|
switch (action) {
|
|
7398
8218
|
case "status":
|
|
7399
8219
|
text = JSON.stringify((await http.get("/v1/storage/status")).data, null, 2);
|
|
7400
8220
|
break;
|
|
7401
|
-
case "list_buckets": {
|
|
7402
|
-
need("ref", ref);
|
|
7403
|
-
const res = await http.get(`/v1/storage/${ref}/buckets`);
|
|
7404
|
-
if (!res.ok) {
|
|
7405
|
-
text = `❌ Failed (${res.status})`;
|
|
7406
|
-
break;
|
|
7407
|
-
}
|
|
7408
|
-
const buckets = res.data;
|
|
7409
|
-
if (!Array.isArray(buckets) || !buckets.length) {
|
|
7410
|
-
text = "No buckets found.";
|
|
7411
|
-
break;
|
|
7412
|
-
}
|
|
7413
|
-
text = `\uD83D\uDCE6 Buckets (${buckets.length}):
|
|
7414
|
-
` + buckets.map((b) => ` - ${b.name} (${b.public ? "\uD83D\uDD13 public" : "\uD83D\uDD12 private"})`).join(`
|
|
7415
|
-
`);
|
|
7416
|
-
break;
|
|
7417
|
-
}
|
|
7418
8221
|
case "list_files": {
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
const
|
|
7422
|
-
if (!
|
|
7423
|
-
text = `❌ Failed (${
|
|
8222
|
+
const ref = requiredProjectRef(args);
|
|
8223
|
+
const bucket = requiredBucketId(args);
|
|
8224
|
+
const response = await http.get(`/v1/storage/${ref}/buckets/${bucket}/files`);
|
|
8225
|
+
if (!response.ok) {
|
|
8226
|
+
text = `❌ Failed (${response.status})`;
|
|
7424
8227
|
break;
|
|
7425
8228
|
}
|
|
7426
|
-
const files =
|
|
8229
|
+
const files = response.data;
|
|
7427
8230
|
if (!Array.isArray(files) || !files.length) {
|
|
7428
8231
|
text = "No files.";
|
|
7429
8232
|
break;
|
|
7430
8233
|
}
|
|
7431
8234
|
text = `\uD83D\uDCC1 Files (${files.length}):
|
|
7432
|
-
` + files.map((
|
|
8235
|
+
` + files.map((file) => ` - ${file.name} (${file.size ? (file.size / 1024).toFixed(1) + "KB" : "?"})`).join(`
|
|
7433
8236
|
`);
|
|
7434
8237
|
break;
|
|
7435
8238
|
}
|
|
7436
8239
|
case "upload_base64": {
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
8240
|
+
const ref = requiredProjectRef(args);
|
|
8241
|
+
const bucket = requiredBucketId(args);
|
|
8242
|
+
const filename = requiredText(args, "filename");
|
|
8243
|
+
const base64Content = requiredText(args, "base64_content");
|
|
7441
8244
|
try {
|
|
7442
|
-
const buffer = Buffer.from(
|
|
7443
|
-
const blob = new Blob([buffer], { type: mime_type
|
|
8245
|
+
const buffer = Buffer.from(base64Content, "base64");
|
|
8246
|
+
const blob = new Blob([buffer], { type: typeof args.mime_type === "string" ? args.mime_type : "application/octet-stream" });
|
|
7444
8247
|
const formData = new FormData;
|
|
7445
8248
|
formData.append("file", blob, filename);
|
|
7446
|
-
const
|
|
7447
|
-
text =
|
|
7448
|
-
} catch (
|
|
7449
|
-
text = `❌ Error: ${
|
|
8249
|
+
const response = await http.postMultipart(`/v1/storage/${ref}/buckets/${bucket}/upload`, formData);
|
|
8250
|
+
text = response.ok ? `✅ File ${filename} uploaded to ${bucket}` : `❌ Upload failed (${response.status})`;
|
|
8251
|
+
} catch (error) {
|
|
8252
|
+
text = `❌ Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
7450
8253
|
}
|
|
7451
8254
|
break;
|
|
7452
8255
|
}
|
|
7453
|
-
case "delete_file":
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` :
|
|
8256
|
+
case "delete_file": {
|
|
8257
|
+
const ref = requiredProjectRef(args);
|
|
8258
|
+
const bucket = requiredBucketId(args);
|
|
8259
|
+
const filename = requiredText(args, "filename");
|
|
8260
|
+
text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` : "❌ Failed";
|
|
7458
8261
|
break;
|
|
8262
|
+
}
|
|
7459
8263
|
default:
|
|
7460
|
-
|
|
8264
|
+
return releaseControlFailure(`storage.${action}`, "INVALID_RESPONSE", null);
|
|
7461
8265
|
}
|
|
7462
8266
|
return { content: [{ type: "text", text }] };
|
|
7463
8267
|
});
|
|
@@ -7540,7 +8344,29 @@ function parseFunctionFiles(input) {
|
|
|
7540
8344
|
}
|
|
7541
8345
|
}
|
|
7542
8346
|
var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
|
|
8347
|
+
function parseFunctionVersion(input) {
|
|
8348
|
+
const version = String(input);
|
|
8349
|
+
if (!CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
|
|
8350
|
+
throw new Error("Function version must be a canonical safe integer");
|
|
8351
|
+
}
|
|
8352
|
+
return version;
|
|
8353
|
+
}
|
|
8354
|
+
var CANONICAL_FUNCTION_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)$/;
|
|
8355
|
+
var SAFE_FUNCTION_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
8356
|
+
var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
8357
|
+
var FUNCTION_ACTIVATION_ARGUMENTS = new Set(["action", "ref", "slug", "version"]);
|
|
8358
|
+
var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
|
|
8359
|
+
Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
|
8360
|
+
Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
8361
|
+
]), Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), parseFunctionVersion));
|
|
7543
8362
|
var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
|
|
8363
|
+
var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
8364
|
+
var MAX_SECRET_COUNT = 1024;
|
|
8365
|
+
var MAX_ENVIRONMENT_SECRET_BYTES = 24 * 1024;
|
|
8366
|
+
var MAX_SECRET_JSON_BYTES = 1024 * 1024;
|
|
8367
|
+
var INVALID_ENVIRONMENT_SECRET_NAMES_MESSAGE = "Environment secret names are invalid";
|
|
8368
|
+
var INVALID_ENVIRONMENT_SECRET_VALUES_MESSAGE = "Environment secret values are missing or exceed safe limits";
|
|
8369
|
+
var INVALID_SECRET_LIST_RESPONSE = "❌ Project secret list response is invalid";
|
|
7544
8370
|
function parseSecrets(value) {
|
|
7545
8371
|
if (Array.isArray(value))
|
|
7546
8372
|
return value;
|
|
@@ -7567,6 +8393,89 @@ function parseSecrets(value) {
|
|
|
7567
8393
|
}).filter((entry) => entry.name);
|
|
7568
8394
|
}
|
|
7569
8395
|
var secretsSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), secretListSchema]), secretListSchema, parseSecrets));
|
|
8396
|
+
function validEnvironmentSecretNames(names) {
|
|
8397
|
+
if (names.length === 0 || names.length > MAX_SECRET_COUNT)
|
|
8398
|
+
return false;
|
|
8399
|
+
const uniqueNames = new Set;
|
|
8400
|
+
for (const name of names) {
|
|
8401
|
+
if (!ENVIRONMENT_SECRET_NAME_PATTERN.test(name) || uniqueNames.has(name))
|
|
8402
|
+
return false;
|
|
8403
|
+
uniqueNames.add(name);
|
|
8404
|
+
}
|
|
8405
|
+
return true;
|
|
8406
|
+
}
|
|
8407
|
+
function parseEnvironmentSecretNames(input) {
|
|
8408
|
+
const names = input.split(",", MAX_SECRET_COUNT + 1).map((name) => name.trim());
|
|
8409
|
+
if (!validEnvironmentSecretNames(names)) {
|
|
8410
|
+
throw new Error(INVALID_ENVIRONMENT_SECRET_NAMES_MESSAGE);
|
|
8411
|
+
}
|
|
8412
|
+
return names;
|
|
8413
|
+
}
|
|
8414
|
+
var environmentSecretNamesSchema = Type.Optional(decodedSchema(Type.String(), Type.Array(Type.String({ pattern: ENVIRONMENT_SECRET_NAME_PATTERN.source }), {
|
|
8415
|
+
minItems: 1,
|
|
8416
|
+
maxItems: MAX_SECRET_COUNT,
|
|
8417
|
+
uniqueItems: true
|
|
8418
|
+
}), parseEnvironmentSecretNames));
|
|
8419
|
+
function jsonWithinSecretLimit(payload) {
|
|
8420
|
+
try {
|
|
8421
|
+
const serializedPayload = JSON.stringify(payload);
|
|
8422
|
+
return serializedPayload !== undefined && Buffer.byteLength(serializedPayload) <= MAX_SECRET_JSON_BYTES;
|
|
8423
|
+
} catch (error) {
|
|
8424
|
+
if (error instanceof TypeError)
|
|
8425
|
+
return false;
|
|
8426
|
+
throw error;
|
|
8427
|
+
}
|
|
8428
|
+
}
|
|
8429
|
+
function maskedSecretEntry(candidate) {
|
|
8430
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
8431
|
+
return null;
|
|
8432
|
+
const { name, value } = candidate;
|
|
8433
|
+
if (typeof name !== "string" || !ENVIRONMENT_SECRET_NAME_PATTERN.test(name))
|
|
8434
|
+
return null;
|
|
8435
|
+
if (value !== "********")
|
|
8436
|
+
return null;
|
|
8437
|
+
return { name, value: "********" };
|
|
8438
|
+
}
|
|
8439
|
+
function projectedMaskedSecrets(payload) {
|
|
8440
|
+
if (!Array.isArray(payload) || payload.length > MAX_SECRET_COUNT)
|
|
8441
|
+
return null;
|
|
8442
|
+
if (!jsonWithinSecretLimit(payload))
|
|
8443
|
+
return null;
|
|
8444
|
+
const secretNames = new Set;
|
|
8445
|
+
const projectedSecrets = [];
|
|
8446
|
+
for (const candidate of payload) {
|
|
8447
|
+
const secret = maskedSecretEntry(candidate);
|
|
8448
|
+
if (!secret || secretNames.has(secret.name))
|
|
8449
|
+
return null;
|
|
8450
|
+
secretNames.add(secret.name);
|
|
8451
|
+
projectedSecrets.push(secret);
|
|
8452
|
+
}
|
|
8453
|
+
return projectedSecrets;
|
|
8454
|
+
}
|
|
8455
|
+
function secretsFromEnvironment(names, environment) {
|
|
8456
|
+
const secrets = names.map((name) => {
|
|
8457
|
+
const secretValue = environment[name];
|
|
8458
|
+
if (typeof secretValue !== "string" || secretValue.length === 0 || Buffer.byteLength(secretValue) > MAX_ENVIRONMENT_SECRET_BYTES) {
|
|
8459
|
+
throw new Error(INVALID_ENVIRONMENT_SECRET_VALUES_MESSAGE);
|
|
8460
|
+
}
|
|
8461
|
+
return { name, value: secretValue };
|
|
8462
|
+
});
|
|
8463
|
+
if (!jsonWithinSecretLimit(secrets)) {
|
|
8464
|
+
throw new Error(INVALID_ENVIRONMENT_SECRET_VALUES_MESSAGE);
|
|
8465
|
+
}
|
|
8466
|
+
return secrets;
|
|
8467
|
+
}
|
|
8468
|
+
function secretsForUpsert(inlineSecrets, environmentNames, environment) {
|
|
8469
|
+
if (inlineSecrets !== undefined && environmentNames !== undefined) {
|
|
8470
|
+
throw new Error("'--from-env' cannot be combined with '--secrets'");
|
|
8471
|
+
}
|
|
8472
|
+
if (environmentNames !== undefined) {
|
|
8473
|
+
return secretsFromEnvironment(environmentNames, environment);
|
|
8474
|
+
}
|
|
8475
|
+
if (!inlineSecrets?.length)
|
|
8476
|
+
throw new Error("'secrets' array required");
|
|
8477
|
+
return inlineSecrets;
|
|
8478
|
+
}
|
|
7570
8479
|
function confirmedFunctionConfig(payload, expected) {
|
|
7571
8480
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
7572
8481
|
return false;
|
|
@@ -7587,12 +8496,60 @@ function functionSourceCode(payload) {
|
|
|
7587
8496
|
const code = payload.code;
|
|
7588
8497
|
return typeof code === "string" ? code : null;
|
|
7589
8498
|
}
|
|
7590
|
-
function
|
|
8499
|
+
function objectRecord(candidate) {
|
|
8500
|
+
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
8501
|
+
}
|
|
8502
|
+
function activationResponse(slug, version, response) {
|
|
8503
|
+
const operation = "edge_functions.activate";
|
|
8504
|
+
if (!response.ok)
|
|
8505
|
+
return releaseControlMutationFailure(operation, response);
|
|
8506
|
+
const receipt = objectRecord(response.data);
|
|
8507
|
+
const config = objectRecord(receipt?.config);
|
|
8508
|
+
if (receipt?.success !== true || receipt.version !== version || config?.version !== version || typeof config.verify_jwt !== "boolean") {
|
|
8509
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
8510
|
+
}
|
|
8511
|
+
return releaseControlSuccess(operation, {
|
|
8512
|
+
slug,
|
|
8513
|
+
version,
|
|
8514
|
+
verify_jwt: config.verify_jwt
|
|
8515
|
+
});
|
|
8516
|
+
}
|
|
8517
|
+
function readOnlyActivationResult() {
|
|
8518
|
+
return {
|
|
8519
|
+
isError: true,
|
|
8520
|
+
content: [{ type: "text", text: "⚠️ Edge Function activation blocked in read-only mode." }]
|
|
8521
|
+
};
|
|
8522
|
+
}
|
|
8523
|
+
function functionActivationTarget(args) {
|
|
8524
|
+
const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
|
|
8525
|
+
const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
8526
|
+
const version = args.version;
|
|
8527
|
+
if (!SAFE_FUNCTION_REF_PATTERN.test(projectRef))
|
|
8528
|
+
throw new Error("'ref' is invalid for 'activate'");
|
|
8529
|
+
if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
|
|
8530
|
+
throw new Error("'slug' is invalid for 'activate'");
|
|
8531
|
+
if (typeof version !== "string" || !CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
|
|
8532
|
+
throw new Error("'version' is invalid for 'activate'");
|
|
8533
|
+
}
|
|
8534
|
+
return { projectRef, functionSlug, version };
|
|
8535
|
+
}
|
|
8536
|
+
async function activateFunctionVersion(http, args, readOnly = false) {
|
|
8537
|
+
if (readOnly)
|
|
8538
|
+
return readOnlyActivationResult();
|
|
8539
|
+
const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
|
|
8540
|
+
if (unsupported.length > 0)
|
|
8541
|
+
throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
|
|
8542
|
+
const { projectRef, functionSlug, version } = functionActivationTarget(args);
|
|
8543
|
+
const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/functions/${encodeURIComponent(functionSlug)}` + `/versions/${encodeURIComponent(version)}/activate`;
|
|
8544
|
+
return activationResponse(functionSlug, version, await http.post(endpoint));
|
|
8545
|
+
}
|
|
8546
|
+
function registerAdvancedTools(server, http, environment = process.env, options = {}) {
|
|
7591
8547
|
server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Server auto-bundles dependencies.
|
|
7592
|
-
Actions: list, deploy, deploy_bundle, config, source, delete, check`, {
|
|
7593
|
-
action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "delete", "check"]), "Action"),
|
|
8548
|
+
Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
|
|
8549
|
+
action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
|
|
7594
8550
|
ref: withDescription(Type.String(), "Project ref"),
|
|
7595
|
-
slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/delete/check] Function name"),
|
|
8551
|
+
slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
|
|
8552
|
+
version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
|
|
7596
8553
|
code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
|
|
7597
8554
|
path: optional(Type.String(), "[deploy/check] Local file path to read code from (alternative to code)"),
|
|
7598
8555
|
output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
|
|
@@ -7602,6 +8559,8 @@ Actions: list, deploy, deploy_bundle, config, source, delete, check`, {
|
|
|
7602
8559
|
verify_jwt: optional(Type.Boolean(), "[deploy/deploy_bundle/config] Set JWT verification for this function"),
|
|
7603
8560
|
background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI")
|
|
7604
8561
|
}, async (args) => {
|
|
8562
|
+
if (args.action === "activate")
|
|
8563
|
+
return activateFunctionVersion(http, args, options.readOnly);
|
|
7605
8564
|
const { action, ref, slug, path: pathArg, output, files, entrypoint, minify, verify_jwt, background_routes } = args;
|
|
7606
8565
|
let code = args.code;
|
|
7607
8566
|
const need = (f, v) => {
|
|
@@ -7737,18 +8696,28 @@ Actions: list, upsert, delete`, {
|
|
|
7737
8696
|
action: withDescription(stringEnum(["list", "upsert", "delete"]), "Action"),
|
|
7738
8697
|
ref: withDescription(Type.String(), "Project ref"),
|
|
7739
8698
|
secrets: withDescription(secretsSchema, "[upsert] Secret list as JSON array or KEY=VALUE,KEY2=VALUE2"),
|
|
8699
|
+
"from-env": withDescription(environmentSecretNamesSchema, "[upsert] Comma-separated environment variable names; values are read from this CLI process"),
|
|
7740
8700
|
name: optional(Type.String(), "[delete] Secret name to delete")
|
|
7741
8701
|
}, async (args) => {
|
|
7742
8702
|
const { action, ref, secrets, name } = args;
|
|
8703
|
+
const environmentNames = args["from-env"];
|
|
7743
8704
|
let text;
|
|
7744
8705
|
switch (action) {
|
|
7745
|
-
case "list":
|
|
7746
|
-
|
|
8706
|
+
case "list": {
|
|
8707
|
+
const response = await http.get(`/v1/projects/${ref}/secrets`, {
|
|
8708
|
+
maxResponseBytes: MAX_SECRET_JSON_BYTES
|
|
8709
|
+
});
|
|
8710
|
+
if (!response.ok) {
|
|
8711
|
+
text = `❌ Failed (${response.status})`;
|
|
8712
|
+
break;
|
|
8713
|
+
}
|
|
8714
|
+
const maskedSecrets = projectedMaskedSecrets(response.data);
|
|
8715
|
+
text = maskedSecrets === null ? INVALID_SECRET_LIST_RESPONSE : JSON.stringify(maskedSecrets, null, 2);
|
|
7747
8716
|
break;
|
|
8717
|
+
}
|
|
7748
8718
|
case "upsert":
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
text = (await http.post(`/v1/projects/${ref}/secrets`, secrets)).ok ? `✅ Updated ${secrets.length} secrets` : `❌ Failed`;
|
|
8719
|
+
const secretsToUpsert = secretsForUpsert(secrets, environmentNames, environment);
|
|
8720
|
+
text = (await http.post(`/v1/projects/${ref}/secrets`, secretsToUpsert)).ok ? `✅ Updated ${secretsToUpsert.length} secrets` : `❌ Failed`;
|
|
7752
8721
|
break;
|
|
7753
8722
|
case "delete":
|
|
7754
8723
|
if (!name)
|
|
@@ -8171,7 +9140,7 @@ function buildProjectLogsPath(ref, logType) {
|
|
|
8171
9140
|
return `/v1/projects/${ref}/logs?${params.toString()}`;
|
|
8172
9141
|
}
|
|
8173
9142
|
function resolveRef(refFromArgs, defaultRef) {
|
|
8174
|
-
const ref =
|
|
9143
|
+
const ref = refFromArgs || defaultRef;
|
|
8175
9144
|
if (!ref)
|
|
8176
9145
|
throw new Error("'ref' is required for this action");
|
|
8177
9146
|
return ref;
|
|
@@ -8360,7 +9329,7 @@ function formatMessages(data, label = "Messages") {
|
|
|
8360
9329
|
return out;
|
|
8361
9330
|
}
|
|
8362
9331
|
function resolveRef2(refFromArgs, defaultRef) {
|
|
8363
|
-
const ref =
|
|
9332
|
+
const ref = refFromArgs || defaultRef;
|
|
8364
9333
|
if (!ref)
|
|
8365
9334
|
throw new Error("'ref' is required for this action");
|
|
8366
9335
|
return ref;
|
|
@@ -8660,7 +9629,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
8660
9629
|
custom_hostname: optional(Type.String(), "[set_custom_hostname] 自定义域名")
|
|
8661
9630
|
}, async (args) => {
|
|
8662
9631
|
const resolveRef3 = (override) => {
|
|
8663
|
-
const ref2 =
|
|
9632
|
+
const ref2 = override || projectRef;
|
|
8664
9633
|
if (!ref2)
|
|
8665
9634
|
throw new Error("'ref' is required for this action");
|
|
8666
9635
|
return ref2;
|
|
@@ -9602,6 +10571,414 @@ function registerAiTools(server) {
|
|
|
9602
10571
|
});
|
|
9603
10572
|
}
|
|
9604
10573
|
|
|
10574
|
+
// src/shared/tools/scheduled-function-tools.ts
|
|
10575
|
+
import { randomUUID } from "node:crypto";
|
|
10576
|
+
import { readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
|
|
10577
|
+
import { resolve as resolve5 } from "node:path";
|
|
10578
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10579
|
+
var HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/;
|
|
10580
|
+
var ENVIRONMENT_NAME_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
10581
|
+
var FORBIDDEN_HEADER_NAMES = new Set([
|
|
10582
|
+
"apikey",
|
|
10583
|
+
"authorization",
|
|
10584
|
+
"connection",
|
|
10585
|
+
"content-length",
|
|
10586
|
+
"forwarded",
|
|
10587
|
+
"host",
|
|
10588
|
+
"keep-alive",
|
|
10589
|
+
"proxy-authenticate",
|
|
10590
|
+
"proxy-authorization",
|
|
10591
|
+
"proxy-connection",
|
|
10592
|
+
"te",
|
|
10593
|
+
"trailer",
|
|
10594
|
+
"transfer-encoding",
|
|
10595
|
+
"upgrade",
|
|
10596
|
+
"via",
|
|
10597
|
+
"x-project-ref"
|
|
10598
|
+
]);
|
|
10599
|
+
var SCHEDULE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
10600
|
+
var SAFE_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
10601
|
+
var CRON_PART_PATTERN = /^(\*|([0-9]+)(?:-([0-9]+))?)(?:\/([0-9]+))?$/;
|
|
10602
|
+
var CRON_FIELD_BOUNDS = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]];
|
|
10603
|
+
var MAX_CRON_EXPRESSION_LENGTH = 256;
|
|
10604
|
+
var MAX_BODY_FILE_BYTES = 1048576;
|
|
10605
|
+
var MAX_HEADER_COUNT = 64;
|
|
10606
|
+
var MAX_HEADER_VALUE_LENGTH = 8192;
|
|
10607
|
+
var MAX_SCHEDULE_NAME_LENGTH = 120;
|
|
10608
|
+
var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
|
|
10609
|
+
var ACTION_ARGUMENTS2 = {
|
|
10610
|
+
list: new Set(["action", "ref"]),
|
|
10611
|
+
create: new Set(["action", "ref", "name", "slug", "cron", "method", "body_file", "header_env"]),
|
|
10612
|
+
update: new Set(["action", "ref", "schedule_id", "name", "cron", "method", "enabled", "body_file", "header_env"]),
|
|
10613
|
+
delete: new Set(["action", "ref", "schedule_id"])
|
|
10614
|
+
};
|
|
10615
|
+
function parseHeaderEnvironment(input) {
|
|
10616
|
+
if (typeof input !== "string")
|
|
10617
|
+
return input;
|
|
10618
|
+
try {
|
|
10619
|
+
return JSON.parse(input);
|
|
10620
|
+
} catch (error) {
|
|
10621
|
+
if (!(error instanceof SyntaxError))
|
|
10622
|
+
throw error;
|
|
10623
|
+
throw new Error("Invalid header_env JSON object");
|
|
10624
|
+
}
|
|
10625
|
+
}
|
|
10626
|
+
var headerEnvironmentSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), headerEnvironmentRecord]), headerEnvironmentRecord, parseHeaderEnvironment));
|
|
10627
|
+
function objectRecord2(candidate) {
|
|
10628
|
+
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
10629
|
+
}
|
|
10630
|
+
function boundedCronInteger(input, minimum, maximum) {
|
|
10631
|
+
const parsed = Number(input);
|
|
10632
|
+
return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum;
|
|
10633
|
+
}
|
|
10634
|
+
function validCronPart(part, minimum, maximum) {
|
|
10635
|
+
const match = CRON_PART_PATTERN.exec(part);
|
|
10636
|
+
if (!match)
|
|
10637
|
+
return false;
|
|
10638
|
+
const start = match[1] === "*" ? minimum : Number(match[2]);
|
|
10639
|
+
const end = match[1] === "*" ? maximum : Number(match[3] ?? match[2]);
|
|
10640
|
+
const step = Number(match[4] ?? "1");
|
|
10641
|
+
return boundedCronInteger(String(start), minimum, maximum) && boundedCronInteger(String(end), minimum, maximum) && start <= end && boundedCronInteger(String(step), 1, maximum - minimum + 1);
|
|
10642
|
+
}
|
|
10643
|
+
function validCronField(field, minimum, maximum) {
|
|
10644
|
+
const parts = field.split(",");
|
|
10645
|
+
return parts.length <= maximum - minimum + 1 && parts.every((part) => part.length > 0 && validCronPart(part, minimum, maximum));
|
|
10646
|
+
}
|
|
10647
|
+
function validScheduledFunctionCron(expression) {
|
|
10648
|
+
if (!expression || expression.length > MAX_CRON_EXPRESSION_LENGTH)
|
|
10649
|
+
return false;
|
|
10650
|
+
const fields = expression.trim().split(/\s+/);
|
|
10651
|
+
return fields.length === CRON_FIELD_BOUNDS.length && fields.every((field, index) => {
|
|
10652
|
+
const [minimum, maximum] = CRON_FIELD_BOUNDS[index];
|
|
10653
|
+
return validCronField(field, minimum, maximum);
|
|
10654
|
+
});
|
|
10655
|
+
}
|
|
10656
|
+
function readScheduleBodyFile(bodyPathInput) {
|
|
10657
|
+
if (!bodyPathInput.trim())
|
|
10658
|
+
throw new Error("'body_file' must be a path");
|
|
10659
|
+
const bodyPath = resolve5(bodyPathInput);
|
|
10660
|
+
const bodyStat = statSync4(bodyPath);
|
|
10661
|
+
if (!bodyStat.isFile() || bodyStat.size > MAX_BODY_FILE_BYTES) {
|
|
10662
|
+
throw new Error("Scheduled Function body file must be a regular file no larger than 1 MiB");
|
|
10663
|
+
}
|
|
10664
|
+
let payload;
|
|
10665
|
+
try {
|
|
10666
|
+
payload = JSON.parse(readFileSync6(bodyPath, "utf8"));
|
|
10667
|
+
} catch (error) {
|
|
10668
|
+
if (!(error instanceof SyntaxError))
|
|
10669
|
+
throw error;
|
|
10670
|
+
throw new Error("Scheduled Function body file must contain exact JSON");
|
|
10671
|
+
}
|
|
10672
|
+
const body = objectRecord2(payload);
|
|
10673
|
+
if (!body)
|
|
10674
|
+
throw new Error("Scheduled Function body file must contain a JSON object");
|
|
10675
|
+
return body;
|
|
10676
|
+
}
|
|
10677
|
+
function scheduleBody(bodyFile) {
|
|
10678
|
+
if (bodyFile === undefined)
|
|
10679
|
+
return;
|
|
10680
|
+
if (typeof bodyFile !== "string")
|
|
10681
|
+
throw new Error("'body_file' must be a path");
|
|
10682
|
+
return readScheduleBodyFile(bodyFile);
|
|
10683
|
+
}
|
|
10684
|
+
function resolvedHeaderEntry(headerName, environmentName, environment) {
|
|
10685
|
+
const normalizedName = headerName.toLowerCase();
|
|
10686
|
+
if (!HEADER_NAME_PATTERN.test(headerName) || forbiddenHeaderName(normalizedName) || typeof environmentName !== "string" || !ENVIRONMENT_NAME_PATTERN2.test(environmentName)) {
|
|
10687
|
+
throw new Error("SCHEDULE_HEADER_MAPPING_INVALID");
|
|
10688
|
+
}
|
|
10689
|
+
const headerValue = environment[environmentName];
|
|
10690
|
+
if (!headerValue)
|
|
10691
|
+
throw new Error("SCHEDULE_HEADER_ENV_MISSING");
|
|
10692
|
+
if (!headerValueIsStable(normalizedName, headerValue))
|
|
10693
|
+
throw new Error("SCHEDULE_HEADER_INVALID");
|
|
10694
|
+
return [normalizedName, headerValue];
|
|
10695
|
+
}
|
|
10696
|
+
function forbiddenHeaderName(name) {
|
|
10697
|
+
return FORBIDDEN_HEADER_NAMES.has(name) || name.startsWith("x-forwarded-");
|
|
10698
|
+
}
|
|
10699
|
+
function headerValueIsStable(name, value) {
|
|
10700
|
+
if (!value || value.length > MAX_HEADER_VALUE_LENGTH)
|
|
10701
|
+
return false;
|
|
10702
|
+
try {
|
|
10703
|
+
const headers = new Headers;
|
|
10704
|
+
headers.set(name, value);
|
|
10705
|
+
return headers.get(name) === value;
|
|
10706
|
+
} catch {
|
|
10707
|
+
return false;
|
|
10708
|
+
}
|
|
10709
|
+
}
|
|
10710
|
+
function scheduleHeaders(mapping, environment) {
|
|
10711
|
+
if (mapping === undefined)
|
|
10712
|
+
return;
|
|
10713
|
+
const headerEnvironment = objectRecord2(mapping);
|
|
10714
|
+
if (!headerEnvironment)
|
|
10715
|
+
throw new Error("'header_env' must be a JSON object");
|
|
10716
|
+
const entries = Object.entries(headerEnvironment).map(([headerName, environmentName]) => resolvedHeaderEntry(headerName, environmentName, environment));
|
|
10717
|
+
const names = entries.map(([name]) => name);
|
|
10718
|
+
if (entries.length > MAX_HEADER_COUNT || new Set(names).size !== names.length) {
|
|
10719
|
+
throw new Error("SCHEDULE_HEADER_INVALID");
|
|
10720
|
+
}
|
|
10721
|
+
return Object.fromEntries(entries);
|
|
10722
|
+
}
|
|
10723
|
+
function validSafeSchedule(schedule) {
|
|
10724
|
+
return validScheduleIdentity(schedule) && validScheduleDefinition(schedule) && validScheduleMetadata(schedule);
|
|
10725
|
+
}
|
|
10726
|
+
function validScheduleIdentity(schedule) {
|
|
10727
|
+
return typeof schedule.id === "string" && SCHEDULE_ID_PATTERN.test(schedule.id) && typeof schedule.name === "string" && schedule.name.trim().length > 0 && schedule.name.length <= MAX_SCHEDULE_NAME_LENGTH && typeof schedule.slug === "string" && SAFE_SLUG_PATTERN.test(schedule.slug);
|
|
10728
|
+
}
|
|
10729
|
+
function validScheduleDefinition(schedule) {
|
|
10730
|
+
return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
|
|
10731
|
+
}
|
|
10732
|
+
function validScheduleMetadata(schedule) {
|
|
10733
|
+
return typeof schedule.created_at === "string" && typeof schedule.updated_at === "string";
|
|
10734
|
+
}
|
|
10735
|
+
function safeSchedulePayload(schedule) {
|
|
10736
|
+
const headerNames = safeHeaderNames(schedule.header_names);
|
|
10737
|
+
if (typeof schedule.body_empty !== "boolean" || !headerNames)
|
|
10738
|
+
return null;
|
|
10739
|
+
return { body_empty: schedule.body_empty, header_names: headerNames };
|
|
10740
|
+
}
|
|
10741
|
+
function safeHeaderNames(candidate) {
|
|
10742
|
+
if (!Array.isArray(candidate) || candidate.length > MAX_HEADER_COUNT)
|
|
10743
|
+
return null;
|
|
10744
|
+
const names = candidate.map((name) => typeof name === "string" ? name.toLowerCase() : "");
|
|
10745
|
+
const valid = candidate.every((name, index) => typeof name === "string" && HEADER_NAME_PATTERN.test(name) && !forbiddenHeaderName(names[index]));
|
|
10746
|
+
return valid && new Set(names).size === names.length ? names.sort() : null;
|
|
10747
|
+
}
|
|
10748
|
+
function safeSchedule(candidate) {
|
|
10749
|
+
const schedule = objectRecord2(candidate);
|
|
10750
|
+
if (!schedule || !validSafeSchedule(schedule))
|
|
10751
|
+
return null;
|
|
10752
|
+
const safePayload = safeSchedulePayload(schedule);
|
|
10753
|
+
if (!safePayload)
|
|
10754
|
+
return null;
|
|
10755
|
+
return {
|
|
10756
|
+
id: schedule.id,
|
|
10757
|
+
name: schedule.name,
|
|
10758
|
+
slug: schedule.slug,
|
|
10759
|
+
cron: schedule.cron,
|
|
10760
|
+
method: schedule.method,
|
|
10761
|
+
enabled: schedule.enabled,
|
|
10762
|
+
...safePayload,
|
|
10763
|
+
created_at: schedule.created_at,
|
|
10764
|
+
updated_at: schedule.updated_at
|
|
10765
|
+
};
|
|
10766
|
+
}
|
|
10767
|
+
function requiredText2(args, name, action) {
|
|
10768
|
+
const candidate = args[name];
|
|
10769
|
+
if (typeof candidate !== "string" || !candidate.trim()) {
|
|
10770
|
+
throw new Error(`'${name}' is required for '${action}'`);
|
|
10771
|
+
}
|
|
10772
|
+
return candidate.trim();
|
|
10773
|
+
}
|
|
10774
|
+
function assertActionArguments2(action, args) {
|
|
10775
|
+
const unsupported = Object.keys(args).filter((name) => !ACTION_ARGUMENTS2[action].has(name));
|
|
10776
|
+
if (unsupported.length > 0) {
|
|
10777
|
+
throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
|
|
10778
|
+
}
|
|
10779
|
+
}
|
|
10780
|
+
function schedulePath(ref, scheduleId) {
|
|
10781
|
+
const projectRefSegment = projectRefPathSegment(ref, "Scheduled Functions");
|
|
10782
|
+
if (scheduleId !== undefined && !SCHEDULE_ID_PATTERN.test(scheduleId)) {
|
|
10783
|
+
throw new Error("'schedule_id' is invalid");
|
|
10784
|
+
}
|
|
10785
|
+
const root = `/v1/projects/${projectRefSegment}/scheduled-functions`;
|
|
10786
|
+
return scheduleId ? `${root}/${encodeURIComponent(scheduleId)}` : root;
|
|
10787
|
+
}
|
|
10788
|
+
function scheduleFailure(operation, response) {
|
|
10789
|
+
return releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
|
|
10790
|
+
}
|
|
10791
|
+
function listResponse(ref, response) {
|
|
10792
|
+
const operation = "scheduled_functions.list";
|
|
10793
|
+
if (!response.ok)
|
|
10794
|
+
return scheduleFailure(operation, response);
|
|
10795
|
+
const payload = objectRecord2(response.data);
|
|
10796
|
+
const rawSchedules = payload?.schedules;
|
|
10797
|
+
const schedules = Array.isArray(rawSchedules) ? rawSchedules.map(safeSchedule) : null;
|
|
10798
|
+
if (payload?.project_ref !== ref || !schedules || schedules.some((schedule) => !schedule)) {
|
|
10799
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
10800
|
+
}
|
|
10801
|
+
const ids = schedules.map((schedule) => schedule.id);
|
|
10802
|
+
if (new Set(ids).size !== ids.length)
|
|
10803
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
10804
|
+
return releaseControlSuccess(operation, { project_ref: ref, schedules });
|
|
10805
|
+
}
|
|
10806
|
+
function mutationResponse(expectation, response) {
|
|
10807
|
+
const { action, ref, requestId, expectedFields } = expectation;
|
|
10808
|
+
const scheduleId = action === "update" ? expectation.scheduleId : undefined;
|
|
10809
|
+
const operation = `scheduled_functions.${action}`;
|
|
10810
|
+
if (!response.ok)
|
|
10811
|
+
return releaseControlMutationFailure(operation, response);
|
|
10812
|
+
const payload = objectRecord2(response.data);
|
|
10813
|
+
const schedule = safeSchedule(payload?.schedule);
|
|
10814
|
+
const confirmsRequest = schedule && Object.entries(expectedFields).every(([field, expected]) => isDeepStrictEqual(schedule[field], expected));
|
|
10815
|
+
if (payload?.project_ref !== ref || payload.request_id !== requestId || payload?.[action === "create" ? "created" : "updated"] !== true || !schedule || !confirmsRequest || scheduleId !== undefined && schedule.id !== scheduleId) {
|
|
10816
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10817
|
+
}
|
|
10818
|
+
return releaseControlSuccess(operation, { project_ref: ref, request_id: requestId, schedule });
|
|
10819
|
+
}
|
|
10820
|
+
function deleteResponse(ref, scheduleId, response) {
|
|
10821
|
+
const operation = "scheduled_functions.delete";
|
|
10822
|
+
if (!response.ok)
|
|
10823
|
+
return releaseControlMutationFailure(operation, response);
|
|
10824
|
+
const payload = objectRecord2(response.data);
|
|
10825
|
+
if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId) {
|
|
10826
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10827
|
+
}
|
|
10828
|
+
return releaseControlSuccess(operation, {
|
|
10829
|
+
project_ref: ref,
|
|
10830
|
+
schedule_id: scheduleId,
|
|
10831
|
+
deleted: true
|
|
10832
|
+
});
|
|
10833
|
+
}
|
|
10834
|
+
function readOnlyResult2() {
|
|
10835
|
+
return {
|
|
10836
|
+
isError: true,
|
|
10837
|
+
content: [{ type: "text", text: "⚠️ Scheduled Function write blocked in read-only mode." }]
|
|
10838
|
+
};
|
|
10839
|
+
}
|
|
10840
|
+
function createRequest(args, environment) {
|
|
10841
|
+
return {
|
|
10842
|
+
request_id: randomUUID(),
|
|
10843
|
+
name: requiredName(args, "create"),
|
|
10844
|
+
slug: requiredSlug(args, "create"),
|
|
10845
|
+
cron: requiredCron(args, "create"),
|
|
10846
|
+
method: requiredText2(args, "method", "create"),
|
|
10847
|
+
body: scheduleBody(args.body_file) ?? {},
|
|
10848
|
+
headers: scheduleHeaders(args.header_env, environment) ?? {}
|
|
10849
|
+
};
|
|
10850
|
+
}
|
|
10851
|
+
function safeMutationFields(request) {
|
|
10852
|
+
const safeFields = Object.fromEntries(["name", "slug", "cron", "method", "enabled"].filter((field) => request[field] !== undefined).map((field) => [field, request[field]]));
|
|
10853
|
+
if (request.body !== undefined) {
|
|
10854
|
+
safeFields.body_empty = Object.keys(request.body).length === 0;
|
|
10855
|
+
}
|
|
10856
|
+
if (request.headers !== undefined) {
|
|
10857
|
+
safeFields.header_names = Object.keys(request.headers).sort();
|
|
10858
|
+
}
|
|
10859
|
+
return safeFields;
|
|
10860
|
+
}
|
|
10861
|
+
function requiredName(args, action) {
|
|
10862
|
+
const name = requiredText2(args, "name", action);
|
|
10863
|
+
if (name.length > MAX_SCHEDULE_NAME_LENGTH)
|
|
10864
|
+
throw new Error(`'name' is too long for '${action}'`);
|
|
10865
|
+
return name;
|
|
10866
|
+
}
|
|
10867
|
+
function requiredSlug(args, action) {
|
|
10868
|
+
const slug = requiredText2(args, "slug", action);
|
|
10869
|
+
if (!SAFE_SLUG_PATTERN.test(slug))
|
|
10870
|
+
throw new Error(`'slug' is invalid for '${action}'`);
|
|
10871
|
+
return slug;
|
|
10872
|
+
}
|
|
10873
|
+
function requiredCron(args, action) {
|
|
10874
|
+
const cron = requiredText2(args, "cron", action);
|
|
10875
|
+
if (!validScheduledFunctionCron(cron))
|
|
10876
|
+
throw new Error(`'cron' is invalid for '${action}'`);
|
|
10877
|
+
return cron;
|
|
10878
|
+
}
|
|
10879
|
+
function updateRequest(args, environment) {
|
|
10880
|
+
const body = scheduleBody(args.body_file);
|
|
10881
|
+
const headers = scheduleHeaders(args.header_env, environment);
|
|
10882
|
+
const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
|
|
10883
|
+
const name = args.name === undefined ? undefined : requiredName(args, "update");
|
|
10884
|
+
const mutationFields = Object.fromEntries([
|
|
10885
|
+
["name", name],
|
|
10886
|
+
["cron", cron],
|
|
10887
|
+
["method", args.method],
|
|
10888
|
+
["enabled", args.enabled],
|
|
10889
|
+
["body", body],
|
|
10890
|
+
["headers", headers]
|
|
10891
|
+
].filter((entry) => entry[1] !== undefined));
|
|
10892
|
+
if (Object.keys(mutationFields).length === 0) {
|
|
10893
|
+
throw new Error("Scheduled Function update requires at least one field");
|
|
10894
|
+
}
|
|
10895
|
+
return { request_id: randomUUID(), ...mutationFields };
|
|
10896
|
+
}
|
|
10897
|
+
async function executeScheduleAction(http, environment, args, readOnly = false) {
|
|
10898
|
+
const action = args.action;
|
|
10899
|
+
if (readOnly && action !== "list")
|
|
10900
|
+
return readOnlyResult2();
|
|
10901
|
+
assertActionArguments2(action, args);
|
|
10902
|
+
const ref = requiredText2(args, "ref", action);
|
|
10903
|
+
if (action === "list")
|
|
10904
|
+
return listResponse(ref, await http.get(schedulePath(ref)));
|
|
10905
|
+
if (action === "create") {
|
|
10906
|
+
const request = createRequest(args, environment);
|
|
10907
|
+
const requestId = request.request_id;
|
|
10908
|
+
return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
|
|
10909
|
+
}
|
|
10910
|
+
const scheduleId = requiredText2(args, "schedule_id", action);
|
|
10911
|
+
if (action === "update") {
|
|
10912
|
+
const request = updateRequest(args, environment);
|
|
10913
|
+
const requestId = request.request_id;
|
|
10914
|
+
return mutationResponse({
|
|
10915
|
+
action,
|
|
10916
|
+
ref,
|
|
10917
|
+
scheduleId,
|
|
10918
|
+
requestId,
|
|
10919
|
+
expectedFields: safeMutationFields(request)
|
|
10920
|
+
}, await http.patch(schedulePath(ref, scheduleId), request));
|
|
10921
|
+
}
|
|
10922
|
+
return deleteResponse(ref, scheduleId, await http.delete(schedulePath(ref, scheduleId)));
|
|
10923
|
+
}
|
|
10924
|
+
function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
|
|
10925
|
+
server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
|
|
10926
|
+
}
|
|
10927
|
+
var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, create, update, delete";
|
|
10928
|
+
var SCHEDULE_TOOL_SCHEMA = {
|
|
10929
|
+
action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
|
|
10930
|
+
ref: withDescription(Type.String(), "Project ref"),
|
|
10931
|
+
schedule_id: optional(Type.String(), "[update/delete] Schedule ID"),
|
|
10932
|
+
name: optional(Type.String(), "[create/update] Display name"),
|
|
10933
|
+
slug: optional(Type.String(), "[create] Edge Function slug"),
|
|
10934
|
+
cron: optional(Type.String(), "[create/update] Five-field cron expression"),
|
|
10935
|
+
method: optional(stringEnum(["GET", "POST"]), "[create/update] HTTP method"),
|
|
10936
|
+
enabled: optional(Type.Boolean(), "[update] Enabled state"),
|
|
10937
|
+
body_file: optional(Type.String(), "[create/update] Local JSON object file; content is never printed"),
|
|
10938
|
+
header_env: withDescription(headerEnvironmentSchema, "[create/update] JSON map of HTTP header names to environment variable names")
|
|
10939
|
+
};
|
|
10940
|
+
// package.json
|
|
10941
|
+
var package_default = {
|
|
10942
|
+
name: "@supacloud/cli",
|
|
10943
|
+
version: "0.16.0",
|
|
10944
|
+
description: "Project-scoped CLI for SupaCloud users",
|
|
10945
|
+
type: "module",
|
|
10946
|
+
main: "./dist/index.js",
|
|
10947
|
+
bin: {
|
|
10948
|
+
"supacloud-cli": "dist/index.js"
|
|
10949
|
+
},
|
|
10950
|
+
files: [
|
|
10951
|
+
"dist",
|
|
10952
|
+
"README.md",
|
|
10953
|
+
"skills"
|
|
10954
|
+
],
|
|
10955
|
+
scripts: {
|
|
10956
|
+
dev: "bun run --watch src/index.ts",
|
|
10957
|
+
build: "bun build src/index.ts --outdir dist --target node",
|
|
10958
|
+
prepublishOnly: "bun run build",
|
|
10959
|
+
typecheck: "tsc --noEmit"
|
|
10960
|
+
},
|
|
10961
|
+
keywords: [
|
|
10962
|
+
"supacloud",
|
|
10963
|
+
"cli",
|
|
10964
|
+
"deploy",
|
|
10965
|
+
"supabase"
|
|
10966
|
+
],
|
|
10967
|
+
license: "MIT",
|
|
10968
|
+
repository: {
|
|
10969
|
+
type: "git",
|
|
10970
|
+
url: "https://github.com/zuohuadong/supacloud.git",
|
|
10971
|
+
directory: "packages/cli"
|
|
10972
|
+
},
|
|
10973
|
+
dependencies: {
|
|
10974
|
+
"@sinclair/typebox": "^0.34.52"
|
|
10975
|
+
},
|
|
10976
|
+
devDependencies: {
|
|
10977
|
+
"@types/bun": "^1.3.14",
|
|
10978
|
+
typescript: "^7.0.2"
|
|
10979
|
+
}
|
|
10980
|
+
};
|
|
10981
|
+
|
|
9605
10982
|
// src/index.ts
|
|
9606
10983
|
var commandName = "supacloud-cli";
|
|
9607
10984
|
var preferredCommand = commandName;
|
|
@@ -9679,9 +11056,12 @@ async function createProjectStatusResult(context) {
|
|
|
9679
11056
|
const checks = await collectProjectStatusChecks(context);
|
|
9680
11057
|
const statusPayload = {
|
|
9681
11058
|
mode: "project",
|
|
9682
|
-
|
|
11059
|
+
environment: context.environment || null,
|
|
11060
|
+
source: { kind: context.source, path: context.sourcePath },
|
|
9683
11061
|
projectRef: context.projectRef || null,
|
|
9684
11062
|
apiUrl: context.apiUrl || null,
|
|
11063
|
+
readOnly: context.readOnly,
|
|
11064
|
+
production: context.production,
|
|
9685
11065
|
autoLinked: Boolean(context.inferredSupabaseUrl && context.inferredServiceRoleKey),
|
|
9686
11066
|
hasApiToken: Boolean(context.apiToken),
|
|
9687
11067
|
checks
|
|
@@ -9714,7 +11094,7 @@ function captureTools(register) {
|
|
|
9714
11094
|
register(server);
|
|
9715
11095
|
return tools;
|
|
9716
11096
|
}
|
|
9717
|
-
function printHelp(context
|
|
11097
|
+
function printHelp(context) {
|
|
9718
11098
|
const autoLink = context.inferredSupabaseUrl ? `Project context: ${context.inferredSupabaseUrl} (${context.source})` : "Project context: not detected";
|
|
9719
11099
|
console.error(`
|
|
9720
11100
|
╔═══════════════════════════════════════════════════════════╗
|
|
@@ -9724,18 +11104,31 @@ function printHelp(context = resolveSupaCloudContext()) {
|
|
|
9724
11104
|
|
|
9725
11105
|
USAGE
|
|
9726
11106
|
|
|
9727
|
-
${preferredCommand} <module> <action> [--flags]
|
|
9728
|
-
${preferredCommand} status
|
|
11107
|
+
${preferredCommand} [global flags] <module> <action> [--flags]
|
|
11108
|
+
${preferredCommand} [global flags] status
|
|
9729
11109
|
${preferredCommand} --help
|
|
11110
|
+
${preferredCommand} --version
|
|
11111
|
+
|
|
11112
|
+
GLOBAL FLAGS
|
|
11113
|
+
|
|
11114
|
+
--env <name> Load .env.supacloud.<name> from the current directory.
|
|
11115
|
+
--env-file <path> Load an exact file that declares SUPACLOUD_ENV.
|
|
11116
|
+
--confirm-production <ref> Confirm a write to the selected production project.
|
|
11117
|
+
|
|
11118
|
+
Global flags may appear before or after the command. --env and --env-file are
|
|
11119
|
+
mutually exclusive, and a selected source is never mixed with another source.
|
|
9730
11120
|
|
|
9731
11121
|
DEFAULT CONTEXT
|
|
9732
11122
|
|
|
9733
|
-
|
|
11123
|
+
Without a selector or project variables, runs use the current project's legacy .env.
|
|
9734
11124
|
Supported auto-link variables:
|
|
9735
11125
|
SUPABASE_URL / SUPACLOUD_API_URL
|
|
9736
11126
|
SUPABASE_SERVICE_ROLE_KEY / SUPACLOUD_API_TOKEN
|
|
9737
11127
|
SUPACLOUD_PROJECT_REF (when it cannot be inferred from <ref>.api.*)
|
|
9738
11128
|
|
|
11129
|
+
SUPACLOUD_READ_ONLY=true blocks remote writes. Production writes require an
|
|
11130
|
+
exact --confirm-production value, and cannot override the selected project ref.
|
|
11131
|
+
|
|
9739
11132
|
status checks configuration, Management API connectivity, and authentication.
|
|
9740
11133
|
It exits non-zero when a required check fails.
|
|
9741
11134
|
|
|
@@ -9752,6 +11145,7 @@ EXAMPLES
|
|
|
9752
11145
|
${preferredCommand} frontend list --ref abc123
|
|
9753
11146
|
${preferredCommand} database query --sql "select now()"
|
|
9754
11147
|
${preferredCommand} database query --ref abc123 --file ./queries/vector-search.sql
|
|
11148
|
+
${preferredCommand} database migration_inventory --ref abc123
|
|
9755
11149
|
${preferredCommand} database push_migrations --ref abc123 --dir supabase/migrations --dry_run
|
|
9756
11150
|
${preferredCommand} supabase migration_new --name add_accounts
|
|
9757
11151
|
${preferredCommand} supabase db_diff --schema public --name add_accounts
|
|
@@ -9763,7 +11157,10 @@ EXAMPLES
|
|
|
9763
11157
|
${preferredCommand} ai show_skill
|
|
9764
11158
|
${preferredCommand} ai install_skill --dry_run
|
|
9765
11159
|
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
|
|
11160
|
+
${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3
|
|
11161
|
+
${preferredCommand} scheduled_functions list --ref abc123
|
|
9766
11162
|
${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
|
|
11163
|
+
${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
|
|
9767
11164
|
${preferredCommand} gateway routes --ref abc123
|
|
9768
11165
|
${preferredCommand} gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
|
|
9769
11166
|
${preferredCommand} gateway config --ref abc123 --rate_limit_tier pro
|
|
@@ -9777,8 +11174,18 @@ SEPARATE ADMIN CLI
|
|
|
9777
11174
|
npx @supacloud/admin --help
|
|
9778
11175
|
`);
|
|
9779
11176
|
}
|
|
9780
|
-
function
|
|
9781
|
-
|
|
11177
|
+
function authorizedToolMap(tools, context, confirmProduction) {
|
|
11178
|
+
validateExecutionPolicyCoverage(tools);
|
|
11179
|
+
for (const [moduleName, tool] of Object.entries(tools)) {
|
|
11180
|
+
const callback = tool.callback;
|
|
11181
|
+
tool.callback = async (args) => {
|
|
11182
|
+
authorizeExecution(moduleName, args, { context, confirmProduction });
|
|
11183
|
+
return callback(args);
|
|
11184
|
+
};
|
|
11185
|
+
}
|
|
11186
|
+
return tools;
|
|
11187
|
+
}
|
|
11188
|
+
function createCliTools(context, confirmProduction) {
|
|
9782
11189
|
let pushMigrations;
|
|
9783
11190
|
const tools = {
|
|
9784
11191
|
status: {
|
|
@@ -9804,6 +11211,8 @@ function createCliTools() {
|
|
|
9804
11211
|
"⚠️ Project commands need a project-scoped API context.",
|
|
9805
11212
|
"",
|
|
9806
11213
|
"Provide one of these sources:",
|
|
11214
|
+
" - --env <name> for .env.supacloud.<name>",
|
|
11215
|
+
" - --env-file <path> for a file declaring SUPACLOUD_ENV",
|
|
9807
11216
|
" - .env with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY",
|
|
9808
11217
|
" - SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN",
|
|
9809
11218
|
"",
|
|
@@ -9816,7 +11225,7 @@ function createCliTools() {
|
|
|
9816
11225
|
]
|
|
9817
11226
|
})
|
|
9818
11227
|
};
|
|
9819
|
-
for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "diagnostics", "gateway", "branch"]) {
|
|
11228
|
+
for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "diagnostics", "gateway", "branch"]) {
|
|
9820
11229
|
tools[name] = {
|
|
9821
11230
|
schema: { action: genericActionSchema },
|
|
9822
11231
|
callback: async () => ({
|
|
@@ -9830,6 +11239,11 @@ function createCliTools() {
|
|
|
9830
11239
|
})
|
|
9831
11240
|
};
|
|
9832
11241
|
}
|
|
11242
|
+
const storageContextCallback = tools.storage.callback;
|
|
11243
|
+
const storageHelpTool = captureTools((server) => registerStorageTools(server, {})).storage;
|
|
11244
|
+
if (storageHelpTool) {
|
|
11245
|
+
tools.storage = { schema: storageHelpTool.schema, callback: storageContextCallback };
|
|
11246
|
+
}
|
|
9833
11247
|
const branchContextCallback = tools.branch.callback;
|
|
9834
11248
|
const branchHelpTool = captureTools((server) => registerBranchTools(server, {}, {
|
|
9835
11249
|
readOnly: true
|
|
@@ -9853,11 +11267,14 @@ function createCliTools() {
|
|
|
9853
11267
|
`${preferredCommand} expects project-scoped credentials by default.`,
|
|
9854
11268
|
"Provide one of these sources:",
|
|
9855
11269
|
"",
|
|
9856
|
-
" 1.
|
|
11270
|
+
" 1. Named environment file",
|
|
11271
|
+
" supacloud-cli --env test status",
|
|
11272
|
+
"",
|
|
11273
|
+
" 2. Current workspace .env",
|
|
9857
11274
|
" SUPABASE_URL=https://your-project.example.com",
|
|
9858
11275
|
" SUPABASE_SERVICE_ROLE_KEY=...",
|
|
9859
11276
|
"",
|
|
9860
|
-
"
|
|
11277
|
+
" 3. Explicit environment variables",
|
|
9861
11278
|
" SUPACLOUD_API_URL=https://your-project.example.com",
|
|
9862
11279
|
" SUPACLOUD_API_TOKEN=...",
|
|
9863
11280
|
"",
|
|
@@ -9869,7 +11286,7 @@ function createCliTools() {
|
|
|
9869
11286
|
]
|
|
9870
11287
|
})
|
|
9871
11288
|
};
|
|
9872
|
-
return tools;
|
|
11289
|
+
return authorizedToolMap(tools, context, confirmProduction);
|
|
9873
11290
|
}
|
|
9874
11291
|
const http = new HttpTransport({
|
|
9875
11292
|
baseUrl: context.apiUrl,
|
|
@@ -9887,7 +11304,12 @@ function createCliTools() {
|
|
|
9887
11304
|
assign(databaseTools);
|
|
9888
11305
|
assign(captureTools((server) => registerAuthTools(server, http)));
|
|
9889
11306
|
assign(captureTools((server) => registerStorageTools(server, http)));
|
|
9890
|
-
assign(captureTools((server) => registerAdvancedTools(server, http
|
|
11307
|
+
assign(captureTools((server) => registerAdvancedTools(server, http, process.env, {
|
|
11308
|
+
readOnly: context.readOnly
|
|
11309
|
+
})));
|
|
11310
|
+
assign(captureTools((server) => registerScheduledFunctionTools(server, http, process.env, {
|
|
11311
|
+
readOnly: context.readOnly
|
|
11312
|
+
})));
|
|
9891
11313
|
assign(captureTools((server) => registerFrontendTools(server, http)));
|
|
9892
11314
|
assign(captureTools((server) => registerGatewayTools(server, http, {
|
|
9893
11315
|
projectRef: context.projectRef || undefined
|
|
@@ -9900,16 +11322,26 @@ function createCliTools() {
|
|
|
9900
11322
|
projectRef: context.projectRef || undefined
|
|
9901
11323
|
})));
|
|
9902
11324
|
delete tools.platform;
|
|
9903
|
-
return tools;
|
|
11325
|
+
return authorizedToolMap(tools, context, confirmProduction);
|
|
9904
11326
|
}
|
|
9905
11327
|
async function main() {
|
|
9906
|
-
const
|
|
11328
|
+
const rawArgs = process.argv.slice(2);
|
|
11329
|
+
if (rawArgs.length === 1 && rawArgs[0] === "--version") {
|
|
11330
|
+
console.log(package_default.version);
|
|
11331
|
+
return;
|
|
11332
|
+
}
|
|
11333
|
+
const globalOptions = parseGlobalOptions(rawArgs);
|
|
11334
|
+
const args = globalOptions.args;
|
|
11335
|
+
const context = resolveSupaCloudContext(process.env, process.cwd(), {
|
|
11336
|
+
environmentName: globalOptions.environmentName,
|
|
11337
|
+
envFile: globalOptions.envFile
|
|
11338
|
+
});
|
|
9907
11339
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
9908
|
-
printHelp();
|
|
11340
|
+
printHelp(context);
|
|
9909
11341
|
process.exitCode = 0;
|
|
9910
11342
|
return;
|
|
9911
11343
|
}
|
|
9912
|
-
const cliTools = createCliTools();
|
|
11344
|
+
const cliTools = createCliTools(context, globalOptions.confirmProduction);
|
|
9913
11345
|
if (args.length === 1 && !["ai", "supabase"].includes(args[0]) && cliTools[args[0]]) {
|
|
9914
11346
|
const result = await cliTools[args[0]].callback({});
|
|
9915
11347
|
if (result?.content && Array.isArray(result.content)) {
|