diff --git a/kez-chat/document.md b/kez-chat/document.md index fe03ef5..7cb3f8d 100644 --- a/kez-chat/document.md +++ b/kez-chat/document.md @@ -21,6 +21,9 @@ A real-time chat + file-sharing application with verified identities. downloaded when a recipient actually wants them. No background sync. - Identity is portable: the underlying key + sigchain survives the home server going dark. Handles can be migrated to other servers later. +- A **Svelte web app** served directly by `kez-chat-server` is the test + UI. It uses the same HTTP API any native client would use, so the + web app dogfoods the API. See §4.4. This is the Keybase model rebuilt on a decentralized substrate: - **Identity layer** → KEZ (instead of Keybase's central account system) @@ -198,6 +201,7 @@ microservices (NATS broker, sigchain server). | **NATS auth callout** | ✅ Yes | | **WebFinger endpoint** | ✅ Yes | | **HTTP API for clients** | ✅ Yes | +| **Serves the test web app** (Svelte SPA, built into the binary) | ✅ Yes (§4.4) | | **Sigchain storage** | ❌ No — defer to `kez-sig-server` (separate container) | | **NATS broker** | ❌ No — separate `nats-server` (Go) container | | **Iroh pinning** | ❌ No for v0 — files transfer P2P when both peers are online. Pinning is a future tier. | @@ -301,14 +305,22 @@ NATS: 2. Set `NATS_URL=nats://your-broker:4222` in the chat-server's env. 3. Apply our reference `nats.conf` snippet to their NATS deployment. -The auth_callout config snippet: +The auth_callout config snippet (and WebSocket for browser clients): ```conf # nats.conf — patched into whichever NATS deployment is used + +# Enable WebSocket transport so the browser SPA can connect. +# Native CLI clients use the standard NATS port (4222). +websocket { + port: 8443 + no_tls: false # behind a TLS terminator in prod +} + authorization { auth_callout { issuer: "" - auth_users: ["AUTHUSER"] # placeholder identity NATS uses + auth_users: ["AUTHUSER"] # placeholder identity NATS uses account: "DEFAULT" } } @@ -316,12 +328,88 @@ authorization { The chat-server signs auth-callout responses with a long-lived nkey that NATS trusts. When a client connects to NATS with their KEZ -ed25519 key, NATS forwards the auth request to our chat-server, -which checks the handle registry and signs a yes/no response. +ed25519 key — whether via native protocol (CLI) or WebSocket +(browser) — NATS forwards the auth request to our chat-server, +which checks the handle registry and signs a yes/no response. Same +auth path for both transports. -### 4.4 Endpoints +### 4.4 The test web app + +The chat-server serves a Svelte single-page app as static files under +`/`. The web app is the test UI for the project — and crucially, it +**uses the exact same HTTP API a native client would use.** No +backend-rendered pages, no server-side state for the SPA. Every action +in the web UI goes through the public `/v1/...` API, which means the +web app is also a continuous test that the API contract works end to +end. ``` +Browser hits https://kez.lat/ → SPA HTML+JS+CSS +SPA calls https://kez.lat/v1/u/chris → handle lookup (same as CLI) +SPA opens wss://kez.lat/nats → NATS broker over WebSocket +SPA calls https://sig.kez.lat/v1/sigchains/... → fetch sigchain (same as CLI) +``` + +#### Stack + +| Layer | Pick | +|---|---| +| Framework | **Svelte 5 + TypeScript** | +| Build | **Vite** (output: static files served by chat-server) | +| Routing | **`svelte-spa-router`** (hash routing — works under any subpath) | +| NATS client | **`nats.ws`** — the official NATS WebSocket client. nkey auth supported. | +| Crypto | **`@noble/curves`** + **`@noble/hashes`** (same primitives our Node port uses) | +| Key storage | **passphrase-encrypted IndexedDB.** User enters a passphrase on first launch; seed is encrypted with that key. Documented limitation: browsers don't have an equivalent of OS keychain. Native clients (CLI, future Tauri) get better protection via OS keychain. | +| State | **Svelte stores** (built-in; no Redux needed) | +| Styling | **Tailwind** (default; trivially swappable) | + +#### What the web app can do (v0) + +- Account creation: generate ed25519 key in-browser, display mnemonic + for paper backup, register handle via `POST /v1/register`, upload + sigchain endpoint events to `kez-sig-server` via HTTP. +- Contacts: look up handles, fetch sigchains, display verified + identities (uses the same channel-verification logic via in-browser + TypeScript, sharing `@kez/core` and `@kez/channels`). +- 1:1 chat: subscribe to NATS inbox over WebSocket, decrypt incoming, + encrypt + publish outgoing. Real-time messaging works. +- Manifest browse: fetch and decrypt `@chris`'s shared-files manifest; + display the list of files; show metadata. +- Identity verification view: show the user's sigchain visually (claims, + channel proofs, rotations). +- Settings: show/re-display the mnemonic, re-verify it, log out + (clears IndexedDB). + +#### What the web app can't do (v0) + +- **File download / upload via Iroh.** Browsers can't open raw UDP + sockets, and Iroh's WebTransport story isn't ready in 2026. The + web app shows the manifest entries and a "Download (requires + desktop client)" button that points at the CLI. v0.5 may revisit + if Iroh-in-browser matures. +- Anything that requires the OS keychain (proper offline crypto). + +#### Deployment + +The web app's static build output (e.g. `kez-chat-server/web/dist/`) +is bundled into the chat-server's Docker image at build time and +served by axum's `ServeDir`. No separate static host, no separate +CDN. `docker compose up` deploys the SPA along with everything else. + +The build pipeline: +1. `cd web && npm install && npm run build` → produces `web/dist/` +2. Dockerfile copies `web/dist/` into the runtime image +3. Rust binary serves it as `GET /*` (with the API mounted at `/v1/...`) + +For dev: `npm run dev` runs Vite's dev server on port 5173, proxying +`/v1` requests to the locally-running chat-server. Hot module reload +works as normal Svelte dev. + +### 4.5 Endpoints + +``` +GET / the web app (Svelte SPA) +GET /assets/* SPA static assets (CSS, JS, images) GET /v1/healthz GET /v1/u/:handle handle → { primary, sigchain_url, endpoints } POST /v1/register claim a handle (signed body) @@ -331,8 +419,8 @@ GET /.well-known/webfinger?resource=acct:tudisco@kez.lat POST /internal/nats/auth verify nkey signature, return permissions ``` -Sigchain endpoints are **not** on this server — clients talk directly to -`kez-sig-server` for those. +Sigchain endpoints are **not** on this server — both web and native +clients talk directly to `kez-sig-server` for those. --- @@ -427,11 +515,19 @@ The broker sees: the unwrapped content key, verifies BLAKE3 hash. File appears. ``` -**v0 limitation:** If tudisco is offline at step 10, chris waits. -Iroh will retry; download starts when tudisco's node comes back. -Pinning (the server holding a copy) is **not** in v0 — we accept this -limitation in exchange for zero server-side storage cost and the -simplest possible architecture. +**v0 limitations:** + +1. If tudisco is offline at step 10, chris waits. Iroh will retry; + download starts when tudisco's node comes back. Pinning (the + server holding a copy) is **not** in v0 — we accept this + limitation in exchange for zero server-side storage cost and + the simplest possible architecture. +2. **The browser SPA can do steps 1–7 (sender side) and step 8 + (notification + manifest entry visible), but cannot do step 10 + (fetch the blob)** — browsers can't speak native Iroh. Web users + see "File available, open in CLI to download." Native CLI does + the whole flow. v0.5 may revisit when Iroh's WebTransport story + matures. ### 5.5 Browsing someone's files (Keybase-style) @@ -473,15 +569,27 @@ fetching is per-file deliberate. **Recipient never auto-syncs.** ├── kez-chat/ ← THIS PROJECT │ ├── document.md (this file) │ ├── Cargo.toml -│ ├── src/ +│ ├── src/ Rust server │ │ ├── main.rs binary entry │ │ ├── handles.rs handle registry (sqlite-backed) │ │ ├── nats_auth.rs NATS auth callout endpoint │ │ ├── webfinger.rs WebFinger discovery endpoint +│ │ ├── static_files.rs serves the built web app (axum::ServeDir) │ │ └── api.rs axum routes + state +│ ├── web/ Svelte web app (the test UI) +│ │ ├── package.json +│ │ ├── vite.config.ts +│ │ ├── svelte.config.ts +│ │ ├── tailwind.config.ts +│ │ ├── src/ +│ │ │ ├── routes/ pages (login, contacts, chat, profile, settings) +│ │ │ ├── lib/ crypto, nats client, sigchain helpers +│ │ │ └── app.svelte +│ │ └── dist/ build output (copied into Docker image) │ ├── deploy/ │ │ ├── docker-compose.yml chat-server + nats + sig-server -│ │ ├── nats.conf with auth_callout config +│ │ ├── nats.conf with auth_callout + websocket config +│ │ ├── Dockerfile multi-stage: build web → build rust → ship │ │ └── systemd/ alternative deployment │ └── tests/ │ └── http.rs integration tests @@ -522,6 +630,8 @@ import cleanly from the start. ### 6.3 Dependencies (planned) +**Rust server (`kez-chat-server`):** + | Crate | Why | |---|---| | `kez-core` (path) | Identity types, ed25519, signing | @@ -533,13 +643,28 @@ import cleanly from the start. | `serde` / `serde_json` | Standard | | `thiserror` / `anyhow` | Standard | | `tracing` / `tracing-subscriber` | Logging | -| `tower-http` | CORS, request tracing | +| `tower-http` | CORS, request tracing, **`fs` feature for serving the SPA static files** | | `clap` | CLI args | **Not** depended on by the chat-server: - `iroh` — server doesn't run an Iroh node in v0 (no pinning) - nats-server (Go) — separate container, not a Rust dep +**Web app (`web/`):** + +| Package | Why | +|---|---| +| `svelte` 5.x | Framework | +| `typescript` | Types | +| `vite` | Build + dev server | +| `nats.ws` | NATS client over WebSocket (browser-native NATS protocol) | +| `@noble/curves`, `@noble/hashes` | Same crypto primitives used in our Node port | +| `@scure/base` | bech32 (nsec/npub if needed), base64url | +| `canonicalize` | RFC 8785 JCS — for signature interop with native clients | +| `svelte-spa-router` | Hash-based routing | +| `tailwindcss` | Styling (default; trivially swappable) | +| `idb-keyval` | Tiny IndexedDB wrapper for the encrypted seed + cache | + ### 6.4 NATS broker — bundled in compose, not in code NATS is **not embedded in the Rust binary** — it's the official Go @@ -604,34 +729,51 @@ fallback storage) is a future addition (§8). - [ ] Registration signature validation (uses kez-core) - [ ] WebFinger endpoint - [ ] NATS auth callout (POST /internal/nats/auth) +- [ ] Static-file serving for the SPA (`tower-http` `ServeDir`) - [ ] Healthz / metrics - [ ] Integration tests against real nats-server + sig-server in a test docker-compose -### Deployment +### Web app (`web/`) -- [ ] docker-compose.yml (chat + nats + sig-server) -- [ ] nats.conf with auth_callout configured -- [ ] systemd alternative deployment recipe -- [ ] README with TLS / reverse proxy guidance +- [ ] Project scaffold (Svelte 5 + Vite + TypeScript + Tailwind) +- [ ] Account creation flow (key gen in-browser, mnemonic prompt, + registration POST, sigchain upload) +- [ ] Login flow (mnemonic in → derive key → unlock IndexedDB) +- [ ] Contacts list (handle lookup, sigchain fetch + display) +- [ ] 1:1 chat (NATS-over-WebSocket subscribe/publish, E2E encrypt/decrypt) +- [ ] Manifest browse (fetch from sigchain → list entries) +- [ ] "Download requires CLI" affordance on manifest entries +- [ ] Identity verification view (visualize the sigchain) +- [ ] Settings (re-show mnemonic, verify it, log out) +- [ ] Build script integrated into the chat-server Docker image -### Client (`kez-chat-cli` — separate project later) +### CLI client (`kez-chat-cli`) -Out of scope for the server work, but the **server isn't usable without** -at least a CLI client that does: +Same Rust core powers both the CLI and (later) a native GUI. CLI gets +the **file** capabilities the web app can't have: - [ ] Account creation (key gen + mnemonic backup + handle registration) - [ ] Contact lookup + verification -- [ ] Send / receive 1:1 chat messages (E2E via NATS) +- [ ] Send / receive 1:1 chat messages (E2E via NATS native) - [ ] Send / receive files (E2E via Iroh) -- [ ] Browse @user shared-files manifest +- [ ] Browse `` shared-files manifest + download files -UI app comes after CLI proves the flow works. +### Deployment + +- [ ] docker-compose.yml (chat-server [includes SPA] + nats + sig-server) +- [ ] nats.conf with auth_callout + websocket configured +- [ ] Multi-stage Dockerfile (build web → build rust → final image) +- [ ] systemd alternative deployment recipe +- [ ] README with TLS / reverse proxy guidance (Caddy recommended) --- ## 8. Out of scope (v0) - **Iroh pinning** (sender must be online for receiver to fetch) +- **File transfer from the browser** (the web app can browse manifests + but file download/upload needs the CLI; browsers can't speak Iroh + natively in 2026) - **Group chat** (only 1:1 for v0) - **Forward secrecy / ratcheting** (Double Ratchet, MLS) — chat is encrypted but each message uses the same X25519-derived key per pair @@ -675,6 +817,11 @@ Settle yes/no on this and the design is locked. | Handle scope: federation or global? | **Global for v0**, federation-ready design (see §3.5). | | Recovery if key lost? | **Paper backup (24-word mnemonic), Keybase-style.** No server-side recovery. | | Iroh pinning in v0? | **No.** Sender must be online for receiver to fetch. Pinning is a future tier. | +| Test UI: TUI / web / native GUI? | **Web app, served by `kez-chat-server` as static files.** Built in Svelte 5 + TypeScript + Vite + Tailwind. Uses the same HTTP API native clients use, so it dogfoods the contract. Talks NATS over WebSocket (`nats.ws`). | +| Browser file transfer? | **Not in v0.** Browsers can't speak Iroh natively in 2026. The web app shows manifests and prompts "Download requires CLI" for actual files. v0.5 revisit if Iroh's WebTransport story matures. | +| Manifest format? | **Option A** — signed JSON blob, hash committed via a new `set_shared_files` sigchain op. Simpler, reuses sigchain primitives. | +| Frontend framework? | **Svelte 5 + TypeScript + Vite**. Tailwind for styling (trivially swappable). | +| In-browser key storage? | **Passphrase-encrypted IndexedDB.** Documented limitation: browsers lack a Keychain equivalent. Native clients (CLI, future GUI) use OS keychain. | --- @@ -739,18 +886,39 @@ When we start building: No UI. Enough to prove the chat flow works end-to-end against the server. -5. **Iroh integration in the client** (not the server). - - Client runs a local Iroh node +5. **Iroh integration in the CLI** (not the server). + - CLI runs a local Iroh node - `kez-chat share @chris ./file.pdf` - `kez-chat fetch ` 6. **Shared-files manifest.** New `set_shared_files` sigchain op. `kez-chat browse @tudisco` lists his shared files. -7. **Deployment recipe.** docker-compose, systemd, deployment doc. +7. **Web app scaffold** (Svelte 5 + Vite + Tailwind). Set up + `kez-chat/web/`, wire Vite dev proxy to the running chat-server, + "hello world" SPA served by axum's `ServeDir`. Multi-stage + Dockerfile builds `web/dist/` then bakes it into the runtime image. -8. **Then** start the GUI app. Could be Tauri (Rust + web frontend), - Iced (pure Rust UI), or something else. +8. **Web app: account + contacts + identity.** Account creation in + the browser (key gen, mnemonic backup, registration, sigchain + upload). Contacts list with sigchain-based verification. Identity + view (visualize the sigchain). No chat yet. + +9. **Web app: chat.** `nats.ws` connection, nkey auth, subscribe to + inbox subject, encrypt/decrypt with `@noble/curves`. Real-time + chat in the browser. End-to-end: messages sent from CLI arrive + in the web app and vice versa. + +10. **Web app: manifest browse.** Fetch and decrypt the + `set_shared_files` manifest of any contact; display the entries; + show "Download requires CLI" affordances on each. + +11. **Deployment recipe finalized.** Production-ready docker-compose + (chat-server with embedded SPA + nats + sig-server), Caddy config + for TLS, systemd alternative. + +12. **Then** native GUI (Tauri, etc.) — if web app + CLI isn't + enough. Likely v1 stretch, not MVP. --- @@ -763,19 +931,23 @@ the handle) backed by an ed25519 primary key. The same key authenticates to a NA (chat, presence, file tickets — broker is dumb, clients do E2E with ChaCha20-Poly1305 over X25519-derived keys) and identifies an Iroh node (P2P bulk transfer, content-addressed blobs, on-demand fetch). -**Our project ships two Rust services** (`kez-chat-server` for handle -registry + NATS auth callout + HTTP API, and the existing -`kez-sig-server` for sigchain storage) **plus a docker-compose recipe -that includes `nats-server`** for turn-key deployment. NATS isn't in -our Rust code — it's the official Go binary running as its own -container — but it's wired up in our compose so operators can -`docker compose up` and have everything working. Operators with -existing NATS deployments can disable the bundled service and point -us elsewhere. The chat-server does not run an Iroh node -and does not pin files in v0; file transfer is pure P2P between -online peers. Account recovery is via a 24-word paper-backup -mnemonic. Federation across home servers is deferred but the design -keeps it as a flip-the-switch future change. +**Our project ships two Rust services + a Svelte web app + a CLI:** +`kez-chat-server` (handle registry + NATS auth callout + HTTP API + +serves the SPA), the existing `kez-sig-server` (sigchain storage), +the `web/` Svelte app (the test UI, served as static files by the +chat-server, uses the same HTTP API any native client would — +dogfoods the contract), and `kez-chat-cli` (Rust binary that's +also the scripted-test surface). NATS isn't in our Rust code — it's +the official Go binary running as its own container — but it's +wired up in our docker-compose so operators can `docker compose up` +and have everything working. Operators with existing NATS deployments +can disable the bundled service. The chat-server does not run an +Iroh node and does not pin files in v0; file transfer is pure P2P +between online peers, and **the browser can't speak Iroh natively — +so the web app shows manifests but file download requires the CLI**. +Account recovery is via a 24-word paper-backup mnemonic. Federation +across home servers is deferred but the design keeps it as a +flip-the-switch future change. ---