feat(npm): streampub top-level field handlers (part 3/8)

📦 What this MR does

Part 3/8 of the stack splitting the streampub streaming publish-envelope library (S11 Step 13). See Part 1/8 (!432) for the stack overview and the byte-identity contract against the reference, !420 (closed).

  • Targets 10io/npm-local-step-13-part2of8 (Part 2/8). GitLab retargets to main as the parent merges.
  • Depends on Part 2/8 (set as a merge-request dependency).

This part adds the top-level envelope field handlers and routes the walk to them. The five envelope memory bounds and the streaming attachment pipeline still arrive in later parts.

  • walker.godispatchTopLevel + the name / versions / dist-tags key constants; run now dispatches per key. readName (npm name-rule validation) and readDistTags (captures the tag→version map, no count bound yet). _attachments still falls through to skipValue until the attachment part.
  • name.go — the npm package-name rules (scoped/unscoped segments, reserved names, encodeURIComponent-safe charset).
  • versions.goreadVersions (exactly one entry; aborts on the second key before reading its value, S11 AC 47) and readVersionValue (captures the per-version object as normalized JSON via a jsontext.Encoder). The MaxEnvelopePackageJSONSize byte-span bound is added with the other bounds in a later part.
  • token.go — restores readStringValue (now called by readName/readDistTags) and adds abortErr (used by the versions second-key abort; the bounds reuse it later).
  • error.go — adds errPackageName + CodePackageNameInvalid, referenced for the first time here.

🧪 Tests

  • TestProcess_PackageNameInvalid (S11 AC 39, streaming-unit half): uppercase, leading dot/underscore, reserved name, non-URL-safe char, and over-214-byte names all surface package_name_invalid.
  • TestProcess_CapturesVersionPackageJSON: the per-version object is captured as normalized JSON.
  • Plus the shared envelope/tarball builders these need (buildEnvelope, versionObject, buildTarball, wellFormedTarball).

Both exercise behavior the current code implements fully (name validation and version capture run before the not-yet-present attachment/shape stages), so no throwaway tests.

  • golangci-lint (repo-pinned v2.12) → 0 issues
  • GOEXPERIMENT=jsonv2 go test -race -count=1 ./internal/format/npm/streampub/... → pass

🚩 Deviations from the reference (!420 (closed))

Carries forward the Part 1/8 error.go API improvements (type ProcessError, unexported code / wrapped) and the Part 2/8 six-cap validate. Every identifier uses *ProcessError, and abortErr returns *ProcessError.

  • Test label fix: the TestProcess_PackageNameInvalid case for "weird(name)" is relabelled from "non-url-safe paren" to "forbidden rune paren" — parentheses are URL-safe per encodeURIComponent and are rejected by the forbiddenNameRunes check, not urlComponentSafe, so the original label was misleading. Test behavior is unchanged.

  • Added test coverage (a162eec, d566ad9): TestProcess_VersionsShapeInvalid covers four versions-object shape failures, each asserting publish_envelope_invalid (the two abort cases also asserting AbortOffset > 0):

    • empty-versions (versions.go:22) and non-object-version (versions.go:58) — error returns that exist but are untested in the reference (400a21f's TestProcess_EnvelopeShapeInvalid covers missing name/versions/attachments and an attachment-key mismatch, but neither of these two).
    • the single-version-rule abort (AC 47): a two-version-entry case (the second-key abort; also drives the previously-unused extraVersions builder) and a duplicate-version-key case that pins the classification guarantee — the streaming abort fires before the decoder's duplicate-name check, so dup keys surface as publish_envelope_invalid, not bad_request. The reference's TestProcess_TwoVersionsAbortsEarly (part 4) covers the distinct-key size-abort but not the duplicate-key classification.

    Surfaced in review of this part; test-only, no production change.

Edited by David Fernandez

Merge request reports

Loading
Loading