--- name: uniweb lang: ts domain: infra version: 0.4.0 description: "Cliente web browser-nativo del bus unibus: SPA de chat (React+Mantine) con wallet por usuario (BIP39) que habla DIRECTO al bus (nats.ws + control-plane HTTPS firmado), sin gateway. La clave privada nunca sale del navegador." tags: [messaging, web, frontend, e2e] uses_functions: [] uses_types: [] framework: "react" entry_point: "web/src/main.tsx" dir_path: "projects/message_bus/apps/uniweb" repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/uniweb" icon: phosphor: "chats-circle" accent: "#6366f1" e2e_checks: - id: typecheck cmd: "cd web && pnpm install --frozen-lockfile && pnpm exec tsc --noEmit -p tsconfig.app.json" timeout_s: 180 - id: unit cmd: "cd web && pnpm test" timeout_s: 120 - id: web_build cmd: "cd web && pnpm build" timeout_s: 180 --- ## Qué es `uniweb` es el **cliente web browser-nativo** del bus [unibus](../unibus/app.md): la interfaz que un humano usa desde el navegador para hablar por el bus. Es **solo frontend** (`web/`) — una SPA, sin backend Go, sin gateway. Habla **directamente** con el bus, igual que `unibus_android` lo hace en Kotlin: - **Control plane** — HTTPS firmado al `membershipd` (rooms, claves, miembros). Cada request lleva la firma Ed25519 del usuario (cabeceras `X-Unibus-*`). - **Data plane** — NATS sobre WebSocket (`nats.ws`), autenticado con el nkey derivado de la identidad del usuario. Stack: React 18 + Vite + Mantine v9. La identidad criptográfica de cada usuario se deriva de forma determinista de una frase BIP39 de 12 palabras y se cifra at-rest en el dispositivo (AES-256-GCM). **La clave privada nunca sale del navegador**: firma, sella y descifra en el cliente. No hay servidor al que enviarla. ## El SDK del bus (`web/src/bus/`) El protocolo y el cifrado E2E del bus están **portados a TypeScript**, validados byte a byte contra la implementación Go de referencia (vectores de `unibus cmd/busvectors`): - `crypto.ts` — Ed25519, ChaCha20-Poly1305, sealed box (nonce BLAKE2b, igual que Go). - `frame.ts` — wire format = `encoding/json` de Go byte a byte. - `room.ts` — Policy (ModeNATS / ModeMatrix). - `busauth.ts` — nkey NATS (base32 + crc16) + firma de requests del control-plane. - `client.ts` — envelope de room + `BusClient` + `ControlPlane` HTTP firmado. - `wstransport.ts` — transporte `nats.ws`. `busService.ts` es la capa de datos de la SPA sobre el SDK (reemplazó al viejo módulo `api` que hablaba con el gateway). Ya **no depende de `unibus` como módulo Go**: el desacople es total. ## Ejemplo ```bash # Producción: SPA same-origin detrás de Caddy, que sirve la SPA + /api + /nats. # Build estático y despliegue de web/dist: cd web && pnpm install pnpm build # genera web/dist (se despliega a magnus:/opt/uniweb/dist) # Dev (`pnpm dev`): el dev server NO tiene el proxy de Caddy, así que /api y /nats no # existen en localhost. Apunta la SPA a un nodo real del cluster con las env vars # (overrides del default same-origin). El dev server corre en el puerto 5174: VITE_BUS_HTTP=https://:8470 VITE_BUS_WS=wss://:8480 pnpm dev # Navegador: http://localhost:5174 # (Añade http://localhost:5174 a la --cors-origins del nodo, o el control-plane # rechazará la petición por CORS.) ``` ## Cuándo usarla Cuando quieras que un humano hable por el bus desde un navegador, o cuando trabajes en la UI de chat / el onboarding wallet. Para la lógica del bus en sí (membresía, claves, peers programáticos) ve a `unibus`; `uniweb` es el cliente web encima. ## Gotchas - **`wss://` con CA self-signed**: el cluster sirve el WebSocket con el cert del bus (CA propia). Un navegador rechaza `wss://` self-signed salvo que se importe la CA o se ponga un reverse proxy con cert válido (Let's Encrypt). En dev se puede aceptar el cert a mano. - **Onboarding admin-side**: el bus no tiene endpoint de auto-registro (el viejo gateway lo *mockeaba*). En `enforce`, una identidad nueva debe ser autorizada por un admin (`membershipd user add`) antes de poder abrir sesión; el flujo de Join muestra la clave pública del usuario para que un admin la autorice. - **CORS / same-origin**: en producción la SPA es same-origin detrás de Caddy (`/api` y `/nats` proxyados), así que no hay CORS. En dev (`pnpm dev`, puerto 5174) esos paths relativos no existen: hay que apuntar a un nodo con `VITE_BUS_HTTP`/`VITE_BUS_WS` y añadir `http://localhost:5174` a la `--cors-origins` del nodo. El puerto 5173 está reservado a otra app local; si 5174 está ocupado, Vite usa el siguiente libre. - La passphrase del wallet nunca se guarda ni se envía; perderla en un dispositivo sin la mnemónica BIP39 = identidad irrecuperable en ese dispositivo (recuperable en otro con las 12 palabras). ## Capability growth log - v0.3.0 (2026-06-14) — `uniweb` se vuelve **cliente browser-nativo puro** (issue 0001, Fase 2): la SPA se cablea al SDK del bus (`busService.ts` reemplaza el módulo `api`) y se **elimina el gateway Go** (`cmd/webgw`, `go.mod`, `go.sum`). `uniweb` queda como solo `web/`, sin nada de Go, sin dependencia de `unibus` como módulo. La clave privada se usa solo en el navegador (`saveAndOpen`/`unlockAndOpen` abren la sesión localmente; ya NO se hace `POST /api/session` con la privada — se cierra el agujero E2E del modelo gateway). Validado end-to-end contra el cluster descentralizado real (Fase 3): identidad registrada conecta por `nats.ws` y hace round-trip de un mensaje cifrado (crear room → publicar → recibir descifrado + firma verificada). El onboarding por token queda admin-side (el bus no tiene auto-registro). `tsc` + `pnpm build` + 19/19 unit verdes. - v0.2.0 (2026-06-13) — SDK del bus en TypeScript (`web/src/bus/`), issue 0001 Fase 1: el protocolo y el cifrado E2E del bus portados al navegador para que `uniweb` deje de depender del gateway Go. Módulos: `crypto.ts` (Ed25519, ChaCha20-Poly1305, sealed box con nonce BLAKE2b igual que Go), `frame.ts` (wire format = `encoding/json` de Go byte a byte), `room.ts` (Policy), `busauth.ts` (nkey NATS + firma de requests del control-plane), `client.ts` (envelope de room puro + `BusClient` sobre una interfaz de transporte + cliente HTTP firmado) y `wstransport.ts` (adaptador `nats.ws`). Paridad cross-language verificada contra vectores Go (`cmd/busvectors`): **19/19 tests verdes** — endpoint id, firma Ed25519, AEAD, sealed box, frame marshal/sign, nkey y canonical request. La clave privada del usuario nunca se serializa hacia la red. La conexión `nats.ws` + control-plane reales se validan en la Fase 3 (E2E) por requerir un unibus vivo con WebSocket. - v0.1.0 (2026-06-13) — scaffold inicial: extracción de la SPA (`web/`) y el gateway (`cmd/webgw`) desde `unibus` v0.13.0 a su propia app/sub-repo. Sin cambios de capacidad respecto a lo que ya vivía en unibus 0.12.0 (wallet BIP39 + sesiones por usuario); solo cambia la ubicación y el módulo Go. go build/vet/test + pnpm build verdes en la nueva ubicación con los `replace` cross-repo. - v0.2.0 (2026-06-13) — SDK del bus en TypeScript (`web/src/bus/`), issue 0001 Fase 1: el protocolo y el cifrado E2E del bus portados al navegador para que `uniweb` deje de depender del gateway Go. Módulos: `crypto.ts` (Ed25519, ChaCha20-Poly1305, sealed box con nonce BLAKE2b igual que Go), `frame.ts` (wire format = `encoding/json` de Go byte a byte), `room.ts` (Policy), `busauth.ts` (nkey NATS + firma de requests del control-plane), `client.ts` (envelope de room puro + `BusClient` sobre una interfaz de transporte + cliente HTTP firmado) y `wstransport.ts` (adaptador `nats.ws`). Paridad cross-language verificada contra vectores Go (`cmd/busvectors`): **19/19 tests verdes** — endpoint id, firma Ed25519, AEAD, sealed box, frame marshal/sign, nkey y canonical request. La clave privada del usuario nunca se serializa hacia la red. La conexión `nats.ws` + control-plane reales se validan en la Fase 3 (E2E) por requerir un unibus vivo con WebSocket. - v0.1.0 (2026-06-13) — scaffold inicial: extracción de la SPA (`web/`) y el gateway (`cmd/webgw`) desde `unibus` v0.13.0 a su propia app/sub-repo. Sin cambios de capacidad respecto a lo que ya vivía en unibus 0.12.0 (wallet BIP39 + sesiones por usuario); solo cambia la ubicación y el módulo Go. go build/vet/test + pnpm build verdes en la nueva ubicación con los `replace` cross-repo.