@supacloud/admin 0.15.0 → 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 +39 -2
- package/dist/index.js +91 -15
- package/package.json +1 -1
- package/dist/sshcrypto-vd2k5hq9.node +0 -0
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.
|
|
128
|
-
--edge_runtime_version 0.
|
|
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 =
|
|
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
|
|
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
|
|
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);
|
|
@@ -30013,7 +30089,7 @@ async function cleanedCredentialState(cleanup, sanitized) {
|
|
|
30013
30089
|
return sanitized ? "absent" : "unknown";
|
|
30014
30090
|
}
|
|
30015
30091
|
async function removeFailedEnvFile(cleanup) {
|
|
30016
|
-
const { directoryHandle,
|
|
30092
|
+
const { directoryHandle, openFile } = cleanup;
|
|
30017
30093
|
if (!openFile) {
|
|
30018
30094
|
try {
|
|
30019
30095
|
await directoryHandle.close();
|
|
@@ -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
|
|
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:
|
|
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 || !
|
|
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
|
|
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.
|
|
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
|
Binary file
|