Skip to content

fix(pnpr): access-gate install-accelerator files and remove unauthenticated /v1/files#12181

Merged
zkochan merged 2 commits into
mainfrom
pnpr-vuln
Jun 4, 2026
Merged

fix(pnpr): access-gate install-accelerator files and remove unauthenticated /v1/files#12181
zkochan merged 2 commits into
mainfrom
pnpr-vuln

Conversation

@zkochan

@zkochan zkochan commented Jun 3, 2026

Copy link
Copy Markdown
Member

The pnpr install accelerator served content-addressed store files without binding them to the caller's access. A sha512 CAFS digest is shared across packages and reveals nothing about access, so the store's possession of a package's bytes is not a capability to receive them — a caller who knows a private package's digest could pull bytes the registry routes return 401 for. This PR closes both delivery paths.

1. Authorize served packages (/v1/install)

Check every served package against pnpr's own packages: policy before serving — the same decision serve_packument / serve_tarball make, evaluated in process with no network round trip, so a warm shared server keeps its resolution advantage. serve_install resolves the caller's Identity from Authorization; deny_unauthorized_packages denies the install — 401 anonymous, 403 authenticated-but-outside-the-allowed-set. The accelerator reuses the exact policy the registry routes use, so there's no separate ACL and no new way to misconfigure.

Tests (install_accelerator/tests.rs, in-process): anonymous → @private/foo401; authenticated → @private/foo ⇒ allowed; authenticated-but-not-in-allow-list → @team/foo403; in-allow-list ⇒ allowed; anonymous → public ⇒ allowed.

2. Remove the unauthenticated /v1/files

POST /v1/files served any file by digest with no auth and no package identity, so the per-package gate above can't cover it — it had to be removed, not gated. It was already superseded by the single-response inline path (#12178).

  • Server: /v1/install always answers with the inline gzipped body (lockfile + stats + store-index entries + the missing files' contents); the NDJSON two-trip path, the /v1/files route, handle_files, and FilesRequest/is_valid_sha512_hex are gone.
  • TS: @pnpm/pnpr.client now does the one inline request and hands the file frames to @pnpm/worker's writeCafsFiles, which writes them to the CAFS; the fetchAndWriteCafsFiles /v1/files fetcher is replaced.

Verified: cargo test -p pnpr (138 + integration) green, cargo clippy -p pnpr clean; @pnpm/worker, @pnpm/pnpr.client, and @pnpm/installing.deps-installer compile.

Scope note / follow-up

This authorizes against pnpr's own surface — the authority for everything the store can hold today (pnpr fetches anonymously, so cached content is pnpr-hosted or publicly fetchable). When credential forwarding lands, packages the client resolves from external registries under its own token carry no pnpr policy and will need per-caller re-verification against the owning registry (a per-(user, name@version) grant table, cleared on observed denial, with an optional TTL). Tracked in #12184; marked at deny_unauthorized_packages.


Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • Security Enhancements

    • Package downloads are now authenticated and authorized per-caller, preventing unauthorized access and removing implicit bearer capabilities.
  • Performance Improvements

    • Installation uses a single compressed install response that inlines package files for faster, more efficient installs.
    • Worker now writes received file frames directly for quicker materialization.
  • Tests

    • Added access-control tests covering authenticated and anonymous scenarios.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 67097765-12e2-4276-a0f1-06f0ed66ece4

📥 Commits

Reviewing files that changed from the base of the PR and between f3a2c38 and e05d497.

📒 Files selected for processing (11)
  • .changeset/pnpr-inline-only-access.md
  • pnpm/test/install/pnpmRegistry.ts
  • pnpr/client/src/fetchFromPnpmRegistry.ts
  • pnpr/crates/pnpr/src/install_accelerator.rs
  • pnpr/crates/pnpr/src/install_accelerator/diff.rs
  • pnpr/crates/pnpr/src/install_accelerator/protocol.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/tests/install_accelerator.rs
  • worker/src/index.ts
  • worker/src/start.ts
  • worker/src/types.ts
💤 Files with no reviewable changes (2)
  • pnpr/crates/pnpr/tests/install_accelerator.rs
  • pnpr/crates/pnpr/src/install_accelerator/protocol.rs
✅ Files skipped from review due to trivial changes (2)
  • pnpm/test/install/pnpmRegistry.ts
  • .changeset/pnpr-inline-only-access.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • worker/src/types.ts
  • pnpr/crates/pnpr/src/install_accelerator/diff.rs
  • worker/src/start.ts
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/client/src/fetchFromPnpmRegistry.ts
  • pnpr/crates/pnpr/src/install_accelerator.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)

Files:

  • worker/src/index.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12181
