//! DID document structures and parsing use crate::did::Did; use crate::error::{PlcError, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Maximum number of verification methods allowed pub const MAX_VERIFICATION_METHODS: usize = 10; /// Internal PLC state format /// /// This represents the internal state of a did:plc document as stored /// in the PLC directory. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PlcState { /// Rotation keys (1-5 did:key strings) #[serde(rename = "rotationKeys")] pub rotation_keys: Vec, /// Verification methods (max 10 entries) #[serde(rename = "verificationMethods")] pub verification_methods: HashMap, /// Also-known-as URIs #[serde(rename = "alsoKnownAs")] pub also_known_as: Vec, /// Service endpoints pub services: HashMap, } impl PlcState { /// Create a new empty PLC state pub fn new() -> Self { Self { rotation_keys: Vec::new(), verification_methods: HashMap::new(), also_known_as: Vec::new(), services: HashMap::new(), } } /// Validate this PLC state according to the specification /// /// # Errors /// /// Returns errors if: /// - Rotation keys count is not 1-5 /// - Rotation keys contain duplicates /// - Verification methods exceed 10 entries pub fn validate(&self) -> Result<()> { // Validate rotation keys (1-5 required, no duplicates) if self.rotation_keys.is_empty() { return Err(PlcError::InvalidRotationKeys( "At least one rotation key is required".to_string(), )); } if self.rotation_keys.len() > 5 { return Err(PlcError::TooManyEntries { field: "rotation_keys".to_string(), max: 5, actual: self.rotation_keys.len(), }); } // Check for duplicate rotation keys let mut seen = std::collections::HashSet::new(); for key in &self.rotation_keys { if !seen.insert(key) { return Err(PlcError::DuplicateEntry { field: "rotation_keys".to_string(), value: key.clone(), }); } } // Validate all rotation keys are valid did:key format for key in &self.rotation_keys { if !key.starts_with("did:key:") { return Err(PlcError::InvalidRotationKeys(format!( "Rotation key must be in did:key format: {}", key ))); } } // Validate verification methods (max 10) if self.verification_methods.len() > MAX_VERIFICATION_METHODS { return Err(PlcError::TooManyEntries { field: "verification_methods".to_string(), max: MAX_VERIFICATION_METHODS, actual: self.verification_methods.len(), }); } // Validate all verification methods are valid did:key format for (name, key) in &self.verification_methods { if !key.starts_with("did:key:") { return Err(PlcError::InvalidVerificationMethods(format!( "Verification method '{}' must be in did:key format: {}", name, key ))); } } Ok(()) } /// Convert this PLC state to a W3C DID document pub fn to_did_document(&self, did: &Did) -> DidDocument { DidDocument::from_plc_state(did.clone(), self.clone()) } } impl Default for PlcState { fn default() -> Self { Self::new() } } /// Service endpoint definition #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServiceEndpoint { /// Service type (e.g., "AtprotoPersonalDataServer") #[serde(rename = "type")] pub service_type: String, /// Service endpoint URL pub endpoint: String, } impl ServiceEndpoint { /// Create a new service endpoint pub fn new(service_type: String, endpoint: String) -> Self { Self { service_type, endpoint, } } /// Validate this service endpoint pub fn validate(&self) -> Result<()> { if self.service_type.is_empty() { return Err(PlcError::InvalidService( "Service type cannot be empty".to_string(), )); } if self.endpoint.is_empty() { return Err(PlcError::InvalidService( "Service endpoint cannot be empty".to_string(), )); } Ok(()) } } /// W3C DID Document format /// /// This represents a DID document in the W3C standard format. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DidDocument { /// The DID this document describes pub id: Did, /// JSON-LD context #[serde(rename = "@context")] pub context: Vec, /// Verification methods #[serde(rename = "verificationMethod")] pub verification_method: Vec, /// Also-known-as URIs #[serde(rename = "alsoKnownAs", skip_serializing_if = "Vec::is_empty", default)] pub also_known_as: Vec, /// Services #[serde(skip_serializing_if = "Vec::is_empty", default)] pub service: Vec, } impl DidDocument { /// Create a DID document from PLC state pub fn from_plc_state(did: Did, state: PlcState) -> Self { let mut verification_methods = Vec::new(); // Add verification methods for (id, controller) in &state.verification_methods { verification_methods.push(VerificationMethod { id: format!("{}#{}", did, id), method_type: "Multikey".to_string(), controller: did.to_string(), public_key_multibase: controller.clone(), }); } // Add services let services: Vec = state .services .iter() .map(|(id, endpoint)| Service { id: format!("{}#{}", did, id), service_type: endpoint.service_type.clone(), service_endpoint: endpoint.endpoint.clone(), }) .collect(); Self { id: did, context: vec![ "https://www.w3.org/ns/did/v1".to_string(), "https://w3id.org/security/multikey/v1".to_string(), ], verification_method: verification_methods, also_known_as: state.also_known_as.clone(), service: services, } } /// Validate this DID document pub fn validate(&self) -> Result<()> { // Convert to PLC state and validate let plc_state = self.to_plc_state()?; plc_state.validate() } /// Convert this DID document to PLC state pub fn to_plc_state(&self) -> Result { let mut verification_methods = HashMap::new(); for vm in &self.verification_method { // Extract the fragment ID (after '#') let id = vm .id .rsplit('#') .next() .ok_or_else(|| { PlcError::InvalidVerificationMethods(format!( "Invalid verification method ID: {}", vm.id )) })? .to_string(); verification_methods.insert(id, vm.public_key_multibase.clone()); } let mut services = HashMap::new(); for svc in &self.service { let id = svc .id .rsplit('#') .next() .ok_or_else(|| PlcError::InvalidService(format!("Invalid service ID: {}", svc.id)))? .to_string(); services.insert( id, ServiceEndpoint { service_type: svc.service_type.clone(), endpoint: svc.service_endpoint.clone(), }, ); } Ok(PlcState { rotation_keys: Vec::new(), // Not stored in DID document verification_methods, also_known_as: self.also_known_as.clone(), services, }) } } /// Verification method in W3C format #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct VerificationMethod { /// Verification method ID (e.g., "did:plc:xyz#atproto") pub id: String, /// Method type (e.g., "Multikey") #[serde(rename = "type")] pub method_type: String, /// Controller DID pub controller: String, /// Public key in multibase format #[serde(rename = "publicKeyMultibase")] pub public_key_multibase: String, } /// Service in W3C format #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct Service { /// Service ID (e.g., "did:plc:xyz#atproto_pds") pub id: String, /// Service type #[serde(rename = "type")] pub service_type: String, /// Service endpoint URL #[serde(rename = "serviceEndpoint")] pub service_endpoint: String, } #[cfg(test)] mod tests { use super::*; #[test] fn test_plc_state_validation() { let mut state = PlcState::new(); // Empty state should fail (no rotation keys) assert!(state.validate().is_err()); // Add a rotation key state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string()); assert!(state.validate().is_ok()); // Add duplicate rotation key state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string()); assert!(state.validate().is_err()); } #[test] fn test_service_endpoint() { let endpoint = ServiceEndpoint::new( "AtprotoPersonalDataServer".to_string(), "https://pds.example.com".to_string(), ); assert!(endpoint.validate().is_ok()); let empty_type = ServiceEndpoint::new(String::new(), "https://example.com".to_string()); assert!(empty_type.validate().is_err()); } #[test] fn test_did_document_conversion() { let did = Did::parse("did:plc:ewvi7nxzyoun6zhxrhs64oiz").unwrap(); let mut state = PlcState::new(); state.rotation_keys.push("did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N".to_string()); state.verification_methods.insert( "atproto".to_string(), "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169".to_string(), ); let doc = state.to_did_document(&did); assert_eq!(doc.id, did); assert_eq!(doc.verification_method.len(), 1); assert_eq!(doc.verification_method[0].id, "did:plc:ewvi7nxzyoun6zhxrhs64oiz#atproto"); } }