Skip to content

rfc(pnpr): registries declare their namespace; rename mounts to registries#16

Merged
zkochan merged 2 commits into
mainfrom
mount-level-patterns
Jul 3, 2026
Merged

rfc(pnpr): registries declare their namespace; rename mounts to registries#16
zkochan merged 2 commits into
mainfrom
mount-level-patterns

Conversation

@zkochan

@zkochan zkochan commented Jul 2, 2026

Copy link
Copy Markdown
Member

Two revisions to the registry-mounts RFC, now reflecting the model as named registries:

1. Patterns move onto the concrete registries; routers become ordered source lists

Every concrete registry (hosted or upstream) declares the package-name patterns: it serves — its namespace — enforced at the registry itself, on every path to it: an off-pattern publish is rejected and an off-pattern read is a definitive 404, whether the request came through a router or addressed /~<name>/ directly. A router collapses to sources: [a, b, c]: the first source whose patterns claim the name wins, authoritatively. A pattern-less registry serves every name (the catch-all) and must be listed last.

This closes the gaps of the route-level-pattern iteration (implemented by pnpm/pnpm#12747):

  • a hosted registry could accept a publish of any name — dormant squatted state surfaced as authoritative by later route edits, and typo'd scopes published silently;
  • a private upstream would fetch arbitrary public names through its server-owned credential for any caller its access: admitted;
  • the same patterns had to be restated by every router, and the copies could drift.

Routing is now derived from declared ownership — a router orders competing claims but can never assign a name to a registry that doesn't claim it. Validation translates to the same covers() machinery: unreachable sources (including a non-last catch-all), shadowed patterns, duplicate sources/patterns, non-concrete sources — all startup/reload errors. Registries whose namespaces overlap in both directions cannot be ordered at all and fail validation as genuinely ambiguous provenance.

2. Rename: mounts:registries:, defaultTarget:defaultRegistry:

Every explanation of a mount began "a mount is a full npm registry at…" — when every definition of X starts with "X is really a Y", the name should be Y. The rename aligns the server config with the client's namedRegistries, and defaultRegistry is instantly legible because it is npm's own concept. The former blocker — the top-level registry: surface toggle as a sibling key — was removed separately (pnpm/pnpm#12767), freeing the name.

The RFC's vocabulary is now two terms: a registry is a named surface pnpr serves at /~<name>/; an origin is the external URL an upstream registry fetches from. Both prior shapes are recorded as rejected alternatives in the Rationale section (route-level patterns; the mounts: name), and the Implementation section states that pnpm/pnpm#12747 shipped the earlier shape and this revision replaces it outright (pre-1.0, no compatibility mode).

Trade-offs recorded: per-router narrowing is no longer expressible (covered by the packages: ACL in the identity dimension; two upstream registries over one URL where truly needed); a follow-up question notes the specificity-based (order-free) source-selection alternative and why declared order is kept.

The file keeps its 0000-pnpr-registry-mounts.md name so existing links don't break.

A concrete mount now declares the package-name patterns it serves — its
namespace — enforced at the mount itself on reads and writes, on every
path to it. Routers drop per-route patterns and become ordered sources:
lists; a package resolves to the first source whose patterns claim it.

This closes the gaps of the route-level-pattern iteration: a hosted
mount could accept a publish of any name (dormant squatting surfaced by
later route edits, silent typo publishes), a private upstream would
fetch arbitrary public names through its server-owned credential, and
the same patterns were restated by every router.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@zkochan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 929f39aa-75b0-41f9-abd3-b1f5f23a5aa8

📥 Commits

Reviewing files that changed from the base of the PR and between 84639f3 and de76f0b.

📒 Files selected for processing (1)
  • pnpr/text/0000-pnpr-registry-mounts.md
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mount-level-patterns

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

RFC: move package patterns onto mounts and simplify routers to ordered sources

📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Move package-name patterns from router routes onto concrete mounts.
• Define routers as ordered sources:; first mount that claims a name wins.
• Specify mount-level enforcement to prevent off-namespace reads/writes and credential abuse.
Diagram

graph TD
  A([Client]) --> B["Router mount (sources)"] --> C["Concrete mount"] --> D{"Patterns claim name?"}
  D -- "no" --> H["404 / reject"] --> G([Response])
  D -- "yes, hosted" --> E[("Hosted storage")] --> G
  D -- "yes, upstream" --> F[["Upstream registry"]] --> G

  subgraph Legend
    direction LR
    _svc["Service"] ~~~ _dec{"Decision"} ~~~ _db[("Storage")] ~~~ _ext[["External"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Specificity-based source selection (order-free)
  • ➕ Removes ordering footguns (no “catch-all must be last” rule).
  • ➕ Still statically checkable with the restricted pattern language.
  • ➖ Less obvious operational rule than explicit order.
  • ➖ Needs a deterministic tie-break/validation story for equal-specificity overlaps.
2. Keep router route-level patterns, add mount enforcement
  • ➕ Preserves per-router narrowing (different routers expose different slices).
  • ➕ Keeps router as the single routing surface for operators.
  • ➖ Reintroduces duplication/drift (patterns declared on routes and implied by mounts).
  • ➖ Harder to ensure direct /~/ access remains safe without duplicating rules.
3. Per-router per-source pattern overrides
  • ➕ Allows narrowing/expanding a source per router without duplicating whole mounts.
  • ➕ Can preserve mount defaults while enabling exceptional cases.
  • ➖ Adds a second pattern layer, complicating validation and operator mental model.
  • ➖ Risks reintroducing the same “namespace vs routing” decoupling this PR removes.

Recommendation: The PR’s approach (mount-declared patterns: + router sources: ordering) is the strongest default because it makes namespace ownership a single enforced declaration and closes the documented safety gaps (off-namespace publishes, and spending private upstream credentials on public names). The only compelling alternative is specificity-based selection; if ordering mistakes become a common operational issue, consider revisiting specificity once there’s implementation experience.

Files changed (1) +256 / -136

Documentation (1) +256 / -136
0000-pnpr-registry-mounts.mdRevise mounts RFC: mount-level 'patterns:' and router 'sources:' +256/-136

Revise mounts RFC: mount-level 'patterns:' and router 'sources:'

• Updates the RFC to declare package-name patterns on hosted/upstream mounts and enforce them on reads and writes. Redefines routers as ordered 'sources:' where the first claiming mount wins, and revises validation rules, examples, test plan, and trade-off discussion accordingly.

pnpr/text/0000-pnpr-registry-mounts.md

Every explanation of a mount began "a mount is a full npm registry
at..." — name it what it is. The former blocker, the top-level
registry: surface toggle, was removed separately (pnpm/pnpm#12767).

mounts: -> registries:, defaultTarget: -> defaultRegistry:. The
vocabulary is now two terms: a registry is a named surface pnpr
serves at /~<name>/; an origin is the external URL an upstream
registry fetches from. The old name is recorded as a rejected
alternative.
@zkochan zkochan changed the title rfc(pnpr): declare patterns on mounts, reduce routers to ordered sources rfc(pnpr): registries declare their namespace; rename mounts to registries Jul 3, 2026
zkochan added a commit to pnpm/pnpm that referenced this pull request Jul 3, 2026
pnpm/rfcs#16 was updated to rename the config surface: every explanation
of a mount began "a mount is a full npm registry at...", so the unit is
now simply a **registry**. `mounts:` becomes `registries:` and
`defaultTarget:` becomes `defaultRegistry:` (npm's own concept), aligning
the server with the client-side `namedRegistries`. pnpr is pre-1.0, so
the old keys are replaced outright with no compatibility mode.

The Rust surface follows the RFC's reference sketch: `MountKind` →
`Registry`, `Mounts` → `Registries`, `MountConfigError` →
`RegistryConfigError`, the `mount` module → `registry`, `Config.mounts`
→ `Config.registries`, and prose/error messages now say "registry"
("name a hosted registry", `/~<name>/`). The axum-level "routes are
mounted" wording is kept — that is framework vocabulary, not the
renamed concept. The vocabulary is now: **registry** — a named surface
pnpr serves; **origin** — the external URL an upstream registry fetches
from; "uplink" remains only as the internal upstream-backend term.
zkochan added a commit to pnpm/pnpm that referenced this pull request Jul 3, 2026
… sources (#12778)

Implements the revised model from pnpm/rfcs#16, replacing the
route-level-pattern shape outright (pre-1.0, no compatibility mode).

The config surface is renamed per the RFC: `mounts:` -> `registries:`,
`defaultTarget:` -> `defaultRegistry:`, and the Rust types follow
(`MountKind` -> `Registry`, `Mounts` -> `Registries`, the `mount` module
-> `registry`). The vocabulary is now: registry — a named surface pnpr
serves; origin — the external URL an upstream registry fetches from.

Hosted and upstream registries take an optional `patterns:` list — their
declared namespace (omitted = every name) — and a router collapses to an
ordered `sources:` list: a package resolves to the first listed source
whose patterns claim it. The namespace is enforced at the registry, on
every path to it: an off-pattern read is a definitive 404 answered before
storage or the upstream is consulted, and an off-pattern publish is
rejected with a clear reason — through a router and at the registry's own
`/~<name>/` URL alike. This closes the open hosted namespace (no dormant
stored state that a later source edit would surface as authoritative) and
stops an authorized caller from pulling arbitrary public names through a
private upstream's server-owned credential.

Validation translates to the same `covers()` machinery: unreachable
sources (all claims covered by earlier sources' union, including a
non-last pattern-less source), per-pattern shadowing across sources
(identical claims by two sources rejected, so bidirectionally-overlapping
namespaces fail in either order), duplicate sources per router, duplicate
patterns per registry, plus the carried-over unknown/self-referential/
router-as-source/empty-router checks. A registry's own internally
redundant patterns are allowed and do not count as self-shadowing.

Patterns live only in the registry graph (`Config::registries`), not
duplicated into the hosted/uplink tables: enforcement happens in
`Registries::resolve`, which every read, write, search, and cache-header
decision flows through, so the namespace is one declaration. The bundled
config.yaml moves the registry-mock fixture list onto the `local`
registry, and `Config::proxy` / `Config::static_serve` build the
equivalent graphs programmatically.
@zkochan zkochan merged commit e992b4a into main Jul 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant