#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# SPDX-FileCopyrightText: 2026 Birch Point SWE

set -o pipefail
umask 077

MESH_ROOT="${MESH_ROOT:-$HOME/.config/mesh}"
CONF="${MESH_CONF:-$MESH_ROOT/conf}"
STATE="${MESH_STATE:-$MESH_ROOT/state}"
SELF=$(readlink -f "$0" 2>/dev/null || printf '%s' "$0")
SSH_DIR="$HOME/.ssh"
AK="$SSH_DIR/authorized_keys"
SC="$SSH_DIR/config"
KH="$STATE/known_hosts"
AS="$STATE/allowed_signers"
NS="mesh"
CTO="${MESH_CONNECT_TIMEOUT:-5}"
B1="# BEGIN MESH MANAGED"
B2="# END MESH MANAGED"

set_identity() {
  if [ -f "$STATE/key" ]; then TID="$STATE/key"; else TID="$STATE/key.pub"; fi
  SOPTS=(-o BatchMode=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$KH"
    -o "IdentityFile=$TID" -o IdentitiesOnly=yes -o "ConnectTimeout=$CTO"
    -o LogLevel=ERROR)
  GSSH="ssh -o BatchMode=yes -o IdentityFile=$TID -o IdentitiesOnly=yes -o ConnectTimeout=$CTO -o ControlMaster=no -o ControlPath=none"
}

set_identity

GOPTS=(-o BatchMode=yes -o StrictHostKeyChecking=yes -o "UserKnownHostsFile=$KH"
  -o "ConnectTimeout=$CTO" -o LogLevel=ERROR)

die() {
  echo "mesh: $*" >&2
  [ -d "$STATE" ] && printf '%s %s\n' "$(date '+%F %T')" "$*" >> "$STATE/last-error" 2>/dev/null
  exit 1
}
log() { printf '%s mesh %s\n' "$(date '+%H:%M:%S')" "$*" >&2; }

me() { cat "$STATE/name" 2>/dev/null; }

ensure_dirs() {
  mkdir -p "$CONF/nodes" "$CONF/keys" "$CONF/hostkeys" \
    "$STATE/records" "$STATE/roster" "$STATE/in" \
    "$STATE/tmp" "$STATE/hostkeys" "$STATE/invites" "$SSH_DIR"
  chmod 700 "$SSH_DIR"
}

node_get() {
  awk -F= -v k="$2" '$1==k{print substr($0,length(k)+2);exit}' \
    "$CONF/nodes/$1.conf" 2>/dev/null
}

node_get_all() {
  awk -F= -v k="$2" '$1==k{print substr($0,length(k)+2)}' \
    "$CONF/nodes/$1.conf" 2>/dev/null
}

nodes() {
  local f b
  for f in "$CONF/nodes"/*.conf; do
    [ -f "$f" ] || continue
    b=${f##*/}
    b=${b%.conf}
    case "$b" in ''|*[!a-zA-Z0-9_-]*) continue ;; esac
    printf '%s\n' "$b"
  done
}

peers() { nodes | grep -vx "$(me)"; }

is_member() { [ -f "$CONF/keys/$1.pub" ]; }

OPERATORS="$CONF/operators"

is_termux() {
  case "$(uname -o 2>/dev/null)" in *[Aa]ndroid*) return 0 ;; esac
  [ -x /system/bin/app_process ]
}

tx_prefix() {
  case "${PREFIX:-}" in
    *com.termux*) printf '%s\n' "$PREFIX" ;;
    *) printf '/data/data/com.termux/files/usr\n' ;;
  esac
}

p_profile() {
  local ct
  is_termux && { printf 'mobile\n'; return 0; }
  if command -v systemd-detect-virt >/dev/null 2>&1; then
    [ "$(systemd-detect-virt 2>/dev/null)" != "none" ] && { printf 'fixed\n'; return 0; }
  fi
  ct=$(cat /sys/devices/virtual/dmi/id/chassis_type 2>/dev/null)
  case "$ct" in
    8|9|10|11|12|14|30|31|32) printf 'mobile\n'; return 0 ;;
    ''|*[!0-9]*) ;;
    *) printf 'fixed\n'; return 0 ;;
  esac
  ls /sys/class/power_supply/BAT* >/dev/null 2>&1 && { printf 'mobile\n'; return 0; }
  printf 'fixed\n'
}

alive_interval() { [ "$(p_profile)" = mobile ] && printf '180\n' || printf '30\n'; }
poll_interval() { [ "$(p_profile)" = mobile ] && printf '120\n' || printf '30\n'; }

is_private() {
  case "$1" in
    10.*|192.168.*) return 0 ;;
    172.1[6-9].*|172.2[0-9].*|172.3[01].*) return 0 ;;
  esac
  return 1
}

have_git() { command -v git >/dev/null 2>&1; }

mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null; }

sha256() {
  if command -v sha256sum >/dev/null 2>&1; then
    printf '%s' "$1" | sha256sum | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    printf '%s' "$1" | shasum -a 256 | awk '{print $1}'
  else
    printf '%s' "$1" | openssl dgst -sha256 2>/dev/null | awk '{print $NF}'
  fi
}

sha256f() {
  if command -v sha256sum >/dev/null 2>&1; then
    sha256sum "$1" | awk '{print $1}'
  elif command -v shasum >/dev/null 2>&1; then
    shasum -a 256 "$1" | awk '{print $1}'
  else
    openssl dgst -sha256 "$1" 2>/dev/null | awk '{print $NF}'
  fi
}

invites_prune() {
  local f now e
  now=$(date +%s)
  for f in "$STATE"/invites/*; do
    [ -f "$f" ] || continue
    e=$(cat "$f" 2>/dev/null)
    case "$e" in ''|*[!0-9]*) rm -f "$f"; continue ;; esac
    [ "$e" -lt "$now" ] && rm -f "$f"
  done
  return 0
}

invites_active() {
  local f
  invites_prune
  for f in "$STATE"/invites/*; do
    [ -f "$f" ] || continue
    printf '%s\n' "${f##*/}"
  done
  return 0
}

cmd_invite() {
  local ttl="${MESH_INVITE_TTL:-1800}" tok exp serve=0 sport="${MESH_JOIN_PORT:-43117}" wait=""
  while [ $# -gt 0 ]; do
    case "$1" in
      --serve) serve=1; shift ;;
      --sport) sport=$2; shift 2 ;;
      --wait) wait=$2; shift 2 ;;
      *) die "unknown option: $1" ;;
    esac
  done
  [ -n "$(me)" ] || die "not initialized"
  ensure_dirs
  tok=$(od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')
  [ -n "$tok" ] || die "could not generate a token"
  exp=$(( $(date +%s) + ttl ))
  printf '%s\n' "$exp" > "$STATE/invites/$tok"
  log "token: $tok"
  if [ "$serve" -eq 1 ]; then
    case "$sport" in ''|*[!0-9]*) die "sport must be numeric" ;; esac
    [ -n "$wait" ] || wait=$ttl
    case "$wait" in ''|*[!0-9]*) die "wait must be numeric" ;; esac
    serve_join "$tok" "$sport" "$wait"
    return
  fi
  log "one use, valid $((ttl / 60)) min. On the new device, after copying"
  log "conf there:  mesh init <new-name> --token $tok"
  log "it then joins on this node's next tick, or run: mesh adopt --scan --auto"
}

ct_eq() {
  local r
  r=$(od -An -N8 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')
  [ "$(sha256 "$r|$1")" = "$(sha256 "$r|$2")" ]
}

nc_listen() {
  local t=$1 p=$2 in=$3 out=$4
  if [ -z "$NC_STYLE" ]; then
    if nc -h 2>&1 | grep -qi openbsd; then NC_STYLE=bsd; else NC_STYLE=trad; fi
  fi
  if [ "$NC_STYLE" = bsd ]; then
    timeout "$t" nc -l "$p" < "$in" > "$out" 2>/dev/null
  else
    timeout "$t" nc -l -p "$p" < "$in" > "$out" 2>/dev/null
  fi
}

serve_join() {
  local tok=$1 sport=$2 wait=$3 deadline left strm rf psha tag a line
  command -v nc >/dev/null 2>&1 || die "nc is required for --serve"
  conf_is_repo && cmd_sync
  strm="$STATE/tmp/join-strm.$$"
  rf="$STATE/tmp/join-ready.$$"
  tar -C "$MESH_ROOT" -cf "$strm.tar" conf || die "could not pack conf"
  psha=$(sha256f "$strm.tar")
  tag=$(sha256 "mesh-join1|$psha|$tok")
  { printf 'MESH1 %s %s %s\n' "$(wc -c < "$strm.tar" | tr -d ' ')" "$psha" "$tag"
    cat "$strm.tar"
  } > "$strm"
  for a in $(p_addrs); do
    log "on the new device run:  mesh join <new-name> $a:$sport --token $tok"
  done
  deadline=$(( $(date +%s) + wait ))
  while :; do
    left=$(( deadline - $(date +%s) ))
    [ "$left" -gt 0 ] || { log "serve window closed, no device joined"; break; }
    invites_active | grep -qx "$tok" || { log "invite expired or burned, stopping"; break; }
    : > "$rf"
    nc_listen "$left" "$sport" "$strm" "$rf"
    [ -s "$rf" ] || continue
    line=$(head -1 "$rf" | tr -d '\r')
    handle_ready "$tok" "$psha" "$line" && break
  done
  rm -f "$strm" "$strm.tar" "$rf"
}

handle_ready() {
  local tok=$1 psha=$2 name user port addrs tag want br a t2
  set -- $3
  [ "$1" = "MESH1-READY" ] || return 1
  name=$2 user=$3 port=$4 addrs=$5 tag=$6
  case "$name" in ''|*[!a-zA-Z0-9_-]*) return 1 ;; esac
  case "$user" in ''|*[!a-zA-Z0-9._-]*) return 1 ;; esac
  case "$port" in ''|*[!0-9]*) return 1 ;; esac
  case "$addrs" in ''|*[!0-9a-fA-F.:,]*) return 1 ;; esac
  want=$(sha256 "mesh-ready1|$name|$user|$port|$addrs|$psha|$tok")
  ct_eq "$tag" "$want" || { log "READY with a bad authenticator, ignored"; return 1; }
  [ -f "$CONF/nodes/$name.conf" ] && { log "READY for existing node $name, ignored"; return 1; }
  br=$(gitc symbolic-ref --short HEAD 2>/dev/null)
  [ -n "$br" ] || br=main
  for a in ${addrs//,/ }; do
    GIT_SSH_COMMAND=$(adopt_gs "$port") gitc fetch -q "$user@$a:.config/mesh/conf" \
      "$br" >/dev/null 2>&1 || continue
    if t2=$(verify_proof "$name"); then
      if adopt_finish "$name"; then
        rm -f "$STATE/invites/$t2"
        printf '%s\n' "$a" > "$STATE/roster/$name"
        vouch "$name"
        cmd_sync
        log "adopted $name from $user@$a:$port (invite burned)"
        return 0
      fi
      log "conf merge from $name failed"
    else
      log "conf from $user@$a:$port has no valid invite proof, REJECTED"
    fi
  done
  return 1
}

find_seed() {
  local h=$1 sn sp
  for sn in $(nodes); do
    [ "$sn" = "$(me)" ] && continue
    sp=$(node_get "$sn" port)
    [ -n "$sp" ] || continue
    if ident "$h" "$sp" 2>/dev/null | grep -qx "$sn"; then
      printf '%s\n' "$h" > "$STATE/roster/$sn"
      printf '%s\n' "$sn"
      return 0
    fi
  done
  return 1
}

