feat(cli): read pnpm's auth.ini for embedded package credentials [RED-891] [ship] - #1444
Open
sorccu wants to merge 4 commits into
Open
feat(cli): read pnpm's auth.ini for embedded package credentials [RED-891] [ship]#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
sorccu
force-pushed
the
simo/red-891-pnpm-auth-ini-support
branch
from
August 24, 2026 20:26
e51195a to
b9ea0e1
Compare
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.Say where the credentials came from
The hint now also fires on 404 (hedged — a 404 can equally mean the package is absent) and names the source. With credentials arriving from any of five places, "your credentials were rejected" without naming one leaves the reader as stuck as the bare status code:
and when nothing matched, every place that was consulted:
Both halves of a
username/_passwordpair are named, since precedence is per key and the halves routinely live in different files — naming only the username points at the half that cannot expire. An environment variable is named by its verbatim spelling (NPM_CONFIG_REGISTRY, not the case-folded key). The same hint now also covers the registry-metadata request that yarn plans make, which authenticates identically.Redirects.
follow-redirectsstripsAuthorizationacross hosts, so a tarball download that redirects to a CDN is answered by a host that never saw the credentials. Calling them rejected there would send someone to rotate a working token, so the hop is reported instead. Whether the header survived is observed from the post-strip options inbeforeRedirectrather than re-derived from the library's policy — subdomain redirects keep it, protocol downgrades drop it regardless of host.Never echo a URL that cannot be shown safely
A URL in an error is now rebuilt from scheme and host only. Userinfo, path, query and fragment are absent by construction rather than stripped: a registry URL may embed a token, a pre-signed CDN URL puts its signature in the query, and some registries take a token as a path segment. Anything that does not parse, or parses without a host, is withheld entirely — the rule
rest/errors.tsalready applies to proxy URLs.This replaced a regex that tried to strip userinfo from malformed strings. Review found five ways past it (truncating at the first
@, matching an empty userinfo, a host-less parse that left the credential in the path, an authority not at offset 0, and a query never considered), so the approach went rather than its fifth patch. Nothing is lost: 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.Reviewer notes
loadNpmrcConfigreturns{config, files, unreadable, origins}instead of a bare map, andresolveAuthHeaderreturns{header, keys}instead of a string. Both were needed to trace a credential back to its source.diagnostics.tswith a colocated spec. It is worth keeping it directly unit-tested: every one of the redaction leaks above was found by review rather than by the end-to-end tests that were the only coverage at the time.registryHttpError(added by feat(cli): prune the bundled lockfile to the code bundle's contents [RED-886] [ship] #1442) gained ahintcallback so the download and metadata paths share one wrapper without sharing one fixed hint.//host/:@scope:_authToken, written bypnpm login --scope), and trailing-slash tolerance in the nerf-dart walk. Neither is started;resolveAuthHeadercurrently probes only slash-terminated darts and ignores the scope.🤖 Generated with Claude Code
https://claude.ai/code/session_01LKwAcpVJCkJgK3yfxfSVqu