@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.
- package/README.md +129 -1
- package/dist/index.js +1495 -206
- 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
|
-
|
|
25453
|
-
|
|
25454
|
-
|
|
25455
|
-
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
|
|
25462
|
-
|
|
25463
|
-
|
|
25464
|
-
|
|
25465
|
-
|
|
25466
|
-
|
|
25467
|
-
|
|
25468
|
-
}
|
|
25469
|
-
}
|
|
25470
|
-
|
|
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
|
|
25555
|
+
return environment;
|
|
25473
25556
|
}
|
|
25474
|
-
function
|
|
25475
|
-
|
|
25476
|
-
|
|
25477
|
-
|
|
25478
|
-
|
|
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
|
-
|
|
25481
|
-
|
|
25482
|
-
|
|
25483
|
-
|
|
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(
|
|
25496
|
-
const trimmed =
|
|
25570
|
+
function normalizeUrl(urlCandidate) {
|
|
25571
|
+
const trimmed = urlCandidate.trim();
|
|
25497
25572
|
if (!trimmed)
|
|
25498
25573
|
return "";
|
|
25499
25574
|
try {
|
|
25500
|
-
|
|
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
|
|
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(
|
|
25608
|
+
return new URL(urlCandidate).hostname;
|
|
25508
25609
|
} catch {
|
|
25509
25610
|
return "";
|
|
25510
25611
|
}
|
|
25511
25612
|
}
|
|
25512
|
-
function
|
|
25513
|
-
const normalized = normalizeUrl(
|
|
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
|
|
25528
|
-
if (
|
|
25529
|
-
url.hostname = `studio-${
|
|
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
|
|
25535
|
-
const
|
|
25536
|
-
const
|
|
25537
|
-
const explicitApiUrl =
|
|
25538
|
-
const
|
|
25539
|
-
const
|
|
25540
|
-
|
|
25541
|
-
|
|
25542
|
-
|
|
25543
|
-
|
|
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
|
|
25546
|
-
|
|
25547
|
-
|
|
25548
|
-
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
|
|
25554
|
-
|
|
25555
|
-
|
|
25556
|
-
|
|
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
|
-
|
|
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(),
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
28648
|
-
break;
|
|
29206
|
+
return listPhysicalBackups(http, ref);
|
|
28649
29207
|
case "create_backup": {
|
|
28650
29208
|
need("ref", ref);
|
|
28651
|
-
|
|
28652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28785
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
29451
|
-
if (
|
|
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
|
|
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
|
|
29462
|
-
if (
|
|
29463
|
-
for (const chunk of
|
|
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(
|
|
30758
|
+
console.log(JSON.stringify(toolResponse, null, 2));
|
|
29470
30759
|
}
|
|
29471
|
-
if (cliToolResultIsError(
|
|
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(
|
|
30770
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve3(process.argv[1]));
|
|
29482
30771
|
} catch {
|
|
29483
30772
|
return false;
|
|
29484
30773
|
}
|