Ctrl K
ComunidadGuías…

install.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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 "$@"