Compare commits

...

12 Commits

Author SHA1 Message Date
Sameh Abouel-saad
c641d0ae2e feat: Update build scripts for crypto_vault_extension 2025-06-11 14:22:05 +03:00
Sameh Abouel-saad
6f42e5ab8d v2 2025-06-06 05:31:03 +03:00
Sameh Abouel-saad
203cde1cba Reset crypto_vault_extension to clean state before SigSocket implementation 2025-06-05 20:08:10 +03:00
Sameh Abouel-saad
6b037537bf feat: Enhance request management in SigSocket client with new methods and structures 2025-06-05 20:02:34 +03:00
Sameh Abouel-saad
580fd72dce feat: Implement Sign Request Manager component for handling sign requests in the popup (WIP)
- Added SignRequestManager.js to manage sign requests, including UI states for keyspace lock, mismatch, and approval.
- Implemented methods for loading state, rendering UI, and handling user interactions (approve/reject requests).
- Integrated background message listeners for keyspace unlock events and request updates.
2025-06-04 16:55:15 +03:00
Sameh Abouel-saad
a0622629ae feat: Add function to retrieve the current keyspace name in WebAssembly bindings 2025-06-04 16:43:54 +03:00
Sameh Abouel-saad
4e1e707f85 feat: Add SigSocket integration with WASM client and JavaScript bridge for sign requests 2025-06-04 16:11:53 +03:00
Sameh Abouel-saad
9f143ded9d Implement native and WASM WebSocket client for sigsocket communication
- Added `NativeClient` for non-WASM environments with automatic reconnection and message handling.
- Introduced `WasmClient` for WASM environments, supporting WebSocket communication and reconnection logic.
- Created protocol definitions for `SignRequest` and `SignResponse` with serialization and deserialization.
- Developed integration tests for the client functionality and sign request handling.
- Implemented WASM-specific tests to ensure compatibility and functionality in browser environments.
2025-06-04 13:03:15 +03:00
Sameh Abouel-saad
b0d0aaa53d refactor: replace Ed25519 with Secp256k1 for default keypair generation 2025-06-02 15:59:17 +03:00
Sameh Abouel-saad
e00c140396 Fix tests 2025-05-29 14:40:55 +03:00
Sameh Abouel-saad
4ba1e43f4e clean files 2025-05-29 13:42:13 +03:00
Sameh Abouel-saad
b82d457873 Fixed unused imports and variables 2025-05-29 13:41:10 +03:00
51 changed files with 6116 additions and 293 deletions

View File

@ -5,5 +5,5 @@ members = [
"vault",
"evm_client",
"wasm_app",
"sigsocket_client",
]

View File

