# A2 — Adopt the shared `ZB.MOM.WW.GalaxyRepository` library
> **✅ ADOPTED 2026-06-25 (branch `feat/galaxyrepository-adoption`).** mxaccessgw now consumes
> `ZB.MOM.WW.GalaxyRepository` **0.2.0** and the inline Galaxy code is deleted (27 files, −2959 LOC).
> What shipped:
> - **Lib 0.2.0** (published to Gitea) closed the two real upstream gaps: **alarm-attribute discovery**
> (`GalaxyAlarmAttributeRow` + `IGalaxyRepository.GetAlarmAttributesAsync` + `AlarmAttributesSql`) and an
> injectable **`IGalaxyBrowseScopeProvider`** (default `NullGalaxyBrowseScopeProvider` = no scoping;
> HistorianGateway @ 0.1.0 unaffected) wired into the lib's gRPC service. Lib test suite: 64 green.
> - **mxaccessgw** registers `GatewayBrowseScopeProvider : IGalaxyBrowseScopeProvider` (reads the API key's
> `EffectiveConstraints.BrowseSubtrees`) before `AddZbGalaxyRepository(config,"MxGateway:Galaxy")`, maps
> `MapZbGalaxyRepository()`, and switched `GatewayGrpcScopeResolver` galaxy request types to the lib
> `.Grpc` namespace (the global authz interceptor now intercepts the lib service). The **dashboard summary
> stays host-side** (`DashboardGalaxySummaryProjector`, memoized by cache `Sequence`) since the lib entry
> doesn't carry it. Server build zero-warning; 327 targeted tests green.
> - The end-to-end host authz chain is covered (`GalaxyRepositoryHostWiringTests`), and the lib gained the
> ported browse-projector / deploy-notifier / refresh-service (Server-005 timeout guard) tests.
>
> **Caveats / follow-ups** (see "Post-adoption notes" at the bottom).
> Original handoff (now historical) follows.
> Handoff note. Written 2026-06-25 from the HistorianGateway side, where the shared lib is
> already consumed in production. This is the mxaccessgw half of the cross-repo
> "Galaxy-browse normalization" follow-on (HistorianGateway `pending.md` A2 /
> scadaproj component-normalization).
## Goal
Replace mxaccessgw's **inline** Galaxy-browse code (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/**`
plus `Grpc/GalaxyRepositoryGrpcService.cs` + `Grpc/GalaxyProtoMapper.cs`) with a
`PackageReference` to the shared **`ZB.MOM.WW.GalaxyRepository`** library, so there is **one**
Galaxy-browse implementation shared by both sidecars instead of two copies that can drift.
The wire contract is **unchanged** — both repos already serve the same `galaxy_repository.v1`
proto — so **no client change is required**. This is an internal refactor.
## Why this is safe to do now
The shared lib was **extracted from this very code**: the file names under
`src/ZB.MOM.WW.MxGateway.Server/Galaxy/**` map ~1:1 onto
`scadaproj/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/**`. HistorianGateway
consumes it (`PackageReference @ 0.1.0`) and runs it live against `wonder-sql-vd03`. So the SQL
provider, hierarchy cache, deploy notifier, snapshot store, background refresh service, projectors,
glob matcher, tag lookup, and the gRPC service are **already proven** in the package — the work here
is wiring + reconciling the mxaccessgw-only pieces, not reimplementing browse.
## The clean part — 1:1 swap
These inline files are superseded by the package (delete on adoption; rebind references to namespace
`ZB.MOM.WW.GalaxyRepository`):
| mxaccessgw inline (`…Server/Galaxy/`) | Shared lib equivalent |
|---|---|
| `GalaxyRepository.cs` / `IGalaxyRepository.cs` | same |
| `GalaxyHierarchyCache.cs` / `IGalaxyHierarchyCache.cs` / `GalaxyHierarchyCacheEntry.cs` | same |
| `GalaxyHierarchyRefreshService.cs` | same (registered as `HostedService` by the lib's DI ext) |
| `GalaxyDeployNotifier.cs` / `IGalaxyDeployNotifier.cs` / `GalaxyDeployEventInfo.cs` | same |
| `GalaxyHierarchySnapshot*.cs` / `IGalaxyHierarchySnapshotStore.cs` | same |
| `GalaxyHierarchyProjector.cs` / `GalaxyBrowseProjector.cs` / `GalaxyHierarchyIndex.cs` | same |
| `GalaxyObjectView.cs` / `GalaxyHierarchyRow.cs` / `GalaxyAttributeRow.cs` | same |
| `GalaxyBrowseChildrenResult.cs` / `GalaxyHierarchyQueryResult.cs` | same |
| `GalaxyTagLookup.cs` / `GalaxyGlobMatcher.cs` / `GalaxyCacheStatus.cs` | same |
| `GalaxyRepositoryOptions.cs` | `GalaxyRepositoryOptions` (lib ships **no validator** — see below) |
| `Grpc/GalaxyRepositoryGrpcService.cs` / `Grpc/GalaxyProtoMapper.cs` | lib bundles its own `Grpc/GalaxyRepositoryGrpcService.cs` + `GalaxyProtoMapper.cs`, mapped by `MapZbGalaxyRepository()` — **but verify authz parity first, see ⚠️ below** |
| `Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs` | replaced by the lib's `AddZbGalaxyRepository` / `MapZbGalaxyRepository` |
## ⚠️ The hard part — mxaccessgw-only consumers the shared lib does NOT cover
HistorianGateway was a greenfield consumer (browse + dashboard tree only). mxaccessgw has **extra
consumers of the Galaxy cache/hierarchy that the shared lib was not extracted with.** These are the
real work and the real risk:
1. **Alarm watch-list / alarm-attribute discovery — capability GAP in the lib.**
- mxaccessgw has `Galaxy/GalaxyAlarmAttributeRow.cs` and `GalaxyRepository.GetAlarmAttributesAsync()`,
consumed by `Alarms/AlarmWatchListResolver.cs` (+ `IAlarmWatchListResolver`, `GatewayAlarmMonitor`).
- The shared lib has **`GalaxyAttributeRow` + `GetAttributesAsync()` only** — there is **no**
`GalaxyAlarmAttributeRow` / `GetAlarmAttributesAsync` in `0.1.0`.
- **Decision needed:** either (a) push the alarm-attributes SQL projection **upstream** into the
shared lib (a `0.2.0` additive feature — preferred, keeps it shared) and re-consume, or
(b) keep `AlarmWatchListResolver` + the alarm-attributes query **inline** in mxaccessgw, layered
**on top of** the shared `IGalaxyRepository`/`IGalaxyHierarchyCache`. (a) is the spirit of A2.
2. **Dashboard consumers** — `Dashboard/DashboardGalaxyProjector.cs`, `DashboardBrowseService.cs`
(+ `IDashboardBrowseService`, `DashboardBrowseModel`), `DashboardSnapshotService.cs`,
`DashboardGalaxySummary.cs`, and the Razor pages `GalaxyPage.razor` / `DashboardHome.razor` all
`using ZB.MOM.WW.MxGateway.Server.Galaxy`. Mechanical rebind to `ZB.MOM.WW.GalaxyRepository`, but
confirm every type they touch is `public` in the lib (most are; double-check projector/snapshot
types).
3. **Security / authorization** — `Security/Authorization/ConstraintEnforcer.cs` and
`GatewayGrpcScopeResolver.cs` consume Galaxy hierarchy to enforce per-key constraints. Rebind, and
see the gRPC-service question below.
⚠️ **gRPC-service authz parity — VERIFIED 2026-06-25: NOT safe to swap wholesale.** The authorization
is **split across two layers**, and the part that matters is **baked into the service body**:
1. **Authentication + scope gating IS interceptor-based ✅ (safe).** `Security/Authorization/GatewayGrpcAuthorizationInterceptor.cs`
authenticates the API key and enforces the required scope; all five Galaxy RPCs map to the `metadata`
scope (`GatewayGrpcScopeResolver.cs:23-27` → `MetadataRead`). The interceptor also pushes the identity
into an ambient `IGatewayRequestIdentityAccessor` for the call. This mirrors HistorianGateway and is
shared-lib compatible.
2. **Per-key browse-subtree CONSTRAINT FILTERING is baked into the service body ❌ (the blocker).**
mxaccessgw's `Grpc/GalaxyRepositoryGrpcService.cs` injects `IGatewayRequestIdentityAccessor` and
`ResolveBrowseSubtrees()` (`:287-291`) reads `identityAccessor.Current.EffectiveConstraints.BrowseSubtrees`,
threading those globs into the projectors for `DiscoverHierarchy` (`:78`), `BrowseChildren` (`:138`),
and the `WatchDeployEvents` count scoping (`:180,196,230-237`) — i.e. it restricts **which Galaxy
objects each API key can see**. The shared lib's service injects no identity accessor and passes
`browseSubtreeGlobs: null` everywhere; its own XML doc states it *"applies no per-identity
browse-subtree filtering … Authorization (including any subtree scoping) is the responsibility of the
hosting gateway's interceptor layer."*
**Severity — silent per-key data exposure.** Glob semantics (`GalaxyHierarchyProjector.cs:225-227`):
`null`/empty → match everything; non-empty → restrict. So keys with **no** browse constraint behave
identically in both services (no regression), but a key **with** a `BrowseSubtrees` constraint would, on
the shared service, receive the **entire** Galaxy hierarchy — a `metadata`-scoped key silently bypassing
its restriction. The boundary is locked by tests that would fail against the shared service:
`Tests/Gateway/Grpc/GalaxyRepositoryGrpcServiceTests.cs` → `BrowseChildren_BrowseSubtreesConstraint_FiltersChildren`
(`:437`) and `DiscoverHierarchy_WithSubtreeRootAndDepth_FiltersDescendants` (`:90`).
**Recommendation.** Do **not** call `MapZbGalaxyRepository()` directly. The shared projectors already
accept a `browseSubtreeGlobs` param, so either fix is small plumbing:
- **Preferred (spirit of A2 — push upstream):** give the shared `GalaxyRepositoryGrpcService` an optional
browse-subtree-constraint provider (e.g. `IBrowseSubtreeConstraintProvider`/delegate returning globs —
`null` in HistorianGateway, identity-derived in mxaccessgw). One shared service, both behaviors; bundle
with the same `0.2.0` bump as the alarm-attributes gap above.
- **Fallback (isolate to mxaccessgw):** keep a thin mxaccessgw service that resolves subtrees from
identity and calls the shared (public) projectors, and map **that** instead of `MapZbGalaxyRepository()`.
## Concrete wiring steps (template from the HistorianGateway side)
1. **`nuget.config`** — mxaccessgw's `packageSourceMapping` for the `dohertj2-gitea` source currently
lists `ZB.MOM.WW.*` up to `Theme` but **NOT** GalaxyRepository. Add:
```xml
```
under the `dohertj2-gitea` ``. (Easy to miss — restore will fail without it.)
2. **Server `.csproj`** — add:
```xml
```
(or whatever version ships the alarm-attributes add, if you go route 1(a) above.)
3. **DI + endpoint wiring** (in mxaccessgw's host composition — `GatewayApplication.cs` / its
`Program`), mirroring HistorianGateway's `Program.cs`:
```csharp
using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.GalaxyRepository.DependencyInjection;
// service registration — bind from mxaccessgw's section path (NOT top-level "Galaxy"):
builder.Services.AddZbGalaxyRepository(builder.Configuration, "MxGateway:Galaxy"); // confirm the exact section path mxaccessgw uses today
// endpoint pipeline (after AddGrpc):
app.MapZbGalaxyRepository();
```
Delete mxaccessgw's own `Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs` registrations.
4. **Option validation** — the shared lib **binds only, ships no validator** (deliberate). mxaccessgw
already validates Galaxy options via `Configuration/GatewayOptionsValidator.cs` — **keep that**; it
stays the owner of fail-fast validation, exactly as HistorianGateway's `ConfigPreflight` does.
5. **Health check** — keep mxaccessgw's existing Galaxy-SQL readiness check; read the connection
string from the same `MxGateway:Galaxy` section the lib binds (HistorianGateway does this with a raw
`SELECT 1` `SqlConnectionHealthCheck` — see `Program.cs:196`).
## Tests
mxaccessgw has a large `Tests/Galaxy/**` suite that **duplicates the shared lib's own tests**
(`GalaxyHierarchyCacheTests`, `GalaxyHierarchyProjectorTests`, `GalaxyHierarchySnapshotStoreTests`,
`GalaxyProtoMapperTests`, `GalaxyHierarchyIndexTests`). On adoption these are covered upstream and can
be **deleted**. **Keep** the mxaccessgw-specific ones that exercise behavior the lib doesn't own:
`GalaxyAlarmAttributeMappingTests`, `GalaxyFilterInputSafetyTests`, `GalaxyRepositoryGrpcServiceTests`
(unless their subjects move upstream too), and the live `IntegrationTests/Galaxy/**`.
## Suggested order — all DONE (2026-06-25)
1. ~~Verify the **gRPC-service authz parity** question~~ **DONE:** wholesale swap was unsafe — per-key
browse-subtree filtering was baked into the service body. Resolved by pushing an injectable
`IGalaxyBrowseScopeProvider` hook **upstream** into the lib (option preferred in the ⚠️ block).
2. ~~Decide alarm-attributes~~ **DONE:** upstreamed into the lib as part of `0.2.0`.
3. ~~Upstream + publish + bump~~ **DONE:** lib `0.1.0 → 0.2.0`, packed and pushed to the Gitea feed (verified live).
4. ~~`nuget.config` + `csproj` + DI/endpoint wiring; delete inline; rebind~~ **DONE.**
5. ~~Delete duplicated tests; build zero-warning; run the suite~~ **DONE** (lib 64 green; gateway 327 targeted green).
Live-validate browse + alarm watch-list is the one remaining **manual** step (needs Galaxy SQL + a running
gateway — opt-in `MXGATEWAY_RUN_LIVE_GALAXY_TESTS=1`); not runnable from the dev Mac.
6. **Remaining:** propagate to the scadaproj umbrella index + HistorianGateway's `pending.md` §A2 (mark adopted)
— cross-repo, do in those repos.
## Post-adoption notes / caveats
- **Deployment config (NSSM):** the deployed services (`MxAccessGw` on 10.100.0.48; the wonder host) read
config from **NSSM environment variables, not `appsettings.json`**. The lib's `SnapshotCachePath` default
is empty (persistence no-ops). `appsettings.json` sets `MxGateway:Galaxy:SnapshotCachePath` +
`PersistSnapshot`, but the deployments must carry `MxGateway__Galaxy__SnapshotCachePath` and
`MxGateway__Galaxy__PersistSnapshot` in their NSSM env on redeploy, or snapshot persistence silently
no-ops in production.
- **Pre-existing NU1903 (unrelated) — ✅ RESOLVED (2026-07-18, commit `2f0cfe3`):** adding the package surfaced a transitive `SQLitePCLRaw.lib.e_sqlite3`
2.1.11 advisory (GHSA-2m69-gcr7-jv3q, at the time no upstream patch) that breaks the build under `TreatWarningsAsErrors`
— already red on `main`. Initially resolved with a targeted `NuGetAuditSuppress` in `src/Directory.Build.props`
(its own commit). The patched e_sqlite3 (2.1.12) has since shipped: the suppression was **removed** and the
patched native lib pinned intentionally (`src/Directory.Build.props` now documents this in place of the suppression).
- **Pre-existing IntegrationTests break (unrelated, NOT fixed here) — ✅ RESOLVED since:** `IntegrationTests/WorkerLiveMxAccessSmokeTests.cs`
constructed `EventStreamService` with 6 ctor args, but a prior event-stream refactor reduced that ctor — so
the IntegrationTests project did not compile (already broken on `main`, independent of Galaxy). The call site
has since been fixed to match the 3-arg ctor (`sessionManager, options, metrics` —
`Grpc/EventStreamService.cs:11-14`; call site at `WorkerLiveMxAccessSmokeTests.cs:~1580`) and the project compiles
(verified 2026-08-07).
- **No republish needed for the lib test additions:** the browse-projector / deploy-notifier / refresh-service
tests were added to the lib AFTER 0.2.0 was published; tests aren't shipped, so 0.2.0 is unchanged.
## Reference pointers
- **Working consumer (copy this):** `~/Desktop/HistorianGateway/src/ZB.MOM.WW.HistorianGateway.Server/Program.cs`
(`AddZbGalaxyRepository` @ ~line 174, `MapZbGalaxyRepository` @ ~line 314) + its `nuget.config` + Server `.csproj`.
- **Shared lib source:** `~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/` (DI ext at
`src/ZB.MOM.WW.GalaxyRepository/DependencyInjection/GalaxyRepositoryServiceCollectionExtensions.cs`).
- **Tracked in:** HistorianGateway `pending.md` §A2 + `CLAUDE.md` → Known Follow-Ons; scadaproj component normalization.