From e24674274b9e1482be63885b881efd8491a279b2 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 12:26:53 -0800 Subject: [PATCH 1/6] fix(server): keep mirror options on the release metadata request --- apps/server/src/cloud/npmRelease.test.ts | 9 +++++++-- apps/server/src/cloud/npmRelease.ts | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/server/src/cloud/npmRelease.test.ts b/apps/server/src/cloud/npmRelease.test.ts index 96b16c1e6dc22..530d69911e4db 100644 --- a/apps/server/src/cloud/npmRelease.test.ts +++ b/apps/server/src/cloud/npmRelease.test.ts @@ -169,11 +169,16 @@ it.effect("authenticates the fork API without sending the GitHub token to an ass expect(request.headers.authorization).toBe("Bearer test-token"); return json([{ tag_name: `v${version}` }]); } - expect(request.url).toBe(`https://mirror.example/v${version}/t3-release.json`); + expect(request.url).toBe( + `https://mirror.example/releases/v${version}/t3-release.json?region=test`, + ); expect(request.headers.authorization).toBeUndefined(); return json({ version, databaseMigration: 85 }); }, - { GH_TOKEN: "test-token", T3CODE_RELEASE_BASE_URL: "https://mirror.example/" }, + { + GH_TOKEN: "test-token", + T3CODE_RELEASE_BASE_URL: "https://mirror.example/releases/?region=test", + }, ); expect(yield* service.get({ spec: "nightly" })).toEqual({ version, databaseMigration: 85 }); }), diff --git a/apps/server/src/cloud/npmRelease.ts b/apps/server/src/cloud/npmRelease.ts index 57490e2098142..d676926c5a0eb 100644 --- a/apps/server/src/cloud/npmRelease.ts +++ b/apps/server/src/cloud/npmRelease.ts @@ -7,7 +7,7 @@ import { CLI_RELEASE_BASE_URL_ENV, CLI_RELEASE_METADATA_FILE, CLI_RELEASE_REPOSITORY, - cliReleaseDownloadBaseUrl, + cliReleaseAssetUrl, cliReleaseIndexPageUrl, newestCliReleaseVersion, } from "@t3tools/shared/cliRelease"; @@ -112,7 +112,7 @@ export const make = Effect.fn("NpmRelease.make")(function* () { const version = yield* resolveVersion(spec); if (version === null) return null; const json = yield* read( - `${cliReleaseDownloadBaseUrl(version, releaseBaseUrl)}/${CLI_RELEASE_METADATA_FILE}`, + cliReleaseAssetUrl(version, CLI_RELEASE_METADATA_FILE, releaseBaseUrl), { api: false, allowMissing: true }, ); // Older releases have no migration asset; track-switch checks must keep them blocked. From 3b5bb6544c14e72084b692705da3a17cf0fbdca7 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 12:26:47 -0800 Subject: [PATCH 2/6] fix(cli): preserve release mirror query parameters in installers --- scripts/build-npm-launcher.test.ts | 14 ++++++++++--- scripts/install.ps1 | 12 ++++++++--- scripts/install.sh | 7 +++++-- scripts/install.test.ts | 33 +++++++++++++++++++++++------- scripts/npm-launcher/t3.mjs | 15 +++++++++----- 5 files changed, 61 insertions(+), 20 deletions(-) diff --git a/scripts/build-npm-launcher.test.ts b/scripts/build-npm-launcher.test.ts index 5facd9dabeb33..01b2ad41a1b10 100644 --- a/scripts/build-npm-launcher.test.ts +++ b/scripts/build-npm-launcher.test.ts @@ -77,9 +77,13 @@ exit 7 const requests: Array = []; const server = NodeHttp.createServer((request, response) => { requests.push(request.url ?? ""); - if (request.url === `/v${VERSION}/SHA256SUMS`) { + const url = new URL(request.url ?? "", "http://localhost"); + const prefix = url.pathname.startsWith("/mirror/") ? "/mirror" : ""; + if (prefix && url.search !== "?region=test") { + response.writeHead(400).end(); + } else if (url.pathname === `${prefix}/v${VERSION}/SHA256SUMS`) { response.end(`${checksum} ${archiveName}\n`); - } else if (request.url === `/v${VERSION}/${archiveName}`) { + } else if (url.pathname === `${prefix}/v${VERSION}/${archiveName}`) { response.writeHead(200, { "Content-Length": archive.length }).end(archive); } else { response.writeHead(404).end(); @@ -153,7 +157,7 @@ it.layer(NodeServices.layer)("build-npm-launcher", (it) => { PATH: (yield* HostProcess.Environment).PATH, T3CODE_HOME: home, T3CODE_TEST_CALLS: path.join(root, "calls"), - T3CODE_RELEASE_BASE_URL: release.baseUrl, + T3CODE_RELEASE_BASE_URL: `${release.baseUrl}/mirror///?region=test#download`, }; const launcher = yield* fs.realPath(path.join(packageDir, "bin/t3.mjs")); const first = yield* run(process.execPath, [launcher, "serve", "a b"], { @@ -177,6 +181,10 @@ it.layer(NodeServices.layer)("build-npm-launcher", (it) => { [VERSION], ); + assert.deepStrictEqual(release.requests, [ + `/mirror/v${VERSION}/SHA256SUMS?region=test`, + `/mirror/v${VERSION}/t3-${VERSION}-${HostProcess.Platform.defaultValue()}-${HostProcess.Architecture.defaultValue()}.tar.gz?region=test`, + ]); const requestsAfterFirst = release.requests.length; const second = yield* run(process.execPath, [launcher, "--version"], { cwd: root, env }); assert.equal(second.exitCode, 0); diff --git a/scripts/install.ps1 b/scripts/install.ps1 index edc963758a438..79644f118f4f2 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -17,10 +17,16 @@ $ErrorActionPreference = "Stop" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $repo = "ScriptedAlchemy/t3code" -$baseUrl = if ($env:T3CODE_RELEASE_BASE_URL) { $env:T3CODE_RELEASE_BASE_URL.TrimEnd("/") } else { "https://github.com/$repo/releases/download" } +$baseUrl = if ($env:T3CODE_RELEASE_BASE_URL) { $env:T3CODE_RELEASE_BASE_URL } else { "https://github.com/$repo/releases/download" } $t3Home = if ($env:T3CODE_HOME) { $env:T3CODE_HOME } else { Join-Path $HOME ".t3" } $binDir = if ($env:T3CODE_INSTALL_BIN_DIR) { $env:T3CODE_INSTALL_BIN_DIR } else { Join-Path $HOME ".local\bin" } +function Release-Url([string] $file) { + $url = New-Object System.UriBuilder($baseUrl) + $url.Path = "$($url.Path.TrimEnd("/"))/v$version/$file" + return $url.Uri.AbsoluteUri +} + function Fail([string] $message) { Write-Error "t3 install: $message" exit 1 @@ -173,7 +179,7 @@ if ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq $version)) { [Console]::Error.WriteLine(" ${muted}Installing$reset T3 Code $bold$version$reset`n") Step "Downloading..." try { - Fetch "$baseUrl/v$version/SHA256SUMS" (Join-Path $staging "SHA256SUMS") + Fetch (Release-Url "SHA256SUMS") (Join-Path $staging "SHA256SUMS") } catch { $status = $_.Exception.Response.StatusCode.value__ if ($status -eq 404) { @@ -181,7 +187,7 @@ if ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq $version)) { } throw } - Fetch "$baseUrl/v$version/$archive" (Join-Path $staging $archive) -progress + Fetch (Release-Url $archive) (Join-Path $staging $archive) -progress Step "Verifying the download..." diff --git a/scripts/install.sh b/scripts/install.sh index 7871428cef8ed..3b39c2af33a94 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -19,6 +19,9 @@ set -eu repo="ScriptedAlchemy/t3code" base_url="${T3CODE_RELEASE_BASE_URL:-https://github.com/${repo}/releases/download}" +base_path="${base_url%%[?#]*}" +base_suffix="${base_url#"$base_path"}" +while [ "${base_path%/}" != "$base_path" ]; do base_path="${base_path%/}"; done t3_home="${T3CODE_HOME:-$HOME/.t3}" case "$t3_home" in /*) ;; @@ -211,7 +214,7 @@ else printf ' %sInstalling%s T3 Code %s%s%s\n\n' "$muted" "$reset" "$bold" "$version" "$reset" >&2 step "Downloading..." fetch_status=0 - fetch "${base_url}/v${version}/SHA256SUMS" "${staging}/SHA256SUMS" || fetch_status=$? + fetch "${base_path}/v${version}/SHA256SUMS${base_suffix}" "${staging}/SHA256SUMS" || fetch_status=$? if [ "$fetch_status" -eq 44 ]; then fail "t3 ${version} has no release archive for ${platform}-${arch}; choose a published fork build from https://github.com/${repo}/releases" elif [ "$fetch_status" -ne 0 ]; then @@ -219,7 +222,7 @@ else fi expected="$(grep " \*\{0,1\}${archive}\$" "${staging}/SHA256SUMS" | cut -d' ' -f1)" [ -n "$expected" ] || fail "t3 ${version} has no release archive for ${platform}-${arch}" - download "${base_url}/v${version}/${archive}" "${staging}/${archive}" + download "${base_path}/v${version}/${archive}${base_suffix}" "${staging}/${archive}" step "Verifying the download..." actual="$(checksum "${staging}/${archive}")" diff --git a/scripts/install.test.ts b/scripts/install.test.ts index 5279d34b48223..10a067b1b5f55 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -130,14 +130,20 @@ describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer chan }); }); -describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer checksum", () => { +describe.skipIf(HostProcess.Platform.defaultValue() === "win32")("installer checksum", () => { it.each( ["home\\data", "home\ndata"].flatMap((homeName) => - [true, false].map((validChecksum) => ({ homeName, validChecksum })), + [true, false].flatMap((validChecksum) => + ["", "/mirror/", "/mirror///?region=test#download"].map((releaseSuffix) => ({ + homeName, + validChecksum, + releaseSuffix, + })), + ), ), )( - "verifies home $homeName with valid checksum $validChecksum", - async ({ homeName, validChecksum }) => { + "verifies home $homeName with valid checksum $validChecksum using mirror $releaseSuffix", + async ({ homeName, validChecksum, releaseSuffix }) => { const availableTools = ["sha256sum", "shasum"].filter( (tool) => NodeChildProcess.spawnSync("sh", ["-c", 'command -v "$1"', "sh", tool]).status === 0, @@ -147,7 +153,7 @@ describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer chec const root = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-install-checksum-")); const home = NodePath.join(root, homeName); const version = "1.2.3"; - const stem = `t3-${version}-linux-${HostProcess.Architecture.defaultValue()}`; + const stem = `t3-${version}-${HostProcess.Platform.defaultValue()}-${HostProcess.Architecture.defaultValue()}`; const archiveName = `${stem}.tar.gz`; const server = NodeHttp.createServer(); try { @@ -185,9 +191,18 @@ describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer chec NodeChildProcess.execFileSync("tar", ["-czf", archivePath, "-C", root, stem]); const archive = await NodeFSP.readFile(archivePath); const digest = NodeCrypto.createHash("sha256").update(archive).digest("hex"); + const prefix = releaseSuffix ? "/mirror" : ""; + const search = releaseSuffix.includes("?") ? "?region=test" : ""; + const requests: string[] = []; server.on("request", (request, response) => { + requests.push(request.url ?? ""); + const url = new URL(request.url ?? "", "http://localhost"); + if (!url.pathname.startsWith(`${prefix}/v${version}/`) || url.search !== search) { + response.writeHead(404).end(); + return; + } response.end( - request.url?.endsWith("/SHA256SUMS") + url.pathname.endsWith("/SHA256SUMS") ? `${validChecksum ? digest : "0".repeat(64)} ${archiveName}\n` : archive, ); @@ -207,7 +222,7 @@ describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer chec T3CODE_VERSION: version, T3CODE_HOME: home, T3CODE_INSTALL_BIN_DIR: NodePath.join(root, "bin"), - T3CODE_RELEASE_BASE_URL: `http://127.0.0.1:${address.port}`, + T3CODE_RELEASE_BASE_URL: `http://127.0.0.1:${address.port}${releaseSuffix}`, }, stdio: ["ignore", "pipe", "pipe"], }, @@ -219,6 +234,10 @@ describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("installer chec child.on("close", (code) => resolve({ code, output })); }, ); + expect(requests).toEqual([ + `${prefix}/v${version}/SHA256SUMS${search}`, + `${prefix}/v${version}/${archiveName}${search}`, + ]); const versions = NodePath.join(home, "runtime/versions"); if (validChecksum) { expect(code, `${checksumTool}: ${output}`).toBe(0); diff --git a/scripts/npm-launcher/t3.mjs b/scripts/npm-launcher/t3.mjs index 867b547eedddd..bdf048bafbda3 100644 --- a/scripts/npm-launcher/t3.mjs +++ b/scripts/npm-launcher/t3.mjs @@ -63,11 +63,16 @@ async function download(url, file) { async function install(dir, key) { const archive = `t3-${version}-${key}.${process.platform === "win32" ? "zip" : "tar.gz"}`; - const base = ( + const base = new URL( process.env.T3CODE_RELEASE_BASE_URL || - "https://github.com/ScriptedAlchemy/t3code/releases/download" - ).replace(/\/+$/, ""); - const sums = await (await get(`${base}/v${version}/SHA256SUMS`)).text(); + "https://github.com/ScriptedAlchemy/t3code/releases/download", + ); + const releaseUrl = (file) => { + const url = new URL(base); + url.pathname = `${url.pathname.replace(/\/+$/, "")}/v${version}/${file}`; + return url.href; + }; + const sums = await (await get(releaseUrl("SHA256SUMS"))).text(); const expected = /^([0-9a-f]{64})\s+\*?(.+)$/im; const line = sums .split(/\r?\n/) @@ -80,7 +85,7 @@ async function install(dir, key) { const staging = fs.mkdtempSync(`${dir}.staging-`); try { const file = path.join(staging, archive); - if ((await download(`${base}/v${version}/${archive}`, file)) !== line[1].toLowerCase()) { + if ((await download(releaseUrl(archive), file)) !== line[1].toLowerCase()) { throw new Error(`checksum mismatch for ${archive}`); } // Windows' own tar.exe reads zip; a Git-for-Windows tar on PATH cannot. From 0bda6eafad9ed2f541ec56be834d107c0451278c Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 12:24:57 -0800 Subject: [PATCH 3/6] fix(ssh): preserve mirror queries in archive download URLs --- packages/ssh/src/tunnel.test.ts | 30 ++++++++++++++++++++++++++++-- packages/ssh/src/tunnel.ts | 17 +++++++++++------ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 6f4f14ea9eb64..0cf70b71cd905 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -90,7 +90,11 @@ const runArchiveRunner = Effect.fn("test.runArchiveRunner")( function* ( installedVersion: string, installDirectory = ".local/bin", - options?: { readonly cliScript?: string; readonly fifo?: boolean }, + options?: { + readonly cliScript?: string; + readonly fifo?: boolean; + readonly releaseBaseUrl?: string; + }, ) { const fs = yield* FileSystem.FileSystem; const root = yield* fs.makeTempDirectoryScoped(); @@ -112,7 +116,15 @@ const runArchiveRunner = Effect.fn("test.runArchiveRunner")( } yield* fs.writeFileString(`${root}/bin/curl`, '#!/bin/sh\necho "HTTP 404" >&2\nexit 22\n'); yield* fs.chmod(`${root}/bin/curl`, 0o700); - yield* fs.writeFileString(`${root}/runner.sh`, SshTunnel.buildRemoteT3RunnerScript(ARCHIVE)); + yield* fs.writeFileString( + `${root}/runner.sh`, + SshTunnel.buildRemoteT3RunnerScript({ + ...ARCHIVE, + ...(options?.releaseBaseUrl === undefined + ? {} + : { releaseBaseUrl: options.releaseBaseUrl }), + }), + ); const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const child = yield* spawner.spawn( ChildProcess.make("sh", [`${root}/runner.sh`, "pair", "--json"], { @@ -268,6 +280,20 @@ esac }), ); + it.live("preserves the release mirror query and fragment after the checksum path", () => + Effect.gen(function* () { + const result = yield* runArchiveRunner("0.0.1", ".t3/bin", { + releaseBaseUrl: "https://mirror.example/releases/?region=test#download", + }); + assert.notEqual(result.exitCode, 0); + assert.include( + result.stderr, + `https://mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS?region=test#download`, + ); + assert.isFalse(result.runtimeExists); + }), + ); + it.live( "skips an installed CLI whose probes hang and falls through to the archive download", () => diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index cb08ae9851c4e..cbaa9c7ab7291 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -3,7 +3,7 @@ import type { DesktopSshEnvironmentTarget, } from "@t3tools/contracts"; import { waitForHttpReady as waitForHttpReadyShared } from "@t3tools/shared/httpReadiness"; -import { cliReleaseDownloadBaseUrl } from "@t3tools/shared/cliRelease"; +import { cliReleaseAssetUrl } from "@t3tools/shared/cliRelease"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; import * as NetService from "@t3tools/shared/Net"; import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson"; @@ -442,6 +442,7 @@ fi # Self-contained release archive: no Node, npm, or compiler on the remote. # Unpacked into the pinned-runtime layout so \`t3 service install\` reuses it. T3_RELEASE_BASE_URL=@@T3_RELEASE_BASE_URL@@ +T3_RELEASE_URL_SUFFIX=@@T3_RELEASE_URL_SUFFIX@@ T3_RUNTIME_DIR="$HOME/.t3/runtime/versions/$T3_ARCHIVE_VERSION" t3_runtime_ready() { [ -x "$T3_RUNTIME_DIR/t3" ] && [ "$(cat "$T3_RUNTIME_DIR/.install-complete" 2>/dev/null)" = "$T3_ARCHIVE_VERSION" ] @@ -537,8 +538,8 @@ if ! t3_runtime_ready; then printf 'Could not download T3 release artifact: %s\\nInstall the exact T3 CLI (%s) on the remote host, or use a desktop version with published CLI archives.\\n' "$1" "$T3_ARCHIVE_VERSION" >&2 return 1 } - t3_fetch "$T3_RELEASE_BASE_URL/v$T3_ARCHIVE_VERSION/SHA256SUMS" "$T3_STAGING/SHA256SUMS" @@T3_ARCHIVE_CHECKSUMS_SECONDS@@ - t3_fetch "$T3_RELEASE_BASE_URL/v$T3_ARCHIVE_VERSION/$T3_ARCHIVE" "$T3_STAGING/$T3_ARCHIVE" @@T3_ARCHIVE_DOWNLOAD_SECONDS@@ + t3_fetch "$T3_RELEASE_BASE_URL/SHA256SUMS$T3_RELEASE_URL_SUFFIX" "$T3_STAGING/SHA256SUMS" @@T3_ARCHIVE_CHECKSUMS_SECONDS@@ + t3_fetch "$T3_RELEASE_BASE_URL/$T3_ARCHIVE$T3_RELEASE_URL_SUFFIX" "$T3_STAGING/$T3_ARCHIVE" @@T3_ARCHIVE_DOWNLOAD_SECONDS@@ T3_EXPECTED="$(grep " \\*\\{0,1\\}$T3_ARCHIVE$" "$T3_STAGING/SHA256SUMS" | cut -d' ' -f1)" if command -v sha256sum >/dev/null 2>&1; then T3_ACTUAL="$(sha256sum "$T3_STAGING/$T3_ARCHIVE" | cut -d' ' -f1)" @@ -860,15 +861,19 @@ export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string if (archiveVersion !== "" && !EXACT_ARCHIVE_VERSION.test(archiveVersion)) { throw new SshInvalidArchiveVersionError({ archiveVersion }); } - const releaseBaseUrl = cliReleaseDownloadBaseUrl("", input?.releaseBaseUrl ?? undefined).replace( - /\/v$/u, - "", + const releaseUrl = new URL( + cliReleaseAssetUrl(archiveVersion, "", input?.releaseBaseUrl ?? undefined), ); + const releaseUrlSuffix = `${releaseUrl.search}${releaseUrl.hash}`; + releaseUrl.search = ""; + releaseUrl.hash = ""; + const releaseBaseUrl = releaseUrl.toString().replace(/\/+$/u, ""); return stripTrailingNewlines( applyScriptPlaceholders(REMOTE_RUNNER_SCRIPT, { T3_NODE_SCRIPT_PATH: shellSingleQuote(nodeScriptPath), T3_ARCHIVE_VERSION: shellSingleQuote(archiveVersion), T3_RELEASE_BASE_URL: shellSingleQuote(releaseBaseUrl), + T3_RELEASE_URL_SUFFIX: shellSingleQuote(releaseUrlSuffix), T3_ARCHIVE_LOCK_WAIT_SECONDS: String(REMOTE_ARCHIVE_LOCK_WAIT_SECONDS), T3_INSTALLED_PROBE_SECONDS: String(REMOTE_INSTALLED_PROBE_SECONDS), T3_ARCHIVE_DOWNLOAD_SECONDS: String(REMOTE_ARCHIVE_DOWNLOAD_SECONDS), From ad28fc3e4da2577401184b76734e5b69b444b917 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 12:29:13 -0800 Subject: [PATCH 4/6] fix(shared): build versioned mirror roots with the common URL helper --- packages/shared/src/cliRelease.ts | 2 +- packages/ssh/src/tunnel.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shared/src/cliRelease.ts b/packages/shared/src/cliRelease.ts index 05271fffea019..a86b627f73d4b 100644 --- a/packages/shared/src/cliRelease.ts +++ b/packages/shared/src/cliRelease.ts @@ -49,7 +49,7 @@ export function cliReleaseDownloadBaseUrl( version: string, baseUrl: string | undefined = CLI_RELEASE_DEFAULT_BASE_URL, ): string { - return `${(baseUrl?.trim() || CLI_RELEASE_DEFAULT_BASE_URL).replace(/\/+$/, "")}/v${version}`; + return releaseAssetUrl(baseUrl?.trim() || CLI_RELEASE_DEFAULT_BASE_URL, `v${version}`); } /** `segments` added to `baseUrl`'s path, keeping its query, such as a signed mirror URL's token. */ diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index cbaa9c7ab7291..69d1ade981271 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -3,7 +3,7 @@ import type { DesktopSshEnvironmentTarget, } from "@t3tools/contracts"; import { waitForHttpReady as waitForHttpReadyShared } from "@t3tools/shared/httpReadiness"; -import { cliReleaseAssetUrl } from "@t3tools/shared/cliRelease"; +import { cliReleaseDownloadBaseUrl } from "@t3tools/shared/cliRelease"; import * as KeyedLock from "@t3tools/shared/KeyedLock"; import * as NetService from "@t3tools/shared/Net"; import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson"; @@ -862,12 +862,12 @@ export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string throw new SshInvalidArchiveVersionError({ archiveVersion }); } const releaseUrl = new URL( - cliReleaseAssetUrl(archiveVersion, "", input?.releaseBaseUrl ?? undefined), + cliReleaseDownloadBaseUrl(archiveVersion, input?.releaseBaseUrl ?? undefined), ); const releaseUrlSuffix = `${releaseUrl.search}${releaseUrl.hash}`; releaseUrl.search = ""; releaseUrl.hash = ""; - const releaseBaseUrl = releaseUrl.toString().replace(/\/+$/u, ""); + const releaseBaseUrl = releaseUrl.toString(); return stripTrailingNewlines( applyScriptPlaceholders(REMOTE_RUNNER_SCRIPT, { T3_NODE_SCRIPT_PATH: shellSingleQuote(nodeScriptPath), From 982aba499c3fb1fe2f087f1f42ac3d0f16ccd549 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 12:34:43 -0800 Subject: [PATCH 5/6] fix(cli): keep URL fragments out of POSIX downloader requests --- packages/ssh/src/tunnel.test.ts | 2 +- packages/ssh/src/tunnel.ts | 10 +++++----- scripts/install.sh | 9 +++++---- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 0cf70b71cd905..9973166cb0090 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -288,7 +288,7 @@ esac assert.notEqual(result.exitCode, 0); assert.include( result.stderr, - `https://mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS?region=test#download`, + `https://mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS?region=test`, ); assert.isFalse(result.runtimeExists); }), diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 69d1ade981271..787b70c35fab0 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -442,7 +442,7 @@ fi # Self-contained release archive: no Node, npm, or compiler on the remote. # Unpacked into the pinned-runtime layout so \`t3 service install\` reuses it. T3_RELEASE_BASE_URL=@@T3_RELEASE_BASE_URL@@ -T3_RELEASE_URL_SUFFIX=@@T3_RELEASE_URL_SUFFIX@@ +T3_RELEASE_QUERY=@@T3_RELEASE_QUERY@@ T3_RUNTIME_DIR="$HOME/.t3/runtime/versions/$T3_ARCHIVE_VERSION" t3_runtime_ready() { [ -x "$T3_RUNTIME_DIR/t3" ] && [ "$(cat "$T3_RUNTIME_DIR/.install-complete" 2>/dev/null)" = "$T3_ARCHIVE_VERSION" ] @@ -538,8 +538,8 @@ if ! t3_runtime_ready; then printf 'Could not download T3 release artifact: %s\\nInstall the exact T3 CLI (%s) on the remote host, or use a desktop version with published CLI archives.\\n' "$1" "$T3_ARCHIVE_VERSION" >&2 return 1 } - t3_fetch "$T3_RELEASE_BASE_URL/SHA256SUMS$T3_RELEASE_URL_SUFFIX" "$T3_STAGING/SHA256SUMS" @@T3_ARCHIVE_CHECKSUMS_SECONDS@@ - t3_fetch "$T3_RELEASE_BASE_URL/$T3_ARCHIVE$T3_RELEASE_URL_SUFFIX" "$T3_STAGING/$T3_ARCHIVE" @@T3_ARCHIVE_DOWNLOAD_SECONDS@@ + t3_fetch "$T3_RELEASE_BASE_URL/SHA256SUMS$T3_RELEASE_QUERY" "$T3_STAGING/SHA256SUMS" @@T3_ARCHIVE_CHECKSUMS_SECONDS@@ + t3_fetch "$T3_RELEASE_BASE_URL/$T3_ARCHIVE$T3_RELEASE_QUERY" "$T3_STAGING/$T3_ARCHIVE" @@T3_ARCHIVE_DOWNLOAD_SECONDS@@ T3_EXPECTED="$(grep " \\*\\{0,1\\}$T3_ARCHIVE$" "$T3_STAGING/SHA256SUMS" | cut -d' ' -f1)" if command -v sha256sum >/dev/null 2>&1; then T3_ACTUAL="$(sha256sum "$T3_STAGING/$T3_ARCHIVE" | cut -d' ' -f1)" @@ -864,7 +864,7 @@ export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string const releaseUrl = new URL( cliReleaseDownloadBaseUrl(archiveVersion, input?.releaseBaseUrl ?? undefined), ); - const releaseUrlSuffix = `${releaseUrl.search}${releaseUrl.hash}`; + const releaseQuery = releaseUrl.search; releaseUrl.search = ""; releaseUrl.hash = ""; const releaseBaseUrl = releaseUrl.toString(); @@ -873,7 +873,7 @@ export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string T3_NODE_SCRIPT_PATH: shellSingleQuote(nodeScriptPath), T3_ARCHIVE_VERSION: shellSingleQuote(archiveVersion), T3_RELEASE_BASE_URL: shellSingleQuote(releaseBaseUrl), - T3_RELEASE_URL_SUFFIX: shellSingleQuote(releaseUrlSuffix), + T3_RELEASE_QUERY: shellSingleQuote(releaseQuery), T3_ARCHIVE_LOCK_WAIT_SECONDS: String(REMOTE_ARCHIVE_LOCK_WAIT_SECONDS), T3_INSTALLED_PROBE_SECONDS: String(REMOTE_INSTALLED_PROBE_SECONDS), T3_ARCHIVE_DOWNLOAD_SECONDS: String(REMOTE_ARCHIVE_DOWNLOAD_SECONDS), diff --git a/scripts/install.sh b/scripts/install.sh index 3b39c2af33a94..89cc23d60120c 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -19,8 +19,9 @@ set -eu repo="ScriptedAlchemy/t3code" base_url="${T3CODE_RELEASE_BASE_URL:-https://github.com/${repo}/releases/download}" -base_path="${base_url%%[?#]*}" -base_suffix="${base_url#"$base_path"}" +base_url="${base_url%%#*}" +base_path="${base_url%%\?*}" +base_query="${base_url#"$base_path"}" while [ "${base_path%/}" != "$base_path" ]; do base_path="${base_path%/}"; done t3_home="${T3CODE_HOME:-$HOME/.t3}" case "$t3_home" in @@ -214,7 +215,7 @@ else printf ' %sInstalling%s T3 Code %s%s%s\n\n' "$muted" "$reset" "$bold" "$version" "$reset" >&2 step "Downloading..." fetch_status=0 - fetch "${base_path}/v${version}/SHA256SUMS${base_suffix}" "${staging}/SHA256SUMS" || fetch_status=$? + fetch "${base_path}/v${version}/SHA256SUMS${base_query}" "${staging}/SHA256SUMS" || fetch_status=$? if [ "$fetch_status" -eq 44 ]; then fail "t3 ${version} has no release archive for ${platform}-${arch}; choose a published fork build from https://github.com/${repo}/releases" elif [ "$fetch_status" -ne 0 ]; then @@ -222,7 +223,7 @@ else fi expected="$(grep " \*\{0,1\}${archive}\$" "${staging}/SHA256SUMS" | cut -d' ' -f1)" [ -n "$expected" ] || fail "t3 ${version} has no release archive for ${platform}-${arch}" - download "${base_path}/v${version}/${archive}${base_suffix}" "${staging}/${archive}" + download "${base_path}/v${version}/${archive}${base_query}" "${staging}/${archive}" step "Verifying the download..." actual="$(checksum "${staging}/${archive}")" From a7572cd20836c563b3a1637b25448027182b7399 Mon Sep 17 00:00:00 2001 From: Zack Jackson <25274700+ScriptedAlchemy@users.noreply.github.com> Date: Sun, 8 Nov 2026 18:03:20 -0800 Subject: [PATCH 6/6] fix(cli): redact release mirror credentials from download failures --- packages/ssh/src/tunnel.test.ts | 22 +++++++++-- packages/ssh/src/tunnel.ts | 11 ++++-- scripts/build-npm-launcher.test.ts | 40 +++++++++++++++++++ scripts/install.ps1 | 16 +++++++- scripts/install.sh | 20 ++++++++-- scripts/install.test.ts | 63 ++++++++++++++++++++++++++++++ scripts/npm-launcher/t3.mjs | 16 ++++++-- 7 files changed, 173 insertions(+), 15 deletions(-) diff --git a/packages/ssh/src/tunnel.test.ts b/packages/ssh/src/tunnel.test.ts index 34b9f7d266bf1..addd176e01350 100644 --- a/packages/ssh/src/tunnel.test.ts +++ b/packages/ssh/src/tunnel.test.ts @@ -114,7 +114,10 @@ const runArchiveRunner = Effect.fn("test.runArchiveRunner")( ); yield* fs.chmod(`${root}/${installDirectory}/t3`, 0o700); } - yield* fs.writeFileString(`${root}/bin/curl`, '#!/bin/sh\necho "HTTP 404" >&2\nexit 22\n'); + yield* fs.writeFileString( + `${root}/bin/curl`, + '#!/bin/sh\nprintf "%s\\n" "$@" > "$HOME/request"\necho "HTTP 404 $*" >&2\nexit 22\n', + ); yield* fs.chmod(`${root}/bin/curl`, 0o700); yield* fs.writeFileString( `${root}/runner.sh`, @@ -146,6 +149,9 @@ const runArchiveRunner = Effect.fn("test.runArchiveRunner")( stdout, stderr, exitCode, + request: (yield* fs.exists(`${root}/request`)) + ? yield* fs.readFileString(`${root}/request`) + : "", runtimeExists: yield* fs.exists(`${root}/.t3/runtime/versions/${ARCHIVE.archiveVersion}`), }; }, @@ -304,16 +310,24 @@ esac }), ); - it.live("preserves the release mirror query and fragment after the checksum path", () => + it.live("keeps signed mirror credentials in the request and out of diagnostics", () => Effect.gen(function* () { const result = yield* runArchiveRunner("0.0.1", ".t3/bin", { - releaseBaseUrl: "https://mirror.example/releases/?region=test#download", + releaseBaseUrl: + "https://mirror-user:mirror-pass@mirror.example/releases/?signature=test-secret#download", }); assert.notEqual(result.exitCode, 0); assert.include( result.stderr, - `https://mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS?region=test`, + `https://mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS`, + ); + assert.include( + result.request, + `https://mirror-user:mirror-pass@mirror.example/releases/v${ARCHIVE.archiveVersion}/SHA256SUMS?signature=test-secret`, ); + for (const secret of ["mirror-user", "mirror-pass", "signature", "test-secret", "download"]) { + assert.notInclude(result.stderr, secret === "download" ? "#download" : secret); + } assert.isFalse(result.runtimeExists); }), ); diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 0415eccd98afc..d26ae134c684f 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -442,6 +442,7 @@ fi # Self-contained release archive: no Node, npm, or compiler on the remote. # Unpacked into the pinned-runtime layout so \`t3 service install\` reuses it. T3_RELEASE_BASE_URL=@@T3_RELEASE_BASE_URL@@ +T3_RELEASE_DISPLAY_URL=@@T3_RELEASE_DISPLAY_URL@@ T3_RELEASE_QUERY=@@T3_RELEASE_QUERY@@ T3_RUNTIME_DIR="$HOME/.t3/runtime/versions/$T3_ARCHIVE_VERSION" t3_runtime_ready() { @@ -530,12 +531,12 @@ if ! t3_runtime_ready; then trap 'rm -rf "$T3_STAGING" "$T3_LOCK"' EXIT t3_fetch() { if command -v curl >/dev/null 2>&1; then - if curl -fsSL --connect-timeout 30 --max-time "$3" "$1" -o "$2"; then return; fi + if curl -fsSL --connect-timeout 30 --max-time "$3" "$1" -o "$2" 2>/dev/null; then return; else T3_FETCH_EXIT=$?; fi elif command -v wget >/dev/null 2>&1; then - if wget -q --timeout=30 --tries=1 "$1" -O "$2"; then return; fi + if wget -q --timeout=30 --tries=1 "$1" -O "$2" 2>/dev/null; then return; else T3_FETCH_EXIT=$?; fi else printf 'Remote host needs curl or wget to download %s.\\n' "$T3_ARCHIVE" >&2; exit 1 fi - printf 'Could not download T3 release artifact: %s\\nInstall the exact T3 CLI (%s) on the remote host, or use a desktop version with published CLI archives.\\n' "$1" "$T3_ARCHIVE_VERSION" >&2 + printf 'Could not download T3 release artifact: %s (downloader exit %s)\\nInstall the exact T3 CLI (%s) on the remote host, or use a desktop version with published CLI archives.\\n' "$T3_RELEASE_DISPLAY_URL/\${2##*/}" "$T3_FETCH_EXIT" "$T3_ARCHIVE_VERSION" >&2 return 1 } t3_fetch "$T3_RELEASE_BASE_URL/SHA256SUMS$T3_RELEASE_QUERY" "$T3_STAGING/SHA256SUMS" @@T3_ARCHIVE_CHECKSUMS_SECONDS@@ @@ -875,12 +876,16 @@ export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string releaseUrl.search = ""; releaseUrl.hash = ""; const releaseBaseUrl = releaseUrl.toString(); + releaseUrl.username = ""; + releaseUrl.password = ""; + const releaseDisplayUrl = releaseUrl.toString(); return stripTrailingNewlines( applyScriptPlaceholders(REMOTE_RUNNER_SCRIPT, { T3_NODE_SCRIPT_PATH: shellSingleQuote(nodeScriptPath), T3_ARCHIVE_VERSION: shellSingleQuote(archiveVersion), T3_RELEASE_BASE_URL: shellSingleQuote(releaseBaseUrl), T3_RELEASE_QUERY: shellSingleQuote(releaseQuery), + T3_RELEASE_DISPLAY_URL: shellSingleQuote(releaseDisplayUrl), T3_ARCHIVE_LOCK_WAIT_SECONDS: String(REMOTE_ARCHIVE_LOCK_WAIT_SECONDS), T3_INSTALLED_PROBE_SECONDS: String(REMOTE_INSTALLED_PROBE_SECONDS), T3_ARCHIVE_DOWNLOAD_SECONDS: String(REMOTE_ARCHIVE_DOWNLOAD_SECONDS), diff --git a/scripts/build-npm-launcher.test.ts b/scripts/build-npm-launcher.test.ts index 1a51c8d8134f9..14e096b29e4c7 100644 --- a/scripts/build-npm-launcher.test.ts +++ b/scripts/build-npm-launcher.test.ts @@ -151,6 +151,46 @@ it.layer(NodeServices.layer)("build-npm-launcher", (it) => { assert.equal(unpack.exitCode, 0, unpack.stderr); const packageDir = path.join(installed, "package"); + const diagnosticHook = path.join(root, "diagnostic-hook.mjs"); + for (const status of [404, 403, 0]) { + yield* fs.writeFileString( + diagnosticHook, + `globalThis.fetch = async (url) => { + if (url !== "https://mirror-user:mirror-pass@mirror.example/releases/v${VERSION}/SHA256SUMS?signature=test-secret#download") throw new Error("wrong request"); + if (${status} === 0) throw new Error("transport signature=test-secret mirror-pass"); + return new Response("", { status: ${status} }); + };`, + ); + const failed = yield* run( + process.execPath, + ["--import", diagnosticHook, path.join(packageDir, "bin/t3.mjs")], + { + cwd: root, + env: { + PATH: (yield* HostProcess.Environment).PATH, + T3CODE_HOME: path.join(root, `failed-${status}`), + T3CODE_RELEASE_BASE_URL: + "https://mirror-user:mirror-pass@mirror.example/releases/?signature=test-secret#download", + }, + }, + ); + assert.equal(failed.exitCode, 1); + assert.include(failed.stderr, `https://mirror.example/releases/v${VERSION}/SHA256SUMS`); + assert.include( + failed.stderr, + status === 404 ? "does not exist" : status === 403 ? "HTTP 403" : "failed to connect", + ); + for (const secret of [ + "mirror-user", + "mirror-pass", + "signature", + "test-secret", + "#download", + ]) { + assert.notInclude(failed.stdout + failed.stderr, secret); + } + } + const release = yield* serveFakeRelease(root); const home = path.join(root, "home"); const env = { diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 79644f118f4f2..744797f6dd349 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -24,6 +24,7 @@ $binDir = if ($env:T3CODE_INSTALL_BIN_DIR) { $env:T3CODE_INSTALL_BIN_DIR } else function Release-Url([string] $file) { $url = New-Object System.UriBuilder($baseUrl) $url.Path = "$($url.Path.TrimEnd("/"))/v$version/$file" + $url.Fragment = "" return $url.Uri.AbsoluteUri } @@ -69,6 +70,19 @@ function Draw-Download([long] $bytes, [long] $total) { } } function Fetch([string] $uri, [string] $destination, [switch] $progress) { + try { + Fetch-Request $uri $destination -progress:$progress + } catch { + $status = $_.Exception.Response.StatusCode.value__ + $display = New-Object System.UriBuilder($uri) + $display.UserName = $display.Password = $display.Query = $display.Fragment = "" + $detail = if ($status) { "returned HTTP $status" } else { "failed to download" } + $failure = New-Object System.Exception("GET $($display.Uri.AbsoluteUri) $detail") + $failure.Data["HttpStatus"] = $status + throw $failure + } +} +function Fetch-Request([string] $uri, [string] $destination, [switch] $progress) { if (-not $progress -or -not $interactive) { $ProgressPreference = if ($progress -and $terminal) { "Continue" } else { "SilentlyContinue" } Invoke-WebRequest -Uri $uri -OutFile $destination -UseBasicParsing @@ -181,7 +195,7 @@ if ((Test-Path $marker) -and ((Get-Content $marker -Raw).Trim() -eq $version)) { try { Fetch (Release-Url "SHA256SUMS") (Join-Path $staging "SHA256SUMS") } catch { - $status = $_.Exception.Response.StatusCode.value__ + $status = $_.Exception.Data["HttpStatus"] if ($status -eq 404) { Fail "t3 $version has no release archive for win32-$arch; choose a published fork build from https://github.com/$repo/releases" } diff --git a/scripts/install.sh b/scripts/install.sh index 89cc23d60120c..e4e9da59dac99 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -63,18 +63,30 @@ fi step "Finding your release..." # Exit 44 on a 404 so callers can tell "no such asset" from a network failure. +display_url() { + display_path="${1%%\?*}" + display_path="${display_path%%#*}" + case "$display_path" in + *://*@*) + display_rest="${display_path#*://}" + display_authority="${display_rest%%/*}" + printf '%s://%s%s' "${display_path%%://*}" "${display_authority##*@}" "${display_rest#"$display_authority"}" + ;; + *) printf '%s' "$display_path" ;; + esac +} fetch() { if command -v curl >/dev/null 2>&1; then - status="$(curl -sSL -H "Accept: ${3:-*/*}" -w '%{http_code}' "$1" -o "$2")" || return 1 + status="$(curl -sSL -H "Accept: ${3:-*/*}" -w '%{http_code}' "$1" -o "$2" 2>/dev/null)" || { printf 'GET %s failed to connect\n' "$(display_url "$1")" >&2; return 1; } case "$status" in 2??) return 0 ;; 404) return 44 ;; - *) printf 'GET %s returned HTTP %s\n' "$1" "$status" >&2; return 1 ;; + *) printf 'GET %s returned HTTP %s\n' "$(display_url "$1")" "$status" >&2; return 1 ;; esac elif command -v wget >/dev/null 2>&1; then wget -q --server-response --header="Accept: ${3:-*/*}" "$1" -O "$2" 2>"$2.headers" && rm -f "$2.headers" && return 0 if grep -q ' 404 ' "$2.headers" 2>/dev/null; then rm -f "$2.headers"; return 44; fi - cat "$2.headers" >&2; rm -f "$2.headers"; return 1 + printf 'GET %s failed\n' "$(display_url "$1")" >&2; rm -f "$2.headers"; return 1 else fail "curl or wget is required" fi @@ -127,7 +139,7 @@ download() { download_pid= if [ "$result" -ne 0 ]; then printf '\n' >&2 - if [ -f "$2.errors" ]; then cat "$2.errors" >&2; else cat "$2.headers" >&2; fi + printf 'GET %s failed (downloader exit %s)\n' "$(display_url "$1")" "$result" >&2 return "$result" fi size="$(mb "$(wc -c < "$2")")" diff --git a/scripts/install.test.ts b/scripts/install.test.ts index 10a067b1b5f55..b67d19d00ec33 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -264,3 +264,66 @@ describe.skipIf(HostProcess.Platform.defaultValue() === "win32")("installer chec }, ); }); + +describe.skipIf(HostProcess.Platform.defaultValue() === "win32")( + "installer download diagnostics", + () => { + it.each([false, true])( + "redacts signed mirror URLs on transport failure %s", + async (transportFailure) => { + const source = await NodeFSP.readFile(new URL("./install.sh", import.meta.url), "utf8"); + const functions = source.slice( + source.indexOf("display_url() {"), + source.indexOf("\nmb() {"), + ); + const root = await NodeFSP.mkdtemp( + NodePath.join(NodeOS.tmpdir(), "t3-install-diagnostic-"), + ); + try { + const request = + "https://mirror-user:mirror-pass@mirror.example/v1.2.3/SHA256SUMS?signature=test-secret#download"; + const child = NodeChildProcess.spawnSync( + "sh", + [ + "-c", + ` + curl() { + printf '%s\\n' "$@" > "$REQUEST_FILE" + printf '%s\\n' "$*" >&2 + printf '403' + return ${transportFailure ? 7 : 0} + } + ${functions} + fetch "$REQUEST_URL" "$DESTINATION" + `, + ], + { + encoding: "utf8", + env: { + ...process.env, + REQUEST_URL: request, + REQUEST_FILE: NodePath.join(root, "request"), + DESTINATION: NodePath.join(root, "asset"), + }, + }, + ); + expect(child.status).toBe(1); + expect(await NodeFSP.readFile(NodePath.join(root, "request"), "utf8")).toContain(request); + expect(child.stderr).toContain("https://mirror.example/v1.2.3/SHA256SUMS"); + expect(child.stderr).toContain(transportFailure ? "failed to connect" : "HTTP 403"); + for (const secret of [ + "mirror-user", + "mirror-pass", + "signature", + "test-secret", + "#download", + ]) { + expect(child.stdout + child.stderr).not.toContain(secret); + } + } finally { + await NodeFSP.rm(root, { recursive: true, force: true }); + } + }, + ); + }, +); diff --git a/scripts/npm-launcher/t3.mjs b/scripts/npm-launcher/t3.mjs index bdf048bafbda3..1d730f8ef3e39 100644 --- a/scripts/npm-launcher/t3.mjs +++ b/scripts/npm-launcher/t3.mjs @@ -21,13 +21,23 @@ function t3Home() { } async function get(url) { - const response = await fetch(url); + const display = new URL(url); + display.username = ""; + display.password = ""; + display.search = ""; + display.hash = ""; + let response; + try { + response = await fetch(url); + } catch { + throw new Error(`GET ${display.href} failed to connect`); + } if (response.status === 404) { throw new Error( - `${url} does not exist yet. If ${version} was just released, try again in a few minutes.`, + `${display.href} does not exist yet. If ${version} was just released, try again in a few minutes.`, ); } - if (!response.ok) throw new Error(`GET ${url} returned HTTP ${response.status}`); + if (!response.ok) throw new Error(`GET ${display.href} returned HTTP ${response.status}`); return response; }