feat(client): opt-in nkey NATS connection via NewWithOptions

nats.go refuses to connect with an nkey to a server that does not advertise
nkey auth, so the connection cannot blindly always present one. New keeps the
legacy plain connection; NewWithOptions(Options{UseNkey:true}) presents the
peer's identity-derived nkey. NewWithOptions is the single place the data-plane
connection is built, so every peer gets identical behavior from the same
Options (TLS fields arrive in phase 0001d).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 12:37:59 +02:00
parent b09bafe242
commit 1630f6f163
+31 -3
View File
@@ -28,6 +28,7 @@ import (
cs "fn-registry/functions/cybersecurity"
"github.com/enmanuel/unibus/pkg/busauth"
"github.com/enmanuel/unibus/pkg/frame"
"github.com/enmanuel/unibus/pkg/membership"
"github.com/enmanuel/unibus/pkg/room"
@@ -56,10 +57,37 @@ type Client struct {
signCache map[string][]byte // sender endpoint -> sign pub (for verification)
}
// New connects to NATS and records the control-plane URL. The identity holds
// the peer's long-term keypairs.
// Options configures how a client connects to the bus. The zero value is the
// legacy behavior: a plain NATS connection with no nkey and no TLS — what dev
// stacks and a not-yet-secured server expect. Secured deployments set these.
type Options struct {
// UseNkey authenticates the NATS connection with the peer's Ed25519 identity
// reused as a NATS nkey. It MUST match the server: nats.go refuses to connect
// with an nkey to a server that does not advertise nkey auth ("nkeys not
// supported by the server"), so this is opt-in rather than always-on.
UseNkey bool
}
// New connects to NATS and records the control-plane URL with default Options
// (no nkey, no TLS). The identity holds the peer's long-term keypairs.
func New(natsURL, ctrlURL string, id cs.Identity) (*Client, error) {
nc, err := nats.Connect(natsURL, nats.Name("unibus-client"))
return NewWithOptions(natsURL, ctrlURL, id, Options{})
}
// NewWithOptions is New with explicit connection options (nkey auth, and, from
// phase 0001d, TLS). It is the single place the data-plane connection is built,
// so every peer (worker, chat, mobile, gateway) gets identical behavior by
// passing the same Options.
func NewWithOptions(natsURL, ctrlURL string, id cs.Identity, opts Options) (*Client, error) {
natsOpts := []nats.Option{nats.Name("unibus-client")}
if opts.UseNkey {
nkeyPub, nkeySign, err := busauth.ClientNkey(id.SignPriv)
if err != nil {
return nil, fmt.Errorf("client: derive nkey: %w", err)
}
natsOpts = append(natsOpts, nats.Nkey(nkeyPub, nkeySign))
}
nc, err := nats.Connect(natsURL, natsOpts...)
if err != nil {
return nil, fmt.Errorf("client: connect nats %q: %w", natsURL, err)
}