New users with no peer/group set automatically join #ops so they
have someone to talk to. Saved peer overrides this on subsequent visits.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TUI:
- Header shows peer ETH address (resolved on /peer set)
- Own messages show ETH format
- Resolve display shows full formatted fingerprint (xxxx:xxxx:...)
- peer_eth field stored on App for header display
Web:
- Pasting 0x address in peer input box now resolves via /v1/resolve/
- Send path resolves 0x/@ before encrypting
- Click messages area → focuses text input
- Own messages show ETH format
Version: 0.0.23 → 0.0.24, SW cache wz-v4 → wz-v5
Build script: --local, --local-ship, --local-clean commands
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Version: 0.0.22 → 0.0.23, SW cache wz-v3 → wz-v4
TUI:
- Own messages show ETH address (0x...) instead of fingerprint
- Received messages: async ETH cache lookup (resolve on first sight)
- /info shows Identity + Fingerprint
- Welcome message shows ETH address
Web:
- Header shows only ETH address (single element, click to copy)
- Own messages show ETH format
- Received messages resolve sender ETH via /v1/resolve/
- /peer 0x... resolves via /v1/resolve/ endpoint
- Click messages area → focuses text input
Client:
- register_bundle sends eth_address to server
- ETH↔fingerprint mapping stored on registration
Build:
- --local: build on current machine (auto-detect apt/dnf/pacman/brew)
- --local-ship: build locally + deploy to all servers
- --local-clean: build + clean cargo cache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TUI header: shows ETH address (0x...) instead of fingerprint
/peer 0x...: resolves via GET /v1/resolve/:address endpoint
Cmd+A/E/U/K/W: macOS SUPER modifier now handled alongside CONTROL
Added resolve_address() method for ETH/any address resolution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
/file <path> now works in group mode (#group):
- Sends file header + chunks to each group member
- Same fan-out approach as group text messages
- Each member receives and reassembles independently
- Progress shown: "Sending 'file.pdf' to group #ops..."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Storage:
- contacts sled tree: auto-tracked on send/receive
- fingerprint, alias, first_seen, last_seen, message_count
- history sled tree: all messages stored locally
- key: hist:<peer_fp>:<timestamp>:<uuid> for ordered scan
- sender, text, is_self, timestamp
TUI commands:
- /contacts or /c — list all contacts (sorted by most recent)
Shows alias, fingerprint, message count
- /history or /h — show last 50 messages with current peer
- /h <fingerprint> — show history with specific peer
Auto-tracking:
- On send: touch_contact + store_message (is_self=true)
- On receive: touch_contact + store_message (is_self=false)
- Both KeyExchange and Message variants tracked
Backup: contacts + history included in export_all (encrypted backup).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Keyboard shortcuts:
- Left/Right: move cursor
- Home / Ctrl+A: beginning of line
- End / Ctrl+E: end of line
- Alt+Left/Right: word jump
- Alt+Backspace: delete word back
- Ctrl+W: delete word back
- Ctrl+U: clear entire line
- Ctrl+K: kill to end of line
- Delete: delete char at cursor
- Backspace: delete char before cursor
Cursor position tracked, chars insert at cursor (not just append).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TUI fixes:
- /r and /reply now work: tracks last_dm_peer from received messages
- /r switches peer to last DM sender, then type normally
- /p @alias works as shortcut for /peer @alias
- /eth shows Ethereum address in TUI
- /unalias removes your alias
Web fixes:
- /p @alias and /peer @alias resolve and set peer
- /r and /reply work (switch to last DM sender)
- /unalias removes alias
- /admin-unalias <alias> <password> for admin removal
- File download now shows as clickable link (not auto-download)
Server:
- POST /v1/alias/unregister — remove own alias
- POST /v1/alias/admin-remove — admin removes any alias
- WARZONE_ADMIN_PASSWORD env var (default: "admin")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aliases:
- /unalias — remove your own alias
- /admin-unalias <alias> <password> — admin removes any alias
- Admin password via WARZONE_ADMIN_PASSWORD env var (default: "admin")
- POST /v1/alias/unregister + POST /v1/alias/admin-remove
Reply:
- /r or /reply — switches peer to whoever last DM'd you
- lastDmPeer tracked on both web and TUI
- Then type normally to reply
Web:
- Version bumped to 0.0.15 (was stuck at 0.0.10)
- WASM rebuilt with latest protocol
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- POST /groups/:name/leave — remove self from group
- POST /groups/:name/kick — creator can kick members
- GET /groups/:name/members — list with aliases + creator badge
CLI TUI:
- /gleave — leave current group
- /gkick <fp_or_alias> — kick (creator only)
- /gmembers — show member list with aliases and ★ for creator
Web client:
- Same commands: /gleave, /gkick, /gmembers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Protocol:
- WireMessage::Receipt { sender_fingerprint, message_id, receipt_type }
- ReceiptType enum: Delivered, Read
- id field added to KeyExchange and Message variants
- Receipts are plaintext (not encrypted) — contain only ID + type
Web client:
- Auto-sends Delivered receipt on successful decrypt
- Tracks sent message IDs with receipt status
- Displays: ✓ (sent, gray), ✓✓ (delivered, white), ✓✓ (read, blue)
- Receipt indicators update live via DOM reference
CLI TUI:
- Auto-sends Delivered receipt back to sender on decrypt
- Tracks receipt status per message ID
- Displays receipt indicators after sent messages
WASM:
- create_receipt() function for web client
- encrypt_with_id/encrypt_key_exchange_with_id for tracking
- decrypt_wire_message handles Receipt variant
17/17 protocol tests pass. Zero warnings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
poll_loop now:
1. Tries WebSocket connection to /v1/ws/<fingerprint>
2. On success: receives messages in real-time (instant push)
3. On disconnect: reconnects after 3 seconds
4. On WS failure: falls back to HTTP polling every 2 seconds
Refactored message processing into shared functions:
- process_incoming() handles raw bytes
- process_wire_message() handles deserialized WireMessage
- Used by both WS and HTTP paths
Both CLI TUI and web client now use WebSocket:
- No more HTTP polling spam in server logs
- Messages arrive instantly on both clients
- HTTP poll kept as fallback for scripts/mules
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
warzone-wasm crate:
- Compiles warzone-protocol to WebAssembly via wasm-pack
- Exposes WasmIdentity, WasmSession, decrypt_wire_message to JS
- Same X25519 + ChaCha20-Poly1305 + X3DH + Double Ratchet as CLI
- 344KB WASM binary (optimized with wasm-opt)
WireMessage moved to warzone-protocol:
- Shared type used by CLI client, WASM bridge, and TUI
- Guarantees identical bincode serialization across all clients
Web client rewritten:
- Loads WASM module on startup (/wasm/warzone_wasm.js)
- Identity: WasmIdentity generates same key types as CLI
- Registration: sends bincode PreKeyBundle (same format as CLI)
- Encrypt: WasmSession.encrypt/encrypt_key_exchange
- Decrypt: decrypt_wire_message (handles KeyExchange + Message)
- Sessions persisted in localStorage (base64 ratchet state)
- Groups: per-member WASM encryption (interop with CLI members)
Server routes:
- GET /wasm/warzone_wasm.js — serves WASM JS glue
- GET /wasm/warzone_wasm_bg.wasm — serves WASM binary
- Both embedded at compile time via include_str!/include_bytes!
Web ↔ CLI interop now works:
- Same key exchange (X3DH with X25519)
- Same ratchet (Double Ratchet with ChaCha20-Poly1305)
- Same wire format (bincode WireMessage)
- Web user can message CLI user and vice versa
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Storage:
- Detects sled lock contention, shows actionable error:
"Database locked by another warzone process"
with ps command to find the process and rm command to force unlock
TUI:
- Poll loop no longer calls load_seed() (was re-prompting passphrase)
- Seed passed from main.rs to run_tui to poll_loop
- Single passphrase prompt per app launch
Warnings fixed:
- Removed unused `Context` import in tui/app.rs
- Added #[allow(dead_code)] on validate_token (used when auth middleware wired)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sending a message includes `from` fingerprint
- Server renews alias TTL on send (proves identity: you encrypted it)
- Polling/receiving does NOT renew (anyone can spam messages to you)
- Key registration does NOT renew (separate concern)
This prevents alias keepalive attacks where someone spams a user
just to keep their alias from expiring.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- POST /v1/alias/register — claim an alias (one per fingerprint)
- GET /v1/alias/resolve/:name — alias → fingerprint
- GET /v1/alias/whois/:fingerprint — fingerprint → alias (reverse)
- GET /v1/alias/list — list all aliases
- Bidirectional mapping in sled (a:name→fp, fp:fp→name)
- One alias per person, re-registering replaces old alias
Web client:
- /alias <name> — register your alias
- /aliases — list all registered aliases
- /info — now shows alias alongside fingerprint
- Peer input accepts @alias (resolved before sending)
- Received messages show @alias instead of fingerprint
- DM: paste @alias or fingerprint in peer input
CLI TUI:
- /alias <name> — register alias
- /aliases — list all aliases
- /peer @alias — resolves alias to fingerprint
- Alias resolution displayed in system messages
Addressing model:
- @manwe (local) → server resolves → fingerprint
- @manwe.b1.example.com (federated) → DNS resolve (Phase 3)
- Raw fingerprint → always works, no resolution
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Server: /join endpoint creates the group if it doesn't exist
- CLI TUI: /g <name> auto-joins before switching
- Web: /g <name> auto-joins before switching
- No more "group not found" errors — just /g ops and go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server:
- POST /v1/groups/create — create named group
- POST /v1/groups/:name/join — join group
- GET /v1/groups/:name — get group info + member list
- GET /v1/groups — list all groups
- POST /v1/groups/:name/send — fan-out encrypted messages to members
- Groups stored in sled, members tracked by fingerprint
Web client:
- /gcreate <name> — create group
- /gjoin <name> — join group
- /g <name> — switch to group chat mode
- /glist — list all groups
- /dm — switch back to DM mode
- Group messages encrypted per-member (ECDH + AES-GCM for each)
- Group tag shown on received messages: "sender [groupname]"
CLI TUI client:
- Same commands: /gcreate, /gjoin, /g, /glist, /dm
- Group messages encrypted per-member (X3DH + Double Ratchet for each)
- Automatic X3DH key exchange with new group members on first message
- Sessions established and persisted per-member
Architecture:
- Client-side fan-out encryption: message encrypted N times (once per member)
- Server stores one copy per recipient in their message queue
- Reuses existing 1:1 encryption — no new crypto primitives needed
- Works for groups ≤ 50 members (per DESIGN.md)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`warzone chat [peer-fp] -s <server>` launches an interactive terminal UI:
- Header: your fingerprint, peer fingerprint, server URL
- Message area: color-coded (green=you, yellow=peer, cyan=system)
- Input bar with cursor at bottom
- Background polling every 2s for incoming messages
- Full X3DH + Double Ratchet on send/receive
- Session persistence across messages
Commands in TUI:
- /peer <fingerprint> — set who you're chatting with
- /info — show your fingerprint
- /quit or /q or Esc or Ctrl+C — exit
Usage:
warzone chat "6baf:6d0b:4541:9cae:f06b:83da:69bc:05ee" -s http://localhost:7700
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Client: strip colons before putting fingerprints in URL paths
(colons in URLs confuse axum path matching).
Server: normalize fingerprints in message routes too.
All fingerprint storage and lookup is now hex-only, case-insensitive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Server: normalize fingerprints by stripping colons and lowercasing
before storing/looking up in sled. Adds tracing for register/lookup.
Client: check HTTP status before parsing JSON response body.
Shows clear error when user is not registered instead of parse error.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All data paths now use keystore::data_dir() which checks
WARZONE_HOME first, falls back to ~/.warzone.
This avoids the HOME override hack that breaks rustup/cargo.
Usage: WARZONE_HOME=/tmp/bob warzone init
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>