Portable encrypted identity backup compatible with the sister project's format — argon2id KDF + nacl secretbox, peer ID visible in plaintext for pre-import verification. - crypto: ExportIdentity, ImportIdentity, SaveIdentity - proto: export_identity / import_identity IPC commands + events - ipc: export_identity returns encrypted blob; import_identity validates and decrypts (read-only — on-disk write requires daemon restart) - netmgr: MasterIdentity() accessor - daemon: --import-identity / --import-passphrase flags write identity.json and exit, enabling scripted account migration - tests: roundtrip and wrong-passphrase rejection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.6 KiB
Web UI — Design Notes
Invite URL pattern
The invite URL path segment is the truncated network hash — already computed
by the daemon as the first 8 bytes of SHA-256("yaw2-net:" + name).
https://waste.dev.xplwd.com/78aa5621196bf200/
The web UI reads window.location.pathname, pre-fills:
- WebSocket URL:
wss://waste.dev.xplwd.com/ws(or/<hash>/signal) - Network ID: extracted from the path — no separate field needed
Fragment vs path
Using /#78aa5621196bf200 instead of a path means the network ID never
reaches the anchor's access logs. The anchor cannot distinguish an invite
visit from a regular visit. Slightly more private — worth deciding before
building the React routing layer.
Per-network signal path
Moving the WebSocket endpoint to /<nethash>/signal enables nginx to route
/<hash>/signal to the anchor and /<hash>/ to a CDN or static host.
The anchor never has to serve HTML. Keeps concerns cleanly separated.
Anchor changes needed (if it serves the UI)
Right now cmd/anchor only handles WebSocket on /ws. To support the
invite URL pattern it would also need to serve static files (the React
bundle) for any other path, and optionally move the WebSocket endpoint to
/<nethash>/signal for per-network isolation.
Open decision: does the anchor serve the UI at all?
Two options — decide before building the React routing layer:
A) Anchor is purely a signaling relay
Static files served from a CDN or separate host. Anchor only handles
WebSocket. Simpler, easier to scale, no Go HTTP file serving code.
nginx routes /<hash>/signal → anchor, everything else → static host.
B) Anchor serves the React bundle Single deployment, one domain. Anchor handles both WebSocket and static file serving. More convenient but mixes concerns and means deploying a new anchor binary every time the UI changes.
Invite expiry
Encode a TTL in the invite (e.g. 72h). The anchor rejects join attempts on expired tokens. Permanent invites are a liability — a leaked link stays open forever.
Privacy & safety — URL invites / anchor
- Use the network hash in the URL, not the name. A base64'd name is trivially reversible. The hash reveals nothing about the network or its members.
- Link previews will betray you. iMessage, Slack, WhatsApp etc.
pre-fetch
https://links for preview generation. That pre-fetch hits the anchor and effectively probes the network. Serve a generic preview (no network info in og:tags), or use a#fragment— fragments never leave the browser. - The anchor is a metadata oracle. It can't read content but sees who connects, when, and how often. Log as little as possible — no IPs beyond what's needed to route, no persistent connection records. stderr only, no disk writes.
Privacy & safety — identity / contact cards
- Private key never leaves the device. In Tauri, store in OS keychain via Tauri's secure storage — not localStorage.
- Make public vs private explicit in the UI. The card is a public address. Never show the private key, not even "for backup."
- Aliases are not authenticated — say so. Anyone can claim any alias, including yours. The peer ID is the real identity. Make the short 4-group hex ID glanceable so users build the habit of verifying it.
- Contact cards expose your anchor URL. If Alice shares her card and later wants to cut someone off, they still know her anchor. Consider supporting anchor rotation or anchor-less cards for LAN scenarios.
Privacy & safety — trust model
- Default-deny inbound connections. Unknown peers get
byebefore any data flows. The pending prompt should show the peer ID, not just the claimed alias. - Mutual acceptance before any messages. Don't buffer messages from unaccepted peers. Nothing stored until both sides have accepted.
- Removal is immediate. Close the DataChannel, remove from accepted
list, send
bye. Don't wait for reconnect. - Block list separate from accept list. Removing a contact means "not accepted." Blocking should actively refuse — important if they still know the anchor URL.
Tauri / local daemon
- IPC binds localhost only. Already the case — keep it. In Tauri, use a random port chosen at startup (written to a local socket file) rather than a fixed port.
- No auto-join on startup. Invites are processed only when the UI is open and the user confirms.
- Clear data means clear data. Uninstall / "delete account" must wipe the SQLite store, the identity key, and all cached peer data. Don't rely on the OS.
Onboarding flow (contact card model)
Inspired by the Friends app pattern:
- App generates an identity on first launch.
- User picks a nickname — advisory only, not authenticated.
- User copies their contact card (
yaw:<peerid>?n=alias&a=wss://anchor). UI makes clear: this is your public address, not a password. - User pastes a friend's card into an Accept box, optionally sets a local nickname for them.
- Trust is mutual — connection completes only once both sides have accepted each other's card.
- Pending inbound connections show peer ID + claimed alias; user approves or blocks.
Daemon changes needed
accepted_peerstable in SQLiteaccept_peer/remove_peer/block_peerIPC commands- After hello verification: check allowlist — send
byeand close if not accepted; emitpending_peerevent if unknown - Network concept may simplify to "your contact list" for the personal use case; named group networks remain as a separate concept for group chats
End-game stack
- React + Tauri standalone desktop application
- Go daemon runs as a Tauri sidecar
- React talks to daemon via existing IPC (local TCP, bridged through Tauri's invoke API)
- Anchor stays as a lightweight relay — no content, minimal metadata
Anchor host as onboarding hub
The anchor host serves a web UI regardless of which client the user ends up on. It is the universal entry point:
- New user follows an invite link → lands on the web UI → creates an identity → joins the network
- Existing TUI user wants to switch to Tauri client → exports identity from current client → imports into new one
- Mobile user with no install → uses the web UI directly
nginx serves the static React bundle at /. The anchor handles WebSocket
only. No Go HTTP file serving needed — clean separation.
Identity portability
The identity (Ed25519 keypair + alias) is the one thing that ties all clients together. It must be portable, stable, and independently documented.
Portable identity format
Use the same format as the sister project for interoperability:
{
"yaw": "yaw-key-backup-1",
"id": "<hex peer id>",
"alg": "argon2id-secretbox",
"ops": 2,
"mem": 67108864,
"salt": "<base64>",
"nonce": "<base64>",
"ct": "<base64 ciphertext>"
}
yawis the format version tagidis the public peer ID (hex) — visible without decrypting, useful for confirming you're importing the right identityalgsignals argon2id KDF + nacl secretbox encryptionops/memare argon2id parametersctunseals to the raw Ed25519 private key + alias
The passphrase is the only secret — the file itself is safe to copy anywhere. Same format means credentials backed up via the sister project can be imported directly into waste and vice versa.
Migration flows
TUI → Tauri client
waste-daemon export-identity --out identity.enc(or IPC command)- Copy file to new machine, import in Tauri onboarding screen
Web UI → any client
- Web UI shows "export your identity" → downloads the encrypted file
- User imports into TUI or Tauri with passphrase
New user via web UI, later installs Tauri
- Creates identity in browser (stored in secure browser storage)
- Exports encrypted file at any point
- Imports into Tauri — same peer ID, same contacts, history syncs via peers (not server)
QR code transfer (mobile / LAN)
- Encrypted identity blob encoded as QR
- Scan on new device, enter passphrase
- No file transfer needed
Open decisions
- Does the web UI generate and hold the private key in-browser, or does it proxy through a server-side session? (In-browser is safer — key never leaves the device even via the anchor host.)
- Browser storage for the key: IndexedDB + WebCrypto non-extractable key, or just the encrypted blob with passphrase re-entry on each session?
- History portability: messages are local-only today. Cross-client sync would require either exporting the SQLite file or accepting that history starts fresh on each new client.