Skip to content

publish: accept basic auth credentials from .npmrc and bunfig.toml - #38776

Open
robobun wants to merge 1 commit into
mainfrom
farm/a0b14173/publish-basic-auth
Open

publish: accept basic auth credentials from .npmrc and bunfig.toml#38776
robobun wants to merge 1 commit into
mainfrom
farm/a0b14173/publish-basic-auth

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Related: #18670 (the _auth / Nexus comments on it are this bug; the publishConfig.registry report itself is #38322). Redoes #26178, which was closed as stale when the Zig sources went away.

Problem

  • bun publish fails with error: missing authentication (run `bunx npm login`) and sends no request when the registry credentials are Basic auth rather than a bearer token. bun install and bun pm whoami accept the same configuration, and npm publish works with the same .npmrc. Same on 1.3.14 and current main.
  • Affected spellings, all of which bun install sends as Authorization: Basic: .npmrc //host/:_auth=, .npmrc //host/:username= + //host/:_password=, .npmrc registry=http://user:pass@host/, and bunfig.toml install.registry = { url, username, password }. Nexus and Artifactory setup snippets commonly hand out the first two.
  • Cause: the pre-flight check in PublishCommand::publish (src/runtime/cli/publish_command.rs:870) only looks at registry.token (plus userinfo left in the registry URL). Every spelling above ends up in Scope.auth (Scope::from_api, src/install/npm.rs:473), which the check ignores, so it returns NeedAuth before any request is built.
  • The request builders in the same file (construct_publish_headers, check_package_version_exists) already send Basic <auth> when token is empty, so only the pre-flight check was out of step.

Fix

  • The check also accepts a non-empty registry.auth. Nothing else changes: a registry with neither credential still gets the same error and still sends nothing.
  • Test: test/cli/install/bun-publish.test.ts, describe("basic auth"). Each of the four spellings publishes against an in-process registry that records every request, and the test asserts the registry saw exactly PUT /basic-auth-pkg with Authorization: Basic base64(alice:s3cret). A fifth case pins that no credentials still produces the error and no request. HOME/XDG_CONFIG_HOME point at an empty directory so the machine's own config cannot supply credentials.
  • USE_SYSTEM_BUN=1: the four credential cases fail with the error above, the no-credential case passes. bun bd test: all five pass. The whole of bun-publish.test.ts (44 tests) passes with the debug build.

Background

  • A Scope (npm::registry::Scope) is bun's resolved registry: URL plus credentials. token holds a bearer token (_authToken, bunfig token, NPM_CONFIG_TOKEN); auth holds the base64 user:password that goes into a Basic header. The .npmrc loader decodes _auth into username/password, and Scope::from_api re-encodes any username/password pair into auth, so all the Basic spellings converge on that one field.
  • Not changed here: a registry URL with userinfo given through --registry or npm_config_registry passes this check (the URL clause) but never turns into an Authorization header on any command; that is a separate pre-existing issue and is tracked separately. .npmrc/bunfig registry= strings with userinfo are split into username/password at parse time and are covered by this fix.
  • install: resolve .npmrc credentials by path-segment ancestor #33869 (npmrc path-walk rewrite) carries the same one-line change to this check; landing this first leaves it a trivial conflict to resolve there.

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/install/bun-publish.test.ts

`bun publish` returned "missing authentication" unless a bearer token was
configured, even though `_auth`, `username` + `_password`, a registry url
with userinfo, and bunfig's `{ username, password }` all populate
`Scope.auth`, and the publish request builder already sends them as
`Authorization: Basic`. Check `auth` alongside `token` before giving up.

Co-authored-by: Gavin Hailey <gavin.hailey@ibotta.com>
@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: 17 minutes

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: d5643cb6-7479-4242-9232-d27cfb5fd41d

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf3f36 and 16af988.

📒 Files selected for processing (2)
  • src/runtime/cli/publish_command.rs
  • test/cli/install/bun-publish.test.ts

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

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 PM PT - Aug 14th, 2026

@robobun, your commit 16af988 has some failures in Build #96994 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38776

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

bun-38776 --bun

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The diff is green on every CI lane that ran; the build's only red is two macOS jobs that never executed.

Reproduced on the released binary (USE_SYSTEM_BUN=1) with an in-process registry recording every request: bun publish configured with .npmrc _auth, .npmrc username + _password, .npmrc registry=http://user:pass@host/, or bunfig.toml { url, username, password } exits with missing authentication and the registry receives nothing, while _authToken publishes. With this branch all four send PUT /basic-auth-pkg with Authorization: Basic <base64 user:pass>, and a registry with no credentials still gets the same error and no request.

