use anyhow::Result;
use cleanlib_client::config;
use cleanlib_client::Client;
use cleanlib_client::errors::{CleanLibraryError, TransportError};
use crate::auth;
fn validate_api_key(api_key: &str) -> Result<()> {
if api_key.is_empty() {
anyhow::bail!(
"CLIENT_BEARER_EMPTY — API key must not be empty.\n\
Pass a non-empty value via `cleanlib login --api-key <KEY>` or\n\
set CLEANLIB_ENRICH_BEARER in your environment."
);
}
if api_key.trim().is_empty() {
anyhow::bail!(
"CLIENT_BEARER_WHITESPACE — API key must not be whitespace-only.\n\
Pass the literal API key value to `cleanlib login --api-key <KEY>`."
);
}
Ok(())
}
pub async fn run(api_key: String, use_keyring: bool) -> Result<()> {
validate_api_key(&api_key)?;
{
let mut probe_cfg = config::load_with_env_overrides(config::default_path().as_deref()).unwrap_or_default();
probe_cfg.auth.api_key = Some(api_key.clone());
let client = Client::from_config(&probe_cfg)?;
match client.probe_auth().await {
Ok(()) => {}
Err(CleanLibraryError::Authentication { ref reason_code, .. })
if reason_code == "EXPIRED_OR_INVALID_API_KEY" =>
{
anyhow::bail!(
"KEY_EXPIRED — Your API key has expired.\nGenerate a new key and run: cleanlib login --api-key <NEW_KEY>"
);
}
Err(CleanLibraryError::Authentication { ref reason_code, .. })
if reason_code == "INVALID_API_KEY" =>
{
anyhow::bail!(
"INVALID_API_KEY — Key not recognised by server.\nCheck your key and run: cleanlib login --api-key <KEY>"
);
}
Err(CleanLibraryError::Authentication { ref reason_code, .. })
if reason_code == "MISSING_OR_INVALID_AUTH_HEADER" =>
{
anyhow::bail!(
"BAD_KEY_FORMAT — Key format is invalid.\nRe-copy your key exactly and run: cleanlib login --api-key <KEY>"
);
}
Err(CleanLibraryError::Authentication { ref reason_code, .. }) => {
anyhow::bail!(
"AUTH_FAILED [{}] — Authentication rejected.\nIf this persists contact support with the reason code above.",
reason_code
);
}
Err(CleanLibraryError::ServerError { status: 503, .. }) => {
anyhow::bail!(
"CDP_UNAVAILABLE — Authentication service temporarily unavailable.\nYour key may be valid. Wait and try again: cleanlib login --api-key <KEY>"
);
}
Err(CleanLibraryError::ServerError { status, .. }) => {
anyhow::bail!(
"SERVER_ERROR [{}] — Unexpected server error.\nIf this persists contact support.",
status
);
}
Err(CleanLibraryError::Transport(TransportError::Timeout)) => {
anyhow::bail!(
"TIMEOUT — Server did not respond.\nCheck your network and try again."
);
}
Err(CleanLibraryError::Transport(TransportError::Network(e))) => {
anyhow::bail!(
"NETWORK_ERROR — Cannot reach server: {}\nCheck your internet connection.",
e
);
}
Err(other) => {
anyhow::bail!(
"LOGIN_ERROR — Unexpected error: {}",
other
);
}
}
}
if use_keyring {
auth::bearer::store_in_keyring(&api_key)?;
eprintln!(
"api key stored in keyring (service={}, user={})",
auth::KEYRING_SERVICE,
auth::KEYRING_USER
);
return Ok(());
}
let path = config::default_path()
.ok_or_else(|| anyhow::anyhow!("home directory not discoverable; cannot write config"))?;
let mut cfg = if path.exists() {
config::load(&path)?
} else {
config::Config::default()
};
cfg.auth.api_key = Some(api_key);
config::save(&cfg, &path)?;
eprintln!("api key stored in {}", path.display());
Ok(())
}
#[cfg(test)]
mod tests {
use super::validate_api_key;
#[test]
fn rejects_empty_api_key() {
let err = validate_api_key("").unwrap_err();
assert!(err.to_string().contains("CLIENT_BEARER_EMPTY"));
}
#[test]
fn rejects_whitespace_only_api_key() {
let err = validate_api_key(" ").unwrap_err();
assert!(err.to_string().contains("CLIENT_BEARER_WHITESPACE"));
}
#[test]
fn rejects_tab_and_newline_only() {
assert!(validate_api_key("\t").is_err());
assert!(validate_api_key("\n").is_err());
assert!(validate_api_key(" \t\n ").is_err());
}
#[test]
fn accepts_valid_tier_keyed_bearer_shape() {
assert!(validate_api_key("std_001").is_ok());
assert!(validate_api_key("clk_xyz_long_token_value_12345").is_ok());
}
#[tokio::test]
async fn probe_auth_accepts_valid_key() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use cleanlib_client::transport;
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/audit"))
.respond_with(ResponseTemplate::new(200)
.set_body_json(serde_json::json!({
"records": [],
"backend_status": "not_wired"
})))
.mount(&server)
.await;
let client = transport::Client::new(
&server.uri(),
Some("clk_std_test_001".into()),
).unwrap();
assert!(client.probe_auth().await.is_ok());
}
#[tokio::test]
async fn probe_auth_rejects_invalid_key() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use cleanlib_client::transport;
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/audit"))
.respond_with(ResponseTemplate::new(401)
.insert_header(
"x-cleanlibrary-reason",
"INVALID_API_KEY"
))
.mount(&server)
.await;
let client = transport::Client::new(
&server.uri(),
Some("fake_invalid_key".into()),
).unwrap();
let result = client.probe_auth().await;
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("authentication failed") || err.contains("INVALID_API_KEY"));
}
#[tokio::test]
async fn probe_auth_rejects_expired_key() {
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use cleanlib_client::transport;
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v1/audit"))
.respond_with(ResponseTemplate::new(401)
.insert_header(
"x-cleanlibrary-reason",
"EXPIRED_OR_INVALID_API_KEY"
))
.mount(&server)
.await;
let client = transport::Client::new(
&server.uri(),
Some("clk_expired_key".into()),
).unwrap();
let result = client.probe_auth().await;
assert!(result.is_err());
}
}