Skip to content

install: do not resolve an exact version from an abbreviated manifest under minimum-release-age - #38659

Open
robobun wants to merge 1 commit into
mainfrom
farm/c75ffe7c/min-age-abbreviated-manifest
Open

robobun wants to merge 1 commit into
mainfrom
farm/c75ffe7c/min-age-abbreviated-manifest

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • bun install --minimum-release-age N (or install.minimumReleaseAge in bunfig) installs a version younger than N when package.json pins an exact version and the package's abbreviated manifest is already in the install cache (for example from an earlier bun install without the setting, in any project sharing the cache). bun add pkg@x.y.z takes the same path.
  • No request is made to the registry at all, so the full manifest that carries publish times is never fetched and the age gate never runs.
  • Cause, in the npm branch of enqueue_dependency_with_main_and_success_fn (src/install/PackageManager/PackageManagerEnqueue.rs:1035-1110 on main):
    • by_name_hash_allow_expired hands back the cached manifest even when an age gate is set and the manifest is abbreviated; it only marks it expired (src/install/PackageManifestMap.rs:186 and :217). That is deliberate: the manifest's ETag is needed for the network request further down.
    • The exact-version shortcut right after it (:1049) does not check which flavor it got. An abbreviated manifest has publish_timestamp_ms == 0 for every version (src/install/npm.rs:764), so is_package_version_too_recent (npm.rs:1558) is false and the pin is resolved from the cache.
  • The regular resolution path is not affected: get_or_put_resolved_package uses by_name_hash (PackageManagerEnqueue.rs:2424), which refuses an abbreviated manifest when an age gate is set. Only the exact-version shortcut bypasses it. The same structure existed before the Rust port, so this is not a regression.

Fix

  • The shortcut now also requires !needs_extended_manifest || manifest.pkg.has_extended_manifest. With an age gate and an abbreviated manifest, the code falls through to the network request it would have made for any non-exact specifier.
  • Correct because that request already handles this exact state: NetworkTask::for_manifest asks for the full manifest and skips the conditional headers when the loaded manifest is abbreviated (src/install/NetworkTask.rs:567), so the registry returns the full document, and the re-enqueued dependency is then resolved (or rejected) by the regular path with real publish times. The condition is the same one PackageManifestMap and for_manifest already use to decide whether a manifest is good enough under an age gate.
  • Without an age gate, needs_extended_manifest is false and the shortcut behaves exactly as before. With an age gate and a full manifest in the cache (even a stale one), the shortcut is still taken and still applies its own age check; two of the new tests pin that down.
  • Packages listed in minimumReleaseAgeExcludes with an exact pin now also refetch the full manifest once, like they already did for non-exact specifiers.
  • Verified with test/cli/install/minimum-release-age.test.ts, new exact version with a cached manifest block (5 tests):
    • The three abbreviated-manifest tests (bun install on a too-recent pin, bun install on an old enough pin, bun add on a too-recent pin) fail on the released binary: the too-recent pins install with zero registry requests, and the old enough pin resolves without the full-manifest request.
    • The two full-manifest tests pass before and after; they use BUN_MANIFEST_CACHE=1 so the cached manifest counts as stale and the shortcut is the path taken (confirmed by its distinct error message).
    • All 54 tests in the file pass with bun bd test.
    • The mock registry now records each request's path and Accept header; the new tests use that to assert whether the abbreviated or the full manifest was requested.

Background

  • Abbreviated vs full manifest: the registry serves two documents for a package. With Accept: application/vnd.npm.install-v1+json it returns the abbreviated one (versions and tarballs, no time field); with Accept: application/json it returns the full one, including time with a publish date per version. Bun asks for the full one only when a minimum release age is configured (needs_extended_manifest), and records which one it parsed in pkg.has_extended_manifest. Both are written to the same cache file, so a later process can load either flavor.
  • Exact-version shortcut: when package.json pins pkg@1.2.3 and a cached manifest (even one past its freshness window) contains that version, bun resolves it from the cache instead of re-requesting the manifest, since a published version does not change. The network request below the shortcut is what every other specifier uses when the cached manifest cannot be used.
  • BUN_MANIFEST_CACHE=1 (used by two tests): keep reading the manifest cache but treat every cached manifest as past its freshness window, which is the state the shortcut exists for.

… under minimum-release-age

