use axum::extract::State;
use axum::http::HeaderMap;
use axum::response::Json;
use brain_db_sdk::wire::types::GetCapabilitiesRequest;
use crate::app::handlers::llm_proxy::{KeyCheck, Upstream};
use crate::crypto::SecretBinding;
use crate::dto::llm_credential::{
DeleteLlmCredentialDto, LlmCredentialDto, PutLlmCredentialRequest,
};
use crate::error::ApiError;
use crate::port::{LlmCredentialStore, StoredLlmCredential, key_fingerprint};
use crate::state::EdgeState;
fn now_unix_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
async fn caller_namespace(state: &EdgeState, headers: &HeaderMap) -> Result<String, ApiError> {
let ident = state.resolve(headers).await?;
let client = state.client_for(&ident.credential).await?;
let _ = client
.capabilities(&GetCapabilitiesRequest::default())
.await;
Ok(client.connection().namespace.clone())
}
fn store_of(state: &EdgeState) -> Result<&std::sync::Arc<dyn LlmCredentialStore>, ApiError> {
state.llm_credentials().ok_or_else(|| {
ApiError::not_implemented(
"BYOK is not configured on this edge: no key-encryption key is set",
)
})
}
pub async fn put(
State(state): State<EdgeState>,
headers: HeaderMap,
Json(body): Json<PutLlmCredentialRequest>,
) -> Result<Json<LlmCredentialDto>, ApiError> {
let provider = body.validate().map_err(ApiError::bad_request)?;
let upstream = Upstream::from_name(provider)
.ok_or_else(|| ApiError::internal("validated provider is not routable"))?;
let store = store_of(&state)?;
let namespace = caller_namespace(&state, &headers).await?;
match crate::app::handlers::llm_proxy::validate_key(upstream, &body.api_key).await {
KeyCheck::Valid => {}
KeyCheck::Rejected(why) => return Err(ApiError::bad_request(why)),
KeyCheck::Unreachable(why) => tracing::warn!(
namespace = %namespace,
provider,
reason = %why,
"storing provider key without verification",
),
}
let binding = SecretBinding {
namespace: &namespace,
provider,
};
let sealed = state.seal_llm_key(&body.api_key, binding)?;
let record = StoredLlmCredential {
provider: provider.to_string(),
model: body.model.trim().to_string(),
sealed,
fingerprint: key_fingerprint(&body.api_key),
created_at_unix_secs: now_unix_secs(),
};
store.put(&namespace, record.clone()).await?;
tracing::info!(
namespace = %namespace,
provider,
model = %record.model,
fingerprint = %record.fingerprint,
"BYOK provider key registered",
);
Ok(Json(LlmCredentialDto::from(record)))
}
pub async fn get(
State(state): State<EdgeState>,
headers: HeaderMap,
) -> Result<Json<LlmCredentialDto>, ApiError> {
let store = store_of(&state)?;
let namespace = caller_namespace(&state, &headers).await?;
let found = store.get(&namespace).await?;
let record = found.ok_or_else(|| {
ApiError::not_found("no LLM provider key is registered for this namespace")
})?;
Ok(Json(LlmCredentialDto::from(record)))
}
pub async fn delete(
State(state): State<EdgeState>,
headers: HeaderMap,
) -> Result<Json<DeleteLlmCredentialDto>, ApiError> {
let store = store_of(&state)?;
let namespace = caller_namespace(&state, &headers).await?;
let deleted = store.delete(&namespace).await?;
if deleted {
tracing::info!(namespace = %namespace, "BYOK provider key removed");
}
Ok(Json(DeleteLlmCredentialDto { deleted }))
}