Document the wallet-model account flow in the Users tab: invite-link account
creation (with the configurable client base URL for the join link), permanent
hard-delete with strong confirmation, and the deploy gap (cluster still on bus
v0.11.0; this branch merges after the bus reaches master).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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 (alta por invitación + baja por hard-delete) y el estado del cluster.
Panel web de administración del bus de mensajería unibus (NATS + JetStream con
cifrado E2E por room). Un único binario Go que:
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.
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 la allowlist; crear usuario por enlace de invitación (modelo wallet, sin manejar claves); añadir por clave conocida; revocar (status flip, auditable) y eliminar (hard-delete permanente, con confirmación fuerte)
pkg/client (ListUsers/AddUser/RevokeUser/DeleteUser/CreateInvite/ListInvites) contra la API admin-only del plano de control, firmando como el operador. Funciona en cluster sin acceso directo al store. --db queda como fallback single-node opcional
Cómo arrancar
# 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
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-<hash>.organic-machine.com). Credenciales en pass
(unibus/admin-panel-password, unibus/admin-panel-url). Artefactos de deploy en
deploy/.
Creación de usuarios (modelo wallet) y enlace de invitación
La pestaña Users crea usuarios SIN que el operador maneje claves: «Crear usuario»
acuña un enlace de invitación de un solo uso (POST /api/invites → bus
POST /invites, admin-only). El gateway construye el enlace
<<APP_BASE>>/join?token=XXX, donde <<APP_BASE>> es la URL del cliente
final (la página que hospeda /join), NO la del panel. Se configura en el
gateway con --join-base-url https://chat.unibus.example o la variable
UNIBUS_JOIN_BASE_URL; el valor se expone en /api/me (join_base_url). Si no
se configura, la SPA usa su propio origen como respaldo y avisa de configurarlo.
El usuario abre el enlace en su dispositivo, genera ahí su par de claves (la
privada nunca sale del equipo) y se registra (POST /register, lo consume la
página /join del cliente web — ver el contrato en el report del bus).
«Eliminar» es un hard-delete permanente (DELETE /api/users/{pub} → bus
DELETE /users/{signpub}), distinto de «Revocar» (status flip auditable). La UI
exige teclear el handle para confirmarlo (no se dispara por un clic accidental).
Gaps conocidos
Cuentas (invites + hard-delete) contra el cluster desplegado: el gateway y la
SPA ya consumen POST/GET /api/invites y DELETE /api/users/{pub}, verificados
end-to-end contra un membershipd v0.12.0 local (crear invite → registrar por
/register sin firma → aparece en /users → hard-delete → desaparece) y vía
--mock. El gap es de despliegue: el cluster corre hoy v0.11.0 (sin
/invites//register/DELETE /users), así que la pestaña Users sólo creará/
eliminará cuentas en producción cuando el bus se actualice a v0.12.0 (rollout
pendiente). Verificado contra el cluster vivo: /register devuelve 401 en
magnus/homer/datardos (ruta aún no desplegada) frente a 400 en el nodo v0.12.0
local. El merge de esta rama del panel a master debe seguir al merge del bus.
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.
Capability growth log
v0.2.0 (2026-06-07) — capa de CUENTAS estilo WhatsApp sobre el bus v0.12.0. El
gateway gana POST/GET /api/invites (acuñar/listar invitaciones de un solo uso,
consumiendo client.CreateInvite/ListInvites) y DELETE /api/users/{pub}
(hard-delete, client.DeleteUser), con la misma doble vía que el resto de users
(plano de control firmado en cluster / store directo en single-node). El enlace de
invitación <<APP_BASE>>/join?token=… lo construye el gateway desde
--join-base-url / UNIBUS_JOIN_BASE_URL (URL del cliente final, no del panel),
expuesto en /api/me. La SPA añade a la pestaña Users: botón «Crear usuario»
(modal handle+rol+caducidad → enlace copiable), card de invitaciones pendientes
(handle, rol, token parcial, caducidad, copiar enlace), y botón «Eliminar» con
confirmación FUERTE (teclear el handle) que distingue el borrado permanente del
revoke. Verificado: flujo e2e contra membershipd v0.12.0 local (invite → register
por curl sin firma → aparece en /users → re-register 409 → hard-delete → desaparece)
y gateway vía --mock (las rutas nuevas + el SPA embebido sirven). El cluster vivo
sigue en v0.11.0 (rollout del bus pendiente del orquestador); esta rama del panel se
mergea a master tras el merge del bus a master. build/vet/web-build verdes.