Files
featherChat/warzone/scripts/build-linux.sh
Siavash Sameni dbf5d136cf fix: WASM double-X3DH bug, federated aliases, deploy tooling
WASM fix (critical):
- encrypt_key_exchange_with_id was calling x3dh::initiate a second time,
  generating a new ephemeral key that didn't match the ratchet — receiver
  always failed to decrypt. Now stores X3DH result from initiate() and
  reuses it. Added 2 protocol tests confirming the fix + the bug.
- Bumped service worker cache to wz-v2 to force browsers to re-fetch.
- Disabled wasm-opt for Hetzner builds (libc compat issue).

Federation — alias support:
- resolve_alias falls back to federation peer if not found locally
- register_alias checks peer server before allowing — globally unique aliases
- Added resolve_remote_alias() and is_alias_taken_remote() to FederationHandle

Federation — key proxy fix:
- Remote bundles no longer cached locally (stale cache caused decrypt failures)
- Local vs remote determined by device: prefix in keys DB

Client fixes:
- Self-messaging blocked ("Cannot send messages to yourself")
- /peer <self> blocked
- last_dm_peer never set to self
- /r <message> sends reply inline (switches peer + sends in one command)

Deploy tooling:
- scripts/build-linux.sh with --ship (build + deploy + destroy)
- --update-all, --status, --logs commands
- WASM rebuilt on Hetzner VM before server binary
- deploy/ directory: systemd service, federation configs, setup script
- Journald log cap (50MB, 7-day retention)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:59:19 +04:00

