Skip to content

fix(config/reader): resolve relative cafile path against the .npmrc directory#11726

Merged
zkochan merged 4 commits into
pnpm:mainfrom
shiminshen:fix/cafile-relative-path-from-cwd
May 19, 2026
Merged

fix(config/reader): resolve relative cafile path against the .npmrc directory#11726
zkochan merged 4 commits into
pnpm:mainfrom
shiminshen:fix/cafile-relative-path-from-cwd

Conversation

@shiminshen

@shiminshen shiminshen commented May 18, 2026

Copy link
Copy Markdown
Contributor

What

cafile=<relative-path> in .npmrc is read via fs.readFileSync, which resolves relative paths against process.cwd(). When pnpm is invoked from a different cwd than the project — pnpm --dir <project> install in CI wrappers, monorepo scripts, npx-style invocations — the CA file silently fails to load: the try/catch in the loader drops the CA list, the install proceeds without the configured CA, and the user only sees TLS errors against a private registry, with no log line tying back to the wrong path.

This PR resolves relative cafile= values against path.dirname(<the .npmrc that declared the key>), mirroring the user-facing intent of "this CA file lives next to the project's .npmrc."

Closes #11624.

Why

  • The user-facing intent of "put a cafile in the project's .npmrc" is "this CA applies to this project". Resolving against whatever cwd happens to be active when pnpm runs breaks that intent the moment --dir is used.
  • The failure is silent — the try/catch drops the CA list to undefined, the install continues, and the user only notices when TLS errors surface against a registry the system trust store happens not to cover.

How

In readAndFilterNpmrc (config/reader/src/loadNpmrcFiles.ts), after env-var substitution but before keys are written into the layer dictionary, normalize a relative cafile= to path.resolve(npmrcDir, value) where npmrcDir = path.dirname(filePath). The cafile then flows through loadCAFile as an absolute path, and fs.readFileSync resolves it correctly regardless of the active cwd.

The fix only applies to values that come from a .npmrc (workspace, user, pnpm auth). CLI --cafile and programmatic defaults are untouched.

Compatibility

  • Users currently using absolute cafile= paths (the dominant CI shape) — no change.
  • Users who happen to cd into the project before running pnpm — no change (cwd == npmrcDir, so resolution was identical to the new behavior).
  • Users running pnpm --dir /other install from elsewhere with a relative cafile= — they currently see a silent CA-drop, and now see a correctly resolved path. Strictly an improvement.

Test plan

  • New test getConfig() resolves a relative cafile= from .npmrc against the npmrc directory, not process.cwd() in config/reader/test/index.ts — sets up a project with .npmrc + certs/ca.pem, runs getConfig({ cliOptions: { dir: projectDir } }) from a different cwd, asserts config.ca is loaded.
  • Existing getConfig() should read cafile (absolute path) still passes — same code path is unchanged for absolute paths.
  • Manual repro on a fresh project mirroring the issue:
    • Before: config.ca === undefined
    • After: config.ca is the parsed CA bundle.
  • Changeset added.

Notes

This contribution was prepared with the assistance of Claude Code.

Summary by CodeRabbit

  • Bug Fixes
    • Relative cafile paths in .npmrc are now resolved against the .npmrc’s directory so configured CA files load correctly when running from a different working directory.
  • Tests
    • Added a regression test verifying relative cafile resolution uses the .npmrc location.
  • Documentation
    • Updated release metadata and docs to reflect the new cafile resolution behavior.

Review Change Stack

@shiminshen shiminshen requested a review from zkochan as a code owner May 18, 2026 17:07
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

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: 1e0084c4-da65-4a5c-a2bf-ed8ebe76a3c3

📥 Commits

Reviewing files that changed from the base of the PR and between bc07244 and 7f1a3b0.

📒 Files selected for processing (1)
  • pacquet/crates/config/src/npmrc_auth/tests.rs
📜 Recent 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). (9)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Doc
  • GitHub Check: Format
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Run benchmark on ubuntu-latest
  • GitHub Check: Compile & Lint
  • GitHub Check: Code Coverage
