Skip to content

fix: contain hoisted dependency aliases (GHSA-fr4h-3cph-29xv)#12343

Merged
zkochan merged 5 commits into
mainfrom
fix/contain-hoisted-dependency-aliases
Jun 12, 2026
Merged

fix: contain hoisted dependency aliases (GHSA-fr4h-3cph-29xv)#12343
zkochan merged 5 commits into
mainfrom
fix/contain-hoisted-dependency-aliases

Conversation

@zkochan

@zkochan zkochan commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

The nodeLinker: hoisted install restores its dependency graph straight from the lockfile via lockfileToHoistedDepGraph, which joins each dependency alias under a node_modules directory and imports the package files there. On a frozen / up-to-date lockfile, resolution is skipped entirely, so the alias validation added for the resolution path in #11954 never runs.

A crafted lockfile alias such as ../../../escape could therefore be joined directly under a hoisted node_modules directory and escape the install root, while reserved aliases such as .bin, .pnpm, or node_modules could overwrite pnpm-owned layout. Note the existing containment check alone is insufficient for the reserved-name case: .bin resolves inside node_modules, so only package-name validation rejects it.

Fix

The fix adds two complementary layers, in both stacks.

1. Linker sink guard

  • pnpm: fs/symlink-dependency/src/safeJoinModulesDir.ts now rejects aliases that are not valid npm package names (path-traversal, absolute, and reserved names) via validate-npm-package-name, in addition to its existing containment check. The hoisted-graph dep.name sink in lockfileToHoistedDepGraph.ts now goes through it instead of a raw path.join.
  • pacquet: new safe_join_modules_dir helper reusing the resolver's is_valid_dependency_alias; hoisted_dep_graph.rs validates the hoister's dep.0.name before adding the graph node or recursing.

2. Lockfile verification gate (defense in depth)

