Compare commits
12 Commits
b0b6359be1
...
c641d0ae2e
Author | SHA1 | Date | |
---|---|---|---|
|
c641d0ae2e | ||
|
6f42e5ab8d | ||
|
203cde1cba | ||
|
6b037537bf | ||
|
580fd72dce | ||
|
a0622629ae | ||
|
4e1e707f85 | ||
|
9f143ded9d | ||
|
b0d0aaa53d | ||
|
e00c140396 | ||
|
4ba1e43f4e | ||
|
b82d457873 |
@ -5,5 +5,5 @@ members = [
|
||||
"vault",
|
||||
"evm_client",
|
||||
"wasm_app",
|
||||
"sigsocket_client",
|
||||
]
|
||||
|
||||
|
7
Makefile
7
Makefile
@ -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/
|
||||
|
@ -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
|
||||
|
26
build.sh
26
build.sh
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
410
crypto_vault_extension/background/sigsocket.js
Normal file
410
crypto_vault_extension/background/sigsocket.js
Normal 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;
|
75
crypto_vault_extension/demo/README.md
Normal file
75
crypto_vault_extension/demo/README.md
Normal 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
|
232
crypto_vault_extension/demo/mock_sigsocket_server.js
Normal file
232
crypto_vault_extension/demo/mock_sigsocket_server.js
Normal 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;
|
21
crypto_vault_extension/demo/package.json
Normal file
21
crypto_vault_extension/demo/package.json
Normal 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"
|
||||
}
|
@ -6,7 +6,8 @@
|
||||
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab"
|
||||
"activeTab",
|
||||
"notifications"
|
||||
],
|
||||
|
||||
"icons": {
|
||||
|
@ -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">
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
443
crypto_vault_extension/popup/components/SignRequestManager.js
Normal file
443
crypto_vault_extension/popup/components/SignRequestManager.js
Normal 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;
|
||||
}
|
@ -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%);
|
||||
}
|
114
crypto_vault_extension/test_extension.md
Normal file
114
crypto_vault_extension/test_extension.md
Normal 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
|
@ -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;
|
||||
|
Binary file not shown.
@ -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;
|
||||
|
@ -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.
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
1
hero_vault_extension/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
dist
|
205
hero_vault_extension/dist/assets/index-b58c7e43.js
vendored
205
hero_vault_extension/dist/assets/index-b58c7e43.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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()))
|
||||
}
|
||||
}
|
||||
|
53
sigsocket_client/Cargo.toml
Normal file
53
sigsocket_client/Cargo.toml
Normal 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"
|
214
sigsocket_client/IMPLEMENTATION.md
Normal file
214
sigsocket_client/IMPLEMENTATION.md
Normal 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
218
sigsocket_client/README.md
Normal 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
|
133
sigsocket_client/examples/basic_usage.rs
Normal file
133
sigsocket_client/examples/basic_usage.rs
Normal 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
|
||||
}
|
384
sigsocket_client/src/client.rs
Normal file
384
sigsocket_client/src/client.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
|
168
sigsocket_client/src/error.rs
Normal file
168
sigsocket_client/src/error.rs
Normal 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))
|
||||
}
|
||||
}
|
72
sigsocket_client/src/lib.rs
Normal file
72
sigsocket_client/src/lib.rs
Normal 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
|
||||
};
|
||||
}
|
232
sigsocket_client/src/native.rs
Normal file
232
sigsocket_client/src/native.rs
Normal 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
|
||||
}
|
||||
}
|
256
sigsocket_client/src/protocol.rs
Normal file
256
sigsocket_client/src/protocol.rs
Normal 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());
|
||||
}
|
||||
}
|
549
sigsocket_client/src/wasm.rs
Normal file
549
sigsocket_client/src/wasm.rs
Normal 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()))
|
||||
}
|
162
sigsocket_client/tests/integration_test.rs
Normal file
162
sigsocket_client/tests/integration_test.rs
Normal 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);
|
||||
}
|
92
sigsocket_client/tests/request_management_test.rs
Normal file
92
sigsocket_client/tests/request_management_test.rs
Normal 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");
|
||||
}
|
181
sigsocket_client/tests/wasm_tests.rs
Normal file
181
sigsocket_client/tests/wasm_tests.rs
Normal 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);
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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}"))
|
||||
|
@ -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()) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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")));
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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() {
|
||||
|
168
wasm_app/src/sigsocket/connection.rs
Normal file
168
wasm_app/src/sigsocket/connection.rs
Normal 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()
|
||||
}
|
||||
}
|
51
wasm_app/src/sigsocket/handler.rs
Normal file
51
wasm_app/src/sigsocket/handler.rs
Normal 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);
|
||||
}
|
11
wasm_app/src/sigsocket/mod.rs
Normal file
11
wasm_app/src/sigsocket/mod.rs
Normal 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;
|
528
wasm_app/src/sigsocket_bindings.rs
Normal file
528
wasm_app/src/sigsocket_bindings.rs
Normal 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(¤t_workspace).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?;
|
||||
|
||||
let current_public_key = current_public_key_js.as_string()
|
||||
.ok_or_else(|| JsValue::from_str("Current public key is not a string"))?;
|
||||
|
||||
// 3. Check the request
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||
|
||||
// Get the request
|
||||
let request = client.get_pending_request(request_id)
|
||||
.ok_or_else(|| JsValue::from_str("Request not found"))?;
|
||||
|
||||
// Check if request matches current session
|
||||
let can_approve = request.target_public_key == current_public_key;
|
||||
|
||||
console_log!("Can approve request {}: {} (current: {}, target: {})",
|
||||
request_id, can_approve, current_public_key, request.target_public_key);
|
||||
|
||||
Ok(can_approve)
|
||||
})
|
||||
}
|
||||
|
||||
/// Approve a sign request and send the signature to the server
|
||||
///
|
||||
/// This performs the complete approval flow:
|
||||
/// 1. Validates the request can be approved
|
||||
/// 2. Signs the message using the vault
|
||||
/// 3. Sends the signature to the SigSocket server
|
||||
/// 4. Removes the request from pending list
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_id` - The ID of the request to approve
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(signature)` - Base64-encoded signature that was sent
|
||||
/// * `Err(error)` - If approval failed
|
||||
#[wasm_bindgen]
|
||||
pub async fn approve_request(request_id: &str) -> Result<String, JsValue> {
|
||||
// 1. Validate we can approve this request
|
||||
if !Self::can_approve_request(request_id).await? {
|
||||
return Err(JsValue::from_str("Cannot approve this request"));
|
||||
}
|
||||
|
||||
// 2. Get request details and sign the message
|
||||
let (message_bytes, original_request) = SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected"))?;
|
||||
|
||||
let request = client.get_pending_request(request_id)
|
||||
.ok_or_else(|| JsValue::from_str("Request not found"))?;
|
||||
|
||||
// Decode the message
|
||||
let message_bytes = request.message_bytes()
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid message format: {}", e)))?;
|
||||
|
||||
Ok::<(Vec<u8>, SignRequest), JsValue>((message_bytes, request.request.clone()))
|
||||
})?;
|
||||
|
||||
// 3. Sign with vault
|
||||
let signature_result = sign_with_default_keypair(&message_bytes).await?;
|
||||
let signature_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(¤t_workspace).await
|
||||
.map_err(|e| JsValue::from_str(&format!("Failed to get current public key: {:?}", e)))?;
|
||||
|
||||
let current_public_key = current_public_key_js.as_string()
|
||||
.ok_or_else(|| JsValue::from_str("Current public key is not a string"))?;
|
||||
|
||||
// Filter requests for current workspace
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
let client = client.as_ref().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||
|
||||
let filtered_requests: Vec<_> = client.get_requests_for_public_key(¤t_public_key);
|
||||
|
||||
console_log!("Filtered requests: {} total, {} for current workspace",
|
||||
client.pending_request_count(), filtered_requests.len());
|
||||
|
||||
// Serialize and return
|
||||
serde_json::to_string(&filtered_requests)
|
||||
.map_err(|e| JsValue::from_str(&format!("Serialization error: {}", e)))
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a pending sign request (called when request arrives from server)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `request_json` - JSON string containing the sign request
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Request added successfully
|
||||
/// * `Err(error)` - If adding failed
|
||||
#[wasm_bindgen]
|
||||
pub fn add_pending_request(request_json: &str) -> Result<(), JsValue> {
|
||||
// Parse the request
|
||||
let request: SignRequest = serde_json::from_str(request_json)
|
||||
.map_err(|e| JsValue::from_str(&format!("Invalid request JSON: {}", e)))?;
|
||||
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client = c.borrow_mut();
|
||||
let client = client.as_mut().ok_or_else(|| JsValue::from_str("Not connected to SigSocket"))?;
|
||||
|
||||
// Get the connected public key as the target
|
||||
let target_public_key = client.connected_public_key()
|
||||
.ok_or_else(|| JsValue::from_str("No connected public key"))?
|
||||
.to_string();
|
||||
|
||||
// Add the request
|
||||
client.add_pending_request(request, target_public_key);
|
||||
|
||||
console_log!("Added pending request: {}", request_json);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Get connection status
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(status_json)` - JSON object with connection status
|
||||
/// * `Err(error)` - If getting status failed
|
||||
#[wasm_bindgen]
|
||||
pub fn get_connection_status() -> Result<String, JsValue> {
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let client = c.borrow();
|
||||
|
||||
if let Some(client) = client.as_ref() {
|
||||
let status = serde_json::json!({
|
||||
"is_connected": client.is_connected(),
|
||||
"connected_public_key": client.connected_public_key(),
|
||||
"pending_request_count": client.pending_request_count(),
|
||||
"server_url": client.url()
|
||||
});
|
||||
|
||||
Ok(status.to_string())
|
||||
} else {
|
||||
let status = serde_json::json!({
|
||||
"is_connected": false,
|
||||
"connected_public_key": null,
|
||||
"pending_request_count": 0,
|
||||
"server_url": null
|
||||
});
|
||||
|
||||
Ok(status.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Clear all pending requests
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(())` - Requests cleared successfully
|
||||
#[wasm_bindgen]
|
||||
pub fn clear_pending_requests() -> Result<(), JsValue> {
|
||||
SIGSOCKET_CLIENT.with(|c| {
|
||||
let mut client = c.borrow_mut();
|
||||
if let Some(client) = client.as_mut() {
|
||||
client.clear_pending_requests();
|
||||
console_log!("Cleared all pending requests");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
@ -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> {
|
||||
|
Loading…
Reference in New Issue
Block a user