use std::time::Duration;
use secrecy::SecretString;
use crate::api::client::ApiClient;
use crate::auth::{CredentialStore, FileCredentialStore, KeyringCredentialStore};
use crate::config::{self, Config};
use crate::error::{OlError, OL_4200_TOKEN_EXPIRED};
const DEFAULT_API_URL: &str = "https://api.openlatch.ai";
pub async fn make_client() -> Result<ApiClient, OlError> {
let token = retrieve_token().await?;
let cfg = Config::load().unwrap_or_default();
let api_url = effective_api_url(&cfg);
ApiClient::new(api_url, token)
}
pub async fn retrieve_token() -> Result<SecretString, OlError> {
let store = KeyringCredentialStore::new();
if let Ok(s) = store.retrieve_async().await {
return Ok(s);
}
if let Ok(val) = std::env::var("OPENLATCH_TOKEN") {
if !val.is_empty() {
return Ok(SecretString::from(val));
}
}
let path = config::provider_dir().join("credentials.enc");
let machine_id = config::machine_id_or_init().unwrap_or_else(|_| "unknown".into());
FileCredentialStore::new(path, machine_id)
.retrieve()
.map_err(|_| {
OlError::new(
OL_4200_TOKEN_EXPIRED,
"no editor token found in keyring, OPENLATCH_TOKEN env, or credentials file",
)
.with_suggestion("Run `openlatch-provider login` to authenticate.")
})
}
pub fn effective_api_url(cfg: &Config) -> String {
if let Ok(env) = std::env::var("OPENLATCH_API_URL") {
if !env.is_empty() {
return env;
}
}
if let Some(profile) = cfg.profiles.get("default") {
if let Some(ref url) = profile.api_url {
return url.clone();
}
}
DEFAULT_API_URL.to_string()
}
pub fn hard_confirm(expected: &str, interactive: bool, yes: bool) -> Result<(), OlError> {
if yes {
return Ok(());
}
if !interactive {
return Err(OlError::new(
OL_4200_TOKEN_EXPIRED,
"destructive operation requires --yes in non-interactive mode",
));
}
use std::io::{BufRead, Write};
let mut stdout = std::io::stdout();
write!(
stdout,
"Type `{expected}` to confirm (or anything else to cancel): "
)
.ok();
stdout.flush().ok();
let stdin = std::io::stdin();
let mut line = String::new();
stdin
.lock()
.read_line(&mut line)
.map_err(|e| OlError::new(OL_4200_TOKEN_EXPIRED, format!("read confirmation: {e}")))?;
let typed = line.trim();
if typed != expected {
return Err(OlError::new(
OL_4200_TOKEN_EXPIRED,
"confirmation text did not match — operation cancelled",
));
}
Ok(())
}
#[doc(hidden)]
pub async fn _yield_short() {
tokio::time::sleep(Duration::from_millis(1)).await;
}