// 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 ( "crypto/tls" "fmt" "time" server "github.com/nats-io/nats-server/v2/server" ) // ServerConfig is the full set of knobs for the embedded NATS server. The zero // value (empty StoreDir aside) yields a dev-friendly server: JetStream on, bound // to all interfaces, no client auth, no TLS. Secured deployments set Auth and // TLS; tests set Host to loopback and a free Port. type ServerConfig struct { StoreDir string // JetStream store directory Host string // bind interface; "" = nats-server default ("0.0.0.0") Port int // listen port // Auth, when non-nil, is installed as CustomClientAuthentication so the data // plane only accepts approved clients (nkey signature + bus allowlist). Auth server.Authentication // TLS, when non-nil, makes the server present a certificate and require TLS // on the data plane. Clients must trust the issuing CA (see busauth). TLS *tls.Config } // Start is a thin backward-compatible wrapper: embedded JetStream server on the // default interface, no auth, no TLS. func Start(storeDir string, port int) (*server.Server, error) { return StartServer(ServerConfig{StoreDir: storeDir, Port: 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 StartServer(ServerConfig{StoreDir: storeDir, Host: host, Port: port}) } // StartHostAuth is StartHost with an optional custom client authenticator. When // auth is non-nil only clients the authenticator approves may connect; when nil // the server accepts any client (legacy, network-trusted behavior). func StartHostAuth(storeDir, host string, port int, auth server.Authentication) (*server.Server, error) { return StartServer(ServerConfig{StoreDir: storeDir, Host: host, Port: port, Auth: auth}) } // StartServer launches an embedded nats-server with JetStream from cfg. It // blocks until the server is ready to accept connections (up to 5s) and returns // the running server; the caller must Shutdown it. func StartServer(cfg ServerConfig) (*server.Server, error) { opts := &server.Options{ JetStream: true, StoreDir: cfg.StoreDir, Host: cfg.Host, Port: cfg.Port, DontListen: false, // Keep the embedded server quiet by default; the host app logs the URLs. NoLog: true, NoSigs: true, } if cfg.Auth != nil { opts.CustomClientAuthentication = cfg.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 } if cfg.TLS != nil { opts.TLSConfig = cfg.TLS opts.TLS = 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() }