Skip to content

fix(pnpr): verify proxied tarball integrity#12570

Merged
zkochan merged 1 commit into
mainfrom
pnpr-vuln
Jun 22, 2026
Merged

fix(pnpr): verify proxied tarball integrity#12570
zkochan merged 1 commit into
mainfrom
pnpr-vuln

Conversation

@zkochan

@zkochan zkochan commented Jun 22, 2026

Copy link
Copy Markdown
Member

Summary

  • Verifies proxied pnpr tarballs against the selected version dist.integrity before serving or caching them.
  • Stores a verified cache marker with canonical SRI and length so cache hits skip repeat hashing unless the marker is missing, malformed, or mismatched.
  • Enforces a 100 MiB spool limit for verified tarball downloads, including cache: false uplinks.
  • Creates tarball and cache-marker temp files with exclusive, retrying, randomized temp paths so pre-existing files or symlinks are not clobbered.
  • Rejects missing, malformed, empty, or unsupported tarball integrity metadata before fetching bytes.
  • No TypeScript CLI or pacquet/ port is needed; this change is scoped to the pnpr registry server.

Addresses https://github.com/pnpm/pnpm/security/advisories/GHSA-5f9g-98vq-2jxw.

Squash Commit Body

Bind proxied tarball requests to the selected packument version.

Require supported dist.integrity before serving upstream or cached tarballs.

Verify cache hits without repeated hashing when their cache marker matches the expected SRI and byte length. Missing, malformed, or mismatched markers still force full verification and marker refresh.

Delete invalid cache entries and promote upstream bytes only after SRI verification.

For cache:false uplinks, verify into a temp file and remove it after streaming.

Bound verified tarball spooling to 100 MiB using both Content-Length and a running byte counter.

Create tarball and cache-marker temp files with exclusive, retrying, randomized paths so pre-existing files or symlinks are not clobbered.

Harden publish attachment SRI parsing for missing or unsupported integrity.

Addresses GHSA-5f9g-98vq-2jxw.

Checklist

  • Added or updated tests.

Written by an agent (Codex, GPT-5).

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Enforced strict SRI integrity verification for proxied tarballs using each version’s declared dist.integrity. Missing, malformed, or unsupported values now fail with gateway-style errors and prevent serving/caching corrupted content. Absent versions return 404.
  • Improvements
    • Hardened tarball cache behavior with integrity-aware replay, discard-on-mismatch, and cleanup of temporary/sidecar artifacts.
  • Tests
    • Added coverage for integrity parsing, publish rejection cases, oversized/cancelled downloads, and cache/temp cleanup.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end SRI tarball integrity verification to the pnpr proxy. A new RegistryError::TarballIntegrity variant is introduced, the streaming module is rewritten with integrity-aware download helpers replacing tee_to_cache, TarballWrite gains an Option-based lifecycle, serve_tarball is rewritten to verify cached and upstream tarballs against packument-declared integrity, upstream URL rewriting now derives canonical filenames from version keys, and extensive integration tests validate the complete flow.

Changes

Tarball Integrity Verification

Layer / File(s) Summary
Error type and PackageName API contracts
pnpr/crates/pnpr/src/error.rs, pnpr/crates/pnpr/src/package_name.rs, pnpr/crates/pnpr/src/package_name/tests.rs
Adds RegistryError::TarballIntegrity variant with EINTEGRITY display, "tarball_integrity" log kind, and 502 BAD_GATEWAY status; introduces PackageName::tarball_name_for_version() helper to centralize canonical filename construction; updates unit tests to verify the new API.
TarballWrite lifecycle and Storage tarball API
pnpr/crates/pnpr/src/storage.rs, pnpr/crates/pnpr/src/storage/tests.rs
Refactors TarballWrite to use Option-wrapped file/temp-path fields with new write_all, finalize, into_temp_file, abandon methods; introduces CachedTarballIntegrity sidecar struct with integrity metadata; replaces composed open_tarball with explicit open_hosted_tarball and open_cached_tarball/remove_cached_tarball integrity APIs; updates temp-file creation with randomized suffixes and retry logic; adds tests for integrity roundtrip, sidecar removal, symlink safety, and finalize-failure cleanup.
Streaming integrity verification primitives
pnpr/crates/pnpr/src/streaming.rs, pnpr/crates/pnpr/src/streaming/tests.rs
Replaces tee_to_cache with TarballStreamError enum, parse_integrity/integrity_checker SRI validation helpers, verify_file for cached-file verification, download_verified_to_cache/download_verified_to_temp for upstream download with incremental hashing and verification, and stream_file_and_remove for temp-file streaming with best-effort cleanup; adds unit tests for SRI parsing acceptance/rejection and async tests for in-flight cancellation cleanup and oversized-response handling.
Upstream URL rewriting and version-keyed tarball naming
pnpr/crates/pnpr/src/upstream.rs, pnpr/crates/pnpr/src/upstream/tests.rs
Updates rewrite_tarball_urls to iterate only versions entries and derive canonical filenames via tarball_name_for_version(version) instead of extracting basename from existing URLs; updates extract_version_manifest to call rewrite_dist_tarball directly; adds test fixtures with deliberately mismatched filenames to validate version-keyed rewriting logic.
Publish stream integrity validation
pnpr/crates/pnpr/src/publish.rs, pnpr/crates/pnpr/src/publish/tests.rs
Updates publish tarball verification to use shared parse_integrity and integrity_checker helpers from crate::streaming instead of direct ssri imports, reducing duplication; adds tests validating rejection of empty-hash and unsupported-algorithm integrity values with EINTEGRITY error messages.
serve_tarball integrity-verified handler
pnpr/crates/pnpr/src/server.rs
Rewrites serve_tarball to parse tarball name/version, serve hosted tarballs first (bypassing integrity checks), derive expected integrity from packument, verify cached tarballs before serving (with invalid-cache discard), and route upstream fetches through integrity-aware download paths. Introduces MAX_TARBALL_BYTES (100 MiB) limit and helpers for error mapping, cache invalidation, and integrity-marker persistence. Adds expected_tarball_integrity, tarball_stream_error, discard_cached_tarball, and integrity-marker-recording helpers.
Server integration test expansion
pnpr/crates/pnpr/tests/server.rs
Adds sha512_integrity and mock_packument_for_tarball helpers to generate integrity-consistent mocked packuments; wires all existing tarball tests with integrity metadata; adds new tests for version-keyed cache routing, tampered-tarball rejection (upstream and cache:false), missing/invalid integrity early rejection, and concurrent-fetch settlement behavior; updates truncated-upstream TCP server to embed integrity in packument and updates assertions to require BAD_GATEWAY on stream errors; refactors cache inspection utilities to handle canonical and temporary tarball entries consistently.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant serve_tarball
  participant Storage
  participant expected_tarball_integrity
  participant verify_file
  participant download_verified_to_cache
  participant download_verified_to_temp

  Client->>serve_tarball: GET /:pkg/-/:tarball.tgz
  serve_tarball->>serve_tarball: parse_tarball_name → version
  serve_tarball->>Storage: open_hosted_tarball
  alt hosted tarball exists
    Storage-->>Client: 200 Body (hosted)
  end
  serve_tarball->>expected_tarball_integrity: packument JSON + version
  alt integrity absent or malformed
    expected_tarball_integrity-->>serve_tarball: RegistryError::TarballIntegrity
    serve_tarball-->>Client: 502 BAD_GATEWAY
  end
  serve_tarball->>Storage: open_cached_tarball
  alt cache hit
    serve_tarball->>verify_file: File + Integrity
    alt verification OK
      serve_tarball-->>Client: 200 Body (cached)
    else verification failed
      serve_tarball->>Storage: remove_cached_tarball
    end
  end
  serve_tarball->>serve_tarball: select upstream
  alt cache:true
    serve_tarball->>download_verified_to_cache: Response + TarballWrite + Integrity
    download_verified_to_cache-->>serve_tarball: u64 (verified size)
    serve_tarball->>Storage: open_cached_tarball + verify_file
    serve_tarball-->>Client: 200 Body (newly cached)
  else cache:false
    serve_tarball->>download_verified_to_temp: Response + TarballWrite + Integrity
    download_verified_to_temp-->>serve_tarball: (File, u64, PathBuf)
    serve_tarball->>serve_tarball: stream_file_and_remove
    serve_tarball-->>Client: 200 Body (temp deleted on Drop)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • pnpm/pnpm#12195: The main PR extends the hosted-vs-proxy cached tarball storage plumbing introduced in #12195 (same pnpr/crates/pnpr/src/storage.rs/server.rs concepts like open_hosted_tarball/cache paths) by adding SRI integrity sidecars and refactoring TarballWrite/download verification around that two-root layout.
  • pnpm/pnpm#12198: Both PRs modify the tarball serving pipeline—feat(pnpr): store hosted packages in an S3-compatible object store #12198 restructures Storage::open_tarball/hosted serving in storage.rs and adjusts /tarball handling in server.rs, while the main PR extends server.rs::serve_tarball to enforce dist.integrity SRI and adds cached tarball integrity sidecars/error handling on top of that streaming tarball flow.
  • pnpm/pnpm#12205: Both PRs are tied to SRI dist.integrity handling for cached/proxied packuments: the main PR enforces tarball integrity by reading versions[version].dist.integrity, while the retrieved PR updates the test registry-mock to fetch that integrity from pnpr's .pnpr-cache packument location.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pnpr-vuln

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

fix(pnpr): verify proxied tarball SRI before serving or caching
🐞 Bug fix 🧪 Tests ✨ Enhancement 🕐 40+ Minutes

Grey Divider

Description

• Verify proxied tarballs against packument-selected version dist.integrity before
 serving/caching.
• Rewrite dist.tarball URLs by versions keys to prevent cross-version route poisoning.
• Fail closed on missing/invalid integrity and add coverage for cache/temp cleanup paths.
Diagram

graph TD
C{{"Client"}} --> S["pnpr registry"]
S --> H[("Hosted store")]
S --> P["Packument lookup"] --> I["Expected dist.integrity"]
S --> K[("Proxy cache")]
K --> V["SRI verify"]
S --> U{{"Upstream registry"}} --> D["Download+verify"] --> K

subgraph Legend
  direction LR
  _svc["Service/Handler"] ~~~ _db[("Storage")] ~~~ _ext{{"External"}}
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Verify while streaming directly to client
  • ➕ Lower latency (no pre-buffering to disk)
  • ➕ No extra disk I/O for cache:false uplinks
  • ➖ Cannot fail closed: bytes may be sent before integrity mismatch is detected
  • ➖ Harder to guarantee cache isn’t poisoned on partial writes/client disconnects
2. Cache verification sidecar (trust-after-first-verify)
  • ➕ Avoids re-hashing cached tarballs on every hit
  • ➕ Keeps response path fast for hot artifacts
  • ➖ Adds metadata management and invalidation complexity
  • ➖ Still vulnerable to disk corruption/tampering without periodic re-verify

Recommendation: The PR’s approach (derive expected SRI from the selected version, then verify cache hits and upstream downloads before serving) is the safest default for a registry proxy: it fails closed and prevents cross-version tarball poisoning. If performance becomes an issue later, consider an optional sidecar/verified-bit optimization, but keep the current full verification as the secure baseline.

Files changed (13) +993 / -272

Enhancement (2) +236 / -118
storage.rsSeparate hosted vs cached tarball access and harden temp writer cleanup +88/-36

Separate hosted vs cached tarball access and harden temp writer cleanup

• Adds 'open_hosted_tarball' and changes cached tarball opens to return raw files for pre-response verification; adds targeted removal for invalid cached tarballs. Refactors 'TarballWrite' to support 'write_all', 'into_temp_file', and guaranteed temp cleanup via 'Drop' across errors and cancellation.

pnpr/crates/pnpr/src/storage.rs

streaming.rsReplace tee-to-cache with verified download and cache-hit verification flows +148/-82

Replace tee-to-cache with verified download and cache-hit verification flows

• Introduces SRI parsing/validation helpers and new streaming primitives: verify cached files, download+verify into cache, and download+verify into temp for cache:false. Adds a stream wrapper that removes temp files after streaming completes.

pnpr/crates/pnpr/src/streaming.rs

Bug fix (4) +162 / -44
error.rsAdd TarballIntegrity error and map it to 502 +13/-1

Add TarballIntegrity error and map it to 502

• Introduces a dedicated EINTEGRITY-style error variant for tarball integrity failures. Adds metric/error-kind classification and maps the new error to BAD_GATEWAY responses.

pnpr/crates/pnpr/src/error.rs

publish.rsHarden publish attachment integrity parsing and verification +8/-5

Harden publish attachment integrity parsing and verification

• Switches dist.integrity handling to shared streaming helpers that reject empty/unsupported SRI metadata. Ensures the publish decode+verify flow requires supported integrity hashes before writing final artifacts.

pnpr/crates/pnpr/src/publish.rs

server.rsVerify proxied tarballs against packument-selected dist.integrity +128/-21

Verify proxied tarballs against packument-selected dist.integrity

• Binds tarball requests to the parsed '<pkg>-<version>.tgz' version, loads the packument, and derives expected 'dist.integrity' for that version. Verifies cached tarballs before responding, deletes invalid cache entries, and verifies upstream downloads into temp/cache before constructing responses; cache:false uplinks stream from verified temp files and remove them after.

pnpr/crates/pnpr/src/server.rs

upstream.rsRewrite tarball URLs using versions-map keys +13/-17

Rewrite tarball URLs using versions-map keys

• Changes tarball URL rewriting to derive the server route from the 'versions' map key rather than the upstream tarball URL basename. Also binds version-manifest rewriting directly to the resolved version key to prevent cross-version routing/integrity mismatch attacks.

pnpr/crates/pnpr/src/upstream.rs

Refactor (1) +3 / -12
package_name.rsDerive canonical tarball filename from version +3/-12

Derive canonical tarball filename from version

• Replaces the old tarball-name validation entrypoint with a helper that deterministically formats '<basename>-<version>.tgz'. Reuses this helper in tarball-name parsing/canonicalization.

pnpr/crates/pnpr/src/package_name.rs

Tests (6) +592 / -98
tests.rsUpdate package-name tests for new tarball naming API +7/-5

Update package-name tests for new tarball naming API

• Adjusts tests to assert 'tarball_name_for_version' output and to validate via 'parse_tarball_name'. Keeps rejection coverage for invalid/other-package filenames.

pnpr/crates/pnpr/src/package_name/tests.rs

tests.rsAdd publish tests for empty and unsupported SRI +20/-0

Add publish tests for empty and unsupported SRI

• Adds cases ensuring empty/whitespace integrity strings are rejected and unsupported algorithms fail early. Confirms no destination artifact is written on failure.

pnpr/crates/pnpr/src/publish/tests.rs

tests.rsTest TarballWrite temp cleanup on failed finalize +16/-0

Test TarballWrite temp cleanup on failed finalize

• Adds an async test that forces rename failure during finalize and asserts the temporary tarball file is removed. Covers the new Drop/cleanup expectations for cache writers.

pnpr/crates/pnpr/src/storage/tests.rs

tests.rsAdd streaming tests for SRI parsing and cancellation cleanup +125/-0

Add streaming tests for SRI parsing and cancellation cleanup

• Adds unit tests for supported/unsupported integrity parsing and ensures integrity checkers reject empty-hash SRIs. Includes an integration-style test that aborting an in-flight verified download removes the temp file and does not leave a final cache entry.

pnpr/crates/pnpr/src/streaming/tests.rs

tests.rsUpdate upstream rewrite tests for version-key binding +2/-5

Update upstream rewrite tests for version-key binding

• Adjusts expectations so rewritten tarball URLs follow the version map keys even when upstream tarball basenames disagree. Keeps coverage for scoped package behavior and dist-tag manifest extraction.

pnpr/crates/pnpr/src/upstream/tests.rs

server.rsExpand server tests for integrity enforcement and fail-closed proxying +422/-88

Expand server tests for integrity enforcement and fail-closed proxying

• Adds helpers to generate packuments with sha512 SRI and exercises new security properties: selected version controls tarball route and offline cache key; tampered upstream/cached tarballs are rejected and not persisted; missing/invalid integrity fails before fetch; cache:false uplinks verify to temp and leave no mirror artifacts. Updates prior proxy/cache tests to account for packument prefetch and fail-closed behavior.

pnpr/crates/pnpr/tests/server.rs

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (17) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Hosted errors fail open 🐞 Bug ⛨ Security
Description
In serve_tarball(), an error opening a hosted tarball only logs a warning and then falls through
into the proxy/cache/upstream flow, so a request for a locally-hosted (authoritative) package can be
served from an upstream source during hosted storage faults. This violates the “hosted is
authoritative” trust boundary and can serve bytes from the wrong provenance for the same package
name.
Code

pnpr/crates/pnpr/src/server.rs[R795-801]

+    match state.inner.storage.open_hosted_tarball(&name, &filename).await {
     Ok(Some((body, len))) => return tarball_response(body, len),
     Ok(None) => {}
     Err(err) => {
-            tracing::warn!(?err, package = %name.as_str(), %filename, canonical = %canonical_filename, "tarball cache open failed");
+            tracing::warn!(?err, package = %name.as_str(), %filename, "hosted tarball open failed");
     }
 }
Evidence
The new tarball handler explicitly logs and continues on hosted tarball open failures, and then
proceeds to load packument integrity and potentially fetch from upstream, enabling fail-open
behavior across the hosted/proxy trust boundary.

pnpr/crates/pnpr/src/server.rs[795-856]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serve_tarball()` treats `open_hosted_tarball()` errors as non-fatal (warn + continue), which allows the handler to continue into proxy-cache and upstream fetch paths.
Because the hosted store is intended to be authoritative, a hosted-store I/O error should be surfaced as an error response (fail closed) rather than falling back to upstream bytes.
### Issue Context
- The handler already distinguishes `Ok(None)` (not hosted) from `Err(_)` (real I/O/backend failure).
- Falling through on `Err(_)` can serve the tarball from `resolve_upstream()` even when the hosted store should have been the source of truth.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[795-801]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Temp tarball reopen race 🐞 Bug ⛨ Security
Description
TarballWrite::into_temp_file() drops the verified write handle and then re-opens the temp path for
streaming, creating a TOCTOU window where the streamed bytes can differ from the bytes hashed in
download_verified_to_temp(). If the cache directory is attacker-writable, this can bypass the new
integrity check for cache:false uplinks by swapping the temp file after verification but before
open().
Code

pnpr/crates/pnpr/src/storage.rs[R102-115]

+    pub async fn into_temp_file(mut self) -> std::io::Result<(fs::File, u64, PathBuf)> {
+        let Some(file) = self.file.take() else {
+            return Err(std::io::Error::other("tarball cache writer is closed"));
+        };
+        file.sync_all().await?;
+        let len = file.metadata().await?.len();
+        let tmp_path = self
+            .tmp_path
+            .clone()
+            .ok_or_else(|| std::io::Error::other("tarball cache temp path is missing"))?;
+        drop(file);
+        let file = fs::File::open(&tmp_path).await?;
+        self.tmp_path = None;
+        Ok((file, len, tmp_path))
Evidence
The new cache:false flow verifies the body and then calls into_temp_file(); into_temp_file() reopens
by path after dropping the verified handle, which is exactly the kind of filesystem TOCTOU the
repo’s security guide flags as in-scope for attacker-controlled paths.

pnpr/crates/pnpr/src/storage.rs[102-115]
pnpr/crates/pnpr/src/streaming.rs[98-109]
REVIEW_GUIDE.md[30-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TarballWrite::into_temp_file()` currently `sync_all()`s, **drops** the write handle, and then re-opens `tmp_path` for reading. This introduces a TOCTOU window where a local attacker who can modify the cache directory can replace the temp file after verification but before the `open()`, causing pnpr to stream bytes that were not the ones that passed SRI verification.
### Issue Context
- The cache:false tarball flow verifies via `streaming::download_verified_to_temp(...)` and then streams the file returned by `TarballWrite::into_temp_file()`.
- REVIEW_GUIDE requires treating filesystem paths as attacker-controlled and calls out TOCTOU risks.
### Fix approach
- Open the temp file as **read+write** from the start (`OpenOptions::read(true).write(true).create_new(true)`), so you can:
1) write+hash during download,
2) `sync_all()`, get `len`, then `seek(0)`,
3) return the **same open file handle** for streaming (no re-open by path).
- Update `into_temp_file()` accordingly (seek to start instead of reopening).
### Fix Focus Areas
- pnpr/crates/pnpr/src/storage.rs[102-115]
- pnpr/crates/pnpr/src/storage.rs[736-745]
- pnpr/crates/pnpr/src/streaming.rs[98-109]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. cache:false temp-file clobber 🐞 Bug ⛨ Security
Description
serve_tarball now spools cache:false uplinks into a temp file under cache_storage via
open_cached_tarball_tmp, which uses deterministic *.tmp.. names created with File::create
(follows symlinks). If an attacker can plant symlinks in cache_storage (e.g., shared writable
cache dir), a remote tarball GET can be turned into truncation/overwrite of an arbitrary file the
pnpr process can write.
Code

