idprova_core/aid/
builder.rs1use chrono::Utc;
2
3use super::document::*;
4use crate::crypto::KeyPair;
5use crate::{IdprovaError, Result};
6
7pub struct AidBuilder {
9 id: Option<String>,
10 controller: Option<String>,
11 name: Option<String>,
12 description: Option<String>,
13 model: Option<String>,
14 runtime: Option<String>,
15 config_attestation: Option<String>,
16 verification_methods: Vec<VerificationMethod>,
17 trust_level: Option<String>,
18}
19
20impl AidBuilder {
21 pub fn new() -> Self {
22 Self {
23 id: None,
24 controller: None,
25 name: None,
26 description: None,
27 model: None,
28 runtime: None,
29 config_attestation: None,
30 verification_methods: Vec::new(),
31 trust_level: None,
32 }
33 }
34
35 pub fn id(mut self, id: impl Into<String>) -> Self {
37 self.id = Some(id.into());
38 self
39 }
40
41 pub fn controller(mut self, controller: impl Into<String>) -> Self {
43 self.controller = Some(controller.into());
44 self
45 }
46
47 pub fn name(mut self, name: impl Into<String>) -> Self {
49 self.name = Some(name.into());
50 self
51 }
52
53 pub fn description(mut self, description: impl Into<String>) -> Self {
55 self.description = Some(description.into());
56 self
57 }
58
59 pub fn model(mut self, model: impl Into<String>) -> Self {
61 self.model = Some(model.into());
62 self
63 }
64
65 pub fn runtime(mut self, runtime: impl Into<String>) -> Self {
67 self.runtime = Some(runtime.into());
68 self
69 }
70
71 pub fn config_attestation(mut self, hash: impl Into<String>) -> Self {
73 self.config_attestation = Some(hash.into());
74 self
75 }
76
77 pub fn add_ed25519_key(mut self, keypair: &KeyPair) -> Self {
79 let did = self.id.clone().unwrap_or_default();
80 self.verification_methods.push(VerificationMethod {
81 id: "#key-ed25519".to_string(),
82 key_type: "Ed25519VerificationKey2020".to_string(),
83 controller: self.controller.clone().unwrap_or(did),
84 public_key_multibase: keypair.public_key_multibase(),
85 });
86 self
87 }
88
89 pub fn trust_level(mut self, level: impl Into<String>) -> Self {
91 self.trust_level = Some(level.into());
92 self
93 }
94
95 pub fn build(self) -> Result<AidDocument> {
97 let id = self
98 .id
99 .ok_or_else(|| IdprovaError::AidValidation("id is required".into()))?;
100 let controller = self
101 .controller
102 .ok_or_else(|| IdprovaError::AidValidation("controller is required".into()))?;
103 let name = self
104 .name
105 .ok_or_else(|| IdprovaError::AidValidation("name is required".into()))?;
106
107 if self.verification_methods.is_empty() {
108 return Err(IdprovaError::AidValidation(
109 "at least one verification method required".into(),
110 ));
111 }
112
113 let auth_refs: Vec<String> = self
114 .verification_methods
115 .iter()
116 .map(|vm| vm.id.clone())
117 .collect();
118
119 let metadata = AgentMetadata {
120 name,
121 description: self.description,
122 model: self.model,
123 runtime: self.runtime,
124 config_attestation: self.config_attestation,
125 };
126
127 let service = vec![AidService {
128 id: "#idprova-metadata".to_string(),
129 service_type: "IdprovaAgentMetadata".to_string(),
130 service_endpoint: serde_json::to_value(&metadata)?,
131 }];
132
133 let now = Utc::now();
134
135 let doc = AidDocument {
136 context: vec![
137 "https://www.w3.org/ns/did/v1".to_string(),
138 "https://idprova.dev/v1".to_string(),
139 ],
140 id,
141 controller,
142 verification_method: self.verification_methods,
143 authentication: auth_refs,
144 service: Some(service),
145 trust_level: self.trust_level,
146 version: Some(1),
147 created: Some(now),
148 updated: Some(now),
149 proof: None,
150 };
151
152 doc.validate()?;
153 Ok(doc)
154 }
155}
156
157impl Default for AidBuilder {
158 fn default() -> Self {
159 Self::new()
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::crypto::KeyPair;
167
168 #[test]
169 fn test_build_minimal_aid() {
170 let kp = KeyPair::generate();
171 let doc = AidBuilder::new()
172 .id("did:idprova:example.com:test-agent")
173 .controller("did:idprova:example.com:alice")
174 .name("Test Agent")
175 .add_ed25519_key(&kp)
176 .build()
177 .unwrap();
178
179 assert_eq!(doc.id, "did:idprova:example.com:test-agent");
180 assert_eq!(doc.controller, "did:idprova:example.com:alice");
181 assert_eq!(doc.verification_method.len(), 1);
182 assert!(doc.proof.is_none());
183 }
184
185 #[test]
186 fn test_build_full_aid() {
187 let kp = KeyPair::generate();
188 let doc = AidBuilder::new()
189 .id("did:idprova:techblaze.com.au:kai")
190 .controller("did:idprova:techblaze.com.au:pratyush")
191 .name("Kai Lead Agent")
192 .description("Primary orchestration agent")
193 .model("acme-ai/agent-v2")
194 .runtime("openclaw/v2.1")
195 .config_attestation("blake3:abcdef1234567890")
196 .trust_level("L1")
197 .add_ed25519_key(&kp)
198 .build()
199 .unwrap();
200
201 assert_eq!(doc.trust_level.as_deref(), Some("L1"));
202 assert!(doc.service.is_some());
203 }
204
205 #[test]
206 fn test_build_missing_id_fails() {
207 let kp = KeyPair::generate();
208 let result = AidBuilder::new()
209 .controller("did:idprova:example.com:alice")
210 .name("Test")
211 .add_ed25519_key(&kp)
212 .build();
213 assert!(result.is_err());
214 }
215
216 #[test]
217 fn test_build_no_keys_fails() {
218 let result = AidBuilder::new()
219 .id("did:idprova:example.com:agent")
220 .controller("did:idprova:example.com:alice")
221 .name("Test")
222 .build();
223 assert!(result.is_err());
224 }
225
226 #[test]
227 fn test_serialization_roundtrip() {
228 let kp = KeyPair::generate();
229 let doc = AidBuilder::new()
230 .id("did:idprova:example.com:agent")
231 .controller("did:idprova:example.com:alice")
232 .name("Test Agent")
233 .add_ed25519_key(&kp)
234 .build()
235 .unwrap();
236
237 let json = serde_json::to_string_pretty(&doc).unwrap();
238 let parsed: AidDocument = serde_json::from_str(&json).unwrap();
239 assert_eq!(parsed.id, doc.id);
240 assert_eq!(parsed.controller, doc.controller);
241 }
242}