Skip to content

fix(config): normalize registry URL trailing slash in nerfDart to prevent scope key mismatch#9555

Open
heunghingwan wants to merge 2 commits into
npm:latestfrom
heunghingwan:latest
Open

fix(config): normalize registry URL trailing slash in nerfDart to prevent scope key mismatch#9555
heunghingwan wants to merge 2 commits into
npm:latestfrom
heunghingwan:latest

Conversation

@heunghingwan

Copy link
Copy Markdown

Problem

nerfDart() in @npmcli/config uses new URL('.', url) to compute a scope key (e.g. //host/path/) for credential lookup. URL resolution treats a path without a trailing slash as a file, so the last segment is discarded:

Input nerfDart output
https://host/npm/ //host/npm/
https://host/npm //host/ (segment lost)

getCredentialsByURI only checks this single computed key with no backtracking. This means a registry URL with a path component (e.g. https://host/npm) produces the scope key //host/, which collapses auth to the entire host and can collide with other registries on the same host at different paths.

Meanwhile, npm-registry-fetch (the HTTP layer) uses its own path-backtracking logic and handles this correctly.

Fix

Introduce nerfDartURI() — a wrapper around nerfDart() that ensures the URL pathname always ends with / before computing the scope key. Since all callers (getCredentialsByURI, setCredentialsByURI, clearCredentialsByURI, and the auth validation in validate) receive registry base URLs (not package/API URLs), this normalization is safe.

Updated all 4 nerfDart() call sites in workspaces/config/lib/index.js to use nerfDartURI().

Before vs After

nerfDart("https://host/npm")  →  //host/      (wrong — loses path)
nerfDartURI("https://host/npm") → //host/npm/  (correct)

Both https://host/npm/ and https://host/npm now produce the same key //host/npm/.

Impact

  • Existing .npmrc files with auth stored at an incorrect collapsed scope (//host/:_authToken instead of //host/path/:_authToken) will need npm login to regenerate the key at the correct scope.
  • This is a bug fix — the old behavior was overly broad in its scope matching.

@heunghingwan heunghingwan requested review from a team as code owners June 14, 2026 17:51

@owlstronaut owlstronaut left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice work, this fixes the real scope-collapse bug and I confirmed the path now survives nerfing. Two things give me pause though: since getCredentialsByURI and logout's getAuth only check the single normalized key without backtracking, a registry configured without a trailing slash now breaks npm logout (it can't find //host/npm/, throws ENEEDAUTH, and leaves the token in .npmrc), and existing users whose tokens are stored at the old //host/:_authToken will start getting ENEEDAUTH on publish/whoami even though the wire-level auth still works. I think normalizing the slash in logout.js before getAuth, and giving getCredentialsByURI the same backtracking that npm-registry-fetch already does, would close both gaps. Could you also add a test or two for the new nerfDartURI behavior?

…alize logout registry

- Add #findAuthNerf to getCredentialsByURI, walking up the nerf dart path
  like npm-registry-fetch's regFromURI so legacy credentials stored at a
  less specific key (e.g. //host/ for a registry configured as
  https://host/npm) are still resolved after trailing-slash normalization.
- Normalize the trailing slash on the registry URL in logout before getAuth,
  npmFetch, and clearCredentialsByURI so logout finds the token at
  //host/npm/ instead of throwing ENEEDAUTH and leaving it in .npmrc.
- Export nerfDartURI from @npmcli/config and add tests for its trailing-slash
  behavior, getCredentialsByURI backtracking, and the logout no-slash case.
@heunghingwan

Copy link
Copy Markdown
Author

Thanks for the thorough review @owlstronaut — both gaps were real, and reproducing the logout case confirmed the token-stranding behavior you described. Pushed an update (357ae2c) addressing all three points:

1. getCredentialsByURI backtracking
Added a private #findAuthNerf(nerfed) that walks up the nerf dart path one segment at a time (stopping at //host) and returns the deepest key holding any usable auth — token, username+_password, or _auth. getCredentialsByURI then extracts against that key, mirroring regFromURI in npm-registry-fetch. So a token written at the legacy //host/ key (by pre-normalization npm, for a registry configured as https://host/npm) is now resolved when reading https://host/npm, which nerfs to //host/npm/. Email and certfile/keyfile still look up against the exact nerfed key to preserve the existing additive behavior and snapshots.

2. logout.js normalization
Added a small withTrailingSlash helper and run reg through it before getAuth, npmFetch's registry option, and clearCredentialsByURI. Since getAuth's regFromURI backtracks but doesn't normalize, the trailing slash now matches the key config writes to — npm logout finds the token, hits the DELETE endpoint, and clears .npmrc instead of throwing ENEEDAUTH.

3. Tests for nerfDartURI
Exported nerfDartURI from @npmcli/config and added direct unit tests covering the with/without-trailing-slash cases. Also added a getCredentialsByURI backtracking test (exact key wins, single-level walk-up to //host/, multi-level walk-up, empty result) and a logout test for the no-trailing-slash registry scenario — that last one fails with ENEEDAUTH on the pre-fix code, confirming it covers the regression.

All existing config snapshots are unchanged, and whoami/login/owner still pass.

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