@supacloud/admin 0.7.11 → 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 +15 -5
  2. package/dist/index.js +706 -551
  3. package/package.json +1 -1
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
- import { dirname } from "node:path";
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";
@@ -25735,10 +25737,10 @@ function quoteShell(shellText) {
25735
25737
 
25736
25738
  // src/shared/releases/local-upgrade-bundle.ts
25737
25739
  import { spawn } from "node:child_process";
25738
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
25739
- import { accessSync, chmodSync, constants as fsConstants, createWriteStream, mkdirSync, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync, writeFileSync } from "node:fs";
25740
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
25741
+ import { accessSync, chmodSync as chmodSync2, constants as fsConstants, createWriteStream, lstatSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
25740
25742
  import { get } from "node:https";
25741
- import { tmpdir } from "node:os";
25743
+ import { tmpdir as tmpdir2 } from "node:os";
25742
25744
  import { basename, delimiter, join } from "node:path";
25743
25745
  import { pipeline } from "node:stream/promises";
25744
25746
  import { Transform as Transform2 } from "node:stream";
@@ -25905,6 +25907,97 @@ function parseReleaseChecksums(text, manifest) {
25905
25907
  return checksums;
25906
25908
  }
25907
25909
 
25910
+ // ../management-api/src/sigstore-trusted-root.ts
25911
+ import { createHash } from "node:crypto";
25912
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
25913
+ import { tmpdir } from "node:os";
25914
+ import path from "node:path";
25915
+
25916
+ // ../management-api/src/assets/sigstore-public-good-trusted-root.jsonl
25917
+ var sigstore_public_good_trusted_root_default = '{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1","tlogs":[{"baseUrl":"https://rekor.sigstore.dev","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-01-12T11:53:27Z"}},"logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}},{"baseUrl":"https://log2025-1.rekor.sigstore.dev","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=","keyDetails":"PKIX_ED25519","validFor":{"start":"2025-09-23T00:00:00Z"}},"logId":{"keyId":"zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A="}}],"certificateAuthorities":[{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="}]},"validFor":{"start":"2021-03-07T03:20:29Z","end":"2022-12-31T23:59:59.999Z"}},{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="},{"rawBytes":"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"}]},"validFor":{"start":"2022-04-13T20:06:15Z"}}],"ctlogs":[{"baseUrl":"https://ctfe.sigstore.dev/test","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-03-14T00:00:00Z","end":"2022-10-31T23:59:59.999Z"}},"logId":{"keyId":"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="}},{"baseUrl":"https://ctfe.sigstore.dev/2022","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2022-10-20T00:00:00Z"}},"logId":{"keyId":"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="}}],"timestampAuthorities":[{"subject":{"organization":"sigstore.dev","commonName":"sigstore-tsa-selfsigned"},"uri":"https://timestamp.sigstore.dev/api/v1/timestamp","certChain":{"certificates":[{"rawBytes":"MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw=="},{"rawBytes":"MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E"}]},"validFor":{"start":"2025-07-04T00:00:00Z"}}]}\n';
25918
+
25919
+ // ../management-api/src/sigstore-trusted-root.ts
25920
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME = "trusted_root.jsonl";
25921
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256 = "3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1";
25922
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE = 5748;
25923
+ function sha256(text) {
25924
+ return createHash("sha256").update(text).digest("hex");
25925
+ }
25926
+ function validateEmbeddedTrustedRoot(jsonl) {
25927
+ if (Buffer.byteLength(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
25928
+ throw new Error("Embedded Sigstore Public Good trusted root does not match its pinned digest");
25929
+ }
25930
+ if (!jsonl.endsWith(`
25931
+ `) || jsonl.slice(0, -1).includes(`
25932
+ `)) {
25933
+ throw new Error("Embedded Sigstore Public Good trusted root must be compact JSONL");
25934
+ }
25935
+ let trustedRoot;
25936
+ try {
25937
+ trustedRoot = JSON.parse(jsonl);
25938
+ } catch {
25939
+ throw new Error("Embedded Sigstore Public Good trusted root is not valid JSON");
25940
+ }
25941
+ if (!trustedRoot || typeof trustedRoot !== "object" || Array.isArray(trustedRoot) || `${JSON.stringify(trustedRoot)}
25942
+ ` !== jsonl) {
25943
+ throw new Error("Embedded Sigstore Public Good trusted root is not canonical compact JSONL");
25944
+ }
25945
+ return jsonl;
25946
+ }
25947
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL = validateEmbeddedTrustedRoot(sigstore_public_good_trusted_root_default);
25948
+ function createSigstoreVerificationDirectory() {
25949
+ const directory = mkdtempSync(path.join(tmpdir(), "supacloud-sigstore-"));
25950
+ try {
25951
+ chmodSync(directory, 448);
25952
+ const trustedRootPath = path.join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
25953
+ writeFileSync(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, {
25954
+ flag: "wx",
25955
+ mode: 384
25956
+ });
25957
+ chmodSync(trustedRootPath, 384);
25958
+ return { directory, trustedRootPath };
25959
+ } catch (operationError) {
25960
+ try {
25961
+ rmSync(directory, { recursive: true });
25962
+ } catch (cleanupError) {
25963
+ throw new AggregateError([operationError, cleanupError], "Sigstore trusted root creation failed and temporary directory cleanup did not complete");
25964
+ }
25965
+ throw operationError;
25966
+ }
25967
+ }
25968
+ function throwVerificationDirectoryOutcome(outcome) {
25969
+ if (outcome.operationFailed && outcome.cleanupFailed) {
25970
+ throw new AggregateError([outcome.operationError, outcome.cleanupError], "Sigstore verification failed and temporary trusted root cleanup did not complete");
25971
+ }
25972
+ if (outcome.operationFailed)
25973
+ throw outcome.operationError;
25974
+ if (outcome.cleanupFailed) {
25975
+ throw new AggregateError([outcome.cleanupError], "Temporary Sigstore trusted root cleanup did not complete");
25976
+ }
25977
+ }
25978
+ async function withSigstoreVerificationDirectory(operation) {
25979
+ const resources = createSigstoreVerificationDirectory();
25980
+ let operationOutput;
25981
+ let operationFailed = false;
25982
+ let operationError;
25983
+ try {
25984
+ operationOutput = await operation(resources);
25985
+ } catch (error) {
25986
+ operationFailed = true;
25987
+ operationError = error;
25988
+ }
25989
+ let cleanupFailed = false;
25990
+ let cleanupError;
25991
+ try {
25992
+ rmSync(resources.directory, { recursive: true });
25993
+ } catch (error) {
25994
+ cleanupFailed = true;
25995
+ cleanupError = error;
25996
+ }
25997
+ throwVerificationDirectoryOutcome({ operationFailed, operationError, cleanupFailed, cleanupError });
25998
+ return operationOutput;
25999
+ }
26000
+
25908
26001
  // src/shared/releases/local-upgrade-bundle.ts
25909
26002
  var RELEASES_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/releases`;
25910
26003
  var ATTESTATIONS_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/attestations`;
@@ -25938,7 +26031,8 @@ var STRICT_GITHUB_CAPABILITY_FLAGS = [
25938
26031
  "--signer-workflow",
25939
26032
  "--source-ref",
25940
26033
  "--source-digest",
25941
- "--deny-self-hosted-runners"
26034
+ "--deny-self-hosted-runners",
26035
+ "--custom-trusted-root"
25942
26036
  ];
25943
26037
 
25944
26038
  class RetryableDownloadError extends Error {
@@ -25953,7 +26047,56 @@ function githubCliArchiveIdentity(architecture) {
25953
26047
  };
25954
26048
  }
25955
26049
  function sha256File(filePath) {
25956
- return createHash("sha256").update(readFileSync3(filePath)).digest("hex");
26050
+ return createHash2("sha256").update(readFileSync3(filePath)).digest("hex");
26051
+ }
26052
+ function effectiveLocalOwner() {
26053
+ if (process.platform === "win32")
26054
+ return;
26055
+ const uid = process.geteuid?.() ?? process.getuid?.();
26056
+ const gid = process.getegid?.() ?? process.getgid?.();
26057
+ if (uid === undefined || gid === undefined) {
26058
+ throw new Error("Local artifact transport requires POSIX ownership checks");
26059
+ }
26060
+ return { uid, gid };
26061
+ }
26062
+ function assertLocalOwner(uid, gid, label) {
26063
+ const owner = effectiveLocalOwner();
26064
+ if (!owner)
26065
+ return;
26066
+ if (uid !== owner.uid || gid !== owner.gid) {
26067
+ throw new Error(`${label} must be owned by the current effective user`);
26068
+ }
26069
+ }
26070
+ function assertLocalTrustedRootDirectory(directory) {
26071
+ const directoryStats = lstatSync(directory);
26072
+ if (!directoryStats.isDirectory() || directoryStats.isSymbolicLink()) {
26073
+ throw new Error("Local trusted-root directory must be a directory without links");
26074
+ }
26075
+ assertPrivateLocalDirectoryMode(directoryStats.mode, "Local trusted-root directory");
26076
+ assertLocalOwner(directoryStats.uid, directoryStats.gid, "Local trusted-root directory");
26077
+ const entries = readdirSync(directory);
26078
+ if (entries.length !== 1 || entries[0] !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME) {
26079
+ throw new Error("Local trusted-root directory does not match its strict file allowlist");
26080
+ }
26081
+ const trustedRootPath = join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
26082
+ assertLocalTrustedRootFile(trustedRootPath);
26083
+ return trustedRootPath;
26084
+ }
26085
+ function assertPrivateLocalDirectoryMode(mode, label) {
26086
+ if (process.platform !== "win32" && (mode & 4095) !== 448) {
26087
+ throw new Error(`${label} must use exact mode 0700 without special permission bits`);
26088
+ }
26089
+ }
26090
+ function assertLocalTrustedRootFile(trustedRootPath) {
26091
+ const stats = lstatSync(trustedRootPath);
26092
+ if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
26093
+ throw new Error("Local trusted root must be a direct regular file without links");
26094
+ }
26095
+ assertPrivateLocalFileMode(stats.mode, "Local trusted root");
26096
+ assertLocalOwner(stats.uid, stats.gid, "Local trusted root");
26097
+ if (stats.size !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256File(trustedRootPath) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
26098
+ throw new Error("Local trusted root does not match its pinned size and digest");
26099
+ }
25957
26100
  }
25958
26101
  function assertPrivateLocalFileMode(mode, label) {
25959
26102
  if (process.platform !== "win32" && (mode & 4095) !== 384) {
@@ -26055,10 +26198,10 @@ async function downloadDirect(url, destination, maxBytes) {
26055
26198
  const deadline = Date.now() + DOWNLOAD_TIMEOUT_MS;
26056
26199
  const retryFailures = [];
26057
26200
  for (let attempt = 1;attempt <= 3; attempt += 1) {
26058
- rmSync(destination, { force: true });
26201
+ rmSync2(destination, { force: true });
26059
26202
  try {
26060
26203
  await downloadHttpsResponse({ url: parsed, destination, maxBytes, redirects: 0, deadline });
26061
- chmodSync(destination, 384);
26204
+ chmodSync2(destination, 384);
26062
26205
  return;
26063
26206
  } catch (error) {
26064
26207
  if (!downloadCanRetry(error))
@@ -26214,13 +26357,13 @@ async function assertLocalGithubVerifier() {
26214
26357
  throw new Error("Local artifact transport requires a current gh attestation verifier");
26215
26358
  }
26216
26359
  }
26217
- async function verifyManifestAttestation(manifestPath, bundlePath, manifest) {
26218
- const verification = await runGithubCli([
26360
+ function githubAttestationVerificationArguments(request) {
26361
+ return [
26219
26362
  "attestation",
26220
26363
  "verify",
26221
- manifestPath,
26364
+ request.artifactPath,
26222
26365
  "--bundle",
26223
- bundlePath,
26366
+ request.bundlePath,
26224
26367
  "--repo",
26225
26368
  RELEASE_REPOSITORY,
26226
26369
  "--signer-workflow",
@@ -26228,9 +26371,14 @@ async function verifyManifestAttestation(manifestPath, bundlePath, manifest) {
26228
26371
  "--source-ref",
26229
26372
  RELEASE_SOURCE_REF,
26230
26373
  "--source-digest",
26231
- manifest.source.commit,
26232
- "--deny-self-hosted-runners"
26233
- ], GH_VERIFICATION_TIMEOUT_MS);
26374
+ request.manifest.source.commit,
26375
+ "--deny-self-hosted-runners",
26376
+ "--custom-trusted-root",
26377
+ request.trustedRootPath
26378
+ ];
26379
+ }
26380
+ async function verifyManifestAttestation(request) {
26381
+ const verification = await runGithubCli(githubAttestationVerificationArguments(request), GH_VERIFICATION_TIMEOUT_MS);
26234
26382
  if (verification.exitCode !== 0) {
26235
26383
  throw new Error(`gh attestation verify failed: ${verification.stderr.trim().slice(-1000) || verification.exitCode}`);
26236
26384
  }
@@ -26261,7 +26409,7 @@ async function downloadReleaseMetadata(component, version, directory) {
26261
26409
  await downloadDirect(`${RELEASES_API}/tags/${expectedTag}`, metadataPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
26262
26410
  return parseGithubReleaseMetadata(parseJsonFile(metadataPath, "GitHub release metadata"), expectedTag);
26263
26411
  } finally {
26264
- rmSync(metadataPath, { force: true });
26412
+ rmSync2(metadataPath, { force: true });
26265
26413
  }
26266
26414
  }
26267
26415
  async function downloadManifestAttestation(manifestPath, destination) {
@@ -26269,10 +26417,10 @@ async function downloadManifestAttestation(manifestPath, destination) {
26269
26417
  try {
26270
26418
  await downloadDirect(`${ATTESTATIONS_API}/sha256:${sha256File(manifestPath)}`, responsePath, RELEASE_BUNDLE_SIZE_LIMITS.attestation);
26271
26419
  const bundles = serializeAttestationBundles(parseJsonFile(responsePath, "GitHub attestation response"));
26272
- writeFileSync(destination, bundles, { mode: 384, flag: "wx" });
26273
- chmodSync(destination, 384);
26420
+ writeFileSync2(destination, bundles, { mode: 384, flag: "wx" });
26421
+ chmodSync2(destination, 384);
26274
26422
  } finally {
26275
- rmSync(responsePath, { force: true });
26423
+ rmSync2(responsePath, { force: true });
26276
26424
  }
26277
26425
  }
26278
26426
  async function downloadComponent(request) {
@@ -26282,7 +26430,12 @@ async function downloadComponent(request) {
26282
26430
  await downloadDirect(releaseAssetUrl(release, RELEASE_MANIFEST_NAME), manifestPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
26283
26431
  const manifest = parseReleaseManifest(readFileSync3(manifestPath, "utf8"), request);
26284
26432
  await downloadManifestAttestation(manifestPath, attestationPath);
26285
- await verifyManifestAttestation(manifestPath, attestationPath, manifest);
26433
+ await verifyManifestAttestation({
26434
+ artifactPath: manifestPath,
26435
+ bundlePath: attestationPath,
26436
+ manifest,
26437
+ trustedRootPath: request.trustedRootPath
26438
+ });
26286
26439
  const checksumsPath = directChildPath(request.destination, RELEASE_CHECKSUMS_NAME);
26287
26440
  await downloadDirect(releaseAssetUrl(release, RELEASE_CHECKSUMS_NAME), checksumsPath, RELEASE_BUNDLE_SIZE_LIMITS.checksums);
26288
26441
  assertSignedArtifact(checksumsPath, manifest);
@@ -26305,7 +26458,7 @@ async function downloadPinnedGithubCli(directory, architecture) {
26305
26458
  return localUpgradeFile(archivePath, `verifier/${identity.archiveName}`);
26306
26459
  }
26307
26460
  function cleanupLocalUpgradeBundle(bundle) {
26308
- rmSync(bundle.directory, { recursive: true, force: true });
26461
+ rmSync2(bundle.directory, { recursive: true, force: true });
26309
26462
  }
26310
26463
  function assertLocalUpgradeBundleSize(files) {
26311
26464
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
@@ -26342,15 +26495,15 @@ async function settleLocalBundleDownloads(downloads) {
26342
26495
  ];
26343
26496
  }
26344
26497
  function createLocalBundleLayout(request) {
26345
- const directory = mkdtempSync(join(tmpdir(), "supacloud-admin-upgrade-"));
26346
- chmodSync(directory, 448);
26498
+ const directory = mkdtempSync2(join(tmpdir2(), "supacloud-admin-upgrade-"));
26499
+ chmodSync2(directory, 448);
26347
26500
  const bundleDirectory = join(directory, "bundle");
26348
26501
  const managementDirectory = join(bundleDirectory, "management-api");
26349
26502
  const edgeDirectory = join(bundleDirectory, "edge-runtime");
26350
26503
  const verifierDirectory = join(directory, "verifier");
26351
26504
  for (const createdDirectory of [bundleDirectory, managementDirectory, edgeDirectory, verifierDirectory]) {
26352
26505
  mkdirSync(createdDirectory, { recursive: true, mode: 448 });
26353
- chmodSync(createdDirectory, 448);
26506
+ chmodSync2(createdDirectory, 448);
26354
26507
  }
26355
26508
  return {
26356
26509
  directory,
@@ -26361,20 +26514,22 @@ function createLocalBundleLayout(request) {
26361
26514
  edgeRuntimeBinaryName: `supacloud-edge-runtime-linux-${request.architecture}`
26362
26515
  };
26363
26516
  }
26364
- async function downloadLocalBundle(request, layout) {
26517
+ async function downloadLocalBundle(request, layout, trustedRootPath) {
26365
26518
  const { managementBinaryName, edgeRuntimeBinaryName } = layout;
26366
26519
  const [managementFiles, edgeFiles, verifierArchive] = await settleLocalBundleDownloads([
26367
26520
  downloadComponent({
26368
26521
  component: "management-api",
26369
26522
  version: request.managementVersion,
26370
26523
  assetNames: [managementBinaryName, "web-console-build.tar.gz"],
26371
- destination: layout.managementDirectory
26524
+ destination: layout.managementDirectory,
26525
+ trustedRootPath
26372
26526
  }),
26373
26527
  downloadComponent({
26374
26528
  component: "edge-runtime",
26375
26529
  version: request.edgeRuntimeVersion,
26376
26530
  assetNames: [edgeRuntimeBinaryName],
26377
- destination: layout.edgeDirectory
26531
+ destination: layout.edgeDirectory,
26532
+ trustedRootPath
26378
26533
  }),
26379
26534
  request.verifierProvisioning === "bundled" ? downloadPinnedGithubCli(layout.verifierDirectory, request.architecture) : Promise.resolve(null)
26380
26535
  ]);
@@ -26388,9 +26543,16 @@ async function prepareLocalUpgradeBundle(request) {
26388
26543
  await assertLocalGithubVerifier();
26389
26544
  const layout = createLocalBundleLayout(request);
26390
26545
  try {
26391
- return await downloadLocalBundle(request, layout);
26546
+ return await withSigstoreVerificationDirectory(async (verificationDirectory) => {
26547
+ const trustedRootPath = assertLocalTrustedRootDirectory(verificationDirectory.directory);
26548
+ return await downloadLocalBundle(request, layout, trustedRootPath);
26549
+ });
26392
26550
  } catch (error) {
26393
- rmSync(layout.directory, { recursive: true, force: true });
26551
+ try {
26552
+ rmSync2(layout.directory, { recursive: true, force: true });
26553
+ } catch (cleanupError) {
26554
+ throw new AggregateError([error, cleanupError], "Local bundle preparation failed and cleanup did not complete");
26555
+ }
26394
26556
  throw error;
26395
26557
  }
26396
26558
  }
@@ -26606,7 +26768,7 @@ function upgradeScriptSetup(paths) {
26606
26768
  "write_status RUNNING",
26607
26769
  buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
26608
26770
  `while IFS='=' read -r variable _; do case "$variable" in *PROXY*|*Proxy*|*proxy*) unset "$variable" ;; esac; done < <(env)`,
26609
- "unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW NODE_USE_ENV_PROXY"
26771
+ "unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_ATTESTATION_TRUSTED_ROOT NODE_USE_ENV_PROXY"
26610
26772
  ];
26611
26773
  }
26612
26774
  function upgradeScriptFilesystemChecks(bundle) {
@@ -27106,493 +27268,7 @@ async function executeLocalUpgradeTransfer(ssh, request) {
27106
27268
  }
27107
27269
 
27108
27270
  // ../../scripts/lib/release_assets.sh
27109
- var release_assets_default = `#!/usr/bin/env bash
27110
-
27111
- SUPACLOUD_GITHUB_REPOSITORY="\${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"
27112
- SUPACLOUD_RELEASES_API="\${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/releases}"
27113
- SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="\${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-\${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"
27114
- SUPACLOUD_GH_VERSION="\${SUPACLOUD_GH_VERSION:-2.96.0}"
27115
- SUPACLOUD_GH_MIN_VERSION="\${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"
27116
- SUPACLOUD_GH_AMD64_SHA256="\${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"
27117
- SUPACLOUD_GH_ARM64_SHA256="\${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"
27118
-
27119
- supacloud_curl_release_json() {
27120
- local url="$1"
27121
- local output="$2"
27122
- curl -fsSL --proto '=https' --proto-redir '=https' \\
27123
- --retry 1 --retry-delay 2 --retry-max-time 60 \\
27124
- --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\
27125
- -o "$output" "$url"
27126
- }
27127
-
27128
- supacloud_curl_release_asset() {
27129
- local url="$1"
27130
- local output="$2"
27131
- curl -fL --proto '=https' --proto-redir '=https' \\
27132
- --retry 1 --retry-delay 2 --retry-max-time 180 \\
27133
- --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\
27134
- -o "$output" "$url"
27135
- }
27136
-
27137
- supacloud_component_tag() {
27138
- local component="$1"
27139
- local version="$2"
27140
- case "$version" in
27141
- "\${component}-v"*) printf '%s' "$version" ;;
27142
- v*) printf '%s-%s' "$component" "$version" ;;
27143
- *) printf '%s-v%s' "$component" "$version" ;;
27144
- esac
27145
- }
27146
-
27147
- supacloud_select_release() {
27148
- local component="$1"
27149
- shift
27150
- local required_assets_json
27151
- [[ $# -gt 0 ]] || {
27152
- echo "at least one required release asset must be specified" >&2
27153
- return 1
27154
- }
27155
- required_assets_json=$(printf '%s\\n' "$@" | jq -Rsc 'split("\\n")[:-1]')
27156
- jq -ce --arg prefix "\${component}-v" --argjson required "$required_assets_json" '
27157
- map(select(
27158
- (.draft | not)
27159
- and (.prerelease | not)
27160
- and (.tag_name | startswith($prefix))
27161
- and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
27162
- and any(.assets[]?; .name == "SHA256SUMS")
27163
- ))
27164
- | first
27165
- // error("no matching component release contains all required assets and SHA256SUMS")
27166
- '
27167
- }
27168
-
27169
- supacloud_fetch_component_release() {
27170
- local component="$1"
27171
- local version="\${2:-latest}"
27172
- shift 2
27173
- local required_assets=("$@")
27174
- local required_assets_json
27175
- local response
27176
- [[ \${#required_assets[@]} -gt 0 ]] || {
27177
- echo "at least one required release asset must be specified" >&2
27178
- return 1
27179
- }
27180
- required_assets_json=$(printf '%s\\n' "\${required_assets[@]}" | jq -Rsc 'split("\\n")[:-1]')
27181
-
27182
- if [[ -n "$version" && "$version" != "latest" ]]; then
27183
- local tag
27184
- tag=$(supacloud_component_tag "$component" "$version")
27185
- response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}/tags/\${tag}") || return 1
27186
- jq -ce --argjson required "$required_assets_json" '
27187
- select(
27188
- (.draft | not)
27189
- and (.prerelease | not)
27190
- and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
27191
- and any(.assets[]?; .name == "SHA256SUMS")
27192
- )
27193
- // error("release does not contain all required assets and SHA256SUMS")
27194
- ' <<< "$response"
27195
- return
27196
- fi
27197
-
27198
- response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}?per_page=100") || return 1
27199
- supacloud_select_release "$component" "\${required_assets[@]}" <<< "$response"
27200
- }
27201
-
27202
- supacloud_fetch_release_json() (
27203
- local url="$1"
27204
- local response_file
27205
- response_file=$(mktemp) || return 1
27206
- trap 'rm -f "$response_file"' EXIT
27207
- trap 'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1' HUP INT TERM
27208
- supacloud_download_release_metadata_url "$url" "$response_file" || return 1
27209
- cat "$response_file"
27210
- )
27211
-
27212
- supacloud_release_asset_url() {
27213
- local release_json="$1"
27214
- local asset_name="$2"
27215
- jq -er --arg asset "$asset_name" '
27216
- first(.assets[]? | select(.name == $asset) | .browser_download_url)
27217
- // error("release asset URL is missing")
27218
- ' <<< "$release_json"
27219
- }
27220
-
27221
- supacloud_download_url() {
27222
- local url="$1"
27223
- local output="$2"
27224
- local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
27225
-
27226
- if supacloud_curl_release_asset "$url" "$output"; then
27227
- return 0
27228
- fi
27229
- if [[ -n "$proxy" ]]; then
27230
- supacloud_curl_release_asset "\${proxy%/}/\${url}" "$output"
27231
- return
27232
- fi
27233
- return 1
27234
- }
27235
-
27236
- supacloud_download_release_metadata_url() {
27237
- local url="$1"
27238
- local output="$2"
27239
- local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
27240
-
27241
- if supacloud_curl_release_json "$url" "$output"; then
27242
- return 0
27243
- fi
27244
- if [[ -n "$proxy" ]]; then
27245
- supacloud_curl_release_json "\${proxy%/}/\${url}" "$output"
27246
- return
27247
- fi
27248
- return 1
27249
- }
27250
-
27251
- supacloud_verify_checksum() {
27252
- local artifact_file="$1"
27253
- local asset_name="$2"
27254
- local checksum_file="$3"
27255
- local expected
27256
- expected=$(awk -v asset="$asset_name" '$2 == asset || $2 == "*" asset { print $1; exit }' "$checksum_file")
27257
- if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then
27258
- echo "SHA256SUMS does not contain a valid checksum for \${asset_name}" >&2
27259
- return 1
27260
- fi
27261
-
27262
- local actual
27263
- actual=$(sha256sum "$artifact_file" | awk '{print $1}')
27264
- actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
27265
- expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
27266
- if [[ "$actual" != "$expected" ]]; then
27267
- echo "SHA256 mismatch for \${asset_name}" >&2
27268
- return 1
27269
- fi
27270
- }
27271
-
27272
- supacloud_validate_binary() {
27273
- local artifact_file="$1"
27274
- local asset_name="$2"
27275
- local description
27276
- description=$(file -b "$artifact_file")
27277
- if [[ "$description" != *ELF* ]]; then
27278
- echo "\${asset_name} is not an ELF binary: \${description}" >&2
27279
- return 1
27280
- fi
27281
-
27282
- case "$asset_name" in
27283
- *amd64)
27284
- [[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {
27285
- echo "\${asset_name} does not contain an x86-64 ELF binary" >&2
27286
- return 1
27287
- }
27288
- ;;
27289
- *arm64)
27290
- [[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {
27291
- echo "\${asset_name} does not contain an arm64 ELF binary" >&2
27292
- return 1
27293
- }
27294
- ;;
27295
- esac
27296
- }
27297
-
27298
- supacloud_install_pinned_tar_xz_binary() (
27299
- local archive="$1"
27300
- local member="$2"
27301
- local expected_sha256="$3"
27302
- local arch="$4"
27303
- local target="$5"
27304
- local actual_sha256 member_count member_details extract_dir candidate staged_target
27305
-
27306
- actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
27307
- actual_sha256=$(printf '%s' "$actual_sha256" | tr '[:upper:]' '[:lower:]')
27308
- expected_sha256=$(printf '%s' "$expected_sha256" | tr '[:upper:]' '[:lower:]')
27309
- if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then
27310
- echo "SHA256 mismatch for pinned archive" >&2
27311
- return 1
27312
- fi
27313
-
27314
- member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)
27315
- if [[ "$member_count" != "1" ]]; then
27316
- echo "Pinned archive must contain the exact member once: $member" >&2
27317
- return 1
27318
- fi
27319
- member_details=$(tar -tvJf "$archive" "$member") || return 1
27320
- if [[ "\${member_details:0:1}" != "-" ]]; then
27321
- echo "Pinned archive member is not a regular file: $member" >&2
27322
- return 1
27323
- fi
27324
-
27325
- extract_dir=$(mktemp -d)
27326
- trap 'rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"' EXIT
27327
- trap 'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"; exit 1' HUP INT TERM
27328
- if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then
27329
- return 1
27330
- fi
27331
- candidate="\${extract_dir}/\${member}"
27332
- supacloud_validate_binary "$candidate" "pinned-linux-\${arch}" || return 1
27333
-
27334
- mkdir -p "$(dirname "$target")"
27335
- staged_target=$(mktemp "\${target}.tmp.XXXXXX")
27336
- install -m 0755 "$candidate" "$staged_target"
27337
- mv -f "$staged_target" "$target"
27338
- staged_target=""
27339
- )
27340
-
27341
- supacloud_version_at_least() {
27342
- local current="\${1#v}"
27343
- local required="\${2#v}"
27344
- local current_major=0 current_minor=0 current_patch=0
27345
- local required_major=0 required_minor=0 required_patch=0
27346
- [[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
27347
- [[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
27348
- IFS=. read -r current_major current_minor current_patch <<EOF
27349
- \${current%%-*}
27350
- EOF
27351
- IFS=. read -r required_major required_minor required_patch <<EOF
27352
- \${required%%-*}
27353
- EOF
27354
- current_major=\${current_major:-0}; current_minor=\${current_minor:-0}; current_patch=\${current_patch:-0}
27355
- required_major=\${required_major:-0}; required_minor=\${required_minor:-0}; required_patch=\${required_patch:-0}
27356
- if (( current_major != required_major )); then (( current_major > required_major )); return; fi
27357
- if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi
27358
- (( current_patch >= required_patch ))
27359
- }
27360
-
27361
- supacloud_gh_version() {
27362
- gh --version 2>/dev/null | awk 'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }'
27363
- }
27364
-
27365
- supacloud_install_gh_archive() {
27366
- local archive="$1"
27367
- local version="$2"
27368
- local arch="$3"
27369
- local expected_sha256="$4"
27370
- local target="$5"
27371
- local member="gh_\${version}_linux_\${arch}/bin/gh"
27372
- local actual_sha256 member_count member_details extracted_dir candidate version_output
27373
-
27374
- actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
27375
- if [[ "$actual_sha256" != "$expected_sha256" ]]; then
27376
- echo "GitHub CLI archive SHA256 mismatch" >&2
27377
- return 1
27378
- fi
27379
-
27380
- member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)
27381
- if [[ "$member_count" != "1" ]]; then
27382
- echo "GitHub CLI archive does not contain the exact expected member: $member" >&2
27383
- return 1
27384
- fi
27385
- member_details=$(tar -tvzf "$archive" "$member") || return 1
27386
- if [[ "\${member_details:0:1}" != "-" ]]; then
27387
- echo "GitHub CLI archive member is not a regular file: $member" >&2
27388
- return 1
27389
- fi
27390
-
27391
- extracted_dir=$(mktemp -d)
27392
- candidate="\${extracted_dir}/\${member}"
27393
- if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\
27394
- || ! supacloud_validate_binary "$candidate" "gh-linux-\${arch}"; then
27395
- rm -rf "$extracted_dir"
27396
- return 1
27397
- fi
27398
- chmod 0755 "$candidate"
27399
- version_output=$("$candidate" --version 2>/dev/null | head -1) || {
27400
- rm -rf "$extracted_dir"
27401
- echo "GitHub CLI bootstrap binary failed its version check" >&2
27402
- return 1
27403
- }
27404
- if [[ "$version_output" != "gh version \${version}"* ]]; then
27405
- rm -rf "$extracted_dir"
27406
- echo "GitHub CLI bootstrap version mismatch: \${version_output}" >&2
27407
- return 1
27408
- fi
27409
- mkdir -p "$(dirname "$target")"
27410
- install -m 0755 "$candidate" "$target"
27411
- rm -rf "$extracted_dir"
27412
- }
27413
-
27414
- supacloud_install_pinned_gh() {
27415
- local target="\${1:-/usr/local/bin/gh}"
27416
- local machine arch expected_sha256 asset url archive
27417
- machine=$(uname -m)
27418
- case "$machine" in
27419
- x86_64|amd64)
27420
- arch="amd64"
27421
- expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"
27422
- ;;
27423
- aarch64|arm64)
27424
- arch="arm64"
27425
- expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"
27426
- ;;
27427
- *)
27428
- echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2
27429
- return 1
27430
- ;;
27431
- esac
27432
- asset="gh_\${SUPACLOUD_GH_VERSION}_linux_\${arch}.tar.gz"
27433
- url="https://github.com/cli/cli/releases/download/v\${SUPACLOUD_GH_VERSION}/\${asset}"
27434
- archive=$(mktemp)
27435
- if ! supacloud_download_url "$url" "$archive" \\
27436
- || ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then
27437
- rm -f "$archive"
27438
- return 1
27439
- fi
27440
- rm -f "$archive"
27441
- }
27442
-
27443
- supacloud_validate_tar() {
27444
- local artifact_file="$1"
27445
- local entries
27446
- entries=$(tar -tzf "$artifact_file") || {
27447
- echo "Web Console archive is not a readable gzip tarball" >&2
27448
- return 1
27449
- }
27450
- if ! printf '%s\\n' "$entries" | awk '
27451
- /^\\// { exit 1 }
27452
- /(^|\\/)\\.\\.($|\\/)/ { exit 1 }
27453
- '; then
27454
- echo "Web Console archive contains an unsafe path" >&2
27455
- return 1
27456
- fi
27457
- if ! tar -tvzf "$artifact_file" | awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }'; then
27458
- echo "Web Console archive contains links or special files" >&2
27459
- return 1
27460
- fi
27461
- printf '%s\\n' "$entries" | grep -Eq '(^|/)index\\.html$' || {
27462
- echo "Web Console archive is invalid or does not contain index.html" >&2
27463
- return 1
27464
- }
27465
- }
27466
-
27467
- supacloud_record_integrity_mode() {
27468
- local mode="$1"
27469
- local record_file="\${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"
27470
- mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0
27471
- printf '%s\\n' "$mode" > "$record_file" 2>/dev/null || return 0
27472
- chmod 600 "$record_file" 2>/dev/null || true
27473
- }
27474
-
27475
- supacloud_fetch_attestation_bundle() {
27476
- local artifact_file="$1"
27477
- local bundle_file="$2"
27478
- local digest response
27479
- digest=$(sha256sum "$artifact_file" | awk '{print $1}') || return 1
27480
- [[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {
27481
- echo "Unable to calculate the artifact digest for attestation lookup" >&2
27482
- return 1
27483
- }
27484
- digest=$(printf '%s' "$digest" | tr '[:upper:]' '[:lower:]')
27485
- response=$(supacloud_fetch_release_json \\
27486
- "https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:\${digest}") || {
27487
- echo "Unable to download the public GitHub artifact attestation bundle" >&2
27488
- return 1
27489
- }
27490
- if ! jq -ce '
27491
- .attestations
27492
- | if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")
27493
- then error("no valid attestation bundles returned")
27494
- else .[].bundle
27495
- end
27496
- ' <<< "$response" > "$bundle_file"; then
27497
- echo "GitHub artifact attestation response did not contain a valid bundle" >&2
27498
- return 1
27499
- fi
27500
- }
27501
-
27502
- supacloud_verify_attestation() (
27503
- local artifact_file="$1"
27504
- if supacloud_attestation_verifier_available; then
27505
- local verification_output bundle_dir bundle_file
27506
- bundle_dir=$(mktemp -d "\${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1
27507
- trap 'rm -rf -- "$bundle_dir"' EXIT
27508
- trap 'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1' HUP INT TERM
27509
- bundle_file="\${bundle_dir}/bundle.jsonl"
27510
- if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then
27511
- return 1
27512
- fi
27513
- if ! verification_output=$(gh attestation verify "$artifact_file" \\
27514
- --bundle "$bundle_file" \\
27515
- --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\
27516
- --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\
27517
- --source-ref "refs/heads/main" \\
27518
- --deny-self-hosted-runners 2>&1); then
27519
- echo "GitHub artifact attestation verification failed: \${verification_output}" >&2
27520
- return 1
27521
- fi
27522
- supacloud_record_integrity_mode "github-attestation+same-release-sha256"
27523
- return
27524
- fi
27525
-
27526
- if [[ "\${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then
27527
- echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2
27528
- supacloud_record_integrity_mode "break-glass:same-release-sha256-only"
27529
- return 0
27530
- fi
27531
-
27532
- 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
27533
- return 1
27534
- )
27535
-
27536
- supacloud_attestation_verifier_available() {
27537
- local version help
27538
- command -v gh >/dev/null 2>&1 || return 1
27539
- version=$(supacloud_gh_version)
27540
- [[ -n "$version" ]] || return 1
27541
- supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1
27542
- help=$(gh attestation verify --help 2>&1) || return 1
27543
- grep -Eq -- '(^|[[:space:]])--bundle([=[:space:]]|$)' <<< "$help" || return 1
27544
- grep -Eq -- '(^|[[:space:]])--signer-workflow([=[:space:]]|$)' <<< "$help" || return 1
27545
- grep -Eq -- '(^|[[:space:]])--source-ref([=[:space:]]|$)' <<< "$help" || return 1
27546
- grep -Eq -- '(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)' <<< "$help"
27547
- }
27548
-
27549
- supacloud_download_release_asset() (
27550
- local release_json="$1"
27551
- local asset_name="$2"
27552
- local destination="$3"
27553
- local asset_kind="$4"
27554
- local asset_url checksum_url temporary_artifact temporary_checksums
27555
-
27556
- asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1
27557
- checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1
27558
- mkdir -p "$(dirname "$destination")"
27559
- temporary_artifact=$(mktemp "\${destination}.tmp.XXXXXX")
27560
- temporary_checksums=$(mktemp "\${destination}.SHA256SUMS.tmp.XXXXXX")
27561
- trap 'rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"' EXIT
27562
- trap 'trap - EXIT HUP INT TERM; rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"; exit 1' HUP INT TERM
27563
-
27564
- if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\
27565
- || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\
27566
- || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then
27567
- rm -f "$temporary_artifact" "$temporary_checksums"
27568
- return 1
27569
- fi
27570
-
27571
- # Authenticate the digest before parsing archives or inspecting binaries.
27572
- if ! supacloud_verify_attestation "$temporary_artifact"; then
27573
- rm -f "$temporary_artifact" "$temporary_checksums"
27574
- return 1
27575
- fi
27576
-
27577
- case "$asset_kind" in
27578
- binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;
27579
- tar) supacloud_validate_tar "$temporary_artifact" ;;
27580
- *)
27581
- echo "Unknown release asset kind: $asset_kind" >&2
27582
- rm -f "$temporary_artifact" "$temporary_checksums"
27583
- return 1
27584
- ;;
27585
- esac || {
27586
- rm -f "$temporary_artifact" "$temporary_checksums"
27587
- return 1
27588
- }
27589
-
27590
- mv -f "$temporary_artifact" "$destination"
27591
- temporary_artifact=""
27592
- rm -f "$temporary_checksums"
27593
- temporary_checksums=""
27594
- )
27595
- `;
27271
+ var release_assets_default = '#!/usr/bin/env bash\n\nSUPACLOUD_GITHUB_REPOSITORY="${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/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';
27596
27272
 
27597
27273
  // src/shared/tools/ssh-tools.ts
27598
27274
  var SAFE_CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
@@ -27606,6 +27282,92 @@ var SAFE_DB_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$/;
27606
27282
  var MINIMUM_COMPONENT_UPGRADE_VERSION = "0.50.27";
27607
27283
  var DIRECT_UPGRADE_SSH_TIMEOUT_MS = 720000;
27608
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
+ };
27609
27371
  function hostnameSchema(fieldName) {
27610
27372
  return decodedSchema(Type.String(), Type.String({ minLength: 1, maxLength: 253 }), (value) => {
27611
27373
  const normalized = value.trim();
@@ -27663,7 +27425,8 @@ function assertSafeReleaseTag(value) {
27663
27425
  return value;
27664
27426
  }
27665
27427
  function assertExactStableVersion2(value, fieldName) {
27666
- if (!/^v?\d+\.\d+\.\d+$/.test(value)) {
27428
+ const normalized = value.startsWith("v") ? value.slice(1) : value;
27429
+ if (!EXACT_STABLE_SEMVER.test(normalized)) {
27667
27430
  throw new Error(`${fieldName} must be an exact stable semantic version`);
27668
27431
  }
27669
27432
  return value;
@@ -27707,6 +27470,27 @@ function signalSafeCleanupTraps(cleanupCommand) {
27707
27470
  `trap ${quoteEnvValue(`trap - EXIT HUP INT TERM; ${cleanupCommand}; exit 1`)} HUP INT TERM`
27708
27471
  ];
27709
27472
  }
27473
+ function trustedRootPreflightCommands(helperPath) {
27474
+ const helperDirectory = dirname(helperPath);
27475
+ const trustedRootPath = join2(helperDirectory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
27476
+ return [
27477
+ `HELPER_DIRECTORY=${quoteEnvValue(helperDirectory)}`,
27478
+ `TRUSTED_ROOT=${quoteEnvValue(trustedRootPath)}`,
27479
+ `test -d "$HELPER_DIRECTORY" && test ! -L "$HELPER_DIRECTORY" || { echo 'Upgrade helper directory is not secure' >&2; exit 1; }`,
27480
+ `test "$(stat -c '%a' "$HELPER_DIRECTORY")" = 700 || { echo 'Upgrade helper directory must use mode 0700' >&2; exit 1; }`,
27481
+ `EXPECTED_HELPER_ENTRIES=$(printf '%s\\n' ${quoteEnvValue(basename3(helperPath))} ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME)} | LC_ALL=C sort)`,
27482
+ `ACTUAL_HELPER_ENTRIES=$(find "$HELPER_DIRECTORY" -mindepth 1 -maxdepth 1 -printf '%f\\n' | LC_ALL=C sort)`,
27483
+ `test "$ACTUAL_HELPER_ENTRIES" = "$EXPECTED_HELPER_ENTRIES" || { echo 'Upgrade helper directory does not match its strict file allowlist' >&2; exit 1; }`,
27484
+ `test -f "$TRUSTED_ROOT" && test ! -L "$TRUSTED_ROOT" && test "$(stat -c '%h' "$TRUSTED_ROOT")" = 1 || { echo 'Pinned trusted root must be a direct regular file without links' >&2; exit 1; }`,
27485
+ `test "$(stat -c '%a' "$TRUSTED_ROOT")" = 600 || { echo 'Pinned trusted root must use mode 0600' >&2; exit 1; }`,
27486
+ `test "$(stat -c '%s' "$TRUSTED_ROOT")" = ${SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE} || { echo 'Pinned trusted root size mismatch' >&2; exit 1; }`,
27487
+ `test "$(sha256sum "$TRUSTED_ROOT" | awk '{print $1}')" = ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256)} || { echo 'Pinned trusted root digest mismatch' >&2; exit 1; }`,
27488
+ 'chown -h root:root "$TRUSTED_ROOT"',
27489
+ `test "$(stat -c '%u:%g:%a:%h' "$TRUSTED_ROOT")" = 0:0:600:1 || { echo 'Pinned trusted root ownership or mode changed during adoption' >&2; exit 1; }`,
27490
+ `test "$(stat -c '%s' "$TRUSTED_ROOT")" = ${SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE} && test "$(sha256sum "$TRUSTED_ROOT" | awk '{print $1}')" = ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256)} || { echo 'Pinned trusted root changed during adoption' >&2; exit 1; }`,
27491
+ 'export SUPACLOUD_ATTESTATION_TRUSTED_ROOT="$TRUSTED_ROOT"'
27492
+ ];
27493
+ }
27710
27494
  function buildRootUpgradeScript(request) {
27711
27495
  const envAssignments = upgradeEnvAssignments(request);
27712
27496
  return [
@@ -27714,16 +27498,18 @@ function buildRootUpgradeScript(request) {
27714
27498
  "umask 077",
27715
27499
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
27716
27500
  "export PATH",
27717
- "unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_GH_VERSION SUPACLOUD_GH_MIN_VERSION SUPACLOUD_GH_AMD64_SHA256 SUPACLOUD_GH_ARM64_SHA256 GH_PROXY",
27501
+ "unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_ATTESTATION_TRUSTED_ROOT SUPACLOUD_GH_VERSION SUPACLOUD_GH_MIN_VERSION SUPACLOUD_GH_AMD64_SHA256 SUPACLOUD_GH_ARM64_SHA256 GH_PROXY",
27718
27502
  request.githubProxy ? `export SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : "unset SUPACLOUD_GITHUB_PROXY SUPACLOUD_GITHUB_PROXIES",
27719
- 'for tool in curl jq file sha256sum stat tar flock; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
27503
+ 'for tool in curl jq file sha256sum stat tar flock find sort chown; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
27720
27504
  "test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
27721
27505
  "test -x /usr/local/bin/supacloud || { echo 'SupaCloud binary not found at /usr/local/bin/supacloud; run ssh install first.' >&2; exit 127; }",
27506
+ ...trustedRootPreflightCommands(request.helperPath),
27722
27507
  buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
27723
27508
  ...componentPreflightCommands(request),
27724
27509
  "STAGED_MANAGEMENT=''",
27725
27510
  ...signalSafeCleanupTraps('test -z "$STAGED_MANAGEMENT" || rm -f "$STAGED_MANAGEMENT"'),
27726
27511
  `source ${quoteEnvValue(request.helperPath)}`,
27512
+ "supacloud_attestation_trusted_root_available || { echo 'Pinned Sigstore trusted root failed helper validation' >&2; exit 1; }",
27727
27513
  "if ! supacloud_attestation_verifier_available; then supacloud_install_pinned_gh /usr/local/bin/gh; fi",
27728
27514
  "supacloud_attestation_verifier_available || { echo 'Pinned GitHub attestation verifier is unavailable' >&2; exit 1; }",
27729
27515
  ...componentBootstrapCommands(request),
@@ -27745,8 +27531,7 @@ async function prepareRemoteUpgradeHelperDirectory(ssh, helperPath) {
27745
27531
  const command = [
27746
27532
  "set -e",
27747
27533
  "umask 077",
27748
- `test ! -e ${quoteEnvValue(helperDirectory)}`,
27749
- `install -d -m 700 -- ${quoteEnvValue(helperDirectory)}`
27534
+ `mkdir -m 700 -- ${quoteEnvValue(helperDirectory)}`
27750
27535
  ].join("; ");
27751
27536
  const preparation = await ssh.exec(command, 30000);
27752
27537
  if (!preparation.success) {
@@ -27773,44 +27558,56 @@ function remoteUpgradeFailure(execution) {
27773
27558
  const diagnostic = execution.stderr.trim() || execution.stdout.trim() || "no remote diagnostic";
27774
27559
  return new Error(`Remote upgrade failed (exit ${execution.code}): ${diagnostic.slice(-500)}`);
27775
27560
  }
27776
- function officialUpgradeOutcome(execution, executionError, cleanupError) {
27777
- if (executionError && cleanupError) {
27778
- throw new AggregateError([executionError, cleanupError], "Upgrade execution failed and helper cleanup did not complete");
27561
+ function officialUpgradeOutcome(outcome) {
27562
+ if (outcome.executionFailed && outcome.cleanupFailed) {
27563
+ throw new AggregateError([outcome.executionError, outcome.cleanupError], "Upgrade execution failed and helper cleanup did not complete");
27779
27564
  }
27780
- if (executionError)
27781
- throw executionError;
27782
- if (!execution)
27565
+ if (outcome.executionFailed)
27566
+ throw outcome.executionError;
27567
+ if (!outcome.execution)
27783
27568
  throw new Error("Upgrade execution did not return a result");
27784
- if (cleanupError && !execution.success) {
27785
- throw new AggregateError([remoteUpgradeFailure(execution), cleanupError], "Remote upgrade failed and helper cleanup did not complete");
27569
+ if (outcome.cleanupFailed && !outcome.execution.success) {
27570
+ throw new AggregateError([remoteUpgradeFailure(outcome.execution), outcome.cleanupError], "Remote upgrade failed and helper cleanup did not complete");
27786
27571
  }
27787
- if (cleanupError)
27788
- throw cleanupError;
27789
- if (!execution.success)
27790
- throw remoteUpgradeFailure(execution);
27791
- return execution;
27572
+ if (outcome.cleanupFailed)
27573
+ throw outcome.cleanupError;
27574
+ if (!outcome.execution.success)
27575
+ throw remoteUpgradeFailure(outcome.execution);
27576
+ return outcome.execution;
27792
27577
  }
27793
27578
  async function executeOfficialUpgrade(ssh, helperPath, command, timeoutMs) {
27794
27579
  let execution;
27580
+ let executionFailed = false;
27795
27581
  let executionError;
27796
27582
  let helperPrepared = false;
27797
27583
  try {
27798
27584
  await prepareRemoteUpgradeHelperDirectory(ssh, helperPath);
27799
27585
  helperPrepared = true;
27800
27586
  await ssh.uploadText(helperPath, release_assets_default, 384);
27587
+ const trustedRootPath = join2(dirname(helperPath), SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
27588
+ await ssh.uploadText(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, 384);
27801
27589
  execution = await ssh.exec(command, timeoutMs);
27802
27590
  } catch (error) {
27591
+ executionFailed = true;
27803
27592
  executionError = error;
27804
27593
  }
27594
+ let cleanupFailed = false;
27805
27595
  let cleanupError;
27806
27596
  if (helperPrepared) {
27807
27597
  try {
27808
27598
  await removeRemoteUpgradeHelper(ssh, helperPath);
27809
27599
  } catch (error) {
27600
+ cleanupFailed = true;
27810
27601
  cleanupError = error;
27811
27602
  }
27812
27603
  }
27813
- return officialUpgradeOutcome(execution, executionError, cleanupError);
27604
+ return officialUpgradeOutcome({
27605
+ execution,
27606
+ executionFailed,
27607
+ executionError,
27608
+ cleanupFailed,
27609
+ cleanupError
27610
+ });
27814
27611
  }
27815
27612
  function assertSafeGithubProxy(value) {
27816
27613
  const trimmed = value.trim();
@@ -27949,14 +27746,366 @@ function assertSafeExecCommand(command) {
27949
27746
  }
27950
27747
  return reject();
27951
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
+ }
27952
28100
  function registerSshTools(server, ssh) {
27953
28101
  server.tool("ssh", `Server management via SSH. Available before & after SupaCloud installation.
27954
- 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`, {
27955
28103
  action: withDescription(stringEnum([
27956
28104
  "ping",
27957
28105
  "setup",
27958
28106
  "install",
27959
28107
  "upgrade",
28108
+ "versions",
27960
28109
  "diagnose",
27961
28110
  "exec",
27962
28111
  "troubleshoot",
@@ -28033,7 +28182,7 @@ ${bootstrapDeps.stderr.slice(-500)}`;
28033
28182
  break;
28034
28183
  }
28035
28184
  const osCheck = await ssh.exec("cat /etc/os-release | grep -E 'NAME|VERSION_ID' | head -4");
28036
- const clone2 = await ssh.exec(`set -e; umask 077; rm -rf ${quoteEnvValue(BOOTSTRAP)}; ` + `${bootstrapClone}; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} remote set-url origin ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} remote get-url origin)" = ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} symbolic-ref --short HEAD)" = main; ` + `test -z "$(git -C ${quoteEnvValue(BOOTSTRAP)} status --porcelain --untracked-files=no)"; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} ls-files --error-unmatch setup.sh scripts/lib/install_config.sh scripts/lib/release_assets.sh >/dev/null; ` + `test -f ${quoteEnvValue(`${BOOTSTRAP}/setup.sh`)}; echo BOOTSTRAP_OK`, 120000);
28185
+ const clone2 = await ssh.exec(`set -e; umask 077; rm -rf ${quoteEnvValue(BOOTSTRAP)}; ` + `${bootstrapClone}; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} remote set-url origin ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} remote get-url origin)" = ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} symbolic-ref --short HEAD)" = main; ` + `test -z "$(git -C ${quoteEnvValue(BOOTSTRAP)} status --porcelain --untracked-files=no)"; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} ls-files --error-unmatch setup.sh scripts/lib/install_config.sh scripts/lib/release_assets.sh packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl >/dev/null; ` + `test -f ${quoteEnvValue(`${BOOTSTRAP}/setup.sh`)}; echo BOOTSTRAP_OK`, 120000);
28037
28186
  if (!clone2.stdout.includes("BOOTSTRAP_OK")) {
28038
28187
  await ssh.exec(`rm -rf ${quoteEnvValue(BOOTSTRAP)}`);
28039
28188
  text = `❌ Trusted bootstrap clone failed
@@ -28098,22 +28247,16 @@ ${result.stderr.slice(-500)}`;
28098
28247
  break;
28099
28248
  }
28100
28249
  case "upgrade": {
28101
- const version = args.version ? assertSafeReleaseTag(args.version) : undefined;
28102
- 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;
28103
28252
  if (edgeRuntimeVersion && !version) {
28104
28253
  throw new Error("'version' is required with 'edge_runtime_version'");
28105
28254
  }
28106
- if (edgeRuntimeVersion && version) {
28107
- assertExactStableVersion2(version, "version");
28108
- assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
28109
- }
28110
28255
  const validatedProxy = args.github_proxy ? assertSafeGithubProxy(args.github_proxy) : undefined;
28111
28256
  if (args.artifact_transport === "local") {
28112
28257
  if (!version || !edgeRuntimeVersion) {
28113
28258
  throw new Error("Local artifact transport requires exact 'version' and 'edge_runtime_version'");
28114
28259
  }
28115
- assertExactStableVersion2(version, "version");
28116
- assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
28117
28260
  if (validatedProxy && !["direct", "none"].includes(validatedProxy.toLowerCase())) {
28118
28261
  throw new Error("Local artifact transport only supports direct GitHub downloads");
28119
28262
  }
@@ -28139,17 +28282,27 @@ ${result.stderr.slice(-500)}`;
28139
28282
  ${upgradeExecution.stdout.slice(-300)}${edgeBoundary}`;
28140
28283
  break;
28141
28284
  }
28285
+ case "versions": {
28286
+ text = JSON.stringify(await platformVersions(ssh), null, 2);
28287
+ break;
28288
+ }
28142
28289
  case "diagnose": {
28143
28290
  const cmds = [
28291
+ "set -o pipefail",
28144
28292
  "echo '=== OS ===' && uname -a",
28145
28293
  "echo '=== Memory ===' && free -h",
28146
28294
  "echo '=== Disk ===' && df -h /",
28147
28295
  "echo '=== Docker ===' && (docker ps --format 'table {{.Names}}\t{{.Status}}' 2>/dev/null || echo 'Not found')",
28148
- "echo '=== PostgreSQL ===' && (pg_isready 2>/dev/null && echo 'Running' || echo 'Not detected')",
28149
- "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'"
28150
28302
  ];
28151
- const r = await ssh.exec(cmds.join(" && "));
28152
- 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));
28153
28306
  break;
28154
28307
  }
28155
28308
  case "exec": {
@@ -28984,7 +29137,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
28984
29137
  // package.json
28985
29138
  var package_default = {
28986
29139
  name: "@supacloud/admin",
28987
- version: "0.7.11",
29140
+ version: "0.8.0",
28988
29141
  description: "Platform administration CLI for SupaCloud operators",
28989
29142
  type: "module",
28990
29143
  main: "./dist/index.js",
@@ -29054,6 +29207,7 @@ var sshActionSchema = stringEnum([
29054
29207
  "setup",
29055
29208
  "install",
29056
29209
  "upgrade",
29210
+ "versions",
29057
29211
  "diagnose",
29058
29212
  "exec",
29059
29213
  "troubleshoot",
@@ -29108,6 +29262,7 @@ EXAMPLES
29108
29262
 
29109
29263
  supacloud-admin status
29110
29264
  supacloud-admin ssh ping
29265
+ supacloud-admin ssh versions
29111
29266
  supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
29112
29267
  supacloud-admin project create --name my-app
29113
29268
  supacloud-admin project list