Skip to content

Commit 65aa074

Browse files
committed
Restore hosted bun.lockb pins natively for the vendor takeover
With the hosted ledger gone, `vendor` over a live hosted pin in a binary bun.lockb (Bun 0.8.1-1.1.x's default lock, Bun 1.2's legacy lock) was refused `redirect_revert_failed`: format_of() mapped bun.lockb to Unsupported, so the backtest matrix's hosted-then-vendored cell exited partial_failure. The upstream restore gains a bun.lockb restorer (patch/redirect/upstream/bun_lockb.rs). It rebuilds each hosted remote-tarball record as Bun's npm registry record for name@version, from the registry's dist.tarball / dist.integrity (SOCKET_NPM_REGISTRY aware), via the new BunLockb::set_registry_package. That function re-interns the URL (re-using the original pool offset), drops the hosted URL string from the pool tail and re-derives the metadata hash. The staged restore view now carries binary files. To make the rebuild byte-exact, set_package keeps the registry record's inactive bytes (padding, semver) when it writes a remote tarball record. Early writers leave uninitialized padding there (Bun 0.8.1), so this matters. A re-pin to a later grant's URL now drops the superseded URL from the pool. A record without the retained bytes is rebuilt the way Bun writes one; prerelease versions are refused in that case. The rebuild is exact for every fixture writer except a format-1 lock (kept promoted) and workspace locks (behaviors kept normalized). So only the vendor takeover and eject opt in (RestoreOptions::bun_lockb): their vendor ledger records the rebuilt record, and `vendor --revert` returns the pre-hosted bytes. `rollback` / `remove` keep refusing a hosted bun.lockb pin with the `git checkout -- bun.lockb` remedy, as the harness's rollbackLockbRefused expects. An offline vendor still refuses. Tests: core restorer tests over every real fixture, codec tests for the rebuild (retained and zeroed records, re-pin), a hermetic CLI test (vendor_eject_bun_lockb.rs: takeover, eject, rollback and offline refusals on Bun 0.8.1 / 1.1.38 / 1.2.0 locks), and the real-Bun e2e_bun_lockb takeover now vendors online and reverts byte-exact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tncU7u7FK48tubFNA12UQ
1 parent 08c30b7 commit 65aa074

13 files changed

Lines changed: 1127 additions & 41 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ into the new version's section — see docs/releasing.md.
9595
restore <purl> to its upstream registry entry: <why>; restore it from
9696
version control instead (`git checkout -- <files>`)`: every pin under
9797
`--offline` except Maven, a registry that does not answer or no longer
98-
describes the entry, a binary `bun.lockb` (always), a composer entry that
98+
describes the entry, a binary `bun.lockb` (for `rollback` / `remove`; see
99+
the vendor takeover below), a composer entry that
99100
is not packagist-sourced or whose `dist.reference` packagist no longer
100101
serves, a gem whose upstream section is ambiguous or not rubygems.org, a
101102
nuget id the restored config would not resolve from nuget.org alone, a
@@ -183,6 +184,21 @@ into the new version's section — see docs/releasing.md.
183184
live hosted pin: …`) and leaves it hosted. The cargo backend's
184185
`hosted_redirect_live` refusal now names `socket-patch rollback` and
185186
`git checkout -- Cargo.toml Cargo.lock` instead of the ledger.
187+
- **Vendoring over a hosted binary `bun.lockb` works again** (Bun 0.8–1.1's
188+
default lock and Bun 1.2's legacy lock; it was refused
189+
`redirect_revert_failed` once the hosted ledger was gone). The takeover
190+
and the eject rebuild each hosted remote-tarball record as Bun's npm
191+
registry record for `name@version` from the registry's `dist.tarball` /
192+
`dist.integrity`, re-derive the package metadata hash, drop the hosted
193+
URL from the string pool, then vendor; `vendor --revert` returns the
194+
pre-hosted lock. The hosted rewrite now keeps the registry record's
195+
inactive bytes (padding, semver) in the tarball record it writes, and a
196+
re-pin to a later grant's URL drops the superseded URL from the string
197+
pool, so the rebuild is byte-exact — early writers' uninitialized padding included —
198+
except for a binary format 1 lock (kept promoted to format 2) and a
199+
workspace lock (dependency behaviors kept normalized). Those exceptions
200+
keep `rollback` / `remove` refusing a hosted `bun.lockb` pin with the
201+
`git checkout -- bun.lockb` remedy; an `--offline` vendor still refuses.
186202
- **`vendor_supersedes_redirect` is removed.** The vendored flows no longer
187203
warn about (or auto-reconcile, or unwind the `.npmrc` for) a stale hosted
188204
ledger record: once the lock routes a package to `.socket/vendor/`, no

