Repository navigation
Fix vendored npm/Bun revert treating an upgrade as drift (#1155) - #1187
Mikola Lysenko (mikolalysenko) merged 15 commits into
Conversation
Moving a vendored package to another version (`npm install pkg@x`, `bun update`, a Dependabot bump) left the vendored copy and its ledger entry stuck. `scan --prune`, `vendor --revert`, `remove` and `rollback` called the moved lock entry "drift" and kept everything, so `vendor --check` stayed red and every remedy it named looped. A lock entry that now locks a different version than the one vendored means the vendored version left the lock graph, the same as after `npm uninstall`. The npm and Bun text-lock reverts now report it as `vendor_lock_entry_removed`, leave the user's lock untouched, and delete the artifact once no lock resolves through it. A re-resolution at the same version is still drift and still keeps the artifact. Fixes #1155 Assisted-by: Claude Code:claude-opus-5-5
Covers the `scan --prune` leg of #1155 end to end: after `npm install left-pad@1.3.1` over a vendored 1.3.0, one prune run reverts the entry, removes the artifact and leaves the user's lock byte-identical. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
A lock entry that kept the vendored key but changed its version was reverted as an upgrade no matter where it resolved. An edited lock could point that entry at any tarball, and `scan --prune`, `remove` or `rollback` would then delete the vendored copy and turn `vendor --check` green. An upgrade now has to be the same package from the registry the pre-vendor entry used: for npm the new `resolved` must share the original's `<registry>/<name>/-/` tarball directory, and for Bun the spec's package name and the tuple's registry field must match. Anything else stays drift, keeps the artifact and leaves `vendor --check` red. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Older npm locks record `http://` registry tarball URLs, and the next `npm install` rewrites them to `https://`. An upgrade made that way was still called drift, so the vendored copy stayed stuck as in #1155. A move from `http` to `https` on the same registry now counts as an upgrade; a move from `https` down to `http` stays drift. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
An upgraded npm entry was accepted when its `resolved` sat under the recorded registry directory and ended in `.tgz`. A URL written with backslashes or `..` passed that check, but npm normalizes it before fetching, so it could point at another package's tarball while the vendored copy was deleted. The tarball file must now be exactly `<name>-<version>.tgz`, with the name taken from the pre-vendor tarball and a plain version; anything else stays drift. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
BugBot review Generated by Claude Code |
Lockfile v1 alias rows store their version as `npm:left-pad@1.3.0`, so the new exact tarball-name check never matched them and a real alias upgrade was still kept as drift. The tarball version is now read from after the last `@` of an `npm:` spec before the same strict check runs. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[agent] The failure: one test, Why it isn't this PR's:
Fix: none exists yet. I haven't re-run the job: the head has moved to 264b6ff, whose CI run includes the same Windows leg, so I'll use that result. If it fails the same way on 264b6ff (or on Generated by Claude Code |
An entry npm installs from a git, URL or `file:` spec could still be read as a registry upgrade when its lock `resolved` was written in the registry tarball shape. npm ci installs such an edge from the spec, so the revert deleted the vendored copy while something else got installed. The revert now reads the same non-registry edge set the vendor scan uses (including a registry override's rescue, #490) and keeps any such entry as drift. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
Two more ways an edited project could make the revert delete a vendored copy while something else gets installed: - npm: a package.json override can swap a registry edge for a git, URL or file: spec, which npm ci installs instead of the lock's resolved tarball. While any override names the vendored package, a version change now stays drift. - Bun: an empty registry field means the default registry, which a committed bunfig.toml or .npmrc can rebind. When either file names a registry, an upgrade from the default registry now stays drift. Both fall back to the pre-#1155 behavior, which keeps the vendored copy, whenever the project's own config could change the install source. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
[final reviewer] Tanmay Singla (@Tanmay182003) Four non-merge commits landed after your approval on
All four make the revert more conservative: when in doubt it keeps the vendored copy, which is how it behaved before #1155. CI is green and the PR is mergeable at Generated by Claude Code |
|
[burn-down agent] Labeled Ready for review at
Generated by Claude Code |
…resolved-version-drift
#1147 landed a bun.lockb revert that calls bun_lock::revert_one_record for a migrated text record, and main moved npm_lock's npm_origin import. Both broke against this branch in the merge queue (clippy: E0061, E0425). Pass `false` for the new default-registry argument from the bun.lockb path, which keeps a moved default-registry tuple there as drift: bun.lockb stays outside the #1155 upgrade path. Import legacy_packages_key again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mo7HM9gyRkUAMxWqi62Dxz
|
[agent] The merge queue dropped this PR because
Fixed in 0c50224, on top of a merge of
Checked locally on the merged tree:
The push dismisses earlier approvals if branch protection is set to, so this needs a re-approval before it can be queued again. Generated by Claude Code |
|
[agent] Bun patch compatibility (every Root cause.
How the other failures follow from it:
What it needs: a fix outside this PR that moves the live-service suites ( Generated by Claude Code |
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0c50224. Configure here.
Port of #1301 (fixes #1293). Production withdrew the free minimist@1.2.2 patch 80630680 and republished the fix as 642d7f02, which turned hosted-e2e, e2e_safety_pnpm and every Bun native leg red here as on main. The vlt harness also now reads the republished patch's unprefixed file keys. Test-only; a no-op once main carries #1301. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mo7HM9gyRkUAMxWqi62Dxz
|
[agent] Production republished the minimist fix as patch The port changes:
This is a no-op once #1301 lands. CI now re-runs Generated by Claude Code |
|
[final reviewer] I disarmed auto-merge (it was re-armed at 18:23Z). Non-merge commits since your approval at
Tanmay Singla (@Tanmay182003), please take another look; re-approving lets it go to the queue. Generated by Claude Code |
| /// registry" trusts that move only when this is false (#1155). User- and | ||
| /// machine-level config files (`~/.npmrc`, `~/.bunfig.toml`, the global | ||
| /// npmrc) are not read. | ||
| pub(super) async fn project_may_redirect_registry(project_root: &Path) -> bool { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: HIGH
Description: Version-move cleanup trusts a lock URL prefix as the same registry and deletes the vendored patch, but project_may_redirect_registry ignores the npm/Bun layers that rewrite that URL at fetch time. With registry set only in ~/.npmrc, the global npmrc, or a builtin npmrc, npm's default replace-registry-host=npmjs still fetches a https://registry.npmjs.org//-/-.tgz lock URL from that other registry. Bun does the same for an empty registry field when only ~/.bunfig.toml or $XDG_CONFIG_HOME/.bunfig.toml sets install.registry. Env keys other than NPM_CONFIG_REGISTRY, npm_config_registry, and BUN_CONFIG_REGISTRY are also unread: npm_config_@scope:registry redirects scoped packages, and npm_config_strict_ssl / npm_config_cafile change the TLS identity. npm_config_replace_registry_host matters when a registry is set in one of those unread layers; it does not by itself retarget a registry.npmjs.org URL whose effective registry is still registry.npmjs.org. A normal https-proxy does not substitute the tarball body. version_moved_off never checks the lock integrity against the recorded host; npm/bun only check the bytes they download against the integrity the lock editor wrote.
Impact: A lockfile edit that keeps the pre-vendor registry tarball prefix (or Bun's empty registry field) and changes the version is classified as a clean removal. rollback, remove, and scan --prune then delete .socket/vendor/npm/ and leave vendor --check green, while npm ci or bun install still fetches from a registry or TLS context the recorded URL does not name. If that origin serves bytes matching the lock integrity, those bytes are what gets installed. Before this change the same edit stayed drift, so the patched tarball was kept and the check stayed red.
Remediation: Do not take the version-move cleanup path unless every fetch-affecting npm/Bun layer is known not to rewrite the host or TLS identity. Reuse the existing outer-layer resolution (env, project, user, global, builtin, and ~/.bunfig.toml / $XDG_CONFIG_HOME/.bunfig.toml) for registry, scope registry, replace-registry-host, cafile, and strict-ssl. If any of those is set or unreadable, keep the pre-change drift verdict and the artifact. A proxy setting alone need not fail the check.
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1155
Summary
Upgrading or downgrading a vendored package (
npm install left-pad@1.3.1,bun update, a Dependabot bump) now reverts cleanly in a plain project. Before,scan --prune,vendor --revert,removeandrollbackkept the vendored artifact and ledger entry as "drift".vendor --checkthen stayed red, and every remedy it named looped. The only way out was deleting.socket/vendor/npm/<uuid>/and editingstate.jsonby hand.Root cause
After vendoring, an upgrade keeps the same lock key but locks a new version from the registry. The npm revert (
npm_lock.rsrevert_one_record) and the Bun text-lock revert (bun_lock.rs) decided whether a record was still ours only by asking whetherresolvedpoints into our uuid dir. Any other resolution warnedvendor_lock_entry_drifted, andRevertOutcome::drift_skipped()then keeps the artifact and the ledger entry. "Undo the drift" would mean downgrading back to the vulnerable version.Fix
A live entry that is a same-package version move on the same registry means the vendored version left the lock graph, which is the same situation as
npm uninstall. Both backends now emitvendor_lock_entry_removedfor it, with the new version in the detail. The existing #665 gate (keep_artifact_while_lock_references_it) then deletes the artifact only once no lock still names the uuid, and keeps it otherwise.Because this path deletes the only patched copy, it trusts a version move only when nothing visible could make the install come from somewhere else. In every other case the revert drift-keeps exactly as before #1155.
Project gate (both backends,
npm_common::project_may_redirect_registry). The project must have:overridesinpackage.json(npm);.npmrcorbunfig.toml. Blank and comment lines are fine; an unreadable file counts as a setting.NPM_CONFIG_REGISTRY,npm_config_registryorBUN_CONFIG_REGISTRYin the environment.Entry check, npm. All of the following must hold:
versiondiffers from bothrec.newandrec.original.resolvedis exactly<registry>/<name>/-/<basename>-<version>.tgz, using the pre-vendor tarball's directory and file stem. That leaves no room for\,.., extra segments, a query or a fragment.<version>is a plain[A-Za-z0-9.+-]token. A legacy v1 alias row'snpm:left-pad@1.3.1is read as1.3.1.httptohttps(npm does this to old npm 6 locks).npm_non_registry_entries, so no git / URL /file:spec edge qualifies.Entry check, Bun. The same package name and the same registry field as the pre-vendor tuple, with a different plain version.
The provenance rules were tightened across 8294b94, d1428a3, 4b1630c, 264b6ff, f379b2d, 5c52a32, c5c0736 and 949593f in response to Bugbot.
Open questions for a maintainer. Two Bugbot threads are left open on purpose:
~/.npmrc, the global npmrc,~/.bunfig.toml) can also redirect fetches. Reading those would make the revert depend on who runs it.~/.npmrcauth lines would also disable the fix almost everywhere.The decision is whether to record the registry at vendor time and/or read machine config. Both are follow-ups.
Scope
package-lock.json/npm-shrinkwrap.json(legacy v1 and v2/v3 entries) and Bunbun.lock, in projects with no install-source config.bun.lockb, which Fix vendored revert reading a removed dependency as drift (#1132, #1140, #1142) #1147 is reworking (it's in the merge queue). A follow-up can apply the same rule there once that lands.npm/,pypi/andgem/only dispatch to the binary.One existing test changed.
bun_lock::tests::revert_leaves_drifted_entries_alone_with_warningusedbun updateto 1.3.1 as its drift example, which is exactly the #1155 behavior. It now drifts the entry to a same-version fork URL, so its drift and keep-gate-liveness coverage stays. The 1.3.1 case moved to a new test that expects the revert.Tests
mainvendor::npm_lock::tests::revert_after_version_change_drops_the_unreferenced_artifact!drift_skipped()vendor::npm_lock::tests::revert_after_partial_downgrade_restores_the_rest_and_drops_artifact!drift_skipped()http://lock upgraded tohttps://vendor::npm_lock::tests::revert_after_version_change_accepts_http_to_https_upgrade_only.npmrcvendor::npm_lock::tests::revert_after_version_change_ignores_a_comment_only_npmrcvendor::npm_lock::tests::version_moved_off_reads_legacy_alias_versionsbun updatevendor::bun_lock::tests::revert_after_version_change_drops_the_unreferenced_artifact!drift_skipped()rollbackandvendor --checkin_process_vendor::rollback_after_version_upgrade_cleans_up_and_convergesremovein_process_vendor::remove_after_version_upgrade_reverts_vendoringscan --prunescan_vendor_e2e::scan_prune_reverts_vendored_entry_after_version_upgradekeptVendoredEntries: [pkg:npm/left-pad@1.3.0]Guards (negative cases; the artifact is kept):
revert_keeps_artifact_when_version_changed_but_uuid_still_referencedrevert_keeps_version_change_resolved_off_the_recorded_registry_as_driftrevert_keeps_version_change_behind_a_non_registry_spec_as_driftrevert_keeps_version_change_as_drift_while_any_override_is_declaredrevert_keeps_version_change_as_drift_while_a_project_npmrc_sets_anything(registry, scoped registry, replace-registry-host, proxy + strict-ssl, cafile)legacy_pointer_maps_to_its_packages_keyhttps→httphalf of the scheme test, and the off-tarball half of the alias testrevert_keeps_version_change_from_another_registry_as_driftrevert_keeps_default_registry_upgrade_as_drift_when_the_registry_is_configuredLocal runs (on 949593f)
cargo clippy --workspace --all-features -- -D warnings: clean.rustfmt --checkon every changed file: clean.mainitself isn'tcargo fmt --all-clean, so I didn't reformat unrelated files.cargo test -p socket-patch-core --all-features --lib -- vendor::npm_lock vendor::bun_lock vendor::npm_origin vendor::npm_common: 258/258 passed.vendor::module on c5c0736: 2,415 passed, 2 failed. Both failures also fail onmainhere: they're chmod tests, and the container runs as uid 0.in_process_vendor: 124/124 passed.scan_vendor_e2e: 38/38 passed.CI
test (windows-latest, 1)failed once onlock_inventory::view::tests::a_read_set_notices_a_changed_file, a test that arrived with Decide whether a hosted patch is pinned through lockfile discovery alone #1058 onmain. The same leg passed on every later head.🤖 Generated with Claude Code
https://claude.ai/code/session_01Mo7HM9gyRkUAMxWqi62Dxz
Generated by Claude Code
Note
Medium Risk
Changes vendored revert/rollback semantics and can delete
.socket/vendorartifacts when locks show a trusted registry upgrade; misclassification could drop a still-needed patch, though guards limit that to “plain” upgrades with no redirecting config.Overview
Fixes #1155: when a vendored npm/Bun package is bumped to another registry version (e.g.
npm install left-pad@1.3.1or Dependabot), rollback, remove, scan --prune, and lock revert no longer treat that as permanent drift that keeps the artifact and leavesvendor --checkstuck.npm (
npm_lock) and Bun text lock (bun_lock) now detect a same package, same registry, different version move and emitvendor_lock_entry_removedinstead ofvendor_lock_entry_drifted, so cleanup matches an uninstall once nothing in the lock still resolves through the vendor uuid. The user’s upgraded lockfile is left unchanged.That “plain upgrade” path is only taken when install provenance looks trustworthy: no
package.jsonoverrides, no meaningful project.npmrc/bunfig.tomlor registry env vars (project_may_redirect_registry), plus strict checks onresolved/ tarball shape (npm) or registry tuple fields (Bun). Other cases (git/URL/file specs, wrong host, overrides, configured default registry for Bun, uuid still referenced) still drift-keep as before.Adds CLI e2e coverage for rollback/remove/scan prune and a large set of unit tests for positive reverts and negative guards.
bun.lockband pnpm/yarn are out of scope; one Bun drift test now uses a same-version fork URL instead of a version bump.Reviewed by Cursor Bugbot for commit 0c50224. Configure here.