Skip to content

feat(registry): add auth/dist-tag/publish endpoints + wire TS tests onto pnpm-registry#11914

Merged
zkochan merged 20 commits into
mainfrom
pnpm-registry-auth
May 25, 2026
Merged

feat(registry): add auth/dist-tag/publish endpoints + wire TS tests onto pnpm-registry#11914
zkochan merged 20 commits into
mainfrom
pnpm-registry-auth

Conversation

@zkochan

@zkochan zkochan commented May 24, 2026

Copy link
Copy Markdown
Member

Summary

Lands the pieces of the npm registry protocol that pnpm-registry was missing, and switches the TypeScript test harness off verdaccio onto pnpm-registry. @pnpm/registry-mock (the npm package) is untouched.

Server-side additions (registry/crates/pnpm-registry)

  • PUT /-/user/org.couchdb.user:<name> — adduser / login, returns a Bearer token. In-memory user + token stores.
  • PUT /:pkg — publish (scoped + unscoped). Base64-decodes _attachments, merges into the existing packument, writes manifest + tarball atomically. 100 MiB body limit.
  • GET /-/package/:pkg/dist-tags + PUT/DELETE /-/package/:pkg/dist-tags/:tag — rewrites the on-disk packument so tag changes survive a restart.
  • Authorization: Bearer and Authorization: Basic both identify the caller.
  • Per-package access policy (wax glob patterns). Defaults mirror @pnpm/registry-mock's config.yaml: @private/* and @pnpm.e2e/needs-auth require auth; everything else is anonymous read, authenticated write. Enforced on every packument / version-manifest / tarball GET and every write endpoint.

TypeScript-test migration

  • __utils__/jest-config/with-registry/globalSetup.js keeps prepare() from @pnpm/registry-mock (still needed for the tempy storage path written into the runtime-config yaml — getIntegrity reads it from there) but spawns pnpm-registry instead of verdaccio. addUser, addDistTag, getIntegrity, REGISTRY_MOCK_* from registry-mock work as-is — they're plain npm-wire-protocol HTTP calls.
  • Binary lookup follows pacquet's pattern: PNPM_REGISTRY_BIN env override, then target/release/pnpm-registry, then target/debug/pnpm-registry.
  • CI test job (.github/workflows/test.yml) installs the Rust toolchain via the existing ./.github/actions/rustup composite action and builds pnpm-registry --release before tests run. Per-platform — Linux and Windows in the matrix each build their own.

Test plan

  • cargo test -p pnpm-registry — 65 tests pass (12 new in tests/auth_publish.rs covering adduser, Bearer/Basic auth on protected packages, anonymous publish rejection, publish round-trip for scoped + unscoped, dist-tag add/remove, tarball-name validation).
  • cargo clippy -p pnpm-registry --all-targets -- --deny warnings
  • cargo fmt --check
  • cargo check --workspace --all-targets
  • Local TS tests against pnpm-registry, no test changes needed:
    • installing/deps-installer/test/install/auth.ts — 5 passed (2 skipOnNode17)
    • installing/deps-installer/test/cache.ts — 2 passed (uses addDistTag)
    • installing/deps-installer/test/install/aliases.ts + install/peerDependencies.ts — 71 passed
    • installing/deps-installer/test/brokenLockfileIntegrity.ts + install/local.ts — 19 passed
  • Full CI matrix (ubuntu + windows × Node 22/24/26)

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

Summary by CodeRabbit

  • New Features

    • Registry: in-memory auth (add/login, tokens), per-package access/publish policies, authenticated publish/unpublish, dist-tag CRUD, scoped tarball handling, and a local search endpoint.
  • Tests

    • New integration suite covering auth, publish, attachments, dist-tags, search, scoped packages, unpublish, and access-policy scenarios.
  • Chores

    • Test infra updates, added jest-config runtime dependency, new CI script, and workflow branch handling for registry-only test runs.
  • Bug Fixes

    • Stricter validation and clearer error responses for publish/unpublish, attachments, and auth.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds in-memory auth/token issuance, per-package policy rules, publish/attachment handling and canonicalization, local search, router-enforced access control, CI/Jest wiring to spawn pnpm-registry, and comprehensive integration tests for auth, publish, dist-tags, search, and unpublish flows.

Changes

Registry Auth & Publishing Feature

Layer / File(s) Summary
Workflow & Jest Global Setup
.github/workflows/test.yml, .github/actions/rustup/action.yml, __utils__/jest-config/package.json, __utils__/jest-config/with-registry/globalSetup.js, package.json
CI test-scope adds a pnpm-registry-auth special-case that runs ci:test-registry-access-only; composite action inputs drop explicit type annotations; Jest global setup now spawns the pnpm-registry binary (resolving via env/Cargo targets), reads runtime-config for storage, waits for readiness, and registers a mock user/token; adds a registry-only CI script.
Auth & Policy Modules
registry/crates/pnpm-registry/Cargo.toml, registry/crates/pnpm-registry/src/auth.rs, registry/crates/pnpm-registry/src/policy.rs, registry/crates/pnpm-registry/src/lib.rs
Adds UserStore/TokenStore with constant-time checks and token issuance; identify() parses Bearer/Basic headers; AccessRule/PackagePolicy/PackagePolicies implement globbed access/publish rules; adds base64 and sha2 deps and re-exports policy types.
Config, Errors & Re-exports
registry/crates/pnpm-registry/src/config.rs, registry/crates/pnpm-registry/src/error.rs, registry/crates/pnpm-registry/src/lib.rs
Config gains a public policies: PackagePolicies field initialized to registry-mock defaults; RegistryError adds validation/auth/attachment/write variants mapped to 400/401/403; policy types are re-exported from crate root.
Publish Data Handling
registry/crates/pnpm-registry/src/package_name.rs, registry/crates/pnpm-registry/src/publish.rs, registry/crates/pnpm-registry/src/cache.rs
PackageName adds canonicalize_tarball_name; publish helpers extract/validate base64 _attachments, merge incoming manifests (versions/dist-tags/time) with stable ordering and now_iso timestamps; Cache adds remove_package/remove_tarball.
Local Search
registry/crates/pnpm-registry/src/search.rs
Implements local /-/v1/search scanning on-disk packuments, query parsing (text/q fallback and percent-decoding), size clamping, packument traversal/projection to npm search v1 shape, and unit tests.
Server Router & Handlers
registry/crates/pnpm-registry/src/server.rs
App state includes UserStore/TokenStore and applies a 100 MiB publish body limit; GET handlers enforce access; PUT /-/user/org.couchdb.user:{name} issues tokens; publish_package enforces publish access, extracts/canonicalizes/writes attachments, merges and persists packuments; dist-tag, tarball delete, search, and unpublish endpoints enforce access via enforce_access.
Integration Test Suite
registry/crates/pnpm-registry/tests/auth_publish.rs
Adds comprehensive tests for adduser/token behavior, bearer/basic auth, protected fixture access, authenticated publish persistence and tarball serving, dist-tag semantics and auth enforcement, attachment filename validation, scoped publish/unpublish, local search, and unpublish behaviors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • pnpm/pnpm#11898: Extends the pnpm-registry crate implementation introduced in that PR with authentication, authorization policies, and publishing support.

Suggested reviewers

  • KSXGitHub

Poem

🐰 I spawned a registry, bright and spry,
Tokens hummed soft as bytes went by,
Rules and tags in tidy rows,
Tests hop, publish comes and goes,
A rabbit cheers for each deploy!

🚥 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 summarizes the main changes: adding auth/dist-tag/publish endpoints to the registry and migrating TypeScript tests from verdaccio to pnpm-registry.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pnpm-registry-auth

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 May 24, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.7±0.19ms   562.0 KB/sec    1.00      7.7±0.23ms   563.0 KB/sec

@codecov-commenter

codecov-commenter commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.13946% with 163 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.62%. Comparing base (3788a8b) to head (6da5602).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
registry/crates/pnpm-registry/src/server.rs 81.21% 102 Missing ⚠️
registry/crates/pnpm-registry/src/publish.rs 88.26% 23 Missing ⚠️
registry/crates/pnpm-registry/src/search.rs 89.13% 20 Missing ⚠️
registry/crates/pnpm-registry/src/policy.rs 87.32% 9 Missing ⚠️
registry/crates/pnpm-registry/src/auth.rs 96.73% 5 Missing ⚠️
registry/crates/pnpm-registry/src/cache.rs 78.57% 3 Missing ⚠️
registry/crates/pnpm-registry/src/error.rs 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11914      +/-   ##
==========================================
- Coverage   87.66%   87.62%   -0.05%     
==========================================
  Files         215      219       +4     
  Lines       25458    26666    +1208     
