#!/usr/bin/env bash
# =============================================================================
# spawn install bootstrap — fetch, VERIFY, and place a Spawn release.
#
# Served at https://updates.seglamater.app/spawn/install.sh
#
#   curl -fsSL https://updates.seglamater.app/spawn/install.sh -o get-spawn.sh
#   # compare the fingerprint below against the one in your onboarding email
#   bash get-spawn.sh --pubkey-sha256 <fingerprint-from-that-email>
#
# ── WHY THE FINGERPRINT ARGUMENT EXISTS, AND WHY IT IS THE WHOLE POINT ───────
#
# This script, the public key, the manifest, its signature and the tarball are
# ALL served from one origin. An attacker who controls that origin controls all
# five. Signing the tarball does not help: they would serve their own key and
# their own signature, and every check below would pass. Signing a checksum
# instead of the bundle is the same circle drawn differently — a sidecar served
# beside the file it describes attests TRANSPORT, never ORIGIN.
#
# ONLY AN OUT-OF-BAND ANCHOR CLOSES IT. The anchor is the SHA-256 of the Spawn
# release public key, delivered to you by a channel this server cannot write to
# — your onboarding email, a signed document, or a person you called.
#
#   --pubkey-sha256 <hex>   the anchor you received out of band. AUTHORITATIVE.
#   --pubkey-file <path>    a key file you already hold out of band. STRONGER
#                           still: nothing is fetched for the trust root at all.
#
# The value compiled in below is a CONVENIENCE for the ordinary case, not a
# security boundary: it arrived over the same wire as everything else. When you
# supply --pubkey-sha256 the supplied value WINS, and a disagreement between the
# two is a hard refusal rather than a warning — a mismatch there is exactly the
# event this mechanism exists to detect.
#
# ── THE CHAIN, ONCE THE ANCHOR IS TRUSTED ───────────────────────────────────
#
#   anchor (out of band)
#     -> spawn.pub                 fingerprint must equal the anchor
#     -> manifest.json             cosign signature verifies under that key
#     -> artifact sha256           pinned INSIDE the signed manifest
#     -> the tarball bytes         hashed and compared to that pin
#
# Every link after the anchor is origin-independent: a mirror, a USB stick or a
# hostile CDN all produce the same verdict, because nothing in the chain is
# trusted for being where it was found.
#
# Exit: 0 succeeded. 1 a verification refusal. 2 the check could not run
#       (missing tool, unreachable server).
# =============================================================================
set -euo pipefail

# ── TRUST ANCHOR (convenience copy — see the header) ─────────────────────────
# SHA-256 of .well-known/keys/spawn.pub, key published 2026-09-06.
EMBEDDED_PUBKEY_SHA256="ec005e3643f7baac232925a1a42ef31ac0058cb5ddc2844442e3ffc9b7f1bb93"

BASE_URL="https://updates.seglamater.app"
CHANNEL="stable"
ANCHOR=""
PUBKEY_FILE=""
VERIFY_ONLY=0
PLACE_ARGS=()
HIVEMIND_VERSION=""
WORK=""

cleanup() { [ -n "$WORK" ] && rm -rf "$WORK"; return 0; }
trap cleanup EXIT

die()  { printf '\nspawn-get: %s\n' "$*" >&2; exit 1; }
cant() { printf '\nspawn-get: %s\n' "$*" >&2; exit 2; }
say()  { printf '  %s\n' "$*"; }

usage() {
    cat <<'USAGE'
spawn download + verification bootstrap

Usage:
  get-spawn.sh [options]

Trust anchor (pick one; the first is the ordinary case):
  --pubkey-sha256 <hex>   SHA-256 of the Spawn release key, from your onboarding
                          email. AUTHORITATIVE — overrides the copy compiled into
                          this script, and a disagreement is a hard refusal.
  --pubkey-file <path>    A spawn.pub you already hold out of band. Nothing is
                          fetched for the trust root when this is given.

Selection:
  --channel <name>        stable | beta | canary   (default: stable)
  --base-url <url>        Update server. Default https://updates.seglamater.app
                          A non-https base is refused unless
                          SPAWN_INSTALL_ALLOW_INSECURE_BASE=1, and that mode
                          CANNOT place binaries — it is verify-only, for testing
                          a mirror or a local tree.

Mode:
  --verify-only           Resolve and verify the whole chain, then stop.
                          Downloads and hashes the artifact.
  --hivemind-version <v>  Check this release's declared Hivemind compatibility
                          against the version you actually run.
  --                      Everything after this is passed to the placement
                          script inside the tarball (try `-- --help`).
  --help                  This message.
USAGE
}

