@supacloud/admin 0.15.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,8 +124,8 @@ external Edge Runtime as a single rollback-capable transaction:
124
124
 
125
125
  ```bash
126
126
  npx @supacloud/admin ssh upgrade \
127
- --version 0.50.31 \
128
- --edge_runtime_version 0.16.8 \
127
+ --version 0.61.4 \
128
+ --edge_runtime_version 0.18.2 \
129
129
  --artifact_transport local \
130
130
  --github_proxy direct
131
131
  ```
@@ -150,6 +150,43 @@ enabled state. Component upgrades require persisted `EDGE_RUNTIME_MODE=external`
150
150
  embedded mode is rejected before release artifacts or services are changed.
151
151
  Local, remote, and direct server upgrades share one nonblocking host-wide lock.
152
152
 
153
+ Local artifact upgrades require Management 0.61.4 or newer. After stopping the
154
+ old Management service and before writing runtime secrets or running
155
+ `--init-db`, the target binary verifies that `DATABASE_URL` identifies the
156
+ Management control-plane database, rejects a registered tenant database, and
157
+ creates a verified custom-format PostgreSQL archive. Backups and their private
158
+ receipts are stored under
159
+ `/var/lib/supacloud/backups/control-plane-upgrades/<backup-id>/`; the five most
160
+ recent trusted backups are retained.
161
+
162
+ The inspection transaction remains open while `pg_dump` imports its exported
163
+ snapshot, so a different PostgreSQL server cannot satisfy the dump step. The
164
+ root process reserves the private archive first; the `postgres` identity receives
165
+ only that inherited output descriptor and never gains path access to the
166
+ root-only backup directory.
167
+
168
+ The inspection transaction also keeps its exported snapshot alive until staged
169
+ `--init-db` finishes. The child receives the database fingerprint and snapshot
170
+ only in its process environment, imports that live snapshot, verifies the
171
+ PostgreSQL cluster and database identity, and performs all initialization writes
172
+ inside that same transaction. This keeps the writes on one PostgreSQL backend
173
+ even when `DATABASE_URL` points through a transaction-pooling proxy. A copied
174
+ data directory, promoted standby, restored disk snapshot, or other node cannot
175
+ import the exporter-owned snapshot even when its static fingerprint matches.
176
+ The guard is not persisted or printed. A guard rejection restores the previous
177
+ runtime environment but leaves Management stopped for explicit reconciliation
178
+ instead of reconnecting it to an unverified target.
179
+
180
+ A committed transaction emits exactly one redacted
181
+ `supacloud.control-plane-upgrade-safety.v1` receipt. It contains only the backup
182
+ identifier and directory, byte count, SHA-256 digest, migration candidate
183
+ counts, checkpoint state, and completion time. Admin validates the exact
184
+ receipt schema before deleting the transient
185
+ status and log records. A missing or malformed receipt leaves those records in
186
+ place and requires reconciliation. Restoring a control-plane backup is a
187
+ separate destructive operation and requires an explicit, independently reviewed
188
+ recovery decision; the upgrade command never restores one automatically.
189
+
153
190
  Admin observes the remote transaction for up to 30 minutes. Reaching that
154
191
  deadline stops only local observation; it does not stop, clean up, or mark the
155
192
  remote transaction as failed. The CLI reports the unit, stage, status, log, and
package/dist/index.js CHANGED
@@ -4757,11 +4757,6 @@ var require_utils = __commonJS((exports, module) => {
4757
4757
  };
4758
4758
  });
4759
4759
 
4760
- // node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node
4761
- var require_sshcrypto = __commonJS((exports, module) => {
4762
- module.exports = __require("./sshcrypto-vd2k5hq9.node");
4763
- });
4764
-
4765
4760
  // node_modules/ssh2/lib/protocol/crypto/poly1305.js