@ -26,6 +26,7 @@ build-wasm-app:
cd wasm_app && wasm-pack build --target web
# Build Hero Vault extension: wasm, copy, then extension
build-hero-vault-extension:
cd wasm_app && wasm-pack build --target web
cd hero_vault_extension && npm run build
build-crypto-vault-extension: build-wasm-app
cp wasm_app/pkg/wasm_app* crypto_vault_extension/wasm/
cp wasm_app/pkg/*.d.ts crypto_vault_extension/wasm/
cp wasm_app/pkg/*.js crypto_vault_extension/wasm/

View File

@ -134,13 +134,13 @@ For questions, contributions, or more details, see the architecture docs or open
### Native
```sh
cargo check --workspace --features kvstore/native
cargo check --workspace
cargo test --workspace
```
### WASM (kvstore only)
### WASM
```sh
cd kvstore
wasm-pack test --headless --firefox --features web
make test-browser-all
```
# Rhai Scripting System

View File

@ -17,32 +17,22 @@ cd "$(dirname "$0")/wasm_app" || exit 1
wasm-pack build --target web
echo -e "${GREEN}✓ WASM build successful!${RESET}"
# Step 2: Build the frontend extension
echo -e "${BLUE}Building frontend extension...${RESET}"
cd ../hero_vault_extension || exit 1
# Step 2: Prepare the frontend extension
echo -e "${BLUE}Preparing frontend extension...${RESET}"
cd ../crypto_vault_extension || exit 1
# Copy WASM files to the extension's public directory
echo "Copying WASM files..."
mkdir -p public/wasm
cp ../wasm_app/pkg/wasm_app* public/wasm/
cp ../wasm_app/pkg/*.d.ts public/wasm/
cp ../wasm_app/pkg/package.json public/wasm/
cp ../wasm_app/pkg/wasm_app* wasm/
cp ../wasm_app/pkg/*.d.ts wasm/
cp ../wasm_app/pkg/*.js wasm/
# Build the extension without TypeScript checking
echo "Building extension..."
export NO_TYPECHECK=true
npm run build
# Ensure the background script is properly built
echo "Building background script..."
node scripts/build-background.js
echo -e "${GREEN}✓ Frontend build successful!${RESET}"
echo -e "${GREEN}=== Build Complete ===${RESET}"
echo "Extension is ready in: $(pwd)/dist"
echo "Extension is ready in: $(pwd)"
echo ""
echo -e "${BLUE}To load the extension in Chrome:${RESET}"
echo "1. Go to chrome://extensions/"
echo "2. Enable Developer mode (toggle in top-right)"
echo "3. Click 'Load unpacked'"
echo "4. Select the 'dist' directory: $(pwd)/dist"
echo "4. Select the $(pwd) directory"

View File

@ -6,6 +6,9 @@ let sessionTimeoutDuration = 15; // Default 15 seconds
let sessionTimeoutId = null; // Background timer
let popupPort = null; // Track popup connection
// SigSocket service instance
let sigSocketService = null;
// Utility function to convert Uint8Array to hex
function toHex(uint8Array) {
return Array.from(uint8Array)
@ -135,8 +138,9 @@ async function restoreSession() {
return null;
}
// Import WASM module functions
// Import WASM module functions and SigSocket service
import init, * as wasmFunctions from './wasm/wasm_app.js';
import SigSocketService from './background/sigsocket.js';
// Initialize WASM module
async function initVault() {
@ -151,6 +155,13 @@ async function initVault() {
vault = wasmFunctions;
isInitialized = true;
// Initialize SigSocket service
if (!sigSocketService) {
sigSocketService = new SigSocketService();
await sigSocketService.initialize(vault);
console.log('🔌 SigSocket service initialized');
}
// Try to restore previous session
await restoreSession();
@ -172,6 +183,20 @@ const messageHandlers = {
initSession: async (request) => {
await vault.init_session(request.keyspace, request.password);
await sessionManager.save(request.keyspace);
// Smart auto-connect to SigSocket when session is initialized
if (sigSocketService) {
try {
// This will reuse existing connection if same workspace, or switch if different
const connected = await sigSocketService.connectToServer(request.keyspace);
if (connected) {
console.log(`🔗 SigSocket ready for workspace: ${request.keyspace}`);
}
} catch (error) {
console.warn('Failed to auto-connect to SigSocket:', error);
}
}
return { success: true };
},
@ -261,6 +286,62 @@ const messageHandlers = {
await chrome.storage.local.set({ sessionTimeout: request.timeout });
resetSessionTimeout(); // Restart with new duration
return { success: true };
},
// SigSocket handlers
connectSigSocket: async (request) => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const connected = await sigSocketService.connectToServer(request.workspace);
return { success: connected };
},
disconnectSigSocket: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
await sigSocketService.disconnect();
return { success: true };
},
getSigSocketStatus: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const status = await sigSocketService.getStatus();
return { success: true, status };
},
getPendingSignRequests: async () => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
try {
// Use WASM filtered requests which handles workspace filtering
const requests = await sigSocketService.getFilteredRequests();
return { success: true, requests };
} catch (error) {
console.error('Failed to get pending requests:', error);
return { success: false, error: error.message };
}
},
approveSignRequest: async (request) => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const approved = await sigSocketService.approveSignRequest(request.requestId);
return { success: approved };
},
rejectSignRequest: async (request) => {
if (!sigSocketService) {
return { success: false, error: 'SigSocket service not initialized' };
}
const rejected = await sigSocketService.rejectSignRequest(request.requestId, request.reason);
return { success: rejected };
}
};
@ -302,6 +383,11 @@ chrome.runtime.onConnect.addListener((port) => {
// Track popup connection
popupPort = port;
// Connect SigSocket service to popup
if (sigSocketService) {
sigSocketService.setPopupPort(port);
}
// If we have an active session, ensure keep-alive is running
if (currentSession) {
startKeepAlive();
@ -311,6 +397,11 @@ chrome.runtime.onConnect.addListener((port) => {
// Popup closed, clear reference and stop keep-alive
popupPort = null;
stopKeepAlive();
// Disconnect SigSocket service from popup
if (sigSocketService) {
sigSocketService.setPopupPort(null);
}
});
}
});

View File

@ -0,0 +1,410 @@
/**
* SigSocket Service - Clean Implementation with New WASM APIs
*
* This service provides a clean interface for SigSocket functionality using
* the new WASM-based APIs that handle all WebSocket management, request storage,
* and security validation internally.
*
* Architecture:
* - WASM handles: WebSocket connection, message parsing, request storage, security
* - Extension handles: UI notifications, badge updates, user interactions
*/
class SigSocketService {
constructor() {
// Connection state
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
// Configuration
this.defaultServerUrl = "ws://localhost:8080/ws";
// WASM module reference
this.wasmModule = null;
// UI communication
this.popupPort = null;
}
/**
* Initialize the service with WASM module
* @param {Object} wasmModule - The loaded WASM module with SigSocketManager
*/
async initialize(wasmModule) {
this.wasmModule = wasmModule;
// Load server URL from storage
try {
const result = await chrome.storage.local.get(['sigSocketUrl']);
if (result.sigSocketUrl) {
this.defaultServerUrl = result.sigSocketUrl;
}
} catch (error) {
console.warn('Failed to load SigSocket URL from storage:', error);
}
console.log('🔌 SigSocket service initialized with WASM APIs');
}
/**
* Connect to SigSocket server using WASM APIs
* WASM handles all connection logic (reuse, switching, etc.)
* @param {string} workspaceId - The workspace/keyspace identifier
* @returns {Promise<boolean>} - True if connected successfully
*/
async connectToServer(workspaceId) {
try {
if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId}`);
// Let WASM handle all connection logic (reuse, switching, etc.)
const connectionInfo = await this.wasmModule.SigSocketManager.connect_workspace_with_events(
workspaceId,
this.defaultServerUrl,
(event) => this.handleSigSocketEvent(event)
);
// Parse connection info
const info = JSON.parse(connectionInfo);
this.currentWorkspace = info.workspace;
this.connectedPublicKey = info.public_key;
this.isConnected = info.is_connected;
console.log(`✅ SigSocket connection result:`, {
workspace: this.currentWorkspace,
publicKey: this.connectedPublicKey?.substring(0, 16) + '...',
connected: this.isConnected
});
// Update badge to show current state
this.updateBadge();
return this.isConnected;
} catch (error) {
console.error('❌ SigSocket connection failed:', error);
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
return false;
}
}
/**
* Handle events from the WASM SigSocket client
* This is called automatically when requests arrive
* @param {Object} event - Event from WASM layer
*/
handleSigSocketEvent(event) {
console.log('📨 Received SigSocket event:', event);
if (event.type === 'sign_request') {
console.log(`🔐 New sign request: ${event.request_id}`);
// The request is automatically stored by WASM
// We just handle UI updates
this.showSignRequestNotification();
this.updateBadge();
this.notifyPopupOfNewRequest();
}
}
/**
* Approve a sign request using WASM APIs
* @param {string} requestId - Request to approve
* @returns {Promise<boolean>} - True if approved successfully
*/
async approveSignRequest(requestId) {
try {
if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
console.log(`✅ Approving request: ${requestId}`);
// WASM handles all validation, signing, and server communication
await this.wasmModule.SigSocketManager.approve_request(requestId);
console.log(`🎉 Request approved successfully: ${requestId}`);
// Update UI
this.updateBadge();
this.notifyPopupOfRequestUpdate();
return true;
} catch (error) {
console.error(`❌ Failed to approve request ${requestId}:`, error);
return false;
}
}
/**
* Reject a sign request using WASM APIs
* @param {string} requestId - Request to reject
* @param {string} reason - Reason for rejection
* @returns {Promise<boolean>} - True if rejected successfully
*/
async rejectSignRequest(requestId, reason = 'User rejected') {
try {
if (!this.wasmModule?.SigSocketManager) {
throw new Error('WASM SigSocketManager not available');
}
console.log(`❌ Rejecting request: ${requestId}, reason: ${reason}`);
// WASM handles rejection and server communication
await this.wasmModule.SigSocketManager.reject_request(requestId, reason);
console.log(`✅ Request rejected successfully: ${requestId}`);
// Update UI
this.updateBadge();
this.notifyPopupOfRequestUpdate();
return true;
} catch (error) {
console.error(`❌ Failed to reject request ${requestId}:`, error);
return false;
}
}
/**
* Get pending requests from WASM (filtered by current workspace)
* @returns {Promise<Array>} - Array of pending requests for current workspace
*/
async getPendingRequests() {
return this.getFilteredRequests();
}
/**
* Get filtered requests from WASM (workspace-aware)
* @returns {Promise<Array>} - Array of filtered requests
*/
async getFilteredRequests() {
try {
if (!this.wasmModule?.SigSocketManager) {
return [];
}
const requestsJson = await this.wasmModule.SigSocketManager.get_filtered_requests();
const requests = JSON.parse(requestsJson);
console.log(`📋 Retrieved ${requests.length} filtered requests for current workspace`);
return requests;
} catch (error) {
console.error('Failed to get filtered requests:', error);
return [];
}
}
/**
* Check if a request can be approved (keyspace validation)
* @param {string} requestId - Request ID to check
* @returns {Promise<boolean>} - True if can be approved
*/
async canApproveRequest(requestId) {
try {
if (!this.wasmModule?.SigSocketManager) {
return false;
}
return await this.wasmModule.SigSocketManager.can_approve_request(requestId);
} catch (error) {
console.error('Failed to check request approval status:', error);
return false;
}
}
/**
* Show notification for new sign request
*/
showSignRequestNotification() {
try {
if (chrome.notifications && chrome.notifications.create) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: 'SigSocket Sign Request',
message: 'New signature request received. Click to review.'
});
console.log('📢 Notification shown for sign request');
} else {
console.log('📢 Notifications not available, skipping notification');
}
} catch (error) {
console.warn('Failed to show notification:', error);
}
}
/**
* Update extension badge with pending request count
*/
async updateBadge() {
try {
const requests = await this.getPendingRequests();
const count = requests.length;
const badgeText = count > 0 ? count.toString() : '';
console.log(`🔢 Updating badge: ${count} pending requests`);
chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: '#ff6b6b' });
} catch (error) {
console.error('Failed to update badge:', error);
}
}
/**
* Notify popup about new request
*/
async notifyPopupOfNewRequest() {
if (!this.popupPort) {
console.log('No popup connected, skipping notification');
return;
}
try {
const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
this.popupPort.postMessage({
type: 'NEW_SIGN_REQUEST',
canApprove,
pendingRequests: requests
});
console.log(`📤 Notified popup: ${requests.length} requests, canApprove: ${canApprove}`);
} catch (error) {
console.error('Failed to notify popup:', error);
}
}
/**
* Notify popup about request updates
*/
async notifyPopupOfRequestUpdate() {
if (!this.popupPort) return;
try {
const requests = await this.getPendingRequests();
this.popupPort.postMessage({
type: 'REQUESTS_UPDATED',
pendingRequests: requests
});
} catch (error) {
console.error('Failed to notify popup of update:', error);
}
}
/**
* Disconnect from SigSocket server
* WASM handles all disconnection logic
*/
async disconnect() {
try {
if (this.wasmModule?.SigSocketManager) {
await this.wasmModule.SigSocketManager.disconnect();
}
// Clear local state
this.isConnected = false;
this.currentWorkspace = null;
this.connectedPublicKey = null;
this.updateBadge();
console.log('🔌 SigSocket disconnection requested');
} catch (error) {
console.error('Failed to disconnect:', error);
}
}
/**
* Get connection status from WASM
* @returns {Promise<Object>} - Connection status information
*/
async getStatus() {
try {
if (!this.wasmModule?.SigSocketManager) {
return {
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: this.defaultServerUrl
};
}
// Let WASM provide the authoritative status
const statusJson = await this.wasmModule.SigSocketManager.get_connection_status();
const status = JSON.parse(statusJson);
const requests = await this.getPendingRequests();
return {
isConnected: status.is_connected,
workspace: status.workspace,
publicKey: status.public_key,
pendingRequestCount: requests.length,
serverUrl: this.defaultServerUrl
};
} catch (error) {
console.error('Failed to get status:', error);
return {
isConnected: false,
workspace: null,
publicKey: null,
pendingRequestCount: 0,
serverUrl: this.defaultServerUrl
};
}
}
/**
* Set the popup port for communication
* @param {chrome.runtime.Port} port - The popup port
*/
setPopupPort(port) {
this.popupPort = port;
console.log('📱 Popup connected to SigSocket service');
}
/**
* Called when keyspace is unlocked - notify popup of current state
*/
async onKeypaceUnlocked() {
if (!this.popupPort) return;
try {
const requests = await this.getPendingRequests();
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
this.popupPort.postMessage({
type: 'KEYSPACE_UNLOCKED',
canApprove,
pendingRequests: requests
});
console.log(`🔓 Keyspace unlocked notification sent: ${requests.length} requests, canApprove: ${canApprove}`);
} catch (error) {
console.error('Failed to handle keyspace unlock:', error);
}
}
}
// Export for use in background script
export default SigSocketService;

View File

@ -0,0 +1,75 @@
# Mock SigSocket Server Demo
This directory contains a mock SigSocket server for testing the browser extension functionality.
## Setup
1. Install dependencies:
```bash
npm install
```
2. Start the mock server:
```bash
npm start
```
The server will listen on `ws://localhost:8080/ws`
## Usage
### Interactive Commands
Once the server is running, you can use these commands:
- `test` - Send a test sign request to all connected clients
- `status` - Show server status and connected clients
- `quit` - Shutdown the server
### Testing Flow
1. Start the mock server
2. Load the browser extension in Chrome
3. Create a keyspace and keypair in the extension
4. The extension should automatically connect to the server
5. The server will send a test sign request after 3 seconds
6. Use the extension popup to approve or reject the request
7. The server will log the response and send another request after 10 seconds
### Expected Output
When a client connects:
```
New WebSocket connection from: ::1
Received message: 04a8b2c3d4e5f6...
Client registered: client_1234567890_abc123 with public key: 04a8b2c3d4e5f6...
📝 Sending sign request to client_1234567890_abc123: req_1_1234567890
Message: "Test message 1 - 2024-01-01T12:00:00.000Z"
```
When a sign response is received:
```
Received sign response from client_1234567890_abc123: {
id: 'req_1_1234567890',
message: 'VGVzdCBtZXNzYWdlIDEgLSAyMDI0LTAxLTAxVDEyOjAwOjAwLjAwMFo=',
signature: '3045022100...'
}
✅ Sign request req_1_1234567890 completed successfully
Signature: 3045022100...
```
## Protocol
The mock server implements a simplified version of the SigSocket protocol:
1. **Client Introduction**: Client sends hex-encoded public key
2. **Welcome Message**: Server responds with welcome JSON
3. **Sign Requests**: Server sends JSON with `id` and `message` (base64)
4. **Sign Responses**: Client sends JSON with `id`, `message`, and `signature`
## Troubleshooting
- **Connection refused**: Make sure the server is running on port 8080
- **No sign requests**: Check that the extension is properly connected
- **Extension errors**: Check the browser console for JavaScript errors
- **WASM errors**: Ensure the WASM files are properly built and loaded

View File

@ -0,0 +1,232 @@
#!/usr/bin/env node
/**
* Mock SigSocket Server for Testing Browser Extension
*
* This is a simple WebSocket server that simulates the SigSocket protocol
* for testing the browser extension functionality.
*
* Usage:
* node mock_sigsocket_server.js
*
* The server will listen on ws://localhost:8080/ws
*/
const WebSocket = require('ws');
const http = require('http');
class MockSigSocketServer {
constructor(port = 8080) {
this.port = port;
this.clients = new Map(); // clientId -> { ws, publicKey }
this.requestCounter = 0;
this.setupServer();
}
setupServer() {
// Create HTTP server
this.httpServer = http.createServer();
// Create WebSocket server
this.wss = new WebSocket.Server({
server: this.httpServer,
path: '/ws'
});
this.wss.on('connection', (ws, req) => {
console.log('New WebSocket connection from:', req.socket.remoteAddress);
this.handleConnection(ws);
});
this.httpServer.listen(this.port, () => {
console.log(`Mock SigSocket Server listening on ws://localhost:${this.port}/ws`);
console.log('Waiting for browser extension connections...');
});
}
handleConnection(ws) {
let clientId = null;
let publicKey = null;
ws.on('message', (data) => {
try {
const message = data.toString();
console.log('Received message:', message);
// Check if this is a client introduction (hex-encoded public key)
if (!clientId && this.isHexString(message)) {
publicKey = message;
clientId = this.generateClientId();
this.clients.set(clientId, { ws, publicKey });
console.log(`Client registered: ${clientId} with public key: ${publicKey.substring(0, 16)}...`);
// Send welcome message
ws.send(JSON.stringify({
type: 'welcome',
clientId: clientId,
message: 'Connected to Mock SigSocket Server'
}));
// Schedule a test sign request after 3 seconds
setTimeout(() => {
this.sendTestSignRequest(clientId);
}, 3000);
return;
}
// Try to parse as JSON (sign response)
try {
const jsonMessage = JSON.parse(message);
this.handleSignResponse(clientId, jsonMessage);
} catch (e) {
console.log('Received non-JSON message:', message);
}
} catch (error) {
console.error('Error handling message:', error);
}
});
ws.on('close', () => {
if (clientId) {
this.clients.delete(clientId);
console.log(`Client disconnected: ${clientId}`);
}
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
}
handleSignResponse(clientId, response) {
console.log(`Received sign response from ${clientId}:`, response);
if (response.id && response.signature) {
console.log(`✅ Sign request ${response.id} completed successfully`);
console.log(` Signature: ${response.signature.substring(0, 32)}...`);
// Send another test request after 10 seconds
setTimeout(() => {
this.sendTestSignRequest(clientId);
}, 10000);
} else {
console.log('❌ Invalid sign response format');
}
}
sendTestSignRequest(clientId) {
const client = this.clients.get(clientId);
if (!client) {
console.log(`Client ${clientId} not found`);
return;
}
this.requestCounter++;
const requestId = `req_${this.requestCounter}_${Date.now()}`;
const testMessage = `Test message ${this.requestCounter} - ${new Date().toISOString()}`;
const messageBase64 = Buffer.from(testMessage).toString('base64');
const signRequest = {
id: requestId,
message: messageBase64
};
console.log(`📝 Sending sign request to ${clientId}:`, requestId);
console.log(` Message: "${testMessage}"`);
try {
client.ws.send(JSON.stringify(signRequest));
} catch (error) {
console.error(`Failed to send sign request to ${clientId}:`, error);
}
}
isHexString(str) {
return /^[0-9a-fA-F]+$/.test(str) && str.length >= 32; // At least 16 bytes
}
generateClientId() {
return `client_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
}
// Send a test request to all connected clients
broadcastTestRequest() {
console.log('\n📢 Broadcasting test sign request to all clients...');
for (const [clientId] of this.clients) {
this.sendTestSignRequest(clientId);
}
}
// Get server status
getStatus() {
return {
port: this.port,
connectedClients: this.clients.size,
clients: Array.from(this.clients.keys())
};
}
}
// Create and start the server
const server = new MockSigSocketServer();
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down Mock SigSocket Server...');
server.httpServer.close(() => {
console.log('Server closed');
process.exit(0);
});
});
// Add some interactive commands
process.stdin.setEncoding('utf8');
console.log('\n📋 Available commands:');
console.log(' "test" - Send test sign request to all clients');
console.log(' "status" - Show server status');
console.log(' "quit" - Shutdown server');
console.log(' Type a command and press Enter\n');
process.stdin.on('readable', () => {
const chunk = process.stdin.read();
if (chunk !== null) {
const command = chunk.trim().toLowerCase();
switch (command) {
case 'test':
server.broadcastTestRequest();
break;
case 'status':
const status = server.getStatus();
console.log('\n📊 Server Status:');
console.log(` Port: ${status.port}`);
console.log(` Connected clients: ${status.connectedClients}`);
if (status.clients.length > 0) {
console.log(` Client IDs: ${status.clients.join(', ')}`);
}
console.log('');
break;
case 'quit':
case 'exit':
process.emit('SIGINT');
break;
case '':
break;
default:
console.log(`Unknown command: ${command}`);
break;
}
}
});
// Export for testing
module.exports = MockSigSocketServer;

View File

@ -0,0 +1,21 @@
{
"name": "mock-sigsocket-server",
"version": "1.0.0",
"description": "Mock SigSocket server for testing browser extension",
"main": "mock_sigsocket_server.js",
"scripts": {
"start": "node mock_sigsocket_server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"ws": "^8.14.0"
},
"keywords": [
"websocket",
"sigsocket",
"testing",
"mock"
],
"author": "",
"license": "MIT"
}

View File

@ -6,7 +6,8 @@
"permissions": [
"storage",
"activeTab"
"activeTab",
"notifications"
],
"icons": {

View File

@ -73,6 +73,50 @@
</div>
</div>
<!-- SigSocket Requests Section -->
<div class="card sigsocket-section" id="sigSocketSection">
<div class="section-header">
<h3>🔌 SigSocket Requests</h3>
<div class="connection-status" id="connectionStatus">
<span class="status-dot" id="connectionDot"></span>
<span id="connectionText">Disconnected</span>
</div>
</div>
<div class="requests-container" id="requestsContainer">
<div class="no-requests" id="noRequestsMessage">
<div class="empty-state">
<div class="empty-icon">📝</div>
<p>No pending sign requests</p>
<small>Requests will appear here when received from SigSocket server</small>
</div>
</div>
<div class="requests-list hidden" id="requestsList">
<!-- Requests will be populated here -->
</div>
</div>
<div class="sigsocket-actions">
<button id="refreshRequestsBtn" class="btn btn-ghost btn-small">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path>
</svg>
Refresh
</button>
<button id="sigSocketStatusBtn" class="btn btn-ghost btn-small">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="6" x2="12" y2="12"></line>
<line x1="16" y1="16" x2="12" y2="12"></line>
</svg>
Status
</button>
</div>
</div>
<div class="vault-header">
<h2>Your Keypairs</h2>
<button id="toggleAddKeypairBtn" class="btn btn-primary">

View File

@ -931,4 +931,293 @@ const verifySignature = () => performCryptoOperation({
<span>${text}</span>
</div>`;
}
});
});
// SigSocket functionality
let sigSocketRequests = [];
let sigSocketStatus = { isConnected: false, workspace: null };
// Initialize SigSocket UI elements
const sigSocketElements = {
connectionStatus: document.getElementById('connectionStatus'),
connectionDot: document.getElementById('connectionDot'),
connectionText: document.getElementById('connectionText'),
requestsContainer: document.getElementById('requestsContainer'),
noRequestsMessage: document.getElementById('noRequestsMessage'),
requestsList: document.getElementById('requestsList'),
refreshRequestsBtn: document.getElementById('refreshRequestsBtn'),
sigSocketStatusBtn: document.getElementById('sigSocketStatusBtn')
};
// Add SigSocket event listeners
document.addEventListener('DOMContentLoaded', () => {
// Add SigSocket button listeners
sigSocketElements.refreshRequestsBtn?.addEventListener('click', refreshSigSocketRequests);
sigSocketElements.sigSocketStatusBtn?.addEventListener('click', showSigSocketStatus);
// Load initial SigSocket state
loadSigSocketState();
});
// Listen for messages from background script about SigSocket events
if (backgroundPort) {
backgroundPort.onMessage.addListener((message) => {
if (message.type === 'NEW_SIGN_REQUEST') {
handleNewSignRequest(message);
} else if (message.type === 'REQUESTS_UPDATED') {
updateRequestsList(message.pendingRequests);
} else if (message.type === 'KEYSPACE_UNLOCKED') {
handleKeypaceUnlocked(message);
}
});
}
// Load SigSocket state when popup opens
async function loadSigSocketState() {
try {
// Get SigSocket status
const statusResponse = await sendMessage('getSigSocketStatus');
if (statusResponse?.success) {
updateConnectionStatus(statusResponse.status);
}
// Get pending requests
const requestsResponse = await sendMessage('getPendingSignRequests');
if (requestsResponse?.success) {
updateRequestsList(requestsResponse.requests);
}
} catch (error) {
console.warn('Failed to load SigSocket state:', error);
}
}
// Update connection status display
function updateConnectionStatus(status) {
sigSocketStatus = status;
if (sigSocketElements.connectionDot && sigSocketElements.connectionText) {
if (status.isConnected) {
sigSocketElements.connectionDot.classList.add('connected');
sigSocketElements.connectionText.textContent = `Connected (${status.workspace || 'Unknown'})`;
} else {
sigSocketElements.connectionDot.classList.remove('connected');
sigSocketElements.connectionText.textContent = 'Disconnected';
}
}
}
// Update requests list display
function updateRequestsList(requests) {
sigSocketRequests = requests || [];
if (!sigSocketElements.requestsContainer) return;
if (sigSocketRequests.length === 0) {
sigSocketElements.noRequestsMessage?.classList.remove('hidden');
sigSocketElements.requestsList?.classList.add('hidden');
} else {
sigSocketElements.noRequestsMessage?.classList.add('hidden');
sigSocketElements.requestsList?.classList.remove('hidden');
if (sigSocketElements.requestsList) {
sigSocketElements.requestsList.innerHTML = sigSocketRequests.map(request =>
createRequestItem(request)
).join('');
// Add event listeners to approve/reject buttons
addRequestEventListeners();
}
}
}
// Create HTML for a single request item
function createRequestItem(request) {
const requestTime = new Date(request.timestamp || Date.now()).toLocaleTimeString();
const shortId = request.id.substring(0, 8) + '...';
const decodedMessage = request.message ? atob(request.message) : 'No message';
return `
<div class="request-item" data-request-id="${request.id}">
<div class="request-header">
<div class="request-id" title="${request.id}">${shortId}</div>
<div class="request-time">${requestTime}</div>
</div>
<div class="request-message" title="${decodedMessage}">
${decodedMessage.length > 100 ? decodedMessage.substring(0, 100) + '...' : decodedMessage}
</div>
<div class="request-actions">
<button class="btn-approve" data-request-id="${request.id}">
Approve
</button>
<button class="btn-reject" data-request-id="${request.id}">
Reject
</button>
</div>
</div>
`;
}
// Add event listeners to request action buttons
function addRequestEventListeners() {
// Approve buttons
document.querySelectorAll('.btn-approve').forEach(btn => {
btn.addEventListener('click', async (e) => {
const requestId = e.target.getAttribute('data-request-id');
await approveSignRequest(requestId);
});
});
// Reject buttons
document.querySelectorAll('.btn-reject').forEach(btn => {
btn.addEventListener('click', async (e) => {
const requestId = e.target.getAttribute('data-request-id');
await rejectSignRequest(requestId);
});
});
}
// Handle new sign request notification
function handleNewSignRequest(message) {
// Update requests list
if (message.pendingRequests) {
updateRequestsList(message.pendingRequests);
}
// Show notification if workspace doesn't match
if (!message.canApprove) {
showWorkspaceMismatchWarning();
}
}
// Handle keyspace unlocked event
function handleKeypaceUnlocked(message) {
// Update requests list
if (message.pendingRequests) {
updateRequestsList(message.pendingRequests);
}
// Update button states based on whether requests can be approved
updateRequestButtonStates(message.canApprove);
}
// Show workspace mismatch warning
function showWorkspaceMismatchWarning() {
const existingWarning = document.querySelector('.workspace-mismatch');
if (existingWarning) return; // Don't show multiple warnings
const warning = document.createElement('div');
warning.className = 'workspace-mismatch';
warning.innerHTML = `
Sign requests received for a different workspace.
Switch to the correct workspace to approve requests.
`;
if (sigSocketElements.requestsContainer) {
sigSocketElements.requestsContainer.insertBefore(warning, sigSocketElements.requestsContainer.firstChild);
}
// Auto-remove warning after 10 seconds
setTimeout(() => {
warning.remove();
}, 10000);
}
// Update request button states
function updateRequestButtonStates(canApprove) {
document.querySelectorAll('.btn-approve, .btn-reject').forEach(btn => {
btn.disabled = !canApprove;
});
}
// Approve a sign request
async function approveSignRequest(requestId) {
try {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`);
setButtonLoading(button, true);
const response = await sendMessage('approveSignRequest', { requestId });
if (response?.success) {
showToast('Request approved and signed!', 'success');
await refreshSigSocketRequests();
} else {
throw new Error(getResponseError(response, 'approve request'));
}
} catch (error) {
showToast(`Failed to approve request: ${error.message}`, 'error');
} finally {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-approve`);
setButtonLoading(button, false);
}
}
// Reject a sign request
async function rejectSignRequest(requestId) {
try {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`);
setButtonLoading(button, true);
const response = await sendMessage('rejectSignRequest', {
requestId,
reason: 'User rejected via extension'
});
if (response?.success) {
showToast('Request rejected', 'info');
await refreshSigSocketRequests();
} else {
throw new Error(getResponseError(response, 'reject request'));
}
} catch (error) {
showToast(`Failed to reject request: ${error.message}`, 'error');
} finally {
const button = document.querySelector(`[data-request-id="${requestId}"].btn-reject`);
setButtonLoading(button, false);
}
}
// Refresh SigSocket requests
async function refreshSigSocketRequests() {
try {
setButtonLoading(sigSocketElements.refreshRequestsBtn, true);
const response = await sendMessage('getPendingSignRequests');
if (response?.success) {
updateRequestsList(response.requests);
showToast('Requests refreshed', 'success');
} else {
throw new Error(getResponseError(response, 'refresh requests'));
}
} catch (error) {
showToast(`Failed to refresh requests: ${error.message}`, 'error');
} finally {
setButtonLoading(sigSocketElements.refreshRequestsBtn, false);
}
}
// Show SigSocket status
async function showSigSocketStatus() {
try {
const response = await sendMessage('getSigSocketStatus');
if (response?.success) {
const status = response.status;
const statusText = `
SigSocket Status:
Connected: ${status.isConnected ? 'Yes' : 'No'}
Workspace: ${status.workspace || 'None'}
Public Key: ${status.publicKey ? status.publicKey.substring(0, 16) + '...' : 'None'}
Pending Requests: ${status.pendingRequestCount || 0}
Server URL: ${status.serverUrl}
`.trim();
showToast(statusText, 'info');
updateConnectionStatus(status);
} else {
throw new Error(getResponseError(response, 'get status'));
}
} catch (error) {
showToast(`Failed to get status: ${error.message}`, 'error');
}
}

View File

@ -0,0 +1,443 @@
/**
* Sign Request Manager Component
*
* Handles the display and management of SigSocket sign requests in the popup.
* Manages different UI states:
* 1. Keyspace locked: Show unlock form
* 2. Wrong keyspace: Show mismatch message
* 3. Correct keyspace: Show approval UI
*/
class SignRequestManager {
constructor() {
this.pendingRequests = [];
this.isKeypaceUnlocked = false;
this.keypaceMatch = false;
this.connectionStatus = { isConnected: false };
this.container = null;
this.initialized = false;
}
/**
* Initialize the component
* @param {HTMLElement} container - Container element to render into
*/
async initialize(container) {
this.container = container;
this.initialized = true;
// Load initial state
await this.loadState();
// Render initial UI
this.render();
// Set up event listeners
this.setupEventListeners();
// Listen for background messages
this.setupBackgroundListener();
}
/**
* Load current state from background script
*/
async loadState() {
try {
// Check if keyspace is unlocked
const unlockedResponse = await this.sendMessage('isUnlocked');
this.isKeypaceUnlocked = unlockedResponse?.unlocked || false;
// Get pending requests
const requestsResponse = await this.sendMessage('getPendingRequests');
this.pendingRequests = requestsResponse?.requests || [];
// Get SigSocket status
const statusResponse = await this.sendMessage('getSigSocketStatus');
this.connectionStatus = statusResponse?.status || { isConnected: false };
// If keyspace is unlocked, notify background to check keyspace match
if (this.isKeypaceUnlocked) {
await this.sendMessage('keypaceUnlocked');
}
} catch (error) {
console.error('Failed to load sign request state:', error);
}
}
/**
* Render the component UI
*/
render() {
if (!this.container) return;
const hasRequests = this.pendingRequests.length > 0;
if (!hasRequests) {
this.renderNoRequests();
return;
}
if (!this.isKeypaceUnlocked) {
this.renderUnlockPrompt();
return;
}
if (!this.keypaceMatch) {
this.renderKeypaceMismatch();
return;
}
this.renderApprovalUI();
}
/**
* Render no requests state
*/
renderNoRequests() {
this.container.innerHTML = `
<div class="sign-request-manager">
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
<span class="status-indicator"></span>
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
</div>
<div class="no-requests">
<p>No pending sign requests</p>
</div>
</div>
`;
}
/**
* Render unlock prompt
*/
renderUnlockPrompt() {
const requestCount = this.pendingRequests.length;
this.container.innerHTML = `
<div class="sign-request-manager">
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
<span class="status-indicator"></span>
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
</div>
<div class="unlock-prompt">
<h3>🔒 Unlock Keyspace</h3>
<p>Unlock your keyspace to see ${requestCount} pending sign request${requestCount !== 1 ? 's' : ''}.</p>
<p class="hint">Use the login form above to unlock your keyspace.</p>
</div>
</div>
`;
}
/**
* Render keyspace mismatch message
*/
renderKeypaceMismatch() {
this.container.innerHTML = `
<div class="sign-request-manager">
<div class="connection-status ${this.connectionStatus.isConnected ? 'connected' : 'disconnected'}">
<span class="status-indicator"></span>
SigSocket: ${this.connectionStatus.isConnected ? 'Connected' : 'Disconnected'}
</div>
<div class="keyspace-mismatch">
<h3> Wrong Keyspace</h3>
<p>The unlocked keyspace doesn't match the connected SigSocket session.</p>
<p class="hint">Please unlock the correct keyspace to approve sign requests.</p>
</div>
</div>
`;
}
/**
* Render approval UI with pending requests
*/
renderApprovalUI() {
const requestsHtml = this.pendingRequests.map(request => this.renderSignRequestCard(request)).join('');
this.container.innerHTML = `
<div class="sign-request-manager">
<div class="connection-status connected">
<span class="status-indicator"></span>
SigSocket: Connected
</div>
<div class="requests-header">
<h3>📝 Sign Requests (${this.pendingRequests.length})</h3>
</div>
<div class="requests-list">
${requestsHtml}
</div>
</div>
`;
}
/**
* Render individual sign request card
* @param {Object} request - Sign request data
* @returns {string} - HTML string for the request card
*/
renderSignRequestCard(request) {
const timestamp = new Date(request.timestamp).toLocaleTimeString();
const messagePreview = this.getMessagePreview(request.message);
return `
<div class="sign-request-card" data-request-id="${request.id}">
<div class="request-header">
<div class="request-id">Request: ${request.id.substring(0, 8)}...</div>
<div class="request-time">${timestamp}</div>
</div>
<div class="request-message">
<label>Message:</label>
<div class="message-content">
<div class="message-preview">${messagePreview}</div>
<button class="expand-message" data-request-id="${request.id}">
<span class="expand-text">Show Full</span>
</button>
</div>
</div>
<div class="request-actions">
<button class="btn-reject" data-request-id="${request.id}">
Reject
</button>
<button class="btn-approve" data-request-id="${request.id}">
Approve & Sign
</button>
</div>
</div>
`;
}
/**
* Get a preview of the message content
* @param {string} messageBase64 - Base64 encoded message
* @returns {string} - Preview text
*/
getMessagePreview(messageBase64) {
try {
const decoded = atob(messageBase64);
const preview = decoded.length > 50 ? decoded.substring(0, 50) + '...' : decoded;
return preview;
} catch (error) {
return `Base64: ${messageBase64.substring(0, 20)}...`;
}
}
/**
* Set up event listeners
*/
setupEventListeners() {
if (!this.container) return;
// Use event delegation for dynamic content
this.container.addEventListener('click', (e) => {
const target = e.target;
if (target.classList.contains('btn-approve')) {
const requestId = target.getAttribute('data-request-id');
this.approveRequest(requestId);
} else if (target.classList.contains('btn-reject')) {
const requestId = target.getAttribute('data-request-id');
this.rejectRequest(requestId);
} else if (target.classList.contains('expand-message')) {
const requestId = target.getAttribute('data-request-id');
this.toggleMessageExpansion(requestId);
}
});
}
/**
* Set up listener for background script messages
*/
setupBackgroundListener() {
// Listen for keyspace unlock events
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'KEYSPACE_UNLOCKED') {
this.isKeypaceUnlocked = true;
this.keypaceMatch = message.keypaceMatches;
this.pendingRequests = message.pendingRequests || [];
this.render();
}
});
}
/**
* Approve a sign request
* @param {string} requestId - Request ID to approve
*/
async approveRequest(requestId) {
try {
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`);
if (button) {
button.disabled = true;
button.textContent = 'Signing...';
}
const response = await this.sendMessage('approveSignRequest', { requestId });
if (response?.success) {
// Remove the request from UI
this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId);
this.render();
// Show success message
this.showToast('Sign request approved successfully!', 'success');
} else {
throw new Error(response?.error || 'Failed to approve request');
}
} catch (error) {
console.error('Failed to approve request:', error);
this.showToast('Failed to approve request: ' + error.message, 'error');
// Re-enable button
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-approve`);
if (button) {
button.disabled = false;
button.textContent = '✅ Approve & Sign';
}
}
}
/**
* Reject a sign request
* @param {string} requestId - Request ID to reject
*/
async rejectRequest(requestId) {
try {
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`);
if (button) {
button.disabled = true;
button.textContent = 'Rejecting...';
}
const response = await this.sendMessage('rejectSignRequest', {
requestId,
reason: 'User rejected'
});
if (response?.success) {
// Remove the request from UI
this.pendingRequests = this.pendingRequests.filter(r => r.id !== requestId);
this.render();
// Show success message
this.showToast('Sign request rejected', 'info');
} else {
throw new Error(response?.error || 'Failed to reject request');
}
} catch (error) {
console.error('Failed to reject request:', error);
this.showToast('Failed to reject request: ' + error.message, 'error');
// Re-enable button
const button = this.container.querySelector(`[data-request-id="${requestId}"].btn-reject`);
if (button) {
button.disabled = false;
button.textContent = '❌ Reject';
}
}
}
/**
* Toggle message expansion
* @param {string} requestId - Request ID
*/
toggleMessageExpansion(requestId) {
const request = this.pendingRequests.find(r => r.id === requestId);
if (!request) return;
const card = this.container.querySelector(`[data-request-id="${requestId}"]`);
const messageContent = card.querySelector('.message-content');
const expandButton = card.querySelector('.expand-message');
const isExpanded = messageContent.classList.contains('expanded');
if (isExpanded) {
messageContent.classList.remove('expanded');
messageContent.querySelector('.message-preview').textContent = this.getMessagePreview(request.message);
expandButton.querySelector('.expand-text').textContent = 'Show Full';
} else {
messageContent.classList.add('expanded');
try {
const fullMessage = atob(request.message);
messageContent.querySelector('.message-preview').textContent = fullMessage;
} catch (error) {
messageContent.querySelector('.message-preview').textContent = `Base64: ${request.message}`;
}
expandButton.querySelector('.expand-text').textContent = 'Show Less';
}
}
/**
* Send message to background script
* @param {string} action - Action to perform
* @param {Object} data - Additional data
* @returns {Promise<Object>} - Response from background script
*/
async sendMessage(action, data = {}) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ action, ...data }, resolve);
});
}
/**
* Show toast notification
* @param {string} message - Message to show
* @param {string} type - Toast type (success, error, info)
*/
showToast(message, type = 'info') {
// Use the existing toast system from popup.js
if (typeof showToast === 'function') {
showToast(message, type);
} else {
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
/**
* Update component state
* @param {Object} newState - New state data
*/
updateState(newState) {
console.log('SignRequestManager.updateState called with:', newState);
console.log('Current state before update:', {
isKeypaceUnlocked: this.isKeypaceUnlocked,
keypaceMatch: this.keypaceMatch,
pendingRequests: this.pendingRequests.length
});
Object.assign(this, newState);
// Fix the property name mismatch
if (newState.keypaceMatches !== undefined) {
this.keypaceMatch = newState.keypaceMatches;
}
console.log('State after update:', {
isKeypaceUnlocked: this.isKeypaceUnlocked,
keypaceMatch: this.keypaceMatch,
pendingRequests: this.pendingRequests.length
});
if (this.initialized) {
console.log('Rendering SignRequestManager with new state');
this.render();
}
}
/**
* Refresh component data
*/
async refresh() {
await this.loadState();
this.render();
}
}
// Export for use in popup
if (typeof module !== 'undefined' && module.exports) {
module.exports = SignRequestManager;
} else {
window.SignRequestManager = SignRequestManager;
}

View File

@ -1069,4 +1069,183 @@ input::placeholder, textarea::placeholder {
.verification-icon svg {
width: 20px;
height: 20px;
}
/* SigSocket Requests Styles */
.sigsocket-section {
margin-bottom: 20px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.section-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.connection-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent-error);
transition: background-color 0.3s ease;
}
.status-dot.connected {
background: var(--accent-success);
}
.requests-container {
min-height: 80px;
}
.empty-state {
text-align: center;
padding: 20px;
color: var(--text-secondary);
}
.empty-icon {
font-size: 24px;
margin-bottom: 8px;
}
.empty-state p {
margin: 0 0 4px 0;
font-weight: 500;
}
.empty-state small {
font-size: 11px;
opacity: 0.8;
}
.request-item {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
background: var(--bg-secondary);
transition: all 0.2s ease;
}
.request-item:hover {
border-color: var(--border-focus);
box-shadow: 0 2px 8px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.1);
}
.request-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.request-id {
font-family: 'Courier New', monospace;
font-size: 11px;
color: var(--text-secondary);
background: var(--bg-input);
padding: 2px 6px;
border-radius: 4px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
}
.request-time {
font-size: 11px;
color: var(--text-secondary);
}
.request-message {
margin: 8px 0;
padding: 8px;
background: var(--bg-input);
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 11px;
word-break: break-all;
max-height: 60px;
overflow-y: auto;
}
.request-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.btn-approve {
background: var(--accent-success);
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-approve:hover {
background: hsl(var(--accent-hue), 65%, 40%);
}
.btn-reject {
background: var(--accent-error);
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-reject:hover {
background: hsl(0, 70%, 50%);
}
.btn-approve:disabled,
.btn-reject:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.sigsocket-actions {
display: flex;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border-color);
}
.workspace-mismatch {
background: hsla(35, 85%, 85%, 0.8);
border: 1px solid hsla(35, 85%, 70%, 0.5);
color: hsl(35, 70%, 30%);
padding: 8px;
border-radius: 4px;
font-size: 12px;
margin-bottom: 8px;
}
[data-theme="dark"] .workspace-mismatch {
background: hsla(35, 60%, 15%, 0.8);
border-color: hsla(35, 60%, 30%, 0.5);
color: hsl(35, 70%, 70%);
}

View File

@ -0,0 +1,114 @@
# Testing the SigSocket Browser Extension
## Prerequisites
1. **SigSocket Server**: You need a running SigSocket server at `ws://localhost:8080/ws`
2. **Browser**: Chrome or Chromium-based browser with developer mode enabled
## Test Steps
### 1. Load the Extension
1. Open Chrome and go to `chrome://extensions/`
2. Enable "Developer mode" in the top right
3. Click "Load unpacked" and select the `crypto_vault_extension` directory
4. The CryptoVault extension should appear in your extensions list
### 2. Basic Functionality Test
1. Click the CryptoVault extension icon in the toolbar
2. Create a new keyspace:
- Enter a keyspace name (e.g., "test-workspace")
- Enter a password
- Click "Create New"
3. The extension should automatically connect to the SigSocket server
4. Add a keypair:
- Click "Add Keypair"
- Enter a name for the keypair
- Click "Create Keypair"
### 3. SigSocket Integration Test
1. **Check Connection Status**:
- Look for the SigSocket connection status at the bottom of the popup
- It should show "SigSocket: Connected" with a green indicator
2. **Test Sign Request Flow**:
- Send a sign request to the SigSocket server (you'll need to implement this on the server side)
- The extension should show a notification
- The extension badge should show the number of pending requests
- Open the extension popup to see the sign request
3. **Test Approval Flow**:
- If keyspace is locked, you should see "Unlock keyspace to see X pending requests"
- Unlock the keyspace using the login form
- You should see the sign request details
- Click "Approve & Sign" to approve the request
- The request should be signed and sent back to the server
### 4. Settings Test
1. Click the settings gear icon in the extension popup
2. Change the SigSocket server URL if needed
3. Adjust the session timeout if desired
## Expected Behavior
- ✅ Extension loads without errors
- ✅ Can create keyspaces and keypairs
- ✅ SigSocket connection is established automatically
- ✅ Sign requests are received and displayed
- ✅ Approval flow works correctly
- ✅ Settings can be configured
## Troubleshooting
### Common Issues
1. **Extension won't load**: Check the console for JavaScript errors
2. **SigSocket won't connect**: Verify the server is running and the URL is correct
3. **WASM errors**: Check that the WASM files are properly built and copied
4. **Sign requests not appearing**: Check the browser console for callback errors
### Debug Steps
1. Open Chrome DevTools
2. Go to the Extensions tab
3. Find CryptoVault and click "Inspect views: background page"
4. Check the console for any errors
5. Also inspect the popup by right-clicking the extension icon and selecting "Inspect popup"
## Server-Side Testing
To fully test the extension, you'll need a SigSocket server that can:
1. Accept WebSocket connections at `/ws`
2. Handle client introduction messages (hex-encoded public keys)
3. Send sign requests in the format:
```json
{
"id": "unique-request-id",
"message": "base64-encoded-message"
}
```
4. Receive sign responses in the format:
```json
{
"id": "request-id",
"message": "base64-encoded-message",
"signature": "base64-encoded-signature"
}
```
## Next Steps
If basic functionality works:
1. Test with multiple concurrent sign requests
2. Test connection recovery after network issues
3. Test with different keyspace configurations
4. Test the rejection flow
5. Test session timeout behavior

View File

@ -277,6 +277,42 @@ export function is_unlocked() {
return ret !== 0;
}
/**
* Get the default public key for a workspace (keyspace)
* This returns the public key of the first keypair in the keyspace
* @param {string} workspace_id
* @returns {Promise<any>}
*/
export function get_workspace_default_public_key(workspace_id) {
const ptr0 = passStringToWasm0(workspace_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.get_workspace_default_public_key(ptr0, len0);
return ret;
}
/**
* Get the current unlocked public key as hex string
* @returns {string}
*/
export function get_current_unlocked_public_key() {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.get_current_unlocked_public_key();
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* Get all keypairs from the current session
* Returns an array of keypair objects with id, type, and metadata
@ -323,7 +359,7 @@ function passArray8ToWasm0(arg, malloc) {
return ptr;
}
/**
* Sign message with current session
* Sign message with current session (requires selected keypair)
* @param {Uint8Array} message
* @returns {Promise<any>}
*/
@ -334,6 +370,41 @@ export function sign(message) {
return ret;
}
/**
* Get the current keyspace name
* @returns {string}
*/
export function get_current_keyspace_name() {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.get_current_keyspace_name();
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* Sign message with default keypair (first keypair in keyspace) without changing session state
* @param {Uint8Array} message
* @returns {Promise<any>}
*/
export function sign_with_default_keypair(message) {
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.sign_with_default_keypair(ptr0, len0);
return ret;
}
/**
* Verify a signature with the current session's selected keypair
* @param {Uint8Array} message
@ -395,24 +466,391 @@ export function run_rhai(script) {
return takeFromExternrefTable0(ret[0]);
}
function __wbg_adapter_32(arg0, arg1, arg2) {
wasm.closure121_externref_shim(arg0, arg1, arg2);
function __wbg_adapter_34(arg0, arg1, arg2) {
wasm.closure174_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_35(arg0, arg1, arg2) {
wasm.closure150_externref_shim(arg0, arg1, arg2);
function __wbg_adapter_39(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha4436a3f79fb1a0f(arg0, arg1);
}
function __wbg_adapter_38(arg0, arg1, arg2) {
wasm.closure227_externref_shim(arg0, arg1, arg2);
function __wbg_adapter_44(arg0, arg1, arg2) {
wasm.closure237_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_138(arg0, arg1, arg2, arg3) {
wasm.closure1879_externref_shim(arg0, arg1, arg2, arg3);
function __wbg_adapter_49(arg0, arg1) {
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf148c54a4a246cea(arg0, arg1);
}
function __wbg_adapter_52(arg0, arg1, arg2) {
wasm.closure308_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_55(arg0, arg1, arg2) {
wasm.closure392_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_207(arg0, arg1, arg2, arg3) {
wasm.closure2046_externref_shim(arg0, arg1, arg2, arg3);
}
const __wbindgen_enum_BinaryType = ["blob", "arraybuffer"];
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
const SigSocketConnectionFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_sigsocketconnection_free(ptr >>> 0, 1));
/**
* 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
*/
export class SigSocketConnection {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
SigSocketConnectionFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_sigsocketconnection_free(ptr, 0);
}
/**
* Create a new SigSocket connection
*/
constructor() {
const ret = wasm.sigsocketconnection_new();
this.__wbg_ptr = ret >>> 0;
SigSocketConnectionFinalization.register(this, this.__wbg_ptr, this);
return this;
}
/**
* 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
* @param {string} server_url
* @param {string} public_key_hex
* @returns {Promise<void>}
*/
connect(server_url, public_key_hex) {
const ptr0 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(public_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketconnection_connect(this.__wbg_ptr, ptr0, len0, ptr1, len1);
return ret;
}
/**
* 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
* @param {string} request_id
* @param {string} message_base64
* @param {string} signature_hex
* @returns {Promise<void>}
*/
send_response(request_id, message_base64, signature_hex) {
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(message_base64, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passStringToWasm0(signature_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len2 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketconnection_send_response(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
return ret;
}
/**
* 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
* @param {string} request_id
* @param {string} reason
* @returns {Promise<void>}
*/
send_rejection(request_id, reason) {
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketconnection_send_rejection(this.__wbg_ptr, ptr0, len0, ptr1, len1);
return ret;
}
/**
* Disconnect from the SigSocket server
*/
disconnect() {
wasm.sigsocketconnection_disconnect(this.__wbg_ptr);
}
/**
* Check if connected to the server
* @returns {boolean}
*/
is_connected() {
const ret = wasm.sigsocketconnection_is_connected(this.__wbg_ptr);
return ret !== 0;
}
}
const SigSocketManagerFinalization = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(ptr => wasm.__wbg_sigsocketmanager_free(ptr >>> 0, 1));
/**
* SigSocket manager for high-level operations
*/
export class SigSocketManager {
__destroy_into_raw() {
const ptr = this.__wbg_ptr;
this.__wbg_ptr = 0;
SigSocketManagerFinalization.unregister(this);
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_sigsocketmanager_free(ptr, 0);
}
/**
* Connect to SigSocket server with smart connection management
*
* This handles all connection logic:
* - Reuses existing connection if same workspace
* - Switches connection if different workspace
* - Creates new connection if none exists
*
* # 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
* @param {string} workspace
* @param {string} server_url
* @param {Function} event_callback
* @returns {Promise<string>}
*/
static connect_workspace_with_events(workspace, server_url, event_callback) {
const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketmanager_connect_workspace_with_events(ptr0, len0, ptr1, len1, event_callback);
return ret;
}
/**
* 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
* @param {string} workspace
* @param {string} server_url
* @returns {Promise<string>}
*/
static connect_workspace(workspace, server_url) {
const ptr0 = passStringToWasm0(workspace, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(server_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketmanager_connect_workspace(ptr0, len0, ptr1, len1);
return ret;
}
/**
* Disconnect from SigSocket server
*
* # Returns
* * `Ok(())` - Successfully disconnected
* * `Err(error)` - If disconnect failed
* @returns {Promise<void>}
*/
static disconnect() {
const ret = wasm.sigsocketmanager_disconnect();
return ret;
}
/**
* 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
* @param {string} request_id
* @returns {Promise<boolean>}
*/
static can_approve_request(request_id) {
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketmanager_can_approve_request(ptr0, len0);
return ret;
}
/**
* 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
* @param {string} request_id
* @returns {Promise<string>}
*/
static approve_request(request_id) {
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketmanager_approve_request(ptr0, len0);
return ret;
}
/**
* 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
* @param {string} request_id
* @param {string} reason
* @returns {Promise<void>}
*/
static reject_request(request_id, reason) {
const ptr0 = passStringToWasm0(request_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(reason, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketmanager_reject_request(ptr0, len0, ptr1, len1);
return ret;
}
/**
* 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
* @returns {Promise<string>}
*/
static get_filtered_requests() {
const ret = wasm.sigsocketmanager_get_filtered_requests();
return ret;
}
/**
* 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
* @param {string} request_json
*/
static add_pending_request(request_json) {
const ptr0 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.sigsocketmanager_add_pending_request(ptr0, len0);
if (ret[1]) {
throw takeFromExternrefTable0(ret[0]);
}
}
/**
* Get connection status
*
* # Returns
* * `Ok(status_json)` - JSON object with connection status
* * `Err(error)` - If getting status failed
* @returns {string}
*/
static get_connection_status() {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.sigsocketmanager_get_connection_status();
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* Clear all pending requests
*
* # Returns
* * `Ok(())` - Requests cleared successfully
*/
static clear_pending_requests() {
const ret = wasm.sigsocketmanager_clear_pending_requests();
if (ret[1]) {
throw takeFromExternrefTable0(ret[0]);
}
}
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
@ -459,6 +897,9 @@ function __wbg_get_imports() {
const ret = arg0.call(arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_close_2893b7d056a0627d = function() { return handleError(function (arg0) {
arg0.close();
}, arguments) };
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
return ret;
@ -467,6 +908,10 @@ function __wbg_get_imports() {
const ret = arg0.crypto;
return ret;
};
imports.wbg.__wbg_data_432d9c3df2630942 = function(arg0) {
const ret = arg0.data;
return ret;
};
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
console.error(arg0);
};
@ -539,10 +984,23 @@ function __wbg_get_imports() {
const ret = result;
return ret;
};
imports.wbg.__wbg_instanceof_Window_def73ea0955fc569 = function(arg0) {
let result;
try {
result = arg0 instanceof Window;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
const ret = arg0.length;
return ret;
};
imports.wbg.__wbg_log_c222819a41e063d3 = function(arg0) {
console.log(arg0);
};
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
const ret = arg0.msCrypto;
return ret;
@ -558,7 +1016,7 @@ function __wbg_get_imports() {
const a = state0.a;
state0.a = 0;
try {
return __wbg_adapter_138(a, state0.b, arg0, arg1);
return __wbg_adapter_207(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
@ -577,6 +1035,10 @@ function __wbg_get_imports() {
const ret = new Array();
return ret;
};
imports.wbg.__wbg_new_92c54fc74574ef55 = function() { return handleError(function (arg0, arg1) {
const ret = new WebSocket(getStringFromWasm0(arg0, arg1));
return ret;
}, arguments) };
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
const ret = new Uint8Array(arg0);
return ret;
@ -609,6 +1071,12 @@ function __wbg_get_imports() {
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
return ret;
}, arguments) };
imports.wbg.__wbg_onConnectionStateChanged_b0dc098522afadba = function(arg0) {
onConnectionStateChanged(arg0 !== 0);
};
imports.wbg.__wbg_onSignRequestReceived_93232ba7a0919705 = function(arg0, arg1, arg2, arg3) {
onSignRequestReceived(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
};
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
return ret;
@ -643,6 +1111,10 @@ function __wbg_get_imports() {
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
arg0.randomFillSync(arg1);
}, arguments) };
imports.wbg.__wbg_readyState_7ef6e63c349899ed = function(arg0) {
const ret = arg0.readyState;
return ret;
};
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
const ret = module.require;
return ret;
@ -655,12 +1127,38 @@ function __wbg_get_imports() {
const ret = arg0.result;
return ret;
}, arguments) };
imports.wbg.__wbg_send_0293179ba074ffb4 = function() { return handleError(function (arg0, arg1, arg2) {
arg0.send(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_setTimeout_f2fe5af8e3debeb3 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.setTimeout(arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
arg0.set(arg1, arg2 >>> 0);
};
imports.wbg.__wbg_set_bb8cecf6a62b9f46 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = Reflect.set(arg0, arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_setbinaryType_92fa1ffd873b327c = function(arg0, arg1) {
arg0.binaryType = __wbindgen_enum_BinaryType[arg1];
};
imports.wbg.__wbg_setonclose_14fc475a49d488fc = function(arg0, arg1) {
arg0.onclose = arg1;
};
imports.wbg.__wbg_setonerror_8639efe354b947cd = function(arg0, arg1) {
arg0.onerror = arg1;
};
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
arg0.onerror = arg1;
};
imports.wbg.__wbg_setonmessage_6eccab530a8fb4c7 = function(arg0, arg1) {
arg0.onmessage = arg1;
};
imports.wbg.__wbg_setonopen_2da654e1f39745d5 = function(arg0, arg1) {
arg0.onopen = arg1;
};
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
arg0.onsuccess = arg1;
};
@ -695,6 +1193,10 @@ function __wbg_get_imports() {
const ret = arg0.then(arg1);
return ret;
};
imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) {
const ret = arg0.then(arg1, arg2);
return ret;
};
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
return ret;
@ -703,6 +1205,9 @@ function __wbg_get_imports() {
const ret = arg0.versions;
return ret;
};
imports.wbg.__wbg_warn_4ca3906c248c47c4 = function(arg0) {
console.warn(arg0);
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = arg0.original;
if (obj.cnt-- == 1) {
@ -712,16 +1217,40 @@ function __wbg_get_imports() {
const ret = false;
return ret;
};
imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 122, __wbg_adapter_32);
imports.wbg.__wbindgen_closure_wrapper1015 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 309, __wbg_adapter_52);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper549 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 151, __wbg_adapter_35);
imports.wbg.__wbindgen_closure_wrapper1320 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 393, __wbg_adapter_55);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper857 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 228, __wbg_adapter_38);
imports.wbg.__wbindgen_closure_wrapper423 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper424 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper425 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_39);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper428 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper766 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper767 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_44);
return ret;
};
imports.wbg.__wbindgen_closure_wrapper770 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 238, __wbg_adapter_49);
return ret;
};
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
@ -778,6 +1307,14 @@ function __wbg_get_imports() {
const ret = wasm.memory;
return ret;
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = arg1;
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return ret;

View File

@ -68,7 +68,7 @@ impl EvmClient {
mut tx: provider::Transaction,
signer: &dyn crate::signer::Signer,
) -> Result<ethers_core::types::H256, EvmError> {
use ethers_core::types::{U256, H256, Bytes, Address};
use ethers_core::types::{U256, H256};
use std::str::FromStr;
use serde_json::json;
use crate::provider::{send_rpc, parse_signature_rs_v};
@ -131,7 +131,7 @@ impl EvmClient {
// 3. Sign the RLP-encoded unsigned transaction
let sig = signer.sign(&rlp_unsigned).await?;
let (r, s, v) = parse_signature_rs_v(&sig, tx.chain_id.unwrap()).ok_or_else(|| EvmError::Signing("Invalid signature format".to_string()))?;
let (r, s, _v) = parse_signature_rs_v(&sig, tx.chain_id.unwrap()).ok_or_else(|| EvmError::Signing("Invalid signature format".to_string()))?;
// 4. RLP encode signed transaction (EIP-155)
use rlp::RlpStream;

View File

@ -1,7 +1,7 @@
//! Rhai bindings for EVM Client module
//! Provides a single source of truth for scripting integration for EVM actions.
use rhai::{Engine, Map};
use rhai::Engine;
pub use crate::EvmClient; // Ensure EvmClient is public and defined in lib.rs
/// Register EVM Client APIs with the Rhai scripting engine.
@ -25,7 +25,7 @@ pub fn register_rhai_api(engine: &mut Engine, evm_client: std::sync::Arc<EvmClie
engine.register_type::<RhaiEvmClient>();
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
// Register instance for scripts
let rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
let _rhai_ec = RhaiEvmClient { inner: evm_client.clone() };
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.
}

View File

@ -2,7 +2,6 @@
//! These use block_on for native, and should be adapted for WASM as needed.
use crate::EvmClient;
use rhai::Map;
#[cfg(not(target_arch = "wasm32"))]
use tokio::runtime::Handle;

View File

@ -37,7 +37,6 @@
#[cfg(not(target_arch = "wasm32"))]
#[tokio::test]
async fn test_get_balance_real_address() {
use ethers_core::types::{Address, U256};
use evm_client::provider::get_balance;
// Vitalik's address

1
hero_vault_extension/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -130,6 +130,7 @@ store.put(&js_value, Some(&JsValue::from_str(key)))?.await
#[cfg(not(target_arch = "wasm32"))]
pub struct WasmStore;
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl KVStore for WasmStore {
@ -139,10 +140,16 @@ impl KVStore for WasmStore {
async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
}
async fn delete(&self, _key: &str) -> Result<()> {
async fn remove(&self, _key: &str) -> Result<()> {
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
}
async fn exists(&self, _key: &str) -> Result<bool> {
async fn contains_key(&self, _key: &str) -> Result<bool> {
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
}
async fn keys(&self) -> Result<Vec<String>> {
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
}
async fn clear(&self) -> Result<()> {
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
}
}

View File

@ -0,0 +1,53 @@
[package]
name = "sigsocket_client"
version = "0.1.0"
edition = "2021"
description = "WebSocket client for sigsocket server with WASM-first support"
license = "MIT OR Apache-2.0"
repository = "https://git.ourworld.tf/samehabouelsaad/sal-modular"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
# Core dependencies (both native and WASM)
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
log = "0.4"
hex = "0.4"
base64 = "0.21"
url = "2.5"
async-trait = "0.1"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
# Native-only dependencies
tokio = { version = "1.0", features = ["full"] }
tokio-tungstenite = "0.21"
futures-util = "0.3"
thiserror = "1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
# WASM-only dependencies
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3", features = [
"console",
"WebSocket",
"MessageEvent",
"Event",
"BinaryType",
"CloseEvent",
"ErrorEvent",
"Window",
] }
js-sys = "0.3"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { version = "1.0", features = ["full"] }
env_logger = "0.10"
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3"
console_error_panic_hook = "0.1"

View File

@ -0,0 +1,214 @@
# SigSocket Client Implementation
## Overview
This document describes the implementation of the `sigsocket_client` crate, a WebSocket client library designed for connecting to sigsocket servers with **WASM-first support**.
## Architecture
### Core Design Principles
1. **WASM-First**: Designed primarily for browser environments with native support as a secondary target
2. **No Signing Logic**: The client delegates all signing operations to the application
3. **User Approval Flow**: Applications are notified about incoming requests and handle user approval
4. **Protocol Compatibility**: Fully compatible with the sigsocket server protocol
5. **Async/Await**: Modern async Rust API throughout
### Module Structure
```
sigsocket_client/
├── src/
│ ├── lib.rs # Main library entry point
│ ├── error.rs # Error types (native + WASM versions)
│ ├── protocol.rs # Protocol message definitions
│ ├── client.rs # Main client interface
│ ├── native.rs # Native (tokio) implementation
│ └── wasm.rs # WASM (web-sys) implementation
├── examples/
│ ├── basic_usage.rs # Native usage example
│ └── wasm_usage.rs # WASM usage example
├── tests/
│ └── integration_test.rs
└── README.md
```
## Protocol Implementation
The sigsocket protocol is simple and consists of three message types:
### 1. Introduction Message
When connecting, the client sends its public key as a hex-encoded string:
```
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9
```
### 2. Sign Request (Server → Client)
```json
{
"id": "req_123",
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
}
```
### 3. Sign Response (Client → Server)
```json
{
"id": "req_123",
"message": "dGVzdCBtZXNzYWdl", // original message
"signature": "c2lnbmF0dXJl" // base64-encoded signature
}
```
## Key Features Implemented
### ✅ Dual Platform Support
- **Native**: Uses `tokio` and `tokio-tungstenite` for async WebSocket communication
- **WASM**: Uses `web-sys` and `wasm-bindgen` for browser WebSocket API
### ✅ Type-Safe Protocol
- `SignRequest` and `SignResponse` structs with serde serialization
- Helper methods for base64 encoding/decoding
- Comprehensive error handling
### ✅ Flexible Sign Handler Interface
```rust
trait SignRequestHandler {
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
}
```
### ✅ Connection Management
- Automatic connection state tracking
- Clean disconnect handling
- Connection status queries
### ✅ Error Handling
- Comprehensive error types for different failure modes
- Platform-specific error conversions
- WASM-compatible error handling (no `std::error::Error` dependency)
## Platform-Specific Implementations
### Native Implementation (`native.rs`)
- Uses `tokio-tungstenite` for WebSocket communication
- Spawns separate tasks for reading and writing
- Thread-safe with `Arc<RwLock<T>>` for shared state
- Supports `Send + Sync` trait bounds
### WASM Implementation (`wasm.rs`)
- Uses `web-sys::WebSocket` for browser WebSocket API
- Event-driven with JavaScript closures
- Single-threaded (no `Send + Sync` requirements)
- Browser console logging for debugging
## Usage Patterns
### Native Usage
```rust
#[tokio::main]
async fn main() -> Result<()> {
let public_key = hex::decode("02f9308a...")?;
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
client.set_sign_handler(MySignHandler);
client.connect().await?;
// Client handles requests automatically
Ok(())
}
```
### WASM Usage
```rust
#[wasm_bindgen]
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
let public_key = get_user_public_key()?;
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
client.set_sign_handler(WasmSignHandler);
client.connect().await?;
Ok(())
}
```
## Testing
### Unit Tests
- Protocol message serialization/deserialization
- Error handling and conversion
- Client creation and configuration
### Integration Tests
- End-to-end usage patterns
- Sign request/response cycles
- Error scenarios
### Documentation Tests
- Example code in documentation is verified to compile
## Dependencies
### Core Dependencies (Both Platforms)
- `serde` + `serde_json` - JSON serialization
- `hex` - Hex encoding/decoding
- `base64` - Base64 encoding/decoding
- `url` - URL parsing and validation
### Native-Only Dependencies
- `tokio` - Async runtime
- `tokio-tungstenite` - WebSocket client
- `futures-util` - Stream utilities
- `thiserror` - Error derive macros
### WASM-Only Dependencies
- `wasm-bindgen` - Rust/JavaScript interop
- `web-sys` - Browser API bindings
- `js-sys` - JavaScript type bindings
- `wasm-bindgen-futures` - Async support
## Build Targets
### Native Build
```bash
cargo build --features native
cargo test --features native
cargo run --example basic_usage --features native
```
### WASM Build
```bash
cargo check --target wasm32-unknown-unknown --features wasm
wasm-pack build --target web --features wasm
```
## Future Enhancements
### Potential Improvements
1. **Reconnection Logic**: Automatic reconnection with exponential backoff
2. **Request Queuing**: Queue multiple concurrent sign requests
3. **Timeout Handling**: Configurable timeouts for requests
4. **Metrics**: Connection and request metrics
5. **Logging**: Structured logging with configurable levels
### WASM Enhancements
1. **Better Callback System**: More ergonomic callback handling in WASM
2. **Browser Wallet Integration**: Direct integration with MetaMask, etc.
3. **Service Worker Support**: Background request handling
## Security Considerations
1. **No Private Key Storage**: The client never handles private keys
2. **User Approval Required**: All signing requires explicit user approval
3. **Message Validation**: All incoming messages are validated
4. **Secure Transport**: Requires WebSocket Secure (WSS) in production
## Compatibility
- **Rust Version**: 1.70+
- **WASM Target**: `wasm32-unknown-unknown`
- **Browser Support**: Modern browsers with WebSocket support
- **Server Compatibility**: Compatible with sigsocket server protocol
This implementation provides a solid foundation for applications that need to connect to sigsocket servers while maintaining security and user control over signing operations.

218
sigsocket_client/README.md Normal file
View File

@ -0,0 +1,218 @@
# SigSocket Client
A WebSocket client library for connecting to sigsocket servers with **WASM-first support**.
## Features
- 🌐 **WASM-first design**: Optimized for browser environments
- 🖥️ **Native support**: Works in native Rust applications
- 🔐 **No signing logic**: Delegates signing to the application
- 👤 **User approval flow**: Notifies applications about incoming requests
- 🔌 **sigsocket compatible**: Fully compatible with sigsocket server protocol
- 🚀 **Async/await**: Modern async Rust API
- 🔄 **Automatic reconnection**: Both platforms support reconnection with exponential backoff
- ⏱️ **Connection timeouts**: Proper timeout handling and connection management
- 🛡️ **Production ready**: Comprehensive error handling and reliability features
## Quick Start
### Native Usage
```rust
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
struct MySignHandler;
impl SignRequestHandler for MySignHandler {
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
// 1. Present request to user
println!("Sign request: {}", request.message);
// 2. Get user approval
// ... your UI logic here ...
// 3. Sign the message (using your signing logic)
let signature = your_signing_function(&request.message_bytes()?)?;
Ok(signature)
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Your public key bytes
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")?;
// Create and configure client
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
client.set_sign_handler(MySignHandler);
// Connect and handle requests
client.connect().await?;
// Client will automatically handle incoming signature requests
// Keep the connection alive...
Ok(())
}
```
### WASM Usage
```rust
use sigsocket_client::{SigSocketClient, SignRequestHandler, SignRequest, Result};
use wasm_bindgen::prelude::*;
struct WasmSignHandler;
impl SignRequestHandler for WasmSignHandler {
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
// Show request to user in browser
web_sys::window()
.unwrap()
.alert_with_message(&format!("Sign request: {}", request.id))
.unwrap();
// Your signing logic here...
let signature = sign_with_browser_wallet(&request.message_bytes()?)?;
Ok(signature)
}
}
#[wasm_bindgen]
pub async fn connect_to_sigsocket() -> Result<(), JsValue> {
let public_key = get_user_public_key()?;
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
client.set_sign_handler(WasmSignHandler);
client.connect().await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(())
}
```
## Protocol
The sigsocket client implements a simple WebSocket protocol:
### 1. Introduction
Upon connection, the client sends its public key as a hex-encoded string:
```
02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388
```
### 2. Sign Requests
The server sends signature requests as JSON:
```json
{
"id": "req_123",
"message": "dGVzdCBtZXNzYWdl" // base64-encoded message
}
```
### 3. Sign Responses
The client responds with signatures as JSON:
```json
{
"id": "req_123",
"message": "dGVzdCBtZXNzYWdl", // original message
"signature": "c2lnbmF0dXJl" // base64-encoded signature
}
```
## API Reference
### `SigSocketClient`
Main client for connecting to sigsocket servers.
#### Methods
- `new(url, public_key)` - Create a new client
- `set_sign_handler(handler)` - Set the signature request handler
- `connect()` - Connect to the server with automatic reconnection
- `disconnect()` - Disconnect from the server
- `send_sign_response(response)` - Manually send a signature response
- `state()` - Get current connection state
- `is_connected()` - Check if connected
#### Reconnection Configuration (WASM only)
- `set_auto_reconnect(enabled)` - Enable/disable automatic reconnection
- `set_reconnect_config(max_attempts, initial_delay_ms)` - Configure reconnection parameters
**Default settings:**
- Max attempts: 5
- Initial delay: 1000ms (with exponential backoff: 1s, 2s, 4s, 8s, 16s)
- Auto-reconnect: enabled
### `SignRequestHandler` Trait
Implement this trait to handle incoming signature requests.
```rust
trait SignRequestHandler {
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
}
```
### `SignRequest`
Represents a signature request from the server.
#### Fields
- `id: String` - Unique request identifier
- `message: String` - Base64-encoded message to sign
#### Methods
- `message_bytes()` - Decode message to bytes
- `message_hex()` - Get message as hex string
### `SignResponse`
Represents a signature response to send to the server.
#### Methods
- `new(id, message, signature)` - Create a new response
- `from_request_and_signature(request, signature)` - Create from request and signature bytes
## Examples
Run the basic example:
```bash
cargo run --example basic_usage
```
## Building
### Native Build
```bash
cargo build
cargo test
cargo run --example basic_usage
```
### WASM Build
```bash
wasm-pack build --target web
wasm-pack test --headless --firefox # Run WASM tests
```
## Requirements
### Native
- Rust 1.70+
- tokio runtime
### WASM
- wasm-pack
- Modern browser with WebSocket support
## License
MIT OR Apache-2.0

View File

@ -0,0 +1,133 @@
//! Basic usage example for sigsocket_client
//!
//! This example demonstrates how to:
//! 1. Create a sigsocket client
//! 2. Set up a sign request handler
//! 3. Connect to a sigsocket server
//! 4. Handle incoming signature requests
//!
//! This example only runs on native (non-WASM) targets.
#[cfg(not(target_arch = "wasm32"))]
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
#[cfg(not(target_arch = "wasm32"))]
/// Example sign request handler
///
/// In a real application, this would:
/// - Present the request to the user
/// - Get user approval
/// - Use a secure signing method (hardware wallet, etc.)
/// - Return the signature
struct ExampleSignHandler;
#[cfg(not(target_arch = "wasm32"))]
impl SignRequestHandler for ExampleSignHandler {
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
println!("📝 Received sign request:");
println!(" ID: {}", request.id);
println!(" Message (base64): {}", request.message);
// Decode the message to show what we're signing
match request.message_bytes() {
Ok(message_bytes) => {
println!(" Message (hex): {}", hex::encode(&message_bytes));
println!(" Message (text): {}", String::from_utf8_lossy(&message_bytes));
}
Err(e) => {
println!(" ⚠️ Failed to decode message: {}", e);
return Err(SigSocketError::Base64(e.to_string()));
}
}
// In a real implementation, you would:
// 1. Show this to the user
// 2. Get user approval
// 3. Sign the message using a secure method
println!("🤔 Would you like to sign this message? (This is a simulation)");
println!("✅ Auto-approving for demo purposes...");
// Simulate signing - in reality, this would be a real signature
let fake_signature = format!("fake_signature_for_{}", request.id);
Ok(fake_signature.into_bytes())
}
}
#[cfg(not(target_arch = "wasm32"))]
#[tokio::main]
async fn main() -> Result<()> {
// Initialize logging
env_logger::init();
println!("🚀 SigSocket Client Example");
println!("============================");
// Example public key (in a real app, this would be your actual public key)
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9388")
.expect("Invalid public key hex");
println!("🔑 Public key: {}", hex::encode(&public_key));
// Create the client
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
println!("📡 Created client for: {}", client.url());
// Set up the sign request handler
client.set_sign_handler(ExampleSignHandler);
println!("✅ Sign request handler configured");
// Connect to the server
println!("🔌 Connecting to sigsocket server...");
match client.connect().await {
Ok(()) => {
println!("✅ Connected successfully!");
println!("📊 Connection state: {:?}", client.state());
}
Err(e) => {
println!("❌ Failed to connect: {}", e);
println!("💡 Make sure the sigsocket server is running on localhost:8080");
return Err(e);
}
}
// Keep the connection alive and handle requests
println!("👂 Listening for signature requests...");
println!(" (Press Ctrl+C to exit)");
// In a real application, you might want to:
// - Handle reconnection
// - Provide a UI for user interaction
// - Manage multiple concurrent requests
// - Store and manage signatures
// For this example, we'll just wait
tokio::signal::ctrl_c().await.expect("Failed to listen for ctrl-c");
println!("\n🛑 Shutting down...");
client.disconnect().await?;
println!("✅ Disconnected cleanly");
Ok(())
}
// Example of how you might manually send a response (if needed)
#[cfg(not(target_arch = "wasm32"))]
#[allow(dead_code)]
async fn send_manual_response(client: &SigSocketClient) -> Result<()> {
let response = SignResponse::new(
"example-request-id",
"dGVzdCBtZXNzYWdl", // "test message" in base64
"ZmFrZV9zaWduYXR1cmU=", // "fake_signature" in base64
);
client.send_sign_response(&response).await?;
println!("📤 Sent manual response: {}", response.id);
Ok(())
}
// WASM main function (does nothing since this example is native-only)
#[cfg(target_arch = "wasm32")]
fn main() {
// This example is designed for native use only
}

View File

@ -0,0 +1,384 @@
//! Main client interface for sigsocket communication
#[cfg(target_arch = "wasm32")]
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)]
pub enum ConnectionState {
/// Client is disconnected
Disconnected,
/// Client is connecting
Connecting,
/// Client is connected and ready
Connected,
/// Client connection failed
Failed,
}
/// Trait for handling sign requests from the sigsocket server
///
/// Applications should implement this trait to handle incoming signature requests.
/// The implementation should:
/// 1. Present the request to the user
/// 2. Get user approval
/// 3. Sign the message (using external signing logic)
/// 4. Return the signature
#[cfg(not(target_arch = "wasm32"))]
pub trait SignRequestHandler: Send + Sync {
/// Handle a sign request from the server
///
/// This method is called when the server sends a signature request.
/// The implementation should:
/// - Decode and validate the message
/// - Present it to the user for approval
/// - If approved, sign the message and return the signature
/// - If rejected, return an error
///
/// # Arguments
/// * `request` - The sign request from the server
///
/// # Returns
/// * `Ok(signature_bytes)` - The signature as raw bytes
/// * `Err(error)` - If the request was rejected or signing failed
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
}
/// WASM version of SignRequestHandler (no Send + Sync requirements)
#[cfg(target_arch = "wasm32")]
pub trait SignRequestHandler {
/// Handle a sign request from the server
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>>;
}
/// Main sigsocket client
///
/// This is the primary interface for connecting to sigsocket servers.
/// It handles the WebSocket connection, protocol communication, and
/// delegates signing requests to the application.
pub struct SigSocketClient {
/// WebSocket server URL
url: String,
/// Client's public key (hex-encoded)
public_key: Vec<u8>,
/// Current connection state
state: ConnectionState,
/// Sign request handler
sign_handler: Option<Box<dyn SignRequestHandler>>,
/// Pending sign requests managed by the client
pending_requests: HashMap<String, ManagedSignRequest>,
/// Connected public key (hex-encoded) - set when connection is established
connected_public_key: Option<String>,
/// Platform-specific implementation
#[cfg(not(target_arch = "wasm32"))]
inner: Option<crate::native::NativeClient>,
#[cfg(target_arch = "wasm32")]
inner: Option<crate::wasm::WasmClient>,
}
impl SigSocketClient {
/// Create a new sigsocket client
///
/// # Arguments
/// * `url` - WebSocket server URL (e.g., "ws://localhost:8080/ws")
/// * `public_key` - Client's public key as bytes
///
/// # Returns
/// * `Ok(client)` - New client instance
/// * `Err(error)` - If the URL is invalid or public key is invalid
pub fn new(url: impl Into<String>, public_key: Vec<u8>) -> Result<Self> {
let url = url.into();
// Validate URL
let _ = url::Url::parse(&url)?;
// Validate public key (should be 33 bytes for compressed secp256k1)
if public_key.is_empty() {
return Err(SigSocketError::InvalidPublicKey("Public key cannot be empty".into()));
}
Ok(Self {
url,
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<H>(&mut self, handler: H)
where
H: SignRequestHandler + 'static,
{
self.sign_handler = Some(Box::new(handler));
}
/// Get the current connection state
pub fn state(&self) -> ConnectionState {
self.state
}
/// Check if the client is connected
pub fn is_connected(&self) -> bool {
self.state == ConnectionState::Connected
}
/// Get the client's public key as hex string
pub fn public_key_hex(&self) -> String {
hex::encode(&self.public_key)
}
/// Get the WebSocket server URL
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<ManagedSignRequest> {
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<String, ManagedSignRequest> {
&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
impl SigSocketClient {
/// Connect to the sigsocket server
///
/// This establishes a WebSocket connection and sends the introduction message
/// with the client's public key.
///
/// # Returns
/// * `Ok(())` - Successfully connected
/// * `Err(error)` - Connection failed
pub async fn connect(&mut self) -> Result<()> {
if self.state == ConnectionState::Connected {
return Err(SigSocketError::AlreadyConnected);
}
self.state = ConnectionState::Connecting;
#[cfg(not(target_arch = "wasm32"))]
{
let mut client = crate::native::NativeClient::new(&self.url, &self.public_key)?;
if let Some(handler) = self.sign_handler.take() {
client.set_sign_handler_boxed(handler);
}
client.connect().await?;
self.inner = Some(client);
}
#[cfg(target_arch = "wasm32")]
{
let mut client = crate::wasm::WasmClient::new(&self.url, &self.public_key)?;
if let Some(handler) = self.sign_handler.take() {
client.set_sign_handler_boxed(handler);
}
client.connect().await?;
self.inner = Some(client);
}
self.state = ConnectionState::Connected;
self.connected_public_key = Some(self.public_key_hex());
Ok(())
}
/// Disconnect from the sigsocket server
///
/// # Returns
/// * `Ok(())` - Successfully disconnected
/// * `Err(error)` - Disconnect failed
pub async fn disconnect(&mut self) -> Result<()> {
if let Some(inner) = &mut self.inner {
inner.disconnect().await?;
}
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
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
if !self.is_connected() {
return Err(SigSocketError::NotConnected);
}
if let Some(inner) = &self.inner {
inner.send_sign_response(response).await
} else {
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 {
fn drop(&mut self) {
// Cleanup will be handled by the platform-specific implementations
}
}

View File

@ -0,0 +1,168 @@
//! Error types for the sigsocket client
#[cfg(target_arch = "wasm32")]
use alloc::{string::{String, ToString}, format};
#[cfg(not(target_arch = "wasm32"))]
use thiserror::Error;
#[cfg(target_arch = "wasm32")]
use core::fmt;
/// Result type alias for sigsocket client operations
pub type Result<T> = core::result::Result<T, SigSocketError>;
/// Error types that can occur when using the sigsocket client
#[cfg(not(target_arch = "wasm32"))]
#[derive(Error, Debug)]
pub enum SigSocketError {
/// WebSocket connection error
#[error("Connection error: {0}")]
Connection(String),
/// WebSocket protocol error
#[error("Protocol error: {0}")]
Protocol(String),
/// Message serialization/deserialization error
#[error("Serialization error: {0}")]
Serialization(String),
/// Invalid public key format
#[error("Invalid public key: {0}")]
InvalidPublicKey(String),
/// Invalid URL format
#[error("Invalid URL: {0}")]
InvalidUrl(String),
/// Client is not connected
#[error("Client is not connected")]
NotConnected,
/// Client is already connected
#[error("Client is already connected")]
AlreadyConnected,
/// Timeout error
#[error("Operation timed out")]
Timeout,
/// Send error
#[error("Failed to send message: {0}")]
Send(String),
/// Receive error
#[error("Failed to receive message: {0}")]
Receive(String),
/// Base64 encoding/decoding error
#[error("Base64 error: {0}")]
Base64(String),
/// Hex encoding/decoding error
#[error("Hex error: {0}")]
Hex(String),
/// Generic error
#[error("Error: {0}")]
Other(String),
}
/// WASM version of error types (no thiserror dependency)
#[cfg(target_arch = "wasm32")]
#[derive(Debug)]
pub enum SigSocketError {
/// WebSocket connection error
Connection(String),
/// WebSocket protocol error
Protocol(String),
/// Message serialization/deserialization error
Serialization(String),
/// Invalid public key format
InvalidPublicKey(String),
/// Invalid URL format
InvalidUrl(String),
/// Client is not connected
NotConnected,
/// Client is already connected
AlreadyConnected,
/// Timeout error
Timeout,
/// Send error
Send(String),
/// Receive error
Receive(String),
/// Base64 encoding/decoding error
Base64(String),
/// Hex encoding/decoding error
Hex(String),
/// Generic error
Other(String),
}
#[cfg(target_arch = "wasm32")]
impl fmt::Display for SigSocketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SigSocketError::Connection(msg) => write!(f, "Connection error: {}", msg),
SigSocketError::Protocol(msg) => write!(f, "Protocol error: {}", msg),
SigSocketError::Serialization(msg) => write!(f, "Serialization error: {}", msg),
SigSocketError::InvalidPublicKey(msg) => write!(f, "Invalid public key: {}", msg),
SigSocketError::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg),
SigSocketError::NotConnected => write!(f, "Client is not connected"),
SigSocketError::AlreadyConnected => write!(f, "Client is already connected"),
SigSocketError::Timeout => write!(f, "Operation timed out"),
SigSocketError::Send(msg) => write!(f, "Failed to send message: {}", msg),
SigSocketError::Receive(msg) => write!(f, "Failed to receive message: {}", msg),
SigSocketError::Base64(msg) => write!(f, "Base64 error: {}", msg),
SigSocketError::Hex(msg) => write!(f, "Hex error: {}", msg),
SigSocketError::Other(msg) => write!(f, "Error: {}", msg),
}
}
}
// Implement From traits for common error types
impl From<serde_json::Error> for SigSocketError {
fn from(err: serde_json::Error) -> Self {
SigSocketError::Serialization(err.to_string())
}
}
impl From<base64::DecodeError> for SigSocketError {
fn from(err: base64::DecodeError) -> Self {
SigSocketError::Base64(err.to_string())
}
}
impl From<hex::FromHexError> for SigSocketError {
fn from(err: hex::FromHexError) -> Self {
SigSocketError::Hex(err.to_string())
}
}
impl From<url::ParseError> for SigSocketError {
fn from(err: url::ParseError) -> Self {
SigSocketError::InvalidUrl(err.to_string())
}
}
// Native-specific error conversions
#[cfg(not(target_arch = "wasm32"))]
mod native_errors {
use super::SigSocketError;
impl From<tokio_tungstenite::tungstenite::Error> for SigSocketError {
fn from(err: tokio_tungstenite::tungstenite::Error) -> Self {
SigSocketError::Connection(err.to_string())
}
}
}
// WASM-specific error conversions
#[cfg(target_arch = "wasm32")]
impl From<wasm_bindgen::JsValue> for SigSocketError {
fn from(err: wasm_bindgen::JsValue) -> Self {
SigSocketError::Other(format!("{:?}", err))
}
}

View File

@ -0,0 +1,72 @@
//! # SigSocket Client
//!
//! A WebSocket client library for connecting to sigsocket servers with WASM-first support.
//!
//! This library provides a unified interface for both native and WASM environments,
//! allowing applications to connect to sigsocket servers using a public key and handle
//! incoming signature requests.
//!
//! ## Features
//!
//! - **WASM-first design**: Optimized for browser environments
//! - **Native support**: Works in native Rust applications
//! - **No signing logic**: Delegates signing to the application
//! - **User approval flow**: Notifies applications about incoming requests
//! - **sigsocket compatible**: Fully compatible with sigsocket server protocol
//!
//! ## Example
//!
//! ```rust,no_run
//! use sigsocket_client::{SigSocketClient, SignRequest, SignRequestHandler, Result};
//!
//! struct MyHandler;
//! impl SignRequestHandler for MyHandler {
//! fn handle_sign_request(&self, _request: &SignRequest) -> Result<Vec<u8>> {
//! Ok(b"fake_signature".to_vec())
//! }
//! }
//!
//! #[tokio::main]
//! async fn main() -> Result<()> {
//! // Create client with public key
//! let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9").unwrap();
//! let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key)?;
//!
//! // Set up request handler
//! client.set_sign_handler(MyHandler);
//!
//! // Connect to server
//! client.connect().await?;
//! Ok(())
//! }
//! ```
#![cfg_attr(target_arch = "wasm32", no_std)]
#[cfg(target_arch = "wasm32")]
extern crate alloc;
#[cfg(target_arch = "wasm32")]
use alloc::{string::String, vec::Vec};
mod error;
mod protocol;
mod client;
#[cfg(not(target_arch = "wasm32"))]
mod native;
#[cfg(target_arch = "wasm32")]
mod wasm;
pub use error::{SigSocketError, Result};
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, ManagedSignRequest, RequestStatus,
SignRequestHandler, ConnectionState, SigSocketError, Result
};
}

View File

@ -0,0 +1,232 @@
//! Native (non-WASM) implementation of the sigsocket client
use std::sync::Arc;
use tokio::sync::{mpsc, RwLock};
use tokio_tungstenite::{connect_async, tungstenite::Message};
use futures_util::{SinkExt, StreamExt};
use url::Url;
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
/// Native WebSocket client implementation
pub struct NativeClient {
url: String,
public_key: Vec<u8>,
sign_handler: Option<Arc<dyn SignRequestHandler>>,
sender: Option<mpsc::UnboundedSender<Message>>,
connected: Arc<RwLock<bool>>,
reconnect_attempts: u32,
max_reconnect_attempts: u32,
reconnect_delay_ms: u64,
}
impl NativeClient {
/// Create a new native client
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
Ok(Self {
url: url.to_string(),
public_key: public_key.to_vec(),
sign_handler: None,
sender: None,
connected: Arc::new(RwLock::new(false)),
reconnect_attempts: 0,
max_reconnect_attempts: 5,
reconnect_delay_ms: 1000, // Start with 1 second
})
}
/// Set the sign request handler
pub fn set_sign_handler<H>(&mut self, handler: H)
where
H: SignRequestHandler + 'static,
{
self.sign_handler = Some(Arc::new(handler));
}
/// Set the sign request handler from a boxed trait object
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
self.sign_handler = Some(Arc::from(handler));
}
/// Connect to the WebSocket server with automatic reconnection
pub async fn connect(&mut self) -> Result<()> {
self.reconnect_attempts = 0;
self.connect_with_retry().await
}
/// Connect with retry logic
async fn connect_with_retry(&mut self) -> Result<()> {
loop {
match self.try_connect().await {
Ok(()) => {
self.reconnect_attempts = 0; // Reset on successful connection
return Ok(());
}
Err(e) => {
self.reconnect_attempts += 1;
if self.reconnect_attempts > self.max_reconnect_attempts {
log::error!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts);
return Err(e);
}
let delay = self.reconnect_delay_ms * (2_u64.pow(self.reconnect_attempts - 1)); // Exponential backoff
log::warn!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
self.reconnect_attempts, self.max_reconnect_attempts, delay, e);
tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await;
}
}
}
}
/// Single connection attempt
async fn try_connect(&mut self) -> Result<()> {
let url = Url::parse(&self.url)?;
// Connect to WebSocket
let (ws_stream, _) = connect_async(url).await
.map_err(|e| SigSocketError::Connection(e.to_string()))?;
let (mut write, mut read) = ws_stream.split();
// Send introduction message (hex-encoded public key)
let intro_message = hex::encode(&self.public_key);
write.send(Message::Text(intro_message)).await
.map_err(|e| SigSocketError::Send(e.to_string()))?;
// Set up message sender channel
let (tx, mut rx) = mpsc::unbounded_channel();
self.sender = Some(tx);
// Set connected state
*self.connected.write().await = true;
// Spawn write task
let write_task = tokio::spawn(async move {
while let Some(message) = rx.recv().await {
if let Err(e) = write.send(message).await {
log::error!("Failed to send message: {}", e);
break;
}
}
});
// Spawn read task
let connected = self.connected.clone();
let sign_handler = self.sign_handler.clone();
let sender = self.sender.as_ref().unwrap().clone();
let read_task = tokio::spawn(async move {
while let Some(message) = read.next().await {
match message {
Ok(Message::Text(text)) => {
if let Err(e) = Self::handle_text_message(&text, &sign_handler, &sender).await {
log::error!("Failed to handle message: {}", e);
}
}
Ok(Message::Close(_)) => {
log::info!("WebSocket connection closed");
break;
}
Err(e) => {
log::error!("WebSocket error: {}", e);
break;
}
_ => {
// Ignore other message types
}
}
}
// Mark as disconnected
*connected.write().await = false;
});
// Store tasks (in a real implementation, you'd want to manage these properly)
tokio::spawn(async move {
let _ = tokio::try_join!(write_task, read_task);
});
Ok(())
}
/// Handle incoming text messages
async fn handle_text_message(
text: &str,
sign_handler: &Option<Arc<dyn SignRequestHandler>>,
sender: &mpsc::UnboundedSender<Message>,
) -> Result<()> {
log::debug!("Received message: {}", text);
// Handle simple acknowledgment messages
if text == "Connected" {
log::info!("Server acknowledged connection");
return Ok(());
}
// Try to parse as sign request
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
if let Some(handler) = sign_handler {
// Handle the sign request
match handler.handle_sign_request(&sign_request) {
Ok(signature) => {
// Create and send response
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
let response_json = serde_json::to_string(&response)?;
sender.send(Message::Text(response_json))
.map_err(|e| SigSocketError::Send(e.to_string()))?;
log::info!("Sent signature response for request {}", response.id);
}
Err(e) => {
log::warn!("Sign request rejected: {}", e);
// Optionally send an error response to the server
}
}
} else {
log::warn!("No sign request handler registered, ignoring request");
}
return Ok(());
}
log::warn!("Failed to parse message: {}", text);
Ok(())
}
/// Disconnect from the WebSocket server
pub async fn disconnect(&mut self) -> Result<()> {
*self.connected.write().await = false;
if let Some(sender) = &self.sender {
// Send close message
let _ = sender.send(Message::Close(None));
}
self.sender = None;
Ok(())
}
/// Send a sign response to the server
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
if let Some(sender) = &self.sender {
let response_json = serde_json::to_string(response)?;
sender.send(Message::Text(response_json))
.map_err(|e| SigSocketError::Send(e.to_string()))?;
Ok(())
} else {
Err(SigSocketError::NotConnected)
}
}
/// Check if connected
pub async fn is_connected(&self) -> bool {
*self.connected.read().await
}
}
impl Drop for NativeClient {
fn drop(&mut self) {
// Cleanup will be handled by the async tasks
}
}

View File

@ -0,0 +1,256 @@
//! Protocol definitions for sigsocket communication
#[cfg(target_arch = "wasm32")]
use alloc::{string::String, vec::Vec};
use serde::{Deserialize, Serialize};
/// Sign request from the sigsocket server
///
/// This represents a request from the server for the client to sign a message.
/// The client should present this to the user for approval before signing.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SignRequest {
/// Unique identifier for this request
pub id: String,
/// Message to be signed (base64-encoded)
pub message: String,
}
/// Sign response to send back to the sigsocket server
///
/// This represents the client's response after the user has approved and signed the message.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SignResponse {
/// Request identifier (must match the original request)
pub id: String,
/// Original message that was signed (base64-encoded)
pub message: String,
/// Signature of the message (base64-encoded)
pub signature: String,
}
impl SignRequest {
/// Create a new sign request
pub fn new(id: impl Into<String>, message: impl Into<String>) -> Self {
Self {
id: id.into(),
message: message.into(),
}
}
/// Get the message as bytes (decoded from base64)
pub fn message_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.message)
}
/// Get the message as a hex string (for display purposes)
pub fn message_hex(&self) -> Result<String, base64::DecodeError> {
self.message_bytes().map(|bytes| hex::encode(bytes))
}
}
impl SignResponse {
/// Create a new sign response
pub fn new(
id: impl Into<String>,
message: impl Into<String>,
signature: impl Into<String>,
) -> Self {
Self {
id: id.into(),
message: message.into(),
signature: signature.into(),
}
}
/// Create a sign response from a request and signature bytes
pub fn from_request_and_signature(
request: &SignRequest,
signature: &[u8],
) -> Self {
Self {
id: request.id.clone(),
message: request.message.clone(),
signature: base64::Engine::encode(&base64::engine::general_purpose::STANDARD, signature),
}
}
/// Get the signature as bytes (decoded from base64)
pub fn signature_bytes(&self) -> Result<Vec<u8>, base64::DecodeError> {
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &self.signature)
}
}
/// 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<Vec<u8>, 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::*;
#[test]
fn test_sign_request_creation() {
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
assert_eq!(request.id, "test-id");
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
}
#[test]
fn test_sign_request_message_bytes() {
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
let bytes = request.message_bytes().unwrap();
assert_eq!(bytes, b"test message");
}
#[test]
fn test_sign_request_message_hex() {
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl"); // "test message" in base64
let hex = request.message_hex().unwrap();
assert_eq!(hex, hex::encode(b"test message"));
}
#[test]
fn test_sign_response_creation() {
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl"); // "signature" in base64
assert_eq!(response.id, "test-id");
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
assert_eq!(response.signature, "c2lnbmF0dXJl");
}
#[test]
fn test_sign_response_from_request() {
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
let signature = b"signature";
let response = SignResponse::from_request_and_signature(&request, signature);
assert_eq!(response.id, request.id);
assert_eq!(response.message, request.message);
assert_eq!(response.signature_bytes().unwrap(), signature);
}
#[test]
fn test_serialization() {
let request = SignRequest::new("test-id", "dGVzdCBtZXNzYWdl");
let json = serde_json::to_string(&request).unwrap();
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
assert_eq!(request, deserialized);
let response = SignResponse::new("test-id", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
let json = serde_json::to_string(&response).unwrap();
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());
}
}

