feat(npm): package_json schema validator (S11 Step 5)

📦 What

Step 5 of the npm local plan, sliced into four stacked ~500-LOC MRs. Slice 1/4 (base) — the package_json schema validator.

New internal/datastore/schemas/npm_package_json.goValidateNpmPackageJSON: a //go:embed-ed strict JSON Schema (additionalProperties: false over the spec's allow-list, plus required: ["name", "version"] and "type": "string" on name/version/dist.tarball) compiled once at init, with a 20 KB (20000-byte, SI) ceiling. This is the authoritative package_json gate per ADR 007 (app-layer validation, since CloudSQL disallows the pg_jsonschema DB CHECK the original Step 2 draft used). The version-write slice (3/4) calls this before every npm_versions.package_json write.

Testing

internal/datastore/schemas/npm_package_json_test.go (unit): allow-listed accept; denylist (scripts / preinstall / readme), unknown-field, non-string name/version/dist.tarball, missing required name/version, and over-20 KB reject; size boundary.

Size: ~462 LOC, ~64% test.

🔍 Reviewer note

The validator rejects an empty / JSON-null package_json (Reading A), which diverges from the merged OCI annotations precedent (returns nil for empty). Chosen for consistency with the NOT NULL column and the "authoritative gate" framing; the unit test documents it as PROVISIONAL pending a spec-author nod.

Reviewer follow-ups (addressed in-thread): name/version/dist.tarball are now type-constrained to strings — the schema previously allow-listed field names but enforced no type, so {"name":{...},"version":[]} validated. And the size ceiling was reconciled from 20480 to 20000 to match npm.max_package_json_size (SI "20KB") and the Step 16 handler filter. The remaining AppSec value-shape items — bin/man/directories path-traversal and dist.tarball as format: uri — stay a spec-author follow-up, as they change the allow-list contract beyond a type tightening.

🔗 Stack

Step 5 was split into four stacked MRs (each targets the previous; GitLab auto-retargets to main as they merge), rebased onto the reorganized Step 4b:

  1. !499 (merged)package_json schema validator (base → Step 4b)
  2. !503 (merged) — package & file writes + shared test harness
  3. !504 (merged) — version writes
  4. !505 (merged) — dist-tag writes

The metadata-cache writes that originally shared slice 4 were carved out: the Step 4b reorg removed the npm_metadata_files read layer they build on (deferred to a later step), so they follow it there.

Related to #122 (closed)

Edited by Dzmitry (Dima) Meshcharakou

Merge request reports

Loading
Loading