From f45a446e3b9b66a5d94972e00a5a39c50ae03cd4 Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Fri, 4 Sep 2026 09:45:58 -0400 Subject: [PATCH 1/2] fix: Checksum match must allow the ./ prefix real releases carry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installer refuses every install against every published release: error: checksums.txt has no entry for abctl_v0.7.0-alpha.3_darwin_arm64.tar.gz — refusing to install it unverified The entry is there. The release workflow generates checksums with `sha256sum ./*.tar.gz`, so every line reads "HASH ./abctl_....tar.gz", and the pattern I introduced in 79f2f575 — "[[:space:]]\*?NAME$" — requires the name immediately after whitespace or a binary-mode asterisk. A "./" in between means nothing matches. That commit fixed a fail-OPEN (an alternation succeeded on one of two archives, so a partial checksums.txt installed the other unverified) and replaced it with a fail-CLOSED that blocks everyone. The fail-closed is the safer direction of the two, but it is still a bug, and it is worse in practice: nobody can install at all. The pattern now accepts the name preceded by start-of-line, whitespace, "*", or "/", which covers "./name", "dist/name", "*name" and a bare "name". Tested against the real published checksums.txt for v0.7.0-alpha.3 plus six constructed cases: no prefix, binary mode, a nested path, partial coverage (the fail-open this guard exists for — still caught), a decoy where the name appears mid-line, and a suffix impostor "xyzabctl_....tar.gz". All seven behave. End to end, the exact failing command now verifies both archives ("./abctl_...: OK", "./authbridge-proxy_...: OK") and installs both binaries. The lesson worth recording: the first version of this guard was never run against a real checksums.txt, only against fixtures I wrote from the same mistaken assumption about the format. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- authbridge/install.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/authbridge/install.sh b/authbridge/install.sh index d89fcc84..943ee4e7 100755 --- a/authbridge/install.sh +++ b/authbridge/install.sh @@ -276,7 +276,12 @@ info "Verifying checksums..." # checksums.txt can't make verification fail on a file we never fetched. : > "${tmp}/checksums.filtered" for archive in "${abctl_tgz}" "${proxy_tgz}"; do - grep -E "[[:space:]]\*?${archive}\$" "${tmp}/checksums.txt" >> "${tmp}/checksums.filtered" \ + # The name may be preceded by whitespace, sha256sum's binary-mode "*", or a + # path component: the release workflow runs `sha256sum ./*.tar.gz`, so every + # real line reads "HASH ./abctl_....tar.gz". An earlier version of this + # pattern required the name immediately after whitespace or "*", which matched + # nothing against an actual release and refused every install. + grep -E "(^|[[:space:]*/])${archive}\$" "${tmp}/checksums.txt" >> "${tmp}/checksums.filtered" \ || die "checksums.txt has no entry for ${archive} — refusing to install it unverified" done # Both entries present, and exactly the two we asked for. From c4d5ea45ae6e3c6e688fd454a10206d9e23cfe2d Mon Sep 17 00:00:00 2001 From: Hai Huang Date: Fri, 4 Sep 2026 10:09:21 -0400 Subject: [PATCH 2/2] feat: Install from the newest release, not from main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The documented command fetches install.sh from main and runs it. main is whatever landed last, so a `curl | sh` executes unreviewed and unreleased changes on someone's laptop the moment they merge — which is exactly how a broken checksum pattern of mine reached a user and blocked every install. The script now re-runs the copy from the newest release and hands it the same arguments. Releases are tested; main is not. Two escape hatches: --ref=main run this copy, unreleased changes included --ref=vX.Y.Z pin the installer to a release AUTHBRIDGE_REF is the environment equivalent. When the script came from a release tag, the binaries default to that same tag, so the script and the binaries it installs are one tested set rather than two independently-moving things; AUTHBRIDGE_VERSION still overrides. Details that took a test to get right: - --ref is stripped before re-exec. A released script from before --ref existed rejects it as an unknown option, which is exactly what happened on the first run of this. - The argument list is rebuilt by rotating the positional parameters rather than building a string, so an argument containing a space survives. - AUTHBRIDGE_SCRIPT_REF is both the ref name and the recursion guard: the child sees it set and does not bootstrap again. Verified the bootstrap line appears exactly once. - If the resolved ref has no authbridge/install.sh, it warns and continues with the current copy. That is not hypothetical: the newest release today is v0.7.0-alpha.3, which predates the rename from install-demo.sh, so the fallback is the live path until the next release exists. Tested: the default (falls back with a warning today), --ref=main, --ref with a commit SHA that does have the script, AUTHBRIDGE_REF, the recursion guard, argument propagation, version pinning, and --help through the documented pipe. The SHA test is the one worth naming: the parent had the checksum fix and the child did not, and the child failed on the checksum bug — which is direct evidence the re-exec runs the pinned copy's code rather than the parent's. Assisted-By: Claude (Anthropic AI) Signed-off-by: Hai Huang --- README.md | 4 ++ authbridge/install.sh | 96 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 16e14707..47da8d9f 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ decrypted and parsed live. No Kubernetes. macOS or Linux, amd64 or arm64. | sh -s -- --claude-code ``` + The URL is on `main`, but the script immediately re-runs the copy from the + newest **release** — so a `curl | sh` never executes an unreleased change. + Add `--ref=main` to opt into main anyway, or `--ref=vX.Y.Z` to pin. + 2. **Open the viewer** in another terminal: ```sh diff --git a/authbridge/install.sh b/authbridge/install.sh index 943ee4e7..df0c2843 100755 --- a/authbridge/install.sh +++ b/authbridge/install.sh @@ -36,8 +36,15 @@ # curl, not sh, so the script runs without it. `sh -s -- --flag` has no such # failure mode. The env vars below still work. # +# By default this script re-runs the copy from the newest RELEASE rather than +# executing whatever is currently on main — main is unstable by definition, and a +# `curl | sh` should not be the first thing to run a change nobody has released. +# --ref=main opts back in; --ref=vX.Y.Z pins. +# # Environment: -# AUTHBRIDGE_VERSION=vX.Y.Z install a specific release tag (default: newest) +# AUTHBRIDGE_REF=REF same as --ref +# AUTHBRIDGE_VERSION=vX.Y.Z install binaries from a specific release +# (default: the release this script came from) # AUTHBRIDGE_INSTALL_ONLY=1 same as --install-only # AUTHBRIDGE_SKIP_DOWNLOAD=1 use the already-installed binaries in ~/.local/bin # instead of downloading (re-run setup offline) @@ -77,6 +84,10 @@ Options: --claude-code after starting, offer to configure Claude Code to use it, so it runs as plain `claude` with no environment variables --local the default, spelled out + --ref=REF take THIS SCRIPT from a git ref instead of the newest release + (e.g. --ref=main for unreleased changes, --ref=v0.7.0-alpha.4 + to pin). Binaries come from the same release unless + AUTHBRIDGE_VERSION says otherwise. -h, --help this text Environment: @@ -97,6 +108,7 @@ for arg in "$@"; do case "$arg" in --install-only) MODE=install-only ;; --claude-code) WIRE_CLAUDE_CODE=1 ;; + --ref=*) AUTHBRIDGE_REF="${arg#*=}" ;; # --local is the default; accepted so writing it out explicitly works, and # so it mirrors the proxy flag of the same name. --local) MODE=local ;; @@ -104,7 +116,7 @@ for arg in "$@"; do usage exit 0 ;; - *) die "unknown option: $arg (try --claude-code, --install-only, --local, or no argument)" ;; + *) die "unknown option: $arg (try --claude-code, --install-only, --local, --ref=REF, or no argument)" ;; esac done # Env form kept working; the flag wins if both are given. @@ -115,6 +127,70 @@ fi command -v curl >/dev/null 2>&1 || die "curl is required" command -v tar >/dev/null 2>&1 || die "tar is required" +# newest_release prints the newest release tag, prereleases included. +# `releases/latest` excludes prereleases and this project ships them, so list +# releases (newest first) and take the first tag_name. +newest_release() { + curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=1" 2>/dev/null \ + | grep -m1 '"tag_name"' | sed -e 's/.*"tag_name": *"//' -e 's/".*//' +} + +# --- run the released copy of this script, not the one from main --- +# +# The documented command fetches this file from main, which is whatever landed +# last: an unreviewed or half-finished change there runs on someone's laptop +# immediately. Releases are tested, so by default this bootstrap re-runs the copy +# from the newest release and hands it the same arguments. +# +# SCRIPT_REF names the ref this copy came from and doubles as the recursion guard: +# the child sees it set and does not bootstrap again. +SCRIPT_REF="${AUTHBRIDGE_SCRIPT_REF:-}" +if [ -z "${SCRIPT_REF}" ]; then + want_ref="${AUTHBRIDGE_REF:-}" + if [ -z "${want_ref}" ]; then + want_ref="$(newest_release)" || true + fi + if [ -z "${want_ref}" ]; then + warn "could not resolve the newest release; continuing with the copy from main" + SCRIPT_REF="main" + elif [ "${want_ref}" = "main" ]; then + # Explicitly asked for main: this copy already is main. + SCRIPT_REF="main" + else + # Rebuild the argument list without --ref: it is meta, consumed here, and a + # released script from before --ref existed rejects it as an unknown option. + # Rotating the positional parameters keeps arguments with spaces intact, + # which building a string would not. + argc=$# + argi=0 + while [ "${argi}" -lt "${argc}" ]; do + a="$1" + shift + argi=$((argi + 1)) + case "$a" in + --ref=*) ;; + *) set -- "$@" "$a" ;; + esac + done + + boot=$(mktemp) + url="https://raw.githubusercontent.com/${REPO}/${want_ref}/authbridge/install.sh" + if curl -fsSL "${url}" -o "${boot}" 2>/dev/null && [ -s "${boot}" ]; then + info "Using the installer from ${want_ref}." + AUTHBRIDGE_SCRIPT_REF="${want_ref}" sh "${boot}" "$@" + status=$? + rm -f "${boot}" + exit "${status}" + fi + rm -f "${boot}" + # A release from before this script existed under that name, or a network + # blip. Falling back is better than refusing to install, but say which + # copy is running so a surprise is attributable. + warn "${want_ref} has no authbridge/install.sh; continuing with the copy from main" + SCRIPT_REF="main" + fi +fi + # Verify the checklist file passed as $1 (run from the directory holding the # files). shasum is preferred: it's always present on macOS and its -c reads the # GNU-style checksums.txt reliably, whereas some non-GNU sha256sum builds reject @@ -241,14 +317,18 @@ if [ "${AUTHBRIDGE_SKIP_DOWNLOAD:-}" = "1" ]; then else # --- resolve the release tag --- -# `releases/latest` excludes prereleases, and the project ships prereleases, so -# list releases (newest first) and take the first tag_name instead. version="${AUTHBRIDGE_VERSION:-}" if [ -z "$version" ]; then - info "Resolving newest release..." - version=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=1" \ - | grep -m1 '"tag_name"' | sed -e 's/.*"tag_name": *"//' -e 's/".*//') - [ -n "$version" ] || die "could not resolve the newest release (set AUTHBRIDGE_VERSION=vX.Y.Z)" + # Default the binaries to the same release this script came from, so the + # script and the binaries it installs are one tested set rather than two + # independently-moving things. + case "${SCRIPT_REF}" in + v*) version="${SCRIPT_REF}" ;; + *) + info "Resolving newest release..." + version=$(newest_release) || die "could not resolve the newest release (set AUTHBRIDGE_VERSION=vX.Y.Z)" + ;; + esac fi info "Release: $version"