Skip to main content

aep_platform/
types.rs

1use std::{collections::BTreeMap, fmt, sync::Arc, time::Duration};
2
3use aep_core::{
4    AssertionOperation, ClientAssertionClaims, ClientAssertionVerifyingKey, ErrorCode,
5    ProblemDetails, SigningAlgorithm,
6};
7use async_trait::async_trait;
8use futures::future::BoxFuture;
9use http::HeaderMap;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use time::OffsetDateTime;
13
14use crate::PlatformError;
15
16pub const DID_MEDIA_TYPE: &str = "application/did+json";
17pub const HOSTED_IDENTITY_DRAFT: &str = "draft-kavian-aep-platform-hosted-identity-01";
18pub const WELL_KNOWN_PATH: &str = "/.well-known/aep-platform";
19
20#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
21#[serde(rename_all = "lowercase")]
22pub enum ManagedAgentStatus {
23    Active,
24    Revoked,
25    Suspended,
26    Terminated,
27}
28
29impl ManagedAgentStatus {
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::Active => "active",
33            Self::Revoked => "revoked",
34            Self::Suspended => "suspended",
35            Self::Terminated => "terminated",
36        }
37    }
38}
39
40#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
41pub struct DiscoveryDocument {
42    pub aep_version: String,
43    pub endpoints: DiscoveryEndpoints,
44    pub http: DiscoveryHttp,
45    pub identity: DiscoveryIdentity,
46    pub platform: DiscoveryPlatform,
47    pub signing: DiscoverySigning,
48    #[serde(flatten)]
49    pub additional: BTreeMap<String, Value>,
50}
51
52#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
53pub struct DiscoveryEndpoints {
54    #[serde(
55        default,
56        deserialize_with = "deserialize_optional_non_null",
57        skip_serializing_if = "Option::is_none"
58    )]
59    pub hosted_verification: Option<String>,
60    pub lifecycle: String,
61    pub list: String,
62    pub provision: String,
63    pub sign: String,
64    #[serde(flatten)]
65    pub additional: BTreeMap<String, Value>,
66}
67
68#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
69pub struct DiscoveryHttp {
70    pub endpoint_base: String,
71    #[serde(flatten)]
72    pub additional: BTreeMap<String, Value>,
73}
74
75#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
76pub struct DiscoveryIdentity {
77    pub did_methods: Vec<String>,
78    pub did_url_template: String,
79    #[serde(flatten)]
80    pub additional: BTreeMap<String, Value>,
81}
82
83#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
84pub struct DiscoveryPlatform {
85    #[serde(
86        default,
87        deserialize_with = "deserialize_optional_non_null",
88        skip_serializing_if = "Option::is_none"
89    )]
90    pub did: Option<String>,
91    pub hosted_verification: bool,
92    pub name: String,
93    #[serde(flatten)]
94    pub additional: BTreeMap<String, Value>,
95}
96
97#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
98pub struct DiscoverySigning {
99    pub algorithms: Vec<SigningAlgorithm>,
100    pub default_lifetime_seconds: String,
101    #[serde(flatten)]
102    pub additional: BTreeMap<String, Value>,
103}
104
105#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
106pub struct AgentIdentity {
107    pub agent_did: String,
108    pub agent_identity_id: String,
109    pub created_at: String,
110    pub did_document_url: String,
111    pub key_id: String,
112    pub service_did: String,
113    pub signing_algorithms: Vec<SigningAlgorithm>,
114    pub status: ManagedAgentStatus,
115    pub updated_at: String,
116    #[serde(flatten)]
117    pub additional: BTreeMap<String, Value>,
118}
119
120#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
121pub struct AgentIdentityListResponse {
122    pub count: String,
123    pub data: Vec<AgentIdentity>,
124    pub total: String,
125    #[serde(flatten)]
126    pub additional: BTreeMap<String, Value>,
127}
128
129#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
130#[serde(deny_unknown_fields)]
131pub struct ProvisionRequest {
132    pub service_did: String,
133}
134
135#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
136#[serde(deny_unknown_fields)]
137pub struct LifecycleRequest {
138    pub status: ManagedAgentStatus,
139}
140
141#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
142#[serde(rename_all = "lowercase")]
143pub enum SignStatus {
144    Completed,
145    Pending,
146}
147
148#[derive(Clone, Deserialize, PartialEq, Serialize)]
149#[serde(deny_unknown_fields)]
150pub struct SignRequest {
151    pub jti: String,
152    #[serde(
153        default,
154        deserialize_with = "deserialize_optional_non_null",
155        skip_serializing_if = "Option::is_none"
156    )]
157    pub lifetime_seconds: Option<String>,
158    pub op: AssertionOperation,
159    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
160    pub platform_context: BTreeMap<String, Value>,
161    #[serde(
162        default,
163        deserialize_with = "deserialize_optional_non_null",
164        skip_serializing_if = "Option::is_none"
165    )]
166    pub resource: Option<String>,
167    pub service_did: String,
168}
169
170impl fmt::Debug for SignRequest {
171    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172        formatter
173            .debug_struct("SignRequest")
174            .field("jti", &self.jti)
175            .field("lifetime_seconds", &self.lifetime_seconds)
176            .field("op", &self.op)
177            .field("platform_context", &"[REDACTED]")
178            .field("resource", &self.resource.as_ref().map(|_| "[REDACTED]"))
179            .field("service_did", &self.service_did)
180            .finish()
181    }
182}
183
184#[derive(Clone, Deserialize, PartialEq, Serialize)]
185pub struct SignResponse {
186    #[serde(
187        default,
188        deserialize_with = "deserialize_optional_non_null",
189        skip_serializing_if = "Option::is_none"
190    )]
191    pub agent_did: Option<String>,
192    #[serde(
193        default,
194        deserialize_with = "deserialize_optional_non_null",
195        skip_serializing_if = "Option::is_none"
196    )]
197    pub client_assertion: Option<String>,
198    #[serde(
199        default,
200        deserialize_with = "deserialize_optional_non_null",
201        skip_serializing_if = "Option::is_none"
202    )]
203    pub expires_at: Option<String>,
204    #[serde(
205        default,
206        deserialize_with = "deserialize_optional_non_null",
207        skip_serializing_if = "Option::is_none"
208    )]
209    pub issued_at: Option<String>,
210    #[serde(
211        default,
212        deserialize_with = "deserialize_optional_non_null",
213        skip_serializing_if = "Option::is_none"
214    )]
215    pub jti: Option<String>,
216    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
217    pub platform_context: BTreeMap<String, Value>,
218    #[serde(
219        default,
220        deserialize_with = "deserialize_optional_non_null",
221        skip_serializing_if = "Option::is_none"
222    )]
223    pub retry_after_seconds: Option<String>,
224    #[serde(
225        default,
226        deserialize_with = "deserialize_optional_non_null",
227        skip_serializing_if = "Option::is_none"
228    )]
229    pub service_did: Option<String>,
230    pub status: SignStatus,
231    #[serde(flatten)]
232    pub additional: BTreeMap<String, Value>,
233}
234
235impl fmt::Debug for SignResponse {
236    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237        formatter
238            .debug_struct("SignResponse")
239            .field("agent_did", &self.agent_did)
240            .field(
241                "client_assertion",
242                &self.client_assertion.as_ref().map(|_| "[REDACTED]"),
243            )
244            .field("expires_at", &self.expires_at)
245            .field("issued_at", &self.issued_at)
246            .field("jti", &self.jti)
247            .field("platform_context", &"[REDACTED]")
248            .field("retry_after_seconds", &self.retry_after_seconds)
249            .field("service_did", &self.service_did)
250            .field("status", &self.status)
251            .field("additional", &"[REDACTED]")
252            .finish()
253    }
254}
255
256#[derive(Clone, Deserialize, PartialEq, Serialize)]
257#[serde(deny_unknown_fields)]
258pub struct VerificationRequest {
259    pub client_assertion: String,
260    pub op: AssertionOperation,
261    #[serde(
262        default,
263        deserialize_with = "deserialize_optional_non_null",
264        skip_serializing_if = "Option::is_none"
265    )]
266    pub resource: Option<String>,
267    pub service_did: String,
268}
269
270impl fmt::Debug for VerificationRequest {
271    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
272        formatter
273            .debug_struct("VerificationRequest")
274            .field("client_assertion", &"[REDACTED]")
275            .field("op", &self.op)
276            .field("resource", &self.resource.as_ref().map(|_| "[REDACTED]"))
277            .field("service_did", &self.service_did)
278            .finish()
279    }
280}
281
282#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
283pub struct VerificationResponse {
284    #[serde(
285        default,
286        deserialize_with = "deserialize_optional_non_null",
287        skip_serializing_if = "Option::is_none"
288    )]
289    pub agent_did: Option<String>,
290    #[serde(
291        default,
292        deserialize_with = "deserialize_optional_non_null",
293        skip_serializing_if = "Option::is_none"
294    )]
295    pub agent_identity_id: Option<String>,
296    #[serde(
297        default,
298        deserialize_with = "deserialize_optional_non_null",
299        skip_serializing_if = "Option::is_none"
300    )]
301    pub op: Option<AssertionOperation>,
302    pub reason: String,
303    pub service_did: String,
304    #[serde(
305        default,
306        deserialize_with = "deserialize_optional_non_null",
307        skip_serializing_if = "Option::is_none"
308    )]
309    pub status: Option<ManagedAgentStatus>,
310    pub verified: bool,
311    #[serde(flatten)]
312    pub additional: BTreeMap<String, Value>,
313}
314
315#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
316#[serde(deny_unknown_fields)]
317pub struct DidVerificationMethod {
318    pub controller: String,
319    pub id: String,
320    #[serde(rename = "publicKeyJwk")]
321    pub public_key_jwk: Value,
322    #[serde(rename = "type")]
323    pub method_type: String,
324}
325
326#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
327#[serde(deny_unknown_fields)]
328pub struct DidDocument {
329    #[serde(rename = "@context")]
330    pub context: Vec<String>,
331    #[serde(rename = "assertionMethod")]
332    pub assertion_method: Vec<String>,
333    pub authentication: Vec<String>,
334    #[serde(rename = "capabilityInvocation")]
335    pub capability_invocation: Vec<String>,
336    pub id: String,
337    #[serde(rename = "verificationMethod")]
338    pub verification_method: Vec<DidVerificationMethod>,
339}
340
341#[derive(Clone, Debug, PartialEq)]
342pub enum ResponseBody<T> {
343    Success(T),
344    Problem(Box<ProblemDetails>),
345}
346
347#[derive(Clone, PartialEq)]
348pub struct PlatformResponse<T> {
349    pub body: ResponseBody<T>,
350    pub headers: HeaderMap,
351    pub status: u16,
352}
353
354impl<T: fmt::Debug> fmt::Debug for PlatformResponse<T> {
355    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
356        formatter
357            .debug_struct("PlatformResponse")
358            .field("body", &self.body)
359            .field("headers", &"[REDACTED]")
360            .field("status", &self.status)
361            .finish()
362    }
363}
364
365impl<T: Serialize> PlatformResponse<T> {
366    pub fn to_json(&self) -> Result<Value, serde_json::Error> {
367        match &self.body {
368            ResponseBody::Success(value) => serde_json::to_value(value),
369            ResponseBody::Problem(value) => serde_json::to_value(value),
370        }
371    }
372}
373
374#[derive(Clone, Default, Eq, PartialEq)]
375pub struct RequestContext {
376    pub authorization: Option<String>,
377    pub idempotency_key: Option<String>,
378    pub now: Option<OffsetDateTime>,
379    pub principal: String,
380    pub request_id: Option<String>,
381}
382
383impl fmt::Debug for RequestContext {
384    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
385        formatter
386            .debug_struct("RequestContext")
387            .field(
388                "authorization",
389                &self.authorization.as_ref().map(|_| "[REDACTED]"),
390            )
391            .field("idempotency_key", &self.idempotency_key)
392            .field("now", &self.now)
393            .field("principal", &self.principal)
394            .field("request_id", &self.request_id)
395            .finish()
396    }
397}
398
399#[derive(Clone, Debug, PartialEq)]
400pub struct IdentityRecord {
401    pub agent_did: String,
402    pub agent_did_id: String,
403    pub agent_identity_id: String,
404    pub created_at: OffsetDateTime,
405    pub did_document_url: String,
406    pub key_id: String,
407    pub principal: String,
408    pub service_did: String,
409    pub signing_algorithms: Vec<SigningAlgorithm>,
410    pub status: ManagedAgentStatus,
411    pub updated_at: OffsetDateTime,
412}
413
414#[derive(Clone, Debug, Default, Eq, PartialEq)]
415pub struct IdentityListQuery {
416    pub descending: bool,
417    pub limit: usize,
418    pub offset: usize,
419    pub service_did: Option<String>,
420    pub status: Option<ManagedAgentStatus>,
421}
422
423#[derive(Clone, Debug, PartialEq)]
424pub struct IdentityListResult {
425    pub identities: Vec<IdentityRecord>,
426    pub total: usize,
427}
428
429pub type IdentityCreation =
430    Box<dyn FnOnce() -> BoxFuture<'static, Result<IdentityRecord, PlatformError>> + Send>;
431
432#[async_trait]
433pub trait IdentityStore: Send + Sync {
434    async fn find_or_create(
435        &self,
436        principal: &str,
437        service_did: &str,
438        create: IdentityCreation,
439    ) -> Result<IdentityRecord, PlatformError>;
440    async fn find_by_agent_did(
441        &self,
442        agent_did: &str,
443    ) -> Result<Option<IdentityRecord>, PlatformError>;
444    async fn find_by_agent_did_id(
445        &self,
446        agent_did_id: &str,
447    ) -> Result<Option<IdentityRecord>, PlatformError>;
448    async fn get(&self, agent_identity_id: &str) -> Result<Option<IdentityRecord>, PlatformError>;
449    async fn list(
450        &self,
451        principal: &str,
452        query: &IdentityListQuery,
453    ) -> Result<IdentityListResult, PlatformError>;
454    async fn update_status(
455        &self,
456        agent_identity_id: &str,
457        status: ManagedAgentStatus,
458        updated_at: OffsetDateTime,
459    ) -> Result<Option<IdentityRecord>, PlatformError>;
460}
461
462#[async_trait]
463pub trait KeyStore: Send + Sync {
464    async fn create_key(&self, identity: &IdentityRecord) -> Result<(), PlatformError>;
465    async fn did_verification_method(
466        &self,
467        identity: &IdentityRecord,
468    ) -> Result<DidVerificationMethod, PlatformError>;
469    async fn sign(
470        &self,
471        identity: &IdentityRecord,
472        claims: &ClientAssertionClaims,
473    ) -> Result<String, PlatformError>;
474    async fn verification_key(
475        &self,
476        identity: &IdentityRecord,
477    ) -> Result<ClientAssertionVerifyingKey, PlatformError>;
478}
479
480#[async_trait]
481pub trait ServiceDidResolver: Send + Sync {
482    async fn resolve(&self, service_did: &str) -> Result<bool, PlatformError>;
483}
484
485#[derive(Clone, Debug, PartialEq)]
486pub enum AuthorizationRequest {
487    GetIdentity {
488        identity: IdentityRecord,
489    },
490    ListIdentities {
491        query: IdentityListQuery,
492    },
493    Provision {
494        request: ProvisionRequest,
495    },
496    Sign {
497        identity: IdentityRecord,
498        request: SignRequest,
499    },
500    UpdateIdentity {
501        identity: IdentityRecord,
502        request: LifecycleRequest,
503    },
504    Verify {
505        identity: IdentityRecord,
506        request: VerificationRequest,
507    },
508}
509
510#[async_trait]
511pub trait Authorizer: Send + Sync {
512    async fn authorize(
513        &self,
514        request: &AuthorizationRequest,
515        context: &RequestContext,
516    ) -> Result<bool, PlatformError>;
517}
518
519#[async_trait]
520pub trait LifecyclePolicy: Send + Sync {
521    async fn can_sign(
522        &self,
523        identity: &IdentityRecord,
524        context: &RequestContext,
525    ) -> Result<bool, PlatformError>;
526    async fn can_transition(
527        &self,
528        identity: &IdentityRecord,
529        status: ManagedAgentStatus,
530        context: &RequestContext,
531    ) -> Result<bool, PlatformError>;
532    async fn can_verify(
533        &self,
534        identity: &IdentityRecord,
535        context: &RequestContext,
536    ) -> Result<bool, PlatformError>;
537}
538
539#[async_trait]
540pub trait ReplayStore: Send + Sync {
541    async fn consume(
542        &self,
543        key: &str,
544        expires_at: OffsetDateTime,
545        now: OffsetDateTime,
546    ) -> Result<bool, PlatformError>;
547}
548
549#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
550pub enum IdempotentOperation {
551    HostedVerification,
552    Provision,
553    Sign,
554}
555
556#[derive(Clone, Debug, Eq, PartialEq)]
557pub struct IdempotencyInput {
558    pub idempotency_key: String,
559    pub operation: IdempotentOperation,
560    pub principal: String,
561    pub request_hash: String,
562}
563
564#[derive(Clone, PartialEq)]
565pub struct StoredResponse {
566    pub body: Vec<u8>,
567    pub content_type: String,
568    pub created_at: OffsetDateTime,
569    pub headers: HeaderMap,
570    pub status: u16,
571}
572
573impl fmt::Debug for StoredResponse {
574    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
575        formatter
576            .debug_struct("StoredResponse")
577            .field("body", &"[REDACTED]")
578            .field("content_type", &self.content_type)
579            .field("created_at", &self.created_at)
580            .field("headers", &"[REDACTED]")
581            .field("status", &self.status)
582            .finish()
583    }
584}
585
586#[derive(Clone, Debug, PartialEq)]
587pub enum IdempotencyResult {
588    Created(StoredResponse),
589    Replayed(StoredResponse),
590    Conflict,
591}
592
593pub type IdempotencyOperation =
594    Box<dyn FnOnce() -> BoxFuture<'static, Result<StoredResponse, PlatformError>> + Send>;
595
596#[async_trait]
597pub trait IdempotencyStore: Send + Sync {
598    async fn execute(
599        &self,
600        input: IdempotencyInput,
601        operation: IdempotencyOperation,
602    ) -> Result<IdempotencyResult, PlatformError>;
603}
604
605#[async_trait]
606pub trait SignHandler: Send + Sync {
607    async fn sign(
608        &self,
609        identity: &IdentityRecord,
610        request: &SignRequest,
611        context: &RequestContext,
612    ) -> Result<Option<PlatformResponse<SignResponse>>, PlatformError>;
613}
614
615pub trait Clock: Send + Sync {
616    fn now(&self) -> OffsetDateTime;
617}
618
619#[derive(Clone, Debug, Default, Eq, PartialEq)]
620pub struct DiscoveryOptions {
621    pub endpoint_base: String,
622    pub hosted_verification_endpoint: Option<String>,
623    pub lifecycle_endpoint: String,
624    pub list_endpoint: String,
625    pub platform_did: Option<String>,
626    pub platform_name: String,
627    pub provision_endpoint: String,
628    pub sign_endpoint: String,
629}
630
631#[derive(Clone)]
632pub struct PlatformOptions {
633    pub agent_did_id_generator:
634        Option<Arc<dyn Fn() -> Result<String, PlatformError> + Send + Sync>>,
635    pub authorizer: Arc<dyn Authorizer>,
636    pub clock: Option<Arc<dyn Clock>>,
637    pub default_lifetime: Duration,
638    pub did_host: String,
639    pub did_path_prefix: String,
640    pub did_url_template: String,
641    pub discovery: DiscoveryOptions,
642    pub hosted_verification: bool,
643    pub identifier: Option<Arc<dyn Fn() -> Result<String, PlatformError> + Send + Sync>>,
644    pub idempotency_store: Option<Arc<dyn IdempotencyStore>>,
645    pub identity_store: Option<Arc<dyn IdentityStore>>,
646    pub key_store: Arc<dyn KeyStore>,
647    pub lifecycle_policy: Option<Arc<dyn LifecyclePolicy>>,
648    pub maximum_lifetime: Duration,
649    pub replay_store: Option<Arc<dyn ReplayStore>>,
650    pub service_did_resolver: Arc<dyn ServiceDidResolver>,
651    pub sign_handler: Option<Arc<dyn SignHandler>>,
652    pub signing_algorithms: Vec<SigningAlgorithm>,
653}
654
655impl PlatformOptions {
656    pub fn new(
657        authorizer: Arc<dyn Authorizer>,
658        key_store: Arc<dyn KeyStore>,
659        service_did_resolver: Arc<dyn ServiceDidResolver>,
660        did_host: impl Into<String>,
661        did_url_template: impl Into<String>,
662    ) -> Self {
663        Self {
664            agent_did_id_generator: None,
665            authorizer,
666            clock: None,
667            default_lifetime: aep_core::MAX_ASSERTION_LIFETIME,
668            did_host: did_host.into(),
669            did_path_prefix: "agents".to_owned(),
670            did_url_template: did_url_template.into(),
671            discovery: DiscoveryOptions::default(),
672            hosted_verification: false,
673            identifier: None,
674            idempotency_store: None,
675            identity_store: None,
676            key_store,
677            lifecycle_policy: None,
678            maximum_lifetime: aep_core::MAX_ASSERTION_LIFETIME,
679            replay_store: None,
680            service_did_resolver,
681            sign_handler: None,
682            signing_algorithms: vec![SigningAlgorithm::EdDsa, SigningAlgorithm::Es256],
683        }
684    }
685}
686
687pub(crate) fn lifecycle_error_code(status: ManagedAgentStatus) -> ErrorCode {
688    match status {
689        ManagedAgentStatus::Terminated => ErrorCode::IdentityTerminated,
690        ManagedAgentStatus::Revoked | ManagedAgentStatus::Suspended => ErrorCode::IdentitySuspended,
691        ManagedAgentStatus::Active => ErrorCode::IdentityUnavailable,
692    }
693}
694
695fn deserialize_optional_non_null<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
696where
697    D: serde::Deserializer<'de>,
698    T: Deserialize<'de>,
699{
700    T::deserialize(deserializer).map(Some)
701}