feat: Enhance request management in SigSocket client with new methods and structures
This commit is contained in:
@@ -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
|
||||
|
427
wasm_app/src/sigsocket_bindings.rs
Normal file
427
wasm_app/src/sigsocket_bindings.rs
Normal file
@@ -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<Option<SigSocketClient>> = 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<Vec<u8>> {
|
||||
// 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<String, JsValue> {
|
||||
// 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<String, JsValue> {
|
||||
// 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<bool, JsValue> {
|
||||
// 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<String, JsValue> {
|
||||
// 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<u8>, 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<String, JsValue> {
|
||||
// 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<String, JsValue> {
|
||||
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(())
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user