Repository navigation
Hosted pnpm rollback/remove restores pnpm-lock.yaml from npmjs's version document instead of the project's .npmrc registry, so a mirror project loses its tarball: URL (cold frozen install 404s) or is moved to npmjs #919
Description
[agent] Found by the scheduled pnpm bug-hunt routine (ledger #303).
Summary
This is the pnpm side of #908 (yarn berry) and #521 (vlt). Draft PR #918 fixes the shared root cause only for berry and vlt. restore_pnpm_locks isn't in its plan.
When rollback / remove undo a hosted pin in pnpm-lock.yaml, restore_pnpm_locks gets the upstream dist from fetch_dists → UpstreamClient::npm_dist. That always reads the version document from SOCKET_NPM_REGISTRY, or https://registry.npmjs.org when it's unset. The project's .npmrc registry= is read (pnpm_tarball_policy), but only to decide whether the tarball: key is written. It never decides which registry the dist comes from. So on a project that installs from a private mirror, with SOCKET_NPM_REGISTRY unset (the normal case), there are two failure modes:
- The mirror's
dist.tarballis non-conventional (CDN/download paths, e.g. GitHub Packages…/download/…, or a mirror whose tarballs live on another path or host). pnpm wrotetarball: <mirror URL>into the lock. The restore sees npmjs's conventional URL, decides pnpm derives it, and writes a bare{integrity}. pnpm then derives<mirror>/left-pad/-/left-pad-1.3.0.tgz, and a coldpnpm install --frozen-lockfilefails withERR_PNPM_FETCH_404. lockfileIncludeTarballUrl/lockfile-include-tarball-urlis on (a setting the file the installed pnpm reads; that's the policy path Hosted pnpm rollback drops the upstreamtarball:URL from locks written withlockfileIncludeTarballUrl, so the restore isn't byte-exact #557 / Fix hosted restore dropping registry tarball URLs (#557, #817) #818 added). The restore writes npmjs'sdist.tarballin place of the mirror's URL. The project silently moves from its mirror to the public registry. That breaks air-gapped or allow-listed CI, and the result isn't byte-exact.
Both commands exit 0 with status: success.
Impact
After a hosted rollback or remove, a project on a private mirror can't install on a clean CI cache (mode 1), or fetches from a registry it isn't configured for (mode 2). socket-patch reports success, and nothing tells users to set SOCKET_NPM_REGISTRY to their mirror. CLI_CONTRACT documents the variable only as the base the restore reads.
Repro (Linux, main 9c43dfc, real pnpm)
Three local HTTP servers: a patch-API mock (:18601, which also serves the hosted tarball); an npmjs stand-in (:18602) whose dist.tarball is conventional under its own base, like npmjs (the sandbox binary can't reach npmjs, so the stand-in plays SOCKET_NPM_REGISTRY's default); and the project's mirror (:18603). The mirror serves the real left-pad-1.3.0.tgz, either at the conventional path or (CDN style) only at /_t/left-pad/1.3.0.tgz, with dist.tarball set to match.
mkdir proj && cd proj
echo '{"name":"proj","version":"1.0.0","dependencies":{"left-pad":"1.3.0"}}' > package.json
printf 'registry=http://127.0.0.1:18603/\n' > .npmrc
# mode 2 only: printf 'lockfile-include-tarball-url=true\n' >> .npmrc (pnpm 9/10)
# or 'lockfileIncludeTarballUrl: true' in pnpm-workspace.yaml (pnpm 10/12)
pnpm install && cp pnpm-lock.yaml lock.orig
export SOCKET_PATCH_SERVER_URL=http://127.0.0.1:18601 SOCKET_NPM_REGISTRY=http://127.0.0.1:18602 # = "unset" (npmjs)
socket-patch scan --mode hosted --yes --api-url http://127.0.0.1:18601 --org test-org --api-token fake # pins left-pad
socket-patch rollback --yes --json # rc 0, success (same with `remove <uuid>`)
diff lock.orig pnpm-lock.yaml
# mode 1 (CDN mirror):
# < resolution: {integrity: sha512-XI5M…, tarball: http://127.0.0.1:18603/_t/left-pad/1.3.0.tgz}
# > resolution: {integrity: sha512-XI5M…}
# mode 2 (conventional mirror + tarball URLs):
# < resolution: {integrity: sha512-XI5M…, tarball: http://127.0.0.1:18603/left-pad/-/left-pad-1.3.0.tgz}
# > resolution: {integrity: sha512-XI5M…, tarball: http://127.0.0.1:18602/left-pad/-/left-pad-1.3.0.tgz}
# copy the tree without node_modules, cold store + cache:
pnpm install --frozen-lockfile
# mode 1: ERR_PNPM_FETCH_404 GET http://127.0.0.1:18603/left-pad/-/left-pad-1.3.0.tgz: Not Found - 404
# mode 2: fetches from :18602 (npmjs) instead of the mirror; the stand-in serves no tarballs, so it 404s hereControl: the same steps with SOCKET_NPM_REGISTRY=http://127.0.0.1:18603 (the project's own mirror) give a byte-exact lock and a clean cold frozen install. Another control: a conventional mirror without tarball URLs is byte-exact even with SOCKET_NPM_REGISTRY unset.
Expected vs actual
- Expected: CLI_CONTRACT's upstream restore puts back "resolution + integrity … from the npm registry's version document" so that the lock is what pnpm would write. For pnpm that's the registry the project resolves against (
.npmrcregistry=, or@scope:registry=for scoped names). Rollback of a hosted pin should return the lock to its pre-pin bytes, as it already does for a conventional mirror and for npmjs projects. - Actual: the
distalways comes from npmjs (orSOCKET_NPM_REGISTRY). The mirror'starball:is dropped (mode 1) or replaced with npmjs's (mode 2), and the commands exit 0.
OS × version
| OS | pnpm | Mode 1 (CDN mirror), rollback / remove | Mode 2 (tarball URLs), rollback / remove | Control (SOCKET_NPM_REGISTRY = mirror) |
|---|---|---|---|---|
| Linux | 9.15.9 | fail / untested | fail (.npmrc) / untested |
untested |
| Linux | 10.34.5 | fail (×2) / fail | fail (.npmrc) / fail (workspace file) |
pass |
| Linux | 12.8.1 | fail / untested | fail (workspace file) / untested | pass |
| macOS, Windows | — | untested (the code path isn't OS-specific) |
Not a regression. Before #818 the pnpm restore never wrote tarball: (#557), so release 4.0.0 drops the mirror URL the same way in mode 1.
Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:815:restore_pnpm_lockscallsfetch_dists(→npm.rs:38ctx.client.npm_dist, the default registry only) before it reads the project's registry.npm.rs:765:pnpm_tarball_policyreads.npmrcregistry, but uses it only inregistry_derives_tarball(npm.rs:841-851), not for the lookup. Scoped@scope:registry=keys aren't read at all.- Related: Hosted yarn berry rollback/remove still drops a custom registry's
::__archiveUrl=binding (#817 fix incomplete): the restore looks updist.tarballon npmjs, not the project'snpmRegistryServer, so cold installs 404 #908 / vlt hosted rollback and remove rewrite slot [3] to a synthesized/<name>/-/<leaf>-<ver>.tgzURL instead of the registry's dist.tarball, so the next coldvlt ci404s #521 (same root cause, other lock formats; PR Fix npm-family restore ignoring project registry (#908, #521) #918), Hosted pnpm rollback/remove adds registrytarball:URLs the lock never had whenlockfileIncludeTarballUrlsits in a settings file the installed pnpm ignores (workspace file on pnpm 9,.npmrcon pnpm 11/12) #902 (which settings file decides thetarball:policy).
No probe runs: the sandbox is Linux-only, and this path has no OS-specific branches.
Activity
mikolalysenko commentedon Oct 6, 2026
[agent] Shares root cause with #908, #521: npm-family upstream restore reads the version document only from the default registry, not the registry the project's lock resolves against. Will be fixed together.
Confirmed on PR #918's head: it adds
upstream::npm::fetch_dists_on(project-registry lookup) and wires it intorestore_berryand vlt, butrestore_pnpm_locks(upstream/npm.rs:815) still calls the default-registryfetch_dists. PR #918 is currently being worked by another run, so this issue isn't added to its scope yet; it should either join #918 (pnpm.npmrcregistry=/@scope:registry=→fetch_dists_on) or follow right after it on the same helper.Generated by Claude Code
mikolalysenko commentedon Oct 7, 2026
[agent] Claiming this issue as part of the pnpm open-issue sweep in draft PR #1007. Branch: agent/fix-pnpm-open-issues. Claim-ID: 2026-10-07T12:44:29Z-pnpm07
Fix pnpm restore reading the default registry, not the .npmrc mirror (#…
Read pnpm-workspace.yaml registries in the pnpm restore (#919)
Cover pnpm scope-registry, workspace-mirror and fallback restores (#919)
Format the pnpm registry lookup and tests; clippy is_none_or
mikolalysenko commentedon Oct 7, 2026
[agent] PR #1007 fixes this issue. It has a regression test that fails on
main, CI is fully green (552 checks) and it's ready for review. The issue will close when #1007 merges. The PR description's table gives the root cause, the fix and the test for each issue.mikolalysenko commentedon Oct 9, 2026
[agent] pnpm bug-hunt re-check on main
a80b89e(after #1007). The fix works: the restore now reads the.npmrc/pnpm-workspace.yamlmirror. The mock log showsGET <mirror>/left-pad/1.3.0, and there's no fallback warning.There's one leftover shape, which is minor. The mirror's version document still gives the npmjs
dist.tarball(a metadata-only mirror). pnpm recordstarball: https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz, because that URL can't be derived from the mirror. Butregistry_derives_tarball(crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:447-452) also counts a URL that is conventional under the default registry (npm_registry_base()) as derived. So the restore writes a bare{integrity}:It reproduces on pnpm 9.15.9 (
.npmrc registry=), 11.28.5 and 12.10.1 (both.npmrcandpnpm-workspace.yamlregistry:), withSOCKET_NPM_REGISTRYunset. The restored lock then fetches from<mirror>/left-pad/-/…. My mirror serves that path, so the cold frozen installs still passed, and the visible effect is only that the restore isn't byte-exact (CLI_CONTRACT: "a mirror'starball:comes back as pnpm recorded it"). A mirror that doesn't serve conventional paths would 404, as in the original mode 1. I haven't filed this separately.Generated by Claude Code