From a538981d30ce8ae55af7977a5d4c3cd9938d1595 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 00:09:18 +0000 Subject: [PATCH 1/6] chore: open auto-fix batch claude/friendly-maxwell-3QJhB From d3543cc13964934955375a7e135f60342053c0ba Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 00:30:55 +0000 Subject: [PATCH 2/6] fix(web): add maskable purpose, scope, id to manifest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - icons: purpose "any maskable" so Android adaptive containers crop with safe-zone awareness - top-level scope + id "/" so browsers identify install by id, not URL — prevents collisions between dev and prod builds at same origin Refs #605 --- crates/web/manifest.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/web/manifest.json b/crates/web/manifest.json index 14c3488b..6f6e0216 100644 --- a/crates/web/manifest.json +++ b/crates/web/manifest.json @@ -2,6 +2,8 @@ "name": "Willow", "short_name": "Willow", "description": "P2P encrypted chat", + "id": "/", + "scope": "/", "start_url": "/", "display": "standalone", "background_color": "#1a1a1e", @@ -10,12 +12,14 @@ { "src": "/icon-192.svg", "sizes": "192x192", - "type": "image/svg+xml" + "type": "image/svg+xml", + "purpose": "any maskable" }, { "src": "/icon-512.svg", "sizes": "512x512", - "type": "image/svg+xml" + "type": "image/svg+xml", + "purpose": "any maskable" } ] } From 791faa541b1ed7744f2853f05f14686be5168bb4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 00:33:19 +0000 Subject: [PATCH 3/6] fix(web): port sw register to web_sys + log err - swap js_sys::eval string for web_sys::ServiceWorkerContainer - log JsValue err via tracing::warn so HTTPS/MIME/parse/scope failures surface instead of silent .catch(()=>{}) - drops one unsafe-eval user (refs CSP cleanup #171/#425) Refs #606 --- crates/web/src/main.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/web/src/main.rs b/crates/web/src/main.rs index 107a9872..7e763b29 100644 --- a/crates/web/src/main.rs +++ b/crates/web/src/main.rs @@ -13,10 +13,19 @@ fn main() { } } - // Register the service worker for PWA support. - let _ = js_sys::eval( - "if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').catch(function() {}); }", - ); + // Register the service worker for PWA support. Logs failures (HTTPS + // misconfiguration, MIME mismatch, parse errors, scope violations) so + // they surface instead of being silently swallowed (issue #606). + if let Some(window) = web_sys::window() { + let sw_container = window.navigator().service_worker(); + let promise = sw_container.register("/sw.js"); + wasm_bindgen_futures::spawn_local(async move { + if let Err(e) = wasm_bindgen_futures::JsFuture::from(promise).await { + let msg = e.as_string().unwrap_or_else(|| format!("{e:?}")); + tracing::warn!("service worker registration failed: {msg}"); + } + }); + } // Wire navigator.serviceWorker.onmessage so pushes forwarded by the // service worker (focused-client path) reach the in-app Notifier. From 22969f57bee70c241ce94f5957c00d2a7b1895ac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 00:35:25 +0000 Subject: [PATCH 4/6] fix(web): log create_ephemeral_channel error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace `let _ = h.create_ephemeral_channel(...).await` in the member-list "start temp channel…" button click handler with an explicit `if let Err(e)` branch emitting `tracing::warn!(?e, "create_ephemeral_channel failed")`. - Mirrors the #585 / 41736c0 ICE-candidate pattern: silently dropping the Result hid network/permission/name-collision failures from both the user and the logs. - Log only, no toast: surfacing UI feedback would require touching the failure-UI helper convention, out of scope for an error-logging parity fix. Audit raised both options; narrowed to logging. Refs #620 --- crates/web/src/components/member_list.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/web/src/components/member_list.rs b/crates/web/src/components/member_list.rs index ebdc6ec5..8a669c67 100644 --- a/crates/web/src/components/member_list.rs +++ b/crates/web/src/components/member_list.rs @@ -381,11 +381,13 @@ pub fn MemberList( pid.chars().take(6).collect(); let name = format!("side-{short}"); wasm_bindgen_futures::spawn_local(async move { - let _ = h.create_ephemeral_channel( + if let Err(e) = h.create_ephemeral_channel( &name, willow_state::EphemeralKind::Channel, willow_state::DEFAULT_CHANNEL_THRESHOLD_MS, - ).await; + ).await { + tracing::warn!(?e, "create_ephemeral_channel failed"); + } }); } > From e290875860e7646918686bd4b9b5e22d7cc5e861 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 00:38:12 +0000 Subject: [PATCH 5/6] docs: delete stale PLAN.md (duplicates README + CLAUDE.md) - PLAN.md opened with "Built with Rust, libp2p, and Leptos" and named GossipSub/Kademlia/mDNS in its architecture box. Project migrated to iroh; see docs/specs/2026-03-29-iroh-migration-design.md. - README.md (overview + setup) + CLAUDE.md (architecture, crate layout, state-management, test-tier tree) cover everything PLAN duplicated, with current terminology. - Phase checklist (lines 66-157) was a historical artifact; every phase except Phase 9 read COMPLETE but doc didn't track code evolution. Active voice/video state lives in crates/web/src/voice.rs + specs. - "Deployed" section's port mentions (9090/9091) were a third drift site already flagged by audit #627 / PR #630. - Audit #607 explicitly accepted deletion as the principled fix vs rewrite-and-let-it-drift-again. - No references: grep -rn "PLAN.md" returned zero hits across repo. No include_str! / build.rs / test reads PLAN.md, so cargo test gate doesn't apply per the no-asset-coverage rule. Ran cargo fmt --check and cargo check --workspace; both green. Refs #607 --- PLAN.md | 209 -------------------------------------------------------- 1 file changed, 209 deletions(-) delete mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 5a3016d7..00000000 --- a/PLAN.md +++ /dev/null @@ -1,209 +0,0 @@ -# Willow — P2P Chat Platform - -A decentralized Discord replacement for friend groups. No central servers, no -accounts, no middlemen. Built with Rust, libp2p, and Leptos. - -## Vision - -A web app where you and your friends can: -- Text chat with channels, threads, reactions, pins, and emoji -- Voice call, video call, and screen share -- Share files and images peer-to-peer -- Create servers and channels with role-based permissions -- Discover each other on the local network or via relay nodes -- Own your data — everything is end-to-end encrypted - -## Architecture - -``` -┌─────────────────────────────────────────────────┐ -│ Leptos Web App (willow-web) │ -│ Sidebar │ Chat │ Settings │ Files │ Servers │ -│ Welcome │ Pinned │ Members │ Roles │ Typing │ -├─────────────────────────────────────────────────┤ -│ Client Library (willow-client) │ -│ Event-sourced state │ DisplayMessage view-model│ -│ Server management │ Profiles │ Sync │ Typing │ -├─────────────────────────────────────────────────┤ -│ State Machine (willow-state) │ -│ Pure apply() │ 21 EventKind variants │ Merge │ -│ StateHash │ Permissions │ Pins │ Reactions │ -├─────────────────────────────────────────────────┤ -│ Application Layer │ -│ willow-channel │ willow-messaging │ willow-files│ -├─────────────────────────────────────────────────┤ -│ Crypto Layer │ -│ willow-crypto (ChaCha20-Poly1305, X25519) │ -├─────────────────────────────────────────────────┤ -│ Network Layer │ -│ willow-network (libp2p + tokio / wasm-bindgen) │ -│ GossipSub │ Kademlia │ mDNS │ Relay │ Chunks │ -├─────────────────────────────────────────────────┤ -│ Foundation │ -│ willow-identity │ willow-transport │ -│ Ed25519 signing │ Protocol framing │ -└─────────────────────────────────────────────────┘ -``` - -## Crates - -| Crate | Purpose | Status | -|-------|---------|--------| -| `willow-transport` | Binary serialization, protocol versioning, message framing | Done | -| `willow-identity` | Ed25519 keypairs, signed messages, user profiles, PeerId extraction | Done | -| `willow-messaging` | Chat messages, HLC ordering, message store, SealedContent | Done | -| `willow-crypto` | E2E encryption, key ratchet, X25519 key exchange | Done | -| `willow-channel` | Servers, channels, roles, permissions, invites, key rotation | Done | -| `willow-files` | Content-addressed file chunking, SHA-256 hashing, ChunkStore | Done | -| `willow-network` | libp2p networking, chunk transfer protocol (native + WASM) | Done | -| `willow-state` | Pure event-sourced state machine, 21 EventKind variants | Done | -| `willow-client` | UI-agnostic client library, DisplayMessage, server management | Done | -| `willow-web` | Leptos web UI (primary frontend) | Done | -| `willow-relay` | Relay server bridging TCP and WebSocket peers | Done | - -## Roadmap - -### Phase 1 — Foundation (COMPLETE) -- [x] Transport layer with versioned envelopes -- [x] Cryptographic identity with Ed25519 signing -- [x] Message model with Hybrid Logical Clock ordering -- [x] Channel/server/role/permission model -- [x] libp2p networking with GossipSub, Kademlia, mDNS - -### Phase 2 — Working Chat (COMPLETE) -- [x] Wire up network ↔ messaging over GossipSub -- [x] Text input, message rendering with timestamps and author names -- [x] Channel switching, creation, deletion -- [x] Full integration tests with real libp2p network nodes - -### Phase 3 — E2E Encryption (COMPLETE) -- [x] ChaCha20-Poly1305 content encryption (seal/open) -- [x] Per-channel symmetric keys with ChannelKey -- [x] X25519 key exchange (Ed25519 → X25519 conversion) -- [x] Encrypted key distribution via invite system -- [x] Transparent encrypt-on-send, decrypt-on-receive - -### Phase 4 — Forward Secrecy & Key Rotation (COMPLETE) -- [x] KeyRatchet: per-message key derivation via HKDF -- [x] Key rotation on member removal (re-key all channels) - -### Phase 5 — Persistence & Profiles (COMPLETE) -- [x] Identity persistence (Ed25519 keypair saved to disk / localStorage) -- [x] Full ServerState persistence (save/load entire state, no replay needed) -- [x] Per-server profiles with display names -- [x] Profile broadcasting over gossipsub - -### Phase 6 — File Sharing (COMPLETE) -- [x] Content-addressed chunking (SHA-256, 256 KiB) -- [x] Inline file sharing via gossipsub (<256KB) -- [x] Image/GIF embedding (URL detection + uploaded file embeds) -- [x] File cards with download buttons - -### Phase 7 — Server State Sync (COMPLETE) -- [x] Event-based wire format (WireMessage with Events) -- [x] Catch-up on connect: exchange missing events via state hash -- [x] Relay stores events in SQLite, serves to new peers -- [x] Multi-peer state verification (StateVerification + hash tracking) - -### Phase 8 — Event-Sourced State Machine (COMPLETE) -- [x] willow-state crate: pure deterministic state machine (zero I/O) -- [x] 21 EventKind variants (channels, roles, permissions, messages, - edits, deletes, reactions, pins, profiles, server metadata) -- [x] StateHash (SHA-256) for divergence detection -- [x] apply() with permission enforcement and dedup -- [x] Fine-grained Permission model -- [x] merge() for divergent history resolution -- [x] DisplayMessage view-model computed from event_state (never stored) -- [x] Single source of truth: UI reads directly from ServerState -- [x] Legacy Op system fully removed — Events only - -### Phase 9 — Voice & Video (IN PROGRESS) -- [ ] WebRTC-like media transport -- [ ] Voice channels with Opus audio -- [ ] Video with VP8/VP9 -- [ ] Screen sharing - -### Phase 10 — Polish & UX (COMPLETE) -- [x] Discord-style dark/light theme with CSS variables -- [x] IBM Plex typography, refined design -- [x] Welcome/onboarding screen (no default server) -- [x] Create server + join server flows -- [x] Per-server profile editing -- [x] Server settings page (invites, roles, separate from profile) -- [x] Message editing and deletion -- [x] Reply threads with parent preview and jump-to-parent -- [x] Mention highlighting (replies to you) -- [x] Emoji reactions (toggle on click, Discord-style) -- [x] Pinned messages with clickable URLs and jump-to-message -- [x] Typing indicators with debounce and auto-expiry -- [x] Single action dropdown menu (Reply, React, Edit, Delete, Pin, Download) -- [x] Time-gap grouping (5min threshold re-shows author/timestamp) -- [x] URL detection and clickable links in messages -- [x] Member list with online/offline status and role badges -- [x] Mobile member list slide-out panel -- [x] Mobile responsive layout with touch targets -- [x] Auto-focus input on reply/edit -- [x] Notification sounds (Web Audio API) -- [x] PWA manifest + service worker -- [x] Message deduplication (msg_id across sessions) -- [x] Clipboard fallback for non-HTTPS - -### Infrastructure (COMPLETE) -- [x] WASM dual-target support (all crates compile for wasm32) -- [x] Relay server with display name (TCP + WebSocket) -- [x] Cross-platform storage (filesystem / localStorage) -- [x] Cross-platform clipboard (web_sys + textarea fallback) -- [x] Deploy skill for automated builds and deployment -- [x] justfile with all build/test/serve commands - -## Data Flow - -``` -Action → Create Event → apply_event() → ServerState mutated → save_server_state() - → broadcast_event() → peers receive → apply to their state - -UI calls client.messages(channel) → computed Vec from event_state - → display names resolved fresh (never stale) - → reactions, edits, deletes, pins all from authoritative state - → no stored intermediate copies, no manual merging -``` - -## Test Coverage - -353 tests across 7 tiers: - -| Tier | Tests | Scope | -|------|-------|-------| -| Pure state machine | 82 | Determinism, permissions, merges, pins, reactions, replay | -| Client library | 114 | DisplayMessage, server management, typing, dedup, profiles | -| Network integration | 14 | Real libp2p, 3-node topology, file chunks | -| Scaling | 7 | 5/10/20 peers, message flood, 532k events/sec | -| E2E flow | 9 | State machine + network: invite, chat, sync, merge | -| Relay history | 3 | Store events, serve to new peers, multi-peer | -| Browser (Leptos) | 39 | Real DOM, signals, events, all components | -| Web component units | 11 | URL extraction, image detection, MIME types | -| Other crates | ~74 | Transport, identity, crypto, channel, files | - -Run: `just test` (cargo), `just test-browser` (headless Firefox), -`just test-all` (both), `just test-scale` (with output), -`just test-e2e` (end-to-end flow) - -## Key Technical Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Language | Rust | Memory safety, performance, async ecosystem | -| Web UI | Leptos 0.7 CSR | Reactive, WASM-native, component-based | -| Networking | libp2p 0.53 | Battle-tested P2P, NAT traversal | -| State | Event-sourced | Deterministic, mergeable, auditable | -| Encryption | ChaCha20-Poly1305 + X25519 | AEAD + DH key exchange | -| Signing | Ed25519 | Fast, small keys, message authenticity | -| Serialization | bincode | Fast, compact binary format | -| Message Ordering | Hybrid Logical Clocks | No central coordination needed | -| Persistence | Full ServerState save | Instant load, no replay needed | - -## Deployed - -- **Relay**: `172.234.217.219` ports 9090 (TCP) + 9091 (WebSocket) -- **Web app**: `172.234.217.219` via nginx -- **PeerId**: `12D3KooWMBmUF1rHYG5CneKi8JZfKdMAciJd4oCgknTJkbwCUurd` From 18d55644b0caabe82f83de8c4e51e32dba6cf795 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 00:41:32 +0000 Subject: [PATCH 6/6] docs(skill): pre-flight verify audit's claimed mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audits sometimes prescribe a fix predicated on a stated mechanism — log via tracing::warn!, the rebuild Effect picks it up, the existing test would catch this. Pre-flight verify the mechanism actually exists at HEAD AND doesn't violate a module-local constraint (privacy contracts, observability forbids, etc.). Surfaced this run by #622: search/handle.rs module-doc forbids all tracing::* (per local-search privacy spec) AND the rebuild Effect that insert's own doc-comment cites doesn't exist as described — the real recovery is an event-loop subscription with the same do_send fragility. Audit's both options were non-viable as-stated; correct call was ambiguous-fix-path skip without close. Refs #622 --- .claude/skills/resolving-issues/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.claude/skills/resolving-issues/SKILL.md b/.claude/skills/resolving-issues/SKILL.md index c12c1537..2627a6ef 100644 --- a/.claude/skills/resolving-issues/SKILL.md +++ b/.claude/skills/resolving-issues/SKILL.md @@ -84,6 +84,8 @@ Fresh agent per issue, scoped to one issue + master branch ref. Steps: 1. Read the issue. Decide if more context needed. 2. **Research (optional, parallel OK):** spawn research subagents for codebase grep, related-file reads, spec lookups. Synthesize before coding. Re-grep cited line numbers / LOC counts at HEAD before working from issue-body literal positions — they drift fast across refactors and may be off by hundreds of lines after a recent file move. + + **Verify the audit's claimed mechanism, not just its line numbers.** Audits sometimes prescribe a fix that depends on a stated mechanism — "log via `tracing::warn!`", "the rebuild Effect recovers dropped inserts", "the existing CSP test would catch this". That stated mechanism may (a) violate a module-local constraint (e.g. a privacy contract in the module doc forbidding `tracing::*`), or (b) describe code that doesn't actually exist as cited (e.g. a "rebuild Effect" referenced in a doc-comment but not present in any `app.rs` Effect — the actual recovery path is a different shape with the same fragility). **Pre-flight read** the cited file's module-doc + the cited recovery code at HEAD before dispatching. If either check fails, the issue is an **ambiguous-fix-path** (see step 6 of the Core Loop): coordinator-skip without close, note in run-end Lessons Learned, leave for a future session with fresh eyes. Saves a wasted dispatch + a likely rejected commit. Examples surfaced this way: `crates/client/src/search/handle.rs` module-doc forbids `tracing::*` (privacy contract) — kills audit's "log the drop" prescription; the "rebuild Effect" cited in `insert`'s doc-comment is itself misleading (only an event-loop subscription exists, sharing the same `do_send` fragility). 3. **Complexity gate — automated brainstorm + plan when warranted:** - **Trigger any of:** issue spans > 1 crate, fix touches state machine / wire format / migration paths, ≥ 2 reasonable approaches exist, root cause not obvious from issue text, fix likely > 5 files OR > 200 LOC, "it depends" question on scope. - **Skip when:** issue is a one-liner / config swap / typo / clearly mechanical (single rg-pattern site) / has explicit "Suggested fix" the implementer can follow verbatim.