484 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Build featherChat Linux x86_64 release binaries using a Hetzner Cloud VPS.
# Prerequisites: hcloud CLI authenticated, SSH key "wz" registered.
#
# Usage:
# ./scripts/build-linux.sh --prepare Create VM, install deps, upload source
# ./scripts/build-linux.sh --build Build release binaries on the VM
# ./scripts/build-linux.sh --transfer Download binaries from VM to local
# ./scripts/build-linux.sh --destroy Delete the VM
# ./scripts/build-linux.sh --all Run prepare + build + transfer (no destroy)
# ./scripts/build-linux.sh --upload Re-upload source to existing VM
#
# The VM persists between steps so you can iterate on build errors.
# Reuses the same WZP builder VM if it already exists.
VM_NAME="fc-builder"
SSH_KEY_NAME="wz"
SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp"
SERVER_TYPE="cx33"
IMAGE="debian-12"
REMOTE_USER="root"
OUTPUT_DIR="target/linux-x86_64"
PROJECT_DIR="/Users/manwe/CascadeProjects/featherChat/warzone"
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10"
# Binaries to build
BINS="warzone-server warzone-client"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
get_vm_ip() {
local ip
ip=$(hcloud server list -o columns=name,ipv4 -o noheader 2>/dev/null | grep "$VM_NAME" | awk '{print $2}' | tr -d ' ')
if [ -z "$ip" ]; then
echo "ERROR: No VM '$VM_NAME' found. Run --prepare first." >&2
exit 1
fi
echo "$ip"
}
ssh_cmd() {
local ip
ip=$(get_vm_ip)
ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "$@"
}
scp_to() {
local ip
ip=$(get_vm_ip)
scp $SSH_OPTS -i "$SSH_KEY_PATH" "$@" "$REMOTE_USER@$ip:/root/" 2>/dev/null
}
scp_from() {
local ip
ip=$(get_vm_ip)
# args: remote_path local_path
scp $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip:$1" "$2" 2>/dev/null
}
# ---------------------------------------------------------------------------
# --prepare: Create VM, install deps, upload source
# ---------------------------------------------------------------------------
do_prepare() {
# Check if VM already exists
local existing
existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$VM_NAME" || true)
if [ -n "$existing" ]; then
echo "VM already exists: $existing"
echo "Reusing it. Uploading fresh source..."
do_upload
return
fi
echo "[1/5] Creating Hetzner VM: $VM_NAME ($SERVER_TYPE, $IMAGE)..."
hcloud server create \
--name "$VM_NAME" \
--type "$SERVER_TYPE" \
--image "$IMAGE" \
--ssh-key "$SSH_KEY_NAME" \
--location fsn1 \
--quiet
local ip
ip=$(get_vm_ip)
echo " VM: $VM_NAME @ $ip"
# Wait for SSH
echo "[2/5] Waiting for SSH..."
for i in $(seq 1 30); do
if ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "echo ok" &>/dev/null; then
break
fi
sleep 2
done
# Install build dependencies
echo "[3/5] Installing build dependencies..."
ssh_cmd "apt-get update -qq && apt-get install -y -qq \
build-essential \
pkg-config \
libssl-dev \
curl \
git \
> /dev/null 2>&1"
# Install Rust + wasm-pack
echo "[4/5] Installing Rust + wasm-pack..."
ssh_cmd "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable > /dev/null 2>&1"
ssh_cmd "source ~/.cargo/env && rustup target add wasm32-unknown-unknown > /dev/null 2>&1"
ssh_cmd "source ~/.cargo/env && cargo install wasm-pack > /dev/null 2>&1 || true"
# Upload source
echo "[5/5] Uploading source code..."
do_upload
echo ""
echo "=== VM Ready ==="
echo "IP: $ip"
echo "SSH: ssh -i $SSH_KEY_PATH root@$ip"
echo ""
echo "Next: ./scripts/build-linux.sh --build"
}
do_upload() {
echo " Creating source tarball..."
# Create tarball excluding build artifacts and non-essential files
tar czf /tmp/fc-src.tar.gz \
--exclude='target' \
--exclude='.git' \
--exclude='.claude' \
--exclude='warzone-phone' \
--exclude='notes' \
-C "$PROJECT_DIR" . 2>/dev/null
local ip
ip=$(get_vm_ip)
local size
size=$(du -h /tmp/fc-src.tar.gz | cut -f1)
echo " Uploading $size to VM..."
scp $SSH_OPTS -i "$SSH_KEY_PATH" /tmp/fc-src.tar.gz "$REMOTE_USER@$ip:/root/fc-src.tar.gz" 2>/dev/null
ssh_cmd "rm -rf /root/featherChat && mkdir -p /root/featherChat && tar xzf /root/fc-src.tar.gz -C /root/featherChat" 2>/dev/null
rm -f /tmp/fc-src.tar.gz
echo " Source uploaded."
}
# ---------------------------------------------------------------------------
# --build: Build release binaries on the VM
# ---------------------------------------------------------------------------
do_build() {
local ip
ip=$(get_vm_ip)
echo "=== Building on $ip ==="
local bin_args=""
for bin in $BINS; do
bin_args="$bin_args --bin $bin"
done
echo "[1/3] Building WASM (warzone-wasm)..."
ssh_cmd "source ~/.cargo/env && cd /root/featherChat && wasm-pack build crates/warzone-wasm --target web --out-dir ../../wasm-pkg 2>&1" | tail -5
echo ""
echo "[2/3] Building: $BINS"
ssh_cmd "source ~/.cargo/env && cd /root/featherChat && cargo build --release $bin_args 2>&1"
echo ""
echo "[3/3] Verifying binaries..."
for bin in $BINS; do
ssh_cmd "ls -lh /root/featherChat/target/release/$bin" 2>/dev/null
done
echo ""
echo "=== Build Complete ==="
echo "Next: ./scripts/build-linux.sh --transfer"
}
# ---------------------------------------------------------------------------
# --transfer: Download binaries from VM to local
# ---------------------------------------------------------------------------
do_transfer() {
local ip
ip=$(get_vm_ip)
echo "=== Downloading binaries from $ip ==="
mkdir -p "$OUTPUT_DIR"
for bin in $BINS; do
echo " $bin..."
scp_from "/root/featherChat/target/release/$bin" "$OUTPUT_DIR/$bin"
done
# Also grab the embedded web client HTML if it exists
if ssh_cmd "test -f /root/featherChat/target/release/warzone-server" 2>/dev/null; then
echo " federation.example.json..."
scp $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip:/root/featherChat/federation.example.json" "$OUTPUT_DIR/federation.example.json" 2>/dev/null || true
fi
echo ""
echo "=== Transfer Complete ==="
ls -lh "$OUTPUT_DIR"/warzone-*
echo ""
echo "Deploy with:"
echo " scp $OUTPUT_DIR/warzone-server $OUTPUT_DIR/warzone-client user@mequ:~/warzone/"
echo ""
echo "Run on server:"
echo " ./warzone-server --bind 0.0.0.0:7700"
echo " ./warzone-server --bind 0.0.0.0:7700 --federation federation.json"
}
# ---------------------------------------------------------------------------
# --destroy: Delete the VM
# ---------------------------------------------------------------------------
do_destroy() {
local existing
existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$VM_NAME" || true)
if [ -z "$existing" ]; then
echo "No VM '$VM_NAME' to destroy."
return
fi
echo "Deleting VM: $VM_NAME"
hcloud server delete "$VM_NAME"
echo "Done."
}
# ---------------------------------------------------------------------------
# --deploy: Transfer + deploy to production server
# ---------------------------------------------------------------------------
do_deploy() {
local deploy_host="${2:-}"
if [ -z "$deploy_host" ]; then
echo "Usage: $0 --deploy <user@host> [--federation <config.json>]"
echo ""
echo "Example:"
echo " $0 --deploy root@mequ.example.com"
echo " $0 --deploy root@mequ.example.com --federation federation.json"
exit 1
fi
echo "=== Deploying to $deploy_host ==="
# Ensure binaries exist locally
if [ ! -f "$OUTPUT_DIR/warzone-server" ]; then
echo "ERROR: No binaries in $OUTPUT_DIR. Run --build and --transfer first."
exit 1
fi
echo "[1/3] Uploading binaries..."
scp "$OUTPUT_DIR/warzone-server" "$OUTPUT_DIR/warzone-client" "$deploy_host:~/warzone/"
# Upload federation config if specified
local fed_arg=""
if [ "${3:-}" = "--federation" ] && [ -n "${4:-}" ]; then
echo "[2/3] Uploading federation config..."
scp "$4" "$deploy_host:~/warzone/federation.json"
fed_arg="--federation ~/warzone/federation.json"
else
echo "[2/3] No federation config (standalone mode)"
fi
echo "[3/3] Restarting server..."
ssh "$deploy_host" "pkill warzone-server || true; sleep 1; cd ~/warzone && nohup ./warzone-server --bind 0.0.0.0:7700 $fed_arg > server.log 2>&1 &"
echo ""
echo "=== Deployed ==="
echo "Server running at $deploy_host:7700"
echo "Logs: ssh $deploy_host 'tail -f ~/warzone/server.log'"
}
# ---------------------------------------------------------------------------
# Production servers
# ---------------------------------------------------------------------------
PROD_SERVERS=(
"root@mequ"
"root@kh3rad3ree"
)
PROD_SERVICE="warzone-server"
PROD_BIN_DIR="/home/warzone"
# ---------------------------------------------------------------------------
# --update <host>: Stop service, upload binaries, restart
# ---------------------------------------------------------------------------
do_update() {
local host="${1:-}"
if [ -z "$host" ]; then
echo "Usage: $0 --update <user@host>"
echo " or: $0 --update-all"
exit 1
fi
if [ ! -f "$OUTPUT_DIR/warzone-server" ]; then
echo "ERROR: No binaries in $OUTPUT_DIR. Run --all first."
exit 1
fi
echo "=== Updating $host ==="
echo "[1/4] Stopping service..."
ssh "$host" "systemctl stop $PROD_SERVICE 2>/dev/null || true"
echo "[2/4] Uploading binaries..."
scp "$OUTPUT_DIR/warzone-server" "$OUTPUT_DIR/warzone-client" "$host:$PROD_BIN_DIR/"
ssh "$host" "chmod +x $PROD_BIN_DIR/warzone-server $PROD_BIN_DIR/warzone-client"
echo "[3/4] Starting service..."
ssh "$host" "systemctl start $PROD_SERVICE"
echo "[4/4] Verifying..."
sleep 1
local status
status=$(ssh "$host" "systemctl is-active $PROD_SERVICE 2>/dev/null" || true)
if [ "$status" = "active" ]; then
echo " $host: $PROD_SERVICE is running"
else
echo " WARNING: $host: $PROD_SERVICE status = $status"
echo " Check logs: ssh $host 'journalctl -u $PROD_SERVICE -n 20'"
fi
echo ""
}
# ---------------------------------------------------------------------------
# --update-all: Update all production servers
# ---------------------------------------------------------------------------
do_update_all() {
if [ ! -f "$OUTPUT_DIR/warzone-server" ]; then
echo "ERROR: No binaries in $OUTPUT_DIR. Run --all first."
exit 1
fi
echo "=== Updating all production servers ==="
echo ""
for host in "${PROD_SERVERS[@]}"; do
do_update "$host"
done
echo "=== All servers updated ==="
}
# ---------------------------------------------------------------------------
# --status: Check service status on all production servers
# ---------------------------------------------------------------------------
do_status() {
echo "=== Production server status ==="
for host in "${PROD_SERVERS[@]}"; do
local status
status=$(ssh "$host" "systemctl is-active $PROD_SERVICE 2>/dev/null" || echo "unreachable")
local uptime
uptime=$(ssh "$host" "systemctl show $PROD_SERVICE --property=ActiveEnterTimestamp --value 2>/dev/null" || echo "?")
printf " %-20s %s (since %s)\n" "$host" "$status" "$uptime"
done
echo ""
# Check federation
for host in "${PROD_SERVERS[@]}"; do
local addr
addr=$(echo "$host" | cut -d@ -f2)
echo " Federation ($addr):"
curl -s "http://$addr:7700/v1/federation/status" 2>/dev/null | python3 -m json.tool 2>/dev/null || echo " (unreachable)"
echo ""
done
}
# ---------------------------------------------------------------------------
# --logs <host>: Tail logs
# ---------------------------------------------------------------------------
do_logs() {
local host="${1:-${PROD_SERVERS[0]}}"
echo "=== Logs from $host ==="
ssh "$host" "journalctl -u $PROD_SERVICE -f --no-pager"
}
# ---------------------------------------------------------------------------
# --ship: Build + deploy to all servers + destroy VM (full pipeline)
# ---------------------------------------------------------------------------
do_ship() {
echo "========================================"
echo " SHIPPING featherChat to production"
echo "========================================"
echo ""
do_prepare
echo ""
do_build
echo ""
do_transfer
echo ""
do_update_all
echo ""
do_destroy
echo ""
do_status
echo ""
echo "========================================"
echo " SHIP COMPLETE"
echo "========================================"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
case "${1:-}" in
--prepare)
do_prepare
;;
--build)
do_build
;;
--transfer)
do_transfer
;;
--destroy)
do_destroy
;;
--deploy)
do_deploy "$@"
;;
--update)
do_update "${2:-}"
;;
--update-all)
do_update_all
;;
--status)
do_status
;;
--logs)
do_logs "${2:-}"
;;
--all)
do_prepare
do_build
do_transfer
echo ""
echo "VM is still running. Destroy with: ./scripts/build-linux.sh --destroy"
;;
--ship)
do_ship
;;
--upload)
do_upload
;;
*)
echo "Usage: $0 <command> [args]"
echo ""
echo "One command:"
echo " --ship Build + deploy to all servers + destroy VM"
echo ""
echo "Build (Hetzner VM):"
echo " --prepare Create VM, install deps, upload source"
echo " --build Build release binaries"
echo " --transfer Download binaries to $OUTPUT_DIR"
echo " --destroy Delete the build VM"
echo " --all prepare + build + transfer (VM persists)"
echo " --upload Re-upload source to existing VM"
echo ""
echo "Deploy:"
echo " --update <user@host> Stop service, upload binaries, restart"
echo " --update-all Update mequ + kh3rad3ree"
echo " --deploy <user@host> First-time deploy (upload + start)"
echo ""
echo "Monitor:"
echo " --status Check service status on all servers"
echo " --logs [user@host] Tail server logs (default: mequ)"
exit 1
;;
esac