feat(npm): dist-tags handler primitives (S11 Step 10, MR 1/5)

📦 What

S11 Step 10 (dist-tags GET endpoint) is a five-MR stack, in dependency order (each targets the one above; GitLab auto-retargets to main as they merge):

  • !622 (merged) - dist-tags handler primitives + in-package unit tests. <- this MR
  • !623 (merged) - inline-build handler (resolve -> 404 -> build {tag:version} -> serve).
  • !624 (merged) - kind=1 npm_metadata_files cache fast-path + conditional-GET 304-without-blob-read.
  • !625 (merged) - inline-build singleflight (collapse concurrent cache misses).
  • !626 (merged) - integration suite (real datastore finders + stub BlobStore).

This MR targets main - the base of the stack.

Slice 1 - the unexported primitives the handler is built from. The helpers are package-private, so they are covered by in-package (package npm) unit tests; the handler-level behaviour lands with the handler in the next slices' external npm_test suite.

  • distTagsPackageName - extract {package_name} from the request path between the /-/package/ marker and the /dist-tags suffix.
  • inlineBuildKey - the singleflight key (namespace_id, npm_package_id, kind=1), keyed on npm_package_id (not the name) so two same-named packages in different repositories of one namespace cannot collapse onto one builder.
  • strongETag - format the strong "sha256-<hex>" validator from a checksum.SHA256Sum.
  • ifNoneMatchSatisfied - RFC 9110 If-None-Match matching (* wildcard + comma-separated validator list).
  • setDistTagsHeaders / cacheControlForVisibility - the ETag + Vary: Authorization + visibility-varying Cache-Control directives.

Rebased onto current main: the blob_sha256->SHA256Sum conversion reuses download.go's sha256SumFromBytes (landed on main with Step 9), so this slice no longer declares its own. The later slices likewise reuse Step 9's package_not_found / package_name_invalid transport codes and the existing NpmPackageFinder / tagLister / NpmVersionFinder / RepositoryVisibilityFinder finder interfaces.

Spec coverage

Behaviour Test
Strong "sha256-<hex>" ETag format (no weak W/) TestStrongETag
If-None-Match matching: wildcard, comma list, surrounding whitespace TestIfNoneMatchSatisfied
Cache-Control varies by visibility (public vs private/internal) TestCacheControlForVisibility, TestSetDistTagsHeaders
{package_name} extraction (unscoped, scoped, missing marker) TestDistTagsPackageName
Singleflight key on npm_package_id (no cross-repo / cross-namespace collision) TestInlineBuildKey

⚠️ Known gap - handler not yet wired into the route table

No handler ships in this slice (primitives only). The dist-tags handler lands in feat(npm): dist-tags inline-build handler (S11 ... (!623 - merged) and, like the rest of S11's handlers, is not wired into the route table until Step 23: internal/format/npm/handler.go serves the 501 Not Implemented placeholder for the dist-tags route. This is an unassigned plan-level gap (the npm hosted plan creates handler.go with placeholders in Step 8 and never assigns the placeholder->real-handler swap to a handler step); it surfaces no later than Step 23 (real npm-CLI conformance) and is closed via a plan amendment, not bolted onto this stack. Deliberately out of scope.

🧪 Testing

  • go test ./internal/format/npm/... - pass
  • golangci-lint run ./internal/format/npm/... - 0 issues
  • go vet -tags integration ./internal/format/npm/... - clean (integration build compiles)

Related to #122 (closed)

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading