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>
484 lines
14 KiB
Bash
Executable File
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
|