Files
bun-src/test/cli/install/npmrc.test.ts
T
2026-08-27 21:09:14 +00:00

817 lines
28 KiB
TypeScript

import { write } from "bun";
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
import { rm } from "fs/promises";
import { VerdaccioRegistry, bunExe, bunEnv as env, isIPv6, tempDir } from "harness";
import { join } from "path";
const { iniInternals } = require("bun:internal-for-testing");
const { loadNpmrc } = iniInternals;
var registry = new VerdaccioRegistry();
beforeAll(async () => {
await registry.start();
});
afterAll(() => {
registry.stop();
});
describe("npmrc", async () => {
const isBase64Encoded = (opt: string) => opt === "_auth" || opt === "_password";
it("should convert to utf8 if BOM", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Promise.all([
write(join(packageDir, ".npmrc"), Buffer.from(`\ufeff\ncache=hi!`, "utf16le")),
write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0" })),
rm(join(packageDir, "bunfig.toml"), { force: true }),
]);
const originalCacheDir = env.BUN_INSTALL_CACHE_DIR;
delete env.BUN_INSTALL_CACHE_DIR;
const { stdout, stderr, exited } = Bun.spawn({
cmd: [bunExe(), "pm", "cache"],
cwd: packageDir,
env,
stdout: "pipe",
stderr: "pipe",
});
env.BUN_INSTALL_CACHE_DIR = originalCacheDir;
const out = await stdout.text();
const err = await stderr.text();
console.log({ out, err });
expect(err).toBeEmpty();
expect(out.endsWith("hi!")).toBeTrue();
expect(await exited).toBe(0);
});
it("works with empty file", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
console.log("package dir", packageDir);
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const ini = /* ini */ ``;
await Bun.$`echo ${ini} > ${packageDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "foo",
dependencies: {},
})} > package.json`.cwd(packageDir);
await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true);
});
it("sets default registry", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
console.log("package dir", packageDir);
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const ini = /* ini */ `
registry = http://localhost:${registry.port}/
`;
await Bun.$`echo ${ini} > ${packageDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
},
})} > package.json`.cwd(packageDir);
await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true);
});
it("sets scoped registry", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const ini = /* ini */ `
@types:registry=http://localhost:${registry.port}/
`;
await Bun.$`echo ${ini} > ${packageDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "foo",
dependencies: {
"@types/no-deps": "1.0.0",
},
})} > package.json`.cwd(packageDir);
await Bun.$`${bunExe()} install`.cwd(packageDir).throws(true);
});
it("works with home config", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
console.log("package dir", packageDir);
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const homeDir = `${packageDir}/home_dir`;
await Bun.$`mkdir -p ${homeDir}`;
console.log("home dir", homeDir);
const ini = /* ini */ `
registry=http://localhost:${registry.port}/
`;
await Bun.$`echo ${ini} > ${homeDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
},
})} > package.json`.cwd(packageDir);
await Bun.$`${bunExe()} install`
.env({
...process.env,
XDG_CONFIG_HOME: `${homeDir}`,
})
.cwd(packageDir)
.throws(true);
});
it("works with two configs", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
console.log("package dir", packageDir);
const packageIni = /* ini */ `
@types:registry=http://localhost:${registry.port}/
`;
await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`;
const homeDir = `${packageDir}/home_dir`;
await Bun.$`mkdir -p ${homeDir}`;
console.log("home dir", homeDir);
const homeIni = /* ini */ `
registry = http://localhost:${registry.port}/
`;
await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
"@types/no-deps": "1.0.0",
},
})} > package.json`.cwd(packageDir);
await Bun.$`${bunExe()} install`
.env({
...process.env,
XDG_CONFIG_HOME: `${homeDir}`,
})
.cwd(packageDir)
.throws(true);
});
describe("user .npmrc lookup", () => {
const npmrc = (port: number) => `registry=http://localhost:${port}/\n//localhost:${port}/:_authToken=token\n`;
const pkg = { "pkg/package.json": JSON.stringify({ name: "npmrc-lookup", version: "0.0.1" }) };
// `publish --dry-run` never contacts the registry, but it still requires a token
// for it and prints which registry it picked up, so it shows which .npmrc was read.
// bunEnv spreads process.env and CI runners commonly export XDG_CONFIG_HOME, so it
// is removed here and each case passes back exactly the value it is testing.
async function publishDryRun(dir: string, envOverride: Record<string, string>) {
const spawnEnv = { ...env, HOME: join(dir, "home"), USERPROFILE: join(dir, "home") };
delete spawnEnv.XDG_CONFIG_HOME;
await using proc = Bun.spawn({
cmd: [bunExe(), "publish", "--dry-run"],
cwd: join(dir, "pkg"),
env: { ...spawnEnv, ...envOverride },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
return { stdout, stderr, exitCode };
}
const usesRegistry = (port: number) => ({
stdout: expect.stringContaining(`Registry: http://localhost:${port}/\n`),
stderr: expect.not.stringContaining("missing authentication"),
exitCode: 0,
});
it.concurrent("uses $XDG_CONFIG_HOME/.npmrc when it exists", async () => {
using dir = tempDir("npmrc-xdg", { ...pkg, "home/.npmrc": npmrc(1), "xdg/.npmrc": npmrc(2) });
const result = await publishDryRun(String(dir), { XDG_CONFIG_HOME: join(String(dir), "xdg") });
expect(result).toEqual(usesRegistry(2));
});
// https://github.com/oven-sh/bun/issues/24124: GitHub Actions exports
// XDG_CONFIG_HOME=~/.config, while `npm login` writes ~/.npmrc.
it.concurrent("falls back to $HOME/.npmrc when $XDG_CONFIG_HOME has no .npmrc", async () => {
using dir = tempDir("npmrc-xdg-without-npmrc", { ...pkg, "home/.npmrc": npmrc(1), "xdg/.keep": "" });
const result = await publishDryRun(String(dir), { XDG_CONFIG_HOME: join(String(dir), "xdg") });
expect(result).toEqual(usesRegistry(1));
});
it.concurrent("uses $HOME/.npmrc when $XDG_CONFIG_HOME is unset", async () => {
using dir = tempDir("npmrc-xdg-unset", { ...pkg, "home/.npmrc": npmrc(1) });
const result = await publishDryRun(String(dir), {});
expect(result).toEqual(usesRegistry(1));
});
it.concurrent("uses $HOME/.npmrc when $XDG_CONFIG_HOME is empty", async () => {
using dir = tempDir("npmrc-xdg-empty", { ...pkg, "home/.npmrc": npmrc(1) });
const result = await publishDryRun(String(dir), { XDG_CONFIG_HOME: "" });
expect(result).toEqual(usesRegistry(1));
});
});
it("package config overrides home config", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
console.log("package dir", packageDir);
const packageIni = /* ini */ `
@types:registry=http://localhost:${registry.port}/
`;
await Bun.$`echo ${packageIni} > ${packageDir}/.npmrc`;
const homeDir = `${packageDir}/home_dir`;
await Bun.$`mkdir -p ${homeDir}`;
console.log("home dir", homeDir);
const homeIni = /* ini */ "@types:registry=https://registry.npmjs.org/";
await Bun.$`echo ${homeIni} > ${homeDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "foo",
dependencies: {
"@types/no-deps": "1.0.0",
},
})} > package.json`.cwd(packageDir);
await Bun.$`${bunExe()} install`
.env({
...process.env,
XDG_CONFIG_HOME: `${homeDir}`,
})
.cwd(packageDir)
.throws(true);
});
it("default registry from env variable", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const ini = /* ini */ `
registry=\${LOL}
`;
const result = loadNpmrc(ini, { LOL: `http://localhost:${registry.port}/` });
expect(result.default_registry_url).toBe(`http://localhost:${registry.port}/`);
});
it("default registry from env variable 2", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const ini = /* ini */ `
registry=http://localhost:\${PORT}/
`;
const result = loadNpmrc(ini, { ...env, PORT: registry.port });
expect(result.default_registry_url).toEqual(`http://localhost:${registry.port}/`);
});
async function makeTest(
options: [option: string, value: string][],
check: (result: {
default_registry_url: string;
default_registry_token: string;
default_registry_username: string;
default_registry_password: string;
default_registry_email: string;
}) => void,
) {
const optionName = await Promise.all(options.map(async ([name, val]) => `${name} = ${val}`));
test(optionName.join(" "), async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const iniInner = await Promise.all(
options.map(async ([option, value]) => {
let finalValue = value;
finalValue = isBase64Encoded(option) ? Buffer.from(finalValue).toString("base64") : finalValue;
return `//registry.npmjs.org/:${option}=${finalValue}`;
}),
);
const ini = /* ini */ `
${iniInner.join("\n")}
`;
await Bun.$`echo ${JSON.stringify({
name: "hello",
main: "index.js",
version: "1.0.0",
dependencies: {
"is-even": "1.0.0",
},
})} > package.json`.cwd(packageDir);
await Bun.$`echo ${ini} > ${packageDir}/.npmrc`;
const result = loadNpmrc(ini);
check(result);
});
}
await makeTest([["_authToken", "skibidi"]], result => {
expect(result.default_registry_url).toEqual("https://registry.npmjs.org/");
expect(result.default_registry_token).toEqual("skibidi");
});
await makeTest(
[
["username", "zorp"],
["_password", "skibidi"],
],
result => {
expect(result.default_registry_url).toEqual("https://registry.npmjs.org/");
expect(result.default_registry_username).toEqual("zorp");
expect(result.default_registry_password).toEqual("skibidi");
},
);
it("authentication works", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const ini = /* ini */ `
registry = http://localhost:${registry.port}/
@needs-auth:registry=http://localhost:${registry.port}/
//localhost:${registry.port}/:_authToken=${await registry.generateUser("bilbo_swaggins", "verysecure")}
`;
await Bun.$`echo ${ini} > ${packageDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "hi",
main: "index.js",
version: "1.0.0",
dependencies: {
"no-deps": "1.0.0",
"@needs-auth/test-pkg": "1.0.0",
},
"publishConfig": {
"registry": `http://localhost:${registry.port}`,
},
})} > package.json`.cwd(packageDir);
await Bun.$`${bunExe()} install`.env(env).cwd(packageDir).throws(true);
});
type EnvMap =
| Omit<
{
[key: string]: string;
},
"dotEnv"
>
| { dotEnv?: Record<string, string> };
function registryConfigOptionTest(
name: string,
_opts: Record<string, string> | (() => Promise<Record<string, string>>),
_env?: EnvMap | (() => Promise<EnvMap>),
check?: (stdout: string, stderr: string) => void,
) {
it(`sets scoped registry option: ${name}`, async () => {
const { packageDir, packageJson } = await registry.createTestDir();
console.log("PACKAGE DIR", packageDir);
await Bun.$`rm -rf ${packageDir}/bunfig.toml`;
const { dotEnv, ...restOfEnv } = _env
? typeof _env === "function"
? await _env()
: _env
: { dotEnv: undefined };
const opts = _opts ? (typeof _opts === "function" ? await _opts() : _opts) : {};
const dotEnvInner = dotEnv
? Object.entries(dotEnv)
.map(([k, v]) => `${k}=${k.includes("SECRET_") ? Buffer.from(v).toString("base64") : v}`)
.join("\n")
: "";
const ini = `
registry = http://localhost:${registry.port}/
${Object.keys(opts)
.map(
k =>
`//localhost:${registry.port}/:${k}=${isBase64Encoded(k) && !opts[k].includes("${") ? Buffer.from(opts[k]).toString("base64") : opts[k]}`,
)
.join("\n")}
`;
if (dotEnvInner.length > 0) await Bun.$`echo ${dotEnvInner} > ${packageDir}/.env`;
await Bun.$`echo ${ini} > ${packageDir}/.npmrc`;
await Bun.$`echo ${JSON.stringify({
name: "hi",
main: "index.js",
version: "1.0.0",
dependencies: {
"@needs-auth/test-pkg": "1.0.0",
},
"publishConfig": {
"registry": `http://localhost:${registry.port}`,
},
})} > package.json`.cwd(packageDir);
const { stdout, stderr } = await Bun.$`${bunExe()} install`
.env({ ...env, ...restOfEnv })
.cwd(packageDir)
.throws(check === undefined);
if (check) check(stdout.toString(), stderr.toString());
});
}
registryConfigOptionTest("_authToken", async () => ({
"_authToken": await registry.generateUser("bilbo_baggins", "verysecure"),
}));
registryConfigOptionTest(
"_authToken with env variable value",
async () => ({ _authToken: "${SUPER_SECRET_TOKEN}" }),
async () => ({ SUPER_SECRET_TOKEN: await registry.generateUser("bilbo_baggins420", "verysecure") }),
);
registryConfigOptionTest("username and password", async () => {
await registry.generateUser("gandalf429", "verysecure");
return { username: "gandalf429", _password: "verysecure" };
});
registryConfigOptionTest(
"username and password with env variable password",
async () => {
await registry.generateUser("gandalf422", "verysecure");
return { username: "gandalf422", _password: "${SUPER_SECRET_PASSWORD}" };
},
{
SUPER_SECRET_PASSWORD: Buffer.from("verysecure").toString("base64"),
},
);
registryConfigOptionTest(
"username and password with .env variable password",
async () => {
await registry.generateUser("gandalf421", "verysecure");
return { username: "gandalf421", _password: "${SUPER_SECRET_PASSWORD}" };
},
{
dotEnv: { SUPER_SECRET_PASSWORD: "verysecure" },
},
);
registryConfigOptionTest("_auth", async () => {
await registry.generateUser("linus", "verysecure");
const _auth = "linus:verysecure";
return { _auth };
});
registryConfigOptionTest(
"_auth from .env variable",
async () => {
await registry.generateUser("zack", "verysecure");
return { _auth: "${SECRET_AUTH}" };
},
{
dotEnv: { SECRET_AUTH: "zack:verysecure" },
},
);
registryConfigOptionTest(
"_auth from .env variable with no value",
async () => {
await registry.generateUser("zack420", "verysecure");
return { _auth: "${SECRET_AUTH}" };
},
{
dotEnv: { SECRET_AUTH: "" },
},
(stdout: string, stderr: string) => {
expect(stderr).toContain("received an empty string");
},
);
await makeTest([["email", "[email protected]"]], result => {
expect(result.default_registry_url).toEqual("https://registry.npmjs.org/");
expect(result.default_registry_email).toEqual("[email protected]");
});
await makeTest(
[
["username", "testuser"],
["_password", "testpass"],
["email", "[email protected]"],
],
result => {
expect(result.default_registry_url).toEqual("https://registry.npmjs.org/");
expect(result.default_registry_username).toEqual("testuser");
expect(result.default_registry_password).toEqual("testpass");
expect(result.default_registry_email).toEqual("[email protected]");
},
);
test("applies auth tokens to default registry correctly - same host different paths", () => {
// Regression test for https://github.com/oven-sh/bun/issues/26350
// When multiple auth tokens exist for the same host but different paths,
// Bun should match the token by both host AND path, not just host.
const ini = `
registry=https://somehost.com/org1/npm/registry/
//somehost.com/org1/npm/registry/:_authToken=jwt1
//somehost.com/org2/npm/registry/:_authToken=jwt2
//somehost.com/org3/npm/registry/:_authToken=jwt3
`;
const result = loadNpmrc(ini);
expect(result.default_registry_url).toEqual("https://somehost.com/org1/npm/registry/");
expect(result.default_registry_token).toBe("jwt1");
});
test("auth token not applied when paths don't match - same host", () => {
// Regression test for https://github.com/oven-sh/bun/issues/26350
// When auth tokens exist for a different path on the same host,
// they should not be applied to the default registry.
const ini = `
registry=https://somehost.com/org1/npm/registry/
//somehost.com/org2/npm/registry/:_authToken=jwt2
`;
const result = loadNpmrc(ini);
expect(result.default_registry_url).toEqual("https://somehost.com/org1/npm/registry/");
// Should be empty since there's no matching token for /org1/npm/registry/
expect(result.default_registry_token).toBe("");
});
describe("credentials keyed to a bracketed IPv6 host", () => {
// The `//` is stripped off the key before it is parsed as a URL, leaving
// `[::1]:4873/`. A leading `[` used to parse to an empty host, so these keys
// never matched the registry they were written for.
test.each([
["loopback with a port", "http://[::1]:4873/", "//[::1]:4873/"],
["loopback without a port", "http://[::1]/", "//[::1]/"],
["full address with a path", "http://[2001:db8::1]:4873/npm/registry/", "//[2001:db8::1]:4873/npm/registry/"],
["key without the trailing slash", "http://[::1]:4873/", "//[::1]:4873"],
])("_authToken is applied: %s", (_, registryUrl, key) => {
const result = loadNpmrc(`registry=${registryUrl}\n${key}:_authToken=v6-token\n`);
expect(result).toEqual({
default_registry_url: registryUrl,
default_registry_token: "v6-token",
default_registry_username: "",
default_registry_password: "",
default_registry_email: "",
});
});
test("username, _password and _auth are applied", () => {
const password = Buffer.from("v6-password").toString("base64");
expect(
loadNpmrc(`registry=http://[::1]:4873/\n//[::1]:4873/:username=v6-user\n//[::1]:4873/:_password=${password}\n`),
).toEqual({
default_registry_url: "http://[::1]:4873/",
default_registry_token: "",
default_registry_username: "v6-user",
default_registry_password: "v6-password",
default_registry_email: "",
});
const auth = Buffer.from("v6-user:v6-password").toString("base64");
expect(loadNpmrc(`registry=http://[::1]:4873/\n//[::1]:4873/:_auth=${auth}\n`)).toEqual({
default_registry_url: "http://[::1]:4873/",
default_registry_token: "",
default_registry_username: "v6-user",
default_registry_password: "v6-password",
default_registry_email: "",
});
});
test.each([
["a different port", "//[::1]:4874/"],
["a different address", "//[::2]:4873/"],
["a different path", "//[::1]:4873/other/"],
])("a key for %s is not applied", (_, key) => {
const result = loadNpmrc(`registry=http://[::1]:4873/\n${key}:_authToken=v6-token\n`);
expect(result.default_registry_url).toBe("http://[::1]:4873/");
expect(result.default_registry_token).toBe("");
});
});
it("does not print an undecodable _password value", async () => {
const secret = "s!ecret!pass";
using dir = tempDir("npmrc-password-decode", {
".npmrc": `//registry.npmjs.org/:_password=${secret}\n`,
"package.json": JSON.stringify({ name: "foo", version: "1.0.0" }),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: { ...env, NO_COLOR: "1" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("_password is not valid base64");
expect(stderr).toContain("_password=" + Buffer.alloc(secret.length, "*").toString());
expect(stderr).not.toContain(secret);
expect(exitCode).toBe(0);
});
});
describe("scoped registry routing", () => {
// A request for a @scope package must be sent only to that scope's configured
// registry with that scope's token. The registry map was keyed by a bare
// Wyhash11 hash of the scope name, so a different scope whose name hashed to
// the same value would overwrite it and silently inherit its registry + token.
// https://github.com/oven-sh/bun/issues/32741
test("does not route to a hash-colliding scope's registry or token", async () => {
// scopeA and scopeB collide under Bun's internal scope-name hash
// (Wyhash11(0) == 0xd2c80616f46b9bf2) but are distinct strings.
const scopeA = "cuxk74rj1jlebf5o-cigmevrqk5-74swpkgcollapkgcollbaaaaaaaa8k0b-p2s";
const scopeB = "cuxk74rj1jlebf5o-cigmevrqk5-74swpkgcollapkgcollbbbbbbbbb8k0b-p2s";
type Req = { path: string; auth: string | null };
const reqsA: Req[] = [];
const reqsB: Req[] = [];
const notFound = () =>
new Response(JSON.stringify({ error: "not found" }), {
status: 404,
headers: { "content-type": "application/json" },
});
await using serverA = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch(req) {
reqsA.push({ path: new URL(req.url).pathname, auth: req.headers.get("authorization") });
return notFound();
},
});
await using serverB = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch(req) {
reqsB.push({ path: new URL(req.url).pathname, auth: req.headers.get("authorization") });
return notFound();
},
});
const portA = serverA.port;
const portB = serverB.port;
const urlA = `http://127.0.0.1:${portA}/`;
const urlB = `http://127.0.0.1:${portB}/`;
// scopeA is declared first, so scopeB's colliding entry overwrites it in the
// hash-keyed registry map. The default registry also points at A so that a
// correct fallback stays offline instead of reaching the public registry.
using dir = tempDir("npmrc-scope-collision", {
".npmrc":
`registry=${urlA}\n` +
`@${scopeA}:registry=${urlA}\n` +
`//127.0.0.1:${portA}/:_authToken=scope-A-SECRET-token\n` +
`@${scopeB}:registry=${urlB}\n` +
`//127.0.0.1:${portB}/:_authToken=scope-B-SECRET-token\n`,
"package.json": JSON.stringify({
name: "victim",
version: "0.0.0",
dependencies: { [`@${scopeA}/probe`]: "^1.0.0" },
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--no-cache"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
// The install fails (probe does not exist); we only care where it asked.
const [, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).not.toBe(0);
// scopeB's registry must never see the @scopeA/probe request, and must
// never be handed scopeB's secret token for it.
expect(reqsB).toEqual([]);
// The request must have been attempted against scopeA's own registry.
expect(reqsA.some(r => r.path.includes("probe"))).toBe(true);
});
});
describe("--registry override", () => {
test("does not send the token configured for the previous registry host to the --registry host", async () => {
const tgz = join(import.meta.dir, "registry", "packages", "no-deps", "no-deps-1.0.0.tgz");
type Req = { path: string; auth: string | null };
const reqsA: Req[] = [];
const reqsB: Req[] = [];
await using serverA = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch(req) {
reqsA.push({ path: new URL(req.url).pathname, auth: req.headers.get("authorization") });
return new Response("not found", { status: 404 });
},
});
await using serverB = Bun.serve({
port: 0,
hostname: "127.0.0.1",
fetch(req) {
const url = new URL(req.url);
reqsB.push({ path: url.pathname, auth: req.headers.get("authorization") });
if (url.pathname.endsWith(".tgz")) return new Response(Bun.file(tgz));
if (url.pathname === "/no-deps") {
return Response.json({
name: "no-deps",
"dist-tags": { latest: "1.0.0" },
versions: {
"1.0.0": {
name: "no-deps",
version: "1.0.0",
dist: { tarball: `http://127.0.0.1:${serverB.port}/no-deps/-/no-deps-1.0.0.tgz` },
},
},
});
}
return new Response("not found", { status: 404 });
},
});
using dir = tempDir("npmrc-registry-override", {
".npmrc":
`registry=http://127.0.0.1:${serverA.port}/\n` +
`//127.0.0.1:${serverA.port}/:_authToken=first-host-SECRET-token\n`,
"package.json": JSON.stringify({
name: "app",
version: "1.0.0",
dependencies: { "no-deps": "1.0.0" },
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--registry", `http://127.0.0.1:${serverB.port}/`],
cwd: String(dir),
env: { ...env, BUN_INSTALL_CACHE_DIR: join(String(dir), ".cache") },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(reqsA).toEqual([]);
expect(reqsB.length).toBeGreaterThan(0);
expect(reqsB.map(r => r.auth)).toEqual(reqsB.map(() => null));
expect(stdout).toContain("+ [email protected]");
expect(exitCode).toBe(0);
});
});
describe.skipIf(!isIPv6())("registry on a bracketed IPv6 host", () => {
test("sends the token keyed to //[::1]:port/ to the default and the scoped registry", async () => {
type Req = { path: string; auth: string | null };
const reqs: Req[] = [];
await using server = Bun.serve({
port: 0,
hostname: "::1",
fetch(req) {
reqs.push({ path: new URL(req.url).pathname, auth: req.headers.get("authorization") });
return new Response("not found", { status: 404 });
},
});
const url = `http://[::1]:${server.port}/`;
using dir = tempDir("npmrc-ipv6-registry", {
".npmrc": `registry=${url}\n@v6:registry=${url}\n//[::1]:${server.port}/:_authToken=v6-SECRET-token\n`,
"package.json": JSON.stringify({
name: "app",
version: "1.0.0",
dependencies: { "no-deps": "1.0.0", "@v6/no-deps": "1.0.0" },
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "install", "--no-cache"],
cwd: String(dir),
// An ambient proxy would intercept the requests to the local registry.
env: { ...env, http_proxy: "", https_proxy: "", HTTP_PROXY: "", HTTPS_PROXY: "" },
stdout: "pipe",
stderr: "pipe",
});
const [, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(reqs.toSorted((a, b) => a.path.localeCompare(b.path))).toEqual([
{ path: "/@v6%2fno-deps", auth: "Bearer v6-SECRET-token" },
{ path: "/no-deps", auth: "Bearer v6-SECRET-token" },
]);
// The registry answers 404 to both manifest requests, so the install itself fails.
expect(exitCode).not.toBe(0);
});
});