feat(npm): add the resolution holder and middleware replay (part 2/4) (S09 Enforcement plan: 13/20)
Split into 4 stacked MRs to keep each part within the review size target (~800 reviewable LoC ideal). Each part targets the previous one (part 1 targets main); the stack is reviewed and merged bottom-up.
Part 2 of 4 of the S09 (authorization) enforcement plan — Step 13: npm wiring and route-table parity. Spec: S09 (authorization).
📦 What this part does
Adds npm's per-request mutable resolution holder — WithResolutionHolder, SetResolution, and the SeededResolutionFor reader — and teaches the format middleware to replay a seeded resolution instead of resolving the repository a second time.
While the wiring part (part 4) is unmerged, nothing installs a holder or seeds it, so every request takes the existing fresh-resolve path and this part changes no observable behavior. Once part 4 lands, the authz adapter seeds the holder on an authorized request and the middleware replays it, making the per-request resolver call count exactly one.
Naming note: the reader is SeededResolutionFor rather than maven's ResolutionFromContext because npm already uses that name for the immutable handler-facing pair; unifying the per-format reader names across formats is tracked in #392.
SetResolution enforces the Kind bindings Resolver.Resolve enforces — an out-of-range Kind, or a Remote/Virtual pointer whose nilness disagrees with it, panics at the boundary. A replay skips the resolve, so this is the only place left that can catch the partly filled Resolution a hand-written projection produces.
The branch is metered by gitlab_artifact_registry_npm_resolutions_total, labeled resolution_source over {replayed, fresh}: without it a wiring regression in part 4 would restore the double resolve silently, and no test can tell the branches apart. That is what carries this part outside internal/format/npm/ — a budget and closed-set row in internal/metrics/cardinality.go, and a row in docs/dev/observability.md.
🔬 e2e scenarios
No scenario is added or affected by this part alone — the seeded path is unreachable until the wiring part lands. The e2e catalog update (docs/testing/e2e/npm.md) rides part 4, where enforcement becomes observable.
📚 Stacked MRs (review/merge bottom-up)
- refactor(npm): move the npm mount predicate int... (!2131 - merged) • David Fernandez • 19.4
- feat(npm): add the resolution holder and middle... (!2132 - merged) • David Fernandez • 19.4
👈 - refactor(npm): drive the dispatcher mux from a ... (!2133 - merged) • David Fernandez • 19.4
- feat(npm): wire authz enforcement for the npm d... (!2134 - merged) • David Fernandez • 19.4
Related to #855 (closed)