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
|
name: unibus
|
||||||
lang: go
|
lang: go
|
||||||
domain: infra
|
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."
|
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]
|
tags: [service, messaging, nats, e2e]
|
||||||
uses_functions:
|
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
|
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
|
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.
|
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`,
|
- **Identidad = secreto crítico.** El archivo de identidad (`worker.id`,
|
||||||
`chat.id`) contiene las claves privadas (Ed25519 + X25519). Se escribe 0600.
|
`chat.id`) contiene las claves privadas (Ed25519 + X25519). Se escribe 0600.
|
||||||
Perderlo = mensajes ilegibles, sin recuperación. Trátalo como una clave SSH.
|
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
|
## 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
|
- 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
|
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
|
forma DESACOPLADA del debug-log. Antes el monitoring solo se abría con
|
||||||
|
|||||||
Reference in New Issue
Block a user