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>
This commit is contained in:
483
warzone/scripts/build-linux.sh
Executable file
483
warzone/scripts/build-linux.sh
Executable file
@@ -0,0 +1,483 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user