🧰 Additional context used
📓 Path-based instructions (1)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: When porting a function that fires pnpm:<channel> events through globalLogger, logger.debug(), or streamParser.write(), mirror the call site, payload, and ordering so the reporter parses pacquet's NDJSON the same way it parses pnpm's.
Declare a newtype wrapper for branded string types. Do not collapse the brand into a plain String or &str.
If upstream always validates before construction, validate in pacquet's wrapper too. The wrapper must construct only via TryFrom<String> and/or FromStr. Do not provide an infallible public constructor.
If upstream never validates, just brand for type-safety. Expose an infallible From<String> (and From<&str> when convenient).
If upstream occasionally constructs without validation, expose from_str_unchecked as an escape hatch alongside the validating constructor.
Match upstream serde behavior for branded types that cross JSON, YAML, or INI boundaries. Use #[serde(try_from = "String")] for deserialization and #[serde(into = "String")] for serialization.
Use #[derive(derive_more::From)] and #[derive(derive_more::Into)] for mechanical conversion impls. Fall back to manual impl only when conversion needs custom logic.
String-literal unions should become enums, not newtype wrappers. Model closed sets of valid string values as enums.
Template literal types should be treated as branded strings with validation discipline from rules 2-5.
Choose owned vs. borrowed parameters to minimize copies. Widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming conventions.
Do not use star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;. Two forms stay allowed: external-crate preludes like use rayon::prelude::*; and root-of-module re-...

Files:

  • pacquet/crates/config/src/npmrc_auth/tests.rs
🔇 Additional comments (3)
pacquet/crates/config/src/npmrc_auth/tests.rs (3)

1-2: LGTM!


40-40: LGTM!

Also applies to: 51-52, 82-82, 98-98, 105-105, 112-112, 124-124, 144-144, 157-157, 179-179, 196-196, 208-208, 223-223, 238-238, 251-251, 264-264, 278-278, 292-292, 307-307, 319-319, 338-338, 372-373, 379-380, 386-386, 395-398, 401-404, 411-411, 419-421, 437-438, 446-446, 454-455, 488-490, 537-537, 543-543, 600-608, 626-626, 779-781, 795-795, 803-806, 821-821, 840-840, 857-857, 867-867, 875-876, 891-891


547-589: LGTM!


📝 Walkthrough

Walkthrough

Resolve relative cafile entries against the directory containing the .npmrc that declared them; add a regression test for --dir usage, update pacquet Rust parsing to accept the npmrc directory, and update tests and a changeset documenting the fix.

Changes

CA File Path Resolution Fix

Layer / File(s) Summary
Cafile reader, regression test, and changeset
config/reader/src/loadNpmrcFiles.ts, config/reader/test/index.ts, .changeset/cafile-resolve-against-npmrc-dir.md
readAndFilterNpmrc computes the .npmrc directory and resolves non-empty, non-absolute cafile values against it. Adds a regression test verifying getConfig() loads the PEM when cliOptions.dir points to the project, and a changeset documenting the patch.
Rust: propagate npmrc source dir into Config::current
pacquet/crates/config/src/lib.rs
Config::current now tracks the directory the .npmrc text was read from and supplies that directory when constructing NpmrcAuth from ini text.
Rust: NpmrcAuth parses cafile relative to npmrc_dir
pacquet/crates/config/src/npmrc_auth.rs
NpmrcAuth::from_ini gains an npmrc_dir: &Path parameter; cafile values are resolved via resolve_cafile, turning relative paths into paths anchored to the .npmrc directory; docs/comments updated accordingly.
Rust tests: update calls to new from_ini signature
pacquet/crates/config/src/npmrc_auth/tests.rs
All tests updated to pass an explicit base Path to NpmrcAuth::from_ini, including TLS/cafile regression tests that exercise relative-to-tempdir resolution.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as pnpm CLI
  participant Reader as loadNpmrcFiles
  participant FS as Filesystem
  participant Pacquet as NpmrcAuth::from_ini

  CLI->>Reader: read .npmrc (with path)
  Reader->>Reader: derive npmrcDir and parse cafile value
  Reader->>FS: fs.readFileSync(resolvedCafilePath)
  CLI->>Pacquet: Config::current passes (npmrc_text, npmrcDir)
  Pacquet->>Pacquet: resolve_cafile(value, npmrcDir)
  Pacquet->>FS: fs.readFileSync(resolved_cafile) (when applying TLS)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • zkochan

