Skip to content

install: bump default lockfileVersion to 2, gate stricter parse checks behind it#31539

Merged
Jarred-Sumner merged 10 commits into
mainfrom
farm/69c82f0b/lockfile-version-2
May 29, 2026
Merged

install: bump default lockfileVersion to 2, gate stricter parse checks behind it#31539
Jarred-Sumner merged 10 commits into
mainfrom
farm/69c82f0b/lockfile-version-2

Conversation

@robobun

@robobun robobun commented May 28, 2026

Copy link
Copy Markdown
Collaborator

What

Introduces text lockfileVersion 2, makes it the default, and gates two parse-time checks — added after the Rust rewrite — behind it so that older lockfiles keep loading.

Background

Two checks landed in src/install/lockfile/bun.lock.rs that reject a text lockfile the earlier versions accepted:

  1. Off-registry npm tarball integrity — an npm package resolved to a tarball URL that is not under the configured/default registry must carry a supported integrity hash, otherwise parsing fails with Missing integrity hash for npm package resolved to a tarball URL outside the configured registry.
  2. Unsafe git .bun-tag — a git/github .bun-tag that is not a safe single path component (is_safe_resolved_tag) is rejected at parse with Invalid git dependency tag.

Both are breaking: a bun.lock written before these checks existed (e.g. an npm entry with an off-registry tarball and no integrity hash) now fails to parse.

Change

  • Add Version::V2 and an at_least() helper; bump Version::CURRENT from V1 to V2. The writer always emits the current version, so fresh lockfiles are now "lockfileVersion": 2.
  • Gate both parse-time rejections behind lockfile_version.at_least(Version::V2). For v0/v1 lockfiles the checks are skipped (the pre-check behavior), so existing lockfiles keep loading.

Security note

The git .bun-tag check is a path-traversal / checkout-arg guard. It is re-validated at the point of use in Repository::checkout (src/install/repository.rs) with the same is_safe_resolved_tag, before any cache directory path is built or git is invoked — and a git dependency can only reach the cache through checkout. Skipping the parse-time rejection for older lockfile versions therefore does not reopen the vulnerability; an unsafe tag still cannot be checked out or turned into a traversal path.

The off-registry integrity gate restores, for v0/v1 only, the behavior Bun shipped before the check was added. Lockfiles written at v2 always carry integrity for off-registry tarballs, so enforcement there is unaffected.

Verification

New offline test test/cli/install/lockfile-version-2.test.ts (uses file: deps, no registry):

  • a freshly written text lockfile defaults to version 2
  • an existing v1 lockfile still loads
  • off-registry npm tarball integrity is enforced only at version 2 (v2 rejects at parse; v1 does not)
  • unsafe git .bun-tag is rejected only at version 2

Fail-before/pass-after confirmed by building with the src/ change stashed: the three version-gated tests fail (v2 is an unknown version / v1 still rejects), and pass with the change applied.

Also updated the existing off-registry-integrity test in test/cli/install/bun-lock.test.ts to assert the v2 behavior, and regenerated the written-lockfile snapshots (lockfileVersion 1 → 2). Input-fixture lockfiles in tests are deliberately left at version 1 to keep exercising backward-compatible loading.

…s behind it

Two parse-time checks added after the Rust rewrite reject text lockfiles
that earlier versions accepted:

- an npm package resolved to a tarball URL outside the configured registry
  must carry a supported integrity hash (bun.lock.rs)
- a git/github .bun-tag must be a safe path/checkout component

Both break lockfiles written before the checks existed. Introduce
lockfileVersion 2, make it the default, and gate both rejections behind
it so v0/v1 lockfiles keep loading. The git tag is still re-validated in
Repository::checkout before any cache path is built or git is run, so
skipping the parse-time check for older versions does not reopen the
checkout hole.

Updates written-lockfile snapshots from version 1 to 2.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 7 minutes and 53 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 35834c6e-7924-4a03-8434-0f12705e1513

📥 Commits

Reviewing files that changed from the base of the PR and between cffec9e and 272af67.

