socket-patch supports hosted, vendored and agent-mode npm patches in Bun
projects using text bun.lock or native binary bun.lockb. Real-Bun evidence backs both formats:
- The native matrix —
scripts/backtest-bun.pyruns real Bun releases against the public free Socket patch forminimist@1.2.2(642d7f02-ebc1-4ab0-99e2-07f5dd8463cb) with the production CLI and patch service, without a token or substitute service, and checks the INSTALLED bytes, lock stability, digest rejection and rollback on Linux, macOS and Windows (workflow). - The hermetic real-Bun suites —
crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs(hosted),e2e_vendor_bun_build.rs(vendored),e2e_bun_lockb.rs(native binary) andmode_migration_bun.rs(hosted ⇄ vendored takeover, scoped unwind) drive a realbun installagainst a wiremock patch service. The text suites run inci.yml'se2ematrix; the binary writer/reader suite runs inbun-compatibility.yml.
Underneath, the hosted and vendored bun rewriters are pinned by shared golden
fixtures (crates/socket-patch-core/tests/fixtures/redirect/npm/bun/*, shared
with the depscan TypeScript backend, whose golden harness asserts every case it
does not list as lagging — the cases authored Rust-first here lag until
bun.ts is ported, see depscan TS parity; refusal
fixtures pin their warning code through expected-warnings.json) and by
hermetic CLI suites that
need no Bun binary (tests/vendor/in_process_vendor_bun.rs,
tests/in_process_vendor_bun_takeover.rs, the bun cases of
tests/in_process_redirect.rs, tests/covgap_commands_scan_hosted.rs,
tests/covgap_commands_scan_mod.rs, tests/covgap_commands_rollback.rs,
tests/scan_vendor_e2e.rs, tests/get/get_modes_e2e.rs,
tests/repair/repair_vendor_flavors_e2e.rs). The
machine contract is the
authority on envelopes and codes; this page is the measured matrix behind it.
See the ecosystem matrix for the
other npm lockfile flavors.
| Input | Hosted (scan / get --mode hosted) |
Vendored (scan / get --mode vendored, vendor) |
Agent / discovery |
|---|---|---|---|
Text bun.lock, lockfileVersion 0, 1 or 2, no workspace: packages |
Registry 4-tuple ["name@ver", "<registry>", {deps}, "sha512-…"] → URL 3-tuple ["name@https://patch.socket.dev/…/name-ver.tgz", {deps}, "sha512-<patched>"]; the {deps} meta object (dependencies, bin, …), the lock's version line and its line endings are kept verbatim. |
Same entry → local 3-tuple ["name@.socket/vendor/npm/<uuid>/name-ver.tgz", {deps}, "sha512-<ours>"], tarball committed under .socket/vendor/npm/<uuid>/; the patch record lives in .socket/vendor/state.json (the ledger embeds it — vendored mode writes no .socket/manifest.json). |
The installed tree is patched in place; the lock's registry tuples are inventoried, so lockfile-only packages join discovery. |
Version-0 lock (Bun 1.1.39–1.1.45 --save-text-lockfile) with workspace: packages — 2-tuple entries "consumer": ["consumer@workspace:packages/consumer", { "dependencies": { … } }] |
Refused redirect_bun_workspace_unsupported, lock untouched, exit 0. Remedy: delete bun.lock and re-lock with Bun ≥ 1.2 (writes lockfileVersion 1, which hosted mode accepts; 2 on Bun ≥ 1.4). A plain in-place bun install bumps the version only when a workspace depends on another workspace (root → member — the matrix's workspace shape, the only shape it was measured on); otherwise Bun 1.2.0 keeps version 0 and 1.2.23+ fail to resolve (see In-place re-versioning). |
Refused vendor_bun_workspace_unsupported (pre-version-2 policy, next row); its remedy tail for a version-0 lock says to re-lock with Bun ≥ 1.2 before trying --mode hosted, which refuses version 0 too. |
Works. |
Version-1 lock (Bun 1.2–1.3 default) with workspace: packages — 1-tuple entries ["consumer@workspace:packages/consumer"] |
Rewritten (golden lock-v1-workspace; matrix 1.2.0–1.3.14 workspace / workspace-nested). |
Refused vendor_bun_workspace_unsupported before any write. Policy, not a grammar limit: Bun 1.2.x–1.3.x resolve a workspace member's local-tarball path relative to the MEMBER (our root-relative tuple ENOENTs on bun install), 1.4.x relative to the lockfile, and a committed lockfileVersion-2 lock is the only proof that every consumer runs Bun ≥ 1.4 (1.3.x cannot parse v2). A deliberate over-approximation: a package declared only by the workspace ROOT vendors and installs on v1 too, but the lock cannot cheaply prove which workspace declares a hoisted entry. Remedy in the detail: delete bun.lock, re-run bun install with Bun ≥ 1.4 (an in-place bun install keeps the existing version), or — version 1 — use --mode hosted, which accepts version-1 workspace locks (a version-0 lock is told to re-lock with Bun ≥ 1.2 first). NOT refused: purls the vendor ledger wires at the selected uuid, purls whose every matching lock tuple already points into .socket/vendor/npm/ (any uuid — a superseding patch re-pins in place; the lock-derived rule the engine uses), in-sync re-runs and repair rebuilds. vendor and the vendor step run the same preflight BEFORE a hosted → vendored takeover's revert, so a hosted-redirected purl on such a lock stays hosted-patched (failed vendor_bun_workspace_unsupported, lock and ledgers untouched; vendor --dry-run previews the same code). A .socket/vendor/state.json the preflight cannot read is vendor_state_unreadable, fail-closed. |
Works. |
Version-2 lock (Bun 1.4+) with workspace: packages, nested versions included |
Rewritten (golden lock-v2-workspace-nested — provenance: its nested same-version consumer/left-pad entry is a synthetic, grammar-valid extension of the 1.4.2 capture; bun hoists identical resolutions and never writes that entry itself, but bun 1.4.2 installs the fixture unchanged, and it is the only case pinning the rewrite of every matching tuple in one lock). |
Vendored (matrix 1.4.0 / 1.4.2 workspace, workspace-nested, already-vendored-workspace). |
Works. |
Binary bun.lockb (binary format revisions 1, 2 and 3) |
Package resolution and integrity records are rewritten in place. The CLI does not spawn Bun or produce a text lock. rollback / remove cannot restore a hosted bun.lockb entry to its upstream registry entry (v5.0 keeps no ledger to replay, and the binary lock is not re-derived), so they refuse it with the git checkout -- bun.lockb remedy. |
Native local-tarball wiring, committed artifact, repair and vendored → hosted takeover. Hosted → vendored rebuilds a hosted bun.lockb pin's npm registry record from the registry (byte-exact for a lock socket-patch wired hosted), then vendors; vendor --revert returns the pre-hosted lock. Offline it refuses (redirect_revert_failed), leaving it hosted. |
Registry package records are inventoried directly, including lockfile-only projects without node_modules. |
A package the project patches itself with bun patch (a patchedDependencies key for its name@version, or its bare name, in the root package.json or mirrored in bun.lock) (#367) |
Left on its registry tuple (text and binary lock): Bun applies the user's patch only to the registry name@version, so a hosted URL would drop it from every install with exit 0. Warns redirect_bun_patched_dependency_skipped naming the key, and the in-run VEX never assumes the patch applied; other packages in the lock are still rewired. |
Refused vendor_lock_entry_unsupported before any write or download (text and binary lock), naming the key. |
The installed tree is patched in place, as for any package. |
A package on Bun's default trusted list (better-sqlite3, esbuild, sharp, …) in a project that declares no trustedDependencies (root package.json, or mirrored in bun.lock) (#371) |
Rewired (text and binary lock), with warning redirect_bun_default_trust_lost: Bun 1.3.5+ apply the default list only to npm-registry resolutions, so a hosted URL makes bun install skip the package's install scripts with exit 0 (measured: 1.3.4 runs them, 1.3.5–1.4.2 do not). The remedy is adding the package to trustedDependencies, which replaces the default list. |
Same, vendor_bun_default_trust_lost, for the local tarball tuple (text and binary lock). |
Not affected: the installed tree keeps its registry resolution. |
Truncated, corrupt or unrecognized binary bun.lockb |
Refused with redirect_bun_lockb_invalid, preserving the lock. |
Refused with vendor_bun_lockb_invalid before downloads or artifact creation. |
The inventory reports the malformed lock. |
bun.lock with a lockfileVersion ≥ 3, no integer version, or a packages section outside bun's single-line grammar |
Refused redirect_bun_lock_unsupported. |
Refused vendor_lockfile_version_unsupported (preflight and engine). |
The inventory skips the lock. |
One detail text serves both modes for the version gate: a newer version says "update socket-patch, or re-lock with a Bun release that writes lockfileVersion 0–2" (re-locking with a newer Bun would reproduce it); a missing integer says "re-lock with Bun ≥ 1.2".
Pre-download preflight (vendored). scan --mode vendored and
get --mode vendored (search and uuid paths) check
bun.lock / bun.lockb ONCE before any patch download when the selection
holds an npm purl. A refused project marks every npm result failed with the
vendor code + detail, fetches nothing and records no patch: the scan /
get <purl> path writes nothing under .socket/ (vendored mode is
manifest-free — a .socket/manifest.json seeded for another purl is left
byte-untouched) and exits partial_failure / 1; get <uuid> --mode vendored
exits 1 with status: "error" and error: {code, message}, likewise without
creating .socket/. --silent keeps the
code-tagged refusal on stderr; --dry-run previews it as the additive
would_refuse action. Agent-mode get --save-only is not preflighted.
Mode conversion. Hosted → vendored (scan / get --mode vendored,
vendor over a hosted bun.lock) restores the hosted line to its upstream
registry tuple — re-resolved from the npm registry, since v5.0 keeps no hosted
ledger — and vendors (vendor_takeover_reverted_redirect; vendor --dry-run
resolves the same restore and reports vendor_would_revert_redirect) — but only after the Bun vendored
preflight accepted the lock: on a lock the vendored backend refuses (a
pre-version-2 workspace: lock) vendor, the vendor step and the dry run
report failed vendor_bun_workspace_unsupported BEFORE the restore, leaving the
hosted wiring and bun.lock byte-untouched (the purl
stays hosted-patched). Vendored → hosted reverts the vendored
wiring, ledger entry and committed artifact first
(redirect_takeover_reverted_vendored). rollback <purl> / remove <purl>
restore one of several hosted bun packages to its upstream registry tuple; an
unscoped rollback restores every hosted pin the same way (no ledger replay).
A hosted bun.lockb pin is refused by rollback / remove (git checkout -- bun.lockb); the hosted → vendored takeover rebuilds its registry record and
vendors (tests/vendor_eject_bun_lockb.rs, real Bun in tests/e2e_bun_lockb.rs). Pinned hermetically by
tests/in_process_vendor_bun_takeover.rs, against real Bun by
tests/mode_migration_bun.rs (CI: Bun 1.4.2 on three OSes, 1.3.14 on Linux)
and by the matrix's hosted-then-vendored / vendored-then-hosted shapes.
Binary locks are parsed and patched directly. The codec understands the original version-1 representation, the version-2 URL representation, the later scripts package field, and version 3's wider semantic-version representation. It keeps package IDs, dependency edges, hoisting data, package metadata and optional extensions intact, except where a vendored re-run folds a duplicate record of the patched package into its tarball record (#861, below). Historical workspace dependency flags and literals are normalized to the equivalent representation accepted by old and new readers; the original encoding is retained for rollback. Unknown versions and invalid offsets fail closed.
Vendoring uses per-package binary snapshots in its ledger. This allows a scoped
rollback or mode switch to restore one package while keeping other packages wired.
Hosted mode (v5.0) keeps no ledger. The hosted → vendored takeover (and the
eject) rebuild a hosted bun.lockb entry as Bun's npm registry record from the
registry's dist.tarball / dist.integrity; the hosted rewrite keeps the
registry record's inactive bytes, so that rebuild is byte-exact. A lock the
rewrite had to normalize is marked in the root package's resolution value
bytes (never read for a root resolution): a format-1 lock it promoted is
demoted back to its exact format-1 bytes (verified by promoting again), and a
lock whose workspace dependency behaviors it normalized is refused with the
checkout remedy. rollback and remove refuse a hosted
bun.lockb entry instead (restore bun.lockb from version control). Scoped rollback restores package
resolutions and may retain equivalent binary normalization; if Bun itself has
subsequently upgraded the binary schema, rollback preserves that schema and
restores the original package resolutions.
Dry runs validate the same input and drift conditions without changing it. No Bun
executable is required to inspect, rewrite or restore a binary lock.
Binary locks stay binary through patching, mode changes and rollback. A format-1
input is promoted directly to binary format 2 when needed for tarball resolutions,
with an exact original snapshot for rollback. When bun.lock also exists, it
takes precedence, matching modern Bun's installer.
Binary workspace vendoring records byte-identical tarball copies under each
workspace's .socket/vendor/npm/<uuid>/ directory as well as the canonical root
artifact. This supports Bun releases that interpret local-tarball paths relative
to the workspace and those that interpret them relative to the lockfile. Commit
these copies along with the root artifact. Repair rebuilds missing or corrupt
copies, including recovery when the local ledger is missing; rollback and mode
switches remove the tracked copies with drift checks.
Bun 1.2 and newer can still write native binary locks. Use the following project
configuration in bunfig.toml when creating a new binary lock:
[install]
saveTextLockfile = falseThe committed real-Bun captures live in
crates/socket-patch-core/tests/fixtures/bun-lockb/<version>/, with their manifest,
lock and SHA-256 provenance. They cover 0.1.1, 0.1.6, 0.1.7, 0.5.9, 0.6.7, 0.6.8,
0.8.1, 1.0.0, 1.0.36, 1.1.0, 1.1.38, 1.1.45, 1.2.0, 1.2.23, 1.3.0,
1.3.14 and 1.4.2. The fixture boundaries follow Bun's
binary lock implementation.
Run the dedicated binary acceptance matrix:
python3 scripts/backtest-bun-lockb.py \
--tools /tmp/bun-tools \
--output /tmp/bun-lockb-compatibilityBun 0.1.x predates upstream checksum manifests; its official release assets use
the reviewed SHA-256 pins in scripts/bun-historical-shas.json. Later releases
are verified against their published SHASUMS256.txt.
Native writers from Bun 0.5.9 onward are paired with their own reader. Bun
0.1.1, 0.1.6 and 0.1.7 have no tarball installer: they silently omit tarball
packages even when installation exits zero. Their original binary locks are
therefore tested with Bun 0.5.9, the first tarball-capable reader; this upstream
limitation is recorded explicitly in each matrix row. Newer readers also consume an
unchanged 1.1.45 binary lock. The Rust tests assert cold frozen installs from an
empty cache, exact patched and bystander bytes, preservation of the binary file,
hosted and vendored reruns, both takeover directions, dry-run immutability,
artifact repair, manifest-free vendored scans and byte-exact rollback. Extended cells cover npm
aliases, overridden transitives, workspace members, multiple versions, root and
workspace scripts, and GitHub resolutions. Workspace cells also exercise missing
and corrupt copies, with and without the local ledger.
Production cells from writer 0.8.1 onward patch two packages, retain a transitive
dependency and its bin plus root lifecycle-script metadata, and exclude a dev
alias and another dev package. They verify cold frozen production installs after
each scoped rollback, in both package orders.
The public-service matrix additionally verifies ordinary installs, warmed-cache
frozen and ordinary installs, and digest tampering.
SOCKET_PATCH_BUN_LOCKB_REQUIRED=1 makes missing tools a failure. A regular
cargo test -p socket-patch-cli --test e2e_bun_lockb uses Bun on PATH; a modern
Bun reads the committed binary fixture, so no separate old writer is needed.
The binary matrix keeps each fixture's temporary directory and cache isolated; old Bun writers can collide when sharing temporary paths. Windows has no official Bun binaries before 1.1.0. Official Bun 0.5.9, 0.6.7, and 0.6.8 can crash during pristine installs on Ubuntu 24.04, before Socket Patch runs; the compatibility workflow retains those historical pairs on Ubuntu 22.04. Read the workflow and its uploaded diagnostics for each run's coverage and results.
Local probes ran on macOS arm64 with the releases named below; the workflow
downloads the matching linux / darwin / windows build (x64, or aarch64 on ARM
runners) from the GitHub releases and verifies it against SHASUMS256.txt. Every measurement sets
BUN_INSTALL_CACHE_DIR and BUN_INSTALL per project and passes
--ignore-scripts.
- Lock history. Binary
bun.lockbonly through 1.1.38. The text lock arrives in 1.1.39 as the--save-text-lockfileopt-in (lockfileVersion 0: trailing commas, noconfigVersion, 2-tuple workspace entries);--lockfile-onlyexists from 1.1.43; text is the default from 1.2.0 (lockfileVersion 1) through 1.3.x; 1.4.0 writes 2 for a FRESH lock behind an unchanged grammar. Registry 4-tuples are byte-identical across 0/1/2, so the rewrite is version-independent; the workspace grammar, the binary layout and digest enforcement are not. - In-place re-versioning. Bun never bumps an existing version-1 lock in
place — 1.4.x
install,add,update,--forceand--save-text-lockfileall keep 1; only deletingbun.lockand re-locking writes 2 (hence the vendored workspace remedy). A version-0 lock WITHOUT workspaces stays 0 under 1.2.0, 1.2.23 and 1.3.0 and is rewritten as 1 by 1.3.9, 1.3.10, 1.3.13, 1.3.14, 1.4.0 and 1.4.2 (the first bumping release lies in (1.3.0, 1.3.9]). A version-0 lock WITH workspaces whose root depends on the member (the matrix'sworkspaceshape) is rewritten as 1 by a plainbun installon 1.2.0, 1.2.23, 1.3.0, 1.3.14 and 1.4.2 — the root dependency's spelling changes from a bare path toworkspace:*, which forces the save — while 1.1.45 keeps it at 0. That bump is CONDITIONAL on an inter-workspace dependency: on a version-0 workspace lock whose root does not depend on its members, a plainbun installkeeps version 0 on 1.2.0 (exit 0, lock byte-identical) and fails with<pkg>@<ver> failed to resolveon 1.2.23–1.4.2 (exit 1, lock unchanged;--forceand--save-text-lockfiletoo), so hosted mode keeps refusing. The hosted refusal's remedy is thereforerm bun.lock && bun installwith Bun ≥ 1.2, which converges on every release (version 1 on 1.2–1.3, 2 on 1.4); the in-place bump is a measured convenience for the matrix's shape only, and the root-independent shape is not in the matrix. - Binary → text migration of a workspace lock. Hosted and vendored
bun.lockbwrites store each inter-workspace dependency literal as the member's resolved path (older binary readers need it). Bun 1.4.2'sbun install --save-text-lockfilecarries that path intobun.lock("consumer": "packages/consumer"), and its text reader then re-resolves the workspace: frozen installs fail and an unfrozen install drops the pins (#803). Bun 1.3.14 accepts the path. A hosted or vendored re-run on the migrated lock restores the manifest'sworkspace:literal (Bun's own spelling) whenever the lock holds a pin; version-0 locks, where Bun 1.1 writes the bare path itself, are left alone. Covered bye2e_bun_lockb::workspace_text_migration_heals_on_rerunon the 1.4.2 leg. - Binary → text migration of a vendored lock. The migration deletes
bun.lockband carries the vendored tuples intobun.lock, while the vendor state still recordsbun.lockbpackage snapshots.vendor --revert,rollbackand the hosted takeover then restore each recorded registry package as the tuple Bun writes for it (an empty registry slot underhttps://registry.npmjs.org, the tarball URL otherwise), and a superseding re-vendor carries that tuple over as its pre-vendor original (#784). Covered bye2e_bun_lockb::vendored_text_migration_reverts_to_registry(skipped below Bun 1.2) on the 1.4.2 leg. - Late dependents of a vendored package (#861). After a workspace
bun.lockbis vendored, a new dependent of the patchedname@version(a member added later, orbun addin a member) makes Bun write a second, nested registry record of it, since the hoisted record is now a local tarball. Rewiring that record to the same tarball leaves two records with one resolution, which Bun's own writer never produces: on the isolated linker both map to onenode_modules/.bun/store directory and cold frozen installs fail intermittently withEEXIST(measured on 1.3.9 and 1.4.2; 1.2.23, the hoisted linker and textbun.lockwere unaffected). The vendored re-run instead folds the duplicate into the tarball record: its dependents resolve to that record, the record and its own dependency edges are dropped and later package IDs and dependency slices renumbered, and the hoisting trees are re-derived with Bun's hoister (1.3.x refuses a frozen binary lock whose re-hoisted trees differ). The fold needs the duplicate's dependencies to resolve to the same packages as the tarball record's (so nothing is orphaned), and the codec's hoister to reproduce the lock's own trees and need no rule it does not model (a peer meeting another version, a cyclic folder); otherwise every record is rewired as before, withvendor_bun_lockb_duplicate_records. A patched package with dependencies of its own (mkdirp@0.5.6) folds the same way, and an unfrozen install by 1.3.9 and 1.4.2 leaves the folded lock byte-identical. An optional peer nothing installs (ws@8'sbufferutilandutf-8-validate, common in real locks) is an unresolved edge: the hoister skips it from the written trees as Bun does, so it does not block the fold; the e2e-adderand-depsshapes depend on ws@8.18.0 for this. Covered bye2e_bun_lockb::workspace_late_dependent_rerun_shares_the_tarball_recordandworkspace_late_dependent_with_dependencies_rerun_shares_the_tarball_recordon the 1.3.14 and 1.4.2 legs, and hermetically by thebun-lockb/late-dependent/fixtures. - Workspace-member local tarballs. Bun 1.2.x–1.3.x resolve a
local-tarball dependency declared by a workspace member relative to the
member (
.socket/vendor/…→ ENOENT onbun install); 1.4.x resolve it relative to the lockfile. A package declared only by the root installs on version-1 locks as well; the vendored gate still refuses (policy above). - Digest enforcement. Bun verifies the sha512 of URL and local-tarball
tuples only from 1.3.10: 1.3.9 installs a tarball whose bytes do not
match the lock with exit 0, 1.3.10 fails with
Integrity check failed. Registry 4-tuples are verified from 1.2.0. So on 1.1.39–1.3.9 a hosted or vendored rewrite REMOVES digest enforcement for the patched package (the registry tuple it replaced was checked; the URL / local tuple is not) — the committed lock and artifact are the protection there. The PR's first matrix placed the boundary at 1.3.14 because it sampled only 1.3.0 and 1.3.14; the hermetic suites pin it from both sides (TARBALL_INTEGRITY_ENFORCED_FROM = (1, 3, 10)with the 1.1.45 / 1.2.23 / 1.4.2 legs) and the matrix carries 1.3.9 and 1.3.10. - Digest-less re-saves. The same releases (every text-lock Bun below
1.3.10 — measured on 1.1.45 at lockfileVersion 0, 1.2.23 and 1.3.9)
re-save a URL or local-tarball tuple WITHOUT its
sha512whenever the lock is re-saved for another reason:bun add <pkg>, orbun installafter a package.json / workspace change (a root rename alone does not re-save; a frozen install never writes). The 3-tuple comes back as the 2-tuple["name@<url|path>", {meta}], spec and meta intact; 1.3.10, 1.3.14 and 1.4.2 keep the digest. The CLI recognises that spelling as its own wiring: the repeat hosted run heals it (redirected: 1, noredirect_bun_entry_not_found), the vendored re-run staysalready_vendoredand re-pins the digest on disk,repairrebuilds through it, androllback/ scopedrollback/remove/vendor --revert/ both takeovers accept the digest-less spelling of a recorded line and restore the registry original over it. Before the fix every one of those refused after any lock re-save on those releases (redirect_bun_entry_not_foundbesideredirected: 1,rollback→partial_failure,vendor_lock_entry_not_found/vendor_lock_entry_drifted); thealready-vendored-workspacematrix shape on 1.2.0–1.3.9 is the regression guard. - Both lockfiles present. Modern Bun prefers text
bun.lockoverbun.lockb. The CLI follows the selected lock format; native binary writes never create a sibling text lock. - Bun 0.8.1 / 1.0.0 with peer or overridden-transitive shapes do not install the selected patched version. Those projects remain unchanged; the matrix records this upstream limitation and checks lock presence explicitly.
- Modern frozen installs preserve the lock. Bun 0.5.9 upgrades an untouched
format-1 registry lock to format 2 even with
--frozen-lockfile; that historical exception is checked explicitly. Modern ordinary installs upgrade format 2 to 3, which the matrix validates using Bun's complete semantic lockfile dump. Outside these schema transitions, only a plainbun installcan observe re-serialization drift — the matrix'sordinaryStableLockcheck and the plain-install legs of the hermetic suites both run it.
cargo build --locked -p socket-patch-cli
python3 scripts/backtest-bun.py \
--cli target/debug/socket-patch \
--cli-revision "$(git rev-parse HEAD)" \
--output /tmp/bun-compatibility \
--modes hosted vendoredUse --versions 1.4.2 --shapes workspace-nested for a focused reproduction,
--tools <dir> to reuse pre-downloaded binaries
(<dir>/<version>/bun-<os>-<arch>/bun[.exe], the layout the workflow
pre-populates), --cli-build-sha <sha> (or CLI_BUILD_SHA in the
environment) when the built commit differs from --cli-revision, and
--jobs N for parallel cells. A narrowing that leaves no applicable
(version, shape, mode) cell — e.g. --shapes isolated on Bun < 1.3.0 —
prints a notice, writes a single {"noCells": true, "passed": false, …}
summary row and exits 0 (the default shape list always holds direct, which
applies everywhere, so an un-narrowed run can never go vacuous). Windows uses
target/debug/socket-patch.exe. Bun binaries are downloaded from the GitHub
release with retries and verified against SHASUMS256.txt (bunSha256 in the
provenance); releases before 1.1.0 have no Windows binary. The legacy-lockb
shape also needs Bun 1.1.38 (its baseline writer), fetched or reused the same
way whenever a legacy-lockb cell applies to a requested release.
Public-service cells retry at most three times when a captured CLI response
contains an explicit request transport error. Every retry recreates the project
and its caches, and preserves the failed attempt's logs, result and captured
tree under attempts/; the final row records networkRetryAttempts. Functional
failures without a transport error are never retried. CI limits the public
matrix to six concurrent jobs, with three cells per job. Each cell has its own
temporary directory so historical Bun processes cannot collide while extracting
identically named packages.
On macOS the job first runs .github/actions/pin-socket-hosts
(scripts/pin-socket-hosts.py): the hosted macOS resolver intermittently
answers patch.socket.dev with EAI_NONAME for minutes at a time, at job start
or mid-job, while the service is up, which failed every hosted cell in the
window (FailedToOpenSocket, [Errno 8] nodename nor servname provided, or a
Bun 1.3.x workspace install that never exits). The action resolves the patch
hosts once (the system resolver, then DNS-over-HTTPS by IP literal), keeps only
addresses whose TLS handshake verifies the hostname, and pins them in
/etc/hosts, so cells still reach the production service over verified TLS
without depending on the runner's resolver. The vlt and Poetry workflows run
the same step.
Pinned versions: 0.8.1, 1.0.0, 1.0.36, 1.1.0, 1.1.38 (binary lock),
1.1.39 (first text lock, version 0), 1.1.43 (first --lockfile-only), 1.1.45
(last version-0 writer), 1.2.0, 1.2.23, 1.3.0 (version 1), 1.3.9 / 1.3.10
(digest boundary), 1.3.14 (last pre-v2 default), 1.4.0, 1.4.2 (version 2).
Shapes. direct, dev, optional, peer, alias (npm: alias
install), transitive (overridden transitive), two-versions, workspace
(the member declares the dep), workspace-nested (root and member at
different versions), workspace-root (the root declares the dep, the member
something else), text-workspace (Bun 1.1.39–1.1.45 --save-text-lockfile
on the workspace project — a REAL version-0 workspace lock), workspace-get-uuid / workspace-get-search
(get by uuid and by PURL on the workspace project), already-vendored- workspace (vendor a plain project, add a workspace member, bun install,
re-run — must be already_vendored; then repair rebuilds a deleted
tarball), crlf (CRLF manifest), crlf-lock (CRLF bun.lock),
space-unicode (a path with spaces and Unicode), custom-registry (a
non-empty registry slot the rewrite must drop; the project configures no
registry, so a hosted rollback restores Bun's "" npmjs slot, #992), text (--save-text-lockfile
opt-in, Bun ≥ 1.1.39 only — asserts bun.lock exists after the baseline),
isolated / hoisted linkers, lockfile-only (no node_modules),
production, get-uuid, get-search, legacy-lockb (the baseline is
installed with Bun 1.1.38 so the project starts with bun.lockb; the matrix
Bun then runs the CLI), hosted-then-vendored / vendored-then-hosted (mode
conversion on ONE project) and preexisting-manifest (a record seeded for
another purl must survive a refused vendored run).
Expectation oracle. expected_outcome(version, shape, mode) encodes the
boundaries above — not the CLI's own output — and every cell asserts
supported against it and the refusal codes EXACTLY, after removing an
explicit informational allowlist (vendor_prebuilt_downloaded,
vendor_fetched_missing, reinstall_required,
vendor_bun_reinstall_required / redirect_bun_reinstall_required,
…); substring matching is never used. *_bun_default_trust_lost,
*_non_registry_entry_skipped and vendor_bun_lockb_duplicate_records are
deliberately not on it: no fixture rewires a default-trusted package, holds a
non-registry copy or a duplicate bun.lockb record, so each would be a
misclassification. A configuration expected to be
supported FAILS on unexpected warnings, redirect_bun_entry_not_found
or redirect_revert_failed. Exit codes are recorded for every invocation and
asserted: supported → 0; hosted refusals → 0 with redirect.redirected == 0
(the documented hosted-refusal posture); vendored and get refusals
→ non-zero, with download.downloaded == 0 and no stray manifest record.
Every supported case verifies:
- the patch uuid (the hosted URL in the lock for hosted — v5.0 writes no hosted
ledger — and the vendor ledger record for vendored; no mode writes
.socket/manifest.json, and every cell asserts its absence) is the expected published patch uuid; - a fresh
bun install --frozen-lockfileand a fresh ordinarybun install(empty caches, nonode_modules) install the record's exactafterHashbytes and leave the lockfile byte-identical; - the repeat run is a no-op with the documented envelope — hosted:
status: success,redirect.redirected == 1, no non-informational warning; vendored:summary.applied == 0,summary.skipped == 1,summary.failed == 0, onealready_vendoredevent, nofailedaction — and preserves the lock bytes; registryDigestEnforced: before the CLI runs, a copy of the project with a tampered REGISTRY-tuple sha512 failsbun install --frozen-lockfileon every release whose baseline wrote a text lock (documents what the rewrite is compared against);rejectCorruptDigest: a tampered sha512 on the PATCHED tuple is rejected on Bun ≥ 1.3.10; below that the observation is RECORDED (legacyDigestBehavior) rather than asserted;- rollback (run over a patched install) restores the original manifest /
lock bytes, removes the
.socket/vendorstate, and the reinstall reproduces the record'sbeforeHashbytes; text projects retainbun.lock, and binary projects retainbun.lockbwithout creating a text lock. The original lock presence and SHA-256 are both checked. The reinstall is a plainbun installover the keptnode_modules; Bun's hoisted linker keeps the patched copy there (#764), so when it does the rollback must have emittedvendor_bun_reinstall_required/redirect_bun_reinstall_required(rollbackReinstallAdvised; for the refused hostedbun.lockb, the refusal's checkout remedy names it) and the cell follows that advice withbun install --force. The isolated linker links the registry entry and leaves the superseded patched store entry undernode_modules/.bununlinked; the byte oracle counts only store entries something links to. Bun 1.3.0 (only; 1.3.1 fixed it, measured 1.3.0–1.3.14) also leaves its hidden hoist linknode_modules/.bun/node_modules/minimiston that superseded entry, even under--force; when that link is the only patched copy left, the cell records it underupstreamLimitationsinstead of failing.
The runner captures the exact project manifests, lockfiles, the ledgers (and a
.socket/manifest.json only where the preexisting-manifest shape seeded one),
CLI JSON, exit codes, file hashes and assertion
results (captures/<version>-<shape>-<mode>/), plus provenance
(cliRevision — the branch-resolvable commit the row is about; cliBuildSha
— the commit actions/checkout actually built, refs/pull/N/merge on a pull
request, null when not supplied; cliSha256; bunSha256;
bunArchiveSha256). The depscan SBOM tests import
these captures through their fixture validation framework
(bun-compatibility/generate-fixtures.py --captures, depscan #26453).
Vendored artifact contents are verified by the native runner; they are not
needed for SBOM lockfile annotation.
The bun golden fixtures are shared with the depscan TypeScript backend
(bun.ts), whose golden.test.ts asserts a byte match for every case it does
not list in TS_LAGGING. Several bun cases were authored Rust-first on this
branch, so the depscan submodule bump that adopts them must either port
bun.ts or extend TS_LAGGING — otherwise its golden suite fails:
TS_LAGGINGentries the bump needs, all undernpm/bun/:lock-v0(pre-existing: TS refuses lockfileVersion 0),alias,lock-v2-crlf,lock-v2-workspace-nested,re-redirect-stale-url,digestless-hosted-already-wired,digestless-hosted-stale-url-repin, andlock-v0-workspace-refusalfor as long asbun.tslacks the version-0 workspace gate (TS refuses that lock withredirect_bun_lock_unsupportedwhileexpected-warnings.jsonpinsredirect_bun_workspace_unsupported, so the case matches on files/edits by coincidence today and fails the moment the TS harness reads the warnings file).lock-v2-crlfis not a gate lag but abun.tsbug twin: it drops the\ron the rewritten line (the defect 532c40b fixed here).- Harness requirement:
golden.test.tsmust read the optionalexpected-warnings.jsonand assert the warning-code set, asredirect_golden.rsdoes; until the dispatcher-levelredirect_npm_no_lockfilesuppression for bun-only projects is ported, that assertion fails every bun case that ships the file. bun.tsporting items (what lifts each case out ofTS_LAGGING):SUPPORTED_LOCK_VERSIONS = [0, 1, 2]with the shared gate text; the version-0 workspace gate; CR re-emit on the rewritten line; the prior-URL re-pin arm (a hosted URL left by an earlier grant of the samename@versionis re-pinned in place); the digest-less 2-tuple heal (Bun 1.1.39–1.3.9 re-saves); blank-line andconfigVersiontolerance in thepackageswalk.
The fixtures carry the grammar bun writes (captured from 1.1.45 / 1.3.14 /
1.4.2) with one deliberate exception, lock-v2-workspace-nested's
same-version nested consumer/left-pad entry — see its row in the format
table above.
| Claim | Real-Bun matrix (backtest-bun.py) |
Real-Bun hermetic suites (ci.yml e2e) |
Bun-less unit / CLI tests |
|---|---|---|---|
| Text lock 0 / 1 / 2 rewritten and installed, both modes | 1.1.39–1.4.2 | e2e_redirect_bun_build + e2e_vendor_bun_build on 1.4.2 (3 OS), 1.1.45 and 1.2.23 (Linux); the fixture asserts the lock version matches the era table, the v1-on-1.4 leg proves a committed v1 lock keeps installing |
goldens lock-v0, basic (v1), lock-v2; bun_lock.rs, lock_inventory/bun.rs |
| Native binary formats 1 / 2 / 3: inventory, hosted, vendored, repair, takeovers, rollback | direct, legacy-lockb, conversion shapes; dedicated backtest-bun-lockb.py |
e2e_bun_lockb across writer / reader revisions |
bun_lockb.rs and committed real binary fixtures; native CLI tests |
| Version-0 workspace hosted refusal + remedy | text-workspace (1.1.39–1.1.45) |
— | golden lock-v0-workspace-refusal (+ expected-warnings.json), redirect/mod.rs unit tests |
| Pre-v2 workspace vendored refusal (policy) + remedy; version 2 supported incl. nested | v1: 1.2.0–1.3.14 workspace*; v0: text-workspace; v2: 1.4.x |
e2e_vendor_bun_build scoped leg (deps + bin meta survive) |
bun_lock.rs (legacy_workspace_tarballs_refuse_before_writes, in-sync / rebuild exemptions), vendor::in_process_vendor_bun, repair::repair_vendor_flavors_e2e over {0, 1, 2} × workspace shapes |
| Digest boundary 1.3.10 (registry tuples 1.2.0) | 1.3.9 vs 1.3.10 cells, registryDigestEnforced |
tampered twins in both suites, pinned from both sides | — |
Digest-less re-saves below 1.3.10 recognised, healed and unwound (both modes, takeovers, scoped unwinds, repair) |
already-vendored-workspace on 1.2.0–1.3.9 (digestDroppedOnResave; resaveKeepsDigest from 1.3.10) |
bun_redirect_survives_a_digest_dropping_lock_resave, bun_vendor_survives_a_digest_dropping_lock_resave (real file:-dep re-save; the era's spelling asserted from both sides) |
goldens digestless-hosted-already-wired, digestless-hosted-stale-url-repin; bun_lock_text.rs (same_wiring_modulo_integrity), redirect/mod.rs, replay.rs, takeover.rs, bun_lock.rs unit tests; in_process_redirect, vendor::in_process_vendor_bun, in_process_vendor_bun_takeover |
Mode conversion both directions; scoped rollback / remove |
hosted-then-vendored, vendored-then-hosted |
mode_migration_bun (1.4.2 × 3 OS, 1.3.14) |
in_process_vendor_bun_takeover, takeover.rs, covgap_commands_rollback |
| CRLF lockfiles preserved (hosted line, vendored, rollback) | crlf-lock |
— | golden lock-v2-crlf, bun_lock.rs |
| Bun 0.8.1 / 1.0.0 peer / transitive upstream limitation | recorded per cell; no selected target is installed, exit 0 and lock unchanged | — | — |
Manifest-less VEX: attested (redirected) / (vendored) from the lock with no .socket/manifest.json (ledgers kept, then deleted too), --offline → record_unavailable, reverted lock → not attested (also --no-verify), embedded apply --vex / vendor --vex |
every supported cell after its installs (vex* checks; public patch API) |
vex_e2e_common/bun.rs step at the end of every e2e_redirect_bun_build / e2e_vendor_bun_build / mode_migration_bun / e2e_bun_lockb flow (fresh checkout + real frozen install, wiremock API with a zero-request oracle offline); tampered install / artifact → hash_mismatch / vendor_hash_mismatch; stale takeover manifest → the wired uuid wins; production bun_*_install_proof legs |
e2e_vex_lockfile::bun (text v0/1/2 + bun.lockb × both modes: spoofed hosts, record mismatch, pristine / tampered / alias installs, stale lockb); in_process_vendor_bun* lockfile-only twins |
Pre-download preflight envelopes, --silent, --dry-run would_refuse, manifest-free footprint |
get-uuid / get-search / workspace-get-uuid / workspace-get-search refusals (exit codes, downloaded == 0) |
— | vendor::in_process_vendor_bun (exact uuid-path envelope), scan_vendor_e2e, get::get_modes_e2e, vendor_flow.rs |
Not measured: a --cwd <workspace member> run (the member holds no
bun.lock, so the preflight passes and the engine refuses
vendor_lockfile_missing — pinned as today's behaviour, not a promise), and
package shapes the real registry never installs for the free patch
(non-empty peer meta on the oldest releases).
ci.ymle2ematrix —oven-sh/setup-buninstalls the pinned release and the job exportsSOCKET_PATCH_BUN_E2E_REQUIRED=1+SOCKET_PATCH_BUN_E2E_VERSION, under which the suites hard-fail instead of soft-skipping whenbunis missing, is the wrong version, or the fixture install yields no text lock. Legs:e2e_redirect_bun_buildande2e_vendor_bun_buildon ubuntu / macos / windows with Bun 1.4.2 plus ubuntu lock-era legs on 1.1.45 (version 0) and 1.2.23 (version 1);mode_migration_bunon the three OSes with 1.4.2 and on ubuntu with 1.3.14. Without thebun:key the suites printSKIPand pass — the plaintestjob never exercises them.bun-compatibility.yml— the native matrix (16 releases × 3 OS, minus the three pre-1.1 Windows cells) on pull requests that touch the bun code paths, on pushes tomain(the only rust-cache writer; the push filter coversCargo.lock,core/src/vendor/**, the hosted rewriter + unwinds, and the CLI'sscan/**,get,vendor,repair_vendor,removeandrollbackcommands) and onworkflow_dispatchwithversions/shapes/modesinputs. Each cell pre-downloads the release it runs — plus Bun 1.1.38, thelegacy-lockbbaseline, de-duplicated — with retries, checks theSHASUMS256.txtlisting before fetching the archive and verifies the archive against it, passes--cli-revisionand--cli-build-sha, and uploadssummary.jsonplus the captures asbun-results-<os>-<bun>. On dispatch, aversionsoverride runs in every cell (the matrix is static), releases before 1.1.0 are skipped on Windows with a notice (an empty remainder writes anoCellssummary and passes), and ashapes/modesnarrowing that applies to none of a cell's release yields the script'snoCellsrow rather than a red cell.- Native binary matrix —
bun-compatibility.ymlalso runsscripts/backtest-bun-lockb.pyon Linux, macOS and Windows. Unix exercises all 17 pinned binary writers and their compatible readers, plus modern readers of 1.1.45 and the earliest format-1 / pre-scripts locks. Windows starts at 1.1.0, its first available release. Each job builds the Rust test executable, rejects missing tools and skipped required cases, and uploads every pair's log and SHA-256 provenance inbun-binary-results-<os>. - Production suites (on demand).
e2e_hosted_production::bun_hosted_install_proofruns inci.yml'shosted-e2ejob (npm install -g bun@1,SOCKET_PATCH_HOSTED_E2E_STRICT=1);e2e_vendored_production::bun_vendored_install_proofis#[ignore]-gated and runs only by hand (cargo test -p socket-patch-cli --test e2e_vendored_production -- --ignored) — the production vendored proof for bun on three OSes is the native matrix above.