Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/pm/npmrc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ Bun supports the following options:
- `_authToken`
- `username`
- `_password` (base64 encoded password)
- `_auth` (base64 encoded username:password, for example `btoa(username + ":" + password)`)
- `email`
- `_auth` (sent as written in a `Basic` header; npm's form is base64 encoded username:password, for example `btoa(username + ":" + password)`)
- `email` (accepted and ignored, as npm does)

The equivalent `bunfig.toml` option is to add a key in [`install.scopes`](/runtime/bunfig#install-scopes):

Expand Down
2 changes: 1 addition & 1 deletion src/api/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// ──────────────────────────────────────────────────────────────────────────

pub use bun_options_types::schema::api::{
BunInstall, Ca, NodeLinker, NpmRegistry, NpmRegistryMap, PnpmMatcher,
BunInstall, Ca, NodeLinker, NpmRegistry, NpmRegistryMap, NpmUrlAuth, PnpmMatcher,
};

// ──────────────────────────────────────────────────────────────────────────
Expand Down
29 changes: 29 additions & 0 deletions src/ast/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,35 @@ impl Log {
)
}

/// A warning placed at `loc` that never prints the source line: for a line whose
/// text may hold a secret the redactor cannot recognise.
#[cold]
pub fn add_warning_fmt_no_excerpt(
&mut self,
source: &Source,
loc: Loc,
args: fmt::Arguments<'_>,
) {
if !Kind::Warn.should_print(self.level) {
return;
}
self.warnings += 1;
let mut location = Location::init_or_null(Some(source), Range { loc, len: 1 });
if let Some(location) = location.as_mut() {
location.line_text = None;
}
let data = Data {
text: alloc_print(args),
location,
}
.clone_line_text(self.clone_line_text);
self.add_msg(Msg {
kind: Kind::Warn,
data,
..Default::default()
})
}

#[cold]
pub fn add_warning_fmt_line_col(
&mut self,
Expand Down
673 changes: 414 additions & 259 deletions src/ini/lib.rs

Large diffs are not rendered by default.

21 changes: 7 additions & 14 deletions src/install/NetworkTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,26 +390,19 @@ fn append_auth(header_builder: &mut HeaderBuilder, scope: &npm::registry::Scope)
// Routing through `format_args!`/`BStr` Display would be
// lossy for non-UTF-8 tokens (U+FFFD expands 1→3 bytes) and overrun the
// exact byte count reserved by `count_auth`. Use raw-byte append.
if !scope.token.is_empty() {
header_builder.append_bytes_value("Authorization", b"Bearer ", &scope.token);
} else if !scope.auth.is_empty() {
header_builder.append_bytes_value("Authorization", b"Basic ", &scope.auth);
} else {
let Some((scheme, value)) = scope.authorization_parts() else {
return;
}
};
header_builder.append_bytes_value("Authorization", scheme, value);
header_builder.append("npm-auth-type", "legacy");
}

fn count_auth(header_builder: &mut HeaderBuilder, scope: &npm::registry::Scope) {
if !scope.token.is_empty() {
header_builder.count("Authorization", "");
header_builder.content.cap += "Bearer ".len() + scope.token.len();
} else if !scope.auth.is_empty() {
header_builder.count("Authorization", "");
header_builder.content.cap += "Basic ".len() + scope.auth.len();
} else {
let Some((scheme, value)) = scope.authorization_parts() else {
return;
}
};
header_builder.count("Authorization", "");
header_builder.content.cap += scheme.len() + value.len();
header_builder.count("npm-auth-type", "legacy");
}

Expand Down
19 changes: 13 additions & 6 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,7 @@ fn overlay_bunfig_install(install: &mut Api::BunInstall, bunfig: Api::BunInstall
let Api::BunInstall {
default_registry,
scoped,
url_auth,
lockfile_path,
save_lockfile_path,
cache_directory,
Expand Down Expand Up @@ -1424,6 +1425,8 @@ fn overlay_bunfig_install(install: &mut Api::BunInstall, bunfig: Api::BunInstall
}
}

install.url_auth.extend(url_auth);

macro_rules! overlay {
($($field:ident),* $(,)?) => {
$( if $field.is_some() { install.$field = $field; } )*
Expand Down Expand Up @@ -1482,6 +1485,10 @@ pub fn init(
cli: CommandLineArguments,
subcommand: Subcommand,
) -> Result<(&'static mut PackageManager, Box<[u8]>), Error> {
// `.npmrc` is read below, before `Options::load` applies the log level.
if cli.log_level.is_silent() {
bun_ast::DEFAULT_LOG_LEVEL.store(bun_ast::Level::Err);
}
if cli.global {
// Non-consuming peek: `ctx.install` is
// `Option<Box<BunInstall>>` borrowed via `&mut ContextData`; reborrow with
Expand Down Expand Up @@ -1915,7 +1922,7 @@ pub fn init(

{
// npmrc < bunfig < CLI
let mut bunfig_install = ctx
let bunfig_install = ctx
.install
.take()
.map_or_else(Api::BunInstall::default, |b| *b);
Expand Down Expand Up @@ -1944,18 +1951,18 @@ pub fn init(
}
}

let registry_auth = if global_len > 0 {
if global_len > 0 {
ini::load_npmrc_config(
&mut install,
&bunfig_install,
env,
true,
&[ZStr::from_buf(&buf[..], global_len), &*npmrc_local],
)
);
} else {
ini::load_npmrc_config(&mut install, env, true, &[&*npmrc_local])
};
ini::load_npmrc_config(&mut install, &bunfig_install, env, true, &[&*npmrc_local]);
}

ini::apply_registry_auth(&mut bunfig_install, &registry_auth);
overlay_bunfig_install(&mut install, bunfig_install);
ctx.install = Some(Box::new(install));
}
Expand Down
75 changes: 55 additions & 20 deletions src/install/PackageManager/PackageManagerOptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ pub struct Options {
pub scope: Npm::registry::Scope,

pub(crate) registries: Npm::registry::Map,
/// Every `.npmrc` credential key, for the registries whose URL is only known
/// after the config files were read.
pub(crate) url_auth: Vec<Npm::registry::UrlAuth>,
pub(crate) cache_directory: &'static [u8],
pub enable: Enable,
pub do_: Do,
Expand Down Expand Up @@ -127,6 +130,7 @@ impl Default for Options {
// Always assigned in `load()` before read.
scope: Npm::registry::Scope::default(),
registries: Npm::registry::Map::default(),
url_auth: Vec::new(),
cache_directory: b"",
enable: Enable::default(),
do_: Do::default(),
Expand Down Expand Up @@ -418,6 +422,25 @@ fn leak_static(s: &[u8]) -> &'static [u8] {
}

impl Options {
/// Give every scope that ended up without credentials the ones `.npmrc`
/// configures for its registry URL, by npm's key walk. A registry from
/// `bunfig.toml`, `--registry` or `$NPM_CONFIG_REGISTRY` is only known here, and
/// a credential from any of those sources outranks a `.npmrc` line.
fn fill_credentials_from_url_auth(&mut self) {
if self.url_auth.is_empty() {
return;
}
let url_auth = &self.url_auth;
for scope in core::iter::once(&mut self.scope).chain(self.registries.values_mut()) {
if scope.has_credentials() && !scope.credentials_from_url {
continue;
}
if let Some(credentials) = Npm::registry::UrlAuth::find(url_auth, &scope.url.url()) {
scope.copy_credentials_from(credentials);
}
}
}

pub(crate) fn load(
&mut self,
log: &mut bun_ast::Log,
Expand Down Expand Up @@ -468,6 +491,12 @@ impl Options {
}
}

for url_auth in &config.url_auth {
if let Some(url_auth) = Npm::registry::UrlAuth::from_api(url_auth, env)? {
self.url_auth.push(url_auth);
}
}

if let Some(ca) = &config.ca {
match ca {
Api::Ca::List(ca_list) => {
Expand Down Expand Up @@ -632,13 +661,11 @@ impl Options {
let mut api_registry = Api::NpmRegistry::from_url(registry_);
// Credentials in the URL win, as they do for `registry=` in .npmrc.
if !api_registry.has_credentials() {
let prev_url = self.scope.url.url();
let new_url = bun_url::URL::parse(&api_registry.url);
if bun_core::without_trailing_slash(new_url.host)
== bun_core::without_trailing_slash(prev_url.host)
&& (new_url.is_https() || !prev_url.is_https())
{
if same_host_not_downgraded(&new_url, &self.scope.url.url()) {
api_registry.token = core::mem::take(&mut self.scope.token);
api_registry.auth = core::mem::take(&mut self.scope.auth);
api_registry.credentials_from_url = self.scope.credentials_from_url;
}
}
self.scope = Npm::registry::Scope::from_api(b"", api_registry, env)?;
Expand All @@ -650,24 +677,19 @@ impl Options {

if let Some(cli) = &maybe_cli {
if !cli.registry.is_empty() {
let api_registry = Api::NpmRegistry::from_url(cli.registry);
if api_registry.has_credentials() {
self.scope = Npm::registry::Scope::from_api(b"", api_registry, env)?;
} else {
let mut api_registry = Api::NpmRegistry::from_url(cli.registry);
// Credentials in the URL win, as they do for `registry=` in .npmrc.
if !api_registry.has_credentials() {
let new_url = bun_url::URL::parse(&api_registry.url);
let same_origin = {
let prev_url = self.scope.url.url();
bun_core::without_trailing_slash(new_url.host)
== bun_core::without_trailing_slash(prev_url.host)
&& (new_url.is_https() || !prev_url.is_https())
};
if !same_origin {
self.scope.token = Box::default();
self.scope.auth = Box::default();
self.scope.user = Box::default();
if same_host_not_downgraded(&new_url, &self.scope.url.url()) {
api_registry.token = core::mem::take(&mut self.scope.token);
api_registry.auth = core::mem::take(&mut self.scope.auth);
api_registry.credentials_from_url = self.scope.credentials_from_url;
}
self.scope.set_url(api_registry.url);
}
// Through the same builder as every other registry, so a credential
// embedded in the URL is stripped from the request path here too.
self.scope = Npm::registry::Scope::from_api(b"", api_registry, env)?;
}
}

Expand All @@ -682,6 +704,7 @@ impl Options {
if let Some(token) = env.get(token_key) {
if !token.is_empty() {
self.scope.token = token.into();
self.scope.credentials_from_url = false;
break;
}
}
Expand Down Expand Up @@ -737,6 +760,7 @@ impl Options {

if !cli.token.is_empty() {
self.scope.token = cli.token.into();
self.scope.credentials_from_url = false;
}

if cli.no_save {
Expand Down Expand Up @@ -933,6 +957,10 @@ impl Options {
}

// moved from `defer { ... }` after scope assignment (see note above).
// After every source that can set a registry URL (bunfig, .npmrc, environment,
// command line) has been applied.
self.fill_credentials_from_url_auth();

self.did_override_default_scope = self.scope.url_hash != *Npm::registry::DEFAULT_URL_HASH;

// The manifest cache is the data source for --prefer-offline/--offline; keep it on
Expand Down Expand Up @@ -1115,3 +1143,10 @@ impl Enable {
self.contains(Enable::GLOBAL_VIRTUAL_STORE)
}
}

/// `new` is on `base`'s host and does not downgrade https to http, so `base`'s
/// credentials may follow it: the shape `npm_config_registry` and `--registry` share.
fn same_host_not_downgraded(new: &bun_url::URL, base: &bun_url::URL) -> bool {
bun_core::without_trailing_slash(new.host) == bun_core::without_trailing_slash(base.host)
&& (new.is_https() || !base.is_https())
}
Loading
Loading