View File

@ -0,0 +1,549 @@
//! WASM implementation of the sigsocket client
use alloc::{string::{String, ToString}, vec::Vec, boxed::Box, rc::Rc, format};
use core::cell::RefCell;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{WebSocket, MessageEvent, Event, BinaryType};
use crate::{SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
/// WASM WebSocket client implementation
pub struct WasmClient {
url: String,
public_key: Vec<u8>,
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
websocket: Option<WebSocket>,
connected: Rc<RefCell<bool>>,
reconnect_attempts: Rc<RefCell<u32>>,
max_reconnect_attempts: u32,
reconnect_delay_ms: u64,
auto_reconnect: bool,
}
impl WasmClient {
/// Create a new WASM client
pub fn new(url: &str, public_key: &[u8]) -> Result<Self> {
Ok(Self {
url: url.to_string(),
public_key: public_key.to_vec(),
sign_handler: None,
websocket: None,
connected: Rc::new(RefCell::new(false)),
reconnect_attempts: Rc::new(RefCell::new(0)),
max_reconnect_attempts: 5,
reconnect_delay_ms: 1000, // Start with 1 second
auto_reconnect: false, // Disable auto-reconnect to avoid multiple connections
})
}
/// Set the sign request handler from a boxed trait object
pub fn set_sign_handler_boxed(&mut self, handler: Box<dyn SignRequestHandler>) {
self.sign_handler = Some(Rc::new(RefCell::new(handler)));
}
/// Enable or disable automatic reconnection
pub fn set_auto_reconnect(&mut self, enabled: bool) {
self.auto_reconnect = enabled;
}
/// Set reconnection parameters
pub fn set_reconnect_config(&mut self, max_attempts: u32, initial_delay_ms: u64) {
self.max_reconnect_attempts = max_attempts;
self.reconnect_delay_ms = initial_delay_ms;
}
/// Connect to the WebSocket server with automatic reconnection
pub async fn connect(&mut self) -> Result<()> {
*self.reconnect_attempts.borrow_mut() = 0;
self.connect_with_retry().await
}
/// Connect with retry logic
async fn connect_with_retry(&mut self) -> Result<()> {
loop {
match self.try_connect().await {
Ok(()) => {
*self.reconnect_attempts.borrow_mut() = 0; // Reset on successful connection
return Ok(());
}
Err(e) => {
let mut attempts = self.reconnect_attempts.borrow_mut();
*attempts += 1;
if *attempts > self.max_reconnect_attempts {
web_sys::console::error_1(&format!("Max reconnection attempts ({}) exceeded", self.max_reconnect_attempts).into());
return Err(e);
}
let delay = self.reconnect_delay_ms * (2_u64.pow(*attempts - 1)); // Exponential backoff
web_sys::console::warn_1(&format!("Connection failed (attempt {}/{}), retrying in {}ms: {}",
*attempts, self.max_reconnect_attempts, delay, e).into());
// Drop the borrow before the async sleep
drop(attempts);
// Wait before retrying
self.sleep_ms(delay).await;
}
}
}
}
/// Sleep for the specified number of milliseconds (WASM-compatible)
async fn sleep_ms(&self, ms: u64) -> () {
use wasm_bindgen_futures::JsFuture;
use js_sys::Promise;
let promise = Promise::new(&mut |resolve, _reject| {
let timeout_callback = Closure::wrap(Box::new(move || {
resolve.call0(&wasm_bindgen::JsValue::UNDEFINED).unwrap();
}) as Box<dyn FnMut()>);
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
timeout_callback.as_ref().unchecked_ref(),
ms as i32,
)
.unwrap();
timeout_callback.forget();
});
let _ = JsFuture::from(promise).await;
}
/// 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| {
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 public_key_clone = public_key.clone();
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
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_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!("MAIN CONNECTION: Failed to send introduction: {:?}", e).into());
} else {
web_sys::console::log_1(&"MAIN CONNECTION: Public key sent successfully".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::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
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, &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::<dyn FnMut(Event)>::new(move |event| {
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
{
let connected = connected.clone();
let auto_reconnect = self.auto_reconnect;
let reconnect_attempts = self.reconnect_attempts.clone();
let max_attempts = self.max_reconnect_attempts;
let url = self.url.clone();
let public_key = self.public_key.clone();
let sign_handler = self.sign_handler.clone();
let delay_ms = self.reconnect_delay_ms;
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
*connected.borrow_mut() = false;
web_sys::console::log_1(&"WebSocket connection closed".into());
// Trigger auto-reconnection if enabled
if auto_reconnect {
let attempts = reconnect_attempts.clone();
let current_attempts = *attempts.borrow();
if current_attempts < max_attempts {
web_sys::console::log_1(&"Attempting automatic reconnection...".into());
// Schedule reconnection attempt
Self::schedule_reconnection(
url.clone(),
public_key.clone(),
sign_handler.clone(),
attempts.clone(),
max_attempts,
delay_ms,
connected.clone(),
);
} else {
web_sys::console::error_1(&format!("Max reconnection attempts ({}) reached, giving up", max_attempts).into());
}
}
});
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
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);
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
async fn wait_for_connection(&self) -> Result<()> {
use wasm_bindgen_futures::JsFuture;
use js_sys::Promise;
web_sys::console::log_1(&"wait_for_connection: Starting to wait for connection".into());
// 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 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<dyn FnMut()>);
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
timeout_callback.as_ref().unchecked_ref(),
500,
)
.unwrap();
timeout_callback.forget();
});
let _ = JsFuture::from(promise).await;
if attempt % 10 == 0 {
web_sys::console::log_1(&format!("wait_for_connection: Still waiting... attempt {}/60", attempt).into());
}
}
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)
fn schedule_reconnection(
url: String,
public_key: Vec<u8>,
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
reconnect_attempts: Rc<RefCell<u32>>,
_max_attempts: u32,
delay_ms: u64,
connected: Rc<RefCell<bool>>,
) {
let mut attempts = reconnect_attempts.borrow_mut();
*attempts += 1;
let current_attempt = *attempts;
drop(attempts); // Release the borrow
let delay = delay_ms * (2_u64.pow(current_attempt - 1)); // Exponential backoff
web_sys::console::log_1(&format!("Scheduling reconnection attempt {} in {}ms", current_attempt, delay).into());
// Schedule the reconnection attempt
let timeout_callback = Closure::wrap(Box::new(move || {
// Create a new client instance for reconnection
match Self::attempt_reconnection(url.clone(), public_key.clone(), sign_handler.clone(), connected.clone()) {
Ok(_) => {
web_sys::console::log_1(&"Reconnection attempt initiated".into());
}
Err(e) => {
web_sys::console::error_1(&format!("Failed to initiate reconnection: {:?}", e).into());
}
}
}) as Box<dyn FnMut()>);
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
timeout_callback.as_ref().unchecked_ref(),
delay as i32,
)
.unwrap();
timeout_callback.forget();
}
/// Attempt to reconnect (helper method)
fn attempt_reconnection(
url: String,
public_key: Vec<u8>,
sign_handler: Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
connected: Rc<RefCell<bool>>,
) -> Result<()> {
// Create WebSocket
let ws = WebSocket::new(&url)
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
ws.set_binary_type(BinaryType::Arraybuffer);
// Send public key on open
{
let public_key_clone = public_key.clone();
let connected_clone = connected.clone();
let ws_clone = ws.clone();
let onopen_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
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 {
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
}
});
ws.set_onopen(Some(onopen_callback.as_ref().unchecked_ref()));
onopen_callback.forget();
}
// Set up message handler for reconnected socket
{
let ws_clone = ws.clone();
let handler_clone = sign_handler.clone();
let connected_clone = connected.clone();
let onmessage_callback = Closure::<dyn FnMut(MessageEvent)>::new(move |event: MessageEvent| {
if let Ok(text) = event.data().dyn_into::<js_sys::JsString>() {
let message = text.as_string().unwrap_or_default();
Self::handle_message(&message, &ws_clone, &handler_clone, &connected_clone);
}
});
ws.set_onmessage(Some(onmessage_callback.as_ref().unchecked_ref()));
onmessage_callback.forget();
}
// Set up error handler
{
let onerror_callback = Closure::<dyn FnMut(Event)>::new(move |event| {
web_sys::console::error_1(&format!("Reconnection WebSocket error: {:?}", event).into());
});
ws.set_onerror(Some(onerror_callback.as_ref().unchecked_ref()));
onerror_callback.forget();
}
// Set up close handler (for potential future reconnections)
{
let connected_clone = connected.clone();
let onclose_callback = Closure::<dyn FnMut(Event)>::new(move |_event| {
*connected_clone.borrow_mut() = false;
web_sys::console::log_1(&"Reconnected WebSocket closed".into());
});
ws.set_onclose(Some(onclose_callback.as_ref().unchecked_ref()));
onclose_callback.forget();
}
Ok(())
}
/// Handle incoming messages with full sign request support
fn handle_message(
text: &str,
ws: &WebSocket,
sign_handler: &Option<Rc<RefCell<Box<dyn SignRequestHandler>>>>,
connected: &Rc<RefCell<bool>>
) {
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;
}
// Try to parse as sign request
if let Ok(sign_request) = serde_json::from_str::<SignRequest>(text) {
web_sys::console::log_1(&format!("Received sign request: {}", sign_request.id).into());
// Handle the sign request if we have a handler
if let Some(handler_rc) = sign_handler {
match handler_rc.try_borrow() {
Ok(handler) => {
match handler.handle_sign_request(&sign_request) {
Ok(signature) => {
// Create and send response
let response = SignResponse::from_request_and_signature(&sign_request, &signature);
match serde_json::to_string(&response) {
Ok(response_json) => {
if let Err(e) = ws.send_with_str(&response_json) {
web_sys::console::error_1(&format!("Failed to send response: {:?}", e).into());
} else {
web_sys::console::log_1(&format!("Sent signature response for request {}", response.id).into());
}
}
Err(e) => {
web_sys::console::error_1(&format!("Failed to serialize response: {}", e).into());
}
}
}
Err(e) => {
web_sys::console::warn_1(&format!("Sign request rejected: {}", e).into());
// Optionally send an error response to the server
}
}
}
Err(_) => {
web_sys::console::error_1(&"Failed to borrow sign handler".into());
}
}
} else {
web_sys::console::warn_1(&"No sign request handler registered, ignoring request".into());
}
return;
}
web_sys::console::warn_1(&format!("Failed to parse message: {}", text).into());
}
/// Disconnect from the WebSocket server
pub async fn disconnect(&mut self) -> Result<()> {
if let Some(ws) = &self.websocket {
ws.close()
.map_err(|e| SigSocketError::Connection(format!("{:?}", e)))?;
}
*self.connected.borrow_mut() = false;
self.websocket = None;
Ok(())
}
/// Send a sign response to the server
pub async fn send_sign_response(&self, response: &SignResponse) -> Result<()> {
if let Some(ws) = &self.websocket {
let response_json = serde_json::to_string(response)?;
ws.send_with_str(&response_json)
.map_err(|e| SigSocketError::Send(format!("{:?}", e)))?;
Ok(())
} else {
Err(SigSocketError::NotConnected)
}
}
/// Check if connected
pub fn is_connected(&self) -> bool {
*self.connected.borrow()
}
}
impl Drop for WasmClient {
fn drop(&mut self) {
// Close WebSocket connection if it exists
if let Some(ws) = self.websocket.take() {
ws.close().unwrap_or_else(|e| {
web_sys::console::warn_1(&format!("Failed to close WebSocket: {:?}", e).into());
});
web_sys::console::log_1(&"🔌 WebSocket connection closed on drop".into());
}
}
}
// WASM-specific utilities
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
// Helper macro for logging in WASM
#[allow(unused_macros)]
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

