use crate::core::KNOWN_PROVIDERS;
use crate::secrets::result::SecretsLoadResult;
use secrecy::SecretString;
use spn_client::{ExposeSecret, SpnClient};
use std::sync::OnceLock;
use tokio::sync::Mutex;
use tracing::{debug, info, trace, warn};
static CLIENT: OnceLock<Mutex<Option<SpnClient>>> = OnceLock::new();
async fn get_or_init_client() -> Option<&'static Mutex<Option<SpnClient>>> {
if CLIENT.get().is_none() {
match SpnClient::connect_with_fallback().await {
Ok(client) => {
let is_fallback = client.is_fallback_mode();
let _ = CLIENT.set(Mutex::new(Some(client)));
if is_fallback {
warn!("nika daemon not running, using env var fallback");
} else {
debug!("Connected to nika daemon");
}
}
Err(e) => {
warn!("Failed to connect to nika daemon: {}", e);
let _ = CLIENT.set(Mutex::new(None));
}
}
}
CLIENT.get()
}
pub fn daemon_available() -> bool {
spn_client::daemon_socket_exists()
}
pub async fn load_from_daemon_or_fallback() -> SecretsLoadResult {
let mut result = SecretsLoadResult::default();
let client_lock = match get_or_init_client().await {
Some(lock) => lock,
None => {
return load_fallback_only().await;
}
};
let mut guard = client_lock.lock().await;
let client = match guard.as_mut() {
Some(c) => c,
None => {
drop(guard);
return load_fallback_only().await;
}
};
result.daemon_available = !client.is_fallback_mode();
for p in KNOWN_PROVIDERS {
let provider_id = p.id;
let env_var = p.env_var;
if std::env::var(env_var)
.map(|v| !v.is_empty())
.unwrap_or(false)
{
trace!("{}: already in env", provider_id);
result.from_fallback.push(provider_id.to_string());
continue;
}
match client.get_secret(provider_id).await {
Ok(secret) if !secret.expose_secret().is_empty() => {
std::env::set_var(env_var, secret.expose_secret());
if client.is_fallback_mode() {
debug!("{}: loaded from env fallback → {}", provider_id, env_var);
result.from_fallback.push(provider_id.to_string());
} else {
debug!("{}: loaded from daemon → {}", provider_id, env_var);
result.from_daemon.push(provider_id.to_string());
}
}
_ => {
trace!("{}: not found in daemon", provider_id);
result.not_found.push(provider_id.to_string());
}
}
}
info!("Secrets: {}", result.summary());
result
}
pub async fn get_secret(provider: &str) -> Option<SecretString> {
let env_var = provider_env_var(provider);
if let Ok(value) = std::env::var(env_var) {
if !value.is_empty() {
return Some(SecretString::from(value));
}
}
if let Some(client_lock) = get_or_init_client().await {
let mut guard = client_lock.lock().await;
if let Some(client) = guard.as_mut() {
if let Ok(secret) = client.get_secret(provider).await {
if !secret.expose_secret().is_empty() {
return Some(secret);
}
}
}
}
None
}
pub async fn has_secret(provider: &str) -> bool {
let env_var = provider_env_var(provider);
if std::env::var(env_var)
.map(|v| !v.is_empty())
.unwrap_or(false)
{
return true;
}
if let Some(client_lock) = get_or_init_client().await {
let mut guard = client_lock.lock().await;
if let Some(client) = guard.as_mut() {
if let Ok(exists) = client.has_secret(provider).await {
return exists;
}
}
}
false
}
async fn load_fallback_only() -> SecretsLoadResult {
let mut result = SecretsLoadResult {
daemon_available: false,
..Default::default()
};
for p in KNOWN_PROVIDERS {
let provider_id = p.id;
let env_var = p.env_var;
if std::env::var(env_var)
.map(|v| !v.is_empty())
.unwrap_or(false)
{
trace!("{}: already in env", provider_id);
result.from_fallback.push(provider_id.to_string());
} else {
result.not_found.push(provider_id.to_string());
}
}
info!("Secrets (fallback only): {}", result.summary());
result
}
fn provider_env_var(provider: &str) -> &'static str {
crate::core::provider_to_env_var(provider).unwrap_or("UNKNOWN_API_KEY")
}