4766
4761
  var require_poly1305 = __commonJS((exports, module) => {
4767
4762
  var __dirname = "/home/runner/work/supacloud/supacloud/packages/admin/node_modules/ssh2/lib/protocol/crypto", __filename = "/home/runner/work/supacloud/supacloud/packages/admin/node_modules/ssh2/lib/protocol/crypto/poly1305.js";
@@ -5248,7 +5243,7 @@ var require_crypto = __commonJS((exports, module) => {
5248
5243
  var ChaChaPolyDecipher;
5249
5244
  var GenericDecipher;
5250
5245
  try {
5251
- binding = require_sshcrypto();
5246
+ binding = (()=>{throw new Error("Cannot require module "+"./crypto/build/Release/sshcrypto.node");})();
5252
5247
  ({
5253
5248
  AESGCMCipher,
5254
5249
  ChaChaPolyCipher,
@@ -27192,6 +27187,9 @@ var REMOTE_RUN_ROOT = "/var/lib/supacloud/upgrade-runs";
27192
27187
  var REMOTE_LOG_ROOT = "/var/log/supacloud";
27193
27188
  var REMOTE_UPLOAD_ROOT = "/var/tmp";
27194
27189
  var REMOTE_COMMAND_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
27190
+ var CONTROL_PLANE_BACKUP_ROOT = "/var/lib/supacloud/backups/control-plane-upgrades";
27191
+ var CONTROL_PLANE_SAFETY_PREFIX = "SUPACLOUD_CONTROL_PLANE_UPGRADE_SAFETY=";
27192
+ var MINIMUM_CONTROL_PLANE_SAFETY_VERSION = [0, 61, 4];
27195
27193
  var POLL_INTERVAL_MS = 2000;
27196
27194
  var STATE_READ_ATTEMPTS = 3;
27197
27195
  var REMOTE_STATE_READ_TIMEOUT_MS = 15000;
@@ -27199,6 +27197,76 @@ var UPGRADE_OBSERVATION_TIMEOUT_MS = 30 * 60000;
27199
27197
 
27200
27198
  class RemoteUpgradeReconciliationError extends AggregateError {
27201
27199
  }
27200
+ function exactObjectKeys(candidate, expected) {
27201
+ const actual = Object.keys(candidate).sort();
27202
+ return actual.length === expected.length && actual.every((key, index) => key === [...expected].sort()[index]);
27203
+ }
27204
+ function nonNegativeIntegerRecord(candidate, expectedKeys) {
27205
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate))
27206
+ return false;
27207
+ const record = candidate;
27208
+ return exactObjectKeys(record, expectedKeys) && Object.values(record).every((value) => Number.isSafeInteger(value) && Number(value) >= 0);
27209
+ }
27210
+ function canonicalTimestamp(candidate) {
27211
+ if (typeof candidate !== "string")
27212
+ return false;
27213
+ const timestamp = new Date(candidate);
27214
+ return Number.isFinite(timestamp.valueOf()) && timestamp.toISOString() === candidate;
27215
+ }
27216
+ function parseControlPlaneSafetyEvidence(log) {
27217
+ const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(CONTROL_PLANE_SAFETY_PREFIX));
27218
+ if (receiptLines.length !== 1)
27219
+ throw new Error("Remote upgrade did not emit one control-plane safety receipt");
27220
+ let candidate;
27221
+ try {
27222
+ candidate = JSON.parse(receiptLines[0].slice(CONTROL_PLANE_SAFETY_PREFIX.length));
27223
+ } catch {
27224
+ throw new Error("Remote control-plane safety receipt is not valid JSON");
27225
+ }
27226
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
27227
+ throw new Error("Remote control-plane safety receipt is invalid");
27228
+ }
27229
+ const receipt = candidate;
27230
+ const expectedKeys = [
27231
+ "backup_id",
27232
+ "backup_directory",
27233
+ "bytes",
27234
+ "candidate_counts",
27235
+ "completed_at",
27236
+ "current_key_checkpoint_present",
27237
+ "schema",
27238
+ "sha256"
27239
+ ];
27240
+ const candidateKeys = [
27241
+ "deprecated_webhook_secrets",
27242
+ "legacy_deployment_history_rows",
27243
+ "legacy_project_config_rows",
27244
+ "opaque_key_backfill_projects",
27245
+ "stored_secret_values"
27246
+ ];
27247
+ const validBackupId = typeof receipt.backup_id === "string" && /^control-plane-\d{8}T\d{6}Z-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(receipt.backup_id);
27248
+ const validDigest = typeof receipt.sha256 === "string" && /^[0-9a-f]{64}$/.test(receipt.sha256);
27249
+ if (!exactObjectKeys(receipt, expectedKeys) || receipt.schema !== "supacloud.control-plane-upgrade-safety.v1" || !validBackupId || receipt.backup_directory !== `${CONTROL_PLANE_BACKUP_ROOT}/${receipt.backup_id}` || !Number.isSafeInteger(receipt.bytes) || Number(receipt.bytes) <= 0 || !canonicalTimestamp(receipt.completed_at) || typeof receipt.current_key_checkpoint_present !== "boolean" || !validDigest || !nonNegativeIntegerRecord(receipt.candidate_counts, candidateKeys)) {
27250
+ throw new Error("Remote control-plane safety receipt is invalid");
27251
+ }
27252
+ return receipt;
27253
+ }
27254
+ function assertControlPlaneSafetyVersion(version) {
27255
+ const match = version.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/);
27256
+ if (!match)
27257
+ throw new Error("Management version must be an exact stable version");
27258
+ const requested = match.slice(1).map(Number);
27259
+ if (!requested.every(Number.isSafeInteger)) {
27260
+ throw new Error("Management version must be an exact stable version");
27261
+ }
27262
+ for (let index = 0;index < requested.length; index += 1) {
27263
+ if (requested[index] > MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index])
27264
+ return;
27265
+ if (requested[index] < MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index]) {
27266
+ throw new Error("Local artifact upgrades require Management 0.61.4 or newer with control-plane backup safety");
27267
+ }
27268
+ }
27269
+ }
27202
27270
  function quoteShell2(shellText) {
27203
27271
  return `'${shellText.split("'").join("'\\''")}'`;
27204
27272
  }
