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
65 changes: 38 additions & 27 deletions src/runtime/cli/publish_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ impl PublishCommand {
},
ctx.uses_workspaces,
ctx.manager.options.publish_config.auth_type,
true,
)?;

let mut response_buf = MutableString::init(1024)?;
Expand Down Expand Up @@ -1050,6 +1051,7 @@ impl PublishCommand {
Some(&otp),
ctx.uses_workspaces,
ctx.manager.options.publish_config.auth_type,
true,
)?;

response_buf.reset();
Expand Down Expand Up @@ -1184,16 +1186,15 @@ impl PublishCommand {
break 'try_web;
};
let done_url = URL::parse(crate::cli::cli_dupe(done_url_str));
{
let registry_url = registry.url.url();
if !(done_url.is_http() || done_url.is_https())
|| done_url.protocol != registry_url.protocol
|| done_url.hostname != registry_url.hostname
|| done_url.get_port_auto() != registry_url.get_port_auto()
{
break 'try_web;
}
if !(done_url.is_http() || done_url.is_https()) {
break 'try_web;
}
let done_url_same_origin = {
let registry_url = registry.url.url();
done_url.is_https() == registry_url.is_https()
&& done_url.hostname == registry_url.hostname
&& done_url.get_port_auto() == registry_url.get_port_auto()
};

if auth_url_is_web {
bun_core::prettyln!(
Expand Down Expand Up @@ -1285,6 +1286,7 @@ impl PublishCommand {
None,
ctx.uses_workspaces,
ctx.manager.options.publish_config.auth_type,
done_url_same_origin,
)?;

loop {
Expand Down Expand Up @@ -1915,6 +1917,7 @@ impl PublishCommand {
maybe_otp: Option<&[u8]>,
uses_workspaces: bool,
auth_type: Option<AuthType>,
include_auth: bool,
) -> Result<http::HeaderBuilder, AllocError> {
let mut headers = http::HeaderBuilder::default();
let npm_auth_type: &[u8] = if maybe_otp.is_none() {
Expand All @@ -1932,14 +1935,16 @@ impl PublishCommand {
headers.count(b"accept", b"*/*");
headers.count(b"accept-encoding", b"gzip,deflate");

if !registry.token.is_empty() {
write!(print_buf, "Bearer {}", bstr::BStr::new(&registry.token)).ok();
headers.count(b"authorization", &**print_buf);
print_buf.clear();
} else if !registry.auth.is_empty() {
write!(print_buf, "Basic {}", bstr::BStr::new(&registry.auth)).ok();
headers.count(b"authorization", &**print_buf);
print_buf.clear();
if include_auth {
if !registry.token.is_empty() {
write!(print_buf, "Bearer {}", bstr::BStr::new(&registry.token)).ok();
headers.count(b"authorization", &**print_buf);
print_buf.clear();
} else if !registry.auth.is_empty() {
write!(print_buf, "Basic {}", bstr::BStr::new(&registry.auth)).ok();
headers.count(b"authorization", &**print_buf);
print_buf.clear();
}
}

if maybe_json_len.is_some() {
Expand Down Expand Up @@ -1969,7 +1974,9 @@ impl PublishCommand {
print_buf.clear();

headers.count(b"Connection", b"keep-alive");
headers.count(b"Host", registry.url.url().host);
if include_auth {
headers.count(b"Host", registry.url.url().host);
}

if let Some(json_len) = maybe_json_len {
write!(print_buf, "{}", json_len).ok();
Expand All @@ -1984,14 +1991,16 @@ impl PublishCommand {
headers.append(b"accept", b"*/*");
headers.append(b"accept-encoding", b"gzip,deflate");

if !registry.token.is_empty() {
write!(print_buf, "Bearer {}", bstr::BStr::new(&registry.token)).ok();
headers.append(b"authorization", &**print_buf);
print_buf.clear();
} else if !registry.auth.is_empty() {
write!(print_buf, "Basic {}", bstr::BStr::new(&registry.auth)).ok();
headers.append(b"authorization", &**print_buf);
print_buf.clear();
if include_auth {
if !registry.token.is_empty() {
write!(print_buf, "Bearer {}", bstr::BStr::new(&registry.token)).ok();
headers.append(b"authorization", &**print_buf);
print_buf.clear();
} else if !registry.auth.is_empty() {
write!(print_buf, "Basic {}", bstr::BStr::new(&registry.auth)).ok();
headers.append(b"authorization", &**print_buf);
print_buf.clear();
}
}

if maybe_json_len.is_some() {
Expand Down Expand Up @@ -2021,7 +2030,9 @@ impl PublishCommand {
print_buf.clear();

headers.append(b"Connection", b"keep-alive");
headers.append(b"Host", registry.url.url().host);
if include_auth {
headers.append(b"Host", registry.url.url().host);
}

if let Some(json_len) = maybe_json_len {
write!(print_buf, "{}", json_len).ok();
Expand Down
34 changes: 22 additions & 12 deletions test/cli/install/bun-publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ describe("otp", async () => {
const { packageDir, packageJson } = await registry.createTestDir();
const token = await registry.generateUser("otp-classic-fallback", "otp");

let doneHits = 0;
const doneAuthHeaders: (string | null)[] = [];
using mockRegistry = Bun.serve({
port: 0,
fetch(req: Request) {
Expand All @@ -192,7 +192,7 @@ describe("otp", async () => {
);
}
if (req.url.endsWith("done")) {
doneHits++;
doneAuthHeaders.push(req.headers.get("authorization"));
return new Response(JSON.stringify({ token }), { status: 200 });
}
return new Response("unexpected url", { status: 500 });
Expand Down Expand Up @@ -234,29 +234,36 @@ describe("otp", async () => {
expect(out).toContain("customapp://login");
expect(out).not.toContain("open in browser");
expect(out).not.toContain("Enter OTP: ");
expect(doneHits).toBeGreaterThan(0);
// same-origin done url polling keeps the registry's credentials attached
expect(doneAuthHeaders).toEqual([`Bearer ${token}`]);
expect(out).toContain(" + otp-pkg-5@5.5.5");
expect(exitCode).toBe(0);
});

test("done url on a different origin is not polled and login falls back to the OTP prompt", async () => {
test("done url on a different origin is polled without credentials", async () => {
const packageDir = tmpdirSync();
const otpCode = "424242";
const registryToken = "registry-secret-token";

let foreignDoneHits = 0;
const foreignDoneRequests: { authorization: string | null; host: string | null }[] = [];
using foreign = Bun.serve({
port: 0,
fetch() {
foreignDoneHits++;
fetch(req: Request) {
foreignDoneRequests.push({
authorization: req.headers.get("authorization"),
host: req.headers.get("host"),
});
return new Response(JSON.stringify({ token: otpCode }), { status: 200 });
},
});

let localDoneHits = 0;
const registryAuthHeaders: (string | null)[] = [];
using mockRegistry = Bun.serve({
port: 0,
fetch(req: Request) {
if (req.method === "PUT") {
registryAuthHeaders.push(req.headers.get("authorization"));
if (req.headers.get("npm-otp") === otpCode) {
return new Response("OK", { status: 200 });
}
Expand All @@ -279,7 +286,7 @@ describe("otp", async () => {
await Promise.all([
write(
join(packageDir, "bunfig.toml"),
`[install]\ncache = false\nregistry = { url = "http://localhost:${mockRegistry.port}", token = "unused" }\n`,
`[install]\ncache = false\nregistry = { url = "http://localhost:${mockRegistry.port}", token = "${registryToken}" }\n`,
),
write(join(packageDir, "package.json"), JSON.stringify({ name: "otp-pkg-6", version: "6.6.6" })),
]);
Expand All @@ -289,15 +296,18 @@ describe("otp", async () => {
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: Buffer.from(otpCode + "\n"),
stdin: "ignore",
env,
});

const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(out).toContain("Enter OTP: ");
expect(out).not.toContain("Authenticate your account at");
expect(foreignDoneHits).toBe(0);
expect(out).toContain("Authenticate your account at");
expect(out).not.toContain("Enter OTP: ");
// the cross-origin done url must be polled, without the registry's credentials
expect(foreignDoneRequests).toEqual([{ authorization: null, host: `127.0.0.1:${foreign.port}` }]);
// the registry itself still receives credentials on both PUTs
expect(registryAuthHeaders).toEqual([`Bearer ${registryToken}`, `Bearer ${registryToken}`]);
expect(localDoneHits).toBe(0);
expect(out).toContain(" + otp-pkg-6@6.6.6");
expect(exitCode).toBe(0);
Expand Down
Loading