feat(npm): remote-repository create with cache-validity pin (S15 Step 7, part 2/3)

📚 Stacked MRs

This change is split into 3 stacked MRs to keep each within the review size limit (≈800 reviewable LoC). Each part targets the previous one (part 1 targets main); review and merge proceed bottom-up.

📦 What this MR does

Part 2 of 3 of npm remote plan — Step 7: repo service layer — credentials + cache-validity:

Adds CreateNpmRemoteRepository, the tx-accepting insert of the npm remote binding (npm_remote_repositories), composable inside the management layer's future create transaction:

  • Parent gate: the composite FK proves existence only, so the create path verifies the parent is an active npm-format remote repository — without it a binding (and its plaintext token) could land under a parent every read path reports as ErrNotFound. The check is a shared cross-store primitive (internal/datastore/repository_parent_gate.go): one activeParentPredicate spelling of "active repositories row of this format and kind", consumed both as a composable predicate by the npm remote reads and as the write-path existence check, parameterized by a named parentSpec so the format and kind cannot be transposed. Maven remote reuses it rather than forking a second gate.
  • Cache-validity resolution at create time: an explicit operator value wins; a registry.npmjs.org upstream in any normalized spelling is created pinned (cache_validity_hours = 0, npmjs.org forbids republishing); otherwise the column is written as the SQL DEFAULT keyword so the schema DEFAULT stands (no Go-side copy of the default). Optional metadata_cache_validity_hours passthrough.
  • Token invariants: the S13 plaintext cap (remote.MaxCredentialLength, rune-counted) enforced before persistence; a non-nil empty token rejected. URLs are persisted in normalized form via remote.ParseUpstreamBase.
  • Error classification without leakage: the repository FK, the duplicate-binding unique violation (matched by bracketing the partition leaf-index name), and CHECK refusals map to bare sentinels, so PgError.Detail — which echoes the whole failing row, token included — never travels the error chain.

Spec coverage

Requirement Tests
S15 Tarball proxy AC 4, create clause — a remote created for registry.npmjs.org defaults to the pin TestNpmRemoteCacheValidityHours (unit), TestNpmRemoteRepositoryStore_CreateNpmRemoteRepository (integration: pin across normalized spellings, explicit-wins, DEFAULT)
S13 plaintext cap + token invariants TestValidateNpmRemoteAuthToken (unit, incl. rune-count boundary), over-cap integration case
Parent gate + error classification TestParentRepositoryIsActive (integration: positive hits across formats and kinds, wrong-format/wrong-kind/soft-deleted/cross-namespace/absent refusals); create-path integration cases: maven/hosted/virtual/soft-deleted/missing parents, duplicate binding, CHECK refusal with token-leak assertion, rollback atomicity

🔍 Notes for reviewers

No e2e scenario catalog update: this is the datastore service layer beneath the not-yet-shipped management CRUD; no HTTP surface changes.

Related to #287

:bricks: Stacked MRs (review/merge bottom-up)

Edited by David Fernandez

Merge request reports

Loading
Loading