@supacloud/admin 0.7.6 → 0.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +38 -14
  2. package/dist/index.js +1659 -73
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24926,10 +24926,12 @@ import { realpathSync } from "node:fs";
24926
24926
 
24927
24927
  // src/shared/transports/ssh.ts
24928
24928
  var import_ssh2 = __toESM(require_lib3(), 1);
24929
- import { timingSafeEqual } from "node:crypto";
24929
+ import { randomUUID, timingSafeEqual } from "node:crypto";
24930
24930
  import { readFileSync } from "node:fs";
24931
24931
  var DEFAULT_MAX_OUTPUT_BYTES = 1024 * 1024;
24932
24932
  var MAX_CONFIGURABLE_OUTPUT_BYTES = 16 * 1024 * 1024;
24933
+ var DEFAULT_UPLOAD_TIMEOUT_MS = 10 * 60000;
24934
+ var DEFAULT_TEXT_UPLOAD_TIMEOUT_MS = 60000;
24933
24935
  function normalizeSshHostFingerprint(value) {
24934
24936
  const trimmed = value.trim();
24935
24937
  const match = trimmed.match(/^SHA256:([A-Za-z0-9+/]{43}=?)$/);
@@ -25096,6 +25098,11 @@ class SshConnectionPool {
25096
25098
  } catch {}
25097
25099
  }
25098
25100
  }
