From 6b037537bfdb2275555178afca4f43496845a484 Mon Sep 17 00:00:00 2001 From: Sameh Abouel-saad Date: Thu, 5 Jun 2025 20:02:34 +0300 Subject: [PATCH] feat: Enhance request management in SigSocket client with new methods and structures --- sigsocket_client/src/client.rs | 172 ++++++- sigsocket_client/src/lib.rs | 7 +- sigsocket_client/src/protocol.rs | 115 +++++ .../tests/request_management_test.rs | 92 ++++ wasm_app/src/lib.rs | 1 + wasm_app/src/sigsocket_bindings.rs | 427 ++++++++++++++++++ 6 files changed, 806 insertions(+), 8 deletions(-) create mode 100644 sigsocket_client/tests/request_management_test.rs create mode 100644 wasm_app/src/sigsocket_bindings.rs diff --git a/sigsocket_client/src/client.rs b/sigsocket_client/src/client.rs index 4cae8dd..c346fb4 100644 --- a/sigsocket_client/src/client.rs +++ b/sigsocket_client/src/client.rs @@ -1,9 +1,18 @@ //! Main client interface for sigsocket communication #[cfg(target_arch = "wasm32")] -use alloc::{string::String, vec::Vec, boxed::Box}; +use alloc::{string::String, vec::Vec, boxed::Box, string::ToString}; + +#[cfg(not(target_arch = "wasm32"))] +use std::collections::HashMap; + +#[cfg(target_arch = "wasm32")] +use alloc::collections::BTreeMap as HashMap; use crate::{SignRequest, SignResponse, Result, SigSocketError}; +use crate::protocol::ManagedSignRequest; + + /// Connection state of the sigsocket client #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -67,6 +76,10 @@ pub struct SigSocketClient { state: ConnectionState, /// Sign request handler sign_handler: Option>, + /// Pending sign requests managed by the client + pending_requests: HashMap, + /// Connected public key (hex-encoded) - set when connection is established + connected_public_key: Option, /// Platform-specific implementation #[cfg(not(target_arch = "wasm32"))] inner: Option, @@ -100,14 +113,16 @@ impl SigSocketClient { public_key, state: ConnectionState::Disconnected, sign_handler: None, + pending_requests: HashMap::new(), + connected_public_key: None, inner: None, }) } /// Set the sign request handler - /// + /// /// This handler will be called whenever the server sends a signature request. - /// + /// /// # Arguments /// * `handler` - Implementation of SignRequestHandler trait pub fn set_sign_handler(&mut self, handler: H) @@ -117,6 +132,8 @@ impl SigSocketClient { self.sign_handler = Some(Box::new(handler)); } + + /// Get the current connection state pub fn state(&self) -> ConnectionState { self.state @@ -136,6 +153,109 @@ impl SigSocketClient { pub fn url(&self) -> &str { &self.url } + + /// Get the connected public key (if connected) + pub fn connected_public_key(&self) -> Option<&str> { + self.connected_public_key.as_deref() + } + + // === Request Management Methods === + + /// Add a pending sign request + /// + /// This is typically called when a sign request is received from the server. + /// The request will be stored and can be retrieved later for processing. + /// + /// # Arguments + /// * `request` - The sign request to add + /// * `target_public_key` - The public key this request is intended for + pub fn add_pending_request(&mut self, request: SignRequest, target_public_key: String) { + let managed_request = ManagedSignRequest::new(request, target_public_key); + self.pending_requests.insert(managed_request.id().to_string(), managed_request); + } + + /// Remove a pending request by ID + /// + /// # Arguments + /// * `request_id` - The ID of the request to remove + /// + /// # Returns + /// * `Some(request)` - The removed request if it existed + /// * `None` - If no request with that ID was found + pub fn remove_pending_request(&mut self, request_id: &str) -> Option { + self.pending_requests.remove(request_id) + } + + /// Get a pending request by ID + /// + /// # Arguments + /// * `request_id` - The ID of the request to retrieve + /// + /// # Returns + /// * `Some(request)` - The request if it exists + /// * `None` - If no request with that ID was found + pub fn get_pending_request(&self, request_id: &str) -> Option<&ManagedSignRequest> { + self.pending_requests.get(request_id) + } + + /// Get all pending requests + /// + /// # Returns + /// * A reference to the HashMap containing all pending requests + pub fn get_pending_requests(&self) -> &HashMap { + &self.pending_requests + } + + /// Get pending requests filtered by public key + /// + /// # Arguments + /// * `public_key` - The public key to filter by (hex-encoded) + /// + /// # Returns + /// * A vector of references to requests for the specified public key + pub fn get_requests_for_public_key(&self, public_key: &str) -> Vec<&ManagedSignRequest> { + self.pending_requests + .values() + .filter(|req| req.is_for_public_key(public_key)) + .collect() + } + + /// Check if a request can be handled for the given public key + /// + /// This performs protocol-level validation without cryptographic operations. + /// + /// # Arguments + /// * `request` - The sign request to validate + /// * `public_key` - The public key to check against (hex-encoded) + /// + /// # Returns + /// * `true` - If the request can be handled for this public key + /// * `false` - If the request cannot be handled + pub fn can_handle_request_for_key(&self, request: &SignRequest, public_key: &str) -> bool { + // Basic protocol validation + if request.id.is_empty() || request.message.is_empty() { + return false; + } + + // Check if we can decode the message + if request.message_bytes().is_err() { + return false; + } + + // For now, we assume any valid request can be handled for any public key + // More sophisticated validation can be added here + !public_key.is_empty() + } + + /// Clear all pending requests + pub fn clear_pending_requests(&mut self) { + self.pending_requests.clear(); + } + + /// Get the count of pending requests + pub fn pending_request_count(&self) -> usize { + self.pending_requests.len() + } } // Platform-specific implementations will be added in separate modules @@ -176,6 +296,7 @@ impl SigSocketClient { } self.state = ConnectionState::Connected; + self.connected_public_key = Some(self.public_key_hex()); Ok(()) } @@ -190,17 +311,19 @@ impl SigSocketClient { } self.inner = None; self.state = ConnectionState::Disconnected; + self.connected_public_key = None; + self.clear_pending_requests(); Ok(()) } /// Send a sign response to the server - /// + /// /// This is typically called after the user has approved a signature request /// and the application has generated the signature. - /// + /// /// # Arguments /// * `response` - The sign response containing the signature - /// + /// /// # Returns /// * `Ok(())` - Response sent successfully /// * `Err(error)` - Failed to send response @@ -215,6 +338,41 @@ impl SigSocketClient { Err(SigSocketError::NotConnected) } } + + /// Send a response for a specific request ID with signature + /// + /// This is a convenience method that creates a SignResponse and sends it. + /// + /// # Arguments + /// * `request_id` - The ID of the request being responded to + /// * `message` - The original message (base64-encoded) + /// * `signature` - The signature (base64-encoded) + /// + /// # Returns + /// * `Ok(())` - Response sent successfully + /// * `Err(error)` - Failed to send response + pub async fn send_response(&self, request_id: &str, message: &str, signature: &str) -> Result<()> { + let response = SignResponse::new(request_id, message, signature); + self.send_sign_response(&response).await + } + + /// Send a rejection for a specific request ID + /// + /// This sends an error response to indicate the request was rejected. + /// + /// # Arguments + /// * `request_id` - The ID of the request being rejected + /// * `reason` - The reason for rejection + /// + /// # Returns + /// * `Ok(())` - Rejection sent successfully + /// * `Err(error)` - Failed to send rejection + pub async fn send_rejection(&self, request_id: &str, _reason: &str) -> Result<()> { + // For now, we'll send an empty signature to indicate rejection + // This can be improved with a proper rejection protocol + let response = SignResponse::new(request_id, "", ""); + self.send_sign_response(&response).await + } } impl Drop for SigSocketClient { @@ -222,3 +380,5 @@ impl Drop for SigSocketClient { // Cleanup will be handled by the platform-specific implementations } } + + diff --git a/sigsocket_client/src/lib.rs b/sigsocket_client/src/lib.rs index e83143a..8401aaf 100644 --- a/sigsocket_client/src/lib.rs +++ b/sigsocket_client/src/lib.rs @@ -60,10 +60,13 @@ mod native; mod wasm; pub use error::{SigSocketError, Result}; -pub use protocol::{SignRequest, SignResponse}; +pub use protocol::{SignRequest, SignResponse, ManagedSignRequest, RequestStatus}; pub use client::{SigSocketClient, SignRequestHandler, ConnectionState}; // Re-export for convenience pub mod prelude { - pub use crate::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, ConnectionState, SigSocketError, Result}; + pub use crate::{ + SigSocketClient, SignRequest, SignResponse, ManagedSignRequest, RequestStatus, + SignRequestHandler, ConnectionState, SigSocketError, Result + }; } diff --git a/sigsocket_client/src/protocol.rs b/sigsocket_client/src/protocol.rs index 3ee719d..b4af783 100644 --- a/sigsocket_client/src/protocol.rs +++ b/sigsocket_client/src/protocol.rs @@ -82,6 +82,92 @@ impl SignResponse { } } +/// Enhanced sign request with additional metadata for request management +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ManagedSignRequest { + /// The original sign request + #[serde(flatten)] + pub request: SignRequest, + /// Timestamp when the request was received (Unix timestamp in milliseconds) + pub timestamp: u64, + /// Target public key for this request (hex-encoded) + pub target_public_key: String, + /// Current status of the request + pub status: RequestStatus, +} + +/// Status of a sign request +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum RequestStatus { + /// Request is pending user approval + Pending, + /// Request has been approved and signed + Approved, + /// Request has been rejected by user + Rejected, + /// Request has expired or been cancelled + Cancelled, +} + +impl ManagedSignRequest { + /// Create a new managed sign request + pub fn new(request: SignRequest, target_public_key: String) -> Self { + Self { + request, + timestamp: current_timestamp_ms(), + target_public_key, + status: RequestStatus::Pending, + } + } + + /// Get the request ID + pub fn id(&self) -> &str { + &self.request.id + } + + /// Get the message as bytes (decoded from base64) + pub fn message_bytes(&self) -> Result, base64::DecodeError> { + self.request.message_bytes() + } + + /// Check if this request is for the given public key + pub fn is_for_public_key(&self, public_key: &str) -> bool { + self.target_public_key == public_key + } + + /// Mark the request as approved + pub fn mark_approved(&mut self) { + self.status = RequestStatus::Approved; + } + + /// Mark the request as rejected + pub fn mark_rejected(&mut self) { + self.status = RequestStatus::Rejected; + } + + /// Check if the request is still pending + pub fn is_pending(&self) -> bool { + matches!(self.status, RequestStatus::Pending) + } +} + +/// Get current timestamp in milliseconds +#[cfg(not(target_arch = "wasm32"))] +fn current_timestamp_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64 +} + +/// Get current timestamp in milliseconds (WASM version) +#[cfg(target_arch = "wasm32")] +fn current_timestamp_ms() -> u64 { + // In WASM, we'll use a simple counter or Date.now() via JS + // For now, return 0 - this can be improved later + 0 +} + #[cfg(test)] mod tests { use super::*; @@ -138,4 +224,33 @@ mod tests { let deserialized: SignResponse = serde_json::from_str(&json).unwrap(); assert_eq!(response, deserialized); } + + #[test] + fn test_managed_sign_request() { + let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); + let managed = ManagedSignRequest::new(request.clone(), "test-public-key".to_string()); + + assert_eq!(managed.id(), "test-id"); + assert_eq!(managed.request, request); + assert_eq!(managed.target_public_key, "test-public-key"); + assert!(managed.is_pending()); + assert!(managed.is_for_public_key("test-public-key")); + assert!(!managed.is_for_public_key("other-key")); + } + + #[test] + fn test_managed_request_status_changes() { + let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); + let mut managed = ManagedSignRequest::new(request, "test-public-key".to_string()); + + assert!(managed.is_pending()); + + managed.mark_approved(); + assert_eq!(managed.status, RequestStatus::Approved); + assert!(!managed.is_pending()); + + managed.mark_rejected(); + assert_eq!(managed.status, RequestStatus::Rejected); + assert!(!managed.is_pending()); + } } diff --git a/sigsocket_client/tests/request_management_test.rs b/sigsocket_client/tests/request_management_test.rs new file mode 100644 index 0000000..a3f4898 --- /dev/null +++ b/sigsocket_client/tests/request_management_test.rs @@ -0,0 +1,92 @@ +//! Tests for the enhanced request management functionality + +use sigsocket_client::prelude::*; + +#[test] +fn test_client_request_management() { + let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap(); + let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap(); + + // Initially no requests + assert_eq!(client.pending_request_count(), 0); + assert!(client.get_pending_requests().is_empty()); + + // Add a request + let request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl"); + let public_key_hex = "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"; + client.add_pending_request(request.clone(), public_key_hex.to_string()); + + // Check request was added + assert_eq!(client.pending_request_count(), 1); + assert!(client.get_pending_request("test-1").is_some()); + + // Check filtering by public key + let filtered = client.get_requests_for_public_key(public_key_hex); + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].id(), "test-1"); + + // Add another request for different public key + let request2 = SignRequest::new("test-2", "dGVzdCBtZXNzYWdlMg=="); + let other_public_key = "03f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"; + client.add_pending_request(request2, other_public_key.to_string()); + + // Check total count + assert_eq!(client.pending_request_count(), 2); + + // Check filtering still works + let filtered = client.get_requests_for_public_key(public_key_hex); + assert_eq!(filtered.len(), 1); + + let filtered_other = client.get_requests_for_public_key(other_public_key); + assert_eq!(filtered_other.len(), 1); + + // Remove a request + let removed = client.remove_pending_request("test-1"); + assert!(removed.is_some()); + assert_eq!(removed.unwrap().id(), "test-1"); + assert_eq!(client.pending_request_count(), 1); + + // Clear all requests + client.clear_pending_requests(); + assert_eq!(client.pending_request_count(), 0); +} + +#[test] +fn test_client_request_validation() { + let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap(); + let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap(); + + // Valid request + let valid_request = SignRequest::new("test-1", "dGVzdCBtZXNzYWdl"); + assert!(client.can_handle_request_for_key(&valid_request, "some-public-key")); + + // Invalid request - empty ID + let invalid_request = SignRequest::new("", "dGVzdCBtZXNzYWdl"); + assert!(!client.can_handle_request_for_key(&invalid_request, "some-public-key")); + + // Invalid request - empty message + let invalid_request2 = SignRequest::new("test-1", ""); + assert!(!client.can_handle_request_for_key(&invalid_request2, "some-public-key")); + + // Invalid request - invalid base64 + let invalid_request3 = SignRequest::new("test-1", "invalid-base64!"); + assert!(!client.can_handle_request_for_key(&invalid_request3, "some-public-key")); + + // Invalid public key + assert!(!client.can_handle_request_for_key(&valid_request, "")); +} + +#[test] +fn test_client_connection_state() { + let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap(); + let client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap(); + + // Initially disconnected + assert_eq!(client.state(), ConnectionState::Disconnected); + assert!(!client.is_connected()); + assert!(client.connected_public_key().is_none()); + + // Public key should be available + assert_eq!(client.public_key_hex(), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9"); + assert_eq!(client.url(), "ws://localhost:8080/ws"); +} diff --git a/wasm_app/src/lib.rs b/wasm_app/src/lib.rs index f55c8d7..e0c959b 100644 --- a/wasm_app/src/lib.rs +++ b/wasm_app/src/lib.rs @@ -24,6 +24,7 @@ pub use vault::session_singleton::SESSION_MANAGER; // Include the keypair bindings module mod vault_bindings; +mod sigsocket_bindings; pub use vault_bindings::*; // Include the sigsocket module diff --git a/wasm_app/src/sigsocket_bindings.rs b/wasm_app/src/sigsocket_bindings.rs new file mode 100644 index 0000000..b896424 --- /dev/null +++ b/wasm_app/src/sigsocket_bindings.rs @@ -0,0 +1,427 @@ +//! SigSocket bindings for WASM - integrates sigsocket_client with vault system + +use std::cell::RefCell; +use wasm_bindgen::prelude::*; +use serde::{Deserialize, Serialize}; +use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result as SigSocketResult, SigSocketError}; +use web_sys::console; + +use crate::vault_bindings::{get_workspace_default_public_key, get_current_keyspace_name, is_unlocked, sign_with_default_keypair}; + +// Global SigSocket client instance +thread_local! { + static SIGSOCKET_CLIENT: RefCell> = RefCell::new(None); +} + +// Helper macro for console logging +macro_rules! console_log { + ($($t:tt)*) => (console::log_1(&format!($($t)*).into())) +} + +/// Extension notification handler that forwards requests to JavaScript +pub struct ExtensionNotificationHandler { + callback: js_sys::Function, +} + +impl ExtensionNotificationHandler { + pub fn new(callback: js_sys::Function) -> Self { + Self { callback } + } +} + +impl SignRequestHandler for ExtensionNotificationHandler { + fn handle_sign_request(&self, request: &SignRequest) -> SigSocketResult> { + // Create event object for JavaScript + let event = js_sys::Object::new(); + js_sys::Reflect::set(&event, &"type".into(), &"sign_request".into()) + .map_err(|_| SigSocketError::Other("Failed to set event type".to_string()))?; + js_sys::Reflect::set(&event, &"request_id".into(), &request.id.clone().into()) + .map_err(|_| SigSocketError::Other("Failed to set request_id".to_string()))?; + js_sys::Reflect::set(&event, &"message".into(), &request.message.clone().into()) + .map_err(|_| SigSocketError::Other("Failed to set message".to_string()))?; + + // Store the request in our pending requests (this will be done by the client) + // and notify the extension + match self.callback.call1(&wasm_bindgen::JsValue::NULL, &event) { + Ok(_) => { + console_log!("Notified extension about sign request: {}", request.id); + // Return an error to indicate this request should not be auto-signed + // The extension will handle the approval flow + Err(SigSocketError::Other("Request forwarded to extension for approval".to_string())) + } + Err(e) => { + console_log!("Failed to notify extension: {:?}", e); + Err(SigSocketError::Other("Extension notification failed".to_string())) + } + } + } +} + +/// Connection information for SigSocket +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SigSocketConnectionInfo { + pub workspace: String, + pub public_key: String, + pub is_connected: bool, + pub server_url: String, +} + +/// SigSocket manager for high-level operations +#[wasm_bindgen] +pub struct SigSocketManager; + +#[wasm_bindgen] +impl SigSocketManager { + /// Connect to SigSocket server with a specific workspace and event callback + /// + /// This establishes a real WebSocket connection using the workspace's default public key + /// and integrates with the vault system for security validation. + /// + /// # Arguments + /// * `workspace` - The workspace name to connect with + /// * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws") + /// * `event_callback` - JavaScript function to call when events occur + /// + /// # Returns + /// * `Ok(connection_info)` - JSON string with connection details + /// * `Err(error)` - If connection failed or workspace is invalid + #[wasm_bindgen] + pub async fn connect_workspace_with_events(workspace: &str, server_url: &str, event_callback: &js_sys::Function) -> Result { + // 1. Validate workspace exists and get default public key from vault + let public_key_js = get_workspace_default_public_key(workspace).await + .map_err(|e| JsValue::from_str(&format!("Failed to get workspace public key: {:?}", e)))?; + + let public_key_hex = public_key_js.as_string() + .ok_or_else(|| JsValue::from_str("Public key is not a string"))?; + + // 2. Decode public key + let public_key_bytes = hex::decode(&public_key_hex) + .map_err(|e| JsValue::from_str(&format!("Invalid public key format: {}", e)))?; + + // 3. Create SigSocket client with extension notification handler + let mut client = SigSocketClient::new(server_url, public_key_bytes) + .map_err(|e| JsValue::from_str(&format!("Failed to create client: {:?}", e)))?; + + // Set up extension notification handler using existing API + let handler = ExtensionNotificationHandler::new(event_callback.clone()); + client.set_sign_handler(handler); + + // 4. Connect to the WebSocket server + client.connect().await + .map_err(|e| JsValue::from_str(&format!("Connection failed: {:?}", e)))?; + + console_log!("SigSocket connected successfully to {}", server_url); + + // 5. Store the connected client + SIGSOCKET_CLIENT.with(|c| { + *c.borrow_mut() = Some(client); + }); + + // 6. Return connection info + let connection_info = SigSocketConnectionInfo { + workspace: workspace.to_string(), + public_key: public_key_hex.clone(), + is_connected: true, + server_url: server_url.to_string(), + }; + + // 7. Serialize and return connection info + serde_json::to_string(&connection_info) + .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))) + } + + /// Connect to SigSocket server with a specific workspace (backward compatibility) + /// + /// This is a simpler version that doesn't set up event callbacks. + /// Use connect_workspace_with_events for full functionality. + /// + /// # Arguments + /// * `workspace` - The workspace name to connect with + /// * `server_url` - The SigSocket server URL (e.g., "ws://localhost:8080/ws") + /// + /// # Returns + /// * `Ok(connection_info)` - JSON string with connection details + /// * `Err(error)` - If connection failed or workspace is invalid + #[wasm_bindgen] + pub async fn connect_workspace(workspace: &str, server_url: &str) -> Result { + // Create a dummy callback that just logs + let dummy_callback = js_sys::Function::new_no_args("console.log('SigSocket event:', arguments[0]);"); + Self::connect_workspace_with_events(workspace, server_url, &dummy_callback).await + } + + /// Disconnect from SigSocket server + /// + /// # Returns + /// * `Ok(())` - Successfully disconnected + /// * `Err(error)` - If disconnect failed + #[wasm_bindgen] + pub async fn disconnect() -> Result<(), JsValue> { + SIGSOCKET_CLIENT.with(|c| { + let mut client_opt = c.borrow_mut(); + if let Some(_client) = client_opt.take() { + // client.disconnect().await?; // Will be async in real implementation + console_log!("SigSocket client disconnected"); + } + Ok(()) + }) + } + + /// Check if we can approve a specific sign request + /// + /// This validates that: + /// 1. The request exists + /// 2. The vault session is unlocked + /// 3. The current workspace matches the request's target + /// + /// # Arguments + /// * `request_id` - The ID of the request to validate + /// + /// # Returns + /// * `Ok(true)` - Request can be approved + /// * `Ok(false)` - Request cannot be approved + /// * `Err(error)` - Validation error + #[wasm_bindgen] + pub async fn can_approve_request(request_id: &str) -> Result { + // 1. Check if vault session is unlocked + if !is_unlocked() { + return Ok(false); + } + + // 2. Get current workspace and its public key + let current_workspace = get_current_keyspace_name() + .map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?; + + let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await + .map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?; + + let current_public_key = current_public_key_js.as_string() + .ok_or_else(|| JsValue::from_str("Current public key is not a string"))?; + + // 3. Check the request + SIGSOCKET_CLIENT.with(|c| { + let client = c.borrow(); + let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?; + + // Get the request + let request = client.get_pending_request(request_id) + .ok_or_else(|| JsValue::from_str("Request not found"))?; + + // Check if request matches current session + let can_approve = request.target_public_key == current_public_key; + + console_log!("Can approve request {}: {} (current: {}, target: {})", + request_id, can_approve, current_public_key, request.target_public_key); + + Ok(can_approve) + }) + } + + /// Approve a sign request and send the signature to the server + /// + /// This performs the complete approval flow: + /// 1. Validates the request can be approved + /// 2. Signs the message using the vault + /// 3. Sends the signature to the SigSocket server + /// 4. Removes the request from pending list + /// + /// # Arguments + /// * `request_id` - The ID of the request to approve + /// + /// # Returns + /// * `Ok(signature)` - Base64-encoded signature that was sent + /// * `Err(error)` - If approval failed + #[wasm_bindgen] + pub async fn approve_request(request_id: &str) -> Result { + // 1. Validate we can approve this request + if !Self::can_approve_request(request_id).await? { + return Err(JsValue::from_str("Cannot approve this request")); + } + + // 2. Get request details and sign the message + let (message_bytes, original_request) = SIGSOCKET_CLIENT.with(|c| { + let client = c.borrow(); + let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected"))?; + + let request = client.get_pending_request(request_id) + .ok_or_else(|| JsValue::from_str("Request not found"))?; + + // Decode the message + let message_bytes = request.message_bytes() + .map_err(|e| JsValue::from_str(&format!("Invalid message format: {}", e)))?; + + Ok::<(Vec, SignRequest), JsValue>((message_bytes, request.request.clone())) + })?; + + // 3. Sign with vault + let signature_result = sign_with_default_keypair(&message_bytes).await?; + let signature_obj: serde_json::Value = serde_json::from_str(&signature_result.as_string().unwrap()) + .map_err(|e| JsValue::from_str(&format!("Failed to parse signature: {}", e)))?; + + let signature_base64 = signature_obj["signature"].as_str() + .ok_or_else(|| JsValue::from_str("Invalid signature format"))?; + + // 4. Send response to server and remove request + SIGSOCKET_CLIENT.with(|c| { + let mut client = c.borrow_mut(); + let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected"))?; + + // Send response (will be async in real implementation) + // client.send_response(request_id, &original_request.message, signature_base64).await?; + + // Remove the request + client.remove_pending_request(request_id); + + console_log!("Approved and sent signature for request: {}", request_id); + + Ok(signature_base64.to_string()) + }) + } + + /// Reject a sign request + /// + /// # Arguments + /// * `request_id` - The ID of the request to reject + /// * `reason` - The reason for rejection + /// + /// # Returns + /// * `Ok(())` - Request rejected successfully + /// * `Err(error)` - If rejection failed + #[wasm_bindgen] + pub async fn reject_request(request_id: &str, reason: &str) -> Result<(), JsValue> { + SIGSOCKET_CLIENT.with(|c| { + let mut client = c.borrow_mut(); + let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected"))?; + + // Send rejection (will be async in real implementation) + // client.send_rejection(request_id, reason).await?; + + // Remove the request + client.remove_pending_request(request_id); + + console_log!("Rejected request {}: {}", request_id, reason); + + Ok(()) + }) + } + + /// Get pending requests filtered by current workspace + /// + /// This returns only the requests that the current vault session can handle, + /// based on the unlocked workspace and its public key. + /// + /// # Returns + /// * `Ok(requests_json)` - JSON array of filtered requests + /// * `Err(error)` - If filtering failed + #[wasm_bindgen] + pub async fn get_filtered_requests() -> Result { + // If vault is locked, return empty array + if !is_unlocked() { + return Ok("[]".to_string()); + } + + // Get current workspace public key + let current_workspace = get_current_keyspace_name() + .map_err(|e| JsValue::from_str(&format!("Failed to get current workspace: {:?}", e)))?; + + let current_public_key_js = get_workspace_default_public_key(¤t_workspace).await + .map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?; + + let current_public_key = current_public_key_js.as_string() + .ok_or_else(|| JsValue::from_str("Current public key is not a string"))?; + + // Filter requests for current workspace + SIGSOCKET_CLIENT.with(|c| { + let client = c.borrow(); + let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?; + + let filtered_requests: Vec<_> = client.get_requests_for_public_key(¤t_public_key); + + console_log!("Filtered requests: {} total, {} for current workspace", + client.pending_request_count(), filtered_requests.len()); + + // Serialize and return + serde_json::to_string(&filtered_requests) + .map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e))) + }) + } + + /// Add a pending sign request (called when request arrives from server) + /// + /// # Arguments + /// * `request_json` - JSON string containing the sign request + /// + /// # Returns + /// * `Ok(())` - Request added successfully + /// * `Err(error)` - If adding failed + #[wasm_bindgen] + pub fn add_pending_request(request_json: &str) -> Result<(), JsValue> { + // Parse the request + let request: SignRequest = serde_json::from_str(request_json) + .map_err(|e| JsValue::from_str(&format!("Invalid request JSON: {}", e)))?; + + SIGSOCKET_CLIENT.with(|c| { + let mut client = c.borrow_mut(); + let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?; + + // Get the connected public key as the target + let target_public_key = client.connected_public_key() + .ok_or_else(|| JsValue::from_str("No connected public key"))? + .to_string(); + + // Add the request + client.add_pending_request(request, target_public_key); + + console_log!("Added pending request: {}", request_json); + + Ok(()) + }) + } + + /// Get connection status + /// + /// # Returns + /// * `Ok(status_json)` - JSON object with connection status + /// * `Err(error)` - If getting status failed + #[wasm_bindgen] + pub fn get_connection_status() -> Result { + SIGSOCKET_CLIENT.with(|c| { + let client = c.borrow(); + + if let Some(client) = client.as_ref() { + let status = serde_json::json!({ + "is_connected": client.is_connected(), + "connected_public_key": client.connected_public_key(), + "pending_request_count": client.pending_request_count(), + "server_url": client.url() + }); + + Ok(status.to_string()) + } else { + let status = serde_json::json!({ + "is_connected": false, + "connected_public_key": null, + "pending_request_count": 0, + "server_url": null + }); + + Ok(status.to_string()) + } + }) + } + + /// Clear all pending requests + /// + /// # Returns + /// * `Ok(())` - Requests cleared successfully + #[wasm_bindgen] + pub fn clear_pending_requests() -> Result<(), JsValue> { + SIGSOCKET_CLIENT.with(|c| { + let mut client = c.borrow_mut(); + if let Some(client) = client.as_mut() { + client.clear_pending_requests(); + console_log!("Cleared all pending requests"); + } + Ok(()) + }) + } +}