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's internal/namespace resolver: same two-sentinel pattern (ErrNamespaceNotFound / ErrRepositoryNotFound), nil-finder panic-at-construction, %w wrapping, 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 calling next. No auth concern in this step: S08's central auth middleware runs before npm code.
  • S01 error-envelope transport (transport.go) - WriteJSON / WriteError / Msg over the S01 standard envelope, mirroring internal/transport. The Code and SafeMessage named 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

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 real FindBySlug query and the real repositories resolution: 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.

Merge request reports

Loading
Loading