verifyLockfileResolutions gains an always-on, policy-independent structural check that rejects any importer or package-snapshot dependency alias that is not a valid package name. It runs before the cache lookup (so a record written by a version predating the rule can't fast-path around it) and before the packages guard (so a tampered importer alias is caught even when nothing is installed), failing the install early — before any fetch or filesystem work — for every node linker at once.

The two layers are complementary: trustLockfile disables the verifier but the sink guards still run; the sink guards are per-linker but the verifier covers a future linker that might add a new alias→path sink. isValidDependencyAlias is now exported from @pnpm/installing.deps-resolver; pacquet mirrors the gate in its lockfile-verification crate. Other name-keyed maps (overrides, patchedDependencies, peer deps) are deliberately excluded — they don't become directories.

Both stacks surface ERR_PNPM_INVALID_DEPENDENCY_NAME / INVALID_DEPENDENCY_NAME.

Tests

  • @pnpm/fs.symlink-dependency: helper tests extended with reserved-name aliases and valid positive controls (25 passed).
  • @pnpm/installing.deps-restorer: exploit-regression test asserting the hoisted graph rejects traversal/reserved aliases before any fetch or filesystem work (6 passed, verified failing without the fix). Existing node-linker=hoisted install controls still pass.
  • @pnpm/installing.deps-installer: verifier tests for invalid importer aliases, invalid snapshot-nested aliases, and valid positive controls (35 passed).
  • pacquet: safe_join_modules_dir unit tests + walker_rejects_invalid_hoisted_alias (package-manager, 449 lib tests passed); verifier-gate alias tests (lockfile-verification, 43 passed). cargo fmt, cargo doc -D warnings, cargo clippy --deny warnings, and dylint all clean.

Written by an agent (Claude Code, claude-fable-5).

Summary by CodeRabbit

  • Bug Fixes

    • Installs now validate dependency alias names early and fail fast for invalid aliases (including path‑traversal patterns and reserved directory names), preventing unsafe or escaped module paths and providing clearer diagnostics referencing the offending alias.
  • Tests

    • Added extensive tests covering alias validation during lockfile resolution, hoisted graph construction, and symlink handling.

The `nodeLinker: hoisted` install restores its dependency graph straight
from the lockfile via `lockfileToHoistedDepGraph`, which joins each
dependency alias under a `node_modules` directory and imports the
package files there. On a frozen / up-to-date lockfile, resolution is
skipped entirely, so the alias validation added for the resolution path
never runs. A crafted lockfile alias such as `../../../escape` could
therefore escape the install root, and reserved aliases such as `.bin`,
`.pnpm`, or `node_modules` could overwrite pnpm-owned layout.

Validate every alias at the hoisted-graph directory sink. The shared
`safeJoinModulesDir` helper now rejects aliases that are not valid npm
package names (path-traversal, absolute, and reserved names) in addition
to its containment check, and the hoisted graph routes its `dep.name`
sink through it. Pacquet mirrors the boundary: `safe_join_modules_dir`
validates the hoister's `dep.0.name` before adding the graph node or
recursing, reusing the same dependency-name rule it already applies to
direct-dependency aliases. Both stacks surface
`ERR_PNPM_INVALID_DEPENDENCY_NAME`.

---
Written by an agent (Claude Code, claude-fable-5).
@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f22f0ab4-962d-459a-9f20-1851145215c1

📥 Commits

Reviewing files that changed from the base of the PR and between f2994fd and af36ba0.

📒 Files selected for processing (4)
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📜 Recent 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:

  • installing/deps-installer/src/install/verifyLockfileResolutions.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:

  • installing/deps-installer/src/install/verifyLockfileResolutions.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:

  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
🔇 Additional comments (6)
installing/deps-installer/src/install/verifyLockfileResolutions.ts (6)

38-49: LGTM!


51-64: LGTM!


170-176: LGTM!


226-242: LGTM!


365-404: LGTM!


406-417: LGTM!


📝 Walkthrough

Walkthrough

This PR adds dual-layer protection against malicious dependency aliases: an always-on lockfile scan that rejects invalid alias keys, and safe-join helpers used by hoisted graph builders to validate aliases before creating node_modules paths. Changes touch both pnpm (TypeScript) and pacquet (Rust) and include tests.

Changes

Dependency Alias Containment

Layer / File(s) Summary
Changeset
.changeset/contain-hoisted-dependency-aliases.md
Documents the fix and notes the two enforcement points (safe join and lockfile verification).
Validation helper and exports
fs/symlink-dependency/package.json, fs/symlink-dependency/src/safeJoinModulesDir.ts, fs/symlink-dependency/src/index.ts, fs/symlink-dependency/test/safeJoinModulesDir.test.ts, installing/deps-resolver/src/index.ts
safeJoinModulesDir validates npm package names (rejecting traversal and reserved names) before joining paths, centralizes error formatting (ERR_PNPM_INVALID_DEPENDENCY_NAME), adds validate-npm-package-name deps, re-exports helpers, and expands tests for valid and invalid aliases.
Early lockfile verification gate
installing/deps-installer/src/install/verifyLockfileResolutions.ts, installing/deps-installer/test/install/verifyLockfileResolutions.ts
Adds an offline, policy-independent check that scans importer and snapshot dependency alias keys and fails fast with INVALID_DEPENDENCY_ALIAS_CODE when invalid aliases are found; updates verification cache identities and adds tests.
Record verification cache identity (TS)
installing/deps-installer/src/install/recordLockfileVerified.ts
Switches to combined offline-check cache identities (withOfflineCheckCacheIdentities) when recording/verifying lockfile checks.
TypeScript hoisted graph integration
installing/deps-restorer/src/lockfileToHoistedDepGraph.ts, installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts
Replaces direct path.join with safeJoinModulesDir when computing hoisted dependency directories; tests assert rejection occurs before store/fetch or filesystem writes.
Rust verification diagnostics & cache wiring
pacquet/crates/lockfile-verification/src/errors.rs, pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs, pacquet/crates/lockfile-verification/Cargo.toml
Adds InvalidDependencyAlias diagnostic, implements alias collection and early failure, introduces offline cache identity wiring for alias checks, and updates workspace dependencies.
Record verification cache identity (Rust)
pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs
Records verification results using the combined offline-check cache identities.
Rust safe join and hoisted graph integration
pacquet/crates/package-manager/src/safe_join_modules_dir.rs, pacquet/crates/package-manager/src/lib.rs, pacquet/crates/package-manager/src/hoisted_dep_graph.rs, tests
Introduces safe_join_modules_dir and InvalidDependencyAliasError, wires it into package-manager, uses it in hoisted_dep_graph with a new error variant, and adds unit and walker tests covering valid, traversal, and reserved aliases.
Comprehensive tests
fs/symlink-dependency/test/safeJoinModulesDir.test.ts, installing/deps-installer/test/install/verifyLockfileResolutions.ts, installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts, pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs, pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs, pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
Parameterized tests cover traversal and reserved aliases, valid scoped/unscoped aliases, and ensure rejection occurs before store/fetch or file creation.

Sequence Diagram

sequenceDiagram
  participant Client as pnpm install
  participant VLR as verifyLockfileResolutions
  participant Restorer as lockfileToHoistedDepGraph
  participant SafeJoinTS as safeJoinModulesDir
  participant SafeJoinRS as safe_join_modules_dir
  participant FS as node_modules filesystem
  Client->>VLR: provide lockfile
  VLR->>VLR: scan importer & snapshot alias keys
  VLR-->>Client: reject if invalid aliases found
  Client->>Restorer: build hoisted graph
  Restorer->>SafeJoinTS: safeJoinModulesDir(modules, alias)
  Restorer->>SafeJoinRS: safe_join_modules_dir(modules, alias)
  SafeJoinTS->>SafeJoinTS: validate alias name
  SafeJoinRS->>SafeJoinRS: validate alias name
  SafeJoinTS-->>Restorer: return safe path or error
  SafeJoinRS-->>Restorer: return safe path or error
  SafeJoinTS->>FS: use returned path
  SafeJoinRS->>FS: use returned path
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • pnpm/pnpm#11714: Related changes to lockfile verification cache identity recording.
  • pnpm/pnpm#11954: Overlaps on validating dependency aliases before path joining (safeJoinModulesDir) and related tests.
  • pnpm/pnpm#11788: Overlaps in hoisted dependency graph logic and alias handling.

🐰 A rabbit sniffs lockfiles with a curious twitch,
It hops on safe joins, skips aliases that glitch.
With early checks and tidy paths the burrow stays sound,
No .bin or ../ will ever run aground.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main security fix: containing hoisted dependency aliases to prevent escape/traversal attacks, with reference to the CVE identifier.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ 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 fix/contain-hoisted-dependency-aliases

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


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.

Add an always-on, policy-independent structural check to
verifyLockfileResolutions that rejects any importer or package-snapshot
dependency alias that is not a valid npm package name. A dependency
alias becomes a `node_modules/<alias>` directory at link time, so an
alias with path-traversal segments or a reserved name (`.bin`, `.pnpm`,
`node_modules`) could escape the install root or overwrite pnpm-owned
layout.

This complements the linker-sink guards: the verifier runs before any
fetch or filesystem work and covers every node linker at once, while the
sink guards still protect the `trustLockfile` path the verifier skips.
The check runs before the cache lookup so a record written by a version
that predates the rule cannot fast-path around it, and before the
`packages` guard so a tampered importer alias is caught even when nothing
is installed.

`isValidDependencyAlias` is now exported from `@pnpm/installing.deps-resolver`
and reused here. Pacquet mirrors the gate in its lockfile-verification
crate with a matching `ERR_PNPM_INVALID_DEPENDENCY_NAME` verdict.

---
Written by an agent (Claude Code, claude-fable-5).
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.00      7.5±0.39ms   575.9 KB/sec    1.03      7.7±0.26ms   561.5 KB/sec

@codecov-commenter

codecov-commenter commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.25000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.87%. Comparing base (615c669) to head (af36ba0).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...le-verification/src/verify_lockfile_resolutions.rs 95.83% 2 Missing ⚠️
pacquet/crates/lockfile-verification/src/errors.rs 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12343      +/-   ##
==========================================
+ Coverage   87.74%   87.87%   +0.12%     
==========================================
  Files         291      292       +1     
  Lines       36107    36314     +207     
==========================================
+ Hits        31683    31911     +228     
+ Misses       4424     4403      -21     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Each scenario reports direct installs and pnpr installs. Bencher consumes pacquet@HEAD and pnpr@HEAD.

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 4.377 ± 0.278 3.977 4.965 1.68 ± 0.17
pacquet@main 4.130 ± 0.100 3.966 4.269 1.59 ± 0.14
pnpr@HEAD 2.598 ± 0.213 2.317 3.031 1.00
pnpr@main 3.070 ± 0.586 2.353 4.054 1.18 ± 0.25
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 4.377330827820001,
      "stddev": 0.2781173100356417,
      "median": 4.402347111120001,
      "user": 2.8573838,
      "system": 2.6121643800000003,
      "min": 3.9768500341200004,
      "max": 4.96535384612,
      "times": [
        4.08793296312,
        4.24170967512,
        4.416587214120001,
        4.38810700812,
        4.53162370612,
        3.9768500341200004,
        4.96535384612,
        4.44705739812,
        4.52763864312,
        4.19044779012
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 4.13005415072,
      "stddev": 0.10041743366041282,
      "median": 4.14273492662,
      "user": 2.9045291000000004,
      "system": 2.64640808,
      "min": 3.96608451712,
      "max": 4.26880467312,
      "times": [
        4.23807367512,
        4.21336441212,
        4.16106350312,
        4.12440635012,
        3.96608451712,
        4.17123762812,
        4.26880467312,
        4.04279912012,
        4.10801301212,
        4.00669461612
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.5979040776199995,
      "stddev": 0.21344351046027205,
      "median": 2.53644944912,
      "user": 2.1107948000000003,
      "system": 2.3223705799999994,
      "min": 2.31731459612,
      "max": 3.03099437512,
      "times": [
        3.03099437512,
        2.4909043311200003,
        2.70864599112,
        2.3869260061200004,
        2.31731459612,
        2.5321111101200002,
        2.77479906812,
        2.46296807712,
        2.73358943312,
        2.5407877881200003
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 3.06961543292,
      "stddev": 0.586227140249338,
      "median": 2.9854653661200006,
      "user": 2.1186835000000004,
      "system": 2.31509508,
      "min": 2.35300539912,
      "max": 4.0536466221200005,
      "times": [
        2.8709139311200005,
        2.57406195212,
        2.35300539912,
        2.62082791112,
        3.7803643081200002,
        2.56373531412,
        3.10001680112,
        3.10906030912,
        3.67052178112,
        4.0536466221200005
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 738.6 ± 93.9 649.2 906.5 1.00
pacquet@main 883.6 ± 284.5 661.1 1627.4 1.20 ± 0.41
pnpr@HEAD 959.2 ± 187.1 757.9 1241.4 1.30 ± 0.30
pnpr@main 893.0 ± 113.8 747.3 1086.0 1.21 ± 0.22
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.738645417,
      "stddev": 0.09385516840763992,
      "median": 0.7187194625000001,
      "user": 0.30964334,
      "system": 1.0490084400000002,
      "min": 0.649156441,
      "max": 0.906504492,
      "times": [
        0.8619985370000001,
        0.6504922180000001,
        0.906504492,
        0.675256657,
        0.7847431060000001,
        0.649156441,
        0.6636390200000001,
        0.7744652510000001,
        0.7621822680000001,
        0.6580161800000001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.8836337962,
      "stddev": 0.28453077068087823,
      "median": 0.7767645575000002,
      "user": 0.30864724,
      "system": 1.0547681399999997,
      "min": 0.6611154010000001,
      "max": 1.627424506,
      "times": [
        0.6664956650000001,
        0.774752069,
        0.6611154010000001,
        0.8593267700000001,
        0.8734742150000001,
        0.7785156500000001,
        0.7653131140000001,
        0.7750134650000001,
        1.054907107,
        1.627424506
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.9592326386000002,
      "stddev": 0.18709076440574854,
      "median": 0.8779011320000001,
      "user": 0.32599673999999995,
      "system": 1.0421471400000002,
      "min": 0.7579202760000001,
      "max": 1.241372394,
      "times": [
        0.8842513070000001,
        0.8715509570000001,
        0.8323232970000001,
        1.233282338,
        0.7579202760000001,
        0.8280699950000001,
        0.8594328980000001,
        0.8847516930000001,
        1.241372394,
        1.199371231
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.8930337122,
      "stddev": 0.11380254664543527,
      "median": 0.8810808415000001,
      "user": 0.31779714,
      "system": 1.04850554,
      "min": 0.747314934,
      "max": 1.0860204,
      "times": [
        0.7520329590000001,
        0.8595523150000001,
        0.9407801870000001,
        0.8723763600000001,
        0.9759753820000001,
        0.8897853230000001,
        0.747314934,
        1.020027372,
        1.0860204,
        0.7864718900000001
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 3.661 ± 0.055 3.607 3.777 1.44 ± 0.15
pacquet@main 3.714 ± 0.148 3.613 4.121 1.46 ± 0.16
pnpr@HEAD 2.546 ± 0.258 2.188 2.895 1.00
pnpr@main 2.560 ± 0.393 2.195 3.524 1.01 ± 0.19
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 3.6613346665599997,
      "stddev": 0.05549386786004042,
      "median": 3.64159800396,
      "user": 2.8264179799999996,
      "system": 2.56958342,
      "min": 3.60727419346,
      "max": 3.77687703346,
      "times": [
        3.66820642846,
        3.70284739846,
        3.60727419346,
        3.62360790546,
        3.71760366146,
        3.77687703346,
        3.62395344346,
        3.65276189046,
        3.63043411746,
        3.60978059346
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 3.71388029236,
      "stddev": 0.1484854379858388,
      "median": 3.6836783519600003,
      "user": 2.83599028,
      "system": 2.59062892,
      "min": 3.61324339746,
      "max": 4.12136504946,
      "times": [
        3.68112523946,
        3.64031717446,
        3.61930050646,
        3.71577256146,
        3.71390342846,
        3.61324339746,
        3.68623146446,
        3.63605883646,
        4.12136504946,
        3.71148526546
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 2.54596885546,
      "stddev": 0.25808509466500185,
      "median": 2.60475199896,
      "user": 1.9574768800000002,
      "system": 2.2223293199999996,
      "min": 2.18816327946,
      "max": 2.89546559146,
      "times": [
        2.18816327946,
        2.7013850274599998,
        2.75148123746,
        2.44648806846,
        2.20332433546,
        2.53266777146,
        2.89546559146,
        2.67683622646,
        2.2693453464599997,
        2.79453167046
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 2.5599563018600002,
      "stddev": 0.39345364100633445,
      "median": 2.4887551654599998,
      "user": 1.9677421800000001,
      "system": 2.20640752,
      "min": 2.1947882014599998,
      "max": 3.52374969046,
      "times": [
        2.1947882014599998,
        2.58602334646,
        2.26702731846,
        2.36934149846,
        2.49970578246,
        2.2026633964599998,
        2.47780454846,
        2.79340751046,
        3.52374969046,
        2.68505172546
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.243 ± 0.237 1.030 1.753 1.72 ± 0.37
pacquet@main 1.137 ± 0.159 1.034 1.471 1.57 ± 0.27
pnpr@HEAD 0.723 ± 0.108 0.663 0.975 1.00 ± 0.18
pnpr@main 0.722 ± 0.069 0.670 0.871 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.2425540150600003,
      "stddev": 0.23702670800834613,
      "median": 1.1555552106600002,
      "user": 0.8571356400000001,
      "system": 1.3541041000000003,
      "min": 1.0303886126600001,
      "max": 1.7534784716600003,
      "times": [
        1.1314446796600002,
        1.0763950666600002,
        1.11017764566,
        1.23127182666,
        1.0436788406600002,
        1.0303886126600001,
        1.17966574166,
        1.7534784716600003,
        1.32324464366,
        1.5457946216600003
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.1367826746600003,
      "stddev": 0.1585618961948948,
      "median": 1.04946400066,
      "user": 0.86279244,
      "system": 1.3686142999999997,
      "min": 1.03410438466,
      "max": 1.4707856166600002,
      "times": [
        1.03410438466,
        1.03535358366,
        1.04183077266,
        1.1952346636600002,
        1.3686397356600002,
        1.4707856166600002,
        1.0837491856600001,
        1.04124843666,
        1.03978313866,
        1.05709722866
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.72309299066,
      "stddev": 0.10847209876013326,
      "median": 0.67666492966,
      "user": 0.26655014,
      "system": 1.0094452999999999,
      "min": 0.66296142166,
      "max": 0.9751309436600001,
      "times": [
        0.67745057466,
        0.66501861466,
        0.66296142166,
        0.67212353466,
        0.67653185066,
        0.9751309436600001,
        0.8713696186600001,
        0.68434941066,
        0.66919592866,
        0.6767980086600001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.72244252846,
      "stddev": 0.06890051842523026,
      "median": 0.69678475016,
      "user": 0.27460754,
      "system": 1.0186591999999999,
      "min": 0.66971770266,
      "max": 0.8707462556600001,
      "times": [
        0.69041834566,
        0.71153370666,
        0.69847137366,
        0.69874163366,
        0.69509812666,
        0.66971770266,
        0.8707462556600001,
        0.67647565666,
        0.82898202066,
        0.68424046266
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.414 ± 0.054 2.359 2.559 3.50 ± 0.08
pacquet@main 2.524 ± 0.192 2.359 2.971 3.66 ± 0.28
pnpr@HEAD 0.775 ± 0.125 0.658 1.014 1.13 ± 0.18
pnpr@main 0.689 ± 0.005 0.679 0.696 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.41364635162,
      "stddev": 0.054478632825727645,
      "median": 2.3996336446199997,
      "user": 1.2426568199999999,
      "system": 1.5983964,
      "min": 2.3590983621199997,
      "max": 2.55872340912,
      "times": [
        2.39782143512,
        2.3867825471199997,
        2.42481558212,
        2.42080355912,
        2.41236976112,
        2.38635178512,
        2.40144585412,
        2.3590983621199997,
        2.38825122112,
        2.55872340912
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.52436815942,
      "stddev": 0.1921451409920944,
      "median": 2.4817463536199997,
      "user": 1.23076162,
      "system": 1.5590362999999996,
      "min": 2.3592476851199997,
      "max": 2.97093455712,
      "times": [
        2.5175690671199997,
        2.36518371212,
        2.59457622012,
        2.4459236401199997,
        2.3592476851199997,
        2.36621061312,
        2.37572693612,
        2.6565595591199997,
        2.59174960412,
        2.97093455712
      ]
    },
    {
      "command": "pnpr@HEAD",
      "mean": 0.77539165012,
      "stddev": 0.12514807524208826,
      "median": 0.71450771412,
      "user": 0.26673751999999995,
      "system": 1.0166507,
      "min": 0.65762981412,
      "max": 1.01419119812,
      "times": [
        0.87050799412,
        0.65762981412,
        0.67819499312,
        0.66532038012,
        0.87287432712,
        1.01419119812,
        0.74674013212,
        0.68227529612,
        0.68223907312,
        0.8839432931200001
      ]
    },
    {
      "command": "pnpr@main",
      "mean": 0.6888916102199999,
      "stddev": 0.005082505397228882,
      "median": 0.68913811412,
      "user": 0.26041152,
      "system": 1.0420797999999998,
      "min": 0.67922832812,
      "max": 0.69636013912,
      "times": [
        0.68827784112,
        0.68928285812,
        0.68290086712,
        0.67922832812,
        0.6889933701200001,
        0.69636013912,
        0.69136193312,
        0.69507611412,
        0.68983274612,
        0.6876019051200001
      ]
    }
  ]
}

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12343
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
3,661.33 ms
(-55.01%)Baseline: 8,137.86 ms
9,765.44 ms
(37.49%)
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
🚷 view threshold
2,413.65 ms
(-46.55%)Baseline: 4,515.97 ms
5,419.16 ms
(44.54%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,242.55 ms
(-7.76%)Baseline: 1,347.15 ms
1,616.58 ms
(76.86%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
4,377.33 ms
(-51.37%)Baseline: 9,000.90 ms
10,801.08 ms
(40.53%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
738.65 ms
(+11.02%)Baseline: 665.32 ms
798.39 ms
(92.52%)
🐰 View full continuous benchmarking report in Bencher

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12343
Testbedpnpr

⚠️ WARNING: No Threshold found!

Without a Threshold, no Alerts will ever be generated.

Click here to create a new Threshold
For more information, see the Threshold documentation.
To only post results if a Threshold exists, set the --ci-only-thresholds flag.

Click to view all benchmark results
BenchmarkLatencymilliseconds (ms)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,545.97 ms
isolated-linker.fresh-install.cold-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
775.39 ms
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
723.09 ms
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
⚠️ NO THRESHOLD
2,597.90 ms
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
⚠️ NO THRESHOLD
959.23 ms
🐰 View full continuous benchmarking report in Bencher

`is_valid_dependency_alias` is in scope via `use`, so the bare
intra-doc link resolves on its own. The explicit path target tripped
`rustdoc::redundant-explicit-links` under the CI Doc job's
`cargo doc --document-private-items` (the local pre-push hook runs
`cargo doc` without that flag, so it didn't surface).

---
Written by an agent (Claude Code, claude-fable-5).
@zkochan zkochan marked this pull request as ready for review June 12, 2026 06:19
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0)

Grey Divider


Action required

1. Alias check bypassed without packages ✓ Resolved 🐞 Bug ⛨ Security
Description
verifyLockfileResolutions() returns immediately when lockfile.packages is missing, so the new
invalidAliases check in collectCandidates() never runs for lockfiles that only have importers. Since
LockfileObject allows packages to be absent, a tampered lockfile can bypass
ERR_PNPM_INVALID_DEPENDENCY_NAME at the verifier gate in this shape.
Code

installing/deps-installer/src/install/verifyLockfileResolutions.ts[R171-174]

+  const { candidates, shapeViolations, invalidAliases } = collectCandidates(lockfile)
+  if (invalidAliases.length > 0) {
+    throw buildInvalidAliasError(invalidAliases)
+  }
Evidence
The verifier’s new invalid-alias rejection runs only after calling collectCandidates(), but the
function returns before that when packages is absent; and the lockfile type explicitly allows
packages to be optional, so this bypass is reachable.

installing/deps-installer/src/install/verifyLockfileResolutions.ts[109-174]
lockfile/types/src/index.ts[29-32]

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

## Issue description
`verifyLockfileResolutions()` exits early on `!lockfile.packages`, which prevents the new importer/snapshot dependency-alias validation from running on lockfiles that omit `packages`. This breaks the intended “always-on” structural alias gate for that valid lockfile shape.
## Issue Context
- `LockfileObject` defines `packages` as optional, so `packages`-less lockfiles are a supported in-memory shape.
- The new alias validation currently lives inside `collectCandidates()`, which is only called after the early return.
## Fix Focus Areas
- installing/deps-installer/src/install/verifyLockfileResolutions.ts[109-177]
- installing/deps-installer/src/install/verifyLockfileResolutions.ts[386-426]
- lockfile/types/src/index.ts[29-32]
## Implementation sketch
- Move alias validation ahead of the `if (!lockfile.packages) return` guard, e.g.:
- Add a small helper that collects/validates importer alias keys (and snapshot alias keys only when present).
- If invalid aliases are found, throw `buildInvalidAliasError()`.
- Only after that, keep the existing `packages`-based return for the resolution-shape/policy verification path.
- Alternatively, change the early return to still run the new alias validation when `packages` is missing (and then return).

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



Remediation recommended

2. Pacquet error code mismatch 🐞 Bug ≡ Correctness
Description
pacquet’s new hoisted-sink guard (InvalidDependencyAliasError) reports diagnostic code
INVALID_DEPENDENCY_NAME even though the same condition in the verifier gate is surfaced as
ERR_PNPM_INVALID_DEPENDENCY_NAME, making error handling/log assertions inconsistent for identical
lockfile tampering.
Code

pacquet/crates/package-manager/src/safe_join_modules_dir.rs[R18-24]

+/// A dependency alias that would escape `modules` or collide with
+/// pnpm's own `node_modules` layout. Surfaces pnpm's
+/// `ERR_PNPM_INVALID_DEPENDENCY_NAME`.
+#[derive(Debug, Display, Error, Diagnostic)]
+#[display("Refusing to place a dependency under {} with the invalid alias {alias:?}", modules.display())]
+#[diagnostic(code(INVALID_DEPENDENCY_NAME))]
+pub struct InvalidDependencyAliasError {
Evidence
The new hoisted-sink guard claims to surface pnpm’s ERR_PNPM_INVALID_DEPENDENCY_NAME but is
annotated with INVALID_DEPENDENCY_NAME, while the verifier gate uses
ERR_PNPM_INVALID_DEPENDENCY_NAME. pacquet’s CLI tests show a pattern of asserting ERR_PNPM_*
codes in stderr, so this mismatch is user-visible and may break expectations.

pacquet/crates/package-manager/src/safe_join_modules_dir.rs[18-28]
pacquet/crates/lockfile-verification/src/errors.rs[102-108]
pacquet/crates/cli/tests/catalog.rs[141-158]

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

## Issue description
pacquet’s new `InvalidDependencyAliasError` is annotated with `#[diagnostic(code(INVALID_DEPENDENCY_NAME))]`, but the verifier gate and surrounding docs use `ERR_PNPM_INVALID_DEPENDENCY_NAME` for the same scenario. This leads to inconsistent error codes for the same tampering path (verifier vs hoisted sink) and can break tooling/tests that assert on pnpm-style `ERR_PNPM_*` codes.
## Issue Context
- `lockfile-verification` already emits `ERR_PNPM_INVALID_DEPENDENCY_NAME` for invalid dependency aliases.
- pacquet CLI tests commonly assert that stderr contains `ERR_PNPM_*` codes.
## Fix Focus Areas
- pacquet/crates/package-manager/src/safe_join_modules_dir.rs[18-24]
- pacquet/crates/lockfile-verification/src/errors.rs[102-108]
- pacquet/crates/cli/tests/catalog.rs[141-158]
## Expected change
- Update the diagnostic code on `InvalidDependencyAliasError` to `ERR_PNPM_INVALID_DEPENDENCY_NAME` (to match the verifier gate and documented compatibility expectations).
- Optionally audit other `INVALID_DEPENDENCY_NAME` diagnostics for consistency, but at minimum align the new hoisted-sink guard with the verifier gate introduced in this PR.

ⓘ 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

Copy link
Copy Markdown

PR Summary by Qodo

Contain hoisted lockfile dependency aliases (GHSA-fr4h-3cph-29xv)
🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

Walkthroughs

Description
• Reject path-traversal and reserved lockfile dependency aliases before any install work begins.
• Guard hoisted linker alias→path joins with npm package-name validation to prevent escaping
  node_modules.
• Add regression/unit tests across pnpm and pacquet for invalid/valid alias scenarios.
Diagram
graph TD
  L["pnpm-lock.yaml"] --> V["Lockfile verify gate"] --> H["Hoisted dep graph"] --> S["safeJoinModulesDir"] --> NM["node_modules/<alias>"]
  L --> PV["Pacquet verify gate"] --> PH["Pacquet hoisted graph"] --> PS["safe_join_modules_dir"] --> NM
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Verifier-only (lockfile gate only)
  • ➕ Blocks tampered lockfiles before any fetch/FS work across all linkers
  • ➕ Centralized policy with consistent error messaging
  • ➖ Does not protect when verification is bypassed (e.g., trustLockfile)
  • ➖ Does not protect future alias→path sinks that aren’t covered by the verifier’s scan shape
2. Sink-only (safe join at every alias→path join)
  • ➕ Protects even when lockfile verification is disabled
  • ➕ Local, hard-to-bypass guard at the actual filesystem sink
  • ➖ Requires auditing every current/future sink to ensure coverage
  • ➖ Errors may occur later in the install after some work has already happened
3. Canonicalize/sanitize aliases (rewrite to safe names)
  • ➕ Could allow installs to proceed while preventing escapes/overwrites
  • ➖ Silently changes dependency graph semantics and can mask lockfile tampering
  • ➖ Complicates reproducibility and debugging; risk of collisions

Recommendation: Keep the layered approach in this PR: an always-on lockfile structural gate for early, linker-wide rejection, plus sink-level guards for defense-in-depth when verification is skipped and to harden the actual alias→path boundary. This balances early failure (no fetch/FS) with robust containment at the filesystem sink.

Grey Divider

File Changes

Enhancement (3)
index.ts Export safeJoinModulesDir from fs.symlink-dependency +1/-0

Export safeJoinModulesDir from fs.symlink-dependency

• Re-exports safeJoinModulesDir so other packages (e.g., deps-restorer) can use the shared sink guard.

fs/symlink-dependency/src/index.ts


index.ts Export dependency-alias validation helpers from deps-resolver +1/-0

Export dependency-alias validation helpers from deps-resolver

• Exports assertValidDependencyAliases and isValidDependencyAlias for consumption by verifier/linker paths.

installing/deps-resolver/src/index.ts


errors.rs Introduce InvalidDependencyAlias error variant with pnpm-aligned hint +45/-0

Introduce InvalidDependencyAlias error variant with pnpm-aligned hint

• Adds a new diagnostic error variant surfaced as ERR_PNPM_INVALID_DEPENDENCY_NAME, including deterministic breakdown formatting and a security-focused hint explaining the risk.

pacquet/crates/lockfile-verification/src/errors.rs


Bug fix (6)
safeJoinModulesDir.ts Validate dependency aliases as npm package names before joining paths +27/-9

Validate dependency aliases as npm package names before joining paths

• Adds validate-npm-package-name checks to reject traversal/absolute/reserved aliases (e.g., .bin/.pnpm/node_modules) and retains the containment check as a secondary guard. Consolidates error construction under ERR_PNPM_INVALID_DEPENDENCY_NAME.

fs/symlink-dependency/src/safeJoinModulesDir.ts


verifyLockfileResolutions.ts Add always-on lockfile alias structural validation gate +70/-0

Add always-on lockfile alias structural validation gate

• Introduces a pre-cache, policy-independent scan of importer and snapshot dependency alias keys, rejecting any alias that is not a valid dependency alias. Throws a dedicated INVALID_DEPENDENCY_NAME PnpmError (ERR_PNPM_INVALID_DEPENDENCY_NAME) with a capped, deterministic breakdown.

installing/deps-installer/src/install/verifyLockfileResolutions.ts


lockfileToHoistedDepGraph.ts Route hoisted alias→dir join through safeJoinModulesDir +2/-1

Route hoisted alias→dir join through safeJoinModulesDir

• Replaces raw path.join(modules, dep.name) with safeJoinModulesDir to enforce alias validity at the hoisted restore sink.

installing/deps-restorer/src/lockfileToHoistedDepGraph.ts


verify_lockfile_resolutions.rs Add always-on invalid alias scan to pacquet lockfile verification +58/-0

Add always-on invalid alias scan to pacquet lockfile verification

• Mirrors pnpm’s structural alias validation by scanning importer and snapshot dependency aliases using the same 'validForOldPackages' rule, rejecting invalid aliases before cache/FS work.

pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs


hoisted_dep_graph.rs Guard hoisted alias→dir join via safe_join_modules_dir +10/-1

Guard hoisted alias→dir join via safe_join_modules_dir

• Replaces modules.join(alias) with safe_join_modules_dir and wires a new HoistedDepGraphError variant to surface invalid alias failures at the hoisted graph sink.

pacquet/crates/package-manager/src/hoisted_dep_graph.rs


safe_join_modules_dir.rs Add Rust safe_join_modules_dir sink guard helper +47/-0

Add Rust safe_join_modules_dir sink guard helper

• Introduces a helper that validates a dependency alias using existing resolver rules before joining under node_modules, returning a typed diagnostic error on invalid aliases.

pacquet/crates/package-manager/src/safe_join_modules_dir.rs


Refactor (1)
lib.rs Register new safe_join_modules_dir module +1/-0

Register new safe_join_modules_dir module

• Adds the safe_join_modules_dir module to the package-manager crate module tree.

pacquet/crates/package-manager/src/lib.rs


Tests (6)
safeJoinModulesDir.test.ts Extend safeJoinModulesDir tests for reserved aliases and valid controls +24/-1

Extend safeJoinModulesDir tests for reserved aliases and valid controls

• Expands negative cases to include reserved names that still resolve inside node_modules and adds positive tests for valid aliases (scoped/unscoped).

fs/symlink-dependency/test/safeJoinModulesDir.test.ts


verifyLockfileResolutions.ts Add verifier tests for invalid/valid dependency aliases +54/-0

Add verifier tests for invalid/valid dependency aliases

• Adds tests ensuring invalid aliases are rejected even with no verifiers and even when nested inside snapshots, plus acceptance tests for valid scoped/unscoped aliases.

installing/deps-installer/test/install/verifyLockfileResolutions.ts


lockfileToHoistedDepGraph.test.ts Add hoisted restore regression tests for traversal/reserved aliases +83/-0

Add hoisted restore regression tests for traversal/reserved aliases

• Introduces tests that craft a lockfile with malicious alias keys and assert early rejection with ERR_PNPM_INVALID_DEPENDENCY_NAME, including a check that no escaped path is created.

installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts


tests.rs Add pacquet verifier tests for invalid/valid aliases +96/-0

Add pacquet verifier tests for invalid/valid aliases

• Adds tests that invalid importer or snapshot-nested aliases are rejected even without verifiers, and that valid scoped/unscoped aliases pass.

pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs


tests.rs Add hoisted walker regression test for invalid aliases +33/-0

Add hoisted walker regression test for invalid aliases

• Adds a test ensuring traversal/reserved aliases from deserialized lockfiles are rejected before node insertion or recursion, matching pnpm’s behavior.

pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs


tests.rs Add unit tests for safe_join_modules_dir +34/-0

Add unit tests for safe_join_modules_dir

• Adds acceptance tests for valid aliases and rejection tests for traversal and reserved aliases.

pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs


Documentation (1)
contain-hoisted-dependency-aliases.md Add changeset documenting lockfile alias containment fix +14/-0

Add changeset documenting lockfile alias containment fix

• Introduces a changeset describing the vulnerability (path traversal/reserved aliases from lockfile) and the two-layer mitigation (sink guard + verification gate).

.changeset/contain-hoisted-dependency-aliases.md


Other (3)
package.json Add validate-npm-package-name dependency for alias validation +4/-2

Add validate-npm-package-name dependency for alias validation

• Adds validate-npm-package-name (runtime) and its types (dev) to support package-name validation in safeJoinModulesDir.

fs/symlink-dependency/package.json


Cargo.toml Add parse-wanted-dependency crate for alias validation +5/-4

Add parse-wanted-dependency crate for alias validation

• Adds pacquet-resolving-parse-wanted-dependency dependency to reuse npm package-name validation logic for the lockfile verification gate.

pacquet/crates/lockfile-verification/Cargo.toml


pnpm-lock.yaml Record new validate-npm-package-name dependencies in lockfile +6/-0

Record new validate-npm-package-name dependencies in lockfile

• Updates lockfile importers to include validate-npm-package-name and its type package as new dependencies.

pnpm-lock.yaml


Grey Divider

Qodo Logo

@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)
installing/deps-installer/test/install/verifyLockfileResolutions.ts (1)

484-536: ⚡ Quick win

Keep the invalid-alias fixture consistent across installing/deps-installer/test/install/verifyLockfileResolutions.ts, installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts, pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs, pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs, and pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs. These suites are all pinning the same security contract, but the newly added tables diverge and currently omit other invalid package names like "" and "." that are already covered in fs/symlink-dependency/test/safeJoinModulesDir.test.ts. Reusing one shared corpus, or at least adding those cases everywhere, would make the cross-layer regression coverage much harder to drift.

🤖 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 `@installing/deps-installer/test/install/verifyLockfileResolutions.ts` around
lines 484 - 536, The invalid-alias fixture needs to include the same set of bad
names across suites; update the test data used by the rejecting tests so it
contains the missing cases (e.g. "" and ".") in the test.each array in the
rejects-an-importer-dependency-alias test and in the nested-package snapshot
used in the rejects-an-invalid-alias-nested-in-a-package-snapshot test (the
lockfile/specifiers/dependencies entries created in verifyLockfileResolutions
tests), and mirror those additions in the other suites named in the comment
(installing/deps-restorer..., pacquet/.../tests.rs, etc.) so all tests that
assert ERR_PNPM_INVALID_DEPENDENCY_NAME use the identical corpus of invalid
names; locate the arrays and lockfile fixtures around the test names "rejects an
importer dependency alias %p" and "rejects an invalid alias nested in a package
snapshot" and add the missing entries.
🤖 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 `@installing/deps-installer/test/install/verifyLockfileResolutions.ts`:
- Around line 484-536: The invalid-alias fixture needs to include the same set
of bad names across suites; update the test data used by the rejecting tests so
it contains the missing cases (e.g. "" and ".") in the test.each array in the
rejects-an-importer-dependency-alias test and in the nested-package snapshot
used in the rejects-an-invalid-alias-nested-in-a-package-snapshot test (the
lockfile/specifiers/dependencies entries created in verifyLockfileResolutions
tests), and mirror those additions in the other suites named in the comment
(installing/deps-restorer..., pacquet/.../tests.rs, etc.) so all tests that
assert ERR_PNPM_INVALID_DEPENDENCY_NAME use the identical corpus of invalid
names; locate the arrays and lockfile fixtures around the test names "rejects an
importer dependency alias %p" and "rejects an invalid alias nested in a package
snapshot" and add the missing entries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 07346239-2313-4b6e-b271-e6be33eba06a

📥 Commits

Reviewing files that changed from the base of the PR and between 01b3d45 and 9a45e11.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • .changeset/contain-hoisted-dependency-aliases.md
  • fs/symlink-dependency/package.json
  • fs/symlink-dependency/src/index.ts
  • fs/symlink-dependency/src/safeJoinModulesDir.ts
  • fs/symlink-dependency/test/safeJoinModulesDir.test.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
  • installing/deps-installer/test/install/verifyLockfileResolutions.ts
  • installing/deps-resolver/src/index.ts
  • installing/deps-restorer/src/lockfileToHoistedDepGraph.ts
  • installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts
  • pacquet/crates/lockfile-verification/Cargo.toml
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs
📜 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). (2)
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (4)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
**/*.{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:

  • fs/symlink-dependency/src/index.ts
  • installing/deps-resolver/src/index.ts
  • installing/deps-restorer/src/lockfileToHoistedDepGraph.ts
  • installing/deps-installer/test/install/verifyLockfileResolutions.ts
  • fs/symlink-dependency/test/safeJoinModulesDir.test.ts
  • installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
  • fs/symlink-dependency/src/safeJoinModulesDir.ts
pacquet/**/Cargo.toml

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/Cargo.toml: Check whether the workspace already depends on something suitable in [workspace.dependencies] in the root Cargo.toml before adding a new dependency
Keep dependencies at the right level — add a new dependency to the specific crate that needs it, not to the workspace root or shared crate unless multiple crates depend on it

Files:

  • pacquet/crates/lockfile-verification/Cargo.toml
**/*.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:

  • fs/symlink-dependency/test/safeJoinModulesDir.test.ts
  • installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts
🧠 Learnings (9)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.

Applied to files:

  • pacquet/crates/package-manager/src/lib.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir/tests.rs
  • pacquet/crates/package-manager/src/safe_join_modules_dir.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph.rs
  • pacquet/crates/package-manager/src/hoisted_dep_graph/tests.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions/tests.rs
  • pacquet/crates/lockfile-verification/src/errors.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 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:

  • fs/symlink-dependency/src/index.ts
  • installing/deps-resolver/src/index.ts
  • installing/deps-restorer/src/lockfileToHoistedDepGraph.ts
  • installing/deps-installer/test/install/verifyLockfileResolutions.ts
  • fs/symlink-dependency/test/safeJoinModulesDir.test.ts
  • installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
  • fs/symlink-dependency/src/safeJoinModulesDir.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:

  • fs/symlink-dependency/src/index.ts
  • installing/deps-resolver/src/index.ts
  • installing/deps-restorer/src/lockfileToHoistedDepGraph.ts
  • installing/deps-installer/test/install/verifyLockfileResolutions.ts
  • fs/symlink-dependency/test/safeJoinModulesDir.test.ts
  • installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
  • fs/symlink-dependency/src/safeJoinModulesDir.ts
📚 Learning: 2026-05-05T23:03:04.286Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11479
File: __utils__/scripts/package.json:6-9
Timestamp: 2026-05-05T23:03:04.286Z
Learning: The pattern cross-env NODE_OPTIONS="$NODE_OPTIONS ..." in package.json scripts is an established convention in the pnpm/pnpm repository and is used across many packages (e.g., fs/hard-link-dir, worker, __utils__/scripts). Do not flag this as a cross-platform issue in individual files; if a change is needed, apply it as a repo-wide change in a separate PR. Scope this guidance to all package.json files in the repo; use the minimatch pattern '**/package.json' to identify relevant files and review changes at the repository level rather than per-file.

Applied to files:

  • fs/symlink-dependency/package.json
📚 Learning: 2026-05-26T21:01:06.666Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11966
File: .changeset/require-tarball-integrity.md:6-6
Timestamp: 2026-05-26T21:01:06.666Z
Learning: In pnpm lockfile-related release notes/docs (especially changeset markdown), preserve URL hostnames exactly as they appear in pnpm-lock.yaml tarball resolution entries—keep hosts like `codeload.github.com`, `bitbucket.org`, and `gitlab.com` in lowercase. Do not “correct” them to title-case/preserve brand capitalization (e.g., LanguageTool rules like `GITHUB` capitalization) because these are literal URL fragments, not platform brand names.

Applied to files:

  • .changeset/contain-hoisted-dependency-aliases.md
📚 Learning: 2026-06-05T13:47:05.929Z
Learnt from: vsumner
Repo: pnpm/pnpm PR: 12190
File: installing/deps-installer/test/install/frozenStore.ts:2-17
Timestamp: 2026-06-05T13:47:05.929Z
Learning: In the pnpm/pnpm repository, the shared Jest preset keeps `injectGlobals` at its default (`true`), so `test` and `expect` are available as Jest globals. Therefore, reviewers should not flag (or treat as TypeScript/compilation errors) missing `import { test, expect } from 'jest/globals'` when a test file uses `test`/`expect` without importing them. Importing from `jest/globals` may still be used for consistency with sibling files, but it is not required for execution in this repo unless a Jest preset is explicitly configured with `injectGlobals: false`.

Applied to files:

  • installing/deps-installer/test/install/verifyLockfileResolutions.ts
  • fs/symlink-dependency/test/safeJoinModulesDir.test.ts
  • installing/deps-restorer/test/lockfileToHoistedDepGraph.test.ts
🔇 Additional comments (11)
.changeset/contain-hoisted-dependency-aliases.md (1)

1-15: LGTM!

fs/symlink-dependency/package.json (1)

40-41: LGTM!

Also applies to: 50-51

fs/symlink-dependency/src/safeJoinModulesDir.ts (1)

1-37: LGTM!

fs/symlink-dependency/src/index.ts (1)

1-27: LGTM!

installing/deps-resolver/src/index.ts (1)

73-73: LGTM!

installing/deps-installer/src/install/verifyLockfileResolutions.ts (2)

101-120: LGTM!


232-279: LGTM!

installing/deps-restorer/src/lockfileToHoistedDepGraph.ts (1)

11-11: LGTM!

Also applies to: 222-222

pacquet/crates/package-manager/src/safe_join_modules_dir.rs (1)

18-44: LGTM!

pacquet/crates/package-manager/src/hoisted_dep_graph.rs (1)

329-336: LGTM!

Also applies to: 702-702

pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs (1)

26-26: Confirm verifier ↔ sink alias predicate equivalence.

pacquet_resolving_deps_resolver::is_valid_dependency_alias(alias) delegates directly to pacquet_resolving_parse_wanted_dependency::is_valid_old_npm_package_name(alias), so both the early lockfile gate and the hoisted safe_join_modules_dir path sink enforce the exact same alias validity rules.

… candidate pass

The dependency-alias check ran as its own full traversal of the lockfile
in addition to collectCandidates' existing pass over every package
snapshot. Fold it into that pass instead: collectCandidates now also
validates each importer and snapshot dependency alias and returns the
invalid ones alongside the resolution-shape violations, so the lockfile
is walked once per verification rather than twice.

Because collectCandidates runs after the verification-cache lookup, the
alias check is now covered by the cache the same way the resolution-shape
check is: a new dependencyAliasCheck cache identity makes a record
written before this rule existed fail canTrustPastCheck, forcing a
re-verification. The shared helper is renamed
withOfflineCheckCacheIdentities and appends both offline-structural-check
identities.

No behavior change for valid lockfiles; the same
ERR_PNPM_INVALID_DEPENDENCY_NAME is thrown for invalid aliases. Mirrored
in pacquet's lockfile-verification crate.

---
Written by an agent (Claude Code, claude-fable-5).
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

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

@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 `@installing/deps-installer/src/install/verifyLockfileResolutions.ts`:
- Around line 227-228: There are two consecutive JSDoc openers (`/**`) at the
start of the documentation block in verifyLockfileResolutions.ts which produces
a malformed JSDoc; remove the stray leading `/**` so only a single `/**` begins
the doc comment and ensure the block ends with a matching `*/` to restore a
valid JSDoc for the surrounding function/statement.
🪄 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: 319a0ead-aacc-44bf-aac6-eacfb44e9944

📥 Commits

Reviewing files that changed from the base of the PR and between 9a45e11 and f2994fd.

📒 Files selected for processing (4)
  • installing/deps-installer/src/install/recordLockfileVerified.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
  • pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Log emissions are part of matching pnpm — when porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(...), or streamParser.write(...), mirror the call site, payload, and ordering so @pnpm/cli.default-reporter parses pacquet's NDJSON the same way
Declare a newtype wrapper for branded string types instead of collapsing the brand into a plain String or &str in Rust
If upstream TypeScript always validates before construction of a branded string, validate in the Rust wrapper too via TryFrom<String> and/or FromStr and do not provide an infallible public constructor
If upstream TypeScript never validates a branded string, just brand for type-safety in Rust by exposing an infallible From<String> constructor
If upstream TypeScript occasionally constructs a branded string without validation, expose from_str_unchecked in Rust as an escape hatch alongside the validating constructor
Match upstream serde behavior for branded strings crossing JSON, YAML, or INI boundaries by using #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization
Derive simple conversions for branded strings using #[derive(derive_more::From)] and #[derive(derive_more::Into)] instead of handwriting impl blocks; use manual impl only when conversion needs custom logic
Model TypeScript string literal unions (like 'auto' | 'always' | 'never') as Rust enums instead of newtype wrappers, since the set of valid values is closed
Treat TypeScript string template literal types (like `${string}@${string}`) the same as branded string types in Rust, using a newtype wrapper with validation
Follow the code style guide in CODE_STYLE_GUIDE.md — imports, modules, naming, ownership and borrowing, parameter type selection, trait bounds, pattern matching, pipe-trait, error handling, test layout, and cloning of Arc and Rc
Choose owned vs. borrowed parameters to minimize copies; widen to t...

Files:

  • pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
**/*.{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:

  • installing/deps-installer/src/install/recordLockfileVerified.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
🧠 Learnings (6)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 Learning: 2026-06-06T18:58:37.156Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 12243
File: pacquet/crates/package-manager/src/install_package_by_snapshot.rs:319-322
Timestamp: 2026-06-06T18:58:37.156Z
Learning: When reviewing Rust code, do not assume `matches!(expr, Pattern(_))` will move out of `expr` if `Pattern(_)` contains no by-value bindings. `matches!` desugars to a `match` that auto-borrows the scrutinee for discrimination, so even if `expr` is a non-`Copy` value behind a shared reference (e.g., `&T`), the macro should not move-out of the borrowed data purely due to `matches!`. Treat `matches!(&expr, Pattern(_))` as a readability/clarity improvement, not a correctness requirement. Only flag potential move-out-of-borrow risks when the pattern includes by-value bindings that would require moving the matched value.

Applied to files:

  • pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs
  • pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs
📚 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:

  • installing/deps-installer/src/install/recordLockfileVerified.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.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:

  • installing/deps-installer/src/install/recordLockfileVerified.ts
  • installing/deps-installer/src/install/verifyLockfileResolutions.ts
🔇 Additional comments (12)
installing/deps-installer/src/install/verifyLockfileResolutions.ts (5)

47-65: LGTM!


171-178: LGTM!


240-245: LGTM!


247-263: LGTM!


386-426: LGTM!

installing/deps-installer/src/install/recordLockfileVerified.ts (1)

5-6: LGTM!

Also applies to: 34-34

pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs (5)

99-99: LGTM!

Also applies to: 144-147


228-249: LGTM!

Also applies to: 261-269, 271-287


367-398: LGTM!


219-224: LGTM!

Also applies to: 438-438


326-346: Ensure Rust is_valid_old_npm_package_name matches pnpm’s validForOldPackages exactly

  • pnpm’s TypeScript isValidDependencyAlias(alias) returns validateNpmPackageName(alias).validForOldPackages, and parseWantedDependency also gates alias handling on validForOldPackages.
  • push_invalid_aliases in pacquet/crates/lockfile-verification/src/verify_lockfile_resolutions.rs relies on is_valid_old_npm_package_name for the “invalid alias” decision; this must implement the same validForOldPackages rules (including rejection of reserved names like .bin / .pnpm / node_modules and path-traversal/absolute cases) so there’s no security bypass between TS and Rust.
pacquet/crates/lockfile-verification/src/record_lockfile_verified.rs (1)

25-25: LGTM!

Also applies to: 52-52

Comment thread installing/deps-installer/src/install/verifyLockfileResolutions.ts Outdated
…d comments

Move `pushInvalidAliases` below `collectCandidates`, following the
repo's declare-after-use convention. Collapse the repeated "an alias
becomes a node_modules directory, so a traversal/reserved name escapes
or overwrites layout" explanation that was copied across the verifier,
the hoisted-graph error, and the pacquet mirror down to a single
reference each — the full rationale lives once in the validating sink
(`safeJoinModulesDir` / `safe_join_modules_dir`) and the user-facing
error hints.

---
Written by an agent (Claude Code, claude-fable-5).
@qodo-free-for-open-source-projects

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

Copy link
Copy Markdown

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

@zkochan zkochan merged commit f648e9b into main Jun 12, 2026
25 checks passed
@zkochan zkochan deleted the fix/contain-hoisted-dependency-aliases branch June 12, 2026 07:47
zkochan added a commit that referenced this pull request Jun 18, 2026
…12504)

Backports three merged security fixes from main to release/10:

- Contain hoisted dependency aliases so a crafted lockfile alias cannot
  escape node_modules or overwrite pnpm-owned layout. The hoisted graph
  builder now validates each alias at the safeJoinModulesDir sink.
  (GHSA-fr4h-3cph-29xv, #12343)
- Contain pnpm patch-remove deletions to the configured patches
  directory. (#12341)
- Reject path-traversal config dependency names and versions from
  pnpm-workspace.yaml before they are used to build filesystem paths.
  (GHSA-qrv3-253h-g69c, #12470)

Written by an agent (Claude Code, claude-opus-4-8).
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.

2 participants