forked from
smokesignal.events/atproto-plc
Rust and WASM did-method-plc tools and structures
1//! WASM bindings for did:plc operations
2//!
3//! This module is only compiled when both:
4//! - Building for wasm32 target (`target_arch = "wasm32"`)
5//! - The "wasm" feature is enabled (provides wasm_bindgen and related dependencies)
6
7#![cfg(all(target_arch = "wasm32", feature = "wasm"))]
8
9use crate::builder::DidBuilder as NativeDidBuilder;
10use crate::crypto::{SigningKey as NativeSigningKey, VerifyingKey as NativeVerifyingKey};
11use crate::did::Did as NativeDid;
12use crate::document::{DidDocument as NativeDidDocument, ServiceEndpoint as NativeServiceEndpoint};
13use crate::operations::Operation as NativeOperation;
14use serde::{Deserialize, Serialize};
15use wasm_bindgen::prelude::*;
16
17/// WASM wrapper for DID
18#[wasm_bindgen]
19pub struct WasmDid {
20 inner: NativeDid,
21}
22
23#[wasm_bindgen]
24impl WasmDid {
25 /// Parse and validate a DID string
26 ///
27 /// # Errors
28 ///
29 /// Throws a JavaScript error if the DID is invalid
30 #[wasm_bindgen(constructor)]
31 pub fn parse(did_string: &str) -> Result<WasmDid, JsValue> {
32 NativeDid::parse(did_string)
33 .map(|did| WasmDid { inner: did })
34 .map_err(|e| JsValue::from_str(&e.to_string()))
35 }
36
37 /// Check if this DID is valid
38 ///
39 /// Since DIDs can only be constructed through validation, this always returns true
40 #[wasm_bindgen(js_name = "isValid")]
41 pub fn is_valid(&self) -> bool {
42 self.inner.is_valid()
43 }
44
45 /// Get the 24-character identifier portion (without "did:plc:" prefix)
46 #[wasm_bindgen(getter)]
47 pub fn identifier(&self) -> String {
48 self.inner.identifier().to_string()
49 }
50
51 /// Get the full DID string including "did:plc:" prefix
52 #[wasm_bindgen(js_name = "toString")]
53 pub fn to_string_js(&self) -> String {
54 self.inner.to_string()
55 }
56
57 /// Get the full DID string as a getter
58 #[wasm_bindgen(getter)]
59 pub fn did(&self) -> String {
60 self.inner.to_string()
61 }
62}
63
64/// WASM wrapper for SigningKey
65#[wasm_bindgen]
66pub struct WasmSigningKey {
67 inner: NativeSigningKey,
68}
69
70#[wasm_bindgen]
71impl WasmSigningKey {
72 /// Generate a new P-256 key pair
73 #[wasm_bindgen(js_name = "generateP256")]
74 pub fn generate_p256() -> WasmSigningKey {
75 WasmSigningKey {
76 inner: NativeSigningKey::generate_p256(),
77 }
78 }
79
80 /// Generate a new secp256k1 key pair
81 #[wasm_bindgen(js_name = "generateK256")]
82 pub fn generate_k256() -> WasmSigningKey {
83 WasmSigningKey {
84 inner: NativeSigningKey::generate_k256(),
85 }
86 }
87
88 /// Convert this signing key to a did:key string
89 #[wasm_bindgen(js_name = "toDidKey")]
90 pub fn to_did_key(&self) -> String {
91 self.inner.to_did_key()
92 }
93}
94
95/// WASM wrapper for ServiceEndpoint
96#[wasm_bindgen]
97#[derive(Serialize, Deserialize)]
98pub struct WasmServiceEndpoint {
99 /// Service type (e.g., "AtprotoPersonalDataServer")
100 #[wasm_bindgen(skip)]
101 pub service_type: String,
102 /// Service endpoint URL
103 #[wasm_bindgen(skip)]
104 pub endpoint: String,
105}
106
107#[wasm_bindgen]
108impl WasmServiceEndpoint {
109 /// Create a new service endpoint
110 #[wasm_bindgen(constructor)]
111 pub fn new(service_type: String, endpoint: String) -> WasmServiceEndpoint {
112 WasmServiceEndpoint {
113 service_type,
114 endpoint,
115 }
116 }
117
118 /// Get the service type
119 #[wasm_bindgen(getter = serviceType)]
120 pub fn service_type(&self) -> String {
121 self.service_type.clone()
122 }
123
124 /// Get the endpoint URL
125 #[wasm_bindgen(getter)]
126 pub fn endpoint(&self) -> String {
127 self.endpoint.clone()
128 }
129}
130
131impl From<WasmServiceEndpoint> for NativeServiceEndpoint {
132 fn from(wasm: WasmServiceEndpoint) -> Self {
133 NativeServiceEndpoint::new(wasm.service_type, wasm.endpoint)
134 }
135}
136
137/// WASM wrapper for DidBuilder
138#[wasm_bindgen]
139pub struct WasmDidBuilder {
140 inner: NativeDidBuilder,
141}
142
143#[wasm_bindgen]
144impl WasmDidBuilder {
145 /// Create a new DID builder
146 #[wasm_bindgen(constructor)]
147 pub fn new() -> WasmDidBuilder {
148 WasmDidBuilder {
149 inner: NativeDidBuilder::new(),
150 }
151 }
152
153 /// Add a rotation key
154 #[wasm_bindgen(js_name = "addRotationKey")]
155 pub fn add_rotation_key(mut self, key: WasmSigningKey) -> WasmDidBuilder {
156 self.inner = self.inner.add_rotation_key(key.inner);
157 self
158 }
159
160 /// Add a verification method
161 #[wasm_bindgen(js_name = "addVerificationMethod")]
162 pub fn add_verification_method(mut self, name: String, key: WasmSigningKey) -> WasmDidBuilder {
163 self.inner = self.inner.add_verification_method(name, key.inner);
164 self
165 }
166
167 /// Add an also-known-as URI
168 #[wasm_bindgen(js_name = "addAlsoKnownAs")]
169 pub fn add_also_known_as(mut self, uri: String) -> WasmDidBuilder {
170 self.inner = self.inner.add_also_known_as(uri);
171 self
172 }
173
174 /// Add a service endpoint
175 #[wasm_bindgen(js_name = "addService")]
176 pub fn add_service(mut self, name: String, endpoint: WasmServiceEndpoint) -> WasmDidBuilder {
177 self.inner = self.inner.add_service(name, endpoint.into());
178 self
179 }
180
181 /// Build and sign the genesis operation
182 ///
183 /// Returns a JavaScript object with:
184 /// - did: The created DID string
185 /// - operation: The signed genesis operation as JSON
186 ///
187 /// # Errors
188 ///
189 /// Throws a JavaScript error if building fails
190 #[wasm_bindgen]
191 pub fn build(self) -> Result<JsValue, JsValue> {
192 let (did, operation, _keys) = self
193 .inner
194 .build()
195 .map_err(|e| JsValue::from_str(&e.to_string()))?;
196
197 // Create a result object
198 let result = js_sys::Object::new();
199
200 // Set the DID
201 js_sys::Reflect::set(
202 &result,
203 &JsValue::from_str("did"),
204 &JsValue::from_str(did.as_str()),
205 )
206 .map_err(|e| JsValue::from_str(&format!("Failed to set did: {:?}", e)))?;
207
208 // Serialize the operation to JSON
209 let operation_json = serde_json::to_string(&operation)
210 .map_err(|e| JsValue::from_str(&e.to_string()))?;
211
212 js_sys::Reflect::set(
213 &result,
214 &JsValue::from_str("operation"),
215 &JsValue::from_str(&operation_json),
216 )
217 .map_err(|e| JsValue::from_str(&format!("Failed to set operation: {:?}", e)))?;
218
219 Ok(result.into())
220 }
221}
222
223/// WASM wrapper for DID Document
224#[wasm_bindgen]
225pub struct WasmDidDocument {
226 inner: NativeDidDocument,
227}
228
229#[wasm_bindgen]
230impl WasmDidDocument {
231 /// Parse a DID document from JSON
232 #[wasm_bindgen(js_name = "fromJson")]
233 pub fn from_json(json: &str) -> Result<WasmDidDocument, JsValue> {
234 let doc: NativeDidDocument =
235 serde_json::from_str(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
236
237 Ok(WasmDidDocument { inner: doc })
238 }
239
240 /// Convert this DID document to JSON
241 #[wasm_bindgen(js_name = "toJson")]
242 pub fn to_json(&self) -> Result<String, JsValue> {
243 serde_json::to_string_pretty(&self.inner).map_err(|e| JsValue::from_str(&e.to_string()))
244 }
245
246 /// Get the DID this document describes
247 #[wasm_bindgen(getter)]
248 pub fn id(&self) -> String {
249 self.inner.id.to_string()
250 }
251
252 /// Validate this DID document
253 #[wasm_bindgen]
254 pub fn validate(&self) -> Result<(), JsValue> {
255 self.inner
256 .validate()
257 .map_err(|e| JsValue::from_str(&e.to_string()))
258 }
259}
260
261/// WASM wrapper for VerifyingKey
262#[wasm_bindgen]
263pub struct WasmVerifyingKey {
264 inner: NativeVerifyingKey,
265}
266
267#[wasm_bindgen]
268impl WasmVerifyingKey {
269 /// Create a verifying key from a did:key string
270 #[wasm_bindgen(js_name = "fromDidKey")]
271 pub fn from_did_key(did_key: &str) -> Result<WasmVerifyingKey, JsValue> {
272 NativeVerifyingKey::from_did_key(did_key)
273 .map(|key| WasmVerifyingKey { inner: key })
274 .map_err(|e| JsValue::from_str(&e.to_string()))
275 }
276
277 /// Convert this verifying key to a did:key string
278 #[wasm_bindgen(js_name = "toDidKey")]
279 pub fn to_did_key(&self) -> String {
280 self.inner.to_did_key()
281 }
282}
283
284/// WASM wrapper for Operation
285#[wasm_bindgen]
286pub struct WasmOperation {
287 inner: NativeOperation,
288}
289
290#[wasm_bindgen]
291impl WasmOperation {
292 /// Parse an operation from JSON
293 #[wasm_bindgen(js_name = "fromJson")]
294 pub fn from_json(json: &str) -> Result<WasmOperation, JsValue> {
295 let op: NativeOperation =
296 serde_json::from_str(json).map_err(|e| JsValue::from_str(&e.to_string()))?;
297
298 Ok(WasmOperation { inner: op })
299 }
300
301 /// Convert this operation to JSON
302 #[wasm_bindgen(js_name = "toJson")]
303 pub fn to_json(&self) -> Result<String, JsValue> {
304 serde_json::to_string_pretty(&self.inner).map_err(|e| JsValue::from_str(&e.to_string()))
305 }
306
307 /// Check if this is a genesis operation
308 #[wasm_bindgen(js_name = "isGenesis")]
309 pub fn is_genesis(&self) -> bool {
310 self.inner.is_genesis()
311 }
312
313 /// Get the CID of this operation
314 #[wasm_bindgen]
315 pub fn cid(&self) -> Result<String, JsValue> {
316 self.inner
317 .cid()
318 .map_err(|e| JsValue::from_str(&e.to_string()))
319 }
320
321 /// Get the prev field (CID of previous operation), or null for genesis
322 #[wasm_bindgen]
323 pub fn prev(&self) -> Option<String> {
324 self.inner.prev().map(|s| s.to_string())
325 }
326
327 /// Get the signature of this operation
328 #[wasm_bindgen]
329 pub fn signature(&self) -> String {
330 self.inner.signature().to_string()
331 }
332
333 /// Get the rotation keys from this operation (if any)
334 #[wasm_bindgen(js_name = "rotationKeys")]
335 pub fn rotation_keys(&self) -> Option<Vec<String>> {
336 self.inner.rotation_keys().map(|keys| keys.to_vec())
337 }
338
339 /// Verify this operation's signature using the provided rotation keys
340 ///
341 /// Returns true if the signature is valid with at least one of the keys
342 #[wasm_bindgen]
343 pub fn verify(&self, rotation_keys: Vec<WasmVerifyingKey>) -> Result<bool, JsValue> {
344 let native_keys: Vec<NativeVerifyingKey> =
345 rotation_keys.into_iter().map(|k| k.inner).collect();
346
347 match self.inner.verify(&native_keys) {
348 Ok(_) => Ok(true),
349 Err(_) => Ok(false),
350 }
351 }
352
353 /// Verify this operation and return which key index verified it (0-based)
354 ///
355 /// Returns the index of the rotation key that verified the signature,
356 /// or throws an error if none verified
357 #[wasm_bindgen(js_name = "verifyWithKeyIndex")]
358 pub fn verify_with_key_index(&self, rotation_keys: Vec<WasmVerifyingKey>) -> Result<usize, JsValue> {
359 for (i, key) in rotation_keys.iter().enumerate() {
360 if self.inner.verify(&[key.inner]).is_ok() {
361 return Ok(i);
362 }
363 }
364 Err(JsValue::from_str("No rotation key verified the signature"))
365 }
366}
367
368/// Initialize the WASM module
369///
370/// This should be called before using any other functions
371#[wasm_bindgen(start)]
372pub fn init() {
373 // WASM module initialization
374 // For better panic messages in development, consider adding console_error_panic_hook
375}