feat(npm): slug/repo resolver, request context, error envelope (S11 Step 6)
What
Step 6 of the npm-local plan: the cross-cutting helpers every npm endpoint shares. No per-endpoint handlers yet (packument, tarball, publish, dist-tags land in later steps).
- Slug/repo resolver (
middleware.go) - walks{slug}→namespace_id→{repo}→repository_id, composed from two small finder interfaces (NamespaceFinder,RepositoryFinder) injected at the wire boundary in Step 8. The npm analogue of OCI'sinternal/namespaceresolver: same two-sentinel pattern (ErrNamespaceNotFound/ErrRepositoryNotFound), nil-finder panic-at-construction,%wwrapping, and short-circuit on a namespace miss. - Request middleware (
middleware.go) - resolves the URL prefix, stores the(namespace_id, slug, repository_id)triple in the request context for downstream handlers, and on a miss writes the tier-accurate 404 envelope without callingnext. No auth concern in this step: S08's central auth middleware runs before npm code. - S01 error-envelope transport (
transport.go) -WriteJSON/WriteError/Msgover the S01 standard envelope, mirroringinternal/transport. TheCodeandSafeMessagenamed types make "caller used a canonical code" and "no internal detail in the body" compile-time gates.
The npm repository row is resolved by (namespace_id, {repo}, format=npm, kind=local), excluding soft-deleted rows.
Spec / plan
- Spec: S11 npm-local - API Contracts, Error Cases (AC 32 / AC 33), Security Considerations
- Plan: 2026-05-11 npm-local, Step 6
The private-repo collapse to namespace_not_found for anonymous reads (AC 33) is deferred by design to the S08/S09 auth layer, which runs before npm code; the resolver carries no visibility signal and must not synthesize one.
Tests
transport_test.go(unit) - envelope shape, status/code table, missing-correlation-ID, content-type, code constants.middleware_test.go(unit) - resolver happy / namespace-miss / repo-miss / underlying-error-at-each-tier, context round-trip, middleware HTTP behavior (success + both 404s + no-leak 500).middleware_integration_test.go(//go:build integration) - drives the resolver and middleware against the realFindBySlugquery and the realrepositoriesresolution: happy path, unknown slug, missing repo, wrong format (container), wrong kind (virtual), soft-deleted, and cross-namespace isolation.
CI: internal/format/npm/... added to the test:integration package set and to rules:changes.
Review
Reviewed via /review-branch: APPROVE - 0 blocking, 1 warning, 7 observations. The single warning is MR size (see below); the observations are process notes or deferred-by-design. Mechanical gates pass: build, vet (incl. -tags integration), lint, goimports, -race unit tests. The review-flagged American-English spelling nits are fixed in this branch.
One forward-looking note for Step 8: these tests validate the resolver against a test-only SQL adapter. When the production RepositoryFinder adapter lands, it must be driven through the same format/kind/soft-delete/cross-namespace matrix so a dropped predicate can't slip through.
Reviewable size
The raw diff is ~1,520 lines, but ~75% (1,140 lines) is the test floor: under the project's test-first authorship contract the tests land with their implementation as one logical unit and can't be split out. Production surface is 378 lines (middleware.go 255, transport.go 123); CI is 2.