#!/bin/sh # daybox installer — the `curl daybox.dev/install.sh | sh` bootstrap. # # TEMPLATE. scripts/cut.sh stamps DAYBOX_RELEASE and DAYBOX_SUMS_SHA256 # below and writes the result to dist/install.sh. Upload THAT to R2, never # this file: unstamped, it refuses to run. # # Installs the prebuilt `daybox` CLI onto a laptop that has NO repo checkout # (unlike the repo-root install.sh, which wires an existing clone into a # machine). Detects OS/arch, downloads the matching binary, verifies it, and # drops it on PATH. Then you run `daybox init`. # # ------------------------------------------------------------- integrity ---- # Two anchors, both pinned in THIS file, which is served over TLS from a # domain we control. Neither depends on the host the artifacts came from, so # the backing store stays swappable (R2 today, anything later). # # 1. DAYBOX_SUMS_SHA256 — the sha256 of this release's SHA256SUMS. Checked # with sha256sum/shasum/openssl, all of which are already everywhere. # This is the MANDATORY anchor, and it is what lets the one-liner work # on a bare machine with nothing installed. # 2. DAYBOX_MINISIGN_PUBKEY — the release signing key. Checked only when a # verifier happens to be present, as defense in depth. # # Why the hash is primary: requiring minisign at bootstrap meant a stranger on # a fresh machine got a refusal instead of a daybox. The hash is equally strong # against a hostile artifact store (an attacker who owns the store still cannot # make a forged SHA256SUMS hash to a value pinned here) and it additionally # stops ROLLBACK — an old but validly-signed release dropped at /dl/latest/ is # refused, which a signature alone cannot do. # # Installing a version this file does NOT attest (DAYBOX_VERSION=...) has no # pinned hash to check, so there the signature becomes mandatory and minisign # is required. That is the honest trade, stated at the point of use. # # Integrity files are fetched from DAYBOX_VERIFY_URL, separate from the # artifact base DAYBOX_URL, so repointing the store never moves the anchor. # # --------------------------------------------------------------- layout ----- # /dl/v0.1.0/daybox-darwin-arm64 /dl/v0.1.0/SHA256SUMS # /dl/v0.1.0/daybox-darwin-amd64 /dl/v0.1.0/SHA256SUMS.minisig # /dl/v0.1.0/daybox-linux-amd64 /dl/v0.1.0/daybox-controlplane.tar.gz # /dl/v0.1.0/daybox-linux-arm64 # /dl/latest/... same set; the current release # # --------------------------------------------------------------- deploy ----- # On the trusted laptop (never CI — SECURITY.md, "no automated release # pipeline, ever"): # scripts/release.sh v0.1.0 # cut + sign + upload to R2 + verify, one command # (scripts/cut.sh v0.1.0 builds the artifacts alone — offline, reproducible; # release.sh calls it, then signs SHA256SUMS with the release key whose # secret half never leaves the laptop, and publishes under /dl// # + /dl/latest/ plus this installer at site/install.sh.) # # NB: install.sh is deliberately NOT listed in SHA256SUMS — it pins that # file's hash, so including it would be circular. # # Hosting (as deployed 2026-07-21): artifacts in the Cloudflare R2 bucket # `daybox-releases`, fronted by a Worker on daybox.dev, which also serves this # installer at daybox.dev/install.sh from R2 key `site/install.sh`. Pages was # the plan but its direct-upload API needs wrangler's blake3 hashing (npm), # which the supply-chain rule forbids. The control plane NEVER serves # artifacts. # # Env knobs: DAYBOX_URL (artifact base), DAYBOX_VERIFY_URL (integrity base — # keep on a domain you control), DAYBOX_VERSION (defaults to the attested # release), DAYBOX_BIN_DIR (default ~/.local/bin), DAYBOX_AUTH (user:pass if # the edge gates a private beta). set -eu # --- stamped by scripts/cut.sh --- DAYBOX_RELEASE="v0.5.1" DAYBOX_SUMS_SHA256="6a32e0d034d8ade995f11cb895223b9590100cbbb7bc1d5257363d901fb22caa" # --- pinned by hand; stable across releases --- DAYBOX_MINISIGN_PUBKEY="RWSIiu1rtvgQzS1cqko1+oQxjHyw07jZqyzaid/zVPFIzxKyQ+rkz0/2" DAYBOX_URL="${DAYBOX_URL:-https://daybox.dev/dl}" DAYBOX_VERIFY_URL="${DAYBOX_VERIFY_URL:-https://daybox.dev/dl}" DAYBOX_BIN_DIR="${DAYBOX_BIN_DIR:-$HOME/.local/bin}" DAYBOX_AUTH="${DAYBOX_AUTH:-}" say() { printf ' %s\n' "$*"; } note() { printf ' · %s\n' "$*"; } die() { printf 'daybox install: %s\n' "$*" >&2; exit 1; } have() { command -v "$1" >/dev/null 2>&1; } # Publish-time guards: an installer that was never stamped, or was published # without a signing key, must not run at all. case "$DAYBOX_RELEASE" in ""|__DAYBOX_*) die "this installer was published unstamped — refusing to run. It must be generated by scripts/cut.sh (which fills in the release and its SHA256SUMS hash), not uploaded from the repo template. Report it." ;; esac case "$DAYBOX_SUMS_SHA256" in ""|__DAYBOX_*) die "this installer has no pinned release checksum — refusing to run. Generate it with scripts/cut.sh. Report it; do not work around it." ;; esac [ -n "$DAYBOX_MINISIGN_PUBKEY" ] || die \ "no signing key pinned in this installer — refusing to run. Report it; do not work around it." detect_os() { case "$(uname -s)" in Linux) echo linux ;; Darwin) echo darwin ;; *) die "unsupported OS '$(uname -s)' (linux and darwin only)" ;; esac } detect_arch() { case "$(uname -m)" in x86_64|amd64) echo amd64 ;; arm64|aarch64) echo arm64 ;; *) die "unsupported arch '$(uname -m)' (amd64 and arm64 only)" ;; esac } # fetch URL OUT [optional] — curl (preferred) or wget. Credentials go through # a mode-600 curl config so they never appear on argv / in `ps`. fetch() { url=$1; out=$2; optional=${3:-} if have curl; then set -- -fsSL --retry 2 -o "$out" "$url" [ -n "$CURL_CFG" ] && set -- --config "$CURL_CFG" "$@" curl "$@" 2>/dev/null && return 0 elif have wget; then auth="" [ -n "$DAYBOX_AUTH" ] && auth="--user=${DAYBOX_AUTH%%:*} --password=${DAYBOX_AUTH#*:}" # shellcheck disable=SC2086 wget $auth -qO "$out" "$url" 2>/dev/null && return 0 else die "need curl or wget to download" fi [ -n "$optional" ] && return 1 die "download failed: $url" } sha256_of() { if have sha256sum; then sha256sum "$1" | awk '{print $1}' elif have shasum; then shasum -a 256 "$1" | awk '{print $1}' elif have openssl; then openssl dgst -sha256 "$1" | awk '{print $NF}' else die "no sha256 tool (need sha256sum, shasum, or openssl)" fi } # The mandatory anchor: SHA256SUMS must hash to the value pinned above. verify_sums_pin() { sums=$1 actual=$(sha256_of "$sums") [ "$actual" = "$DAYBOX_SUMS_SHA256" ] || die \ "RELEASE CHECKSUM MISMATCH — refusing to install expected (pinned in this installer): $DAYBOX_SUMS_SHA256 actual (fetched from the store): $actual Either the artifact store is serving something this installer does not attest, or this installer is stale. Do not work around this." say "release pin OK" } # Defense in depth when a verifier exists; mandatory when installing a version # this installer does not attest. verify_signature() { sums=$1; sig=$2; required=$3 if ! have minisign; then if [ -n "$required" ]; then die "installing an unattested version requires a signature check. This installer pins the checksum for $DAYBOX_RELEASE only, so verifying another version needs minisign: macOS: brew install minisign Debian: apt install minisign Or install the attested release by omitting DAYBOX_VERSION." fi note "minisign not installed — release pin already verified; skipping signature" return 0 fi if [ ! -f "$sig" ]; then [ -n "$required" ] && die "SHA256SUMS.minisig missing — refusing to install" note "no signature published for this release; release pin already verified" return 0 fi minisign -Vm "$sums" -P "$DAYBOX_MINISIGN_PUBKEY" >/dev/null 2>&1 \ || die "SIGNATURE VERIFICATION FAILED — refusing to install" say "signature OK" } verify_checksum() { file=$1; name=$2; sums=$3 expected=$(awk -v f="$name" '{n=$2; sub(/^\*/,"",n); if(n==f) print $1}' "$sums") [ -n "$expected" ] || die "no checksum for $name in SHA256SUMS" actual=$(sha256_of "$file") [ "$expected" = "$actual" ] || die \ "CHECKSUM MISMATCH — refusing to install expected: $expected actual: $actual" say "checksum OK" } install_bin() { src=$1; dst=$2 mkdir -p "$(dirname "$dst")" if have install; then install -m 755 "$src" "$dst" else cp "$src" "$dst" && chmod 755 "$dst"; fi say "installed $dst" } post_install() { dir=$1 case ":${PATH}:" in *":$dir:"*) : ;; *) printf ' ! %s is not on PATH — add it, e.g.:\n' "$dir" >&2 printf ' ! echo '"'"'export PATH="%s:$PATH"'"'"' >> ~/.profile\n' "$dir" >&2 ;; esac cat <<'EOF' Done. Next: daybox init # interviews you; provisions the control plane from a # cloud token and enrolls this device daybox up # summon a box (auto-reaps when idle) EOF } main() { os=$(detect_os) arch=$(detect_arch) asset="daybox-${os}-${arch}" ver="${DAYBOX_VERSION:-$DAYBOX_RELEASE}" art_base="${DAYBOX_URL%/}/$ver" ver_base="${DAYBOX_VERIFY_URL%/}/$ver" tmp=$(mktemp -d "${TMPDIR:-/tmp}/daybox.XXXXXX") trap 'rm -rf "$tmp"' EXIT INT TERM CURL_CFG="" if [ -n "$DAYBOX_AUTH" ]; then CURL_CFG="$tmp/curlrc" ( umask 077; printf 'user = "%s"\n' "$DAYBOX_AUTH" > "$CURL_CFG" ) fi say "installing $asset ($ver)" say " artifact: $art_base" say " integrity: $ver_base" fetch "$art_base/$asset" "$tmp/$asset" fetch "$ver_base/SHA256SUMS" "$tmp/SHA256SUMS" fetch "$ver_base/SHA256SUMS.minisig" "$tmp/SHA256SUMS.minisig" optional || true if [ "$ver" = "$DAYBOX_RELEASE" ]; then verify_sums_pin "$tmp/SHA256SUMS" verify_signature "$tmp/SHA256SUMS" "$tmp/SHA256SUMS.minisig" "" else note "$ver is not the release this installer attests ($DAYBOX_RELEASE)" verify_signature "$tmp/SHA256SUMS" "$tmp/SHA256SUMS.minisig" required fi verify_checksum "$tmp/$asset" "$asset" "$tmp/SHA256SUMS" install_bin "$tmp/$asset" "$DAYBOX_BIN_DIR/daybox" post_install "$DAYBOX_BIN_DIR" } main "$@"