join_session() {
  local name=$1 tok=$2 host=$3 m size psha tag tf user port addrs n=0 seed="" br jf
  shift 3
  IFS=' ' read -r -t 30 m size psha tag <&3 || die "no header from the seed"
  [ "$m" = "MESH1" ] || die "unrecognized protocol header"
  case "$size" in ''|*[!0-9]*) die "bad size in header" ;; esac
  [ "$size" -le 52428800 ] || die "conf implausibly large"
  tf="$STATE/tmp/join.$$.tar"
  head -c "$size" <&3 > "$tf"
  [ "$(wc -c < "$tf" | tr -d ' ')" = "$size" ] || die "short read from the seed"
  [ "$(sha256f "$tf")" = "$psha" ] || die "conf corrupted in transit"
  ct_eq "$tag" "$(sha256 "mesh-join1|$psha|$tok")" || \
    die "AUTHENTICATION FAILED: wrong token, or something other than the real
     seed answered. No conf was installed."
  tar -C "$MESH_ROOT" -xf "$tf" || die "unpack failed"
  rm -f "$tf"
  log "conf verified against the invite token and installed"
  cmd_init "$name" --token "$tok" "$@"
  if seed=$(find_seed "$host"); then
    log "seed identified as $seed at $host"
  else
    seed=""
    log "cannot match the seed's host key yet; retrying while waiting to be adopted"
  fi
  user=$(node_get "$name" user)
  port=$(node_get "$name" port)
  addrs=$(p_addrs | paste -sd, -)
  [ -n "$addrs" ] || die "no local address to announce"
  printf 'MESH1-READY %s %s %s %s %s\n' "$name" "$user" "$port" "$addrs" \
    "$(sha256 "mesh-ready1|$name|$user|$port|$addrs|$psha|$tok")" >&3
  exec 3>&-
  log "announced readiness, waiting for the seed to adopt"
  br=$(gitc symbolic-ref --short HEAD 2>/dev/null)
  [ -n "$br" ] || br=main
  jf="$STATE/tmp/join-fetch.$$"
  while [ "$n" -lt "${MESH_ADOPT_WAIT:-100}" ]; do
    sleep 6
    if [ -z "$seed" ] && seed=$(find_seed "$host"); then
      log "seed identified as $seed at $host"
    fi
    if [ -n "$seed" ]; then
      if GIT_SSH_COMMAND="$GSSH" gitc fetch -q "$seed:.config/mesh/conf" "$br" 2> "$jf"; then
        if conf_check_fetched "$br"; then
          gitc merge -q --no-edit FETCH_HEAD >/dev/null 2>&1
          strip_proof "$name"
          gen_as
          gen_kh
          compile_ak
          compile_sc
          cmd_sync
          rm -f "$jf"
          log "adopted; the mesh is live here. Next: mesh schedule"
          return 0
        fi
        log "the seed's conf failed verification, retrying"
      elif [ $((n % 5)) -eq 0 ]; then
        log "still waiting: fetch from $seed says $(tr -d '\r' < "$jf" | grep -v '^$' | tail -1)"
      fi
    elif [ $((n % 5)) -eq 0 ]; then
      log "still waiting: the seed's host key does not answer (its sshd penalizes recent failed logins from this address)"
    fi
    n=$((n + 1))
  done
  rm -f "$jf"
  log "no confirmation yet; the seed adopts on its next tick, or run"
  log "'mesh adopt --scan --auto' there"
}

cmd_join() {
  local name="" seed="" tok="" host sport n=0
  local -a passthru=()
  while [ $# -gt 0 ]; do
    case "$1" in
      --token) tok=$2; shift 2 ;;
      --port|--user|--static) passthru+=("$1" "$2"); shift 2 ;;
      -*) die "unknown option: $1" ;;
      *) if [ -z "$name" ]; then name=$1; else seed=$1; fi; shift ;;
    esac
  done
  [ -n "$name" ] && [ -n "$seed" ] && [ -n "$tok" ] || \
    die "usage: mesh join <new-name> <seed-addr[:port]> --token <tok> [init options]"
  case "$name" in *[!a-zA-Z0-9_-]*) die "name must be [a-zA-Z0-9_-]" ;; esac
  [ -n "$(me)" ] && die "already initialized as $(me)"
  ls "$CONF"/keys/*.pub >/dev/null 2>&1 && die "conf already present here; use mesh init"
  have_git || die "git is required"
  host=${seed%%:*}
  sport=${seed##*:}
  [ "$sport" = "$seed" ] && sport="${MESH_JOIN_PORT:-43117}"
  case "$sport" in ''|*[!0-9]*) die "bad seed port" ;; esac
  ensure_dirs
  until { command exec 3<>"/dev/tcp/$host/$sport"; } 2>/dev/null; do
    n=$((n + 1))
    [ "$n" -lt "${MESH_JOIN_WAIT:-30}" ] || \
      die "could not reach the seed at $host:$sport (is 'mesh invite --serve' running there?)"
    [ "$n" -eq 1 ] && log "seed at $host:$sport not listening yet, retrying"
    sleep 2
  done
  join_session "$name" "$tok" "$host" "${passthru[@]}"
}

port_open() { timeout "$CTO" bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null; }

p_addrs() {
  { ip -o -4 addr show 2>/dev/null | awk '!/ lo /{print $4}' | cut -d/ -f1
    ifconfig 2>/dev/null | awk '/inet /{for(i=1;i<=NF;i++)if($i=="inet")print $(i+1)}'
  } | grep -vE '^127\.|^$' | sort -u
  return 0
}

p_subnets() {
  local a
  for a in $(p_addrs); do
    is_private "$a" && printf '%s\n' "${a%.*}.0/24"
  done | sort -u | tr '\n' ' '
}

p_hostkey_pub() {
  local d t
  for t in ed25519 rsa ecdsa; do
    for d in "$(tx_prefix)/etc/ssh" /etc/ssh; do
      [ -f "$d/ssh_host_${t}_key.pub" ] && { cat "$d/ssh_host_${t}_key.pub"; return 0; }
    done
  done
  return 1
}

p_ensure_sshd() {
  local port
  port=$(node_get "$(me)" port)
  [ -n "$port" ] || return 1
  port_open 127.0.0.1 "$port" && return 0
  if is_termux; then
    export SVDIR="$(tx_prefix)/var/service" LOGDIR="$(tx_prefix)/var/log"
    service-daemon start >/dev/null 2>&1 4>&- 9>&-
    sv up sshd 2>/dev/null 4>&- 9>&-
  else
    systemctl start sshd 2>/dev/null || systemctl start ssh 2>/dev/null || \
      /usr/sbin/sshd 2>/dev/null 4>&- 9>&-
  fi
  sleep 1
  port_open 127.0.0.1 "$port"
}

have_systemd_user() {
  command -v systemctl >/dev/null 2>&1 || return 1
  [ -d "/run/user/$(id -u)/systemd" ] || return 1
  systemctl --user show-environment >/dev/null 2>&1
}

start_daemon_bg() {
  if ! ( exec 4>"$STATE/daemon.lock"; flock -n 4 ) 2>/dev/null; then
    log "daemon already running"
    return 0
  fi
  if [ -f "$STATE/daemon.log" ] &&
      [ "$(wc -c <"$STATE/daemon.log")" -gt "${MESH_LOG_MAX:-1048576}" ]; then
    tail -n 500 "$STATE/daemon.log" > "$STATE/daemon.log.tmp" &&
      mv "$STATE/daemon.log.tmp" "$STATE/daemon.log"
  fi
  nohup "$SELF" daemon >>"$STATE/daemon.log" 2>&1 &
  log "daemon started (pid $!, log $STATE/daemon.log)"
}

p_schedule() {
  local self unit boot svc
  self=$SELF
  log "profile: $(p_profile) (keepalive $(alive_interval)s, poll $(poll_interval)s)"
  if is_termux && command -v termux-job-scheduler >/dev/null 2>&1; then
    svc="$(tx_prefix)/var/service/mesh"
    if command -v sv-enable >/dev/null 2>&1 && [ -d "${svc%/mesh}" ]; then
      mkdir -p "$svc/log"
      printf '#!%s/bin/sh\nexec %q daemon 2>&1\n' "$(tx_prefix)" "$self" > "$svc/run"
      printf '#!%s/bin/sh\np=${PWD%%/*}\np=${p##*/}\nmkdir -p "$LOGDIR/sv/$p"\nexec svlogd -tt "$LOGDIR/sv/$p"\n' \
        "$(tx_prefix)" > "$svc/log/run"
      chmod 700 "$svc/run" "$svc/log/run"
      sv-enable mesh >/dev/null 2>&1
      sv up mesh >/dev/null 2>&1
      log "runsv service installed: sv status mesh"
    else
      log "termux-services missing; the daemon will not survive a closed session"
    fi
    boot="$HOME/.termux/boot/start-mesh.sh"
    if [ -d "$HOME/.termux/boot" ]; then
      printf '#!%s/bin/sh\n. %s/etc/profile.d/start-services.sh\nsv-enable mesh\n' \
        "$(tx_prefix)" "$(tx_prefix)" > "$boot"
      chmod 700 "$boot"
      log "termux:boot script installed: $boot"
    else
      log "no ~/.termux/boot (install Termux:Boot and open it once)"
    fi
    printf '#!%s/bin/sh\nexec %q daemon --once\n' "$(tx_prefix)" "$self" > "$STATE/tick.sh"
    chmod 700 "$STATE/tick.sh"
    termux-job-scheduler --job-id 3388 --script "$STATE/tick.sh" \
      --period-ms "${MESH_PERIOD_MS:-86400000}" --persisted true --network any \
      >/dev/null 2>&1
    log "job-scheduler 3388 installed as the restart-if-dead backstop"
    log "undo: mesh reset (cancels the job and removes the boot script)"
    return 0
  fi
  if have_systemd_user; then
    unit="$HOME/.config/systemd/user/mesh.service"
    mkdir -p "$(dirname "$unit")"
    { printf '[Unit]\nDescription=mesh\nAfter=network.target\n\n'
      printf '[Service]\nType=simple\nExecStart=%s daemon\n' "$self"
      printf 'Restart=always\nRestartSec=10\n\n'
      printf '[Install]\nWantedBy=default.target\n'
    } > "$unit"
    systemctl --user daemon-reload >/dev/null 2>&1
    systemctl --user enable --now mesh.service >/dev/null 2>&1
    if loginctl enable-linger "$(id -un)" >/dev/null 2>&1; then
      log "lingering enabled, so it starts at boot without a login"
    else
      log "could not enable lingering; run: sudo loginctl enable-linger $(id -un)"
    fi
    log "systemd user unit installed and started: $unit"
    log "status: systemctl --user status mesh   undo: mesh reset"
    return 0
  fi
  if command -v crontab >/dev/null 2>&1; then
    { crontab -l 2>/dev/null | grep -v "mesh tick" | grep -v "mesh daemon"
      printf '@reboot %q daemon\n' "$self"
      printf '23 4 * * * %q daemon --once\n' "$self"
    } | crontab - 2>/dev/null
    log "cron installed: daemon at boot, daily backstop at 04:23"
    start_daemon_bg
    log "undo: mesh reset"
    return 0
  fi
  log "no supervisor found. Start '$self daemon' at boot yourself"
  log "(systemd user unit, launchd agent, or an init script)."
  return 1
}

unresolved_members() {
  local n
  for n in $(peers); do
    is_member "$n" || continue
    [ -f "$STATE/roster/$n" ] || return 0
  done
  return 1
}

cmd_daemon() {
  local once=0 prev cur n=0 poll cycles pnodes cnodes last
  [ "$1" = "--once" ] && once=1
  [ -n "$(me)" ] || die "not initialized"
  ensure_dirs
  exec 4>"$STATE/daemon.lock"
  if ! flock -n 4; then
    [ "$once" -eq 1 ] && { log "daemon already running"; return 0; }
    die "daemon already running"
  fi
  poll=$(poll_interval)
  cycles=$(( ${MESH_BACKSTOP:-21600} / poll ))
  [ "$cycles" -lt 1 ] && cycles=1
  log "daemon starting (profile $(p_profile), poll ${poll}s)"
  cmd_tick
  if [ "$once" -eq 1 ]; then
    log "single pass done"
    return 0
  fi
  prev=$(p_addrs | sort | tr '\n' ',')
  pnodes=$(nodes | sort | tr '\n' ',')
  last=$(date +%s)
  while :; do
    sleep "$poll" 4>&-
    cur=$(p_addrs | sort | tr '\n' ',')
    if [ "$cur" != "$prev" ]; then
      log "addresses changed: ${prev:-none} -> ${cur:-none}"
      prev=$cur
      n=0
      cmd_tick
      continue
    fi
    if unresolved_members && [ $(( $(date +%s) - last )) -ge "${MESH_RETRY:-300}" ]; then
      log "peers still unresolved, retrying"
      last=$(date +%s)
      n=0
      cmd_tick
      continue
    fi
    cnodes=$(nodes | sort | tr '\n' ',')
    if [ "$cnodes" != "$pnodes" ]; then
      log "membership changed, converging"
      pnodes=$cnodes
      n=0
      cmd_tick
      continue
    fi
    n=$((n + 1))
    if [ "$n" -ge "$cycles" ]; then
      n=0
      cmd_tick
    fi
  done
}