Tests: test/cli/install/bun-publish.test.ts describe("basic auth"), 4 cases fail before / pass after, 1 negative case passes both ways. Whole file passes with the debug build.

CI (build 96994, finished): 177 of 179 jobs passed. Every test failure in the annotations passed on retry or when re-run alone, and none of them is in publish or in this test file. The other two jobs are both darwin 14 aarch64 - test-bun; they were never picked up by an agent and expired on all four attempts because the test-darwin aarch64 previous-tier lane has a backlog of roughly 300 jobs across all builds. Nothing in this diff is platform-specific. I have not re-triggered the build, since while that backlog persists a re-run would end the same way; once it drains, retrying just those two jobs from Buildkite is enough.

Bot reviews: CodeRabbit hit its rate limit (no review); the other automated review found nothing to change. No open review threads.

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

LGTM — one-line fix bringing the pre-flight auth check in line with the header builders that already send Basic <auth>.

What was reviewed:

  • Confirmed construct_publish_headers and check_package_version_exists already branch on registry.auth, so only the gate was out of step; no other NeedAuth sites exist.
  • Tests: hermetic in-process registry on port: 0, tempDir with isolated HOME/USERPROFILE/XDG_CONFIG_HOME, exact toEqual on the recorded request+header, and the no-credential case still fails with no request sent.
  • Checked that loosening the check adds no new surface — .auth is only populated from user config that bun install already honors.
Extended reasoning...

Overview

One line added to PublishCommand::publish in src/runtime/cli/publish_command.rs: the pre-flight NeedAuth check now also passes when registry.auth (base64 user:password) is non-empty, not just registry.token. A new describe.concurrent("basic auth") block in test/cli/install/bun-publish.test.ts covers four Basic-auth config spellings (.npmrc _auth, .npmrc username+_password, .npmrc registry= with userinfo, bunfig install.registry.{username,password}) plus a negative case that still refuses without credentials.

Security risks

None introduced. This relaxes a client-side pre-flight guard, but only to match what the request builders in the same file already do — construct_publish_headers and check_package_version_exists already emit Authorization: Basic <auth> when token is empty and auth is not. Scope.auth is only populated from the user's own .npmrc/bunfig, and bun install already sends it. No credential leakage: the tests assert the exact header on the wire, and the existing href_without_auth() paths for the summary line and dist.tarball are unchanged.

Level of scrutiny

Low. The production change is a single boolean clause aligning one check with two sibling call sites in the same file. The mechanism is fully explained in the PR description and verified against src/install/npm.rs (Scope.auth is the base64 user:password field). No CODEOWNERS coverage on either file.

Other factors

Test quality is high per the repo's review rules: using for server and tempdir, port: 0, describe.concurrent for independent subprocess tests, {...env, HOME/USERPROFILE/XDG_CONFIG_HOME} pointing at an empty dir so ambient config cannot supply credentials, exact-value assertions (requests.toEqual([...]) with the precise Basic header), stderr checked before exit code, and the negative contract (no credentials → same error, zero requests) pinned. The PR states USE_SYSTEM_BUN=1 fails the four credential cases and the debug build passes all 44 tests in the file. No prior human reviews or outstanding comments.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

I opened #38782 for the same bug a few minutes after this (same one-line change to the check in publish()), and am closing it in favor of this PR. Branch for reference: farm/1498ce1a/publish-basic-auth (main...farm/1498ce1a/publish-basic-auth).

