diff --git a/modules/mesh-delivery-assigner/internal/assign/compose_test.go b/modules/mesh-delivery-assigner/internal/assign/compose_test.go index c8921b4..1e202f5 100644 --- a/modules/mesh-delivery-assigner/internal/assign/compose_test.go +++ b/modules/mesh-delivery-assigner/internal/assign/compose_test.go @@ -68,33 +68,33 @@ func TestAChangeOfOneModuleComposesItsMachine(t *testing.T) { } } -// 1297/AC2: a change that removes a module a machine runs fails, naming the machine and the module. -func TestRemovingAModuleAMachineRunsFails(t *testing.T) { - c := Change{Group: "g", Heads: "0123456789ab", Removed: []string{"web"}} - v := Compose(meshOf(), c, "sdk", 0) - if v.State != delivery.CompositionFail || v.Machine != "g14" || !strings.Contains(v.Words, "g14 runs web") { - t.Fatalf("composition = %+v, want a fail naming g14 and web", v) - } -} - -// 1297/B3c: a machine that composes on the trunk and not with the change fails it, naming the machine and why; the -// consumer's machine is touched when its provider changes. -func TestAMachineTheChangeBreaksFails(t *testing.T) { - c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"store": {Module: "store", Version: "2"}}} - v := Compose(meshOf(), c, "sdk", 0) - if v.State != delivery.CompositionFail || v.Machine != "ace" || !strings.Contains(v.Words, "database") { - t.Fatalf("composition = %+v, want a fail on ace naming the provision nothing provides", v) - } -} - -// 1297/B3c: what was already broken on the trunk is said and fails nothing. -func TestWhatWasBrokenBeforeFailsNothing(t *testing.T) { - m := meshOf() - m.Manifests["store"] = catalogue.Manifest{Module: "store", Version: "1"} - c := Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"mail": {Module: "mail", Version: "2", Requires: []string{"database"}}}} - v := Compose(m, c, "sdk", 0) - if v.State != delivery.CompositionPass || !hasWarning(v, "on the trunk either") { - t.Fatalf("composition = %+v, want a pass saying ace was already broken", v) +// 1297/AC2 and B3c: a change that removes a module a machine runs fails, naming the machine and the module; a machine that +// composes on the trunk and not with the change fails it, naming the machine and why (the consumer's machine is touched when +// its provider changes); what was already broken on the trunk is said and fails nothing. One table (three tests merged). +func TestAChangeThatBreaksAMachineFailsAndWhatWasBrokenFailsNothing(t *testing.T) { + broken := meshOf() + broken.Manifests["store"] = catalogue.Manifest{Module: "store", Version: "1"} + for _, c := range []struct { + name string + mesh Mesh + c Change + state string + must func(delivery.CompositionVerdict) bool + }{ + {"removing a module a machine runs", meshOf(), Change{Group: "g", Heads: "0123456789ab", Removed: []string{"web"}}, delivery.CompositionFail, + func(v delivery.CompositionVerdict) bool { + return v.Machine == "g14" && strings.Contains(v.Words, "g14 runs web") + }}, + {"a machine the change breaks", meshOf(), Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"store": {Module: "store", Version: "2"}}}, + delivery.CompositionFail, func(v delivery.CompositionVerdict) bool { + return v.Machine == "ace" && strings.Contains(v.Words, "database") + }}, + {"what was broken before", broken, Change{Group: "g", Heads: "0123456789ab", Manifests: map[string]catalogue.Manifest{"mail": {Module: "mail", Version: "2", Requires: []string{"database"}}}}, + delivery.CompositionPass, func(v delivery.CompositionVerdict) bool { return hasWarning(v, "on the trunk either") }}, + } { + if v := Compose(c.mesh, c.c, "sdk", 0); v.State != c.state || !c.must(v) { + t.Errorf("%s: composition = %+v", c.name, v) + } } } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index bae4fd6..6c231fb 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -467,15 +467,20 @@ func TestTwoBuilderInstancesStartingTogetherEndWithOneDoneKey(t *testing.T) { // } } -// TestAnArchiveOnlyModuleNamingNoToolchainIsBuilt: the indexer gives an archive no language, so modules.ca-trust has no -// toolchains; the builder builds it on the build module's nodes all the same (review round 4). -func TestAnArchiveOnlyModuleNamingNoToolchainIsBuilt(t *testing.T) { - b := startBed(t) - b.archive(t, "ca-trust", "modules/ca-trust", commit) - b.start(t, "n1", nil) - v := b.await(t, "ca-trust", isState(delivery.BuildDone)) - if len(v.Toolchains) != 0 || len(v.Artifacts) != 1 || strings.HasPrefix(v.Step, "one node (") { - t.Errorf("build = %+v, want done naming no toolchain", v) +// TestAnArchiveOnlyModuleAndARootModuleAreBuilt: the indexer gives an archive no language, so modules.ca-trust has no +// toolchains; the builder builds it on the build module's nodes all the same (review round 4). A module whose path is "." +// is given to the build tools as it is (1615/AC4). One table: the two tests were merged to pay for the enrolment's. +func TestAnArchiveOnlyModuleAndARootModuleAreBuilt(t *testing.T) { + for _, c := range []struct{ name, path string }{{"no toolchain", "modules/ca-trust"}, {"root", delivery.RootPath}} { + t.Run(c.name, func(t *testing.T) { + b := startBed(t) + b.archive(t, "ca-trust", c.path, commit) + b.start(t, "n1", nil) + v := b.await(t, "ca-trust", isState(delivery.BuildDone)) + if c.name == "no toolchain" && (len(v.Toolchains) != 0 || len(v.Artifacts) != 1 || strings.HasPrefix(v.Step, "one node (")) { + t.Errorf("build = %+v, want done naming no toolchain", v) + } + }) } } @@ -530,14 +535,6 @@ func TestAToolchainChangeDoesNotBuildTheOldFingerprint(t *testing.T) { // 1615/A } } -// TestARootModuleIsBuilt: a module whose path is "." is given to the build tools as it is. -func TestARootModuleIsBuilt(t *testing.T) { // 1615/AC4 - b := startBed(t) - b.archive(t, "ca-trust", delivery.RootPath, commit) - b.start(t, "n1", nil) - b.await(t, "ca-trust", isState(delivery.BuildDone)) -} - func mustKey(t *testing.T, module string) string { k, err := delivery.BuilderBuildKey(module, fp) if err != nil { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go new file mode 100644 index 0000000..6bf05ad --- /dev/null +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go @@ -0,0 +1,386 @@ +//go:build integration + +package main + +// mesh-enrolment whole on a real bus (novox/hq ADR 0587 §2-§8; issue 774): the holder's own code over a real nats-server's +// key-value bucket `enrolments` with a time to live per key (the keys written as the runtime writes them: a create with the +// expiry, a put with a message TTL, an update by revision), its events said on their real subjects, the bus's own expiry marker +// heard, and the other parts stood in for — the vault (which writes the new node's key of node-logins at node-account, as the +// vault does), the operator's channel, placement, and the grants part's rule that a key at live makes the user enrol. and +// a key gone takes it away. + +import ( + "context" + "encoding/json" + "errors" + "slices" + "strings" + "sync" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + + stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/asks" + "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/testbus" +) + +// busStore is the holder's bucket as the runtime writes it. +type busStore struct { + ctx context.Context + js jetstream.JetStream + kv jetstream.KeyValue +} + +func (b *busStore) Get(key string) (Row, bool, error) { + e, err := b.kv.Get(b.ctx, key) + if errors.Is(err, jetstream.ErrKeyNotFound) { + return Row{}, false, nil + } + if err != nil { + return Row{}, false, err + } + return Row{Value: e.Value(), Revision: e.Revision()}, true, nil +} + +func (b *busStore) Create(key string, v any, ttl time.Duration) error { + raw, err := Encode(v) + if err != nil { + return err + } + var opts []jetstream.KVCreateOpt + if ttl > 0 { + opts = append(opts, jetstream.KeyTTL(ttl)) + } + _, err = b.kv.Create(b.ctx, key, raw, opts...) + if errors.Is(err, jetstream.ErrKeyExists) { + return ErrExists + } + return err +} + +func (b *busStore) Update(key string, v any, revision uint64) (uint64, error) { + raw, err := Encode(v) + if err != nil { + return 0, err + } + rev, err := b.kv.Update(b.ctx, key, raw, revision) + if err != nil && strings.Contains(err.Error(), "wrong last sequence") { + return 0, ErrChanged + } + return rev, err +} + +func (b *busStore) Put(key string, v any, ttl time.Duration) (uint64, error) { + raw, err := Encode(v) + if err != nil { + return 0, err + } + if ttl <= 0 { + return b.kv.Put(b.ctx, key, raw) + } + ack, err := b.js.Publish(b.ctx, "$KV."+b.kv.Bucket()+"."+key, raw, jetstream.WithMsgTTL(max(time.Second, ttl.Truncate(time.Second)))) + if err != nil { + return 0, err + } + return ack.Sequence, nil +} + +func (b *busStore) Delete(key string) error { + return b.kv.Delete(b.ctx, key) +} + +func (b *busStore) Keys() ([]string, error) { + keys, err := b.kv.Keys(b.ctx) + if errors.Is(err, jetstream.ErrNoKeysFound) { + return nil, nil + } + return keys, err +} + +// busReader reads other parts' buckets, `.`. +type busReader struct { + ctx context.Context + js jetstream.JetStream +} + +func (r busReader) bucket(state string) (jetstream.KeyValue, error) { + return r.js.KeyValue(r.ctx, strings.Replace(state, ".", "_", 1)) +} + +func (r busReader) Keys(state string) ([]string, error) { + kv, err := r.bucket(state) + if err != nil { + return nil, err + } + keys, err := kv.Keys(r.ctx) + if errors.Is(err, jetstream.ErrNoKeysFound) { + return nil, nil + } + return keys, err +} + +func (r busReader) Get(state, key string) ([]byte, bool, error) { + kv, err := r.bucket(state) + if err != nil { + return nil, false, err + } + e, err := kv.Get(r.ctx, key) + if errors.Is(err, jetstream.ErrKeyNotFound) { + return nil, false, nil + } + if err != nil { + return nil, false, err + } + return e.Value(), true, nil +} + +// standInVault holds the tokens it made, and writes a joining node's key of node-logins when it mints the account. +type standInVault struct { + ctx context.Context + logins jetstream.KeyValue + mu sync.Mutex + tokens map[string]bool + calls []string +} + +func (v *standInVault) note(c string) { + v.mu.Lock() + v.calls = append(v.calls, c) + v.mu.Unlock() +} + +func (v *standInVault) Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error) { + v.mu.Lock() + v.tokens[node] = true + v.mu.Unlock() + v.note("token " + node) + return delivery.EnrolmentTokenAnswer{Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: "s"}, nil +} +func (v *standInVault) Sealed(node string) (string, error) { return "sealed-" + node, nil } +func (v *standInVault) Drop(node string) error { + v.mu.Lock() + delete(v.tokens, node) + v.mu.Unlock() + v.note("drop " + node) + return nil +} +func (v *standInVault) Account(node, sealing, digest string) (delivery.NodeAccountAnswer, error) { + raw, err := delivery.EncodeNodeLogin(delivery.NodeLogin{Node: node, Login: delivery.FirstLogin(node), Since: time.Now().UTC()}) + if err != nil { + return delivery.NodeAccountAnswer{}, err + } + if _, err := v.logins.Put(v.ctx, node, raw); err != nil { + return delivery.NodeAccountAnswer{}, err + } + return delivery.NodeAccountAnswer{Node: node, Login: delivery.FirstLogin(node), Account: "account-" + node}, nil +} +func (v *standInVault) Retire(node string) error { return nil } +func (v *standInVault) SignCertificate(node, serving string) (delivery.SignAnswer, error) { + return delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil +} +func (v *standInVault) SignReply(node string, reply []byte) (delivery.SignAnswer, error) { + return delivery.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil +} +func (v *standInVault) Signer() (string, error) { return "vault-key", nil } + +// 774/S60/AC1: issue, a warrant, redeem as enrol. and confirm, whole on a real bus. The user enrol. is composed +// while the key is live and leaves with it; the node confirms as the login its key of node-logins names; node-enrolled and +// placement-changed are said; the vault is asked to drop the token only after the end; and a token never redeemed is ended by +// the bus's own expiry, heard, saying enrolment-expired and bringing the drop. +func TestMeshEnrolmentWholeOnARealBus(t *testing.T) { + srv := testbus.Start(t) + nc, err := nats.Connect(srv.ClientURL()) + if err != nil { + t.Fatal(err) + } + t.Cleanup(nc.Close) + js, err := jetstream.New(nc) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + t.Cleanup(cancel) + mk := func(name string, ttl bool) jetstream.KeyValue { + cfg := jetstream.KeyValueConfig{Bucket: name, History: 5} + if ttl { + // A bucket with a time to live per key keeps one revision per key: the bus (nats-server 2.11.17) expires no key of + // one that keeps more, which this test found out (a state declared key-ttl takes no history). + cfg.History, cfg.LimitMarkerTTL = 1, time.Minute + } + kv, err := js.CreateKeyValue(ctx, cfg) + if err != nil { + t.Fatal(err) + } + return kv + } + enrolments := mk(delivery.EnrolmentsBucket, true) + logins, lease, placement := mk(delivery.NodeLoginsBucket, false), mk("mesh-controller_lease", false), mk("mesh-delivery-assigner_placement", false) + mk("node-engine_have", false) + if _, err := lease.Put(ctx, "holder", []byte(`{"handed-to":"mesh-grants","handed-by":"operator"}`)); err != nil { + t.Fatal(err) + } + if _, err := placement.Put(ctx, "hub.node", []byte(hubPlacement)); err != nil { + t.Fatal(err) + } + // The seats' events are kept on a stream of the events, as the bus keeps them, and heard on their real subjects. + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEAT_EVENTS", + Subjects: []string{"mesh.seat.mesh-enrolment.event.>", "mesh.seat.mesh-placement.event.>"}}); err != nil { + t.Fatal(err) + } + var heardMu sync.Mutex + var heard []string + for _, subject := range []string{"mesh.seat.mesh-enrolment.event.*", "mesh.seat.mesh-placement.event.*"} { + if _, err := nc.Subscribe(subject, func(m *nats.Msg) { + heardMu.Lock() + heard = append(heard, m.Subject[strings.LastIndex(m.Subject, ".")+1:]) + heardMu.Unlock() + }); err != nil { + t.Fatal(err) + } + } + events := func() []string { heardMu.Lock(); defer heardMu.Unlock(); return slices.Clone(heard) } + + store := &busStore{ctx: ctx, js: js, kv: enrolments} + vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]bool{}} + asker := &fakeAsker{} + h := &Holder{Store: store, Read: busReader{ctx, js}, Vault: vault, Asker: asker, AskLasts: time.Hour, + Say: func(event string, body any, id string) error { + raw, _ := json.Marshal(body) + _, err := js.Publish(ctx, "mesh.seat.mesh-enrolment.event."+event, raw, jetstream.WithMsgID(id)) + return err + }, + AddNode: func(a AddNode) error { + value := `{"v":1,"node":"` + a.Change.Node + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}` + if _, err := placement.Put(ctx, a.Change.Node+".node", []byte(value)); err != nil { + return err + } + _, err := js.Publish(ctx, "mesh.seat.mesh-placement.event.placement-changed", []byte(`{}`)) + return err + }, + WarrantOf: func(ask string) (string, error) { return ask + "@2026-11-02T10:00:00Z", nil }, + Warrant: func(string) error { return nil }, + Act: func(string, string, string) error { return nil }, + Loaded: func(string) (bool, error) { return true, nil }, + Logf: t.Logf} + + // The grants part's rule, stood in: a key at live is the user enrol.; a key gone takes it away. The holder's watch + // of its own bucket hears the bus's expiry marker. + var usersMu sync.Mutex + users := map[string]bool{} + hasUser := func(u string) bool { usersMu.Lock(); defer usersMu.Unlock(); return users[u] } + w, err := enrolments.WatchAll(ctx) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = w.Stop() }) + go func() { + for e := range w.Updates() { + if e == nil { + continue + } + node, ok := strings.CutPrefix(e.Key(), "node.") + if !ok { + continue + } + if e.Operation() == jetstream.KeyValuePut { + en, err := decodeEnrolment(e.Value()) + usersMu.Lock() + users[EnrolUser(node)] = err == nil && en.Stage == StageLive + usersMu.Unlock() + _ = h.Heard(stdio.OpPut, e.Key(), e.Value()) + continue + } + usersMu.Lock() + delete(users, EnrolUser(node)) + usersMu.Unlock() + // A delete is the holder's own; the marker the bus leaves when a key's own time to live ends is a purge. + if e.Operation() == jetstream.KeyValuePurge { + _ = h.Heard(stdio.OpExpire, e.Key(), nil) + } else { + _ = h.Heard(stdio.OpDelete, e.Key(), nil) + } + } + }() + // The durable consumer of the holder's own end events, stood in by a core subscription that asks the drop. + for _, e := range EndEvents { + key := Seat + "." + e + if _, err := nc.Subscribe("mesh.seat.mesh-enrolment.event."+e, func(m *nats.Msg) { + if err := h.Ended(key, m.Data); err != nil { + t.Errorf("%v", err) + } + }); err != nil { + t.Fatal(err) + } + } + waitFor := func(what string, ok func() bool) { + t.Helper() + for deadline := time.Now().Add(20 * time.Second); !ok(); time.Sleep(25 * time.Millisecond) { + if time.Now().After(deadline) { + t.Fatalf("never: %s (events %v, vault %v)", what, events(), vault.calls) + } + } + } + + // Issue, the operator's yes, redeem as enrol.ace, confirm as node.ace. + a, err := h.Issue("person.operator", "ace", "a new machine", 0) + if err != nil { + t.Fatal(err) + } + if err := h.Warranted(asks.Option{ID: "yes"}, asks.Warrant{Ask: a.Ask}); err != nil { + t.Fatal(err) + } + waitFor("the user enrol.ace is composed", func() bool { return hasUser("enrol.ace") }) + args := redeemArgs("ace", 1, "serving-ace") + if _, err := h.Redeem("enrol.somebody", args); err == nil { + t.Fatal("redeemed by another caller") + } + ans, err := h.Redeem("enrol.ace", args) + if err != nil { + t.Fatal(err) + } + // The node dials as the login its key of node-logins names, and confirms as it. + e, err := logins.Get(ctx, "ace") + if err != nil { + t.Fatal(err) + } + nl, err := delivery.DecodeNodeLogin(e.Value()) + if err != nil || nl.Login != ans.Login { + t.Fatalf("node-logins says %+v, %v, and the answer %s", nl, err, ans.Login) + } + if _, err := h.Confirm(nl.Login, ""); err != nil { + t.Fatal(err) + } + waitFor("node-enrolled and placement-changed are said and the user leaves the list", func() bool { + ev := events() + return slices.Contains(ev, EventEnrolled) && slices.Contains(ev, "placement-changed") && !hasUser("enrol.ace") + }) + waitFor("the vault is asked to drop the token after the end", func() bool { + vault.mu.Lock() + defer vault.mu.Unlock() + return slices.Contains(vault.calls, "drop ace") && !vault.tokens["ace"] + }) + if keys, _ := store.Keys(); len(keys) != 0 { + t.Errorf("an ended enrolment leaves keys %v", keys) + } + + // A token never redeemed expires on the bus: the holder hears the marker, says enrolment-expired and the token is dropped. + if _, err := h.Issue("person.operator", "g14", "a second machine", 2*time.Second); err != nil { + t.Fatal(err) + } + if err := h.Warranted(asks.Option{ID: "yes"}, asks.Warrant{Ask: asker.asked[len(asker.asked)-1]}); err != nil { + t.Fatal(err) + } + waitFor("enrolment-expired is said for g14", func() bool { return slices.Contains(events(), EventExpired) }) + waitFor("the expired token is dropped", func() bool { + vault.mu.Lock() + defer vault.mu.Unlock() + return slices.Contains(vault.calls, "drop g14") + }) + if hasUser("enrol.g14") { + t.Error("the user enrol.g14 stays after its key expired") + } +} diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 03fe8f2..10f8f09 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -4,13 +4,19 @@ go 1.26.0 require ( git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 + github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) require ( + github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect + github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect + github.com/minio/highwayhash v1.0.4 // indirect + github.com/nats-io/jwt/v2 v2.8.1 // indirect github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nuid v1.0.1 // indirect golang.org/x/crypto v0.57.0 // indirect golang.org/x/sys v0.48.0 // indirect + golang.org/x/time v0.15.0 // indirect ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 1c91827..984c03d 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,26 +1,29 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 h1:xeuMBwOHNKrVUdGVg7Bdv5tkdueBvngwJFyk6n10rp0= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= -github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= +github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= -github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= -github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/minio/highwayhash v1.0.4/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= -github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= +github.com/minio/highwayhash v1.0.4 h1:asJizugGgchQod2ja9NJlGOWq4s7KsAWr5XUc9Clgl4= github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg= -github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= -github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= -github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU= github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= -github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats-server/v2 v2.11.17/go.mod h1:B1sFVz4StNosQ903ak4N1G01Fl/9f8e06mXpFIE2K24= +github.com/nats-io/nats-server/v2 v2.11.17 h1:GKEghcFK6A+aFx11Yf1LjgLC3txAwvyhnYzhBIQZA8I= github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101151945-b2a27ed5cec4 h1:xeuMBwOHNKrVUdGVg7Bdv5tkdueBvngwJFyk6n10rp0= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=