while [ $# -gt 0 ]; do
    case "$1" in
        --pubkey-sha256)    ANCHOR="${2:?--pubkey-sha256 needs a value}"; shift 2 ;;
        --pubkey-file)      PUBKEY_FILE="${2:?--pubkey-file needs a path}"; shift 2 ;;
        --channel)          CHANNEL="${2:?--channel needs a value}"; shift 2 ;;
        --base-url)         BASE_URL="${2:?--base-url needs a value}"; shift 2 ;;
        --hivemind-version) HIVEMIND_VERSION="${2:?--hivemind-version needs a value}"; shift 2 ;;
        --verify-only)      VERIFY_ONLY=1; shift ;;
        --help|-h)          usage; exit 0 ;;
        --)                 shift; PLACE_ARGS=("$@"); break ;;
        *)                  die "unknown option: $1 (try --help)" ;;
    esac
done

BASE_URL="${BASE_URL%/}"

# ── A NON-HTTPS BASE MAY VERIFY BUT MAY NEVER PLACE BINARIES ─────────────────
# The escape hatch is deliberately NARROWER than the action it guards: an
# insecure base is useful for testing a mirror or a local tree, and there is no
# case where it should put binaries on a machine. So it downgrades the run to
# verify-only rather than unlocking the placement step.
case "$BASE_URL" in
    https://*) ;;
    *)  if [ "${SPAWN_INSTALL_ALLOW_INSECURE_BASE:-0}" != "1" ]; then
            die "refusing a non-https base URL: $BASE_URL
  Set SPAWN_INSTALL_ALLOW_INSECURE_BASE=1 to verify against it anyway. That
  mode cannot place binaries; it only checks the chain."
        fi
        printf '\n!! INSECURE BASE URL — verification only, nothing will be placed.\n\n' >&2
        VERIFY_ONLY=1
        ;;
esac

# ── PREREQUISITES ────────────────────────────────────────────────────────────
# Named once, up front. A tool discovered missing 200 lines in reports the
# symptom rather than the cause, and the estate has paid for that shape before.
MISSING=()
for tool in curl sha256sum python3 tar cosign; do
    command -v "$tool" >/dev/null 2>&1 || MISSING+=("$tool")
done
# zstd may be built into tar; probe the capability rather than the binary.
if ! tar --help 2>/dev/null | grep -q -- '--zstd'; then
    command -v zstd >/dev/null 2>&1 || MISSING+=("zstd")
