From bf0884527e3223e3f6ab93ae69f7e4c788d2ddad Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 14 Jun 2026 11:27:04 +0200 Subject: [PATCH] chore: auto-commit (2 archivos) - dev/ - registry.db Co-Authored-By: Claude Opus 4.7 (1M context) --- dev/issues/0001-browser-native-client.md | 171 +++++++++++++++++++++++ registry.db | 0 2 files changed, 171 insertions(+) create mode 100644 dev/issues/0001-browser-native-client.md create mode 100644 registry.db diff --git a/dev/issues/0001-browser-native-client.md b/dev/issues/0001-browser-native-client.md new file mode 100644 index 0000000..d320a42 --- /dev/null +++ b/dev/issues/0001-browser-native-client.md @@ -0,0 +1,171 @@ +--- +issue: 0001 +title: uniweb como cliente browser-nativo del bus (eliminar el gateway Go, desacoplar de unibus) +status: spec +created: 2026-06-13 +domain: architecture +scope: uniweb (web/src/bus, web/src, eliminar cmd/webgw + go.mod) + unibus (pkg/embeddednats WebSocket, cmd/membershipd CORS, export de vectores de test cripto) +--- + +# Objetivo + +Convertir `uniweb` en un cliente del bus **browser-nativo y autónomo**, al mismo nivel que +`unibus_android`: la SPA habla directamente con el data plane (NATS sobre WebSocket) y el +control plane (HTTP), e implementa el protocolo del bus y su cifrado E2E **en el propio +navegador** (TypeScript + `@noble`). Como consecuencia se **elimina el gateway Go** +(`cmd/webgw`) y `uniweb` deja de depender del módulo Go de `unibus`: queda como una app de solo +frontend (`web/`), sin `go.mod`, sin `replace => ../unibus`. + +# Por qué (estado actual auditado el 13/06/2026) + +`uniweb` v0.1.0 nació partiendo `unibus` v0.13.0 en dos: la SPA (`web/`) y un gateway Go +(`cmd/webgw`). El gateway importa `pkg/{busauth,client,frame,room}` de unibus y +`functions/cybersecurity` del registry, así que `uniweb` solo compila con `unibus` presente en +disco (`replace github.com/enmanuel/unibus => ../unibus`). Esa es la deuda que este issue salda. + +Hay además un **defecto de seguridad** que este rediseño corrige de paso. Hoy el flujo wallet +funciona así: + +- La SPA deriva la identidad del usuario (Ed25519 + X25519) de una mnemónica BIP39 y la cifra + at-rest en el dispositivo. +- Pero al abrir sesión, `POST /api/session` **envía al gateway la identidad COMPLETA, + incluida la clave privada** (`sign_priv` 64 bytes + `kex_priv` 32 bytes, en hex). El gateway + construye un `cs.Identity` con la privada y abre un cliente del bus que **actúa como el + usuario server-side** (ver `cmd/webgw/session.go`, `cmd/webgw/identity.go`). + +Es decir: la clave privada del usuario **viaja al servidor y vive en la RAM del gateway**. El +cifrado de contenido sigue siendo E2E respecto al broker NATS, pero NO respecto al gateway — +el gateway puede leer y firmar todo en nombre del usuario. El report 0019 prometía que "la +clave privada nunca viaja al servidor"; eso solo será cierto cuando la cripto del bus ocurra +en el navegador. Este issue lo hace cierto. + +`unibus_android` ya demuestra que el patrón es viable: es un cliente Kotlin que habla directo +con NATS (:4250) y el control plane (:8470) y hace toda la cripto E2E en el dispositivo, sin +gateway y sin compartir su clave privada. + +# Diseño objetivo + +``` + ┌─────────────────────────────────────────────┐ + │ unibus (el bus) │ + │ membershipd :8470 control-plane HTTP │ + │ + CORS (nuevo) /register /session... │ + │ nats-server (embebido) │ + │ :4250 TCP (peers Go, android) │ + │ :4250+ WS (navegador, nats.ws) (nuevo)│ + └─────────────────────────────────────────────┘ + ▲ TCP ▲ WebSocket + │ │ + ┌────────┴───────┐ ┌─────────┴──────────────┐ + │ peers Go / │ │ uniweb (SPA, browser) │ + │ android (Kt) │ │ web/src/bus/ (TS SDK)│ + │ cripto nativa │ │ cripto en @noble │ + └────────────────┘ │ priv NUNCA sale │ + └────────────────────────┘ +``` + +`uniweb` final = solo `web/`. Sin `cmd/webgw`, sin `go.mod`. Se sirve como estáticos (cualquier +static server; en dev, vite). La identidad del usuario se desbloquea y se usa **solo** en el +navegador. + +# Fases + +## Fase 0 — Preparar unibus (habilitar clientes browser) + +Cambios en `unibus` (aditivos, no rompen peers Go/android existentes): + +1. **WebSocket en el nats-server embebido** (`pkg/embeddednats`): configurar `opts.Websocket` + (puerto dedicado, `NoTLS` en loopback para dev; TLS en prod). Permite que `nats.ws` conecte + desde el navegador. Flag/env para el puerto WS. +2. **CORS en el control plane** (`cmd/membershipd` / `pkg/membership`): añadir cabeceras + `Access-Control-Allow-Origin/Methods/Headers` y manejar preflight `OPTIONS` en los endpoints + que el browser llamará directo (`/register`, `/session` o equivalente, listado de rooms, + invite, etc.). Configurable (allowlist de orígenes), no `*` en prod. +3. **Exportar vectores de test cripto**: un comando/test que vuelca vectores deterministas + (derivación de identidad desde seed, envelope de frame seal/open, reparto sealed-box de la + room key, firma por-mensaje) a JSON, para que los tests TS validen **paridad byte a byte** + con la implementación Go de referencia. + +## Fase 1 — SDK del bus en TypeScript (`web/src/bus/`) + +Port de la lógica que hoy vive en el gateway (`pkg/client` y dependencias). La cripto base ya +está disponible (`@noble/curves`, `@noble/hashes`, `@noble/ciphers`); lo que se porta es el +**protocolo**, no las primitivas. + +- `frame.ts` — port de `pkg/frame` (~100 LOC): estructura del frame, ULID ids, timestamp, + threading (`ThreadID`, `ReplyTo`), tipos `MSG`/`REACT`. +- `room.ts` — port de `pkg/room` (~42 LOC): `Policy`, tipos de clave de room, epochs. +- `busauth.ts` — port de `pkg/busauth` (~331 LOC): autenticación nkey contra el control plane + (nonce + firma Ed25519). Verificar cómo el browser obtiene/firma el nonce con la identidad + del wallet (decisión: ¿la identidad del bus deriva del mismo seed BIP39?). +- `client.ts` — port de `pkg/client` (~1368 LOC, el grueso): conexión `nats.ws`, `Join`, + `Publish`/`PublishReply`/`React`, `Subscribe`, listado de rooms/users, `CreateRoom`, + `Invite`, `Kick`; envelope E2E (ChaCha20-Poly1305 seal/open con la room key); reparto de la + room key por sealed-box (X25519) a los invitados; rotación de epoch en LEAVE/KICK. + +## Fase 2 — Cablear la SPA al SDK y eliminar el gateway + +- Reemplazar `web/src/api.ts` (hoy llama `/api/*` del gateway) por llamadas al SDK del bus + + al control plane directo. +- La identidad del wallet (`web/src/wallet/`) se usa **localmente** para firmar/abrir; la + privada nunca se serializa hacia la red. +- Borrar `cmd/webgw/`, `go.mod`, `go.sum`. `uniweb` queda como app de solo frontend. +- Servir la SPA: static server trivial (sin lógica de bus). Documentar en `app.md`. +- Actualizar `app.md`: `lang` deja de ser `go` puro (o se marca `framework: react`, sin + `entry_point` Go), quitar `uses_functions` Go, ajustar `service`/`e2e_checks`. + +## Fase 3 — Validación (DoD) + +- Tests TS de paridad cripto contra los vectores de la Fase 0. +- E2E visual: join (invitación) / login (passphrase) / recover (mnemónica) + enviar y recibir + mensajes cifrados **browser ↔ browser** y **browser ↔ peer Go**. +- Auditoría de red (DevTools / proxy): confirmar que **ninguna petición transporta la clave + privada**. + +# Riesgos / decisiones abiertas + +- **nkey auth del navegador**: el bus autentica peers con nkey (Ed25519). Decidir si la + identidad nkey del bus deriva del mismo seed BIP39 del wallet o es separada. Afecta a + `busauth.ts` y a cómo el control plane reconoce al usuario. +- **WebSocket + TLS en prod**: el navegador exigirá `wss://` salvo loopback. Encaja con el + issue de TLS del bus (unibus 0001-bus-auth-and-tls). +- **CORS vs same-origin**: alternativa a CORS = servir la SPA detrás de un reverse proxy que + comparta origen con el control plane. Decidir en Fase 0/2. +- **Tamaño del bundle**: la cripto + nats.ws añaden peso; medir y, si hace falta, code-split. +- **Paridad de protocolo**: cualquier divergencia en el envelope o el reparto de claves rompe + la interoperabilidad con peers Go/android. Los vectores de la Fase 0 son el contrato. + +# Definition of Done (3 capas) + +## Capa 1 — Mecánica +- `uniweb` compila y se sirve **sin** `unibus` en disco y **sin** `go.mod` (es solo `web/`). +- `pnpm build` verde; `unibus` build/vet/test verdes tras los cambios aditivos de Fase 0. + +## Capa 2 — Cobertura de comportamiento +| Escenario | Tipo | Evidencia | Esperado | +|---|---|---|---| +| Golden: browser ↔ peer Go intercambian mensaje cifrado | e2e | dos clientes en una room, mensaje round-trip | plaintext idéntico en ambos extremos | +| Edge: recover en dispositivo nuevo re-deriva la misma identidad | e2e | pegar mnemónica → `sign_pub` reconstruido | igual al original (vector Go) | +| Edge: rotación de epoch en KICK | e2e | KICK a un miembro → publicar | el kicked ya no descifra; los demás sí | +| Error: passphrase incorrecta | unit | unlock con clave mala | `WrongPasswordError`, sin tocar la red | +| Paridad: envelope seal/open TS vs Go | unit | vectores de Fase 0 | bytes idénticos | + +## Capa 3 — Vida útil +| Métrica | Umbral | Dónde | Ventana | +|---|---|---|---| +| Mensajes browser↔Go sin pérdida | 100% | uso real | 7 días | +| Peticiones que filtran la priv | 0 | audit de red | continuo | +| Errores de descifrado | 0 | consola/log SPA | 7 días | + +## Anti-criterios (invalidan la DoD) +- La clave privada aparece en CUALQUIER petición de red. +- `uniweb` necesita `unibus` en disco o como módulo Go para compilar/servir. +- Mensajes browser↔Go ilegibles por divergencia de protocolo. +- Queda algún `cmd/webgw`/`go.mod` en `uniweb`. + +# Notas + +Onboarding: tras este issue, `uniweb` se desarrolla y despliega como cualquier SPA estática. +No necesita el binario del bus para nada salvo apuntar `nats.ws` al puerto WebSocket del bus y +el control plane HTTP a `membershipd`. El bus (`unibus`) y los demás clientes (`unibus_android`) +no cambian de contrato: este issue solo añade el transporte WebSocket y CORS, ambos aditivos. diff --git a/registry.db b/registry.db new file mode 100644 index 0000000..e69de29