@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.
Files changed (3) hide show
  1. package/README.md +164 -5
  2. package/dist/index.js +1606 -174
  3. 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
- import { existsSync, readFileSync } from "node:fs";
6219
- function readDotEnvFile(cwd) {
6220
- const envPath = resolve(cwd, ".env");
6221
- if (!existsSync(envPath))
6222
- return {};
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
- try {
6225
- const envContent = readFileSync(envPath, "utf-8");
6226
- for (const line of envContent.split(`
6227
- `)) {
6228
- const match = line.trim().match(/^([^=]+)=(.*)$/);
6229
- if (!match)
6230
- continue;
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
- } catch {
6236
- return {};
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 pickValue(env, dotenv, keys) {
6241
- for (const key of keys) {
6242
- const envValue = env[key];
6243
- if (envValue)
6244
- return { value: envValue, source: "env" };
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
- for (const key of keys) {
6247
- const dotenvValue = dotenv[key];
6248
- if (dotenvValue)
6249
- return { value: dotenvValue, source: "dotenv" };
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
- const match = new URL(normalized).hostname.match(/^([a-z0-9-]+)\.api\./i);
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 managedHostMatch = host.match(/^([a-z0-9-]+)\.api\.(.+)$/i);
6301
- if (managedHostMatch) {
6302
- url.hostname = `studio-${managedHostMatch[1]}.${managedHostMatch[2]}`;
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 resolveSupaCloudContext(env = process.env, cwd = process.cwd()) {
6308
- const dotenv = readDotEnvFile(cwd);
6309
- const supabaseUrl = pickValue(env, dotenv, ["SUPABASE_URL"]);
6310
- const explicitApiUrl = pickValue(env, dotenv, ["SUPACLOUD_API_URL", "SUPACLOUD_MANAGEMENT_API_URL", "MANAGEMENT_API_URL"]);
6311
- const inferredToken = pickValue(env, dotenv, ["SUPABASE_SERVICE_ROLE_KEY", "SUPACLOUD_API_TOKEN"]);
6312
- const explicitProjectRef = pickValue(env, dotenv, ["SUPACLOUD_PROJECT_REF", "X_PROJECT_REF"]).value;
6313
- const hostFromEnv = env.SUPACLOUD_HOST;
6314
- const normalizedSupabaseUrl = normalizeUrl(supabaseUrl.value);
6315
- const projectRef = explicitProjectRef || inferProjectRefFromSupabaseUrl(normalizedSupabaseUrl);
6316
- const apiUrl = normalizeUrl(explicitApiUrl.value) || inferManagementApiUrlFromSupabaseUrl(normalizedSupabaseUrl, projectRef) || (hostFromEnv ? `http://${hostFromEnv}:9090` : "");
6317
- const resolvedHostFromUrl = hostFromUrl(apiUrl || normalizedSupabaseUrl);
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
- host: hostFromEnv ?? resolvedHostFromUrl,
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: env.SUPACLOUD_API_TOKEN ?? inferredToken.value,
6326
- projectRef,
6327
- readOnly: env.SUPACLOUD_READ_ONLY === "true",
6328
- inferredSupabaseUrl: normalizedSupabaseUrl,
6329
- inferredServiceRoleKey: inferredToken.value,
6330
- source: detectSource([
6331
- supabaseUrl.value || explicitApiUrl.value ? supabaseUrl.value ? supabaseUrl.source : explicitApiUrl.source : "none",
6332
- inferredToken.value ? inferredToken.source : "none"
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 { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
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 { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
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 { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
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 { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
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 { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
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 { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
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 = projectRef || args.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(Type.Record(Type.String(), Type.Unknown()), "[update_settings/update_config] Config fields")
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
- text = (await http.patch(`/v1/projects/${ref}/auth/config`, config)).ok ? `✅ Auth settings updated` : `❌ Failed`;
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
- text = (await http.patch(`/v1/projects/${ref}/config/auth`, config)).ok ? `✅ Auth config updated` : `❌ Failed`;
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(["status", "list_buckets", "list_files", "upload_base64", "delete_file"]), "Action"),
7382
- ref: optional(Type.String(), "Project ref (required except for 'status')"),
7383
- bucket: optional(Type.String(), "[list_files/upload/delete] Bucket name"),
7384
- filename: optional(Type.String(), "[upload/delete] File name/path"),
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 { action, ref, bucket, filename, base64_content, mime_type } = args;
7389
- const need = (f, v) => {
7390
- if (!v)
7391
- throw new Error(`'${f}' required for '${action}'`);
7392
- };
7393
- const fmt = (data, label, fmtFn) => {
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
- need("ref", ref);
7420
- need("bucket", bucket);
7421
- const res = await http.get(`/v1/storage/${ref}/buckets/${bucket}/files`);
7422
- if (!res.ok) {
7423
- text = `❌ Failed (${res.status})`;
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 = res.data;
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((f) => ` - ${f.name} (${f.size ? (f.size / 1024).toFixed(1) + "KB" : "?"})`).join(`
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
- need("ref", ref);
7438
- need("bucket", bucket);
7439
- need("filename", filename);
7440
- need("base64_content", base64_content);
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(base64_content, "base64");
7443
- const blob = new Blob([buffer], { type: mime_type || "application/octet-stream" });
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 res = await http.postMultipart(`/v1/storage/${ref}/buckets/${bucket}/upload`, formData);
7447
- text = res.ok ? `✅ File ${filename} uploaded to ${bucket}` : `❌ Upload failed (${res.status})`;
7448
- } catch (e) {
7449
- text = `❌ Error: ${e.message}`;
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
- need("ref", ref);
7455
- need("bucket", bucket);
7456
- need("filename", filename);
7457
- text = (await http.delete(`/v1/storage/${ref}/buckets/${bucket}/files/${filename}`)).ok ? `✅ File ${filename} deleted` : `❌ Failed`;
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
- text = `❌ Unknown action: ${action}`;
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 registerAdvancedTools(server, http) {
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
- text = JSON.stringify((await http.get(`/v1/projects/${ref}/secrets`)).data, null, 2);
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
- if (!secrets?.length)
7750
- throw new Error("'secrets' array required");
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 = defaultRef || refFromArgs;
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 = defaultRef || refFromArgs;
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 = projectRef || override;
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
- source: context.source,
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 = resolveSupaCloudContext()) {
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
- Unauthenticated runs default to the current project's .env.
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 createCliTools() {
9781
- const context = resolveSupaCloudContext();
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. Current workspace .env",
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
- " 2. Explicit environment variables",
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 args = process.argv.slice(2);
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)) {