load_platform() {
  [ -f "$STATE/platform.sh" ] && . "$STATE/platform.sh"
  return 0
}

gen_as() {
  local n t="$AS.$$"
  : > "$t"
  for n in $(nodes); do
    [ -f "$CONF/keys/$n.pub" ] || continue
    printf '%s %s\n' "$n" "$(cut -d' ' -f1,2 "$CONF/keys/$n.pub")" >> "$t"
  done
  mv "$t" "$AS"
}

gen_kh() {
  local n f t="$KH.$$"
  : > "$t"
  for n in $(nodes); do
    f="$CONF/hostkeys/$n.pub"
    [ -f "$f" ] || f="$STATE/hostkeys/$n.pub"
    [ -f "$f" ] || continue
    printf '%s %s\n' "$n" "$(head -n 1 "$f" | cut -d' ' -f1,2)" >> "$t"
  done
  awk '{print $2" "$3}' "$t" | sort | uniq -d | while read -r k; do
    log "WARNING: nodes $(awk -v k="$k" '($2" "$3)==k{printf "%s ",$1}' "$t")share one host key (cloned image?); dials may reach the wrong one"
  done
  mv "$t" "$KH"
}

ext_path() {
  local p="${SELF%/*}/mesh-$1"
  [ -x "$p" ] || p=$(command -v "mesh-$1" 2>/dev/null)
  [ -n "$p" ] && [ -x "$p" ] || return 1
  printf '%s\n' "$p"
}

guest_addr() {
  local n=$1 rcmd out addr hk
  if [ "$(node_get "$n" dynamic)" != 1 ]; then
    node_get "$n" static
    return 0
  fi
  if ! rcmd=$(ext_path "resolve-$n"); then
    guest_gossiped "$n" && return 0
    log "$n is dynamic, no 'mesh-resolve-$n' here, and no fresh record"
    return 1
  fi
  if ! out=$(guest_resolve "$n" "$rcmd"); then
    guest_gossiped "$n" && return 0
    return 1
  fi
  addr=$(printf '%s\n' "$out" | sed -n 1p)
  hk=$(printf '%s\n' "$out" | sed -n 2p)
  [ -n "$addr" ] || return 1
  if [ -n "$hk" ]; then
    printf '%s\n' "$hk" > "$STATE/hostkeys/$n.pub"
    gen_kh
    publish_guest "$n" "$addr" "$hk"
  fi
  printf '%s\n' "$addr"
}

guest_resolve() {
  local n=$1 rcmd=$2 err="$STATE/tmp/resolve-$1.err" try out msg
  for try in 1 2; do
    if out=$("$rcmd" 2>"$err"); then
      rm -f "$err"
      printf '%s\n' "$out"
      return 0
    fi
    [ "$try" = 2 ] || sleep "${MESH_RESOLVE_RETRY_WAIT:-3}"
  done
  msg=$(tail -n 1 "$err" 2>/dev/null)
  log "resolver for $n failed twice: ${msg:-no output}"
  return 1
}

guest_record_fresh() {
  local f="$STATE/records/guest-$1.rec" sq
  [ -f "$f" ] || return 1
  sq=$(awk -F= '$1=="seq"{print $2; exit}' "$f")
  case "$sq" in ''|*[!0-9]*) return 1 ;; esac
  [ $(( $(date +%s) - sq )) -le "${MESH_GUEST_TTL:-3600}" ]
}

publish_guest() {
  local n=$1 addr=$2 hk=$3 f="$STATE/records/guest-$1.rec"
  [ -f "$STATE/key" ] || return 0
  { printf 'guest=%s\nby=%s\nseq=%s\naddr=%s\nhostkey=%s\n' \
      "$n" "$(me)" "$(date +%s)" "$addr" "$hk"
  } > "$f"
  rm -f "$f.sig"
  ssh-keygen -Y sign -f "$STATE/key.pub" -n "$NS" "$f" >/dev/null 2>&1 || rm -f "$f"
  return 0
}

guest_gossiped() {
  local n=$1 f="$STATE/records/guest-$1.rec" sq now addr hk port
  if guest_record_fresh "$n"; then
    :
  elif [ -z "$GUEST_REFRESHED" ]; then
    GUEST_REFRESHED=1
    log "no fresh record for $n, asking peers"
    exchange
  fi
  [ -f "$f" ] || return 1
  sq=$(awk -F= '$1=="seq"{print $2; exit}' "$f")
  case "$sq" in ''|*[!0-9]*) return 1 ;; esac
  now=$(date +%s)
  addr=$(awk -F= '$1=="addr"{print $2; exit}' "$f")
  hk=$(awk -F= '$1=="hostkey"{print substr($0,9); exit}' "$f")
  [ -n "$addr" ] && [ -n "$hk" ] || return 1
  if [ $((now - sq)) -gt "${MESH_GUEST_TTL:-3600}" ]; then
    case "$addr" in
      ssm:*)
        log "record for $n is $(( (now - sq) / 60 )) min old, using its ssm address unprobed"
        ;;
      *)
        port=$(node_get "$n" port)
        port_open "$addr" "${port:-22}" || {
          log "gossiped endpoint for $n is stale and $addr does not answer"
          return 1
        }
        log "record for $n is $(( (now - sq) / 60 )) min old but $addr answers, using it"
        ;;
    esac
  fi
  printf '%s\n' "$hk" > "$STATE/hostkeys/$n.pub"
  gen_kh
  log "$n located from a record published by $(awk -F= '$1=="by"{print $2; exit}' "$f")"
  printf '%s\n' "$addr"
  return 0
}

gitc() { git -C "$CONF" -c core.hooksPath=/dev/null "$@"; }

conf_is_repo() { have_git && [ -d "$CONF/.git" ]; }

conf_peers() {
  local n
  for n in $(peers); do
    is_member "$n" || continue
    [ -f "$STATE/roster/$n" ] || continue
    printf '%s:.config/mesh/conf\n' "$n"
  done
  return 0
}

install_hook() {
  local h="$STATE/githooks" sh=/bin/sh
  is_termux && [ -x "$(tx_prefix)/bin/sh" ] && sh="$(tx_prefix)/bin/sh"
  mkdir -p "$h"
  gitc config core.hooksPath "$h" >/dev/null 2>&1
  gitc config gpg.format ssh >/dev/null 2>&1
  gitc config user.signingkey "$STATE/key.pub" >/dev/null 2>&1
  gitc config commit.gpgsign true >/dev/null 2>&1
  gitc config gpg.ssh.allowedSignersFile "$AS" >/dev/null 2>&1
  { printf '#!%s\n' "$sh"
    printf 'unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX\n'
    printf 'exec %q confcheck\n' "$SELF"
  } > "$h/pre-receive"
  chmod 700 "$h/pre-receive"
  { printf '#!%s\nexec >/dev/null 2>&1 </dev/null\n' "$sh"
    printf 'unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX\n'
    printf 'unset GIT_QUARANTINE_PATH GIT_OBJECT_DIRECTORY\n'
    printf 'unset GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_PUSH_CERT\n'
    printf 'export GIT_DIR GIT_WORK_TREE\n'
    printf '%q compile\n' "$SELF"
    printf '( setsid %q sync & ) 2>/dev/null || ( %q sync & )\n' "$SELF" "$SELF"
  } > "$h/post-receive"
  chmod 700 "$h/post-receive"
}

cmd_sync() {
  conf_is_repo || { log "conf is not a git repo (run mesh init here)"; return 1; }
  local out br u got=0 sent=0
  exec 6>"$STATE/sync.lock"
  flock -n 6 || { exec 6>&-; return 0; }
  br=$(gitc symbolic-ref --short HEAD 2>/dev/null)
  [ -n "$br" ] || br=main
  gitc config receive.denyCurrentBranch updateInstead >/dev/null 2>&1
  install_hook
  gitc add -A >/dev/null 2>&1
  gitc cat-file -e HEAD:operators 2>/dev/null && gitc reset -q HEAD -- operators >/dev/null 2>&1
  if ! gitc diff --cached --quiet 2>/dev/null; then
    if out=$(gitc commit -q --no-verify -m auto 2>&1); then
      log "conf committed"
    else
      log "conf commit failed: $(printf '%s' "$out" | head -2 | tr '\n' ' ')"
      exec 6>&-
      return 1
    fi
  fi
  for u in $(conf_peers); do
    GIT_SSH_COMMAND="$GSSH" gitc fetch -q "$u" "$br" \
      >/dev/null 2>&1 || continue
    got=$((got + 1))
    if ! conf_check_fetched "$br"; then
      log "conf from $u failed verification, left alone"
      continue
    fi
    gitc merge -q --no-edit FETCH_HEAD >/dev/null 2>&1 && continue
    gitc merge --abort >/dev/null 2>&1
    log "conf merge conflict from $u, left alone"
  done
  for u in $(conf_peers); do
    GIT_SSH_COMMAND="$GSSH" gitc push -q "$u" \
      "HEAD:refs/heads/$br" >/dev/null 2>&1 && sent=$((sent + 1))
  done
  [ "$got" -eq 0 ] && [ "$sent" -eq 0 ] && \
    log "conf sync reached no peers (offline, or none authorized yet)"
  exec 6>&-
  return 0
}

intro_node() {
  local sha=$1 pth n="" o
  for pth in $(gitc diff-tree --no-commit-id --name-only -r "$sha" 2>/dev/null); do
    case "$pth" in
      nodes/*.conf) o=${pth#nodes/}; o=${o%.conf} ;;
      keys/*.pub|hostkeys/*.pub) o=${pth#*/}; o=${o%.pub} ;;
      *) return 1 ;;
    esac
    [ -z "$n" ] || [ "$n" = "$o" ] || return 1
    n=$o
  done
  [ -n "$n" ] || return 1
  case "$n" in *[!a-zA-Z0-9_-]*) return 1 ;; esac
  grep -q "^$n " "$AS" 2>/dev/null && return 1
  gitc cat-file -e "$sha:keys/$n.pub" 2>/dev/null || return 1
  printf '%s\n' "$n"
}

