@supacloud/admin 0.7.11 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/dist/index.js +706 -551
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25702,8 +25702,10 @@ class HttpTransport {
|
|
|
25702
25702
|
}
|
|
25703
25703
|
|
|
25704
25704
|
// src/shared/tools/ssh-tools.ts
|
|
25705
|
+
import { Buffer as Buffer2 } from "node:buffer";
|
|
25705
25706
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
25706
|
-
import { dirname } from "node:path";
|
|
25707
|
+
import { basename as basename3, dirname, join as join2 } from "node:path";
|
|
25708
|
+
import { TextDecoder as TextDecoder2 } from "node:util";
|
|
25707
25709
|
|
|
25708
25710
|
// src/shared/releases/local-upgrade-transfer.ts
|
|
25709
25711
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
@@ -25735,10 +25737,10 @@ function quoteShell(shellText) {
|
|
|
25735
25737
|
|
|
25736
25738
|
// src/shared/releases/local-upgrade-bundle.ts
|
|
25737
25739
|
import { spawn } from "node:child_process";
|
|
25738
|
-
import { createHash, randomUUID as randomUUID2 } from "node:crypto";
|
|
25739
|
-
import { accessSync, chmodSync, constants as fsConstants, createWriteStream, mkdirSync, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync, writeFileSync } from "node:fs";
|
|
25740
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
|
|
25741
|
+
import { accessSync, chmodSync as chmodSync2, constants as fsConstants, createWriteStream, lstatSync, mkdirSync, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, readdirSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
25740
25742
|
import { get } from "node:https";
|
|
25741
|
-
import { tmpdir } from "node:os";
|
|
25743
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
25742
25744
|
import { basename, delimiter, join } from "node:path";
|
|
25743
25745
|
import { pipeline } from "node:stream/promises";
|
|
25744
25746
|
import { Transform as Transform2 } from "node:stream";
|
|
@@ -25905,6 +25907,97 @@ function parseReleaseChecksums(text, manifest) {
|
|
|
25905
25907
|
return checksums;
|
|
25906
25908
|
}
|
|
25907
25909
|
|
|
25910
|
+
// ../management-api/src/sigstore-trusted-root.ts
|
|
25911
|
+
import { createHash } from "node:crypto";
|
|
25912
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
25913
|
+
import { tmpdir } from "node:os";
|
|
25914
|
+
import path from "node:path";
|
|
25915
|
+
|
|
25916
|
+
// ../management-api/src/assets/sigstore-public-good-trusted-root.jsonl
|
|
25917
|
+
var sigstore_public_good_trusted_root_default = '{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1","tlogs":[{"baseUrl":"https://rekor.sigstore.dev","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-01-12T11:53:27Z"}},"logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="}},{"baseUrl":"https://log2025-1.rekor.sigstore.dev","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=","keyDetails":"PKIX_ED25519","validFor":{"start":"2025-09-23T00:00:00Z"}},"logId":{"keyId":"zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A="}}],"certificateAuthorities":[{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="}]},"validFor":{"start":"2021-03-07T03:20:29Z","end":"2022-12-31T23:59:59.999Z"}},{"subject":{"organization":"sigstore.dev","commonName":"sigstore"},"uri":"https://fulcio.sigstore.dev","certChain":{"certificates":[{"rawBytes":"MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="},{"rawBytes":"MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"}]},"validFor":{"start":"2022-04-13T20:06:15Z"}}],"ctlogs":[{"baseUrl":"https://ctfe.sigstore.dev/test","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2021-03-14T00:00:00Z","end":"2022-10-31T23:59:59.999Z"}},"logId":{"keyId":"CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="}},{"baseUrl":"https://ctfe.sigstore.dev/2022","hashAlgorithm":"SHA2_256","publicKey":{"rawBytes":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==","keyDetails":"PKIX_ECDSA_P256_SHA_256","validFor":{"start":"2022-10-20T00:00:00Z"}},"logId":{"keyId":"3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="}}],"timestampAuthorities":[{"subject":{"organization":"sigstore.dev","commonName":"sigstore-tsa-selfsigned"},"uri":"https://timestamp.sigstore.dev/api/v1/timestamp","certChain":{"certificates":[{"rawBytes":"MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw=="},{"rawBytes":"MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E"}]},"validFor":{"start":"2025-07-04T00:00:00Z"}}]}\n';
|
|
25918
|
+
|
|
25919
|
+
// ../management-api/src/sigstore-trusted-root.ts
|
|
25920
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME = "trusted_root.jsonl";
|
|
25921
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256 = "3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1";
|
|
25922
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE = 5748;
|
|
25923
|
+
function sha256(text) {
|
|
25924
|
+
return createHash("sha256").update(text).digest("hex");
|
|
25925
|
+
}
|
|
25926
|
+
function validateEmbeddedTrustedRoot(jsonl) {
|
|
25927
|
+
if (Buffer.byteLength(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256(jsonl) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
|
|
25928
|
+
throw new Error("Embedded Sigstore Public Good trusted root does not match its pinned digest");
|
|
25929
|
+
}
|
|
25930
|
+
if (!jsonl.endsWith(`
|
|
25931
|
+
`) || jsonl.slice(0, -1).includes(`
|
|
25932
|
+
`)) {
|
|
25933
|
+
throw new Error("Embedded Sigstore Public Good trusted root must be compact JSONL");
|
|
25934
|
+
}
|
|
25935
|
+
let trustedRoot;
|
|
25936
|
+
try {
|
|
25937
|
+
trustedRoot = JSON.parse(jsonl);
|
|
25938
|
+
} catch {
|
|
25939
|
+
throw new Error("Embedded Sigstore Public Good trusted root is not valid JSON");
|
|
25940
|
+
}
|
|
25941
|
+
if (!trustedRoot || typeof trustedRoot !== "object" || Array.isArray(trustedRoot) || `${JSON.stringify(trustedRoot)}
|
|
25942
|
+
` !== jsonl) {
|
|
25943
|
+
throw new Error("Embedded Sigstore Public Good trusted root is not canonical compact JSONL");
|
|
25944
|
+
}
|
|
25945
|
+
return jsonl;
|
|
25946
|
+
}
|
|
25947
|
+
var SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL = validateEmbeddedTrustedRoot(sigstore_public_good_trusted_root_default);
|
|
25948
|
+
function createSigstoreVerificationDirectory() {
|
|
25949
|
+
const directory = mkdtempSync(path.join(tmpdir(), "supacloud-sigstore-"));
|
|
25950
|
+
try {
|
|
25951
|
+
chmodSync(directory, 448);
|
|
25952
|
+
const trustedRootPath = path.join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
25953
|
+
writeFileSync(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, {
|
|
25954
|
+
flag: "wx",
|
|
25955
|
+
mode: 384
|
|
25956
|
+
});
|
|
25957
|
+
chmodSync(trustedRootPath, 384);
|
|
25958
|
+
return { directory, trustedRootPath };
|
|
25959
|
+
} catch (operationError) {
|
|
25960
|
+
try {
|
|
25961
|
+
rmSync(directory, { recursive: true });
|
|
25962
|
+
} catch (cleanupError) {
|
|
25963
|
+
throw new AggregateError([operationError, cleanupError], "Sigstore trusted root creation failed and temporary directory cleanup did not complete");
|
|
25964
|
+
}
|
|
25965
|
+
throw operationError;
|
|
25966
|
+
}
|
|
25967
|
+
}
|
|
25968
|
+
function throwVerificationDirectoryOutcome(outcome) {
|
|
25969
|
+
if (outcome.operationFailed && outcome.cleanupFailed) {
|
|
25970
|
+
throw new AggregateError([outcome.operationError, outcome.cleanupError], "Sigstore verification failed and temporary trusted root cleanup did not complete");
|
|
25971
|
+
}
|
|
25972
|
+
if (outcome.operationFailed)
|
|
25973
|
+
throw outcome.operationError;
|
|
25974
|
+
if (outcome.cleanupFailed) {
|
|
25975
|
+
throw new AggregateError([outcome.cleanupError], "Temporary Sigstore trusted root cleanup did not complete");
|
|
25976
|
+
}
|
|
25977
|
+
}
|
|
25978
|
+
async function withSigstoreVerificationDirectory(operation) {
|
|
25979
|
+
const resources = createSigstoreVerificationDirectory();
|
|
25980
|
+
let operationOutput;
|
|
25981
|
+
let operationFailed = false;
|
|
25982
|
+
let operationError;
|
|
25983
|
+
try {
|
|
25984
|
+
operationOutput = await operation(resources);
|
|
25985
|
+
} catch (error) {
|
|
25986
|
+
operationFailed = true;
|
|
25987
|
+
operationError = error;
|
|
25988
|
+
}
|
|
25989
|
+
let cleanupFailed = false;
|
|
25990
|
+
let cleanupError;
|
|
25991
|
+
try {
|
|
25992
|
+
rmSync(resources.directory, { recursive: true });
|
|
25993
|
+
} catch (error) {
|
|
25994
|
+
cleanupFailed = true;
|
|
25995
|
+
cleanupError = error;
|
|
25996
|
+
}
|
|
25997
|
+
throwVerificationDirectoryOutcome({ operationFailed, operationError, cleanupFailed, cleanupError });
|
|
25998
|
+
return operationOutput;
|
|
25999
|
+
}
|
|
26000
|
+
|
|
25908
26001
|
// src/shared/releases/local-upgrade-bundle.ts
|
|
25909
26002
|
var RELEASES_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/releases`;
|
|
25910
26003
|
var ATTESTATIONS_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/attestations`;
|
|
@@ -25938,7 +26031,8 @@ var STRICT_GITHUB_CAPABILITY_FLAGS = [
|
|
|
25938
26031
|
"--signer-workflow",
|
|
25939
26032
|
"--source-ref",
|
|
25940
26033
|
"--source-digest",
|
|
25941
|
-
"--deny-self-hosted-runners"
|
|
26034
|
+
"--deny-self-hosted-runners",
|
|
26035
|
+
"--custom-trusted-root"
|
|
25942
26036
|
];
|
|
25943
26037
|
|
|
25944
26038
|
class RetryableDownloadError extends Error {
|
|
@@ -25953,7 +26047,56 @@ function githubCliArchiveIdentity(architecture) {
|
|
|
25953
26047
|
};
|
|
25954
26048
|
}
|
|
25955
26049
|
function sha256File(filePath) {
|
|
25956
|
-
return
|
|
26050
|
+
return createHash2("sha256").update(readFileSync3(filePath)).digest("hex");
|
|
26051
|
+
}
|
|
26052
|
+
function effectiveLocalOwner() {
|
|
26053
|
+
if (process.platform === "win32")
|
|
26054
|
+
return;
|
|
26055
|
+
const uid = process.geteuid?.() ?? process.getuid?.();
|
|
26056
|
+
const gid = process.getegid?.() ?? process.getgid?.();
|
|
26057
|
+
if (uid === undefined || gid === undefined) {
|
|
26058
|
+
throw new Error("Local artifact transport requires POSIX ownership checks");
|
|
26059
|
+
}
|
|
26060
|
+
return { uid, gid };
|
|
26061
|
+
}
|
|
26062
|
+
function assertLocalOwner(uid, gid, label) {
|
|
26063
|
+
const owner = effectiveLocalOwner();
|
|
26064
|
+
if (!owner)
|
|
26065
|
+
return;
|
|
26066
|
+
if (uid !== owner.uid || gid !== owner.gid) {
|
|
26067
|
+
throw new Error(`${label} must be owned by the current effective user`);
|
|
26068
|
+
}
|
|
26069
|
+
}
|
|
26070
|
+
function assertLocalTrustedRootDirectory(directory) {
|
|
26071
|
+
const directoryStats = lstatSync(directory);
|
|
26072
|
+
if (!directoryStats.isDirectory() || directoryStats.isSymbolicLink()) {
|
|
26073
|
+
throw new Error("Local trusted-root directory must be a directory without links");
|
|
26074
|
+
}
|
|
26075
|
+
assertPrivateLocalDirectoryMode(directoryStats.mode, "Local trusted-root directory");
|
|
26076
|
+
assertLocalOwner(directoryStats.uid, directoryStats.gid, "Local trusted-root directory");
|
|
26077
|
+
const entries = readdirSync(directory);
|
|
26078
|
+
if (entries.length !== 1 || entries[0] !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME) {
|
|
26079
|
+
throw new Error("Local trusted-root directory does not match its strict file allowlist");
|
|
26080
|
+
}
|
|
26081
|
+
const trustedRootPath = join(directory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
26082
|
+
assertLocalTrustedRootFile(trustedRootPath);
|
|
26083
|
+
return trustedRootPath;
|
|
26084
|
+
}
|
|
26085
|
+
function assertPrivateLocalDirectoryMode(mode, label) {
|
|
26086
|
+
if (process.platform !== "win32" && (mode & 4095) !== 448) {
|
|
26087
|
+
throw new Error(`${label} must use exact mode 0700 without special permission bits`);
|
|
26088
|
+
}
|
|
26089
|
+
}
|
|
26090
|
+
function assertLocalTrustedRootFile(trustedRootPath) {
|
|
26091
|
+
const stats = lstatSync(trustedRootPath);
|
|
26092
|
+
if (!stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
26093
|
+
throw new Error("Local trusted root must be a direct regular file without links");
|
|
26094
|
+
}
|
|
26095
|
+
assertPrivateLocalFileMode(stats.mode, "Local trusted root");
|
|
26096
|
+
assertLocalOwner(stats.uid, stats.gid, "Local trusted root");
|
|
26097
|
+
if (stats.size !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE || sha256File(trustedRootPath) !== SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256) {
|
|
26098
|
+
throw new Error("Local trusted root does not match its pinned size and digest");
|
|
26099
|
+
}
|
|
25957
26100
|
}
|
|
25958
26101
|
function assertPrivateLocalFileMode(mode, label) {
|
|
25959
26102
|
if (process.platform !== "win32" && (mode & 4095) !== 384) {
|
|
@@ -26055,10 +26198,10 @@ async function downloadDirect(url, destination, maxBytes) {
|
|
|
26055
26198
|
const deadline = Date.now() + DOWNLOAD_TIMEOUT_MS;
|
|
26056
26199
|
const retryFailures = [];
|
|
26057
26200
|
for (let attempt = 1;attempt <= 3; attempt += 1) {
|
|
26058
|
-
|
|
26201
|
+
rmSync2(destination, { force: true });
|
|
26059
26202
|
try {
|
|
26060
26203
|
await downloadHttpsResponse({ url: parsed, destination, maxBytes, redirects: 0, deadline });
|
|
26061
|
-
|
|
26204
|
+
chmodSync2(destination, 384);
|
|
26062
26205
|
return;
|
|
26063
26206
|
} catch (error) {
|
|
26064
26207
|
if (!downloadCanRetry(error))
|
|
@@ -26214,13 +26357,13 @@ async function assertLocalGithubVerifier() {
|
|
|
26214
26357
|
throw new Error("Local artifact transport requires a current gh attestation verifier");
|
|
26215
26358
|
}
|
|
26216
26359
|
}
|
|
26217
|
-
|
|
26218
|
-
|
|
26360
|
+
function githubAttestationVerificationArguments(request) {
|
|
26361
|
+
return [
|
|
26219
26362
|
"attestation",
|
|
26220
26363
|
"verify",
|
|
26221
|
-
|
|
26364
|
+
request.artifactPath,
|
|
26222
26365
|
"--bundle",
|
|
26223
|
-
bundlePath,
|
|
26366
|
+
request.bundlePath,
|
|
26224
26367
|
"--repo",
|
|
26225
26368
|
RELEASE_REPOSITORY,
|
|
26226
26369
|
"--signer-workflow",
|
|
@@ -26228,9 +26371,14 @@ async function verifyManifestAttestation(manifestPath, bundlePath, manifest) {
|
|
|
26228
26371
|
"--source-ref",
|
|
26229
26372
|
RELEASE_SOURCE_REF,
|
|
26230
26373
|
"--source-digest",
|
|
26231
|
-
manifest.source.commit,
|
|
26232
|
-
"--deny-self-hosted-runners"
|
|
26233
|
-
|
|
26374
|
+
request.manifest.source.commit,
|
|
26375
|
+
"--deny-self-hosted-runners",
|
|
26376
|
+
"--custom-trusted-root",
|
|
26377
|
+
request.trustedRootPath
|
|
26378
|
+
];
|
|
26379
|
+
}
|
|
26380
|
+
async function verifyManifestAttestation(request) {
|
|
26381
|
+
const verification = await runGithubCli(githubAttestationVerificationArguments(request), GH_VERIFICATION_TIMEOUT_MS);
|
|
26234
26382
|
if (verification.exitCode !== 0) {
|
|
26235
26383
|
throw new Error(`gh attestation verify failed: ${verification.stderr.trim().slice(-1000) || verification.exitCode}`);
|
|
26236
26384
|
}
|
|
@@ -26261,7 +26409,7 @@ async function downloadReleaseMetadata(component, version, directory) {
|
|
|
26261
26409
|
await downloadDirect(`${RELEASES_API}/tags/${expectedTag}`, metadataPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
|
|
26262
26410
|
return parseGithubReleaseMetadata(parseJsonFile(metadataPath, "GitHub release metadata"), expectedTag);
|
|
26263
26411
|
} finally {
|
|
26264
|
-
|
|
26412
|
+
rmSync2(metadataPath, { force: true });
|
|
26265
26413
|
}
|
|
26266
26414
|
}
|
|
26267
26415
|
async function downloadManifestAttestation(manifestPath, destination) {
|
|
@@ -26269,10 +26417,10 @@ async function downloadManifestAttestation(manifestPath, destination) {
|
|
|
26269
26417
|
try {
|
|
26270
26418
|
await downloadDirect(`${ATTESTATIONS_API}/sha256:${sha256File(manifestPath)}`, responsePath, RELEASE_BUNDLE_SIZE_LIMITS.attestation);
|
|
26271
26419
|
const bundles = serializeAttestationBundles(parseJsonFile(responsePath, "GitHub attestation response"));
|
|
26272
|
-
|
|
26273
|
-
|
|
26420
|
+
writeFileSync2(destination, bundles, { mode: 384, flag: "wx" });
|
|
26421
|
+
chmodSync2(destination, 384);
|
|
26274
26422
|
} finally {
|
|
26275
|
-
|
|
26423
|
+
rmSync2(responsePath, { force: true });
|
|
26276
26424
|
}
|
|
26277
26425
|
}
|
|
26278
26426
|
async function downloadComponent(request) {
|
|
@@ -26282,7 +26430,12 @@ async function downloadComponent(request) {
|
|
|
26282
26430
|
await downloadDirect(releaseAssetUrl(release, RELEASE_MANIFEST_NAME), manifestPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
|
|
26283
26431
|
const manifest = parseReleaseManifest(readFileSync3(manifestPath, "utf8"), request);
|
|
26284
26432
|
await downloadManifestAttestation(manifestPath, attestationPath);
|
|
26285
|
-
await verifyManifestAttestation(
|
|
26433
|
+
await verifyManifestAttestation({
|
|
26434
|
+
artifactPath: manifestPath,
|
|
26435
|
+
bundlePath: attestationPath,
|
|
26436
|
+
manifest,
|
|
26437
|
+
trustedRootPath: request.trustedRootPath
|
|
26438
|
+
});
|
|
26286
26439
|
const checksumsPath = directChildPath(request.destination, RELEASE_CHECKSUMS_NAME);
|
|
26287
26440
|
await downloadDirect(releaseAssetUrl(release, RELEASE_CHECKSUMS_NAME), checksumsPath, RELEASE_BUNDLE_SIZE_LIMITS.checksums);
|
|
26288
26441
|
assertSignedArtifact(checksumsPath, manifest);
|
|
@@ -26305,7 +26458,7 @@ async function downloadPinnedGithubCli(directory, architecture) {
|
|
|
26305
26458
|
return localUpgradeFile(archivePath, `verifier/${identity.archiveName}`);
|
|
26306
26459
|
}
|
|
26307
26460
|
function cleanupLocalUpgradeBundle(bundle) {
|
|
26308
|
-
|
|
26461
|
+
rmSync2(bundle.directory, { recursive: true, force: true });
|
|
26309
26462
|
}
|
|
26310
26463
|
function assertLocalUpgradeBundleSize(files) {
|
|
26311
26464
|
const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
@@ -26342,15 +26495,15 @@ async function settleLocalBundleDownloads(downloads) {
|
|
|
26342
26495
|
];
|
|
26343
26496
|
}
|
|
26344
26497
|
function createLocalBundleLayout(request) {
|
|
26345
|
-
const directory =
|
|
26346
|
-
|
|
26498
|
+
const directory = mkdtempSync2(join(tmpdir2(), "supacloud-admin-upgrade-"));
|
|
26499
|
+
chmodSync2(directory, 448);
|
|
26347
26500
|
const bundleDirectory = join(directory, "bundle");
|
|
26348
26501
|
const managementDirectory = join(bundleDirectory, "management-api");
|
|
26349
26502
|
const edgeDirectory = join(bundleDirectory, "edge-runtime");
|
|
26350
26503
|
const verifierDirectory = join(directory, "verifier");
|
|
26351
26504
|
for (const createdDirectory of [bundleDirectory, managementDirectory, edgeDirectory, verifierDirectory]) {
|
|
26352
26505
|
mkdirSync(createdDirectory, { recursive: true, mode: 448 });
|
|
26353
|
-
|
|
26506
|
+
chmodSync2(createdDirectory, 448);
|
|
26354
26507
|
}
|
|
26355
26508
|
return {
|
|
26356
26509
|
directory,
|
|
@@ -26361,20 +26514,22 @@ function createLocalBundleLayout(request) {
|
|
|
26361
26514
|
edgeRuntimeBinaryName: `supacloud-edge-runtime-linux-${request.architecture}`
|
|
26362
26515
|
};
|
|
26363
26516
|
}
|
|
26364
|
-
async function downloadLocalBundle(request, layout) {
|
|
26517
|
+
async function downloadLocalBundle(request, layout, trustedRootPath) {
|
|
26365
26518
|
const { managementBinaryName, edgeRuntimeBinaryName } = layout;
|
|
26366
26519
|
const [managementFiles, edgeFiles, verifierArchive] = await settleLocalBundleDownloads([
|
|
26367
26520
|
downloadComponent({
|
|
26368
26521
|
component: "management-api",
|
|
26369
26522
|
version: request.managementVersion,
|
|
26370
26523
|
assetNames: [managementBinaryName, "web-console-build.tar.gz"],
|
|
26371
|
-
destination: layout.managementDirectory
|
|
26524
|
+
destination: layout.managementDirectory,
|
|
26525
|
+
trustedRootPath
|
|
26372
26526
|
}),
|
|
26373
26527
|
downloadComponent({
|
|
26374
26528
|
component: "edge-runtime",
|
|
26375
26529
|
version: request.edgeRuntimeVersion,
|
|
26376
26530
|
assetNames: [edgeRuntimeBinaryName],
|
|
26377
|
-
destination: layout.edgeDirectory
|
|
26531
|
+
destination: layout.edgeDirectory,
|
|
26532
|
+
trustedRootPath
|
|
26378
26533
|
}),
|
|
26379
26534
|
request.verifierProvisioning === "bundled" ? downloadPinnedGithubCli(layout.verifierDirectory, request.architecture) : Promise.resolve(null)
|
|
26380
26535
|
]);
|
|
@@ -26388,9 +26543,16 @@ async function prepareLocalUpgradeBundle(request) {
|
|
|
26388
26543
|
await assertLocalGithubVerifier();
|
|
26389
26544
|
const layout = createLocalBundleLayout(request);
|
|
26390
26545
|
try {
|
|
26391
|
-
return await
|
|
26546
|
+
return await withSigstoreVerificationDirectory(async (verificationDirectory) => {
|
|
26547
|
+
const trustedRootPath = assertLocalTrustedRootDirectory(verificationDirectory.directory);
|
|
26548
|
+
return await downloadLocalBundle(request, layout, trustedRootPath);
|
|
26549
|
+
});
|
|
26392
26550
|
} catch (error) {
|
|
26393
|
-
|
|
26551
|
+
try {
|
|
26552
|
+
rmSync2(layout.directory, { recursive: true, force: true });
|
|
26553
|
+
} catch (cleanupError) {
|
|
26554
|
+
throw new AggregateError([error, cleanupError], "Local bundle preparation failed and cleanup did not complete");
|
|
26555
|
+
}
|
|
26394
26556
|
throw error;
|
|
26395
26557
|
}
|
|
26396
26558
|
}
|
|
@@ -26606,7 +26768,7 @@ function upgradeScriptSetup(paths) {
|
|
|
26606
26768
|
"write_status RUNNING",
|
|
26607
26769
|
buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
|
|
26608
26770
|
`while IFS='=' read -r variable _; do case "$variable" in *PROXY*|*Proxy*|*proxy*) unset "$variable" ;; esac; done < <(env)`,
|
|
26609
|
-
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW NODE_USE_ENV_PROXY"
|
|
26771
|
+
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_ATTESTATION_TRUSTED_ROOT NODE_USE_ENV_PROXY"
|
|
26610
26772
|
];
|
|
26611
26773
|
}
|
|
26612
26774
|
function upgradeScriptFilesystemChecks(bundle) {
|
|
@@ -27106,493 +27268,7 @@ async function executeLocalUpgradeTransfer(ssh, request) {
|
|
|
27106
27268
|
}
|
|
27107
27269
|
|
|
27108
27270
|
// ../../scripts/lib/release_assets.sh
|
|
27109
|
-
var release_assets_default = `#!/usr/bin/env bash
|
|
27110
|
-
|
|
27111
|
-
SUPACLOUD_GITHUB_REPOSITORY="\${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"
|
|
27112
|
-
SUPACLOUD_RELEASES_API="\${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/releases}"
|
|
27113
|
-
SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="\${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-\${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"
|
|
27114
|
-
SUPACLOUD_GH_VERSION="\${SUPACLOUD_GH_VERSION:-2.96.0}"
|
|
27115
|
-
SUPACLOUD_GH_MIN_VERSION="\${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"
|
|
27116
|
-
SUPACLOUD_GH_AMD64_SHA256="\${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"
|
|
27117
|
-
SUPACLOUD_GH_ARM64_SHA256="\${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"
|
|
27118
|
-
|
|
27119
|
-
supacloud_curl_release_json() {
|
|
27120
|
-
local url="$1"
|
|
27121
|
-
local output="$2"
|
|
27122
|
-
curl -fsSL --proto '=https' --proto-redir '=https' \\
|
|
27123
|
-
--retry 1 --retry-delay 2 --retry-max-time 60 \\
|
|
27124
|
-
--connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\
|
|
27125
|
-
-o "$output" "$url"
|
|
27126
|
-
}
|
|
27127
|
-
|
|
27128
|
-
supacloud_curl_release_asset() {
|
|
27129
|
-
local url="$1"
|
|
27130
|
-
local output="$2"
|
|
27131
|
-
curl -fL --proto '=https' --proto-redir '=https' \\
|
|
27132
|
-
--retry 1 --retry-delay 2 --retry-max-time 180 \\
|
|
27133
|
-
--connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\
|
|
27134
|
-
-o "$output" "$url"
|
|
27135
|
-
}
|
|
27136
|
-
|
|
27137
|
-
supacloud_component_tag() {
|
|
27138
|
-
local component="$1"
|
|
27139
|
-
local version="$2"
|
|
27140
|
-
case "$version" in
|
|
27141
|
-
"\${component}-v"*) printf '%s' "$version" ;;
|
|
27142
|
-
v*) printf '%s-%s' "$component" "$version" ;;
|
|
27143
|
-
*) printf '%s-v%s' "$component" "$version" ;;
|
|
27144
|
-
esac
|
|
27145
|
-
}
|
|
27146
|
-
|
|
27147
|
-
supacloud_select_release() {
|
|
27148
|
-
local component="$1"
|
|
27149
|
-
shift
|
|
27150
|
-
local required_assets_json
|
|
27151
|
-
[[ $# -gt 0 ]] || {
|
|
27152
|
-
echo "at least one required release asset must be specified" >&2
|
|
27153
|
-
return 1
|
|
27154
|
-
}
|
|
27155
|
-
required_assets_json=$(printf '%s\\n' "$@" | jq -Rsc 'split("\\n")[:-1]')
|
|
27156
|
-
jq -ce --arg prefix "\${component}-v" --argjson required "$required_assets_json" '
|
|
27157
|
-
map(select(
|
|
27158
|
-
(.draft | not)
|
|
27159
|
-
and (.prerelease | not)
|
|
27160
|
-
and (.tag_name | startswith($prefix))
|
|
27161
|
-
and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
|
|
27162
|
-
and any(.assets[]?; .name == "SHA256SUMS")
|
|
27163
|
-
))
|
|
27164
|
-
| first
|
|
27165
|
-
// error("no matching component release contains all required assets and SHA256SUMS")
|
|
27166
|
-
'
|
|
27167
|
-
}
|
|
27168
|
-
|
|
27169
|
-
supacloud_fetch_component_release() {
|
|
27170
|
-
local component="$1"
|
|
27171
|
-
local version="\${2:-latest}"
|
|
27172
|
-
shift 2
|
|
27173
|
-
local required_assets=("$@")
|
|
27174
|
-
local required_assets_json
|
|
27175
|
-
local response
|
|
27176
|
-
[[ \${#required_assets[@]} -gt 0 ]] || {
|
|
27177
|
-
echo "at least one required release asset must be specified" >&2
|
|
27178
|
-
return 1
|
|
27179
|
-
}
|
|
27180
|
-
required_assets_json=$(printf '%s\\n' "\${required_assets[@]}" | jq -Rsc 'split("\\n")[:-1]')
|
|
27181
|
-
|
|
27182
|
-
if [[ -n "$version" && "$version" != "latest" ]]; then
|
|
27183
|
-
local tag
|
|
27184
|
-
tag=$(supacloud_component_tag "$component" "$version")
|
|
27185
|
-
response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}/tags/\${tag}") || return 1
|
|
27186
|
-
jq -ce --argjson required "$required_assets_json" '
|
|
27187
|
-
select(
|
|
27188
|
-
(.draft | not)
|
|
27189
|
-
and (.prerelease | not)
|
|
27190
|
-
and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))
|
|
27191
|
-
and any(.assets[]?; .name == "SHA256SUMS")
|
|
27192
|
-
)
|
|
27193
|
-
// error("release does not contain all required assets and SHA256SUMS")
|
|
27194
|
-
' <<< "$response"
|
|
27195
|
-
return
|
|
27196
|
-
fi
|
|
27197
|
-
|
|
27198
|
-
response=$(supacloud_fetch_release_json "\${SUPACLOUD_RELEASES_API}?per_page=100") || return 1
|
|
27199
|
-
supacloud_select_release "$component" "\${required_assets[@]}" <<< "$response"
|
|
27200
|
-
}
|
|
27201
|
-
|
|
27202
|
-
supacloud_fetch_release_json() (
|
|
27203
|
-
local url="$1"
|
|
27204
|
-
local response_file
|
|
27205
|
-
response_file=$(mktemp) || return 1
|
|
27206
|
-
trap 'rm -f "$response_file"' EXIT
|
|
27207
|
-
trap 'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1' HUP INT TERM
|
|
27208
|
-
supacloud_download_release_metadata_url "$url" "$response_file" || return 1
|
|
27209
|
-
cat "$response_file"
|
|
27210
|
-
)
|
|
27211
|
-
|
|
27212
|
-
supacloud_release_asset_url() {
|
|
27213
|
-
local release_json="$1"
|
|
27214
|
-
local asset_name="$2"
|
|
27215
|
-
jq -er --arg asset "$asset_name" '
|
|
27216
|
-
first(.assets[]? | select(.name == $asset) | .browser_download_url)
|
|
27217
|
-
// error("release asset URL is missing")
|
|
27218
|
-
' <<< "$release_json"
|
|
27219
|
-
}
|
|
27220
|
-
|
|
27221
|
-
supacloud_download_url() {
|
|
27222
|
-
local url="$1"
|
|
27223
|
-
local output="$2"
|
|
27224
|
-
local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
|
|
27225
|
-
|
|
27226
|
-
if supacloud_curl_release_asset "$url" "$output"; then
|
|
27227
|
-
return 0
|
|
27228
|
-
fi
|
|
27229
|
-
if [[ -n "$proxy" ]]; then
|
|
27230
|
-
supacloud_curl_release_asset "\${proxy%/}/\${url}" "$output"
|
|
27231
|
-
return
|
|
27232
|
-
fi
|
|
27233
|
-
return 1
|
|
27234
|
-
}
|
|
27235
|
-
|
|
27236
|
-
supacloud_download_release_metadata_url() {
|
|
27237
|
-
local url="$1"
|
|
27238
|
-
local output="$2"
|
|
27239
|
-
local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
|
|
27240
|
-
|
|
27241
|
-
if supacloud_curl_release_json "$url" "$output"; then
|
|
27242
|
-
return 0
|
|
27243
|
-
fi
|
|
27244
|
-
if [[ -n "$proxy" ]]; then
|
|
27245
|
-
supacloud_curl_release_json "\${proxy%/}/\${url}" "$output"
|
|
27246
|
-
return
|
|
27247
|
-
fi
|
|
27248
|
-
return 1
|
|
27249
|
-
}
|
|
27250
|
-
|
|
27251
|
-
supacloud_verify_checksum() {
|
|
27252
|
-
local artifact_file="$1"
|
|
27253
|
-
local asset_name="$2"
|
|
27254
|
-
local checksum_file="$3"
|
|
27255
|
-
local expected
|
|
27256
|
-
expected=$(awk -v asset="$asset_name" '$2 == asset || $2 == "*" asset { print $1; exit }' "$checksum_file")
|
|
27257
|
-
if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then
|
|
27258
|
-
echo "SHA256SUMS does not contain a valid checksum for \${asset_name}" >&2
|
|
27259
|
-
return 1
|
|
27260
|
-
fi
|
|
27261
|
-
|
|
27262
|
-
local actual
|
|
27263
|
-
actual=$(sha256sum "$artifact_file" | awk '{print $1}')
|
|
27264
|
-
actual=$(printf '%s' "$actual" | tr '[:upper:]' '[:lower:]')
|
|
27265
|
-
expected=$(printf '%s' "$expected" | tr '[:upper:]' '[:lower:]')
|
|
27266
|
-
if [[ "$actual" != "$expected" ]]; then
|
|
27267
|
-
echo "SHA256 mismatch for \${asset_name}" >&2
|
|
27268
|
-
return 1
|
|
27269
|
-
fi
|
|
27270
|
-
}
|
|
27271
|
-
|
|
27272
|
-
supacloud_validate_binary() {
|
|
27273
|
-
local artifact_file="$1"
|
|
27274
|
-
local asset_name="$2"
|
|
27275
|
-
local description
|
|
27276
|
-
description=$(file -b "$artifact_file")
|
|
27277
|
-
if [[ "$description" != *ELF* ]]; then
|
|
27278
|
-
echo "\${asset_name} is not an ELF binary: \${description}" >&2
|
|
27279
|
-
return 1
|
|
27280
|
-
fi
|
|
27281
|
-
|
|
27282
|
-
case "$asset_name" in
|
|
27283
|
-
*amd64)
|
|
27284
|
-
[[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {
|
|
27285
|
-
echo "\${asset_name} does not contain an x86-64 ELF binary" >&2
|
|
27286
|
-
return 1
|
|
27287
|
-
}
|
|
27288
|
-
;;
|
|
27289
|
-
*arm64)
|
|
27290
|
-
[[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {
|
|
27291
|
-
echo "\${asset_name} does not contain an arm64 ELF binary" >&2
|
|
27292
|
-
return 1
|
|
27293
|
-
}
|
|
27294
|
-
;;
|
|
27295
|
-
esac
|
|
27296
|
-
}
|
|
27297
|
-
|
|
27298
|
-
supacloud_install_pinned_tar_xz_binary() (
|
|
27299
|
-
local archive="$1"
|
|
27300
|
-
local member="$2"
|
|
27301
|
-
local expected_sha256="$3"
|
|
27302
|
-
local arch="$4"
|
|
27303
|
-
local target="$5"
|
|
27304
|
-
local actual_sha256 member_count member_details extract_dir candidate staged_target
|
|
27305
|
-
|
|
27306
|
-
actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
|
|
27307
|
-
actual_sha256=$(printf '%s' "$actual_sha256" | tr '[:upper:]' '[:lower:]')
|
|
27308
|
-
expected_sha256=$(printf '%s' "$expected_sha256" | tr '[:upper:]' '[:lower:]')
|
|
27309
|
-
if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then
|
|
27310
|
-
echo "SHA256 mismatch for pinned archive" >&2
|
|
27311
|
-
return 1
|
|
27312
|
-
fi
|
|
27313
|
-
|
|
27314
|
-
member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)
|
|
27315
|
-
if [[ "$member_count" != "1" ]]; then
|
|
27316
|
-
echo "Pinned archive must contain the exact member once: $member" >&2
|
|
27317
|
-
return 1
|
|
27318
|
-
fi
|
|
27319
|
-
member_details=$(tar -tvJf "$archive" "$member") || return 1
|
|
27320
|
-
if [[ "\${member_details:0:1}" != "-" ]]; then
|
|
27321
|
-
echo "Pinned archive member is not a regular file: $member" >&2
|
|
27322
|
-
return 1
|
|
27323
|
-
fi
|
|
27324
|
-
|
|
27325
|
-
extract_dir=$(mktemp -d)
|
|
27326
|
-
trap 'rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"' EXIT
|
|
27327
|
-
trap 'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"; exit 1' HUP INT TERM
|
|
27328
|
-
if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then
|
|
27329
|
-
return 1
|
|
27330
|
-
fi
|
|
27331
|
-
candidate="\${extract_dir}/\${member}"
|
|
27332
|
-
supacloud_validate_binary "$candidate" "pinned-linux-\${arch}" || return 1
|
|
27333
|
-
|
|
27334
|
-
mkdir -p "$(dirname "$target")"
|
|
27335
|
-
staged_target=$(mktemp "\${target}.tmp.XXXXXX")
|
|
27336
|
-
install -m 0755 "$candidate" "$staged_target"
|
|
27337
|
-
mv -f "$staged_target" "$target"
|
|
27338
|
-
staged_target=""
|
|
27339
|
-
)
|
|
27340
|
-
|
|
27341
|
-
supacloud_version_at_least() {
|
|
27342
|
-
local current="\${1#v}"
|
|
27343
|
-
local required="\${2#v}"
|
|
27344
|
-
local current_major=0 current_minor=0 current_patch=0
|
|
27345
|
-
local required_major=0 required_minor=0 required_patch=0
|
|
27346
|
-
[[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
|
|
27347
|
-
[[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1
|
|
27348
|
-
IFS=. read -r current_major current_minor current_patch <<EOF
|
|
27349
|
-
\${current%%-*}
|
|
27350
|
-
EOF
|
|
27351
|
-
IFS=. read -r required_major required_minor required_patch <<EOF
|
|
27352
|
-
\${required%%-*}
|
|
27353
|
-
EOF
|
|
27354
|
-
current_major=\${current_major:-0}; current_minor=\${current_minor:-0}; current_patch=\${current_patch:-0}
|
|
27355
|
-
required_major=\${required_major:-0}; required_minor=\${required_minor:-0}; required_patch=\${required_patch:-0}
|
|
27356
|
-
if (( current_major != required_major )); then (( current_major > required_major )); return; fi
|
|
27357
|
-
if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi
|
|
27358
|
-
(( current_patch >= required_patch ))
|
|
27359
|
-
}
|
|
27360
|
-
|
|
27361
|
-
supacloud_gh_version() {
|
|
27362
|
-
gh --version 2>/dev/null | awk 'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }'
|
|
27363
|
-
}
|
|
27364
|
-
|
|
27365
|
-
supacloud_install_gh_archive() {
|
|
27366
|
-
local archive="$1"
|
|
27367
|
-
local version="$2"
|
|
27368
|
-
local arch="$3"
|
|
27369
|
-
local expected_sha256="$4"
|
|
27370
|
-
local target="$5"
|
|
27371
|
-
local member="gh_\${version}_linux_\${arch}/bin/gh"
|
|
27372
|
-
local actual_sha256 member_count member_details extracted_dir candidate version_output
|
|
27373
|
-
|
|
27374
|
-
actual_sha256=$(sha256sum "$archive" | awk '{print $1}')
|
|
27375
|
-
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
|
|
27376
|
-
echo "GitHub CLI archive SHA256 mismatch" >&2
|
|
27377
|
-
return 1
|
|
27378
|
-
fi
|
|
27379
|
-
|
|
27380
|
-
member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)
|
|
27381
|
-
if [[ "$member_count" != "1" ]]; then
|
|
27382
|
-
echo "GitHub CLI archive does not contain the exact expected member: $member" >&2
|
|
27383
|
-
return 1
|
|
27384
|
-
fi
|
|
27385
|
-
member_details=$(tar -tvzf "$archive" "$member") || return 1
|
|
27386
|
-
if [[ "\${member_details:0:1}" != "-" ]]; then
|
|
27387
|
-
echo "GitHub CLI archive member is not a regular file: $member" >&2
|
|
27388
|
-
return 1
|
|
27389
|
-
fi
|
|
27390
|
-
|
|
27391
|
-
extracted_dir=$(mktemp -d)
|
|
27392
|
-
candidate="\${extracted_dir}/\${member}"
|
|
27393
|
-
if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\
|
|
27394
|
-
|| ! supacloud_validate_binary "$candidate" "gh-linux-\${arch}"; then
|
|
27395
|
-
rm -rf "$extracted_dir"
|
|
27396
|
-
return 1
|
|
27397
|
-
fi
|
|
27398
|
-
chmod 0755 "$candidate"
|
|
27399
|
-
version_output=$("$candidate" --version 2>/dev/null | head -1) || {
|
|
27400
|
-
rm -rf "$extracted_dir"
|
|
27401
|
-
echo "GitHub CLI bootstrap binary failed its version check" >&2
|
|
27402
|
-
return 1
|
|
27403
|
-
}
|
|
27404
|
-
if [[ "$version_output" != "gh version \${version}"* ]]; then
|
|
27405
|
-
rm -rf "$extracted_dir"
|
|
27406
|
-
echo "GitHub CLI bootstrap version mismatch: \${version_output}" >&2
|
|
27407
|
-
return 1
|
|
27408
|
-
fi
|
|
27409
|
-
mkdir -p "$(dirname "$target")"
|
|
27410
|
-
install -m 0755 "$candidate" "$target"
|
|
27411
|
-
rm -rf "$extracted_dir"
|
|
27412
|
-
}
|
|
27413
|
-
|
|
27414
|
-
supacloud_install_pinned_gh() {
|
|
27415
|
-
local target="\${1:-/usr/local/bin/gh}"
|
|
27416
|
-
local machine arch expected_sha256 asset url archive
|
|
27417
|
-
machine=$(uname -m)
|
|
27418
|
-
case "$machine" in
|
|
27419
|
-
x86_64|amd64)
|
|
27420
|
-
arch="amd64"
|
|
27421
|
-
expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"
|
|
27422
|
-
;;
|
|
27423
|
-
aarch64|arm64)
|
|
27424
|
-
arch="arm64"
|
|
27425
|
-
expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"
|
|
27426
|
-
;;
|
|
27427
|
-
*)
|
|
27428
|
-
echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2
|
|
27429
|
-
return 1
|
|
27430
|
-
;;
|
|
27431
|
-
esac
|
|
27432
|
-
asset="gh_\${SUPACLOUD_GH_VERSION}_linux_\${arch}.tar.gz"
|
|
27433
|
-
url="https://github.com/cli/cli/releases/download/v\${SUPACLOUD_GH_VERSION}/\${asset}"
|
|
27434
|
-
archive=$(mktemp)
|
|
27435
|
-
if ! supacloud_download_url "$url" "$archive" \\
|
|
27436
|
-
|| ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then
|
|
27437
|
-
rm -f "$archive"
|
|
27438
|
-
return 1
|
|
27439
|
-
fi
|
|
27440
|
-
rm -f "$archive"
|
|
27441
|
-
}
|
|
27442
|
-
|
|
27443
|
-
supacloud_validate_tar() {
|
|
27444
|
-
local artifact_file="$1"
|
|
27445
|
-
local entries
|
|
27446
|
-
entries=$(tar -tzf "$artifact_file") || {
|
|
27447
|
-
echo "Web Console archive is not a readable gzip tarball" >&2
|
|
27448
|
-
return 1
|
|
27449
|
-
}
|
|
27450
|
-
if ! printf '%s\\n' "$entries" | awk '
|
|
27451
|
-
/^\\// { exit 1 }
|
|
27452
|
-
/(^|\\/)\\.\\.($|\\/)/ { exit 1 }
|
|
27453
|
-
'; then
|
|
27454
|
-
echo "Web Console archive contains an unsafe path" >&2
|
|
27455
|
-
return 1
|
|
27456
|
-
fi
|
|
27457
|
-
if ! tar -tvzf "$artifact_file" | awk 'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }'; then
|
|
27458
|
-
echo "Web Console archive contains links or special files" >&2
|
|
27459
|
-
return 1
|
|
27460
|
-
fi
|
|
27461
|
-
printf '%s\\n' "$entries" | grep -Eq '(^|/)index\\.html$' || {
|
|
27462
|
-
echo "Web Console archive is invalid or does not contain index.html" >&2
|
|
27463
|
-
return 1
|
|
27464
|
-
}
|
|
27465
|
-
}
|
|
27466
|
-
|
|
27467
|
-
supacloud_record_integrity_mode() {
|
|
27468
|
-
local mode="$1"
|
|
27469
|
-
local record_file="\${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"
|
|
27470
|
-
mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0
|
|
27471
|
-
printf '%s\\n' "$mode" > "$record_file" 2>/dev/null || return 0
|
|
27472
|
-
chmod 600 "$record_file" 2>/dev/null || true
|
|
27473
|
-
}
|
|
27474
|
-
|
|
27475
|
-
supacloud_fetch_attestation_bundle() {
|
|
27476
|
-
local artifact_file="$1"
|
|
27477
|
-
local bundle_file="$2"
|
|
27478
|
-
local digest response
|
|
27479
|
-
digest=$(sha256sum "$artifact_file" | awk '{print $1}') || return 1
|
|
27480
|
-
[[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {
|
|
27481
|
-
echo "Unable to calculate the artifact digest for attestation lookup" >&2
|
|
27482
|
-
return 1
|
|
27483
|
-
}
|
|
27484
|
-
digest=$(printf '%s' "$digest" | tr '[:upper:]' '[:lower:]')
|
|
27485
|
-
response=$(supacloud_fetch_release_json \\
|
|
27486
|
-
"https://api.github.com/repos/\${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:\${digest}") || {
|
|
27487
|
-
echo "Unable to download the public GitHub artifact attestation bundle" >&2
|
|
27488
|
-
return 1
|
|
27489
|
-
}
|
|
27490
|
-
if ! jq -ce '
|
|
27491
|
-
.attestations
|
|
27492
|
-
| if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")
|
|
27493
|
-
then error("no valid attestation bundles returned")
|
|
27494
|
-
else .[].bundle
|
|
27495
|
-
end
|
|
27496
|
-
' <<< "$response" > "$bundle_file"; then
|
|
27497
|
-
echo "GitHub artifact attestation response did not contain a valid bundle" >&2
|
|
27498
|
-
return 1
|
|
27499
|
-
fi
|
|
27500
|
-
}
|
|
27501
|
-
|
|
27502
|
-
supacloud_verify_attestation() (
|
|
27503
|
-
local artifact_file="$1"
|
|
27504
|
-
if supacloud_attestation_verifier_available; then
|
|
27505
|
-
local verification_output bundle_dir bundle_file
|
|
27506
|
-
bundle_dir=$(mktemp -d "\${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1
|
|
27507
|
-
trap 'rm -rf -- "$bundle_dir"' EXIT
|
|
27508
|
-
trap 'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1' HUP INT TERM
|
|
27509
|
-
bundle_file="\${bundle_dir}/bundle.jsonl"
|
|
27510
|
-
if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then
|
|
27511
|
-
return 1
|
|
27512
|
-
fi
|
|
27513
|
-
if ! verification_output=$(gh attestation verify "$artifact_file" \\
|
|
27514
|
-
--bundle "$bundle_file" \\
|
|
27515
|
-
--repo "$SUPACLOUD_GITHUB_REPOSITORY" \\
|
|
27516
|
-
--signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\
|
|
27517
|
-
--source-ref "refs/heads/main" \\
|
|
27518
|
-
--deny-self-hosted-runners 2>&1); then
|
|
27519
|
-
echo "GitHub artifact attestation verification failed: \${verification_output}" >&2
|
|
27520
|
-
return 1
|
|
27521
|
-
fi
|
|
27522
|
-
supacloud_record_integrity_mode "github-attestation+same-release-sha256"
|
|
27523
|
-
return
|
|
27524
|
-
fi
|
|
27525
|
-
|
|
27526
|
-
if [[ "\${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then
|
|
27527
|
-
echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2
|
|
27528
|
-
supacloud_record_integrity_mode "break-glass:same-release-sha256-only"
|
|
27529
|
-
return 0
|
|
27530
|
-
fi
|
|
27531
|
-
|
|
27532
|
-
echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2
|
|
27533
|
-
return 1
|
|
27534
|
-
)
|
|
27535
|
-
|
|
27536
|
-
supacloud_attestation_verifier_available() {
|
|
27537
|
-
local version help
|
|
27538
|
-
command -v gh >/dev/null 2>&1 || return 1
|
|
27539
|
-
version=$(supacloud_gh_version)
|
|
27540
|
-
[[ -n "$version" ]] || return 1
|
|
27541
|
-
supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1
|
|
27542
|
-
help=$(gh attestation verify --help 2>&1) || return 1
|
|
27543
|
-
grep -Eq -- '(^|[[:space:]])--bundle([=[:space:]]|$)' <<< "$help" || return 1
|
|
27544
|
-
grep -Eq -- '(^|[[:space:]])--signer-workflow([=[:space:]]|$)' <<< "$help" || return 1
|
|
27545
|
-
grep -Eq -- '(^|[[:space:]])--source-ref([=[:space:]]|$)' <<< "$help" || return 1
|
|
27546
|
-
grep -Eq -- '(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)' <<< "$help"
|
|
27547
|
-
}
|
|
27548
|
-
|
|
27549
|
-
supacloud_download_release_asset() (
|
|
27550
|
-
local release_json="$1"
|
|
27551
|
-
local asset_name="$2"
|
|
27552
|
-
local destination="$3"
|
|
27553
|
-
local asset_kind="$4"
|
|
27554
|
-
local asset_url checksum_url temporary_artifact temporary_checksums
|
|
27555
|
-
|
|
27556
|
-
asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1
|
|
27557
|
-
checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1
|
|
27558
|
-
mkdir -p "$(dirname "$destination")"
|
|
27559
|
-
temporary_artifact=$(mktemp "\${destination}.tmp.XXXXXX")
|
|
27560
|
-
temporary_checksums=$(mktemp "\${destination}.SHA256SUMS.tmp.XXXXXX")
|
|
27561
|
-
trap 'rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"' EXIT
|
|
27562
|
-
trap 'trap - EXIT HUP INT TERM; rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"; exit 1' HUP INT TERM
|
|
27563
|
-
|
|
27564
|
-
if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\
|
|
27565
|
-
|| ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\
|
|
27566
|
-
|| ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then
|
|
27567
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27568
|
-
return 1
|
|
27569
|
-
fi
|
|
27570
|
-
|
|
27571
|
-
# Authenticate the digest before parsing archives or inspecting binaries.
|
|
27572
|
-
if ! supacloud_verify_attestation "$temporary_artifact"; then
|
|
27573
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27574
|
-
return 1
|
|
27575
|
-
fi
|
|
27576
|
-
|
|
27577
|
-
case "$asset_kind" in
|
|
27578
|
-
binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;
|
|
27579
|
-
tar) supacloud_validate_tar "$temporary_artifact" ;;
|
|
27580
|
-
*)
|
|
27581
|
-
echo "Unknown release asset kind: $asset_kind" >&2
|
|
27582
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27583
|
-
return 1
|
|
27584
|
-
;;
|
|
27585
|
-
esac || {
|
|
27586
|
-
rm -f "$temporary_artifact" "$temporary_checksums"
|
|
27587
|
-
return 1
|
|
27588
|
-
}
|
|
27589
|
-
|
|
27590
|
-
mv -f "$temporary_artifact" "$destination"
|
|
27591
|
-
temporary_artifact=""
|
|
27592
|
-
rm -f "$temporary_checksums"
|
|
27593
|
-
temporary_checksums=""
|
|
27594
|
-
)
|
|
27595
|
-
`;
|
|
27271
|
+
var release_assets_default = '#!/usr/bin/env bash\n\nSUPACLOUD_GITHUB_REPOSITORY="${SUPACLOUD_GITHUB_REPOSITORY:-zuohuadong/supacloud}"\nSUPACLOUD_RELEASES_API="${SUPACLOUD_RELEASES_API:-https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/releases}"\nSUPACLOUD_ATTESTATION_SIGNER_WORKFLOW="${SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW:-${SUPACLOUD_GITHUB_REPOSITORY}/.github/workflows/release-please.yml}"\nSUPACLOUD_GH_VERSION="${SUPACLOUD_GH_VERSION:-2.96.0}"\nSUPACLOUD_GH_MIN_VERSION="${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"\nSUPACLOUD_GH_AMD64_SHA256="${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"\nSUPACLOUD_GH_ARM64_SHA256="${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"\nSUPACLOUD_RELEASE_ASSETS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"\nSUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT="${SUPACLOUD_RELEASE_ASSETS_DIR}/../../packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256="3c2cc7f357dc064ec527fdcd78da6e9245c21a381e1abaa0f2b62b186bcac1a1"\nreadonly SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE="5748"\n\nsupacloud_curl_release_json() {\n local url="$1"\n local output="$2"\n curl -fsSL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 60 \\\n --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\\n -o "$output" "$url"\n}\n\nsupacloud_curl_release_asset() {\n local url="$1"\n local output="$2"\n curl -fL --proto \'=https\' --proto-redir \'=https\' \\\n --retry 1 --retry-delay 2 --retry-max-time 180 \\\n --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\\n -o "$output" "$url"\n}\n\nsupacloud_component_tag() {\n local component="$1"\n local version="$2"\n case "$version" in\n "${component}-v"*) printf \'%s\' "$version" ;;\n v*) printf \'%s-%s\' "$component" "$version" ;;\n *) printf \'%s-v%s\' "$component" "$version" ;;\n esac\n}\n\nsupacloud_select_release() {\n local component="$1"\n shift\n local required_assets_json\n [[ $# -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "$@" | jq -Rsc \'split("\\n")[:-1]\')\n jq -ce --arg prefix "${component}-v" --argjson required "$required_assets_json" \'\n map(select(\n (.draft | not)\n and (.prerelease | not)\n and (.tag_name | startswith($prefix))\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n ))\n | first\n // error("no matching component release contains all required assets and SHA256SUMS")\n \'\n}\n\nsupacloud_fetch_component_release() {\n local component="$1"\n local version="${2:-latest}"\n shift 2\n local required_assets=("$@")\n local required_assets_json\n local response\n [[ ${#required_assets[@]} -gt 0 ]] || {\n echo "at least one required release asset must be specified" >&2\n return 1\n }\n required_assets_json=$(printf \'%s\\n\' "${required_assets[@]}" | jq -Rsc \'split("\\n")[:-1]\')\n\n if [[ -n "$version" && "$version" != "latest" ]]; then\n local tag\n tag=$(supacloud_component_tag "$component" "$version")\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}/tags/${tag}") || return 1\n jq -ce --argjson required "$required_assets_json" \'\n select(\n (.draft | not)\n and (.prerelease | not)\n and (. as $release | all($required[]; . as $asset | any($release.assets[]?; .name == $asset)))\n and any(.assets[]?; .name == "SHA256SUMS")\n )\n // error("release does not contain all required assets and SHA256SUMS")\n \' <<< "$response"\n return\n fi\n\n response=$(supacloud_fetch_release_json "${SUPACLOUD_RELEASES_API}?per_page=100") || return 1\n supacloud_select_release "$component" "${required_assets[@]}" <<< "$response"\n}\n\nsupacloud_fetch_release_json() (\n local url="$1"\n local response_file\n response_file=$(mktemp) || return 1\n trap \'rm -f "$response_file"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1\' HUP INT TERM\n supacloud_download_release_metadata_url "$url" "$response_file" || return 1\n cat "$response_file"\n)\n\nsupacloud_release_asset_url() {\n local release_json="$1"\n local asset_name="$2"\n jq -er --arg asset "$asset_name" \'\n first(.assets[]? | select(.name == $asset) | .browser_download_url)\n // error("release asset URL is missing")\n \' <<< "$release_json"\n}\n\nsupacloud_download_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_asset "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_asset "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_download_release_metadata_url() {\n local url="$1"\n local output="$2"\n local proxy="${SUPACLOUD_GITHUB_PROXY:-${GH_PROXY:-}}"\n\n if supacloud_curl_release_json "$url" "$output"; then\n return 0\n fi\n if [[ -n "$proxy" ]]; then\n supacloud_curl_release_json "${proxy%/}/${url}" "$output"\n return\n fi\n return 1\n}\n\nsupacloud_verify_checksum() {\n local artifact_file="$1"\n local asset_name="$2"\n local checksum_file="$3"\n local expected\n expected=$(awk -v asset="$asset_name" \'$2 == asset || $2 == "*" asset { print $1; exit }\' "$checksum_file")\n if [[ ! "$expected" =~ ^[0-9a-fA-F]{64}$ ]]; then\n echo "SHA256SUMS does not contain a valid checksum for ${asset_name}" >&2\n return 1\n fi\n\n local actual\n actual=$(sha256sum "$artifact_file" | awk \'{print $1}\')\n actual=$(printf \'%s\' "$actual" | tr \'[:upper:]\' \'[:lower:]\')\n expected=$(printf \'%s\' "$expected" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ "$actual" != "$expected" ]]; then\n echo "SHA256 mismatch for ${asset_name}" >&2\n return 1\n fi\n}\n\nsupacloud_validate_binary() {\n local artifact_file="$1"\n local asset_name="$2"\n local description\n description=$(file -b "$artifact_file")\n if [[ "$description" != *ELF* ]]; then\n echo "${asset_name} is not an ELF binary: ${description}" >&2\n return 1\n fi\n\n case "$asset_name" in\n *amd64)\n [[ "$description" == *x86-64* || "$description" == *x86_64* ]] || {\n echo "${asset_name} does not contain an x86-64 ELF binary" >&2\n return 1\n }\n ;;\n *arm64)\n [[ "$description" == *aarch64* || "$description" == *ARM64* ]] || {\n echo "${asset_name} does not contain an arm64 ELF binary" >&2\n return 1\n }\n ;;\n esac\n}\n\nsupacloud_install_pinned_tar_xz_binary() (\n local archive="$1"\n local member="$2"\n local expected_sha256="$3"\n local arch="$4"\n local target="$5"\n local actual_sha256 member_count member_details extract_dir candidate staged_target\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n actual_sha256=$(printf \'%s\' "$actual_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n expected_sha256=$(printf \'%s\' "$expected_sha256" | tr \'[:upper:]\' \'[:lower:]\')\n if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ || "$actual_sha256" != "$expected_sha256" ]]; then\n echo "SHA256 mismatch for pinned archive" >&2\n return 1\n fi\n\n member_count=$(tar -tJf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "Pinned archive must contain the exact member once: $member" >&2\n return 1\n fi\n member_details=$(tar -tvJf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "Pinned archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extract_dir=$(mktemp -d)\n trap \'rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "${staged_target:-}" ]] || rm -f "$staged_target"; exit 1\' HUP INT TERM\n if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then\n return 1\n fi\n candidate="${extract_dir}/${member}"\n supacloud_validate_binary "$candidate" "pinned-linux-${arch}" || return 1\n\n mkdir -p "$(dirname "$target")"\n staged_target=$(mktemp "${target}.tmp.XXXXXX")\n install -m 0755 "$candidate" "$staged_target"\n mv -f "$staged_target" "$target"\n staged_target=""\n)\n\nsupacloud_version_at_least() {\n local current="${1#v}"\n local required="${2#v}"\n local current_major=0 current_minor=0 current_patch=0\n local required_major=0 required_minor=0 required_patch=0\n [[ "$current" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n [[ "$required" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+(-.*)?$ ]] || return 1\n IFS=. read -r current_major current_minor current_patch <<EOF\n${current%%-*}\nEOF\n IFS=. read -r required_major required_minor required_patch <<EOF\n${required%%-*}\nEOF\n current_major=${current_major:-0}; current_minor=${current_minor:-0}; current_patch=${current_patch:-0}\n required_major=${required_major:-0}; required_minor=${required_minor:-0}; required_patch=${required_patch:-0}\n if (( current_major != required_major )); then (( current_major > required_major )); return; fi\n if (( current_minor != required_minor )); then (( current_minor > required_minor )); return; fi\n (( current_patch >= required_patch ))\n}\n\nsupacloud_gh_version() {\n gh --version 2>/dev/null | awk \'NR == 1 && $1 == "gh" && $2 == "version" { print $3; exit }\'\n}\n\nsupacloud_install_gh_archive() {\n local archive="$1"\n local version="$2"\n local arch="$3"\n local expected_sha256="$4"\n local target="$5"\n local member="gh_${version}_linux_${arch}/bin/gh"\n local actual_sha256 member_count member_details extracted_dir candidate version_output\n\n actual_sha256=$(sha256sum "$archive" | awk \'{print $1}\')\n if [[ "$actual_sha256" != "$expected_sha256" ]]; then\n echo "GitHub CLI archive SHA256 mismatch" >&2\n return 1\n fi\n\n member_count=$(tar -tzf "$archive" | grep -Fxc "$member" || true)\n if [[ "$member_count" != "1" ]]; then\n echo "GitHub CLI archive does not contain the exact expected member: $member" >&2\n return 1\n fi\n member_details=$(tar -tvzf "$archive" "$member") || return 1\n if [[ "${member_details:0:1}" != "-" ]]; then\n echo "GitHub CLI archive member is not a regular file: $member" >&2\n return 1\n fi\n\n extracted_dir=$(mktemp -d)\n candidate="${extracted_dir}/${member}"\n if ! tar --no-same-owner --no-same-permissions -xzf "$archive" -C "$extracted_dir" "$member" \\\n || ! supacloud_validate_binary "$candidate" "gh-linux-${arch}"; then\n rm -rf "$extracted_dir"\n return 1\n fi\n chmod 0755 "$candidate"\n version_output=$("$candidate" --version 2>/dev/null | head -1) || {\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap binary failed its version check" >&2\n return 1\n }\n if [[ "$version_output" != "gh version ${version}"* ]]; then\n rm -rf "$extracted_dir"\n echo "GitHub CLI bootstrap version mismatch: ${version_output}" >&2\n return 1\n fi\n mkdir -p "$(dirname "$target")"\n install -m 0755 "$candidate" "$target"\n rm -rf "$extracted_dir"\n}\n\nsupacloud_install_pinned_gh() {\n local target="${1:-/usr/local/bin/gh}"\n local machine arch expected_sha256 asset url archive\n machine=$(uname -m)\n case "$machine" in\n x86_64|amd64)\n arch="amd64"\n expected_sha256="$SUPACLOUD_GH_AMD64_SHA256"\n ;;\n aarch64|arm64)\n arch="arm64"\n expected_sha256="$SUPACLOUD_GH_ARM64_SHA256"\n ;;\n *)\n echo "Unsupported architecture for GitHub CLI bootstrap: $machine" >&2\n return 1\n ;;\n esac\n asset="gh_${SUPACLOUD_GH_VERSION}_linux_${arch}.tar.gz"\n url="https://github.com/cli/cli/releases/download/v${SUPACLOUD_GH_VERSION}/${asset}"\n archive=$(mktemp)\n if ! supacloud_download_url "$url" "$archive" \\\n || ! supacloud_install_gh_archive "$archive" "$SUPACLOUD_GH_VERSION" "$arch" "$expected_sha256" "$target"; then\n rm -f "$archive"\n return 1\n fi\n rm -f "$archive"\n}\n\nsupacloud_validate_tar() {\n local artifact_file="$1"\n local entries\n entries=$(tar -tzf "$artifact_file") || {\n echo "Web Console archive is not a readable gzip tarball" >&2\n return 1\n }\n if ! printf \'%s\\n\' "$entries" | awk \'\n /^\\// { exit 1 }\n /(^|\\/)\\.\\.($|\\/)/ { exit 1 }\n \'; then\n echo "Web Console archive contains an unsafe path" >&2\n return 1\n fi\n if ! tar -tvzf "$artifact_file" | awk \'substr($1, 1, 1) != "-" && substr($1, 1, 1) != "d" { exit 1 }\'; then\n echo "Web Console archive contains links or special files" >&2\n return 1\n fi\n printf \'%s\\n\' "$entries" | grep -Eq \'(^|/)index\\.html$\' || {\n echo "Web Console archive is invalid or does not contain index.html" >&2\n return 1\n }\n}\n\nsupacloud_record_integrity_mode() {\n local mode="$1"\n local record_file="${SUPACLOUD_INTEGRITY_MODE_RECORD:-/var/lib/supacloud/artifact-integrity-mode}"\n mkdir -p "$(dirname "$record_file")" 2>/dev/null || return 0\n printf \'%s\\n\' "$mode" > "$record_file" 2>/dev/null || return 0\n chmod 600 "$record_file" 2>/dev/null || true\n}\n\nsupacloud_fetch_attestation_bundle() {\n local artifact_file="$1"\n local bundle_file="$2"\n local digest response\n digest=$(sha256sum "$artifact_file" | awk \'{print $1}\') || return 1\n [[ "$digest" =~ ^[0-9a-fA-F]{64}$ ]] || {\n echo "Unable to calculate the artifact digest for attestation lookup" >&2\n return 1\n }\n digest=$(printf \'%s\' "$digest" | tr \'[:upper:]\' \'[:lower:]\')\n response=$(supacloud_fetch_release_json \\\n "https://api.github.com/repos/${SUPACLOUD_GITHUB_REPOSITORY}/attestations/sha256:${digest}") || {\n echo "Unable to download the public GitHub artifact attestation bundle" >&2\n return 1\n }\n if ! jq -ce \'\n .attestations\n | if type != "array" or length == 0 or any(.[]; (.bundle | type) != "object")\n then error("no valid attestation bundles returned")\n else .[].bundle\n end\n \' <<< "$response" > "$bundle_file"; then\n echo "GitHub artifact attestation response did not contain a valid bundle" >&2\n return 1\n fi\n}\n\nsupacloud_attestation_trusted_root_available() {\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n local actual_size actual_sha256\n [[ "$trusted_root" == /* && -f "$trusted_root" && ! -L "$trusted_root" ]] || return 1\n actual_size=$(wc -c < "$trusted_root" | tr -d \'[:space:]\') || return 1\n [[ "$actual_size" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n actual_sha256=$(sha256sum "$trusted_root" | awk \'{print $1}\') || return 1\n [[ "$actual_sha256" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]] || return 1\n [[ "$(wc -l < "$trusted_root" | tr -d \'[:space:]\')" == "1" ]] || return 1\n jq -e \'type == "object" and .mediaType == "application/vnd.dev.sigstore.trustedroot+json;version=0.1"\' \\\n "$trusted_root" >/dev/null 2>&1\n}\n\nsupacloud_prepare_attestation_trusted_root() {\n local destination="$1"\n local trusted_root="${SUPACLOUD_ATTESTATION_TRUSTED_ROOT:-$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_DEFAULT}"\n supacloud_attestation_trusted_root_available || {\n echo "Pinned Sigstore Public Good trusted root is missing or invalid" >&2\n return 1\n }\n jq -ce . "$trusted_root" > "$destination" || return 1\n chmod 600 "$destination"\n [[ "$(wc -c < "$destination" | tr -d \'[:space:]\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SIZE" ]] || return 1\n [[ "$(sha256sum "$destination" | awk \'{print $1}\')" == "$SUPACLOUD_ATTESTATION_TRUSTED_ROOT_SHA256" ]]\n}\n\nsupacloud_verify_attestation() (\n local artifact_file="$1"\n if supacloud_attestation_verifier_available; then\n local verification_output bundle_dir bundle_file trusted_root_file\n bundle_dir=$(mktemp -d "${TMPDIR:-/tmp}/supacloud-attestation.XXXXXX") || return 1\n trap \'rm -rf -- "$bundle_dir"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -rf -- "$bundle_dir"; exit 1\' HUP INT TERM\n bundle_file="${bundle_dir}/bundle.jsonl"\n trusted_root_file="${bundle_dir}/trusted_root.jsonl"\n if ! supacloud_fetch_attestation_bundle "$artifact_file" "$bundle_file"; then\n return 1\n fi\n supacloud_prepare_attestation_trusted_root "$trusted_root_file" || return 1\n if ! verification_output=$(gh attestation verify "$artifact_file" \\\n --bundle "$bundle_file" \\\n --custom-trusted-root "$trusted_root_file" \\\n --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\\n --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\\n --source-ref "refs/heads/main" \\\n --deny-self-hosted-runners 2>&1); then\n echo "GitHub artifact attestation verification failed: ${verification_output}" >&2\n return 1\n fi\n supacloud_record_integrity_mode "github-attestation+same-release-sha256"\n return\n fi\n\n if [[ "${SUPACLOUD_ALLOW_UNVERIFIED_RELEASE:-false}" == "true" ]]; then\n echo "BREAK-GLASS LIMITED INTEGRITY MODE: artifact attestation verification is unavailable; only the same-release SHA256 checksum was verified." >&2\n supacloud_record_integrity_mode "break-glass:same-release-sha256-only"\n return 0\n fi\n\n echo "Artifact attestation verification is required, but gh attestation verify is unavailable. Install GitHub CLI or explicitly set SUPACLOUD_ALLOW_UNVERIFIED_RELEASE=true for emergency break-glass use." >&2\n return 1\n)\n\nsupacloud_attestation_verifier_available() {\n local version help\n supacloud_attestation_trusted_root_available || return 1\n command -v gh >/dev/null 2>&1 || return 1\n version=$(supacloud_gh_version)\n [[ -n "$version" ]] || return 1\n supacloud_version_at_least "$version" "$SUPACLOUD_GH_MIN_VERSION" || return 1\n help=$(gh attestation verify --help 2>&1) || return 1\n grep -Eq -- \'(^|[[:space:]])--bundle([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--signer-workflow([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--source-ref([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--custom-trusted-root([=[:space:]]|$)\' <<< "$help" || return 1\n grep -Eq -- \'(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)\' <<< "$help"\n}\n\nsupacloud_download_release_asset() (\n local release_json="$1"\n local asset_name="$2"\n local destination="$3"\n local asset_kind="$4"\n local asset_url checksum_url temporary_artifact temporary_checksums\n\n asset_url=$(supacloud_release_asset_url "$release_json" "$asset_name") || return 1\n checksum_url=$(supacloud_release_asset_url "$release_json" SHA256SUMS) || return 1\n mkdir -p "$(dirname "$destination")"\n temporary_artifact=$(mktemp "${destination}.tmp.XXXXXX")\n temporary_checksums=$(mktemp "${destination}.SHA256SUMS.tmp.XXXXXX")\n trap \'rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"\' EXIT\n trap \'trap - EXIT HUP INT TERM; rm -f "${temporary_artifact:-}" "${temporary_checksums:-}"; exit 1\' HUP INT TERM\n\n if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\\n || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\\n || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n # Authenticate the digest before parsing archives or inspecting binaries.\n if ! supacloud_verify_attestation "$temporary_artifact"; then\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n fi\n\n case "$asset_kind" in\n binary) supacloud_validate_binary "$temporary_artifact" "$asset_name" ;;\n tar) supacloud_validate_tar "$temporary_artifact" ;;\n *)\n echo "Unknown release asset kind: $asset_kind" >&2\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n ;;\n esac || {\n rm -f "$temporary_artifact" "$temporary_checksums"\n return 1\n }\n\n mv -f "$temporary_artifact" "$destination"\n temporary_artifact=""\n rm -f "$temporary_checksums"\n temporary_checksums=""\n)\n';
|
|
27596
27272
|
|
|
27597
27273
|
// src/shared/tools/ssh-tools.ts
|
|
27598
27274
|
var SAFE_CONTAINER_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/;
|
|
@@ -27606,6 +27282,92 @@ var SAFE_DB_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$/;
|
|
|
27606
27282
|
var MINIMUM_COMPONENT_UPGRADE_VERSION = "0.50.27";
|
|
27607
27283
|
var DIRECT_UPGRADE_SSH_TIMEOUT_MS = 720000;
|
|
27608
27284
|
var PROXIED_UPGRADE_SSH_TIMEOUT_MS = 1320000;
|
|
27285
|
+
var PLATFORM_PROBE_TIMEOUT_MS = 1e4;
|
|
27286
|
+
var PLATFORM_HASH_TIMEOUT_MS = 30000;
|
|
27287
|
+
var WEB_CONSOLE_PROBE_TIMEOUT_MS = 60000;
|
|
27288
|
+
var BINARY_VERSION_MAX_BYTES = 2048;
|
|
27289
|
+
var WEB_CONSOLE_MARKER_MAX_BYTES = 4096;
|
|
27290
|
+
var WEB_CONSOLE_CURRENT_DIR = "/opt/supacloud/web-console/current";
|
|
27291
|
+
var STABLE_SEMVER_CORE = "(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)";
|
|
27292
|
+
var EXACT_STABLE_SEMVER = new RegExp(`^(?:${STABLE_SEMVER_CORE})(?![\\s\\S])`);
|
|
27293
|
+
var SEMVER_CORE_TOKEN = /\d+\.\d+\.\d+/;
|
|
27294
|
+
var VERSION_TOKEN_SEPARATOR = /[\s"'()\[\]{}:,;=]+/;
|
|
27295
|
+
var BINARY_HASH_BEFORE_FAILED = 70;
|
|
27296
|
+
var BINARY_VERSION_FAILED = 71;
|
|
27297
|
+
var BINARY_HASH_AFTER_FAILED = 72;
|
|
27298
|
+
var PROBE_CHANGED_DURING_READ = 75;
|
|
27299
|
+
var WEB_CONSOLE_ROOT_MISSING = 43;
|
|
27300
|
+
var WEB_CONSOLE_MARKER_MISSING = 44;
|
|
27301
|
+
var WEB_CONSOLE_ROOT_INVALID = 64;
|
|
27302
|
+
var WEB_CONSOLE_MARKER_INVALID = 65;
|
|
27303
|
+
var WEB_CONSOLE_TREE_INVALID = 66;
|
|
27304
|
+
var WEB_CONSOLE_TREE_FAILED = 67;
|
|
27305
|
+
var BINARY_HASH_BEFORE_LABEL = "SUPACLOUD_BINARY_HASH_BEFORE=";
|
|
27306
|
+
var BINARY_VERSION_LABEL = "SUPACLOUD_BINARY_VERSION_BASE64=";
|
|
27307
|
+
var BINARY_HASH_AFTER_LABEL = "SUPACLOUD_BINARY_HASH_AFTER=";
|
|
27308
|
+
var WEB_ROOT_REAL_BEFORE_LABEL = "SUPACLOUD_WEB_ROOT_REAL_BEFORE=";
|
|
27309
|
+
var WEB_ROOT_ID_BEFORE_LABEL = "SUPACLOUD_WEB_ROOT_ID_BEFORE=";
|
|
27310
|
+
var WEB_MARKER_BEFORE_LABEL = "SUPACLOUD_WEB_MARKER_BASE64_BEFORE=";
|
|
27311
|
+
var WEB_TREE_BEFORE_LABEL = "SUPACLOUD_WEB_TREE_SHA256_BEFORE=";
|
|
27312
|
+
var WEB_TREE_AFTER_LABEL = "SUPACLOUD_WEB_TREE_SHA256_AFTER=";
|
|
27313
|
+
var WEB_MARKER_AFTER_LABEL = "SUPACLOUD_WEB_MARKER_BASE64_AFTER=";
|
|
27314
|
+
var WEB_ROOT_REAL_AFTER_LABEL = "SUPACLOUD_WEB_ROOT_REAL_AFTER=";
|
|
27315
|
+
var WEB_ROOT_ID_AFTER_LABEL = "SUPACLOUD_WEB_ROOT_ID_AFTER=";
|
|
27316
|
+
function fixedBinaryProbeCommand(executablePath, versionArgument) {
|
|
27317
|
+
const executable = quoteEnvValue(executablePath);
|
|
27318
|
+
return [
|
|
27319
|
+
"set -o pipefail",
|
|
27320
|
+
`if exec {BINARY_FD}<${executable}; then :; else exit ${BINARY_HASH_BEFORE_FAILED}; fi`,
|
|
27321
|
+
"PINNED_EXECUTABLE=/proc/$$/fd/$BINARY_FD",
|
|
27322
|
+
`if PINNED_ID=$(stat -Lc '%d:%i' -- "$PINNED_EXECUTABLE"); then :; else exit ${BINARY_HASH_BEFORE_FAILED}; fi`,
|
|
27323
|
+
`if HASH_BEFORE=$(sha256sum -- "$PINNED_EXECUTABLE" | awk '{print $1}'); then :; else exit ${BINARY_HASH_BEFORE_FAILED}; fi`,
|
|
27324
|
+
`printf '${BINARY_HASH_BEFORE_LABEL}%s\\n' "$HASH_BEFORE"`,
|
|
27325
|
+
`if VERSION_BASE64=$( (exec -a ${executable} "$PINNED_EXECUTABLE" ${versionArgument} {BINARY_FD}<&-) 2>&1 | head -c ${BINARY_VERSION_MAX_BYTES + 1} | base64 | tr -d '\\n'); then :; else exit ${BINARY_VERSION_FAILED}; fi`,
|
|
27326
|
+
`printf '${BINARY_VERSION_LABEL}%s\\n' "$VERSION_BASE64"`,
|
|
27327
|
+
`if HASH_AFTER=$(sha256sum -- "$PINNED_EXECUTABLE" | awk '{print $1}'); then :; else exit ${BINARY_HASH_AFTER_FAILED}; fi`,
|
|
27328
|
+
`printf '${BINARY_HASH_AFTER_LABEL}%s\\n' "$HASH_AFTER"`,
|
|
27329
|
+
`[ "$HASH_BEFORE" = "$HASH_AFTER" ] || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27330
|
+
`if CURRENT_ID=$(stat -Lc '%d:%i' -- ${executable}); then :; else exit ${PROBE_CHANGED_DURING_READ}; fi`,
|
|
27331
|
+
`[ "$PINNED_ID" = "$CURRENT_ID" ] || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27332
|
+
"exec {BINARY_FD}<&-"
|
|
27333
|
+
].join(`
|
|
27334
|
+
`);
|
|
27335
|
+
}
|
|
27336
|
+
var BINARY_PROBES = {
|
|
27337
|
+
management_api: {
|
|
27338
|
+
unit: "supacloud.service",
|
|
27339
|
+
source: "systemd:supacloud.service:ExecStart",
|
|
27340
|
+
commands: {
|
|
27341
|
+
"/usr/local/bin/supacloud": {
|
|
27342
|
+
command: fixedBinaryProbeCommand("/usr/local/bin/supacloud", "--version")
|
|
27343
|
+
},
|
|
27344
|
+
"/opt/supacloud/bin/supacloud": {
|
|
27345
|
+
command: fixedBinaryProbeCommand("/opt/supacloud/bin/supacloud", "--version")
|
|
27346
|
+
}
|
|
27347
|
+
}
|
|
27348
|
+
},
|
|
27349
|
+
edge_runtime: {
|
|
27350
|
+
unit: "supacloud-edge-runtime.service",
|
|
27351
|
+
source: "systemd:supacloud-edge-runtime.service:ExecStart",
|
|
27352
|
+
commands: {
|
|
27353
|
+
"/usr/local/bin/supacloud-edge-runtime": {
|
|
27354
|
+
command: fixedBinaryProbeCommand("/usr/local/bin/supacloud-edge-runtime", "--version")
|
|
27355
|
+
},
|
|
27356
|
+
"/opt/supacloud/bin/supacloud-edge-runtime": {
|
|
27357
|
+
command: fixedBinaryProbeCommand("/opt/supacloud/bin/supacloud-edge-runtime", "--version")
|
|
27358
|
+
}
|
|
27359
|
+
}
|
|
27360
|
+
},
|
|
27361
|
+
caddy: {
|
|
27362
|
+
unit: "supacloud-caddy.service",
|
|
27363
|
+
source: "systemd:supacloud-caddy.service:ExecStart",
|
|
27364
|
+
commands: {
|
|
27365
|
+
"/usr/local/bin/supacloud-caddy": {
|
|
27366
|
+
command: fixedBinaryProbeCommand("/usr/local/bin/supacloud-caddy", "version")
|
|
27367
|
+
}
|
|
27368
|
+
}
|
|
27369
|
+
}
|
|
27370
|
+
};
|
|
27609
27371
|
function hostnameSchema(fieldName) {
|
|
27610
27372
|
return decodedSchema(Type.String(), Type.String({ minLength: 1, maxLength: 253 }), (value) => {
|
|
27611
27373
|
const normalized = value.trim();
|
|
@@ -27663,7 +27425,8 @@ function assertSafeReleaseTag(value) {
|
|
|
27663
27425
|
return value;
|
|
27664
27426
|
}
|
|
27665
27427
|
function assertExactStableVersion2(value, fieldName) {
|
|
27666
|
-
|
|
27428
|
+
const normalized = value.startsWith("v") ? value.slice(1) : value;
|
|
27429
|
+
if (!EXACT_STABLE_SEMVER.test(normalized)) {
|
|
27667
27430
|
throw new Error(`${fieldName} must be an exact stable semantic version`);
|
|
27668
27431
|
}
|
|
27669
27432
|
return value;
|
|
@@ -27707,6 +27470,27 @@ function signalSafeCleanupTraps(cleanupCommand) {
|
|
|
27707
27470
|
`trap ${quoteEnvValue(`trap - EXIT HUP INT TERM; ${cleanupCommand}; exit 1`)} HUP INT TERM`
|
|
27708
27471
|
];
|
|
27709
27472
|
}
|
|
27473
|
+
function trustedRootPreflightCommands(helperPath) {
|
|
27474
|
+
const helperDirectory = dirname(helperPath);
|
|
27475
|
+
const trustedRootPath = join2(helperDirectory, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
27476
|
+
return [
|
|
27477
|
+
`HELPER_DIRECTORY=${quoteEnvValue(helperDirectory)}`,
|
|
27478
|
+
`TRUSTED_ROOT=${quoteEnvValue(trustedRootPath)}`,
|
|
27479
|
+
`test -d "$HELPER_DIRECTORY" && test ! -L "$HELPER_DIRECTORY" || { echo 'Upgrade helper directory is not secure' >&2; exit 1; }`,
|
|
27480
|
+
`test "$(stat -c '%a' "$HELPER_DIRECTORY")" = 700 || { echo 'Upgrade helper directory must use mode 0700' >&2; exit 1; }`,
|
|
27481
|
+
`EXPECTED_HELPER_ENTRIES=$(printf '%s\\n' ${quoteEnvValue(basename3(helperPath))} ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME)} | LC_ALL=C sort)`,
|
|
27482
|
+
`ACTUAL_HELPER_ENTRIES=$(find "$HELPER_DIRECTORY" -mindepth 1 -maxdepth 1 -printf '%f\\n' | LC_ALL=C sort)`,
|
|
27483
|
+
`test "$ACTUAL_HELPER_ENTRIES" = "$EXPECTED_HELPER_ENTRIES" || { echo 'Upgrade helper directory does not match its strict file allowlist' >&2; exit 1; }`,
|
|
27484
|
+
`test -f "$TRUSTED_ROOT" && test ! -L "$TRUSTED_ROOT" && test "$(stat -c '%h' "$TRUSTED_ROOT")" = 1 || { echo 'Pinned trusted root must be a direct regular file without links' >&2; exit 1; }`,
|
|
27485
|
+
`test "$(stat -c '%a' "$TRUSTED_ROOT")" = 600 || { echo 'Pinned trusted root must use mode 0600' >&2; exit 1; }`,
|
|
27486
|
+
`test "$(stat -c '%s' "$TRUSTED_ROOT")" = ${SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE} || { echo 'Pinned trusted root size mismatch' >&2; exit 1; }`,
|
|
27487
|
+
`test "$(sha256sum "$TRUSTED_ROOT" | awk '{print $1}')" = ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256)} || { echo 'Pinned trusted root digest mismatch' >&2; exit 1; }`,
|
|
27488
|
+
'chown -h root:root "$TRUSTED_ROOT"',
|
|
27489
|
+
`test "$(stat -c '%u:%g:%a:%h' "$TRUSTED_ROOT")" = 0:0:600:1 || { echo 'Pinned trusted root ownership or mode changed during adoption' >&2; exit 1; }`,
|
|
27490
|
+
`test "$(stat -c '%s' "$TRUSTED_ROOT")" = ${SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SIZE} && test "$(sha256sum "$TRUSTED_ROOT" | awk '{print $1}')" = ${quoteEnvValue(SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_SHA256)} || { echo 'Pinned trusted root changed during adoption' >&2; exit 1; }`,
|
|
27491
|
+
'export SUPACLOUD_ATTESTATION_TRUSTED_ROOT="$TRUSTED_ROOT"'
|
|
27492
|
+
];
|
|
27493
|
+
}
|
|
27710
27494
|
function buildRootUpgradeScript(request) {
|
|
27711
27495
|
const envAssignments = upgradeEnvAssignments(request);
|
|
27712
27496
|
return [
|
|
@@ -27714,16 +27498,18 @@ function buildRootUpgradeScript(request) {
|
|
|
27714
27498
|
"umask 077",
|
|
27715
27499
|
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
27716
27500
|
"export PATH",
|
|
27717
|
-
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_GH_VERSION SUPACLOUD_GH_MIN_VERSION SUPACLOUD_GH_AMD64_SHA256 SUPACLOUD_GH_ARM64_SHA256 GH_PROXY",
|
|
27501
|
+
"unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW SUPACLOUD_ATTESTATION_TRUSTED_ROOT SUPACLOUD_GH_VERSION SUPACLOUD_GH_MIN_VERSION SUPACLOUD_GH_AMD64_SHA256 SUPACLOUD_GH_ARM64_SHA256 GH_PROXY",
|
|
27718
27502
|
request.githubProxy ? `export SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : "unset SUPACLOUD_GITHUB_PROXY SUPACLOUD_GITHUB_PROXIES",
|
|
27719
|
-
'for tool in curl jq file sha256sum stat tar flock; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
|
|
27503
|
+
'for tool in curl jq file sha256sum stat tar flock find sort chown; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
|
|
27720
27504
|
"test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
|
|
27721
27505
|
"test -x /usr/local/bin/supacloud || { echo 'SupaCloud binary not found at /usr/local/bin/supacloud; run ssh install first.' >&2; exit 127; }",
|
|
27506
|
+
...trustedRootPreflightCommands(request.helperPath),
|
|
27722
27507
|
buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
|
|
27723
27508
|
...componentPreflightCommands(request),
|
|
27724
27509
|
"STAGED_MANAGEMENT=''",
|
|
27725
27510
|
...signalSafeCleanupTraps('test -z "$STAGED_MANAGEMENT" || rm -f "$STAGED_MANAGEMENT"'),
|
|
27726
27511
|
`source ${quoteEnvValue(request.helperPath)}`,
|
|
27512
|
+
"supacloud_attestation_trusted_root_available || { echo 'Pinned Sigstore trusted root failed helper validation' >&2; exit 1; }",
|
|
27727
27513
|
"if ! supacloud_attestation_verifier_available; then supacloud_install_pinned_gh /usr/local/bin/gh; fi",
|
|
27728
27514
|
"supacloud_attestation_verifier_available || { echo 'Pinned GitHub attestation verifier is unavailable' >&2; exit 1; }",
|
|
27729
27515
|
...componentBootstrapCommands(request),
|
|
@@ -27745,8 +27531,7 @@ async function prepareRemoteUpgradeHelperDirectory(ssh, helperPath) {
|
|
|
27745
27531
|
const command = [
|
|
27746
27532
|
"set -e",
|
|
27747
27533
|
"umask 077",
|
|
27748
|
-
`
|
|
27749
|
-
`install -d -m 700 -- ${quoteEnvValue(helperDirectory)}`
|
|
27534
|
+
`mkdir -m 700 -- ${quoteEnvValue(helperDirectory)}`
|
|
27750
27535
|
].join("; ");
|
|
27751
27536
|
const preparation = await ssh.exec(command, 30000);
|
|
27752
27537
|
if (!preparation.success) {
|
|
@@ -27773,44 +27558,56 @@ function remoteUpgradeFailure(execution) {
|
|
|
27773
27558
|
const diagnostic = execution.stderr.trim() || execution.stdout.trim() || "no remote diagnostic";
|
|
27774
27559
|
return new Error(`Remote upgrade failed (exit ${execution.code}): ${diagnostic.slice(-500)}`);
|
|
27775
27560
|
}
|
|
27776
|
-
function officialUpgradeOutcome(
|
|
27777
|
-
if (
|
|
27778
|
-
throw new AggregateError([executionError, cleanupError], "Upgrade execution failed and helper cleanup did not complete");
|
|
27561
|
+
function officialUpgradeOutcome(outcome) {
|
|
27562
|
+
if (outcome.executionFailed && outcome.cleanupFailed) {
|
|
27563
|
+
throw new AggregateError([outcome.executionError, outcome.cleanupError], "Upgrade execution failed and helper cleanup did not complete");
|
|
27779
27564
|
}
|
|
27780
|
-
if (
|
|
27781
|
-
throw executionError;
|
|
27782
|
-
if (!execution)
|
|
27565
|
+
if (outcome.executionFailed)
|
|
27566
|
+
throw outcome.executionError;
|
|
27567
|
+
if (!outcome.execution)
|
|
27783
27568
|
throw new Error("Upgrade execution did not return a result");
|
|
27784
|
-
if (
|
|
27785
|
-
throw new AggregateError([remoteUpgradeFailure(execution), cleanupError], "Remote upgrade failed and helper cleanup did not complete");
|
|
27569
|
+
if (outcome.cleanupFailed && !outcome.execution.success) {
|
|
27570
|
+
throw new AggregateError([remoteUpgradeFailure(outcome.execution), outcome.cleanupError], "Remote upgrade failed and helper cleanup did not complete");
|
|
27786
27571
|
}
|
|
27787
|
-
if (
|
|
27788
|
-
throw cleanupError;
|
|
27789
|
-
if (!execution.success)
|
|
27790
|
-
throw remoteUpgradeFailure(execution);
|
|
27791
|
-
return execution;
|
|
27572
|
+
if (outcome.cleanupFailed)
|
|
27573
|
+
throw outcome.cleanupError;
|
|
27574
|
+
if (!outcome.execution.success)
|
|
27575
|
+
throw remoteUpgradeFailure(outcome.execution);
|
|
27576
|
+
return outcome.execution;
|
|
27792
27577
|
}
|
|
27793
27578
|
async function executeOfficialUpgrade(ssh, helperPath, command, timeoutMs) {
|
|
27794
27579
|
let execution;
|
|
27580
|
+
let executionFailed = false;
|
|
27795
27581
|
let executionError;
|
|
27796
27582
|
let helperPrepared = false;
|
|
27797
27583
|
try {
|
|
27798
27584
|
await prepareRemoteUpgradeHelperDirectory(ssh, helperPath);
|
|
27799
27585
|
helperPrepared = true;
|
|
27800
27586
|
await ssh.uploadText(helperPath, release_assets_default, 384);
|
|
27587
|
+
const trustedRootPath = join2(dirname(helperPath), SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_FILENAME);
|
|
27588
|
+
await ssh.uploadText(trustedRootPath, SIGSTORE_PUBLIC_GOOD_TRUSTED_ROOT_JSONL, 384);
|
|
27801
27589
|
execution = await ssh.exec(command, timeoutMs);
|
|
27802
27590
|
} catch (error) {
|
|
27591
|
+
executionFailed = true;
|
|
27803
27592
|
executionError = error;
|
|
27804
27593
|
}
|
|
27594
|
+
let cleanupFailed = false;
|
|
27805
27595
|
let cleanupError;
|
|
27806
27596
|
if (helperPrepared) {
|
|
27807
27597
|
try {
|
|
27808
27598
|
await removeRemoteUpgradeHelper(ssh, helperPath);
|
|
27809
27599
|
} catch (error) {
|
|
27600
|
+
cleanupFailed = true;
|
|
27810
27601
|
cleanupError = error;
|
|
27811
27602
|
}
|
|
27812
27603
|
}
|
|
27813
|
-
return officialUpgradeOutcome(
|
|
27604
|
+
return officialUpgradeOutcome({
|
|
27605
|
+
execution,
|
|
27606
|
+
executionFailed,
|
|
27607
|
+
executionError,
|
|
27608
|
+
cleanupFailed,
|
|
27609
|
+
cleanupError
|
|
27610
|
+
});
|
|
27814
27611
|
}
|
|
27815
27612
|
function assertSafeGithubProxy(value) {
|
|
27816
27613
|
const trimmed = value.trim();
|
|
@@ -27949,14 +27746,366 @@ function assertSafeExecCommand(command) {
|
|
|
27949
27746
|
}
|
|
27950
27747
|
return reject();
|
|
27951
27748
|
}
|
|
27749
|
+
async function runFixedRemoteCommand(ssh, command, timeoutMs) {
|
|
27750
|
+
try {
|
|
27751
|
+
return await ssh.exec(command, timeoutMs);
|
|
27752
|
+
} catch {
|
|
27753
|
+
return null;
|
|
27754
|
+
}
|
|
27755
|
+
}
|
|
27756
|
+
function singleSystemdProperty(output, property) {
|
|
27757
|
+
const prefix = `${property}=`;
|
|
27758
|
+
const matches = output.split(/\r?\n/).filter((line) => line.startsWith(prefix));
|
|
27759
|
+
return matches.length === 1 ? matches[0].slice(prefix.length) : null;
|
|
27760
|
+
}
|
|
27761
|
+
function strictExecStartPath(execStart) {
|
|
27762
|
+
if (execStart.length > 4096 || /[\r\n\0]/.test(execStart))
|
|
27763
|
+
return null;
|
|
27764
|
+
const matches = Array.from(execStart.matchAll(/(?:^|[{;]\s*)path=([^;}]+?)\s*(?=;|}|$)/g));
|
|
27765
|
+
if (matches.length !== 1)
|
|
27766
|
+
return null;
|
|
27767
|
+
const executablePath = matches[0]?.[1]?.trim() || "";
|
|
27768
|
+
return /^\/[a-zA-Z0-9._/-]+$/.test(executablePath) ? executablePath : null;
|
|
27769
|
+
}
|
|
27770
|
+
function execStartEvidence(output) {
|
|
27771
|
+
if (output.length > 8192 || /\0/.test(output)) {
|
|
27772
|
+
return { status: "error", path: null, error: "systemd_output_invalid" };
|
|
27773
|
+
}
|
|
27774
|
+
if (!output.trim())
|
|
27775
|
+
return { status: "unknown", path: null, error: "unit_missing" };
|
|
27776
|
+
const loadState = singleSystemdProperty(output, "LoadState");
|
|
27777
|
+
const execStart = singleSystemdProperty(output, "ExecStart");
|
|
27778
|
+
if (loadState === null || execStart === null) {
|
|
27779
|
+
return { status: "error", path: null, error: "systemd_output_invalid" };
|
|
27780
|
+
}
|
|
27781
|
+
if (loadState !== "loaded")
|
|
27782
|
+
return { status: "unknown", path: null, error: "unit_not_loaded" };
|
|
27783
|
+
if (execStart === "")
|
|
27784
|
+
return { status: "unknown", path: null, error: "exec_start_missing" };
|
|
27785
|
+
const executablePath = strictExecStartPath(execStart);
|
|
27786
|
+
return executablePath ? { status: "ok", path: executablePath, error: null } : { status: "error", path: null, error: "exec_start_invalid" };
|
|
27787
|
+
}
|
|
27788
|
+
function stableVersion(output) {
|
|
27789
|
+
if (output.length > BINARY_VERSION_MAX_BYTES || /\0/.test(output))
|
|
27790
|
+
return null;
|
|
27791
|
+
const versionTokens = output.split(VERSION_TOKEN_SEPARATOR).filter((token) => SEMVER_CORE_TOKEN.test(token));
|
|
27792
|
+
const versions = versionTokens.map((token) => token.startsWith("v") ? token.slice(1) : token);
|
|
27793
|
+
if (versions.length === 0 || versions.some((version) => !EXACT_STABLE_SEMVER.test(version))) {
|
|
27794
|
+
return null;
|
|
27795
|
+
}
|
|
27796
|
+
const uniqueVersions = new Set(versions);
|
|
27797
|
+
return uniqueVersions.size === 1 ? [...uniqueVersions][0] : null;
|
|
27798
|
+
}
|
|
27799
|
+
function stableSha256(output) {
|
|
27800
|
+
return /^[0-9a-f]{64}$/.test(output) ? output : null;
|
|
27801
|
+
}
|
|
27802
|
+
function unavailableBinaryEvidence(definition, evidence) {
|
|
27803
|
+
return {
|
|
27804
|
+
status: evidence.status,
|
|
27805
|
+
version: null,
|
|
27806
|
+
sha256: null,
|
|
27807
|
+
path: evidence.path,
|
|
27808
|
+
source: definition.source,
|
|
27809
|
+
error: evidence.error
|
|
27810
|
+
};
|
|
27811
|
+
}
|
|
27812
|
+
function canonicalBase64Text(encoded, maximumBytes) {
|
|
27813
|
+
if (encoded.length > Math.ceil(maximumBytes / 3) * 4 || encoded.length % 4 !== 0)
|
|
27814
|
+
return null;
|
|
27815
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded))
|
|
27816
|
+
return null;
|
|
27817
|
+
const decodedBytes = Buffer2.from(encoded, "base64");
|
|
27818
|
+
if (decodedBytes.length > maximumBytes || decodedBytes.toString("base64") !== encoded)
|
|
27819
|
+
return null;
|
|
27820
|
+
try {
|
|
27821
|
+
return new TextDecoder2("utf-8", { fatal: true }).decode(decodedBytes);
|
|
27822
|
+
} catch {
|
|
27823
|
+
return null;
|
|
27824
|
+
}
|
|
27825
|
+
}
|
|
27826
|
+
function taggedProbeValue(line, label) {
|
|
27827
|
+
return line?.startsWith(label) ? line.slice(label.length) : null;
|
|
27828
|
+
}
|
|
27829
|
+
function binaryProbeFields(output) {
|
|
27830
|
+
if (output.length > 8192 || /[\r\0]/.test(output))
|
|
27831
|
+
return null;
|
|
27832
|
+
const lines = (output.endsWith(`
|
|
27833
|
+
`) ? output.slice(0, -1) : output).split(`
|
|
27834
|
+
`);
|
|
27835
|
+
if (lines.length < 2 || lines.length > 3)
|
|
27836
|
+
return null;
|
|
27837
|
+
const hashBefore = taggedProbeValue(lines[0], BINARY_HASH_BEFORE_LABEL);
|
|
27838
|
+
const versionBase64 = taggedProbeValue(lines[1], BINARY_VERSION_LABEL);
|
|
27839
|
+
const hashAfter = lines.length === 3 ? taggedProbeValue(lines[2], BINARY_HASH_AFTER_LABEL) : null;
|
|
27840
|
+
if (hashBefore === null || versionBase64 === null || lines.length === 3 && hashAfter === null)
|
|
27841
|
+
return null;
|
|
27842
|
+
const versionOutput = canonicalBase64Text(versionBase64, BINARY_VERSION_MAX_BYTES);
|
|
27843
|
+
return versionOutput === null ? null : { versionOutput, hashBefore, hashAfter };
|
|
27844
|
+
}
|
|
27845
|
+
function completedBinaryProbe(fields) {
|
|
27846
|
+
const version = stableVersion(fields.versionOutput);
|
|
27847
|
+
const hashBefore = stableSha256(fields.hashBefore);
|
|
27848
|
+
const hashAfter = fields.hashAfter === null ? null : stableSha256(fields.hashAfter);
|
|
27849
|
+
if (hashBefore && hashAfter && hashBefore !== hashAfter) {
|
|
27850
|
+
return { version: null, sha256: null, error: "binary_changed_during_probe" };
|
|
27851
|
+
}
|
|
27852
|
+
const sha2562 = hashBefore && hashAfter ? hashBefore : null;
|
|
27853
|
+
if (!version)
|
|
27854
|
+
return { version: null, sha256: sha2562, error: "version_output_invalid" };
|
|
27855
|
+
return sha2562 ? { version, sha256: sha2562, error: null } : { version, sha256: null, error: "sha256_output_invalid" };
|
|
27856
|
+
}
|
|
27857
|
+
function reservedBinaryProbeFailure(execution, fields) {
|
|
27858
|
+
if (execution.code === PROBE_CHANGED_DURING_READ) {
|
|
27859
|
+
return { version: null, sha256: null, error: "binary_changed_during_probe" };
|
|
27860
|
+
}
|
|
27861
|
+
if (execution.code === BINARY_HASH_BEFORE_FAILED) {
|
|
27862
|
+
return { version: null, sha256: null, error: "sha256_probe_failed" };
|
|
27863
|
+
}
|
|
27864
|
+
if (execution.code === BINARY_VERSION_FAILED) {
|
|
27865
|
+
return { version: null, sha256: null, error: "version_probe_failed" };
|
|
27866
|
+
}
|
|
27867
|
+
if (execution.code === BINARY_HASH_AFTER_FAILED) {
|
|
27868
|
+
const version = fields ? stableVersion(fields.versionOutput) : null;
|
|
27869
|
+
return version ? { version, sha256: null, error: "sha256_probe_failed" } : { version: null, sha256: null, error: fields ? "version_output_invalid" : "binary_probe_output_invalid" };
|
|
27870
|
+
}
|
|
27871
|
+
return null;
|
|
27872
|
+
}
|
|
27873
|
+
function binaryProbeValues(execution) {
|
|
27874
|
+
if (!execution)
|
|
27875
|
+
return { version: null, sha256: null, error: "binary_probe_transport_failed" };
|
|
27876
|
+
const fields = binaryProbeFields(execution.stdout);
|
|
27877
|
+
const reservedFailure = reservedBinaryProbeFailure(execution, fields);
|
|
27878
|
+
if (reservedFailure)
|
|
27879
|
+
return reservedFailure;
|
|
27880
|
+
if (!execution.success)
|
|
27881
|
+
return { version: null, sha256: null, error: "binary_probe_failed" };
|
|
27882
|
+
return fields ? completedBinaryProbe(fields) : { version: null, sha256: null, error: "binary_probe_output_invalid" };
|
|
27883
|
+
}
|
|
27884
|
+
async function binaryComponentEvidence(ssh, definition) {
|
|
27885
|
+
const execStart = await systemdBinaryEvidence(ssh, definition);
|
|
27886
|
+
if (execStart.status !== "ok" || !execStart.path)
|
|
27887
|
+
return unavailableBinaryEvidence(definition, execStart);
|
|
27888
|
+
const fixedProbe = definition.commands[execStart.path];
|
|
27889
|
+
if (!fixedProbe)
|
|
27890
|
+
return unavailableBinaryEvidence(definition, {
|
|
27891
|
+
status: "error",
|
|
27892
|
+
path: execStart.path,
|
|
27893
|
+
error: "exec_start_not_allowed"
|
|
27894
|
+
});
|
|
27895
|
+
const evidence = await fixedBinaryEvidence(ssh, definition, execStart.path, fixedProbe);
|
|
27896
|
+
const verifiedExecStart = await systemdBinaryEvidence(ssh, definition);
|
|
27897
|
+
if (verifiedExecStart.status !== "ok") {
|
|
27898
|
+
return unavailableBinaryEvidence(definition, {
|
|
27899
|
+
status: "error",
|
|
27900
|
+
path: execStart.path,
|
|
27901
|
+
error: "exec_start_recheck_failed"
|
|
27902
|
+
});
|
|
27903
|
+
}
|
|
27904
|
+
return verifiedExecStart.path === execStart.path ? evidence : unavailableBinaryEvidence(definition, {
|
|
27905
|
+
status: "error",
|
|
27906
|
+
path: execStart.path,
|
|
27907
|
+
error: "exec_start_changed_during_probe"
|
|
27908
|
+
});
|
|
27909
|
+
}
|
|
27910
|
+
async function systemdBinaryEvidence(ssh, definition) {
|
|
27911
|
+
const systemdCommand = `systemctl show --property=LoadState --property=ExecStart -- ${definition.unit}`;
|
|
27912
|
+
const systemdExecution = await runFixedRemoteCommand(ssh, systemdCommand, PLATFORM_PROBE_TIMEOUT_MS);
|
|
27913
|
+
if (!systemdExecution)
|
|
27914
|
+
return { status: "error", path: null, error: "systemd_probe_transport_failed" };
|
|
27915
|
+
if (!systemdExecution.success)
|
|
27916
|
+
return { status: "error", path: null, error: "systemd_probe_failed" };
|
|
27917
|
+
return execStartEvidence(systemdExecution.stdout);
|
|
27918
|
+
}
|
|
27919
|
+
async function fixedBinaryEvidence(ssh, definition, executablePath, fixedProbe) {
|
|
27920
|
+
const execution = await runFixedRemoteCommand(ssh, fixedProbe.command, PLATFORM_HASH_TIMEOUT_MS);
|
|
27921
|
+
const probe = binaryProbeValues(execution);
|
|
27922
|
+
return {
|
|
27923
|
+
status: probe.error ? "error" : "ok",
|
|
27924
|
+
version: probe.version,
|
|
27925
|
+
sha256: probe.sha256,
|
|
27926
|
+
path: executablePath,
|
|
27927
|
+
source: definition.source,
|
|
27928
|
+
error: probe.error
|
|
27929
|
+
};
|
|
27930
|
+
}
|
|
27931
|
+
function parseWebConsoleVersion(markerOutput) {
|
|
27932
|
+
if (markerOutput.length > WEB_CONSOLE_MARKER_MAX_BYTES || /\0/.test(markerOutput))
|
|
27933
|
+
return null;
|
|
27934
|
+
try {
|
|
27935
|
+
const marker = JSON.parse(markerOutput);
|
|
27936
|
+
if (typeof marker !== "object" || marker === null || Array.isArray(marker))
|
|
27937
|
+
return null;
|
|
27938
|
+
const fields = marker;
|
|
27939
|
+
if (fields.schema_version !== 1 || fields.component !== "web-console")
|
|
27940
|
+
return null;
|
|
27941
|
+
return typeof fields.version === "string" && EXACT_STABLE_SEMVER.test(fields.version) ? fields.version : null;
|
|
27942
|
+
} catch {
|
|
27943
|
+
return null;
|
|
27944
|
+
}
|
|
27945
|
+
}
|
|
27946
|
+
var WEB_CONSOLE_PROBE_COMMAND = [
|
|
27947
|
+
"set -o pipefail",
|
|
27948
|
+
`ROOT=${quoteEnvValue(WEB_CONSOLE_CURRENT_DIR)}`,
|
|
27949
|
+
'if [ ! -e "$ROOT" ] && [ ! -L "$ROOT" ]; then exit 43; fi',
|
|
27950
|
+
`test -d "$ROOT" || exit ${WEB_CONSOLE_ROOT_INVALID}`,
|
|
27951
|
+
`ROOT_REAL_BEFORE=$(readlink -f -- "$ROOT") || exit ${WEB_CONSOLE_ROOT_INVALID}`,
|
|
27952
|
+
`case "$ROOT_REAL_BEFORE" in "$ROOT"|/opt/supacloud/web-console/releases/*) ;; *) exit ${WEB_CONSOLE_ROOT_INVALID} ;; esac`,
|
|
27953
|
+
`ROOT_ID_BEFORE=$(stat -c '%d:%i' -- "$ROOT_REAL_BEFORE") || exit ${WEB_CONSOLE_ROOT_INVALID}`,
|
|
27954
|
+
"MARKER_BEFORE=$ROOT_REAL_BEFORE/.supacloud-component.json",
|
|
27955
|
+
`test ! -L "$MARKER_BEFORE" || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27956
|
+
`test -e "$MARKER_BEFORE" || exit ${WEB_CONSOLE_MARKER_MISSING}`,
|
|
27957
|
+
`test -f "$MARKER_BEFORE" || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27958
|
+
`test "$(stat -c '%s' -- "$MARKER_BEFORE")" -le ${WEB_CONSOLE_MARKER_MAX_BYTES} || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27959
|
+
`MARKER_BASE64_BEFORE=$(head -c ${WEB_CONSOLE_MARKER_MAX_BYTES + 1} -- "$MARKER_BEFORE" | base64 | tr -d '\\n') || exit ${WEB_CONSOLE_MARKER_INVALID}`,
|
|
27960
|
+
"web_tree_sha256() {",
|
|
27961
|
+
" local TREE_ROOT INVALID_ENTRY TREE_DIGEST",
|
|
27962
|
+
" TREE_ROOT=$1",
|
|
27963
|
+
` INVALID_ENTRY=$(cd -- "$TREE_ROOT" && find . -xdev ! -type d ! -type f -print -quit) || return ${WEB_CONSOLE_TREE_FAILED}`,
|
|
27964
|
+
` test -z "$INVALID_ENTRY" || return ${WEB_CONSOLE_TREE_INVALID}`,
|
|
27965
|
+
` TREE_DIGEST=$(cd -- "$TREE_ROOT" && find . -xdev -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum -- | sha256sum | awk '{print $1}') || return ${WEB_CONSOLE_TREE_FAILED}`,
|
|
27966
|
+
` printf '%s' "$TREE_DIGEST"`,
|
|
27967
|
+
"}",
|
|
27968
|
+
'TREE_BEFORE=$(web_tree_sha256 "$ROOT_REAL_BEFORE")',
|
|
27969
|
+
"TREE_STATUS=$?",
|
|
27970
|
+
'test "$TREE_STATUS" -eq 0 || exit "$TREE_STATUS"',
|
|
27971
|
+
'TREE_AFTER=$(web_tree_sha256 "$ROOT_REAL_BEFORE")',
|
|
27972
|
+
"TREE_STATUS=$?",
|
|
27973
|
+
`test "$TREE_STATUS" -eq 0 || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27974
|
+
"MARKER_AFTER=$ROOT_REAL_BEFORE/.supacloud-component.json",
|
|
27975
|
+
`test ! -L "$MARKER_AFTER" && test -f "$MARKER_AFTER" || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27976
|
+
`test "$(stat -c '%s' -- "$MARKER_AFTER")" -le ${WEB_CONSOLE_MARKER_MAX_BYTES} || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27977
|
+
`MARKER_BASE64_AFTER=$(head -c ${WEB_CONSOLE_MARKER_MAX_BYTES + 1} -- "$MARKER_AFTER" | base64 | tr -d '\\n') || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27978
|
+
`test -d "$ROOT" || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27979
|
+
`ROOT_REAL_AFTER=$(readlink -f -- "$ROOT") || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27980
|
+
`ROOT_ID_AFTER=$(stat -c '%d:%i' -- "$ROOT_REAL_AFTER") || exit ${PROBE_CHANGED_DURING_READ}`,
|
|
27981
|
+
`printf '${WEB_ROOT_REAL_BEFORE_LABEL}%s\\n' "$ROOT_REAL_BEFORE"`,
|
|
27982
|
+
`printf '${WEB_ROOT_ID_BEFORE_LABEL}%s\\n' "$ROOT_ID_BEFORE"`,
|
|
27983
|
+
`printf '${WEB_MARKER_BEFORE_LABEL}%s\\n' "$MARKER_BASE64_BEFORE"`,
|
|
27984
|
+
`printf '${WEB_TREE_BEFORE_LABEL}%s\\n' "$TREE_BEFORE"`,
|
|
27985
|
+
`printf '${WEB_TREE_AFTER_LABEL}%s\\n' "$TREE_AFTER"`,
|
|
27986
|
+
`printf '${WEB_MARKER_AFTER_LABEL}%s\\n' "$MARKER_BASE64_AFTER"`,
|
|
27987
|
+
`printf '${WEB_ROOT_REAL_AFTER_LABEL}%s\\n' "$ROOT_REAL_AFTER"`,
|
|
27988
|
+
`printf '${WEB_ROOT_ID_AFTER_LABEL}%s\\n' "$ROOT_ID_AFTER"`
|
|
27989
|
+
].join(`
|
|
27990
|
+
`);
|
|
27991
|
+
var WEB_CONSOLE_PROBE_LABELS = [
|
|
27992
|
+
WEB_ROOT_REAL_BEFORE_LABEL,
|
|
27993
|
+
WEB_ROOT_ID_BEFORE_LABEL,
|
|
27994
|
+
WEB_MARKER_BEFORE_LABEL,
|
|
27995
|
+
WEB_TREE_BEFORE_LABEL,
|
|
27996
|
+
WEB_TREE_AFTER_LABEL,
|
|
27997
|
+
WEB_MARKER_AFTER_LABEL,
|
|
27998
|
+
WEB_ROOT_REAL_AFTER_LABEL,
|
|
27999
|
+
WEB_ROOT_ID_AFTER_LABEL
|
|
28000
|
+
];
|
|
28001
|
+
function taggedWebConsoleProbeValues(output) {
|
|
28002
|
+
if (output.length > 16384 || /[\r\0]/.test(output))
|
|
28003
|
+
return null;
|
|
28004
|
+
const lines = (output.endsWith(`
|
|
28005
|
+
`) ? output.slice(0, -1) : output).split(`
|
|
28006
|
+
`);
|
|
28007
|
+
if (lines.length !== WEB_CONSOLE_PROBE_LABELS.length)
|
|
28008
|
+
return null;
|
|
28009
|
+
const probeValues = WEB_CONSOLE_PROBE_LABELS.map((label, index) => taggedProbeValue(lines[index], label));
|
|
28010
|
+
return probeValues.some((probeValue) => probeValue === null) ? null : probeValues;
|
|
28011
|
+
}
|
|
28012
|
+
function webConsoleConsistencyError(probeValues) {
|
|
28013
|
+
const [rootBefore, rootIdBefore, markerBefore, treeBefore, treeAfter, markerAfter, rootAfter, rootIdAfter] = probeValues;
|
|
28014
|
+
if (rootBefore !== rootAfter || rootIdBefore !== rootIdAfter) {
|
|
28015
|
+
return "web_console_root_changed_during_probe";
|
|
28016
|
+
}
|
|
28017
|
+
if (markerBefore !== markerAfter)
|
|
28018
|
+
return "marker_changed_during_probe";
|
|
28019
|
+
return treeBefore !== treeAfter ? "tree_sha256_changed_during_probe" : null;
|
|
28020
|
+
}
|
|
28021
|
+
function webConsoleProbeFields(output) {
|
|
28022
|
+
const probeValues = taggedWebConsoleProbeValues(output);
|
|
28023
|
+
if (!probeValues)
|
|
28024
|
+
return null;
|
|
28025
|
+
const [rootBefore, rootIdBefore, markerBefore, treeBefore, , markerAfter, rootAfter, rootIdAfter] = probeValues;
|
|
28026
|
+
const supportedRoot = /^\/opt\/supacloud\/web-console\/(?:current|releases\/[A-Za-z0-9][A-Za-z0-9._-]*)$/;
|
|
28027
|
+
if (![rootBefore, rootAfter].every((root) => supportedRoot.test(root)))
|
|
28028
|
+
return null;
|
|
28029
|
+
if (![rootIdBefore, rootIdAfter].every((rootId) => /^\d+:\d+$/.test(rootId)))
|
|
28030
|
+
return null;
|
|
28031
|
+
const markerOutput = canonicalBase64Text(markerBefore, WEB_CONSOLE_MARKER_MAX_BYTES);
|
|
28032
|
+
if (markerOutput === null || canonicalBase64Text(markerAfter, WEB_CONSOLE_MARKER_MAX_BYTES) === null)
|
|
28033
|
+
return null;
|
|
28034
|
+
const consistencyError = webConsoleConsistencyError(probeValues);
|
|
28035
|
+
return {
|
|
28036
|
+
markerOutput,
|
|
28037
|
+
treeSha256: consistencyError ? null : stableSha256(treeBefore),
|
|
28038
|
+
consistencyError
|
|
28039
|
+
};
|
|
28040
|
+
}
|
|
28041
|
+
function webConsoleProbeError(execution, fields, version, treeSha256) {
|
|
28042
|
+
if (!execution)
|
|
28043
|
+
return { status: "error", error: "web_console_probe_transport_failed" };
|
|
28044
|
+
if (execution.code === WEB_CONSOLE_ROOT_MISSING)
|
|
28045
|
+
return { status: "unknown", error: "web_console_missing" };
|
|
28046
|
+
if (execution.code === WEB_CONSOLE_MARKER_MISSING)
|
|
28047
|
+
return { status: "unknown", error: "marker_missing" };
|
|
28048
|
+
if (execution.code === PROBE_CHANGED_DURING_READ) {
|
|
28049
|
+
return { status: "error", error: "web_console_changed_during_probe" };
|
|
28050
|
+
}
|
|
28051
|
+
if (execution.code === WEB_CONSOLE_ROOT_INVALID || execution.code === WEB_CONSOLE_TREE_INVALID) {
|
|
28052
|
+
return { status: "error", error: "web_console_invalid_file" };
|
|
28053
|
+
}
|
|
28054
|
+
if (execution.code === WEB_CONSOLE_MARKER_INVALID)
|
|
28055
|
+
return { status: "error", error: "marker_invalid" };
|
|
28056
|
+
if (execution.code === WEB_CONSOLE_TREE_FAILED)
|
|
28057
|
+
return { status: "error", error: "tree_sha256_probe_failed" };
|
|
28058
|
+
if (!execution.success)
|
|
28059
|
+
return { status: "error", error: "web_console_probe_failed" };
|
|
28060
|
+
if (!fields)
|
|
28061
|
+
return { status: "error", error: "web_console_probe_output_invalid" };
|
|
28062
|
+
if (fields.consistencyError)
|
|
28063
|
+
return { status: "error", error: fields.consistencyError };
|
|
28064
|
+
if (!version)
|
|
28065
|
+
return { status: "error", error: "marker_invalid" };
|
|
28066
|
+
return treeSha256 ? { status: "ok", error: null } : { status: "error", error: "tree_sha256_output_invalid" };
|
|
28067
|
+
}
|
|
28068
|
+
async function webConsoleEvidence(ssh) {
|
|
28069
|
+
const execution = await runFixedRemoteCommand(ssh, WEB_CONSOLE_PROBE_COMMAND, WEB_CONSOLE_PROBE_TIMEOUT_MS);
|
|
28070
|
+
const fields = execution?.success ? webConsoleProbeFields(execution.stdout) : null;
|
|
28071
|
+
const version = fields && !fields.consistencyError ? parseWebConsoleVersion(fields.markerOutput) : null;
|
|
28072
|
+
const treeSha256 = fields?.treeSha256 ?? null;
|
|
28073
|
+
const state = webConsoleProbeError(execution, fields, version, treeSha256);
|
|
28074
|
+
return {
|
|
28075
|
+
status: state.status,
|
|
28076
|
+
version,
|
|
28077
|
+
tree_sha256: treeSha256,
|
|
28078
|
+
path: WEB_CONSOLE_CURRENT_DIR,
|
|
28079
|
+
source: "component_marker_and_tree_sha256",
|
|
28080
|
+
error: state.error
|
|
28081
|
+
};
|
|
28082
|
+
}
|
|
28083
|
+
async function platformVersions(ssh) {
|
|
28084
|
+
const [managementApi, edgeRuntime, caddy, webConsole] = await Promise.all([
|
|
28085
|
+
binaryComponentEvidence(ssh, BINARY_PROBES.management_api),
|
|
28086
|
+
binaryComponentEvidence(ssh, BINARY_PROBES.edge_runtime),
|
|
28087
|
+
binaryComponentEvidence(ssh, BINARY_PROBES.caddy),
|
|
28088
|
+
webConsoleEvidence(ssh)
|
|
28089
|
+
]);
|
|
28090
|
+
return {
|
|
28091
|
+
schema_version: 1,
|
|
28092
|
+
components: {
|
|
28093
|
+
management_api: managementApi,
|
|
28094
|
+
edge_runtime: edgeRuntime,
|
|
28095
|
+
caddy,
|
|
28096
|
+
web_console: webConsole
|
|
28097
|
+
}
|
|
28098
|
+
};
|
|
28099
|
+
}
|
|
27952
28100
|
function registerSshTools(server, ssh) {
|
|
27953
28101
|
server.tool("ssh", `Server management via SSH. Available before & after SupaCloud installation.
|
|
27954
|
-
Actions: ping, setup, install, upgrade, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
|
|
28102
|
+
Actions: ping, setup, install, upgrade, versions, diagnose, exec, troubleshoot, container_logs, tenant_manage, tenant_list, tenant_inspect, tenant_diagnose, tenant_migrate`, {
|
|
27955
28103
|
action: withDescription(stringEnum([
|
|
27956
28104
|
"ping",
|
|
27957
28105
|
"setup",
|
|
27958
28106
|
"install",
|
|
27959
28107
|
"upgrade",
|
|
28108
|
+
"versions",
|
|
27960
28109
|
"diagnose",
|
|
27961
28110
|
"exec",
|
|
27962
28111
|
"troubleshoot",
|
|
@@ -28033,7 +28182,7 @@ ${bootstrapDeps.stderr.slice(-500)}`;
|
|
|
28033
28182
|
break;
|
|
28034
28183
|
}
|
|
28035
28184
|
const osCheck = await ssh.exec("cat /etc/os-release | grep -E 'NAME|VERSION_ID' | head -4");
|
|
28036
|
-
const clone2 = await ssh.exec(`set -e; umask 077; rm -rf ${quoteEnvValue(BOOTSTRAP)}; ` + `${bootstrapClone}; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} remote set-url origin ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} remote get-url origin)" = ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} symbolic-ref --short HEAD)" = main; ` + `test -z "$(git -C ${quoteEnvValue(BOOTSTRAP)} status --porcelain --untracked-files=no)"; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} ls-files --error-unmatch setup.sh scripts/lib/install_config.sh scripts/lib/release_assets.sh >/dev/null; ` + `test -f ${quoteEnvValue(`${BOOTSTRAP}/setup.sh`)}; echo BOOTSTRAP_OK`, 120000);
|
|
28185
|
+
const clone2 = await ssh.exec(`set -e; umask 077; rm -rf ${quoteEnvValue(BOOTSTRAP)}; ` + `${bootstrapClone}; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} remote set-url origin ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} remote get-url origin)" = ${quoteEnvValue(REPO)}; ` + `test "$(git -C ${quoteEnvValue(BOOTSTRAP)} symbolic-ref --short HEAD)" = main; ` + `test -z "$(git -C ${quoteEnvValue(BOOTSTRAP)} status --porcelain --untracked-files=no)"; ` + `git -C ${quoteEnvValue(BOOTSTRAP)} ls-files --error-unmatch setup.sh scripts/lib/install_config.sh scripts/lib/release_assets.sh packages/management-api/src/assets/sigstore-public-good-trusted-root.jsonl >/dev/null; ` + `test -f ${quoteEnvValue(`${BOOTSTRAP}/setup.sh`)}; echo BOOTSTRAP_OK`, 120000);
|
|
28037
28186
|
if (!clone2.stdout.includes("BOOTSTRAP_OK")) {
|
|
28038
28187
|
await ssh.exec(`rm -rf ${quoteEnvValue(BOOTSTRAP)}`);
|
|
28039
28188
|
text = `❌ Trusted bootstrap clone failed
|
|
@@ -28098,22 +28247,16 @@ ${result.stderr.slice(-500)}`;
|
|
|
28098
28247
|
break;
|
|
28099
28248
|
}
|
|
28100
28249
|
case "upgrade": {
|
|
28101
|
-
const version = args.version ? assertSafeReleaseTag(args.version) : undefined;
|
|
28102
|
-
const edgeRuntimeVersion = args.edge_runtime_version ? assertSafeReleaseTag(args.edge_runtime_version) : undefined;
|
|
28250
|
+
const version = args.version ? assertExactStableVersion2(assertSafeReleaseTag(args.version), "version") : undefined;
|
|
28251
|
+
const edgeRuntimeVersion = args.edge_runtime_version ? assertExactStableVersion2(assertSafeReleaseTag(args.edge_runtime_version), "edge_runtime_version") : undefined;
|
|
28103
28252
|
if (edgeRuntimeVersion && !version) {
|
|
28104
28253
|
throw new Error("'version' is required with 'edge_runtime_version'");
|
|
28105
28254
|
}
|
|
28106
|
-
if (edgeRuntimeVersion && version) {
|
|
28107
|
-
assertExactStableVersion2(version, "version");
|
|
28108
|
-
assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
|
|
28109
|
-
}
|
|
28110
28255
|
const validatedProxy = args.github_proxy ? assertSafeGithubProxy(args.github_proxy) : undefined;
|
|
28111
28256
|
if (args.artifact_transport === "local") {
|
|
28112
28257
|
if (!version || !edgeRuntimeVersion) {
|
|
28113
28258
|
throw new Error("Local artifact transport requires exact 'version' and 'edge_runtime_version'");
|
|
28114
28259
|
}
|
|
28115
|
-
assertExactStableVersion2(version, "version");
|
|
28116
|
-
assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
|
|
28117
28260
|
if (validatedProxy && !["direct", "none"].includes(validatedProxy.toLowerCase())) {
|
|
28118
28261
|
throw new Error("Local artifact transport only supports direct GitHub downloads");
|
|
28119
28262
|
}
|
|
@@ -28139,17 +28282,27 @@ ${result.stderr.slice(-500)}`;
|
|
|
28139
28282
|
${upgradeExecution.stdout.slice(-300)}${edgeBoundary}`;
|
|
28140
28283
|
break;
|
|
28141
28284
|
}
|
|
28285
|
+
case "versions": {
|
|
28286
|
+
text = JSON.stringify(await platformVersions(ssh), null, 2);
|
|
28287
|
+
break;
|
|
28288
|
+
}
|
|
28142
28289
|
case "diagnose": {
|
|
28143
28290
|
const cmds = [
|
|
28291
|
+
"set -o pipefail",
|
|
28144
28292
|
"echo '=== OS ===' && uname -a",
|
|
28145
28293
|
"echo '=== Memory ===' && free -h",
|
|
28146
28294
|
"echo '=== Disk ===' && df -h /",
|
|
28147
28295
|
"echo '=== Docker ===' && (docker ps --format 'table {{.Names}}\t{{.Status}}' 2>/dev/null || echo 'Not found')",
|
|
28148
|
-
"echo '===
|
|
28149
|
-
"
|
|
28296
|
+
"echo '=== Management API /health ==='",
|
|
28297
|
+
"(curl --fail --silent --show-error --max-time 5 --max-filesize 4096 http://127.0.0.1:9090/health 2>&1 | head -c 4096) || echo 'Probe failed'",
|
|
28298
|
+
"printf '\\n'",
|
|
28299
|
+
"echo '=== Management API /monitor/health ==='",
|
|
28300
|
+
"(curl --fail --silent --show-error --max-time 15 --max-filesize 16384 http://127.0.0.1:9090/monitor/health 2>&1 | head -c 16384) || echo 'Probe failed'",
|
|
28301
|
+
"printf '\\n'"
|
|
28150
28302
|
];
|
|
28151
|
-
const r = await ssh.exec(cmds.join(
|
|
28152
|
-
|
|
28303
|
+
const r = await ssh.exec(cmds.join(`
|
|
28304
|
+
`), 30000);
|
|
28305
|
+
text = redactTenantConfig((r.stdout || r.stderr).slice(0, 32768));
|
|
28153
28306
|
break;
|
|
28154
28307
|
}
|
|
28155
28308
|
case "exec": {
|
|
@@ -28984,7 +29137,7 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
|
|
|
28984
29137
|
// package.json
|
|
28985
29138
|
var package_default = {
|
|
28986
29139
|
name: "@supacloud/admin",
|
|
28987
|
-
version: "0.
|
|
29140
|
+
version: "0.8.0",
|
|
28988
29141
|
description: "Platform administration CLI for SupaCloud operators",
|
|
28989
29142
|
type: "module",
|
|
28990
29143
|
main: "./dist/index.js",
|
|
@@ -29054,6 +29207,7 @@ var sshActionSchema = stringEnum([
|
|
|
29054
29207
|
"setup",
|
|
29055
29208
|
"install",
|
|
29056
29209
|
"upgrade",
|
|
29210
|
+
"versions",
|
|
29057
29211
|
"diagnose",
|
|
29058
29212
|
"exec",
|
|
29059
29213
|
"troubleshoot",
|
|
@@ -29108,6 +29262,7 @@ EXAMPLES
|
|
|
29108
29262
|
|
|
29109
29263
|
supacloud-admin status
|
|
29110
29264
|
supacloud-admin ssh ping
|
|
29265
|
+
supacloud-admin ssh versions
|
|
29111
29266
|
supacloud-admin ssh install --public_domain api.example.com --studio_domain studio.example.com
|
|
29112
29267
|
supacloud-admin project create --name my-app
|
|
29113
29268
|
supacloud-admin project list
|