#!/bin/sh
# Install the kite CLI from a published release.
#
#   curl -fsSL https://get.kiteplane.dev | sh
#
# fleet's channel target copies this file to the root of the releases address
# when edge moves, and get.kiteplane.dev serves it at every path. It runs on a
# person's machine, macOS or Linux, amd64 or arm64, with sh and curl and
# nothing from a checkout. No root: it writes to a directory the person owns.
#
# KITE_CHANNEL names the channel to follow, stable by default. KITE_VERSION
# names one release instead. KITE_INSTALL_DIR is where `kite` goes,
# ~/.local/bin by default. KITE_RELEASES replaces the address, and
# KITE_RELEASE_KEY the key, which a test points at its own.
#
# release.json's signature is checked against the publisher's key with
# OpenSSL 3 or Node.js, whichever is here first, and the CLI against the size
# and SHA-256 the signed manifest gives it, before anything is replaced.
# KITE_INSTALL_VERIFY=openssl or node picks one. Where neither is here, the
# signature goes unchecked and this says so: the installed CLI checks the
# signature of every release it deploys. docs/internal/conventions.md has the
# layout.

set -eu

# The key in packages/cli/src/deploy/build.rs. scripts/install.test.ts fails
# when the two differ, so a new key cannot ship an installer that refuses
# every release.
PUBLISHER_KEY="Jl+3PDBmpcytY/ZDAL/weYz0hpwtIpuiXew6kEwIJNQ="

releases="${KITE_RELEASES:-https://releases.kiteplane.dev}"
channel="${KITE_CHANNEL:-stable}"
version="${KITE_VERSION:-}"
dir="${KITE_INSTALL_DIR:-$HOME/.local/bin}"
key="${KITE_RELEASE_KEY:-$PUBLISHER_KEY}"

fail() {
  printf 'kite install: %s\n' "$*" >&2
  exit 1
}

case "$(uname -s)" in
  Darwin) os=darwin ;;
  Linux) os=linux ;;
  *) fail "no kite CLI is built for $(uname -s). It is built for macOS and Linux." ;;
esac
case "$(uname -m)" in
  x86_64 | amd64) arch=amd64 ;;
  arm64 | aarch64) arch=arm64 ;;
  *) fail "no kite CLI is built for $(uname -m). It is built for amd64 and arm64." ;;
esac
file="kite-$os-$arch"

if [ -z "$version" ]; then
  version="$(curl -fsSL "$releases/channels/$channel")" \
    || fail "no channel '$channel' at $releases. The channels are stable and edge."
  version="$(printf '%s' "$version" | tr -d '[:space:]')"
fi
case "$version" in
  "" | *[!0-9A-Za-z.-]*) fail "'$version' is not a release's version." ;;
esac

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -fsSL "$releases/releases/$version/release.json" -o "$tmp/release.json" \
  || fail "release $version is not published."

# release.json is one line of JSON with two base64 strings.
field() {
  sed -n "s/.*\"$1\":\"\([A-Za-z0-9+/=]*\)\".*/\1/p" "$tmp/release.json"
}
manifest="$(field manifest)"
signature="$(field signature)"
[ -n "$manifest" ] && [ -n "$signature" ] \
  || fail "release $version's release.json is not a signed manifest."
printf '%s' "$manifest" | base64 --decode >"$tmp/manifest" \
  || fail "release $version's manifest is not base64."
printf '%s' "$signature" | base64 --decode >"$tmp/signature" \
  || fail "release $version's signature is not base64."

# An Ed25519 public key as SubjectPublicKeyInfo DER: this prefix, then the
# 32 raw bytes of the key.
{
  printf '\060\052\060\005\006\003\053\145\160\003\041\000'
  printf '%s' "$key" | base64 --decode
} >"$tmp/key.der" || fail "the publisher key is not base64."

# OpenSSL checks Ed25519 from 3.0. The LibreSSL macOS ships cannot.
openssl3() {
  case "$(openssl version 2>/dev/null)" in
    "OpenSSL "[3-9]*) return 0 ;;
  esac
  return 1
}

verifier="${KITE_INSTALL_VERIFY:-}"
if [ -z "$verifier" ]; then
  if openssl3; then
    verifier=openssl
  elif command -v node >/dev/null 2>&1; then
    verifier=node
  else
    verifier=none
  fi
fi
unsigned="release $version's release.json is not signed by kite's publisher. Nothing was installed."
case "$verifier" in
  openssl)
    openssl3 \
      || fail "KITE_INSTALL_VERIFY=openssl needs OpenSSL 3 or later to check Ed25519, and this is $(openssl version 2>/dev/null || echo 'no openssl')."
    openssl pkeyutl -verify -pubin -keyform DER -inkey "$tmp/key.der" -rawin \
      -in "$tmp/manifest" -sigfile "$tmp/signature" >/dev/null 2>&1 || fail "$unsigned"
    ;;
  node)
    node -e '
      const { createPublicKey, verify } = require("node:crypto");
      const { readFileSync } = require("node:fs");
      const [key, manifest, signature] = process.argv.slice(1);
      const pub = createPublicKey({ key: readFileSync(key), format: "der", type: "spki" });
      process.exit(verify(null, readFileSync(manifest), pub, readFileSync(signature)) ? 0 : 1);
    ' "$tmp/key.der" "$tmp/manifest" "$tmp/signature" || fail "$unsigned"
    ;;
  none)
    printf 'kite install: the signature of release %s is not checked: install OpenSSL 3 or Node.js to check it.\n' "$version" >&2
    ;;
  *) fail "KITE_INSTALL_VERIFY is openssl or node, not '$verifier'." ;;
esac

grep -q '"product":"kite"' "$tmp/manifest" || fail "release $version is not a kite release."
grep -q "\"version\":\"$version\"" "$tmp/manifest" \
  || fail "release $version's manifest names another version."
# Each file is a flat object in the manifest's `files`: its name, its SHA-256
# and its size.
entry="$(grep -o '{[^{}]*}' "$tmp/manifest" | grep "\"name\":\"$file\"" || true)"
want="$(printf '%s' "$entry" | sed -n 's/.*"sha256":"\([0-9a-f]*\)".*/\1/p')"
size="$(printf '%s' "$entry" | sed -n 's/.*"size":\([0-9]*\).*/\1/p')"
[ -n "$want" ] && [ -n "$size" ] \
  || fail "release $version has no kite CLI for $os-$arch. Set KITE_CHANNEL or KITE_VERSION to install another release."

curl -fsSL "$releases/releases/$version/$file" -o "$tmp/kite" || fail "could not download $file."
if command -v sha256sum >/dev/null 2>&1; then
  got="$(sha256sum "$tmp/kite" | awk '{ print $1 }')"
else
  got="$(shasum -a 256 "$tmp/kite" | awk '{ print $1 }')"
fi
bytes="$(wc -c <"$tmp/kite" | tr -d '[:space:]')"
[ "$got" = "$want" ] && [ "$bytes" = "$size" ] \
  || fail "$file does not match its sum and size in the signed manifest. Nothing was installed."

mkdir -p "$dir"
chmod 755 "$tmp/kite"
# A rename, so a kite running from $dir finishes on the file it started with.
mv -f "$tmp/kite" "$dir/kite"
printf 'installed %s\n' "$("$dir/kite" version)"
case ":$PATH:" in
  *":$dir:"*) ;;
  *) printf 'add %s to PATH to run it as kite\n' "$dir" ;;
esac
