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_filescache 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-tagssuffix.inlineBuildKey- the singleflight key(namespace_id, npm_package_id, kind=1), keyed onnpm_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 achecksum.SHA256Sum.ifNoneMatchSatisfied- RFC 9110If-None-Matchmatching (*wildcard + comma-separated validator list).setDistTagsHeaders/cacheControlForVisibility- theETag+Vary: Authorization+ visibility-varyingCache-Controldirectives.
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/...- passgolangci-lint run ./internal/format/npm/...- 0 issuesgo vet -tags integration ./internal/format/npm/...- clean (integration build compiles)
Related to #122 (closed)