6b3ace1d39
Branch-by-abstraction for the control-plane store (issue 0003b), so the membership state can move off process-local SQLite onto replicated JetStream KV without rewriting callers and without breaking master. pkg/membership: - Store is now an interface (rooms/members/keys + user allowlist + Close). The existing SQLite implementation is renamed sqliteStore and stays the default: Open(path) still returns it. openSQLite keeps the concrete type for internal callers (the 0003c migration). - ErrNotFound is a storage-agnostic "no such record" sentinel; both backends return it (the SQLite store maps sql.ErrNoRows to it). The control plane now branches on ErrNotFound instead of sql.ErrNoRows, so server.go no longer imports database/sql. - jetstreamStore (new) implements Store over five replicated KV buckets: rooms, members, rooms_by_member (reverse index for ListRoomsForEndpoint), room_keys, users. Replication factor is configurable (R1..R5) for the R1->R3 rollout. Every read is bounded by OpTimeout and IsAuthorized / HasAdmin FAIL CLOSED on any backend error (a KV quorum loss denies, never admits), per the audit's requirement for the decentralized store. dev/feature_flags.json: - Add the `decentralized` flag (OFF): sqliteStore default while off, jetstreamStore behind it. The membershipd boot wiring that selects the KV store is deliberately deferred to 0003e/0003f (the embedded-NATS authenticator<->store bootstrap is part of the session/deploy redesign); OFF keeps the single-node SQLite control plane unchanged. Tests (DoD: golden + edges + error path): - TestJetStreamStoreRoomsCRUD: encrypted room + owner + invited member round-trip through every room/member/key method, including latest-epoch resolution and rekey. - TestJetStreamStoreUsers: add/get/authorize/list/revoke + admin gate, with case-insensitive key normalization and duplicate rejection. - TestJetStreamStoreNotFound: ErrNotFound mapping for misses. - TestJetStreamStoreIsAuthorizedFailClosed: NATS backend shut down -> IsAuthorized and HasAdmin both DENY within the bounded timeout. The full existing suite stays green: sqliteStore is unchanged behavior.