1extern crate thiserror;
2
3use async_trait::async_trait;
4use didkit::{
5 DIDMethod, DIDResolver, Document, DocumentMetadata, ResolutionInputMetadata,
6 ResolutionMetadata,
7};
8use operation::{PLCOperation, Service, SignedOperation, SignedPLCOperation, UnsignedPLCOperation};
9use util::op_from_json;
10
11mod audit;
12mod error;
13mod keypair;
14mod multicodec;
15mod op_builder;
16pub mod operation;
17mod util;
18
19pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
20pub const DEFAULT_HOST: &str = "https://plc.directory";
21
22pub use audit::{AuditLog, DIDAuditLogs};
23pub use error::PLCError;
24pub use keypair::{BlessedAlgorithm, Keypair};
25pub use op_builder::OperationBuilder;
26
27pub struct PLCOperationResult {
28 pub did: String,
29 pub status: u16,
30 pub body: String,
31}
32
33pub struct DIDPLC {
37 host: String,
38 client: reqwest::Client,
39}
40
41impl DIDPLC {
42 pub fn new(host: &str) -> Self {
43 let client = reqwest::Client::builder()
44 .user_agent(USER_AGENT)
45 .build()
46 .unwrap();
47
48 Self {
49 host: host.to_string(),
50 client,
51 }
52 }
53
54 pub async fn execute_op(&self, did: &str, op: &SignedPLCOperation) -> Result<PLCOperationResult, PLCError> {
55 let res = self
56 .client
57 .post(format!("{}/{}", self.host, did))
58 .header(reqwest::header::CONTENT_TYPE, "application/json")
59 .body(op.to_json())
60 .send()
61 .await?;
62
63 let status = res.status().as_u16();
64 let body: String = res.text().await?;
65 Ok(PLCOperationResult {
66 did: did.to_string(),
67 status: status,
68 body,
69 })
70 }
71
72 pub async fn get_log(&self, did: &str) -> Result<Vec<PLCOperation>, PLCError> {
73 let res = self
74 .client
75 .get(format!("{}/{}/log", self.host, did))
76 .send()
77 .await?;
78
79 let body: String = res.text().await?;
80 let mut operations: Vec<PLCOperation> = vec![];
81 let json: Vec<serde_json::Value> =
82 serde_json::from_str(&body).map_err(|e| PLCError::Other(e.into()))?;
83
84 for op in json {
85 operations.push(
86 op_from_json(
87 serde_json::to_string(&op)
88 .map_err(|e| PLCError::Other(e.into()))?
89 .as_str(),
90 )
91 .map_err(|e| PLCError::Other(e.into()))?,
92 );
93 }
94
95 Ok(operations)
96 }
97
98 pub async fn get_audit_log(&self, did: &str) -> Result<DIDAuditLogs, PLCError> {
99 let res = self
100 .client
101 .get(format!("{}/{}/log/audit", self.host, did))
102 .send()
103 .await?;
104
105 if !res.status().is_success() {
106 return Err(PLCError::Http(
107 res.status().as_u16(),
108 res.text().await.unwrap_or_default(),
109 ));
110 }
111
112 let body: String = res.text().await?;
113
114 Ok(DIDAuditLogs::from_json(&body).map_err(|e| PLCError::Other(e.into()))?)
115 }
116
117 pub async fn get_last_log(&self, did: &str) -> Result<PLCOperation, PLCError> {
118 let res = self
119 .client
120 .get(format!("{}/{}/log/last", self.host, did))
121 .send()
122 .await?;
123
124 let body: String = res.text().await?;
125 let op: serde_json::Value =
126 serde_json::from_str(&body).map_err(|e| PLCError::Other(e.into()))?;
127
128 Ok(op_from_json(
129 serde_json::to_string(&op)
130 .map_err(|e| PLCError::Other(e.into()))?
131 .as_str(),
132 )?)
133 }
134
135 pub async fn get_current_state(&self, did: &str) -> Result<PLCOperation, PLCError> {
136 let res = self
137 .client
138 .get(format!("{}/{}/data", self.host, did))
139 .send()
140 .await?;
141
142 let body: String = res.text().await?;
143
144 Ok(PLCOperation::UnsignedPLC(serde_json::from_str::<UnsignedPLCOperation>(&body)
145 .map_err(|e| PLCError::Other(e.into()))?
146 ))
147 }
148}
149
150impl Default for DIDPLC {
151 fn default() -> Self {
152 Self::new(DEFAULT_HOST)
153 }
154}
155
156#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
157#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
158impl DIDMethod for DIDPLC {
159 fn name(&self) -> &'static str {
160 "did:plc"
161 }
162
163 fn to_resolver(&self) -> &dyn DIDResolver {
164 self
165 }
166}
167
168#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
169#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
170impl DIDResolver for DIDPLC {
171 async fn resolve(
172 &self,
173 did: &str,
174 _input_metadata: &ResolutionInputMetadata,
175 ) -> (
176 ResolutionMetadata,
177 Option<Document>,
178 Option<DocumentMetadata>,
179 ) {
180 let res = match self
181 .client
182 .get(format!("{}/{}", self.host, did))
183 .send()
184 .await
185 {
186 Ok(res) => res,
187 Err(err) => {
188 return (
189 ResolutionMetadata::from_error(&format!("Failed to get URL: {:?}", err)),
190 None,
191 None,
192 )
193 }
194 };
195
196 match res.status().as_u16() {
197 200 => {
198 let text = match res.text().await {
199 Ok(json) => json,
200 Err(err) => {
201 return (
202 ResolutionMetadata::from_error(&format!(
203 "Failed to parse JSON response: {:?}",
204 err
205 )),
206 None,
207 None,
208 )
209 }
210 };
211
212 match Document::from_json(text.as_str()) {
213 Ok(document) => (ResolutionMetadata::default(), Some(document), None),
214 Err(err) => (
215 ResolutionMetadata::from_error(&format!(
216 "Unable to parse DID document: {:?}",
217 err
218 )),
219 None,
220 None,
221 ),
222 }
223 }
224 404 => (
225 ResolutionMetadata::from_error(&format!("DID not found: {}", did)),
226 None,
227 None,
228 ),
229 _ => (
230 ResolutionMetadata::from_error(&format!("Failed to resolve DID: {}", res.status())),
231 None,
232 None,
233 ),
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use operation::PLCOperationType;
241
242 use super::*;
243
244 const PLC_HOST: &str = "https://plc.directory"; #[actix_rt::test]
247 async fn test_didplc_resolve() {
248 let didplc = DIDPLC::default();
249 let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
250 let (res_metadata, document, _) = didplc
251 .resolve(did, &ResolutionInputMetadata::default())
252 .await;
253
254 assert!(res_metadata.error.is_none());
255 assert!(document.is_some());
256 }
257
258 #[actix_rt::test]
259 async fn test_didplc_get_log() {
260 let didplc = DIDPLC::default();
261 let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
262 let log = didplc.get_log(did).await;
263
264 assert!(log.is_ok());
265 assert!(log.unwrap().len() > 0);
266 }
267
268 #[actix_rt::test]
269 async fn test_didplc_get_audit_log() {
270 let didplc = DIDPLC::default();
271 let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
272 let log = didplc.get_audit_log(did).await;
273
274 assert!(log.is_ok());
275 assert!(log.unwrap().len() > 0);
276 }
277
278 #[actix_rt::test]
279 async fn test_didplc_get_last_log() {
280 let didplc = DIDPLC::default();
281 let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
282 let log = didplc.get_last_log(did).await;
283
284 assert!(log.is_ok());
285 }
286
287 #[actix_rt::test]
288 async fn test_didplc_get_current_state() {
289 let didplc = DIDPLC::default();
290 let did = "did:plc:ui5pgpumwvufhfnnz52c4lyl";
291 let log = didplc.get_current_state(did).await;
292
293 assert!(log.is_ok());
294 }
295
296 #[actix_rt::test]
297 async fn test_didplc_operations() {
298 let didplc = DIDPLC::new(PLC_HOST);
299 let recovery_key = Keypair::generate(BlessedAlgorithm::P256);
300 let signing_key = Keypair::generate(BlessedAlgorithm::P256);
301 let verification_key = Keypair::generate(BlessedAlgorithm::P256);
302
303 let create_op = OperationBuilder::new(&didplc)
304 .with_key(&signing_key)
305 .with_validation_key(&verification_key)
306 .add_rotation_key(&recovery_key)
307 .add_rotation_key(&signing_key)
308 .with_handle("example.test".to_owned())
309 .with_pds("example.test".to_owned())
310 .build(PLCOperationType::Operation)
311 .await;
312
313 assert!(create_op.is_ok(), "Failed to build create op: {:?}", create_op.err());
314 let create_op = create_op.unwrap();
315 let did = &create_op.to_did().expect("Failed to turn op to DID");
316
317 let create_res = didplc.execute_op(did, &create_op).await;
318
319 assert!(create_res.is_ok(), "Failed to execute create op: {:?}", create_res.err());
320 let create_res = create_res.unwrap();
321
322 assert!(create_res.status == 200, "Failed to execute create op: status = {}, body = {:?}", create_res.status, create_res.body);
323 assert!(&create_res.did == did, "Failed to execute create op: did = {}, expected = {}", create_res.did, did);
324
325 let update_op = OperationBuilder::for_did(&didplc, did.clone())
326 .with_key(&signing_key)
327 .with_validation_key(&verification_key)
328 .add_rotation_key(&recovery_key)
329 .add_rotation_key(&signing_key)
330 .with_handle("touma.example.test".to_owned())
331 .with_pds("example.test".to_owned())
332 .build(PLCOperationType::Operation)
333 .await;
334
335 assert!(update_op.is_ok(), "Failed to build update op: {:?}", update_op.err());
336 let update_op = update_op.unwrap();
337 let update_res = didplc.execute_op(did, &update_op).await;
338 assert!(update_res.is_ok(), "Failed to execute update op: {:?}", update_res.err());
339
340 let update_res = update_res.unwrap();
341 assert!(update_res.status == 200, "Failed to execute update op: status = {}, body = {:?}, json = {}", update_res.status, update_res.body, update_op.to_json());
342 assert!(&update_res.did == did, "Failed to execute update op: did = {}, expected = {}", update_res.did, did);
343
344 let deactivate_op = OperationBuilder::for_did(&didplc, did.clone())
345 .with_key(&signing_key)
346 .with_validation_key(&verification_key)
347 .add_rotation_key(&recovery_key)
348 .add_rotation_key(&signing_key)
349 .with_handle("touma.example.test".to_owned())
350 .with_pds("example.test".to_owned())
351 .build(PLCOperationType::Tombstone)
352 .await;
353 assert!(deactivate_op.is_ok(), "Failed to build deactivate op: {:?}", deactivate_op.err());
354 let deactivate_op = deactivate_op.unwrap();
355 let deactivate_res = didplc.execute_op(did, &deactivate_op).await;
356 assert!(deactivate_res.is_ok(), "Failed to execute deactivate op: {:?}, json = {}", deactivate_res.err(), deactivate_op.to_json());
357
358 let deactivate_res = deactivate_res.unwrap();
359 assert!(deactivate_res.status == 200, "Failed to execute deactivate op: status = {}, body = {:?}", deactivate_res.status, deactivate_res.body);
360 assert!(&deactivate_res.did == did, "Failed to execute deactivate op: did = {}, expected = {}", deactivate_res.did, did);
361
362 let recover_op = OperationBuilder::for_did(&didplc, did.clone())
363 .with_key(&recovery_key)
364 .with_validation_key(&verification_key)
365 .add_rotation_key(&recovery_key)
366 .add_rotation_key(&signing_key)
367 .with_handle("touma.example.test".to_owned())
368 .with_pds("example.test".to_owned())
369 .build(PLCOperationType::Operation)
370 .await;
371 assert!(recover_op.is_ok(), "Failed to build recover op: {:?}", recover_op.err());
372 let recover_op = recover_op.unwrap();
373 let recover_res = didplc.execute_op(did, &recover_op).await;
374 assert!(recover_res.is_ok(), "Failed to execute recover op: {:?}, json = {}", recover_res.err(), recover_op.to_json());
375
376 let recover_res = recover_res.unwrap();
377 assert!(recover_res.status == 200, "Failed to execute recover op: status = {}, body = {:?}", recover_res.status, recover_res.body);
378 assert!(&recover_res.did == did, "Failed to execute recover op: did = {}, expected = {}", recover_res.did, did);
379 }
380}