- dev/ - registry.db Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.8 KiB
issue, title, status, created, domain, scope
| issue | title | status | created | domain | scope |
|---|---|---|---|---|---|
| 0001 | uniweb como cliente browser-nativo del bus (eliminar el gateway Go, desacoplar de unibus) | spec | 2026-06-13 | architecture | 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/sessionenvía al gateway la identidad COMPLETA, incluida la clave privada (sign_priv64 bytes +kex_priv32 bytes, en hex). El gateway construye uncs.Identitycon la privada y abre un cliente del bus que actúa como el usuario server-side (vercmd/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):
- WebSocket en el nats-server embebido (
pkg/embeddednats): configuraropts.Websocket(puerto dedicado,NoTLSen loopback para dev; TLS en prod). Permite quenats.wsconecte desde el navegador. Flag/env para el puerto WS. - CORS en el control plane (
cmd/membershipd/pkg/membership): añadir cabecerasAccess-Control-Allow-Origin/Methods/Headersy manejar preflightOPTIONSen los endpoints que el browser llamará directo (/register,/sessiono equivalente, listado de rooms, invite, etc.). Configurable (allowlist de orígenes), no*en prod. - 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 depkg/frame(~100 LOC): estructura del frame, ULID ids, timestamp, threading (ThreadID,ReplyTo), tiposMSG/REACT.room.ts— port depkg/room(~42 LOC):Policy, tipos de clave de room, epochs.busauth.ts— port depkg/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 depkg/client(~1368 LOC, el grueso): conexiónnats.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.uniwebqueda como app de solo frontend. - Servir la SPA: static server trivial (sin lógica de bus). Documentar en
app.md. - Actualizar
app.md:langdeja de sergopuro (o se marcaframework: react, sinentry_pointGo), quitaruses_functionsGo, ajustarservice/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.tsy 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
uniwebcompila y se sirve sinunibusen disco y singo.mod(es soloweb/).pnpm buildverde;unibusbuild/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.
uniwebnecesitaunibusen disco o como módulo Go para compilar/servir.- Mensajes browser↔Go ilegibles por divergencia de protocolo.
- Queda algún
cmd/webgw/go.modenuniweb.
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.