install.sh
Rama main · réplica de lectura del repositorio original.
#!/bin/sh
# URLCode installer. Downloads a published release tarball, verifies its
# SHA-256 against a same-origin SHA256SUMS file (protects against a corrupted
# or truncated download, not against a compromised or substituted origin),
# and installs the CLI with npm. Every release is separately attested by
# GitHub. Pass --verify-attestation to check that signed provenance during
# install with the `gh` CLI, or run `gh attestation verify` yourself
# afterwards; this is opt-in because it needs a network call to GitHub and an
# authenticated `gh`, neither of which this script otherwise requires.
#
# curl -fsSL https://raw.githubusercontent.com/jimhoyd-com/urlcode/main/install.sh | sh
# ... | sh -s -- --version 0.3.0 --prefix "$HOME/.local" # pin a release; omit --version for the latest
#
# This script never runs project code and never needs root for a --prefix install.
# Everything below runs inside main() at the bottom of the file, so a
# truncated `curl | sh` download (a partial script body) fails to call main
# and does nothing, rather than executing partial top-level statements.
set -eu
REPO=jimhoyd-com/urlcode
VERSION=""
PREFIX=""
VERIFY_ATTESTATION=""
MIN_NODE_MAJOR=22
MIN_NODE_MINOR=13
usage() {
cat <<'USAGE'
Usage: install.sh [--version X.Y.Z] [--prefix DIR] [--verify-attestation]
--version Release to install. Defaults to the latest published release.
--prefix Install into DIR/lib/node_modules and link DIR/bin/urlcode.
Defaults to the npm global prefix, which may require privileges.
--verify-attestation Also verify the release's signed GitHub attestation with
`gh attestation verify` before installing. Requires an
authenticated `gh` and a network call to GitHub.
--help Show this message.
USAGE
}
# Everything the installer does lives in main(), called with the script's
# original arguments at the bottom of this file. A `curl | sh` pipe that is
# truncated mid-download yields a body with no trailing `main "$@"` call (or
# a syntactically incomplete one), so the shell never executes a partial
# install — unlike top-level statements, which run as they are parsed.
main() {
while [ $# -gt 0 ]; do
case "$1" in
--version) [ $# -ge 2 ] || { echo "install: --version needs a value" >&2; exit 2; }; VERSION="$2"; shift 2 ;;
--prefix) [ $# -ge 2 ] || { echo "install: --prefix needs a value" >&2; exit 2; }; PREFIX="$2"; shift 2 ;;
--verify-attestation) VERIFY_ATTESTATION=1; shift ;;
--help|-h) usage; exit 0 ;;
*) echo "install: unknown option $1" >&2; usage >&2; exit 2 ;;
esac
done
version_is_safe() { case "$1" in *[!0-9A-Za-z.+-]*|"") return 1 ;; *) return 0 ;; esac; }
if [ -n "$VERSION" ] && ! version_is_safe "$VERSION"; then
echo "install: refusing suspicious version '$VERSION'" >&2; exit 2
fi
need() { command -v "$1" >/dev/null 2>&1 || { echo "install: $1 is required" >&2; exit 1; }; }
need node
need npm
# Prefer a real download tool, but fall back to node, which is already required.
# Slim container images ship neither curl nor wget, and that must not stop an install.
# Transport is pinned to the URL's own scheme: https gets a minimum TLS
# version and a locked-down protocol list, and the loopback/file exceptions
# carved out below (for mirrors and this repository's own installer test)
# still only ever speak the scheme they were validated for.
if command -v curl >/dev/null 2>&1; then
fetch() {
case "$1" in
https://*) curl -fsSL --proto '=https' --tlsv1.2 "$1" -o "$2" ;;
*) curl -fsSL --proto '=http,file' "$1" -o "$2" ;;
esac
}
elif command -v wget >/dev/null 2>&1; then
fetch() {
case "$1" in
https://*) wget -qO "$2" --https-only "$1" ;;
*) wget -qO "$2" "$1" ;;
esac
}
else fetch() {
node -e '
const {writeFileSync} = require("node:fs");
fetch(process.argv[1])
.then(r => r.ok ? r.arrayBuffer() : Promise.reject(new Error("HTTP " + r.status)))
.then(body => writeFileSync(process.argv[2], Buffer.from(body)))
.catch(e => { console.error("install: download failed: " + e.message); process.exit(1); });
' "$1" "$2"
}
fi
node_version=$(node -p 'process.versions.node')
node_major=${node_version%%.*}
node_rest=${node_version#*.}
node_minor=${node_rest%%.*}
if [ "$node_major" -lt "$MIN_NODE_MAJOR" ] || { [ "$node_major" -eq "$MIN_NODE_MAJOR" ] && [ "$node_minor" -lt "$MIN_NODE_MINOR" ]; }; then
echo "install: Node $MIN_NODE_MAJOR.$MIN_NODE_MINOR or newer is required; found $node_version" >&2
exit 1
fi
if [ -z "$VERSION" ]; then
VERSION=$(node -e "
const url='https://api.github.com/repos/$REPO/releases/latest';
fetch(url,{headers:{'user-agent':'urlcode-install'}})
.then(r=>r.ok?r.json():Promise.reject(new Error('HTTP '+r.status)))
.then(r=>{const t=String(r.tag_name||'');if(!/^v\d+\.\d+\.\d+/.test(t))throw new Error('unexpected tag '+t);process.stdout.write(t.slice(1));})
.catch(e=>{console.error('install: cannot resolve latest release: '+e.message);process.exit(1);});
")
fi
# Re-check after resolution: a released tag is input too.
version_is_safe "$VERSION" || { echo "install: refusing suspicious version '$VERSION'" >&2; exit 2; }
# The release asset is npm's packed name: a scope becomes a leading segment
# joined by a dash. test/release.test.ts holds this to package.json.
TARBALL="jimhoyd-urlcode-$VERSION.tgz"
# URLCODE_DOWNLOAD_BASE serves mirrors and this repository's own installer test.
# It replaces both the tarball and the SHA256SUMS it is checked against, so a
# base you do not control is a base you are trusting: verify the release's
# signed provenance with `gh attestation verify` (or --verify-attestation)
# after installing. Only https:// bases are trusted in general; loopback
# http:// and file:// are additionally allowed because they cannot be
# intercepted off-host and this script's own tests rely on a loopback server.
# A loopback host must be followed by a port, a path or nothing, so a name
# that merely starts with it (http://localhost.example.tld) is refused.
BASE="${URLCODE_DOWNLOAD_BASE:-https://github.com/$REPO/releases/download/v$VERSION}"
case "$BASE" in
https://*) ;;
http://127.0.0.1|http://127.0.0.1:*|http://127.0.0.1/*) ;;
http://localhost|http://localhost:*|http://localhost/*) ;;
'http://[::1]'|'http://[::1]:'*|'http://[::1]/'*|file://*) ;;
*) echo "install: URLCODE_DOWNLOAD_BASE must be https:// (loopback http:// and file:// are allowed for local testing)" >&2; exit 2 ;;
esac
TMP=$(mktemp -d "${TMPDIR:-/tmp}/urlcode-install.XXXXXX")
trap 'rm -rf "$TMP"' EXIT INT TERM
echo "install: downloading $TARBALL"
fetch "$BASE/$TARBALL" "$TMP/$TARBALL"
fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS"
if command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$TMP/$TARBALL" | cut -d' ' -f1)
elif command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$TMP/$TARBALL" | cut -d' ' -f1)
else actual=$(node -e "const{createHash}=require('node:crypto'),{readFileSync}=require('node:fs');process.stdout.write(createHash('sha256').update(readFileSync(process.argv[1])).digest('hex'))" "$TMP/$TARBALL")
fi
expected=""
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
*" $TARBALL") expected=${line%% *} ;;
esac
done < "$TMP/SHA256SUMS"
if [ -z "$expected" ]; then echo "install: $TARBALL is not listed in SHA256SUMS" >&2; exit 1; fi
if [ "$actual" != "$expected" ]; then
echo "install: checksum mismatch for $TARBALL" >&2
echo " expected $expected" >&2
echo " actual $actual" >&2
exit 1
fi
echo "install: checksum verified (same-origin SHA256SUMS; catches corruption, not a substituted origin)"
if [ -n "$VERIFY_ATTESTATION" ]; then
need gh
echo "install: verifying signed GitHub attestation for $TARBALL"
gh attestation verify "$TMP/$TARBALL" --repo "$REPO"
echo "install: attestation verified"
fi
# --ignore-scripts: a package install must not execute lifecycle code.
# The published tarball pins its direct dependencies exactly but does not
# ship a lockfile, so transitive versions resolve at install time from the
# npm registry rather than from bytes this release fixed; review
# `npm ls --all` after installing if that matters for your environment.
if [ -n "$PREFIX" ]; then
npm install --global --ignore-scripts --no-audit --no-fund --prefix "$PREFIX" "$TMP/$TARBALL"
echo "install: urlcode $VERSION installed; add $PREFIX/bin to PATH if it is not already there"
else
npm install --global --ignore-scripts --no-audit --no-fund "$TMP/$TARBALL"
echo "install: urlcode $VERSION installed"
fi
if [ -z "$VERIFY_ATTESTATION" ]; then
# The downloaded copy is removed on exit, so name where to fetch it again.
echo "install: verify the signed provenance of this release with"
echo " curl -fsSLO $BASE/$TARBALL"
echo " gh attestation verify $TARBALL --repo $REPO"
fi
}
main "$@"