@supacloud/admin 0.15.3 → 0.15.5
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/dist/index.js +263 -51
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25000,11 +25000,10 @@ class BoundedOutputCollector {
|
|
|
25000
25000
|
output = lastNewline >= 0 ? output.slice(0, lastNewline + 1) : "";
|
|
25001
25001
|
}
|
|
25002
25002
|
const redacted = redactSshOutput(output);
|
|
25003
|
-
|
|
25004
|
-
return redacted;
|
|
25005
|
-
return `${redacted}${redacted && !redacted.endsWith(`
|
|
25003
|
+
const bounded = !this.truncated ? redacted : `${redacted}${redacted && !redacted.endsWith(`
|
|
25006
25004
|
`) ? `
|
|
25007
25005
|
` : ""}[TRUNCATED: output exceeded ${this.limit}-byte limit]`;
|
|
25006
|
+
return { output: bounded, redacted: redacted !== output };
|
|
25008
25007
|
}
|
|
25009
25008
|
}
|
|
25010
25009
|
var BLOCKED_COMMANDS = [
|
|
@@ -25025,11 +25024,73 @@ var BLOCKED_COMMANDS = [
|
|
|
25025
25024
|
"chmod -R 777 /"
|
|
25026
25025
|
];
|
|
25027
25026
|
function redactSshCommand(command) {
|
|
25028
|
-
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/]
|
|
25027
|
+
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]@");
|
|
25028
|
+
}
|
|
25029
|
+
var SENSITIVE_STRUCTURED_FIELD = /(?:password|pass|secret|token|key|credential|db_uri|database_url|dsn)/i;
|
|
25030
|
+
function decodedStructuredFieldName(encodedName) {
|
|
25031
|
+
let decodedName = encodedName.replace(/\\'/g, "'");
|
|
25032
|
+
for (let decodingPass = 0;decodingPass < 3; decodingPass += 1) {
|
|
25033
|
+
if (decodedName.length > 256)
|
|
25034
|
+
return null;
|
|
25035
|
+
try {
|
|
25036
|
+
const nextName = JSON.parse(`"${decodedName.replace(/"/g, "\\\"")}"`);
|
|
25037
|
+
if (typeof nextName !== "string" || nextName === decodedName)
|
|
25038
|
+
return decodedName;
|
|
25039
|
+
decodedName = nextName;
|
|
25040
|
+
} catch {
|
|
25041
|
+
return null;
|
|
25042
|
+
}
|
|
25043
|
+
}
|
|
25044
|
+
return decodedName;
|
|
25045
|
+
}
|
|
25046
|
+
function containsSensitiveStructuredField(message) {
|
|
25047
|
+
const normalizedQuotes = message.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
25048
|
+
const fieldPatterns = [
|
|
25049
|
+
/"((?:\\.|[^"\\\r\n])*)"\s*:/g,
|
|
25050
|
+
/'((?:\\.|[^'\\\r\n])*)'\s*:/g,
|
|
25051
|
+
/(?:^|[{,]\s*)([A-Za-z_$\\][A-Za-z0-9_$\\{}]{0,255})\s*:/g
|
|
25052
|
+
];
|
|
25053
|
+
return fieldPatterns.some((pattern) => [...normalizedQuotes.matchAll(pattern)].some((match) => {
|
|
25054
|
+
const decodedName = decodedStructuredFieldName(match[1]);
|
|
25055
|
+
return decodedName === null || SENSITIVE_STRUCTURED_FIELD.test(decodedName);
|
|
25056
|
+
}));
|
|
25057
|
+
}
|
|
25058
|
+
function structuredSensitiveFieldsAreRedacted(message) {
|
|
25059
|
+
const normalizedQuotes = message.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
25060
|
+
try {
|
|
25061
|
+
const parsedDiagnostic = JSON.parse(normalizedQuotes);
|
|
25062
|
+
if (!parsedDiagnostic || typeof parsedDiagnostic !== "object")
|
|
25063
|
+
return false;
|
|
25064
|
+
let foundSensitiveField = false;
|
|
25065
|
+
const pending = [parsedDiagnostic];
|
|
25066
|
+
while (pending.length > 0) {
|
|
25067
|
+
const current = pending.pop();
|
|
25068
|
+
if (!current || typeof current !== "object")
|
|
25069
|
+
continue;
|
|
25070
|
+
for (const [fieldName, fieldValue] of Object.entries(current)) {
|
|
25071
|
+
if (SENSITIVE_STRUCTURED_FIELD.test(fieldName)) {
|
|
25072
|
+
foundSensitiveField = true;
|
|
25073
|
+
if (fieldValue !== "[REDACTED]")
|
|
25074
|
+
return false;
|
|
25075
|
+
} else if (fieldValue && typeof fieldValue === "object") {
|
|
25076
|
+
pending.push(fieldValue);
|
|
25077
|
+
}
|
|
25078
|
+
}
|
|
25079
|
+
}
|
|
25080
|
+
return foundSensitiveField;
|
|
25081
|
+
} catch {
|
|
25082
|
+
return false;
|
|
25083
|
+
}
|
|
25029
25084
|
}
|
|
25030
25085
|
function redactSshOutput(output) {
|
|
25031
|
-
const
|
|
25032
|
-
|
|
25086
|
+
const redactedEscapedFields = output.split(`
|
|
25087
|
+
`).map((line) => containsSensitiveStructuredField(line) && !structuredSensitiveFieldsAreRedacted(line) ? "[REDACTED: structured secret output]" : line).join(`
|
|
25088
|
+
`);
|
|
25089
|
+
const redactedLines = redactedEscapedFields.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]");
|
|
25090
|
+
const redactedStructuredFields = redactedLines.replace(/((?:["']?(?:password|pass|secret|token|key|credential|db_uri|database_url|dsn)["']?)\s*:\s*)("[^"]*"|'[^']*'|[^,}\]\r\n]+)/gi, (_match, prefix, literal) => {
|
|
25091
|
+
const quote = literal[0];
|
|
25092
|
+
return `${prefix}${quote === '"' || quote === "'" ? `${quote}[REDACTED]${quote}` : "[REDACTED]"}`;
|
|
25093
|
+
});
|
|
25033
25094
|
return redactSshCommand(redactedStructuredFields);
|
|
25034
25095
|
}
|
|
25035
25096
|
function isCommandBlocked(command) {
|
|
@@ -25172,13 +25233,17 @@ class SshTransport {
|
|
|
25172
25233
|
reject(new SshCommandOutcomeUnknownError("SSH command stream closed without a terminal status; remote outcome is unknown"));
|
|
25173
25234
|
return;
|
|
25174
25235
|
}
|
|
25236
|
+
const finalizedStdout = stdout.finalize();
|
|
25237
|
+
const finalizedStderr = stderr.finalize();
|
|
25175
25238
|
resolve({
|
|
25176
25239
|
success: code === 0,
|
|
25177
|
-
stdout:
|
|
25178
|
-
stderr:
|
|
25240
|
+
stdout: finalizedStdout.output,
|
|
25241
|
+
stderr: finalizedStderr.output,
|
|
25179
25242
|
code: code ?? 128,
|
|
25180
25243
|
stdoutTruncated: stdout.truncated,
|
|
25181
|
-
stderrTruncated: stderr.truncated
|
|
25244
|
+
stderrTruncated: stderr.truncated,
|
|
25245
|
+
stdoutRedacted: finalizedStdout.redacted,
|
|
25246
|
+
stderrRedacted: finalizedStderr.redacted
|
|
25182
25247
|
});
|
|
25183
25248
|
}).on("error", () => {
|
|
25184
25249
|
clearTimeout(timer);
|
|
@@ -26367,7 +26432,7 @@ function quoteShell(shellText) {
|
|
|
26367
26432
|
// src/shared/releases/local-upgrade-bundle.ts
|
|
26368
26433
|
import { spawn } from "node:child_process";
|
|
26369
26434
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
26370
|
-
import { accessSync, chmodSync as chmodSync2, constants as fsConstants, createWriteStream, lstatSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync,
|
|
26435
|
+
import { accessSync, chmodSync as chmodSync2, constants as fsConstants, createWriteStream, lstatSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
26371
26436
|
import { get } from "node:https";
|
|
26372
26437
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
26373
26438
|
import { basename, delimiter, join } from "node:path";
|
|
@@ -26649,6 +26714,7 @@ async function withSigstoreVerificationDirectory(operation) {
|
|
|
26649
26714
|
// src/shared/releases/local-upgrade-bundle.ts
|
|
26650
26715
|
var RELEASES_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/releases`;
|
|
26651
26716
|
var ATTESTATIONS_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/attestations`;
|
|
26717
|
+
var GITHUB_CLI_REPOSITORY = "cli/cli";
|
|
26652
26718
|
var GH_VERSION = "2.96.0";
|
|
26653
26719
|
var GH_ARCHIVE_SHA256 = {
|
|
26654
26720
|
amd64: "83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60",
|
|
@@ -26859,6 +26925,36 @@ async function downloadDirect(url, destination, maxBytes) {
|
|
|
26859
26925
|
}
|
|
26860
26926
|
throw new AggregateError(retryFailures, `Unable to download ${parsed.hostname}${parsed.pathname}`);
|
|
26861
26927
|
}
|
|
26928
|
+
async function downloadGithubReleaseAsset(request) {
|
|
26929
|
+
const download = await runGithubCliDownload([
|
|
26930
|
+
"release",
|
|
26931
|
+
"download",
|
|
26932
|
+
request.tag,
|
|
26933
|
+
"--repo",
|
|
26934
|
+
`github.com/${request.repository}`,
|
|
26935
|
+
"--pattern",
|
|
26936
|
+
request.assetName,
|
|
26937
|
+
"--output",
|
|
26938
|
+
"-"
|
|
26939
|
+
], request.destination, request.maxBytes, DOWNLOAD_TIMEOUT_MS);
|
|
26940
|
+
if (download.exitCode !== 0) {
|
|
26941
|
+
rmSync2(request.destination, { force: true });
|
|
26942
|
+
throw new Error(`GitHub release asset download failed: ${download.stderr.trim().slice(-1000) || download.exitCode}`);
|
|
26943
|
+
}
|
|
26944
|
+
assertDownloadedReleaseAsset(request);
|
|
26945
|
+
}
|
|
26946
|
+
function assertDownloadedReleaseAsset(request) {
|
|
26947
|
+
const stats = lstatSync(request.destination);
|
|
26948
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
26949
|
+
rmSync2(request.destination, { force: true });
|
|
26950
|
+
throw new Error("GitHub release asset must be a direct regular file");
|
|
26951
|
+
}
|
|
26952
|
+
if (stats.size > request.maxBytes) {
|
|
26953
|
+
rmSync2(request.destination, { force: true });
|
|
26954
|
+
throw new Error(`GitHub release asset exceeded ${request.maxBytes} bytes`);
|
|
26955
|
+
}
|
|
26956
|
+
chmodSync2(request.destination, 384);
|
|
26957
|
+
}
|
|
26862
26958
|
function parseJsonFile(filePath, label) {
|
|
26863
26959
|
const contents = readFileSync3(filePath, "utf8");
|
|
26864
26960
|
try {
|
|
@@ -26930,7 +27026,7 @@ function serializeAttestationBundles(candidate) {
|
|
|
26930
27026
|
function directEnvironment() {
|
|
26931
27027
|
const environment = { ...process.env };
|
|
26932
27028
|
for (const key of Object.keys(environment)) {
|
|
26933
|
-
if (/(?:^|_)proxy$/i.test(key) || /^(?:SUPACLOUD_GITHUB_PROXIES|NODE_USE_ENV_PROXY)$/.test(key)) {
|
|
27029
|
+
if (/(?:^|_)proxy$/i.test(key) || /^(?:GH_HOST|GH_REPO|SUPACLOUD_GITHUB_PROXIES|NODE_USE_ENV_PROXY)$/.test(key)) {
|
|
26934
27030
|
delete environment[key];
|
|
26935
27031
|
}
|
|
26936
27032
|
}
|
|
@@ -26955,15 +27051,15 @@ function githubCliExecutable(environment) {
|
|
|
26955
27051
|
}
|
|
26956
27052
|
throw new Error("GitHub CLI executable was not found in PATH");
|
|
26957
27053
|
}
|
|
26958
|
-
|
|
26959
|
-
|
|
26960
|
-
|
|
26961
|
-
|
|
26962
|
-
|
|
26963
|
-
|
|
26964
|
-
|
|
26965
|
-
|
|
26966
|
-
|
|
27054
|
+
function spawnGithubCli(arguments_) {
|
|
27055
|
+
const environment = directEnvironment();
|
|
27056
|
+
return spawn(githubCliExecutable(environment), arguments_, {
|
|
27057
|
+
env: environment,
|
|
27058
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
27059
|
+
});
|
|
27060
|
+
}
|
|
27061
|
+
function githubCliExitCode(child, timeoutMs) {
|
|
27062
|
+
return new Promise((resolve2, reject) => {
|
|
26967
27063
|
let timedOut = false;
|
|
26968
27064
|
let settled = false;
|
|
26969
27065
|
let forceKillTimer;
|
|
@@ -26982,18 +27078,46 @@ async function runGithubCli(arguments_, timeoutMs) {
|
|
|
26982
27078
|
if (error)
|
|
26983
27079
|
reject(error);
|
|
26984
27080
|
else
|
|
26985
|
-
resolve2(
|
|
27081
|
+
resolve2(timedOut ? 124 : exitCode);
|
|
26986
27082
|
};
|
|
26987
|
-
child.stdout.on("data", (chunk) => {
|
|
26988
|
-
stdout = `${stdout}${chunk.toString()}`.slice(-8000);
|
|
26989
|
-
});
|
|
26990
|
-
child.stderr.on("data", (chunk) => {
|
|
26991
|
-
stderr = `${stderr}${chunk.toString()}`.slice(-8000);
|
|
26992
|
-
});
|
|
26993
27083
|
child.once("error", (error) => settleExecution(error, 127));
|
|
26994
27084
|
child.once("close", (code) => settleExecution(undefined, code ?? 1));
|
|
26995
27085
|
});
|
|
26996
27086
|
}
|
|
27087
|
+
async function runGithubCli(arguments_, timeoutMs) {
|
|
27088
|
+
const child = spawnGithubCli(arguments_);
|
|
27089
|
+
let stdout = "";
|
|
27090
|
+
let stderr = "";
|
|
27091
|
+
child.stdout.on("data", (chunk) => {
|
|
27092
|
+
stdout = `${stdout}${chunk.toString()}`.slice(-8000);
|
|
27093
|
+
});
|
|
27094
|
+
child.stderr.on("data", (chunk) => {
|
|
27095
|
+
stderr = `${stderr}${chunk.toString()}`.slice(-8000);
|
|
27096
|
+
});
|
|
27097
|
+
const exitCode = await githubCliExitCode(child, timeoutMs);
|
|
27098
|
+
return { exitCode, stdout, stderr };
|
|
27099
|
+
}
|
|
27100
|
+
async function runGithubCliDownload(arguments_, destination, maxBytes, timeoutMs) {
|
|
27101
|
+
const child = spawnGithubCli(arguments_);
|
|
27102
|
+
let stderr = "";
|
|
27103
|
+
child.stderr.on("data", (chunk) => {
|
|
27104
|
+
stderr = `${stderr}${chunk.toString()}`.slice(-8000);
|
|
27105
|
+
});
|
|
27106
|
+
const write = pipeline(child.stdout, boundedWriter(maxBytes), createWriteStream(destination, { flags: "wx", mode: 384 })).catch((error) => {
|
|
27107
|
+
child.kill("SIGKILL");
|
|
27108
|
+
throw error;
|
|
27109
|
+
});
|
|
27110
|
+
const [writeState, exitState] = await Promise.allSettled([write, githubCliExitCode(child, timeoutMs)]);
|
|
27111
|
+
if (writeState.status === "rejected") {
|
|
27112
|
+
rmSync2(destination, { force: true });
|
|
27113
|
+
throw writeState.reason;
|
|
27114
|
+
}
|
|
27115
|
+
if (exitState.status === "rejected") {
|
|
27116
|
+
rmSync2(destination, { force: true });
|
|
27117
|
+
throw exitState.reason;
|
|
27118
|
+
}
|
|
27119
|
+
return { exitCode: exitState.value, stdout: "", stderr };
|
|
27120
|
+
}
|
|
26997
27121
|
function supportsStrictGithubVerification(execution) {
|
|
26998
27122
|
const tokens = `${execution.stdout}
|
|
26999
27123
|
${execution.stderr}`.split(/\s+/);
|
|
@@ -27071,16 +27195,22 @@ function manifestAttestationDownloadUrl(release, manifest, manifestDigest) {
|
|
|
27071
27195
|
}
|
|
27072
27196
|
async function downloadManifestAttestation(request) {
|
|
27073
27197
|
const { release, manifest, manifestPath, destination } = request;
|
|
27198
|
+
if (manifest.repository !== RELEASE_REPOSITORY) {
|
|
27199
|
+
await downloadGithubReleaseAsset({
|
|
27200
|
+
repository: RELEASE_REPOSITORY,
|
|
27201
|
+
tag: release.tag_name,
|
|
27202
|
+
assetName: RELEASE_ATTESTATION_NAME,
|
|
27203
|
+
destination,
|
|
27204
|
+
maxBytes: RELEASE_BUNDLE_SIZE_LIMITS.attestation
|
|
27205
|
+
});
|
|
27206
|
+
return;
|
|
27207
|
+
}
|
|
27074
27208
|
const responsePath = `${destination}.response`;
|
|
27075
27209
|
try {
|
|
27076
27210
|
await downloadDirect(manifestAttestationDownloadUrl(release, manifest, sha256File(manifestPath)), responsePath, RELEASE_BUNDLE_SIZE_LIMITS.attestation);
|
|
27077
|
-
|
|
27078
|
-
|
|
27079
|
-
|
|
27080
|
-
chmodSync2(destination, 384);
|
|
27081
|
-
} else {
|
|
27082
|
-
renameSync(responsePath, destination);
|
|
27083
|
-
}
|
|
27211
|
+
const bundles = serializeAttestationBundles(parseJsonFile(responsePath, "GitHub attestation response"));
|
|
27212
|
+
writeFileSync2(destination, bundles, { mode: 384, flag: "wx" });
|
|
27213
|
+
chmodSync2(destination, 384);
|
|
27084
27214
|
} finally {
|
|
27085
27215
|
rmSync2(responsePath, { force: true });
|
|
27086
27216
|
}
|
|
@@ -27089,7 +27219,14 @@ async function downloadComponent(request) {
|
|
|
27089
27219
|
const release = await downloadReleaseMetadata(request.component, request.version, request.destination);
|
|
27090
27220
|
const manifestPath = directChildPath(request.destination, RELEASE_MANIFEST_NAME);
|
|
27091
27221
|
const attestationPath = directChildPath(request.destination, RELEASE_ATTESTATION_NAME);
|
|
27092
|
-
|
|
27222
|
+
releaseAssetUrl(release, RELEASE_MANIFEST_NAME);
|
|
27223
|
+
await downloadGithubReleaseAsset({
|
|
27224
|
+
repository: RELEASE_REPOSITORY,
|
|
27225
|
+
tag: release.tag_name,
|
|
27226
|
+
assetName: RELEASE_MANIFEST_NAME,
|
|
27227
|
+
destination: manifestPath,
|
|
27228
|
+
maxBytes: RELEASE_BUNDLE_SIZE_LIMITS.manifest
|
|
27229
|
+
});
|
|
27093
27230
|
const manifest = parseReleaseManifest(readFileSync3(manifestPath, "utf8"), request);
|
|
27094
27231
|
await downloadManifestAttestation({ release, manifest, manifestPath, destination: attestationPath });
|
|
27095
27232
|
await verifyManifestAttestation({
|
|
@@ -27099,12 +27236,26 @@ async function downloadComponent(request) {
|
|
|
27099
27236
|
trustedRootPath: request.trustedRootPath
|
|
27100
27237
|
});
|
|
27101
27238
|
const checksumsPath = directChildPath(request.destination, RELEASE_CHECKSUMS_NAME);
|
|
27102
|
-
|
|
27239
|
+
releaseAssetUrl(release, RELEASE_CHECKSUMS_NAME);
|
|
27240
|
+
await downloadGithubReleaseAsset({
|
|
27241
|
+
repository: RELEASE_REPOSITORY,
|
|
27242
|
+
tag: release.tag_name,
|
|
27243
|
+
assetName: RELEASE_CHECKSUMS_NAME,
|
|
27244
|
+
destination: checksumsPath,
|
|
27245
|
+
maxBytes: RELEASE_BUNDLE_SIZE_LIMITS.checksums
|
|
27246
|
+
});
|
|
27103
27247
|
assertSignedArtifact(checksumsPath, manifest);
|
|
27104
27248
|
const checksums = parseReleaseChecksums(readFileSync3(checksumsPath, "utf8"), manifest);
|
|
27105
27249
|
for (const assetName of request.assetNames) {
|
|
27106
27250
|
const assetPath = directChildPath(request.destination, assetName);
|
|
27107
|
-
|
|
27251
|
+
releaseAssetUrl(release, assetName);
|
|
27252
|
+
await downloadGithubReleaseAsset({
|
|
27253
|
+
repository: RELEASE_REPOSITORY,
|
|
27254
|
+
tag: release.tag_name,
|
|
27255
|
+
assetName,
|
|
27256
|
+
destination: assetPath,
|
|
27257
|
+
maxBytes: releaseAssetSizeLimit(request.component, assetName)
|
|
27258
|
+
});
|
|
27108
27259
|
verifyDownloadedFile(assetPath, manifest, checksums);
|
|
27109
27260
|
}
|
|
27110
27261
|
return [RELEASE_MANIFEST_NAME, RELEASE_ATTESTATION_NAME, RELEASE_CHECKSUMS_NAME, ...request.assetNames].map((name) => localUpgradeFile(directChildPath(request.destination, name), `bundle/${request.component}/${name}`));
|
|
@@ -27112,8 +27263,13 @@ async function downloadComponent(request) {
|
|
|
27112
27263
|
async function downloadPinnedGithubCli(directory, architecture) {
|
|
27113
27264
|
const identity = githubCliArchiveIdentity(architecture);
|
|
27114
27265
|
const archivePath = directChildPath(directory, identity.archiveName);
|
|
27115
|
-
|
|
27116
|
-
|
|
27266
|
+
await downloadGithubReleaseAsset({
|
|
27267
|
+
repository: GITHUB_CLI_REPOSITORY,
|
|
27268
|
+
tag: `v${identity.version}`,
|
|
27269
|
+
assetName: identity.archiveName,
|
|
27270
|
+
destination: archivePath,
|
|
27271
|
+
maxBytes: MAX_GH_ARCHIVE_BYTES
|
|
27272
|
+
});
|
|
27117
27273
|
if (sha256File(archivePath) !== identity.sha256) {
|
|
27118
27274
|
throw new Error("Pinned GitHub CLI archive SHA256 mismatch");
|
|
27119
27275
|
}
|
|
@@ -27226,8 +27382,10 @@ var REMOTE_LOG_ROOT = "/var/log/supacloud";
|
|
|
27226
27382
|
var REMOTE_UPLOAD_ROOT = "/var/tmp";
|
|
27227
27383
|
var REMOTE_COMMAND_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
27228
27384
|
var CONTROL_PLANE_BACKUP_ROOT = "/var/lib/supacloud/backups/control-plane-upgrades";
|
|
27385
|
+
var CONTROL_PLANE_PREFLIGHT_PREFIX = "SUPACLOUD_CONTROL_PLANE_UPGRADE_PREFLIGHT=";
|
|
27229
27386
|
var CONTROL_PLANE_SAFETY_PREFIX = "SUPACLOUD_CONTROL_PLANE_UPGRADE_SAFETY=";
|
|
27230
|
-
var
|
|
27387
|
+
var UPGRADE_FAILURE_PREFIX = "SUPACLOUD_UPGRADE_FAILURE=";
|
|
27388
|
+
var MINIMUM_CONTROL_PLANE_SAFETY_VERSION = [0, 61, 7];
|
|
27231
27389
|
var POLL_INTERVAL_MS = 2000;
|
|
27232
27390
|
var STATE_READ_ATTEMPTS = 3;
|
|
27233
27391
|
var REMOTE_STATE_READ_TIMEOUT_MS = 15000;
|
|
@@ -27251,13 +27409,13 @@ function canonicalTimestamp(candidate) {
|
|
|
27251
27409
|
const timestamp = new Date(candidate);
|
|
27252
27410
|
return Number.isFinite(timestamp.valueOf()) && timestamp.toISOString() === candidate;
|
|
27253
27411
|
}
|
|
27254
|
-
function
|
|
27255
|
-
const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(
|
|
27412
|
+
function parseControlPlaneSafetyReceipt(log, prefix) {
|
|
27413
|
+
const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(prefix));
|
|
27256
27414
|
if (receiptLines.length !== 1)
|
|
27257
27415
|
throw new Error("Remote upgrade did not emit one control-plane safety receipt");
|
|
27258
27416
|
let candidate;
|
|
27259
27417
|
try {
|
|
27260
|
-
candidate = JSON.parse(receiptLines[0].slice(
|
|
27418
|
+
candidate = JSON.parse(receiptLines[0].slice(prefix.length));
|
|
27261
27419
|
} catch {
|
|
27262
27420
|
throw new Error("Remote control-plane safety receipt is not valid JSON");
|
|
27263
27421
|
}
|
|
@@ -27289,6 +27447,46 @@ function parseControlPlaneSafetyEvidence(log) {
|
|
|
27289
27447
|
}
|
|
27290
27448
|
return receipt;
|
|
27291
27449
|
}
|
|
27450
|
+
function parseControlPlaneSafetyEvidence(log) {
|
|
27451
|
+
return parseControlPlaneSafetyReceipt(log, CONTROL_PLANE_SAFETY_PREFIX);
|
|
27452
|
+
}
|
|
27453
|
+
function parseControlPlanePreflightEvidence(log) {
|
|
27454
|
+
return parseControlPlaneSafetyReceipt(log, CONTROL_PLANE_PREFLIGHT_PREFIX);
|
|
27455
|
+
}
|
|
27456
|
+
function parsedUpgradeFailureReceipt(log) {
|
|
27457
|
+
const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(UPGRADE_FAILURE_PREFIX));
|
|
27458
|
+
if (receiptLines.length !== 1)
|
|
27459
|
+
throw new Error("Remote upgrade did not emit one structured failure receipt");
|
|
27460
|
+
let candidate;
|
|
27461
|
+
try {
|
|
27462
|
+
candidate = JSON.parse(receiptLines[0].slice(UPGRADE_FAILURE_PREFIX.length));
|
|
27463
|
+
} catch {
|
|
27464
|
+
throw new Error("Remote upgrade failure receipt is not valid JSON");
|
|
27465
|
+
}
|
|
27466
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
27467
|
+
throw new Error("Remote upgrade failure receipt is invalid");
|
|
27468
|
+
}
|
|
27469
|
+
return candidate;
|
|
27470
|
+
}
|
|
27471
|
+
function upgradeFailureReceiptIsInvalid(receipt) {
|
|
27472
|
+
const causes = receipt.causes;
|
|
27473
|
+
return !exactObjectKeys(receipt, ["causes", "schema", "summary"]) || receipt.schema !== "supacloud.upgrade-failure.v1" || typeof receipt.summary !== "string" || !receipt.summary || receipt.summary.length > 500 || /[\u0000-\u001f\u007f]/.test(receipt.summary) || !Array.isArray(causes) || causes.length > 8 || causes.some((cause) => typeof cause !== "string" || !cause || cause.length > 500 || /[\u0000-\u001f\u007f]/.test(cause));
|
|
27474
|
+
}
|
|
27475
|
+
function upgradeFailureMessageContainsSensitiveData(message) {
|
|
27476
|
+
const bareBearer = /\bBearer\s+(?!\[REDACTED\](?:\s|$|[,;.)]))\S+/i;
|
|
27477
|
+
return redactSshOutput(message) !== message || bareBearer.test(message);
|
|
27478
|
+
}
|
|
27479
|
+
function parseUpgradeFailureEvidence(log) {
|
|
27480
|
+
const receipt = parsedUpgradeFailureReceipt(log);
|
|
27481
|
+
if (upgradeFailureReceiptIsInvalid(receipt)) {
|
|
27482
|
+
throw new Error("Remote upgrade failure receipt is invalid");
|
|
27483
|
+
}
|
|
27484
|
+
const messages = [receipt.summary, ...receipt.causes];
|
|
27485
|
+
if (messages.some(upgradeFailureMessageContainsSensitiveData)) {
|
|
27486
|
+
throw new Error("Remote upgrade failure receipt contains sensitive data");
|
|
27487
|
+
}
|
|
27488
|
+
return receipt;
|
|
27489
|
+
}
|
|
27292
27490
|
function assertControlPlaneSafetyVersion(version) {
|
|
27293
27491
|
const match = version.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/);
|
|
27294
27492
|
if (!match)
|
|
@@ -27301,7 +27499,7 @@ function assertControlPlaneSafetyVersion(version) {
|
|
|
27301
27499
|
if (requested[index] > MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index])
|
|
27302
27500
|
return;
|
|
27303
27501
|
if (requested[index] < MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index]) {
|
|
27304
|
-
throw new Error("Local artifact upgrades require Management 0.61.
|
|
27502
|
+
throw new Error("Local artifact upgrades require Management 0.61.7 or newer with control-plane backup preflight safety");
|
|
27305
27503
|
}
|
|
27306
27504
|
}
|
|
27307
27505
|
}
|
|
@@ -27622,6 +27820,7 @@ function bundledVerifierSetup(paths, architecture) {
|
|
|
27622
27820
|
function upgradeScriptExecution(paths, bundle, request) {
|
|
27623
27821
|
const runnerAsset = `${paths.stage}/bundle/management-api/${bundle.managementBinaryName}`;
|
|
27624
27822
|
const runner = `${paths.stage}/runner`;
|
|
27823
|
+
const preflight = 'CONTROL_PLANE_PREFLIGHT_RECEIPT=$(env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" --control-plane-upgrade-preflight)';
|
|
27625
27824
|
return [
|
|
27626
27825
|
`MANAGEMENT_VERSION=${quoteShell2(request.managementVersion)}`,
|
|
27627
27826
|
`RUNNER_ASSET=${quoteShell2(runnerAsset)}`,
|
|
@@ -27632,7 +27831,9 @@ function upgradeScriptExecution(paths, bundle, request) {
|
|
|
27632
27831
|
'"$RUNNER" --version | grep -Eq "(^|[^0-9])${MANAGEMENT_VERSION//./\\.}([^0-9]|$)"',
|
|
27633
27832
|
`timeout 5s "$RUNNER" --systemd-unit-helper-sha256 | grep -Eq 'SupaCloud systemd-unit helper SHA-256: [0-9a-f]{64}'`,
|
|
27634
27833
|
`timeout 5s "$RUNNER" --postgrest-launcher-sha256 | grep -Eq 'SupaCloud PostgREST launcher SHA-256: [0-9a-f]{64}'`,
|
|
27635
|
-
|
|
27834
|
+
preflight,
|
|
27835
|
+
`env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" upgrade --yes --target-version ${quoteShell2(request.managementVersion)} --edge-runtime-version ${quoteShell2(request.edgeRuntimeVersion)} --asset-bundle-dir "$BUNDLE"`,
|
|
27836
|
+
`printf '%s\\n' "$CONTROL_PLANE_PREFLIGHT_RECEIPT"`
|
|
27636
27837
|
];
|
|
27637
27838
|
}
|
|
27638
27839
|
function buildLocalUpgradeRunScript(paths, bundle, request, architecture) {
|
|
@@ -27848,6 +28049,9 @@ async function remoteLogTail(ssh, paths) {
|
|
|
27848
28049
|
const output = await ssh.exec(rootCommand(script), 15000);
|
|
27849
28050
|
if (!output.success)
|
|
27850
28051
|
throw remoteFailure("Unable to read the remote upgrade log", output);
|
|
28052
|
+
if (output.stdoutRedacted || output.stderrRedacted || output.stdoutTruncated || output.stderrTruncated) {
|
|
28053
|
+
throw new Error("Remote upgrade log required transport redaction or truncation");
|
|
28054
|
+
}
|
|
27851
28055
|
return output.stdout.slice(-4000);
|
|
27852
28056
|
}
|
|
27853
28057
|
async function cleanupRemoteRecords(ssh, paths) {
|
|
@@ -27945,11 +28149,13 @@ async function completedUpgradeOutput(ssh, paths) {
|
|
|
27945
28149
|
} catch (error) {
|
|
27946
28150
|
throw remoteReconciliationFailure("Upgrade succeeded but its retained log could not be read", [error], paths);
|
|
27947
28151
|
}
|
|
28152
|
+
let preflightEvidence;
|
|
27948
28153
|
let safetyEvidence;
|
|
27949
28154
|
try {
|
|
28155
|
+
preflightEvidence = parseControlPlanePreflightEvidence(log);
|
|
27950
28156
|
safetyEvidence = parseControlPlaneSafetyEvidence(log);
|
|
27951
28157
|
} catch (error) {
|
|
27952
|
-
throw remoteReconciliationFailure("Upgrade succeeded but control-plane safety evidence is invalid", [error], paths);
|
|
28158
|
+
throw remoteReconciliationFailure("Upgrade succeeded but control-plane preflight or safety evidence is invalid", [error], paths);
|
|
27953
28159
|
}
|
|
27954
28160
|
try {
|
|
27955
28161
|
await cleanupRemoteRecords(ssh, paths);
|
|
@@ -27957,7 +28163,7 @@ async function completedUpgradeOutput(ssh, paths) {
|
|
|
27957
28163
|
throw remoteReconciliationFailure("Upgrade succeeded but remote evidence cleanup could not be confirmed", [error], paths);
|
|
27958
28164
|
}
|
|
27959
28165
|
return `✅ Upgrade done
|
|
27960
|
-
${JSON.stringify(safetyEvidence)}
|
|
28166
|
+
${JSON.stringify({ preflight: preflightEvidence, transaction: safetyEvidence })}
|
|
27961
28167
|
${log.slice(-1500)}`;
|
|
27962
28168
|
}
|
|
27963
28169
|
async function throwRemoteUpgradeFailure(ssh, paths, status) {
|
|
@@ -27967,13 +28173,19 @@ async function throwRemoteUpgradeFailure(ssh, paths, status) {
|
|
|
27967
28173
|
} catch (error) {
|
|
27968
28174
|
throw remoteReconciliationFailure("Remote upgrade failed but its retained log could not be read", [error], paths);
|
|
27969
28175
|
}
|
|
27970
|
-
const failure = new Error(`Remote local upgrade failed (${status}): ${log.slice(-1500)}`);
|
|
27971
28176
|
if (status.endsWith(":CLEANUP_AFTER_TRANSACTION")) {
|
|
27972
|
-
throw remoteReconciliationFailure("Upgrade transaction completed but staging cleanup is incomplete", [
|
|
28177
|
+
throw remoteReconciliationFailure("Upgrade transaction completed but staging cleanup is incomplete", [new Error(`Remote local upgrade ended with ${status}`)], paths);
|
|
27973
28178
|
}
|
|
27974
28179
|
if (status.includes("CLEANUP")) {
|
|
27975
|
-
throw remoteReconciliationFailure("Upgrade transaction and staging cleanup both failed", [
|
|
28180
|
+
throw remoteReconciliationFailure("Upgrade transaction and staging cleanup both failed", [new Error(`Remote local upgrade ended with ${status}`)], paths);
|
|
28181
|
+
}
|
|
28182
|
+
let failureEvidence;
|
|
28183
|
+
try {
|
|
28184
|
+
failureEvidence = parseUpgradeFailureEvidence(log);
|
|
28185
|
+
} catch (error) {
|
|
28186
|
+
throw remoteReconciliationFailure("Remote upgrade failed without valid structured failure evidence", [error], paths);
|
|
27976
28187
|
}
|
|
28188
|
+
const failure = new Error(`Remote local upgrade failed (${status}): ${failureEvidence.summary}` + (failureEvidence.causes.length > 0 ? `; causes: ${failureEvidence.causes.join(" | ")}` : ""));
|
|
27977
28189
|
try {
|
|
27978
28190
|
await cleanupRemoteRecords(ssh, paths);
|
|
27979
28191
|
} catch (cleanupError) {
|
|
@@ -32154,7 +32366,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
|
|
|
32154
32366
|
// package.json
|
|
32155
32367
|
var package_default = {
|
|
32156
32368
|
name: "@supacloud/admin",
|
|
32157
|
-
version: "0.15.
|
|
32369
|
+
version: "0.15.5",
|
|
32158
32370
|
description: "Platform administration CLI for SupaCloud operators",
|
|
32159
32371
|
type: "module",
|
|
32160
32372
|
main: "./dist/index.js",
|