Loading
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): oneactiveParentPredicatespelling 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 namedparentSpecso 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.orgupstream in any normalized spelling is created pinned (cache_validity_hours = 0, npmjs.org forbids republishing); otherwise the column is written as the SQLDEFAULTkeyword so the schemaDEFAULTstands (no Go-side copy of the default). Optionalmetadata_cache_validity_hourspassthrough. - 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 viaremote.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