Skip to content

install: resolve .npmrc //host/ credentials by request URL - #38800

Closed
robobun wants to merge 4 commits into
mainfrom
farm/55395501/npmrc-url-credentials
Closed

robobun wants to merge 4 commits into
mainfrom
farm/55395501/npmrc-url-credentials

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • .npmrc credential lines (//host/path/:_authToken=, :_auth=, :username= + :_password=) are only attached to registries declared in a config file, at the moment that file is read (src/ini/lib.rs, load_npmrc / apply_registry_auth). Nothing looks a credential up by the URL of the request being made, which is how npm applies them.
  • Setups that npm handles therefore send no Authorization header and fail with a bare error: GET <url> - 401:
    • ~/.npmrc containing only //reg/:_authToken=T, plus bun install --registry http://reg/
    • the same .npmrc, plus NPM_CONFIG_REGISTRY or BUN_CONFIG_REGISTRY (both are applied in Options::load, after the .npmrc lines were matched)
    • a registry whose dist.tarball URLs are on another host that has its own //host/ line (NetworkTask::for_tarball only ever consulted the package's scope, and correctly refuses to send the scope's credentials cross-origin)
    • a registry that moved: bun.lock still holds absolute tarball URLs on the old host, .npmrc has lines for both hosts, bun install --frozen-lockfile on a cold cache 401s on every tarball
    • auth tokens not sent to scoped-registry tarball URLs when token path diverges from registry URL path (regression from 1.3.10) #30513: the token is keyed to GitLab's project-level path, tarballs are served from that path, and the registry is declared at the instance-level path, so the line matches neither the registry URL nor anything else bun compares it against
    • a line keyed to a parent path of the registry URL (//host/ with a registry at /api/v4/.../npm/)
  • The failure output gives no hint that the request went out without credentials or which line would fix it.

Fix

Rebase notes

Fixes #30513

Background

  • Scope (npm::registry::Scope): bun's unit of registry configuration, one for the default registry plus one per @scope:registry. It holds the registry URL and the credentials sent with every request to it; Options::scope_for_package_name picks it per package, and NetworkTask::for_manifest / for_tarball send its token (bearer) or auth (base64 user:pass).
  • Credential lines (npm calls the keys "nerf darts"): .npmrc keys of the form //host/path/. npm never attaches them to a registry; for every request it builds //host/path from the request URL and walks up the path until a key with credentials matches. A registry's own key matches its manifests and tarballs because they share its host and path; a tarball served elsewhere gets credentials only if that location has its own key.
  • Why tarballs are special: the tarball URL comes from the manifest, which the registry controls, so a hostile registry could direct a download anywhere. bun therefore sends a scope's credentials only to the scope's own origin. The by-URL lookup only ever sends credentials to the place the user wrote them down for, so it does not weaken that.
  • Config order (PackageManager::init, then Options::load): .npmrc files are parsed into BunInstall, bunfig is overlaid, then Options::load applies environment variables and command-line flags. Registries set in that last step did not exist when the .npmrc lines were attached, which is why the lookup has to run at the end of load and again at request time for tarballs.

[review] gate passed · iteration 8 · 10 files touched

fails on main (without fix)
ASAN without fix: 14 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/npmrc.test.ts
bun test v1.4.0 (8326d1bd3)

test/cli/install/npmrc.test.ts:
{
  out: "/tmp/verdaccio-test-_fXI0v8/hi!",
  err: "",
}
(pass) npmrc > should convert to utf8 if BOM [185.77ms]
package dir /tmp/verdaccio-test-_XKIOv3
bun install v1.4.0 (8326d1bd3)
No packages! Deleted empty lockfile

[92.00ms] done
(pass) npmrc > works with empty file [187.64ms]
package dir /tmp/verdaccio-test-_aFVDYU
bun install v1.4.0 (8326d1bd3)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [150.00ms]
(pass) npmrc > sets default registry [216.92ms]
bun install v1.4.0 (8326d1bd3)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ @types/no-deps@1.0.0 (v2.0.0 available)

1 package installed [133.00ms]
(pass) npmrc > sets scoped registry [211.77ms]
package dir /tmp/verdaccio-test-_UMH3IM
home dir /tmp/verdaccio-test-_UMH3IM/home_dir
bun install v1.4.0 (8326d1bd3)
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package in
... (truncated)

release without fix: 15 FAILED
bun test v1.4.0-canary.1 (8326d1bd3)

test/cli/install/npmrc.test.ts:
{
  out: "/tmp/verdaccio-test-_i8dErt/hi!",
  err: "",
}
(pass) npmrc > should convert to utf8 if BOM [4.62ms]
package dir /tmp/verdaccio-test-_r1N7W1
bun install v1.4.0-canary.1 (8326d1bd3)
No packages! Deleted empty lockfile

[0.00ms] done
(pass) npmrc > works with empty file [4.30ms]
package dir /tmp/verdaccio-test-_X5jgrE
bun install v1.4.0-canary.1 (8326d1bd3)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [19.00ms]
(pass) npmrc > sets default registry [22.67ms]
bun install v1.4.0-canary.1 (8326d1bd3)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ @types/no-deps@1.0.0 (v2.0.0 available)

1 package installed [4.00ms]
(pass) npmrc > sets scoped registry [7.71ms]
package dir /tmp/verdaccio-test-_MiX1NW
home dir /tmp/verdaccio-test-_MiX1NW/home_dir
bun install v1.4.0-canary.1 (8326d1bd3)
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [1.00ms]
(pass) npmrc > works with home config [4.25ms]
package dir /tmp/verdaccio-test-_FALt7K
home dir /tmp/verdaccio-te
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/npmrc.test.ts
bun test v1.4.0 (8326d1bd3)

test/cli/install/npmrc.test.ts:
{
  out: "/tmp/verdaccio-test-_AsA7H5/hi!",
  err: "",
}
(pass) npmrc > should convert to utf8 if BOM [199.84ms]
package dir /tmp/verdaccio-test-_LLASwW
bun install v1.4.0 (8326d1bd3)
No packages! Deleted empty lockfile

[97.00ms] done
(pass) npmrc > works with empty file [194.34ms]
package dir /tmp/verdaccio-test-_t4XuKB
bun install v1.4.0 (8326d1bd3)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package installed [146.00ms]
(pass) npmrc > sets default registry [213.96ms]
bun install v1.4.0 (8326d1bd3)
Resolving dependencies
Resolved, downloaded and extracted [2]
Saved lockfile

+ @types/no-deps@1.0.0 (v2.0.0 available)

1 package installed [129.00ms]
(pass) npmrc > sets scoped registry [198.56ms]
package dir /tmp/verdaccio-test-_UKfePt
home dir /tmp/verdaccio-test-_UKfePt/home_dir
bun install v1.4.0 (8326d1bd3)
Saved lockfile

+ no-deps@1.0.0 (v2.0.0 available)

1 package in
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 1003ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/642] cc obj/vendor/tinycc/tccrun.c.o
[2/642] cc obj/vendor/tinycc/x86_64-link.c.o
[3/642] cc obj/vendor/tinycc/i386-asm.c.o
[4/642] cc obj/vendor/tinycc/x86_64-gen.c.o
[5/642] fetch mimalloc
[mimalloc] up to date
[6/641] fetch libspng
[libspng] up to date
[7/640] fetch boringssl
[boringssl] up to date
[8/218] fetch lsquic
[lsquic] up to date
[9/149] cc obj/codegen/InternalModuleRegistryConstants.S.o
[10/149] fetch lolhtml
[lolhtml] up to date
[10/149] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m std v0.0.0 (/root/.rustup/toolchains/nightly-2026-07-20-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std)
�[1m�[92m   Compiling�[0m bun_output_tags v0.0.0 (/workspace/bun/src/bun_output_tags)
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_parsers v0.0.0 (/workspace/bun/src/parsers)
�[1m�[92m   Comp
... (truncated)
diff hotspot
docs/pm/npmrc.mdx                                  |   2 +-
 src/api/lib.rs                                     |   2 +-
 src/ini/lib.rs                                     |  23 +-
 src/install/NetworkTask.rs                         |  90 ++---
 src/install/PackageManager.rs                      |   3 +
 .../PackageManager/PackageManagerOptions.rs        | 158 ++++++++
 src/install/PackageManager/runTasks.rs             |  49 ++-
 src/install/npm.rs                                 |  89 +++++
 src/options_types/schema.rs                        |  22 ++
 test/cli/install/npmrc.test.ts                     | 433 +++++++++++++++++++++
 10 files changed, 819 insertions(+), 52 deletions(-)

gate history · 2 passed · 2 rejected · iteration 8

evidence per changed file
file                                                 reads  edits  tests
docs/pm/npmrc.mdx                                        2      3      0
src/api/lib.rs                                           2      6      0
src/ini/lib.rs                                           6      8      0
src/install/NetworkTask.rs                               7     14      0
src/install/PackageManager.rs                            4      2      0
src/install/PackageManager/PackageManagerOptions.rs      8     25      0
src/install/PackageManager/runTasks.rs                   7      7      0
src/install/npm.rs                                       5     10      0
src/options_types/schema.rs                              2      5      0
test/cli/install/npmrc.test.ts                          10     18      0

@coderabbitai

coderabbitai Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Limit details: You’ve used all 5 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 05bfb53c-b40b-48f5-b763-019c2181a5c1

📥 Commits

Reviewing files that changed from the base of the PR and between 875c761 and 396ad6e.

📒 Files selected for processing (10)
  • docs/pm/npmrc.mdx
  • src/api/lib.rs
  • src/ini/lib.rs
  • src/install/NetworkTask.rs
  • src/install/PackageManager.rs
  • src/install/PackageManager/PackageManagerOptions.rs
  • src/install/PackageManager/runTasks.rs
  • src/install/npm.rs
  • src/options_types/schema.rs
  • test/cli/install/npmrc.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review and mergeable. Rebased onto main three times (on top of #38796, #38183, and now #38867); current head 396ad6e. The #38867 merge is the only one that needed real resolution (its tarball userinfo header and this PR's lookup now form one precedence chain; summarized under "Rebase notes" in the description), plus one small follow-up commit so the 401 note also stays quiet after a rejected userinfo header. CI build 100223 on the current tree (396ad6e, same tree as 4688f54) passed on all 179 jobs, darwin lanes included.

Reproduced with Bun.serve mock registries that 401 unless the expected Authorization header arrives, against the 1.4.0 release build: token-only .npmrc plus --registry, plus NPM_CONFIG_REGISTRY, plus BUN_CONFIG_REGISTRY, a tarball on a second host that has its own //host/ line, a lockfile recorded against a registry that has since moved, and the #30513 path shape. All sent no header and exited 1; a declared registry= plus token (control) passed, and a cross-origin tarball host without a line still receives nothing after the change.

Scope note: the first push also carried fixes for credentials embedded in --registry / env var URLs and for bun publish with basic auth. Those were already open as #38796 (since merged; this PR is rebased on it) and #38776, so this PR was narrowed to the by-URL lookup and the 401 note; the env var and --registry code is not touched here.

Tests: the new block in test/cli/install/npmrc.test.ts (16 tests; 14 fail on the release build, the other two pin the note guards and fail on the revisions before each was added). On the rebased tree the whole file (56 tests, including #38796's and #38828's) passes with the debug build, as do #38867's tarball userinfo block and #38183's registry spellings block in bun-install.test.ts, config-precedence and redacted-config-logs.

@robobun
robobun force-pushed the farm/55395501/npmrc-url-credentials branch from f3ea94a to f4fd5de Compare August 15, 2026 01:48
@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:28 PM PT - Aug 17th, 2026

✅ @robobun, your commit 396ad6ef8c6b18910b3f7f140476535fc51dfbbb passed in Build #100223! 🎉


🧪   To try this PR locally:

bunx bun-pr 38800

That installs a local version of the PR into your bun-38800 executable, so you can run:

bun-38800 --bun

@robobun
robobun force-pushed the farm/55395501/npmrc-url-credentials branch from f4fd5de to 727d5a2 Compare August 15, 2026 02:00

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

The 3cc622f follow-up correctly addresses the earlier note: NetworkTask.authorization is stored at enqueue time, RequestKind::Tarball carries it to missing_credentials_note, and the NoAuthorization arm returns empty so github:/URL-tarball 401s no longer get an .npmrc suggestion that wouldn't apply; the new test pins that. I re-reviewed and found no further issues. Because this changes which Authorization header is sent to which host, a human pass over the credential-routing surface (UrlAuth::matches/find, tarball_credentials, fill_credentials_from_url_auth) would still be worthwhile.

What was reviewed: the cross-origin guard is preserved (tarball_credentials returns the scope's creds only on is_same_origin, otherwise only what .npmrc names for that exact host); UrlAuth::matches requires host + effective-port equality and a directory-boundary path prefix (the /no vs /no-deps test covers the string-prefix trap); entries without a sendable header are dropped in from_api so they can't shadow a shallower key; write_init seeds the new authorization field so pooled slots don't carry a stale value.

Extended reasoning...

Overview

Adds npm-style by-URL resolution of .npmrc //host/path/ credential lines. bun_ini now records every such line into BunInstall.url_auth; Options::load converts them to npm::registry::UrlAuth and, after all config sources are applied, back-fills any credential-less scope from the entry matching its registry URL. NetworkTask::for_tarball now consults Options::tarball_credentials (scope creds if same-origin, else the url_auth entry for the tarball URL, else nothing). A 401/403 hint is appended to the two GET <url> - <status> sites, gated on the request having been allowed to carry credentials at all. 13 new subprocess tests assert the exact Authorization header each mock server receives.

Follow-up on prior review

My earlier finding was that the 401 hint would suggest an .npmrc line for github:/URL-tarball dependencies even though for_tarball short-circuits to no credentials for Authorization::NoAuthorization. Commit 3cc622f threads the enqueue-time Authorization through NetworkTask.authorization → RequestKind::Tarball(auth) → missing_credentials_note, whose NoAuthorization arm now returns empty. write_init seeds the field to NoAuthorization so a reused pool slot cannot leak a stale AllowAuthorization. The added test ("a 401 for a tarball dependency given by URL is reported without a suggestion") asserts both that no header was sent and that stderr contains no .npmrc mention. This fully resolves the concern.

Security risks

This is the security-sensitive part of the PR: it changes when an Authorization header is attached to an outbound request. I checked: (1) the same-origin guard on scope credentials is preserved — tarball_credentials returns the scope only when is_same_origin(tarball, scope.url), and is_same_origin compares protocol/hostname case-insensitively plus effective port, which is the correct normalization of the previous inline check; (2) the fallback only ever sends credentials the user explicitly keyed to that host in .npmrc, so a hostile registry pointing dist.tarball elsewhere still cannot exfiltrate anything the user didn't configure for that destination; (3) UrlAuth::matches enforces host, port (default-port aware), and directory-boundary path prefix — the /no ≠ /no-deps/... test covers the substring-prefix pitfall; (4) fill_credentials_from_url_auth only fills scopes that have no credentials, so --token / BUN_CONFIG_TOKEN and bunfig-set credentials are not overridden; (5) entries with no sendable header (email alone, empty hostname, unparseable port) are dropped in UrlAuth::from_api so they cannot shadow a shallower usable key via the deepest-path rule.

Level of scrutiny

High — this is credential routing in the package manager. The change is additive and well-tested (each test asserts the exact header the server received, plus negative cases where no header must be sent), and the design mirrors npm's nerf-dart semantics. Still, per the approval guidelines, auth-header decisions are exactly the class of change that should get a maintainer's eyes rather than a bot approval, particularly the interaction with the three sibling PRs (#38796, #38776, #33869) the description says this composes with.

Other factors

Test coverage is thorough: --registry, both env vars, basic auth from username/_password, cross-host tarballs, deepest-path precedence, string-prefix-not-directory rejection, GitLab-style divergent paths (#30513), moved-registry frozen-lockfile, and both hint-present and hint-absent cases. The overlay_bunfig_install destructure was updated for the new url_auth field. The refactor of the same-origin check in for_tarball from an inline block to is_same_origin + tarball_credentials is behavior-preserving modulo the case-insensitive protocol/hostname comparison, which is correct for origin equality.

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

The 3cc622f fix addresses the earlier note — NetworkTask now stores the Authorization mode and missing_credentials_note returns empty for Tarball(NoAuthorization), with a regression test pinning it. No further issues found this run. Because this changes how credentials are selected and sent to registry/tarball hosts, a maintainer look at the security model (the tarball_credentials same-origin guard and UrlAuth::matches path-prefix semantics) is still worthwhile.

What was reviewed:

  • is_same_origin vs the removed inline check in for_tarball — now case-insensitive on protocol/hostname (spec-correct), still compares effective port; scope credentials still cannot cross origins.
  • UrlAuth::matches path-prefix boundary (/no not matching /no-deps/) and deepest-key precedence — both covered by tests.
  • fill_credentials_from_url_auth runs after --registry/env/--token are applied and skips scopes that already have credentials, so it does not override an explicit token.
  • The RequestKind::Tarball(NoAuthorization) arm and the URL-dependency 401 test cover the earlier finding.
Extended reasoning...

Overview

Adds npm-style by-request-URL resolution of .npmrc //host/path/ credential lines. The .npmrc loader (src/ini/lib.rs) now records every credential line into BunInstall.url_auth alongside the existing per-registry attachment; Options::load converts them to npm::registry::UrlAuth entries and, after all config sources are applied, backfills any credential-less scope from the entry matching its registry URL. NetworkTask::for_tarball replaces its inline same-origin check with Options::tarball_credentials, which returns the scope's credentials on same-origin and otherwise the deepest matching UrlAuth entry. A 401/403 hint is added to the two GET <url> - <status> sites in runTasks.rs, gated on the request's stored Authorization mode so github:/URL-tarball dependencies (which never carry registry credentials) don't get a misleading suggestion. 13 new tests in npmrc.test.ts exercise --registry, NPM_CONFIG_REGISTRY/BUN_CONFIG_REGISTRY, separate tarball hosts, path-prefix precedence, the GitLab #30513 shape, moved-registry lockfiles, and both the presence and absence of the 401 note.

Security risks

This is credential-routing code. The primary risk is sending a token somewhere the user didn't intend. The existing cross-origin guard (a hostile registry cannot direct a scope's credentials to an arbitrary dist.tarball host) is preserved: tarball_credentials still requires is_same_origin before returning scope credentials, and the fallback UrlAuth::find only ever returns credentials the user explicitly keyed to that host/path in .npmrc — the same model npm uses. is_same_origin is now case-insensitive on protocol/hostname (previously byte-equal); hostnames are case-insensitive per RFC so this is a correctness relaxation, not a weakening. UrlAuth::matches checks a directory-boundary prefix (next byte must be / or end), and entries with no sendable credential (e.g. email alone) are dropped so they can't shadow a shallower key. I did not find a path where credentials cross an origin the user didn't configure them for.

Level of scrutiny

High. This is a non-trivial behavioral change to authentication in bun install — it adds new code paths where an Authorization header is emitted, reworks the tarball same-origin gate into a shared helper, and encodes npm's nerf-dart matching semantics (host/port equality, scheme-default port for portless keys, deepest-path-wins). A maintainer should confirm the matching semantics are the ones Bun wants and that the case-insensitive origin comparison is intentional.

Other factors

The one issue raised on the previous revision (the 401 hint suggesting an .npmrc line that NoAuthorization tarballs would ignore) was fixed by threading the Authorization mode through NetworkTask → RequestKind::Tarball, and a regression test was added. Test coverage is thorough — each shape is asserted against a Bun.serve registry that 401s unless the exact expected header arrives, and the negative cases (rejected token → no hint; wrong-path key → no auth sent) are covered. CI is reported green on 177/179 lanes with the two remaining being infrastructure (darwin agent queue). Given the security-sensitive surface and the number of interacting config sources, this warrants a human sign-off rather than automated approval.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun fix conflicts

@robobun
robobun force-pushed the farm/55395501/npmrc-url-credentials branch from f15d359 to b36bf6a Compare August 15, 2026 09:15
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (on top of #38796) and force-pushed as b36bf6a. The only textual conflicts were the docs sentence and the end of npmrc.test.ts, where the IPv6 block from #38828 landed; both sides are kept. The Rust side merged without conflicts and stays additive around the new env var / --registry code. On the rebased tree the merged npmrc.test.ts (54 tests), config-precedence and redacted-config-logs pass with the debug build, and CI build 97798 on this head passed on all 179 jobs.

@robobun
robobun force-pushed the farm/55395501/npmrc-url-credentials branch from b36bf6a to 8816afc Compare August 15, 2026 17:21
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

main moved again right after that (#38183 added Scope::set_url next to the two helpers this PR adds), so rebased once more; now at 8816afc and GitHub reports it mergeable. #38183 helps this change: the scope URL the end-of-load lookup and the tarball same-origin check read is now the canonical WHATWG form. Re-verified on this tree with the debug build: npmrc.test.ts (54), the Registry URLs / spellings block of bun-install.test.ts that #38183 added (tarball Authorization on the normalized origin still goes through tarball_credentials), config-precedence, redacted-config-logs, and bun-audit; CI is running on the new head.

@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 re-reviewed after the fix in 3cc622f and the rebase; no further issues found. The Authorization mode is now stored on the NetworkTask and threaded through as RequestKind::Tarball(task.authorization), so the .npmrc hint is correctly suppressed for github:/URL tarball dependencies, and the new regression test pins it. Because this changes which hosts receive Authorization headers during install, a human sign-off on the credential-routing model would still be worthwhile.

What was reviewed:

  • tarball_credentials preserves the existing cross-origin guard (scope credentials only to scope's origin; url_auth only to hosts the user explicitly keyed) — a hostile registry still cannot redirect credentials.
  • UrlAuth::matches path check requires a directory boundary, not a string prefix; port-less keys default to the request scheme's port; entries without sendable credentials are dropped so they can't shadow a shallower key.
  • fill_credentials_from_url_auth runs after --token/BUN_CONFIG_TOKEN and skips scopes that already have credentials, so it can't downgrade an explicitly-set token.
Extended reasoning...

Overview

This PR adds npm-style by-URL resolution of .npmrc //host/path/ credential lines. It touches the .npmrc loader (src/ini/lib.rs), the config schema (src/options_types/schema.rs, src/api/lib.rs), package-manager options (PackageManagerOptions.rs), the tarball request path (NetworkTask.rs), the 401/403 error reporting (runTasks.rs), the registry types (npm.rs), the bunfig overlay (PackageManager.rs), docs, and adds 14 end-to-end tests in npmrc.test.ts against Bun.serve mock registries that assert the exact Authorization header received.

Security risks

This is credential-routing code: it decides which Authorization header (if any) is attached to manifest and tarball requests. The primary risk is sending a credential to a host the user did not configure it for. I checked the two guards that prevent this: (1) tarball_credentials returns the scope's credentials only when is_same_origin(tarball, scope.url) — the same protocol+hostname+effective-port check the pre-PR code used, now factored into a helper; (2) for any other origin it returns only a UrlAuth entry the user wrote for that exact host+port, with the path required to be a parent directory of the request path (the /no vs /no-deps test pins the boundary check). A registry-controlled dist.tarball therefore cannot exfiltrate the scope's token, and url_auth credentials go only where the user keyed them. fill_credentials_from_url_auth only fills scopes that have no credentials, so it cannot overwrite --token, BUN_CONFIG_TOKEN, or credentials embedded in a --registry URL.

Level of scrutiny

High — this is production credential handling in bun install. The implementation is additive (the existing load-time attachment and the same-origin guard are unchanged), well-tested (each shape has a mock-registry test that fails on the release build and asserts the exact header), and CI passed on all 179 jobs. But per the approval guidelines, changes to which remote hosts receive auth headers warrant a maintainer's sign-off rather than bot approval.

Other factors

My previous review's finding (the 401 hint being shown for NoAuthorization tarballs where the suggested .npmrc line would be ignored) was addressed: NetworkTask now stores authorization, write_init initializes it, both for_manifest and for_tarball set it, and missing_credentials_note returns empty for RequestKind::Tarball(NoAuthorization). The added regression test ("a 401 for a tarball dependency given by URL is reported without a suggestion") covers it. Jarred has engaged with the PR (requested conflict resolution, which was done) but has not yet approved.

The //host/path/:_authToken= lines of .npmrc were only ever attached to
registries declared in a config file, at the time the file was read. A
registry that arrives later (--registry, BUN_CONFIG_REGISTRY,
NPM_CONFIG_REGISTRY) never picked up its line, and a tarball served from
somewhere other than its registry's origin (a separate download host, a
project-level path next to an instance-level registry path, a lockfile
recorded against a registry that has since moved) was sent without
credentials even when that URL had a line of its own. All of these
failed with a bare "GET ... - 401".

Keep every //host/path/ line past .npmrc loading (BunInstall.url_auth)
and resolve them by URL the way npm does (same host and port, key path
is the request path or a parent of it, deepest key wins): once every
registry source has been applied, a scope still without credentials
takes the line matching its registry URL, and a tarball whose origin is
not its scope's origin takes the line matching the tarball URL. A
scope's own credentials are still never sent to another origin.

A 401/403 for a request that went out without credentials now names
the .npmrc line that would have authenticated it.
…egistry credentials

github: and URL tarball dependencies are fetched without registry
credentials, so suggesting a //host/ line for them describes a fix the
code would not apply. Remember what the request was allowed to send on
the NetworkTask and only suggest a line when one would have been used.
@robobun
robobun force-pushed the farm/55395501/npmrc-url-credentials branch from 8816afc to 4688f54 Compare August 17, 2026 23:01
@robobun

robobun commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main again, now on top of #38867 (tarball URL userinfo), head 4688f54, mergeable. That merge needed a real resolution in for_tarball: #38867 sends the URL's userinfo as basic auth when the scope's credentials do not apply, and this PR replaces the scope-only decision with the line lookup, so the two are now one chain: configured credentials (scope on its own origin, else a .npmrc line for the tarball host), else the URL's userinfo, else nothing, which is npm's order between configured credentials and URL auth. The lookup runs on the URL after the userinfo is removed. Follow-up commit 4688f54: since a tarball request can now carry a header the 401 note did not know about, the note now goes by whether any Authorization header was actually sent (NetworkTask::sent_authorization) instead of recomputing; two tests cover the interaction (a rejected userinfo header gets no suggestion; a line for the tarball host wins over userinfo). Details under "Rebase notes" in the description. Verified on this tree: npmrc.test.ts 56/56, #38867's userinfo block and #38183's spellings block in bun-install.test.ts, config-precedence, redacted-config-logs; clippy clean.

…n header was sent

Tarball requests can now also carry the URL's own userinfo, which the
note could not see because it recomputed which configured credentials
applied. Record on the NetworkTask whether any header went out and
decide from that; a .npmrc line for the tarball host still takes
precedence over the URL's userinfo, as configured credentials do in npm.
@robobun
robobun force-pushed the farm/55395501/npmrc-url-credentials branch from 4688f54 to 396ad6e Compare August 17, 2026 23:10

@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 re-reviewed after the fix in 3cc622f and the #38867 rebase — the NoAuthorization / sent_authorization guards on the 401 note look right now, and no new issues surfaced. Given this reworks which credentials go out on tarball requests and adds a new by-URL credential lookup, a human pass on the security boundary (tarball_credentials / UrlAuth::matches) is still worthwhile.

Checked: the same-origin guard for scope credentials is preserved (moved into tarball_credentials, component-wise compare kept); UrlAuth::matches enforces a directory boundary (/no does not match /no-deps/..., tested); a portless key falls back to the request scheme's default port; entries with no sendable credential are dropped so they cannot shadow a shallower key; the configured-credentials → URL-userinfo precedence matches npm and the existing cross-origin test in bun-install.test.ts is untouched.

Extended reasoning...

Overview

The PR adds npm-style by-URL resolution of .npmrc //host/path/ credential lines. bun_ini now records every such line into BunInstall.url_auth; Options::load converts them to UrlAuth entries and, at the end of load, backfills any credential-less scope whose registry URL matches one. NetworkTask::for_tarball now asks Options::tarball_credentials (scope credentials on the scope's own origin, else the deepest matching .npmrc line, else nothing) instead of doing its own same-origin check inline; the URL's own userinfo remains the last fallback. On 401/403, runTasks.rs appends a note naming the .npmrc key that would have authenticated the request, gated on the NetworkTask's recorded authorization mode and whether any header was actually sent. ~730 lines across 10 files, ~390 of which are 16 new integration tests against local Bun.serve registries that 401 unless the exact expected header arrives.

Security risks

This is credential-routing code. The two invariants that matter are (a) a scope's credentials never go to another origin, and (b) a .npmrc line's credentials only go to a URL its key covers. (a) is preserved: tarball_credentials returns scope only when is_same_origin(tarball, scope.url) (protocol + hostname case-insensitive + effective port), which is the same check that used to live inline in for_tarball. (b) is enforced by UrlAuth::matches: hostname case-insensitive, port equal (portless key = scheme default), and the key's pathname must be / or a directory prefix of the request path with a / boundary — the /no vs /no-deps test pins this. Entries whose Scope::from_api yields no token/auth are dropped in UrlAuth::from_api, so an email-only line at a deeper path cannot shadow a real token at a shallower one. The precedence chain in for_tarball (configured credentials beat URL userinfo) matches npm and what #38867 already did between scope and userinfo. The npm-auth-type: legacy header still accompanies configured credentials via count_auth/append_auth; sent_authorization is derived from whether the header buffer is non-empty, which is accurate for all three arms.

Level of scrutiny

High. This is production install-path code that decides which Authorization header to attach to outbound HTTP requests. A mistake here leaks credentials to the wrong host or breaks private-registry installs. The change is additive (the load-time attachment in ini/lib.rs is unchanged; fill_credentials_from_url_auth only fills scopes that have no credentials; --token / BUN_CONFIG_TOKEN set scope.token before the fill runs so they still win), and the test matrix is thorough, but the surface warrants a maintainer's eyes.

Other factors

My earlier finding (the note suggesting an .npmrc line for github: / URL-tarball dependencies that would never be consulted) was addressed: NetworkTask now records authorization and sent_authorization, RequestKind::Tarball carries both, and missing_credentials_note returns empty for NoAuthorization and for sent_authorization: true. Three tests pin these guards (URL dependency 401 → no note; rejected token → no note; rejected userinfo → no note). The #38867 merge collapsed the two header-building tails into store_header_buf, which is a clean refactor. write_init initializes both new fields. The overlay_bunfig_install destructure was updated so a bunfig-side url_auth (currently always empty — bunfig has no syntax for it) is extended rather than dropped, keeping the exhaustive-destructure invariant.

@alii

alii commented Aug 25, 2026

Copy link
Copy Markdown
Member

Closing in favour of a split of #33869. The request-time lookup here (--registry and env registries, tarballs on another host or path, #30513, moved-registry lockfiles) lands as its own PR on top of the .npmrc key walk, with the path scoping from #40058 folded in. Your test block is ported as is; the 401 hint messages are left out for now and can come back separately. Link to follow.

@alii alii closed this Aug 25, 2026
@robobun

robobun commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Sounds good. For whoever picks up the 401 hint later, the pieces are in this branch (farm/55395501/npmrc-url-credentials, head 396ad6e): Options::missing_credentials_note and RequestKind in PackageManagerOptions.rs, the two call sites in runTasks.rs, and the authorization / sent_authorization fields on NetworkTask. The hint only prints when a request went out without any Authorization header and a .npmrc line would have been used (never for github: or URL tarball dependencies, never after a header was rejected); the three tests for those guards are at the end of the ported block. The IPv6 key fix (#38828) and the tarball userinfo precedence (#38867) are already on main, so the ported block should not need changes for them.

@alii

alii commented Aug 25, 2026

Copy link
Copy Markdown
Member

Landed as #40425 (stacked on #40424 and #40423).

@robobun

robobun commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks. #40425 is the one to track for the request-time lookup (--registry and env registries, tarballs on another host or path, #30513, moved-registry lockfiles); the test block from this PR is ported there.

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.

auth tokens not sent to scoped-registry tarball URLs when token path diverges from registry URL path (regression from 1.3.10)

3 participants