Two leftovers from pnpm/pnpm#14189, both behaviour-neutral.
`artifactBlobDigest` was a pass-through around a private `blobId` that did
the actual work, so the digest had two names and the doc comment sat on
neither the definition nor every call site. There is now one exported
function; the three call sites that only wanted the integrity validated
call it and discard the result, as they did before under the other name.
The restore's store lookup tested `!downloaded.contains_key` and then the
download below tested it again, so a reader had to hold both in mind to see
that the lookup only runs when the bytes are not already in memory. Folding
the lookup into the download's own branch says it once. `blob_id`'s error is
propagated rather than swallowed by `if let Ok(..)`: the manifest validator
rejects a malformed integrity long before hydration, so this is unreachable,
and a silent fall-through to downloading is a worse way to express that than
a `?`.
Follow-up to pnpm/pnpm#14189.
Reuse store content when restoring a remote build.
A remote side-effects restore downloaded every file of the artifact, then handed the bytes to the store, which addresses content by the very digest the artifact manifest already carries. A built package's files are mostly its own, and artifacts share files with one another, so most of what was transferred was content the store already had.
Ask the store first. `locateFileInStore` answers with the path it holds for a digest and mode, or nothing, and the restore only fetches what is missing. The store keeps executable and non-executable content apart, so the mode is part of the question.
Nothing is trusted that was not trusted before: the store addresses content by its hash, so a file it holds under an artifact's digest is that artifact's bytes.
The pacquet side already had the store helper this needs; its test pins the part that could silently drift — a digest or executable-bit rule diverging between the write side and the artifact side would not fail anything, it would just make the lookup always miss.
Follow-up to pnpm/pnpm#14171.
Ambiguous object-store failures can commit a shared-artifact blob while returning an error. Conservatively retaining the reserved quota prevents limit bypasses, but can permanently charge storage that no committed envelope references.
Register each publication in the conditionally written quota document before it reads or creates immutable objects. When an ambiguous failure marks reclamation as necessary, let one replica acquire a token after all active publications drain. The token blocks new publications while the collector derives reachability from stored envelopes, removes unreferenced blobs, rescans physical usage, and atomically replaces the quota state. Preserve the retry marker when collection fails, retry transient cleanup failures from live requests, jitter conditional-write backoff, and retain the documented quiesced quota-reset procedure for registrations or tokens left by terminated processes.
Also synchronize the recursive-run bail summary regression with bounded marker waits so it deterministically exercises recording a task that was in flight when another task failed without risking a hung test.
On Windows, a competing `create_dir` can report `AccessDenied` while the prior lock directory is delete-pending. Retry that platform-specific transient error within the caller's wait and a one-second release budget, while continuing to report persistent permission failures.
Related to pnpm/pnpm#14220.
Object-store write errors do not prove that a create-only write failed. A
backend can commit an artifact and then return a transport error, so releasing
that object reservation can make the quota counter smaller than physical
storage.
Retain quota for every attempted object on non-conflict errors while still
releasing the reservation for later, unattempted objects. This deliberately
prefers conservative overcounting to allowing the storage limits to be
exceeded. Document the recovery procedure and cover both pre-commit and
post-commit failure paths.
Follow-up to pnpm/pnpm#14222.
Promote the signed shared-artifact service from a resolver sub-feature to an independent pnpr surface, allowing artifact-only deployments.
Use the configured object store for immutable artifact blobs and envelopes. Coordinate the durable quota counter with conditional writes so concurrent replicas cannot lose updates, while retaining the existing local cache layout and a small local quota lock for filesystem deployments. Apply the protocol's variant bound while resolving instead of serializing publishers.
Closespnpm/pnpm#14220.
Integrate the remote side-effects cache proof of concept with dependency installation in both TypeScript pnpm and pacquet.
Plan eligible build candidates before contacting pnpr, batch lookup requests, verify the configured P-256 trust keys and artifact integrity, hydrate verified files into CAFS, and select the restored side-effects map before lifecycle scripts run. Fall back to the ordinary local build on all cache failures.
Configure the feature through the remoteSideEffectsCache setting, assembled by the config reader from the workspace file, the global config file and the environment. A repository declares which organization and packages are eligible and nothing else: everything describing the act of signing stays with the machine holding the key, so a cloned repository cannot turn that key into a signing oracle.
Capture and publish the actual post-build diff from trusted builders. Bound artifact hydration and blob downloads, retain CAFS paths instead of downloaded buffers, and withhold direct store writes from a store opened read-only. Add transactional owner and global storage accounting to pnpr alongside request, response, timeout, streaming-memory, manifest, variant, descriptor, lock-namespace, concurrency, and crash-residue bounds.
Keep the proof of concept organization-scoped and Linux glibc-only. Defer lockfile pinning, persistent quarantine, publisher ownership, and key lifecycle policy to the RFC.
Related to pnpm/rfcs#20.
Allow revision-only updates to delegate dependency resolution to pnpr.
Carry updatePatches across both pnpr clients and map it server-side to pacquet's RefreshRevisions seed policy. Explicit refreshes bypass frozen reuse and pnpr's whole-resolution cache, while the registry resolver revalidates metadata even when its in-memory or disk cache is warm.
Route both pnpm implementations through their existing pnpr install and materialization paths. The Rust update command accepts --pnpr-server, and partial TypeScript workspace refreshes continue locally so unselected importers are not refreshed.
Follow-up to https://github.com/pnpm/pnpm/pull/14175.
Related to https://github.com/pnpm/rfcs/pull/19.
The pnpr resolution protocol omitted patchedDependencies and packageExtensions. Its fast path therefore returned an untransformed lockfile, while bypassing pnpr for these projects would sacrifice the remote-resolution performance the setting exists to provide.
Compute patch hashes on the client and send only the ordered selector-to-hash map to pnpr. The server uses those hashes to key patched snapshots in the lockfile, while the client retains the local paths and applies the patch files during materialization. Send package extensions and unused-patch policy alongside them so pnpr resolves the same transformed dependency graph as local resolution.
Key pnpr's interned configuration and resolution cache by the forwarded transforms, validate patch digests at the protocol boundary, and scan package-extension dependency URLs through the existing fetch allowlist and inline-credential checks. Implement the protocol in both TypeScript and Rust clients and cover it with live server-to-client tests. Both clients validate the returned transform metadata so older servers that ignore the new request fields fail with a stable protocol error instead of silently returning an untransformed lockfile. pnpr advertises transform support in the HTTP response headers, allowing the Rust client to validate compatibility before consuming any package frames. Current servers retain streaming prefetch performance; older servers fail before they can trigger downloads or unbounded hint buffering.
Fixespnpm/pnpm#14177.
A resolve request carried the client's `minimumReleaseAge` and
`trustPolicy` but not its `resolutionMode`, and `intern_config` never
set `config.resolution_mode`, so every pnpr-delegated resolve ran on the
server's `highest` default. A project configured for `time-based` or
`lowest-direct` got a lockfile pinning the highest satisfying version of
everything, with nothing to show that the setting had been dropped.
The mode now travels with the request and reaches the interned config,
which is what `PickPolicy::from_config` reads on the server side. It
also joins both cache keys — the interned-config key and the resolution
cache key — so two modes can neither share a config nor replay each
other's resolution.
`VerifyLockfileOptions` deliberately does not carry it: verification
judges an existing lockfile against the client's policy and picks no
version.
Source the fallback for an omitted setting from the config's own
defaults rather than a hardcoded copy of them, and restore the
rationale for gating the input-lockfile fallback on a frozen request.
Send the three settings inline like every other optional field of the
resolve request: the server reads an explicit null the same as an
absent key, so the manual inserts bought nothing.
Give the resolver settings their own forwarding test instead of
folding them into the verification-policy one.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
Registries do not all lay out tarball URLs the way the npm registry does.
JFrog Artifactory repeats the scope in a scoped package's tarball filename
(`@acme/widget/-/@acme/widget-1.0.0.tgz`) where npm strips it. pnpm cannot
rebuild such a URL, so it writes it out for every scoped package instead of
omitting it, and the lockfile carries a host-specific URL per dependency.
pnpm had no place to record such a fact, because it had no place to
describe a registry at all — only three ways to name one. `registries`
mapped a scope to a URL, `namedRegistries` mapped a bare-specifier prefix
to a URL, and neither could carry anything else.
`registries` now declares a registry once, keyed by its URL, with every
fact about it in the entry: a `serverType`, the `scopes` routed to it,
and the `prefix` it answers to. `serverType` has three states:
undeclared strict; only the exact canonical URL is reconstructible
npm also serves the percent-encoded scoped path
artifactory repeats the scope in the tarball filename
registry.npmjs.org resolves to `npm` as a built-in, so its behavior is
unchanged and the old hostname check becomes that one default rather than a
special case in the predicate. `npm` cannot be the default: asserting
npmjs-compatibility is a claim only the operator can make.
The URL is the key because every fact in an entry is a fact about that
server. Keying the layout by scope would bind it to whoever the scope
currently points at, so two developers whose scope resolves differently
would write lockfiles that disagree about which URLs may be omitted.
`scopes` and `prefix` are routes to the registry and are inverted at
config-read time into the two lookups the rest of pnpm already queries,
leaving the resolver, installer, and lockfile layers untouched and the
precedence chain (builtin < .npmrc < yaml < `_auth` < CLI) unchanged.
The layout is declared, never inferred. Sniffing the registry URL cannot work:
a virtual repository serves both layouts at once, depending on whether each
package was synced from upstream or published locally, so no registry-level
signal — route, response header, or probe — decides it. Declaring it also
keeps a wrong guess a fixable misconfiguration instead of a silent breakage.
`serverType` feeds a single URL builder that both sides of the lockfile use:
the writer omits a tarball URL only when the builder reproduces it, and the
reader rebuilds it with the same call. They therefore agree by construction,
so pnpm never has to assume a registry serves some second URL as well.
The setting lives in pnpm-workspace.yaml rather than .npmrc because the
lockfile depends on it: one developer omitting URLs that another reconstructs
differently would break a frozen install. A `serverType` in the global
config.yaml is ignored for the same reason, while the routes declared
alongside it are kept. Credentials are rejected there — the file is
committed — and still belong in .npmrc. The registry URL is the map key, so
the request-destination env gate applies to keys as well as values.
Credentials and unknown fields are refused after parsing, since a parse
error renders the offending source line verbatim.
A map whose values are all strings is the older `<scope>: <url>` shape and
is still read as one. Mixing the two shapes in one map is refused, and so is
a URL-keyed entry written as a string. `namedRegistries` is deprecated in
favor of `prefix` and is read only for prefixes `registries` does not
declare; a prefix stays singular because it is the registry's identity in a
lockfile dep path.
An entry that routes nothing to itself and matches no configured registry is
reported as a warning rather than silently ignored; it is a warning and not
an error because a shared config dependency can legitimately describe
registries a given project does not use.
Config dependencies and pnpr-server-mode resolution pass no server type; in
both, the writer and the reader share that default, so they stay consistent.
Closespnpm/get-npm-tarball-url#16. Supersedes pnpm/pnpm#13920.
pnpr resolves and enforces policy server-side, and neither client runs its own
verifyLockfileResolutions when a pnpr server is configured. The server assigns
each field from the request unconditionally, so a field a client omits is cleared
rather than defaulted, and the server resolves under inputs the user never chose.
The TypeScript client sent only minimumReleaseAge of the verification policy, so
minimumReleaseAgeExclude entries stopped applying and the input-lockfile verifier
could reject a lockfile whose entries the user had explicitly excluded. It also
never sent the resolution mode, so --frozen-lockfile silently resolved and rewrote
the lockfile it promises to leave alone.
pacquet never sent catalogs, so every catalog: specifier failed to resolve — the
bug #13233 fixed for the TypeScript client and the server, leaving the Rust client
behind.
Drop the TRUST_POLICY_INCOMPATIBLE_WITH_PNPR guard, written before the server
gained trust-policy enforcement and refusing an install pacquet runs happily.
A `catalog:` specifier in a workspace's dependencies or overrides failed to
resolve when the install was routed through a pnpr server, erroring with
"No catalog entry '<name>' was found for catalog 'default'." even though the
catalog entry existed.
The pnpr server reconstructs the requested workspace in a temp dir from the
resolve request. That reconstruction wrote a pnpm-workspace.yaml with only a
packages: section and no catalog definitions, and passed catalogs_override:
None to the install — so the server had no catalogs and could not resolve any
catalog: specifier, in dependencies or overrides.
Forward the client's workspace catalogs in the resolve request and use them as
the install's catalogs_override, which feeds both dependency and override
catalog resolution. The client now sends its raw overrides (the server resolves
their catalog: references) instead of pre-resolving them locally.
Closespnpm/pnpm#13232
Remove the legacy repository changelog files now that release changelog storage defaults to the registry. The publish path composes and injects CHANGELOG.md into release tarballs, so keeping historical copies in source control duplicates generated release data.
Update adm-zip to the patched 0.6 release and override vulnerable transitive versions after the dependency audit began rejecting versions below 0.6.0.
Pacquet's recursive command pipeline previously dispatched selected projects independently or lost the selection before package-manager execution. This could overwrite earlier manifest mutations, truncate workspace lockfile state, or materialize projects outside the requested filter.
Thread one workspace selection through command dispatch and package-manager execution, batch manifest mutations into a single install, retain the complete wanted lockfile, and derive current-lockfile and node_modules state from the selected dependency closure. Apply the same workspace root and selection semantics to query, state, global, deploy, and pnpr-backed paths.
Keep full-workspace recursive installs on the ordinary unfiltered fast and frozen paths, and add regression coverage for filtering, lockfile preservation, dependency closure materialization, pnpr multi-importer responses, and TypeScript parity edge cases.
Depends on pnpm/pnpm#13016.
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
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.
The pnpr resolver's two POST endpoints were the only proprietary routes
served outside npm's reserved /-/ namespace, where they overlapped the
package path space (a package literally named `v1`). Move them under the
/-/pnpr namespace alongside the existing capability handshake:
POST /v1/resolve -> POST /-/pnpr/v0/resolve
POST /v1/verify-lockfile -> POST /-/pnpr/v0/verify-lockfile
The GET /-/pnpr handshake now advertises protocol version 0 to match, and
the Rust client's PROTOCOL_VERSION drops to 0. Keeping every pnpr route in
the reserved namespace removes the package-collision concern and lets the
disabled-resolver branch stop special-casing the old npm-compatible GET
paths.
No backward compatibility is kept: the resolver protocol is not yet
released. Server and both clients (Rust pacquet-pnpr-client and the
TypeScript `@pnpm/pnpr.client`) change together.
The TypeScript pnpm CLI freezes at v11; pnpm 12 will be the Rust pacquet
port. To make that split legible, all TypeScript source, test, and build
directories move under a new top-level pnpm11/ directory. The name states
the version boundary rather than implying a behavioral fork, since the two
stacks are meant to behave identically.
Scope is source-only: the shared workspace root stays at the repo root.
pnpm-workspace.yaml, package.json, pnpm-lock.yaml, .pnpmfile.cjs,
.meta-updater, __patches__, .changeset, .husky, and the lint/spell configs
remain in place, so one pnpm workspace and one Cargo workspace still span
all three products. pnpr/client and pacquet/tasks/registry-mock stay as
cross-product workspace members.
Rewiring the move required:
- pnpm-workspace.yaml globs prefixed with pnpm11/
- root package.json script paths, eslint.config.mjs, tsconfig.lint.json,
.gitignore, and CODEOWNERS updated
- .meta-updater/src/index.ts literals repointed (pnpm11/pnpm/package.json,
pnpm11/__utils__, pnpm11/__typings__, and the main package directory)
- regenerated every moved package's repository/homepage URL via meta-updater
- pnpm11/pnpm/bundle-deps.ts and __utils__/scripts/src/typecheck-only.ts
climb one more level to reach the repo root
.meta-updater stays at the repo root because @pnpm/meta-updater resolves
its config at <cwd>/.meta-updater/main.mjs.
TS CI (.github/workflows/ci.yml) now only runs when pnpm11/-relevant paths
change, via a dorny/paths-filter changes job plus a TS CI / Success
aggregate gate; branch protection should require only that gate.
pnpm can now use different auth tokens for different package scopes, even when those scopes use the same registry URL.
Previously, auth was selected only by registry URL. If `@org-a` and `@org-b` both used `https://npm.pkg.github.com/`, they had to share the same token. This caused problems for registries that issue tokens per organization or per scope.
Configure a scope-specific token by adding the package scope after the registry URL in the auth key:
```ini
@org-a:registry=https://npm.pkg.github.com/
@org-b:registry=https://npm.pkg.github.com/
//npm.pkg.github.com/:@org-a:_authToken=${ORG_A_TOKEN}
//npm.pkg.github.com/:@org-b:_authToken=${ORG_B_TOKEN}
//npm.pkg.github.com/:_authToken=${FALLBACK_TOKEN}
```
`pnpm login --registry=https://npm.pkg.github.com --scope=@org-a` writes the token to the same scope-specific auth key.
When installing or publishing `@org-a/*`, pnpm uses `ORG_A_TOKEN`. For `@org-b/*`, pnpm uses `ORG_B_TOKEN`. Packages without a matching scope continue to use the registry-wide fallback token.
* fix(pnpr): parse /v1/resolve NDJSON stream in the TypeScript client
The pnpr server streams the /v1/resolve response as application/x-ndjson
(one package frame per resolved tarball, then a terminal done/error/
violations frame), but the TypeScript client still parsed the whole body
as a single JSON object, failing with "Unexpected non-whitespace
character after JSON". Parse the NDJSON frames and act on the terminal
frame instead.
Closes#12234 follow-up.
* fix(pnpr): reject unknown /v1/resolve frame types in the client
Fail fast with a protocol error when the NDJSON stream carries a frame
whose type is neither package nor a known terminal frame, instead of
returning it and surfacing a confusing lockfile error downstream.
## Summary
Reworks pnpr from an install/file accelerator into a resolve-only accelerator:
- `POST /v1/resolve` resolves against the client-supplied registries and returns a gzipped JSON lockfile response
- pacquet/pnpm clients then fetch tarballs normally from registries with their own credentials and existing parallel fetch/integrity paths
- pnpr no longer serves package file bytes or store-index rows, so the server-side file diff, file-frame response, grant table, and public-package byte-gating code are removed
The follow-up resolution fast paths are included on the new measured path:
- repeated public no-lockfile resolves use a bounded in-memory TTL cache
- fresh frozen input lockfiles skip the server-side lockfile-only pacquet resolve after verification proves the lockfile is usable
- input lockfile verification and the verdict cache are preserved
## Benchmark
Integrated benchmark on Linux shows small improvements in all pnpr rows, with the clearest movement in hot restore. This should be treated as an incremental win rather than a large install-speed change.
| Scenario | `pnpr@HEAD` | `pnpr@main` | Change |
| --- | ---: | ---: | ---: |
| fresh restore, cold cache + cold store | `1.677 s ± 0.090` | `1.686 s ± 0.070` | ~0.6% faster |
| fresh restore, hot cache + hot store | `492.5 ms ± 18.1` | `521.9 ms ± 33.4` | ~5.6% faster |
| fresh install, cold cache + cold store | `1.997 s ± 0.025` | `2.003 s ± 0.038` | ~0.3% faster |
| fresh install, hot cache + hot store | `1.211 s ± 0.024` | `1.236 s ± 0.038` | ~2.0% faster |
## Trade-off
Going registry-direct means pnpr no longer gates tarball bytes itself. Private package access is enforced by the upstream registry when the client fetches tarballs. Resolution policy still runs server-side: lockfile verification, release-age policy, trust policy, and resolved package selection continue to happen before the client fetches bytes.
Closes#12184 (part 2).
#12181 shipped the per-caller access gate on `POST /v1/install`, which authorizes every served package against pnpr's own `packages:` policy — the complete answer **while pnpr fetches anonymously**. This PR adds the remaining piece: forwarding the caller's per-registry credentials so the accelerator can resolve/fetch **external private** content as the caller, and gating that content per user against the registry that actually owns it.
## Credential forwarding (issue steps 1–2)
- **Wire:** `POST /v1/install` gains an `authHeaders` body map (`{ "//host/path/": "Bearer …" }`, the shape `AuthHeaders::from_map` consumes / `getAuthHeadersFromCreds` produces) plus an HTTP `Authorization` header. The body map carries the *upstream* registry tokens; the header identifies the caller to pnpr's own gate and keys the grant table.
- **pacquet plumbing:** a request-scoped `Arc<AuthHeaders>` is threaded via a new `Install.auth_override` field and an `auth_override` param on `build_resolution_verifiers`, so resolution/verification run as the caller **without** baking per-user auth into the interned `&'static Config` (which would leak one config per user).
- **Server:** `handle_install` builds the per-request `AuthHeaders` and threads it through resolve, verify, and `fetch_uncached` (which now returns the freshly-fetched set).
- **Clients:** pacquet `pnpr-client` and `@pnpm/pnpr.client` send `registry` / `namedRegistries` / `authHeaders` + `Authorization`; the TS path sources them from the caller's registry credentials via `@pnpm/network.auth-header` (`getAuthHeadersFromCreds` is newly re-exported). `@pnpm/worker` is unchanged — downloads happen server-side.
- **Credential scope:** both clients forward the caller's *full* credential map, not a subset scoped to the declared registries. The registries a dependency graph touches aren't knowable up front — a transitive package can be scope-routed to another registry or pinned to a tarball URL on a host that's in `.npmrc` but isn't a declared registry — so pnpr attaches the right token per fetched URL exactly as a local install does. These are package-fetch credentials going to the very service the caller configured to fetch its packages.
## Per-user grant table (issue steps 3–4)
Externally-resolved private content carries no pnpr policy, so the store's possession of the bytes must not authorize a user the upstream never cleared. A served package is dispatched by **whether a forwarded credential was used to fetch it**:
- **No forwarded cred → pnpr-as-authority:** the existing local `packages:` policy check, unchanged.
- **Forwarded cred → upstream-as-authority:** gated against a persistent `(user, name@version)` grant table (SQLite, modeled on `VerdictCache`). Freshly fetched this request ⇒ record + allow (the upstream just accepted the token). Cache hit with a standing grant ⇒ allow, no upstream trip. Cache hit, no grant ⇒ re-verify against the owning registry with the caller's credential — record on success; **clear-on-discovery** (purge the user's grants for the package) + deny on `401`/`403`. TTL is the `installAccelerator.grantTtl` config knob (default: permanent).
## Public vs private (no per-user gating for public packages)
A forwarded credential matching a registry doesn't mean a package is *private* — in a mixed proxy (one registry serving a company's private packages **and** public ones), the token matches everything, and gating public content per user would cost a grant row and a re-verify round trip per user for bytes anyone may read. So before the per-user path, a not-yet-classified cache hit is probed **anonymously**: a `2xx` classifies the package public in a global set (no user pays for it again, no grant, no further round trip); a `401`/`403` means it's genuinely private and falls through to the grant / re-verify path above. Public packages thus cost **one anonymous probe across the whole fleet**, not one per user.
## Tests
- pnpr: grant-table + public-set mechanics, regime dispatch, the upstream-authorization paths (fresh-fetch, granted cache hit, private re-verify-and-record, denied-clears-grants, public-classified-once-then-free), and forwarded-cred-routes-around-local-policy.
- pacquet `pnpr-client`: a test asserting `authHeaders` + `Authorization` travel on the wire.
- Full suites green: `pnpr` (237), `pacquet-package-manager` (389), `pacquet-pnpr-client` (12), `pacquet-network`/`config` (325); clippy `-D warnings`, `cargo fmt`, rustdoc `-D warnings --document-private-items`, `typos`, and the TS compile all clean.
## Scoped follow-ups (not in this PR)
- Clear-on-discovery fires at the re-verify hook only. A `401`/`403` during the cold resolve aborts the request anyway (nothing is served); threading the offending package out of the deep resolve error to also clear stale grants for *future* requests needs structured auth errors.
- Per-scope external registries route via the default registry, since pacquet doesn't yet surface `@scope:registry` routing in `collect_packages`.
* fix(pnpr): authorize served packages against pnpr's policy in /v1/install
A content-addressed digest in the install-accelerator store is shared
across packages and says nothing about access, so the store's possession
of a package's bytes is not a capability to receive them. `/v1/install`
served files for any package found in the store, including ones reached
only on the cache-hit / frozen-lockfile path where no access check
happened — letting a caller who knows a private package's digest pull
bytes the registry routes would 401 on.
Check every served package against pnpr's own `packages:` policy before
serving — the same decision `serve_packument` / `serve_tarball` make, in
process, with no network round trip (so a warm shared server keeps its
resolution advantage). `serve_install` resolves the caller's identity
from `Authorization`; `deny_unauthorized_packages` denies the install
(401 anonymous / 403 authenticated-but-outside-the-allowed-set) when any
served package is not readable by the caller.
This authorizes against pnpr's own surface, the authority for everything
the store can hold today (pnpr fetches anonymously, so cached content is
pnpr-hosted or publicly fetchable). When credential forwarding lands,
packages the client resolved from external registries under its own token
carry no pnpr policy and will need per-caller re-verification against the
owning registry (TTL-cached) — noted at the check and tracked in #12184.
The raw `/v1/files` endpoint is still unauthenticated; removing it (it is
superseded by the inline single-response path) is a follow-up (#12184)
that also ports the TS `@pnpm/pnpr.client` + worker off the two-trip path.
---
Written by an agent (Claude Code, claude-opus-4-8).
* fix(pnpr): remove the unauthenticated /v1/files endpoint
`POST /v1/files` served any CAFS file by digest with no authentication
and no package identity, so the access gate on `/v1/install` (which is
per package) couldn't cover it — it had to be removed, not gated. It was
already superseded by the single-response inline path (#12178).
* Server: `/v1/install` always answers with the inline gzipped body
(lockfile + stats + store-index entries + the missing files' contents);
the NDJSON two-trip path, the `/v1/files` route, `handle_files`, and the
`FilesRequest`/`is_valid_sha512_hex` helpers are gone.
* TS client + worker: `@pnpm/pnpr.client` now does the one inline request
and hands the file frames to `@pnpm/worker`'s `writeCafsFiles`, which
writes them to the CAFS; the `fetchAndWriteCafsFiles` /v1/files fetcher
is replaced. Error bodies are decompressed before being surfaced, since
the server also gzips its JSON error responses (e.g. an access denial).
Verified end to end by `pnpm/test/install/pnpmRegistry.ts` (11 tests:
install / add / remove / workspace through a real pnpr server).
Closes the second half of the install-accelerator access work (#12184);
file-bearing responses are now both inline-only and access-gated.
* refactor: rename the agent client + setting to pnpr
The pnpm-side client and its config setting still carried the old
"agent" name after the server moved to pnpr. Align both with pnpr (and
with pacquet, which already uses `pnprServer`):
- Move `agent/client` → `pnpr/client` and rename the package
`@pnpm/agent.client` → `@pnpm/pnpr.client` (exported `AgentProject`
type → `PnprProject`).
- Rename the config setting `agent` → `pnprServer` (`--pnpr-server`
CLI flag), matching pacquet's setting name.
- Rename the internal install-path symbols and the user-facing log /
error strings that mentioned "pnpm agent" to "pnpr".
No behavioral change — only names. The e2e suite now drives
`--config.pnprServer`.
* fix: forward optionalDependencies to the pnpr server
`PnprProject` and the install-request body only carried `dependencies`
and `devDependencies`, so a project's `optionalDependencies` were
dropped on the way to the pnpr server — it resolved as if they didn't
exist, producing a different lockfile than the local resolver.
Thread `optionalDependencies` through the client request shape, the
deps-installer single-project and workspace request builders, and the
pnpr server (`InstallRequestProject` / `InstallRequest` + the throwaway
manifest it writes for resolution). Adds an e2e case asserting an
optional dependency is resolved through `pnprServer`.