wails build -o with a relative path doesn't resolve against the shell's
CWD — Wails joins it onto its own build/bin/ output dir, so the binary
landed in cmd/app/dist/ instead of the top-level dist/ the release step
globs. Build with the default name and cp it into dist/ explicitly.
Also document how to download and run the prebuilt daemon/anchor
release binaries on Linux, macOS, and Windows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TUI /react shows usage hint on bad/missing args instead of silently no-oping
- README: document /join /net /react slash commands and Ctrl+N keybinding
- README: add send_reaction IPC command and reaction IPC event to protocol reference
- FUTURE.md: mark reactions, link previews, responsive mobile layout, TUI
multi-network and TUI reactions as shipped; add prose sections for each
- EXTENSIONS.md: add EXT-008 documenting reaction wire protocol, SQLite
schema, IPC commands/events, history replay, and browser-mode implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Multi-network:
- Left panel now shows Networks section above Rooms
- /join <name> joins a new network at runtime; -network flag now optional
- ctrl+n cycles between joined networks; /net <n|name> switches by number or name
- All state (rooms, peers, messages) scoped per network via netData struct
- Messages keyed by "netId:room" (same as web store)
Reactions:
- /react <emoji> — react to the last message in the current room
- /react <n> <emoji> — react to message number n
- Reactions displayed as a dimmed line below each message: 👍 2 ❤️ 1
- EvtReaction handler updates in-place and refreshes viewport
History:
- EvtHistoryLoaded now handled: historical messages prepended, deduped by mid, sorted by time
UX:
- Each message prefixed with [n] line number so /react targets are unambiguous
- -network flag is now optional (start idle, /join to connect)
- Status bar hint updated with new commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Include mid on the wire in sendChat() so recipients can reference it
- Handle incoming 'reaction' wire message in PeerConn._onControl
- Add PeerConn.sendReaction() to send the reaction wire message
- Handle 'send_reaction' IPC command in BrowserAdapter.send()
- Emit 'reaction' IPC event on receive and on local send
- Use wire mid (not synthetic peer-ts mid) when receiving chat messages
Reactions now work across all combinations: daemon↔daemon,
browser↔browser, and daemon↔browser.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On screens ≤600px the sidebar collapses off-screen. A hamburger button
in the message header opens it as a slide-over drawer with a dim
overlay; tapping a room/network or the ✕ button closes it. Desktop
layout is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The form had no submit button so there was no visible way to confirm.
Enter should have worked but was unreliable with two inputs. Added a
disabled-until-valid Join button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two networks sharing a room name (e.g. "general") were clobbering each
other's message lists. Now keyed by `${networkId}:${room}` throughout
the web store, MessagePane, and Sidebar DM detection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sidebar: "+" button next to Networks opens an inline join form;
in browser mode shows anchor URL field; saves to waste_saved_networks
- Onboarding: saved networks shown as quick-join chips above the join
form; auto-rejoin all saved networks on reconnect (not just one)
- Browser mode: networks persisted as [{name, anchor}] array in
localStorage (waste_saved_networks); legacy waste_last_network kept
for backward compat
- Daemon mode: daemon already tracks joined networks; web UI just needs
the "+" to send join_network for additional ones
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Timestamps now show "Jun 28 10:58" for older messages (dropped
"Yesterday" which overflowed the fixed-width column)
- Timestamp column widened from 52px to 72px to fit date+time
- state_snapshot now includes known_peers (historically seen, not
currently connected) so the UI can resolve aliases in history
- MessagePane aliasFor falls back to knownPeers map
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MessagePane: show date in timestamps (Yesterday/MMM D) for messages
not from today; divider now appears at the top of history on load
rather than waiting for a live message to create the boundary
- TUI: same date-aware timestamp formatting (Yesterday / Jan 2)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
history_loaded events were fired when peers exchanged history gossip,
but the browser UI often connects to the daemon after that handshake
has already happened. Now the daemon pushes recent messages for all
known rooms immediately after the state_snapshot on each new IPC
connection, so the browser always gets history regardless of timing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. RecentMessagesSince had msg_id IS NOT NULL filter — messages sent
before EXT-007 deployment all have msg_id=NULL so nothing was returned.
Removed the filter; mid-based INSERT OR IGNORE dedup is sufficient.
2. queryMessages didn't SELECT room, so gossipped messages had empty room
field. Added room to SELECT and Scan in queryMessages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
file_complete in daemon mode carries transfer_id but no offer field.
The old condition required msg.offer, so the progress/cancel row was
never removed. Now clears using transfer_id first, offer.xid as fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Daemon scans the download directory for .tmp.meta sidecars on network join
and emits resumable_transfers IPC event. Web UI shows them in the Transfers
panel with a dimmed progress bar and "will resume on reconnect" note.
- proto: ResumableFile type, EvtResumableTransfers, resumable_files IpcMessage field
- mesh: ScanResumable() scans download dir and emits the event
- netmgr: call ScanResumable() after join (both Join and JoinByHash paths)
- web: resumableFiles store state, resumable_transfers handler, Transfers UI section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- store: handle history_loaded event — prepend gossipped messages,
dedup by mid, sort by ts, record cutoff timestamp per room
- MessagePane: show "earlier messages" divider between history and
live messages based on the cutoff timestamp
- types: add history_loaded, room_created, create_room to IpcMsgType;
add messages field to IpcMessage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The CI runner resolves @emnapi deps differently than local npm, causing
npm ci to fail regardless of Node version. npm install is more forgiving.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After hello verification, the connecting peer sends history_request to
the first peer it meets (one per room, no fan-out). The responder queries
SQLite and replies with a history_chunk. Received history is stored via
INSERT OR IGNORE (mid dedup) and emitted as history_loaded IPC events.
- proto: MsgHistoryRequest/Chunk types, HistoryEntry, EvtHistoryLoaded,
ComputeMsgID (sha256 content-addressed ID), MsgID field on ChatMessage
- store: ALTER TABLE ADD COLUMN msg_id + unique index migration (idempotent);
RecentMessagesSince query (msg_id IS NOT NULL filter); msg_id persisted on save
- mesh: RequestHistoryFrom, HandleHistoryRequest, HandleHistoryChunk methods;
historyRequested/historyFirstPeer state to ensure single-peer requests
- peer: dispatch history_request/history_chunk; RequestHistoryFrom after hello;
stamp MsgID on incoming chat messages
- ipc: stamp MsgID on outgoing group chat messages
- EXTENSIONS.md: EXT-007 documented
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
package-lock.json was generated with npm 11 (Node 24). CI was running
Node 20 which resolves @emnapi deps differently, causing npm ci to fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
libwebkit2gtk-4.0-dev was dropped in Noble; use 4.1 and pass
-tags webkit2_41 to wails build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gitea act_runner intercepts actions/upload-artifact regardless of version
tag and uses an incompatible built-in. Restructured as one job that builds
all server binaries (cross-compiled) and the Linux desktop app, then
publishes directly to the release — no artifact handoff needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
upload-artifact@v4 and download-artifact@v4 are not supported on GHES.
Also drop merge-multiple (v4-only) and adjust release glob to artifacts/*/*.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three paths: browser-only, desktop app download, headless daemon.
Covers invites, anchor server basics, and mobile install.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Daemon:
- Per-network download directories (-download-dir flag, set_download_dir IPC)
- File transfer resume after disconnection: .tmp.meta sidecars survive
interruption; resume_offset in file-accept lets sender seek and continue
- set_download_dir IPC command; download_dir reported in state_snapshot
Protocol:
- PeerMessage.ResumeOffset (EXT-006) for file transfer resume
- IpcMessage.ShareNetworks json tag changed from "networks" to "network_ids"
to fix duplicate json tag collision with Networks []NetworkInfo
Desktop app (cmd/app):
- Wails v2 shell embedding daemon logic directly (no subprocess)
- System tray on Linux/Windows via getlantern/systray; macOS hides to Dock
- OS notifications for message_received and file_complete via Wails events
- notray build tag for headless/CI builds without GTK tray headers
- build-app.sh: builds web frontend, copies dist, runs wails build
Web / PWA:
- manifest.json + Apple touch icon meta tags for mobile "Add to Home Screen"
- PNG icons (192px, 512px, 180px) generated from SVG
- Wails EventsOn("notify") hook in App.tsx for native OS notifications
CI:
- .gitea/workflows/build.yml: server binaries cross-compiled for 5 platforms,
desktop app for Linux amd64, release artifacts published on v* tags
Docs:
- README: download dir, file transfer resume, desktop app, PWA, CI sections
- EXTENSIONS.md: EXT-004 daemon mode marked shipped; EXT-006 resume added
- FUTURE.md: roadmap updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rooms that receive a message while not active show a * prefix in the
sidebar. The marker clears when you tab to that room.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/room <name> in the TUI sends create_room to the daemon, which persists
it in the rooms SQLite table and echoes room_created back. state_snapshot
now includes persisted rooms so they survive reconnects. Tab navigation
and room rendering pick them up automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-turn-url and -turn-secret flags on the daemon; credentials generated
using coturn use-auth-secret HMAC-SHA1 scheme (same as browser mode).
ICEServers field on mesh.Mesh threads extra ICE servers through to
every PeerConnection created by the anchor client.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Signed invites: waste: URI gains inviter+sig fields (Ed25519); hello
carries the invite so receiving peers can verify against known keys
- RequireInvite per-network flag: rejects peers without valid signed invite
- Hash-based hang links: #waste:base64 fragment pre-fills join form without
server-side leakage of network name
- Multi-share: shares.json (daemon) + waste_shares localStorage (browser);
IPC add_share/remove_share/list_shares commands
- EXTENSIONS.md: addendum documenting all waste-go protocol deviations from
YAW/2; all extensions are additive and backward compatible
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Daemon side:
- internal/shares: new package reads/writes shares.json next to identity
- proto: add ShareEntry type, add_share/remove_share/list_shares IPC commands,
shares_list event, path field on FileEntry
- netmgr: load shares.json on startup; ScanAllShares combines legacy ShareDir
with shares.json entries (recursive walk with relative paths)
- mesh: ScanFiles callback lets manager inject multi-share scanning without
mesh knowing about shares.json
- ipc: handle add_share, remove_share, list_shares commands
Browser side:
- ShareManager component replaces FolderPicker: shows list of named shares
with remove/re-pick buttons; persists share records to waste_shares in
localStorage (name, global flag, networkId)
- FolderPicker retained for internal use; Sidebar now uses ShareManager
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- FolderPicker: uses webkitRelativePath so paths like docs/report.pdf
are preserved; adds "include subfolders" checkbox (default on)
- FileBrowser: virtual directory tree with breadcrumb navigation,
sort by name/size, search filter, folders shown before files
- Protocol: browse sends path alongside name; get uses path as key
- FileEntry type gains optional path field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
coturn's use-auth-secret mode requires the TURN password to be
HMAC-SHA1(secret, username), not the raw secret. Pass ice servers
as a constructor argument so the async credential generation happens
before RTCPeerConnection is created.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Committed a generated hex string that looked like a real secret.
Replaced with YOUR_SECRET_HERE to make it obviously an example.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace placeholder YOUR_RANDOM_SECRET with an actual openssl rand -hex 32
output, with a note to generate a fresh one. Keeps the example copy-pasteable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Browser adapter reads turnURL + turnSecret from WASTE_CONFIG and adds a
TURN ICEServer entry automatically. Uses time-limited credentials
compatible with coturn's use-auth-secret mode.
README: new section under Hosting with coturn install, turnserver.conf,
firewall note (UDP 3478, no NPM needed), and config.js snippet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Emit peer_connected + peer_status(connecting) as soon as a PeerConn is
created, so peers appear in the sidebar even if ICE is still negotiating
or ultimately fails. The alias updates in-place once the DataChannel hello
arrives with the real nick.
Fixes: mobile/CGNAT peers being completely invisible when STUN hole
punching fails — they now show with a connecting/failed dot instead of
not appearing at all.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Additional rooms:
- Store tracks customRooms per networkId; createRoom action slugifies and
deduplicates, switches active room on creation
- Sidebar: + button next to Rooms label opens an inline input; Escape
dismisses; Enter creates the room
Per-network share directories:
- Store tracks sharedFilesByNetwork keyed by networkId instead of a single
global map; setSharedFiles(files, networkId?) defaults to activeNetworkId
- FolderPicker reads per-network map to display accurate file count label
File transfer UX:
- Incoming file-offer now emits pending_offer instead of auto-accepting;
user sees Accept / Reject buttons in the sidebar Transfers section
- Progress events emitted per chunk during receive; Transfers panel shows
a live progress bar with received/total sizes
- file-cancel protocol message: sender or receiver can cancel an in-flight
transfer; DataChannel is closed and buffers discarded
- PeerConn: acceptOffer(), rejectOffer(), cancelRecv(), cancelSend()
- BrowserAdapter: acceptOffer(), rejectOffer(), cancelTransfer()
- Store: pendingOffers, fileProgress, acceptOffer, rejectOffer, cancelTransfer
- Transfers component renders in sidebar; auto-hides when no activity
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark web UI, peer gossip, and identity backup as shipped. Expand TURN
section with concrete implementation notes (one-liner ICEServers change +
3 daemon flags). Add per-network share directories and additional
channels/rooms as next items. Remove stale "not implemented" notes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document the folder sharing, peer file browsing, and direct push (📎)
workflows added in the previous commit. Clarify that browser mode also
activates locally when config.js sets signalURL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements both pull (browse & download from a shared folder) and push
(send a file directly to a peer) using the yaw2 file protocol over WebRTC
DataChannels.
- PeerConn: handle browse/files/get/file-offer/file-accept/file-done
control messages; stream files in 64KB chunks over f:xid binary
DataChannels; auto-accept incoming offers (push and pull)
- PeerConn.sendFilePush(): initiate an outbound file transfer without
requiring the peer to browse first
- BrowserAdapter: sharedFiles map, setSharedFiles(), requestBrowse(),
requestGet(), sendFileTo() — propagates share map to all active peers
- Store: activeFilePeer, setActiveFilePeer, setSharedFiles, browseFiles,
sendFileTo; file_complete handler triggers browser download automatically
- FileBrowser component: third-column panel listing a peer's shared files
with name, size, and a download button
- FolderPicker component: webkitdirectory input in the sidebar (browser
mode only) for selecting a local folder to share
- Sidebar: 📎 send-file button on peer rows (hover) with inline file picker
- App.tsx: use browser mode when WASTE_CONFIG.signalURL is set, even on
localhost — allows testing browser mode via npm run dev with config.js
- deploy-daemon.sh: kill existing daemon before scp to avoid upload failure
when the binary is locked; remove duplicate kill block in restart step
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace bold duplicate-looking self row with dimmed name + small accent
"you" badge. Removes the ID (already shown in identity section at top).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch back to flex, set explicit widths: 52px timestamp (nowrap, 24h),
90px alias (right-aligned). Eliminates wrapping and the large gap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Messages now use a fixed grid (44px ts · 120px alias right-aligned · 1fr text)
instead of flex, so text always starts at the same column regardless of alias length.
Self shown at top of peer list with accent dot and bold name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hooks pc.OnConnectionStateChange in WireDataChannel to emit peer_status
events. On connected, calls pc.GetStats() to find the nominated candidate
pair and report candidateType (host/srflx/relay) and remote address.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Peers moved from right panel into sidebar below Rooms
- Messages left-aligned IRC style: time · alias · text
- Per-peer colored dot: green=direct/NAT-punched, amber=relay/connecting, red=failed
- Hover title on peer row shows full ID, connection type, remote address, last seen
- Removed right PeerList panel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Go daemon was using higher-ID-offers while the browser adapter and
yaw2 use lower-ID-offers. This meant daemon↔browser pairs where the
daemon had the lower ID would never connect — neither side would offer.
All three comparison sites in anchor/client.go flipped: > 0 → < 0,
<= 0 → >= 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
nginx-waste.conf: serve web/dist/ at root, proxy /ws to anchor (port
8080), SPA fallback for clean invite URLs, immutable cache for assets.
deploy-web.sh: build + rsync to anchor host, skip config.js so the
host's runtime config is not clobbered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BrowserAdapter speaks yaw/2.1 signaling + WebRTC protocol entirely in
the browser (libsodium-wrappers for Ed25519/X25519). Identity stored in
localStorage; peers connect via any waste/yaw2 anchor.
When served from a non-localhost origin the app defaults to browser
mode. On localhost the daemon adapter is tried first with a one-click
switch to browser mode. config.js (gitignored, served by anchor) lets
the host pre-inject signalURL and other defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Protocol:
- invite.go: include full 64-char `net` hash in waste: invite blob
(matches yaw2's `net` field — any client parsing the base64 JSON can
join without knowing the plaintext name). Expose NetHash() helper.
- netmgr: add JoinByHash() — join via full 64-char hex hash alone,
storing the short ID as display name. Enables joining yaw2 networks
from a URL that only carries the hash.
- anchor: expose RunByHash() so netmgr can pass a pre-computed hash
directly without a name→hash roundtrip.
- ipc/proto: add network_hash field to join_network — routes to
JoinByHash when present and network_name is absent.
Web UI:
- Parse ?net=<64hex> (yaw2 URL param) and ?a=<anchor> in addition to
existing ?n= / ?invite= params. Hash-only joins send network_hash.
- Sidebar shows yaw: contact card (yaw:<masterID>?n=<alias>) using the
master identity — compatible with yaw2 contact card format. Click to
copy to clipboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Messaging:
- Append \n to WS IPC sends so bufio.Scanner fires (messages from web
were silently dropped — the line scanner never saw a newline)
- Handle session_ready in web store to update peer aliases after hello
Alias resolution:
- Seed AddPeer alias from SQLite cache so returning peers resolve
immediately, before hello exchange completes
- Remove stale SavePeer call from AddPeer (was writing placeholder IDs)
- Add PeerAlias() point-lookup to store
Onboarding redesign:
- Three-state UI: disconnected (daemon instructions), connecting,
connected-no-network (join form)
- Read ?n= / ?network= / ?anchor= / ?invite=waste:... URL params to
pre-fill the join form for invite links
- Show daemon alias + peer ID before any network is joined
(master_alias + master_id now included in state_snapshot)
- Identity backup: export (passphrase-protected yaw-key-backup-1 JSON,
download button) and restore (paste + passphrase verify)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously --ws-port started WS in a goroutine then called Run() which
blocked; if TCP IPC failed it took the whole process down even though WS
was up. Now both run in goroutines and the first error from either kills
the daemon.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
React + TypeScript UI on port 5274. Connects to local daemon via
WebSocket IPC (DaemonAdapter). Full chat layout with sidebar, message
pane, and peer list. Mirrors the TUI feature set.
- src/types.ts — IPC types mirroring proto.go
- src/adapter/daemon — WebSocket IPC adapter with auto-reconnect
- src/store/index.ts — Zustand store; handles all daemon events
- src/pages/ — Onboarding (connect + join) and Chat
- src/components/ — Sidebar, MessagePane, PeerList
Dev: cd web && npm run dev (port 5274)
Build: cd web && npm run build
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add --ws-port flag to waste-daemon; when set, starts a WebSocket IPC
server alongside the existing TCP one. The web UI connects to this from
the browser. Uses nhooyr.io/websocket with localhost-only origin policy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Local-only scripts for deploying the anchor and launching the TUI —
not intended for the repository.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a peer's hello is verified, send a gossip burst of currently
connected peers so the new peer can discover and connect to nodes it
hasn't met yet — without relying on the anchor for a full peer-join
notification for each one.
- mesh: add PendingConnect chan for gossip-discovered peer IDs
- peer: send gossip burst after hello; push unknown peers from incoming
gossip onto PendingConnect (skip self + already-connected)
- anchor: drain PendingConnect per runOnce, initiate startOffer for
unknown peers using the same lexicographic tiebreak as anchor join;
drain goroutine is scoped to the runOnce lifetime to avoid using stale
sessions after a reconnect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each network now carries its own share dir, set at join_network time via
optional share_dir field or updated live with set_share_dir. The global
-share-dir daemon flag becomes a fallback default.
- proto: add ShareDir/DownloadDir to NetworkInfo and IpcMessage
- netmgr: Join accepts shareDir override; SetShareDir updates live
- ipc: wire join_network share_dir and set_share_dir command
- daemon: remove -share-dir from auto-join path (pass "" for default)
- test-network.sh: per-network join with share_dir; isolation verification
section confirms alice/friends and alice/work share dirs are independent
- test-tui.sh: join_network with share_dir; peer IDs resolved after join
All tests pass: YAW/2.1 FS, share isolation, file transfer, persistence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark as done: TUI, WebRTC/ICE/STUN, multi-network, invite system,
file transfer, YAW/2.1 forward-secret signaling.
Remove stale content: "TCP with custom framing" transport section,
old roadmap priorities (waste-relay, UDP hole punching, QUIC) that
are now handled by WebRTC/pion.
Remaining work: per-network share dirs, peer gossip (anchor-free
reconnection), file transfer UX improvements, native UI, optional TURN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Upgrades the signaling layer from static X25519 (2.0) to per-session
ephemeral X25519 (2.1). Recorded signaling traffic cannot be decrypted
even if long-term Ed25519 keys later leak, because esk is zeroed on
session close.
Protocol:
- Each peer generates a fresh X25519 keypair (esk/epk) per session.
- Peers exchange signed `ekey` messages sealed under static keys before
the offer/answer. Offer/answer/candidate payloads are then sealed with
ephemeral keys (crypto_box(·, peer_epk, my_esk)).
- ekey sig binds both peer IDs and the epk to prevent replay to third parties.
- Offerer waits up to 2 s for the peer's ekey; if none arrives it falls back
to YAW/2.0 static-key sealing and logs "2.0 fallback offer".
- 2.0 peers silently ignore the unknown `ekey` kind — full interop preserved.
Implementation:
- crypto.go: add EphemeralKey.PublicRaw/PrivateRaw/Wipe helpers.
- proto.go: add SigEkey kind; EPK/V/EkeySig fields on SignalingPayload.
- anchor/client.go: replace flat pcs map with peerSession struct tracking
ephemeral keys, peerEPK, and fs flag; openBoxAuto tries ephemeral then
static; sealAndSend chooses seal based on session state.
- test-network.sh: pipe daemon stderr through tee to daemon.log; add
YAW/2.1 FS verification section.
- test-tui.sh: same daemon.log capture.
- README.md: document 2.1 forward secrecy, file transfer IPC, updated roadmap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
File chunks travel over a per-transfer "f:<xid>" WebRTC DataChannel —
direct DTLS-encrypted P2P, the anchor never sees file data. Once the
initial handshake is done the anchor can disappear and transfers continue.
Key design choices:
- Receiver sends "ok" on the file DC before sender streams, eliminating
a race where tiny files could be fully sent/closed before the receiver's
OnMessage handler is registered (open-race §6 analogue for data DCs).
- Auto-accept: receiver accepts every incoming offer immediately.
- Download dir: per-network at <data-dir>/downloads-<netid>/.
- Backpressure: bufferedAmountLowThreshold to avoid overwhelming the DC.
- SHA-256 verified on receive; mismatches emit EvtError and discard temp file.
- IPC: send_file {peer_id, path} → offers the named file from share dir.
- EvtFileComplete {transfer_id, path} emitted on success.
IPC command: {"type":"send_file","peer_id":"<hex>","path":"<filename>"}
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Share directories are now created under \$DATA_ROOT/<peer>/share/
with generated dummy files instead of pointing at /home/frejoh/Downloads.
Tests are now fully self-contained and safe to share.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five interop issues fixed against PROTOCOL.md:
- Join signature now covers nonce_raw || net_ascii (64-char UTF-8 hex
string) as specified in §5.1, not net_raw_bytes. Both anchor server
and client updated to match.
- Chat wire fields renamed to spec names: text/ts (Unix ms int64)
replacing body/sent_at (ISO timestamp). Flat layout on PeerMessage
matches §8 exactly; store and TUI updated accordingly.
- Direct messages now use the spec "pm" type (flat {type,mid,text,ts})
instead of chat+to. Receiver reconstructs a ChatMessage with
dm:<short-id> room for IPC/storage. §8 compliant.
- File transfer message types changed to spec hyphenated names:
file-offer, file-accept, file-cancel, file-done with spec field
names (name/size not filename/size_bytes). §9 compliant.
- DataChannel open-race (§14 gotcha #3) fixed with sync.Once: doOpen
fires on OnOpen callback or immediately if the channel is already
open when WireDataChannel is called (answerer race).
Also fixes two bugs found during testing:
- mid was missing from outgoing wire messages, causing all received
messages to arrive with mid="" and collide on the UNIQUE DB
constraint. mid is now included on all sent chat/pm messages; a
random mid is generated for any received message that omits it.
- Test scripts hardened: kill -9 + active lsof polling replaces blind
sleep for port cleanup; join_network sent before peer_field queries
(local_peer is now network-scoped and nil until joined).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- proto: FileEntry, FileListResp types; MsgFileListReq/Resp msg types;
CmdGetFileList + EvtFileList IPC types; Files field on IpcMessage
- mesh: ShareDir field + ScanShareDir(); on DataChannel open, auto-send
MsgFileListReq to new peer; handle MsgFileListReq (scan + reply) and
MsgFileListResp (emit EvtFileList to IPC subscribers)
- ipc: get_file_list command — own list returned immediately; remote peer
list requested via DataChannel (response arrives as EvtFileList event)
- daemon: -share-dir flag wired to mesh.ShareDir
- test scripts: pass -share-dir /home/frejoh/Downloads/{alice,bob,charlie};
test-network.sh verifies each peer's own file list via get_file_list
- FUTURE.md: document per-network share directories and multi-network design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- IPC examples now show correct ChatMessage shape (mid, to, sent_at),
DM syntax, generate_invite command, and all event types
- New Onboarding section shows the Alice→Bob invite flow end-to-end
- TUI options table: add -join flag
- TUI key bindings: add ctrl+i and Esc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/invite: Encode/Decode waste:<base64json{anchor,network}>
- proto: CmdGenerateInvite + EvtInviteGenerated + InviteString field
- ipc: track active network name; handle generate_invite (returns invite
string when joined to a network, errors if not joined or no anchor)
- daemon: --join <invite> flag — decodes anchor URL + network name,
sets anchor and auto-joins on startup
- tui: --join <invite> flag — extracts network name, skips -network
requirement; ctrl+i generates invite and shows it full-screen;
Esc dismisses the invite overlay
- FUTURE.md: document multi-network derived-identity design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TUI: replace per-call bufio.Scanner with a goroutine+channel reader
(lineReader) so a single scanner lives for the connection lifetime;
previous pattern silently dropped messages due to scanner read-ahead
- test-tui.sh / test-network.sh: add `return 0` to wait_port — when the
port opens and the while condition becomes false, bash returned the
condition's exit code (1) to the caller, tripping set -e immediately
- .gitignore: use /tui (root-only) instead of tui to avoid ignoring cmd/tui/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Connects to a running daemon's IPC port on startup
- Sends join_network then get_state; listens for events in real time
- Three-pane layout: room list (left), message history (centre), peers (right)
- Tab/Shift+Tab to switch rooms, Enter to send, PgUp/PgDn to scroll
- DM rooms appear automatically when a DM arrives
- test-tui.sh boots the full stack (anchor + 3 peers) and opens the TUI
as alice, with bob and charlie sending periodic messages as live noise
- README: layout diagram, key bindings, TUI section; roadmap item marked done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ipc: CmdSendMessage now routes to the named recipient only (m.SendTo) when
the "to" field is set, rather than broadcasting to all peers. Group messages
continue to use Broadcast. This is the correct privacy behaviour for DMs.
test-network.sh:
- Resolve each peer's hex id from get_state before joining the network,
using a retry loop with timeout to handle slow daemon startup.
- Added a DM section: alice→bob, bob→alice, charlie→alice, alice→charlie.
DMs use the "to" + "room":"dm:<peer_id>" convention.
- Fixed jq -cn (compact) instead of jq -n (pretty) so the IPC newline-
delimited protocol receives a single JSON line per command.
- pretty() now renders DMs as 📨 DM <from→to> rather than 💬 #room.
- Persistence check now breaks down totals by group vs DM message count.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each daemon writes to <data-dir>/messages.db on startup. Messages received
or sent are stored immediately; duplicate mids (INSERT OR IGNORE) are safe
to call multiple times. Peer aliases are upserted on peer_connected and
again after hello verification when the real nick is known.
Schema
- messages(mid UNIQUE, room, from_peer, body, sent_at) — mid is the YAW/2
dedup key added in the proto migration; index on (room, sent_at) for
efficient per-room queries.
- peers(peer_id PK, alias, last_seen) — cache of every peer ever seen,
used to resolve hex ids to names when peers are offline.
Wiring
- store.Open called in cmd/daemon/main.go, passed to mesh.New.
- mesh.Mesh holds *store.Store (nil-safe; persistence is optional).
- mesh.SaveMessage called in dispatchPeerMessage (incoming) and ipc
CmdSendMessage (outgoing) so the local node's own messages are stored.
- mesh.UpdatePeerAlias called after hello verification updates the alias
with the verified nick rather than the placeholder short-id.
Messages only accumulate from join time forward — no history replay to
late-joining peers; each node's view starts from when it connected.
test-network.sh: added SQLite verification block that queries each node's
DB after the test and prints message + peer counts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test-network.sh: shell script that starts anchor + alice + bob + charlie,
joins them to a named network, exchanges five messages, then has charlie
leave — verifying the full join/chat/leave cycle with coloured per-peer output.
ipc: two fixes exposed by the test script:
- Network join context was per-IPC-client, so the join was immediately
cancelled when the nc connection closed. Lifted join/leave state to the
Run() level (shared across all clients, protected by a mutex) so the
network stays connected independent of which client issued the command.
- Event-pusher goroutine could panic with "send on closed channel" when
the command loop closed writeCh while the pusher was mid-select. Added
defer recover() to both the pusher goroutine and the send helper, and
removed the default arm so the select blocks cleanly on done.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>