conf_check() {
  local old new ref sha signer rng asig bad=0 tas ops n k v ok vouched p p1 p2 t changed
  conf_is_repo || return 0
  ensure_dirs
  gen_as
  tas="$STATE/tmp/as.$$"
  ops="$STATE/tmp/ops.$$"
  op_signers > "$ops"
  while read -r old new ref; do
    [ -n "$new" ] || continue
    case "$new" in *[!0]*) ;; *) continue ;; esac
    case "$old" in
      *[!0]*) rng="$old..$new" ;;
      *) rng="$new" ;;
    esac
    cp "$AS" "$tas" 2>/dev/null || : > "$tas"
    for n in "$@"; do
      key_blob_ok "$new:keys/$n.pub" || continue
      printf '%s %s\n' "$n" "$(gitc show "$new:keys/$n.pub" 2>/dev/null | cut -d' ' -f1,2)" >> "$tas"
    done
    changed=1
    while [ -n "$changed" ]; do
      changed=""
      vouched=""
      for sha in $(gitc rev-list "$rng" 2>/dev/null); do
        signer=$(gitc -c "gpg.ssh.allowedSignersFile=$tas" show --no-patch --format=%GS "$sha" 2>/dev/null)
        [ -n "$signer" ] && grep -q "^$signer " "$tas" 2>/dev/null && vouched="$vouched $sha"
      done
      for sha in $(gitc rev-list --no-merges --reverse "$rng" 2>/dev/null); do
        n=$(intro_node "$sha") || continue
        grep -q "^$n " "$tas" 2>/dev/null && continue
        key_blob_ok "$sha:keys/$n.pub" || continue
        k=$(gitc show "$sha:keys/$n.pub" 2>/dev/null | cut -d' ' -f1,2)
        printf '%s %s\n' "$n" "$k" > "$tas.one"
        asig=$(gitc -c "gpg.ssh.allowedSignersFile=$tas.one" show --no-patch --format='%G?%GS' "$sha" 2>/dev/null)
        rm -f "$tas.one"
        [ "$asig" = "G$n" ] || continue
        ok=""
        for v in $vouched; do
          gitc merge-base --is-ancestor "$sha" "$v" 2>/dev/null && { ok=1; break; }
        done
        [ -n "$ok" ] && { printf '%s %s\n' "$n" "$k" >> "$tas"; changed=1; }
      done
    done
    for sha in $(gitc rev-list --no-merges "$rng" 2>/dev/null); do
      signer=$(gitc -c "gpg.ssh.allowedSignersFile=$tas" show --no-patch --format=%GS "$sha" 2>/dev/null)
      conf_check_paths "$sha" "$signer" $(gitc diff-tree --no-commit-id --name-only -r "$sha" 2>/dev/null)
    done
    for sha in $(gitc rev-list --merges "$rng" 2>/dev/null); do
      p=$(gitc rev-list --parents -n 1 "$sha" 2>/dev/null)
      p1=$(printf '%s\n' "$p" | awk '{print $2}')
      p2=$(printf '%s\n' "$p" | awk 'NF==3{print $3}')
      if [ -n "$p2" ] && t=$(gitc merge-tree --write-tree "$p1" "$p2" 2>/dev/null) &&
          [ "$t" = "$(gitc rev-parse "$sha^{tree}" 2>/dev/null)" ]; then
        continue
      fi
      signer=$(gitc -c "gpg.ssh.allowedSignersFile=$tas" show --no-patch --format=%GS "$sha" 2>/dev/null)
      conf_check_paths "$sha" "$signer" $(gitc diff-tree --no-commit-id --name-only -r "$p1" "$sha" 2>/dev/null)
    done
  done
  rm -f "$tas" "$ops"
  [ "$bad" -eq 0 ]
}

op_signers() {
  if conf_is_repo && gitc cat-file -e HEAD:operators 2>/dev/null; then
    gitc show HEAD:operators 2>/dev/null
  else
    cat "$OPERATORS" 2>/dev/null
  fi
  return 0
}

key_blob_ok() {
  gitc show "$1" 2>/dev/null | awk '
    NR > 1 { bad = 1 }
    NR == 1 && $1 !~ /^(ssh-(ed25519|rsa)|ecdsa-sha2-nistp[0-9]+|sk-(ssh-ed25519|ecdsa-sha2-nistp256)@openssh\.com)$/ { bad = 1 }
    NR == 1 && $2 !~ /^[A-Za-z0-9+\/=]+$/ { bad = 1 }
    END { exit (NR == 0 || bad) }'
}

conf_check_paths() {
  local sha=$1 signer=$2 pth owner osig=""
  shift 2
  [ -s "$ops" ] && osig=$(gitc -c "gpg.ssh.allowedSignersFile=$ops" show --no-patch --format='%G?' "$sha" 2>/dev/null)
  for pth in "$@"; do
        case "$pth" in
          keys/*.pub|hostkeys/*.pub)
            if gitc cat-file -e "$sha:$pth" 2>/dev/null && ! key_blob_ok "$sha:$pth"; then
              echo "mesh: REJECTED $pth in ${sha%%??????????????????????????????????}..: not a single public key line" >&2
              bad=1
              continue
            fi
            ;;
        esac
        case "$pth" in
          operators)
            if [ -s "$ops" ]; then
              [ "$osig" = G ] && continue
              echo "mesh: REJECTED conf/operators: not signed by a current operator key" >&2
              bad=1
            elif [ -n "$signer" ] && grep -q "^$signer " "$tas"; then
              continue
            else
              echo "mesh: REJECTED conf/operators: bootstrap must come from a member" >&2
              bad=1
            fi
            continue
            ;;
          nodes/*.conf) owner=${pth#nodes/}; owner=${owner%.conf} ;;
          keys/*.pub|hostkeys/*.pub) owner=${pth#*/}; owner=${owner%.pub} ;;
          *)
            echo "mesh: REJECTED $pth: conf holds only nodes/, keys/, hostkeys/ and operators" >&2
            bad=1
            continue
            ;;
        esac
        case "$owner" in ''|*[!a-zA-Z0-9_-]*)
          echo "mesh: REJECTED $pth: node names must be [a-zA-Z0-9_-]" >&2
          bad=1
          continue
          ;;
        esac
        [ "$signer" = "$owner" ] && continue
        [ "$osig" = G ] && continue
        if ! grep -q "^$owner " "$tas" && [ -n "$signer" ] && grep -q "^$signer " "$tas"; then
          continue
        fi
        echo "mesh: REJECTED $pth in ${sha%%??????????????????????????????????}..: only '$owner' may write it, commit signed by '${signer:-nobody}'" >&2
        bad=1
  done
}

vouch() {
  gitc commit -q --allow-empty --no-verify -m "adopt $1" >/dev/null 2>&1 || \
    log "warning: could not sign the adoption of $1"
}

cmd_confcheck() {
  conf_check && exit 0
  exit 1
}

cmd_serve() {
  local c=${SSH_ORIGINAL_COMMAND:-} v a p
  ensure_dirs
  set -- $c
  v=${1:-}
  case "$v" in
    recs)
      tar -C "$STATE/records" -cf - . 2>/dev/null
      ;;
    probe)
      a=${2:-}
      p=${3:-}
      case "$p" in ''|*[!0-9]*) exit 1 ;; esac
      case "$a" in ''|*[!0-9a-fA-F.:]*) exit 1 ;; esac
      port_open "$a" "$p" || exit 1
      ;;
    git-upload-pack|git-receive-pack)
      case "$c" in
        "$v '.config/mesh/conf'"|"$v .config/mesh/conf") ;;
        *) log "serve: refused $c"; exit 1 ;;
      esac
      exec git "${v#git-}" ".config/mesh/conf"
      ;;
    *)
      log "serve: refused ${c:-<shell request>}"
      exit 1
      ;;
  esac
  exit 0
}

op_name() {
  local c
  c=$(cut -d' ' -f3 "$1" 2>/dev/null | tr -c 'a-zA-Z0-9_-' '_' | sed 's/_*$//')
  case "$c" in ''|*[!a-zA-Z0-9_-]*) printf 'operator\n' ;; *) printf '%s\n' "$c" ;; esac
}

op_add() {
  local name=$1 keyf=$2 key
  key=$(cut -d' ' -f1,2 "$keyf")
  [ -n "$key" ] || die "could not read a key from $keyf"
  touch "$OPERATORS"
  grep -v "^$name " "$OPERATORS" > "$OPERATORS.t" 2>/dev/null || true
  printf '%s %s\n' "$name" "$key" >> "$OPERATORS.t"
  sort -o "$OPERATORS" "$OPERATORS.t"
  rm -f "$OPERATORS.t"
}

op_sign_setup() {
  local as=$1
  [ -s "$OPERATORS" ] || die "no conf/operators here; this mesh is not initialized"
  [ -n "$as" ] || as=$(awk '{print $1; exit}' "$OPERATORS")
  OP_AS=$as
  OP_TMP="$STATE/tmp/op.$$.pub"
  awk -v p="$as" '$1==p{print $2" "$3}' "$OPERATORS" > "$OP_TMP"
  [ -s "$OP_TMP" ] || { rm -f "$OP_TMP"; die "operator '$as' is not in conf/operators"; }
}

cmd_evict() {
  local name="" as="" p
  while [ $# -gt 0 ]; do
    case "$1" in
      --as) as=$2; shift 2 ;;
      -*) die "unknown option: $1" ;;
      *) name=$1; shift ;;
    esac
  done
  [ -n "$name" ] || die "usage: mesh evict <node> [--as <operator>]"
  case "$name" in *[!a-zA-Z0-9_-]*) die "name must be [a-zA-Z0-9_-]" ;; esac
  ensure_dirs
  conf_is_repo || die "conf is not a git repo here"
  [ "$name" = "$(me)" ] && die "refusing to evict this node; run 'mesh reset' here instead"
  [ -f "$CONF/nodes/$name.conf" ] || is_member "$name" || die "no node '$name' in conf"
  op_sign_setup "$as"
  gitc rm -q --ignore-unmatch -- "nodes/$name.conf" "keys/$name.pub" "hostkeys/$name.pub" >/dev/null 2>&1
  if ! gitc -c "user.signingkey=$OP_TMP" commit -q --no-verify -m "evict $name" >/dev/null 2>&1; then
    rm -f "$OP_TMP"
    for p in "nodes/$name.conf" "keys/$name.pub" "hostkeys/$name.pub"; do
      gitc reset -q HEAD -- "$p" >/dev/null 2>&1
      gitc checkout -q -- "$p" >/dev/null 2>&1
    done
    die "could not sign the eviction with '$OP_AS' (is its key in your agent?)"
  fi
  rm -f "$OP_TMP" "$STATE/roster/$name" "$STATE/records/$name.rec" "$STATE/records/$name.rec.sig" \
    "$STATE/records/guest-$name.rec" "$STATE/hostkeys/$name.pub"
  gen_as
  gen_kh
  compile_ak
  compile_sc
  log "evicted $name, signed by operator '$OP_AS'; it loses access at each node's next sync"
  cmd_sync
}

cmd_operator() {
  local name="" keyf="" as="" tmp remove=0
  while [ $# -gt 0 ]; do
    case "$1" in
      --as) as=$2; shift 2 ;;
      --remove) remove=1; shift ;;
      -*) die "unknown option: $1" ;;
      *) if [ -z "$name" ]; then name=$1; else keyf=$1; fi; shift ;;
    esac
  done
  ensure_dirs
  if [ -z "$name" ]; then
    [ -s "$OPERATORS" ] || die "no conf/operators: nothing can open a shell on this mesh"
    printf 'operators:\n'
    awk '{printf "  %s %s...\n", $1, substr($3,1,24)}' "$OPERATORS"
    return 0
  fi
  [ -n "$keyf" ] || [ "$remove" -eq 1 ] || \
    die "usage: mesh operator [<name> <pubkey-file>] [--as <operator>]
     mesh operator <name> --remove [--as <operator>]"
  if [ "$remove" -eq 1 ]; then
    grep -q "^$name " "$OPERATORS" 2>/dev/null || die "no operator '$name'"
    [ "$(wc -l < "$OPERATORS")" -gt 1 ] || \
      die "'$name' is the only operator; removing it would leave no way to open
     a shell anywhere on this mesh. Record its replacement first."
  else
    [ -f "$keyf" ] || die "no such pubkey file: $keyf"
  fi
  op_sign_setup "$as"
  as=$OP_AS
  tmp=$OP_TMP
  if [ "$remove" -eq 1 ]; then
    grep -v "^$name " "$OPERATORS" > "$OPERATORS.t"
    mv "$OPERATORS.t" "$OPERATORS"
  else
    op_add "$name" "$keyf"
  fi
  if conf_is_repo; then
    gitc add operators >/dev/null 2>&1
    if ! gitc -c "user.signingkey=$tmp" commit -q --no-verify -m auto \
        >/dev/null 2>&1; then
      rm -f "$tmp"
      gitc reset -q HEAD -- operators >/dev/null 2>&1
      gitc checkout -q -- operators >/dev/null 2>&1
      die "could not sign the operators change with '$as' (is its key in your agent?)"
    fi
    log "committed, signed by operator '$as'"
  fi
  rm -f "$tmp"
  compile_ak
  conf_is_repo && cmd_sync
  if [ "$remove" -eq 1 ]; then
    log "operator '$name' removed; it loses access at each node's next compile"
  else
    log "operator '$name' recorded; it can open a shell on every member"
  fi
}

