Skip to content

fix: harden package-manager bootstrap metadata#12296

Merged
zkochan merged 2 commits into
mainfrom
vuln-063
Jun 9, 2026
Merged

fix: harden package-manager bootstrap metadata#12296
zkochan merged 2 commits into
mainfrom
vuln-063

Conversation

@zkochan

@zkochan zkochan commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

  • Resolve package-manager bootstrap metadata through trusted user/CLI registries and trusted network config, defaulting to the public npm registry instead of project/workspace registry settings.
  • Apply that bootstrap config in switchCliVersion() and syncEnvLockfile() so repository .npmrc proxy/TLS/configByUri values cannot steer package-manager bootstrap traffic.
  • Validate repository-provided package-manager env-lockfile entries before auto-switch install/execution: dependency paths must be registry package paths and package records must use integrity-only resolutions.
  • Preserve the fast path for fully resolved, valid package-manager metadata; incomplete metadata is still resolved through trusted bootstrap registries.
  • Handle peer-suffixed package-manager snapshots by looking up packages entries with removeSuffix(depPath) while keeping snapshots keyed by the full dep path.

Tests

  • ./node_modules/.bin/tsgo --build config/reader/tsconfig.json pnpm/tsconfig.json
  • ./node_modules/.bin/tsgo --build pnpm/tsconfig.json
  • PNPR_PREPARE_BIN=/Users/zoltan/.cargo_shared_target/debug/pnpr-prepare PNPR_BIN=/Users/zoltan/.cargo_shared_target/debug/pnpr PNPM_REGISTRY_MOCK_PORT=7799 NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../node_modules/.bin/jest src/switchCliVersion.test.ts src/syncEnvLockfile.test.ts test/getConfig.test.ts --runInBand
  • NODE_OPTIONS="--experimental-vm-modules --disable-warning=ExperimentalWarning --disable-warning=DEP0169" ../../node_modules/.bin/jest test/index.ts -t "package manager bootstrap registries|registries in current directory|CLI --registry overrides|request destinations do not expand" --runInBand
  • ./node_modules/.bin/eslint config/reader/src/Config.ts config/reader/src/index.ts config/reader/src/loadNpmrcFiles.ts config/reader/test/index.ts pnpm/src/packageManagerLockfile.ts pnpm/src/packageManagerRegistries.ts pnpm/src/switchCliVersion.ts pnpm/src/switchCliVersion.test.ts pnpm/src/syncEnvLockfile.ts pnpm/src/syncEnvLockfile.test.ts pnpm/test/getConfig.test.ts
  • ./node_modules/.bin/eslint pnpm/src/packageManagerLockfile.ts pnpm/src/switchCliVersion.test.ts
  • git diff --check
  • pre-push hook passed; it reported skipped-test warnings only.

Written by an agent (Codex, GPT-5).

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR separates trusted package-manager bootstrap registries/network config from project registries, validates env-lockfile package-manager entries require registry-based integrity-only resolutions, and threads the trusted bootstrap config through switch and sync flows.

Changes

Package Manager Bootstrap Registries

Layer / File(s) Summary
Configuration contract and trusted registry derivation
config/reader/src/Config.ts, config/reader/src/loadNpmrcFiles.ts, config/reader/src/index.ts, config/reader/test/index.ts
Config adds packageManagerRegistries? and packageManagerNetworkConfig?. loadNpmrcConfig computes trustedConfig from non-project sources (builtin, defaults, user .npmrc, auth, CLI) excluding workspace .npmrc. getConfig derives packageManagerRegistries and packageManagerNetworkConfig from the trusted subset and syncs CLI registry env to both targets; tests assert precedence and CLI override behavior.
Registry selection utility
pnpm/src/packageManagerRegistries.ts
Adds DEFAULT_PACKAGE_MANAGER_REGISTRY, getPackageManagerRegistries(config) returning default merged with config.packageManagerRegistries, and getPackageManagerBootstrapConfig(config) mapping packageManagerNetworkConfig into a bootstrap config.
Lockfile validation for registry-only resolutions
pnpm/src/packageManagerLockfile.ts
Adds assertPackageManagerLockfileUsesRegistryResolutions(envLockfile) that walks package-manager dependencies and packages/snapshots, enforcing registry package paths (no non-semver components, no id, matching name/version) and integrity-only resolutions (single integrity string). Throws PnpmError code INVALID_PACKAGE_MANAGER_LOCKFILE on failure.
CLI version switching integration
pnpm/src/switchCliVersion.ts, pnpm/src/switchCliVersion.test.ts
switchCliVersion computes bootstrap config via getPackageManagerBootstrapConfig, uses its registries for store/controller and integrity resolution, validates env-lockfile via assertPackageManagerLockfileUsesRegistryResolutions, ensures cleanup on errors, and calls installPnpmToStore with the derived registries. Tests cover trusted/default registry selection and rejections for poisoned or non-registry lockfiles.
Env lockfile sync integration
pnpm/src/syncEnvLockfile.ts, pnpm/src/syncEnvLockfile.test.ts
syncEnvLockfile derives bootstrap config and passes packageManagerConfig.registries to createStoreController and resolvePackageManagerIntegrities. Tests assert trusted registries are used when present and default to https://registry.npmjs.org/ when package-manager registries are absent.
Release documentation
.changeset/clean-package-manager-registries.md
Adds changeset noting bootstrap dependencies are resolved using trusted user/CLI registries/network config and that env-lockfile entries lacking registry-based integrity-only resolutions are rejected before auto-switch.

Sequence Diagram

sequenceDiagram
  participant SwitchCli as switchCliVersion
  participant Validator as assertPackageManagerLockfileUsesRegistryResolutions
  participant StoreCtrl as createStoreController
  participant Resolver as resolvePackageManagerIntegrities
  participant Installer as installPnpmToStore

  SwitchCli->>Validator: validate(envLockfile)
  Validator-->>SwitchCli: pass / throw INVALID_PACKAGE_MANAGER_LOCKFILE
  SwitchCli->>StoreCtrl: create(store opts with bootstrap.registries)
  StoreCtrl-->>SwitchCli: storeController
  SwitchCli->>Resolver: resolve integrities(registries: bootstrap.registries, envLockfile)
  Resolver-->>SwitchCli: resolved integrities
  SwitchCli->>Installer: installPnpmToStore(registries: bootstrap.registries)
  Installer-->>SwitchCli: installation result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • pnpm/pnpm#11681: Overlaps sync/resolve logic in env-lockfile handling and package-manager integrity resolution.
  • pnpm/pnpm#11754: Overlaps getConfig registry/default propagation and workspace registry behavior.

Poem

🐰 A rabbit hops through registries bright,
Trusted paths kept safely from workspace light,
Lockfiles checked for integrity's song,
Switches and syncs now follow along,
A little hop keeps package-manager right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: harden package-manager bootstrap metadata' accurately reflects the main security-focused change—validating and hardening how package-manager bootstrap metadata is resolved and validated, as evidenced by the new validation function, registry resolution through trusted sources, and lockfile integrity checks across the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vuln-063

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zkochan zkochan marked this pull request as ready for review June 9, 2026 20:05
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0)

