@supacloud/admin 0.7.10 → 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 +253 -541
  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
  }
@@ -26400,6 +26560,7 @@ var REMOTE_STAGE_ROOT = "/var/lib/supacloud/upgrade-staging";
26400
26560
  var REMOTE_RUN_ROOT = "/var/lib/supacloud/upgrade-runs";
26401
26561
  var REMOTE_LOG_ROOT = "/var/log/supacloud";
26402
26562
  var REMOTE_UPLOAD_ROOT = "/var/tmp";
26563
+ var REMOTE_COMMAND_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
26403
26564
  var POLL_INTERVAL_MS = 2000;
26404
26565
  var STATE_READ_ATTEMPTS = 3;
26405
26566
  var REMOTE_STATE_READ_TIMEOUT_MS = 15000;
@@ -26458,11 +26619,12 @@ function buildRemotePreflightScript() {
26458
26619
  const capabilityFlags = STRICT_GITHUB_CAPABILITY_FLAGS.join(" ");
26459
26620
  return [
26460
26621
  "set -euo pipefail",
26461
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
26622
+ `PATH=${REMOTE_COMMAND_PATH}`,
26462
26623
  "export PATH",
26463
26624
  trustedInstalledGithubFunction(),
26464
26625
  "test -d /run/systemd/system || { echo 'systemd is not the active init system' >&2; exit 1; }",
26465
- 'for tool in systemctl systemd-run sha256sum stat realpath tar file find sort awk grep timeout flock; do command -v "$tool" >/dev/null 2>&1 || { echo "Required local-upgrade tool is missing: $tool" >&2; exit 127; }; done',
26626
+ 'for tool in systemctl systemd-run sha256sum stat realpath tar file find sort awk grep tail timeout flock; do command -v "$tool" >/dev/null 2>&1 || { echo "Required local-upgrade tool is missing: $tool" >&2; exit 127; }; done',
26627
+ "tail -n 0 -- /dev/null >/dev/null 2>&1 || { echo 'A tail implementation with -n and -- support is required' >&2; exit 1; }",
26466
26628
  "systemd-run --help | grep -Eq -- '(^|[[:space:]])--collect([=[:space:]]|$)' || { echo 'systemd-run --collect is required' >&2; exit 1; }",
26467
26629
  "test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
26468
26630
  "test -d /var/lib/supacloud || { echo '/var/lib/supacloud is unavailable' >&2; exit 1; }",
@@ -26586,7 +26748,7 @@ function upgradeScriptSetup(paths) {
26586
26748
  "#!/usr/bin/env bash",
26587
26749
  "set -euo pipefail",
26588
26750
  "umask 077",
26589
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
26751
+ `PATH=${REMOTE_COMMAND_PATH}`,
26590
26752
  "export PATH",
26591
26753
  `STAGE=${quoteShell2(paths.stage)}`,
26592
26754
  `STATUS=${quoteShell2(paths.status)}`,
@@ -26604,7 +26766,7 @@ function upgradeScriptSetup(paths) {
26604
26766
  "write_status RUNNING",
26605
26767
  buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
26606
26768
  `while IFS='=' read -r variable _; do case "$variable" in *PROXY*|*Proxy*|*proxy*) unset "$variable" ;; esac; done < <(env)`,
26607
- "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"
26608
26770
  ];
26609
26771
  }
26610
26772
  function upgradeScriptFilesystemChecks(bundle) {
@@ -26889,9 +27051,11 @@ async function observeRemoteState(ssh, paths, observation) {
26889
27051
  }
26890
27052
  async function remoteLogTail(ssh, paths) {
26891
27053
  const script = [
27054
+ `PATH=${REMOTE_COMMAND_PATH}`,
27055
+ "export PATH",
26892
27056
  `LOG=${quoteShell2(paths.log)}`,
26893
27057
  `test -f "$LOG" && test ! -L "$LOG" || { echo 'Remote upgrade log is not a regular file' >&2; exit 1; }`,
26894
- 'tail -80 -- "$LOG"'
27058
+ 'tail -n 80 -- "$LOG"'
26895
27059
  ].join(`
26896
27060
  `);
26897
27061
  const output = await ssh.exec(rootCommand(script), 15000);
@@ -27102,493 +27266,7 @@ async function executeLocalUpgradeTransfer(ssh, request) {
27102
27266
  }
27103
27267
 
27104
27268
  // ../../scripts/lib/release_assets.sh
27105
- var release_assets_default = `#!/usr/bin/env bash
27106
-
27107
- SUPACLOUD_GITHUB_REPOSITORY="\${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"
27108
- SUPACLOUD_RELEASES_API="\${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/releases}"
27109
- SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="\${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-\${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"
27110
- SUPACLOUD_GH_VERSION="\${SUPACLOUD_GH_VERSION:-2.96.0}"
27111
- SUPACLOUD_GH_MIN_VERSION="\${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"
27112
- SUPACLOUD_GH_AMD64_SHA256="\${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"
27113
- SUPACLOUD_GH_ARM64_SHA256="\${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"
27114
-
27115
- supacloud_curl_release_json() {
27116
- local url="$1"
27117
- local output="$2"
27118
- curl -fsSL --proto '=https' --proto-redir '=https' \\
27119
- --retry 1 --retry-delay 2 --retry-max-time 60 \\
27120
- --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\
27121
- -o "$output" "$url"
27122
- }
27123
-
27124
- supacloud_curl_release_asset() {
27125
- local url="$1"
27126
- local output="$2"
27127
- curl -fL --proto '=https' --proto-redir '=https' \\
27128
- --retry 1 --retry-delay 2 --retry-max-time 180 \\
27129
- --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\
27130
- -o "$output" "$url"
27131
- }
27132
-
27133
- supacloud_component_tag() {
27134
- local component="$1"
27135
- local version="$2"
27136
- case "$version" in
27137
- "\${component}-v"*) printf '%s' "$version" ;;
27138
- v*) printf '%s-%s' "$component" "$version" ;;
27139
- *) printf '%s-v%s' "$component" "$version" ;;
27140
- esac
27141
- }
27142
-
27143
- supacloud_select_release() {
27144
- local component="$1"
27145
- shift
27146
- local required_assets_json
27147
- [[ $# -gt 0 ]] || {
27148
- echo "at least one required release asset must be specified" >&2
27149
- return 1
27150
- }
27151
- required_assets_json=$(printf '%s\\n' "$@" | jq -Rsc 'split("\\n")[:-1]')
27152
- jq -ce --arg prefix "\${component}-v" --argjson required "$required_assets_json" '
27153
- map(select(
27154
- (.draft | not)
27155
- and (.prerelease | not)
27156
- and (.tag_name | startswith($prefix))
27157
- and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
27158
- and any(.assets[]?; .name == "SHA256SUMS")
27159
- ))
27160
- | first
27161
- // error("no matching component release contains all required assets and SHA256SUMS")
27162
- '
27163
- }
27164
-
27165
- supacloud_fetch_component_release() {
27166
- local component="$1"
27167
- local version="\${2:-latest}"
27168
- shift 2
27169
- local required_assets=("$@")
27170
- local required_assets_json
27171
- local response
27172
- [[ \${#required_assets[@]} -gt 0 ]] || {
27173
- echo "at least one required release asset must be specified" >&2
27174
- return 1
27175
- }
27176
- required_assets_json=$(printf '%s\\n' "\${required_assets[@]}" | jq -Rsc 'split("\\n")[:-1]')
27177
-
27178
- if [[ -n "$version" && "$version" != "latest" ]]; then
27179
- local tag
27180
- tag=$(supacloud_component_tag "$component" "$version")
27181
- response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}/tags/\${tag}") || return 1
27182
- jq -ce --argjson required "$required_assets_json" '
27183
- select(
27184
- (.draft | not)
27185
- and (.prerelease | not)
27186
- and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
27187
- and any(.assets[]?; .name == "SHA256SUMS")
27188
- )
27189
- // error("release does not contain all required assets and SHA256SUMS")
27190
- ' <<< "$response"
27191
- return
27192
- fi
27193
-
27194
- response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}?per_page=100") || return 1
27195
- supacloud_select_release "$component" "\${required_assets[@]}" <<< "$response"
27196
- }
27197
-
27198
- supacloud_fetch_release_json() (
27199
- local url="$1"
27200
- local response_file
27201
- response_file=$(mktemp) || return 1
27202
- trap 'rm -f "$response_file"' EXIT
27203
- trap 'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1' HUP INT TERM
27204
- supacloud_download_release_metadata_url "$url" "$response_file" || return 1
27205
- cat "$response_file"
27206
- )
27207
-
27208
- supacloud_release_asset_url() {
27209
- local release_json="$1"
27210
- local asset_name="$2"
27211
- jq -er --arg asset "$asset_name" '
27212
- first(.assets[]? | select(.name == $asset) | .browser_download_url)
27213
- // error("release asset URL is missing")
27214
- ' <<< "$release_json"
27215
- }
27216
-
27217
- supacloud_download_url() {
27218
- local url="$1"
27219
- local output="$2"
27220
- local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
27221
-
27222
- if supacloud_curl_release_asset "$url" "$output"; then
27223
- return 0
27224
- fi
27225
- if [[ -n "$proxy" ]]; then
27226
- supacloud_curl_release_asset "\${proxy%/}/\${url}" "$output"
27227
- return
27228
- fi
27229
- return 1
27230
- }
27231
-
27232
- supacloud_download_release_metadata_url() {
27233
- local url="$1"
27234
- local output="$2"
27235
- local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
27236
-
27237
- if supacloud_curl_release_json "$url" "$output"; then
27238
- return 0
27239
- fi
27240
- if [[ -n "$proxy" ]]; then
27241
- supacloud_curl_release_json "\${proxy%/}/\${url}" "$output"
27242
- return
27243
- fi
27244
- return 1
27245
- }
27246
-
27247
- supacloud_verify_checksum() {
27248
- local artifact_file="$1"
27249
- local asset_name="$2"
27250
- local checksum_file="$3"
27251
- local expected
27252
- expected=$(awk -v asset="$asset_name" '$2 == asset || $2 == "*" asset { print $1; exit }' "$checksum_file")
27253
- if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then
27254
- echo "SHA256SUMS does not contain a valid checksum for \${asset_name}" >&2
27255
- return 1
27256
- fi
27257
-
27258
- local actual
27259
- actual=$(sha256sum "$artifact_file" | awk '{print $1}')
27260
- actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
27261
- expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
27262
- if [[ "$actual" != "$expected" ]]; then
27263
- echo "SHA256 mismatch for \${asset_name}" >&2
27264
- return 1
27265
- fi
27266
- }
27267
-
27268
- supacloud_validate_binary() {
27269
- local artifact_file="$1"
27270
- local asset_name="$2"
27271
- local description
27272
- description=$(file -b "$artifact_file")
27273
- if [[ "$description" != *ELF* ]]; then
27274
- echo "\${asset_name} is not an ELF binary: \${description}" >&2
27275
- return 1
27276
- fi
27277
-
27278
- case "$asset_name" in
27279
- *amd64)
27280
- [[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {
27281
- echo "\${asset_name} does not contain an x86-64 ELF binary" >&2
27282
- return 1
27283
- }
27284
- ;;
27285
- *arm64)
27286
- [[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {
27287
- echo "\${asset_name} does not contain an arm64 ELF binary" >&2
27288
- return 1
27289
- }
27290
- ;;
27291
- esac
27292
- }
27293
-
27294
- supacloud_install_pinned_tar_xz_binary() (
27295
- local archive="$1"
27296
- local member="$2"
27297
- local expected_sha256="$3"
27298
- local arch="$4"
27299
- local target="$5"
27300
- local actual_sha256 member_count member_details extract_dir candidate staged_target
27301
-
27302
- actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
27303
- actual_sha256=$(printf '%s' "$actual_sha256" | tr '[:upper:]' '[:lower:]')
27304
- expected_sha256=$(printf '%s' "$expected_sha256" | tr '[:upper:]' '[:lower:]')
27305
- if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then
27306
- echo "SHA256 mismatch for pinned archive" >&2
27307
- return 1
27308
- fi
27309
-
27310
- member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)
27311
- if [[ "$member_count" != "1" ]]; then
27312
- echo "Pinned archive must contain the exact member once: $member" >&2
27313
- return 1
27314
- fi
27315
- member_details=$(tar -tvJf "$archive" "$member") || return 1
27316
- if [[ "\${member_details:0:1}" != "-" ]]; then
27317
- echo "Pinned archive member is not a regular file: $member" >&2
27318
- return 1
27319
- fi
27320
-
27321
- extract_dir=$(mktemp -d)
27322
- trap 'rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"' EXIT
27323
- trap 'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"; exit 1' HUP INT TERM
27324
- if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then
27325
- return 1
27326
- fi
27327
- candidate="\${extract_dir}/\${member}"
27328
- supacloud_validate_binary "$candidate" "pinned-linux-\${arch}" || return 1
27329
-
27330
- mkdir -p "$(dirname "$target")"
27331
- staged_target=$(mktemp "\${target}.tmp.XXXXXX")
27332
- install -m 0755 "$candidate" "$staged_target"
27333
- mv -f "$staged_target" "$target"
27334
- staged_target=""
27335
- )
27336
-
27337
- supacloud_version_at_least() {
27338
- local current="\${1#v}"
27339
- local required="\${2#v}"
27340
- local current_major=0 current_minor=0 current_patch=0
27341
- local required_major=0 required_minor=0 required_patch=0
27342
- [[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
27343
- [[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
27344
- IFS=. read -r current_major current_minor current_patch <<EOF
27345
- \${current%%-*}
27346
- EOF
27347
- IFS=. read -r required_major required_minor required_patch <<EOF
27348
- \${required%%-*}
27349
- EOF
27350
- current_major=\${current_major:-0}; current_minor=\${current_minor:-0}; current_patch=\${current_patch:-0}
27351
- required_major=\${required_major:-0}; required_minor=\${required_minor:-0}; required_patch=\${required_patch:-0}
27352
- if (( current_major != required_major )); then (( current_major > required_major )); return; fi
27353
- if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi
27354
- (( current_patch >= required_patch ))
27355
- }
27356
-
27357
- supacloud_gh_version() {
27358
- gh --version 2>/dev/null | awk 'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }'
27359
- }
27360
-
27361
- supacloud_install_gh_archive() {
27362
- local archive="$1"
27363
- local version="$2"
27364
- local arch="$3"
27365
- local expected_sha256="$4"
27366
- local target="$5"
27367
- local member="gh_\${version}_linux_\${arch}/bin/gh"
27368
- local actual_sha256 member_count member_details extracted_dir candidate version_output
27369
-
27370
- actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
27371
- if [[ "$actual_sha256" != "$expected_sha256" ]]; then
27372
- echo "GitHub CLI archive SHA256 mismatch" >&2
27373
- return 1
27374
- fi
27375
-
27376
- member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)
27377
- if [[ "$member_count" != "1" ]]; then
27378
- echo "GitHub CLI archive does not contain the exact expected member: $member" >&2
27379
- return 1
27380
- fi
27381
- member_details=$(tar -tvzf "$archive" "$member") || return 1
27382
- if [[ "\${member_details:0:1}" != "-" ]]; then
27383
- echo "GitHub CLI archive member is not a regular file: $member" >&2
27384
- return 1
27385
- fi
27386
-
27387
- extracted_dir=$(mktemp -d)
27388
- candidate="\${extracted_dir}/\${member}"
27389
- if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\
27390
- || ! supacloud_validate_binary "$candidate" "gh-linux-\${arch}"; then
27391
- rm -rf "$extracted_dir"
27392
- return 1
27393
- fi
27394
- chmod 0755 "$candidate"
27395
- version_output=$("$candidate" --version 2>/dev/null | head -1) || {
27396
- rm -rf "$extracted_dir"
27397
- echo "GitHub CLI bootstrap binary failed its version check" >&2
27398
- return 1
27399
- }
27400
- if [[ "$version_output" != "gh version \${version}"* ]]; then
27401
- rm -rf "$extracted_dir"
27402
- echo "GitHub CLI bootstrap version mismatch: \${version_output}" >&2
27403
- return 1
27404
- fi
27405
- mkdir -p "$(dirname "$target")"
27406
- install -m 0755 "$candidate" "$target"
27407
- rm -rf "$extracted_dir"
27408
- }
27409
-
27410
- supacloud_install_pinned_gh() {
27411
- local target="\${1:-/usr/local/bin/gh}"
27412
- local machine arch expected_sha256 asset url archive
27413
- machine=$(uname -m)
27414
- case "$machine" in
27415
- x86_64|amd64)
27416
- arch="amd64"
27417
- expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"
27418
- ;;
27419
- aarch64|arm64)
27420
- arch="arm64"
27421
- expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"
27422
- ;;
27423
- *)
27424
- echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2
27425
- return 1
27426
- ;;
27427
- esac
27428
- asset="gh_\${SUPACLOUD_GH_VERSION}_linux_\${arch}.tar.gz"
27429
- url="https://github.com/cli/cli/releases/download/v\${SUPACLOUD_GH_VERSION}/\${asset}"
27430
- archive=$(mktemp)
27431
- if ! supacloud_download_url "$url" "$archive" \\
27432
- || ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then
27433
- rm -f "$archive"
27434
- return 1
27435
- fi
27436
- rm -f "$archive"
27437
- }
27438
-
27439
- supacloud_validate_tar() {
27440
- local artifact_file="$1"
27441
- local entries
27442
- entries=$(tar -tzf "$artifact_file") || {
27443
- echo "Web Console archive is not a readable gzip tarball" >&2
27444
- return 1
27445
- }
27446
- if ! printf '%s\\n' "$entries" | awk '
27447
- /^\\// { exit 1 }
27448
- /(^|\\/)\\.\\.($|\\/)/ { exit 1 }
27449
- '; then
27450
- echo "Web Console archive contains an unsafe path" >&2
27451
- return 1
27452
- fi
27453
- if ! tar -tvzf "$artifact_file" | awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }'; then
27454
- echo "Web Console archive contains links or special files" >&2
27455
- return 1
27456
- fi
27457
- printf '%s\\n' "$entries" | grep -Eq '(^|/)index\\.html$' || {
27458
- echo "Web Console archive is invalid or does not contain index.html" >&2
27459
- return 1
27460
- }
27461
- }
27462
-
27463
- supacloud_record_integrity_mode() {
27464
- local mode="$1"
27465
- local record_file="\${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"
27466
- mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0
27467
- printf '%s\\n' "$mode" > "$record_file" 2>/dev/null || return 0
27468
- chmod 600 "$record_file" 2>/dev/null || true
27469
- }
27470
-
27471
- supacloud_fetch_attestation_bundle() {
27472
- local artifact_file="$1"
27473
- local bundle_file="$2"
27474
- local digest response
27475
- digest=$(sha256sum "$artifact_file" | awk '{print $1}') || return 1
27476
- [[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {
27477
- echo "Unable to calculate the artifact digest for attestation lookup" >&2
27478
- return 1
27479
- }
27480
- digest=$(printf '%s' "$digest" | tr '[:upper:]' '[:lower:]')
27481
- response=$(supacloud_fetch_release_json \\
27482
- "https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:\${digest}") || {
27483
- echo "Unable to download the public GitHub artifact attestation bundle" >&2
27484
- return 1
27485
- }
27486
- if ! jq -ce '
27487
- .attestations
27488
- | if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")
27489
- then error("no valid attestation bundles returned")
27490
- else .[].bundle
27491
- end
27492
- ' <<< "$response" > "$bundle_file"; then
27493
- echo "GitHub artifact attestation response did not contain a valid bundle" >&2
27494
- return 1
27495
- fi
27496
- }
27497
-
27498
- supacloud_verify_attestation() (
27499
- local artifact_file="$1"
27500
- if supacloud_attestation_verifier_available; then
27501
- local verification_output bundle_dir bundle_file
27502
- bundle_dir=$(mktemp -d "\${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1
27503
- trap 'rm -rf -- "$bundle_dir"' EXIT
27504
- trap 'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1' HUP INT TERM
27505
- bundle_file="\${bundle_dir}/bundle.jsonl"
27506
- if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then
27507
- return 1
27508
- fi
27509
- if ! verification_output=$(gh attestation verify "$artifact_file" \\
27510
- --bundle "$bundle_file" \\
27511
- --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\
27512
- --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\
27513
- --source-ref "refs/heads/main" \\
27514
- --deny-self-hosted-runners 2>&1); then
27515
- echo "GitHub artifact attestation verification failed: \${verification_output}" >&2
27516
- return 1
27517
- fi
27518
- supacloud_record_integrity_mode "github-attestation+same-release-sha256"
27519
- return
27520
- fi
27521
-
27522
- if [[ "\${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then
27523
- echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2
27524
- supacloud_record_integrity_mode "break-glass:same-release-sha256-only"
27525
- return 0
27526
- fi
27527
-
27528
- 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
27529
- return 1
27530
- )
27531
-
27532
- supacloud_attestation_verifier_available() {
27533
- local version help
27534
- command -v gh >/dev/null 2>&1 || return 1
27535
- version=$(supacloud_gh_version)
27536
- [[ -n "$version" ]] || return 1
27537
- supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1
27538
- help=$(gh attestation verify --help 2>&1) || return 1
27539
- grep -Eq -- '(^|[[:space:]])--bundle([=[:space:]]|$)' <<< "$help" || return 1
27540
- grep -Eq -- '(^|[[:space:]])--signer-workflow([=[:space:]]|$)' <<< "$help" || return 1
27541
- grep -Eq -- '(^|[[:space:]])--source-ref([=[:space:]]|$)' <<< "$help" || return 1
27542
- grep -Eq -- '(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)' <<< "$help"
27543
- }
27544
-
27545
- supacloud_download_release_asset() (
27546
- local release_json="$1"
27547
- local asset_name="$2"
27548
- local destination="$3"
27549
- local asset_kind="$4"
27550
- local asset_url checksum_url temporary_artifact temporary_checksums
27551
-
27552
- asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1
27553
- checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1
27554
- mkdir -p "$(dirname "$destination")"
27555
- temporary_artifact=$(mktemp "\${destination}.tmp.XXXXXX")
27556
- temporary_checksums=$(mktemp "\${destination}.SHA256SUMS.tmp.XXXXXX")
27557
- trap 'rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"' EXIT
27558
- trap 'trap - EXIT HUP INT TERM; rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"; exit 1' HUP INT TERM
27559
-
27560
- if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\
27561
- || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\
27562
- || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then
27563
- rm -f "$temporary_artifact" "$temporary_checksums"
27564
- return 1
27565
- fi
27566
-
27567
- # Authenticate the digest before parsing archives or inspecting binaries.
27568
- if ! supacloud_verify_attestation "$temporary_artifact"; then
27569
- rm -f "$temporary_artifact" "$temporary_checksums"
27570
- return 1
27571
- fi
27572
-
27573
- case "$asset_kind" in
27574
- binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;
27575
- tar) supacloud_validate_tar "$temporary_artifact" ;;
27576
- *)
27577
- echo "Unknown release asset kind: $asset_kind" >&2
27578
- rm -f "$temporary_artifact" "$temporary_checksums"
27579
- return 1
27580
- ;;
27581
- esac || {
27582
- rm -f "$temporary_artifact" "$temporary_checksums"
27583
- return 1
27584
- }
27585
-
27586
- mv -f "$temporary_artifact" "$destination"
27587
- temporary_artifact=""
27588
- rm -f "$temporary_checksums"
27589
- temporary_checksums=""
27590
- )
27591
- `;
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';
27592
27270
 
27593
27271
  // src/shared/tools/ssh-tools.ts
27594
27272
  var SAFE_CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
@@ -27703,6 +27381,27 @@ function signalSafeCleanupTraps(cleanupCommand) {
27703
27381
  `trap ${quoteEnvValue(`trap - EXIT HUP INT TERM; ${cleanupCommand}; exit 1`)} HUP INT TERM`
27704
27382
  ];
27705
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
+ }
27706
27405
  function buildRootUpgradeScript(request) {
27707
27406
  const envAssignments = upgradeEnvAssignments(request);
27708
27407
  return [
@@ -27710,16 +27409,18 @@ function buildRootUpgradeScript(request) {
27710
27409
  "umask 077",
27711
27410
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
27712
27411
  "export PATH",
27713
- "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",
27714
27413
  request.githubProxy ? `export SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : "unset SUPACLOUD_GITHUB_PROXY SUPACLOUD_GITHUB_PROXIES",
27715
- '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',
27716
27415
  "test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
27717
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),
27718
27418
  buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
27719
27419
  ...componentPreflightCommands(request),
27720
27420
  "STAGED_MANAGEMENT=''",
27721
27421
  ...signalSafeCleanupTraps('test -z "$STAGED_MANAGEMENT" || rm -f "$STAGED_MANAGEMENT"'),
27722
27422
  `source ${quoteEnvValue(request.helperPath)}`,
27423
+ "supacloud_attestation_trusted_root_available || { echo 'Pinned Sigstore trusted root failed helper validation' >&2; exit 1; }",
27723
27424
  "if ! supacloud_attestation_verifier_available; then supacloud_install_pinned_gh /usr/local/bin/gh; fi",
27724
27425
  "supacloud_attestation_verifier_available || { echo 'Pinned GitHub attestation verifier is unavailable' >&2; exit 1; }",
27725
27426
  ...componentBootstrapCommands(request),
@@ -27741,8 +27442,7 @@ async function prepareRemoteUpgradeHelperDirectory(ssh, helperPath) {
27741
27442
  const command = [
27742
27443
  "set -e",
27743
27444
  "umask 077",
27744
- `test ! -e ${quoteEnvValue(helperDirectory)}`,
27745
- `install -d -m 700 -- ${quoteEnvValue(helperDirectory)}`
27445
+ `mkdir -m 700 -- ${quoteEnvValue(helperDirectory)}`
27746
27446
  ].join("; ");
27747
27447
  const preparation = await ssh.exec(command, 30000);
27748
27448
  if (!preparation.success) {
@@ -27769,44 +27469,56 @@ function remoteUpgradeFailure(execution) {
27769
27469
  const diagnostic = execution.stderr.trim() || execution.stdout.trim() || "no remote diagnostic";
27770
27470
  return new Error(`Remote upgrade failed (exit ${execution.code}): ${diagnostic.slice(-500)}`);
27771
27471
  }
27772
- function officialUpgradeOutcome(execution, executionError, cleanupError) {
27773
- if (executionError && cleanupError) {
27774
- 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");
27775
27475
  }
27776
- if (executionError)
27777
- throw executionError;
27778
- if (!execution)
27476
+ if (outcome.executionFailed)
27477
+ throw outcome.executionError;
27478
+ if (!outcome.execution)
27779
27479
  throw new Error("Upgrade execution did not return a result");
27780
- if (cleanupError && !execution.success) {
27781
- 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");
27782
27482
  }
27783
- if (cleanupError)
27784
- throw cleanupError;
27785
- if (!execution.success)
27786
- throw remoteUpgradeFailure(execution);
27787
- return execution;
27483
+ if (outcome.cleanupFailed)
27484
+ throw outcome.cleanupError;
27485
+ if (!outcome.execution.success)
27486
+ throw remoteUpgradeFailure(outcome.execution);
27487
+ return outcome.execution;
27788
27488
  }
27789
27489
  async function executeOfficialUpgrade(ssh, helperPath, command, timeoutMs) {
27790
27490
  let execution;
27491
+ let executionFailed = false;
27791
27492
  let executionError;
27792
27493
  let helperPrepared = false;
27793
27494
  try {
27794
27495
  await prepareRemoteUpgradeHelperDirectory(ssh, helperPath);
27795
27496
  helperPrepared = true;
27796
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);
27797
27500
  execution = await ssh.exec(command, timeoutMs);
27798
27501
  } catch (error) {
27502
+ executionFailed = true;
27799
27503
  executionError = error;
27800
27504
  }
27505
+ let cleanupFailed = false;
27801
27506
  let cleanupError;
27802
27507
  if (helperPrepared) {
27803
27508
  try {
27804
27509
  await removeRemoteUpgradeHelper(ssh, helperPath);
27805
27510
  } catch (error) {
27511
+ cleanupFailed = true;
27806
27512
  cleanupError = error;
27807
27513
  }
27808
27514
  }
27809
- return officialUpgradeOutcome(execution, executionError, cleanupError);
27515
+ return officialUpgradeOutcome({
27516
+ execution,
27517
+ executionFailed,
27518
+ executionError,
27519
+ cleanupFailed,
27520
+ cleanupError
27521
+ });
27810
27522
  }
27811
27523
  function assertSafeGithubProxy(value) {
27812
27524
  const trimmed = value.trim();
@@ -28029,7 +27741,7 @@ ${bootstrapDeps.stderr.slice(-500)}`;
28029
27741
  break;
28030
27742
  }
28031
27743
  const osCheck = await ssh.exec("cat /etc/os-release | grep -E 'NAME|VERSION_ID' | head -4");
28032
- 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);
28033
27745
  if (!clone2.stdout.includes("BOOTSTRAP_OK")) {
28034
27746
  await ssh.exec(`rm -rf ${quoteEnvValue(BOOTSTRAP)}`);
28035
27747
  text = `❌ Trusted bootstrap clone failed
@@ -28980,7 +28692,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
28980
28692
  // package.json
28981
28693
  var package_default = {
28982
28694
  name: "@supacloud/admin",
28983
- version: "0.7.10",
28695
+ version: "0.7.12",
28984
28696
  description: "Platform administration CLI for SupaCloud operators",
28985
28697
  type: "module",
28986
28698
  main: "./dist/index.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/admin",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "description": "Platform administration CLI for SupaCloud operators",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",