# A2 — Adopt the shared `ZB.MOM.WW.GalaxyRepository` library
> 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
1. ~~Verify the **gRPC-service authz parity** question~~ **DONE (2026-06-25):** wholesale swap is unsafe —
per-key browse-subtree filtering is baked into the service body. The service must keep an mxaccessgw
subtree-scoping hook (push the provider upstream, or wrap). See the ⚠️ block above.
2. Decide alarm-attributes: **upstream into the lib (`0.2.0`)** vs keep inline on top of shared interfaces.
3. If upstreaming: do that in `scadaproj/ZB.MOM.WW.GalaxyRepository` first, publish, bump the version.
4. `nuget.config` + `csproj` + DI/endpoint wiring; delete the superseded inline files; rebind namespaces
in dashboard/alarms/security consumers.
5. Delete duplicated tests; build zero-warning; run the suite; live-validate browse + alarm watch-list.
6. Propagate to the scadaproj umbrella index + HistorianGateway's `pending.md` A2 (mark adopted).
## 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.