==========================================
+ Hits        22319    23365    +1046     
- Misses       3139     3301     +162     

☔ View full report in Codecov by Sentry.
📢 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 changed the title feat(registry): add auth, dist-tag, and publish endpoints feat(registry): add auth/dist-tag/publish endpoints + wire TS tests onto pnpm-registry May 24, 2026
@zkochan zkochan marked this pull request as ready for review May 24, 2026 21:08
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@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: 5

🤖 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 @.github/workflows/test.yml:
- Around line 76-77: The workflow is failing actionlint because the local action
at ./.github/actions/rustup declares an invalid input field (`type` on the
`shared-key` input); open the action metadata (action.yml) inside
./.github/actions/rustup, remove or replace the unsupported `type` key on the
`shared-key` input with supported keys (e.g., `required`, `description`,
`default`) so the inputs section conforms to GitHub Actions schema, run
actionlint locally to confirm no more metadata errors, then re-enable uses:
./.github/actions/rustup in the job.

In `@registry/crates/pnpm-registry/src/auth.rs`:
- Around line 214-218: The test is currently ignoring the boolean result of
matches! when calling store.add_or_login; update the assertions to actually
verify the variant returned by add_or_login by asserting the outcome value —
e.g., after let outcome = store.add_or_login("alice", "secret").unwrap();
assert!(matches!(outcome, UpsertOutcome::Created)); and similarly
assert!(matches!(outcome, UpsertOutcome::LoggedIn)) for the second call (or use
assert_eq!(outcome, UpsertOutcome::Created) / assert_eq!(outcome,
UpsertOutcome::LoggedIn) if the UpsertOutcome implements PartialEq) so the test
fails when the wrong variant is returned.

In `@registry/crates/pnpm-registry/src/server.rs`:
- Around line 629-689: update_dist_tag() currently mutates the "dist-tags"
object but never updates the packument's "time.modified" field, so add logic
after the mutate(tags) call (and before serializing/writing) to ensure
packument_obj.entry("time") is an object (create one if missing) and set its
"modified" property to the current wall-clock RFC3339 timestamp (e.g.
chrono::Utc::now().to_rfc3339() or equivalent). Place this update just before
calling serde_json::to_vec_pretty(&packument) and keep using the same
packument_obj / tags variables; then proceed to write_packument as before.
- Around line 470-477: Validate the package name in the parsed JSON against the
package name from the request URL before merging: after parsing into incoming
and after extract_attachments, read incoming.get("name").and_then(|v|
v.as_str()) and if it is present and != the route/package name variable (the
name you extracted from the URL in this handler), immediately return a 400 via
error_response with an appropriate RegistryError (e.g. BadRequest or a new
variant) so mismatched bodies are rejected; update the handler around the
existing incoming/attachments logic (refer to incoming, extract_attachments,
error_response, and RegistryError) to perform this check prior to any
merge/persist operations.
- Around line 494-503: The current seed-publish path uses
state.inner.cache.read_packument_any_age(&name) and treats a cache miss as None,
causing merges to drop upstream versions; instead, when read_packument_any_age
returns None, fall back to loading the upstream packument via
state.inner.cache.load_packument_bytes(&name).await (mirroring update_dist_tag
behavior) and use that bytes result for existing_bytes before deserializing and
calling merge_manifest; preserve the existing error handling by returning
error_response on Err and only treating actual absence after the fallback as
None.
🪄 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: afc85157-fbfb-4fd8-be36-84b72a82fbd4

📥 Commits

Reviewing files that changed from the base of the PR and between f5d7723 and 07d40d4.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • .github/workflows/test.yml
  • __utils__/jest-config/package.json
  • __utils__/jest-config/with-registry/globalSetup.js
  • registry/crates/pnpm-registry/Cargo.toml
  • registry/crates/pnpm-registry/src/auth.rs
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/error.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/package_name.rs
  • registry/crates/pnpm-registry/src/policy.rs
  • registry/crates/pnpm-registry/src/publish.rs
  • registry/crates/pnpm-registry/src/server.rs
  • registry/crates/pnpm-registry/tests/auth_publish.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: 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: Run benchmark on ubuntu-latest
  • GitHub Check: Code Coverage
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (3)
registry/crates/**/Cargo.toml

📄 CodeRabbit inference engine (registry/AGENTS.md)

registry/crates/**/Cargo.toml: When using existing pacquet-* crates, declare them in the root [workspace.dependencies] and use { workspace = true } in the crate's Cargo.toml
New registry-only crates must be placed under registry/crates// and named pnpm-registry- in Cargo.toml, with the package name using the pnpm-registry- prefix exclusively
Do not reach for the pacquet- prefix to name new registry-side crates; use pnpm-registry- prefix exclusively for registry-only crates

Files:

  • registry/crates/pnpm-registry/Cargo.toml
registry/**/Cargo.toml

📄 CodeRabbit inference engine (registry/AGENTS.md)

Add new registry-only crates to [workspace.dependencies] at the root with the pnpm-registry- prefix so other crates can use { workspace = true }

Files:

  • registry/crates/pnpm-registry/Cargo.toml
registry/crates/**/*.rs

📄 CodeRabbit inference engine (registry/AGENTS.md)

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

Files:

  • registry/crates/pnpm-registry/src/package_name.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/error.rs
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/policy.rs
  • registry/crates/pnpm-registry/tests/auth_publish.rs
  • registry/crates/pnpm-registry/src/publish.rs
  • registry/crates/pnpm-registry/src/auth.rs
  • registry/crates/pnpm-registry/src/server.rs
🧠 Learnings (2)
📚 Learning: 2026-05-05T23:03:04.286Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11479
File: __utils__/scripts/package.json:6-9
Timestamp: 2026-05-05T23:03:04.286Z
Learning: The pattern cross-env NODE_OPTIONS="$NODE_OPTIONS ..." in package.json scripts is an established convention in the pnpm/pnpm repository and is used across many packages (e.g., fs/hard-link-dir, worker, __utils__/scripts). Do not flag this as a cross-platform issue in individual files; if a change is needed, apply it as a repo-wide change in a separate PR. Scope this guidance to all package.json files in the repo; use the minimatch pattern '**/package.json' to identify relevant files and review changes at the repository level rather than per-file.

Applied to files:

  • __utils__/jest-config/package.json
📚 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:

  • __utils__/jest-config/with-registry/globalSetup.js
🪛 actionlint (1.7.12)
.github/workflows/test.yml

[error] 77-77: could not parse action metadata in "/home/jailuser/git/.github/actions/rustup": line 7: unexpected key "type" for definition of input "shared-key"

(action)

🔇 Additional comments (5)
__utils__/jest-config/with-registry/globalSetup.js (1)

125-126: Remove the Node compatibility concern for import.meta.dirname in repoRoot()

import.meta.dirname is supported starting in Node v20.11.0, and CI runs this Jest global setup on Node >= 22.13.0 (.github/workflows/ci.yml matrix: 22.13.0, 24.0.0, 26.0.0). The repo also already uses import.meta.dirname in multiple test/utility modules, so this should not fail due to Node version.

registry/crates/pnpm-registry/src/lib.rs (1)

10-23: LGTM!

registry/crates/pnpm-registry/src/package_name.rs (1)

43-76: LGTM!

registry/crates/pnpm-registry/src/publish.rs (1)

18-307: LGTM!

registry/crates/pnpm-registry/tests/auth_publish.rs (1)

18-507: LGTM!

