From d8abf74542359aaf2d14afe38a6b5a4c662de61d Mon Sep 17 00:00:00 2001 From: Will King Date: Wed, 16 Sep 2026 13:47:21 -0500 Subject: [PATCH 1/2] Put cb login activation code in the URL fragment. Keep the code out of the query string so it is not sent to the dashboard server or written to access logs. Co-authored-by: Cursor --- CHANGELOG.md | 4 ++++ spec/cb/login_spec.cr | 4 ++++ src/cb/login.cr | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcf9654..5efc389 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Changed +- `cb login` now puts the activation code in the URL fragment (`#code=`) + instead of the query string, so it is not sent to the server or written + to access logs. ## [3.7.1] - 2026-08-13 ### Fixed diff --git a/spec/cb/login_spec.cr b/spec/cb/login_spec.cr index fda6908..0781e91 100644 --- a/spec/cb/login_spec.cr +++ b/spec/cb/login_spec.cr @@ -36,6 +36,8 @@ Spectator.describe CB::Login do result = action.call expect(result).to_not be_empty + expect(action.output.to_s).to contain "#code=" + expect(action.output.to_s).to_not contain "?code=" expect(action.output.to_s.ends_with?("Logged in as #{account.email}\n")).to be_true end @@ -49,6 +51,8 @@ Spectator.describe CB::Login do result = action.call expect(result).to_not be_empty + expect(action.output.to_s).to contain "#code=" + expect(action.output.to_s).to_not contain "?code=" expect(action.output.to_s.ends_with?("Logged in as #{account.email}\n")).to be_true end diff --git a/src/cb/login.cr b/src/cb/login.cr index d0fb156..970ad20 100644 --- a/src/cb/login.cr +++ b/src/cb/login.cr @@ -60,7 +60,8 @@ module CB # Request a session intent. si_params = Client::SessionIntentCreateParams.new(agent_name: "cb #{CB::VERSION}") session_intent = @client.create_session_intent si_params - login_url = "https://#{DASHBOARD_HOST}/account/verify-cli/#{session_intent.id}?code=#{session_intent.code}" + # Fragment (`#code=`) is not sent to the server or written to access logs. + login_url = "https://#{DASHBOARD_HOST}/account/verify-cli/#{session_intent.id}#code=#{session_intent.code}" # Start polling for completion of session authentication. poll_login_channel = Channel(LoginResult).new From 00b979f3b950e687c02e56a6202b2ce2a745a8de Mon Sep 17 00:00:00 2001 From: Adam Brightwell Date: Thu, 17 Sep 2026 13:58:24 -0400 Subject: [PATCH 2/2] Assert the login URL passed to the browser. Our specs checked the login URL we print, but not the one we hand to the browser, which is the URL that has to carry the code in the fragment. We now assert the full expected URL at both call sites. --- CHANGELOG.md | 3 +-- spec/cb/login_spec.cr | 17 ++++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5efc389..558761c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed - `cb login` now puts the activation code in the URL fragment (`#code=`) - instead of the query string, so it is not sent to the server or written - to access logs. + instead of the query string. ## [3.7.1] - 2026-08-13 ### Fixed diff --git a/spec/cb/login_spec.cr b/spec/cb/login_spec.cr index 0781e91..f0bc511 100644 --- a/spec/cb/login_spec.cr +++ b/spec/cb/login_spec.cr @@ -15,6 +15,7 @@ Spectator.describe CB::Login do let(process_mock) { class_mock(Process) } let(account) { Factory.account } + let(session_intent) { Factory.session_intent } before_each { ENV["CB_API_KEY"] = nil @@ -24,9 +25,11 @@ Spectator.describe CB::Login do } it "creates and stores a new session (browser)" do + expected_url = "https://#{CB::DASHBOARD_HOST}/account/verify-cli/#{session_intent.id}#code=#{session_intent.code}" + expect(lib_open_mock).to receive(:can_open_browser?).and_return(true) - expect(lib_open_mock).to receive(:run).and_return(true) - expect(client).to receive(:create_session_intent).and_return(Factory.session_intent) + expect(lib_open_mock).to receive(:run).with([expected_url]).and_return(true) + expect(client).to receive(:create_session_intent).and_return(session_intent) expect(client).to receive(:get_account).and_return(account) expect(client).to receive(:get_session_intent).and_return( Factory.session_intent(expires_at: Time.utc + 1.day, session: Factory.session) @@ -36,14 +39,15 @@ Spectator.describe CB::Login do result = action.call expect(result).to_not be_empty - expect(action.output.to_s).to contain "#code=" - expect(action.output.to_s).to_not contain "?code=" + expect(action.output.to_s).to contain expected_url expect(action.output.to_s.ends_with?("Logged in as #{account.email}\n")).to be_true end it "creates and stores a new session (headless)" do + expected_url = "https://#{CB::DASHBOARD_HOST}/account/verify-cli/#{session_intent.id}#code=#{session_intent.code}" + expect(lib_open_mock).to receive(:can_open_browser?).and_return(false) - expect(client).to receive(:create_session_intent).and_return(Factory.session_intent) + expect(client).to receive(:create_session_intent).and_return(session_intent) expect(client).to receive(:get_account).and_return(account) expect(client).to receive(:get_session_intent).and_return( Factory.session_intent(expires_at: Time.utc + 1.day, session: Factory.session) @@ -51,8 +55,7 @@ Spectator.describe CB::Login do result = action.call expect(result).to_not be_empty - expect(action.output.to_s).to contain "#code=" - expect(action.output.to_s).to_not contain "?code=" + expect(action.output.to_s).to contain expected_url expect(action.output.to_s.ends_with?("Logged in as #{account.email}\n")).to be_true end