Conversation
…ides registry When a workspace `.npmrc` overrides `registry=` to a different value than the user's `~/.npmrc` or `~/.config/pnpm/auth.ini` would have set, do not bind unscoped/default credentials (`_authToken`, `_auth`, `username`/`_password`) from the user-level config to the workspace-selected registry. The previous behavior leaked user-trusted credentials to whatever registry an untrusted workspace `.npmrc` pointed at. Reported by JUNYI LIU.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR rescopes unscoped npm auth/TLS keys to URL-scoped form at config load time (pinned to the declaring source’s registry), removes the default-registry placeholder from auth-header construction, and updates callers and tests accordingly. ChangesCredential Rebind Defense
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…gistry Move the rebind defense to after all config layers (CLI, env vars, pnpm-workspace.yaml, .npmrc) have settled. Compare the final resolved default registry against what the user-level config alone would produce, and skip the check entirely if the user requested a registry via CLI/env themselves.
Emit a per-file warning whenever an .npmrc or auth.ini contains an unscoped auth value (_authToken, _auth, username, _password, tokenHelper). URL-scoped tokens have been npm's recommended pattern since npm@9, and unscoped credentials are slated for removal in a future major. The warning fires independently of whether the rebind defense rejects the credentials, so users see the deprecation even when their setup happens to be safe today.
…stead of detecting rebinds post-merge Each .npmrc / auth.ini / CLI source's unscoped credential keys (_authToken, _auth, username, _password, tokenHelper) are rewritten to their URL-scoped equivalent during load, using the same source's registry= value (or the npmjs default if it declares none). A later layer overriding registry= can no longer rebind a credential to its own registry — the credential is already pinned to the URL its author intended. This removes the post-merge source-tracking defense and replaces it with the simpler per-source normalization. Each rescope emits a deprecation warning so users migrate to writing the URL-scoped form directly.
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
config/reader/test/index.ts (1)
857-1040: 💤 Low valueConsider adding test coverage for
tokenHelperrescoping.The
UNSCOPED_CRED_KEYSconstant includestokenHelper, but the new test suite only exercises_authToken,_auth, andusername/_password. While the rescoping logic is shared, an explicit test would guard against regressions iftokenHelperhandling ever diverges.🤖 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 `@config/reader/test/index.ts` around lines 857 - 1040, Add a test in this suite that exercises rescoping for the tokenHelper key (which is listed in UNSCOPED_CRED_KEYS) similar to the existing tests for _authToken/_auth/username+_password: write a user-level file containing "tokenHelper=some-helper" (and optionally a workspace .npmrc with a different registry), call getConfig(...) with cliOptions: { userconfig } and env including XDG_CONFIG_HOME, then assert that config.configByUri pins the tokenHelper entry to the source file's registry (e.g. '//registry.npmjs.org/' or the userconfig's registry) and that it is not present under the workspace/CLI registry; reference the test helpers getConfig and the config.configByUri map when adding the assertion.
🤖 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 `@config/reader/test/index.ts`:
- Around line 857-1040: Add a test in this suite that exercises rescoping for
the tokenHelper key (which is listed in UNSCOPED_CRED_KEYS) similar to the
existing tests for _authToken/_auth/username+_password: write a user-level file
containing "tokenHelper=some-helper" (and optionally a workspace .npmrc with a
different registry), call getConfig(...) with cliOptions: { userconfig } and env
including XDG_CONFIG_HOME, then assert that config.configByUri pins the
tokenHelper entry to the source file's registry (e.g. '//registry.npmjs.org/' or
the userconfig's registry) and that it is not present under the workspace/CLI
registry; reference the test helpers getConfig and the config.configByUri map
when adding the assertion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 08f11bb7-d97b-429c-aacf-f7896bf38725
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
.changeset/credential-rebind-defense.mdconfig/reader/package.jsonconfig/reader/src/index.tsconfig/reader/src/loadNpmrcFiles.tsconfig/reader/test/index.tscspell.json
📜 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 (1)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}: Follow Standard Style with trailing commas, preferring functions over classes, and declaring functions after they are used (relying on hoisting)
Use a single options object instead of multiple parameters when a function needs more than two or three arguments
Follow Import Order: Standard libraries first, then external dependencies (alphabetically), then relative imports
Write self-documenting code where function names, parameters, and types explain what a function does without requiring prose comments
Do not write comments that restate what the code already says; refactor via renaming, splitting helpers, or restructuring instead
Do not repeat documentation at call sites that already exists in JSDoc on the callee; update JSDoc once for all call sites to benefit
Use JSDoc only for a function's contract (preconditions, postconditions, edge cases, why the function exists), not for re-narrating the body
Do not record past implementation shape, refactor history, or 'the previous code did X' framing in code; use git log and git blame instead
Write comments only when: the reason for code is non-obvious (hidden invariant, workaround for known bug, deliberate exception), or the right name doesn't fit (temporary technical constraint)
Files:
config/reader/src/index.tsconfig/reader/test/index.tsconfig/reader/src/loadNpmrcFiles.ts
🧠 Learnings (2)
📚 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:
config/reader/package.json
📚 Learning: 2026-05-14T09:04:00.133Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11622
File: resolving/npm-resolver/test/publishedBy.test.ts:350-354
Timestamp: 2026-05-14T09:04:00.133Z
Learning: In the pnpm/pnpm repository, ESLint is the authoritative style linter. Do not raise review findings for missing trailing commas in multiline function calls (e.g., `fs.writeFileSync(...)`) when this repo’s ESLint configuration does not report them and lint passes. Prefer deferring to the ESLint results for this specific trailing-comma rule rather than enforcing it manually in code review.
Applied to files:
config/reader/src/index.tsconfig/reader/test/index.tsconfig/reader/src/loadNpmrcFiles.ts
🔇 Additional comments (10)
.changeset/credential-rebind-defense.md (1)
1-11: LGTM!config/reader/package.json (1)
40-40: LGTM!cspell.json (1)
145-145: LGTM!Also applies to: 289-291
config/reader/src/loadNpmrcFiles.ts (4)
6-7: LGTM!Also applies to: 75-78, 132-139, 192-215
141-178: LGTM!
93-99: LGTM!
103-120: LGTM!config/reader/src/index.ts (1)
41-41: LGTM!Also applies to: 324-325
config/reader/test/index.ts (2)
857-1040: LGTM!
1042-1134: LGTM!
After load-time rescoping, no source can populate configByUri[''] — every credential is either URL-scoped from the start or rewritten to the URL-scoped form during the .npmrc / auth.ini / CLI parse. The runtime fallback that re-keyed configByUri[''] onto the merged default registry, and the publish-side fallback that read it, are both dead code. Removed: - empty-string handling in getAuthHeadersFromCreds, including its defaultRegistry parameter - defaultRegistry parameter from createGetAuthHeaderByURI - the corresponding dedicated unit test - the configByUri['']?.creds fallback in publishPackedPkg.ts - empty-key assertions in config/reader tests Updated all ~16 call sites of createGetAuthHeaderByURI to drop the now unused second argument.
The same trust-boundary issue that affected unscoped credentials applies to client TLS settings: an unscoped cert=/key= would be presented to whatever registry the merged config settles on, even if a later layer (workspace .npmrc, pnpm-workspace.yaml, CLI flag) overrode it. The existing rescope helper now also rewrites unscoped `cert` and `key` to their URL-scoped form, pinning them to the registry their author named in the same source. `ca`/`cafile` are intentionally left unscoped: they're trust anchors, not credentials, and corporate MITM-proxy setups depend on them applying to every HTTPS request. The default-registry override can't weaponize an unscoped CA — the attacker would need a cert signed by it. `certfile`/`keyfile` (file-path variants) are not rescoped either: `certfile` isn't read unscoped by pnpm today (asymmetric vs. `keyfile` in NPM_AUTH_SETTINGS), and supporting only one of them would be confusing. Users wanting the path form can write it URL-scoped directly.
`keyfile` was listed in NPM_AUTH_SETTINGS so unscoped `keyfile=<path>` passed the .npmrc filter and ended up in authConfig — but nothing in the codebase ever read it from there. The dispatcher uses `opts.key` (inline PEM) and `configByUri[host].tls.key` (URL-scoped path/inline content), neither of which is populated from unscoped `keyfile=`. `certfile` was already absent from the allowlist for the same reason, so this also removes the asymmetry between the two file-path variants. URL-scoped `//host/:certfile=...` and `//host/:keyfile=...` continue to work via `tryParseSslKey` and are unaffected.
This test exercised the configByUri[''] re-keying path that was removed in the rescope-at-load refactor. With createGetAuthHeaderByURI no longer accepting a defaultRegistry parameter and unscoped credentials no longer reaching the merged config, the scenario the test described is structurally unreachable.
Two CI fixes:
1. When a source's `registry=` resolves to an empty string (e.g. an
unresolved `${ENV_VAR}` placeholder), `new URL(...)` inside
`nerfDart` throws. Guard the call with try/catch: drop the
unscoped per-registry keys (a bare token has nowhere safe to bind)
and emit a warning naming the offending source.
2. Update `.npmrc does not load pnpm settings` to expect the rescoped
form of unscoped `_authToken`/`username` in `authConfig` — they
now appear as `//registry.npmjs.org/:_authToken` etc. since the
test's .npmrc declares no `registry=` of its own.
Integrated-Benchmark Report (Linux)Scenario: Isolated linker: fresh restore, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.6084010764200003,
"stddev": 0.031025033475340257,
"median": 1.5955710108199999,
"user": 2.9654172,
"system": 2.01188738,
"min": 1.56159312632,
"max": 1.6595527803199999,
"times": [
1.5971078373199998,
1.64328949132,
1.5940341843199999,
1.6343201513199999,
1.58526918132,
1.59353623832,
1.56159312632,
1.6595527803199999,
1.62857497632,
1.5867327973199998
]
},
{
"command": "pacquet@main",
"mean": 1.63145189142,
"stddev": 0.07068976166748245,
"median": 1.64684293232,
"user": 2.9556261,
"system": 2.06965588,
"min": 1.54242822832,
"max": 1.7622597623199998,
"times": [
1.6612679153199998,
1.64106132132,
1.7622597623199998,
1.66177972032,
1.54242822832,
1.6956319573199998,
1.5690323093199998,
1.54791296832,
1.65262454332,
1.58052018832
]
}
]
}Scenario: Isolated linker: fresh restore, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 0.46941238502000004,
"stddev": 0.03741102128496132,
"median": 0.45963804302000005,
"user": 0.3507476,
"system": 0.79815152,
"min": 0.44723359652000005,
"max": 0.57462404852,
"times": [
0.57462404852,
0.46478847252000005,
0.46244865252000006,
0.45426634252000003,
0.46214840452000006,
0.45712768152000005,
0.4644066975200001,
0.45231765252000006,
0.44723359652000005,
0.4547623015200001
]
},
{
"command": "pacquet@main",
"mean": 0.46810197662,
"stddev": 0.015866199284764945,
"median": 0.46088623452000005,
"user": 0.35844510000000007,
"system": 0.79511082,
"min": 0.45329342352000007,
"max": 0.5031256875200001,
"times": [
0.5031256875200001,
0.47451144052000005,
0.46182957052000007,
0.46853994952000005,
0.45329342352000007,
0.45982710252000003,
0.4866109035200001,
0.45678008752000004,
0.45655870252,
0.45994289852000003
]
}
]
}Scenario: Isolated linker: fresh install, cold cache + cold store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 2.01052426682,
"stddev": 0.02231347543830063,
"median": 2.01052034642,
"user": 4.1016899,
"system": 1.93856322,
"min": 1.97670875242,
"max": 2.0410153744199997,
"times": [
1.99174447542,
2.01162176442,
1.99690369242,
2.03532871742,
1.98577812742,
1.97670875242,
2.02963362642,
2.0270892094199997,
2.0410153744199997,
2.0094189284199997
]
},
{
"command": "pacquet@main",
"mean": 2.0066116558199996,
"stddev": 0.02891035265400616,
"median": 2.01118514942,
"user": 4.111163500000001,
"system": 1.9431165199999998,
"min": 1.94529370442,
"max": 2.05611220042,
"times": [
1.9781146194199999,
2.01887113142,
2.0189433174199998,
2.00242406742,
2.05611220042,
1.94529370442,
2.0176435434199997,
2.01405670742,
2.0063436754199997,
2.00831359142
]
}
]
}Scenario: Isolated linker: fresh install, hot cache + hot store
BENCHMARK_REPORT.json{
"results": [
{
"command": "pacquet@HEAD",
"mean": 1.21464340682,
"stddev": 0.010889947712912233,
"median": 1.21340820032,
"user": 1.6863376999999997,
"system": 1.14733874,
"min": 1.19925392982,
"max": 1.23096962582,
"times": [
1.23096962582,
1.22820195682,
1.20424958182,
1.20164842282,
1.22074620182,
1.21272051682,
1.21236884582,
1.21409588382,
1.22217910282,
1.19925392982
]
},
{
"command": "pacquet@main",
"mean": 1.2080053690199999,
"stddev": 0.032468360693195364,
"median": 1.20455907932,
"user": 1.6835093,
"system": 1.1152469399999998,
"min": 1.18076938882,
"max": 1.28914055682,
"times": [
1.2018738068200001,
1.18156219382,
1.18076938882,
1.20724435182,
1.18463397782,
1.18320198282,
1.21591042982,
1.21979856382,
1.28914055682,
1.21591843782
]
}
]
} |
|
| Branch | pr/11953 |
| Testbed | pacquet |
Click to view all benchmark results
| Benchmark | Latency | Benchmark Result milliseconds (ms) (Result Δ%) | Upper Boundary milliseconds (ms) (Limit %) |
|---|---|---|---|
| isolated-linker.fresh-install.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 2,010.52 ms(-37.88%)Baseline: 3,236.37 ms | 3,883.65 ms (51.77%) |
| isolated-linker.fresh-install.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 1,214.64 ms(-47.51%)Baseline: 2,313.90 ms | 2,776.68 ms (43.74%) |
| isolated-linker.fresh-restore.cold-cache.cold-store | 📈 view plot 🚷 view threshold | 1,608.40 ms(-24.43%)Baseline: 2,128.46 ms | 2,554.15 ms (62.97%) |
| isolated-linker.fresh-restore.hot-cache.hot-store | 📈 view plot 🚷 view threshold | 469.41 ms(-28.90%)Baseline: 660.23 ms | 792.28 ms (59.25%) |
This test passed credentials via the configByUri[''] empty-string slot, which the auth-header layer re-keyed to the merged default registry at request time. That slot was removed in the rescope-at-load refactor — credentials are now always URL-scoped before they reach configByUri, so the empty-key entry is unreachable from any code path. The scenario the test covered (basicAuth via username/password) is already exercised by the existing "installing a package that need authentication, using password" test using the URL-scoped form.
Removing the unscoped credential fallback in getAuthHeadersFromConfig is a breaking change to the package's contract — callers passing unscoped _authToken/_auth/username+_password/tokenHelper alongside a default registry no longer get a header back. Matches the major bump in #11953. --- Written by an agent (Claude Code, claude-opus-4-7).
…istry at load time Each .npmrc / auth.ini / CLI source's unscoped per-registry settings (_authToken, _auth, username/_password, tokenHelper, inline cert/key) are rewritten to their URL-scoped equivalent during load, using the same source's registry= value (or the npmjs default if none). After this rewrite the merged config contains only URL-scoped settings, so a later layer overriding registry= (workspace .npmrc, pnpm-workspace.yaml, CLI --registry) cannot rebind a credential or client certificate to a different host. Each rescope emits a deprecation warning naming the source and the URL the setting was pinned to. ca/cafile are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally. Ported from #11953. Reported by JUNYI LIU. --- Written by an agent (Claude Code, claude-opus-4-7).
Removing the unscoped credential fallback in getAuthHeadersFromConfig is a breaking change to the package's contract — callers passing unscoped _authToken/_auth/username+_password/tokenHelper alongside a default registry no longer get a header back. Matches the major bump in #11953. --- Written by an agent (Claude Code, claude-opus-4-7).
…istry at load time (backport #11953 to v10) (#11986) * fix(config): pin unscoped per-registry settings to their source's registry at load time Each .npmrc / auth.ini / CLI source's unscoped per-registry settings (_authToken, _auth, username/_password, tokenHelper, inline cert/key) are rewritten to their URL-scoped equivalent during load, using the same source's registry= value (or the npmjs default if none). After this rewrite the merged config contains only URL-scoped settings, so a later layer overriding registry= (workspace .npmrc, pnpm-workspace.yaml, CLI --registry) cannot rebind a credential or client certificate to a different host. Each rescope emits a deprecation warning naming the source and the URL the setting was pinned to. ca/cafile are intentionally not rescoped — they're trust anchors, not credentials, and corporate MITM-proxy setups rely on them applying globally. Ported from #11953. Reported by JUNYI LIU. --- Written by an agent (Claude Code, claude-opus-4-7). * chore(changeset): bump @pnpm/network.auth-header to major Removing the unscoped credential fallback in getAuthHeadersFromConfig is a breaking change to the package's contract — callers passing unscoped _authToken/_auth/username+_password/tokenHelper alongside a default registry no longer get a header back. Matches the major bump in #11953. --- Written by an agent (Claude Code, claude-opus-4-7).
Summary
Each
.npmrc,auth.ini, and CLI source's unscoped per-registry settings (_authToken,_auth,username,_password,tokenHelper,cert,key) are rewritten to their URL-scoped equivalent during load, using the same source'sregistry=value — or the npmjs default if the source declares none. After this rewrite the merged config contains only URL-scoped settings, so a later layer overridingregistry=cannot rebind a credential or a client certificate to its own registry.Every rescope emits a deprecation warning telling the user where the setting was pinned and how to write it directly:
npm has rejected unscoped credentials outright since
npm@9(ERR_INVALID_AUTH); pnpm will catch up in a future major.Threat the change closes
A project-local
.npmrc(orpnpm-workspace.yaml, or a CLI flag) that overridesregistry=would cause pnpm to send user-trusted credentials (Authorization: Bearer <user-secret>) or a client TLS cert/key to the new registry, even though the user had written them into~/.npmrc(or~/.config/pnpm/auth.ini) for a different destination.With this PR, those user-level settings are pinned at load time to whichever registry the file named (or to the npmjs default if it named none). The workspace's
registry=override changes where the merged registry resolves, but does not move the setting.What gets rescoped
_authToken,_auth,username,_password,tokenHelpercert,key(inline PEM)ca,cafilecertfile,keyfile(file-path variants)certfileisn't read unscoped today; supporting onlykeyfilewould be asymmetric. Users wanting the path form can write//host/:certfile=...directlystrict-sslWhy this shape
A prior version of this fix lived post-merge in
getConfigand used a source-tracking helper (workspaceRegistryRebindsUserCreds) to detect when a workspace override would rebind user-level credentials, then dropped the binding. That worked but carried two complications: the helper had to reason about three source maps (userConfig,pnpmAuthConfig,workspaceNpmrc), and it had to honor anexplicitlySetKeys.has('registry')carve-out so the CLI's--registrycould still pull an unscoped token along.The load-time rescope is simpler:
registry=value (or fall back to npmjs default), rewrite its unscoped per-registry keys, done.getConfigno longer needs the helper at all —getNetworkConfigsnaturally picks up the URL-scoped form.pnpm install --registry=Ywhile~/.npmrchas an unscoped_authToken=T(noregistry=), T is pinned to the npmjs default at load and is NOT sent to Y. Per the discussion on this PR, this is the correct behavior, not a regression: an unscoped credential is ambient; it shouldn't follow whatever registry the CLI happens to point at. Users who want T sent to Y should write//Y/:_authToken=T.Follow-on cleanup: the empty-string
configByUri['']placeholder slot (the previous "default registry" re-keying target) is now structurally unreachable, so the runtime fallback ingetAuthHeadersFromCredsandpublishPackedPkg.tsis removed, along with the now-unuseddefaultRegistryparameter oncreateGetAuthHeaderByURIandgetAuthHeadersFromCreds. Bumps@pnpm/network.auth-headermajor.Implementation
config/reader/src/loadNpmrcFiles.ts:rescopeUnscopedCreds(source, sourceLabel, warnings)helper. For each unscoped key insource, it computes the nerf-darted form ofsource.registry(ornpmDefaults.registry), copies the value to<nerf>:<key>unless an explicit scoped value is already there, deletes the unscoped key, and emits a deprecation warning naming the source.readAndFilterNpmrcruns the rescope on every file (workspace.npmrc, user.npmrc,auth.ini,pnpmrcbuiltin).opts.cliOptionsis cloned and rescoped before it joins the merge.@pnpm/config.nerf-dart.config/reader/src/index.ts:workspaceRegistryRebindsUserCreds,UNSCOPED_CRED_KEYS, and thegetDefaultCredscall.network/auth-header/src/getAuthHeadersFromConfig.tsandnetwork/auth-header/src/index.ts:defaultRegistryparameter.releasing/commands/src/publish/publishPackedPkg.ts: removed thecreds ??= configByUri['']?.credsfallback.All ~16 call sites of
createGetAuthHeaderByURIupdated to drop the now-unused second argument.Pacquet
Pacquet has the same logical sink but only reads one
.npmrcat a time (no merge), so it isn't vulnerable today. The day pacquet adds layered.npmrcsupport, the equivalent load-time rescope should land in the same change. Tracked in #11950.Test plan
main(pnpm v11.3.0): 3 attacker auth hits._authToken+ same-fileregistry=trusted, no workspace .npmrc → token sent to trusted..npmrcre-declares same registry → token sent to that registry.registry+_authToken→ workspace token sent to workspace registry.//host/:_authToken=passes through unchanged with no deprecation warning.cafile=/path/to/corp-ca.pemstill applies globally (CA is not rescoped).config/reader/test/index.ts:--registry), and the inline cert/key case.auth-headertests pass (20/20 — dropped the dedicated empty-key test).Behavior changes (intentional)
Two cases that previously bound an unscoped user-level credential to a lower-trust registry now don't. Both are documented in the deprecation warning; users get a concrete suggestion on how to write the credential URL-scoped.
~/.npmrchasregistry=trustedonly;auth.inihas unscoped_authToken=Ttrusted(merged registry from~/.npmrc)auth.iniload; not sent totrusted~/.npmrchas unscoped_authToken=Tonly; CLI--registry=YThe fix path is the same for both: write the credential URL-scoped (
//that-registry/:_authToken=T) in either of the user-level files.Note on disclosure
This is a public draft because the reporter's repro and the fix are already shareable. Six unrelated tests in
config/reader/test/index.tsfail identically onmainwithout this PR — they appear to leak the maintainer's real~/.config/pnpm/auth.iniinto URL-scoped-token assertions; not addressed here.Written by an agent (Claude Code, claude-opus-4-7).
Summary by CodeRabbit
Bug Fixes
Tests
Documentation