‎README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,9 @@ clean tree and an empty store, then check with `socket-patch vex`. See
351351
352352
Both text `bun.lock` and binary `bun.lockb` support hosted and vendored
353353
patches, mode switching, repair, and rollback — with one exception: a hosted
354-
`bun.lockb` entry cannot be restored to its upstream registry entry, so
355-
`rollback`, `remove` and switching it to vendored mode refuse it and point you at
356-
`git checkout -- bun.lockb`. Binary locks are read and
354+
`bun.lockb` entry is not rolled back to its upstream registry entry, so
355+
`rollback` and `remove` refuse it and point you at `git checkout -- bun.lockb`
356+
(switching it to vendored mode rebuilds the registry entry and works). Binary locks are read and
357357
patched natively: Socket Patch does not need Bun installed to discover or
358358
rewrite them, and does not convert them to text. If both filenames exist,
359359
`bun.lock` takes precedence. See [Bun compatibility](docs/testing/bun-compatibility.md)

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

‎crates/socket-patch-cli/src/commands/rollback.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,9 @@ pub(crate) async fn run_hosted_leg(common: &GlobalArgs, pins: &[HostedPin]) -> H
10571057
dry_run: common.dry_run,
10581058
offline: common.offline,
10591059
patch_server_origins: origins,
1060+
// A binary bun.lockb pin refuses with the checkout remedy: its
1061+
// rebuilt registry record is not byte-exact for every lock.
1062+
bun_lockb: false,
10601063
};
10611064
let outcome = restore_upstream(&common.cwd, pins, &opts).await;
10621065
for pin in &outcome.pins {

‎crates/socket-patch-cli/src/commands/vendor.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,7 @@ async fn run_eject(args: &VendorArgs, pins: Vec<HostedPin>) -> i32 {
11231123
dry_run: true,
11241124
offline: common.offline,
11251125
patch_server_origins: origins.clone(),
1126+
bun_lockb: true,
11261127
},
11271128
)
11281129
.await;
@@ -1222,6 +1223,7 @@ async fn run_eject(args: &VendorArgs, pins: Vec<HostedPin>) -> i32 {
12221223
dry_run: false,
12231224
offline: common.offline,
12241225
patch_server_origins: origins,
1226+
bun_lockb: true,
12251227
},
12261228
)
12271229
.await;
@@ -2954,6 +2956,7 @@ pub(crate) async fn vendor_records_reusing(
29542956
dry_run: common.dry_run,
29552957
offline: common.offline,
29562958
patch_server_origins: origins,
2959+
bun_lockb: true,
29572960
},
29582961
)
29592962
.await;

‎crates/socket-patch-cli/tests/e2e_bun_lockb.rs‎

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
//! Real Bun binary-lock acceptance tests. The CLI must never invoke a Bun
22
//! conversion or replace bun.lockb with text. Every terminal mode is checked
33
//! with an empty-cache frozen install, and rollback restores the exact input
4-
//! (v5: a hosted pin in a binary lock is refused by rollback and by a vendor
5-
//! takeover — only bun can re-encode its upstream entry — and restored from
6-
//! version control instead).
4+
//! (v5: a hosted pin in a binary lock is refused by rollback — restored from
5+
//! version control instead — while a vendor takeover rebuilds its registry
6+
//! record from the npm registry, refusing only offline).
77
//!
88
//! Run scripts/backtest-bun-lockb.py for the writer/reader release matrix.
99
//! SOCKET_PATCH_BUN_LOCKB_REQUIRED=1 makes missing tools a hard error;
@@ -65,8 +65,14 @@ fn require_success(output: Output, label: &str) -> Output {
6565
}
6666

