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}