25101
+ discard(conn) {
25102
+ try {
25103
+ conn.end();
25104
+ } catch {}
25105
+ }
25099
25106
  closeAll() {
25100
25107
  for (const conn of this.pool) {
25101
25108
  try {
@@ -25130,18 +25137,20 @@ class SshTransport {
25130
25137
  }
25131
25138
  auditCommand(command, this.config.host, false);
25132
25139
  const conn = await this.pool.acquire();
25140
+ let connectionReusable = true;
25133
25141
  try {
25134
25142
  return await new Promise((resolve, reject) => {
25135
25143
  const outputLimit = this.config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
25136
25144
  const stdout = new BoundedOutputCollector(outputLimit);
25137
25145
  const stderr = new BoundedOutputCollector(outputLimit);
25138
25146
  const timer = setTimeout(() => {
25139
- conn.end();
25147
+ connectionReusable = false;
25140
25148
  reject(new Error(`SSH command timed out after ${timeoutMs}ms`));
25141
25149
  }, timeoutMs);
25142
25150
  conn.exec(command, (err, stream) => {
25143
25151
  if (err) {
25144
25152
  clearTimeout(timer);
25153
+ connectionReusable = false;
25145
25154
  return reject(err);
25146
25155
  }
25147
25156
  stream.on("close", (code) => {
@@ -25154,6 +25163,10 @@ class SshTransport {
25154
25163
  stdoutTruncated: stdout.truncated,
25155
25164
  stderrTruncated: stderr.truncated
25156
25165
  });
25166
+ }).on("error", (streamError) => {
25167
+ clearTimeout(timer);
25168
+ connectionReusable = false;
25169
+ reject(streamError);
25157
25170
  }).on("data", (data) => {
25158
25171
  stdout.append(data);
25159
25172
  }).stderr.on("data", (data) => {
@@ -25162,48 +25175,66 @@ class SshTransport {
25162
25175
  });
25163
25176
  });
25164
25177
  } finally {
25165
- this.pool.release(conn);
25178
+ if (connectionReusable)
25179
+ this.pool.release(conn);
25180
+ else
25181
+ this.pool.discard(conn);
25166
25182
  }
25167
25183
  }
25168
- async upload(localPath, remotePath) {
25169
- const conn = await this.pool.acquire();
25170
- try {
25171
- return await new Promise((resolve, reject) => {
25172
- conn.sftp((err, sftp) => {
25173
- if (err)
25174
- return reject(err);
25175
- sftp.fastPut(localPath, remotePath, (err2) => {
25176
- if (err2)
25177
- return reject(err2);
25178
- resolve();
25179
- });
25180
- });
25181
- });
25182
- } finally {
25183
- this.pool.release(conn);
25184
- }
25184
+ async upload(localPath, remotePath, options = {}) {
25185
+ const mode = normalizeUploadMode(options.mode);
25186
+ const timeoutMs = normalizeUploadTimeout(options.timeoutMs, DEFAULT_UPLOAD_TIMEOUT_MS);
25187
+ const partialPath = `${remotePath}.part-${randomUUID()}`;
25188
+ auditCommand(`upload ${remotePath} (local content redacted)`, this.config.host, false);
25189
+ await this.runSftpOperation(timeoutMs, async (sftp) => {
25190
+ try {
25191
+ await sftpFastPut(sftp, localPath, partialPath);
25192
+ await sftpChmod(sftp, partialPath, mode);
25193
+ await sftpRename(sftp, partialPath, remotePath);
25194
+ } catch (error) {
25195
+ await removePartialUpload(sftp, partialPath, error);
25196
+ }
25197
+ });
25185
25198
  }
25186
25199
  async uploadText(remotePath, content, mode = 384) {
25187
25200
  auditCommand(`upload ${remotePath} (${Buffer.byteLength(content)} bytes; content redacted)`, this.config.host, false);
25201
+ const normalizedMode = normalizeUploadMode(mode);
25202
+ const partialPath = `${remotePath}.part-${randomUUID()}`;
25203
+ await this.runSftpOperation(DEFAULT_TEXT_UPLOAD_TIMEOUT_MS, async (sftp) => {
25204
+ try {
25205
+ await sftpWriteFile(sftp, partialPath, content, normalizedMode);
25206
+ await sftpChmod(sftp, partialPath, normalizedMode);
25207
+ await sftpRename(sftp, partialPath, remotePath);
25208
+ } catch (error) {
25209
+ await removePartialUpload(sftp, partialPath, error);
25210
+ }
25211
+ });
25212
+ }
25213
+ async runSftpOperation(timeoutMs, operation) {
25188
25214
  const conn = await this.pool.acquire();
25215
+ let connectionReusable = true;
25216
+ let timeoutHandle;
25189
25217
  try {
25190
- await new Promise((resolve, reject) => {
25191
- conn.sftp((err, sftp) => {
25192
- if (err)
25193
- return reject(err);
25194
- sftp.writeFile(remotePath, content, { mode }, (writeError) => {
25195
- if (writeError)
25196
- return reject(writeError);
25197
- sftp.chmod(remotePath, mode, (chmodError) => {
25198
- if (chmodError)
25199
- return reject(chmodError);
25200
- resolve();
25201
- });
25202
- });
25203
- });
25204
- });
25218
+ const operationPromise = openSftp(conn).then(operation);
25219
+ await Promise.race([
25220
+ operationPromise,
25221
+ new Promise((_resolve, reject) => {
25222
+ timeoutHandle = setTimeout(() => {
25223
+ connectionReusable = false;
25224
+ reject(new Error(`SFTP upload timed out after ${timeoutMs}ms`));
25225
+ }, timeoutMs);
25226
+ })
25227
+ ]);
25228
+ } catch (error) {
25229
+ connectionReusable = false;
25230
+ throw error;
25205
25231
  } finally {
25206
- this.pool.release(conn);
25232
+ if (timeoutHandle)
25233
+ clearTimeout(timeoutHandle);
25234
+ if (connectionReusable)
25235
+ this.pool.release(conn);
25236
+ else
25237
+ this.pool.discard(conn);
25207
25238
  }
25208
25239
  }
25209
25240
  async ping() {
@@ -25214,6 +25245,58 @@ class SshTransport {
25214
25245
  this.pool.closeAll();
25215
25246
  }
25216
25247
  }
25248
+ function normalizeUploadMode(mode = 384) {
25249
+ if (!Number.isInteger(mode) || mode < 0 || mode > 511)
25250
+ throw new Error("Upload mode must be an octal permission between 000 and 777");
25251
+ return mode;
25252
+ }
25253
+ function normalizeUploadTimeout(timeoutMs, fallback) {
25254
+ const resolved = timeoutMs ?? fallback;
25255
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > 60 * 60000) {
25256
+ throw new Error("Upload timeout must be between 1ms and 1 hour");
25257
+ }
25258
+ return resolved;
25259
+ }
25260
+ function openSftp(conn) {
25261
+ return new Promise((resolve, reject) => {
25262
+ conn.sftp((error, sftp) => error ? reject(error) : resolve(sftp));
25263
+ });
25264
+ }
25265
+ function sftpFastPut(sftp, localPath, remotePath) {
25266
+ return new Promise((resolve, reject) => {
25267
+ sftp.fastPut(localPath, remotePath, (error) => error ? reject(error) : resolve());
25268
+ });
25269
+ }
25270
+ function sftpWriteFile(sftp, remotePath, content, mode) {
25271
+ return new Promise((resolve, reject) => {
25272
+ sftp.writeFile(remotePath, content, { mode }, (error) => error ? reject(error) : resolve());
25273
+ });
25274
+ }
25275
+ function sftpChmod(sftp, remotePath, mode) {
25276
+ return new Promise((resolve, reject) => {
25277
+ sftp.chmod(remotePath, mode, (error) => error ? reject(error) : resolve());
25278
+ });
25279
+ }
25280
+ function sftpRename(sftp, sourcePath, destinationPath) {
25281
+ return new Promise((resolve, reject) => {
25282
+ sftp.rename(sourcePath, destinationPath, (error) => error ? reject(error) : resolve());
25283
+ });
25284
+ }
25285
+ function sftpUnlink(sftp, remotePath) {
25286
+ return new Promise((resolve, reject) => {
25287
+ sftp.unlink(remotePath, (error) => error ? reject(error) : resolve());
25288
+ });
25289
+ }
25290
+ async function removePartialUpload(sftp, partialPath, uploadError) {
25291
+ try {
25292
+ await sftpUnlink(sftp, partialPath);
25293
+ } catch (cleanupError) {
25294
+ if (cleanupError.code === 2)
25295
+ throw uploadError;
25296
+ throw new AggregateError([uploadError, cleanupError], `SFTP upload failed and partial cleanup did not complete: ${partialPath}`);
25297
+ }
25298
+ throw uploadError;
25299
+ }
25217
25300
 
25218
25301
  // src/shared/cli.ts
25219
25302
  function coerceCliValue(value) {
@@ -25231,7 +25314,10 @@ function sanitizedCliDiagnostic(error) {
25231
25314
  }
25232
25315
  function nestedCliDiagnostics(error) {
25233
25316
  if (error instanceof AggregateError) {
25234
- return error.errors.flatMap((candidate) => nestedCliDiagnostics(candidate));
25317
+ return [
25318
+ sanitizedCliDiagnostic(error),
25319
+ ...error.errors.flatMap((candidate) => nestedCliDiagnostics(candidate))
25320
+ ];
25235
25321
  }
25236
25322
  return [sanitizedCliDiagnostic(error)];
25237
25323
  }
@@ -25613,7 +25699,1376 @@ class HttpTransport {
25613
25699
  }
25614
25700
 
25615
25701
  // src/shared/tools/ssh-tools.ts
25616
- import { randomUUID } from "node:crypto";
25702
+ import { randomUUID as randomUUID4 } from "node:crypto";
25703
+ import { dirname } from "node:path";
25704
+
25705
+ // src/shared/releases/local-upgrade-transfer.ts
25706
+ import { randomUUID as randomUUID3 } from "node:crypto";
25707
+ import { basename as basename2 } from "node:path";
25708
+
25709
+ // ../management-api/src/upgrade-lock.ts
25710
+ var SUPACLOUD_UPGRADE_LOCK_PATH = "/run/lock/supacloud-upgrade.lock";
25711
+ var INHERITED_UPGRADE_LOCK_FD = 9;
25712
+ var UPGRADE_LOCK_FD_ENV = "SUPACLOUD_UPGRADE_LOCK_FD";
25713
+ function buildUpgradeLockScript(lockPath) {
25714
+ return [
25715
+ `UPGRADE_LOCK=${quoteShell(lockPath)}`,
25716
+ `test ! -L "$UPGRADE_LOCK" || { echo 'SupaCloud upgrade lock must not be a symlink' >&2; exit 1; }`,
25717
+ 'if [ ! -e "$UPGRADE_LOCK" ]; then (umask 077; : >> "$UPGRADE_LOCK"); fi',
25718
+ `test -f "$UPGRADE_LOCK" && test ! -L "$UPGRADE_LOCK" || { echo 'SupaCloud upgrade lock must be a regular file' >&2; exit 1; }`,
25719
+ `test "$(stat -c '%u:%g' "$UPGRADE_LOCK")" = "$(id -u):$(id -g)" || { echo 'SupaCloud upgrade lock has an unexpected owner' >&2; exit 1; }`,
25720
+ `LOCK_MODE=$(stat -c '%a' "$UPGRADE_LOCK")`,
25721
+ `case "$LOCK_MODE" in [0-7]|[0-7][0-7]|[0-7][0-7][0-7]) ;; *) echo 'SupaCloud upgrade lock has special permission bits' >&2; exit 1 ;; esac`,
25722
+ "(( (8#$LOCK_MODE & 0022) == 0 )) || { echo 'SupaCloud upgrade lock is group/other writable' >&2; exit 1; }",
25723
+ `exec ${INHERITED_UPGRADE_LOCK_FD}<>"$UPGRADE_LOCK"`,
25724
+ `flock -E 75 -n ${INHERITED_UPGRADE_LOCK_FD} || { echo 'Another SupaCloud upgrade is already running' >&2; exit 75; }`,
25725
+ `export ${UPGRADE_LOCK_FD_ENV}=${INHERITED_UPGRADE_LOCK_FD}`
25726
+ ].join(`
25727
+ `);
25728
+ }
25729
+ function quoteShell(shellText) {
25730
+ return `'${shellText.split("'").join("'\\''")}'`;
25731
+ }
25732
+
25733
+ // src/shared/releases/local-upgrade-bundle.ts
25734
+ import { spawn } from "node:child_process";
25735
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
25736
+ import { accessSync, chmodSync, constants as fsConstants, createWriteStream, mkdirSync, mkdtempSync, readFileSync as readFileSync3, rmSync, statSync, writeFileSync } from "node:fs";
25737
+ import { get } from "node:https";
25738
+ import { tmpdir } from "node:os";
25739
+ import { basename, delimiter, join } from "node:path";
25740
+ import { pipeline } from "node:stream/promises";
25741
+ import { Transform as Transform2 } from "node:stream";
25742
+
25743
+ // ../management-api/src/release-manifest.ts
25744
+ var RELEASE_MANIFEST_NAME = "SUPACLOUD-RELEASE.json";
25745
+ var RELEASE_ATTESTATION_NAME = "SUPACLOUD-RELEASE.attestation.jsonl";
25746
+ var RELEASE_CHECKSUMS_NAME = "SHA256SUMS";
25747
+ var RELEASE_REPOSITORY = "zuohuadong/supacloud";
25748
+ var RELEASE_SOURCE_REF = "refs/heads/main";
25749
+ var RELEASE_SIGNER_WORKFLOW = `${RELEASE_REPOSITORY}/.github/workflows/release-please.yml`;
25750
+ var MEBIBYTE = 1024 * 1024;
25751
+ var RELEASE_BUNDLE_SIZE_LIMITS = {
25752
+ manifest: MEBIBYTE,
25753
+ checksums: MEBIBYTE,
25754
+ attestation: 32 * MEBIBYTE,
25755
+ managementBinary: 160 * MEBIBYTE,
25756
+ edgeRuntimeBinary: 160 * MEBIBYTE,
25757
+ webConsole: 64 * MEBIBYTE,
25758
+ caddy: 96 * MEBIBYTE,
25759
+ total: 384 * MEBIBYTE
25760
+ };
25761
+ var RELEASE_ASSETS = {
25762
+ "management-api": [
25763
+ "SHA256SUMS",
25764
+ "SHA256SUMS.caddy",
25765
+ "supacloud-caddy-linux-amd64",
25766
+ "supacloud-caddy-linux-arm64",
25767
+ "supacloud-linux-amd64",
25768
+ "supacloud-linux-arm64",
25769
+ "supacloud-macos-amd64",
25770
+ "supacloud-macos-arm64",
25771
+ "web-console-build.tar.gz"
25772
+ ],
25773
+ "edge-runtime": [
25774
+ "SHA256SUMS",
25775
+ "supacloud-edge-runtime-linux-amd64",
25776
+ "supacloud-edge-runtime-linux-arm64",
25777
+ "supacloud-edge-runtime-macos-amd64",
25778
+ "supacloud-edge-runtime-macos-arm64"
25779
+ ]
25780
+ };
25781
+ function manifestObject(candidate, label) {
25782
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
25783
+ throw new Error(`${label} must be a JSON object`);
25784
+ }
25785
+ return candidate;
25786
+ }
25787
+ function assertExactKeys(record, keys, label) {
25788
+ const actual = Object.keys(record).sort();
25789
+ const expected = [...keys].sort();
25790
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
25791
+ throw new Error(`${label} contains unsupported or missing fields`);
25792
+ }
25793
+ }
25794
+ function exactStableVersion(version) {
25795
+ if (typeof version !== "string" || !/^\d+\.\d+\.\d+$/.test(version)) {
25796
+ throw new Error("Release manifest version must be an exact stable version");
25797
+ }
25798
+ return version;
25799
+ }
25800
+ function releaseTag(component, version) {
25801
+ exactStableVersion(version);
25802
+ return `${component}-v${version}`;
25803
+ }
25804
+ function releaseAssetNames(component) {
25805
+ return RELEASE_ASSETS[component];
25806
+ }
25807
+ function releaseAssetSizeLimit(component, name) {
25808
+ if (name === "SHA256SUMS" || name === "SHA256SUMS.caddy") {
25809
+ return RELEASE_BUNDLE_SIZE_LIMITS.checksums;
25810
+ }
25811
+ if (name === "web-console-build.tar.gz")
25812
+ return RELEASE_BUNDLE_SIZE_LIMITS.webConsole;
25813
+ if (name.startsWith("supacloud-caddy-"))
25814
+ return RELEASE_BUNDLE_SIZE_LIMITS.caddy;
25815
+ return component === "management-api" ? RELEASE_BUNDLE_SIZE_LIMITS.managementBinary : RELEASE_BUNDLE_SIZE_LIMITS.edgeRuntimeBinary;
25816
+ }
25817
+ function parseSource(candidate) {
25818
+ const source = manifestObject(candidate, "Release manifest source");
25819
+ assertExactKeys(source, ["ref", "commit"], "Release manifest source");
25820
+ if (source.ref !== RELEASE_SOURCE_REF || typeof source.commit !== "string" || !/^[0-9a-f]{40}$/.test(source.commit)) {
25821
+ throw new Error("Release manifest source is invalid");
25822
+ }
25823
+ return { ref: RELEASE_SOURCE_REF, commit: source.commit };
25824
+ }
25825
+ function parseRelease(candidate, expected) {
25826
+ const release = manifestObject(candidate, "Release manifest release");
25827
+ assertExactKeys(release, ["component", "version", "tag"], "Release manifest release");
25828
+ const version = exactStableVersion(release.version);
25829
+ if (release.component !== expected.component || version !== expected.version || release.tag !== releaseTag(expected.component, expected.version)) {
25830
+ throw new Error("Release manifest component, version, or tag does not match the requested release");
25831
+ }
25832
+ return { component: expected.component, version, tag: release.tag };
25833
+ }
25834
+ function parseArtifact(candidate, index) {
25835
+ const artifact = manifestObject(candidate, `Release manifest artifact ${index}`);
25836
+ assertExactKeys(artifact, ["name", "sha256", "size"], `Release manifest artifact ${index}`);
25837
+ if (typeof artifact.name !== "string" || !/^[A-Za-z0-9._-]+$/.test(artifact.name) || typeof artifact.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(artifact.sha256) || !Number.isSafeInteger(artifact.size) || artifact.size <= 0) {
25838
+ throw new Error(`Release manifest artifact ${index} is invalid`);
25839
+ }
25840
+ return artifact;
25841
+ }
25842
+ function parseArtifacts(candidate, component) {
25843
+ if (!Array.isArray(candidate))
25844
+ throw new Error("Release manifest artifacts must be an array");
25845
+ const artifacts = candidate.map(parseArtifact);
25846
+ const expectedNames = releaseAssetNames(component);
25847
+ const actualNames = artifacts.map((artifact) => artifact.name);
25848
+ if (actualNames.length !== expectedNames.length || actualNames.some((name, index) => name !== expectedNames[index])) {
25849
+ throw new Error(`Release manifest artifacts do not match the ${component} allowlist`);
25850
+ }
25851
+ const oversized = artifacts.find((artifact) => artifact.size > releaseAssetSizeLimit(component, artifact.name));
25852
+ if (oversized)
25853
+ throw new Error(`Release manifest artifact ${oversized.name} exceeds its size limit`);
25854
+ return artifacts;
25855
+ }
25856
+ function parseReleaseManifest(text, expected) {
25857
+ let candidate;
25858
+ try {
25859
+ candidate = JSON.parse(text);
25860
+ } catch {
25861
+ throw new Error("Release manifest is not valid JSON");
25862
+ }
25863
+ const manifest = manifestObject(candidate, "Release manifest");
25864
+ assertExactKeys(manifest, ["schemaVersion", "repository", "source", "workflow", "release", "artifacts"], "Release manifest");
25865
+ if (manifest.schemaVersion !== 1 || manifest.repository !== RELEASE_REPOSITORY || manifest.workflow !== RELEASE_SIGNER_WORKFLOW) {
25866
+ throw new Error("Release manifest identity is invalid");
25867
+ }
25868
+ return {
25869
+ schemaVersion: 1,
25870
+ repository: RELEASE_REPOSITORY,
25871
+ source: parseSource(manifest.source),
25872
+ workflow: RELEASE_SIGNER_WORKFLOW,
25873
+ release: parseRelease(manifest.release, expected),
25874
+ artifacts: parseArtifacts(manifest.artifacts, expected.component)
25875
+ };
25876
+ }
25877
+ function manifestArtifact(manifest, name) {
25878
+ const artifact = manifest.artifacts.find((candidate) => candidate.name === name);
25879
+ if (!artifact)
25880
+ throw new Error(`Release manifest does not contain ${name}`);
25881
+ return artifact;
25882
+ }
25883
+ function parseReleaseChecksums(text, manifest) {
25884
+ const lines = text.endsWith(`
25885
+ `) ? text.slice(0, -1).split(`
25886
+ `) : text.split(`
25887
+ `);
25888
+ const expectedNames = releaseAssetNames(manifest.release.component).filter((name) => name !== RELEASE_CHECKSUMS_NAME);
25889
+ if (lines.length !== expectedNames.length)
25890
+ throw new Error("SHA256SUMS has an unexpected number of entries");
25891
+ const checksums = new Map;
25892
+ lines.forEach((line, index) => {
25893
+ const match = line.match(/^([0-9a-f]{64}) ([A-Za-z0-9._-]+)$/);
25894
+ if (!match || match[2] !== expectedNames[index])
25895
+ throw new Error("SHA256SUMS is not strict or sorted");
25896
+ const [digest, name] = [match[1], match[2]];
25897
+ if (manifestArtifact(manifest, name).sha256 !== digest) {
25898
+ throw new Error(`SHA256SUMS and release manifest disagree for ${name}`);
25899
+ }
25900
+ checksums.set(name, digest);
25901
+ });
25902
+ return checksums;
25903
+ }
25904
+
25905
+ // src/shared/releases/local-upgrade-bundle.ts
25906
+ var RELEASES_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/releases`;
25907
+ var ATTESTATIONS_API = `https://api.github.com/repos/${RELEASE_REPOSITORY}/attestations`;
25908
+ var GH_VERSION = "2.96.0";
25909
+ var GH_ARCHIVE_SHA256 = {
25910
+ amd64: "83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60",
25911
+ arm64: "06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909"
25912
+ };
25913
+ var MAX_GH_ARCHIVE_BYTES = 64 * 1024 * 1024;
25914
+ var DOWNLOAD_TIMEOUT_MS = 10 * 60000;
25915
+ var DOWNLOAD_IDLE_TIMEOUT_MS = 30000;
25916
+ var GH_CAPABILITY_TIMEOUT_MS = 30000;
25917
+ var GH_VERIFICATION_TIMEOUT_MS = 2 * 60000;
25918
+ var GH_TERMINATION_GRACE_MS = 2000;
25919
+ var MAX_REDIRECTS = 6;
25920
+ var RETRYABLE_DOWNLOAD_CODES = new Set([
25921
+ "EAI_AGAIN",
25922
+ "ECONNREFUSED",
25923
+ "ECONNRESET",
25924
+ "ENETUNREACH",
25925
+ "EPIPE",
25926
+ "ETIMEDOUT"
25927
+ ]);
25928
+ var OFFICIAL_DOWNLOAD_HOSTS = new Set([
25929
+ "api.github.com",
25930
+ "github.com",
25931
+ "release-assets.githubusercontent.com"
25932
+ ]);
25933
+ var STRICT_GITHUB_CAPABILITY_FLAGS = [
25934
+ "--bundle",
25935
+ "--signer-workflow",
25936
+ "--source-ref",
25937
+ "--source-digest",
25938
+ "--deny-self-hosted-runners"
25939
+ ];
25940
+
25941
+ class RetryableDownloadError extends Error {
25942
+ }
25943
+ function githubCliArchiveIdentity(architecture) {
25944
+ const directory = `gh_${GH_VERSION}_linux_${architecture}`;
25945
+ return {
25946
+ archiveName: `${directory}.tar.gz`,
25947
+ member: `${directory}/bin/gh`,
25948
+ sha256: GH_ARCHIVE_SHA256[architecture],
25949
+ version: GH_VERSION
25950
+ };
25951
+ }
25952
+ function sha256File(filePath) {
25953
+ return createHash("sha256").update(readFileSync3(filePath)).digest("hex");
25954
+ }
25955
+ function assertPrivateLocalFileMode(mode, label) {
25956
+ if (process.platform !== "win32" && (mode & 4095) !== 384) {
25957
+ throw new Error(`${label} must use exact mode 0600 without special permission bits`);
25958
+ }
25959
+ }
25960
+ function localUpgradeFile(localPath, relativePath) {
25961
+ const stats = statSync(localPath);
25962
+ if (!stats.isFile())
25963
+ throw new Error(`Local upgrade artifact is not a regular file: ${relativePath}`);
25964
+ assertPrivateLocalFileMode(stats.mode, relativePath);
25965
+ return { localPath, relativePath, sha256: sha256File(localPath), size: stats.size };
25966
+ }
25967
+ function assertExactStableVersion(version, field) {
25968
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
25969
+ throw new Error(`${field} must be an exact stable semantic version`);
25970
+ }
25971
+ }
25972
+ function directChildPath(directory, name) {
25973
+ if (basename(name) !== name || !/^[A-Za-z0-9._-]+$/.test(name)) {
25974
+ throw new Error(`Unsafe release asset name: ${name}`);
25975
+ }
25976
+ return join(directory, name);
25977
+ }
25978
+ function assertOfficialDownloadUrl(url) {
25979
+ if (url.protocol !== "https:" || url.username || url.password || url.port || !OFFICIAL_DOWNLOAD_HOSTS.has(url.hostname)) {
25980
+ throw new Error(`Release download URL is not an approved official GitHub endpoint: ${url.hostname}`);
25981
+ }
25982
+ }
25983
+ function boundedWriter(maxBytes) {
25984
+ let receivedBytes = 0;
25985
+ return new Transform2({
25986
+ transform(chunk, _encoding, callback) {
25987
+ receivedBytes += chunk.length;
25988
+ if (receivedBytes > maxBytes)
25989
+ callback(new Error(`Download exceeded ${maxBytes} bytes`));
25990
+ else
25991
+ callback(null, chunk);
25992
+ }
25993
+ });
25994
+ }
25995
+ function responseLocation(currentUrl, location) {
25996
+ if (!location)
25997
+ throw new Error(`HTTPS redirect from ${currentUrl.hostname} did not include Location`);
25998
+ const redirected = new URL(location, currentUrl);
25999
+ assertOfficialDownloadUrl(redirected);
26000
+ return redirected;
26001
+ }
26002
+ function downloadCanRetry(error) {
26003
+ return error instanceof RetryableDownloadError || RETRYABLE_DOWNLOAD_CODES.has(error.code || "");
26004
+ }
26005
+ async function consumeDownloadResponse(response, download) {
26006
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400) {
26007
+ response.resume();
26008
+ await downloadHttpsResponse({
26009
+ ...download,
26010
+ url: responseLocation(download.url, response.headers.location),
26011
+ redirects: download.redirects + 1
26012
+ });
26013
+ return;
26014
+ }
26015
+ if (response.statusCode !== 200) {
26016
+ response.resume();
26017
+ const message = `HTTPS download returned HTTP ${response.statusCode ?? "unknown"}`;
26018
+ if (response.statusCode && response.statusCode >= 500)
26019
+ throw new RetryableDownloadError(message);
26020
+ throw new Error(message);
26021
+ }
26022
+ const contentLength = Number(response.headers["content-length"] || 0);
26023
+ if (contentLength > download.maxBytes) {
26024
+ response.resume();
26025
+ throw new Error(`Download Content-Length exceeded ${download.maxBytes} bytes`);
26026
+ }
26027
+ response.setTimeout(DOWNLOAD_IDLE_TIMEOUT_MS, () => response.destroy(new RetryableDownloadError("Release download stalled")));
26028
+ await pipeline(response, boundedWriter(download.maxBytes), createWriteStream(download.destination, { flags: "wx", mode: 384 }));
26029
+ }
26030
+ function requestDownloadResponse(download, remainingTime) {
26031
+ return new Promise((resolve2, reject) => {
26032
+ const request = get(download.url, { headers: { "User-Agent": "SupaCloud-Admin" } }, (response) => {
26033
+ consumeDownloadResponse(response, download).then(resolve2, reject);
26034
+ });
26035
+ request.setTimeout(DOWNLOAD_IDLE_TIMEOUT_MS, () => request.destroy(new RetryableDownloadError("Release download connection stalled")));
26036
+ request.on("error", reject);
26037
+ const totalTimer = setTimeout(() => request.destroy(new RetryableDownloadError("Release download timed out")), remainingTime);
26038
+ request.on("close", () => clearTimeout(totalTimer));
26039
+ });
26040
+ }
26041
+ async function downloadHttpsResponse(download) {
26042
+ if (download.redirects > MAX_REDIRECTS)
26043
+ throw new Error("Release download exceeded the redirect limit");
26044
+ const remainingTime = download.deadline - Date.now();
26045
+ if (remainingTime <= 0)
26046
+ throw new Error("Release download timed out");
26047
+ await requestDownloadResponse(download, remainingTime);
26048
+ }
26049
+ async function downloadDirect(url, destination, maxBytes) {
26050
+ const parsed = new URL(url);
26051
+ assertOfficialDownloadUrl(parsed);
26052
+ const deadline = Date.now() + DOWNLOAD_TIMEOUT_MS;
26053
+ const retryFailures = [];
26054
+ for (let attempt = 1;attempt <= 3; attempt += 1) {
26055
+ rmSync(destination, { force: true });
26056
+ try {
26057
+ await downloadHttpsResponse({ url: parsed, destination, maxBytes, redirects: 0, deadline });
26058
+ chmodSync(destination, 384);
26059
+ return;
26060
+ } catch (error) {
26061
+ if (!downloadCanRetry(error))
26062
+ throw error;
26063
+ retryFailures.push(error);
26064
+ }
26065
+ }
26066
+ throw new AggregateError(retryFailures, `Unable to download ${parsed.hostname}${parsed.pathname}`);
26067
+ }
26068
+ function parseJsonFile(filePath, label) {
26069
+ const contents = readFileSync3(filePath, "utf8");
26070
+ try {
26071
+ return JSON.parse(contents);
26072
+ } catch (error) {
26073
+ if (!(error instanceof SyntaxError))
26074
+ throw error;
26075
+ throw new Error(`${label} is not valid JSON`);
26076
+ }
26077
+ }
26078
+ function parseGithubReleaseMetadata(candidate, expectedTag) {
26079
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
26080
+ throw new Error("GitHub release metadata must be an object");
26081
+ }
26082
+ const release = candidate;
26083
+ if (release.tag_name !== expectedTag || release.draft !== false || release.prerelease !== false || !Array.isArray(release.assets)) {
26084
+ throw new Error(`GitHub release metadata does not describe stable release ${expectedTag}`);
26085
+ }
26086
+ const assets = release.assets.map((asset) => githubReleaseAsset(asset, expectedTag));
26087
+ if (new Set(assets.map((asset) => asset.name)).size !== assets.length) {
26088
+ throw new Error(`GitHub release ${expectedTag} contains duplicate asset names`);
26089
+ }
26090
+ return { tag_name: expectedTag, draft: false, prerelease: false, assets };
26091
+ }
26092
+ function githubReleaseAsset(candidate, expectedTag) {
26093
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
26094
+ throw new Error("GitHub release asset metadata must be an object");
26095
+ }
26096
+ const asset = candidate;
26097
+ if (typeof asset.name !== "string" || typeof asset.browser_download_url !== "string") {
26098
+ throw new Error("GitHub release asset metadata is incomplete");
26099
+ }
26100
+ const downloadUrl = new URL(asset.browser_download_url);
26101
+ assertOfficialDownloadUrl(downloadUrl);
26102
+ const expectedPath = `/${RELEASE_REPOSITORY}/releases/download/${expectedTag}/${asset.name}`;
26103
+ if (!/^[A-Za-z0-9._-]+$/.test(asset.name) || downloadUrl.hostname !== "github.com" || downloadUrl.pathname !== expectedPath || downloadUrl.search || downloadUrl.hash) {
26104
+ throw new Error(`Release asset ${asset.name} does not use its official GitHub release path`);
26105
+ }
26106
+ return { name: asset.name, browser_download_url: downloadUrl.toString() };
26107
+ }
26108
+ function releaseAssetUrl(release, name) {
26109
+ const matching = release.assets.find((asset) => asset.name === name);
26110
+ if (!matching)
26111
+ throw new Error(`Release ${release.tag_name} does not contain ${name}`);
26112
+ return matching.browser_download_url;
26113
+ }
26114
+ function serializeAttestationBundles(candidate) {
26115
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
26116
+ throw new Error("GitHub attestation response must be an object");
26117
+ }
26118
+ const attestations = candidate.attestations;
26119
+ if (!Array.isArray(attestations) || attestations.length === 0) {
26120
+ throw new Error("GitHub attestation response did not contain attestations");
26121
+ }
26122
+ const bundles = attestations.map((attestation) => {
26123
+ if (!attestation || typeof attestation !== "object" || Array.isArray(attestation)) {
26124
+ throw new Error("GitHub attestation entry is invalid");
26125
+ }
26126
+ const bundle = attestation.bundle;
26127
+ if (!bundle || typeof bundle !== "object" || Array.isArray(bundle)) {
26128
+ throw new Error("GitHub attestation entry did not contain a bundle");
26129
+ }
26130
+ return JSON.stringify(bundle);
26131
+ });
26132
+ return `${bundles.join(`
26133
+ `)}
26134
+ `;
26135
+ }
26136
+ function directEnvironment() {
26137
+ const environment = { ...process.env };
26138
+ for (const key of Object.keys(environment)) {
26139
+ if (/(?:^|_)proxy$/i.test(key) || /^(?:SUPACLOUD_GITHUB_PROXIES|NODE_USE_ENV_PROXY)$/.test(key)) {
26140
+ delete environment[key];
26141
+ }
26142
+ }
26143
+ return environment;
26144
+ }
26145
+ function githubCliExecutable(environment) {
26146
+ for (const directory of (environment.PATH || "").split(delimiter).filter(Boolean)) {
26147
+ const candidate = join(directory, "gh");
26148
+ try {
26149
+ accessSync(candidate, fsConstants.X_OK);
26150
+ const stats = statSync(candidate);
26151
+ if (stats.isFile()) {
26152
+ if (process.platform !== "win32" && (stats.mode & 4095) > 511) {
26153
+ throw new Error(`GitHub CLI executable has special permission bits: ${candidate}`);
26154
+ }
26155
+ return candidate;
26156
+ }
26157
+ } catch (error) {
26158
+ if (error.code !== "ENOENT" && error.code !== "EACCES" && error.code !== "ENOTDIR")
26159
+ throw error;
26160
+ }
26161
+ }
26162
+ throw new Error("GitHub CLI executable was not found in PATH");
26163
+ }
26164
+ async function runGithubCli(arguments_, timeoutMs) {
26165
+ return await new Promise((resolve2, reject) => {
26166
+ const environment = directEnvironment();
26167
+ const child = spawn(githubCliExecutable(environment), arguments_, {
26168
+ env: environment,
26169
+ stdio: ["ignore", "pipe", "pipe"]
26170
+ });
26171
+ let stdout = "";
26172
+ let stderr = "";
26173
+ let timedOut = false;
26174
+ let settled = false;
26175
+ let forceKillTimer;
26176
+ const timeout = setTimeout(() => {
26177
+ timedOut = true;
26178
+ child.kill("SIGTERM");
26179
+ forceKillTimer = setTimeout(() => child.kill("SIGKILL"), GH_TERMINATION_GRACE_MS);
26180
+ }, timeoutMs);
26181
+ const settleExecution = (error, exitCode) => {
26182
+ if (settled)
26183
+ return;
26184
+ settled = true;
26185
+ clearTimeout(timeout);
26186
+ if (forceKillTimer)
26187
+ clearTimeout(forceKillTimer);
26188
+ if (error)
26189
+ reject(error);
26190
+ else
26191
+ resolve2({ exitCode: timedOut ? 124 : exitCode, stdout, stderr });
26192
+ };
26193
+ child.stdout.on("data", (chunk) => {
26194
+ stdout = `${stdout}${chunk.toString()}`.slice(-8000);
26195
+ });
26196
+ child.stderr.on("data", (chunk) => {
26197
+ stderr = `${stderr}${chunk.toString()}`.slice(-8000);
26198
+ });
26199
+ child.once("error", (error) => settleExecution(error, 127));
26200
+ child.once("close", (code) => settleExecution(undefined, code ?? 1));
26201
+ });
26202
+ }
26203
+ function supportsStrictGithubVerification(execution) {
26204
+ const tokens = `${execution.stdout}
26205
+ ${execution.stderr}`.split(/\s+/);
26206
+ return execution.exitCode === 0 && STRICT_GITHUB_CAPABILITY_FLAGS.every((flag) => tokens.some((token) => token === flag || token.startsWith(`${flag}=`)));
26207
+ }
26208
+ async function assertLocalGithubVerifier() {
26209
+ const help = await runGithubCli(["attestation", "verify", "--help"], GH_CAPABILITY_TIMEOUT_MS);
26210
+ if (!supportsStrictGithubVerification(help)) {
26211
+ throw new Error("Local artifact transport requires a current gh attestation verifier");
26212
+ }
26213
+ }
26214
+ async function verifyManifestAttestation(manifestPath, bundlePath, manifest) {
26215
+ const verification = await runGithubCli([
26216
+ "attestation",
26217
+ "verify",
26218
+ manifestPath,
26219
+ "--bundle",
26220
+ bundlePath,
26221
+ "--repo",
26222
+ RELEASE_REPOSITORY,
26223
+ "--signer-workflow",
26224
+ RELEASE_SIGNER_WORKFLOW,
26225
+ "--source-ref",
26226
+ RELEASE_SOURCE_REF,
26227
+ "--source-digest",
26228
+ manifest.source.commit,
26229
+ "--deny-self-hosted-runners"
26230
+ ], GH_VERIFICATION_TIMEOUT_MS);
26231
+ if (verification.exitCode !== 0) {
26232
+ throw new Error(`gh attestation verify failed: ${verification.stderr.trim().slice(-1000) || verification.exitCode}`);
26233
+ }
26234
+ }
26235
+ function assertSignedArtifact(filePath, manifest) {
26236
+ const artifact = manifestArtifact(manifest, basename(filePath));
26237
+ const fileStats = statSync(filePath);
26238
+ if (!fileStats.isFile() || fileStats.size !== artifact.size) {
26239
+ throw new Error(`${artifact.name} does not match the signed release size`);
26240
+ }
26241
+ assertPrivateLocalFileMode(fileStats.mode, artifact.name);
26242
+ const digest = sha256File(filePath);
26243
+ if (digest !== artifact.sha256) {
26244
+ throw new Error(`${artifact.name} does not match the signed release hashes`);
26245
+ }
26246
+ return digest;
26247
+ }
26248
+ function verifyDownloadedFile(filePath, manifest, checksums) {
26249
+ const digest = assertSignedArtifact(filePath, manifest);
26250
+ if (checksums.get(basename(filePath)) !== digest) {
26251
+ throw new Error(`${basename(filePath)} does not match SHA256SUMS`);
26252
+ }
26253
+ }
26254
+ async function downloadReleaseMetadata(component, version, directory) {
26255
+ const expectedTag = releaseTag(component, version);
26256
+ const metadataPath = join(directory, `.release-${randomUUID2()}.json`);
26257
+ try {
26258
+ await downloadDirect(`${RELEASES_API}/tags/${expectedTag}`, metadataPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
26259
+ return parseGithubReleaseMetadata(parseJsonFile(metadataPath, "GitHub release metadata"), expectedTag);
26260
+ } finally {
26261
+ rmSync(metadataPath, { force: true });
26262
+ }
26263
+ }
26264
+ async function downloadManifestAttestation(manifestPath, destination) {
26265
+ const responsePath = `${destination}.response`;
26266
+ try {
26267
+ await downloadDirect(`${ATTESTATIONS_API}/sha256:${sha256File(manifestPath)}`, responsePath, RELEASE_BUNDLE_SIZE_LIMITS.attestation);
26268
+ const bundles = serializeAttestationBundles(parseJsonFile(responsePath, "GitHub attestation response"));
26269
+ writeFileSync(destination, bundles, { mode: 384, flag: "wx" });
26270
+ chmodSync(destination, 384);
26271
+ } finally {
26272
+ rmSync(responsePath, { force: true });
26273
+ }
26274
+ }
26275
+ async function downloadComponent(request) {
26276
+ const release = await downloadReleaseMetadata(request.component, request.version, request.destination);
26277
+ const manifestPath = directChildPath(request.destination, RELEASE_MANIFEST_NAME);
26278
+ const attestationPath = directChildPath(request.destination, RELEASE_ATTESTATION_NAME);
26279
+ await downloadDirect(releaseAssetUrl(release, RELEASE_MANIFEST_NAME), manifestPath, RELEASE_BUNDLE_SIZE_LIMITS.manifest);
26280
+ const manifest = parseReleaseManifest(readFileSync3(manifestPath, "utf8"), request);
26281
+ await downloadManifestAttestation(manifestPath, attestationPath);
26282
+ await verifyManifestAttestation(manifestPath, attestationPath, manifest);
26283
+ const checksumsPath = directChildPath(request.destination, RELEASE_CHECKSUMS_NAME);
26284
+ await downloadDirect(releaseAssetUrl(release, RELEASE_CHECKSUMS_NAME), checksumsPath, RELEASE_BUNDLE_SIZE_LIMITS.checksums);
26285
+ assertSignedArtifact(checksumsPath, manifest);
26286
+ const checksums = parseReleaseChecksums(readFileSync3(checksumsPath, "utf8"), manifest);
26287
+ for (const assetName of request.assetNames) {
26288
+ const assetPath = directChildPath(request.destination, assetName);
26289
+ await downloadDirect(releaseAssetUrl(release, assetName), assetPath, releaseAssetSizeLimit(request.component, assetName));
26290
+ verifyDownloadedFile(assetPath, manifest, checksums);
26291
+ }
26292
+ return [RELEASE_MANIFEST_NAME, RELEASE_ATTESTATION_NAME, RELEASE_CHECKSUMS_NAME, ...request.assetNames].map((name) => localUpgradeFile(directChildPath(request.destination, name), `bundle/${request.component}/${name}`));
26293
+ }
26294
+ async function downloadPinnedGithubCli(directory, architecture) {
26295
+ const identity = githubCliArchiveIdentity(architecture);
26296
+ const archivePath = directChildPath(directory, identity.archiveName);
26297
+ const url = `https://github.com/cli/cli/releases/download/v${identity.version}/${identity.archiveName}`;
26298
+ await downloadDirect(url, archivePath, MAX_GH_ARCHIVE_BYTES);
26299
+ if (sha256File(archivePath) !== identity.sha256) {
26300
+ throw new Error("Pinned GitHub CLI archive SHA256 mismatch");
26301
+ }
26302
+ return localUpgradeFile(archivePath, `verifier/${identity.archiveName}`);
26303
+ }
26304
+ function cleanupLocalUpgradeBundle(bundle) {
26305
+ rmSync(bundle.directory, { recursive: true, force: true });
26306
+ }
26307
+ function assertLocalUpgradeBundleSize(files) {
26308
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
26309
+ if (!Number.isSafeInteger(totalBytes) || totalBytes > RELEASE_BUNDLE_SIZE_LIMITS.total) {
26310
+ throw new Error("Local upgrade bundle exceeds its total size limit");
26311
+ }
26312
+ }
26313
+ async function observeDownloadFailure(download, failures) {
26314
+ try {
26315
+ return await download;
26316
+ } catch (error) {
26317
+ failures.push(error);
26318
+ throw error;
26319
+ }
26320
+ }
26321
+ function fulfilledDownload(settlement) {
26322
+ if (settlement.status === "rejected")
26323
+ throw settlement.reason;
26324
+ return settlement.value;
26325
+ }
26326
+ async function settleLocalBundleDownloads(downloads) {
26327
+ const failures = [];
26328
+ const settlements = await Promise.allSettled([
26329
+ observeDownloadFailure(downloads[0], failures),
26330
+ observeDownloadFailure(downloads[1], failures),
26331
+ observeDownloadFailure(downloads[2], failures)
26332
+ ]);
26333
+ if (failures.length > 0)
26334
+ throw failures[0];
26335
+ return [
26336
+ fulfilledDownload(settlements[0]),
26337
+ fulfilledDownload(settlements[1]),
26338
+ fulfilledDownload(settlements[2])
26339
+ ];
26340
+ }
26341
+ function createLocalBundleLayout(request) {
26342
+ const directory = mkdtempSync(join(tmpdir(), "supacloud-admin-upgrade-"));
26343
+ chmodSync(directory, 448);
26344
+ const bundleDirectory = join(directory, "bundle");
26345
+ const managementDirectory = join(bundleDirectory, "management-api");
26346
+ const edgeDirectory = join(bundleDirectory, "edge-runtime");
26347
+ const verifierDirectory = join(directory, "verifier");
26348
+ for (const createdDirectory of [bundleDirectory, managementDirectory, edgeDirectory, verifierDirectory]) {
26349
+ mkdirSync(createdDirectory, { recursive: true, mode: 448 });
26350
+ chmodSync(createdDirectory, 448);
26351
+ }
26352
+ return {
26353
+ directory,
26354
+ managementDirectory,
26355
+ edgeDirectory,
26356
+ verifierDirectory,
26357
+ managementBinaryName: `supacloud-linux-${request.architecture}`,
26358
+ edgeRuntimeBinaryName: `supacloud-edge-runtime-linux-${request.architecture}`
26359
+ };
26360
+ }
26361
+ async function downloadLocalBundle(request, layout) {
26362
+ const { managementBinaryName, edgeRuntimeBinaryName } = layout;
26363
+ const [managementFiles, edgeFiles, verifierArchive] = await settleLocalBundleDownloads([
26364
+ downloadComponent({
26365
+ component: "management-api",
26366
+ version: request.managementVersion,
26367
+ assetNames: [managementBinaryName, "web-console-build.tar.gz"],
26368
+ destination: layout.managementDirectory
26369
+ }),
26370
+ downloadComponent({
26371
+ component: "edge-runtime",
26372
+ version: request.edgeRuntimeVersion,
26373
+ assetNames: [edgeRuntimeBinaryName],
26374
+ destination: layout.edgeDirectory
26375
+ }),
26376
+ request.verifierProvisioning === "bundled" ? downloadPinnedGithubCli(layout.verifierDirectory, request.architecture) : Promise.resolve(null)
26377
+ ]);
26378
+ const files = [...managementFiles, ...edgeFiles];
26379
+ assertLocalUpgradeBundleSize([...files, ...verifierArchive ? [verifierArchive] : []]);
26380
+ return { directory: layout.directory, files, verifierArchive, managementBinaryName, edgeRuntimeBinaryName };
26381
+ }
26382
+ async function prepareLocalUpgradeBundle(request) {
26383
+ assertExactStableVersion(request.managementVersion, "version");
26384
+ assertExactStableVersion(request.edgeRuntimeVersion, "edge_runtime_version");
26385
+ await assertLocalGithubVerifier();
26386
+ const layout = createLocalBundleLayout(request);
26387
+ try {
26388
+ return await downloadLocalBundle(request, layout);
26389
+ } catch (error) {
26390
+ rmSync(layout.directory, { recursive: true, force: true });
26391
+ throw error;
26392
+ }
26393
+ }
26394
+
26395
+ // src/shared/releases/local-upgrade-transfer.ts
26396
+ var REMOTE_STAGE_ROOT = "/var/lib/supacloud/upgrade-staging";
26397
+ var REMOTE_RUN_ROOT = "/var/lib/supacloud/upgrade-runs";
26398
+ var REMOTE_LOG_ROOT = "/var/log/supacloud";
26399
+ var POLL_INTERVAL_MS = 2000;
26400
+ var STATE_READ_ATTEMPTS = 3;
26401
+ var REMOTE_STATE_READ_TIMEOUT_MS = 15000;
26402
+ var UPGRADE_OBSERVATION_TIMEOUT_MS = 30 * 60000;
26403
+
26404
+ class RemoteUpgradeReconciliationError extends AggregateError {
26405
+ }
26406
+ function quoteShell2(shellText) {
26407
+ return `'${shellText.split("'").join("'\\''")}'`;
26408
+ }
26409
+ function rootCommand(script) {
26410
+ return [
26411
+ 'if [ "$(id -u)" -eq 0 ]; then',
26412
+ ` /bin/bash -c ${quoteShell2(script)}`,
26413
+ "else",
26414
+ " sudo -n true",
26415
+ ` sudo -n /bin/bash -c ${quoteShell2(script)}`,
26416
+ "fi"
26417
+ ].join(`
26418
+ `);
26419
+ }
26420
+ function trustedInstalledGithubFunction() {
26421
+ return [
26422
+ "trusted_installed_gh() {",
26423
+ " local verifier=$1 mode",
26424
+ ` test -f "$verifier" && test ! -L "$verifier" && test -x "$verifier" || { echo 'Installed GitHub verifier is not a regular executable file' >&2; return 1; }`,
26425
+ ` test "$(stat -c '%u:%g' "$verifier")" = '0:0' || { echo 'Installed GitHub verifier is not owned by root:root' >&2; return 1; }`,
26426
+ ` mode=$(stat -c '%a' "$verifier")`,
26427
+ ` case "$mode" in [0-7]|[0-7][0-7]|[0-7][0-7][0-7]) ;; *) echo 'Installed GitHub verifier has special permission bits' >&2; return 1 ;; esac`,
26428
+ " (( (8#$mode & 0022) == 0 )) || { echo 'Installed GitHub verifier is group/other writable' >&2; return 1; }",
26429
+ "}"
26430
+ ].join(`
26431
+ `);
26432
+ }
26433
+ function remotePaths(runId) {
26434
+ return {
26435
+ drop: `/tmp/.supacloud-upgrade-upload-${runId}`,
26436
+ stage: `${REMOTE_STAGE_ROOT}/${runId}`,
26437
+ status: `${REMOTE_RUN_ROOT}/${runId}.status`,
26438
+ log: `${REMOTE_LOG_ROOT}/upgrade-${runId}.log`,
26439
+ unit: `supacloud-upgrade-${runId}.service`
26440
+ };
26441
+ }
26442
+ function parseRemotePreflight(output) {
26443
+ const architecture = output.match(/^ARCH=(amd64|arm64)$/m)?.[1];
26444
+ const verifierProvisioning = output.match(/^VERIFIER=(installed|bundled)$/m)?.[1];
26445
+ if (!architecture || !verifierProvisioning) {
26446
+ throw new Error("Remote upgrade preflight did not return its architecture and verifier capability");
26447
+ }
26448
+ return {
26449
+ architecture,
26450
+ verifierProvisioning
26451
+ };
26452
+ }
26453
+ function buildRemotePreflightScript() {
26454
+ const capabilityFlags = STRICT_GITHUB_CAPABILITY_FLAGS.join(" ");
26455
+ return [
26456
+ "set -euo pipefail",
26457
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
26458
+ "export PATH",
26459
+ trustedInstalledGithubFunction(),
26460
+ "test -d /run/systemd/system || { echo 'systemd is not the active init system' >&2; exit 1; }",
26461
+ 'for tool in systemctl systemd-run sha256sum stat realpath tar file find sort awk grep timeout flock; do command -v "$tool" >/dev/null 2>&1 || { echo "Required local-upgrade tool is missing: $tool" >&2; exit 127; }; done',
26462
+ "systemd-run --help | grep -Eq -- '(^|[[:space:]])--collect([=[:space:]]|$)' || { echo 'systemd-run --collect is required' >&2; exit 1; }",
26463
+ "test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
26464
+ "test -d /var/lib/supacloud || { echo '/var/lib/supacloud is unavailable' >&2; exit 1; }",
26465
+ "test -f /etc/supabase/management-api.env || { echo 'EDGE_RUNTIME_MODE is unavailable' >&2; exit 1; }",
26466
+ `EDGE_MODE=$(awk -F= '$1 == "EDGE_RUNTIME_MODE" { value=$2 } END { gsub(/^[[:space:]\\"'"']+|[[:space:]\\"'"']+$/, "", value); print value }' /etc/supabase/management-api.env)`,
26467
+ `test "$EDGE_MODE" = external || { echo 'Local component upgrade requires persisted external Edge Runtime mode' >&2; exit 1; }`,
26468
+ `case "$(uname -m)" in x86_64|amd64) echo ARCH=amd64 ;; aarch64|arm64) echo ARCH=arm64 ;; *) echo 'Unsupported remote architecture' >&2; exit 1 ;; esac`,
26469
+ "VERIFIER=bundled",
26470
+ "GH=$(type -P gh || true)",
26471
+ 'if [ -n "$GH" ] && trusted_installed_gh "$GH"; then',
26472
+ ' if GH_HELP=$(timeout 15s "$GH" attestation verify --help 2>&1); then',
26473
+ " GH_CAPABLE=true",
26474
+ ` for flag in ${capabilityFlags}; do printf '%s\\n' "$GH_HELP" | grep -Eq -- "(^|[[:space:]])${"$"}{flag}([=[:space:]]|$)" || GH_CAPABLE=false; done`,
26475
+ ' if [ "$GH_CAPABLE" = true ]; then VERIFIER=installed; fi',
26476
+ " fi",
26477
+ "fi",
26478
+ "echo VERIFIER=$VERIFIER"
26479
+ ].join(`
26480
+ `);
26481
+ }
26482
+ async function remoteUpgradePreflight(ssh) {
26483
+ const preflight = await ssh.exec(rootCommand(buildRemotePreflightScript()), 30000);
26484
+ if (!preflight.success)
26485
+ throw remoteFailure("Remote local-upgrade preflight failed", preflight);
26486
+ return parseRemotePreflight(preflight.stdout);
26487
+ }
26488
+ function requiredRemoteBytes(bundle) {
26489
+ const transferBytes = [...bundle.files, ...bundle.verifierArchive ? [bundle.verifierArchive] : []].reduce((total, upload) => total + upload.size, 0);
26490
+ return transferBytes * 3 + 512 * 1024 * 1024;
26491
+ }
26492
+ function prepareDropCommand(paths, bundle) {
26493
+ const directories = [
26494
+ paths.drop,
26495
+ `${paths.drop}/bundle`,
26496
+ `${paths.drop}/bundle/management-api`,
26497
+ `${paths.drop}/bundle/edge-runtime`
26498
+ ];
26499
+ if (bundle.verifierArchive)
26500
+ directories.push(`${paths.drop}/verifier`);
26501
+ const requiredBytes = requiredRemoteBytes(bundle);
26502
+ return [
26503
+ "set -euo pipefail",
26504
+ "umask 077",
26505
+ `test ! -e ${quoteShell2(paths.drop)} || { echo 'Remote upload drop already exists' >&2; exit 1; }`,
26506
+ "TMP_AVAILABLE_KB=$(df -Pk /tmp | awk 'NR == 2 { print $4 }')",
26507
+ "VAR_AVAILABLE_KB=$(df -Pk /var/lib/supacloud | awk 'NR == 2 { print $4 }')",
26508
+ `test "${"$"}TMP_AVAILABLE_KB" -ge ${Math.ceil(requiredBytes / 1024)} && test "${"$"}VAR_AVAILABLE_KB" -ge ${Math.ceil(requiredBytes / 1024)} || { echo 'Insufficient remote disk space for verified upgrade staging' >&2; exit 1; }`,
26509
+ `install -d -m 700 ${directories.map(quoteShell2).join(" ")}`
26510
+ ].join(`
26511
+ `);
26512
+ }
26513
+ async function prepareRemoteDrop(ssh, paths, bundle) {
26514
+ const prepared = await ssh.exec(prepareDropCommand(paths, bundle), 30000);
26515
+ if (!prepared.success)
26516
+ throw remoteFailure("Unable to prepare remote upload drop", prepared);
26517
+ }
26518
+ async function uploadBundleFiles(ssh, paths, bundle) {
26519
+ const uploads = [...bundle.files, ...bundle.verifierArchive ? [bundle.verifierArchive] : []];
26520
+ for (const upload of uploads) {
26521
+ await ssh.upload(upload.localPath, `${paths.drop}/${upload.relativePath}`, { mode: 384, timeoutMs: 10 * 60000 });
26522
+ }
26523
+ }
26524
+ function expectedComponentFiles(bundle) {
26525
+ return {
26526
+ management: [
26527
+ "SHA256SUMS",
26528
+ "SUPACLOUD-RELEASE.attestation.jsonl",
26529
+ "SUPACLOUD-RELEASE.json",
26530
+ bundle.managementBinaryName,
26531
+ "web-console-build.tar.gz"
26532
+ ],
26533
+ edge: [
26534
+ "SHA256SUMS",
26535
+ "SUPACLOUD-RELEASE.attestation.jsonl",
26536
+ "SUPACLOUD-RELEASE.json",
26537
+ bundle.edgeRuntimeBinaryName
26538
+ ]
26539
+ };
26540
+ }
26541
+ function shellArray(name, entries) {
26542
+ return `${name}=(${entries.map(quoteShell2).join(" ")})`;
26543
+ }
26544
+ function finishUpgradeFunction() {
26545
+ return [
26546
+ "finish_upgrade() {",
26547
+ " local code=$?",
26548
+ " trap '' HUP INT TERM",
26549
+ " trap - EXIT",
26550
+ " set +e",
26551
+ ' if [ "$code" -ne 0 ]; then',
26552
+ ' write_status "FAILED:${code}:TRANSACTION" || true',
26553
+ ' if ! rm -rf -- "$STAGE"; then write_status "FAILED:${code}:TRANSACTION_AND_CLEANUP" || true; fi',
26554
+ ' exit "$code"',
26555
+ " fi",
26556
+ " write_status CLEANING || { echo 'Unable to publish cleanup state' >&2; exit 1; }",
26557
+ ' if ! rm -rf -- "$STAGE"; then',
26558
+ " write_status 'FAILED:1:CLEANUP_AFTER_TRANSACTION' || true",
26559
+ " exit 1",
26560
+ " fi",
26561
+ " write_status SUCCEEDED || exit 1",
26562
+ " exit 0",
26563
+ "}"
26564
+ ].join(`
26565
+ `);
26566
+ }
26567
+ function upgradeScriptSetup(paths) {
26568
+ const bundleDirectory = `${paths.stage}/bundle`;
26569
+ return [
26570
+ "#!/usr/bin/env bash",
26571
+ "set -euo pipefail",
26572
+ "umask 077",
26573
+ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
26574
+ "export PATH",
26575
+ `STAGE=${quoteShell2(paths.stage)}`,
26576
+ `STATUS=${quoteShell2(paths.status)}`,
26577
+ `LOG=${quoteShell2(paths.log)}`,
26578
+ `BUNDLE=${quoteShell2(bundleDirectory)}`,
26579
+ `MANAGEMENT_DIR=${quoteShell2(`${bundleDirectory}/management-api`)}`,
26580
+ `EDGE_DIR=${quoteShell2(`${bundleDirectory}/edge-runtime`)}`,
26581
+ `write_status() { local next="\${STATUS}.next"; printf '%s\\n' "$1" > "$next"; chmod 600 "$next"; mv -f "$next" "$STATUS"; }`,
26582
+ finishUpgradeFunction(),
26583
+ "trap finish_upgrade EXIT",
26584
+ "trap 'exit 129' HUP",
26585
+ "trap 'exit 130' INT",
26586
+ "trap 'exit 143' TERM",
26587
+ 'exec >>"$LOG" 2>&1',
26588
+ "write_status RUNNING",
26589
+ buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
26590
+ `while IFS='=' read -r variable _; do case "$variable" in *PROXY*|*Proxy*|*proxy*) unset "$variable" ;; esac; done < <(env)`,
26591
+ "unset SUPACLOUD_ALLOW_UNVERIFIED_RELEASE SUPACLOUD_GITHUB_REPOSITORY SUPACLOUD_RELEASES_API SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW NODE_USE_ENV_PROXY"
26592
+ ];
26593
+ }
26594
+ function upgradeScriptFilesystemChecks(bundle) {
26595
+ const componentFiles = expectedComponentFiles(bundle);
26596
+ const stageEntries = ["bundle", "run.sh", ...bundle.verifierArchive ? ["verifier"] : []];
26597
+ const checks = [
26598
+ shellArray("MANAGEMENT_FILES", componentFiles.management),
26599
+ shellArray("EDGE_FILES", componentFiles.edge),
26600
+ `assert_directory() { local path=$1; test -d "$path" && test ! -L "$path"; test "$(stat -c '%u:%g' "$path")" = '0:0'; test "$(stat -c '%a' "$path")" = '700'; }`,
26601
+ `assert_file() { local path=$1 parent=$2; test -f "$path" && test ! -L "$path"; test "$(stat -c '%u:%g:%h' "$path")" = '0:0:1'; test "$(stat -c '%a' "$path")" = '600'; test "$(dirname "$(realpath "$path")")" = "$parent"; }`,
26602
+ `assert_entries() { local directory=$1; shift; local actual expected; actual=$(find "$directory" -mindepth 1 -maxdepth 1 -printf '%f\\n' | LC_ALL=C sort); expected=$(printf '%s\\n' "$@" | LC_ALL=C sort); test "$actual" = "$expected"; }`,
26603
+ 'assert_directory "$STAGE"',
26604
+ shellArray("STAGE_FILES", stageEntries),
26605
+ 'assert_entries "$STAGE" "${STAGE_FILES[@]}"',
26606
+ 'assert_file "$STAGE/run.sh" "$STAGE"',
26607
+ 'assert_directory "$BUNDLE"',
26608
+ 'assert_entries "$BUNDLE" edge-runtime management-api',
26609
+ 'assert_directory "$MANAGEMENT_DIR"',
26610
+ 'assert_directory "$EDGE_DIR"',
26611
+ 'assert_entries "$MANAGEMENT_DIR" "${MANAGEMENT_FILES[@]}"',
26612
+ 'assert_entries "$EDGE_DIR" "${EDGE_FILES[@]}"',
26613
+ 'for name in "${MANAGEMENT_FILES[@]}"; do assert_file "$MANAGEMENT_DIR/$name" "$MANAGEMENT_DIR"; done',
26614
+ 'for name in "${EDGE_FILES[@]}"; do assert_file "$EDGE_DIR/$name" "$EDGE_DIR"; done'
26615
+ ];
26616
+ if (bundle.verifierArchive) {
26617
+ const archiveName = basename2(bundle.verifierArchive.relativePath);
26618
+ checks.push('assert_directory "$STAGE/verifier"', `assert_entries "$STAGE/verifier" ${quoteShell2(archiveName)}`, `assert_file "$STAGE/verifier/${archiveName}" "$STAGE/verifier"`);
26619
+ }
26620
+ return checks;
26621
+ }
26622
+ function upgradeScriptTransferVerification(bundle) {
26623
+ const files = [...bundle.files, ...bundle.verifierArchive ? [bundle.verifierArchive] : []];
26624
+ return [
26625
+ `verify_transfer() { local relative=$1 expected_size=$2 expected_sha=$3; local path=$STAGE/$relative; test "$(stat -c '%s' "$path")" = "$expected_size"; test "$(sha256sum "$path" | awk '{print $1}')" = "$expected_sha"; }`,
26626
+ ...files.map((file) => `verify_transfer ${quoteShell2(file.relativePath)} ${file.size} ${quoteShell2(file.sha256)}`)
26627
+ ];
26628
+ }
26629
+ function upgradeScriptVerifier(paths, bundle, architecture) {
26630
+ const setup = bundle.verifierArchive ? bundledVerifierSetup(paths, architecture) : [
26631
+ trustedInstalledGithubFunction(),
26632
+ "GH=$(type -P gh)",
26633
+ `trusted_installed_gh "$GH" || { echo 'Installed GitHub verifier trust check failed' >&2; exit 1; }`
26634
+ ];
26635
+ const capabilityFlags = STRICT_GITHUB_CAPABILITY_FLAGS.join(" ");
26636
+ return [
26637
+ ...setup,
26638
+ `GH_HELP=$(timeout 15s "$GH" attestation verify --help 2>&1) || { echo 'GitHub verifier capability check failed' >&2; exit 1; }`,
26639
+ `for flag in ${capabilityFlags}; do printf '%s\\n' "$GH_HELP" | grep -Eq -- "(^|[[:space:]])${"$"}{flag}([=[:space:]]|$)" || { echo "GitHub verifier lacks $flag" >&2; exit 1; }; done`,
26640
+ 'VERIFIER_PATH=$(dirname "$GH")'
26641
+ ];
26642
+ }
26643
+ function bundledVerifierSetup(paths, architecture) {
26644
+ const verifier = githubCliArchiveIdentity(architecture);
26645
+ return [
26646
+ `GH_ARCHIVE=${quoteShell2(`${paths.stage}/verifier/${verifier.archiveName}`)}`,
26647
+ `GH_MEMBER=${quoteShell2(verifier.member)}`,
26648
+ 'test "$(tar -tzf "$GH_ARCHIVE" | grep -Fxc "$GH_MEMBER")" = 1',
26649
+ `test "$(tar -tvzf "$GH_ARCHIVE" "$GH_MEMBER" | cut -c1)" = '-'`,
26650
+ 'VERIFIER_ROOT=$(mktemp -d "${STAGE}/verifier/gh.XXXXXX")',
26651
+ 'tar --no-same-owner --same-permissions -xzf "$GH_ARCHIVE" -C "$VERIFIER_ROOT" "$GH_MEMBER"',
26652
+ `test "$(stat -c '%a' "$VERIFIER_ROOT/$GH_MEMBER")" = '755'`,
26653
+ 'install -m 0755 "$VERIFIER_ROOT/$GH_MEMBER" "${STAGE}/verifier/gh"',
26654
+ 'rm -rf -- "$VERIFIER_ROOT"',
26655
+ "GH=${STAGE}/verifier/gh",
26656
+ `case "$("$GH" --version | head -1)" in 'gh version ${verifier.version}'*) ;; *) echo 'Pinned GitHub verifier version mismatch' >&2; exit 1 ;; esac`
26657
+ ];
26658
+ }
26659
+ function upgradeScriptExecution(paths, bundle, request) {
26660
+ const runnerAsset = `${paths.stage}/bundle/management-api/${bundle.managementBinaryName}`;
26661
+ const runner = `${paths.stage}/runner`;
26662
+ return [
26663
+ `MANAGEMENT_VERSION=${quoteShell2(request.managementVersion)}`,
26664
+ `RUNNER_ASSET=${quoteShell2(runnerAsset)}`,
26665
+ `RUNNER=${quoteShell2(runner)}`,
26666
+ 'file -b "$RUNNER_ASSET" | grep -q ELF',
26667
+ 'install -m 0755 "$RUNNER_ASSET" "$RUNNER"',
26668
+ `test "$(sha256sum "$RUNNER"|awk '{print $1}')" = "$(sha256sum "$RUNNER_ASSET"|awk '{print $1}')"`,
26669
+ '"$RUNNER" --version | grep -Eq "(^|[^0-9])${MANAGEMENT_VERSION//./\\.}([^0-9]|$)"',
26670
+ `env PATH="$VERIFIER_PATH:$PATH" "$RUNNER" upgrade --yes --target-version ${quoteShell2(request.managementVersion)} --edge-runtime-version ${quoteShell2(request.edgeRuntimeVersion)} --asset-bundle-dir "$BUNDLE"`
26671
+ ];
26672
+ }
26673
+ function buildLocalUpgradeRunScript(paths, bundle, request, architecture) {
26674
+ return [
26675
+ ...upgradeScriptSetup(paths),
26676
+ ...upgradeScriptFilesystemChecks(bundle),
26677
+ ...upgradeScriptTransferVerification(bundle),
26678
+ ...upgradeScriptVerifier(paths, bundle, architecture),
26679
+ ...upgradeScriptExecution(paths, bundle, request)
26680
+ ].join(`
26681
+ `);
26682
+ }
26683
+ async function uploadRunScript(ssh, paths, bundle, request, architecture) {
26684
+ await ssh.uploadText(`${paths.drop}/run.sh`, buildLocalUpgradeRunScript(paths, bundle, request, architecture), 384);
26685
+ }
26686
+ function failedAdoptionRollbackFunction() {
26687
+ return [
26688
+ "rollback_failed_adoption() {",
26689
+ " local code=$? cleanup_failed=false",
26690
+ " trap '' HUP INT TERM",
26691
+ " trap - EXIT",
26692
+ ' if [ "$ADOPTION_ACTIVE" != true ]; then exit "$code"; fi',
26693
+ " set +e",
26694
+ ' rm -rf -- "$STAGE" || cleanup_failed=true',
26695
+ ' rm -f -- "$STATUS" "${STATUS}.next" "$LOG" || cleanup_failed=true',
26696
+ ' if [ "$cleanup_failed" = true ]; then echo "Upgrade adoption failed (exit $code) and rollback did not complete" >&2; exit 1; fi',
26697
+ ' echo "Upgrade adoption failed (exit $code); transferred state was rolled back" >&2',
26698
+ ' exit "$code"',
26699
+ "}"
26700
+ ].join(`
26701
+ `);
26702
+ }
26703
+ function buildAdoptDropScript(paths) {
26704
+ return [
26705
+ "set -euo pipefail",
26706
+ "umask 077",
26707
+ `DROP=${quoteShell2(paths.drop)}`,
26708
+ `STAGE=${quoteShell2(paths.stage)}`,
26709
+ `STATUS=${quoteShell2(paths.status)}`,
26710
+ `LOG=${quoteShell2(paths.log)}`,
26711
+ `UNIT=${quoteShell2(paths.unit)}`,
26712
+ "ADOPTION_ACTIVE=false",
26713
+ failedAdoptionRollbackFunction(),
26714
+ "trap rollback_failed_adoption EXIT",
26715
+ "trap 'exit 129' HUP",
26716
+ "trap 'exit 130' INT",
26717
+ "trap 'exit 143' TERM",
26718
+ `install -d -o root -g root -m 700 ${quoteShell2(REMOTE_STAGE_ROOT)} ${quoteShell2(REMOTE_RUN_ROOT)} ${quoteShell2(REMOTE_LOG_ROOT)}`,
26719
+ 'test ! -e "$STAGE" && test ! -e "$STATUS" && test ! -e "$LOG"',
26720
+ `systemctl status "$UNIT" >/dev/null 2>&1 && { echo 'Upgrade unit already exists' >&2; exit 1; } || true`,
26721
+ "ADOPTION_ACTIVE=true",
26722
+ 'mv "$DROP" "$STAGE"',
26723
+ 'chown -hR root:root "$STAGE"',
26724
+ ': > "$STATUS"; : > "$LOG"; chmod 600 "$STATUS" "$LOG"',
26725
+ 'printf \'PREPARED\\n\' > "${STATUS}.next"; chmod 600 "${STATUS}.next"; mv -f "${STATUS}.next" "$STATUS"',
26726
+ "ADOPTION_ACTIVE=false",
26727
+ "trap - EXIT HUP INT TERM"
26728
+ ].join(`
26729
+ `);
26730
+ }
26731
+ async function adoptRemoteDrop(ssh, paths) {
26732
+ let adoptionFailure;
26733
+ let adoptionOutcomeUncertain = false;
26734
+ try {
26735
+ const adopted = await ssh.exec(rootCommand(buildAdoptDropScript(paths)), 60000);
26736
+ if (adopted.success)
26737
+ return;
26738
+ adoptionFailure = remoteFailure("Unable to adopt the verified upgrade bundle", adopted);
26739
+ } catch (error) {
26740
+ adoptionOutcomeUncertain = true;
26741
+ adoptionFailure = operationError(error, "Unable to adopt the verified upgrade bundle");
26742
+ }
26743
+ const state = await observeRemoteState(ssh, paths, {
26744
+ failureMessage: "Unable to reconcile the interrupted upgrade adoption",
26745
+ precedingFailures: [adoptionFailure]
26746
+ });
26747
+ if (isFullyPreparedUnstartedState(state))
26748
+ return;
26749
+ if (adoptionOutcomeUncertain) {
26750
+ throw remoteReconciliationFailure("Upgrade adoption outcome is still uncertain after SSH interruption", [adoptionFailure], paths);
26751
+ }
26752
+ const noAdoptedStateRemains = !state.stageExists && !state.statusExists && !state.logExists && !state.unitExists;
26753
+ if (noAdoptedStateRemains)
26754
+ throw adoptionFailure;
26755
+ throw remoteReconciliationFailure("Upgrade adoption stopped in an ambiguous state", [adoptionFailure], paths);
26756
+ }
26757
+ function startUnitScript(paths) {
26758
+ return [
26759
+ "set -euo pipefail",
26760
+ `systemd-run --quiet --collect --unit=${quoteShell2(paths.unit)} --property=Type=exec --property=KillMode=control-group --property=TimeoutStopSec=30s /bin/bash ${quoteShell2(`${paths.stage}/run.sh`)}`,
26761
+ `systemctl is-active --quiet ${quoteShell2(paths.unit)}`
26762
+ ].join(`
26763
+ `);
26764
+ }
26765
+ async function startRemoteUpgrade(ssh, paths) {
26766
+ let startFailure;
26767
+ let startOutcomeUncertain = false;
26768
+ try {
26769
+ const started = await ssh.exec(rootCommand(startUnitScript(paths)), 30000);
26770
+ if (started.success)
26771
+ return;
26772
+ startFailure = remoteFailure("Unable to start the transient upgrade unit", started);
26773
+ } catch (error) {
26774
+ startOutcomeUncertain = true;
26775
+ startFailure = operationError(error, "Unable to start the transient upgrade unit");
26776
+ }
26777
+ const state = await observeRemoteState(ssh, paths, {
26778
+ failureMessage: "Unable to reconcile the interrupted upgrade start",
26779
+ precedingFailures: [startFailure]
26780
+ });
26781
+ if (unitIsRunning(state.serviceState) || /^(?:RUNNING|CLEANING|SUCCEEDED|FAILED:)/.test(state.status))
26782
+ return;
26783
+ if (startOutcomeUncertain) {
26784
+ throw remoteReconciliationFailure("Upgrade start outcome is still uncertain after SSH interruption", [startFailure], paths);
26785
+ }
26786
+ if (!isFullyPreparedUnstartedState(state)) {
26787
+ throw remoteReconciliationFailure("Upgrade start stopped in an ambiguous state", [startFailure], paths);
26788
+ }
26789
+ try {
26790
+ await cleanupUnstartedUpgrade(ssh, paths);
26791
+ } catch (cleanupError) {
26792
+ throw remoteReconciliationFailure("Remote upgrade start and cleanup both failed", [startFailure, cleanupError], paths);
26793
+ }
26794
+ throw startFailure;
26795
+ }
26796
+ function readStateScript(paths) {
26797
+ return [
26798
+ "set -euo pipefail",
26799
+ `DROP=${quoteShell2(paths.drop)}`,
26800
+ `LOG=${quoteShell2(paths.log)}`,
26801
+ `STAGE=${quoteShell2(paths.stage)}`,
26802
+ `STATUS=${quoteShell2(paths.status)}`,
26803
+ `UNIT=${quoteShell2(paths.unit)}`,
26804
+ `printf 'STATUS=%s\\n' "$(sed -n '1p' "$STATUS" 2>/dev/null || true)"`,
26805
+ `printf 'UNIT=%s\\n' "$(systemctl is-active "$UNIT" 2>/dev/null || true)"`,
26806
+ 'if [ -e "$STAGE" ]; then echo STAGE_EXISTS=yes; else echo STAGE_EXISTS=no; fi',
26807
+ 'if [ -d "$STAGE" ] && [ ! -L "$STAGE" ]; then echo STAGE_DIRECTORY=yes; else echo STAGE_DIRECTORY=no; fi',
26808
+ 'if [ -e "$DROP" ]; then echo DROP_EXISTS=yes; else echo DROP_EXISTS=no; fi',
26809
+ 'if [ -e "$STATUS" ]; then echo STATUS_EXISTS=yes; else echo STATUS_EXISTS=no; fi',
26810
+ 'if [ -e "$LOG" ]; then echo LOG_EXISTS=yes; else echo LOG_EXISTS=no; fi',
26811
+ 'UNIT_LOAD_STATE=$(systemctl show --property=LoadState --value "$UNIT" 2>/dev/null || true)',
26812
+ `test -n "$UNIT_LOAD_STATE" || { echo 'Unable to read remote upgrade unit load state' >&2; exit 1; }`,
26813
+ `printf 'UNIT_LOAD=%s\\n' "$UNIT_LOAD_STATE"`,
26814
+ 'if [ -n "$UNIT_LOAD_STATE" ] && [ "$UNIT_LOAD_STATE" != not-found ]; then echo UNIT_EXISTS=yes; else echo UNIT_EXISTS=no; fi'
26815
+ ].join(`
26816
+ `);
26817
+ }
26818
+ function remoteStateValue(output, field) {
26819
+ const match = output.match(new RegExp(`^${field}=(.*)$`, "m"));
26820
+ if (!match)
26821
+ throw new Error(`Remote upgrade state is missing ${field}`);
26822
+ return match[1]?.trim() || "";
26823
+ }
26824
+ function remoteStatePresence(output, field) {
26825
+ const presence = remoteStateValue(output, field);
26826
+ if (presence !== "yes" && presence !== "no") {
26827
+ throw new Error(`Remote upgrade state has invalid ${field}`);
26828
+ }
26829
+ return presence === "yes";
26830
+ }
26831
+ async function readRemoteState(ssh, paths, timeoutMs = REMOTE_STATE_READ_TIMEOUT_MS) {
26832
+ const state = await ssh.exec(rootCommand(readStateScript(paths)), timeoutMs);
26833
+ if (!state.success)
26834
+ throw remoteFailure("Unable to read remote upgrade state", state);
26835
+ return {
26836
+ dropExists: remoteStatePresence(state.stdout, "DROP_EXISTS"),
26837
+ logExists: remoteStatePresence(state.stdout, "LOG_EXISTS"),
26838
+ serviceState: remoteStateValue(state.stdout, "UNIT") || "unknown",
26839
+ stageExists: remoteStatePresence(state.stdout, "STAGE_EXISTS"),
26840
+ stageIsDirectory: remoteStatePresence(state.stdout, "STAGE_DIRECTORY"),
26841
+ status: remoteStateValue(state.stdout, "STATUS"),
26842
+ statusExists: remoteStatePresence(state.stdout, "STATUS_EXISTS"),
26843
+ unitExists: remoteStatePresence(state.stdout, "UNIT_EXISTS"),
26844
+ unitLoadState: remoteStateValue(state.stdout, "UNIT_LOAD")
26845
+ };
26846
+ }
26847
+ function observationDeadlineFailure(paths, failures) {
26848
+ return remoteReconciliationFailure("Remote upgrade is still nonterminal after the observation deadline", failures, paths);
26849
+ }
26850
+ async function observeRemoteState(ssh, paths, observation) {
26851
+ const readFailures = [];
26852
+ for (let attempt = 1;attempt <= STATE_READ_ATTEMPTS; attempt += 1) {
26853
+ const remainingMs = observation.deadline === undefined ? REMOTE_STATE_READ_TIMEOUT_MS : observation.deadline - Date.now();
26854
+ if (remainingMs <= 0) {
26855
+ throw observationDeadlineFailure(paths, readFailures);
26856
+ }
26857
+ try {
26858
+ return await readRemoteState(ssh, paths, Math.min(REMOTE_STATE_READ_TIMEOUT_MS, remainingMs));
26859
+ } catch (error) {
26860
+ readFailures.push(error);
26861
+ if (attempt < STATE_READ_ATTEMPTS) {
26862
+ const retryDelay = observation.deadline === undefined ? POLL_INTERVAL_MS : Math.min(POLL_INTERVAL_MS, observation.deadline - Date.now());
26863
+ if (retryDelay <= 0)
26864
+ throw observationDeadlineFailure(paths, readFailures);
26865
+ await delay(retryDelay);
26866
+ }
26867
+ }
26868
+ }
26869
+ throw remoteReconciliationFailure(observation.failureMessage, [...observation.precedingFailures ?? [], ...readFailures], paths);
26870
+ }
26871
+ async function remoteLogTail(ssh, paths) {
26872
+ const script = [
26873
+ `LOG=${quoteShell2(paths.log)}`,
26874
+ `test -f "$LOG" && test ! -L "$LOG" || { echo 'Remote upgrade log is not a regular file' >&2; exit 1; }`,
26875
+ 'tail -80 -- "$LOG"'
26876
+ ].join(`
26877
+ `);
26878
+ const output = await ssh.exec(rootCommand(script), 15000);
26879
+ if (!output.success)
26880
+ throw remoteFailure("Unable to read the remote upgrade log", output);
26881
+ return output.stdout.slice(-4000);
26882
+ }
26883
+ async function cleanupRemoteRecords(ssh, paths) {
26884
+ const cleanup = await ssh.exec(rootCommand(`rm -f -- ${quoteShell2(paths.status)} ${quoteShell2(paths.log)}`), 15000);
26885
+ if (!cleanup.success)
26886
+ throw remoteFailure("Unable to remove remote upgrade status records", cleanup);
26887
+ }
26888
+ function buildCleanupUnstartedUpgradeScript(paths) {
26889
+ return [
26890
+ "set -euo pipefail",
26891
+ "cleanup_failed=false",
26892
+ `rm -rf -- ${quoteShell2(paths.stage)} || cleanup_failed=true`,
26893
+ `rm -f -- ${quoteShell2(paths.status)} ${quoteShell2(paths.log)} || cleanup_failed=true`,
26894
+ 'test "$cleanup_failed" = false'
26895
+ ].join(`
26896
+ `);
26897
+ }
26898
+ async function cleanupUnstartedUpgrade(ssh, paths) {
26899
+ const cleanup = await ssh.exec(rootCommand(buildCleanupUnstartedUpgradeScript(paths)), 15000);
26900
+ if (!cleanup.success)
26901
+ throw remoteFailure("Unable to clean an unstarted local upgrade", cleanup);
26902
+ }
26903
+ async function cleanupRemoteDrop(ssh, paths) {
26904
+ const cleanup = await ssh.exec(`rm -rf -- ${quoteShell2(paths.drop)}`, 15000);
26905
+ if (!cleanup.success)
26906
+ throw remoteFailure("Unable to remove remote upload drop", cleanup);
26907
+ }
26908
+ function remoteFailure(message, execution) {
26909
+ const diagnostic = execution.stderr.trim() || execution.stdout.trim() || `exit ${execution.code}`;
26910
+ return new Error(`${message}: ${diagnostic.slice(-500)}`);
26911
+ }
26912
+ function operationError(error, message) {
26913
+ return error instanceof Error ? error : new Error(`${message}: ${String(error)}`);
26914
+ }
26915
+ function remoteEvidence(paths) {
26916
+ return `unit=${paths.unit} stage=${paths.stage} status=${paths.status} log=${paths.log} drop=${paths.drop}`;
26917
+ }
26918
+ function remoteReconciliationFailure(message, failures, paths) {
26919
+ return new RemoteUpgradeReconciliationError(failures.map((failure) => operationError(failure, message)), `${message}; reconcile remote evidence at ${remoteEvidence(paths)}; do not retry blindly`);
26920
+ }
26921
+ function failureRequiresRemoteReconciliation(error) {
26922
+ return error instanceof RemoteUpgradeReconciliationError;
26923
+ }
26924
+ function delay(milliseconds) {
26925
+ return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
26926
+ }
26927
+ function unitIsRunning(serviceState) {
26928
+ return ["active", "activating", "deactivating", "reloading"].includes(serviceState);
26929
+ }
26930
+ function isFullyPreparedUnstartedState(state) {
26931
+ return state.stageExists && state.stageIsDirectory && state.statusExists && state.logExists && !state.dropExists && !state.unitExists && state.unitLoadState === "not-found" && state.status === "PREPARED";
26932
+ }
26933
+ function unitStoppedNormally(state) {
26934
+ return state.serviceState === "inactive" && ["loaded", "not-found"].includes(state.unitLoadState) || state.serviceState === "unknown" && state.unitLoadState === "not-found";
26935
+ }
26936
+ function assertSuccessfulUnitStoppedNormally(state, paths) {
26937
+ if (unitStoppedNormally(state))
26938
+ return;
26939
+ throw remoteReconciliationFailure(`Remote upgrade published SUCCEEDED but the unit stopped abnormally or ambiguously ` + `(state=${state.serviceState} load=${state.unitLoadState})`, [], paths);
26940
+ }
26941
+ function assertTerminalEvidence(state, paths) {
26942
+ if (state.status !== "SUCCEEDED" && !state.status.startsWith("FAILED:"))
26943
+ return;
26944
+ const evidenceIssues = [];
26945
+ if (!state.statusExists)
26946
+ evidenceIssues.push("status record is missing");
26947
+ if (!state.logExists)
26948
+ evidenceIssues.push("log is missing");
26949
+ if (state.dropExists)
26950
+ evidenceIssues.push("upload drop still exists");
26951
+ if (state.status === "SUCCEEDED" && state.stageExists)
26952
+ evidenceIssues.push("successful stage still exists");
26953
+ if (state.status.startsWith("FAILED:") && !state.status.includes("CLEANUP") && state.stageExists) {
26954
+ evidenceIssues.push("failed stage still exists without a cleanup failure status");
26955
+ }
26956
+ if (evidenceIssues.length === 0)
26957
+ return;
26958
+ throw remoteReconciliationFailure(`Remote upgrade terminal evidence is incomplete or inconsistent (${evidenceIssues.join(", ")})`, [], paths);
26959
+ }
26960
+ async function completedUpgradeOutput(ssh, paths) {
26961
+ let log;
26962
+ try {
26963
+ log = await remoteLogTail(ssh, paths);
26964
+ } catch (error) {
26965
+ throw remoteReconciliationFailure("Upgrade succeeded but its retained log could not be read", [error], paths);
26966
+ }
26967
+ try {
26968
+ await cleanupRemoteRecords(ssh, paths);
26969
+ } catch (error) {
26970
+ throw remoteReconciliationFailure("Upgrade succeeded but remote evidence cleanup could not be confirmed", [error], paths);
26971
+ }
26972
+ return `✅ Upgrade done
26973
+ ${log.slice(-1500)}`;
26974
+ }
26975
+ async function throwRemoteUpgradeFailure(ssh, paths, status) {
26976
+ let log;
26977
+ try {
26978
+ log = await remoteLogTail(ssh, paths);
26979
+ } catch (error) {
26980
+ throw remoteReconciliationFailure("Remote upgrade failed but its retained log could not be read", [error], paths);
26981
+ }
26982
+ const failure = new Error(`Remote local upgrade failed (${status}): ${log.slice(-1500)}`);
26983
+ if (status.endsWith(":CLEANUP_AFTER_TRANSACTION")) {
26984
+ throw remoteReconciliationFailure("Upgrade transaction completed but staging cleanup is incomplete", [failure], paths);
26985
+ }
26986
+ if (status.includes("CLEANUP")) {
26987
+ throw remoteReconciliationFailure("Upgrade transaction and staging cleanup both failed", [failure], paths);
26988
+ }
26989
+ try {
26990
+ await cleanupRemoteRecords(ssh, paths);
26991
+ } catch (cleanupError) {
26992
+ throw remoteReconciliationFailure("Remote local upgrade failed and status cleanup did not complete", [failure, cleanupError], paths);
26993
+ }
26994
+ throw failure;
26995
+ }
26996
+ function assertObservableUpgradeState(state, unitRunning, paths) {
26997
+ const knownStatus = ["PREPARED", "RUNNING", "CLEANING", "SUCCEEDED"].includes(state.status) || state.status.startsWith("FAILED:");
26998
+ if (!unitRunning && !knownStatus) {
26999
+ throw remoteReconciliationFailure("Remote upgrade stopped without a terminal status", [], paths);
27000
+ }
27001
+ }
27002
+ async function awaitRemoteUpgrade(ssh, paths) {
27003
+ const observationDeadline = Date.now() + UPGRADE_OBSERVATION_TIMEOUT_MS;
27004
+ let stoppedObservations = 0;
27005
+ while (true) {
27006
+ const state = await observeRemoteState(ssh, paths, {
27007
+ deadline: observationDeadline,
27008
+ failureMessage: "Unable to observe the remote upgrade lifecycle"
27009
+ });
27010
+ const unitRunning = unitIsRunning(state.serviceState);
27011
+ if (!unitRunning)
27012
+ assertTerminalEvidence(state, paths);
27013
+ if (state.status === "SUCCEEDED" && !unitRunning) {
27014
+ assertSuccessfulUnitStoppedNormally(state, paths);
27015
+ return await completedUpgradeOutput(ssh, paths);
27016
+ }
27017
+ if (state.status.startsWith("FAILED:") && !unitRunning) {
27018
+ await throwRemoteUpgradeFailure(ssh, paths, state.status);
27019
+ }
27020
+ if (Date.now() >= observationDeadline) {
27021
+ throw observationDeadlineFailure(paths, []);
27022
+ }
27023
+ assertObservableUpgradeState(state, unitRunning, paths);
27024
+ stoppedObservations = unitRunning ? 0 : stoppedObservations + 1;
27025
+ if (stoppedObservations >= 3 && ["PREPARED", "RUNNING", "CLEANING"].includes(state.status)) {
27026
+ throw remoteReconciliationFailure("Remote upgrade unit stopped before publishing a terminal status", [], paths);
27027
+ }
27028
+ const remainingObservationMs = observationDeadline - Date.now();
27029
+ if (remainingObservationMs <= 0)
27030
+ throw observationDeadlineFailure(paths, []);
27031
+ await delay(Math.min(POLL_INTERVAL_MS, remainingObservationMs));
27032
+ }
27033
+ }
27034
+ async function executeLocalUpgradeTransfer(ssh, request) {
27035
+ const runId = randomUUID3();
27036
+ const paths = remotePaths(runId);
27037
+ const preflight = await remoteUpgradePreflight(ssh);
27038
+ const bundle = await prepareLocalUpgradeBundle({ ...preflight, ...request });
27039
+ let adopted = false;
27040
+ let transferError;
27041
+ try {
27042
+ try {
27043
+ await prepareRemoteDrop(ssh, paths, bundle);
27044
+ await uploadBundleFiles(ssh, paths, bundle);
27045
+ await uploadRunScript(ssh, paths, bundle, request, preflight.architecture);
27046
+ await adoptRemoteDrop(ssh, paths);
27047
+ adopted = true;
27048
+ await startRemoteUpgrade(ssh, paths);
27049
+ return await awaitRemoteUpgrade(ssh, paths);
27050
+ } catch (error) {
27051
+ transferError = error;
27052
+ if (!adopted && !failureRequiresRemoteReconciliation(error)) {
27053
+ try {
27054
+ await cleanupRemoteDrop(ssh, paths);
27055
+ } catch (cleanupError) {
27056
+ transferError = new AggregateError([transferError, cleanupError], "Local upgrade failed and upload-drop cleanup did not complete");
27057
+ }
27058
+ }
27059
+ throw transferError;
27060
+ }
27061
+ } finally {
27062
+ try {
27063
+ cleanupLocalUpgradeBundle(bundle);
27064
+ } catch (cleanupError) {
27065
+ if (transferError) {
27066
+ throw new AggregateError([transferError, cleanupError], "Local upgrade and local bundle cleanup both failed");
27067
+ }
27068
+ throw new AggregateError([cleanupError], "Remote local upgrade completed but local bundle cleanup did not complete");
27069
+ }
27070
+ }
27071
+ }
25617
27072
 
25618
27073
  // ../../scripts/lib/release_assets.sh
25619
27074
  var release_assets_default = `#!/usr/bin/env bash
@@ -25626,6 +27081,24 @@ SUPACLOUD_GH_MIN_VERSION="\${SUPACLOUD_GH_MIN_VERSION:-2.68.0}"
25626
27081
  SUPACLOUD_GH_AMD64_SHA256="\${SUPACLOUD_GH_AMD64_SHA256:-83d5c2ccad5498f58bf6368acb1ab32588cf43ab3a4b1c301bf36328b1c8bd60}"
25627
27082
  SUPACLOUD_GH_ARM64_SHA256="\${SUPACLOUD_GH_ARM64_SHA256:-06f86ec7103d41993b76cd78072f43595c34aaa56506d971d9860e67140bf909}"
25628
27083
 
27084
+ supacloud_curl_release_json() {
27085
+ local url="$1"
27086
+ local output="$2"
27087
+ curl -fsSL --proto '=https' --proto-redir '=https' \\
27088
+ --retry 1 --retry-delay 2 --retry-max-time 60 \\
27089
+ --connect-timeout 15 --max-time 30 --speed-limit 128 --speed-time 10 \\
27090
+ -o "$output" "$url"
27091
+ }
27092
+
27093
+ supacloud_curl_release_asset() {
27094
+ local url="$1"
27095
+ local output="$2"
27096
+ curl -fL --proto '=https' --proto-redir '=https' \\
27097
+ --retry 1 --retry-delay 2 --retry-max-time 180 \\
27098
+ --connect-timeout 15 --max-time 90 --speed-limit 128 --speed-time 60 \\
27099
+ -o "$output" "$url"
27100
+ }
27101
+
25629
27102
  supacloud_component_tag() {
25630
27103
  local component="$1"
25631
27104
  local version="$2"
@@ -25691,18 +27164,15 @@ supacloud_fetch_component_release() {
25691
27164
  supacloud_select_release "$component" "\${required_assets[@]}" <<< "$response"
25692
27165
  }
25693
27166
 
25694
- supacloud_fetch_release_json() {
27167
+ supacloud_fetch_release_json() (
25695
27168
  local url="$1"
25696
- local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
25697
- if curl -fsSL --retry 3 --connect-timeout 15 "$url"; then
25698
- return 0
25699
- fi
25700
- if [[ -n "$proxy" ]]; then
25701
- curl -fsSL --retry 3 --connect-timeout 15 "\${proxy%/}/\${url}"
25702
- return
25703
- fi
25704
- return 1
25705
- }
27169
+ local response_file
27170
+ response_file=$(mktemp) || return 1
27171
+ trap 'rm -f "$response_file"' EXIT
27172
+ trap 'trap - EXIT HUP INT TERM; rm -f "$response_file"; exit 1' HUP INT TERM
27173
+ supacloud_download_release_metadata_url "$url" "$response_file" || return 1
27174
+ cat "$response_file"
27175
+ )
25706
27176
 
25707
27177
  supacloud_release_asset_url() {
25708
27178
  local release_json="$1"
@@ -25718,11 +27188,26 @@ supacloud_download_url() {
25718
27188
  local output="$2"
25719
27189
  local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
25720
27190
 
25721
- if curl -fL --retry 3 --connect-timeout 15 -o "$output" "$url"; then
27191
+ if supacloud_curl_release_asset "$url" "$output"; then
25722
27192
  return 0
25723
27193
  fi
25724
27194
  if [[ -n "$proxy" ]]; then
25725
- curl -fL --retry 3 --connect-timeout 15 -o "$output" "\${proxy%/}/\${url}"
27195
+ supacloud_curl_release_asset "\${proxy%/}/\${url}" "$output"
27196
+ return
27197
+ fi
27198
+ return 1
27199
+ }
27200
+
27201
+ supacloud_download_release_metadata_url() {
27202
+ local url="$1"
27203
+ local output="$2"
27204
+ local proxy="\${SUPACLOUD_GITHUB_PROXY:-\${GH_PROXY:-}}"
27205
+
27206
+ if supacloud_curl_release_json "$url" "$output"; then
27207
+ return 0
27208
+ fi
27209
+ if [[ -n "$proxy" ]]; then
27210
+ supacloud_curl_release_json "\${proxy%/}/\${url}" "$output"
25726
27211
  return
25727
27212
  fi
25728
27213
  return 1
@@ -25803,7 +27288,8 @@ supacloud_install_pinned_tar_xz_binary() (
25803
27288
  fi
25804
27289
 
25805
27290
  extract_dir=$(mktemp -d)
25806
- trap 'rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"' EXIT HUP INT TERM
27291
+ trap 'rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"' EXIT
27292
+ trap 'trap - EXIT HUP INT TERM; rm -rf "$extract_dir"; [[ -z "\${staged_target:-}" ]] || rm -f "$staged_target"; exit 1' HUP INT TERM
25807
27293
  if ! tar --no-same-owner --no-same-permissions -xJf "$archive" -C "$extract_dir" "$member"; then
25808
27294
  return 1
25809
27295
  fi
@@ -25993,7 +27479,8 @@ supacloud_verify_attestation() (
25993
27479
  --bundle "$bundle_file" \\
25994
27480
  --repo "$SUPACLOUD_GITHUB_REPOSITORY" \\
25995
27481
  --signer-workflow "$SUPACLOUD_ATTESTATION_SIGNER_WORKFLOW" \\
25996
- --source-ref "refs/heads/main" 2>&1); then
27482
+ --source-ref "refs/heads/main" \\
27483
+ --deny-self-hosted-runners 2>&1); then
25997
27484
  echo "GitHub artifact attestation verification failed: \${verification_output}" >&2
25998
27485
  return 1
25999
27486
  fi
@@ -26020,7 +27507,8 @@ supacloud_attestation_verifier_available() {
26020
27507
  help=$(gh attestation verify --help 2>&1) || return 1
26021
27508
  grep -Eq -- '(^|[[:space:]])--bundle([=[:space:]]|$)' <<< "$help" || return 1
26022
27509
  grep -Eq -- '(^|[[:space:]])--signer-workflow([=[:space:]]|$)' <<< "$help" || return 1
26023
- grep -Eq -- '(^|[[:space:]])--source-ref([=[:space:]]|$)' <<< "$help"
27510
+ grep -Eq -- '(^|[[:space:]])--source-ref([=[:space:]]|$)' <<< "$help" || return 1
27511
+ grep -Eq -- '(^|[[:space:]])--deny-self-hosted-runners([=[:space:]]|$)' <<< "$help"
26024
27512
  }
26025
27513
 
26026
27514
  supacloud_download_release_asset() (
@@ -26035,10 +27523,11 @@ supacloud_download_release_asset() (
26035
27523
  mkdir -p "$(dirname "$destination")"
26036
27524
  temporary_artifact=$(mktemp "\${destination}.tmp.XXXXXX")
26037
27525
  temporary_checksums=$(mktemp "\${destination}.SHA256SUMS.tmp.XXXXXX")
26038
- trap 'rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"' EXIT HUP INT TERM
27526
+ trap 'rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"' EXIT
27527
+ trap 'trap - EXIT HUP INT TERM; rm -f "\${temporary_artifact:-}" "\${temporary_checksums:-}"; exit 1' HUP INT TERM
26039
27528
 
26040
27529
  if ! supacloud_download_url "$asset_url" "$temporary_artifact" \\
26041
- || ! supacloud_download_url "$checksum_url" "$temporary_checksums" \\
27530
+ || ! supacloud_download_release_metadata_url "$checksum_url" "$temporary_checksums" \\
26042
27531
  || ! supacloud_verify_checksum "$temporary_artifact" "$asset_name" "$temporary_checksums"; then
26043
27532
  rm -f "$temporary_artifact" "$temporary_checksums"
26044
27533
  return 1
@@ -26080,6 +27569,8 @@ var SAFE_HOSTNAME = /^(?=.{1,253}$)(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9
26080
27569
  var SAFE_SYSTEMD_UNIT = /^[a-zA-Z0-9][a-zA-Z0-9_.@:-]{0,127}$/;
26081
27570
  var SAFE_DB_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_-]{0,62}$/;
26082
27571
  var MINIMUM_COMPONENT_UPGRADE_VERSION = "0.50.27";
27572
+ var DIRECT_UPGRADE_SSH_TIMEOUT_MS = 720000;
27573
+ var PROXIED_UPGRADE_SSH_TIMEOUT_MS = 1320000;
26083
27574
  function hostnameSchema(fieldName) {
26084
27575
  return decodedSchema(Type.String(), Type.String({ minLength: 1, maxLength: 253 }), (value) => {
26085
27576
  const normalized = value.trim();
@@ -26136,7 +27627,7 @@ function assertSafeReleaseTag(value) {
26136
27627
  }
26137
27628
  return value;
26138
27629
  }
26139
- function assertExactStableVersion(value, fieldName) {
27630
+ function assertExactStableVersion2(value, fieldName) {
26140
27631
  if (!/^v?\d+\.\d+\.\d+$/.test(value)) {
26141
27632
  throw new Error(`${fieldName} must be an exact stable semantic version`);
26142
27633
  }
@@ -26177,8 +27668,8 @@ function componentPreflightCommands(request) {
26177
27668
  }
26178
27669
  function signalSafeCleanupTraps(cleanupCommand) {
26179
27670
  return [
26180
- `trap '${cleanupCommand}' EXIT`,
26181
- `trap 'trap - EXIT HUP INT TERM; ${cleanupCommand}; exit 1' HUP INT TERM`
27671
+ `trap ${quoteEnvValue(cleanupCommand)} EXIT`,
27672
+ `trap ${quoteEnvValue(`trap - EXIT HUP INT TERM; ${cleanupCommand}; exit 1`)} HUP INT TERM`
26182
27673
  ];
26183
27674
  }
26184
27675
  function buildRootUpgradeScript(request) {
@@ -26190,8 +27681,10 @@ function buildRootUpgradeScript(request) {
26190
27681
  "export PATH",
26191
27682
  "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",
26192
27683
  request.githubProxy ? `export SUPACLOUD_GITHUB_PROXY=${quoteEnvValue(request.githubProxy)}` : "unset SUPACLOUD_GITHUB_PROXY SUPACLOUD_GITHUB_PROXIES",
26193
- 'for tool in curl jq file sha256sum tar; do command -v "$tool" >/dev/null 2>&1 || { echo "Required upgrade tool is missing: $tool" >&2; exit 127; }; done',
27684
+ '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',
27685
+ "test -d /run/lock || { echo '/run/lock is unavailable' >&2; exit 1; }",
26194
27686
  "test -x /usr/local/bin/supacloud || { echo 'SupaCloud binary not found at /usr/local/bin/supacloud; run ssh install first.' >&2; exit 127; }",
27687
+ buildUpgradeLockScript(SUPACLOUD_UPGRADE_LOCK_PATH),
26195
27688
  ...componentPreflightCommands(request),
26196
27689
  "STAGED_MANAGEMENT=''",
26197
27690
  ...signalSafeCleanupTraps('test -z "$STAGED_MANAGEMENT" || rm -f "$STAGED_MANAGEMENT"'),
@@ -26205,14 +27698,38 @@ function buildRootUpgradeScript(request) {
26205
27698
  }
26206
27699
  function buildOfficialUpgradeCommand(request) {
26207
27700
  const rootScript = buildRootUpgradeScript(request);
27701
+ const cleanupCommand = `rm -rf -- ${quoteEnvValue(dirname(request.helperPath))}`;
26208
27702
  return [
26209
27703
  "set -e",
26210
- ...signalSafeCleanupTraps(`rm -f ${request.helperPath}`),
27704
+ ...signalSafeCleanupTraps(cleanupCommand),
26211
27705
  'if [ "$(id -u)" -eq 0 ]; then ' + `bash -c ${quoteEnvValue(rootScript)}; ` + "else sudo -n true; " + `sudo -n bash -c ${quoteEnvValue(rootScript)}; fi`
26212
27706
  ].join("; ");
26213
27707
  }
27708
+ async function prepareRemoteUpgradeHelperDirectory(ssh, helperPath) {
27709
+ const helperDirectory = dirname(helperPath);
27710
+ const command = [
27711
+ "set -e",
27712
+ "umask 077",
27713
+ `test ! -e ${quoteEnvValue(helperDirectory)}`,
27714
+ `install -d -m 700 -- ${quoteEnvValue(helperDirectory)}`
27715
+ ].join("; ");
27716
+ const preparation = await ssh.exec(command, 30000);
27717
+ if (!preparation.success) {
27718
+ throw new Error(`Failed to prepare remote upgrade helper directory (exit ${preparation.code}): ${preparation.stderr.slice(-300)}`);
27719
+ }
27720
+ }
26214
27721
  async function removeRemoteUpgradeHelper(ssh, helperPath) {
26215
- const cleanup = await ssh.exec(`rm -f ${quoteEnvValue(helperPath)}`);
27722
+ const helperDirectory = dirname(helperPath);
27723
+ const removeCommand = `rm -rf -- ${quoteEnvValue(helperDirectory)}`;
27724
+ const command = [
27725
+ 'if [ "$(id -u)" -eq 0 ]; then',
27726
+ ` ${removeCommand}`,
27727
+ "else",
27728
+ ` ${removeCommand} || sudo -n ${removeCommand}`,
27729
+ "fi"
27730
+ ].join(`
27731
+ `);
27732
+ const cleanup = await ssh.exec(command, 30000);
26216
27733
  if (!cleanup.success) {
26217
27734
  throw new Error(`Failed to remove remote upgrade helper (exit ${cleanup.code}): ${cleanup.stderr.slice(-300)}`);
26218
27735
  }
@@ -26238,20 +27755,25 @@ function officialUpgradeOutcome(execution, executionError, cleanupError) {
26238
27755
  throw remoteUpgradeFailure(execution);
26239
27756
  return execution;
26240
27757
  }
26241
- async function executeOfficialUpgrade(ssh, helperPath, command) {
27758
+ async function executeOfficialUpgrade(ssh, helperPath, command, timeoutMs) {
26242
27759
  let execution;
26243
27760
  let executionError;
27761
+ let helperPrepared = false;
26244
27762
  try {
27763
+ await prepareRemoteUpgradeHelperDirectory(ssh, helperPath);
27764
+ helperPrepared = true;
26245
27765
  await ssh.uploadText(helperPath, release_assets_default, 384);
26246
- execution = await ssh.exec(command, 600000);
27766
+ execution = await ssh.exec(command, timeoutMs);
26247
27767
  } catch (error) {
26248
27768
  executionError = error;
26249
27769
  }
26250
27770
  let cleanupError;
26251
- try {
26252
- await removeRemoteUpgradeHelper(ssh, helperPath);
26253
- } catch (error) {
26254
- cleanupError = error;
27771
+ if (helperPrepared) {
27772
+ try {
27773
+ await removeRemoteUpgradeHelper(ssh, helperPath);
27774
+ } catch (error) {
27775
+ cleanupError = error;
27776
+ }
26255
27777
  }
26256
27778
  return officialUpgradeOutcome(execution, executionError, cleanupError);
26257
27779
  }
@@ -26420,6 +27942,7 @@ Actions: ping, setup, install, upgrade, diagnose, exec, troubleshoot, container_
26420
27942
  storage_type: optional(stringEnum(["juicefs", "minio"]), "[install] Storage backend configurable through Admin"),
26421
27943
  version: optional(Type.String(), "[upgrade] Specific version"),
26422
27944
  edge_runtime_version: optional(Type.String(), "[upgrade] Exact independent Edge Runtime version"),
27945
+ artifact_transport: optional(stringEnum(["local", "remote"]), "[upgrade] Download verified release assets locally or on the server (default: remote)"),
26423
27946
  github_proxy: optional(Type.String(), "[install/upgrade] Explicit GitHub proxy prefix, or direct/none"),
26424
27947
  focus: optional(stringEnum(["all", "containers", "database", "network", "disk", "logs"]), "[troubleshoot] Focus area"),
26425
27948
  container: optional(Type.String(), "[container_logs] Container name"),
@@ -26456,7 +27979,7 @@ Actions: ping, setup, install, upgrade, diagnose, exec, troubleshoot, container_
26456
27979
  case "install": {
26457
27980
  if (!args.public_domain)
26458
27981
  throw new Error("'public_domain' required");
26459
- const installId = randomUUID();
27982
+ const installId = randomUUID4();
26460
27983
  const DIR = "/opt/supacloud";
26461
27984
  const LOG = `/var/log/supacloud/install-${installId}.log`;
26462
27985
  const STATUS = `/var/log/supacloud/install-${installId}.status`;
@@ -26546,19 +28069,35 @@ ${result.stderr.slice(-500)}`;
26546
28069
  throw new Error("'version' is required with 'edge_runtime_version'");
26547
28070
  }
