aboutsummaryrefslogtreecommitdiffstats
path: root/crates/atuin-client/src/settings.rs
diff options
context:
space:
mode:
authorMichelle Tilley <michelle@michelletilley.net>2026-03-23 09:33:04 -0700
committerGitHub <noreply@github.com>2026-03-23 09:33:04 -0700
commit7f06ba0ee93eebf4482a7eb5d5d25e9d8a072f9d (patch)
tree20f214ce5d0ac08dc6eee0beb2c3c70128050a8e /crates/atuin-client/src/settings.rs
parentfeat: hex init nu (#3330) (diff)
downloadatuin-7f06ba0ee93eebf4482a7eb5d5d25e9d8a072f9d.zip
chore: Refactor CLI auth flows and token storage (#3317)
This PR eplaces the binary `is_hub_sync()` auth routing with an explicit `SyncAuth` enum that classifies the client's authentication state at runtime. This fixes a class of bugs where CLI session tokens were silently mis-stored or used with the wrong auth scheme during Hub migration.
Diffstat (limited to '')
-rw-r--r--crates/atuin-client/src/settings.rs125
1 files changed, 99 insertions, 26 deletions
diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs
index becf72db..5b18d9ea 100644
--- a/crates/atuin-client/src/settings.rs
+++ b/crates/atuin-client/src/settings.rs
@@ -383,6 +383,45 @@ pub enum SyncProtocol {
Auto,
}
+/// Resolved authentication state for sync operations.
+///
+/// Determined at runtime by examining which tokens are available and what
+/// server the client is configured to talk to. Operations use this to pick
+/// the right auth header and endpoint style.
+#[cfg(feature = "sync")]
+#[derive(Debug, Clone)]
+pub enum SyncAuth {
+ /// Self-hosted Rust server. Uses `Authorization: Token <session>` and
+ /// legacy endpoints.
+ Legacy { token: String },
+ /// Hub with a valid Hub API token (`atapi_*`). Uses
+ /// `Authorization: Bearer <token>` and v0 endpoints.
+ Hub { token: String },
+ /// Targeting Hub but only has a CLI session token. Uses
+ /// `Authorization: Token <session>` against compat/record endpoints.
+ /// Sync, password change, and account deletion still work, but the user
+ /// should be nudged to run `atuin login` for full Hub auth.
+ HubViaCli { token: String },
+ /// Not authenticated at all. Contains an actionable user-facing message.
+ NotLoggedIn { reason: String },
+}
+
+#[cfg(feature = "sync")]
+impl SyncAuth {
+ /// Convert into the auth token type used by the API client.
+ ///
+ /// Returns an error with an actionable message for `NotLoggedIn`.
+ pub fn into_auth_token(self) -> Result<crate::api_client::AuthToken> {
+ use crate::api_client::AuthToken;
+ match self {
+ SyncAuth::Legacy { token } => Ok(AuthToken::Token(token)),
+ SyncAuth::Hub { token } => Ok(AuthToken::Bearer(token)),
+ SyncAuth::HubViaCli { token } => Ok(AuthToken::Token(token)),
+ SyncAuth::NotLoggedIn { reason } => Err(eyre!(reason)),
+ }
+ }
+}
+
#[derive(Clone, Debug, Deserialize, Default, Serialize)]
pub struct Keys {
pub scroll_exits: bool,
@@ -1239,40 +1278,74 @@ impl Settings {
}
}
- /// Returns the best available auth token for sync operations.
- ///
- /// Token priority when using Hub sync:
- /// 1. Hub token (Bearer) - enables unified Hub auth
- /// 2. CLI session token (Token) - fallback if Hub token revoked
- ///
- /// For legacy/self-hosted sync, only CLI session token is used.
- ///
- /// Hub tokens are preferred when available because they provide unified
- /// authentication across CLI and Hub features, and users can manage them
- /// via the Hub web interface.
+ /// Examines the configured sync target and available tokens to determine
+ /// the correct auth strategy. Also performs cleanup of mis-stored tokens
+ /// (e.g. a CLI token incorrectly saved in the Hub session slot).
#[cfg(feature = "sync")]
- pub async fn sync_auth_token(&self) -> Result<crate::api_client::AuthToken> {
- use crate::api_client::AuthToken;
+ pub async fn resolve_sync_auth(&self) -> SyncAuth {
+ let meta = match Self::meta_store().await {
+ Ok(m) => m,
+ Err(e) => {
+ return SyncAuth::NotLoggedIn {
+ reason: format!("Failed to open meta store: {e}"),
+ };
+ }
+ };
- let meta = Self::meta_store().await?;
+ if !self.is_hub_sync() {
+ // Self-hosted / legacy server
+ return match meta.session_token().await {
+ Ok(Some(token)) => SyncAuth::Legacy { token },
+ _ => SyncAuth::NotLoggedIn {
+ reason: "Not logged in. Run 'atuin login' to authenticate \
+ with your sync server."
+ .into(),
+ },
+ };
+ }
- // Try Hub token first if we're using Hub sync
- if self.is_hub_sync()
- && let Some(hub_token) = meta.hub_session_token().await?
- {
- return Ok(AuthToken::Bearer(hub_token));
+ // Targeting Hub — check for a valid Hub API token first
+ if let Ok(Some(hub_token)) = meta.hub_session_token().await {
+ if hub_token.starts_with("atapi_") {
+ return SyncAuth::Hub { token: hub_token };
+ }
+
+ // A non-atapi_ token in the hub_session slot is a mis-stored CLI
+ // token (from the migration-fallback bug). Move it to the CLI
+ // session slot if that slot is empty, then clear hub_session
+ // only if the move succeeded.
+ if let Ok(None) = meta.session_token().await {
+ if meta.save_session(&hub_token).await.is_ok() {
+ let _ = meta.delete_hub_session().await;
+ }
+ } else {
+ // CLI slot already has a token; just clear the bad hub_session
+ let _ = meta.delete_hub_session().await;
+ }
+ // Fall through to check CLI token below
}
- // Fall back to CLI session token
- match meta.session_token().await? {
- Some(token) => Ok(AuthToken::Token(token)),
- None => Err(eyre!(
- "Not logged in - no Hub session or CLI session found. \
- Run 'atuin login' or 'atuin register' to authenticate."
- )),
+ // No valid Hub token — check for a CLI session token
+ match meta.session_token().await {
+ Ok(Some(token)) => SyncAuth::HubViaCli { token },
+ _ => SyncAuth::NotLoggedIn {
+ reason: "Not logged in. Run 'atuin login' or 'atuin register' \
+ to authenticate."
+ .into(),
+ },
}
}
+ /// Returns the appropriate auth token for sync operations.
+ ///
+ /// Delegates to [`resolve_sync_auth`] and converts the result to an
+ /// `AuthToken`. Callers that need to distinguish between auth states
+ /// (e.g. to show different UI) should call `resolve_sync_auth` directly.
+ #[cfg(feature = "sync")]
+ pub async fn sync_auth_token(&self) -> Result<crate::api_client::AuthToken> {
+ self.resolve_sync_auth().await.into_auth_token()
+ }
+
#[cfg(feature = "check-update")]
async fn needs_update_check(&self) -> Result<bool> {
let last_check = Settings::last_version_check().await?;