Skip to content

url: emit a single slash in href_without_auth() for root-path registry URLs - #38812

Open
robobun wants to merge 5 commits into
mainfrom
farm/112debf9/href-without-auth-root-slash
Open

url: emit a single slash in href_without_auth() for root-path registry URLs#38812
robobun wants to merge 5 commits into
mainfrom
farm/112debf9/href-without-auth-root-slash

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A registry whose URL carries credentials that bun strips out (.npmrc registry=http://alice:s3cret@127.0.0.1:PORT/, the same string in bunfig.toml, or the documented url = "http://host/_authToken=TOKEN" suffix form) is stored as http://127.0.0.1:PORT//. The same registry configured without credentials is stored as written, with one slash.
  • The stored href is printed as-is by bun pm whoami (failed to authenticate with registry 'http://127.0.0.1:PORT//') and quoted in the for_manifest diagnostics (src/install/NetworkTask.rs:480-554), and everything that builds a request from it inherits the extra slash: today the manifest URL (error: GET http://127.0.0.1:PORT//somepkg - 401) and the tarball URLs synthesized by the package-lock.json migration (http://127.0.0.1:PORT//foo/-/foo-1.0.0.tgz, written into bun.lock).
  • Cosmetic on the wire: the HTTP client parses request URLs with bun_url::URL::parse, which collapses a leading // in the pathname, so the server still receives GET /somepkg. Found by inspection while working on install: send credentials embedded in --registry and registry env var URLs #38796 (which leaves this helper alone), not from a user report.
  • Cause: URL::href_without_auth() (src/url/lib.rs) writes <proto>://<host> + / + trim(pathname, "/") + / unconditionally, so a root pathname (trimmed to an empty string) yields //. Registries with a path (/npm/) come out right. The Zig implementation used the same format string, so this is not a regression.

Fix

  • When the trimmed path is empty, stop after the first /. The result now always ends in exactly one slash: http://host/, http://host:8080/, http://host/npm/.
  • Correct because http://host/ is what the same registry is stored as when nothing needs stripping, and it is the only producer of the // form: both credential-stripping callers (src/api/lib.rs for .npmrc/bunfig.toml strings, Scope::from_api in src/install/npm.rs for the _authToken=/_auth=/:username=/:_password= suffixes) store its return value, and install: send credentials embedded in --registry and registry env var URLs #38796 still calls it after moving the first caller.
  • Nothing else changes: the consumers that neither print nor build on the raw href (url_hash, tarball build_url, the manifest cache header, url_is_under_registry, bun publish, bun pm view, bun audit) already strip trailing slashes before use. The one persisted effect is the @@<host>__<hash> cache folder name, used for hostnames longer than 32 bytes, which hashes the href: such a registry configured with credentials in its URL gets a fresh cache folder once. (install: preserve last registry path segment when URL has no trailing slash #36294 and install: canonical registry URL in Scope; redact secrets in bun audit registry URLs #38183 deliberately leave the stored href as configured; this change rewrites only the value this helper already rewrites.)
  • Relationship to the other changes in this area: install: canonical registry URL in Scope; redact secrets in bun audit registry URLs #38183 (merged, this branch is rebased on it) passes the stored href through the WHATWG serializer, which keeps // (new URL("http://h//").href is http://h//), so the 7 failing tests below still fail on current main without this change. The open install: preserve last registry path segment when URL has no trailing slash #36294 normalizes the join base in for_manifest and Fix package-lock.json migration for scope registries configured without a trailing slash #38698 fixes the migration concatenation, so each of those removes the // from the request it builds regardless of what is stored; neither changes the stored value, whoami, or the quoted diagnostics, which is what this change fixes. None of them touches src/url/lib.rs; this composes with all of them. If the stored href should get a trailing-slash policy of its own, Scope::set_url from install: canonical registry URL in Scope; redact secrets in bun audit registry URLs #38183 is the place, as a follow-up.
  • Test: test/cli/install/npmrc.test.ts, new registry URL with embedded credentials block. The loadNpmrc rows assert the stored default_registry_url for the user:pass@ and :token@ forms (root path with and without a trailing slash or port, plus path forms that must stay unchanged); the bun pm whoami cases assert the stored href that Scope::from_api produces for the _authToken= suffix form (root and /npm/) and that the token is still sent. Both assert the stored value directly, so they keep exercising this change once install: preserve last registry path segment when URL has no trailing slash #36294 lands. The bun install case pins today's GET line for the .npmrc userinfo form and checks the request path and Basic header. 7 of the 11 new tests fail on bun 1.4.0 and on a debug build of current main (//), all pass with this change.
  • Also run with the fix: the rest of npmrc.test.ts, redacted-config-logs.test.ts, config-precedence.test.ts, the whoami tests in bun-install-registry.test.ts, and the two bun-publish.test.ts tests that print or embed the registry href.

Background

  • bun_url::URL is bun's lenient zero-copy URL scanner: each field is a slice of the input, and pathname defaults to / when the input has no path. href_without_auth() re-serializes such a URL without its user:pass@ part and is only used when bun removes credentials from a configured registry URL; the resulting string becomes the registry's stored href (Scope.url).
  • NetworkTask::for_manifest builds manifest URLs with bun_url::join, the WHATWG parser. Like new URL("pkg", "http://host//"), it keeps the double slash, which is how the stored value reached the GET line.
Output on bun 1.4.0 (mock registry answering 401)
.npmrc registry=http://127.0.0.1:PORT/                      -> error: GET http://127.0.0.1:PORT/somepkg - 401
.npmrc registry=http://alice:s3cret@127.0.0.1:PORT/         -> error: GET http://127.0.0.1:PORT//somepkg - 401
.npmrc registry=http://:s3cret@127.0.0.1:PORT/              -> error: GET http://127.0.0.1:PORT//somepkg - 401
.npmrc registry=http://alice:s3cret@127.0.0.1:PORT/npm/     -> error: GET http://127.0.0.1:PORT/npm/somepkg - 401
bunfig registry = "http://alice:s3cret@127.0.0.1:PORT/"     -> error: GET http://127.0.0.1:PORT//somepkg - 401
bunfig url = "http://127.0.0.1:PORT/_authToken=abc"         -> error: GET http://127.0.0.1:PORT//somepkg - 401
bunfig url = "http://127.0.0.1:PORT/_authToken=abc", bun pm whoami against a registry answering {} ->
    error: failed to authenticate with registry 'http://127.0.0.1:PORT//'

In every install case the mock server logged GET /somepkg; whoami requested /-/whoami with Bearer abc.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

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

Next review available in: 18 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: 3cf4c811-94df-46cc-8259-62dc947db6d4

📥 Commits

Reviewing files that changed from the base of the PR and between aa47e37 and 0ae2bd4.

📒 Files selected for processing (2)
  • src/url/lib.rs
  • test/cli/install/npmrc.test.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8af6bdf5-bd7c-4acc-8714-498373198df8

📥 Commits

Reviewing files that changed from the base of the PR and between a5c86ae and aa47e37.

📒 Files selected for processing (2)
  • src/url/lib.rs
  • test/cli/install/npmrc.test.ts

Walkthrough

Changes

Registry URL normalization

Layer / File(s) Summary
Normalize URL path formatting
src/url/lib.rs
href_without_auth now preserves exactly one trailing slash, including for root paths.
Validate registry credential handling
test/cli/install/npmrc.test.ts
Tests cover Basic and Bearer authentication, credential removal, trailing-slash normalization, and registry path preservation.

Possibly related PRs

  • oven-sh/bun#38001: Covers related registry redirect credential handling and URL normalization.
  • oven-sh/bun#38322: Covers related npm registry URL normalization and credential handling.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary fix: emitting one slash for root-path registry URLs.
Description check ✅ Passed The description explains the problem, fix, scope, and verification results, although it does not use the template headings exactly.

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

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:05 AM PT - Aug 15th, 2026

@robobun, your commit 0ae2bd4 has some failures in Build #97938 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38812

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

bun-38812 --bun

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed (rebased on main after #38183 merged), waiting on CI.

Reproduced on bun 1.4.0 with a Bun.serve mock registry: .npmrc registry=http://alice:s3cret@127.0.0.1:PORT/ makes bun install print error: GET http://127.0.0.1:PORT//somepkg - 401 (the server receives GET /somepkg), and the bunfig.toml url = "http://127.0.0.1:PORT/_authToken=x" form makes bun pm whoami print failed to authenticate with registry 'http://127.0.0.1:PORT//'. With a path (/npm/) or without credentials the stored URL has one slash.

The new tests in test/cli/install/npmrc.test.ts (registry URL with embedded credentials) assert the stored href through loadNpmrc and bun pm whoami, plus one bun install case for the GET line. 7 of the 11 fail on 1.4.0 and on a debug build of current main (#38183's WHATWG serialization keeps the //); all pass with this branch. The rest of the file, redacted-config-logs, config-precedence, the whoami tests and the two publish tests that use the registry href also pass with the change. The open #36294 and #38698 are discussed in the description; neither touches this helper.

@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 — a 4-line guard so href_without_auth() emits http://host/ instead of http://host// for root-path registry URLs, with thorough test coverage. The inline nit about the undrained stdout pipe is a harness-convention consistency point, not a blocker (a failing single-dep install writes far under 64KB to stdout).

Extended reasoning...

Overview

The Rust change in src/url/lib.rs is 4 lines: href_without_auth() previously wrote / + trimmed-path + / unconditionally, so a root pathname (which trims to "") produced a trailing //. The fix wraps the path + second / in if !path.is_empty(), so the result always ends in exactly one slash. The doc comment is updated to match. The non-empty-path branch is byte-identical to before.

The test change adds a describe.concurrent block to test/cli/install/npmrc.test.ts with 8 parametrized loadNpmrc assertions (user:pass@ and :token@ forms, root vs. path, with/without port and trailing slash) and 3 integration tests that spawn bun install against a local 401 mock server, asserting the printed GET line, the request path the server received, and the Authorization header.

Security risks

None. This is display/serialization of the stored registry href. The actual HTTP request path was already correct (URL::parse collapses leading // in the pathname). Credentials handling is unchanged — the tests confirm the Basic/Bearer header is still sent. No new user input reaches this code path.

Level of scrutiny

Low-to-medium. The Rust change is mechanically obvious and touches a single serialization helper. I verified all four call sites of href_without_auth():

  • src/api/lib.rs:47,52 and src/install/npm.rs:503 — these store the result as the registry URL; they are the intended fix targets.
  • src/runtime/cli/publish_command.rs:895,1500 — both pass the result through strings::without_trailing_slash, which loops off all trailing slashes, so these sites produce identical output before and after.

The PR description's consumer audit (url_hash, tarball build_url, manifest cache header, url_is_under_registry) and the acknowledged one-time cache-folder-hash side effect for >32-byte hostnames are consistent with what I see.

Other factors

The one finding from the bug-hunting run is a nit: the new install() helper pipes stdout but only awaits stderr + exited. This is a real harness-convention inconsistency (neighboring helpers in the same file drain both), but a failing bun install for one dependency writes ~dozens of bytes to stdout, so no deadlock risk in practice. It doesn't block approval. Test coverage is strong — the test.each matrix plus the three end-to-end cases cover both entry points (.npmrc userinfo and bunfig.toml _authToken= suffix) and include a path-form negative case to guard against regression.

Comment thread test/cli/install/npmrc.test.ts Outdated
Comment thread src/url/lib.rs Outdated

@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 — the earlier nit (undrained stdout pipe in the install() helper) is addressed in aa47e37, and the 4-line href_without_auth() change is straightforward.

What was reviewed:

  • Traced all four href_without_auth() callers (src/api/lib.rs, src/install/npm.rs, both publish_command.rs sites) — the publish sites strip trailing slashes before use, so output is unchanged there.
  • New tests are hermetic (port: 0, local Bun.serve, per-dir cache), drain both pipes, and cover root/path/port and both .npmrc userinfo + bunfig.toml _authToken= entry points.
  • The comment-cop bot flag on lib.rs:395 targets a doc comment stating the function's output contract, not a workaround justification — looks like a false positive.
Extended reasoning...

Overview

Two files: a 4-line change in src/url/lib.rs (href_without_auth(): skip appending path + '/' when the trimmed pathname is empty, so a root-path registry serializes as http://host/ instead of http://host//) plus a 2-line doc-comment update, and a new describe.concurrent block in test/cli/install/npmrc.test.ts with 8 test.each parse-only cases and 3 subprocess integration tests against a local 401 mock registry.

Security risks

None. The function already stripped user:password from the URL; this change only affects how many trailing slashes appear when the pathname is root. No new user input is parsed, no auth handling changes — the tests confirm the same Authorization header is still sent.

Level of scrutiny

Low. The Rust change is a trivial conditional guard on an already-computed value; the invariant ("result always ends in exactly one slash") is easy to verify by inspection. Vec::with_capacity now over-reserves by 1 byte in the empty-path case, which is harmless. I checked all four call sites: src/api/lib.rs:47,52 and src/install/npm.rs:503 store the href as the registry URL (the fix target), and both publish_command.rs sites call without_trailing_slash on the result first, so their output is unchanged.

Other factors

  • My prior inline nit (piped stdout not drained) was fixed in commit aa47e37 — the helper now reads proc.stdout.text() in the same Promise.all.
  • The github-actions comment-cop bot flagged the doc comment at lib.rs:395. That comment is a 3-line doc string describing the function's output format ("…so the result always ends in exactly one slash"), not an inline justification for a workaround; the code itself is the fix, not a workaround. I read this as a heuristic false positive rather than an actionable review request.
  • Tests follow harness conventions: tempDir, bunEnv spread, port: 0, await using for server/proc, exit-code asserted last, no external network, per-test BUN_INSTALL_CACHE_DIR. The PR description confirms 7/11 new tests fail on 1.4.0 (the // cases) and all pass with the change.

Comment thread src/url/lib.rs Outdated
Comment thread src/url/lib.rs Outdated

@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 — the earlier nit (drain stdout in the install() helper) and the comment-cop feedback (doc comment is now one line) are both addressed.

Checked: href_without_auth() now emits exactly one trailing / when the trimmed path is empty; verified all four call sites (src/api/lib.rs, src/install/npm.rs, and both in publish_command.rs) — the two publish sites already without_trailing_slash() the result, so behavior there is unchanged. Tests cover the user:pass@ and :token@ forms with and without a port/path via loadNpmrc, plus end-to-end .npmrc and bunfig.toml installs against a local 401 server asserting the printed URL, request path, and that credentials are still sent.

Extended reasoning...

Overview

The PR fixes URL::href_without_auth() in src/url/lib.rs so a root-path registry URL serializes as http://host/ instead of http://host//. The Rust change is 4 lines: wrap the trailing path + / append in if !path.is_empty(). The doc comment is condensed to one line. test/cli/install/npmrc.test.ts gains a describe.concurrent block with 8 loadNpmrc table cases and 3 subprocess tests running bun install against a local 401-answering Bun.serve mock.

Security risks

None. The function's purpose is to strip userinfo from a registry URL; the change only affects how many trailing slashes appear when the pathname is root. Credentials are still split off correctly (the new tests assert default_registry_username/password/token and the Authorization header the mock server receives). No auth, crypto, or permission code touched.

Level of scrutiny

Low. The Rust change is a trivially correct guard on an empty string. I checked every caller of href_without_auth() (grep across the repo): src/api/lib.rs:47,52 and src/install/npm.rs:503 store the result as the registry href (the paths the tests exercise); both publish_command.rs sites strip trailing slashes before use, so their output is unchanged. The one documented side effect — the @@<host>__<hash> cache folder name changing for >32-byte hostnames with embedded credentials — is benign (fresh cache once). The bug was cosmetic to begin with: the actual HTTP request already collapsed the //.

Other factors

Since my prior review, three follow-up commits landed: aa47e377 added proc.stdout.text() to the Promise.all in the install() helper (my nit), and 61a496b4 + 432adca1 shortened the href_without_auth doc comment to a single line (the comment-cop bot's feedback). The tests follow harness conventions: tempDir, bunEnv spread, port: 0, await using for server/proc, all pipes drained, test.each for the matrix, exit code asserted last. The PR description enumerates every consumer of the stored href and confirms which ones strip trailing slashes; it also names the adjacent test files that were re-run with the fix. Straightforward, well-tested, and all prior feedback resolved.

@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 — the earlier nit (undrained stdout: "pipe") and the comment-cop warnings are both addressed.

What was reviewed:

  • href_without_auth(): skipping the path + '/' append when the trimmed path is empty; non-empty-path output is byte-identical.
  • All four callers (api/lib.rs, install/npm.rs, and the two in publish_command.rs): the publish sites already without_trailing_slash the result, so only the two store-verbatim sites see a change, which is the intended fix.
  • New tests: cover root/port/path variants for both the .npmrc userinfo form and the bunfig _authToken= suffix form; run() now drains stdout.
Extended reasoning...

Overview

Two files: a 4-line change to URL::href_without_auth() in src/url/lib.rs (skip appending the trimmed path + trailing slash when the path trims to empty, so root-path registry URLs serialize as http://host/ instead of http://host//), plus a shortened doc comment; and a new describe.concurrent block in test/cli/install/npmrc.test.ts with 11 assertions covering the .npmrc user:pass@ / :token@ forms via loadNpmrc, the bunfig _authToken= suffix form via bun pm whoami, and the end-to-end bun install GET line + Authorization header.

Security risks

None. The function only reformats an already-parsed registry URL string when credentials are being stripped out; no new parsing, no auth logic changes. The tests confirm the credential (Bearer tok, Basic ...) is still sent unchanged.

Level of scrutiny

Low-to-medium. The Rust change is a trivially-inspectable conditional guard around two existing lines; the non-empty-path branch produces byte-identical output to before. I checked all four call sites: src/api/lib.rs:47,52 and src/install/npm.rs:503 store the result verbatim as the registry href (the sites the fix targets), while both publish_command.rs uses pass through strings::without_trailing_slash first and are therefore unaffected either way. The Vec::with_capacity hint is now over by 1 byte in the empty-path case, which is harmless.

Other factors

My prior review left one nit (the run() helper piped stdout without draining it) — fixed in aa47e37. The comment-cop bot flagged the multi-line doc comment three times — reduced to one line in 432adca. All inline threads are resolved. Tests follow harness conventions (tempDir, bunEnv spread, port: 0, await using for server/proc, three-way Promise.all draining both pipes, exit-code asserted last, test.each for the matrix, describe.concurrent for independent subprocess tests). The PR description documents that 7/11 new tests fail on 1.4.0 and pass with the change, and that the surrounding suites (redacted-config-logs, config-precedence, whoami, publish) still pass.

@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 — the empty-path guard in href_without_auth() is a minimal, obviously-correct fix, and the earlier nit (undrained stdout) and comment-cop feedback have both been addressed.

What was reviewed:

  • Traced all four href_without_auth() call sites (src/api/lib.rs, src/install/npm.rs, two in publish_command.rs) — non-empty-path output is byte-identical, empty-path drops from // to /; the publish callers either append their own / or strip trailing slashes, so no regression there.
  • New tests cover both credential forms (user:pass@ and :token@), root vs. path vs. port variants, and assert both the stored href and the actual request path/auth header via a local mock registry.
  • Confirmed the run() helper now drains stdout in the same Promise.all, and the doc comment is down to one line.
Extended reasoning...

Overview

Two files touched: a 4-line guard in src/url/lib.rs::URL::href_without_auth() that skips appending <path>/ when the trimmed pathname is empty (so a root-path registry serializes as http://host/ instead of http://host//), plus a new describe.concurrent block in test/cli/install/npmrc.test.ts with 11 cases covering loadNpmrc, bun pm whoami, and bun install against a local mock registry.

Security risks

None. href_without_auth() exists to strip userinfo from a configured registry URL; this change only affects how many trailing slashes the credential-free result carries when the path is empty. Credential extraction (url.username/url.password) is unchanged, and the new tests assert the extracted username/password/token and the Authorization header sent on the wire.

Level of scrutiny

Low-to-medium. The Rust change is a trivial conditional whose non-empty-path branch is byte-for-byte identical to the old unconditional code; only the empty-path case changes, and it now matches what the same registry stores when no credentials need stripping. I grepped for all call sites: the two the description names plus two in publish_command.rs — one wraps the result in without_trailing_slash (unaffected), the other appends its own / in a format string (a pre-existing quirk this PR strictly improves for the root case and leaves unchanged otherwise). The one persisted side effect (a fresh @@<host>__<hash> cache dir for >32-byte hostnames configured with URL-embedded credentials) is called out in the description and is benign.

Other factors

All prior review feedback is resolved: my stdout-drain nit is fixed (the run() helper now does Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited])), and the comment-cop complaints were addressed by shrinking the doc comment to one line. The tests follow harness conventions (tempDir, port: 0, await using, test.each, describe.concurrent, per-test cache dir), stay offline, and assert exact values. The PR description enumerates related open PRs and confirms none touches this file.

Jarred-Sumner pushed a commit that referenced this pull request Aug 15, 2026
…38824)

### Problem
- `registry = { url = "http://alice:s3cret@host/" }` in bunfig.toml (and
the same object shape under `[install.scopes]`) sends no `Authorization`
header: the registry sees `authorization: null` on the manifest and the
tarball request. The same URL written as the string form, `registry =
"http://alice:s3cret@host/"`, sends `Authorization: Basic
YWxpY2U6czNjcmV0` on both. `{ url = "http://:token@host/" }` drops the
token the same way.
- Because the credentials stay inside the stored URL, they are also
printed when a request fails: `error: GET
http://alice:s3cret@127.0.0.1:PORT/no-deps - 401`.
- Cause: `parse_registry_object` in `src/bunfig/bunfig.rs` copies `url`
into `NpmRegistry.url` verbatim. Only the string form runs the userinfo
splitter
(`bun_api::npm_registry::Parser::parse_registry_url_string_impl`,
`src/api/lib.rs`). Nothing downstream reads userinfo out of
`NpmRegistry.url`: `Scope::from_api` (`src/install/npm.rs`) builds the
header from `token` / `username` / `password` only, and the HTTP client
ignores userinfo in a request URL.

### Fix
- `parse_registry_object` now runs `url` through the same splitter the
string form uses (`parse_registry_url`, shared by both forms), so the
URL's credentials land in the credential fields and the stored URL no
longer carries them. The `match obj.get(b"url")` block is the fix; the
rest of the diff renames the string-form helper to take bytes.
- If the object has any `username` / `password` / `token` key, the
credentials taken from the URL are dropped before the keys are applied.
Objects with credential keys therefore produce exactly the `NpmRegistry`
they produce today (the URL's userinfo was always ignored for them); the
only configs whose behavior changes are the ones that sent nothing. This
has to be all-or-nothing rather than per field: `from_api` sends a token
in preference to a username/password pair, so merging per field would
let a `:token@` in the URL outrank `username` / `password` keys written
next to it, which works today. It is also the rule the `.npmrc` merge
already uses for the next layer down (`apply_registry_auth` in
`src/ini/lib.rs` only fills in `.npmrc` credentials when the registry
has none).
- Correct because `NpmRegistry` is read on the assumption that
credentials live in its fields and `url` carries none: `Scope::from_api`
only reads the fields, and the `.npmrc` merge decides whether to apply
`//host/:_authToken=` lines by checking those fields. The string form
establishes that shape at parse time; the object form was the one
producer that did not. Splitting at the same point makes a given `url`
text mean the same thing in both forms, including in the `.npmrc`
precedence check (tested).
- URLs without userinfo come back from the splitter byte for byte (it
returns the input href unchanged in that case), so existing object-form
configs, with or without a trailing slash, are unaffected.
- Not changed here: `--registry` and the `*_config_registry` env vars
(#38796), `registry = "$VAR"` where the variable's value carries
credentials (the expansion happens later, in `from_api`; filed
separately), and the `http://host//` spelling the splitter produces for
root-path URLs (#38812, cosmetic, same for both forms). No docs change:
the documented way to pass credentials in the object form stays the
explicit keys; this only makes the `url` value behave like the string
form's.
- Verified: `test/cli/install/config-precedence.test.ts` gains three
string/object pairs (`user:password` in the URL, `:token` in the URL,
`user:password` in a scoped registry URL), a `.npmrc` precedence test
(URL credentials in the object beat a `~/.npmrc` `_authToken` for the
same host), and a three-row table pinning that credential keys beat the
URL (`username`/`password` keys over `user:password` in the URL,
`username`/`password` keys over `:token` in the URL, `token` key over
`user:password` in the URL). Every test asserts the exact
`Authorization` header seen on each request and that the auth-only
package installs. On the released build the four url-only object tests
fail (`authorization: null`, 401, password in the error line); the
string rows and the keyed rows pass there, as they must, since that
behavior is unchanged. The `:token` row fails against a per-field merge,
which is the regression it guards. All 39 tests in the file pass with
this branch.
- Also run with the fix: the Registry URLs / scoped authentication /
same-origin tarball tests of `bun-install.test.ts`, `npmrc.test.ts`,
`bun-install-retry.test.ts`, `redacted-config-logs.test.ts`,
`bun-install-pathname-trailing-slash.test.ts`, `bun-run-bunfig.test.ts`,
`test/config/bunfig`, `cargo clippy -p bun_bunfig`, and the source
lints. In `bun-publish.test.ts` the lifecycle tests hit the local 5 s
default timeout both with and without this change (CI passes a longer
`--timeout`); the rest of the file passes.

### Background
- bunfig accepts a registry in two shapes: a string, `registry =
"<url>"`, and an object, `registry = { url, username, password, token
}`. Both `[install] registry` and each `[install.scopes]` entry go
through `parse_registry`, so one fix covers both.
- `api::NpmRegistry` (`src/options_types/schema.rs`) is the config-level
record produced by the bunfig and `.npmrc` loaders: `url` plus
`username` / `password` / `token`. After the loaders run, `.npmrc`
credential lines are merged into it, and the package manager turns it
into an `npm::registry::Scope`, which holds the final URL and the
pre-computed `Authorization` value. `from_api` uses `token` when it is
set and only otherwise builds Basic auth from `username` / `password`.
- The userinfo splitter takes `scheme://user:pass@host/path` and returns
an `NpmRegistry` with `username` / `password` set (or `token`, when the
user part is empty) and `url` rewritten without the userinfo. The bunfig
string form and the `.npmrc` `registry=` / `@scope:registry=` keys all
use it.

<details>
<summary>Probe: mock registry on port 0 logging the Authorization
header, released 1.4.0 vs this branch</summary>

```
registry = "http://alice:s3cret@127.0.0.1:PORT/"                    (string form)
  1.4.0:       /no-deps  Basic YWxpY2U6czNjcmV0   /no-deps/-/x-1.0.0.tgz  Basic YWxpY2U6czNjcmV0
  this branch: same

registry = { url = "http://alice:s3cret@127.0.0.1:PORT/" }
  1.4.0:       /no-deps  null                     /no-deps/-/x-1.0.0.tgz  null
  this branch: /no-deps  Basic YWxpY2U6czNjcmV0   /no-deps/-/x-1.0.0.tgz  Basic YWxpY2U6czNjcmV0

registry = { url = "http://:tok123@127.0.0.1:PORT/" }
  1.4.0:       null on both
  this branch: Bearer tok123 on both

registry = { url = "http://alice:s3cret@127.0.0.1:PORT/", token = "explicit" }
  1.4.0:       Bearer explicit on both
  this branch: same

registry = { url = "http://alice:s3cret@127.0.0.1:PORT/", password = "override" }
  1.4.0:       null on both (no username configured by keys)
  this branch: same

[install.scopes] myorg = { url = "http://bob:hunter2@127.0.0.1:PORT/" }
  1.4.0:       null on both
  this branch: Basic Ym9iOmh1bnRlcjI= on both (same as the scoped string form)

401 from the registry, object form:
  1.4.0:       error: GET http://alice:s3cret@127.0.0.1:PORT/no-deps - 401
  this branch: error: GET http://127.0.0.1:PORT//no-deps - 401   (the // is #38812)
```

</details>

<details>
<summary>Superseded first version</summary>

The first push merged the URL's credentials with the keys field by field
(a `password` key on top of `user:pass@` in the URL gave `user:<key>`).
Review pointed out that `{ url = "http://:t@host/", username = "u",
password = "p" }`, which sends `Basic u:p` today, would have started
sending `Bearer t`, because the URL's token survived next to the keys
and `from_api` prefers a token. The current version drops the URL's
credentials whenever a credential key is present, and the keyed rows in
the test table pin that.

</details>
Jarred-Sumner added a commit that referenced this pull request Aug 15, 2026
… registry URLs (#38183)

### Problem
- A registry configured as `http:host:port/path/` (scheme followed by a
single colon, which `new URL()` and npm accept) fails every resolution
before a request is made:
  ```
error: Invalid package name "react": manifest URL
"http://host:port/path/react" is not on registry "http:host:port/path/"
  error: InvalidURL
  ```
- Same failure for the other spellings the WHATWG parser rewrites (probe
against a local server on the released 1.4.0, full output in the details
below): a `..` segment in the path, an unencoded space in the path,
backslashes, surrounding whitespace. Three entries of the "Registry
URLs" table in `bun-install.test.ts` (`https:example.org`,
`https://////example.com///`, `http://點看`) hit it too; the table did not
notice because `failed to resolve` is also printed after this rejection.
- An upper-case scheme passes the manifest check (it compares
case-insensitively) but fails `for_tarball`'s same-origin comparison
(`src/install/NetworkTask.rs`, `send_auth`), which is case-sensitive, so
the tarball is requested without the `Authorization` header.
- Cause: `Scope::from_api` (`src/install/npm.rs`) and the `--registry`
branch of `Options::load`
(`src/install/PackageManager/PackageManagerOptions.rs`) store the
registry href as written. `NetworkTask::for_manifest` builds the
manifest URL with `bun_url::join`, which runs the href through the
WHATWG parser, and then compares the result against `URL::parse` of the
stored string. `URL::parse` is Bun's lenient scanner: it only recognizes
a scheme followed by `://`, does not resolve `..`, percent-encode, strip
whitespace or lower-case, so for these spellings the two sides describe
different URLs (`http:host:port/...` parses as hostname `http` with no
protocol). The same stored string feeds `for_tarball`'s origin
comparison, `extract_tarball::build_url`, `url_is_under_registry` in
`bun.lock.rs`, the DNS prefetch and the `@@<hostname>` cache folder
name.

### Fix
- Adds `Scope::set_url`: stores the WHATWG serialization of the
configured URL (`bun_url::URL::from_string`, the same parser `join`
uses) and derives `url_hash` from it. `Scope::from_api`, the
`--registry` branch and the `parseManifest` test helper
(`src/install_jsc/npm_jsc.rs`) all build the URL through it, so there is
one place that decides what a `Scope` holds.
- Correct because the stored href now equals the base `join` resolves
against, so every consumer that compares with, concatenates onto or
hashes the href agrees with the URL actually requested. The on-registry
check itself is unchanged and still rejects a name that joins outside
the registry directory (tested).
- Credentials cannot reach anything new. The manifest request URL is
unchanged (`join` parsed its base with the WHATWG parser before this
change too, so `join(as written, name)` and `join(normalized, name)` are
the same URL); only the value it is compared against changes. Both
checks compare against the stored href, so the only origin a tarball
request can now carry credentials to is the normalized registry origin,
which is where the manifest request (which always carries them) already
went. Before, a non-canonical spelling could only make the comparisons
fail. The one request URL that does change is the
`extract_tarball::build_url` fallback (manifest or `bun.lock` entry
without a tarball URL), which concatenates onto the href and now
produces a URL on that same origin instead of a string that failed the
`http(s)://` prefix check.
- A string the WHATWG parser rejects is stored as written, so the
`Failed to join registry "<as written>"` diagnostics for the invalid
entries of the table are unchanged (the table still asserts them).
- `set_url` runs after `from_api` has split the `/:_authToken=` style
credentials off the path, because the WHATWG parser would percent-encode
them. This is also why the normalization is not applied earlier, in the
config loaders: the `.npmrc` / bunfig string form extracts userinfo
credentials with the lenient parser first (an unencoded `#` in a
password is accepted there today).
- Already-canonical URLs serialize to themselves, so their `url_hash`,
manifest cache files and cache folder names are unchanged;
`https://registry.npmjs.org/` in particular still hashes to
`DEFAULT_URL_HASH`. The hash changes only for spellings the parser
rewrites; of those, only upper-case spellings worked before, and for
them the cost is one re-download of the cache.
- Not covered, on purpose: `.npmrc` credential lines
(`//host/path/:_authToken=`) are matched against the registry URL in
`src/ini/lib.rs` before a `Scope` exists, still by lenient parse of the
string as written, so a registry spelled `https:host/path/` gets its
requests but not its `.npmrc` token. That matching is being reworked in
#33869; filed separately. Spellings where the lenient parser takes the
port for a `:key=value` credential suffix (`http:/host:port/`,
`http:////host:port/`) are still mangled by the credential stripping
that runs before `set_url`; without a port they work.
- Verified: `test/cli/install/bun-install.test.ts` ("Registry URLs"):
new `spellings the WHATWG parser rewrites` block (bunfig registry object
with a token for each spelling, asserting the paths and `Authorization`
header of the manifest and tarball requests plus the cache folder name;
`.npmrc registry=`; `--registry`; the rejection message for a name that
joins outside the registry), and the table's handled entries now also
assert the rejection did not happen. 12 tests fail on the released build
(9 with the error above, the upper-case one with `authorization: null`
on the tarball, the rejection test because the message quoted the raw
spelling), all pass with this change.
- Also run with the change: the rest of `bun-install.test.ts` (remaining
failures are the bitbucket/gitlab/`some.url` network tests and
`--registry CLI flag`, which fail identically on the released build in
this container), `npmrc.test.ts`, the registry/whoami/manifest-cache
tests of `bun-install-registry.test.ts`,
`bun-install-pathname-trailing-slash.test.ts`, `cargo clippy` on
`bun_install` and `bun_install_jsc`, and the source lints.

### Background
- `npm::registry::Scope` is the package manager's record of one registry
(the default one or an `[install.scopes]` entry): its URL, credentials
and `url_hash`. `url_hash` keys the manifest cache files and tells
whether the default registry was overridden, which switches cache folder
names from `name@version` to `name@version@@<hostname>`.
- Bun has two URL parsers. `bun_url::URL::parse` is a lenient,
allocation-free scanner over the input bytes that the HTTP client and
the package manager use to read components out of a URL they already
hold. `bun_url::join` / `URL::from_string` call WTF::URL, the WHATWG
parser behind `new URL()`, which normalizes (scheme and host case,
missing slashes, `.`/`..` segments, percent-encoding, IDN) and rejects
what it cannot parse. The lenient scanner gives correct answers on
WHATWG output; the bug was feeding it the user's input instead.
- The "is not on registry" check exists so that a dependency name that
joins to another origin (an alias like `npm:\\other-host\pkg`) cannot
make Bun send the scope's credentials there; the tarball check in
`for_tarball` does the same for `dist.tarball` URLs returned by the
registry. Both are same-origin comparisons of a request URL against the
stored registry URL.

<details>
<summary>Probe: registry spellings against a local server (released
1.4.0 vs this branch)</summary>

Each row configures the spelling in `bunfig.toml` with one dependency
and records what reaches the server.

| registry as written | 1.4.0 | this branch |
| --- | --- | --- |
| `http://localhost:PORT/some/path/` | `GET /some/path/react` | same |
| `http:localhost:PORT/some/path/` | no request, `is not on registry` |
`GET /some/path/react` |
| `http:\\localhost:PORT\some\path\` | no request, `is not on registry`
| `GET /some/path/react` |
| `http://localhost:PORT/some/x/../path/` | no request, `is not on
registry` | `GET /some/path/react` |
| `http://localhost:PORT/some path/` | no request, `is not on registry`
| `GET /some%20path/react` |
| ` http://localhost:PORT/some/path/` (leading space) | no request, `is
not on registry` | `GET /some/path/react` |
| `HTTP://LOCALHOST:PORT/some/path/` | `GET /some/path/react` (tarball
would be sent without `Authorization`) | same request, tarball
authorized (covered by the test) |
| `http:/localhost:PORT/some/path/` | stored as `http://http/localhost/`
by the credential-suffix stripping | unchanged (see Fix) |
| `http:////localhost:PORT/some/path/` | stored as
`http://localhost/localhost/` by the credential-suffix stripping |
unchanged (see Fix) |

</details>

---

## Also folded in: audit — redact secrets in the registry URLs printed
by `bun audit` / `bun audit fix` (from #38844)

#### Problem
- With a registry URL that carries a secret, `bun audit` and `bun audit
fix` print it. With
`npm_config_registry=http://alice:s3cret@127.0.0.1:PORT/` and a registry
answering 404, stderr is `error: POST
http://alice:s3cret@127.0.0.1:PORT/-/npm/v1/security/advisories/bulk -
404`. A token in the registry path (`http://host/npm_.../`) is printed
the same way, and reaches these lines from every config source,
including `.npmrc`.
- Four outputs format the registry href verbatim (`BStr::new`), where
the rest of the package manager formats such URLs with
`bun_core::fmt::redacted_npm_url` (`Npm::response_error` in
`src/install/npm.rs`, the verbose request trace in `src/http/lib.rs`,
`bun pm whoami`):
- `src/runtime/cli/audit_command.rs` `send_audit_request`: the `POST
<url> - <status or error>` line (the repro above).
- `src/runtime/cli/audit_command.rs` `report_non_json_response`:
`<registry> returned a non-JSON audit response`, reached from the
report, `--json` and `audit fix` paths.
- `src/install/audit_fix.rs` `print_unaudited`: `warn: <registry> did
not answer the audit request (<reason>); skipped <packages>`, printed by
both commands for a scoped registry that failed.
- `src/install/audit_fix/json.rs`: the `registry` field of each
`unaudited` entry in `bun audit fix --json`.
- The href is `scope.url.href()` as configured
(`AuditRegistry::from_scope`; `unaudited()` copies it into the
`UnauditedRegistry` record behind the last two outputs). `.npmrc` and
bunfig registry strings move `user:password@` out of it while loading,
the bunfig object form, the registry env vars and `--registry` keep it
(#38796 and #38834 change the latter two), and a token in the path stays
in it in every case.
- The 1.3.x binaries print `audit request failed (status N)` without a
URL; the URL in these lines came with the audit rewrite in #38333, so
this has not shipped in a release.

#### Fix
- The `POST` line and `report_non_json_response` format the URL with
`redacted_npm_url`: these quote the request URL, so they get the same
masked form as the manifest and tarball error lines
(`http://alice:******@host/...`, path token as `***`).
`AuditRegistry.href` itself stays raw because it is also what the
request is sent to.
- The skipped-registry record names a registry rather than quoting a
request, so `unaudited()` builds it from `href_without_auth()` (trailing
slash stripped), the same form `bun publish` prints as its registry:
credentials written into the URL are left out instead of masked, and the
record reads the same whichever config source the scope came from
(`http://host:PORT`, matching what `.npmrc` scopes already produced).
Its two emitters, the warning and the `--json` field, format it with
`redacted_npm_url` for tokens in the path (`http://host:PORT/***`); the
`--json` value is rendered into a buffer first because the JSON string
writer takes bytes. The `unaudited` array is new in #38333, so nothing
depends on the raw form.
- For a URL without a password, UUID or `npm_` token the output is byte
for byte unchanged; the 177 existing tests in `bun-audit.test.ts`, many
of which assert these exact lines with plain URLs, still pass.
- Not touched, same class elsewhere: `bun audit fix`'s `was not checked
for updates` lines repeat the install log's `GET <url> - <status>` text,
which #38817 redacts at its source; the registry URL lines in
`src/install/NetworkTask.rs` (`Failed to join registry ...`, `... is not
on registry ...`) and `src/install/pnpm.rs` (`fetching pnpm registry ...
from <url>`) are install-side and have been filed separately. This PR
and #38817 touch disjoint files.
- Tests: `test/cli/install/bun-audit.test.ts`, new `bun audit with a
secret in the registry URL` block, one test per output: the `POST` line,
the non-JSON line from the response check, the non-JSON line from the
parse step in report, `--json` and `fix` mode, the skipped-registry
warning and `unaudited[].registry` with a token in the registry path
(`.npmrc` scope), and the same two outputs with `user:password@` in the
URL (bunfig object-form scope, which keeps it), asserting the
credential-free form. The path-token cases use a token rather than a
password because a token reaches the audit command from every config
source; the credential case asserts the stripped form, which stays true
once #38796 / #38834 strip credentials earlier, so this PR does not
depend on their landing order. All five fail on this branch with `src/`
stashed (the token or password is printed); the credential case also
fails with only the `unaudited()` hunk removed (it then prints
`alice:******@`), and the `--json` case with only the `json.rs` hunk
removed; all 182 tests in the file pass with the change.
- Also ran `cargo clippy -p bun_install -p bun_runtime`, `cargo fmt
--check` on both crates, and `test/internal/source-lints`.

#### Background
- `bun audit` POSTs the lockfile's package versions to
`<registry>/-/npm/v1/security/advisories/bulk`. Packages whose scope
(`@foo/*`) is configured with its own registry are sent to that registry
instead; when a non-default registry fails to answer (HTTP error,
connection error, non-JSON body) its packages are reported as skipped
(one `UnauditedRegistry` record per registry, printed as the warning
and, by `audit fix --json`, as the `unaudited` entries) rather than
failing the command, while a failure from the default registry fails the
command with the `POST` or non-JSON line.
- `redacted_npm_url` (`src/bun_core/fmt.rs`) is a `Display` adapter over
URL bytes: the password of `scheme://user:password@host` is written as
one `*` per byte (the per-byte form is shared with the config-excerpt
redactor, which needs column alignment), and any UUID or `npm_`/`npms_`
token anywhere in the string as `***`; everything else is written
through unchanged. `Output::err_generic`, `warn!` and `pretty_errorln!`
take any `Display` argument, so it is a drop-in replacement for
`BStr::new` (`err_generic`'s `{s}`/`{f}` placeholder letters are
cosmetic; `{f}` is what the other `redacted_npm_url` call site uses).
- `URL::href_without_auth()` (`src/url/lib.rs`) rebuilds
`scheme://host[:port]/path/` from the parsed URL, dropping any userinfo;
it is what the config loaders use to store a registry URL whose
credentials were split out, and what `bun publish` prints as
`Registry:`. It currently yields `http://host//` for a root-path
registry (#38812 changes that to one slash); `unaudited()` strips
trailing slashes afterwards, so the record is `http://host:PORT` either
way. Tokens that are part of the path survive it, which is why the
record's emitters still go through `redacted_npm_url`.

<details>
<summary>Before / after on a debug build (404 registry, non-JSON
registry, scoped registry with a path token, scoped registry configured
as <code>{ url = "http://alice:s3cret@..." }</code>)</summary>

Before:

```
error: POST http://alice:s3cret@127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8/-/npm/v1/security/advisories/bulk - 404
error: http://alice:s3cret@127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 returned a non-JSON audit response
warn: http://127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8 did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://127.0.0.1:PORT/npm_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8","packages":["@foo/bar"],"reason":"404"}],...}
warn: http://alice:s3cret@localhost:PORT did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://alice:s3cret@localhost:PORT","packages":["@foo/bar"],"reason":"404"}],...}
```

After:

```
error: POST http://alice:******@127.0.0.1:PORT/***/-/npm/v1/security/advisories/bulk - 404
error: http://alice:******@127.0.0.1:PORT/*** returned a non-JSON audit response
warn: http://127.0.0.1:PORT/*** did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://127.0.0.1:PORT/***","packages":["@foo/bar"],"reason":"404"}],...}
warn: http://localhost:PORT did not answer the audit request (404); skipped @foo/bar
{"dryRun":false,...,"unaudited":[{"registry":"http://localhost:PORT","packages":["@foo/bar"],"reason":"404"}],...}
```

</details>

<details>
<summary>Earlier revision of this PR</summary>

The first revision applied `redacted_npm_url` to the `UnauditedRegistry`
record as well, so for the config sources that keep credentials in the
URL the warning and the `--json` field came out as
`http://alice:******@host` (username and password length, and a
different shape from the `.npmrc` case, which had no userinfo to begin
with). Review pointed out that every other place bun emits a registry
URL as data drops the credentials instead; the record is now built with
`href_without_auth()` and the per-byte masking is confined to the two
lines that quote the request URL.

</details>

Closes #38844

<!-- robobun:evidence:begin -->

---

**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-install.test.ts

<!-- robobun:evidence:end -->

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
href_without_auth() appended "/" + trimmed pathname + "/" unconditionally,
so a registry URL whose credentials were stripped and whose path was "/"
was stored as "http://host//". Manifest URLs are joined onto that href, so
every "GET <url> - <status>" line for such a registry printed a double
slash. Skip the path and its trailing slash when the trimmed path is empty.
The loadNpmrc rows and the whoami cases read the stored href directly, so
they keep exercising href_without_auth() even once the manifest join
normalizes its base on its own. One install case keeps pinning the GET line.
@robobun
robobun force-pushed the farm/112debf9/href-without-auth-root-slash branch from b2aa825 to 0ae2bd4 Compare August 15, 2026 11:02

@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 — the earlier nit (drain stdout) and the comment-cop feedback are both addressed.

What was reviewed:

  • The href_without_auth change: skipping the {path}/ suffix when the trimmed path is empty produces exactly one trailing slash; the capacity hint over-reserves by one byte in that case, which is harmless.
  • All 6 call sites: options_types/schema.rs and install/npm.rs store the result (the fix); audit_command.rs and publish_command.rs:1501 pass it through without_trailing_slash (loops, so both // and / reduce to the same string — no change); publish_command.rs:896 is display-only.
  • Tests follow the file's harness conventions (test.each, describe.concurrent, tempDir, port: 0, local Bun.serve mock, all pipes drained, exit code asserted last).
Extended reasoning...

Overview

Two files. src/url/lib.rs changes URL::href_without_auth() to skip appending {path}/ when the trimmed pathname is empty, so a root-path registry serializes as http://host/ instead of http://host//. The 6-line doc comment is replaced with a one-liner (per the comment-cop bot). test/cli/install/npmrc.test.ts gains a describe.concurrent block with 11 cases: 8 loadNpmrc rows asserting the stored default_registry_url for the user:pass@ and :token@ forms across root/port/path variants, 2 bun pm whoami cases exercising the bunfig _authToken= suffix form via a local Bun.serve mock, and 1 bun install case pinning the printed GET line and the Basic header sent.

Security risks

None. The helper strips userinfo from a URL string used for display and as a request base; credentials are removed before this code runs and are stored separately. The change only affects how many trailing slashes the auth-free href has. The tests spin up local Bun.serve on 127.0.0.1:0 and never touch the network.

Level of scrutiny

Low-to-medium. The Rust change is 4 effective lines with an obvious invariant ("emit exactly one trailing slash"). I checked every caller (rg href_without_auth): the two config-loading sites store the result verbatim (this is what the PR fixes); audit_command.rs:422 and publish_command.rs:1501 immediately feed it through strings::without_trailing_slash, which loops over all trailing slashes, so http://host// and http://host/ both reduce to http://host — no behavior change; publish_command.rs:896 prints Registry: {}/ for display, which is cosmetic and only improved. Nothing depends on the double slash.

Other factors

My earlier nit about the undrained stdout pipe was fixed in aa47e37 — the run() helper now does the three-way Promise.all. The comment-cop feedback on the multi-line doc comment was addressed. All inline threads are resolved. The tests match the surrounding file's conventions (harness tempDir/bunEnv, await using for server and process, port: 0, stderr asserted before exit code) and the loadNpmrc rows are pure — registry.example.com is never contacted.

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.

1 participant