pnpm/pnpm#14243 recorded every verified shared build artifact's envelope
digest in the consuming package snapshot, keyed by input key, artifact
owner, and consumer platform fingerprint. Reproducibility is now enforced
by the registry instead: a published artifact occupies a write-once slot
per input key and compatibility set, so the digest a lockfile would have
pinned is the only one that entry can ever serve.
That makes the pins pure lockfile churn. They were per-machine — a
platform fingerprint only the machines sharing it could write — so a
lockfile grew an entry per package, per owner, and per platform in the
team, and no two contributors produced the same diff.
Remove the `artifactPins` snapshot field, the `pnpm update
--build-artifacts` mode that rewrote it, and the pin plumbing through
resolution, materialization, and the pnpr client in both stacks. Stored
artifacts are still reverified against current trust, policy, platform,
and source before reuse; that check now reports a verdict rather than
returning the digest that fed a pin.
The feature was never released, so no changeset records its removal.
Related to pnpm/pnpm#13771.
`sideEffectsCache` now carries the whole story — whether a build is
restored, whether one is saved, and the remote tier that shares it
between machines. `remoteSideEffectsCache` broke the rule that a setting
extending another starts with its name, sorting away from the family it
belongs to; uniting the three rather than renaming one follows what the
registries settings did. The remote tier's `organization` becomes `org`,
which is what pnpr calls that namespace and what its endpoints are built
from.
Both spellings shipped in pacquet 12.0.0 but in no released pnpm 11, so
the TypeScript side is a free rename and only pacquet needs the aliases.
Every older spelling keeps working. Precedence is per field rather than
per section, and does not depend on key order: the two spellings
accumulate separately, `organization` is resolved to `org` per source,
and they are combined once after every source has been read. The Rust
side uses an explicit field rather than a serde alias, which would make a
file carrying both keys a duplicate-field parse error.
Two behaviours move toward pacquet, which already had them right:
`sideEffectsCacheReadonly` blocks writing, and setting it alongside
`sideEffectsCache: false` gives a read-only view rather than switching
the cache off. The declaration can also express writing without reading,
which the Rust `cache`/`readonly` pair had no spelling for, so `Config`
gained the two settings its helpers now prefer.
The shared-artifact protocol encoded package identity and source integrity at the top level of every candidate and signed payload. That shape could only represent dependency side effects, even though task caching is designed to reuse the same transport and trust model.
Move those fields into a discriminated subject shared by the Rust and TypeScript implementations. Dependency artifacts retain their package and source integrity, while workspace-task artifacts identify a workspace-relative project and task. Validate each subject against its artifact kind, input-key domain, and allowed owner type, and include the subject in pnpr's entry identity and candidate matching.
This intentionally changes the experimental wire format, so pnpr servers and clients must be upgraded together.
Related to pnpm/rfcs#22.
Teach the shared-artifact protocol to recognize additive v1 Darwin and Windows tags that encode architecture, Node major, and a minimum operating-system version.
Select exact supported tags before version floors, then choose the greatest compatible floor and keep universal artifacts as the final fallback. Detect macOS product versions with the absolute system `sw_vers` path and Windows NT kernel versions with the platform APIs. Enable restoration and publishing on Darwin and Windows x64 and arm64.
Implement the same protocol, fingerprints, selection, and host-detection behavior in the TypeScript CLI and pacquet. Keep all existing Linux tags and fingerprints stable.
Related to pnpm/pnpm#13771 and pnpm/rfcs#24.
Persist the signed origin metadata for hydrated remote build artifacts in each package's store-index row. This lets later installs reuse a verified artifact without contacting pnpr, while preserving enough evidence to reevaluate the artifact under the consumer's current trust configuration.
Before reuse, verify the envelope signature, signer, owner, package identity, source integrity, input key, compatibility constraints, lockfile pin, builder profile, manifest-to-diff mapping, and the digest and size of every stored CAS blob. Remote entries do not pass through the ordinary local side-effects-cache path when this verification is unavailable or fails.
Track trusted invalid manifests and corrupt remote blobs in a bounded per-channel quarantine stored alongside the package index. Keep transient network, server, and local filesystem failures out of quarantine so they can recover normally. Frozen stores may reuse valid persisted artifacts but do not hydrate, persist, or quarantine.
Apply the same behavior and serialized metadata shape to the TypeScript CLI and pacquet.
Related to pnpm/pnpm#13771.
Persist verified shared build artifact envelope digests in the package snapshot
that consumed them. Scope each pin by dependency input key, artifact owner, and
consumer platform so compatible variants remain independently reproducible.
Apply pins only after the existing trust and compatibility checks. A missing
pinned artifact is a remote-cache miss and falls back to a local build; frozen
installs enforce pins without mutating the wanted lockfile.
Add `pnpm update --build-artifacts` as an explicit update mode that clears
existing pins, forces materialization, and records only artifacts that complete
verification and hydration without changing package versions. Reject command
combinations that cannot safely write or verify the lockfile.
Implement the lockfile format, install plumbing, update mode, and tests in both
TypeScript pnpm and pacquet.
Related to pnpm/pnpm#13771.
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.