# 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 `//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 `//signal` enables nginx to route `//signal` to the anchor and `//` 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 `//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 `//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 `bye` before 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: 1. App generates an identity on first launch. 2. User picks a nickname — advisory only, not authenticated. 3. User copies their contact card (`yaw:?n=alias&a=wss://anchor`). UI makes clear: *this is your public address, not a password.* 4. User pastes a friend's card into an Accept box, optionally sets a local nickname for them. 5. Trust is mutual — connection completes only once both sides have accepted each other's card. 6. Pending inbound connections show peer ID + claimed alias; user approves or blocks. --- ## Daemon changes needed - `accepted_peers` table in SQLite - `accept_peer` / `remove_peer` / `block_peer` IPC commands - After hello verification: check allowlist — send `bye` and close if not accepted; emit `pending_peer` event 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: ```json { "yaw": "yaw-key-backup-1", "id": "", "alg": "argon2id-secretbox", "ops": 2, "mem": 67108864, "salt": "", "nonce": "", "ct": "" } ``` - `yaw` is the format version tag - `id` is the public peer ID (hex) — visible without decrypting, useful for confirming you're importing the right identity - `alg` signals argon2id KDF + nacl secretbox encryption - `ops`/`mem` are argon2id parameters - `ct` unseals 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** 1. `waste-daemon export-identity --out identity.enc` (or IPC command) 2. Copy file to new machine, import in Tauri onboarding screen **Web UI → any client** 1. Web UI shows "export your identity" → downloads the encrypted file 2. User imports into TUI or Tauri with passphrase **New user via web UI, later installs Tauri** 1. Creates identity in browser (stored in secure browser storage) 2. Exports encrypted file at any point 3. 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.