#!/usr/bin/env bash # Bindu first run: find a nearby network, join an existing one, or start a new one. set -euo pipefail FOUND_URL="" FOUND_NAME="" FOUND_SECURE_URL="" say() { printf '%s\n' "$*"; } fail() { printf 'Bindu: %s\n' "$*" >&2; exit 1; } validate_url() { case "$1" in https://*) ;; http://*.local|http://*.local:*) ;; http://10.*:*|http://192.168.*:*|http://172.1[6-9].*:*|http://172.2[0-9].*:*|http://172.3[01].*:*) ;; http://127.0.0.1:*|http://localhost:*) ;; *) return 1 ;; esac case "$1" in *[!a-zA-Z0-9._:/-]*|*///*) return 1 ;; esac return 0 } probe_hub() { local base="${1%/}" body secure validate_url "$base" || return 1 local curl_args=(--fail --silent --show-error --connect-timeout 1 --max-time 3) if [ -n "${BINDU_CA_CERT:-}" ]; then [ -r "$BINDU_CA_CERT" ] || fail "Cannot read BINDU_CA_CERT at $BINDU_CA_CERT." curl_args+=(--cacert "$BINDU_CA_CERT") fi body="$(curl "${curl_args[@]}" \ "$base/bindu/mesh/discovery" 2>/dev/null)" || return 1 printf '%s' "$body" | grep -Eq '"serviceType"[[:space:]]*:[[:space:]]*"_bindu\._tcp\.local"' || return 1 printf '%s' "$body" | grep -Eq '"protocolVersion"[[:space:]]*:[[:space:]]*"1\.[0-9]+\.[0-9]+"' || return 1 FOUND_URL="$base" secure="$(printf '%s' "$body" | sed -n 's/.*"https"[[:space:]]*:[[:space:]]*"\(https:[^"\\]*\)".*/\1/p' | head -n 1)" FOUND_SECURE_URL="" if validate_url "$secure"; then FOUND_SECURE_URL="$secure"; fi FOUND_NAME="$(printf '%s' "$body" | sed -n 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p' | head -n 1)" FOUND_NAME="${FOUND_NAME:-Bindu network}" return 0 } add_candidate() { local candidate="${1%/}" [ -n "$candidate" ] || return 0 case " $CANDIDATES " in *" $candidate "*) return 0 ;; esac CANDIDATES="$CANDIDATES $candidate" } discover_nearby() { local candidate record ip port host browse_file browse_pid records_file CANDIDATES="" # Explicit candidates are useful on LANs that block multicast and for tests. for candidate in ${BINDU_DISCOVERY_CANDIDATES:-}; do add_candidate "$candidate"; done if command -v avahi-browse >/dev/null 2>&1; then while IFS=';' read -r _ _ _ _ _ _ host ip port _; do if [ -n "$ip" ] && [ -n "$port" ]; then add_candidate "http://$ip:$port"; fi done < <(avahi-browse --resolve --terminate --parsable _bindu._tcp 2>/dev/null | grep '^=' || true) fi if command -v dns-sd >/dev/null 2>&1; then browse_file="$(mktemp)" dns-sd -B _bindu._tcp local >"$browse_file" 2>&1 & browse_pid=$! sleep 2 kill "$browse_pid" 2>/dev/null || true wait "$browse_pid" 2>/dev/null || true records_file="$(mktemp)" awk '$2 == "Add" && /_bindu\._tcp/ { for (i=7; i<=NF; i++) printf "%s%s", $i, (i"$records_file" rm -f "$browse_file" while IFS= read -r record; do [ -n "$record" ] || continue browse_file="$(mktemp)" dns-sd -L "$record" _bindu._tcp local >"$browse_file" 2>&1 & browse_pid=$! sleep 2 kill "$browse_pid" 2>/dev/null || true wait "$browse_pid" 2>/dev/null || true host="$(sed -n 's/.*can be reached at \([^: ]*\):\([0-9][0-9]*\).*/\1/p' "$browse_file" | head -n 1)" host="${host%.}" port="$(sed -n 's/.*can be reached at \([^: ]*\):\([0-9][0-9]*\).*/\2/p' "$browse_file" | head -n 1)" rm -f "$browse_file" if [ -n "$host" ] && [ -n "$port" ]; then add_candidate "http://$host:$port"; fi done <"$records_file" rm -f "$records_file" fi add_candidate "http://bindu.local:3033" for candidate in $CANDIDATES; do if probe_hub "$candidate"; then return 0; fi done return 1 } install_client() { local target="$1" response secure_url installer local download_args=(--fail --silent --show-error --max-time 30) probe_hub "$target" || fail "No responding Bindu network at $target. Connect to its LAN or Tailscale network, then retry." if [[ "$FOUND_URL" == https://* ]]; then FOUND_SECURE_URL="$FOUND_URL" fi [ -n "$FOUND_SECURE_URL" ] || fail "This network did not advertise a secure URL. Ask its operator for an HTTPS address and CA certificate. No client was installed." secure_url="$FOUND_SECURE_URL" probe_hub "$secure_url" || fail "Cannot verify $secure_url over HTTPS. Ask the operator for its CA certificate and set BINDU_CA_CERT. No client was installed." say "Found: $FOUND_NAME at $secure_url" say "This discovery response is not proof of identity. Confirm the network address with its operator." if [ "${BINDU_ASSUME_YES:-}" != "1" ]; then [ -r /dev/tty ] || fail "A terminal confirmation is required." printf 'Use this network? [y/N] ' >/dev/tty IFS= read -r response /dev/null 2>&1 || fail "Node.js 22.9 or newer is needed to start a LAN network." command -v tar >/dev/null 2>&1 || fail "The tar tool is needed to unpack a LAN network." if [ -n "${BINDU_NETWORK_PACKAGE_CA_CERT:-}" ]; then [ -r "$BINDU_NETWORK_PACKAGE_CA_CERT" ] || fail "Cannot read the Network package CA certificate." fi if [ -d "$destination" ]; then printf 'Using the existing Network package at %s\n' "$destination" >&2 printf '%s\n' "$destination" return 0 fi [ ! -e "$destination" ] || fail "Inspect the existing file at $destination before installing a network." parent="$(dirname "$destination")" mkdir -p "$parent" staging="$(mktemp -d "$parent/.bindu-download.XXXXXX")" printf 'Downloading the Bindu Network pilot package…\n' >&2 local download_args=(--fail --silent --show-error --max-time 120) if [ -n "${BINDU_NETWORK_PACKAGE_CA_CERT:-}" ]; then download_args+=(--cacert "$BINDU_NETWORK_PACKAGE_CA_CERT") fi if ! curl "${download_args[@]}" "$archive_url" -o "$staging/package.tar.gz"; then rm -rf -- "$staging" fail "The Network package could not be downloaded. No network was installed." fi actual="$(node -e 'const fs=require("node:fs"),crypto=require("node:crypto");process.stdout.write(crypto.createHash("sha256").update(fs.readFileSync(process.argv[1])).digest("hex"))' "$staging/package.tar.gz")" if [ "$actual" != "$expected" ]; then rm -rf -- "$staging" fail "The Network package checksum did not match. No network was installed." fi mkdir -p "$staging/payload" if ! tar -xzf "$staging/package.tar.gz" -C "$staging/payload" || ! BINDU_GUARD_CLI=1 node "$staging/payload/scripts/network-package-guard.mjs" "$staging/payload" >&2; then rm -rf -- "$staging" fail "The Network package failed its audit. No network was installed." fi mv "$staging/payload" "$destination" rm -rf -- "$staging" printf '%s\n' "$destination" } mode="${1:-interactive}" case "$mode" in --discover) if discover_nearby; then say "Nearby Bindu network: $FOUND_NAME ($FOUND_URL)"; else say "No Bindu network found on this LAN."; fi ;; --join) [ -n "${2:-}" ] || fail "Usage: bindu-first-run --join https://your-bindu-network" install_client "$2" ;; --new) start_new ;; interactive) say "Welcome to Bindu. Looking for a network on this LAN…" if discover_nearby; then say "Found $FOUND_NAME at $FOUND_URL" say "1. Use this network" say "2. Start a new network on this LAN" say "3. Join another network" else say "No Bindu network found on this LAN." say "1. Start a new network on this LAN" say "2. Join another network" fi [ -r /dev/tty ] || fail "Run in a terminal, or use --discover, --join URL, or --new." printf 'Choose a path: ' >/dev/tty IFS= read -r choice /dev/tty; IFS= read -r target /dev/tty; IFS= read -r target