@supacloud/admin 0.7.1 → 0.7.3
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 +31 -0
- package/dist/index.js +1080 -505
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6360,7 +6360,7 @@ var require_crypto = __commonJS((exports, module) => {
|
|
|
6360
6360
|
MAC_INFO,
|
|
6361
6361
|
bindingAvailable: !!binding,
|
|
6362
6362
|
init: (() => {
|
|
6363
|
-
return new Promise(async (
|
|
6363
|
+
return new Promise(async (resolve, reject) => {
|
|
6364
6364
|
try {
|
|
6365
6365
|
POLY1305_WASM_MODULE = await require_poly1305()();
|
|
6366
6366
|
POLY1305_RESULT_MALLOC = POLY1305_WASM_MODULE._malloc(16);
|
|
@@ -6368,7 +6368,7 @@ var require_crypto = __commonJS((exports, module) => {
|
|
|
6368
6368
|
} catch (ex) {
|
|
6369
6369
|
return reject(ex);
|
|
6370
6370
|
}
|
|
6371
|
-
|
|
6371
|
+
resolve();
|
|
6372
6372
|
});
|
|
6373
6373
|
})(),
|
|
6374
6374
|
NullCipher,
|
|
@@ -7540,7 +7540,7 @@ var require_agent = __commonJS((exports, module) => {
|
|
|
7540
7540
|
var __dirname = "/home/runner/work/supacloud/supacloud/packages/admin/node_modules/ssh2/lib";
|
|
7541
7541
|
var { Socket } = __require("net");
|
|
7542
7542
|
var { Duplex } = __require("stream");
|
|
7543
|
-
var { resolve
|
|
7543
|
+
var { resolve } = __require("path");
|
|
7544
7544
|
var { readFile } = __require("fs");
|
|
7545
7545
|
var { execFile, spawn } = __require("child_process");
|
|
7546
7546
|
var { isParsedKey, parseKey } = require_keyParser();
|
|
@@ -7672,7 +7672,7 @@ var require_agent = __commonJS((exports, module) => {
|
|
|
7672
7672
|
const RET_ERR_BINSTDIN = 13;
|
|
7673
7673
|
const RET_ERR_BINSTDOUT = 14;
|
|
7674
7674
|
const RET_ERR_BADLEN = 15;
|
|
7675
|
-
const EXEPATH =
|
|
7675
|
+
const EXEPATH = resolve(__dirname, "..", "util/pagent.exe");
|
|
7676
7676
|
const ERROR = {
|
|
7677
7677
|
[RET_ERR_BADARGS]: new Error("Invalid pagent.exe arguments"),
|
|
7678
7678
|
[RET_ERR_UNAVAILABLE]: new Error("Pageant is not running"),
|
|
@@ -24921,7 +24921,299 @@ function schemaProperties(schema) {
|
|
|
24921
24921
|
|
|
24922
24922
|
// src/index.ts
|
|
24923
24923
|
import { resolve as resolve2 } from "node:path";
|
|
24924
|
-
import {
|
|
24924
|
+
import { fileURLToPath } from "node:url";
|
|
24925
|
+
import { realpathSync } from "node:fs";
|
|
24926
|
+
|
|
24927
|
+
// src/shared/transports/ssh.ts
|
|
24928
|
+
var import_ssh2 = __toESM(require_lib3(), 1);
|
|
24929
|
+
import { timingSafeEqual } from "node:crypto";
|
|
24930
|
+
import { readFileSync } from "node:fs";
|
|
24931
|
+
var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
24932
|
+
var MAX_CONFIGURABLE_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
24933
|
+
function normalizeSshHostFingerprint(value) {
|
|
24934
|
+
const trimmed = value.trim();
|
|
24935
|
+
const match = trimmed.match(/^SHA256:([A-Za-z0-9+/]{43}=?)$/);
|
|
24936
|
+
if (!match) {
|
|
24937
|
+
throw new Error("SUPACLOUD_SSH_HOST_FINGERPRINT must use OpenSSH SHA256:<base64> format");
|
|
24938
|
+
}
|
|
24939
|
+
const decoded = Buffer.from(match[1], "base64");
|
|
24940
|
+
if (decoded.length !== 32) {
|
|
24941
|
+
throw new Error("SUPACLOUD_SSH_HOST_FINGERPRINT must contain a 32-byte SHA256 digest");
|
|
24942
|
+
}
|
|
24943
|
+
return `SHA256:${match[1].replace(/=+$/, "")}`;
|
|
24944
|
+
}
|
|
24945
|
+
function createHostVerifier(fingerprint) {
|
|
24946
|
+
const expected = Buffer.from(normalizeSshHostFingerprint(fingerprint).slice("SHA256:".length), "base64");
|
|
24947
|
+
return (actualHash) => {
|
|
24948
|
+
if (!/^[a-f0-9]{64}$/i.test(actualHash))
|
|
24949
|
+
return false;
|
|
24950
|
+
const actual = Buffer.from(actualHash, "hex");
|
|
24951
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
24952
|
+
};
|
|
24953
|
+
}
|
|
24954
|
+
function normalizeMaxOutputBytes(value) {
|
|
24955
|
+
const resolved = value ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
24956
|
+
if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > MAX_CONFIGURABLE_OUTPUT_BYTES) {
|
|
24957
|
+
throw new Error(`maxOutputBytes must be an integer between 1 and ${MAX_CONFIGURABLE_OUTPUT_BYTES}`);
|
|
24958
|
+
}
|
|
24959
|
+
return resolved;
|
|
24960
|
+
}
|
|
24961
|
+
|
|
24962
|
+
class BoundedOutputCollector {
|
|
24963
|
+
limit;
|
|
24964
|
+
storage;
|
|
24965
|
+
bytes = 0;
|
|
24966
|
+
truncated = false;
|
|
24967
|
+
constructor(limit) {
|
|
24968
|
+
this.limit = limit;
|
|
24969
|
+
this.storage = Buffer.allocUnsafe(limit);
|
|
24970
|
+
}
|
|
24971
|
+
append(data) {
|
|
24972
|
+
if (this.truncated)
|
|
24973
|
+
return;
|
|
24974
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
24975
|
+
const remaining = this.limit - this.bytes;
|
|
24976
|
+
const copied = Math.min(buffer.length, Math.max(0, remaining));
|
|
24977
|
+
if (copied > 0)
|
|
24978
|
+
buffer.copy(this.storage, this.bytes, 0, copied);
|
|
24979
|
+
this.bytes += copied;
|
|
24980
|
+
this.truncated = buffer.length > copied;
|
|
24981
|
+
}
|
|
24982
|
+
finalize() {
|
|
24983
|
+
let output = this.storage.subarray(0, this.bytes).toString("utf8");
|
|
24984
|
+
if (this.truncated) {
|
|
24985
|
+
const lastNewline = output.lastIndexOf(`
|
|
24986
|
+
`);
|
|
24987
|
+
output = lastNewline >= 0 ? output.slice(0, lastNewline + 1) : "";
|
|
24988
|
+
}
|
|
24989
|
+
const redacted = redactSshOutput(output);
|
|
24990
|
+
if (!this.truncated)
|
|
24991
|
+
return redacted;
|
|
24992
|
+
return `${redacted}${redacted && !redacted.endsWith(`
|
|
24993
|
+
`) ? `
|
|
24994
|
+
` : ""}[TRUNCATED: output exceeded ${this.limit}-byte limit]`;
|
|
24995
|
+
}
|
|
24996
|
+
}
|
|
24997
|
+
var BLOCKED_COMMANDS = [
|
|
24998
|
+
"rm -rf /",
|
|
24999
|
+
"mkfs",
|
|
25000
|
+
"dd if=",
|
|
25001
|
+
":(){:|:&};:",
|
|
25002
|
+
"shutdown",
|
|
25003
|
+
"reboot",
|
|
25004
|
+
"init 0",
|
|
25005
|
+
"init 6",
|
|
25006
|
+
"passwd",
|
|
25007
|
+
"userdel",
|
|
25008
|
+
"usermod -L",
|
|
25009
|
+
"iptables -F",
|
|
25010
|
+
"ufw disable",
|
|
25011
|
+
"crontab -r",
|
|
25012
|
+
"chmod -R 777 /"
|
|
25013
|
+
];
|
|
25014
|
+
function redactSshCommand(command) {
|
|
25015
|
+
return command.replace(/(\b[A-Z0-9_]*(?:PASSWORD|PASS|SECRET|TOKEN|KEY|CREDENTIAL)[A-Z0-9_]*=)(?:"[^"]*"|'[^']*'|[^\s;]+)/gi, "$1[REDACTED]").replace(/(Authorization\s*[:=]\s*)(['"]?)Bearer\s+[^'"\s;]+\2/gi, "$1$2Bearer [REDACTED]$2").replace(/(\b--(?:password|token|secret|api-key)\s+)(\S+)/gi, "$1[REDACTED]").replace(/(postgres(?:ql)?:\/\/[^:\s/]+:)[^@\s]+@/gi, "$1[REDACTED]@");
|
|
25016
|
+
}
|
|
25017
|
+
function redactSshOutput(output) {
|
|
25018
|
+
const redactedLines = output.replace(/^(\s*(?:export\s+)?[A-Z0-9_]*(?:PASSWORD|PASS|SECRET|TOKEN|KEY|CREDENTIAL|DB_URI|DATABASE_URL|DSN)[A-Z0-9_]*\s*=\s*).*$/gim, "$1[REDACTED]");
|
|
25019
|
+
const redactedStructuredFields = redactedLines.replace(/((?:["']?(?:password|pass|secret|token|key|credential|db_uri|database_url|dsn)["']?)\s*:\s*)(?:"[^"]*"|'[^']*'|[^,}\]\r\n]+)/gi, "$1[REDACTED]");
|
|
25020
|
+
return redactSshCommand(redactedStructuredFields);
|
|
25021
|
+
}
|
|
25022
|
+
function isCommandBlocked(command) {
|
|
25023
|
+
const normalized = command.trim().toLowerCase();
|
|
25024
|
+
return BLOCKED_COMMANDS.some((blocked) => normalized.includes(blocked));
|
|
25025
|
+
}
|
|
25026
|
+
var auditLog = [];
|
|
25027
|
+
function auditCommand(command, host, blocked) {
|
|
25028
|
+
const safeCommand = redactSshCommand(command);
|
|
25029
|
+
const entry = { timestamp: new Date().toISOString(), command: safeCommand, host, blocked };
|
|
25030
|
+
auditLog.push(entry);
|
|
25031
|
+
if (auditLog.length > 1000)
|
|
25032
|
+
auditLog.shift();
|
|
25033
|
+
if (blocked) {
|
|
25034
|
+
console.error(`[SSH-AUDIT] BLOCKED command on ${host}: ${safeCommand}`);
|
|
25035
|
+
} else {
|
|
25036
|
+
console.log(`[SSH-AUDIT] Executing on ${host}: ${safeCommand.substring(0, 200)}`);
|
|
25037
|
+
}
|
|
25038
|
+
}
|
|
25039
|
+
class SshConnectionPool {
|
|
25040
|
+
pool = [];
|
|
25041
|
+
maxSize = 3;
|
|
25042
|
+
config;
|
|
25043
|
+
creating = 0;
|
|
25044
|
+
clientFactory;
|
|
25045
|
+
constructor(config, clientFactory) {
|
|
25046
|
+
this.config = config;
|
|
25047
|
+
this.clientFactory = clientFactory;
|
|
25048
|
+
}
|
|
25049
|
+
async createConnection() {
|
|
25050
|
+
const conn = this.clientFactory();
|
|
25051
|
+
return new Promise((resolve, reject) => {
|
|
25052
|
+
const timeout = setTimeout(() => {
|
|
25053
|
+
conn.end();
|
|
25054
|
+
reject(new Error("SSH connection timeout"));
|
|
25055
|
+
}, 15000);
|
|
25056
|
+
conn.on("ready", () => {
|
|
25057
|
+
clearTimeout(timeout);
|
|
25058
|
+
resolve(conn);
|
|
25059
|
+
}).on("error", (err) => {
|
|
25060
|
+
clearTimeout(timeout);
|
|
25061
|
+
reject(err);
|
|
25062
|
+
}).connect({
|
|
25063
|
+
host: this.config.host,
|
|
25064
|
+
port: this.config.port,
|
|
25065
|
+
username: this.config.username,
|
|
25066
|
+
...this.config.privateKeyPath ? { privateKey: readFileSync(this.config.privateKeyPath) } : {},
|
|
25067
|
+
...this.config.password ? { password: this.config.password } : {},
|
|
25068
|
+
hostHash: "sha256",
|
|
25069
|
+
hostVerifier: createHostVerifier(this.config.hostFingerprint),
|
|
25070
|
+
readyTimeout: 15000,
|
|
25071
|
+
keepaliveInterval: 30000
|
|
25072
|
+
});
|
|
25073
|
+
});
|
|
25074
|
+
}
|
|
25075
|
+
async acquire() {
|
|
25076
|
+
if (this.pool.length > 0) {
|
|
25077
|
+
const conn = this.pool.pop();
|
|
25078
|
+
return conn;
|
|
25079
|
+
}
|
|
25080
|
+
if (this.creating < this.maxSize) {
|
|
25081
|
+
this.creating++;
|
|
25082
|
+
try {
|
|
25083
|
+
return await this.createConnection();
|
|
25084
|
+
} finally {
|
|
25085
|
+
this.creating--;
|
|
25086
|
+
}
|
|
25087
|
+
}
|
|
25088
|
+
return this.createConnection();
|
|
25089
|
+
}
|
|
25090
|
+
release(conn) {
|
|
25091
|
+
if (this.pool.length < this.maxSize) {
|
|
25092
|
+
this.pool.push(conn);
|
|
25093
|
+
} else {
|
|
25094
|
+
try {
|
|
25095
|
+
conn.end();
|
|
25096
|
+
} catch {}
|
|
25097
|
+
}
|
|
25098
|
+
}
|
|
25099
|
+
closeAll() {
|
|
25100
|
+
for (const conn of this.pool) {
|
|
25101
|
+
try {
|
|
25102
|
+
conn.end();
|
|
25103
|
+
} catch {}
|
|
25104
|
+
}
|
|
25105
|
+
this.pool = [];
|
|
25106
|
+
}
|
|
25107
|
+
}
|
|
25108
|
+
|
|
25109
|
+
class SshTransport {
|
|
25110
|
+
config;
|
|
25111
|
+
pool;
|
|
25112
|
+
constructor(config, options = {}) {
|
|
25113
|
+
this.config = {
|
|
25114
|
+
...config,
|
|
25115
|
+
hostFingerprint: normalizeSshHostFingerprint(config.hostFingerprint || ""),
|
|
25116
|
+
maxOutputBytes: normalizeMaxOutputBytes(config.maxOutputBytes)
|
|
25117
|
+
};
|
|
25118
|
+
this.pool = new SshConnectionPool(this.config, options.clientFactory ?? (() => new import_ssh2.Client));
|
|
25119
|
+
}
|
|
25120
|
+
async exec(command, timeoutMs = 300000) {
|
|
25121
|
+
if (isCommandBlocked(command)) {
|
|
25122
|
+
auditCommand(command, this.config.host, true);
|
|
25123
|
+
const safeCommand = redactSshCommand(command);
|
|
25124
|
+
return {
|
|
25125
|
+
success: false,
|
|
25126
|
+
stdout: "",
|
|
25127
|
+
stderr: `Command blocked by security policy: "${safeCommand.substring(0, 100)}". ` + `Destructive or system-altering commands are not allowed via supacloud-admin.`,
|
|
25128
|
+
code: 126
|
|
25129
|
+
};
|
|
25130
|
+
}
|
|
25131
|
+
auditCommand(command, this.config.host, false);
|
|
25132
|
+
const conn = await this.pool.acquire();
|
|
25133
|
+
try {
|
|
25134
|
+
return await new Promise((resolve, reject) => {
|
|
25135
|
+
const outputLimit = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
25136
|
+
const stdout = new BoundedOutputCollector(outputLimit);
|
|
25137
|
+
const stderr = new BoundedOutputCollector(outputLimit);
|
|
25138
|
+
const timer = setTimeout(() => {
|
|
25139
|
+
conn.end();
|
|
25140
|
+
reject(new Error(`SSH command timed out after ${timeoutMs}ms`));
|
|
25141
|
+
}, timeoutMs);
|
|
25142
|
+
conn.exec(command, (err, stream) => {
|
|
25143
|
+
if (err) {
|
|
25144
|
+
clearTimeout(timer);
|
|
25145
|
+
return reject(err);
|
|
25146
|
+
}
|
|
25147
|
+
stream.on("close", (code) => {
|
|
25148
|
+
clearTimeout(timer);
|
|
25149
|
+
resolve({
|
|
25150
|
+
success: code === 0,
|
|
25151
|
+
stdout: stdout.finalize(),
|
|
25152
|
+
stderr: stderr.finalize(),
|
|
25153
|
+
code,
|
|
25154
|
+
stdoutTruncated: stdout.truncated,
|
|
25155
|
+
stderrTruncated: stderr.truncated
|
|
25156
|
+
});
|
|
25157
|
+
}).on("data", (data) => {
|
|
25158
|
+
stdout.append(data);
|
|
25159
|
+
}).stderr.on("data", (data) => {
|
|
25160
|
+
stderr.append(data);
|
|
25161
|
+
});
|
|
25162
|
+
});
|
|
25163
|
+
});
|
|
25164
|
+
} finally {
|
|
25165
|
+
this.pool.release(conn);
|
|
25166
|
+
}
|
|
25167
|
+
}
|
|
25168
|
+
async upload(localPath, remotePath) {
|
|
25169
|
+
const conn = await this.pool.acquire();
|
|
25170
|
+
try {
|
|
25171
|
+
return await new Promise((resolve, reject) => {
|
|
25172
|
+
conn.sftp((err, sftp) => {
|
|
25173
|
+
if (err)
|
|
25174
|
+
return reject(err);
|
|
25175
|
+
sftp.fastPut(localPath, remotePath, (err2) => {
|
|
25176
|
+
if (err2)
|
|
25177
|
+
return reject(err2);
|
|
25178
|
+
resolve();
|
|
25179
|
+
});
|
|
25180
|
+
});
|
|
25181
|
+
});
|
|
25182
|
+
} finally {
|
|
25183
|
+
this.pool.release(conn);
|
|
25184
|
+
}
|
|
25185
|
+
}
|
|
25186
|
+
async uploadText(remotePath, content, mode = 384) {
|
|
25187
|
+
auditCommand(`upload ${remotePath} (${Buffer.byteLength(content)} bytes; content redacted)`, this.config.host, false);
|
|
25188
|
+
const conn = await this.pool.acquire();
|
|
25189
|
+
try {
|
|
25190
|
+
await new Promise((resolve, reject) => {
|
|
25191
|
+
conn.sftp((err, sftp) => {
|
|
25192
|
+
if (err)
|
|
25193
|
+
return reject(err);
|
|
25194
|
+
sftp.writeFile(remotePath, content, { mode }, (writeError) => {
|
|
25195
|
+
if (writeError)
|
|
25196
|
+
return reject(writeError);
|
|
25197
|
+
sftp.chmod(remotePath, mode, (chmodError) => {
|
|
25198
|
+
if (chmodError)
|
|
25199
|
+
return reject(chmodError);
|
|
25200
|
+
resolve();
|
|
25201
|
+
});
|
|
25202
|
+
});
|
|
25203
|
+
});
|
|
25204
|
+
});
|
|
25205
|
+
} finally {
|
|
25206
|
+
this.pool.release(conn);
|
|
25207
|
+
}
|
|
25208
|
+
}
|
|
25209
|
+
async ping() {
|
|
25210
|
+
const result = await this.exec("echo pong", 1e4);
|
|
25211
|
+
return result.success && result.stdout.trim() === "pong";
|
|
25212
|
+
}
|
|
25213
|
+
close() {
|
|
25214
|
+
this.pool.closeAll();
|
|
25215
|
+
}
|
|
25216
|
+
}
|
|
24925
25217
|
|
|
24926
25218
|
// src/shared/cli.ts
|
|
24927
25219
|
function coerceCliValue(value) {
|
|
@@ -24933,6 +25225,26 @@ function coerceCliValue(value) {
|
|
|
24933
25225
|
return Number(value);
|
|
24934
25226
|
return value;
|
|
24935
25227
|
}
|
|
25228
|
+
function sanitizedCliDiagnostic(error) {
|
|
25229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25230
|
+
return redactSshOutput(message).replace(/[\r\n\t]+/g, " ").trim().slice(0, 1000);
|
|
25231
|
+
}
|
|
25232
|
+
function nestedCliDiagnostics(error) {
|
|
25233
|
+
if (error instanceof AggregateError) {
|
|
25234
|
+
return error.errors.flatMap((candidate) => nestedCliDiagnostics(candidate));
|
|
25235
|
+
}
|
|
25236
|
+
return [sanitizedCliDiagnostic(error)];
|
|
25237
|
+
}
|
|
25238
|
+
function formatCliError(error) {
|
|
25239
|
+
const summary = sanitizedCliDiagnostic(error);
|
|
25240
|
+
const diagnostics = [...new Set(nestedCliDiagnostics(error))].filter((message) => message && message !== summary);
|
|
25241
|
+
if (diagnostics.length === 0)
|
|
25242
|
+
return summary;
|
|
25243
|
+
return `${summary}
|
|
25244
|
+
Details:
|
|
25245
|
+
${diagnostics.map((message) => ` - ${message}`).join(`
|
|
25246
|
+
`)}`;
|
|
25247
|
+
}
|
|
24936
25248
|
async function runCli(cliTools, args, options = {}) {
|
|
24937
25249
|
const commandName = options.commandName || "supacloud-admin";
|
|
24938
25250
|
const formatAvailableCommands = () => Object.keys(cliTools).filter((k) => !["setup_help", "deploy_web_console"].includes(k)).join(`
|
|
@@ -25031,7 +25343,7 @@ async function runCli(cliTools, args, options = {}) {
|
|
|
25031
25343
|
}
|
|
25032
25344
|
process.exit(result && typeof result === "object" && "isError" in result && result.isError === true ? 1 : 0);
|
|
25033
25345
|
} catch (error) {
|
|
25034
|
-
const message =
|
|
25346
|
+
const message = formatCliError(error);
|
|
25035
25347
|
console.error(`❌ Error: ${message}`);
|
|
25036
25348
|
if (message.includes("required")) {
|
|
25037
25349
|
console.error(`Hint: Pass arguments like --ref YOUR_REF`);
|
|
@@ -25043,14 +25355,14 @@ async function runCli(cliTools, args, options = {}) {
|
|
|
25043
25355
|
// src/shared/context.ts
|
|
25044
25356
|
import { homedir } from "node:os";
|
|
25045
25357
|
import { resolve } from "node:path";
|
|
25046
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
25358
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
25047
25359
|
function readDotEnvFile(cwd) {
|
|
25048
25360
|
const envPath = resolve(cwd, ".env");
|
|
25049
25361
|
if (!existsSync(envPath))
|
|
25050
25362
|
return {};
|
|
25051
25363
|
const values = {};
|
|
25052
25364
|
try {
|
|
25053
|
-
const envContent =
|
|
25365
|
+
const envContent = readFileSync2(envPath, "utf-8");
|
|
25054
25366
|
for (const line of envContent.split(`
|
|
25055
25367
|
`)) {
|
|
25056
25368
|
const match = line.trim().match(/^([^=]+)=(.*)$/);
|
|
@@ -25069,530 +25381,657 @@ function pickValue(env, dotenv, keys) {
|
|
|
25069
25381
|
for (const key of keys) {
|
|
25070
25382
|
const envValue = env[key];
|
|
25071
25383
|
if (envValue)
|
|
25072
|
-
return { value: envValue, source: "env" };
|
|
25073
|
-
}
|
|
25074
|
-
for (const key of keys) {
|
|
25075
|
-
const dotenvValue = dotenv[key];
|
|
25076
|
-
if (dotenvValue)
|
|
25077
|
-
return { value: dotenvValue, source: "dotenv" };
|
|
25078
|
-
}
|
|
25079
|
-
return { value: "", source: "env" };
|
|
25080
|
-
}
|
|
25081
|
-
function detectSource(sources) {
|
|
25082
|
-
const present = new Set(sources.filter((value) => value !== "none"));
|
|
25083
|
-
if (present.size === 0)
|
|
25084
|
-
return "none";
|
|
25085
|
-
if (present.size === 1)
|
|
25086
|
-
return present.has("env") ? "env" : "dotenv";
|
|
25087
|
-
return "mixed";
|
|
25088
|
-
}
|
|
25089
|
-
function normalizeUrl(value) {
|
|
25090
|
-
const trimmed = value.trim().replace(/\/+$/, "");
|
|
25091
|
-
if (!trimmed)
|
|
25092
|
-
return "";
|
|
25093
|
-
try {
|
|
25094
|
-
return new URL(trimmed).toString().replace(/\/+$/, "");
|
|
25095
|
-
} catch {
|
|
25096
|
-
return "";
|
|
25097
|
-
}
|
|
25098
|
-
}
|
|
25099
|
-
function hostFromUrl(value) {
|
|
25100
|
-
try {
|
|
25101
|
-
return new URL(value).hostname;
|
|
25102
|
-
} catch {
|
|
25103
|
-
return "";
|
|
25104
|
-
}
|
|
25105
|
-
}
|
|
25106
|
-
function inferManagementApiUrlFromSupabaseUrl(value, projectRef = "") {
|
|
25107
|
-
const normalized = normalizeUrl(value);
|
|
25108
|
-
if (!normalized)
|
|
25109
|
-
return "";
|
|
25110
|
-
const url = new URL(normalized);
|
|
25111
|
-
const host = url.hostname;
|
|
25112
|
-
if (host.startsWith("api.")) {
|
|
25113
|
-
url.hostname = `studio.${host.slice("api.".length)}`;
|
|
25114
|
-
return url.toString().replace(/\/+$/, "");
|
|
25115
|
-
}
|
|
25116
|
-
const ref = projectRef.trim();
|
|
25117
|
-
if (ref && host.startsWith(`${ref}.api.`)) {
|
|
25118
|
-
url.hostname = `studio-${ref}.${host.slice(`${ref}.api.`.length)}`;
|
|
25119
|
-
return url.toString().replace(/\/+$/, "");
|
|
25120
|
-
}
|
|
25121
|
-
const managedHostMatch = host.match(/^([a-z0-9-]+)\.api\.(.+)$/i);
|
|
25122
|
-
if (managedHostMatch) {
|
|
25123
|
-
url.hostname = `studio-${managedHostMatch[1]}.${managedHostMatch[2]}`;
|
|
25124
|
-
return url.toString().replace(/\/+$/, "");
|
|
25125
|
-
}
|
|
25126
|
-
return normalized;
|
|
25127
|
-
}
|
|
25128
|
-
function resolveSupaCloudContext(env = process.env, cwd = process.cwd()) {
|
|
25129
|
-
const dotenv = readDotEnvFile(cwd);
|
|
25130
|
-
const supabaseUrl = pickValue(env, dotenv, ["SUPABASE_URL"]);
|
|
25131
|
-
const explicitApiUrl = pickValue(env, dotenv, ["SUPACLOUD_API_URL", "SUPACLOUD_MANAGEMENT_API_URL", "MANAGEMENT_API_URL"]);
|
|
25132
|
-
const inferredToken = pickValue(env, dotenv, ["SUPABASE_SERVICE_ROLE_KEY", "SUPACLOUD_API_TOKEN"]);
|
|
25133
|
-
const projectRef = pickValue(env, dotenv, ["SUPACLOUD_PROJECT_REF", "X_PROJECT_REF"]).value;
|
|
25134
|
-
const hostFromEnv = env.SUPACLOUD_HOST;
|
|
25135
|
-
const normalizedSupabaseUrl = normalizeUrl(supabaseUrl.value);
|
|
25136
|
-
const apiUrl = normalizeUrl(explicitApiUrl.value) || inferManagementApiUrlFromSupabaseUrl(normalizedSupabaseUrl, projectRef) || (hostFromEnv ? `http://${hostFromEnv}:9090` : "");
|
|
25137
|
-
const resolvedHostFromUrl = hostFromUrl(apiUrl || normalizedSupabaseUrl);
|
|
25138
|
-
return {
|
|
25139
|
-
host: hostFromEnv ?? resolvedHostFromUrl,
|
|
25140
|
-
sshUser: env.SUPACLOUD_SSH_USER ?? "root",
|
|
25141
|
-
sshPort: parseInt(env.SUPACLOUD_SSH_PORT ?? "22", 10),
|
|
25142
|
-
sshKey: env.SUPACLOUD_SSH_KEY ?? resolve(homedir(), ".ssh", "id_rsa"),
|
|
25143
|
-
sshPass: env.SUPACLOUD_SSH_PASS ?? "",
|
|
25144
|
-
sshHostFingerprint: env.SUPACLOUD_SSH_HOST_FINGERPRINT ?? "",
|
|
25145
|
-
apiUrl,
|
|
25146
|
-
apiToken: env.SUPACLOUD_API_TOKEN ?? inferredToken.value,
|
|
25147
|
-
projectRef,
|
|
25148
|
-
readOnly: env.SUPACLOUD_READ_ONLY === "true",
|
|
25149
|
-
inferredSupabaseUrl: normalizedSupabaseUrl,
|
|
25150
|
-
inferredServiceRoleKey: inferredToken.value,
|
|
25151
|
-
source: detectSource([
|
|
25152
|
-
supabaseUrl.value || explicitApiUrl.value ? supabaseUrl.value ? supabaseUrl.source : explicitApiUrl.source : "none",
|
|
25153
|
-
inferredToken.value ? inferredToken.source : "none"
|
|
25154
|
-
])
|
|
25155
|
-
};
|
|
25156
|
-
}
|
|
25157
|
-
|
|
25158
|
-
// src/shared/transports/http.ts
|
|
25159
|
-
var DEFAULT_TIMEOUT = 30000;
|
|
25160
|
-
var MAX_RETRIES = 2;
|
|
25161
|
-
var RETRY_BASE_DELAY = 500;
|
|
25162
|
-
function isRetryableMethod(method) {
|
|
25163
|
-
const normalizedMethod = (method ?? "GET").toUpperCase();
|
|
25164
|
-
return normalizedMethod === "GET" || normalizedMethod === "HEAD";
|
|
25165
|
-
}
|
|
25166
|
-
function isRetryableError(error) {
|
|
25167
|
-
if (!(error instanceof Error))
|
|
25168
|
-
return false;
|
|
25169
|
-
const networkError = error;
|
|
25170
|
-
return networkError.name === "AbortError" || networkError.code === "ECONNREFUSED" || networkError.code === "ECONNRESET";
|
|
25171
|
-
}
|
|
25172
|
-
async function fetchWithTimeout(url, options) {
|
|
25173
|
-
const controller = new AbortController;
|
|
25174
|
-
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
25175
|
-
try {
|
|
25176
|
-
return await fetch(url, {
|
|
25177
|
-
...options,
|
|
25178
|
-
signal: controller.signal
|
|
25179
|
-
});
|
|
25180
|
-
} finally {
|
|
25181
|
-
clearTimeout(timeout);
|
|
25182
|
-
}
|
|
25183
|
-
}
|
|
25184
|
-
async function fetchWithRetry(url, options) {
|
|
25185
|
-
const retries = isRetryableMethod(options.method) ? MAX_RETRIES : 0;
|
|
25186
|
-
for (let attempt = 0;attempt <= retries; attempt++) {
|
|
25187
|
-
try {
|
|
25188
|
-
const res = await fetchWithTimeout(url, options);
|
|
25189
|
-
if (res.status >= 500 && res.status < 600 && attempt < retries) {
|
|
25190
|
-
const delay = RETRY_BASE_DELAY * Math.pow(2, attempt);
|
|
25191
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
25192
|
-
continue;
|
|
25193
|
-
}
|
|
25194
|
-
return res;
|
|
25195
|
-
} catch (error) {
|
|
25196
|
-
if (attempt < retries && isRetryableError(error)) {
|
|
25197
|
-
const delay = RETRY_BASE_DELAY * Math.pow(2, attempt);
|
|
25198
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
25199
|
-
continue;
|
|
25200
|
-
}
|
|
25201
|
-
throw error;
|
|
25202
|
-
}
|
|
25203
|
-
}
|
|
25204
|
-
throw new Error("Unreachable");
|
|
25205
|
-
}
|
|
25206
|
-
|
|
25207
|
-
class HttpTransport {
|
|
25208
|
-
baseUrl;
|
|
25209
|
-
token;
|
|
25210
|
-
constructor(config) {
|
|
25211
|
-
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
25212
|
-
this.token = config.token;
|
|
25213
|
-
}
|
|
25214
|
-
headers() {
|
|
25215
|
-
return {
|
|
25216
|
-
Authorization: `Bearer ${this.token}`,
|
|
25217
|
-
"Content-Type": "application/json"
|
|
25218
|
-
};
|
|
25219
|
-
}
|
|
25220
|
-
async get(path) {
|
|
25221
|
-
try {
|
|
25222
|
-
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25223
|
-
method: "GET",
|
|
25224
|
-
headers: this.headers()
|
|
25225
|
-
});
|
|
25226
|
-
const data = await res.json().catch(() => null);
|
|
25227
|
-
return { ok: res.ok, status: res.status, data };
|
|
25228
|
-
} catch (error) {
|
|
25229
|
-
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25230
|
-
}
|
|
25231
|
-
}
|
|
25232
|
-
async post(path, body) {
|
|
25233
|
-
try {
|
|
25234
|
-
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25235
|
-
method: "POST",
|
|
25236
|
-
headers: this.headers(),
|
|
25237
|
-
body: body ? JSON.stringify(body) : undefined
|
|
25238
|
-
});
|
|
25239
|
-
const data = await res.json().catch(() => null);
|
|
25240
|
-
return { ok: res.ok, status: res.status, data };
|
|
25241
|
-
} catch (error) {
|
|
25242
|
-
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25243
|
-
}
|
|
25244
|
-
}
|
|
25245
|
-
async postMultipart(path, formData) {
|
|
25246
|
-
try {
|
|
25247
|
-
const headers = { Authorization: `Bearer ${this.token}` };
|
|
25248
|
-
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25249
|
-
method: "POST",
|
|
25250
|
-
headers,
|
|
25251
|
-
body: formData
|
|
25252
|
-
});
|
|
25253
|
-
const data = await res.json().catch(() => null);
|
|
25254
|
-
return { ok: res.ok, status: res.status, data };
|
|
25255
|
-
} catch (error) {
|
|
25256
|
-
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25257
|
-
}
|
|
25258
|
-
}
|
|
25259
|
-
async patch(path, body) {
|
|
25260
|
-
try {
|
|
25261
|
-
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25262
|
-
method: "PATCH",
|
|
25263
|
-
headers: this.headers(),
|
|
25264
|
-
body: body ? JSON.stringify(body) : undefined
|
|
25265
|
-
});
|
|
25266
|
-
const data = await res.json().catch(() => null);
|
|
25267
|
-
return { ok: res.ok, status: res.status, data };
|
|
25268
|
-
} catch (error) {
|
|
25269
|
-
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25270
|
-
}
|
|
25271
|
-
}
|
|
25272
|
-
async put(path, body) {
|
|
25273
|
-
try {
|
|
25274
|
-
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25275
|
-
method: "PUT",
|
|
25276
|
-
headers: this.headers(),
|
|
25277
|
-
body: body ? JSON.stringify(body) : undefined
|
|
25278
|
-
});
|
|
25279
|
-
const data = await res.json().catch(() => null);
|
|
25280
|
-
return { ok: res.ok, status: res.status, data };
|
|
25281
|
-
} catch (error) {
|
|
25282
|
-
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25283
|
-
}
|
|
25284
|
-
}
|
|
25285
|
-
async delete(path) {
|
|
25286
|
-
try {
|
|
25287
|
-
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25288
|
-
method: "DELETE",
|
|
25289
|
-
headers: this.headers()
|
|
25290
|
-
});
|
|
25291
|
-
const data = await res.json().catch(() => null);
|
|
25292
|
-
return { ok: res.ok, status: res.status, data };
|
|
25293
|
-
} catch (error) {
|
|
25294
|
-
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25295
|
-
}
|
|
25296
|
-
}
|
|
25297
|
-
async ping() {
|
|
25298
|
-
const res = await this.get("/v1/projects").catch(() => null);
|
|
25299
|
-
return res?.ok ?? false;
|
|
25300
|
-
}
|
|
25301
|
-
}
|
|
25302
|
-
|
|
25303
|
-
// src/shared/transports/ssh.ts
|
|
25304
|
-
var import_ssh2 = __toESM(require_lib3(), 1);
|
|
25305
|
-
import { timingSafeEqual } from "node:crypto";
|
|
25306
|
-
import { readFileSync as readFileSync2 } from "node:fs";
|
|
25307
|
-
var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
25308
|
-
var MAX_CONFIGURABLE_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
25309
|
-
function normalizeSshHostFingerprint(value) {
|
|
25310
|
-
const trimmed = value.trim();
|
|
25311
|
-
const match = trimmed.match(/^SHA256:([A-Za-z0-9+/]{43}=?)$/);
|
|
25312
|
-
if (!match) {
|
|
25313
|
-
throw new Error("SUPACLOUD_SSH_HOST_FINGERPRINT must use OpenSSH SHA256:<base64> format");
|
|
25314
|
-
}
|
|
25315
|
-
const decoded = Buffer.from(match[1], "base64");
|
|
25316
|
-
if (decoded.length !== 32) {
|
|
25317
|
-
throw new Error("SUPACLOUD_SSH_HOST_FINGERPRINT must contain a 32-byte SHA256 digest");
|
|
25384
|
+
return { value: envValue, source: "env" };
|
|
25318
25385
|
}
|
|
25319
|
-
|
|
25386
|
+
for (const key of keys) {
|
|
25387
|
+
const dotenvValue = dotenv[key];
|
|
25388
|
+
if (dotenvValue)
|
|
25389
|
+
return { value: dotenvValue, source: "dotenv" };
|
|
25390
|
+
}
|
|
25391
|
+
return { value: "", source: "env" };
|
|
25320
25392
|
}
|
|
25321
|
-
function
|
|
25322
|
-
const
|
|
25323
|
-
|
|
25324
|
-
|
|
25325
|
-
|
|
25326
|
-
|
|
25327
|
-
|
|
25328
|
-
};
|
|
25393
|
+
function detectSource(sources) {
|
|
25394
|
+
const present = new Set(sources.filter((value) => value !== "none"));
|
|
25395
|
+
if (present.size === 0)
|
|
25396
|
+
return "none";
|
|
25397
|
+
if (present.size === 1)
|
|
25398
|
+
return present.has("env") ? "env" : "dotenv";
|
|
25399
|
+
return "mixed";
|
|
25329
25400
|
}
|
|
25330
|
-
function
|
|
25331
|
-
const
|
|
25332
|
-
if (!
|
|
25333
|
-
|
|
25401
|
+
function normalizeUrl(value) {
|
|
25402
|
+
const trimmed = value.trim().replace(/\/+$/, "");
|
|
25403
|
+
if (!trimmed)
|
|
25404
|
+
return "";
|
|
25405
|
+
try {
|
|
25406
|
+
return new URL(trimmed).toString().replace(/\/+$/, "");
|
|
25407
|
+
} catch {
|
|
25408
|
+
return "";
|
|
25334
25409
|
}
|
|
25335
|
-
return resolved;
|
|
25336
25410
|
}
|
|
25337
|
-
|
|
25338
|
-
|
|
25339
|
-
|
|
25340
|
-
|
|
25341
|
-
|
|
25342
|
-
truncated = false;
|
|
25343
|
-
constructor(limit) {
|
|
25344
|
-
this.limit = limit;
|
|
25345
|
-
this.storage = Buffer.allocUnsafe(limit);
|
|
25411
|
+
function hostFromUrl(value) {
|
|
25412
|
+
try {
|
|
25413
|
+
return new URL(value).hostname;
|
|
25414
|
+
} catch {
|
|
25415
|
+
return "";
|
|
25346
25416
|
}
|
|
25347
|
-
|
|
25348
|
-
|
|
25349
|
-
|
|
25350
|
-
|
|
25351
|
-
|
|
25352
|
-
|
|
25353
|
-
|
|
25354
|
-
|
|
25355
|
-
|
|
25356
|
-
|
|
25417
|
+
}
|
|
25418
|
+
function inferManagementApiUrlFromSupabaseUrl(value, projectRef = "") {
|
|
25419
|
+
const normalized = normalizeUrl(value);
|
|
25420
|
+
if (!normalized)
|
|
25421
|
+
return "";
|
|
25422
|
+
const url = new URL(normalized);
|
|
25423
|
+
const host = url.hostname;
|
|
25424
|
+
if (host.startsWith("api.")) {
|
|
25425
|
+
url.hostname = `studio.${host.slice("api.".length)}`;
|
|
25426
|
+
return url.toString().replace(/\/+$/, "");
|
|
25357
25427
|
}
|
|
25358
|
-
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
|
|
25362
|
-
`);
|
|
25363
|
-
output = lastNewline >= 0 ? output.slice(0, lastNewline + 1) : "";
|
|
25364
|
-
}
|
|
25365
|
-
const redacted = redactSshOutput(output);
|
|
25366
|
-
if (!this.truncated)
|
|
25367
|
-
return redacted;
|
|
25368
|
-
return `${redacted}${redacted && !redacted.endsWith(`
|
|
25369
|
-
`) ? `
|
|
25370
|
-
` : ""}[TRUNCATED: output exceeded ${this.limit}-byte limit]`;
|
|
25428
|
+
const ref = projectRef.trim();
|
|
25429
|
+
if (ref && host.startsWith(`${ref}.api.`)) {
|
|
25430
|
+
url.hostname = `studio-${ref}.${host.slice(`${ref}.api.`.length)}`;
|
|
25431
|
+
return url.toString().replace(/\/+$/, "");
|
|
25371
25432
|
}
|
|
25433
|
+
const managedHostMatch = host.match(/^([a-z0-9-]+)\.api\.(.+)$/i);
|
|
25434
|
+
if (managedHostMatch) {
|
|
25435
|
+
url.hostname = `studio-${managedHostMatch[1]}.${managedHostMatch[2]}`;
|
|
25436
|
+
return url.toString().replace(/\/+$/, "");
|
|
25437
|
+
}
|
|
25438
|
+
return normalized;
|
|
25372
25439
|
}
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
"
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
"
|
|
25379
|
-
|
|
25380
|
-
|
|
25381
|
-
|
|
25382
|
-
|
|
25383
|
-
|
|
25384
|
-
|
|
25385
|
-
|
|
25386
|
-
|
|
25387
|
-
|
|
25388
|
-
|
|
25389
|
-
|
|
25390
|
-
|
|
25391
|
-
|
|
25392
|
-
|
|
25393
|
-
|
|
25394
|
-
|
|
25395
|
-
|
|
25396
|
-
|
|
25440
|
+
function resolveSupaCloudContext(env = process.env, cwd = process.cwd()) {
|
|
25441
|
+
const dotenv = readDotEnvFile(cwd);
|
|
25442
|
+
const supabaseUrl = pickValue(env, dotenv, ["SUPABASE_URL"]);
|
|
25443
|
+
const explicitApiUrl = pickValue(env, dotenv, ["SUPACLOUD_API_URL", "SUPACLOUD_MANAGEMENT_API_URL", "MANAGEMENT_API_URL"]);
|
|
25444
|
+
const inferredToken = pickValue(env, dotenv, ["SUPABASE_SERVICE_ROLE_KEY", "SUPACLOUD_API_TOKEN"]);
|
|
25445
|
+
const projectRef = pickValue(env, dotenv, ["SUPACLOUD_PROJECT_REF", "X_PROJECT_REF"]).value;
|
|
25446
|
+
const hostFromEnv = env.SUPACLOUD_HOST;
|
|
25447
|
+
const normalizedSupabaseUrl = normalizeUrl(supabaseUrl.value);
|
|
25448
|
+
const apiUrl = normalizeUrl(explicitApiUrl.value) || inferManagementApiUrlFromSupabaseUrl(normalizedSupabaseUrl, projectRef) || (hostFromEnv ? `http://${hostFromEnv}:9090` : "");
|
|
25449
|
+
const resolvedHostFromUrl = hostFromUrl(apiUrl || normalizedSupabaseUrl);
|
|
25450
|
+
return {
|
|
25451
|
+
host: hostFromEnv ?? resolvedHostFromUrl,
|
|
25452
|
+
sshUser: env.SUPACLOUD_SSH_USER ?? "root",
|
|
25453
|
+
sshPort: parseInt(env.SUPACLOUD_SSH_PORT ?? "22", 10),
|
|
25454
|
+
sshKey: env.SUPACLOUD_SSH_KEY ?? resolve(homedir(), ".ssh", "id_rsa"),
|
|
25455
|
+
sshPass: env.SUPACLOUD_SSH_PASS ?? "",
|
|
25456
|
+
sshHostFingerprint: env.SUPACLOUD_SSH_HOST_FINGERPRINT ?? "",
|
|
25457
|
+
apiUrl,
|
|
25458
|
+
apiToken: env.SUPACLOUD_API_TOKEN ?? inferredToken.value,
|
|
25459
|
+
projectRef,
|
|
25460
|
+
readOnly: env.SUPACLOUD_READ_ONLY === "true",
|
|
25461
|
+
inferredSupabaseUrl: normalizedSupabaseUrl,
|
|
25462
|
+
inferredServiceRoleKey: inferredToken.value,
|
|
25463
|
+
source: detectSource([
|
|
25464
|
+
supabaseUrl.value || explicitApiUrl.value ? supabaseUrl.value ? supabaseUrl.source : explicitApiUrl.source : "none",
|
|
25465
|
+
inferredToken.value ? inferredToken.source : "none"
|
|
25466
|
+
])
|
|
25467
|
+
};
|
|
25397
25468
|
}
|
|
25398
|
-
|
|
25399
|
-
|
|
25400
|
-
|
|
25469
|
+
|
|
25470
|
+
// src/shared/transports/http.ts
|
|
25471
|
+
var DEFAULT_TIMEOUT = 30000;
|
|
25472
|
+
var MAX_RETRIES = 2;
|
|
25473
|
+
var RETRY_BASE_DELAY = 500;
|
|
25474
|
+
function isRetryableMethod(method) {
|
|
25475
|
+
const normalizedMethod = (method ?? "GET").toUpperCase();
|
|
25476
|
+
return normalizedMethod === "GET" || normalizedMethod === "HEAD";
|
|
25401
25477
|
}
|
|
25402
|
-
|
|
25403
|
-
|
|
25404
|
-
|
|
25405
|
-
const
|
|
25406
|
-
|
|
25407
|
-
if (auditLog.length > 1000)
|
|
25408
|
-
auditLog.shift();
|
|
25409
|
-
if (blocked) {
|
|
25410
|
-
console.error(`[SSH-AUDIT] BLOCKED command on ${host}: ${safeCommand}`);
|
|
25411
|
-
} else {
|
|
25412
|
-
console.log(`[SSH-AUDIT] Executing on ${host}: ${safeCommand.substring(0, 200)}`);
|
|
25413
|
-
}
|
|
25478
|
+
function isRetryableError(error) {
|
|
25479
|
+
if (!(error instanceof Error))
|
|
25480
|
+
return false;
|
|
25481
|
+
const networkError = error;
|
|
25482
|
+
return networkError.name === "AbortError" || networkError.code === "ECONNREFUSED" || networkError.code === "ECONNRESET";
|
|
25414
25483
|
}
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
this.config = config;
|
|
25423
|
-
this.clientFactory = clientFactory;
|
|
25424
|
-
}
|
|
25425
|
-
async createConnection() {
|
|
25426
|
-
const conn = this.clientFactory();
|
|
25427
|
-
return new Promise((resolve2, reject) => {
|
|
25428
|
-
const timeout = setTimeout(() => {
|
|
25429
|
-
conn.end();
|
|
25430
|
-
reject(new Error("SSH connection timeout"));
|
|
25431
|
-
}, 15000);
|
|
25432
|
-
conn.on("ready", () => {
|
|
25433
|
-
clearTimeout(timeout);
|
|
25434
|
-
resolve2(conn);
|
|
25435
|
-
}).on("error", (err) => {
|
|
25436
|
-
clearTimeout(timeout);
|
|
25437
|
-
reject(err);
|
|
25438
|
-
}).connect({
|
|
25439
|
-
host: this.config.host,
|
|
25440
|
-
port: this.config.port,
|
|
25441
|
-
username: this.config.username,
|
|
25442
|
-
...this.config.privateKeyPath ? { privateKey: readFileSync2(this.config.privateKeyPath) } : {},
|
|
25443
|
-
...this.config.password ? { password: this.config.password } : {},
|
|
25444
|
-
hostHash: "sha256",
|
|
25445
|
-
hostVerifier: createHostVerifier(this.config.hostFingerprint),
|
|
25446
|
-
readyTimeout: 15000,
|
|
25447
|
-
keepaliveInterval: 30000
|
|
25448
|
-
});
|
|
25484
|
+
async function fetchWithTimeout(url, options) {
|
|
25485
|
+
const controller = new AbortController;
|
|
25486
|
+
const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
25487
|
+
try {
|
|
25488
|
+
return await fetch(url, {
|
|
25489
|
+
...options,
|
|
25490
|
+
signal: controller.signal
|
|
25449
25491
|
});
|
|
25492
|
+
} finally {
|
|
25493
|
+
clearTimeout(timeout);
|
|
25450
25494
|
}
|
|
25451
|
-
|
|
25452
|
-
|
|
25453
|
-
|
|
25454
|
-
|
|
25455
|
-
|
|
25456
|
-
|
|
25457
|
-
|
|
25458
|
-
|
|
25459
|
-
|
|
25460
|
-
|
|
25461
|
-
this.creating--;
|
|
25495
|
+
}
|
|
25496
|
+
async function fetchWithRetry(url, options) {
|
|
25497
|
+
const retries = isRetryableMethod(options.method) ? MAX_RETRIES : 0;
|
|
25498
|
+
for (let attempt = 0;attempt <= retries; attempt++) {
|
|
25499
|
+
try {
|
|
25500
|
+
const res = await fetchWithTimeout(url, options);
|
|
25501
|
+
if (res.status >= 500 && res.status < 600 && attempt < retries) {
|
|
25502
|
+
const delay = RETRY_BASE_DELAY * Math.pow(2, attempt);
|
|
25503
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
25504
|
+
continue;
|
|
25462
25505
|
}
|
|
25506
|
+
return res;
|
|
25507
|
+
} catch (error) {
|
|
25508
|
+
if (attempt < retries && isRetryableError(error)) {
|
|
25509
|
+
const delay = RETRY_BASE_DELAY * Math.pow(2, attempt);
|
|
25510
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
25511
|
+
continue;
|
|
25512
|
+
}
|
|
25513
|
+
throw error;
|
|
25463
25514
|
}
|
|
25464
|
-
return this.createConnection();
|
|
25465
|
-
}
|
|
25466
|
-
release(conn) {
|
|
25467
|
-
if (this.pool.length < this.maxSize) {
|
|
25468
|
-
this.pool.push(conn);
|
|
25469
|
-
} else {
|
|
25470
|
-
try {
|
|
25471
|
-
conn.end();
|
|
25472
|
-
} catch {}
|
|
25473
|
-
}
|
|
25474
|
-
}
|
|
25475
|
-
closeAll() {
|
|
25476
|
-
for (const conn of this.pool) {
|
|
25477
|
-
try {
|
|
25478
|
-
conn.end();
|
|
25479
|
-
} catch {}
|
|
25480
|
-
}
|
|
25481
|
-
this.pool = [];
|
|
25482
25515
|
}
|
|
25516
|
+
throw new Error("Unreachable");
|
|
25483
25517
|
}
|
|
25484
25518
|
|
|
25485
|
-
class
|
|
25486
|
-
|
|
25487
|
-
|
|
25488
|
-
constructor(config
|
|
25489
|
-
this.
|
|
25490
|
-
|
|
25491
|
-
|
|
25492
|
-
|
|
25519
|
+
class HttpTransport {
|
|
25520
|
+
baseUrl;
|
|
25521
|
+
token;
|
|
25522
|
+
constructor(config) {
|
|
25523
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
25524
|
+
this.token = config.token;
|
|
25525
|
+
}
|
|
25526
|
+
headers() {
|
|
25527
|
+
return {
|
|
25528
|
+
Authorization: `Bearer ${this.token}`,
|
|
25529
|
+
"Content-Type": "application/json"
|
|
25493
25530
|
};
|
|
25494
|
-
this.pool = new SshConnectionPool(this.config, options.clientFactory ?? (() => new import_ssh2.Client));
|
|
25495
25531
|
}
|
|
25496
|
-
async
|
|
25497
|
-
|
|
25498
|
-
|
|
25499
|
-
|
|
25500
|
-
|
|
25501
|
-
|
|
25502
|
-
|
|
25503
|
-
|
|
25504
|
-
|
|
25505
|
-
};
|
|
25532
|
+
async get(path) {
|
|
25533
|
+
try {
|
|
25534
|
+
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25535
|
+
method: "GET",
|
|
25536
|
+
headers: this.headers()
|
|
25537
|
+
});
|
|
25538
|
+
const data = await res.json().catch(() => null);
|
|
25539
|
+
return { ok: res.ok, status: res.status, data };
|
|
25540
|
+
} catch (error) {
|
|
25541
|
+
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25506
25542
|
}
|
|
25507
|
-
|
|
25508
|
-
|
|
25543
|
+
}
|
|
25544
|
+
async post(path, body) {
|
|
25509
25545
|
try {
|
|
25510
|
-
|
|
25511
|
-
|
|
25512
|
-
|
|
25513
|
-
|
|
25514
|
-
const timer = setTimeout(() => {
|
|
25515
|
-
conn.end();
|
|
25516
|
-
reject(new Error(`SSH command timed out after ${timeoutMs}ms`));
|
|
25517
|
-
}, timeoutMs);
|
|
25518
|
-
conn.exec(command, (err, stream) => {
|
|
25519
|
-
if (err) {
|
|
25520
|
-
clearTimeout(timer);
|
|
25521
|
-
return reject(err);
|
|
25522
|
-
}
|
|
25523
|
-
stream.on("close", (code) => {
|
|
25524
|
-
clearTimeout(timer);
|
|
25525
|
-
resolve2({
|
|
25526
|
-
success: code === 0,
|
|
25527
|
-
stdout: stdout.finalize(),
|
|
25528
|
-
stderr: stderr.finalize(),
|
|
25529
|
-
code,
|
|
25530
|
-
stdoutTruncated: stdout.truncated,
|
|
25531
|
-
stderrTruncated: stderr.truncated
|
|
25532
|
-
});
|
|
25533
|
-
}).on("data", (data) => {
|
|
25534
|
-
stdout.append(data);
|
|
25535
|
-
}).stderr.on("data", (data) => {
|
|
25536
|
-
stderr.append(data);
|
|
25537
|
-
});
|
|
25538
|
-
});
|
|
25546
|
+
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25547
|
+
method: "POST",
|
|
25548
|
+
headers: this.headers(),
|
|
25549
|
+
body: body ? JSON.stringify(body) : undefined
|
|
25539
25550
|
});
|
|
25540
|
-
|
|
25541
|
-
|
|
25551
|
+
const data = await res.json().catch(() => null);
|
|
25552
|
+
return { ok: res.ok, status: res.status, data };
|
|
25553
|
+
} catch (error) {
|
|
25554
|
+
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25542
25555
|
}
|
|
25543
25556
|
}
|
|
25544
|
-
async
|
|
25545
|
-
const conn = await this.pool.acquire();
|
|
25557
|
+
async postMultipart(path, formData) {
|
|
25546
25558
|
try {
|
|
25547
|
-
|
|
25548
|
-
|
|
25549
|
-
|
|
25550
|
-
|
|
25551
|
-
|
|
25552
|
-
if (err2)
|
|
25553
|
-
return reject(err2);
|
|
25554
|
-
resolve2();
|
|
25555
|
-
});
|
|
25556
|
-
});
|
|
25559
|
+
const headers = { Authorization: `Bearer ${this.token}` };
|
|
25560
|
+
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25561
|
+
method: "POST",
|
|
25562
|
+
headers,
|
|
25563
|
+
body: formData
|
|
25557
25564
|
});
|
|
25558
|
-
|
|
25559
|
-
|
|
25565
|
+
const data = await res.json().catch(() => null);
|
|
25566
|
+
return { ok: res.ok, status: res.status, data };
|
|
25567
|
+
} catch (error) {
|
|
25568
|
+
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25560
25569
|
}
|
|
25561
25570
|
}
|
|
25562
|
-
async
|
|
25563
|
-
auditCommand(`upload ${remotePath} (${Buffer.byteLength(content)} bytes; content redacted)`, this.config.host, false);
|
|
25564
|
-
const conn = await this.pool.acquire();
|
|
25571
|
+
async patch(path, body) {
|
|
25565
25572
|
try {
|
|
25566
|
-
|
|
25567
|
-
|
|
25568
|
-
|
|
25569
|
-
|
|
25570
|
-
sftp.writeFile(remotePath, content, { mode }, (writeError) => {
|
|
25571
|
-
if (writeError)
|
|
25572
|
-
return reject(writeError);
|
|
25573
|
-
sftp.chmod(remotePath, mode, (chmodError) => {
|
|
25574
|
-
if (chmodError)
|
|
25575
|
-
return reject(chmodError);
|
|
25576
|
-
resolve2();
|
|
25577
|
-
});
|
|
25578
|
-
});
|
|
25579
|
-
});
|
|
25573
|
+
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25574
|
+
method: "PATCH",
|
|
25575
|
+
headers: this.headers(),
|
|
25576
|
+
body: body ? JSON.stringify(body) : undefined
|
|
25580
25577
|
});
|
|
25581
|
-
|
|
25582
|
-
|
|
25578
|
+
const data = await res.json().catch(() => null);
|
|
25579
|
+
return { ok: res.ok, status: res.status, data };
|
|
25580
|
+
} catch (error) {
|
|
25581
|
+
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25583
25582
|
}
|
|
25584
25583
|
}
|
|
25585
|
-
async
|
|
25586
|
-
|
|
25587
|
-
|
|
25584
|
+
async put(path, body) {
|
|
25585
|
+
try {
|
|
25586
|
+
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25587
|
+
method: "PUT",
|
|
25588
|
+
headers: this.headers(),
|
|
25589
|
+
body: body ? JSON.stringify(body) : undefined
|
|
25590
|
+
});
|
|
25591
|
+
const data = await res.json().catch(() => null);
|
|
25592
|
+
return { ok: res.ok, status: res.status, data };
|
|
25593
|
+
} catch (error) {
|
|
25594
|
+
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25595
|
+
}
|
|
25588
25596
|
}
|
|
25589
|
-
|
|
25590
|
-
|
|
25597
|
+
async delete(path) {
|
|
25598
|
+
try {
|
|
25599
|
+
const res = await fetchWithRetry(`${this.baseUrl}${path}`, {
|
|
25600
|
+
method: "DELETE",
|
|
25601
|
+
headers: this.headers()
|
|
25602
|
+
});
|
|
25603
|
+
const data = await res.json().catch(() => null);
|
|
25604
|
+
return { ok: res.ok, status: res.status, data };
|
|
25605
|
+
} catch (error) {
|
|
25606
|
+
return { ok: false, status: 500, data: { error: "Network Error", details: error.message } };
|
|
25607
|
+
}
|
|
25608
|
+
}
|
|
25609
|
+
async ping() {
|
|
25610
|
+
const res = await this.get("/v1/projects").catch(() => null);
|
|
25611
|
+
return res?.ok ?? false;
|
|
25591
25612
|
}
|
|
25592
25613
|
}
|
|
25593
25614
|
|
|
25594
25615
|
// src/shared/tools/ssh-tools.ts
|
|
25595
25616
|
import { randomUUID } from "node:crypto";
|
|
25617
|
+
|
|
25618
|
+
// ../../scripts/lib/release_assets.sh
|
|
25619
|
+
var release_assets_default = `#!/usr/bin/env bash
|
|
25620
|
+
|
|
25621
|
+
SUPACLOUD_GITHUB_REPOSITORY="\${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"
|
|
25622
|
+
SUPACLOUD_RELEASES_API="\${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/releases}"
|
|
25623
|
+
SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="\${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-\${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"
|
|
25624
|
+
SUPACLOUD_GH_VERSION="\${SUPACLOUD_GH_VERSION:-2.96.0}"
|
|
25625
|
+
SUPACLOUD_GH_MIN_VERSION="\${SUPACLOUD_GH_MIN_VERSION:-2.51.0}"
|
|
25626
|
+
SUPACLOUD_GH_AMD64_SHA256="\${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"
|
|
25627
|
+
SUPACLOUD_GH_ARM64_SHA256="\${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"
|
|
25628
|
+
|
|
25629
|
+
supacloud_component_tag() {
|
|
25630
|
+
local component="$1"
|
|
25631
|
+
local version="$2"
|
|
25632
|
+
case "$version" in
|
|
25633
|
+
"\${component}-v"*) printf '%s' "$version" ;;
|
|
25634
|
+
v*) printf '%s-%s' "$component" "$version" ;;
|
|
25635
|
+
*) printf '%s-v%s' "$component" "$version" ;;
|
|
25636
|
+
esac
|
|
25637
|
+
}
|
|
25638
|
+
|
|
25639
|
+
supacloud_select_release() {
|
|
25640
|
+
local component="$1"
|
|
25641
|
+
shift
|
|
25642
|
+
local required_assets_json
|
|
25643
|
+
[[ $# -gt 0 ]] || {
|
|
25644
|
+
echo "at least one required release asset must be specified" >&2
|
|
25645
|
+
return 1
|
|
25646
|
+
}
|
|
25647
|
+
required_assets_json=$(printf '%s\\n' "$@" | jq -Rsc 'split("\\n")[:-1]')
|
|
25648
|
+
jq -ce --arg prefix "\${component}-v" --argjson required "$required_assets_json" '
|
|
25649
|
+
map(select(
|
|
25650
|
+
(.draft | not)
|
|
25651
|
+
and (.prerelease | not)
|
|
25652
|
+
and (.tag_name | startswith($prefix))
|
|
25653
|
+
and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
|
|
25654
|
+
and any(.assets[]?; .name == "SHA256SUMS")
|
|
25655
|
+
))
|
|
25656
|
+
| first
|
|
25657
|
+
// error("no matching component release contains all required assets and SHA256SUMS")
|
|
25658
|
+
'
|
|
25659
|
+
}
|
|
25660
|
+
|
|
25661
|
+
supacloud_fetch_component_release() {
|
|
25662
|
+
local component="$1"
|
|
25663
|
+
local version="\${2:-latest}"
|
|
25664
|
+
shift 2
|
|
25665
|
+
local required_assets=("$@")
|
|
25666
|
+
local required_assets_json
|
|
25667
|
+
local response
|
|
25668
|
+
[[ \${#required_assets[@]} -gt 0 ]] || {
|
|
25669
|
+
echo "at least one required release asset must be specified" >&2
|
|
25670
|
+
return 1
|
|
25671
|
+
}
|
|
25672
|
+
required_assets_json=$(printf '%s\\n' "\${required_assets[@]}" | jq -Rsc 'split("\\n")[:-1]')
|
|
25673
|
+
|
|
25674
|
+
if [[ -n "$version" && "$version" != "latest" ]]; then
|
|
25675
|
+
local tag
|
|
25676
|
+
tag=$(supacloud_component_tag "$component" "$version")
|
|
25677
|
+
response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}/tags/\${tag}") || return 1
|
|
25678
|
+
jq -ce --argjson required "$required_assets_json" '
|
|
25679
|
+
select(
|
|
25680
|
+
(.draft | not)
|
|
25681
|
+
and (.prerelease | not)
|
|
25682
|
+
and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
|
|
25683
|
+
and any(.assets[]?; .name == "SHA256SUMS")
|
|
25684
|
+
)
|
|
25685
|
+
// error("release does not contain all required assets and SHA256SUMS")
|
|
25686
|
+
' <<< "$response"
|
|
25687
|
+
return
|
|
25688
|
+
fi
|
|
25689
|
+
|
|
25690
|
+
response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}?per_page=100") || return 1
|
|
25691
|
+
supacloud_select_release "$component" "\${required_assets[@]}" <<< "$response"
|
|
25692
|
+
}
|
|
25693
|
+
|
|
25694
|
+
supacloud_fetch_release_json() {
|
|
25695
|
+
local url="$1"
|
|
25696
|
+
local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
|
|
25697
|
+
if curl -fsSL --retry 3 --connect-timeout 15 "$url"; then
|
|
25698
|
+
return 0
|
|
25699
|
+
fi
|
|
25700
|
+
if [[ -n "$proxy" ]]; then
|
|
25701
|
+
curl -fsSL --retry 3 --connect-timeout 15 "\${proxy%/}/\${url}"
|
|
25702
|
+
return
|
|
25703
|
+
fi
|
|
25704
|
+
return 1
|
|
25705
|
+
}
|
|
25706
|
+
|
|
25707
|
+
supacloud_release_asset_url() {
|
|
25708
|
+
local release_json="$1"
|
|
25709
|
+
local asset_name="$2"
|
|
25710
|
+
jq -er --arg asset "$asset_name" '
|
|
25711
|
+
first(.assets[]? | select(.name == $asset) | .browser_download_url)
|
|
25712
|
+
// error("release asset URL is missing")
|
|
25713
|
+
' <<< "$release_json"
|
|
25714
|
+
}
|
|
25715
|
+
|
|
25716
|
+
supacloud_download_url() {
|
|
25717
|
+
local url="$1"
|
|
25718
|
+
local output="$2"
|
|
25719
|
+
local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
|
|
25720
|
+
|
|
25721
|
+
if curl -fL --retry 3 --connect-timeout 15 -o "$output" "$url"; then
|
|
25722
|
+
return 0
|
|
25723
|
+
fi
|
|
25724
|
+
if [[ -n "$proxy" ]]; then
|
|
25725
|
+
curl -fL --retry 3 --connect-timeout 15 -o "$output" "\${proxy%/}/\${url}"
|
|
25726
|
+
return
|
|
25727
|
+
fi
|
|
25728
|
+
return 1
|
|
25729
|
+
}
|
|
25730
|
+
|
|
25731
|
+
supacloud_verify_checksum() {
|
|
25732
|
+
local artifact_file="$1"
|
|
25733
|
+
local asset_name="$2"
|
|
25734
|
+
local checksum_file="$3"
|
|
25735
|
+
local expected
|
|
25736
|
+
expected=$(awk -v asset="$asset_name" '$2 == asset || $2 == "*" asset { print $1; exit }' "$checksum_file")
|
|
25737
|
+
if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then
|
|
25738
|
+
echo "SHA256SUMS does not contain a valid checksum for \${asset_name}" >&2
|
|
25739
|
+
return 1
|
|
25740
|
+
fi
|
|
25741
|
+
|
|
25742
|
+
local actual
|
|
25743
|
+
actual=$(sha256sum "$artifact_file" | awk '{print $1}')
|
|
25744
|
+
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
|
|
25745
|
+
expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
|
|
25746
|
+
if [[ "$actual" != "$expected" ]]; then
|
|
25747
|
+
echo "SHA256 mismatch for \${asset_name}" >&2
|
|
25748
|
+
return 1
|
|
25749
|
+
fi
|
|
25750
|
+
}
|
|
25751
|
+
|
|
25752
|
+
supacloud_validate_binary() {
|
|
25753
|
+
local artifact_file="$1"
|
|
25754
|
+
local asset_name="$2"
|
|
25755
|
+
local description
|
|
25756
|
+
description=$(file -b "$artifact_file")
|
|
25757
|
+
if [[ "$description" != *ELF* ]]; then
|
|
25758
|
+
echo "\${asset_name} is not an ELF binary: \${description}" >&2
|
|
25759
|
+
return 1
|
|
25760
|
+
fi
|
|
25761
|
+
|
|
25762
|
+
case "$asset_name" in
|
|
25763
|
+
*amd64)
|
|
25764
|
+
[[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {
|
|
25765
|
+
echo "\${asset_name} does not contain an x86-64 ELF binary" >&2
|
|
25766
|
+
return 1
|
|
25767
|
+
}
|
|
25768
|
+
;;
|
|
25769
|
+
*arm64)
|
|
25770
|
+
[[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {
|
|
25771
|
+
echo "\${asset_name} does not contain an arm64 ELF binary" >&2
|
|
25772
|
+
return 1
|
|
25773
|
+
}
|
|
25774
|
+
;;
|
|
25775
|
+
esac
|
|
25776
|
+
}
|
|
25777
|
+
|
|
25778
|
+
supacloud_install_pinned_tar_xz_binary() (
|
|
25779
|
+
local archive="$1"
|
|
25780
|
+
local member="$2"
|
|
25781
|
+
local expected_sha256="$3"
|
|
25782
|
+
local arch="$4"
|
|
25783
|
+
local target="$5"
|
|
25784
|
+
local actual_sha256 member_count member_details extract_dir candidate staged_target
|
|
25785
|
+
|
|
25786
|
+
actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
|
|
25787
|
+
actual_sha256=$(printf '%s' "$actual_sha256" | tr '[:upper:]' '[:lower:]')
|
|
25788
|
+
expected_sha256=$(printf '%s' "$expected_sha256" | tr '[:upper:]' '[:lower:]')
|
|
25789
|
+
if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then
|
|
25790
|
+
echo "SHA256 mismatch for pinned archive" >&2
|
|
25791
|
+
return 1
|
|
25792
|
+
fi
|
|
25793
|
+
|
|
25794
|
+
member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)
|
|
25795
|
+
if [[ "$member_count" != "1" ]]; then
|
|
25796
|
+
echo "Pinned archive must contain the exact member once: $member" >&2
|
|
25797
|
+
return 1
|
|
25798
|
+
fi
|
|
25799
|
+
member_details=$(tar -tvJf "$archive" "$member") || return 1
|
|
25800
|
+
if [[ "\${member_details:0:1}" != "-" ]]; then
|
|
25801
|
+
echo "Pinned archive member is not a regular file: $member" >&2
|
|
25802
|
+
return 1
|
|
25803
|
+
fi
|
|
25804
|
+
|
|
25805
|
+
extract_dir=$(mktemp -d)
|
|
25806
|
+
trap 'rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"' EXIT HUP INT TERM
|
|
25807
|
+
if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then
|
|
25808
|
+
return 1
|
|
25809
|
+
fi
|
|
25810
|
+
candidate="\${extract_dir}/\${member}"
|
|
25811
|
+
supacloud_validate_binary "$candidate" "pinned-linux-\${arch}" || return 1
|
|
25812
|
+
|
|
25813
|
+
mkdir -p "$(dirname "$target")"
|
|
25814
|
+
staged_target=$(mktemp "\${target}.tmp.XXXXXX")
|
|
25815
|
+
install -m 0755 "$candidate" "$staged_target"
|
|
25816
|
+
mv -f "$staged_target" "$target"
|
|
25817
|
+
staged_target=""
|
|
25818
|
+
)
|
|
25819
|
+
|
|
25820
|
+
supacloud_version_at_least() {
|
|
25821
|
+
local current="\${1#v}"
|
|
25822
|
+
local required="\${2#v}"
|
|
25823
|
+
local current_major=0 current_minor=0 current_patch=0
|
|
25824
|
+
local required_major=0 required_minor=0 required_patch=0
|
|
25825
|
+
[[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
|
|
25826
|
+
[[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
|
|
25827
|
+
IFS=. read -r current_major current_minor current_patch <<EOF
|
|
25828
|
+
\${current%%-*}
|
|
25829
|
+
EOF
|
|
25830
|
+
IFS=. read -r required_major required_minor required_patch <<EOF
|
|
25831
|
+
\${required%%-*}
|
|
25832
|
+
EOF
|
|
25833
|
+
current_major=\${current_major:-0}; current_minor=\${current_minor:-0}; current_patch=\${current_patch:-0}
|
|
25834
|
+
required_major=\${required_major:-0}; required_minor=\${required_minor:-0}; required_patch=\${required_patch:-0}
|
|
25835
|
+
if (( current_major != required_major )); then (( current_major > required_major )); return; fi
|
|
25836
|
+
if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi
|
|
25837
|
+
(( current_patch >= required_patch ))
|
|
25838
|
+
}
|
|
25839
|
+
|
|
25840
|
+
supacloud_gh_version() {
|
|
25841
|
+
gh --version 2>/dev/null | awk 'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }'
|
|
25842
|
+
}
|
|
25843
|
+
|
|
25844
|
+
supacloud_install_gh_archive() {
|
|
25845
|
+
local archive="$1"
|
|
25846
|
+
local version="$2"
|
|
25847
|
+
local arch="$3"
|
|
25848
|
+
local expected_sha256="$4"
|
|
25849
|
+
local target="$5"
|
|
25850
|
+
local member="gh_\${version}_linux_\${arch}/bin/gh"
|
|
25851
|
+
local actual_sha256 member_count member_details extracted_dir candidate version_output
|
|
25852
|
+
|
|
25853
|
+
actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
|
|
25854
|
+
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
|
|
25855
|
+
echo "GitHub CLI archive SHA256 mismatch" >&2
|
|
25856
|
+
return 1
|
|
25857
|
+
fi
|
|
25858
|
+
|
|
25859
|
+
member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)
|
|
25860
|
+
if [[ "$member_count" != "1" ]]; then
|
|
25861
|
+
echo "GitHub CLI archive does not contain the exact expected member: $member" >&2
|
|
25862
|
+
return 1
|
|
25863
|
+
fi
|
|
25864
|
+
member_details=$(tar -tvzf "$archive" "$member") || return 1
|
|
25865
|
+
if [[ "\${member_details:0:1}" != "-" ]]; then
|
|
25866
|
+
echo "GitHub CLI archive member is not a regular file: $member" >&2
|
|
25867
|
+
return 1
|
|
25868
|
+
fi
|
|
25869
|
+
|
|
25870
|
+
extracted_dir=$(mktemp -d)
|
|
25871
|
+
candidate="\${extracted_dir}/\${member}"
|
|
25872
|
+
if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\
|
|
25873
|
+
|| ! supacloud_validate_binary "$candidate" "gh-linux-\${arch}"; then
|
|
25874
|
+
rm -rf "$extracted_dir"
|
|
25875
|
+
return 1
|
|
25876
|
+
fi
|
|
25877
|
+
chmod 0755 "$candidate"
|
|
25878
|
+
version_output=$("$candidate" --version 2>/dev/null | head -1) || {
|
|
25879
|
+
rm -rf "$extracted_dir"
|
|
25880
|
+
echo "GitHub CLI bootstrap binary failed its version check" >&2
|
|
25881
|
+
return 1
|
|
25882
|
+
}
|
|
25883
|
+
if [[ "$version_output" != "gh version \${version}"* ]]; then
|
|
25884
|
+
rm -rf "$extracted_dir"
|
|
25885
|
+
echo "GitHub CLI bootstrap version mismatch: \${version_output}" >&2
|
|
25886
|
+
return 1
|
|
25887
|
+
fi
|
|
25888
|
+
mkdir -p "$(dirname "$target")"
|
|
25889
|
+
install -m 0755 "$candidate" "$target"
|
|
25890
|
+
rm -rf "$extracted_dir"
|
|
25891
|
+
}
|
|
25892
|
+
|
|
25893
|
+
supacloud_install_pinned_gh() {
|
|
25894
|
+
local target="\${1:-/usr/local/bin/gh}"
|
|
25895
|
+
local machine arch expected_sha256 asset url archive
|
|
25896
|
+
machine=$(uname -m)
|
|
25897
|
+
case "$machine" in
|
|
25898
|
+
x86_64|amd64)
|
|
25899
|
+
arch="amd64"
|
|
25900
|
+
expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"
|
|
25901
|
+
;;
|
|
25902
|
+
aarch64|arm64)
|
|
25903
|
+
arch="arm64"
|
|
25904
|
+
expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"
|
|
25905
|
+
;;
|
|
25906
|
+
*)
|
|
25907
|
+
echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2
|
|
25908
|
+
return 1
|
|
25909
|
+
;;
|
|
25910
|
+
esac
|
|
25911
|
+
asset="gh_\${SUPACLOUD_GH_VERSION}_linux_\${arch}.tar.gz"
|
|
25912
|
+
url="https://github.com/cli/cli/releases/download/v\${SUPACLOUD_GH_VERSION}/\${asset}"
|
|
25913
|
+
archive=$(mktemp)
|
|
25914
|
+
if ! supacloud_download_url "$url" "$archive" \\
|
|
25915
|
+
|| ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then
|
|
25916
|
+
rm -f "$archive"
|
|
25917
|
+
return 1
|
|
25918
|
+
fi
|
|
25919
|
+
rm -f "$archive"
|
|
25920
|
+
}
|
|
25921
|
+
|
|
25922
|
+
supacloud_validate_tar() {
|
|
25923
|
+
local artifact_file="$1"
|
|
25924
|
+
local entries
|
|
25925
|
+
entries=$(tar -tzf "$artifact_file") || {
|
|
25926
|
+
echo "Web Console archive is not a readable gzip tarball" >&2
|
|
25927
|
+
return 1
|
|
25928
|
+
}
|
|
25929
|
+
if ! printf '%s\\n' "$entries" | awk '
|
|
25930
|
+
/^\\// { exit 1 }
|
|
25931
|
+
/(^|\\/)\\.\\.($|\\/)/ { exit 1 }
|
|
25932
|
+
'; then
|
|
25933
|
+
echo "Web Console archive contains an unsafe path" >&2
|
|
25934
|
+
return 1
|
|
25935
|
+
fi
|
|
25936
|
+
if ! tar -tvzf "$artifact_file" | awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }'; then
|
|
25937
|
+
echo "Web Console archive contains links or special files" >&2
|
|
25938
|
+
return 1
|
|
25939
|
+
fi
|
|
25940
|
+
printf '%s\\n' "$entries" | grep -Eq '(^|/)index\\.html$' || {
|
|
25941
|
+
echo "Web Console archive is invalid or does not contain index.html" >&2
|
|
25942
|
+
return 1
|
|
25943
|
+
}
|
|
25944
|
+
}
|
|
25945
|
+
|
|
25946
|
+
supacloud_record_integrity_mode() {
|
|
25947
|
+
local mode="$1"
|
|
25948
|
+
local record_file="\${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"
|
|
25949
|
+
mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0
|
|
25950
|
+
printf '%s\\n' "$mode" > "$record_file" 2>/dev/null || return 0
|
|
25951
|
+
chmod 600 "$record_file" 2>/dev/null || true
|
|
25952
|
+
}
|
|
25953
|
+
|
|
25954
|
+
supacloud_verify_attestation() {
|
|
25955
|
+
local artifact_file="$1"
|
|
25956
|
+
if supacloud_attestation_verifier_available; then
|
|
25957
|
+
local verification_output
|
|
25958
|
+
if ! verification_output=$(gh attestation verify "$artifact_file" \\
|
|
25959
|
+
--repo "$SUPACLOUD_GITHUB_REPOSITORY" \\
|
|
25960
|
+
--signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" 2>&1); then
|
|
25961
|
+
echo "GitHub artifact attestation verification failed: \${verification_output}" >&2
|
|
25962
|
+
return 1
|
|
25963
|
+
fi
|
|
25964
|
+
supacloud_record_integrity_mode "github-attestation+same-release-sha256"
|
|
25965
|
+
return
|
|
25966
|
+
fi
|
|
25967
|
+
|
|
25968
|
+
if [[ "\${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then
|
|
25969
|
+
echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2
|
|
25970
|
+
supacloud_record_integrity_mode "break-glass:same-release-sha256-only"
|
|
25971
|
+
return 0
|
|
25972
|
+
fi
|
|
25973
|
+
|
|
25974
|
+
echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2
|
|
25975
|
+
return 1
|
|
25976
|
+
}
|
|
25977
|
+
|
|
25978
|
+
supacloud_attestation_verifier_available() {
|
|
25979
|
+
local version
|
|
25980
|
+
command -v gh >/dev/null 2>&1 || return 1
|
|
25981
|
+
version=$(supacloud_gh_version)
|
|
25982
|
+
[[ -n "$version" ]] || return 1
|
|
25983
|
+
supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1
|
|
25984
|
+
gh attestation verify --help 2>&1 | grep -q -- '--signer-workflow'
|
|
25985
|
+
}
|
|
25986
|
+
|
|
25987
|
+
supacloud_download_release_asset() (
|
|
25988
|
+
local release_json="$1"
|
|
25989
|
+
local asset_name="$2"
|
|
25990
|
+
local destination="$3"
|
|
25991
|
+
local asset_kind="$4"
|
|
25992
|
+
local asset_url checksum_url temporary_artifact temporary_checksums
|
|
25993
|
+
|
|
25994
|
+
asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1
|
|
25995
|
+
checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1
|
|
25996
|
+
mkdir -p "$(dirname "$destination")"
|
|
25997
|
+
temporary_artifact=$(mktemp "\${destination}.tmp.XXXXXX")
|
|
25998
|
+
temporary_checksums=$(mktemp "\${destination}.SHA256SUMS.tmp.XXXXXX")
|
|
25999
|
+
trap 'rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"' EXIT HUP INT TERM
|
|
26000
|
+
|
|
26001
|
+
if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\
|
|
26002
|
+
|| ! supacloud_download_url "$checksum_url" "$temporary_checksums" \\
|
|
26003
|
+
|| ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then
|
|
26004
|
+
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
26005
|
+
return 1
|
|
26006
|
+
fi
|
|
26007
|
+
|
|
26008
|
+
# Authenticate the digest before parsing archives or inspecting binaries.
|
|
26009
|
+
if ! supacloud_verify_attestation "$temporary_artifact"; then
|
|
26010
|
+
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
26011
|
+
return 1
|
|
26012
|
+
fi
|
|
26013
|
+
|
|
26014
|
+
case "$asset_kind" in
|
|
26015
|
+
binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;
|
|
26016
|
+
tar) supacloud_validate_tar "$temporary_artifact" ;;
|
|
26017
|
+
*)
|
|
26018
|
+
echo "Unknown release asset kind: $asset_kind" >&2
|
|
26019
|
+
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
26020
|
+
return 1
|
|
26021
|
+
;;
|
|
26022
|
+
esac || {
|
|
26023
|
+
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
26024
|
+
return 1
|
|
26025
|
+
}
|
|
26026
|
+
|
|
26027
|
+
mv -f "$temporary_artifact" "$destination"
|
|
26028
|
+
temporary_artifact=""
|
|
26029
|
+
rm -f "$temporary_checksums"
|
|
26030
|
+
temporary_checksums=""
|
|
26031
|
+
)
|
|
26032
|
+
`;
|
|
26033
|
+
|
|
26034
|
+
// src/shared/tools/ssh-tools.ts
|
|
25596
26035
|
var SAFE_CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
|
|
25597
26036
|
var SAFE_PROJECT_REF = /^[a-z0-9-]{1,20}$/;
|
|
25598
26037
|
var SAFE_SCHEMA_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/;
|
|
@@ -25601,6 +26040,7 @@ var SAFE_TIMEOUT_SECONDS = 300;
|
|
|
25601
26040
|
var SAFE_HOSTNAME = /^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)(?:\.(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?))*$/;
|
|
25602
26041
|
var SAFE_SYSTEMD_UNIT = /^[a-zA-Z0-9][a-zA-Z0-9_.@:-]{0,127}$/;
|
|
25603
26042
|
var SAFE_DB_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$/;
|
|
26043
|
+
var MINIMUM_COMPONENT_UPGRADE_VERSION = "0.50.27";
|
|
25604
26044
|
function hostnameSchema(fieldName) {
|
|
25605
26045
|
return decodedSchema(Type.String(), Type.String({ minLength: 1, maxLength: 253 }), (value) => {
|
|
25606
26046
|
const normalized = value.trim();
|
|
@@ -25657,6 +26097,115 @@ function assertSafeReleaseTag(value) {
|
|
|
25657
26097
|
}
|
|
25658
26098
|
return value;
|
|
25659
26099
|
}
|
|
26100
|
+
function assertExactStableVersion(value, fieldName) {
|
|
26101
|
+
if (!/^v?\d+\.\d+\.\d+$/.test(value)) {
|
|
26102
|
+
throw new Error(`${fieldName} must be an exact stable semantic version`);
|
|
26103
|
+
}
|
|
26104
|
+
return value;
|
|
26105
|
+
}
|
|
26106
|
+
function upgradeEnvAssignments(request) {
|
|
26107
|
+
return [
|
|
26108
|
+
request.version ? `SUPACLOUD_UPGRADE_TAG=${quoteEnvValue(request.version)}` : "",
|
|
26109
|
+
request.edgeRuntimeVersion ? `SUPACLOUD_EDGE_RUNTIME_UPGRADE_TAG=${quoteEnvValue(request.edgeRuntimeVersion)}` : "",
|
|
26110
|
+
request.githubProxy ? `SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : ""
|
|
26111
|
+
].filter(Boolean).join(" ");
|
|
26112
|
+
}
|
|
26113
|
+
function componentBootstrapCommands(request) {
|
|
26114
|
+
if (!request.edgeRuntimeVersion)
|
|
26115
|
+
return [`UPGRADE_RUNNER=${quoteEnvValue("/usr/local/bin/supacloud")}`];
|
|
26116
|
+
const managementVersion = request.version || "latest";
|
|
26117
|
+
return [
|
|
26118
|
+
`ACTIVE_VERSION=$(/usr/local/bin/supacloud --version 2>&1 | grep -Eo '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1 || true)`,
|
|
26119
|
+
`UPGRADE_RUNNER=${quoteEnvValue("/usr/local/bin/supacloud")}`,
|
|
26120
|
+
`if ! supacloud_version_at_least "$ACTIVE_VERSION" ${quoteEnvValue(MINIMUM_COMPONENT_UPGRADE_VERSION)}; then`,
|
|
26121
|
+
` case "$(uname -m)" in x86_64|amd64) MANAGEMENT_ASSET=supacloud-linux-amd64 ;; aarch64|arm64) MANAGEMENT_ASSET=supacloud-linux-arm64 ;; *) echo 'Unsupported Management architecture' >&2; exit 1 ;; esac`,
|
|
26122
|
+
` STAGED_MANAGEMENT=$(mktemp /tmp/supacloud-management-upgrade.XXXXXX)`,
|
|
26123
|
+
` MANAGEMENT_RELEASE=$(supacloud_fetch_component_release management-api ${quoteEnvValue(managementVersion)} "$MANAGEMENT_ASSET" web-console-build.tar.gz)`,
|
|
26124
|
+
` supacloud_download_release_asset "$MANAGEMENT_RELEASE" "$MANAGEMENT_ASSET" "$STAGED_MANAGEMENT" binary`,
|
|
26125
|
+
` chmod 0755 "$STAGED_MANAGEMENT"`,
|
|
26126
|
+
` STAGED_VERSION=$("$STAGED_MANAGEMENT" --version 2>&1 | grep -Eo '[0-9]+\\.[0-9]+\\.[0-9]+' | head -1 || true)`,
|
|
26127
|
+
` supacloud_version_at_least "$STAGED_VERSION" ${quoteEnvValue(MINIMUM_COMPONENT_UPGRADE_VERSION)} || { echo 'Target Management release lacks Edge Runtime transaction capability' >&2; exit 1; }`,
|
|
26128
|
+
` UPGRADE_RUNNER="$STAGED_MANAGEMENT"`,
|
|
26129
|
+
"fi"
|
|
26130
|
+
];
|
|
26131
|
+
}
|
|
26132
|
+
function componentPreflightCommands(request) {
|
|
26133
|
+
return request.edgeRuntimeVersion ? [
|
|
26134
|
+
"test -f /etc/supabase/management-api.env || { echo 'EDGE_RUNTIME_MODE is unavailable; component upgrade requires external mode' >&2; exit 1; }",
|
|
26135
|
+
`EDGE_RUNTIME_MODE_VALUE=$(awk -F= '$1 == "EDGE_RUNTIME_MODE" { value=$2 } END { gsub(/^[[:space:]\\"'"']+|[[:space:]\\"'"']+$/, "", value); print value }' /etc/supabase/management-api.env)`,
|
|
26136
|
+
`test "$EDGE_RUNTIME_MODE_VALUE" = external || { echo 'Edge Runtime component upgrade supports persisted external mode only' >&2; exit 1; }`
|
|
26137
|
+
] : [];
|
|
26138
|
+
}
|
|
26139
|
+
function buildRootUpgradeScript(request) {
|
|
26140
|
+
const envAssignments = upgradeEnvAssignments(request);
|
|
26141
|
+
return [
|
|
26142
|
+
"set -euo pipefail",
|
|
26143
|
+
"umask 077",
|
|
26144
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
26145
|
+
"export PATH",
|
|
26146
|
+
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_GH_VERSION SUPACLOUD_GH_MIN_VERSION SUPACLOUD_GH_AMD64_SHA256 SUPACLOUD_GH_ARM64_SHA256 GH_PROXY",
|
|
26147
|
+
request.githubProxy ? `export SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : "unset SUPACLOUD_GITHUB_PROXY",
|
|
26148
|
+
'for tool in curl jq file sha256sum tar; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
|
|
26149
|
+
"test -x /usr/local/bin/supacloud || { echo 'SupaCloud binary not found at /usr/local/bin/supacloud; run ssh install first.' >&2; exit 127; }",
|
|
26150
|
+
...componentPreflightCommands(request),
|
|
26151
|
+
"STAGED_MANAGEMENT=''",
|
|
26152
|
+
`trap 'test -z "$STAGED_MANAGEMENT" || rm -f "$STAGED_MANAGEMENT"' EXIT HUP INT TERM`,
|
|
26153
|
+
`source ${quoteEnvValue(request.helperPath)}`,
|
|
26154
|
+
"if ! supacloud_attestation_verifier_available; then supacloud_install_pinned_gh /usr/local/bin/gh; fi",
|
|
26155
|
+
"supacloud_attestation_verifier_available || { echo 'Pinned GitHub attestation verifier is unavailable' >&2; exit 1; }",
|
|
26156
|
+
...componentBootstrapCommands(request),
|
|
26157
|
+
`${envAssignments ? `env ${envAssignments} ` : ""}"$UPGRADE_RUNNER" upgrade --yes`
|
|
26158
|
+
].join(`
|
|
26159
|
+
`);
|
|
26160
|
+
}
|
|
26161
|
+
function buildOfficialUpgradeCommand(request) {
|
|
26162
|
+
const rootScript = buildRootUpgradeScript(request);
|
|
26163
|
+
return `set -e; trap 'rm -f ${request.helperPath}' EXIT HUP INT TERM; ` + 'if [ "$(id -u)" -eq 0 ]; then ' + `bash -c ${quoteEnvValue(rootScript)}; ` + "else sudo -n true; " + `sudo -n bash -c ${quoteEnvValue(rootScript)}; fi`;
|
|
26164
|
+
}
|
|
26165
|
+
async function removeRemoteUpgradeHelper(ssh, helperPath) {
|
|
26166
|
+
const cleanup = await ssh.exec(`rm -f ${quoteEnvValue(helperPath)}`);
|
|
26167
|
+
if (!cleanup.success) {
|
|
26168
|
+
throw new Error(`Failed to remove remote upgrade helper (exit ${cleanup.code}): ${cleanup.stderr.slice(-300)}`);
|
|
26169
|
+
}
|
|
26170
|
+
}
|
|
26171
|
+
function remoteUpgradeFailure(execution) {
|
|
26172
|
+
const diagnostic = execution.stderr.trim() || execution.stdout.trim() || "no remote diagnostic";
|
|
26173
|
+
return new Error(`Remote upgrade failed (exit ${execution.code}): ${diagnostic.slice(-500)}`);
|
|
26174
|
+
}
|
|
26175
|
+
function officialUpgradeOutcome(execution, executionError, cleanupError) {
|
|
26176
|
+
if (executionError && cleanupError) {
|
|
26177
|
+
throw new AggregateError([executionError, cleanupError], "Upgrade execution failed and helper cleanup did not complete");
|
|
26178
|
+
}
|
|
26179
|
+
if (executionError)
|
|
26180
|
+
throw executionError;
|
|
26181
|
+
if (!execution)
|
|
26182
|
+
throw new Error("Upgrade execution did not return a result");
|
|
26183
|
+
if (cleanupError && !execution.success) {
|
|
26184
|
+
throw new AggregateError([remoteUpgradeFailure(execution), cleanupError], "Remote upgrade failed and helper cleanup did not complete");
|
|
26185
|
+
}
|
|
26186
|
+
if (cleanupError)
|
|
26187
|
+
throw cleanupError;
|
|
26188
|
+
if (!execution.success)
|
|
26189
|
+
throw remoteUpgradeFailure(execution);
|
|
26190
|
+
return execution;
|
|
26191
|
+
}
|
|
26192
|
+
async function executeOfficialUpgrade(ssh, helperPath, command) {
|
|
26193
|
+
let execution;
|
|
26194
|
+
let executionError;
|
|
26195
|
+
try {
|
|
26196
|
+
await ssh.uploadText(helperPath, release_assets_default, 384);
|
|
26197
|
+
execution = await ssh.exec(command, 600000);
|
|
26198
|
+
} catch (error) {
|
|
26199
|
+
executionError = error;
|
|
26200
|
+
}
|
|
26201
|
+
let cleanupError;
|
|
26202
|
+
try {
|
|
26203
|
+
await removeRemoteUpgradeHelper(ssh, helperPath);
|
|
26204
|
+
} catch (error) {
|
|
26205
|
+
cleanupError = error;
|
|
26206
|
+
}
|
|
26207
|
+
return officialUpgradeOutcome(execution, executionError, cleanupError);
|
|
26208
|
+
}
|
|
25660
26209
|
function assertSafeGithubProxy(value) {
|
|
25661
26210
|
const trimmed = value.trim();
|
|
25662
26211
|
if (/[\s\n\r;&|`$<>{}\[\]()*!?\\'\"]/.test(trimmed)) {
|
|
@@ -25767,7 +26316,7 @@ function assertSafeExecCommand(command) {
|
|
|
25767
26316
|
return trimmed;
|
|
25768
26317
|
if (trimmed === "cat /etc/os-release")
|
|
25769
26318
|
return trimmed;
|
|
25770
|
-
if (commandName === "hostname" && ["hostname", "hostname -f"].includes(trimmed))
|
|
26319
|
+
if (commandName === "hostname" && ["hostname", "hostname -f", "hostname -I"].includes(trimmed))
|
|
25771
26320
|
return trimmed;
|
|
25772
26321
|
if (commandName === "pg_isready") {
|
|
25773
26322
|
const seen = new Set;
|
|
@@ -25821,6 +26370,7 @@ Actions: ping, setup, install, upgrade, diagnose, exec, troubleshoot, container_
|
|
|
25821
26370
|
edge_runtime: optional(stringEnum(["bun"]), "[install] Runtime (default: bun)"),
|
|
25822
26371
|
storage_type: optional(stringEnum(["juicefs", "minio"]), "[install] Storage backend configurable through Admin"),
|
|
25823
26372
|
version: optional(Type.String(), "[upgrade] Specific version"),
|
|
26373
|
+
edge_runtime_version: optional(Type.String(), "[upgrade] Exact independent Edge Runtime version"),
|
|
25824
26374
|
github_proxy: optional(Type.String(), "[install/upgrade] Explicit GitHub proxy prefix, or direct/none"),
|
|
25825
26375
|
focus: optional(stringEnum(["all", "containers", "database", "network", "disk", "logs"]), "[troubleshoot] Focus area"),
|
|
25826
26376
|
container: optional(Type.String(), "[container_logs] Container name"),
|
|
@@ -25837,7 +26387,9 @@ Actions: ping, setup, install, upgrade, diagnose, exec, troubleshoot, container_
|
|
|
25837
26387
|
switch (action) {
|
|
25838
26388
|
case "ping": {
|
|
25839
26389
|
const ok = await ssh.ping();
|
|
25840
|
-
|
|
26390
|
+
if (!ok)
|
|
26391
|
+
throw new Error("SSH ping failed: server returned an unexpected response");
|
|
26392
|
+
text = "✅ Server reachable";
|
|
25841
26393
|
break;
|
|
25842
26394
|
}
|
|
25843
26395
|
case "setup": {
|
|
@@ -25939,16 +26491,29 @@ ${result.stderr.slice(-500)}`;
|
|
|
25939
26491
|
break;
|
|
25940
26492
|
}
|
|
25941
26493
|
case "upgrade": {
|
|
25942
|
-
const
|
|
25943
|
-
|
|
25944
|
-
|
|
25945
|
-
|
|
25946
|
-
|
|
25947
|
-
|
|
25948
|
-
|
|
25949
|
-
|
|
25950
|
-
|
|
25951
|
-
|
|
26494
|
+
const version = args.version ? assertSafeReleaseTag(args.version) : undefined;
|
|
26495
|
+
const edgeRuntimeVersion = args.edge_runtime_version ? assertSafeReleaseTag(args.edge_runtime_version) : undefined;
|
|
26496
|
+
if (edgeRuntimeVersion && !version) {
|
|
26497
|
+
throw new Error("'version' is required with 'edge_runtime_version'");
|
|
26498
|
+
}
|
|
26499
|
+
if (edgeRuntimeVersion && version) {
|
|
26500
|
+
assertExactStableVersion(version, "version");
|
|
26501
|
+
assertExactStableVersion(edgeRuntimeVersion, "edge_runtime_version");
|
|
26502
|
+
}
|
|
26503
|
+
const validatedProxy = args.github_proxy ? assertSafeGithubProxy(args.github_proxy) : undefined;
|
|
26504
|
+
const githubProxy = validatedProxy && !["direct", "none"].includes(validatedProxy.toLowerCase()) ? validatedProxy : undefined;
|
|
26505
|
+
const helperPath = `/tmp/.supacloud-release-assets-${randomUUID()}.sh`;
|
|
26506
|
+
const cmd = buildOfficialUpgradeCommand({
|
|
26507
|
+
version,
|
|
26508
|
+
edgeRuntimeVersion,
|
|
26509
|
+
githubProxy,
|
|
26510
|
+
helperPath
|
|
26511
|
+
});
|
|
26512
|
+
const upgradeExecution = await executeOfficialUpgrade(ssh, helperPath, cmd);
|
|
26513
|
+
const edgeBoundary = edgeRuntimeVersion ? "" : `
|
|
26514
|
+
⚠️ Edge Runtime was not upgraded; provide --edge_runtime_version for a component transaction.`;
|
|
26515
|
+
text = `✅ Upgrade done
|
|
26516
|
+
${upgradeExecution.stdout.slice(-300)}${edgeBoundary}`;
|
|
25952
26517
|
break;
|
|
25953
26518
|
}
|
|
25954
26519
|
case "diagnose": {
|
|
@@ -25969,6 +26534,10 @@ ${r.stderr.slice(-500)}`;
|
|
|
25969
26534
|
throw new Error("'command' required");
|
|
25970
26535
|
const command = assertSafeExecCommand(args.command);
|
|
25971
26536
|
const r = await ssh.exec(command, getExecTimeoutMs(args.timeout_seconds));
|
|
26537
|
+
if (!r.success) {
|
|
26538
|
+
const diagnostic = (r.stderr || r.stdout).trim() || "no remote diagnostic";
|
|
26539
|
+
throw new Error(`Remote diagnostic command failed (exit ${r.code}): ${diagnostic.slice(-500)}`);
|
|
26540
|
+
}
|
|
25972
26541
|
text = `exit: ${r.code}
|
|
25973
26542
|
|
|
25974
26543
|
stdout:
|
|
@@ -27060,7 +27629,13 @@ async function main() {
|
|
|
27060
27629
|
await runCli(cliTools, args, { commandName: "supacloud-admin" });
|
|
27061
27630
|
}
|
|
27062
27631
|
function isDirectRun() {
|
|
27063
|
-
|
|
27632
|
+
if (!process.argv[1])
|
|
27633
|
+
return false;
|
|
27634
|
+
try {
|
|
27635
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(resolve2(process.argv[1]));
|
|
27636
|
+
} catch {
|
|
27637
|
+
return false;
|
|
27638
|
+
}
|
|
27064
27639
|
}
|
|
27065
27640
|
if (isDirectRun()) {
|
|
27066
27641
|
main().catch((error) => {
|