#!/bin/sh # Token Harbor connect — universal installer (macOS / Linux). # curl -fsSL https://tokenharbor.ai/connect.sh | sh # # No prerequisites: if Node >=18 isn't on the machine, a PINNED copy of the # official Node LTS runtime is downloaded into ~/.tokenharbor/node and its # SHA-256 checked against nodejs.org's signed SHASUMS256.txt. Then the CLI is # fetched, its hash checked against the manifest, staged, and swapped into # place atomically. POSIX sh — works in dash/zsh/bash. # # THREE THINGS THIS SCRIPT WILL NOT DO, deliberately: # * run anything it has not hashed first; # * follow a moving "latest" URL — a pinned version is a version somebody # reviewed; # * leave you without a working install if a step fails halfway. The old # copy stays until the new one is verified. set -e BASE="${TOKENHARBOR_BASE:-https://tokenharbor.ai}" TH_DIR="$HOME/.tokenharbor" BIN_DIR="$TH_DIR/bin" CLI_DIR="$TH_DIR/cli" NODE_DIR="$TH_DIR/node" # Pinned. Bumping this is a commit, which is the point: an unpinned # "latest-v24.x" means a bad upstream release reaches every user immediately # and no diff ever showed it. NODE_VERSION="v24.19.0" NODE_DIST="https://nodejs.org/dist/$NODE_VERSION" # Pinned by scripts/installers/build.mjs at release time. These three lines are # the binding: the installer you are reading names the exact artifact and its # hash, so nothing it downloads can be swapped afterwards. # # Fetching a manifest at runtime and trusting whatever hash it carried would # have been no binding at all — the manifest and the artifact come from the # same origin, so anyone able to replace one could replace the other. A hash # only means something when it was fixed before the download. # TH_CLI_PINS_START CLI_VERSION="0.6.1" CLI_FILE="connect-0.6.1.tgz" CLI_SHA256="c1595895338d00019f86344136da6a54f19b29b221bb8707a2102aad7d6aa7fa" # TH_CLI_PINS_END say() { printf '%s\n' "$1"; } die() { printf '%s\n' "$1" >&2; exit 1; } # Staging directories and temp downloads must not survive an interrupt. Ctrl-C # during a 30 MB download used to leave ~/.tokenharbor/node.new behind, and the # next run would then be extracting into a directory that already had content. TH_TMP="" cleanup() { [ -n "$TH_TMP" ] && rm -rf "$TH_TMP" rm -rf "$TH_DIR/node.new" "$TH_DIR/cli.new" } trap cleanup EXIT INT TERM # sha256 is spelled three different ways across the platforms we support. sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' elif command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$1" | awk '{print $NF}' else echo ""; fi } verify_sha256() { file="$1"; want="$2"; what="$3" got="$(sha256_of "$file")" if [ -z "$got" ]; then die "No sha256 tool found (sha256sum / shasum / openssl). Refusing to install $what unverified." fi if [ "$got" != "$want" ]; then die "Checksum mismatch for $what. expected $want got $got Nothing was installed. If this repeats, please contact support@tokenharbor.ai." fi } say '' say 'Token Harbor connect - one command to wire your AI agents.' say '' mkdir -p "$BIN_DIR" # ── 1. Find (or provision) Node >= 18 ─────────────────────────────────────── NODE_BIN="" node_ok() { v="$("$1" -e 'console.log(process.versions.node.split(".")[0])' 2>/dev/null || echo 0)" [ "$v" -ge 18 ] 2>/dev/null } if command -v node >/dev/null 2>&1 && node_ok "$(command -v node)"; then NODE_BIN="$(command -v node)" elif [ -x "$NODE_DIR/bin/node" ] && node_ok "$NODE_DIR/bin/node"; then NODE_BIN="$NODE_DIR/bin/node" else say "Node.js 18+ not found - installing a private copy ($NODE_VERSION, ~30 MB, one time)..." case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; *) die "Unsupported OS: $(uname -s). Install Node 18+ and re-run." ;; esac case "$(uname -m)" in arm64|aarch64) arch=arm64 ;; x86_64|amd64) arch=x64 ;; *) die "Unsupported CPU: $(uname -m). Install Node 18+ and re-run." ;; esac file="node-$NODE_VERSION-$os-$arch.tar.gz" tmp="$(mktemp -d)"; TH_TMP="$tmp" # The checksum file is fetched to be USED, not merely to look up a filename. # The previous version downloaded SHASUMS256.txt, grepped a name out of it # and then never compared anything. curl -fsSL "$NODE_DIST/SHASUMS256.txt" -o "$tmp/SHASUMS256.txt" \ || die "Could not fetch Node checksums. Nothing was installed." want="$(grep " $file\$" "$tmp/SHASUMS256.txt" | awk '{print $1}' | head -1)" [ -n "$want" ] || die "No checksum published for $file. Nothing was installed." curl -fL --progress-bar "$NODE_DIST/$file" -o "$tmp/node.tgz" \ || die "Could not download Node. Nothing was installed." verify_sha256 "$tmp/node.tgz" "$want" "the Node runtime" # Extract to a staging dir and only then replace the old one, so a failure # here leaves the previous working runtime intact. rm -rf "$NODE_DIR.new"; mkdir -p "$NODE_DIR.new" tar -xzf "$tmp/node.tgz" -C "$NODE_DIR.new" --strip-components=1 node_ok "$NODE_DIR.new/bin/node" || { rm -rf "$NODE_DIR.new" "$tmp"; die 'Node runtime install failed. Nothing was changed.'; } # old -> .old, stage -> live, and put .old back if the second move fails. # Two renames cannot be one atomic step, so the recovery has to be written # out rather than assumed. rm -rf "$NODE_DIR.old" moved_aside=0 if [ -d "$NODE_DIR" ]; then mv "$NODE_DIR" "$NODE_DIR.old" && moved_aside=1; fi if ! mv "$NODE_DIR.new" "$NODE_DIR"; then [ "$moved_aside" = 1 ] && mv "$NODE_DIR.old" "$NODE_DIR" rm -rf "$NODE_DIR.new" "$tmp" die 'Could not install the Node runtime. Your previous one is untouched.' fi rm -rf "$NODE_DIR.old" "$tmp" NODE_BIN="$NODE_DIR/bin/node" fi # ── 2. Install the CLI (versioned + hash-bound) ───────────────────────────── say 'Downloading the Token Harbor CLI...' tmp="$(mktemp -d)"; TH_TMP="$tmp" curl -fsSL "$BASE/cli/$CLI_FILE" -o "$tmp/cli.tgz" \ || die "Could not download the CLI. Nothing was installed." verify_sha256 "$tmp/cli.tgz" "$CLI_SHA256" "the Token Harbor CLI" # Stage, verify the entry point exists, THEN swap. An interrupted install used # to leave $CLI_DIR emptied — `rm -rf` ran before the extract — so a failure # took the working copy with it. rm -rf "$CLI_DIR.new"; mkdir -p "$CLI_DIR.new" tar -xzf "$tmp/cli.tgz" -C "$CLI_DIR.new" --strip-components=1 # npm pack root is package/ [ -f "$CLI_DIR.new/src/cli.mjs" ] || { rm -rf "$CLI_DIR.new" "$tmp"; die 'CLI archive looks wrong. Nothing was changed.'; } rm -rf "$CLI_DIR.old" moved_aside=0 if [ -d "$CLI_DIR" ]; then mv "$CLI_DIR" "$CLI_DIR.old" && moved_aside=1; fi if ! mv "$CLI_DIR.new" "$CLI_DIR"; then [ "$moved_aside" = 1 ] && mv "$CLI_DIR.old" "$CLI_DIR" rm -rf "$CLI_DIR.new" "$tmp" die 'Could not install the CLI. Your previous one is untouched.' fi rm -rf "$CLI_DIR.old" "$tmp" say "Installed CLI ${CLI_VERSION:-(unknown version)}." # ── 3. Launcher on PATH ───────────────────────────────────────────────────── cat > "$BIN_DIR/tokenharbor" <> "$prof" } case "${SHELL:-}" in *fish*) mkdir -p "$HOME/.config/fish/conf.d" printf 'set -gx PATH "$HOME/.tokenharbor/bin" $PATH # tokenharbor cli\n' \ > "$HOME/.config/fish/conf.d/tokenharbor-path.fish" ;; *zsh*) add_path_line "$HOME/.zshrc" ;; *) if [ "$(uname -s)" = "Darwin" ]; then add_path_line "$HOME/.bash_profile" else add_path_line "$HOME/.bashrc"; fi ;; esac say '' say "Installed. ($BIN_DIR/tokenharbor)" say '' # ── 4. Run connect now (re-attach the terminal for the key prompt) ────────── if ( : /dev/null; then "$BIN_DIR/tokenharbor" menu