fi
if [ ${#MISSING[@]} -gt 0 ]; then
    cant "missing required tool(s): ${MISSING[*]}
  cosign is NOT optional and there is no flag to skip it. Without a signature
  check this script would be a download command wearing a verification costume,
  and the origin-trust problem it exists to solve would be wide open.
  cosign: https://docs.sigstore.dev/cosign/system_config/installation/"
fi

WORK="$(mktemp -d)"

fetch() { # fetch <url> <dest>
    curl -fsSL --max-time 60 "$1" -o "$2" || cant "could not fetch $1"
}

# ── STEP 1: ESTABLISH THE TRUST ROOT ─────────────────────────────────────────
PUB="$WORK/spawn.pub"
if [ -n "$PUBKEY_FILE" ]; then
    [ -r "$PUBKEY_FILE" ] || cant "--pubkey-file is not readable: $PUBKEY_FILE"
    cp "$PUBKEY_FILE" "$PUB"
    say "trust root: your own key file ($PUBKEY_FILE) — nothing fetched."
    if [ -n "$ANCHOR" ]; then
        got="$(sha256sum "$PUB" | awk '{print $1}')"
        [ "$got" = "$ANCHOR" ] || die "the key file does not match --pubkey-sha256.
  file:   $got
  anchor: $ANCHOR"
        say "trust root: file fingerprint matches your out-of-band anchor."
    fi
else
    fetch "$BASE_URL/.well-known/keys/spawn.pub" "$PUB"
    got="$(sha256sum "$PUB" | awk '{print $1}')"

    if [ -n "$ANCHOR" ]; then
        # The supplied anchor is authoritative. It is also cross-checked against
        # the compiled-in copy, because a disagreement means one of the two
        # channels is lying and neither result should be used quietly.
        if [ "$got" != "$ANCHOR" ]; then
            die "TRUST ANCHOR MISMATCH — refusing.
  fetched key:      $got
  your anchor:      $ANCHOR
  The key this server served is not the key you were told to expect. Do not
  proceed. Contact support@seglamater.com over a channel you already trust."
        fi
        if [ "$ANCHOR" != "$EMBEDDED_PUBKEY_SHA256" ]; then
            printf '\n!! Your out-of-band anchor differs from the value compiled into this\n' >&2
            printf '   script. The fetched key matches YOURS, so this script may be an old\n' >&2
            printf '   copy — or it may not be ours. Continuing on your anchor, which is the\n' >&2
            printf '   authoritative one, but confirm with support before relying on it.\n\n' >&2
        fi
        say "trust root: key fingerprint matches your out-of-band anchor."
    else
        [ "$got" = "$EMBEDDED_PUBKEY_SHA256" ] || die "public key fingerprint mismatch.
  fetched: $got
  pinned:  $EMBEDDED_PUBKEY_SHA256"
        printf '\n!! NO OUT-OF-BAND ANCHOR SUPPLIED.\n' >&2
        printf '   The key was checked against a fingerprint that arrived from the SAME\n' >&2
        printf '   server as the key. That detects corruption and a stale mirror. It does\n' >&2
        printf '   NOT detect a compromised origin, because an attacker who serves the key\n' >&2
        printf '   also serves this script.\n' >&2
        printf '   Re-run with --pubkey-sha256 <fingerprint from your onboarding email> to\n' >&2
        printf '   close that gap. Expected value:\n' >&2
        printf '     %s\n\n' "$EMBEDDED_PUBKEY_SHA256"
    fi
fi

# ── STEP 2: RESOLVE THE CHANNEL POINTER ──────────────────────────────────────
POINTER="$WORK/latest.json"
fetch "$BASE_URL/spawn/channels/$CHANNEL/latest.json" "$POINTER"

MANIFEST_URL="$(python3 - "$POINTER" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
if d.get("product") != "spawn":
    sys.exit("channel pointer is not a spawn pointer: product=%r" % d.get("product"))
u = d.get("manifest_url")
if not isinstance(u, str) or not u:
    sys.exit("channel pointer has no manifest_url")
print(u)
PY
)" || die "channel pointer is unusable"

# The pointer's manifest_url is absolute and names the canonical origin. When
# checking a MIRROR the pointer is the mirror's, so keep every fetch on the base
# the caller named and take only the PATH from the pointer. One origin per run,
# named by the caller, never silently redirected by a file we have not verified.
MANIFEST_PATH="/${MANIFEST_URL#*://*/}"
MANIFEST_DIR="$(dirname "$MANIFEST_PATH")"
say "channel $CHANNEL -> $MANIFEST_PATH"

# ── STEP 3: FETCH THE MANIFEST AND ITS DETACHED SIGNATURE ────────────────────
MANIFEST="$WORK/manifest.json"
SIG="$WORK/manifest.json.sig"
fetch "$BASE_URL$MANIFEST_PATH" "$MANIFEST"
fetch "$BASE_URL$MANIFEST_PATH.sig" "$SIG"

# ── STEP 4: VERIFY THE SIGNATURE BEFORE READING ONE FIELD ────────────────────
# Order matters. Parsing first and verifying later means every decision taken in
# between was taken on unauthenticated bytes.
#
# --insecure-ignore-tlog: Spawn manifests are signed with --tlog-upload=false, so
# there is no transparency-log entry to look up and requiring one would fail every
# legitimate release. The trust root here is the pinned key, not the log.
#
# --bundle, NOT --signature. The .sig we publish is a sigstore BUNDLE (JSON,
# application/vnd.dev.sigstore.bundle.v0.3+json) — the cosign v3 default — not a bare
# base64 signature, whatever the extension suggests. `--signature` expects the bare
# form and dies on a bundle with `ecdsa: Invalid IEEE_P1363 encoded bytes`.
#
# THIS SAID --signature UNTIL 2026-09-10, WHICH MADE EVERY RELEASE UNINSTALLABLE.
# Measured against the published 0.1.3 artifact: --signature fails, --bundle returns
# Verified OK on the identical files. And it did not fail quietly — it fell into the
# branch below, which tells the customer "The manifest was not signed by the key you
# anchored. Do not proceed." A correctly signed release was therefore reported to the
# customer as evidence of tampering, which is the worst possible direction for this
# specific check to be wrong in.
#
# It survived because the only test that reads this file, test-manifest-consumer.sh,
# extracts the MANIFEST PARSER block by its MF_VERSION marker and exercises that. The
# parser was always correct. Nothing exercised this verify call, so a 9/9 green run
# sat directly above a broken signature check.
if ! cosign verify-blob \
        --key "$PUB" \
        --bundle "$SIG" \
        --insecure-ignore-tlog=true \
        "$MANIFEST" >/dev/null 2>"$WORK/cosign.err"; then
    printf '\n' >&2
    sed 's/^/  cosign: /' "$WORK/cosign.err" >&2
    die "SIGNATURE VERIFICATION FAILED for $BASE_URL$MANIFEST_PATH
  The manifest was not signed by the key you anchored. Do not proceed."
fi
say "manifest signature verifies under the anchored key."

# ── STEP 5: READ THE VERIFIED MANIFEST ───────────────────────────────────────
# Everything from here is authentic, so parsing is safe. The eval below is
# confined to values this script produced from JSON that has already been
# signature-checked, each one shlex-quoted by the producer.
eval "$(python3 - "$MANIFEST" "$HIVEMIND_VERSION" <<'PY'
import json, re, shlex, sys

d = json.load(open(sys.argv[1]))
want_hive = sys.argv[2]

def fail(msg):
    print("MF_ERROR=%s" % shlex.quote(msg)); sys.exit(0)

if d.get("product") != "spawn" or d.get("app") != "spawn":
    fail("manifest is not a spawn manifest (product=%r app=%r)"
         % (d.get("product"), d.get("app")))

version = d.get("version") or ""
art = d.get("artifact")
if not art or version == "0.0.0":
    fail("channel currently advertises a PLACEHOLDER (version %s). No Spawn "
         "release has been published to it yet; there is nothing to fetch."
         % (version or "?"))

name = art.get("name") or ""
sha  = (art.get("sha256") or "").lower()
if not re.fullmatch(r"[0-9a-f]{64}", sha):
    fail("artifact sha256 is not 64 hex characters: %r" % sha)
if "/" in name or name in ("", ".", ".."):
    fail("artifact name is not a plain filename: %r" % name)

warn = ""
req = ((d.get("compatibility") or {}).get("requires") or {}).get("hivemind") or {}
hmin, hmax = req.get("min"), req.get("max_tested")
if hmin and want_hive:
    key = lambda v: [int(x) for x in re.findall(r"\d+", v)[:3]]
    if key(want_hive) < key(hmin):
        fail("spawn %s requires Hivemind >= %s and you named %s. Taking it would "
             "give you a Spawn your server cannot talk to." % (version, hmin, want_hive))
    if hmax and key(want_hive) > key(hmax):
        warn = ("Hivemind %s is NEWER than the %s this Spawn release was tested "
                "against. Supported, not proven." % (want_hive, hmax))

print("MF_VERSION=%s" % shlex.quote(version))
print("MF_NAME=%s"    % shlex.quote(name))
print("MF_SHA=%s"     % shlex.quote(sha))
print("MF_HMIN=%s"    % shlex.quote(hmin or ""))
print("MF_HMAX=%s"    % shlex.quote(hmax or ""))
print("MF_WARN=%s"    % shlex.quote(warn))
PY
)"

