--- name: unibus_admin lang: go domain: infra version: 0.1.0 description: "Panel web de administración de unibus: un binario Go que sirve una SPA Mantine embebida y expone una REST API. Tiene la identidad ADMIN del operador, firma cada petición al plano de control del bus, y gestiona rooms, miembros, claves, usuarios y el estado del cluster." tags: [service, messaging, admin, nats, e2e] uses_functions: - sign_ed25519_go_cybersecurity uses_types: [] framework: "" entry_point: "main.go" dir_path: "projects/message_bus/apps/unibus_admin" repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus_admin" service: port: 8480 health_endpoint: /healthz health_timeout_s: 3 systemd_unit: unibus-admin.service systemd_scope: system restart_policy: always runtime: systemd-system pc_targets: - magnus is_local_only: false e2e_checks: - id: build cmd: "CGO_ENABLED=0 go build ./..." timeout_s: 180 - id: vet cmd: "CGO_ENABLED=0 go vet ./..." timeout_s: 120 - id: web_build cmd: "cd web && pnpm install --frozen-lockfile && pnpm build" timeout_s: 240 - id: smoke_mock cmd: "./unibus_admin --mock --port 18490 & sleep 2 && curl -fsS http://127.0.0.1:18490/healthz && curl -fsS http://127.0.0.1:18490/api/cluster >/dev/null && kill %1" timeout_s: 30 --- # unibus_admin Panel web de administración del bus de mensajería **unibus** (NATS + JetStream con cifrado E2E por room). Un único binario Go que: 1. **Sirve la SPA Mantine compilada y embebida** (`embed.FS` sobre `web/dist`), con el mismo look índigo/oscuro que la web del cliente del bus. 2. **Expone una REST API de administración** bajo `/api`. El binario tiene la identidad ADMIN del operador y media cada acción privilegiada contra el plano de control de unibus, **firmando cada petición**. El navegador nunca firma, nunca habla NATS y nunca ve una clave privada. ## Arquitectura ``` navegador (SPA Mantine) │ fetch /api/* (basic auth de Caddy + bind loopback) ▼ unibus_admin (gateway Go, identidad ADMIN del operador) ├── pkg/client (unibus) → CreateRoom / Invite / Kick / ListMyRooms (firma + cripto E2E) ├── pkg/client (unibus) → ListUsers / AddUser / RevokeUser (API admin firmada; funciona en cluster) ├── GET firmado → /rooms/{id}/members (CanonicalRequest + SignEd25519, reusa la construcción del bus) ├── GET /healthz (CA-pinned)→ estado + posture de los 3 nodos del cluster └── membership.Store (opc.) → users (allowlist) como fallback single-node con --db ▼ cluster unibus (magnus + homer + datardos, enforce + ACL + TLS + KV) ``` El gateway reutiliza el cliente del bus (`github.com/enmanuel/unibus/pkg/client`) para todo lo que lleva criptografía (sellar la clave de room, firmar invite/rekey), y construye GETs firmados con la **única fuente de verdad** de la firma del bus (`membership.CanonicalRequest` + `cs.SignEd25519`) para las lecturas que el cliente no expone. Nunca reimplementa firma ni cripto. ## Capacidades | Pestaña | Qué hace | Vía | |---|---|---| | **Cluster** | up/down + posture (enforce/acl/tls/cluster/store) + latencia de cada nodo | `GET /healthz` (auth-exempt) de los nodos en `--nodes`, TLS pin a la CA del bus | | **Rooms** | listar (rooms del admin), crear (subject + E2E/persist/firmado), ver miembros, invitar, expulsar+rekey | `pkg/client` (mutaciones) + GET firmado (miembros) | | **Users** | listar/añadir/revocar la allowlist del bus | `pkg/client` (`ListUsers`/`AddUser`/`RevokeUser`) contra la API admin-only del plano de control, firmando como el operador. Funciona en cluster (los nodos escriben al mismo store que las rooms) sin acceso directo al store. `--db` queda como fallback single-node opcional | ## Cómo arrancar ```bash # Mock (iterar la SPA sin bus): ./unibus_admin --mock --port 8480 # Real contra un membershipd local (dev, sin TLS). Users vía la API del plano de # control; añade --db sólo si quieres gestionar users contra un SQLite local: ./unibus_admin --port 8480 \ --ctrl-url http://127.0.0.1:8470 --nats-url nats://127.0.0.1:4250 \ --identity-pass unibus/operator-identity # Producción (cluster magnus, enforce + TLS + nkey). Sin --db: la pestaña Users # gestiona la allowlist por la API admin firmada del plano de control: ./unibus_admin --port 8480 --bind 127.0.0.1 \ --ctrl-url https://127.0.0.1:8470 --nats-url tls://127.0.0.1:4250 \ --ca /opt/unibus/tls/ca.crt \ --identity-file /opt/unibus_admin/identity.json \ --nodes "magnus=https://127.0.0.1:8470,homer=https://141.94.69.66:8470,datardos=https://51.91.100.142:8470" ``` ## Build ```bash cd web && pnpm install && pnpm build # compila la SPA a web/dist (embebida) cd .. && CGO_ENABLED=0 go build -o unibus_admin . ``` ## Seguridad - La identidad admin se carga de `pass` (`unibus/operator-identity`) o de un fichero 0600 (`--identity-file`); nunca va hardcodeada, ni a git, ni a argv. - El panel exige autenticación: en producción lo fronta Caddy con basic auth sobre un subdominio ofuscado, y el gateway bindea sólo a loopback. - Acciones destructivas (revocar user, expulsar miembro + rekey) piden confirmación explícita en la UI. - El operador debe estar en la allowlist del bus (rol admin) para que el gateway pueda conectar bajo `enforce`. ## Despliegue actual Desplegado en **magnus** como `unibus-admin.service` (systemd system, `Restart=always`), puerto `8480` en loopback, fronteado por Caddy con basic auth en un subdominio ofuscado (`admin-.organic-machine.com`). Credenciales en `pass` (`unibus/admin-panel-password`, `unibus/admin-panel-url`). Artefactos de deploy en `deploy/`. ## Gaps conocidos - **Users contra el cluster desplegado**: el código del plano de control (unibus master, v0.10.0) ya expone la API admin-only de users (`GET/POST /users`, `POST /users/{signpub}/revoke`) y el gateway la consume firmando como el operador. La cadena completa (list/add/revoke + idempotencia 409) está verificada end-to-end contra un `membershipd` master local. El gap restante es de **despliegue**: los binarios `membershipd` que corren hoy en el cluster (magnus/homer/datardos) son anteriores al merge de esta ruta y devuelven `404` en `/users`, así que la pestaña Users sólo será funcional en producción cuando el bus se actualice a v0.10.0. El gateway ya conecta y firma correctamente contra esos nodos (verificado: `/api/me` responde con el endpoint real del operador y pasa el `enforce` de magnus). Para gestión single-node sin esperar al cluster, `--db` sigue disponible. - **meta-leader / tamaño de quórum** del cluster: `/healthz` no los expone; requieren el endpoint de monitoreo de NATS (varz/jsz). La pestaña Cluster muestra up/posture. - **Invite a room E2E**: requiere las claves públicas (sign_pub + kex_pub) del invitado en hex, porque la clave de room se sella contra su X25519. La UI las pide manualmente; no hay directorio de claves públicas todavía.