Two things from that branch that may be worth lifting here:

  • Test cases for a bunfig [install.scopes] registry with username + password (the configuration in Bun Publish doesn't support token based login (Azure Devops) #17531, which this PR fixes and could close: the scoped registry is picked by scope_for_package_name rather than being the default scope) and for bun publish <tarball>, which reaches the same check through from_tarball_path.
  • The two lifecycle scripts > should run in order tests in bun-publish.test.ts take about 5s on a debug build here (six child bun processes each); when they time out, the runner's dangling-process cleanup kills the Verdaccio shared by the rest of the file and everything after them fails. Giving those two tests a 30s timeout made the full file pass under the debug build in that environment.

Jarred-Sumner added a commit that referenced this pull request Aug 15, 2026
… URLs (#38796)

### Problem
- A registry URL with credentials in it (`http://user:pass@host/`, or
`http://:token@host/`) passed as `--registry`, or through
`BUN_CONFIG_REGISTRY` / `NPM_CONFIG_REGISTRY` / `npm_config_registry`,
sends no `Authorization` header. Against a registry that logs the header
(bun 1.4.0, also `main`):
- `bun publish --registry http://alice:s3cret@127.0.0.1:PORT/` publishes
with exit 0 and the registry sees `PUT /ui-pkg authorization=null`; same
with `npm_config_registry=...`.
- `bun pm view somepkg --registry http://alice:s3cret@...` and `bun
install --registry http://alice:s3cret@...`: `GET /somepkg
authorization=null`.
- The same URL as `registry=` in `.npmrc` sends `Authorization: Basic
YWxpY2U6czNjcmV0` on the manifest and the tarball. npm 11 sends Basic
for the flag and the env var too (`minipass-fetch` passes URL userinfo
as node's `auth` option).
- Because the credentials stay inside the stored registry URL, they end
up in the manifest URL and `bun install` prints them on failure: `error:
GET http://alice:s3cret@127.0.0.1:PORT/somepkg - 401`.
- Cause: the config file string forms go through the userinfo splitter
(`parse_registry_url_string_impl`, `src/api/lib.rs`), which moves
`user:pass` into `NpmRegistry.username`/`password` (or `token`) and
strips it from the URL. The flag and env var paths did not:
- `src/install/PackageManager/PackageManagerOptions.rs`, registry env
vars: built `Api::NpmRegistry { url: <raw value> }` and called
`Scope::from_api`, which does not read URL userinfo.
- same file, `--registry`: stored the raw href into `self.scope.url`
directly.
- `src/runtime/cli/publish_command.rs` `publish()`: the "missing
authentication" check accepted userinfo in the URL as credentials, so
publish went ahead and sent the PUT without a header. The HTTP client
(`src/http`) never reads URL userinfo.
- Found while looking at the publish pre-flight check (#38776), not from
a user report; the effects above are what the repro shows.
- Also fixed here (folded in from #38764, cdca4ff):
`BUN_CONFIG_TOKEN` / `NPM_CONFIG_TOKEN` were applied before
`--registry`, so the host-mismatch check in the `--registry` block
dropped them too and `BUN_CONFIG_TOKEN=... bun install --registry <other
host>` failed with a 401.

### Fix
- `NpmRegistry::from_url` (`src/options_types/schema.rs`) is the
splitter, moved next to the type so `bun_install` can call it;
`bun_api`'s `parse_registry_url_string_impl` (the bunfig and `.npmrc`
entry point) delegates to it, so there is one parse for every registry
URL string. `bun_api` loses its now unused `bun_url` dependency, and
`bun_ini`'s private `has_credentials` helper is replaced by the method
the new call sites needed.
- The registry env vars and `--registry` run their value through
`from_url`:
- credentials in the URL: the scope is rebuilt with `Scope::from_api`,
exactly as for a config file registry string, so `Scope.auth` (Basic) or
`Scope.token` (Bearer) is populated and the stored URL no longer
contains the userinfo. They replace whatever the config files set up for
that registry, which is the rule `.npmrc` already applies (a `registry=`
line with userinfo wins over `//host/:_authToken=`:
`apply_registry_auth` in `src/ini/lib.rs` skips registries that already
have credentials).
- no credentials in the URL: unchanged (env var keeps a same-host token,
`--registry` keeps same-origin credentials and drops them otherwise);
`from_url` returns the input href unchanged in that case.
- The `--registry` block now runs before the token env vars are read
(cdca4ff), so the env token survives a registry switch and layers on
top of both overrides. Resulting order, lowest to highest: config files,
credentials in the `--registry` / registry env var URL,
`BUN_CONFIG_TOKEN` / `NPM_CONFIG_TOKEN`, `--token`.
- `publish()` also accepts `Scope.auth` in its pre-flight check, which
is where the Basic credentials land. Same one-line hunk as #38776 (the
config file side of that check, reported in #17531); it merges in either
order and closing those reports is left to #38776.
- Why the split belongs at the string entry points rather than only
inside `Scope::from_api`: the loaders need the split result before a
scope exists. `.npmrc` loading decides whether host-keyed `_authToken`
entries apply by looking at the parsed registry's credentials, and the
two override layers decide whether to carry configured credentials over
the same way. So the parse-time split is required regardless, and this
change wires in the two string entry points that were missing from it.
Two ways a URL with userinfo can still reach `from_api` unsplit remain:
the bunfig object form `registry = { url = "http://u:p@host/" }`
(documented with separate `username`/`password`/`token` fields) and a
`registry = "$VAR"` whose value carries userinfo, since `$VAR` is only
expanded inside `from_api`. Both need a split inside `from_api` itself,
which is tracked separately and kept out of this change because #38183
is rewriting that function's tail; this is also why the publish check
keeps its URL userinfo clause.
- Tests (every new case fails on the released bun and passes with this
change):
- `test/cli/install/config-precedence.test.ts`: `bun install` with
`--registry` and with each of the three env vars, user:pass form (Basic
arrives on every request and verdaccio accepts it,
`@needs-auth/test-pkg` installs, password not in stderr) and `:token@`
form (Bearer); URL credentials replace a same-host `.npmrc` `_authToken`
(flag and env var); from cdca4ff: `BUN_CONFIG_TOKEN` beats a token in
the `--registry` URL, `BUN_CONFIG_TOKEN` / `NPM_CONFIG_TOKEN` apply to
the `--registry` registry, and `--registry` drops the `.npmrc` token for
the previous host but keeps `BUN_CONFIG_TOKEN`.
- `test/cli/install/bun-publish.test.ts`, `credentials in the registry
url`: `--registry` user:pass (PUT carries Basic, summary line has no
userinfo), `--registry` `:token@` (Bearer; previously "missing
authentication"), the three env vars.
- `test/cli/install/redacted-config-logs.test.ts`: the `pm view` test
that asserted the password is masked in the 401 line now asserts Basic
is sent and the printed URL has no userinfo (the masked form depended on
this bug); a verbose `fetch()` case keeps the URL password masking of
`redacted_npm_url` covered.
- Also run with the debug build: all of `bun-publish.test.ts`,
`config-precedence.test.ts`, `redacted-config-logs.test.ts`,
`npmrc.test.ts`, the `.env` registry override and env var priority tests
in `bun-install-registry.test.ts`; `cargo clippy` on the touched crates;
the source lints.
- Overlap with open work: #37095 adds the same `from_url` while deleting
`bun_api` and `schema.rs` (whichever lands second rebases onto the
other's copy; the call sites are the same either way); #38183 touches
the same `--registry` block for URL normalization.

### Background
- `Api::NpmRegistry` is the parsed config for one registry: `url` plus
optional `username`/`password`/`token`, produced by bunfig and `.npmrc`
loading. `Scope::from_api` (`src/install/npm.rs`) turns it into a
`Scope`, the resolved registry the package manager uses: the URL,
`token` (sent as `Authorization: Bearer`) and `auth` (base64
`user:password`, sent as `Authorization: Basic`; `user` keeps the plain
pair for `pm whoami`). Requests only ever look at `Scope.token` /
`Scope.auth`; a URL's userinfo is never sent by the HTTP client.
- `Options::load` builds the default scope in layers: config files, the
registry env vars, `--registry`, the token env vars, then the remaining
CLI flags (`--token`). The registry env vars and `--registry` are the
two layers that accept a registry URL string without going through the
config parsers, which is why they are the ones changed.
- `URL::href_without_auth()` is the existing helper the splitter uses to
rebuild the URL without its userinfo. For a registry with no path it
yields `http://host//`; every consumer strips or collapses the extra
slash (this pre-dates the change and applies to the `.npmrc` form
today), so it is left alone here.

<details>
<summary>Probe against a mock registry (released bun 1.4.0 vs this
branch)</summary>

```
# released
bun publish --registry http://alice:s3cret@127.0.0.1:PORT/        -> exit 0,  REQ PUT /ui-pkg authorization=null
npm_config_registry=http://alice:s3cret@...  bun publish            -> exit 0,  REQ PUT /ui-pkg authorization=null
bun pm view somepkg version --registry http://alice:s3cret@...      ->          REQ GET /somepkg authorization=null
bun install --registry http://alice:s3cret@...  (registry answers 401)
    error: GET http://alice:s3cret@127.0.0.1:PORT/somepkg - 401
.npmrc registry=http://alice:s3cret@...  bun install                 ->          REQ GET /somepkg authorization=Basic YWxpY2U6czNjcmV0
npm view somepkg --registry http://alice:s3cret@...                 ->          REQ GET /somepkg authorization=Basic YWxpY2U6czNjcmV0

# this branch
bun publish --registry http://alice:s3cret@...                      -> Registry: http://127.0.0.1:PORT/   REQ PUT /ui-pkg authorization=Basic YWxpY2U6czNjcmV0
BUN_CONFIG_REGISTRY=http://alice:s3cret@...  bun publish            ->                                    REQ PUT /ui-pkg authorization=Basic YWxpY2U6czNjcmV0
bun publish --registry http://:tok123@...                           ->                                    REQ PUT /ui-pkg authorization=Bearer tok123
bun pm view somepkg version --registry http://alice:s3cret@...      ->                                    REQ GET /somepkg authorization=Basic YWxpY2U6czNjcmV0
bun pm whoami --registry http://alice:s3cret@...                    -> alice
bun publish --registry http://alice@...  (no password)              -> error: missing authentication (unchanged)
bunfig install.registry = "http://alice:s3cret@..." (string form)   -> still Basic, via the delegating parser
```
</details>

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants