@supacloud/admin 0.15.4 → 0.16.0
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 +28 -0
- package/dist/index.js +292 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,7 @@ npx @supacloud/admin status
|
|
|
100
100
|
npx @supacloud/admin ssh ping
|
|
101
101
|
npx @supacloud/admin ssh versions
|
|
102
102
|
npx @supacloud/admin ssh diagnose
|
|
103
|
+
npx @supacloud/admin ssh upgrade_status --transaction_id 11111111-1111-4111-8111-111111111111
|
|
103
104
|
npx @supacloud/admin project create --name my-app --domain example.com \
|
|
104
105
|
--env_file /secure/path/.env.project-credentials.test --environment test
|
|
105
106
|
npx @supacloud/admin project list
|
|
@@ -193,6 +194,33 @@ remote transaction as failed. The CLI reports the unit, stage, status, log, and
|
|
|
193
194
|
upload-drop paths for reconciliation. Inspect that evidence before retrying and
|
|
194
195
|
do not retry blindly while the remote transaction may still be running.
|
|
195
196
|
|
|
197
|
+
When observation ends after 30 minutes, or to safely reconcile a retained
|
|
198
|
+
local-artifact upgrade transaction, use the read-only `ssh upgrade_status`
|
|
199
|
+
command:
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npx @supacloud/admin ssh upgrade_status \
|
|
203
|
+
--transaction_id 11111111-1111-4111-8111-111111111111
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`ssh upgrade_status` is classified as a read-only command and is permitted in
|
|
207
|
+
read-only mode (`SUPACLOUD_READ_ONLY=true`). It requires a strict UUID v4
|
|
208
|
+
transaction ID before performing any SSH access. It never deletes status, log,
|
|
209
|
+
stage, or upload-drop records, and never mutates service state. The command emits
|
|
210
|
+
a strict JSON projection (`supacloud.admin.upgrade-status.v1`) containing the
|
|
211
|
+
normalized transaction ID, lifecycle state (`running`, `succeeded`, or
|
|
212
|
+
`failed`), raw bounded status, systemd active/load states, boolean
|
|
213
|
+
evidence-presence flags, and validated structured receipts:
|
|
214
|
+
- Nonterminal (`running`): no receipts or failure evidence are included.
|
|
215
|
+
- Succeeded: path-free projections of the validated control-plane preflight and
|
|
216
|
+
transaction safety receipts.
|
|
217
|
+
- Failed: validated failure evidence with credentials and remote paths redacted.
|
|
218
|
+
|
|
219
|
+
The command fails closed for missing or inconsistent terminal evidence, stopped
|
|
220
|
+
nonterminal units, malformed or missing structured receipts, redacted or
|
|
221
|
+
truncated SSH output, or unknown status, without emitting raw logs, remote
|
|
222
|
+
filesystem paths, secrets, bearer material, env values, or customer data.
|
|
223
|
+
|
|
196
224
|
`--artifact_transport local` accepts only `--github_proxy direct` or `none` and
|
|
197
225
|
clears proxy environment variables on both hosts. The server-download path
|
|
198
226
|
remains available as `--artifact_transport remote`; it verifies and executes
|
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);
|
|
@@ -25837,7 +25902,8 @@ var ACTION_POLICY = {
|
|
|
25837
25902
|
"container_logs",
|
|
25838
25903
|
"tenant_list",
|
|
25839
25904
|
"tenant_inspect",
|
|
25840
|
-
"tenant_diagnose"
|
|
25905
|
+
"tenant_diagnose",
|
|
25906
|
+
"upgrade_status"
|
|
25841
25907
|
],
|
|
25842
25908
|
write: ["setup", "install", "upgrade", "tenant_migrate"]
|
|
25843
25909
|
}
|
|
@@ -27317,8 +27383,10 @@ var REMOTE_LOG_ROOT = "/var/log/supacloud";
|
|
|
27317
27383
|
var REMOTE_UPLOAD_ROOT = "/var/tmp";
|
|
27318
27384
|
var REMOTE_COMMAND_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
27319
27385
|
var CONTROL_PLANE_BACKUP_ROOT = "/var/lib/supacloud/backups/control-plane-upgrades";
|
|
27386
|
+
var CONTROL_PLANE_PREFLIGHT_PREFIX = "SUPACLOUD_CONTROL_PLANE_UPGRADE_PREFLIGHT=";
|
|
27320
27387
|
var CONTROL_PLANE_SAFETY_PREFIX = "SUPACLOUD_CONTROL_PLANE_UPGRADE_SAFETY=";
|
|
27321
|
-
var
|
|
27388
|
+
var UPGRADE_FAILURE_PREFIX = "SUPACLOUD_UPGRADE_FAILURE=";
|
|
27389
|
+
var MINIMUM_CONTROL_PLANE_SAFETY_VERSION = [0, 61, 7];
|
|
27322
27390
|
var POLL_INTERVAL_MS = 2000;
|
|
27323
27391
|
var STATE_READ_ATTEMPTS = 3;
|
|
27324
27392
|
var REMOTE_STATE_READ_TIMEOUT_MS = 15000;
|
|
@@ -27326,6 +27394,34 @@ var UPGRADE_OBSERVATION_TIMEOUT_MS = 30 * 60000;
|
|
|
27326
27394
|
|
|
27327
27395
|
class RemoteUpgradeReconciliationError extends AggregateError {
|
|
27328
27396
|
}
|
|
27397
|
+
var UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
27398
|
+
function assertValidTransactionId(transactionId) {
|
|
27399
|
+
if (typeof transactionId !== "string" || !UUID_V4_PATTERN.test(transactionId)) {
|
|
27400
|
+
throw new Error("Invalid 'transaction_id': must be a valid UUID v4");
|
|
27401
|
+
}
|
|
27402
|
+
return transactionId.toLowerCase();
|
|
27403
|
+
}
|
|
27404
|
+
function upgradeStatusControlPlaneEvidence(receipt) {
|
|
27405
|
+
return {
|
|
27406
|
+
backup_id: receipt.backup_id,
|
|
27407
|
+
bytes: receipt.bytes,
|
|
27408
|
+
candidate_counts: { ...receipt.candidate_counts },
|
|
27409
|
+
completed_at: receipt.completed_at,
|
|
27410
|
+
current_key_checkpoint_present: receipt.current_key_checkpoint_present,
|
|
27411
|
+
receipt_schema: receipt.schema,
|
|
27412
|
+
sha256: receipt.sha256
|
|
27413
|
+
};
|
|
27414
|
+
}
|
|
27415
|
+
function redactRemotePaths(message) {
|
|
27416
|
+
return message.replace(/\bhttps?:\/\/[^\s"'`,;)\]}]+/gi, "[REDACTED_URL]").replace(/\bfile:\/\/\/[^\s"'`,;)\]}]+/gi, "[REDACTED_PATH]").replace(/(^|[^A-Za-z0-9:/])\/(?!\/)[^\s"'`,;)\]}]+/g, "$1[REDACTED_PATH]");
|
|
27417
|
+
}
|
|
27418
|
+
function upgradeStatusFailureEvidence(receipt) {
|
|
27419
|
+
return {
|
|
27420
|
+
causes: receipt.causes.map(redactRemotePaths),
|
|
27421
|
+
receipt_schema: receipt.schema,
|
|
27422
|
+
summary: redactRemotePaths(receipt.summary)
|
|
27423
|
+
};
|
|
27424
|
+
}
|
|
27329
27425
|
function exactObjectKeys(candidate, expected) {
|
|
27330
27426
|
const actual = Object.keys(candidate).sort();
|
|
27331
27427
|
return actual.length === expected.length && actual.every((key, index) => key === [...expected].sort()[index]);
|
|
@@ -27342,13 +27438,13 @@ function canonicalTimestamp(candidate) {
|
|
|
27342
27438
|
const timestamp = new Date(candidate);
|
|
27343
27439
|
return Number.isFinite(timestamp.valueOf()) && timestamp.toISOString() === candidate;
|
|
27344
27440
|
}
|
|
27345
|
-
function
|
|
27346
|
-
const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(
|
|
27441
|
+
function parseControlPlaneSafetyReceipt(log, prefix) {
|
|
27442
|
+
const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(prefix));
|
|
27347
27443
|
if (receiptLines.length !== 1)
|
|
27348
27444
|
throw new Error("Remote upgrade did not emit one control-plane safety receipt");
|
|
27349
27445
|
let candidate;
|
|
27350
27446
|
try {
|
|
27351
|
-
candidate = JSON.parse(receiptLines[0].slice(
|
|
27447
|
+
candidate = JSON.parse(receiptLines[0].slice(prefix.length));
|
|
27352
27448
|
} catch {
|
|
27353
27449
|
throw new Error("Remote control-plane safety receipt is not valid JSON");
|
|
27354
27450
|
}
|
|
@@ -27380,6 +27476,46 @@ function parseControlPlaneSafetyEvidence(log) {
|
|
|
27380
27476
|
}
|
|
27381
27477
|
return receipt;
|
|
27382
27478
|
}
|
|
27479
|
+
function parseControlPlaneSafetyEvidence(log) {
|
|
27480
|
+
return parseControlPlaneSafetyReceipt(log, CONTROL_PLANE_SAFETY_PREFIX);
|
|
27481
|
+
}
|
|
27482
|
+
function parseControlPlanePreflightEvidence(log) {
|
|
27483
|
+
return parseControlPlaneSafetyReceipt(log, CONTROL_PLANE_PREFLIGHT_PREFIX);
|
|
27484
|
+
}
|
|
27485
|
+
function parsedUpgradeFailureReceipt(log) {
|
|
27486
|
+
const receiptLines = log.split(/\r?\n/).filter((line) => line.startsWith(UPGRADE_FAILURE_PREFIX));
|
|
27487
|
+
if (receiptLines.length !== 1)
|
|
27488
|
+
throw new Error("Remote upgrade did not emit one structured failure receipt");
|
|
27489
|
+
let candidate;
|
|
27490
|
+
try {
|
|
27491
|
+
candidate = JSON.parse(receiptLines[0].slice(UPGRADE_FAILURE_PREFIX.length));
|
|
27492
|
+
} catch {
|
|
27493
|
+
throw new Error("Remote upgrade failure receipt is not valid JSON");
|
|
27494
|
+
}
|
|
27495
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
27496
|
+
throw new Error("Remote upgrade failure receipt is invalid");
|
|
27497
|
+
}
|
|
27498
|
+
return candidate;
|
|
27499
|
+
}
|
|
27500
|
+
function upgradeFailureReceiptIsInvalid(receipt) {
|
|
27501
|
+
const causes = receipt.causes;
|
|
27502
|
+
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));
|
|
27503
|
+
}
|
|
27504
|
+
function upgradeFailureMessageContainsSensitiveData(message) {
|
|
27505
|
+
const bareBearer = /\bBearer\s+(?!\[REDACTED\](?:\s|$|[,;.)]))\S+/i;
|
|
27506
|
+
return redactSshOutput(message) !== message || bareBearer.test(message);
|
|
27507
|
+
}
|
|
27508
|
+
function parseUpgradeFailureEvidence(log) {
|
|
27509
|
+
const receipt = parsedUpgradeFailureReceipt(log);
|
|
27510
|
+
if (upgradeFailureReceiptIsInvalid(receipt)) {
|
|
27511
|
+
throw new Error("Remote upgrade failure receipt is invalid");
|
|
27512
|
+
}
|
|
27513
|
+
const messages = [receipt.summary, ...receipt.causes];
|
|
27514
|
+
if (messages.some(upgradeFailureMessageContainsSensitiveData)) {
|
|
27515
|
+
throw new Error("Remote upgrade failure receipt contains sensitive data");
|
|
27516
|
+
}
|
|
27517
|
+
return receipt;
|
|
27518
|
+
}
|
|
27383
27519
|
function assertControlPlaneSafetyVersion(version) {
|
|
27384
27520
|
const match = version.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/);
|
|
27385
27521
|
if (!match)
|
|
@@ -27392,7 +27528,7 @@ function assertControlPlaneSafetyVersion(version) {
|
|
|
27392
27528
|
if (requested[index] > MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index])
|
|
27393
27529
|
return;
|
|
27394
27530
|
if (requested[index] < MINIMUM_CONTROL_PLANE_SAFETY_VERSION[index]) {
|
|
27395
|
-
throw new Error("Local artifact upgrades require Management 0.61.
|
|
27531
|
+
throw new Error("Local artifact upgrades require Management 0.61.7 or newer with control-plane backup preflight safety");
|
|
27396
27532
|
}
|
|
27397
27533
|
}
|
|
27398
27534
|
}
|
|
@@ -27713,6 +27849,7 @@ function bundledVerifierSetup(paths, architecture) {
|
|
|
27713
27849
|
function upgradeScriptExecution(paths, bundle, request) {
|
|
27714
27850
|
const runnerAsset = `${paths.stage}/bundle/management-api/${bundle.managementBinaryName}`;
|
|
27715
27851
|
const runner = `${paths.stage}/runner`;
|
|
27852
|
+
const preflight = 'CONTROL_PLANE_PREFLIGHT_RECEIPT=$(env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" --control-plane-upgrade-preflight)';
|
|
27716
27853
|
return [
|
|
27717
27854
|
`MANAGEMENT_VERSION=${quoteShell2(request.managementVersion)}`,
|
|
27718
27855
|
`RUNNER_ASSET=${quoteShell2(runnerAsset)}`,
|
|
@@ -27723,7 +27860,9 @@ function upgradeScriptExecution(paths, bundle, request) {
|
|
|
27723
27860
|
'"$RUNNER" --version | grep -Eq "(^|[^0-9])${MANAGEMENT_VERSION//./\\.}([^0-9]|$)"',
|
|
27724
27861
|
`timeout 5s "$RUNNER" --systemd-unit-helper-sha256 | grep -Eq 'SupaCloud systemd-unit helper SHA-256: [0-9a-f]{64}'`,
|
|
27725
27862
|
`timeout 5s "$RUNNER" --postgrest-launcher-sha256 | grep -Eq 'SupaCloud PostgREST launcher SHA-256: [0-9a-f]{64}'`,
|
|
27726
|
-
|
|
27863
|
+
preflight,
|
|
27864
|
+
`env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" upgrade --yes --target-version ${quoteShell2(request.managementVersion)} --edge-runtime-version ${quoteShell2(request.edgeRuntimeVersion)} --asset-bundle-dir "$BUNDLE"`,
|
|
27865
|
+
`printf '%s\\n' "$CONTROL_PLANE_PREFLIGHT_RECEIPT"`
|
|
27727
27866
|
];
|
|
27728
27867
|
}
|
|
27729
27868
|
function buildLocalUpgradeRunScript(paths, bundle, request, architecture) {
|
|
@@ -27939,6 +28078,9 @@ async function remoteLogTail(ssh, paths) {
|
|
|
27939
28078
|
const output = await ssh.exec(rootCommand(script), 15000);
|
|
27940
28079
|
if (!output.success)
|
|
27941
28080
|
throw remoteFailure("Unable to read the remote upgrade log", output);
|
|
28081
|
+
if (output.stdoutRedacted || output.stderrRedacted || output.stdoutTruncated || output.stderrTruncated) {
|
|
28082
|
+
throw new Error("Remote upgrade log required transport redaction or truncation");
|
|
28083
|
+
}
|
|
27942
28084
|
return output.stdout.slice(-4000);
|
|
27943
28085
|
}
|
|
27944
28086
|
async function cleanupRemoteRecords(ssh, paths) {
|
|
@@ -28036,11 +28178,13 @@ async function completedUpgradeOutput(ssh, paths) {
|
|
|
28036
28178
|
} catch (error) {
|
|
28037
28179
|
throw remoteReconciliationFailure("Upgrade succeeded but its retained log could not be read", [error], paths);
|
|
28038
28180
|
}
|
|
28181
|
+
let preflightEvidence;
|
|
28039
28182
|
let safetyEvidence;
|
|
28040
28183
|
try {
|
|
28184
|
+
preflightEvidence = parseControlPlanePreflightEvidence(log);
|
|
28041
28185
|
safetyEvidence = parseControlPlaneSafetyEvidence(log);
|
|
28042
28186
|
} catch (error) {
|
|
28043
|
-
throw remoteReconciliationFailure("Upgrade succeeded but control-plane safety evidence is invalid", [error], paths);
|
|
28187
|
+
throw remoteReconciliationFailure("Upgrade succeeded but control-plane preflight or safety evidence is invalid", [error], paths);
|
|
28044
28188
|
}
|
|
28045
28189
|
try {
|
|
28046
28190
|
await cleanupRemoteRecords(ssh, paths);
|
|
@@ -28048,7 +28192,7 @@ async function completedUpgradeOutput(ssh, paths) {
|
|
|
28048
28192
|
throw remoteReconciliationFailure("Upgrade succeeded but remote evidence cleanup could not be confirmed", [error], paths);
|
|
28049
28193
|
}
|
|
28050
28194
|
return `✅ Upgrade done
|
|
28051
|
-
${JSON.stringify(safetyEvidence)}
|
|
28195
|
+
${JSON.stringify({ preflight: preflightEvidence, transaction: safetyEvidence })}
|
|
28052
28196
|
${log.slice(-1500)}`;
|
|
28053
28197
|
}
|
|
28054
28198
|
async function throwRemoteUpgradeFailure(ssh, paths, status) {
|
|
@@ -28058,13 +28202,19 @@ async function throwRemoteUpgradeFailure(ssh, paths, status) {
|
|
|
28058
28202
|
} catch (error) {
|
|
28059
28203
|
throw remoteReconciliationFailure("Remote upgrade failed but its retained log could not be read", [error], paths);
|
|
28060
28204
|
}
|
|
28061
|
-
const failure = new Error(`Remote local upgrade failed (${status}): ${log.slice(-1500)}`);
|
|
28062
28205
|
if (status.endsWith(":CLEANUP_AFTER_TRANSACTION")) {
|
|
28063
|
-
throw remoteReconciliationFailure("Upgrade transaction completed but staging cleanup is incomplete", [
|
|
28206
|
+
throw remoteReconciliationFailure("Upgrade transaction completed but staging cleanup is incomplete", [new Error(`Remote local upgrade ended with ${status}`)], paths);
|
|
28064
28207
|
}
|
|
28065
28208
|
if (status.includes("CLEANUP")) {
|
|
28066
|
-
throw remoteReconciliationFailure("Upgrade transaction and staging cleanup both failed", [
|
|
28209
|
+
throw remoteReconciliationFailure("Upgrade transaction and staging cleanup both failed", [new Error(`Remote local upgrade ended with ${status}`)], paths);
|
|
28210
|
+
}
|
|
28211
|
+
let failureEvidence;
|
|
28212
|
+
try {
|
|
28213
|
+
failureEvidence = parseUpgradeFailureEvidence(log);
|
|
28214
|
+
} catch (error) {
|
|
28215
|
+
throw remoteReconciliationFailure("Remote upgrade failed without valid structured failure evidence", [error], paths);
|
|
28067
28216
|
}
|
|
28217
|
+
const failure = new Error(`Remote local upgrade failed (${status}): ${failureEvidence.summary}` + (failureEvidence.causes.length > 0 ? `; causes: ${failureEvidence.causes.join(" | ")}` : ""));
|
|
28068
28218
|
try {
|
|
28069
28219
|
await cleanupRemoteRecords(ssh, paths);
|
|
28070
28220
|
} catch (cleanupError) {
|
|
@@ -28151,6 +28301,115 @@ async function executeLocalUpgradeTransfer(ssh, request) {
|
|
|
28151
28301
|
}
|
|
28152
28302
|
}
|
|
28153
28303
|
}
|
|
28304
|
+
function upgradeStatusLifecycle(status) {
|
|
28305
|
+
if (["PREPARED", "RUNNING", "CLEANING"].includes(status))
|
|
28306
|
+
return "running";
|
|
28307
|
+
if (status === "SUCCEEDED")
|
|
28308
|
+
return "succeeded";
|
|
28309
|
+
const failedStatus = status.match(/^FAILED:([1-9]\d{0,2}):(TRANSACTION|TRANSACTION_AND_CLEANUP|CLEANUP_AFTER_TRANSACTION)$/);
|
|
28310
|
+
if (failedStatus && Number(failedStatus[1]) <= 255)
|
|
28311
|
+
return "failed";
|
|
28312
|
+
throw new Error("Remote upgrade status is unknown or invalid");
|
|
28313
|
+
}
|
|
28314
|
+
function upgradeStatusEvidence(state) {
|
|
28315
|
+
return {
|
|
28316
|
+
drop_exists: state.dropExists,
|
|
28317
|
+
log_exists: state.logExists,
|
|
28318
|
+
stage_exists: state.stageExists,
|
|
28319
|
+
stage_is_directory: state.stageIsDirectory,
|
|
28320
|
+
status_exists: state.statusExists,
|
|
28321
|
+
unit_exists: state.unitExists
|
|
28322
|
+
};
|
|
28323
|
+
}
|
|
28324
|
+
async function readUpgradeStatusState(ssh, paths) {
|
|
28325
|
+
try {
|
|
28326
|
+
return await readRemoteState(ssh, paths);
|
|
28327
|
+
} catch {
|
|
28328
|
+
throw new Error("Unable to read remote upgrade state safely");
|
|
28329
|
+
}
|
|
28330
|
+
}
|
|
28331
|
+
async function readUpgradeStatusLog(ssh, paths) {
|
|
28332
|
+
try {
|
|
28333
|
+
return await remoteLogTail(ssh, paths);
|
|
28334
|
+
} catch {
|
|
28335
|
+
throw new Error("Remote upgrade retained log could not be read safely");
|
|
28336
|
+
}
|
|
28337
|
+
}
|
|
28338
|
+
function assertUpgradeStatusTerminalEvidence(state, paths, lifecycle) {
|
|
28339
|
+
try {
|
|
28340
|
+
if (lifecycle === "succeeded")
|
|
28341
|
+
assertSuccessfulUnitStoppedNormally(state, paths);
|
|
28342
|
+
else
|
|
28343
|
+
assertFailedUnitReachedTerminalState(state, paths);
|
|
28344
|
+
assertTerminalEvidence(state, paths);
|
|
28345
|
+
} catch {
|
|
28346
|
+
throw new Error("Remote upgrade terminal evidence is incomplete or inconsistent");
|
|
28347
|
+
}
|
|
28348
|
+
}
|
|
28349
|
+
function succeededUpgradeStatus(transactionId, state, log) {
|
|
28350
|
+
let preflight;
|
|
28351
|
+
let transaction;
|
|
28352
|
+
try {
|
|
28353
|
+
preflight = parseControlPlanePreflightEvidence(log);
|
|
28354
|
+
transaction = parseControlPlaneSafetyEvidence(log);
|
|
28355
|
+
} catch {
|
|
28356
|
+
throw new Error("Remote upgrade safety receipts are missing or invalid");
|
|
28357
|
+
}
|
|
28358
|
+
return {
|
|
28359
|
+
schema: "supacloud.admin.upgrade-status.v1",
|
|
28360
|
+
transaction_id: transactionId,
|
|
28361
|
+
lifecycle: "succeeded",
|
|
28362
|
+
status: state.status,
|
|
28363
|
+
service_state: state.serviceState,
|
|
28364
|
+
unit_load_state: state.unitLoadState,
|
|
28365
|
+
evidence: upgradeStatusEvidence(state),
|
|
28366
|
+
preflight: upgradeStatusControlPlaneEvidence(preflight),
|
|
28367
|
+
transaction: upgradeStatusControlPlaneEvidence(transaction)
|
|
28368
|
+
};
|
|
28369
|
+
}
|
|
28370
|
+
function failedUpgradeStatus(transactionId, state, log) {
|
|
28371
|
+
let failure;
|
|
28372
|
+
try {
|
|
28373
|
+
failure = parseUpgradeFailureEvidence(log);
|
|
28374
|
+
} catch {
|
|
28375
|
+
throw new Error("Remote upgrade failure receipt is missing or invalid");
|
|
28376
|
+
}
|
|
28377
|
+
return {
|
|
28378
|
+
schema: "supacloud.admin.upgrade-status.v1",
|
|
28379
|
+
transaction_id: transactionId,
|
|
28380
|
+
lifecycle: "failed",
|
|
28381
|
+
status: state.status,
|
|
28382
|
+
service_state: state.serviceState,
|
|
28383
|
+
unit_load_state: state.unitLoadState,
|
|
28384
|
+
evidence: upgradeStatusEvidence(state),
|
|
28385
|
+
failure: upgradeStatusFailureEvidence(failure)
|
|
28386
|
+
};
|
|
28387
|
+
}
|
|
28388
|
+
function runningUpgradeStatus(transactionId, state) {
|
|
28389
|
+
if (!unitIsRunning(state.serviceState) || state.unitLoadState !== "loaded" || !state.statusExists) {
|
|
28390
|
+
throw new Error("Remote upgrade nonterminal state is inconsistent");
|
|
28391
|
+
}
|
|
28392
|
+
return {
|
|
28393
|
+
schema: "supacloud.admin.upgrade-status.v1",
|
|
28394
|
+
transaction_id: transactionId,
|
|
28395
|
+
lifecycle: "running",
|
|
28396
|
+
status: state.status,
|
|
28397
|
+
service_state: state.serviceState,
|
|
28398
|
+
unit_load_state: state.unitLoadState,
|
|
28399
|
+
evidence: upgradeStatusEvidence(state)
|
|
28400
|
+
};
|
|
28401
|
+
}
|
|
28402
|
+
async function inspectRemoteUpgradeStatus(ssh, transactionIdInput) {
|
|
28403
|
+
const transactionId = assertValidTransactionId(transactionIdInput);
|
|
28404
|
+
const paths = buildRemoteUpgradePaths(transactionId);
|
|
28405
|
+
const state = await readUpgradeStatusState(ssh, paths);
|
|
28406
|
+
const lifecycle = upgradeStatusLifecycle(state.status);
|
|
28407
|
+
if (lifecycle === "running")
|
|
28408
|
+
return runningUpgradeStatus(transactionId, state);
|
|
28409
|
+
assertUpgradeStatusTerminalEvidence(state, paths, lifecycle);
|
|
28410
|
+
const log = await readUpgradeStatusLog(ssh, paths);
|
|
28411
|
+
return lifecycle === "succeeded" ? succeededUpgradeStatus(transactionId, state, log) : failedUpgradeStatus(transactionId, state, log);
|
|
28412
|
+
}
|
|
28154
28413
|
|
|
28155
28414
|
// ../../scripts/lib/release_assets.sh
|
|
28156
28415
|
var release_assets_default = '#!/usr/bin/env bash\n\nSUPACLOUD_GITHUB_REPOSITORY="${SUPACLOUD_GITHUB_REPOSITORY:-vibeunion/supacloud}"\nSUPACLOUD_RELEASES_API="${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/releases}"\nSUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"\nSUPACLOUD_GH_VERSION="${SUPACLOUD_GH_VERSION:-2.96.0}"\nSUPACLOUD_GH_MIN_VERSION="${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"\nSUPACLOUD_GH_AMD64_SHA256="${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"\nSUPACLOUD_GH_ARM64_SHA256="${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"\nSUPACLOUD_RELEASE_ASSETS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\nSUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT="${SUPACLOUD_RELEASE_ASSETS_DIR}/../../packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256="3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE="5748"\n\nsupacloud_curl_release_json() {\n local url="$1"\n local output="$2"\n curl -fsSL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 60 \\\n --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\\n -o "$output" "$url"\n}\n\nsupacloud_curl_release_asset() {\n local url="$1"\n local output="$2"\n curl -fL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 180 \\\n --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\\n -o "$output" "$url"\n}\n\nsupacloud_component_tag() {\n local component="$1"\n local version="$2"\n case "$version" in\n "${component}-v"*) printf \'%s\' "$version" ;;\n v*) printf \'%s-%s\' "$component" "$version" ;;\n *) printf \'%s-v%s\' "$component" "$version" ;;\n esac\n}\n\nsupacloud_select_release() {\n local component="$1"\n shift\n local required_assets_json\n [[ $# -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "$@" | jq -Rsc \'split("\\n")[:-1]\')\n jq -ce --arg prefix "${component}-v" --argjson required "$required_assets_json" \'\n map(select(\n (.draft | not)\n and (.prerelease | not)\n and (.tag_name | startswith($prefix))\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n ))\n | first\n // error("no matching component release contains all required assets and SHA256SUMS")\n \'\n}\n\nsupacloud_fetch_component_release() {\n local component="$1"\n local version="${2:-latest}"\n shift 2\n local required_assets=("$@")\n local required_assets_json\n local response\n [[ ${#required_assets[@]} -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "${required_assets[@]}" | jq -Rsc \'split("\\n")[:-1]\')\n\n if [[ -n "$version" && "$version" != "latest" ]]; then\n local tag\n tag=$(supacloud_component_tag "$component" "$version")\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}/tags/${tag}") || return 1\n jq -ce --argjson required "$required_assets_json" \'\n select(\n (.draft | not)\n and (.prerelease | not)\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n )\n // error("release does not contain all required assets and SHA256SUMS")\n \' <<< "$response"\n return\n fi\n\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}?per_page=100") || return 1\n supacloud_select_release "$component" "${required_assets[@]}" <<< "$response"\n}\n\nsupacloud_fetch_release_json() (\n local url="$1"\n local response_file\n response_file=$(mktemp) || return 1\n trap \'rm -f "$response_file"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1\' HUP INT TERM\n supacloud_download_release_metadata_url "$url" "$response_file" || return 1\n cat "$response_file"\n)\n\nsupacloud_release_asset_url() {\n local release_json="$1"\n local asset_name="$2"\n jq -er --arg asset "$asset_name" \'\n first(.assets[]? | select(.name == $asset) | .browser_download_url)\n // error("release asset URL is missing")\n \' <<< "$release_json"\n}\n\nsupacloud_download_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_asset "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_asset "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_download_release_metadata_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_json "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_json "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_verify_checksum() {\n local artifact_file="$1"\n local asset_name="$2"\n local checksum_file="$3"\n local expected\n expected=$(awk -v asset="$asset_name" \'$2 == asset || $2 == "*" asset { print $1; exit }\' "$checksum_file")\n if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then\n echo "SHA256SUMS does not contain a valid checksum for ${asset_name}" >&2\n return 1\n fi\n\n local actual\n actual=$(sha256sum "$artifact_file" | awk \'{print $1}\')\n actual=$(printf \'%s\' "$actual" | tr \'[:upper:]\' \'[:lower:]\')\n expected=$(printf \'%s\' "$expected" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ "$actual" != "$expected" ]]; then\n echo "SHA256 mismatch for ${asset_name}" >&2\n return 1\n fi\n}\n\nsupacloud_validate_binary() {\n local artifact_file="$1"\n local asset_name="$2"\n local description\n description=$(file -b "$artifact_file")\n if [[ "$description" != *ELF* ]]; then\n echo "${asset_name} is not an ELF binary: ${description}" >&2\n return 1\n fi\n\n case "$asset_name" in\n *amd64)\n [[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {\n echo "${asset_name} does not contain an x86-64 ELF binary" >&2\n return 1\n }\n ;;\n *arm64)\n [[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {\n echo "${asset_name} does not contain an arm64 ELF binary" >&2\n return 1\n }\n ;;\n esac\n}\n\nsupacloud_install_pinned_tar_xz_binary() (\n local archive="$1"\n local member="$2"\n local expected_sha256="$3"\n local arch="$4"\n local target="$5"\n local actual_sha256 member_count member_details extract_dir candidate staged_target\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n actual_sha256=$(printf \'%s\' "$actual_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n expected_sha256=$(printf \'%s\' "$expected_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then\n echo "SHA256 mismatch for pinned archive" >&2\n return 1\n fi\n\n member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "Pinned archive must contain the exact member once: $member" >&2\n return 1\n fi\n member_details=$(tar -tvJf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "Pinned archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extract_dir=$(mktemp -d)\n trap \'rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"; exit 1\' HUP INT TERM\n if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then\n return 1\n fi\n candidate="${extract_dir}/${member}"\n supacloud_validate_binary "$candidate" "pinned-linux-${arch}" || return 1\n\n mkdir -p "$(dirname "$target")"\n staged_target=$(mktemp "${target}.tmp.XXXXXX")\n install -m 0755 "$candidate" "$staged_target"\n mv -f "$staged_target" "$target"\n staged_target=""\n)\n\nsupacloud_version_at_least() {\n local current="${1#v}"\n local required="${2#v}"\n local current_major=0 current_minor=0 current_patch=0\n local required_major=0 required_minor=0 required_patch=0\n [[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n [[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n IFS=. read -r current_major current_minor current_patch <<EOF\n${current%%-*}\nEOF\n IFS=. read -r required_major required_minor required_patch <<EOF\n${required%%-*}\nEOF\n current_major=${current_major:-0}; current_minor=${current_minor:-0}; current_patch=${current_patch:-0}\n required_major=${required_major:-0}; required_minor=${required_minor:-0}; required_patch=${required_patch:-0}\n if (( current_major != required_major )); then (( current_major > required_major )); return; fi\n if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi\n (( current_patch >= required_patch ))\n}\n\nsupacloud_gh_version() {\n gh --version 2>/dev/null | awk \'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }\'\n}\n\nsupacloud_install_gh_archive() {\n local archive="$1"\n local version="$2"\n local arch="$3"\n local expected_sha256="$4"\n local target="$5"\n local member="gh_${version}_linux_${arch}/bin/gh"\n local actual_sha256 member_count member_details extracted_dir candidate version_output\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n if [[ "$actual_sha256" != "$expected_sha256" ]]; then\n echo "GitHub CLI archive SHA256 mismatch" >&2\n return 1\n fi\n\n member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "GitHub CLI archive does not contain the exact expected member: $member" >&2\n return 1\n fi\n member_details=$(tar -tvzf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "GitHub CLI archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extracted_dir=$(mktemp -d)\n candidate="${extracted_dir}/${member}"\n if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\\n || ! supacloud_validate_binary "$candidate" "gh-linux-${arch}"; then\n rm -rf "$extracted_dir"\n return 1\n fi\n chmod 0755 "$candidate"\n version_output=$("$candidate" --version 2>/dev/null | head -1) || {\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap binary failed its version check" >&2\n return 1\n }\n if [[ "$version_output" != "gh version ${version}"* ]]; then\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap version mismatch: ${version_output}" >&2\n return 1\n fi\n mkdir -p "$(dirname "$target")"\n install -m 0755 "$candidate" "$target"\n rm -rf "$extracted_dir"\n}\n\nsupacloud_install_pinned_gh() {\n local target="${1:-/usr/local/bin/gh}"\n local machine arch expected_sha256 asset url archive\n machine=$(uname -m)\n case "$machine" in\n x86_64|amd64)\n arch="amd64"\n expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"\n ;;\n aarch64|arm64)\n arch="arm64"\n expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"\n ;;\n *)\n echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2\n return 1\n ;;\n esac\n asset="gh_${SUPACLOUD_GH_VERSION}_linux_${arch}.tar.gz"\n url="https://github.com/cli/cli/releases/download/v${SUPACLOUD_GH_VERSION}/${asset}"\n archive=$(mktemp)\n if ! supacloud_download_url "$url" "$archive" \\\n || ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then\n rm -f "$archive"\n return 1\n fi\n rm -f "$archive"\n}\n\nsupacloud_validate_tar() {\n local artifact_file="$1"\n local entries\n entries=$(tar -tzf "$artifact_file") || {\n echo "Web Console archive is not a readable gzip tarball" >&2\n return 1\n }\n if ! printf \'%s\\n\' "$entries" | awk \'\n /^\\// { exit 1 }\n /(^|\\/)\\.\\.($|\\/)/ { exit 1 }\n \'; then\n echo "Web Console archive contains an unsafe path" >&2\n return 1\n fi\n if ! tar -tvzf "$artifact_file" | awk \'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }\'; then\n echo "Web Console archive contains links or special files" >&2\n return 1\n fi\n printf \'%s\\n\' "$entries" | grep -Eq \'(^|/)index\\.html$\' || {\n echo "Web Console archive is invalid or does not contain index.html" >&2\n return 1\n }\n}\n\nsupacloud_record_integrity_mode() {\n local mode="$1"\n local record_file="${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"\n mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0\n printf \'%s\\n\' "$mode" > "$record_file" 2>/dev/null || return 0\n chmod 600 "$record_file" 2>/dev/null || true\n}\n\nsupacloud_fetch_attestation_bundle() {\n local artifact_file="$1"\n local bundle_file="$2"\n local digest response\n digest=$(sha256sum "$artifact_file" | awk \'{print $1}\') || return 1\n [[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {\n echo "Unable to calculate the artifact digest for attestation lookup" >&2\n return 1\n }\n digest=$(printf \'%s\' "$digest" | tr \'[:upper:]\' \'[:lower:]\')\n response=$(supacloud_fetch_release_json \\\n "https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:${digest}") || {\n echo "Unable to download the public GitHub artifact attestation bundle" >&2\n return 1\n }\n if ! jq -ce \'\n .attestations\n | if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")\n then error("no valid attestation bundles returned")\n else .[].bundle\n end\n \' <<< "$response" > "$bundle_file"; then\n echo "GitHub artifact attestation response did not contain a valid bundle" >&2\n return 1\n fi\n}\n\nsupacloud_attestation_trusted_root_available() {\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n local actual_size actual_sha256\n [[ "$trusted_root" == /* && -f "$trusted_root" && ! -L "$trusted_root" ]] || return 1\n actual_size=$(wc -c < "$trusted_root" | tr -d \'[:space:]\') || return 1\n [[ "$actual_size" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n actual_sha256=$(sha256sum "$trusted_root" | awk \'{print $1}\') || return 1\n [[ "$actual_sha256" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]] || return 1\n [[ "$(wc -l < "$trusted_root" | tr -d \'[:space:]\')" == "1" ]] || return 1\n jq -e \'type == "object" and .mediaType == "application/vnd.dev.sigstore.trustedroot+json;version=0.1"\' \\\n "$trusted_root" >/dev/null 2>&1\n}\n\nsupacloud_prepare_attestation_trusted_root() {\n local destination="$1"\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n supacloud_attestation_trusted_root_available || {\n echo "Pinned Sigstore Public Good trusted root is missing or invalid" >&2\n return 1\n }\n jq -ce . "$trusted_root" > "$destination" || return 1\n chmod 600 "$destination"\n [[ "$(wc -c < "$destination" | tr -d \'[:space:]\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n [[ "$(sha256sum "$destination" | awk \'{print $1}\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]]\n}\n\nsupacloud_verify_attestation() (\n local artifact_file="$1"\n if supacloud_attestation_verifier_available; then\n local verification_output bundle_dir bundle_file trusted_root_file\n bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1\n trap \'rm -rf -- "$bundle_dir"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1\' HUP INT TERM\n bundle_file="${bundle_dir}/bundle.jsonl"\n trusted_root_file="${bundle_dir}/trusted_root.jsonl"\n if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then\n return 1\n fi\n supacloud_prepare_attestation_trusted_root "$trusted_root_file" || return 1\n if ! verification_output=$(gh attestation verify "$artifact_file" \\\n --bundle "$bundle_file" \\\n --custom-trusted-root "$trusted_root_file" \\\n --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\\n --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\\n --source-ref "refs/heads/main" \\\n --deny-self-hosted-runners 2>&1); then\n echo "GitHub artifact attestation verification failed: ${verification_output}" >&2\n return 1\n fi\n supacloud_record_integrity_mode "github-attestation+same-release-sha256"\n return\n fi\n\n if [[ "${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then\n echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2\n supacloud_record_integrity_mode "break-glass:same-release-sha256-only"\n return 0\n fi\n\n echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2\n return 1\n)\n\nsupacloud_attestation_verifier_available() {\n local version help\n supacloud_attestation_trusted_root_available || return 1\n command -v gh >/dev/null 2>&1 || return 1\n version=$(supacloud_gh_version)\n [[ -n "$version" ]] || return 1\n supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1\n help=$(gh attestation verify --help 2>&1) || return 1\n grep -Eq -- \'(^|[[:space:]])--bundle([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--signer-workflow([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--source-ref([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--custom-trusted-root([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)\' <<< "$help"\n}\n\nsupacloud_download_release_asset() (\n local release_json="$1"\n local asset_name="$2"\n local destination="$3"\n local asset_kind="$4"\n local asset_url checksum_url temporary_artifact temporary_checksums\n\n asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1\n checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1\n mkdir -p "$(dirname "$destination")"\n temporary_artifact=$(mktemp "${destination}.tmp.XXXXXX")\n temporary_checksums=$(mktemp "${destination}.SHA256SUMS.tmp.XXXXXX")\n trap \'rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"; exit 1\' HUP INT TERM\n\n if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\\n || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\\n || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n # Authenticate the digest before parsing archives or inspecting binaries.\n if ! supacloud_verify_attestation "$temporary_artifact"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n case "$asset_kind" in\n binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;\n tar) supacloud_validate_tar "$temporary_artifact" ;;\n *)\n echo "Unknown release asset kind: $asset_kind" >&2\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n ;;\n esac || {\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n }\n\n mv -f "$temporary_artifact" "$destination"\n temporary_artifact=""\n rm -f "$temporary_checksums"\n temporary_checksums=""\n)\n';
|
|
@@ -29043,12 +29302,13 @@ function platformVersionsToolResult(report) {
|
|
|
29043
29302
|
}
|
|
29044
29303
|
function registerSshTools(server, ssh) {
|
|
29045
29304
|
server.tool("ssh", `Server management via SSH. Available before & after SupaCloud installation.
|
|
29046
|
-
Actions: ping, setup, install, upgrade, versions, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
|
|
29305
|
+
Actions: ping, setup, install, upgrade, upgrade_status, versions, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
|
|
29047
29306
|
action: withDescription(stringEnum([
|
|
29048
29307
|
"ping",
|
|
29049
29308
|
"setup",
|
|
29050
29309
|
"install",
|
|
29051
29310
|
"upgrade",
|
|
29311
|
+
"upgrade_status",
|
|
29052
29312
|
"versions",
|
|
29053
29313
|
"diagnose",
|
|
29054
29314
|
"exec",
|
|
@@ -29068,6 +29328,7 @@ Actions: ping, setup, install, upgrade, versions, diagnose, exec, troubleshoot,
|
|
|
29068
29328
|
dashboard_password: optional(secretSchema("dashboard_password"), "[install] Console password"),
|
|
29069
29329
|
edge_runtime: optional(stringEnum(["bun"]), "[install] Runtime (default: bun)"),
|
|
29070
29330
|
storage_type: optional(stringEnum(["juicefs", "minio"]), "[install] Storage backend configurable through Admin"),
|
|
29331
|
+
transaction_id: optional(Type.String(), "[upgrade_status] UUID v4 transaction ID of a retained local-artifact upgrade"),
|
|
29071
29332
|
version: optional(Type.String(), "[upgrade] Specific version"),
|
|
29072
29333
|
edge_runtime_version: optional(Type.String(), "[upgrade] Exact independent Edge Runtime version"),
|
|
29073
29334
|
artifact_transport: optional(stringEnum(["local", "remote"]), "[upgrade] Download verified release assets locally or on the server (default: remote)"),
|
|
@@ -29226,6 +29487,14 @@ ${result.stderr.slice(-500)}`;
|
|
|
29226
29487
|
${upgradeExecution.stdout.slice(-300)}${edgeBoundary}`;
|
|
29227
29488
|
break;
|
|
29228
29489
|
}
|
|
29490
|
+
case "upgrade_status": {
|
|
29491
|
+
if (!args.transaction_id)
|
|
29492
|
+
throw new Error("'transaction_id' required");
|
|
29493
|
+
const transactionId = assertValidTransactionId(args.transaction_id);
|
|
29494
|
+
const projection = await inspectRemoteUpgradeStatus(ssh, transactionId);
|
|
29495
|
+
text = JSON.stringify(projection, null, 2);
|
|
29496
|
+
break;
|
|
29497
|
+
}
|
|
29229
29498
|
case "versions": {
|
|
29230
29499
|
return platformVersionsToolResult(await platformVersions(ssh));
|
|
29231
29500
|
}
|
|
@@ -32245,7 +32514,7 @@ Actions: list_releases, get_release, upload_release, activate_release`, {
|
|
|
32245
32514
|
// package.json
|
|
32246
32515
|
var package_default = {
|
|
32247
32516
|
name: "@supacloud/admin",
|
|
32248
|
-
version: "0.
|
|
32517
|
+
version: "0.16.0",
|
|
32249
32518
|
description: "Platform administration CLI for SupaCloud operators",
|
|
32250
32519
|
type: "module",
|
|
32251
32520
|
main: "./dist/index.js",
|