@@ -928,10 +928,22 @@ pub(crate) async fn revert_bun_opts(
928928 }
929929 }
930930
931+ // An empty registry field means "the default registry", which a
932+ // committed bunfig.toml or .npmrc can rebind; with either naming a
933+ // registry, an empty field proves nothing about where an upgrade
934+ // installs from (#1155).
935+ let default_registry_pinned = !project_configures_a_registry ( project_root) . await ;
931936 let mut dirty = false ;
932937 if let Some ( lines) = lines. as_mut ( ) {
933938 for rec in entry. wiring . iter ( ) . rev ( ) . filter ( |r| r. file == BUN_LOCK ) {
934- revert_one_record ( lines, rec, & entry. uuid , & mut dirty, & mut outcome. warnings ) ;
939+ revert_one_record (
940+ lines,
941+ rec,
942+ & entry. uuid ,
943+ default_registry_pinned,
944+ & mut dirty,
945+ & mut outcome. warnings ,
946+ ) ;
935947 }
936948 if dirty {
937949 if let Err ( e) = atomic_write_bytes_preserving_mode (
@@ -986,6 +998,7 @@ fn revert_one_record(
986998 lines : & mut [ String ] ,
987999 rec : & WiringRecord ,
9881000 entry_uuid : & str ,
1001+ default_registry_pinned : bool ,
9891002 dirty : & mut bool ,
9901003 warnings : & mut Vec < VendorWarning > ,
9911004) {
@@ -1043,7 +1056,7 @@ fn revert_one_record(
10431056 // exactly as after `bun remove`. Nothing to restore; the caller
10441057 // keeps the artifact only while the lock still resolves through
10451058 // it. A same-version re-resolution stays drift.
1046- if let Some ( live_version) = version_moved_off ( rec, & parsed) {
1059+ if let Some ( live_version) = version_moved_off ( rec, & parsed, default_registry_pinned ) {
10471060 warnings. push ( VendorWarning :: new (
10481061 super :: LOCK_ENTRY_REMOVED_CODE ,
10491062 format ! (
@@ -1104,16 +1117,36 @@ fn registry_name_version(entry: &BunEntry) -> Option<(String, String)> {
11041117/// keeps the caller's drift verdict: an `original: None` record, a URL or
11051118/// `file:` spec, another package or another registry is not a plain upgrade
11061119/// and must keep the artifact and leave `vendor --check` red.
1107- fn version_moved_off ( rec : & WiringRecord , live : & BunEntry ) -> Option < String > {
1120+ fn version_moved_off (
1121+ rec : & WiringRecord ,
1122+ live : & BunEntry ,
1123+ default_registry_pinned : bool ,
1124+ ) -> Option < String > {
11081125 let ( live_name, live_version) = registry_name_version ( live) ?;
11091126 let original = parse_entry_line ( rec. original . as_ref ( ) . and_then ( Value :: as_str) ?) . ok ( ) ?;
11101127 let ( original_name, original_version) = registry_name_version ( & original) ?;
1111- let same_registry = matches ! ( ( live. elems. get( 1 ) , original. elems. get( 1 ) ) ,
1112- ( Some ( l) , Some ( o) ) if decode_json_string( l) . is_some( ) && decode_json_string( l) == decode_json_string( o) ) ;
1128+ let live_registry = decode_json_string ( live. elems . get ( 1 ) ?) ?;
1129+ let same_registry = Some ( & live_registry) == decode_json_string ( original. elems . get ( 1 ) ?) . as_ref ( )
1130+ && ( !live_registry. is_empty ( ) || default_registry_pinned) ;
11131131 ( same_registry && live_name == original_name && live_version != original_version)
11141132 . then_some ( live_version)
11151133}
11161134
1135+ /// True when the project's own `bunfig.toml` or `.npmrc` mentions a
1136+ /// registry at all (or can't be read), so Bun's default registry may not
1137+ /// be npmjs. Deliberately coarse: any doubt keeps the drift verdict.
1138+ async fn project_configures_a_registry ( project_root : & Path ) -> bool {
1139+ for name in [ "bunfig.toml" , ".npmrc" ] {
1140+ match read_regular_to_string ( & project_root. join ( name) ) . await {
1141+ Ok ( text) if text. to_ascii_lowercase ( ) . contains ( "registry" ) => return true ,
1142+ Ok ( _) => { }
1143+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: NotFound => { }
1144+ Err ( _) => return true ,
1145+ }
1146+ }
1147+ false
1148+ }
1149+
11171150// ───────────────────────── vendor-specific classification ─────────────────
11181151// The conservative line grammar (`BunEntry`, `parse_*`, `scan_*`, …) lives in
11191152// `crate::vendor::bun_lock_text`; this module keeps only the vendor tuple
@@ -3256,6 +3289,45 @@ mod tests {
32563289 }
32573290 }
32583291
3292+ /// #1155 provenance guard: an empty registry field means "the default
3293+ /// registry", and a committed bunfig.toml or .npmrc can rebind that.
3294+ /// With either naming a registry, a version change from `""` is not
3295+ /// proven to come from the pre-vendor registry, so it stays drift.
3296+ #[ tokio:: test]
3297+ async fn revert_keeps_default_registry_upgrade_as_drift_when_the_registry_is_configured ( ) {
3298+ for ( file, text) in [
3299+ (
3300+ "bunfig.toml" ,
3301+ "[install]\n registry = \" https://evil.example.com/\" \n " ,
3302+ ) ,
3303+ ( ".npmrc" , "registry=https://evil.example.com/\n " ) ,
3304+ ] {
3305+ let fx = fixture_with ( BN3_BEFORE_LOCK , "node_modules/left-pad" ) . await ;
3306+ let ( _, entry, _) = expect_done ( fx. vendor ( false ) . await ) ;
3307+ let entry = entry. unwrap ( ) ;
3308+ let upgraded_line =
3309+ " \" left-pad\" : [\" left-pad@1.3.1\" , \" \" , {}, \" sha512-other==\" ]," ;
3310+ let live = fx. read_lock ( ) . await ;
3311+ let new_line = entry. wiring [ 0 ]
3312+ . new
3313+ . as_ref ( )
3314+ . and_then ( Value :: as_str)
3315+ . unwrap ( ) ;
3316+ tokio:: fs:: write (
3317+ fx. root ( ) . join ( BUN_LOCK ) ,
3318+ live. replace ( new_line, upgraded_line) ,
3319+ )
3320+ . await
3321+ . unwrap ( ) ;
3322+ tokio:: fs:: write ( fx. root ( ) . join ( file) , text) . await . unwrap ( ) ;
3323+
3324+ let outcome = revert_bun ( & entry, fx. root ( ) , false ) . await ;
3325+ assert ! ( outcome. success, "{file}: {:?}" , outcome. error) ;
3326+ assert ! ( outcome. drift_skipped( ) , "{file}: {:?}" , outcome. warnings) ;
3327+ assert ! ( outcome. kept_artifact, "{file}: {:?}" , outcome. warnings) ;
3328+ }
3329+ }
3330+
32593331 #[ tokio:: test]
32603332 async fn revert_leaves_drifted_entries_alone_with_warning ( ) {
32613333 let fx = fixture_with ( BN3_BEFORE_LOCK , "node_modules/left-pad" ) . await ;
0 commit comments