Grey Divider


Action required

1. Peer suffix breaks lookup 🐞 Bug ≡ Correctness
Description
assertRegistryPackageManagerDependency() indexes envLockfile.packages using the full depPath, but
env lockfiles store package entries keyed by the depPath with peer suffixes removed. This can
falsely throw INVALID_PACKAGE_MANAGER_LOCKFILE for valid lockfiles (peer-qualified dep paths) and
block switchCliVersion() bootstrap/install.
Code

pnpm/src/packageManagerLockfile.ts[R30-37]

+  const packageInfo = envLockfile.packages[depPath]
+  const snapshot = envLockfile.snapshots[depPath]
+  if (packageInfo == null || snapshot == null) {
+    throw invalidPackageManagerLockfile(depPath)
+  }
+
+  assertRegistryPackagePath(depPath, packageInfo)
+  assertIntegrityOnlyResolution(depPath, packageInfo.resolution)
Evidence
The env-lockfile file format stores snapshots keyed by full depPath but stores packages keyed by
removeSuffix(depPath), so indexing packages with a peer-qualified depPath will miss even when
the lockfile is valid. The codebase also acknowledges peer suffixes exist in dep references by
stripping (...) in lockfile processing, making this mismatch user-reachable.

pnpm/src/packageManagerLockfile.ts[30-37]
lockfile/fs/src/lockfileFormatConverters.ts[31-58]
lockfile/fs/src/lockfileFormatConverters.ts[129-134]
installing/env-installer/src/resolvePackageManagerIntegrities.ts[86-94]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`assertRegistryPackageManagerDependency()` looks up `envLockfile.packages[depPath]`, but in lockfile file format `packages` is keyed by `removeSuffix(depPath)` while `snapshots` is keyed by the full depPath. If a package-manager dep (or transitive dep) is peer-qualified (e.g. `foo@1.0.0(bar@2.0.0)`), `snapshots[depPath]` exists but `packages[depPath]` does not, causing a false INVALID_PACKAGE_MANAGER_LOCKFILE.
### Issue Context
- `lockfile/fs/src/lockfileFormatConverters.ts` writes `packages` keys using `removeSuffix(depPath)`.
- Dep-path refs can include peer suffixes (the converter explicitly strips `(...)` in at least one traversal).
### Fix Focus Areas
- pnpm/src/packageManagerLockfile.ts[30-38]
- lockfile/fs/src/lockfileFormatConverters.ts[31-58]
- lockfile/fs/src/lockfileFormatConverters.ts[125-139]
### Suggested change
- Import `removeSuffix` from `@pnpm/deps.path` (or use an equivalent helper) and change the packages lookup to:
- `const packageInfo = envLockfile.packages[removeSuffix(depPath)]`
- Keep `const snapshot = envLockfile.snapshots[depPath]` as-is.
- Consider passing the original `depPath` into error messages, but use the normalized key for `packages` access.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Untrusted network config used ✓ Resolved 🐞 Bug ⛨ Security
Description
switchCliVersion()/syncEnvLockfile() override only the registry URLs for package-manager bootstrap,
but still pass the full Config into createStoreController, so workspace .npmrc-derived
proxy/strict-ssl and per-registry TLS/creds (configByUri) can affect bootstrap traffic. This
undermines the intent of resolving bootstrap metadata through trusted user/CLI config because an
untrusted repo can still influence routing/TLS validation for those requests.
Code

pnpm/src/switchCliVersion.ts[R35-45]

