refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)

Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
+51 -51
View File
@@ -2,7 +2,7 @@
| Field | Value |
|-------|-------|
| Module | `src/ScadaLink.InboundAPI` |
| Module | `src/ZB.MOM.WW.ScadaBridge.InboundAPI` |
| Design doc | `docs/requirements/Component-InboundAPI.md` |
| Status | Reviewed |
| Last reviewed | 2026-05-28 |
@@ -33,7 +33,7 @@ are High severity and should be addressed before production use.
#### Re-review 2026-05-17 (commit `39d737e`)
All 13 findings from the initial review remain `Resolved`; the module source under
`src/ScadaLink.InboundAPI` is unchanged since the last InboundAPI fix commit
`src/ZB.MOM.WW.ScadaBridge.InboundAPI` is unchanged since the last InboundAPI fix commit
(`8dd7412`), which precedes `39d737e`. This re-review re-walked all 10 checklist
categories against the resolved code and surfaced **4 new findings** — none touching
the previously-fixed concurrency/trust-model code, but all in areas the first pass
@@ -68,16 +68,16 @@ statement that the timeout covers routed calls (InboundAPI-016); and (4) `RouteH
All 17 prior findings remain `Resolved`. The module has grown materially since the
last pass — a new `AuditWriteMiddleware` (Audit Log #23 M4 Bundle D) now lives under
`src/ScadaLink.InboundAPI/Middleware/`, the `ApiKeyValidator` was rewired to hash the
`src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/`, the `ApiKeyValidator` was rewired to hash the
candidate with `IApiKeyHasher` (ConfigurationDatabase-012), and an `IInstanceRouter`
seam was introduced. This re-review re-walked all 10 checklist categories against
`1eb6e97` and surfaced **8 new findings** concentrated on the new audit middleware
and a stranded follow-up from InboundAPI-008:
1. The InboundAPI-008 resolution explicitly deferred registering an `IActiveNodeGate`
implementation in `ScadaLink.Host` as a "follow-up outside this module's scope" —
implementation in `ZB.MOM.WW.ScadaBridge.Host` as a "follow-up outside this module's scope" —
that follow-up is still unfulfilled (no production registration anywhere in
`src/ScadaLink.Host/`), so the design-mandated standby-node gating is silently
`src/ZB.MOM.WW.ScadaBridge.Host/`), so the design-mandated standby-node gating is silently
disabled in production today (`InboundAPI-022`, High).
2. `AuditWriteMiddleware` is wired in `Program.cs` against `/api/*` rather than the
specific `POST /api/{methodName}` route, so GETs against `/api/audit/query` and
@@ -133,7 +133,7 @@ configuration database, but the invariant is undocumented.)
| Severity | High |
| Category | Concurrency & thread safety |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:17`, `:32`, `:40`, `:89`, `:123-128` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:17`, `:32`, `:40`, `:89`, `:123-128` |
**Description**
@@ -169,7 +169,7 @@ via `GetOrAdd` so concurrent first-callers share one handler. Regression tests
| Severity | Medium — re-triaged: already fixed by the InboundAPI-001 fix; verified and closed |
| Category | Concurrency & thread safety |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:152-161` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:152-161` |
**Description**
@@ -209,7 +209,7 @@ handler that `GetOrAdd` keeps. Regression test
| Severity | High |
| Category | Security |
| Status | Resolved |
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/InboundApiRepository.cs:22-23`, consumed by `src/ScadaLink.InboundAPI/ApiKeyValidator.cs:33` |
| Location | `src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/Repositories/InboundApiRepository.cs:22-23`, consumed by `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiKeyValidator.cs:33` |
**Description**
@@ -251,7 +251,7 @@ longer depends on it.
| Severity | Medium |
| Category | Error handling & resilience |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:117-141` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:117-141` |
**Description**
@@ -293,7 +293,7 @@ added.
| Severity | High |
| Category | Security |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:56-93` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:56-93` |
**Description**
@@ -337,7 +337,7 @@ Regression tests `CompileAndRegister_ForbiddenApi_RejectsScript` (theory),
| Severity | Medium |
| Category | Security |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:54-62` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:54-62` |
**Description**
@@ -365,7 +365,7 @@ It rejects requests whose declared `Content-Length` exceeds `InboundApiOptions.
MaxRequestBodyBytes` (default 1 MiB) with HTTP 413 *before* the handler buffers the
body into a `JsonDocument`, and also lowers the per-request `IHttpMaxRequestBodySizeFeature`
cap so a chunked/unknown-length stream is cut off by Kestrel while being read. The
limit is configurable via the bound `ScadaLink:InboundApi` options section. Regression
limit is configurable via the bound `ScadaBridge:InboundApi` options section. Regression
tests `OversizedBody_ShortCircuitsWith413_AndDoesNotRunHandler`, `BodyAtLimit_RunsHandler`,
and `FilterCapsMaxRequestBodySizeFeature` added.
@@ -376,7 +376,7 @@ and `FilterCapsMaxRequestBodySizeFeature` added.
| Severity | Medium |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:188-203` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:188-203` |
**Description**
@@ -402,7 +402,7 @@ Resolved 2026-05-16 (commit `<pending>`). The drift was confirmed real:
so a method script following the documented `Database.Connection("name")` API
would fail to compile. Resolution direction: the design doc is stale, not the
code. Implementing `Database.Connection()` would hand inbound API scripts a
*raw* MS SQL client, in direct tension with the ScadaLink script trust model
*raw* MS SQL client, in direct tension with the ScadaBridge script trust model
(scripts are forbidden `System.IO`, raw network, etc.; `ForbiddenApiChecker`
statically enforces this). Rather than carve a hole in the trust model, the
"Database Access" section was removed from `docs/requirements/Component-InboundAPI.md`
@@ -418,7 +418,7 @@ explicit design change. Code and doc now agree; no code or test change required.
| Severity | Medium |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:19-23`, `src/ScadaLink.Host/Program.cs:149` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:19-23`, `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:149` |
**Description**
@@ -447,7 +447,7 @@ auth/script work, so Traefik/clients only reach the live node — consistent wit
registered (non-clustered host / tests) the endpoint defaults to "allow", preserving
prior behaviour. Regression tests `StandbyNode_ShortCircuitsWith503_AndDoesNotRunHandler`,
`ActiveNode_PassesGate_RunsHandler`, and `NoGateRegistered_PassesGate_RunsHandler`
added. **Follow-up (outside this module's scope):** `ScadaLink.Host` should register
added. **Follow-up (outside this module's scope):** `ZB.MOM.WW.ScadaBridge.Host` should register
an `IActiveNodeGate` implementation backed by `ActiveNodeHealthCheck` /
`Cluster.State.Leader` in the central-role branch of `Program.cs` so the gate is
actually enforced in production; until then the endpoint defaults to "allow".
@@ -459,7 +459,7 @@ actually enforced in production; until then the endpoint defaults to "allow".
| Severity | Low |
| Category | Performance & resource management |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:123-128` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:123-128` |
**Description**
@@ -496,7 +496,7 @@ re-evaluated. Regression tests `FailedCompilation_IsNotRetriedOnEveryRequest`
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/ParameterValidator.cs:64-90`, `:112-118` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs:64-90`, `:112-118` |
**Description**
@@ -541,7 +541,7 @@ follow-up. Regression tests `UnexpectedBodyField_ReturnsInvalid` and
| Severity | Low |
| Category | Security |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/ApiKeyValidator.cs:39-52` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiKeyValidator.cs:39-52` |
**Description**
@@ -580,14 +580,14 @@ indistinguishable contract.
| Severity | Low |
| Category | Code organization & conventions |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/ParameterValidator.cs:128-133` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ParameterValidator.cs:128-133` |
**Description**
`ParameterDefinition` is a persistence-/contract-shaped POCO: it is the deserialized
form of `ApiMethod.ParameterDefinitions` (a column in the configuration database) and
describes the public API contract. CLAUDE.md's code-organization rules place
persistence-ignorant entity/contract types in `ScadaLink.Commons`. Defining it inside
persistence-ignorant entity/contract types in `ZB.MOM.WW.ScadaBridge.Commons`. Defining it inside
the InboundAPI project means any other component that needs to read or produce method
parameter definitions (e.g. Central UI's method editor, CLI, Management Service)
cannot share the type and will duplicate it.
@@ -595,7 +595,7 @@ cannot share the type and will duplicate it.
**Recommendation**
Move `ParameterDefinition` (and a matching return-definition type, if added) to
`ScadaLink.Commons` under the InboundApi entity/types namespace so it is shared by all
`ZB.MOM.WW.ScadaBridge.Commons` under the InboundApi entity/types namespace so it is shared by all
components that work with method definitions.
**Resolution**
@@ -604,22 +604,22 @@ Resolved 2026-05-16 (commit `<pending>`): root cause confirmed against the sourc
`ParameterDefinition` was a persistence-ignorant, API-contract-shaped POCO (the
deserialized form of the `ApiMethod.ParameterDefinitions` configuration-database
column) declared inside the component project, contrary to CLAUDE.md's
code-organization rule that such shared contract types live in `ScadaLink.Commons`.
The type was moved to `src/ScadaLink.Commons/Types/InboundApi/ParameterDefinition.cs`
(namespace `ScadaLink.Commons.Types.InboundApi`) — placed under `Types/` with an
code-organization rule that such shared contract types live in `ZB.MOM.WW.ScadaBridge.Commons`.
The type was moved to `src/ZB.MOM.WW.ScadaBridge.Commons/Types/InboundApi/ParameterDefinition.cs`
(namespace `ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi`) — placed under `Types/` with an
`InboundApi` domain subfolder, matching the existing `Types/Scripts/` precedent, since
the column itself is the persisted form and this type is its deserialized contract
shape (not an EF-mapped entity). It remains a pure POCO with no EF attributes and no
behaviour. `ParameterValidator` now imports the moved type via a `using
ScadaLink.Commons.Types.InboundApi;` directive; a tree-wide search confirmed
ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;` directive; a tree-wide search confirmed
`ParameterValidator.cs` was the type's only declaration and only direct consumer (all
other `ParameterDefinition*` matches are the unrelated `ParameterDefinitions` string
property). No return-definition type exists in the codebase — only a `ReturnDefinition`
string column — so none was invented. No behavioural change, so no new runtime
regression test: this is a compile-level type move, and the existing 52
`ScadaLink.InboundAPI.Tests` (including the `ParameterValidator` suite) act as the
regression guard. `dotnet test` for `ScadaLink.InboundAPI.Tests` (52 passed) and
`ScadaLink.Commons.Tests` (226 passed) are green; `dotnet build ScadaLink.slnx`
`ZB.MOM.WW.ScadaBridge.InboundAPI.Tests` (including the `ParameterValidator` suite) act as the
regression guard. `dotnet test` for `ZB.MOM.WW.ScadaBridge.InboundAPI.Tests` (52 passed) and
`ZB.MOM.WW.ScadaBridge.Commons.Tests` (226 passed) are green; `dotnet build ZB.MOM.WW.ScadaBridge.slnx`
succeeds with 0 warnings / 0 errors.
### InboundAPI-013 — `ApiKeyValidationResult.NotFound` factory returns HTTP 400, contradicting its name
@@ -629,7 +629,7 @@ succeeds with 0 warnings / 0 errors.
| Severity | Low |
| Category | Documentation & comments |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/ApiKeyValidator.cs:78-79` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ApiKeyValidator.cs:78-79` |
**Description**
@@ -667,7 +667,7 @@ from "key not approved"), but that doc edit is outside this module's editable sc
| Severity | Medium |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:201-205`, `src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs:10` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:201-205`, `src/ZB.MOM.WW.ScadaBridge.Commons/Entities/InboundApi/ApiMethod.cs:10` |
**Description**
@@ -725,7 +725,7 @@ is validation-only (no coercion). Regression tests: `ReturnValueValidatorTests`
| Severity | Medium |
| Category | Security |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/ForbiddenApiChecker.cs:63-119`, `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:109-126` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/ForbiddenApiChecker.cs:63-119`, `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:109-126` |
**Description**
@@ -799,7 +799,7 @@ namespace-deny-list regression guards.
| Severity | Medium |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/RouteHelper.cs:59-152`, `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:177`, `:199` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs:59-152`, `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:177`, `:199` |
**Description**
@@ -863,13 +863,13 @@ of running orphaned. Regression tests (in the new `RouteHelperTests`):
| Severity | Low |
| Category | Testing coverage |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/RouteHelper.cs:1-165`, `tests/ScadaLink.InboundAPI.Tests/` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs:1-165`, `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/` |
**Description**
`RouteHelper`/`RouteTarget` is the entire WP-4 cross-site routing surface — the
`Route.To().Call()/GetAttribute(s)/SetAttribute(s)` API that inbound API scripts use
to reach instances at any site. It has zero tests: the `ScadaLink.InboundAPI.Tests`
to reach instances at any site. It has zero tests: the `ZB.MOM.WW.ScadaBridge.InboundAPI.Tests`
project covers `ApiKeyValidator`, `ParameterValidator`, `InboundScriptExecutor`, and
`InboundApiEndpointFilter`, but no test file exercises `RouteHelper`. Untested
behaviours include site resolution via `IInstanceLocator` (including the
@@ -892,7 +892,7 @@ wiring is added.
**Resolution**
Resolved 2026-05-17 (commit `<pending>`): confirmed — `ScadaLink.InboundAPI.Tests` had
Resolved 2026-05-17 (commit `<pending>`): confirmed — `ZB.MOM.WW.ScadaBridge.InboundAPI.Tests` had
no file exercising `RouteHelper`/`RouteTarget`. To make the surface testable without a
live actor system, an `IInstanceRouter` seam was introduced in the module (the routing
transport `RouteHelper` depends on); the production `CommunicationServiceInstanceRouter`
@@ -912,7 +912,7 @@ the InboundAPI-016 deadline-token inheritance behaviour. All 15 pass.
| Severity | Medium |
| Category | Error handling & resilience |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs:257` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs:257` |
**Resolution (2026-05-28):** kept the fire-and-forget (audit emission must
never block or alter the user-facing response per alog.md §13) but added
@@ -968,7 +968,7 @@ synchronous throw) to pin the new contract.
|--|--|
| Severity | Low |
| Category | Performance & resource management |
| Location | `src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs:141` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs:141` |
| Status | Resolved |
**Resolution (2026-05-28):** Added a `RequestHasBody(HttpRequest)` guard that returns `true` only when `ContentLength > 0` or the method is POST / PUT / PATCH (the `HttpMethods.IsPost/Put/Patch` helpers); the `EnableBuffering` + `ReadBufferedRequestBodyAsync` call now sits behind that guard. Bodyless GET / HEAD / DELETE / TRACE / OPTIONS requests (and any explicit `Content-Length: 0`) skip the `FileBufferingReadStream` allocation; body-carrying methods with no Content-Length (chunked POST etc.) still buffer. Regression tests `BodylessMethod_SkipsEnableBuffering_RequestStreamIsNotReplaced` (GET/HEAD/DELETE theory), `BodylessPost_ContentLengthZero_SkipsEnableBuffering`, and `PostWithBody_StillEnablesBuffering_AndCapturesRequestSummary` (anti-regression).
@@ -1000,7 +1000,7 @@ bodyless request through the middleware.
| Severity | Low |
| Category | Correctness & logic bugs |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:70` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:70` |
**Resolution (2026-05-28):** swapped the case-sensitive `Contains("json")`
substring match for `Contains("json", StringComparison.OrdinalIgnoreCase)` so
@@ -1041,7 +1041,7 @@ regression test posting with `application/JSON` and Transfer-Encoding: chunked.
| Severity | Medium |
| Category | Design-document adherence |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/RouteHelper.cs:141-143`, `:182-183`, `:225-226`; `src/ScadaLink.Commons/Messages/InboundApi/RouteToInstanceRequest.cs:15-21`, `:36-40`, `:55-59` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/RouteHelper.cs:141-143`, `:182-183`, `:225-226`; `src/ZB.MOM.WW.ScadaBridge.Commons/Messages/InboundApi/RouteToInstanceRequest.cs:15-21`, `:36-40`, `:55-59` |
**Description**
@@ -1104,9 +1104,9 @@ backlog), they can stamp the parent id without any further plumbing.
| Severity | High |
| Category | Security |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/IActiveNodeGate.cs`, `src/ScadaLink.InboundAPI/InboundApiEndpointFilter.cs:52-60`; absent from `src/ScadaLink.Host/Program.cs` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/IActiveNodeGate.cs`, `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundApiEndpointFilter.cs:52-60`; absent from `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs` |
**Resolution** — Added `src/ScadaLink.Host/Health/ActiveNodeGate.cs`, a production `IActiveNodeGate` implementation backed by `AkkaHostedService` that mirrors `ActiveNodeHealthCheck`'s leadership probe (member status `Up` AND `Cluster.State.Leader == SelfAddress`), and registered it as a singleton in the central-role branch of `Program.cs`. A structural regression test (`CentralCompositionRootTests.Central_IActiveNodeGate_IsRegisteredAsActiveNodeGate`) reflects over the built `IServiceProvider` to assert the registration's existence and concrete type — failing on `main` and passing after the fix. The `InboundApiEndpointFilter`'s fall-through-to-allow behaviour is retained as the documented safe default for non-clustered hosts and tests.
**Resolution** — Added `src/ZB.MOM.WW.ScadaBridge.Host/Health/ActiveNodeGate.cs`, a production `IActiveNodeGate` implementation backed by `AkkaHostedService` that mirrors `ActiveNodeHealthCheck`'s leadership probe (member status `Up` AND `Cluster.State.Leader == SelfAddress`), and registered it as a singleton in the central-role branch of `Program.cs`. A structural regression test (`CentralCompositionRootTests.Central_IActiveNodeGate_IsRegisteredAsActiveNodeGate`) reflects over the built `IServiceProvider` to assert the registration's existence and concrete type — failing on `main` and passing after the fix. The `InboundApiEndpointFilter`'s fall-through-to-allow behaviour is retained as the documented safe default for non-clustered hosts and tests.
**Description**
@@ -1118,14 +1118,14 @@ when `gate is { IsActiveNode: false }` returns HTTP 503. The filter's behaviour
when **no implementation is registered** (line 51 comment) is to fall through and
serve the request — the resolution paragraph for InboundAPI-008 closes with:
> "Follow-up (outside this module's scope): `ScadaLink.Host` should register an
> "Follow-up (outside this module's scope): `ZB.MOM.WW.ScadaBridge.Host` should register an
> `IActiveNodeGate` implementation backed by `ActiveNodeHealthCheck` /
> `Cluster.State.Leader` in the central-role branch of `Program.cs` so the gate is
> actually enforced in production; until then the endpoint defaults to "allow"."
A grep of the entire `src/ScadaLink.Host/` tree at `1eb6e97` finds **zero**
A grep of the entire `src/ZB.MOM.WW.ScadaBridge.Host/` tree at `1eb6e97` finds **zero**
`IActiveNodeGate` registrations: `grep -rn "IActiveNodeGate\|AddSingleton.*ActiveNode"
src/ScadaLink.Host/` returns no matches. The follow-up was never carried out. So
src/ZB.MOM.WW.ScadaBridge.Host/` returns no matches. The follow-up was never carried out. So
in production today the standby central node still serves the inbound API exactly
as InboundAPI-008 described — executes method scripts, runs `Route.To()` calls,
races the active node, and may operate against stale singleton state. The new
@@ -1138,7 +1138,7 @@ The design says the inbound API is "Central cluster only (active node)" and
**Recommendation**
Register an `IActiveNodeGate` implementation in the central-role branch of
`ScadaLink.Host/Program.cs`. The natural backing is the existing
`ZB.MOM.WW.ScadaBridge.Host/Program.cs`. The natural backing is the existing
`ActiveNodeHealthCheck` (already wired for `/health/active`) or a direct read of
`Cluster.Get(actorSystem).State.Leader == Cluster.Get(actorSystem).SelfAddress`.
Add an integration test in the Host that spins up the central role and asserts
@@ -1153,9 +1153,9 @@ realisation of the InboundAPI-008 vulnerability.
| Severity | Low |
| Category | Testing coverage |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/EndpointExtensions.cs:31-140`, `tests/ScadaLink.InboundAPI.Tests/` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/EndpointExtensions.cs:31-140`, `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/` |
**Resolution (2026-05-28):** Added `tests/ScadaLink.InboundAPI.Tests/EndpointExtensionsTests.cs`, a `TestServer`-hosted suite (same pattern as `EndpointContentTypeTests`) that drives the `POST /api/{methodName}` wiring end-to-end. Seven cases pin the composed flow: happy path (200 + script result body), missing API key (401), unknown method (403, indistinguishable from "not approved" per InboundAPI-011), invalid JSON body (400), missing required parameter (400 from `ParameterValidator`), script throws (500 with sanitized error body — the executor's catch-all replaces the raw exception with `"Internal script error"`), and the `HttpContext.Items[AuditWriteMiddleware.AuditActorItemKey]` actor-stash invariant (verified by an inline capture middleware reading the slot after the endpoint runs). All 7 new tests pass; total InboundAPI.Tests now 158 (was 151).
**Resolution (2026-05-28):** Added `tests/ZB.MOM.WW.ScadaBridge.InboundAPI.Tests/EndpointExtensionsTests.cs`, a `TestServer`-hosted suite (same pattern as `EndpointContentTypeTests`) that drives the `POST /api/{methodName}` wiring end-to-end. Seven cases pin the composed flow: happy path (200 + script result body), missing API key (401), unknown method (403, indistinguishable from "not approved" per InboundAPI-011), invalid JSON body (400), missing required parameter (400 from `ParameterValidator`), script throws (500 with sanitized error body — the executor's catch-all replaces the raw exception with `"Internal script error"`), and the `HttpContext.Items[AuditWriteMiddleware.AuditActorItemKey]` actor-stash invariant (verified by an inline capture middleware reading the slot after the endpoint runs). All 7 new tests pass; total InboundAPI.Tests now 158 (was 151).
**Description**
@@ -1190,7 +1190,7 @@ resolved key name after successful auth, but is absent on auth failures).
| Severity | Low |
| Category | Performance & resource management |
| Status | Resolved |
| Location | `src/ScadaLink.InboundAPI/InboundScriptExecutor.cs:30`, `:77`, `:223`, `:233` |
| Location | `src/ZB.MOM.WW.ScadaBridge.InboundAPI/InboundScriptExecutor.cs:30`, `:77`, `:223`, `:233` |
**Resolution (2026-05-28):** capped the cache at `KnownBadMethodsCap = 1000`
entries via a new `TryRecordBadMethod` helper that short-circuits when the cap
@@ -1236,7 +1236,7 @@ immediate change required; this is a watch-list item.
| Severity | Medium |
| Category | Correctness & logic bugs |
| Status | Resolved |
| Location | `src/ScadaLink.Host/Program.cs:183-185`; consumers: `src/ScadaLink.ManagementService/AuditEndpoints.cs:93-94`; emitter: `src/ScadaLink.InboundAPI/Middleware/AuditWriteMiddleware.cs:175-252` |
| Location | `src/ZB.MOM.WW.ScadaBridge.Host/Program.cs:183-185`; consumers: `src/ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs:93-94`; emitter: `src/ZB.MOM.WW.ScadaBridge.InboundAPI/Middleware/AuditWriteMiddleware.cs:175-252` |
**Resolution (2026-05-28):** Took the defensive path-exclusion in
`Program.cs` (Option 1 from the recommendation). The `UseWhen` predicate
@@ -1255,7 +1255,7 @@ the excluded prefixes, which would be noisy in code review.
`Program.cs` wires the audit middleware as
`app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), branch => branch.UseAuditWriteMiddleware())`
— scoped to the `/api` *prefix*, not to the `POST /api/{methodName}` route.
Meanwhile, `ScadaLink.ManagementService/AuditEndpoints.cs` maps
Meanwhile, `ZB.MOM.WW.ScadaBridge.ManagementService/AuditEndpoints.cs` maps
`MapGet("/api/audit/query", ...)` (line 93) and `MapGet("/api/audit/export", ...)`
(line 94). Both routes therefore inherit `AuditWriteMiddleware`, which emits an
`AuditEvent { Channel = AuditChannel.ApiInbound, Kind = AuditKind.InboundRequest, ... }`