View File

@ -0,0 +1,162 @@
//! Integration tests for sigsocket_client
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
/// Test sign request handler
struct TestSignHandler {
should_approve: bool,
}
impl TestSignHandler {
fn new(should_approve: bool) -> Self {
Self { should_approve }
}
}
impl SignRequestHandler for TestSignHandler {
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
if self.should_approve {
// Create a test signature
let signature = format!("test_signature_for_{}", request.id);
Ok(signature.into_bytes())
} else {
Err(SigSocketError::Other("User rejected request".to_string()))
}
}
}
#[test]
fn test_sign_request_creation() {
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
assert_eq!(request.id, "test-123");
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
}
#[test]
fn test_sign_request_message_decoding() {
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
let bytes = request.message_bytes().unwrap();
assert_eq!(bytes, b"test message");
let hex = request.message_hex().unwrap();
assert_eq!(hex, hex::encode(b"test message"));
}
#[test]
fn test_sign_response_creation() {
let response = SignResponse::new("test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
assert_eq!(response.id, "test-123");
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
assert_eq!(response.signature, "c2lnbmF0dXJl");
}
#[test]
fn test_sign_response_from_request() {
let request = SignRequest::new("test-123", "dGVzdCBtZXNzYWdl");
let signature = b"test_signature";
let response = SignResponse::from_request_and_signature(&request, signature);
assert_eq!(response.id, request.id);
assert_eq!(response.message, request.message);
assert_eq!(response.signature_bytes().unwrap(), signature);
}
#[test]
fn test_protocol_serialization() {
// Test SignRequest serialization
let request = SignRequest::new("req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
let json = serde_json::to_string(&request).unwrap();
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
assert_eq!(request, deserialized);
// Test SignResponse serialization
let response = SignResponse::new("req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
let json = serde_json::to_string(&response).unwrap();
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
assert_eq!(response, deserialized);
}
#[test]
fn test_client_creation() {
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
.unwrap();
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
assert_eq!(client.url(), "ws://localhost:8080/ws");
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
assert!(!client.is_connected());
}
#[test]
fn test_client_invalid_url() {
let public_key = vec![1, 2, 3];
let result = SigSocketClient::new("invalid-url", public_key);
assert!(result.is_err());
}
#[test]
fn test_client_empty_public_key() {
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
assert!(result.is_err());
if let Err(error) = result {
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
}
}
#[test]
fn test_sign_handler_approval() {
let handler = TestSignHandler::new(true);
let request = SignRequest::new("test-789", "dGVzdA==");
let result = handler.handle_sign_request(&request);
assert!(result.is_ok());
let signature = result.unwrap();
assert_eq!(signature, b"test_signature_for_test-789");
}
#[test]
fn test_sign_handler_rejection() {
let handler = TestSignHandler::new(false);
let request = SignRequest::new("test-789", "dGVzdA==");
let result = handler.handle_sign_request(&request);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
}
#[test]
fn test_error_display() {
let error = SigSocketError::NotConnected;
assert_eq!(error.to_string(), "Client is not connected");
let error = SigSocketError::Connection("test error".to_string());
assert_eq!(error.to_string(), "Connection error: test error");
}
// Test that demonstrates the expected usage pattern
#[test]
fn test_usage_pattern() {
// 1. Create client
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
.unwrap();
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
// 2. Set handler
client.set_sign_handler(TestSignHandler::new(true));
// 3. Verify state
assert!(!client.is_connected());
// 4. Create a test request/response cycle
let request = SignRequest::new("test-request", "dGVzdCBtZXNzYWdl");
let handler = TestSignHandler::new(true);
let signature = handler.handle_sign_request(&request).unwrap();
let response = SignResponse::from_request_and_signature(&request, &signature);
// 5. Verify the response
assert_eq!(response.id, request.id);
assert_eq!(response.message, request.message);
assert_eq!(response.signature_bytes().unwrap(), signature);
}

View File

@ -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");
}

View File

@ -0,0 +1,181 @@
#![cfg(target_arch = "wasm32")]
//! WASM/browser tests for sigsocket_client using wasm-bindgen-test
use wasm_bindgen_test::*;
use sigsocket_client::{SigSocketClient, SignRequest, SignResponse, SignRequestHandler, Result, SigSocketError};
wasm_bindgen_test_configure!(run_in_browser);
/// Test sign request handler for WASM tests
struct TestWasmSignHandler {
should_approve: bool,
}
impl TestWasmSignHandler {
fn new(should_approve: bool) -> Self {
Self { should_approve }
}
}
impl SignRequestHandler for TestWasmSignHandler {
fn handle_sign_request(&self, request: &SignRequest) -> Result<Vec<u8>> {
if self.should_approve {
// Create a test signature
let signature = format!("wasm_test_signature_for_{}", request.id);
Ok(signature.into_bytes())
} else {
Err(SigSocketError::Other("User rejected request in WASM test".to_string()))
}
}
}
#[wasm_bindgen_test]
fn test_sign_request_creation_wasm() {
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
assert_eq!(request.id, "wasm-test-123");
assert_eq!(request.message, "dGVzdCBtZXNzYWdl");
}
#[wasm_bindgen_test]
fn test_sign_request_message_decoding_wasm() {
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl"); // "test message" in base64
let bytes = request.message_bytes().unwrap();
assert_eq!(bytes, b"test message");
let hex = request.message_hex().unwrap();
assert_eq!(hex, hex::encode(b"test message"));
}
#[wasm_bindgen_test]
fn test_sign_response_creation_wasm() {
let response = SignResponse::new("wasm-test-123", "dGVzdCBtZXNzYWdl", "c2lnbmF0dXJl");
assert_eq!(response.id, "wasm-test-123");
assert_eq!(response.message, "dGVzdCBtZXNzYWdl");
assert_eq!(response.signature, "c2lnbmF0dXJl");
}
#[wasm_bindgen_test]
fn test_sign_response_from_request_wasm() {
let request = SignRequest::new("wasm-test-123", "dGVzdCBtZXNzYWdl");
let signature = b"wasm_test_signature";
let response = SignResponse::from_request_and_signature(&request, signature);
assert_eq!(response.id, request.id);
assert_eq!(response.message, request.message);
assert_eq!(response.signature_bytes().unwrap(), signature);
}
#[wasm_bindgen_test]
fn test_protocol_serialization_wasm() {
// Test SignRequest serialization
let request = SignRequest::new("wasm-req-456", "SGVsbG8gV29ybGQ="); // "Hello World" in base64
let json = serde_json::to_string(&request).unwrap();
let deserialized: SignRequest = serde_json::from_str(&json).unwrap();
assert_eq!(request, deserialized);
// Test SignResponse serialization
let response = SignResponse::new("wasm-req-456", "SGVsbG8gV29ybGQ=", "c2lnbmF0dXJlXzEyMw==");
let json = serde_json::to_string(&response).unwrap();
let deserialized: SignResponse = serde_json::from_str(&json).unwrap();
assert_eq!(response, deserialized);
}
#[wasm_bindgen_test]
fn test_client_creation_wasm() {
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
.unwrap();
let client = SigSocketClient::new("ws://localhost:8080/ws", public_key.clone()).unwrap();
assert_eq!(client.url(), "ws://localhost:8080/ws");
assert_eq!(client.public_key_hex(), hex::encode(&public_key));
assert!(!client.is_connected());
}
#[wasm_bindgen_test]
fn test_client_invalid_url_wasm() {
let public_key = vec![1, 2, 3];
let result = SigSocketClient::new("invalid-url", public_key);
assert!(result.is_err());
}
#[wasm_bindgen_test]
fn test_client_empty_public_key_wasm() {
let result = SigSocketClient::new("ws://localhost:8080/ws", vec![]);
assert!(result.is_err());
if let Err(error) = result {
assert!(matches!(error, SigSocketError::InvalidPublicKey(_)));
}
}
#[wasm_bindgen_test]
fn test_sign_handler_approval_wasm() {
let handler = TestWasmSignHandler::new(true);
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
let result = handler.handle_sign_request(&request);
assert!(result.is_ok());
let signature = result.unwrap();
assert_eq!(signature, b"wasm_test_signature_for_wasm-test-789");
}
#[wasm_bindgen_test]
fn test_sign_handler_rejection_wasm() {
let handler = TestWasmSignHandler::new(false);
let request = SignRequest::new("wasm-test-789", "dGVzdA==");
let result = handler.handle_sign_request(&request);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SigSocketError::Other(_)));
}
#[wasm_bindgen_test]
fn test_error_display_wasm() {
let error = SigSocketError::NotConnected;
assert_eq!(error.to_string(), "Client is not connected");
let error = SigSocketError::Connection("wasm test error".to_string());
assert_eq!(error.to_string(), "Connection error: wasm test error");
}
// Test that demonstrates the expected WASM usage pattern
#[wasm_bindgen_test]
fn test_wasm_usage_pattern() {
// 1. Create client
let public_key = hex::decode("02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9")
.unwrap();
let mut client = SigSocketClient::new("ws://localhost:8080/ws", public_key).unwrap();
// 2. Set handler
client.set_sign_handler(TestWasmSignHandler::new(true));
// 3. Verify state
assert!(!client.is_connected());
// 4. Create a test request/response cycle
let request = SignRequest::new("wasm-test-request", "dGVzdCBtZXNzYWdl");
let handler = TestWasmSignHandler::new(true);
let signature = handler.handle_sign_request(&request).unwrap();
let response = SignResponse::from_request_and_signature(&request, &signature);
// 5. Verify the response
assert_eq!(response.id, request.id);
assert_eq!(response.message, request.message);
assert_eq!(response.signature_bytes().unwrap(), signature);
}
// Test WASM-specific console logging (if needed)
#[wasm_bindgen_test]
fn test_wasm_console_logging() {
// This test verifies that WASM console logging works
web_sys::console::log_1(&"SigSocket WASM test logging works!".into());
// Test that we can create and log protocol messages
let request = SignRequest::new("log-test", "dGVzdA==");
let json = serde_json::to_string(&request).unwrap();
web_sys::console::log_1(&format!("Sign request JSON: {}", json).into());
// This test always passes - it's just for verification that logging works
assert!(true);
}