ak_rest() {
  sed "\|^$B1\$|,\|^$B2\$|d" "$AK" 2>/dev/null | grep -v '^[[:space:]]*$'
}

ak_drift() {
  [ -f "$STATE/ak.base" ] || return 1
  [ "$(sha256 "$(ak_rest)")" != "$(cat "$STATE/ak.base")" ]
}

ak_check() {
  if [ ! -f "$STATE/ak.base" ]; then
    sha256 "$(ak_rest)" > "$STATE/ak.base"
    return 0
  fi
  ak_drift || return 0
  log "WARNING: authorized_keys changed outside the managed block"
  log "inspect $AK, then accept with: rm $STATE/ak.base"
  command -v termux-notification >/dev/null 2>&1 && \
    timeout 30 termux-notification --id mesh-ak --group mesh \
      --title "mesh: authorized_keys drift" \
      --content "unmanaged keys changed on $(me)" >/dev/null 2>&1
  return 0
}

managed_write() {
  local file=$1 content=$2 t="$STATE/tmp/mw.$$"
  touch "$file"
  chmod 600 "$file"
  sed "\|^$B1\$|,\|^$B2\$|d" "$file" > "$t"
  if [ -n "$content" ]; then
    { printf '%s\n%s\n%s\n' "$B1" "$content" "$B2"; cat "$t"; } > "$file"
  else
    cat "$t" > "$file"
  fi
  rm -f "$t"
}

compile_ak() {
  local n t k out="" opts ops
  opts="restrict,port-forwarding,command=\"$SELF serve\" "
  for n in $(nodes); do
    [ -f "$CONF/keys/$n.pub" ] || continue
    out+="$opts$(head -n 1 "$CONF/keys/$n.pub" | cut -d' ' -f1,2)"$'\n'
  done
  if conf_is_repo && gitc cat-file -e HEAD:operators 2>/dev/null && \
      ! gitc diff --quiet HEAD -- operators 2>/dev/null; then
    log "WARNING: conf/operators differs from its last commit and is ignored; change it with 'mesh operator', or restore it: git -C $CONF checkout -- operators"
  fi
  ops=$(op_signers)
  if [ -n "$ops" ]; then
    while read -r _ t k; do
      [ -n "$k" ] && out+="$t $k"$'\n'
    done <<< "$ops"
  else
    log "WARNING: no conf/operators, so nothing can open a shell on this node"
  fi
  managed_write "$AK" "$out"
}

stanza() {
  local alias=$1 n=$2 extra=$3 user
  user=$(node_get "$n" user)
  case "$user" in ''|*[!a-zA-Z0-9._-]*)
    log "skipping $alias: bad or missing user="
    return 1
    ;;
  esac
  if ! is_member "$n" && ext_path "auth-$n" >/dev/null; then
    printf 'Match originalhost %s exec "%s auth %s"\n' "$alias" "$SELF" "$n"
    printf '  IdentityFile %s/auth/%s/current\n  IdentitiesOnly yes\n' "$STATE" "$n"
  fi
  printf 'Host %s\n' "$alias"
  printf '  User %s\n  HostKeyAlias %s\n  UserKnownHostsFile %s\n' "$user" "$n" "$KH"
  printf '  StrictHostKeyChecking yes\n'
  is_member "$n" && printf '  IdentitiesOnly no\n'
  printf '  ProxyCommand %s dial %s\n' "$SELF" "$n"
  if [ "$alias" = "$n" ]; then
    printf '  ControlMaster auto\n  ControlPath %s/cm-%%n\n  ControlPersist 30\n' "$STATE"
  else
    printf '  ControlMaster no\n  ControlPath none\n  ExitOnForwardFailure yes\n'
  fi
  printf '  ServerAliveInterval 15\n  ServerAliveCountMax 4\n'
  [ -n "$extra" ] && printf '%s\n' "$extra"
  return 0
}

stanza_self() {
  local m=$1 user port
  user=$(node_get "$m" user)
  port=$(node_get "$m" port)
  case "$user" in ''|*[!a-zA-Z0-9._-]*) return 1 ;; esac
  printf 'Host %s\n  HostName 127.0.0.1\n  Port %s\n  User %s\n' "$m" "$port" "$user"
  printf '  HostKeyAlias %s\n  UserKnownHostsFile %s\n  StrictHostKeyChecking yes\n' "$m" "$KH"
  printf '  IdentityFile %s\n  IdentitiesOnly yes\n' "$STATE/key"
}

compile_sc() {
  local n block="" st sfx lp rh rp
  st=$(stanza_self "$(me)") && block+="$st"$'\n\n'
  for n in $(peers); do
    st=$(stanza "$n" "$n" "") && block+="$st"$'\n\n'
    while IFS=: read -r sfx lp rh rp; do
      case "$sfx" in ''|*[!a-zA-Z0-9_-]*) continue ;; esac
      case "$rh" in ''|*[!a-zA-Z0-9.:_-]*) continue ;; esac
      case "$lp" in ''|*[!0-9]*) continue ;; esac
      case "$rp" in ''|*[!0-9]*) continue ;; esac
      st=$(stanza "$n-$sfx" "$n" \
        "  LocalForward 127.0.0.1:$lp $rh:$rp") && block+="$st"$'\n\n'
    done < <(node_get_all "$n" fwd)
  done
  managed_write "$SC" "$block"
}

cmd_publish() {
  local m f a s
  m=$(me)
  f="$STATE/records/$m.rec"
  s=$(node_get "$m" static)
  { printf 'name=%s\nseq=%s\n' "$m" "$(date +%s)"
    for a in $(p_addrs); do printf 'ep=%s\n' "$a"; done
    [ -n "$s" ] && printf 'ep=%s\n' "$s"
  } > "$f"
  rm -f "$f.sig"
  if ! ssh-keygen -Y sign -f "$STATE/key.pub" -n "$NS" "$f" >/dev/null 2>&1; then
    rm -f "$f"
    log "record signing failed (no key on disk and none in the agent), skipping publish"
    return 1
  fi
  return 0
}

merge_guest_record() {
  local f=$1 g by sq old
  g=$(awk -F= '$1=="guest"{print $2;exit}' "$f")
  by=$(awk -F= '$1=="by"{print $2;exit}' "$f")
  case "$g" in ''|*[!a-zA-Z0-9_-]*) return 1 ;; esac
  case "$by" in ''|*[!a-zA-Z0-9_-]*) return 1 ;; esac
  [ -f "$CONF/keys/$by.pub" ] || return 1
  ssh-keygen -Y verify -f "$AS" -I "$by" -n "$NS" -s "$f.sig" \
    < "$f" >/dev/null 2>&1 || { log "bad signature on guest record from $by"; return 1; }
  sq=$(awk -F= '$1=="seq"{print $2;exit}' "$f")
  old=$(awk -F= '$1=="seq"{print $2;exit}' "$STATE/records/guest-$g.rec" 2>/dev/null)
  if [ "${sq:-0}" -gt "${old:-0}" ] 2>/dev/null; then
    mv "$f" "$STATE/records/guest-$g.rec"
    mv "$f.sig" "$STATE/records/guest-$g.rec.sig"
    return 0
  fi
  return 1
}

merge_in() {
  local f n sq old
  for f in "$STATE/in"/*.rec; do
    [ -f "$f" ] || continue
    if grep -q '^guest=' "$f" 2>/dev/null; then
      merge_guest_record "$f" || rm -f "$f" "$f.sig"
      continue
    fi
    n=$(awk -F= '$1=="name"{print $2;exit}' "$f")
    case "$n" in ''|*[!a-zA-Z0-9_-]*) rm -f "$f" "$f.sig"; continue ;; esac
    [ "$n" = "$(me)" ] && { rm -f "$f" "$f.sig"; continue; }
    [ -f "$CONF/keys/$n.pub" ] || { rm -f "$f" "$f.sig"; continue; }
    if ! ssh-keygen -Y verify -f "$AS" -I "$n" -n "$NS" -s "$f.sig" \
        < "$f" >/dev/null 2>&1; then
      log "bad signature on record for $n, discarded"
      rm -f "$f" "$f.sig"
      continue
    fi
    sq=$(awk -F= '$1=="seq"{print $2;exit}' "$f")
    old=$(awk -F= '$1=="seq"{print $2;exit}' "$STATE/records/$n.rec" 2>/dev/null)
    if [ "${sq:-0}" -gt "${old:-0}" ] 2>/dev/null; then
      mv "$f" "$STATE/records/$n.rec"
      mv "$f.sig" "$STATE/records/$n.rec.sig"
    else
      rm -f "$f" "$f.sig"
    fi
  done
  rm -f "$STATE/in"/*
}

exchange() {
  local n port user r
  local -a popts
  for n in $(peers); do
    is_member "$n" || continue
    r=$(cat "$STATE/roster/$n" 2>/dev/null)
    [ -n "$r" ] || continue
    port=$(node_get "$n" port)
    user=$(node_get "$n" user)
    popts=()
    case "$r" in
      jump\ *) popts=(-o "ProxyCommand=$SELF dial $n"); r=$n ;;
    esac
    ssh "${SOPTS[@]}" "${popts[@]}" -o "HostKeyAlias=$n" -p "$port" \
      "$user@$r" recs </dev/null 2>/dev/null |
      tar -C "$STATE/in" -xf - 2>/dev/null
    merge_in
  done
}

ident() {
  local a=$1 p=$2 t k f hk
  ssh-keyscan -T "$CTO" -p "$p" "$a" 2>/dev/null | while read -r _ t k; do
    for f in "$CONF/hostkeys"/*.pub; do
      [ -f "$f" ] || continue
      hk=$(cut -d' ' -f1,2 "$f")
      if [ "$hk" = "$t $k" ]; then
        f=${f##*/}
        printf '%s\n' "${f%.pub}"
      fi
    done
  done | sort -u
}

relay_probe() {
  local b=$1 baddr=$2 a=$3 p=$4 bport buser
  bport=$(node_get "$b" port)
  buser=$(node_get "$b" user)
  timeout "$((CTO * 3))" ssh "${SOPTS[@]}" -o "HostKeyAlias=$b" -p "$bport" \
    "$buser@$baddr" "probe $a $p" </dev/null >/dev/null 2>&1
}

resolve_one() {
  local n=$1 port cands ordered r s a b baddr
  port=$(node_get "$n" port)
  cands=""
  r=$(cat "$STATE/roster/$n" 2>/dev/null)
  case "$r" in ''|jump\ *) ;; *) cands="$r" ;; esac
  s=$(node_get "$n" static)
  [ -n "$s" ] && cands="$cands $s"
  [ -f "$STATE/records/$n.rec" ] && \
    cands="$cands $(awk -F= '$1=="ep"{print $2}' "$STATE/records/$n.rec" | tr '\n' ' ')"
  cands=$(printf '%s\n' $cands | awk '!x[$0]++')
  ordered=""
  for a in $cands; do is_private "$a" && ordered="$ordered $a"; done
  for a in $cands; do is_private "$a" || ordered="$ordered $a"; done
  for a in $ordered; do
    port_open "$a" "$port" || continue
    ident "$a" "$port" | grep -qx "$n" || continue
    printf '%s\n' "$a" > "$STATE/roster/$n"
    log "$n direct at $a"
    return 0
  done
  for b in $(peers); do
    [ "$b" = "$n" ] && continue
    is_member "$b" || continue
    baddr=$(cat "$STATE/roster/$b" 2>/dev/null)
    case "$baddr" in ''|jump\ *) continue ;; esac
    for a in $ordered; do
      relay_probe "$b" "$baddr" "$a" "$port" || continue
      printf 'jump %s %s %s\n' "$b" "$baddr" "$a" > "$STATE/roster/$n"
      log "$n relayed through $b at $a"
      return 0
    done
  done
  rm -f "$STATE/roster/$n"
  return 1
}

