@supacloud/admin 0.8.2 → 0.10.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 +129 -1
  2. package/dist/index.js +1495 -206
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18847,6 +18847,11 @@ var require_lib3 = __commonJS((exports, module) => {
18847
18847
  };
18848
18848
  });
18849
18849
 
18850
+ // src/index.ts
18851
+ import { resolve as resolve3 } from "node:path";
18852
+ import { fileURLToPath } from "node:url";
18853
+ import { realpathSync } from "node:fs";
18854
+
18850
18855
  // node_modules/@sinclair/typebox/build/esm/type/guard/value.mjs
18851
18856
  var exports_value = {};
18852
18857
  __export(exports_value, {
@@ -24900,6 +24905,9 @@ function decodedSchema(inputSchema, outputSchema, decode2, options) {
24900
24905
  return options ? { ...transform2, ...options } : transform2;
24901
24906
  }
24902
24907
  function schemaEnumValues(schema) {
24908
+ if (typeof schema.const === "string" || typeof schema.const === "number") {
24909
+ return [String(schema.const)];
24910
+ }
24903
24911
  if (!Array.isArray(schema.anyOf))
24904
24912
  return [];
24905
24913
  return schema.anyOf.flatMap((branch) => {
@@ -24919,11 +24927,6 @@ function schemaProperties(schema) {
24919
24927
  return schema;
24920
24928
  }
24921
24929
 
24922
- // src/index.ts
24923
- import { resolve as resolve2 } from "node:path";
24924
- import { fileURLToPath } from "node:url";
24925
- import { realpathSync } from "node:fs";
24926
-
24927
24930
  // src/shared/transports/ssh.ts
24928
24931
  var import_ssh2 = __toESM(require_lib3(), 1);
24929
24932
  import { randomUUID, timingSafeEqual } from "node:crypto";
@@ -25447,70 +25450,174 @@ async function runCli(cliTools, args, options = {}) {
25447
25450
  }
25448
25451
 
25449
25452
  // src/shared/context.ts
25453
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
25450
25454
  import { homedir } from "node:os";
25451
25455
  import { resolve } from "node:path";
25452
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
25453
- function readDotEnvFile(cwd) {
25454
- const envPath = resolve(cwd, ".env");
25455
- if (!existsSync(envPath))
25456
- return {};
25457
- const values = {};
25458
- try {
25459
- const envContent = readFileSync2(envPath, "utf-8");
25460
- for (const line of envContent.split(`
25461
- `)) {
25462
- const match = line.trim().match(/^([^=]+)=(.*)$/);
25463
- if (!match)
25464
- continue;
25465
- const key = match[1].trim();
25466
- const value = match[2].trim().replace(/^["']|["']$/g, "");
25467
- values[key] = value;
25468
- }
25469
- } catch {
25470
- return {};
25456
+
25457
+ // src/shared/global-options.ts
25458
+ var GLOBAL_FLAGS = {
25459
+ "--env": "environmentName",
25460
+ "--env-file": "envFile",
25461
+ "--confirm-production": "confirmProduction"
25462
+ };
25463
+ var ENVIRONMENT_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
25464
+ function globalFlag(argument) {
25465
+ return Object.keys(GLOBAL_FLAGS).find((flag) => argument === flag || argument.startsWith(`${flag}=`)) ?? null;
25466
+ }
25467
+ function globalFlagValue(args, index, flag) {
25468
+ if (args[index].startsWith(`${flag}=`)) {
25469
+ const inlineValue = args[index].slice(flag.length + 1);
25470
+ if (!inlineValue)
25471
+ throw new Error(`${flag} requires a value`);
25472
+ return { flagValue: inlineValue, consumed: 1 };
25473
+ }
25474
+ const followingValue = args[index + 1];
25475
+ if (!followingValue || followingValue.startsWith("--")) {
25476
+ throw new Error(`${flag} requires a value`);
25477
+ }
25478
+ return { flagValue: followingValue, consumed: 2 };
25479
+ }
25480
+ function normalizeEnvironmentName(name) {
25481
+ if (!ENVIRONMENT_NAME_PATTERN.test(name)) {
25482
+ throw new Error("--env and SUPACLOUD_ENV must match ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$");
25483
+ }
25484
+ const normalized = name.toLowerCase();
25485
+ return normalized === "prod" || normalized === "production" ? "production" : normalized;
25486
+ }
25487
+ function parseGlobalArgument(args, index, selectedOptions) {
25488
+ const flag = globalFlag(args[index]);
25489
+ if (!flag)
25490
+ return { commandArgument: args[index], consumed: 1 };
25491
+ const optionKey = GLOBAL_FLAGS[flag];
25492
+ if (selectedOptions[optionKey] !== undefined) {
25493
+ throw new Error(`${flag} may be provided only once`);
25494
+ }
25495
+ const parsedFlag = globalFlagValue(args, index, flag);
25496
+ return { optionKey, optionValue: parsedFlag.flagValue, consumed: parsedFlag.consumed };
25497
+ }
25498
+ function validateEnvironmentSelection(selectedOptions) {
25499
+ if (selectedOptions.environmentName && selectedOptions.envFile) {
25500
+ throw new Error("--env and --env-file are mutually exclusive");
25501
+ }
25502
+ if (selectedOptions.environmentName)
25503
+ normalizeEnvironmentName(selectedOptions.environmentName);
25504
+ }
25505
+ function parseGlobalAdminOptions(args) {
25506
+ const selectedOptions = {};
25507
+ const commandArgs = [];
25508
+ for (let index = 0;index < args.length; ) {
25509
+ const parsedArgument = parseGlobalArgument(args, index, selectedOptions);
25510
+ if (parsedArgument.commandArgument !== undefined) {
25511
+ commandArgs.push(parsedArgument.commandArgument);
25512
+ } else if (parsedArgument.optionKey && parsedArgument.optionValue !== undefined) {
25513
+ selectedOptions[parsedArgument.optionKey] = parsedArgument.optionValue;
25514
+ }
25515
+ index += parsedArgument.consumed;
25516
+ }
25517
+ validateEnvironmentSelection(selectedOptions);
25518
+ return { ...selectedOptions, args: commandArgs };
25519
+ }
25520
+
25521
+ // src/shared/context.ts
25522
+ var ADMIN_CONTEXT_KEYS = [
25523
+ "SUPABASE_URL",
25524
+ "SUPABASE_SERVICE_ROLE_KEY",
25525
+ "SUPACLOUD_API_URL",
25526
+ "SUPACLOUD_MANAGEMENT_API_URL",
25527
+ "MANAGEMENT_API_URL",
25528
+ "SUPACLOUD_API_TOKEN",
25529
+ "SUPACLOUD_PROJECT_REF",
25530
+ "X_PROJECT_REF",
25531
+ "SUPACLOUD_HOST",
25532
+ "SUPACLOUD_SSH_USER",
25533
+ "SUPACLOUD_SSH_PORT",
25534
+ "SUPACLOUD_SSH_KEY",
25535
+ "SUPACLOUD_SSH_PASS",
25536
+ "SUPACLOUD_SSH_HOST_FINGERPRINT",
25537
+ "SUPACLOUD_READ_ONLY"
25538
+ ];
25539
+ var SOURCE_ENVIRONMENT_KEYS = [...ADMIN_CONTEXT_KEYS, "SUPACLOUD_ENV"];
25540
+ function unquotedEnvValue(rawValue) {
25541
+ const trimmedValue = rawValue.trim();
25542
+ const quote = trimmedValue[0];
25543
+ return quote && (quote === '"' || quote === "'") && trimmedValue.endsWith(quote) ? trimmedValue.slice(1, -1) : trimmedValue;
25544
+ }
25545
+ function parseEnvFile(contents) {
25546
+ const environment = {};
25547
+ for (const rawLine of contents.split(/\r?\n/)) {
25548
+ const line = rawLine.trim();
25549
+ if (!line || line.startsWith("#"))
25550
+ continue;
25551
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
25552
+ if (match)
25553
+ environment[match[1]] = unquotedEnvValue(match[2]);
25471
25554
  }
25472
- return values;
25555
+ return environment;
25473
25556
  }
25474
- function pickValue(env, dotenv, keys) {
25475
- for (const key of keys) {
25476
- const envValue = env[key];
25477
- if (envValue)
25478
- return { value: envValue, source: "env" };
25557
+ function readEnvFile(path, required) {
25558
+ if (!existsSync(path)) {
25559
+ if (required)
25560
+ throw new Error(`SupaCloud environment file not found: ${path}`);
25561
+ return {};
25479
25562
  }
25480
- for (const key of keys) {
25481
- const dotenvValue = dotenv[key];
25482
- if (dotenvValue)
25483
- return { value: dotenvValue, source: "dotenv" };
25563
+ try {
25564
+ return parseEnvFile(readFileSync2(path, "utf8"));
25565
+ } catch (error) {
25566
+ const message = error instanceof Error ? error.message : String(error);
25567
+ throw new Error(`Failed to read SupaCloud environment file ${path}: ${message}`);
25484
25568
  }
25485
- return { value: "", source: "env" };
25486
- }
25487
- function detectSource(sources) {
25488
- const present = new Set(sources.filter((value) => value !== "none"));
25489
- if (present.size === 0)
25490
- return "none";
25491
- if (present.size === 1)
25492
- return present.has("env") ? "env" : "dotenv";
25493
- return "mixed";
25494
25569
  }
25495
- function normalizeUrl(value) {
25496
- const trimmed = value.trim().replace(/\/+$/, "");
25570
+ function normalizeUrl(urlCandidate) {
25571
+ const trimmed = urlCandidate.trim();
25497
25572
  if (!trimmed)
25498
25573
  return "";
25499
25574
  try {
25500
- return new URL(trimmed).toString().replace(/\/+$/, "");
25575
+ const url = new URL(trimmed);
25576
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
25577
+ return "";
25578
+ return url.toString().replace(/\/+$/, "");
25501
25579
  } catch {
25502
25580
  return "";
25503
25581
  }
25504
25582
  }
25505
- function hostFromUrl(value) {
25583
+ function managementApiUrlCandidate(environment) {
25584
+ return environment.SUPACLOUD_API_URL || environment.SUPACLOUD_MANAGEMENT_API_URL || environment.MANAGEMENT_API_URL || "";
25585
+ }
25586
+ function assertAdminCredentialScope(environment, source) {
25587
+ const hasProjectApplicationCredentials = Boolean(environment.SUPABASE_URL?.trim() || environment.SUPABASE_SERVICE_ROLE_KEY?.trim());
25588
+ const hasManagementApiUrl = Boolean(normalizeUrl(managementApiUrlCandidate(environment)));
25589
+ const hasManagementApiToken = Boolean(environment.SUPACLOUD_API_TOKEN?.trim());
25590
+ if (hasProjectApplicationCredentials && (!hasManagementApiUrl || !hasManagementApiToken)) {
25591
+ throw new Error(`Project application credentials in ${source} cannot be used as a SupaCloud Admin profile; ` + "SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN are required");
25592
+ }
25593
+ }
25594
+ function assertAdminFileContext(environment, path) {
25595
+ assertAdminCredentialScope(environment, path);
25596
+ const hasManagementApiUrl = Boolean(normalizeUrl(managementApiUrlCandidate(environment)));
25597
+ const hasManagementApiToken = Boolean(environment.SUPACLOUD_API_TOKEN?.trim());
25598
+ if (hasManagementApiUrl !== hasManagementApiToken) {
25599
+ throw new Error(`SupaCloud Admin API context in ${path} requires both SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN`);
25600
+ }
25601
+ }
25602
+ function processContextSource(environment, environmentName) {
25603
+ assertAdminCredentialScope(environment, "process environment");
25604
+ return { environment, kind: "process_env", path: null, environmentName };
25605
+ }
25606
+ function hostFromUrl(urlCandidate) {
25506
25607
  try {
25507
- return new URL(value).hostname;
25608
+ return new URL(urlCandidate).hostname;
25508
25609
  } catch {
25509
25610
  return "";
25510
25611
  }
25511
25612
  }
25512
- function inferManagementApiUrlFromSupabaseUrl(value, projectRef = "") {
25513
- const normalized = normalizeUrl(value);
25613
+ function inferProjectRefFromSupabaseUrl(supabaseUrlCandidate) {
25614
+ const normalized = normalizeUrl(supabaseUrlCandidate);
25615
+ if (!normalized)
25616
+ return "";
25617
+ return new URL(normalized).hostname.match(/^([a-z0-9-]+)\.api\./i)?.[1] ?? "";
25618
+ }
25619
+ function inferManagementApiUrlFromSupabaseUrl(supabaseUrlCandidate, projectRef = "") {
25620
+ const normalized = normalizeUrl(supabaseUrlCandidate);
25514
25621
  if (!normalized)
25515
25622
  return "";
25516
25623
  const url = new URL(normalized);
@@ -25524,45 +25631,357 @@ function inferManagementApiUrlFromSupabaseUrl(value, projectRef = "") {
25524
25631
  url.hostname = `studio-${ref}.${host.slice(`${ref}.api.`.length)}`;
25525
25632
  return url.toString().replace(/\/+$/, "");
25526
25633
  }
25527
- const managedHostMatch = host.match(/^([a-z0-9-]+)\.api\.(.+)$/i);
25528
- if (managedHostMatch) {
25529
- url.hostname = `studio-${managedHostMatch[1]}.${managedHostMatch[2]}`;
25634
+ const managedHost = host.match(/^([a-z0-9-]+)\.api\.(.+)$/i);
25635
+ if (managedHost) {
25636
+ url.hostname = `studio-${managedHost[1]}.${managedHost[2]}`;
25530
25637
  return url.toString().replace(/\/+$/, "");
25531
25638
  }
25532
25639
  return normalized;
25533
25640
  }
25534
- function resolveSupaCloudContext(env = process.env, cwd = process.cwd()) {
25535
- const dotenv = readDotEnvFile(cwd);
25536
- const supabaseUrl = pickValue(env, dotenv, ["SUPABASE_URL"]);
25537
- const explicitApiUrl = pickValue(env, dotenv, ["SUPACLOUD_API_URL", "SUPACLOUD_MANAGEMENT_API_URL", "MANAGEMENT_API_URL"]);
25538
- const inferredToken = pickValue(env, dotenv, ["SUPABASE_SERVICE_ROLE_KEY", "SUPACLOUD_API_TOKEN"]);
25539
- const projectRef = pickValue(env, dotenv, ["SUPACLOUD_PROJECT_REF", "X_PROJECT_REF"]).value;
25540
- const hostFromEnv = env.SUPACLOUD_HOST;
25541
- const normalizedSupabaseUrl = normalizeUrl(supabaseUrl.value);
25542
- const apiUrl = normalizeUrl(explicitApiUrl.value) || inferManagementApiUrlFromSupabaseUrl(normalizedSupabaseUrl, projectRef) || (hostFromEnv ? `http://${hostFromEnv}:9090` : "");
25543
- const resolvedHostFromUrl = hostFromUrl(apiUrl || normalizedSupabaseUrl);
25641
+ function sourceAdminCore(environment) {
25642
+ const supabaseUrl = normalizeUrl(environment.SUPABASE_URL || "");
25643
+ const projectRef = (environment.SUPACLOUD_PROJECT_REF || environment.X_PROJECT_REF || "").trim() || inferProjectRefFromSupabaseUrl(supabaseUrl);
25644
+ const explicitApiUrl = managementApiUrlCandidate(environment);
25645
+ const apiUrl = normalizeUrl(explicitApiUrl) || inferManagementApiUrlFromSupabaseUrl(supabaseUrl, projectRef) || normalizeUrl(environment.SUPACLOUD_HOST ? `http://${environment.SUPACLOUD_HOST}:9090` : "");
25646
+ const apiToken = environment.SUPACLOUD_API_TOKEN || environment.SUPABASE_SERVICE_ROLE_KEY || "";
25647
+ return { apiUrl, apiToken, projectRef, supabaseUrl };
25648
+ }
25649
+ function processEnvironment(env) {
25650
+ return Object.fromEntries(SOURCE_ENVIRONMENT_KEYS.flatMap((key) => {
25651
+ const environmentValue = env[key];
25652
+ return environmentValue === undefined ? [] : [[key, environmentValue]];
25653
+ }));
25654
+ }
25655
+ function hasProcessContext(env) {
25656
+ return ADMIN_CONTEXT_KEYS.some((key) => Object.hasOwn(env, key));
25657
+ }
25658
+ function completeAdminContext(environment) {
25659
+ const adminCore = sourceAdminCore(environment);
25660
+ const hasApiContext = Boolean(adminCore.apiUrl && adminCore.apiToken);
25661
+ const hasSshContext = Boolean(environment.SUPACLOUD_HOST && environment.SUPACLOUD_SSH_HOST_FINGERPRINT);
25662
+ return hasApiContext || hasSshContext;
25663
+ }
25664
+ function namedEnvironmentSource(cwd, selector) {
25665
+ const environment = normalizeEnvironmentName(selector);
25666
+ const path = resolve(cwd, `.env.supacloud.${selector}`);
25667
+ const selectedEnvironment = readEnvFile(path, true);
25668
+ if (!selectedEnvironment.SUPACLOUD_ENV) {
25669
+ throw new Error(`SUPACLOUD_ENV is required in ${path}`);
25670
+ }
25671
+ if (normalizeEnvironmentName(selectedEnvironment.SUPACLOUD_ENV) !== environment) {
25672
+ throw new Error(`SUPACLOUD_ENV in ${path} does not match selector ${selector}`);
25673
+ }
25674
+ assertAdminFileContext(selectedEnvironment, path);
25675
+ return {
25676
+ environment: selectedEnvironment,
25677
+ kind: "named_env_file",
25678
+ path,
25679
+ environmentName: environment
25680
+ };
25681
+ }
25682
+ function explicitEnvironmentSource(cwd, envFile) {
25683
+ const path = resolve(cwd, envFile);
25684
+ const selectedEnvironment = readEnvFile(path, true);
25685
+ if (!selectedEnvironment.SUPACLOUD_ENV)
25686
+ throw new Error(`SUPACLOUD_ENV is required in ${path}`);
25687
+ assertAdminFileContext(selectedEnvironment, path);
25688
+ return {
25689
+ environment: selectedEnvironment,
25690
+ kind: "explicit_env_file",
25691
+ path,
25692
+ environmentName: normalizeEnvironmentName(selectedEnvironment.SUPACLOUD_ENV)
25693
+ };
25694
+ }
25695
+ function legacyEnvironmentSource(cwd) {
25696
+ const path = resolve(cwd, ".env");
25697
+ const environment = readEnvFile(path, false);
25698
+ if (Object.keys(environment).length > 0)
25699
+ assertAdminFileContext(environment, path);
25700
+ const environmentName = environment.SUPACLOUD_ENV ? normalizeEnvironmentName(environment.SUPACLOUD_ENV) : "";
25701
+ return {
25702
+ environment,
25703
+ kind: Object.keys(environment).length ? "legacy_dotenv" : "none",
25704
+ path: existsSync(path) ? path : null,
25705
+ environmentName
25706
+ };
25707
+ }
25708
+ function ambientContextSource(env, cwd) {
25709
+ const environment = processEnvironment(env);
25710
+ if (env.SUPACLOUD_ENV) {
25711
+ const environmentName = normalizeEnvironmentName(env.SUPACLOUD_ENV);
25712
+ if (completeAdminContext(environment)) {
25713
+ return processContextSource(environment, environmentName);
25714
+ }
25715
+ return namedEnvironmentSource(cwd, env.SUPACLOUD_ENV);
25716
+ }
25717
+ if (hasProcessContext(env)) {
25718
+ return processContextSource(environment, "");
25719
+ }
25720
+ return legacyEnvironmentSource(cwd);
25721
+ }
25722
+ function contextSource(env, cwd, selection) {
25723
+ if (selection.environmentName && selection.envFile) {
25724
+ throw new Error("Environment selectors are mutually exclusive");
25725
+ }
25726
+ if (selection.environmentName)
25727
+ return namedEnvironmentSource(cwd, selection.environmentName);
25728
+ if (selection.envFile)
25729
+ return explicitEnvironmentSource(cwd, selection.envFile);
25730
+ return ambientContextSource(env, cwd);
25731
+ }
25732
+ function resolvedSshContext(environment) {
25733
+ const rawPort = environment.SUPACLOUD_SSH_PORT || "22";
25734
+ if (!/^\d+$/.test(rawPort)) {
25735
+ throw new Error("SUPACLOUD_SSH_PORT must be an integer between 1 and 65535");
25736
+ }
25737
+ const sshPort = Number(rawPort);
25738
+ if (!Number.isSafeInteger(sshPort) || sshPort < 1 || sshPort > 65535) {
25739
+ throw new Error("SUPACLOUD_SSH_PORT must be an integer between 1 and 65535");
25740
+ }
25741
+ return {
25742
+ sshUser: environment.SUPACLOUD_SSH_USER || "root",
25743
+ sshPort,
25744
+ sshKey: environment.SUPACLOUD_SSH_KEY || resolve(homedir(), ".ssh", "id_rsa"),
25745
+ sshPass: environment.SUPACLOUD_SSH_PASS || "",
25746
+ sshHostFingerprint: environment.SUPACLOUD_SSH_HOST_FINGERPRINT || ""
25747
+ };
25748
+ }
25749
+ function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selection = {}) {
25750
+ const source = contextSource(env, cwd, selection);
25751
+ const adminCore = sourceAdminCore(source.environment);
25752
+ const host = source.environment.SUPACLOUD_HOST || hostFromUrl(adminCore.apiUrl || adminCore.supabaseUrl);
25544
25753
  return {
25545
- host: hostFromEnv ?? resolvedHostFromUrl,
25546
- sshUser: env.SUPACLOUD_SSH_USER ?? "root",
25547
- sshPort: parseInt(env.SUPACLOUD_SSH_PORT ?? "22", 10),
25548
- sshKey: env.SUPACLOUD_SSH_KEY ?? resolve(homedir(), ".ssh", "id_rsa"),
25549
- sshPass: env.SUPACLOUD_SSH_PASS ?? "",
25550
- sshHostFingerprint: env.SUPACLOUD_SSH_HOST_FINGERPRINT ?? "",
25551
- apiUrl,
25552
- apiToken: env.SUPACLOUD_API_TOKEN ?? inferredToken.value,
25553
- projectRef,
25554
- readOnly: env.SUPACLOUD_READ_ONLY === "true",
25555
- inferredSupabaseUrl: normalizedSupabaseUrl,
25556
- inferredServiceRoleKey: inferredToken.value,
25557
- source: detectSource([
25558
- supabaseUrl.value || explicitApiUrl.value ? supabaseUrl.value ? supabaseUrl.source : explicitApiUrl.source : "none",
25559
- inferredToken.value ? inferredToken.source : "none"
25560
- ])
25754
+ host,
25755
+ ...resolvedSshContext(source.environment),
25756
+ apiUrl: adminCore.apiUrl,
25757
+ apiToken: adminCore.apiToken,
25758
+ projectRef: adminCore.projectRef,
25759
+ readOnly: env.SUPACLOUD_READ_ONLY === "true" || source.environment.SUPACLOUD_READ_ONLY === "true",
25760
+ environment: source.environmentName,
25761
+ production: source.environmentName === "production",
25762
+ inferredSupabaseUrl: adminCore.supabaseUrl,
25763
+ inferredServiceRoleKey: source.environment.SUPABASE_SERVICE_ROLE_KEY || "",
25764
+ source: source.kind,
25765
+ sourcePath: source.path
25561
25766
  };
25562
25767
  }
25563
25768
 
25769
+ // src/shared/execution-policy.ts
25770
+ var ACTION_POLICY = {
25771
+ project: {
25772
+ read: ["list", "get", "settings", "api_keys", "health", "logs", "tasks", "services"],
25773
+ write: ["create", "delete", "pause", "restore", "restart", "update_settings"]
25774
+ },
25775
+ platform: {
25776
+ read: ["metrics", "list_backups", "network", "list_orgs", "get_org"],
25777
+ write: ["create_backup", "update_network"]
25778
+ },
25779
+ gateway: {
25780
+ read: ["routes", "get_certificate", "custom_hostname"],
25781
+ write: [
25782
+ "upsert_route",
25783
+ "update_route",
25784
+ "delete_route",
25785
+ "config",
25786
+ "update_certificate",
25787
+ "issue_certificate",
25788
+ "deploy_certificate",
25789
+ "rebuild",
25790
+ "set_custom_hostname",
25791
+ "delete_custom_hostname",
25792
+ "verify_custom_hostname"
25793
+ ]
25794
+ },
25795
+ ssh: {
25796
+ read: [
25797
+ "ping",
25798
+ "versions",
25799
+ "diagnose",
25800
+ "exec",
25801
+ "troubleshoot",
25802
+ "container_logs",
25803
+ "tenant_list",
25804
+ "tenant_inspect",
25805
+ "tenant_diagnose"
25806
+ ],
25807
+ write: ["setup", "install", "upgrade", "tenant_migrate"]
25808
+ }
25809
+ };
25810
+ var DYNAMIC_ACTION_FIELDS = {
25811
+ "project.service_control": "service_action",
25812
+ "ssh.tenant_manage": "tenant_action"
25813
+ };
25814
+ var REFLESS_WRITE_ACTIONS = new Set([
25815
+ "project.create",
25816
+ "ssh.setup",
25817
+ "ssh.install",
25818
+ "ssh.upgrade"
25819
+ ]);
25820
+ var SSH_PROJECT_REF_ACTIONS = new Set(["tenant_manage", "tenant_inspect", "tenant_diagnose"]);
25821
+ var PLATFORM_PROJECT_REF_ACTIONS = new Set([
25822
+ "list_backups",
25823
+ "create_backup",
25824
+ "network",
25825
+ "update_network"
25826
+ ]);
25827
+ function declaredMode(moduleName, action) {
25828
+ const policy = ACTION_POLICY[moduleName];
25829
+ if (!policy)
25830
+ return;
25831
+ for (const mode of ["read", "write", "local"]) {
25832
+ if (policy[mode]?.includes(action))
25833
+ return mode;
25834
+ }
25835
+ return;
25836
+ }
25837
+ function serviceControlMode(serviceAction) {
25838
+ if (serviceAction === "status")
25839
+ return "read";
25840
+ if (["start", "stop", "restart", "pause", "resume"].includes(String(serviceAction))) {
25841
+ return "write";
25842
+ }
25843
+ return;
25844
+ }
25845
+ function executionMode(moduleName, action, args) {
25846
+ if (moduleName === "project" && action === "service_control") {
25847
+ return serviceControlMode(args.service_action);
25848
+ }
25849
+ if (moduleName === "ssh" && action === "tenant_manage") {
25850
+ return serviceControlMode(args.tenant_action);
25851
+ }
25852
+ return declaredMode(moduleName, action);
25853
+ }
25854
+ function stringArgument(args, field) {
25855
+ const candidate = args[field];
25856
+ return typeof candidate === "string" && candidate ? candidate : null;
25857
+ }
25858
+ function sshRequestedProjectRef(action, args) {
25859
+ if (action === "tenant_migrate")
25860
+ return stringArgument(args, "target_ref");
25861
+ return SSH_PROJECT_REF_ACTIONS.has(action) ? stringArgument(args, "project_ref") : null;
25862
+ }
25863
+ function requestedProjectRef(moduleName, action, args) {
25864
+ if (moduleName === "ssh")
25865
+ return sshRequestedProjectRef(action, args);
25866
+ if (moduleName === "project") {
25867
+ return ["list", "create"].includes(action) ? null : stringArgument(args, "ref");
25868
+ }
25869
+ if (moduleName === "platform") {
25870
+ return PLATFORM_PROJECT_REF_ACTIONS.has(action) ? stringArgument(args, "ref") : null;
25871
+ }
25872
+ return moduleName === "gateway" ? stringArgument(args, "ref") : null;
25873
+ }
25874
+ function requiresProjectRef(moduleName, action) {
25875
+ return !REFLESS_WRITE_ACTIONS.has(`${moduleName}.${action}`);
25876
+ }
25877
+ function apiConfirmationTarget(context) {
25878
+ if (!context.apiUrl) {
25879
+ throw new Error("Production platform write requires a configured SUPACLOUD_API_URL");
25880
+ }
25881
+ return `platform:${new URL(context.apiUrl).host}`;
25882
+ }
25883
+ function sshConfirmationTarget(context) {
25884
+ if (!context.host) {
25885
+ throw new Error("Production SSH write requires a configured SUPACLOUD_HOST");
25886
+ }
25887
+ const portSuffix = context.sshPort === 22 ? "" : `:${context.sshPort}`;
25888
+ return `host:${context.host}${portSuffix}`;
25889
+ }
25890
+ function productionConfirmationTarget(moduleName, action, args, context) {
25891
+ const projectRef = requestedProjectRef(moduleName, action, args);
25892
+ if (projectRef)
25893
+ return projectRef;
25894
+ if (moduleName === "ssh")
25895
+ return sshConfirmationTarget(context);
25896
+ return apiConfirmationTarget(context);
25897
+ }
25898
+ function assertProfileProjectBoundary(requestedRef, context) {
25899
+ if (requestedRef && context.projectRef && requestedRef !== context.projectRef) {
25900
+ throw new Error("Production profiles cannot target a different project ref");
25901
+ }
25902
+ }
25903
+ function requiredProjectRefFlag(moduleName, action) {
25904
+ if (moduleName !== "ssh")
25905
+ return "ref";
25906
+ return action === "tenant_migrate" ? "target_ref" : "project_ref";
25907
+ }
25908
+ function authorizeWrite(request) {
25909
+ const { moduleName, action, args, projectRef, authorization } = request;
25910
+ const { context, confirmProduction } = authorization;
25911
+ if (context.readOnly) {
25912
+ throw new Error(`Remote write ${moduleName}.${action} is blocked in read-only mode (SUPACLOUD_READ_ONLY=true)`);
25913
+ }
25914
+ if (!context.environment) {
25915
+ throw new Error(`Remote write ${moduleName}.${action} requires an explicit SUPACLOUD_ENV`);
25916
+ }
25917
+ if (!context.production)
25918
+ return;
25919
+ if (requiresProjectRef(moduleName, action) && !projectRef) {
25920
+ throw new Error(`Production write ${moduleName}.${action} requires --${requiredProjectRefFlag(moduleName, action)}`);
25921
+ }
25922
+ const confirmationTarget = productionConfirmationTarget(moduleName, action, args, context);
25923
+ if (confirmProduction !== confirmationTarget) {
25924
+ throw new Error(`Production write requires --confirm-production ${confirmationTarget}`);
25925
+ }
25926
+ }
25927
+ function assertKnownMode(moduleName, action, mode, context) {
25928
+ if (!mode && (context.production || context.readOnly)) {
25929
+ throw new Error(`Execution policy has no classification for ${moduleName}.${action}`);
25930
+ }
25931
+ }
25932
+ function authorizeExecution(moduleName, args, authorization) {
25933
+ const action = typeof args.action === "string" ? args.action : "";
25934
+ if (!action)
25935
+ return;
25936
+ const mode = executionMode(moduleName, action, args);
25937
+ const { context } = authorization;
25938
+ assertKnownMode(moduleName, action, mode, context);
25939
+ if (!mode)
25940
+ return;
25941
+ const projectRef = requestedProjectRef(moduleName, action, args);
25942
+ if (context.production)
25943
+ assertProfileProjectBoundary(projectRef, context);
25944
+ if (mode !== "write")
25945
+ return;
25946
+ authorizeWrite({ moduleName, action, args, projectRef, authorization });
25947
+ }
25948
+ function validateDynamicAction(tool, moduleName, action, field) {
25949
+ const fieldSchema = schemaProperties(tool.schema)[field];
25950
+ const fieldActions = fieldSchema ? schemaEnumValues(fieldSchema) : [];
25951
+ if (fieldActions.length === 0) {
25952
+ throw new Error(`Execution policy cannot inspect ${moduleName}.${action}.${field}`);
25953
+ }
25954
+ for (const fieldAction of fieldActions) {
25955
+ if (!executionMode(moduleName, action, { [field]: fieldAction })) {
25956
+ throw new Error(`Execution policy has no classification for ${moduleName}.${action}.${fieldAction}`);
25957
+ }
25958
+ }
25959
+ }
25960
+ function validateRegisteredAction(tool, moduleName, action) {
25961
+ const dynamicField = DYNAMIC_ACTION_FIELDS[`${moduleName}.${action}`];
25962
+ if (dynamicField)
25963
+ return validateDynamicAction(tool, moduleName, action, dynamicField);
25964
+ if (!declaredMode(moduleName, action)) {
25965
+ throw new Error(`Execution policy has no classification for ${moduleName}.${action}`);
25966
+ }
25967
+ }
25968
+ function validateExecutionPolicyCoverage(tools) {
25969
+ for (const [moduleName, tool] of Object.entries(tools)) {
25970
+ const actionSchema = schemaProperties(tool.schema).action;
25971
+ if (!actionSchema)
25972
+ continue;
25973
+ const registeredActions = schemaEnumValues(actionSchema);
25974
+ if (registeredActions.length === 0) {
25975
+ throw new Error(`Execution policy cannot inspect ${moduleName}.action`);
25976
+ }
25977
+ for (const action of registeredActions)
25978
+ validateRegisteredAction(tool, moduleName, action);
25979
+ }
25980
+ }
25981
+
25564
25982
  // src/shared/transports/http.ts
25565
25983
  var DEFAULT_TIMEOUT = 30000;
25984
+ var MAX_POST_TIMEOUT_MS = 35 * 60000;
25566
25985
  var MAX_RETRIES = 2;
25567
25986
  var RETRY_BASE_DELAY = 500;
25568
25987
  function isRetryableMethod(method) {
@@ -25575,9 +25994,26 @@ function isRetryableError(error) {
25575
25994
  const networkError = error;
25576
25995
  return networkError.name === "AbortError" || networkError.code === "ECONNREFUSED" || networkError.code === "ECONNRESET";
25577
25996
  }
25578
- async function fetchWithTimeout(url, options) {
25997
+ function transportFailure(error) {
25998
+ const networkError = error instanceof Error ? error : null;
25999
+ const code = networkError?.name === "AbortError" ? "TIMEOUT" : networkError?.code === "ECONNRESET" ? "CONNECTION_RESET" : "NETWORK_ERROR";
26000
+ return {
26001
+ ok: false,
26002
+ status: 500,
26003
+ data: { error: "Network Error", code },
26004
+ transportError: true
26005
+ };
26006
+ }
26007
+ function validatedPostTimeout(options) {
26008
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT;
26009
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_POST_TIMEOUT_MS) {
26010
+ throw new RangeError(`HTTP request timeout must be between 1 and ${MAX_POST_TIMEOUT_MS} ms`);
26011
+ }
26012
+ return timeoutMs;
26013
+ }
26014
+ async function fetchWithTimeout(url, options, timeoutMs = DEFAULT_TIMEOUT) {
25579
26015
  const controller = new AbortController;
25580
- const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
26016
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
25581
26017
  try {
25582
26018
  return await fetch(url, {
25583
26019
  ...options,
@@ -25587,11 +26023,11 @@ async function fetchWithTimeout(url, options) {
25587
26023
  clearTimeout(timeout);
25588
26024
  }
25589
26025
  }
25590
- async function fetchWithRetry(url, options) {
26026
+ async function fetchWithRetry(url, options, timeoutMs = DEFAULT_TIMEOUT) {
25591
26027
  const retries = isRetryableMethod(options.method) ? MAX_RETRIES : 0;
25592
26028
  for (let attempt = 0;attempt <= retries; attempt++) {
25593
26029
  try {
25594
- const res = await fetchWithTimeout(url, options);
26030
+ const res = await fetchWithTimeout(url, options, timeoutMs);
25595
26031
  if (res.status >= 500 && res.status < 600 && attempt < retries) {
25596
26032
  const delay = RETRY_BASE_DELAY * Math.pow(2, attempt);
25597
26033
  await new Promise((r) => setTimeout(r, delay));
@@ -25632,20 +26068,21 @@ class HttpTransport {
25632
26068
  const data = await res.json().catch(() => null);
25633
26069
  return { ok: res.ok, status: res.status, data };
25634
26070
  } catch (error) {
25635
- return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
26071
+ return transportFailure(error);
25636
26072
  }
25637
26073
  }
25638
- async post(path, body) {
26074
+ async post(path, body, options) {
26075
+ const timeoutMs = validatedPostTimeout(options);
25639
26076
  try {
25640
26077
  const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
25641
26078
  method: "POST",
25642
26079
  headers: this.headers(),
25643
26080
  body: body ? JSON.stringify(body) : undefined
25644
- });
26081
+ }, timeoutMs);
25645
26082
  const data = await res.json().catch(() => null);
25646
26083
  return { ok: res.ok, status: res.status, data };
25647
26084
  } catch (error) {
25648
- return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
26085
+ return transportFailure(error);
25649
26086
  }
25650
26087
  }
25651
26088
  async postMultipart(path, formData) {
@@ -25659,7 +26096,7 @@ class HttpTransport {
25659
26096
  const data = await res.json().catch(() => null);
25660
26097
  return { ok: res.ok, status: res.status, data };
25661
26098
  } catch (error) {
25662
- return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
26099
+ return transportFailure(error);
25663
26100
  }
25664
26101
  }
25665
26102
  async patch(path, body) {
@@ -25672,7 +26109,7 @@ class HttpTransport {
25672
26109
  const data = await res.json().catch(() => null);
25673
26110
  return { ok: res.ok, status: res.status, data };
25674
26111
  } catch (error) {
25675
- return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
26112
+ return transportFailure(error);
25676
26113
  }
25677
26114
  }
25678
26115
  async put(path, body) {
@@ -25685,7 +26122,7 @@ class HttpTransport {
25685
26122
  const data = await res.json().catch(() => null);
25686
26123
  return { ok: res.ok, status: res.status, data };
25687
26124
  } catch (error) {
25688
- return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
26125
+ return transportFailure(error);
25689
26126
  }
25690
26127
  }
25691
26128
  async delete(path) {
@@ -25697,7 +26134,7 @@ class HttpTransport {
25697
26134
  const data = await res.json().catch(() => null);
25698
26135
  return { ok: res.ok, status: res.status, data };
25699
26136
  } catch (error) {
25700
- return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
26137
+ return transportFailure(error);
25701
26138
  }
25702
26139
  }
25703
26140
  async ping() {
@@ -28475,6 +28912,127 @@ ${r.stderr.slice(-1000)}`;
28475
28912
  });
28476
28913
  }
28477
28914
 
28915
+ // src/shared/tools/backup-release-control.ts
28916
+ var SAFE_PATH_SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
28917
+ var SAFE_BACKUP_FIELD = /^[A-Za-z0-9_.-]{1,128}$/;
28918
+ var PHYSICAL_BACKUP_REQUEST_TIMEOUT_MS = 35 * 60000;
28919
+ function nonnegativeSafeInteger(candidate) {
28920
+ return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0;
28921
+ }
28922
+ function backupTimestamp(candidate) {
28923
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
28924
+ return null;
28925
+ const timestamp = candidate;
28926
+ if (!nonnegativeSafeInteger(timestamp.start) || !nonnegativeSafeInteger(timestamp.stop))
28927
+ return null;
28928
+ if (timestamp.stop > 0 && timestamp.stop < timestamp.start)
28929
+ return null;
28930
+ return { start: timestamp.start, stop: timestamp.stop };
28931
+ }
28932
+ function backupFailure(operation, code, httpStatus) {
28933
+ return {
28934
+ isError: true,
28935
+ content: [{
28936
+ type: "text",
28937
+ text: JSON.stringify({
28938
+ ok: false,
28939
+ operation,
28940
+ error: { code, http_status: httpStatus }
28941
+ })
28942
+ }]
28943
+ };
28944
+ }
28945
+ function backupRecord(candidate) {
28946
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
28947
+ return null;
28948
+ const record = candidate;
28949
+ const timestamp = backupTimestamp(record.timestamp);
28950
+ if (!timestamp)
28951
+ return null;
28952
+ if (typeof record.id !== "string" || !SAFE_BACKUP_FIELD.test(record.id))
28953
+ return null;
28954
+ if (record.type !== "full" && record.type !== "incr" && record.type !== "diff")
28955
+ return null;
28956
+ if (!nonnegativeSafeInteger(record.size))
28957
+ return null;
28958
+ if (typeof record.database !== "string" || !SAFE_BACKUP_FIELD.test(record.database))
28959
+ return null;
28960
+ return {
28961
+ id: record.id,
28962
+ type: record.type,
28963
+ timestamp,
28964
+ size: record.size,
28965
+ database: record.database
28966
+ };
28967
+ }
28968
+ function backupInventory(payload) {
28969
+ if (!Array.isArray(payload))
28970
+ return null;
28971
+ const records = payload.map(backupRecord);
28972
+ if (records.some((record) => record === null))
28973
+ return null;
28974
+ const inventory = records;
28975
+ return new Set(inventory.map((record) => record.id)).size === inventory.length ? inventory : null;
28976
+ }
28977
+ function backupEndpoint(projectRef) {
28978
+ if (!SAFE_PATH_SEGMENT.test(projectRef))
28979
+ throw new Error("'ref' is invalid for physical backup");
28980
+ return `/v1/projects/${encodeURIComponent(projectRef)}/database/backups`;
28981
+ }
28982
+ async function readBackupInventory(http, endpoint) {
28983
+ const response = await http.get(endpoint);
28984
+ return { response, inventory: response.ok ? backupInventory(response.data) ?? undefined : undefined };
28985
+ }
28986
+ function successfulBackupResponse(payload) {
28987
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
28988
+ }
28989
+ function completedFullBackup(before, after) {
28990
+ const previousIds = new Set(before.map((backup) => backup.id));
28991
+ const candidates = after.filter((backup) => !previousIds.has(backup.id) && backup.type === "full" && backup.timestamp.stop > 0 && backup.size > 0);
28992
+ return candidates.length === 1 ? candidates[0] : null;
28993
+ }
28994
+ function mutationFailureCode(statusCode) {
28995
+ return statusCode >= 400 && statusCode < 500 && statusCode !== 408 ? "HTTP_ERROR" : "OUTCOME_UNKNOWN";
28996
+ }
28997
+ function confirmsFullBackup(payload) {
28998
+ return Boolean(payload) && typeof payload === "object" && !Array.isArray(payload) && payload.message === "full backup completed";
28999
+ }
29000
+ function reconciledCreationResponse(projectRef, before, creation, afterRead) {
29001
+ if (!creation.ok) {
29002
+ return backupFailure("platform.create_backup", mutationFailureCode(creation.status), creation.status);
29003
+ }
29004
+ if (!confirmsFullBackup(creation.data)) {
29005
+ return backupFailure("platform.create_backup", "OUTCOME_UNKNOWN", creation.status);
29006
+ }
29007
+ if (!afterRead.response.ok || !afterRead.inventory) {
29008
+ return backupFailure("platform.create_backup", "OUTCOME_UNKNOWN", afterRead.response.status);
29009
+ }
29010
+ const backup = completedFullBackup(before, afterRead.inventory);
29011
+ if (!backup)
29012
+ return backupFailure("platform.create_backup", "OUTCOME_UNKNOWN", afterRead.response.status);
29013
+ return successfulBackupResponse({ project_ref: projectRef, requested_type: "full", backup });
29014
+ }
29015
+ async function listPhysicalBackups(http, projectRef) {
29016
+ const { response, inventory } = await readBackupInventory(http, backupEndpoint(projectRef));
29017
+ if (!response.ok)
29018
+ return backupFailure("platform.list_backups", "HTTP_ERROR", response.status);
29019
+ if (!inventory)
29020
+ return backupFailure("platform.list_backups", "INVALID_RESPONSE", null);
29021
+ return successfulBackupResponse(inventory);
29022
+ }
29023
+ async function createFullPhysicalBackup(http, projectRef) {
29024
+ const endpoint = backupEndpoint(projectRef);
29025
+ const beforeRead = await readBackupInventory(http, endpoint);
29026
+ if (!beforeRead.response.ok) {
29027
+ return backupFailure("platform.create_backup", "HTTP_ERROR", beforeRead.response.status);
29028
+ }
29029
+ if (!beforeRead.inventory)
29030
+ return backupFailure("platform.create_backup", "INVALID_RESPONSE", null);
29031
+ const creation = await http.post(endpoint, { type: "full" }, { timeoutMs: PHYSICAL_BACKUP_REQUEST_TIMEOUT_MS });
29032
+ const afterRead = await readBackupInventory(http, endpoint);
29033
+ return reconciledCreationResponse(projectRef, beforeRead.inventory, creation, afterRead);
29034
+ }
29035
+
28478
29036
  // src/shared/tools/advanced-tools.ts
28479
29037
  function registerAdvancedTools(server, http) {
28480
29038
  server.tool("edge_functions", `Edge Function management (Deno/Bun serverless). Server auto-bundles dependencies.
@@ -28628,11 +29186,12 @@ Actions: metrics, list_backups, create_backup, network, update_network, list_org
28628
29186
  "list_orgs",
28629
29187
  "get_org"
28630
29188
  ]), "Action"),
28631
- ref: optional(Type.String(), "Project ref (for backup/network actions)"),
29189
+ ref: optional(Type.String(), "[*] Project ref for project-scoped platform actions"),
29190
+ backup_type: optional(stringEnum(["full"]), "[create_backup] Explicit physical backup type"),
28632
29191
  slug: optional(Type.String(), "[get_org] Organization slug"),
28633
29192
  allowed_cidrs: optional(Type.Array(Type.String()), "[update_network] Allowed CIDRs")
28634
29193
  }, async (args) => {
28635
- const { action, ref, slug, allowed_cidrs } = args;
29194
+ const { action, ref, backup_type, slug, allowed_cidrs } = args;
28636
29195
  const need = (f, v) => {
28637
29196
  if (!v)
28638
29197
  throw new Error(`'${f}' required for '${action}'`);
@@ -28644,14 +29203,11 @@ Actions: metrics, list_backups, create_backup, network, update_network, list_org
28644
29203
  break;
28645
29204
  case "list_backups":
28646
29205
  need("ref", ref);
28647
- text = JSON.stringify((await http.get(`/v1/projects/${ref}/database/backups`)).data, null, 2);
28648
- break;
29206
+ return listPhysicalBackups(http, ref);
28649
29207
  case "create_backup": {
28650
29208
  need("ref", ref);
28651
- const r = await http.post(`/v1/projects/${ref}/database/backups`);
28652
- text = r.ok ? `✅ Backup created
28653
- ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status})`;
28654
- break;
29209
+ need("backup_type", backup_type);
29210
+ return createFullPhysicalBackup(http, ref);
28655
29211
  }
28656
29212
  case "network":
28657
29213
  need("ref", ref);
@@ -28677,7 +29233,704 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status})`;
28677
29233
  });
28678
29234
  }
28679
29235
 
29236
+ // src/shared/tools/project-create-env.ts
29237
+ import { constants } from "node:fs";
29238
+ import { isAbsolute, basename as basename4, dirname as dirname2, join as join3, normalize, parse as parse2, resolve as resolve2 } from "node:path";
29239
+ import {
29240
+ lstat,
29241
+ open,
29242
+ realpath,
29243
+ unlink
29244
+ } from "node:fs/promises";
29245
+ var PROJECT_REF = /^[a-z]{20}$/;
29246
+ var SERVICE_ROLE_KEY = /^[A-Za-z0-9_-]{8,2048}\.[A-Za-z0-9_-]{8,8192}\.[A-Za-z0-9_-]{8,2048}$/;
29247
+ var SERVICE_ROLE_ALGORITHMS = new Set(["HS256", "ES256"]);
29248
+ var MAX_ENV_FILE_PATH_BYTES = 4096;
29249
+ var ENV_FILE_MODE = 384;
29250
+ var GROUP_OR_WORLD_WRITE_MODE = 18;
29251
+ var PROC_SELF_FD = "/proc/self/fd";
29252
+
29253
+ class ProjectCreateEnvError extends Error {
29254
+ code;
29255
+ credentialFileState;
29256
+ constructor(code, credentialFileState = "absent") {
29257
+ super(code);
29258
+ this.code = code;
29259
+ this.credentialFileState = credentialFileState;
29260
+ this.name = "ProjectCreateEnvError";
29261
+ }
29262
+ }
29263
+ function descriptorRelativePath(directory, filename) {
29264
+ return `${PROC_SELF_FD}/${directory.fd}/${filename}`;
29265
+ }
29266
+ var nodeProjectEnvFileOperations = {
29267
+ platform: process.platform,
29268
+ effectiveUid: () => process.geteuid?.() ?? -1,
29269
+ lstat,
29270
+ realpath,
29271
+ openDirectory: (path2) => open(path2, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW),
29272
+ openExclusiveAt: (directory, filename, mode) => open(descriptorRelativePath(directory, filename), "wx", mode),
29273
+ lstatAt: (directory, filename) => lstat(descriptorRelativePath(directory, filename)),
29274
+ unlinkAt: (directory, filename) => unlink(descriptorRelativePath(directory, filename))
29275
+ };
29276
+ function fileSystemErrorCode(candidate) {
29277
+ if (!candidate || typeof candidate !== "object" || !("code" in candidate))
29278
+ return null;
29279
+ return typeof candidate.code === "string" ? candidate.code : null;
29280
+ }
29281
+ async function assertTargetAbsent(path2, operations) {
29282
+ try {
29283
+ await operations.lstat(path2);
29284
+ } catch (error) {
29285
+ if (fileSystemErrorCode(error) === "ENOENT")
29286
+ return;
29287
+ throw new ProjectCreateEnvError("ENV_FILE_PATH_INVALID");
29288
+ }
29289
+ throw new ProjectCreateEnvError("ENV_FILE_EXISTS");
29290
+ }
29291
+ function hasStableCanonicalParent(directory, canonicalParent, parentBeforeRealpath, parentAfterRealpath) {
29292
+ return parentBeforeRealpath.isDirectory() && !parentBeforeRealpath.isSymbolicLink() && parentAfterRealpath.isDirectory() && !parentAfterRealpath.isSymbolicLink() && canonicalParent === directory && sameFileIdentity(parentBeforeRealpath, parentAfterRealpath);
29293
+ }
29294
+ function parentIdentity(canonicalPath, parentStat) {
29295
+ return {
29296
+ canonicalPath,
29297
+ dev: parentStat.dev,
29298
+ ino: parentStat.ino,
29299
+ mode: parentStat.mode,
29300
+ uid: parentStat.uid
29301
+ };
29302
+ }
29303
+ async function verifiedParentIdentity(directory, operations) {
29304
+ const parentBeforeRealpath = await operations.lstat(directory);
29305
+ const canonicalParent = await operations.realpath(directory);
29306
+ await assertTrustedDirectoryChain(directory, operations);
29307
+ const parentAfterRealpath = await operations.lstat(directory);
29308
+ assertOwnedTargetParent(parentAfterRealpath, operations.effectiveUid());
29309
+ const stableParent = hasStableCanonicalParent(directory, canonicalParent, parentBeforeRealpath, parentAfterRealpath);
29310
+ if (!stableParent)
29311
+ throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
29312
+ return parentIdentity(canonicalParent, parentAfterRealpath);
29313
+ }
29314
+ async function assertSafeParent(directory, operations) {
29315
+ try {
29316
+ return await verifiedParentIdentity(directory, operations);
29317
+ } catch (error) {
29318
+ if (error instanceof ProjectCreateEnvError)
29319
+ throw error;
29320
+ throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
29321
+ }
29322
+ }
29323
+ function directoryChain(directory) {
29324
+ const root = parse2(directory).root;
29325
+ const ancestors = [root];
29326
+ let current = root;
29327
+ for (const component of directory.slice(root.length).split("/").filter(Boolean)) {
29328
+ current = join3(current, component);
29329
+ ancestors.push(current);
29330
+ }
29331
+ return ancestors;
29332
+ }
29333
+ function assertTrustedAncestor(directoryStat, effectiveUid) {
29334
+ const ownerIsTrusted = directoryStat.uid === 0 || directoryStat.uid === effectiveUid;
29335
+ const hasUntrustedWriteAccess = Boolean(directoryStat.mode & GROUP_OR_WORLD_WRITE_MODE);
29336
+ if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || !ownerIsTrusted || hasUntrustedWriteAccess) {
29337
+ throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
29338
+ }
29339
+ }
29340
+ function assertOwnedTargetParent(directoryStat, effectiveUid) {
29341
+ assertTrustedAncestor(directoryStat, effectiveUid);
29342
+ if (directoryStat.uid !== effectiveUid) {
29343
+ throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
29344
+ }
29345
+ }
29346
+ async function assertTrustedDirectoryChain(directory, operations) {
29347
+ const effectiveUid = operations.effectiveUid();
29348
+ if (!Number.isSafeInteger(effectiveUid) || effectiveUid < 0) {
29349
+ throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
29350
+ }
29351
+ for (const ancestor of directoryChain(directory)) {
29352
+ assertTrustedAncestor(await operations.lstat(ancestor), effectiveUid);
29353
+ }
29354
+ }
29355
+ function sameFileIdentity(first, second) {
29356
+ return first.dev === second.dev && first.ino === second.ino;
29357
+ }
29358
+ function sameParentIdentity(current, expected) {
29359
+ return sameFileIdentity(current, expected) && current.mode === expected.mode && current.uid === expected.uid;
29360
+ }
29361
+ function hasSafeRequestedPath(requestedPath) {
29362
+ return Boolean(requestedPath) && isAbsolute(requestedPath) && requestedPath === normalize(requestedPath) && requestedPath === resolve2(requestedPath) && Buffer.byteLength(requestedPath, "utf8") <= MAX_ENV_FILE_PATH_BYTES && !/[\0\r\n]/.test(requestedPath) && ![".", ".."].includes(basename4(requestedPath));
29363
+ }
29364
+ async function prepareProjectEnvFile(requestedPath, environment, operations = nodeProjectEnvFileOperations) {
29365
+ if (operations.platform !== "linux") {
29366
+ throw new ProjectCreateEnvError("ENV_FILE_PLATFORM_UNSUPPORTED");
29367
+ }
29368
+ if (!hasSafeRequestedPath(requestedPath)) {
29369
+ throw new ProjectCreateEnvError("ENV_FILE_PATH_INVALID");
29370
+ }
29371
+ const directory = dirname2(requestedPath);
29372
+ const filename = basename4(requestedPath);
29373
+ const parentIdentity2 = await assertSafeParent(directory, operations);
29374
+ await assertTargetAbsent(requestedPath, operations);
29375
+ let directoryHandle = null;
29376
+ let openFile = null;
29377
+ let fileIdentity = null;
29378
+ try {
29379
+ directoryHandle = await operations.openDirectory(directory);
29380
+ await assertDirectoryBinding(directory, directoryHandle, parentIdentity2, operations);
29381
+ openFile = await openProjectEnvFile(directoryHandle, filename, operations);
29382
+ const openFileStat = await openFile.stat();
29383
+ assertSecureFileType(openFileStat);
29384
+ fileIdentity = { dev: openFileStat.dev, ino: openFileStat.ino };
29385
+ await openFile.chmod(ENV_FILE_MODE);
29386
+ const prepared = {
29387
+ path: requestedPath,
29388
+ filename,
29389
+ environment,
29390
+ parentIdentity: parentIdentity2,
29391
+ fileIdentity,
29392
+ directoryHandle,
29393
+ openFile
29394
+ };
29395
+ await assertPreparedFileBinding(prepared, operations);
29396
+ return prepared;
29397
+ } catch (error) {
29398
+ if (directoryHandle) {
29399
+ const credentialFileState = await removeFailedEnvFile({ directoryHandle, filename, openFile, fileIdentity, operations });
29400
+ if (credentialFileState === "unknown") {
29401
+ throw new ProjectCreateEnvError("ENV_FILE_CLEANUP_FAILED", "unknown");
29402
+ }
29403
+ }
29404
+ if (error instanceof ProjectCreateEnvError)
29405
+ throw error;
29406
+ throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
29407
+ }
29408
+ }
29409
+ function environmentFileContent(credentials, environment) {
29410
+ return [
29411
+ `SUPACLOUD_ENV=${environment}`,
29412
+ `SUPACLOUD_PROJECT_REF=${credentials.projectRef}`,
29413
+ `SUPABASE_URL=${credentials.apiUrl}`,
29414
+ `SUPABASE_SERVICE_ROLE_KEY=${credentials.serviceRoleKey}`,
29415
+ ""
29416
+ ].join(`
29417
+ `);
29418
+ }
29419
+ async function sanitizeAndClose(openFile) {
29420
+ let sanitized = true;
29421
+ try {
29422
+ await openFile.truncate(0);
29423
+ await openFile.sync();
29424
+ } catch {
29425
+ sanitized = false;
29426
+ }
29427
+ try {
29428
+ await openFile.close();
29429
+ } catch {}
29430
+ return sanitized;
29431
+ }
29432
+ async function directorySyncSucceeded(directoryHandle) {
29433
+ try {
29434
+ await directoryHandle.sync();
29435
+ return true;
29436
+ } catch {
29437
+ return false;
29438
+ }
29439
+ }
29440
+ async function cleanedCredentialState(cleanup, sanitized) {
29441
+ const { directoryHandle, filename, fileIdentity, operations } = cleanup;
29442
+ try {
29443
+ const targetStat = await operations.lstatAt(directoryHandle, filename);
29444
+ if (!fileIdentity || !sameFileIdentity(targetStat, fileIdentity))
29445
+ return "unknown";
29446
+ await operations.unlinkAt(directoryHandle, filename);
29447
+ } catch (error) {
29448
+ if (fileSystemErrorCode(error) !== "ENOENT")
29449
+ return "unknown";
29450
+ }
29451
+ if (!await directorySyncSucceeded(directoryHandle))
29452
+ return "unknown";
29453
+ return sanitized ? "absent" : "unknown";
29454
+ }
29455
+ async function removeFailedEnvFile(cleanup) {
29456
+ const { directoryHandle, filename, openFile, fileIdentity, operations } = cleanup;
29457
+ if (!openFile) {
29458
+ try {
29459
+ await directoryHandle.close();
29460
+ } catch {}
29461
+ return "absent";
29462
+ }
29463
+ const sanitized = await sanitizeAndClose(openFile);
29464
+ const credentialFileState = await cleanedCredentialState(cleanup, sanitized);
29465
+ try {
29466
+ await directoryHandle.close();
29467
+ } catch {}
29468
+ return credentialFileState;
29469
+ }
29470
+ function assertSecureFileType(fileStat) {
29471
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
29472
+ throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
29473
+ }
29474
+ }
29475
+ function assertSecureOpenFile(fileStat, effectiveUid) {
29476
+ assertSecureFileType(fileStat);
29477
+ if ((fileStat.mode & 511) !== ENV_FILE_MODE || fileStat.uid !== effectiveUid) {
29478
+ throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
29479
+ }
29480
+ }
29481
+ async function openProjectEnvFile(directory, filename, operations) {
29482
+ try {
29483
+ return await operations.openExclusiveAt(directory, filename, ENV_FILE_MODE);
29484
+ } catch (error) {
29485
+ if (fileSystemErrorCode(error) === "EEXIST") {
29486
+ throw new ProjectCreateEnvError("ENV_FILE_EXISTS");
29487
+ }
29488
+ throw error;
29489
+ }
29490
+ }
29491
+ async function assertDirectoryBinding(path2, directoryHandle, expectedIdentity, operations) {
29492
+ const heldDirectoryStat = await directoryHandle.stat();
29493
+ const currentPathIdentity = await assertSafeParent(path2, operations);
29494
+ assertOwnedTargetParent(heldDirectoryStat, operations.effectiveUid());
29495
+ if (!sameParentIdentity(heldDirectoryStat, expectedIdentity) || !sameParentIdentity(currentPathIdentity, expectedIdentity)) {
29496
+ throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
29497
+ }
29498
+ }
29499
+ async function assertPreparedFileBinding(prepared, operations) {
29500
+ if (!prepared.directoryHandle || !prepared.openFile) {
29501
+ throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
29502
+ }
29503
+ await assertDirectoryBinding(dirname2(prepared.path), prepared.directoryHandle, prepared.parentIdentity, operations);
29504
+ const openFileStat = await prepared.openFile.stat();
29505
+ const targetStat = await operations.lstatAt(prepared.directoryHandle, prepared.filename);
29506
+ const effectiveUid = operations.effectiveUid();
29507
+ assertSecureOpenFile(openFileStat, effectiveUid);
29508
+ assertSecureOpenFile(targetStat, effectiveUid);
29509
+ if (!sameFileIdentity(openFileStat, prepared.fileIdentity) || !sameFileIdentity(targetStat, prepared.fileIdentity)) {
29510
+ throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
29511
+ }
29512
+ }
29513
+ async function assertCompletedFileBinding(prepared, operations) {
29514
+ if (!prepared.directoryHandle)
29515
+ throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
29516
+ await assertDirectoryBinding(dirname2(prepared.path), prepared.directoryHandle, prepared.parentIdentity, operations);
29517
+ const targetStat = await operations.lstatAt(prepared.directoryHandle, prepared.filename);
29518
+ assertSecureOpenFile(targetStat, operations.effectiveUid());
29519
+ if (!sameFileIdentity(targetStat, prepared.fileIdentity)) {
29520
+ throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
29521
+ }
29522
+ }
29523
+ async function discardPreparedProjectEnvFile(prepared, operations = nodeProjectEnvFileOperations) {
29524
+ if (!prepared.directoryHandle)
29525
+ return "absent";
29526
+ const directoryHandle = prepared.directoryHandle;
29527
+ const openFile = prepared.openFile;
29528
+ prepared.directoryHandle = null;
29529
+ prepared.openFile = null;
29530
+ return removeFailedEnvFile({
29531
+ directoryHandle,
29532
+ filename: prepared.filename,
29533
+ openFile,
29534
+ fileIdentity: prepared.fileIdentity,
29535
+ operations
29536
+ });
29537
+ }
29538
+ async function writeProjectEnvFile(prepared, credentials, operations = nodeProjectEnvFileOperations) {
29539
+ if (!prepared.directoryHandle || !prepared.openFile) {
29540
+ throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
29541
+ }
29542
+ const directoryHandle = prepared.directoryHandle;
29543
+ const openFile = prepared.openFile;
29544
+ try {
29545
+ await assertPreparedFileBinding(prepared, operations);
29546
+ await openFile.chmod(ENV_FILE_MODE);
29547
+ await assertPreparedFileBinding(prepared, operations);
29548
+ await openFile.writeFile(environmentFileContent(credentials, prepared.environment));
29549
+ await openFile.sync();
29550
+ await assertPreparedFileBinding(prepared, operations);
29551
+ await openFile.close();
29552
+ prepared.openFile = null;
29553
+ await assertCompletedFileBinding(prepared, operations);
29554
+ await directoryHandle.sync();
29555
+ await directoryHandle.close();
29556
+ prepared.directoryHandle = null;
29557
+ } catch (error) {
29558
+ prepared.directoryHandle = null;
29559
+ prepared.openFile = null;
29560
+ const credentialFileState = await removeFailedEnvFile({
29561
+ directoryHandle,
29562
+ filename: prepared.filename,
29563
+ openFile,
29564
+ fileIdentity: prepared.fileIdentity,
29565
+ operations
29566
+ });
29567
+ if (credentialFileState === "unknown") {
29568
+ throw new ProjectCreateEnvError("ENV_FILE_CLEANUP_FAILED", "unknown");
29569
+ }
29570
+ if (error instanceof ProjectCreateEnvError)
29571
+ throw error;
29572
+ throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
29573
+ }
29574
+ }
29575
+ function isRecord(candidate) {
29576
+ return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
29577
+ }
29578
+ function decodedJwtPart(encodedPart) {
29579
+ try {
29580
+ const decodedPart = JSON.parse(Buffer.from(encodedPart, "base64url").toString("utf8"));
29581
+ return isRecord(decodedPart) ? decodedPart : null;
29582
+ } catch {
29583
+ return null;
29584
+ }
29585
+ }
29586
+ function isServiceRoleKey(candidate) {
29587
+ if (typeof candidate !== "string" || !SERVICE_ROLE_KEY.test(candidate))
29588
+ return false;
29589
+ const [encodedHeader, encodedClaims] = candidate.split(".");
29590
+ const jwtHeader = decodedJwtPart(encodedHeader);
29591
+ const jwtClaims = decodedJwtPart(encodedClaims);
29592
+ return jwtHeader?.typ === "JWT" && typeof jwtHeader.alg === "string" && SERVICE_ROLE_ALGORITHMS.has(jwtHeader.alg) && jwtClaims?.role === "service_role" && jwtClaims.iss === "supabase" && typeof jwtClaims.exp === "number" && Number.isFinite(jwtClaims.exp) && jwtClaims.exp > Date.now() / 1000;
29593
+ }
29594
+ function parsedProjectApiUrl(candidate) {
29595
+ if (typeof candidate !== "string" || candidate.length > 2048)
29596
+ return null;
29597
+ try {
29598
+ return new URL(candidate);
29599
+ } catch {
29600
+ return null;
29601
+ }
29602
+ }
29603
+ function hasSafeProjectApiUrlShape(parsedUrl) {
29604
+ const loopbackHttp = parsedUrl.protocol === "http:" && ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsedUrl.hostname);
29605
+ return (parsedUrl.protocol === "https:" || loopbackHttp) && !parsedUrl.username && !parsedUrl.password && !parsedUrl.search && !parsedUrl.hash && (parsedUrl.pathname === "/" || parsedUrl.pathname === "");
29606
+ }
29607
+ function hasExpectedProjectApiOrigin(parsedUrl, projectRef, expectedApiOrigin) {
29608
+ const hostname = parsedUrl.hostname.toLowerCase();
29609
+ if (expectedApiOrigin)
29610
+ return parsedUrl.origin === expectedApiOrigin;
29611
+ return !parsedUrl.port && hostname.split(".")[0] === projectRef;
29612
+ }
29613
+ function projectApiUrl(candidate, projectRef, expectedApiOrigin) {
29614
+ const parsedUrl = parsedProjectApiUrl(candidate);
29615
+ if (!parsedUrl || !hasSafeProjectApiUrlShape(parsedUrl))
29616
+ return null;
29617
+ if (!hasExpectedProjectApiOrigin(parsedUrl, projectRef, expectedApiOrigin))
29618
+ return null;
29619
+ return parsedUrl.origin;
29620
+ }
29621
+ function parseProjectCreateIdentity(responsePayload, expectedApiOrigin) {
29622
+ if (!isRecord(responsePayload) || typeof responsePayload.ref !== "string")
29623
+ return null;
29624
+ if (!PROJECT_REF.test(responsePayload.ref) || !isRecord(responsePayload.api))
29625
+ return null;
29626
+ const apiUrl = projectApiUrl(responsePayload.api.url, responsePayload.ref, expectedApiOrigin);
29627
+ return apiUrl ? { projectRef: responsePayload.ref, apiUrl } : null;
29628
+ }
29629
+ function parseProjectCreateWithoutCredentials(responsePayload, expectedApiOrigin) {
29630
+ const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
29631
+ if (!identity || !isRecord(responsePayload))
29632
+ return null;
29633
+ return Object.hasOwn(responsePayload, "credentials") ? null : identity;
29634
+ }
29635
+ function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expectedProjectName) {
29636
+ if (!expectedApiOrigin || !expectedProjectName || !isRecord(responsePayload))
29637
+ return null;
29638
+ if (responsePayload.name !== expectedProjectName)
29639
+ return null;
29640
+ const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
29641
+ if (!identity)
29642
+ return null;
29643
+ if (!isRecord(responsePayload.credentials))
29644
+ return null;
29645
+ const serviceRoleKey = responsePayload.credentials.service_role_key;
29646
+ return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
29647
+ }
29648
+
28680
29649
  // src/shared/tools/project-cli-tools.ts
29650
+ var PROJECT_SERVICE_NAMES = [
29651
+ "postgrest",
29652
+ "gotrue",
29653
+ "storage",
29654
+ "postgresql",
29655
+ "realtime",
29656
+ "gateway"
29657
+ ];
29658
+ var PROJECT_SERVICE_CONTROL_ACTIONS = [
29659
+ "start",
29660
+ "stop",
29661
+ "restart",
29662
+ "pause",
29663
+ "resume",
29664
+ "status"
29665
+ ];
29666
+ var STUDIO_PROJECT_SERVICE_NAMES = [
29667
+ "db",
29668
+ "rest",
29669
+ "auth",
29670
+ "realtime",
29671
+ "storage"
29672
+ ];
29673
+ var STUDIO_PROJECT_SERVICE_STATUSES = [
29674
+ "ACTIVE_HEALTHY",
29675
+ "COMING_UP",
29676
+ "UNHEALTHY"
29677
+ ];
29678
+ var AUTH_RUNTIME_MANAGED_BY_OWNER = "AUTH_RUNTIME_MANAGED_BY_OWNER";
29679
+ var AUTH_SERVICE_HOST_SUFFIX = "-auth";
29680
+ var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
29681
+ var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
29682
+ var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
29683
+ var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
29684
+ var PROJECT_CREATE_OPERATION = "project.create";
29685
+ var PROJECT_ENVIRONMENTS = ["test", "production"];
29686
+ var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
29687
+ var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
29688
+ postgrest: ["start", "stop", "restart", "pause", "resume", "status"],
29689
+ gotrue: ["start", "stop", "restart"],
29690
+ storage: ["start", "stop", "restart"],
29691
+ postgresql: ["start", "stop", "restart"],
29692
+ realtime: ["start", "stop", "restart"],
29693
+ gateway: ["start", "stop", "restart"]
29694
+ };
29695
+ function projectToolResponse(text) {
29696
+ return { content: [{ type: "text", text }] };
29697
+ }
29698
+ function failedProjectServiceResponse(message) {
29699
+ return {
29700
+ content: [{ type: "text", text: `❌ ${message}` }],
29701
+ isError: true
29702
+ };
29703
+ }
29704
+ function projectCreateReceipt(payload) {
29705
+ return {
29706
+ content: [{ type: "text", text: JSON.stringify({
29707
+ schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
29708
+ ...payload
29709
+ }, null, 2) }]
29710
+ };
29711
+ }
29712
+ function projectCreateErrorReceipt(payload) {
29713
+ return { ...projectCreateReceipt(payload), isError: true };
29714
+ }
29715
+ function projectCreateFailure(code, httpStatus) {
29716
+ return projectCreateErrorReceipt({
29717
+ ok: false,
29718
+ operation: PROJECT_CREATE_OPERATION,
29719
+ error: { code, http_status: httpStatus }
29720
+ });
29721
+ }
29722
+ function projectCreateMutationFailure(response) {
29723
+ const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
29724
+ return outcomeUnknown ? projectCreateFailure("OUTCOME_UNKNOWN", response.transportError ? null : response.status) : projectCreateFailure("HTTP_ERROR", response.status);
29725
+ }
29726
+ function isCompleteApiHostname(hostname) {
29727
+ if (hostname === "localhost")
29728
+ return true;
29729
+ if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
29730
+ return hostname.split(".").every((octet) => Number(octet) <= 255);
29731
+ }
29732
+ if (hostname.startsWith("[") && hostname.endsWith("]"))
29733
+ return true;
29734
+ const labels = hostname.split(".");
29735
+ return labels.length >= 2 && labels.every((label) => DNS_LABEL.test(label));
29736
+ }
29737
+ function normalizedApiDomain(candidate) {
29738
+ const normalized = candidate?.trim().toLowerCase();
29739
+ if (!normalized || normalized.length > 253 || /[\s/@?#]/.test(normalized))
29740
+ return;
29741
+ try {
29742
+ const parsed = new URL(`https://${normalized}`);
29743
+ const exactHostname = parsed.hostname === normalized && !parsed.port;
29744
+ return exactHostname && isCompleteApiHostname(normalized) ? normalized : undefined;
29745
+ } catch {
29746
+ return;
29747
+ }
29748
+ }
29749
+ function canonicalProjectApiOrigin(hostname) {
29750
+ const loopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
29751
+ return new URL(`${loopback ? "http" : "https"}://${hostname}`).origin;
29752
+ }
29753
+ function expectedProjectApiOrigin(domain, apiDomain) {
29754
+ if (apiDomain) {
29755
+ const normalizedApi = normalizedApiDomain(apiDomain);
29756
+ return normalizedApi ? canonicalProjectApiOrigin(normalizedApi) : undefined;
29757
+ }
29758
+ const normalizedDomain = normalizedApiDomain(domain);
29759
+ if (!normalizedDomain)
29760
+ return;
29761
+ const hostname = normalizedDomain.startsWith("api.") ? normalizedDomain : `api.${normalizedDomain}`;
29762
+ return canonicalProjectApiOrigin(hostname);
29763
+ }
29764
+ function projectCreateEnvironment(candidate) {
29765
+ return candidate === "test" || candidate === "production" ? candidate : undefined;
29766
+ }
29767
+ function projectCreateFileFailure(identity, credentialFileState, code, httpStatus) {
29768
+ return projectCreateErrorReceipt({
29769
+ ok: false,
29770
+ operation: PROJECT_CREATE_OPERATION,
29771
+ project_ref: identity.projectRef,
29772
+ api_url: identity.apiUrl,
29773
+ remote_created: true,
29774
+ credential_file_state: credentialFileState,
29775
+ ...credentialFileState === "absent" ? { credentials_written: false } : {},
29776
+ retry_safe: false,
29777
+ error: { code, http_status: httpStatus }
29778
+ });
29779
+ }
29780
+ async function writeCreatedProjectEnv(preparedEnvFile, credentials, responseStatus, fileOperations) {
29781
+ try {
29782
+ await writeProjectEnvFile(preparedEnvFile, credentials, fileOperations);
29783
+ return null;
29784
+ } catch (error) {
29785
+ const code = error instanceof ProjectCreateEnvError ? error.code : "ENV_FILE_WRITE_FAILED";
29786
+ if (!(error instanceof ProjectCreateEnvError) || error.credentialFileState === "unknown") {
29787
+ return projectCreateFileFailure(credentials, "unknown", code, responseStatus);
29788
+ }
29789
+ return projectCreateFileFailure(credentials, "absent", code, responseStatus);
29790
+ }
29791
+ }
29792
+ function successfulProjectCreate(identity, preparedEnvFile) {
29793
+ return projectCreateReceipt({
29794
+ ok: true,
29795
+ operation: PROJECT_CREATE_OPERATION,
29796
+ project_ref: identity.projectRef,
29797
+ api_url: identity.apiUrl,
29798
+ credentials_written: Boolean(preparedEnvFile),
29799
+ ...preparedEnvFile ? {
29800
+ env_file: preparedEnvFile.path,
29801
+ env_file_scope: "project_application"
29802
+ } : {}
29803
+ });
29804
+ }
29805
+ function invalidProjectCreateResponse(responsePayload, responseStatus, expectedApiOrigin) {
29806
+ const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
29807
+ return identity ? projectCreateFileFailure(identity, "absent", "INVALID_RESPONSE", responseStatus) : projectCreateFailure("OUTCOME_UNKNOWN", responseStatus);
29808
+ }
29809
+ function projectCreateWithoutEnvResponse(response, expectedApiOrigin) {
29810
+ const identity = parseProjectCreateWithoutCredentials(response.data, expectedApiOrigin);
29811
+ return identity ? successfulProjectCreate(identity) : invalidProjectCreateResponse(response.data, response.status, expectedApiOrigin);
29812
+ }
29813
+ async function discardProjectEnvReservation(preparedEnvFile, response, expectedApiOrigin, fileOperations) {
29814
+ const credentialFileState = await discardPreparedProjectEnvFile(preparedEnvFile, fileOperations);
29815
+ if (credentialFileState === "absent")
29816
+ return null;
29817
+ const identity = parseProjectCreateIdentity(response.data, expectedApiOrigin);
29818
+ return identity ? projectCreateFileFailure(identity, "unknown", "ENV_FILE_CLEANUP_FAILED", response.status) : projectCreateFailure("ENV_FILE_CLEANUP_FAILED", response.status);
29819
+ }
29820
+ async function projectCreateWithEnvResponse(response, preparedEnvFile, binding, fileOperations) {
29821
+ const credentials = parseProjectCreateCredentials(response.data, binding.apiOrigin, binding.projectName);
29822
+ if (!credentials) {
29823
+ const cleanupFailure = await discardProjectEnvReservation(preparedEnvFile, response, binding.apiOrigin, fileOperations);
29824
+ return cleanupFailure ?? invalidProjectCreateResponse(response.data, response.status, binding.apiOrigin);
29825
+ }
29826
+ const writeFailure = await writeCreatedProjectEnv(preparedEnvFile, credentials, response.status, fileOperations);
29827
+ return writeFailure ?? successfulProjectCreate(credentials, preparedEnvFile);
29828
+ }
29829
+ async function projectCreateResponse(response, preparedEnvFile, binding, fileOperations) {
29830
+ if (!response.ok || response.status !== 201) {
29831
+ if (preparedEnvFile) {
29832
+ const cleanupFailure = await discardProjectEnvReservation(preparedEnvFile, response, binding.apiOrigin, fileOperations);
29833
+ if (cleanupFailure)
29834
+ return cleanupFailure;
29835
+ }
29836
+ return response.ok ? projectCreateFailure("OUTCOME_UNKNOWN", response.status) : projectCreateMutationFailure(response);
29837
+ }
29838
+ return preparedEnvFile ? projectCreateWithEnvResponse(response, preparedEnvFile, binding, fileOperations) : projectCreateWithoutEnvResponse(response, binding.apiOrigin);
29839
+ }
29840
+ function failedProjectServiceHttpResponse(response) {
29841
+ if (response.status === 409 && isRecordPayload(response.data) && response.data.code === AUTH_RUNTIME_MANAGED_BY_OWNER && typeof response.data.authority_project_ref === "string" && SAFE_AUTHORITY_PROJECT_REF.test(response.data.authority_project_ref)) {
29842
+ const ownerBoundary = {
29843
+ status: response.status,
29844
+ code: AUTH_RUNTIME_MANAGED_BY_OWNER,
29845
+ authority_project_ref: response.data.authority_project_ref
29846
+ };
29847
+ return failedProjectServiceResponse(JSON.stringify(ownerBoundary));
29848
+ }
29849
+ return failedProjectServiceResponse(`Failed (${response.status})`);
29850
+ }
29851
+ function isRecordPayload(payload) {
29852
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload);
29853
+ }
29854
+ function isStudioProjectServiceName(name) {
29855
+ return typeof name === "string" && STUDIO_PROJECT_SERVICE_NAMES.some((allowedName) => name === allowedName);
29856
+ }
29857
+ function hasValidStudioServiceHealth(serviceId, status, healthy) {
29858
+ if (status === "INACTIVE")
29859
+ return serviceId === "auth" && healthy === false;
29860
+ const knownStatus = STUDIO_PROJECT_SERVICE_STATUSES.some((allowedStatus) => status === allowedStatus);
29861
+ return knownStatus && healthy === (status === "ACTIVE_HEALTHY");
29862
+ }
29863
+ function hasValidStudioServiceHost(serviceId, projectRef, hostIds) {
29864
+ if (!Array.isArray(hostIds) || hostIds.length !== 1 || typeof hostIds[0] !== "string")
29865
+ return false;
29866
+ if (serviceId === "auth") {
29867
+ if (!hostIds[0].endsWith(AUTH_SERVICE_HOST_SUFFIX))
29868
+ return false;
29869
+ return SAFE_AUTHORITY_PROJECT_REF.test(hostIds[0].slice(0, -AUTH_SERVICE_HOST_SUFFIX.length));
29870
+ }
29871
+ return hostIds[0] === `${projectRef}-${serviceId}`;
29872
+ }
29873
+ function isProjectServiceStatus(payload, projectRef) {
29874
+ if (!isRecordPayload(payload))
29875
+ return false;
29876
+ if (!isStudioProjectServiceName(payload.id) || payload.name !== payload.id)
29877
+ return false;
29878
+ return hasValidStudioServiceHealth(payload.id, payload.status, payload.healthy) && hasValidStudioServiceHost(payload.id, projectRef, payload.service_host_ids);
29879
+ }
29880
+ function projectServiceStatusOutput(status) {
29881
+ return {
29882
+ id: status.id,
29883
+ name: status.name,
29884
+ status: status.status,
29885
+ healthy: status.healthy,
29886
+ service_host_ids: [status.service_host_ids[0]]
29887
+ };
29888
+ }
29889
+ function projectServicesResponse(projectRef, response) {
29890
+ if (!response.ok)
29891
+ return failedProjectServiceHttpResponse(response);
29892
+ if (!SAFE_PROJECT_REF2.test(projectRef) || !Array.isArray(response.data) || response.data.length !== 5) {
29893
+ return failedProjectServiceResponse("Project service inventory response is invalid");
29894
+ }
29895
+ if (!response.data.every((service) => isProjectServiceStatus(service, projectRef))) {
29896
+ return failedProjectServiceResponse("Project service inventory response is invalid");
29897
+ }
29898
+ const serviceIds = new Set(response.data.map((service) => service.id));
29899
+ if (serviceIds.size !== STUDIO_PROJECT_SERVICE_NAMES.length) {
29900
+ return failedProjectServiceResponse("Project service inventory response is invalid");
29901
+ }
29902
+ const services = response.data.map(projectServiceStatusOutput);
29903
+ return projectToolResponse(JSON.stringify({ project_ref: projectRef, services }, null, 2));
29904
+ }
29905
+ function supportsProjectServiceAction(service, action) {
29906
+ return SUPPORTED_PROJECT_SERVICE_ACTIONS[service].includes(action);
29907
+ }
29908
+ function projectServiceReceiptError(receipt, requestedService, requestedAction) {
29909
+ if (!isRecordPayload(receipt))
29910
+ return "Project service control response is invalid";
29911
+ if (receipt.service !== requestedService || receipt.action !== requestedAction) {
29912
+ return "Project service control response does not match the request";
29913
+ }
29914
+ if (receipt.success === false)
29915
+ return "Project service control failed";
29916
+ if (receipt.success !== true || typeof receipt.message !== "string" || receipt.message.length > MAX_SERVICE_CONTROL_MESSAGE_LENGTH) {
29917
+ return "Project service control response is invalid";
29918
+ }
29919
+ return null;
29920
+ }
29921
+ function projectServiceControlResponse(projectRef, requestedService, requestedAction, response) {
29922
+ if (!response.ok)
29923
+ return failedProjectServiceHttpResponse(response);
29924
+ const receiptError = projectServiceReceiptError(response.data, requestedService, requestedAction);
29925
+ if (receiptError)
29926
+ return failedProjectServiceResponse(receiptError);
29927
+ return projectToolResponse(JSON.stringify({
29928
+ project_ref: projectRef,
29929
+ service: requestedService,
29930
+ action: requestedAction,
29931
+ success: true
29932
+ }, null, 2));
29933
+ }
28681
29934
  var formatTasks = (data) => {
28682
29935
  if (!Array.isArray(data))
28683
29936
  return JSON.stringify(data, null, 2);
@@ -28720,8 +29973,9 @@ function resolveRef(refFromArgs, defaultRef) {
28720
29973
  throw new Error("'ref' is required for this action");
28721
29974
  return ref;
28722
29975
  }
28723
- function registerAdminProjectCliTools(server, http) {
28724
- server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks", {
29976
+ function registerAdminProjectCliTools(server, http, options = {}) {
29977
+ const fileOperations = options.projectEnvFileOperations;
29978
+ server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, service_control", {
28725
29979
  action: withDescription(stringEnum([
28726
29980
  "list",
28727
29981
  "create",
@@ -28735,9 +29989,11 @@ function registerAdminProjectCliTools(server, http) {
28735
29989
  "api_keys",
28736
29990
  "health",
28737
29991
  "logs",
28738
- "tasks"
29992
+ "tasks",
29993
+ "services",
29994
+ "service_control"
28739
29995
  ]), "Action to perform"),
28740
- ref: optional(Type.String(), "Project ref (required for most actions except 'list' and 'create')"),
29996
+ ref: optional(Type.String(), "[*] Project ref (required for most actions except 'list' and 'create')"),
28741
29997
  name: optional(Type.String(), "[create] Project name"),
28742
29998
  region: optional(Type.String(), "[create] Region (default: local)"),
28743
29999
  organization_id: optional(Type.String(), "[create] Organization ID"),
@@ -28745,8 +30001,12 @@ function registerAdminProjectCliTools(server, http) {
28745
30001
  api_domain: optional(Type.String(), "[create] Explicit API domain"),
28746
30002
  auth_domain: optional(Type.String(), "[create] Explicit Auth/OIDC domain"),
28747
30003
  studio_domain: optional(Type.String(), "[create] Explicit Studio domain"),
30004
+ env_file: optional(Type.String(), "[create] Linux-only absolute new application env path for parent-bound service-role delivery (0600)"),
30005
+ environment: optional(stringEnum(PROJECT_ENVIRONMENTS), "[create] Required with env_file; application credential environment"),
28748
30006
  settings: optional(Type.Record(Type.String(), Type.Unknown()), "[update_settings] Config fields to update"),
28749
- log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service")
30007
+ log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service"),
30008
+ service: optional(stringEnum(PROJECT_SERVICE_NAMES), "[service_control] Canonical service name"),
30009
+ service_action: optional(stringEnum(PROJECT_SERVICE_CONTROL_ACTIONS), "[service_control] Supported action for the selected service")
28750
30010
  }, async ({
28751
30011
  action,
28752
30012
  ref,
@@ -28757,8 +30017,12 @@ function registerAdminProjectCliTools(server, http) {
28757
30017
  api_domain,
28758
30018
  auth_domain,
28759
30019
  studio_domain,
30020
+ env_file,
30021
+ environment,
28760
30022
  settings,
28761
- log_type
30023
+ log_type,
30024
+ service,
30025
+ service_action
28762
30026
  }) => {
28763
30027
  let text;
28764
30028
  switch (action) {
@@ -28768,6 +30032,23 @@ function registerAdminProjectCliTools(server, http) {
28768
30032
  case "create": {
28769
30033
  if (!name)
28770
30034
  throw new Error("'name' is required for create");
30035
+ const boundApiOrigin = expectedProjectApiOrigin(domain, api_domain);
30036
+ if (env_file && !boundApiOrigin) {
30037
+ return projectCreateFailure("API_DOMAIN_BINDING_REQUIRED", null);
30038
+ }
30039
+ let preparedEnvFile;
30040
+ if (env_file) {
30041
+ const boundEnvironment = projectCreateEnvironment(environment);
30042
+ if (!boundEnvironment) {
30043
+ return projectCreateFailure("ENVIRONMENT_BINDING_REQUIRED", null);
30044
+ }
30045
+ try {
30046
+ preparedEnvFile = await prepareProjectEnvFile(env_file, boundEnvironment, fileOperations);
30047
+ } catch (error) {
30048
+ const code = error instanceof ProjectCreateEnvError ? error.code : "ENV_FILE_PATH_INVALID";
30049
+ return projectCreateFailure(code, null);
30050
+ }
30051
+ }
28771
30052
  const createRequest = {
28772
30053
  name,
28773
30054
  region: region || "local",
@@ -28781,8 +30062,9 @@ function registerAdminProjectCliTools(server, http) {
28781
30062
  createRequest.auth_domain = auth_domain;
28782
30063
  if (studio_domain)
28783
30064
  createRequest.studio_domain = studio_domain;
28784
- text = ok(await http.post("/v1/projects", createRequest));
28785
- break;
30065
+ if (preparedEnvFile)
30066
+ createRequest.credential_delivery = "response";
30067
+ return projectCreateResponse(await http.post("/v1/projects", createRequest), preparedEnvFile, { projectName: name, apiOrigin: boundApiOrigin }, fileOperations);
28786
30068
  }
28787
30069
  case "get":
28788
30070
  text = ok(await http.get(`/v1/projects/${resolveRef(ref)}`));
@@ -28834,6 +30116,24 @@ function registerAdminProjectCliTools(server, http) {
28834
30116
  text = res.ok ? formatTasks(res.data) : `❌ Failed (${res.status})`;
28835
30117
  break;
28836
30118
  }
30119
+ case "services": {
30120
+ const resolvedRef = resolveRef(ref);
30121
+ return projectServicesResponse(resolvedRef, await http.get(`/v1/projects/${encodeURIComponent(resolvedRef)}/services`));
30122
+ }
30123
+ case "service_control": {
30124
+ const resolvedRef = resolveRef(ref);
30125
+ if (!service)
30126
+ throw new Error("'service' is required for service_control");
30127
+ if (!service_action)
30128
+ throw new Error("'service_action' is required for service_control");
30129
+ if (!supportsProjectServiceAction(service, service_action)) {
30130
+ throw new Error(`'${service_action}' is not supported for service '${service}'`);
30131
+ }
30132
+ const encodedRef = encodeURIComponent(resolvedRef);
30133
+ const encodedService = encodeURIComponent(service);
30134
+ const encodedAction = encodeURIComponent(service_action);
30135
+ return projectServiceControlResponse(resolvedRef, service, service_action, await http.post(`/v1/projects/${encodedRef}/services/${encodedService}/${encodedAction}`));
30136
+ }
28837
30137
  default:
28838
30138
  text = `❌ Unknown action: ${action}`;
28839
30139
  }
@@ -29172,7 +30472,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
29172
30472
  // package.json
29173
30473
  var package_default = {
29174
30474
  name: "@supacloud/admin",
29175
- version: "0.8.2",
30475
+ version: "0.10.0",
29176
30476
  description: "Platform administration CLI for SupaCloud operators",
29177
30477
  type: "module",
29178
30478
  main: "./dist/index.js",
@@ -29213,31 +30513,6 @@ var package_default = {
29213
30513
  };
29214
30514
 
29215
30515
  // src/index.ts
29216
- var platformActionSchema = stringEnum([
29217
- "metrics",
29218
- "list_backups",
29219
- "create_backup",
29220
- "network",
29221
- "update_network",
29222
- "list_orgs",
29223
- "get_org"
29224
- ]);
29225
- var sshActionSchema = stringEnum([
29226
- "ping",
29227
- "setup",
29228
- "install",
29229
- "upgrade",
29230
- "versions",
29231
- "diagnose",
29232
- "exec",
29233
- "troubleshoot",
29234
- "container_logs",
29235
- "tenant_manage",
29236
- "tenant_list",
29237
- "tenant_inspect",
29238
- "tenant_diagnose",
29239
- "tenant_migrate"
29240
- ]);
29241
30516
  function captureTools(register) {
29242
30517
  const tools = {};
29243
30518
  const server = {
@@ -29252,6 +30527,31 @@ function captureTools(register) {
29252
30527
  register(server);
29253
30528
  return tools;
29254
30529
  }
30530
+ function unavailableTool(schema, message) {
30531
+ return {
30532
+ schema,
30533
+ callback: async () => ({
30534
+ isError: true,
30535
+ content: [{ type: "text", text: message }]
30536
+ })
30537
+ };
30538
+ }
30539
+ function unavailableAdminSchemas() {
30540
+ const schemaOnlyHttp = {};
30541
+ return {
30542
+ project: captureTools((server) => registerAdminProjectCliTools(server, schemaOnlyHttp)).project.schema,
30543
+ platform: captureTools((server) => registerAdvancedTools(server, schemaOnlyHttp)).platform.schema,
30544
+ gateway: captureTools((server) => registerGatewayTools(server, schemaOnlyHttp)).gateway.schema,
30545
+ ssh: captureTools((server) => registerSshTools(server, {})).ssh.schema
30546
+ };
30547
+ }
30548
+ function registerUnavailableAdminTools(tools) {
30549
+ const schemas = unavailableAdminSchemas();
30550
+ tools.project = unavailableTool(schemas.project, "⚠️ Project lifecycle commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
30551
+ tools.platform = unavailableTool(schemas.platform, "⚠️ Platform commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
30552
+ tools.ssh = unavailableTool(schemas.ssh, "⚠️ SSH commands require SUPACLOUD_HOST, SSH credentials, and SUPACLOUD_SSH_HOST_FINGERPRINT.");
30553
+ tools.gateway = unavailableTool(schemas.gateway, "⚠️ Gateway / Caddy commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN (admin privileges).");
30554
+ }
29255
30555
  function printHelp(context = resolveSupaCloudContext()) {
29256
30556
  console.error(`
29257
30557
  ╔═══════════════════════════════════════════════════════════╗
@@ -29261,11 +30561,20 @@ function printHelp(context = resolveSupaCloudContext()) {
29261
30561
 
29262
30562
  USAGE
29263
30563
 
29264
- supacloud-admin <module> <action> [--flags]
29265
- supacloud-admin status
30564
+ supacloud-admin [global flags] <module> <action> [--flags]
30565
+ supacloud-admin [global flags] status
29266
30566
  supacloud-admin --help
29267
30567
  supacloud-admin --version
29268
30568
 
30569
+ GLOBAL FLAGS
30570
+
30571
+ --env <name> Load .env.supacloud.<name> from the current directory.
30572
+ --env-file <path> Load an exact file that declares SUPACLOUD_ENV.
30573
+ --confirm-production <target> Confirm the exact production project or platform target.
30574
+
30575
+ Global flags may appear before or after the command. --env and --env-file are
30576
+ mutually exclusive, and a selected source is never mixed with another source.
30577
+
29269
30578
  EXPECTED CONTEXT
29270
30579
 
29271
30580
  Platform commands typically rely on:
@@ -29274,18 +30583,28 @@ EXPECTED CONTEXT
29274
30583
  SUPACLOUD_SSH_HOST_FINGERPRINT=SHA256:...
29275
30584
  SUPACLOUD_API_URL
29276
30585
  SUPACLOUD_API_TOKEN
30586
+ SUPACLOUD_ENV
30587
+ SUPACLOUD_READ_ONLY
29277
30588
 
30589
+ Environment: ${context.environment || "(unclassified)"}
30590
+ Context source: ${context.source}${context.sourcePath ? ` (${context.sourcePath})` : ""}
29278
30591
  Detected host: ${context.host || "(none)"}
29279
30592
  Detected API URL: ${context.apiUrl || "(none)"}
29280
30593
 
30594
+ SUPACLOUD_READ_ONLY=true blocks every remote write. Production project writes
30595
+ require the exact project ref. Production writes without a project ref use
30596
+ platform:<API host> or host:<SSH host[:port]> as the confirmation target.
30597
+
29281
30598
  EXAMPLES
29282
30599
 
29283
30600
  supacloud-admin status
29284
30601
  supacloud-admin ssh ping
29285
30602
  supacloud-admin ssh versions
29286
30603
  supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
29287
- supacloud-admin project create --name my-app --domain example.com
30604
+ supacloud-admin project create --name my-app --domain example.com --env_file /secure/path/.env.project-credentials.test --environment test
29288
30605
  supacloud-admin project list
30606
+ supacloud-admin project services --ref abc123
30607
+ supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
29289
30608
  supacloud-admin platform metrics
29290
30609
  supacloud-admin gateway routes --ref abc123
29291
30610
  supacloud-admin gateway upsert_route --ref abc123 --route_id webhook --hosts "api.example.com" --paths "/webhook/*" --upstream 10.0.0.5:8080
@@ -29293,7 +30612,18 @@ EXAMPLES
29293
30612
  supacloud-admin gateway rebuild --ref abc123 --clean
29294
30613
  `);
29295
30614
  }
29296
- function createAdminTools(context = resolveSupaCloudContext()) {
30615
+ function authorizedToolMap(tools, context, confirmProduction) {
30616
+ validateExecutionPolicyCoverage(tools);
30617
+ for (const [moduleName, tool] of Object.entries(tools)) {
30618
+ const callback = tool.callback;
30619
+ tool.callback = async (args) => {
30620
+ authorizeExecution(moduleName, args, { context, confirmProduction });
30621
+ return callback(args);
30622
+ };
30623
+ }
30624
+ return tools;
30625
+ }
30626
+ function createAdminTools(context = resolveSupaCloudContext(), confirmProduction) {
29297
30627
  const tools = {
29298
30628
  status: {
29299
30629
  schema: {},
@@ -29308,65 +30638,17 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29308
30638
  hasApiToken: Boolean(context.apiToken),
29309
30639
  hasSshKey: Boolean(context.sshKey),
29310
30640
  hasSshHostFingerprint: Boolean(context.sshHostFingerprint),
29311
- source: context.source
30641
+ environment: context.environment || null,
30642
+ production: context.production,
30643
+ readOnly: context.readOnly,
30644
+ source: { kind: context.source, path: context.sourcePath }
29312
30645
  }, null, 2)
29313
30646
  }
29314
30647
  ]
29315
30648
  })
29316
30649
  }
29317
30650
  };
29318
- const registerAdminHelp = () => {
29319
- const projectHelpTool = captureTools((server) => registerAdminProjectCliTools(server, {})).project;
29320
- tools.project = {
29321
- schema: projectHelpTool.schema,
29322
- callback: async () => ({
29323
- isError: true,
29324
- content: [
29325
- {
29326
- type: "text",
29327
- text: "⚠️ Project lifecycle commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN."
29328
- }
29329
- ]
29330
- })
29331
- };
29332
- tools.platform = {
29333
- schema: { action: platformActionSchema },
29334
- callback: async () => ({
29335
- isError: true,
29336
- content: [
29337
- {
29338
- type: "text",
29339
- text: "⚠️ Platform commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN."
29340
- }
29341
- ]
29342
- })
29343
- };
29344
- tools.ssh = {
29345
- schema: { action: sshActionSchema },
29346
- callback: async () => ({
29347
- isError: true,
29348
- content: [
29349
- {
29350
- type: "text",
29351
- text: "⚠️ SSH commands require SUPACLOUD_HOST, SSH credentials, and SUPACLOUD_SSH_HOST_FINGERPRINT."
29352
- }
29353
- ]
29354
- })
29355
- };
29356
- tools.gateway = {
29357
- schema: { action: Type.String() },
29358
- callback: async () => ({
29359
- isError: true,
29360
- content: [
29361
- {
29362
- type: "text",
29363
- text: "⚠️ Gateway / Caddy commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN (admin privileges)."
29364
- }
29365
- ]
29366
- })
29367
- };
29368
- };
29369
- registerAdminHelp();
30651
+ registerUnavailableAdminTools(tools);
29370
30652
  if (context.host && context.sshHostFingerprint) {
29371
30653
  try {
29372
30654
  const ssh = new SshTransport({
@@ -29381,7 +30663,7 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29381
30663
  } catch (error) {
29382
30664
  const message = error instanceof Error ? error.message : String(error);
29383
30665
  tools.ssh = {
29384
- schema: { action: sshActionSchema },
30666
+ schema: tools.ssh.schema,
29385
30667
  callback: async () => ({
29386
30668
  isError: true,
29387
30669
  content: [{
@@ -29393,7 +30675,7 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29393
30675
  }
29394
30676
  } else if (context.host) {
29395
30677
  tools.ssh = {
29396
- schema: { action: sshActionSchema },
30678
+ schema: tools.ssh.schema,
29397
30679
  callback: async () => ({
29398
30680
  isError: true,
29399
30681
  content: [{
@@ -29444,31 +30726,38 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29444
30726
  })
29445
30727
  };
29446
30728
  }
29447
- return tools;
30729
+ return authorizedToolMap(tools, context, confirmProduction);
29448
30730
  }
29449
30731
  async function main() {
29450
- const args = process.argv.slice(2);
29451
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
29452
- printHelp();
29453
- process.exit(0);
29454
- }
29455
- if (args.length === 1 && args[0] === "--version") {
30732
+ const rawArgs = process.argv.slice(2);
30733
+ if (rawArgs.length === 1 && rawArgs[0] === "--version") {
29456
30734
  console.log(package_default.version);
29457
30735
  return;
29458
30736
  }
29459
- const cliTools = createAdminTools();
30737
+ const globalOptions = parseGlobalAdminOptions(rawArgs);
30738
+ const args = globalOptions.args;
30739
+ const context = resolveSupaCloudContext(process.env, process.cwd(), {
30740
+ environmentName: globalOptions.environmentName,
30741
+ envFile: globalOptions.envFile
30742
+ });
30743
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
30744
+ printHelp(context);
30745
+ process.exitCode = 0;
30746
+ return;
30747
+ }
30748
+ const cliTools = createAdminTools(context, globalOptions.confirmProduction);
29460
30749
  if (args.length === 1 && cliTools[args[0]]) {
29461
- const result = await cliTools[args[0]].callback({});
29462
- if (result?.content && Array.isArray(result.content)) {
29463
- for (const chunk of result.content) {
30750
+ const toolResponse = await cliTools[args[0]].callback({});
30751
+ if (toolResponse?.content && Array.isArray(toolResponse.content)) {
30752
+ for (const chunk of toolResponse.content) {
29464
30753
  if (chunk.type === "text") {
29465
30754
  console.log(chunk.text);
29466
30755
  }
29467
30756
  }
29468
30757
  } else {
29469
- console.log(JSON.stringify(result, null, 2));
30758
+ console.log(JSON.stringify(toolResponse, null, 2));
29470
30759
  }
29471
- if (cliToolResultIsError(result))
30760
+ if (cliToolResultIsError(toolResponse))
29472
30761
  process.exitCode = 1;
29473
30762
  return;
29474
30763
  }
@@ -29478,7 +30767,7 @@ function isDirectRun() {
29478
30767
  if (!process.argv[1])
29479
30768
  return false;
29480
30769
  try {
29481
- return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve2(process.argv[1]));
30770
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve3(process.argv[1]));
29482
30771
  } catch {
29483
30772
  return false;
29484
30773
  }