@supacloud/admin 0.7.12 → 0.8.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 +8 -0
- package/dist/index.js +462 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,11 +27,19 @@ Examples:
|
|
|
27
27
|
```bash
|
|
28
28
|
npx @supacloud/admin status
|
|
29
29
|
npx @supacloud/admin ssh ping
|
|
30
|
+
npx @supacloud/admin ssh versions
|
|
30
31
|
npx @supacloud/admin ssh diagnose
|
|
31
32
|
npx @supacloud/admin project create --name my-app
|
|
32
33
|
npx @supacloud/admin project list
|
|
33
34
|
```
|
|
34
35
|
|
|
36
|
+
`ssh versions` emits JSON with `schema_version: 1` and fixed
|
|
37
|
+
`management_api`, `edge_runtime`, `caddy`, and `web_console` component fields.
|
|
38
|
+
Each component reports `status` as `ok`, `unknown`, or `error`; a failed probe
|
|
39
|
+
never substitutes a guessed version. Binary evidence is bound to the active
|
|
40
|
+
systemd `ExecStart`, and Web Console evidence comes from its component marker
|
|
41
|
+
plus an explicit `tree_sha256` digest.
|
|
42
|
+
|
|
35
43
|
## Verified platform upgrades
|
|
36
44
|
|
|
37
45
|
Use one explicit Admin command to upgrade Management, Web Console, and an
|
package/dist/index.js
CHANGED
|
@@ -25702,8 +25702,10 @@ class HttpTransport {
|
|
|
25702
25702
|
}
|
|
25703
25703
|
|
|
25704
25704
|
// src/shared/tools/ssh-tools.ts
|
|
25705
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
25705
25706
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
25706
25707
|
import { basename as basename3, dirname, join as join2 } from "node:path";
|
|
25708
|
+
import { TextDecoder as TextDecoder2 } from "node:util";
|
|
25707
25709
|
|
|
25708
25710
|
// src/shared/releases/local-upgrade-transfer.ts
|
|
25709
25711
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -27280,6 +27282,92 @@ var SAFE_DB_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$/;
|
|
|
27280
27282
|
var MINIMUM_COMPONENT_UPGRADE_VERSION = "0.50.27";
|
|
27281
27283
|
var DIRECT_UPGRADE_SSH_TIMEOUT_MS = 720000;
|
|
27282
27284
|
var PROXIED_UPGRADE_SSH_TIMEOUT_MS = 1320000;
|
|
27285
|
+
var PLATFORM_PROBE_TIMEOUT_MS = 1e4;
|
|
27286
|
+
var PLATFORM_HASH_TIMEOUT_MS = 30000;
|
|
27287
|
+
var WEB_CONSOLE_PROBE_TIMEOUT_MS = 60000;
|
|
27288
|
+
var BINARY_VERSION_MAX_BYTES = 2048;
|
|
27289
|
+
var WEB_CONSOLE_MARKER_MAX_BYTES = 4096;
|
|
27290
|
+
var WEB_CONSOLE_CURRENT_DIR = "/opt/supacloud/web-console/current";
|
|
27291
|
+
var STABLE_SEMVER_CORE = "(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)";
|
|
27292
|
+
var EXACT_STABLE_SEMVER = new RegExp(`^(?:${STABLE_SEMVER_CORE})(?![\\s\\S])`);
|
|
27293
|
+
var SEMVER_CORE_TOKEN = /\d+\.\d+\.\d+/;
|
|
27294
|
+
var VERSION_TOKEN_SEPARATOR = /[\s"'()\[\]{}:,;=]+/;
|
|
27295
|
+
var BINARY_HASH_BEFORE_FAILED = 70;
|
|
27296
|
+
var BINARY_VERSION_FAILED = 71;
|
|
27297
|
+
var BINARY_HASH_AFTER_FAILED = 72;
|
|
27298
|
+
var PROBE_CHANGED_DURING_READ = 75;
|
|
27299
|
+
var WEB_CONSOLE_ROOT_MISSING = 43;
|
|
27300
|
+
var WEB_CONSOLE_MARKER_MISSING = 44;
|
|
27301
|
+
var WEB_CONSOLE_ROOT_INVALID = 64;
|
|
27302
|
+
var WEB_CONSOLE_MARKER_INVALID = 65;
|
|
27303
|
+
var WEB_CONSOLE_TREE_INVALID = 66;
|
|
27304
|
+
var WEB_CONSOLE_TREE_FAILED = 67;
|
|
27305
|
+
var BINARY_HASH_BEFORE_LABEL = "SUPACLOUD_BINARY_HASH_BEFORE=";
|
|
27306
|
+
var BINARY_VERSION_LABEL = "SUPACLOUD_BINARY_VERSION_BASE64=";
|
|
27307
|
+
var BINARY_HASH_AFTER_LABEL = "SUPACLOUD_BINARY_HASH_AFTER=";
|
|
27308
|
+
var WEB_ROOT_REAL_BEFORE_LABEL = "SUPACLOUD_WEB_ROOT_REAL_BEFORE=";
|
|
27309
|
+
var WEB_ROOT_ID_BEFORE_LABEL = "SUPACLOUD_WEB_ROOT_ID_BEFORE=";
|
|
27310
|
+
var WEB_MARKER_BEFORE_LABEL = "SUPACLOUD_WEB_MARKER_BASE64_BEFORE=";
|
|
27311
|
+
var WEB_TREE_BEFORE_LABEL = "SUPACLOUD_WEB_TREE_SHA256_BEFORE=";
|
|
27312
|
+
var WEB_TREE_AFTER_LABEL = "SUPACLOUD_WEB_TREE_SHA256_AFTER=";
|
|
27313
|
+
var WEB_MARKER_AFTER_LABEL = "SUPACLOUD_WEB_MARKER_BASE64_AFTER=";
|
|
27314
|
+
var WEB_ROOT_REAL_AFTER_LABEL = "SUPACLOUD_WEB_ROOT_REAL_AFTER=";
|
|
27315
|
+
var WEB_ROOT_ID_AFTER_LABEL = "SUPACLOUD_WEB_ROOT_ID_AFTER=";
|
|
27316
|
+
function fixedBinaryProbeCommand(executablePath, versionArgument) {
|
|
27317
|
+
const executable = quoteEnvValue(executablePath);
|
|
27318
|
+
return [
|
|
27319
|
+
"set -o pipefail",
|
|
27320
|
+
`if exec {BINARY_FD}<${executable}; then :; else exit ${BINARY_HASH_BEFORE_FAILED}; fi`,
|
|
27321
|
+
"PINNED_EXECUTABLE=/proc/$$/fd/$BINARY_FD",
|
|
27322
|
+
`if PINNED_ID=$(stat -Lc '%d:%i' -- "$PINNED_EXECUTABLE"); then :; else exit ${BINARY_HASH_BEFORE_FAILED}; fi`,
|
|
27323
|
+
`if HASH_BEFORE=$(sha256sum -- "$PINNED_EXECUTABLE" | awk '{print $1}'); then :; else exit ${BINARY_HASH_BEFORE_FAILED}; fi`,
|
|
27324
|
+
`printf '${BINARY_HASH_BEFORE_LABEL}%s\\n' "$HASH_BEFORE"`,
|
|
27325
|
+
`if VERSION_BASE64=$( (exec -a ${executable} "$PINNED_EXECUTABLE" ${versionArgument} {BINARY_FD}<&-) 2>&1 | head -c ${BINARY_VERSION_MAX_BYTES + 1} | base64 | tr -d '\\n'); then :; else exit ${BINARY_VERSION_FAILED}; fi`,
|
|
27326
|
+
`printf '${BINARY_VERSION_LABEL}%s\\n' "$VERSION_BASE64"`,
|
|
27327
|
+
`if HASH_AFTER=$(sha256sum -- "$PINNED_EXECUTABLE" | awk '{print $1}'); then :; else exit ${BINARY_HASH_AFTER_FAILED}; fi`,
|
|
27328
|
+
`printf '${BINARY_HASH_AFTER_LABEL}%s\\n' "$HASH_AFTER"`,
|
|
27329
|
+
`[ "$HASH_BEFORE" = "$HASH_AFTER" ] || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27330
|
+
`if CURRENT_ID=$(stat -Lc '%d:%i' -- ${executable}); then :; else exit ${PROBE_CHANGED_DURING_READ}; fi`,
|
|
27331
|
+
`[ "$PINNED_ID" = "$CURRENT_ID" ] || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27332
|
+
"exec {BINARY_FD}<&-"
|
|
27333
|
+
].join(`
|
|
27334
|
+
`);
|
|
27335
|
+
}
|
|
27336
|
+
var BINARY_PROBES = {
|
|
27337
|
+
management_api: {
|
|
27338
|
+
unit: "supacloud.service",
|
|
27339
|
+
source: "systemd:supacloud.service:ExecStart",
|
|
27340
|
+
commands: {
|
|
27341
|
+
"/usr/local/bin/supacloud": {
|
|
27342
|
+
command: fixedBinaryProbeCommand("/usr/local/bin/supacloud", "--version")
|
|
27343
|
+
},
|
|
27344
|
+
"/opt/supacloud/bin/supacloud": {
|
|
27345
|
+
command: fixedBinaryProbeCommand("/opt/supacloud/bin/supacloud", "--version")
|
|
27346
|
+
}
|
|
27347
|
+
}
|
|
27348
|
+
},
|
|
27349
|
+
edge_runtime: {
|
|
27350
|
+
unit: "supacloud-edge-runtime.service",
|
|
27351
|
+
source: "systemd:supacloud-edge-runtime.service:ExecStart",
|
|
27352
|
+
commands: {
|
|
27353
|
+
"/usr/local/bin/supacloud-edge-runtime": {
|
|
27354
|
+
command: fixedBinaryProbeCommand("/usr/local/bin/supacloud-edge-runtime", "--version")
|
|
27355
|
+
},
|
|
27356
|
+
"/opt/supacloud/bin/supacloud-edge-runtime": {
|
|
27357
|
+
command: fixedBinaryProbeCommand("/opt/supacloud/bin/supacloud-edge-runtime", "--version")
|
|
27358
|
+
}
|
|
27359
|
+
}
|
|
27360
|
+
},
|
|
27361
|
+
caddy: {
|
|
27362
|
+
unit: "supacloud-caddy.service",
|
|
27363
|
+
source: "systemd:supacloud-caddy.service:ExecStart",
|
|
27364
|
+
commands: {
|
|
27365
|
+
"/usr/local/bin/supacloud-caddy": {
|
|
27366
|
+
command: fixedBinaryProbeCommand("/usr/local/bin/supacloud-caddy", "version")
|
|
27367
|
+
}
|
|
27368
|
+
}
|
|
27369
|
+
}
|
|
27370
|
+
};
|
|
27283
27371
|
function hostnameSchema(fieldName) {
|
|
27284
27372
|
return decodedSchema(Type.String(), Type.String({ minLength: 1, maxLength: 253 }), (value) => {
|
|
27285
27373
|
const normalized = value.trim();
|
|
@@ -27337,7 +27425,8 @@ function assertSafeReleaseTag(value) {
|
|
|
27337
27425
|
return value;
|
|
27338
27426
|
}
|
|
27339
27427
|
function assertExactStableVersion2(value, fieldName) {
|
|
27340
|
-
|
|
27428
|
+
const normalized = value.startsWith("v") ? value.slice(1) : value;
|
|
27429
|
+
if (!EXACT_STABLE_SEMVER.test(normalized)) {
|
|
27341
27430
|
throw new Error(`${fieldName} must be an exact stable semantic version`);
|
|
27342
27431
|
}
|
|
27343
27432
|
return value;
|
|
@@ -27657,14 +27746,366 @@ function assertSafeExecCommand(command) {
|
|
|
27657
27746
|
}
|
|
27658
27747
|
return reject();
|
|
27659
27748
|
}
|
|
27749
|
+
async function runFixedRemoteCommand(ssh, command, timeoutMs) {
|
|
27750
|
+
try {
|
|
27751
|
+
return await ssh.exec(command, timeoutMs);
|
|
27752
|
+
} catch {
|
|
27753
|
+
return null;
|
|
27754
|
+
}
|
|
27755
|
+
}
|
|
27756
|
+
function singleSystemdProperty(output, property) {
|
|
27757
|
+
const prefix = `${property}=`;
|
|
27758
|
+
const matches = output.split(/\r?\n/).filter((line) => line.startsWith(prefix));
|
|
27759
|
+
return matches.length === 1 ? matches[0].slice(prefix.length) : null;
|
|
27760
|
+
}
|
|
27761
|
+
function strictExecStartPath(execStart) {
|
|
27762
|
+
if (execStart.length > 4096 || /[\r\n\0]/.test(execStart))
|
|
27763
|
+
return null;
|
|
27764
|
+
const matches = Array.from(execStart.matchAll(/(?:^|[{;]\s*)path=([^;}]+?)\s*(?=;|}|$)/g));
|
|
27765
|
+
if (matches.length !== 1)
|
|
27766
|
+
return null;
|
|
27767
|
+
const executablePath = matches[0]?.[1]?.trim() || "";
|
|
27768
|
+
return /^\/[a-zA-Z0-9._/-]+$/.test(executablePath) ? executablePath : null;
|
|
27769
|
+
}
|
|
27770
|
+
function execStartEvidence(output) {
|
|
27771
|
+
if (output.length > 8192 || /\0/.test(output)) {
|
|
27772
|
+
return { status: "error", path: null, error: "systemd_output_invalid" };
|
|
27773
|
+
}
|
|
27774
|
+
if (!output.trim())
|
|
27775
|
+
return { status: "unknown", path: null, error: "unit_missing" };
|
|
27776
|
+
const loadState = singleSystemdProperty(output, "LoadState");
|
|
27777
|
+
const execStart = singleSystemdProperty(output, "ExecStart");
|
|
27778
|
+
if (loadState === null || execStart === null) {
|
|
27779
|
+
return { status: "error", path: null, error: "systemd_output_invalid" };
|
|
27780
|
+
}
|
|
27781
|
+
if (loadState !== "loaded")
|
|
27782
|
+
return { status: "unknown", path: null, error: "unit_not_loaded" };
|
|
27783
|
+
if (execStart === "")
|
|
27784
|
+
return { status: "unknown", path: null, error: "exec_start_missing" };
|
|
27785
|
+
const executablePath = strictExecStartPath(execStart);
|
|
27786
|
+
return executablePath ? { status: "ok", path: executablePath, error: null } : { status: "error", path: null, error: "exec_start_invalid" };
|
|
27787
|
+
}
|
|
27788
|
+
function stableVersion(output) {
|
|
27789
|
+
if (output.length > BINARY_VERSION_MAX_BYTES || /\0/.test(output))
|
|
27790
|
+
return null;
|
|
27791
|
+
const versionTokens = output.split(VERSION_TOKEN_SEPARATOR).filter((token) => SEMVER_CORE_TOKEN.test(token));
|
|
27792
|
+
const versions = versionTokens.map((token) => token.startsWith("v") ? token.slice(1) : token);
|
|
27793
|
+
if (versions.length === 0 || versions.some((version) => !EXACT_STABLE_SEMVER.test(version))) {
|
|
27794
|
+
return null;
|
|
27795
|
+
}
|
|
27796
|
+
const uniqueVersions = new Set(versions);
|
|
27797
|
+
return uniqueVersions.size === 1 ? [...uniqueVersions][0] : null;
|
|
27798
|
+
}
|
|
27799
|
+
function stableSha256(output) {
|
|
27800
|
+
return /^[0-9a-f]{64}$/.test(output) ? output : null;
|
|
27801
|
+
}
|
|
27802
|
+
function unavailableBinaryEvidence(definition, evidence) {
|
|
27803
|
+
return {
|
|
27804
|
+
status: evidence.status,
|
|
27805
|
+
version: null,
|
|
27806
|
+
sha256: null,
|
|
27807
|
+
path: evidence.path,
|
|
27808
|
+
source: definition.source,
|
|
27809
|
+
error: evidence.error
|
|
27810
|
+
};
|
|
27811
|
+
}
|
|
27812
|
+
function canonicalBase64Text(encoded, maximumBytes) {
|
|
27813
|
+
if (encoded.length > Math.ceil(maximumBytes / 3) * 4 || encoded.length % 4 !== 0)
|
|
27814
|
+
return null;
|
|
27815
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded))
|
|
27816
|
+
return null;
|
|
27817
|
+
const decodedBytes = Buffer2.from(encoded, "base64");
|
|
27818
|
+
if (decodedBytes.length > maximumBytes || decodedBytes.toString("base64") !== encoded)
|
|
27819
|
+
return null;
|
|
27820
|
+
try {
|
|
27821
|
+
return new TextDecoder2("utf-8", { fatal: true }).decode(decodedBytes);
|
|
27822
|
+
} catch {
|
|
27823
|
+
return null;
|
|
27824
|
+
}
|
|
27825
|
+
}
|
|
27826
|
+
function taggedProbeValue(line, label) {
|
|
27827
|
+
return line?.startsWith(label) ? line.slice(label.length) : null;
|
|
27828
|
+
}
|
|
27829
|
+
function binaryProbeFields(output) {
|
|
27830
|
+
if (output.length > 8192 || /[\r\0]/.test(output))
|
|
27831
|
+
return null;
|
|
27832
|
+
const lines = (output.endsWith(`
|
|
27833
|
+
`) ? output.slice(0, -1) : output).split(`
|
|
27834
|
+
`);
|
|
27835
|
+
if (lines.length < 2 || lines.length > 3)
|
|
27836
|
+
return null;
|
|
27837
|
+
const hashBefore = taggedProbeValue(lines[0], BINARY_HASH_BEFORE_LABEL);
|
|
27838
|
+
const versionBase64 = taggedProbeValue(lines[1], BINARY_VERSION_LABEL);
|
|
27839
|
+
const hashAfter = lines.length === 3 ? taggedProbeValue(lines[2], BINARY_HASH_AFTER_LABEL) : null;
|
|
27840
|
+
if (hashBefore === null || versionBase64 === null || lines.length === 3 && hashAfter === null)
|
|
27841
|
+
return null;
|
|
27842
|
+
const versionOutput = canonicalBase64Text(versionBase64, BINARY_VERSION_MAX_BYTES);
|
|
27843
|
+
return versionOutput === null ? null : { versionOutput, hashBefore, hashAfter };
|
|
27844
|
+
}
|
|
27845
|
+
function completedBinaryProbe(fields) {
|
|
27846
|
+
const version = stableVersion(fields.versionOutput);
|
|
27847
|
+
const hashBefore = stableSha256(fields.hashBefore);
|
|
27848
|
+
const hashAfter = fields.hashAfter === null ? null : stableSha256(fields.hashAfter);
|
|
27849
|
+
if (hashBefore && hashAfter && hashBefore !== hashAfter) {
|
|
27850
|
+
return { version: null, sha256: null, error: "binary_changed_during_probe" };
|
|
27851
|
+
}
|
|
27852
|
+
const sha2562 = hashBefore && hashAfter ? hashBefore : null;
|
|
27853
|
+
if (!version)
|
|
27854
|
+
return { version: null, sha256: sha2562, error: "version_output_invalid" };
|
|
27855
|
+
return sha2562 ? { version, sha256: sha2562, error: null } : { version, sha256: null, error: "sha256_output_invalid" };
|
|
27856
|
+
}
|
|
27857
|
+
function reservedBinaryProbeFailure(execution, fields) {
|
|
27858
|
+
if (execution.code === PROBE_CHANGED_DURING_READ) {
|
|
27859
|
+
return { version: null, sha256: null, error: "binary_changed_during_probe" };
|
|
27860
|
+
}
|
|
27861
|
+
if (execution.code === BINARY_HASH_BEFORE_FAILED) {
|
|
27862
|
+
return { version: null, sha256: null, error: "sha256_probe_failed" };
|
|
27863
|
+
}
|
|
27864
|
+
if (execution.code === BINARY_VERSION_FAILED) {
|
|
27865
|
+
return { version: null, sha256: null, error: "version_probe_failed" };
|
|
27866
|
+
}
|
|
27867
|
+
if (execution.code === BINARY_HASH_AFTER_FAILED) {
|
|
27868
|
+
const version = fields ? stableVersion(fields.versionOutput) : null;
|
|
27869
|
+
return version ? { version, sha256: null, error: "sha256_probe_failed" } : { version: null, sha256: null, error: fields ? "version_output_invalid" : "binary_probe_output_invalid" };
|
|
27870
|
+
}
|
|
27871
|
+
return null;
|
|
27872
|
+
}
|
|
27873
|
+
function binaryProbeValues(execution) {
|
|
27874
|
+
if (!execution)
|
|
27875
|
+
return { version: null, sha256: null, error: "binary_probe_transport_failed" };
|
|
27876
|
+
const fields = binaryProbeFields(execution.stdout);
|
|
27877
|
+
const reservedFailure = reservedBinaryProbeFailure(execution, fields);
|
|
27878
|
+
if (reservedFailure)
|
|
27879
|
+
return reservedFailure;
|
|
27880
|
+
if (!execution.success)
|
|
27881
|
+
return { version: null, sha256: null, error: "binary_probe_failed" };
|
|
27882
|
+
return fields ? completedBinaryProbe(fields) : { version: null, sha256: null, error: "binary_probe_output_invalid" };
|
|
27883
|
+
}
|
|
27884
|
+
async function binaryComponentEvidence(ssh, definition) {
|
|
27885
|
+
const execStart = await systemdBinaryEvidence(ssh, definition);
|
|
27886
|
+
if (execStart.status !== "ok" || !execStart.path)
|
|
27887
|
+
return unavailableBinaryEvidence(definition, execStart);
|
|
27888
|
+
const fixedProbe = definition.commands[execStart.path];
|
|
27889
|
+
if (!fixedProbe)
|
|
27890
|
+
return unavailableBinaryEvidence(definition, {
|
|
27891
|
+
status: "error",
|
|
27892
|
+
path: execStart.path,
|
|
27893
|
+
error: "exec_start_not_allowed"
|
|
27894
|
+
});
|
|
27895
|
+
const evidence = await fixedBinaryEvidence(ssh, definition, execStart.path, fixedProbe);
|
|
27896
|
+
const verifiedExecStart = await systemdBinaryEvidence(ssh, definition);
|
|
27897
|
+
if (verifiedExecStart.status !== "ok") {
|
|
27898
|
+
return unavailableBinaryEvidence(definition, {
|
|
27899
|
+
status: "error",
|
|
27900
|
+
path: execStart.path,
|
|
27901
|
+
error: "exec_start_recheck_failed"
|
|
27902
|
+
});
|
|
27903
|
+
}
|
|
27904
|
+
return verifiedExecStart.path === execStart.path ? evidence : unavailableBinaryEvidence(definition, {
|
|
27905
|
+
status: "error",
|
|
27906
|
+
path: execStart.path,
|
|
27907
|
+
error: "exec_start_changed_during_probe"
|
|
27908
|
+
});
|
|
27909
|
+
}
|
|
27910
|
+
async function systemdBinaryEvidence(ssh, definition) {
|
|
27911
|
+
const systemdCommand = `systemctl show --property=LoadState --property=ExecStart -- ${definition.unit}`;
|
|
27912
|
+
const systemdExecution = await runFixedRemoteCommand(ssh, systemdCommand, PLATFORM_PROBE_TIMEOUT_MS);
|
|
27913
|
+
if (!systemdExecution)
|
|
27914
|
+
return { status: "error", path: null, error: "systemd_probe_transport_failed" };
|
|
27915
|
+
if (!systemdExecution.success)
|
|
27916
|
+
return { status: "error", path: null, error: "systemd_probe_failed" };
|
|
27917
|
+
return execStartEvidence(systemdExecution.stdout);
|
|
27918
|
+
}
|
|
27919
|
+
async function fixedBinaryEvidence(ssh, definition, executablePath, fixedProbe) {
|
|
27920
|
+
const execution = await runFixedRemoteCommand(ssh, fixedProbe.command, PLATFORM_HASH_TIMEOUT_MS);
|
|
27921
|
+
const probe = binaryProbeValues(execution);
|
|
27922
|
+
return {
|
|
27923
|
+
status: probe.error ? "error" : "ok",
|
|
27924
|
+
version: probe.version,
|
|
27925
|
+
sha256: probe.sha256,
|
|
27926
|
+
path: executablePath,
|
|
27927
|
+
source: definition.source,
|
|
27928
|
+
error: probe.error
|
|
27929
|
+
};
|
|
27930
|
+
}
|
|
27931
|
+
function parseWebConsoleVersion(markerOutput) {
|
|
27932
|
+
if (markerOutput.length > WEB_CONSOLE_MARKER_MAX_BYTES || /\0/.test(markerOutput))
|
|
27933
|
+
return null;
|
|
27934
|
+
try {
|
|
27935
|
+
const marker = JSON.parse(markerOutput);
|
|
27936
|
+
if (typeof marker !== "object" || marker === null || Array.isArray(marker))
|
|
27937
|
+
return null;
|
|
27938
|
+
const fields = marker;
|
|
27939
|
+
if (fields.schema_version !== 1 || fields.component !== "web-console")
|
|
27940
|
+
return null;
|
|
27941
|
+
return typeof fields.version === "string" && EXACT_STABLE_SEMVER.test(fields.version) ? fields.version : null;
|
|
27942
|
+
} catch {
|
|
27943
|
+
return null;
|
|
27944
|
+
}
|
|
27945
|
+
}
|
|
27946
|
+
var WEB_CONSOLE_PROBE_COMMAND = [
|
|
27947
|
+
"set -o pipefail",
|
|
27948
|
+
`ROOT=${quoteEnvValue(WEB_CONSOLE_CURRENT_DIR)}`,
|
|
27949
|
+
'if [ ! -e "$ROOT" ] && [ ! -L "$ROOT" ]; then exit 43; fi',
|
|
27950
|
+
`test -d "$ROOT" || exit ${WEB_CONSOLE_ROOT_INVALID}`,
|
|
27951
|
+
`ROOT_REAL_BEFORE=$(readlink -f -- "$ROOT") || exit ${WEB_CONSOLE_ROOT_INVALID}`,
|
|
27952
|
+
`case "$ROOT_REAL_BEFORE" in "$ROOT"|/opt/supacloud/web-console/releases/*) ;; *) exit ${WEB_CONSOLE_ROOT_INVALID} ;; esac`,
|
|
27953
|
+
`ROOT_ID_BEFORE=$(stat -c '%d:%i' -- "$ROOT_REAL_BEFORE") || exit ${WEB_CONSOLE_ROOT_INVALID}`,
|
|
27954
|
+
"MARKER_BEFORE=$ROOT_REAL_BEFORE/.supacloud-component.json",
|
|
27955
|
+
`test ! -L "$MARKER_BEFORE" || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27956
|
+
`test -e "$MARKER_BEFORE" || exit ${WEB_CONSOLE_MARKER_MISSING}`,
|
|
27957
|
+
`test -f "$MARKER_BEFORE" || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27958
|
+
`test "$(stat -c '%s' -- "$MARKER_BEFORE")" -le ${WEB_CONSOLE_MARKER_MAX_BYTES} || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27959
|
+
`MARKER_BASE64_BEFORE=$(head -c ${WEB_CONSOLE_MARKER_MAX_BYTES + 1} -- "$MARKER_BEFORE" | base64 | tr -d '\\n') || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27960
|
+
"web_tree_sha256() {",
|
|
27961
|
+
" local TREE_ROOT INVALID_ENTRY TREE_DIGEST",
|
|
27962
|
+
" TREE_ROOT=$1",
|
|
27963
|
+
` INVALID_ENTRY=$(cd -- "$TREE_ROOT" && find . -xdev ! -type d ! -type f -print -quit) || return ${WEB_CONSOLE_TREE_FAILED}`,
|
|
27964
|
+
` test -z "$INVALID_ENTRY" || return ${WEB_CONSOLE_TREE_INVALID}`,
|
|
27965
|
+
` TREE_DIGEST=$(cd -- "$TREE_ROOT" && find . -xdev -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum -- | sha256sum | awk '{print $1}') || return ${WEB_CONSOLE_TREE_FAILED}`,
|
|
27966
|
+
` printf '%s' "$TREE_DIGEST"`,
|
|
27967
|
+
"}",
|
|
27968
|
+
'TREE_BEFORE=$(web_tree_sha256 "$ROOT_REAL_BEFORE")',
|
|
27969
|
+
"TREE_STATUS=$?",
|
|
27970
|
+
'test "$TREE_STATUS" -eq 0 || exit "$TREE_STATUS"',
|
|
27971
|
+
'TREE_AFTER=$(web_tree_sha256 "$ROOT_REAL_BEFORE")',
|
|
27972
|
+
"TREE_STATUS=$?",
|
|
27973
|
+
`test "$TREE_STATUS" -eq 0 || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27974
|
+
"MARKER_AFTER=$ROOT_REAL_BEFORE/.supacloud-component.json",
|
|
27975
|
+
`test ! -L "$MARKER_AFTER" && test -f "$MARKER_AFTER" || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27976
|
+
`test "$(stat -c '%s' -- "$MARKER_AFTER")" -le ${WEB_CONSOLE_MARKER_MAX_BYTES} || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27977
|
+
`MARKER_BASE64_AFTER=$(head -c ${WEB_CONSOLE_MARKER_MAX_BYTES + 1} -- "$MARKER_AFTER" | base64 | tr -d '\\n') || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27978
|
+
`test -d "$ROOT" || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27979
|
+
`ROOT_REAL_AFTER=$(readlink -f -- "$ROOT") || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27980
|
+
`ROOT_ID_AFTER=$(stat -c '%d:%i' -- "$ROOT_REAL_AFTER") || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27981
|
+
`printf '${WEB_ROOT_REAL_BEFORE_LABEL}%s\\n' "$ROOT_REAL_BEFORE"`,
|
|
27982
|
+
`printf '${WEB_ROOT_ID_BEFORE_LABEL}%s\\n' "$ROOT_ID_BEFORE"`,
|
|
27983
|
+
`printf '${WEB_MARKER_BEFORE_LABEL}%s\\n' "$MARKER_BASE64_BEFORE"`,
|
|
27984
|
+
`printf '${WEB_TREE_BEFORE_LABEL}%s\\n' "$TREE_BEFORE"`,
|
|
27985
|
+
`printf '${WEB_TREE_AFTER_LABEL}%s\\n' "$TREE_AFTER"`,
|
|
27986
|
+
`printf '${WEB_MARKER_AFTER_LABEL}%s\\n' "$MARKER_BASE64_AFTER"`,
|
|
27987
|
+
`printf '${WEB_ROOT_REAL_AFTER_LABEL}%s\\n' "$ROOT_REAL_AFTER"`,
|
|
27988
|
+
`printf '${WEB_ROOT_ID_AFTER_LABEL}%s\\n' "$ROOT_ID_AFTER"`
|
|
27989
|
+
].join(`
|
|
27990
|
+
`);
|
|
27991
|
+
var WEB_CONSOLE_PROBE_LABELS = [
|
|
27992
|
+
WEB_ROOT_REAL_BEFORE_LABEL,
|
|
27993
|
+
WEB_ROOT_ID_BEFORE_LABEL,
|
|
27994
|
+
WEB_MARKER_BEFORE_LABEL,
|
|
27995
|
+
WEB_TREE_BEFORE_LABEL,
|
|
27996
|
+
WEB_TREE_AFTER_LABEL,
|
|
27997
|
+
WEB_MARKER_AFTER_LABEL,
|
|
27998
|
+
WEB_ROOT_REAL_AFTER_LABEL,
|
|
27999
|
+
WEB_ROOT_ID_AFTER_LABEL
|
|
28000
|
+
];
|
|
28001
|
+
function taggedWebConsoleProbeValues(output) {
|
|
28002
|
+
if (output.length > 16384 || /[\r\0]/.test(output))
|
|
28003
|
+
return null;
|
|
28004
|
+
const lines = (output.endsWith(`
|
|
28005
|
+
`) ? output.slice(0, -1) : output).split(`
|
|
28006
|
+
`);
|
|
28007
|
+
if (lines.length !== WEB_CONSOLE_PROBE_LABELS.length)
|
|
28008
|
+
return null;
|
|
28009
|
+
const probeValues = WEB_CONSOLE_PROBE_LABELS.map((label, index) => taggedProbeValue(lines[index], label));
|
|
28010
|
+
return probeValues.some((probeValue) => probeValue === null) ? null : probeValues;
|
|
28011
|
+
}
|
|
28012
|
+
function webConsoleConsistencyError(probeValues) {
|
|
28013
|
+
const [rootBefore, rootIdBefore, markerBefore, treeBefore, treeAfter, markerAfter, rootAfter, rootIdAfter] = probeValues;
|
|
28014
|
+
if (rootBefore !== rootAfter || rootIdBefore !== rootIdAfter) {
|
|
28015
|
+
return "web_console_root_changed_during_probe";
|
|
28016
|
+
}
|
|
28017
|
+
if (markerBefore !== markerAfter)
|
|
28018
|
+
return "marker_changed_during_probe";
|
|
28019
|
+
return treeBefore !== treeAfter ? "tree_sha256_changed_during_probe" : null;
|
|
28020
|
+
}
|
|
28021
|
+
function webConsoleProbeFields(output) {
|
|
28022
|
+
const probeValues = taggedWebConsoleProbeValues(output);
|
|
28023
|
+
if (!probeValues)
|
|
28024
|
+
return null;
|
|
28025
|
+
const [rootBefore, rootIdBefore, markerBefore, treeBefore, , markerAfter, rootAfter, rootIdAfter] = probeValues;
|
|
28026
|
+
const supportedRoot = /^\/opt\/supacloud\/web-console\/(?:current|releases\/[A-Za-z0-9][A-Za-z0-9._-]*)$/;
|
|
28027
|
+
if (![rootBefore, rootAfter].every((root) => supportedRoot.test(root)))
|
|
28028
|
+
return null;
|
|
28029
|
+
if (![rootIdBefore, rootIdAfter].every((rootId) => /^\d+:\d+$/.test(rootId)))
|
|
28030
|
+
return null;
|
|
28031
|
+
const markerOutput = canonicalBase64Text(markerBefore, WEB_CONSOLE_MARKER_MAX_BYTES);
|
|
28032
|
+
if (markerOutput === null || canonicalBase64Text(markerAfter, WEB_CONSOLE_MARKER_MAX_BYTES) === null)
|
|
28033
|
+
return null;
|
|
28034
|
+
const consistencyError = webConsoleConsistencyError(probeValues);
|
|
28035
|
+
return {
|
|
28036
|
+
markerOutput,
|
|
28037
|
+
treeSha256: consistencyError ? null : stableSha256(treeBefore),
|
|
28038
|
+
consistencyError
|
|
28039
|
+
};
|
|
28040
|
+
}
|
|
28041
|
+
function webConsoleProbeError(execution, fields, version, treeSha256) {
|
|
28042
|
+
if (!execution)
|
|
28043
|
+
return { status: "error", error: "web_console_probe_transport_failed" };
|
|
28044
|
+
if (execution.code === WEB_CONSOLE_ROOT_MISSING)
|
|
28045
|
+
return { status: "unknown", error: "web_console_missing" };
|
|
28046
|
+
if (execution.code === WEB_CONSOLE_MARKER_MISSING)
|
|
28047
|
+
return { status: "unknown", error: "marker_missing" };
|
|
28048
|
+
if (execution.code === PROBE_CHANGED_DURING_READ) {
|
|
28049
|
+
return { status: "error", error: "web_console_changed_during_probe" };
|
|
28050
|
+
}
|
|
28051
|
+
if (execution.code === WEB_CONSOLE_ROOT_INVALID || execution.code === WEB_CONSOLE_TREE_INVALID) {
|
|
28052
|
+
return { status: "error", error: "web_console_invalid_file" };
|
|
28053
|
+
}
|
|
28054
|
+
if (execution.code === WEB_CONSOLE_MARKER_INVALID)
|
|
28055
|
+
return { status: "error", error: "marker_invalid" };
|
|
28056
|
+
if (execution.code === WEB_CONSOLE_TREE_FAILED)
|
|
28057
|
+
return { status: "error", error: "tree_sha256_probe_failed" };
|
|
28058
|
+
if (!execution.success)
|
|
28059
|
+
return { status: "error", error: "web_console_probe_failed" };
|
|
28060
|
+
if (!fields)
|
|
28061
|
+
return { status: "error", error: "web_console_probe_output_invalid" };
|
|
28062
|
+
if (fields.consistencyError)
|
|
28063
|
+
return { status: "error", error: fields.consistencyError };
|
|
28064
|
+
if (!version)
|
|
28065
|
+
return { status: "error", error: "marker_invalid" };
|
|
28066
|
+
return treeSha256 ? { status: "ok", error: null } : { status: "error", error: "tree_sha256_output_invalid" };
|
|
28067
|
+
}
|
|
28068
|
+
async function webConsoleEvidence(ssh) {
|
|
28069
|
+
const execution = await runFixedRemoteCommand(ssh, WEB_CONSOLE_PROBE_COMMAND, WEB_CONSOLE_PROBE_TIMEOUT_MS);
|
|
28070
|
+
const fields = execution?.success ? webConsoleProbeFields(execution.stdout) : null;
|
|
28071
|
+
const version = fields && !fields.consistencyError ? parseWebConsoleVersion(fields.markerOutput) : null;
|
|
28072
|
+
const treeSha256 = fields?.treeSha256 ?? null;
|
|
28073
|
+
const state = webConsoleProbeError(execution, fields, version, treeSha256);
|
|
28074
|
+
return {
|
|
28075
|
+
status: state.status,
|
|
28076
|
+
version,
|
|
28077
|
+
tree_sha256: treeSha256,
|
|
28078
|
+
path: WEB_CONSOLE_CURRENT_DIR,
|
|
28079
|
+
source: "component_marker_and_tree_sha256",
|
|
28080
|
+
error: state.error
|
|
28081
|
+
};
|
|
28082
|
+
}
|
|
28083
|
+
async function platformVersions(ssh) {
|
|
28084
|
+
const [managementApi, edgeRuntime, caddy, webConsole] = await Promise.all([
|
|
28085
|
+
binaryComponentEvidence(ssh, BINARY_PROBES.management_api),
|
|
28086
|
+
binaryComponentEvidence(ssh, BINARY_PROBES.edge_runtime),
|
|
28087
|
+
binaryComponentEvidence(ssh, BINARY_PROBES.caddy),
|
|
28088
|
+
webConsoleEvidence(ssh)
|
|
28089
|
+
]);
|
|
28090
|
+
return {
|
|
28091
|
+
schema_version: 1,
|
|
28092
|
+
components: {
|
|
28093
|
+
management_api: managementApi,
|
|
28094
|
+
edge_runtime: edgeRuntime,
|
|
28095
|
+
caddy,
|
|
28096
|
+
web_console: webConsole
|
|
28097
|
+
}
|
|
28098
|
+
};
|
|
28099
|
+
}
|
|
27660
28100
|
function registerSshTools(server, ssh) {
|
|
27661
28101
|
server.tool("ssh", `Server management via SSH. Available before & after SupaCloud installation.
|
|
27662
|
-
Actions: ping, setup, install, upgrade, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
|
|
28102
|
+
Actions: ping, setup, install, upgrade, versions, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
|
|
27663
28103
|
action: withDescription(stringEnum([
|
|
27664
28104
|
"ping",
|
|
27665
28105
|
"setup",
|
|
27666
28106
|
"install",
|
|
27667
28107
|
"upgrade",
|
|
28108
|
+
"versions",
|
|
27668
28109
|
"diagnose",
|
|
27669
28110
|
"exec",
|
|
27670
28111
|
"troubleshoot",
|
|
@@ -27806,22 +28247,16 @@ ${result.stderr.slice(-500)}`;
|
|
|
27806
28247
|
break;
|
|
27807
28248
|
}
|
|
27808
28249
|
case "upgrade": {
|
|
27809
|
-
const version = args.version ? assertSafeReleaseTag(args.version) : undefined;
|
|
27810
|
-
const edgeRuntimeVersion = args.edge_runtime_version ? assertSafeReleaseTag(args.edge_runtime_version) : undefined;
|
|
28250
|
+
const version = args.version ? assertExactStableVersion2(assertSafeReleaseTag(args.version), "version") : undefined;
|
|
28251
|
+
const edgeRuntimeVersion = args.edge_runtime_version ? assertExactStableVersion2(assertSafeReleaseTag(args.edge_runtime_version), "edge_runtime_version") : undefined;
|
|
27811
28252
|
if (edgeRuntimeVersion && !version) {
|
|
27812
28253
|
throw new Error("'version' is required with 'edge_runtime_version'");
|
|
27813
28254
|
}
|
|
27814
|
-
if (edgeRuntimeVersion && version) {
|
|
27815
|
-
assertExactStableVersion2(version, "version");
|
|
27816
|
-
assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
|
|
27817
|
-
}
|
|
27818
28255
|
const validatedProxy = args.github_proxy ? assertSafeGithubProxy(args.github_proxy) : undefined;
|
|
27819
28256
|
if (args.artifact_transport === "local") {
|
|
27820
28257
|
if (!version || !edgeRuntimeVersion) {
|
|
27821
28258
|
throw new Error("Local artifact transport requires exact 'version' and 'edge_runtime_version'");
|
|
27822
28259
|
}
|
|
27823
|
-
assertExactStableVersion2(version, "version");
|
|
27824
|
-
assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
|
|
27825
28260
|
if (validatedProxy && !["direct", "none"].includes(validatedProxy.toLowerCase())) {
|
|
27826
28261
|
throw new Error("Local artifact transport only supports direct GitHub downloads");
|
|
27827
28262
|
}
|
|
@@ -27847,17 +28282,27 @@ ${result.stderr.slice(-500)}`;
|
|
|
27847
28282
|
${upgradeExecution.stdout.slice(-300)}${edgeBoundary}`;
|
|
27848
28283
|
break;
|
|
27849
28284
|
}
|
|
28285
|
+
case "versions": {
|
|
28286
|
+
text = JSON.stringify(await platformVersions(ssh), null, 2);
|
|
28287
|
+
break;
|
|
28288
|
+
}
|
|
27850
28289
|
case "diagnose": {
|
|
27851
28290
|
const cmds = [
|
|
28291
|
+
"set -o pipefail",
|
|
27852
28292
|
"echo '=== OS ===' && uname -a",
|
|
27853
28293
|
"echo '=== Memory ===' && free -h",
|
|
27854
28294
|
"echo '=== Disk ===' && df -h /",
|
|
27855
28295
|
"echo '=== Docker ===' && (docker ps --format 'table {{.Names}}\t{{.Status}}' 2>/dev/null || echo 'Not found')",
|
|
27856
|
-
"echo '===
|
|
27857
|
-
"
|
|
28296
|
+
"echo '=== Management API /health ==='",
|
|
28297
|
+
"(curl --fail --silent --show-error --max-time 5 --max-filesize 4096 http://127.0.0.1:9090/health 2>&1 | head -c 4096) || echo 'Probe failed'",
|
|
28298
|
+
"printf '\\n'",
|
|
28299
|
+
"echo '=== Management API /monitor/health ==='",
|
|
28300
|
+
"(curl --fail --silent --show-error --max-time 15 --max-filesize 16384 http://127.0.0.1:9090/monitor/health 2>&1 | head -c 16384) || echo 'Probe failed'",
|
|
28301
|
+
"printf '\\n'"
|
|
27858
28302
|
];
|
|
27859
|
-
const r = await ssh.exec(cmds.join(
|
|
27860
|
-
|
|
28303
|
+
const r = await ssh.exec(cmds.join(`
|
|
28304
|
+
`), 30000);
|
|
28305
|
+
text = redactTenantConfig((r.stdout || r.stderr).slice(0, 32768));
|
|
27861
28306
|
break;
|
|
27862
28307
|
}
|
|
27863
28308
|
case "exec": {
|
|
@@ -28692,7 +29137,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
28692
29137
|
// package.json
|
|
28693
29138
|
var package_default = {
|
|
28694
29139
|
name: "@supacloud/admin",
|
|
28695
|
-
version: "0.
|
|
29140
|
+
version: "0.8.0",
|
|
28696
29141
|
description: "Platform administration CLI for SupaCloud operators",
|
|
28697
29142
|
type: "module",
|
|
28698
29143
|
main: "./dist/index.js",
|
|
@@ -28762,6 +29207,7 @@ var sshActionSchema = stringEnum([
|
|
|
28762
29207
|
"setup",
|
|
28763
29208
|
"install",
|
|
28764
29209
|
"upgrade",
|
|
29210
|
+
"versions",
|
|
28765
29211
|
"diagnose",
|
|
28766
29212
|
"exec",
|
|
28767
29213
|
"troubleshoot",
|
|
@@ -28816,6 +29262,7 @@ EXAMPLES
|
|
|
28816
29262
|
|
|
28817
29263
|
supacloud-admin status
|
|
28818
29264
|
supacloud-admin ssh ping
|
|
29265
|
+
supacloud-admin ssh versions
|
|
28819
29266
|
supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
|
|
28820
29267
|
supacloud-admin project create --name my-app
|
|
28821
29268
|
supacloud-admin project list
|