Is there an existing issue for this?
Related, not the same: #4924 and #7015 report a version-specific unpublish that removed every version, but with --force. Here --force was not given, and the "Refusing to delete the last version" check did not stop it.
This issue exists in the latest npm version
Reproduced on npm 11.19.0 and on latest of this repository (83ea1a1).
Current Behavior
A package of mine (name left out here) had three versions: 0.0.0-stage, 0.0.1, and 0.1.0. 0.1.0 was published about six minutes earlier, through a trusted publisher. Then:
npm unpublish <pkg>@0.0.1
printed - <pkg>@0.0.1 and exited 0, but the registry removed the whole package: all three versions. The debug log shows a whole-package DELETE, and no PUT:
16 http fetch GET 200 https://registry.npmjs.org/<pkg>?write=true 395ms (cache miss)
17 http fetch GET 200 https://registry.npmjs.org/<pkg>?write=true 194ms (cache miss)
18 http fetch DELETE 401 https://registry.npmjs.org/<pkg>/-rev/3-56bf641ba8f19a0f2c2bd1d5197c6040 221ms
...
25 http fetch DELETE 200 https://registry.npmjs.org/<pkg>/-rev/3-56bf641ba8f19a0f2c2bd1d5197c6040 814ms
30 verbose exit 0
(The 401 is the web login; the retry after it is the same request.)
The package can now not be published for 24 hours, and the three version numbers can never be used again.
The cause in the code:
lib/commands/unpublish.js refuses when versions.length === 1 && spec.rawSpec === versions[0]. It reads versions with pacote.packument(), which requests the abbreviated packument (Accept: application/vnd.npm.install-v1+json).
libnpmpublish's unpublish() reads the full packument (Accept: */*) and deletes the whole package when that document has one version or none.
The check and the deletion read two different documents. When the full one lists fewer versions than the abbreviated one, the check passes and the deletion removes the whole package. Request 16 in the log is the check and request 17 is libnpmpublish. We cannot see from the log why the full document had one version or none at revision 3. The new 0.1.0 not being in it yet is our guess.
A full packument with no versions also passes the check (0 !== 1) and deletes the whole package.
Expected Behavior
npm unpublish <pkg>@<version> without --force either removes that one version, or refuses with "Refusing to delete the last version of the package". It never deletes the whole package.
Steps To Reproduce
-
Save this mock registry as mock-registry.mjs. Its abbreviated document lists 0.0.1 and 0.1.0; its full document lists only 0.0.1:
import http from 'node:http'
const v = (n) => ({ name: 'demo-pkg', version: n, dist: { tarball: `http://localhost:4873/demo-pkg/-/demo-pkg-${n}.tgz` } })
const abbreviated = { name: 'demo-pkg', 'dist-tags': { latest: '0.1.0' }, versions: { '0.0.1': v('0.0.1'), '0.1.0': v('0.1.0') } }
const full = { _id: 'demo-pkg', name: 'demo-pkg', _rev: '3-abc', 'dist-tags': { latest: '0.0.1' }, versions: { '0.0.1': v('0.0.1') } }
http.createServer((req, res) => {
console.log(req.method, req.url, (req.headers.accept ?? '').slice(0, 30))
if (req.method === 'GET') {
res.setHeader('content-type', 'application/json')
return res.end(JSON.stringify(req.headers.accept?.startsWith('application/vnd.npm.install-v1') ? abbreviated : full))
}
res.end('{}')
}).listen(4873)
-
node mock-registry.mjs
-
In another shell: echo '//localhost:4873/:_authToken=fake' > .npmrc and npm unpublish demo-pkg@0.0.1 --registry http://localhost:4873/ --userconfig ./.npmrc
-
npm prints - demo-pkg@0.0.1. The registry log shows the same three requests as the real log above:
GET /demo-pkg?write=true application/vnd.npm.install-v1
GET /demo-pkg?write=true */*
DELETE /demo-pkg/-rev/3-abc */*
Environment
- npm: 11.19.0
- Node.js: 26.7.0
- OS Name: macOS (arm64)
- npm config: registry
https://registry.npmjs.org/, no other settings
Is there an existing issue for this?
Related, not the same: #4924 and #7015 report a version-specific unpublish that removed every version, but with
--force. Here--forcewas not given, and the "Refusing to delete the last version" check did not stop it.This issue exists in the latest npm version
Reproduced on npm 11.19.0 and on
latestof this repository (83ea1a1).Current Behavior
A package of mine (name left out here) had three versions:
0.0.0-stage,0.0.1, and0.1.0.0.1.0was published about six minutes earlier, through a trusted publisher. Then:printed
- <pkg>@0.0.1and exited 0, but the registry removed the whole package: all three versions. The debug log shows a whole-packageDELETE, and noPUT:(The 401 is the web login; the retry after it is the same request.)
The package can now not be published for 24 hours, and the three version numbers can never be used again.
The cause in the code:
lib/commands/unpublish.jsrefuses whenversions.length === 1 && spec.rawSpec === versions[0]. It readsversionswithpacote.packument(), which requests the abbreviated packument (Accept: application/vnd.npm.install-v1+json).libnpmpublish'sunpublish()reads the full packument (Accept: */*) and deletes the whole package when that document has one version or none.The check and the deletion read two different documents. When the full one lists fewer versions than the abbreviated one, the check passes and the deletion removes the whole package. Request 16 in the log is the check and request 17 is libnpmpublish. We cannot see from the log why the full document had one version or none at revision 3. The new
0.1.0not being in it yet is our guess.A full packument with no
versionsalso passes the check (0 !== 1) and deletes the whole package.Expected Behavior
npm unpublish <pkg>@<version>without--forceeither removes that one version, or refuses with "Refusing to delete the last version of the package". It never deletes the whole package.Steps To Reproduce
Save this mock registry as
mock-registry.mjs. Its abbreviated document lists0.0.1and0.1.0; its full document lists only0.0.1:node mock-registry.mjsIn another shell:
echo '//localhost:4873/:_authToken=fake' > .npmrcandnpm unpublish demo-pkg@0.0.1 --registry http://localhost:4873/ --userconfig ./.npmrcnpm prints
- demo-pkg@0.0.1. The registry log shows the same three requests as the real log above:Environment
https://registry.npmjs.org/, no other settings