package main import ( "fmt" "sync" "github.com/enmanuel/unibus/pkg/membership" ) // storeHolder is a concurrency-safe slot for the control-plane store, used to // break the decentralized bootstrap cycle (issue 0006c): the NATS authenticator // must be built BEFORE the embedded server starts, but the JetStream KV store can // only be opened AFTER NATS is up (it needs a JetStream client). The authenticator // therefore consults the holder instead of a concrete store. // // Fail-closed by construction: until the store is set, IsAuthorized denies and // SubjectACL errors, so any client connecting in the startup window is rejected. // The only connection expected in that window is membershipd's own internal // service identity, which the authenticator recognizes by key and lets through // without consulting the store at all. In the SQLite (default) path the store is // set before StartServer, so the window does not exist and behavior is identical // to the pre-0006c baseline. type storeHolder struct { mu sync.RWMutex s membership.Store } func (h *storeHolder) set(s membership.Store) { h.mu.Lock() h.s = s h.mu.Unlock() } func (h *storeHolder) get() membership.Store { h.mu.RLock() defer h.mu.RUnlock() return h.s } // IsAuthorized reports whether signPubHex is an active bus user, denying while the // store is not yet set (fail closed). It is the predicate the nkey authenticator // uses for every connecting client. func (h *storeHolder) IsAuthorized(signPubHex string) bool { s := h.get() if s == nil { return false } return s.IsAuthorized(signPubHex) } // subjectACL derives the per-subject permissions for signPubHex via the live // store, erroring (so the caller fails closed and denies the connection) while the // store is not yet set. func (h *storeHolder) subjectACL(signPubHex string) ([]string, error) { s := h.get() if s == nil { return nil, fmt.Errorf("control-plane store not ready") } return membership.SubjectACLFor(s)(signPubHex) }