⛔ Files ignored due to path filters (1)
  • test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • docs/pm/catalogs.mdx
  • packages/bun-types/bun.d.ts
  • src/install/lockfile/bun.lock.rs
  • test/cli/install/bun-lock.test.ts
  • test/cli/install/lockfile-version-2.test.ts

Walkthrough

This PR introduces lockfile format version V2 as the new default, gates two security validations (npm tarball integrity and git tag safety) to enforce only on V2+, and maintains backward compatibility for V1 lockfiles. Version-gating is implemented via a new at_least() method on the Version enum.

Changes

Lockfile V2 Versioning and Validation

Layer / File(s) Summary
Lockfile version enum and infrastructure
src/install/lockfile/bun.lock.rs
Version::V2 variant is added, Version::CURRENT is switched to V2, and Version::at_least() method enables version-gated validation checks in the parser.
Version-gated npm tarball integrity validation
src/install/lockfile/bun.lock.rs, test/cli/install/bun-lock.test.ts
Parser enforces required integrity hashes for off-registry tarball URLs only when lockfile version is V2+; older V1 lockfiles parse without this check. Test refactored to explicitly target V2 with rewritten lockfile construction and new explanatory comments.
Version-gated git tag safety validation
src/install/lockfile/bun.lock.rs, test/cli/install/lockfile-version-2.test.ts
Parser rejects unsafe git .bun-tag values only for V2+ for bare git: entries; github: entries are rejected regardless of version. Dedicated integration tests verify behavior across versions.
New lockfile-version-2 integration test suite
test/cli/install/lockfile-version-2.test.ts
Comprehensive CLI tests verify fresh saves default to lockfileVersion: 2, existing V1 lockfiles remain loadable under --frozen-lockfile, and version-gated validations behave as specified.
Test snapshot updates for V2 lockfileVersion
test/cli/install/bun-install.test.ts, test/cli/install/bun-workspaces.test.ts, test/cli/install/config-version.test.ts, test/cli/install/minimum-release-age.test.ts
Inline snapshots across test files updated to expect lockfileVersion: 2 as the new default, reflecting the CURRENT version switch without changing test logic.
Docs and types
docs/pm/catalogs.mdx, packages/bun-types/bun.d.ts
Documentation example updated to show lockfileVersion: 2. BunLockFile.lockfileVersion union extended to include 2.

Suggested reviewers

  • RiskyMH
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main changes: bumping the default lockfileVersion to 2 and gating stricter parse checks behind it.
Description check ✅ Passed The description comprehensively covers what the PR does, why it's needed, and how it was verified, fully aligning with the template's requirements for 'What does this PR do?' and 'How did you verify your code works?'
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented May 28, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Done — ready for review.

Bumped default lockfileVersion to 2 and gated two post-rewrite parse-time rejections behind it:

  • off-registry npm tarball integrity requirement (bun.lock.rs)
  • unsafe git .bun-tag rejection (is_safe_resolved_tag)

Both broke lockfiles written before the checks existed; v0/v1 now skip them and keep loading. The git-tag guard is still enforced at checkout time (Repository::checkout), so gating the parse-time check doesn't reopen the traversal hole.

New offline test test/cli/install/lockfile-version-2.test.ts covers: default→v2, v1 still loads, integrity enforced only at v2, unsafe git tag rejected only at v2. Verified fail-before (src stashed) / pass-after. Written-lockfile snapshots regenerated 1→2; input fixtures left at v1 to keep testing backward-compat loading.

@robobun

robobun commented May 28, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:05 AM PT - May 29th, 2026

@robobun, your commit 272af67 has 1 failures in Build #58877 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31539

That installs a local version of the PR into your bun-31539 executable, so you can run:

bun-31539 --bun

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

Comment thread test/cli/install/bun-lock.test.ts Outdated
Comment thread test/cli/install/lockfile-version-2.test.ts Outdated
- git .bun-tag test: point at an unreachable local endpoint (127.0.0.1:1)
  so the v1 case, which now parses successfully and proceeds to git clone,
  fails fast offline instead of contacting github.com.
