docs(specs): add S15 npm remote spec and fix S13 cache-freshness model

📦 What

Adds docs/specs/S15-npm-remote.md and links it from the spec index with status Approved.

An npm remote repository is a faithful caching proxy of a single external npm registry (for example registry.npmjs.org): it relays what the upstream serves, applies exactly one transform (rewriting dist.tarball URLs back to this repository), and caches per client demand. The client surface is read-only: publish, dist-tag mutation, and unpublish return 405.

Scoped to the remote kind only, per the remote/virtual split in !900 (merged). It extracts the remote parts of the pre-split combined draft !862 (merged); the npm virtual slice remains S31.

🔧 Foundation corrections (S13)

This MR makes two S13 corrections plus one S13 interface addition, all confirmed with the S13 DRI.

Cache-freshness model. S13 mislabeled cache_validity_hours as a metadata/mutable-pointer threshold and declared npm tarballs never-revalidated. Realigned to the reference implementation's content-file-versus-metadata-file split:

  • cache_validity_hours → content files (npm tarballs, Maven artifacts) and container tags; 0 = never revalidate, the opt-out for an immutable upstream such as npmjs.org / Maven Central.
  • metadata_cache_validity_hours → metadata files (maven-metadata.xml; npm packuments and dist-tags); validated > 0, so metadata always refreshes and newly published versions surface.
  • Only digest-keyed entities (container blobs and manifests) remain never-revalidated. No ADR change (ADR-008 blob immutability is preserved — only the (version, filename) -> blob cache row is revalidated and repointed).

Upstream content trust. S13 previously delegated "format-specific content validation (manifest schema, Maven checksums, npm tarball integrity)" to the remote slices. Reframed: S13 relays upstream bytes faithfully and performs no format-specific content validation. The one carve-out is digest-addressed content (OCI blobs and manifests), verified against its requested digest as an intrinsic property of content-addressable storage (ADR-008, owned by S16) — that is storing-by-digest, not format validation.

Fetch interface (transform seam + per-request cap). S13's Fetch gains an optional streaming body transform (applied inside the tee, format-agnostic) and a per-request max_body_bytes override of the per-type body-size cap. Single-flight follower behavior is generalized: a raw fetch tees followers from the leader's in-flight body, a transformed fetch serves followers from the leader's committed document. S15's dist.tarball rewrite is now a consumer of the seam, not S15-owned machinery, so the transform, tee, commit-on-complete, and single-flight all stay in S13.

🎯 Design — caching proxy

  • Metadata (packument, dist-tags): stream + transform. As the upstream body streams, rewrite every dist.tarball URL and cache the upstream's own document per variant (kind=0 full, kind=2 abbreviated, kind=1 dist-tags), lazily — a variant is fetched only when a client requests it. No parse into per-version rows, no reconstruction, no deriving one variant from another. The served document is a faithful relay plus one URL rewrite.
  • Tarballs: S13's teed Fetch. Stream to the client while the cache-fill commits the content-addressed blob on complete receipt. No server-side checksum gate — the client enforces tarball integrity against the dist.shasum/dist.integrity the packument relays unchanged.
  • Tag installs (pkg@tag) resolve client-side from the packument's dist-tags field; the registry stores no tags and never resolves a tag during install.
  • Freshness: revalidate with If-None-Match; 304 bumps the timestamp, a changed ETag is treated as a cold fill (new blob, repoint the row, dereference the old — reclaimed by GC). cache_validity_hours = 0 pins npmjs.org; metadata_cache_validity_hours > 0 keeps metadata always-revalidating.
  • Config: max_remote_packument_size (64 MB) added to S11's existing NpmConfig at proto field 12, kept as a cached-document-size / streaming DoS guard (sized above vite's ~37 MB full packument). Passed per request as S13 Fetch's max_body_bytes to supersede the 16 MB metadata cap. Reuses S13's virtual_repositories and S11's public_registry_url. No S27 dependency.
  • Removed relative to the pre-split draft: server-side tarball integrity verification (and its 502 upstream_content_invalid), packument reconstruction / per-version parsing, the skipped-version and dangling-tag machinery, and their metrics — see the schema reasoning below.

🧱 Schema delta and reasoning

The remote's tables were originally modeled on the hosted npm schema (S11). That was the category error: hosted is a producer that assembles a packument from many per-version fragments; a remote is a cache of a producer that receives the already-assembled document. Modeling the cache like the producer forced it to replicate assembly it never performs. The delta realigns the schema to a proxy:

  • Drop npm_remote_versions.package_json. Hosted stores per-version package.json fragments because it assembles the packument from them (they are the source of truth, read by the packument rebuild job). A remote inherits the finished document from the upstream and never assembles, so nothing reads the column once reconstruction is removed; keeping it would also re-import the per-version filter/validate/skip adjudication a proxy shouldn't do.
  • Drop the npm_remote_tags table. dist-tags live inside the packument's own dist-tags field (relayed unchanged), so tag listing and pkg@tag installs resolve from the cached document — client-side, or by proxying the dist-tags endpoint. The table mirrored data the document already carries and was the source of the dangling-latest coherence problem. The reference implementation stores no npm tags.
  • Keep npm_remote_versions as a slim anchor (drop only the column above). Retained for the packages → versions → files hierarchy (parity with hosted and Maven remote, whose versions fan out into multiple files) and for version-level lifecycle/retention. Populated lazily on tarball pull, so it holds only versions actually cached — no whole-history bloat.
  • npm_remote_metadata_files becomes the sole metadata store: an opaque transformed-document blob per (package, kind), populated lazily per variant. Dropped the "all three kinds rebuilt in one transaction / cannot drift" coupling — each variant is now cached independently on its own freshness clock.
  • npm_remote_files unchanged — still keyed by and FK'd to the slim npm_remote_versions anchor.

Net effect: the remote's stored shape tracks actual client demand (one document per variant touched, one blob per file pulled), the property that distinguishes a cache from a mirror.

  • S11 npm hosted ( Implemented) — format definitions, validation, NpmConfig (extended with the new field).
  • S13 virtual and remote foundation (🚧 Approved spec; implementation in progress) — Lookup/Probe/Fetch, cache freshness, health monitoring, credential management, the upstream HTTP client, and single-flight; tarballs use S13's teed Fetch, metadata a streaming dist.tarball transform over the same client. The concrete Go interface and stub are agreed with the S13 DRI before the first S15 implementation MR, per the development-model conciliation process.
  • S06 storage ( Implemented), S17 management API ( Approved), S08/S09 auth (stub).

📄 Notes

  • Status: Approved in the spec file — merge is the approval signal, per the spec-MR guardrail.
  • Reference finding. The GitLab Workhorse experiment's streaming, constant-memory rewrite of the packument's tarball URL prefix is now the primary metadata mechanism (not a deferred optimization): S15 streams and rewrites rather than buffering, parsing, and reconstructing.
  • AppSec review for the npm remote/virtual format is tracked in #244.
  • Deferred follow-ups: packument-first tarball revalidation (skip the tarball re-download when the strongest declared hash is unchanged); credential encryption at rest (https://gitlab.com/gitlab-org/ops/artifact-registry/-/work_items/68, GA-blocking); non-standard upstream tarball hosting (#266).

🔖 Tracking

  • Spec issue (this MR): #282
  • Implementation counterpart: #287

Both under the remote-repositories epic gitlab-org&22810.

Edited by David Fernandez

Merge request reports

Loading
Loading