mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-02 11:55:17 -04:00
Follow-up to #11691 — item 2 from #11687, plus a related shortcut. ## What When the `minimumReleaseAge` lockfile verification gate needs to know when a version was published, it used to fetch a multi-MB full metadata document per package just to read one timestamp. This PR replaces that single-step path with a four-layer lookup that pays the cheapest viable source first: 1. **Abbreviated metadata's `modified` field** — the resolver already fetches this for resolution. If the package as a whole hasn't been modified within the policy cutoff, every version it contains is at least that old; return `modified` as a conservative upper-bound and skip the rest of the chain. 2. **Local `FULL_META_DIR` mirror** — exact per-version times if a previous verification populated it. 3. **npm attestation endpoint** (`/-/npm/v1/attestations/<name>@<version>`) — a tens-of-KB Sigstore bundle whose Rekor inclusion time stands in for publish time. Wins on cold cache when the package was published with provenance. 4. **Full metadata fetch** — last resort. ## Why The verification cache from #11691 made repeat installs against an unchanged lockfile effectively free. The remaining cost is the *first* verification on a fresh CI runner with no restored cache — particularly `pnpm install --frozen-lockfile`, where every locked package's publish timestamp has to be confirmed. Fetching the full metadata document for each package is wasteful when: - The resolver has usually already cached abbreviated metadata, whose `modified` field alone is enough to clear stable packages (the common case). - For recently-modified packages, the per-version attestation endpoint is orders of magnitude smaller than full metadata. ## How ### Abbreviated `modified` shortcut `fetchFullMetadataCached` is refactored to share an internal helper with a new `fetchAbbreviatedMetadataCached`. Both do conditional GETs against their respective on-disk mirrors. On a non-frozen install the abbreviated mirror is already populated by the resolver, so the shortcut hits the local cache at headers-only cost. On `--frozen-lockfile` the fetch is still cheaper than full metadata. If `Date.parse(modified) < cutoff`, return `modified` — it's an upper bound on every version's publish time in this package, and the verifier's `published < cutoff` check passes trivially. ### Attestation endpoint `fetchAttestationPublishedAt` (new module) hits `/-/npm/v1/attestations/<name>@<version>`, parses the response, and reads the earliest `bundle.verificationMaterial.tlogEntries[].integratedTime` across the attestation bundles. That's the Rekor inclusion time — a couple of seconds after publish, well within tolerance for a policy that operates in minutes/hours/days. Returns `undefined` on 404 / network error / malformed body so the caller falls back. ### Per-install dedup The lookup carries a `PublishedAtLookupContext` with four memo maps: abbreviated meta per (registry, name), local full meta per (registry, name), full meta network fetch per (registry, name), final published-at per (registry, name, version). Verifying many versions of the same package only pays the disk/network costs once. ## Trust model **No Sigstore signature verification on the attestation path.** The trust model is identical to reading the registry's `time` field on the full metadata document — we already trust the registry to serve correct publish timestamps for the gate's purpose. The win is purely bandwidth. Full Sigstore verification (Fulcio cert chain + Rekor inclusion proof) would harden the timestamp against a compromised registry. It pulls in the `sigstore` npm package and the TUF root — a separate dependency-surface discussion, parked as future work. ## Tests - **13 unit tests** in `resolving/npm-resolver/test/fetchAttestationPublishedAt.test.ts`: ISO timestamp extraction, URL construction (scoped + unscoped), 404 / 5xx / network error / malformed JSON / missing fields → undefined, earliest-of-multiple-attestations, defensive number-as-integratedTime, auth header forwarding, trailing-slash normalization. - Existing `minimumReleaseAge` + `verifyLockfileResolutions` integration suites (45 tests) still pass — the fallback chain preserves end-to-end behavior when the new shortcuts don't apply.
@pnpm/resolving.npm-resolver
Resolver for npm-hosted packages
Installation
pnpm add @pnpm/resolving.npm-resolver
Usage
'use strict'
const createResolveFromNpm = require('@pnpm/resolving.npm-resolver').default
const resolveFromNpm = createResolveFromNpm({
store: '.store',
offline: false,
rawConfig: {
registry: 'https://registry.npmjs.org/',
},
})
resolveFromNpm({alias: 'is-positive', bareSpecifier: '1.0.0'}, {
registry: 'https://registry.npmjs.org/',
})
.then(resolveResult => console.log(JSON.stringify(resolveResult, null, 2)))
//> {
// "id": "registry.npmjs.org/is-positive/1.0.0",
// "latest": "3.1.0",
// "package": {
// "name": "is-positive",
// "version": "1.0.0",
// "devDependencies": {
// "ava": "^0.0.4"
// },
// "_hasShrinkwrap": false,
// "directories": {},
// "dist": {
// "shasum": "88009856b64a2f1eb7d8bb0179418424ae0452cb",
// "tarball": "https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz"
// },
// "engines": {
// "node": ">=0.10.0"
// }
// },
// "resolution": {
// "integrity": "sha1-iACYVrZKLx632LsBeUGEJK4EUss=",
// "registry": "https://registry.npmjs.org/",
// "tarball": "https://registry.npmjs.org/is-positive/-/is-positive-1.0.0.tgz"
// },
// "resolvedVia": "npm-registry"
// }
License
MIT