resolve() {
  local n
  for n in $(peers); do
    is_member "$n" || continue
    resolve_one "$n" || true
  done
  for n in $(peers); do
    is_member "$n" || continue
    [ -f "$STATE/roster/$n" ] && continue
    resolve_one "$n" || log "$n unreachable"
  done
  return 0
}

sweep() {
  command -v nmap >/dev/null 2>&1 || return 0
  local m n missing="" subs ports ip p nm hit
  m=$(me)
  for n in $(peers); do
    is_member "$n" || continue
    [ -f "$STATE/roster/$n" ] || missing="$missing $n"
  done
  [ -n "${missing// /}" ] || return 0
  subs=$(p_subnets)
  [ -n "${subs// /}" ] || return 0
  ports=$(for n in $(nodes); do node_get "$n" port; done | sort -un | paste -sd, -)
  log "sweeping $subs for$missing"
  while read -r ip; do
    for p in ${ports//,/ }; do
      port_open "$ip" "$p" || continue
      hit=""
      for nm in $(ident "$ip" "$p"); do
        [ "$nm" = "$m" ] && continue
        is_member "$nm" || continue
        printf '%s\n' "$ip" > "$STATE/roster/$nm"
        log "found $nm at $ip"
        hit=1
      done
      [ -n "$hit" ] && break
    done
  done < <(exec 4>&-; timeout "${MESH_SWEEP_TIMEOUT:-300}" \
    nmap -p"$ports" --open -oG - $subs 2>/dev/null |
    awk '/Host:/{print $2}' | sort -u)
}

pipe_to() {
  local n=$1 r=$2 port=$3 b p baddr bport buser cp reg iid acmd key
  local -a jopts
  case "$r" in
    jump\ *)
      set -- $r
      b=$2
      baddr=$3
      p=$4
      bport=$(node_get "$b" port)
      buser=$(node_get "$b" user)
      if is_member "$b"; then
        jopts=("${SOPTS[@]}")
      else
        jopts=("${GOPTS[@]}")
      fi
      if acmd=$(ext_path "auth-$b"); then
        key=$("$acmd" "$baddr") || die "mesh-auth-$b failed for $b"
        jopts+=(-i "$key" -o IdentitiesOnly=yes)
      fi
      case "$baddr" in
        ssm:*)
          IFS=: read -r _ reg iid <<< "$baddr"
          jopts+=(-o "ProxyCommand=aws ssm start-session --region $reg --target %h --document-name AWS-StartSSHSession --parameters portNumber=%p")
          baddr=$iid
          ;;
      esac
      exec ssh "${jopts[@]}" -o "HostKeyAlias=$b" -p "$bport" \
        -W "$p:$port" "$buser@$baddr"
      ;;
    ssm:*)
      IFS=: read -r _ reg iid <<< "$r"
      exec aws ssm start-session --region "$reg" --target "$iid" \
        --document-name AWS-StartSSHSession --parameters "portNumber=$port"
      ;;
    *)
      if command -v nc >/dev/null 2>&1; then
        exec nc "$r" "$port"
      fi
      port_open "$r" "$port" || die "connect to $n failed"
      exec 3<>"/dev/tcp/$r/$port"
      cat <&3 &
      cp=$!
      cat >&3
      kill "$cp" 2>/dev/null
      ;;
  esac
}

cmd_auth() {
  local n=$1 acmd addr key
  [ -n "$n" ] || die "usage: mesh auth <guest>"
  [ -f "$CONF/nodes/$n.conf" ] || die "unknown guest: $n"
  [ -S "$STATE/cm-$n" ] && return 0
  ensure_dirs
  acmd=$(ext_path "auth-$n") || die "no 'mesh-auth-$n' here"
  [ -z "$(node_get "$n" jump)" ] || die "$n is reached through a jump"
  addr=$(guest_addr "$n") || die "resolver for $n failed"
  key=$("$acmd" "$addr") || die "mesh-auth-$n failed for $n"
  mkdir -p "$STATE/auth/$n"
  ln -sfn "$key" "$STATE/auth/$n/current"
}

member_addr() {
  local n=$1 r
  r=$(cat "$STATE/roster/$n" 2>/dev/null)
  case "$r" in jump\ *) r="" ;; esac
  if [ -z "$r" ] || ! port_open "$r" "$(node_get "$n" port)"; then
    resolve_one "$n" >/dev/null 2>&1
    r=$(cat "$STATE/roster/$n" 2>/dev/null)
    case "$r" in jump\ *) r="" ;; esac
  fi
  [ -n "$r" ] || r=$(node_get "$n" static)
  printf '%s\n' "$r"
}

cmd_dial() {
  local n=$1 r port bport jump addr jaddr
  [ -n "$n" ] || die "usage: mesh dial <peer>"
  [ -f "$CONF/nodes/$n.conf" ] || die "unknown peer: $n"
  ensure_dirs
  port=$(node_get "$n" port)
  if ! is_member "$n"; then
    jump=$(node_get "$n" jump)
    if [ -z "$jump" ]; then
      addr=$(guest_addr "$n") || die "resolver for $n failed"
      [ -n "$addr" ] || die "no address for $n"
      pipe_to "$n" "$addr" "$port"
      exit 0
    fi
    [ -f "$CONF/nodes/$jump.conf" ] || die "unknown jump node: $jump"
    addr=$(node_get "$n" static)
    [ -n "$addr" ] || die "no address for $n"
    if [ "$jump" = "$(me)" ]; then
      pipe_to "$n" "$addr" "$port"
      exit 0
    fi
    if is_member "$jump"; then
      jaddr=$(member_addr "$jump")
    else
      jaddr=$(guest_addr "$jump") || \
        die "jump $jump unresolvable (is it launched?)"
    fi
    [ -n "$jaddr" ] || die "jump $jump has no address"
    pipe_to "$n" "jump $jump $jaddr $addr" "$port"
    exit 0
  fi
  r=$(cat "$STATE/roster/$n" 2>/dev/null)
  case "$r" in
    '') ;;
    jump\ *)
      set -- $r
      bport=$(node_get "$2" port)
      port_open "$3" "$bport" || r=""
      ;;
    *)
      port_open "$r" "$port" || r=""
      ;;
  esac
  if [ -z "$r" ]; then
    log "no live route to $n, re-resolving"
    if ! resolve_one "$n"; then
      exchange
      resolve_one "$n" || sweep
    fi
    r=$(cat "$STATE/roster/$n" 2>/dev/null)
  fi
  [ -n "$r" ] || die "cannot reach $n"
  rm -f "$STATE/last-error"
  pipe_to "$n" "$r" "$port"
}

