Skip to content

feat(pnpr): merge the namespace and the ACL into per-registry packages: maps (RFC pnpm/rfcs#17)#12787

Merged
zkochan merged 6 commits into
mainfrom
pnpr-registry-packages
Jul 3, 2026
Merged

feat(pnpr): merge the namespace and the ACL into per-registry packages: maps (RFC pnpm/rfcs#17)#12787
zkochan merged 6 commits into
mainfrom
pnpr-registry-packages

Conversation

@zkochan

@zkochan zkochan commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

Implements the revised registries model from pnpm/rfcs#17 as a full replacement of the interim patterns:-plus-global-ACL shape (pre-1.0, no compatibility mode).

One declaration: the per-registry packages: map

Every concrete registry (hosted or upstream) declares one packages: map whose keys are its namespace (replacing the patterns: list) and whose values are the per-package access/publish/unpublish rules (replacing the top-level packages: ACL, scoped to the one registry that serves the name). One declaration routes, filters, and authorizes. The registry-level access: is the default an entry's omitted fields fall back to; publish defaults to $authenticated and unpublish to nobody.

registries:
  local:
    type: hosted
    access: $all
    packages:
      '@private/*': { access: $authenticated, publish: $authenticated, unpublish: $authenticated }
      '@pnpm.e2e/*': { unpublish: $authenticated }
      '@pnpm.e2e/needs-auth': { access: $authenticated, publish: $authenticated, unpublish: $authenticated }

Specificity selection — key order carries no meaning

The most specific matching key wins: an exact name beats @scope/* beats @*/* beats **. YAML mappings are formally unordered, so a formatter or yq round-trip must not change which access rule applies; the restricted pattern language makes the winner unique (at most one matching key per tier), no entry can be dead, and a duplicate key is the only within-registry error. Router sources: stay an ordered list with their unreachable-source/shadowed-claim validation intact.

The removed global ACL fails loudly

A config still containing a top-level packages: block is a startup error naming the per-registry replacement. It used to enforce access, so dropping it like an unknown verdaccio key would silently open previously gated packages on upgrade.

Upstream rules

public: true keeps describing the upstream fetch (no credential, no headers, no registry-level access: default), but per-package access rules are now permitted on a public upstream — they gate who may read the name through pnpr, not how pnpr fetches it. publish/unpublish values on any upstream are rejected on every tier: no write can land there.

Hosted denials answer by tier

The decision is always the effective per-package access — an explicit entry fully decides its names, including opening one name (access: $all) on an otherwise-private registry. Only the shape of a denial is tiered, preserving both prior behaviors through one mechanism: a caller the registry-level default also denies is masked with 404 — a blanket-private registry never confirms which names exist, even explicitly ruled ones — while an explicit entry denying a caller the default admits rejects loudly (401 anonymous / 403 authenticated), so clients can prompt for credentials (the registry-mock needs-auth contract the pnpm e2e suite relies on).

Resolver classification follows the graph

Path-less fetches classify through the registry graph — the same dispatch serving uses — so a fetch resolving to an upstream source classifies through that upstream's alias, and hosted private-access descriptors are registry-qualified (registry\0package): the same name@version on two hosted registries can never share a cache key. Unqualified descriptors from older builds fail closed and re-resolve.

Review-round additions

  • Per-package-aware alias selection (route classification): a caller the upstream's effective access denies for a name is never handed the server-owned credential — fresh resolves fail closed where serving would deny; cache replay stays registry-scoped by design (documented at the descriptor gate).
  • Indexed rule lookup: for_package resolves the winner via per-tier maps instead of scanning every rule.
  • Search fast path restored: a hosted registry no rule of which could admit the caller is skipped without a storage scan, so the blanket mask can't be used for enumeration or scan-timing.

Config surface

The bundled config.yaml moves the fixture namespace and old ACL into the local registry's map (YAML anchors keep the per-key unpublish contract readable); Config::proxy / Config::static_serve carry the registry-mock rules programmatically; registry-mock keeps working with no task or fixture-seed changes.

Checklist

  • pnpr is a standalone server; the RFC's client/lockfile changes are deliberately out of scope (implements the pnpr side of rfc(pnpr): merge the namespace and the ACL into a per-registry packages: map rfcs#17 only).
  • No changeset: server-only change, no published npm package behavior changes.
  • Added or updated tests: specificity selection (including key-reorder stability via YAML), top-level packages: rejection, upstream write-rule rejection, public-upstream per-package access, upstream namespace bounds, tiered hosted denials; existing suites migrated to the map shape. 585 pnpr tests pass; clippy, rustfmt, and rustdoc clean; workspace check clean.
  • Updated the documentation (bundled config.yaml documents the map shape).

Written by an agent (Claude Code, claude-fable-5).

Summary by CodeRabbit

  • New Features
    • Per-registry packages rules now drive access, publish, and unpublish for both hosted and upstream registries.
    • Rule matching is deterministic by specificity (exact, scope, scoped-wildcard, catch-all), including registry default fallback behavior.
  • Bug Fixes
    • Authorization and privacy gating are now based on the resolved concrete registry source, improving private access correctness and masking.
    • Configuration validation now rejects unsupported top-level package declarations and invalid/duplicate packages entries.
  • Tests
    • Updated and expanded coverage for the new rules-driven YAML shape, routing/authorization semantics, and publish/write gating behavior.

…s: maps

Implements the revised model from pnpm/rfcs#17, replacing the interim
patterns:-plus-global-ACL shape outright (pre-1.0, no compatibility
mode).

Every concrete registry now declares one packages: map whose keys are
its namespace (the former patterns: list) and whose values are the
per-package access/publish/unpublish rules (the former top-level
packages: block, scoped to the one registry that serves the name). One
declaration routes, filters, and authorizes. The registry-level access:
is the default an entry's omitted fields fall back to; publish defaults
to $authenticated and unpublish to nobody.

Selection is by specificity, not key order: an exact name beats
@scope/* beats @*/* beats **. YAML mappings are formally unordered, so
a formatter or yq round-trip must not change which rule applies; the
restricted pattern language makes the winner unique (at most one
matching key per tier), no entry can be dead, and a duplicate key is
the only within-registry error. Router sources: stay an ordered list
with their unreachable/shadowed-claim validation intact.

The removed top-level packages: block is a startup error naming the
per-registry replacement — it used to enforce access, so dropping it
like an unknown verdaccio key would silently open previously gated
packages on upgrade.

public: true keeps meaning the upstream fetch (no credential, no
headers, no registry-level access default), but per-package access
rules are now permitted on a public upstream — they gate who may read
the name through pnpr, not how pnpr fetches it. publish/unpublish
values on any upstream are rejected: no write can land there.

Hosted denials answer by tier, preserving both prior behaviors: a
caller the registry-level default denies is masked with 404 for every
name (a blanket-private registry never reveals which names exist, even
explicitly ruled ones), while an explicit entry denying a caller the
registry itself admits rejects loudly — 401 anonymous / 403
authenticated — so clients can prompt for credentials (the
registry-mock needs-auth contract).

The resolver's route classification resolves path-less fetches through
the registry graph — the same dispatch serving uses — and hosted
private-access descriptors are registry-qualified (registry NUL
package), so the same name@version on two hosted registries can never
share a cache key; unqualified descriptors from older builds fail
closed and re-resolve.

The bundled config.yaml moves the fixture namespace and the old ACL
into the local registry's packages: map (YAML anchors keep it
readable), and Config::proxy / Config::static_serve carry the
registry-mock rules programmatically.

Implements the pnpr side of pnpm/rfcs#17 only; no client or lockfile
changes.
Copilot AI review requested due to automatic review settings July 3, 2026 13:35
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR replaces pnpr’s global package-policy model with per-registry PackageRules, updates config loading and fixture YAML to use packages: maps, and rewires route and server authorization to resolve access from the concrete registry source. It also removes legacy benchmark mock-config fields.

Changes

Registry access rules refactor

Layer / File(s) Summary
Core rule model
pnpr/crates/pnpr/src/policy.rs, pnpr/crates/pnpr/src/registry.rs, pnpr/crates/pnpr/src/lib.rs, pnpr/crates/pnpr/src/error.rs, pnpr/crates/pnpr/src/policy/tests.rs
PackageRule/PackageRules replace PackagePolicy/PackagePolicies, with specificity-based selection, explicit-access tracking, and updated tests and exports.
Config schema and compilation
pnpr/crates/pnpr/src/config.rs, pnpr/crates/pnpr/config.yaml, pnpr/crates/pnpr/src/config/tests.rs, pnpr/crates/pnpr/tests/auth_publish.rs, pnpr/crates/pnpr/tests/policy.rs, pnpr/crates/pnpr/tests/server.rs, pnpr/crates/pnpr/src/resolver/tests.rs, pnpr/crates/pnpr/src/upstream/tests.rs, pacquet/crates/pnpr-client/tests/integration.rs
Hosted and upstream registries now carry compiled rules, YAML uses per-registry packages: maps, and legacy top-level packages: ACLs are rejected.
Route classification with hosted rules
pnpr/crates/pnpr/src/route.rs, pnpr/crates/pnpr/src/route/tests.rs
Route state stores registry graphs plus per-registry rules, and hosted classification now uses registry-qualified policy ids.
Server authorization by resolved source
pnpr/crates/pnpr/src/server.rs, pnpr/crates/pnpr/src/server/tests.rs
Read/write authorization now keys off resolved registry source and write target data, with hosted gate helpers and updated server tests.
Resolver and upstream test wiring
pnpr/crates/pnpr/src/resolver/tests.rs, pnpr/crates/pnpr/src/upstream/tests.rs, pacquet/crates/pnpr-client/tests/integration.rs
Test helpers and upstream config setup now initialize PackageRules and hosted footprints explicitly.

Benchmark config shape update

Layer / File(s) Summary
Benchmark mock config
pacquet/tasks/integrated-benchmark/src/work_env.rs
The cold mock benchmark config drops legacy uplinks and packages fields and keeps the modern mounts and registries shape.

Estimated code review effort: 4 (Complex) | ~75 minutes

Possibly related PRs

  • pnpm/pnpm#11970 - Both PRs reshape registry mock/config YAML toward per-package package-rule maps.
  • pnpm/pnpm#12043 - Both PRs derive access enforcement from YAML packages: entries.
  • pnpm/pnpm#12778 - Both PRs extend the registry graph and routing model around packages/sources.

Suggested labels: product: pnpr

Suggested reviewers: juanpicado

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pnpr-registry-packages

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

pnpr: move namespace + ACL into per-registry packages: rules (RFC #17)

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Replace patterns: + global ACL with per-registry packages: rules selected by specificity.
• Enforce new auth semantics for hosted/upstream reads/writes, including tiered hosted denials.
• Update bundled config and migrate/add tests for rule selection and validation failures.
Diagram

graph TD
  A["config.yaml"] --> B["Config::from_yaml"] --> C[("PackageRules") ] --> D[("Registries") ] --> E["Server handlers"] --> F["RouteContext"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add temporary compatibility shim for old config
  • ➕ Easier upgrades for existing pnpr users
  • ➕ Can emit structured deprecation warnings and auto-migrate in-memory
  • ➖ More code paths to secure and test
  • ➖ Risk of silently choosing the wrong registry/rule interpretation during transition
2. Keep ordered rule list semantics (verdaccio-style)
  • ➕ Familiar behavior for users coming from verdaccio
  • ➕ Simpler mental model for ‘first match wins’
  • ➖ Conflicts with YAML mapping’s unordered nature (formatter/yq reorder hazards)
  • ➖ Requires preserving key order through parsing/serialization
3. Precompute a specialized matcher (tiered map/index)
  • ➕ Faster per-request matching than scanning rules and picking max specificity
  • ➕ Makes uniqueness-by-tier explicit in the structure
  • ➖ More implementation complexity for modest real-world benefit
  • ➖ Harder to keep ergonomic for programmatic constructors/tests

Recommendation: The PR’s approach (specificity-based selection with per-registry packages: maps and a hard error for removed global ACL) is the safest pre-1.0 choice: it avoids YAML key-order security pitfalls, keeps routing and authorization derived from the same declaration, and fails loudly to prevent accidental access widening. A compatibility shim could be reconsidered only if external configs already depend on the old global packages: block.

Files changed (17) +1245 / -868

Enhancement (5) +668 / -403
config.rsCompile per-registry 'packages:' into 'PackageRules' and reject global ACL +150/-107

Compile per-registry 'packages:' into 'PackageRules' and reject global ACL

• Removes 'Config.packages'/'Config.policies' and introduces per-registry 'rules' on hosted and upstream configs. Parses 'packages:' maps (key=namespace pattern, value=optional access/publish/unpublish overrides) into 'PackageRules', derives registry graph patterns from rule keys, rejects upstream write rules, and fails startup if a top-level 'packages:' block is present.

pnpr/crates/pnpr/src/config.rs

policy.rsReplace ordered policies with specificity-based per-registry rules +106/-89

Replace ordered policies with specificity-based per-registry rules

• Reworks the policy module into 'PackageRules' (per-registry) and 'PackageRule' (per-map-entry), with defaults and optional per-entry overrides. Implements specificity-based winner selection and tracks whether access came from an explicit entry to drive hosted denial behavior.

pnpr/crates/pnpr/src/policy.rs

registry.rsAdd 'PackagePattern::specificity()' helper +16/-0

Add 'PackagePattern::specificity()' helper

• Introduces a numeric specificity ranking used by 'PackageRules' to pick the most specific matching key deterministically.

pnpr/crates/pnpr/src/registry.rs

route.rsClassify routes via registry graph and per-hosted rules +102/-26

Classify routes via registry graph and per-hosted rules

• Route classification now resolves path-less fetches through the configured registry graph and consults the resolved registry’s rules. Hosted private descriptors become registry-qualified ('registry\0package') and invalid/legacy descriptors fail closed to force re-resolution.

pnpr/crates/pnpr/src/route.rs

server.rsEnforce per-registry rules in read/write paths with tiered hosted denials +294/-181

Enforce per-registry rules in read/write paths with tiered hosted denials

• Threads resolved registry source into authorization so upstream and hosted reads are gated by the serving registry’s rules. Implements hosted gating that masks registry-default denials as 404 while explicit-entry denials return 401/403, and routes publish/batch publish through validation that resolves the write target and checks per-registry publish rules.

pnpr/crates/pnpr/src/server.rs

Refactor (2) +1 / -11
error.rsRemove obsolete invalid-policy-pattern error variant +0/-10

Remove obsolete invalid-policy-pattern error variant

• Drops 'InvalidPolicyPattern' and its mappings now that package rule keys use the internal 'PackagePattern' parser and config errors surface as 'InvalidConfig'.

pnpr/crates/pnpr/src/error.rs

lib.rsRe-export new policy types +1/-1

Re-export new policy types

• Switches public exports from 'PackagePolicies/PackagePolicy' to 'PackageRules/PackageRule' to reflect the per-registry rule model.

pnpr/crates/pnpr/src/lib.rs

Tests (9) +499 / -367
tests.rsUpdate config parsing tests for removed top-level 'packages:' +244/-207

Update config parsing tests for removed top-level 'packages:'

• Removes now-unneeded 'packages: {}' stubs from many YAML snippets and adjusts expectations to match the new config surface. Adds/updates coverage around the stricter config shape (top-level ACL no longer accepted).

pnpr/crates/pnpr/src/config/tests.rs

tests.rsAdd tests for specificity selection and default fallbacks +98/-8

Add tests for specificity selection and default fallbacks

• Replaces ‘first match wins’ expectations with tests asserting specificity ordering across exact/@scope/*/@*/*/** tiers, key-order independence, and correct fallback to registry defaults when winning entries omit fields.

pnpr/crates/pnpr/src/policy/tests.rs

tests.rsUpdate resolver cache tests for registry-qualified hosted descriptors +22/-16

Update resolver cache tests for registry-qualified hosted descriptors

• Adjusts hosted private-access footprints to include 'registry\0package' IDs and updates fixtures to mutate hosted rules (per-registry) instead of global policies.

pnpr/crates/pnpr/src/resolver/tests.rs

tests.rsUpdate route tests for hosted policy ID qualification +5/-3

Update route tests for hosted policy ID qualification

• Migrates expected hosted route classifications to the new 'local\0@private/pkg' descriptor format and relies on hosted registry rules instead of global policies.

pnpr/crates/pnpr/src/route/tests.rs

tests.rsExercise group-based access and explicit-entry denial semantics +23/-12

Exercise group-based access and explicit-entry denial semantics

• Updates tests to configure per-hosted 'PackageRules' directly and verifies that explicit-entry denials produce 401 for anonymous callers (rather than being masked) while allowed callers proceed to normal not-found behavior.

pnpr/crates/pnpr/src/server/tests.rs

tests.rsAdapt upstream test fixtures to include default rules field +1/-0

Adapt upstream test fixtures to include default rules field

• Updates test upstream configs to populate the new 'rules' field with defaults.

pnpr/crates/pnpr/src/upstream/tests.rs

auth_publish.rsNest test ACL blocks under 'registries.<name>.packages' +19/-8

Nest test ACL blocks under 'registries.<name>.packages'

• Rewrites generated YAML in integration tests to place package rules on the 'local' hosted registry (including adding a '**' entry to keep the namespace unbounded as before).

pnpr/crates/pnpr/tests/auth_publish.rs

policy.rsMigrate integration policy tests to per-registry 'packages:' model +23/-13

Migrate integration policy tests to per-registry 'packages:' model

• Updates YAML generation and assertions to reflect that rules are compiled from a registry’s 'packages:' map, including the explicit-entry 401/403 behavior and '$anonymous' handling.

pnpr/crates/pnpr/tests/policy.rs

server.rsUpdate server integration tests for per-registry rules and upstream read gating +64/-100

Update server integration tests for per-registry rules and upstream read gating

• Refactors helpers to build hosted registries with default access and per-entry rules, moves upstream access restrictions onto 'upstreams.<name>.rules', and updates hosted privacy/masking expectations under the new tiered hosted gate semantics.

pnpr/crates/pnpr/tests/server.rs

Other (1) +77 / -87
config.yamlMigrate bundled config to per-registry 'packages:' map +77/-87

Migrate bundled config to per-registry 'packages:' map

• Replaces 'local.patterns' and the top-level 'packages:' ACL with 'registries.local.packages' entries (using anchors for default rules). Documents specificity behavior and keeps the registry-mock unpublish contract via per-entry 'unpublish: $authenticated'.

pnpr/crates/pnpr/config.yaml

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements pnpr’s RFC-17 “revised registries model” by replacing the interim global packages: ACL + per-registry patterns: namespace with a single per-registry packages: map that both (1) declares namespace and (2) defines access/publish/unpublish rules, with rule selection by specificity (order-independent).

Changes:

  • Replaced global PackagePolicies / PackagePolicy with per-registry PackageRules / PackageRule, including specificity-based rule selection and registry-level defaults.
  • Reworked server read/write authorization and routing to consult the resolved concrete registry’s rules (including registry-qualified private-cache descriptors for hosted registries).
  • Updated config parsing + bundled config.yaml and migrated/added tests to cover the new model and invariants.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
pnpr/crates/pnpr/tests/server.rs Migrates integration tests from global ACL to per-registry rules and adds helpers for rules setup.
pnpr/crates/pnpr/tests/policy.rs Updates YAML-driven integration tests to nest packages: under a registry.
pnpr/crates/pnpr/tests/auth_publish.rs Updates publish/auth integration tests to generate new per-registry packages: YAML.
pnpr/crates/pnpr/src/upstream/tests.rs Updates upstream test fixture construction to include default rules.
pnpr/crates/pnpr/src/server/tests.rs Migrates server tests to use hosted registry rules instead of global policies.
pnpr/crates/pnpr/src/server.rs Rewires authorization/routing to use resolved registry rules; adds hosted gating tiers and write-target plumbing.
pnpr/crates/pnpr/src/route/tests.rs Updates route classification tests for registry-qualified hosted descriptors.
pnpr/crates/pnpr/src/route.rs Makes route classification resolve through the registry graph and consult hosted registry rules; qualifies hosted cache descriptors.
pnpr/crates/pnpr/src/resolver/tests.rs Updates resolver-cache tests for registry-qualified hosted descriptors and per-registry hosted rules.
pnpr/crates/pnpr/src/registry.rs Adds pattern specificity ranking used for order-independent rule selection.
pnpr/crates/pnpr/src/policy/tests.rs Adds/updates tests for specificity selection, defaults, and registry-mock rule behavior.
pnpr/crates/pnpr/src/policy.rs Replaces ordered glob policy list with per-registry PackageRules keyed by PackagePattern + specificity selection.
pnpr/crates/pnpr/src/lib.rs Updates public exports to the new rule types.
pnpr/crates/pnpr/src/error.rs Removes obsolete invalid-policy-pattern error variant tied to wax globs.
pnpr/crates/pnpr/src/config/tests.rs Updates config parsing tests to the new registries+packages shape and adds new validation assertions.
pnpr/crates/pnpr/src/config.rs Moves access rules into per-registry config; rejects top-level packages:; builds rules from per-registry maps.
pnpr/crates/pnpr/config.yaml Updates bundled config to declare fixture namespace + access rules via the per-registry packages: map (with YAML anchors).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1919 to +1922
let effective = hosted.rules.for_package(package);
if effective.access.allows(identity) {
return HostedGate::Allowed(hosted.org.clone());
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended, but the doc comment invited this read — sharpened in 0dd24c3. The decision is always the effective per-package access: an explicit entry fully decides its names, including opening one name (access: $all) on an otherwise-private registry (the RFC's field-fallback semantics — the registry-level access: is a default, not an independent AND-gate). Only the shape of a denial is tiered: default-denied callers are masked, explicit-entry denials inside a visible registry are loud.

Comment on lines +476 to +484
let Some(rules) = self.hosted_rules.get(registry) else {
return RouteClass::Public;
};
let access = rules.for_package(package).access;
if access.allows(&Identity::Anonymous) {
return RouteClass::Public;
}
if access.allows(identity) {
RouteClass::Hosted { policy_id: package.to_string() }
RouteClass::Hosted { policy_id: hosted_policy_id(registry, package) }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classification admits with the same effective-access lookup the serving gate does (rules.for_package(package).access.allows(identity)), so they cannot disagree about whether a caller may read a name — the serving tiers only vary the denial's shape (mask vs 401/403), and every denial classifies as an anonymous public fetch the endpoint fails closed on. Added a comment stating the invariant in 0dd24c3.

Comment thread pnpr/crates/pnpr/src/policy.rs Outdated
Comment on lines +261 to +265
let winner = self
.rules
.iter()
.filter(|rule| rule.pattern.matches(package))
.max_by_key(|rule| rule.pattern.specificity());

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 0dd24c3: for_package now resolves the winner through a per-tier index (exact/scope maps plus any-scoped/all slots) — the specificity chain means each tier holds at most one candidate, so the lookup is a couple of map probes instead of a scan.

Comment on lines +531 to +535
Some((registry, package)) => self
.hosted_rules
.get(registry)
.is_some_and(|rules| rules.for_package(package).access.allows(identity)),
None => false,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the classification note: the registry-level default is a field fallback inside for_package, not an independent gate, so this check is already consistent with hosted_gate's Allowed branch. The denial tiers only affect response shape on the serving path.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Cache replay skips upstream rules ✓ Resolved 🐞 Bug ⛨ Security
Description
Resolver cache reuse consults Footprint::allows, but allows_descriptor validates upstream alias
descriptors using select_alias(..., package: None), bypassing upstream per-package access
refinements. As a result, a cached resolution produced under an allowed upstream package rule can be
replayed to a caller who passes the registry-level alias gate but is denied by per-package upstream
rules, leaking restricted dependency metadata via the resolver cache.
Code

pnpr/crates/pnpr/src/route.rs[R563-576]

+                // a rotated credential also fails closed here. Replay
+                // granularity is registry-scoped by design — the descriptor
+                // names no package, so the gate is the registry-level alias
+                // selection (`package: None`), shared among the callers the
+                // upstream's `access:` admits; per-package `access`
+                // refinements bound fresh resolves and every serving read.
           self.aliases.iter().find(|candidate| candidate.name == alias.as_str()).is_some_and(
               |candidate| {
-                        self.select_alias(identity, &candidate.origin).is_some_and(|selected| {
-                            selected.name == alias.as_str()
-                                && selected.credential_digest == *credential_digest
-                        })
+                        self.select_alias(identity, &candidate.origin, None).is_some_and(
+                            |selected| {
+                                selected.name == alias.as_str()
+                                    && selected.credential_digest == *credential_digest
+                            },
+                        )
Evidence
Fresh alias selection is per-package-aware, but alias-descriptor replay explicitly uses `package:
None`, so upstream per-package rules are skipped during cache reuse checks. Resolver cache reuse is
gated only by Footprint::allows, so this omission directly affects whether cached results can be
returned across callers.

pnpr/crates/pnpr/src/route.rs[334-343]
pnpr/crates/pnpr/src/route.rs[534-545]
pnpr/crates/pnpr/src/route.rs[547-578]
pnpr/crates/pnpr/src/route.rs[200-205]
pnpr/crates/pnpr/src/resolver.rs[571-595]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PrivateAccessDescriptor::Alias` cache replay authorization is registry-scoped and intentionally passes `package: None` into `select_alias`, which means `upstream_admits` (per-package upstream rules) is not evaluated for cache replay. This creates an authorization mismatch: fresh resolves are per-package-aware, but cached resolutions can be replayed without re-checking per-package upstream access.
## Issue Context
- Fresh upstream alias selection is per-package-aware (`select_alias` calls `upstream_admits` when `package` is `Some`).
- Resolver cache replay uses `Footprint::allows(...)` to decide whether a cached resolution can be returned to the current caller.
- `Footprint::allows(...)` delegates to `RouteContext::allows_descriptor(...)`.
- For alias descriptors, `allows_descriptor` currently re-runs alias selection with `package: None`, skipping per-package upstream rules.
## Fix Focus Areas
- pnpr/crates/pnpr/src/route.rs[534-578]
- pnpr/crates/pnpr/src/route.rs[158-206]
- pnpr/crates/pnpr/src/resolver.rs[571-595]
## Suggested fix approach
Choose one of these (ordered by correctness):
1) **Make alias descriptors package-aware when upstream rules refine access**:
- Extend `PrivateAccessDescriptor::Alias` (or add a new variant) to optionally include a package identifier (e.g. the concrete package name, or a stable “effective-rule id” derived from the winning pattern tier).
- In `RouteHook::authorize`, when returning `RouteClass::Proxied` and `package.is_some()`, record the package-aware descriptor whenever the upstream has any per-package access refinement.
- Update `allows_descriptor` to re-check `upstream_rules.for_package(package).access.allows(identity)` for that descriptor.
2) **Fail closed on replay when upstream has per-package access refinements**:
- Detect upstreams with any per-package `access` entries and disallow replay of `Alias` descriptors for them (forcing re-resolve).
Add/adjust tests to cover:
- User A allowed for `@scope/pkg` via upstream per-package rule populates cache.
- User B allowed for upstream alias at registry-level but denied by upstream per-package rule must not receive the cached resolution.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Null packages rejection bypass ✓ Resolved 🐞 Bug ⛨ Security
Description
Config::from_yaml_str_with_overrides rejects the removed top-level packages: only when
file.packages.is_some(), but packages is deserialized as Option, so packages: null (or a
bare packages:) becomes None and is not rejected. This undermines the PR’s “fail loudly” safety
goal and can let a legacy ACL key remain in config without a startup error, potentially leading
operators to believe access is still enforced when it is not.
Code

pnpr/crates/pnpr/src/config.rs[R1542-1549]

+        if file.packages.is_some() {
+            return Err(RegistryError::InvalidConfig {
+                reason: "the top-level `packages:` block was removed: declare per-package rules \
+                         on the registry that serves them, as `registries.<name>.packages` \
+                         (pattern keys, `access`/`publish`/`unpublish` values)"
+                    .to_string(),
+            });
+        }
Evidence
The config struct explicitly relies on Option to accept a bare YAML key as null for resolver:;
the same serde behavior applies to packages, which is also an Option. Because the rejection
checks only is_some(), a present-but-null packages: key will not trigger the error path.

pnpr/crates/pnpr/src/config.rs[1021-1027]
pnpr/crates/pnpr/src/config.rs[1037-1044]
pnpr/crates/pnpr/src/config.rs[1538-1549]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The startup rejection for the removed top-level `packages:` block can be bypassed when the YAML value is `null` (including `packages:` with no value), because the field is `Option<...>` and serde treats null as `None`.
## Issue Context
The PR intends that *any* presence of a top-level `packages:` key is a startup error to avoid silently dropping a formerly-enforced ACL.
## Fix Focus Areas
- pnpr/crates/pnpr/src/config.rs[1019-1045]
- pnpr/crates/pnpr/src/config.rs[1538-1549]
## Suggested fix
Change the `ConfigFile.packages` field to detect presence even when null, e.g. `packages: Option<Option<serde::de::IgnoredAny>>` (or a custom “present” wrapper), and then reject when the outer `Option` is `Some(_)` (meaning the key was present), not when the inner value is non-null.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Private search DoS scan ✓ Resolved 🐞 Bug ⛨ Security
Description
serve_search now enumerates hosted registry storage even when the caller is effectively denied for
all names (i.e. hosted_gate never returns Allowed), enabling a remotely-triggerable disk/CPU (or
S3 listing) DoS and leaking private registry size via response timing. This contradicts the
function’s own contract comment that denied registries should be masked without enumerating
packages.
Code

pnpr/crates/pnpr/src/server.rs[R3045-3061]

+        let Some(org) = state.inner.config.hosted.get(&source).map(|hosted| hosted.org.clone())
+        else {
continue;
};
let storage = hosted_storage(state, Some(&org));
// The caller was resolved once by the middleware; both filters run
-        // synchronously against it inside the scan.
+        // synchronously against it inside the scan. Visibility is
+        // per-package: each hit is gated by this hosted registry's effective
+        // access for that name, so a per-package rule can open (or close) a
+        // name regardless of the registry-level default.
let keep = |name: &str| {
matches!(
   resolve_registry_source(state, &registry, name),
   RegistrySource::Hosted(resolved) if resolved == source,
-            ) && authorize(state, identity, name, Action::Access).is_ok()
+            ) && matches!(hosted_gate(state, identity, &source, name), HostedGate::Allowed(_))
};
match crate::search::run_local_search(&storage, &text, size - objects.len(), keep).await {
Evidence
serve_search obtains the hosted org and runs run_local_search for every hosted source, with
visibility enforced only inside the per-name keep filter. run_local_search is explicitly
implemented as a one-shot storage scan over hosted_package_names, so even a fully-masked caller
forces enumeration work and produces timing differences proportional to registry size.

pnpr/crates/pnpr/src/server.rs[2999-3064]
pnpr/crates/pnpr/src/search.rs[11-15]
pnpr/crates/pnpr/src/search.rs[80-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`serve_search` always calls `run_local_search()` for each hosted source and only filters results via a per-package `keep` closure. When a hosted registry is blanket-masked for a caller (common: anonymous vs `$authenticated` default), this still enumerates all package names, wasting resources and leaking registry size via timing.
### Issue Context
- `run_local_search()` enumerates `storage.hosted_package_names()` and lowercases each candidate before applying `keep`.
- `serve_search` no longer has a pre-scan caller visibility gate; it fetches `org` unconditionally and proceeds to scan.
### Fix Focus Areas
- Add a fast-path to skip scanning a hosted source when the caller cannot possibly see any package in that source (e.g. default access denies the caller **and** no explicit rule could admit them).
- Consider adding a helper on `PackageRules` to answer something like `might_admit(identity)` / `any_explicit_access_allows(identity)` so `serve_search` can decide to skip the scan without enumerating storage.
- Ensure the behavior still preserves the intended “explicit allow can open names” semantics if that is a supported configuration.
#### Code pointers
- pnpr/crates/pnpr/src/server.rs[3045-3061]
- pnpr/crates/pnpr/src/search.rs[11-15]
- pnpr/crates/pnpr/src/search.rs[80-95]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Upstream access misclassified ✓ Resolved 🐞 Bug ⛨ Security
Description
RouteContext::classify returns RouteClass::Public for upstream registries that have no
access-bearing alias, without consulting that upstream registry’s per-package packages: access
rules, so restricted upstream packages can be treated as “public” for resolver footprinting/caching.
This can allow resolution-cache reuse across identities even when upstream.rules denies anonymous
access for the package.
Code

pnpr/crates/pnpr/src/route.rs[R379-397]

+            return match self.registries.resolve_default(package) {
+                Resolved::Concrete { registry, kind: ConcreteKind::Hosted } => {
+                    self.classify_hosted(identity, registry, Some(package))
+                }
+                Resolved::Concrete { registry, kind: ConcreteKind::Upstream } => {
+                    match self
+                        .aliases
+                        .iter()
+                        .find(|alias| alias.name == registry && alias.access.allows(identity))
+                    {
+                        Some(alias) => RouteClass::Proxied {
+                            alias: alias.name.clone(),
+                            credential_digest: alias.credential_digest.clone(),
+                        },
+                        // A public upstream source (no alias) is an anonymous
+                        // public fetch; an unauthorized caller falls through
+                        // to one the endpoint fails closed on.
+                        None => RouteClass::Public,
+                    }
Evidence
The resolver’s cache privacy depends on RouteClass: Public produces MetadataCacheScope::Public
and no footprint entry, so misclassifying a caller-gated upstream route as Public can make a
restricted resolution shareable. The server code explicitly recognizes that public upstreams may
still deny anonymous access via per-package rules, but RouteContext::classify does not consult
upstream rules when no alias exists.

pnpr/crates/pnpr/src/route.rs[341-402]
pnpr/crates/pnpr/src/route.rs[672-709]
pnpr/crates/pnpr/src/config.rs[501-520]
pnpr/crates/pnpr/src/server.rs[1772-1792]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RouteContext::classify()` currently treats upstream routes with no access-bearing alias as `RouteClass::Public`, but upstream registries now have per-package `rules` (from `registries.<name>.packages`) that can deny anonymous access. When such a route is classified as `Public`, `RouteHook::metadata_scope()` returns `MetadataCacheScope::Public` and `RouteHook::authorize()` records no private descriptor, so the resolver’s footprint can remain empty and the resulting resolution can be cached/replayed as globally shareable.
### Issue Context
- Upstream per-package access can deny anonymous reads (`upstream.rules.for_package(..).access`).
- Resolver cache privacy relies on route classification: `RouteClass::Public` => public metadata scope and no footprint entry.
- The bug affects both `/~<registry>/...` fetches and path-less fetches that resolve to an upstream.
### Fix Focus Areas
- pnpr/crates/pnpr/src/route.rs[342-402]
### What to change
1. Extend `RouteContext` to retain upstream rule sets (e.g. `upstream_rules: IndexMap<String, PackageRules>`) similar to `hosted_rules`.
2. In `classify()`, when a fetch resolves to an upstream registry (either `/~<name>/` or via `registries.resolve_default(package)`):
- Look up that upstream’s rules and compute effective access for `package`.
- If anonymous is allowed, keep `RouteClass::Public`.
- If anonymous is denied but `identity` is allowed, return a **private** classification that records a private descriptor (so metadata scope becomes `Private` and the footprint is non-empty).
- If `identity` is denied, fall back to the existing “fail closed by letting the endpoint reject” behavior.
3. Add a descriptor type that can be re-validated on cache hits (either:
- a new `PrivateAccessDescriptor`/`RouteClass` variant for upstream-rule-gated routes, or
- generalize the existing `Hosted` descriptor handling to include upstream registries too).
4. Update `allows_descriptor()` to re-check upstream rules for this descriptor, so cached private resolutions are only reused when the caller is still authorized.
5. Add tests covering: public upstream with per-package access rule => authorized caller produces a private footprint + private metadata scope; anonymous caller does not.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Over-scoped cache descriptors 🐞 Bug ➹ Performance
Description
RouteContext::alias_package_qualifier qualifies proxied upstream cache descriptors whenever a
package’s winning rule has an explicit access field, even if that explicit access is identical to
the upstream’s registry-level default. This can unnecessarily fragment the private metadata cache
into extra per-package scopes and increase filesystem churn in large configs with redundant access
declarations.
Code

pnpr/crates/pnpr/src/route.rs[R358-360]

+    fn alias_package_qualifier(&self, alias: &str, package: Option<&str>) -> Option<String> {
+        let (package, rules) = (package?, self.upstream_rules.get(alias)?);
+        rules.for_package(package).access_is_explicit.then(|| package.to_string())
Evidence
The qualifier is driven solely by access_is_explicit in alias_package_qualifier, but
access_is_explicit is defined as explicit_access.is_some() (presence of an access field), not
as “effective access differs from the registry default”. Therefore explicit-but-equivalent rules
trigger per-package descriptor scoping even when there is no real per-package refinement to protect
on cache replay.

pnpr/crates/pnpr/src/route.rs[342-361]
pnpr/crates/pnpr/src/policy.rs[310-336]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`alias_package_qualifier` currently returns `Some(package)` based on `access_is_explicit`, which only means an `access` field was present in the winning rule, not that it *refines* access beyond the upstream registry’s default. This causes unnecessary per-package cache descriptor IDs when the explicit `access` equals the default.
### Issue Context
- The intent (per the function comment) is to qualify only when upstream rules **explicitly refine** per-package access, so cache replay re-checks that refinement.
- `Effective.access_is_explicit` is currently computed as “did the winning rule specify `access`?”, not “does effective access differ from the default?”.
### Fix Focus Areas
- pnpr/crates/pnpr/src/route.rs[353-361]
- pnpr/crates/pnpr/src/policy.rs[317-336]
### Suggested change
In `alias_package_qualifier`, compute the effective access and only qualify when it both:
1) comes from an explicit rule (`access_is_explicit`), **and**
2) is different from the upstream registry default (`rules.default_access()`).
Example sketch:

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Route ignores namespace claim 🐞 Bug ➹ Performance
Description
RouteContext::classify treats any /~/... URL as hosted/proxied based on registry existence and
rules, without verifying that the registry actually claims the package name in the routing graph.
This can diverge from server dispatch (which resolves via the graph and returns Unclaimed/404),
causing unnecessary private-cache keying and potential cache amplification for random package
probes.
Code

pnpr/crates/pnpr/src/route.rs[R376-399]

        if let Some(rest) = fetch.strip_prefix(hosted)
-                && let Some(upstream) =
+                && let Some(registry) =
                rest.strip_prefix('~').and_then(|rest| rest.split('/').next())
-                && !upstream.is_empty()
+                && !registry.is_empty()
        {
-                return match self
+                if let Some(alias) = self
                .aliases
                .iter()
-                    .find(|alias| alias.name == upstream && alias.access.allows(identity))
+                    .find(|alias| alias.name == registry && alias.access.allows(identity))
            {
-                    Some(alias) => RouteClass::Proxied {
-                        alias: alias.name.clone(),
-                        credential_digest: alias.credential_digest.clone(),
-                    },
-                    None => RouteClass::Public,
-                };
+                    // Per-package refinement: a name the upstream's rules
+                    // deny this caller gets no credential — the anonymous
+                    // fetch fails closed at the endpoint, matching serving.
+                    if self.upstream_admits(&alias.name, identity, package) {
+                        return RouteClass::Proxied {
+                            alias: alias.name.clone(),
+                            credential_digest: alias.credential_digest.clone(),
+                        };
+                    }
+                    return RouteClass::Public;
+                }
+                if self.hosted_rules.contains_key(registry) {
+                    return self.classify_hosted(identity, registry, package);
+                }
Evidence
The classifier short-circuits on registry existence (hosted_rules.contains_key) and runs
classify_hosted which uses access rules but does not check whether the package is in the
registry’s claimed namespace. In contrast, the server dispatch resolves (registry, package)
through the routing graph and returns Unclaimed when namespace_claims fails, making these two
paths disagree for off-namespace package names.

pnpr/crates/pnpr/src/route.rs[366-400]
pnpr/crates/pnpr/src/route.rs[505-532]
pnpr/crates/pnpr/src/server.rs[1753-1769]
pnpr/crates/pnpr/src/registry.rs[223-227]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RouteContext::classify` decides `/~<registry>/...` routing class without consulting the routing graph, so it may classify a package as Hosted/Proxied even when that registry does not claim the package (server dispatch returns `Unclaimed` and 404). This creates classification/serving disagreement and can lead to unnecessary private cache scoping.
### Issue Context
Server dispatch uses `config.registries.resolve(registry, package)` to enforce namespace claims (`namespace_claims`). The route classifier should mirror that to keep cache scoping aligned with actual serving.
### Fix Focus Areas
- pnpr/crates/pnpr/src/route.rs[366-437]
### Suggested fix
- In the `/~<registry>/` branch, when `package.is_some()`:
- Call `self.registries.resolve(registry, package)`.
- Only return `RouteClass::Hosted` when the graph resolves to that hosted registry.
- Only return `RouteClass::Proxied` when the graph resolves to that upstream registry.
- If the graph returns `Resolved::Unclaimed`/`Resolved::UnknownRegistry`, classify as `Public` (matches server’s definitive 404, and avoids needless private cache scoping).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Fail-open rules fallback 🐞 Bug ⛨ Security
Description
source_rules() falls back to PackageRules::default() when the resolved registry is missing from
config.hosted/config.upstreams, and that default makes reads $all. If the in-memory config
becomes inconsistent (possible with programmatic Config construction/mutation), authorization can
silently allow reads that should be denied instead of failing closed.
Code

pnpr/crates/pnpr/src/server.rs[R3752-3770]

+/// The `packages:` rules of the concrete registry a request resolved to.
+/// Authorization is entirely registry-scoped — there is no global,
+/// name-keyed ACL — so every check consults the one registry that serves
+/// the package. The fallback (safe defaults: reads open, publishes need
+/// auth, destructive writes denied) only fires for a programmatically
+/// built config whose serving tables miss the graph entry.
+fn source_rules<'a>(state: &'a AppState, source: &RegistrySource) -> &'a PackageRules {
+    static SAFE_DEFAULTS: LazyLock<PackageRules> = LazyLock::new(PackageRules::default);
+    match source {
+        RegistrySource::Hosted(name) => {
+            state.inner.config.hosted.get(name).map(|hosted| &hosted.rules)
+        }
+        RegistrySource::Upstream(name) => {
+            state.inner.config.upstreams.get(name).map(|upstream| &upstream.rules)
+        }
+        RegistrySource::Unclaimed | RegistrySource::NotFound => None,
+    }
+    .unwrap_or(&SAFE_DEFAULTS)
+}
Evidence
The new helper explicitly falls back to PackageRules::default() on lookup miss, and
PackageRules::default()/PackageRules::new(..., None) default the read gate to $all, so a miss
becomes public read access. Because Config exposes registries, hosted, and upstreams as
public fields, programmatic construction/mutation can produce such a miss without a compile-time
guard.

pnpr/crates/pnpr/src/server.rs[3752-3770]
pnpr/crates/pnpr/src/policy.rs[238-244]
pnpr/crates/pnpr/src/policy.rs[262-274]
pnpr/crates/pnpr/src/config.rs[54-142]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pnpr/crates/pnpr/src/server.rs::source_rules()` returns permissive `PackageRules::default()` when it cannot find rules for a resolved hosted/upstream registry in `config.hosted`/`config.upstreams`. Because `PackageRules::default()` uses `$all` for read access, this is a fail-open behavior that can expose packages if the runtime config tables are inconsistent.
### Issue Context
This is mainly a defensive-programming/invariant issue: YAML-driven construction likely keeps `registries` and `hosted/upstreams` in sync, but `Config` is a public struct with public fields and can be constructed/mutated programmatically (embedders/tests). In that scenario, an inconsistency would currently turn into “reads open” rather than a hard failure/deny.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[3752-3770]
### Suggested fix direction
- Replace `SAFE_DEFAULTS` with a *strict* default (deny reads/writes), or
- Change `source_rules()` to surface an internal error (and have callers return 500 / fail closed) when a resolved Hosted/Upstream registry has no corresponding rules entry.
- At minimum, emit an error-level log when this invariant is violated so it’s diagnosable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
8. Routing scans package keys 🐞 Bug ➹ Performance
Description
build_registries now derives each registry’s routing namespace from every key in its packages:
map (rules.patterns()), but registry dispatch still checks namespace claims via a linear scan over
patterns. This makes hot-path dispatch (registry resolution and resolver route classification)
scale with the number of per-registry packages: entries, increasing CPU/latency for large configs.
Code

pnpr/crates/pnpr/src/config.rs[R1814-1818]

+                let access = registry.access.as_ref().map(AccessSpec::to_access_list);
+                let rules = build_rules(&name, &registry.packages, access)?;
+                let patterns = rules.patterns();
+                hosted.insert(name.clone(), HostedConfig { org: registry.org, rules });
      graph.insert(name, Registry::Hosted { patterns });
Evidence
The registry graph’s patterns are now sourced from the entire per-registry packages: map keyset
(PackageRules::patterns()), and registry resolution checks claims by scanning that patterns list
(patterns.iter().any(...)). Route classification calls resolve_default on the graph for
path-less fetches, so this scan can execute frequently on the resolver hot path.

pnpr/crates/pnpr/src/config.rs[1814-1818]
pnpr/crates/pnpr/src/config.rs[1827-1843]
pnpr/crates/pnpr/src/policy.rs[294-300]
pnpr/crates/pnpr/src/registry.rs[223-227]
pnpr/crates/pnpr/src/registry.rs[304-357]
pnpr/crates/pnpr/src/route.rs[402-437]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Registry namespace matching is currently a linear scan over a `Vec<PackagePattern>`, but this PR populates that vector from every `packages:` key (`rules.patterns()`), so routing/dispatch work grows with the size of the per-registry ruleset.
## Issue Context
- `PackageRules` already builds an index for *policy selection* (`for_package`), but the registry graph still uses `patterns.iter().any(...)` for *namespace claim* checks.
- `RouteContext::classify` now calls `registries.resolve_default(package)` for path-less fetch classification, so the claim check can run for many metadata/tarball fetches during resolver operation.
## Fix Focus Areas
- Introduce a compact, indexed representation for a registry’s namespace claim checks (e.g., exact-name set, scope set, any-scoped flag, all flag), built once at config load.
- Avoid cloning the full `PackagePattern` list into the routing graph when an index can answer `claims(package)` in O(1)/O(log n).
- Use the indexed `claims()` in `Registries::resolve`/`namespace_claims` instead of `patterns.iter().any(...)`.
### Suggested implementation direction
- Add a `NamespaceIndex` (similar to `RuleIndex`) that can answer whether any of the 4 pattern tiers matches.
- Store that index inside `Registry::Hosted` / `Registry::Upstream` (or alongside) instead of (or in addition to) a raw `Vec<PackagePattern>`.
- Build it from the `PackageRules` patterns once in `build_registries`.
## Fix Focus Areas (code references)
- pnpr/crates/pnpr/src/config.rs[1814-1843]
- pnpr/crates/pnpr/src/policy.rs[294-300]
- pnpr/crates/pnpr/src/registry.rs[223-357]
- pnpr/crates/pnpr/src/route.rs[402-437]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. RouteContext clones rules 🐞 Bug ➹ Performance
Description
RouteContext::from_config deep-clones config.registries and each hosted registry’s
PackageRules, duplicating potentially large packages: maps in memory and increasing
startup/config-build cost. With large per-registry rule maps, this can materially increase RSS and
reload/build time for the resolver runtime.
Code

pnpr/crates/pnpr/src/route.rs[R305-317]

+        let hosted_rules = config
+            .hosted
+            .iter()
+            .map(|(name, hosted)| (name.clone(), hosted.rules.clone()))
+            .collect();
Self {
hosted_origin,
public_routes,
aliases,
upstream_origins,
-            policies: config.policies.clone(),
+            registries: config.registries.clone(),
+            hosted_rules,
}
Evidence
RouteContext::from_config explicitly clones the registry graph and clones each hosted registry’s
rules into a new hosted_rules map. Config already stores registries and `HostedConfig {
rules: PackageRules }`, so the clone is redundant and scales with the size of the new per-registry
packages: maps.

pnpr/crates/pnpr/src/route.rs[284-317]
pnpr/crates/pnpr/src/config.rs[137-158]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`RouteContext::from_config` clones the registries graph and every hosted registry’s `PackageRules`. If `packages:` maps become large, this duplicates memory and adds unnecessary config-build overhead.
### Issue Context
`Config` already owns both `Registries` and hosted `PackageRules`. `RouteContext` is stored behind an `Arc`, so it can safely hold shared ownership (e.g., `Arc<Registries>`, `Arc<PackageRules>`) instead of cloning full structures.
### Fix Focus Areas
- Prefer sharing via `Arc` for `Registries` and `PackageRules` (or store an `Arc<Config>` / `Arc<RegistryConfig>` in `RouteContext` and look up by reference).
- If ownership/lifetimes require independent storage, consider storing `IndexMap<String, Arc<PackageRules>>` instead of cloning each rule set.
#### Code pointers
- pnpr/crates/pnpr/src/route.rs[284-317]
- pnpr/crates/pnpr/src/config.rs[137-158]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Rules scan per request ✓ Resolved 🐞 Bug ➹ Performance
Description
PackageRules::for_package linearly scans all rules and computes a max-specificity match on every
lookup, and these lookups occur on request paths (authorization, hosted gating, resolver route
classification). With large per-registry packages: maps, this can add noticeable CPU overhead per
packument/tarball fetch.
Code

pnpr/crates/pnpr/src/policy.rs[R261-266]

+        let winner = self
+            .rules
+            .iter()
+            .filter(|rule| rule.pattern.matches(package))
+            .max_by_key(|rule| rule.pattern.specificity());
+        let explicit_access = winner.and_then(|rule| rule.access.as_ref());
Evidence
The new lookup implementation scans all rules to find a max-specificity match. Authorization paths
call for_package() for access decisions, and route classification calls it to determine whether a
hosted route is public vs private, so the scan is on the request hot path.

pnpr/crates/pnpr/src/policy.rs[254-275]
pnpr/crates/pnpr/src/server.rs[3766-3786]
pnpr/crates/pnpr/src/route.rs[459-485]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PackageRules::for_package()` currently does a full scan (`iter().filter(...).max_by_key(...)`) to select the most specific match. This is O(N) per lookup and is used in hot paths (`authorize`, `hosted_gate`, resolver route classification), so large rule sets can create avoidable per-request CPU cost.
### Issue Context
The pattern language has only four specificity tiers (`Exact`, `Scope`, `AnyScoped`, `All`). That makes the winner computable without scanning the entire list.
### Fix Focus Areas
- pnpr/crates/pnpr/src/policy.rs[254-275]
### What to change
1. Change `PackageRules` internal representation to pre-index by tier, for example:
- `exact: HashMap<String, RuleFields>`
- `scopes: HashMap<String, RuleFields>` (keyed by scope)
- `any_scoped: Option<RuleFields>`
- `all: Option<RuleFields>`
2. Implement `for_package()` as:
- exact hit => return
- else if scoped name => scope hit => return
- else if scoped name => any_scoped => return
- else => all => return
- else => registry defaults
3. Preserve the current “missing field falls back to registry default” semantics.
4. Keep validation that prevents duplicates for a tier/key (e.g. duplicate exact name), so behavior stays order-independent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

11. push_rule overwrites duplicates 🐞 Bug ≡ Correctness
Description
PackageRules::push_rule allows adding duplicate patterns in programmatic configs; RuleIndex::build
overwrites earlier entries via map inserts, so the last duplicate silently wins and earlier rules
become unreachable. This makes programmatic access decisions insertion-order dependent and can
unexpectedly open/close access compared to the intended “duplicate key is an error” invariant.
Code

pnpr/crates/pnpr/src/policy.rs[R286-292]

+    /// Add one entry to the map. Selection stays order-free (specificity);
+    /// duplicate keys are the caller's to avoid — YAML loading rejects them.
+    /// For tests and embedders that build rules programmatically.
+    pub fn push_rule(&mut self, rule: PackageRule) {
+        self.rules.push(rule);
+        self.index = RuleIndex::build(&self.rules);
+    }
Evidence
push_rule mutates the rules vector and rebuilds the index with no duplicate detection. The index
construction overwrites entries for the same key via insert/assignment, so duplicates change the
winner silently.

pnpr/crates/pnpr/src/policy.rs[286-292]
pnpr/crates/pnpr/src/policy.rs[202-216]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PackageRules::push_rule` appends rules without checking whether a `PackagePattern` already exists in the ruleset. `RuleIndex::build` then uses `BTreeMap::insert` for Exact/Scope and `Option` replacement for AnyScoped/All, which silently overwrites the indexed winner when duplicates exist.
This is primarily a footgun for programmatic configs/tests (YAML mapping keys are already unique), but it can still lead to surprising allow/deny behavior.
### Issue Context
The code and docs state duplicates are the only within-registry error; the programmatic builder should uphold the same invariant or explicitly define last-wins semantics.
### Fix Focus Areas
- pnpr/crates/pnpr/src/policy.rs[202-216]
- pnpr/crates/pnpr/src/policy.rs[286-292]
### Suggested fix
- Change `push_rule(&mut self, rule: PackageRule)` to return `Result<(), RegistryError>` (or a dedicated error type) and:
- Check for an existing rule with the same `PackagePattern` (exact string / same scope / AnyScoped / All).
- If found, return an error instead of silently overriding.
- Alternatively, provide an explicit `replace_rule` API and document last-wins, but do not keep silent overwrite as the only behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Stale patterns error text 🐞 Bug ◔ Observability
Description
On publish to an Unclaimed name, the rejection message still says “declared patterns:”, but the
new model uses per-registry packages: maps for namespace claims. This will mislead operators
debugging routing/ACL failures after upgrading.
Code

pnpr/crates/pnpr/src/server.rs[1]

/// any of its sources is.
Evidence
The server constructs a rejection string that still references patterns:. The config model in this
PR clearly defines registry namespaces via the packages: map on each registry entry.

pnpr/crates/pnpr/src/server.rs[2492-2497]
pnpr/crates/pnpr/src/config.rs[933-943]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A user-facing publish rejection message still references the removed `patterns:` concept. With the RFC #17 model, namespace claims live in each registry’s `packages:` map keys, so the message should match the new config surface.
### Issue Context
Operators will see this on typos/off-namespace publishes; pointing them at `patterns:` is confusing because that field no longer exists.
### Fix Focus Areas
- pnpr/crates/pnpr/src/server.rs[2492-2497]
### Suggested fix
- Replace the message with wording like:
- "...: no registry’s declared `packages:` namespace claims this package name"
- or (if you want to be extra actionable) "...: no registry’s `registries.<name>.packages` key set claims this package name"

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +379 to +397
return match self.registries.resolve_default(package) {
Resolved::Concrete { registry, kind: ConcreteKind::Hosted } => {
self.classify_hosted(identity, registry, Some(package))
}
Resolved::Concrete { registry, kind: ConcreteKind::Upstream } => {
match self
.aliases
.iter()
.find(|alias| alias.name == registry && alias.access.allows(identity))
{
Some(alias) => RouteClass::Proxied {
alias: alias.name.clone(),
credential_digest: alias.credential_digest.clone(),
},
// A public upstream source (no alias) is an anonymous
// public fetch; an unauthorized caller falls through
// to one the endpoint fails closed on.
None => RouteClass::Public,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Upstream access misclassified 🐞 Bug ⛨ Security

RouteContext::classify returns RouteClass::Public for upstream registries that have no
access-bearing alias, without consulting that upstream registry’s per-package packages: access
rules, so restricted upstream packages can be treated as “public” for resolver footprinting/caching.
This can allow resolution-cache reuse across identities even when upstream.rules denies anonymous
access for the package.
Agent Prompt
### Issue description
`RouteContext::classify()` currently treats upstream routes with no access-bearing alias as `RouteClass::Public`, but upstream registries now have per-package `rules` (from `registries.<name>.packages`) that can deny anonymous access. When such a route is classified as `Public`, `RouteHook::metadata_scope()` returns `MetadataCacheScope::Public` and `RouteHook::authorize()` records no private descriptor, so the resolver’s footprint can remain empty and the resulting resolution can be cached/replayed as globally shareable.

### Issue Context
- Upstream per-package access can deny anonymous reads (`upstream.rules.for_package(..).access`).
- Resolver cache privacy relies on route classification: `RouteClass::Public` => public metadata scope and no footprint entry.
- The bug affects both `/~<registry>/...` fetches and path-less fetches that resolve to an upstream.

### Fix Focus Areas
- pnpr/crates/pnpr/src/route.rs[342-402]

### What to change
1. Extend `RouteContext` to retain upstream rule sets (e.g. `upstream_rules: IndexMap<String, PackageRules>`) similar to `hosted_rules`.
2. In `classify()`, when a fetch resolves to an upstream registry (either `/~<name>/` or via `registries.resolve_default(package)`):
   - Look up that upstream’s rules and compute effective access for `package`.
   - If anonymous is allowed, keep `RouteClass::Public`.
   - If anonymous is denied but `identity` is allowed, return a **private** classification that records a private descriptor (so metadata scope becomes `Private` and the footprint is non-empty).
   - If `identity` is denied, fall back to the existing “fail closed by letting the endpoint reject” behavior.
3. Add a descriptor type that can be re-validated on cache hits (either:
   - a new `PrivateAccessDescriptor`/`RouteClass` variant for upstream-rule-gated routes, or
   - generalize the existing `Hosted` descriptor handling to include upstream registries too).
4. Update `allows_descriptor()` to re-check upstream rules for this descriptor, so cached private resolutions are only reused when the caller is still authorized.
5. Add tests covering: public upstream with per-package access rule => authorized caller produces a private footprint + private metadata scope; anonymous caller does not.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 715dedd: alias selection is now per-package-aware on every classification path (direct origin, /~<name>/, and the path-less graph resolution) — a caller the upstream's effective access denies for a name is never handed the credential, so a fresh resolve fails closed exactly where the serving endpoint would deny the read, and cannot warm a cache entry for that name. One deliberate boundary, now documented at the descriptor gate: cache replay granularity stays registry-scoped (the alias descriptor names no package), shared among callers the registry-level access: admits — per-package refinements bound fresh resolves and every serving read.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
pnpr/crates/pnpr/src/server.rs (1)

3100-3167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated resolve_write_target + authorize pattern.

The sequence resolve_write_target(...) → build RegistrySource::Hosted(target.source.clone()) → loop/call authorize(..., action) is duplicated near-identically in update_packument (Lines 3111-3121), delete_package (Lines 3333-3346), delete_tarball (Lines 3382-3395), update_dist_tag (Lines 3501-3514), and validate_publish_doc (Lines 2725-2733). A helper folding both steps (accepting one or more Actions) would remove five near-identical blocks and reduce the risk of a future write handler forgetting a required action check.

♻️ Proposed consolidation
+fn resolve_authorized_write_target(
+    state: &AppState,
+    identity: &Identity,
+    registry: Option<&str>,
+    name: &PackageName,
+    actions: &[Action],
+) -> Result<WriteTarget, Box<Response>> {
+    let target = resolve_write_target(state, identity, registry, name)?;
+    let source = RegistrySource::Hosted(target.source.clone());
+    for action in actions {
+        authorize(state, identity, &source, name.as_str(), *action)
+            .map_err(|err| Box::new(error_response(&err)))?;
+    }
+    Ok(target)
+}

Call sites then collapse to e.g. let target = resolve_authorized_write_target(state, identity, registry, &name, &[Action::Publish, Action::Unpublish])?;.

Also applies to: 3323-3409, 3483-3576, 3588-3610

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpr/crates/pnpr/src/server.rs` around lines 3100 - 3167, The repeated
write-target resolution and authorization flow should be extracted into a shared
helper to avoid duplication across update_packument and the other write
handlers. Create a helper that combines resolve_write_target, building
RegistrySource::Hosted(target.source.clone()), and the authorize checks for one
or more Action values, then update update_packument, delete_package,
delete_tarball, update_dist_tag, and validate_publish_doc to call it. Keep the
existing behavior and error propagation unchanged, just centralize the pattern
under a clear name like resolve_authorized_write_target.
pnpr/crates/pnpr/tests/auth_publish.rs (1)

31-61: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate YAML-splicing helper vs. tests/policy.rs.

This helper and config_from_yaml in pnpr/crates/pnpr/tests/policy.rs (Lines 25-53) both re-indent a caller-supplied packages: block and splice it under registries.local.packages, but with subtly different rules (blank-line handling, strip_prefix contract, catch-all appending). Consider factoring a single shared test helper (e.g. in a common test-support module) that both integration test files call, so the splicing/indentation contract only needs to be correct — and updated — in one place.

As per path instructions, "Focus your added depth on: ... (3) missed reuse or duplication across the monorepo."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpr/crates/pnpr/tests/auth_publish.rs` around lines 31 - 61, The test helper
for YAML package-block splicing is duplicated between
static_config_with_packages and config_from_yaml, with slightly different
indentation/blank-line/catch-all behavior. Extract this logic into a shared
test-support helper used by both auth_publish.rs and policy.rs, and keep the
contract for re-indenting under registries.local.packages in one place so future
changes only need to be made once.

Source: Path instructions

🤖 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/route.rs`:
- Around line 383-402: The resolve footprint for package-level upstreams is
still being classified as public even when upstream ACL rules should make the
fetch private. Update the RouteClass selection in route.rs, especially the
Resolved::Concrete { kind: ConcreteKind::Upstream } branch and
RouteContext/route keying, so upstream package rules are consulted when building
the context and read-gated upstreams get a private cache key instead of the
shared public one. Keep anonymous public fetches as RouteClass::Public, but
thread the upstream access rules into the resolve path used by the route
context.

In `@pnpr/crates/pnpr/tests/server.rs`:
- Line 2934: Fix the flagged spelling in the comment near the merged model/ACL
discussion by changing “mis-order” to “misorder” in the test commentary within
server.rs; no code logic changes are needed, just update the wording in the
nearby comment text so Rust CI spell check passes.

---

Nitpick comments:
In `@pnpr/crates/pnpr/src/server.rs`:
- Around line 3100-3167: The repeated write-target resolution and authorization
flow should be extracted into a shared helper to avoid duplication across
update_packument and the other write handlers. Create a helper that combines
resolve_write_target, building RegistrySource::Hosted(target.source.clone()),
and the authorize checks for one or more Action values, then update
update_packument, delete_package, delete_tarball, update_dist_tag, and
validate_publish_doc to call it. Keep the existing behavior and error
propagation unchanged, just centralize the pattern under a clear name like
resolve_authorized_write_target.

In `@pnpr/crates/pnpr/tests/auth_publish.rs`:
- Around line 31-61: The test helper for YAML package-block splicing is
duplicated between static_config_with_packages and config_from_yaml, with
slightly different indentation/blank-line/catch-all behavior. Extract this logic
into a shared test-support helper used by both auth_publish.rs and policy.rs,
and keep the contract for re-indenting under registries.local.packages in one
place so future changes only need to be made once.
🪄 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: 33ef93e8-fc24-4172-ad93-26fcaab1823e

📥 Commits

Reviewing files that changed from the base of the PR and between 02ef9b5 and 522bdfb.

📒 Files selected for processing (17)
  • pnpr/crates/pnpr/config.yaml
  • pnpr/crates/pnpr/src/config.rs
  • pnpr/crates/pnpr/src/config/tests.rs
  • pnpr/crates/pnpr/src/error.rs
  • pnpr/crates/pnpr/src/lib.rs
  • pnpr/crates/pnpr/src/policy.rs
  • pnpr/crates/pnpr/src/policy/tests.rs
  • pnpr/crates/pnpr/src/registry.rs
  • pnpr/crates/pnpr/src/resolver/tests.rs
  • pnpr/crates/pnpr/src/route.rs
  • pnpr/crates/pnpr/src/route/tests.rs
  • pnpr/crates/pnpr/src/server.rs
  • pnpr/crates/pnpr/src/server/tests.rs
  • pnpr/crates/pnpr/src/upstream/tests.rs
  • pnpr/crates/pnpr/tests/auth_publish.rs
  • pnpr/crates/pnpr/tests/policy.rs
  • pnpr/crates/pnpr/tests/server.rs
💤 Files with no reviewable changes (1)
  • pnpr/crates/pnpr/src/error.rs

Comment thread pnpr/crates/pnpr/src/route.rs
Comment thread pnpr/crates/pnpr/tests/server.rs Outdated
Comment thread pnpr/crates/pnpr/src/server.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 522bdfb

- pacquet-pnpr-client's integration test builds an UpstreamConfig
  literal; add the new rules field (this compile error was failing the
  CI test jobs and coverage).
- Index PackageRules::for_package by specificity tier (exact/scope
  maps + any-scoped/all slots) instead of scanning every rule: the
  lookup runs on every read, write, search hit, and route
  classification, and the tier chain makes the winner a map lookup.
- Sharpen the hosted_gate and classify_hosted docs: the effective
  per-package access decides — an explicit entry fully decides its
  names, including opening one name on an otherwise-private registry —
  and only the denial's *shape* varies by tier (default denial masks,
  explicit-entry denial is loud). Classification admits with the same
  lookup serving does, so the two cannot diverge.
- Appease dylint (single-letter closure params, intra-doc link),
  clippy (trailing comma), and typos (mis-order).
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      6.7±0.05ms   650.4 KB/sec    1.02      6.8±0.46ms   638.8 KB/sec

…t path

Route classification now consults the upstream registry's packages:
rules per name: a caller the effective access denies is never handed
the server-owned credential, so a fresh resolve fails closed exactly
where the serving endpoint would deny the read. Cache replay stays
registry-scoped by design (the alias descriptor names no package) —
documented at the descriptor gate.

Search skips a hosted registry outright when no rule of it could admit
the caller, restoring the pre-merge fast path: a blanket mask must not
become an enumeration or scan-timing primitive.
Copilot AI review requested due to automatic review settings July 3, 2026 14:00
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

@github-actions github-actions Bot added the reviewed: coderabbit CodeRabbit submitted an approving review label Jul 3, 2026
@codecov-commenter

codecov-commenter commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.34862% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.62%. Comparing base (02ef9b5) to head (53fed93).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pnpr/crates/pnpr/src/route.rs 87.61% 13 Missing ⚠️
pnpr/crates/pnpr/src/server.rs 95.34% 8 Missing ⚠️
pnpr/crates/pnpr/src/registry.rs 30.00% 7 Missing ⚠️
pnpr/crates/pnpr/src/config.rs 98.83% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12787      +/-   ##
==========================================
+ Coverage   85.59%   85.62%   +0.02%     
==========================================
  Files         413      413              
  Lines       64125    64311     +186     
==========================================
+ Hits        54888    55065     +177     
- Misses       9237     9246       +9     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 715dedd

The cold-mock config carried three routing shapes so one file could
drive any benchmarked pnpr revision, on the premise that every server
ignores the blocks it doesn't recognize. HEAD broke that premise on
purpose: a top-level packages: block is now a startup error, so the
pnpr@HEAD revision mock refused to start and the benchmark job failed.

Keep the two shapes that still coexist (registries:/defaultRegistry:
and mounts:/defaultTarget:) and document that a pre-mount pnpr can no
longer share a config file with current ones.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7403adb

Comment thread pnpr/crates/pnpr/src/config.rs
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 7403adb

Option<IgnoredAny> maps a present-but-null packages: (a bare key, or
~) to None, slipping past the loud rejection. Detect presence through
a custom deserializer that consumes any value — including null — so
every spelling of the removed key fails startup identically.
Copilot AI review requested due to automatic review settings July 3, 2026 15:36
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Commit: 53fed93ae0a6

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.018 ± 0.100 3.927 4.231 1.91 ± 0.11
pacquet@main 4.015 ± 0.124 3.881 4.294 1.91 ± 0.12
pnpr@HEAD 2.101 ± 0.109 1.980 2.279 1.00
pnpr@main 2.161 ± 0.109 1.981 2.329 1.03 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.018140557320001,
      "stddev": 0.10039456693647687,
      "median": 3.9854780939200003,
      "user": 3.8487341,
      "system": 3.4992582399999996,
      "min": 3.92705808442,
      "max": 4.23132079242,
      "times": [
        4.04234923742,
        3.92705808442,
        3.95821687842,
        4.15438093642,
        3.92844858642,
        4.01270347442,
        3.9765743164200003,
        3.9943818714200003,
        4.23132079242,
        3.95597139542
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.01514801362,
      "stddev": 0.12417226788066171,
      "median": 3.96475723042,
      "user": 3.8699114000000003,
      "system": 3.46761074,
      "min": 3.8806904874200003,
      "max": 4.29366291842,
      "times": [
        3.95836019442,
        3.9422018854200003,
        4.29366291842,
        3.9655925304200004,
        4.12366687042,
        3.92336138342,
        3.96392193042,
        3.99143810742,
        4.1085838284200005,
        3.8806904874200003
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.10060477712,
      "stddev": 0.10948574706385154,
      "median": 2.07458233492,
      "user": 2.659427,
      "system": 2.99546154,
      "min": 1.97957946242,
      "max": 2.2785783034200002,
      "times": [
        2.2785783034200002,
        2.0106268804200003,
        2.07463359842,
        2.07453107142,
        2.0752831134200003,
        1.97957946242,
        2.0280885684200003,
        2.2429739784200002,
        2.23295101642,
        2.00880177842
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.1611873879199996,
      "stddev": 0.10927019275365787,
      "median": 2.15917986492,
      "user": 2.6271036,
      "system": 3.00990774,
      "min": 1.98055361042,
      "max": 2.32873525742,
      "times": [
        2.15276659842,
        2.11873715942,
        2.23063180942,
        2.32873525742,
        2.08633651942,
        1.98055361042,
        2.16559313142,
        2.23866247142,
        2.27400987442,
        2.03584744742
      ]
    }
  ]
}

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

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 633.5 ± 11.8 616.8 656.2 1.00
pacquet@main 664.2 ± 78.0 620.6 884.0 1.05 ± 0.12
pnpr@HEAD 726.8 ± 92.7 672.3 985.5 1.15 ± 0.15
pnpr@main 756.4 ± 80.0 693.4 949.9 1.19 ± 0.13
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.63348431038,
      "stddev": 0.011809711790930304,
      "median": 0.6351319211800001,
      "user": 0.3705574800000001,
      "system": 1.32630516,
      "min": 0.6167603071800001,
      "max": 0.65617657518,
      "times": [
        0.63099406418,
        0.63941268918,
        0.63853514918,
        0.6167603071800001,
        0.61988293518,
        0.6373785011800001,
        0.6328853411800001,
        0.65617657518,
        0.64101600418,
        0.62180153718
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.66420539258,
      "stddev": 0.07804366533794395,
      "median": 0.64202782018,
      "user": 0.36657128000000005,
      "system": 1.3470020599999997,
      "min": 0.62063131618,
      "max": 0.88397855818,
      "times": [
        0.64370527918,
        0.62063131618,
        0.64190312618,
        0.6421525141800001,
        0.6285042671800001,
        0.63305710118,
        0.66181055018,
        0.64938838318,
        0.6369228301800001,
        0.88397855818
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.72676939488,
      "stddev": 0.0927484848449469,
      "median": 0.69919339818,
      "user": 0.39236827999999996,
      "system": 1.3641371599999998,
      "min": 0.67232387618,
      "max": 0.98545938718,
      "times": [
        0.6989174971800001,
        0.67232387618,
        0.71119111918,
        0.68090208318,
        0.6938629321800001,
        0.6998546841800001,
        0.68571047018,
        0.69946929918,
        0.98545938718,
        0.7400026001800001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.7563722883799999,
      "stddev": 0.08002526520944013,
      "median": 0.72917760318,
      "user": 0.39235968000000004,
      "system": 1.38667626,
      "min": 0.69335756618,
      "max": 0.9499379721800001,
      "times": [
        0.79897180918,
        0.69335756618,
        0.69590638218,
        0.81124151318,
        0.73152080418,
        0.7026964961800001,
        0.69855877818,
        0.9499379721800001,
        0.7268344021800001,
        0.75469716018
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.211 ± 0.023 4.179 4.242 1.97 ± 0.10
pacquet@main 4.212 ± 0.030 4.176 4.254 1.97 ± 0.10
pnpr@HEAD 2.142 ± 0.108 2.042 2.397 1.00
pnpr@main 2.217 ± 0.126 1.983 2.429 1.03 ± 0.08
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.21057094792,
      "stddev": 0.023391391822376255,
      "median": 4.200291248320001,
      "user": 3.7352734399999994,
      "system": 3.38196384,
      "min": 4.17853999932,
      "max": 4.24162402632,
      "times": [
        4.23134272532,
        4.24162402632,
        4.2003686493200005,
        4.19133407432,
        4.19412575332,
        4.23469811432,
        4.200213847320001,
        4.17853999932,
        4.23901116332,
        4.194451126320001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.21222148962,
      "stddev": 0.02970439001268445,
      "median": 4.20081348632,
      "user": 3.73557774,
      "system": 3.3717686400000004,
      "min": 4.175752274320001,
      "max": 4.253875846320001,
      "times": [
        4.253875846320001,
        4.18295392832,
        4.175752274320001,
        4.20217520332,
        4.19945176932,
        4.23821435232,
        4.25094684932,
        4.18610595932,
        4.19558749832,
        4.23715121532
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.1418604980200002,
      "stddev": 0.10845013190591293,
      "median": 2.11970891232,
      "user": 2.48229754,
      "system": 2.91761374,
      "min": 2.0421416533200003,
      "max": 2.3966652013200003,
      "times": [
        2.10138475232,
        2.1380330723200003,
        2.2436339643200003,
        2.1616001223200003,
        2.04225880732,
        2.08425679732,
        2.3966652013200003,
        2.13984840632,
        2.0421416533200003,
        2.06878220332
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.2165111428200004,
      "stddev": 0.12564788285738857,
      "median": 2.23227594932,
      "user": 2.46721794,
      "system": 2.92573884,
      "min": 1.98319294432,
      "max": 2.42897991432,
      "times": [
        2.21667054132,
        2.42897991432,
        2.12583074032,
        2.2630622433200003,
        1.98319294432,
        2.1180820863200003,
        2.33160480632,
        2.2883715273200003,
        2.2478813573200003,
        2.1614352673200004
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.342 ± 0.013 1.322 1.358 2.01 ± 0.10
pacquet@main 1.407 ± 0.072 1.356 1.603 2.11 ± 0.15
pnpr@HEAD 0.667 ± 0.033 0.646 0.756 1.00
pnpr@main 0.677 ± 0.094 0.641 0.943 1.01 ± 0.15
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.3417539080399998,
      "stddev": 0.013036564430902797,
      "median": 1.3433600705400002,
      "user": 1.31754006,
      "system": 1.68699896,
      "min": 1.32191355254,
      "max": 1.35812235854,
      "times": [
        1.35812235854,
        1.34823781154,
        1.33993298454,
        1.35275352454,
        1.32191355254,
        1.32325035154,
        1.33264861254,
        1.34678715654,
        1.33701889954,
        1.3568738285400002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.4066138695400001,
      "stddev": 0.0715925857597112,
      "median": 1.39059505954,
      "user": 1.3364731600000002,
      "system": 1.7551601599999997,
      "min": 1.35593881254,
      "max": 1.60316791854,
      "times": [
        1.39502800954,
        1.3604703825400002,
        1.60316791854,
        1.4032570155400002,
        1.3722182975400001,
        1.40871534154,
        1.38616210954,
        1.40693143354,
        1.37424937454,
        1.35593881254
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.66676623124,
      "stddev": 0.03308375617872055,
      "median": 0.65594723904,
      "user": 0.33600776,
      "system": 1.2990068599999998,
      "min": 0.6456536845399999,
      "max": 0.75612153054,
      "times": [
        0.64853838954,
        0.75612153054,
        0.67695678954,
        0.65874214054,
        0.64709289554,
        0.6722777975399999,
        0.65636023354,
        0.65553424454,
        0.65038460654,
        0.6456536845399999
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6765368492399999,
      "stddev": 0.09370474337042041,
      "median": 0.64656093754,
      "user": 0.33299776,
      "system": 1.2920636600000002,
      "min": 0.6409560935399999,
      "max": 0.94302316854,
      "times": [
        0.65329017654,
        0.64526533654,
        0.64728255154,
        0.64583932354,
        0.64833175054,
        0.65210720054,
        0.6409560935399999,
        0.64494276554,
        0.64433012554,
        0.94302316854
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.013 ± 0.052 2.964 3.151 4.60 ± 0.09
pacquet@main 3.016 ± 0.038 2.982 3.117 4.60 ± 0.08
pnpr@HEAD 0.682 ± 0.007 0.671 0.694 1.04 ± 0.02
pnpr@main 0.655 ± 0.008 0.642 0.668 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.0128920316800003,
      "stddev": 0.05162303238297894,
      "median": 2.99622050208,
      "user": 1.7844675000000003,
      "system": 1.9609562399999998,
      "min": 2.96419977108,
      "max": 3.15070494808,
      "times": [
        2.99125439408,
        3.0050940110799997,
        2.99979748608,
        2.96419977108,
        2.99047424308,
        3.01564787808,
        2.98743674408,
        2.99264351808,
        3.15070494808,
        3.0316673230799998
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.0157537883800005,
      "stddev": 0.03789501850431374,
      "median": 3.00995607208,
      "user": 1.7972662,
      "system": 1.97431144,
      "min": 2.98150009608,
      "max": 3.11723741708,
      "times": [
        3.01830975808,
        3.00946336408,
        2.99726016608,
        3.00793009208,
        2.98150009608,
        2.98459859508,
        3.01403909908,
        3.01044878008,
        3.11723741708,
        3.01675051608
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.68198339008,
      "stddev": 0.007246548275964527,
      "median": 0.6831774750800002,
      "user": 0.33759429999999996,
      "system": 1.3432887399999998,
      "min": 0.6712437610800001,
      "max": 0.6935887460800001,
      "times": [
        0.6712437610800001,
        0.6841944130800001,
        0.6821564320800001,
        0.6753176990800001,
        0.6853049840800001,
        0.6717809300800001,
        0.6851884290800001,
        0.6821605370800001,
        0.6935887460800001,
        0.6888979690800001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.65538075478,
      "stddev": 0.007543499425344166,
      "median": 0.65418612458,
      "user": 0.32299770000000005,
      "system": 1.2904883399999998,
      "min": 0.6423239890800001,
      "max": 0.6676932040800001,
      "times": [
        0.6629601170800001,
        0.6515722100800001,
        0.6423239890800001,
        0.65485053808,
        0.66117931908,
        0.6598306770800001,
        0.6490158530800001,
        0.6676932040800001,
        0.6508599290800001,
        0.6535217110800001
      ]
    }
  ]
}

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

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 6.911 ± 0.190 6.647 7.299 1.39 ± 0.05
pacquet@main 6.856 ± 0.157 6.637 7.171 1.38 ± 0.05
pnpr@HEAD 4.954 ± 0.128 4.797 5.277 1.00
pnpr@main 4.980 ± 0.095 4.853 5.176 1.01 ± 0.03
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 6.91076838868,
      "stddev": 0.1902083518358758,
      "median": 6.849158878280001,
      "user": 4.01540584,
      "system": 3.7789683800000007,
      "min": 6.647473675780001,
      "max": 7.29928241278,
      "times": [
        6.932701846780001,
        6.830538999780001,
        7.29928241278,
        6.79069801678,
        6.86777875678,
        7.1668374987800005,
        6.93071207778,
        6.82365942878,
        6.81800117278,
        6.647473675780001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 6.855683388979999,
      "stddev": 0.1566863838474555,
      "median": 6.81479851628,
      "user": 4.0336269399999995,
      "system": 3.7845370799999998,
      "min": 6.63666390778,
      "max": 7.17137910178,
      "times": [
        6.77639461378,
        6.9521960617800005,
        7.17137910178,
        6.96706589778,
        6.71900002278,
        6.8532024187800005,
        6.95374544278,
        6.76854067078,
        6.63666390778,
        6.7586457517800005
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 4.954338699279999,
      "stddev": 0.12799288213924553,
      "median": 4.93651372728,
      "user": 2.75993944,
      "system": 3.22496268,
      "min": 4.79694728978,
      "max": 5.27703530378,
      "times": [
        4.991598795780001,
        5.27703530378,
        4.92716597478,
        4.947442623780001,
        4.9458614797800005,
        4.85410580878,
        4.9122244557800006,
        4.79694728978,
        4.991935170780001,
        4.89907008978
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 4.97969829168,
      "stddev": 0.09528845991846709,
      "median": 4.9602803052799995,
      "user": 2.756878339999999,
      "system": 3.24735368,
      "min": 4.85332706578,
      "max": 5.17634656278,
      "times": [
        4.97646704978,
        5.04017315978,
        5.040141691780001,
        4.89345110878,
        5.03561230778,
        4.85332706578,
        4.9341753097800005,
        5.17634656278,
        4.94409356078,
        4.9031950997800005
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12787
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
4,210.57 ms
(-5.92%)Baseline: 4,475.62 ms
5,370.74 ms
(78.40%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
3,012.89 ms
(-1.50%)Baseline: 3,058.75 ms
3,670.50 ms
(82.08%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,341.75 ms
(-1.63%)Baseline: 1,363.99 ms
1,636.79 ms
(81.97%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,018.14 ms
(-8.00%)Baseline: 4,367.73 ms
5,241.28 ms
(76.66%)
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
🚷 view threshold
6,910.77 ms
(-2.01%)Baseline: 7,052.43 ms
8,462.92 ms
(81.66%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
633.48 ms
(+0.65%)Baseline: 629.39 ms
755.27 ms
(83.88%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12787
Testbedpnpr

⚠️ 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-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,141.86 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
681.98 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
666.77 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,100.60 ms
isolated-linker.fresh-restore.cold-cache.cold-store.cold-pnpr📈 view plot
⚠️ NO THRESHOLD
4,954.34 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
726.77 ms
🐰 View full continuous benchmarking report in Bencher

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Comment on lines +1784 to +1792
// A private upstream (registry-level `access:`) is caller-gated for
// every name; a public one can still gate individual names through a
// per-package `access` rule.
RegistrySource::Upstream(source) => {
state.inner.config.upstreams.get(&source).is_some_and(|upstream| {
upstream.access.is_some()
|| !upstream.rules.for_package(package).access.allows(&Identity::Anonymous)
})
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The disjunction is deliberate, and deriving privacy solely from effective per-package access would under-mark here: unlike a hosted registry, an upstream's registry-level access: is enforced independently at serving — authorized_upstream runs before per-package rules on every upstream read — so a per-package access: $all entry cannot open a name on a private upstream, and access.is_some() alone already means the response varies by caller (403 vs 200). Marking such a response shared-cacheable would let a shared HTTP cache replay an authenticated response to an anonymous caller. Documented the asymmetry at the check in 53fed93.

Comment thread pnpr/crates/pnpr/src/route.rs Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5a5c797

@zkochan zkochan added this pull request to the merge queue Jul 3, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 5a5c797

@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 3, 2026
Cache replay was registry-scoped for alias descriptors, so a caller
passing the registry-level gate but denied by a per-package upstream
access refinement could replay a cached resolution a fresh resolve
would refuse them. The alias descriptor now carries the package name —
only when the upstream's rules explicitly refine that name's access —
and replay re-checks the refinement through the same per-package-aware
alias selection a fresh resolve uses. Unrefined names keep the plain
registry-scoped descriptor, so the common footprint stays one
descriptor per alias. The refined metadata mirror namespaces per
package for the same reason.

Also document why resolves_to_private_source treats every name on an
access-bearing upstream as caller-gated: unlike hosted registries, the
upstream registry-level gate is enforced independently at serving
(authorized_upstream runs before per-package rules), so a per-package
'access: $all' entry cannot open a name on a private upstream.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 53fed93

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 53fed93

@zkochan zkochan merged commit 90dd346 into main Jul 3, 2026
34 checks passed
@zkochan zkochan deleted the pnpr-registry-packages branch July 3, 2026 19:49
zkochan added a commit to pnpm/pnpm.io that referenced this pull request Jul 3, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product: pnpr reviewed: coderabbit CodeRabbit submitted an approving review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants