use std::path::{Path, PathBuf};
use lingshu_core::config::lingshu_home;
use serde_json::{Map, Value};
use crate::error::ProxyError;
use super::auth_lock::with_auth_store_lock;
use super::auth_store::provider_state_from_doc;
pub fn default_auth_path() -> PathBuf {
lingshu_home().join("auth.json")
}
fn hermes_auth_path() -> Option<PathBuf> {
dirs::home_dir().map(|h| h.join(".hermes").join("auth.json"))
}
fn doc_has_provider(doc: &Value, provider: &str) -> bool {
if super::auth_store::provider_state_from_doc(doc, provider).is_some() {
return true;
}
!read_credential_pool_entries(doc, provider).is_empty()
}
pub fn auth_path_for_provider(provider: &str) -> PathBuf {
let lingshu = default_auth_path();
if lingshu.is_file()
&& load_auth_doc(&lingshu)
.ok()
.is_some_and(|doc| doc_has_provider(&doc, provider))
{
return lingshu;
}
if let Some(hermes) = hermes_auth_path().filter(|p| p.is_file())
&& load_auth_doc(&hermes)
.ok()
.is_some_and(|doc| doc_has_provider(&doc, provider))
{
return hermes;
}
lingshu
}
pub fn load_auth_doc(path: &Path) -> Result<Value, ProxyError> {
with_auth_store_lock(path, || {
let raw = std::fs::read_to_string(path).map_err(|e| {
ProxyError::UpstreamAuth(format!("cannot read auth file {}: {e}", path.display()))
})?;
serde_json::from_str(&raw).map_err(|e| {
ProxyError::UpstreamAuth(format!("invalid JSON in {}: {e}", path.display()))
})
})
}
pub fn read_provider_state(path: &Path, provider: &str) -> Result<Value, ProxyError> {
let doc = load_auth_doc(path)?;
provider_state_from_doc(&doc, provider).ok_or_else(|| {
ProxyError::UpstreamAuth(format!(
"provider '{provider}' not found in {}",
path.display()
))
})
}
pub fn write_auth_doc(path: &Path, doc: &Value) -> Result<(), ProxyError> {
with_auth_store_lock(path, || {
let bytes = serde_json::to_vec_pretty(doc)
.map_err(|e| ProxyError::UpstreamAuth(format!("serialize auth store: {e}")))?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| {
ProxyError::UpstreamAuth(format!("create auth dir {}: {e}", parent.display()))
})?;
}
std::fs::write(path, bytes).map_err(|e| {
ProxyError::UpstreamAuth(format!("write auth file {}: {e}", path.display()))
})
})
}
pub fn remove_provider_state(path: &Path, provider: &str) -> Result<(), ProxyError> {
with_auth_store_lock(path, || {
if !path.exists() {
return Ok(());
}
let raw = std::fs::read_to_string(path).map_err(|e| {
ProxyError::UpstreamAuth(format!("cannot read auth file {}: {e}", path.display()))
})?;
let mut doc: Value = serde_json::from_str(&raw).map_err(|e| {
ProxyError::UpstreamAuth(format!("invalid JSON in {}: {e}", path.display()))
})?;
if let Some(providers) = doc.get_mut("providers").and_then(|v| v.as_object_mut()) {
providers.remove(provider);
}
if let Some(pool) = doc
.get_mut("credential_pool")
.and_then(|v| v.as_object_mut())
{
pool.remove(provider);
}
write_auth_doc(path, &doc)
})
}
pub fn write_provider_state(path: &Path, provider: &str, state: &Value) -> Result<(), ProxyError> {
with_auth_store_lock(path, || {
let mut doc = if path.exists() {
let raw = std::fs::read_to_string(path).map_err(|e| {
ProxyError::UpstreamAuth(format!("cannot read auth file {}: {e}", path.display()))
})?;
serde_json::from_str(&raw).map_err(|e| {
ProxyError::UpstreamAuth(format!("invalid JSON in {}: {e}", path.display()))
})?
} else {
Value::Object(Map::new())
};
let providers = doc.as_object_mut().ok_or_else(|| {
ProxyError::UpstreamAuth("auth store root must be a JSON object".into())
})?;
let map = providers
.entry("providers")
.or_insert_with(|| Value::Object(Map::new()));
if let Some(obj) = map.as_object_mut() {
obj.insert(provider.to_string(), state.clone());
} else {
return Err(ProxyError::UpstreamAuth(
"auth store providers field must be an object".into(),
));
}
write_auth_doc(path, &doc)
})
}
pub fn read_credential_pool_entries(doc: &Value, provider: &str) -> Vec<Value> {
doc.get("credential_pool")
.and_then(|p| p.get(provider))
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter(|e| e.is_object()).cloned().collect())
.unwrap_or_default()
}
pub fn bearer_and_base_from_entry(entry: &Value, fallback_base: &str) -> Option<(String, String)> {
let bearer = entry
.get("access_token")
.or_else(|| entry.get("runtime_api_key"))
.or_else(|| entry.get("agent_key"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
.or_else(|| {
entry
.get("tokens")
.and_then(|t| t.get("access_token"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_string)
})?;
let base = entry
.get("base_url")
.or_else(|| entry.get("runtime_base_url"))
.and_then(|v| v.as_str())
.map(|s| s.trim_end_matches('/').to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| fallback_base.trim_end_matches('/').to_string());
Some((bearer, base))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_pool_and_tokens_access() {
let doc = serde_json::json!({
"credential_pool": {
"xai-oauth": [{"access_token": "pool-tok", "base_url": "https://api.x.ai/v1"}]
},
"providers": {
"xai-oauth": {
"tokens": { "access_token": "singleton-tok" }
}
}
});
let pool = read_credential_pool_entries(&doc, "xai-oauth");
assert_eq!(pool.len(), 1);
let (b, _) =
bearer_and_base_from_entry(&pool[0], "https://fallback/v1").expect("pool entry bearer");
assert_eq!(b, "pool-tok");
let state = provider_state_from_doc(&doc, "xai-oauth").expect("provider state");
let (b2, _) =
bearer_and_base_from_entry(&state, "https://fallback/v1").expect("state bearer");
assert_eq!(b2, "singleton-tok");
}
#[test]
fn auth_path_for_provider_prefers_lingshu_when_both_have_provider() {
let dir = tempfile::tempdir().expect("tempdir");
let ec = dir.path().join("lingshu");
let hermes_dir = dir.path().join(".hermes");
std::fs::create_dir_all(&ec).expect("mkdir ec");
std::fs::create_dir_all(&hermes_dir).expect("mkdir hermes");
let doc = serde_json::json!({
"providers": {
"xai-oauth": { "tokens": { "access_token": "tok" } }
}
});
std::fs::write(ec.join("auth.json"), doc.to_string()).expect("write ec");
std::fs::write(hermes_dir.join("auth.json"), doc.to_string()).expect("write hm");
unsafe {
std::env::set_var("EDGECRAB_HOME", ec.to_str().expect("utf8"));
std::env::set_var("HOME", dir.path().to_str().expect("utf8"));
}
assert_eq!(
auth_path_for_provider("xai-oauth"),
ec.join("auth.json")
);
}
#[test]
fn auth_path_for_provider_returns_lingshu_when_neither_has_provider() {
let dir = tempfile::tempdir().expect("tempdir");
let ec = dir.path().join("lingshu");
let hermes_dir = dir.path().join(".hermes");
std::fs::create_dir_all(&ec).expect("mkdir ec");
std::fs::create_dir_all(&hermes_dir).expect("mkdir hermes");
std::fs::write(hermes_dir.join("auth.json"), r#"{"providers":{}}"#).expect("write hm");
unsafe {
std::env::set_var("EDGECRAB_HOME", ec.to_str().expect("utf8"));
std::env::set_var("HOME", dir.path().to_str().expect("utf8"));
}
assert_eq!(
auth_path_for_provider("xai-oauth"),
ec.join("auth.json")
);
}
}