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 ClientAssertionReplayRecord, ClientAssertionReplayStore, Clock, CommandIdempotencyInput,
12 CommandIdempotencyRecord, CommandIdempotencyResult, CommandIdempotencyStore, CommandOperation,
13 EnrollmentDecision, EnrollmentPolicy, EnrollmentRecord, EnrollmentStore, ServiceError,
14};
15
16type IdempotencyKey = (String, String);
17type IdempotencyLock = Arc<futures::lock::Mutex<()>>;
18
19const IDEMPOTENCY_RETENTION: Duration = Duration::from_secs(3600);
24
25#[derive(Default)]
26pub struct SystemClock;
27
28impl Clock for SystemClock {
29 fn now(&self) -> OffsetDateTime {
30 OffsetDateTime::now_utc()
31 }
32}
33
34pub struct StaticEnrollmentPolicy {
35 decision: EnrollmentDecision,
36}
37
38impl StaticEnrollmentPolicy {
39 pub fn new(decision: EnrollmentDecision) -> Self {
40 Self { decision }
41 }
42}
43
44impl Default for StaticEnrollmentPolicy {
45 fn default() -> Self {
46 Self::new(EnrollmentDecision::default())
47 }
48}
49
50#[async_trait]
51impl EnrollmentPolicy for StaticEnrollmentPolicy {
52 async fn decide(
53 &self,
54 _request: &aep_core::EnrollRequest,
55 _now: OffsetDateTime,
56 ) -> Result<EnrollmentDecision, ServiceError> {
57 Ok(self.decision.clone())
58 }
59}
60
61#[derive(Default)]
62pub struct MemoryEnrollmentStore {
63 records: Mutex<BTreeMap<String, EnrollmentRecord>>,
64}
65
66impl MemoryEnrollmentStore {
67 pub fn new(records: impl IntoIterator<Item = EnrollmentRecord>) -> Result<Self, ServiceError> {
68 let mut entries = BTreeMap::new();
69 for record in records {
70 if record.agent_did.is_empty() {
71 return Err(ServiceError::Store(
72 "AEP enrollment Agent DID must not be empty".to_owned(),
73 ));
74 }
75 entries.insert(record.agent_did.clone(), record);
76 }
77 Ok(Self {
78 records: Mutex::new(entries),
79 })
80 }
81}
82
83#[async_trait]
84impl EnrollmentStore for MemoryEnrollmentStore {
85 async fn find(&self, agent_did: &str) -> Result<Option<EnrollmentRecord>, ServiceError> {
86 Ok(self
87 .records
88 .lock()
89 .map_err(lock_error)?
90 .get(agent_did)
91 .cloned())
92 }
93
94 async fn save(&self, record: EnrollmentRecord) -> Result<EnrollmentRecord, ServiceError> {
95 if record.agent_did.is_empty() {
96 return Err(ServiceError::Store(
97 "AEP enrollment Agent DID must not be empty".to_owned(),
98 ));
99 }
100 self.records
101 .lock()
102 .map_err(lock_error)?
103 .insert(record.agent_did.clone(), record.clone());
104 Ok(record)
105 }
106}
107
108#[derive(Default)]
109pub struct MemoryClientAssertionReplayStore {
110 records: Mutex<BTreeMap<(String, String), ClientAssertionReplayRecord>>,
111}
112
113#[async_trait]
114impl ClientAssertionReplayStore for MemoryClientAssertionReplayStore {
115 async fn consume(
116 &self,
117 record: ClientAssertionReplayRecord,
118 now: OffsetDateTime,
119 ) -> Result<bool, ServiceError> {
120 let mut records = self.records.lock().map_err(lock_error)?;
121 records.retain(|_, existing| existing.expires_at > now);
122 let key = (record.sub.clone(), record.jti.clone());
123 if records.contains_key(&key) {
124 return Ok(false);
125 }
126 records.insert(key, record);
127 Ok(true)
128 }
129}
130
131pub struct MemoryCommandIdempotencyStore {
132 clock: Arc<dyn Clock>,
133 locks: Mutex<BTreeMap<IdempotencyKey, IdempotencyLock>>,
134 records: Mutex<BTreeMap<IdempotencyKey, CommandIdempotencyRecord>>,
135}
136
137impl Default for MemoryCommandIdempotencyStore {
138 fn default() -> Self {
139 Self::with_clock(Arc::new(SystemClock))
140 }
141}
142
143impl MemoryCommandIdempotencyStore {
144 pub fn with_clock(clock: Arc<dyn Clock>) -> Self {
147 Self {
148 clock,
149 locks: Mutex::new(BTreeMap::new()),
150 records: Mutex::new(BTreeMap::new()),
151 }
152 }
153
154 pub fn new(
155 records: impl IntoIterator<Item = CommandIdempotencyRecord>,
156 ) -> Result<Self, ServiceError> {
157 let mut entries = BTreeMap::new();
158 for record in records {
159 let key = (
160 record.input.agent_did.clone(),
161 record.input.idempotency_key.clone(),
162 );
163 if key.0.is_empty() || key.1.is_empty() {
164 return Err(ServiceError::Store(
165 "AEP idempotency record key must not be empty".to_owned(),
166 ));
167 }
168 entries.insert(key, record);
169 }
170 Ok(Self {
171 clock: Arc::new(SystemClock),
172 locks: Mutex::new(BTreeMap::new()),
173 records: Mutex::new(entries),
174 })
175 }
176
177 fn command_lock(&self, key: &IdempotencyKey) -> Result<IdempotencyLock, ServiceError> {
178 Ok(self
179 .locks
180 .lock()
181 .map_err(lock_error)?
182 .entry(key.clone())
183 .or_default()
184 .clone())
185 }
186
187 fn release_command_lock(
188 &self,
189 key: &IdempotencyKey,
190 command_lock: &IdempotencyLock,
191 ) -> Result<(), ServiceError> {
192 let mut locks = self.locks.lock().map_err(lock_error)?;
193 if locks
194 .get(key)
195 .is_some_and(|current| Arc::ptr_eq(current, command_lock))
196 && Arc::strong_count(command_lock) == 2
197 {
198 locks.remove(key);
199 }
200 Ok(())
201 }
202}
203
204#[async_trait]
205impl CommandIdempotencyStore for MemoryCommandIdempotencyStore {
206 async fn execute(
207 &self,
208 input: CommandIdempotencyInput,
209 operation: CommandOperation,
210 ) -> Result<CommandIdempotencyResult, ServiceError> {
211 let key = (input.agent_did.clone(), input.idempotency_key.clone());
212 let command_lock = self.command_lock(&key)?;
213 let result = {
214 let _guard = command_lock.lock().await;
215 let now = self.clock.now();
216 let existing = {
217 let mut records = self.records.lock().map_err(lock_error)?;
218 records.retain(|_, record| record.created_at + IDEMPOTENCY_RETENTION >= now);
219 records.get(&key).cloned()
220 };
221 if let Some(record) = existing {
222 if record.input.command == input.command
223 && record.input.request_hash == input.request_hash
224 {
225 Ok(CommandIdempotencyResult::Replayed(record.response))
226 } else {
227 Ok(CommandIdempotencyResult::Conflict)
228 }
229 } else {
230 match operation().await {
231 Ok(response) => {
232 let created_at = self.clock.now();
233 self.records.lock().map_err(lock_error)?.insert(
234 key.clone(),
235 CommandIdempotencyRecord {
236 input,
237 response: response.clone(),
238 created_at,
239 },
240 );
241 Ok(CommandIdempotencyResult::Created(response))
242 }
243 Err(error) => Err(error),
244 }
245 }
246 };
247 self.release_command_lock(&key, &command_lock)?;
248 result
249 }
250}
251
252fn lock_error<T>(_error: std::sync::PoisonError<T>) -> ServiceError {
253 ServiceError::Store("AEP Service memory store lock is poisoned".to_owned())
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn retains_an_idempotency_lock_while_another_request_holds_it() {
262 let store = MemoryCommandIdempotencyStore::default();
263 let key = ("did:web:agent.example".to_owned(), "request-1".to_owned());
264 let first = store.command_lock(&key).expect("first lock");
265 let waiting = store.command_lock(&key).expect("waiting lock");
266
267 store
268 .release_command_lock(&key, &first)
269 .expect("retain lock");
270 let concurrent = store.command_lock(&key).expect("concurrent lock");
271 assert!(Arc::ptr_eq(&first, &waiting));
272 assert!(Arc::ptr_eq(&first, &concurrent));
273
274 drop(waiting);
275 drop(concurrent);
276 store
277 .release_command_lock(&key, &first)
278 .expect("release lock");
279 assert!(!store.locks.lock().expect("locks").contains_key(&key));
280 }
281}