diff options
| author | Michelle Tilley <michelle@michelletilley.net> | 2026-03-11 08:50:17 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-11 08:50:17 -0700 |
| commit | c8fd7d3d8f2c93c24a4a5fc41361dfed8714e73a (patch) | |
| tree | e88cf8afa702be0925c6f606df2bb3c270679fce /crates/atuin-client/src/settings.rs | |
| parent | chore: update changelog (diff) | |
| download | atuin-c8fd7d3d8f2c93c24a4a5fc41361dfed8714e73a.zip | |
feat: Allow authenticating with Atuin Hub (#3237)
## Summary
This PR enables the Atuin CLI to authenticate with Atuin Hub, unifying
authentication across CLI sync and Hub features (AI, runbooks, etc.).
### Key Changes
- **Dual auth support**: New `AuthToken` enum supports both `Bearer`
(Hub) and `Token` (legacy CLI) authentication
- **Smart protocol selection**: New `sync_protocol` setting
(`auto`/`hub`/`legacy`) determines auth method. By default,
`api.atuin.sh` uses Hub auth; custom sync addresses use legacy auth
- **Hub login flow**: `atuin login` now initiates an OAuth-like flow for
Hub users—generates a code, user authorizes in browser, CLI polls for
completion
- **Account linking**: After Hub auth, silently attempts to link
existing CLI sync account to Hub account for seamless migration
- **Graceful fallback**: `sync_auth_token()` prefers Hub token when
available, falls back to CLI session token
### Auth Flow
1. User runs `atuin login` (with default sync address)
2. CLI requests auth code from Hub, displays URL
3. User opens URL, logs in/registers on Hub
4. Hub attaches API token to code
5. CLI polls, receives token, saves as hub session
6. If user had existing CLI sync account, it's automatically linked
### Backward Compatibility
- Existing self-hosted users: unaffected (legacy auth via `Token`
header)
- Existing `api.atuin.sh` users: continue working with CLI session until
they run `atuin login`
- New users: go through Hub flow automatically
## Test Plan
- [ ] New user registration via Hub flow
- [ ] Existing CLI user can still sync without changes
- [ ] `atuin login` links CLI account to Hub account
- [ ] Self-hosted users unaffected by changes
- [ ] AI commands work after Hub auth
---------
Co-authored-by: Ellie Huxtable <ellie@elliehuxtable.com>
Diffstat (limited to 'crates/atuin-client/src/settings.rs')
| -rw-r--r-- | crates/atuin-client/src/settings.rs | 121 |
1 files changed, 117 insertions, 4 deletions
diff --git a/crates/atuin-client/src/settings.rs b/crates/atuin-client/src/settings.rs index ddd047cd..62b3a098 100644 --- a/crates/atuin-client/src/settings.rs +++ b/crates/atuin-client/src/settings.rs @@ -342,6 +342,27 @@ pub struct Sync { pub records: bool, } +/// Sync protocol type for authentication. +/// +/// This setting is primarily for development/testing. When not explicitly set, +/// the protocol is inferred from the sync_address: +/// - Default sync address (api.atuin.sh) → Hub protocol +/// - Custom sync address → Legacy protocol +/// +/// Set explicitly to "hub" to use Hub authentication with a custom sync_address +/// (useful for local development against a Hub instance). +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum SyncProtocol { + /// Use Hub authentication (Bearer token from Hub OAuth flow) + Hub, + /// Use legacy CLI authentication (Token from CLI register/login) + Legacy, + /// Infer from sync_address (default behavior) + #[default] + Auto, +} + #[derive(Clone, Debug, Deserialize, Default, Serialize)] pub struct Keys { pub scroll_exits: bool, @@ -960,12 +981,15 @@ pub struct Settings { pub auto_sync: bool, pub update_check: bool, - /// The address of the Atuin Hub. Used for Hub-specific features like AI. - pub hub_address: String, - /// The sync address for atuin. pub sync_address: String, + /// Sync protocol for authentication. When set to "auto" (default), the protocol + /// is inferred from sync_address. Set to "hub" to force Hub auth with a custom + /// sync_address (useful for local development). + #[serde(default)] + pub sync_protocol: SyncProtocol, + pub sync_frequency: String, pub db_path: String, pub record_store_path: String, @@ -1142,6 +1166,96 @@ impl Settings { } } + pub async fn hub_session_token(&self) -> Result<String> { + match Self::meta_store().await?.hub_session_token().await? { + Some(token) => Ok(token), + None => Err(eyre!("Tried to load hub session; not logged in")), + } + } + + /// Default sync address for Atuin's hosted service + pub const DEFAULT_SYNC_ADDRESS: &'static str = "https://api.atuin.sh"; + + /// Default Hub web/API endpoint for Atuin's hosted service + pub const DEFAULT_HUB_ENDPOINT: &'static str = "https://hub.atuin.sh"; + + /// Normalize a URL for comparison by trimming trailing slashes + fn normalize_url(url: &str) -> &str { + url.trim_end_matches('/') + } + + /// Check if a URL matches one of Atuin's official hosted addresses + fn is_official_address(url: &str) -> bool { + let normalized = Self::normalize_url(url); + normalized == Self::normalize_url(Self::DEFAULT_SYNC_ADDRESS) + || normalized == Self::normalize_url(Self::DEFAULT_HUB_ENDPOINT) + } + + /// Returns whether this configuration uses Hub-style sync. + /// + /// Hub sync uses Bearer token authentication and is the default for + /// Atuin's hosted service. This returns true when: + /// - `sync_protocol` is explicitly set to `Hub`, OR + /// - `sync_protocol` is `Auto` and `sync_address` is an official Atuin address + pub fn is_hub_sync(&self) -> bool { + match self.sync_protocol { + SyncProtocol::Hub => true, + SyncProtocol::Legacy => false, + SyncProtocol::Auto => Self::is_official_address(&self.sync_address), + } + } + + /// Returns the base URL for the Hub endpoint. + /// + /// For Atuin's official hosted service, this always returns `https://hub.atuin.sh` + /// regardless of whether `sync_address` is `api.atuin.sh` or `hub.atuin.sh`. + /// For self-hosted instances, returns the configured `sync_address`. + pub fn active_hub_endpoint(&self) -> Option<String> { + if self.is_hub_sync() { + if Self::is_official_address(&self.sync_address) { + Some(Self::DEFAULT_HUB_ENDPOINT.to_string()) + } else { + Some(self.sync_address.clone()) + } + } else { + None + } + } + + /// 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. + #[cfg(feature = "sync")] + pub async fn sync_auth_token(&self) -> Result<crate::api_client::AuthToken> { + use crate::api_client::AuthToken; + + let meta = Self::meta_store().await?; + + // 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)); + } + + // 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." + )), + } + } + #[cfg(feature = "check-update")] async fn needs_update_check(&self) -> Result<bool> { let last_check = Settings::last_version_check().await?; @@ -1253,7 +1367,6 @@ impl Settings { .set_default("timezone", "local")? .set_default("auto_sync", true)? .set_default("update_check", cfg!(feature = "check-update"))? - .set_default("hub_address", "https://hub.atuin.sh")? .set_default("sync_address", "https://api.atuin.sh")? .set_default("sync_frequency", "5m")? .set_default("search_mode", "fuzzy")? |
