You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
npm vendored refuses a registry package with vendor_workspace_member whenever a local file: directory (or workspace member) has the same name@version, and the hosted→vendored takeover then un-hosts it, leaving it unpatched #688
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
scan_lock_matches in the npm vendored backend returns LockScan::WorkspaceMember as soon as it sees anypackages key outside node_modules/ whose name@version matches the patch, before it looks at the other entries. A project that has both:
a normal registry install, node_modules/left-pad → https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz, and
an unrelated local directory dependency whose package.json happens to say left-pad@1.3.0 (for example a fork under third_party/, consumed as "lp-local": "file:./third_party/left-pad", which gives the lock key third_party/left-pad)
gets the whole package refused: vendor_workspace_member: third_party/left-pad is a workspace member of this project; patch the source directly instead of vendoring it. But the patch target is the registry copy, which is fully rewritable. Hosted mode handles the same project correctly: it pins node_modules/left-pad, leaves the file: link alone, npm ci installs the patched bytes, and vex attests.
The refusal also hits the hosted→vendored takeover, which is the #659 ordering problem on a v2/v3 lock. scan --mode vendored and get <uuid> --mode vendored on the hosted project first restore the hosted pin to upstream (vendor_takeover_reverted_redirect) and delete .npmrc. Then they hit the refusal and exit 1 without restoring the pin. The package ends up patched in neither mode, and the next npm ci installs the upstream bytes. (vendor eject runs the same refusal but rolls back correctly to hosted.)
Impact
Vendored mode can't patch a registry package at all when the repo also carries a local copy, fork or workspace member with the same name@version. The error tells the user to "patch the source directly", which doesn't apply to a registry copy.
A hosted→vendored switch through scan/get silently drops a working hosted patch: exit 1, but the project is left unpatched with .npmrc removed and no ledger.
Repro (main 045d7ec, Linux; local mock patch API serving left-pad@1.3.0)
mkdir -p p/third_party/left-pad &&cd p
printf'{"name":"left-pad","version":"1.3.0","main":"index.js"}\n'> third_party/left-pad/package.json
printf'module.exports = () => "first-party fork";\n'> third_party/left-pad/index.js
echo'{"name":"p","version":"1.0.0","private":true,"dependencies":{"left-pad":"1.3.0","lp-local":"file:./third_party/left-pad"}}'> package.json
npm i # lock: "node_modules/left-pad" (registry), "node_modules/lp-local" (link), "third_party/left-pad" {name: left-pad, version: 1.3.0}
git init -q && git add -A && git commit -qm i
# 1. plain vendored: refused
socket-patch scan --mode vendored --json --yes <api flags>;echo$?# 1, vendor_workspace_member# 2. hosted works
socket-patch scan --mode hosted --json --yes <api flags># 0; npm ci → node_modules/left-pad patched; vex attests
git add -A && git commit -qm hosted
# 3. hosted → vendored takeover
socket-patch scan --mode vendored --json --yes <api flags>;echo$?# 1: vendor_takeover_reverted_redirect, then vendor_workspace_member
ls .npmrc; grep '"resolved"' package-lock.json # .npmrc gone; left-pad back on registry.npmjs.org; no .socket/vendor/state.json
rm -rf node_modules && npm ci && head -c 20 node_modules/left-pad/index.js # upstream bytes
socket-patch vex --output o.vex --json # exit 2, nothing to attest
Actual: one matching non-node_modules key refuses every copy, and the takeover's restore isn't undone.
Matrix (Linux, main 045d7ec)
npm
lockfileVersion
plain vendored
hosted (npm ci patched + vex)
takeover via scan
takeover via get <uuid>
vendor eject
8.19.4 (Node 22)
2
refused, exit 1
pass
un-hosted, then refused; fresh npm ci unpatched
un-hosted, then refused
refused, rolled back to hosted
10.9.4 (Node 22)
3
refused, exit 1 (x3)
pass
same (x2)
same
same
12.2.0 (Node 24)
3
refused, exit 1
pass
same
same
same
macOS / Windows: not run, because the decision is made purely on lock JSON. v4.0.0 refuses the plain vendored scan the same way, so it isn't a regression. The takeover half has the same shape as #659 (the v1 gate), but it's reached through a different gate on v2/v3 locks.
Suspect code
crates/socket-patch-core/src/vendor/npm_lock.rs:880-881: if !key.contains(NODE_MODULES_SEG) { return LockScan::WorkspaceMember { … } } returns early for the whole package. It should record the member and continue, and refuse only when matches ends up empty.
[agent] Found by the scheduled npm bug-hunt routine (ledger #302).
Summary
scan_lock_matchesin the npm vendored backend returnsLockScan::WorkspaceMemberas soon as it sees anypackageskey outsidenode_modules/whose name@version matches the patch, before it looks at the other entries. A project that has both:node_modules/left-pad→https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz, andleft-pad@1.3.0(for example a fork underthird_party/, consumed as"lp-local": "file:./third_party/left-pad", which gives the lock keythird_party/left-pad)gets the whole package refused:
vendor_workspace_member: third_party/left-pad is a workspace member of this project; patch the source directly instead of vendoring it. But the patch target is the registry copy, which is fully rewritable. Hosted mode handles the same project correctly: it pinsnode_modules/left-pad, leaves thefile:link alone,npm ciinstalls the patched bytes, andvexattests.The refusal also hits the hosted→vendored takeover, which is the #659 ordering problem on a v2/v3 lock.
scan --mode vendoredandget <uuid> --mode vendoredon the hosted project first restore the hosted pin to upstream (vendor_takeover_reverted_redirect) and delete.npmrc. Then they hit the refusal and exit 1 without restoring the pin. The package ends up patched in neither mode, and the nextnpm ciinstalls the upstream bytes. (vendoreject runs the same refusal but rolls back correctly to hosted.)Impact
scan/getsilently drops a working hosted patch: exit 1, but the project is left unpatched with.npmrcremoved and no ledger.Repro (main
045d7ec, Linux; local mock patch API servingleft-pad@1.3.0)Expected vs actual
vendor_workspace_member; see the Agent-mode apply writes through node_modules links into first-party source (npm workspace members, file: deps, npm link targets), overwriting the user's code, and rollback restores upstream bytes instead #626 discussion). A non-node_moduleskey should be skipped, likelink/inBundle/ non-registry entries are (with a warning), and the registry entries should still be rewired. The refusal should fire only when no rewritable entry remains. That is howvendor_lock_entry_not_rewritablealready works. Separately, the takeover should run the per-package gates before restoring the hosted pin. CLI_CONTRACT documents that rule for Bun ("Bun lock version, grammar and workspace compatibility are checked before a vendored takeover … these refusals preserve the existing lock") and berry (Hosted → vendored takeover on yarn berry reverts the hosted redirect before a per-package vendor refusal, leaving the package unpatched in both modes #369 / Fix berry mode takeover reverting before gates (#468, #369) #470), but npm doesn't follow it.node_moduleskey refuses every copy, and the takeover's restore isn't undone.Matrix (Linux, main
045d7ec)npm cipatched +vex)scanget <uuid>vendorejectnpm ciunpatchedmacOS / Windows: not run, because the decision is made purely on lock JSON. v4.0.0 refuses the plain vendored scan the same way, so it isn't a regression. The takeover half has the same shape as #659 (the v1 gate), but it's reached through a different gate on v2/v3 locks.
Suspect code
crates/socket-patch-core/src/vendor/npm_lock.rs:880-881:if !key.contains(NODE_MODULES_SEG) { return LockScan::WorkspaceMember { … } }returns early for the whole package. It should record the member andcontinue, and refuse only whenmatchesends up empty.npm_lock's per-package refusals run (same root cause as npm lockfileVersion 1: scan/get --mode vendored un-host a hosted patch and then refuse to vendor it, so the project silently goes back to unpatched (vendor eject rolls back correctly) #659; the berry fix for Hosted → vendored takeover on yarn berry reverts the hosted redirect before a per-package vendor refusal, leaving the package unpatched in both modes #369 is the model).