Poem

🐰 I hopped through paths both near and far,
A cafile lost when cwd misled the star,
Now each .npmrc points to its own gate,
The PEM is found — no more silent fate,
Hooray! My hops fixed the CA file debate!

🚥 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 describes the main change: resolving relative cafile paths against the .npmrc directory instead of process.cwd().
Linked Issues check ✅ Passed All coding objectives from issue #11624 are met: relative cafile paths are resolved against npmrcDir in both JS and Rust implementations, with comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the cafile path resolution fix across both pnpm and pacquet implementations, with no extraneous modifications.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot added area: cli/dlx area: supply chain security Issues related to minimumReleaseAge, blockExoticSubdeps, build script safety, and trust policies. labels May 18, 2026
…irectory

`cafile=<relative-path>` in `.npmrc` was being read via `fs.readFileSync`,
which resolves relative paths against `process.cwd()`. When pnpm is invoked
from a different cwd than the project (e.g. `pnpm --dir <project> install`
in CI wrappers and monorepo scripts), the CA file silently failed to load:
the `try/catch` in the loader dropped the CA list, the install proceeded
without the configured CA, and the user only saw TLS errors against a
private registry — with no log line tying back to the wrong path.

Resolve relative `cafile` values in `readAndFilterNpmrc` against
`path.dirname(filePath)` of the .npmrc that declared the key, before
`loadCAFile` reads the file. Absolute paths (the dominant CI shape) and
CLI `--cafile` are unchanged.

Ref: pnpm#11624
@zkochan zkochan force-pushed the fix/cafile-relative-path-from-cwd branch from e2daf25 to 3b88a15 Compare May 19, 2026 21:15
zkochan added 2 commits May 19, 2026 23:42
The test name and the linked issue already describe the failure mode,
so the 4-line preamble on the test and the 5-line in-line comment on
the implementation were re-narrating what the tests document.

---
Written by an agent (Claude Code, claude-opus-4-7).
…directory

Pacquet's `NpmrcAuth::from_ini` used to store the `cafile=` value
verbatim and pass it to `std::fs::read_to_string` at apply time. A
relative path therefore resolved against the process cwd, so a
project `.npmrc` containing `cafile=certs/ca.pem` reached via
`pacquet --dir <proj>` from a different cwd silently failed to load
the CA — same failure mode as pnpm#11624 on the TypeScript side,
which the parent commit fixed by resolving against `path.dirname` of
the `.npmrc`.

Mirrors the parent commit on the pacquet side:

- `NpmrcAuth::from_ini` now takes the directory the `.npmrc` was
  loaded from. A relative non-empty `cafile=` value is resolved
  against that directory via `npmrc_dir.join(...)`; empty and
  absolute values pass through unchanged.

- `Config::current` tracks which of `start_dir` / home dir actually
  provided the `.npmrc` text and passes that path through.

- The `load_cafile` doc comment that documented "matches pnpm's
  surprising cwd-resolution behavior" is gone; that caveat was
  current only as long as pnpm itself had the bug.

- Existing tests updated mechanically to pass `Path::new("")` for
  the new parameter; four new tests cover the resolution branches
  (relative resolves, absolute passes through, empty passes through,
  end-to-end load via `apply_to` with a real tempdir-based fixture).

---
Written by an agent (Claude Code, claude-opus-4-7).
@codecov-commenter

codecov-commenter commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.06%. Comparing base (0a40f64) to head (7f1a3b0).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11726      +/-   ##
==========================================
- Coverage   90.06%   90.06%   -0.01%     
==========================================
  Files         146      146              
  Lines       16795    16803       +8     
==========================================
+ Hits        15127    15134       +7     
- Misses       1668     1669       +1     

☔ View full report in Codecov by Sentry.
📢 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.

… macros

`perfectionist::macro-trailing-comma` is enforced via dylint at CI and
ran clean before the cafile port. Rustfmt reflowed two `assert_eq!`
calls in `parses_strict_ssl_true_and_false` onto multiple lines when
the `Path::new("")` argument made the line too long, but did not add
the trailing comma the dylint rule wants on the last macro argument.

