The replication actor was created only when something resolved ISecretStore
(store -> replicator -> SecretReplicationActorProvider.ActorRef). A node that
expanded its ${secret:} references pre-host and never touched the store at
runtime had no actor: no topic subscription, no announces, no pull replies.
AddZbSecretsAkkaReplication now registers SecretReplicationActorStarter, which
creates the actor on IHostApplicationLifetime.ApplicationStarted (after every
hosted service, incl. an Akka.Hosting AkkaHostedService registered later), or
in StartAsync when no lifetime is registered.
- tests: EagerActorStartTests (generic host in both registration orders with a
no-actor-before-start control; bare container) - all fail without the fix
- docs: README, CHANGELOG
- code-reviews: Secrets-010 Resolved; README regenerated
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
8.1 KiB
Code Reviews
Comprehensive, per-library code reviews of the ZB.MOM.WW.* shared libraries hosted
in this repo. Each library (one self-contained .slnx at the repo root) has its own
folder containing a findings.md. This README is the aggregated index — the single
place to see all outstanding work.
Generated by
regen-readme.pyfrom the per-libraryfindings.mdfiles. Do not edit by hand — edit the findings files and re-run the script.
Status lives in Gitea. Open items from this file are tracked as issues on gitea.dohertylan.com/dohertj2/scadaproj (label
migrated, 2026-11-06). This file is a historical/design record — do not track status here; file new work (including new review findings) as an issue.
How it works
- Reviews are performed one library at a time against a fixed checklist.
- Each library is reviewed against its normalized component spec under
components/. - Every finding is recorded in the library's
findings.mdwith a severity and status. - Findings are never deleted — they are closed by changing their status, keeping a full audit trail.
- This README aggregates every pending finding (
Open/In Progress) across all libraries.
See REVIEW-PROCESS.md for the full procedure: the review checklist, severity definitions, finding format, the library → component-spec mapping, and how to mark items resolved.
Layout
code-reviews/
├── README.md # this file — process overview + pending findings
├── REVIEW-PROCESS.md # how to perform a review and track findings
├── regen-readme.py # regenerates this README from the findings files
├── _template/findings.md # copy-this template for a library review
└── <Library>/findings.md # one folder per ZB.MOM.WW.* shared library
Summary
10 of 10 libraries reviewed. 26 pending findings across all libraries.
Coverage: Secrets, LocalDb, GalaxyRepository and Overview first reviewed, and Auth, Health, Theme, Audit, Configuration and Telemetry re-reviewed, on 2026-11-09 (#15). Each finding also carries a P1/P2/P3 priority (P1 = Critical/High, P2 = Medium, P3 = Low).
| Severity | Open findings |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 7 |
| Low | 19 |
| Total | 26 |
Library Status
| Library | Last reviewed | Commit | Open (C/H/M/L) | Open | Total |
|---|---|---|---|---|---|
| Audit | 2026-11-09 | 91fb2b4 |
0/0/0/0 | 0 | 5 |
| Auth | 2026-11-09 | 91fb2b4 |
0/0/1/3 | 4 | 10 |
| Configuration | 2026-11-09 | 91fb2b4 |
0/0/0/0 | 0 | 4 |
| GalaxyRepository | 2026-11-09 | 91fb2b4 |
0/0/0/0 | 0 | 8 |
| Health | 2026-11-09 | 91fb2b4 |
0/0/0/0 | 0 | 7 |
| LocalDb | 2026-11-09 | 91fb2b4 |
0/0/4/3 | 7 | 7 |
| Overview | 2026-11-09 | 91fb2b4 |
0/0/0/0 | 0 | 5 |
| Secrets | 2026-11-09 | 91fb2b4 |
0/0/2/13 | 15 | 19 |
| Telemetry | 2026-11-09 | 91fb2b4 |
0/0/0/0 | 0 | 11 |
| Theme | 2026-11-09 | 91fb2b4 |
0/0/0/0 | 0 | 8 |
Pending Findings
Every Open / In Progress finding across all libraries, highest severity first.
Resolved findings drop off this list but remain recorded in their library's
findings.md (see REVIEW-PROCESS.md §4–§5). Full detail —
description, location, recommendation — lives in the library's findings.md.
Critical (0)
None open.
High (0)
None open.
Medium (7)
| ID | Library | Title |
|---|---|---|
| Auth-007 | Auth | User-bind mode never checks that the entry whose groups it returns is the account that bound |
| LocalDb-001 | LocalDb | Sync endpoint is unauthenticated by default, and ApiKey is documented as "expected from the peer" |
| LocalDb-002 | LocalDb | Snapshot streams tables by name and rows by pk_json text, so FK children land before parents and are dead-lettered for good |
| LocalDb-003 | LocalDb | INSERT OR REPLACE apply silently deletes a different row on a secondary UNIQUE conflict, so the pair diverges |
| LocalDb-004 | LocalDb | FailClosedOnDrift is bypassed on the snapshot path, so a future-stamped snapshot row moves the local HLC forward permanently |
| Secrets-013 | Secrets | UI add/rotate/delete: if the audit write throws after the store write succeeded, the change is reported and audited as a failure and the resolver cache is not evicted |
| Secrets-017 | Secrets | secret set/rotate take the plaintext on argv; the host builder re-parses argv as configuration and can crash with the value in the error |
Low (19)
| ID | Library | Title |
|---|---|---|
| Auth-008 | Auth | SQL Server store silently truncates over-length values and matches key ids case-insensitively, diverging from SQLite |
| Auth-009 | Auth | Canonical username is the first value of a possibly multi-valued attribute, not the value the user matched |
| Auth-010 | Auth | ApiKeyVerifier lets store exceptions escape despite its "only exception path is cancellation" contract |
| LocalDb-005 | LocalDb | Auto-assigned rowid / AUTOINCREMENT keys collide across nodes and LWW silently drops one row |
| LocalDb-006 | LocalDb | Column names containing ' are captured but break the apply and snapshot SQL |
| LocalDb-007 | LocalDb | README is stale on version, adoption, test count, Synchronous values and baseline scope |
| Secrets-004 | Secrets | Corrupt or wrong-length crypto fields escape Decrypt as ArgumentException: not audited, and breaks the cipher contract |
| Secrets-005 | Secrets | Store-path rules are lexical: drive-relative paths pass "rooted", and \\?\ / UNC / 8.3 / symlink spellings bypass "under content root" |
| Secrets-006 | Secrets | Resolver cache can re-cache a stale value after Invalidate, and expired plaintext is never evicted |
| Secrets-007 | Secrets | KEK is re-resolved on every access: KekId and key are read non-atomically, the work is repeated per op, and key copies are never zeroed |
| Secrets-008 | Secrets | Hub push sweep is not isolated per row, so one rejected row stops every later push |
| Secrets-009 | Secrets | README wiring example uses a relative SqlitePath that the validator rejects; empty or unterminated tokens pass through as literals |
| Secrets-011 | Secrets | HandleAnnounceTick publishes from a thread-pool continuation: announces carry no sender under an async store, and faults are silently dropped |
| Secrets-012 | Secrets | Akka anti-entropy replies are unchunked; a large store exceeds Akka.Remote's frame size and never converges |
| Secrets-014 | Secrets | UI "Add secret" silently overwrites an existing (or tombstoned) secret, clobbers its metadata, and audits it as secret.add |
| Secrets-015 | Secrets | Reveal shows the plaintext even when its audit write fails, and swallows the failure with no log |
| Secrets-016 | Secrets | The public UI components enforce no authorization of their own; gating relies entirely on the host rendering them inside AuthorizeView |
| Secrets-018 | Secrets | Bundle import aborts mid-run on one bad entry, leaving earlier rows applied but reporting only "Import failed" |
| Secrets-019 | Secrets | Hub-client cancellation surfaces as RpcException(Cancelled), so shutdown is recorded as a failed sweep and the failover reader rotates through every endpoint with a cancelled token |