@supacloud/admin 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -1
- package/dist/index.js +1320 -201
- 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
25569
|
}
|
|
25487
|
-
function
|
|
25488
|
-
const
|
|
25489
|
-
if (present.size === 0)
|
|
25490
|
-
return "none";
|
|
25491
|
-
if (present.size === 1)
|
|
25492
|
-
return present.has("env") ? "env" : "dotenv";
|
|
25493
|
-
return "mixed";
|
|
25494
|
-
}
|
|
25495
|
-
function normalizeUrl(value) {
|
|
25496
|
-
const trimmed = value.trim().replace(/\/+$/, "");
|
|
25570
|
+
function normalizeUrl(urlCandidate) {
|
|
25571
|
+
const trimmed = urlCandidate.trim();
|
|
25497
25572
|
if (!trimmed)
|
|
25498
25573
|
return "";
|
|
25499
25574
|
try {
|
|
25500
|
-
|
|
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);
|
|
25544
25675
|
return {
|
|
25545
|
-
|
|
25546
|
-
|
|
25547
|
-
|
|
25548
|
-
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
|
|
25553
|
-
|
|
25554
|
-
|
|
25555
|
-
|
|
25556
|
-
|
|
25557
|
-
|
|
25558
|
-
|
|
25559
|
-
|
|
25560
|
-
|
|
25676
|
+
environment: selectedEnvironment,
|
|
25677
|
+
kind: "named_env_file",
|
|
25678
|
+
path,
|
|
25679
|
+
environmentName: environment
|
|
25680
|
+
};
|
|
25681
|
+
}
|
|
25682
|
+
function explicitEnvironmentSource(cwd, envFile) {
|
|
25683
|
+
const path = resolve(cwd, envFile);
|
|
25684
|
+
const selectedEnvironment = readEnvFile(path, true);
|
|
25685
|
+
if (!selectedEnvironment.SUPACLOUD_ENV)
|
|
25686
|
+
throw new Error(`SUPACLOUD_ENV is required in ${path}`);
|
|
25687
|
+
assertAdminFileContext(selectedEnvironment, path);
|
|
25688
|
+
return {
|
|
25689
|
+
environment: selectedEnvironment,
|
|
25690
|
+
kind: "explicit_env_file",
|
|
25691
|
+
path,
|
|
25692
|
+
environmentName: normalizeEnvironmentName(selectedEnvironment.SUPACLOUD_ENV)
|
|
25693
|
+
};
|
|
25694
|
+
}
|
|
25695
|
+
function legacyEnvironmentSource(cwd) {
|
|
25696
|
+
const path = resolve(cwd, ".env");
|
|
25697
|
+
const environment = readEnvFile(path, false);
|
|
25698
|
+
if (Object.keys(environment).length > 0)
|
|
25699
|
+
assertAdminFileContext(environment, path);
|
|
25700
|
+
const environmentName = environment.SUPACLOUD_ENV ? normalizeEnvironmentName(environment.SUPACLOUD_ENV) : "";
|
|
25701
|
+
return {
|
|
25702
|
+
environment,
|
|
25703
|
+
kind: Object.keys(environment).length ? "legacy_dotenv" : "none",
|
|
25704
|
+
path: existsSync(path) ? path : null,
|
|
25705
|
+
environmentName
|
|
25706
|
+
};
|
|
25707
|
+
}
|
|
25708
|
+
function ambientContextSource(env, cwd) {
|
|
25709
|
+
const environment = processEnvironment(env);
|
|
25710
|
+
if (env.SUPACLOUD_ENV) {
|
|
25711
|
+
const environmentName = normalizeEnvironmentName(env.SUPACLOUD_ENV);
|
|
25712
|
+
if (completeAdminContext(environment)) {
|
|
25713
|
+
return processContextSource(environment, environmentName);
|
|
25714
|
+
}
|
|
25715
|
+
return namedEnvironmentSource(cwd, env.SUPACLOUD_ENV);
|
|
25716
|
+
}
|
|
25717
|
+
if (hasProcessContext(env)) {
|
|
25718
|
+
return processContextSource(environment, "");
|
|
25719
|
+
}
|
|
25720
|
+
return legacyEnvironmentSource(cwd);
|
|
25721
|
+
}
|
|
25722
|
+
function contextSource(env, cwd, selection) {
|
|
25723
|
+
if (selection.environmentName && selection.envFile) {
|
|
25724
|
+
throw new Error("Environment selectors are mutually exclusive");
|
|
25725
|
+
}
|
|
25726
|
+
if (selection.environmentName)
|
|
25727
|
+
return namedEnvironmentSource(cwd, selection.environmentName);
|
|
25728
|
+
if (selection.envFile)
|
|
25729
|
+
return explicitEnvironmentSource(cwd, selection.envFile);
|
|
25730
|
+
return ambientContextSource(env, cwd);
|
|
25731
|
+
}
|
|
25732
|
+
function resolvedSshContext(environment) {
|
|
25733
|
+
const rawPort = environment.SUPACLOUD_SSH_PORT || "22";
|
|
25734
|
+
if (!/^\d+$/.test(rawPort)) {
|
|
25735
|
+
throw new Error("SUPACLOUD_SSH_PORT must be an integer between 1 and 65535");
|
|
25736
|
+
}
|
|
25737
|
+
const sshPort = Number(rawPort);
|
|
25738
|
+
if (!Number.isSafeInteger(sshPort) || sshPort < 1 || sshPort > 65535) {
|
|
25739
|
+
throw new Error("SUPACLOUD_SSH_PORT must be an integer between 1 and 65535");
|
|
25740
|
+
}
|
|
25741
|
+
return {
|
|
25742
|
+
sshUser: environment.SUPACLOUD_SSH_USER || "root",
|
|
25743
|
+
sshPort,
|
|
25744
|
+
sshKey: environment.SUPACLOUD_SSH_KEY || resolve(homedir(), ".ssh", "id_rsa"),
|
|
25745
|
+
sshPass: environment.SUPACLOUD_SSH_PASS || "",
|
|
25746
|
+
sshHostFingerprint: environment.SUPACLOUD_SSH_HOST_FINGERPRINT || ""
|
|
25747
|
+
};
|
|
25748
|
+
}
|
|
25749
|
+
function resolveSupaCloudContext(env = process.env, cwd = process.cwd(), selection = {}) {
|
|
25750
|
+
const source = contextSource(env, cwd, selection);
|
|
25751
|
+
const adminCore = sourceAdminCore(source.environment);
|
|
25752
|
+
const host = source.environment.SUPACLOUD_HOST || hostFromUrl(adminCore.apiUrl || adminCore.supabaseUrl);
|
|
25753
|
+
return {
|
|
25754
|
+
host,
|
|
25755
|
+
...resolvedSshContext(source.environment),
|
|
25756
|
+
apiUrl: adminCore.apiUrl,
|
|
25757
|
+
apiToken: adminCore.apiToken,
|
|
25758
|
+
projectRef: adminCore.projectRef,
|
|
25759
|
+
readOnly: env.SUPACLOUD_READ_ONLY === "true" || source.environment.SUPACLOUD_READ_ONLY === "true",
|
|
25760
|
+
environment: source.environmentName,
|
|
25761
|
+
production: source.environmentName === "production",
|
|
25762
|
+
inferredSupabaseUrl: adminCore.supabaseUrl,
|
|
25763
|
+
inferredServiceRoleKey: source.environment.SUPABASE_SERVICE_ROLE_KEY || "",
|
|
25764
|
+
source: source.kind,
|
|
25765
|
+
sourcePath: source.path
|
|
25561
25766
|
};
|
|
25562
25767
|
}
|
|
25563
25768
|
|
|
25769
|
+
// src/shared/execution-policy.ts
|
|
25770
|
+
var ACTION_POLICY = {
|
|
25771
|
+
project: {
|
|
25772
|
+
read: ["list", "get", "settings", "api_keys", "health", "logs", "tasks", "services"],
|
|
25773
|
+
write: ["create", "delete", "pause", "restore", "restart", "update_settings"]
|
|
25774
|
+
},
|
|
25775
|
+
platform: {
|
|
25776
|
+
read: ["metrics", "list_backups", "network", "list_orgs", "get_org"],
|
|
25777
|
+
write: ["create_backup", "update_network"]
|
|
25778
|
+
},
|
|
25779
|
+
gateway: {
|
|
25780
|
+
read: ["routes", "get_certificate", "custom_hostname"],
|
|
25781
|
+
write: [
|
|
25782
|
+
"upsert_route",
|
|
25783
|
+
"update_route",
|
|
25784
|
+
"delete_route",
|
|
25785
|
+
"config",
|
|
25786
|
+
"update_certificate",
|
|
25787
|
+
"issue_certificate",
|
|
25788
|
+
"deploy_certificate",
|
|
25789
|
+
"rebuild",
|
|
25790
|
+
"set_custom_hostname",
|
|
25791
|
+
"delete_custom_hostname",
|
|
25792
|
+
"verify_custom_hostname"
|
|
25793
|
+
]
|
|
25794
|
+
},
|
|
25795
|
+
ssh: {
|
|
25796
|
+
read: [
|
|
25797
|
+
"ping",
|
|
25798
|
+
"versions",
|
|
25799
|
+
"diagnose",
|
|
25800
|
+
"exec",
|
|
25801
|
+
"troubleshoot",
|
|
25802
|
+
"container_logs",
|
|
25803
|
+
"tenant_list",
|
|
25804
|
+
"tenant_inspect",
|
|
25805
|
+
"tenant_diagnose"
|
|
25806
|
+
],
|
|
25807
|
+
write: ["setup", "install", "upgrade", "tenant_migrate"]
|
|
25808
|
+
}
|
|
25809
|
+
};
|
|
25810
|
+
var DYNAMIC_ACTION_FIELDS = {
|
|
25811
|
+
"project.service_control": "service_action",
|
|
25812
|
+
"ssh.tenant_manage": "tenant_action"
|
|
25813
|
+
};
|
|
25814
|
+
var REFLESS_WRITE_ACTIONS = new Set([
|
|
25815
|
+
"project.create",
|
|
25816
|
+
"ssh.setup",
|
|
25817
|
+
"ssh.install",
|
|
25818
|
+
"ssh.upgrade"
|
|
25819
|
+
]);
|
|
25820
|
+
var SSH_PROJECT_REF_ACTIONS = new Set(["tenant_manage", "tenant_inspect", "tenant_diagnose"]);
|
|
25821
|
+
var PLATFORM_PROJECT_REF_ACTIONS = new Set([
|
|
25822
|
+
"list_backups",
|
|
25823
|
+
"create_backup",
|
|
25824
|
+
"network",
|
|
25825
|
+
"update_network"
|
|
25826
|
+
]);
|
|
25827
|
+
function declaredMode(moduleName, action) {
|
|
25828
|
+
const policy = ACTION_POLICY[moduleName];
|
|
25829
|
+
if (!policy)
|
|
25830
|
+
return;
|
|
25831
|
+
for (const mode of ["read", "write", "local"]) {
|
|
25832
|
+
if (policy[mode]?.includes(action))
|
|
25833
|
+
return mode;
|
|
25834
|
+
}
|
|
25835
|
+
return;
|
|
25836
|
+
}
|
|
25837
|
+
function serviceControlMode(serviceAction) {
|
|
25838
|
+
if (serviceAction === "status")
|
|
25839
|
+
return "read";
|
|
25840
|
+
if (["start", "stop", "restart", "pause", "resume"].includes(String(serviceAction))) {
|
|
25841
|
+
return "write";
|
|
25842
|
+
}
|
|
25843
|
+
return;
|
|
25844
|
+
}
|
|
25845
|
+
function executionMode(moduleName, action, args) {
|
|
25846
|
+
if (moduleName === "project" && action === "service_control") {
|
|
25847
|
+
return serviceControlMode(args.service_action);
|
|
25848
|
+
}
|
|
25849
|
+
if (moduleName === "ssh" && action === "tenant_manage") {
|
|
25850
|
+
return serviceControlMode(args.tenant_action);
|
|
25851
|
+
}
|
|
25852
|
+
return declaredMode(moduleName, action);
|
|
25853
|
+
}
|
|
25854
|
+
function stringArgument(args, field) {
|
|
25855
|
+
const candidate = args[field];
|
|
25856
|
+
return typeof candidate === "string" && candidate ? candidate : null;
|
|
25857
|
+
}
|
|
25858
|
+
function sshRequestedProjectRef(action, args) {
|
|
25859
|
+
if (action === "tenant_migrate")
|
|
25860
|
+
return stringArgument(args, "target_ref");
|
|
25861
|
+
return SSH_PROJECT_REF_ACTIONS.has(action) ? stringArgument(args, "project_ref") : null;
|
|
25862
|
+
}
|
|
25863
|
+
function requestedProjectRef(moduleName, action, args) {
|
|
25864
|
+
if (moduleName === "ssh")
|
|
25865
|
+
return sshRequestedProjectRef(action, args);
|
|
25866
|
+
if (moduleName === "project") {
|
|
25867
|
+
return ["list", "create"].includes(action) ? null : stringArgument(args, "ref");
|
|
25868
|
+
}
|
|
25869
|
+
if (moduleName === "platform") {
|
|
25870
|
+
return PLATFORM_PROJECT_REF_ACTIONS.has(action) ? stringArgument(args, "ref") : null;
|
|
25871
|
+
}
|
|
25872
|
+
return moduleName === "gateway" ? stringArgument(args, "ref") : null;
|
|
25873
|
+
}
|
|
25874
|
+
function requiresProjectRef(moduleName, action) {
|
|
25875
|
+
return !REFLESS_WRITE_ACTIONS.has(`${moduleName}.${action}`);
|
|
25876
|
+
}
|
|
25877
|
+
function apiConfirmationTarget(context) {
|
|
25878
|
+
if (!context.apiUrl) {
|
|
25879
|
+
throw new Error("Production platform write requires a configured SUPACLOUD_API_URL");
|
|
25880
|
+
}
|
|
25881
|
+
return `platform:${new URL(context.apiUrl).host}`;
|
|
25882
|
+
}
|
|
25883
|
+
function sshConfirmationTarget(context) {
|
|
25884
|
+
if (!context.host) {
|
|
25885
|
+
throw new Error("Production SSH write requires a configured SUPACLOUD_HOST");
|
|
25886
|
+
}
|
|
25887
|
+
const portSuffix = context.sshPort === 22 ? "" : `:${context.sshPort}`;
|
|
25888
|
+
return `host:${context.host}${portSuffix}`;
|
|
25889
|
+
}
|
|
25890
|
+
function productionConfirmationTarget(moduleName, action, args, context) {
|
|
25891
|
+
const projectRef = requestedProjectRef(moduleName, action, args);
|
|
25892
|
+
if (projectRef)
|
|
25893
|
+
return projectRef;
|
|
25894
|
+
if (moduleName === "ssh")
|
|
25895
|
+
return sshConfirmationTarget(context);
|
|
25896
|
+
return apiConfirmationTarget(context);
|
|
25897
|
+
}
|
|
25898
|
+
function assertProfileProjectBoundary(requestedRef, context) {
|
|
25899
|
+
if (requestedRef && context.projectRef && requestedRef !== context.projectRef) {
|
|
25900
|
+
throw new Error("Production profiles cannot target a different project ref");
|
|
25901
|
+
}
|
|
25902
|
+
}
|
|
25903
|
+
function requiredProjectRefFlag(moduleName, action) {
|
|
25904
|
+
if (moduleName !== "ssh")
|
|
25905
|
+
return "ref";
|
|
25906
|
+
return action === "tenant_migrate" ? "target_ref" : "project_ref";
|
|
25907
|
+
}
|
|
25908
|
+
function authorizeWrite(request) {
|
|
25909
|
+
const { moduleName, action, args, projectRef, authorization } = request;
|
|
25910
|
+
const { context, confirmProduction } = authorization;
|
|
25911
|
+
if (context.readOnly) {
|
|
25912
|
+
throw new Error(`Remote write ${moduleName}.${action} is blocked in read-only mode (SUPACLOUD_READ_ONLY=true)`);
|
|
25913
|
+
}
|
|
25914
|
+
if (!context.environment) {
|
|
25915
|
+
throw new Error(`Remote write ${moduleName}.${action} requires an explicit SUPACLOUD_ENV`);
|
|
25916
|
+
}
|
|
25917
|
+
if (!context.production)
|
|
25918
|
+
return;
|
|
25919
|
+
if (requiresProjectRef(moduleName, action) && !projectRef) {
|
|
25920
|
+
throw new Error(`Production write ${moduleName}.${action} requires --${requiredProjectRefFlag(moduleName, action)}`);
|
|
25921
|
+
}
|
|
25922
|
+
const confirmationTarget = productionConfirmationTarget(moduleName, action, args, context);
|
|
25923
|
+
if (confirmProduction !== confirmationTarget) {
|
|
25924
|
+
throw new Error(`Production write requires --confirm-production ${confirmationTarget}`);
|
|
25925
|
+
}
|
|
25926
|
+
}
|
|
25927
|
+
function assertKnownMode(moduleName, action, mode, context) {
|
|
25928
|
+
if (!mode && (context.production || context.readOnly)) {
|
|
25929
|
+
throw new Error(`Execution policy has no classification for ${moduleName}.${action}`);
|
|
25930
|
+
}
|
|
25931
|
+
}
|
|
25932
|
+
function authorizeExecution(moduleName, args, authorization) {
|
|
25933
|
+
const action = typeof args.action === "string" ? args.action : "";
|
|
25934
|
+
if (!action)
|
|
25935
|
+
return;
|
|
25936
|
+
const mode = executionMode(moduleName, action, args);
|
|
25937
|
+
const { context } = authorization;
|
|
25938
|
+
assertKnownMode(moduleName, action, mode, context);
|
|
25939
|
+
if (!mode)
|
|
25940
|
+
return;
|
|
25941
|
+
const projectRef = requestedProjectRef(moduleName, action, args);
|
|
25942
|
+
if (context.production)
|
|
25943
|
+
assertProfileProjectBoundary(projectRef, context);
|
|
25944
|
+
if (mode !== "write")
|
|
25945
|
+
return;
|
|
25946
|
+
authorizeWrite({ moduleName, action, args, projectRef, authorization });
|
|
25947
|
+
}
|
|
25948
|
+
function validateDynamicAction(tool, moduleName, action, field) {
|
|
25949
|
+
const fieldSchema = schemaProperties(tool.schema)[field];
|
|
25950
|
+
const fieldActions = fieldSchema ? schemaEnumValues(fieldSchema) : [];
|
|
25951
|
+
if (fieldActions.length === 0) {
|
|
25952
|
+
throw new Error(`Execution policy cannot inspect ${moduleName}.${action}.${field}`);
|
|
25953
|
+
}
|
|
25954
|
+
for (const fieldAction of fieldActions) {
|
|
25955
|
+
if (!executionMode(moduleName, action, { [field]: fieldAction })) {
|
|
25956
|
+
throw new Error(`Execution policy has no classification for ${moduleName}.${action}.${fieldAction}`);
|
|
25957
|
+
}
|
|
25958
|
+
}
|
|
25959
|
+
}
|
|
25960
|
+
function validateRegisteredAction(tool, moduleName, action) {
|
|
25961
|
+
const dynamicField = DYNAMIC_ACTION_FIELDS[`${moduleName}.${action}`];
|
|
25962
|
+
if (dynamicField)
|
|
25963
|
+
return validateDynamicAction(tool, moduleName, action, dynamicField);
|
|
25964
|
+
if (!declaredMode(moduleName, action)) {
|
|
25965
|
+
throw new Error(`Execution policy has no classification for ${moduleName}.${action}`);
|
|
25966
|
+
}
|
|
25967
|
+
}
|
|
25968
|
+
function validateExecutionPolicyCoverage(tools) {
|
|
25969
|
+
for (const [moduleName, tool] of Object.entries(tools)) {
|
|
25970
|
+
const actionSchema = schemaProperties(tool.schema).action;
|
|
25971
|
+
if (!actionSchema)
|
|
25972
|
+
continue;
|
|
25973
|
+
const registeredActions = schemaEnumValues(actionSchema);
|
|
25974
|
+
if (registeredActions.length === 0) {
|
|
25975
|
+
throw new Error(`Execution policy cannot inspect ${moduleName}.action`);
|
|
25976
|
+
}
|
|
25977
|
+
for (const action of registeredActions)
|
|
25978
|
+
validateRegisteredAction(tool, moduleName, action);
|
|
25979
|
+
}
|
|
25980
|
+
}
|
|
25981
|
+
|
|
25564
25982
|
// src/shared/transports/http.ts
|
|
25565
25983
|
var DEFAULT_TIMEOUT = 30000;
|
|
25984
|
+
var MAX_POST_TIMEOUT_MS = 35 * 60000;
|
|
25566
25985
|
var MAX_RETRIES = 2;
|
|
25567
25986
|
var RETRY_BASE_DELAY = 500;
|
|
25568
25987
|
function isRetryableMethod(method) {
|
|
@@ -25575,9 +25994,26 @@ function isRetryableError(error) {
|
|
|
25575
25994
|
const networkError = error;
|
|
25576
25995
|
return networkError.name === "AbortError" || networkError.code === "ECONNREFUSED" || networkError.code === "ECONNRESET";
|
|
25577
25996
|
}
|
|
25578
|
-
|
|
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,6 +29233,419 @@ ${JSON.stringify(r.data, null, 2)}` : `❌ Failed (${r.status})`;
|
|
|
28677
29233
|
});
|
|
28678
29234
|
}
|
|
28679
29235
|
|
|
29236
|
+
// src/shared/tools/project-create-env.ts
|
|
29237
|
+
import { constants } from "node:fs";
|
|
29238
|
+
import { isAbsolute, basename as basename4, dirname as dirname2, join as join3, normalize, parse as parse2, resolve as resolve2 } from "node:path";
|
|
29239
|
+
import {
|
|
29240
|
+
lstat,
|
|
29241
|
+
open,
|
|
29242
|
+
realpath,
|
|
29243
|
+
unlink
|
|
29244
|
+
} from "node:fs/promises";
|
|
29245
|
+
var PROJECT_REF = /^[a-z]{20}$/;
|
|
29246
|
+
var SERVICE_ROLE_KEY = /^[A-Za-z0-9_-]{8,2048}\.[A-Za-z0-9_-]{8,8192}\.[A-Za-z0-9_-]{8,2048}$/;
|
|
29247
|
+
var SERVICE_ROLE_ALGORITHMS = new Set(["HS256", "ES256"]);
|
|
29248
|
+
var MAX_ENV_FILE_PATH_BYTES = 4096;
|
|
29249
|
+
var ENV_FILE_MODE = 384;
|
|
29250
|
+
var GROUP_OR_WORLD_WRITE_MODE = 18;
|
|
29251
|
+
var PROC_SELF_FD = "/proc/self/fd";
|
|
29252
|
+
|
|
29253
|
+
class ProjectCreateEnvError extends Error {
|
|
29254
|
+
code;
|
|
29255
|
+
credentialFileState;
|
|
29256
|
+
constructor(code, credentialFileState = "absent") {
|
|
29257
|
+
super(code);
|
|
29258
|
+
this.code = code;
|
|
29259
|
+
this.credentialFileState = credentialFileState;
|
|
29260
|
+
this.name = "ProjectCreateEnvError";
|
|
29261
|
+
}
|
|
29262
|
+
}
|
|
29263
|
+
function descriptorRelativePath(directory, filename) {
|
|
29264
|
+
return `${PROC_SELF_FD}/${directory.fd}/${filename}`;
|
|
29265
|
+
}
|
|
29266
|
+
var nodeProjectEnvFileOperations = {
|
|
29267
|
+
platform: process.platform,
|
|
29268
|
+
effectiveUid: () => process.geteuid?.() ?? -1,
|
|
29269
|
+
lstat,
|
|
29270
|
+
realpath,
|
|
29271
|
+
openDirectory: (path2) => open(path2, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW),
|
|
29272
|
+
openExclusiveAt: (directory, filename, mode) => open(descriptorRelativePath(directory, filename), "wx", mode),
|
|
29273
|
+
lstatAt: (directory, filename) => lstat(descriptorRelativePath(directory, filename)),
|
|
29274
|
+
unlinkAt: (directory, filename) => unlink(descriptorRelativePath(directory, filename))
|
|
29275
|
+
};
|
|
29276
|
+
function fileSystemErrorCode(candidate) {
|
|
29277
|
+
if (!candidate || typeof candidate !== "object" || !("code" in candidate))
|
|
29278
|
+
return null;
|
|
29279
|
+
return typeof candidate.code === "string" ? candidate.code : null;
|
|
29280
|
+
}
|
|
29281
|
+
async function assertTargetAbsent(path2, operations) {
|
|
29282
|
+
try {
|
|
29283
|
+
await operations.lstat(path2);
|
|
29284
|
+
} catch (error) {
|
|
29285
|
+
if (fileSystemErrorCode(error) === "ENOENT")
|
|
29286
|
+
return;
|
|
29287
|
+
throw new ProjectCreateEnvError("ENV_FILE_PATH_INVALID");
|
|
29288
|
+
}
|
|
29289
|
+
throw new ProjectCreateEnvError("ENV_FILE_EXISTS");
|
|
29290
|
+
}
|
|
29291
|
+
function hasStableCanonicalParent(directory, canonicalParent, parentBeforeRealpath, parentAfterRealpath) {
|
|
29292
|
+
return parentBeforeRealpath.isDirectory() && !parentBeforeRealpath.isSymbolicLink() && parentAfterRealpath.isDirectory() && !parentAfterRealpath.isSymbolicLink() && canonicalParent === directory && sameFileIdentity(parentBeforeRealpath, parentAfterRealpath);
|
|
29293
|
+
}
|
|
29294
|
+
function parentIdentity(canonicalPath, parentStat) {
|
|
29295
|
+
return {
|
|
29296
|
+
canonicalPath,
|
|
29297
|
+
dev: parentStat.dev,
|
|
29298
|
+
ino: parentStat.ino,
|
|
29299
|
+
mode: parentStat.mode,
|
|
29300
|
+
uid: parentStat.uid
|
|
29301
|
+
};
|
|
29302
|
+
}
|
|
29303
|
+
async function verifiedParentIdentity(directory, operations) {
|
|
29304
|
+
const parentBeforeRealpath = await operations.lstat(directory);
|
|
29305
|
+
const canonicalParent = await operations.realpath(directory);
|
|
29306
|
+
await assertTrustedDirectoryChain(directory, operations);
|
|
29307
|
+
const parentAfterRealpath = await operations.lstat(directory);
|
|
29308
|
+
assertOwnedTargetParent(parentAfterRealpath, operations.effectiveUid());
|
|
29309
|
+
const stableParent = hasStableCanonicalParent(directory, canonicalParent, parentBeforeRealpath, parentAfterRealpath);
|
|
29310
|
+
if (!stableParent)
|
|
29311
|
+
throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
|
|
29312
|
+
return parentIdentity(canonicalParent, parentAfterRealpath);
|
|
29313
|
+
}
|
|
29314
|
+
async function assertSafeParent(directory, operations) {
|
|
29315
|
+
try {
|
|
29316
|
+
return await verifiedParentIdentity(directory, operations);
|
|
29317
|
+
} catch (error) {
|
|
29318
|
+
if (error instanceof ProjectCreateEnvError)
|
|
29319
|
+
throw error;
|
|
29320
|
+
throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
|
|
29321
|
+
}
|
|
29322
|
+
}
|
|
29323
|
+
function directoryChain(directory) {
|
|
29324
|
+
const root = parse2(directory).root;
|
|
29325
|
+
const ancestors = [root];
|
|
29326
|
+
let current = root;
|
|
29327
|
+
for (const component of directory.slice(root.length).split("/").filter(Boolean)) {
|
|
29328
|
+
current = join3(current, component);
|
|
29329
|
+
ancestors.push(current);
|
|
29330
|
+
}
|
|
29331
|
+
return ancestors;
|
|
29332
|
+
}
|
|
29333
|
+
function assertTrustedAncestor(directoryStat, effectiveUid) {
|
|
29334
|
+
const ownerIsTrusted = directoryStat.uid === 0 || directoryStat.uid === effectiveUid;
|
|
29335
|
+
const hasUntrustedWriteAccess = Boolean(directoryStat.mode & GROUP_OR_WORLD_WRITE_MODE);
|
|
29336
|
+
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink() || !ownerIsTrusted || hasUntrustedWriteAccess) {
|
|
29337
|
+
throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
|
|
29338
|
+
}
|
|
29339
|
+
}
|
|
29340
|
+
function assertOwnedTargetParent(directoryStat, effectiveUid) {
|
|
29341
|
+
assertTrustedAncestor(directoryStat, effectiveUid);
|
|
29342
|
+
if (directoryStat.uid !== effectiveUid) {
|
|
29343
|
+
throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
|
|
29344
|
+
}
|
|
29345
|
+
}
|
|
29346
|
+
async function assertTrustedDirectoryChain(directory, operations) {
|
|
29347
|
+
const effectiveUid = operations.effectiveUid();
|
|
29348
|
+
if (!Number.isSafeInteger(effectiveUid) || effectiveUid < 0) {
|
|
29349
|
+
throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
|
|
29350
|
+
}
|
|
29351
|
+
for (const ancestor of directoryChain(directory)) {
|
|
29352
|
+
assertTrustedAncestor(await operations.lstat(ancestor), effectiveUid);
|
|
29353
|
+
}
|
|
29354
|
+
}
|
|
29355
|
+
function sameFileIdentity(first, second) {
|
|
29356
|
+
return first.dev === second.dev && first.ino === second.ino;
|
|
29357
|
+
}
|
|
29358
|
+
function sameParentIdentity(current, expected) {
|
|
29359
|
+
return sameFileIdentity(current, expected) && current.mode === expected.mode && current.uid === expected.uid;
|
|
29360
|
+
}
|
|
29361
|
+
function hasSafeRequestedPath(requestedPath) {
|
|
29362
|
+
return Boolean(requestedPath) && isAbsolute(requestedPath) && requestedPath === normalize(requestedPath) && requestedPath === resolve2(requestedPath) && Buffer.byteLength(requestedPath, "utf8") <= MAX_ENV_FILE_PATH_BYTES && !/[\0\r\n]/.test(requestedPath) && ![".", ".."].includes(basename4(requestedPath));
|
|
29363
|
+
}
|
|
29364
|
+
async function prepareProjectEnvFile(requestedPath, environment, operations = nodeProjectEnvFileOperations) {
|
|
29365
|
+
if (operations.platform !== "linux") {
|
|
29366
|
+
throw new ProjectCreateEnvError("ENV_FILE_PLATFORM_UNSUPPORTED");
|
|
29367
|
+
}
|
|
29368
|
+
if (!hasSafeRequestedPath(requestedPath)) {
|
|
29369
|
+
throw new ProjectCreateEnvError("ENV_FILE_PATH_INVALID");
|
|
29370
|
+
}
|
|
29371
|
+
const directory = dirname2(requestedPath);
|
|
29372
|
+
const filename = basename4(requestedPath);
|
|
29373
|
+
const parentIdentity2 = await assertSafeParent(directory, operations);
|
|
29374
|
+
await assertTargetAbsent(requestedPath, operations);
|
|
29375
|
+
let directoryHandle = null;
|
|
29376
|
+
let openFile = null;
|
|
29377
|
+
let fileIdentity = null;
|
|
29378
|
+
try {
|
|
29379
|
+
directoryHandle = await operations.openDirectory(directory);
|
|
29380
|
+
await assertDirectoryBinding(directory, directoryHandle, parentIdentity2, operations);
|
|
29381
|
+
openFile = await openProjectEnvFile(directoryHandle, filename, operations);
|
|
29382
|
+
const openFileStat = await openFile.stat();
|
|
29383
|
+
assertSecureFileType(openFileStat);
|
|
29384
|
+
fileIdentity = { dev: openFileStat.dev, ino: openFileStat.ino };
|
|
29385
|
+
await openFile.chmod(ENV_FILE_MODE);
|
|
29386
|
+
const prepared = {
|
|
29387
|
+
path: requestedPath,
|
|
29388
|
+
filename,
|
|
29389
|
+
environment,
|
|
29390
|
+
parentIdentity: parentIdentity2,
|
|
29391
|
+
fileIdentity,
|
|
29392
|
+
directoryHandle,
|
|
29393
|
+
openFile
|
|
29394
|
+
};
|
|
29395
|
+
await assertPreparedFileBinding(prepared, operations);
|
|
29396
|
+
return prepared;
|
|
29397
|
+
} catch (error) {
|
|
29398
|
+
if (directoryHandle) {
|
|
29399
|
+
const credentialFileState = await removeFailedEnvFile({ directoryHandle, filename, openFile, fileIdentity, operations });
|
|
29400
|
+
if (credentialFileState === "unknown") {
|
|
29401
|
+
throw new ProjectCreateEnvError("ENV_FILE_CLEANUP_FAILED", "unknown");
|
|
29402
|
+
}
|
|
29403
|
+
}
|
|
29404
|
+
if (error instanceof ProjectCreateEnvError)
|
|
29405
|
+
throw error;
|
|
29406
|
+
throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
|
|
29407
|
+
}
|
|
29408
|
+
}
|
|
29409
|
+
function environmentFileContent(credentials, environment) {
|
|
29410
|
+
return [
|
|
29411
|
+
`SUPACLOUD_ENV=${environment}`,
|
|
29412
|
+
`SUPACLOUD_PROJECT_REF=${credentials.projectRef}`,
|
|
29413
|
+
`SUPABASE_URL=${credentials.apiUrl}`,
|
|
29414
|
+
`SUPABASE_SERVICE_ROLE_KEY=${credentials.serviceRoleKey}`,
|
|
29415
|
+
""
|
|
29416
|
+
].join(`
|
|
29417
|
+
`);
|
|
29418
|
+
}
|
|
29419
|
+
async function sanitizeAndClose(openFile) {
|
|
29420
|
+
let sanitized = true;
|
|
29421
|
+
try {
|
|
29422
|
+
await openFile.truncate(0);
|
|
29423
|
+
await openFile.sync();
|
|
29424
|
+
} catch {
|
|
29425
|
+
sanitized = false;
|
|
29426
|
+
}
|
|
29427
|
+
try {
|
|
29428
|
+
await openFile.close();
|
|
29429
|
+
} catch {}
|
|
29430
|
+
return sanitized;
|
|
29431
|
+
}
|
|
29432
|
+
async function directorySyncSucceeded(directoryHandle) {
|
|
29433
|
+
try {
|
|
29434
|
+
await directoryHandle.sync();
|
|
29435
|
+
return true;
|
|
29436
|
+
} catch {
|
|
29437
|
+
return false;
|
|
29438
|
+
}
|
|
29439
|
+
}
|
|
29440
|
+
async function cleanedCredentialState(cleanup, sanitized) {
|
|
29441
|
+
const { directoryHandle, filename, fileIdentity, operations } = cleanup;
|
|
29442
|
+
try {
|
|
29443
|
+
const targetStat = await operations.lstatAt(directoryHandle, filename);
|
|
29444
|
+
if (!fileIdentity || !sameFileIdentity(targetStat, fileIdentity))
|
|
29445
|
+
return "unknown";
|
|
29446
|
+
await operations.unlinkAt(directoryHandle, filename);
|
|
29447
|
+
} catch (error) {
|
|
29448
|
+
if (fileSystemErrorCode(error) !== "ENOENT")
|
|
29449
|
+
return "unknown";
|
|
29450
|
+
}
|
|
29451
|
+
if (!await directorySyncSucceeded(directoryHandle))
|
|
29452
|
+
return "unknown";
|
|
29453
|
+
return sanitized ? "absent" : "unknown";
|
|
29454
|
+
}
|
|
29455
|
+
async function removeFailedEnvFile(cleanup) {
|
|
29456
|
+
const { directoryHandle, filename, openFile, fileIdentity, operations } = cleanup;
|
|
29457
|
+
if (!openFile) {
|
|
29458
|
+
try {
|
|
29459
|
+
await directoryHandle.close();
|
|
29460
|
+
} catch {}
|
|
29461
|
+
return "absent";
|
|
29462
|
+
}
|
|
29463
|
+
const sanitized = await sanitizeAndClose(openFile);
|
|
29464
|
+
const credentialFileState = await cleanedCredentialState(cleanup, sanitized);
|
|
29465
|
+
try {
|
|
29466
|
+
await directoryHandle.close();
|
|
29467
|
+
} catch {}
|
|
29468
|
+
return credentialFileState;
|
|
29469
|
+
}
|
|
29470
|
+
function assertSecureFileType(fileStat) {
|
|
29471
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
29472
|
+
throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
|
|
29473
|
+
}
|
|
29474
|
+
}
|
|
29475
|
+
function assertSecureOpenFile(fileStat, effectiveUid) {
|
|
29476
|
+
assertSecureFileType(fileStat);
|
|
29477
|
+
if ((fileStat.mode & 511) !== ENV_FILE_MODE || fileStat.uid !== effectiveUid) {
|
|
29478
|
+
throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
|
|
29479
|
+
}
|
|
29480
|
+
}
|
|
29481
|
+
async function openProjectEnvFile(directory, filename, operations) {
|
|
29482
|
+
try {
|
|
29483
|
+
return await operations.openExclusiveAt(directory, filename, ENV_FILE_MODE);
|
|
29484
|
+
} catch (error) {
|
|
29485
|
+
if (fileSystemErrorCode(error) === "EEXIST") {
|
|
29486
|
+
throw new ProjectCreateEnvError("ENV_FILE_EXISTS");
|
|
29487
|
+
}
|
|
29488
|
+
throw error;
|
|
29489
|
+
}
|
|
29490
|
+
}
|
|
29491
|
+
async function assertDirectoryBinding(path2, directoryHandle, expectedIdentity, operations) {
|
|
29492
|
+
const heldDirectoryStat = await directoryHandle.stat();
|
|
29493
|
+
const currentPathIdentity = await assertSafeParent(path2, operations);
|
|
29494
|
+
assertOwnedTargetParent(heldDirectoryStat, operations.effectiveUid());
|
|
29495
|
+
if (!sameParentIdentity(heldDirectoryStat, expectedIdentity) || !sameParentIdentity(currentPathIdentity, expectedIdentity)) {
|
|
29496
|
+
throw new ProjectCreateEnvError("ENV_FILE_PARENT_INVALID");
|
|
29497
|
+
}
|
|
29498
|
+
}
|
|
29499
|
+
async function assertPreparedFileBinding(prepared, operations) {
|
|
29500
|
+
if (!prepared.directoryHandle || !prepared.openFile) {
|
|
29501
|
+
throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
|
|
29502
|
+
}
|
|
29503
|
+
await assertDirectoryBinding(dirname2(prepared.path), prepared.directoryHandle, prepared.parentIdentity, operations);
|
|
29504
|
+
const openFileStat = await prepared.openFile.stat();
|
|
29505
|
+
const targetStat = await operations.lstatAt(prepared.directoryHandle, prepared.filename);
|
|
29506
|
+
const effectiveUid = operations.effectiveUid();
|
|
29507
|
+
assertSecureOpenFile(openFileStat, effectiveUid);
|
|
29508
|
+
assertSecureOpenFile(targetStat, effectiveUid);
|
|
29509
|
+
if (!sameFileIdentity(openFileStat, prepared.fileIdentity) || !sameFileIdentity(targetStat, prepared.fileIdentity)) {
|
|
29510
|
+
throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
|
|
29511
|
+
}
|
|
29512
|
+
}
|
|
29513
|
+
async function assertCompletedFileBinding(prepared, operations) {
|
|
29514
|
+
if (!prepared.directoryHandle)
|
|
29515
|
+
throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
|
|
29516
|
+
await assertDirectoryBinding(dirname2(prepared.path), prepared.directoryHandle, prepared.parentIdentity, operations);
|
|
29517
|
+
const targetStat = await operations.lstatAt(prepared.directoryHandle, prepared.filename);
|
|
29518
|
+
assertSecureOpenFile(targetStat, operations.effectiveUid());
|
|
29519
|
+
if (!sameFileIdentity(targetStat, prepared.fileIdentity)) {
|
|
29520
|
+
throw new ProjectCreateEnvError("ENV_FILE_VERIFY_FAILED");
|
|
29521
|
+
}
|
|
29522
|
+
}
|
|
29523
|
+
async function discardPreparedProjectEnvFile(prepared, operations = nodeProjectEnvFileOperations) {
|
|
29524
|
+
if (!prepared.directoryHandle)
|
|
29525
|
+
return "absent";
|
|
29526
|
+
const directoryHandle = prepared.directoryHandle;
|
|
29527
|
+
const openFile = prepared.openFile;
|
|
29528
|
+
prepared.directoryHandle = null;
|
|
29529
|
+
prepared.openFile = null;
|
|
29530
|
+
return removeFailedEnvFile({
|
|
29531
|
+
directoryHandle,
|
|
29532
|
+
filename: prepared.filename,
|
|
29533
|
+
openFile,
|
|
29534
|
+
fileIdentity: prepared.fileIdentity,
|
|
29535
|
+
operations
|
|
29536
|
+
});
|
|
29537
|
+
}
|
|
29538
|
+
async function writeProjectEnvFile(prepared, credentials, operations = nodeProjectEnvFileOperations) {
|
|
29539
|
+
if (!prepared.directoryHandle || !prepared.openFile) {
|
|
29540
|
+
throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
|
|
29541
|
+
}
|
|
29542
|
+
const directoryHandle = prepared.directoryHandle;
|
|
29543
|
+
const openFile = prepared.openFile;
|
|
29544
|
+
try {
|
|
29545
|
+
await assertPreparedFileBinding(prepared, operations);
|
|
29546
|
+
await openFile.chmod(ENV_FILE_MODE);
|
|
29547
|
+
await assertPreparedFileBinding(prepared, operations);
|
|
29548
|
+
await openFile.writeFile(environmentFileContent(credentials, prepared.environment));
|
|
29549
|
+
await openFile.sync();
|
|
29550
|
+
await assertPreparedFileBinding(prepared, operations);
|
|
29551
|
+
await openFile.close();
|
|
29552
|
+
prepared.openFile = null;
|
|
29553
|
+
await assertCompletedFileBinding(prepared, operations);
|
|
29554
|
+
await directoryHandle.sync();
|
|
29555
|
+
await directoryHandle.close();
|
|
29556
|
+
prepared.directoryHandle = null;
|
|
29557
|
+
} catch (error) {
|
|
29558
|
+
prepared.directoryHandle = null;
|
|
29559
|
+
prepared.openFile = null;
|
|
29560
|
+
const credentialFileState = await removeFailedEnvFile({
|
|
29561
|
+
directoryHandle,
|
|
29562
|
+
filename: prepared.filename,
|
|
29563
|
+
openFile,
|
|
29564
|
+
fileIdentity: prepared.fileIdentity,
|
|
29565
|
+
operations
|
|
29566
|
+
});
|
|
29567
|
+
if (credentialFileState === "unknown") {
|
|
29568
|
+
throw new ProjectCreateEnvError("ENV_FILE_CLEANUP_FAILED", "unknown");
|
|
29569
|
+
}
|
|
29570
|
+
if (error instanceof ProjectCreateEnvError)
|
|
29571
|
+
throw error;
|
|
29572
|
+
throw new ProjectCreateEnvError("ENV_FILE_WRITE_FAILED");
|
|
29573
|
+
}
|
|
29574
|
+
}
|
|
29575
|
+
function isRecord(candidate) {
|
|
29576
|
+
return typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
|
|
29577
|
+
}
|
|
29578
|
+
function decodedJwtPart(encodedPart) {
|
|
29579
|
+
try {
|
|
29580
|
+
const decodedPart = JSON.parse(Buffer.from(encodedPart, "base64url").toString("utf8"));
|
|
29581
|
+
return isRecord(decodedPart) ? decodedPart : null;
|
|
29582
|
+
} catch {
|
|
29583
|
+
return null;
|
|
29584
|
+
}
|
|
29585
|
+
}
|
|
29586
|
+
function isServiceRoleKey(candidate) {
|
|
29587
|
+
if (typeof candidate !== "string" || !SERVICE_ROLE_KEY.test(candidate))
|
|
29588
|
+
return false;
|
|
29589
|
+
const [encodedHeader, encodedClaims] = candidate.split(".");
|
|
29590
|
+
const jwtHeader = decodedJwtPart(encodedHeader);
|
|
29591
|
+
const jwtClaims = decodedJwtPart(encodedClaims);
|
|
29592
|
+
return jwtHeader?.typ === "JWT" && typeof jwtHeader.alg === "string" && SERVICE_ROLE_ALGORITHMS.has(jwtHeader.alg) && jwtClaims?.role === "service_role" && jwtClaims.iss === "supabase" && typeof jwtClaims.exp === "number" && Number.isFinite(jwtClaims.exp) && jwtClaims.exp > Date.now() / 1000;
|
|
29593
|
+
}
|
|
29594
|
+
function parsedProjectApiUrl(candidate) {
|
|
29595
|
+
if (typeof candidate !== "string" || candidate.length > 2048)
|
|
29596
|
+
return null;
|
|
29597
|
+
try {
|
|
29598
|
+
return new URL(candidate);
|
|
29599
|
+
} catch {
|
|
29600
|
+
return null;
|
|
29601
|
+
}
|
|
29602
|
+
}
|
|
29603
|
+
function hasSafeProjectApiUrlShape(parsedUrl) {
|
|
29604
|
+
const loopbackHttp = parsedUrl.protocol === "http:" && ["localhost", "127.0.0.1", "::1", "[::1]"].includes(parsedUrl.hostname);
|
|
29605
|
+
return (parsedUrl.protocol === "https:" || loopbackHttp) && !parsedUrl.username && !parsedUrl.password && !parsedUrl.search && !parsedUrl.hash && (parsedUrl.pathname === "/" || parsedUrl.pathname === "");
|
|
29606
|
+
}
|
|
29607
|
+
function hasExpectedProjectApiOrigin(parsedUrl, projectRef, expectedApiOrigin) {
|
|
29608
|
+
const hostname = parsedUrl.hostname.toLowerCase();
|
|
29609
|
+
if (expectedApiOrigin)
|
|
29610
|
+
return parsedUrl.origin === expectedApiOrigin;
|
|
29611
|
+
return !parsedUrl.port && hostname.split(".")[0] === projectRef;
|
|
29612
|
+
}
|
|
29613
|
+
function projectApiUrl(candidate, projectRef, expectedApiOrigin) {
|
|
29614
|
+
const parsedUrl = parsedProjectApiUrl(candidate);
|
|
29615
|
+
if (!parsedUrl || !hasSafeProjectApiUrlShape(parsedUrl))
|
|
29616
|
+
return null;
|
|
29617
|
+
if (!hasExpectedProjectApiOrigin(parsedUrl, projectRef, expectedApiOrigin))
|
|
29618
|
+
return null;
|
|
29619
|
+
return parsedUrl.origin;
|
|
29620
|
+
}
|
|
29621
|
+
function parseProjectCreateIdentity(responsePayload, expectedApiOrigin) {
|
|
29622
|
+
if (!isRecord(responsePayload) || typeof responsePayload.ref !== "string")
|
|
29623
|
+
return null;
|
|
29624
|
+
if (!PROJECT_REF.test(responsePayload.ref) || !isRecord(responsePayload.api))
|
|
29625
|
+
return null;
|
|
29626
|
+
const apiUrl = projectApiUrl(responsePayload.api.url, responsePayload.ref, expectedApiOrigin);
|
|
29627
|
+
return apiUrl ? { projectRef: responsePayload.ref, apiUrl } : null;
|
|
29628
|
+
}
|
|
29629
|
+
function parseProjectCreateWithoutCredentials(responsePayload, expectedApiOrigin) {
|
|
29630
|
+
const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
|
|
29631
|
+
if (!identity || !isRecord(responsePayload))
|
|
29632
|
+
return null;
|
|
29633
|
+
return Object.hasOwn(responsePayload, "credentials") ? null : identity;
|
|
29634
|
+
}
|
|
29635
|
+
function parseProjectCreateCredentials(responsePayload, expectedApiOrigin, expectedProjectName) {
|
|
29636
|
+
if (!expectedApiOrigin || !expectedProjectName || !isRecord(responsePayload))
|
|
29637
|
+
return null;
|
|
29638
|
+
if (responsePayload.name !== expectedProjectName)
|
|
29639
|
+
return null;
|
|
29640
|
+
const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
|
|
29641
|
+
if (!identity)
|
|
29642
|
+
return null;
|
|
29643
|
+
if (!isRecord(responsePayload.credentials))
|
|
29644
|
+
return null;
|
|
29645
|
+
const serviceRoleKey = responsePayload.credentials.service_role_key;
|
|
29646
|
+
return isServiceRoleKey(serviceRoleKey) ? { ...identity, serviceRoleKey } : null;
|
|
29647
|
+
}
|
|
29648
|
+
|
|
28680
29649
|
// src/shared/tools/project-cli-tools.ts
|
|
28681
29650
|
var PROJECT_SERVICE_NAMES = [
|
|
28682
29651
|
"postgrest",
|
|
@@ -28711,6 +29680,10 @@ var AUTH_SERVICE_HOST_SUFFIX = "-auth";
|
|
|
28711
29680
|
var SAFE_PROJECT_REF2 = /^[a-z0-9-]{1,20}$/;
|
|
28712
29681
|
var SAFE_AUTHORITY_PROJECT_REF = /^[A-Za-z0-9_-]{1,20}$/;
|
|
28713
29682
|
var MAX_SERVICE_CONTROL_MESSAGE_LENGTH = 256;
|
|
29683
|
+
var RELEASE_CONTROL_RESPONSE_SCHEMA = "supacloud.cli.release-control.v1";
|
|
29684
|
+
var PROJECT_CREATE_OPERATION = "project.create";
|
|
29685
|
+
var PROJECT_ENVIRONMENTS = ["test", "production"];
|
|
29686
|
+
var DNS_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
28714
29687
|
var SUPPORTED_PROJECT_SERVICE_ACTIONS = {
|
|
28715
29688
|
postgrest: ["start", "stop", "restart", "pause", "resume", "status"],
|
|
28716
29689
|
gotrue: ["start", "stop", "restart"],
|
|
@@ -28728,6 +29701,142 @@ function failedProjectServiceResponse(message) {
|
|
|
28728
29701
|
isError: true
|
|
28729
29702
|
};
|
|
28730
29703
|
}
|
|
29704
|
+
function projectCreateReceipt(payload) {
|
|
29705
|
+
return {
|
|
29706
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
29707
|
+
schema: RELEASE_CONTROL_RESPONSE_SCHEMA,
|
|
29708
|
+
...payload
|
|
29709
|
+
}, null, 2) }]
|
|
29710
|
+
};
|
|
29711
|
+
}
|
|
29712
|
+
function projectCreateErrorReceipt(payload) {
|
|
29713
|
+
return { ...projectCreateReceipt(payload), isError: true };
|
|
29714
|
+
}
|
|
29715
|
+
function projectCreateFailure(code, httpStatus) {
|
|
29716
|
+
return projectCreateErrorReceipt({
|
|
29717
|
+
ok: false,
|
|
29718
|
+
operation: PROJECT_CREATE_OPERATION,
|
|
29719
|
+
error: { code, http_status: httpStatus }
|
|
29720
|
+
});
|
|
29721
|
+
}
|
|
29722
|
+
function projectCreateMutationFailure(response) {
|
|
29723
|
+
const outcomeUnknown = response.transportError || response.status === 408 || response.status >= 500;
|
|
29724
|
+
return outcomeUnknown ? projectCreateFailure("OUTCOME_UNKNOWN", response.transportError ? null : response.status) : projectCreateFailure("HTTP_ERROR", response.status);
|
|
29725
|
+
}
|
|
29726
|
+
function isCompleteApiHostname(hostname) {
|
|
29727
|
+
if (hostname === "localhost")
|
|
29728
|
+
return true;
|
|
29729
|
+
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostname)) {
|
|
29730
|
+
return hostname.split(".").every((octet) => Number(octet) <= 255);
|
|
29731
|
+
}
|
|
29732
|
+
if (hostname.startsWith("[") && hostname.endsWith("]"))
|
|
29733
|
+
return true;
|
|
29734
|
+
const labels = hostname.split(".");
|
|
29735
|
+
return labels.length >= 2 && labels.every((label) => DNS_LABEL.test(label));
|
|
29736
|
+
}
|
|
29737
|
+
function normalizedApiDomain(candidate) {
|
|
29738
|
+
const normalized = candidate?.trim().toLowerCase();
|
|
29739
|
+
if (!normalized || normalized.length > 253 || /[\s/@?#]/.test(normalized))
|
|
29740
|
+
return;
|
|
29741
|
+
try {
|
|
29742
|
+
const parsed = new URL(`https://${normalized}`);
|
|
29743
|
+
const exactHostname = parsed.hostname === normalized && !parsed.port;
|
|
29744
|
+
return exactHostname && isCompleteApiHostname(normalized) ? normalized : undefined;
|
|
29745
|
+
} catch {
|
|
29746
|
+
return;
|
|
29747
|
+
}
|
|
29748
|
+
}
|
|
29749
|
+
function canonicalProjectApiOrigin(hostname) {
|
|
29750
|
+
const loopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
29751
|
+
return new URL(`${loopback ? "http" : "https"}://${hostname}`).origin;
|
|
29752
|
+
}
|
|
29753
|
+
function expectedProjectApiOrigin(domain, apiDomain) {
|
|
29754
|
+
if (apiDomain) {
|
|
29755
|
+
const normalizedApi = normalizedApiDomain(apiDomain);
|
|
29756
|
+
return normalizedApi ? canonicalProjectApiOrigin(normalizedApi) : undefined;
|
|
29757
|
+
}
|
|
29758
|
+
const normalizedDomain = normalizedApiDomain(domain);
|
|
29759
|
+
if (!normalizedDomain)
|
|
29760
|
+
return;
|
|
29761
|
+
const hostname = normalizedDomain.startsWith("api.") ? normalizedDomain : `api.${normalizedDomain}`;
|
|
29762
|
+
return canonicalProjectApiOrigin(hostname);
|
|
29763
|
+
}
|
|
29764
|
+
function projectCreateEnvironment(candidate) {
|
|
29765
|
+
return candidate === "test" || candidate === "production" ? candidate : undefined;
|
|
29766
|
+
}
|
|
29767
|
+
function projectCreateFileFailure(identity, credentialFileState, code, httpStatus) {
|
|
29768
|
+
return projectCreateErrorReceipt({
|
|
29769
|
+
ok: false,
|
|
29770
|
+
operation: PROJECT_CREATE_OPERATION,
|
|
29771
|
+
project_ref: identity.projectRef,
|
|
29772
|
+
api_url: identity.apiUrl,
|
|
29773
|
+
remote_created: true,
|
|
29774
|
+
credential_file_state: credentialFileState,
|
|
29775
|
+
...credentialFileState === "absent" ? { credentials_written: false } : {},
|
|
29776
|
+
retry_safe: false,
|
|
29777
|
+
error: { code, http_status: httpStatus }
|
|
29778
|
+
});
|
|
29779
|
+
}
|
|
29780
|
+
async function writeCreatedProjectEnv(preparedEnvFile, credentials, responseStatus, fileOperations) {
|
|
29781
|
+
try {
|
|
29782
|
+
await writeProjectEnvFile(preparedEnvFile, credentials, fileOperations);
|
|
29783
|
+
return null;
|
|
29784
|
+
} catch (error) {
|
|
29785
|
+
const code = error instanceof ProjectCreateEnvError ? error.code : "ENV_FILE_WRITE_FAILED";
|
|
29786
|
+
if (!(error instanceof ProjectCreateEnvError) || error.credentialFileState === "unknown") {
|
|
29787
|
+
return projectCreateFileFailure(credentials, "unknown", code, responseStatus);
|
|
29788
|
+
}
|
|
29789
|
+
return projectCreateFileFailure(credentials, "absent", code, responseStatus);
|
|
29790
|
+
}
|
|
29791
|
+
}
|
|
29792
|
+
function successfulProjectCreate(identity, preparedEnvFile) {
|
|
29793
|
+
return projectCreateReceipt({
|
|
29794
|
+
ok: true,
|
|
29795
|
+
operation: PROJECT_CREATE_OPERATION,
|
|
29796
|
+
project_ref: identity.projectRef,
|
|
29797
|
+
api_url: identity.apiUrl,
|
|
29798
|
+
credentials_written: Boolean(preparedEnvFile),
|
|
29799
|
+
...preparedEnvFile ? {
|
|
29800
|
+
env_file: preparedEnvFile.path,
|
|
29801
|
+
env_file_scope: "project_application"
|
|
29802
|
+
} : {}
|
|
29803
|
+
});
|
|
29804
|
+
}
|
|
29805
|
+
function invalidProjectCreateResponse(responsePayload, responseStatus, expectedApiOrigin) {
|
|
29806
|
+
const identity = parseProjectCreateIdentity(responsePayload, expectedApiOrigin);
|
|
29807
|
+
return identity ? projectCreateFileFailure(identity, "absent", "INVALID_RESPONSE", responseStatus) : projectCreateFailure("OUTCOME_UNKNOWN", responseStatus);
|
|
29808
|
+
}
|
|
29809
|
+
function projectCreateWithoutEnvResponse(response, expectedApiOrigin) {
|
|
29810
|
+
const identity = parseProjectCreateWithoutCredentials(response.data, expectedApiOrigin);
|
|
29811
|
+
return identity ? successfulProjectCreate(identity) : invalidProjectCreateResponse(response.data, response.status, expectedApiOrigin);
|
|
29812
|
+
}
|
|
29813
|
+
async function discardProjectEnvReservation(preparedEnvFile, response, expectedApiOrigin, fileOperations) {
|
|
29814
|
+
const credentialFileState = await discardPreparedProjectEnvFile(preparedEnvFile, fileOperations);
|
|
29815
|
+
if (credentialFileState === "absent")
|
|
29816
|
+
return null;
|
|
29817
|
+
const identity = parseProjectCreateIdentity(response.data, expectedApiOrigin);
|
|
29818
|
+
return identity ? projectCreateFileFailure(identity, "unknown", "ENV_FILE_CLEANUP_FAILED", response.status) : projectCreateFailure("ENV_FILE_CLEANUP_FAILED", response.status);
|
|
29819
|
+
}
|
|
29820
|
+
async function projectCreateWithEnvResponse(response, preparedEnvFile, binding, fileOperations) {
|
|
29821
|
+
const credentials = parseProjectCreateCredentials(response.data, binding.apiOrigin, binding.projectName);
|
|
29822
|
+
if (!credentials) {
|
|
29823
|
+
const cleanupFailure = await discardProjectEnvReservation(preparedEnvFile, response, binding.apiOrigin, fileOperations);
|
|
29824
|
+
return cleanupFailure ?? invalidProjectCreateResponse(response.data, response.status, binding.apiOrigin);
|
|
29825
|
+
}
|
|
29826
|
+
const writeFailure = await writeCreatedProjectEnv(preparedEnvFile, credentials, response.status, fileOperations);
|
|
29827
|
+
return writeFailure ?? successfulProjectCreate(credentials, preparedEnvFile);
|
|
29828
|
+
}
|
|
29829
|
+
async function projectCreateResponse(response, preparedEnvFile, binding, fileOperations) {
|
|
29830
|
+
if (!response.ok || response.status !== 201) {
|
|
29831
|
+
if (preparedEnvFile) {
|
|
29832
|
+
const cleanupFailure = await discardProjectEnvReservation(preparedEnvFile, response, binding.apiOrigin, fileOperations);
|
|
29833
|
+
if (cleanupFailure)
|
|
29834
|
+
return cleanupFailure;
|
|
29835
|
+
}
|
|
29836
|
+
return response.ok ? projectCreateFailure("OUTCOME_UNKNOWN", response.status) : projectCreateMutationFailure(response);
|
|
29837
|
+
}
|
|
29838
|
+
return preparedEnvFile ? projectCreateWithEnvResponse(response, preparedEnvFile, binding, fileOperations) : projectCreateWithoutEnvResponse(response, binding.apiOrigin);
|
|
29839
|
+
}
|
|
28731
29840
|
function failedProjectServiceHttpResponse(response) {
|
|
28732
29841
|
if (response.status === 409 && isRecordPayload(response.data) && response.data.code === AUTH_RUNTIME_MANAGED_BY_OWNER && typeof response.data.authority_project_ref === "string" && SAFE_AUTHORITY_PROJECT_REF.test(response.data.authority_project_ref)) {
|
|
28733
29842
|
const ownerBoundary = {
|
|
@@ -28864,7 +29973,8 @@ function resolveRef(refFromArgs, defaultRef) {
|
|
|
28864
29973
|
throw new Error("'ref' is required for this action");
|
|
28865
29974
|
return ref;
|
|
28866
29975
|
}
|
|
28867
|
-
function registerAdminProjectCliTools(server, http) {
|
|
29976
|
+
function registerAdminProjectCliTools(server, http, options = {}) {
|
|
29977
|
+
const fileOperations = options.projectEnvFileOperations;
|
|
28868
29978
|
server.tool("project", "Platform-level project lifecycle management. Actions: list, create, get, delete, pause, restore, restart, settings, update_settings, api_keys, health, logs, tasks, services, service_control", {
|
|
28869
29979
|
action: withDescription(stringEnum([
|
|
28870
29980
|
"list",
|
|
@@ -28891,6 +30001,8 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28891
30001
|
api_domain: optional(Type.String(), "[create] Explicit API domain"),
|
|
28892
30002
|
auth_domain: optional(Type.String(), "[create] Explicit Auth/OIDC domain"),
|
|
28893
30003
|
studio_domain: optional(Type.String(), "[create] Explicit Studio domain"),
|
|
30004
|
+
env_file: optional(Type.String(), "[create] Linux-only absolute new application env path for parent-bound service-role delivery (0600)"),
|
|
30005
|
+
environment: optional(stringEnum(PROJECT_ENVIRONMENTS), "[create] Required with env_file; application credential environment"),
|
|
28894
30006
|
settings: optional(Type.Record(Type.String(), Type.Unknown()), "[update_settings] Config fields to update"),
|
|
28895
30007
|
log_type: optional(stringEnum(["all", "auth", "database", "api"]), "[logs] Filter by service"),
|
|
28896
30008
|
service: optional(stringEnum(PROJECT_SERVICE_NAMES), "[service_control] Canonical service name"),
|
|
@@ -28905,6 +30017,8 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28905
30017
|
api_domain,
|
|
28906
30018
|
auth_domain,
|
|
28907
30019
|
studio_domain,
|
|
30020
|
+
env_file,
|
|
30021
|
+
environment,
|
|
28908
30022
|
settings,
|
|
28909
30023
|
log_type,
|
|
28910
30024
|
service,
|
|
@@ -28918,6 +30032,23 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28918
30032
|
case "create": {
|
|
28919
30033
|
if (!name)
|
|
28920
30034
|
throw new Error("'name' is required for create");
|
|
30035
|
+
const boundApiOrigin = expectedProjectApiOrigin(domain, api_domain);
|
|
30036
|
+
if (env_file && !boundApiOrigin) {
|
|
30037
|
+
return projectCreateFailure("API_DOMAIN_BINDING_REQUIRED", null);
|
|
30038
|
+
}
|
|
30039
|
+
let preparedEnvFile;
|
|
30040
|
+
if (env_file) {
|
|
30041
|
+
const boundEnvironment = projectCreateEnvironment(environment);
|
|
30042
|
+
if (!boundEnvironment) {
|
|
30043
|
+
return projectCreateFailure("ENVIRONMENT_BINDING_REQUIRED", null);
|
|
30044
|
+
}
|
|
30045
|
+
try {
|
|
30046
|
+
preparedEnvFile = await prepareProjectEnvFile(env_file, boundEnvironment, fileOperations);
|
|
30047
|
+
} catch (error) {
|
|
30048
|
+
const code = error instanceof ProjectCreateEnvError ? error.code : "ENV_FILE_PATH_INVALID";
|
|
30049
|
+
return projectCreateFailure(code, null);
|
|
30050
|
+
}
|
|
30051
|
+
}
|
|
28921
30052
|
const createRequest = {
|
|
28922
30053
|
name,
|
|
28923
30054
|
region: region || "local",
|
|
@@ -28931,8 +30062,9 @@ function registerAdminProjectCliTools(server, http) {
|
|
|
28931
30062
|
createRequest.auth_domain = auth_domain;
|
|
28932
30063
|
if (studio_domain)
|
|
28933
30064
|
createRequest.studio_domain = studio_domain;
|
|
28934
|
-
|
|
28935
|
-
|
|
30065
|
+
if (preparedEnvFile)
|
|
30066
|
+
createRequest.credential_delivery = "response";
|
|
30067
|
+
return projectCreateResponse(await http.post("/v1/projects", createRequest), preparedEnvFile, { projectName: name, apiOrigin: boundApiOrigin }, fileOperations);
|
|
28936
30068
|
}
|
|
28937
30069
|
case "get":
|
|
28938
30070
|
text = ok(await http.get(`/v1/projects/${resolveRef(ref)}`));
|
|
@@ -29340,7 +30472,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
29340
30472
|
// package.json
|
|
29341
30473
|
var package_default = {
|
|
29342
30474
|
name: "@supacloud/admin",
|
|
29343
|
-
version: "0.
|
|
30475
|
+
version: "0.10.0",
|
|
29344
30476
|
description: "Platform administration CLI for SupaCloud operators",
|
|
29345
30477
|
type: "module",
|
|
29346
30478
|
main: "./dist/index.js",
|
|
@@ -29381,31 +30513,6 @@ var package_default = {
|
|
|
29381
30513
|
};
|
|
29382
30514
|
|
|
29383
30515
|
// src/index.ts
|
|
29384
|
-
var platformActionSchema = stringEnum([
|
|
29385
|
-
"metrics",
|
|
29386
|
-
"list_backups",
|
|
29387
|
-
"create_backup",
|
|
29388
|
-
"network",
|
|
29389
|
-
"update_network",
|
|
29390
|
-
"list_orgs",
|
|
29391
|
-
"get_org"
|
|
29392
|
-
]);
|
|
29393
|
-
var sshActionSchema = stringEnum([
|
|
29394
|
-
"ping",
|
|
29395
|
-
"setup",
|
|
29396
|
-
"install",
|
|
29397
|
-
"upgrade",
|
|
29398
|
-
"versions",
|
|
29399
|
-
"diagnose",
|
|
29400
|
-
"exec",
|
|
29401
|
-
"troubleshoot",
|
|
29402
|
-
"container_logs",
|
|
29403
|
-
"tenant_manage",
|
|
29404
|
-
"tenant_list",
|
|
29405
|
-
"tenant_inspect",
|
|
29406
|
-
"tenant_diagnose",
|
|
29407
|
-
"tenant_migrate"
|
|
29408
|
-
]);
|
|
29409
30516
|
function captureTools(register) {
|
|
29410
30517
|
const tools = {};
|
|
29411
30518
|
const server = {
|
|
@@ -29420,6 +30527,31 @@ function captureTools(register) {
|
|
|
29420
30527
|
register(server);
|
|
29421
30528
|
return tools;
|
|
29422
30529
|
}
|
|
30530
|
+
function unavailableTool(schema, message) {
|
|
30531
|
+
return {
|
|
30532
|
+
schema,
|
|
30533
|
+
callback: async () => ({
|
|
30534
|
+
isError: true,
|
|
30535
|
+
content: [{ type: "text", text: message }]
|
|
30536
|
+
})
|
|
30537
|
+
};
|
|
30538
|
+
}
|
|
30539
|
+
function unavailableAdminSchemas() {
|
|
30540
|
+
const schemaOnlyHttp = {};
|
|
30541
|
+
return {
|
|
30542
|
+
project: captureTools((server) => registerAdminProjectCliTools(server, schemaOnlyHttp)).project.schema,
|
|
30543
|
+
platform: captureTools((server) => registerAdvancedTools(server, schemaOnlyHttp)).platform.schema,
|
|
30544
|
+
gateway: captureTools((server) => registerGatewayTools(server, schemaOnlyHttp)).gateway.schema,
|
|
30545
|
+
ssh: captureTools((server) => registerSshTools(server, {})).ssh.schema
|
|
30546
|
+
};
|
|
30547
|
+
}
|
|
30548
|
+
function registerUnavailableAdminTools(tools) {
|
|
30549
|
+
const schemas = unavailableAdminSchemas();
|
|
30550
|
+
tools.project = unavailableTool(schemas.project, "⚠️ Project lifecycle commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
|
|
30551
|
+
tools.platform = unavailableTool(schemas.platform, "⚠️ Platform commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN.");
|
|
30552
|
+
tools.ssh = unavailableTool(schemas.ssh, "⚠️ SSH commands require SUPACLOUD_HOST, SSH credentials, and SUPACLOUD_SSH_HOST_FINGERPRINT.");
|
|
30553
|
+
tools.gateway = unavailableTool(schemas.gateway, "⚠️ Gateway / Caddy commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN (admin privileges).");
|
|
30554
|
+
}
|
|
29423
30555
|
function printHelp(context = resolveSupaCloudContext()) {
|
|
29424
30556
|
console.error(`
|
|
29425
30557
|
╔═══════════════════════════════════════════════════════════╗
|
|
@@ -29429,11 +30561,20 @@ function printHelp(context = resolveSupaCloudContext()) {
|
|
|
29429
30561
|
|
|
29430
30562
|
USAGE
|
|
29431
30563
|
|
|
29432
|
-
supacloud-admin <module> <action> [--flags]
|
|
29433
|
-
supacloud-admin status
|
|
30564
|
+
supacloud-admin [global flags] <module> <action> [--flags]
|
|
30565
|
+
supacloud-admin [global flags] status
|
|
29434
30566
|
supacloud-admin --help
|
|
29435
30567
|
supacloud-admin --version
|
|
29436
30568
|
|
|
30569
|
+
GLOBAL FLAGS
|
|
30570
|
+
|
|
30571
|
+
--env <name> Load .env.supacloud.<name> from the current directory.
|
|
30572
|
+
--env-file <path> Load an exact file that declares SUPACLOUD_ENV.
|
|
30573
|
+
--confirm-production <target> Confirm the exact production project or platform target.
|
|
30574
|
+
|
|
30575
|
+
Global flags may appear before or after the command. --env and --env-file are
|
|
30576
|
+
mutually exclusive, and a selected source is never mixed with another source.
|
|
30577
|
+
|
|
29437
30578
|
EXPECTED CONTEXT
|
|
29438
30579
|
|
|
29439
30580
|
Platform commands typically rely on:
|
|
@@ -29442,17 +30583,25 @@ EXPECTED CONTEXT
|
|
|
29442
30583
|
SUPACLOUD_SSH_HOST_FINGERPRINT=SHA256:...
|
|
29443
30584
|
SUPACLOUD_API_URL
|
|
29444
30585
|
SUPACLOUD_API_TOKEN
|
|
30586
|
+
SUPACLOUD_ENV
|
|
30587
|
+
SUPACLOUD_READ_ONLY
|
|
29445
30588
|
|
|
30589
|
+
Environment: ${context.environment || "(unclassified)"}
|
|
30590
|
+
Context source: ${context.source}${context.sourcePath ? ` (${context.sourcePath})` : ""}
|
|
29446
30591
|
Detected host: ${context.host || "(none)"}
|
|
29447
30592
|
Detected API URL: ${context.apiUrl || "(none)"}
|
|
29448
30593
|
|
|
30594
|
+
SUPACLOUD_READ_ONLY=true blocks every remote write. Production project writes
|
|
30595
|
+
require the exact project ref. Production writes without a project ref use
|
|
30596
|
+
platform:<API host> or host:<SSH host[:port]> as the confirmation target.
|
|
30597
|
+
|
|
29449
30598
|
EXAMPLES
|
|
29450
30599
|
|
|
29451
30600
|
supacloud-admin status
|
|
29452
30601
|
supacloud-admin ssh ping
|
|
29453
30602
|
supacloud-admin ssh versions
|
|
29454
30603
|
supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
|
|
29455
|
-
supacloud-admin project create --name my-app --domain example.com
|
|
30604
|
+
supacloud-admin project create --name my-app --domain example.com --env_file /secure/path/.env.project-credentials.test --environment test
|
|
29456
30605
|
supacloud-admin project list
|
|
29457
30606
|
supacloud-admin project services --ref abc123
|
|
29458
30607
|
supacloud-admin project service_control --ref abc123 --service gotrue --service_action stop
|
|
@@ -29463,7 +30612,18 @@ EXAMPLES
|
|
|
29463
30612
|
supacloud-admin gateway rebuild --ref abc123 --clean
|
|
29464
30613
|
`);
|
|
29465
30614
|
}
|
|
29466
|
-
function
|
|
30615
|
+
function authorizedToolMap(tools, context, confirmProduction) {
|
|
30616
|
+
validateExecutionPolicyCoverage(tools);
|
|
30617
|
+
for (const [moduleName, tool] of Object.entries(tools)) {
|
|
30618
|
+
const callback = tool.callback;
|
|
30619
|
+
tool.callback = async (args) => {
|
|
30620
|
+
authorizeExecution(moduleName, args, { context, confirmProduction });
|
|
30621
|
+
return callback(args);
|
|
30622
|
+
};
|
|
30623
|
+
}
|
|
30624
|
+
return tools;
|
|
30625
|
+
}
|
|
30626
|
+
function createAdminTools(context = resolveSupaCloudContext(), confirmProduction) {
|
|
29467
30627
|
const tools = {
|
|
29468
30628
|
status: {
|
|
29469
30629
|
schema: {},
|
|
@@ -29478,65 +30638,17 @@ function createAdminTools(context = resolveSupaCloudContext()) {
|
|
|
29478
30638
|
hasApiToken: Boolean(context.apiToken),
|
|
29479
30639
|
hasSshKey: Boolean(context.sshKey),
|
|
29480
30640
|
hasSshHostFingerprint: Boolean(context.sshHostFingerprint),
|
|
29481
|
-
|
|
30641
|
+
environment: context.environment || null,
|
|
30642
|
+
production: context.production,
|
|
30643
|
+
readOnly: context.readOnly,
|
|
30644
|
+
source: { kind: context.source, path: context.sourcePath }
|
|
29482
30645
|
}, null, 2)
|
|
29483
30646
|
}
|
|
29484
30647
|
]
|
|
29485
30648
|
})
|
|
29486
30649
|
}
|
|
29487
30650
|
};
|
|
29488
|
-
|
|
29489
|
-
const projectHelpTool = captureTools((server) => registerAdminProjectCliTools(server, {})).project;
|
|
29490
|
-
tools.project = {
|
|
29491
|
-
schema: projectHelpTool.schema,
|
|
29492
|
-
callback: async () => ({
|
|
29493
|
-
isError: true,
|
|
29494
|
-
content: [
|
|
29495
|
-
{
|
|
29496
|
-
type: "text",
|
|
29497
|
-
text: "⚠️ Project lifecycle commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN."
|
|
29498
|
-
}
|
|
29499
|
-
]
|
|
29500
|
-
})
|
|
29501
|
-
};
|
|
29502
|
-
tools.platform = {
|
|
29503
|
-
schema: { action: platformActionSchema },
|
|
29504
|
-
callback: async () => ({
|
|
29505
|
-
isError: true,
|
|
29506
|
-
content: [
|
|
29507
|
-
{
|
|
29508
|
-
type: "text",
|
|
29509
|
-
text: "⚠️ Platform commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN."
|
|
29510
|
-
}
|
|
29511
|
-
]
|
|
29512
|
-
})
|
|
29513
|
-
};
|
|
29514
|
-
tools.ssh = {
|
|
29515
|
-
schema: { action: sshActionSchema },
|
|
29516
|
-
callback: async () => ({
|
|
29517
|
-
isError: true,
|
|
29518
|
-
content: [
|
|
29519
|
-
{
|
|
29520
|
-
type: "text",
|
|
29521
|
-
text: "⚠️ SSH commands require SUPACLOUD_HOST, SSH credentials, and SUPACLOUD_SSH_HOST_FINGERPRINT."
|
|
29522
|
-
}
|
|
29523
|
-
]
|
|
29524
|
-
})
|
|
29525
|
-
};
|
|
29526
|
-
tools.gateway = {
|
|
29527
|
-
schema: { action: Type.String() },
|
|
29528
|
-
callback: async () => ({
|
|
29529
|
-
isError: true,
|
|
29530
|
-
content: [
|
|
29531
|
-
{
|
|
29532
|
-
type: "text",
|
|
29533
|
-
text: "⚠️ Gateway / Caddy commands require SUPACLOUD_API_URL and SUPACLOUD_API_TOKEN (admin privileges)."
|
|
29534
|
-
}
|
|
29535
|
-
]
|
|
29536
|
-
})
|
|
29537
|
-
};
|
|
29538
|
-
};
|
|
29539
|
-
registerAdminHelp();
|
|
30651
|
+
registerUnavailableAdminTools(tools);
|
|
29540
30652
|
if (context.host && context.sshHostFingerprint) {
|
|
29541
30653
|
try {
|
|
29542
30654
|
const ssh = new SshTransport({
|
|
@@ -29551,7 +30663,7 @@ function createAdminTools(context = resolveSupaCloudContext()) {
|
|
|
29551
30663
|
} catch (error) {
|
|
29552
30664
|
const message = error instanceof Error ? error.message : String(error);
|
|
29553
30665
|
tools.ssh = {
|
|
29554
|
-
schema:
|
|
30666
|
+
schema: tools.ssh.schema,
|
|
29555
30667
|
callback: async () => ({
|
|
29556
30668
|
isError: true,
|
|
29557
30669
|
content: [{
|
|
@@ -29563,7 +30675,7 @@ function createAdminTools(context = resolveSupaCloudContext()) {
|
|
|
29563
30675
|
}
|
|
29564
30676
|
} else if (context.host) {
|
|
29565
30677
|
tools.ssh = {
|
|
29566
|
-
schema:
|
|
30678
|
+
schema: tools.ssh.schema,
|
|
29567
30679
|
callback: async () => ({
|
|
29568
30680
|
isError: true,
|
|
29569
30681
|
content: [{
|
|
@@ -29614,31 +30726,38 @@ function createAdminTools(context = resolveSupaCloudContext()) {
|
|
|
29614
30726
|
})
|
|
29615
30727
|
};
|
|
29616
30728
|
}
|
|
29617
|
-
return tools;
|
|
30729
|
+
return authorizedToolMap(tools, context, confirmProduction);
|
|
29618
30730
|
}
|
|
29619
30731
|
async function main() {
|
|
29620
|
-
const
|
|
29621
|
-
if (
|
|
29622
|
-
printHelp();
|
|
29623
|
-
process.exit(0);
|
|
29624
|
-
}
|
|
29625
|
-
if (args.length === 1 && args[0] === "--version") {
|
|
30732
|
+
const rawArgs = process.argv.slice(2);
|
|
30733
|
+
if (rawArgs.length === 1 && rawArgs[0] === "--version") {
|
|
29626
30734
|
console.log(package_default.version);
|
|
29627
30735
|
return;
|
|
29628
30736
|
}
|
|
29629
|
-
const
|
|
30737
|
+
const globalOptions = parseGlobalAdminOptions(rawArgs);
|
|
30738
|
+
const args = globalOptions.args;
|
|
30739
|
+
const context = resolveSupaCloudContext(process.env, process.cwd(), {
|
|
30740
|
+
environmentName: globalOptions.environmentName,
|
|
30741
|
+
envFile: globalOptions.envFile
|
|
30742
|
+
});
|
|
30743
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
30744
|
+
printHelp(context);
|
|
30745
|
+
process.exitCode = 0;
|
|
30746
|
+
return;
|
|
30747
|
+
}
|
|
30748
|
+
const cliTools = createAdminTools(context, globalOptions.confirmProduction);
|
|
29630
30749
|
if (args.length === 1 && cliTools[args[0]]) {
|
|
29631
|
-
const
|
|
29632
|
-
if (
|
|
29633
|
-
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) {
|
|
29634
30753
|
if (chunk.type === "text") {
|
|
29635
30754
|
console.log(chunk.text);
|
|
29636
30755
|
}
|
|
29637
30756
|
}
|
|
29638
30757
|
} else {
|
|
29639
|
-
console.log(JSON.stringify(
|
|
30758
|
+
console.log(JSON.stringify(toolResponse, null, 2));
|
|
29640
30759
|
}
|
|
29641
|
-
if (cliToolResultIsError(
|
|
30760
|
+
if (cliToolResultIsError(toolResponse))
|
|
29642
30761
|
process.exitCode = 1;
|
|
29643
30762
|
return;
|
|
29644
30763
|
}
|
|
@@ -29648,7 +30767,7 @@ function isDirectRun() {
|
|
|
29648
30767
|
if (!process.argv[1])
|
|
29649
30768
|
return false;
|
|
29650
30769
|
try {
|
|
29651
|
-
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(
|
|
30770
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve3(process.argv[1]));
|
|
29652
30771
|
} catch {
|
|
29653
30772
|
return false;
|
|
29654
30773
|
}
|