View File

@ -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;
@ -217,7 +216,7 @@ impl<S: KVStore> Vault<S> {
// --- Keypair Management APIs ---
/// Create a default Ed25519 keypair for client identity
/// Create a default Secp256k1 keypair for client identity
/// This keypair is deterministically generated from the password and salt
/// and will always be the first keypair in the keyspace
async fn create_default_keypair(
@ -229,26 +228,32 @@ impl<S: KVStore> Vault<S> {
// 1. Derive a deterministic seed using standard PBKDF2
let seed = kdf::keyspace_key(password, salt);
// 2. Generate Ed25519 keypair from the seed
use ed25519_dalek::{SigningKey, VerifyingKey};
// 2. Generate Secp256k1 keypair from the seed
use k256::ecdsa::{SigningKey, VerifyingKey};
// Use the seed to create a deterministic keypair
let signing = SigningKey::from_bytes(seed.as_slice().try_into().unwrap());
let verifying: VerifyingKey = (&signing).into();
// Use the seed as the private key directly (32 bytes)
let mut secret_key_bytes = [0u8; 32];
secret_key_bytes.copy_from_slice(&seed[..32]);
let priv_bytes = signing.to_bytes().to_vec();
let pub_bytes = verifying.to_bytes().to_vec();
// Create signing key
let signing_key = SigningKey::from_bytes(&secret_key_bytes.into())
.map_err(|e| VaultError::Crypto(format!("Failed to create signing key: {}", e)))?;
// Create an ID for the default keypair
// Get verifying key
let verifying_key = VerifyingKey::from(&signing_key);
// Convert keys to bytes
let priv_bytes = signing_key.to_bytes().to_vec();
let pub_bytes = verifying_key.to_encoded_point(false).as_bytes().to_vec();
let id = hex::encode(&pub_bytes);
// 3. Unlock the keyspace to get its data
// 3. Unlock keyspace to add the keypair
let mut data = self.unlock_keyspace(keyspace, password).await?;
// 4. Add to keypairs (as the first entry)
// 4. Create key entry
let entry = KeyEntry {
id: id.clone(),
key_type: KeyType::Ed25519,
key_type: KeyType::Secp256k1,
private_key: priv_bytes,
public_key: pub_bytes,
metadata: Some(KeyMetadata {
@ -460,14 +465,15 @@ impl<S: KVStore> Vault<S> {
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())
}
}
}
@ -511,7 +517,11 @@ impl<S: KVStore> Vault<S> {
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())
}

View File

@ -9,7 +9,7 @@ use crate::session::SessionManager;
#[cfg(not(target_arch = "wasm32"))]
pub fn register_rhai_api<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static>(
engine: &mut Engine,
session_manager: std::sync::Arc<std::sync::Mutex<SessionManager<S>>>,
_session_manager: std::sync::Arc<std::sync::Mutex<SessionManager<S>>>,
) {
engine.register_type::<RhaiSessionManager<S>>();
engine.register_fn("select_keypair", RhaiSessionManager::<S>::select_keypair);
@ -37,7 +37,7 @@ impl<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static> RhaiSessionMan
// Use Mutex for interior mutability, &self is sufficient
self.inner.lock().unwrap().select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}"))
}
pub fn select_default_keypair(&self) -> Result<(), String> {
self.inner.lock().unwrap().select_default_keypair()
.map_err(|e| format!("select_default_keypair error: {e}"))

View File

@ -3,7 +3,6 @@
//! All state is local to the SessionManager instance. No global state.
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
use std::collections::HashMap;
use zeroize::Zeroize;
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
@ -130,17 +129,17 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
self.current_keyspace()
.and_then(|ks| ks.keypairs.first())
}
/// Selects the default keypair (first keypair) as the current keypair.
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
let default_id = self
.default_keypair()
.map(|k| k.id.clone())
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
self.select_keypair(&default_id)
}
/// Returns true if the current keypair is the default keypair (first keypair).
pub fn is_default_keypair_selected(&self) -> bool {
match (self.current_keypair(), self.default_keypair()) {
@ -312,17 +311,17 @@ impl<S: KVStore> SessionManager<S> {
self.current_keyspace()
.and_then(|ks| ks.keypairs.first())
}
/// Selects the default keypair (first keypair) as the current keypair.
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
let default_id = self
.default_keypair()
.map(|k| k.id.clone())
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
self.select_keypair(&default_id)
}
/// Returns true if the current keypair is the default keypair (first keypair).
pub fn is_default_keypair_selected(&self) -> bool {
match (self.current_keypair(), self.default_keypair()) {

View File

@ -26,6 +26,8 @@ async fn test_keypair_management_and_crypto() {
vault.create_keyspace(keyspace, password, None).await.unwrap();
debug!("after create_keyspace: keyspace={} password={}", keyspace, hex::encode(password));
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
assert_eq!(keys.len(), 1); // should be 1 because we added a default keypair on create_keyspace
debug!("before add Ed25519 keypair");
let key_id = vault.add_keypair(keyspace, password, Some(KeyType::Ed25519), Some(KeyMetadata { name: Some("edkey".into()), created_at: None, tags: None })).await;
match &key_id {
@ -38,7 +40,7 @@ async fn test_keypair_management_and_crypto() {
debug!("before list_keypairs");
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
assert_eq!(keys.len(), 2);
assert_eq!(keys.len(), 3);
debug!("before export Ed25519 keypair");
let (priv_bytes, pub_bytes) = vault.export_keypair(keyspace, password, &key_id).await.unwrap();
@ -65,5 +67,5 @@ async fn test_keypair_management_and_crypto() {
// Remove a keypair
vault.remove_keypair(keyspace, password, &key_id).await.unwrap();
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
assert_eq!(keys.len(), 1);
assert_eq!(keys.len(), 2);
}

View File

@ -11,7 +11,7 @@ async fn session_manager_end_to_end() {
use tempfile::TempDir;
let tmp_dir = TempDir::new().expect("create temp dir");
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("open NativeStore");
let mut vault = Vault::new(store);
let vault = Vault::new(store);
let keyspace = "personal";
let password = b"testpass";

View File

@ -32,8 +32,7 @@ async fn test_session_manager_lock_unlock_keypairs_persistence() {
// 3. List, store keys and names
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
let keypairs_before = session.list_keypairs().expect("list_keypairs before").iter().map(|k| (k.id.clone(), k.public_key.clone(), k.private_key.clone(), k.metadata.clone())).collect::<Vec<_>>();
assert_eq!(keypairs_before.len(), 2);
assert_eq!(keypairs_before.len(), 3);
assert!(keypairs_before.iter().any(|k| k.0 == id1 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-one")));
assert!(keypairs_before.iter().any(|k| k.0 == id2 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-two")));

View File

@ -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"

View File

@ -24,8 +24,13 @@ 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
mod sigsocket;
pub use sigsocket::*;
/// Initialize the scripting environment (must be called before run_rhai)
#[wasm_bindgen]
pub fn init_rhai_env() {

View File

@ -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<SigSocketClient>,
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()
}
}

View File

@ -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<Vec<u8>> {
// 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);
}

View File

@ -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;

View File

@ -0,0 +1,528 @@
//! 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 base64::prelude::*;
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>> {
console_log!("📨 WASM: Handling sign request: {}", request.id);
// First, store the request in the WASM client
let store_result = SIGSOCKET_CLIENT.with(|c| {
let mut client_opt = c.borrow_mut();
if let Some(client) = client_opt.as_mut() {
// Get the connected public key as the target
if let Some(target_public_key) = client.connected_public_key() {
client.add_pending_request(request.clone(), target_public_key.to_string());
console_log!("✅ WASM: Stored sign request: {}", request.id);
Ok(())
} else {
Err(SigSocketError::Other("No connected public key".to_string()))
}
} else {
Err(SigSocketError::Other("No SigSocket client available".to_string()))
}
});
// If storage failed, return error
if let Err(e) = store_result {
console_log!("❌ WASM: Failed to store request: {:?}", e);
return Err(e);
}
// Create event object for JavaScript notification
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()))?;
// Notify the extension
match self.callback.call1(&wasm_bindgen::JsValue::NULL, &event) {
Ok(_) => {
console_log!("✅ WASM: 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!("❌ WASM: 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 smart connection management
///
/// This handles all connection logic:
/// - Reuses existing connection if same workspace
/// - Switches connection if different workspace
/// - Creates new connection if none exists
///
/// # 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. Check if already connected to same workspace and handle disconnection
let should_connect = SIGSOCKET_CLIENT.with(|c| {
let mut client_opt = c.borrow_mut();
// Check if we already have a client for this workspace
if let Some(existing_client) = client_opt.as_ref() {
if let Some(existing_key) = existing_client.connected_public_key() {
if existing_key == hex::encode(&public_key_bytes) && existing_client.is_connected() {
console_log!("🔄 WASM: Already connected to workspace: {}", workspace);
return false; // Reuse existing connection
} else {
console_log!("🔄 WASM: Switching workspace from {} to {}",
existing_key, hex::encode(&public_key_bytes));
// Disconnect the old client
*client_opt = None; // This will drop the old client and close WebSocket
console_log!("🔌 WASM: Disconnected from old workspace");
return true; // Need new connection
}
}
}
true // Need new connection, no old one to disconnect
});
// 4. Create and connect if needed
if should_connect {
console_log!("🔗 WASM: Creating new connection for workspace: {}", workspace);
// Create new client
let mut client = SigSocketClient::new(server_url, public_key_bytes.clone())
.map_err(|e| JsValue::from_str(&format!("Failed to create client: {:?}", e)))?;
// Set up extension notification handler
let handler = ExtensionNotificationHandler::new(event_callback.clone());
client.set_sign_handler(handler);
// Connect to the WebSocket server
client.connect().await
.map_err(|e| JsValue::from_str(&format!("Connection failed: {:?}", e)))?;
console_log!("✅ WASM: Connected to SigSocket server for workspace: {}", workspace);
// 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() {
let workspace_info = client.connected_public_key()
.map(|key| key[..16].to_string())
.unwrap_or_else(|| "unknown".to_string());
// Dropping the client will close the WebSocket connection
drop(client);
console_log!("🔌 WASM: Disconnected SigSocket client (was: {}...)", workspace_info);
} else {
console_log!("🔌 WASM: No SigSocket client to disconnect");
}
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(&current_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_hex = signature_result.as_string()
.ok_or_else(|| JsValue::from_str("Signature result is not a string"))?;
// Convert hex signature to base64 for SigSocket protocol
let signature_bytes = hex::decode(&signature_hex)
.map_err(|e| JsValue::from_str(&format!("Invalid hex signature: {}", e)))?;
let signature_base64 = base64::prelude::BASE64_STANDARD.encode(&signature_bytes);
// 4. Get original message for response
let original_message = 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"))?;
Ok::<String, JsValue>(request.request.message.clone())
})?;
// 5. Send response to server (create a new scope to avoid borrowing issues)
{
let client_ref = SIGSOCKET_CLIENT.with(|c| {
c.borrow().as_ref().map(|client| client as *const SigSocketClient)
}).ok_or_else(|| JsValue::from_str("Not connected"))?;
// SAFETY: We know the client exists and we're using it synchronously
let client = unsafe { &*client_ref };
client.send_response(request_id, &original_message, &signature_base64).await
.map_err(|e| JsValue::from_str(&format!("Failed to send response: {:?}", e)))?;
console_log!("✅ WASM: Sent signature response to server for request: {}", request_id);
}
// 6. Remove the request after successful send
SIGSOCKET_CLIENT.with(|c| {
let mut client = c.borrow_mut();
if let Some(client) = client.as_mut() {
client.remove_pending_request(request_id);
console_log!("✅ WASM: Removed request from pending list: {}", request_id);
}
});
console_log!("🎉 WASM: Successfully approved and sent signature for request: {}", request_id);
Ok(signature_base64)
}
/// 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> {
// Send rejection to server first
{
let client_ref = SIGSOCKET_CLIENT.with(|c| {
c.borrow().as_ref().map(|client| client as *const SigSocketClient)
}).ok_or_else(|| JsValue::from_str("Not connected"))?;
// SAFETY: We know the client exists and we're using it synchronously
let client = unsafe { &*client_ref };
client.send_rejection(request_id, reason).await
.map_err(|e| JsValue::from_str(&format!("Failed to send rejection: {:?}", e)))?;
console_log!("✅ WASM: Sent rejection to server for request: {}", request_id);
}
// Remove the request after successful send
SIGSOCKET_CLIENT.with(|c| {
let mut client = c.borrow_mut();
if let Some(client) = client.as_mut() {
client.remove_pending_request(request_id);
console_log!("✅ WASM: Removed rejected request from pending list: {}", request_id);
}
});
console_log!("🚫 WASM: Successfully rejected request: {} (reason: {})", 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(&current_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(&current_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(())
})
}
}

View File

@ -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<JsValue, JsValue> {
// 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<String, JsValue> {
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<JsValue, JsValue> {
{
@ -235,6 +270,79 @@ pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
}
}
/// Get the current keyspace name
#[wasm_bindgen]
pub fn get_current_keyspace_name() -> Result<String, JsValue> {
SESSION_MANAGER.with(|cell| {
if let Some(session) = cell.borrow().as_ref() {
if let Some(keyspace_name) = session.current_keyspace_name() {
Ok(keyspace_name.to_string())
} else {
Err(JsValue::from_str("No keyspace unlocked"))
}
} else {
Err(JsValue::from_str("Session not initialized"))
}
})
}
/// 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<JsValue, JsValue> {
// 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<kvstore::wasm::WasmStore> = 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<JsValue, JsValue> {