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 for service in &doc.service { 235 if service.service_type == "AtprotoAppView" 236 || service.id.contains("atproto_appview") 237 || service.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 246 for service in &doc.service { 247 if service.service_type.contains("AppView") || service.id.contains("appview") { 248 return Some(ResolvedService { 249 url: service.service_endpoint.clone(), 250 did: doc.id.clone(), 251 }); 252 } 253 } 254 255 if let Some(service) = doc.service.first() 256 && service.service_endpoint.starts_with("http") 257 { 258 warn!( 259 "No explicit AppView service found for {}, using first service: {}", 260 doc.id, service.service_endpoint 261 ); 262 return Some(ResolvedService { 263 url: service.service_endpoint.clone(), 264 did: doc.id.clone(), 265 }); 266 } 267 268 if doc.id.starts_with("did:web:") { 269 let host = doc.id.strip_prefix("did:web:")?; 270 let decoded_host = host.replace("%3A", ":"); 271 let base_host = decoded_host.split('/').next()?; 272 let scheme = if base_host.starts_with("localhost") 273 || base_host.starts_with("127.0.0.1") 274 || base_host.contains(':') 275 { 276 "http" 277 } else { 278 "https" 279 }; 280 warn!( 281 "No service found for {}, deriving URL from DID: {}://{}", 282 doc.id, scheme, base_host 283 ); 284 return Some(ResolvedService { 285 url: format!("{}://{}", scheme, base_host), 286 did: doc.id.clone(), 287 }); 288 } 289 290 None 291 } 292 293 pub async fn resolve_did_document(&self, did: &str) -> Option<serde_json::Value> { 294 { 295 let cache = self.did_doc_cache.read().await; 296 if let Some(cached) = cache.get(did) 297 && cached.resolved_at.elapsed() < self.cache_ttl 298 { 299 return Some(cached.document.clone()); 300 } 301 } 302 303 let result = if did.starts_with("did:web:") { 304 self.fetch_did_document_web(did).await 305 } else if did.starts_with("did:plc:") { 306 self.fetch_did_document_plc(did).await 307 } else { 308 warn!("Unsupported DID method for document resolution: {}", did); 309 return None; 310 }; 311 312 match result { 313 Ok(doc) => { 314 let mut cache = self.did_doc_cache.write().await; 315 cache.insert( 316 did.to_string(), 317 CachedDidDocument { 318 document: doc.clone(), 319 resolved_at: Instant::now(), 320 }, 321 ); 322 Some(doc) 323 } 324 Err(e) => { 325 warn!("Failed to resolve DID document for {}: {}", did, e); 326 None 327 } 328 } 329 } 330 331 async fn fetch_did_document_web(&self, did: &str) -> Result<serde_json::Value, String> { 332 let url = Self::build_did_web_url(did)?; 333 334 let resp = self 335 .client 336 .get(&url) 337 .send() 338 .await 339 .map_err(|e| format!("HTTP request failed: {}", e))?; 340 341 if !resp.status().is_success() { 342 return Err(format!("HTTP {}", resp.status())); 343 } 344 345 resp.json::<serde_json::Value>() 346 .await 347 .map_err(|e| format!("Failed to parse DID document: {}", e)) 348 } 349 350 async fn fetch_did_document_plc(&self, did: &str) -> Result<serde_json::Value, String> { 351 let url = format!("{}/{}", self.plc_directory_url, urlencoding::encode(did)); 352 353 let resp = self 354 .client 355 .get(&url) 356 .send() 357 .await 358 .map_err(|e| format!("HTTP request failed: {}", e))?; 359 360 if resp.status() == reqwest::StatusCode::NOT_FOUND { 361 return Err("DID not found".to_string()); 362 } 363 364 if !resp.status().is_success() { 365 return Err(format!("HTTP {}", resp.status())); 366 } 367 368 resp.json::<serde_json::Value>() 369 .await 370 .map_err(|e| format!("Failed to parse DID document: {}", e)) 371 } 372 373 pub async fn invalidate_cache(&self, did: &str) { 374 let mut cache = self.did_cache.write().await; 375 cache.remove(did); 376 drop(cache); 377 let mut doc_cache = self.did_doc_cache.write().await; 378 doc_cache.remove(did); 379 } 380} 381 382impl Default for DidResolver { 383 fn default() -> Self { 384 Self::new() 385 } 386} 387 388pub fn create_did_resolver() -> Arc<DidResolver> { 389 Arc::new(DidResolver::new()) 390}