use std::sync::Arc;
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use meerkat_core::{
AuthError, AuthLease, AuthMetadata, AuthRefreshReason, BackendProfile, HttpAuthorizer,
Provider, ResolvedAuthKind,
};
use meerkat_core::provider_matrix::anthropic::{AnthropicAuthMethod, AnthropicBackendKind};
use meerkat_core::provider_matrix::google::{GoogleAuthMethod, GoogleBackendKind};
use meerkat_core::provider_matrix::openai::{OpenAiAuthMethod, OpenAiBackendKind};
use meerkat_core::provider_matrix::self_hosted::{SelfHostedAuthMethod, SelfHostedBackendKind};
pub use crate::provider_runtime::catalog::ValidatedBinding;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NormalizedBackendKind {
OpenAi(OpenAiBackendKind),
Anthropic(AnthropicBackendKind),
Google(GoogleBackendKind),
SelfHosted(SelfHostedBackendKind),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NormalizedAuthMethod {
OpenAi(OpenAiAuthMethod),
Anthropic(AnthropicAuthMethod),
Google(GoogleAuthMethod),
SelfHosted(SelfHostedAuthMethod),
}
#[derive(Clone)]
pub struct ResolvedConnection {
pub provider: Provider,
pub backend: NormalizedBackendKind,
pub backend_profile: Arc<BackendProfile>,
pub auth_lease: Arc<dyn AuthLease>,
}
impl std::fmt::Debug for ResolvedConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResolvedConnection")
.field("provider", &self.provider)
.field("backend", &self.backend)
.field("backend_profile_id", &self.backend_profile.id)
.finish()
}
}
impl ResolvedConnection {
pub fn resolved_secret(&self) -> Option<String> {
match self.auth_lease.kind() {
meerkat_core::ResolvedAuthKind::InlineSecret(secret) => Some((**secret).clone()),
_ => None,
}
}
pub fn resolved_authorizer(&self) -> Option<Arc<dyn HttpAuthorizer>> {
match self.auth_lease.kind() {
meerkat_core::ResolvedAuthKind::DynamicAuthorizer(auth) => Some(auth.clone()),
_ => None,
}
}
}
pub struct StaticLease {
kind: ResolvedAuthKind,
metadata: AuthMetadata,
expires_at: Option<DateTime<Utc>>,
source_label: String,
}
impl StaticLease {
pub fn new(
headers: Vec<(String, String)>,
metadata: AuthMetadata,
expires_at: Option<DateTime<Utc>>,
source_label: impl Into<String>,
) -> Self {
Self {
kind: ResolvedAuthKind::StaticHeaders(headers),
metadata,
expires_at,
source_label: source_label.into(),
}
}
pub fn inline_secret(
secret: String,
metadata: AuthMetadata,
expires_at: Option<DateTime<Utc>>,
source_label: impl Into<String>,
) -> Self {
Self {
kind: ResolvedAuthKind::InlineSecret(Arc::new(secret)),
metadata,
expires_at,
source_label: source_label.into(),
}
}
pub fn empty_lease(metadata: AuthMetadata, source_label: impl Into<String>) -> Self {
Self {
kind: ResolvedAuthKind::None,
metadata,
expires_at: None,
source_label: source_label.into(),
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AuthLease for StaticLease {
fn kind(&self) -> &ResolvedAuthKind {
&self.kind
}
fn metadata(&self) -> &AuthMetadata {
&self.metadata
}
fn expires_at(&self) -> Option<DateTime<Utc>> {
self.expires_at
}
fn source_label(&self) -> &str {
&self.source_label
}
async fn refresh(&self, _reason: AuthRefreshReason) -> Result<(), AuthError> {
Ok(())
}
}
pub struct DynamicLease {
authorizer: Arc<dyn HttpAuthorizer>,
metadata: AuthMetadata,
expires_at: Option<DateTime<Utc>>,
source_label: String,
kind: ResolvedAuthKind,
}
impl DynamicLease {
pub fn new(
authorizer: Arc<dyn HttpAuthorizer>,
metadata: AuthMetadata,
expires_at: Option<DateTime<Utc>>,
source_label: impl Into<String>,
) -> Self {
let kind = ResolvedAuthKind::DynamicAuthorizer(authorizer.clone());
Self {
authorizer,
metadata,
expires_at,
source_label: source_label.into(),
kind,
}
}
pub fn from_authorizer(
authorizer: Arc<dyn HttpAuthorizer>,
metadata: AuthMetadata,
source_label: impl Into<String>,
) -> Self {
Self::new(authorizer, metadata, None, source_label)
}
pub fn authorizer(&self) -> &Arc<dyn HttpAuthorizer> {
&self.authorizer
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AuthLease for DynamicLease {
fn kind(&self) -> &ResolvedAuthKind {
&self.kind
}
fn metadata(&self) -> &AuthMetadata {
&self.metadata
}
fn expires_at(&self) -> Option<DateTime<Utc>> {
self.expires_at.or_else(|| self.authorizer.expires_at())
}
fn source_label(&self) -> &str {
&self.source_label
}
async fn refresh(&self, reason: AuthRefreshReason) -> Result<(), AuthError> {
Err(AuthError::RefreshFailed(format!(
"dynamic lease '{}' cannot refresh in place for reason {reason:?}; re-resolve the typed auth_binding",
self.source_label
)))
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[tokio::test]
async fn static_lease_satisfies_trait() {
let lease: Arc<dyn AuthLease> = Arc::new(StaticLease::new(
Vec::new(),
AuthMetadata::default(),
None,
"test",
));
assert!(matches!(lease.kind(), ResolvedAuthKind::StaticHeaders(_)));
assert_eq!(lease.source_label(), "test");
assert!(lease.refresh(AuthRefreshReason::Manual).await.is_ok());
}
#[tokio::test]
async fn dynamic_lease_refresh_reports_unsupported_instead_of_success() {
#[derive(Debug)]
struct TestAuthorizer;
#[async_trait::async_trait]
impl HttpAuthorizer for TestAuthorizer {
async fn authorize(
&self,
_req: &mut meerkat_core::auth::HttpAuthorizationRequest<'_>,
) -> Result<(), AuthError> {
Ok(())
}
fn label(&self) -> &'static str {
"test-authorizer"
}
}
let lease: Arc<dyn AuthLease> = Arc::new(DynamicLease::new(
Arc::new(TestAuthorizer),
AuthMetadata::default(),
None,
"dynamic:test",
));
let err = lease
.refresh(AuthRefreshReason::Manual)
.await
.expect_err("dynamic refresh must not report success without work");
assert!(matches!(err, AuthError::RefreshFailed(_)));
}
#[tokio::test]
async fn dynamic_lease_projects_authorizer_freshness() {
#[derive(Debug)]
struct ExpiringAuthorizer {
expires_at: DateTime<Utc>,
}
#[async_trait::async_trait]
impl HttpAuthorizer for ExpiringAuthorizer {
async fn authorize(
&self,
_req: &mut meerkat_core::auth::HttpAuthorizationRequest<'_>,
) -> Result<(), AuthError> {
Ok(())
}
fn label(&self) -> &'static str {
"expiring-authorizer"
}
fn expires_at(&self) -> Option<DateTime<Utc>> {
Some(self.expires_at)
}
}
let expires_at =
chrono::TimeZone::with_ymd_and_hms(&chrono::Utc, 2026, 4, 28, 12, 0, 0).unwrap();
let lease: Arc<dyn AuthLease> = Arc::new(DynamicLease::from_authorizer(
Arc::new(ExpiringAuthorizer { expires_at }),
AuthMetadata::default(),
"dynamic:expiring",
));
assert_eq!(lease.expires_at(), Some(expires_at));
}
}