Files
unibus/pkg/embeddednats/embeddednats.go
T
egutierrez b09bafe242 feat(embeddednats): nkey CustomClientAuthentication against the allowlist
busauth.NewNkeyAuthenticator verifies a client's nkey signature over the
server nonce (decoding like nats-server: raw-url then std base64), maps the
nkey to its Ed25519 hex, and consults an injected IsAuthorized predicate.
Checking on every connection (rather than a static Options.Nkeys map) means
revoking a user denies its next connection with no restart. embeddednats
gains StartHostAuth(auth) and sets AlwaysEnableNonce so the server advertises
the nonce nkey clients need; Start/StartHost stay open (auth=nil) for dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 12:37:46 +02:00

81 lines
3.1 KiB
Go

// Package embeddednats starts an in-process NATS server with JetStream enabled.
//
// This lets the whole unibus stack run with `go run` without installing or
// managing a separate NATS deployment. In production, point clients at an
// external NATS via the --nats-url flag instead of using this.
package embeddednats
import (
"fmt"
"time"
server "github.com/nats-io/nats-server/v2/server"
)
// Start launches an embedded nats-server with JetStream enabled, listening on
// the given port and persisting JetStream state under storeDir. The listen host
// is left at the nats-server default ("0.0.0.0", all interfaces). It blocks
// until the server is ready to accept connections (up to 5s) and returns the
// running server. The caller is responsible for calling Shutdown on it.
//
// Start is a thin backward-compatible wrapper over StartHost; callers that need
// to control the bind interface (loopback vs LAN) should use StartHost directly.
func Start(storeDir string, port int) (*server.Server, error) {
return StartHost(storeDir, "", port)
}
// StartHost is Start with explicit control over the bind interface. host selects
// which network interface the data plane listens on: pass "127.0.0.1" to keep
// NATS loopback-only (the safe default for a single-host dev stack) or "0.0.0.0"
// to expose it to the LAN so remote peers (phones, other PCs) can connect. An
// empty host falls back to the nats-server default ("0.0.0.0", all interfaces).
func StartHost(storeDir, host string, port int) (*server.Server, error) {
return StartHostAuth(storeDir, host, port, nil)
}
// StartHostAuth is StartHost with an optional custom client authenticator. When
// auth is non-nil it is installed as Options.CustomClientAuthentication, so the
// data plane only accepts clients the authenticator approves (nkey signature +
// bus allowlist). When auth is nil the server accepts any client (the legacy,
// network-trusted behavior) — used by dev stacks and tests that have not enabled
// bus auth.
func StartHostAuth(storeDir, host string, port int, auth server.Authentication) (*server.Server, error) {
opts := &server.Options{
JetStream: true,
StoreDir: storeDir,
Host: host,
Port: port,
DontListen: false,
// Keep the embedded server quiet by default; the host app logs the URLs.
NoLog: true,
NoSigs: true,
}
if auth != nil {
opts.CustomClientAuthentication = auth
// A CustomClientAuthentication alone does not make the server advertise a
// nonce in its INFO line, and nats.go refuses to connect with an nkey to a
// server that does not ("nkeys not supported by the server"). Forcing the
// nonce makes nkey clients sign the challenge our authenticator verifies.
opts.AlwaysEnableNonce = true
}
ns, err := server.NewServer(opts)
if err != nil {
return nil, fmt.Errorf("embeddednats: new server: %w", err)
}
go ns.Start()
if !ns.ReadyForConnections(5 * time.Second) {
ns.Shutdown()
return nil, fmt.Errorf("embeddednats: server not ready for connections within 5s")
}
return ns, nil
}
// ClientURL returns a NATS connection URL for the running embedded server.
func ClientURL(ns *server.Server) string {
return ns.ClientURL()
}