Skip to main content

tap_wasm/
wasm_agent.rs

1use crate::util::js_to_tap_message;
2use js_sys::{Array, Object, Promise, Reflect};
3use std::sync::Arc;
4use tap_agent::agent::TapAgent;
5use tap_agent::{
6    did::DIDGenerationOptions,
7    message::SecurityMode,
8    message_packing::{PackOptions, UnpackOptions},
9    AgentConfig, AgentKeyManager, AgentKeyManagerBuilder, KeyType, Packable, Unpackable,
10};
11
12// Extension trait for TapAgent in WASM context
13trait WasmTapAgentExt {
14    // Get the key manager for this agent
15    fn agent_key_manager(&self) -> &Arc<AgentKeyManager>;
16}
17
18impl WasmTapAgentExt for TapAgent {
19    fn agent_key_manager(&self) -> &Arc<AgentKeyManager> {
20        // Use the public key_manager() method
21        self.key_manager()
22    }
23}
24use tap_msg::didcomm::PlainMessage;
25use wasm_bindgen::prelude::*;
26use wasm_bindgen_futures::future_to_promise;
27use web_sys::console;
28
29/// TAP Agent implementation for WASM bindings
30#[wasm_bindgen]
31#[derive(Clone)]
32pub struct WasmTapAgent {
33    /// The underlying TapAgent
34    agent: TapAgent,
35    /// Nickname for the agent
36    nickname: Option<String>,
37    /// Debug mode flag
38    debug: bool,
39    /// Store the private key directly for export (temporary fix)
40    private_key_hex: Option<String>,
41}
42
43#[wasm_bindgen]
44impl WasmTapAgent {
45    /// Creates a new agent from an existing private key
46    #[wasm_bindgen(js_name = fromPrivateKey)]
47    pub async fn from_private_key(
48        private_key_hex: String,
49        key_type_str: String,
50    ) -> Result<WasmTapAgent, JsValue> {
51        // Panic hook is set up in lib.rs start() function
52
53        #[cfg(any(
54            feature = "crypto-ed25519",
55            feature = "crypto-p256",
56            feature = "crypto-secp256k1"
57        ))]
58        {
59            // Convert hex string to bytes
60            let private_key_bytes = hex::decode(&private_key_hex)
61                .map_err(|e| JsValue::from_str(&format!("Invalid hex private key: {}", e)))?;
62
63            // Convert key type string to KeyType enum
64            let key_type = match key_type_str.as_str() {
65                #[cfg(feature = "crypto-ed25519")]
66                "Ed25519" => KeyType::Ed25519,
67                #[cfg(feature = "crypto-p256")]
68                "P256" => KeyType::P256,
69                #[cfg(feature = "crypto-secp256k1")]
70                "Secp256k1" => KeyType::Secp256k1,
71                _ => {
72                    return Err(JsValue::from_str(&format!(
73                        "Invalid or disabled key type: {}",
74                        key_type_str
75                    )))
76                }
77            };
78
79            // Create TapAgent from private key
80            let (agent, did) = TapAgent::from_private_key(&private_key_bytes, key_type, false)
81                .await
82                .map_err(|e| {
83                    JsValue::from_str(&format!("Failed to create agent from private key: {}", e))
84                })?;
85
86            if agent.config.debug {
87                console::log_1(&JsValue::from_str(&format!(
88                    "Created WASM TAP Agent from private key with DID: {}",
89                    did
90                )));
91            }
92
93            Ok(WasmTapAgent {
94                agent,
95                nickname: None,
96                debug: false,
97                private_key_hex: Some(private_key_hex.clone()),
98            })
99        }
100
101        #[cfg(not(any(
102            feature = "crypto-ed25519",
103            feature = "crypto-p256",
104            feature = "crypto-secp256k1"
105        )))]
106        {
107            Err(JsValue::from_str("No cryptographic features enabled"))
108        }
109    }
110
111    /// Creates a new agent with the specified configuration
112    #[wasm_bindgen(constructor)]
113    pub fn new(config: JsValue) -> std::result::Result<WasmTapAgent, JsValue> {
114        // Panic hook is set up in lib.rs start() function
115
116        let nickname =
117            if let Ok(nickname_prop) = Reflect::get(&config, &JsValue::from_str("nickname")) {
118                nickname_prop.as_string()
119            } else {
120                None
121            };
122
123        let debug = if let Ok(debug_prop) = Reflect::get(&config, &JsValue::from_str("debug")) {
124            debug_prop.is_truthy()
125        } else {
126            false
127        };
128
129        // Get the DID from config
130        let did_string = if let Ok(did_prop) = Reflect::get(&config, &JsValue::from_str("did")) {
131            did_prop.as_string()
132        } else {
133            None
134        };
135
136        // Create a key manager
137        let key_manager_builder = AgentKeyManagerBuilder::new();
138        let key_manager = match key_manager_builder.build() {
139            Ok(km) => km,
140            Err(e) => {
141                return Err(JsValue::from_str(&format!(
142                    "Failed to build key manager: {}",
143                    e
144                )))
145            }
146        };
147
148        let agent = if let Some(did) = did_string {
149            // Create a config with the provided DID
150            let agent_config = AgentConfig::new(did).with_debug(debug);
151
152            // Create the agent with the provided DID
153            TapAgent::new(agent_config, Arc::new(key_manager))
154        } else {
155            // Generate a new key and DID for WASM
156            let options = DIDGenerationOptions {
157                key_type: KeyType::Ed25519,
158            };
159            let generated_key = match key_manager.generate_key_without_save(options) {
160                Ok(key) => key,
161                Err(e) => return Err(JsValue::from_str(&format!("Failed to generate key: {}", e))),
162            };
163
164            // Add the key to the key manager
165            if let Err(e) = key_manager.add_key_without_save(&generated_key) {
166                return Err(JsValue::from_str(&format!("Failed to add key: {}", e)));
167            }
168
169            let agent_config = AgentConfig::new(generated_key.did.clone()).with_debug(debug);
170            TapAgent::new(agent_config, Arc::new(key_manager))
171        };
172
173        if debug {
174            console::log_1(&JsValue::from_str(&format!(
175                "Created WASM TAP Agent with DID: {}",
176                agent.config.agent_did
177            )));
178        }
179
180        Ok(WasmTapAgent {
181            agent,
182            nickname,
183            debug,
184            private_key_hex: None,
185        })
186    }
187
188    /// Gets the agent's DID
189    pub fn get_did(&self) -> String {
190        self.agent.config.agent_did.clone()
191    }
192
193    /// Gets the agent's nickname
194    pub fn nickname(&self) -> Option<String> {
195        self.nickname.clone()
196    }
197
198    /// Export the agent's private key as a hex string
199    #[wasm_bindgen(js_name = exportPrivateKey)]
200    pub fn export_private_key(&self) -> Result<String, JsValue> {
201        // If we have a stored private key (from from_private_key method), use it directly
202        if let Some(stored_key) = &self.private_key_hex {
203            return Ok(stored_key.clone());
204        }
205
206        // Use get_private_key() which checks both generated_keys and secrets
207        let key_manager = self.agent.agent_key_manager();
208        let did = &self.agent.config.agent_did;
209
210        let (private_key_bytes, _key_type) = key_manager
211            .get_private_key(did)
212            .map_err(|e| JsValue::from_str(&format!("Failed to get key for DID {}: {}", did, e)))?;
213
214        Ok(hex::encode(&private_key_bytes))
215    }
216
217    /// Export the agent's public key as a hex string
218    #[wasm_bindgen(js_name = exportPublicKey)]
219    pub fn export_public_key(&self) -> Result<String, JsValue> {
220        // Get the key manager from the agent
221        let key_manager = self.agent.agent_key_manager();
222
223        // Get the agent's DID
224        let did = &self.agent.config.agent_did;
225
226        // Get the generated key from the key manager
227        let generated_key = key_manager
228            .get_generated_key(did)
229            .map_err(|e| JsValue::from_str(&format!("Failed to get key for DID {}: {}", did, e)))?;
230
231        // Convert public key bytes to hex string
232        let hex_public_key = hex::encode(&generated_key.public_key);
233
234        Ok(hex_public_key)
235    }
236
237    /// Pack a message using this agent's keys for transmission
238    #[wasm_bindgen(js_name = packMessage)]
239    pub fn pack_message(&self, message_js: JsValue) -> Promise {
240        let agent = self.agent.clone();
241        let debug = self.debug;
242
243        future_to_promise(async move {
244            // Convert JS message to a TapMessageBody
245            let tap_message = match js_to_tap_message(&message_js) {
246                Ok(msg) => msg,
247                Err(e) => {
248                    return Err(JsValue::from_str(&format!(
249                        "Failed to convert JS message: {}",
250                        e
251                    )))
252                }
253            };
254
255            // Create pack options
256            let security_mode = SecurityMode::Signed; // Default to signed
257
258            // Get the actual key ID from the key manager instead of hardcoding #keys-1
259            let sender_kid = {
260                let key_manager = agent.agent_key_manager();
261                if let Ok(key) = key_manager.get_generated_key(&agent.config.agent_did) {
262                    // Use the first verification method ID from the DID document
263                    if let Some(vm) = key.did_doc.verification_method.first() {
264                        Some(vm.id.clone())
265                    } else {
266                        // Fallback to proper DID:key format
267                        if agent.config.agent_did.starts_with("did:key:") {
268                            let key_part = &agent.config.agent_did[8..]; // Skip "did:key:"
269                            Some(format!("{}#{}", agent.config.agent_did, key_part))
270                        } else {
271                            Some(format!("{}#keys-1", agent.config.agent_did))
272                        }
273                    }
274                } else {
275                    // Fallback to proper DID:key format
276                    if agent.config.agent_did.starts_with("did:key:") {
277                        let key_part = &agent.config.agent_did[8..]; // Skip "did:key:"
278                        Some(format!("{}#{}", agent.config.agent_did, key_part))
279                    } else {
280                        Some(format!("{}#keys-1", agent.config.agent_did))
281                    }
282                }
283            };
284            let recipient_kid = None; // Can be set from message if needed
285
286            let pack_options = PackOptions {
287                security_mode,
288                sender_kid,
289                recipient_kid,
290            };
291
292            // Pack the message
293            let key_manager = agent.agent_key_manager();
294
295            // Debug log the message we're about to pack
296            if debug {
297                console::log_1(&JsValue::from_str(&format!(
298                    "Packing message: id={}, type={}, from={}, to={:?}",
299                    tap_message.id, tap_message.type_, tap_message.from, tap_message.to
300                )));
301            }
302
303            let packed = match tap_message.pack(&**key_manager, pack_options).await {
304                Ok(packed_msg) => {
305                    if debug {
306                        console::log_1(&JsValue::from_str(&format!(
307                            "Packed message length: {}, preview: {}...",
308                            packed_msg.len(),
309                            &packed_msg.chars().take(50).collect::<String>()
310                        )));
311                    }
312                    packed_msg
313                }
314                Err(e) => {
315                    console::error_1(&JsValue::from_str(&format!("Pack error: {:?}", e)));
316                    return Err(JsValue::from_str(&format!("Failed to pack message: {}", e)));
317                }
318            };
319
320            if debug {
321                console::log_1(&JsValue::from_str(&format!(
322                    "✅ Message packed successfully for sender {}",
323                    agent.config.agent_did
324                )));
325            }
326
327            // Create a JS object to return with the packed message
328            let result = Object::new();
329            Reflect::set(
330                &result,
331                &JsValue::from_str("message"),
332                &JsValue::from_str(&packed),
333            )?;
334
335            // Add metadata
336            let metadata = Object::new();
337            Reflect::set(
338                &metadata,
339                &JsValue::from_str("type"),
340                &JsValue::from_str("signed"),
341            )?;
342            Reflect::set(
343                &metadata,
344                &JsValue::from_str("sender"),
345                &JsValue::from_str(&agent.config.agent_did),
346            )?;
347
348            Reflect::set(&result, &JsValue::from_str("metadata"), &metadata)?;
349
350            Ok(result.into())
351        })
352    }
353
354    /// Unpack a message received by this agent
355    #[wasm_bindgen(js_name = unpackMessage)]
356    pub fn unpack_message(&self, packed_message: &str, expected_type: Option<String>) -> Promise {
357        let agent = self.agent.clone();
358        let debug = self.debug;
359        let packed_message = packed_message.to_string(); // Clone the string to avoid lifetime issues
360
361        future_to_promise(async move {
362            // Create unpack options
363            // For signed messages, we don't expect a specific recipient
364            // For encrypted messages, we expect to be one of the recipients
365            let unpack_options = UnpackOptions {
366                expected_security_mode: SecurityMode::Any,
367                expected_recipient_kid: None, // Don't require specific recipient for signed messages
368                require_signature: false,
369            };
370
371            // Unpack the message
372            let key_manager = agent.agent_key_manager();
373            let plain_message: PlainMessage =
374                match String::unpack(&packed_message, &**key_manager, unpack_options).await {
375                    Ok(msg) => msg,
376                    Err(e) => {
377                        return Err(JsValue::from_str(&format!(
378                            "Failed to unpack message: {}",
379                            e
380                        )))
381                    }
382                };
383
384            if debug {
385                console::log_1(&JsValue::from_str(&format!(
386                    "✅ Message unpacked successfully for recipient {}",
387                    agent.config.agent_did
388                )));
389            }
390
391            // If an expected type was provided, validate it
392            if let Some(expected) = expected_type {
393                if plain_message.type_ != expected {
394                    return Err(JsValue::from_str(&format!(
395                        "Expected message type {} but got {}",
396                        expected, plain_message.type_
397                    )));
398                }
399            }
400
401            // Convert the unpacked message to a JS object
402            let result = Object::new();
403
404            // Add message ID
405            Reflect::set(
406                &result,
407                &JsValue::from_str("id"),
408                &JsValue::from_str(&plain_message.id),
409            )?;
410
411            // Add message type
412            Reflect::set(
413                &result,
414                &JsValue::from_str("type"),
415                &JsValue::from_str(&plain_message.type_),
416            )?;
417
418            // Add from and to
419            Reflect::set(
420                &result,
421                &JsValue::from_str("from"),
422                &JsValue::from_str(&plain_message.from),
423            )?;
424
425            let to_array = Array::new();
426            for to_did in &plain_message.to {
427                to_array.push(&JsValue::from_str(to_did));
428            }
429            Reflect::set(&result, &JsValue::from_str("to"), &to_array)?;
430
431            // Add body as a JS object
432            let body_str = serde_json::to_string(&plain_message.body)
433                .map_err(|e| JsValue::from_str(&format!("Failed to serialize body: {}", e)))?;
434
435            let body_js = js_sys::JSON::parse(&body_str)
436                .map_err(|e| JsValue::from_str(&format!("Failed to parse body: {:?}", e)))?;
437
438            Reflect::set(&result, &JsValue::from_str("body"), &body_js)?;
439
440            // Add created time if available
441            if let Some(created) = plain_message.created_time {
442                Reflect::set(
443                    &result,
444                    &JsValue::from_str("created"),
445                    &JsValue::from_f64(created as f64),
446                )?;
447            }
448
449            // Add expires time if available
450            if let Some(expires) = plain_message.expires_time {
451                Reflect::set(
452                    &result,
453                    &JsValue::from_str("expires"),
454                    &JsValue::from_f64(expires as f64),
455                )?;
456            }
457
458            // Add thread ID if available
459            if let Some(thid) = plain_message.thid {
460                Reflect::set(
461                    &result,
462                    &JsValue::from_str("thid"),
463                    &JsValue::from_str(&thid),
464                )?;
465            }
466
467            // Add parent thread ID if available
468            if let Some(pthid) = plain_message.pthid {
469                Reflect::set(
470                    &result,
471                    &JsValue::from_str("pthid"),
472                    &JsValue::from_str(&pthid),
473                )?;
474            }
475
476            Ok(result.into())
477        })
478    }
479}