aep-agent 0.1.0

Agent-side workflows for the Agent Enrollment Protocol.
Documentation
use std::{
    collections::BTreeMap,
    sync::{Arc, Mutex},
};

use async_trait::async_trait;
use time::OffsetDateTime;
use url::Url;
use uuid::Uuid;

use crate::{
    AgentError, AgentIdentity, Clock, CredentialRecord, CredentialStore, IdempotencyKeyProvider,
    IdentityStore, InspectCache, InspectCacheEntry, OperationKey,
};

#[derive(Default)]
pub struct SystemClock;

impl Clock for SystemClock {
    fn now(&self) -> OffsetDateTime {
        OffsetDateTime::now_utc()
    }
}

#[derive(Default)]
pub struct TimerDelay;

#[async_trait]
impl crate::Delay for TimerDelay {
    async fn sleep(&self, duration: std::time::Duration) {
        futures_timer::Delay::new(duration).await;
    }
}

#[derive(Default)]
pub struct RandomIdempotencyKeyProvider;

#[async_trait]
impl IdempotencyKeyProvider for RandomIdempotencyKeyProvider {
    async fn create_key(&self, _operation: &OperationKey) -> Result<String, AgentError> {
        Ok(Uuid::new_v4().to_string())
    }
}

#[derive(Default)]
pub struct MemoryIdentityStore {
    entries: Mutex<BTreeMap<String, AgentIdentity>>,
}

#[async_trait]
impl IdentityStore for MemoryIdentityStore {
    async fn find(&self, service_did: &str) -> Result<Option<AgentIdentity>, AgentError> {
        Ok(self
            .entries
            .lock()
            .map_err(lock_error)?
            .get(service_did)
            .cloned())
    }
    async fn save(&self, identity: AgentIdentity) -> Result<(), AgentError> {
        self.entries
            .lock()
            .map_err(lock_error)?
            .insert(identity.service_did.clone(), identity);
        Ok(())
    }
}

pub struct MemoryCredentialStore {
    clock: Arc<dyn Clock>,
    entries: Mutex<BTreeMap<(String, String), CredentialRecord>>,
}

impl MemoryCredentialStore {
    pub fn new(clock: Arc<dyn Clock>) -> Self {
        Self {
            clock,
            entries: Mutex::new(BTreeMap::new()),
        }
    }
}

#[async_trait]
impl CredentialStore for MemoryCredentialStore {
    async fn delete(&self, service_did: &str, credential_id: &str) -> Result<(), AgentError> {
        self.entries
            .lock()
            .map_err(lock_error)?
            .remove(&(service_did.to_owned(), credential_id.to_owned()));
        Ok(())
    }
    async fn find(
        &self,
        service_did: &str,
        credential_id: &str,
    ) -> Result<Option<CredentialRecord>, AgentError> {
        let key = (service_did.to_owned(), credential_id.to_owned());
        let mut entries = self.entries.lock().map_err(lock_error)?;
        if entries
            .get(&key)
            .is_some_and(|record| record.expires_at <= self.clock.now())
        {
            entries.remove(&key);
        }
        Ok(entries.get(&key).cloned())
    }
    async fn list(&self, service_did: &str) -> Result<Vec<CredentialRecord>, AgentError> {
        let now = self.clock.now();
        let mut entries = self.entries.lock().map_err(lock_error)?;
        entries.retain(|_, record| record.expires_at > now);
        let mut records = entries
            .values()
            .filter(|record| record.service_did == service_did)
            .cloned()
            .collect::<Vec<_>>();
        records.sort_by(|left, right| {
            right
                .issued_at
                .cmp(&left.issued_at)
                .then_with(|| left.credential_id.cmp(&right.credential_id))
        });
        Ok(records)
    }
    async fn save(&self, credential: CredentialRecord) -> Result<(), AgentError> {
        crate::authentication::validate_record(
            &credential,
            &credential.service_did,
            self.clock.now(),
        )?;
        let key = (
            credential.service_did.clone(),
            credential.credential_id.clone(),
        );
        self.entries
            .lock()
            .map_err(lock_error)?
            .insert(key, credential);
        Ok(())
    }
}

#[derive(Default)]
pub struct MemoryInspectCache {
    entries: Mutex<BTreeMap<String, InspectCacheEntry>>,
}

#[async_trait]
impl InspectCache for MemoryInspectCache {
    async fn delete(&self, inspect_url: &Url) -> Result<(), AgentError> {
        self.entries
            .lock()
            .map_err(lock_error)?
            .remove(inspect_url.as_str());
        Ok(())
    }
    async fn find(&self, inspect_url: &Url) -> Result<Option<InspectCacheEntry>, AgentError> {
        Ok(self
            .entries
            .lock()
            .map_err(lock_error)?
            .get(inspect_url.as_str())
            .cloned())
    }
    async fn save(&self, inspect_url: &Url, entry: InspectCacheEntry) -> Result<(), AgentError> {
        self.entries
            .lock()
            .map_err(lock_error)?
            .insert(inspect_url.to_string(), entry);
        Ok(())
    }
}

fn lock_error<T>(_error: std::sync::PoisonError<T>) -> AgentError {
    AgentError::Store("AEP Agent memory store lock is poisoned".to_owned())
}