1use std::{
2 collections::BTreeMap,
3 sync::{Arc, Mutex},
4 time::Duration,
5};
6
7use async_trait::async_trait;
8use time::OffsetDateTime;
9
10use crate::{
11 Clock, IdempotencyInput, IdempotencyOperation, IdempotencyResult, IdempotencyStore,
12 IdentityCreation, IdentityListQuery, IdentityListResult, IdentityRecord, IdentityStore,
13 LifecyclePolicy, ManagedAgentStatus, PlatformError, ReplayStore, RequestContext,
14 StoredResponse, validate_identity_record,
15};
16
17type ScopeLock = Arc<futures::lock::Mutex<()>>;
18
19const RETENTION: Duration = Duration::from_secs(3600);
21type IdempotencyKey = (String, String);
22
23#[derive(Default)]
24struct IdentityRecords {
25 by_agent_did: BTreeMap<String, String>,
26 by_agent_did_id: BTreeMap<String, String>,
27 by_scope: BTreeMap<(String, String), String>,
28 records: BTreeMap<String, IdentityRecord>,
29}
30
31#[derive(Default)]
32pub struct MemoryIdentityStore {
33 locks: Mutex<BTreeMap<(String, String), ScopeLock>>,
34 records: Mutex<IdentityRecords>,
35}
36
37impl MemoryIdentityStore {
38 fn scope_lock(&self, key: &(String, String)) -> Result<ScopeLock, PlatformError> {
39 Ok(self
40 .locks
41 .lock()
42 .map_err(lock_error)?
43 .entry(key.clone())
44 .or_default()
45 .clone())
46 }
47
48 fn find_indexed(
49 &self,
50 key: &str,
51 index: impl FnOnce(&IdentityRecords) -> &BTreeMap<String, String>,
52 ) -> Result<Option<IdentityRecord>, PlatformError> {
53 let records = self.records.lock().map_err(lock_error)?;
54 Ok(index(&records)
55 .get(key)
56 .and_then(|identity_id| records.records.get(identity_id))
57 .cloned())
58 }
59
60 fn release_scope_lock(
61 &self,
62 key: &(String, String),
63 scope_lock: &ScopeLock,
64 ) -> Result<(), PlatformError> {
65 let mut locks = self.locks.lock().map_err(lock_error)?;
66 if locks
67 .get(key)
68 .is_some_and(|current| Arc::ptr_eq(current, scope_lock))
69 && Arc::strong_count(scope_lock) == 2
70 {
71 locks.remove(key);
72 }
73 Ok(())
74 }
75}
76
77#[async_trait]
78impl IdentityStore for MemoryIdentityStore {
79 async fn find_or_create(
80 &self,
81 principal: &str,
82 service_did: &str,
83 create: IdentityCreation,
84 ) -> Result<IdentityRecord, PlatformError> {
85 if principal.is_empty() || service_did.is_empty() {
86 return Err(PlatformError::Store(
87 "AEP Platform identity scope must not be empty".to_owned(),
88 ));
89 }
90 let key = (principal.to_owned(), service_did.to_owned());
91 let scope_lock = self.scope_lock(&key)?;
92 let result = async {
93 let _guard = scope_lock.lock().await;
94 let existing = {
95 let records = self.records.lock().map_err(lock_error)?;
96 records
97 .by_scope
98 .get(&key)
99 .and_then(|identity_id| records.records.get(identity_id))
100 .cloned()
101 };
102 if let Some(existing) = existing {
103 Ok(existing)
104 } else {
105 let identity = create().await?;
106 validate_identity_record(&identity)?;
107 if identity.principal != principal || identity.service_did != service_did {
108 return Err(PlatformError::Store(
109 "AEP Platform identity does not match its requested scope".to_owned(),
110 ));
111 }
112 let mut records = self.records.lock().map_err(lock_error)?;
113 if records.records.contains_key(&identity.agent_identity_id)
114 || records.by_agent_did.contains_key(&identity.agent_did)
115 || records.by_agent_did_id.contains_key(&identity.agent_did_id)
116 {
117 return Err(PlatformError::Store(
118 "AEP Platform identity material must be unique".to_owned(),
119 ));
120 }
121 records
122 .by_scope
123 .insert(key.clone(), identity.agent_identity_id.clone());
124 records.by_agent_did.insert(
125 identity.agent_did.clone(),
126 identity.agent_identity_id.clone(),
127 );
128 records.by_agent_did_id.insert(
129 identity.agent_did_id.clone(),
130 identity.agent_identity_id.clone(),
131 );
132 records
133 .records
134 .insert(identity.agent_identity_id.clone(), identity.clone());
135 Ok(identity)
136 }
137 }
138 .await;
139 self.release_scope_lock(&key, &scope_lock)?;
140 result
141 }
142
143 async fn find_by_agent_did(
144 &self,
145 agent_did: &str,
146 ) -> Result<Option<IdentityRecord>, PlatformError> {
147 self.find_indexed(agent_did, |records| &records.by_agent_did)
148 }
149
150 async fn find_by_agent_did_id(
151 &self,
152 agent_did_id: &str,
153 ) -> Result<Option<IdentityRecord>, PlatformError> {
154 self.find_indexed(agent_did_id, |records| &records.by_agent_did_id)
155 }
156
157 async fn get(&self, agent_identity_id: &str) -> Result<Option<IdentityRecord>, PlatformError> {
158 Ok(self
159 .records
160 .lock()
161 .map_err(lock_error)?
162 .records
163 .get(agent_identity_id)
164 .cloned())
165 }
166
167 async fn list(
168 &self,
169 principal: &str,
170 query: &IdentityListQuery,
171 ) -> Result<IdentityListResult, PlatformError> {
172 if principal.is_empty() {
173 return Err(PlatformError::Store(
174 "AEP Platform identity-list principal must not be empty".to_owned(),
175 ));
176 }
177 let mut identities = self
178 .records
179 .lock()
180 .map_err(lock_error)?
181 .records
182 .values()
183 .filter(|identity| {
184 identity.principal == principal
185 && query
186 .service_did
187 .as_ref()
188 .is_none_or(|service_did| &identity.service_did == service_did)
189 && query.status.is_none_or(|status| identity.status == status)
190 })
191 .cloned()
192 .collect::<Vec<_>>();
193 identities.sort_by(|left, right| {
194 left.created_at
195 .cmp(&right.created_at)
196 .then_with(|| left.agent_identity_id.cmp(&right.agent_identity_id))
197 });
198 if query.descending {
199 identities.reverse();
200 }
201 let total = identities.len();
202 let identities = identities
203 .into_iter()
204 .skip(query.offset)
205 .take(query.limit)
206 .collect();
207 Ok(IdentityListResult { identities, total })
208 }
209
210 async fn update_status(
211 &self,
212 agent_identity_id: &str,
213 status: ManagedAgentStatus,
214 updated_at: OffsetDateTime,
215 ) -> Result<Option<IdentityRecord>, PlatformError> {
216 let mut records = self.records.lock().map_err(lock_error)?;
217 let Some(identity) = records.records.get_mut(agent_identity_id) else {
218 return Ok(None);
219 };
220 identity.status = status;
221 identity.updated_at = updated_at;
222 Ok(Some(identity.clone()))
223 }
224}
225
226#[derive(Clone, Debug)]
227struct IdempotencyRecord {
228 input: IdempotencyInput,
229 response: StoredResponse,
230 stored_at: OffsetDateTime,
232}
233
234pub struct MemoryIdempotencyStore {
235 clock: Arc<dyn Clock>,
236 locks: Mutex<BTreeMap<IdempotencyKey, ScopeLock>>,
237 records: Mutex<BTreeMap<IdempotencyKey, IdempotencyRecord>>,
238}
239
240impl MemoryIdempotencyStore {
241 pub fn new(clock: Arc<dyn Clock>) -> Self {
242 Self {
243 clock,
244 locks: Mutex::new(BTreeMap::new()),
245 records: Mutex::new(BTreeMap::new()),
246 }
247 }
248
249 fn operation_lock(&self, key: &IdempotencyKey) -> Result<ScopeLock, PlatformError> {
250 Ok(self
251 .locks
252 .lock()
253 .map_err(lock_error)?
254 .entry(key.clone())
255 .or_default()
256 .clone())
257 }
258
259 fn release_operation_lock(
260 &self,
261 key: &IdempotencyKey,
262 operation_lock: &ScopeLock,
263 ) -> Result<(), PlatformError> {
264 let mut locks = self.locks.lock().map_err(lock_error)?;
265 if locks
266 .get(key)
267 .is_some_and(|current| Arc::ptr_eq(current, operation_lock))
268 && Arc::strong_count(operation_lock) == 2
269 {
270 locks.remove(key);
271 }
272 Ok(())
273 }
274}
275
276#[async_trait]
277impl IdempotencyStore for MemoryIdempotencyStore {
278 async fn execute(
279 &self,
280 input: IdempotencyInput,
281 operation: IdempotencyOperation,
282 ) -> Result<IdempotencyResult, PlatformError> {
283 if input.principal.is_empty()
284 || input.idempotency_key.is_empty()
285 || input.request_hash.is_empty()
286 {
287 return Err(PlatformError::Store(
288 "AEP Platform idempotency input is invalid".to_owned(),
289 ));
290 }
291 let key = (input.principal.clone(), input.idempotency_key.clone());
292 let operation_lock = self.operation_lock(&key)?;
293 let result = async {
294 let _guard = operation_lock.lock().await;
295 let now = self.clock.now();
296 let existing = {
297 let mut records = self.records.lock().map_err(lock_error)?;
298 records.retain(|_, record| record.stored_at + RETENTION >= now);
301 records.get(&key).cloned()
302 };
303 if let Some(existing) = existing {
304 if existing.input.operation == input.operation
305 && existing.input.request_hash == input.request_hash
306 {
307 Ok(IdempotencyResult::Replayed(existing.response))
308 } else {
309 Ok(IdempotencyResult::Conflict)
310 }
311 } else {
312 let mut response = operation().await?;
313 let stored_at = self.clock.now();
314 if response.created_at == OffsetDateTime::UNIX_EPOCH {
315 response.created_at = stored_at;
316 }
317 self.records.lock().map_err(lock_error)?.insert(
318 key.clone(),
319 IdempotencyRecord {
320 input,
321 response: response.clone(),
322 stored_at,
323 },
324 );
325 Ok(IdempotencyResult::Created(response))
326 }
327 }
328 .await;
329 self.release_operation_lock(&key, &operation_lock)?;
330 result
331 }
332}
333
334impl Default for MemoryIdempotencyStore {
335 fn default() -> Self {
336 Self::new(Arc::new(SystemClock))
337 }
338}
339
340#[derive(Default)]
341pub struct MemoryReplayStore {
342 records: Mutex<BTreeMap<String, OffsetDateTime>>,
343}
344
345#[async_trait]
346impl ReplayStore for MemoryReplayStore {
347 async fn consume(
348 &self,
349 key: &str,
350 expires_at: OffsetDateTime,
351 now: OffsetDateTime,
352 ) -> Result<bool, PlatformError> {
353 if key.is_empty() || expires_at <= now {
354 return Ok(false);
355 }
356 let mut records = self.records.lock().map_err(lock_error)?;
357 records.retain(|_, expiry| *expiry > now);
358 if records.contains_key(key) {
359 return Ok(false);
360 }
361 records.insert(key.to_owned(), expires_at);
362 Ok(true)
363 }
364}
365
366#[derive(Default)]
367pub struct SystemClock;
368
369impl Clock for SystemClock {
370 fn now(&self) -> OffsetDateTime {
371 OffsetDateTime::now_utc()
372 }
373}
374
375#[derive(Default)]
376pub struct DefaultLifecyclePolicy;
377
378#[async_trait]
379impl LifecyclePolicy for DefaultLifecyclePolicy {
380 async fn can_sign(
381 &self,
382 identity: &IdentityRecord,
383 _context: &RequestContext,
384 ) -> Result<bool, PlatformError> {
385 Ok(identity.status == ManagedAgentStatus::Active)
386 }
387
388 async fn can_transition(
389 &self,
390 _identity: &IdentityRecord,
391 _status: ManagedAgentStatus,
392 _context: &RequestContext,
393 ) -> Result<bool, PlatformError> {
394 Ok(true)
395 }
396
397 async fn can_verify(
398 &self,
399 identity: &IdentityRecord,
400 _context: &RequestContext,
401 ) -> Result<bool, PlatformError> {
402 Ok(identity.status == ManagedAgentStatus::Active)
403 }
404}
405
406fn lock_error<T>(_error: std::sync::PoisonError<T>) -> PlatformError {
407 PlatformError::Store("AEP Platform memory store lock is poisoned".to_owned())
408}