docs(unibus): bump 0.12.0 — accounts via single-use invites + hard-delete
Document the wallet-model account layer: invite-link account creation and real hard-delete, both gotcha and capability growth log entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
name: unibus
|
||||
lang: go
|
||||
domain: infra
|
||||
version: 0.11.0
|
||||
version: 0.12.0
|
||||
description: "Bus de mensajería unificado sobre NATS+JetStream con cifrado E2E por room (megolm/olm reducido): service de membresía/claves, librería cliente y peers demo."
|
||||
tags: [service, messaging, nats, e2e]
|
||||
uses_functions:
|
||||
@@ -137,6 +137,26 @@ Para apuntar a un NATS externo en producción: `--nats-url nats://host:4222` en
|
||||
firme el primer `POST /users`); a partir de ahí toda la gestión es HTTP admin-only. El
|
||||
alta es idempotente igual que la CLI: re-alta de una clave ya registrada = 409, sin
|
||||
sobrescribir ni elevar rol; el revoke es un flip de status (sin hard-delete), auditable.
|
||||
- **Cuentas estilo WhatsApp: alta por invitación, baja por hard-delete.** Sobre la API
|
||||
admin anterior, `unibus` añade el modelo wallet de cuentas. El admin NO genera claves:
|
||||
`POST /invites` (admin-only) acuña un enlace de invitación de un solo uso con caducidad
|
||||
(token de 32 bytes `crypto/rand` en hex; TTL default 7 días), fijando `handle` y `role`.
|
||||
El nuevo usuario abre el enlace en SU cliente, que genera el par de claves localmente
|
||||
(la privada nunca sale del dispositivo) y llama `POST /register` con `{token, sign_pub,
|
||||
kex_pub}`. `/register` es la ÚNICA ruta que añade al allowlist sin firma admin —
|
||||
autorizada por el TOKEN, porque la identidad nueva aún no está en el allowlist y no puede
|
||||
firmar. Está endurecida: token fuerte de un solo uso (consumo atómico, doble uso → 409),
|
||||
caducidad (→ 410), `handle`/`role` fijados por el invite (sin escalado), validación
|
||||
estricta de ambas claves hex de 64 chars, y rate-limit por IP heredado del control plane
|
||||
(solo `/healthz` está exento). El borrado de cuenta es `DELETE /users/{signpub}`
|
||||
(admin-only): hard-delete real del allowlist, distinto del `revoke` (que se mantiene:
|
||||
revoke = quitar acceso dejando rastro auditable; delete = purga). Tras hard-delete, las
|
||||
membresías de rooms del ex-usuario quedan inertes (ya no puede autenticarse en ningún
|
||||
plano); NO se limpian a medias — un owner expulsa/rekey su room si quiere forward secrecy.
|
||||
Invites y users viven en el MISMO store (SQLite `invites`/`users`, KV `UNIBUS_invites`/
|
||||
`UNIBUS_users`). `pkg/client` gana `CreateInvite/ListInvites/CancelInvite/Register/
|
||||
DeleteUser`; solo `Register` va sin firmar. Recovery: hard-delete del último admin se
|
||||
recupera con la CLI local `membershipd user add` (mismo seam que siembra el admin #0).
|
||||
- **Identidad = secreto crítico.** El archivo de identidad (`worker.id`,
|
||||
`chat.id`) contiene las claves privadas (Ed25519 + X25519). Se escribe 0600.
|
||||
Perderlo = mensajes ilegibles, sin recuperación. Trátalo como una clave SSH.
|
||||
@@ -169,6 +189,35 @@ agent.<nombre>.{in,out} inbox/outbox de agente LLM (agent.scout.in)
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v0.12.0 (2026-06-07) — capa de CUENTAS estilo WhatsApp sobre el modelo wallet: alta de
|
||||
usuario por enlace de invitación de un solo uso + baja por hard-delete real. El admin
|
||||
nunca ve la clave privada del usuario. (1) **Invites**: nuevo backend de datos en ambos
|
||||
stores (SQLite `invites` vía migración aditiva `003_invites.sql`; KV `UNIBUS_invites`).
|
||||
Tipo `Invite{Token, Handle, Role, ExpiresAt, Used, CreatedAt}` + campos de auditoría del
|
||||
consumo (`UsedAt/UsedSignPub/UsedKexPub`). Métodos `Store.CreateInvite` (token 32 bytes
|
||||
`crypto/rand` hex, TTL default 7d), `GetInvite`, `ListInvites`, `ConsumeInvite` (valida
|
||||
existe/no-usado/no-caducado → registra el sign_pub con el handle/role del invite → marca
|
||||
usado, atómico) y `CancelInvite`. Consumo single-use garantizado en ambos backends: tx
|
||||
SQLite (mark guard `used=0` + insert) y CAS sobre la revisión KV (mark-first); burn-on-
|
||||
claim idéntico si la clave ya existe. (2) **Hard-delete**: `Store.DeleteUser` (SQLite
|
||||
`DELETE FROM users`, KV `users.Delete`) purga el allowlist — distinto del `revoke`
|
||||
(status flip, conservado). Las membresías de rooms del ex-usuario quedan inertes
|
||||
(documentado, sin limpieza parcial). (3) **Endpoints HTTP**: `POST /invites`, `GET
|
||||
/invites` (solo pendientes), `DELETE /invites/{token}`, `DELETE /users/{signpub}`
|
||||
(todos admin-only vía `requireAdmin`) y `POST /register` — la única ruta auth-exempt de
|
||||
firma admin (autorizada por el token), rate-limited (se separa `isRateExempt`, solo
|
||||
`/healthz`, de `isAuthExempt`) y con validación hex estricta de `sign_pub`+`kex_pub`
|
||||
ANTES de gastar el token. Errores mapeados: token desconocido 404, usado 409, caducado
|
||||
410, identidad ya registrada 409. (4) **pkg/client**: `CreateInvite/ListInvites/
|
||||
CancelInvite/Register/DeleteUser`; `Register` va sin firma vía un helper `doUnsigned`.
|
||||
(5) Fix de consistencia: el `GetUser` de SQLite ahora mapea `sql.ErrNoRows` → `ErrNotFound`
|
||||
como el KV y como documenta `store.go`. Tests nuevos: suite de invites store-level en
|
||||
AMBOS backends (golden + single-use + token desconocido + caducado + cancel + hard-delete
|
||||
+ burn-on-claim), suite HTTP (crear invite → register sin auth → aparece en allowlist →
|
||||
re-register 409 → caducado 410 → no-admin 403 en las 4 rutas admin → hard-delete purga),
|
||||
y test de cliente end-to-end (admin acuña invite → joiner no-registrado redime sin firma →
|
||||
aparece → hard-delete desaparece). Cambios 100% aditivos: el comportamiento previo no
|
||||
cambia; build/vet/test verdes (`CGO_ENABLED=0`).
|
||||
- v0.11.0 (2026-06-07) — flag dedicado `UNIBUS_NATS_MONITOR` que abre el endpoint
|
||||
de monitoring HTTP del nats-server embebido (`127.0.0.1:8222`, loopback only) de
|
||||
forma DESACOPLADA del debug-log. Antes el monitoring solo se abría con
|
||||
|
||||
Reference in New Issue
Block a user