pnpr/crates/pnpr/src/server.rs[R842-884]

+    let write = match state.inner.storage.open_cached_tarball_tmp(&name, &filename).await {
 Ok(w) => w,
-        Err(err) => {
-            tracing::warn!(?err, package = %name.as_str(), %filename, "tarball cache tmp-open failed; streaming without cache");
-            let body = Body::from_stream(response.bytes_stream());
-            return tarball_response(body, upstream_len);
+        Err(err) => return error_response(&err),
+    };
+
+    if upstream.caches() {
+        let len = match streaming::download_verified_to_cache(
+            response,
+            write,
+            &integrity,
+            MAX_TARBALL_BYTES,
+        )
+        .await
+        {
+            Ok(len) => len,
+            Err(err) => return error_response(&tarball_stream_error(err, &name, &filename)),
+        };
+        record_cached_tarball_integrity(
+            state,
+            &name,
+            &filename,
+            cached_tarball_integrity(&integrity, len),
+        )
+        .await;
+
+        match state.inner.storage.open_cached_tarball(&name, &filename).await {
+            Ok(Some((file, len))) => tarball_response(streaming::stream_file(file), Some(len)),
+            Ok(None) => error_response(&tarball_integrity_error(
+                &name,
+                &filename,
+                "verified cache entry disappeared before it could be served".to_string(),
+            )),
+            Err(err) => error_response(&err),
 }
+    } else {
+        match streaming::download_verified_to_temp(response, write, &integrity, MAX_TARBALL_BYTES)
+            .await
+        {
+            Ok((file, len, tmp_path)) => {
+                tarball_response(streaming::stream_file_and_remove(file, tmp_path), Some(len))
+            }
+            Err(err) => error_response(&tarball_stream_error(err, &name, &filename)),
+        }
Evidence
The handler now always opens a cache temp writer before serving upstream tarballs, even on
cache:false uplinks, and the storage layer creates that temp file at a deterministic name via
File::create; deterministic names plus non-exclusive open is the standard symlink-clobber pattern
when the directory is attacker-writable.

pnpr/crates/pnpr/src/server.rs[761-885]
pnpr/crates/pnpr/src/storage.rs[579-587]
pnpr/crates/pnpr/src/storage.rs[723-736]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`cache:false` tarball requests now require creating a temp file under `cache_storage` before responding. The temp filename is predictable (`<final>.tmp.<pid>.<counter>`) and the file is opened with `tokio::fs::File::create`, which follows symlinks. In deployments where `cache_storage` is writable by other principals (shared volume, misconfigured perms), this enables a local symlink clobber that a remote request can trigger.
### Issue Context
This is a local-filesystem precondition issue (attacker must be able to plant symlinks in `cache_storage`), but the PR newly makes it reachable for `cache:false` uplinks (previously mirror-less streaming avoided any disk write).
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[842-884]
- pnpr/crates/pnpr/src/storage.rs[579-587]
- pnpr/crates/pnpr/src/storage.rs[723-736]
### Concrete fix
1. Change cache temp-file creation to be exclusive and symlink-resistant:
- Replace `fs::File::create(&tmp_path)` with `tokio::fs::OpenOptions::new().write(true).create_new(true).open(&tmp_path)`.
- If the open fails with `AlreadyExists`, generate a new tmp name (ideally add randomness, not just pid+counter) and retry in a small loop.
2. Apply the same exclusive-create pattern to any other temp write paths used by tarball serving (including the `.integrity` sidecar writer if it uses the same temp strategy).
3. Add a focused test that pre-creates a file (or symlink on Unix) at the predicted tmp path and asserts the server does not overwrite it (i.e., it retries to a different temp name or fails safely).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. Unbounded temp tarball writes 🐞 Bug ⛨ Security
Description
streaming::download_verified writes the full upstream body into a temp file without any
maximum-size enforcement, and serve_tarball uses this path even for cache: false uplinks. A
malicious/compromised uplink (or many concurrent requests for very large tarballs) can exhaust disk
space via in-flight temp files before a response is served.
Code

pnpr/crates/pnpr/src/streaming.rs[R105-127]

+async fn download_verified(
+    response: reqwest::Response,
+    write: &mut TarballWrite,
+    integrity: &Integrity,
+) -> Result<(), TarballStreamError> {
+    let url = response.url().to_string();
+    let mut upstream = response.bytes_stream();
+    let mut checker = integrity_checker(integrity).map_err(TarballStreamError::Integrity)?;
+    while let Some(chunk_result) = upstream.next().await {
+        let chunk = match chunk_result {
+            Ok(chunk) => chunk,
+            Err(source) => return Err(TarballStreamError::Upstream { url, source }),
+        };
+        if let Err(err) = write.write_all(&chunk).await {
+            return Err(TarballStreamError::Io(err));
+        }
+        checker.input(&chunk);
+    }
+
+    if let Err(err) = checker.result() {
+        return Err(TarballStreamError::Integrity(err));
+    }
+    Ok(())
Evidence
The tarball verification loop writes every received chunk to disk and never checks total bytes or
Content-Length. serve_tarball always opens a cache temp writer and uses the temp-file download
path for cache:false uplinks, so disk usage scales with concurrent tarball size even when
mirroring is disabled.

pnpr/crates/pnpr/src/streaming.rs[105-127]
pnpr/crates/pnpr/src/server.rs[822-854]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`download_verified()` streams an upstream tarball into a temp file with no upper bound. Because `serve_tarball()` now spools even `cache:false` uplinks to disk for verification, a large/never-ending response (chunked transfer) or high concurrency can fill the cache volume and take the registry down.
### Issue Context
This PR intentionally buffers tarballs to fail closed on SRI mismatch, but buffering needs a hard byte limit (and ideally a per-uplink/operator-configurable cap) to prevent disk exhaustion.
### Fix Focus Areas
- pnpr/crates/pnpr/src/streaming.rs[105-127]
- pnpr/crates/pnpr/src/server.rs[822-854]
### What to change
- Add a maximum allowed tarball size (configurable is best; otherwise a conservative default).
- Enforce the limit in `download_verified()`:
- If `response.content_length()` is `Some(len)` and `len > max`, abort immediately.
- Maintain a running `written` counter; if `written > max`, abort and abandon the temp file.
- Plumb the limit from `Config`/uplink settings into the tarball handler and then into `download_verified_to_cache` / `download_verified_to_temp`.
- Return a controlled error (e.g. `RegistryError::TarballIntegrity { reason: "tarball too large" }` or a dedicated error kind) so it maps to a stable status code and logs clearly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Double-reads tarballs per request 🐞 Bug ➹ Performance
Description
serve_tarball() now fully hashes cached tarballs before serving them, then streams them (a second
full read), and after an upstream download it also re-opens + re-hashes the just-written cache entry
before streaming. This makes tarball GETs O(size of tarball) extra disk I/O (often twice), which can
bottleneck the registry under load (high-QPS popular packages, large tarballs).
Code

pnpr/crates/pnpr/src/server.rs[R782-846]

+    let packument = match load_packument_bytes(state, &name).await {
+        PackumentLoad::Ok(bytes) => bytes,
+        PackumentLoad::NotFound => return not_found(),
+        PackumentLoad::Err(err) => return error_response(&err),
+    };
+    let integrity = match expected_tarball_integrity(&packument, &name, &filename, &version) {
+        Ok(Some(integrity)) => integrity,
+        Ok(None) => return not_found(),
+        Err(err) => return error_response(&err),
+    };
+
+    let upstream = resolve_upstream(state, &name);
+    let should_read_cache = upstream.as_ref().is_none_or(|upstream| upstream.caches());
+    if should_read_cache {
+        match state.inner.storage.open_cached_tarball(&name, &filename).await {
+            Ok(Some((file, len))) => match streaming::verify_file(file, &integrity).await {
+                Ok(file) => return tarball_response(streaming::stream_file(file), Some(len)),
+                Err(err) => {
+                    let err = tarball_stream_error(err, &name, &filename);
+                    tracing::warn!(?err, package = %name.as_str(), %filename, "cached tarball failed verification");
+                    discard_cached_tarball(state, &name, &filename).await;
+                }
+            },
+            Ok(None) => {}
+            Err(err) => {
+                tracing::warn!(?err, package = %name.as_str(), %filename, "tarball cache open failed");
+            }
+        }
+    }
+
+    let Some(upstream) = upstream else {
return not_found();
};
-    let response = match upstream.fetch_tarball_response(&name, filename).await {
+    let response = match upstream.fetch_tarball_response(&name, &filename).await {
Ok(FetchOutcome::Ok(response)) => response,
Ok(FetchOutcome::NotFound) => return not_found(),
Err(err) => return error_response(&err),
};
-    let upstream_len = response.content_length();
-
-    // `cache: false` uplinks (verdaccio) are mirror-less: stream the
-    // tarball straight to the client without writing it to disk.
-    if !upstream.caches() {
-        return tarball_response(Body::from_stream(response.bytes_stream()), upstream_len);
-    }
-    let write = match state.inner.storage.open_cached_tarball_tmp(&name, filename).await {
+    let write = match state.inner.storage.open_cached_tarball_tmp(&name, &filename).await {
Ok(w) => w,
-        Err(err) => {
-            tracing::warn!(?err, package = %name.as_str(), %filename, "tarball cache tmp-open failed; streaming without cache");
-            let body = Body::from_stream(response.bytes_stream());
-            return tarball_response(body, upstream_len);
+        Err(err) => return error_response(&err),
+    };
+
+    if upstream.caches() {
+        if let Err(err) = streaming::download_verified_to_cache(response, write, &integrity).await {
+            return error_response(&tarball_stream_error(err, &name, &filename));
}
+
+        match state.inner.storage.open_cached_tarball(&name, &filename).await {
+            Ok(Some((file, len))) => match streaming::verify_file(file, &integrity).await {
+                Ok(file) => tarball_response(streaming::stream_file(file), Some(len)),
+                Err(err) => {
+                    discard_cached_tarball(state, &name, &filename).await;
+                    error_response(&tarball_stream_error(err, &name, &filename))
+                }
+            },
+            Ok(None) => error_response(&tarball_integrity_error(
+                &name,
+                &filename,
+                "verified cache entry disappeared before it could be served".to_string(),
+            )),
+            Err(err) => error_response(&err),
+        }
Evidence
The handler always loads/parses the packument to obtain expected SRI, then verifies cache hits by
reading the entire file before streaming it, and after caching it repeats verification by reading
the cached file again before streaming.

pnpr/crates/pnpr/src/server.rs[756-855]
pnpr/crates/pnpr/src/streaming.rs[57-75]
pnpr/crates/pnpr/src/streaming.rs[105-128]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Tarball serving now does multiple full-file reads per request:
- Cache hit: hash entire file (`verify_file`) then stream entire file (`stream_file`).
- Cache miss: hash while downloading, then re-open + hash again, then stream.
This is deterministic extra I/O on the tarball hot path and can materially reduce throughput / increase latency.
### Issue Context
The security fix (fail-closed SRI verification bound to `versions[version].dist.integrity`) is correct, but it should not require re-hashing the same bytes on every GET.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[756-855]
- pnpr/crates/pnpr/src/streaming.rs[57-128]
### Suggested remediation
1) Eliminate the *redundant* post-download re-verify step:
- After `download_verified_to_cache(...)` succeeds, open the cached file and stream it without calling `verify_file` again (the bytes were already verified before `finalize`).
2) Add a fast-path marker for cache hits to avoid full re-hash on every request while still failing closed when unverified:
- When a tarball is verified+finalized, write a small sidecar (e.g. `foo-1.0.0.tgz.integrity` containing the exact SRI string + length).
- On cache hit, read sidecar and compare to expected SRI from packument; if it matches, skip `verify_file` and stream directly.
- If sidecar is missing/mismatched, perform `verify_file`, then write/update the sidecar.
This keeps the security invariant (don’t serve bytes unless they’re known-good) but avoids re-hashing on every request.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Blocking Drop cleanup 🐞 Bug ➹ Performance ⭐ New
Description
TarballWrite (and similarly RemoveOnDropFile) deletes temp files via synchronous
std::fs::remove_file inside Drop, which can block Tokio worker threads when requests are
cancelled or response bodies are dropped. This can add avoidable latency spikes under load,
especially when many temp files are being cleaned up on slower/contended filesystems.
Code

pnpr/crates/pnpr/src/storage.rs[R135-145]

+impl Drop for TarballWrite {
+    fn drop(&mut self) {
+        drop(self.file.take());
+        let Some(tmp_path) = self.tmp_path.take() else { return };
+        match std::fs::remove_file(&tmp_path) {
+            Ok(()) => {}
+            Err(err) if err.kind() == ErrorKind::NotFound => {}
+            Err(err) => {
+                tracing::warn!(?err, path = %tmp_path.display(), "tarball cache temp cleanup failed");
+            }
+        }
Evidence
The PR adds a Drop implementation for TarballWrite that unlinks the temp file using the blocking
stdlib API, which can run on Tokio worker threads when the request task is cancelled or a streaming
body is dropped. The same pattern exists for RemoveOnDropFile used by stream_file_and_remove, so
the behavior is not isolated to one codepath.

pnpr/crates/pnpr/src/storage.rs[135-145]
pnpr/crates/pnpr/src/streaming.rs[198-209]
pnpr/crates/pnpr/src/server.rs[898-905]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`TarballWrite::drop` performs a synchronous filesystem unlink (`std::fs::remove_file`). Since `TarballWrite` is used on the async tarball request path, drops can occur on Tokio runtime threads (e.g., cancellation/abort paths), causing avoidable blocking.

## Issue Context
This PR intentionally relies on `Drop` to remove temporary tarball files on cancellation/error paths. That’s correct functionally, but the current implementation uses blocking std I/O.

## Fix Focus Areas
- pnpr/crates/pnpr/src/storage.rs[135-145]

### Suggested approach
- Replace the synchronous `std::fs::remove_file` in `Drop` with a best-effort async cleanup:
 - If a Tokio runtime is available (`tokio::runtime::Handle::try_current()`), spawn an async task that calls `tokio::fs::remove_file(path).await` (or `spawn_blocking` to call `std::fs::remove_file`).
 - If no runtime is available (e.g., drop during shutdown or in some tests), fall back to `std::fs::remove_file`.
- (Optional but consistent) apply the same helper pattern to other temp-file `Drop` cleanups in the PR (e.g., `RemoveOnDropFile` in `streaming.rs`) so temp cleanup is uniformly non-blocking on the runtime.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Cache discard race 🐞 Bug ☼ Reliability
Description
When a cached tarball fails verification, serve_tarball() deletes the cache entry by path, which can
race with a concurrent request that has already replaced that same path with a newly verified
tarball via rename. This can delete the good cache entry, causing intermittent failures and cache
thrash under concurrency.
Code

pnpr/crates/pnpr/src/server.rs[R839-843]

+                    Err(err) => {
+                        let err = tarball_stream_error(err, &name, &filename);
+                        tracing::warn!(?err, package = %name.as_str(), %filename, "cached tarball failed verification");
+                        discard_cached_tarball(state, &name, &filename).await;
+                    }
Evidence
The tarball verification failure path unconditionally removes the cache entry by path, while cache
writes are promoted to that same path via atomic rename, enabling a TOCTOU race under concurrent
requests.

pnpr/crates/pnpr/src/server.rs[821-855]
pnpr/crates/pnpr/src/storage.rs[75-103]
pnpr/crates/pnpr/src/storage.rs[441-445]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serve_tarball()` deletes a cached tarball by pathname after verification failure. Because cache writers promote temp files with `rename()` into the same final path, the final path may be replaced by another concurrent request between the failed verification and deletion, causing the newly-written verified tarball to be removed.
### Issue Context
- Verification is performed on an already-open `File` handle, but deletion is performed later by path.
- Cache population uses temp-file + `rename()` promotion, so the final path can change atomically under concurrent requests.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[821-907]
- pnpr/crates/pnpr/src/storage.rs[549-648]
### Suggested fix
1. Extend `Storage::open_cached_tarball()` (or a new helper) to return additional identity info for the opened file, e.g. `mtime` (and optionally platform-specific file ID/inode if available).
2. On verification failure, before deleting by path, re-stat the current cache path and only delete if it still matches the identity of the file that was verified (e.g. same `mtime` + `len`). If it no longer matches, skip deletion (another request likely replaced it).
3. Alternatively, implement a `remove_cached_tarball_if_unchanged(name, filename, expected_mtime, expected_len)` in `Storage`/`Store` and call that from `serve_tarball()` on verify failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Integrity failure returns 404 🐞 Bug ◔ Observability
Description
If a cached tarball fails integrity verification and there is no upstream configured,
serve_tarball() discards the cache entry and then returns 404 Not Found. This masks an integrity
failure as a missing resource, reducing debuggability and potentially hiding corruption in
offline/orphaned-cache deployments.
Code

pnpr/crates/pnpr/src/server.rs[R839-855]

+                    Err(err) => {
+                        let err = tarball_stream_error(err, &name, &filename);
+                        tracing::warn!(?err, package = %name.as_str(), %filename, "cached tarball failed verification");
+                        discard_cached_tarball(state, &name, &filename).await;
+                    }
+                }
+            }
+            Ok(None) => {}
+            Err(err) => {
+                tracing::warn!(?err, package = %name.as_str(), %filename, "tarball cache open failed");
+            }
+        }
+    }
+
+    let Some(upstream) = upstream else {
       return not_found();
   };
Evidence
The code explicitly discards the cached tarball on verification error but then falls through to a
not_found() return when no upstream exists, producing a 404 despite an integrity failure having
occurred.

pnpr/crates/pnpr/src/server.rs[819-855]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
On cached tarball verification failure, the handler logs + discards the cache entry but does not return an error response. If `resolve_upstream()` returned `None`, the function later hits `return not_found()`, turning an integrity failure into a 404.
### Issue Context
This happens specifically when there is no upstream for the package (e.g. orphaned cache / proxy config removed) and the cache entry exists but fails verification.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[819-855]
### Suggested fix
In the `Err(err)` branch of the cached-tarball verification:
- If `upstream.is_none()`, immediately return `error_response(&tarball_stream_error(err, &name, &filename))` (or return a `TarballIntegrity` error with an explicit message).
- Only fall through to upstream fetch when an upstream exists.
This preserves current behavior when upstream is available (refetch/self-heal), while surfacing a correct failure mode when offline.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (9)
9. Predictable tmp paths on RNG fail 🐞 Bug ⛨ Security
Description
storage::unique_tmp_path() falls back to a constant random suffix (0) when getrandom fails, making
temp filenames predictable in that scenario. In an attacker-writable cache directory, this can be
exploited by pre-creating the next candidate temp paths so create_tmp_file_with() exhausts its retry
budget and tarball/cache writes fail.
Code

pnpr/crates/pnpr/src/storage.rs[R770-776]

+    let mut random = [0u8; 8];
+    let random = match getrandom::fill(&mut random) {
+        Ok(()) => u64::from_ne_bytes(random),
+        Err(_) => 0,
+    };
  let mut name = base.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
-    name.push(format!(".tmp.{pid}.{counter}"));
+    name.push(format!(".tmp.{pid}.{counter}.{random:016x}"));
Evidence
The new temp-path scheme is explicitly relying on a randomized suffix for collision resistance, but
the current implementation drops that entropy to a fixed value on RNG failure. Temp creation only
retries a fixed number of times on collisions, so predictability makes forced-collision failures
feasible when directories are attacker-controlled (a stated review assumption).

pnpr/crates/pnpr/src/storage.rs[742-762]
pnpr/crates/pnpr/src/storage.rs[767-780]
REVIEW_GUIDE.md[30-58]
REVIEW_GUIDE.md[86-88]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`unique_tmp_path()` currently treats `getrandom::fill` failure as non-fatal and substitutes a constant `0` random suffix. This removes the unpredictability that the randomized suffix was added to provide, and under attacker-writable directories can allow deliberate collision/pre-creation that forces `create_tmp_file_with()` to fail after `MAX_TEMP_CREATE_ATTEMPTS` retries.
### Issue Context
- Temp files are created with `create_new(true)` and retried only a bounded number of times; predictability increases the chance that an attacker can force repeated `AlreadyExists`.
- REVIEW_GUIDE.md explicitly calls out attacker-controlled filesystem paths and collision-resistant identifiers as security concerns.
### Fix Focus Areas
- pnpr/crates/pnpr/src/storage.rs[742-762]
- pnpr/crates/pnpr/src/storage.rs[767-780]
### Suggested fix
- Do not silently fall back to a constant value when `getrandom` fails.
- Preferred: propagate the RNG failure and fail the operation (return an error) so you never operate with predictable temp names.
- Alternative (if you must remain best-effort): mix in an additional non-constant, per-attempt value (e.g., high-resolution time + extra counter) and emit a warning log when randomness is unavailable. Ensure the fallback cannot be a fixed constant across attempts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Unbounded sidecar file read 🐞 Bug ☼ Reliability
Description
Store::read_tarball_integrity() loads the per-tarball .integrity sidecar using fs::read() with
no size cap, so a large/corrupted sidecar in an attacker-writable (or otherwise corrupted) cache
directory can force large allocations and repeated I/O on tarball requests. This is a
local/operator-environment DoS/resource amplification risk tied to the new cache marker feature.
Code

pnpr/crates/pnpr/src/storage.rs[R558-566]

+    async fn read_tarball_integrity(
+        &self,
+        name: &PackageName,
+        filename: &str,
+    ) -> Option<CachedTarballIntegrity> {
+        match fs::read(self.tarball_integrity_path(name, filename)).await {
+            Ok(bytes) => serde_json::from_slice(&bytes).ok(),
+            Err(_) => None,
+        }
Evidence
The new cache marker is read via an unbounded fs::read() and is used by serve_tarball() to
decide whether to skip verification, so an oversized sidecar directly impacts request-time
memory/I/O behavior.

pnpr/crates/pnpr/src/storage.rs[558-567]
pnpr/crates/pnpr/src/server.rs[819-828]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`read_tarball_integrity()` uses `tokio::fs::read()` to read the entire `.integrity` sidecar into memory before JSON parsing. If that sidecar file is unexpectedly large (e.g., corrupted disk, local attacker with write access to cache dir), this can cause large allocations and repeated expensive reads on the tarball hot path.
### Issue Context
- The `.integrity` sidecar is a small JSON struct (`{ integrity: String, len: u64 }`).
- The handler consults this sidecar on cache-hit paths to skip re-hashing, so the read happens in normal operation.
### Fix Focus Areas
- pnpr/crates/pnpr/src/storage.rs[558-567]
### Suggested approach
- Check `fs::metadata(...).len()` first and reject/ignore files above a small limit (e.g., 4–16 KiB).
- Optionally log a warning when the sidecar is oversized/malformed to aid diagnosis.
- Alternatively, stream-read with an explicit maximum (`take(limit)`) and treat over-limit as invalid.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Oversize tarball returns 502 🐞 Bug ☼ Reliability
Description
TarballStreamError::TooLarge is converted into RegistryError::TarballIntegrity, which is always
surfaced as HTTP 502; this misclassifies a local policy limit as an upstream failure and encourages
retry behavior while also suppressing the useful EINTEGRITY message in the response body.
Code

pnpr/crates/pnpr/src/server.rs[R947-951]

+        streaming::TarballStreamError::TooLarge { limit, received } => tarball_integrity_error(
+            name,
+            filename,
+            format!("tarball body exceeds {limit} byte limit (received {received} bytes)"),
+        ),
Evidence
The oversize condition is explicitly mapped to TarballIntegrity in tarball_stream_error.
TarballIntegrity is then mapped to 502 Bad Gateway, and public_message() collapses 5xx
responses to the canonical reason, which hides the EINTEGRITY details and aligns with the documented
retryable semantics for 5xx gateway errors.

pnpr/crates/pnpr/src/server.rs[934-952]
pnpr/crates/pnpr/src/error.rs[239-275]
pnpr/crates/pnpr/src/server.rs[2402-2414]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Oversized tarball bodies (exceeding `MAX_TARBALL_BYTES`) currently map to `RegistryError::TarballIntegrity` and therefore to HTTP `502 Bad Gateway`. Per the project's own error semantics, 5xx are treated as retryable gateway failures and `public_message()` intentionally collapses server errors to the canonical reason, hiding the actionable `EINTEGRITY` detail.
## Issue Context
This is a *local* enforcement (spool limit), not an upstream transport/status failure. It should be classified as a client-visible, non-retryable error (e.g. `413 Payload Too Large`) so clients don't keep retrying an irrecoverable condition and operators can distinguish it from actual upstream faults.
## Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[934-953]
- pnpr/crates/pnpr/src/error.rs[238-304]
### Concrete direction
- Introduce a dedicated error variant (e.g. `RegistryError::TarballTooLarge { package, filename, limit, received }`) and map it to `StatusCode::PAYLOAD_TOO_LARGE`.
- In `tarball_stream_error`, map `TarballStreamError::TooLarge` to that new variant.
- Update/add tests asserting oversized tarballs return `413` (and optionally that the message includes the limit details since it will now be a 4xx).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Temp tarball fsync overhead 🐞 Bug ➹ Performance
Description
TarballWrite::into_temp_file() calls sync_all() even though it is used by the new cache:false
tarball path where the temp file is immediately streamed and removed, adding a full disk flush to
the hot path for mirror-less tarball GETs.
Code

pnpr/crates/pnpr/src/storage.rs[R102-107]

+    pub async fn into_temp_file(mut self) -> std::io::Result<(fs::File, u64, PathBuf)> {
+        let Some(file) = self.file.take() else {
+            return Err(std::io::Error::other("tarball cache writer is closed"));
+        };
+        file.sync_all().await?;
+        let len = file.metadata().await?.len();
Evidence
The temp-file conversion path explicitly fsyncs (sync_all) and is invoked by
download_verified_to_temp, which is used in serve_tarball for cache:false uplinks (the
mirror-less path that streams and then deletes the temp file).

pnpr/crates/pnpr/src/storage.rs[102-116]
pnpr/crates/pnpr/src/streaming.rs[98-109]
pnpr/crates/pnpr/src/server.rs[892-900]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `cache:false` tarball flow now spools the full response to a temp file and then streams it while removing the file afterward. In that path, `TarballWrite::into_temp_file()` unconditionally calls `file.sync_all()`, which forces an fsync despite the file being disposable. This can significantly increase latency and reduce throughput under load.
## Issue Context
Durability (`sync_all` before rename) is useful for cached artifacts you want to survive crashes, but it provides little value for a temp file that will be deleted after the response completes.
## Fix Focus Areas
- pnpr/crates/pnpr/src/storage.rs[102-116]
- pnpr/crates/pnpr/src/streaming.rs[98-109]
- pnpr/crates/pnpr/src/server.rs[892-900]
### Concrete direction
- Remove `sync_all()` from `TarballWrite::into_temp_file()` (or replace it with a lighter-weight operation if needed, e.g. just close the writer before reopening).
- Consider opening the temp file as read+write initially (via `OpenOptions`) so you can `seek(0)` and stream from the same handle, avoiding the close/reopen entirely.
- Keep `sync_all()` in `TarballWrite::finalize()` for the cache-promotion path if durability is desired there.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Sidecar bypasses verification 🐞 Bug ⛨ Security
Description
In serve_tarball(), a cached tarball is served without hashing when the .integrity sidecar matches
the expected integrity+len tuple, so the tarball bytes themselves are no longer verified on that
path. Because filesystem/cache contents are treated as attacker-controlled in this repo, a
same-length cache tamper (or other on-disk modification) can be served without re-validation.
Code

pnpr/crates/pnpr/src/server.rs[R803-812]

+                let expected = cached_tarball_integrity(&integrity, len);
+                if state
+                    .inner
+                    .storage
+                    .read_cached_tarball_integrity(&name, &filename)
+                    .await
+                    .is_some_and(|cached| cached == expected)
+                {
+                    return tarball_response(streaming::stream_file(file), Some(len));
+                }
Evidence
The cache-hit fast path returns a response before hashing when the cached sidecar equals the
expected tuple; the sidecar is read from disk as JSON and does not prove the tarball bytes still
match SRI, while the repo’s security guidance treats filesystem paths as attacker-controlled.

pnpr/crates/pnpr/src/server.rs[798-817]
pnpr/crates/pnpr/src/storage.rs[558-566]
REVIEW_GUIDE.md[32-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serve_tarball()` trusts the cached tarball integrity sidecar as a sufficient proof of tarball validity and skips `streaming::verify_file()` entirely when it matches. This allows serving cached bytes without re-validation if the tarball file was modified on disk but the sidecar still matches.
### Issue Context
The repo’s review guide explicitly treats filesystem paths/cache as attacker-controlled input, so the decision to skip verification should be conditioned on a property that changes when the file changes.
### Fix
Keep the fast path, but extend the sidecar to include file metadata that changes on modification (at minimum `mtime`, ideally also `inode` where available), and only skip hashing when `(integrity, len, mtime, ...)` matches the current file metadata. If metadata mismatches, run `verify_file()`, then rewrite the sidecar with the new metadata.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[798-823]
- pnpr/crates/pnpr/src/storage.rs[158-167]
- pnpr/crates/pnpr/src/storage.rs[558-577]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Cache ignores tarball size cap 🐞 Bug ➹ Performance
Description
MAX_TARBALL_BYTES is enforced when downloading upstream tarballs, but cached tarball hits are
hashed/served regardless of their on-disk size. A pre-existing oversized cached entry can therefore
force very high disk I/O and CPU hashing per request and bypass the new per-request spooling bound.
Code

pnpr/crates/pnpr/src/server.rs[R801-817]

+        match state.inner.storage.open_cached_tarball(&name, &filename).await {
+            Ok(Some((file, len))) => {
+                let expected = cached_tarball_integrity(&integrity, len);
+                if state
+                    .inner
+                    .storage
+                    .read_cached_tarball_integrity(&name, &filename)
+                    .await
+                    .is_some_and(|cached| cached == expected)
+                {
+                    return tarball_response(streaming::stream_file(file), Some(len));
+                }
+                match streaming::verify_file(file, &integrity).await {
+                    Ok(file) => {
+                        record_cached_tarball_integrity(state, &name, &filename, expected).await;
+                        return tarball_response(streaming::stream_file(file), Some(len));
+                    }
Evidence
The code defines a 100MiB tarball cap and applies it during upstream download, but the cache-hit
branch neither checks len against the cap nor rejects oversized cached files before
hashing/serving.

pnpr/crates/pnpr/src/server.rs[49-53]
pnpr/crates/pnpr/src/server.rs[800-824]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `MAX_TARBALL_BYTES` limit is applied to upstream downloads, but the cache-hit path will still read/hash/serve any cached tarball size.
### Issue Context
`open_cached_tarball()` returns `(file, len)` and `MAX_TARBALL_BYTES` is already defined in this module, so the handler has all information needed to bound the cache-hit path too.
### Fix
Before the sidecar check and before `verify_file()`, add a `len > MAX_TARBALL_BYTES` guard:
- treat it as an integrity failure (fail closed),
- delete the cached tarball + sidecar (best-effort),
- return a controlled error (e.g., `RegistryError::TarballIntegrity` with a “cached tarball exceeds limit” reason).
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[49-53]
- pnpr/crates/pnpr/src/server.rs[800-824]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Sidecar delete blocks removal 🐞 Bug ☼ Reliability
Description
Store::remove_tarball() returns early if deleting the .integrity sidecar fails, preventing
deletion of the tarball itself. This can break cache self-healing when serve_tarball() tries to
discard a failed-verification cache entry and can also block unpublish flows from removing tarballs.
Code

pnpr/crates/pnpr/src/storage.rs[R632-637]

+        let integrity_path = self.tarball_integrity_path(name, filename);
+        match fs::remove_file(&integrity_path).await {
+            Ok(()) => {}
+            Err(err) if err.kind() == ErrorKind::NotFound => {}
+            Err(err) => return Err(err.into()),
+        }
Evidence
The implementation returns an error immediately on sidecar deletion failure, and serve_tarball’s
verification-failure path depends on remove_cached_tarball() for self-healing; this early return can
leave the bad tarball in place.

pnpr/crates/pnpr/src/storage.rs[630-642]
pnpr/crates/pnpr/src/server.rs[818-822]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`remove_tarball()` aborts tarball deletion if `.integrity` sidecar removal fails with an error other than NotFound.
### Issue Context
The sidecar is auxiliary metadata; failing to remove it should not prevent removing the primary tarball bytes, especially when trying to discard a known-bad cached tarball.
### Fix
Attempt tarball removal regardless of sidecar removal outcome:
- if sidecar delete fails, log a warning but continue,
- return the tarball removal result,
- optionally attempt sidecar cleanup after tarball deletion as well.
### Fix Focus Areas
- pnpr/crates/pnpr/src/storage.rs[630-642]
- pnpr/crates/pnpr/src/server.rs[818-822]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. Tarball GET parses packument 🐞 Bug ➹ Performance
Description
serve_tarball unconditionally loads and fully parses the packument to extract dist.integrity
before it even attempts to open a cached tarball. This adds avoidable disk+CPU work to every tarball
GET (including cache hits) and makes cached tarball service depend on packument availability.
Code

pnpr/crates/pnpr/src/server.rs[R782-799]

+    let packument = match load_packument_bytes(state, &name).await {
+        PackumentLoad::Ok(bytes) => bytes,
+        PackumentLoad::NotFound => return not_found(),
+        PackumentLoad::Err(err) => return error_response(&err),
+    };
+    let integrity = match expected_tarball_integrity(&packument, &name, &filename, &version) {
+        Ok(Some(integrity)) => integrity,
+        Ok(None) => return not_found(),
+        Err(err) => return error_response(&err),
+    };
+
+    let upstream = resolve_upstream(state, &name);
+    let should_read_cache = upstream.as_ref().is_none_or(|upstream| upstream.caches());
+    if should_read_cache {
+        match state.inner.storage.open_cached_tarball(&name, &filename).await {
+            Ok(Some((file, len))) => match streaming::verify_file(file, &integrity).await {
+                Ok(file) => return tarball_response(streaming::stream_file(file), Some(len)),
+                Err(err) => {
Evidence
The tarball handler loads packument bytes and calls expected_tarball_integrity() (which does a
full serde_json::from_slice) before attempting to open/verify a cached tarball. This guarantees
the extra work on every tarball GET, including cache hits.

pnpr/crates/pnpr/src/server.rs[756-810]
pnpr/crates/pnpr/src/server.rs[857-885]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Tarball requests now always call `load_packument_bytes()` and `expected_tarball_integrity()` (full JSON parse) before checking `open_cached_tarball()`. This means cache hits pay packument read+parse cost, and tarball service becomes coupled to packument cache presence.
### Issue Context
The integrity binding is the correct security direction, but per REVIEW_GUIDE.md (§1 + §2) security gates on hot paths should have a fast path. Here, the fast path can avoid packument parsing for repeated requests once the integrity for a given `(package, version)` is known.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[756-810]
- pnpr/crates/pnpr/src/server.rs[857-885]
### What to change (options)
- Option A (disk sidecar, strong fast path):
- When caching a tarball, persist the expected SRI alongside it (e.g. `foo-1.0.0.tgz.integrity`).
- On cache hit, read the small sidecar to get expected SRI and verify the tarball without loading/parsing the packument.
- Keep packument parsing for cache misses (needed to decide expected SRI before fetching upstream).
- Option B (in-memory cache, lower complexity):
- Maintain an LRU map of `(package, version) -> Integrity` keyed/invalidated by packument cache validators (ETag/Last-Modified) or by cached packument mtime.
- On cache hit, use cached integrity when valid; fall back to parsing packument when missing/invalid.
Either approach keeps the security property (fail closed when integrity is unknown) while avoiding repeated full packument parses on the hot tarball path.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Unsafe version used in URL 🐞 Bug ⛨ Security
Description
rewrite_tarball_urls() constructs proxied tarball URLs from attacker-controlled versions map keys
via tarball_name_for_version(), but tarball_name_for_version() does not validate the version as a
safe path segment. A malicious upstream packument can therefore cause the registry to emit
broken/invalid tarball URLs (e.g., containing /), which can confuse clients and produce
non-routable links.
Code

pnpr/crates/pnpr/src/upstream.rs[R370-386]

+        for (version, manifest) in versions {
+            rewrite_dist_tarball(manifest, pkg, version, public_url);
}
}
-    rewrite_dist_tarball(value, pkg, public_url);
}
-fn rewrite_dist_tarball(value: &mut Value, pkg: &PackageName, public_url: &str) {
+fn rewrite_dist_tarball(value: &mut Value, pkg: &PackageName, version: &str, public_url: &str) {
let Some(dist) = value.get_mut("dist").and_then(Value::as_object_mut) else {
return;
};
let Some(tarball_value) = dist.get_mut(...

Comment thread pnpr/crates/pnpr/src/server.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pnpr/crates/pnpr/src/publish.rs (1)

118-123: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Minor redundancy: integrity_checker re-validates after parse_integrity.

Both parse_integrity (line 118) and integrity_checker (line 122) call ensure_supported_hash. Since parse_integrity already validated the integrity, the second check in integrity_checker will always pass. The duplicate validation is harmless (cheap check, defense-in-depth), but if the streaming API stabilizes, consider whether integrity_checker should accept pre-validated Integrity without re-checking.

Not blocking—the shared helper usage is correct per pnpr/AGENTS.md guidance on reusing streaming module functionality.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpr/crates/pnpr/src/publish.rs` around lines 118 - 123, Both parse_integrity
and integrity_checker are performing redundant validation of the hash algorithm
through the shared ensure_supported_hash check. Refactor integrity_checker to
accept the already-validated Integrity object directly without re-validating,
eliminating the duplicate ensure_supported_hash call. This way, parse_integrity
performs the initial validation (lines 118-119), and integrity_checker (line
122) uses the pre-validated Integrity result without repeating the hash type
validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pnpr/crates/pnpr/src/publish.rs`:
- Around line 118-123: Both parse_integrity and integrity_checker are performing
redundant validation of the hash algorithm through the shared
ensure_supported_hash check. Refactor integrity_checker to accept the
already-validated Integrity object directly without re-validating, eliminating
the duplicate ensure_supported_hash call. This way, parse_integrity performs the
initial validation (lines 118-119), and integrity_checker (line 122) uses the
pre-validated Integrity result without repeating the hash type validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 24544a2d-8663-482d-8f87-8fb4718424d1

📥 Commits

Reviewing files that changed from the base of the PR and between e72b482 and cd73fcf.

📒 Files selected for processing (13)
  • pnpr/crates/pnpr/src/error.rs
  • pnpr/crates/pnpr/src/package_name.rs
  • pnpr/crates/pnpr/src/package_name/tests.rs
  • pnpr/crates/pnpr/src/publish.rs
  • pnpr/crates/pnpr/src/publish/tests.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/storage.rs
  • pnpr/crates/pnpr/src/storage/tests.rs
  • pnpr/crates/pnpr/src/streaming.rs
  • pnpr/crates/pnpr/src/streaming/tests.rs
  • pnpr/crates/pnpr/src/upstream.rs
  • pnpr/crates/pnpr/src/upstream/tests.rs
  • pnpr/crates/pnpr/tests/server.rs

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jun 22, 2026
@codecov-commenter

codecov-commenter commented Jun 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.10462% with 53 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.98%. Comparing base (bae694f) to head (16d9850).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/storage.rs 83.09% 24 Missing ⚠️
pnpr/crates/pnpr/src/server.rs 84.05% 22 Missing ⚠️
pnpr/crates/pnpr/src/streaming.rs 93.85% 7 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12570      +/-   ##
==========================================
- Coverage   87.99%   87.98%   -0.02%     
==========================================
  Files         324      324              
  Lines       45693    46008     +315     
==========================================
+ Hits        40209    40480     +271     
- Misses       5484     5528      +44     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread pnpr/crates/pnpr/src/streaming.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit cd73fcf

@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.680 ± 0.200 4.425 5.070 1.53 ± 0.09
pacquet@main 4.627 ± 0.143 4.461 4.845 1.51 ± 0.08
pnpr@HEAD 3.146 ± 0.201 2.892 3.443 1.03 ± 0.08
pnpr@main 3.065 ± 0.127 2.887 3.277 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.6795592060199995,
      "stddev": 0.20038660622343635,
      "median": 4.6458518802199995,
      "user": 3.63550204,
      "system": 3.4735350999999994,
      "min": 4.425094048719999,
      "max": 5.06992986772,
      "times": [
        4.66243183472,
        4.425094048719999,
        4.944185773719999,
        5.06992986772,
        4.55825443272,
        4.75375199372,
        4.6292719257199995,
        4.69707970272,
        4.478747290719999,
        4.576845189719999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.626651276719999,
      "stddev": 0.14316109136157926,
      "median": 4.60318461422,
      "user": 3.63449254,
      "system": 3.4876158999999993,
      "min": 4.46063669472,
      "max": 4.845244065719999,
      "times": [
        4.55130758172,
        4.463902001719999,
        4.80607319772,
        4.751160500719999,
        4.845244065719999,
        4.46063669472,
        4.70378520372,
        4.61078208972,
        4.478034292719999,
        4.595587138719999
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 3.1461569349199996,
      "stddev": 0.20104316405072464,
      "median": 3.1739929582200004,
      "user": 2.68422094,
      "system": 3.0671215,
      "min": 2.8921237957200003,
      "max": 3.44317086272,
      "times": [
        3.36264293072,
        2.92204585372,
        2.8921237957200003,
        3.25964375772,
        3.30434462172,
        3.10815750272,
        3.44317086272,
        2.96121821472,
        2.96839339572,
        3.23982841372
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 3.06507530252,
      "stddev": 0.12670776995138572,
      "median": 3.05324554172,
      "user": 2.71942654,
      "system": 3.0517996,
      "min": 2.88669317972,
      "max": 3.2765189607200003,
      "times": [
        3.02086259572,
        2.91864664072,
        3.08562848772,
        3.2765189607200003,
        3.18537888772,
        2.88669317972,
        2.97925614672,
        3.18325460972,
        2.99314712372,
        3.12136639272
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 633.3 ± 17.1 611.8 666.7 1.01 ± 0.04
pacquet@main 624.1 ± 14.7 604.7 642.7 1.00
pnpr@HEAD 693.1 ± 49.7 658.7 832.0 1.11 ± 0.08
pnpr@main 682.8 ± 17.0 668.7 719.8 1.09 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6333493801800001,
      "stddev": 0.01712864225501911,
      "median": 0.6308284912800001,
      "user": 0.37451154000000003,
      "system": 1.3199324,
      "min": 0.6118490052800001,
      "max": 0.6667185222800001,
      "times": [
        0.6270075582800001,
        0.62479264028,
        0.62402765128,
        0.65394929828,
        0.63614091928,
        0.63464942428,
        0.6667185222800001,
        0.6404917842800001,
        0.61386699828,
        0.6118490052800001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.62411543858,
      "stddev": 0.014714645930533362,
      "median": 0.6235290152800002,
      "user": 0.35974494,
      "system": 1.3258869999999998,
      "min": 0.60466224428,
      "max": 0.6427448652800001,
      "times": [
        0.6117433102800001,
        0.60612321128,
        0.6427448652800001,
        0.60466224428,
        0.63676916128,
        0.6284241422800001,
        0.63989562328,
        0.63793141628,
        0.61422652328,
        0.6186338882800001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6930918032800001,
      "stddev": 0.049685159417846916,
      "median": 0.6795963037800001,
      "user": 0.38207114,
      "system": 1.3535344,
      "min": 0.65865655728,
      "max": 0.8319873962800001,
      "times": [
        0.6903573232800001,
        0.6792239212800001,
        0.6881666502800001,
        0.68058552528,
        0.6799686862800001,
        0.65865655728,
        0.6768022702800001,
        0.6662339152800001,
        0.8319873962800001,
        0.67893578728
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.68282543498,
      "stddev": 0.016981374733951678,
      "median": 0.67485439378,
      "user": 0.37912513999999997,
      "system": 1.3593102,
      "min": 0.6687333332800001,
      "max": 0.7197906662800001,
      "times": [
        0.6719508082800001,
        0.66991190828,
        0.6873417832800001,
        0.67062320328,
        0.68651602428,
        0.7197906662800001,
        0.70367783528,
        0.6753347862800001,
        0.67437400128,
        0.6687333332800001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.702 ± 0.044 4.625 4.770 1.57 ± 0.08
pacquet@main 4.748 ± 0.040 4.687 4.836 1.59 ± 0.08
pnpr@HEAD 2.987 ± 0.141 2.786 3.179 1.00
pnpr@main 3.039 ± 0.171 2.796 3.299 1.02 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.7022333851199996,
      "stddev": 0.04402117614907084,
      "median": 4.718608317719999,
      "user": 3.7299306999999997,
      "system": 3.3928167599999997,
      "min": 4.62543423872,
      "max": 4.76963329372,
      "times": [
        4.7218091187199995,
        4.69335550872,
        4.7275178447199995,
        4.73208924372,
        4.71565447372,
        4.64220900872,
        4.62543423872,
        4.67306895872,
        4.76963329372,
        4.72156216172
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.748066675719999,
      "stddev": 0.03991975848494852,
      "median": 4.74719347822,
      "user": 3.7549552,
      "system": 3.42825166,
      "min": 4.68712225972,
      "max": 4.83570453572,
      "times": [
        4.70463606472,
        4.68712225972,
        4.83570453572,
        4.76362525772,
        4.76493072272,
        4.74581618272,
        4.73047275272,
        4.74857077372,
        4.73956356072,
        4.76022464672
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.9866253619199994,
      "stddev": 0.14096968759975662,
      "median": 2.9908365762199995,
      "user": 2.5214790999999996,
      "system": 2.9733222599999998,
      "min": 2.7856358317199996,
      "max": 3.17893236872,
      "times": [
        2.82836920072,
        3.00611035272,
        3.1299696607199996,
        2.7856358317199996,
        3.17893236872,
        2.9501955727199998,
        3.02864660772,
        2.9755627997199996,
        3.1530322047199997,
        2.82979901972
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 3.0386174904199996,
      "stddev": 0.17088047876001208,
      "median": 3.05617549672,
      "user": 2.528645,
      "system": 2.9614471599999996,
      "min": 2.79566394072,
      "max": 3.29901603072,
      "times": [
        3.0700303657199997,
        3.0964564067199998,
        3.29901603072,
        3.0423206277199997,
        3.17201162872,
        2.79566394072,
        2.83705752772,
        3.01515750972,
        2.83886949472,
        3.21959137172
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.328 ± 0.013 1.305 1.347 2.03 ± 0.04
pacquet@main 1.318 ± 0.026 1.293 1.386 2.01 ± 0.05
pnpr@HEAD 0.656 ± 0.036 0.634 0.755 1.00 ± 0.06
pnpr@main 0.655 ± 0.009 0.639 0.675 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.32829801322,
      "stddev": 0.013412258003630738,
      "median": 1.3286762203200002,
      "user": 1.32113478,
      "system": 1.7189157599999998,
      "min": 1.3048131518200001,
      "max": 1.3468538378200001,
      "times": [
        1.32126527882,
        1.3248985768200001,
        1.3048131518200001,
        1.33829975582,
        1.3468538378200001,
        1.31937737382,
        1.3373931618200001,
        1.3149924698200002,
        1.33245386382,
        1.34263266182
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.31841244672,
      "stddev": 0.025957077302788862,
      "median": 1.3164601133200002,
      "user": 1.29241808,
      "system": 1.7180592599999998,
      "min": 1.29297896682,
      "max": 1.38562057982,
      "times": [
        1.32247035782,
        1.30047171582,
        1.38562057982,
        1.29297896682,
        1.32066810882,
        1.3137695898200001,
        1.31243572682,
        1.2961647438200001,
        1.3203940408200001,
        1.31915063682
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6556501811200002,
      "stddev": 0.03646384359794694,
      "median": 0.6426393008200001,
      "user": 0.33694368,
      "system": 1.2673569599999999,
      "min": 0.63362321182,
      "max": 0.7545972498200001,
      "times": [
        0.63522718582,
        0.63527517582,
        0.6443781928200001,
        0.6472002088200001,
        0.63362321182,
        0.7545972498200001,
        0.6380582988200001,
        0.6653532528200001,
        0.6409004088200001,
        0.6618886258200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.65454320412,
      "stddev": 0.009322232553246,
      "median": 0.65372205782,
      "user": 0.3490266799999999,
      "system": 1.27489616,
      "min": 0.6391529918200001,
      "max": 0.6747791288200001,
      "times": [
        0.6581635998200001,
        0.6391529918200001,
        0.6571308108200001,
        0.6480278928200001,
        0.6524505958200001,
        0.6549935198200001,
        0.6485423908200001,
        0.6598453218200001,
        0.6747791288200001,
        0.65234578882
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.995 ± 0.062 2.917 3.103 4.58 ± 0.11
pacquet@main 2.964 ± 0.011 2.945 2.985 4.54 ± 0.06
pnpr@HEAD 0.653 ± 0.009 0.635 0.666 1.00
pnpr@main 0.662 ± 0.049 0.639 0.802 1.01 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.9948598004,
      "stddev": 0.06224688629895745,
      "median": 2.9982920924,
      "user": 1.7605566799999999,
      "system": 1.9524906400000002,
      "min": 2.9171887579,
      "max": 3.1025843619,
      "times": [
        2.9870747249,
        2.9250697149,
        2.9395866428999997,
        2.9494404379,
        2.9171887579,
        3.0095094599,
        3.0142966509,
        3.1025843619,
        3.0582653008999996,
        3.0455819518999996
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.9644247318,
      "stddev": 0.010868956089893144,
      "median": 2.9669181793999995,
      "user": 1.72465108,
      "system": 1.9520985400000002,
      "min": 2.9449760489,
      "max": 2.9846274549,
      "times": [
        2.9449760489,
        2.9678517048999997,
        2.9846274549,
        2.9573335549,
        2.9659846538999997,
        2.9543938629,
        2.9682012318999997,
        2.9598028439,
        2.9713621529,
        2.9697138089
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.6534543752,
      "stddev": 0.008504248932055198,
      "median": 0.6544355524000001,
      "user": 0.33510158,
      "system": 1.2873143400000002,
      "min": 0.6352037009,
      "max": 0.6663926819,
      "times": [
        0.6663926819,
        0.6569638379,
        0.6563039339000001,
        0.6493861109000001,
        0.6352037009,
        0.6540182659,
        0.6623830239,
        0.6500034959000001,
        0.6548528389,
        0.6490358619000001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6623714879,
      "stddev": 0.04948526330751643,
      "median": 0.6473817779000001,
      "user": 0.33870718,
      "system": 1.2787692400000001,
      "min": 0.6389349879,
      "max": 0.8018539199000001,
      "times": [
        0.6412989779,
        0.6389349879,
        0.6394614409,
        0.6490664459000001,
        0.6413096039,
        0.6522470989000001,
        0.6585096709,
        0.6553356229,
        0.6456971099000001,
        0.8018539199000001
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12570
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,702.23 ms
(+11.57%)Baseline: 4,214.56 ms
5,057.48 ms
(92.98%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,994.86 ms
(-0.31%)Baseline: 3,004.19 ms
3,605.03 ms
(83.07%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,328.30 ms
(+0.24%)Baseline: 1,325.07 ms
1,590.08 ms
(83.54%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,679.56 ms
(+18.50%)Baseline: 3,948.99 ms
4,738.79 ms
(98.75%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
633.35 ms
(+1.77%)Baseline: 622.31 ms
746.77 ms
(84.81%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12570
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,986.63 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
653.45 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
655.65 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
3,146.16 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
693.09 ms
🐰 View full continuous benchmarking report in Bencher

Comment thread pnpr/crates/pnpr/src/server.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ae4de9b

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ae4de9b

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c2b106f

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pnpr/crates/pnpr/src/package_name/tests.rs (1)

7-9: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Assert parsed tuple values, not only parse success.

These calls only verify that parsing doesn’t error; they don’t prove the extracted version/canonical filename contract.

Suggested test tightening
 fn accepts_unscoped() {
     let name = PackageName::parse("lodash").unwrap();
     assert_eq!(name.as_str(), "lodash");
     assert_eq!(name.tarball_name_for_version("4.17.21"), "lodash-4.17.21.tgz");
-    name.parse_tarball_name("lodash-4.17.21.tgz").unwrap();
+    assert_eq!(
+        name.parse_tarball_name("lodash-4.17.21.tgz").unwrap(),
+        ("lodash-4.17.21.tgz".to_string(), "4.17.21".to_string()),
+    );
 }
 
 #[test]
 fn accepts_scoped() {
     let name = PackageName::parse("`@types/node`").unwrap();
     assert_eq!(name.as_str(), "`@types/node`");
     assert_eq!(name.tarball_name_for_version("20.0.0"), "node-20.0.0.tgz");
-    name.parse_tarball_name("node-20.0.0.tgz").unwrap();
+    assert_eq!(
+        name.parse_tarball_name("node-20.0.0.tgz").unwrap(),
+        ("node-20.0.0.tgz".to_string(), "20.0.0".to_string()),
+    );
 }

As per coding guidelines, “ensure tests prove the regression/changed behavior (not just execution).”

Also applies to: 13-16

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpr/crates/pnpr/src/package_name/tests.rs` around lines 7 - 9, The test
currently only verifies that parse_tarball_name succeeds by calling unwrap
without asserting the actual parsed values. Capture the result of
parse_tarball_name("lodash-4.17.21.tgz").unwrap() into a variable and add
assertions to verify the returned tuple contains the expected extracted values
(version and canonical name). Apply the same fix to all similar
parse_tarball_name calls in the test to ensure the parsed contract is properly
validated, not just that parsing completes without error.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pnpr/crates/pnpr/src/package_name/tests.rs`:
- Around line 7-9: The test currently only verifies that parse_tarball_name
succeeds by calling unwrap without asserting the actual parsed values. Capture
the result of parse_tarball_name("lodash-4.17.21.tgz").unwrap() into a variable
and add assertions to verify the returned tuple contains the expected extracted
values (version and canonical name). Apply the same fix to all similar
parse_tarball_name calls in the test to ensure the parsed contract is properly
validated, not just that parsing completes without error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 15523dde-f37c-4b86-aba5-29d80ef4c3dd

📥 Commits

Reviewing files that changed from the base of the PR and between c2b106f and 5d98c4f.

📒 Files selected for processing (13)
  • pnpr/crates/pnpr/src/error.rs
  • pnpr/crates/pnpr/src/package_name.rs
  • pnpr/crates/pnpr/src/package_name/tests.rs
  • pnpr/crates/pnpr/src/publish.rs
  • pnpr/crates/pnpr/src/publish/tests.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/storage.rs
  • pnpr/crates/pnpr/src/storage/tests.rs
  • pnpr/crates/pnpr/src/streaming.rs
  • pnpr/crates/pnpr/src/streaming/tests.rs
  • pnpr/crates/pnpr/src/upstream.rs
  • pnpr/crates/pnpr/src/upstream/tests.rs
  • pnpr/crates/pnpr/tests/server.rs
✅ Files skipped from review due to trivial changes (1)
  • pnpr/crates/pnpr/src/upstream/tests.rs
🚧 Files skipped from review as they are similar to previous changes (8)
  • pnpr/crates/pnpr/src/publish/tests.rs
  • pnpr/crates/pnpr/src/error.rs
  • pnpr/crates/pnpr/src/streaming/tests.rs
  • pnpr/crates/pnpr/src/publish.rs
  • pnpr/crates/pnpr/src/upstream.rs
  • pnpr/crates/pnpr/src/streaming.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/tests/server.rs

Comment thread pnpr/crates/pnpr/src/storage.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3299fb3

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Comment thread pnpr/crates/pnpr/src/server.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5d98c4f

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4b04173

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 22, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Bind proxied tarball requests to the selected packument version.

Require supported dist.integrity before serving upstream or cached tarballs.

Verify cache hits before response construction.

Fail closed on a hosted-store fault instead of falling through to the
upstream proxy, so an I/O error in the authoritative store can never serve
bytes of a different provenance for the same package name.

Delete invalid cache entries and promote upstream bytes only after SRI verification.

For cache:false uplinks, verify into a temp file and stream the same open
handle (rewound to the start) instead of dropping and reopening it by path,
closing the TOCTOU window where an attacker-writable cache directory could
swap the verified bytes before they are served; remove the temp file after
streaming.

Harden publish attachment SRI parsing for missing or unsupported integrity.

Addresses GHSA-5f9g-98vq-2jxw.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 16d9850

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 16d9850

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Rust CI / Test / windows

Failed stage: Test pnpr [❌]

Failed test name: pnpr::server hosted_tarball_open_failure_fails_closed

Failure summary:

The action failed during the just test-pnpr step when a Rust nextest run had a failing test.
-
Failing test: pnpr::server hosted_tarball_open_failure_fails_closed
- Failure reason: the test
panicked in mockito because it expected 0 HTTP requests to GET /foo, but 1 request was
received, violating the mock expectation.
- Panic location: ...mockito-1.7.2\src\mock.rs:633:13
-
Because of this failure, nextest stopped early (Cancelling due to test failure) and the job exited
with code 100 (recipe test-pnpr failed on line 62), causing the GitHub Action to fail.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

340:  Received 1069547520 of 1312429306 (81.5%), 126.9 MBs/sec
341:  Received 1207959552 of 1312429306 (92.0%), 126.8 MBs/sec
342:  Received 1312429306 of 1312429306 (100.0%), 127.1 MBs/sec
343:  Cache Size: ~1252 MB (1312429306 B)
344:  [command]"C:\Program Files\Git\usr\bin\tar.exe" -xf D:/a/_temp/157aa57b-6f88-4ce0-947b-fa0b411cd4c6/cache.tzst -P -C D:/a/pnpm/pnpm --force-local --use-compress-program "zstd -d"
345:  Cache restored successfully
346:  Restored from cache key "v0-rust-warm-Windows_NT-x64-be4ef524-ecf0eb8d" full match: true.
347:  ##[group]Run pnpm/setup@b1cac37306e39c21283b9dd6cb0ac288fb35ba6b
348:  with:
349:  install: false
350:  dest: ~/setup-pnpm
351:  cache: false
352:  cache-dependency-path: pnpm-lock.yaml
353:  package-json-file: package.json
354:  env:
355:  CACHE_ON_FAILURE: false
356:  CARGO_INCREMENTAL: 0
...

379:  Installation Completed!
380:  ##[group]Installing runtime node@26.3.1...
381:  Progress: resolved 1, reused 0, downloaded 0, added 0
382:  Packages: +1
383:  +
384:  Progress: resolved 1, reused 0, downloaded 1, added 0
385:  Progress: resolved 1, reused 0, downloaded 1, added 1, done
386:  global:
387:  + node 26.3.1
388:  Done in 4.9s using pnpm v11.8.0
389:  ##[endgroup]
390:  ##[group]Run pnpm runtime set node 22 --global
391:  �[36;1mpnpm runtime set node 22 --global�[0m
392:  shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"
393:  env:
394:  CACHE_ON_FAILURE: false
395:  CARGO_INCREMENTAL: 0
...

403:  [WARN] The target bin directory already contains an exe called node, so removing C:\Users\runneradmin\setup-pnpm\node_modules\@pnpm\exe\bin\node.EXE
404:  global:
405:  + node 22.23.0
406:  Done in 4.3s using pnpm v11.8.0
407:  ##[group]Run actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
408:  with:
409:  key: ci-pnpm-v11-windows-latest
410:  path: C:\Users\runneradmin\setup-pnpm\node_modules\@pnpm\exe/store/v11
411:  /.local/share/pnpm/store/v11
412:  
413:  enableCrossOsArchive: false
414:  fail-on-cache-miss: false
415:  lookup-only: false
416:  save-always: false
417:  env:
418:  CACHE_ON_FAILURE: false
419:  CARGO_INCREMENTAL: 0
420:  PNPM_HOME: C:\Users\runneradmin\setup-pnpm\node_modules\@pnpm\exe
421:  ##[endgroup]
422:  Cache hit for: ci-pnpm-v11-windows-latest
423:  Received 79691776 of 177239123 (45.0%), 75.4 MBs/sec
424:  Received 177239123 of 177239123 (100.0%), 101.3 MBs/sec
425:  Cache Size: ~169 MB (177239123 B)
426:  [command]"C:\Program Files\Git\usr\bin\tar.exe" -xf D:/a/_temp/358ac39c-7fe2-4117-9efc-aa95106eeb95/cache.tzst -P -C D:/a/pnpm/pnpm --force-local --use-compress-program "zstd -d"
427:  Cache restored successfully
428:  Cache restored from key: ci-pnpm-v11-windows-latest
429:  ##[group]Run taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154
430:  with:
431:  tool: just
432:  checksum: true
433:  fallback: cargo-binstall
434:  env:
435:  CACHE_ON_FAILURE: false
436:  CARGO_INCREMENTAL: 0
437:  PNPM_HOME: C:\Users\runneradmin\setup-pnpm\node_modules\@pnpm\exe
438:  ##[endgroup]
439:  ##[group]Run Set-StrictMode -Version Latest
440:  �[36;1mSet-StrictMode -Version Latest�[0m
441:  �[36;1m$remove_env = @('ENV','BASH_ENV','CDPATH','SHELLOPTS','BASHOPTS','BASH_FUNC_*')�[0m
442:  �[36;1mforeach ($name in $remove_env) {�[0m
443:  �[36;1m  if (Test-Path "Env:$name") { Remove-Item "Env:\$name" }�[0m
444:  �[36;1m}�[0m
445:  �[36;1mfor ($i=1; $i -le 10; $i++) {�[0m
446:  �[36;1m  $prev_err_action = $ErrorActionPreference�[0m
447:  �[36;1m  $ErrorActionPreference = "Continue"�[0m
448:  �[36;1m  & bash --noprofile --norc "$env:GITHUB_ACTION_PATH\main.sh"�[0m
449:  �[36;1m  $code = $LASTEXITCODE�[0m
450:  �[36;1m  $ErrorActionPreference = "$prev_err_action"�[0m
451:  �[36;1m  if (Test-Path "$env:USERPROFILE\.install-action\init") {�[0m
452:  �[36;1m    # If bash started successfully, main.sh creates init file.�[0m
453:  �[36;1m    Remove-Item "$env:USERPROFILE\.install-action\init" -Force�[0m
454:  �[36;1m    exit $code�[0m
455:  �[36;1m  }�[0m
456:  �[36;1m  if ($i -lt 10) {�[0m
457:  �[36;1m    Write-Output "::warning::install-action: installation failed due to bash startup failure (<https://github.com/actions/partner-runner-images/issues/169>); retrying..."�[0m
458:  �[36;1m  }�[0m
459:  �[36;1m}�[0m
460:  �[36;1mWrite-Output "::error::install-action: installation failed due to bash startup failure (<https://github.com/actions/partner-runner-images/issues/169>); this maybe resolved by re-running job"�[0m
461:  �[36;1mexit 1�[0m
462:  shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"
463:  env:
464:  CACHE_ON_FAILURE: false
465:  CARGO_INCREMENTAL: 0
...

472:  RUNNER_OS: Windows
473:  RUNNER_ARCH: X64
474:  ##[endgroup]
475:  info: host platform: x86_64_windows
476:  info: cargo is located at /c/Users/runneradmin/.cargo/bin/cargo
477:  info: installing just@latest
478:  info: downloading https://github.com/casey/just/releases/download/1.51.0/just-1.51.0-x86_64-pc-windows-msvc.zip
479:  info: verifying sha256 checksum for just-1.51.0-x86_64-pc-windows-msvc.zip
480:  info: just installed at /c/Users/runneradmin/.cargo/bin/just.exe
481:  + just --version
482:  just 1.51.0
483:  ##[group]Run just install
484:  �[36;1mjust install�[0m
485:  shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"
486:  env:
487:  CACHE_ON_FAILURE: false
488:  CARGO_INCREMENTAL: 0
...

518:  Progress: resolved 1646, reused 0, downloaded 1647, added 1048
519:  Progress: resolved 1646, reused 0, downloaded 1647, added 1303
520:  Progress: resolved 1646, reused 0, downloaded 1647, added 1526
521:  Progress: resolved 1646, reused 0, downloaded 1647, added 1630
522:  ✓ Lockfile passes supply-chain policies (1709 entries in 17.9s)
523:  Progress: resolved 1646, reused 0, downloaded 1647, added 1646
524:  Progress: resolved 1646, reused 0, downloaded 1647, added 1646, done
525:  .../node_modules/fuse-native install$ node-gyp-build
526:  .../node_modules/ghooks install$ node ./bin/module-install
527:  .../node_modules/esbuild postinstall$ node install.js
528:  .../node_modules/unrs-resolver postinstall$ node postinstall.js
529:  .../node_modules/fuse-native install: node:fs:2749
530:  .../node_modules/fuse-native install:     const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */);
531:  .../node_modules/fuse-native install:                         ^
532:  .../node_modules/fuse-native install: 
533:  .../node_modules/fuse-native install: Error: EISDIR: illegal operation on a directory, lstat 'D:'
534:  .../node_modules/fuse-native install:     at Object.realpathSync (node:fs:2749:25)
...

537:  .../node_modules/fuse-native install:     at resolveMainPath (node:internal/modules/run_main:39:23)
538:  .../node_modules/fuse-native install:     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:163:20)
539:  .../node_modules/fuse-native install:     at node:internal/main/run_main_module:36:49 {
540:  .../node_modules/fuse-native install:   errno: -4068,
541:  .../node_modules/fuse-native install:   code: 'EISDIR',
542:  .../node_modules/fuse-native install:   syscall: 'lstat',
543:  .../node_modules/fuse-native install:   path: 'D:'
544:  .../node_modules/fuse-native install: }
545:  .../node_modules/fuse-native install: 
546:  .../node_modules/fuse-native install: Node.js v22.23.0
547:  .../node_modules/ghooks install: This does not seem to be a git project.
548:  .../node_modules/ghooks install: Although ghooks was installed, the actual git hooks have not.
549:  .../node_modules/ghooks install: Run "git init" and then "npm explore ghooks -- npm run install".
550:  .../node_modules/ghooks install: 
551:  .../node_modules/ghooks install: Please ignore this message if you are not using ghooks directly.
552:  .../node_modules/fuse-native install: Failed
553:  .../node_modules/msgpackr-extract install$ node-gyp-build-optional-packages
554:  .../node_modules/ghooks install: Done
555:  .../node_modules/msgpackr-extract install: node:fs:2749
556:  .../node_modules/msgpackr-extract install:     const out = binding.lstat(base, false, undefined, true /* throwIfNoEntry */);
557:  .../node_modules/msgpackr-extract install:                         ^
558:  .../node_modules/msgpackr-extract install: 
559:  .../node_modules/msgpackr-extract install: Error: EISDIR: illegal operation on a directory, lstat 'D:'
560:  .../node_modules/msgpackr-extract install:     at Object.realpathSync (node:fs:2749:25)
561:  .../node_modules/msgpackr-extract install:     at toRealPath (node:internal/modules/helpers:61:13)
562:  .../node_modules/msgpackr-extract install:     at Function._findPath (node:internal/modules/cjs/loader:760:22)
563:  .../node_modules/msgpackr-extract install:     at resolveMainPath (node:internal/modules/run_main:39:23)
564:  .../node_modules/msgpackr-extract install:     at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:163:20)
565:  .../node_modules/msgpackr-extract install:     at node:internal/main/run_main_module:36:49 {
566:  .../node_modules/msgpackr-extract install:   errno: -4068,
567:  .../node_modules/msgpackr-extract install:   code: 'EISDIR',
568:  .../node_modules/msgpackr-extract install:   syscall: 'lstat',
569:  .../node_modules/msgpackr-extract install:   path: 'D:'
570:  .../node_modules/msgpackr-extract install: }
571:  .../node_modules/msgpackr-extract install: 
572:  .../node_modules/msgpackr-extract install: Node.js v22.23.0
573:  .../node_modules/msgpackr-extract install: Failed
574:  .../node_modules/esbuild postinstall: Done
575:  .../node_modules/unrs-resolver postinstall: [napi-postinstall@0.3.4] Failed to find package "@unrs/resolver-binding-win32-x64-msvc" on the file system
576:  .../node_modules/unrs-resolver postinstall: 
577:  .../node_modules/unrs-resolver postinstall: This can happen if you use the "--no-optional" flag. The "optionalDependencies"
578:  .../node_modules/unrs-resolver postinstall: package.json feature is used by unrs-resolver to install the correct napi binary
579:  .../node_modules/unrs-resolver postinstall: for your current platform. This install script will now attempt to work around
580:  .../node_modules/unrs-resolver postinstall: this. If that fails, you need to remove the "--no-optional" flag to use unrs-resolver.
581:  .../node_modules/unrs-resolver postinstall: 
582:  .../node_modules/unrs-resolver postinstall: [napi-postinstall@0.3.4] Trying to install package "@unrs/resolver-binding-win32-x64-msvc" using npm
583:  .../node_modules/unrs-resolver postinstall: [napi-postinstall@0.3.4] Failed to install package "@unrs/resolver-binding-win32-x64-msvc" using npm Cannot find module 'unrs-resolver/package.json'
584:  .../node_modules/unrs-resolver postinstall: Require stack:
...

615:  + husky 9.1.7
616:  + jest 30.4.2
617:  + keyv 5.6.0
618:  + lcov-result-merger 6.0.0
619:  + node 26.3.1
620:  + rimraf 6.1.3
621:  + shx 0.4.0
622:  + typescript 6.0.3
623:  Done in 21.2s using pnpm v11.8.0
624:  ##[group]Run taiki-e/install-action@0631aa6515c7d545823c67cfae7ef4fc7f490154
625:  with:
626:  tool: cargo-nextest
627:  checksum: true
628:  fallback: cargo-binstall
629:  env:
630:  CACHE_ON_FAILURE: false
631:  CARGO_INCREMENTAL: 0
632:  PNPM_HOME: C:\Users\runneradmin\setup-pnpm\node_modules\@pnpm\exe
633:  ##[endgroup]
634:  ##[group]Run Set-StrictMode -Version Latest
635:  �[36;1mSet-StrictMode -Version Latest�[0m
636:  �[36;1m$remove_env = @('ENV','BASH_ENV','CDPATH','SHELLOPTS','BASHOPTS','BASH_FUNC_*')�[0m
637:  �[36;1mforeach ($name in $remove_env) {�[0m
638:  �[36;1m  if (Test-Path "Env:$name") { Remove-Item "Env:\$name" }�[0m
639:  �[36;1m}�[0m
640:  �[36;1mfor ($i=1; $i -le 10; $i++) {�[0m
641:  �[36;1m  $prev_err_action = $ErrorActionPreference�[0m
642:  �[36;1m  $ErrorActionPreference = "Continue"�[0m
643:  �[36;1m  & bash --noprofile --norc "$env:GITHUB_ACTION_PATH\main.sh"�[0m
644:  �[36;1m  $code = $LASTEXITCODE�[0m
645:  �[36;1m  $ErrorActionPreference = "$prev_err_action"�[0m
646:  �[36;1m  if (Test-Path "$env:USERPROFILE\.install-action\init") {�[0m
647:  �[36;1m    # If bash started successfully, main.sh creates init file.�[0m
648:  �[36;1m    Remove-Item "$env:USERPROFILE\.install-action\init" -Force�[0m
649:  �[36;1m    exit $code�[0m
650:  �[36;1m  }�[0m
651:  �[36;1m  if ($i -lt 10) {�[0m
652:  �[36;1m    Write-Output "::warning::install-action: installation failed due to bash startup failure (<https://github.com/actions/partner-runner-images/issues/169>); retrying..."�[0m
653:  �[36;1m  }�[0m
654:  �[36;1m}�[0m
655:  �[36;1mWrite-Output "::error::install-action: installation failed due to bash startup failure (<https://github.com/actions/partner-runner-images/issues/169>); this maybe resolved by re-running job"�[0m
656:  �[36;1mexit 1�[0m
657:  shell: C:\Program Files\PowerShell\7\pwsh.EXE -command ". '{0}'"
658:  env:
659:  CACHE_ON_FAILURE: false
660:  CARGO_INCREMENTAL: 0
...

679:  commit-hash: 75ddba7e911b44c5c0700dac0415d824403de9bd
680:  commit-date: 2026-05-26
681:  host: x86_64-pc-windows-msvc
682:  ##[group]Run # removing env vars is a temporary workaround for unit tests in pacquet relying on external environment
683:  �[36;1m# removing env vars is a temporary workaround for unit tests in pacquet relying on external environment�[0m
684:  �[36;1m# this should be removed in the future�[0m
685:  �[36;1munset PNPM_HOME�[0m
686:  �[36;1munset XDG_DATA_HOME�[0m
687:  �[36;1m�[0m
688:  �[36;1mnode .github/scripts/measure-command.mjs \�[0m
689:  �[36;1m  --name tests.all \�[0m
690:  �[36;1m  --output .bench/pacquet-tests-all.json \�[0m
691:  �[36;1m  -- just test-pacquet�[0m
692:  shell: C:\Program Files\Git\bin\bash.EXE --noprofile --norc -e -o pipefail {0}
693:  env:
694:  CACHE_ON_FAILURE: false
695:  CARGO_INCREMENTAL: 0
...

783:  3 | use tempfile::TempDir;
784:  |     ^^^^^^^^^^^^^^^^^
785:  warning: unused import: `super::StopArgs`
786:  --> pacquet\crates\cli\src\cli_args\stop\tests.rs:1:5
787:  |
788:  1 | use super::StopArgs;
789:  |     ^^^^^^^^^^^^^^^
790:  warning: unused import: `tempfile::TempDir`
791:  --> pacquet\crates\cli\src\cli_args\stop\tests.rs:3:5
792:  |
793:  3 | use tempfile::TempDir;
794:  |     ^^^^^^^^^^^^^^^^^
795:  warning: unused import: `import_into_fresh_target`
796:  --> pacquet\crates\package-manager\src\link_file\tests.rs:5:5
797:  |
798:  5 |     import_into_fresh_target, is_call_error, is_cross_device, link_file,
799:  |     ^^^^^^^^^^^^^^^^^^^^^^^^
...

814:  warning: `pacquet-cli` (lib test) generated 6 warnings (run `cargo fix --lib -p pacquet-cli --tests` to apply 4 suggestions)
815:  warning: `pacquet-package-manager` (lib test) generated 1 warning (run `cargo fix --lib -p pacquet-package-manager --tests` to apply 1 suggestion)
816:  Finished `test` profile [unoptimized + debuginfo] target(s) in 6m 24s
817:  ────────────
818:  Nextest run ID 5d1487ba-b21b-478d-899c-65b350ea83e9 with nextest profile: default
819:  Starting 3165 tests across 118 binaries (12 tests skipped)
820:  PASS [   0.015s] (   1/3165) pacquet-catalogs-config tests::combines_implicit_default_and_named_catalogs
821:  PASS [   0.015s] (   2/3165) pacquet-catalogs-config tests::throws_if_default_catalog_is_defined_multiple_times
822:  PASS [   0.016s] (   3/3165) pacquet-catalogs-config tests::returns_empty_map_for_missing_workspace_manifest
823:  PASS [   0.016s] (   4/3165) pacquet-catalogs-config tests::combines_explicit_default_and_named_catalogs
824:  PASS [   0.013s] (   5/3165) pacquet-catalogs-protocol-parser tests::parses_implicit_default_catalog
825:  PASS [   0.014s] (   6/3165) pacquet-catalogs-protocol-parser tests::returns_none_for_specifier_not_using_catalog_protocol
826:  PASS [   0.016s] (   7/3165) pacquet-catalogs-protocol-parser tests::parses_explicit_default_catalog
827:  PASS [   0.018s] (   8/3165) pacquet-catalogs-protocol-parser tests::parses_named_catalog
828:  PASS [   0.014s] (   9/3165) pacquet-catalogs-resolver tests::default_catalog_resolves_using_explicit_name
829:  PASS [   0.012s] (  10/3165) pacquet-catalogs-resolver tests::returns_error_for_file_protocol_in_catalog
830:  PASS [   0.014s] (  11/3165) pacquet-catalogs-resolver tests::resolves_named_catalog
831:  PASS [   0.017s] (  12/3165) pacquet-catalogs-resolver tests::default_catalog_resolves_using_implicit_name
832:  PASS [   0.013s] (  13/3165) pacquet-catalogs-resolver tests::returns_error_for_link_protocol_in_catalog
833:  PASS [   0.013s] (  14/3165) pacquet-catalogs-resolver tests::returns_error_for_missing_unresolved_catalog
834:  PASS [   0.014s] (  15/3165) pacquet-catalogs-resolver tests::returns_error_for_workspace_protocol_in_catalog
835:  PASS [   0.016s] (  16/3165) pacquet-catalogs-resolver tests::returns_error_for_recursive_catalog
836:  PASS [   0.018s] (  17/3165) pacquet-catalogs-resolver tests::returns_unused_for_specifier_not_using_catalog_protocol
...

846:  PASS [   0.023s] (  27/3165) pacquet-cli cli_args::create::tests::scoped_underscore_prefix_gets_double_prefix
847:  PASS [   0.017s] (  28/3165) pacquet-cli cli_args::create::tests::unscoped_already_prefixed_is_unchanged
848:  PASS [   0.022s] (  29/3165) pacquet-cli cli_args::create::tests::scoped_with_version
849:  PASS [   0.023s] (  30/3165) pacquet-cli cli_args::create::tests::scoped_unprefixed_gets_create_prefix
850:  PASS [   0.021s] (  31/3165) pacquet-cli cli_args::create::tests::unscoped_empty_prefix_is_unchanged
851:  PASS [   0.020s] (  32/3165) pacquet-cli cli_args::create::tests::unscoped_underscore_prefix_gets_double_prefix
852:  PASS [   0.020s] (  33/3165) pacquet-cli cli_args::create::tests::unscoped_unprefixed_gets_create_prefix
853:  PASS [   0.023s] (  34/3165) pacquet-cli cli_args::create::tests::unscoped_with_version
854:  PASS [   0.030s] (  35/3165) pacquet-cli cli_args::dlx::tests::architecture_flags_accumulate_and_default_empty
855:  PASS [   0.018s] (  36/3165) pacquet-cli cli_args::dlx::tests::create_cache_key_allow_build_is_order_independent
856:  PASS [   0.025s] (  37/3165) pacquet-cli cli_args::dlx::tests::architecture_flags_do_not_consume_the_trailing_command
857:  PASS [   0.020s] (  38/3165) pacquet-cli cli_args::dlx::tests::create_cache_key_changes_with_allow_build
858:  PASS [   0.022s] (  39/3165) pacquet-cli cli_args::dlx::tests::create_cache_key_changes_with_supported_architectures
859:  PASS [   0.022s] (  40/3165) pacquet-cli cli_args::dlx::tests::create_cache_key_depends_on_registry
860:  PASS [   0.017s] (  41/3165) pacquet-cli cli_args::dlx::tests::create_cache_key_is_order_independent_and_deterministic
861:  PASS [   0.026s] (  42/3165) pacquet-cli cli_args::dlx::tests::get_bin_name_errors_on_ambiguous_bins
862:  PASS [   0.029s] (  43/3165) pacquet-cli cli_args::dlx::tests::get_bin_name_errors_when_no_dependency
863:  PASS [   0.026s] (  44/3165) pacquet-cli cli_args::dlx::tests::get_bin_name_returns_single_bin
...

886:  PASS [   0.026s] (  67/3165) pacquet-cli cli_args::install::tests::resolve_workspace_concurrency_positive_flag_overrides_config
887:  PASS [   0.020s] (  68/3165) pacquet-cli cli_args::install::tests::workspace_concurrency_default_is_none
888:  PASS [   0.022s] (  69/3165) pacquet-cli cli_args::install::tests::workspace_concurrency_parses_positive
889:  PASS [   0.026s] (  70/3165) pacquet-cli cli_args::install::tests::workspace_concurrency_parses_negative
890:  PASS [   0.022s] (  71/3165) pacquet-cli cli_args::outdated::tests::classify_detects_each_bump_kind
891:  PASS [   0.023s] (  72/3165) pacquet-cli cli_args::outdated::tests::default_sort_orders_by_change_then_name
892:  PASS [   0.015s] (  73/3165) pacquet-cli cli_args::outdated::tests::include_no_optional_drops_optional
893:  PASS [   0.018s] (  74/3165) pacquet-cli cli_args::outdated::tests::include_dev_keeps_only_dev
894:  PASS [   0.024s] (  75/3165) pacquet-cli cli_args::outdated::tests::include_default_covers_all_three_groups
895:  PASS [   0.020s] (  76/3165) pacquet-cli cli_args::outdated::tests::include_prod_keeps_dependencies_and_optional
896:  PASS [   0.021s] (  77/3165) pacquet-cli cli_args::outdated::tests::json_report_has_expected_shape
897:  PASS [   0.021s] (  78/3165) pacquet-cli cli_args::outdated::tests::json_report_long_includes_latest_manifest
898:  PASS [   0.024s] (  79/3165) pacquet-cli cli_args::outdated::tests::render_latest_outdated_and_deprecated
899:  PASS [   0.023s] (  80/3165) pacquet-cli cli_args::outdated::tests::render_latest_outdated_and_not_deprecated
900:  PASS [   0.022s] (  81/3165) pacquet-cli cli_args::remove::tests::dependency_options_to_save_type
901:  PASS [   0.024s] (  82/3165) pacquet-cli cli_args::run::tests::hidden_filter_all_hidden_yields_all_hidden_error
902:  PASS [   0.027s] (  83/3165) pacquet-cli cli_args::run::tests::hidden_filter_passes_visible_scripts
...

946:  PASS [   1.166s] ( 127/3165) pacquet-cli::add add_explicit_range_ignores_pin_from_non_registry_prev
947:  PASS [   1.245s] ( 128/3165) pacquet-cli::add add_explicit_range_respects_existing_operator
948:  PASS [   1.462s] ( 129/3165) pacquet-cli::add add_explicit_tilde_range_is_not_widened_to_latest
949:  PASS [   2.505s] ( 130/3165) pacquet-cli::add add_npm_alias_spec_is_kept_verbatim
950:  PASS [   4.872s] ( 131/3165) pacquet-cli::add add_registry_tarball_url_is_kept_verbatim
951:  PASS [   5.092s] ( 132/3165) pacquet-cli::add save_exact_writes_exact_version
952:  PASS [   7.560s] ( 133/3165) pacquet-cli::add add_prerelease_resolved_version_keeps_no_prefix
953:  PASS [   6.835s] ( 134/3165) pacquet-cli::add save_exact_overrides_save_prefix
954:  PASS [   2.445s] ( 135/3165) pacquet-cli::add save_prefix_arbitrary_value_falls_back_to_caret
955:  PASS [   2.988s] ( 136/3165) pacquet-cli::add save_prefix_defaults_to_caret
956:  PASS [   2.972s] ( 137/3165) pacquet-cli::add save_prefix_tilde_writes_tilde_range
957:  PASS [   2.947s] ( 138/3165) pacquet-cli::add should_add_dev_dependency
958:  PASS [   4.195s] ( 139/3165) pacquet-cli::add save_prefix_empty_writes_exact_version
959:  PASS [   0.178s] ( 140/3165) pacquet-cli::cat_file cat_file_works
960:  PASS [   0.098s] ( 141/3165) pacquet-cli::cat_file cat_file_works_with_binary
961:  PASS [   0.144s] ( 142/3165) pacquet-cli::cat_file should_fail_on_invalid_base64
962:  PASS [   0.117s] ( 143/3165) pacquet-cli::cat_file should_fail_on_invalid_hash_format
963:  PASS [   0.208s] ( 144/3165) pacquet-cli::cat_file should_fail_on_missing_hash
964:  PASS [   2.195s] ( 145/3165) pacquet-cli::add should_add_to_package_json
965:  PASS [   0.283s] ( 146/3165) pacquet-cli::cat_file should_prevent_path_traversal
966:  PASS [   2.299s] ( 147/3165) pacquet-cli::add should_add_peer_dependency
967:  PASS [   1.218s] ( 148/3165) pacquet-cli::cat_index should_cat_index_of_npm_alias
968:  PASS [   0.211s] ( 149/3165) pacquet-cli::cat_index should_fail_on_missing_package
969:  PASS [   1.637s] ( 150/3165) pacquet-cli::cat_index should_cat_index_of_installed_package
970:  PASS [   3.813s] ( 151/3165) pacquet-cli::add should_install_all_dependencies
971:  PASS [   1.655s] ( 152/3165) pacquet-cli::cat_index should_cat_index_with_dir_pointing_to_workspace_project
972:  PASS [   0.451s] ( 153/3165) pacquet-cli::catalog add_mismatched_version_strict_errors
973:  PASS [   1.057s] ( 154/3165) pacquet-cli::catalog add_prefer_catalogs_a_new_dependency
974:  PASS [   1.126s] ( 155/3165) pacquet-cli::catalog add_strict_catalogs_a_new_dependency
975:  PASS [   0.954s] ( 156/3165) pacquet-cli::catalog readd_catalog_dependency_preserves_specifier
976:  PASS [   1.340s] ( 157/3165) pacquet-cli::catalog install_reruns_when_catalog_entry_changes
977:  PASS [   0.628s] ( 158/3165) pacquet-cli::config_dependencies add_config_writes_workspace_yaml_and_installs
978:  PASS [   1.104s] ( 159/3165) pacquet-cli::catalog update_latest_named_catalog_bumps_the_entry
979:  PASS [   1.118s] ( 160/3165) pacquet-cli::catalog update_latest_no_save_leaves_the_catalog_untouched
980:  PASS [   1.435s] ( 161/3165) pacquet-cli::catalog update_latest_keeps_catalog_referencing_override_in_sync
981:  PASS [   0.484s] ( 162/3165) pacquet-cli::config_dependencies installs_configurational_dependencies
982:  PASS [   0.044s] ( 163/3165) pacquet-cli::create create_errors_when_no_name_given
983:  PASS [   0.446s] ( 164/3165) pacquet-cli::config_dependencies second_install_keeps_config_dependency
984:  PASS [   1.453s] ( 165/3165) pacquet-cli::config_dependencies update_config_hook_injects_catalog
985:  PASS [   1.412s] ( 166/3165) pacquet-cli::config_dependencies update_config_hook_mutates_config_before_install
986:  PASS [   1.753s] ( 167/3165) pacquet-cli::custom_resolvers custom_resolver_takes_precedence_over_builtin_resolvers
987:  PASS [   2.094s] ( 168/3165) pacquet-cli::custom_resolvers custom_resolver_receives_current_pkg_on_subsequent_installs
988:  PASS [   1.124s] ( 169/3165) pacquet-cli::dedupe_direct_deps dedupe_off_by_default_keeps_shared_workspace_link
989:  PASS [   2.643s] ( 170/3165) pacquet-cli::custom_resolvers failing_should_refresh_resolution_aborts_the_install
990:  PASS [   2.620s] ( 171/3165) pacquet-cli::dedupe_direct_deps dedupe_direct_deps_disabled_keeps_per_project_symlinks
991:  PASS [   3.438s] ( 172/3165) pacquet-cli::custom_resolvers should_refresh_resolution_forces_re_resolution_past_the_frozen_path
992:  PASS [   2.372s] ( 173/3165) pacquet-cli::dedupe_direct_deps dedupe_under_shamefully_hoist
993:  PASS [   1.852s] ( 174/3165) pacquet-cli::dedupe_direct_deps dedupes_direct_dep_against_publicly_hoisted_root_dep
994:  PASS [   1.382s] ( 175/3165) pacquet-cli::dedupe_direct_deps dedupes_direct_deps_against_workspace_root
995:  PASS [   1.316s] ( 176/3165) pacquet-cli::dedupe_direct_deps dedupes_direct_deps_with_frozen_lockfile
996:  PASS [   0.841s] ( 177/3165) pacquet-cli::dedupe_direct_deps dedupes_link_deps_resolving_to_the_same_dir_via_different_segments
997:  PASS [   1.017s] ( 178/3165) pacquet-cli::dedupe_injected_deps injected_leaf_workspace_dep_is_deduped_to_link
998:  PASS [   1.148s] ( 179/3165) pacquet-cli::dedupe_direct_deps dedupes_only_overlapping_direct_deps
999:  PASS [   1.143s] ( 180/3165) pacquet-cli::dedupe_injected_deps injected_peer_suffixed_workspace_dep_stays_file_after_remove
1000:  PASS [   0.070s] ( 181/3165) pacquet-cli::dlx dlx_errors_when_no_command_given
1001:  PASS [   1.160s] ( 182/3165) pacquet-cli::dedupe_injected_deps injected_workspace_dep_with_children_stays_link_after_remove
1002:  PASS [   0.323s] ( 183/3165) pacquet-cli::dry_run dry_run_rejects_pnpr_server
1003:  PASS [   0.848s] ( 184/3165) pacquet-cli::dedupe_injected_deps injected_workspace_dep_with_dedupe_off_materialises_under_gvs
1004:  PASS [   0.881s] ( 185/3165) pacquet-cli::dedupe_injected_deps injected_workspace_dep_with_dedupe_off_writes_file_arm
1005:  PASS [   0.063s] ( 186/3165) pacquet-cli::exec exec_errors_when_command_not_found
1006:  PASS [   0.038s] ( 187/3165) pacquet-cli::exec exec_errors_when_no_command_given
1007:  PASS [   0.144s] ( 188/3165) pacquet-cli::exec exec_shell_mode_preserves_embedded_quotes
1008:  PASS [   0.860s] ( 189/3165) pacquet-cli::dry_run dry_run_reports_changes_without_writing
1009:  PASS [   0.874s] ( 190/3165) pacquet-cli::dry_run dry_run_reports_no_changes_when_up_to_date
1010:  PASS [   0.103s] ( 191/3165) pacquet-cli::find_hash should_fail_on_invalid_base64
1011:  PASS [   1.123s] ( 192/3165) pacquet-cli::dry_run dry_run_reports_added_dependency_without_touching_the_lockfile
1012:  PASS [   0.137s] ( 193/3165) pacquet-cli::find_hash should_fail_on_oversized_base64
1013:  PASS [   0.240s] ( 194/3165) pacquet-cli::init should_create_package_json
1014:  PASS [   1.324s] ( 195/3165) pacquet-cli::find_hash find_hash_works
1015:  PASS [   0.367s] ( 196/3165) pacquet-cli::init should_throw_on_existing_file
1016:  PASS [   1.035s] ( 197/3165) pacquet-cli::find_hash find_hash_works_with_base64
1017:  PASS [   0.822s] ( 198/3165) pacquet-cli::find_hash should_fail_on_missing_hash
1018:  PASS [   0.649s] ( 199/3165) pacquet-cli::inject_workspace_packages dependencies_meta_injected_per_dep_overrides_global_off
...

1030:  PASS [   0.834s] ( 211/3165) pacquet-cli::install install_surfaces_catalog_misconfiguration
1031:  PASS [   0.939s] ( 212/3165) pacquet-cli::install peer_dependencies_resolve_from_aliased_subdependencies
1032:  PASS [   0.960s] ( 213/3165) pacquet-cli::install peer_dependency_prefers_highest_aliased_subdependency_version
1033:  PASS [   0.971s] ( 214/3165) pacquet-cli::install peer_dependency_prefers_highest_version_among_aliases_of_same_package
1034:  PASS [   0.927s] ( 215/3165) pacquet-cli::install peer_dependency_prefers_non_aliased_provider_over_alias
1035:  PASS [   0.840s] ( 216/3165) pacquet-cli::install peer_dependency_resolves_from_alias_that_differs_from_real_name
1036:  PASS [   0.836s] ( 217/3165) pacquet-cli::install peer_dependency_resolves_from_aliased_direct_dependency
1037:  PASS [   0.915s] ( 218/3165) pacquet-cli::install resolution_mode_highest_picks_highest_direct_version
1038:  PASS [   0.967s] ( 219/3165) pacquet-cli::install peer_shared_through_a_diamond_is_resolved_consistently
1039:  PASS [   0.798s] ( 220/3165) pacquet-cli::install resolution_mode_lowest_direct_picks_lowest_direct_version
1040:  PASS [   0.839s] ( 221/3165) pacquet-cli::install should_install_circular_dependencies
1041:  PASS [   0.987s] ( 222/3165) pacquet-cli::install should_install_dependencies
1042:  PASS [   1.108s] ( 223/3165) pacquet-cli::install should_install_exec_files
1043:  PASS [   0.829s] ( 224/3165) pacquet-cli::install should_install_index_files
1044:  PASS [   0.693s] ( 225/3165) pacquet-cli::install transitive_pending_peer_uses_provider_final_suffix_in_lockfile
1045:  PASS [   0.956s] ( 226/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::add_fails_under_strict_dep_builds_when_a_build_is_ignored
1046:  PASS [   0.929s] ( 227/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::add_warns_without_strict_dep_builds_when_a_build_is_ignored
1047:  PASS [   0.845s] ( 228/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::bins_linked_even_if_scripts_ignored
1048:  PASS [   0.648s] ( 229/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::install_ignore_scripts_does_not_fail_under_strict_dep_builds
1049:  PASS [   1.322s] ( 230/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::headless_run_pre_postinstall_scripts
1050:  PASS [   0.920s] ( 231/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::lifecycle_scripts_run_after_linking_root_deps
1051:  PASS [   1.012s] ( 232/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::lifecycle_scripts_run_before_linking_bins
1052:  PASS [   0.826s] ( 233/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::run_install_scripts
1053:  PASS [   1.620s] ( 234/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::rebuild_after_allow_builds_changes
1054:  PASS [   1.259s] ( 235/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::run_pre_and_postinstall_scripts
1055:  PASS [   1.563s] ( 236/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::selectively_allow_scripts_by_allow_builds
1056:  PASS [   3.041s] ( 237/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::lifecycle_scripts_run_in_dependency_order
1057:  PASS [   1.405s] ( 238/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::selectively_allow_scripts_by_allow_builds_exact_versions
1058:  PASS [   1.441s] ( 239/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::selectively_ignore_scripts_by_allow_builds
1059:  PASS [   1.021s] ( 240/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::strict_install_keeps_failing_on_warm_rerun
1060:  PASS [   0.669s] ( 241/3165) pacquet-cli::lifecycle_scripts project_scripts::failing_project_script_fails_the_install
1061:  PASS [   0.870s] ( 242/3165) pacquet-cli::lifecycle_scripts project_scripts::add_does_not_run_project_lifecycle_scripts
1062:  PASS [   1.126s] ( 243/3165) pacquet-cli::lifecycle_scripts dependency_build_scripts::strict_install_keeps_failing_with_unreadable_modules_yaml
1063:  PASS [   0.603s] ( 244/3165) pacquet-cli::lifecycle_scripts project_scripts::ignore_scripts_skips_project_lifecycle_scripts
1064:  PASS [   0.782s] ( 245/3165) pacquet-cli::lifecycle_scripts project_scripts::project_script_sees_init_cwd
1065:  PASS [   1.078s] ( 246/3165) pacquet-cli::lifecycle_scripts project_scripts::runs_project_lifecycle_scripts_in_order
1066:  PASS [   0.771s] ( 247/3165) pacquet-cli::lifecycle_scripts project_scripts::runs_scripts_when_project_name_differs_from_directory
1067:  PASS [   0.587s] ( 248/3165) pacquet-cli::lockfile_only frozen_lockfile_only_rejects_a_stale_lockfile
1068:  PASS [   0.117s] ( 249/3165) pacquet-cli::lockfile_only lockfile_false_with_lockfile_only_is_a_config_conflict
1069:  PASS [   0.590s] ( 250/3165) pacquet-cli::lockfile_only frozen_lockfile_only_succeeds_without_materializing_when_fresh
1070:  PASS [   0.643s] ( 251/3165) pacquet-cli::lockfile_only lockfile_only_updates_importers_when_a_project_is_added
1071:  PASS [   1.721s] ( 252/3165) pacquet-cli::lifecycle_scripts project_scripts::runs_project_lifecycle_scripts_on_frozen_install
1072:  PASS [   0.928s] ( 253/3165) pacquet-cli::lockfile_only writes_lockfile_without_downloading_or_linking
1073:  PASS [   0.790s] ( 254/3165) pacquet-cli::lockfile_resolution_reuse reinstalling_an_unchanged_manifest_keeps_the_lockfile_byte_identical
1074:  PASS [   0.631s] ( 255/3165) pacquet-cli::lockfile_verification install_fails_under_huge_minimum_release_age
1075:  PASS [   1.096s] ( 256/3165) pacquet-cli::lockfile_resolution_reuse reuses_unchanged_subtree_without_re_resolving_from_the_registry
1076:  PASS [   1.693s] ( 257/3165) pacquet-cli::lockfile_resolution_reuse a_reused_tree_is_structurally_identical_to_a_fresh_resolve
1077:  PASS [   0.710s] ( 258/3165) pacquet-cli::outdated outdated_compatible_ignores_out_of_range_releases
1078:  PASS [   0.446s] ( 259/3165) pacquet-cli::outdated outdated_json_empty_when_up_to_date
1079:  PASS [   0.554s] ( 260/3165) pacquet-cli::outdated outdated_json_format
1080:  PASS [   0.618s] ( 261/3165) pacquet-cli::outdated outdated_list_format
1081:  PASS [   0.097s] ( 262/3165) pacquet-cli::outdated outdated_no_dependencies_no_lockfile_is_empty
1082:  PASS [   0.536s] ( 263/3165) pacquet-cli::outdated outdated_long_shows_deprecation_details
1083:  PASS [   0.474s] ( 264/3165) pacquet-cli::outdated outdated_npm_alias_reports_real_name
1084:  PASS [   0.473s] ( 265/3165) pacquet-cli::outdated outdated_pattern_filters_dependencies
1085:  PASS [   0.824s] ( 266/3165) pacquet-cli::outdated outdated_prod_dev_filtering
1086:  PASS [   0.714s] ( 267/3165) pacquet-cli::outdated outdated_recursive_is_rejected
1087:  PASS [   0.476s] ( 268/3165) pacquet-cli::outdated outdated_reports_deprecated_package
1088:  PASS [   0.463s] ( 269/3165) pacquet-cli::outdated outdated_reports_newer_version
1089:  PASS [   0.091s] ( 270/3165) pacquet-cli::outdated outdated_without_lockfile_errors
1090:  PASS [   0.387s] ( 271/3165) pacquet-cli::outdated outdated_up_to_date_exits_zero
1091:  PASS [   0.634s] ( 272/3165) pacquet-cli::pnpr_install install_via_pnpr_links_node_modules
1092:  PASS [   0.849s] ( 273/3165) pacquet-cli::pnpr_install frozen_install_via_pnpr_verifies_the_local_lockfile_without_resolving_or_redownloading
1093:  PASS [   0.248s] ( 274/3165) pacquet-cli::pnpr_install install_via_pnpr_lockfile_only_writes_lockfile_without_linking
1094:  PASS [   0.081s] ( 275/3165) pacquet-cli::remove should_fail_when_dependency_is_missing
1095:  PASS [   0.099s] ( 276/3165) pacquet-cli::remove should_fail_when_no_package_specified
1096:  PASS [   0.819s] ( 277/3165) pacquet-cli::remove should_accept_aliases
1097:  PASS [   0.675s] ( 278/3165) pacquet-cli::remove should_remove_from_package_json
1098:  PASS [   0.099s] ( 279/3165) pacquet-cli::remove should_report_project_has_no_dependencies
1099:  PASS [   0.037s] ( 280/3165) pacquet-cli::restart stop_without_script_fails
1100:  PASS [   0.031s] ( 281/3165) pacquet-cli::restart stop_without_script_succeeds_with_if_present
1101:  PASS [   0.036s] ( 282/3165) pacquet-cli::run run_empty_start_script_hits_server_js_guard
1102:  PASS [   0.035s] ( 283/3165) pacquet-cli::run run_errors_on_missing_script_without_if_present
1103:  PASS [   0.041s] ( 284/3165) pacquet-cli::run run_lists_scripts_when_no_name_given
1104:  PASS [   0.481s] ( 285/3165) pacquet-cli::remove should_remove_only_from_targeted_field
1105:  PASS [   0.031s] ( 286/3165) pacquet-cli::run run_start_without_script_or_server_errors
1106:  PASS [   0.031s] ( 287/3165) pacquet-cli::run run_with_if_present_is_a_noop_for_missing_script
1107:  PASS [   0.099s] ( 288/3165) pacquet-cli::run run_preserves_embedded_quotes_in_script
1108:  PASS [   0.671s] ( 289/3165) pacquet-cli::stale_pin_dedupe does_not_refresh_an_aliased_transitive_dependency
1109:  PASS [   0.642s] ( 290/3165) pacquet-cli::stale_pin_dedupe refreshes_stale_transitive_pin_for_caret_range_direct_dep
1110:  PASS [   0.037s] ( 291/3165) pacquet-cli::store store_path_should_return_store_dir_from_pnpm_workspace_yaml
1111:  PASS [   0.490s] ( 292/3165) pacquet-cli::stale_pin_dedupe refreshes_stale_transitive_pin_to_higher_direct_dep_version
1112:  PASS [   0.732s] ( 293/3165) pacquet-cli::tarball_url_dependency remote_tarball_integrity_survives_unrelated_install
1113:  PASS [   0.500s] ( 294/3165) pacquet-cli::tarball_url_dependency remote_tarball_reresolves_from_warm_store_without_refetch
1114:  PASS [   0.610s] ( 295/3165) pacquet-cli::update update_aliases_work
1115:  PASS [   0.586s] ( 296/3165) pacquet-cli::update update_bumps_within_range
1116:  PASS [   0.528s] ( 297/3165) pacquet-cli::update update_compatible_all_direct_ignored_still_updates_indirect
1117:  PASS [   0.461s] ( 298/3165) pacquet-cli::update update_compatible_honors_ignore_dependencies
1118:  PASS [   0.416s] ( 299/3165) pacquet-cli::update update_depth_zero_unknown_package_errors
1119:  PASS [   0.562s] ( 300/3165) pacquet-cli::update update_latest_all_direct_ignored_does_not_touch_indirect
1120:  PASS [   0.568s] ( 301/3165) pacquet-cli::update update_latest_all_ignored_is_noop
1121:  PASS [   0.603s] ( 302/3165) pacquet-cli::update update_latest_catalog_preserves_reference_and_operator
1122:  PASS [   0.579s] ( 303/3165) pacquet-cli::update update_latest_default_catalog_preserves_reference
1123:  PASS [   0.537s] ( 304/3165) pacquet-cli::update update_latest_honors_ignore_dependencies
1124:  PASS [   0.564s] ( 305/3165) pacquet-cli::update update_latest_no_save_catalog_bumps_lockfile_only
1125:  PASS [   0.609s] ( 306/3165) pacquet-cli::update update_latest_no_save_keeps_manifest
1126:  PASS [   0.601s] ( 307/3165) pacquet-cli::update update_latest_preserves_exact
1127:  PASS [   0.564s] ( 308/3165) pacquet-cli::update update_latest_preserves_tilde
1128:  PASS [   0.446s] ( 309/3165) pacquet-cli::update update_latest_preserves_workspace_local_path_specifier
1129:  PASS [   0.527s] ( 310/3165) pacquet-cli::update update_latest_rewrites_manifest
1130:  PASS [   0.081s] ( 311/3165) pacquet-cli::update update_latest_unmatched_selector_does_not_read_catalogs
1131:  PASS [   0.541s] ( 312/3165) pacquet-cli::update update_latest_save_exact_preserves_existing_caret
1132:  PASS [   0.538s] ( 313/3165) pacquet-cli::update update_latest_with_negation_selector
1133:  PASS [   0.556s] ( 314/3165) pacquet-cli::update update_latest_with_selector_is_scoped
1134:  PASS [   0.478s] ( 315/3165) pacquet-cli::update update_latest_with_spec_is_rejected
1135:  PASS [   0.085s] ( 316/3165) pacquet-cli::update update_strict_catalog_range_mismatch_errors
1136:  PASS [   0.634s] ( 317/3165) pacquet-cli::update update_prod_scopes_and_honors_ignore
1137:  PASS [   0.788s] ( 318/3165) pacquet-cli::update update_transitive_glob_mixed_with_direct_selector
1138:  PASS [   0.746s] ( 319/3165) pacquet-cli::update update_transitive_mixed_with_direct_selector
1139:  PASS [   0.586s] ( 320/3165) pacquet-cli::why why_depth_limits_output
1140:  PASS [   0.539s] ( 321/3165) pacquet-cli::why why_fails_without_package_name
1141:  PASS [   0.588s] ( 322/3165) pacquet-cli::why why_shows_reverse_tree_for_direct_dep
...

1166:  PASS [   0.008s] ( 347/3165) pacquet-cmd-shim bin_resolver::tests::no_bin_field_returns_empty
1167:  PASS [   0.008s] ( 348/3165) pacquet-cmd-shim bin_resolver::tests::pkg_owns_bin_default_rule
1168:  PASS [   0.008s] ( 349/3165) pacquet-cmd-shim bin_resolver::tests::pkg_owns_bin_overrides
1169:  PASS [   0.008s] ( 350/3165) pacquet-cmd-shim bin_resolver::tests::rejects_path_traversal_outside_package_root
1170:  PASS [   0.008s] ( 351/3165) pacquet-cmd-shim bin_resolver::tests::rejects_unsafe_bin_names
1171:  PASS [   0.007s] ( 352/3165) pacquet-cmd-shim bin_resolver::tests::reserved_relative_bin_names_are_rejected
1172:  PASS [   0.008s] ( 353/3165) pacquet-cmd-shim bin_resolver::tests::scoped_bin_name_strips_scope_prefix
1173:  PASS [   0.008s] ( 354/3165) pacquet-cmd-shim bin_resolver::tests::skip_dangerous_bin_locations
1174:  PASS [   0.008s] ( 355/3165) pacquet-cmd-shim bin_resolver::tests::skip_dangerous_bin_names
1175:  PASS [   0.008s] ( 356/3165) pacquet-cmd-shim bin_resolver::tests::skip_scoped_bin_names_with_path_traversal
1176:  PASS [   0.015s] ( 357/3165) pacquet-cmd-shim link_bins::tests::direct_origin_wins_over_hoisted_regardless_of_lexical
1177:  PASS [   0.015s] ( 358/3165) pacquet-cmd-shim link_bins::tests::hoisted_origin_loses_to_existing_direct
1178:  PASS [   0.008s] ( 359/3165) pacquet-cmd-shim link_bins::tests::link_bins_handles_missing_modules_dir
1179:  PASS [   0.015s] ( 360/3165) pacquet-cmd-shim link_bins::tests::lexical_compare_breaks_tie_when_neither_owns
1180:  PASS [   0.011s] ( 361/3165) pacquet-cmd-shim link_bins::tests::link_bins_of_packages_no_op_when_no_bins
1181:  PASS [   0.010s] ( 362/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_chmod_error_via_di
1182:  PASS [   0.010s] ( 363/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_create_bin_dir_error_via_di
1183:  PASS [   0.008s] ( 364/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_modules_dir_read_error_via_di
1184:  PASS [   0.010s] ( 365/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_parse_manifest_error
1185:  PASS [   0.009s] ( 366/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_probe_shim_source_error_via_di
1186:  PASS [   0.008s] ( 367/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_read_manifest_error_via_di
1187:  PASS [   0.010s] ( 368/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_target_chmod_error_via_di
1188:  PASS [   0.010s] ( 369/3165) pacquet-cmd-shim link_bins::tests::link_bins_propagates_write_shim_error_via_di
1189:  PASS [   0.015s] ( 370/3165) pacquet-cmd-shim link_bins::tests::link_bins_rewrites_when_only_canonical_flavor_exists
...

1208:  PASS [   0.008s] ( 389/3165) pacquet-cmd-shim shim::tests::generate_sh_shim_does_not_append_exe_twice
1209:  PASS [   0.008s] ( 390/3165) pacquet-cmd-shim shim::tests::generate_sh_shim_emits_direct_exec_when_no_runtime
1210:  PASS [   0.008s] ( 391/3165) pacquet-cmd-shim shim::tests::generate_sh_shim_matches_pnpm_typical_case
1211:  PASS [   0.008s] ( 392/3165) pacquet-cmd-shim shim::tests::generate_sh_shim_threads_args_when_prog_is_none
1212:  PASS [   0.008s] ( 393/3165) pacquet-cmd-shim shim::tests::generate_sh_shim_uses_windows_target_only_for_exe_branches
1213:  PASS [   0.009s] ( 394/3165) pacquet-cmd-shim shim::tests::is_shim_pointing_at_round_trips_through_marker
1214:  PASS [   0.008s] ( 395/3165) pacquet-cmd-shim shim::tests::lexical_normalize_drops_curdir_components
1215:  PASS [   0.008s] ( 396/3165) pacquet-cmd-shim shim::tests::lexical_normalize_drops_curdir_segments_directly
1216:  PASS [   0.008s] ( 397/3165) pacquet-cmd-shim shim::tests::lexical_normalize_keeps_leading_parent_segments
1217:  PASS [   0.008s] ( 398/3165) pacquet-cmd-shim shim::tests::parse_shebang_from_bytes_handles_crlf_and_lossy_utf8
1218:  PASS [   0.008s] ( 399/3165) pacquet-cmd-shim shim::tests::parse_shebang_returns_none_for_empty_prog
1219:  PASS [   0.008s] ( 400/3165) pacquet-cmd-shim shim::tests::parses_direct_shebang
1220:  PASS [   0.008s] ( 401/3165) pacquet-cmd-shim shim::tests::parses_env_dash_s_shebang
1221:  PASS [   0.008s] ( 402/3165) pacquet-cmd-shim shim::tests::parses_env_node_shebang
1222:  PASS [   0.008s] ( 403/3165) pacquet-cmd-shim shim::tests::read_head_filled_accumulates_short_reads_from_fake
1223:  PASS [   0.008s] ( 404/3165) pacquet-cmd-shim shim::tests::read_head_filled_propagates_io_error_from_fake
1224:  PASS [   0.010s] ( 405/3165) pacquet-cmd-shim shim::tests::read_head_filled_real_fs_long_file_fills_buffer
1225:  PASS [   0.010s] ( 406/3165) pacquet-cmd-shim shim::tests::read_head_filled_real_fs_short_file_returns_partial
1226:  PASS [   0.009s] ( 407/3165) pacquet-cmd-shim shim::tests::read_head_filled_terminates_on_zero_byte_read_from_fake
1227:  PASS [   0.009s] ( 408/3165) pacquet-cmd-shim shim::tests::real_fs_read_head_propagates_not_found
1228:  PASS [   0.011s] ( 409/3165) pacquet-cmd-shim shim::tests::real_fs_read_head_reads_up_to_buffer_size
1229:  PASS [   0.010s] ( 410/3165) pacquet-cmd-shim shim::tests::rejects_non_shebang_lines
1230:  PASS [   0.009s] ( 411/3165) pacquet-cmd-shim shim::tests::relative_target_collapses_to_dot_when_paths_share_dir
1231:  PASS [   0.010s] ( 412/3165) pacquet-cmd-shim shim::tests::relative_target_traverses_into_sibling_package
1232:  PASS [   0.011s] ( 413/3165) pacquet-cmd-shim shim::tests::search_script_runtime_falls_back_to_cmd_with_c_switch
1233:  PASS [   0.011s] ( 414/3165) pacquet-cmd-shim shim::tests::search_script_runtime_falls_back_to_extension
1234:  PASS [   0.009s] ( 415/3165) pacquet-cmd-shim shim::tests::search_script_runtime_propagates_non_not_found_io_errors
1235:  PASS [   0.017s] ( 416/3165) pacquet-cmd-shim shim::tests::search_script_runtime_reads_shebang_from_real_file
...

1285:  PASS [   0.013s] ( 466/3165) pacquet-config npmrc_auth::tests::cafile_not_found_is_silently_treated_as_unset
1286:  PASS [   0.014s] ( 467/3165) pacquet-config npmrc_auth::tests::cafile_reads_and_splits_into_per_cert_pems
1287:  PASS [   0.014s] ( 468/3165) pacquet-config npmrc_auth::tests::cafile_relative_path_loads_ca_from_disk_via_apply
1288:  PASS [   0.014s] ( 469/3165) pacquet-config npmrc_auth::tests::cafile_relative_path_resolves_against_npmrc_dir
1289:  PASS [   0.013s] ( 470/3165) pacquet-config npmrc_auth::tests::cafile_trailing_garbage_is_preserved_for_downstream_parser
1290:  PASS [   0.013s] ( 471/3165) pacquet-config npmrc_auth::tests::cascade_env_fallback_only_fires_when_npmrc_unset
1291:  PASS [   0.013s] ( 472/3165) pacquet-config npmrc_auth::tests::cascade_env_var_lowercase_lookup
1292:  PASS [   0.012s] ( 473/3165) pacquet-config npmrc_auth::tests::cascade_explicit_https_proxy_wins_over_legacy_key
1293:  PASS [   0.013s] ( 474/3165) pacquet-config npmrc_auth::tests::cascade_http_proxy_env_fallback_chain_proxy_var
1294:  PASS [   0.013s] ( 475/3165) pacquet-config npmrc_auth::tests::cascade_http_proxy_uses_resolved_https_proxy
1295:  PASS [   0.013s] ( 476/3165) pacquet-config npmrc_auth::tests::cascade_https_proxy_uses_legacy_proxy_when_unset
1296:  PASS [   0.012s] ( 477/3165) pacquet-config npmrc_auth::tests::cascade_no_proxy_comma_list_trimmed
1297:  PASS [   0.012s] ( 478/3165) pacquet-config npmrc_auth::tests::cascade_no_proxy_true_literal_becomes_bypass_variant
1298:  PASS [   0.013s] ( 479/3165) pacquet-config npmrc_auth::tests::cascade_npmrc_value_wins_over_env
1299:  PASS [   0.012s] ( 480/3165) pacquet-config npmrc_auth::tests::defaults_leave_tls_config_empty
1300:  PASS [   0.013s] ( 481/3165) pacquet-config npmrc_auth::tests::env_replace_failure_on_key_warns_and_drops_unresolved_to_empty
1301:  PASS [   0.013s] ( 482/3165) pacquet-config npmrc_auth::tests::env_replace_failure_preserves_resolved_and_default_placeholders
1302:  PASS [   0.013s] ( 483/3165) pacquet-config npmrc_auth::tests::env_replace_failure_warns_and_drops_unresolved_to_empty
1303:  PASS [   0.013s] ( 484/3165) pacquet-config npmrc_auth::tests::env_replace_substitutes_token
...

1363:  PASS [   0.016s] ( 544/3165) pacquet-config store_path::tests::resolve_store_dir_same_volume_uses_home_default
1364:  PASS [   0.018s] ( 545/3165) pacquet-config tests::auth_ini_without_registry_falls_back_to_npmjs_default
1365:  PASS [   0.013s] ( 546/3165) pacquet-config tests::empty_npm_config_workspace_dir_falls_through
1366:  PASS [   0.015s] ( 547/3165) pacquet-config tests::explicit_url_scoped_creds_pass_through
1367:  PASS [   0.012s] ( 548/3165) pacquet-config tests::fetch_retries_defaults_match_pnpm
1368:  PASS [   0.013s] ( 549/3165) pacquet-config tests::fetch_retry_keys_in_npmrc_are_ignored
1369:  PASS [   0.016s] ( 550/3165) pacquet-config tests::global_config_npmrc_auth_file_expands_env
1370:  PASS [   0.015s] ( 551/3165) pacquet-config tests::global_config_yaml_enables_gvs
1371:  PASS [   0.016s] ( 552/3165) pacquet-config tests::global_config_yaml_request_destination_values_expand_env
1372:  PASS [   0.016s] ( 553/3165) pacquet-config tests::global_config_yaml_workspace_only_keys_are_ignored
1373:  PASS [   0.015s] ( 554/3165) pacquet-config tests::global_virtual_store_dir_survives_workspace_yaml_anchor
1374:  PASS [   0.012s] ( 555/3165) pacquet-config tests::gvs_default_is_off_and_paths_derive_cleanly
1375:  PASS [   0.014s] ( 556/3165) pacquet-config tests::gvs_disabled_keeps_project_local_virtual_store
1376:  PASS [   0.014s] ( 557/3165) pacquet-config tests::gvs_user_pinned_virtual_store_routes_into_global_virtual_store_dir
1377:  PASS [   0.013s] ( 558/3165) pacquet-config tests::have_default_values
1378:  PASS [   0.014s] ( 559/3165) pacquet-config tests::invalid_workspace_yaml_propagates_error
1379:  PASS [   0.012s] ( 560/3165) pacquet-config tests::network_settings_defaults_match_pnpm
...

1422:  PASS [   0.014s] ( 603/3165) pacquet-config tests::virtual_store_dir_max_length_env_var_overrides_yaml
1423:  PASS [   0.014s] ( 604/3165) pacquet-config tests::virtual_store_dir_max_length_from_workspace_yaml
1424:  PASS [   0.013s] ( 605/3165) pacquet-config tests::virtual_store_dir_max_length_matches_pnpm_default
1425:  PASS [   0.015s] ( 606/3165) pacquet-config tests::workspace_subdir_anchors_modules_at_workspace_root
1426:  PASS [   0.016s] ( 607/3165) pacquet-config tests::workspace_subdir_reads_workspace_root_npmrc
1427:  PASS [   0.014s] ( 608/3165) pacquet-config tests::workspace_unscoped_creds_pin_to_workspace_registry
1428:  PASS [   0.014s] ( 609/3165) pacquet-config tests::yaml_global_virtual_store_dir_wins_over_derivation
1429:  PASS [   0.013s] ( 610/3165) pacquet-config version_policy::tests::bare_name_expands_verbatim
1430:  PASS [   0.012s] ( 611/3165) pacquet-config version_policy::tests::create_policy_bare_rule_after_exact_keeps_exact_versions
1431:  PASS [   0.012s] ( 612/3165) pacquet-config version_policy::tests::create_policy_bare_rule_listed_first_wins_over_later_exact
1432:  PASS [   0.013s] ( 613/3165) pacquet-config version_policy::tests::create_policy_deduplicates_repeated_versions_across_rules
1433:  PASS [   0.013s] ( 614/3165) pacquet-config version_policy::tests::create_policy_distinct_name_rules
1434:  PASS [   0.013s] ( 615/3165) pacquet-config version_policy::tests::create_policy_exact_version_match_returns_versions
1435:  PASS [   0.012s] ( 616/3165) pacquet-config version_policy::tests::create_policy_merges_exact_versions_and_unions_for_same_name
1436:  PASS [   0.012s] ( 617/3165) pacquet-config version_policy::tests::create_policy_multiple_exact_version_rules_for_same_name_merge
1437:  PASS [   0.013s] ( 618/3165) pacquet-config version_policy::tests::create_policy_range_specifier_in_version_errors
1438:  PASS [   0.012s] ( 619/3165) pacquet-config version_policy::tests::create_policy_scoped_bare_name_returns_any_version
1439:  PASS [   0.012s] ( 620/3165) pacquet-config version_policy::tests::create_policy_scoped_name_at_exact_version
1440:  PASS [   0.012s] ( 621/3165) pacquet-config version_policy::tests::create_policy_version_union_handles_whitespace
1441:  PASS [   0.012s] ( 622/3165) pacquet-config version_policy::tests::create_policy_version_union_scoped
1442:  PASS [   0.012s] ( 623/3165) pacquet-config version_policy::tests::create_policy_version_union_unscoped
1443:  PASS [   0.012s] ( 624/3165) pacquet-config version_policy::tests::create_policy_wildcard_after_exact_keeps_exact_versions
1444:  PASS [   0.013s] ( 625/3165) pacquet-config version_policy::tests::create_policy_wildcard_listed_first_wins_over_later_exact
1445:  PASS [   0.013s] ( 626/3165) pacquet-config version_policy::tests::create_policy_wildcard_name_matches_via_matcher
1446:  PASS [   0.012s] ( 627/3165) pacquet-config version_policy::tests::create_policy_wildcard_with_version_errors
1447:  PASS [   0.013s] ( 628/3165) pacquet-config version_policy::tests::duplicate_specs_collapse_in_set
1448:  PASS [   0.012s] ( 629/3165) pacquet-config version_policy::tests::empty_input_yields_empty_set
1449:  PASS [   0.012s] ( 630/3165) pacquet-config version_policy::tests::mixed_valid_invalid_union_errors
1450:  PASS [   0.011s] ( 631/3165) pacquet-config version_policy::tests::name_at_exact_version_expands_to_one_literal
1451:  PASS [   0.012s] ( 632/3165) pacquet-config version_policy::tests::name_with_wildcard_alone_is_kept_verbatim
1452:  PASS [   0.012s] ( 633/3165) pacquet-config version_policy::tests::non_semver_version_in_union_errors
1453:  PASS [   0.013s] ( 634/3165) pacquet-config version_policy::tests::scoped_bare_name_expands_verbatim
1454:  PASS [   0.012s] ( 635/3165) pacquet-config version_policy::tests::scoped_name_at_exact_version_expands_to_one_literal
1455:  PASS [   0.012s] ( 636/3165) pacquet-config version_policy::tests::version_union_expands_into_separate_literals
1456:  PASS [   0.012s] ( 637/3165) pacquet-config version_policy::tests::version_union_trims_whitespace_around_each_version
1457:  PASS [   0.012s] ( 638/3165) pacquet-config version_policy::tests::wildcard_name_with_version_errors
1458:  PASS [   0.013s] ( 639/3165) pacquet-config workspace_yaml::tests::apply_leaves_unset_fields_alone
1459:  PASS [   0.013s] ( 640/3165) pacquet-config workspace_yaml::tests::apply_overrides_npmrc_defaults
1460:  PASS [   0.014s] ( 641/3165) pacquet-config workspace_yaml::tests::apply_pushes_patched_dependencies_and_workspace_dir
1461:  PASS [   0.013s] ( 642/3165) pacquet-config workspace_yaml::tests::apply_replaces_git_shallow_hosts_defaults
1462:  PASS [   0.012s] ( 643/3165) pacquet-config workspace_yaml::tests::apply_resolves_relative_paths_against_base_dir
1463:  PASS [   0.012s] ( 644/3165) pacquet-config workspace_yaml::tests::catalog_mode_yaml_values_round_trip
1464:  PASS [   0.013s] ( 645/3165) pacquet-config workspace_yaml::tests::config_dependencies_cleared_as_workspace_only_field
1465:  PASS [   0.013s] ( 646/3165) pacquet-config workspace_yaml::tests::empty_overrides_clears_prior_non_empty_assignment
1466:  PASS [   0.012s] ( 647/3165) pacquet-config workspace_yaml::tests::empty_overrides_map_collapses_to_none
1467:  PASS [   0.012s] ( 648/3165) pacquet-config workspace_yaml::tests::empty_package_extensions_map_collapses_to_none
1468:  PASS [   0.013s] ( 649/3165) pacquet-config workspace_yaml::tests::expands_env_vars_inside_non_registry_workspace_values
1469:  PASS [   0.013s] ( 650/3165) pacquet-config workspace_yaml::tests::find_propagates_parse_yaml_error_on_malformed_manifest
1470:  PASS [   0.013s] ( 651/3165) pacquet-config workspace_yaml::tests::find_propagates_when_manifest_path_is_a_directory
...

1526:  PASS [   0.013s] ( 707/3165) pacquet-config workspace_yaml::tests::resolution_mode_yaml_values_round_trip
1527:  PASS [   0.013s] ( 708/3165) pacquet-config workspace_yaml::tests::script_shell_and_node_options_null_clears_inherited_value
1528:  PASS [   0.012s] ( 709/3165) pacquet-config workspace_yaml::tests::side_effects_cache_gates_truth_table
1529:  PASS [   0.012s] ( 710/3165) pacquet-config workspace_yaml::tests::swallows_unknown_top_level_keys
1530:  PASS [   0.013s] ( 711/3165) pacquet-config workspace_yaml::tests::trust_policy_yaml_values_round_trip
1531:  PASS [   0.012s] ( 712/3165) pacquet-config workspace_yaml::tests::trusted_settings_expand_env_vars_inside_request_destination_values
1532:  PASS [   0.013s] ( 713/3165) pacquet-config workspace_yaml::tests::unsafe_perm_force_true_on_windows
1533:  PASS [   0.013s] ( 714/3165) pacquet-config workspace_yaml::tests::workspace_and_child_concurrency_are_independent
1534:  PASS [   0.008s] ( 715/3165) pacquet-config-dir tests::linux_uses_dot_config
1535:  PASS [   0.008s] ( 716/3165) pacquet-config-dir tests::macos_uses_library_preferences
1536:  PASS [   0.008s] ( 717/3165) pacquet-config-dir tests::none_when_home_missing_and_env_bypass_unset
1537:  PASS [   0.008s] ( 718/3165) pacquet-config-dir tests::prefers_xdg_config_home_on_every_os_without_consulting_home
1538:  PASS [   0.008s] ( 719/3165) pacquet-config-dir tests::windows_uses_local_app_data
1539:  PASS [   0.008s] ( 720/3165) pacquet-config-dir tests::windows_without_local_app_data_falls_back_to_dot_config
1540:  PASS [   0.008s] ( 721/3165) pacquet-config-parse-overrides tests::catalog_protocol_resolves_to_catalog_specifier
1541:  PASS [   0.008s] ( 722/3165) pacquet-config-parse-overrides tests::catalog_protocol_with_missing_entry_errors
1542:  PASS [   0.008s] ( 723/3165) pacquet-config-parse-overrides tests::catalog_protocol_with_named_catalog_resolves
...

1546:  PASS [   0.008s] ( 727/3165) pacquet-config-parse-overrides tests::parses_bare_name_override
1547:  PASS [   0.008s] ( 728/3165) pacquet-config-parse-overrides tests::parses_name_at_version_override
1548:  PASS [   0.008s] ( 729/3165) pacquet-config-parse-overrides tests::parses_parent_child_selectors
1549:  PASS [   0.008s] ( 730/3165) pacquet-config-parse-overrides tests::parses_range_operators_in_target
1550:  PASS [   0.008s] ( 731/3165) pacquet-config-parse-overrides tests::range_operator_on_parent_does_not_split
1551:  PASS [   0.008s] ( 732/3165) pacquet-config-parse-overrides tests::rejects_invalid_selector
1552:  PASS [   0.008s] ( 733/3165) pacquet-config-parse-overrides tests::rejects_invalid_selector_with_whitespace
1553:  PASS [   0.010s] ( 734/3165) pacquet-crypto-hash tests::hash_from_file_normalizes_crlf
1554:  PASS [   0.008s] ( 735/3165) pacquet-crypto-hash tests::hash_is_sha256_base64_with_prefix
1555:  PASS [   0.008s] ( 736/3165) pacquet-crypto-hash tests::short_hash_is_first_32_hex_chars_of_sha256
1556:  PASS [   0.007s] ( 737/3165) pacquet-crypto-hash tests::shorten_above_threshold_hashes_to_max_length
1557:  PASS [   0.008s] ( 738/3165) pacquet-crypto-hash tests::shorten_below_threshold_is_identity
1558:  PASS [   0.008s] ( 739/3165) pacquet-crypto-hash tests::shorten_triggered_by_uppercase_unless_file_protocol
1559:  PASS [   0.021s] ( 740/3165) pacquet-crypto-shasums-file tests::malformed_hash_raises_malformed
1560:  PASS [   0.011s] ( 741/3165) pacquet-crypto-shasums-file tests::missing_file_name_raises_not_found
1561:  PASS [   0.056s] ( 742/3165) pacquet-crypto-shasums-file tests::missing_node_shasums_signature_fails
1562:  PASS [   0.014s] ( 743/3165) pacquet-crypto-shasums-file tests::parses_rows_into_sri_encoded_integrities
...

1738:  PASS [   0.009s] ( 919/3165) pacquet-env-replace tests::placeholder_inside_url
1739:  PASS [   0.009s] ( 920/3165) pacquet-env-replace tests::preserves_resolved_and_default_placeholders_alongside_unresolved
1740:  PASS [   0.008s] ( 921/3165) pacquet-env-replace tests::substitutes_simple_placeholder
1741:  PASS [   0.008s] ( 922/3165) pacquet-env-replace tests::trailing_dollar_with_no_byte_after_is_passthrough
1742:  PASS [   0.008s] ( 923/3165) pacquet-env-replace tests::unresolved_drops_to_empty_and_collects_placeholder
1743:  PASS [   0.008s] ( 924/3165) pacquet-env-replace tests::uses_default_when_variable_empty
1744:  PASS [   0.008s] ( 925/3165) pacquet-env-replace tests::uses_default_when_variable_unset
1745:  PASS [   0.008s] ( 926/3165) pacquet-env-replace tests::variable_wins_over_default_when_set
1746:  PASS [   0.009s] ( 927/3165) pacquet-executor extend_path::tests::extra_bin_paths_come_after_bins_and_node_gyp
1747:  PASS [   0.008s] ( 928/3165) pacquet-executor extend_path::tests::no_ancestors_when_wd_has_no_node_modules_segment
1748:  PASS [   0.008s] ( 929/3165) pacquet-executor extend_path::tests::node_gyp_comes_after_node_modules_dot_bin
1749:  PASS [   0.009s] ( 930/3165) pacquet-executor extend_path::tests::original_path_is_appended_last
1750:  PASS [   0.008s] ( 931/3165) pacquet-executor extend_path::tests::scripts_prepend_node_path_always_appends_dirname_of_node
1751:  PASS [   0.009s] ( 932/3165) pacquet-executor extend_path::tests::scripts_prepend_node_path_never_and_warn_only_do_not_prepend
1752:  PASS [   0.008s] ( 933/3165) pacquet-executor extend_path::tests::virtual_store_walk_orders_deepest_first
1753:  PASS [   0.024s] ( 934/3165) pacquet-executor lifecycle::tests::lifecycle_emits_exit_with_nonzero_code_on_failure
1754:  PASS [   0.022s] ( 935/3165) pacquet-executor lifecycle::tests::lifecycle_runs_under_silent_reporter
1755:  PASS [   0.010s] ( 936/3165) pacquet-executor lifecycle::tests::malformed_manifest_propagates_error
1756:  PASS [   0.009s] ( 937/3165) pacquet-executor lifecycle::tests::missing_manifest_returns_false
...

1764:  PASS [   0.008s] ( 945/3165) pacquet-executor make_env::tests::make_env_stamps_lifecycle_specific_keys
1765:  PASS [   0.008s] ( 946/3165) pacquet-executor make_env::tests::make_env_tmpdir_gating_mirrors_unsafe_perm
1766:  PASS [   0.008s] ( 947/3165) pacquet-executor make_env::tests::sanitize_env_key_matches_upstream_regex
1767:  PASS [   0.008s] ( 948/3165) pacquet-executor make_env::tests::stamp_package_handles_arrays
1768:  PASS [   0.008s] ( 949/3165) pacquet-executor make_env::tests::stamp_package_recurses_into_kept_buckets
1769:  PASS [   0.007s] ( 950/3165) pacquet-executor run_script::tests::build_command_without_args_returns_script_unchanged
1770:  PASS [   0.007s] ( 951/3165) pacquet-executor run_script::tests::posix_quote_escapes_embedded_single_quotes
1771:  PASS [   0.008s] ( 952/3165) pacquet-executor run_script::tests::posix_quote_leaves_safe_strings_unquoted
1772:  PASS [   0.008s] ( 953/3165) pacquet-executor run_script::tests::posix_quote_wraps_unsafe_strings
1773:  PASS [   0.008s] ( 954/3165) pacquet-executor shell::tests::batch_file_script_shell_allowed_on_posix
1774:  PASS [   0.008s] ( 955/3165) pacquet-executor shell::tests::batch_file_script_shell_rejected_on_windows
1775:  PASS [   0.008s] ( 956/316...

@zkochan zkochan merged commit 1c04a00 into main Jun 22, 2026
31 of 33 checks passed
@zkochan zkochan deleted the pnpr-vuln branch June 22, 2026 11:00
zkochan added a commit that referenced this pull request Jun 22, 2026
#12570 made pnpr download a proxied tarball in full, hash it, fsync
and rename it into the cache, then reopen and reread it before responding.
That serialized the upstream and client transfers (doubling the latency-bound
time per tarball) and added a full extra disk round-trip, regressing the
cold-cache integrated benchmarks by ~0.5s.

Restore concurrent streaming: tee each upstream chunk to the client as it
arrives while hashing it into a temp file, and promote the temp file into the
proxy cache only when the complete body matches the declared SRI. The cache
therefore still never stores unverified bytes, but the proxy no longer waits
for the whole body before the first byte reaches the client. Authoritative
verification of installed bytes remains the client's own SRI check against the
packument; the proxy guards only what it persists.

The structural guarantees from #12570 are kept: hosted-store
fail-closed, request-to-version binding, and the cache-hit integrity sidecar
fast path. Replace download_verified_to_cache with tee_verified_to_cache and
drop the reopen/reread on the cache-miss path.
zkochan added a commit that referenced this pull request Jun 22, 2026
… the packument

#12570 added, to the tarball *serve* path, a packument load + full
JSON parse (to derive the expected integrity) plus a sidecar read and a
conditional re-hash — on every request, before the cache lookup. Pre-#12570 a
cached/proxied tarball was streamed straight from disk with none of that. On a
warm proxy cache that per-serve work is the cold-store regression, not the
cache-miss buffering: the integrated benchmark serves from a warm pnpr cache,
so the streaming-tee change alone moved nothing.

Take the cache lookup first and trust the integrity sidecar: when a cached
tarball has a sidecar whose recorded length matches the file, the bytes were
already verified against the packument integrity when they were stored (the
cache-miss tee only promotes on an SRI match), so serve them directly without
re-loading or re-parsing the packument and without re-hashing. The packument is
now resolved lazily — only for a cache entry lacking a trustworthy sidecar
(verified once, then the sidecar is recorded) and for cache misses (the verified
fetch needs it). The client still verifies the bytes it installs against its own
resolved integrity, so the proxy guards what it writes, not every read.
zkochan added a commit that referenced this pull request Jun 22, 2026
The proxied-tarball integrity fix (#12570, GHSA-5f9g-98vq-2jxw)
started reconstructing each rewritten dist.tarball filename from its
version key (`<name>-<version>.tgz`). That assumes upstream tarball names
are always canonical, which breaks packages like esprima-fb whose real
npm tarball is `esprima-fb-3001.0001.0000-dev-harmony-fb.tgz` for version
`3001.1.0-dev-harmony-fb`: the rewritten URL no longer matched what npm
hosts, so the client recorded the wrong lockfile URL and the proxied
fetch 404'd.

Preserve the upstream basename verbatim in the rewrite again, and resolve
a tarball request's version and dist.integrity by matching the requested
filename against each version's dist.tarball basename instead of parsing
the version out of the filename. OSV screening re-runs against the
resolved version when it differs from the filename-derived one.

The GHSA protection is unchanged: served bytes are still verified against
the SRI declared by the version that owns that tarball name, so a
preserved name cannot smuggle in bytes of another provenance. Tests that
encoded the canonical-reconstruction behavior are updated to assert
basename preservation, with new coverage for non-canonical names.

Also fix a pre-existing compile break in the pnpr server test target:
UserStore::in_memory gained a MaxUsers argument (#12581) but one
test call site was not updated.
zkochan added a commit that referenced this pull request Jun 29, 2026
The proxy-cache tarball serve path re-hashed each cached `.tgz` against the
version's `dist.integrity` on every request (#12570). On a cold-store
install the client pulls ~1300 tarballs from pnpr, so the accelerator paid a
full SRI pass per tarball — the cold-store regression visible in Bencher from
the auth-era change cluster onward (~2.1s -> ~2.9s, persistent).

The re-hash is redundant. A tarball only enters the cache through
`download_verified_to_cache`, which verifies the bytes against `dist.integrity`
as they are written, so nothing unverified is ever stored. Every install client
also re-verifies the tarball it receives against that same integrity. Serve the
cached bytes directly instead.

Write-time verification (and rejection of a poisoned upstream tarball, never
caching it) is preserved, so GHSA-5f9g-98vq-2jxw stays mitigated. The
namespaced `/~<uplink>/` route keeps its sidecar-keyed integrity cache and is
unaffected.

Drops the now-dead `record_cached_tarball_integrity`/`discard_cached_tarball`
helpers and the per-serve cached-tarball integrity sidecar.
zkochan added a commit that referenced this pull request Jun 29, 2026
Removing only the on-read SRI re-hash recovered part of the cold-store
regression but not all of it: #12570 also made every tarball serve load
and parse the package's packument to bind the request to a version and its
`dist.integrity`. On a warm-cache cold-store install (~1300 tarballs pulled from
pnpr) that is a packument parse per tarball — the larger half of the regression.

The cache hit needs neither binding. A tarball only enters the proxy cache
through `download_verified_to_cache`, which resolves the version and verifies the
bytes against `dist.integrity` as they are written, so the cache only ever holds
correct bytes for a given (name, filename); the install client re-verifies
whatever it receives against its own expected integrity regardless. The OSV
screen on the filename's version still runs before the cache read.

So serve cached bytes directly and defer the packument load to the cache-miss
download path, which still needs the integrity to verify the freshly-fetched
bytes before caching them. This matches the pre-regression cache-hit serve cost
while keeping write-time verification (which the pre-regression path lacked), so
the bytes that enter the cache are still verified and GHSA-5f9g-98vq-2jxw stays
mitigated.
zkochan added a commit that referenced this pull request Jun 29, 2026
…from its own pnpr mock

The integrated benchmark fronts all arms with a single registry-mock built
from the PR's HEAD (`just integrated-benchmark` runs `cargo build --release
--bin=pnpr`, and the mock resolves that one `target/release/pnpr`). Tarball
serving — where the pnpr proxy spends its per-request time — happens in that
shared mock, not in the per-revision `pnpr@<rev>` accelerators (which only
offload resolution; clients fetch tarballs from the client registry). So a
tarball-serve change slows or speeds every arm equally and cancels out of the
`pnpr@HEAD` vs `pnpr@main` comparison — the blind spot that let the serve-path
regression in #12570 merge looking flat and only step up on the `main`
trend afterward.

Give each compared revision its own tarball-serving mock, built from that
revision's `pnpr`. `plan_revision_mocks` assigns a client-facing latency-proxy
port to every revision that has a `pnpr@<rev>` target (so its binary is built)
before `init()` bakes the port into each target's `.npmrc`; `benchmark()` then
spawns the mock (after `build()` produced the binary, after `init()` warmed the
shared storage) and fronts it with the same latency + bandwidth link the shared
mock uses. `pacquet@<rev>` and `pnpr@<rev>` of the same revision share that
revision's mock, so the pnpr-vs-direct ratio stays fair within a revision while
the serve-path delta becomes visible across revisions.

All revisions share the one warm runtime storage: serving a warm cache is
read-only (a hit neither re-downloads nor, after #12709, writes a
sidecar), so concurrent mocks don't contend. Non-Verdaccio modes plan no mock
and keep the existing single-registry behavior.

Also retry `rm -rf` on the transient "Directory not empty" macOS/APFS raises
between a just-finished install's store writes settling and the next
iteration's cleanup, so the benchmark can run locally on macOS at all.
zkochan added a commit that referenced this pull request Jun 29, 2026
- Drop the refactor-history reference (what #12570 added and why the
  old serve path was slow) from the cache-hit comment in serve_tarball; keep
  the current invariant (write-time + client verification) that explains why a
  hit can serve directly. That history belongs in the commit log.
- Put the "per-revision mock makes a serve-path delta visible (a shared mock
  cancels it out)" rationale in one place — `plan_revision_mocks` — and have
  `registry_for`, `benchmark()`, and `pnpr_command_with_binary` reference or
  state it briefly instead of re-deriving it.
- Drop the "served unscreened before this fix" framing from the OSV cache-hit
  test comment; describe the current behavior instead.
zkochan added a commit that referenced this pull request Jun 29, 2026
…ve path (#12709)

#12570 made every tarball serve — warm cache hits included — both
re-hash the cached bytes against dist.integrity and load+parse the packument to
bind the request to a version. The benchmark mock is pnpr and a cold-store
install pulls ~1300 tarballs through it, so both costs are paid per tarball:
together the cold-store regression from ~2.15s to ~3.15s on the Bencher pnpr
testbed (#12700 recovered only the resolution-cache part).

Both are redundant on a cache hit. A tarball only enters the proxy cache via
download_verified_to_cache, which resolves the version and verifies the bytes
against dist.integrity as they are written, so the cache only ever holds correct
bytes for a (name, filename); the install client re-verifies whatever it receives
anyway. Serve cached bytes directly and defer the packument load to the
cache-miss download path, which still verifies freshly-fetched bytes before
caching them. The OSV screen on the filename version still runs first.

Write-time verification is preserved, so GHSA-5f9g-98vq-2jxw stays mitigated —
the bytes that enter the cache are still verified, which the pre-regression serve
path did not even do. Drops the now-dead per-serve integrity sidecar and helpers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pnpr reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants