bf0884527e
- dev/ - registry.db Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
9.8 KiB
Markdown
172 lines
9.8 KiB
Markdown
---
|
|
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.
|