@supacloud/admin 0.7.11 → 0.7.12

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 +7 -5
  2. package/dist/index.js +245 -537
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,11 +49,13 @@ Local artifact transport downloads exact releases directly on the Admin host,
49
49
  verifies their signed release manifests, checksums, sizes, provenance, source
50
50
  commit, and architecture, then transfers them through an atomic SFTP staging
51
51
  directory. The server takes root ownership, repeats offline verification, and
52
- runs the uploaded target Management binary without GitHub egress or a
53
- third-party proxy. The server reuses an installed `gh` only when it supports all
54
- required strict attestation flags. Otherwise Admin transfers a pinned temporary
55
- Linux `gh` verifier that is removed with the staging directory and never
56
- replaces `/usr/local/bin/gh`.
52
+ runs the uploaded target Management binary without GitHub or Sigstore TUF
53
+ egress or a third-party proxy. Admin and Management use the same reviewed,
54
+ digest-pinned Sigstore Public Good trusted root via `--custom-trusted-root`, so
55
+ the offline handoff does not depend on a pre-populated TUF cache. The server
56
+ reuses an installed `gh` only when it supports all required strict attestation
57
+ flags. Otherwise Admin transfers a pinned temporary Linux `gh` verifier that is
58
+ removed with the staging directory and never replaces `/usr/local/bin/gh`.
57
59
 
58
60
  The command supports direct root SSH and passwordless `sudo -n`. The transaction
59
61
  runs in a uniquely named transient systemd unit with protected atomic status