File: worker/src/start.ts:504-520
Timestamp: 2026-06-04T06:04:01.216Z
Learning: In pnpm/pnpm's pnpr install accelerator, the `/v1/install` response has a two-level framing structure:
1. **Outer layer** (full HTTP body): `[u32 outer header length][outer header JSON][files payload]` — `fetchFromPnpmRegistry` (pnpr/client/src/fetchFromPnpmRegistry.ts) strips the outer layer with `body.subarray(4 + headerLength)` and passes the remaining bytes to `writeCafsFiles`.
2. **Inner layer** (files payload): the files payload itself starts with its own `[u32 inner json length][inner header JSON]` prefix (built by the server's `build_files_payload` / `empty_files_payload_prefix`), followed by `[64-byte digest][u32 size][1-byte exec][content]` frames and a 64-zero-byte end marker.

`writeCafsFiles` in `worker/src/start.ts` is correct to read `jsonLen = payload.readUInt32BE(0)` and start frames at `offset = 4 + jsonLen` — this skips the inner header. The same two-level structure is mirrored in the Rust reference client (`parse_inline_response` + `write_files_payload`). Do not fla...
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:08:06.093Z
Learning: Pacquet (pnpm's Rust port) has a cardinal rule: "match pnpm exactly — do not fix pnpm quirks unless the same fix has landed in pnpm first." Review comments should not suggest behavioral deviations from upstream pnpm, even when the upstream behavior appears buggy. If a real bug is identified, it must be fixed upstream first.
📚 Learning: 2026-06-04T06:04:01.216Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12181
File: worker/src/start.ts:504-520
Timestamp: 2026-06-04T06:04:01.216Z
Learning: In pnpm/pnpm's pnpr install accelerator, the `/v1/install` response has a two-level framing structure:
1. **Outer layer** (full HTTP body): `[u32 outer header length][outer header JSON][files payload]` — `fetchFromPnpmRegistry` (pnpr/client/src/fetchFromPnpmRegistry.ts) strips the outer layer with `body.subarray(4 + headerLength)` and passes the remaining bytes to `writeCafsFiles`.
2. **Inner layer** (files payload): the files payload itself starts with its own `[u32 inner json length][inner header JSON]` prefix (built by the server's `build_files_payload` / `empty_files_payload_prefix`), followed by `[64-byte digest][u32 size][1-byte exec][content]` frames and a 64-zero-byte end marker.

`writeCafsFiles` in `worker/src/start.ts` is correct to read `jsonLen = payload.readUInt32BE(0)` and start frames at `offset = 4 + jsonLen` — this skips the inner header. The same two-level structure is mirrored in the Rust reference client (`parse_inline_response` + `write_files_payload`). Do not fla...

Applied to files:

  • worker/src/index.ts
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • worker/src/index.ts
🔇 Additional comments (1)
worker/src/index.ts (1)

15-22: LGTM!

Also applies to: 204-225


📝 Walkthrough

Walkthrough

This PR consolidates the pnpr install accelerator to a single gzipped POST /v1/install response that embeds missing-file contents, enforces per-caller package-read policies using the caller Identity and PackagePolicies, and updates client and worker code to materialize inlined file frames into CAFS.

Changes

Consolidate to single authorized install response

Layer / File(s) Summary
Protocol contracts
pnpr/crates/pnpr/src/install_accelerator/protocol.rs, pnpr/crates/pnpr/src/install_accelerator/diff.rs
InstallRequest drops inline_files; FilesRequest, FileDigest, and digest validator removed; MissingFile now contains only digest and executable; install-response framing docs updated.
Server authorization enforcement
pnpr/crates/pnpr/src/install_accelerator.rs, pnpr/crates/pnpr/src/install_accelerator/tests.rs
handle_install now accepts policies and Identity; new deny_unauthorized_packages enforces per-package policy and returns 401 (anonymous) or 403 (authenticated) when access is denied; verification failures render inline headers and empty files payload; unit tests added.
Server HTTP routing and compression
pnpr/crates/pnpr/src/server.rs
POST /v1/files route removed; serve_install derives Identity from Authorization and passes policies to handle_install; CompressionLayer exclusion updated for application/x-pnpr-install-inline.
Client response parsing
pnpr/client/src/fetchFromPnpmRegistry.ts
Client buffers and (if needed) gunzips the install response, parses [u32 header length][header JSON][file frames], rejects on violations, decodes indexEntries, and writes frames to CAFS via writeCafsFiles unless lockfileOnly; postInstall changed to buffered POST with gzip detection.
Worker file materialization
worker/src/types.ts, worker/src/index.ts, worker/src/start.ts
Worker message contract changes to write-cafs-files with payload: Uint8Array; worker parses header and [64-byte digest][u32 size][1-byte exec][content] frames terminated by 64 zero bytes, validates truncation, creates directories, writes files with proper exec mode, and recovers from truncated existing files atomically.
Release documentation & test harness comment
.changeset/pnpr-inline-only-access.md, pnpm/test/install/pnpmRegistry.ts
Changeset records the inline-only /v1/install behavior and removed /v1/files; test harness comment updated to reflect registry-mock serves only /v1/install.

Sequence Diagram — Authorized inline install flow:

sequenceDiagram
  participant Client
  participant serve_install
  participant handle_install
  participant write_cafs_worker
  Client->>serve_install: POST /v1/install + Authorization
  serve_install->>handle_install: runtime + policies + Identity + body
  handle_install->>write_cafs_worker: send framed payload (on success)
  write_cafs_worker-->>handle_install: filesWritten
  handle_install-->>Client: inline header + file frames
Loading

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#12178: Implements consolidation of missing-file contents into /v1/install with client parsing and framing updates.
  • pnpm/pnpm#12077: Introduced the original NDJSON /v1/install + /v1/files protocol that this PR replaces.
  • pnpm/pnpm#12170: Related changes to CompressionLayer handling for install-related content types.

Poem

🐰 One post, one gzipped tale,
Files inlined where policies prevail,
No bearer digests, identity holds sway,
CAFS filled from frames today—hip hooray! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the two main objectives: adding access control authorization to the install-accelerator and removing the unauthenticated /v1/files endpoint.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pnpr-vuln

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration.


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.

@codecov-commenter

codecov-commenter commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.82759% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.56%. Comparing base (f06ab5e) to head (e05d497).

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/install_accelerator.rs 94.73% 2 Missing ⚠️
pnpr/crates/pnpr/src/server.rs 94.73% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12181      +/-   ##
==========================================
+ Coverage   87.46%   87.56%   +0.09%     
==========================================
  Files         269      269              
  Lines       30848    30817      -31     
==========================================
+ Hits        26982    26985       +3     
+ Misses       3866     3832      -34     

☔ 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.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario has pacquet rows (direct install) and pnpr rows (the same client through the pnpr install accelerator), so pnpr@HEAD vs pacquet@HEAD is the pnpr-vs-direct ratio. Cold-store scenarios wipe the client store between runs (warm server); hot-store scenarios keep it warm. The pacquet@HEAD rows feed the pacquet Bencher testbed; the pnpr@HEAD rows feed the pnpr testbed.

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.674 ± 0.042 4.632 4.772 2.74 ± 0.13
pacquet@main 4.650 ± 0.045 4.603 4.761 2.73 ± 0.13
pnpr@HEAD 1.703 ± 0.082 1.610 1.867 1.00
pnpr@main 1.720 ± 0.072 1.630 1.859 1.01 ± 0.06
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.67418976078,
      "stddev": 0.042367746579312575,
      "median": 4.6628959092799995,
      "user": 2.4641971599999994,
      "system": 2.2962226600000006,
      "min": 4.63225850078,
      "max": 4.77229249978,
      "times": [
        4.6401374537799995,
        4.67234263578,
        4.63225850078,
        4.64770088378,
        4.69390129978,
        4.67531999878,
        4.65344918278,
        4.64507981878,
        4.70941533378,
        4.77229249978
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.65023027188,
      "stddev": 0.04529855343097615,
      "median": 4.64226688478,
      "user": 2.4512621599999997,
      "system": 2.2915428600000003,
      "min": 4.60333865978,
      "max": 4.76073291178,
      "times": [
        4.67612714778,
        4.76073291178,
        4.66349272278,
        4.65344268678,
        4.63112348778,
        4.60499732178,
        4.6432331287799995,
        4.60333865978,
        4.62451401078,
        4.64130064078
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.70296498278,
      "stddev": 0.0822687264520943,
      "median": 1.6743682747800002,
      "user": 2.79675926,
      "system": 1.99954316,
      "min": 1.6096718467800002,
      "max": 1.86653490878,
      "times": [
        1.76665008578,
        1.65423112178,
        1.86653490878,
        1.6464410187800003,
        1.6848639327800001,
        1.74979397478,
        1.6096718467800002,
        1.6638726167800002,
        1.76999172678,
        1.6175985947800002
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.71960507668,
      "stddev": 0.0718699978662101,
      "median": 1.72077272178,
      "user": 2.8122055599999998,
      "system": 2.00754716,
      "min": 1.6301069057800002,
      "max": 1.85915291478,
      "times": [
        1.7755715397800003,
        1.71232851278,
        1.72921693078,
        1.6301069057800002,
        1.85915291478,
        1.7367927307800002,
        1.77608652078,
        1.6779995277800002,
        1.66745020878,
        1.63134497478
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 501.3 ± 49.4 476.5 641.2 1.01 ± 0.12
pacquet@main 497.1 ± 29.7 482.5 581.1 1.00
pnpr@HEAD 520.1 ± 68.3 476.6 704.3 1.05 ± 0.15
pnpr@main 539.3 ± 63.5 484.7 668.1 1.09 ± 0.14
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.50131690486,
      "stddev": 0.049377852062072285,
      "median": 0.48621977946000006,
      "user": 0.36386781999999995,
      "system": 0.7924559199999999,
      "min": 0.47650176446000003,
      "max": 0.64117550446,
      "times": [
        0.64117550446,
        0.49415124046000003,
        0.48818053046000004,
        0.48501340446,
        0.48603575046,
        0.48539956346,
        0.48994146346000006,
        0.48640380846000003,
        0.48036601846000004,
        0.47650176446000003
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.49706991136,
      "stddev": 0.02973291948180692,
      "median": 0.48802642196,
      "user": 0.36412382,
      "system": 0.7902288199999999,
      "min": 0.48247816946000005,
      "max": 0.5810857114600001,
      "times": [
        0.5810857114600001,
        0.48679686046000004,
        0.48592770646000005,
        0.48792914446,
        0.49231574746000006,
        0.48890223846,
        0.48812369946000006,
        0.48247816946000005,
        0.49394390846,
        0.48319592746000006
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.5201138248599999,
      "stddev": 0.06827915327095403,
      "median": 0.49638850546,
      "user": 0.36596872,
      "system": 0.79141372,
      "min": 0.47657036946000003,
      "max": 0.7042748954600001,
      "times": [
        0.54342082146,
        0.47657036946000003,
        0.5212963204600001,
        0.50025988146,
        0.48002182846,
        0.48359003146,
        0.48138418646000003,
        0.49251712946000004,
        0.7042748954600001,
        0.51780278446
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.53934744936,
      "stddev": 0.06354535239779385,
      "median": 0.51725756996,
      "user": 0.36559971999999996,
      "system": 0.7836665199999999,
      "min": 0.48467706146000006,
      "max": 0.66808183846,
      "times": [
        0.63763420546,
        0.5550521904600001,
        0.49414081546000005,
        0.48467706146000006,
        0.50402190946,
        0.5182707174600001,
        0.49038302246000004,
        0.51624442246,
        0.66808183846,
        0.52496831046
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.928 ± 0.035 1.896 2.000 1.00
pacquet@main 1.935 ± 0.021 1.903 1.968 1.00 ± 0.02
pnpr@HEAD 1.936 ± 0.025 1.899 1.981 1.00 ± 0.02
pnpr@main 1.942 ± 0.017 1.919 1.980 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.9281450636,
      "stddev": 0.035328514324185906,
      "median": 1.9112929503,
      "user": 3.7479796,
      "system": 1.8790861199999995,
      "min": 1.8961289958,
      "max": 1.9998730468,
      "times": [
        1.9559181218000001,
        1.8976136318,
        1.9091946688,
        1.9510880578,
        1.9998730468,
        1.9561347438,
        1.8994647508000002,
        1.8961289958,
        1.9026433868000001,
        1.9133912318000001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.9349451229000003,
      "stddev": 0.021057315981856145,
      "median": 1.9366814213,
      "user": 3.7600420999999997,
      "system": 1.8973891199999997,
      "min": 1.9032497438000002,
      "max": 1.9683388528,
      "times": [
        1.9617792328,
        1.9499484038000001,
        1.9032497438000002,
        1.9382270358,
        1.9683388528,
        1.9194739358,
        1.9195717228,
        1.9351358068000002,
        1.9144405048000002,
        1.9392859898
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.9362276846,
      "stddev": 0.024667339933882176,
      "median": 1.9382255373000001,
      "user": 3.7428252,
      "system": 1.9072701199999997,
      "min": 1.8993874818,
      "max": 1.9805652768,
      "times": [
        1.9237276448000002,
        1.9805652768,
        1.8993874818,
        1.9320616448,
        1.9230803908,
        1.9528342588,
        1.9535415208000002,
        1.9443894298,
        1.9043297628,
        1.9483594348000002
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.9420318833999999,
      "stddev": 0.01722972315349656,
      "median": 1.9441889683000002,
      "user": 3.7493513999999997,
      "system": 1.9198462199999997,
      "min": 1.9193984678,
      "max": 1.9801034558000001,
      "times": [
        1.9477176658000002,
        1.9217799278,
        1.9801034558000001,
        1.9456852168,
        1.9468564178,
        1.9426927198000001,
        1.9334223768,
        1.9325159838000001,
        1.9193984678,
        1.9501466018
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.108 ± 0.021 1.090 1.149 1.01 ± 0.02
pacquet@main 1.116 ± 0.018 1.093 1.151 1.02 ± 0.02
pnpr@HEAD 1.105 ± 0.017 1.082 1.139 1.01 ± 0.02
pnpr@main 1.096 ± 0.018 1.075 1.129 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.10760687162,
      "stddev": 0.020710872600309065,
      "median": 1.09871823262,
      "user": 1.41505562,
      "system": 1.05676006,
      "min": 1.08976543462,
      "max": 1.1491846266199999,
      "times": [
        1.09928108762,
        1.10645168962,
        1.14244020062,
        1.09746697262,
        1.09815537762,
        1.10276819662,
        1.08976543462,
        1.09749533562,
        1.09305979462,
        1.1491846266199999
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.1156042646200002,
      "stddev": 0.017654245781451496,
      "median": 1.11583422612,
      "user": 1.4240138199999997,
      "system": 1.0363499600000001,
      "min": 1.09251425562,
      "max": 1.15067350862,
      "times": [
        1.12704546262,
        1.11258040062,
        1.09908714662,
        1.10748736762,
        1.09731645962,
        1.11908805162,
        1.12933438662,
        1.09251425562,
        1.15067350862,
        1.12091560662
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.10480838522,
      "stddev": 0.017033052137785738,
      "median": 1.10253001562,
      "user": 1.3945571199999995,
      "system": 1.04353916,
      "min": 1.08186494162,
      "max": 1.13947770462,
      "times": [
        1.10183521362,
        1.11234069262,
        1.10322481762,
        1.13947770462,
        1.08269024862,
        1.08186494162,
        1.10835091562,
        1.12040231562,
        1.09821749262,
        1.09967950962
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.0962499102199998,
      "stddev": 0.017562662306849205,
      "median": 1.0946012721199998,
      "user": 1.40150092,
      "system": 1.0357466599999998,
      "min": 1.07532550362,
      "max": 1.12934008862,
      "times": [
        1.12934008862,
        1.10848664762,
        1.07642743962,
        1.11286060462,
        1.09087865062,
        1.09463189662,
        1.07749963662,
        1.09457064762,
        1.10247798662,
        1.07532550362
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12181
Testbedpacquet

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Upper Boundary
(Limit %)
isolated-linker.fresh-restore.cold-cache.cold-storeLatency
seconds (s)
📈 plot
🚷 threshold
🚨 alert (🔔)
4.67 s
(+93.95%)Baseline: 2.41 s
2.89 s
(161.63%)

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
1,928.15 ms
(-16.08%)Baseline: 2,297.64 ms
2,757.17 ms
(69.93%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,107.61 ms
(-24.71%)Baseline: 1,471.10 ms
1,765.32 ms
(62.74%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
🚨 view alert (🔔)
4,674.19 ms
(+93.95%)Baseline: 2,409.99 ms
2,891.98 ms
(161.63%)

isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
501.32 ms
(-22.77%)Baseline: 649.08 ms
778.90 ms
(64.36%)
🐰 View full continuous benchmarking report in Bencher

…tall

A content-addressed digest in the install-accelerator store is shared
across packages and says nothing about access, so the store's possession
of a package's bytes is not a capability to receive them. `/v1/install`
served files for any package found in the store, including ones reached
only on the cache-hit / frozen-lockfile path where no access check
happened — letting a caller who knows a private package's digest pull
bytes the registry routes would 401 on.

Check every served package against pnpr's own `packages:` policy before
serving — the same decision `serve_packument` / `serve_tarball` make, in
process, with no network round trip (so a warm shared server keeps its
resolution advantage). `serve_install` resolves the caller's identity
from `Authorization`; `deny_unauthorized_packages` denies the install
(401 anonymous / 403 authenticated-but-outside-the-allowed-set) when any
served package is not readable by the caller.

This authorizes against pnpr's own surface, the authority for everything
the store can hold today (pnpr fetches anonymously, so cached content is
pnpr-hosted or publicly fetchable). When credential forwarding lands,
packages the client resolved from external registries under its own token
carry no pnpr policy and will need per-caller re-verification against the
owning registry (TTL-cached) — noted at the check and tracked in #12184.

The raw `/v1/files` endpoint is still unauthenticated; removing it (it is
superseded by the inline single-response path) is a follow-up (#12184)
that also ports the TS `@pnpm/pnpr.client` + worker off the two-trip path.

---
Written by an agent (Claude Code, claude-opus-4-8).
@zkochan zkochan changed the title fix(pnpr): authorize cached-package delivery per caller in /v1/install fix(pnpr): access-gate install-accelerator files and remove unauthenticated /v1/files Jun 4, 2026
@zkochan zkochan marked this pull request as ready for review June 4, 2026 05:23
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Authorize pnpr install packages and remove unauthenticated /v1/files endpoint

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Authorize served packages against pnpr's access policy in /v1/install
  - Every package's files checked before serving (401 anonymous, 403 forbidden)
  - Reuses existing packages: policy, evaluated in-process with no network round trip
• Remove unauthenticated /v1/files endpoint entirely
  - Collapse to single gzipped inline response from /v1/install
  - Client receives lockfile, stats, store-index entries, and file contents in one round trip
• Simplify client-side file handling
  - Replace two-trip NDJSON streaming with single binary response parsing
  - Worker receives decompressed payload directly instead of making separate HTTP request
Diagram
flowchart LR
  A["POST /v1/install<br/>with inlineFiles"] -->|resolve + diff| B["Check package<br/>access policy"]
  B -->|authorized| C["Build single<br/>gzipped response"]
  B -->|denied| D["401/403 error"]
  C -->|header + frames| E["Client parses<br/>header"]
  E -->|file frames| F["Worker writes<br/>to CAFS"]
  G["POST /v1/files<br/>REMOVED"] -.->|no longer exists| H["Eliminated<br/>unauth path"]

Loading

Grey Divider

File Changes

1. pnpr/crates/pnpr/src/install_accelerator.rs Bug fix, enhancement +110/-130

Add access gate and remove /v1/files handler

pnpr/crates/pnpr/src/install_accelerator.rs


2. pnpr/crates/pnpr/src/install_accelerator/diff.rs Refactoring +1/-6

Remove size field from MissingFile struct

pnpr/crates/pnpr/src/install_accelerator/diff.rs


3. pnpr/crates/pnpr/src/install_accelerator/protocol.rs ✨ Enhancement +0/-32

Remove inline_files flag and FilesRequest types

pnpr/crates/pnpr/src/install_accelerator/protocol.rs


View more (9)
4. pnpr/crates/pnpr/src/install_accelerator/tests.rs 🧪 Tests +73/-0

Add unit tests for package access authorization

pnpr/crates/pnpr/src/install_accelerator/tests.rs


5. pnpr/crates/pnpr/src/server.rs ✨ Enhancement +27/-21

Remove /v1/files route and update compression layer

pnpr/crates/pnpr/src/server.rs


6. pnpr/tests/install_accelerator.rs 🧪 Tests +0/-0

Remove integration tests for /v1/files endpoint

pnpr/tests/install_accelerator.rs


7. pnpr/client/src/fetchFromPnpmRegistry.ts ✨ Enhancement +82/-145

Simplify to single inline response, remove NDJSON streaming

pnpr/client/src/fetchFromPnpmRegistry.ts


8. worker/src/index.ts ✨ Enhancement +7/-9

Replace fetchAndWriteCafsFiles with writeCafsFiles

worker/src/index.ts


9. worker/src/start.ts ✨ Enhancement +62/-179

Update worker message handler for new write-cafs-files type

worker/src/start.ts


10. worker/src/types.ts ✨ Enhancement +9/-4

Replace FetchAndWriteCafsMessage with WriteCafsFilesMessage

worker/src/types.ts


11. .changeset/pnpr-inline-only-access.md 📝 Documentation +7/-0

Document breaking changes to pnpr install accelerator

.changeset/pnpr-inline-only-access.md


12. pnpr/crates/pnpr/tests/install_accelerator.rs Additional files +0/-117

...

pnpr/crates/pnpr/tests/install_accelerator.rs


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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pnpr/crates/pnpr/src/install_accelerator.rs (1)

232-264: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Authorize against all resolved packages, not result.package_index.

This gate misses two real paths: lockfile_only returns an empty package_index, and compute_diff() only populates package_index for packages the client does not already claim in store_integrities. That means an unauthorized caller can still get the resolved lockfile/metadata for private packages whenever no new index entry is emitted. Move the policy check to the full packages set immediately after resolve::collect_packages(), before fetch_uncached().

Suggested direction
     let packages = resolve::collect_packages(&lockfile, &config.registry);
+    if let Some(denied) = deny_unauthorized_resolved_packages(policies, &identity, &packages) {
+        return denied;
+    }

     // `--lockfile-only`: pnpm resolves and writes the lockfile but
     // fetches nothing and links nothing. Skip the tarball fetch + the
     // file-level diff and return just the lockfile; the client writes it
     // and stops, so the response carries no `D`/`I` lines.
@@
-    if let Some(denied) = deny_unauthorized_packages(policies, &identity, &result.package_index) {
-        return denied;
-    }
-
     let stats_json = stats_json(&result.stats);
     inline_response(runtime, &lockfile, &stats_json, &result)
🤖 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/install_accelerator.rs` around lines 232 - 264, The
authorization check currently calls deny_unauthorized_packages(...) against
result.package_index, which misses cases (lockfile_only and packages already in
store_integrities); instead, perform the policy check immediately after
resolve::collect_packages() using the full resolved packages list (the
`packages` Vec produced by resolve::collect_packages()) before calling
resolve::fetch_uncached() or computing diffs; move or add the
deny_unauthorized_packages call to use `packages` (mapping to the same
identity/package identifier fields used by deny_unauthorized_packages) so
unauthorized callers are rejected for any resolved package, including when
request.lockfile_only is true or compute_diff omits entries.
pnpr/crates/pnpr/src/server.rs (1)

1529-1534: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Bump the advertised protocol version for this wire break.

The handshake still says versions: [1], but v1 is no longer the old contract: /v1/install switched from NDJSON + /v1/files to a gzipped length-prefixed binary response, and /v1/files is gone. An older client that only knows the previous v1 flow will now negotiate successfully and then fail mid-install instead of failing fast at handshake time.

Minimal fix here
-    (StatusCode::OK, axum::Json(serde_json::json!({ "pnpr": { "versions": [1] } }))).into_response()
+    (StatusCode::OK, axum::Json(serde_json::json!({ "pnpr": { "versions": [2] } }))).into_response()

That needs the matching client/path rollout, or continued support for the old v1 flow during migration.

🤖 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/server.rs` around lines 1529 - 1534, The handshake in
serve_pnpr_handshake currently advertises "versions: [1]" but the v1 wire
contract changed; update the advertised protocol versions array (e.g., replace 1
with the new protocol number such as 2) in serve_pnpr_handshake so clients will
negotiate the correct protocol; ensure this change is coordinated with
client/path rollout or alternatively add explicit backward-compat handling in
the server for the old v1 flow (so serve_pnpr_handshake only advertises v1 if
that legacy flow is still supported).
🤖 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.

Inline comments:
In `@pnpr/client/src/fetchFromPnpmRegistry.ts`:
- Around line 194-213: In fetchFromPnpmRegistry.ts inside the HTTP response
handler (the callback using res, chunks, raw and gunzip), decompress the body
first (respecting content-encoding header or gzip magic bytes) into a single
Buffer/variable, then inspect res.statusCode and either reject with the
decompressed UTF-8 string for non-200 codes or resolve with the decompressed
Buffer for 200; specifically, replace the current branch that rejects on non-200
before decompression with logic that calls gunzip when needed (handling gunzip
errors via reject), sets a decompressed variable for both compressed and
uncompressed paths, and only then checks res.statusCode to decide
resolve/reject.

In `@worker/src/start.ts`:
- Around line 504-520: The code in writeCafsFiles incorrectly re-parses a
header: remove the JSON header parsing and start reading frames at offset = 0
(do not read jsonLen or advance by 4+jsonLen) because fetchFromPnpmRegistry
already strips the length-prefixed header; update the initial payload handling
(variables payload, jsonLen, and offset) so offset is initialized to 0 and any
length checks are adjusted accordingly so the loop that scans for END_MARKER and
reads [digest,size,exec,content] frames in writeCafsFiles works against the raw
frames buffer.

---

Outside diff comments:
In `@pnpr/crates/pnpr/src/install_accelerator.rs`:
- Around line 232-264: The authorization check currently calls
deny_unauthorized_packages(...) against result.package_index, which misses cases
(lockfile_only and packages already in store_integrities); instead, perform the
policy check immediately after resolve::collect_packages() using the full
resolved packages list (the `packages` Vec produced by
resolve::collect_packages()) before calling resolve::fetch_uncached() or
computing diffs; move or add the deny_unauthorized_packages call to use
`packages` (mapping to the same identity/package identifier fields used by
deny_unauthorized_packages) so unauthorized callers are rejected for any
resolved package, including when request.lockfile_only is true or compute_diff
omits entries.

In `@pnpr/crates/pnpr/src/server.rs`:
- Around line 1529-1534: The handshake in serve_pnpr_handshake currently
advertises "versions: [1]" but the v1 wire contract changed; update the
advertised protocol versions array (e.g., replace 1 with the new protocol number
such as 2) in serve_pnpr_handshake so clients will negotiate the correct
protocol; ensure this change is coordinated with client/path rollout or
alternatively add explicit backward-compat handling in the server for the old v1
flow (so serve_pnpr_handshake only advertises v1 if that legacy flow is still
supported).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c71eb48-f0de-4ed7-929b-807ddd9ac90a

📥 Commits

Reviewing files that changed from the base of the PR and between f06ab5e and f3a2c38.

📒 Files selected for processing (11)
  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
  • pnpr/crates/pnpr/src/install_accelerator.rs
  • pnpr/crates/pnpr/src/install_accelerator/diff.rs
  • pnpr/crates/pnpr/src/install_accelerator/protocol.rs
  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/tests/install_accelerator.rs
  • worker/src/index.ts
  • worker/src/start.ts
  • worker/src/types.ts
💤 Files with no reviewable changes (2)
  • pnpr/crates/pnpr/tests/install_accelerator.rs
  • pnpr/crates/pnpr/src/install_accelerator/protocol.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)

Files:

  • worker/src/types.ts
  • worker/src/index.ts
  • worker/src/start.ts
  • pnpr/client/src/fetchFromPnpmRegistry.ts
pnpr/**/pnpr/**/*.rs

📄 CodeRabbit inference engine (pnpr/AGENTS.md)

pnpr/**/pnpr/**/*.rs: Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling
Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions

Files:

  • pnpr/crates/pnpr/src/install_accelerator/diff.rs
  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/install_accelerator.rs
🧠 Learnings (39)
📓 Common learnings
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:11:04.272Z
Learning: In the pacquet Rust port (pnpm/pnpm repo), the `ResolvedPackage.optional` AND-folding on revisit intentionally mirrors pnpm's `resolveDependencies.ts:1627-1648` behavior: only the directly-revisited package's `optional` flag is updated; transitive descendants are not re-walked. pnpm CLI corrects stale optional flags downstream via `copyDependencySubGraph` BFS in `lockfile/pruner/src/index.ts:160-205`, which tracks a `nonOptional` set and re-stamps any package reachable by an all-non-optional path. Pacquet does not yet have this pruner equivalent, so the stale flags flow directly through `dependencies_graph_to_lockfile.rs:409` → `create_virtual_store.rs:762` → `installability.rs:394`. A follow-up to port `copyDependencySubGraph` is planned; until then, do not flag the resolver-layer optional propagation gap as a bug in pacquet PRs — it is intentional parity with pnpm's resolver layer.
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: User-visible changes (CLI flags, defaults, environment variables, lockfile/manifest/state-file formats, error codes/messages, log emissions, store layout, hook semantics) in pnpm must be mirrored to pacquet in the same PR
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • worker/src/types.ts
  • worker/src/index.ts
  • worker/src/start.ts
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-23T16:55:36.507Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11878
File: pacquet/crates/cli/tests/lockfile_verification.rs:158-162
Timestamp: 2026-05-23T16:55:36.507Z
Learning: In `pacquet/crates/cli/tests/lockfile_verification.rs`, the `trust_lockfile_skips_verification` and `trust_lockfile_cli_flag_skips_verification` tests intentionally do NOT assert `output.status.success()`. The hand-rolled fixture lockfile uses a placeholder integrity hash (`sha512-AAA…`), so the install always fails the downstream tarball integrity check regardless of the supply-chain gate. The contract being tested is "gate-skipped, not install-succeeded"; asserting success would require generating a real lockfile via the `generate_lockfile` pattern (see `hoist.rs`) which is considered not worth the extra wiring for an opt-out smoke test.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Tests that need the mocked registry should start `pnpr` through `pacquet-testing-utils`; `cargo test` / `cargo nextest run` should not require a separate `just registry-mock launch` step

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/**/*.rs : Follow the pacquet contributing guide (../pacquet/CONTRIBUTING.md) for test layout and Rust conventions

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Use snapshot tests with `insta` and carefully review diffs when intentional changes alter snapshots; accept with `cargo insta review` only after careful review

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : Tests are documentation — do not duplicate test scenarios, edge cases, failure modes, or worked examples in prose when they are already captured by tests

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Prefer `#[cfg_attr(target_os = "windows", ignore = "...")]` (or matching `#[cfg(unix)]` gates) over runtime probe-and-skip helpers for platform-locked tools

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
📚 Learning: 2026-05-20T21:18:56.391Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11778
File: pacquet/crates/resolving-local-resolver/tests/resolve.rs:365-372
Timestamp: 2026-05-20T21:18:56.391Z
Learning: In `pacquet/crates/resolving-local-resolver/tests/resolve.rs`, the test `fail_when_resolving_from_not_existing_directory_an_injected_dependency` intentionally uses `injected: false`. The test is a verbatim port of the upstream pnpm TypeScript test (resolving/local-resolver/test/index.ts at ef87f3ccff). The `injected` flag only affects the file/link protocol choice for plain directory paths; when the `file:` scheme is explicit in the bare specifier, the flag has no effect on the resolution code path. The misleading test name is inherited from upstream.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Tests must not be tolerant of missing build/runtime environment by silently returning early when a tool isn't found — let the test panic when a required tool is absent

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Prefer real fixtures over dependency-injection seams — use `tempfile::TempDir`, the mocked registry, or integration tests spawning the actual binary for happy paths and most error paths; use the DI seam only for filesystem error kinds, deterministic time, shared process-global state, or external-service happy paths

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator/tests.rs
  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-25T12:36:42.202Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: User-visible changes (CLI flags, defaults, environment variables, lockfile/manifest/state-file formats, error codes/messages, log emissions, store layout, hook semantics) in pnpm must be mirrored to pacquet in the same PR

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-25T12:36:42.202Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: Always explicitly include 'pnpm' in changeset files with appropriate version bump (patch, minor, or major)

Applied to files:

  • .changeset/pnpr-inline-only-access.md
📚 Learning: 2026-05-25T12:36:42.202Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-05-25T12:36:42.202Z
Learning: Version pnpm CLI patch for bug fixes, internal refactors, and changes that don't require documentation; minor for new features/settings that should be documented; major for breaking changes

Applied to files:

  • .changeset/pnpr-inline-only-access.md
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Do not change lockfile format, store layout, `.npmrc` semantics, or CLI surface unless pnpm changed them first

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-24T21:11:04.272Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:11:04.272Z
Learning: In the pacquet Rust port (pnpm/pnpm repo), the `ResolvedPackage.optional` AND-folding on revisit intentionally mirrors pnpm's `resolveDependencies.ts:1627-1648` behavior: only the directly-revisited package's `optional` flag is updated; transitive descendants are not re-walked. pnpm CLI corrects stale optional flags downstream via `copyDependencySubGraph` BFS in `lockfile/pruner/src/index.ts:160-205`, which tracks a `nonOptional` set and re-stamps any package reachable by an all-non-optional path. Pacquet does not yet have this pruner equivalent, so the stale flags flow directly through `dependencies_graph_to_lockfile.rs:409` → `create_virtual_store.rs:762` → `installability.rs:394`. A follow-up to port `copyDependencySubGraph` is planned; until then, do not flag the resolver-layer optional propagation gap as a bug in pacquet PRs — it is intentional parity with pnpm's resolver layer.

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Match how the same feature is implemented in the TypeScript pnpm CLI — any change in pacquet must match pnpm's behavior, logic, edge cases, config resolution, error messages, file/lockfile formats, and existing tests

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-20T23:08:06.093Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:08:06.093Z
Learning: Pacquet (pnpm's Rust port) has a cardinal rule: "match pnpm exactly — do not fix pnpm quirks unless the same fix has landed in pnpm first." Review comments should not suggest behavioral deviations from upstream pnpm, even when the upstream behavior appears buggy. If a real bug is identified, it must be fixed upstream first.

Applied to files:

  • .changeset/pnpr-inline-only-access.md
📚 Learning: 2026-05-24T21:11:04.272Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11915
File: pacquet/crates/resolving-deps-resolver/src/resolve_dependency_tree.rs:553-617
Timestamp: 2026-05-24T21:11:04.272Z
Learning: In pacquet (pnpm/pnpm repo), `ResolvedPackage.optional` AND-folding intentionally mirrors pnpm's resolveDependencies.ts:1627-1648 revisit behavior: only the directly-visited package's `optional` flag is updated on revisit, not transitive descendants. pnpm CLI corrects stale optional flags via `copyDependencySubGraph` BFS in `lockfile/pruner/src/index.ts:160-205`. Pacquet does not yet have this pruner equivalent, so raw `node.optional` flows directly into snapshot/virtual-store via `dependencies_graph_to_lockfile.rs:409` → `create_virtual_store.rs:762` → `installability.rs:394`. A follow-up issue to port `copyDependencySubGraph` is planned.

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-26T18:31:14.579Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11967
File: .changeset/git-fetcher-reject-non-sha-commits.md:2-2
Timestamp: 2026-05-26T18:31:14.579Z
Learning: In the pnpm monorepo, the `fetching/` directory contains multiple separate npm packages each with their own scoped name using a dot-separator convention, e.g., `pnpm/fetching.git-fetcher` (declared in `fetching/git-fetcher/package.json`) and `pnpm/fetching.tarball-fetcher`. There is no top-level `pnpm/fetching` package. Changesets targeting the git-fetcher should use `"pnpm/fetching.git-fetcher"`.

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-18T20:35:22.917Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11729
File: pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs:55-57
Timestamp: 2026-05-18T20:35:22.917Z
Learning: In `pacquet/crates/resolving-npm-resolver/src/fetch_attestation_published_at.rs`, the npm attestation endpoint (`/-/npm/v1/attestations/{pkg_name}@{version}`) intentionally does NOT percent-encode the package name — the endpoint accepts literal `/` in scoped package names (e.g. `scope/pkg`). This matches upstream pnpm's `fetchAttestationPublishedAt.ts` behavior. Do not flag missing URL encoding here. By contrast, the full-metadata fetch paths (`fetch_full_metadata`, `fetch_full_metadata_cached`) DO percent-encode via the `registry_url::to_registry_url` helper, matching upstream's `toUri` behavior.

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-23T17:30:06.849Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11878
File: resolving/npm-resolver/src/createNpmResolutionVerifier.ts:381-418
Timestamp: 2026-05-23T17:30:06.849Z
Learning: In `resolving/npm-resolver/src/pickPackage.ts` (pnpm/pnpm), the resolver's `PackageMetaCache` keys by `name` (abbreviated) and `name:full` (full metadata) only — no registry component is included. This is a pre-existing limitation meaning that if two different registries serve packages of the same name in one install, the cache will only hold the first fetched entry. The `createNpmResolutionVerifier.ts` shares this same cache and inherits the limitation; a `validateSharedMeta` name-check guards against cross-package contamination but cannot distinguish same-named packages from different registries. Tightening to a registry-qualified key would require a coordinated change to the resolver's cache key shape. The Pacquet/Rust side is already registry-qualified (`{registry}\x00{name}:full`).

Applied to files:

  • .changeset/pnpr-inline-only-access.md
  • pnpr/client/src/fetchFromPnpmRegistry.ts
  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/pnpr-inline-only-access.md
📚 Learning: 2026-05-20T20:41:50.322Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11773
File: pacquet/crates/package-manager/src/install_package_from_registry.rs:111-114
Timestamp: 2026-05-20T20:41:50.322Z
Learning: In pacquet (pnpm/pnpm repo, Rust codebase), `install_package_from_registry` is the npm-only install path. The npm resolver always stamps `ResolveResult.id` (a `PkgResolutionId`) as `nameversion`. Parsing it back through `PkgNameVer` with `.expect()` is intentional — a parse failure means a mis-dispatch bug, not malformed external input. Per pacquet's CLAUDE.md: "Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees." Do not suggest replacing such `.expect()` calls with graceful error handling.

Applied to files:

  • pnpr/client/src/fetchFromPnpmRegistry.ts
  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-06-01T08:59:48.719Z
Learnt from: KSXGitHub
Repo: pnpm/pnpm PR: 12093
File: pacquet/crates/cli/src/cli_args/run/recursive.rs:290-315
Timestamp: 2026-06-01T08:59:48.719Z
Learning: In pacquet's recursive run implementation (`pacquet/crates/cli/src/cli_args/run/recursive.rs`), the `pnpm-exec-summary.json` format for failed package entries correctly includes `prefix` and `message` fields in addition to `status` and `duration`. This matches pnpm's `ActionFailure` variant in `cli/utils/src/recursiveSummary.ts` and the direct serialization in `exec/commands/src/exec.ts`. There is no `ExecutionStatusInSummary` type in pnpm. The only intentional divergence is omitting the JS `error` field, whose `JSON.stringify` output is non-deterministic due to non-enumerable `Error` properties.

Applied to files:

  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-14T07:57:23.823Z
Learnt from: mandarini
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/src/pickPackage.ts:183-221
Timestamp: 2026-05-14T07:57:23.823Z
Learning: In `resolving/npm-resolver/src/pickPackage.ts`, the error-fallback `catch` block (the `loadMeta(pkgMirror)` path that fires when the primary network fetch throws) intentionally does NOT call `maybeUpgradeAbbreviatedMetaForReleaseAge` or retry with `fullMetadata: true`. This is by design: the network is already known-iffy at that point, so an extra fetch would risk compounding the failure. The `ignoreMissingTimeField` warn-and-skip path is the accepted graceful degradation here.

Applied to files:

  • pnpr/client/src/fetchFromPnpmRegistry.ts
📚 Learning: 2026-05-23T09:14:43.635Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11867
File: pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs:726-730
Timestamp: 2026-05-23T09:14:43.635Z
Learning: In `pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs`, the fresh-lockfile path intentionally does not run lifecycle scripts (`BuildModules` is not invoked, and `side_effects_maps_by_snapshot` from `CreateVirtualStoreOutput` is discarded). This is pre-existing behavior documented by an in-code comment mirroring upstream `link.ts:167-170`. The frozen-lockfile path wires `BuildModules` end-to-end as normal. Wiring lifecycle scripts into the fresh-lockfile path is tracked as future work, separate from this file's concern.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-23T09:14:43.635Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11867
File: pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs:726-730
Timestamp: 2026-05-23T09:14:43.635Z
Learning: In `pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs`, the fresh-lockfile path intentionally does not invoke `BuildModules` and discards `side_effects_maps_by_snapshot` from `CreateVirtualStoreOutput`. This is pre-existing, documented behavior (mirroring upstream `link.ts:167-170`): `importing_done` fires once extraction and symlink linking are complete, and the fresh-lockfile path does not run lifecycle scripts. The frozen-lockfile path wires `BuildModules` end-to-end as normal. Do not flag this omission as a bug; wiring lifecycle scripts into the fresh-lockfile path is tracked as future work separate from perf refactors.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-06-02T16:13:39.456Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12144
File: pnpr/crates/pnpr/src/install_accelerator/verdict_cache.rs:57-61
Timestamp: 2026-06-02T16:13:39.456Z
Learning: In `pnpr/crates/pnpr/src/install_accelerator/verdict_cache.rs`, the `lockfile_verdicts` SQLite table intentionally uses `hash` alone (not a composite `(hash, policy)` key) as the primary key — last-write-wins per hash. This mirrors the local `lockfile-verified.jsonl` cache design in pnpm. A looser current policy can trust a stricter cached pass via `can_trust_past_check`; alternating-policy re-verification is an accepted trade-off. A composite key was explicitly rejected to maintain parity with the local cache model.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-20T20:41:30.632Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11773
File: pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs:115-117
Timestamp: 2026-05-20T20:41:30.632Z
Learning: In the pacquet Rust port of pnpm, the `is_http_url` helper in `pacquet/crates/resolving-tarball-resolver/src/tarball_resolver.rs` intentionally uses `bare.starts_with("http:") || bare.starts_with("https:")` (not `"http://"` / `"https://"`) to match upstream pnpm's `startsWith('http:')` / `startsWith('https:')` check byte-for-byte. Pacquet's cardinal rule (pacquet/AGENTS.md) requires matching pnpm even on quirks; malformed non-URL inputs are rejected downstream by `reqwest::Url::parse` as a `ResolveError`.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-20T22:49:17.652Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11787
File: pacquet/crates/catalogs-resolver/src/lib.rs:156-167
Timestamp: 2026-05-20T22:49:17.652Z
Learning: In pacquet's `catalogs-resolver` crate (`pacquet/crates/catalogs-resolver/src/lib.rs`), the protocol detection pattern `catalog_lookup.split(':').next().unwrap_or("")` is an intentional byte-for-byte port of pnpm's upstream JavaScript `getProtocol`/split logic in `catalogs/resolver/src/resolveFromCatalog.ts#L95`. A bare value like `"workspace"` (without a colon) is deliberately classified as the `"workspace"` protocol, matching upstream behavior. pacquet's cardinal rule is to match upstream pnpm behavior including quirks; any behavioral change must land in pnpm first and then be ported here.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/crates/**/Cargo.toml : New registry-only crates must be placed under pnpr/crates/<short-name>/ and named pnpr-<short-name> in Cargo.toml, never using the pacquet- prefix

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Applies to pnpr/**/pnpr/**/*.rs : Follow the pacquet code-style guide (../pacquet/CODE_STYLE_GUIDE.md) for Rust-level conventions including imports, naming, ownership, and error handling

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/*.rs : User-facing errors go through `miette` via the `pacquet-diagnostics` crate; match pnpm's error codes and messages where pnpm defines them since error codes are part of the public contract

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-25T14:58:11.105Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11931
File: pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs:560-589
Timestamp: 2026-05-25T14:58:11.105Z
Learning: In `pacquet/crates/resolving-npm-resolver/src/create_npm_resolution_verifier.rs`, all per-`(registry, name[, version])` caches in `NpmResolutionVerifier` (`published_at`, `full_meta`, `full_meta_for_trust`, `abbreviated_meta`, `local_meta`) intentionally use the same pattern: lock → miss-check → release lock → await fetch/load → re-acquire lock → insert. This uniform pattern is deliberate; do not flag individual caches for using it. The known follow-up improvement (replacing the pattern with `tokio::sync::OnceCell` per key inside a `Mutex<HashMap<…>>`) is tracked as a future structural change to cover all five caches simultaneously.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:15.372Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pacquet/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:15.372Z
Learning: Applies to pacquet/**/tests/**/*.rs : Follow test-logging guidance — log before non-`assert_eq!` assertions, `dbg!` complex structures, skip logging for simple scalar `assert_eq!`

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-29T18:03:24.797Z
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Prefer existing pacquet-* crates over writing new code; check pacquet-tarball, pacquet-crypto-hash, pacquet-crypto-shasums-file, pacquet-package-manifest, pacquet-network, pacquet-registry, pacquet-fs, and pacquet-diagnostics before implementing non-trivial functionality

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
📚 Learning: 2026-05-20T10:06:55.749Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11760
File: pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs:15-18
Timestamp: 2026-05-20T10:06:55.749Z
Learning: In `pacquet/crates/resolving-deps-resolver/src/resolved_tree.rs`, the `DirectDep.id`, `ResolvedPackage.id`, and `ResolvedTree.packages` HashMap keys are intentionally plain `String` for now. The natural branded type would be `pacquet_lockfile::PkgNameVer`, but it cannot be used as a HashMap key because `node_semver::Version` does not derive `Hash`. The upstream parity type is `PkgResolutionId` (which carries an optional peer-dep suffix), and the branded type should be introduced alongside peer-dep resolution and lockfile generation work to avoid locking the seam too early.

Applied to files:

  • pnpr/crates/pnpr/src/install_accelerator.rs
🔇 Additional comments (2)
pnpr/crates/pnpr/src/install_accelerator/tests.rs (1)

1-42: LGTM!

Also applies to: 44-73

.changeset/pnpr-inline-only-access.md (1)

1-7: LGTM!

Comment thread pnpr/client/src/fetchFromPnpmRegistry.ts Outdated
Comment thread worker/src/start.ts
`POST /v1/files` served any CAFS file by digest with no authentication
and no package identity, so the access gate on `/v1/install` (which is
per package) couldn't cover it — it had to be removed, not gated. It was
already superseded by the single-response inline path (#12178).

* Server: `/v1/install` always answers with the inline gzipped body
  (lockfile + stats + store-index entries + the missing files' contents);
  the NDJSON two-trip path, the `/v1/files` route, `handle_files`, and the
  `FilesRequest`/`is_valid_sha512_hex` helpers are gone.
* TS client + worker: `@pnpm/pnpr.client` now does the one inline request
  and hands the file frames to `@pnpm/worker`'s `writeCafsFiles`, which
  writes them to the CAFS; the `fetchAndWriteCafsFiles` /v1/files fetcher
  is replaced. Error bodies are decompressed before being surfaced, since
  the server also gzips its JSON error responses (e.g. an access denial).

Verified end to end by `pnpm/test/install/pnpmRegistry.ts` (11 tests:
install / add / remove / workspace through a real pnpr server).

Closes the second half of the install-accelerator access work (#12184);
file-bearing responses are now both inline-only and access-gated.

---
Written by an agent (Claude Code, claude-opus-4-8).
@zkochan zkochan merged commit 3b76b8e into main Jun 4, 2026
26 checks passed
@zkochan zkochan deleted the pnpr-vuln branch June 4, 2026 07:32
zkochan added a commit that referenced this pull request Jun 4, 2026
…ternal private registries (#12184) (#12189)

Closes #12184 (part 2).

#12181 shipped the per-caller access gate on `POST /v1/install`, which authorizes every served package against pnpr's own `packages:` policy — the complete answer **while pnpr fetches anonymously**. This PR adds the remaining piece: forwarding the caller's per-registry credentials so the accelerator can resolve/fetch **external private** content as the caller, and gating that content per user against the registry that actually owns it.

## Credential forwarding (issue steps 1–2)

- **Wire:** `POST /v1/install` gains an `authHeaders` body map (`{ "//host/path/": "Bearer …" }`, the shape `AuthHeaders::from_map` consumes / `getAuthHeadersFromCreds` produces) plus an HTTP `Authorization` header. The body map carries the *upstream* registry tokens; the header identifies the caller to pnpr's own gate and keys the grant table.
- **pacquet plumbing:** a request-scoped `Arc<AuthHeaders>` is threaded via a new `Install.auth_override` field and an `auth_override` param on `build_resolution_verifiers`, so resolution/verification run as the caller **without** baking per-user auth into the interned `&'static Config` (which would leak one config per user).
- **Server:** `handle_install` builds the per-request `AuthHeaders` and threads it through resolve, verify, and `fetch_uncached` (which now returns the freshly-fetched set).
- **Clients:** pacquet `pnpr-client` and `@pnpm/pnpr.client` send `registry` / `namedRegistries` / `authHeaders` + `Authorization`; the TS path sources them from the caller's registry credentials via `@pnpm/network.auth-header` (`getAuthHeadersFromCreds` is newly re-exported). `@pnpm/worker` is unchanged — downloads happen server-side.
- **Credential scope:** both clients forward the caller's *full* credential map, not a subset scoped to the declared registries. The registries a dependency graph touches aren't knowable up front — a transitive package can be scope-routed to another registry or pinned to a tarball URL on a host that's in `.npmrc` but isn't a declared registry — so pnpr attaches the right token per fetched URL exactly as a local install does. These are package-fetch credentials going to the very service the caller configured to fetch its packages.

## Per-user grant table (issue steps 3–4)

Externally-resolved private content carries no pnpr policy, so the store's possession of the bytes must not authorize a user the upstream never cleared. A served package is dispatched by **whether a forwarded credential was used to fetch it**:

- **No forwarded cred → pnpr-as-authority:** the existing local `packages:` policy check, unchanged.
- **Forwarded cred → upstream-as-authority:** gated against a persistent `(user, name@version)` grant table (SQLite, modeled on `VerdictCache`). Freshly fetched this request ⇒ record + allow (the upstream just accepted the token). Cache hit with a standing grant ⇒ allow, no upstream trip. Cache hit, no grant ⇒ re-verify against the owning registry with the caller's credential — record on success; **clear-on-discovery** (purge the user's grants for the package) + deny on `401`/`403`. TTL is the `installAccelerator.grantTtl` config knob (default: permanent).

## Public vs private (no per-user gating for public packages)

A forwarded credential matching a registry doesn't mean a package is *private* — in a mixed proxy (one registry serving a company's private packages **and** public ones), the token matches everything, and gating public content per user would cost a grant row and a re-verify round trip per user for bytes anyone may read. So before the per-user path, a not-yet-classified cache hit is probed **anonymously**: a `2xx` classifies the package public in a global set (no user pays for it again, no grant, no further round trip); a `401`/`403` means it's genuinely private and falls through to the grant / re-verify path above. Public packages thus cost **one anonymous probe across the whole fleet**, not one per user.

## Tests

- pnpr: grant-table + public-set mechanics, regime dispatch, the upstream-authorization paths (fresh-fetch, granted cache hit, private re-verify-and-record, denied-clears-grants, public-classified-once-then-free), and forwarded-cred-routes-around-local-policy.
- pacquet `pnpr-client`: a test asserting `authHeaders` + `Authorization` travel on the wire.
- Full suites green: `pnpr` (237), `pacquet-package-manager` (389), `pacquet-pnpr-client` (12), `pacquet-network`/`config` (325); clippy `-D warnings`, `cargo fmt`, rustdoc `-D warnings --document-private-items`, `typos`, and the TS compile all clean.

## Scoped follow-ups (not in this PR)

- Clear-on-discovery fires at the re-verify hook only. A `401`/`403` during the cold resolve aborts the request anyway (nothing is served); threading the offending package out of the deep resolve error to also clear stale grants for *future* requests needs structured auth errors.
- Per-scope external registries route via the default registry, since pacquet doesn't yet surface `@scope:registry` routing in `collect_packages`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants