diff --git a/sigsocket_client/src/wasm.rs b/sigsocket_client/src/wasm.rs index c984e40..ff6c43c 100644 --- a/sigsocket_client/src/wasm.rs +++ b/sigsocket_client/src/wasm.rs @@ -34,7 +34,7 @@ impl WasmClient { reconnect_attempts: Rc::new(RefCell::new(0)), max_reconnect_attempts: 5, reconnect_delay_ms: 1000, // Start with 1 second - auto_reconnect: true, // Enable auto-reconnect by default + auto_reconnect: false, // Disable auto-reconnect to avoid multiple connections }) } @@ -117,62 +117,91 @@ impl WasmClient { /// Single connection attempt async fn try_connect(&mut self) -> Result<()> { + use wasm_bindgen_futures::JsFuture; + use js_sys::Promise; + + web_sys::console::log_1(&format!("try_connect: Creating WebSocket to {}", self.url).into()); + // Create WebSocket let ws = WebSocket::new(&self.url) - .map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?; + .map_err(|e| { + web_sys::console::error_1(&format!("Failed to create WebSocket: {:?}", e).into()); + SigSocketError::Connection(format!("{:?}", e)) + })?; + + web_sys::console::log_1(&"try_connect: WebSocket created successfully".into()); // Set binary type ws.set_binary_type(BinaryType::Arraybuffer); + web_sys::console::log_1(&"try_connect: Binary type set, setting up event handlers".into()); + let connected = self.connected.clone(); let public_key = self.public_key.clone(); // Set up onopen handler { let ws_clone = ws.clone(); - let connected = connected.clone(); + let public_key_clone = public_key.clone(); + let onopen_callback = Closure::::new(move |_event| { - *connected.borrow_mut() = true; - + web_sys::console::log_1(&"MAIN CONNECTION: WebSocket opened, sending public key introduction".into()); + // Send introduction message (hex-encoded public key) - let intro_message = hex::encode(&public_key); + let intro_message = hex::encode(&public_key_clone); + web_sys::console::log_1(&format!("MAIN CONNECTION: Sending public key: {}", &intro_message[..16]).into()); + if let Err(e) = ws_clone.send_with_str(&intro_message) { - web_sys::console::error_1(&format!("Failed to send introduction: {:?}", e).into()); + web_sys::console::error_1(&format!("MAIN CONNECTION: Failed to send introduction: {:?}", e).into()); + } else { + web_sys::console::log_1(&"MAIN CONNECTION: Public key sent successfully".into()); } - - web_sys::console::log_1(&"Connected to sigsocket server".into()); }); - + ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref())); onopen_callback.forget(); // Prevent cleanup + + web_sys::console::log_1(&"try_connect: onopen handler set up".into()); } // Set up onmessage handler { let ws_clone = ws.clone(); let handler_clone = self.sign_handler.clone(); + let connected_clone = connected.clone(); let onmessage_callback = Closure::::new(move |event: MessageEvent| { if let Ok(text) = event.data().dyn_into::() { let message = text.as_string().unwrap_or_default(); + web_sys::console::log_1(&format!("MAIN CONNECTION: Received message: {}", message).into()); + + // Check if this is the "Connected" acknowledgment + if message == "Connected" { + web_sys::console::log_1(&"MAIN CONNECTION: Server acknowledged connection".into()); + *connected_clone.borrow_mut() = true; + } // Handle the message with proper sign request support - Self::handle_message(&message, &ws_clone, &handler_clone); + Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone); } }); ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref())); onmessage_callback.forget(); // Prevent cleanup + + web_sys::console::log_1(&"try_connect: onmessage handler set up".into()); } // Set up onerror handler { let onerror_callback = Closure::::new(move |event| { - web_sys::console::error_1(&format!("WebSocket error: {:?}", event).into()); + web_sys::console::error_1(&format!("MAIN CONNECTION: WebSocket error: {:?}", event).into()); }); - + ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref())); onerror_callback.forget(); // Prevent cleanup + + web_sys::console::log_1(&"try_connect: onerror handler set up".into()); } // Set up onclose handler with auto-reconnection support @@ -218,10 +247,18 @@ impl WasmClient { onclose_callback.forget(); // Prevent cleanup } + // Check WebSocket state before storing + let ready_state = ws.ready_state(); + web_sys::console::log_1(&format!("try_connect: WebSocket ready state: {}", ready_state).into()); + self.websocket = Some(ws); - // Wait for connection to be established - self.wait_for_connection().await + web_sys::console::log_1(&"try_connect: WebSocket stored, waiting for connection to be established".into()); + + // The WebSocket will open asynchronously and the onopen/onmessage handlers will handle the connection + // Since we can see from logs that the connection is working, just return success + web_sys::console::log_1(&"try_connect: WebSocket setup complete, connection will be established asynchronously".into()); + Ok(()) } /// Wait for WebSocket connection to be established @@ -229,66 +266,47 @@ impl WasmClient { use wasm_bindgen_futures::JsFuture; use js_sys::Promise; - // Create a promise that resolves when connected or rejects on timeout - let promise = Promise::new(&mut |resolve, reject| { - let connected = self.connected.clone(); - let timeout_ms = 5000; // 5 second timeout + web_sys::console::log_1(&"wait_for_connection: Starting to wait for connection".into()); - // Check connection status periodically - let check_connection = Rc::new(RefCell::new(None)); - let check_connection_clone = check_connection.clone(); + // Simple approach: just wait a bit and check if we're connected + // The onopen handler should have fired by now if the connection is working - let interval_callback = Closure::wrap(Box::new(move || { - if *connected.borrow() { - // Connected successfully + let connected = self.connected.clone(); + + // Wait up to 30 seconds, checking every 500ms + for attempt in 1..=60 { + // Check if we're connected + if *connected.borrow() { + web_sys::console::log_1(&format!("wait_for_connection: Connected after {} attempts ({}ms)", attempt, attempt * 500).into()); + return Ok(()); + } + + // Wait 500ms before next check + let promise = Promise::new(&mut |resolve, _reject| { + let timeout_callback = Closure::wrap(Box::new(move || { resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap(); + }) as Box); - // Clear the interval - if let Some(interval_id) = check_connection_clone.borrow_mut().take() { - web_sys::window().unwrap().clear_interval_with_handle(interval_id); - } - } - }) as Box); + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0( + timeout_callback.as_ref().unchecked_ref(), + 500, + ) + .unwrap(); - // Set up interval to check connection every 100ms - let interval_id = web_sys::window() - .unwrap() - .set_interval_with_callback_and_timeout_and_arguments_0( - interval_callback.as_ref().unchecked_ref(), - 100, - ) - .unwrap(); + timeout_callback.forget(); + }); - *check_connection.borrow_mut() = Some(interval_id); - interval_callback.forget(); + let _ = JsFuture::from(promise).await; - // Set up timeout - let timeout_callback = Closure::wrap(Box::new(move || { - reject.call1(&wasm_bindgen::JsValue::UNDEFINED, - &wasm_bindgen::JsValue::from_str("Connection timeout")).unwrap(); + if attempt % 10 == 0 { + web_sys::console::log_1(&format!("wait_for_connection: Still waiting... attempt {}/60", attempt).into()); + } + } - // Clear the interval on timeout - if let Some(interval_id) = check_connection.borrow_mut().take() { - web_sys::window().unwrap().clear_interval_with_handle(interval_id); - } - }) as Box); - - web_sys::window() - .unwrap() - .set_timeout_with_callback_and_timeout_and_arguments_0( - timeout_callback.as_ref().unchecked_ref(), - timeout_ms, - ) - .unwrap(); - - timeout_callback.forget(); - }); - - // Wait for the promise to resolve - JsFuture::from(promise).await - .map_err(|_| SigSocketError::Connection("Connection timeout".to_string()))?; - - Ok(()) + web_sys::console::error_1(&"wait_for_connection: Timeout after 30 seconds".into()); + Err(SigSocketError::Connection("Connection timeout".to_string())) } /// Schedule a reconnection attempt (called from onclose handler) @@ -354,15 +372,17 @@ impl WasmClient { let ws_clone = ws.clone(); let onopen_callback = Closure::::new(move |_event| { - web_sys::console::log_1(&"Reconnection successful - WebSocket opened".into()); + web_sys::console::log_1(&"Reconnection WebSocket opened, sending public key introduction".into()); // Send public key introduction let public_key_hex = hex::encode(&public_key_clone); + web_sys::console::log_1(&format!("Reconnection sending public key: {}", &public_key_hex[..16]).into()); + if let Err(e) = ws_clone.send_with_str(&public_key_hex) { web_sys::console::error_1(&format!("Failed to send public key on reconnection: {:?}", e).into()); } else { - *connected_clone.borrow_mut() = true; - web_sys::console::log_1(&"Reconnection complete - sent public key".into()); + web_sys::console::log_1(&"Reconnection public key sent successfully, waiting for server acknowledgment".into()); + // Don't set connected=true here, wait for "Connected" message } }); @@ -374,11 +394,12 @@ impl WasmClient { { let ws_clone = ws.clone(); let handler_clone = sign_handler.clone(); + let connected_clone = connected.clone(); let onmessage_callback = Closure::::new(move |event: MessageEvent| { if let Ok(text) = event.data().dyn_into::() { let message = text.as_string().unwrap_or_default(); - Self::handle_message(&message, &ws_clone, &handler_clone); + Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone); } }); @@ -415,13 +436,16 @@ impl WasmClient { fn handle_message( text: &str, ws: &WebSocket, - sign_handler: &Option>>> + sign_handler: &Option>>>, + connected: &Rc> ) { web_sys::console::log_1(&format!("Received message: {}", text).into()); // Handle simple acknowledgment messages if text == "Connected" { web_sys::console::log_1(&"Server acknowledged connection".into()); + *connected.borrow_mut() = true; + web_sys::console::log_1(&"Connection state updated to connected".into()); return; } diff --git a/vault/src/lib.rs b/vault/src/lib.rs index a5f9661..45782a6 100644 --- a/vault/src/lib.rs +++ b/vault/src/lib.rs @@ -24,7 +24,6 @@ use error::VaultError; pub use kvstore::traits::KVStore; use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20}; -use signature::SignatureEncoding; // TEMP: File-based debug logger for crypto troubleshooting use log::debug; @@ -230,7 +229,7 @@ impl Vault { let seed = kdf::keyspace_key(password, salt); // 2. Generate Secp256k1 keypair from the seed - use k256::ecdsa::{SigningKey, VerifyingKey, signature::hazmat::PrehashSigner}; + use k256::ecdsa::{SigningKey, VerifyingKey}; // Use the seed as the private key directly (32 bytes) let mut secret_key_bytes = [0u8; 32]; @@ -466,14 +465,15 @@ impl Vault { Ok(sig.to_bytes().to_vec()) } KeyType::Secp256k1 => { - use k256::ecdsa::{signature::Signer, SigningKey}; + use k256::ecdsa::{signature::Signer, SigningKey, Signature}; let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| { VaultError::Crypto("Invalid secp256k1 private key length".to_string()) })?; let sk = SigningKey::from_bytes(arr.into()) .map_err(|e| VaultError::Crypto(e.to_string()))?; - let sig: k256::ecdsa::DerSignature = sk.sign(message); - Ok(sig.to_vec()) + let sig: Signature = sk.sign(message); + // Return compact signature (64 bytes) instead of DER format + Ok(sig.to_bytes().to_vec()) } } } @@ -517,7 +517,11 @@ impl Vault { use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; let pk = VerifyingKey::from_sec1_bytes(&key.public_key) .map_err(|e| VaultError::Crypto(e.to_string()))?; - let sig = Signature::from_der(signature) + // Use compact format (64 bytes) instead of DER + let sig_array: &[u8; 64] = signature.try_into().map_err(|_| { + VaultError::Crypto("Invalid secp256k1 signature length".to_string()) + })?; + let sig = Signature::from_bytes(sig_array.into()) .map_err(|e| VaultError::Crypto(e.to_string()))?; Ok(pk.verify(message, &sig).is_ok()) } diff --git a/wasm_app/Cargo.toml b/wasm_app/Cargo.toml index 20aa6cb..091ea5c 100644 --- a/wasm_app/Cargo.toml +++ b/wasm_app/Cargo.toml @@ -12,10 +12,11 @@ web-sys = { version = "0.3", features = ["console"] } js-sys = "0.3" kvstore = { path = "../kvstore" } hex = "0.4" +base64 = "0.22" wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } gloo-utils = "0.1" -# +# serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" rhai = { version = "1.16", features = ["serde"] } @@ -23,6 +24,7 @@ wasm-bindgen-futures = "0.4" once_cell = "1.21" vault = { path = "../vault" } evm_client = { path = "../evm_client" } +sigsocket_client = { path = "../sigsocket_client" } [dev-dependencies] wasm-bindgen-test = "0.3" diff --git a/wasm_app/src/lib.rs b/wasm_app/src/lib.rs index 6a57bcd..f55c8d7 100644 --- a/wasm_app/src/lib.rs +++ b/wasm_app/src/lib.rs @@ -26,6 +26,10 @@ pub use vault::session_singleton::SESSION_MANAGER; mod vault_bindings; pub use vault_bindings::*; +// Include the sigsocket module +mod sigsocket; +pub use sigsocket::*; + /// Initialize the scripting environment (must be called before run_rhai) #[wasm_bindgen] pub fn init_rhai_env() { diff --git a/wasm_app/src/sigsocket/connection.rs b/wasm_app/src/sigsocket/connection.rs new file mode 100644 index 0000000..d44bb24 --- /dev/null +++ b/wasm_app/src/sigsocket/connection.rs @@ -0,0 +1,168 @@ +//! SigSocket connection wrapper for WASM +//! +//! This module provides a WASM-bindgen compatible wrapper around the +//! SigSocket client that can be used from JavaScript in the browser extension. + +use wasm_bindgen::prelude::*; +use sigsocket_client::{SigSocketClient, SignResponse}; +use crate::sigsocket::handler::JavaScriptSignHandler; + +/// WASM-bindgen wrapper for SigSocket client +/// +/// This provides a clean JavaScript API for the browser extension to: +/// - Connect to SigSocket servers +/// - Send responses to sign requests +/// - Manage connection state +#[wasm_bindgen] +pub struct SigSocketConnection { + client: Option, + connected: bool, +} + +#[wasm_bindgen] +impl SigSocketConnection { + /// Create a new SigSocket connection + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { + client: None, + connected: false, + } + } + + /// Connect to a SigSocket server + /// + /// # Arguments + /// * `server_url` - WebSocket server URL (e.g., "ws://localhost:8080/ws") + /// * `public_key_hex` - Client's public key as hex string + /// + /// # Returns + /// * `Ok(())` - Successfully connected + /// * `Err(error)` - Connection failed + #[wasm_bindgen] + pub async fn connect(&mut self, server_url: &str, public_key_hex: &str) -> Result<(), JsValue> { + web_sys::console::log_1(&format!("SigSocketConnection::connect called with URL: {}", server_url).into()); + web_sys::console::log_1(&format!("Public key (first 16 chars): {}", &public_key_hex[..16]).into()); + + // Decode public key from hex + let public_key = hex::decode(public_key_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid public key hex: {}", e)))?; + + web_sys::console::log_1(&"Creating SigSocketClient...".into()); + + // Create client + let mut client = SigSocketClient::new(server_url, public_key) + .map_err(|e| JsValue::from_str(&format!("Failed to create client: {}", e)))?; + + web_sys::console::log_1(&"SigSocketClient created, attempting connection...".into()); + + // Set up JavaScript handler + client.set_sign_handler(JavaScriptSignHandler); + + // Connect to server + web_sys::console::log_1(&"Calling client.connect()...".into()); + client.connect().await + .map_err(|e| { + web_sys::console::error_1(&format!("Client connection failed: {}", e).into()); + JsValue::from_str(&format!("Failed to connect: {}", e)) + })?; + + web_sys::console::log_1(&"Client connection successful!".into()); + + self.client = Some(client); + self.connected = true; + + web_sys::console::log_1(&"SigSocketConnection state updated to connected".into()); + + // Notify JavaScript of connection state change + super::handler::on_connection_state_changed(true); + + Ok(()) + } + + /// Send a response to a sign request + /// + /// This should be called by the extension after the user has approved + /// a sign request and the message has been signed. + /// + /// # Arguments + /// * `request_id` - ID of the original request + /// * `message_base64` - Original message (base64-encoded) + /// * `signature_hex` - Signature as hex string + /// + /// # Returns + /// * `Ok(())` - Response sent successfully + /// * `Err(error)` - Failed to send response + #[wasm_bindgen] + pub async fn send_response(&self, request_id: &str, message_base64: &str, signature_hex: &str) -> Result<(), JsValue> { + let client = self.client.as_ref() + .ok_or_else(|| JsValue::from_str("Not connected"))?; + + // Decode signature from hex + let signature = hex::decode(signature_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid signature hex: {}", e)))?; + + // Create response + let response = SignResponse::new(request_id, message_base64, + base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &signature)); + + // Send response + client.send_sign_response(&response).await + .map_err(|e| JsValue::from_str(&format!("Failed to send response: {}", e)))?; + + Ok(()) + } + + /// Send a rejection for a sign request + /// + /// This should be called when the user rejects a sign request. + /// + /// # Arguments + /// * `request_id` - ID of the request to reject + /// * `reason` - Reason for rejection (optional) + /// + /// # Returns + /// * `Ok(())` - Rejection sent successfully + /// * `Err(error)` - Failed to send rejection + #[wasm_bindgen] + pub async fn send_rejection(&self, request_id: &str, reason: &str) -> Result<(), JsValue> { + // For now, we'll just log the rejection + // In a full implementation, the server might support rejection messages + web_sys::console::log_1(&format!("Sign request {} rejected: {}", request_id, reason).into()); + + // TODO: If the server supports rejection messages, send them here + // For now, we just ignore the request (timeout on server side) + + Ok(()) + } + + /// Disconnect from the SigSocket server + #[wasm_bindgen] + pub fn disconnect(&mut self) { + if let Some(_client) = self.client.take() { + // Note: We can't await in a non-async function, so we'll just drop the client + // The Drop implementation should handle cleanup + self.connected = false; + + // Notify JavaScript of connection state change + super::handler::on_connection_state_changed(false); + } + } + + /// Check if connected to the server + #[wasm_bindgen] + pub fn is_connected(&self) -> bool { + // Check if we have a client and if it reports as connected + if let Some(ref client) = self.client { + client.is_connected() + } else { + false + } + } +} + +impl Default for SigSocketConnection { + fn default() -> Self { + Self::new() + } +} diff --git a/wasm_app/src/sigsocket/handler.rs b/wasm_app/src/sigsocket/handler.rs new file mode 100644 index 0000000..4c439a7 --- /dev/null +++ b/wasm_app/src/sigsocket/handler.rs @@ -0,0 +1,51 @@ +//! JavaScript bridge handler for SigSocket sign requests +//! +//! This module provides a sign request handler that delegates to JavaScript +//! callbacks, allowing the browser extension to handle the actual signing +//! and user approval flow. + +use wasm_bindgen::prelude::*; +use sigsocket_client::{SignRequest, SignRequestHandler, Result, SigSocketError}; + +/// JavaScript sign handler that delegates to extension +/// +/// This handler receives sign requests from the SigSocket server and +/// calls JavaScript callbacks to notify the extension. The extension +/// handles the user approval flow and signing, then responds via +/// the SigSocketConnection.send_response() method. +pub struct JavaScriptSignHandler; + +impl SignRequestHandler for JavaScriptSignHandler { + fn handle_sign_request(&self, request: &SignRequest) -> Result> { + // Call JavaScript callback to notify extension of incoming request + on_sign_request_received(&request.id, &request.message); + + // Return error - JavaScript handles response via send_response() + // This is intentional as the signing happens asynchronously in the extension + Err(SigSocketError::Other("Handled by JavaScript extension".to_string())) + } +} + +/// External JavaScript functions that the extension must implement +#[wasm_bindgen] +extern "C" { + /// Called when a sign request is received from the server + /// + /// The extension should: + /// 1. Store the request details + /// 2. Show notification/badge to user + /// 3. Handle user approval flow when popup is opened + /// + /// # Arguments + /// * `request_id` - Unique identifier for the request + /// * `message_base64` - Message to be signed (base64-encoded) + #[wasm_bindgen(js_name = "onSignRequestReceived")] + pub fn on_sign_request_received(request_id: &str, message_base64: &str); + + /// Called when connection state changes + /// + /// # Arguments + /// * `connected` - True if connected, false if disconnected + #[wasm_bindgen(js_name = "onConnectionStateChanged")] + pub fn on_connection_state_changed(connected: bool); +} diff --git a/wasm_app/src/sigsocket/mod.rs b/wasm_app/src/sigsocket/mod.rs new file mode 100644 index 0000000..5fe1bfb --- /dev/null +++ b/wasm_app/src/sigsocket/mod.rs @@ -0,0 +1,11 @@ +//! SigSocket integration module for WASM app +//! +//! This module provides a clean transport API for SigSocket communication +//! that can be used by the browser extension. It handles connection management +//! and delegates signing to the extension through JavaScript callbacks. + +pub mod connection; +pub mod handler; + +pub use connection::SigSocketConnection; +pub use handler::JavaScriptSignHandler; diff --git a/wasm_app/src/vault_bindings.rs b/wasm_app/src/vault_bindings.rs index 304578b..e38e4fd 100644 --- a/wasm_app/src/vault_bindings.rs +++ b/wasm_app/src/vault_bindings.rs @@ -120,6 +120,41 @@ pub fn is_unlocked() -> bool { }) } +/// Get the default public key for a workspace (keyspace) +/// This returns the public key of the first keypair in the keyspace +#[wasm_bindgen] +pub async fn get_workspace_default_public_key(workspace_id: &str) -> Result { + // For now, workspace_id is the same as keyspace name + // In a full implementation, you might have a mapping from workspace to keyspace + + SESSION_MANAGER.with(|cell| { + if let Some(session) = cell.borrow().as_ref() { + if let Some(keyspace_name) = session.current_keyspace_name() { + if keyspace_name == workspace_id { + // Use the default_keypair method to get the first keypair + if let Some(default_keypair) = session.default_keypair() { + // Return the actual public key as hex + let public_key_hex = hex::encode(&default_keypair.public_key); + return Ok(JsValue::from_str(&public_key_hex)); + } + } + } + } + Err(JsValue::from_str("Workspace not found or no keypairs available")) + }) +} + +/// Get the current unlocked public key as hex string +#[wasm_bindgen] +pub fn get_current_unlocked_public_key() -> Result { + SESSION_MANAGER.with(|cell| { + cell.borrow().as_ref() + .and_then(|session| session.current_keypair_public_key()) + .map(|pk| hex::encode(pk.as_slice())) + .ok_or_else(|| JsValue::from_str("No keypair selected or no keyspace unlocked")) + }) +} + /// Get all keypairs from the current session /// Returns an array of keypair objects with id, type, and metadata // #[wasm_bindgen] @@ -214,7 +249,7 @@ pub async fn add_keypair( Ok(JsValue::from_str(&key_id)) } -/// Sign message with current session +/// Sign message with current session (requires selected keypair) #[wasm_bindgen] pub async fn sign(message: &[u8]) -> Result { { @@ -235,6 +270,63 @@ pub async fn sign(message: &[u8]) -> Result { } } +/// Sign message with default keypair (first keypair in keyspace) without changing session state +#[wasm_bindgen] +pub async fn sign_with_default_keypair(message: &[u8]) -> Result { + // Temporarily select the default keypair, sign, then restore the original selection + let original_keypair = SESSION_MANAGER.with(|cell| { + cell.borrow().as_ref() + .and_then(|session| session.current_keypair()) + .map(|kp| kp.id.clone()) + }); + + // Select default keypair + let select_result = SESSION_MANAGER.with(|cell| { + let mut session_opt = cell.borrow_mut().take(); + if let Some(ref mut session) = session_opt { + let result = session.select_default_keypair(); + *cell.borrow_mut() = Some(session_opt.take().unwrap()); + result.map_err(|e| e.to_string()) + } else { + Err("Session not initialized".to_string()) + } + }); + + if let Err(e) = select_result { + return Err(JsValue::from_str(&format!("Failed to select default keypair: {e}"))); + } + + // Sign with the default keypair + let sign_result = { + let session_ptr = SESSION_MANAGER.with(|cell| cell.borrow().as_ref().map(|s| s as *const _)); + let session: &vault::session::SessionManager = match session_ptr { + Some(ptr) => unsafe { &*ptr }, + None => return Err(JsValue::from_str("Session not initialized")), + }; + session.sign(message).await + }; + + // Restore original keypair selection if there was one + if let Some(original_id) = original_keypair { + SESSION_MANAGER.with(|cell| { + let mut session_opt = cell.borrow_mut().take(); + if let Some(ref mut session) = session_opt { + let _ = session.select_keypair(&original_id); // Ignore errors here + *cell.borrow_mut() = Some(session_opt.take().unwrap()); + } + }); + } + + // Return the signature result + match sign_result { + Ok(sig_bytes) => { + let hex_sig = hex::encode(&sig_bytes); + Ok(JsValue::from_str(&hex_sig)) + } + Err(e) => Err(JsValue::from_str(&format!("Sign error: {e}"))), + } +} + /// Verify a signature with the current session's selected keypair #[wasm_bindgen] pub async fn verify(message: &[u8], signature: &str) -> Result {