- off-registry integrity test: restore the same-registry-URL-accepted
  assertion (at lockfileVersion 2) that exercises the npm_url_needs_integrity
  = false branch, instead of only pointing at the off-registry coverage.
@robobun

robobun commented May 28, 2026

Copy link
Copy Markdown
Collaborator Author

Update — addressed review feedback

claude[bot] flagged two test issues; both fixed in 0b22acaa1e:

  1. git .bun-tag test wasn't truly offline — since v1 now parses the unsafe tag successfully, install proceeded to git clone from github.com over SSH before Repository::checkout re-validates. Pointed the URL at an unreachable local endpoint (git+ssh://git@127.0.0.1:1/...) so the clone fails fast without leaving the host (~530ms).

  2. Dropped same-registry-accepted coverage — restored the assertion (at lockfileVersion: 2) that a tarball URL under the configured registry with empty integrity is accepted and installs, exercising the npm_url_needs_integrity = false branch directly.

Both review threads resolved. Offline test file (lockfile-version-2.test.ts) still green.

Comment thread src/install/lockfile/bun.lock.rs Outdated
Comment thread src/install/lockfile/bun.lock.rs
Comment thread test/cli/install/lockfile-version-2.test.ts Outdated
Comment thread src/install/lockfile/bun.lock.rs
robobun added 2 commits May 29, 2026 00:01
…keep github tag check unconditional

Addresses two issues in the v2 gating:

1. Re-save would defeat the backward-compat gate. The writer always stamped
   Version::CURRENT and round-trips npm integrity / git tags verbatim with no
   backfill, so a v0/v1 lockfile carrying an off-registry tarball without
   integrity (or an unsafe git .bun-tag) got rewritten as lockfileVersion 2 on
   the next save and then failed to re-parse. Add Stringifier::version_to_write:
   stamp v2 only when every package is already v2-valid (npm under the
   configured/default registry or carrying a supported integrity hash; git/github
   resolved tag safe), else stay at v1 so the file round-trips.

2. The github tarball path has no use-site re-validation. The git .bun-tag check
   was gated to v2 on the premise that Repository::checkout re-validates, but
   that only covers git; github dependencies resolve via the tarball-download
   path and feed the resolved tag straight into the cache folder name. Keep the
   safety check unconditional for github while leaving git gated to v2.

Fix the one migration snapshot (yarn-stuff) whose off-registry, no-integrity
npm entry now correctly keeps it at lockfileVersion 1.
- packages/bun-types/bun.d.ts: widen BunLockFile.lockfileVersion to 0 | 1 | 2
- docs/pm/catalogs.mdx: bump the bun.lock excerpt to lockfileVersion 2
@robobun robobun requested a review from alii as a code owner May 29, 2026 00:03
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Update 2 — addressed deeper review feedback

Three substantive findings fixed (commits 58bc805a70, 729957387e):

  1. Re-save defeated the gate (matches the earlier save→load→save concern). The writer always stamped CURRENT and round-tripped integrity/tags verbatim, so a v1 off-registry-no-integrity lockfile got rewritten as v2 and then failed the next parse. Added Stringifier::version_to_write: stamp v2 only when every package is v2-valid (npm under the configured/default registry or has a supported integrity hash; git/github tag safe), else stay v1. Now round-trips cleanly.

  2. GitHub tag check was wrongly gated. Repository::checkout re-validates only the git path; github resolves via the tarball path and feeds the resolved tag into the cache folder name with no use-site guard. Split the gate: github's .bun-tag safety check is now unconditional (every version); git stays gated to v2.

  3. Missed non-test refs. Widened BunLockFile.lockfileVersion to 0 | 1 | 2 in bun-types, and bumped the docs bun.lock excerpt to 2. Also corrected one migration snapshot (yarn-stuff) whose off-registry-no-integrity entry correctly keeps it at v1.

New tests: github-unconditional rejection + v1-off-registry round-trip (both verified fail-before/pass-after). All 6 review threads resolved.

