* fix(security): harden build-approval artifact identities on v10
Port of pnpm/pnpm#12294 (commit bf1b731ee6) to release/10.
Package-name entries in onlyBuiltDependencies (and allowBuilds) no longer
approve lifecycle scripts for artifacts whose identity a name cannot pin:
git, git-hosted tarball, direct tarball, and local directory dependencies.
To approve such an artifact explicitly, use its peer-suffix-free lockfile
depPath as the key — the GIT_DEP_PREPARE_NOT_ALLOWED hint, pnpm
ignored-builds, and pnpm approve-builds print exactly that key.
- AllowBuild policy functions identify packages by DepPath instead of
caller-supplied name/version. Identity trust is derived from the depPath
shape: a registry-style depPath (name@semver) is a trusted identity.
- The trust is sound because lockfile entries are structurally checked
wherever they are materialized into fetchable resolutions
(pkgSnapshotToResolution) and before rebuild runs scripts: a
registry-style key backed by a git, directory, or git-hosted tarball
resolution is rejected with ERR_PNPM_RESOLUTION_SHAPE_MISMATCH.
- preparePackage requires a pkgResolutionId and gates on the synthesized
name@<resolution id> depPath; scp-style git URLs are normalized to
ssh:// form in resolution ids and the git fetcher reuses
createGitHostedPkgId from the resolver.
- isGitHostedTarballUrl matches case-insensitively and is shared from
@pnpm/lockfile.utils; new removePeersSuffix() in @pnpm/dependency-path
and allowBuildKeyFromIgnoredBuild() in @pnpm/builder.policy.
- pnpm rebuild and approve-builds accept depPath specs for selecting and
approving artifact builds; installs rebuild ignored builds approved by
depPath keys.
- shell-quote is overridden to 1.8.4 (GHSA-w7jw-789q-3m8p /
CVE-2026-9277).
Differences from main: v10 has no lockfile resolution verifier, so the
structural pass lives in pkgSnapshotToResolution and the rebuild loop; the
AllowBuild policy keeps v10's boolean return; the revoked-approval
detection and global-virtual-store hash changes have no v10 counterpart.
* test: serve the direct-tarball fixture from an off-registry origin
The registry mock binds only localhost, so a hard-coded 127.0.0.1 URL is
refused in CI, and a tarball URL on the registry's own origin resolves as
a registry package, which defeats the direct-tarball identity the test
needs. An in-test HTTP server redirecting to the mock provides a separate
origin whose binding the test controls.
* test: approve the git-protocol licenses fixture by its depPath
A git-hosted artifact has an untrusted package identity, so the
ajv-keywords build has to be approved by its depPath. Pin the fixture to
the commit the depPath names.
Cherry-pick of #11481 from main, adapted to the v10 layout.
For git-hosted tarballs (codeload.github.com / gitlab.com / bitbucket.org)
the fetcher dropped the integrity it computed while downloading, so the
lockfile only stored the URL. A compromised git host or man-in-the-middle
could serve a substituted tarball on subsequent installs and pnpm would
install it without lockfile changes.
This pins the SHA-512 SRI of the raw tarball in the lockfile in the same
sha512-<base64> form npm-registry tarballs use; subsequent installs verify
the download against that integrity in the worker.
A new optional gitHosted: boolean field is recorded on TarballResolution
so every store-key consumer can route by a single typed read instead of
re-deriving the routing from the URL. Lockfiles written by older pnpm
versions are enriched on load (URL fallback) so the field can be relied
on uniformly.
🤖 Cherry-picked by Claude (claude-opus-4-7) on behalf of @zkochan
* fix: prevent fork-bomb during packageManager-driven version switching
When pnpm was installed via one method (e.g. `npm install -g pnpm@A`)
and run in a project whose package.json's packageManager field selected
a different pnpm version (pnpm@B), and a pnpm-workspace.yaml existed at
the project root, the install-child spawned by `installPnpmToTools` to
fetch pnpm@B inherited a cwd under the pnpm home directory. pnpm's
workspace walk-up from there discovered the ancestor pnpm-workspace.yaml,
adopted the root package.json, and re-triggered switchCliVersion inside
the child. Because the target tool dir had not yet been symlinked in, the
recursive installPnpmToTools call saw alreadyExisted === false and kicked
off another nested install, recursing forever at 100% CPU.
Force the install-child's environment to disable its own version handling:
- `npm_config_manage_package_manager_versions=false` (v10 setting name)
- `pnpm_config_pm_on_fail=ignore` (v11+ setting name)
Also set the v11 setting on the final spawn at the end of switchCliVersion,
so when v10 hands off to a v11 target the child's check/download paths stay
disabled regardless of which env-var convention the child reads.
Closes#11337.
* test: add v11-switch and same-version regression tests for #11337
- v11 switch with a root pnpm-workspace.yaml: covers the primary #11337
reproducer (target major differs from running major). Before the fix this
fork-bombed via the install-child's workspace walk-up; now it reaches the
terminal spawn and `installPnpmToTools` completes.
- Same-version short-circuit: with a root pnpm-workspace.yaml and
`packageManager: pnpm@<current>`, `switchCliVersion` must return at the
`pm.version === packageManager.version` guard, and the tool dir must not
be created. Guards against a future regression where the ancestor
pnpm-workspace.yaml alone accidentally triggers an install.
* fix(installPnpmToTools): isolate the install-child from the caller's workspace
Pass `--ignore-workspace` to the child pnpm so it doesn't walk up from the
stage directory and adopt the caller's pnpm-workspace.yaml as its own root.
That walk-up was both (a) the mechanism that caused the #11337 fork-bomb
(the child would rediscover the caller's packageManager field and re-enter
switchCliVersion) and (b) a correctness problem in its own right: once the
child treats the caller's project as its workspace, `pnpm add` runs with
semantics that don't match an isolated tool-dir install. The env-var guards
from the previous commit stay in place as a defense-in-depth measure in
case any future code path surfaces a wantedPackageManager without going
through workspace discovery.
Also fold the new v11-switch regression into the existing v11 test rather
than adding a second v11 install, so CI doesn't fetch pnpm@11.0.0-rc.5
from the real npmjs registry twice. The tool-dir assertion in that test
now doubles as a fork-bomb regression check for the v11-target path.
Global Virtual Store (#10939)
Content-verified skip in GVS mode, tolerate EPERM during
bin creation on Windows, handle EPERM in DLX cache symlink.
(cherry picked from commit 62f760ec3d)
Allow approving all pending build dependencies at once without
interactive selection, useful for CI/CD pipelines and project
bootstrapping scenarios where interactive prompts are not feasible.
close#10136
* fix(dlx): print help message on calling pnpm dlx without arguments
Running `pnpm dlx` with no arguments would crash Node.js with a
TypeError as it attempted to call `.indexOf()` on an undefined variable.
This commit adds a guard clause and displays the help message instead
and exits gracefully.
Fixes#10633
* refactor: dlx
---------
Co-authored-by: Zoltan Kochan <z@kochan.io>
* fix: explicitly tell npm the config file path
* fix: `managingAuthSettings`
* feat: other `npm` call-sites
* docs: changeset
* fix: make optional again
* feat: remove the change from `publish`
* fix: eslint
* refactor: just one is sufficient
* fix(run): fail when no packages have script in filtered recursive run
Previously, `pnpm run -r <script>` and `pnpm run --filter <filter> <script>`
would silently succeed with exit code 0 when no packages had the specified
script, as long as a filter was used. This was inconsistent with the
documentation which states "If none of the packages have the command, the
command fails."
This change makes the command fail with ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT in
all cases where no packages have the script, regardless of whether a filter
is used. The `--if-present` flag can be used to suppress this error.
close#6844