Tech 8 min read

One npm account: 964M downloads/week across 7 packages, none with provenance

IkesanContents

TL;DR

What’s affected postcss, nanoid, caniuse-lite, browserslist, autoprefixer, postcss-nested, postcss-js — 7 packages, 964M weekly downloads, all published by the single npm account ai, and none with provenance (per the DEV Community tally).

Not a breach No malicious version has shipped. The concern is structural: one publisher, a huge download base, and no provenance to separate a legit release from a stolen-credential one. Per issue #2096, nanoid and nanospy already moved to staged releases; PostCSS is 1–2 weeks out.

What to check

  1. Whether the target packages are in your lockfile — directly or via build tools
  2. CI that pulls post-publish updates immediately
  3. npm provenance display and staged-releases adoption
  4. A release-age gate and an install-script allowlist

This spans both CSS-build and ID-generation packages, so check transitive build-tool paths, not just direct dependencies.


postcss, nanoid, browserslist, caniuse-lite, and autoprefixer end up in your lockfile even if you never wrote them in package.json yourself.
They’re pulled in everywhere — CSS builds, browser-compat data, ID generation.

A DEV Community post points out that seven packages published by the npm account ai add up to 964M weekly downloads, and none of them carry an npm provenance attestation (the signed record of where and how a package was built).
This isn’t a report that a poisoned version shipped.
The point is that if a legit-looking version were published from stolen credentials, consumers would have a hard time telling it apart using only what the registry shows.

Here are the seven packages.

PackageWeekly downloads (DEV post)How it gets in
postcss245,612,332core of CSS transforms
nanoid206,588,788ID generation
caniuse-lite173,435,668browser-compat data
browserslist167,746,012resolving target browsers
autoprefixer63,517,741adding CSS prefixes
postcss-nested54,486,292nested-syntax transform
postcss-js52,771,544CSS as JS objects

The same reporter’s table in the PostCSS issue counts six packages (dropping postcss-nested) at 869M/week.
Either way you count it, the shape is the same: 800–900 million weekly downloads sitting behind a single account.

The risk is one account plus an unverifiable publish path

npm trusted publishing lets you publish from a CI/CD workflow via OIDC.
Per the npm docs, with trusted publishing you publish without storing a long-lived npm token, and on GitHub Actions or GitLab CI/CD a provenance attestation is generated automatically.

With provenance, it’s easier to verify which public repo, which workflow, and how a package was built.
Without it — a package published from a local machine or a long-lived token — it’s hard for outsiders to tell an intended release from one made with stolen credentials.

This setup is close to the axios npm compromise I wrote about earlier.
There, a legit maintainer’s npm account was compromised and a poisoned version shipped from a path that wasn’t the real CI.
It wasn’t the package itself — the postinstall of a fake dependency, plain-crypto-js, ran, so consumers were hit at npm install time.

With the ai account, no such malicious version has shipped.
But the same kind of publish authority covers a very large surface.
It’s not just the CSS-build packages — ID-generation libraries like nanoid sit with the same publisher too, so a single account compromise reaches a wide range of projects.

Provenance is evidence of the publish path, not a safety verdict

I don’t treat presence-or-absence of provenance as a safety verdict on its own.
In the Mini Shai-Hulud TanStack/Mistral wave, a legit GitHub Actions path was poisoned and malicious npm packages shipped with SLSA provenance.
More recently, the Miasma worm that hit Red Hat’s @redhat-cloud-services was the same type: on June 1, 2026, 32 packages were published with valid SLSA provenance from a compromised GitHub Actions OIDC path.
Provenance proves “this CI built it,” but not “this CI’s cache and runner weren’t compromised.” In the Red Hat case, the provenance was correct while the CI was building malware.

Conversely, in the Mastra easy-day-js compromise, the publish came from a personal token and provenance was missing.
Mastra’s legit releases went out through a trusted-publisher flow, so the missing provenance worked as a signal that the release didn’t come from the real CI.

The ai account situation is closer to having none of that detection room.
Provenance doesn’t always guarantee safety, but without it you have less to mechanically separate a “not-the-real-CI publish” from a legit one.

The maintainer is going with staged releases

In PostCSS GitHub issue #2096, maintainer Andrey Sitnik (ai on both npm and GitHub) replies that provenance alone can’t stop every supply-chain attack.
His argument: making CI the publisher actually raises attack risk compared to manual publishing with 2FA.
TanStack was compromised precisely because it published from CI and that token was stolen — so adding a CI publish path just shifts what you have to defend from the local machine to the CI runner.

Instead, the plan is to add staged releases.
The npm docs describe staged publishing like this: instead of publishing directly with npm publish, you stage a version with npm stage publish, and the maintainer approves it with 2FA before it goes public.
Combined with trusted publishing, CI pushes to staging and a human gives the final approval.

In a June 19, 2026 UTC issue comment, the maintainer writes that nanoid and nanospy have moved to the new process, and PostCSS will follow in a week or two.
And indeed, as of writing, nanoid@5.1.14 shows staged publishing while postcss@8.5.15 does not yet.
At the time of the original post all seven packages had no provenance, but the migration has started, and the state now differs package by package.

What you can check isn’t limited to whether provenance exists.

What to look atWhat it tells youWhat still remains
provenancethe publish path and build sourcethe CI itself being compromised
staged releaseswhether a human 2FA approval gates publishinga compromised approver machine, thin review
extra publisherthe recovery path if the account is lockeddistributing publish authority
release-age gatewhether you pull a just-published poisoned versionlong-lived poisoned versions, existing lockfiles

On the consumer side: check your lockfile and post-publish updates

The check you can do right now is to see where the target packages come from, in your lockfile.
Even if they’re not in your direct dependencies, they come in through Vite, Astro, Tailwind, PostCSS plugins, and browser-compat data updates.

rg '"postcss"|"nanoid"|"caniuse-lite"|"browserslist"|"autoprefixer"|"postcss-nested"|"postcss-js"' package-lock.json pnpm-lock.yaml yarn.lock
npm ls postcss nanoid caniuse-lite browserslist autoprefixer postcss-nested postcss-js

Showing up here doesn’t mean you’re in danger.
The more a project pulls these in, the more it matters whether you push post-publish updates straight into CI or wait a few days.

As I wrote in pnpm 11 and Yarn 4.10’s release-age gate, a setting that won’t pull a new version until some time has passed since publish lowers the odds of hitting a poisoned version that gets deleted quickly.
The ai account’s packages have huge download counts, so plenty of environments pull them within the first hour of a release.
The more a project does a fresh install on every CI run, the more this setting shows its value.

On install scripts, not all of these packages carry the same risk.
But in the axios/Mastra pattern — add one dependency line to a legit package and let that dependency’s postinstall run — whether the original package usually has an install script isn’t enough to catch it.
npm v12’s allowScripts is a defense that makes this post-fetch auto-execution opt-in.

References