feat: browser-native client — wire SPA to the SDK, delete the Go gateway
Phase 2 of issue 0001. uniweb becomes a pure frontend (web/ only), like unibus_android: the SPA talks directly to the bus and the Go gateway is gone. - busService.ts: the new data layer over the bus SDK, replacing the old api module. It holds the user's wallet identity and a connected BusClient IN THE BROWSER and opens the session locally — the private key is never sent anywhere (closes the gateway-era hole where the browser POSTed its private key to /api/session). - Wire account/App/ChatShell/ChatPanel/WalletLogin/Recover/Join to busService; subscribeRoom replaces the SSE streamRoom; ApiError -> SessionError. - SDK: ControlPlane.createRoom + listMemberRooms, and fetchRoom mapped to the real control-plane wire shape (snake_case, no id) — all verified by the live round-trip. - Delete cmd/webgw, go.mod, go.sum, src/api.ts and the orphan operator Login. uniweb now has zero Go and no dependency on unibus as a module. - vite: drop the /api proxy, dev server on 5173 to match the bus CORS allowlist; add vite-env typings. app.md: lang ts, no uses_functions, e2e_checks are now web-only. Bump 0.3.0. Onboarding by token is now admin-side (the bus has no self-register endpoint; the gateway only mocked it). tsc + pnpm build + 19/19 unit green.
This commit is contained in:
@@ -1,123 +1,128 @@
|
||||
---
|
||||
name: uniweb
|
||||
lang: go
|
||||
lang: ts
|
||||
domain: infra
|
||||
version: 0.2.0
|
||||
description: "Frontend web del bus unibus: SPA de chat (React+Mantine) con wallet por usuario (BIP39) + gateway Go (REST+SSE) que actúa de peer del bus para el navegador."
|
||||
tags: [service, messaging, web, frontend, e2e]
|
||||
uses_functions:
|
||||
- generate_identity_go_cybersecurity
|
||||
- seal_aead_go_cybersecurity
|
||||
- open_aead_go_cybersecurity
|
||||
- seal_key_box_go_cybersecurity
|
||||
- open_key_box_go_cybersecurity
|
||||
- sign_ed25519_go_cybersecurity
|
||||
- verify_ed25519_go_cybersecurity
|
||||
version: 0.3.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: "cmd/webgw"
|
||||
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"
|
||||
service:
|
||||
port: 8481
|
||||
health_endpoint: null
|
||||
health_timeout_s: 3
|
||||
systemd_unit: null
|
||||
systemd_scope: null
|
||||
restart_policy: always
|
||||
runtime: manual
|
||||
pc_targets:
|
||||
- lucas-linux
|
||||
is_local_only: false
|
||||
e2e_checks:
|
||||
- id: build
|
||||
cmd: "CGO_ENABLED=0 go build ./..."
|
||||
- id: typecheck
|
||||
cmd: "cd web && pnpm install --frozen-lockfile && pnpm exec tsc --noEmit -p tsconfig.app.json"
|
||||
timeout_s: 180
|
||||
- id: vet
|
||||
cmd: "CGO_ENABLED=0 go vet ./..."
|
||||
timeout_s: 120
|
||||
- id: unit
|
||||
cmd: "CGO_ENABLED=0 go test ./..."
|
||||
cmd: "cd web && pnpm test"
|
||||
timeout_s: 120
|
||||
- id: web_build
|
||||
cmd: "cd web && pnpm install --frozen-lockfile && pnpm build"
|
||||
cmd: "cd web && pnpm build"
|
||||
timeout_s: 180
|
||||
---
|
||||
|
||||
## Qué es
|
||||
|
||||
`uniweb` es el frontend web del bus [unibus](../unibus/app.md): la interfaz que un humano
|
||||
usa desde el navegador para hablar por el bus. Se separó de `unibus` (v0.13.0) para que el
|
||||
plano del bus (membresía, claves, librería cliente) quede limpio y el frontend tenga su
|
||||
propia carpeta de servicio y su propio ciclo de release.
|
||||
`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:
|
||||
|
||||
Tiene dos mitades que viven juntas:
|
||||
- **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.
|
||||
|
||||
- **SPA (`web/`)** — React 18 + Vite + Mantine v9. Pantallas de chat y onboarding wallet
|
||||
(join por invitación, login por passphrase local, recover por mnemónica). 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 viaja
|
||||
al servidor en claro.
|
||||
- **Gateway (`cmd/webgw`)** — binario Go (`package main`, REST + SSE) que actúa como peer
|
||||
del bus en nombre del navegador. Mantiene una sesión wallet por usuario, registra claves
|
||||
públicas por token de invitación, y traduce HTTP/SSE ↔ el protocolo del bus usando la
|
||||
librería cliente de unibus.
|
||||
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.
|
||||
|
||||
## Cómo se acopla a unibus
|
||||
## El SDK del bus (`web/src/bus/`)
|
||||
|
||||
`uniweb` consume `unibus` como **módulo Go**, no reimplementa nada del 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`):
|
||||
|
||||
```
|
||||
replace github.com/enmanuel/unibus => ../unibus # pkg/{busauth,client,frame,room}
|
||||
replace fn-registry => ../../../../ # functions/cybersecurity
|
||||
```
|
||||
- `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`.
|
||||
|
||||
Los `replace` no son transitivos en Go, así que `uniweb` (módulo principal) declara los dos:
|
||||
el de `unibus` (de donde importa la librería cliente) y el de `fn-registry` (de donde
|
||||
`pkg/client` toma las primitivas de cifrado). Compila con `CGO_ENABLED=0` igual que unibus.
|
||||
`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
|
||||
# 1. Backend: el control-plane del bus (en la carpeta de unibus)
|
||||
cd ../unibus && CGO_ENABLED=0 go run ./cmd/membershipd # :8470
|
||||
# El bus ya corre (cluster unibus con WebSocket habilitado, --ws-port). Apunta la SPA a un
|
||||
# nodo y arráncala en dev (puerto 5173, que coincide con la CORS allowlist del cluster):
|
||||
cd web && pnpm install
|
||||
VITE_BUS_HTTP=https://<nodo>:8470 VITE_BUS_WS=wss://<nodo>:8480 pnpm dev
|
||||
# Navegador: http://localhost:5173
|
||||
|
||||
# 2. Build de la SPA
|
||||
cd web && pnpm install && pnpm build # genera web/dist
|
||||
|
||||
# 3. Gateway sirviendo la SPA + API contra el control-plane
|
||||
cd .. && CGO_ENABLED=0 go run ./cmd/webgw \
|
||||
--port 8481 --ctrl-url http://127.0.0.1:8470 --web-dir web/dist
|
||||
# Navegador: http://127.0.0.1:8481
|
||||
|
||||
# Desarrollo de la SPA con hot-reload (gateway en modo API-only, sin --web-dir):
|
||||
cd web && pnpm dev # vite proxya /api + /stream al gateway
|
||||
# Producción: build estático y sirve web/dist con cualquier static server.
|
||||
cd web && pnpm build # genera web/dist
|
||||
```
|
||||
|
||||
## 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` solo es la capa web encima.
|
||||
programáticos) ve a `unibus`; `uniweb` es el cliente web encima.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El gateway necesita el control-plane de unibus vivo (`--ctrl-url`, por defecto
|
||||
`http://127.0.0.1:8470`); si no, las sesiones fallan al abrir el peer.
|
||||
- `--web-dir` es **opcional**: vacío = API-only (úsalo con el dev server de vite); apuntando a
|
||||
`web/dist` = sirve la SPA buildeada. Un path inválido degrada a API-only con un WARN, no
|
||||
peta.
|
||||
- Build cross-repo: `uniweb` no compila si `../unibus` no está presente en disco (el `replace`
|
||||
es local). Para deploy hay que llevar ambos repos, o vendorizar unibus.
|
||||
- **`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**: el dev server corre en `http://localhost:5173` para coincidir con la
|
||||
`--cors-origins` del cluster. Otro origen necesita añadirse a esa allowlist.
|
||||
- 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,
|
||||
|
||||
Reference in New Issue
Block a user