An install without minimum-release-age caches the abbreviated manifest,
which the registry serves without publish times. A later install with
minimum-release-age and an exact version in package.json resolved that
version straight from the cached manifest: every version in it has a
publish time of 0, so the age check passed and no request was made for
the full manifest.

Skip the exact-version shortcut when an age gate is set and the cached
manifest is not the full one, so the install falls through to the
network request that already asks for the full manifest.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef4e47b7-4b28-4288-91bc-a4bf073173c7

📥 Commits

Reviewing files that changed from the base of the PR and between 97a4363 and 429e2c3.

📒 Files selected for processing (2)
  • src/install/PackageManager/PackageManagerEnqueue.rs
  • test/cli/install/minimum-release-age.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed, waiting on CI.

Reproduced on the released binary (1.4.0-canary.1) with a mock registry that, like the real one, only includes time in the full manifest: a plain bun install of an exact pin in one directory, then bun install --minimum-release-age 432000 of the same pin (1 day old) in a second directory sharing the cache installs it and makes no registry request. Same with bun add pkg@x.y.z.

The new exact version with a cached manifest tests in test/cli/install/minimum-release-age.test.ts fail on the released binary (3 of 5; the other 2 pin down that the shortcut still works for a cached full manifest) and all 54 tests in the file pass with the fix.

@claude claude Bot left a comment

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.

LGTM — one-condition guard that closes an age-gate bypass in the exact-version shortcut, using the same needs_extended_manifest && !has_extended_manifest predicate already applied at PackageManifestMap.rs:186/217 and NetworkTask.rs:567.

What was reviewed

  • Verified loaded_manifest is Some two lines above the added .unwrap() (line 1045), matching the existing .unwrap()s on 1062/1076/1108.
  • Traced the fall-through: by_name_hash_allow_expired sets expired = true for an abbreviated manifest under an age gate, so the !expired fast path at 1137 is skipped and for_manifest at ~1170 drops conditional headers and requests the full document.
  • Confirmed is_package_version_too_recent (npm.rs:1558) returns false for publish_timestamp_ms == 0, which is why the abbreviated shortcut silently passed before.
  • Tests: shared registryRequests array is safe because the new tests are serial and each run slices from a captured start index; each test uses an isolated BUN_INSTALL_CACHE_DIR.
Extended reasoning...

Overview

Adds a single guard to the exact-version resolution shortcut in enqueue_dependency_with_main_and_success_fn (src/install/PackageManager/PackageManagerEnqueue.rs): when minimum-release-age is configured, the shortcut is skipped unless the cached manifest is the extended (full) variant with real publish timestamps. Five new tests in test/cli/install/minimum-release-age.test.ts cover the abbreviated-cache-then-gated matrix (install block/allow, bun add block) plus two pinning tests that the shortcut still fires with a stale full manifest. The mock registry gains a per-request log so tests can assert which manifest flavor was fetched.

Security risks

The feature under repair is itself a supply-chain guard. This change strictly tightens it — the only new behavior is refusing to resolve from a cache entry that lacks publish times and instead fetching the full manifest. There is no path where the change loosens a check. A bug here would degrade to an extra registry request, not a bypass.

Level of scrutiny

Medium. The touched function is on the package manager's hot enqueue path, but the change is a single boolean conjunction that mirrors the exact predicate used at three other sites in the same subsystem (PackageManifestMap.rs:186, PackageManifestMap.rs:217, NetworkTask.rs:567). No new state, no control-flow restructuring, no allocation. The .unwrap() on loaded_manifest is provably safe (assigned Some immediately above) and matches four existing .unwrap()s in the same block.

Other factors

  • The PR description cites specific line numbers for every claim (by_name_hash_allow_expired demoting to expired, publish_timestamp_ms == 0 for abbreviated manifests, for_manifest dropping ETag/If-Modified-Since when upgrading); I verified each against the current tree.
  • The fall-through path was already exercised by every non-exact specifier under an age gate, so the network-task machinery being reused is not new.
  • Tests follow harness conventions: tempDir, bunEnv spread, Promise.all draining stdout/stderr/exited, per-test isolated cache dir via BUN_INSTALL_CACHE_DIR, BUN_MANIFEST_CACHE=1 to force the stale-shortcut path for the two pinning tests.
  • No CODEOWNERS entry covers src/install/. No prior reviewer comments to address.

This branch has not been deployed

No deployments
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.

1 participant