1use std::collections::HashSet;
27use std::sync::Arc;
28
29use tokio::sync::RwLock;
30use tracing::warn;
31
32#[derive(Clone)]
33pub struct LongLivedTokenStore {
34 inner: Arc<RwLock<LongLivedTokenStoreInner>>,
35}
36
37struct LongLivedTokenStoreInner {
38 tokens: HashSet<String>,
39 allow_any: bool,
42}
43
44impl LongLivedTokenStore {
45 pub fn empty() -> Self {
48 Self {
49 inner: Arc::new(RwLock::new(LongLivedTokenStoreInner {
50 tokens: HashSet::new(),
51 allow_any: false,
52 })),
53 }
54 }
55
56 pub fn from_env() -> Self {
61 let store = Self::empty();
62 if let Ok(raw) = std::env::var("HOMECORE_TOKENS") {
63 if let Ok(mut guard) = store.inner.try_write() {
68 for raw_token in raw.split(',') {
69 let t = raw_token.trim();
70 if !t.is_empty() {
71 guard.tokens.insert(t.to_string());
72 }
73 }
74 }
75 }
76 store
77 }
78
79 pub fn allow_any_non_empty() -> Self {
84 Self {
85 inner: Arc::new(RwLock::new(LongLivedTokenStoreInner {
86 tokens: HashSet::new(),
87 allow_any: true,
88 })),
89 }
90 }
91
92 pub async fn register(&self, token: impl Into<String>) -> bool {
95 let mut guard = self.inner.write().await;
96 guard.tokens.insert(token.into())
97 }
98
99 pub async fn revoke(&self, token: &str) -> bool {
101 let mut guard = self.inner.write().await;
102 guard.tokens.remove(token)
103 }
104
105 pub async fn is_valid(&self, token: &str) -> bool {
109 if token.is_empty() {
110 return false;
111 }
112 let guard = self.inner.read().await;
113 if guard.allow_any {
114 warn!(
115 "LongLivedTokenStore::is_valid called in `allow_any` mode — \
116 any non-empty bearer is accepted. Provision real tokens via \
117 HOMECORE_TOKENS or LongLivedTokenStore::register() before \
118 production."
119 );
120 return true;
121 }
122 guard.tokens.contains(token)
123 }
124
125 pub async fn len(&self) -> usize {
127 self.inner.read().await.tokens.len()
128 }
129
130 pub async fn is_empty(&self) -> bool {
131 self.inner.read().await.tokens.is_empty()
132 }
133
134 pub async fn is_dev_mode(&self) -> bool {
136 self.inner.read().await.allow_any
137 }
138}
139
140impl Default for LongLivedTokenStore {
141 fn default() -> Self {
142 Self::empty()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[tokio::test]
151 async fn empty_store_rejects_everything() {
152 let s = LongLivedTokenStore::empty();
153 assert!(!s.is_valid("anything").await);
154 assert!(!s.is_valid("").await);
155 }
156
157 #[tokio::test]
158 async fn registered_token_is_valid() {
159 let s = LongLivedTokenStore::empty();
160 s.register("hc_abc_123").await;
161 assert!(s.is_valid("hc_abc_123").await);
162 assert!(!s.is_valid("hc_abc_124").await);
163 }
164
165 #[tokio::test]
166 async fn revoke_invalidates() {
167 let s = LongLivedTokenStore::empty();
168 s.register("t1").await;
169 s.register("t2").await;
170 assert!(s.is_valid("t1").await);
171 assert!(s.revoke("t1").await);
172 assert!(!s.is_valid("t1").await);
173 assert!(s.is_valid("t2").await);
174 assert_eq!(s.len().await, 1);
175 }
176
177 #[tokio::test]
178 async fn register_is_idempotent() {
179 let s = LongLivedTokenStore::empty();
180 assert!(s.register("t").await);
181 assert!(!s.register("t").await);
182 assert_eq!(s.len().await, 1);
183 }
184
185 #[tokio::test]
186 async fn empty_token_always_rejected() {
187 let s = LongLivedTokenStore::allow_any_non_empty();
188 assert!(!s.is_valid("").await);
189 }
190
191 #[tokio::test]
192 async fn allow_any_mode_accepts_any_non_empty() {
193 let s = LongLivedTokenStore::allow_any_non_empty();
194 assert!(s.is_valid("literally-anything").await);
195 assert!(s.is_dev_mode().await);
196 }
197
198 #[tokio::test]
199 async fn from_env_unset_is_empty() {
200 std::env::remove_var("HOMECORE_TOKENS");
202 let s = LongLivedTokenStore::from_env();
203 assert_eq!(s.len().await, 0);
204 }
205}