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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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.
- 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