Fixes the 8 findings from the 2026-06-24 re-review (commit c42bb485), with a
regression test per Medium finding:
- DataConnectionLayer-029 (Med): HandleAlarmSubscribeCompleted now mirrors the
tag-path re-check — if a feed is already stored for the source, release the
redundant just-created subscription instead of overwriting + leaking the first
one (the double-subscribe window DCL-023 reopened). +regression test.
- InboundAPI-031 (Med): remove WaitForAttribute's local 5s grace backstop (tighter
than the CommunicationService Ask's timeout+IntegrationTimeout round-trip budget,
so a slow-but-valid timed-out 'false' got cancelled into a 500). Link only the
client-abort + explicit caller tokens; the lower layer owns the backstop. +test.
- SiteRuntime-032 (Med): derive the deployed count from an authoritative set of
deployed config names (HashSet) instead of a map-presence-gated int, so deleting
a DISABLED instance decrements correctly (SiteRuntime-029's gate leaked it).
+deploy->disable->delete regression test.
- StoreAndForward-028 (Med): reset _bufferedCount in StopAsync alongside the
register-guard so a same-instance Stop->Start re-seeds from a clean base (no ~2N
gauge double-count). +restart regression test.
- AuditLog-017 (Low): test the OnIngestAsync scope-resolution guard (actor survives,
replies empty, counts the failure) — no longer unpinned.
- CentralUI-037 / ScriptAnalysis-009 / SiteRuntime-033 (Low): doc-comment + spec
fixes (Database-throws in the inbound sandbox; baseReferences param wording;
native-alarm cap return-to-normal + per-condition NativeAlarmDropped eviction).
Targeted suites green: SiteRuntime 5, StoreAndForward 6, InboundAPI 31,
DataConnectionLayer 10, AuditLog 5, ScriptAnalysis 40, CentralUI ScriptAnalysis 52.
Resolves InboundAPI-026/027/028/029 (+ newly-surfaced -030).
- 026: authorize the scoped Database helper in the design doc; SQL-injection
protection is parameter binding (values never concatenated); allow writes via
ExecuteAsync; drop the false 'read-only' claim. Named connections only.
- 027: async ADO.NET end-to-end (no .GetAwaiter().GetResult()); honour the method
deadline token on ExecuteScalarAsync/ExecuteReaderAsync/ExecuteNonQueryAsync +
a CommandTimeout backstop derived from the method timeout.
- 028: negative-path tests (null-gateway, deadline cancellation, parameterization)
+ e2e Database + WaitForAttribute cases through the real endpoint.
- 029: WaitForAttribute is bounded by its WAIT timeout (per-wait CTS + client-abort
+ explicit token), NOT the method deadline (spec §6) — a long wait may outlive the
method timeout; WithRequestAborted threads the raw client-abort token separately.
- 030: Central UI compile-surface mirrors (InboundScriptHost / SandboxInboundScriptHost)
gained the Database member (drifted since the runtime helper was added) so the
authorized async API type-checks at the design-time gate.
1. Request headers in Extra JSON (AuditWriteMiddleware): adds a `requestHeaders`
object to the existing Extra JSON alongside remoteIp/userAgent; headers whose
names appear in AuditLogOptions.HeaderRedactList (Authorization, X-Api-Key,
Cookie, Set-Cookie by default) are replaced with "<redacted>" using
OrdinalIgnoreCase matching — same policy as ScadaBridgeAuditRedactor.
2. AuditInboundCeilingHits counter: new IAuditInboundCeilingHitsCounter interface
+ NoOpAuditInboundCeilingHitsCounter default; AuditCentralHealthSnapshot
implements the interface (Interlocked field, thread-safe) and exposes
AuditInboundCeilingHits on IAuditCentralHealthSnapshot; AddAuditLog registers
the NoOp default, AddAuditLogCentralMaintenance forwards to the snapshot;
AuditWriteMiddleware accepts the counter as an optional ctor arg and increments
it once per request where either the request or response body hit the cap.
3. Per-method SkipBodyCapture opt-out: adds SkipBodyCapture bool to
PerTargetRedactionOverride; AuditWriteMiddleware consults the per-target
override map at the start of InvokeAsync (before EnableBuffering) and, when
set, skips body read + capture entirely — the audit row still emits with
headers/metadata but null RequestSummary/ResponseSummary; truncation flags
are also cleared so the ceiling-hits counter is not bumped for opted-out methods.
- Add WhitespaceAuthorization_ValidXApiKey_Returns200: pins the IsNullOrWhiteSpace
fall-through — a present-but-blank Authorization header is treated as absent so a
valid X-API-Key still authenticates (200).
- Remove MissingBearer_Returns401 (added in 510559e): identical path to
NeitherHeader_Returns401 (no Authorization + no X-API-Key → 401); keep the
descriptively-named NeitherHeader variant.
- Change "legacy 'X-API-Key'" -> "alternate 'X-API-Key'" in EndpointExtensions.cs and
the BuildPostWithApiKeyHeader/HappyPath doc comments to avoid implying Bearer is
the older transport (Bearer was itself introduced by the prior auth re-arch).
Object/List parameters and return values were shape-validated only (object vs
array), with no field-level/nested type checks — type-wrong nested data passed
inbound validation and failed only at script runtime. Add recursive type
validation (declared Object field types, List element type, scalars at any depth)
with path-qualified errors, symmetric across ParameterValidator and ReturnValueValidator.
Both validators now parse the canonical JSON Schema definition format (the
Central UI / MigrateParametersToJsonSchema output) via a shared recursive engine,
Commons.Types.InboundApi.InboundApiSchema, instead of the legacy flat
[{name,type}] array which they could not even deserialize from migrated rows.
The legacy flat-array form is still accepted on read for transition safety.
Undeclared fields are rejected at every level (consistent with the existing
top-level unexpected-parameter rejection); a present-but-null value satisfies
any type, only absence of a required field is an error.