mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-14 09:42:37 -04:00
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.
@pnpm/pnpr.client
Client library for the pnpr server. Resolves a project's dependencies server-side and returns the resolved lockfile.
How it works
- Sends
POST /v1/installto the pnpr server with the project's dependencies (and the existing lockfile, if any, for incremental resolution). - 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.
- 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