Closes the defect in scadaproj#1. The hang was never Akka: the package's DI wiring closed a circular singleton dependency the container cannot see through factory lambdas — ISecretStore (ReplicatingSecretStore decorator) -> ISecretReplicator -> SecretReplicationActorProvider -> ISecretCacheInvalidator -> DefaultSecretResolver -> ISecretStore. Resolution recurses around the loop until MS.DI's StackGuard hops it onto a fresh thread-pool thread, which then blocks forever on a singleton call-site lock the first thread still holds: a silent permanent hang instead of a stack overflow. Managed stacks from dotnet-dump show the repeating cycle and both parked threads; both candidate causes in the issue (DistributedPubSub.Get vs the Lazy lock, missing Akka.Cluster.Tools HOCON) are disproven — the actor constructor was never reached, and the deadlock reproduces on a single non-clustered node. Fix: defer the one cycle-closing edge. The provider now gets a DeferredSecretCacheInvalidator that resolves the real invalidator on first eviction — which only happens when a replicated row is applied, strictly after graph resolution. Severing the edge instead is wrong: a null-invalidator experiment ran the live gate at 5/6, with deleted secrets still resolving on the peer. The SqlServer package never had the cycle (its replicator chain never touches the invalidator), which is why the hub gate always passed. Verified: live 2-node convergence gate now 6/6 (was: infinite hang), including the delete-visibility check that proves the deferred invalidator really evicts. New HostedProcessResolutionTests builds the graph as a host does (container- registered ActorSystem, hosted services, watchdogged resolves) and fails on 0.2.1; DeferredSecretCacheInvalidatorTests pins the wrapper contract. Full suite 180 passed / 0 failed / 15 skipped (env-gated live SQL). Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
Secrets (encrypted secret store + ${secret:} resolution)
Normalizes how the family stores and consumes secrets — SQL/login passwords, API-key HMAC
peppers, LDAP bind passwords, connection strings, TLS material — which are handled ad-hoc
and inconsistently across the three apps today (Data-Protection-encrypted connection
strings in ScadaBridge; peppers/passwords in environment variables; LDAP passwords in
appsettings).
The goal is the shared ZB.MOM.WW.Secrets library: AES-256-GCM envelope encryption at
rest, a pluggable master-key provider and store, an audited ISecretResolver +
${secret:name} config expander for app runtime, and a Blazor /admin/secrets management
UI. The library is built, published (0.1.2), and live-proven via its reference consumer;
per-app adoption is the tracked follow-on.
- Target:
spec/SPEC.md - Realized API:
shared-contract/ZB.MOM.WW.Secrets.md - Implementation:
../../ZB.MOM.WW.Secrets/· designdocs/plans/2026-07-15-secrets-manager-design.md - Adoption backlog:
GAPS.md
Status
| State | |
|---|---|
| Library | Built + published — ZB.MOM.WW.Secrets{,.Abstractions,.Ui} 0.1.2 on the dohertj2-gitea feed; .Cli in-repo (not packed); .Akka replicator deferred (design only) |
| Reference consumer | HistorianGateway — adopted + live-proven (2026-07-16): historian password sourced via ${secret:}, authenticated read against the real wonder historian |
| Three sister apps | Not yet adopted — see per-app current-state + GAPS |
Per-project current state
| Project | Today (baseline) | Doc |
|---|---|---|
| OtOpcUa | (code-verified baseline) | current-state/otopcua/CURRENT-STATE.md |
| MxAccessGateway | (code-verified baseline) | current-state/mxaccessgw/CURRENT-STATE.md |
| ScadaBridge | (code-verified baseline) | current-state/scadabridge/CURRENT-STATE.md |
Not applicable as a fourth adopter row but the exemplar: HistorianGateway already consumes the lib — its wiring is the template the three apps follow (see the shared-contract "Consumer wiring" section).