@@ -27254,7 +27322,7 @@ function buildRemotePreflightScript() {
27254
27322
  "export PATH",
27255
27323
  trustedInstalledGithubFunction(),
27256
27324
  "test -d /run/systemd/system || { echo 'systemd is not the active init system' >&2; exit 1; }",
27257
- 'for tool in systemctl systemd-run sha256sum stat realpath tar file find sort awk grep tail timeout flock install mktemp; do command -v "$tool" >/dev/null 2>&1 || { echo "Required local-upgrade tool is missing: $tool" >&2; exit 127; }; done',
27325
+ 'for tool in systemctl systemd-run sha256sum stat realpath tar file find sort awk grep tail timeout flock install mktemp setpriv id pg_dump pg_restore; do command -v "$tool" >/dev/null 2>&1 || { echo "Required local-upgrade tool is missing: $tool" >&2; exit 127; }; done',
27258
27326
  "tail -n 0 -- /dev/null >/dev/null 2>&1 || { echo 'A tail implementation with -n and -- support is required' >&2; exit 1; }",
27259
27327
  "systemd-run --help | grep -Eq -- '(^|[[:space:]])--collect([=[:space:]]|$)' || { echo 'systemd-run --collect is required' >&2; exit 1; }",
27260
27328
  "test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
@@ -27839,12 +27907,19 @@ async function completedUpgradeOutput(ssh, paths) {
27839
27907
  } catch (error) {
27840
27908
  throw remoteReconciliationFailure("Upgrade succeeded but its retained log could not be read", [error], paths);
27841
27909
  }
27910
+ let safetyEvidence;
27911
+ try {
27912
+ safetyEvidence = parseControlPlaneSafetyEvidence(log);
27913
+ } catch (error) {
27914
+ throw remoteReconciliationFailure("Upgrade succeeded but control-plane safety evidence is invalid", [error], paths);
27915
+ }
27842
27916
  try {
27843
27917
  await cleanupRemoteRecords(ssh, paths);
27844
27918
  } catch (error) {
27845
27919
  throw remoteReconciliationFailure("Upgrade succeeded but remote evidence cleanup could not be confirmed", [error], paths);
27846
27920
  }
27847
27921
  return `✅ Upgrade done
27922
+ ${JSON.stringify(safetyEvidence)}
27848
27923
  ${log.slice(-1500)}`;
27849
27924
  }
27850
27925
  async function throwRemoteUpgradeFailure(ssh, paths, status) {
@@ -27908,6 +27983,7 @@ async function awaitRemoteUpgrade(ssh, paths) {
27908
27983
  }
27909
27984
  }
