use axum::http::{HeaderMap, header};
use crate::error::ApiError;
#[derive(Clone, Debug)]
pub struct ResolvedCredential {
pub credential: String,
pub tenant: Option<String>,
}
#[async_trait::async_trait]
pub trait CredentialResolver: Send + Sync + 'static {
async fn resolve(&self, headers: &HeaderMap) -> Result<ResolvedCredential, ApiError>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Outcome {
Ok,
Err,
}
impl Outcome {
#[must_use]
pub fn of<T, E>(result: &Result<T, E>) -> Self {
if result.is_ok() { Self::Ok } else { Self::Err }
}
}
#[derive(Debug)]
pub struct MeterEvent<'a> {
pub tenant: Option<&'a str>,
pub op: &'a str,
pub outcome: Outcome,
}
pub trait MeteringSink: Send + Sync + 'static {
fn record(&self, event: &MeterEvent<'_>);
}
#[derive(Debug)]
pub struct BearerResolver;
#[async_trait::async_trait]
impl CredentialResolver for BearerResolver {
async fn resolve(&self, headers: &HeaderMap) -> Result<ResolvedCredential, ApiError> {
Ok(ResolvedCredential {
credential: bearer_token(headers)?,
tenant: None,
})
}
}
#[derive(Debug)]
pub struct NoopMeter;
impl MeteringSink for NoopMeter {
fn record(&self, _event: &MeterEvent<'_>) {}
}
pub fn bearer_token(headers: &HeaderMap) -> Result<String, ApiError> {
if let Some(raw) = headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
{
let stripped = raw
.strip_prefix("Bearer ")
.or_else(|| raw.strip_prefix("bearer "));
if let Some(tok) = stripped {
let tok = tok.trim();
if !tok.is_empty() {
return Ok(tok.to_string());
}
}
}
if let Some(raw) = headers.get("x-api-key").and_then(|v| v.to_str().ok()) {
let tok = raw.trim();
if !tok.is_empty() {
return Ok(tok.to_string());
}
}
Err(ApiError::unauthorized(
"missing API key — send `Authorization: Bearer <key>` or `X-API-Key`",
))
}