Commit Graph

33 Commits

Author SHA1 Message Date
egutierrez e0ef3a27cc feat(client): sign every control-plane request (transport auth headers)
doJSON, putBlob and getBlob now go through newSignedRequest, which attaches
X-Unibus-Pub/Ts/Nonce/Sig signing membership.CanonicalRequest with the peer's
Ed25519 key. GETs are signed too so the server can authenticate the caller
uniformly under enforce. The payload-level owner signature (invite/rekey)
is unchanged and coexists with this transport-level signature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:31:50 +02:00
egutierrez 3e39e23fe0 feat(membership): signed control-plane auth middleware + anti-replay
Adds the bus-auth rollout (off|soft|enforce) to the control plane. The
middleware verifies an Ed25519 request signature over CanonicalRequest
(method, request-URI, ts, nonce, sha256(body)), checks the timestamp is
within +/-30s, rejects replayed nonces via an in-memory TTL cache (60s), and
requires the signer to be an active user in the allowlist. soft logs
rejections but lets requests through so clients can migrate without an
outage; off is the legacy no-op default. /healthz is exempt so health probes
work before any identity exists. CanonicalRequest is exported as the single
source of truth shared with the client.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:31:50 +02:00
egutierrez e9711bf74b Merge issue/0001a-users: bus user allowlist (store + CLI + migration 002)
Phase 0001a of issue 0001 (bus auth + TLS). Adds the users table, store CRUD
(AddUser/GetUser/ListUsers/RevokeUser/IsAuthorized/HasAdmin), the local
'membershipd user' admin CLI for seeding the first admin, and the bus-auth /
bus-tls feature flags (both off). No behavior change yet: the allowlist is
not consulted until phase 0001b wires the control-plane middleware.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:23:29 +02:00
egutierrez 822982b71b test(membership): cover user store golden/edge/error paths
Golden: add -> get -> IsAuthorized true, admin seeded. Edge: empty role
defaults to member, case-insensitive hex lookup, list ordering, revoke
denies authorization and stamps revoked_at. Error: duplicate key
(ErrUserExists), invalid role, empty sign_pub, unknown user not authorized,
revoke of unknown/already-revoked. Plus users-table migration idempotency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:23:23 +02:00
egutierrez ddc6cabc24 feat(flags): declare bus-auth and bus-tls feature flags (off)
bus-auth carries the off -> soft -> enforce rollout state; bus-tls is a
boolean. Both start disabled so master keeps compiling and passing tests
while the auth/TLS code lands behind them across phases 0001a-0001e.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:23:23 +02:00
egutierrez 0d7ab22d4a feat(membershipd): add 'user add/list/revoke' local admin CLI
Local administration surface for the user allowlist, dispatched before the
server flag set parses os.Args. It opens the SQLite store directly with no
network or auth: running on the bus host is trusted by design, which is how
the first admin is seeded (breaking the chicken-egg of needing an admin to
add an admin). Validates that sign-pub is a 32-byte Ed25519 key in hex and
tolerates the sign-pub positional appearing before or after --db.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:23:16 +02:00
egutierrez c5387028e0 feat(membership): add 002_users.sql migration and user CRUD store
Bus-level user allowlist (issue 0001a): the authoritative directory of
Ed25519 signing identities permitted to use the bus, independent of room
membership. Migration is additive and mirrored byte-for-byte between the
module-root migrations/ and the embedded pkg/membership/migrations/.

