feat(0003d): replicated blob store on NATS Object Store

Branch-by-abstraction for the blob store (issue 0003d): media ciphertext
can live in a replicated JetStream Object Store instead of local disk, so
a blob uploaded to one node survives a node loss and is reachable from
any node.

pkg/blobstore:
- Store is now an interface (Put/Get/Has). The filesystem backend is
  renamed diskStore and stays the default: New(dir) returns it.
- objectStore (new) implements Store over a NATS Object Store bucket with
  a configurable replication factor (R1..R5), matching the KV store's
  R1->R3 rollout. Content-addressing (sha256-hex) is identical, so the
  wire contract is unchanged.

pkg/membership:
- Server.blobs and NewServer take the blobstore.Store interface instead
  of the concrete type; no behavior change with the disk default.

Tests (DoD: golden + edge + contract):
- TestObjectStoreRoundTrip: put/get/has + content-addressed dedup.
- TestObjectStoreMissing: unknown hash is absent and unreadable.
- TestObjectStoreAddressMatchesDisk: the Object Store and disk backends
  address identical bytes to the IDENTICAL hash (portable blob refs).

Like the KV store (0003b), wiring membershipd to select the Object Store
is deferred to the decentralized boot path (flag off); disk stays default.
This commit is contained in:
agent
2026-06-07 15:12:45 +02:00
parent 94e7ced1ef
commit d6e668b984
4 changed files with 263 additions and 12 deletions
+2 -2
View File
@@ -56,7 +56,7 @@ const (
// (mTLS, capabilities, rate limits) is a later phase.
type Server struct {
store Store
blobs *blobstore.Store
blobs blobstore.Store
mux *http.ServeMux
authMode AuthMode
nonces *nonceCache
@@ -78,7 +78,7 @@ type Server struct {
// tests that have not migrated to signed requests yet). It installs a per-IP
// rate limiter with the package defaults; loopback dev behavior is unchanged
// because the burst comfortably exceeds any single client's request rate.
func NewServer(store Store, blobs *blobstore.Store, authMode AuthMode) *Server {
func NewServer(store Store, blobs blobstore.Store, authMode AuthMode) *Server {
s := &Server{
store: store,
blobs: blobs,