Problem
pnpr's config has two top-level surface toggles:
registry:
enabled: true # npm registry surface: packument/tarball reads, publish,
# dist-tags, search, login/token endpoints
resolver:
enabled: true # install accelerator: /-/pnpr, /resolve, /verify-lockfile
The registry: field duplicates information the config already carries, and can contradict it. mounts: declared together with registry: {enabled: false} is a config that declares structure and then disclaims it: the mount graph is parsed and validated, but upstream credentials are deliberately not resolved, so the mounts are dead weight the resolver cannot use either. The field also forces defensive code — deny_unknown_fields exists on the feature block specifically so an enable: typo doesn't silently leave a surface on.
Naming is a secondary problem: a top-level key named after the product's central noun turns out to mean "HTTP surface toggle", and it collides with any future registries:-style key.
Proposal
Remove only registry:. Derive the registry surface from declared structure. Keep resolver:. Decouple the account endpoints from the registry surface.
- The npm registry surface is served iff at least one mount is declared. No
mounts: ⇒ nothing to serve ⇒ the surface is not mounted. The YAML registry: key is removed (pnpr is pre-1.0; no compatibility shim).
- The
resolver: key stays. It is not the same case: there is no structure it duplicates and nothing it can contradict — it is the only expression of a genuinely non-derivable operator choice ("does this installation offer resolution compute to token holders at all?"). That is an installation-level policy that belongs versioned in the config file, not scattered across launch flags. Removing it would only serve symmetry, and the asymmetry is a feature: one surface has declared structure to derive from, the other doesn't.
- The user/token endpoints move out of the registry surface.
PUT /-/user/org.couchdb.user:{name} (login), /-/whoami, /-/npm/v1/user, /-/npm/v1/tokens, and the token-revocation endpoints are pnpr account management, not package-registry functionality — they only ride the registry surface today because they arrived with the npm protocol implementation. The misclassification is visible in the code itself: the auth backend already loads whenever either surface is enabled ("both the registry and resolver need auth because resolver requests control outbound dependency-resolution work"), yet the endpoints that mint the tokens the resolver demands are gated on registry_enabled. A resolver-only tier can therefore validate tokens but not issue them — today users must mint tokens on a registry-serving replica sharing the auth backend and hand-write the _authToken line for the resolver host into .npmrc. Serve the account endpoints whenever any authenticated surface is up (effectively always, alongside /-/ping):
pnpm login --registry https://<resolver-host>/ works on a resolver-only tier; the token is a pnpr account credential, and npm's login protocol just needs a base URL.
- Without this, deriving the registry surface from mounts would institutionalize the gap: a no-mounts config would lose login by construction rather than by explicit choice.
- No new exposure: token validation already runs on every route of every tier; minting stays gated by
auth.htpasswd.max_users and existing-user passwords exactly as before.
--disable-registry / --disable-resolver stay for per-tier overrides. A resolver-only tier is a deployment property: the same shared config.yaml deploys to both tiers, differing only by flag. The loader's existing behavior (a disabled registry surface skips strict upstream-credential resolution, so a resolver tier neither fails on nor carries registry secrets it never uses) keys off effective enablement, which already folds CLI overrides in — that path barely changes.
- The "at least one surface" startup check becomes: no mounts and the resolver disabled (by config or flag) ⇒ "nothing to serve" error. Same guarantee, derived from the same facts.
Consequences to handle deliberately
- The mount graph should still be built and validated on every tier (as today), so a broken router fails startup even on a resolver-only deployment.
- Docs: the
registry/resolver sections in pnpm.io's pnpr configuration.md, endpoints.md (the user/token endpoints table moves out of the "mounted when registry.enabled" group), cli.md, and the install-acceleration login note need updating once this lands.
Context
Problem
pnpr's config has two top-level surface toggles:
The
registry:field duplicates information the config already carries, and can contradict it.mounts:declared together withregistry: {enabled: false}is a config that declares structure and then disclaims it: the mount graph is parsed and validated, but upstream credentials are deliberately not resolved, so the mounts are dead weight the resolver cannot use either. The field also forces defensive code —deny_unknown_fieldsexists on the feature block specifically so anenable:typo doesn't silently leave a surface on.Naming is a secondary problem: a top-level key named after the product's central noun turns out to mean "HTTP surface toggle", and it collides with any future
registries:-style key.Proposal
Remove only
registry:. Derive the registry surface from declared structure. Keepresolver:. Decouple the account endpoints from the registry surface.mounts:⇒ nothing to serve ⇒ the surface is not mounted. The YAMLregistry:key is removed (pnpr is pre-1.0; no compatibility shim).resolver:key stays. It is not the same case: there is no structure it duplicates and nothing it can contradict — it is the only expression of a genuinely non-derivable operator choice ("does this installation offer resolution compute to token holders at all?"). That is an installation-level policy that belongs versioned in the config file, not scattered across launch flags. Removing it would only serve symmetry, and the asymmetry is a feature: one surface has declared structure to derive from, the other doesn't.PUT /-/user/org.couchdb.user:{name}(login),/-/whoami,/-/npm/v1/user,/-/npm/v1/tokens, and the token-revocation endpoints are pnpr account management, not package-registry functionality — they only ride the registry surface today because they arrived with the npm protocol implementation. The misclassification is visible in the code itself: the auth backend already loads whenever either surface is enabled ("both the registry and resolver need auth because resolver requests control outbound dependency-resolution work"), yet the endpoints that mint the tokens the resolver demands are gated onregistry_enabled. A resolver-only tier can therefore validate tokens but not issue them — today users must mint tokens on a registry-serving replica sharing the auth backend and hand-write the_authTokenline for the resolver host into.npmrc. Serve the account endpoints whenever any authenticated surface is up (effectively always, alongside/-/ping):pnpm login --registry https://<resolver-host>/works on a resolver-only tier; the token is a pnpr account credential, and npm's login protocol just needs a base URL.auth.htpasswd.max_usersand existing-user passwords exactly as before.--disable-registry/--disable-resolverstay for per-tier overrides. A resolver-only tier is a deployment property: the same sharedconfig.yamldeploys to both tiers, differing only by flag. The loader's existing behavior (a disabled registry surface skips strict upstream-credential resolution, so a resolver tier neither fails on nor carries registry secrets it never uses) keys off effective enablement, which already folds CLI overrides in — that path barely changes.Consequences to handle deliberately
registry/resolversections in pnpm.io's pnpr configuration.md, endpoints.md (the user/token endpoints table moves out of the "mounted whenregistry.enabled" group), cli.md, and the install-acceleration login note need updating once this lands.Context
pnpr/crates/pnpr/src/config.rs; the account endpoints'registry_enabledgating and the "both surfaces need auth" startup comment live inpnpr/crates/pnpr/src/server.rs.