Store adds AddUser/GetUser/ListUsers/RevokeUser/IsAuthorized/HasAdmin.
IsAuthorized is the single fail-closed predicate both the control plane and
the NATS data plane will consult, so revocation is a status flip that denies
access on both without a restart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:23:11 +02:00
agent 7de05c8591 docs(security): fijar exposición pública en el spec 0001
Decisión del operador: el bus se expone a internet protegido por auth+TLS
(WireGuard pasa a ser una vía más, no la barrera). ufw en om abre 8470/4250;
el server cert lleva SAN con la IP pública 135.125.201.30 + la IP WG 10.42.0.1
+ hostname; los clientes controlados embeben el ca.crt propio (sin Let's Encrypt).
La fase de despliegue 0001f la ejecuta el humano; el agente entrega 0001a-0001e.
2026-06-07 12:15:32 +02:00
agent 9a915839c8 docs(security): spec issue 0001 — sistema de usuarios + auth firmada del control plane + NATS nkey + TLS
Diseño de las tres capas de seguridad del bus para que WireGuard pase a ser
opcional: tabla users (allowlist Ed25519 con roles/revocación), middleware de
firma Ed25519 + anti-replay en el control plane (generaliza signRequest/
verifyOwnerSig ya existentes), y NATS endurecido con CustomClientAuthentication
(nkey sobre la identidad del peer, revocación dinámica) + TLS con CA propia.
Incluye 6 fases TBD con feature flag bus-auth (off->soft->enforce), migración de
clientes (pkg/client centraliza el cambio), plan de despliegue a om y matriz de
tests (golden/edge/error).
2026-06-07 12:12:19 +02:00
egutierrez 0dde60a05e merge: f0 room discovery — GET /members/{endpoint}/rooms + ListMyRooms
Base para Matrix-out de agents_and_robots: un bot descubre por polling las rooms
cifradas a las que lo invitaron. Aditivo, tests verdes. Bump 0.4.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:08:07 +02:00
egutierrez 7fab473bc3 docs(app): bump to 0.4.0 — room discovery endpoint growth log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:08:07 +02:00
egutierrez 92d4e4cb97 feat(membership): room discovery — GET /members/{endpoint}/rooms + ListMyRooms
A peer invited to an encrypted room needs to find it: the control plane is
pull-based (no server push of invitations), so add a discovery endpoint that
lists every room an endpoint belongs to, with the room's metadata and the
endpoint's role.

- store.ListRoomsForEndpoint: JOIN members+rooms, ordered by room id, empty
  slice (not error) for an endpoint in no rooms.
- membershipd: GET /members/{endpoint}/rooms returns {room_id, subject, epoch,
  policy, role}[].
- client.ListMyRooms + RoomRef: a bot polls this to discover and then Join +
  Subscribe rooms it was invited to.

Tests: store-level (owner in N rooms, member in one, unknown endpoint → []) and
client-level e2e through the embedded harness (B discovers a room A invited it
to, without prior knowledge of the room id; owner sees role=owner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:07:34 +02:00
egutierrez ab4b099ab1 merge: cliente web + móvil de unibus
SPA de chat (web/, React+Vite+Mantine v9) contra el gateway, y app Android
nativa (android/, Kotlin+Compose) sobre el binding gomobile, con E2E en el
dispositivo. Amplía el binding (Card/Invite/Kick) y el gateway (rooms/members
+ CORS). Verificado end-to-end: chat cifrado en vivo entre dos pestañas web y
envío/recepción en el AVD Pixel_API34. Ver reports/0002.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:43:31 +02:00
egutierrez a11d67cf70 feat(android): app Kotlin/Compose sobre el binding gomobile
Cliente móvil nativo: embebe un peer real del bus (unibus.aar), de modo que
el cifrado E2E y el transporte NATS corren en el dispositivo.

- Conexión: Host (control plane) + NATS (data plane) + identidad; defaults
  10.0.2.2 para el emulador, configurables (sin IPs hardcodeadas).
- BusViewModel: llamadas de red del binding en Dispatchers.IO; los frames
  entrantes (FrameListener.onFrame, hilo NATS) se publican en un StateFlow
  thread-safe que Compose recolecta en el hilo principal.
- Chat: crear/unir room (toggle cifrado), enviar, recibir.
- El .aar es artefacto (gitignored); se regenera con gomobile bind (README).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:43:10 +02:00
egutierrez d33ca6278a feat(web): SPA de chat (React + Vite + Mantine v9)
Cliente web sobre el gateway (REST + SSE). El navegador no habla NATS ni
cripto: el peer Go del gateway lo hace.

- Pantalla de conexión: gateway URL + identidad (persistidas en localStorage).
- Navbar: crear room (con toggle de cifrado E2E), unirse por id, lista de rooms.
- Centro: mensajes en vivo por SSE, burbujas con autor y hora, composer.
- Lateral: miembros (rol owner), invitar por peer conectado, expulsar (owner).
- Mantine v9 (createTheme + MantineProvider), @tabler/icons-react, layout con
  AppShell/Stack/Group; sin Tailwind ni CSS manual. React 19 (peer dep de v9).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:43:10 +02:00
egutierrez 915f926136 feat(playground): endpoints rooms/members + CORS para la SPA
Madura el gateway web para servir a una SPA en otro origen:
- GET /api/rooms?peer=: rooms que conoce un peer (creadas o unidas).
- GET /api/members?room_id=: proxy al control plane (endpoint + rol).
- withCORS: middleware con preflight OPTIONS y headers permisivos para el
  dev server de Vite (mismo modelo de confianza de red que el control plane).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:42:56 +02:00
egutierrez 12fc77f25a feat(mobile): Card/Invite/Kick en el binding gomobile
Añade al binding plano sobre pkg/client:
- Card(): exporta la identidad pública del peer (id + sign_pub + kex_pub)
  como JSON portable, para intercambio peer-a-peer (paste/QR) sin gateway.
- Invite(roomID, peerCard): parsea una Card y sella la clave de room al
  invitado (delega en client.Invite).
- Kick(roomID, endpointID): expulsa y rota la clave (forward secrecy).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:42:56 +02:00
egutierrez 69079d17d5 docs(app): bump to 0.3.0 — service hardening + frame threading growth log
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:30:55 +02:00
egutierrez 22092834bd feat(frame): additive threading — ThreadID, ReplyTo + REACT type
Chat bots need replies, threads and reactions. Add two optional, omitempty
envelope fields (ThreadID, ReplyTo) plus a REACT frame type. The fields ride the
cleartext envelope (message-id references, not secret content) and are omitted
when unset, so non-threaded frames are byte-for-byte identical on the wire and
their signatures unchanged — a non-breaking, additive change.

Client gains PublishReply (threaded reply) and React (emoji reaction). The
reaction content travels in the payload, so it is sealed like any message and
stays confidential in E2E rooms; receivers dispatch on Frame.Type == REACT and
read Frame.ReplyTo for the target. Publish is refactored to share one
publishFrame path with the new helpers; its behavior is unchanged.

Tests: frame round-trip of a threaded REACT frame (golden), non-threaded
wire/sig back-compat asserting thr/re keys are absent (edge), Unmarshal of
garbage errors (error path), and an end-to-end reply+reaction round-trip in an
encrypted ModeMatrix room.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:10:44 +02:00
egutierrez b2e6712dd2 docs(app): fill service: block — systemd-user, Restart=always, LAN-reachable
membershipd now ships as a systemd user service (unit unibus-membershipd.service,
restart_policy always, runtime systemd-user). is_local_only flips to false since
--bind 0.0.0.0 makes both planes LAN-reachable. fn doctor services-spec: OK, no
drift.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:06:33 +02:00
egutierrez f6b53620e9 feat(deploy): systemd user unit + install script for membershipd
Add deploy/unibus-membershipd.service (Restart=always, binds both planes to
0.0.0.0 for LAN reachability), an idempotent deploy/install.sh that builds the
binary, symlinks the unit, and enables+starts it, plus deploy/README.md with
operate/health instructions.

Restart=always is deliberate: a clean SIGTERM exits 0 and Restart=on-failure
would not restart it, leaving the service silently dead (the sqlite_api gotcha).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:05:53 +02:00
egutierrez 01f8988cc3 feat(membershipd): --bind flag governs HTTP + embedded NATS interface
Add a --bind flag (default 127.0.0.1) to membershipd that controls which
network interface both the control-plane HTTP API and the embedded NATS data
plane listen on. Use 0.0.0.0 to expose the stack to the LAN so remote peers
(phones, other PCs) can connect; keep the default for a loopback-only dev stack.

embeddednats gains StartHost(storeDir, host, port) for explicit interface
control; Start stays a backward-compatible wrapper (host "" = nats default
0.0.0.0) so the playground and tests are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 18:05:05 +02:00
agent 0bd6eb5315 feat(mobile): gomobile-friendly wrapper over pkg/client for Android binding 2026-06-05 17:40:28 +02:00
egutierrez 7aad903236 chore: auto-commit (1 archivos)
- mobile/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 17:34:22 +02:00
egutierrez 9f05aa1a2a merge: benchmark de rendimiento en el playground de unibus (v0.2.0) 2026-06-03 22:33:26 +02:00
Egutierrez 6b162deeb0 feat(playground): benchmark de rendimiento con flags JetStream/E2E/payload
Añade GET /api/bench (SSE) y una seccion de simulador en index.html: un publisher
inunda una room con miles de mensajes a N subscribers y una grafica en vivo anima
el throughput. Las dos politicas de room se exponen como flags independientes
(persist=JetStream, encrypt=E2E AEAD+Ed25519) mas tamano de payload, midiendo el
coste de cada capa con la libreria cliente real. El benchmark usa peers efimeros
propios, sin tocar los peers nombrados del sandbox manual.

Verificado: las 4 combinaciones enc x persist con fan-out exacto. Bump app v0.2.0.
2026-06-03 22:33:26 +02:00
agent 8c680bc002 feat: optional per-room JetStream persistence (history + offline replay), gated by RoomPolicy.Persist 2026-06-03 21:48:55 +02:00
fn-registry agent bb2e412744 chore: sync from fn-registry agent 2026-06-03 21:37:25 +02:00
agent b1d1f64c16 fix: surface clear error when joining encrypted room without invitation
- membership server returns 403 + human-readable message on missing sealed key (was leaking 'sql: no rows in result set')
- client doJSON unwraps the server's {"error"} field instead of pasting the raw HTTP envelope
2026-06-03 21:33:42 +02:00
agent c203ba63e6 chore: remove dead roomFor method in playground server 2026-06-03 21:19:48 +02:00
agent 22d6106e32 feat: add web playground for interactive bus testing (SSE + vanilla UI, all-in-one server) 2026-06-03 21:18:27 +02:00
agent 888ff75236 fix: default ports 8470 (HTTP) / 4250 (NATS) to avoid clash with registry_api on 8420 2026-06-03 19:54:36 +02:00
agent cd02a52191 feat: initial scaffold of unibus message bus (membership service + client lib + demo peers) 2026-06-03 19:47:32 +02:00