6767
fn cli(project: &Path, args: &[&str]) -> Value {
68+
cli_env(project, args, &[])
69+
}
70+
71+
/// [`cli`] with extra environment variables.
72+
fn cli_env(project: &Path, args: &[&str], envs: &[(&str, &str)]) -> Value {
6873
let output = require_success(
6974
command(env!("CARGO_BIN_EXE_socket-patch"), project)
75+
.envs(envs.iter().copied())
7076
.args(args)
7177
.args([
7278
"--cwd",
@@ -110,10 +116,10 @@ fn cli_code(project: &Path, args: &[&str]) -> (i32, Value) {
110116
}
111117

112118
/// v5 keeps no hosted ledger, so undoing a hosted pin means restoring the
113-
/// entry's upstream registry form — which a binary `bun.lockb` cannot get
114-
/// from socket-patch (only bun can re-encode it). `rollback` therefore
115-
/// REFUSES the pin, naming the checkout remedy, and leaves the lock exactly
116-
/// as found; the test then applies that remedy (`git checkout --
119+
/// entry's upstream registry form. For a binary `bun.lockb` that rebuilt
120+
/// record is not byte-exact for every lock (format 1, workspace locks), so
121+
/// `rollback` REFUSES the pin, naming the checkout remedy, and leaves the
122+
/// lock exactly as found; the test then applies that remedy (`git checkout --
117123
/// bun.lockb`, here: the original bytes written back).
118124
fn rollback_refuses_binary_hosted_pin_then_checkout(fixture: &Fixture, server: &MockServer) {
119125
let hosted_lock = fixture.lock();
@@ -743,10 +749,13 @@ async fn native_binary_hosted_vendored_takeover_roundtrip() {
743749
std::fs::rename(&modules, project.join("node_modules")).unwrap();
744750

745751
// Hosted -> vendored: v5 restores a hosted pin's upstream entry before
746-
// vendoring over it, which a binary bun.lockb cannot get — the takeover
747-
// is REFUSED (dry and wet alike, nothing written) with the checkout
748-
// remedy. After `git checkout -- bun.lockb` the vendor proceeds, with a
749-
// truthful dry run and exact rerun state.
752+
// vendoring over it, re-resolving the registry record from the npm
753+
// registry — which an OFFLINE takeover cannot do, so it is REFUSED (dry
754+
// and wet alike, nothing written) with the checkout remedy. Online, the
755+
// takeover rebuilds the binary registry record exactly and vendors over
756+
// it; `vendor --revert` then gives back the original bytes. After that
757+
// (equivalently, after `git checkout -- bun.lockb`) the offline vendor
758+
// proceeds, with a truthful dry run and exact rerun state.
750759
fixture.stage();
751760
let uri = server.uri();
752761
let before = snapshot(project);
@@ -780,7 +789,69 @@ async fn native_binary_hosted_vendored_takeover_roundtrip() {
780789
"refused vendor {extra:?} wrote nothing"
781790
);
782791
}
783-
std::fs::write(project.join("bun.lockb"), &fixture.original_lock).unwrap();
792+
// The npm registry's version document for minimist@1.2.2 (the public
793+
// registry's values, which the original lock pins), served locally.
794+
let integrity = "sha512-rIqbOrKb8GJmx/5bc2M0QchhUouMXSpd1RTclXsB41JdL+VtnojfaJR+h7F9k18/4kHUsBFgk80Uk+q569vjPA==";
795+
let digest = base64::engine::general_purpose::STANDARD
796+
.decode(integrity.trim_start_matches("sha512-"))
797+
.unwrap();
798+
assert!(
799+
fixture.original_lock.windows(64).any(|w| w == digest.as_slice()),
800+
"the original lock pins the registry digest"
801+
);
802+
Mock::given(method("GET"))
803+
.and(path("/minimist/1.2.2"))
804+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"dist": {
805+
"tarball": "https://registry.npmjs.org/minimist/-/minimist-1.2.2.tgz",
806+
"integrity": integrity}})))
807+
.mount(&server)
808+
.await;
809+
let taken_over = cli_env(
810+
project,
811+
&["vendor", "--patch-server-url", &uri, "--vendor-source", "build"],
812+
&[("SOCKET_NPM_REGISTRY", &uri)],
813+
);
814+
assert_eq!(
815+
taken_over["summary"]["applied"], 1,
816+
"online vendor over the binary hosted pin: {taken_over}"
817+
);
818+
assert!(
819+
taken_over["events"].as_array().is_some_and(|events| events
820+
.iter()
821+
.any(|e| e["errorCode"] == "vendor_takeover_reverted_redirect")),
822+
"the takeover is reported: {taken_over}"
823+
);
824+
let vendor_lock = fixture.lock();
825+
assert!(
826+
!vendor_lock
827+
.windows(uri.len())
828+
.any(|w| w == uri.as_bytes()),
829+
"no hosted URL is left in bun.lockb"
830+
);
831+
let state: Value = serde_json::from_slice(
832+
&std::fs::read(project.join(".socket/vendor/state.json")).unwrap(),
833+
)
834+
.unwrap();
835+
let original = state["entries"][PURL]["wiring"]
836+
.as_array()
837+
.and_then(|w| w.iter().find(|r| r["kind"] == "bun_lockb_package"))
838+
.map(|r| r["original"].clone())
839+
.unwrap_or_else(|| panic!("bun_lockb_package wiring: {state}"));
840+
assert_eq!(original["name"], "minimist", "{original}");
841+
assert_eq!(original["version"], "1.2.2", "{original}");
842+
assert_eq!(
843+
original["resolution"],
844+
"https://registry.npmjs.org/minimist/-/minimist-1.2.2.tgz",
845+
"the vendor ledger records the registry record: {original}"
846+
);
847+
fixture.frozen("taken-over", &fixture.patched, "minimist");
848+
let reverted = cli(project, &["vendor", "--revert", "--offline"]);
849+
assert_eq!(
850+
fixture.lock(),
851+
fixture.original_lock,
852+
"the revert restores the pre-hosted bytes exactly: {reverted}"
853+
);
854+
assert!(!project.join(".socket/vendor").exists(), "{reverted}");
784855
let before = snapshot(project);
785856
let preview = cli(project, &["vendor", "--offline", "--dry-run"]);
786857
assert_eq!(snapshot(project), before, "vendor dry run: {preview}");

0 commit comments

Comments
 (0)