Server:
- --bots-config <path> loads JSON array of system bots on startup
- Bots auto-created if missing, aliases restored on every start
- Bot list stored in DB for welcome screen (system:bot_list key)
- GET /v1/bot/list returns system bots (public, no auth)
Welcome screen:
- Web + TUI show available bots on first login
- "Available bots: @helpbot — featherChat help, @codebot — Coding..."
- Clickable in web (via address detection)
Config: bots.example.json with 10 suggested bots
Usage: warzone-server --enable-bots --bots-config bots.json
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Privacy: from.id is now Hash(bot_token + user_fp) → different bots see
different numeric IDs for the same user. Prevents cross-bot user correlation.
Removed id_str (raw hex fingerprint) from all bot API responses.
Updated LLM_BOT_DEV.md and LLM_HELP.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Built-in BotFather (Rust, server-side):
- Intercepts messages to @botfather in deliver_or_queue
- Commands: /newbot <name>, /mybots, /deletebot <name>, /token <name>
- Creates bot with fingerprint, token, alias, tracks ownership
- Replies via push_to_client or queue (works offline)
- Only active when --enable-bots is set
Standalone BotFather (Python):
- tools/botfather.py: uses bot API (getUpdates/sendMessage)
- Delegates core ops to built-in handler
- Extensible for additional features
- Reads token from BOTFATHER_TOKEN env or .botfather_token file
Flow: User messages @botfather → "/newbot MyBot" → gets token back
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>
Version:
- Workspace + protocol: 0.0.21 → 0.0.22
- Web client VERSION: 0.0.17 → 0.0.22
- Service worker cache: wz-v2 → wz-v3
ETH identity:
- Added WasmIdentity::eth_address() export (derives from seed via secp256k1)
- Web client sends eth_address during key registration
- Identity display shows ETH address first, then fingerprint
- No more server-side resolve needed — computed client-side
CLAUDE.md:
- Added MANDATORY version bump rule (4 places to update)
- Must bump on every functional change, never skip SW cache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bot API (routes/bot.rs — full rewrite):
- getUpdates: persistent update_id counter, offset acknowledgement,
limit (max 100), long-poll up to 30s with 1s intervals
- sendMessage: parse_mode, reply_to_message_id, reply_markup (inline keyboards)
- answerCallbackQuery: acknowledge button clicks
- editMessageText: update sent messages
- setWebhook / deleteWebhook / getWebhookInfo: webhook configuration
- sendDocument: file reference with caption
- Bot queue: raw messages migrated to bot_queue:<fp>:<update_id> for ordering
Web client (routes/web.rs):
- Bot messages rendered properly (was showing "[message could not be decrypted]")
- Handles bot_message, bot_edit, bot_document as both Text and Binary WS frames
- Inline keyboard buttons rendered as bracketed text
- Missed call notifications handled in Text frame path
Docs:
- LLM_BOT_DEV.md: token-optimized bot dev reference for coding assistant LLM
(Python + Node.js examples, all endpoints, TG compatibility table)
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>
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>
Files now appear as a styled clickable link in chat:
📎 filename.pdf (1.6 KB) from sender
Click to download. No auto-save dialog.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Web client:
- Paperclip file upload button in chat bar
- Chunked upload: 64KB chunks, SHA-256 integrity
- Progress display during send/receive
- Auto-download on complete (browser save dialog)
- Max 10MB per file
WASM:
- decrypt_wire_message now returns file_header and file_chunk
with type, id, filename, chunk data (hex encoded)
Receive flow:
- FileHeader: registers pending transfer
- FileChunk: stores chunk, shows progress
- All chunks received: assembles, triggers blob download
Send flow (web→web or web→CLI):
- File sent as JSON messages (not bincode, for simplicity)
- Receiver handles both JSON and bincode formats
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>
Server:
- WS endpoint: /v1/ws/:fingerprint
- Connection registry in AppState (fingerprint → WS senders)
- On connect: flushes queued DB messages, then pushes in real-time
- send_message: pushes to WS if connected, falls back to DB queue
- Auto-cleanup on disconnect
- WS accepts both binary and JSON text frames for sending
Web client:
- Replaces 2-second HTTP polling with persistent WebSocket
- Auto-reconnects on disconnect (3-second backoff)
- Sends via WS when connected, HTTP fallback
- Messages arrive instantly (no polling delay)
- "Real-time connection established" shown on connect
HTTP polling still works:
- CLI recv command uses HTTP (unchanged)
- Web falls back to HTTP if WS fails
- Mules/scripts can still use HTTP API
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each peer gets a stable color from a 12-color palette based on
their fingerprint/alias hash. Self messages stay green.
No more same-color for different users.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: web client's bundle included OTPKs, so X3DH initiate()
did 4 DH ops (DH4 with OTPK). But decrypt_wire_message() called
respond() with None for OTPK, doing only 3 DH ops.
Different DH concat → different shared secret → decrypt fails.
Fix: web client bundles have one_time_pre_key: None.
initiate() skips DH4 when no OTPK present.
respond() also skips DH4 with None.
Both sides now do exactly 3 DH ops → shared secrets match.
OTPKs are an anti-replay optimization, not required for E2E.
Will add OTPK support to web client in Phase 2 with proper
server-side OTPK storage and consumption tracking.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Version shown on chat load (v0.0.2)
- Self-test now does step-by-step: X3DH shared secret comparison,
then manual ratchet init + decrypt (not via decrypt_wire_message)
- Shows: rng output, shared_match, alice/bob shared secrets, decrypt result
- This isolates whether X3DH or ratchet or AEAD fails
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/selftest — runs full Alice→Bob encrypt/decrypt cycle within WASM
(tests X3DH + Double Ratchet + bincode serialize/deserialize)
/bundleinfo — dumps bundle contents, verifies SPK secret matches
SPK public key in the registered bundle
These help isolate whether the bug is in WASM crypto (self-test fails)
or in CLI↔WASM interop (self-test passes but cross-client fails).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
/reset — clears all localStorage (identity, sessions, SPK)
/sessions — shows active session peers and SPK secret prefix
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
<script type="module"> doesn't expose functions to onclick attributes.
Replaced all onclick="fn()" with document.getElementById().onclick = fn
so buttons work from module scope.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: WASM was regenerating random pre-keys on every call to
decrypt_wire_message, instead of using the SPK that was registered
with the server. CLI sender encrypts to the registered SPK, but
WASM was trying to decrypt with a different random key.
Fix:
- WasmIdentity now stores spk_secret_bytes internally
- SPK secret persisted to localStorage as 'wz-spk'
- On load: restored from localStorage, not regenerated
- bundle_bytes() uses stored SPK secret (cached, deterministic)
- decrypt_wire_message() takes spk_secret_hex parameter
- Web UI passes stored SPK to all decrypt calls
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>
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>
- fetchPeerKey: catch JSON parse error for CLI bincode bundles,
show clear "CLI client — needs WASM bridge" message
- Group send: silently skip CLI members instead of showing
error per member (mixed groups work, web members get messages,
CLI members are skipped without noise)
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>
X3DH fix:
- Added identity_encryption_key (X25519) to PreKeyBundle
- initiate() and respond() now use correct DH operations per Signal spec:
DH1=IK_a*SPK_b, DH2=EK_a*IK_b, DH3=EK_a*SPK_b, DH4=EK_a*OPK_b
- All 17 tests pass including x3dh_shared_secret_matches
Web client (served at /):
- Identity generation with seed (stored in localStorage)
- Recovery from hex-encoded seed
- Auto-load saved identity on page load
- Fingerprint display (same format as CLI: xxxx:xxxx:xxxx:xxxx)
- Key registration with server via /v1/keys/register
- Chat UI with message polling (5s interval)
- Commands: /help, /info, /seed
- Dark theme matching warzone aesthetic
Both clients (CLI + Web) now exist:
- CLI: warzone init, warzone info, warzone recover
- Web: http://localhost:7700/ (served by warzone-server)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>