feat(cli): read pnpm's auth.ini for embedded package credentials [RED-891] - #1444
Open
sorccu wants to merge 4 commits into
Open
feat(cli): read pnpm's auth.ini for embedded package credentials [RED-891]#1444sorccu wants to merge 4 commits into
sorccu wants to merge 4 commits into
Conversation
…ED-891] pnpm 11 stopped writing registry credentials to .npmrc: `pnpm login` writes them to auth.ini in pnpm's global config directory instead. Embedded package downloads only merged .npmrc files, so a logged-in pnpm user looked unauthenticated and embedding a private package failed with HTTP 404 — npm answers 404 rather than 401 for packages an unauthorized caller may not see. Resolve pnpm's config directory the way pnpm does and merge auth.ini into the configuration. The file is always consulted; only its precedence depends on the project's lockfile: above the user .npmrc for pnpm projects, below it for others, so each project follows its own package manager's model. Precedence is what makes this work — the reported failure sent a stale .npmrc token, so reading auth.ini without outranking that token would change nothing. An unreadable auth.ini is skipped rather than fatal: unlike a .npmrc, it belongs to another tool and must not take the whole command down. Expressing that required defaultNpmrcPaths to return records instead of paths. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKwAcpVJCkJgK3yfxfSVqu
…ad fails [RED-891] The credential hint fired only on 401/403, but registries routinely hide packages an unauthorized caller may not see behind a 404 — npm does. So the most common authentication failure produced a bare "HTTP 404" that reads as "this package does not exist", sending people to look in the wrong place entirely. Extend the hint to 404, hedged, since a 404 can equally mean the package is genuinely absent. Then say which credential was used and where it came from: credentials can now arrive from a project .npmrc, the workspace .npmrc, pnpm's auth.ini, the user .npmrc, or an npm_config_* environment variable, so "your credentials were rejected" without naming the source leaves the reader as stuck as the bare status code did. Tracking that source required loadNpmrcConfig to report which key each value came from, and resolveAuthHeader to report the keys it matched. Both halves of a username/_password pair are reported, because precedence is per key and the halves routinely live in different files — naming only the username would point at the half that cannot expire. Credentials embedded in a registry URL are attributed to the URL and to whatever configured it: axios sends those itself and drops the Authorization header when it does, so reporting the config entry would name a credential that never reached the wire. Config file paths and key names appear in these messages; credential values never do, and the tests assert that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKwAcpVJCkJgK3yfxfSVqu
Download failure messages ran unparseable URLs through a regex to strip userinfo before printing them. Five review rounds found five ways past it: it stopped at the first `@` so a password containing one kept its tail; a widened form matched an empty userinfo and ate a path separator; a host-less string parsed as an opaque scheme so the parser left the credential untouched; anchoring the pattern let an authority at a non-zero offset through; and a query string carrying a pre-signed signature was never considered at all. Telling userinfo from a path in a malformed string needs a parser, so stop trying. A URL is now rebuilt from scheme, host and path — userinfo, query and fragment are gone by construction rather than stripped — and anything that does not parse, or parses without a host, is withheld entirely. This is the rule `rest/errors.ts` already applies to proxy URLs. Nothing is lost by withholding it: the invalid-URL error now names the config key and file that produced the value, or the lockfile that recorded it, which is where the reader goes to fix it anyway. Also here, from the same review rounds: - Config origins are structured rather than a sentinel string, so an environment variable is named by its verbatim spelling. NPM_CONFIG_REGISTRY was being reported as npm_config_registry, a name that exists nowhere. - A redirect is reported for any failing status, and the credentials are only called rejected when the answering host actually received them. follow-redirects drops the Authorization header across hosts, so blaming a credential the CDN never saw sent readers to rotate a working token. - The message vocabulary moves to diagnostics.ts with a colocated spec. Direct unit tests there would have caught all five redaction leaks; they were previously reachable only through the HTTP sandbox harness. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKwAcpVJCkJgK3yfxfSVqu
…891] Rebuilding a URL from scheme, host and path still kept the one credential-bearing component the proxy-URL precedent in rest/errors.ts drops. Some registries take a token as a path segment (https://host/<token>/npm/), so the path is not safe to echo either. Only scheme and host survive now. The path was the component least worth keeping: every message that shows a URL already names the package and version separately, which is what the path encodes. Also correct the invalid-URL message, whose advice did not match its branch. A tarball URL recorded in the lockfile was answered with "a registry must be an absolute URL", pointing the reader at a registry setting that is not involved and is probably already correct. Each branch now describes the source it actually came from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKwAcpVJCkJgK3yfxfSVqu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: RED-891
pnpm 11 moved
pnpm logincredentials out of.npmrcintoauth.iniin pnpm's global config directory. Embedded package downloads only merged.npmrcfiles, so a logged-in pnpm user looked unauthenticated and embedding a private package failed withHTTP 404— npm answers 404, not 401, for packages an unauthorized caller may not see, so the failure read as "this package does not exist".Read pnpm's
auth.iniResolves pnpm's config directory the way pnpm does (
XDG_CONFIG_HOME/pnpm→ macOS~/Library/Preferences/pnpm→~/.config/pnpm→ Windows%LOCALAPPDATA%/pnpm/config;PNPM_HOMEis deliberately not consulted — pnpm uses it only for data and state directories).The file is always read; only its precedence depends on the lockfile: above the user
.npmrcfor pnpm projects, below it for others, mirroring pnpm's own order. Precedence is the load-bearing part — the reported failure was a stale.npmrctoken being sent, so readingauth.iniwithout outranking that token would change nothing. Both end-to-end tests write a different token into each file and assert which one arrives; a fixture with a token in only one file passes under either ordering and cannot detect inverted precedence.An unreadable
auth.iniis skipped rather than fatal: unlike a.npmrc, it belongs to another tool and must not take the whole command down.Name the credential source on failure
The hint now also fires on 404 (hedged — a 404 can equally mean the package is absent), and says which credential was used and where it came from. With five possible sources, "your credentials were rejected" without naming one leaves the reader as stuck as the bare status code:
Both halves of a
username/_passwordpair are reported, since precedence is per key and the halves routinely live in different files — naming only the username points at the half that cannot expire. Credentials embedded in a registry URL are attributed to the URL and to whatever configured it, because axios sends those itself and drops theAuthorizationheader when it does.Config paths and key names appear in these messages; credential values never do, and the tests assert that.
Reviewer notes
loadNpmrcConfigreturns{config, sources, unreadable, origins}instead of a bare map, andresolveAuthHeaderreturns{header, keys}instead of a string. Both were needed to trace a credential back to its file.NPM_CONFIG_*variable is reported under its lowercase alias; attribution is computed pre-request, so a cross-host redirect that strips the auth header would still be reported as a rejection; the lockfile arm of URL attribution is untested.redactUrl's non-URL fallback can leak the tail of a password containing a literal@— pre-existing and untouched.//host/:@scope:_authToken, written bypnpm login --scope), and trailing-slash tolerance in the nerf-dart walk.🤖 Generated with Claude Code
https://claude.ai/code/session_01LKwAcpVJCkJgK3yfxfSVqu