package/dist/index.js CHANGED
@@ -25703,7 +25703,7 @@ class HttpTransport {
25703
25703
 
25704
25704
  // src/shared/tools/ssh-tools.ts
25705
25705
  import { randomUUID as randomUUID4 } from "node:crypto";
25706
- import { dirname } from "node:path";
25706
+ import { basename as basename3, dirname, join as join2 } from "node:path";
25707
25707
 
25708
25708
  // src/shared/releases/local-upgrade-transfer.ts
25709
25709
  import { randomUUID as randomUUID3 } from "node:crypto";
@@ -25735,10 +25735,10 @@ function quoteShell(shellText) {
25735
25735
 
25736
25736
  // src/shared/releases/local-upgrade-bundle.ts
25737
25737
  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";
25738
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
25739
+ 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
25740
  import { get } from "node:https";
25741
- import { tmpdir } from "node:os";
25741
+ import { tmpdir as tmpdir2 } from "node:os";
25742
25742
  import { basename, delimiter, join } from "node:path";
25743
25743
  import { pipeline } from "node:stream/promises";
25744
25744
  import { Transform as Transform2 } from "node:stream";
@@ -25905,6 +25905,97 @@ function parseReleaseChecksums(text, manifest) {
25905
25905
  return checksums;
25906
25906
  }
25907
25907
 
25908
+ // ../management-api/src/sigstore-trusted-root.ts
25909
+ import { createHash } from "node:crypto";
25910
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
25911
+ import { tmpdir } from "node:os";
25912
+ import path from "node:path";
25913
+
25914
+ // ../management-api/src/assets/sigstore-public-good-trusted-root.jsonl
25915
+ 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';
25916
+
25917
+ // ../management-api/src/sigstore-trusted-root.ts
25918
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME = "trusted_root.jsonl";
25919
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256 = "3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1";
25920
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE = 5748;
25921
+ function sha256(text) {
25922
+ return createHash("sha256").update(text).digest("hex");
25923
+ }
25924
+ function validateEmbeddedTrustedRoot(jsonl) {
25925
+ if (Buffer.byteLength(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
25926
+ throw new Error("Embedded Sigstore Public Good trusted root does not match its pinned digest");
25927
+ }
25928
+ if (!jsonl.endsWith(`
25929
+ `) || jsonl.slice(0, -1).includes(`
25930
+ `)) {
25931
+ throw new Error("Embedded Sigstore Public Good trusted root must be compact JSONL");
25932
+ }
25933
+ let trustedRoot;
25934
+ try {
25935
+ trustedRoot = JSON.parse(jsonl);
25936
+ } catch {
25937
+ throw new Error("Embedded Sigstore Public Good trusted root is not valid JSON");
25938
+ }
25939
+ if (!trustedRoot || typeof trustedRoot !== "object" || Array.isArray(trustedRoot) || `${JSON.stringify(trustedRoot)}
25940
+ ` !== jsonl) {
25941
+ throw new Error("Embedded Sigstore Public Good trusted root is not canonical compact JSONL");
25942
+ }
25943
+ return jsonl;
25944
+ }
25945
+ var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL = validateEmbeddedTrustedRoot(sigstore_public_good_trusted_root_default);
25946
+ function createSigstoreVerificationDirectory() {
25947
+ const directory = mkdtempSync(path.join(tmpdir(), "supacloud-sigstore-"));
25948
+ try {
25949
+ chmodSync(directory, 448);
25950
+ const trustedRootPath = path.join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
25951
+ writeFileSync(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, {
25952
+ flag: "wx",
25953
+ mode: 384
25954
+ });
25955
+ chmodSync(trustedRootPath, 384);
25956
+ return { directory, trustedRootPath };
25957
+ } catch (operationError) {
25958
+ try {
25959
+ rmSync(directory, { recursive: true });
25960
+ } catch (cleanupError) {
25961
+ throw new AggregateError([operationError, cleanupError], "Sigstore trusted root creation failed and temporary directory cleanup did not complete");
25962
+ }
25963
+ throw operationError;
25964
+ }
25965
+ }
25966
+ function throwVerificationDirectoryOutcome(outcome) {
25967
+ if (outcome.operationFailed && outcome.cleanupFailed) {
25968
+ throw new AggregateError([outcome.operationError, outcome.cleanupError], "Sigstore verification failed and temporary trusted root cleanup did not complete");
25969
+ }
25970
+ if (outcome.operationFailed)
25971
+ throw outcome.operationError;
25972
+ if (outcome.cleanupFailed) {
25973
+ throw new AggregateError([outcome.cleanupError], "Temporary Sigstore trusted root cleanup did not complete");
25974
+ }
25975
+ }
25976
+ async function withSigstoreVerificationDirectory(operation) {
25977
+ const resources = createSigstoreVerificationDirectory();
25978
+ let operationOutput;
25979
+ let operationFailed = false;
25980
+ let operationError;
25981
+ try {
25982
+ operationOutput = await operation(resources);
25983
+ } catch (error) {
25984
+ operationFailed = true;
25985
+ operationError = error;
25986
+ }
25987
+ let cleanupFailed = false;
25988
+ let cleanupError;
25989
+ try {
25990
+ rmSync(resources.directory, { recursive: true });
25991
+ } catch (error) {
25992
+ cleanupFailed = true;
25993
+ cleanupError = error;
25994
+ }
25995
+ throwVerificationDirectoryOutcome({ operationFailed, operationError, cleanupFailed, cleanupError });
25996
+ return operationOutput;
25997
+ }
25998
+
25908
25999
  // src/shared/releases/local-upgrade-bundle.ts
25909
26000
  var RELEASES_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/releases`;
25910
26001
  var ATTESTATIONS_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/attestations`;
@@ -25938,7 +26029,8 @@ var STRICT_GITHUB_CAPABILITY_FLAGS = [
25938
26029
  "--signer-workflow",
25939
26030
  "--source-ref",
25940
26031
  "--source-digest",
25941
- "--deny-self-hosted-runners"
26032
+ "--deny-self-hosted-runners",
26033
+ "--custom-trusted-root"
25942
26034
  ];
25943
26035
 
25944
26036
  class RetryableDownloadError extends Error {
@@ -25953,7 +26045,56 @@ function githubCliArchiveIdentity(architecture) {
25953
26045
  };
25954
26046
  }
25955
26047
  function sha256File(filePath) {
25956
- return createHash("sha256").update(readFileSync3(filePath)).digest("hex");
26048
+ return createHash2("sha256").update(readFileSync3(filePath)).digest("hex");
26049
+ }
26050
+ function effectiveLocalOwner() {
26051
+ if (process.platform === "win32")
26052
+ return;
26053
+ const uid = process.geteuid?.() ?? process.getuid?.();
26054
+ const gid = process.getegid?.() ?? process.getgid?.();
26055
+ if (uid === undefined || gid === undefined) {
26056
+ throw new Error("Local artifact transport requires POSIX ownership checks");
26057
+ }
26058
+ return { uid, gid };
26059
+ }
26060
+ function assertLocalOwner(uid, gid, label) {
26061
+ const owner = effectiveLocalOwner();
26062
+ if (!owner)
26063
+ return;
26064
+ if (uid !== owner.uid || gid !== owner.gid) {
26065
+ throw new Error(`${label} must be owned by the current effective user`);
26066
+ }
26067
+ }
26068
+ function assertLocalTrustedRootDirectory(directory) {
26069
+ const directoryStats = lstatSync(directory);
26070
+ if (!directoryStats.isDirectory() || directoryStats.isSymbolicLink()) {
26071
+ throw new Error("Local trusted-root directory must be a directory without links");
26072
+ }
26073
+ assertPrivateLocalDirectoryMode(directoryStats.mode, "Local trusted-root directory");
26074
+ assertLocalOwner(directoryStats.uid, directoryStats.gid, "Local trusted-root directory");
26075
+ const entries = readdirSync(directory);
26076
+ if (entries.length !== 1 || entries[0] !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME) {
26077
+ throw new Error("Local trusted-root directory does not match its strict file allowlist");
26078
+ }
26079
+ const trustedRootPath = join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
26080
+ assertLocalTrustedRootFile(trustedRootPath);
26081
+ return trustedRootPath;
26082
+ }
26083
+ function assertPrivateLocalDirectoryMode(mode, label) {
26084
+ if (process.platform !== "win32" && (mode & 4095) !== 448) {
26085
+ throw new Error(`${label} must use exact mode 0700 without special permission bits`);
26086
+ }
26087
+ }
26088
+ function assertLocalTrustedRootFile(trustedRootPath) {
26089
+ const stats = lstatSync(trustedRootPath);
26090
+ if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
26091
+ throw new Error("Local trusted root must be a direct regular file without links");
26092
+ }
26093
+ assertPrivateLocalFileMode(stats.mode, "Local trusted root");
26094
+ assertLocalOwner(stats.uid, stats.gid, "Local trusted root");
26095
+ if (stats.size !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256File(trustedRootPath) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
26096
+ throw new Error("Local trusted root does not match its pinned size and digest");
26097
+ }
25957
26098
  }
25958
26099
  function assertPrivateLocalFileMode(mode, label) {
25959
26100
  if (process.platform !== "win32" && (mode & 4095) !== 384) {
@@ -26055,10 +26196,10 @@ async function downloadDirect(url, destination, maxBytes) {
26055
26196
  const deadline = Date.now() + DOWNLOAD_TIMEOUT_MS;
26056
26197
  const retryFailures = [];
26057
26198
  for (let attempt = 1;attempt <= 3; attempt += 1) {
26058
- rmSync(destination, { force: true });
26199
+ rmSync2(destination, { force: true });
26059
26200
  try {
26060
26201
  await downloadHttpsResponse({ url: parsed, destination, maxBytes, redirects: 0, deadline });
26061
- chmodSync(destination, 384);
26202
+ chmodSync2(destination, 384);
26062
26203
  return;
26063
26204
  } catch (error) {
26064
26205
  if (!downloadCanRetry(error))
@@ -26214,13 +26355,13 @@ async function assertLocalGithubVerifier() {
26214
26355
  throw new Error("Local artifact transport requires a current gh attestation verifier");
26215
26356
  }
26216
26357
  }
26217
- async function verifyManifestAttestation(manifestPath, bundlePath, manifest) {
26218
- const verification = await runGithubCli([
26358
+ function githubAttestationVerificationArguments(request) {
26359
+ return [
26219
26360
  "attestation",
26220
26361
  "verify",
26221
- manifestPath,
26362
+ request.artifactPath,
26222
26363
  "--bundle",
26223
- bundlePath,
26364
+ request.bundlePath,
26224
26365
  "--repo",
26225
26366
  RELEASE_REPOSITORY,
26226
26367
  "--signer-workflow",
@@ -26228,9 +26369,14 @@ async function verifyManifestAttestation(manifestPath, bundlePath, manifest) {
26228
26369
  "--source-ref",
26229
26370
  RELEASE_SOURCE_REF,
26230
26371
  "--source-digest",
26231
- manifest.source.commit,
26232
- "--deny-self-hosted-runners"
26233
- ], GH_VERIFICATION_TIMEOUT_MS);
26372
+ request.manifest.source.commit,
26373
+ "--deny-self-hosted-runners",
26374
+ "--custom-trusted-root",
26375
+ request.trustedRootPath
26376
+ ];
26377
+ }
26378
+ async function verifyManifestAttestation(request) {
26379
+ const verification = await runGithubCli(githubAttestationVerificationArguments(request), GH_VERIFICATION_TIMEOUT_MS);
26234
26380
  if (verification.exitCode !== 0) {
26235
26381
  throw new Error(`gh attestation verify failed: ${verification.stderr.trim().slice(-1000) || verification.exitCode}`);
26236
26382
  }
@@ -26261,7 +26407,7 @@ async function downloadReleaseMetadata(component, version, directory) {
26261
26407
  await downloadDirect(`${RELEASES_API}/tags/${expectedTag}`, metadataPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
26262
26408
  return parseGithubReleaseMetadata(parseJsonFile(metadataPath, "GitHub release metadata"), expectedTag);
26263
26409
  } finally {
26264
- rmSync(metadataPath, { force: true });
26410
+ rmSync2(metadataPath, { force: true });
26265
26411
  }
26266
26412
  }
26267
26413
  async function downloadManifestAttestation(manifestPath, destination) {
@@ -26269,10 +26415,10 @@ async function downloadManifestAttestation(manifestPath, destination) {
26269
26415
  try {
26270
26416
  await downloadDirect(`${ATTESTATIONS_API}/sha256:${sha256File(manifestPath)}`, responsePath, RELEASE_BUNDLE_SIZE_LIMITS.attestation);
26271
26417
  const bundles = serializeAttestationBundles(parseJsonFile(responsePath, "GitHub attestation response"));
26272
- writeFileSync(destination, bundles, { mode: 384, flag: "wx" });
26273
- chmodSync(destination, 384);
26418
+ writeFileSync2(destination, bundles, { mode: 384, flag: "wx" });
26419
+ chmodSync2(destination, 384);
26274
26420
  } finally {
26275
- rmSync(responsePath, { force: true });
26421
+ rmSync2(responsePath, { force: true });
26276
26422
  }
26277
26423
  }
26278
26424
  async function downloadComponent(request) {
@@ -26282,7 +26428,12 @@ async function downloadComponent(request) {
26282
26428
  await downloadDirect(releaseAssetUrl(release, RELEASE_MANIFEST_NAME), manifestPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
26283
26429
  const manifest = parseReleaseManifest(readFileSync3(manifestPath, "utf8"), request);
26284
26430
  await downloadManifestAttestation(manifestPath, attestationPath);
26285
- await verifyManifestAttestation(manifestPath, attestationPath, manifest);
26431
+ await verifyManifestAttestation({
26432
+ artifactPath: manifestPath,
26433
+ bundlePath: attestationPath,
26434
+ manifest,
26435
+ trustedRootPath: request.trustedRootPath
26436
+ });
26286
26437
  const checksumsPath = directChildPath(request.destination, RELEASE_CHECKSUMS_NAME);
26287
26438
  await downloadDirect(releaseAssetUrl(release, RELEASE_CHECKSUMS_NAME), checksumsPath, RELEASE_BUNDLE_SIZE_LIMITS.checksums);
26288
26439
  assertSignedArtifact(checksumsPath, manifest);
@@ -26305,7 +26456,7 @@ async function downloadPinnedGithubCli(directory, architecture) {
26305
26456
  return localUpgradeFile(archivePath, `verifier/${identity.archiveName}`);
26306
26457
  }
26307
26458
  function cleanupLocalUpgradeBundle(bundle) {
26308
- rmSync(bundle.directory, { recursive: true, force: true });
26459
+ rmSync2(bundle.directory, { recursive: true, force: true });
26309
26460
  }
26310
26461
  function assertLocalUpgradeBundleSize(files) {
26311
26462
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
@@ -26342,15 +26493,15 @@ async function settleLocalBundleDownloads(downloads) {
26342
26493
  ];
26343
26494
  }
26344
26495
  function createLocalBundleLayout(request) {
26345
- const directory = mkdtempSync(join(tmpdir(), "supacloud-admin-upgrade-"));
26346
- chmodSync(directory, 448);
26496
+ const directory = mkdtempSync2(join(tmpdir2(), "supacloud-admin-upgrade-"));
26497
+ chmodSync2(directory, 448);
26347
26498
  const bundleDirectory = join(directory, "bundle");
26348
26499
  const managementDirectory = join(bundleDirectory, "management-api");
26349
26500
  const edgeDirectory = join(bundleDirectory, "edge-runtime");
26350
26501
  const verifierDirectory = join(directory, "verifier");
26351
26502
  for (const createdDirectory of [bundleDirectory, managementDirectory, edgeDirectory, verifierDirectory]) {
26352
26503
  mkdirSync(createdDirectory, { recursive: true, mode: 448 });
26353
- chmodSync(createdDirectory, 448);
26504
+ chmodSync2(createdDirectory, 448);
26354
26505
  }
26355
26506
  return {
26356
26507
  directory,
@@ -26361,20 +26512,22 @@ function createLocalBundleLayout(request) {
26361
26512
  edgeRuntimeBinaryName: `supacloud-edge-runtime-linux-${request.architecture}`
26362
26513
  };
26363
26514
  }
26364
- async function downloadLocalBundle(request, layout) {
26515
+ async function downloadLocalBundle(request, layout, trustedRootPath) {
26365
26516
  const { managementBinaryName, edgeRuntimeBinaryName } = layout;
26366
26517
  const [managementFiles, edgeFiles, verifierArchive] = await settleLocalBundleDownloads([
26367
26518
  downloadComponent({
26368
26519
  component: "management-api",
26369
26520
  version: request.managementVersion,
26370
26521
  assetNames: [managementBinaryName, "web-console-build.tar.gz"],
26371
- destination: layout.managementDirectory
26522
+ destination: layout.managementDirectory,
26523
+ trustedRootPath
26372
26524
  }),
26373
26525
  downloadComponent({
26374
26526
  component: "edge-runtime",
26375
26527
  version: request.edgeRuntimeVersion,
26376
26528
  assetNames: [edgeRuntimeBinaryName],
26377
- destination: layout.edgeDirectory
26529
+ destination: layout.edgeDirectory,
26530
+ trustedRootPath
26378
26531
  }),
26379
26532
  request.verifierProvisioning === "bundled" ? downloadPinnedGithubCli(layout.verifierDirectory, request.architecture) : Promise.resolve(null)
26380
26533
  ]);
@@ -26388,9 +26541,16 @@ async function prepareLocalUpgradeBundle(request) {
26388
26541
  await assertLocalGithubVerifier();
26389
26542
  const layout = createLocalBundleLayout(request);
26390
26543
  try {
26391
- return await downloadLocalBundle(request, layout);
26544
+ return await withSigstoreVerificationDirectory(async (verificationDirectory) => {
26545
+ const trustedRootPath = assertLocalTrustedRootDirectory(verificationDirectory.directory);
26546
+ return await downloadLocalBundle(request, layout, trustedRootPath);
26547
+ });
26392
26548
  } catch (error) {
26393
- rmSync(layout.directory, { recursive: true, force: true });
26549
+ try {
26550
+ rmSync2(layout.directory, { recursive: true, force: true });
26551
+ } catch (cleanupError) {
26552
+ throw new AggregateError([error, cleanupError], "Local bundle preparation failed and cleanup did not complete");
26553
+ }
26394
26554
  throw error;
26395
26555
  }
26396
26556
  }
@@ -26606,7 +26766,7 @@ function upgradeScriptSetup(paths) {
26606
26766
  "write_status RUNNING",
26607
26767
  buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
26608
26768
  `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"
26769
+ "unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_ATTESTATION_TRUSTED_ROOT NODE_USE_ENV_PROXY"
26610
26770
  ];
26611
26771
  }
26612
26772
  function upgradeScriptFilesystemChecks(bundle) {
@@ -27106,493 +27266,7 @@ async function executeLocalUpgradeTransfer(ssh, request) {
27106
27266
  }
27107
27267
 
27108
27268
  // ../../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
- `;
27269
+ 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
27270
 
27597
27271
  // src/shared/tools/ssh-tools.ts
27598
27272
  var SAFE_CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
@@ -27707,6 +27381,27 @@ function signalSafeCleanupTraps(cleanupCommand) {
27707
27381
  `trap ${quoteEnvValue(`trap - EXIT HUP INT TERM; ${cleanupCommand}; exit 1`)} HUP INT TERM`
27708
27382
  ];
27709
27383
  }
27384
+ function trustedRootPreflightCommands(helperPath) {
27385
+ const helperDirectory = dirname(helperPath);
27386
+ const trustedRootPath = join2(helperDirectory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
27387
+ return [
27388
+ `HELPER_DIRECTORY=${quoteEnvValue(helperDirectory)}`,
27389
+ `TRUSTED_ROOT=${quoteEnvValue(trustedRootPath)}`,
27390
+ `test -d "$HELPER_DIRECTORY" && test ! -L "$HELPER_DIRECTORY" || { echo 'Upgrade helper directory is not secure' >&2; exit 1; }`,
27391
+ `test "$(stat -c '%a' "$HELPER_DIRECTORY")" = 700 || { echo 'Upgrade helper directory must use mode 0700' >&2; exit 1; }`,
27392
+ `EXPECTED_HELPER_ENTRIES=$(printf '%s\\n' ${quoteEnvValue(basename3(helperPath))} ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME)} | LC_ALL=C sort)`,
27393
+ `ACTUAL_HELPER_ENTRIES=$(find "$HELPER_DIRECTORY" -mindepth 1 -maxdepth 1 -printf '%f\\n' | LC_ALL=C sort)`,
27394
+ `test "$ACTUAL_HELPER_ENTRIES" = "$EXPECTED_HELPER_ENTRIES" || { echo 'Upgrade helper directory does not match its strict file allowlist' >&2; exit 1; }`,
27395
+ `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; }`,
27396
+ `test "$(stat -c '%a' "$TRUSTED_ROOT")" = 600 || { echo 'Pinned trusted root must use mode 0600' >&2; exit 1; }`,
27397
+ `test "$(stat -c '%s' "$TRUSTED_ROOT")" = ${SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE} || { echo 'Pinned trusted root size mismatch' >&2; exit 1; }`,
27398
+ `test "$(sha256sum "$TRUSTED_ROOT" | awk '{print $1}')" = ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256)} || { echo 'Pinned trusted root digest mismatch' >&2; exit 1; }`,
27399
+ 'chown -h root:root "$TRUSTED_ROOT"',
27400
+ `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; }`,
27401
+ `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; }`,
27402
+ 'export SUPACLOUD_ATTESTATION_TRUSTED_ROOT="$TRUSTED_ROOT"'
27403
+ ];
27404
+ }
27710
27405
  function buildRootUpgradeScript(request) {
27711
27406
  const envAssignments = upgradeEnvAssignments(request);
27712
27407
  return [
@@ -27714,16 +27409,18 @@ function buildRootUpgradeScript(request) {
27714
27409
  "umask 077",
27715
27410
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
27716
27411
  "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",
27412
+ "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
27413
  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',
27414
+ '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
27415
  "test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
27721
27416
  "test -x /usr/local/bin/supacloud || { echo 'SupaCloud binary not found at /usr/local/bin/supacloud; run ssh install first.' >&2; exit 127; }",
27417
+ ...trustedRootPreflightCommands(request.helperPath),
27722
27418
  buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
27723
27419
  ...componentPreflightCommands(request),
27724
27420
  "STAGED_MANAGEMENT=''",
27725
27421
  ...signalSafeCleanupTraps('test -z "$STAGED_MANAGEMENT" || rm -f "$STAGED_MANAGEMENT"'),
27726
27422
  `source ${quoteEnvValue(request.helperPath)}`,
27423
+ "supacloud_attestation_trusted_root_available || { echo 'Pinned Sigstore trusted root failed helper validation' >&2; exit 1; }",
27727
27424
  "if ! supacloud_attestation_verifier_available; then supacloud_install_pinned_gh /usr/local/bin/gh; fi",
27728
27425
  "supacloud_attestation_verifier_available || { echo 'Pinned GitHub attestation verifier is unavailable' >&2; exit 1; }",
27729
27426
  ...componentBootstrapCommands(request),
@@ -27745,8 +27442,7 @@ async function prepareRemoteUpgradeHelperDirectory(ssh, helperPath) {
27745
27442
  const command = [
27746
27443
  "set -e",
27747
27444
  "umask 077",
27748
- `test ! -e ${quoteEnvValue(helperDirectory)}`,
27749
- `install -d -m 700 -- ${quoteEnvValue(helperDirectory)}`
27445
+ `mkdir -m 700 -- ${quoteEnvValue(helperDirectory)}`
27750
27446
  ].join("; ");
27751
27447
  const preparation = await ssh.exec(command, 30000);
27752
27448
  if (!preparation.success) {
@@ -27773,44 +27469,56 @@ function remoteUpgradeFailure(execution) {
27773
27469
  const diagnostic = execution.stderr.trim() || execution.stdout.trim() || "no remote diagnostic";
27774
27470
  return new Error(`Remote upgrade failed (exit ${execution.code}): ${diagnostic.slice(-500)}`);
27775
27471
  }
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");
27472
+ function officialUpgradeOutcome(outcome) {
27473
+ if (outcome.executionFailed && outcome.cleanupFailed) {
27474
+ throw new AggregateError([outcome.executionError, outcome.cleanupError], "Upgrade execution failed and helper cleanup did not complete");
27779
27475
  }
27780
- if (executionError)
27781
- throw executionError;
27782
- if (!execution)
27476
+ if (outcome.executionFailed)
27477
+ throw outcome.executionError;
27478
+ if (!outcome.execution)
27783
27479
  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");
27480
+ if (outcome.cleanupFailed && !outcome.execution.success) {
27481
+ throw new AggregateError([remoteUpgradeFailure(outcome.execution), outcome.cleanupError], "Remote upgrade failed and helper cleanup did not complete");
27786
27482
  }
27787
- if (cleanupError)
27788
- throw cleanupError;
27789
- if (!execution.success)
27790
- throw remoteUpgradeFailure(execution);
27791
- return execution;
27483
+ if (outcome.cleanupFailed)
27484
+ throw outcome.cleanupError;
27485
+ if (!outcome.execution.success)
27486
+ throw remoteUpgradeFailure(outcome.execution);
27487
+ return outcome.execution;
27792
27488
  }
27793
27489
  async function executeOfficialUpgrade(ssh, helperPath, command, timeoutMs) {
27794
27490
  let execution;
27491
+ let executionFailed = false;
27795
27492
  let executionError;
27796
27493
  let helperPrepared = false;
27797
27494
  try {
27798
27495
  await prepareRemoteUpgradeHelperDirectory(ssh, helperPath);
27799
27496
  helperPrepared = true;
27800
27497
  await ssh.uploadText(helperPath, release_assets_default, 384);
27498
+ const trustedRootPath = join2(dirname(helperPath), SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
27499
+ await ssh.uploadText(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, 384);
27801
27500
  execution = await ssh.exec(command, timeoutMs);
27802
27501
  } catch (error) {
27502
+ executionFailed = true;
27803
27503
  executionError = error;
27804
27504
  }
27505
+ let cleanupFailed = false;
27805
27506
  let cleanupError;
27806
27507
  if (helperPrepared) {
27807
27508
  try {
27808
27509
  await removeRemoteUpgradeHelper(ssh, helperPath);
27809
27510
  } catch (error) {
27511
+ cleanupFailed = true;
27810
27512
  cleanupError = error;
27811
27513
  }
27812
27514
  }
27813
- return officialUpgradeOutcome(execution, executionError, cleanupError);
27515
+ return officialUpgradeOutcome({
27516
+ execution,
27517
+ executionFailed,
27518
+ executionError,
27519
+ cleanupFailed,
27520
+ cleanupError
27521
+ });
27814
27522
  }
27815
27523
  function assertSafeGithubProxy(value) {
27816
27524
  const trimmed = value.trim();
@@ -28033,7 +27741,7 @@ ${bootstrapDeps.stderr.slice(-500)}`;
28033
27741
  break;
28034
27742
  }
28035
27743
  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);
27744
+ 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
27745
  if (!clone2.stdout.includes("BOOTSTRAP_OK")) {
28038
27746
  await ssh.exec(`rm -rf ${quoteEnvValue(BOOTSTRAP)}`);
28039
27747
  text = `❌ Trusted bootstrap clone failed
@@ -28984,7 +28692,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
28984
28692
  // package.json
28985
28693
  var package_default = {
28986
28694
  name: "@supacloud/admin",
28987
- version: "0.7.11",
28695
+ version: "0.7.12",
28988
28696
  description: "Platform administration CLI for SupaCloud operators",
28989
28697
  type: "module",
28990
28698
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.7.11",
3
+ "version": "0.7.12",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",