@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.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/index.js +462 -15
  3. 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
- if (!/^v?\d+\.\d+\.\d+$/.test(value)) {
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 '=== PostgreSQL ===' && (pg_isready 2>/dev/null && echo 'Running' || echo 'Not detected')",
27857
- "echo '=== Management API ===' && (curl -sf http://localhost:9090/health > /dev/null && echo 'Running' || echo 'Not running')"
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
- text = r.stdout || r.stderr;
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.7.12",
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.7.12",
3
+ "version": "0.8.0",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",