@supacloud/cli 0.14.6 → 0.15.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 +148 -5
- package/dist/index.js +1133 -121
- 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", "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", "list_files"],
|
|
6487
|
+
write: ["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() {
|
|
@@ -6662,7 +6963,7 @@ Actions: ${allActions.join(", ")}${readOnly ? " (read-only mode)" : ""}`, {
|
|
|
6662
6963
|
owner_column: optional(Type.String(), "[create_table_rls owner] UUID owner column matched to auth.uid()")
|
|
6663
6964
|
}, async (args) => {
|
|
6664
6965
|
const { action } = args;
|
|
6665
|
-
const ref =
|
|
6966
|
+
const ref = args.ref || projectRef;
|
|
6666
6967
|
const schema = args.schema || "public";
|
|
6667
6968
|
const schemas = args.schemas || ["public"];
|
|
6668
6969
|
if (args.file && !args.sql) {
|
|
@@ -7216,6 +7517,62 @@ function pgToTs(t) {
|
|
|
7216
7517
|
}
|
|
7217
7518
|
|
|
7218
7519
|
// src/shared/tools/auth-tools.ts
|
|
7520
|
+
var authConfigRecord = Type.Record(Type.String(), Type.Unknown());
|
|
7521
|
+
var safeAuthMutationCodes = new Set([
|
|
7522
|
+
"AUTH_RUNTIME_APPLY_FAILED",
|
|
7523
|
+
"SUPAUTH_DEPENDENT_REFRESH_FAILED"
|
|
7524
|
+
]);
|
|
7525
|
+
function parseAuthConfig(input) {
|
|
7526
|
+
if (typeof input !== "string")
|
|
7527
|
+
return input;
|
|
7528
|
+
try {
|
|
7529
|
+
return JSON.parse(input);
|
|
7530
|
+
} catch (error) {
|
|
7531
|
+
if (!(error instanceof SyntaxError))
|
|
7532
|
+
throw error;
|
|
7533
|
+
throw new Error("Invalid auth config JSON object");
|
|
7534
|
+
}
|
|
7535
|
+
}
|
|
7536
|
+
var authConfigSchema = decodedSchema(Type.Union([Type.String(), authConfigRecord]), authConfigRecord, parseAuthConfig);
|
|
7537
|
+
function safeAuthFailureFields(payload) {
|
|
7538
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
7539
|
+
return {};
|
|
7540
|
+
const body = payload;
|
|
7541
|
+
const fields = {};
|
|
7542
|
+
if (typeof body.code === "string" && safeAuthMutationCodes.has(body.code)) {
|
|
7543
|
+
fields.code = body.code;
|
|
7544
|
+
}
|
|
7545
|
+
for (const field of ["persisted", "runtime_applied", "dependents_applied"]) {
|
|
7546
|
+
if (typeof body[field] === "boolean")
|
|
7547
|
+
fields[field] = body[field];
|
|
7548
|
+
}
|
|
7549
|
+
if (body.dependent_status === "failed" || body.dependent_status === "unknown") {
|
|
7550
|
+
fields.dependent_status = body.dependent_status;
|
|
7551
|
+
}
|
|
7552
|
+
if (body.runtime_mode === "local" || body.runtime_mode === "owner" || body.runtime_mode === "shared") {
|
|
7553
|
+
fields.runtime_mode = body.runtime_mode;
|
|
7554
|
+
}
|
|
7555
|
+
return fields;
|
|
7556
|
+
}
|
|
7557
|
+
function safeAuthMutationFailure(response) {
|
|
7558
|
+
return {
|
|
7559
|
+
ok: false,
|
|
7560
|
+
http_status: response.status,
|
|
7561
|
+
...safeAuthFailureFields(response.data)
|
|
7562
|
+
};
|
|
7563
|
+
}
|
|
7564
|
+
function authMutationResult(response, successMessage) {
|
|
7565
|
+
if (response.ok) {
|
|
7566
|
+
return { content: [{ type: "text", text: successMessage }] };
|
|
7567
|
+
}
|
|
7568
|
+
return {
|
|
7569
|
+
isError: true,
|
|
7570
|
+
content: [{
|
|
7571
|
+
type: "text",
|
|
7572
|
+
text: JSON.stringify(safeAuthMutationFailure(response), null, 2)
|
|
7573
|
+
}]
|
|
7574
|
+
};
|
|
7575
|
+
}
|
|
7219
7576
|
function formatProviders(data) {
|
|
7220
7577
|
if (!data || typeof data !== "object")
|
|
7221
7578
|
return JSON.stringify(data, null, 2);
|
|
@@ -7272,7 +7629,7 @@ Actions: list_providers, get_provider, configure_provider, update_provider, disa
|
|
|
7272
7629
|
url: optional(Type.String(), "[configure] Custom OAuth URL"),
|
|
7273
7630
|
app_id: optional(Type.String(), "[wechat_*] WeChat App ID"),
|
|
7274
7631
|
app_secret: optional(Type.String(), "[wechat_*] WeChat App Secret"),
|
|
7275
|
-
config: optional(
|
|
7632
|
+
config: optional(authConfigSchema, "[update_settings/update_config] Config fields as a JSON object")
|
|
7276
7633
|
}, async (args) => {
|
|
7277
7634
|
const { action, ref, provider, client_id, client_secret, redirect_uri, url, app_id, app_secret, config } = args;
|
|
7278
7635
|
const need = (f) => {
|
|
@@ -7355,8 +7712,7 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
7355
7712
|
need("ref");
|
|
7356
7713
|
if (!config)
|
|
7357
7714
|
throw new Error("'config' required");
|
|
7358
|
-
|
|
7359
|
-
break;
|
|
7715
|
+
return authMutationResult(await http.patch(`/v1/projects/${ref}/auth/config`, config), "✅ Auth settings updated");
|
|
7360
7716
|
case "get_config":
|
|
7361
7717
|
need("ref");
|
|
7362
7718
|
text = ok(await http.get(`/v1/projects/${ref}/config/auth`));
|
|
@@ -7365,8 +7721,7 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status}): ${JSON.stringif
|
|
|
7365
7721
|
need("ref");
|
|
7366
7722
|
if (!config)
|
|
7367
7723
|
throw new Error("'config' required");
|
|
7368
|
-
|
|
7369
|
-
break;
|
|
7724
|
+
return authMutationResult(await http.patch(`/v1/projects/${ref}/config/auth`, config), "✅ Auth config updated");
|
|
7370
7725
|
default:
|
|
7371
7726
|
text = `❌ Unknown action: ${action}`;
|
|
7372
7727
|
}
|
|
@@ -7469,6 +7824,39 @@ import { tmpdir } from "node:os";
|
|
|
7469
7824
|
import { basename as basename2, join as join2, resolve as resolve2 } from "node:path";
|
|
7470
7825
|
import { promisify } from "node:util";
|
|
7471
7826
|
import { execFile } from "node:child_process";
|
|
7827
|
+
|
|
7828
|
+
// src/shared/tools/release-control-response.ts
|
|
7829
|
+
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
7830
|
+
function releaseControlSuccess(operation, payload) {
|
|
7831
|
+
return releaseControlResponse({
|
|
7832
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7833
|
+
ok: true,
|
|
7834
|
+
operation,
|
|
7835
|
+
...payload
|
|
7836
|
+
});
|
|
7837
|
+
}
|
|
7838
|
+
function releaseControlFailure(operation, code, httpStatus) {
|
|
7839
|
+
return releaseControlErrorResponse({
|
|
7840
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
7841
|
+
ok: false,
|
|
7842
|
+
operation,
|
|
7843
|
+
error: { code, http_status: httpStatus }
|
|
7844
|
+
});
|
|
7845
|
+
}
|
|
7846
|
+
function releaseControlMutationFailure(operation, response) {
|
|
7847
|
+
const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
|
|
7848
|
+
return outcomeUnknown ? releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.transportError ? null : response.status) : releaseControlFailure(operation, "HTTP_ERROR", response.status);
|
|
7849
|
+
}
|
|
7850
|
+
function releaseControlResponse(payload) {
|
|
7851
|
+
return {
|
|
7852
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
7853
|
+
};
|
|
7854
|
+
}
|
|
7855
|
+
function releaseControlErrorResponse(payload) {
|
|
7856
|
+
return { ...releaseControlResponse(payload), isError: true };
|
|
7857
|
+
}
|
|
7858
|
+
|
|
7859
|
+
// src/shared/tools/advanced-tools.ts
|
|
7472
7860
|
var execFileAsync = promisify(execFile);
|
|
7473
7861
|
async function runBunBuild(args) {
|
|
7474
7862
|
try {
|
|
@@ -7540,7 +7928,29 @@ function parseFunctionFiles(input) {
|
|
|
7540
7928
|
}
|
|
7541
7929
|
}
|
|
7542
7930
|
var functionFilesSchema = decodedSchema(Type.Union([Type.String(), functionFilesRecordSchema]), functionFilesRecordSchema, parseFunctionFiles);
|
|
7931
|
+
function parseFunctionVersion(input) {
|
|
7932
|
+
const version = String(input);
|
|
7933
|
+
if (!CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
|
|
7934
|
+
throw new Error("Function version must be a canonical safe integer");
|
|
7935
|
+
}
|
|
7936
|
+
return version;
|
|
7937
|
+
}
|
|
7938
|
+
var CANONICAL_FUNCTION_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)$/;
|
|
7939
|
+
var SAFE_FUNCTION_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7940
|
+
var SAFE_FUNCTION_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
7941
|
+
var FUNCTION_ACTIVATION_ARGUMENTS = new Set(["action", "ref", "slug", "version"]);
|
|
7942
|
+
var functionVersionSchema = Type.Optional(decodedSchema(Type.Union([
|
|
7943
|
+
Type.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }),
|
|
7944
|
+
Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 })
|
|
7945
|
+
]), Type.String({ pattern: CANONICAL_FUNCTION_VERSION_PATTERN.source, maxLength: 16 }), parseFunctionVersion));
|
|
7543
7946
|
var secretListSchema = Type.Array(Type.Object({ name: Type.String(), value: Type.String() }));
|
|
7947
|
+
var ENVIRONMENT_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
7948
|
+
var MAX_SECRET_COUNT = 1024;
|
|
7949
|
+
var MAX_ENVIRONMENT_SECRET_BYTES = 24 * 1024;
|
|
7950
|
+
var MAX_SECRET_JSON_BYTES = 1024 * 1024;
|
|
7951
|
+
var INVALID_ENVIRONMENT_SECRET_NAMES_MESSAGE = "Environment secret names are invalid";
|
|
7952
|
+
var INVALID_ENVIRONMENT_SECRET_VALUES_MESSAGE = "Environment secret values are missing or exceed safe limits";
|
|
7953
|
+
var INVALID_SECRET_LIST_RESPONSE = "❌ Project secret list response is invalid";
|
|
7544
7954
|
function parseSecrets(value) {
|
|
7545
7955
|
if (Array.isArray(value))
|
|
7546
7956
|
return value;
|
|
@@ -7567,6 +7977,89 @@ function parseSecrets(value) {
|
|
|
7567
7977
|
}).filter((entry) => entry.name);
|
|
7568
7978
|
}
|
|
7569
7979
|
var secretsSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), secretListSchema]), secretListSchema, parseSecrets));
|
|
7980
|
+
function validEnvironmentSecretNames(names) {
|
|
7981
|
+
if (names.length === 0 || names.length > MAX_SECRET_COUNT)
|
|
7982
|
+
return false;
|
|
7983
|
+
const uniqueNames = new Set;
|
|
7984
|
+
for (const name of names) {
|
|
7985
|
+
if (!ENVIRONMENT_SECRET_NAME_PATTERN.test(name) || uniqueNames.has(name))
|
|
7986
|
+
return false;
|
|
7987
|
+
uniqueNames.add(name);
|
|
7988
|
+
}
|
|
7989
|
+
return true;
|
|
7990
|
+
}
|
|
7991
|
+
function parseEnvironmentSecretNames(input) {
|
|
7992
|
+
const names = input.split(",", MAX_SECRET_COUNT + 1).map((name) => name.trim());
|
|
7993
|
+
if (!validEnvironmentSecretNames(names)) {
|
|
7994
|
+
throw new Error(INVALID_ENVIRONMENT_SECRET_NAMES_MESSAGE);
|
|
7995
|
+
}
|
|
7996
|
+
return names;
|
|
7997
|
+
}
|
|
7998
|
+
var environmentSecretNamesSchema = Type.Optional(decodedSchema(Type.String(), Type.Array(Type.String({ pattern: ENVIRONMENT_SECRET_NAME_PATTERN.source }), {
|
|
7999
|
+
minItems: 1,
|
|
8000
|
+
maxItems: MAX_SECRET_COUNT,
|
|
8001
|
+
uniqueItems: true
|
|
8002
|
+
}), parseEnvironmentSecretNames));
|
|
8003
|
+
function jsonWithinSecretLimit(payload) {
|
|
8004
|
+
try {
|
|
8005
|
+
const serializedPayload = JSON.stringify(payload);
|
|
8006
|
+
return serializedPayload !== undefined && Buffer.byteLength(serializedPayload) <= MAX_SECRET_JSON_BYTES;
|
|
8007
|
+
} catch (error) {
|
|
8008
|
+
if (error instanceof TypeError)
|
|
8009
|
+
return false;
|
|
8010
|
+
throw error;
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
function maskedSecretEntry(candidate) {
|
|
8014
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
|
|
8015
|
+
return null;
|
|
8016
|
+
const { name, value } = candidate;
|
|
8017
|
+
if (typeof name !== "string" || !ENVIRONMENT_SECRET_NAME_PATTERN.test(name))
|
|
8018
|
+
return null;
|
|
8019
|
+
if (value !== "********")
|
|
8020
|
+
return null;
|
|
8021
|
+
return { name, value: "********" };
|
|
8022
|
+
}
|
|
8023
|
+
function projectedMaskedSecrets(payload) {
|
|
8024
|
+
if (!Array.isArray(payload) || payload.length > MAX_SECRET_COUNT)
|
|
8025
|
+
return null;
|
|
8026
|
+
if (!jsonWithinSecretLimit(payload))
|
|
8027
|
+
return null;
|
|
8028
|
+
const secretNames = new Set;
|
|
8029
|
+
const projectedSecrets = [];
|
|
8030
|
+
for (const candidate of payload) {
|
|
8031
|
+
const secret = maskedSecretEntry(candidate);
|
|
8032
|
+
if (!secret || secretNames.has(secret.name))
|
|
8033
|
+
return null;
|
|
8034
|
+
secretNames.add(secret.name);
|
|
8035
|
+
projectedSecrets.push(secret);
|
|
8036
|
+
}
|
|
8037
|
+
return projectedSecrets;
|
|
8038
|
+
}
|
|
8039
|
+
function secretsFromEnvironment(names, environment) {
|
|
8040
|
+
const secrets = names.map((name) => {
|
|
8041
|
+
const secretValue = environment[name];
|
|
8042
|
+
if (typeof secretValue !== "string" || secretValue.length === 0 || Buffer.byteLength(secretValue) > MAX_ENVIRONMENT_SECRET_BYTES) {
|
|
8043
|
+
throw new Error(INVALID_ENVIRONMENT_SECRET_VALUES_MESSAGE);
|
|
8044
|
+
}
|
|
8045
|
+
return { name, value: secretValue };
|
|
8046
|
+
});
|
|
8047
|
+
if (!jsonWithinSecretLimit(secrets)) {
|
|
8048
|
+
throw new Error(INVALID_ENVIRONMENT_SECRET_VALUES_MESSAGE);
|
|
8049
|
+
}
|
|
8050
|
+
return secrets;
|
|
8051
|
+
}
|
|
8052
|
+
function secretsForUpsert(inlineSecrets, environmentNames, environment) {
|
|
8053
|
+
if (inlineSecrets !== undefined && environmentNames !== undefined) {
|
|
8054
|
+
throw new Error("'--from-env' cannot be combined with '--secrets'");
|
|
8055
|
+
}
|
|
8056
|
+
if (environmentNames !== undefined) {
|
|
8057
|
+
return secretsFromEnvironment(environmentNames, environment);
|
|
8058
|
+
}
|
|
8059
|
+
if (!inlineSecrets?.length)
|
|
8060
|
+
throw new Error("'secrets' array required");
|
|
8061
|
+
return inlineSecrets;
|
|
8062
|
+
}
|
|
7570
8063
|
function confirmedFunctionConfig(payload, expected) {
|
|
7571
8064
|
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
7572
8065
|
return false;
|
|
@@ -7587,12 +8080,60 @@ function functionSourceCode(payload) {
|
|
|
7587
8080
|
const code = payload.code;
|
|
7588
8081
|
return typeof code === "string" ? code : null;
|
|
7589
8082
|
}
|
|
7590
|
-
function
|
|
8083
|
+
function objectRecord(candidate) {
|
|
8084
|
+
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
8085
|
+
}
|
|
8086
|
+
function activationResponse(slug, version, response) {
|
|
8087
|
+
const operation = "edge_functions.activate";
|
|
8088
|
+
if (!response.ok)
|
|
8089
|
+
return releaseControlMutationFailure(operation, response);
|
|
8090
|
+
const receipt = objectRecord(response.data);
|
|
8091
|
+
const config = objectRecord(receipt?.config);
|
|
8092
|
+
if (receipt?.success !== true || receipt.version !== version || config?.version !== version || typeof config.verify_jwt !== "boolean") {
|
|
8093
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
8094
|
+
}
|
|
8095
|
+
return releaseControlSuccess(operation, {
|
|
8096
|
+
slug,
|
|
8097
|
+
version,
|
|
8098
|
+
verify_jwt: config.verify_jwt
|
|
8099
|
+
});
|
|
8100
|
+
}
|
|
8101
|
+
function readOnlyActivationResult() {
|
|
8102
|
+
return {
|
|
8103
|
+
isError: true,
|
|
8104
|
+
content: [{ type: "text", text: "⚠️ Edge Function activation blocked in read-only mode." }]
|
|
8105
|
+
};
|
|
8106
|
+
}
|
|
8107
|
+
function functionActivationTarget(args) {
|
|
8108
|
+
const projectRef = typeof args.ref === "string" ? args.ref.trim() : "";
|
|
8109
|
+
const functionSlug = typeof args.slug === "string" ? args.slug.trim() : "";
|
|
8110
|
+
const version = args.version;
|
|
8111
|
+
if (!SAFE_FUNCTION_REF_PATTERN.test(projectRef))
|
|
8112
|
+
throw new Error("'ref' is invalid for 'activate'");
|
|
8113
|
+
if (!SAFE_FUNCTION_SLUG_PATTERN.test(functionSlug))
|
|
8114
|
+
throw new Error("'slug' is invalid for 'activate'");
|
|
8115
|
+
if (typeof version !== "string" || !CANONICAL_FUNCTION_VERSION_PATTERN.test(version) || !Number.isSafeInteger(Number(version))) {
|
|
8116
|
+
throw new Error("'version' is invalid for 'activate'");
|
|
8117
|
+
}
|
|
8118
|
+
return { projectRef, functionSlug, version };
|
|
8119
|
+
}
|
|
8120
|
+
async function activateFunctionVersion(http, args, readOnly = false) {
|
|
8121
|
+
if (readOnly)
|
|
8122
|
+
return readOnlyActivationResult();
|
|
8123
|
+
const unsupported = Object.keys(args).filter((name) => !FUNCTION_ACTIVATION_ARGUMENTS.has(name));
|
|
8124
|
+
if (unsupported.length > 0)
|
|
8125
|
+
throw new Error(`'${unsupported[0]}' is not supported for 'activate'`);
|
|
8126
|
+
const { projectRef, functionSlug, version } = functionActivationTarget(args);
|
|
8127
|
+
const endpoint = `/v1/projects/${encodeURIComponent(projectRef)}/functions/${encodeURIComponent(functionSlug)}` + `/versions/${encodeURIComponent(version)}/activate`;
|
|
8128
|
+
return activationResponse(functionSlug, version, await http.post(endpoint));
|
|
8129
|
+
}
|
|
8130
|
+
function registerAdvancedTools(server, http, environment = process.env, options = {}) {
|
|
7591
8131
|
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"),
|
|
8132
|
+
Actions: list, deploy, deploy_bundle, config, source, activate, delete, check`, {
|
|
8133
|
+
action: withDescription(stringEnum(["list", "deploy", "deploy_bundle", "config", "source", "activate", "delete", "check"]), "Action"),
|
|
7594
8134
|
ref: withDescription(Type.String(), "Project ref"),
|
|
7595
|
-
slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/delete/check] Function name"),
|
|
8135
|
+
slug: optional(Type.String(), "[deploy/deploy_bundle/config/source/activate/delete/check] Function name"),
|
|
8136
|
+
version: withDescription(functionVersionSchema, "[activate] Existing Function version"),
|
|
7596
8137
|
code: optional(Type.String(), "[deploy/check] Function source code (TypeScript)"),
|
|
7597
8138
|
path: optional(Type.String(), "[deploy/check] Local file path to read code from (alternative to code)"),
|
|
7598
8139
|
output: optional(Type.String(), "[source] Write source to this local file instead of stdout; the file must not already exist"),
|
|
@@ -7602,6 +8143,8 @@ Actions: list, deploy, deploy_bundle, config, source, delete, check`, {
|
|
|
7602
8143
|
verify_jwt: optional(Type.Boolean(), "[deploy/deploy_bundle/config] Set JWT verification for this function"),
|
|
7603
8144
|
background_routes: withDescription(backgroundRoutesSchema, "[deploy/deploy_bundle/config] Background route paths; pass comma-separated or JSON array in CLI")
|
|
7604
8145
|
}, async (args) => {
|
|
8146
|
+
if (args.action === "activate")
|
|
8147
|
+
return activateFunctionVersion(http, args, options.readOnly);
|
|
7605
8148
|
const { action, ref, slug, path: pathArg, output, files, entrypoint, minify, verify_jwt, background_routes } = args;
|
|
7606
8149
|
let code = args.code;
|
|
7607
8150
|
const need = (f, v) => {
|
|
@@ -7737,18 +8280,28 @@ Actions: list, upsert, delete`, {
|
|
|
7737
8280
|
action: withDescription(stringEnum(["list", "upsert", "delete"]), "Action"),
|
|
7738
8281
|
ref: withDescription(Type.String(), "Project ref"),
|
|
7739
8282
|
secrets: withDescription(secretsSchema, "[upsert] Secret list as JSON array or KEY=VALUE,KEY2=VALUE2"),
|
|
8283
|
+
"from-env": withDescription(environmentSecretNamesSchema, "[upsert] Comma-separated environment variable names; values are read from this CLI process"),
|
|
7740
8284
|
name: optional(Type.String(), "[delete] Secret name to delete")
|
|
7741
8285
|
}, async (args) => {
|
|
7742
8286
|
const { action, ref, secrets, name } = args;
|
|
8287
|
+
const environmentNames = args["from-env"];
|
|
7743
8288
|
let text;
|
|
7744
8289
|
switch (action) {
|
|
7745
|
-
case "list":
|
|
7746
|
-
|
|
8290
|
+
case "list": {
|
|
8291
|
+
const response = await http.get(`/v1/projects/${ref}/secrets`, {
|
|
8292
|
+
maxResponseBytes: MAX_SECRET_JSON_BYTES
|
|
8293
|
+
});
|
|
8294
|
+
if (!response.ok) {
|
|
8295
|
+
text = `❌ Failed (${response.status})`;
|
|
8296
|
+
break;
|
|
8297
|
+
}
|
|
8298
|
+
const maskedSecrets = projectedMaskedSecrets(response.data);
|
|
8299
|
+
text = maskedSecrets === null ? INVALID_SECRET_LIST_RESPONSE : JSON.stringify(maskedSecrets, null, 2);
|
|
7747
8300
|
break;
|
|
8301
|
+
}
|
|
7748
8302
|
case "upsert":
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
text = (await http.post(`/v1/projects/${ref}/secrets`, secrets)).ok ? `✅ Updated ${secrets.length} secrets` : `❌ Failed`;
|
|
8303
|
+
const secretsToUpsert = secretsForUpsert(secrets, environmentNames, environment);
|
|
8304
|
+
text = (await http.post(`/v1/projects/${ref}/secrets`, secretsToUpsert)).ok ? `✅ Updated ${secretsToUpsert.length} secrets` : `❌ Failed`;
|
|
7752
8305
|
break;
|
|
7753
8306
|
case "delete":
|
|
7754
8307
|
if (!name)
|
|
@@ -8171,7 +8724,7 @@ function buildProjectLogsPath(ref, logType) {
|
|
|
8171
8724
|
return `/v1/projects/${ref}/logs?${params.toString()}`;
|
|
8172
8725
|
}
|
|
8173
8726
|
function resolveRef(refFromArgs, defaultRef) {
|
|
8174
|
-
const ref =
|
|
8727
|
+
const ref = refFromArgs || defaultRef;
|
|
8175
8728
|
if (!ref)
|
|
8176
8729
|
throw new Error("'ref' is required for this action");
|
|
8177
8730
|
return ref;
|
|
@@ -8360,7 +8913,7 @@ function formatMessages(data, label = "Messages") {
|
|
|
8360
8913
|
return out;
|
|
8361
8914
|
}
|
|
8362
8915
|
function resolveRef2(refFromArgs, defaultRef) {
|
|
8363
|
-
const ref =
|
|
8916
|
+
const ref = refFromArgs || defaultRef;
|
|
8364
8917
|
if (!ref)
|
|
8365
8918
|
throw new Error("'ref' is required for this action");
|
|
8366
8919
|
return ref;
|
|
@@ -8660,7 +9213,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
8660
9213
|
custom_hostname: optional(Type.String(), "[set_custom_hostname] 自定义域名")
|
|
8661
9214
|
}, async (args) => {
|
|
8662
9215
|
const resolveRef3 = (override) => {
|
|
8663
|
-
const ref2 =
|
|
9216
|
+
const ref2 = override || projectRef;
|
|
8664
9217
|
if (!ref2)
|
|
8665
9218
|
throw new Error("'ref' is required for this action");
|
|
8666
9219
|
return ref2;
|
|
@@ -9602,6 +10155,416 @@ function registerAiTools(server) {
|
|
|
9602
10155
|
});
|
|
9603
10156
|
}
|
|
9604
10157
|
|
|
10158
|
+
// src/shared/tools/scheduled-function-tools.ts
|
|
10159
|
+
import { randomUUID } from "node:crypto";
|
|
10160
|
+
import { readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
|
|
10161
|
+
import { resolve as resolve5 } from "node:path";
|
|
10162
|
+
import { isDeepStrictEqual } from "node:util";
|
|
10163
|
+
var HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/;
|
|
10164
|
+
var ENVIRONMENT_NAME_PATTERN2 = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
|
|
10165
|
+
var FORBIDDEN_HEADER_NAMES = new Set([
|
|
10166
|
+
"apikey",
|
|
10167
|
+
"authorization",
|
|
10168
|
+
"connection",
|
|
10169
|
+
"content-length",
|
|
10170
|
+
"forwarded",
|
|
10171
|
+
"host",
|
|
10172
|
+
"keep-alive",
|
|
10173
|
+
"proxy-authenticate",
|
|
10174
|
+
"proxy-authorization",
|
|
10175
|
+
"proxy-connection",
|
|
10176
|
+
"te",
|
|
10177
|
+
"trailer",
|
|
10178
|
+
"transfer-encoding",
|
|
10179
|
+
"upgrade",
|
|
10180
|
+
"via",
|
|
10181
|
+
"x-project-ref"
|
|
10182
|
+
]);
|
|
10183
|
+
var PROJECT_REF_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
10184
|
+
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}$/;
|
|
10185
|
+
var SAFE_SLUG_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
10186
|
+
var CRON_PART_PATTERN = /^(\*|([0-9]+)(?:-([0-9]+))?)(?:\/([0-9]+))?$/;
|
|
10187
|
+
var CRON_FIELD_BOUNDS = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 7]];
|
|
10188
|
+
var MAX_CRON_EXPRESSION_LENGTH = 256;
|
|
10189
|
+
var MAX_BODY_FILE_BYTES = 1048576;
|
|
10190
|
+
var MAX_HEADER_COUNT = 64;
|
|
10191
|
+
var MAX_HEADER_VALUE_LENGTH = 8192;
|
|
10192
|
+
var MAX_SCHEDULE_NAME_LENGTH = 120;
|
|
10193
|
+
var headerEnvironmentRecord = Type.Record(Type.String(), Type.String());
|
|
10194
|
+
var ACTION_ARGUMENTS = {
|
|
10195
|
+
list: new Set(["action", "ref"]),
|
|
10196
|
+
create: new Set(["action", "ref", "name", "slug", "cron", "method", "body_file", "header_env"]),
|
|
10197
|
+
update: new Set(["action", "ref", "schedule_id", "name", "cron", "method", "enabled", "body_file", "header_env"]),
|
|
10198
|
+
delete: new Set(["action", "ref", "schedule_id"])
|
|
10199
|
+
};
|
|
10200
|
+
function parseHeaderEnvironment(input) {
|
|
10201
|
+
if (typeof input !== "string")
|
|
10202
|
+
return input;
|
|
10203
|
+
try {
|
|
10204
|
+
return JSON.parse(input);
|
|
10205
|
+
} catch (error) {
|
|
10206
|
+
if (!(error instanceof SyntaxError))
|
|
10207
|
+
throw error;
|
|
10208
|
+
throw new Error("Invalid header_env JSON object");
|
|
10209
|
+
}
|
|
10210
|
+
}
|
|
10211
|
+
var headerEnvironmentSchema = Type.Optional(decodedSchema(Type.Union([Type.String(), headerEnvironmentRecord]), headerEnvironmentRecord, parseHeaderEnvironment));
|
|
10212
|
+
function objectRecord2(candidate) {
|
|
10213
|
+
return candidate && typeof candidate === "object" && !Array.isArray(candidate) ? candidate : null;
|
|
10214
|
+
}
|
|
10215
|
+
function boundedCronInteger(input, minimum, maximum) {
|
|
10216
|
+
const parsed = Number(input);
|
|
10217
|
+
return Number.isSafeInteger(parsed) && parsed >= minimum && parsed <= maximum;
|
|
10218
|
+
}
|
|
10219
|
+
function validCronPart(part, minimum, maximum) {
|
|
10220
|
+
const match = CRON_PART_PATTERN.exec(part);
|
|
10221
|
+
if (!match)
|
|
10222
|
+
return false;
|
|
10223
|
+
const start = match[1] === "*" ? minimum : Number(match[2]);
|
|
10224
|
+
const end = match[1] === "*" ? maximum : Number(match[3] ?? match[2]);
|
|
10225
|
+
const step = Number(match[4] ?? "1");
|
|
10226
|
+
return boundedCronInteger(String(start), minimum, maximum) && boundedCronInteger(String(end), minimum, maximum) && start <= end && boundedCronInteger(String(step), 1, maximum - minimum + 1);
|
|
10227
|
+
}
|
|
10228
|
+
function validCronField(field, minimum, maximum) {
|
|
10229
|
+
const parts = field.split(",");
|
|
10230
|
+
return parts.length <= maximum - minimum + 1 && parts.every((part) => part.length > 0 && validCronPart(part, minimum, maximum));
|
|
10231
|
+
}
|
|
10232
|
+
function validScheduledFunctionCron(expression) {
|
|
10233
|
+
if (!expression || expression.length > MAX_CRON_EXPRESSION_LENGTH)
|
|
10234
|
+
return false;
|
|
10235
|
+
const fields = expression.trim().split(/\s+/);
|
|
10236
|
+
return fields.length === CRON_FIELD_BOUNDS.length && fields.every((field, index) => {
|
|
10237
|
+
const [minimum, maximum] = CRON_FIELD_BOUNDS[index];
|
|
10238
|
+
return validCronField(field, minimum, maximum);
|
|
10239
|
+
});
|
|
10240
|
+
}
|
|
10241
|
+
function readScheduleBodyFile(bodyPathInput) {
|
|
10242
|
+
if (!bodyPathInput.trim())
|
|
10243
|
+
throw new Error("'body_file' must be a path");
|
|
10244
|
+
const bodyPath = resolve5(bodyPathInput);
|
|
10245
|
+
const bodyStat = statSync4(bodyPath);
|
|
10246
|
+
if (!bodyStat.isFile() || bodyStat.size > MAX_BODY_FILE_BYTES) {
|
|
10247
|
+
throw new Error("Scheduled Function body file must be a regular file no larger than 1 MiB");
|
|
10248
|
+
}
|
|
10249
|
+
let payload;
|
|
10250
|
+
try {
|
|
10251
|
+
payload = JSON.parse(readFileSync6(bodyPath, "utf8"));
|
|
10252
|
+
} catch (error) {
|
|
10253
|
+
if (!(error instanceof SyntaxError))
|
|
10254
|
+
throw error;
|
|
10255
|
+
throw new Error("Scheduled Function body file must contain exact JSON");
|
|
10256
|
+
}
|
|
10257
|
+
const body = objectRecord2(payload);
|
|
10258
|
+
if (!body)
|
|
10259
|
+
throw new Error("Scheduled Function body file must contain a JSON object");
|
|
10260
|
+
return body;
|
|
10261
|
+
}
|
|
10262
|
+
function scheduleBody(bodyFile) {
|
|
10263
|
+
if (bodyFile === undefined)
|
|
10264
|
+
return;
|
|
10265
|
+
if (typeof bodyFile !== "string")
|
|
10266
|
+
throw new Error("'body_file' must be a path");
|
|
10267
|
+
return readScheduleBodyFile(bodyFile);
|
|
10268
|
+
}
|
|
10269
|
+
function resolvedHeaderEntry(headerName, environmentName, environment) {
|
|
10270
|
+
const normalizedName = headerName.toLowerCase();
|
|
10271
|
+
if (!HEADER_NAME_PATTERN.test(headerName) || forbiddenHeaderName(normalizedName) || typeof environmentName !== "string" || !ENVIRONMENT_NAME_PATTERN2.test(environmentName)) {
|
|
10272
|
+
throw new Error("SCHEDULE_HEADER_MAPPING_INVALID");
|
|
10273
|
+
}
|
|
10274
|
+
const headerValue = environment[environmentName];
|
|
10275
|
+
if (!headerValue)
|
|
10276
|
+
throw new Error("SCHEDULE_HEADER_ENV_MISSING");
|
|
10277
|
+
if (!headerValueIsStable(normalizedName, headerValue))
|
|
10278
|
+
throw new Error("SCHEDULE_HEADER_INVALID");
|
|
10279
|
+
return [normalizedName, headerValue];
|
|
10280
|
+
}
|
|
10281
|
+
function forbiddenHeaderName(name) {
|
|
10282
|
+
return FORBIDDEN_HEADER_NAMES.has(name) || name.startsWith("x-forwarded-");
|
|
10283
|
+
}
|
|
10284
|
+
function headerValueIsStable(name, value) {
|
|
10285
|
+
if (!value || value.length > MAX_HEADER_VALUE_LENGTH)
|
|
10286
|
+
return false;
|
|
10287
|
+
try {
|
|
10288
|
+
const headers = new Headers;
|
|
10289
|
+
headers.set(name, value);
|
|
10290
|
+
return headers.get(name) === value;
|
|
10291
|
+
} catch {
|
|
10292
|
+
return false;
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10295
|
+
function scheduleHeaders(mapping, environment) {
|
|
10296
|
+
if (mapping === undefined)
|
|
10297
|
+
return;
|
|
10298
|
+
const headerEnvironment = objectRecord2(mapping);
|
|
10299
|
+
if (!headerEnvironment)
|
|
10300
|
+
throw new Error("'header_env' must be a JSON object");
|
|
10301
|
+
const entries = Object.entries(headerEnvironment).map(([headerName, environmentName]) => resolvedHeaderEntry(headerName, environmentName, environment));
|
|
10302
|
+
const names = entries.map(([name]) => name);
|
|
10303
|
+
if (entries.length > MAX_HEADER_COUNT || new Set(names).size !== names.length) {
|
|
10304
|
+
throw new Error("SCHEDULE_HEADER_INVALID");
|
|
10305
|
+
}
|
|
10306
|
+
return Object.fromEntries(entries);
|
|
10307
|
+
}
|
|
10308
|
+
function validSafeSchedule(schedule) {
|
|
10309
|
+
return validScheduleIdentity(schedule) && validScheduleDefinition(schedule) && validScheduleMetadata(schedule);
|
|
10310
|
+
}
|
|
10311
|
+
function validScheduleIdentity(schedule) {
|
|
10312
|
+
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);
|
|
10313
|
+
}
|
|
10314
|
+
function validScheduleDefinition(schedule) {
|
|
10315
|
+
return typeof schedule.cron === "string" && validScheduledFunctionCron(schedule.cron) && (schedule.method === "GET" || schedule.method === "POST") && typeof schedule.enabled === "boolean";
|
|
10316
|
+
}
|
|
10317
|
+
function validScheduleMetadata(schedule) {
|
|
10318
|
+
return typeof schedule.created_at === "string" && typeof schedule.updated_at === "string";
|
|
10319
|
+
}
|
|
10320
|
+
function safeSchedulePayload(schedule) {
|
|
10321
|
+
const headerNames = safeHeaderNames(schedule.header_names);
|
|
10322
|
+
if (typeof schedule.body_empty !== "boolean" || !headerNames)
|
|
10323
|
+
return null;
|
|
10324
|
+
return { body_empty: schedule.body_empty, header_names: headerNames };
|
|
10325
|
+
}
|
|
10326
|
+
function safeHeaderNames(candidate) {
|
|
10327
|
+
if (!Array.isArray(candidate) || candidate.length > MAX_HEADER_COUNT)
|
|
10328
|
+
return null;
|
|
10329
|
+
const names = candidate.map((name) => typeof name === "string" ? name.toLowerCase() : "");
|
|
10330
|
+
const valid = candidate.every((name, index) => typeof name === "string" && HEADER_NAME_PATTERN.test(name) && !forbiddenHeaderName(names[index]));
|
|
10331
|
+
return valid && new Set(names).size === names.length ? names.sort() : null;
|
|
10332
|
+
}
|
|
10333
|
+
function safeSchedule(candidate) {
|
|
10334
|
+
const schedule = objectRecord2(candidate);
|
|
10335
|
+
if (!schedule || !validSafeSchedule(schedule))
|
|
10336
|
+
return null;
|
|
10337
|
+
const safePayload = safeSchedulePayload(schedule);
|
|
10338
|
+
if (!safePayload)
|
|
10339
|
+
return null;
|
|
10340
|
+
return {
|
|
10341
|
+
id: schedule.id,
|
|
10342
|
+
name: schedule.name,
|
|
10343
|
+
slug: schedule.slug,
|
|
10344
|
+
cron: schedule.cron,
|
|
10345
|
+
method: schedule.method,
|
|
10346
|
+
enabled: schedule.enabled,
|
|
10347
|
+
...safePayload,
|
|
10348
|
+
created_at: schedule.created_at,
|
|
10349
|
+
updated_at: schedule.updated_at
|
|
10350
|
+
};
|
|
10351
|
+
}
|
|
10352
|
+
function requiredText(args, name, action) {
|
|
10353
|
+
const candidate = args[name];
|
|
10354
|
+
if (typeof candidate !== "string" || !candidate.trim()) {
|
|
10355
|
+
throw new Error(`'${name}' is required for '${action}'`);
|
|
10356
|
+
}
|
|
10357
|
+
return candidate.trim();
|
|
10358
|
+
}
|
|
10359
|
+
function assertActionArguments(action, args) {
|
|
10360
|
+
const unsupported = Object.keys(args).filter((name) => !ACTION_ARGUMENTS[action].has(name));
|
|
10361
|
+
if (unsupported.length > 0) {
|
|
10362
|
+
throw new Error(`'${unsupported[0]}' is not supported for '${action}'`);
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
10365
|
+
function schedulePath(ref, scheduleId) {
|
|
10366
|
+
if (!PROJECT_REF_PATTERN.test(ref))
|
|
10367
|
+
throw new Error("'ref' is invalid for Scheduled Functions");
|
|
10368
|
+
if (scheduleId !== undefined && !SCHEDULE_ID_PATTERN.test(scheduleId)) {
|
|
10369
|
+
throw new Error("'schedule_id' is invalid");
|
|
10370
|
+
}
|
|
10371
|
+
const root = `/v1/projects/${encodeURIComponent(ref)}/scheduled-functions`;
|
|
10372
|
+
return scheduleId ? `${root}/${encodeURIComponent(scheduleId)}` : root;
|
|
10373
|
+
}
|
|
10374
|
+
function scheduleFailure(operation, response) {
|
|
10375
|
+
return releaseControlFailure(operation, "HTTP_ERROR", response.transportError ? null : response.status);
|
|
10376
|
+
}
|
|
10377
|
+
function listResponse(ref, response) {
|
|
10378
|
+
const operation = "scheduled_functions.list";
|
|
10379
|
+
if (!response.ok)
|
|
10380
|
+
return scheduleFailure(operation, response);
|
|
10381
|
+
const payload = objectRecord2(response.data);
|
|
10382
|
+
const rawSchedules = payload?.schedules;
|
|
10383
|
+
const schedules = Array.isArray(rawSchedules) ? rawSchedules.map(safeSchedule) : null;
|
|
10384
|
+
if (payload?.project_ref !== ref || !schedules || schedules.some((schedule) => !schedule)) {
|
|
10385
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
10386
|
+
}
|
|
10387
|
+
const ids = schedules.map((schedule) => schedule.id);
|
|
10388
|
+
if (new Set(ids).size !== ids.length)
|
|
10389
|
+
return releaseControlFailure(operation, "INVALID_RESPONSE", null);
|
|
10390
|
+
return releaseControlSuccess(operation, { project_ref: ref, schedules });
|
|
10391
|
+
}
|
|
10392
|
+
function mutationResponse(expectation, response) {
|
|
10393
|
+
const { action, ref, requestId, expectedFields } = expectation;
|
|
10394
|
+
const scheduleId = action === "update" ? expectation.scheduleId : undefined;
|
|
10395
|
+
const operation = `scheduled_functions.${action}`;
|
|
10396
|
+
if (!response.ok)
|
|
10397
|
+
return releaseControlMutationFailure(operation, response);
|
|
10398
|
+
const payload = objectRecord2(response.data);
|
|
10399
|
+
const schedule = safeSchedule(payload?.schedule);
|
|
10400
|
+
const confirmsRequest = schedule && Object.entries(expectedFields).every(([field, expected]) => isDeepStrictEqual(schedule[field], expected));
|
|
10401
|
+
if (payload?.project_ref !== ref || payload.request_id !== requestId || payload?.[action === "create" ? "created" : "updated"] !== true || !schedule || !confirmsRequest || scheduleId !== undefined && schedule.id !== scheduleId) {
|
|
10402
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10403
|
+
}
|
|
10404
|
+
return releaseControlSuccess(operation, { project_ref: ref, request_id: requestId, schedule });
|
|
10405
|
+
}
|
|
10406
|
+
function deleteResponse(ref, scheduleId, response) {
|
|
10407
|
+
const operation = "scheduled_functions.delete";
|
|
10408
|
+
if (!response.ok)
|
|
10409
|
+
return releaseControlMutationFailure(operation, response);
|
|
10410
|
+
const payload = objectRecord2(response.data);
|
|
10411
|
+
if (payload?.deleted !== true || payload.project_ref !== ref || payload.schedule_id !== scheduleId) {
|
|
10412
|
+
return releaseControlFailure(operation, "OUTCOME_UNKNOWN", response.status);
|
|
10413
|
+
}
|
|
10414
|
+
return releaseControlSuccess(operation, {
|
|
10415
|
+
project_ref: ref,
|
|
10416
|
+
schedule_id: scheduleId,
|
|
10417
|
+
deleted: true
|
|
10418
|
+
});
|
|
10419
|
+
}
|
|
10420
|
+
function readOnlyResult2() {
|
|
10421
|
+
return {
|
|
10422
|
+
isError: true,
|
|
10423
|
+
content: [{ type: "text", text: "⚠️ Scheduled Function write blocked in read-only mode." }]
|
|
10424
|
+
};
|
|
10425
|
+
}
|
|
10426
|
+
function createRequest(args, environment) {
|
|
10427
|
+
return {
|
|
10428
|
+
request_id: randomUUID(),
|
|
10429
|
+
name: requiredName(args, "create"),
|
|
10430
|
+
slug: requiredSlug(args, "create"),
|
|
10431
|
+
cron: requiredCron(args, "create"),
|
|
10432
|
+
method: requiredText(args, "method", "create"),
|
|
10433
|
+
body: scheduleBody(args.body_file) ?? {},
|
|
10434
|
+
headers: scheduleHeaders(args.header_env, environment) ?? {}
|
|
10435
|
+
};
|
|
10436
|
+
}
|
|
10437
|
+
function safeMutationFields(request) {
|
|
10438
|
+
const safeFields = Object.fromEntries(["name", "slug", "cron", "method", "enabled"].filter((field) => request[field] !== undefined).map((field) => [field, request[field]]));
|
|
10439
|
+
if (request.body !== undefined) {
|
|
10440
|
+
safeFields.body_empty = Object.keys(request.body).length === 0;
|
|
10441
|
+
}
|
|
10442
|
+
if (request.headers !== undefined) {
|
|
10443
|
+
safeFields.header_names = Object.keys(request.headers).sort();
|
|
10444
|
+
}
|
|
10445
|
+
return safeFields;
|
|
10446
|
+
}
|
|
10447
|
+
function requiredName(args, action) {
|
|
10448
|
+
const name = requiredText(args, "name", action);
|
|
10449
|
+
if (name.length > MAX_SCHEDULE_NAME_LENGTH)
|
|
10450
|
+
throw new Error(`'name' is too long for '${action}'`);
|
|
10451
|
+
return name;
|
|
10452
|
+
}
|
|
10453
|
+
function requiredSlug(args, action) {
|
|
10454
|
+
const slug = requiredText(args, "slug", action);
|
|
10455
|
+
if (!SAFE_SLUG_PATTERN.test(slug))
|
|
10456
|
+
throw new Error(`'slug' is invalid for '${action}'`);
|
|
10457
|
+
return slug;
|
|
10458
|
+
}
|
|
10459
|
+
function requiredCron(args, action) {
|
|
10460
|
+
const cron = requiredText(args, "cron", action);
|
|
10461
|
+
if (!validScheduledFunctionCron(cron))
|
|
10462
|
+
throw new Error(`'cron' is invalid for '${action}'`);
|
|
10463
|
+
return cron;
|
|
10464
|
+
}
|
|
10465
|
+
function updateRequest(args, environment) {
|
|
10466
|
+
const body = scheduleBody(args.body_file);
|
|
10467
|
+
const headers = scheduleHeaders(args.header_env, environment);
|
|
10468
|
+
const cron = args.cron === undefined ? undefined : requiredCron(args, "update");
|
|
10469
|
+
const name = args.name === undefined ? undefined : requiredName(args, "update");
|
|
10470
|
+
const mutationFields = Object.fromEntries([
|
|
10471
|
+
["name", name],
|
|
10472
|
+
["cron", cron],
|
|
10473
|
+
["method", args.method],
|
|
10474
|
+
["enabled", args.enabled],
|
|
10475
|
+
["body", body],
|
|
10476
|
+
["headers", headers]
|
|
10477
|
+
].filter((entry) => entry[1] !== undefined));
|
|
10478
|
+
if (Object.keys(mutationFields).length === 0) {
|
|
10479
|
+
throw new Error("Scheduled Function update requires at least one field");
|
|
10480
|
+
}
|
|
10481
|
+
return { request_id: randomUUID(), ...mutationFields };
|
|
10482
|
+
}
|
|
10483
|
+
async function executeScheduleAction(http, environment, args, readOnly = false) {
|
|
10484
|
+
const action = args.action;
|
|
10485
|
+
if (readOnly && action !== "list")
|
|
10486
|
+
return readOnlyResult2();
|
|
10487
|
+
assertActionArguments(action, args);
|
|
10488
|
+
const ref = requiredText(args, "ref", action);
|
|
10489
|
+
if (action === "list")
|
|
10490
|
+
return listResponse(ref, await http.get(schedulePath(ref)));
|
|
10491
|
+
if (action === "create") {
|
|
10492
|
+
const request = createRequest(args, environment);
|
|
10493
|
+
const requestId = request.request_id;
|
|
10494
|
+
return mutationResponse({ action, ref, requestId, expectedFields: safeMutationFields(request) }, await http.post(schedulePath(ref), request));
|
|
10495
|
+
}
|
|
10496
|
+
const scheduleId = requiredText(args, "schedule_id", action);
|
|
10497
|
+
if (action === "update") {
|
|
10498
|
+
const request = updateRequest(args, environment);
|
|
10499
|
+
const requestId = request.request_id;
|
|
10500
|
+
return mutationResponse({
|
|
10501
|
+
action,
|
|
10502
|
+
ref,
|
|
10503
|
+
scheduleId,
|
|
10504
|
+
requestId,
|
|
10505
|
+
expectedFields: safeMutationFields(request)
|
|
10506
|
+
}, await http.patch(schedulePath(ref, scheduleId), request));
|
|
10507
|
+
}
|
|
10508
|
+
return deleteResponse(ref, scheduleId, await http.delete(schedulePath(ref, scheduleId)));
|
|
10509
|
+
}
|
|
10510
|
+
function registerScheduledFunctionTools(server, http, environment = process.env, options = {}) {
|
|
10511
|
+
server.tool("scheduled_functions", SCHEDULE_TOOL_DESCRIPTION, SCHEDULE_TOOL_SCHEMA, (args) => executeScheduleAction(http, environment, args, options.readOnly));
|
|
10512
|
+
}
|
|
10513
|
+
var SCHEDULE_TOOL_DESCRIPTION = "Scheduled Edge Function lifecycle. Actions: list, create, update, delete";
|
|
10514
|
+
var SCHEDULE_TOOL_SCHEMA = {
|
|
10515
|
+
action: withDescription(stringEnum(["list", "create", "update", "delete"]), "Action"),
|
|
10516
|
+
ref: withDescription(Type.String(), "Project ref"),
|
|
10517
|
+
schedule_id: optional(Type.String(), "[update/delete] Schedule ID"),
|
|
10518
|
+
name: optional(Type.String(), "[create/update] Display name"),
|
|
10519
|
+
slug: optional(Type.String(), "[create] Edge Function slug"),
|
|
10520
|
+
cron: optional(Type.String(), "[create/update] Five-field cron expression"),
|
|
10521
|
+
method: optional(stringEnum(["GET", "POST"]), "[create/update] HTTP method"),
|
|
10522
|
+
enabled: optional(Type.Boolean(), "[update] Enabled state"),
|
|
10523
|
+
body_file: optional(Type.String(), "[create/update] Local JSON object file; content is never printed"),
|
|
10524
|
+
header_env: withDescription(headerEnvironmentSchema, "[create/update] JSON map of HTTP header names to environment variable names")
|
|
10525
|
+
};
|
|
10526
|
+
// package.json
|
|
10527
|
+
var package_default = {
|
|
10528
|
+
name: "@supacloud/cli",
|
|
10529
|
+
version: "0.15.0",
|
|
10530
|
+
description: "Project-scoped CLI for SupaCloud users",
|
|
10531
|
+
type: "module",
|
|
10532
|
+
main: "./dist/index.js",
|
|
10533
|
+
bin: {
|
|
10534
|
+
"supacloud-cli": "dist/index.js"
|
|
10535
|
+
},
|
|
10536
|
+
files: [
|
|
10537
|
+
"dist",
|
|
10538
|
+
"README.md",
|
|
10539
|
+
"skills"
|
|
10540
|
+
],
|
|
10541
|
+
scripts: {
|
|
10542
|
+
dev: "bun run --watch src/index.ts",
|
|
10543
|
+
build: "bun build src/index.ts --outdir dist --target node",
|
|
10544
|
+
prepublishOnly: "bun run build",
|
|
10545
|
+
typecheck: "tsc --noEmit"
|
|
10546
|
+
},
|
|
10547
|
+
keywords: [
|
|
10548
|
+
"supacloud",
|
|
10549
|
+
"cli",
|
|
10550
|
+
"deploy",
|
|
10551
|
+
"supabase"
|
|
10552
|
+
],
|
|
10553
|
+
license: "MIT",
|
|
10554
|
+
repository: {
|
|
10555
|
+
type: "git",
|
|
10556
|
+
url: "https://github.com/zuohuadong/supacloud.git",
|
|
10557
|
+
directory: "packages/cli"
|
|
10558
|
+
},
|
|
10559
|
+
dependencies: {
|
|
10560
|
+
"@sinclair/typebox": "^0.34.52"
|
|
10561
|
+
},
|
|
10562
|
+
devDependencies: {
|
|
10563
|
+
"@types/bun": "^1.3.14",
|
|
10564
|
+
typescript: "^7.0.2"
|
|
10565
|
+
}
|
|
10566
|
+
};
|
|
10567
|
+
|
|
9605
10568
|
// src/index.ts
|
|
9606
10569
|
var commandName = "supacloud-cli";
|
|
9607
10570
|
var preferredCommand = commandName;
|
|
@@ -9679,9 +10642,12 @@ async function createProjectStatusResult(context) {
|
|
|
9679
10642
|
const checks = await collectProjectStatusChecks(context);
|
|
9680
10643
|
const statusPayload = {
|
|
9681
10644
|
mode: "project",
|
|
9682
|
-
|
|
10645
|
+
environment: context.environment || null,
|
|
10646
|
+
source: { kind: context.source, path: context.sourcePath },
|
|
9683
10647
|
projectRef: context.projectRef || null,
|
|
9684
10648
|
apiUrl: context.apiUrl || null,
|
|
10649
|
+
readOnly: context.readOnly,
|
|
10650
|
+
production: context.production,
|
|
9685
10651
|
autoLinked: Boolean(context.inferredSupabaseUrl && context.inferredServiceRoleKey),
|
|
9686
10652
|
hasApiToken: Boolean(context.apiToken),
|
|
9687
10653
|
checks
|
|
@@ -9714,7 +10680,7 @@ function captureTools(register) {
|
|
|
9714
10680
|
register(server);
|
|
9715
10681
|
return tools;
|
|
9716
10682
|
}
|
|
9717
|
-
function printHelp(context
|
|
10683
|
+
function printHelp(context) {
|
|
9718
10684
|
const autoLink = context.inferredSupabaseUrl ? `Project context: ${context.inferredSupabaseUrl} (${context.source})` : "Project context: not detected";
|
|
9719
10685
|
console.error(`
|
|
9720
10686
|
╔═══════════════════════════════════════════════════════════╗
|
|
@@ -9724,18 +10690,31 @@ function printHelp(context = resolveSupaCloudContext()) {
|
|
|
9724
10690
|
|
|
9725
10691
|
USAGE
|
|
9726
10692
|
|
|
9727
|
-
${preferredCommand} <module> <action> [--flags]
|
|
9728
|
-
${preferredCommand} status
|
|
10693
|
+
${preferredCommand} [global flags] <module> <action> [--flags]
|
|
10694
|
+
${preferredCommand} [global flags] status
|
|
9729
10695
|
${preferredCommand} --help
|
|
10696
|
+
${preferredCommand} --version
|
|
10697
|
+
|
|
10698
|
+
GLOBAL FLAGS
|
|
10699
|
+
|
|
10700
|
+
--env <name> Load .env.supacloud.<name> from the current directory.
|
|
10701
|
+
--env-file <path> Load an exact file that declares SUPACLOUD_ENV.
|
|
10702
|
+
--confirm-production <ref> Confirm a write to the selected production project.
|
|
10703
|
+
|
|
10704
|
+
Global flags may appear before or after the command. --env and --env-file are
|
|
10705
|
+
mutually exclusive, and a selected source is never mixed with another source.
|
|
9730
10706
|
|
|
9731
10707
|
DEFAULT CONTEXT
|
|
9732
10708
|
|
|
9733
|
-
|
|
10709
|
+
Without a selector or project variables, runs use the current project's legacy .env.
|
|
9734
10710
|
Supported auto-link variables:
|
|
9735
10711
|
SUPABASE_URL / SUPACLOUD_API_URL
|
|
9736
10712
|
SUPABASE_SERVICE_ROLE_KEY / SUPACLOUD_API_TOKEN
|
|
9737
10713
|
SUPACLOUD_PROJECT_REF (when it cannot be inferred from <ref>.api.*)
|
|
9738
10714
|
|
|
10715
|
+
SUPACLOUD_READ_ONLY=true blocks remote writes. Production writes require an
|
|
10716
|
+
exact --confirm-production value, and cannot override the selected project ref.
|
|
10717
|
+
|
|
9739
10718
|
status checks configuration, Management API connectivity, and authentication.
|
|
9740
10719
|
It exits non-zero when a required check fails.
|
|
9741
10720
|
|
|
@@ -9763,7 +10742,10 @@ EXAMPLES
|
|
|
9763
10742
|
${preferredCommand} ai show_skill
|
|
9764
10743
|
${preferredCommand} ai install_skill --dry_run
|
|
9765
10744
|
${preferredCommand} edge_functions deploy --ref abc123 --slug hello --path ./supabase/functions/hello
|
|
10745
|
+
${preferredCommand} edge_functions activate --ref abc123 --slug hello --version 3
|
|
10746
|
+
${preferredCommand} scheduled_functions list --ref abc123
|
|
9766
10747
|
${preferredCommand} edge_functions config --ref abc123 --slug hello --verify_jwt false --background_routes "/queue/*,/render/*"
|
|
10748
|
+
${preferredCommand} secrets upsert --ref abc123 --from-env API_KEY,WEBHOOK_SECRET
|
|
9767
10749
|
${preferredCommand} gateway routes --ref abc123
|
|
9768
10750
|
${preferredCommand} gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
|
|
9769
10751
|
${preferredCommand} gateway config --ref abc123 --rate_limit_tier pro
|
|
@@ -9777,8 +10759,18 @@ SEPARATE ADMIN CLI
|
|
|
9777
10759
|
npx @supacloud/admin --help
|
|
9778
10760
|
`);
|
|
9779
10761
|
}
|
|
9780
|
-
function
|
|
9781
|
-
|
|
10762
|
+
function authorizedToolMap(tools, context, confirmProduction) {
|
|
10763
|
+
validateExecutionPolicyCoverage(tools);
|
|
10764
|
+
for (const [moduleName, tool] of Object.entries(tools)) {
|
|
10765
|
+
const callback = tool.callback;
|
|
10766
|
+
tool.callback = async (args) => {
|
|
10767
|
+
authorizeExecution(moduleName, args, { context, confirmProduction });
|
|
10768
|
+
return callback(args);
|
|
10769
|
+
};
|
|
10770
|
+
}
|
|
10771
|
+
return tools;
|
|
10772
|
+
}
|
|
10773
|
+
function createCliTools(context, confirmProduction) {
|
|
9782
10774
|
let pushMigrations;
|
|
9783
10775
|
const tools = {
|
|
9784
10776
|
status: {
|
|
@@ -9804,6 +10796,8 @@ function createCliTools() {
|
|
|
9804
10796
|
"⚠️ Project commands need a project-scoped API context.",
|
|
9805
10797
|
"",
|
|
9806
10798
|
"Provide one of these sources:",
|
|
10799
|
+
" - --env <name> for .env.supacloud.<name>",
|
|
10800
|
+
" - --env-file <path> for a file declaring SUPACLOUD_ENV",
|
|
9807
10801
|
" - .env with SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY",
|
|
9808
10802
|
" - SUPACLOUD_API_URL + SUPACLOUD_API_TOKEN",
|
|
9809
10803
|
"",
|
|
@@ -9816,7 +10810,7 @@ function createCliTools() {
|
|
|
9816
10810
|
]
|
|
9817
10811
|
})
|
|
9818
10812
|
};
|
|
9819
|
-
for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "diagnostics", "gateway", "branch"]) {
|
|
10813
|
+
for (const name of ["database", "auth", "storage", "edge_functions", "secrets", "frontend", "queue", "task_events", "scheduled_functions", "diagnostics", "gateway", "branch"]) {
|
|
9820
10814
|
tools[name] = {
|
|
9821
10815
|
schema: { action: genericActionSchema },
|
|
9822
10816
|
callback: async () => ({
|
|
@@ -9853,11 +10847,14 @@ function createCliTools() {
|
|
|
9853
10847
|
`${preferredCommand} expects project-scoped credentials by default.`,
|
|
9854
10848
|
"Provide one of these sources:",
|
|
9855
10849
|
"",
|
|
9856
|
-
" 1.
|
|
10850
|
+
" 1. Named environment file",
|
|
10851
|
+
" supacloud-cli --env test status",
|
|
10852
|
+
"",
|
|
10853
|
+
" 2. Current workspace .env",
|
|
9857
10854
|
" SUPABASE_URL=https://your-project.example.com",
|
|
9858
10855
|
" SUPABASE_SERVICE_ROLE_KEY=...",
|
|
9859
10856
|
"",
|
|
9860
|
-
"
|
|
10857
|
+
" 3. Explicit environment variables",
|
|
9861
10858
|
" SUPACLOUD_API_URL=https://your-project.example.com",
|
|
9862
10859
|
" SUPACLOUD_API_TOKEN=...",
|
|
9863
10860
|
"",
|
|
@@ -9869,7 +10866,7 @@ function createCliTools() {
|
|
|
9869
10866
|
]
|
|
9870
10867
|
})
|
|
9871
10868
|
};
|
|
9872
|
-
return tools;
|
|
10869
|
+
return authorizedToolMap(tools, context, confirmProduction);
|
|
9873
10870
|
}
|
|
9874
10871
|
const http = new HttpTransport({
|
|
9875
10872
|
baseUrl: context.apiUrl,
|
|
@@ -9887,7 +10884,12 @@ function createCliTools() {
|
|
|
9887
10884
|
assign(databaseTools);
|
|
9888
10885
|
assign(captureTools((server) => registerAuthTools(server, http)));
|
|
9889
10886
|
assign(captureTools((server) => registerStorageTools(server, http)));
|
|
9890
|
-
assign(captureTools((server) => registerAdvancedTools(server, http
|
|
10887
|
+
assign(captureTools((server) => registerAdvancedTools(server, http, process.env, {
|
|
10888
|
+
readOnly: context.readOnly
|
|
10889
|
+
})));
|
|
10890
|
+
assign(captureTools((server) => registerScheduledFunctionTools(server, http, process.env, {
|
|
10891
|
+
readOnly: context.readOnly
|
|
10892
|
+
})));
|
|
9891
10893
|
assign(captureTools((server) => registerFrontendTools(server, http)));
|
|
9892
10894
|
assign(captureTools((server) => registerGatewayTools(server, http, {
|
|
9893
10895
|
projectRef: context.projectRef || undefined
|
|
@@ -9900,16 +10902,26 @@ function createCliTools() {
|
|
|
9900
10902
|
projectRef: context.projectRef || undefined
|
|
9901
10903
|
})));
|
|
9902
10904
|
delete tools.platform;
|
|
9903
|
-
return tools;
|
|
10905
|
+
return authorizedToolMap(tools, context, confirmProduction);
|
|
9904
10906
|
}
|
|
9905
10907
|
async function main() {
|
|
9906
|
-
const
|
|
10908
|
+
const rawArgs = process.argv.slice(2);
|
|
10909
|
+
if (rawArgs.length === 1 && rawArgs[0] === "--version") {
|
|
10910
|
+
console.log(package_default.version);
|
|
10911
|
+
return;
|
|
10912
|
+
}
|
|
10913
|
+
const globalOptions = parseGlobalOptions(rawArgs);
|
|
10914
|
+
const args = globalOptions.args;
|
|
10915
|
+
const context = resolveSupaCloudContext(process.env, process.cwd(), {
|
|
10916
|
+
environmentName: globalOptions.environmentName,
|
|
10917
|
+
envFile: globalOptions.envFile
|
|
10918
|
+
});
|
|
9907
10919
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
9908
|
-
printHelp();
|
|
10920
|
+
printHelp(context);
|
|
9909
10921
|
process.exitCode = 0;
|
|
9910
10922
|
return;
|
|
9911
10923
|
}
|
|
9912
|
-
const cliTools = createCliTools();
|
|
10924
|
+
const cliTools = createCliTools(context, globalOptions.confirmProduction);
|
|
9913
10925
|
if (args.length === 1 && !["ai", "supabase"].includes(args[0]) && cliTools[args[0]]) {
|
|
9914
10926
|
const result = await cliTools[args[0]].callback({});
|
|
9915
10927
|
if (result?.content && Array.isArray(result.content)) {
|