feat: initial scaffold of unibus message bus (membership service + client lib + demo peers)

This commit is contained in:
agent
2026-06-03 19:47:32 +02:00
commit cd02a52191
22 changed files with 2888 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
package main
import cs "fn-registry/functions/cybersecurity"
// newEphemeralIdentity generates an in-memory identity for the encrypted demo
// (not persisted to disk — the demo creates fresh peers each run).
func newEphemeralIdentity() (cs.Identity, error) {
return cs.GenerateIdentity()
}
+184
View File
@@ -0,0 +1,184 @@
// Command chat is a demo peer with two modes.
//
// Simple mode (default): joins a room subject and prints every received message
// live. It validates uniformity worker<->chat: a process publishes, a human UI
// subscribes, both speak the same protocol.
//
// Encrypted demo (--demo-encrypted): a self-contained script against a running
// membershipd that proves E2E encryption and forward secrecy. A creates an
// encrypted room, invites B, A publishes a message B can decrypt, then A kicks
// B and publishes again at the new epoch — B can no longer decrypt. Prints a
// PASS/FAIL summary of each assertion.
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/enmanuel/unibus/pkg/client"
"github.com/enmanuel/unibus/pkg/frame"
"github.com/enmanuel/unibus/pkg/room"
)
func main() {
var (
natsURL = flag.String("nats-url", "nats://127.0.0.1:4222", "NATS url")
ctrlURL = flag.String("ctrl-url", "http://127.0.0.1:8420", "membershipd control-plane url")
roomSub = flag.String("room", "proc.test.ticks", "room subject to subscribe to")
idFile = flag.String("id-file", "./local_files/chat.id", "identity file path")
demoEnc = flag.Bool("demo-encrypted", false, "run the encrypted forward-secrecy demo")
)
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
log.SetPrefix("[chat] ")
if *demoEnc {
runEncryptedDemo(*natsURL, *ctrlURL)
return
}
runSimple(*natsURL, *ctrlURL, *roomSub, *idFile)
}
// runSimple subscribes to a cleartext subject and prints messages live.
func runSimple(natsURL, ctrlURL, roomSub, idFile string) {
id, err := client.LoadOrCreateIdentity(idFile)
if err != nil {
log.Fatalf("identity: %v", err)
}
c, err := client.New(natsURL, ctrlURL, id)
if err != nil {
log.Fatalf("connect: %v", err)
}
defer c.Close()
log.Printf("endpoint: %s", c.Endpoint().ID)
// A subscriber needs a room to resolve the subject + policy. For the
// cleartext demo each peer owns a room mapped to the shared subject; NATS
// fans out by subject so worker publishes reach this subscription.
roomID, err := c.CreateRoom(roomSub, room.ModeNATS)
if err != nil {
log.Fatalf("create room: %v", err)
}
if err := c.Join(roomID); err != nil {
log.Fatalf("join: %v", err)
}
sub, err := c.Subscribe(roomID, func(f frame.Frame, plaintext []byte) {
fmt.Printf("[%s] %s: %s\n", f.Subject, shortID(f.Sender), string(plaintext))
})
if err != nil {
log.Fatalf("subscribe: %v", err)
}
defer sub.Unsubscribe()
log.Printf("subscribed to %q; waiting for messages (Ctrl-C to stop)", roomSub)
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
log.Printf("bye")
}
func shortID(id string) string {
if len(id) > 8 {
return id[:8]
}
return id
}
// runEncryptedDemo proves E2E encryption + forward secrecy end-to-end.
func runEncryptedDemo(natsURL, ctrlURL string) {
log.Printf("=== encrypted forward-secrecy demo ===")
pass := true
check := func(name string, ok bool) {
status := "PASS"
if !ok {
status = "FAIL"
pass = false
}
fmt.Printf(" [%s] %s\n", status, name)
}
// Two identities: A (owner) and B (invitee). In-memory only (demo).
idA, err := newEphemeralIdentity()
must(err, "generate A identity")
idB, err := newEphemeralIdentity()
must(err, "generate B identity")
a, err := client.New(natsURL, ctrlURL, idA)
must(err, "connect A")
defer a.Close()
b, err := client.New(natsURL, ctrlURL, idB)
must(err, "connect B")
defer b.Close()
// A creates an encrypted room.
roomID, err := a.CreateRoom("room.test", room.ModeMatrix)
must(err, "A create room")
fmt.Printf(" room.test -> %s (E2E, persisted, signed)\n", roomID)
// A invites B (seals K to B's X25519 key).
must(a.Invite(roomID, b.Endpoint()), "A invite B")
// B joins (fetches + decrypts K).
must(b.Join(roomID), "B join")
// B subscribes; capture received plaintexts.
recv := make(chan string, 4)
subB, err := b.Subscribe(roomID, func(f frame.Frame, plaintext []byte) {
recv <- string(plaintext)
})
must(err, "B subscribe")
defer subB.Unsubscribe()
time.Sleep(200 * time.Millisecond) // let the subscription settle
// A publishes a message B can decrypt.
const msg1 = "hola E2E"
must(a.Publish(roomID, []byte(msg1)), "A publish msg1")
got1, ok := waitMsg(recv, 2*time.Second)
check("B decrypts pre-kick message", ok && got1 == msg1)
// A kicks B (rotates K to a new epoch, re-sealed only for the remaining members).
must(a.Kick(roomID, b.Endpoint().ID), "A kick B")
time.Sleep(200 * time.Millisecond)
// A publishes at the new epoch. B must NOT be able to decrypt: it was removed
// from the member list, so /key returns no key for B at the new epoch.
const msg2 = "secreto post-kick"
must(a.Publish(roomID, []byte(msg2)), "A publish msg2 (post-kick)")
got2, ok2 := waitMsg(recv, 1500*time.Millisecond)
// Forward secrecy holds if B did NOT receive the post-kick plaintext.
check("B cannot decrypt post-kick message (forward secrecy)", !(ok2 && got2 == msg2))
if ok2 {
fmt.Printf(" (unexpected: B received %q after kick)\n", got2)
} else {
fmt.Printf(" (B received nothing decryptable after kick — correct)\n")
}
fmt.Println()
if pass {
fmt.Println("RESULT: PASS — E2E encryption and forward secrecy verified")
os.Exit(0)
}
fmt.Println("RESULT: FAIL — see assertions above")
os.Exit(1)
}
func waitMsg(ch <-chan string, timeout time.Duration) (string, bool) {
select {
case m := <-ch:
return m, true
case <-time.After(timeout):
return "", false
}
}
func must(err error, what string) {
if err != nil {
log.Fatalf("%s: %v", what, err)
}
}
+93
View File
@@ -0,0 +1,93 @@
// Command membershipd is the unibus control-plane service: room metadata,
// member directory, sealed key distribution, and the media blob store. The data
// plane is NATS — if --nats-url is empty it starts an embedded nats-server with
// JetStream so the whole stack runs with `go run` and nothing to install.
package main
import (
"context"
"flag"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
server "github.com/nats-io/nats-server/v2/server"
"github.com/enmanuel/unibus/pkg/blobstore"
"github.com/enmanuel/unibus/pkg/embeddednats"
"github.com/enmanuel/unibus/pkg/membership"
)
func main() {
var (
natsURL = flag.String("nats-url", "", "external NATS url; empty starts an embedded server")
httpPort = flag.String("http-port", "8420", "HTTP port for the control-plane API")
dbPath = flag.String("db", "./local_files/unibus.db", "SQLite database path")
storeDir = flag.String("store-dir", "./local_files/blobs", "blob store directory")
natsPort = flag.Int("nats-port", 4222, "embedded NATS listen port (when --nats-url empty)")
natsStore = flag.String("nats-store", "./local_files/jetstream", "embedded JetStream store dir")
)
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
log.SetPrefix("[membershipd] ")
// Data plane: embedded or external NATS.
var ns *server.Server
natsClientURL := *natsURL
if natsClientURL == "" {
var err error
ns, err = embeddednats.Start(*natsStore, *natsPort)
if err != nil {
log.Fatalf("start embedded nats: %v", err)
}
natsClientURL = embeddednats.ClientURL(ns)
log.Printf("embedded NATS (JetStream) ready: %s", natsClientURL)
} else {
log.Printf("using external NATS: %s", natsClientURL)
}
// Control plane: SQLite store + blob store + HTTP API.
store, err := membership.Open(*dbPath)
if err != nil {
log.Fatalf("open membership store: %v", err)
}
defer store.Close()
log.Printf("membership store: %s", *dbPath)
blobs, err := blobstore.New(*storeDir)
if err != nil {
log.Fatalf("open blob store: %v", err)
}
log.Printf("blob store: %s", *storeDir)
srv := membership.NewServer(store, blobs)
addr := "127.0.0.1:" + *httpPort
httpSrv := &http.Server{Addr: addr, Handler: srv}
go func() {
log.Printf("HTTP control-plane API: http://%s", addr)
log.Printf(" health: http://%s/healthz", addr)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("http server: %v", err)
}
}()
// Graceful shutdown on SIGINT/SIGTERM.
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
log.Printf("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = httpSrv.Shutdown(ctx)
if ns != nil {
ns.Shutdown()
ns.WaitForShutdown()
}
log.Printf("bye")
}
+71
View File
@@ -0,0 +1,71 @@
// Command worker is a demo peer: it creates (or joins) a cleartext room and
// publishes an incrementing counter once per second, to both stdout and the
// bus. It demonstrates that a process is a first-class bus peer, uniform with
// the human chat client.
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/enmanuel/unibus/pkg/client"
"github.com/enmanuel/unibus/pkg/room"
)
func main() {
var (
natsURL = flag.String("nats-url", "nats://127.0.0.1:4222", "NATS url")
ctrlURL = flag.String("ctrl-url", "http://127.0.0.1:8420", "membershipd control-plane url")
roomSub = flag.String("room", "proc.test.ticks", "room subject to publish to")
idFile = flag.String("id-file", "./local_files/worker.id", "identity file path")
)
flag.Parse()
log.SetFlags(log.LstdFlags | log.Lmsgprefix)
log.SetPrefix("[worker] ")
id, err := client.LoadOrCreateIdentity(*idFile)
if err != nil {
log.Fatalf("identity: %v", err)
}
c, err := client.New(*natsURL, *ctrlURL, id)
if err != nil {
log.Fatalf("connect: %v", err)
}
defer c.Close()
log.Printf("endpoint: %s", c.Endpoint().ID)
// Create the room; if it already exists we cannot recreate it under a known
// id (rooms get fresh ULIDs), so for the demo each worker run owns its room.
roomID, err := c.CreateRoom(*roomSub, room.ModeNATS)
if err != nil {
log.Fatalf("create room: %v", err)
}
log.Printf("room %q -> %s (subject %s, cleartext)", *roomSub, roomID, *roomSub)
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
n := 0
for {
select {
case <-ticker.C:
n++
payload := fmt.Sprintf("tick %d @ %s", n, time.Now().UTC().Format(time.RFC3339))
fmt.Println(payload)
if err := c.Publish(roomID, []byte(payload)); err != nil {
log.Printf("publish: %v", err)
}
case <-stop:
log.Printf("stopping after %d ticks", n)
return
}
}
}