DEV Community

Noble Ronin
Noble Ronin

Posted on

npm's "latest" Is a Label a Maintainer Sets. PyPI's and crates.io's Is Just Math.

npm's

I build a scraper that reads npm, PyPI and crates.io for a living (side project —
package-registry-scraper
on Apify). Last week I was diffing version lists for something unrelated and
noticed a number that didn't make sense: vue's "latest" was 3.5.43, but
sitting in the same JSON payload was 3.6.0-rc.10 — a higher version number,
published, live, installable, and not what you get when you type
npm install vue.

My first assumption was a scraper bug. It wasn't. It's npm working exactly as
designed, and once I understood why, I realized the other two registries I
scrape don't even have the concept that caused my confusion.

The thing I didn't know

npm's "latest" isn't computed. It's a label. Every npm package carries a
dist-tags object — a small set of names, each one a maintainer-settable
pointer into the list of published versions. latest is just the tag npm's
own CLI happens to install by default. Nothing stops a maintainer from
pointing it anywhere, including backward, including nowhere near the highest
version number that exists.

Here's react's full tag set, live, right now:

curl -s https://registry.npmjs.org/react | python3 -c "
import json,sys
d = json.load(sys.stdin)
print(d['dist-tags'])
"
# {'beta': '19.0.0-beta-26f2496093-20240514', 'rc': '19.0.0-rc.1',
#  'next': '19.3.0-canary-d5736f09-20260507', 'backport': '19.0.8',
#  'latest': '19.3.0',
#  'experimental': '0.0.0-experimental-278794d7-20261002',
#  'canary': '19.3.0-canary-278794d7-20261002'}
Enter fullscreen mode Exit fullscreen mode

Seven independent pointers into one version list. vue runs the same
pattern, plus a v2-latest tag permanently parked on 2.7.16 for people who
never migrated off Vue 2. If you only ever read dist-tags.latest — which is what
most tooling does, including, until last week, mine — you will never see the
other six.

I pulled 12 well-known packages to see how common the mismatch actually is.
Only vue had it. I'd have bet on react too, and I'd have lost: its canary
builds are all 19.3.0-canary-…, and semver ranks a prerelease below the
release it precedes, so 19.3.0 really is the top. angular, left-pad,
node-sass, request, moment, express, lodash, is-odd, is-even and
colors matched exactly too. So this isn't registry-wide chaos. It's a specific thing that happens once a package
maintains a canary/rc/next channel under the same name instead of a separate
package, and it happens on some of the most-depended-on code on the internet.

What the other two registries do instead

I expected PyPI to have something similar — a latest you could override.
It doesn't. pip install black resolves to whatever PEP 440 says is the
highest stable version number, full stop:

curl -s https://pypi.org/pypi/black/json | python3 -c "
import json,sys
d = json.load(sys.stdin)
print(d['info']['version'])
"
# 26.10.0
Enter fullscreen mode Exit fullscreen mode

black has 74 release entries on record, several of them prereleases
(21.8b0, 23.1a1, 26.1a1...). None of them can become info.version
unless they're literally the highest-numbered stable release. There's no
sibling field to set, no second opinion to query. I checked five packages
with real prerelease history (black, urllib3, Django, pip, numpy)
and every single one resolved the same deterministic way. A PyPI maintainer
cannot do what the Vue maintainers are doing right now.

crates.io goes one step further and doesn't even store the concept. The
sparse index — the actual file cargo downloads, not a convenience API —
is newline-delimited JSON, one line per published version, each line
carrying vers, yanked, a cksum, and a pubtime:

curl -s https://index.crates.io/se/rd/serde | tail -1
# {"name":"serde","vers":"1.0.229", ... "yanked":false,
#  "pubtime":"2026-07-18T23:05:13Z"}
Enter fullscreen mode Exit fullscreen mode

No latest key anywhere in that file. cargo computes "the newest usable
version" itself, client-side, as the highest non-yanked semver that
satisfies whatever range your Cargo.toml asked for. The registry has no
stored opinion for a maintainer to bend.

So you get three different philosophies for the exact same question —
"what version do I get if I don't ask for one in particular" — and they're
not minor implementation details. npm hands the answer to the publisher as
a lever. PyPI computes it from a spec nobody can override. crates.io doesn't
even track the question; it pushes the computation to your own machine.

Where this actually bites

The boring version: if you're writing tooling that audits dependencies —
license scanners, SBOM generators, "what's the newest version of X"
dashboards — and you built it against PyPI or crates.io first, your mental
model is "latest is the biggest number." Port that model to npm unexamined
and you'll either silently ignore legitimate prerelease channels (fine, if
that's the intent) or, worse, misreport dist-tags.latest as "the newest
code that exists" when six other tags, and sometimes a materially newer
canary build, say otherwise.

The sharper version: dist-tags.latest is writable by anyone with publish
access to the package. It's a flag on an API response, not a cryptographic
fact. If you've ever written a script that trusts "latest" as a proxy for
"the version most people are currently running" or "the version the
maintainer currently endorses," you were trusting a string a human can
change at any time, for any reason, with no version bump and no changelog
entry required.

Where I was wrong going in

I went looking for a dirtier story than the one I found. My first guess was
that I'd find a case of a maintainer quietly rolling latest backward
below a version with a known vulnerability — a kind of retroactive
"un-shipping" that the registry's version history wouldn't even hint at.
I checked the obvious candidates (ua-parser-js, coa, node-ipc — all
real incident packages) and didn't find it: when npm packages flag a bad
release, they use the separate per-version deprecated string, not a
dist-tags rewrite, and I'd already covered that mechanism in a piece a
few weeks back. So the "lever" I found this week is real and verifiable,
but it's a UX/tooling-assumption footgun, not (as far as I could confirm
live) a documented attack pattern. I'm flagging the gap between what I set
out to find and what I actually found, rather than quietly rounding my
original hunch up to a conclusion.

I keep the endpoint + field reference for all three registries, including
the full dist-tags shape, in
noble-ronin/package-latest-tag-semantics
if you want the cheatsheet version of this without the narrative.

If you've written anything that reads dist-tags.latest and treats it as
a timestamp-equivalent fact — did you know about the other six tags, or did
you find out the way I did?

Top comments (0)