@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.
Files changed (2) hide show
  1. package/dist/index.js +142 -21
  2. 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
- if (!this.truncated)
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/]+:)[^@\s]+@/gi, "$1[REDACTED]@");
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 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]");
25032
- const redactedStructuredFields = redactedLines.replace(/((?:["']?(?:password|pass|secret|token|key|credential|db_uri|database_url|dsn)["']?)\s*:\s*)(?:"[^"]*"|'[^']*'|[^,}\]\r\n]+)/gi, "$1[REDACTED]");
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: stdout.finalize(),
25178
- stderr: stderr.finalize(),
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 MINIMUM_CONTROL_PLANE_SAFETY_VERSION = [0, 61, 4];
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 parseControlPlaneSafetyEvidence(log) {
27346
- const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(CONTROL_PLANE_SAFETY_PREFIX));
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(CONTROL_PLANE_SAFETY_PREFIX.length));
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.4 or newer with control-plane backup safety");
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
- `env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" upgrade --yes --target-version ${quoteShell2(request.managementVersion)} --edge-runtime-version ${quoteShell2(request.edgeRuntimeVersion)} --asset-bundle-dir "$BUNDLE"`
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", [failure], paths);
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", [failure], paths);
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.4",
32369
+ version: "0.15.5",
32249
32370
  description: "Platform administration CLI for SupaCloud operators",
32250
32371
  type: "module",
32251
32372
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.15.4",
3
+ "version": "0.15.5",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",