feat(registry): add auth/dist-tag/publish endpoints + wire TS tests onto pnpm-registry#11914
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds 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. ChangesRegistry Auth & Publishing Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Micro-Benchmark ResultsLinux |
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lockpnpm-lock.yamlis 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.jsregistry/crates/pnpm-registry/Cargo.tomlregistry/crates/pnpm-registry/src/auth.rsregistry/crates/pnpm-registry/src/config.rsregistry/crates/pnpm-registry/src/error.rsregistry/crates/pnpm-registry/src/lib.rsregistry/crates/pnpm-registry/src/package_name.rsregistry/crates/pnpm-registry/src/policy.rsregistry/crates/pnpm-registry/src/publish.rsregistry/crates/pnpm-registry/src/server.rsregistry/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.rsregistry/crates/pnpm-registry/src/lib.rsregistry/crates/pnpm-registry/src/error.rsregistry/crates/pnpm-registry/src/config.rsregistry/crates/pnpm-registry/src/policy.rsregistry/crates/pnpm-registry/tests/auth_publish.rsregistry/crates/pnpm-registry/src/publish.rsregistry/crates/pnpm-registry/src/auth.rsregistry/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 forimport.meta.dirnameinrepoRoot()
import.meta.dirnameis supported starting in Node v20.11.0, and CI runs this Jest global setup on Node >= 22.13.0 (.github/workflows/ci.ymlmatrix:22.13.0,24.0.0,26.0.0). The repo also already usesimport.meta.dirnamein 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!
Integrated-Benchmark Report (Linux)Scenario: Isolated linker: fresh restore, cold cache + cold store
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
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
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
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
]
}
]
} |
|
| Branch | pr/11914 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark 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%) |
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).
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
registry/crates/pnpm-registry/src/server.rs (3)
867-874:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRefresh
time.modifiedon dist-tag mutations.
update_dist_tag()rewrites the packument without touching thetimeblock. Tag-only changes will therefore look stale to clients that usetime.modifiedas 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 winSeed 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 winReject publish bodies whose
namedisagrees with the URL.Line 530 parses the incoming packument, but this path still never checks that
incoming["name"]matchesraw_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 valueClarify 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)onNotFound. 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 winConsider a more direct assertion for clarity and efficiency.
The current approach collects all keys into a
Vecjust 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
📒 Files selected for processing (5)
registry/crates/pnpm-registry/src/cache.rsregistry/crates/pnpm-registry/src/lib.rsregistry/crates/pnpm-registry/src/search.rsregistry/crates/pnpm-registry/src/server.rsregistry/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.rsregistry/crates/pnpm-registry/src/cache.rsregistry/crates/pnpm-registry/src/search.rsregistry/crates/pnpm-registry/src/server.rsregistry/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 tradeoffRemove traversal risk:
remove_tarballis only called with a canonicalized filename.
registry/crates/pnpm-registry/src/server.rs’sdelete_tarball(...)derivescanonicalvianame.canonicalize_tarball_name(filename)and then callsstate.inner.cache.remove_tarball(&name, &canonical), so thefilenamepassed intoregistry/crates/pnpm-registry/src/cache.rsis validated/canonicalized before deletion.registry/crates/pnpm-registry/tests/auth_publish.rs (2)
480-531: LGTM!
601-666: LGTM!
e0a9394 to
81f805a
Compare
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).
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
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 winKill the spawned registry if setup aborts.
If
waitForServerOnline()oraddUser()throws, Jest can fail before teardown runs, and this child keeps listening in the background. That makes reruns flaky and can leave orphanedpnpm-registryprocesses 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 winValidate partial-unpublish payload shape and package identity before persisting.
update_packumentstill writes arbitrary JSON and does not enforcebody.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
⛔ Files ignored due to path filters (2)
Cargo.lockis excluded by!**/*.lockpnpm-lock.yamlis 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.jsregistry/crates/pnpm-registry/Cargo.tomlregistry/crates/pnpm-registry/src/auth.rsregistry/crates/pnpm-registry/src/cache.rsregistry/crates/pnpm-registry/src/config.rsregistry/crates/pnpm-registry/src/error.rsregistry/crates/pnpm-registry/src/lib.rsregistry/crates/pnpm-registry/src/package_name.rsregistry/crates/pnpm-registry/src/policy.rsregistry/crates/pnpm-registry/src/publish.rsregistry/crates/pnpm-registry/src/search.rsregistry/crates/pnpm-registry/src/server.rsregistry/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.rsregistry/crates/pnpm-registry/src/config.rsregistry/crates/pnpm-registry/src/cache.rsregistry/crates/pnpm-registry/src/error.rsregistry/crates/pnpm-registry/src/package_name.rsregistry/crates/pnpm-registry/src/policy.rsregistry/crates/pnpm-registry/src/search.rsregistry/crates/pnpm-registry/src/auth.rsregistry/crates/pnpm-registry/src/publish.rsregistry/crates/pnpm-registry/tests/auth_publish.rsregistry/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!
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).
There was a problem hiding this comment.
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 winUse the official npmjs.com domain for npm package links.
In
registry/crates/pnpm-registry/src/search.rs(line 236), the code buildshttps://npmx.dev/package/{name}, but npm package pages are served athttps://www.npmjs.com/package/<package-name>(scoped packages use an encoded@scope%2Fname). Switch tonpmjs.comunlessnpmx.devis 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 winReminder: 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 valueConsider 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
📒 Files selected for processing (3)
.github/workflows/test.ymlpackage.jsonregistry/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!
…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).
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: BearerandAuthorization: Basicboth identify the caller.@pnpm/registry-mock'sconfig.yaml:@private/*and@pnpm.e2e/needs-authrequire 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.jskeepsprepare()from@pnpm/registry-mock(still needed for the tempy storage path written into the runtime-config yaml —getIntegrityreads it from there) but spawnspnpm-registryinstead of verdaccio.addUser,addDistTag,getIntegrity,REGISTRY_MOCK_*from registry-mock work as-is — they're plain npm-wire-protocol HTTP calls.PNPM_REGISTRY_BINenv override, thentarget/release/pnpm-registry, thentarget/debug/pnpm-registry..github/workflows/test.yml) installs the Rust toolchain via the existing./.github/actions/rustupcomposite action and buildspnpm-registry --releasebefore 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 intests/auth_publish.rscovering 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 warningscargo fmt --checkcargo check --workspace --all-targetsinstalling/deps-installer/test/install/auth.ts— 5 passed (2skipOnNode17)installing/deps-installer/test/cache.ts— 2 passed (usesaddDistTag)installing/deps-installer/test/install/aliases.ts+install/peerDependencies.ts— 71 passedinstalling/deps-installer/test/brokenLockfileIntegrity.ts+install/local.ts— 19 passedWritten by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
New Features
Tests
Chores
Bug Fixes