Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions docs/pm/npmrc.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ ca[]="-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
cafile=/path/to/ca-bundle.crt
```

If both are set, only `cafile` is used. A `ca` or `cafile` in `bunfig.toml`, or `--ca` / `--cafile` on the command line, replaces both.

### `omit` and `include`: Control dependency types

Control which dependency types Bun installs:
Expand Down
2 changes: 2 additions & 0 deletions docs/runtime/bunfig.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,8 @@ ca = "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
cafile = "path/to/cafile"
```

Set one of the two. If both are set, only `cafile` is used. `--ca` or `--cafile` on the command line replaces both.

### `install.cache`

To configure the cache behavior:
Expand Down
12 changes: 10 additions & 2 deletions src/bunfig/bunfig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,15 @@ impl<'a> Parser<'a> {
install: &mut api::BunInstall,
install_obj: &Expr,
) -> crate::Result<()> {
if let Some(cafile) = install_obj.get(b"cafile") {
let cafile = install_obj.get(b"cafile");
let ca = install_obj.get(b"ca");
// `ca` + `cafile` are one setting: a file that sets either replaces both.
if cafile.is_some() || ca.is_some() {
install.cafile = None;
install.ca = None;
}

if let Some(cafile) = cafile {
install.cafile = match cafile.as_string(self.bump) {
Some(s) => Some(s.into()),
None => {
Expand All @@ -1282,7 +1290,7 @@ impl<'a> Parser<'a> {
};
}

if let Some(ca) = install_obj.get(b"ca") {
if let Some(ca) = ca {
match &ca.data {
ExprData::EArray(arr) => {
let items = arr.items.slice();
Expand Down
12 changes: 10 additions & 2 deletions src/ini/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1367,7 +1367,15 @@ mod draft {
}
}

if let Some(query) = out.as_property(b"ca") {
let ca = out.as_property(b"ca");
let cafile = out.as_property(b"cafile");
// `ca` + `cafile` are one setting: a file that sets either replaces both.
if ca.is_some() || cafile.is_some() {
install.ca = None;
install.cafile = None;
}

if let Some(query) = ca {
if let Some(str_) = query.expr.as_utf8_string_literal() {
install.ca = Some(bun_api::Ca::Str(Box::<[u8]>::from(str_)));
} else if let ExprData::EArray(arr) = &query.expr.data {
Expand All @@ -1381,7 +1389,7 @@ mod draft {
}
}

if let Some(query) = out.as_property(b"cafile") {
if let Some(query) = cafile {
if let Some(cafile) = query.expr.as_string_cloned(bump)? {
install.cafile = Some(Box::<[u8]>::from(cafile));
}
Expand Down
8 changes: 6 additions & 2 deletions src/install/PackageManager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,12 @@ fn overlay_bunfig_install(install: &mut Api::BunInstall, bunfig: Api::BunInstall
}
}

// `ca` + `cafile` are one setting: bunfig replaces both or neither.
if ca.is_some() || cafile.is_some() {
install.ca = ca;
install.cafile = cafile;
}

macro_rules! overlay {
($($field:ident),* $(,)?) => {
$( if $field.is_some() { install.$field = $field; } )*
Expand All @@ -1455,9 +1461,7 @@ fn overlay_bunfig_install(install: &mut Api::BunInstall, bunfig: Api::BunInstall
frozen_lockfile,
exact,
concurrent_scripts,
cafile,
save_text_lockfile,
ca,
ignore_scripts,
link_workspace_packages,
node_linker,
Expand Down
5 changes: 2 additions & 3 deletions src/install/PackageManager/PackageManagerOptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -898,10 +898,9 @@ impl Options {
}
self.publish_config.tolerate_republish = cli.tolerate_republish;

if !cli.ca.is_empty() {
// `--ca` / `--cafile` replace the config file's `ca` + `cafile` as a whole.
if !cli.ca.is_empty() || !cli.ca_file_name.is_empty() {
self.ca = cli.ca.iter().map(|s| Box::<[u8]>::from(*s)).collect();
}
if !cli.ca_file_name.is_empty() {
self.ca_file_name = cli.ca_file_name;
}

Expand Down
105 changes: 105 additions & 0 deletions test/cli/install/bun-install-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,111 @@ describe("certificate authority", () => {
expect(err).not.toContain("error:");
expect(await exited).toBe(0);
});
test("--ca replaces a cafile from bunfig", async () => {
// The registry's certificate is only in --ca; the bunfig cafile holds an
// unrelated CA. The CLI flag replaces the config file's CA settings as a
// whole instead of being shadowed by its cafile.
using server = Bun.serve({
port: 0,
fetch: mockRegistryFetch(),
...tls,
});
await Promise.all([
write(
packageJson,
JSON.stringify({
name: "foo",
version: "1.1.1",
dependencies: {
"no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`,
},
}),
),
write(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({
install: {
cache: false,
registry: `https://localhost:${server.port}/`,
cafile: "unrelated-ca.pem",
},
}),
),
write(
join(packageDir, "unrelated-ca.pem"),
file(join(import.meta.dir, "..", "..", "js", "node", "tls", "fixtures", "ca1-cert.pem")),
),
]);

const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--ca", tls.cert],
cwd: packageDir,
stderr: "pipe",
stdout: "pipe",
env,
});
const out = await stdout.text();
const err = await stderr.text();
expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT");
expect(err).not.toContain("error:");
expect(out).toContain("+ no-deps@");
expect(await exited).toBe(0);
});
Comment thread
robobun marked this conversation as resolved.
// Each higher layer's `ca` must replace a lower layer's `cafile` (the lower
// file holds an unrelated CA, the registry's certificate is only in `ca`).
test.each(["bunfig ca over .npmrc cafile", "project .npmrc ca over user .npmrc cafile"])("%s", async layers => {
using server = Bun.serve({
port: 0,
fetch: mockRegistryFetch(),
...tls,
});
const homeDir = join(packageDir, "home_dir");
const unrelatedCa = join(packageDir, "unrelated-ca.pem");
const npmrcCa = `ca=${JSON.stringify(tls.cert)}\n`;
await Promise.all([
write(
packageJson,
JSON.stringify({
name: "foo",
version: "1.1.1",
dependencies: {
"no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`,
},
}),
),
write(unrelatedCa, file(join(import.meta.dir, "..", "..", "js", "node", "tls", "fixtures", "ca1-cert.pem"))),
write(
join(packageDir, "bunfig.toml"),
Bun.TOML.stringify({
install: {
cache: false,
registry: `https://localhost:${server.port}/`,
...(layers.startsWith("bunfig") ? { ca: tls.cert } : {}),
},
}),
),
layers.startsWith("bunfig")
? write(join(packageDir, ".npmrc"), `cafile=${unrelatedCa}\n`)
: Promise.all([
write(join(homeDir, ".npmrc"), `cafile=${unrelatedCa}\n`),
write(join(packageDir, ".npmrc"), npmrcCa),
]),
]);

const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stderr: "pipe",
stdout: "pipe",
env: { ...env, XDG_CONFIG_HOME: homeDir },
});
const out = await stdout.text();
const err = await stderr.text();
expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT");
expect(err).not.toContain("error:");
expect(out).toContain("+ no-deps@");
expect(await exited).toBe(0);
});
test(`non-existent --cafile`, async () => {
await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } }));

Expand Down
Loading