@supacloud/admin 0.15.4 → 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 +142 -21
- 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);
|
|
@@ -27317,8 +27382,10 @@ var REMOTE_LOG_ROOT = "/var/log/supacloud";
|
|
|
27317
27382
|
var REMOTE_UPLOAD_ROOT = "/var/tmp";
|
|
27318
27383
|
var REMOTE_COMMAND_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
27319
27384
|
var CONTROL_PLANE_BACKUP_ROOT = "/var/lib/supacloud/backups/control-plane-upgrades";
|
|
27385
|
+
var CONTROL_PLANE_PREFLIGHT_PREFIX = "SUPACLOUD_CONTROL_PLANE_UPGRADE_PREFLIGHT=";
|
|
27320
27386
|
var CONTROL_PLANE_SAFETY_PREFIX = "SUPACLOUD_CONTROL_PLANE_UPGRADE_SAFETY=";
|
|
27321
|
-
var
|
|
27387
|
+
var UPGRADE_FAILURE_PREFIX = "SUPACLOUD_UPGRADE_FAILURE=";
|
|
27388
|
+
var MINIMUM_CONTROL_PLANE_SAFETY_VERSION = [0, 61, 7];
|
|
27322
27389
|
var POLL_INTERVAL_MS = 2000;
|
|
27323
27390
|
var STATE_READ_ATTEMPTS = 3;
|
|
27324
27391
|
var REMOTE_STATE_READ_TIMEOUT_MS = 15000;
|
|
@@ -27342,13 +27409,13 @@ function canonicalTimestamp(candidate) {
|
|
|
27342
27409
|
const timestamp = new Date(candidate);
|
|
27343
27410
|
return Number.isFinite(timestamp.valueOf()) && timestamp.toISOString() === candidate;
|
|
27344
27411
|
}
|
|
27345
|
-
function
|
|
27346
|
-
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));
|
|
27347
27414
|
if (receiptLines.length !== 1)
|
|
27348
27415
|
throw new Error("Remote upgrade did not emit one control-plane safety receipt");
|
|
27349
27416
|
let candidate;
|
|
27350
27417
|
try {
|
|
27351
|
-
candidate = JSON.parse(receiptLines[0].slice(
|
|
27418
|
+
candidate = JSON.parse(receiptLines[0].slice(prefix.length));
|
|
27352
27419
|
} catch {
|
|
27353
27420
|
throw new Error("Remote control-plane safety receipt is not valid JSON");
|
|
27354
27421
|
}
|
|
@@ -27380,6 +27447,46 @@ function parseControlPlaneSafetyEvidence(log) {
|
|
|
27380
27447
|
}
|
|
27381
27448
|
return receipt;
|
|
27382
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
|
+
}
|
|
27383
27490
|
function assertControlPlaneSafetyVersion(version) {
|
|
27384
27491
|
const match = version.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/);
|
|
27385
27492
|
if (!match)
|
|
@@ -27392,7 +27499,7 @@ function assertControlPlaneSafetyVersion(version) {
|
|
|
27392
27499
|
if (requested[index] > MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index])
|
|
27393
27500
|
return;
|
|
27394
27501
|
if (requested[index] < MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index]) {
|
|
27395
|
-
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");
|
|
27396
27503
|
}
|
|
27397
27504
|
}
|
|
27398
27505
|
}
|
|
@@ -27713,6 +27820,7 @@ function bundledVerifierSetup(paths, architecture) {
|
|
|
27713
27820
|
function upgradeScriptExecution(paths, bundle, request) {
|
|
27714
27821
|
const runnerAsset = `${paths.stage}/bundle/management-api/${bundle.managementBinaryName}`;
|
|
27715
27822
|
const runner = `${paths.stage}/runner`;
|
|
27823
|
+
const preflight = 'CONTROL_PLANE_PREFLIGHT_RECEIPT=$(env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" --control-plane-upgrade-preflight)';
|
|
27716
27824
|
return [
|
|
27717
27825
|
`MANAGEMENT_VERSION=${quoteShell2(request.managementVersion)}`,
|
|
27718
27826
|
`RUNNER_ASSET=${quoteShell2(runnerAsset)}`,
|
|
@@ -27723,7 +27831,9 @@ function upgradeScriptExecution(paths, bundle, request) {
|
|
|
27723
27831
|
'"$RUNNER" --version | grep -Eq "(^|[^0-9])${MANAGEMENT_VERSION//./\\.}([^0-9]|$)"',
|
|
27724
27832
|
`timeout 5s "$RUNNER" --systemd-unit-helper-sha256 | grep -Eq 'SupaCloud systemd-unit helper SHA-256: [0-9a-f]{64}'`,
|
|
27725
27833
|
`timeout 5s "$RUNNER" --postgrest-launcher-sha256 | grep -Eq 'SupaCloud PostgREST launcher SHA-256: [0-9a-f]{64}'`,
|
|
27726
|
-
|
|
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"`
|
|
27727
27837
|
];
|
|
27728
27838
|
}
|
|
27729
27839
|
function buildLocalUpgradeRunScript(paths, bundle, request, architecture) {
|
|
@@ -27939,6 +28049,9 @@ async function remoteLogTail(ssh, paths) {
|
|
|
27939
28049
|
const output = await ssh.exec(rootCommand(script), 15000);
|
|
27940
28050
|
if (!output.success)
|
|
27941
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
|
+
}
|
|
27942
28055
|
return output.stdout.slice(-4000);
|
|
27943
28056
|
}
|
|
27944
28057
|
async function cleanupRemoteRecords(ssh, paths) {
|
|
@@ -28036,11 +28149,13 @@ async function completedUpgradeOutput(ssh, paths) {
|
|
|
28036
28149
|
} catch (error) {
|
|
28037
28150
|
throw remoteReconciliationFailure("Upgrade succeeded but its retained log could not be read", [error], paths);
|
|
28038
28151
|
}
|
|
28152
|
+
let preflightEvidence;
|
|
28039
28153
|
let safetyEvidence;
|
|
28040
28154
|
try {
|
|
28155
|
+
preflightEvidence = parseControlPlanePreflightEvidence(log);
|
|
28041
28156
|
safetyEvidence = parseControlPlaneSafetyEvidence(log);
|
|
28042
28157
|
} catch (error) {
|
|
28043
|
-
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);
|
|
28044
28159
|
}
|
|
28045
28160
|
try {
|
|
28046
28161
|
await cleanupRemoteRecords(ssh, paths);
|
|
@@ -28048,7 +28163,7 @@ async function completedUpgradeOutput(ssh, paths) {
|
|
|
28048
28163
|
throw remoteReconciliationFailure("Upgrade succeeded but remote evidence cleanup could not be confirmed", [error], paths);
|
|
28049
28164
|
}
|
|
28050
28165
|
return `✅ Upgrade done
|
|
28051
|
-
${JSON.stringify(safetyEvidence)}
|
|
28166
|
+
${JSON.stringify({ preflight: preflightEvidence, transaction: safetyEvidence })}
|
|
28052
28167
|
${log.slice(-1500)}`;
|
|
28053
28168
|
}
|
|
28054
28169
|
async function throwRemoteUpgradeFailure(ssh, paths, status) {
|
|
@@ -28058,13 +28173,19 @@ async function throwRemoteUpgradeFailure(ssh, paths, status) {
|
|
|
28058
28173
|
} catch (error) {
|
|
28059
28174
|
throw remoteReconciliationFailure("Remote upgrade failed but its retained log could not be read", [error], paths);
|
|
28060
28175
|
}
|
|
28061
|
-
const failure = new Error(`Remote local upgrade failed (${status}): ${log.slice(-1500)}`);
|
|
28062
28176
|
if (status.endsWith(":CLEANUP_AFTER_TRANSACTION")) {
|
|
28063
|
-
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);
|
|
28064
28178
|
}
|
|
28065
28179
|
if (status.includes("CLEANUP")) {
|
|
28066
|
-
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);
|
|
28067
28187
|
}
|
|
28188
|
+
const failure = new Error(`Remote local upgrade failed (${status}): ${failureEvidence.summary}` + (failureEvidence.causes.length > 0 ? `; causes: ${failureEvidence.causes.join(" | ")}` : ""));
|
|
28068
28189
|
try {
|
|
28069
28190
|
await cleanupRemoteRecords(ssh, paths);
|
|
28070
28191
|
} catch (cleanupError) {
|
|
@@ -32245,7 +32366,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
|
|
|
32245
32366
|
// package.json
|
|
32246
32367
|
var package_default = {
|
|
32247
32368
|
name: "@supacloud/admin",
|
|
32248
|
-
version: "0.15.
|
|
32369
|
+
version: "0.15.5",
|
|
32249
32370
|
description: "Platform administration CLI for SupaCloud operators",
|
|
32250
32371
|
type: "module",
|
|
32251
32372
|
main: "./dist/index.js",
|