this repo has no description
1use reqwest::Client; 2use serde::{Deserialize, Serialize}; 3use std::collections::HashMap; 4use std::sync::Arc; 5use std::time::{Duration, Instant}; 6use tokio::sync::RwLock; 7use tracing::{debug, error, info, warn}; 8 9#[derive(Debug, Clone, Serialize, Deserialize)] 10pub struct DidDocument { 11 pub id: String, 12 #[serde(default)] 13 pub service: Vec<DidService>, 14} 15 16#[derive(Debug, Clone, Serialize, Deserialize)] 17#[serde(rename_all = "camelCase")] 18pub struct DidService { 19 pub id: String, 20 #[serde(rename = "type")] 21 pub service_type: String, 22 pub service_endpoint: String, 23} 24 25#[derive(Clone)] 26struct CachedDid { 27 url: String, 28 did: String, 29 resolved_at: Instant, 30} 31 32#[derive(Clone)] 33struct CachedDidDocument { 34 document: serde_json::Value, 35 resolved_at: Instant, 36} 37 38#[derive(Debug, Clone)] 39pub struct ResolvedService { 40 pub url: String, 41 pub did: String, 42} 43 44#[derive(Clone)] 45pub struct DidResolver { 46 did_cache: Arc<RwLock<HashMap<String, CachedDid>>>, 47 did_doc_cache: Arc<RwLock<HashMap<String, CachedDidDocument>>>, 48 client: Client, 49 cache_ttl: Duration, 50 plc_directory_url: String, 51} 52 53impl DidResolver { 54 pub fn new() -> Self { 55 let cache_ttl_secs: u64 = std::env::var("DID_CACHE_TTL_SECS") 56 .ok() 57 .and_then(|v| v.parse().ok()) 58 .unwrap_or(300); 59 60 let plc_directory_url = std::env::var("PLC_DIRECTORY_URL") 61 .unwrap_or_else(|_| "https://plc.directory".to_string()); 62 63 let client = Client::builder() 64 .timeout(Duration::from_secs(10)) 65 .connect_timeout(Duration::from_secs(5)) 66 .pool_max_idle_per_host(10) 67 .build() 68 .unwrap_or_else(|_| Client::new()); 69 70 info!("DID resolver initialized"); 71 72 Self { 73 did_cache: Arc::new(RwLock::new(HashMap::new())), 74 did_doc_cache: Arc::new(RwLock::new(HashMap::new())), 75 client, 76 cache_ttl: Duration::from_secs(cache_ttl_secs), 77 plc_directory_url, 78 } 79 } 80 81 fn build_did_web_url(did: &str) -> Result<String, String> { 82 let host = did 83 .strip_prefix("did:web:") 84 .ok_or("Invalid did:web format")?; 85 86 let (host, path) = if host.contains(':') { 87 let decoded = host.replace("%3A", ":"); 88 let parts: Vec<&str> = decoded.splitn(2, '/').collect(); 89 if parts.len() > 1 { 90 (parts[0].to_string(), format!("/{}", parts[1])) 91 } else { 92 (decoded, String::new()) 93 } 94 } else { 95 let parts: Vec<&str> = host.splitn(2, ':').collect(); 96 if parts.len() > 1 && parts[1].contains('/') { 97 let path_parts: Vec<&str> = parts[1].splitn(2, '/').collect(); 98 if path_parts.len() > 1 { 99 ( 100 format!("{}:{}", parts[0], path_parts[0]), 101 format!("/{}", path_parts[1]), 102 ) 103 } else { 104 (host.to_string(), String::new()) 105 } 106 } else { 107 (host.to_string(), String::new()) 108 } 109 }; 110 111 let scheme = 112 if host.starts_with("localhost") || host.starts_with("127.0.0.1") || host.contains(':') 113 { 114 "http" 115 } else { 116 "https" 117 }; 118 119 let url = if path.is_empty() { 120 format!("{}://{}/.well-known/did.json", scheme, host) 121 } else { 122 format!("{}://{}{}/did.json", scheme, host, path) 123 }; 124 125 Ok(url) 126 } 127 128 pub async fn resolve_did(&self, did: &str) -> Option<ResolvedService> { 129 { 130 let cache = self.did_cache.read().await; 131 if let Some(cached) = cache.get(did) 132 && cached.resolved_at.elapsed() < self.cache_ttl 133 { 134 return Some(ResolvedService { 135 url: cached.url.clone(), 136 did: cached.did.clone(), 137 }); 138 } 139 } 140 141 let resolved = self.resolve_did_internal(did).await?; 142 143 { 144 let mut cache = self.did_cache.write().await; 145 cache.insert( 146 did.to_string(), 147 CachedDid { 148 url: resolved.url.clone(), 149 did: resolved.did.clone(), 150 resolved_at: Instant::now(), 151 }, 152 ); 153 } 154 155 Some(resolved) 156 } 157 158 pub async fn refresh_did(&self, did: &str) -> Option<ResolvedService> { 159 { 160 let mut cache = self.did_cache.write().await; 161 cache.remove(did); 162 } 163 self.resolve_did(did).await 164 } 165 166 async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> { 167 let did_doc = if did.starts_with("did:web:") { 168 self.resolve_did_web(did).await 169 } else if did.starts_with("did:plc:") { 170 self.resolve_did_plc(did).await 171 } else { 172 warn!("Unsupported DID method: {}", did); 173 return None; 174 }; 175 176 let doc = match did_doc { 177 Ok(doc) => doc, 178 Err(e) => { 179 error!("Failed to resolve DID {}: {}", did, e); 180 return None; 181 } 182 }; 183 184 self.extract_service_endpoint(&doc) 185 } 186 187 async fn resolve_did_web(&self, did: &str) -> Result<DidDocument, String> { 188 let url = Self::build_did_web_url(did)?; 189 190 debug!("Resolving did:web {} via {}", did, url); 191 192 let resp = self 193 .client 194 .get(&url) 195 .send() 196 .await 197 .map_err(|e| format!("HTTP request failed: {}", e))?; 198 199 if !resp.status().is_success() { 200 return Err(format!("HTTP {}", resp.status())); 201 } 202 203 resp.json::<DidDocument>() 204 .await 205 .map_err(|e| format!("Failed to parse DID document: {}", e)) 206 } 207 208 async fn resolve_did_plc(&self, did: &str) -> Result<DidDocument, String> { 209 let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 210 211 debug!("Resolving did:plc {} via {}", did, url); 212 213 let resp = self 214 .client 215 .get(&url) 216 .send() 217 .await 218 .map_err(|e| format!("HTTP request failed: {}", e))?; 219 220 if resp.status() == reqwest::StatusCode::NOT_FOUND { 221 return Err("DID not found".to_string()); 222 } 223 224 if !resp.status().is_success() { 225 return Err(format!("HTTP {}", resp.status())); 226 } 227 228 resp.json::<DidDocument>() 229 .await 230 .map_err(|e| format!("Failed to parse DID document: {}", e)) 231 } 232 233 fn extract_service_endpoint(&self, doc: &DidDocument) -> Option<ResolvedService> { 234 if let Some(service) = doc.service.iter().find(|s| { 235 s.service_type == "AtprotoAppView" 236 || s.id.contains("atproto_appview") 237 || s.id.ends_with("#bsky_appview") 238 }) { 239 return Some(ResolvedService { 240 url: service.service_endpoint.clone(), 241 did: doc.id.clone(), 242 }); 243 } 244 245 if let Some(service) = doc 246 .service 247 .iter() 248 .find(|s| s.service_type.contains("AppView") || s.id.contains("appview")) 249 { 250 return Some(ResolvedService { 251 url: service.service_endpoint.clone(), 252 did: doc.id.clone(), 253 }); 254 } 255 256 if let Some(service) = doc.service.first() 257 && service.service_endpoint.starts_with("http") 258 { 259 warn!( 260 "No explicit AppView service found for {}, using first service: {}", 261 doc.id, service.service_endpoint 262 ); 263 return Some(ResolvedService { 264 url: service.service_endpoint.clone(), 265 did: doc.id.clone(), 266 }); 267 } 268 269 if doc.id.starts_with("did:web:") { 270 let host = doc.id.strip_prefix("did:web:")?; 271 let decoded_host = host.replace("%3A", ":"); 272 let base_host = decoded_host.split('/').next()?; 273 let scheme = if base_host.starts_with("localhost") 274 || base_host.starts_with("127.0.0.1") 275 || base_host.contains(':') 276 { 277 "http" 278 } else { 279 "https" 280 }; 281 warn!( 282 "No service found for {}, deriving URL from DID: {}://{}", 283 doc.id, scheme, base_host 284 ); 285 return Some(ResolvedService { 286 url: format!("{}://{}", scheme, base_host), 287 did: doc.id.clone(), 288 }); 289 } 290 291 None 292 } 293 294 pub async fn resolve_did_document(&self, did: &str) -> Option<serde_json::Value> { 295 { 296 let cache = self.did_doc_cache.read().await; 297 if let Some(cached) = cache.get(did) 298 && cached.resolved_at.elapsed() < self.cache_ttl 299 { 300 return Some(cached.document.clone()); 301 } 302 } 303 304 let result = if did.starts_with("did:web:") { 305 self.fetch_did_document_web(did).await 306 } else if did.starts_with("did:plc:") { 307 self.fetch_did_document_plc(did).await 308 } else { 309 warn!("Unsupported DID method for document resolution: {}", did); 310 return None; 311 }; 312 313 match result { 314 Ok(doc) => { 315 let mut cache = self.did_doc_cache.write().await; 316 cache.insert( 317 did.to_string(), 318 CachedDidDocument { 319 document: doc.clone(), 320 resolved_at: Instant::now(), 321 }, 322 ); 323 Some(doc) 324 } 325 Err(e) => { 326 warn!("Failed to resolve DID document for {}: {}", did, e); 327 None 328 } 329 } 330 } 331 332 async fn fetch_did_document_web(&self, did: &str) -> Result<serde_json::Value, String> { 333 let url = Self::build_did_web_url(did)?; 334 335 let resp = self 336 .client 337 .get(&url) 338 .send() 339 .await 340 .map_err(|e| format!("HTTP request failed: {}", e))?; 341 342 if !resp.status().is_success() { 343 return Err(format!("HTTP {}", resp.status())); 344 } 345 346 resp.json::<serde_json::Value>() 347 .await 348 .map_err(|e| format!("Failed to parse DID document: {}", e)) 349 } 350 351 async fn fetch_did_document_plc(&self, did: &str) -> Result<serde_json::Value, String> { 352 let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 353 354 let resp = self 355 .client 356 .get(&url) 357 .send() 358 .await 359 .map_err(|e| format!("HTTP request failed: {}", e))?; 360 361 if resp.status() == reqwest::StatusCode::NOT_FOUND { 362 return Err("DID not found".to_string()); 363 } 364 365 if !resp.status().is_success() { 366 return Err(format!("HTTP {}", resp.status())); 367 } 368 369 resp.json::<serde_json::Value>() 370 .await 371 .map_err(|e| format!("Failed to parse DID document: {}", e)) 372 } 373 374 pub async fn invalidate_cache(&self, did: &str) { 375 let mut cache = self.did_cache.write().await; 376 cache.remove(did); 377 drop(cache); 378 let mut doc_cache = self.did_doc_cache.write().await; 379 doc_cache.remove(did); 380 } 381} 382 383impl Default for DidResolver { 384 fn default() -> Self { 385 Self::new() 386 } 387} 388 389pub fn create_did_resolver() -> Arc<DidResolver> { 390 Arc::new(DidResolver::new()) 391}