install: fix default-trusted lifecycle scripts being blocked after yarn.lock migration - #38795
install: fix default-trusted lifecycle scripts being blocked after yarn.lock migration#38795robobun wants to merge 7 commits into
Conversation
… registry entries as npm packages yarn v1 writes registry and remote tarball URLs as <url>#<sha1>. The yarn.lock migrator stored that string verbatim as the npm tarball URL of packages on a non-default registry, so Lockfile::has_trusted_dependency, which compares the URL against the canonical registry tarball URL, denied default-trusted packages (electron, esbuild, ...) their lifecycle scripts after migrating. When the suffix was absent (bun's own --yarn output, registries without a shasum) the same entries were turned into remote tarball packages instead, which are never default-trusted either. Strip the suffix once, and treat every non-URL dependency with a semver version as a registry package whose tarball is the resolved URL, the same way the package-lock.json migrator does. Direct URL dependencies still become remote tarballs (now without the suffix), and the package name is only taken from the URL for those.
|
Warning Review limit reached
Next review available in: 5 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (5)
Comment |
|
Status: fix complete (code unchanged since 30251b7). Build 97776 passed all 177 lanes that ran and only failed because its two darwin aarch64 lanes expired waiting for an agent; the listed test failures all passed on retry and are in unrelated files. Re-ran CI once (ec15522, empty commit) to give those two lanes another attempt. Ready for review, in particular the Reproduced on the released build with the Verdaccio harness: With this branch all of those run the preinstall (or, for the archive, migrate to a plain tarball entry), while a lockfile pointing the |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs; the earlier stale-comment note has been addressed. Because the change widens the default-trusted-dependency grant for yarn-migrated lockfiles (from "always blocked" to "same as a fresh install"), a human sign-off on the trust-model reasoning would still be worthwhile.
What was reviewed:
url_without_hashand the reordered resolution block — verified the.tgz→RemoteTarball fallback now only fires whenversionisn't semver, and direct-URL deps still become RemoteTarball.- The
name_to_userestriction tois_direct_url_dep— checked that suffixed and suffix-less registry entries now name identically from the spec, and the ~1000-entryyarn-cli-reposnapshot only changes two lines (both dropped fragments). - The negative lifecycle test (
electronpointed atall-lifecycle-scripts's tarball) — confirms stripping the fragment doesn't let a non-canonical URL pasthas_trusted_dependency.
Extended reasoning...
Overview
The PR fixes yarn.lock migration in src/install/yarn.rs so that (a) yarn v1's #<sha1> fragment is stripped from stored tarball URLs via a new Entry::url_without_hash, and (b) registry entries with a valid semver version are always stored as npm packages rather than falling into the .tgz→RemoteTarball heuristic. The name-from-URL extraction in the name_to_use block is now gated on is_direct_url_dep so suffix-less and suffixed entries name the same way. Tests are added to bun-install-lifecycle-scripts.test.ts (Verdaccio, positive + negative trust cases) and yarn-lock-migration.test.ts (bun pm migrate shapes for private/default registries and direct-URL deps), plus two snapshot lines lose their #sha1 suffix.
Security risks
This is security-adjacent: has_trusted_dependency gates lifecycle-script execution for packages on the built-in trusted list by comparing the stored tarball URL against the canonical <registry>/<name>/-/<name>-<version>.tgz. Before this change, yarn-migrated entries either kept the #sha1 suffix (comparison fails → blocked) or became RemoteTarball (never trusted → blocked) — overly restrictive but safe. After this change, they store the bare canonical URL and pass the check, so scripts run. That widens the grant for migrated lockfiles to match what a fresh install already grants. The stored URL is still the one from yarn.lock, only the fragment is dropped, so a lockfile that records a non-canonical tarball under a trusted name still fails the comparison — the new "scripts stay blocked" test covers this. I don't see a bypass, but the direction of the change (less restrictive) on a security gate is why I'm deferring rather than approving.
Level of scrutiny
Medium-high. The Rust change itself is small and mechanical (strip a fragment, reorder two branches, tighten a condition), and the ~1000-entry yarn-cli-repo snapshot only moves two lines, which is strong evidence the classification change is narrow. But the effect is on the lifecycle-script trust path, and the name_to_use gating change also subtly alters package identity for suffix-less registry entries. A maintainer who owns the has_trusted_dependency model should confirm the reasoning.
Other factors
My earlier inline comment (misplaced code comment after the branch reshuffle) was addressed in 6a82230, and the comment-cop bot's length complaints were addressed in 0f4989b; all threads are resolved. Test coverage is thorough — both yarn.lock shapes (with/without fragment), private and default registries, an npm: alias, a direct-URL dep, a dependency edge between migrated entries, and a negative trust case. The PR description states all new tests fail on the released build and pass here, and the snapshot diff is minimal.
|
On the trust question: the check in |
…d package (#38806) ### Problem - Migrating a yarn v1 lockfile whose `npm:` alias points at a scoped package records the unscoped tail of the name. For `"my-node-types@npm:@types/node@20.11.5"` resolved from `https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#...`, `bun.lock` gets `"my-node-types": ["node@20.11.5", ...]` instead of `["@types/node@20.11.5", ...]`, and the post-migration manifest pass asks the registry for `node`. The same migration runs inside `bun install` when only a `yarn.lock` exists, which is the `GET https://registry.npmjs.org/monaco-editor-treemended/-/monaco-editor-treemended-1.83.16.tgz - 404` reported in #27781. Reproduces on the released canary (b7a0431) and on main. - Cause: `Entry::get_package_name_from_resolved_url` (`src/install/yarn.rs:239`), which names alias entries after their tarball URL, walks back from `/-/` and checks the last path segment for a leading `@`. In a scoped tarball URL the last segment is the unscoped name and the scope is the segment before it, so the check never fires and only the last segment is returned. The Zig original had the same bug; #27782 and #27889 fixed it there and were closed when the Zig sources went away. ### Fix - `get_package_name_from_resolved_url` now splits the path before `/-/` into its last segment and the one before it, and returns both when the earlier one starts with `@`, otherwise just the last one. It returns `None` (callers then fall back to the spec name) when there is no name segment, where the old code returned an off-by-one slice or an empty name. - Why this is correct: the registry tarball layout is `<registry>/<name>/-/<basename>-<version>.tgz`, and `<name>` is one segment for plain packages and exactly two (`@scope/name`) for scoped ones, so the segment before the name starting with `@` is the only thing that distinguishes the two shapes; nothing else in the URL encodes it. Plain packages take the same path as before, so their output is unchanged. - All three callers benefit: the package name written to the lockfile (`yarn.rs:941`), the `(name, version)` key used to give the alias and a plain entry of the same package one package id (`yarn.rs:843`), and the alias detection that registers an entry under its spec name only when it differs from the real name (`yarn.rs:1603`), which previously fired for every scoped package. - Left alone on purpose: `Entry::parse_npm_alias` also mis-splits `npm:@scope/name@range` at the scope's `@`, but its only consumers are the first dependency pass, whose slices are overwritten by the second pass at `yarn.rs:1695` onward, and a `version` field spelled `npm:...`, which yarn v1 never writes. Nothing observable depends on it. - Test: `test/cli/install/migration/yarn-lock-migration.test.ts`, "yarn.lock with npm aliases of a scoped package keep the scope". One migration covers an unscoped alias, a scoped alias and a transitive alias of `@types/node@20.11.5` (sharing one yarn entry, as yarn writes patterns that resolve to the same tarball) next to a plain `@types/node@18.19.0`, asserts the full `packages` object of the resulting `bun.lock`, and runs against a loopback registry that records which manifests the migration asks for (`@types/node` and the consumer, no `node`). Fails on the released build with the three alias entries reading `node@20.11.5`, passes with this change. - The rest of `yarn-lock-migration.test.ts` (scoped-package and real-world fixtures, 14 snapshots), `migrate.test.ts` and `lockfile-only.test.ts` pass unchanged with the debug build; the `yarn-cli-repo` output is byte-identical to its snapshot. That case still brushes its 5s timeout under debug+ASAN, which is pre-existing and tracked by #35377. - No overlap with the other open yarn migration PRs: #38795 and #38803 rework the `name_to_use` block for tarball entries and leave this function as is (after #38795 it becomes the only source of alias package names). Fixes #27781 ### Background - A yarn v1 `npm:` alias entry is keyed by the alias spec (`alias@npm:real-name@range`) and carries the real package's `version`, `resolved` tarball URL and `integrity`. The migrator stores the package under its real name and hangs it in the tree under the alias, so it needs the real name, and it currently recovers it from the tarball URL. - `bun.lock` `packages` entries are `["<real name>@<version>", "<registry, empty for the default>", {meta}, "<integrity>"]` keyed by the node_modules path. The first element is what `bun install` later downloads, which is why a wrong name there turns into a 404 or a different package. - After migrating, `fetch_necessary_package_metadata_after_yarn_or_pnpm_migration` fetches the manifest of every migrated npm package by the name recorded in the lockfile to fill in bin/os/cpu. The test points that pass at a loopback server and uses the requested names as a second, external view of which package each alias was resolved to.
|
Updated 12:13 PM PT - Aug 15th, 2026
🔄 @robobun, the build for your commit |
… tarball downloads as git dependencies The name block and the resolution block now share one hash-stripped URL. A resolved field like https://github.com/o/r/archive/v1.tar.gz#<sha1> was matched by the bare https://github.com/ prefix and its sha1 taken as a commit, producing a github resolution with a bogus tag; classify those with the same github tarball path rule the dependency parser uses.
…ackage's tarball URL bun.lock files written by the yarn.lock migrator so far carry the suffix on every non-default-registry URL. The fragment never reaches the registry, so comparing without it keeps the canonical-URL check as strict as before while letting those lockfiles keep running the scripts of default-trusted packages.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it relaxes the canonical-URL comparison in has_trusted_dependency (the gate that decides whether default-trusted lifecycle scripts run) and reorders the yarn migrator's resolution classification, a human sign-off is still warranted.
What was reviewed:
has_trusted_dependency: fragment-stripping is sound —bun_urlexcludes#…frompathname/request path, so<canonical>#anythingfetches the same bytes; a differing path still fails. The negative test (trusted name → other package's tarball, with a fragment) confirms blocking is preserved.is_git_dependencynow defers tois_github_tarball_pathon the hash-stripped path;.gitrepo URLs andcodeload.github.comstill classify as before (guard test + unchangedyarn-cli-reposnapshot entries).- The
.tgz → RemoteTarballfallback moving behind!result.valid— checked that direct-URL specs and non-semverversionentries still reach it; theyarn-stuffremotesnapshot row only loses its#sha1.
Extended reasoning...
Overview
The PR fixes default-trusted lifecycle scripts being blocked after migrating a yarn.lock that resolved packages against a non-default registry. It touches three source files: src/install/yarn.rs (adds Entry::url_without_hash, computes the stripped URL once per entry, gates the URL-name-extraction on is_direct_url_dep, moves the .tgz → RemoteTarball fallback behind the invalid-semver branch, and makes is_git_dependency treat GitHub archive/tarball URLs as non-git via the existing is_github_tarball_path classifier), src/install/lockfile.rs (has_trusted_dependency now strips a URL fragment before comparing against the canonical registry tarball URL, so already-migrated lockfiles keep working once the unreleased #31339 check ships), and src/install/dependency.rs (visibility widening only). Tests cover both migration output shape and end-to-end lifecycle-script behavior, including negative guards; two snapshot lines change, both dropping a #sha1 suffix.
Security risks
has_trusted_dependency is the security gate that decides whether packages on the built-in default-trusted list may run install scripts without an explicit trustedDependencies opt-in. This PR relaxes its URL comparison by ignoring the fragment. The argument — that a URL fragment is never transmitted (bun's own URL parser cuts pathname at #, and build_request uses pathname) so <canonical>#x downloads exactly the canonical tarball — is correct, and the integrity check is unchanged. The PR includes a negative test showing a fragment-suffixed URL pointing at a different package's tarball is still blocked. I don't see a bypass here, but any relaxation of a lifecycle-script trust gate deserves a maintainer's explicit sign-off; the author's own comment ("spells this out for whoever signs off on it") anticipates that.
Level of scrutiny
High. The migrator classification changes are behavioral (registry entries with valid semver now always become npm packages rather than remote tarballs; https://github.com/... URLs are now split by the tarball-path predicate rather than blanket-treated as git), and the reader-side change touches a security check. The ~1000-line yarn-cli-repo snapshot being unchanged apart from one onetime row is good evidence the reclassification doesn't disturb the common shapes, but the interaction with #38803 (same name_to_use block) and the trust-gate change both call for a human look.
Other factors
- CI passed on 0f4989b; later commits (fc7f7f9, 107a7bd, 30251b7) added the reader-side fragment handling and tightened comments.
- My earlier inline comment (stale comment placement) was addressed in 6a82230. Remaining unresolved inline comments are from the automated comment-cop bot flagging 2–3-line comments; the author already justified keeping the cross-file-invariant ones and pushed 30251b7 to tighten them. I don't consider those blocking.
- Tests are well-constructed: hermetic (Verdaccio / local 404 registry), concurrent, drain all pipes, include both positive and guard cases, and assert exact
bun.lockentry shapes rather than substrings of the whole file.
|
Summary of what changed since the first review round, for whoever signs off: (1) |
Build 97776 passed all 177 lanes that ran; its two darwin 14 aarch64 test lanes expired waiting for an agent.
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs; my earlier note about the stale comment on the .tgz fallback was addressed. Because the has_trusted_dependency change relaxes the canonical-URL gate that decides whether default-trusted lifecycle scripts run — and the author explicitly flagged that change for sign-off — a human look at that comparison would still be worthwhile.
What was reviewed:
url_without_hashinhas_trusted_dependency: the fragment is dropped from the request path by bun's URL parser, so<canonical>#anythingfetches the canonical tarball; a differing path still fails. The negative test (electron pointed at another package's tarball, with a fragment) confirms the deny case.is_git_dependencynow excludes github.com archive/tarball paths via the existingis_github_tarball_path; the guard test confirms.gitrepo URLs still classify as git.- Resolution-block reordering: the
.tgz→RemoteTarball fallback now only fires whenversionisn't valid semver, so registry entries stay npm packages; direct-URL specs still become remote tarballs. The two snapshot changes are exactly the two dropped#sha1suffixes.
Extended reasoning...
Overview
The PR fixes yarn.lock migration writing tarball URLs that fail has_trusted_dependency's canonical-URL check (from #31339), causing default-trusted packages' lifecycle scripts to be blocked after migration on non-default registries. It touches three source files: src/install/yarn.rs (strip yarn's #sha1 suffix once per entry, reorder the resolution block so semver-versioned entries always become npm packages, gate the URL-name-extraction on direct-URL deps only), src/install/lockfile.rs (compare the stored URL without its fragment in has_trusted_dependency), and src/install/dependency.rs (widen is_github_tarball_path to pub(crate)). Tests cover migration output for suffixed/suffix-less private-registry URLs, default-registry URLs, direct-URL deps, github archive downloads, github git repos, and end-to-end lifecycle-script behavior including a hand-written bun.lock in the shape older migrations produced.
Security risks
has_trusted_dependency is the gate that grants default-trusted lifecycle-script execution by package name. The change from url == canonical to url_without_hash(url) == canonical is a relaxation of a security check. The PR's argument — that bun's HTTP client never transmits the fragment, so <canonical>#anything requests exactly the canonical path — is sound and cited to src/url/lib.rs / src/http/lib.rs, and the test suite includes both a grant case and a still-blocked case (trusted name pointed at another package's tarball with a fragment). I did not find a way for the fragment strip to admit a URL whose request path differs from the canonical one. That said, this is exactly the kind of change REVIEW.md calls out under "Security checks fail closed" as needing explicit sign-off, and the author has asked for it.
Level of scrutiny
Medium-high. The migrator itself is not on a hot path and its output is validated by bun install, but the reader-side has_trusted_dependency change affects every install that consults the default-trusted list, not just migrated lockfiles. The resolution-block reordering also changes classification for a class of entries (suffix-less .tgz URLs with a semver version) from RemoteTarball to Npm; the ~1000-entry yarn-cli-repo snapshot changing by exactly the two expected lines is good evidence this doesn't regress other shapes.
Other factors
My prior inline comment (stale comment left on the wrong branch after the reorder) was addressed. All comment-cop threads are resolved. CI is green on 177 lanes. The PR description documents the one-block conflict with #38803 and how to resolve it. No CODEOWNERS file is present for these paths as far as I can tell. The remaining reason to defer is the security-gate relaxation, which the author themselves flagged for human sign-off.
Found while looking at lifecycle scripts after lockfile migration (the
trustedDependenciesside of that is #38773); this part is about the URLs theyarn.lockmigrator records and is independent of it. Touches the samename_to_useblock as #38803 (a crash fix in the name extraction); see "Relation to #38803" below.Problem
yarn.lockwhose entries resolve to any registry other thanregistry.yarnpkg.com/registry.npmjs.org(a private registry, or the test Verdaccio), packages on bun's default trusted list (electron,esbuild, ...) have their lifecycle scripts blocked on the install that follows. The samebun installwithout theyarn.lockruns them.resolved "<url>#<sha1>". The migrator (src/install/yarn.rs, resolution block) stored that string verbatim as the package's npm tarball URL, sobun.lockends up with"electron": ["electron@1.0.0", "http://localhost:PORT/electron/-/electron-1.0.0.tgz#f1b8bc2c...", {}, "sha512-..."].Lockfile::has_trusted_dependency(src/install/lockfile.rs:3105) only grants default-list trust when the stored URL equals the canonical<registry>/<name>/-/<name>-<version>.tgz, so the suffix fails the comparison.bun install --yarnitself writes, and what registries without a shasum produce) took a different wrong turn: theresolved.ends_with(".tgz")check turned them into remote tarball packages ("electron": ["electron@http://.../electron-1.0.0.tgz", ...]), which are never default-trusted and also skip the post-migrationbin/os/cpumanifest fill-in.package-lock.jsonandpnpm-lock.yamlmigration already store the bare canonical URL for both shapes.resolved "https://github.com/o/r/archive/v1.tar.gz#<sha1>") was read as a git commit, becauseEntry::is_git_dependencymatched everyhttps://github.com/URL. The entry migrated to["r/archive/v1.tar.gz@github:o/r/archive/v1.tar.gz#<sha1 prefix>", {}, ""], which the nextbun install --frozen-lockfilerejects as an invalid git tag.LATESTis still 1.3.14). Everybun.lockthe yarn migrator has written so far on a non-default registry carries the suffix, and a plainbun installkeeps the recorded URL, so without a reader-side fix the first release with Hardening: input validation and bounds tightening across 36 subsystems (round 4) #31339 would start blocking those projects' default-trusted scripts even though this PR fixes new migrations.Fix
Entry::url_without_hashstrips the suffix; the migrator computes the stripped URL once per entry and uses it both to name the package and to build the resolution, so the npm URL and the remote tarball URL of direct URL dependencies are both stored without it.versionis always stored as an npm package whose tarball URL isresolved(empty for the two default registries, as before). The.tgzto remote-tarball fallback now only applies to entries whoseversionis not semver, the one case where it was not misclassifying registry packages. Direct URL dependencies (foo@https://...specs) still become remote tarballs.is_git_dependencyclassifieshttps://github.com/...with the dependency parser's existingis_github_tarball_pathrule (applied to the URL without the suffix), so archive downloads become remote tarballs like any other URL dependency and repositories are still git.has_trusted_dependencycompares the stored URL without its fragment. This is what keeps already-migrated lockfiles working once Hardening: input validation and bounds tightening across 36 subsystems (round 4) #31339 ships. It is exactly as strict as before: the request path bun sends isURL.pathname, whichbun_urlcuts at#(src/url/lib.rs:649-668, used bybuild_requestinsrc/http/lib.rs), so<canonical>#anythingdownloads the canonical tarball and nothing else; a URL whose path differs still fails, fragment or not.src/changes and passes with them, except the two guards noted):test/cli/install/bun-install-lifecycle-scripts.test.ts,describe("default trusted dependencies after yarn.lock migration"):electron@1.0.0viayarn.lockwith and without the suffix runs its preinstall and records the canonical URL; ayarn.lockpointingelectronatall-lifecycle-scripts's tarball keeps the scripts blocked; a hand-writtenbun.lockin the shape the migrator produced until now (...electron-1.0.0.tgz#<sha1>) runs the scripts, and the same lockfile pointing at the other package's tarball (also suffixed) stays blocked (guard, passes before and after). The pre-existing "require the canonical registry tarball URL" test (wrong path, wrong origin) still passes.test/cli/install/migration/yarn-lock-migration.test.ts,describe("yarn.lock migration of registry tarball URLs"):bun pm migrateagainst a local 404 registry checks the migratedbun.lockfor a private registry (suffixed and suffix-less, with a dependency edge between them), the default registry without a suffix (plain andnpm:alias), a direct URL dependency on a registry host and on a GitHub archive, and a github.com repository URL still migrating as git (guard).#sha1suffix (onetimein yarn's own lockfile, anhttp://registry.npmjs.org/URL, and theremotedirect URL dependency inyarn-stuff). The other ~1000 entries of theyarn-cli-reposnapshot, including its git and codeload entries, are unchanged.yarn-lock-migration.test.tsfile,nested-overrides.test.ts"yarn.lock resolutions paths become nested rules" (suffix-less local-registry URLs, migrate + install), the trust tests inbun-pm.test.tsandbun-install-registry.test.ts, and the full lifecycle scripts file (124 pass; the 3 failures neednodeon PATH and fail on main in this environment too).Relation to #38803
name_to_useblock (a panic on crafted URLs, found while working on this). This PR only changes that block's guard (is_direct_url_dep) and the URL it reads (resolved_url), so whichever lands second has a one-block conflict: the combined shape is install: fix yarn.lock migration panic on tarball URLs with "/-/" right after the host #38803's extraction, gated onis_direct_url_dep, reading the stripped URL. install: fix yarn.lock migration panic on tarball URLs with "/-/" right after the host #38803'spinnedrow (a semver entry resolved to a suffix-less tarball) becomes a registry package with this PR, as its description already notes. Landing install: fix yarn.lock migration panic on tarball URLs with "/-/" right after the host #38803 first is simplest; this branch gets rebased either way.Background
package.jsonhas notrustedDependencies, bun runs lifecycle scripts only for packages on a built-in list (src/install/default-trusted-dependencies.txt). Because the grant is keyed by name,has_trusted_dependency(since Hardening: input validation and bounds tightening across 36 subsystems (round 4) #31339) additionally requires the package to be an npm package whose tarball URL is either empty (default registry) or exactly the canonical tarball URL on the configured registry, so a lockfile cannot obtain the grant by recording a different tarball under a trusted name.dist.tarball);bun.lockprints it as""when it is under the default registry and verbatim otherwise. A remote tarball resolution is a different package kind used for dependencies declared as a URL; it has no version and no registry, so it never qualifies for the default list.resolvedfield:<tarball url>#<hex sha1 of the tarball>for registry tarballs, URL dependencies and GitHub archives alike; the suffix is omitted when the registry did not report a shasum. For git dependencies the part after#is the commit instead. bun's ownbun install --yarnprinter writes URLs without a suffix.#. HTTP clients never transmit it; bun's URL parser puts it inhashand excludes it frompathname, which is what becomes the request path.Earlier shape of this PR
The first revision only stripped the suffix in the migrator and reclassified suffix-less registry entries. Self-review turned up the GitHub archive case (same suffix, misread as a commit one layer up) and the population of lockfiles that were already migrated with the suffix, which the unreleased canonical-URL check would otherwise start blocking; both were folded in, the strip was hoisted so the name block sees the same URL as the resolution block, and the tests above were extended to cover them.
no test proof · iteration 4 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-install-lifecycle-scripts.test.ts