use hickory_resolver::Resolver; use crate::error::AppError; use crate::profile::resolve_pds_endpoint; /// Resolve the authority DID and PDS endpoint for the given NSID. /// /// 1. Extract the authority from the NSID (all segments except the last). /// 2. Reverse the authority segments to form a domain name. /// 3. Look up `_lexicon.{domain}` TXT record for a `did=` value. /// 4. Resolve the DID → PDS endpoint via the PLC directory. pub async fn resolve_nsid_authority( http: &reqwest::Client, plc_url: &str, nsid: &str, ) -> Result<(String, String), AppError> { let segments: Vec<&str> = nsid.split('.').collect(); if segments.len() < 2 { return Err(AppError::BadRequest(format!( "invalid NSID (too few segments): {nsid}" ))); } // Authority is everything except the last segment, reversed to form a domain. let authority_segments = &segments[..segments.len() - 1]; let reversed: Vec<&str> = authority_segments.iter().copied().rev().collect(); let domain = reversed.join("."); let lookup_name = format!("_lexicon.{domain}."); let resolver = Resolver::builder_tokio() .map_err(|e| AppError::Internal(format!("failed to create DNS resolver: {e}")))? .build(); let txt_lookup = resolver.txt_lookup(&lookup_name).await.map_err(|e| { AppError::BadRequest(format!("DNS TXT lookup failed for {lookup_name}: {e}")) })?; let did = txt_lookup .iter() .flat_map(|txt| txt.txt_data().iter()) .filter_map(|data| { let s = std::str::from_utf8(data).ok()?; s.strip_prefix("did=") }) .next() .ok_or_else(|| AppError::BadRequest(format!("no did= TXT record found at {lookup_name}")))? .to_string(); let pds_endpoint = resolve_pds_endpoint(http, plc_url, &did).await?; Ok((did, pds_endpoint)) } /// Fetch a lexicon record from a PDS. /// /// Calls `com.atproto.repo.getRecord` with collection `com.atproto.lexicon.schema` /// and the NSID as the rkey. Returns the `value` field (the raw lexicon JSON). pub async fn fetch_lexicon_from_pds( http: &reqwest::Client, pds_endpoint: &str, did: &str, nsid: &str, ) -> Result { let url = format!( "{}/xrpc/com.atproto.repo.getRecord?repo={}&collection=com.atproto.lexicon.schema&rkey={}", pds_endpoint.trim_end_matches('/'), did, nsid, ); let resp = http .get(&url) .send() .await .map_err(|e| AppError::Internal(format!("PDS request failed: {e}")))?; if !resp.status().is_success() { return Err(AppError::NotFound(format!( "lexicon record not found for {nsid} in {did}'s repo" ))); } let body: serde_json::Value = resp .json() .await .map_err(|e| AppError::Internal(format!("invalid PDS response: {e}")))?; body.get("value") .cloned() .ok_or_else(|| AppError::Internal("PDS response missing 'value' field".into())) } #[cfg(test)] mod tests { use super::*; #[test] fn nsid_too_few_segments_is_error() { let rt = tokio::runtime::Runtime::new().unwrap(); let http = reqwest::Client::new(); let result = rt.block_on(resolve_nsid_authority(&http, "http://localhost", "single")); assert!(result.is_err()); } }