Files
pnpm/pnpr/client
Zoltan Kochan 1dd12bd515 feat(pnpr): make resolver cache authorization-aware (#12700)
Make the pnpr resolver cache authorization-aware and route private
dependencies through per-uplink registry endpoints.

Add route classification at the single fetch/auth-selection point: pnpr
selects its own server-owned upstream credentials instead of forwarding
client upstream auth, records a per-resolve footprint, and rejects inline
URL credentials. A uplinks: entry that declares an access: policy becomes
the private-route credential (folding in the former upstreamAliases block),
matched by registry origin and exposed as a read-only registry endpoint at
/~<uplink>/. access reuses bearer-token-backed pnpr identities, package
access policy, and static groups; a SHA-256 digest of the uplink's credential
participates in the footprint, so rotating the upstream credential
automatically moves new resolves to a fresh namespace. The credential is
attached only over a matching scheme (no token over plain http).

Gate every server-side fetch behind a default-deny allowlist (built-in npm
host, public routes, configured uplinks and their /~<uplink>/ endpoints, and
pnpr itself). A registry/namedRegistries matching none is rejected at the
request boundary, closing the resolver's SSRF surface at the source and
superseding the link-local denylist. The boundary also covers direct-URL
(http(s)/git, incl. scp-style) dependency specs, overrides, and lockfile
tarballs; a `..` path segment is rejected; and the same allowlist re-validates
every redirect hop. The official npm registry is a built-in host-level public
route (scoped names included), so the npmjsPublic toggle is gone. With no
off-allowlist route to resolve, every route is public or carries a private
descriptor: RouteClass::Unknown, the non-shareable footprint tier, and the
MetadataCacheScope::Bypass tier are removed. Transitive deps fetched during the
tree walk are a connect-time-guard follow-up (pnpm/pnpm#12705).

Route resolver tarball URLs through those endpoints. A proxied route emits
its /~<uplink>/ endpoint URL; public routes keep their upstream URL (fetched
directly from the registry/CDN); pnpr-hosted packages use pnpr-hosted URLs.
An endpoint URL is canonical for a client whose scope
points there, so the lockfile entry stays integrity-only and the host comes
from the client's registry config rather than the lockfile — a project
resolves to the same lockfile through /resolve or a direct/proxied install.
verification_lockfile reverses endpoint URLs to upstream for input-lockfile
verification, and classification recognizes pnpr's own /~<uplink>/ URLs.
The opaque per-tarball gateway scheme and its in-memory key->URL map are
removed.

Serve each access-bearing uplink as a /~<uplink>/ registry endpoint
(packument + tarball, gated by the uplink access policy) with a private
cache namespaced by an HMAC of (uplink, credential), so a private install
caches like a public one, a rotation re-keys automatically, and a private
uplink's content never lands in the shared mirror.

Store bounded candidate lists under an auth-excluded base key instead of a
single lockfile. Public candidates match every caller; private candidates
carry the footprint and descriptor HMAC and are reused only when the caller
still satisfies the stored uplink-or-hosted gates. Compute the base key for
both no-lockfile and lockfile-seeded requests, using hash_lockfile() for
input lockfiles, and evict expired/LRU private candidates before public
ones.

Record metadata fast-path routes into the footprint. The hook only fires at
the auth-selection point, which the npm resolver's metadata fast paths
(in-memory hit, offline disk read, version-spec exact match, publishedBy
mtime shortcut) bypass. AuthHeaders::record_route drives the hook without a
request, called up front in pick_package so every layer contributes to the
footprint.

Scope the npm metadata mirror by private access descriptor. A
MetadataCacheScope (Public / Private) classified per (registry, package)
fetch threads through pick_package, fetch_full_metadata_cached, the mirror
path, in-memory/fetch-lock keys, and the verifier's local-mirror read. A
private route stores its packument under v11/metadata-private/<descriptor-id>/.
Fail closed on 401/403/private-404. The CLI installs no hook, so every fetch
stays Public and the global mirror is unchanged.

Related to pnpm/pnpm#12699 and pnpm/rfcs#11.
2026-06-28 21:51:47 +02:00
..
2026-06-23 17:16:24 +02:00
2026-06-23 17:16:24 +02:00

@pnpm/pnpr.client

Client library for the pnpr server. Resolves a project's dependencies server-side and returns the resolved lockfile.

How it works

  1. Sends POST /v1/install to the pnpr server with the project's dependencies (and the existing lockfile, if any, for incremental resolution).
  2. The server resolves against the client's registries, verifies the input lockfile under the client's policy, and answers with one gzipped JSON object carrying the resolved lockfile and stats.
  3. Returns the resolved lockfile for use with pnpm's headless install, which fetches every tarball directly from the registries in parallel — like a normal install. See pnpm/pnpm#12230.

pnpr is a stateless resolver: it stores no tarballs and serves no file content.

Usage

This package is used internally by pnpm when the pnprServer config option is set. It is not intended to be called directly, but can be used programmatically:

import { fetchFromPnpmRegistry } from '@pnpm/pnpr.client'

const { lockfile, stats } = await fetchFromPnpmRegistry({
  registryUrl: 'http://localhost:4000',
  dependencies: { react: '^19.0.0' },
  devDependencies: { typescript: '^5.0.0' },
})

console.log(`Resolved ${stats.totalPackages} packages`)
// lockfile is ready for headless install

Configuration

Add to pnpm-workspace.yaml to enable automatically during pnpm install:

pnprServer: http://localhost:4000