feat(membershipd): open JetStream for the embedded node + wire it into the server
The control plane previously opened a privileged JetStream client only when
clustered or running --store kv (needJS). It now also opens one for a standalone
single-node embedded deployment (openJS = needJS || embedded), because the
embedded NATS always ships JetStream and the server needs it to own persisted
rooms' durable streams (ensure on create + serve GET /rooms/{id}/history). An
external NATS without a cluster/KV feature is unchanged (no JetStream; history
degrades to empty).
The internal service identity is generated under the same broadened condition so
the in-process JetStream connection authenticates under enforce. After NewServer
the js context is wired via SetJetStream with the control-plane KV replication
factor, so a persisted room's history is as available as its metadata.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+21
-3
@@ -150,6 +150,16 @@ func main() {
|
|||||||
decentralized := *storeBackend == "kv"
|
decentralized := *storeBackend == "kv"
|
||||||
needJS := clustered || decentralized
|
needJS := clustered || decentralized
|
||||||
enforce := authMode == membership.AuthEnforce
|
enforce := authMode == membership.AuthEnforce
|
||||||
|
embedded := *natsURL == ""
|
||||||
|
// The control plane also needs a privileged JetStream client to OWN the durable
|
||||||
|
// per-room streams of persisted rooms (ensure the stream on room creation so the
|
||||||
|
// subject is captured from the first message — even from a JetStream-less browser
|
||||||
|
// client — and read it back for GET /rooms/{id}/history). The embedded NATS
|
||||||
|
// always ships JetStream, so open the client whenever we run embedded, even for a
|
||||||
|
// standalone SQLite node. For an EXTERNAL NATS we only reach for JetStream when a
|
||||||
|
// cluster/KV feature explicitly requires it (unchanged), so an operator-managed
|
||||||
|
// external deployment without those features behaves exactly as before.
|
||||||
|
openJS := needJS || embedded
|
||||||
|
|
||||||
// Internal service identity (issue 0006a): when the embedded data plane enforces
|
// Internal service identity (issue 0006a): when the embedded data plane enforces
|
||||||
// auth, membershipd must still connect to its OWN server to manage JetStream.
|
// auth, membershipd must still connect to its OWN server to manage JetStream.
|
||||||
@@ -159,7 +169,7 @@ func main() {
|
|||||||
// the server is embedded), so a standalone or non-enforce node is unchanged.
|
// the server is embedded), so a standalone or non-enforce node is unchanged.
|
||||||
var internalID cs.Identity
|
var internalID cs.Identity
|
||||||
var internalPubHex string
|
var internalPubHex string
|
||||||
if needJS && enforce && *natsURL == "" {
|
if openJS && enforce && embedded {
|
||||||
if *internalIDFile != "" {
|
if *internalIDFile != "" {
|
||||||
// Persisted identity: load it, generating + writing it (0600) on first
|
// Persisted identity: load it, generating + writing it (0600) on first
|
||||||
// start. A stable internal key is what `user add --store kv` presents to
|
// start. A stable internal key is what `user add --store kv` presents to
|
||||||
@@ -316,9 +326,9 @@ func main() {
|
|||||||
// only client that can connect in this window (the holder still denies everyone
|
// only client that can connect in this window (the holder still denies everyone
|
||||||
// else; the internal identity bypasses the store).
|
// else; the internal identity bypasses the store).
|
||||||
var js jetstream.JetStream
|
var js jetstream.JetStream
|
||||||
if needJS {
|
if openJS {
|
||||||
var internalNC *nats.Conn
|
var internalNC *nats.Conn
|
||||||
if *natsURL == "" {
|
if embedded {
|
||||||
internalNC, js, err = connectInternalJS(ns, internalID, enforce)
|
internalNC, js, err = connectInternalJS(ns, internalID, enforce)
|
||||||
} else {
|
} else {
|
||||||
internalNC, js, err = connectExternalJS(natsClientURL, *caFile)
|
internalNC, js, err = connectExternalJS(natsClientURL, *caFile)
|
||||||
@@ -340,6 +350,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
srv := membership.NewServer(store, blobs, authMode)
|
srv := membership.NewServer(store, blobs, authMode)
|
||||||
|
// Wire the privileged JetStream context so the control plane owns persisted
|
||||||
|
// rooms' durable streams (ensure on create + serve GET /rooms/{id}/history). The
|
||||||
|
// stream replication factor matches the control-plane KV replication so a room's
|
||||||
|
// history is as available as its metadata. js is nil only for an external NATS
|
||||||
|
// without a cluster/KV feature, where history degrades to empty (see openJS).
|
||||||
|
if js != nil {
|
||||||
|
srv.SetJetStream(js, *kvReplicas)
|
||||||
|
}
|
||||||
// On a public (non-loopback) bind, disable cleartext rooms: the embedded NATS
|
// On a public (non-loopback) bind, disable cleartext rooms: the embedded NATS
|
||||||
// has no per-subject ACL, so cleartext content would be readable by any
|
// has no per-subject ACL, so cleartext content would be readable by any
|
||||||
// registered peer. Forcing E2E keeps message content confidential regardless
|
// registered peer. Forcing E2E keeps message content confidential regardless
|
||||||
|
|||||||
Reference in New Issue
Block a user