at main 112 lines 3.9 kB view raw
1//! DNS-safe encoding for publication AT-URIs. 2//! 3//! Custom domains use self-describing CNAME records pointing to 4//! `nb-{encoded-did}-{rkey}.weaver.sh`. This module handles 5//! encoding and decoding the subdomain portion. 6 7use jacquard::IntoStatic; 8use jacquard::types::string::{Did, Tid}; 9 10const PREFIX: &str = "nb-"; 11 12/// Encode a Publication AT-URI into a DNS-safe subdomain. 13/// 14/// Encoding rules: 15/// - DID `:` separators become `-` 16/// - `did:web` dots become `_` (underscores are invalid in domain names, so unambiguous) 17/// - rkey is appended as last segment (TIDs never contain hyphens) 18/// 19/// Examples: 20/// - `did:plc:z72i7hdynmk6r22` + `3jui7kd2y2e2b` → `nb-plc-z72i7hdynmk6r22-3jui7kd2y2e2b` 21/// - `did:web:example.com` + `3jui7kd2y2e2b` → `nb-web-example_com-3jui7kd2y2e2b` 22pub fn encode_publication_subdomain(did: &Did, rkey: &Tid) -> String { 23 let did_str = did.as_str(); 24 25 // did:plc:abc123 -> plc-abc123 26 // did:web:example.com -> web-example_com 27 let encoded_did = did_str 28 .strip_prefix("did:") 29 .expect("valid DID") 30 .replace(':', "-") 31 .replace('.', "_"); 32 33 format!("{}{}-{}", PREFIX, encoded_did, rkey.as_str()) 34} 35 36/// Decode a publication subdomain back to DID and rkey. 37/// 38/// Parsing strategy: work from both ends since rkeys (TIDs) never contain hyphens. 39pub fn decode_publication_subdomain(subdomain: &str) -> Option<(Did<'static>, Tid)> { 40 let inner = subdomain.strip_prefix(PREFIX)?; 41 42 // Split at the LAST hyphen - rkey is on the right (TIDs have no hyphens) 43 let (did_part, rkey_str) = inner.rsplit_once('-')?; 44 45 // Parse rkey as TID 46 let rkey = Tid::from(rkey_str.to_owned()); 47 48 // Reconstruct DID from encoded form 49 // plc-abc123 -> did:plc:abc123 50 // web-example_com -> did:web:example.com 51 let (method, identifier) = did_part.split_once('-')?; 52 let decoded_identifier = identifier.replace('_', "."); 53 let did_str = format!("did:{}:{}", method, decoded_identifier); 54 let did = Did::new(&did_str).ok()?; 55 56 Some((did.into_static(), rkey)) 57} 58 59#[cfg(test)] 60mod tests { 61 use super::*; 62 use jacquard::IntoStatic; 63 64 #[test] 65 fn roundtrip_did_plc() { 66 let did = Did::new("did:plc:z72i7hdynmk6r22").unwrap(); 67 let rkey = Tid::from("3jui7kd2y2e2b".to_owned()); 68 69 let encoded = encode_publication_subdomain(&did, &rkey); 70 assert_eq!(encoded, "nb-plc-z72i7hdynmk6r22-3jui7kd2y2e2b"); 71 72 let (decoded_did, decoded_rkey) = decode_publication_subdomain(&encoded).unwrap(); 73 assert_eq!(decoded_did, did.into_static()); 74 assert_eq!(decoded_rkey, rkey); 75 } 76 77 #[test] 78 fn roundtrip_did_web() { 79 let did = Did::new("did:web:example.com").unwrap(); 80 let rkey = Tid::from("3jui7kd2y2e2b".to_owned()); 81 82 let encoded = encode_publication_subdomain(&did, &rkey); 83 assert_eq!(encoded, "nb-web-example_com-3jui7kd2y2e2b"); 84 85 let (decoded_did, decoded_rkey) = decode_publication_subdomain(&encoded).unwrap(); 86 assert_eq!(decoded_did, did.into_static()); 87 assert_eq!(decoded_rkey, rkey); 88 } 89 90 #[test] 91 fn roundtrip_did_web_with_hyphens() { 92 let did = Did::new("did:web:my-cool-site.example.com").unwrap(); 93 let rkey = Tid::from("3jui7kd2y2e2b".to_owned()); 94 95 let encoded = encode_publication_subdomain(&did, &rkey); 96 assert_eq!(encoded, "nb-web-my-cool-site_example_com-3jui7kd2y2e2b"); 97 98 let (decoded_did, decoded_rkey) = decode_publication_subdomain(&encoded).unwrap(); 99 assert_eq!(decoded_did, did.into_static()); 100 assert_eq!(decoded_rkey, rkey); 101 } 102 103 #[test] 104 fn decode_invalid_prefix() { 105 assert!(decode_publication_subdomain("foo-plc-abc123-rkey").is_none()); 106 } 107 108 #[test] 109 fn decode_missing_method() { 110 assert!(decode_publication_subdomain("nb-abc123").is_none()); 111 } 112}