[ -n "${MF_ERROR:-}" ] && die "$MF_ERROR"
[ -n "${MF_WARN:-}" ] && printf '\n!! %s\n\n' "$MF_WARN" >&2

say "release: spawn $MF_VERSION ($MF_NAME)"
[ -n "${MF_HMIN:-}" ] && say "declares Hivemind compatibility: >= $MF_HMIN${MF_HMAX:+, tested through $MF_HMAX}"

# ── STEP 6: THE ARTIFACT, FETCHED BESIDE ITS OWN MANIFEST ────────────────────
# Deliberately resolved as <directory of the manifest we just verified>/<name>
# rather than from the manifest's `artifact.url`. The signed manifest pins the
# NAME and the HASH; the location is whichever origin the caller chose. That is
# what makes a mirror, a USB stick and the canonical server interchangeable, and
# it removes a second URL that could disagree with the first.
TARBALL="$WORK/$MF_NAME"
say "fetching $BASE_URL$MANIFEST_DIR/$MF_NAME"
fetch "$BASE_URL$MANIFEST_DIR/$MF_NAME" "$TARBALL"

GOT_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')"
if [ "$GOT_SHA" != "$MF_SHA" ]; then
    die "ARTIFACT HASH MISMATCH — refusing.
  expected (signed manifest): $MF_SHA
  downloaded:                 $GOT_SHA"
fi
say "artifact sha256 matches the signed manifest."

if [ "$VERIFY_ONLY" = "1" ]; then
    printf '\nspawn-get: VERIFIED spawn %s. Stopping as asked.\n' "$MF_VERSION"
    exit 0
fi

# ── STEP 7: HAND OFF TO THE SCRIPT INSIDE THE TARBALL ────────────────────────
# The procedure ships WITH the bytes it places, so instructions cannot drift
# from the artifact they describe. This script's job ends at "these bytes are
# the ones we signed."
EXTRACT="$WORK/x"
mkdir -p "$EXTRACT"
tar -xf "$TARBALL" -C "$EXTRACT"
ROOT="$EXTRACT/spawn-$MF_VERSION"
[ -d "$ROOT" ] || cant "tarball did not contain the expected directory spawn-$MF_VERSION"
[ -f "$ROOT/install.sh" ] || cant "tarball has no install.sh — cannot complete"

printf '\nspawn-get: verified. Running the script that shipped inside the tarball.\n\n'
sh "$ROOT/install.sh" ${PLACE_ARGS+"${PLACE_ARGS[@]}"}
