From 33d5b1f701066724afc7a4429f456d8c4b11260d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 01:42:20 +0000 Subject: [PATCH 1/6] fix(cube-cli): version from lerna.json at build time, 'Cube CLI' in --version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release version-bump commit updates lerna.json but not the crate manifest, so source builds (and any path that skipped the workflow's sed) reported a stale version. build.rs now reads the repo-root lerna.json and injects CUBE_CLI_VERSION at compile time — source and release builds both report the real Cube version, and the workflow's sed step is replaced with a tag-vs-lerna.json consistency check. The Cargo.toml version is a 0.0.0 placeholder used only for out-of-tree builds. --version now prints 'Cube CLI 1.7.5' (clap display_name) instead of 'cube 1.7.5'; version propagation to subcommands is dropped (it rendered as 'Cube CLI-deployments'). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VdcyqFwUF9BbWLeX4uERnk --- .github/workflows/publish.yml | 11 ++++++++--- rust/cube-cli/Cargo.lock | 2 +- rust/cube-cli/Cargo.toml | 11 ++++++++--- rust/cube-cli/README.md | 8 +++++--- rust/cube-cli/build.rs | 21 +++++++++++++++++++++ rust/cube-cli/src/client.rs | 2 +- rust/cube-cli/src/commands/login.rs | 2 +- rust/cube-cli/src/commands/update.rs | 2 +- rust/cube-cli/src/main.rs | 6 +++--- rust/cube-cli/src/telemetry.rs | 4 ++-- rust/cube-cli/src/update.rs | 4 ++-- 11 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 rust/cube-cli/build.rs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 81952ae98409d..1d535061c2f87 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -803,12 +803,17 @@ jobs: with: target: ${{ matrix.target }} cache-workspaces: rust/cube-cli - - name: Set crate version to the release version + # The CLI version comes from lerna.json at build time (see + # rust/cube-cli/build.rs); just guard against tag/lerna drift. + - name: Check release version matches lerna.json shell: bash run: | v="${GITHUB_REF_NAME#v}" - sed -i.bak "s/^version = .*/version = \"$v\"/" rust/cube-cli/Cargo.toml - rm -f rust/cube-cli/Cargo.toml.bak + l=$(jq -r .version lerna.json) + if [ "$v" != "$l" ]; then + echo "tag version $v does not match lerna.json version $l" >&2 + exit 1 + fi - name: Build working-directory: rust/cube-cli run: cargo build --release --target ${{ matrix.target }} diff --git a/rust/cube-cli/Cargo.lock b/rust/cube-cli/Cargo.lock index 68735fccb01d3..d9493ffe27bf2 100644 --- a/rust/cube-cli/Cargo.lock +++ b/rust/cube-cli/Cargo.lock @@ -283,7 +283,7 @@ dependencies = [ [[package]] name = "cube-cli" -version = "1.7.2" +version = "0.0.0" dependencies = [ "anyhow", "clap", diff --git a/rust/cube-cli/Cargo.toml b/rust/cube-cli/Cargo.toml index 9142ff06e353b..b41f3c526ec5d 100644 --- a/rust/cube-cli/Cargo.toml +++ b/rust/cube-cli/Cargo.toml @@ -1,9 +1,11 @@ [package] name = "cube-cli" -# Kept in lockstep with the Cube monorepo version (lerna.json). The release -# workflow overrides this from the pushed `v*` tag at build time. -version = "1.7.2" +# Placeholder — the real version is the Cube monorepo version, read from the +# repo-root lerna.json by build.rs at build time (CUBE_CLI_VERSION). This +# field is only the fallback for builds outside the monorepo. +version = "0.0.0" edition = "2021" +build = "build.rs" description = "Cube Cloud command line interface" license = "Apache-2.0" repository = "https://github.com/cube-js/cube" @@ -40,6 +42,9 @@ etcetera = "0.8" [target.'cfg(unix)'.dependencies] libc = "0.2" +[build-dependencies] +serde_json = "1" + [profile.release] lto = "thin" strip = true diff --git a/rust/cube-cli/README.md b/rust/cube-cli/README.md index ed45f6e28f5d7..67aede1a13849 100644 --- a/rust/cube-cli/README.md +++ b/rust/cube-cli/README.md @@ -58,9 +58,11 @@ no OpenSSL dependency and musl builds work out of the box. ## Versioning & releases -The CLI version tracks the Cube monorepo version (`lerna.json`); `Cargo.toml` -is kept in sync and the release build overrides it from the pushed tag, so -`cube --version` always matches the Cube release. +The CLI version tracks the Cube monorepo version: `build.rs` reads the +repo-root `lerna.json` at build time (the `Cargo.toml` version is only a +fallback for out-of-tree builds), so `cube --version` always reports the +real Cube version — for release builds and source builds alike. The release +workflow just verifies the pushed tag matches `lerna.json`. The CLI is built and published by the **same release workflow as the rest of Cube** (`.github/workflows/publish.yml`, on `v*.*.*` tags). Its `cube-cli` diff --git a/rust/cube-cli/build.rs b/rust/cube-cli/build.rs new file mode 100644 index 0000000000000..2f994a8db3674 --- /dev/null +++ b/rust/cube-cli/build.rs @@ -0,0 +1,21 @@ +use std::path::Path; + +/// The CLI version tracks the Cube monorepo version. The single source of +/// truth is the repo-root `lerna.json` (bumped by every release commit), so +/// read it at build time instead of keeping Cargo.toml in sync by hand — +/// source builds and release builds then always report the real version. +/// Falls back to the crate version if lerna.json isn't present (e.g. the +/// crate directory built outside the monorepo). +fn main() { + println!("cargo:rerun-if-changed=../../lerna.json"); + let version = lerna_version().unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()); + println!("cargo:rustc-env=CUBE_CLI_VERSION={version}"); +} + +fn lerna_version() -> Option { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").ok()?; + let lerna = Path::new(&manifest_dir).join("../../lerna.json"); + let raw = std::fs::read_to_string(lerna).ok()?; + let json: serde_json::Value = serde_json::from_str(&raw).ok()?; + json.get("version")?.as_str().map(str::to_string) +} diff --git a/rust/cube-cli/src/client.rs b/rust/cube-cli/src/client.rs index a5ed881377a1f..a646c786bfad5 100644 --- a/rust/cube-cli/src/client.rs +++ b/rust/cube-cli/src/client.rs @@ -39,7 +39,7 @@ impl Client { } Ok(Self { http: reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build()?, base_url, token: Mutex::new(token.to_string()), diff --git a/rust/cube-cli/src/commands/login.rs b/rust/cube-cli/src/commands/login.rs index dba8f98fbc8ed..b4db894033cbb 100644 --- a/rust/cube-cli/src/commands/login.rs +++ b/rust/cube-cli/src/commands/login.rs @@ -64,7 +64,7 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { async fn device_login(url: &str) -> Result<(String, Option)> { let cfg = oauth::OAuthConfig::from_env(); let http = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build()?; let device = oauth::request_device_code(&http, url, &cfg).await?; diff --git a/rust/cube-cli/src/commands/update.rs b/rust/cube-cli/src/commands/update.rs index f2a44f6d3a190..8a04dfcc6c8ce 100644 --- a/rust/cube-cli/src/commands/update.rs +++ b/rust/cube-cli/src/commands/update.rs @@ -16,7 +16,7 @@ pub struct Args { pub async fn command(args: Args, _ctx: &Ctx) -> Result<()> { let http = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build()?; let release = update::latest_release(&http).await?; diff --git a/rust/cube-cli/src/main.rs b/rust/cube-cli/src/main.rs index 3cdd4d51f26af..5b49cd63ba858 100644 --- a/rust/cube-cli/src/main.rs +++ b/rust/cube-cli/src/main.rs @@ -13,9 +13,9 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command( name = "cube", - version, - about = "Cube Cloud command line interface", - propagate_version = true + display_name = "Cube CLI", + version = env!("CUBE_CLI_VERSION"), + about = "Cube Cloud command line interface" )] struct Cli { #[command(flatten)] diff --git a/rust/cube-cli/src/telemetry.rs b/rust/cube-cli/src/telemetry.rs index e0fef8fa6d66e..f782e85be5186 100644 --- a/rust/cube-cli/src/telemetry.rs +++ b/rust/cube-cli/src/telemetry.rs @@ -37,7 +37,7 @@ pub fn event(name: &str, props: Map) { } let mut payload = json!({ "event": name, - "cliVersion": env!("CARGO_PKG_VERSION"), + "cliVersion": env!("CUBE_CLI_VERSION"), "clientTimestamp": timestamp(), "id": random_id(), "platform": platform(), @@ -58,7 +58,7 @@ pub async fn flush() { return; } let Ok(http) = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .timeout(Duration::from_secs(2)) .build() else { diff --git a/rust/cube-cli/src/update.rs b/rust/cube-cli/src/update.rs index b084fb00d5f04..eee7a7874f359 100644 --- a/rust/cube-cli/src/update.rs +++ b/rust/cube-cli/src/update.rs @@ -15,7 +15,7 @@ fn release_api_base() -> String { std::env::var("CUBE_UPDATE_API").unwrap_or_else(|_| "https://api.github.com".to_string()) } -pub const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CURRENT_VERSION: &str = env!("CUBE_CLI_VERSION"); /// The release target triple this binary maps to. Linux always maps to the /// musl asset — that's the only Linux artifact we ship, and it runs anywhere. @@ -102,7 +102,7 @@ pub fn spawn_check() -> tokio::task::JoinHandle> { return None; } let http = reqwest::Client::builder() - .user_agent(concat!("cube-cli/", env!("CARGO_PKG_VERSION"))) + .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) .build() .ok()?; let release = latest_release(&http).await.ok()?; From 023b9a9efbdc4dc7a0497a8a8c459e9f5e171b4f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 02:04:22 +0000 Subject: [PATCH 2/6] feat(cube-cli): tenant discovery in cube login via generic sign-in Users don't always know their tenant URL. The login prompt now accepts an empty answer (with a help hint) and falls back to the generic https://cubecloud.dev sign-in, which resolves the tenant from the signed-in account; the device token response's tenantUrl is then used as the saved context URL and for credential validation. --url and --api-key behavior unchanged. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VdcyqFwUF9BbWLeX4uERnk --- rust/cube-cli/src/commands/login.rs | 33 +++++++++++++++++++++++------ rust/cube-cli/src/oauth.rs | 4 ++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/rust/cube-cli/src/commands/login.rs b/rust/cube-cli/src/commands/login.rs index b4db894033cbb..21de02c2e4616 100644 --- a/rust/cube-cli/src/commands/login.rs +++ b/rust/cube-cli/src/commands/login.rs @@ -5,9 +5,15 @@ use crate::client::Client; use crate::config::ContextConfig; use crate::{oauth, output, Ctx}; +/// Base used when the user doesn't know their tenant URL: the generic +/// sign-in resolves the tenant from the signed-in account and returns it +/// as `tenantUrl` in the token response. +const GENERIC_URL: &str = "https://cubecloud.dev"; + #[derive(clap::Args)] pub struct Args { - /// Cube Cloud URL, e.g. https://.cubecloud.dev + /// Cube Cloud URL, e.g. https://.cubecloud.dev (omit to sign in + /// via cubecloud.dev, which finds your tenant from your account) #[arg(long)] url: Option, /// Authenticate with an API key instead of the browser device flow @@ -23,13 +29,28 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { Some(url) => url, None => inquire::Text::new("Cube Cloud URL:") .with_placeholder("https://.cubecloud.dev") + .with_help_message( + "don't know your tenant? press Enter to sign in via cubecloud.dev \ + — it finds your tenant from your account", + ) .prompt()?, }; - let url = url.trim_end_matches('/').to_string(); + let mut url = url.trim().trim_end_matches('/').to_string(); + if url.is_empty() { + url = GENERIC_URL.to_string(); + } let (token, refresh_token) = match args.api_key { Some(key) => (key, None), - None => device_login(&url).await?, + None => { + let (token, refresh, tenant_url) = device_login(&url).await?; + // The generic sign-in reports which tenant the account belongs + // to — save and validate against that, not cubecloud.dev. + if let Some(tenant) = tenant_url { + url = tenant.trim_end_matches('/').to_string(); + } + (token, refresh) + } }; // Validate the credentials before saving them. @@ -60,8 +81,8 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { } /// Drive the OAuth 2.0 device authorization grant and return -/// (access_token, refresh_token). -async fn device_login(url: &str) -> Result<(String, Option)> { +/// (access_token, refresh_token, tenant_url). +async fn device_login(url: &str) -> Result<(String, Option, Option)> { let cfg = oauth::OAuthConfig::from_env(); let http = reqwest::Client::builder() .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) @@ -90,5 +111,5 @@ async fn device_login(url: &str) -> Result<(String, Option)> { println!("{}", "Waiting for authorization…".dimmed()); let token = oauth::poll_for_token(&http, url, &cfg, &device).await?; - Ok((token.access_token, token.refresh_token)) + Ok((token.access_token, token.refresh_token, token.tenant_url)) } diff --git a/rust/cube-cli/src/oauth.rs b/rust/cube-cli/src/oauth.rs index 812feafe642c0..1811bab109ccb 100644 --- a/rust/cube-cli/src/oauth.rs +++ b/rust/cube-cli/src/oauth.rs @@ -68,6 +68,10 @@ pub struct TokenResponse { pub access_token: String, #[serde(default, alias = "refreshToken")] pub refresh_token: Option, + /// The user's tenant URL, filled in by the generic cubecloud.dev sign-in + /// (which resolves the tenant from the signed-in account). + #[serde(default, alias = "tenantUrl")] + pub tenant_url: Option, } #[derive(Debug, Deserialize)] From ae002dadd071e0f616c50ebe3b43f8d8b56dd2c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 02:54:23 +0000 Subject: [PATCH 3/6] fix(cube-cli): detect API keys by sk- prefix, 'Cube CLI' in update notice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Bearer/Api-Key heuristic classified credentials by JWT shape, which broke device login: OAuth access tokens aren't necessarily three-segment JWTs, so freshly minted tokens were sent as Api-Key and the post-login /users/me validation failed with 401. Cube Cloud API keys carry an sk- prefix — use that as the discriminator: sk- → Api-Key, everything else → Bearer (CUBE_AUTH_SCHEME still overrides). Update-notice wording: 'A new release of Cube CLI is available' (and Cube CLI in cube update output) instead of 'cube'. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VdcyqFwUF9BbWLeX4uERnk --- rust/cube-cli/src/client.rs | 12 ++++++------ rust/cube-cli/src/commands/update.rs | 4 ++-- rust/cube-cli/src/update.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rust/cube-cli/src/client.rs b/rust/cube-cli/src/client.rs index a646c786bfad5..f5a8475c47edc 100644 --- a/rust/cube-cli/src/client.rs +++ b/rust/cube-cli/src/client.rs @@ -74,16 +74,16 @@ impl Client { self.token.lock().unwrap().clone() } - /// Authorization header value for a credential. JWTs (three dot-separated - /// segments — OAuth access tokens, legacy deploy JWTs) use the `Bearer` - /// scheme; opaque credentials are Cube Cloud API keys and use `Api-Key`. - /// `CUBE_AUTH_SCHEME=bearer|api-key` overrides the heuristic. + /// Authorization header value for a credential. Cube Cloud API keys are + /// prefixed `sk-` and use the `Api-Key` scheme; everything else (OAuth + /// access tokens — not necessarily JWTs — and legacy deploy JWTs) uses + /// `Bearer`. `CUBE_AUTH_SCHEME=bearer|api-key` overrides the detection. fn authorization(token: &str) -> String { let scheme = match std::env::var("CUBE_AUTH_SCHEME").as_deref() { Ok("bearer") | Ok("Bearer") => "Bearer", Ok("api-key") | Ok("Api-Key") => "Api-Key", - _ if token.split('.').count() == 3 => "Bearer", - _ => "Api-Key", + _ if token.starts_with("sk-") => "Api-Key", + _ => "Bearer", }; format!("{scheme} {token}") } diff --git a/rust/cube-cli/src/commands/update.rs b/rust/cube-cli/src/commands/update.rs index 8a04dfcc6c8ce..aca7b25bdd815 100644 --- a/rust/cube-cli/src/commands/update.rs +++ b/rust/cube-cli/src/commands/update.rs @@ -24,7 +24,7 @@ pub async fn command(args: Args, _ctx: &Ctx) -> Result<()> { if !is_newer(latest, CURRENT_VERSION) { output::success(&format!( - "cube {CURRENT_VERSION} is up to date (latest release: {latest})" + "Cube CLI {CURRENT_VERSION} is up to date (latest release: {latest})" )); return Ok(()); } @@ -57,7 +57,7 @@ pub async fn command(args: Args, _ctx: &Ctx) -> Result<()> { let new_binary = extract_binary(&bytes)?; replace_current_exe(&new_binary)?; - output::success(&format!("Updated cube {CURRENT_VERSION} → {latest}")); + output::success(&format!("Updated Cube CLI {CURRENT_VERSION} → {latest}")); Ok(()) } diff --git a/rust/cube-cli/src/update.rs b/rust/cube-cli/src/update.rs index eee7a7874f359..bd6bf09679b3c 100644 --- a/rust/cube-cli/src/update.rs +++ b/rust/cube-cli/src/update.rs @@ -110,7 +110,7 @@ pub fn spawn_check() -> tokio::task::JoinHandle> { if newer_than(&latest, CURRENT_VERSION) { Some(format!( "\n{} {} → {}\nRun {} to install it.", - "A new release of cube is available:".yellow(), + "A new release of Cube CLI is available:".yellow(), CURRENT_VERSION.dimmed(), latest.bold().green(), "cube update".bold().cyan(), From 398e864bcef76c5fa842baa358a9a9da9025f588 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 04:05:54 +0000 Subject: [PATCH 4/6] feat(cube-cli): CUBE_GENERIC_LOGIN_URL override for tenant-discovery login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Enter fallback in cube login targets https://cubecloud.dev, which doesn't serve the device-OAuth endpoints yet; the env override lets the generic sign-in be pointed at a console that does (verified against staging: Enter → device code minted, no 404). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VdcyqFwUF9BbWLeX4uERnk --- rust/cube-cli/src/commands/login.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rust/cube-cli/src/commands/login.rs b/rust/cube-cli/src/commands/login.rs index 21de02c2e4616..b6473b0b438d3 100644 --- a/rust/cube-cli/src/commands/login.rs +++ b/rust/cube-cli/src/commands/login.rs @@ -7,8 +7,11 @@ use crate::{oauth, output, Ctx}; /// Base used when the user doesn't know their tenant URL: the generic /// sign-in resolves the tenant from the signed-in account and returns it -/// as `tenantUrl` in the token response. -const GENERIC_URL: &str = "https://cubecloud.dev"; +/// as `tenantUrl` in the token response. `CUBE_GENERIC_LOGIN_URL` +/// overrides the host (e.g. to point at a staging console). +fn generic_url() -> String { + std::env::var("CUBE_GENERIC_LOGIN_URL").unwrap_or_else(|_| "https://cubecloud.dev".to_string()) +} #[derive(clap::Args)] pub struct Args { @@ -37,7 +40,7 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { }; let mut url = url.trim().trim_end_matches('/').to_string(); if url.is_empty() { - url = GENERIC_URL.to_string(); + url = generic_url(); } let (token, refresh_token) = match args.api_key { From 0edf4bf3723a2643eb8957dde077e7fa85d583fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 04:19:58 +0000 Subject: [PATCH 5/6] =?UTF-8?q?feat(cube-cli):=20route=20generic=20sign-in?= =?UTF-8?q?=20through=20/auth=3Fredirect=5Fto=3D=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the tenant-discovery flow the browser link is now {host}/auth?redirect_to= instead of the bare device page, so the user signs in first (which resolves their tenant) and lands on the approval page. Tenant-URL logins keep the direct verification link. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VdcyqFwUF9BbWLeX4uERnk --- rust/cube-cli/src/commands/login.rs | 37 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/rust/cube-cli/src/commands/login.rs b/rust/cube-cli/src/commands/login.rs index b6473b0b438d3..d54b0427db7e7 100644 --- a/rust/cube-cli/src/commands/login.rs +++ b/rust/cube-cli/src/commands/login.rs @@ -43,10 +43,11 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { url = generic_url(); } + let generic = url == generic_url(); let (token, refresh_token) = match args.api_key { Some(key) => (key, None), None => { - let (token, refresh, tenant_url) = device_login(&url).await?; + let (token, refresh, tenant_url) = device_login(&url, generic).await?; // The generic sign-in reports which tenant the account belongs // to — save and validate against that, not cubecloud.dev. if let Some(tenant) = tenant_url { @@ -85,7 +86,14 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { /// Drive the OAuth 2.0 device authorization grant and return /// (access_token, refresh_token, tenant_url). -async fn device_login(url: &str) -> Result<(String, Option, Option)> { +/// +/// In the generic (tenant-discovery) flow the browser link is routed +/// through the sign-in page — `{host}/auth?redirect_to=` — +/// so the user authenticates first and lands on the approval page. +async fn device_login( + url: &str, + generic: bool, +) -> Result<(String, Option, Option)> { let cfg = oauth::OAuthConfig::from_env(); let http = reqwest::Client::builder() .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) @@ -93,10 +101,15 @@ async fn device_login(url: &str) -> Result<(String, Option, Option Result<(String, Option, Option Option<&str> { + let after_scheme = url.split_once("://").map(|(_, r)| r).unwrap_or(url); + after_scheme.find('/').map(|i| &after_scheme[i..]) +} + +/// Minimal percent-encoding for a query-parameter value. +fn percent_encode(raw: &str) -> String { + raw.bytes() + .map(|b| match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + (b as char).to_string() + } + _ => format!("%{b:02X}"), + }) + .collect() +} From 42948d9f8c3cebe738ad0456bca82b8f115735e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 04:37:23 +0000 Subject: [PATCH 6/6] revert(cube-cli): drop tenant-discovery login fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There's no server to mint device codes on the generic cubecloud.dev host, so the Enter fallback can't work — POST /auth/device/code 404s before the browser ever opens. Reverts the generic sign-in fallback, the /auth?redirect_to= link wrapping, the CUBE_GENERIC_LOGIN_URL override, and the tenantUrl token field; cube login requires the tenant URL again. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VdcyqFwUF9BbWLeX4uERnk --- rust/cube-cli/src/commands/login.rs | 69 +++-------------------------- rust/cube-cli/src/oauth.rs | 4 -- 2 files changed, 7 insertions(+), 66 deletions(-) diff --git a/rust/cube-cli/src/commands/login.rs b/rust/cube-cli/src/commands/login.rs index d54b0427db7e7..f50344848d900 100644 --- a/rust/cube-cli/src/commands/login.rs +++ b/rust/cube-cli/src/commands/login.rs @@ -5,18 +5,9 @@ use crate::client::Client; use crate::config::ContextConfig; use crate::{oauth, output, Ctx}; -/// Base used when the user doesn't know their tenant URL: the generic -/// sign-in resolves the tenant from the signed-in account and returns it -/// as `tenantUrl` in the token response. `CUBE_GENERIC_LOGIN_URL` -/// overrides the host (e.g. to point at a staging console). -fn generic_url() -> String { - std::env::var("CUBE_GENERIC_LOGIN_URL").unwrap_or_else(|_| "https://cubecloud.dev".to_string()) -} - #[derive(clap::Args)] pub struct Args { - /// Cube Cloud URL, e.g. https://.cubecloud.dev (omit to sign in - /// via cubecloud.dev, which finds your tenant from your account) + /// Cube Cloud URL, e.g. https://.cubecloud.dev #[arg(long)] url: Option, /// Authenticate with an API key instead of the browser device flow @@ -32,29 +23,13 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { Some(url) => url, None => inquire::Text::new("Cube Cloud URL:") .with_placeholder("https://.cubecloud.dev") - .with_help_message( - "don't know your tenant? press Enter to sign in via cubecloud.dev \ - — it finds your tenant from your account", - ) .prompt()?, }; - let mut url = url.trim().trim_end_matches('/').to_string(); - if url.is_empty() { - url = generic_url(); - } + let url = url.trim().trim_end_matches('/').to_string(); - let generic = url == generic_url(); let (token, refresh_token) = match args.api_key { Some(key) => (key, None), - None => { - let (token, refresh, tenant_url) = device_login(&url, generic).await?; - // The generic sign-in reports which tenant the account belongs - // to — save and validate against that, not cubecloud.dev. - if let Some(tenant) = tenant_url { - url = tenant.trim_end_matches('/').to_string(); - } - (token, refresh) - } + None => device_login(&url).await?, }; // Validate the credentials before saving them. @@ -85,15 +60,8 @@ pub async fn command(args: Args, ctx: &mut Ctx) -> Result<()> { } /// Drive the OAuth 2.0 device authorization grant and return -/// (access_token, refresh_token, tenant_url). -/// -/// In the generic (tenant-discovery) flow the browser link is routed -/// through the sign-in page — `{host}/auth?redirect_to=` — -/// so the user authenticates first and lands on the approval page. -async fn device_login( - url: &str, - generic: bool, -) -> Result<(String, Option, Option)> { +/// (access_token, refresh_token). +async fn device_login(url: &str) -> Result<(String, Option)> { let cfg = oauth::OAuthConfig::from_env(); let http = reqwest::Client::builder() .user_agent(concat!("cube-cli/", env!("CUBE_CLI_VERSION"))) @@ -101,15 +69,10 @@ async fn device_login( let device = oauth::request_device_code(&http, url, &cfg).await?; - let mut verification = device + let verification = device .verification_uri_complete .clone() .unwrap_or_else(|| device.verification_uri.clone()); - if generic { - if let Some(path) = path_and_query(&verification) { - verification = format!("{url}/auth?redirect_to={}", percent_encode(path)); - } - } println!(); println!("To authorize this CLI, open the following URL in your browser:"); @@ -127,23 +90,5 @@ async fn device_login( println!("{}", "Waiting for authorization…".dimmed()); let token = oauth::poll_for_token(&http, url, &cfg, &device).await?; - Ok((token.access_token, token.refresh_token, token.tenant_url)) -} - -/// Path + query of an absolute URL (`https://host/a/b?c=d` → `/a/b?c=d`). -fn path_and_query(url: &str) -> Option<&str> { - let after_scheme = url.split_once("://").map(|(_, r)| r).unwrap_or(url); - after_scheme.find('/').map(|i| &after_scheme[i..]) -} - -/// Minimal percent-encoding for a query-parameter value. -fn percent_encode(raw: &str) -> String { - raw.bytes() - .map(|b| match b { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { - (b as char).to_string() - } - _ => format!("%{b:02X}"), - }) - .collect() + Ok((token.access_token, token.refresh_token)) } diff --git a/rust/cube-cli/src/oauth.rs b/rust/cube-cli/src/oauth.rs index 1811bab109ccb..812feafe642c0 100644 --- a/rust/cube-cli/src/oauth.rs +++ b/rust/cube-cli/src/oauth.rs @@ -68,10 +68,6 @@ pub struct TokenResponse { pub access_token: String, #[serde(default, alias = "refreshToken")] pub refresh_token: Option, - /// The user's tenant URL, filled in by the generic cubecloud.dev sign-in - /// (which resolves the tenant from the signed-in account). - #[serde(default, alias = "tenantUrl")] - pub tenant_url: Option, } #[derive(Debug, Deserialize)]