+  const packageManagerRegistries = getPackageManagerRegistries(config)
// Check if the env lockfile already has a resolved version that satisfies the wanted version/range.
let pmVersion = envLockfile?.importers['.'].packageManagerDependencies?.['pnpm']?.version
if (!pmVersion || !semver.satisfies(pmVersion, pm.version, { includePrerelease: true })) {
// Resolve to an exact version from the registry.
-    storeToUse = await createStoreController({ ...config, ...context })
+    storeToUse = await createStoreController({ ...config, ...context, registries: packageManagerRegistries })
envLockfile = await resolvePackageManagerIntegrities(pm.version, {
envLockfile,
-      registries: config.registries,
+      registries: packageManagerRegistries,
rootDir: context.rootProjectManifestDir,
Evidence
Workspace .npmrc can set proxy/strict-ssl and per-registry TLS/creds; those values are merged into
Config and then consumed by the store/client used during bootstrap, even though registries are now
trusted.

pnpm/src/switchCliVersion.ts[31-45]
pnpm/src/syncEnvLockfile.ts[23-45]
config/reader/src/loadNpmrcFiles.ts[59-131]
config/reader/src/localConfig.ts[17-24]
config/reader/src/localConfig.ts[192-199]
config/reader/src/index.ts[255-265]
config/reader/src/npmConfigTypes.ts[21-30]
config/reader/src/npmConfigTypes.ts[55-68]
store/connection-manager/src/createNewStoreController.ts[66-110]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Package-manager bootstrap currently overrides `registries` but still inherits other network-affecting settings (proxy/strict-ssl and per-registry TLS/creds via `configByUri`) from the project/workspace `.npmrc` because the full `config` object is spread into `createStoreController(...)`. This leaves a residual trust gap: an untrusted repo can still influence how bootstrap registry traffic is routed and TLS-validated.
## Issue Context
- `loadNpmrcConfig()` merges workspace `.npmrc` into `mergedConfig`, and `.npmrc`-readable keys include proxy and `strict-ssl`.
- `getConfig()` overlays `npmrcResult.mergedConfig` into `pnpmConfig`, so these workspace values end up in `Config`.
- `createNewStoreController()` consumes `httpProxy`/`httpsProxy`/`noProxy`/`strictSsl` and `configByUri` when creating the client.
- Bootstrap code only replaces `registries`, not these other settings.
## Fix Focus Areas
- Build a dedicated "package-manager bootstrap" network config from **trusted** sources (user/global/CLI), not workspace/project.
- Derive `configByUri` from `npmrcResult.trustedConfig` (similar to how `packageManagerRegistries` is derived), and pass it to the store/controller used for bootstrap.
- Ensure proxy and `strict-ssl` used during bootstrap come from trusted config (or secure defaults), not workspace `.npmrc`.
- Apply the sanitized bootstrap network config to both version switching and env-lockfile sync.
### Code pointers
- pnpm/src/switchCliVersion.ts[35-45]
- pnpm/src/syncEnvLockfile.ts[36-42]
- config/reader/src/index.ts[319-331]
- config/reader/src/loadNpmrcFiles.ts[59-121]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Misleading lockfile error target 🐞 Bug ◔ Observability
Description
When a package-manager dependency edge cannot be converted to a depPath (e.g. link:/file: ref),
assertRegistryPackageManagerDependency() throws INVALID_PACKAGE_MANAGER_LOCKFILE using the
*parent* depPath, so the error message points to the wrong lockfile entry. This makes failures
during switchCliVersion() validation harder to diagnose because the thrown message is directly
surfaced to users.
Code

pnpm/src/packageManagerLockfile.ts[R39-46]

+  for (const [name, ref] of Object.entries({
+    ...snapshot.dependencies,
+    ...snapshot.optionalDependencies,
+  })) {
+    const nextDepPath = refToRelative(ref, name)
+    if (nextDepPath == null) {
+      throw invalidPackageManagerLockfile(depPath)
+    }
Evidence
The validator currently throws an error formatted as `The packageManager dependency
"${depPath}"...`, but on an invalid child ref it passes the parent depPath, so the message cannot
identify the actual failing dependency edge. switchCliVersion() catches and rethrows this error
unchanged, making the misleading message user-visible.

pnpm/src/packageManagerLockfile.ts[39-46]
pnpm/src/packageManagerLockfile.ts[83-87]
pnpm/src/switchCliVersion.ts[75-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`assertRegistryPackageManagerDependency()` throws `invalidPackageManagerLockfile(depPath)` when `refToRelative(ref, name)` returns null, but `depPath` is the *parent* package. The resulting error text incorrectly blames the parent dependency instead of the failing child ref, which is what users need to fix.
### Issue Context
`invalidPackageManagerLockfile()` bakes the provided string into the user-facing message (`"The packageManager dependency \"${depPath}\" ..."`). `switchCliVersion()` rethrows this error, so the message becomes the primary diagnostic users see.
### Fix Focus Areas
- pnpm/src/packageManagerLockfile.ts[39-46]
- pnpm/src/packageManagerLockfile.ts[83-87]
### Expected change
Include the failing edge in the thrown error (e.g., mention `name` and `ref`, or format as `${parentDepPath} -> ${name}@${ref}`), so the error identifies the actual invalid record.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Incomplete lockfile blocks switch 🐞 Bug ≡ Correctness
Description
switchCliVersion() may skip resolvePackageManagerIntegrities() based on isPackageManagerResolved()
(version-only) but then still runs assertPackageManagerLockfileUsesRegistryResolutions(), which
requires packages/snapshots entries and integrity-only resolutions, so partially populated env
lockfiles can now fail with INVALID_PACKAGE_MANAGER_LOCKFILE. This is a backward-compatibility
regression for existing/stale env lockfiles and defeats the intended “resolve incomplete metadata
via trusted registries” behavior.
Code

pnpm/src/switchCliVersion.ts[R75-85]

+  if (!envLockfile) {
+    await storeToUse?.ctrl.close()
+    throw new PnpmError('NO_PKG_MANAGER_INTEGRITY', `The packageManager dependency ${pmVersion} was not found in pnpm-lock.yaml`)
+  }
+
+  try {
+    assertPackageManagerLockfileUsesRegistryResolutions(envLockfile)
+  } catch (err: unknown) {
+    await storeToUse?.ctrl.close()
+    throw err
+  }
Evidence
The fast path uses a version-only predicate, but the new validator requires full graph metadata in
packages/snapshots with integrity-only resolutions; therefore an env lockfile can satisfy the
fast-path predicate while still failing validation, causing an unexpected hard failure instead of
re-resolution.

installing/env-installer/src/resolvePackageManagerIntegrities.ts[25-38]
installing/env-installer/src/resolvePackageManagerIntegrities.ts[51-56]
pnpm/src/packageManagerLockfile.ts[6-38]
pnpm/src/switchCliVersion.ts[31-92]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`switchCliVersion()` now validates the env lockfile’s package-manager dependency graph via `assertPackageManagerLockfileUsesRegistryResolutions()`, but the earlier fast-path decision uses `isPackageManagerResolved()` which only checks that `packageManagerDependencies.pnpm/@pnpm/exe` versions match.
This means a lockfile that has matching `packageManagerDependencies` but is missing the corresponding `packages`/`snapshots` records (or lacks integrity-only resolution data) will be rejected immediately rather than being re-resolved through the trusted registries.
### Issue Context
- `isPackageManagerResolved()` is currently version-only and does not assert the presence/shape of `envLockfile.packages`/`envLockfile.snapshots` or integrity-only `resolution`.
- The new validator requires those records to exist and be integrity-only.
### Fix Focus Areas
- pnpm/src/switchCliVersion.ts[31-105]
- installing/env-installer/src/resolvePackageManagerIntegrities.ts[25-56]
- pnpm/src/packageManagerLockfile.ts[6-49]
### Implementation direction
Choose one of the following approaches:
1) **Tighten the fast-path predicate**: extend `isPackageManagerResolved()` (or add a new helper alongside it) to also verify that required `packages`/`snapshots` entries exist for `pnpm`/`@pnpm/exe` and that their `resolution` objects are integrity-only (and optionally that the graph is traversable).
2) **Fallback to re-resolution on “incomplete” failures**: in `switchCliVersion()`, if validation fails due to missing records / missing integrity (as opposed to clearly poisoned records like non-registry dep paths or extra resolution keys), then create a store controller with the trusted bootstrap config and call `resolvePackageManagerIntegrities()` to populate the missing metadata, then re-run validation.
Whichever approach you choose, ensure that genuinely poisoned lockfile records (non-registry dep paths or non-integrity resolution objects) are still rejected as intended.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Recursive graph validation overflow 🐞 Bug ☼ Reliability
Description
assertPackageManagerLockfileUsesRegistryResolutions() traverses the package-manager dependency graph
via recursion, so a crafted pnpm-lock.yaml with a very deep dependency chain can trigger a maximum
call stack error and crash the CLI before reporting INVALID_PACKAGE_MANAGER_LOCKFILE.
Code

pnpm/src/packageManagerLockfile.ts[R22-48]

+function assertRegistryPackageManagerDependency (
+  envLockfile: EnvLockfile,
+  depPath: DepPath,
+  visited: Set<DepPath>
+): void {
+  if (visited.has(depPath)) return
+  visited.add(depPath)
+
+  const packageInfo = envLockfile.packages[depPath]
+  const snapshot = envLockfile.snapshots[depPath]
+  if (packageInfo == null || snapshot == null) {
+    throw invalidPackageManagerLockfile(depPath)
+  }
+
+  assertRegistryPackagePath(depPath, packageInfo)
+  assertIntegrityOnlyResolution(depPath, packageInfo.resolution)
+
+  for (const [name, ref] of Object.entries({
+    ...snapshot.dependencies,
+    ...snapshot.optionalDependencies,
+  })) {
+    const nextDepPath = refToRelative(ref, name)
+    if (nextDepPath == null) {
+      throw invalidPackageManagerLockfile(depPath)
+    }
+    assertRegistryPackageManagerDependency(envLockfile, nextDepPath, visited)
+  }
Evidence
The validator follows dependency edges by calling itself for each referenced dependency, which makes
maximum call stack size dependent on lockfile graph depth. switchCliVersion() reads the env
lockfile from the project and invokes this validator before proceeding, so a crafted lockfile can
trigger the recursive path during auto-switch.

pnpm/src/packageManagerLockfile.ts[22-48]
pnpm/src/switchCliVersion.ts[31-85]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`assertRegistryPackageManagerDependency()` recursively traverses dependency edges and can overflow the JS call stack on deeply nested graphs from a project-controlled `pnpm-lock.yaml`, causing an unhandled runtime crash instead of a clean `INVALID_PACKAGE_MANAGER_LOCKFILE` error.
### Issue Context
This validation is invoked during auto-switch in `switchCliVersion()` on the env lockfile read from the project directory, so the input is attacker-controlled in scenarios where pnpm is run against an untrusted repo.
### Fix Focus Areas
- Convert recursion to an explicit stack/queue traversal (DFS/BFS) while retaining the `visited` set.
- Optionally add a hard cap on processed nodes / depth with a clear `PnpmError`.
### Fix Focus Areas (code refs)
- pnpm/src/packageManagerLockfile.ts[22-48]
- pnpm/src/switchCliVersion.ts[31-85]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
6. Global YAML ignored bootstrap 🐞 Bug ☼ Reliability
Description
getConfig() builds packageManagerNetworkConfig solely from npmrcResult.trustedConfig, so trusted
user global config.yaml network settings (e.g. http/https proxy, noproxy) are ignored for
package-manager bootstrap. This can prevent switchCliVersion()/syncEnvLockfile() from reaching
registries in environments that rely on global YAML network configuration.
Code

config/reader/src/index.ts[R325-335]

+  const trustedAuthConfig = pickIniConfig(npmrcResult.trustedConfig)
+  const trustedNetworkConfigs = getNetworkConfigs(trustedAuthConfig)
pnpmConfig.registries = { ...registriesFromNpmrc }
+  pnpmConfig.packageManagerRegistries = {
+    default: normalizeRegistryUrl(trustedAuthConfig.registry as string),
+    ...trustedNetworkConfigs.registries,
+  }
+  pnpmConfig.packageManagerNetworkConfig = createPackageManagerNetworkConfig(
+    npmrcResult.trustedConfig,
+    trustedNetworkConfigs.configByUri ?? {}
+  )
Evidence
The code explicitly applies global config.yaml to pnpmConfig, but packageManagerNetworkConfig is
then created only from npmrcResult.trustedConfig (which is assembled from .npmrc/auth.ini/CLI and
does not include global YAML). The resulting packageManagerNetworkConfig is what bootstrap paths
(switchCliVersion/syncEnvLockfile) pass to createStoreController via
getPackageManagerBootstrapConfig, so global YAML proxy settings will be ignored during bootstrap.

config/reader/src/index.ts[299-335]
config/reader/src/loadNpmrcFiles.ts[103-121]
config/reader/src/configFileKey.ts[25-46]
config/reader/src/npmConfigTypes.ts[27-68]
pnpm/src/packageManagerRegistries.ts[26-38]
pnpm/src/switchCliVersion.ts[31-46]
pnpm/src/syncEnvLockfile.ts[36-46]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Package-manager bootstrap network configuration (`config.packageManagerNetworkConfig`) is computed only from `npmrcResult.trustedConfig` (built from builtin/default/user .npmrc/auth.ini/CLI), and does not incorporate trusted **global config.yaml** network settings. As a result, environments that configure proxies/TLS via global YAML can fail to resolve/download the package-manager during bootstrap.
### Issue Context
- `getConfig()` applies `globalYamlConfig` to `pnpmConfig` (trusted, user-level) before computing registries.
- But `packageManagerNetworkConfig` is created from `npmrcResult.trustedConfig` and `trustedNetworkConfigs`, which do not include global YAML values.
- `switchCliVersion()` and `syncEnvLockfile()` use `getPackageManagerBootstrapConfig(config)` which sources proxy/TLS settings exclusively from `config.packageManagerNetworkConfig`.
### Fix Focus Areas
- config/reader/src/index.ts[299-335]
- config/reader/src/loadNpmrcFiles.ts[103-121]
### Suggested fix approach
1. Treat `global config.yaml` as a trusted source for *network* keys used in package-manager bootstrap.
2. Merge global YAML network settings into the inputs for `createPackageManagerNetworkConfig()` (and, if applicable, into trusted registries) with a clear precedence order, e.g.:
- CLI overrides
- user .npmrc/auth.ini
- global config.yaml
- builtin/defaults
3. Keep excluding workspace/project `.npmrc` and workspace registries as intended.
Concrete implementation options:
- Option A: Build a `trustedBootstrapNetworkSource` object by mapping selected fields from `globalYamlConfig` into their INI-key equivalents (`https-proxy`, `http-proxy`, `no-proxy`/`noproxy`, `strict-ssl`, `local-address`, `ca`/`cert`/`key` as applicable) and merge it into `npmrcResult.trustedConfig` before calling `createPackageManagerNetworkConfig()`.
- Option B: Extend `createPackageManagerNetworkConfig()` to accept both `npmrcTrustedConfig` and `globalYamlConfig` (typed), and resolve each field from the appropriate trusted precedence chain.
Add/adjust tests to cover a scenario where proxy is set only in global config.yaml and verify it appears in `config.packageManagerNetworkConfig` and is passed into `createStoreController()` during bootstrap.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Type-unsafe lockfile validation 🐞 Bug ☼ Reliability
Description
assertPackageManagerLockfileUsesRegistryResolutions() passes lockfile-derived values directly into
refToRelative(), so a malformed pnpm-lock.yaml (non-string version/ref) can throw a raw TypeError
instead of INVALID_PACKAGE_MANAGER_LOCKFILE. This can crash or surface confusing stack traces during
switchCliVersion() package-manager auto-switch validation.
Code

pnpm/src/packageManagerLockfile.ts[R13-15]

+  for (const [name, dependency] of Object.entries(packageManagerDependencies)) {
+    const depPath = refToRelative(dependency.version, name)
+    if (depPath == null) {
Evidence
The validation function calls refToRelative() with dependency.version and snapshot ref values
without type checks, while refToRelative() requires a string and calls string methods/indexing.
Env lockfile parsing only validates top-level shape and does not validate the types inside
packageManagerDependencies or dependency refs, so malformed values can reach this code path.

pnpm/src/packageManagerLockfile.ts[12-18]
pnpm/src/packageManagerLockfile.ts[39-48]
deps/path/src/index.ts[96-110]
lockfile/fs/src/envLockfile.ts[27-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`assertPackageManagerLockfileUsesRegistryResolutions()` assumes YAML-parsed values are strings and calls `refToRelative()` directly. If an env lockfile is malformed/tampered (e.g. `version: true` or a dependency ref object), `refToRelative()` will throw a `TypeError` (e.g. calling `.startsWith` on a non-string) rather than producing a controlled `PnpmError('INVALID_PACKAGE_MANAGER_LOCKFILE', ...)`.
### Issue Context
This code is part of the new safety gate before auto-switch install/execution, so it should fail closed with the intended pnpm error code/message even on malformed input.
### Fix Focus Areas
- pnpm/src/packageManagerLockfile.ts[6-48]
### Suggested implementation notes
- Before calling `refToRelative(dependency.version, name)`, validate `dependency` is an object and `dependency.version` is a non-empty string; otherwise throw `invalidPackageManagerLockfile(name)`.
- Before calling `refToRelative(ref, name)` for snapshot edges, validate `ref` is a non-empty string; otherwise throw `invalidPackageManagerLockfile(depPath)` (or include the offending ref/name in the error message).
- Keep the behavior as “reject lockfile” (don’t attempt recovery here), but ensure the rejection is always a `PnpmError` with `INVALID_PACKAGE_MANAGER_LOCKFILE`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Validate package-manager lockfile bootstrap metadata
🐞 Bug fix ✨ Enhancement 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Introduce isolated packageManagerRegistries and packageManagerNetworkConfig on Config,
  sourced only from trusted (user/CLI) npmrc layers — excluding project/workspace registries — to
  prevent workspace-level registry overrides from affecting package-manager bootstrap.
• Add assertPackageManagerLockfileUsesRegistryResolutions in packageManagerLockfile.ts that
  recursively validates the env-lockfile dependency graph, rejecting any record that uses a
  non-registry dep path or a resolution object with fields other than integrity.
• Update switchCliVersion and syncEnvLockfile to pass the new packageManagerBootstrapConfig
  (defaulting to https://registry.npmjs.org/) to createStoreController and
  resolvePackageManagerIntegrities instead of project registries.
• Fix error-handling order in switchCliVersion: validate lockfile integrity before creating the
  store controller, and properly close the store on validation failure.
• Add comprehensive tests covering registry isolation, npmjs fallback, fast-path (no re-resolve),
  and rejection of poisoned lockfiles (non-integrity fields, non-registry dep paths).
Diagram
graph TD
    A["loadNpmrcFiles.ts"] -->|"trustedConfig (user+CLI only)"| B["config/reader/index.ts"]
    B -->|"packageManagerRegistries\npackageManagerNetworkConfig"| C["Config interface"]
    C --> D["getPackageManagerBootstrapConfig"]
    D --> E["switchCliVersion.ts"]
    D --> F["syncEnvLockfile.ts"]
    E -->|"validate before install"| G["packageManagerLockfile.ts"]
    E -->|"bootstrap config"| H[("createStoreController")]
    F -->|"bootstrap config"| H
    G -->|"INVALID_PACKAGE_MANAGER_LOCKFILE"| I{{"PnpmError"}}

    subgraph Legend
      direction LR
      _mod["Module"] ~~~ _db[("Store/DB")] ~~~ _err{{"Error"}}
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Filter project registries at call-site
  • ➕ No new Config fields required
  • ➕ Smaller diff in config/reader
  • ➖ Trust logic scattered across multiple call sites
  • ➖ Easy to miss when adding new bootstrap operations
  • ➖ No single source of truth for what 'trusted' means
2. Re-read npmrc on demand in bootstrap functions
  • ➕ Config interface stays unchanged
  • ➖ Adds file I/O on every bootstrap operation
  • ➖ Duplicates npmrc parsing logic
  • ➖ Harder to test in isolation

Recommendation: The PR's approach of computing a separate trustedConfig at config-load time and threading it through as dedicated Config fields is the right architectural choice — it makes the trust boundary explicit and type-safe. An alternative would be to filter project registries at call-site inside switchCliVersion/syncEnvLockfile, but that would scatter the trust logic and make it easy to miss future call sites. A second alternative (a separate getBootstrapConfig() function that re-reads npmrc on demand) would add I/O overhead and duplicate parsing. The current approach is optimal.

Grey Divider

File Changes

Enhancement (5)
Config.ts Add PackageManagerNetworkConfig type and two new Config fields +14/-0

Add PackageManagerNetworkConfig type and two new Config fields

• Introduces the 'PackageManagerNetworkConfig' interface (TLS, proxy, and per-URI registry credentials) and adds 'packageManagerRegistries' and 'packageManagerNetworkConfig' optional fields to the 'Config' interface, providing a typed container for bootstrap-only network settings.

config/reader/src/Config.ts


loadNpmrcFiles.ts Compute trustedConfig from non-project npmrc sources +12/-0

Compute trustedConfig from non-project npmrc sources

• Adds a 'trustedConfig' field to 'NpmrcConfigResult' built from builtin, defaults, user, auth.ini, and CLI sources — intentionally excluding workspace/project '.npmrc' — and returns it alongside the existing 'mergedConfig'.

config/reader/src/loadNpmrcFiles.ts


index.ts Populate packageManagerRegistries and packageManagerNetworkConfig from trustedConfig +49/-0

Populate packageManagerRegistries and packageManagerNetworkConfig from trustedConfig

• Reads 'trustedConfig' from the npmrc result, derives 'trustedNetworkConfigs' from it, and populates 'pnpmConfig.packageManagerRegistries' and 'pnpmConfig.packageManagerNetworkConfig'. Also propagates CLI '--registry' overrides to 'packageManagerRegistries.default'. Adds helper functions 'createPackageManagerNetworkConfig', 'getEnvValue', and 'getProxyValue'.

config/reader/src/index.ts


packageManagerLockfile.ts New module: validate env-lockfile uses registry-only integrity resolutions +88/-0

New module: validate env-lockfile uses registry-only integrity resolutions

• Implements 'assertPackageManagerLockfileUsesRegistryResolutions', which recursively walks the package-manager dependency graph in the env lockfile and throws 'INVALID_PACKAGE_MANAGER_LOCKFILE' if any entry uses a non-registry dep path, has an 'id' override, or has a 'resolution' object with any key other than 'integrity'.

pnpm/src/packageManagerLockfile.ts


packageManagerRegistries.ts New module: build PackageManagerBootstrapConfig from Config +39/-0

New module: build PackageManagerBootstrapConfig from Config

• Exports 'getPackageManagerRegistries' (defaults to 'https://registry.npmjs.org/' when no trusted registries are configured) and 'getPackageManagerBootstrapConfig' (assembles the full network config for bootstrap operations from 'packageManagerNetworkConfig').

pnpm/src/packageManagerRegistries.ts


Bug fix (2)
switchCliVersion.ts Use trusted bootstrap config and validate lockfile before install +21/-11

Use trusted bootstrap config and validate lockfile before install

• Replaces all uses of 'config.registries' with 'packageManagerConfig.registries' and spreads 'packageManagerConfig' into 'createStoreController' calls. Moves the 'envLockfile' null-check before store creation and adds a 'assertPackageManagerLockfileUsesRegistryResolutions' call (with store cleanup on failure) before proceeding to install.

pnpm/src/switchCliVersion.ts


syncEnvLockfile.ts Use trusted bootstrap config in syncEnvLockfile +5/-2

Use trusted bootstrap config in syncEnvLockfile

• Calls 'getPackageManagerBootstrapConfig' and passes the result to 'createStoreController' and 'resolvePackageManagerIntegrities', replacing the previous use of 'config.registries'.

pnpm/src/syncEnvLockfile.ts


Tests (4)
index.ts Add tests for package-manager bootstrap registry isolation +65/-0

Add tests for package-manager bootstrap registry isolation

• Adds a test verifying that 'packageManagerRegistries' and 'packageManagerNetworkConfig' are populated from user-level config only (ignoring project/workspace registries), and extends two existing tests to assert that 'packageManagerRegistries.default' tracks the CLI '--registry' flag and the project '.npmrc' default.

config/reader/test/index.ts


switchCliVersion.test.ts New test file for switchCliVersion registry isolation and lockfile validation +319/-0

New test file for switchCliVersion registry isolation and lockfile validation

• Adds five tests: trusted registries are used instead of project registries, npmjs fallback when no trusted registries are configured, fast-path install without re-resolving when lockfile is valid, rejection of lockfiles with non-integrity resolution fields, and rejection of lockfiles with non-registry dep paths.

pnpm/src/switchCliVersion.test.ts


syncEnvLockfile.test.ts Add registry isolation tests to syncEnvLockfile test suite +94/-1

Add registry isolation tests to syncEnvLockfile test suite

• Adds two tests verifying that 'syncEnvLockfile' passes trusted package-manager registries (not project registries) to the store controller and resolver, and that it defaults to 'https://registry.npmjs.org/' when no trusted registries are configured.

pnpm/src/syncEnvLockfile.test.ts


getConfig.test.ts Update test description to reflect project-level registry warning message change +2/-2

Update test description to reflect project-level registry warning message change

• Renames the test and updates the expected warning string from 'Failed to replace env in config' to 'Ignored project-level request destination "registry"', matching the new warning emitted when a project '.npmrc' contains an unexpandable env variable in a registry URL.

pnpm/test/getConfig.test.ts


Other (1)
clean-package-manager-registries.md Add changeset for package-manager registry isolation patch +6/-0

Add changeset for package-manager registry isolation patch

• Declares a patch-level changeset for '@pnpm/config.reader' and 'pnpm' packages, describing the new trusted-registry bootstrap and lockfile validation behaviour.

.changeset/clean-package-manager-registries.md


Grey Divider

Qodo Logo

Comment thread pnpm/src/switchCliVersion.ts Outdated
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6b3e490

@zkochan zkochan marked this pull request as draft June 9, 2026 20:25
@zkochan zkochan marked this pull request as ready for review June 9, 2026 20:26
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6b3e490

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/reader/src/index.ts`:
- Around line 332-335: The package manager network config is falling back to
process.env because createPackageManagerNetworkConfig uses getProcessEnv
internally, so calling getConfig without threading the current opts.env leads to
incorrect proxy/no_proxy resolution; to fix, update the call that sets
pnpmConfig.packageManagerNetworkConfig to pass the effective environment through
(i.e., provide opts.env or the env used by getConfig into
createPackageManagerNetworkConfig), or modify getProcessEnv usage inside
createPackageManagerNetworkConfig to accept and use a provided env parameter;
reference pnpmConfig.packageManagerNetworkConfig,
createPackageManagerNetworkConfig, and getProcessEnv/GetConfig to locate the
code paths that need the env threaded and ensure the same env object used for
getConfig is forwarded into package-manager network fallback resolution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ce29f1d6-de9d-4072-a6cb-71181c48bf2d

📥 Commits

Reviewing files that changed from the base of the PR and between 498dd45 and 6b3e490.

📒 Files selected for processing (9)
  • .changeset/clean-package-manager-registries.md
  • config/reader/src/Config.ts
  • config/reader/src/index.ts
  • config/reader/test/index.ts
  • pnpm/src/packageManagerRegistries.ts
  • pnpm/src/switchCliVersion.test.ts
  • pnpm/src/switchCliVersion.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • pnpm/src/syncEnvLockfile.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/clean-package-manager-registries.md
🚧 Files skipped from review as they are similar to previous changes (6)
  • pnpm/src/packageManagerRegistries.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • pnpm/src/syncEnvLockfile.ts
  • config/reader/test/index.ts
  • pnpm/src/switchCliVersion.ts
  • pnpm/src/switchCliVersion.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • config/reader/src/index.ts
  • config/reader/src/Config.ts
🧠 Learnings (2)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • config/reader/src/index.ts
  • config/reader/src/Config.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • config/reader/src/index.ts
  • config/reader/src/Config.ts
🔇 Additional comments (1)
config/reader/src/Config.ts (1)

21-31: LGTM!

Also applies to: 242-242

Comment thread config/reader/src/index.ts
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit bbfa9d4

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9223096

@zkochan zkochan marked this pull request as draft June 9, 2026 20:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pnpm/src/switchCliVersion.test.ts (1)

94-162: ⚡ Quick win

Use afterEach for spy restoration instead of inline mockRestore().

Tests 1, 2, and 3 create a process.exit spy and call mockRestore() at the end of each test. If any assertion fails before mockRestore() is reached, the spy will not be restored and could leak into subsequent tests.

Jest's restoreAllMocks can be called in the afterEach hook for more reliable cleanup.

♻️ Refactor to use afterEach for spy cleanup

Add an afterEach hook after the beforeEach block:

 beforeEach(() => {
   closeStore.mockClear()
   createStoreController.mockClear()
   installPnpmToStore.mockClear()
   readEnvLockfile.mockClear()
   readEnvLockfile.mockResolvedValue(envLockfile)
   resolvePackageManagerIntegrities.mockClear()
   resolvePackageManagerIntegrities.mockResolvedValue(envLockfile)
   spawnSync.mockClear()
 })
+
+afterEach(() => {
+  jest.restoreAllMocks()
+})

Then remove the inline exit.mockRestore() calls from each test (lines 161, 214, 240).

Also applies to: 164-215, 217-241

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpm/src/switchCliVersion.test.ts` around lines 94 - 162, The tests create a
process.exit spy (jest.spyOn(process, 'exit')) inside multiple tests and call
exit.mockRestore() inline, which can leak if a test fails early; add an
afterEach hook (after the existing beforeEach) that calls jest.restoreAllMocks()
or specifically restores the process.exit spy to ensure cleanup, and remove the
inline exit.mockRestore() calls from the tests that create the spy (the tests
invoking jest.spyOn(process, 'exit') in switchCliVersion.test.ts).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pnpm/src/switchCliVersion.test.ts`:
- Around line 94-162: The tests create a process.exit spy (jest.spyOn(process,
'exit')) inside multiple tests and call exit.mockRestore() inline, which can
leak if a test fails early; add an afterEach hook (after the existing
beforeEach) that calls jest.restoreAllMocks() or specifically restores the
process.exit spy to ensure cleanup, and remove the inline exit.mockRestore()
calls from the tests that create the spy (the tests invoking jest.spyOn(process,
'exit') in switchCliVersion.test.ts).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 33264577-13dd-49c0-a29d-13b2954382b3

📥 Commits

Reviewing files that changed from the base of the PR and between bbfa9d4 and 4515d82.

📒 Files selected for processing (11)
  • .changeset/clean-package-manager-registries.md
  • config/reader/src/Config.ts
  • config/reader/src/index.ts
  • config/reader/src/loadNpmrcFiles.ts
  • config/reader/test/index.ts
  • pnpm/src/packageManagerLockfile.ts
  • pnpm/src/packageManagerRegistries.ts
  • pnpm/src/switchCliVersion.test.ts
  • pnpm/src/switchCliVersion.ts
  • pnpm/src/syncEnvLockfile.test.ts
  • pnpm/src/syncEnvLockfile.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • config/reader/src/Config.ts
  • .changeset/clean-package-manager-registries.md
  • pnpm/src/syncEnvLockfile.ts
  • pnpm/src/packageManagerRegistries.ts
  • pnpm/src/switchCliVersion.ts
  • pnpm/src/packageManagerLockfile.ts
  • config/reader/test/index.ts
  • config/reader/src/index.ts
  • pnpm/src/syncEnvLockfile.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Compile & Lint
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use Standard Style with trailing commas, prefer functions over classes, declare functions after they are used (relying on hoisting), limit functions to no more than two or three arguments, and use a single options object for functions needing more parameters
Follow import order: standard libraries first, then external dependencies (sorted alphabetically), then relative imports
Do not write comments that restate what the code already says; rename variables, split helpers, or move checks to more obvious places instead
Do not repeat documentation at call sites that already lives on the callee; update the JSDoc once and let every call site benefit
Use JSDoc for the function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the function body
Do not record past implementation shape, refactor history, or removed code in comments; use git log and git blame for that information instead
Write comments only when the reason for code is non-obvious, a hidden invariant exists, a workaround for a known bug is needed, or an exception to surrounding pattern is deliberate

Files:

  • config/reader/src/loadNpmrcFiles.ts
  • pnpm/src/switchCliVersion.test.ts
**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use instanceof Error for checking if a caught error is an Error object in Jest tests; use util.types.isNativeError() instead to work across realms

Files:

  • pnpm/src/switchCliVersion.test.ts
🧠 Learnings (2)
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.

Applied to files:

  • config/reader/src/loadNpmrcFiles.ts
  • pnpm/src/switchCliVersion.test.ts
📚 Learning: 2026-06-05T13:47:26.046Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/src/install/index.ts:2337-2343
Timestamp: 2026-06-05T13:47:26.046Z
Learning: In the pnpm/pnpm codebase, `PnpmError` automatically prefixes `err.code` with `ERR_PNPM_` when you pass a code that does not already start with `ERR_PNPM_` (it normalizes `this.code` via `code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}``). Therefore, during code review you should NOT flag `new PnpmError(...)` call sites for passing a bare error code (e.g., `new PnpmError('FROZEN_STORE_INCOMPATIBLE_WITH_PNPR', ...)`); the resulting `err.code` will still be `ERR_PNPM_FROZEN_STORE_INCOMPATIBLE_WITH_PNPR`.

Applied to files:

  • config/reader/src/loadNpmrcFiles.ts
  • pnpm/src/switchCliVersion.test.ts
🔇 Additional comments (8)
config/reader/src/loadNpmrcFiles.ts (5)

21-22: LGTM!

Also applies to: 49-52


66-71: LGTM!


120-127: LGTM!


182-229: LGTM!


245-274: LGTM!

pnpm/src/switchCliVersion.test.ts (3)

1-3: LGTM!


5-92: LGTM!


243-319: LGTM!

@zkochan zkochan marked this pull request as ready for review June 9, 2026 21:12
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 9223096

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8f67b55

@zkochan zkochan marked this pull request as draft June 9, 2026 21:41
Comment thread pnpm/src/packageManagerLockfile.ts Outdated
@zkochan zkochan marked this pull request as ready for review June 9, 2026 21:58
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jun 9, 2026

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 22c39cf

@zkochan zkochan changed the title Validate package-manager lockfile bootstrap metadata Harden package-manager bootstrap metadata Jun 9, 2026
@zkochan zkochan changed the title Harden package-manager bootstrap metadata fix: harden package-manager bootstrap metadata Jun 9, 2026
@zkochan zkochan merged commit 822beb5 into main Jun 9, 2026
12 checks passed
@zkochan zkochan deleted the vuln-063 branch June 9, 2026 22:30
zkochan added a commit that referenced this pull request Jun 10, 2026
* fix(package-bins): reject reserved manifest bin names

Manifest bin keys "", ".", "..", and scoped forms such as "@scope/.."
passed the bin-name guard because encodeURIComponent leaves them
unchanged. When joined to the global bin directory during global
remove/update/add operations, "." resolves to the bin directory itself
and ".." to its parent, which removeBin then recursively deletes.

Reject empty, ".", and ".." bin names after scope stripping.

Backport of #12289 to v10.

* fix: block untrusted request destination env expansion

Makes environment expansion trust-aware for registry/auth config and
request destinations:

- Stops project and workspace .npmrc files from expanding ${...}
  placeholders in registry/proxy request destinations, URL-scoped keys,
  and registry credential values.
- Stops repository-controlled pnpm-workspace.yaml from expanding
  ${...} placeholders in the registry setting.
- Preserves env expansion for trusted user/global/CLI/env config so
  existing token and registry setup flows continue to work.

Backport of #12291 (CAND-PNPM-122 / GHSA-3qhv-2rgh-x77r) to v10.

* fix(security): verify npm registry signature before spawning a package-manager binary

The packageManager field (and pnpm self-update) makes pnpm download and
run a specific pnpm version. The staged install's bytes were trusted
based on lockfile integrity alone, which proves nothing when the inputs
are repository-controlled.

pnpm now verifies the npm registry signature of the engine it is about
to spawn, over the installed integrity, against npm's public signing
keys embedded in the pnpm CLI (exactly as corepack does):

- verifyPnpmEngineIdentity() checks pnpm/@pnpm/exe and the materialized
  platform binaries of the staged install before it is linked into the
  tools directory.
- Fails closed: any verification failure, including an unreachable
  registry, refuses the version switch rather than running an unverified
  binary. Runs only on a tools-directory cache miss (an actual
  download).
- The embedded keys live in a generated file kept in sync with npm's
  keys endpoint by scripts/update-npm-signing-keys.mjs; the release
  workflow runs the check as a gate so a key rotation cannot silently
  break verification.

Backport of #12292 (CAND-PNPM-097) to v10.

* fix: harden package-manager bootstrap metadata

Resolve package-manager bootstrap traffic through trusted user/CLI
registries and trusted network config, defaulting to the public npm
registry instead of project/workspace registry settings:

- getConfig() now computes packageManagerRegistries and
  packageManagerNetworkConfig from trusted config sources only (CLI
  options, env config, user and global .npmrc) — never the repository's
  project/workspace .npmrc or pnpm-workspace.yaml.
- switchCliVersion() applies that bootstrap config when installing and
  verifying the wanted pnpm version, so repository .npmrc
  proxy/TLS/registry values cannot steer package-manager bootstrap
  traffic.

Backport of #12296 to v10. The v11 env-lockfile validation
parts do not apply: v10 bootstraps the wanted version through a staged
child install instead of an env lockfile.

* fix(security): verify Node.js runtime SHASUMS OpenPGP signature

When a repository requests a Node.js runtime (useNodeVersion or an
execution env), pnpm downloads and then executes a Node binary. The
download mirror is repository-configurable via node-mirror:<channel> in
project .npmrc, and the integrity came from SHASUMS256.txt fetched from
that same mirror — a circular check a malicious mirror can satisfy with
a tampered binary and matching hashes.

pnpm now fetches SHASUMS256.txt.sig and verifies its detached OpenPGP
signature against the Node.js release team's public keys, embedded in
the pnpm CLI, before trusting the hashes:

- @pnpm/crypto.shasums-file: new fetchVerifiedNodeShasums /
  fetchVerifiedNodeShasumsFile verify the signature via openpgp against
  the embedded keys (generated src/nodeReleaseKeys.ts, mirrored from
  the canonical nodejs/release-keys list).
- @pnpm/node.fetcher verifies the configurable-mirror SHASUMS for the
  release channel; pre-release channels (rc, nightly, ...) are unsigned
  by Node and remain unverified.
- scripts/update-node-release-keys.mjs keeps the keys current
  (pnpm run check:node-release-keys / update:node-release-keys), and
  the release workflow runs the check as a gate.

Backport of #12295 to v10 (without the pacquet Rust port,
which does not exist on this branch).

* test(env): sign the SHASUMS fixture for Node.js download tests

The Node.js download tests exercise the release channel, whose
SHASUMS256.txt is now signature-verified. Sign the fixture with a
generated OpenPGP key and trust it through the new
trustedNodeReleaseKeys test seam (threaded from plugin-commands-env via
@pnpm/node.fetcher to fetchVerifiedNodeShasums), so the tests keep
exercising the verification path instead of bypassing it.

* fix(self-updater): redact registry credentials from engine identity errors

Registry URLs may legally embed basic-auth credentials
(https://user:pass@host/). verifyPnpmEngineIdentity() interpolated the
packument URL and registry URL into PnpmError messages, and the
unreachable-registry path surfaced fetch-layer error messages that embed
the request URL — all of which land in terminal output and CI logs.
Strip URL credentials from every error message and truncate the non-200
response body.

* fix: update vulnerable transitive dependencies

Override shell-quote to >=1.8.4 (GHSA-w7jw-789q-3m8p, critical, pulled
in via concurrently) so the audit workflow passes again. The advisory
was published after the last release/10 audit run; it is unrelated to
the security backports on this branch.
@zkochan

zkochan commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

🚢 v11.5.3

zkochan added a commit that referenced this pull request Jun 17, 2026
…tries

Port the GHSA-j2hc-m6cf-6jm8 fix (#12296) to pacquet.

When pnpm auto-switches to the version requested by `packageManager` /
`devEngines.packageManager`, the bootstrap (`pnpm` / `@pnpm/exe`) must be
resolved through trusted registries only. Pacquet was resolving it through
`config.resolved_registries()`, which a malicious repository controls via
the workspace `.npmrc` or `pnpm-workspace.yaml` `registries:` block.

Add `Config::package_manager_bootstrap`, built in `Config::current()` from a
trusted-only fold of the URL-scoped env, `auth.ini`, and user `.npmrc`
sources (the project `.npmrc` is excluded), reusing the existing
registry/proxy/TLS/auth application logic. `PNPM_CONFIG_REGISTRY` still
overrides the bootstrap default registry because it is user-controlled.

`EnvInstallerContext::for_package_manager` routes only the package-manager
bootstrap path (`sync_package_manager_dependencies`) through this trusted
config; project `configDependencies` resolution keeps the project
registries, matching the narrow scope of the upstream TypeScript fix.
zkochan added a commit that referenced this pull request Jun 17, 2026
…tries

Port the GHSA-j2hc-m6cf-6jm8 fix (#12296) to pacquet.

When pnpm auto-switches to the version requested by `packageManager` /
`devEngines.packageManager`, the bootstrap (`pnpm` / `@pnpm/exe`) must be
resolved through trusted registries only. Pacquet was resolving it through
`config.resolved_registries()`, which a malicious repository controls via
the workspace `.npmrc` or `pnpm-workspace.yaml` `registries:` block.

Add `Config::package_manager_bootstrap`, built in `Config::current()` from a
trusted-only fold of the URL-scoped env, `auth.ini`, and user `.npmrc`
sources (the project `.npmrc` is excluded), reusing the existing
registry/proxy/TLS/auth application logic. It defaults to the public npm
registry, and `PNPM_CONFIG_REGISTRY` still overrides the default because it
is user-controlled.

`EnvInstallerContext::for_package_manager` routes only the package-manager
bootstrap path (`sync_package_manager_dependencies`) through this trusted
config; project `configDependencies` resolution keeps the project
registries, matching the narrow scope of the upstream TypeScript fix.
zkochan added a commit that referenced this pull request Jun 17, 2026
…tries (#12471)

Port the GHSA-j2hc-m6cf-6jm8 fix (#12296) to pacquet.

When pnpm auto-switches to the version requested by `packageManager` /
`devEngines.packageManager`, the bootstrap (`pnpm` / `@pnpm/exe`) must be
resolved through trusted registries only. Pacquet was resolving it through
`config.resolved_registries()`, which a malicious repository controls via
the workspace `.npmrc` or `pnpm-workspace.yaml` `registries:` block.

Add `Config::package_manager_bootstrap`, built in `Config::current()` from a
trusted-only fold of the URL-scoped env, `auth.ini`, and user `.npmrc`
sources (the project `.npmrc` is excluded), reusing the existing
registry/proxy/TLS/auth application logic. It defaults to the public npm
registry, and `PNPM_CONFIG_REGISTRY` still overrides the default because it
is user-controlled.

`EnvInstallerContext::for_package_manager` routes only the package-manager
bootstrap path (`sync_package_manager_dependencies`) through this trusted
config; project `configDependencies` resolution keeps the project
registries, matching the narrow scope of the upstream TypeScript fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant