Skip to content

feat(pnpr): store hosted packages in an S3-compatible object store#12198

Merged
zkochan merged 6 commits into
mainfrom
pnpr-s3-hosted-store
Jun 4, 2026
Merged

feat(pnpr): store hosted packages in an S3-compatible object store#12198
zkochan merged 6 commits into
mainfrom
pnpr-s3-hosted-store

Conversation

@zkochan

@zkochan zkochan commented Jun 4, 2026

Copy link
Copy Markdown
Member

What

Lets pnpr store its hosted packages (the ones published to it, plus static-served content) in an S3-compatible object store instead of a local directory. Because the same code targets any S3-compatible endpoint, this also covers Cloudflare R2, MinIO, Backblaze B2, Wasabi, etc.

The local tokio::fs path remains the default — nothing changes unless you add the new s3: config block.

Why

The hosted store is pnpr's source of truth: durable, must be backed up, and can't be regenerated. That's exactly what belongs in object storage:

  • The provider handles durability/replication, so there's no single-node volume to back up.
  • Multiple stateless pnpr replicas can share one hosted store.
  • R2 is the S3 API, so a configurable endpoint gets it (and the other S3-compatibles) for free.

The disposable proxy cache and the install-accelerator SQLite stores deliberately stay on local disk — they're ephemeral, latency-sensitive, and streamed/locked in filesystem-shaped ways.

How

  • New s3.rs module: S3Settings (the YAML s3: block), a client builder (object_store crate; AWS-env credentials with explicit override, plus R2/MinIO/path-style/HTTP knobs), and an S3Store adapter (packument get/put, streaming tarball get, staged upload, delete, prefix-scoped list).
  • storage.rs: a HostedStore { Fs | S3 } backend enum routes the hosted ops; the cached store stays fs-only. Publish stages the decoded+verified tarball to local scratch, then finalize either renames (fs) or uploads (S3). open_tarball now returns a streaming response body so S3 reads stream straight through.
  • config.rs: parses s3: and builds the client once at config-load time (the only fallible step), so Storage construction stays infallible.
  • search.rs: local search now lists package names through the storage abstraction, so it works against a bucket too.
  • Documented (commented) in the bundled config.yaml.

Example: Cloudflare R2

storage: ./storage   # still backs the local proxy cache + upload staging
s3:
  bucket: my-pnpr-packages
  region: auto
  endpoint: https://<account-id>.r2.cloudflarestorage.com
  accessKeyId: ${PNPR_S3_ACCESS_KEY_ID}
  secretAccessKey: ${PNPR_S3_SECRET_ACCESS_KEY}

Tests

  • 6 unit tests for the S3Store adapter (against object_store's in-memory backend): packument round-trip, streaming upload+read with length, scoped keys + prefixes, idempotent delete, package listing.
  • 1 end-to-end test driving the real HTTP handlers (publish → serve) against an in-memory bucket, asserting nothing touches local disk.
  • Full pnpr suite green (252 tests), plus cargo fmt, cargo doc -D warnings, Perfectionist dylint, and taplo all clean.

Notes

  • pnpr is pacquet-side (Rust only); there's no pnpm-CLI registry server to mirror, so no TS changes and no changeset.
  • object_store brings its own reqwest 0.12 alongside the workspace's 0.13 (one extra transitive copy) — unavoidable, harmless.

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

Summary by CodeRabbit

  • New Features

    • S3-compatible hosted package storage (AWS S3, Cloudflare R2, MinIO, Backblaze B2)
    • CLI flag --cache to override the proxy-cache directory
    • Hosted store option: tarballs and package metadata may be stored/served from the configured hosted backend
  • Bug Fixes

    • Object-store errors now surface as a consistent 502 Bad Gateway
  • Documentation

    • Expanded S3/R2/MinIO examples, env-var substitution, and guidance on local vs hosted storage

zkochan added 3 commits June 4, 2026 21:20
The hosted store is pnpr's source of truth — published packages plus
static-served content. It can now live in an S3-compatible object store
instead of a local directory, so the durable data is replicated by the
provider and can be shared by several stateless pnpr replicas.

Any S3-compatible endpoint works: AWS S3, Cloudflare R2, MinIO,
Backblaze B2, etc. — configured via the new YAML `s3:` block. The
disposable proxy cache and the install-accelerator SQLite stores always
stay on local disk; only the hosted store is pluggable.

Implemented with the `object_store` crate behind a `HostedStore`
backend enum; the local `tokio::fs` path remains the default.
@coderabbitai

coderabbitai Bot commented Jun 4, 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: 457a79b2-03a4-4e82-acd8-8a4b0efeb512

📥 Commits

Reviewing files that changed from the base of the PR and between c532f20 and 9c634a1.

📒 Files selected for processing (2)
  • pnpr/crates/pnpr/src/s3.rs
  • pnpr/crates/pnpr/src/storage.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • pnpr/crates/pnpr/src/s3.rs
  • pnpr/crates/pnpr/src/storage.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). (7)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint

📝 Walkthrough

Walkthrough

Adds pluggable hosted storage (Fs or S3) via object_store, implements an S3-compatible backend with staging/upload/read/delete/list operations, refactors Storage and server handlers to use the hosted abstraction, extends config parsing and error mapping, and adds unit and integration tests plus docs/examples.

Changes

S3-backed hosted package storage

