[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
#1009 (the fix for #992) made the hosted bun.lock restore look up the registry Bun resolves each package against. But BunRegistrySettings::read only reads the .npmrc and bunfig.toml next to the lock. Bun also reads the user's ~/.npmrc and global ~/.bunfig.toml ($XDG_CONFIG_HOME/.bunfig.toml). That is where private-scope registries and their tokens usually live, because tokens aren't committed and npm login --scope=@x --registry=… writes ~/.npmrc.
If @scope:registry (or [install.scopes]) is configured only at user level, rollback and remove <purl> treat the scoped package as an npmjs package:
- They send
GET <default registry>/@scope%2fpkg/1.0.0 with no credentials, which leaks the private package name to the public registry.
- If the public registry has no package by that name, the unwind fails.
status: partial_failure, exit 1, hosted.failed: "cannot restore … HTTP 404 Not Found; restore it from version control instead". Bun itself resolves the package fine, so this refusal shouldn't fire.
- If the public registry has a package with the same name and version (a dependency-confusion squat, or an unrelated public package), the unwind reports
success, exit 0, with no warning. It writes ["@scope/pkg@1.0.0", "", {}, "sha512-<public package's integrity>"]. Then:
Impact
Any Bun project whose private registry is configured at user level, which is the common shape for private scopes, can't be safely unwound from hosted mode. In the best case the unwind refuses even though Bun can resolve the package. In the worst case it exits 0 with a lock that breaks CI on current Bun, or on Bun ≤ 1.3.6 installs a public package of the same name in place of the private one. The same lookup also sends private package names to the public registry.
Repro (Linux, main 3380e28, Bun 1.4.2)
A local mock on :8781 serves the authenticated patch API (--api-url), the hosted tarball, a private registry at /reg/ (requires Authorization: Bearer s3cret), and a stand-in for the public registry at /npmjs/. In the squat variant, /npmjs/ serves its own @scope/pkg@1.0.0. SOCKET_PATCH_SERVER_URL and SOCKET_NPM_REGISTRY point at the mock.
export HOME=$PWD/home; mkdir -p home proj; cd proj
printf '@scope:registry=http://127.0.0.1:8781/reg/\n//127.0.0.1:8781/reg/:_authToken=s3cret\n' > ~/.npmrc # user-level only
echo '{"name":"app","version":"1.0.0","dependencies":{"@scope/pkg":"1.0.0"}}' > package.json
echo node_modules/ > .gitignore
bun install
# bun.lock: "@scope/pkg": ["@scope/pkg@1.0.0", "http://127.0.0.1:8781/reg/@scope/pkg/-/pkg-1.0.0.tgz", {}, "sha512-wzqt…"]
git init -q && git add -A && git commit -qm base
socket-patch scan --mode hosted --json --yes # redirected 1
socket-patch rollback --json --yes # (same with `remove pkg:npm/@scope/pkg@1.0.0`)
# mock log: GET /npmjs/@scope%2fpkg/1.0.0 auth=None ← private name sent to the public registry
# no public package → status partial_failure, exit 1, "cannot restore … HTTP 404 Not Found"
# public squat → status success, exit 0, no warning, and bun.lock is now:
# "@scope/pkg": ["@scope/pkg@1.0.0", "", {}, "sha512-BLur…"] (the public package's integrity)
git commit -qam undo; git clone -q . ../fresh; cd ../fresh
bun install --frozen-lockfile # empty cache
# 1.4.2: error: Integrity check failed for tarball: @scope/pkg (exit 1)
# 1.2.23 / 1.3.6: GET https://registry.npmjs.org/@scope/pkg/-/pkg-1.0.0.tgz (fetches the public package)
Control: put the same three lines in the project's .npmrc instead. The restore then sends GET /reg/@scope%2fpkg/1.0.0 with Bearer s3cret, bun.lock comes back byte-exact, and the fresh frozen install gets the original bytes. That holds on 1.2.23 and 1.4.2.
Expected vs actual
- Expected: CLI_CONTRACT.md (hosted unwind, npm family) says "The registry is the one Bun resolves the package against: a scope's
.npmrc @scope:registry or bunfig.toml [install.scopes] entry, else …", and the restored lock must install as the pre-patch lock did. Bun resolves against the user ~/.npmrc and the global bunfig as well as the project files. If the CLI can't determine the registry, it should refuse with the checkout remedy, not query npmjs and write "".
- Actual: user-level config is ignored. The restore uses the default registry's document, writes
"" plus that document's integrity, and exits 0 when a same-name package exists there.
Matrix (Linux; each cell is a fresh project, cold-cache fresh-clone frozen install)
| Bun |
lock |
where the scope is configured |
public same-name pkg |
rollback result |
fresh frozen install |
| 1.4.2 |
text v2 |
~/.npmrc |
yes |
success, "" + public integrity (×3) |
fail: IntegrityCheckFailed |
| 1.4.2 |
text v2 |
~/.bunfig.toml [install.scopes] |
yes |
success, "" + public integrity |
fail: IntegrityCheckFailed |
| 1.4.2 |
text v2 |
~/.npmrc |
no |
partial_failure, exit 1 (404 from the public registry) |
n/a |
| 1.4.2 |
text v2 |
~/.npmrc, remove <purl> |
yes |
success, same lock |
(same as rollback) |
| 1.3.6 |
text v1 |
~/.npmrc |
yes |
success, "" + public integrity |
fetches registry.npmjs.org |
| 1.2.23 |
text v1 |
~/.npmrc |
yes |
success, "" + public integrity |
fetches registry.npmjs.org |
| 1.2.23 / 1.4.2 |
text |
project .npmrc (control) |
yes |
success, byte-exact |
pass, original bytes |
| macOS / Windows |
— |
— |
— |
untested (no probe branches this run). ~/.npmrc lookup is platform-independent; Windows uses %USERPROFILE%\.npmrc |
— |
First bad: not a regression. Before #1009 the slot was always "" (#992). #1009 fixed the project-level case only.
Suspect code
Possible directions: read Bun's user and global config layers in Bun's order. Or, when the lock's existing tuple recorded a non-default tarball URL and no project-level config explains it, refuse with the checkout remedy instead of falling back to the default registry.
[agent] Found by the scheduled Bun bug-hunt routine (ledger #306).
Summary
#1009 (the fix for #992) made the hosted
bun.lockrestore look up the registry Bun resolves each package against. ButBunRegistrySettings::readonly reads the.npmrcandbunfig.tomlnext to the lock. Bun also reads the user's~/.npmrcand global~/.bunfig.toml($XDG_CONFIG_HOME/.bunfig.toml). That is where private-scope registries and their tokens usually live, because tokens aren't committed andnpm login --scope=@x --registry=…writes~/.npmrc.If
@scope:registry(or[install.scopes]) is configured only at user level,rollbackandremove <purl>treat the scoped package as an npmjs package:GET <default registry>/@scope%2fpkg/1.0.0with no credentials, which leaks the private package name to the public registry.status: partial_failure, exit 1,hosted.failed: "cannot restore … HTTP 404 Not Found; restore it from version control instead". Bun itself resolves the package fine, so this refusal shouldn't fire.success, exit 0, with no warning. It writes["@scope/pkg@1.0.0", "", {}, "sha512-<public package's integrity>"]. Then:""through the user's scope config, downloads the private tarball and failsIntegrityCheckFailed. Every cold frozen install breaks.""ashttps://registry.npmjs.org/(see Hosted Bun rollback/remove writes an empty registry slot that Bun < 1.3.7 resolves against npmjs, so custom-registry projects can't frozen-install after a revert #992), so a freshbun install --frozen-lockfilefetches the public package, and the integrity pin, which socket-patch copied from the public package, matches it. I could only show this up to theGET https://registry.npmjs.org/@scope/pkg/-/pkg-1.0.0.tgzrequest (404 in the sandbox), because I can't publish to real npmjs. But the lock bytes are exactly what that install would accept.Impact
Any Bun project whose private registry is configured at user level, which is the common shape for private scopes, can't be safely unwound from hosted mode. In the best case the unwind refuses even though Bun can resolve the package. In the worst case it exits 0 with a lock that breaks CI on current Bun, or on Bun ≤ 1.3.6 installs a public package of the same name in place of the private one. The same lookup also sends private package names to the public registry.
Repro (Linux, main
3380e28, Bun 1.4.2)A local mock on
:8781serves the authenticated patch API (--api-url), the hosted tarball, a private registry at/reg/(requiresAuthorization: Bearer s3cret), and a stand-in for the public registry at/npmjs/. In the squat variant,/npmjs/serves its own@scope/pkg@1.0.0.SOCKET_PATCH_SERVER_URLandSOCKET_NPM_REGISTRYpoint at the mock.Control: put the same three lines in the project's
.npmrcinstead. The restore then sendsGET /reg/@scope%2fpkg/1.0.0withBearer s3cret,bun.lockcomes back byte-exact, and the fresh frozen install gets the original bytes. That holds on 1.2.23 and 1.4.2.Expected vs actual
.npmrc@scope:registryorbunfig.toml[install.scopes]entry, else …", and the restored lock must install as the pre-patch lock did. Bun resolves against the user~/.npmrcand the global bunfig as well as the project files. If the CLI can't determine the registry, it should refuse with the checkout remedy, not query npmjs and write"".""plus that document's integrity, and exits 0 when a same-name package exists there.Matrix (Linux; each cell is a fresh project, cold-cache fresh-clone frozen install)
rollbackresult~/.npmrc""+ public integrity (×3)~/.bunfig.toml[install.scopes]""+ public integrity~/.npmrc~/.npmrc,remove <purl>~/.npmrc""+ public integrity~/.npmrc""+ public integrity.npmrc(control)~/.npmrclookup is platform-independent; Windows uses%USERPROFILE%\.npmrcFirst bad: not a regression. Before #1009 the slot was always
""(#992). #1009 fixed the project-level case only.Suspect code
crates/socket-patch-core/src/patch/redirect/upstream/npm.rs:1807:BunRegistrySettings::readreads only{dir}/.npmrcand{dir}/bunfig.toml. There's no~/.npmrc(orNPM_CONFIG_USERCONFIG) and no$XDG_CONFIG_HOME/.bunfig.toml/~/.bunfig.toml.bun.lockbtakeover restore (upstream/bun_lockb.rs:107). Not exercised this run.::__archiveUrl=binding when the registry comes fromnpmScopes,YARN_NPM_REGISTRY_SERVER,~/.yarnrc.ymlor a parent-dir.yarnrc.yml(#908 fix covers only the project rc's top-level key) #1017 (the yarn berry twin:~/.yarnrc.yml/npmScopesignored by the 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 fix), Hosted Bun rollback/remove writes an empty registry slot that Bun < 1.3.7 resolves against npmjs, so custom-registry projects can't frozen-install after a revert #992, Hosted pnpm rollback/remove restorespnpm-lock.yamlfrom npmjs's version document instead of the project's.npmrcregistry, so a mirror project loses itstarball:URL (cold frozen install 404s) or is moved to npmjs #919.Possible directions: read Bun's user and global config layers in Bun's order. Or, when the lock's existing tuple recorded a non-default tarball URL and no project-level config explains it, refuse with the checkout remedy instead of falling back to the default registry.