26548
28071
  if (edgeRuntimeVersion && version) {
26549
- assertExactStableVersion(version, "version");
26550
- assertExactStableVersion(edgeRuntimeVersion, "edge_runtime_version");
28072
+ assertExactStableVersion2(version, "version");
28073
+ assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
26551
28074
  }
26552
28075
  const validatedProxy = args.github_proxy ? assertSafeGithubProxy(args.github_proxy) : undefined;
28076
+ if (args.artifact_transport === "local") {
28077
+ if (!version || !edgeRuntimeVersion) {
28078
+ throw new Error("Local artifact transport requires exact 'version' and 'edge_runtime_version'");
28079
+ }
28080
+ assertExactStableVersion2(version, "version");
28081
+ assertExactStableVersion2(edgeRuntimeVersion, "edge_runtime_version");
28082
+ if (validatedProxy && !["direct", "none"].includes(validatedProxy.toLowerCase())) {
28083
+ throw new Error("Local artifact transport only supports direct GitHub downloads");
28084
+ }
28085
+ text = await executeLocalUpgradeTransfer(ssh, {
28086
+ managementVersion: version.replace(/^v/, ""),
28087
+ edgeRuntimeVersion: edgeRuntimeVersion.replace(/^v/, "")
28088
+ });
28089
+ break;
28090
+ }
26553
28091
  const githubProxy = validatedProxy && !["direct", "none"].includes(validatedProxy.toLowerCase()) ? validatedProxy : undefined;