Layer / File(s) Summary
Dependencies and module scaffolding
Cargo.toml, pnpr/crates/pnpr/Cargo.toml, pnpr/crates/pnpr/src/lib.rs
Add object_store v0.12 workspace dependency with aws feature and declare internal s3 module.
S3 backend configuration and implementation
pnpr/crates/pnpr/src/s3.rs
Define S3Settings and normalized_prefix(), implement build_s3_store() to construct object-store clients, and provide S3Store with packument read/write, tarball streaming/upload/delete, staging path management, and package name listing.
S3 backend unit tests
pnpr/crates/pnpr/src/s3/tests.rs
Unit tests for packument roundtrips, tarball upload/download streaming, scoped prefix handling, idempotent deletion, package listing, and prefix normalization.
Error handling for object-store operations
pnpr/crates/pnpr/src/error.rs
Add RegistryError::ObjectStore(object_store::Error) and map it to StatusCode::BAD_GATEWAY.
Configuration schema and YAML parsing
pnpr/crates/pnpr/src/config.rs, pnpr/crates/pnpr/config.yaml
Introduce HostedStoreConfig (Fs/S3), add s3 to ConfigFile, default hosted_store to Fs in constructors, and build S3 client + normalized prefix in from_yaml_str; include commented YAML example.
Storage abstraction and tarball lifecycle refactor
pnpr/crates/pnpr/src/storage.rs
Introduce internal HostedStore enum routing ops to Fs or S3, refactor TarballSlot to tmp_path + name/filename, change Storage::new to accept HostedStoreConfig, implement hosted ops, open_tarballBody stream, and delegate finalize_tarball_slot to hosted backend.
Search implementation refactor
pnpr/crates/pnpr/src/search.rs
Refactor run_local_search to use storage.hosted_package_names() and storage.read_hosted_packument instead of filesystem walking; remove filesystem helpers.
Server wiring and handler updates
pnpr/crates/pnpr/src/server.rs
Pass config.hosted_store into Storage::new, return tarball_response from open_tarball's Body/len, and call run_local_search with the Storage instance.
End-to-end S3 backend integration tests
pnpr/crates/pnpr/tests/s3_backend.rs
Integration tests (InMemory object_store) for publish (happy path), rejected publish (integrity mismatch), and unpublish; helpers for token creation, publish payloads, staging and bucket assertions.
Documentation and configuration examples
pnpr/npm/pnpr/README.md, cspell.json
Add --cache CLI flag docs; expand README with S3/R2/MinIO examples and env-var substitution; add Backblaze and minioadmin to cspell allowlist.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#12195: Extends the hosted vs proxy-cache split by adding S3-backed hosted storage as an alternative to on-disk hosted storage.

Suggested reviewers

  • KSXGitHub

Poem

🐰 I hopped into code with a curious twitch,

Pushing tarballs to clouds without a glitch,
Packuments hum and URLs now gleam,
Cache tucked local while S3 holds the dream,
A rabbit cheers: "Deployed—let packages stream!"

🚥 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 clearly and concisely describes the main feature addition: S3-compatible object store support for hosting packages in pnpr.
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-s3-hosted-store

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.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      8.5±0.35ms   511.7 KB/sec    1.02      8.7±0.16ms   499.9 KB/sec

@codecov-commenter

