Skip to content

install: resolve .npmrc credentials by npm's key walk, applied once in the package manager - #40424

Closed
alii wants to merge 15 commits into
claude/npmrc-credential-hygienefrom
claude/npmrc-key-walk
Closed

alii wants to merge 15 commits into
claude/npmrc-credential-hygienefrom
claude/npmrc-key-walk

Conversation

@alii

@alii alii commented Aug 25, 2026 •

Copy link
Copy Markdown
Member

Part 2 of 3, split out of #33869. Stacked on #40423.

#40423 _auth sent as written; no credential left in the request path
#40424 (this) npm's key walk for .npmrc lines, applied once in the package manager
#40425 request-time lookup for tarballs

Fixes #30311. Fixes #28233. Fixes #40549.

What this changes

An .npmrc credential line only applied to a registry when the URL paths matched exactly, so a host-root token was dropped for a registry under a path (GitLab's documented shape, #30311 and #40549) and a scoped registry's own line stopped matching in 1.3.11 (#28233).

npm's model is a flat config map and one walk per URL (npm-registry-fetch's regFromURI). This implements that model in two halves:

  • bun_ini collects every //host/path/:<option>= line from every .npmrc file into one map keyed by the literal text between // and :<option>=, last write wins, and collapses it to one entry per key that carries a complete credential (_authToken, else _auth, else username + _password; an empty value supplies nothing). That list is install.url_auth. bun_ini no longer applies credentials to any registry.
  • Options::load applies them once, after every source that can set a registry URL (.npmrc, bunfig.toml, $NPM_CONFIG_REGISTRY, --registry): each registry still without credentials gets the first key on the walk from its URL (the full path, then one segment shorter each time, down to the bare host; the key with a trailing slash is a distinct key visited first; a string prefix is not an ancestor). A credential from bunfig, the environment or the command line therefore outranks a .npmrc line by construction. A credential written into the registry URL itself (userinfo, or a :_authToken= segment in the path) is the weakest source, as in npm, whose getAuth never reads it: any complete .npmrc line for the key replaces it. A key carries no scheme, so a line applies to an http:// registry with the same host too; only a credential carried over from a previous registry refuses an https-to-http downgrade.

A registry's key comes from the WHATWG serialisation of its URL, the one requests are built from: host lowercased, a default port dropped, the query left out. Hand-written keys are compared as written, after one fold: a scheme is dropped (Bun's docs show //http://localhost:4873/:_authToken=) with that scheme's default port, and the host is lowercased; a scheme-less key keeps its port, as in npm, so //host:443/ is the key of http://host:443/, not of https://host/.

The option name must end the key. //host/:_authtoken= used to match _auth as a substring and go out as Basic <token>; it now warns _authtoken is not a known .npmrc option; ignoring this line at .npmrc:<line> and sends nothing. The warning never echoes the line, so a value under a misspelt name cannot reach stderr, and it only fires for an option-shaped word with a value: a bare //host:port line or a key without = prints nothing. --silent suppresses it. A known option with a non-string value (_authToken=true) is ignored silently.

Dropped on the way: email is accepted and ignored (npm never sends it, and nothing in Bun read it); a lone username or _password on a registry's own key no longer layers over the URL's userinfo (npm's hasAuth needs the pair).

Tests

bun-install.test.ts runs bun install against a local registry for the walk matrix (ancestor keys, string prefixes, trailing slashes, scoped and default registries, bunfig.toml registries with and without credentials, home and project files); npmrc.test.ts covers key normalisation through the loader hook and the diagnostics end to end; redacted-config-logs.test.ts pins that no credential value reaches stderr under any spelling.

Comment thread src/bun_core/lib.rs Outdated
Comment thread src/bun_core/fmt.rs Outdated
Comment thread src/ini/lib.rs Outdated
@alii
alii force-pushed the claude/npmrc-key-walk branch from 18023b7 to 524782e Compare August 25, 2026 07:53
@robobun

robobun commented Aug 25, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 1:52 AM PT - Aug 25th, 2026

@alii, your commit 5ab8568 is building: #105521

Comment thread test/cli/install/npmrc.test.ts
Comment thread src/bun_core/fmt.rs Outdated
Comment thread src/ini/lib.rs

@claude claude Bot 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.

I've reviewed this across four passes now and the latest commit is clean — no findings.

Given this rewrites credential resolution end to end (which key's token reaches which registry host), a human look is still warranted before merge.

What was reviewed

  • The key-walk (RegistryKey::walk, strip_one_key_component, has_auth) against npm's regFromURI/hasAuth: string-prefix vs path-segment ancestry, trailing-slash as a distinct key, _authToken > _auth > username+_password precedence, cross-file last-write-wins collapse.
  • normalize_key / RegistryKey::from_parsed: host lowercasing, default-port drop, bracketed-IPv6 port detection, scheme-prefixed keys.
  • Redaction paths in fmt.rs/lib.rs for the new unknown-option warning under both NO_COLOR and FORCE_COLOR, quoted and unquoted keys.
  • Test hermeticity (every new spawn overrides HOME/USERPROFILE/XDG_CONFIG_HOME) and that the matrix asserts exact Authorization headers against a local server.
Extended reasoning...

Overview

Part 2 of a 3-part stack. Replaces exact-path .npmrc credential matching with npm's key-walk algorithm from npm-registry-fetch/lib/auth.js: collect every //host/path:opt= line from every .npmrc into one flat list, then for each registry walk its key from full path up to bare host and take the first key supplying a complete credential. Also normalises config keys (lowercase host, drop default port, drop leading scheme), requires the option name to be an exact key suffix (fixing _authtoken matching _auth), warns on unknown option names, and closes two redaction gaps in the diagnostic printer. ~400 lines rewritten in src/ini/lib.rs, ~800 lines of new tests.

Security risks

Credential resolution is the security surface here. The two failure modes are (a) sending a credential to a host/path it wasn't configured for and (b) printing a credential in a diagnostic. For (a), the walk is path-segment-based (strip_one_key_component pops one / or one non-/ run), so /projects/12 is never an ancestor of /projects/123/ — covered by explicit tests in both bun-install.test.ts and npmrc.test.ts. Lookups are byte-equal against normalised keys, and lookup uses rfind so last-write-wins matches npm's flat map. For (b), the new unknown-option warning routes through redact_sensitive_information: true; earlier passes found and fixed a quoted-key gap and the password prefix omission, and redacted-config-logs.test.ts now asserts both colour modes for quoted/unquoted misspelt keys.

Level of scrutiny

High. This is a full rewrite of how registry credentials are selected, in the package manager's request path, and it interacts with three config sources (.npmrc files, bunfig.toml, URL userinfo) plus an env override in part 3. The algorithm is derived from a specific upstream (npm-registry-fetch), and the PR description and code comments cite it, but the port is hand-written rather than mechanical.

Other factors

Three prior automated review rounds surfaced six findings, all minor (dead code created by the PR's own edits, a redaction-prefix gap, a misleading diagnostic on non-string values, a non-hermetic test, a stale comment) and all addressed in follow-up commits. The test matrix is unusually thorough — measured against npm 10.9.3 and 11.15.0, asserting exact Authorization header bytes on a local Bun.serve — and every new subprocess test isolates HOME/XDG_CONFIG_HOME. That gives good confidence in correctness, but the scope and the security sensitivity of "which token goes where" put this outside what I'll auto-approve.

@alii
alii force-pushed the claude/npmrc-key-walk branch from 5ab8568 to ddf775b Compare August 25, 2026 19:35
@alii
alii force-pushed the claude/npmrc-credential-hygiene branch from 1f497c8 to a751ac0 Compare August 25, 2026 19:35

@claude claude Bot 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.

All six earlier inline comments are addressed — thanks. No new issues found on this pass. Given this rewrites the credential-to-registry resolution algorithm (which auth header goes to which host) and is part 2 of a stacked series, a human look is still worthwhile before merge.

What was reviewed:

  • npm key-walk (credentials/has_auth/RegistryKey::walk) against npm-registry-fetch semantics: precedence, trailing-slash distinctness, string-prefix-is-not-ancestor.
  • normalize_key / RegistryKey::from_parsed for host lowercasing, default-port stripping, and bracketed-IPv6 handling.
  • Redaction paths (color and NO_COLOR) after the has() deletion — every keyword still covered by has_prefix().
  • Test hermeticity: every new subprocess spawn overrides HOME/USERPROFILE/XDG_CONFIG_HOME.
Extended reasoning...

Overview

This PR replaces exact-path matching for .npmrc credential lines with npm's regFromURI key-walk: config lines from every .npmrc file are collected into one flat list, keys are normalised (scheme dropped, host lowercased, default port removed), and for each registry the resolver walks its <host><path>/ key upward one component at a time until a key supplies a complete credential (_authToken > _auth > username+_password). It also adds an IniOption::Unknown warning for misspelt option names, tightens option-name matching to an exact suffix, and closes two redaction gaps in the diagnostic printer. ~400 lines of new Rust in src/ini/lib.rs (replacing RegistryAuth), small edits in bun_core/fmt.rs / bun_core/lib.rs / ast/lib.rs / install/PackageManager.rs, and ~1000 lines of new tests.

Security risks

Credential routing is the security surface here. The two failure modes to guard against are (a) sending a credential to a host/path it was not scoped to, and (b) dropping a credential that should apply. The new algorithm is a straight port of npm-registry-fetch/lib/auth.js: byte-equal key lookup after normalisation, with the walk stripping one / or one trailing non-/ run per step. The test matrix explicitly covers the string-prefix case (/projects/12 must not authorise /projects/123/), cross-file collapse ordering, and precedence within a single key. The redaction changes only widen what gets masked in local terminal output.

Level of scrutiny

High. This is production credential-handling code in bun install, and the change is an algorithmic rewrite rather than a targeted fix. It is part 2 of 3 stacked on #40423, with the request-time resolution (--registry, tarball hosts) deferred to part 3 — a maintainer should see the whole shape.

Other factors

I raised six inline findings across three earlier passes (dead _authToken redaction branch; has_prefix() missing password; known-option-with-non-string-value falling through to the Unknown warning; a non-hermetic test; has() becoming dead after the password addition; a stale ordering comment). All six were fixed in follow-up commits and the threads are resolved. This run's bug hunt found nothing new. Test coverage is extensive and measured against npm 10.9.3/11.15.0; the PR description states 62 rows fail on main.

@robobun

robobun commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Issue #40549 reports this same bug (GitLab token on /api/v4/packages/, registry at /api/v4/packages/npm/, 401). I reproduced it against a local registry: the request goes out with no Authorization header on current canary, and npm 11 sends the token with the same .npmrc. This PR covers that case, so it may be worth adding a "Fixes #40549" line.

@alii
alii force-pushed the claude/npmrc-credential-hygiene branch from 00b9f38 to 95b2e64 Compare August 27, 2026 04:51
@alii
alii force-pushed the claude/npmrc-key-walk branch from d726cf7 to da02f52 Compare August 27, 2026 04:52
Comment thread src/ini/lib.rs
Comment thread src/ini/lib.rs Outdated
Comment thread test/cli/install/redacted-config-logs.test.ts

@claude claude Bot 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.

Code review found no issues

No high-confidence issues detected in this change.

@alii
alii force-pushed the claude/npmrc-credential-hygiene branch from 95b2e64 to d7fb062 Compare August 27, 2026 19:04
@alii
alii force-pushed the claude/npmrc-key-walk branch from 29ba4bb to 7cc6cf0 Compare August 27, 2026 19:04

@claude claude Bot 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.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 src/ini/lib.rs — nit: pre-existing comment "The single install.scoped = registry_map write-back happens at the bottom of load_npmrc" is stale — this PR moved the write-back into parse_npmrc_into and repurposed load_npmrc as a thin single-file wrapper, so the comment now names the wrong function

    Extended reasoning...

    A reader following the comment at src/ini/lib.rs:1806-1808 looks in load_npmrc for the install.scoped = Some(registry_map) assignment and finds only the new parse_npmrc_into(…); resolve_credentials(…); diagnose_config(…) sequence with no registry_map in sight — the write-back is at line 1872 inside parse_npmrc_into. On the base branch the comment was accurate because load_npmrc was the function that owned this block; the rename/split in this PR made it stale. Per REVIEW.md "Only comment what the code cannot say" and src/CLAUDE.md "NEVER add comments to deleted code blocks", the fix is to update the function name to parse_npmrc_into or delete the comment (the write-back is ~60 lines below and self-evident).

    Verification: nit — the comment is now stale exactly as described. On the base branch (d7fb062), the comment at src/ini/lib.rs:1533-1534 ("The single install.scoped = registry_map write-back happens at the bottom of load_npmrc…") sat inside pub fn load_npmrc (base line 1315), and the write-back install.scoped = Some(registry_map); was at base line 1604 in that same function — the comment was…

…hrough the scope builder, one Authorization builder
@alii
alii force-pushed the claude/npmrc-key-walk branch from 7cc6cf0 to bcd9157 Compare August 27, 2026 20:50
@alii alii changed the title install: resolve .npmrc credentials by npm's key walk, normalise keys on read, warn on unknown options install: resolve .npmrc credentials by npm's key walk, applied once in the package manager Aug 27, 2026

@claude claude Bot 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.

This pull request has now been reviewed several times and this review found new issues. Before patching these one by one, step back: would one root-cause fix close several of them? Is the pull request's scope growing with each push? Prefer root-cause fixes, keep scope fixed, and note out-of-scope improvements as follow-ups.

Comment thread src/ini/lib.rs Outdated
@alii
alii force-pushed the claude/npmrc-key-walk branch from bcd9157 to cb37a43 Compare August 27, 2026 21:34

@claude claude Bot 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.

This pull request has now been reviewed several times and this review found new issues. Before patching these one by one, step back: would one root-cause fix close several of them? Is the pull request's scope growing with each push? Prefer root-cause fixes, keep scope fixed, and note out-of-scope improvements as follow-ups.

Comment thread src/ini/lib.rs

@claude claude Bot 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.

Code review found no issues

No high-confidence issues detected in this change.

@Jarred-Sumner
Jarred-Sumner force-pushed the claude/npmrc-credential-hygiene branch from 176ecb7 to cb87058 Compare August 28, 2026 09:06
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Folded into #40423 (same commits, rebased onto main); #40425 is now stacked directly on #40423.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants