From 87ec014d12fe6bf082100cf30d6800b0403396de Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 20 Oct 2026 05:53:46 +0000 Subject: [PATCH 1/2] Fix --status to validate against the configured sync server --status validated the session against creds.server_url instead of config.sync.server_url, so after re-registering on a different server the check silently probed the stale credentials URL. login/register also re-prompted for a server URL even when sync was already enabled. - status: validate against config.sync.server_url, warn on mismatch, and print a re-auth hint on failure - login/register: fall back to the configured server URL when --server is omitted and sync is enabled --- crates/taskbook-client/src/auth.rs | 84 +++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/crates/taskbook-client/src/auth.rs b/crates/taskbook-client/src/auth.rs index 614e04b..fca1940 100644 --- a/crates/taskbook-client/src/auth.rs +++ b/crates/taskbook-client/src/auth.rs @@ -25,6 +25,17 @@ fn prompt_password(message: &str) -> Result { .map_err(|e| TaskbookError::General(format!("failed to read password: {e}"))) } +/// Resolve the server URL for auth commands: an explicit `--server` value +/// wins; otherwise fall back to the configured sync server when sync is +/// already enabled. +fn configured_server_url(flag: Option<&str>, config: &Config) -> Option { + match flag { + Some(s) => Some(s.to_string()), + None if config.sync.enabled => Some(config.sync.server_url.clone()), + None => None, + } +} + /// Register a new account on the server (interactive). pub fn register( server_url: Option<&str>, @@ -35,8 +46,14 @@ pub fn register( println!("{}", "Register new account".bold()); println!(); - let server = match server_url { - Some(s) => s.to_string(), + let mut config = Config::load_or_default(); + let server = match configured_server_url(server_url, &config) { + Some(s) => { + if server_url.is_none() { + println!("Using server: {}", s); + } + s + } None => prompt("Server URL: ")?, }; @@ -83,7 +100,6 @@ pub fn register( creds.save()?; // Enable sync in config - let mut config = Config::load_or_default(); config.enable_sync(&server)?; println!(); @@ -111,8 +127,14 @@ pub fn login( println!("{}", "Login".bold()); println!(); - let server = match server_url { - Some(s) => s.to_string(), + let mut config = Config::load_or_default(); + let server = match configured_server_url(server_url, &config) { + Some(s) => { + if server_url.is_none() { + println!("Using server: {}", s); + } + s + } None => prompt("Server URL: ")?, }; @@ -146,7 +168,6 @@ pub fn login( creds.save()?; // Enable sync in config - let mut config = Config::load_or_default(); config.enable_sync(&server)?; println!(); @@ -193,7 +214,19 @@ pub fn status() -> Result<()> { println!("Server URL: {}", creds.server_url); if config.sync.enabled { - let client = ApiClient::new(&creds.server_url, Some(&creds.token)); + if creds.server_url != config.sync.server_url { + println!( + "{}", + format!( + "Warning: credentials were saved for {} (config points to {})", + creds.server_url, config.sync.server_url + ) + .yellow() + ); + } + // Validate the session against the configured server (user's + // intent), not a potentially stale credentials URL. + let client = ApiClient::new(&config.sync.server_url, Some(&creds.token)); match client.get_me() { Ok(me) => { println!("Session: {}", "valid".green()); @@ -202,6 +235,7 @@ pub fn status() -> Result<()> { Err(e) => { println!("Session: {}", "invalid".red()); println!("Error: {}", e); + println!("{}", "Hint: run `tb --login` to re-authenticate.".dimmed()); } } } @@ -213,3 +247,39 @@ pub fn status() -> Result<()> { Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + fn config_with_sync(enabled: bool, server_url: &str) -> Config { + let mut config = Config::default(); + config.sync.enabled = enabled; + config.sync.server_url = server_url.to_string(); + config + } + + #[test] + fn flag_wins_over_configured_server() { + let config = config_with_sync(true, "http://configured.example"); + assert_eq!( + configured_server_url(Some("http://flag.example"), &config), + Some("http://flag.example".to_string()) + ); + } + + #[test] + fn configured_server_used_when_sync_enabled() { + let config = config_with_sync(true, "http://configured.example"); + assert_eq!( + configured_server_url(None, &config), + Some("http://configured.example".to_string()) + ); + } + + #[test] + fn no_fallback_when_sync_disabled() { + let config = config_with_sync(false, "http://configured.example"); + assert_eq!(configured_server_url(None, &config), None); + } +} From bf12160a20f1663c78710436263a90949e686bf0 Mon Sep 17 00:00:00 2001 From: tobiashochguertel Date: Tue, 27 Oct 2026 06:48:43 +0000 Subject: [PATCH 2/2] Skip session validation on server mismatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --status sent the saved bearer token to the configured server even when the credentials were issued by a different one. Compare normalized URLs and skip the authenticated request on mismatch, directing the user to re-authenticate. Co-Authored-By: Tobias Hochgürtel --- crates/taskbook-client/src/auth.rs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/taskbook-client/src/auth.rs b/crates/taskbook-client/src/auth.rs index fca1940..0478eda 100644 --- a/crates/taskbook-client/src/auth.rs +++ b/crates/taskbook-client/src/auth.rs @@ -25,6 +25,12 @@ fn prompt_password(message: &str) -> Result { .map_err(|e| TaskbookError::General(format!("failed to read password: {e}"))) } +/// Compare two server URLs using the same trailing-slash normalization as +/// `ApiClient::new`. +fn same_server_url(a: &str, b: &str) -> bool { + a.trim_end_matches('/') == b.trim_end_matches('/') +} + /// Resolve the server URL for auth commands: an explicit `--server` value /// wins; otherwise fall back to the configured sync server when sync is /// already enabled. @@ -214,7 +220,7 @@ pub fn status() -> Result<()> { println!("Server URL: {}", creds.server_url); if config.sync.enabled { - if creds.server_url != config.sync.server_url { + if !same_server_url(&creds.server_url, &config.sync.server_url) { println!( "{}", format!( @@ -223,6 +229,15 @@ pub fn status() -> Result<()> { ) .yellow() ); + // A bearer token issued by one server must not be sent + // to another; skip validation and require re-auth. + println!("Session: {}", "not checked".yellow()); + println!( + "{}", + "Hint: run `tb --login` to authenticate against the configured server." + .dimmed() + ); + return Ok(()); } // Validate the session against the configured server (user's // intent), not a potentially stale credentials URL. @@ -282,4 +297,11 @@ mod tests { let config = config_with_sync(false, "http://configured.example"); assert_eq!(configured_server_url(None, &config), None); } + + #[test] + fn same_server_url_ignores_trailing_slash() { + assert!(same_server_url("http://a.example", "http://a.example/")); + assert!(same_server_url("http://a.example/", "http://a.example")); + assert!(!same_server_url("http://a.example", "http://b.example")); + } }