@supacloud/admin 0.9.0 → 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 +110 -1
  2. package/dist/index.js +1320 -201
  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
25569
  }
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
- }
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);
25544
25675
  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
- ])
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);
25753
+ return {
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,6 +29233,419 @@ ${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
28681
29650
  var PROJECT_SERVICE_NAMES = [
28682
29651
  "postgrest",
@@ -28711,6 +29680,10 @@ var AUTH_SERVICE_HOST_SUFFIX = "-auth";
28711
29680
  var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
28712
29681
  var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
28713
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])?$/;
28714
29687
  var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
28715
29688
  postgrest: ["start", "stop", "restart", "pause", "resume", "status"],
28716
29689
  gotrue: ["start", "stop", "restart"],
@@ -28728,6 +29701,142 @@ function failedProjectServiceResponse(message) {
28728
29701
  isError: true
28729
29702
  };
28730
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
+ }
28731
29840
  function failedProjectServiceHttpResponse(response) {
28732
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)) {
28733
29842
  const ownerBoundary = {
@@ -28864,7 +29973,8 @@ function resolveRef(refFromArgs, defaultRef) {
28864
29973
  throw new Error("'ref' is required for this action");
28865
29974
  return ref;
28866
29975
  }
28867
- function registerAdminProjectCliTools(server, http) {
29976
+ function registerAdminProjectCliTools(server, http, options = {}) {
29977
+ const fileOperations = options.projectEnvFileOperations;
28868
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", {
28869
29979
  action: withDescription(stringEnum([
28870
29980
  "list",
@@ -28891,6 +30001,8 @@ function registerAdminProjectCliTools(server, http) {
28891
30001
  api_domain: optional(Type.String(), "[create] Explicit API domain"),
28892
30002
  auth_domain: optional(Type.String(), "[create] Explicit Auth/OIDC domain"),
28893
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"),
28894
30006
  settings: optional(Type.Record(Type.String(), Type.Unknown()), "[update_settings] Config fields to update"),
28895
30007
  log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service"),
28896
30008
  service: optional(stringEnum(PROJECT_SERVICE_NAMES), "[service_control] Canonical service name"),
@@ -28905,6 +30017,8 @@ function registerAdminProjectCliTools(server, http) {
28905
30017
  api_domain,
28906
30018
  auth_domain,
28907
30019
  studio_domain,
30020
+ env_file,
30021
+ environment,
28908
30022
  settings,
28909
30023
  log_type,
28910
30024
  service,
@@ -28918,6 +30032,23 @@ function registerAdminProjectCliTools(server, http) {
28918
30032
  case "create": {
28919
30033
  if (!name)
28920
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
+ }
28921
30052
  const createRequest = {
28922
30053
  name,
28923
30054
  region: region || "local",
@@ -28931,8 +30062,9 @@ function registerAdminProjectCliTools(server, http) {
28931
30062
  createRequest.auth_domain = auth_domain;
28932
30063
  if (studio_domain)
28933
30064
  createRequest.studio_domain = studio_domain;
28934
- text = ok(await http.post("/v1/projects", createRequest));
28935
- break;
30065
+ if (preparedEnvFile)
30066
+ createRequest.credential_delivery = "response";
30067
+ return projectCreateResponse(await http.post("/v1/projects", createRequest), preparedEnvFile, { projectName: name, apiOrigin: boundApiOrigin }, fileOperations);
28936
30068
  }
28937
30069
  case "get":
28938
30070
  text = ok(await http.get(`/v1/projects/${resolveRef(ref)}`));
@@ -29340,7 +30472,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
29340
30472
  // package.json
29341
30473
  var package_default = {
29342
30474
  name: "@supacloud/admin",
29343
- version: "0.9.0",
30475
+ version: "0.10.0",
29344
30476
  description: "Platform administration CLI for SupaCloud operators",
29345
30477
  type: "module",
29346
30478
  main: "./dist/index.js",
@@ -29381,31 +30513,6 @@ var package_default = {
29381
30513
  };
29382
30514
 
29383
30515
  // src/index.ts
29384
- var platformActionSchema = stringEnum([
29385
- "metrics",
29386
- "list_backups",
29387
- "create_backup",
29388
- "network",
29389
- "update_network",
29390
- "list_orgs",
29391
- "get_org"
29392
- ]);
29393
- var sshActionSchema = stringEnum([
29394
- "ping",
29395
- "setup",
29396
- "install",
29397
- "upgrade",
29398
- "versions",
29399
- "diagnose",
29400
- "exec",
29401
- "troubleshoot",
29402
- "container_logs",
29403
- "tenant_manage",
29404
- "tenant_list",
29405
- "tenant_inspect",
29406
- "tenant_diagnose",
29407
- "tenant_migrate"
29408
- ]);
29409
30516
  function captureTools(register) {
29410
30517
  const tools = {};
29411
30518
  const server = {
@@ -29420,6 +30527,31 @@ function captureTools(register) {
29420
30527
  register(server);
29421
30528
  return tools;
29422
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
+ }
29423
30555
  function printHelp(context = resolveSupaCloudContext()) {
29424
30556
  console.error(`
29425
30557
  ╔═══════════════════════════════════════════════════════════╗
@@ -29429,11 +30561,20 @@ function printHelp(context = resolveSupaCloudContext()) {
29429
30561
 
29430
30562
  USAGE
29431
30563
 
29432
- supacloud-admin <module> <action> [--flags]
29433
- supacloud-admin status
30564
+ supacloud-admin [global flags] <module> <action> [--flags]
30565
+ supacloud-admin [global flags] status
29434
30566
  supacloud-admin --help
29435
30567
  supacloud-admin --version
29436
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
+
29437
30578
  EXPECTED CONTEXT
29438
30579
 
29439
30580
  Platform commands typically rely on:
@@ -29442,17 +30583,25 @@ EXPECTED CONTEXT
29442
30583
  SUPACLOUD_SSH_HOST_FINGERPRINT=SHA256:...
29443
30584
  SUPACLOUD_API_URL
29444
30585
  SUPACLOUD_API_TOKEN
30586
+ SUPACLOUD_ENV
30587
+ SUPACLOUD_READ_ONLY
29445
30588
 
30589
+ Environment: ${context.environment || "(unclassified)"}
30590
+ Context source: ${context.source}${context.sourcePath ? ` (${context.sourcePath})` : ""}
29446
30591
  Detected host: ${context.host || "(none)"}
29447
30592
  Detected API URL: ${context.apiUrl || "(none)"}
29448
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
+
29449
30598
  EXAMPLES
29450
30599
 
29451
30600
  supacloud-admin status
29452
30601
  supacloud-admin ssh ping
29453
30602
  supacloud-admin ssh versions
29454
30603
  supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
29455
- 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
29456
30605
  supacloud-admin project list
29457
30606
  supacloud-admin project services --ref abc123
29458
30607
  supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
@@ -29463,7 +30612,18 @@ EXAMPLES
29463
30612
  supacloud-admin gateway rebuild --ref abc123 --clean
29464
30613
  `);
29465
30614
  }
29466
- 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) {
29467
30627
  const tools = {
29468
30628
  status: {
29469
30629
  schema: {},
@@ -29478,65 +30638,17 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29478
30638
  hasApiToken: Boolean(context.apiToken),
29479
30639
  hasSshKey: Boolean(context.sshKey),
29480
30640
  hasSshHostFingerprint: Boolean(context.sshHostFingerprint),
29481
- source: context.source
30641
+ environment: context.environment || null,
30642
+ production: context.production,
30643
+ readOnly: context.readOnly,
30644
+ source: { kind: context.source, path: context.sourcePath }
29482
30645
  }, null, 2)
29483
30646
  }
29484
30647
  ]
29485
30648
  })
29486
30649
  }
29487
30650
  };
29488
- const registerAdminHelp = () => {
29489
- const projectHelpTool = captureTools((server) => registerAdminProjectCliTools(server, {})).project;
29490
- tools.project = {
29491
- schema: projectHelpTool.schema,
29492
- callback: async () => ({
29493
- isError: true,
29494
- content: [
29495
- {
29496
- type: "text",
29497
- text: "⚠️ Project lifecycle commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN."
29498
- }
29499
- ]
29500
- })
29501
- };
29502
- tools.platform = {
29503
- schema: { action: platformActionSchema },
29504
- callback: async () => ({
29505
- isError: true,
29506
- content: [
29507
- {
29508
- type: "text",
29509
- text: "⚠️ Platform commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN."
29510
- }
29511
- ]
29512
- })
29513
- };
29514
- tools.ssh = {
29515
- schema: { action: sshActionSchema },
29516
- callback: async () => ({
29517
- isError: true,
29518
- content: [
29519
- {
29520
- type: "text",
29521
- text: "⚠️ SSH commands require SUPACLOUD_HOST, SSH credentials, and SUPACLOUD_SSH_HOST_FINGERPRINT."
29522
- }
29523
- ]
29524
- })
29525
- };
29526
- tools.gateway = {
29527
- schema: { action: Type.String() },
29528
- callback: async () => ({
29529
- isError: true,
29530
- content: [
29531
- {
29532
- type: "text",
29533
- text: "⚠️ Gateway / Caddy commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN (admin privileges)."
29534
- }
29535
- ]
29536
- })
29537
- };
29538
- };
29539
- registerAdminHelp();
30651
+ registerUnavailableAdminTools(tools);
29540
30652
  if (context.host && context.sshHostFingerprint) {
29541
30653
  try {
29542
30654
  const ssh = new SshTransport({
@@ -29551,7 +30663,7 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29551
30663
  } catch (error) {
29552
30664
  const message = error instanceof Error ? error.message : String(error);
29553
30665
  tools.ssh = {
29554
- schema: { action: sshActionSchema },
30666
+ schema: tools.ssh.schema,
29555
30667
  callback: async () => ({
29556
30668
  isError: true,
29557
30669
  content: [{
@@ -29563,7 +30675,7 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29563
30675
  }
29564
30676
  } else if (context.host) {
29565
30677
  tools.ssh = {
29566
- schema: { action: sshActionSchema },
30678
+ schema: tools.ssh.schema,
29567
30679
  callback: async () => ({
29568
30680
  isError: true,
29569
30681
  content: [{
@@ -29614,31 +30726,38 @@ function createAdminTools(context = resolveSupaCloudContext()) {
29614
30726
  })
29615
30727
  };
29616
30728
  }
29617
- return tools;
30729
+ return authorizedToolMap(tools, context, confirmProduction);
29618
30730
  }
29619
30731
  async function main() {
29620
- const args = process.argv.slice(2);
29621
- if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
29622
- printHelp();
29623
- process.exit(0);
29624
- }
29625
- if (args.length === 1 && args[0] === "--version") {
30732
+ const rawArgs = process.argv.slice(2);
30733
+ if (rawArgs.length === 1 && rawArgs[0] === "--version") {
29626
30734
  console.log(package_default.version);
29627
30735
  return;
29628
30736
  }
29629
- 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);
29630
30749
  if (args.length === 1 && cliTools[args[0]]) {
29631
- const result = await cliTools[args[0]].callback({});
29632
- if (result?.content && Array.isArray(result.content)) {
29633
- 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) {
29634
30753
  if (chunk.type === "text") {
29635
30754
  console.log(chunk.text);
29636
30755
  }
29637
30756
  }
29638
30757
  } else {
29639
- console.log(JSON.stringify(result, null, 2));
30758
+ console.log(JSON.stringify(toolResponse, null, 2));
29640
30759
  }
29641
- if (cliToolResultIsError(result))
30760
+ if (cliToolResultIsError(toolResponse))
29642
30761
  process.exitCode = 1;
29643
30762
  return;
29644
30763
  }
@@ -29648,7 +30767,7 @@ function isDirectRun() {
29648
30767
  if (!process.argv[1])
29649
30768
  return false;
29650
30769
  try {
29651
- return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve2(process.argv[1]));
30770
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve3(process.argv[1]));
29652
30771
  } catch {
29653
30772
  return false;
29654
30773
  }