26554
- const helperPath = `/tmp/.supacloud-release-assets-${randomUUID()}.sh`;
28092
+ const helperPath = `/tmp/.supacloud-release-assets-${randomUUID4()}/release_assets.sh`;
26555
28093
  const cmd = buildOfficialUpgradeCommand({
26556
28094
  version,
26557
28095
  edgeRuntimeVersion,
26558
28096
  githubProxy,
26559
28097
  helperPath
26560
28098
  });
26561
- const upgradeExecution = await executeOfficialUpgrade(ssh, helperPath, cmd);
28099
+ const upgradeTimeoutMs = githubProxy ? PROXIED_UPGRADE_SSH_TIMEOUT_MS : DIRECT_UPGRADE_SSH_TIMEOUT_MS;
28100
+ const upgradeExecution = await executeOfficialUpgrade(ssh, helperPath, cmd, upgradeTimeoutMs);
26562
28101
  const edgeBoundary = edgeRuntimeVersion ? "" : `
26563
28102
  ⚠️ Edge Runtime was not upgraded; provide --edge_runtime_version for a component transaction.`;
26564
28103
  text = `✅ Upgrade done
@@ -27407,6 +28946,48 @@ Actions: routes, upsert_route, update_route, delete_route, config, get_certifica
27407
28946
  return { content: [{ type: "text", text }] };
27408
28947
  });
27409
28948
  }
28949
+ // package.json
28950
+ var package_default = {
28951
+ name: "@supacloud/admin",
28952
+ version: "0.7.8",
28953
+ description: "Platform administration CLI for SupaCloud operators",
28954
+ type: "module",
28955
+ main: "./dist/index.js",
28956
+ bin: {
28957
+ "supacloud-admin": "dist/index.js"
28958
+ },
28959
+ files: [
28960
+ "dist",
28961
+ "README.md"
28962
+ ],
28963
+ scripts: {
28964
+ dev: "bun run --watch src/index.ts",
28965
+ build: "bun build src/index.ts --outdir dist --target node",
28966
+ prepublishOnly: "bun run build",
28967
+ typecheck: "tsc --noEmit"
28968
+ },
28969
+ keywords: [
28970
+ "supacloud",
28971
+ "admin",
28972
+ "ssh",
28973
+ "ops"
28974
+ ],
28975
+ license: "MIT",
28976
+ repository: {
28977
+ type: "git",
28978
+ url: "https://github.com/zuohuadong/supacloud.git",
28979
+ directory: "packages/admin"
28980
+ },
28981
+ dependencies: {
28982
+ "@sinclair/typebox": "^0.34.52",
28983
+ ssh2: "^1.17.0"
28984
+ },
28985
+ devDependencies: {
28986
+ "@types/bun": "^1.3.14",
28987
+ "@types/ssh2": "^1.15.5",
28988
+ typescript: "^7.0.2"
28989
+ }
28990
+ };
27410
28991
 
27411
28992
  // src/index.ts
27412
28993
  var adminProjectActionSchema = stringEnum([
@@ -27474,6 +29055,7 @@ USAGE
27474
29055
  supacloud-admin <module> <action> [--flags]
27475
29056
  supacloud-admin status
27476
29057
  supacloud-admin --help
29058
+ supacloud-admin --version
27477
29059
 
27478
29060
  EXPECTED CONTEXT
27479
29061
 
@@ -27659,6 +29241,10 @@ async function main() {
27659
29241
  printHelp();
27660
29242
  process.exit(0);
27661
29243
  }
29244
+ if (args.length === 1 && args[0] === "--version") {
29245
+ console.log(package_default.version);
29246
+ return;
29247
+ }
27662
29248
  const cliTools = createAdminTools();
27663
29249
  if (args.length === 1 && cliTools[args[0]]) {
27664
29250
  const result = await cliTools[args[0]].callback({});