A library for ATProtocol identities.
1//! Lexicon resolution functionality for AT Protocol. 2//! 3//! This module handles the resolution of lexicon identifiers to their corresponding 4//! schema definitions according to the AT Protocol specification. 5//! 6//! The resolution process: 7//! 1. Convert NSID to DNS name with "_lexicon" prefix 8//! 2. Perform DNS TXT lookup to get DID 9//! 3. Resolve DID to get DID document 10//! 4. Extract PDS endpoint from DID document 11//! 5. Make XRPC call to com.atproto.repo.getRecord to fetch lexicon 12 13use anyhow::Result; 14use atproto_client::{ 15 client::Auth, 16 com::atproto::repo::{GetRecordResponse, get_record}, 17}; 18use atproto_identity::resolve::{DnsResolver, resolve_subject}; 19use serde_json::Value; 20use tracing::instrument; 21 22use crate::{errors::LexiconResolveError, validation}; 23 24/// Trait for lexicon resolution implementations. 25#[async_trait::async_trait] 26pub trait LexiconResolver: Send + Sync { 27 /// Resolve a lexicon NSID to its schema definition. 28 async fn resolve(&self, nsid: &str) -> Result<Value>; 29} 30 31/// Default lexicon resolver implementation using DNS and XRPC. 32#[derive(Clone)] 33pub struct DefaultLexiconResolver<R> { 34 http_client: reqwest::Client, 35 dns_resolver: R, 36} 37 38impl<R> DefaultLexiconResolver<R> { 39 /// Create a new lexicon resolver. 40 pub fn new(http_client: reqwest::Client, dns_resolver: R) -> Self { 41 Self { 42 http_client, 43 dns_resolver, 44 } 45 } 46} 47 48#[async_trait::async_trait] 49impl<R> LexiconResolver for DefaultLexiconResolver<R> 50where 51 R: DnsResolver + Send + Sync, 52{ 53 #[instrument(skip(self), err)] 54 async fn resolve(&self, nsid: &str) -> Result<Value> { 55 // Step 1: Convert NSID to DNS name 56 let dns_name = validation::nsid_to_dns_name(nsid)?; 57 58 // Step 2: Perform DNS lookup to get DID 59 let did = resolve_lexicon_dns(&self.dns_resolver, &dns_name).await?; 60 61 // Step 3: Resolve DID to get DID document 62 let resolved_did = resolve_subject(&self.http_client, &self.dns_resolver, &did).await?; 63 64 // Step 4: Get PDS endpoint from DID document 65 let pds_endpoint = get_pds_from_did(&self.http_client, &resolved_did).await?; 66 67 // Step 5: Fetch lexicon from PDS 68 let lexicon = 69 fetch_lexicon_from_pds(&self.http_client, &pds_endpoint, &resolved_did, nsid).await?; 70 71 Ok(lexicon) 72 } 73} 74 75/// Resolve lexicon DID from DNS TXT records. 76#[instrument(skip(dns_resolver), err)] 77pub async fn resolve_lexicon_dns<R: DnsResolver + ?Sized>( 78 dns_resolver: &R, 79 lookup_dns: &str, 80) -> Result<String, LexiconResolveError> { 81 let txt_records = dns_resolver.resolve_txt(lookup_dns).await?; 82 83 // Look for did= prefix in TXT records 84 let dids: Vec<String> = txt_records 85 .iter() 86 .filter_map(|record| { 87 record 88 .strip_prefix("did=") 89 .or_else(|| record.strip_prefix("did:")) 90 .map(|did| { 91 // Ensure proper DID format 92 if did.starts_with("plc:") || did.starts_with("web:") { 93 format!("did:{}", did) 94 } else if did.starts_with("did:") { 95 did.to_string() 96 } else { 97 format!("did:{}", did) 98 } 99 }) 100 }) 101 .collect(); 102 103 if dids.is_empty() { 104 return Err(LexiconResolveError::NoDIDsFound); 105 } 106 107 if dids.len() > 1 { 108 return Err(LexiconResolveError::MultipleDIDsFound); 109 } 110 111 Ok(dids[0].clone()) 112} 113 114/// Get PDS endpoint from DID document. 115#[instrument(skip(http_client), err)] 116async fn get_pds_from_did(http_client: &reqwest::Client, did: &str) -> Result<String> { 117 use atproto_identity::{ 118 model::Document, 119 plc, 120 resolve::{InputType, parse_input}, 121 web, 122 }; 123 124 // Get DID document based on DID method 125 let did_document: Document = match parse_input(did)? { 126 InputType::Plc(did) => plc::query(http_client, "plc.directory", &did).await?, 127 InputType::Web(did) => web::query(http_client, &did).await?, 128 _ => { 129 return Err(LexiconResolveError::InvalidDIDFormat { 130 did: did.to_string(), 131 } 132 .into()); 133 } 134 }; 135 136 // Extract PDS endpoint from service array 137 for service in &did_document.service { 138 if service.r#type == "AtprotoPersonalDataServer" { 139 return Ok(service.service_endpoint.clone()); 140 } 141 } 142 143 Err(LexiconResolveError::NoPDSEndpoint.into()) 144} 145 146/// Fetch lexicon schema from PDS using XRPC. 147#[instrument(skip(http_client), err)] 148async fn fetch_lexicon_from_pds( 149 http_client: &reqwest::Client, 150 pds_endpoint: &str, 151 did: &str, 152 nsid: &str, 153) -> Result<Value> { 154 // Construct the record key for the lexicon 155 // Lexicons are stored under the com.atproto.repo.lexicon collection 156 let collection = "com.atproto.lexicon.schema"; 157 158 // Make XRPC call to get the lexicon record without authentication 159 let auth = Auth::None; 160 let response = get_record( 161 http_client, 162 &auth, 163 pds_endpoint, 164 did, 165 collection, 166 nsid, 167 None, 168 ) 169 .await 170 .map_err(|e| LexiconResolveError::PDSFetchFailed { 171 details: e.to_string(), 172 })?; 173 174 // Extract the value from the response 175 match response { 176 GetRecordResponse::Record { value, .. } => Ok(value), 177 GetRecordResponse::Error(err) => { 178 let msg = err 179 .message 180 .or(err.error_description) 181 .or(err.error) 182 .unwrap_or_else(|| "Unknown error".to_string()); 183 Err(LexiconResolveError::PDSErrorResponse { 184 nsid: nsid.to_string(), 185 message: msg, 186 } 187 .into()) 188 } 189 } 190}