1use alloc::boxed::Box;
4use alloc::collections::BTreeMap;
5use alloc::string::String;
6use alloc::sync::Arc;
7use alloc::vec::Vec;
8use core::error::Error as StdError;
9use core::fmt;
10use core::future::Future;
11use core::hash::Hash;
12#[cfg(feature = "std")]
13use miette::Diagnostic;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use smol_str::SmolStr;
17
18use crate::bos::{BosStr, DefaultStr};
19use crate::types::{did::Did, handle::Handle};
20
21#[cfg(feature = "std")]
22use std::path::{Path, PathBuf};
23
24#[cfg(not(feature = "std"))]
26use maitake_sync::RwLock;
27#[cfg(feature = "std")]
28use tokio::sync::RwLock;
29
30#[derive(Debug, thiserror::Error)]
32#[cfg_attr(feature = "std", derive(Diagnostic))]
33#[non_exhaustive]
34pub enum SessionStoreError {
35 #[cfg(feature = "std")]
37 #[error("I/O error: {0}")]
38 #[cfg_attr(feature = "std", diagnostic(code(jacquard::session_store::io)))]
39 Io(#[from] std::io::Error),
40 #[error("serialization error: {0}")]
42 #[cfg_attr(feature = "std", diagnostic(code(jacquard::session_store::serde)))]
43 Serde(#[from] serde_json::Error),
44 #[error(transparent)]
46 #[cfg_attr(feature = "std", diagnostic(code(jacquard::session_store::other)))]
47 Other(#[from] Box<dyn StdError + Send + Sync>),
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
52pub struct SessionKey {
53 pub did: Did,
55 pub session_id: SmolStr,
57}
58
59impl SessionKey {
60 pub fn new(did: Did, session_id: impl Into<SmolStr>) -> Self {
62 Self {
63 did,
64 session_id: session_id.into(),
65 }
66 }
67
68 pub fn did(&self) -> Did<&str> {
70 self.did.borrow()
71 }
72
73 pub fn session_id(&self) -> &str {
75 self.session_id.as_str()
76 }
77}
78
79impl fmt::Display for SessionKey {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "{}/{}", self.did, self.session_id)
82 }
83}
84
85impl From<(Did, SmolStr)> for SessionKey {
86 fn from((did, session_id): (Did, SmolStr)) -> Self {
87 Self { did, session_id }
88 }
89}
90
91impl From<SessionKey> for (Did, SmolStr) {
92 fn from(key: SessionKey) -> Self {
93 (key.did, key.session_id)
94 }
95}
96
97impl SessionHint<DefaultStr> {
98 pub fn any() -> Self {
100 SessionHint::Any
101 }
102
103 pub fn key(key: SessionKey) -> Self {
105 SessionHint::Key(key)
106 }
107
108 pub fn identifier(identifier: DefaultStr) -> Self {
110 SessionHint::Identifier(identifier)
111 }
112
113 pub fn did(did: Did<DefaultStr>) -> Self {
115 SessionHint::Did(did)
116 }
117
118 pub fn handle(handle: Handle<DefaultStr>) -> Self {
120 SessionHint::Handle(handle)
121 }
122}
123
124impl<'a> SessionHint<&'a str> {
125 pub fn from_input(input: &'a str) -> Self {
130 if let Ok(did) = Did::new(input) {
131 SessionHint::Did(did)
132 } else if let Ok(handle) = Handle::new(input) {
133 SessionHint::Handle(handle)
134 } else {
135 SessionHint::Identifier(input)
136 }
137 }
138
139 pub fn from_optional_input(input: Option<&'a str>) -> Self {
143 match input {
144 Some(input) => Self::from_input(input),
145 None => SessionHint::Any,
146 }
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
157pub enum SessionHint<S: BosStr = DefaultStr> {
158 Any,
160 Did(Did<S>),
162 Handle(Handle<S>),
164 Key(SessionKey),
166 Identifier(S),
172}
173
174pub fn match_session_key<I, S>(hint: &SessionHint<S>, keys: I) -> Option<SessionKey>
176where
177 I: IntoIterator<Item = SessionKey>,
178 S: BosStr,
179{
180 match hint {
181 SessionHint::Any => keys.into_iter().next(),
182 SessionHint::Did(did) => keys
183 .into_iter()
184 .find(|key| key.did.as_str() == did.as_ref()),
185 SessionHint::Handle(_) | SessionHint::Identifier(_) => None,
186 SessionHint::Key(target) => keys.into_iter().find(|key| key == target),
187 }
188}
189
190#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
198pub trait SessionSelector<M>: Send + Sync {
199 type Error;
201
202 fn select_session<S: BosStr + Send + Sync>(
204 &self,
205 hint: &SessionHint<S>,
206 ) -> impl Future<Output = Result<Option<M>, Self::Error>>;
207}
208
209#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
211pub trait SessionStore<K, T>: Send + Sync
212where
213 K: Eq + Hash,
214 T: Clone,
215{
216 fn get(&self, key: &K) -> impl Future<Output = Option<T>>;
218 fn set(&self, key: K, session: T) -> impl Future<Output = Result<(), SessionStoreError>>;
220 fn del(&self, key: &K) -> impl Future<Output = Result<(), SessionStoreError>>;
222 fn list_keys(&self) -> impl Future<Output = Result<Vec<K>, SessionStoreError>>
224 where
225 K: Clone,
226 {
227 async { Ok(Vec::new()) }
228 }
229}
230
231#[derive(Clone)]
233pub struct MemorySessionStore<K, T>(Arc<RwLock<BTreeMap<K, T>>>);
234
235impl<K, T> Default for MemorySessionStore<K, T> {
236 fn default() -> Self {
237 Self(Arc::new(RwLock::new(BTreeMap::new())))
238 }
239}
240
241impl<K, T> SessionStore<K, T> for MemorySessionStore<K, T>
242where
243 K: Eq + Hash + Send + Sync + Ord,
244 T: Clone + Send + Sync,
245{
246 async fn get(&self, key: &K) -> Option<T> {
247 self.0.read().await.get(key).cloned()
248 }
249 async fn set(&self, key: K, session: T) -> Result<(), SessionStoreError> {
250 self.0.write().await.insert(key, session);
251 Ok(())
252 }
253 async fn del(&self, key: &K) -> Result<(), SessionStoreError> {
254 self.0.write().await.remove(key);
255 Ok(())
256 }
257
258 async fn list_keys(&self) -> Result<Vec<K>, SessionStoreError>
259 where
260 K: Clone,
261 {
262 Ok(self.0.read().await.keys().cloned().collect())
263 }
264}
265
266impl<T> SessionSelector<SessionKey> for MemorySessionStore<SessionKey, T>
267where
268 T: Clone + Send + Sync,
269{
270 type Error = SessionStoreError;
271
272 async fn select_session<S: BosStr + Send + Sync>(
273 &self,
274 hint: &SessionHint<S>,
275 ) -> Result<Option<SessionKey>, Self::Error> {
276 Ok(match_session_key(hint, self.list_keys().await?))
277 }
278}
279
280#[cfg(feature = "std")]
292#[derive(Clone, Debug)]
293pub struct FileTokenStore {
294 pub path: PathBuf,
296}
297
298#[cfg(feature = "std")]
299impl FileTokenStore {
300 pub fn try_new(path: impl AsRef<Path>) -> Result<Self, SessionStoreError> {
310 let path = path.as_ref();
311
312 if let Some(parent) = path.parent() {
314 if !parent.as_os_str().is_empty() && !parent.exists() {
315 std::fs::create_dir_all(parent)?;
316 }
317 }
318
319 if !path.exists() {
321 std::fs::write(path, b"{}")?;
322 }
323
324 Ok(Self {
325 path: path.to_path_buf(),
326 })
327 }
328
329 pub fn new(path: impl AsRef<Path>) -> Self {
336 Self::try_new(path).expect("failed to initialize FileTokenStore")
337 }
338}
339
340#[cfg(feature = "std")]
341impl FileTokenStore {
342 pub fn get_value(&self, key: &str) -> Result<Option<Value>, SessionStoreError> {
344 let file = std::fs::read_to_string(&self.path)?;
345 let store: Value = serde_json::from_str(&file)?;
346 Ok(store.get(key).cloned())
347 }
348
349 pub fn set_value(&self, key: impl Into<String>, value: Value) -> Result<(), SessionStoreError> {
351 let file = std::fs::read_to_string(&self.path)?;
352 let mut store: Value = serde_json::from_str(&file)?;
353 if let Some(store) = store.as_object_mut() {
354 store.insert(key.into(), value);
355 std::fs::write(&self.path, serde_json::to_string_pretty(&store)?)?;
356 Ok(())
357 } else {
358 Err(SessionStoreError::Other("invalid store".into()))
359 }
360 }
361
362 pub fn remove_value(&self, key: &str) -> Result<(), SessionStoreError> {
364 let file = std::fs::read_to_string(&self.path)?;
365 let mut store: Value = serde_json::from_str(&file)?;
366 if let Some(store) = store.as_object_mut() {
367 store.remove(key);
368 std::fs::write(&self.path, serde_json::to_string_pretty(&store)?)?;
369 Ok(())
370 } else {
371 Err(SessionStoreError::Other("invalid store".into()))
372 }
373 }
374
375 pub fn entries(&self) -> Result<Vec<(String, Value)>, SessionStoreError> {
377 let file = std::fs::read_to_string(&self.path)?;
378 let store: Value = serde_json::from_str(&file)?;
379 if let Some(store) = store.as_object() {
380 Ok(store
381 .iter()
382 .map(|(key, value)| (key.clone(), value.clone()))
383 .collect())
384 } else {
385 Err(SessionStoreError::Other("invalid store".into()))
386 }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use alloc::string::ToString;
394
395 #[test]
396 fn session_key_display_uses_slash_separator() {
397 let did = Did::new_static("did:plc:alice").unwrap();
398 let key = SessionKey::new(did, "session_1");
399 assert_eq!(key.to_string(), "did:plc:alice/session_1");
400 }
401
402 #[tokio::test]
403 async fn memory_store_lists_keys() {
404 let store = MemorySessionStore::<SessionKey, String>::default();
405 let key = SessionKey::new(Did::new_static("did:plc:alice").unwrap(), "session");
406 store.set(key.clone(), "value".to_string()).await.unwrap();
407 assert_eq!(store.list_keys().await.unwrap(), vec![key]);
408 }
409
410 struct EmptyStore;
411
412 impl SessionStore<SessionKey, String> for EmptyStore {
413 async fn get(&self, _key: &SessionKey) -> Option<String> {
414 None
415 }
416
417 async fn set(&self, _key: SessionKey, _session: String) -> Result<(), SessionStoreError> {
418 Ok(())
419 }
420
421 async fn del(&self, _key: &SessionKey) -> Result<(), SessionStoreError> {
422 Ok(())
423 }
424 }
425
426 #[tokio::test]
427 async fn default_list_keys_is_empty() {
428 assert!(EmptyStore.list_keys().await.unwrap().is_empty());
429 }
430
431 #[test]
432 fn match_session_key_is_resolver_free() {
433 let alice = SessionKey::new(Did::new_static("did:plc:alice").unwrap(), "a");
434 let bob = SessionKey::new(Did::new_static("did:plc:bob").unwrap(), "b");
435 let keys = vec![alice.clone(), bob.clone()];
436
437 assert_eq!(
438 match_session_key(&SessionHint::any(), keys.clone()),
439 Some(alice.clone())
440 );
441 assert_eq!(
442 match_session_key(&SessionHint::Did(bob.did.clone()), keys.clone()),
443 Some(bob.clone())
444 );
445 assert_eq!(
446 match_session_key(&SessionHint::key(bob.clone()), keys.clone()),
447 Some(bob.clone())
448 );
449 assert_eq!(
450 match_session_key(
451 &SessionHint::key(SessionKey::new(
452 Did::new_static("did:plc:carol").unwrap(),
453 "c",
454 )),
455 keys.clone(),
456 ),
457 None
458 );
459 assert_eq!(match_session_key(&SessionHint::any(), Vec::new()), None);
460 assert_eq!(
461 match_session_key(
462 &SessionHint::<DefaultStr>::Handle(
463 Handle::new_static("alice.example.com").unwrap()
464 ),
465 keys.clone(),
466 ),
467 None
468 );
469 assert_eq!(
470 match_session_key(
471 &SessionHint::Identifier(SmolStr::new("alice@example.com")),
472 keys
473 ),
474 None
475 );
476 }
477}