feat(pnpr): declare patterns on registries, reduce routers to ordered sources#12778
Conversation
…rces Implements the revised registry-mounts model from pnpm/rfcs#16, replacing the route-level-pattern shape outright (pre-1.0, no `routes:` compatibility mode). Hosted and upstream mounts now take an optional `patterns:` list — their declared namespace (omitted = every name) — and a router collapses to an ordered `sources:` list: a package resolves to the first listed source whose patterns claim it. The namespace is enforced at the mount, on every path to it: an off-pattern read is a definitive 404 answered before storage or the upstream is consulted, and an off-pattern publish is rejected with a clear reason — through a router and at the mount's own `/~<mount>/` URL alike. This closes the open hosted namespace (no dormant stored state that a later source edit would surface as authoritative) and stops an authorized caller from pulling arbitrary public names through a private upstream's server-owned credential. Validation translates to the same `covers()` machinery: unreachable sources (all claims covered by earlier sources' union, including a non-last pattern-less source), per-pattern shadowing across sources (identical claims by two sources rejected, so bidirectionally-overlapping namespaces fail in either order), duplicate sources per router, duplicate patterns per mount, plus the carried-over unknown/self-referential/ router-as-source/empty-router checks. A mount's own internally redundant patterns are allowed and do not count as self-shadowing. Patterns live only in the mount graph (`Config::mounts`), not duplicated into the hosted/uplink tables: enforcement happens in `Mounts::resolve`, which every read, write, search, and cache-header decision flows through, so the namespace is one declaration. The bundled config.yaml moves the registry-mock fixture list onto the `local` mount, and `Config::proxy` / `Config::static_serve` build the equivalent graphs programmatically.
|
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:
📝 WalkthroughWalkthroughThis PR replaces pnpr’s mount/router model with registry-based routing. Hosted and upstream registries now declare patterns, routers declare ordered sources, resolution can return Unclaimed, and config, server dispatch, tests, and docs are updated accordingly. ChangesRegistry routing refactor
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: ✨ 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 |
PR Summary by QodoMove package patterns to mounts; simplify routers to ordered sources
AI Description
Diagram
High-Level Assessment
Files changed (10)
|
Code Review by Qodo
1. Disable-registry skips upstream auth
|
There was a problem hiding this comment.
Pull request overview
This PR updates pnpr’s registry-mount routing model to the revised design from pnpm/rfcs#16: patterns are declared on concrete mounts (hosted/upstream) and routers become ordered sources that select the first source whose declared namespace claims a package.
Changes:
- Move package-name pattern declarations onto concrete mounts (
Hosted/Upstream) and replace routerrouteswith orderedsources. - Enforce declared namespaces in mount resolution (reads 404 early; publishes reject off-pattern names), update server dispatch and config parsing accordingly.
- Update bundled
config.yamland rework/unit/e2e tests to cover new validation and enforcement behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| pnpr/crates/pnpr/tests/server.rs | Updates server integration tests for mount-level patterns + router sources; adds e2e coverage for off-pattern behavior. |
| pnpr/crates/pnpr/tests/policy.rs | Updates YAML test fixture to new router sources shape. |
| pnpr/crates/pnpr/tests/auth_publish.rs | Updates YAML test fixture to new router sources shape. |
| pnpr/crates/pnpr/src/server.rs | Implements Unclaimed resolution semantics and adjusts server request/publish/search routing accordingly. |
| pnpr/crates/pnpr/src/mount/tests.rs | Reworks mount unit tests for new mount/namespace model and router validation rules. |
| pnpr/crates/pnpr/src/mount.rs | Core model change: patterns on mounts, routers as ordered sources; new validation for namespaces/sources. |
| pnpr/crates/pnpr/src/lib.rs | Updates public re-exports to remove removed Route type. |
| pnpr/crates/pnpr/src/config/tests.rs | Updates config parsing tests for mount patterns and router sources, and adds new validation tests. |
| pnpr/crates/pnpr/src/config.rs | Updates YAML schema + config graph building to compile mount patterns and router source lists. |
| pnpr/crates/pnpr/config.yaml | Updates bundled config to declare fixture namespace on local mount and use router sources. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Code review by qodo was updated up to the latest commit bad1e51 |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #12778 +/- ##
==========================================
+ Coverage 85.56% 85.59% +0.02%
==========================================
Files 413 413
Lines 63986 64125 +139
==========================================
+ Hits 54750 54888 +138
- Misses 9236 9237 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Integrated-Benchmark Report (Linux)Commit: Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD. Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.132803087959999,
"stddev": 0.23893096917329243,
"median": 4.00712985986,
"user": 3.8980312399999995,
"system": 3.5191763799999998,
"min": 3.92098871686,
"max": 4.57966316986,
"times": [
4.02121979086,
4.51175222986,
3.9822780038600003,
3.92098871686,
4.57966316986,
3.9930399288600005,
3.9661987358600004,
4.17598211386,
4.22528040986,
3.9516277798600004
]
},
{
"command": "pacquet@main",
"mean": 3.91748550716,
"stddev": 0.028483618457604817,
"median": 3.91430816986,
"user": 3.8935976399999994,
"system": 3.47356438,
"min": 3.8724047828600003,
"max": 3.9587695638600002,
"times": [
3.91672684786,
3.92404722386,
3.9071245658600002,
3.87986822986,
3.9118894918600002,
3.9587695638600002,
3.9348945588600004,
3.91131236286,
3.8724047828600003,
3.9578174438600002
]
},
{
"command": "pnpr@HEAD",
"mean": 2.16410423726,
"stddev": 0.1409353340985199,
"median": 2.19553172836,
"user": 2.70169914,
"system": 3.0266663799999995,
"min": 1.94411268786,
"max": 2.37759562986,
"times": [
2.27779753286,
2.22252947686,
2.24455834986,
2.06304695686,
1.94411268786,
2.37759562986,
2.02804877986,
2.02867604886,
2.1685339798600003,
2.28614292986
]
},
{
"command": "pnpr@main",
"mean": 2.1728785440600005,
"stddev": 0.15272441729036004,
"median": 2.14095810186,
"user": 2.72830294,
"system": 3.0338863799999998,
"min": 1.96015110786,
"max": 2.37987659186,
"times": [
2.11530405286,
2.37987659186,
2.1666121508600003,
2.0613356568600003,
2.08294907986,
2.35305914486,
1.96015110786,
2.0208295288600002,
2.37449490986,
2.2141732168600003
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.6648153428400001,
"stddev": 0.007711800992298111,
"median": 0.66599094424,
"user": 0.39193982,
"system": 1.3550160799999997,
"min": 0.6519938337400001,
"max": 0.6787018597400001,
"times": [
0.65422389674,
0.66140317674,
0.66874148274,
0.6701242727400001,
0.66733635274,
0.66364666474,
0.6519938337400001,
0.6787018597400001,
0.6657520637400001,
0.66622982474
]
},
{
"command": "pacquet@main",
"mean": 0.65907774184,
"stddev": 0.023008274828784935,
"median": 0.6555649752400001,
"user": 0.39166782,
"system": 1.3468110799999997,
"min": 0.63019432574,
"max": 0.69845219774,
"times": [
0.67969501474,
0.63019432574,
0.6374527427400001,
0.6697477767400001,
0.6635430357400001,
0.6456738977400001,
0.69845219774,
0.68203664174,
0.6475869147400001,
0.63639487074
]
},
{
"command": "pnpr@HEAD",
"mean": 0.71324242194,
"stddev": 0.06045258298931623,
"median": 0.69746904674,
"user": 0.39846851999999994,
"system": 1.3676737799999998,
"min": 0.67378063874,
"max": 0.8818238927400001,
"times": [
0.71213005974,
0.68087737674,
0.6926342577400001,
0.68640825874,
0.67378063874,
0.70230383574,
0.70438872774,
0.70674078474,
0.6913363867400001,
0.8818238927400001
]
},
{
"command": "pnpr@main",
"mean": 0.75734462784,
"stddev": 0.11764972416361932,
"median": 0.7311573032400001,
"user": 0.41714182000000005,
"system": 1.3684525799999998,
"min": 0.6738796957400001,
"max": 1.0847374977400002,
"times": [
0.73672752474,
0.73714935274,
0.6932996907400001,
0.7069941767400001,
0.7418171477400001,
0.7255870817400001,
0.7161000457400001,
0.6738796957400001,
1.0847374977400002,
0.75715406474
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 4.209638357920001,
"stddev": 0.042801835375105624,
"median": 4.20522365952,
"user": 3.81144794,
"system": 3.41113402,
"min": 4.15286857952,
"max": 4.27632091552,
"times": [
4.25874273552,
4.23682876352,
4.20495451052,
4.16168652952,
4.16360877952,
4.19477764952,
4.20549280852,
4.24110230752,
4.27632091552,
4.15286857952
]
},
{
"command": "pacquet@main",
"mean": 4.22348999102,
"stddev": 0.04997491949507371,
"median": 4.21914707802,
"user": 3.82117284,
"system": 3.3811452199999996,
"min": 4.15391399652,
"max": 4.30679720052,
"times": [
4.30679720052,
4.15618394052,
4.23137876252,
4.20840104352,
4.24527048552,
4.20231176652,
4.29234855852,
4.15391399652,
4.22848459152,
4.2098095645199995
]
},
{
"command": "pnpr@HEAD",
"mean": 2.2262647297200004,
"stddev": 0.1278582390548118,
"median": 2.23022324502,
"user": 2.53762944,
"system": 2.9155545199999997,
"min": 2.00435848852,
"max": 2.44213310352,
"times": [
2.21926058152,
2.06325200452,
2.22602393752,
2.00435848852,
2.44213310352,
2.27970930052,
2.1617972605199998,
2.34357205752,
2.23442255252,
2.28811801052
]
},
{
"command": "pnpr@main",
"mean": 2.2126045549200004,
"stddev": 0.12620057544592125,
"median": 2.20397561352,
"user": 2.5281956399999994,
"system": 2.9032915199999993,
"min": 1.96626067252,
"max": 2.36205676252,
"times": [
2.17832078552,
2.32162471352,
2.1224110015199997,
2.22963044152,
2.35369061152,
2.1302530485199997,
1.96626067252,
2.30664855852,
2.36205676252,
2.15514895352
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.34863816302,
"stddev": 0.010731992671848637,
"median": 1.35004765892,
"user": 1.3438190999999997,
"system": 1.70457026,
"min": 1.32773397142,
"max": 1.36140856942,
"times": [
1.35859176642,
1.34570581842,
1.36140856942,
1.35155780642,
1.3369724274199999,
1.32773397142,
1.34272457042,
1.3531634854199999,
1.34853751142,
1.35998570342
]
},
{
"command": "pacquet@main",
"mean": 1.3645345782199998,
"stddev": 0.02263309734664884,
"median": 1.36034160192,
"user": 1.3635941999999999,
"system": 1.6975356599999998,
"min": 1.33732899142,
"max": 1.42390719242,
"times": [
1.35413149742,
1.36242232342,
1.42390719242,
1.35272851242,
1.36111581042,
1.35878686542,
1.36906186742,
1.33732899142,
1.35956739342,
1.36629532842
]
},
{
"command": "pnpr@HEAD",
"mean": 0.6756159211199999,
"stddev": 0.023124297273371616,
"median": 0.66982695442,
"user": 0.3503142,
"system": 1.30119806,
"min": 0.64862165642,
"max": 0.73129544542,
"times": [
0.66022618542,
0.64862165642,
0.69208660942,
0.66471746542,
0.67551407642,
0.66524694942,
0.73129544542,
0.66148690842,
0.6744069594200001,
0.68255695542
]
},
{
"command": "pnpr@main",
"mean": 0.6949399138200001,
"stddev": 0.08000637164866568,
"median": 0.67040770842,
"user": 0.36038479999999995,
"system": 1.3065599599999997,
"min": 0.6604173894200001,
"max": 0.92198416442,
"times": [
0.67391511042,
0.66477660842,
0.66829120342,
0.66359021442,
0.68042939542,
0.67252421342,
0.66764039442,
0.6604173894200001,
0.6758304444200001,
0.92198416442
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 3.0708536526600003,
"stddev": 0.04606240923441474,
"median": 3.06598358806,
"user": 1.87882106,
"system": 1.96899482,
"min": 3.01280308806,
"max": 3.18375413906,
"times": [
3.0672935210600003,
3.0954390220600003,
3.01280308806,
3.0290214730600002,
3.0673695490600004,
3.06467365506,
3.0645306690600003,
3.07522265806,
3.18375413906,
3.04842875206
]
},
{
"command": "pacquet@main",
"mean": 3.0065092836600003,
"stddev": 0.05840934056330872,
"median": 2.98630121006,
"user": 1.82761846,
"system": 1.9691463199999997,
"min": 2.96774719406,
"max": 3.15988444706,
"times": [
2.98716005606,
2.98133938106,
2.99259882606,
2.9854423640600003,
2.99993590506,
2.9744325550600004,
3.0467131320600003,
2.96774719406,
3.15988444706,
2.96983897606
]
},
{
"command": "pnpr@HEAD",
"mean": 0.68837921786,
"stddev": 0.01311105811603398,
"median": 0.6880676295600001,
"user": 0.36284426000000003,
"system": 1.33291112,
"min": 0.66432650006,
"max": 0.7073632870600001,
"times": [
0.68724519506,
0.7014557140600001,
0.6798910490600001,
0.6790667680600001,
0.7073632870600001,
0.6888900640600001,
0.69867098806,
0.6976860290600001,
0.66432650006,
0.67919658406
]
},
{
"command": "pnpr@main",
"mean": 0.68233963456,
"stddev": 0.012826080521768786,
"median": 0.67972263056,
"user": 0.35648666,
"system": 1.30093372,
"min": 0.66566041606,
"max": 0.7062995900600001,
"times": [
0.6888916800600001,
0.67956709406,
0.6957779460600001,
0.67987816706,
0.7062995900600001,
0.6886458770600001,
0.6736918370600001,
0.6789154930600001,
0.6660682450600001,
0.66566041606
]
}
]
}Scenario: Isolated linker: fresh restore, cold cache + cold store + cold pnpr
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 6.95953857038,
"stddev": 0.12422953550992917,
"median": 6.96094832118,
"user": 4.09414252,
"system": 3.81877712,
"min": 6.7423204631800004,
"max": 7.11655181318,
"times": [
7.00599036818,
6.91149953818,
7.11655181318,
7.06761732818,
7.05408679818,
7.07512589918,
6.7423204631800004,
6.80803237718,
6.91590627418,
6.89825484418
]
},
{
"command": "pacquet@main",
"mean": 6.912692728080001,
"stddev": 0.18923681774253898,
"median": 6.87235112318,
"user": 4.117097719999999,
"system": 3.816914419999999,
"min": 6.68630813818,
"max": 7.35120335418,
"times": [
6.7657282181800005,
6.93437559918,
7.10526233018,
6.8034008661800005,
6.88034520218,
6.87929552318,
6.8654067231800004,
6.85560132618,
7.35120335418,
6.68630813818
]
},
{
"command": "pnpr@HEAD",
"mean": 5.02960201648,
"stddev": 0.12981345720776577,
"median": 5.04314964818,
"user": 2.83642432,
"system": 3.25297472,
"min": 4.83577668818,
"max": 5.27285732218,
"times": [
5.0400219261800006,
5.04627737018,
5.08521225918,
5.27285732218,
5.01510183618,
5.1287148921800005,
4.9294755201800005,
5.08216051118,
4.86042183918,
4.83577668818
]
},
{
"command": "pnpr@main",
"mean": 4.96600401808,
"stddev": 0.12104569888949136,
"median": 4.937430407180001,
"user": 2.8197785200000003,
"system": 3.2553950200000004,
"min": 4.81164813218,
"max": 5.15352370718,
"times": [
4.88758238818,
5.15352370718,
4.94260251918,
4.89283982418,
5.0796685341800005,
4.95517028618,
4.81164813218,
4.8540368241800005,
5.15070967018,
4.9322582951800005
]
}
]
} |
|
| Branch | pr/12778 |
| Testbed | pnpr |
⚠️ WARNING: No Threshold found!Without a Threshold, no Alerts will ever be generated.
Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the--ci-only-thresholdsflag.
Click to view all benchmark results
| Benchmark | Latency | milliseconds (ms) |
|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot | 2,226.26 ms |
| isolated-linker.fresh-install.cold-cache.hot-store | 📈 view plot | 688.38 ms |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot | 675.62 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot | 2,164.10 ms |
| isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr | 📈 view plot | 5,029.60 ms |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot | 713.24 ms |
pnpm/rfcs#16 was updated to rename the config surface: every explanation of a mount began "a mount is a full npm registry at...", so the unit is now simply a **registry**. `mounts:` becomes `registries:` and `defaultTarget:` becomes `defaultRegistry:` (npm's own concept), aligning the server with the client-side `namedRegistries`. pnpr is pre-1.0, so the old keys are replaced outright with no compatibility mode. The Rust surface follows the RFC's reference sketch: `MountKind` → `Registry`, `Mounts` → `Registries`, `MountConfigError` → `RegistryConfigError`, the `mount` module → `registry`, `Config.mounts` → `Config.registries`, and prose/error messages now say "registry" ("name a hosted registry", `/~<name>/`). The axum-level "routes are mounted" wording is kept — that is framework vocabulary, not the renamed concept. The vocabulary is now: **registry** — a named surface pnpr serves; **origin** — the external URL an upstream registry fetches from; "uplink" remains only as the internal upstream-backend term.
|
Code review by qodo was updated up to the latest commit 2710c1c |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
pnpr/crates/pnpr/src/registry.rs (1)
262-309: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider unifying the repeated Hosted/Upstream dispatch.
The
Hosted/Upstreamnamespace-check-then-classify logic is duplicated four times: twice in the top-level match (lines 267-280) and twice more in the router loop (lines 287-302). A small helper returning(ConcreteKind, &[PackagePattern])for a concreteRegistrywould collapse all four sites to one check.♻️ Sketch of a possible helper
impl Registry { fn is_concrete(&self) -> bool { matches!(self, Registry::Hosted { .. } | Registry::Upstream { .. }) } + + /// The registry's `(ConcreteKind, patterns)` if it is concrete. + fn concrete(&self) -> Option<(ConcreteKind, &[PackagePattern])> { + match self { + Registry::Hosted { patterns } => Some((ConcreteKind::Hosted, patterns)), + Registry::Upstream { patterns } => Some((ConcreteKind::Upstream, patterns)), + Registry::Router { .. } => None, + } + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pnpr/crates/pnpr/src/registry.rs` around lines 262 - 309, The namespace claim and concrete-kind selection logic in resolve is duplicated across the Hosted, Upstream, and Router branches. Add a small helper on Registry (or nearby) that maps a concrete Registry variant to its ConcreteKind and patterns slice, then use it in resolve to perform the namespace_claims check and return Resolved::Concrete in one shared path. Keep the Router loop behavior the same, but have it reuse the helper for each source so the Hosted/Upstream dispatch lives in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@pnpr/crates/pnpr/src/config.rs`:
- Around line 1504-1511: The inline comment in config.rs was mangled by the
mounts-to-registries rename and now contains a nonsensical phrase in the
resolver-only server note. Update that explanatory comment near build_registries
so it clearly states that a resolver-only server skips upstream credential
resolution because it does not expose registry routes, while still building and
validating the registry graph.
---
Nitpick comments:
In `@pnpr/crates/pnpr/src/registry.rs`:
- Around line 262-309: The namespace claim and concrete-kind selection logic in
resolve is duplicated across the Hosted, Upstream, and Router branches. Add a
small helper on Registry (or nearby) that maps a concrete Registry variant to
its ConcreteKind and patterns slice, then use it in resolve to perform the
namespace_claims check and return Resolved::Concrete in one shared path. Keep
the Router loop behavior the same, but have it reuse the helper for each source
so the Hosted/Upstream dispatch lives in one place.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4177bdd1-58de-4fc9-aaea-31aa34ec175a
📒 Files selected for processing (16)
pnpr/crates/pnpr/config.yamlpnpr/crates/pnpr/src/config.rspnpr/crates/pnpr/src/config/tests.rspnpr/crates/pnpr/src/journal.rspnpr/crates/pnpr/src/lib.rspnpr/crates/pnpr/src/main.rspnpr/crates/pnpr/src/registry.rspnpr/crates/pnpr/src/registry/tests.rspnpr/crates/pnpr/src/s3.rspnpr/crates/pnpr/src/search.rspnpr/crates/pnpr/src/server.rspnpr/crates/pnpr/src/storage.rspnpr/crates/pnpr/src/upstream.rspnpr/crates/pnpr/tests/auth_publish.rspnpr/crates/pnpr/tests/policy.rspnpr/crates/pnpr/tests/server.rs
✅ Files skipped from review due to trivial changes (6)
- pnpr/crates/pnpr/src/search.rs
- pnpr/crates/pnpr/src/upstream.rs
- pnpr/crates/pnpr/src/main.rs
- pnpr/crates/pnpr/src/journal.rs
- pnpr/crates/pnpr/src/s3.rs
- pnpr/crates/pnpr/src/storage.rs
🚧 Files skipped from review as they are similar to previous changes (1)
- pnpr/crates/pnpr/tests/auth_publish.rs
|
Code review by qodo was updated up to the latest commit 2710c1c |
…ublishes (review) Two review findings on #12778: Serving dispatch had a fallback that treated any `Config::uplinks` key as an addressable upstream when the registry graph did not know the name, so an embedder-inserted uplink was served at `/~<name>/` without ever passing through `Registries::resolve` — an unbounded, unvalidated side door past the namespace model. Server construction (`try_router*`, `serve`, `serve_listener`) now folds every uplink into the graph as a pattern-less upstream registry and validates the whole graph (`Config::ensure_valid_registry_graph`), then the per-request fallback is gone: the graph is the only dispatch table, a programmatically-built config fails closed like a YAML load, and an embedder can bound an uplink by declaring its registry entry (with patterns) before serving. The off-pattern publish rejection fired before the hosted access mask, so probing `/~<name>/` with an unclaimed name distinguished a private hosted registry (400) from an undefined one (404). The loud rejection is now gated on `registry_visible_to_caller`: hosted registries stay masked behind their access list (the write path answers exactly like a read), upstream registries are not masked (their reads already 403), and a router is visible whenever one of its sources is.
|
Both Qodo findings are fixed in 1b50e94:
All 571 pnpr tests plus the pacquet Written by an agent (Claude Code, claude-fable-5). |
|
Code review by qodo was updated up to the latest commit 1b50e94 |
…y (review) `PackagePattern::parse` accepted `@<scope>/*` shapes whose scope request parsing (`PackageName::parse`) rejects — `@.acme/*`, `@../*`, a scope with a separator or `:` — producing a claim no valid package name can ever match. A mistyped private-scope claim would then be a dead pattern that silently lets the names it was meant to cover land on a later (often public) router source. The scope segment is now validated with the same rule request parsing applies, and an invalid one is the new `ScopePatternNotAScope` config error, mirroring `ExactPatternNotAName`. Also repairs one comment the mechanical mounts-to-registries rename garbled in config.rs.
The cold-pnpr benchmark scenario drives every benchmarked pnpr revision with one cold-mock config that until now carried two routing shapes: `mounts:`/`defaultTarget:` for the mount-model servers and the legacy `uplinks:` + `packages: proxy:` for pre-mount binaries. A pnpr built from this branch reads `registries:`/`defaultRegistry:` and ignores both older blocks, so it derived "no registries declared", started resolver-only, and 404d every install — hyperfine aborted the scenario at its first benchmark (the CI failure on this PR). The config now carries all three shapes; every revision ignores the blocks it does not recognize (no revision sets `deny_unknown_fields` on its config file), so each one proxies to the same origin. Verified by launching this branch's pnpr with the tri-shape config: the registry surface comes up and serves a proxied packument.
|
Code review by qodo was updated up to the latest commit 8851f26 |
|
Code review by qodo was updated up to the latest commit 8851f26 |
…nfigs (review) `Config::ensure_valid_registry_graph` now mirrors the remaining YAML upstream invariant: an upstream with no `access:` gate is publicly reachable at its `/~<name>/` endpoint, so it may not send custom request headers — any header can carry a credential, and an ungated endpoint would let every caller spend it (a confused deputy). YAML loads already reject this shape in `resolve_upstream_registry`; programmatic configs now fail server construction the same way. The header-forwarding e2e test gains the access gate (and an authenticated caller) the invariant demands. Also renames the `UPLINK_TOKEN` test constant the case-sensitive uplinks-to-upstreams sweep missed.
|
Code review by qodo was updated up to the latest commit cf3aa85 |
… configs (review) The uplink fold in `Config::ensure_valid_registry_graph` is a no-op when the name already exists in the graph, so a programmatic config could leave upstream serving config under a name the graph declares as hosted or a router — two different origins behind one `/~<name>/` identity, with the dormant upstream's credential still offered by the resolver. The fold now requires the existing entry to actually be an upstream, and the hosted table gets the mirror check (a hosted serving row under a name the graph declares as a different kind). A hosted row with no graph entry at all stays allowed — it is merely dormant, nothing routes to it. YAML loading already rejects these collisions while building the graph.
|
Code review by qodo was updated up to the latest commit cf3aa85 |
|
Code review by qodo was updated up to the latest commit 934f28a |
…nfig The `REGISTRY_MOCK_LOCAL_PATTERNS` and `Config::proxy` docs claimed the programmatic pattern set is kept in sync with the bundled `config.yaml` `local` registry, but the YAML additionally claims the exact names the TS test suite publishes — names pacquet's in-process registry never sees. Both docs now say the sync target is the fixture subset. Also updates the registry-mock launcher comment describing what the bundled config serves, and fixes an "a upstream" article the rename left behind.
|
Code review by qodo was updated up to the latest commit 934f28a |
|
Code review by qodo was updated up to the latest commit 7fa37d4 |
| let mut upstreams: IndexMap<String, UpstreamConfig> = IndexMap::new(); | ||
| let (hosted, registries) = build_registries( | ||
| &mut upstreams, | ||
| file.registries, | ||
| file.default_registry, | ||
| registry.enabled, | ||
| )?; |
There was a problem hiding this comment.
1. Disable-registry skips upstream auth 🐞 Bug ☼ Reliability
Config::from_yaml_str_with_overrides passes registry.enabled into build_registries as resolve_upstreams, so --disable-registry leaves Config.upstreams empty even when the resolver is enabled. The resolver’s RouteHook ignores client-forwarded auth headers and derives server-managed credentials from Config.upstreams, so private/proxied resolutions will fail (and won’t be routable/cached correctly).
Agent Prompt
### Issue description
`--disable-registry` is intended to disable serving the npm-registry HTTP surface, but it currently also prevents upstream registry credentials/config from being resolved into `Config.upstreams`. The resolver still installs a route hook that ignores client-forwarded auth and relies on `Config.upstreams` to select server-managed credentials for proxied routes, so private dependency resolution breaks.
### Issue Context
- `Config::from_yaml_str_with_overrides` uses `registry.enabled` to decide whether to resolve upstream registries into `Config.upstreams`.
- `RouteContext::from_config` builds its proxied-alias table from `config.upstreams`.
- The resolver attaches a route hook (`RouteHook`) to `AuthHeaders`, and pacquet’s `AuthHeaders` contract is: when a route hook is present, client credentials are ignored.
### Fix Focus Areas
- pnpr/crates/pnpr/src/config.rs[1502-1526]
- pnpr/crates/pnpr/src/config.rs[1724-1791]
### Proposed fix
- Decouple “serve registry routes” from “resolve upstream configs needed by the resolver’s route hook”.
- Pass `resolve_upstreams = registry.enabled || resolver.enabled` (or an equivalent condition) into `build_registries`, so upstream credentials are resolved whenever the resolver is enabled.
- Optionally update related comments/docs so they don’t claim a resolver-only tier ‘never uses’ upstream secrets, since the route hook can require them.
- Consider updating `ensure_valid_registry_graph` invariants if they currently only require upstream backing config when `registry.enabled` is true, but resolver behavior requires them too.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit 7fa37d4 |
…rams-auz8x1 Integrate the pnpr registry-declaration refactor from main (#12778): mount -> registry, Uplink* -> Upstream*, and the server/route/resolver rewrite. Conflicts in the pnpr sources and the pnpr-client/pnpr integration tests were resolved in favor of main's new structure. Re-ran perfectionist::needless_borrowed_parameters over the merged tree and fixed the findings in main's new code the same way as the rest of the branch: production functions (upstream_cache_digest, ResolvedAlias::from_upstream, tarball_integrity_error, Upstream::new) take their String argument by value, with move-or-clone call sites; test fixtures widen to impl Into<String>, or to impl AsRef<str> / impl AsRef<Path> where the helper has a large borrowed-arg fan-out, satisfying the lint without call-site churn. Verified on the merge: cargo dylint (0 findings), cargo clippy --all-targets -D warnings, RUSTDOCFLAGS=-D warnings cargo doc, cargo fmt --check, taplo format --check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnR7HFwhaS9Y5NH56X3MCD
…maps (#834) Documents the redesigned model from pnpm/pnpm#12773, pnpm/pnpm#12778, and pnpm/pnpm#12787: - mounts:/defaultTarget: -> registries:/defaultRegistry: - routers collapse to ordered sources: lists; a package resolves to the first listed source whose declared packages claim it - the namespace and ACL merge into per-registry packages: maps, with most-specific-key selection and namespace enforcement on every path - the registry: config toggle is gone; the npm-registry surface is served iff at least one registry is declared, minus --disable-registry - the account (login/token) endpoints are always served on every tier
Summary
Implements the revised model from pnpm/rfcs#16 in pnpr, replacing the route-level-pattern shape (landed by #12747) outright — pnpr is pre-1.0, so there is no compatibility mode and no migration for the old keys.
registries:/defaultRegistry:(wasmounts:/defaultTarget:), and the Rust surface follows the RFC's sketch (MountKind→Registry,Mounts→Registries,MountConfigError→RegistryConfigError, themountmodule →registry,Config.mounts→Config.registries, "name a hosted registry" errors,/~<name>/in docs). The axum-level "routes are mounted" wording stays — that's framework vocabulary, not the renamed concept.patterns:list (same four-shape decidable language:**,@*/*,@scope/*, exact name; omitted = every name). This is the registry's declared namespace — its routing claim and its request filter in one declaration.sources:lists. A package resolves to the first listed source whose patterns claim it, authoritatively. A pattern-less source claims every name (the catch-all) and must be listed last. A router can order competing claims but can never assign a name to a registry that doesn't claim it./~<name>/URL alike. This closes the open hosted namespace (no dormant stored state that a later config edit or direct address would surface as authoritative, and a typo'd scope fails loudly at publish time) and stops an authorized caller from pulling arbitrary public names through a private upstream's server-owned credential.covers()machinery: unreachable sources (all claims covered by earlier sources' union, including a non-last pattern-less source), per-pattern shadowing across sources (identical claims by two sources are rejected, so namespaces that overlap in both directions fail in either order — genuinely ambiguous provenance), duplicate sources per router, duplicate patterns per registry, plus the carried-over unknown-source / self-reference / router-as-source / empty-router checks. A registry's own internally redundant patterns are allowed and don't count as self-shadowing.Implementation choice: patterns live only in the registry graph (
Config::registries), not duplicated into the hosted/uplink tables — enforcement happens inRegistries::resolve, which every read, write, search, and cache-header decision already flows through, so the namespace really is one declaration. The bundledconfig.yamlmoves the registry-mock fixture list onto thelocalregistry (mainbecomessources: [local, npmjs]), andConfig::proxy/Config::static_servebuild the equivalent graphs programmatically.Tests: the registry unit suite is reworked for the new shapes and validation matrix; config tests cover registry-level
patterns:parsing, duplicate/invalid pattern rejection, theregistries:/defaultRegistry:keys, and the misordered-sources:startup failure; new server e2e tests pin off-pattern publish rejection and read 404s on every path (with anexpect(0)mock proving a bounded upstream is never contacted for an unclaimed name), plus the existing no-fall-through, source-down-is-5xx, masking, and cache-header suites. 575 pnpr tests pass, and the pacquet e2e tests that drive a live pnpr server (pnpr_install) pass. A review round hardened two edges: server construction now folds every programmatic uplink into the registry graph and validates it (the per-request uplink fallback in dispatch is gone, soRegistries::resolveis the only dispatch table), and the off-pattern publish rejection is gated on registry visibility so a denied caller gets the same 404 mask as on reads instead of a private-registry-revealing 400. Further rounds validate programmatic configs exactly like YAML loads (URL-safe names, path-safe and collision-free hosted orgs, scope patterns no package name could match, and concrete graph entries without backing serving config all fail startup), gate writes to private upstreams on theiraccess:list (the read path's 403) before any routing rejection, and rename the remaining verdaccio-era "uplink" vocabulary to "upstream" (UpstreamConfig,Config.upstreams, the~upstreams/private cache namespace) — "uplink" survives only where compatibility demands it (the_uplinksverdaccio storage field and the legacyuplinks:YAML the benchmark emits for pre-registries binaries).Squash Commit Body
Checklist
pacquet/counterpart to mirror (perpnpr/AGENTS.md), and per the RFC there are no lockfile or client-visible protocol changes.config.yamlcomments and module docs; the design doc is the RFC itself, rfc(pnpr): registries declare their namespace; rename mounts to registries rfcs#16).Written by an agent (Claude Code, claude-fable-5).