---
Written by an agent (Claude Code, claude-opus-4-7).
@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Frozen Lockfile

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.517 ± 0.059 2.430 2.618 1.01 ± 0.04
pacquet@main 2.504 ± 0.085 2.391 2.696 1.00
pnpm 5.055 ± 0.068 4.926 5.141 2.02 ± 0.07
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.51693527764,
      "stddev": 0.0589456852520619,
      "median": 2.50442244204,
      "user": 2.7462222599999997,
      "system": 3.8450544399999997,
      "min": 2.43045235254,
      "max": 2.61767438754,
      "times": [
        2.58255876954,
        2.56465115454,
        2.45726348054,
        2.43045235254,
        2.61767438754,
        2.54353133654,
        2.48073083154,
        2.4989338015399998,
        2.48364557954,
        2.50991108254
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.50403080174,
      "stddev": 0.08460281975704594,
      "median": 2.51189908904,
      "user": 2.6943637599999994,
      "system": 3.85162824,
      "min": 2.39061941954,
      "max": 2.69623114254,
      "times": [
        2.39061941954,
        2.41583062754,
        2.54422433454,
        2.44071096154,
        2.52180610654,
        2.69623114254,
        2.51031847454,
        2.51347970354,
        2.48397099354,
        2.52311625354
      ]
    },
    {
      "command": "pnpm",
      "mean": 5.05457030044,
      "stddev": 0.06828588447488329,
      "median": 5.06184074554,
      "user": 8.494114559999998,
      "system": 4.41398324,
      "min": 4.92567753654,
      "max": 5.14092633654,
      "times": [
        5.12317257254,
        4.97976995954,
        5.07647655254,
        5.10760079354,
        4.92567753654,
        5.02870536154,
        5.10024207554,
        5.04720493854,
        5.01592687754,
        5.14092633654
      ]
    }
  ]
}

Scenario: Frozen Lockfile (Hot Cache)

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 699.0 ± 31.4 680.9 786.8 1.00
pacquet@main 707.4 ± 29.1 674.0 770.5 1.01 ± 0.06
pnpm 2768.3 ± 35.6 2729.6 2844.1 3.96 ± 0.18
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.69900789488,
      "stddev": 0.03136649566566289,
      "median": 0.69039771468,
      "user": 0.38746904,
      "system": 1.5928463000000002,
      "min": 0.68088532718,
      "max": 0.7868289821800001,
      "times": [
        0.7868289821800001,
        0.6905553481800001,
        0.6996092191800001,
        0.68475510918,
        0.68088532718,
        0.69024008118,
        0.69077926818,
        0.68686860118,
        0.6959624791800001,
        0.6835945331800001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.7073622777799999,
      "stddev": 0.02913576233664158,
      "median": 0.7023208446800001,
      "user": 0.40391554,
      "system": 1.5957576,
      "min": 0.67404746518,
      "max": 0.7705408811800001,
      "times": [
        0.7365412571800001,
        0.67404746518,
        0.7214390111800001,
        0.7064052231800001,
        0.6892322071800001,
        0.6769378421800001,
        0.7705408811800001,
        0.7021935521800001,
        0.7024481371800001,
        0.69383720118
      ]
    },
    {
      "command": "pnpm",
      "mean": 2.76825257408,
      "stddev": 0.0355846354184123,
      "median": 2.76626992868,
      "user": 3.361547339999999,
      "system": 2.3225587999999995,
      "min": 2.72956502518,
      "max": 2.84407677018,
      "times": [
        2.7887047581799997,
        2.7914647921799998,
        2.84407677018,
        2.72956502518,
        2.7817023331799997,
        2.73786962818,
        2.73707630418,
        2.77819094018,
        2.73952627218,
        2.7543489171799997
      ]
    }
  ]
}

@zkochan zkochan merged commit 3687b0e into pnpm:main May 19, 2026
22 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli/dlx area: supply chain security Issues related to minimumReleaseAge, blockExoticSubdeps, build script safety, and trust policies.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cafile relative paths resolve against process cwd, not the .npmrc directory

3 participants