27910
27985
  async function executeLocalUpgradeTransfer(ssh, request) {
27986
+ assertControlPlaneSafetyVersion(request.managementVersion);
27911
27987
  const runId = randomUUID3();
27912
27988
  const paths = buildRemoteUpgradePaths(runId);
27913
27989
  const preflight = await remoteUpgradePreflight(ssh);
@@ -29367,7 +29443,7 @@ function isPlainRecord(candidate) {
29367
29443
  const prototype = Object.getPrototypeOf(candidate);
29368
29444
  return prototype === Object.prototype || prototype === null;
29369
29445
  }
29370
- function canonicalTimestamp(candidate) {
29446
+ function canonicalTimestamp2(candidate) {
29371
29447
  if (typeof candidate !== "string")
29372
29448
  return false;
29373
29449
  const timestamp = new Date(candidate);
@@ -29384,7 +29460,7 @@ function validBackupDatabase(candidate) {
29384
29460
  return typeof candidate.database === "string" && SAFE_DATABASE.test(candidate.database);
29385
29461
  }
29386
29462
  function validBackupTimestamps(candidate) {
29387
- return canonicalTimestamp(candidate.created_at) && canonicalTimestamp(candidate.completed_at) && new Date(candidate.completed_at).valueOf() >= new Date(candidate.created_at).valueOf();
29463
+ return canonicalTimestamp2(candidate.created_at) && canonicalTimestamp2(candidate.completed_at) && new Date(candidate.completed_at).valueOf() >= new Date(candidate.created_at).valueOf();
29388
29464
  }
29389
29465
  function validBackupEvidence(candidate) {
29390
29466
  return Number.isSafeInteger(candidate.bytes) && Number(candidate.bytes) > 0 && typeof candidate.sha256 === "string" && SHA256.test(candidate.sha256);
@@ -30273,7 +30349,7 @@ function matchingText(candidate, maxLength, pattern) {
30273
30349
  const candidateText = boundedText(candidate, maxLength);
30274
30350
  return candidateText && pattern.test(candidateText) ? candidateText : null;
30275
30351
  }
30276
- function canonicalTimestamp2(candidate) {
30352
+ function canonicalTimestamp3(candidate) {
30277
30353
  const timestamp = boundedText(candidate, 64);
30278
30354
  if (!timestamp)
30279
30355
  return null;
@@ -30288,7 +30364,7 @@ function projectedSummary(project) {
30288
30364
  organization_slug: matchingText(project.organization_slug, 128, SAFE_IDENTIFIER_PATTERN),
30289
30365
  name: boundedText(project.name, 100),
30290
30366
  region: matchingText(project.region, 64, REGION_PATTERN),
30291
- created_at: canonicalTimestamp2(project.created_at),
30367
+ created_at: canonicalTimestamp3(project.created_at),
30292
30368
  status: matchingText(project.status, 64, STATUS_PATTERN)
30293
30369
  };
30294
30370
  return Object.values(summary).every((field) => field !== null) ? summary : null;
@@ -31612,7 +31688,7 @@ function releaseRecord(candidate) {
31612
31688
  "created_at",
31613
31689
  "kind"
31614
31690
  ];
31615
- if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || typeof record.release_id !== "string" || !RELEASE_ID_PATTERN.test(record.release_id) || record.sha256 !== record.release_id || typeof record.tree_sha256 !== "string" || !RELEASE_ID_PATTERN.test(record.tree_sha256) || !Number.isSafeInteger(record.size_bytes) || Number(record.size_bytes) < 1 || !Number.isSafeInteger(record.file_count) || Number(record.file_count) < 1 || !canonicalTimestamp3(record.created_at) || record.kind !== "prebuilt_static")
31691
+ if (!exactKeys(record, keys) || record.schema !== "supacloud.frontend-release.v1" || typeof record.project_ref !== "string" || !PROJECT_REF_PATTERN3.test(record.project_ref) || typeof record.deployment_id !== "string" || !DEPLOYMENT_ID_PATTERN.test(record.deployment_id) || typeof record.release_id !== "string" || !RELEASE_ID_PATTERN.test(record.release_id) || record.sha256 !== record.release_id || typeof record.tree_sha256 !== "string" || !RELEASE_ID_PATTERN.test(record.tree_sha256) || !Number.isSafeInteger(record.size_bytes) || Number(record.size_bytes) < 1 || !Number.isSafeInteger(record.file_count) || Number(record.file_count) < 1 || !canonicalTimestamp4(record.created_at) || record.kind !== "prebuilt_static")
31616
31692
  return null;
31617
31693
  return {
31618
31694
  project_ref: record.project_ref,
@@ -31635,7 +31711,7 @@ function releaseEnvelope(candidate, expected) {
31635
31711
  return null;
31636
31712
  return release;
31637
31713
  }
31638
- function canonicalTimestamp3(candidate) {
31714
+ function canonicalTimestamp4(candidate) {
31639
31715
  if (typeof candidate !== "string" || !TIMESTAMP_PATTERN.test(candidate))
31640
31716
  return false;
31641
31717
  const milliseconds = Date.parse(candidate);
@@ -32040,7 +32116,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
32040
32116
  // package.json
32041
32117
  var package_default = {
32042
32118
  name: "@supacloud/admin",
32043
- version: "0.15.1",
32119
+ version: "0.15.2",
32044
32120
  description: "Platform administration CLI for SupaCloud operators",
32045
32121
  type: "module",
32046
32122
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
Binary file