Comment thread src/install/lockfile/bun.lock.rs Outdated
Since the github `.bun-tag` safety check is enforced at every lockfile
version (its download path has no checkout-time re-validation), no version
can round-trip an unsafe github tag — the v1 fallback in version_to_write
only meaningfully applies to git. Drop the dead `Github` arm and correct the
Version::V2 doc comment to list only what is actually gated to v2. No
functional change.
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI status — failure is infra flake, diff is green

Build #58840's top-level buildkite/bun status shows failing, but no individual job failed: 48 lanes pass, 6 are still pending (the linux-*-build-bun assemble/link steps), and the only failure is the aggregate status itself (Build #58840 is failing).

Key signals that this is a transient BuildKite hiccup, not a real failure:

  • Every lane that ran a build passed — all *-build-cpp / *-build-rust artifacts on every target (linux x64/aarch64/musl, macOS, Windows, FreeBSD, Android).
  • The ASAN test lane (debian-13-x64-asan-test-bun), which runs the install/lockfile tests under sanitizers — i.e. exactly where this PR's changes are exercised — passed (8m23s).
  • The 6 red-herring lanes are still pending (build-assemble), not failed; the aggregate flipped to failing while they await re-dispatch.

No leaf touches this diff is red. I haven't burned a ci: retrigger on this PR, but pushing one now would cancel the 6 in-flight build-bun lanes and wouldn't fix a non-existent test failure — so I'm leaving it for a maintainer to re-run or merge. The diff is green on the lanes that matter.

(Locally: all 6 offline tests in lockfile-version-2.test.ts pass with the debug/ASAN build; cargo check -p bun_install clean.)

Comment thread test/cli/install/lockfile-version-2.test.ts
`--lockfile-only` re-serializes the lockfile unconditionally (via
save_lockfile_only), so the omitted configVersion was an inert red herring —
it is not what triggers the re-save. Reword the comment to describe the actual
trigger and add configVersion: 1 to the input. No behavior change.
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