Comment thread .github/workflows/test.yml
Comment thread registry/crates/pnpm-registry/src/auth.rs Outdated
Comment thread registry/crates/pnpm-registry/src/server.rs
Comment thread registry/crates/pnpm-registry/src/server.rs
Comment thread registry/crates/pnpm-registry/src/server.rs
@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.123 ± 0.171 2.008 2.587 1.04 ± 0.09
pacquet@main 2.042 ± 0.044 1.987 2.095 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.12334959398,
      "stddev": 0.1707497066100276,
      "median": 2.06035299188,
      "user": 2.6964468200000002,
      "system": 3.56394844,
      "min": 2.00818581488,
      "max": 2.58699546588,
      "times": [
        2.16740186488,
        2.14963525088,
        2.04954599388,
        2.05887807588,
        2.08605456088,
        2.00818581488,
        2.04356431588,
        2.58699546588,
        2.02140668888,
        2.0618279078799997
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.04194358768,
      "stddev": 0.04363067770199974,
      "median": 2.03439460188,
      "user": 2.69478522,
      "system": 3.5433217400000006,
      "min": 1.9866255038800003,
      "max": 2.09477420188,
      "times": [
        2.00678770788,
        1.9866255038800003,
        2.01270160688,
        2.0834731018799997,
        2.09477420188,
        2.0907447108799997,
        1.9984458668800003,
        2.05608759688,
        2.00633147488,
        2.08346410488
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 670.5 ± 35.9 649.4 766.9 1.01 ± 0.06
pacquet@main 661.4 ± 9.1 645.7 674.6 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.6704633670400001,
      "stddev": 0.03588926312087732,
      "median": 0.6581770399400001,
      "user": 0.38272796,
      "system": 1.4610349599999999,
      "min": 0.64938496694,
      "max": 0.7668636709400001,
      "times": [
        0.7668636709400001,
        0.6582343599400001,
        0.6904822259400001,
        0.6641707079400001,
        0.6603772869400001,
        0.64938496694,
        0.6581197199400001,
        0.6564052549400001,
        0.6500695569400001,
        0.6505259199400001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.6613919084400001,
      "stddev": 0.009098482491703294,
      "median": 0.66144423894,
      "user": 0.37703076,
      "system": 1.46482176,
      "min": 0.6457125139400001,
      "max": 0.6745947799400001,
      "times": [
        0.6745947799400001,
        0.6457125139400001,
        0.66171644094,
        0.6625178999400001,
        0.66089654894,
        0.6492945499400001,
        0.66117203694,
        0.6710036159400001,
        0.6696515289400001,
        0.6573591689400001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.535 ± 0.030 2.491 2.589 1.00
pacquet@main 2.544 ± 0.057 2.464 2.688 1.00 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.53542636592,
      "stddev": 0.02972648930743821,
      "median": 2.53085352072,
      "user": 4.2959153599999995,
      "system": 3.5324750199999997,
      "min": 2.49101604622,
      "max": 2.58908243322,
      "times": [
        2.51097191522,
        2.51930870722,
        2.49101604622,
        2.53289365622,
        2.58908243322,
        2.57780348922,
        2.54004359122,
        2.5288133852200003,
        2.5188353132200003,
        2.54549512222
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.54388230582,
      "stddev": 0.05682918864767479,
      "median": 2.53158309072,
      "user": 4.3088694599999995,
      "system": 3.53492882,
      "min": 2.46436799022,
      "max": 2.68839165822,
      "times": [
        2.54416559822,
        2.52326735322,
        2.56091783722,
        2.53230103722,
        2.68839165822,
        2.53086514422,
        2.54743260422,
        2.46436799022,
        2.5247496602200004,
        2.52236417522
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.717 ± 0.029 1.666 1.754 1.00 ± 0.02
pacquet@main 1.712 ± 0.014 1.696 1.741 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.7173537799400003,
      "stddev": 0.028724492206035533,
      "median": 1.7165169980400001,
      "user": 2.1152557,
      "system": 2.1578937799999998,
      "min": 1.66638084354,
      "max": 1.75354695054,
      "times": [
        1.6914456235400002,
        1.66638084354,
        1.69989614854,
        1.7520630155399999,
        1.6992469165400002,
        1.73309460954,
        1.75354695054,
        1.74482969554,
        1.71493725654,
        1.7180967395400002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.7119316572400003,
      "stddev": 0.014123148855164804,
      "median": 1.70913904454,
      "user": 2.1041967999999995,
      "system": 2.1670329799999997,
      "min": 1.69630673154,
      "max": 1.7412437795400002,
      "times": [
        1.7412437795400002,
        1.70391944154,
        1.72080669854,
        1.69630673154,
        1.70119826654,
        1.71591824754,
        1.70800479754,
        1.6967311635400002,
        1.7249141545400002,
        1.71027329154
      ]
    }
  ]
}

@github-actions

github-actions Bot commented May 24, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/11914
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
2,535.43 ms
(-44.42%)Baseline: 4,562.07 ms
5,474.48 ms
(46.31%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,717.35 ms
(-51.52%)Baseline: 3,542.47 ms
4,250.97 ms
(40.40%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,123.35 ms
(-8.16%)Baseline: 2,312.10 ms
2,774.52 ms
(76.53%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
670.46 ms
(+1.90%)Baseline: 657.94 ms
789.52 ms
(84.92%)
🐰 View full continuous benchmarking report in Bencher

zkochan added 12 commits May 25, 2026 00:25
Adds the pieces of the npm registry protocol needed for the TypeScript
pnpm test suite to migrate off verdaccio onto pnpm-registry in a
follow-up PR:

* `PUT /-/user/org.couchdb.user:<name>` (adduser / login) returning a
  Bearer token, backed by in-memory user + token stores
* `PUT /:pkg` publish — base64-decodes `_attachments`, merges into the
  existing packument, writes manifest + tarball atomically. 100 MiB
  body limit
* `GET /-/package/:pkg/dist-tags` plus `PUT/DELETE
  /-/package/:pkg/dist-tags/:tag` — rewrites the on-disk packument so
  tag changes survive a restart
* Per-package access policy (wax glob patterns) enforced on every
  packument, version-manifest, and tarball read. Defaults mirror
  `@pnpm/registry-mock`'s `config.yaml`: `@private/*` and
  `@pnpm.e2e/needs-auth` require auth; everything else is anonymous
  read, authenticated write
* `Authorization: Bearer` and `Authorization: Basic` both work for
  identifying the caller

12 new integration tests cover adduser flow + wrong-password
rejection, anonymous vs authenticated access on protected packages,
publish round-trip (scoped + unscoped), dist-tag add/remove via the
spec endpoints, and per-package-name tarball validation on publish.

No changes to `@pnpm/registry-mock` (the npm package) or to the
TypeScript test setup in this PR — those land in a follow-up that
wires the existing `globalSetup.js` over to pnpm-registry, following
the same storage-loading approach pacquet uses today.

---
Written by an agent (Claude Code, claude-opus-4-7).
Switches the TypeScript test harness over to the Rust pnpm-registry
server. `@pnpm/registry-mock` is untouched — its prepare(), addUser,
addDistTag, and getIntegrity helpers all speak the npm wire protocol
that pnpm-registry now implements, so they work transparently against
either backend.

* `__utils__/jest-config/with-registry/globalSetup.js` keeps the
  `prepare()` call (we still need the tempy storage path it writes
  into the runtime-config yaml) but spawns `pnpm-registry` instead
  of verdaccio. Binary location follows the same lookup pacquet uses
  in `pacquet/tasks/registry-mock/src/pnpm_registry_command.rs`:
  `PNPM_REGISTRY_BIN` env var → `target/release/` → `target/debug/`.
* CI test job (`.github/workflows/test.yml`) installs the Rust
  toolchain via the existing `./.github/actions/rustup` composite
  action and builds `pnpm-registry --release` before running tests.
  Each platform builds its own binary (Linux + Windows in the matrix).
* `--packument-ttl-secs 31536000` keeps the fixture packuments
  authoritative across a test run, same as verdaccio's `maxage: 30m`
  fixture-mode behavior.

Verified locally: `test/install/auth.ts` (5 passed), `test/cache.ts`
(2 passed, uses addDistTag), `test/install/aliases.ts` +
`test/install/peerDependencies.ts` (71 passed),
`test/brokenLockfileIntegrity.ts` + `test/install/local.ts` (19
passed) — all green against pnpm-registry with no test changes.

---
Written by an agent (Claude Code, claude-opus-4-7).
Five lint violations surfaced by the Dylint job on CI:

* `policy.rs` — derive list reordered to prefix_then_alphabetical
  (`Debug, Default, Clone`).
* `publish.rs` — rename single-letter `let z` to `epoch_days` and
  drop the no-longer-needed `mut` + silencing hack on `days`.
* `publish.rs` — trailing comma on the multi-line `format!` invocation.
* `server.rs` — generic parameter `F` on update_dist_tag renamed
  to `Mutate`.
* `auth.rs` — trailing comma on the multi-line `format!` invocation
  inside a test.

No behavior change.

---
Written by an agent (Claude Code, claude-opus-4-7).
The previous lint-fix commit missed this assert_eq! in
tests/auth_publish.rs. Verified locally with `cargo dylint --all --
--all-targets --workspace` (clean across the whole workspace).

---
Written by an agent (Claude Code, claude-opus-4-7).
* Drop unused `ssri` workspace dependency. It was added during planning
  for tarball integrity verification on publish, but the publish path
  doesn't verify integrity (we trust whatever shasum/integrity the
  client sends in the manifest, same as verdaccio).

* Remove the hand-rolled `urldecode` helper. axum's `Path` extractor
  already percent-decodes path segments, so the helper was a redundant
  second decode pass. It was also wrong on two axes:
    - `byte as char` is only correct for ASCII; multi-byte UTF-8
      sequences (`%E2%9C%93` for ✓) would have produced three
      separate `char` values for the raw bytes instead of one
      codepoint.
    - A literal `%` in an already-decoded segment (e.g. a username
      like `weird%name`) would have made the helper fail trying to
      parse the next two chars as hex.
  None of pnpm-registry's existing tests caught either bug because
  the inputs they pass only contain ASCII and never round-trip
  through a literal `%` — but the helper sat one weird username
  away from a 400.

* Replace `packument.as_object_mut().expect("packument is always an
  object")` in `update_dist_tag` with a proper `BadRequest` branch.
  The expectation holds today (we only call it after parsing JSON we
  wrote ourselves) but the panic is a footgun if storage ever gets
  hand-edited or a future code path passes a non-object Value.

* Add `dist_tag_set_works_with_url_encoded_scoped_path` regression
  test that exercises the `%2F`-encoded scoped name shape
  `anonymous-npm-registry-client` actually sends. Locks in that
  axum's decoding is sufficient — if anyone reintroduces a manual
  decode that double-processes the bytes, this test will catch it.

Other points from the review (concurrent publish race, 100 MiB body
buffer) are acceptable for a test mock and explicitly out of scope.

---
Written by an agent (Claude Code, claude-opus-4-7).
---
Written by an agent (Claude Code, claude-opus-4-7).
When I added `read-yaml-file` to @pnpm/jest-config's deps, the
pnpm install that pulled it in side-effected the
__fixtures__/with-git-protocol-dep/pnpm-lock.yaml. I then swept
the diff into commit 4605a18 via `git add -u` without
noticing.

The mutated lockfile added is-number@^7.0.0 as a direct dep, but
the fixture's package.json doesn't declare it — so the
frozen-lockfile install in `pn prepare-fixtures` started failing
with ERR_PNPM_OUTDATED_LOCKFILE.

---
Written by an agent (Claude Code, claude-opus-4-7).
`libnpmpublish` (and therefore `pnpm publish` / `npm publish` against
a scoped package) names the `_attachments` key by the full package
name, slash and all: `@scope/name-1.0.0.tgz`. My `validate_tarball_name`
only accepted the canonical disk form `<basename>-<version>.tgz`, so
every scoped publish to pnpm-registry came back 400. Caught running
`@pnpm/releasing.commands`'s `recursivePublish.ts`:

  HttpErrorGeneral: 400 Bad Request - PUT
    http://localhost:62806/@pnpmtest%2ftest-recursive-publish-project-4

  Tarball filename "@pnpmtest/test-recursive-publish-project-4-1.0.0.tgz"
    is not valid for package "@pnpmtest/test-recursive-publish-project-4"

`canonicalize_tarball_name` now accepts both forms (canonical disk
form and libnpmpublish's full-name form) and returns the canonical
filename. `publish_package` uses that canonical form when writing,
so the on-disk path always matches what `serve_tarball` looks for —
`<root>/@scope/name/name-version.tgz`.

`publish_accepts_libnpmpublish_scoped_attachment_filename` regression
test pins the wire shape (full scoped name in `_attachments`) and
asserts the disk file lands at the canonical basename path AND that a
subsequent tarball GET serves the bytes back through the spec URL.

Verified locally: 14/14 in `tests/auth_publish.rs` pass, and the
full @pnpm/releasing.commands suite goes from 5 failed → 0 failed
(22 suites, 284 passed). The earlier `stage.test.ts` 240s timeout
was a cascade — `recursivePublish` failures left the worker busy
on retries and starved later tests; with publish working, it
clears too.

---
Written by an agent (Claude Code, claude-opus-4-7).
So we can validate the verdaccio → pnpm-registry migration runs the
full test matrix in CI, not just the packages whose source files
changed. Revert this branch from the test-scope condition before
merging.

---
Written by an agent (Claude Code, claude-opus-4-7).
Three missing endpoints surfaced by `@pnpm/registry-access.commands`
tests under the verdaccio → pnpm-registry migration:

* `GET /-/v1/search?text=...&size=...` — local-only search that scans
  `<storage>` for `package.json` files and returns name-substring
  matches in npm search v1 shape. Verdaccio behaves the same — npm's
  upstream search is too fuzzy to be useful for tests (a guaranteed-
  not-to-exist query returns ~133 unrelated results, breaking
  `search: empty results returns "No packages found"`).
* `PUT /:pkg/-rev/:rev` — write the body back as the new packument.
  pnpm's partial-unpublish flow GETs the packument, removes the
  unpublished version, then PUTs the result. `_attachments`, `_rev`,
  and `_revisions` are stripped before persistence.
* `DELETE /:pkg/-rev/:rev` — remove the entire package directory
  (`pnpm unpublish --force`).
* `DELETE /:pkg/-/:filename/-rev/:rev` — remove a single tarball.
  Both forms supported: unscoped (5-seg) on the existing route and
  scoped (6-seg) on a new `delete_six_segments` route — pnpm
  reconstructs DELETE URLs from the rewritten tarball URL in the
  packument, which uses literal `/` for the scope segment.

`canonicalize_tarball_name` is reused for `delete_tarball` so the
libnpmpublish-form (`@scope/name-1.0.0.tgz`) is accepted there too.

Three corresponding `Cache` methods (`remove_package`,
`remove_tarball`) treat a missing file as `Ok(false)` so a duplicate
DELETE doesn't fail.

10 new integration tests cover the search empty/match paths, the
partial-unpublish round trip, `--force` package removal, scoped
tarball delete via the 6-seg route, and the auth gate on the
unpublish endpoints. Verified locally: `@pnpm/registry-access.commands`
goes from 5 failed → 0 failed (8 suites, 80 passed),
`@pnpm/releasing.commands` stays at 0 failed (22 suites, 284 passed).

---
Written by an agent (Claude Code, claude-opus-4-7).
* `auth.rs` test — `matches!(outcome, …)` was discarded, so the test
  passed regardless of which variant `add_or_login` returned. Wrap in
  `assert!(matches!(...))` so the assertion has teeth.

* `publish_package` — reject when the body's `name` disagrees with
  the package name in the URL. Without this, a misrouted `PUT /foo`
  with a body for `bar` overwrites `<storage>/foo/package.json` with
  bar's manifest. Returns 400, matching npm/verdaccio. New
  `publish_rejects_body_name_that_doesnt_match_url` test pins it.

* `publish_package` — seed the merge from the upstream packument
  when the cache is cold, the same way `update_dist_tag` already
  does. Without this, publishing a new version of an upstream-only
  package starts the merge from `None` and the newly-written local
  packument masks every upstream version + dist-tag on subsequent
  reads. Now: cache miss → `load_packument_bytes` → merge against
  the upstream view.

The local-action metadata issue CodeRabbit flagged on
`.github/actions/rustup` is pre-existing and outside this PR's scope.

---
Written by an agent (Claude Code, claude-opus-4-7).
CodeRabbit caught that `update_dist_tag` rewrote the packument
without bumping `time.modified`. The publish path treats that field
as freshness metadata, and pacquet's pick_package + npm's
abbreviated-packument staleness check both key off it, so a tag-only
change (which happens often in registry-mock tests via the
`addDistTag` helper) would look stale to clients on the next read.

Now both PUT and DELETE dist-tag paths bump `time.modified` before
persistence. New `dist_tag_mutations_refresh_time_modified`
regression test pins it.

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

@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: 3

♻️ Duplicate comments (3)
registry/crates/pnpm-registry/src/server.rs (3)

867-874: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh time.modified on dist-tag mutations.

update_dist_tag() rewrites the packument without touching the time block. Tag-only changes will therefore look stale to clients that use time.modified as the freshness signal.

🐛 Minimal fix
     if let Err(err) = mutate(tags) {
         return error_response(&err);
     }
     let _ = tag; // tag name is used by the mutate closure
+    let time_entry = packument_obj
+        .entry("time".to_string())
+        .or_insert_with(|| Value::Object(serde_json::Map::new()));
+    let Some(time_obj) = time_entry.as_object_mut() else {
+        return error_response(&RegistryError::BadRequest {
+            reason: "stored time is not an object".to_string(),
+        });
+    };
+    time_obj.insert("modified".to_string(), Value::String(now_iso()));
     let new_bytes = match serde_json::to_vec_pretty(&packument) {
         Ok(b) => b,
         Err(err) => return error_response(&RegistryError::Json(err)),
     };
🤖 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 `@registry/crates/pnpm-registry/src/server.rs` around lines 867 - 874, When
mutating dist-tags in update_dist_tag (inside the mutate(tags) closure where
packument and tag are used), refresh the packument.time.modified field to the
current timestamp so tag-only changes appear fresh; if packument.time or
packument.time.modified is missing, create them and set modified to the current
UTC ISO-8601 string (use your existing datetime utility or
chrono::Utc::now().to_rfc3339()), then continue to serialize packument as before
with serde_json::to_vec_pretty.

554-563: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Seed publish merges from upstream when the local cache is cold.

Line 554 only reads the existing packument from disk. In proxy mode, publishing over a package that exists upstream but has never been cached locally will start the merge from None, and the newly written local packument will hide every upstream version/dist-tag on subsequent reads.

🐛 Minimal fix
-    let existing_bytes = match state.inner.cache.read_packument_any_age(&name).await {
-        Ok(b) => b,
-        Err(err) => return error_response(&err),
-    };
+    let existing_bytes = match state.inner.cache.read_packument_any_age(&name).await {
+        Ok(Some(bytes)) => Some(bytes),
+        Ok(None) => match load_packument_bytes(state, &name).await {
+            PackumentLoad::Ok(bytes) => Some(bytes),
+            PackumentLoad::NotFound => None,
+            PackumentLoad::Err(err) => return error_response(&err),
+        },
+        Err(err) => return error_response(&err),
+    };
🤖 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 `@registry/crates/pnpm-registry/src/server.rs` around lines 554 - 563, The
merge currently uses None when read_packument_any_age(&name) returns no local
packument, which causes local publishes to hide upstream versions; change the
flow so that if existing_bytes is None and the registry is running in proxy mode
you attempt to load the upstream packument and use that as existing for
merge_manifest. Concretely: after read_packument_any_age returns None, call the
code-path that fetches the packument from upstream (e.g., the existing
proxy/upstream fetch helper on state.inner or similar), deserialize it into a
Value, treat errors via error_response like the local read does, and pass that
Value (not None) into merge_manifest(name, incoming, now_iso()) so upstream
versions/dist-tags are preserved.

530-537: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject publish bodies whose name disagrees with the URL.

Line 530 parses the incoming packument, but this path still never checks that incoming["name"] matches raw_name. A mismatched payload can be stored under the wrong package directory and leave the packument internally inconsistent.

🐛 Minimal fix
     let mut incoming: Value = match serde_json::from_slice(&body) {
         Ok(v) => v,
         Err(err) => return error_response(&RegistryError::Json(err)),
     };
+    let body_name = incoming.get("name").and_then(Value::as_str).unwrap_or("");
+    if body_name != name.as_str() {
+        return error_response(&RegistryError::BadRequest {
+            reason: format!(
+                "package in URL ({:?}) does not match body ({body_name:?})",
+                name.as_str()
+            ),
+        });
+    }
     let attachments = match extract_attachments(&mut incoming) {
         Ok(a) => a,
         Err(err) => return error_response(&err),
     };
🤖 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 `@registry/crates/pnpm-registry/src/server.rs` around lines 530 - 537, After
parsing incoming into serde_json::Value (variable incoming) but before calling
extract_attachments, validate that the packument name matches the URL by
checking incoming["name"] is a string and equals raw_name; if the field is
missing, not a string, or not equal to raw_name, return error_response with an
appropriate RegistryError (or a new variant) to reject the request. Implement
this check in the same scope where incoming is available (near the existing
serde_json::from_slice and before extract_attachments) and keep the
extract_attachments, error_response, and RegistryError::Json usage unchanged for
other error paths.
🧹 Nitpick comments (2)
registry/crates/pnpm-registry/src/cache.rs (1)

163-166: 💤 Low value

Clarify comment wording.

The comment reads: "a benign 404 here would surface as a real error to the caller" — but the code specifically prevents that by returning Ok(false) on NotFound. Consider rewording to: "Without this handling, a benign 404 would surface as a real error to the caller."

🤖 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 `@registry/crates/pnpm-registry/src/cache.rs` around lines 163 - 166, Update
the comment above the tarball-removal logic in cache.rs (the block that
documents "Remove a single tarball file. Returns `Ok(false)` when the file is
already gone" and the sentence mentioning a "benign 404") to clarify causality:
rephrase the sentence to something like "Without this handling, a benign 404
would surface as a real error to the caller." Keep the rest of the comment
intact and ensure it still references that the function returns `Ok(false)` on
`NotFound`.
registry/crates/pnpm-registry/tests/auth_publish.rs (1)

577-577: ⚡ Quick win

Consider a more direct assertion for clarity and efficiency.

The current approach collects all keys into a Vec just to compare them. A more idiomatic pattern would check the length and presence directly.

♻️ Proposed refactor
-    assert_eq!(served["versions"].as_object().unwrap().keys().collect::<Vec<_>>(), vec!["2.0.0"]);
+    let versions = served["versions"].as_object().unwrap();
+    assert_eq!(versions.len(), 1);
+    assert!(versions.contains_key("2.0.0"));
🤖 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 `@registry/crates/pnpm-registry/tests/auth_publish.rs` at line 577, The
assertion currently builds a Vec of keys to compare to ["2.0.0"]; instead,
directly assert the map size and presence: use
served["versions"].as_object().unwrap().len() == 1 (or assert_eq!) and
assert!(served["versions"].as_object().unwrap().contains_key("2.0.0")); update
the assertion around the served variable to these two checks to be clearer and
more efficient.
🤖 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 `@registry/crates/pnpm-registry/src/search.rs`:
- Around line 32-39: In parse_query, stop aborting on the first malformed pair
and make "q" a fallback: iterate pairs and use pair.split_once('=') but continue
(skip) when split_once returns None; decode with percent_decode and skip if the
decoded value is empty; if key == "text" return Some(decoded) immediately; if
key == "q" store decoded as a fallback and keep scanning so a later "text" will
override it; after the loop return the stored q fallback (or None). Ensure this
prevents run_local_search() receiving Some("") by ignoring empty decoded values.

In `@registry/crates/pnpm-registry/src/server.rs`:
- Around line 175-183: In get_three_segments, the search branch calls
serve_search(&state, query).await without passing the request headers and does
not enforce package ACLs; update the call to pass the HeaderMap (headers) into
serve_search (adjust serve_search signature to accept headers if needed) and,
after obtaining results from serve_search, iterate the candidate packages and
filter them by calling enforce_access(state, &headers, package_name,
Action::Access) (or the equivalent enforce_access signature used elsewhere)
before returning the response; apply the same change to the similar search
handling around the other occurrence (lines ~616-637) so all search responses
are filtered by enforce_access with Action::Access.
- Around line 660-668: The handler currently accepts any JSON and overwrites the
stored packument; parse the body into JSON as before into packument but then
validate it is an object and that its "name" field matches the package name from
the request URL (the handler's package-name variable) before proceeding; if
packument.as_object_mut() is None or packument.get("name") is missing or not
equal to the requested name, return an error_response (e.g.,
RegistryError::BadRequest or a new RegistryError variant) instead of persisting,
and only after that remove "_attachments", "_rev", "_revisions" and continue
with the normal write flow. Ensure you reference the same
serde_json::from_slice(body) result (packument) and use
error_response/RegistryError consistently for failures.

---

Duplicate comments:
In `@registry/crates/pnpm-registry/src/server.rs`:
- Around line 867-874: When mutating dist-tags in update_dist_tag (inside the
mutate(tags) closure where packument and tag are used), refresh the
packument.time.modified field to the current timestamp so tag-only changes
appear fresh; if packument.time or packument.time.modified is missing, create
them and set modified to the current UTC ISO-8601 string (use your existing
datetime utility or chrono::Utc::now().to_rfc3339()), then continue to serialize
packument as before with serde_json::to_vec_pretty.
- Around line 554-563: The merge currently uses None when
read_packument_any_age(&name) returns no local packument, which causes local
publishes to hide upstream versions; change the flow so that if existing_bytes
is None and the registry is running in proxy mode you attempt to load the
upstream packument and use that as existing for merge_manifest. Concretely:
after read_packument_any_age returns None, call the code-path that fetches the
packument from upstream (e.g., the existing proxy/upstream fetch helper on
state.inner or similar), deserialize it into a Value, treat errors via
error_response like the local read does, and pass that Value (not None) into
merge_manifest(name, incoming, now_iso()) so upstream versions/dist-tags are
preserved.
- Around line 530-537: After parsing incoming into serde_json::Value (variable
incoming) but before calling extract_attachments, validate that the packument
name matches the URL by checking incoming["name"] is a string and equals
raw_name; if the field is missing, not a string, or not equal to raw_name,
return error_response with an appropriate RegistryError (or a new variant) to
reject the request. Implement this check in the same scope where incoming is
available (near the existing serde_json::from_slice and before
extract_attachments) and keep the extract_attachments, error_response, and
RegistryError::Json usage unchanged for other error paths.

---

Nitpick comments:
In `@registry/crates/pnpm-registry/src/cache.rs`:
- Around line 163-166: Update the comment above the tarball-removal logic in
cache.rs (the block that documents "Remove a single tarball file. Returns
`Ok(false)` when the file is already gone" and the sentence mentioning a "benign
404") to clarify causality: rephrase the sentence to something like "Without
this handling, a benign 404 would surface as a real error to the caller." Keep
the rest of the comment intact and ensure it still references that the function
returns `Ok(false)` on `NotFound`.

In `@registry/crates/pnpm-registry/tests/auth_publish.rs`:
- Line 577: The assertion currently builds a Vec of keys to compare to
["2.0.0"]; instead, directly assert the map size and presence: use
served["versions"].as_object().unwrap().len() == 1 (or assert_eq!) and
assert!(served["versions"].as_object().unwrap().contains_key("2.0.0")); update
the assertion around the served variable to these two checks to be clearer and
more efficient.
🪄 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: 2f20e143-838b-4a57-a06e-04569775ac89

📥 Commits

Reviewing files that changed from the base of the PR and between 07d40d4 and b2386ef.

📒 Files selected for processing (5)
  • registry/crates/pnpm-registry/src/cache.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/search.rs
  • registry/crates/pnpm-registry/src/server.rs
  • registry/crates/pnpm-registry/tests/auth_publish.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 (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
🧰 Additional context used
📓 Path-based instructions (1)
registry/crates/**/*.rs

📄 CodeRabbit inference engine (registry/AGENTS.md)

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

Files:

  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/cache.rs
  • registry/crates/pnpm-registry/src/search.rs
  • registry/crates/pnpm-registry/src/server.rs
  • registry/crates/pnpm-registry/tests/auth_publish.rs
🔇 Additional comments (5)
registry/crates/pnpm-registry/src/lib.rs (1)

17-17: LGTM!

registry/crates/pnpm-registry/src/cache.rs (2)

151-161: LGTM!


167-174: ⚖️ Poor tradeoff

Remove traversal risk: remove_tarball is only called with a canonicalized filename.

registry/crates/pnpm-registry/src/server.rs’s delete_tarball(...) derives canonical via name.canonicalize_tarball_name(filename) and then calls state.inner.cache.remove_tarball(&name, &canonical), so the filename passed into registry/crates/pnpm-registry/src/cache.rs is validated/canonicalized before deletion.

registry/crates/pnpm-registry/tests/auth_publish.rs (2)

480-531: LGTM!


601-666: LGTM!

Comment thread registry/crates/pnpm-registry/src/search.rs Outdated
Comment thread registry/crates/pnpm-registry/src/server.rs Outdated
Comment thread registry/crates/pnpm-registry/src/server.rs
@zkochan zkochan force-pushed the pnpm-registry-auth branch from e0a9394 to 81f805a Compare May 24, 2026 22:26
zkochan added 2 commits May 25, 2026 00:29
Two CodeRabbit findings on the new search endpoint:

* `parse_query` had three latent bugs the fixture tests didn't
  surface:
    - A pair with no `=` aborted the whole parse via `?` instead
      of skipping; `text=foo&trailing` would return None.
    - `q=` returned immediately even when a later `text=` was
      present; the order in the URL changed the answer.
    - `text=` (empty value) returned `Some("")`. The downstream
      substring filter uses `contains(needle)` which is always
      true for an empty needle — so an anonymous `?text=` would
      have dumped every package in storage.
  Now: skip malformed pairs, treat empty decoded values as "no
  text", and use `q=` only as a fallback (`text=` wins regardless
  of order).

* `serve_search` ignored the access policy. An anonymous caller
  could enumerate `@pnpm.e2e/needs-auth` and `@private/*`
  packages — packages the packument and tarball GETs correctly
  hide behind 401 — just by hitting `/-/v1/search?text=`. Now
  serve_search receives the request headers and runs each result
  through `enforce_access(..., Action::Access)` before returning.
  `total` is overwritten with the post-filter count so the
  response can't leak the existence of hidden packages.

Four new tests pin both: `text_overrides_q_regardless_of_order`,
`empty_text_is_no_query`, `malformed_pair_doesnt_abort_parse`, and
`search_filters_protected_packages_for_anonymous_callers`.

---
Written by an agent (Claude Code, claude-opus-4-7).
actionlint flags `type: boolean` / `type: string` on composite-action
inputs as an unexpected key — it isn't part of the GitHub Actions
input schema and was silently ignored at runtime. Composite-action
inputs are always strings, and consumers in this action already use
`${{ inputs.X == 'true' }}` comparisons, so the annotation never had
any effect.

This PR is the first place outside `pacquet-*.yml` workflows that
wires in `./.github/actions/rustup` (the new TS-tests step in
`test.yml`), so the actionlint warning now surfaces here — caught
by CodeRabbit at .github/workflows/test.yml:77.

No behavior change.

---
Written by an agent (Claude Code, claude-opus-4-7).
@zkochan

zkochan commented May 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
__utils__/jest-config/with-registry/globalSetup.js (1)

33-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Kill the spawned registry if setup aborts.

If waitForServerOnline() or addUser() throws, Jest can fail before teardown runs, and this child keeps listening in the background. That makes reruns flaky and can leave orphaned pnpm-registry processes behind.

Proposed fix
   const server = spawn(
     bin,
     [
       '--listen', `127.0.0.1:${process.env.PNPM_REGISTRY_MOCK_PORT}`,
@@
     { stdio: 'inherit' }
   )
 
   let killed = false
@@
   global.killServer = () => {
     killed = true
     return kill(server.pid)
   }
 
-  await waitForServerOnline()
-
-  // Register the test user and store the auth token for bearer-based tests
-  const { token } = await addUser({
-    username: REGISTRY_MOCK_CREDENTIALS.username,
-    password: REGISTRY_MOCK_CREDENTIALS.password,
-    email: 'foo@bar.net',
-  })
-  process.env.REGISTRY_MOCK_TOKEN = token
+  try {
+    await waitForServerOnline()
+
+    // Register the test user and store the auth token for bearer-based tests
+    const { token } = await addUser({
+      username: REGISTRY_MOCK_CREDENTIALS.username,
+      password: REGISTRY_MOCK_CREDENTIALS.password,
+      email: 'foo@bar.net',
+    })
+    process.env.REGISTRY_MOCK_TOKEN = token
+  } catch (err) {
+    if (server.pid && !killed) {
+      killed = true
+      await kill(server.pid)
+    }
+    throw err
+  }
 }
🤖 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 `@__utils__/jest-config/with-registry/globalSetup.js` around lines 33 - 72, The
setup can abort after spawning the registry (server) and before teardown,
leaving an orphaned process; wrap the post-spawn async calls
(waitForServerOnline() and addUser({...})) in try/catch (or try/finally) and on
any error call the existing cleanup (use killed = true + kill(server.pid) or
call global.killServer()) to ensure the spawned server is terminated, then
rethrow the error so Jest fails as before; locate the server variable,
waitForServerOnline, addUser, and global.killServer in the file to apply this
change.
♻️ Duplicate comments (1)
registry/crates/pnpm-registry/src/server.rs (1)

710-718: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate partial-unpublish payload shape and package identity before persisting.

update_packument still writes arbitrary JSON and does not enforce body.name == URL package, so a malformed/mismatched payload can replace a valid packument with inconsistent content.

Proposed minimal fix
     let mut packument: Value = match serde_json::from_slice(body) {
         Ok(v) => v,
         Err(err) => return error_response(&RegistryError::Json(err)),
     };
-    if let Some(obj) = packument.as_object_mut() {
-        obj.remove("_attachments");
-        obj.remove("_rev");
-        obj.remove("_revisions");
-    }
+    let Some(obj) = packument.as_object_mut() else {
+        return error_response(&RegistryError::BadRequest {
+            reason: "packument body must be a JSON object".to_string(),
+        });
+    };
+    let body_name = obj.get("name").and_then(Value::as_str);
+    if body_name != Some(name.as_str()) {
+        return error_response(&RegistryError::BadRequest {
+            reason: format!(
+                "package in URL ({:?}) does not match body ({body_name:?})",
+                name.as_str()
+            ),
+        });
+    }
+    obj.remove("_attachments");
+    obj.remove("_rev");
+    obj.remove("_revisions");
🤖 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 `@registry/crates/pnpm-registry/src/server.rs` around lines 710 - 718, The
update_packument handler currently accepts arbitrary JSON in the packument
variable and persists it without validating shape or identity; change
update_packument so it deserializes the request body into a strong type (or at
minimum checks required fields) and verifies that packument["name"] (or the
deserialized name field) equals the package name derived from the request URL
before persisting; if deserialization fails or the name mismatches, return
error_response(&RegistryError::InvalidPayload) (or an appropriate error) instead
of writing the JSON, and keep the existing removal of internal fields
("_attachments", "_rev", "_revisions") only after the payload is validated.
🤖 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 `@registry/crates/pnpm-registry/src/auth.rs`:
- Around line 171-178: The Authorization parsing currently checks only exact
"Bearer"/"bearer" and "Basic"/"basic" prefixes (in identify()/auth parsing), so
schemes like "BEARER" are ignored; change the logic in auth.rs to split the
header value on the first whitespace into (scheme, credentials), compare scheme
case-insensitively (e.g., lowercased) and then branch: if scheme == "bearer"
call tokens.lookup(credentials.trim()), if scheme == "basic" base64-decode
credentials and call users.verify(user, password). Ensure trimming and the
existing error propagation (ok?/? operators) remain unchanged.

---

Outside diff comments:
In `@__utils__/jest-config/with-registry/globalSetup.js`:
- Around line 33-72: The setup can abort after spawning the registry (server)
and before teardown, leaving an orphaned process; wrap the post-spawn async
calls (waitForServerOnline() and addUser({...})) in try/catch (or try/finally)
and on any error call the existing cleanup (use killed = true + kill(server.pid)
or call global.killServer()) to ensure the spawned server is terminated, then
rethrow the error so Jest fails as before; locate the server variable,
waitForServerOnline, addUser, and global.killServer in the file to apply this
change.

---

Duplicate comments:
In `@registry/crates/pnpm-registry/src/server.rs`:
- Around line 710-718: The update_packument handler currently accepts arbitrary
JSON in the packument variable and persists it without validating shape or
identity; change update_packument so it deserializes the request body into a
strong type (or at minimum checks required fields) and verifies that
packument["name"] (or the deserialized name field) equals the package name
derived from the request URL before persisting; if deserialization fails or the
name mismatches, return error_response(&RegistryError::InvalidPayload) (or an
appropriate error) instead of writing the JSON, and keep the existing removal of
internal fields ("_attachments", "_rev", "_revisions") only after the payload is
validated.
🪄 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: cf44b6ef-e70c-46e3-864c-b785b78d46a6

📥 Commits

Reviewing files that changed from the base of the PR and between b2386ef and 03235c9.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (16)
  • .github/actions/rustup/action.yml
  • .github/workflows/test.yml
  • __utils__/jest-config/package.json
  • __utils__/jest-config/with-registry/globalSetup.js
  • registry/crates/pnpm-registry/Cargo.toml
  • registry/crates/pnpm-registry/src/auth.rs
  • registry/crates/pnpm-registry/src/cache.rs
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/error.rs
  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/package_name.rs
  • registry/crates/pnpm-registry/src/policy.rs
  • registry/crates/pnpm-registry/src/publish.rs
  • registry/crates/pnpm-registry/src/search.rs
  • registry/crates/pnpm-registry/src/server.rs
  • registry/crates/pnpm-registry/tests/auth_publish.rs
✅ Files skipped from review due to trivial changes (3)
  • .github/actions/rustup/action.yml
  • registry/crates/pnpm-registry/Cargo.toml
  • utils/jest-config/package.json
📜 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: Code Coverage
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • 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)
registry/crates/**/*.rs

📄 CodeRabbit inference engine (registry/AGENTS.md)

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

Files:

  • registry/crates/pnpm-registry/src/lib.rs
  • registry/crates/pnpm-registry/src/config.rs
  • registry/crates/pnpm-registry/src/cache.rs
  • registry/crates/pnpm-registry/src/error.rs
  • registry/crates/pnpm-registry/src/package_name.rs
  • registry/crates/pnpm-registry/src/policy.rs
  • registry/crates/pnpm-registry/src/search.rs
  • registry/crates/pnpm-registry/src/auth.rs
  • registry/crates/pnpm-registry/src/publish.rs
  • registry/crates/pnpm-registry/tests/auth_publish.rs
  • registry/crates/pnpm-registry/src/server.rs
🧠 Learnings (1)
📚 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:

  • __utils__/jest-config/with-registry/globalSetup.js
🔇 Additional comments (5)
registry/crates/pnpm-registry/src/package_name.rs (1)

43-77: LGTM!

registry/crates/pnpm-registry/src/publish.rs (1)

25-76: LGTM!

Also applies to: 94-155, 170-197

registry/crates/pnpm-registry/src/cache.rs (1)

151-174: LGTM!

registry/crates/pnpm-registry/src/search.rs (1)

44-74: LGTM!

Also applies to: 81-113, 119-150

registry/crates/pnpm-registry/tests/auth_publish.rs (1)

63-827: LGTM!

Comment thread registry/crates/pnpm-registry/src/auth.rs Outdated
RFC 7235 §2.1 says the scheme token is case-insensitive, so `BEARER`,
`bearer`, `Bearer`, etc. all have to resolve to the same thing.
`identify()` was only matching the four hand-rolled spellings
(`Bearer`/`bearer`, `Basic`/`basic`) via `strip_prefix`, so a client
that uppercased the scheme (rare but not invalid) silently fell
through to anonymous access.

Now: split off the first whitespace-delimited token and compare
case-insensitively via `eq_ignore_ascii_case`.

New `identify_parses_auth_scheme_case_insensitively` test iterates
through Bearer/bearer/BEARER/BeArEr (and the matching Basic
spellings) and asserts each resolves to the right user.

---
Written by an agent (Claude Code, claude-opus-4-7).
…rch debug

The is-positive search test is failing in CI but passing locally with
the full registry-access suite (matched=2, returned=2). To debug the
divergence with fast iteration:

* TEMP add a `ci:test-registry-access-only` script that filters to
  just @pnpm/registry-access.commands
* TEMP point the pnpm-registry-auth branch's CI at that script
* Add a `tracing::info!("search: completed scan", ...)` line in
  run_local_search that prints path_count / scanned / matched /
  returned / parse_errors / build_errors so the CI log surfaces
  exactly which step is dropping is-positive

Will revert all three changes once the search failure is diagnosed.

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

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

Caution

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

⚠️ Outside diff range comments (1)
registry/crates/pnpm-registry/src/search.rs (1)

236-236: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the official npmjs.com domain for npm package links.

In registry/crates/pnpm-registry/src/search.rs (line 236), the code builds https://npmx.dev/package/{name}, but npm package pages are served at https://www.npmjs.com/package/<package-name> (scoped packages use an encoded @scope%2Fname). Switch to npmjs.com unless npmx.dev is intentionally supported.

🤖 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 `@registry/crates/pnpm-registry/src/search.rs` at line 236, The current
links.insert call builds "https://npmx.dev/package/{name}" which should point to
the official npm domain and must correctly encode scoped names; update the
Value::String construction used in links.insert(...) to use
"https://www.npmjs.com/package/<encoded-name>" instead of npmx.dev and
percent-URL-encode package names (encode '@' as %40 and '/' as %2F) before
formatting; e.g., replace format!("https://npmx.dev/package/{name}") with a
format using an encoded_name produced by a URL percent-encoding helper (e.g.,
percent_encoding or url::form_urlencoded) so links.insert(...,
Value::String(format!("https://www.npmjs.com/package/{}", encoded_name))) is
stored.
🧹 Nitpick comments (2)
.github/workflows/test.yml (1)

87-92: ⚡ Quick win

Reminder: Remove temporary debug scope before merging.

This temporary branch-specific test scope restriction is clearly marked for reversion before merge, but it would significantly degrade CI coverage if accidentally landed on main.

Consider adding a GitHub issue to track removal, or use a feature flag / environment variable approach if you need this debug capability to persist across multiple pushes.

Would you like me to suggest a more robust approach for conditional test scoping, or open a tracking issue for this cleanup?

🤖 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 @.github/workflows/test.yml around lines 87 - 92, This PR contains a
temporary branch-specific CI guard that limits tests when REF_NAME ==
"pnpm-registry-auth" (writing script=ci:test-registry-access-only and
scope=registry-access only to GITHUB_OUTPUT); remove this block before merging
(or replace it with a safer mechanism) by either deleting the if-block that
checks REF_NAME or changing it to an opt-in environment variable/feature-flag
(e.g., CHECK_ONLY_REGISTRY_ACCESS) so accidental merges to main won’t narrow CI
coverage, and optionally open a tracking issue to remind the team to remove the
debug hook if you keep the flag approach.
registry/crates/pnpm-registry/src/search.rs (1)

103-103: 💤 Low value

Consider logging read errors for consistency.

Lines 107-108 and 121-122 log parse and build failures, but Line 103 silently skips files that can't be read. For debugging purposes, it might be helpful to log read errors as well, especially since you're already tracking statistics.

📊 Optional: Add read error logging
-        let Ok(bytes) = fs::read(&path).await else { continue };
+        let bytes = match fs::read(&path).await {
+            Ok(bytes) => bytes,
+            Err(err) => {
+                tracing::warn!(?err, ?path, "search: failed to read packument");
+                continue;
+            }
+        };

Note: Read errors might be more frequent than parse/build errors (e.g., permission issues), so monitor log volume if you add this.

🤖 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 `@registry/crates/pnpm-registry/src/search.rs` at line 103, The file-read is
silently skipped on failure (let Ok(bytes) = fs::read(&path).await else {
continue };) — change this to capture and log the read error (include the path
and error details) instead of silently continuing; use the same
logger/statistics pattern used for parse/build failures (the surrounding
parse/build error logging) so read errors are recorded and, if appropriate,
increment the same failure counter or metric for consistency.
🤖 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.

Outside diff comments:
In `@registry/crates/pnpm-registry/src/search.rs`:
- Line 236: The current links.insert call builds
"https://npmx.dev/package/{name}" which should point to the official npm domain
and must correctly encode scoped names; update the Value::String construction
used in links.insert(...) to use "https://www.npmjs.com/package/<encoded-name>"
instead of npmx.dev and percent-URL-encode package names (encode '@' as %40 and
'/' as %2F) before formatting; e.g., replace
format!("https://npmx.dev/package/{name}") with a format using an encoded_name
produced by a URL percent-encoding helper (e.g., percent_encoding or
url::form_urlencoded) so links.insert(...,
Value::String(format!("https://www.npmjs.com/package/{}", encoded_name))) is
stored.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 87-92: This PR contains a temporary branch-specific CI guard that
limits tests when REF_NAME == "pnpm-registry-auth" (writing
script=ci:test-registry-access-only and scope=registry-access only to
GITHUB_OUTPUT); remove this block before merging (or replace it with a safer
mechanism) by either deleting the if-block that checks REF_NAME or changing it
to an opt-in environment variable/feature-flag (e.g.,
CHECK_ONLY_REGISTRY_ACCESS) so accidental merges to main won’t narrow CI
coverage, and optionally open a tracking issue to remind the team to remove the
debug hook if you keep the flag approach.

In `@registry/crates/pnpm-registry/src/search.rs`:
- Line 103: The file-read is silently skipped on failure (let Ok(bytes) =
fs::read(&path).await else { continue };) — change this to capture and log the
read error (include the path and error details) instead of silently continuing;
use the same logger/statistics pattern used for parse/build failures (the
surrounding parse/build error logging) so read errors are recorded and, if
appropriate, increment the same failure counter or metric for consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f1c967cb-3165-4569-a127-0b3c847159e4

📥 Commits

Reviewing files that changed from the base of the PR and between 1d1f3c5 and d2608f6.

📒 Files selected for processing (3)
  • .github/workflows/test.yml
  • package.json
  • registry/crates/pnpm-registry/src/search.rs
✅ Files skipped from review due to trivial changes (1)
  • package.json
📜 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: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (1)
registry/crates/**/*.rs

📄 CodeRabbit inference engine (registry/AGENTS.md)

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

Files:

  • registry/crates/pnpm-registry/src/search.rs
🔇 Additional comments (3)
registry/crates/pnpm-registry/src/search.rs (3)

85-87: LGTM!

Also applies to: 127-137


89-92: LGTM!


104-124: LGTM!

zkochan added 4 commits May 25, 2026 02:02
…queries

CI debug output showed `path_count=225, matched=1` for a search of
`is-positive` — meaning the storage walker found 225 packuments but
only one of them (`@pnpm.e2e/postinstall-requires-is-positive`)
contained `is-positive` as a substring. Inspecting the
`@pnpm/registry-mock` npm tarball revealed why: it ships only
**scoped** packuments. The unscoped `is-positive/` directory I had
locally was leftover state from pacquet runs that proxied npm.

Fresh CI storage doesn't contain `is-positive`, so a local-only
search can't surface it — but the TS `releasing.commands` search
test asserts that `pnpm search is-positive` returns it.

Fix: after running the local scan, if the query parses as a valid
package name and isn't already in the local results, augment with a
single upstream packument fetch for the exact name. Hit → prepend
the resulting entry; miss (404) → leave the results untouched. The
fetch goes through `load_packument_bytes`, which also caches the
packument on disk so subsequent searches find it locally.

Naive proxying to npm's `/-/v1/search` was rejected earlier because
npm's search is too fuzzy for the empty-results test
(`this-package-definitely-does-not-exist-xyz-123` returns 1.7M
results from npm). The exact-name augment doesn't suffer from that
— a guaranteed-not-to-exist name 404s on the packument endpoint and
contributes nothing.

Two new tests pin the behavior using `mockito`:
* `search_augments_with_upstream_when_local_misses_exact_name` —
  asserts the augmented entry shows up in `objects` and the
  packument is cached on disk for next time.
* `search_augment_skips_when_upstream_404s` — asserts a 404 from
  upstream leaves the empty result alone.

Also reverts the TEMPORARY `ci:test-registry-access-only` script
and matching workflow narrowing from the previous commit — full
test scope restored on this branch.

---
Written by an agent (Claude Code, claude-opus-4-7).
@zkochan zkochan merged commit d8a79a9 into main May 25, 2026
21 checks passed
@zkochan zkochan deleted the pnpm-registry-auth branch May 25, 2026 07:40
@zkochan zkochan mentioned this pull request May 27, 2026
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