//! DNS-safe encoding for publication AT-URIs. //! //! Custom domains use self-describing CNAME records pointing to //! `nb-{encoded-did}-{rkey}.weaver.sh`. This module handles //! encoding and decoding the subdomain portion. use jacquard::IntoStatic; use jacquard::types::string::{Did, Tid}; const PREFIX: &str = "nb-"; /// Encode a Publication AT-URI into a DNS-safe subdomain. /// /// Encoding rules: /// - DID `:` separators become `-` /// - `did:web` dots become `_` (underscores are invalid in domain names, so unambiguous) /// - rkey is appended as last segment (TIDs never contain hyphens) /// /// Examples: /// - `did:plc:z72i7hdynmk6r22` + `3jui7kd2y2e2b` → `nb-plc-z72i7hdynmk6r22-3jui7kd2y2e2b` /// - `did:web:example.com` + `3jui7kd2y2e2b` → `nb-web-example_com-3jui7kd2y2e2b` pub fn encode_publication_subdomain(did: &Did, rkey: &Tid) -> String { let did_str = did.as_str(); // did:plc:abc123 -> plc-abc123 // did:web:example.com -> web-example_com let encoded_did = did_str .strip_prefix("did:") .expect("valid DID") .replace(':', "-") .replace('.', "_"); format!("{}{}-{}", PREFIX, encoded_did, rkey.as_str()) } /// Decode a publication subdomain back to DID and rkey. /// /// Parsing strategy: work from both ends since rkeys (TIDs) never contain hyphens. pub fn decode_publication_subdomain(subdomain: &str) -> Option<(Did<'static>, Tid)> { let inner = subdomain.strip_prefix(PREFIX)?; // Split at the LAST hyphen - rkey is on the right (TIDs have no hyphens) let (did_part, rkey_str) = inner.rsplit_once('-')?; // Parse rkey as TID let rkey = Tid::from(rkey_str.to_owned()); // Reconstruct DID from encoded form // plc-abc123 -> did:plc:abc123 // web-example_com -> did:web:example.com let (method, identifier) = did_part.split_once('-')?; let decoded_identifier = identifier.replace('_', "."); let did_str = format!("did:{}:{}", method, decoded_identifier); let did = Did::new(&did_str).ok()?; Some((did.into_static(), rkey)) } #[cfg(test)] mod tests { use super::*; use jacquard::IntoStatic; #[test] fn roundtrip_did_plc() { let did = Did::new("did:plc:z72i7hdynmk6r22").unwrap(); let rkey = Tid::from("3jui7kd2y2e2b".to_owned()); let encoded = encode_publication_subdomain(&did, &rkey); assert_eq!(encoded, "nb-plc-z72i7hdynmk6r22-3jui7kd2y2e2b"); let (decoded_did, decoded_rkey) = decode_publication_subdomain(&encoded).unwrap(); assert_eq!(decoded_did, did.into_static()); assert_eq!(decoded_rkey, rkey); } #[test] fn roundtrip_did_web() { let did = Did::new("did:web:example.com").unwrap(); let rkey = Tid::from("3jui7kd2y2e2b".to_owned()); let encoded = encode_publication_subdomain(&did, &rkey); assert_eq!(encoded, "nb-web-example_com-3jui7kd2y2e2b"); let (decoded_did, decoded_rkey) = decode_publication_subdomain(&encoded).unwrap(); assert_eq!(decoded_did, did.into_static()); assert_eq!(decoded_rkey, rkey); } #[test] fn roundtrip_did_web_with_hyphens() { let did = Did::new("did:web:my-cool-site.example.com").unwrap(); let rkey = Tid::from("3jui7kd2y2e2b".to_owned()); let encoded = encode_publication_subdomain(&did, &rkey); assert_eq!(encoded, "nb-web-my-cool-site_example_com-3jui7kd2y2e2b"); let (decoded_did, decoded_rkey) = decode_publication_subdomain(&encoded).unwrap(); assert_eq!(decoded_did, did.into_static()); assert_eq!(decoded_rkey, rkey); } #[test] fn decode_invalid_prefix() { assert!(decode_publication_subdomain("foo-plc-abc123-rkey").is_none()); } #[test] fn decode_missing_method() { assert!(decode_publication_subdomain("nb-abc123").is_none()); } }