Commit Graph
461 Commits
Author SHA1 Message Date
Zoltan Kochan 79b4dd2d78 fix(pnpr): follow a Turso Cloud primary from the embedded replica (#17225)
Enable libsql's `sync` feature so the embedded replica
(`backend.libsql.replicaPath`) can follow a Turso Cloud primary.

The replica was built with libsql's `replication` feature only, whose
protocol Turso Cloud stopped serving: pnpr failed to start with
"Write delegation: Unimplemented ... deprecated version of sync". With
the `sync` feature, `Builder::new_remote_replica` defaults to
`SyncProtocol::Auto`: it probes the primary's `/info` route and uses the
newer sync protocol (bootstrap from `/export`, frames from `/sync`,
writes over Hrana) when the primary answers, and keeps the replication
protocol otherwise. A self-hosted `sqld` answers 404 on `/info`, so it
is unaffected.

The new test runs a mock primary that serves those routes in front of a
local libsql database and checks that the replica bootstraps from the
export, reads locally, and writes to the primary. Without the feature
it fails the way the issue describes.

`connect` also creates the replica's parent directory, as the local
stores do for their files, and with `syncInterval: 0` it runs the
initial sync under the startup deadline, since libsql would otherwise
bootstrap inside `Database::connect` with no bound.

Closes pnpm/tasks#183.
2026-10-22 19:19:52 +02:00
Khải 9c11ea9948 fix(fs/linux): read the umask without writing it (#17219)
`current_umask` set the mask to `0` to learn what it was and put it back,
as POSIX leaves no other way: `umask(2)` returns the value it replaced.
That window is process-wide, so a file another thread created inside it was
created unmasked, with wider permissions than the user asked for. An
install under `umask 077` could leave a world-readable file behind.

The window was known and argued harmless, and the argument holds for what
it reasoned about: an unmasked file carries no executable bit, matches no
desired mode, and so is copied by the import tiers rather than linked. It
says nothing about the file's permissions, which is the defect.

Linux has published the mask in the `Umask:` line of `/proc/self/status`
since 4.7, so it is read from there first and the window never opens. A
kernel without the line, a host without `procfs`, and every other Unix fall
back to the syscall pair, which now has nothing at all between its two
calls.

One read per process either way: the `LazyLock` that kept the import hot
path from paying per file is unchanged.
2026-10-22 16:05:05 +02:00
7e8e991e7e feat(pacquet): load dotenv files named by --env-file (#16570)
Related to pnpm/pnpm#7111.

pnpm has no .env support, so users wrap every invocation with dotenv-cli
or node --env-file. This adds the loading to the Rust CLI itself: a
repeatable global --env-file <path> (clap Vec<PathBuf>) whose files are
parsed with dotenvy and layered into the process environment in order so
later files override earlier files while never overriding variables
already in the initial process environment (matching node --env-file).
Relative paths resolve against the invocation working directory before
--dir or --workspace-root is applied.

The load runs on the main thread before the startup thread exists
(set_var must not run once another thread is alive), ahead of config
resolution, so PNPM_CONFIG_* overrides and user-level .npmrc ${VAR}
placeholders (the registry-token case from pnpm/pnpm#7111) resolve from
the files, and ahead of any pinned-pnpm dispatch so the child inherits
the variables. Shim invocations skip the load (their arguments belong to
the target). The flag is stripped from the child argv (an older pnpm
would reject it). dotenvy $VAR expansion is accepted as standard dotenv
behavior (single-quote to keep a literal $); project-level .npmrc
expansion stays refused as before. Parse errors name the file without
echoing its contents, NUL keys or values fail instead of panicking,
invalid UTF-8 fails as a parse error, and a UTF-8 BOM is tolerated.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-22 16:02:29 +02:00
Zoltan Kochanandgithub-actions[bot] 91774e72a0 chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16 (#17187)
* chore(release): 11.28.7, pacquet 12.15.0, pnpr 0.1.0-alpha.16

* docs(release): curate 11.28.7, pacquet 12.15.0, and pnpr 0.1.0-alpha.16 release notes

Add lead paragraphs and group the patch entries by what the reader was
doing, with security fixes first on each page.

- v11: lead with the registry metadata name check, pinned pnpm
  trustPolicy check, maxTarballUnpackedSize, and the removal of registry
  revisions; merge overlapping entries for minimumReleaseAge metadata
  revalidation, frozen-lockfile directory dependency checks,
  syncInjectedDepsAfterScripts, verifyDepsBeforeRun, pnpm sbom, and
  unreadable config settings.
- v12: order minor highlights from config includes and
  lockfile.integrityTarget down to interactive update styling; drop the
  pnpr-only distribution note from the CLI changelog; merge overlapping
  entries for workspace and pnpr install speedups, minimumReleaseAge and
  If-Modified-Since revalidation, frozen-lockfile and injected package
  checks, verifyDepsBeforeRun, pnpm prune, pnpm pack, pnpm licenses
  list, pnpm self-update, pinned pnpm flag forwarding, and WebContainer
  fixes.
- pnpr: lead the minor section with the breaking ecosystem URL prefix,
  hosted storage layout, artifacts.orgs, and strict config schema
  changes; fold dist.pnpr tarball metadata into one entry and drop the
  redundant /metrics note; lead the patch section with reserved-username
  and auth security fixes.
- Use one issue-link label style across the release pages.

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-21 12:15:12 +02:00
Zoltan Kochan e2a11f5611 perf(install): precalculate package bins and shebang runtimes (#17186)
Precalculate package binary entrypoints and shebang runtimes during tar
streaming in pnpr (dist.pnpr.bins) and during cold tarball extraction in
pnpm so .bin command shims can be linked without reading package.json,
walking directories.bin, or opening bin scripts on disk.

- Add PackageBin, ScriptRuntime, parse_shebang_from_bytes,
  resolve_package_bins, and verify_package_bins in pnpm-package-manifest
  with strict path normalization and shell-safe runtime validation.
- Extend inspect_tar in pnpm-tarball to compute bins and shebang runtimes
  in the same streaming pass as requiresBuild when requiresBuild is false.
- Emit dist.pnpr.bins in pnpr packument responses and cache it in
  pnpr-storage TarMetadataCache.
- Propagate cold-extracted manifests and precalculated bins through
  CreateVirtualStoreOutput into LinkVirtualStoreBins and direct-dep bin
  linking, and verify warm-cache dist.pnpr.bins against the store index
  manifest and CAS file list before skipping shebang reads.

Closes pnpm/tasks#178
2026-10-21 11:42:03 +02:00
Alessio AttilioandZoltan Kochan cd814501e6 perf: optimize pnpm install metadata through pnpr (#17169)
- **New Features**
  - Verified package metadata now includes accurate tarball file counts and unpacked sizes.
  - Registry resolution better supports abbreviated metadata for time-based resolution and trust checks.
  - Package metadata preserves publisher verification markers, bundled dependencies, and precise release timestamps.

- **Bug Fixes**
  - Trust-policy checks more reliably detect downgrades and fetch complete metadata when required.
  - Offline and cached metadata verification is more resilient across supported registry types.
  - Tarball inspection consistently records build requirements, file counts, unpacked sizes, and integrity data.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-21 07:13:27 +02:00
Zoltan Kochan 7164fc7f27 fix: finish repeat-install and verifyDepsBeforeRun unification across v11 and v12 (#17150)
Unify repeat-install freshness and `verifyDepsBeforeRun` validation across
pnpm v11 and v12 (Closes pnpm/tasks#161, pnpm/tasks#157, pnpm/tasks#148):

1. Centralize v11 workspace state updates in `@pnpm/workspace.state`
   (`preserveCarriedSettings`) and `@pnpm/installing.commands`
   (`workspaceStateUpdate.ts`), preserving carried `dev`/`optional`/`production`
   settings when `ignoredWorkspaceStateSettings` ignores them and preserving
   v12-recorded `autoDedupe`. A filtered install that prunes direct
   dependencies no longer records the workspace root as installed.
2. Hold `{.}...` workspace dependencies for non-recursive `pnpm run`,
   `pnpm exec`, and `pnpm restart` inside a workspace project in v11
   (`selectProjectsToVerify.ts`).
3. Align v11 recorded validation (`checkRecordedWorkspace`) and legacy
   unrecorded validation (`checkModifiedSinceLastValidation`) in
   `@pnpm/deps.status` with v12's `check_freshness`, including empty-project
   handling, `dedupeDirectDeps`, `dedupePeerDependents`, `autoInstallPeers`,
   `excludeLinksFromLockfile`, catalog/override config comparison, and
   `opts.lockfileDir` resolution.
4. Forward effective CLI flags (`--lockfile-dir`, `--modules-dir`,
   `--virtual-store-dir`, `--store-dir`, `--dir`, `--workspace-root`, `--prod`,
   `--dev`, `--no-optional`, `--filter`, `--filter-prod`, `--test-pattern`,
   `--changed-files-ignore-pattern`, and `--config.*`) when
   `verifyDepsBeforeRun: install` spawns `pnpm install` in both v11 and v12.
5. Align `GatePolicy::dedupe_peers` and empty-project predicates in v12
   `pnpm-package-manager`, and run `repeat_install_oracle.rs` across v12, v11,
   and mixed v11/v12 writers in CI, with a longer nextest period for the
   peer convergence harness that shares the job.
6. Parse `--config.<key>=<value>` with the type the command declares for
   `<key>` in v11, so the list and number options forwarded to the spawned
   install arrive as lists and numbers; forward `--npmrc-auth-file` in v11
   and `--test-pattern` / `--changed-files-ignore-pattern` in v12, so both
   forward the same set; and skip the current-lockfile read for a project
   whose manifest changed but whose lockfile did not, as v12 does.
7. Resolve relative v11 store overrides against the workspace or project
   root before forwarding them to the spawned install. Remove the isolated
   linker's duplicate package merge so filtered installs retain unselected
   dependencies and drop obsolete selected dependencies from the current
   lockfile. Cover both cases with CLI regressions. The restore API also
   handles a selected project absent from the previous current lockfile.
8. Record workspace validation from the projects the installers actually
   processed. Ignored projects receive no new proof, and empty or entirely
   ignored selections leave state untouched. Cover both lockfile modes and
   an ignored workspace root.
2026-10-20 20:24:27 +02:00
Zoltan Kochan 78838d566d perf(install): canonicalize lockfile-check link targets once per directory (#17156)
The lockfile freshness check verifies every `workspace:` link of every
importer. For a target that is a loaded project it canonicalized the
workspace root, the target directory, and the target's package.json, so
three `realpath` walks per link. On
jamenh/pnpm-workspace-performance-reproduction (6,833 projects, 87,630
workspace edges) that is 262,958 walks and 3.3M readlink calls per
install.

`pnpm_fs::CanonicalPaths` answers `canonicalize` from what it has
already resolved. A path is resolved from its parent; a component under
a canonical directory is canonicalized on its own the first time any
importer reaches that entry, which follows a symlink and spells the
component as `std::fs::canonicalize` does, and `..` steps to the
canonical parent as the kernel does. An error, a non-directory followed
by more components or by a trailing `/` or `/.`, or a relative path
falls back to `std::fs::canonicalize` on the whole path, so results and
errors match it. Elsewhere than Unix every call falls back.

One cache lives for one freshness check and one optimistic repeat
install check, shared by their parallel importer checks.

On the reproduction, measured with an earlier version that checked
each entry with one lstat: readlink calls 3,284,761 -> 32,587, sys time
1.8 s -> 0.4 s, user time +0.3 s on 32 cores (+0.15 s on 4). Wall time
is unchanged on 32 cores and drops 1.35 s -> 1.19 s on 4. The committed
version does one realpath walk per distinct directory entry (about
13,700) in place of the lstat. The lockfile is byte-identical.

Closes pnpm/tasks#173.
2026-10-20 19:30:44 +02:00
Zoltan Kochan ccd3e217e4 chore(bench): add Cargo repeat-install scenario to integrated-benchmark (#17132)
* chore(bench): add Cargo repeat-install scenario to integrated-benchmark

Add the cargo.repeat-install.hot-cache.hot-store scenario to
pnpm-integrated-benchmark and the CI integrated-benchmark workflow
to guard Cargo crate slot reuse (pnpm/pnpm#17105).

* fix(bench): isolate cargo repeat-install benchmark from parent workspace

* fix(bench): connect generated crates in Cargo dependency graph and verify prewarm slots
2026-10-20 11:53:52 +02:00
Zoltan Kochan 01860762ac fix(fs-packlist): unify package selection rules (#17135)
Replace the Gitignore-based files-field filter and post-walk force-includes
with root rule synthesis and the existing required-file traversal. Remove
filesystem rescans and the unused ignore dependency. Reuse the same compiler
for explicit dotenv listings while preserving their rooted matching contract. Use the shared lexical path
validator and reject intermediate directory symlinks before manifest metadata
lookups.
Cache directory checks within each compilation. Preserve minimatch's Windows
UNC-root handling in slash normalization.

Add a differential test against the repository's TypeScript packlist over
177 fixture combinations, plus focused regression tests. The corpus covers
files allowlists, extglobs, exclusions, root and nested ignore files,
entry points, soft defaults, internal directory symlinks, and bundled files.

Root lockfiles remain strict exclusions. Bare positives retain npm-packlist's
basename matching; a ./ prefix roots them. Leading !(*...) differs from
lib/!(*...). Preserve the existing literal, package-contained entry-point policy
and explicit-file behavior under ignored directories, including the cases where
npm-packlist drops the named file.

Closes pnpm/tasks#163.
2026-10-19 20:01:26 +02:00
Zoltan Kochan e01508f253 feat(pnpr): harden secrets, derive subkeys, and validate ecosystems (#17126)
Complete Section B deliverables (B4, B5, B6, B7) for pnpr production readiness.

- B5: Fail fast at startup when secret-bearing configuration fields (secret,
  secrets.cache, secrets.oci, s3.secretAccessKey, auth.scim.token,
  clientSecret, authToken, or upstream tokens) contain unresolved \${VAR}
  references rather than falling back to empty strings or per-process keys.
  Support independent configuration of secrets.cache and secrets.oci, and
  derive domain-separated HMAC-SHA256 subkeys from root secret.
- B4: Enforce per-ecosystem validation rules rejecting \`teamsManagedBy: api\`
  on non-npm registries and \`unpublish\` rules on Cargo and PyPI registries.
- B6: Set \`publish = false\` on \`pnpr/crates/pnpr/Cargo.toml\` to guard against
  accidental crates.io publication.
- B7: Document the v1 extensibility model: compile-time trait implementations
  (\`UserBackend\`, \`TokenBackend\`), no dynamic plugin runtime.

Relates to pnpm/tasks#131.
2026-10-19 15:19:33 +02:00
Zoltan Kochan ca18902a25 refactor(deps-resolver): one owner for peer provider decisions (#17114)
Part of pnpm/tasks#143.

- Move the peer-shadowing rule and the provider lookup with lockfile-pin
  precedence into one owner: provider_for/ProviderPick in peer_provider.rs
  and providerFor in v11 hoistPeers.ts.
- Read and write peer suffixes only through PeerBindings (pnpm-lockfile) and
  parsePeerBindings (@pnpm/deps.path), with a round-trip test over the
  repo's lockfiles.
- v11 picks hoisted optional peers through v12's lockfile tiers.
- Report a cached node's own missing peers in every importer, so a project
  no longer borrows an out-of-range peer from another project's subtree
  (v12, resolvePeersFromWorkspaceRoot: false).
- Classify optional-peer edges by the bindings the key records, so a prod
  install and deploy keep a package's own dependency that is also its
  optional peer (v11 and v12).
- Add proptest graph tests on StubResolver.

Reusing a pin only when the owner returns it (step 2) is left out: deciding
reuse after the peer walk cost 40-60% on lockfile-only installs.
2026-10-19 02:17:20 +02:00
Zoltan Kochan 85f7fa1e98 feat(pnpr)!: give the hosted store a layout with one place per thing (#17100)
The hosted store shared one key space between packages, orgs and blobs:
a registry without an org stored packages at the store root, an org's
registry stored them in `<org>/`, a package's blobs sat beside its
document, and a nested image repository sat inside its parent's
directory. So an org collided with an unscoped package of the same name
(an unpublish of that package removed the org's whole namespace), the
implicit `<ecosystem>~<name>` org of a grouped registry was a valid npm
name, and OCI GC and verify needed ad-hoc exclusion lists. On S3,
object_store percent-encodes `~` in keys while the upload sweep compared
raw prefixes, so grouped OCI registries were never swept. This is part
of section A4 of pnpm/tasks#131.

The new layout (pnpr_storage::layout), the same on disk and in a bucket:

  layout.json                          {"layout": 1}
  default/packages/<name>/document.json
  default/packages/<name>/blobs/<file>
  orgs/<org>/packages/...

Every name, org and file name is one segment: `[a-z0-9._-]` as is,
every other byte as `!` and two hex digits. That keeps nested names
siblings, separates names that differ only in case on case-insensitive
filesystems, and leaves nothing for object_store to escape, so a key is
byte for byte the local path. Per-namespace records stay inside their
namespace; records every namespace shares stay at the root. Listing is
one directory read, so the `.package-index` workaround is gone, and the
blob inventory reports package and blob names instead of paths.

Startup refuses a store marked with another layout or an unmarked store
holding data, and marks an empty one. The proxy cache uses the same
package layout and is emptied once when its marker is missing.
`pnpr upgrade-storage` converts an earlier store on disk or in a bucket,
telling org directories from packages by the configured orgs; it moves
blobs before documents so an interrupted run can resume, and refuses a
directory that is both an org and a package. export, verify and
migrate-storage check the layout, and migrate-storage marks its target.

The registry fixtures the pnpm test suites serve are built in the new
layout, and the cached build is keyed by the layout version.
2026-10-18 21:48:58 +02:00
dependabot[bot] cd23da476d chore(cargo): bump clap from 4.6.6 to 4.6.7 (#16976)
Bumps [clap](https://github.com/clap-rs/clap) from 4.6.6 to 4.6.7.
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/main/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.6...clap_complete-v4.6.7)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-18 19:32:01 +02:00
dependabot[bot] 8c01368c75 chore(cargo): bump object_store from 0.14.1 to 0.14.2 (#16974)
Bumps [object_store](https://github.com/apache/arrow-rs-object-store) from 0.14.1 to 0.14.2.
- [Release notes](https://github.com/apache/arrow-rs-object-store/releases)
- [Changelog](https://github.com/apache/arrow-rs-object-store/blob/main/CHANGELOG-old.md)
- [Commits](https://github.com/apache/arrow-rs-object-store/compare/v0.14.1...v0.14.2)

---
updated-dependencies:
- dependency-name: object_store
  dependency-version: 0.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-18 19:31:36 +02:00
dependabot[bot] a43f4b61d7 chore(cargo): bump icu_locale_core from 2.2.0 to 2.3.0 (#16975)
Bumps [icu_locale_core](https://github.com/unicode-org/icu4x) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/unicode-org/icu4x/releases)
- [Changelog](https://github.com/unicode-org/icu4x/blob/main/CHANGELOG.md)
- [Commits](https://github.com/unicode-org/icu4x/compare/icu@2.2.0...icu@2.3.0)

---
updated-dependencies:
- dependency-name: icu_locale_core
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-18 19:30:40 +02:00
Zoltan Kochan 7b0d6873a6 feat: pin tar hashes with lockfile.integrityTarget and address revisions by tar digest (#17092)
lockfile.integrityTarget: tar makes the integrity of every package tarball
entry the hash of its uncompressed tar, recorded as settings.integrityTarget.
The lockfile then survives a registry recompressing its tarballs, switching
to tar resources, or being replaced by a mirror that does either. A lockfile
recorded under another target is stale: a frozen install fails with
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH, and any other install re-resolves with
update-checksums semantics so no hash of the old kind carries forward. The
setting is opt-in and the lockfile version is unchanged.

The tarball crate now reports both hashes of every extraction: the resource
after content decoding, and the tar a .tgz decodes to. A pin names one of
them (IntegrityKind::Resource / Tar), or either at resolution, where a locked
or published integrity may be of either kind (ResourceOrTar, sha512 only).
Under a tar pin every gzip member is decoded and nothing may follow the tar.
The resource pin keeps today's behaviour, extracting the first member and
tolerating trailing data, but then records no tar.

Store rows extracted from a .tgz record tarIntegrity and get an alias under
the tar key, in a table pnpm v11 never reads. Store lookups follow aliases,
so a tar-pinned frozen install is served offline by a store that .tgz
installs populated, and resolution takes a version's tar hash from its row
before downloading anything.

Registry revisions now always pin their tar, revision 0 included, and are
fetched with a tar pin, so the digest route can use any content coding.
Revision metadata must list revision 0 and the selected revision, and a
digest-route URL without a revision is rejected. pnpr keys its digest
references by tar hash on every registry format, serves the route as a
negotiated tar resource, and projects revision 0 explicitly. pnpm v11 cannot
extract tar resources, so it now rejects revisions with
ERR_PNPM_REGISTRY_REVISIONS_UNSUPPORTED.

Implements pnpm/rfcs text/0000-tar-integrity-for-every-source.md and
pnpm/rfcs#42.
2026-10-18 17:36:30 +02:00
Zoltan Kochan 430d892741 feat(pnpr)!: serve every ecosystem under its own prefix (#17087)
pnpr served npm at the server root when npm was the only configured
ecosystem and moved it under /npm/ as soon as a second ecosystem was
added. Adding an ecosystem therefore broke every .npmrc registry URL,
every tarball URL already written into a lockfile, named-registry URLs,
and OIDC workload publish paths, and /npm meant the package `npm` on one
server and the ecosystem prefix on another. This settles section A1 of
pnpm/tasks#131: every ecosystem is always prefixed.

- Ecosystem::base_path is now independent of the configuration. The
  per-configuration Registries::base_path and is_only_ecosystem are gone,
  and the routes, the resolver's tarball URLs, OIDC workload paths and
  pnpr's own-origin route classification all use it.
- The default image registry still answers at /v2/ on the host root; an
  image client cannot be pointed at a prefix. The registry directory keeps
  its `prefixed` and `namedPrefixed` flags for the web UI.
- The read-only token allowlist matched the npm audit endpoints at the
  root only, so read-only tokens were already refused for audits on
  multi-ecosystem servers. It now matches them under the npm prefix.
- /metrics no longer switches between Prometheus metrics and the npm
  package `metrics` on the Accept header; the package is /npm/metrics.
- The pnpm v11 and v12 test harnesses, the registry-mock crate and the
  integrated benchmark address the fixture registry at /npm/ and keep the
  server origin for the pnpr protocol endpoints. The benchmark asks each
  revision mock's registry directory whether npm is prefixed, so it can
  still compare against a pnpr revision from before this change.
2026-10-18 16:55:55 +02:00
573c4d1e8e fix(pnpr): gate server-only error variants behind server feature (#16972)
pnpr-error depended on axum, bcrypt, object_store, reqwest, rusqlite,
serde_json, tokio and tracing on every non-wasm target. pnpm-config depends
on pnpr-registry for Ecosystem and PackagePattern, and pnpr-registry and
pnpr-package-name both depend on pnpr-error, so every consumer of pnpm-config
compiled and linked the registry server's HTTP, storage and auth crates.

- pnpr-error: those dependencies are optional and enabled by a new `server`
  feature. The response module (IntoResponse, public_message, log_kind) and
  the Upstream, Bcrypt, Sqlite, JoinError, ObjectStore and Json variants are
  gated on it, replacing the cfg(not(target_family = "wasm")) gate.
- The pnpr crates that use those variants or the response rendering (auth,
  osv, pipeline-runs, pnpr, search, shared-artifacts, storage, upstream)
  enable `server`. pnpr-config only builds InvalidConfig and does not.
- pnpr-registry drops its unused pnpr-error dependency.
- pnpr-package-name tests compare Display output instead of
  public_message(), so they pass without the server feature.
- Three workflow_run workflows carry their zizmor ignore directive on the
  trigger line. zizmor did not apply the standalone comment, which failed the
  zizmor job on fork PRs.

Closes pnpm/tasks#134.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:45:10 +02:00
Zoltan Kochan 3f53b06d63 fix(resolving-npm-resolver): decode registry metadata on a dedicated pool (#17081)
glibc gives each thread its own malloc arena, up to eight per CPU, and
keeps the memory freed in it. Packument decodes ran on tokio's blocking
pool, so a lockfile update under `trustPolicy`, which decodes a full
packument per locked package, spread large transient allocations over
about 70 threads, each of which then held an arena of freed packument
memory.

Every network packument decode, and the mirror write after it, now runs
on a pool of at most four threads. The body arrives as chunks and is
joined on the decode thread, and the body string is dropped before the
mirror write. The indexed mirror write streams fragments through a
BufWriter instead of building two whole-file copies.

A process-wide `M_ARENA_MAX=4` reached a similar peak but slowed
hot-store installs on the 8-vCPU benchmark runner by up to 22%.

On the reporter's workspace, the lockfile update peaks at 1.8 to 2.0 GB,
against 2.6 to 3.7 GB on main, and completes under a 2.25 GB memory cap
where 12.14.0 is killed.

Closes pnpm/pnpm#17020
2026-10-18 12:56:01 +02:00
Zoltan Kochan 6737c4a525 feat(pnpr): prevent self-registration of reserved usernames (#17046)
Prevent self-registration from squatting configured admin usernames,
OIDC login, and workload bindings (Section B1 of
https://github.com/pnpm/tasks/issues/131):

- Reject self-registration attempts for reserved usernames with 403
  Forbidden and a descriptive error message.
- Add `is_reserved_name` check on `IdentityConfig` covering `auth.admins`,
  OIDC user logins, and OIDC workload bindings.
- Add `pnpr add-user <username>` CLI command for out-of-band admin
  bootstrapping and password updates.
- Preserve normal CouchDB login for existing accounts matching reserved
  names when provided with the correct password.

Part of https://github.com/pnpm/tasks/issues/131
2026-10-18 01:29:59 +02:00
Zoltan Kochan 8b69106ef5 feat: install from tar registries with zstd content coding (#17054)
Implements pnpm/rfcs#39 in pnpm v12 and pnpr.

pnpm:
- `serverType: pnpr` rebuilds tarball URLs as `<registry>/<name>@<version>`.
- Tarball requests go through HTTP clients that leave Content-Encoding in
  place. pnpm decodes zstd (8 MiB window, RFC 9659) and gzip itself, because
  reqwest's built-in decoder allows a 128 MiB window and cannot be configured.
- The decoded body is sniffed: the gzip magic means a .tgz, anything else a
  tar. The hash covers the decoded resource on both paths.
- A decoded-output budget bounds what a response makes pnpm decode before its
  hash is checked: file payload up to `maxTarballUnpackedSize` (4 GiB),
  tightened by `dist.unpackedSize` for tars only; 256 MiB of non-payload;
  at most 10 KiB of zeros after the end-of-archive marker; empty bodies fail.
- A missing `dist.tarball` is rebuilt from the declared layout, and fails
  with ERR_PNPM_MISSING_TARBALL_URL without one. A missing `dist.integrity`
  is learned by the existing resolve-time archive read.
- `--update-checksums` re-resolves every locked package.

pnpr:
- `tarballs: tar` makes a registry's metadata describe tars and its
  `<name>@<version>` route serve them. Every registry serves that route.
- Tar hashes are derived at publish or on the first decode and stored in a
  reserved `.tar-integrity` namespace of the shared hosted backend.
- A cached upstream .tgz is re-checked as its tar streams; a mismatch fails
  the response and evicts the copy.
- gzip clients get the stored .tgz. zstd clients get a level-19 variant from
  the local cache once a bounded worker made one, and a level-3 stream until
  then. Decoding stops at 4.25 GiB, and copies kept for the worker are
  coalesced per version and drawn from a 1 GiB quota.
2026-10-18 01:25:41 +02:00
Zoltan Kochan 1124edb863 fix(package-manager): record which projects each install proved installed (#17030)
The repeat-install fast path and the verifyDepsBeforeRun gate compared
file mtimes with one workspace-wide lastValidatedTimestamp and a
filteredInstall flag. That could not say which projects an install had
validated: a filtered install refreshed the timestamp for the projects it
left out too, so an edit to one of them went unnoticed, and an edit that
kept its mtime was never seen at all.

The workspace state now records, under a validation key pnpm v11 ignores,
the content digest of each project's manifest and of the lockfile it was
proven against, keyed by importer id, plus a stat signature (mtime, inode
change time, size) and digest for the lockfile, patches, and pnpmfiles. A
project is proven installed while both digests match. Manifests are
hashed from the parsed value with sorted keys. A shared input is read
again only when its stat moved or falls inside the window a racy write
could hide in.

Each gate holds only the projects its install can fix: every project for
an unfiltered install, the selection and the workspace projects it links
for a filtered install or command, and for an unfiltered command every
stale project plus the never-installed ones the selection needs. A held
project is checked against the lockfile, and against the current lockfile
when its lockfile changed or it was never proven, comparing every group
the current lockfile records. A filtered install vouches only for what it
materialized and carries the other records unless a workspace-wide input
changed. Every state write goes through recorded_workspace_state, so a
writer cannot drop a carried field.

The gate now warns when the install it spawned leaves the dependencies
out of date. That exposed two gates that reinstalled before every
command: a shared-lockfile workspace without a root package.json, and
filtered commands under dedicated lockfiles, which now also verify the
workspace projects the selection links.

ManifestFreshness is removed: in-memory manifests are judged by content
like every other one.

A new end-to-end harness checks every gate against a full install in
both directions across lockfile layouts, linkers, and injected packages.
It failed 38 of its 112 tests on the commit this change started from.

Part of pnpm/tasks#141 and pnpm/tasks#142.
2026-10-17 15:26:35 +02:00
Zoltan Kochan 198cc7f398 feat(pnpr): provide minimal production default configuration and separate test fixtures (#17027)
* feat(pnpr): provide minimal production default configuration and separate test fixtures

Replace the bundled test configuration in `pnpr/crates/config/config.yaml`
with a minimal production-safe default configuration (Section A3 of
https://github.com/pnpm/tasks/issues/131):

- Configure public npmjs upstream proxy and hosted local registry at
  `/~local/` with default registry set to npmjs.
- Generate a secure 32-byte CSPRNG random secret by default when secret is
  omitted, and disable registration (`max_users: -1`).
- Clean up unused Verdaccio legacy keys (`plugins`, `middlewares`, `web`)
  and remove test fixtures from production default configuration.
- Move test mock configuration and fixture scopes to `pnpr/.fixtures/config.yaml`.
- Move test presets (`REGISTRY_MOCK_LOCAL_PATTERNS`, `registry_mock_rules`,
  `registry_mock_graph`, and `MockConfigPresets` trait with `Config::proxy`
  and `Config::static_serve`) from `pnpr-config` into `pnpr-fixtures`.
- Update Jest global setup and `pnpm-registry-mock` to load test configuration
  from `pnpr/.fixtures/config.yaml`.
- Update documentation in `pnpr/docs/configuration.md` and `pnpr/docs/quick-start.md`.

Related to https://github.com/pnpm/tasks/issues/131

* fix: drop obsolete quick-xml advisories from deny.toml

quick-xml was upgraded to 0.41.0, resolving RUSTSEC-2026-0194 and
RUSTSEC-2026-0195, so cargo-deny no longer encounters them.

* fix: ignore unreachable zip advisories in deny.toml

* fix(pnpr-config): require authentication to access private packages on local registry
2026-10-17 13:23:18 +02:00
Zoltan Kochan bc35481b80 feat(pnpr): add maintenance tooling and verdaccio htpasswd support (#17002)
Add maintenance subcommands and migration tooling for pnpr (Section C12 of
https://github.com/pnpm/tasks/issues/131):

- Implement `pnpr config check` (`pnpr config-check`) to validate configuration
  and report diagnostic details on routes, upstreams, auth, storage, and surfaces.
- Implement `pnpr verify` to audit hosted package documents against storage
  blobs, checking for missing files, corrupted checksums, and unreferenced orphans.
- Implement `pnpr export --out <dir>` to dump packages, documents, and blobs,
  with optional `--include-auth` support.
- Implement `pnpr migrate-storage` to migrate hosted packages and blobs from local
  filesystem storage to an S3-compatible object store.
- Support Apache/Verdaccio `{SHA}` format in `pnpr-auth`, automatically upgrading
  authenticated users to bcrypt upon first login, and provide `pnpr import-htpasswd`.
- Add backup and disaster recovery documentation in `pnpr/docs/backup-and-restore.md`
  and update CLI documentation.

Related to https://github.com/pnpm/tasks/issues/131
2026-10-17 01:24:35 +02:00
Zoltan Kochan a1ce6a1bc0 feat: split workspace settings across included files (#16967)
Implement the config fragments RFC in pnpm v12.

The root pnpm-workspace.yaml can list local YAML files and config
dependencies under `include`, each mapped to the settings it owns (or
'*' for local files). Every setting and catalog slot (catalog,
catalogs.<name>, catalogs) has exactly one owner; a listed setting is
reserved even when the file omits it, and legacy aliases share their
canonical setting's owner.

pnpm-workspace loads the include index and merges the fragments into one
document that every reader consumes: config loading, workspace package
discovery, catalogs, changed-catalog detection in the projects filter,
and the napi config cache. Without `include` the root text is read as
before. Fragments are normalized (merge keys applied, tags dropped,
scalar keys stringified) before ownership is checked, so a fragment
cannot define a catalog it was not delegated through a tag or a `<<`
merge.

A relative path in a local fragment is relative to the directory of
the fragment's include path, the way a path in the root manifest is
relative to the workspace root. The loader re-expresses each fragment's
relative paths from the workspace root before merging: path settings,
`packages` globs, `pnpmfile`, `patchedDependencies`, a path-like
`scriptShell`, and local specifiers in catalogs and `overrides`
(re-anchored with the same LocalSpec logic catalogs use). Absolute,
`~` and `$VAR` paths are kept. This departs from the RFC, which
resolved fragment paths from the root, so a fragment can sit next to
the files it names. The RFC change is pnpm/rfcs#40.

workspace-manifest-writer routes every write to the owning file. Writes
to settings owned by a config dependency fail, `include` itself is never
written, included files are never deleted, and symlinked includes are
written through. Paths it writes to a fragment, such as patch-commit
entries, link: overrides and local catalog entries, are written
relative to that fragment. Every file's edit is computed before any
file is written, so a rejected edit leaves the workspace unchanged, and
a failed write restores the files written before it.

A config dependency names its settings file in package.json
`pnpm.config`. The entry must stay inside the installed package and may
only define delegated settings. Its values are applied at the project
layer after config dependencies are installed and before hooks run, so
they take effect on the first install too. Installs fail closed when an
included package's entry cannot be loaded.

`bootConfig` holds the policy (minimumReleaseAge*, trustPolicy*) used to
resolve config dependencies and the packageManager pnpm version.
Package-owned keys fall back to the values from below the project layer,
env and CLI overrides still win, and self-update ignores it.
2026-10-16 17:02:06 +02:00
Ben MaurerandZoltan Kochan b260a317ec feat: accept zstd-compressed responses and serve them from pnpr (#16585)
Enable reqwest's `zstd` feature and set `.zstd(true)` next to `.gzip(true)`
in the shared install client. pnpm now sends `Accept-Encoding: zstd,gzip,deflate`
(previously `gzip,deflate`) and decodes zstd bodies transparently.

Full packuments gain the most. A long field repeated across versions, such as
vite's `licenseText` (up to 259 KB per version, 34 MB of the document), spans
far more than gzip's 32 KB window, so gzip cannot reuse the earlier copies.
vite's full packument on registry.npmjs.org is 38.9 MB: gzip -6 gives 4.65 MB,
zstd -3 603 KB, zstd -19 455 KB. pnpm fetches full packuments for
`trustPolicy: no-downgrade`, for `resolutionMode: time-based` on registries
without a `time` field, and for `minimumReleaseAge` when the abbreviated
document has no `time`. Abbreviated packuments gain less (vite: 403 KB gzip,
382 KB zstd -3).

Brotli is left out on purpose. On these payloads it saves about as much as zstd
and would add a second decoder to the binary.

Tarballs are unaffected: registries serve them without `Content-Encoding`, so
integrity verification still hashes the raw `.tgz`. Because `reqwest` is a
workspace dependency, pnpr's upstream client gains zstd too. The WebContainer
transport gets a no-op `zstd()` builder method to match its `gzip()`.
pnpr also serves zstd: tower-http's `compression-zstd` lets its
CompressionLayer pick zstd when the client prefers it. Resolving 1,235
packuments against pnpr with full metadata moved 103.6 MB instead of
115.4 MB (-10%), and took 34.5 s instead of 38.4 s on a 25 Mbit/s link.
`zstd-sys` compiles the bundled libzstd with `cc`, which the build already
needs on every release target. The release binary grows by 152 KiB (+0.3%).

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-15 21:35:31 +02:00
Khải c6f3cd16a6 fix(wasm): give the WebContainer build a dashmap whose locks can park (#16951)
`dashmap` spins and then calls `parking_lot_core::park` once a shard lock is
contended. On a wasm target that crate compiles its atomic parker only under
`all(feature = "nightly", target_family = "wasm", target_feature = "atomics")`,
and otherwise one whose every method panics with "Parking not supported on this
platform". So the WebAssembly pnpm aborted the first time two threads reached
the same shard.

Two of the three conditions already held: the family is wasm, and
`target_feature = "atomics"` is set for `wasm32-wasip1-threads` under the
nightly gating this build uses. The `nightly` Cargo feature was not enabled,
and only a crate that declares `parking_lot_core` can enable it, so it has to
come from `dashmap`.

Override `dashmap` to pnpm/dashmap, which carries a `nightly` feature that
forwards to `parking_lot_core/nightly`, and enable that feature in
`pnpm-package-manager`'s wasm target table, on a dashmap it already uses.

A `[patch]` cannot be scoped to one target: it is a top-level table, and a
virtual manifest rejects a `target` section outright. So the override applies
everywhere while the feature stays wasm-only. That is safe because the fork is
upstream plus that one opt-in feature, so every target leaving the feature off
compiles what it did before.

An override applies only if it satisfies the requirement already in the graph,
and the fork's line is 7.0.0-rc2, so the workspace requirement moves from
6.2.1. Left at the old requirement, cargo reports `patch ... was not used in
the crate graph` as a warning, resolves 6.2.1 from crates.io, and the build
looks fine while still shipping the panicking parker. dashmap 7 needs no source
changes in pnpm and adds no package to the lockfile: it reuses the `equivalent`
and `hashbrown` 0.15 already in the tree and drops `once_cell`.

`deny.toml` lists the fork beside the node-semver one, because `unknown-git` is
a reported source.

Drop the patch once an upstream dashmap release carries the feature.
2026-10-15 16:10:35 +02:00
Florent Destremau 0d1597b3a5 fix(cli): stop aborting when stdout is a closed pipe (#16932)
`println!` panics when its write fails, and the release profile builds
with `panic = "abort"`. When the reader of pnpm's stdout had exited, as
in `pnpm --version | true` or `pnpm config list | head -0`, pnpm raised
SIGABRT: exit code 134 and a core dump.

`pnpm-diagnostics` now exports `println!` and `print!` macros next to the
stderr ones from pnpm/pnpm#16916. A write whose reader closed the pipe is
dropped and the command finishes with its own exit status. Any other
failed write is kept, and `main` flushes stdout and fails the command
with it through `report_fatal_error`, so the NDJSON reporter still gets
a JSON record. Every `print!`/`println!` of the binary and the direct
stdout writes of `outdated`, `cat-file`, `cat-index`, `sbom`, `audit`
and `completion` go through them. clippy's `print_stdout` restriction
lint now rejects the std macros, with the same crate-level opt-outs as
`print_stderr`. The watch agent of `pnpm pipeline --watch` stops with
exit 0 once a write finds its reader gone, so `pnpm pipeline --watch |
head -1` does not hang. `run`, `exec`, `audit` and `sbom` print their
no-projects-matched notice through one helper, which keeps
`run_recursive` within perfectionist's length limit.

The tracking issue asked for pnpm to exit 0 at the first failed write,
as pnpm 11 does. Exiting is the wrong default:

- `outdated` and `audit` carry their verdict in the exit status. An
  `exit(0)` turned `pnpm outdated | head` from 1 into 0, and would let
  `pnpm audit | head` pass a CI job under `pipefail`.
- `clean` prints "Removing <path>" before each removal, and the prune
  report of `audit` comes before its write to pnpm-workspace.yaml. An
  `exit(0)` there ends the command with work undone and a success code.
- No command computes much after its first stdout write: `list`, `why`
  and the registry queries render their whole output, then print it once.

pnpm 11.28.5 shows both failures: `outdated | true` exits 0 with an
outdated dependency, and `clean --lockfile | true` removes nothing and
exits 0.

Restoring the default SIGPIPE disposition is still not an option: pnpm
writes to its children's stdin and relies on the `BrokenPipe` error to
notice a child that stopped reading.

Closes pnpm/tasks#132
2026-10-15 13:19:17 +02:00
Zoltan Kochan 9e13c41ec4 fix: keep importers of projects missing from disk in filtered installs (#16936)
A filtered install whose selection matched every project on disk was
treated as a full install, so it pruned lockfile importers of workspace
projects that are listed but not on disk, as in a sparse checkout.

pnpm v12:
- A --filter selection now always requests its importers, so the
  stale-importer prune is skipped and the filtered lockfile merge runs.
  The workspace state still records the install as unfiltered when every
  project was selected.
- The filtered merge keeps importers the previous lockfile records for
  projects outside the workspace walk, plus their packages and catalog
  entries. Before, it dropped them even for a narrower filter.

pnpm v11:
- pruneLockfileImporters defaults to false when a filter is given.
- The lockfile catalogs keep the entries used by importers that the
  resolution left untouched. Before, a narrower filter kept the
  importer but dropped its catalog entries.

Closes https://github.com/pnpm/pnpm/issues/16931
2026-10-15 01:10:14 +02:00
Zoltan Kochan ad63103879 fix(deps): update rustls to 0.23.46 (#16941)
Resolves RUSTSEC-2026-0346, RUSTSEC-2026-0347, and RUSTSEC-2026-0348,
which fail the Cargo Deny check on every pull request that touches
Cargo.lock.
2026-10-14 23:18:01 +02:00
Zoltan Kochan 583f02ac6d fix(pnpr): address audit security, immutability, and storage bugs (#16927)
Fix the correctness, storage, and authentication audit issues identified in
https://github.com/pnpm/tasks/issues/131 (Section D):

- Preserve existing file permissions on atomic writes and default new auth
  files (`htpasswd`, `tokens.db`) to 0600 on Unix.
- Prevent login user enumeration when registration is disabled by running a
  dummy password verification and returning 401 instead of 403.
- Enforce a 71-byte maximum password length and use non-truncating bcrypt
  verification to prevent 72-byte truncation bypasses.
- Enforce immutability of published version metadata across partial unpublish
  requests and restore omitted metadata fields from the hosted manifest.
- Uniformly apply fetch guards to npm upstream proxy requests.
- Switch `is_write_request` to an explicit read-method and read-endpoint
  allowlist so sensitive endpoints are not bypassed by read-only tokens.
- Revoke active OIDC sessions when an admin deletes a user account.
- Prevent unpublish operations from deleting pnpr internal cache subsystems.
- Correctly normalize prefix comparisons in S3 package listings for
  grouped-registry namespaces.
- Ensure journal replay verifies blob presence in the hosted store to prevent
  tarball loss during crash recovery.
- Remove `deny_unknown_fields` from `PublishPipelineRun` deserialization.
- Domain-separate artifact fields with null bytes in `entry_digest`.
- Redact database passwords, auth tokens, and resolution secrets in config
  Debug representations.

Refs pnpm/tasks#131
2026-10-14 15:20:27 +02:00
33416bfce9 feat(auth-commands): print npm-notice header in web login (#16697)
Registries can return a login verification code in an npm-notice header
on the web-login probe (POST -/v1/login). The header was dropped, so
users never saw the code and could not finish the login.

Collect every npm-notice value before the response body is consumed and
print each one ahead of the auth URL. The text is registry-controlled,
so control and formatting characters are removed first, and a notice
that is empty afterwards is skipped. A notice is display-only and never fails the
login. Error paths print nothing, as before.

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:50:46 +02:00
01e844a052 fix(cli): stop aborting when stderr is a closed pipe (#16916)
`eprintln!` panics when its write fails, and the release profile builds
with `panic = "abort"`. When the reader of pnpm's stderr had exited, as
in `pnpm start 2>&1 | head -1` with a failing script, writing the
`[ELIFECYCLE]` line raised SIGABRT: exit code 134 and a core dump instead
of the script's exit code.

`pnpm-diagnostics` now exports `eprintln!` and `eprint!` macros that
write to stderr and drop a failed write, as the reporter already does.
Every `eprintln!`/`eprint!` in the CLI, `pnpm-store-dir`, and
`pnpm-diagnostics` goes through them. The macros keep the std names, so
perfectionist's `impure_macro_arguments` allow list, which matches the
final path segment, covers them like the std ones.

clippy's `print_stderr` restriction lint now rejects the std macros.
`allow-print-in-tests` exempts unit tests and `#[test]` functions. Code
that is never part of the pnpm binary opts out with a crate-level
`#![expect]`: the developer tools under `pnpm/tasks`, the test-only
binaries of `pnpm-fs`, and the CLI end-to-end suite.

Restoring the default SIGPIPE disposition is not an option: pnpm writes
to its children's stdin and relies on the `BrokenPipe` error to notice a
child that stopped reading.

Fixes pnpm/pnpm#16904

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-14 12:47:46 +02:00
Zoltan Kochan aa555c323a fix(install): install the Windows Rust toolchain for rustup's default host (#16917)
On Windows, the Rust toolchain pnpm installs now follows rustup's
default_host_triple. Without one, pnpm installs the MSVC toolchain for
the native machine architecture, as rustup does whatever target it was
built for. The fallback to the ABI
pnpm itself was built for is removed: a pnpm that installs dependencies
while building a MinGW package is usually the official MSVC build, and
the installed toolchain should not depend on how pnpm was built.

MSYSTEM is not used: Git for Windows' Git Bash sets MSYSTEM=MINGW64 too,
so it would switch most Windows users to the GNU toolchain.

The docs point distributions that build with their own toolchain at
runtime-on-fail=ignore.

Refs pnpm/pnpm#16854
2026-10-14 11:30:16 +02:00
Zoltan Kochan adb6411d85 ci(benchmark): add the store-only install against a mostly reusable lockfile (#16895)
None of the integrated benchmark's scenarios resolved against a lockfile
the walk could mostly reuse, installed store-only (`enableModulesDir:
false`), or read the per-file store index. The install regressions fixed
in pnpm/pnpm#16886, pnpm/pnpm#16888 and pnpm/pnpm#16890 all sat in that shape; the
benchmark would have missed each of them, and would miss their reversal.

The new `store-only.unlocked-direct-deps.cold-cache.hot-store` scenario
seeds the static fixture's lockfile with the direct dependencies whose
locked version carries a peer suffix left out of the importer, while
`packages` and `snapshots` keep the whole graph. That is `pnpm install`
after dependencies were added to `package.json` by hand: the missing
direct dependencies resolve from the registry, land on versions the
graph holds, and the walk reuses their subtrees. The choice of the
peered ones is deliberate: a dependency without peers is taken from the
lockfile's package list without a registry request, and a dependency
whose specifier merely changed is reused as long as the recorded version
satisfies the new range, so neither exercises the fresh pick that the
subtree reuse then follows. The metadata cache is wiped per iteration,
so every package the walk does resolve costs a round trip over the
emulated registry link; with a hot cache on local disk the whole
speculative warm-up costs about 25 ms and sits inside the noise. The
lockfile is trusted (`trustLockfile: true`): its supply-chain
verification reads every entry's packument and left nothing for the
resolution to fetch, hiding the difference entirely. With
`enableModulesDir: false` nothing is linked, so the rest of the timed
work is the check that the store holds every package, read through
`storeIndexFormat: files`.
`--no-frozen-lockfile` is passed explicitly because the runners set
`CI`, where a plain `install` is frozen and would reject the lockfile.
Only `cache-dir` is removed and the lockfile restored between
iterations; `node_modules` is never created and the warm store stays.

The scenario runs direct targets only, like the workspace fixtures: a
pnpr server would take over the resolution being measured. The
comparison script and its matrix-coverage test, the Bencher upload and
the summary list it alongside the others. Every `cleanup` arm now goes
through a `Cleanup` constructor, which keeps the method within the
function-length limit with the new arm.

Measured locally (hyperfine, 5 runs, main vs the commit before pnpm/pnpm#16886):
    main                     1.008 s +- 0.025 s   (0.991 .. 1.052 s), 513 packuments
    before pnpm/pnpm#16886            1.148 s +- 0.026 s   (1.131 .. 1.194 s), 840 packuments
2026-10-13 08:53:08 +02:00
Zoltan Kochan 0a286e7088 perf(python): read index metadata files and fetch ahead while resolving (#16881)
Python resolution read one thing at a time: the solver named one missing
index page or one version's metadata, pnpm fetched it, and the solver
ran again. Each version's metadata came out of the whole wheel, which
was downloaded and handed to the interpreter.

Resolution now reads the PEP 658 metadata file an index publishes
beside a wheel, checked against the SHA-256 the page published and
cached under `python-metadata-v1` for offline resolutions. Indexes that
publish none or no SHA-256 for it, and offline resolutions without a
cached file, still read the wheel. Installing still reads each wheel's
own metadata and re-solves the lockfile against it. Wheels are downloaded only for the selected releases, several
at a time. A build environment, which installs straight from a
resolution, fetches its selected wheels the same way.

The resolve loop also reads ahead: the index pages of everything a
requirement or a read metadata file names, and the metadata of the
version the solver will most likely pick. Reads run through `Fetcher`,
which borrows only the client, the index, and the config, so they stay
in flight while the registry is busy with a git clone, an sdist build,
or a wheel download. A distribution any requirement takes from a direct
URL is never read from the index, and a backtrack forgets what the
restored state lacks. What the solver asks for is checked as before; a
wrong guess costs a read.

The PEP 658 helpers pnpr already had move into `pnpm-python-resolver`,
which both now use.

On a 10-dependency project against PyPI, `install --lockfile-only` went
from 39-56 s to 4-5 s and a cold `install` from 25 s to 15 s.
2026-10-12 23:04:40 +02:00
Zoltan Kochan e68c30a556 fix(hooks): keep the pnpmfile worker from holding pnpm's pipes on macOS (#16871)
macOS has no pipe2, so the standard library creates a child's pipes and
marks them close-on-exec in a second step. A pnpmfile worker spawned on
another thread inside that window inherited the pipe. In the reresolving
install after patch-commit, the installability probe runs `node --version`
and reads its stdout to EOF while the worker spawns. When the worker
inherited the write end, pnpm waited for the worker to exit and the worker
waited for pnpm to close its stdin. The macOS CI job hit this as a 300s
timeout of hooks::update_config_applies_to_patch_commit_and_patch_remove.

The worker now marks every inherited descriptor above stdio close-on-exec
in a pre_exec hook (listed with proc_pidinfo), so it holds only the pipes
it was given.
2026-10-12 20:32:57 +02:00
Zoltan Kochan 96dd75378d feat(store-dir): add a per-package-file store index for shared stores (#16875)
A store on a filesystem that several machines use at once, like a CI
cache shared by many jobs, cannot hold index.db. SQLite's WAL mode
coordinates processes through a shared-memory file and file locks that
only work within one machine, so concurrent installs on such a store
corrupt index.db, and every lookup and write is a network round trip.

The new storeIndexFormat setting adds a "files" format. Every row is
written to its own file under <store>/package-index/, sharded by the
SHA-256 of its key. Each file holds the key and the same msgpack value
an index.db row holds. A row is written to a temporary name and renamed
into place, so writers need no locks and readers never see a partial
row. Two processes that update the same row at once each write a whole
row, and the later rename wins. Batched reads and writes run in
parallel, because a network filesystem's cost is round trips.

The format belongs to the store, not to each process's configuration.
Every StoreIndex open checks for a package-index directory and uses it
when present. That way jobs with different settings never split one
store across two indexes, and read-only tools (cat-index, store status)
need no setting. The store-index writer creates the directory when
storeIndexFormat asks for it, before it opens the index. A store that
switches formats starts with an empty index, and deleting
package-index switches it back.

StoreIndex now dispatches between an SQLite backend and the per-file
backend. The SQLite queries are unchanged.
2026-10-12 19:57:16 +02:00
Zoltan KochanandClaude Opus 5.5 84ecb039c4 revert: record the tools that build git-hosted dependencies (lockBuildTools) (#16859)
Reverts 94f60ac4b6 (pnpm/pnpm PR 16845) while the design of RFC 21 is
reconsidered. A git-hosted dependency that pins its package manager
exactly is already locked by its commit, and one prepared with pnpm
records its tools in its own lockfile, so it is unclear whether a
buildDependencies record in the consuming lockfile is needed.

lockBuildTools has not been released, so nothing depends on the format.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:55:38 +02:00
Zoltan Kochan 94f60ac4b6 feat: record the tools that build git-hosted dependencies (lockBuildTools) (#16845)
Stage 1 of RFC 21 (pnpm/rfcs PR 21), behind the experimental
lockBuildTools setting.

The lockfile records the package manager and Node.js that prepare each
git-hosted dependency: `tool:`-keyed packages entries (Prefix::Tool),
reached through a new snapshot field, buildDependencies. Install walks
never follow it. Pruning and the filtering walks keep the tools'
metadata. Tools get no snapshots entries, because a fresh install
materializes every snapshot. settings.lockBuildTools is recorded, so
toggling it makes the lockfile stale.

A pass after the fresh lockfile is built records the tools. It carries
records over from the previous lockfile while the package's resolution
is unchanged. Otherwise it reads the package at its commit through
detect_build_environment, which the prepare uses too, and resolves the
tools with the install's resolver chain and policies, reusing a
satisfying recorded version. The resolver chain outlives resolution
only while the setting is on.

On a cold fetch of a built git package, deps-restorer validates each
record with build_tool_artifact, fetches it by integrity into a private
store dir, and hands it to the git fetcher. The fetcher writes shims
that run the recorded files and puts the recorded Node.js on PATH. The
store-index key of the built package carries a digest of the tools.
Frozen verification checks tool entries against the registry package
they are published as.

package_manager_source moves into pnpm-tool-protocol so the CLI, the
recorder and the verifier share the package manager -> npm package
mapping. Package-manager detection and the build check work over a
PackageFiles trait, so a git host's archive can be read from the store's
file map.
2026-10-11 23:33:08 +02:00
Zoltan Kochan c3d4af9b76 fix: fall back to fsync when F_FULLFSYNC is unsupported (#16858)
On Apple platforms `File::sync_all` issues `fcntl(F_FULLFSYNC)`, which
virtiofs and SMB shares reject with ENOTTY, so every atomic write on such
a share failed, starting with pnpm-lock.yaml. Add `pnpm_fs::sync_file`,
which retries with plain `fsync` on ENOTTY/ENOTSUP/EOPNOTSUPP/EINVAL (as
SQLite and libuv do), and route every pnpm v12 sync through it.

pnpm v11 is unaffected: libuv's fsync already has this fallback.

Closes pnpm/pnpm#16846
2026-10-11 21:20:59 +02:00
Zoltan Kochan 24e198b479 feat: accept the tool: protocol for provisioned tools (#16838)
First step of the tool: protocol RFC (pnpm/rfcs PR 37). `tool:` is
accepted wherever a user writes a `runtime:` specifier: the node, bun,
deno and yarn resolvers, `pnpm add`, `pnpm update`, `pnx`, the
engines.runtime fold on manifest write, global shims, deploy, catalog
mode, and locked-version reuse. The lockfile freshness check treats
`tool:<spec>` and `runtime:<spec>` as equal, so either spelling in
package.json satisfies a lockfile written with the other.

`yarn@tool:<range>` maps to its Yarn line (npm `yarn`,
`@yarnpkg/cli-dist`, or the zpm release archives) the same way
`yarn@<range>` does, in `pnx` and in the install selector that `pnpm add`
and `pnpm add -g` use.

pnpm still writes `runtime:` in every specifier it saves, in lockfile
keys, and in resolved versions. engines.runtime and devEngines.runtime
store no protocol and read back as `runtime:`, so writing `tool:` into
the lockfile's importer specifiers would make a lockfile that earlier
pnpm versions reject on a frozen install. Switching to `tool:` needs the
lockfileVersion major bump the RFC schedules for the next major.

The spelling lives in a new leaf crate, pnpm-tool-protocol, so the
manifest, lockfile, resolver, and CLI crates share one definition.
2026-10-11 11:27:56 +02:00
Zoltan Kochan 7a349e398f fix(pnpr): connect the libsql backend over TLS (#16835)
The workspace builds libsql without its `tls` feature, so a
`backend.libsql` config panicked at startup with "The `tls` feature is
disabled, you must provide your own http connector", for every remote
URL including plain `http://`.

pnpr now gives libsql its own connector. It is built on the workspace's
rustls 0.23 with the aws-lc-rs provider, and it verifies servers against
the platform trust store through rustls-platform-verifier. Enabling
libsql's `tls` feature instead would add hyper-rustls 0.25 and a second
rustls release. The lockfile gains no new crates.

- The trust store is loaded only for URLs libsql dials over HTTPS, so an
  `http://` database works on a host without CA certificates. The
  `http://` check is case-insensitive.
- A connector built for HTTPS refuses plain-HTTP URIs, so a `base_url`
  in a Hrana response cannot move the auth token out of TLS.
- An `authToken` with a non-loopback `http://` url fails at startup,
  the HTTPS-or-loopback rule pnpr-client applies to its credentials. With a
  token, the connector also refuses each later plain-HTTP request to a
  non-loopback host.
2026-10-10 22:02:25 +02:00
Lazizbek ErgashevandZoltan Kochan 5804cae798 feat(config-deps): save and update config dependencies like project dependencies (#16816)
Config dependencies now follow the save and update rules of project
dependencies.

The env lockfile stores a config dependency's specifier next to the
version it resolved to, and plan_specifier treats an entry whose
specifier matches the declaration as satisfied. add --config went
through the same planner, so `add --config foo@latest` kept the old
version once a newer one was published.

ConfigDepUpdates names the config dependencies to resolve again, with
each one's previous specifier, the update behavior, and the range
style. resolve_and_install_config_deps_updating resolves them with
calc_specifier on, so the npm resolver computes the specifier to save
the same way it does for a package.json dependency, and returns those
specifiers for the caller to write to pnpm-workspace.yaml. An update
keeps a dist-tag declaration.

add --config passes the packages it adds with the savePrefix/saveExact
style, so `add --config foo@latest` saves a range such as ^1.2.0.
pnpm update passes the declared config dependencies its selectors
match: Compatible normally, Latest with --latest. --interactive,
--patches, --no-save, --filter, the dependency-group flags, and
versioned selectors leave them alone.

Fixes https://github.com/pnpm/pnpm/issues/16814

---------

Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-10 15:28:42 +02:00
Zoltan Kochan e5988d13b1 feat: share build output between machines through pnpr or a Turborepo remote cache (#16812)
Dependency side effects and `pnpm pipeline` task results now share one
artifact format, one trust model, and two transports.

`pnpm pipeline` stores cacheable task results as `workspace-task`
artifacts of the shared artifact protocol: P-256 signed envelopes whose
manifest mirrors a local cache entry (`meta.json` and `outputs/`). A
remote artifact is verified against `trustedKeys`, written into the
local cache, and restored through the same checks as a local entry.
Results of tasks that ran are published in the background when
`publish` is on, and the run waits for the uploads before it ends.

`ArtifactStore` in pnpm-pnpr-client is either a pnpr server or a server
that speaks the Turborepo Remote Cache API. Both answers go through the
same client-side verification. On the Turborepo API an artifact is the
signed envelope stored under a hash of the owner, the input key, and
the compatibility scope from `compatibility_scopes`, and each blob once
per owner under a hash of its integrity, mirroring pnpr's resolve and
blob routes. The API cannot create conditionally, so a later
publication replaces an earlier one. A failed per-key lookup is a miss. Both transports
refuse credentials for a server that is neither HTTPS nor loopback.
The remote side-effects cache now goes through `ArtifactStore`, so it
can use a Turborepo server too. pnpr-client's candidate filter now
permits `workspace-task` candidates, which the pnpr server already
accepted.

`ArtifactSigner` replaces the duplicated signing setup in deps-restorer
and the pipeline, and `decode_trusted_keys` moves into the protocol
crate.

The new `remoteCache` setting holds `url`, `team`, `token`, `org`, and
the signing fields, with `PNPM_REMOTE_CACHE_*` overrides. Every field
except `org` is machine-only and refused in pnpm-workspace.yaml with
ERR_PNPM_WORKSPACE_REMOTE_CACHE_TRUST: `token` is not URL-scoped, so a
repository that could name `url` could have the token sent anywhere.
`sideEffectsCache.remote` keeps `packages`; its signing fields remain a
fallback through `Config::remote_cache_settings()`. Only
`remoteCache.publish` publishes pipeline tasks, so a machine that
publishes dependency builds does not start uploading task outputs and
logs. The section is
boxed on `Config` to keep CLI futures under clippy's large_futures
limit.

Each artifact kind has its own size limits: a dependency build keeps
64 MiB per file and per artifact, and a task result may hold files of
up to 4 GiB and 16 GiB in all. Blobs travel between files and servers
as streams, so no result is held in memory. On pnpr,
`PUT /-/pnpr/v0/artifacts/blob` stores one blob ahead of its envelope
and commits it only once its bytes match the integrity, and `HEAD`
tells a publisher which blobs to skip. `artifacts.quota` makes pnpr's
storage bounds configurable.

pnpm-testing-utils gains an in-memory Turborepo Remote Cache server for
tests.

On pnpr, `artifacts.orgs` declares who may read and who may publish what
an organization owns: its signed artifacts, its compiler cache, and its
pipeline run records. It replaces the username-equals-organization rule
for artifacts, `artifacts.compilerCaches`, and `pipeline.workspaces`.
The shared-artifact store asks an `OrgAccess` policy the server builds
from it, and `CallerOrgs` replaces the duplicated authorize functions.
Pipeline run records are keyed `<org>/<workspace>/<run id>` under a new
`.pipeline-runs/v1` namespace; `pnpm pipeline --report` sends
`remoteCache.org`.
2026-10-10 13:51:38 +02:00
ee1519b02d chore(release): pnpr 0.1.0-alpha.15 (#16804)
* chore(release): pnpr 0.1.0-alpha.15

* docs(release): curate pnpr 0.1.0-alpha.15 release notes

Add a lead line naming the headline features and the security fix. Fold
follow-ups that ship in this same release into their features: web UI
sign-in into the web UI entry, If-Match into the rules API entry, and the
admin-removal half of the token fix into the account management entry.
Keep only the shipped part of the token fix (hand-edited htpasswd) as a
patch entry and move it to the top of the patch section. Split the CORS
note out of the teams entry and drop the pinning mechanism from the
integrity entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 18:11:53 +02:00
c1cfe3fdcb fix: handle invalid Windows package filenames (#16752)
On Windows, retry a failed package import with sanitized filename components.
Warn about renamed paths. Keep one source per case-insensitive path,
preferring a name that needed no renaming.
Skip the retry when a sanitized file path would be a directory of another one.
Strip control characters from the renamed-file warning in both CLIs.

---------

Co-authored-by: Sharmila Jesupaul <257629015+sharmila-oai@users.noreply.github.com>
Co-authored-by: Zoltan Kochan <z@kochan.io>
2026-10-09 00:44:19 +02:00
Zoltan Kochan faa2606a8b perf(pacquet): size rayon's pool in the Node.js addon like the CLI does (#16759)
The Node.js addon (`pnpm-napi`) embeds the Rust engine as a cdylib loaded
dynamically by Node.js. Rayon is statically linked into the cdylib and uses
Rayon's global thread pool for parallel operations (such as workspace
discovery, link phase clonefile/hardlinks, and store operations).

Previously, the Rayon global pool sizing policy was defined only in
`pnpm-cli`, meaning the Node.js addon defaulted to Rayon's unconstrained
core-count pool sizing (which can spin up excessive worker threads on
high-core machines and ignores container quota limits).

This change:
1. Moves the sizing policy and initialization logic (`configure_rayon_pool`,
   `rayon_pool_size`, `RAYON_THREADS_PER_CORE`, `MIN_RAYON_THREADS`,
   `MAX_RAYON_THREADS`) into `pnpm-package-manager`.
2. Uses `std::sync::Once` for idempotent initialization across multiple
   calls and tests within the same process.
3. Configures the pool lazily when Node.js addon entry points (`install()`,
   `rebuild()`, `get_peer_dependency_issues()`, etc.) are called (deliberately
   avoiding `module_init` to prevent deadlocking with dynamic loader locks
   during `dlopen`).
4. Adds unit tests validating the sizing bounds and idempotence.

Closes https://github.com/pnpm/tasks/issues/54
2026-10-08 23:45:48 +02:00
Zoltan Kochan fad744e272 feat(cargo): install the Rust toolchain rust-toolchain.toml names (#16741)
With `cargo.enabled`, `pnpm install` reads rustup's toolchain file
(`rust-toolchain.toml` or the older `rust-toolchain`, nearest at or above
each Cargo manifest, never above the checkout) and installs the toolchain
it names, the way pnpm already provisions a Python interpreter.

- The channel manifest is read from static.rust-lang.org (or
  `tools.rust.mirror`) and verified against the embedded Rust release
  signing key. Each component archive is checked against the SHA-256 the
  manifest names.
- Components are unpacked together into
  `<store>/rust/<release>-<host>-<digest>`, the layout rustup gives a
  toolchain, so rustc finds its standard library beside itself. The digest
  covers profile, components, and targets. `rust-docs` is left out of the
  default profile.
- The toolchain is linked as `.pnpm/rust` beside the toolchain file.
  `pnpm run`, `pnpm exec`, the Cargo task cache key, and the `cargo` and
  `rustc` invocations pnpm makes during an install use it.
- Pinned channels need no network once installed. Moving channels are
  re-read once a day; offline, the newest installed release is used.
- The cargo and rustc calls pnpm makes during an install use only
  toolchains this process provisioned, never a `.pnpm/rust` link the
  checkout could have committed.
- Any `runtimeOnFail` other than `download` leaves the toolchain to rustup.

The OpenPGP check in pnpm-crypto-shasums-file is generalized (and accepts
ASCII-armored signatures) so the Node.js and Rust verifiers share it. The
bounded reader the Python interpreter install used moves to pnpm-tarball.
2026-10-08 18:08:07 +02:00