#!/bin/sh # Viko installer for macOS (Apple silicon). # # curl -fsSL https://viko.sh/install | sh -s -- --invite # # What it does, in order: # 1. Downloads the release manifest (https://viko.sh/dl/latest.json) and its signature. # 2. Verifies the manifest's ECDSA P-256 signature against the release key pinned below, using # the openssl that ships with macOS. No valid signature, no install (fail-closed). If # `minisign` is installed, the Ed25519 minisign signature is checked as well. # 3. Refuses expired manifests, manifests older than one this machine already accepted, and # downgrades of an installed vikod. # 4. Downloads vikod, checks its size and SHA-256 against the signed manifest, and installs it # to ~/.viko/bin/vikod (no sudo). # 5. Offers to add ~/.viko/bin to your PATH (asks first; prints the line otherwise). # 6. With --invite, runs `vikod setup --invite ` to pair this Mac with your phone. # # Options: # --invite CODE invite code from your invite link (https://viko.sh/i/CODE) # --hub URL Viko app URL to pair with (default: vikod's own default) # --no-setup install only; do not run `vikod setup` # --add-to-path add ~/.viko/bin to PATH in your shell profile without asking # --no-modify-path never touch shell profiles # -h, --help show this help # # Source: https://viko.sh/install.sh (the same file). Read it before you run it. set -eu # pipefail is not POSIX, but every macOS /bin/sh supports it. # shellcheck disable=SC3040 (set -o pipefail) 2>/dev/null && set -o pipefail # BEGIN PINNED RELEASE KEYS (generated by `pnpm --filter @viko/site keys:embed` from keys/release-keys.json) # 2026-09 primary: p256 BC116675B61FC7D7, minisign 11F859B7A0103956 PINNED_P256_KEYS='-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpX/MYphB8nJGypyLAuLoKh5rIAeO Z5goo3QbLBX/wjKogyFsGriB4m6pvn61xmiV+Z6ePjTZ7DjYWhLFzCDBQw== -----END PUBLIC KEY-----' PINNED_MINISIGN_KEYS='RWRWORCgt1n4EVdLHEjrWfj5EnJL0swLOaeb+NTrIAXYfn1Qs2yBTM8s' # END PINNED RELEASE KEYS BASE_URL="${VIKO_INSTALL_BASE_URL:-https://viko.sh}" INSTALL_DIR="${HOME}/.viko/bin" BIN="${INSTALL_DIR}/vikod" STATE_FILE="${INSTALL_DIR}/.vikod-release" ARTIFACT="vikod-darwin-arm64" invite="" hub="" run_setup=1 path_mode="ask" tmp="" say() { printf '%s\n' "$*" >&2; } step() { printf '\033[1m==>\033[0m %s\n' "$*" >&2; } die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2 exit 1 } usage() { sed -n '2,/^# Source:/p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' >&2 || say "usage: sh install.sh [--invite CODE] [--hub URL] [--no-setup] [--add-to-path|--no-modify-path]" } cleanup() { if [ -n "$tmp" ] && [ -d "$tmp" ]; then rm -rf "$tmp"; fi } parse_args() { while [ $# -gt 0 ]; do case "$1" in --invite) [ $# -ge 2 ] || die "--invite needs a code" invite="$2" shift 2 ;; --invite=*) invite="${1#--invite=}" shift ;; --hub) [ $# -ge 2 ] || die "--hub needs a URL" hub="$2" shift 2 ;; --hub=*) hub="${1#--hub=}" shift ;; --no-setup) run_setup=0; shift ;; --add-to-path) path_mode="yes"; shift ;; --no-modify-path) path_mode="no"; shift ;; -h | --help) usage; exit 0 ;; *) die "unknown option: $1 (try --help)" ;; esac done if [ -n "$invite" ]; then printf '%s' "$invite" | grep -Eq '^[A-Za-z0-9_-]{4,128}$' || die "that invite code doesn't look right. Copy the command again from your invite page." fi if [ -n "$hub" ]; then printf '%s' "$hub" | grep -Eq '^https://[A-Za-z0-9.-]+(:[0-9]+)?/?$' || die "--hub must be an https:// origin" fi printf '%s' "$BASE_URL" | grep -Eq '^(https://[A-Za-z0-9.-]+|http://(127\.0\.0\.1|localhost))(:[0-9]+)?$' || die "VIKO_INSTALL_BASE_URL must be an https:// origin" } check_platform() { os="$(uname -s)" case "$os" in Darwin) ;; Linux) die "Viko runs on macOS (Apple silicon) and Windows for now. Linux isn't supported yet." ;; *) die "Viko runs on macOS (Apple silicon) and Windows for now. $os isn't supported." ;; esac arch="$(uname -m)" # A shell running under Rosetta reports x86_64 on Apple silicon; ask the hardware instead. if [ "$arch" != "arm64" ] && [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" != "1" ]; then die "Viko needs a Mac with Apple silicon for now. Intel Macs aren't supported yet." fi for tool in curl openssl shasum plutil mktemp; do command -v "$tool" >/dev/null 2>&1 || die "missing required tool: $tool" done } fetch() { # fetch case "$BASE_URL" in https://*) curl -fsSL --proto '=https' --tlsv1.2 --retry 2 -o "$2" "${BASE_URL}/$1" ;; *) curl -fsSL --retry 2 -o "$2" "${BASE_URL}/$1" ;; esac || die "download failed: ${BASE_URL}/$1" } field() { # field : read a value from the verified manifest plutil -extract "$1" raw -o - "$tmp/latest.json" 2>/dev/null || die "manifest is missing $1" } is_uint() { printf '%s' "$1" | grep -Eq '^(0|[1-9][0-9]{0,15})$'; } is_semver() { printf '%s' "$1" | grep -Eq '^(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})\.(0|[1-9][0-9]{0,5})$'; } # semver_cmp A B prints -1, 0 or 1 semver_cmp() { awk -v a="$1" -v b="$2" 'BEGIN { split(a, x, "."); split(b, y, ".") for (i = 1; i <= 3; i++) { if (x[i] + 0 < y[i] + 0) { print -1; exit } if (x[i] + 0 > y[i] + 0) { print 1; exit } } print 0 }' } verify_manifest() { step "Checking the release signature" fetch "dl/latest.json" "$tmp/latest.json" # The sequence number is read before verification only to pick the matching immutable # signature file; it is re-read and checked once the signature holds. seq_hint="$(plutil -extract sequence raw -o - "$tmp/latest.json" 2>/dev/null || true)" is_uint "$seq_hint" || die "the release manifest is malformed" fetch "dl/manifests/${seq_hint}.json.p256.sig" "$tmp/latest.json.p256.sig" printf '%s\n' "$PINNED_P256_KEYS" | awk -v dir="$tmp" '/BEGIN PUBLIC KEY/ { n++ } n { print > (dir "/pin" n ".pem") }' verified="" for pem in "$tmp"/pin*.pem; do [ -f "$pem" ] || continue if openssl dgst -sha256 -verify "$pem" -signature "$tmp/latest.json.p256.sig" \ "$tmp/latest.json" >/dev/null 2>&1; then verified=1 break fi done [ -n "$verified" ] || die "the release manifest's signature is not valid. Nothing was installed. This can happen for a moment while a release is being published; try again in a minute. If it keeps happening, stop and report it: the download may have been tampered with." if command -v minisign >/dev/null 2>&1; then fetch "dl/manifests/${seq_hint}.json.minisig" "$tmp/latest.json.minisig" ok="" for pk in $PINNED_MINISIGN_KEYS; do if minisign -Vqm "$tmp/latest.json" -x "$tmp/latest.json.minisig" -P "$pk" >/dev/null 2>&1; then ok=1 break fi done [ -n "$ok" ] || die "the release manifest's minisign signature is not valid. Nothing was installed." fi [ "$(field schema)" = "viko.release/v1" ] || die "unsupported manifest format; update this installer" [ "$(field product)" = "vikod" ] || die "the manifest is not for vikod" version="$(field version)" sequence="$(field sequence)" expires_at="$(field expiresAt)" is_semver "$version" || die "the manifest has an invalid version" is_uint "$sequence" || die "the manifest has an invalid sequence" [ "$sequence" = "$seq_hint" ] || die "the manifest changed while downloading; try again" is_uint "$expires_at" || die "the manifest has an invalid expiry" [ "$(date +%s)" -lt "$expires_at" ] || die "the release manifest has expired, so it can't be trusted. Try again later or report it." file_name="$(field "files.${ARTIFACT}.name")" file_path="$(field "files.${ARTIFACT}.path")" file_sha="$(field "files.${ARTIFACT}.sha256")" file_size="$(field "files.${ARTIFACT}.size")" [ "$file_name" = "$ARTIFACT" ] || die "the manifest names an unexpected file" [ "$file_path" = "${version}/${ARTIFACT}" ] || die "the manifest has an unexpected file path" printf '%s' "$file_sha" | grep -Eq '^[0-9a-f]{64}$' || die "the manifest has an invalid checksum" is_uint "$file_size" || die "the manifest has an invalid size" say " vikod ${version} (release ${sequence}), signed by the pinned Viko release key" } check_rollback() { if [ -f "$STATE_FILE" ]; then seen="$(sed -n 's/^sequence=//p' "$STATE_FILE" | head -n 1)" if is_uint "$seen" && [ "$sequence" -lt "$seen" ]; then die "this manifest (release ${sequence}) is older than one this Mac already installed (release ${seen}). Refusing to roll back." fi fi installed="" if [ -x "$BIN" ]; then installed="$("$BIN" version 2>/dev/null | head -n 1 || true)" if is_semver "$installed" && [ "$(semver_cmp "$installed" "$version")" = "1" ]; then die "vikod ${installed} is already installed, which is newer than ${version}. Refusing to downgrade." fi fi } install_binary() { if [ "$installed" = "$version" ] && [ "$(shasum -a 256 "$BIN" | awk '{print $1}')" = "$file_sha" ]; then step "vikod ${version} is already installed" return fi step "Downloading vikod ${version}" fetch "dl/${file_path}" "$tmp/vikod" got_size="$(wc -c <"$tmp/vikod" | tr -d ' ')" [ "$got_size" = "$file_size" ] || die "downloaded vikod has the wrong size. Nothing was installed." got_sha="$(shasum -a 256 "$tmp/vikod" | awk '{print $1}')" [ "$got_sha" = "$file_sha" ] || die "downloaded vikod does not match the signed checksum. Nothing was installed." say " SHA-256 ${got_sha} matches the signed manifest" chmod 755 "$tmp/vikod" xattr -d com.apple.quarantine "$tmp/vikod" 2>/dev/null || true reported="$("$tmp/vikod" version 2>/dev/null | head -n 1 || true)" [ "$reported" = "$version" ] || die "the downloaded vikod reports version '${reported}', expected ${version}" mv -f "$tmp/vikod" "$BIN" step "Installed vikod ${version} to ${BIN}" } record_state() { printf 'sequence=%s\nversion=%s\n' "$sequence" "$version" >"$STATE_FILE.tmp" mv -f "$STATE_FILE.tmp" "$STATE_FILE" } have_tty() { (exec /dev/null; } setup_path() { if [ "$(command -v vikod 2>/dev/null || true)" = "$BIN" ]; then return; fi line="export PATH=\"\$HOME/.viko/bin:\$PATH\"" case "$(basename "${SHELL:-sh}")" in zsh) rc="$HOME/.zshrc" ;; bash) rc="$HOME/.bash_profile" ;; *) rc="$HOME/.profile" ;; esac if [ -f "$rc" ] && grep -Fq '.viko/bin' "$rc"; then say " ${rc} already adds ~/.viko/bin to PATH (open a new terminal to pick it up)" return fi answer="n" if [ "$path_mode" = "yes" ]; then answer="y" elif [ "$path_mode" = "ask" ] && have_tty; then printf 'Add ~/.viko/bin to your PATH in %s? [y/N] ' "$rc" >&2 read -r answer >"$rc" say " added to ${rc}; open a new terminal to use \`vikod\` directly" ;; *) say " To run \`vikod\` directly, add this line to ${rc}:" say " ${line}" ;; esac } run_setup_step() { if [ "$run_setup" = 0 ]; then say "" say "Installed. Next: ${BIN} setup${invite:+ --invite ${invite}}" return fi if [ -z "$invite" ]; then say "" say "Installed. To pair this Mac, open your invite link (https://viko.sh/i/...) and run:" say " ${BIN} setup --invite " return fi step "Pairing this Mac with Viko" set -- setup --invite "$invite" if [ -n "$hub" ]; then set -- "$@" --hub "$hub"; fi cleanup tmp="" if have_tty; then exec "$BIN" "$@" /dev/null || true tmp="$(mktemp -d "${INSTALL_DIR}/.install.XXXXXX")" trap cleanup EXIT trap 'cleanup; exit 130' INT TERM verify_manifest check_rollback install_binary record_state setup_path run_setup_step } main "$@"