@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.
Files changed (2) hide show
  1. package/dist/index.js +263 -51
  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);
@@ -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, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
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
- async function runGithubCli(arguments_, timeoutMs) {
26959
- return await new Promise((resolve2, reject) => {
26960
- const environment = directEnvironment();
26961
- const child = spawn(githubCliExecutable(environment), arguments_, {
26962
- env: environment,
26963
- stdio: ["ignore", "pipe", "pipe"]
26964
- });
26965
- let stdout = "";
26966
- let stderr = "";
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({ exitCode: timedOut ? 124 : exitCode, stdout, stderr });
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
- if (manifest.repository === RELEASE_REPOSITORY) {
27078
- const bundles = serializeAttestationBundles(parseJsonFile(responsePath, "GitHub attestation response"));
27079
- writeFileSync2(destination, bundles, { mode: 384, flag: "wx" });
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
- await downloadDirect(releaseAssetUrl(release, RELEASE_MANIFEST_NAME), manifestPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
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
- await downloadDirect(releaseAssetUrl(release, RELEASE_CHECKSUMS_NAME), checksumsPath, RELEASE_BUNDLE_SIZE_LIMITS.checksums);
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
- await downloadDirect(releaseAssetUrl(release, assetName), assetPath, releaseAssetSizeLimit(request.component, assetName));
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
- const url = `https://github.com/cli/cli/releases/download/v${identity.version}/${identity.archiveName}`;
27116
- await downloadDirect(url, archivePath, MAX_GH_ARCHIVE_BYTES);
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 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];
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 parseControlPlaneSafetyEvidence(log) {
27255
- 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));
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(CONTROL_PLANE_SAFETY_PREFIX.length));
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.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");
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
- `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"`
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", [failure], paths);
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", [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);
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.3",
32369
+ version: "0.15.5",
32158
32370
  description: "Platform administration CLI for SupaCloud operators",
32159
32371
  type: "module",
32160
32372
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.15.3",
3
+ "version": "0.15.5",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",