codecov-commenter commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.14876% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.80%. Comparing base (43ad094) to head (9c634a1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/s3.rs 91.89% 9 Missing ⚠️
pnpr/crates/pnpr/src/storage.rs 92.03% 9 Missing ⚠️
pnpr/crates/pnpr/src/server.rs 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12198      +/-   ##
==========================================
+ Coverage   87.70%   87.80%   +0.10%     
==========================================
  Files         271      273       +2     
  Lines       31157    31418     +261     
==========================================
+ Hits        27326    27587     +261     
  Misses       3831     3831              

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

@zkochan zkochan marked this pull request as ready for review June 4, 2026 19:55
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Add S3-compatible object store backend for hosted packages

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add S3-compatible object store backend for hosted packages
  - Supports AWS S3, Cloudflare R2, MinIO, Backblaze B2, Wasabi
  - Configured via new YAML s3: block with bucket, region, endpoint, credentials
• Implement pluggable HostedStore enum routing between local filesystem and S3
  - Local tokio::fs remains default; S3 backend optional
  - Disposable proxy cache and install-accelerator stores stay on local disk
• Refactor storage layer to support streaming responses from S3
  - open_tarball now returns Body instead of fs::File
  - Enables direct streaming from object store without buffering
• Update search endpoint to work with both filesystem and S3 backends
  - Abstracts package name listing through storage interface
Diagram
flowchart LR
  Config["Config with s3: block"]
  Builder["build_s3_store"]
  Store["Arc&lt;dyn ObjectStore&gt;"]
  HostedStore["HostedStore enum"]
  Storage["Storage layer"]
  Publish["Publish flow"]
  Serve["Serve flow"]
  
  Config -->|S3Settings| Builder
  Builder -->|creates| Store
  Store -->|wraps in| HostedStore
  HostedStore -->|powers| Storage
  Storage -->|stages to| Publish
  Storage -->|streams from| Serve

Loading

Grey Divider

File Changes

1. pnpr/crates/pnpr/src/s3.rs ✨ Enhancement +240/-0

New S3-compatible object store adapter module

pnpr/crates/pnpr/src/s3.rs


2. pnpr/crates/pnpr/src/s3/tests.rs 🧪 Tests +135/-0

Unit tests for S3Store adapter with in-memory backend

pnpr/crates/pnpr/src/s3/tests.rs


3. pnpr/crates/pnpr/src/config.rs ✨ Enhancement +34/-0

Add HostedStoreConfig enum and S3Settings parsing

pnpr/crates/pnpr/src/config.rs


View more (11)
4. pnpr/crates/pnpr/src/storage.rs ✨ Enhancement +200/-40

Refactor storage to support pluggable hosted backends

pnpr/crates/pnpr/src/storage.rs


5. pnpr/crates/pnpr/src/error.rs Error handling +8/-1

Add ObjectStore error variant and HTTP status mapping

pnpr/crates/pnpr/src/error.rs


6. pnpr/crates/pnpr/src/search.rs ✨ Enhancement +10/-67

Update search to use storage abstraction for package listing

pnpr/crates/pnpr/src/search.rs


7. pnpr/crates/pnpr/src/server.rs ✨ Enhancement +7/-8

Wire hosted store config into Storage initialization

pnpr/crates/pnpr/src/server.rs


8. pnpr/crates/pnpr/src/lib.rs ✨ Enhancement +4/-2

Export new s3 module and HostedStoreConfig type

pnpr/crates/pnpr/src/lib.rs


9. pnpr/crates/pnpr/tests/s3_backend.rs 🧪 Tests +151/-0

End-to-end test of S3-backed hosted store via HTTP

pnpr/crates/pnpr/tests/s3_backend.rs


10. pnpr/crates/pnpr/Cargo.toml Dependencies +1/-0

Add object_store dependency with aws feature

pnpr/crates/pnpr/Cargo.toml


11. pnpr/crates/pnpr/config.yaml 📝 Documentation +16/-0

Document s3 configuration block with examples

pnpr/crates/pnpr/config.yaml


12. pnpr/npm/pnpr/README.md 📝 Documentation +104/-0

Add S3/R2 setup guide with complete examples

pnpr/npm/pnpr/README.md


13. Cargo.toml Dependencies +1/-0

Add object_store to workspace dependencies

Cargo.toml


14. cspell.json ⚙️ Configuration changes +2/-0

Add S3-related terms to spell checker dictionary

cspell.json


Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Jun 4, 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.806 ± 0.049 4.733 4.878 2.33 ± 0.07
pacquet@main 4.787 ± 0.028 4.751 4.824 2.32 ± 0.07
pnpr@HEAD 2.063 ± 0.062 1.994 2.177 1.00
pnpr@main 2.067 ± 0.079 1.950 2.167 1.00 ± 0.05
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.80564815002,
      "stddev": 0.04884466138189717,
      "median": 4.80399958812,
      "user": 2.4446072600000006,
      "system": 3.8464404199999995,
      "min": 4.73288706212,
      "max": 4.87763886012,
      "times": [
        4.87763886012,
        4.73288706212,
        4.84077648712,
        4.80998039912,
        4.73911187312,
        4.84440695012,
        4.77345121012,
        4.7861900971199995,
        4.85401978412,
        4.79801877712
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.78676312422,
      "stddev": 0.027809225048604922,
      "median": 4.7879147156199995,
      "user": 2.46610446,
      "system": 3.8330495199999994,
      "min": 4.75122529112,
      "max": 4.82353220612,
      "times": [
        4.82353220612,
        4.79074284012,
        4.8219296391199995,
        4.75122529112,
        4.7584788401199996,
        4.81935382112,
        4.77087999212,
        4.78994158112,
        4.7556591811199995,
        4.78588785012
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.0634164880199997,
      "stddev": 0.061859396836831,
      "median": 2.04537183612,
      "user": 2.57845016,
      "system": 3.3561240199999993,
      "min": 1.99385978812,
      "max": 2.17658855712,
      "times": [
        2.11019388212,
        2.1099351031199998,
        2.04979142012,
        1.99890540812,
        2.03199136312,
        1.99385978812,
        2.17658855712,
        2.00357290512,
        2.11837420112,
        2.04095225212
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.06720349182,
      "stddev": 0.07873498945466377,
      "median": 2.05355439662,
      "user": 2.5900123600000002,
      "system": 3.3775390200000004,
      "min": 1.94992998912,
      "max": 2.1668633541199998,
      "times": [
        2.16618770412,
        2.06283809412,
        2.1497515531199998,
        2.1668633541199998,
        2.03727592512,
        1.98829569212,
        1.99230893412,
        1.94992998912,
        2.04427069912,
        2.11431297312
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 670.4 ± 34.3 643.3 753.7 1.00
pacquet@main 689.1 ± 93.1 640.0 943.5 1.03 ± 0.15
pnpr@HEAD 701.2 ± 99.7 653.2 969.8 1.05 ± 0.16
pnpr@main 740.1 ± 54.5 666.9 842.8 1.10 ± 0.10
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6704196505000001,
      "stddev": 0.03433174919749147,
      "median": 0.6570707896000001,
      "user": 0.3670496,
      "system": 1.3181934199999998,
      "min": 0.6433220311000001,
      "max": 0.7537090391000001,
      "times": [
        0.7537090391000001,
        0.6906944351000001,
        0.6928643991000001,
        0.6558626561,
        0.6434379651000001,
        0.6537400841000001,
        0.6582789231,
        0.6449935131000001,
        0.6672934591,
        0.6433220311000001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6890553167,
      "stddev": 0.0930749560618876,
      "median": 0.6556960201,
      "user": 0.3691809999999999,
      "system": 1.32927332,
      "min": 0.6399807751000001,
      "max": 0.9434773991000001,
      "times": [
        0.7290722361,
        0.6731732211,
        0.6565337391,
        0.6481745401000001,
        0.6441295181000001,
        0.6436425141000001,
        0.6575109231,
        0.6399807751000001,
        0.6548583011000001,
        0.9434773991000001
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.7011619382000001,
      "stddev": 0.09970851184965053,
      "median": 0.6602271921,
      "user": 0.36648610000000004,
      "system": 1.33243652,
      "min": 0.6531641741,
      "max": 0.9698415081,
      "times": [
        0.7611105821,
        0.6595958541000001,
        0.6531641741,
        0.6577103551000001,
        0.6555222081000001,
        0.6692738191000001,
        0.6587508481000001,
        0.6608585301000001,
        0.6657915031,
        0.9698415081
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7401420407000001,
      "stddev": 0.054538695968646175,
      "median": 0.7339349991,
      "user": 0.3681476,
      "system": 1.3292796199999999,
      "min": 0.6668846711,
      "max": 0.8427746941,
      "times": [
        0.7806623991,
        0.7169659541000001,
        0.6668846711,
        0.6844087951000001,
        0.7088693261000001,
        0.7660590131,
        0.7509040441,
        0.6982828661,
        0.7856086441000001,
        0.8427746941
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.186 ± 0.032 2.158 2.261 1.01 ± 0.02
pacquet@main 2.178 ± 0.016 2.152 2.208 1.01 ± 0.01
pnpr@HEAD 2.162 ± 0.020 2.132 2.190 1.00
pnpr@main 2.165 ± 0.021 2.131 2.190 1.00 ± 0.01
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.1855193533399997,
      "stddev": 0.0318556954554585,
      "median": 2.1767711825399996,
      "user": 3.45891652,
      "system": 3.03561974,
      "min": 2.15799538854,
      "max": 2.26139652954,
      "times": [
        2.15799538854,
        2.17712823854,
        2.16491595354,
        2.16062259054,
        2.22043938654,
        2.1731302315399996,
        2.1764141265399997,
        2.17870480154,
        2.18444628654,
        2.26139652954
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.17818929254,
      "stddev": 0.016071231767408545,
      "median": 2.17996101854,
      "user": 3.47791682,
      "system": 3.03208334,
      "min": 2.1515325285399998,
      "max": 2.2082254945399997,
      "times": [
        2.18456788054,
        2.1515325285399998,
        2.1814369975399996,
        2.17848503954,
        2.1684849055399997,
        2.1763658745399996,
        2.19037291054,
        2.1586182095399997,
        2.18380308454,
        2.2082254945399997
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1619777562399998,
      "stddev": 0.020047158612001046,
      "median": 2.1653248105399996,
      "user": 3.44574582,
      "system": 3.02637504,
      "min": 2.1322400875399996,
      "max": 2.1900016345399997,
      "times": [
        2.18129175654,
        2.1900016345399997,
        2.16085043854,
        2.1322400875399996,
        2.1759943275399998,
        2.1608204925399996,
        2.13589639454,
        2.16979918254,
        2.17336078154,
        2.13952246654
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1654561267399997,
      "stddev": 0.02128877021871887,
      "median": 2.16710229054,
      "user": 3.45134422,
      "system": 3.0440378399999997,
      "min": 2.1305836345399998,
      "max": 2.18997880454,
      "times": [
        2.15679469654,
        2.1305836345399998,
        2.17988656654,
        2.1327576165399997,
        2.16042184354,
        2.1608505715399997,
        2.18936796554,
        2.18997880454,
        2.1733540095399997,
        2.1805655585399997
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.321 ± 0.019 1.294 1.350 1.00
pacquet@main 1.373 ± 0.043 1.329 1.481 1.04 ± 0.04
pnpr@HEAD 1.322 ± 0.049 1.285 1.454 1.00 ± 0.04
pnpr@main 1.330 ± 0.055 1.285 1.469 1.01 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.32088156424,
      "stddev": 0.018766479049382223,
      "median": 1.3171620762399998,
      "user": 1.4690491399999999,
      "system": 1.6891475799999998,
      "min": 1.29418864774,
      "max": 1.35049332074,
      "times": [
        1.3050280557399998,
        1.30794161874,
        1.32130472574,
        1.34993834774,
        1.35049332074,
        1.30953043074,
        1.33019247174,
        1.32717859674,
        1.31301942674,
        1.29418864774
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.3730502216400002,
      "stddev": 0.04291819858732526,
      "median": 1.36851893324,
      "user": 1.4437517399999997,
      "system": 1.7672260799999997,
      "min": 1.32903401474,
      "max": 1.4814240937399998,
      "times": [
        1.32903401474,
        1.34420649174,
        1.3441823957399999,
        1.34611279774,
        1.4814240937399998,
        1.37567969474,
        1.37823085974,
        1.38971156774,
        1.3805621287399998,
        1.36135817174
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 1.32163373774,
      "stddev": 0.048633007696523456,
      "median": 1.31030552524,
      "user": 1.4329523399999997,
      "system": 1.7003504799999998,
      "min": 1.28486711474,
      "max": 1.4541960567399999,
      "times": [
        1.32355631974,
        1.3046733907399999,
        1.3257471807399999,
        1.4541960567399999,
        1.3159376597399999,
        1.3174436597399999,
        1.30111375674,
        1.30145091174,
        1.28486711474,
        1.2873513267399999
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 1.3296497300399999,
      "stddev": 0.05508079165859697,
      "median": 1.30724301574,
      "user": 1.4256969399999997,
      "system": 1.7202080800000001,
      "min": 1.28473345474,
      "max": 1.46850174774,
      "times": [
        1.3507080887399998,
        1.30816356574,
        1.34955937374,
        1.46850174774,
        1.29221042174,
        1.29016989474,
        1.34667477574,
        1.3063224657399999,
        1.28473345474,
        1.29945351174
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12198
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.81 s
(+66.03%)Baseline: 2.89 s
3.47 s
(138.36%)

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
2,185.52 ms
(-4.48%)Baseline: 2,287.98 ms
2,745.58 ms
(79.60%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,320.88 ms
(-7.95%)Baseline: 1,434.92 ms
1,721.90 ms
(76.71%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
🚨 view alert (🔔)
4,805.65 ms
(+66.03%)Baseline: 2,894.46 ms
3,473.35 ms
(138.36%)

isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
670.42 ms
(-0.27%)Baseline: 672.25 ms
806.69 ms
(83.11%)
🐰 View full continuous benchmarking report in Bencher

@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 (1)
pnpr/crates/pnpr/src/server.rs (1)

568-573: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't proxy past authoritative tarball read failures.

If storage.open_tarball() fails because the hosted backend is unavailable, this branch logs and then continues into the upstream fetch path. For a package/version published here that also exists upstream, clients can get the upstream tarball instead of a 5xx, which breaks the hosted-store source-of-truth guarantee and can silently swap bytes/checksums. Please make hosted-store read errors terminal and only fall through on a definite miss; if cached read errors should stay soft, Storage::open_tarball needs to distinguish hosted errors from cache errors.

🤖 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 568 - 573, The match on
state.inner.storage.open_tarball(&name, filename) currently logs Err(err) and
then falls through to the upstream fetch; instead make storage read errors
terminal: in the Err branch (inside the request handler where open_tarball is
matched) log the error and immediately return an appropriate 5xx tarball error
response (e.g., internal server error / tarball error response) rather than
continuing to attempt upstream fetch or returning a success; if you prefer
softer behavior for cache-only errors, then modify Storage::open_tarball to
return a distinct error variant for cache misses vs hosted-store failures and
handle them differently here (only fall through on the definite None miss, not
on Err).
🧹 Nitpick comments (2)
pnpr/crates/pnpr/tests/s3_backend.rs (1)

33-73: ⚡ Quick win

Add one S3-backed /-/v1/search assertion to this E2E.

This test proves publish/packument/tarball wiring, but the new cross-file contract here is also Storage::hosted_package_names()run_local_search() → the search handler. Exercising that route in the same object-store setup would catch a bucket-backed listing regression that this test currently misses.

Based on learnings: "Port relevant pnpm tests to Rust tests whenever they translate when porting behavior from pnpm."

🤖 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/tests/s3_backend.rs` around lines 33 - 73, The test
publishes_to_and_serves_from_the_object_store misses exercising the
hosted-package listing path (Storage::hosted_package_names() →
run_local_search() → search handler). After publishing and before the tarball
GET, add a GET request to the search endpoint ("/-/v1/search" or
"/-/v1/search?text=mypkg" as your handler expects) against the same app instance
and assert the response is StatusCode::OK and that the returned JSON includes
"mypkg" (or the expected packument/entry indicating hosted packages). Use the
same app/router and object-store setup and include Authorization if the search
handler requires it so this E2E covers bucket-backed listing.
pnpr/crates/pnpr/src/s3/tests.rs (1)

101-113: ⚡ Quick win

Add a regression test for sibling-prefix collisions in package-name listing.

Please add a case where the bucket contains keys under a similar-but-different prefix (for example, configured packages/ plus stray packages2/...) and assert list_package_names excludes those entries.

🤖 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/s3/tests.rs` around lines 101 - 113, Update the test
lists_hosted_package_names to add keys under a similar-but-different prefix
(e.g., create objects under "packages2/" or use store_with_prefix that simulates
prefix "packages2") in addition to the normal "packages/" keys, using the same
helpers (upload, write_packument) so the bucket contains stray entries; then
call store.list_package_names() and assert the result does not include those
stray "packages2/..." entries (i.e., only contains "`@scope/thing`" and
"is-positive"), ensuring sibling-prefix collisions are excluded by
list_package_names.
🤖 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/crates/pnpr/src/s3.rs`:
- Around line 215-223: The listing can return keys outside self.prefix because
you fallback to the full key with unwrap_or(key); in the method that builds the
listing (calling self.store.list(scope.as_ref()) and iterating
listing.next().await) ensure you only accept keys that actually start with the
configured prefix: either construct the scope with a trailing '/' via
ObjectPath::from(self.prefix.trim_end_matches('/').to_string()) + "/" or, more
simply, after getting key = meta.location.as_ref() call
key.strip_prefix(self.prefix.as_str()) and if that returns None continue the
loop (skip non-matching keys) instead of using unwrap_or(key); then apply
rest.strip_suffix(&format!("/{PACKUMENT_FILE}")) to extract the package name.

In `@pnpr/crates/pnpr/src/storage.rs`:
- Around line 462-470: The search-walk currently masks I/O errors by using
fs::try_exists(...).await.unwrap_or(false) and if let Ok(mut inner) =
fs::read_dir(...).await which treats real errors as "package missing"; change
these to propagate errors instead: replace unwrap_or(false) with using ? on
try_exists (or call fs::metadata/try_exists and ?-propagate) so failures return
Err from the containing function, and replace the if let Ok(mut inner) =
fs::read_dir(...).await pattern with let mut inner = fs::read_dir(...).await?
(or handle Err by returning it) and bubble any errors from
inner.next_entry().await?; ensure PACKUMENT_FILE checks and the loop return
errors instead of silently continuing so the caller sees permission/IO failures.

---

Outside diff comments:
In `@pnpr/crates/pnpr/src/server.rs`:
- Around line 568-573: The match on state.inner.storage.open_tarball(&name,
filename) currently logs Err(err) and then falls through to the upstream fetch;
instead make storage read errors terminal: in the Err branch (inside the request
handler where open_tarball is matched) log the error and immediately return an
appropriate 5xx tarball error response (e.g., internal server error / tarball
error response) rather than continuing to attempt upstream fetch or returning a
success; if you prefer softer behavior for cache-only errors, then modify
Storage::open_tarball to return a distinct error variant for cache misses vs
hosted-store failures and handle them differently here (only fall through on the
definite None miss, not on Err).

---

Nitpick comments:
In `@pnpr/crates/pnpr/src/s3/tests.rs`:
- Around line 101-113: Update the test lists_hosted_package_names to add keys
under a similar-but-different prefix (e.g., create objects under "packages2/" or
use store_with_prefix that simulates prefix "packages2") in addition to the
normal "packages/" keys, using the same helpers (upload, write_packument) so the
bucket contains stray entries; then call store.list_package_names() and assert
the result does not include those stray "packages2/..." entries (i.e., only
contains "`@scope/thing`" and "is-positive"), ensuring sibling-prefix collisions
are excluded by list_package_names.

In `@pnpr/crates/pnpr/tests/s3_backend.rs`:
- Around line 33-73: The test publishes_to_and_serves_from_the_object_store
misses exercising the hosted-package listing path
(Storage::hosted_package_names() → run_local_search() → search handler). After
publishing and before the tarball GET, add a GET request to the search endpoint
("/-/v1/search" or "/-/v1/search?text=mypkg" as your handler expects) against
the same app instance and assert the response is StatusCode::OK and that the
returned JSON includes "mypkg" (or the expected packument/entry indicating
hosted packages). Use the same app/router and object-store setup and include
Authorization if the search handler requires it so this E2E covers bucket-backed
listing.
🪄 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: c5a5f0a8-bb9e-4d96-a9f8-495b71db99b6

📥 Commits

Reviewing files that changed from the base of the PR and between e7e99f0 and 638363c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml
  • cspell.json
  • pnpr/crates/pnpr/Cargo.toml
  • pnpr/crates/pnpr/config.yaml
  • pnpr/crates/pnpr/src/config.rs
  • pnpr/crates/pnpr/src/error.rs
  • pnpr/crates/pnpr/src/lib.rs
  • pnpr/crates/pnpr/src/s3.rs
  • pnpr/crates/pnpr/src/s3/tests.rs
  • pnpr/crates/pnpr/src/search.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/storage.rs
  • pnpr/crates/pnpr/tests/s3_backend.rs
  • pnpr/npm/pnpr/README.md
📜 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). (2)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (2)
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/error.rs
  • pnpr/crates/pnpr/src/lib.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/tests.rs
  • pnpr/crates/pnpr/src/s3.rs
  • pnpr/crates/pnpr/src/config.rs
  • pnpr/crates/pnpr/src/search.rs
  • pnpr/crates/pnpr/src/storage.rs
pnpr/**/pnpr/**/Cargo.toml

📄 CodeRabbit inference engine (pnpr/AGENTS.md)

Declare new shared dependencies in the root [workspace.dependencies] and use { workspace = true } in pnpr crate's Cargo.toml

Files:

  • pnpr/crates/pnpr/Cargo.toml
🧠 Learnings (42)
📓 Common learnings
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
Learnt from: CR
Repo: pnpm/pnpm PR: 0
File: pnpr/AGENTS.md:0-0
Timestamp: 2026-05-29T18:03:24.797Z
Learning: Use Conventional Commits with 'pnpr' as the scope in commit messages (e.g., feat(pnpr): ..., fix(pnpr): ...)
📚 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/error.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/error.rs
  • pnpr/crates/pnpr/Cargo.toml
  • pnpr/crates/pnpr/src/lib.rs
  • pnpr/crates/pnpr/src/config.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/**/Cargo.toml : Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it

Applied to files:

  • Cargo.toml
  • pnpr/crates/pnpr/Cargo.toml
📚 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/**/Cargo.toml : Declare new shared dependencies in the root [workspace.dependencies] and use { workspace = true } in pnpr crate's Cargo.toml

Applied to files:

  • Cargo.toml
  • pnpr/crates/pnpr/Cargo.toml
  • pnpr/crates/pnpr/src/lib.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: Do not add a dependency not already declared in `[workspace.dependencies]` without explicit human request; ask for approval and request addition to the workspace root `Cargo.toml`

Applied to files:

  • Cargo.toml
  • pnpr/crates/pnpr/Cargo.toml
📚 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/**/Cargo.toml : Check whether the workspace already depends on something suitable in `[workspace.dependencies]` in the root `Cargo.toml` before adding a new dependency

Applied to files:

  • Cargo.toml
  • pnpr/crates/pnpr/Cargo.toml
📚 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 : Cargo.toml files for new registry-only crates must use the pnpr- prefix for the package name

Applied to files:

  • pnpr/crates/pnpr/Cargo.toml
  • pnpr/crates/pnpr/src/lib.rs
  • pnpr/npm/pnpr/README.md
📚 Learning: 2026-05-19T19:23:00.981Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11752
File: pacquet/crates/config/src/lib.rs:1062-1073
Timestamp: 2026-05-19T19:23:00.981Z
Learning: In `pacquet/crates/config/src/lib.rs`, `modules_dir` does not need a `!virtual_store_dir_explicit` guard on its workspace re-anchor because `modules_dir` is in pnpm's `excludedPnpmKeys` (filtered out by `WorkspaceSettings::clear_workspace_only_fields`) and therefore can only be set by workspace yaml (applied immediately after) or env vars (applied later in the cascade) — not by global `config.yaml`. `virtual_store_dir`, by contrast, IS settable from global config and requires the `if !virtual_store_dir_explicit` guard to survive the workspace-root re-anchor.

Applied to files:

  • pnpr/crates/pnpr/Cargo.toml
  • pnpr/crates/pnpr/src/config.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/Cargo.toml
  • pnpr/crates/pnpr/src/lib.rs
  • pnpr/crates/pnpr/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/tests.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:

  • pnpr/crates/pnpr/Cargo.toml
  • pnpr/crates/pnpr/config.yaml
  • pnpr/npm/pnpr/README.md
  • pnpr/crates/pnpr/src/storage.rs
📚 Learning: 2026-06-04T14:40:25.306Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12189
File: pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs:435-439
Timestamp: 2026-06-04T14:40:25.306Z
Learning: In `pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs` (pnpm/pnpm repo), the pnpr install accelerator always invokes `Install` with `lockfile_only: true` (hard-coded in `pnpr/crates/pnpr/src/install_accelerator/resolve.rs`). Under `lockfile_only: true`:
1. The `PrefetchingResolver` wrapper is skipped — the bare `inner_resolver` is used instead, so `PrefetchContext { config }` is never constructed.
2. The function returns before `CreateVirtualStore` is reached, so `install_package_by_snapshot` and its `config.auth_headers` fetch path are never hit.
pnpr's tarball fetch is handled separately in `resolve::fetch_uncached`, which independently receives the request-scoped `auth_headers`. Therefore, `auth_override` only needs to be threaded into the resolver-side components (NpmResolver, TarballResolver, NamedRegistryResolver) — not into PrefetchingResolver or CreateVirtualStore — on the pnpr path. For local installs (`lockfile_only: false`), `auth_override` is always `None` a...

Applied to files:

  • pnpr/crates/pnpr/config.yaml
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/npm/pnpr/README.md
  • pnpr/crates/pnpr/src/s3.rs
  • pnpr/crates/pnpr/src/storage.rs
📚 Learning: 2026-06-04T14:55:48.516Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12189
File: pacquet/crates/cli/src/cli_args/install.rs:0-0
Timestamp: 2026-06-04T14:55:48.516Z
Learning: In `pacquet/crates/cli/src/cli_args/install.rs` (pnpm/pnpm repo), the `install_via_pnpr` function intentionally forwards the **full** `state.config.auth_headers` map to the pnpr server (not filtered to only the declared default/named registries). This is required for correctness: transitive dependencies can be scope-routed to registries not in the explicit registry list, or pinned to tarball URLs on hosts present in `.npmrc` but not a declared registry. Filtering to the declared registries silently drops tokens those sub-dependencies need, causing 401s on the server. pnpr uses the forwarded map to attach the right token per fetched URL exactly as a local install does (`AuthHeaders::for_url`). The pnpr-server's own credential is sent separately in the `Authorization` header and is excluded from the body map. Do NOT flag this as a credential-leakage issue — the rationale is documented in a comment at both call sites.

Applied to files:

  • pnpr/crates/pnpr/config.yaml
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/npm/pnpr/README.md
📚 Learning: 2026-05-24T08:18:06.019Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11895
File: pnpm/test/deploy.ts:91-95
Timestamp: 2026-05-24T08:18:06.019Z
Learning: In the pnpm/pnpm repository, integration tests that hit the real `registry.npmjs.org` (e.g., for `pacquet` or `pnpm/pacquet`) do NOT use a runtime env-var gate (such as `PNPM_RUN_PUBLIC_REGISTRY_TESTS`). They simply pass `--config.registry=https://registry.npmjs.org/` directly to `execPnpm` and set a higher timeout. This is the established pattern, as seen in `pnpm/test/install/pacquet.ts` and `pnpm/test/deploy.ts`. Do not suggest adding env-var guards for these tests.

Applied to files:

  • pnpr/crates/pnpr/config.yaml
📚 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 : External-crate preludes such as `use rayon::prelude::*;` and root-of-module re-exports such as `pub use submodule::*;` in a `lib.rs` are allowed exceptions to the no-star-imports rule

Applied to files:

  • pnpr/crates/pnpr/src/lib.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 : Do not use star imports inside module bodies — write `use super::{Foo, bar}` instead of `use super::*;` for modules you control

Applied to files:

  • pnpr/crates/pnpr/src/lib.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/lib.rs
  • pnpr/crates/pnpr/src/config.rs
  • pnpr/crates/pnpr/src/storage.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: 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:

  • pnpr/npm/pnpr/README.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: 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:

  • pnpr/npm/pnpr/README.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:

  • pnpr/npm/pnpr/README.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 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:

  • pnpr/npm/pnpr/README.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: 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/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/tests.rs
  • pnpr/crates/pnpr/src/search.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/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/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 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/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/tests.rs
  • pnpr/crates/pnpr/src/search.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/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/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/**/*.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/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/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/tests/s3_backend.rs
  • pnpr/crates/pnpr/src/s3/tests.rs
  • pnpr/crates/pnpr/src/search.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/tests/s3_backend.rs
📚 Learning: 2026-05-28T16:19:30.483Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12025
File: pacquet/crates/deps-path/src/link_path_to_peer_version.rs:0-0
Timestamp: 2026-05-28T16:19:30.483Z
Learning: In `pacquet/crates/deps-path/src/link_path_to_peer_version.rs`, the `link_path_to_peer_version` function intentionally deviates from upstream pnpm/JS behavior for non-BMP Unicode code points: the JavaScript `filenamify` v4 implementation sees UTF-16 code units and emits two surrogate fragments for a single non-BMP scalar, while the Rust port iterates `chars()` and emits one clean Unicode scalar. pnpm has no tests for non-ASCII link paths, so the behavior was undefined upstream; the Rust scalar form is the intentional, preferred behavior for pacquet.

Applied to files:

  • pnpr/crates/pnpr/tests/s3_backend.rs
📚 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/tests/s3_backend.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/s3/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/**/*.rs : Follow Rust API Guidelines for naming

Applied to files:

  • pnpr/crates/pnpr/src/s3/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: Consult `plans/TEST_PORTING.md` before adding ported tests and update its checkboxes as items land; use `known_failures` modules and `pacquet_testing_utils::allow_known_failure!` at the not-yet-implemented boundary

Applied to files:

  • pnpr/crates/pnpr/src/s3/tests.rs
📚 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:

  • pnpr/crates/pnpr/src/s3/tests.rs
  • pnpr/crates/pnpr/src/search.rs
📚 Learning: 2026-05-20T13:36:20.653Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11768
File: pacquet/crates/crypto-hash/src/lib.rs:63-69
Timestamp: 2026-05-20T13:36:20.653Z
Learning: In `pacquet/crates/crypto-hash/src/lib.rs`, `shorten_virtual_store_name` intentionally produces a 33-byte `"_<32-hex>"` result even when `max_length < 33`. This mirrors pnpm's upstream `depPathToFilename` (deps/path/src/index.ts), where JavaScript's `String.prototype.substring(0, negative)` clamps to 0, giving an empty prefix and the full `"_<hash>"` suffix. Do not suggest capping to `max_length` for small values — it would diverge from the on-disk naming contract. The upstream test suite only exercises `max_length = 120`; values below ~50 are self-defeating in practice.

Applied to files:

  • pnpr/crates/pnpr/src/s3/tests.rs
📚 Learning: 2026-05-20T10:51:16.296Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11760
File: pacquet/crates/resolving-npm-resolver/src/named_registry.rs:109-127
Timestamp: 2026-05-20T10:51:16.296Z
Learning: In `pacquet/crates/resolving-npm-resolver/src/named_registry.rs`, the `pick_registry_for_package` function's logic `scope_from_bare_specifier(bare_specifier).or_else(|| scope_of(pkg_name))` is intentionally correct and matches upstream pnpm's `getScope` in `config/pick-registry-for-package/src/index.ts`. When bare_specifier is an npm: alias for an unscoped package (e.g., `npm:lodash@^1`), `scope_from_bare_specifier` returns `None`, and the fallthrough to `scope_of(pkg_name)` is correct behavior — allowing a scoped local name like `private/foo` to still route through the `private` registry. Do NOT suggest changing this to an early-return on `npm:` prefix, as that would break pnpm compatibility.

Applied to files:

  • pnpr/crates/pnpr/src/s3/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/**/*.rs : Search for existing shared helpers before writing new code; shared helpers tend to live in `crates/fs`, `crates/testing-utils`, and `crates/diagnostics`

Applied to files:

  • pnpr/crates/pnpr/src/config.rs
  • pnpr/crates/pnpr/src/search.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 : Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (`&Path` over `&PathBuf`, `&str` over `&String`) when it doesn't force extra copies

Applied to files:

  • pnpr/crates/pnpr/src/search.rs
📚 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/crates/pnpr/src/search.rs
📚 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:

  • pnpr/crates/pnpr/src/search.rs
  • pnpr/crates/pnpr/src/storage.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/search.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/search.rs
🪛 LanguageTool
pnpr/npm/pnpr/README.md

[grammar] ~167-~167: Ensure spelling is correct
Context: ...rites the dist.tarball URLs in served packuments, so clients fetch tarballs back through...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (8)
cspell.json (1)

21-21: LGTM!

Also applies to: 179-179

pnpr/npm/pnpr/README.md (1)

44-44: LGTM!

Also applies to: 81-183

Cargo.toml (1)

112-112: LGTM!

pnpr/crates/pnpr/Cargo.toml (1)

48-48: LGTM!

pnpr/crates/pnpr/src/lib.rs (1)

17-17: LGTM!

Also applies to: 26-28

pnpr/crates/pnpr/src/error.rs (1)

132-135: LGTM!

Also applies to: 183-185

pnpr/crates/pnpr/src/config.rs (1)

4-4: LGTM!

Also applies to: 7-7, 13-13, 105-122, 320-324, 423-423, 442-442, 553-558, 578-578

pnpr/crates/pnpr/config.yaml (1)

10-25: LGTM!

Comment thread pnpr/crates/pnpr/src/s3.rs
Comment thread pnpr/crates/pnpr/src/storage.rs
zkochan added 2 commits June 4, 2026 22:14
Addresses CodeRabbit review on #12198: the previous unwrap_or(key)
fallback could synthesize a wrong package name for a key outside the
configured prefix. Also documents the deliberate best-effort tolerance
of per-entry I/O errors in the local search walk.

@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/tests/s3_backend.rs (1)

136-144: ⚡ Quick win

Append trailing slash to prefix for precise S3 scoping.

The current implementation uses the raw prefix string (e.g., "mypkg") which will match any key starting with those characters, including "mypkg", "mypkg/", and "mypkg-dev/". To scope the listing to only objects under a package directory, append a trailing slash if not already present.

♻️ Proposed fix
 async fn bucket_keys(store: &Arc<dyn ObjectStore>, prefix: &str) -> Vec<String> {
-    let scope = ObjectPath::from(prefix);
+    let normalized = if prefix.is_empty() || prefix.ends_with('/') {
+        prefix.to_string()
+    } else {
+        format!("{}/", prefix)
+    };
+    let scope = ObjectPath::from(normalized.as_str());
     store
         .list(Some(&scope))
🤖 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/tests/s3_backend.rs` around lines 136 - 144, bucket_keys
currently uses the raw prefix string which can overmatch (e.g., "mypkg" matches
"mypkg-dev"); modify bucket_keys so it ensures the prefix ends with a trailing
slash before constructing the ObjectPath (i.e., if prefix does not end_with('/')
append '/' to it) so ObjectPath::from receives a directory-scoped prefix; update
references inside bucket_keys (the prefix variable passed to ObjectPath::from)
only—no other logic changes.
🤖 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/tests/s3_backend.rs`:
- Around line 136-144: bucket_keys currently uses the raw prefix string which
can overmatch (e.g., "mypkg" matches "mypkg-dev"); modify bucket_keys so it
ensures the prefix ends with a trailing slash before constructing the ObjectPath
(i.e., if prefix does not end_with('/') append '/' to it) so ObjectPath::from
receives a directory-scoped prefix; update references inside bucket_keys (the
prefix variable passed to ObjectPath::from) only—no other logic changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2a2cf810-79b7-4dbd-8a6a-bb449aace04d

📥 Commits

Reviewing files that changed from the base of the PR and between 638363c and c532f20.

📒 Files selected for processing (3)
  • pnpr/crates/pnpr/src/config/tests.rs
  • pnpr/crates/pnpr/src/error/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.rs
📜 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). (7)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
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/error/tests.rs
  • pnpr/crates/pnpr/src/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.rs
🧠 Learnings (14)
📓 Common learnings
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-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/error/tests.rs
  • pnpr/crates/pnpr/src/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.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/error/tests.rs
  • pnpr/crates/pnpr/src/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.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/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.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 : Do not use star imports inside module bodies — write `use super::{Foo, bar}` instead of `use super::*;` for modules you control

Applied to files:

  • pnpr/crates/pnpr/src/config/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 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/config/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/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.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/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.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/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.rs
📚 Learning: 2026-05-19T19:23:00.981Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11752
File: pacquet/crates/config/src/lib.rs:1062-1073
Timestamp: 2026-05-19T19:23:00.981Z
Learning: In `pacquet/crates/config/src/lib.rs`, `modules_dir` does not need a `!virtual_store_dir_explicit` guard on its workspace re-anchor because `modules_dir` is in pnpm's `excludedPnpmKeys` (filtered out by `WorkspaceSettings::clear_workspace_only_fields`) and therefore can only be set by workspace yaml (applied immediately after) or env vars (applied later in the cascade) — not by global `config.yaml`. `virtual_store_dir`, by contrast, IS settable from global config and requires the `if !virtual_store_dir_explicit` guard to survive the workspace-root re-anchor.

Applied to files:

  • pnpr/crates/pnpr/src/config/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/config/tests.rs
📚 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/config/tests.rs
  • pnpr/crates/pnpr/tests/s3_backend.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/tests/s3_backend.rs
📚 Learning: 2026-06-04T14:40:25.306Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12189
File: pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs:435-439
Timestamp: 2026-06-04T14:40:25.306Z
Learning: In `pacquet/crates/package-manager/src/install_with_fresh_lockfile.rs` (pnpm/pnpm repo), the pnpr install accelerator always invokes `Install` with `lockfile_only: true` (hard-coded in `pnpr/crates/pnpr/src/install_accelerator/resolve.rs`). Under `lockfile_only: true`:
1. The `PrefetchingResolver` wrapper is skipped — the bare `inner_resolver` is used instead, so `PrefetchContext { config }` is never constructed.
2. The function returns before `CreateVirtualStore` is reached, so `install_package_by_snapshot` and its `config.auth_headers` fetch path are never hit.
pnpr's tarball fetch is handled separately in `resolve::fetch_uncached`, which independently receives the request-scoped `auth_headers`. Therefore, `auth_override` only needs to be threaded into the resolver-side components (NpmResolver, TarballResolver, NamedRegistryResolver) — not into PrefetchingResolver or CreateVirtualStore — on the pnpr path. For local installs (`lockfile_only: false`), `auth_override` is always `None` a...

Applied to files:

  • pnpr/crates/pnpr/tests/s3_backend.rs
🔇 Additional comments (13)
pnpr/crates/pnpr/src/error/tests.rs (1)

35-42: LGTM!

pnpr/crates/pnpr/src/config/tests.rs (4)

2-3: LGTM!


132-137: LGTM!


139-158: LGTM!


160-164: LGTM!

pnpr/crates/pnpr/tests/s3_backend.rs (8)

6-22: LGTM!


26-32: LGTM!


34-74: LGTM!


76-103: LGTM!


105-134: LGTM!


146-152: LGTM!


154-185: LGTM!


187-230: LGTM!

@zkochan zkochan merged commit 8e5e764 into main Jun 4, 2026
24 of 25 checks passed
@zkochan zkochan deleted the pnpr-s3-hosted-store branch June 4, 2026 20:44
zkochan added a commit that referenced this pull request Jun 5, 2026
…hase 3) (#12206)

## What

Implements the **auth half** of [#12199](#12199) (phase 3) — making pnpr's remaining per-instance state pluggable so the registry can run as stateless, horizontally-scaled replicas.

### Auth records behind config-selected backends
Users + tokens now sit behind narrow async `UserBackend` / `TokenBackend` traits, built once at startup into `Arc<dyn …>` handles (the same build-once pattern #12198 used for the hosted store). Three implementations:

- **Local** (default) — today's htpasswd file + SQLite token DB, or in-memory when no file is configured. Unchanged behavior.
- **Networked SQLite (libsql / Turso)** — `LibsqlAuth` stores **both** records in one shared database, so several stateless replicas observe a consistent set of users and tokens. The `tokens` table DDL is shared verbatim with the local backend (a DB can migrate between them); users — which the local backend keeps in htpasswd — move into a `users` table.

Selected via a new top-level YAML block:

```yaml
backend:
  libsql:
    url: ${PNPR_LIBSQL_URL}
    authToken: ${PNPR_LIBSQL_TOKEN}
    # optional embedded replica for local-fast hot-path reads:
    replicaPath: ./auth-replica.db
    syncIntervalSecs: 60
```

When the block is absent, auth stays on local disk exactly as before.

### Embedded-replica read acceleration
Token lookups are on the request hot path, so against a remote primary every read would be a network round-trip. With `replicaPath` set, `LibsqlAuth` builds a libsql **embedded replica**: reads hit a local file that libsql keeps current in the background; writes go to the primary. `syncIntervalSecs` is the freshness knob that bounds token-revocation lag.

### Async access path
`identify` / `enforce_access` are now async (a networked lookup is async). `enforce_access` is split into an async `resolve_identity` + a sync `authorize`, so the search endpoint resolves the caller once and authorizes each candidate synchronously (no async-in-`retain`).

### Concurrent-publish guard (cross-cutting follow-up from the issue)
Closes the same-instance lost-update window in the three read-modify-write packument flows (publish, dist-tag change, partial-unpublish): a striped per-package lock serializes same-package writers on one instance while letting different packages proceed in parallel. The **cross-replica** half (S3 `If-Match` / ETag CAS) is documented in-code as the remaining piece — the issue files it under "fix when we get there," and it belongs with the multi-writer S3 publish work, not this auth branch.

## Tests
All green — `cargo test -p pnpr`:
- **176 lib unit tests** incl. new `LibsqlAuth` tests (run against an in-memory libsql DB — same driver + SQL, no server) and `backend.libsql` config-parsing tests (incl. the replica options).
- New `concurrent_publishes_of_distinct_versions_all_survive` integration test for the publish guard.
- Existing auth_persistence / auth_user_endpoints / auth_publish / server / s3_backend suites pass.
- Clean under `cargo fmt`, `clippy`, `RUSTDOCFLAGS=-D warnings cargo doc`, **Dylint perfectionist**, and `taplo`.

## Docs
`backend.libsql` (incl. embedded replica) documented in the bundled `config.yaml` and the `pnpr` npm README, mirroring how the S3 backend was documented in #12198.
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