use std::path::PathBuf;
use std::sync::Arc;
use async_trait::async_trait;
use crate::error::CredentialError;
use crate::handle::CredentialHandle;
use crate::store::{CredentialStore, ValidationReport};
#[async_trait]
pub trait GenericCredentialStore: Send + Sync + 'static {
fn plugin_id(&self) -> &str;
async fn list(&self) -> Vec<String>;
async fn issue(
&self,
account_id: &str,
agent_id: &str,
) -> Result<CredentialHandle, CredentialError>;
async fn resolve_bytes(&self, handle: &CredentialHandle) -> Result<Vec<u8>, CredentialError>;
async fn reload(&self) -> Result<(), CredentialError> {
Ok(())
}
fn validate(&self) -> ValidationReport {
ValidationReport::default()
}
}
pub struct TypedStoreAdapter<S: CredentialStore + 'static>
where
S::Account: serde::Serialize,
{
plugin_id: String,
inner: Arc<S>,
}
impl<S: CredentialStore + 'static> TypedStoreAdapter<S>
where
S::Account: serde::Serialize,
{
pub fn new(plugin_id: impl Into<String>, inner: Arc<S>) -> Self {
Self {
plugin_id: plugin_id.into(),
inner,
}
}
}
#[async_trait]
impl<S: CredentialStore + 'static> GenericCredentialStore for TypedStoreAdapter<S>
where
S::Account: serde::Serialize,
{
fn plugin_id(&self) -> &str {
&self.plugin_id
}
async fn list(&self) -> Vec<String> {
self.inner.list()
}
async fn issue(
&self,
account_id: &str,
agent_id: &str,
) -> Result<CredentialHandle, CredentialError> {
self.inner.issue(account_id, agent_id)
}
async fn resolve_bytes(&self, handle: &CredentialHandle) -> Result<Vec<u8>, CredentialError> {
let account = self.inner.get(handle)?;
serde_json::to_vec(&account).map_err(|e| CredentialError::InvalidSecret {
path: PathBuf::from(format!("<typed-store-adapter:{}>", self.plugin_id)),
message: format!("typed-store serialise failed: {e}"),
})
}
fn validate(&self) -> ValidationReport {
self.inner.validate()
}
}
pub fn validate_all_stores(bundle: &crate::wire::CredentialsBundle) -> ValidationReport {
let mut combined = ValidationReport::default();
for entry in bundle.stores_v2.iter() {
let report = entry.value().validate();
combined.accounts_ok += report.accounts_ok;
combined.warnings.extend(report.warnings);
combined.insecure_paths.extend(report.insecure_paths);
combined.unused.extend(report.unused);
combined.errors.extend(report.errors);
}
combined
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::{Channel, TELEGRAM};
use serde::Serialize;
struct MockGenericStore {
id: String,
accounts: Vec<String>,
resolved: Vec<u8>,
}
#[async_trait]
impl GenericCredentialStore for MockGenericStore {
fn plugin_id(&self) -> &str {
&self.id
}
async fn list(&self) -> Vec<String> {
self.accounts.clone()
}
async fn issue(
&self,
account_id: &str,
agent_id: &str,
) -> Result<CredentialHandle, CredentialError> {
Ok(CredentialHandle::new(TELEGRAM, account_id, agent_id))
}
async fn resolve_bytes(
&self,
_handle: &CredentialHandle,
) -> Result<Vec<u8>, CredentialError> {
Ok(self.resolved.clone())
}
}
#[tokio::test]
async fn mock_generic_store_lists_accounts() {
let store = MockGenericStore {
id: "mock".into(),
accounts: vec!["alpha".into(), "beta".into()],
resolved: vec![],
};
assert_eq!(
store.list().await,
vec!["alpha".to_string(), "beta".to_string()],
);
}
#[tokio::test]
async fn mock_generic_store_resolves_bytes() {
let store = MockGenericStore {
id: "mock".into(),
accounts: vec![],
resolved: b"resolved".to_vec(),
};
let handle = CredentialHandle::new(TELEGRAM, "acc", "agent");
assert_eq!(
store.resolve_bytes(&handle).await.unwrap(),
b"resolved".to_vec(),
);
}
#[derive(Clone, Serialize)]
struct FakeAcc {
id: String,
}
struct FakeTypedStore {
acc: FakeAcc,
}
impl CredentialStore for FakeTypedStore {
type Account = FakeAcc;
fn channel(&self) -> Channel {
TELEGRAM
}
fn get(&self, _handle: &CredentialHandle) -> Result<Self::Account, CredentialError> {
Ok(self.acc.clone())
}
fn issue(
&self,
account_id: &str,
agent_id: &str,
) -> Result<CredentialHandle, CredentialError> {
Ok(CredentialHandle::new(TELEGRAM, account_id, agent_id))
}
fn list(&self) -> Vec<String> {
vec![self.acc.id.clone()]
}
fn allow_agents(&self, _account_id: &str) -> Vec<String> {
Vec::new()
}
fn validate(&self) -> ValidationReport {
ValidationReport::default()
}
}
#[tokio::test]
async fn typed_adapter_serialises_account_via_serde_json() {
let typed = Arc::new(FakeTypedStore {
acc: FakeAcc { id: "acc1".into() },
});
let adapter = TypedStoreAdapter::new("fake", typed);
let handle = CredentialHandle::new(TELEGRAM, "acc1", "agent_x");
let bytes = adapter.resolve_bytes(&handle).await.unwrap();
let expected = serde_json::to_vec(&FakeAcc { id: "acc1".into() }).unwrap();
assert_eq!(bytes, expected);
assert_eq!(adapter.plugin_id(), "fake");
}
#[tokio::test]
async fn default_reload_is_ok() {
let store = MockGenericStore {
id: "mock".into(),
accounts: vec![],
resolved: vec![],
};
assert!(store.reload().await.is_ok());
}
struct ReportingStore {
report: ValidationReport,
}
#[async_trait]
impl GenericCredentialStore for ReportingStore {
fn plugin_id(&self) -> &str {
"reporting"
}
async fn list(&self) -> Vec<String> {
Vec::new()
}
async fn issue(
&self,
account_id: &str,
agent_id: &str,
) -> Result<CredentialHandle, CredentialError> {
Ok(CredentialHandle::new(TELEGRAM, account_id, agent_id))
}
async fn resolve_bytes(
&self,
_handle: &CredentialHandle,
) -> Result<Vec<u8>, CredentialError> {
Ok(Vec::new())
}
fn validate(&self) -> ValidationReport {
ValidationReport {
accounts_ok: self.report.accounts_ok,
warnings: self.report.warnings.clone(),
insecure_paths: self.report.insecure_paths.clone(),
unused: self.report.unused.clone(),
errors: Vec::new(), }
}
}
#[tokio::test]
async fn validate_all_stores_aggregates_warnings() {
use crate::resolver::{AgentCredentialResolver, CredentialStores};
use crate::wire::CredentialsBundle;
use std::sync::Arc;
let resolver = Arc::new(AgentCredentialResolver::empty());
let bundle = CredentialsBundle {
stores: CredentialStores::empty(),
resolver,
breakers: Arc::new(crate::breaker::BreakerRegistry::default()),
warnings: Vec::new(),
stores_v2: dashmap::DashMap::new(),
};
let a = Arc::new(ReportingStore {
report: ValidationReport {
accounts_ok: 2,
warnings: vec!["warn-a".into()],
insecure_paths: vec![],
unused: vec![],
errors: vec![],
},
});
let b = Arc::new(ReportingStore {
report: ValidationReport {
accounts_ok: 3,
warnings: vec!["warn-b".into()],
insecure_paths: vec![],
unused: vec![],
errors: vec![],
},
});
bundle.stores_v2.insert("a".into(), a);
bundle.stores_v2.insert("b".into(), b);
let merged = validate_all_stores(&bundle);
assert_eq!(merged.accounts_ok, 5);
assert_eq!(merged.warnings.len(), 2);
let warns: Vec<&str> = merged.warnings.iter().map(String::as_str).collect();
assert!(warns.contains(&"warn-a"));
assert!(warns.contains(&"warn-b"));
}
#[tokio::test]
async fn empty_stores_v2_validates_clean() {
use crate::resolver::{AgentCredentialResolver, CredentialStores};
use crate::wire::CredentialsBundle;
use std::sync::Arc;
let resolver = Arc::new(AgentCredentialResolver::empty());
let bundle = CredentialsBundle {
stores: CredentialStores::empty(),
resolver,
breakers: Arc::new(crate::breaker::BreakerRegistry::default()),
warnings: Vec::new(),
stores_v2: dashmap::DashMap::new(),
};
let merged = validate_all_stores(&bundle);
assert_eq!(merged.accounts_ok, 0);
assert!(merged.warnings.is_empty());
assert!(merged.errors.is_empty());
}
}