use std::sync::Arc;
use meerkat_core::CredentialSourceSpec;
pub mod auto;
pub mod command;
pub mod ephemeral;
pub mod file;
#[cfg(feature = "keyring")]
pub mod keyring;
pub mod refresh;
pub use auto::AutoTokenStore;
pub use command::{CommandCredentialError, CommandCredentialRunner, CommandCredentialSpec};
pub use ephemeral::EphemeralTokenStore;
pub use file::FileTokenStore;
#[cfg(feature = "keyring")]
pub use keyring::KeyringTokenStore;
#[cfg(feature = "file-lock")]
pub use refresh::FileLockCoordinator;
pub use refresh::InMemoryCoordinator;
pub use meerkat_core::auth::token_store::{
PersistedAuthMode, PersistedTokens, RefreshCoordinator, RefreshError, RefreshFn, TokenKey,
TokenStore, TokenStoreError,
};
pub fn credential_source_uses_persisted_store(source: &CredentialSourceSpec) -> bool {
matches!(
source,
CredentialSourceSpec::ManagedStore | CredentialSourceSpec::PlatformDefault
)
}
pub fn persisted_auth_mode_is_oauth_login(mode: PersistedAuthMode) -> bool {
meerkat_core::persisted_auth_mode_uses_oauth_login_lifecycle(mode)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum TokenStoreBackend {
File {
root: std::path::PathBuf,
},
#[cfg(feature = "keyring")]
Keyring {
service_name: String,
},
Auto {
root: std::path::PathBuf,
service_name: String,
},
Ephemeral,
}
impl TokenStoreBackend {
pub fn default_auto() -> Result<Self, TokenStoreError> {
Self::default_file()
}
pub fn default_keyring_auto() -> Result<Self, TokenStoreError> {
let base = dirs::config_dir()
.ok_or_else(|| TokenStoreError::Unavailable("no XDG_CONFIG_HOME".into()))?;
Ok(Self::Auto {
root: base.join("meerkat").join("credentials"),
service_name: "meerkat-auth".into(),
})
}
pub fn default_file() -> Result<Self, TokenStoreError> {
let base = dirs::config_dir()
.ok_or_else(|| TokenStoreError::Unavailable("no XDG_CONFIG_HOME".into()))?;
Ok(Self::File {
root: base.join("meerkat").join("credentials"),
})
}
pub fn open(self) -> Result<Arc<dyn TokenStore>, TokenStoreError> {
match self {
Self::File { root } => Ok(Arc::new(FileTokenStore::new(root))),
#[cfg(feature = "keyring")]
Self::Keyring { service_name } => Ok(Arc::new(KeyringTokenStore::new(service_name))),
Self::Auto { root, service_name } => {
Ok(Arc::new(AutoTokenStore::new(root, service_name)))
}
Self::Ephemeral => Ok(Arc::new(EphemeralTokenStore::new())),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn direct_secret_methods_beyond_two_literals_are_creatable() {
use meerkat_core::persisted_auth_mode_is_directly_creatable;
use meerkat_core::provider_matrix::{
AnthropicAuthMethod, GoogleAuthMethod, OpenAiAuthMethod,
};
let modes = [
GoogleAuthMethod::ApiKeyExpress.persisted_auth_mode(),
AnthropicAuthMethod::FoundryApiKey.persisted_auth_mode(),
OpenAiAuthMethod::AzureApiKey.persisted_auth_mode(),
GoogleAuthMethod::BearerApiKey.persisted_auth_mode(),
AnthropicAuthMethod::BedrockBearer.persisted_auth_mode(),
];
for mode in modes {
let mode = mode.expect("direct-secret method must resolve a persisted mode");
assert!(
persisted_auth_mode_is_directly_creatable(mode),
"({mode:?}) must be creatable at create surfaces"
);
}
}
#[test]
fn persisted_tokens_round_trip_serde() {
let tokens = PersistedTokens {
auth_mode: PersistedAuthMode::ChatgptOauth,
primary_secret: Some("access-xyz".into()),
refresh_token: Some("refresh-abc".into()),
id_token: Some("jwt".into()),
expires_at: Some(chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap()),
last_refresh: None,
scopes: vec!["openid".into(), "email".into()],
account_id: Some("acct_123".into()),
metadata: serde_json::json!({"plan_type": "pro"}),
};
let json = serde_json::to_string(&tokens).unwrap();
let round: PersistedTokens = serde_json::from_str(&json).unwrap();
assert_eq!(tokens, round);
}
#[test]
fn default_auto_uses_file_backend() {
let backend = TokenStoreBackend::default_auto().expect("default token store path");
assert!(
matches!(backend, TokenStoreBackend::File { .. }),
"default token store must not trigger OS keychain prompts"
);
}
#[test]
fn keyring_auto_stays_explicit() {
let backend = TokenStoreBackend::default_keyring_auto().expect("default token store path");
assert!(
matches!(backend, TokenStoreBackend::Auto { .. }),
"keyring fallback should remain available as an explicit backend"
);
}
#[test]
fn token_key_keyring_account() {
let k = TokenKey::parse("dev", "default_openai").expect("valid slugs");
assert_eq!(k.keyring_account(), "dev:default_openai");
}
#[test]
fn token_key_profile_override_is_part_of_identity() {
let default_key = TokenKey::parse("dev", "default_openai").expect("valid slugs");
let override_key = TokenKey::parse_with_profile("dev", "default_openai", Some("work"))
.expect("valid slugs");
assert_ne!(default_key, override_key);
assert_eq!(override_key.keyring_account(), "dev:default_openai:work");
}
}