CI status (build #58859) — same transient aggregate flake, diff is green

Build #58859 on 70dbf31389 shows the identical non-failure pattern as the earlier build #58840:

  • 46 lanes pass, 8 pending, 0 failing leaves — the only failure is the top-level buildkite/bun aggregate status (Build #58859 is failing); no individual build/test job failed (confirmed via both the statuses and check-runs APIs).
  • The 8 pending lanes are still running (6 linux-*-build-bun assemble steps + the 2 Windows test-bun lanes) — the aggregate flipped to failing while they're in flight, not because any of them failed.
  • The ASAN test lane debian-13-x64-asan-test-bun — which runs this PR's install/lockfile tests under sanitizers — passed again, as did every build-artifact lane on every target.

No leaf that touches this diff is red. This is a BuildKite aggregate-status artifact, not a real failure. I'm not pushing a ci: retrigger: there is no failing job for a re-roll to fix, and pushing would cancel the 8 in-flight lanes. Leaving this for a maintainer to re-run the build or merge — the diff is green on every lane that has completed, and all 8 review threads are resolved.

(Locally: all 6 offline tests in lockfile-version-2.test.ts pass under the debug/ASAN build; cargo check -p bun_install clean.)

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun fix the test failures in test/cli/install/migration/yarn-lock-migration.test.ts

Comment thread test/cli/install/lockfile-version-2.test.ts Outdated
The version-selection pre-pass scanned the entire in-memory pkg_resolutions
buffer, which during a migration (e.g. yarn.lock -> bun.lock) can contain
pruned/unreferenced package entries that never reach the written `packages`
object. Such a stale entry could force lockfileVersion 1 even when every
package actually serialized was v2-valid — breaking the workspace-yarn-migration
snapshot (expected 2, got 1).

Walk the package tree the same way the writer does (tree::Iterator over the
reachable dependencies, resolving each dep to its package via resolution_buf,
skipping invalid_package_id) so version_to_write sees exactly the set of
packages that get written.
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

@Jarred-Sumner Fixed in 4509e0a.

Root cause: the version_to_write pre-pass (which decides whether to stamp lockfileVersion 2 or keep it at 1) was scanning the entire in-memory pkg_resolutions buffer. During a yarn→bun.lock migration that buffer holds pruned/unreferenced entries that never reach the serialized packages object, and one of them forced v1 for workspace-yarn-migration even though every written package was v2-valid — so the writer emitted v1 while the snapshot expected v2.

Fix: version_to_write now walks the package tree exactly like the writer (tree::Iterator → resolve each dep to its package via resolution_buf, skipping invalid_package_id), considering only the packages that actually get serialized. All 16 tests in yarn-lock-migration.test.ts pass.

(The yarn-stuff snapshot correctly stays at v1 — its off-registry ghshort tarball entry with no integrity is serialized, so it genuinely can't be v2-valid.)

@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

Correction on the earlier CI analysis

My two prior comments called build #58840/#58859 pure flake based on the status-API snapshot showing no failing leaf — but the Linux test lanes were still pending at the time I looked, and one of them (the yarn migration lane) did go on to fail for a real reason: the version_to_write bug above. I under-read that by reading the aggregate too early while test lanes were in flight. Apologies for the noise.

Now fixed in 4509e0a; yarn-lock-migration.test.ts is green locally (16/16). Pushing re-runs CI on the new sha.

Only the first two tests use `file:` deps; the rest use loopback/unreachable
endpoints. Reword the header to describe how the whole file stays offline.
Comment on lines +224 to +240
ResolutionTag::Npm => {
if pkg_metas[i].integrity.tag.is_supported() {
continue;
}
// No supported integrity: only v2-clean if the tarball
// URL is under the configured/default registry (mirrors
// the parser's `npm_url_needs_integrity` computation).
let url = res.npm().url.slice(buf);
let configured_registry = options
.scope_for_package_name(pkg_names[i].slice(buf))
.url
.href();
let under_registry = url_is_under_registry(url, configured_registry)
|| url_is_under_registry(url, Npm::Registry::DEFAULT_URL.as_bytes());
if !under_registry {
return Version::V1;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 version_to_write's "v2-clean" check for an integrity-less npm URL accepts URLs under options.scope_for_package_name(...) — the writer's configured/scoped registry — but the parser's npm_url_needs_integrity (lines 2421-2431) uses the reader's config. So a developer with a per-machine scope (e.g. @myorg → http://internal.example.com via ~/.npmrc or env) re-saving a v1 lockfile containing such a URL with empty integrity will stamp it v2, and a teammate / CI without that scope config will then fail to parse it with "Missing integrity hash…" — defeating the round-trip guarantee this function exists for, just across config boundaries. The DEFAULT_URL half of the check is config-independent and safe; consider dropping the scope_for_package_name half so the v2 stamp is conservative and reader-config-independent.

Extended reasoning...

What the bug is

version_to_write() (added in 58bc805 to fix the "re-save stamps v2 and breaks the next parse" issue) decides whether to stamp lockfileVersion: 2 by mirroring the parser's npm_url_needs_integrity predicate: for each npm package without a supported integrity hash, it checks whether the URL is under either options.scope_for_package_name(pkg_name).url or Npm::Registry::DEFAULT_URL (lines 231-237). The parser does the same at lines 2421-2431. The problem is that both halves call scope_for_package_name(...) on the current process's PackageManagerOptions — so the writer evaluates the predicate against its configured scopes, and the parser evaluates it against its own. When those differ across machines, a lockfile the writer judges "v2-clean" can be "v2-invalid" for a reader.

The specific code path

  1. Writer URL serialization (lines 916-923) only blanks the URL to "" when it's under DEFAULT_URL. A URL under a configured-but-not-default registry (e.g. http://internal.example.com) is written verbatim.
  2. version_to_write Npm arm (lines 224-240): for an entry with !integrity.tag.is_supported(), computes under_registry against the writer's options.scope_for_package_name(...)DEFAULT_URL. If true, the entry is treated as v2-clean and the loop continues; if every package passes, the function returns Version::CURRENT (v2).
  3. Parser (lines 2421-2431, 2646-2655): computes npm_url_needs_integrity against the reader's mgr.scope_for_package_name(...)DEFAULT_URL. At v2, an unsupported integrity with npm_url_needs_integrity == true fails the parse.

Why existing code doesn't prevent it

version_to_write's doc comment states the contract: "Only stamp v2 when every package already satisfies the v2 invariants; otherwise stay at v1 so the file round-trips (load → save → load) cleanly." The function correctly mirrors the parser's predicate — but the predicate itself is config-dependent for the configured-registry half, and lockfiles are explicitly meant to be committed and shared across machines with potentially different registry config (~/.npmrc, NPM_CONFIG_REGISTRY, uncommitted bunfig.toml, etc.). The DEFAULT_URL half is config-independent: the writer blanks those URLs to "" and an empty URL never sets npm_url_needs_integrity. The configured-registry half is not.

Step-by-step proof

  1. Dev A has @myorg → http://internal.example.com configured locally (e.g. in ~/.npmrc, not committed).
  2. The repo has a v1 bun.lock containing "@myorg/foo": ["@myorg/foo@1.0.0", "http://internal.example.com/@myorg/foo/-/foo-1.0.0.tgz", {}, ""] — off-default-registry, empty integrity (e.g. from a yarn/pnpm migration or older bun).
  3. Dev A runs bun add bar (or --lockfile-only). version_to_write reaches the @myorg/foo entry: integrity unsupported; options.scope_for_package_name("@myorg/foo").url = http://internal.example.com/; url_is_under_registry(url, that) = trueunder_registry = true → continues. All packages pass → returns V2.
  4. Writer emits "lockfileVersion": 2. At line 916, the URL is not under DEFAULT_URL (npmjs.org), so it's written verbatim. Integrity is written as "".
  5. Dev A commits. Dev B (or CI), with no @myorg scope configured, runs bun install. Parser reads lockfileVersion: 2. At line 2421, mgr.scope_for_package_name("@myorg/foo") falls back to the default scope → configured_registry = npmjs.org. url_is_under_registry("http://internal.example.com/...", npmjs.org) = false; same for DEFAULT_URLnpm_url_needs_integrity = true.
  6. At line 2646: at_least(V2) && npm_url_needs_integrity && !is_supported()parse fails with "Missing integrity hash for npm package resolved to a tarball URL outside the configured registry".

So a v1 lockfile that (post-PR) loads fine for Dev B becomes unloadable after Dev A re-saves it — exactly the load→save→load failure mode version_to_write was added to prevent, just split across two machines' configs.

Impact

This is not a strict regression vs. the immediately-prior release — pre-PR, Dev B would have rejected the v1 lockfile too (the check was unconditional). But it is a gap in this PR's newly-added fix: this PR makes the v1 lockfile loadable for Dev B, and version_to_write exists precisely to keep it that way through a re-save; the configured-registry half undermines that for cross-config teams. The scenario (private scoped registry, per-developer config, legacy lockfile without integrity) is the bread-and-butter enterprise case that the off-registry-integrity backward-compat is for.

Fix

In version_to_write's Npm arm, drop the configured_registry check and only treat the entry as v2-clean if the URL is under DEFAULT_URL:

let url = res.npm().url.slice(buf);
if !url_is_under_registry(url, Npm::Registry::DEFAULT_URL.as_bytes()) {
    return Version::V1;
}

This makes the v2 stamp conservative and reader-config-independent, matching the only writer-side normalization that actually happens (line 916). Packages freshly resolved from a configured registry carry integrity anyway, so they pass the is_supported() early-continue and never reach this branch — the only cost is keeping legacy private-registry-no-integrity lockfiles at v1 a little longer for single-config teams.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robobun fix this in a follow-up PR.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up in #31556 — dropped the scope_for_package_name half so the v2 stamp only trusts the default-registry normalization and is reader-config-independent. Added an offline writer→reader round-trip test across config boundaries.

@Jarred-Sumner Jarred-Sumner merged commit 973d1de into main May 29, 2026
76 of 77 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/69c82f0b/lockfile-version-2 branch May 29, 2026 02:24
@robobun

robobun commented May 29, 2026

Copy link
Copy Markdown
Collaborator Author

✅ Follow-up opened → #31556

version_to_write no longer consults the writer's scoped-registry config when deciding the v2 stamp — an integrity-less off-registry npm tarball is only treated as v2-clean when it's under the default registry (the one normalization the writer applies). So a dev with a private @scope registry re-saving a legacy lockfile keeps it at v1, and it stays loadable for teammates / CI without that scope. New offline test covers the writer→reader round-trip across config boundaries.

Jarred-Sumner pushed a commit that referenced this pull request May 29, 2026
…istry config (#31556)

Follow-up to #31539 (review
[thread](#31539 (comment))).
Jarred asked for this as a separate PR.

## What

`version_to_write` picks whether a re-saved text lockfile can be stamped
`lockfileVersion: 2`. For an npm package without a supported integrity
hash, it treated the entry as "v2-clean" if the tarball URL was under
**either** the writer's scope-configured registry
(`scope_for_package_name(...)`) **or** the default registry. The
`scope_for_package_name` half is config-dependent and undermines the
cross-machine round-trip guarantee the function exists for. This PR
drops it, keeping only the config-independent default-registry check.

## The problem

A lockfile is committed and shared, so whether the **reader** accepts it
must not depend on the **writer's** registry config. But:

1. A dev has `@myorg → http://internal.example.com` configured locally
(`~/.npmrc` / `bunfig.toml`, not committed).
2. The repo has a v1 `bun.lock` with `"@myorg/foo": ["@myorg/foo@1.0.0",
"http://internal.example.com/@myorg/foo/-/foo-1.0.0.tgz", {}, ""]` —
off-default-registry, empty integrity (e.g. from a migration or older
Bun).
3. The dev re-saves (`bun add …`, `--lockfile-only`, …).
`version_to_write` sees the entry: integrity unsupported, but the URL
**is** under `scope_for_package_name("@myorg/foo")` (their `@myorg`
scope) → treated as v2-clean → whole file stamped **v2**.
4. The writer only blanks a URL to `""` when it's under the **default**
registry, so this URL is written verbatim and integrity stays `""`.
5. A teammate / CI with **no** `@myorg` scope runs `bun install`. The
parser's `npm_url_needs_integrity` is evaluated against the *reader's*
config → the URL is under neither the reader's scope nor the default →
at v2, parse **fails** with *"Missing integrity hash for npm package
resolved to a tarball URL outside the configured registry"*.

So a v1 lockfile that (post-#31539) loads fine becomes unloadable for a
teammate after the first dev re-saves it — the exact load→save→load
failure `version_to_write` was added to prevent, just split across two
machines' configs.

## The fix

In `version_to_write`'s `Npm` arm, drop the `scope_for_package_name`
check and only treat an integrity-less entry as v2-clean when the URL is
under `Npm::Registry::DEFAULT_URL`:

```rust
let url = res.npm().url.slice(buf);
if !url_is_under_registry(url, Npm::Registry::DEFAULT_URL.as_bytes()) {
    return Version::V1;
}
```

The default-registry case is the only normalization the writer actually
performs (it blanks those URLs to `""`, and an empty URL never sets
`npm_url_needs_integrity`), so it round-trips for any reader regardless
of config. Packages freshly resolved from a configured registry carry
integrity and pass the `is_supported()` early-continue before this
branch, so the only cost is keeping legacy
private-registry-without-integrity lockfiles at v1 a little longer for
single-config teams. `options` is no longer needed, so it's dropped from
the signature.

## Verification

New test in `test/cli/install/lockfile-version-2.test.ts` — *"re-saving
keeps v1 for a tarball under a writer-only scoped registry"* — runs
fully offline:

- **Writer** has `@myorg` scoped to a loopback registry and re-saves
(`--lockfile-only`) a v1 lockfile with an `@myorg/foo` tarball under
that registry and empty integrity. The re-saved lockfile must stay
`"lockfileVersion": 1`.
- **Reader** (fresh dir, no `@myorg` scope) loads the re-saved lockfile
and must **not** hit the integrity error — proving the round-trip holds
across config boundaries.

Fail-before/pass-after confirmed by building with the `src/` change
stashed: the writer stamps `"lockfileVersion": 2` and the assertion
fails; with the change applied it stays v1 and the reader loads it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants