Commit Graph

735 Commits

Author SHA1 Message Date
Joseph Doherty 2dabb07a59 docs(index): OtOpcUa #796/#797/#798/#806 history — #797 scripted-alarm equipment-scope authorization 2026-11-10 22:31:37 -05:00
Joseph Doherty ed122145ed docs(index): record Linux OPC UA lifecycle diagnostic 2026-11-10 20:28:11 -05:00
Joseph Doherty 16418e48a0 docs(index): record SDK1 terminal server ownership repair 2026-11-10 17:14:53 -05:00
Joseph Doherty 805e3aac67 docs(index): record final SDK2 review and remaining release gates 2026-11-10 16:09:25 -05:00
Joseph Doherty aaa669f536 docs(index): mark historian TLS consumer migration verified 2026-11-10 14:52:08 -05:00
Joseph Doherty a33d8ecdfd docs(index): align current OtOpcUa master and gateway package pins 2026-11-10 14:50:56 -05:00
Joseph Doherty cbc7291f7b docs(index): record SDK2 candidate gates and HistorianGateway 1.0 consumer 2026-11-10 14:50:13 -05:00
Joseph Doherty 4c5a52d85f index + history: HistorianGateway Contracts/Client 1.0.0 released 2026-11-10 14:35:31 -05:00
Joseph Doherty 813ef65dbd historiangateway history: #58 closed (TLS secure by default), #131 filed 2026-11-10 14:13:45 -05:00
Joseph Doherty ccabcc68bc historiangateway history: #85 closed (rename-job polling) 2026-11-10 12:43:33 -05:00
Joseph Doherty 59bf74760c infra(glauth): enable LDAPS on :3894 alongside plain :3893 (#19, mxaccessgw#180)
Self-signed cert (SAN IP 10.100.0.35), public cert checked in, key host-only.
Deployed on 10.100.0.35: TLS chain verifies with the cert and fails without
it; raw LDAPS simple bind returns 0 for a good password and 49 for a bad one;
plain :3893 bind unchanged. README: thumbprint, client config, rotation; also
fixes the user-bind DN example (GLAuth puts users under their primary group OU).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 11:46:48 -05:00
Joseph Doherty 736158f2ad historiangateway history: #126 closed 2026-11-10 11:46:35 -05:00
Joseph Doherty bcbf1d006f historiangateway history: #115 live gate passed, #126 filed, #85/#58 status 2026-11-10 11:14:57 -05:00
Joseph Doherty e1ad6becb0 docs(index): record OtOpcUa HistorianGateway 0.4.0 consumption 2026-11-10 10:36:43 -05:00
Joseph Doherty 9f986e16e8 historiangateway history: CI not required to merge; #64 kit rail look 2026-11-10 10:27:31 -05:00
Joseph Doherty b3de2b7128 HistorianGateway: main branch protection + CI de-flake (historiangw#73) 2026-11-10 10:24:35 -05:00
Joseph Doherty fe5269208f HistorianGateway: native WriteLiveValues hardened (historiangw#25) 2026-11-10 10:00:56 -05:00
Joseph Doherty 92969c9c85 HistorianGateway: Java client 0.2.0 published; follow-ups #119/#120 2026-11-10 09:44:55 -05:00
Joseph Doherty 129d37f9e2 HistorianGateway: Contracts/Client 0.4.0 released (historiangw#79) 2026-11-10 09:39:57 -05:00
Joseph Doherty b9bcf33834 docs(index): HistorianGateway PRs #116/#117 merged (733eb25) 2026-11-10 09:35:54 -05:00
Joseph Doherty a5fd97b013 docs(index): HistorianGateway — ReadBlocks removed, datatype cast-or-reject, event id minting (historiangw 2026-11-10) 2026-11-10 09:35:07 -05:00
Joseph Doherty d430684322 docs(index): OtOpcUa, ScadaBridge, HistorianGateway tips after the 2026-11-10 issue waves
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-10 06:16:05 -05:00
Joseph Doherty 48af177dce fix(theme): shell fills the dynamic viewport — drop calc(100vh - 3.3rem) (scadaproj#10)
.app-shell min-height is now 100vh with a 100dvh override (the 3.3rem was a
donor-app top-bar leftover; ThemeShell has no top bar) and the lg+ sticky
rail is 100vh/100dvh. Source bumped to 0.5.3, UNPUBLISHED. Adds a static
regression test and fixes the CS8602 in NavStateScriptTests (0 warnings).

Browser-checked via Overview on local 0.5.2 vs 0.5.3 packages: at 390x844 a
short page went from a 791px shell (53px gap) to 844px, no extra scroll;
1440x900 rail stays sticky full-height.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:36:20 -05:00
Joseph Doherty eab617f741 docs(index): LocalDb 0.3.7 fixes #16; Secrets Replicator.Grpc deprecated (scadaproj#16, #8)
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:30:57 -05:00
Joseph Doherty ab1ad689df chore(secrets): deprecate Replicator.Grpc — out of the build and pack (scadaproj#8)
Owner chose option (b). ZB.MOM.WW.Secrets.Replicator.Grpc has had no consumer
since ScadaBridge retired the gRPC secrets hub (2026-09-28); OtOpcUa never used it.
- removed the project and its test project from ZB.MOM.WW.Secrets.slnx, so the
  family build/test and build/push.sh's `dotnet pack` skip it;
- csproj IsPackable=false (a direct pack of the project yields nothing);
- SecretsGrpcHubServerExtensions / SecretsGrpcHubClientExtensions [Obsolete];
- README, CHANGELOG (0.6.6 Deprecated), CLAUDE.md and clustered-secrets.md say so.
Source + tests stay in the tree and still build/run standalone (test csproj
suppresses CS0618): 200/200. Feed versions 0.4.1-0.6.3 are left for the owner
to unlist.

Also zeroes the solution's pre-existing warnings: CA1416 on two Unix-only
permission tests (guarded by Skip.IfNot, which the analyzer cannot see — now
[UnsupportedOSPlatform("windows")]) and two xUnit2031 Assert.Single(Where).
Build 0 warnings; tests 429 passed / 21 skipped / 0 failed; pack = 5 nupkgs.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:30:57 -05:00
Joseph Doherty be5e9fd436 fix(localdb): keep a tombstone until the peer holds it — no resurrection past TombstoneRetention (scadaproj#16)
0.3.7 (unpublished), owner-chosen option A. A tombstone is now pruned only when
it is past TombstoneRetention AND the peer is known to hold it:
- held while database-wide or per-table snapshot debt is outstanding (the
  snapshot is built from the row-version ledger, so a pruned tombstone is a
  delete it can no longer carry — the issue's second path);
- a local tombstone held while its delete is still in the unacked oplog.
Delivery (delta ack covering the delete, or snapshot debt discharged) restamps
tombstone_utc so retention runs from delivery. New optional
LocalDb:Replication:TombstoneHardCap (default off, >= TombstoneRetention)
bounds an undelivered tombstone's life. No proto, lib_schema_version or
on-disk schema change.

Regression test TombstoneResurrectionTests (two real nodes over loopback gRPC):
offline update on B, newer delete on A, A's clock +8 days (oplog age cap +
tombstone prune) -> reconnect. Fails on 0.3.6 (row resurrected), passes now;
plus a no-prune positive control and a pruned-after-delivery control, and five
controlled-clock OplogStore tests. Each guard clause mutation-killed. 259/259.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:27:46 -05:00
Joseph Doherty 3215d51154 docs(auth): record parked constraint-policy + JWT-helper decisions (scadaproj#20)
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 22:13:46 -05:00
Joseph Doherty 5c207d1fe5 docs(index): four apps pin Auth 0.4.1 on their default branches (scadaproj#5)
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 18:30:57 -05:00
Joseph Doherty 703461dab1 docs(secrets): OtOpcUa is Akka pair-local, never a gRPC-hub consumer; 0.6.3 has no hub consumer left (scadaproj#8)
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 18:20:30 -05:00
Joseph Doherty 4382446ebd docs(index): unpublished shared-lib versions after the 2026-11-09 issue waves; Theme 0.5.0 on all consumer default branches
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:53:34 -05:00
Joseph Doherty 3df31d0c7a feat(theme): opt-in ThemeShell ScrollNav rail scroll pane (ScadaBridge#61)
Upstreams ScadaBridge's local .rail-scroll pane. ScrollNav (default false)
wraps Nav in .rail-scroll and adds side-rail-scroll; layout.css (lg+ only)
fills the rail and scrolls just the pane so .rail-foot stays pinned.
Default markup/CSS unchanged for other consumers. Stays at 0.5.2, unpublished.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:52:57 -05:00
Joseph Doherty 6123d03f92 build(overview): bump ZB.MOM.WW.Theme 0.4.1 -> 0.5.0 (scadaproj#9)
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:51:24 -05:00
Joseph Doherty ebc493361e fix(secrets): audit malformed ${secret:} tokens, cap SecretName at 450, resolve local UNC shares (scadaproj#52)
Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:38:36 -05:00
Joseph Doherty 4a3c4e9b7e docs(secrets): record the P3 batch and #49 in findings, CHANGELOG 0.6.6 and the review index (scadaproj#45)
Secrets-004–009, 011, 012, 014–016, 018 and 019 marked Resolved with commit, fix and regression
evidence; Secrets open findings 13 -> 0; code-reviews/README.md regenerated (--check passes).
CHANGELOG 0.6.6 records the fixes, behaviour changes and the #49 follow-ups.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty fc81cbbd36 test(secrets-cli): CLI rig keeps the store outside the working directory (scadaproj#45)
The CLI host takes its working directory as the content root, and the rig put the store under it.
The old lexical store-path rule missed that on macOS only because the temp path is spelled
/var/... while the working directory reports /private/var/...; with Secrets-005's link resolution
the rule (correctly) refused it. The rig now runs from a sibling work/ directory.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty db441d9d99 docs(secrets): document the plain-IConfigurationRoot reload gap and OtOpcUa's redundant starter (scadaproj#49)
The reload gap for a plain IConfigurationRoot is documented rather than fixed: closing it would mean
inserting a provider into ConfigurationRoot's internal list. No family host is exposed (plain roots
are built with reloadOnChange: false; the rest pass a ConfigurationManager). clustered-secrets.md
notes that AddZbSecretsAkkaReplication now starts the actor itself, so OtOpcUa's
SecretReplicationStarter becomes redundant once it adopts 0.6.6.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty fac3c5355c fix(secrets-grpc): hub-call cancellation surfaces as cancellation, not a failed sweep (scadaproj#45)
Secrets-019: the hub channels are created with ThrowOperationCanceledOnCancellation = true, so a
call cancelled through its token throws OperationCanceledException. Defensively, the failover
reader rethrows RpcException(Cancelled)-with-a-cancelled-token as cancellation instead of dialing
every fallback with the same cancelled token, and the sweep treats it as shutdown, not a failure
recorded in ISecretsHubSyncStatus. A server-side Cancelled status (token not cancelled) still fails
over.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty 3aa7c728d5 fix(secrets-cli): bundle import validates every entry before writing (scadaproj#45)
Secrets-018: BundleService.ImportAsync now rehydrates every entry and re-wraps every foreign-KEK
row before the first write, so a malformed or tampered entry anywhere fails the import with the
store untouched ("Nothing was imported"). A store fault during the apply phase (no multi-row
transaction) now reports the row it stopped at and every row already written, which the
interactive flow prints, instead of a bare "Import failed".

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty 649d4a0616 fix(secrets-ui): components re-check their own policy; pin the real policy matrix (scadaproj#45)
Secrets-016: RevealButton (RevealPolicy), SecretEditor and ConfirmDeleteModal (ManagePolicy) now
evaluate their policy through IAuthorizationService at the top of the revealing/mutating handler
and fail closed (no auth state, no IAuthorizationService, or a failed policy), instead of relying
on the host to wrap them in AuthorizeView. New tests build the REAL AddSecretsAuthorization
policies and evaluate each role against each policy, so secrets-manager satisfying RevealPolicy
would now fail a test.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty a84c64e26f fix(secrets-ui): Reveal fails closed when its audit cannot be written (scadaproj#45)
Secrets-015: RevealButton now writes the secret.reveal Success audit before showing the value and
shows nothing ("audit record could not be written") if that write throws; the failure is logged as
a warning with actor and name, never the value. The cancellation token is captured up front, so a
component disposed mid-reveal can no longer turn the audit into a swallowed ObjectDisposedException.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty fe94b32d42 fix(secrets-ui): Add refuses an existing name; restoring a tombstone needs a second submit (scadaproj#45)
Secrets-014: SecretEditor in add mode reads the name first. A live row is refused ("already
exists. Use Rotate") with no write and no audit, so a pasted name can no longer replace a live
credential and reset its description/content type under a "secret.add" audit. A tombstoned name is
restored only when the operator submits the same name a second time after the warning.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty 849640f467 fix(secrets-akka): announce from the mailbox with a sender; chunk anti-entropy payloads (scadaproj#45)
Secrets-011: HandleAnnounceTick pipes the manifest read back to the actor and publishes the
announce from the mailbox with Self as sender. Under a genuinely async store it used to publish
from a pool continuation with NoSender, so every peer replied to deadLetters; a faulted manifest
read was dropped silently and is now logged.

Secrets-012: the exchange's push rows, the pull reply and the pull request are split into
messages of at most 64 KB (measured with the wire encoding), half Akka.Remote's default 128000-byte
frame, so a cold node converges against a large store. A single over-budget row is still sent
alone; the manifest announce is not split (documented in clustered-secrets.md).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty 5e2d3dd1bc fix(secrets): malformed ${secret:} references fail closed; README sample validates (scadaproj#45)
Secrets-009: SecretReferenceExpander.ExpandAsync throws FormatException when a value holds a
${secret: marker that is not a complete reference with a non-blank name (${secret:}, an
unterminated ${secret:name, a nested marker), instead of passing the literal text through as the
credential. ExpandConfigurationAsync names the offending key (never the value) and writes nothing.
The README wiring sample drops the relative "SqlitePath": "secrets.db" (which failed validation)
for the per-user default and shows an absolute example; a test validates the sample.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty 262e11feaf fix(secrets-cli): shipped appsettings.json no longer sets a relative SqlitePath (scadaproj#49)
The CLI's own appsettings.json set "SqlitePath": "secrets.db", which the store-path validator
rejects as relative, so the headless CLI failed at startup when run from its own directory unless
Secrets__SqlitePath overrode it. The key is removed: the per-user default
(<LocalApplicationData>/ZB.MOM.WW/secrets.db) applies, and an app's appsettings.json in the working
directory or the environment still override it. Regression test validates the shipped file.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty 64efb49996 fix(secrets): hub push sweep isolates each row (scadaproj#45)
Secrets-008: SqlServerSecretSyncService.SweepAsync now catches a per-row push failure, logs it
(EventId 4, name only) and continues, like the pull side. One row the hub refuses (a >450-char
name, an over-long explicit KEK id) no longer stops every later name from reaching the hub.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty 16fac72858 fix(secrets): resolver never re-caches across Invalidate; expired plaintext is evicted (scadaproj#45)
Secrets-006: DefaultSecretResolver keeps a per-key invalidation generation; Invalidate bumps it
before evicting, and a resolve that read the store under an older generation withdraws its insert,
so a rotation mid-resolve is not undone for another TTL. Expired entries are removed on lookup and
by a periodic TimeProvider sweep (every TTL, at least 1 s); a non-positive TTL caches nothing.
The resolver is now IDisposable (stops the sweep timer).

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty eff8c6a00e fix(secrets): store-path rules compare locations, not spellings (scadaproj#45)
Secrets-005: AddIfNotRooted now uses Path.IsPathFullyQualified, so Windows drive-relative
(C:secrets.db) and root-relative (\secrets.db) paths are rejected. AddIfUnderContentRoot refuses a
store if EITHER its spelled location (device prefix stripped, loopback admin share mapped to its
drive) OR its resolved location (symlinks/junctions followed, 8.3 names expanded on Windows) is
under the likewise-resolved content root. A store spelled through the content root stays refused
even if a link inside carries it elsewhere. UNC paths through other shares are compared as written.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty b64628de0b fix(secrets): malformed rows fail as SecretDecryptionException; one KEK snapshot per cipher op (scadaproj#45)
Secrets-004: AesGcmEnvelopeCipher.Decrypt now wraps the ArgumentException AesGcm throws for a
wrong-length nonce/tag/wrapped DEK as SecretDecryptionException (as Rewrap already did), so the
resolver's decryption-failed audit fires.

Secrets-007: new MasterKeySnapshot (key + KekId from one reading, zeroed on Dispose) and a
default-implemented IMasterKeyProvider.GetSnapshot(); MasterKeyProviderBase overrides it to
resolve the source once and derive the id from that same key. Encrypt/Decrypt/Rewrap take one
snapshot per KEK and dispose it, so a key file swapped mid-operation can no longer stamp a row
with one key's id and wrap it under another. Intermediate key buffers are zeroed. On-disk format
unchanged: a golden row written by the 0.6.5 cipher still decrypts.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00
Joseph Doherty b33f0f93c9 fix(auth): SQL Server key store refuses truncation and matches key ids exactly (Auth-008)
- Over-length key records (key_id > 64, prefix > 16, display name > 256) are
  refused with ArgumentException before connecting; an over-64 key-id lookup
  or mutation answers not-found without querying (SqlClient silently
  truncated the sized parameter, so a different id sharing the first 64
  characters matched).
- Every key_id predicate adds an exact VARBINARY comparison, so lookups are
  case- and trailing-space-exact like SQLite; the PK seek is unchanged and
  there is no schema change.
- SqliteApiKeyStoreImporter reports case-only key-id collisions and
  over-length rows in new init lists (KeyIdCollisions, KeysRejected) instead
  of counting them as skips or importing them truncated.
- Audit text truncation kept deliberately and documented.

Offline refused-port tests and live SQL Server 2022 tests fail pre-fix;
SqlServer suite 101/101 live. Auth findings now 0 open; CLAUDE.md records
the #46/#50 work; code-reviews/README.md regenerated.

(scadaproj#46)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:26:03 -05:00
Joseph Doherty c4366539ca fix(auth): ApiKeyVerifier reports store faults as StoreUnavailable (Auth-010)
A key-store lookup that threw (shared SQL Server unreachable, SQLite I/O
fault) escaped VerifyAsync despite the "only exception path is cancellation"
contract, surfacing as an unaudited 500 / gRPC Unknown. ApiKeyFailure gains
StoreUnavailable (appended; numeric values unchanged) and the lookup maps any
non-cancellation exception to it. Unit theory and an offline refused-port
SqlServerApiKeyStore test fail pre-fix.

(scadaproj#46)

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:21:00 -05:00