cmd_init() {
  local name="" static="" port="" user="" token="" opkey="" opname=""
  local old e hk joining=0
  while [ $# -gt 0 ]; do
    case "$1" in
      --static) static=$2; shift 2 ;;
      --port) port=$2; shift 2 ;;
      --user) user=$2; shift 2 ;;
      --token) token=$2; shift 2 ;;
      --operator) opkey=$2; shift 2 ;;
      -*) die "unknown option: $1" ;;
      *) name=$1; shift ;;
    esac
  done
  [ -n "$name" ] || \
    die "usage: mesh init <new-name> --operator <pubkey-file> [--static addr] [--port p] [--user u]"
  case "$name" in *[!a-zA-Z0-9_-]*) die "name must be [a-zA-Z0-9_-]" ;; esac
  old=$(me)
  [ -n "$old" ] && [ "$old" != "$name" ] && die "already initialized as $old"
  have_git || die "git is required (pkg install git / apt install git)"
  ensure_dirs
  ls "$CONF"/keys/*.pub >/dev/null 2>&1 && joining=1
  if [ -s "$OPERATORS" ]; then
    [ -n "$opkey" ] && die "this mesh already has operators; add one with 'mesh operator'"
  else
    [ -n "$opkey" ] || die "a new mesh needs an operator key: mesh init $name --operator <pubkey-file>
     Device keys never open a shell. Shells come from this key, which belongs
     in an ssh-agent and not on disk. Make one with:
       ssh-keygen -t ed25519 -C <label> -f <path>"
    [ -f "$opkey" ] || die "no such pubkey file: $opkey"
    opname=$(op_name "$opkey")
  fi
  if ! conf_is_repo; then
    if [ "$joining" -eq 0 ]; then
      log "no conf here, so this starts a BRAND NEW mesh."
      log "to join an existing one instead, stop now, copy a member's"
      log "~/.config/mesh/conf here (with its .git), and re-run."
    fi
    gitc init -q -b main
  fi
  gitc config user.name "$name"
  gitc config user.email "$name@mesh"
  gitc config receive.denyCurrentBranch updateInstead
  install_hook
  [ -n "$port" ] || { is_termux && port=8022 || port=22; }
  [ -n "$user" ] || user=$(id -un)
  case "$port" in ''|*[!0-9]*) die "port must be numeric" ;; esac
  case "$user" in ''|*[!a-zA-Z0-9._-]*) die "user must be [a-zA-Z0-9._-]" ;; esac
  [ -f "$STATE/key" ] || ssh-keygen -q -t ed25519 -N "" -f "$STATE/key" -C "$name" || \
    die "keygen failed"
  set_identity
  printf '%s\n' "$name" > "$STATE/name"
  cp "$STATE/key.pub" "$CONF/keys/$name.pub"
  { printf 'user=%s\nport=%s\n' "$user" "$port"
    [ -n "$static" ] && printf 'static=%s\n' "$static"
    [ -n "$token" ] && printf 'proof=%s\n' \
      "$(sha256 "$token$(cut -d' ' -f2 "$STATE/key.pub")")"
  } > "$CONF/nodes/$name.conf"
  [ -n "$opname" ] && op_add "$opname" "$opkey"
  p_ensure_sshd || log "warning: sshd not confirmed on port $port"
  if hk=$(p_hostkey_pub); then
    printf '%s\n' "$hk" > "$CONF/hostkeys/$name.pub"
  else
    log "warning: no host key found, write $CONF/hostkeys/$name.pub yourself"
  fi
  gen_as
  gen_kh
  compile_ak
  compile_sc
  conf_is_repo && cmd_sync
  log "initialized $name (user=$user port=$port${static:+ static=$static})"
  [ -n "$opname" ] && \
    log "operator '$opname' recorded; keep its private half in an agent, not on disk"
  if [ "$joining" -eq 1 ]; then
    for e in $(p_addrs); do
      is_private "$e" || continue
      log "to finish joining, run this on any existing member:"
      log "  mesh adopt $user@$e --port $port"
      break
    done
  fi
  log "then: mesh schedule (without it this node goes stale to peers)"
}

cmd_guest() {
  local name="" addr="" port=22 user="" jump="" fwds="" dynamic=0 hk fw sfx lp rh rp
  while [ $# -gt 0 ]; do
    case "$1" in
      --port) port=$2; shift 2 ;;
      --user) user=$2; shift 2 ;;
      --jump) jump=$2; shift 2 ;;
      --dynamic) dynamic=1; shift ;;
      --fwd) fwds="$fwds$2"$'\n'; shift 2 ;;
      -*) die "unknown option: $1" ;;
      *)
        if [ -z "$name" ]; then name=$1; else addr=$1; fi
        shift
        ;;
    esac
  done
  [ -n "$name" ] || \
    die "usage: mesh guest <name> [<addr>] [--port p] [--user u] [--jump n] [--dynamic] [--fwd sfx:lport:rhost:rport]"
  [ -n "$addr" ] || [ "$dynamic" -eq 1 ] || die "need an address or --dynamic"
  case "$name" in *[!a-zA-Z0-9_-]*) die "name must be [a-zA-Z0-9_-]" ;; esac
  [ -f "$CONF/keys/$name.pub" ] && die "$name is a member, not a guest"
  [ -n "$user" ] || user=$(id -un)
  case "$port" in ''|*[!0-9]*) die "port must be numeric" ;; esac
  case "$user" in ''|*[!a-zA-Z0-9._-]*) die "user must be [a-zA-Z0-9._-]" ;; esac
  while IFS=: read -r sfx lp rh rp; do
    [ -n "$sfx$lp$rh$rp" ] || continue
    case "$sfx" in ''|*[!a-zA-Z0-9_-]*) die "fwd must be <sfx>:<lport>:<rhost>:<rport>, got $sfx:$lp:$rh:$rp" ;; esac
    case "$rh" in ''|*[!a-zA-Z0-9.:_-]*) die "fwd must be <sfx>:<lport>:<rhost>:<rport>, got $sfx:$lp:$rh:$rp" ;; esac
    case "$lp$rp" in ''|*[!0-9]*) die "fwd must be <sfx>:<lport>:<rhost>:<rport>, got $sfx:$lp:$rh:$rp" ;; esac
    [ -n "$lp" ] && [ -n "$rp" ] || die "fwd must be <sfx>:<lport>:<rhost>:<rport>, got $sfx:$lp:$rh:$rp"
  done <<EOF
$fwds
EOF
  ensure_dirs
  { printf 'user=%s\nport=%s\n' "$user" "$port"
    [ -n "$addr" ] && printf 'static=%s\n' "$addr"
    [ -n "$jump" ] && printf 'jump=%s\n' "$jump"
    [ "$dynamic" -eq 1 ] && printf 'dynamic=1\n'
    while read -r fw; do
      [ -n "$fw" ] && printf 'fwd=%s\n' "$fw"
    done <<EOF
$fwds
EOF
  } > "$CONF/nodes/$name.conf"
  [ -n "$jump" ] && [ ! -f "$CONF/nodes/$jump.conf" ] && \
    log "warning: jump node $jump not in conf yet"
  if [ -f "$CONF/hostkeys/$name.pub" ]; then
    log "host key already pinned, leaving it alone"
    log "  $(ssh-keygen -lf "$CONF/hostkeys/$name.pub" 2>/dev/null)"
  elif [ -n "$jump" ]; then
    log "reached via $jump, so no direct keyscan; pin its host key with:"
    log "  ssh $jump -- ssh-keyscan -T 5 $addr"
    log "  then write the 'type key' fields into $CONF/hostkeys/$name.pub"
  elif [ -n "$addr" ]; then
    hk=$(ssh-keyscan -T "$CTO" -p "$port" "$addr" 2>/dev/null |
      awk '$2=="ssh-ed25519"{print $2" "$3; exit}')
    [ -n "$hk" ] || hk=$(ssh-keyscan -T "$CTO" -p "$port" "$addr" 2>/dev/null |
      awk 'NF>=3{print $2" "$3; exit}')
    if [ -n "$hk" ]; then
      printf '%s\n' "$hk" > "$CONF/hostkeys/$name.pub"
      log "pinned host key: $(ssh-keygen -lf "$CONF/hostkeys/$name.pub" 2>/dev/null)"
      log "verify that fingerprint out of band"
    else
      log "warning: keyscan failed (firewalled?); pin the host key yourself:"
      log "  ssh-keyscan -p $port $addr (from a host that can reach it)"
      log "  then write 'type key' into $CONF/hostkeys/$name.pub"
    fi
  else
    log "host key will be pinned by the resolver at first dial"
    [ "$dynamic" -eq 1 ] && \
      log "each device that should reach $name needs 'mesh-resolve-$name' on PATH"
  fi
  gen_kh
  compile_sc
  conf_is_repo && cmd_sync
  log "guest $name added ($user@${addr:-<resolved>}:$port${jump:+ via $jump})"
}

cmd_tick() {
  [ -n "$(me)" ] || die "not initialized, run: mesh init <name>"
  ensure_dirs
  exec 9>"$STATE/lock"
  flock -n 9 || { log "tick already running"; exec 9>&-; return 0; }
  p_ensure_sshd || log "warning: sshd not confirmed"
  if conf_is_repo; then
    cmd_sync || log "continuing with local conf"
  else
    log "WARNING: $CONF is not a git repo, so this node neither sends nor takes conf changes; restore it by copying a member's conf dir with its .git, then run 'mesh init $(me)'"
  fi
  gen_as
  gen_kh
  compile_ak
  cmd_publish
  exchange
  resolve
  sweep
  if [ -n "$(invites_active)" ]; then
    if command -v nmap >/dev/null 2>&1; then
      log "invite outstanding, scanning for the new device"
      adopt_scan 1
    else
      log "invite outstanding, nmap missing, so no scan; a join against 'invite --serve' still completes"
    fi
  fi
  conf_is_repo && cmd_sync
  compile_ak
  compile_sc
  ak_check
  log "roster: $(cd "$STATE/roster" 2>/dev/null && ls | tr '\n' ' ')"
  unresolved_members || rm -f "$STATE/last-error"
  exec 9>&-
}

adopt_gs() { printf 'ssh -p %s -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=%s -o IdentitiesOnly=yes -i %s' "$1" "$CTO" "$STATE/key"; }

verify_proof() {
  local n=$1 proof blob t
  proof=$(gitc show "FETCH_HEAD:nodes/$n.conf" 2>/dev/null |
    awk -F= '$1=="proof"{print $2; exit}')
  [ -n "$proof" ] || return 1
  blob=$(gitc show "FETCH_HEAD:keys/$n.pub" 2>/dev/null | cut -d' ' -f2)
  [ -n "$blob" ] || return 1
  for t in $(invites_active); do
    if [ "$(sha256 "$t$blob")" = "$proof" ]; then
      printf '%s\n' "$t"
      return 0
    fi
  done
  return 1
}

strip_proof() {
  local f="$CONF/nodes/$1.conf" t="$STATE/tmp/np.$$"
  [ -f "$f" ] || return 0
  grep -q '^proof=' "$f" || return 0
  grep -v '^proof=' "$f" > "$t" && mv "$t" "$f"
}

conf_check_fetched() {
  local br=$1 new
  shift
  new=$(gitc rev-parse FETCH_HEAD 2>/dev/null)
  case "$new" in
    ''|*[!0-9a-f]*) log "nothing fetched to verify, refusing"; return 1 ;;
  esac
  printf '%s %s refs/heads/%s\n' "$(gitc rev-parse HEAD)" "$new" "$br" | conf_check "$@"
}

adopt_finish() {
  local br
  br=$(gitc symbolic-ref --short HEAD 2>/dev/null)
  [ -n "$br" ] || br=main
  if ! conf_check_fetched "$br" "$@"; then
    log "conf from the new node failed verification, not adopted"
    return 1
  fi
  if ! gitc merge -q --no-edit FETCH_HEAD >/dev/null 2>&1; then
    gitc merge --abort >/dev/null 2>&1
    log "conf merge failed (histories differ? copy the conf dir with .git)"
    return 1
  fi
  gen_as
  gen_kh
  compile_ak
  compile_sc
  return 0
}

adopt_scan() {
  local auto=$1 br subs ports users ip p u n hit fp found=0 mine tok ok
  have_git || die "git is required"
  command -v nmap >/dev/null 2>&1 || die "nmap is required for --scan"
  ensure_dirs
  br=$(gitc symbolic-ref --short HEAD 2>/dev/null)
  [ -n "$br" ] || br=main
  users=$({ for n in $(nodes); do node_get "$n" user; done; id -un; } |
    grep -v '^$' | sort -u | tr '\n' ' ')
  ports=$({ for n in $(nodes); do node_get "$n" port; done; printf '22\n8022\n'; } |
    grep -E '^[0-9]+$' | sort -un | paste -sd, -)
  mine=$(p_addrs | tr '\n' ' ')
  subs=$(p_subnets)
  [ -n "${subs// /}" ] || die "no local subnets to scan"
  log "scanning $subs on ports $ports"
  while read -r ip; do
    case " $mine " in *" $ip "*) continue ;; esac
    for p in ${ports//,/ }; do
      port_open "$ip" "$p" || continue
      [ -n "$(ident "$ip" "$p")" ] && continue
      for u in $users; do
        GIT_SSH_COMMAND=$(adopt_gs "$p") gitc fetch -q "$u@$ip:.config/mesh/conf" \
          "$br" >/dev/null 2>&1 || continue
        hit=""
        for n in $(gitc ls-tree --name-only FETCH_HEAD nodes/ 2>/dev/null |
            sed 's|^nodes/||; s|\.conf$||'); do
          [ -f "$CONF/nodes/$n.conf" ] || hit="$hit $n"
        done
        found=$((found + 1))
        if [ -z "${hit// /}" ]; then
          log "$u@$ip:$p answers but adds nothing new"
          break
        fi
        log "found new node(s)$hit at $u@$ip:$p"
        ok=""
        for n in $hit; do
          fp=$(gitc show "FETCH_HEAD:keys/$n.pub" 2>/dev/null |
            ssh-keygen -lf - 2>/dev/null | awk '{print $2}')
          if [ -z "$fp" ]; then
            log "  guest $n (dial-only, no key)"
            continue
          fi
          if tok=$(verify_proof "$n"); then
            log "  member $n, key $fp, VALID INVITE"
            ok="$ok $n:$tok"
          else
            log "  member $n, key $fp, no invite"
          fi
        done
        if [ "$auto" = 1 ] && [ -n "${ok// /}" ]; then
          if adopt_finish $(for n in $ok; do printf '%s ' "${n%%:*}"; done); then
            for n in $ok; do
              tok=${n#*:}
              n=${n%%:*}
              rm -f "$STATE/invites/$tok"
              vouch "$n"
              log "  adopted $n (invite burned)"
            done
            cmd_sync
          else
            log "  merge failed, not adopted"
          fi
        else
          log "  adopt with: mesh adopt $u@$ip --port $p"
        fi
        break
      done
    done
  done < <(nmap -p"$ports" --open -oG - $subs 2>/dev/null |
    awk '/Host:/{print $2}' | sort -u)
  [ "$found" -eq 0 ] && log "no un-enrolled mesh devices found"
  return 0
}

cmd_adopt() {
  local tgt="" port=22 br gs scan=0 auto=0 new n
  while [ $# -gt 0 ]; do
    case "$1" in
      --scan) scan=1; shift ;;
      --auto) scan=1; auto=1; shift ;;
      --port) port=$2; shift 2 ;;
      -*) die "unknown option: $1" ;;
      *) tgt=$1; shift ;;
    esac
  done
  if [ "$scan" -eq 1 ]; then
    [ -n "$(me)" ] || die "not initialized"
    conf_is_repo || die "conf is not a git repo"
    adopt_scan "$auto"
    return 0
  fi
  [ -n "$tgt" ] || die "usage: mesh adopt <user@host> [--port p] | mesh adopt --scan"
  case "$port" in ''|*[!0-9]*) die "port must be numeric" ;; esac
  [ -n "$(me)" ] || die "not initialized"
  conf_is_repo || die "conf is not a git repo"
  ensure_dirs
  br=$(gitc symbolic-ref --short HEAD 2>/dev/null)
  [ -n "$br" ] || br=main
  gs=$(adopt_gs "$port")
  if ! GIT_SSH_COMMAND="$gs" gitc fetch -q "$tgt:.config/mesh/conf" "$br" \
      >/dev/null 2>&1; then
    die "could not fetch conf from $tgt (is it initialized, and does its
     authorized_keys already list this node? run 'mesh tick' there)"
  fi
  new=$(gitc diff --name-only --diff-filter=A HEAD FETCH_HEAD -- keys 2>/dev/null | sed 's#^keys/##; s#\.pub$##' | tr '\n' ' ')
  adopt_finish $new || die "conf from $tgt failed verification or merge; copy the conf dir with .git"
  for n in $new; do vouch "$n"; done
  cmd_sync
  log "adopted conf from $tgt; new members are live here and gossiping onward"
}

cmd_reset() {
  local all=0
  [ "$1" = "--all" ] && all=1
  ensure_dirs
  managed_write "$AK" ""
  managed_write "$SC" ""
  if command -v termux-job-scheduler >/dev/null 2>&1; then
    termux-job-scheduler --cancel --job-id 3388 >/dev/null 2>&1
  fi
  rm -f "$HOME/.termux/boot/start-mesh.sh"
  if is_termux && [ -d "$(tx_prefix)/var/service/mesh" ]; then
    sv-disable mesh >/dev/null 2>&1
    sv down mesh >/dev/null 2>&1
    rm -rf "$(tx_prefix)/var/service/mesh"
    log "runsv service removed"
  fi
  if have_systemd_user && [ -f "$HOME/.config/systemd/user/mesh.service" ]; then
    systemctl --user disable --now mesh.service >/dev/null 2>&1
    rm -f "$HOME/.config/systemd/user/mesh.service"
    systemctl --user daemon-reload >/dev/null 2>&1
    log "systemd user unit removed"
  fi
  if command -v crontab >/dev/null 2>&1; then
    crontab -l 2>/dev/null | grep -v "mesh daemon" | grep -v "mesh tick" |
      crontab - >/dev/null 2>&1
  fi
  rm -rf "$STATE"
  if [ "$all" -eq 1 ]; then
    rm -rf "$CONF"
    log "reset complete: state, conf, managed blocks, schedules all removed"
  else
    log "reset complete: state, managed blocks, schedules removed (conf kept)"
  fi
}

route_of() {
  local r
  r=$(cat "$STATE/roster/$1" 2>/dev/null) || return 1
  [ -n "$r" ] || return 1
  case "$r" in jump\ *) set -- $r; r="$4 relayed through $2" ;; esac
  printf '%s\n' "$r"
}

cmd_status() {
  local m n now r age port sfx lp rh rp sq by note pin
  m=$(me)
  [ -n "$m" ] || die "not initialized"
  ensure_dirs
  now=$(date +%s)
  port=$(node_get "$m" port)
  printf 'node\n'
  printf '  %-14s %s@%s  profile=%s\n' "$m" "$(node_get "$m" user)" "$port" \
    "$(p_profile)"
  if port_open 127.0.0.1 "$port"; then
    printf '  sshd listening on %s\n' "$port"
  else
    printf '  sshd NOT listening on %s\n' "$port"
  fi
  printf '\nmembers\n'
  for n in $(peers); do
    is_member "$n" || continue
    if r=$(route_of "$n"); then
      age=$(( (now - $(mtime "$STATE/roster/$n")) / 60 ))
      printf '  %-14s %-30s %sm ago\n' "$n" "$r" "$age"
    else
      printf '  %-14s unresolved (dial re-resolves on use)\n' "$n"
    fi
  done
  printf '\nguests\n'
  for n in $(peers); do
    is_member "$n" && continue
    if [ "$(node_get "$n" dynamic)" = 1 ]; then
      note="dynamic"
      ext_path "resolve-$n" >/dev/null && note="$note, resolver script present"
      if [ -f "$STATE/records/guest-$n.rec" ]; then
        sq=$(awk -F= '$1=="seq"{print $2;exit}' "$STATE/records/guest-$n.rec")
        by=$(awk -F= '$1=="by"{print $2;exit}' "$STATE/records/guest-$n.rec")
        case "$sq" in ''|*[!0-9]*) sq=0 ;; esac
        if [ $((now - sq)) -le "${MESH_GUEST_TTL:-3600}" ]; then
          note="$note, record $(( (now - sq) / 60 ))m old (by $by)"
        else
          note="$note, record stale (refreshes from peers at dial)"
        fi
      else
        note="$note, no record yet"
      fi
    else
      note="$(node_get "$n" static)"
    fi
    r=$(node_get "$n" jump)
    [ -n "$r" ] && note="$note, via $r"
    if [ -s "$CONF/hostkeys/$n.pub" ] || [ -s "$STATE/hostkeys/$n.pub" ]; then
      pin="pinned"
    elif [ "$(node_get "$n" dynamic)" = 1 ]; then
      pin="pin-at-dial"
    else
      pin="UNPINNED"
    fi
    printf '  %-14s %s@%s  %s  %s\n' "$n" "$(node_get "$n" user)" \
      "$(node_get "$n" port)" "$pin" "$note"
  done
  printf '\nforwards\n'
  for n in $(peers); do
    is_member "$n" && continue
    while IFS=: read -r sfx lp rh rp; do
      [ -n "$sfx" ] && [ -n "$lp" ] || continue
      printf '  %-18s 127.0.0.1:%-6s -> %s:%s\n' "$n-$sfx" "$lp" "$rh" "$rp"
    done < <(node_get_all "$n" fwd)
  done
  printf '\noperators\n'
  if [ -s "$OPERATORS" ]; then
    awk '{printf "  %-14s %s...\n", $1, substr($3,1,24)}' "$OPERATORS"
  else
    printf '  NONE: nothing can open a shell here\n'
  fi
  printf '\n'
  ak_drift && \
    printf 'authorized_keys: UNMANAGED KEYS CHANGED (inspect, then rm %s)\n' \
      "$STATE/ak.base"
  grep -q "^$B1\$" "$AK" 2>/dev/null && echo "authorized_keys: managed block present" || \
    echo "authorized_keys: no managed block"
  grep -q "^$B1\$" "$SC" 2>/dev/null && echo "ssh_config: managed block present" || \
    echo "ssh_config: no managed block"
  if [ -s "$STATE/last-error" ]; then
    echo "last dial error:"
    tail -2 "$STATE/last-error" | sed 's/^/  /'
  fi
  return 0
}

cmd_compile() {
  [ -n "$(me)" ] || die "not initialized"
  ensure_dirs
  gen_as
  gen_kh
  compile_ak
  compile_sc
  log "compiled authorized_keys, known_hosts, ssh_config"
}

usage() {
  cat <<'EOF'
usage: mesh <command>

  init <new-name> --operator <pubkey-file> [--static <addr>] [--port <p>]
       [--user <u>] [--token <tok>]
      create this node's identity and add it to conf, naming the device
      <new-name>. A brand new mesh needs an operator public key: device
      keys never open a shell anywhere, so shells come from that key,
      which belongs in an ssh-agent and not on disk. Joining an existing
      mesh takes no --operator, since conf already carries one. To join,
      copy a member's ~/.config/mesh/conf here first (with its .git),
      then run this, then 'mesh adopt' on any member. With no conf
      present this starts a brand new mesh instead, and says so.
  invite [--serve] [--sport <p>] [--wait <s>]
      mint a one-use, time-limited enrollment token (kept only in local
      state, never in conf). While one is outstanding, this node's ticks
      scan for the invited device and adopt it automatically. --serve
      also serves conf, authenticated by the token, on <p> (43117) and
      adopts the device the moment it reports ready: the whole
      enrollment is this one command plus one 'mesh join' over there.
  join <new-name> <seed-addr[:port]> --token <tok> [init options]
      one-command enrollment, run ON the new device, which takes the
      name <new-name>: fetch conf from a seed running 'mesh invite
      --serve', verify it against the token (no hash to eyeball), init
      with the invite proof, then report back so the seed adopts
      instantly. Nothing is installed unless the token authenticates
      the bytes.
  adopt <user@host> [--port <p>] | adopt --scan [--auto]
      run on an EXISTING member to pull a newly initialized device's
      conf directly from it; needs no pre-existing credential, since
      that device already authorized every member when it compiled.
      Use --port 8022 for Termux devices. --scan sweeps the local
      subnets for un-enrolled devices that accept this node's key,
      reports what each would add, and prints the command to run.
      --auto also adopts, but only devices presenting a valid invite
      proof; anything without one is reported, never merged.
  guest <name> [<addr>] [--port <p>] [--user <u>] [--jump <node>]
        [--dynamic] [--fwd <sfx>:<lport>:<rhost>:<rport>]
      add a dial-only node: no member key, no ticks, never contacted in
      the background, authenticated by whatever your agent offers. Pins
      its host key via keyscan; verify that fingerprint yourself.
      --jump routes the dial through another node (ssh -W).
      --dynamic marks the address as discovered at dial time: each
      device that should reach it supplies its own 'mesh-resolve-<name>'
      on PATH, printing two lines, address then host public key. That
      keeps device-local tooling out of the gossiped conf, and whichever
      device resolves it publishes a signed record so the others do not
      need the tooling. An address 'ssm:<region>:<instance-id>' dials
      over AWS SSM (AWS-StartSSHSession) and needs aws and the session
      manager plugin. When a jump node has 'mesh-auth-<jump>' on PATH,
      each dial through it runs that with the jump's address; it prints
      the path of a private key, which the tunnel's ssh logs in with.
      A plain 'ssh <jump>' gets its key the same way: the ssh_config
      stanza runs 'mesh auth <jump>' through Match exec.
      --fwd (repeatable) emits a Host <name>-<sfx> alias whose session
      carries LocalForward <lport> -> <rhost>:<rport>.
  operator [<name> <pubkey-file>|--remove] [--as <operator>]
      show, or change, the keys in conf/operators. These are the only
      keys that open a shell anywhere on the mesh; every device key is
      confined to '<mesh> serve', which speaks gossip verbs and nothing
      else. Changing the file needs a signature from a key already in
      it, so a stolen device key cannot mint itself a shell. Keep the
      private halves in an agent, never on disk.
  evict <node> [--as <operator>]
      remove a member or guest from conf in an operator-signed commit:
      its record, device key and host key go, every member drops its
      authorized_keys line at the next sync, and the name is free for a
      fresh join. The way out when a device, and its key, is lost. A
      reinstall from the same address can take up to ten minutes to be
      confirmed: every peer's sshd penalizes the old key's failed logins.
  tick
      converge: sshd, keys, publish, exchange, resolve, ssh_config
  status
      one screen: this node, members with routes and ages, guests with
      whether they are usable HERE (resolver present, fresh gossiped
      record, or static) and pinned or not, the forward aliases, the
      operator keys, and managed-block drift.
  schedule
      detect the platform and install the right supervisor: Termux:Boot
      plus a job-scheduler backstop, a systemd user unit with lingering,
      or cron. Cadence follows a detected profile (battery = mobile).
  daemon [--once]
      watch local addresses and membership, converge the moment either
      changes, and run a periodic backstop. This is the thing to keep
      running; ticks are a fallback, not the mechanism.
  reset [--all]
      strip both managed blocks, cancel schedules, delete state; --all
      also deletes conf (full uninstall on this device)

work is event-driven: a peer's git push fires a post-receive hook that
recompiles instantly; the daemon converges on every network change; dial
re-resolves on demand. The scheduled tick is only a daily backstop.

unknown subcommands dispatch to 'mesh-<command>' on PATH, so extensions
(mesh-resolve-<guest> and anything else you write) plug in without editing
this script. They are exec'd with MESH_NODE, MESH_ROOT, MESH_CONF and
MESH_STATE exported, so an extension need not hardcode where state lives.

conf  (gossiped, public material only): $MESH_CONF   ~/.config/mesh/conf
state (local, never leaves this device): $MESH_STATE  ~/.config/mesh/state
platform overrides: define p_addrs, p_hostkey_pub, p_ensure_sshd or
p_schedule in state/platform.sh
EOF
}

cmd=$1
[ -n "$cmd" ] && shift
case "$cmd" in
  ''|help|-h|--help) ;;
  *) load_platform ;;
esac
case "$cmd" in
  init) cmd_init "$@" ;;
  join) cmd_join "$@" ;;
  invite) cmd_invite "$@" ;;
  adopt) cmd_adopt "$@" ;;
  guest) cmd_guest "$@" ;;
  operator) cmd_operator "$@" ;;
  evict) cmd_evict "$@" ;;
  tick) cmd_tick ;;
  status) cmd_status ;;
  schedule) p_schedule ;;
  daemon) cmd_daemon "$@" ;;
  reset) cmd_reset "$@" ;;
  dial) cmd_dial "$@" ;;
  auth) cmd_auth "$@" ;;
  serve) cmd_serve ;;
  sync) cmd_sync ;;
  compile) cmd_compile ;;
  confcheck) cmd_confcheck ;;
  help|-h|--help) usage ;;
  *)
    if ext=$(ext_path "$cmd"); then
      export MESH_ROOT MESH_CONF="$CONF" MESH_STATE="$STATE" MESH_NODE="$(me)"
      exec "$ext" "$@"
    fi
    usage
    exit 1
    ;;
esac
