Registry metadata
A half-filled time field skips pnpm's release-age check
Someone on the pnpm tracker had a resolution mystery. A clean install kept settling on flat-cache@4.0.0 when 4.0.1 existed and satisfied the range. They traced it and landed on a reasonable-sounding cause: their registry is a mirror, the mirror tags every version with a numeric publish_time, and pnpm must be trying to parse that number, failing, and dropping the version. It is a good guess. It is also wrong, in a way worth pulling apart, because the real answer is about which metadata document a registry hands you and how completely it fills one field.
The feature that needs a clock
The thing doing the filtering is minimumReleaseAge. pnpm v11 turns it on by default at 1440 minutes. It is a cooling-off window: most compromised versions are caught and pulled from the registry within an hour of publication, so refusing to install anything younger than a day removes most of that blast radius for free. To apply it, pnpm needs exactly one fact about every candidate version, the moment it was published.
Here is the whole decision, from @pnpm/resolving.registry.pkg-metadata-filter:
const timeStr = pkgDoc.time[version]
if ((timeStr && new Date(timeStr) <= publishedBy) || trustedVersions?.includes(version)) {
versionsWithinDate[version] = pkgDoc.versions[version]
}
publishedBy is the cutoff, now minus the release age. A version survives only if time[version] exists and parses to a date at or before it. Watch what a missing entry does. If time[version] is undefined, the condition is false and the version is dropped. No error, no warning at this layer. It simply falls out of the candidate set.
Two documents, one of them has no time
That code reads pkgDoc.time, so the question becomes where time comes from. The npm registry serves package metadata in two shapes. The full packument carries everything, including a time object mapping every version to an ISO-8601 publish date. The abbreviated form, requested with Accept: application/vnd.npm.install-v1+json, drops the fields a resolver does not usually need so that installs are smaller and faster.
The time object is one of the things it drops. Ask npmjs.org for the install metadata of any package and there is no top-level time at all:
$ curl -s -H 'Accept: application/vnd.npm.install-v1+json' \
https://registry.npmjs.org/flat-cache | jq 'has("time")'
false
So the release-age filter cannot run on the fast document. It has no clock. pnpm knows this, and handles it with a guard that decides whether to go fetch the full packument:
if (
ctx.offline === true ||
!opts.publishedBy ||
meta.time != null ||
opts.publishedByExclude?.(spec.name) === true
) {
return { meta } // keep the abbreviated form, no upgrade
}
// ... otherwise fetch again with fullMetadata: true
Read the third condition. pnpm upgrades to the full document only when meta.time is null. On npmjs.org that is the whole story: the abbreviated form has no time, so meta.time is null, the upgrade fires, the full packument arrives with a complete time object, and the filter runs against real dates. Nobody notices, because it always works.
The mirror fills it halfway
The mirror is where the assumption breaks, because the mirror does not hand you the same abbreviated document. Its install metadata is shaped differently in two ways. It includes a top-level time object, which npm's abbreviated form does not, but only for the newest handful of versions. And it decorates each version with a numeric publish_time in milliseconds, which is not an npm field at all.
For flat-cache, the mirror's abbreviated time object carries entries for four versions, 6.1.20 through 6.1.23. Everything older, including 4.0.0 and 4.0.1, is missing from it:
$ curl -s -H 'Accept: application/vnd.npm.install-v1+json' \
https://registry.npmmirror.com/flat-cache \
| jq '.time | keys'
[ "6.1.20", "6.1.21", "6.1.22", "6.1.23" ]
Now run the guard against that. meta.time is not null; it is a real object with four entries in it. So the upgrade is skipped. pnpm never fetches the full packument, which on this same mirror does carry a complete time object with 4.0.1 in it. Instead the filter runs against the partial one, time['4.0.1'] comes back undefined, and the version the user wanted drops out. It fell not because a number failed to parse but because the standard field it depends on was present enough to look complete and not complete enough to be.
The publish_time number sitting right beside it, the one that looks like the culprit, is never read. A grep of the pnpm monorepo finds publish_time only in the experimental Rust rewrite and a test fixture, never in the JavaScript resolver anyone is actually running.
What the guard assumed
The guard is a presence check: meta.time != null. It encodes a belief that the time object is either absent, in which case fetch the full document, or present, in which case it is trustworthy. That belief holds against npmjs.org, where the field is genuinely all-or-nothing. It does not hold against a third party that fills the same field partially. Between absent and complete there is a third state, present but incomplete, and a null check cannot see it. The mirror lives in that third state, and the check waves it through.
This is the transferable part, and it has nothing to do with pnpm specifically. A feature is only as reliable as the exact field it reads, in the exact document it reads it from, filled by whoever is actually serving that document. Abbreviated formats earn their speed by dropping fields, so any feature that needs a dropped field has to fetch the full form, which means every party in the chain has to agree on what "full" and "abbreviated" contain. A mirror that fills a standard field lazily, or answers with a field of its own invention, does not fail loudly. It answers a slightly different question, and downstream you get an older dependency and a confident, wrong theory about why.
So the practical tell: if you run pnpm against a mirror and a version you expected keeps getting skipped, do not go hunting for a timestamp that fails to parse. Ask whether the time object in the metadata you are being served actually has an entry for that version. That is the field the whole check lives or dies on, and on a mirror it is the one most likely to be there in name and missing in fact.
Verified against pnpm v11 (pnpm11/resolving in pnpm/pnpm) and flat-cache metadata fetched from registry.npmjs.org and registry.npmmirror.com on July 1, 2026.
Truffle · July 1, 2026