Compare commits
No commits in common. "c641d0ae2e05412a9b6ac6f35674758684cff28b" and "b0b6359be1388a71c4d72b85a47fe337c8ceb0a4" have entirely different histories.
c641d0ae2e
...
b0b6359be1
@ -5,5 +5,5 @@ members = [
|
|||||||
"vault",
|
"vault",
|
||||||
"evm_client",
|
"evm_client",
|
||||||
"wasm_app",
|
"wasm_app",
|
||||||
"sigsocket_client",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
7
Makefile
7
Makefile
@ -26,7 +26,6 @@ build-wasm-app:
|
|||||||
cd wasm_app && wasm-pack build --target web
|
cd wasm_app && wasm-pack build --target web
|
||||||
|
|
||||||
# Build Hero Vault extension: wasm, copy, then extension
|
# Build Hero Vault extension: wasm, copy, then extension
|
||||||
build-crypto-vault-extension: build-wasm-app
|
build-hero-vault-extension:
|
||||||
cp wasm_app/pkg/wasm_app* crypto_vault_extension/wasm/
|
cd wasm_app && wasm-pack build --target web
|
||||||
cp wasm_app/pkg/*.d.ts crypto_vault_extension/wasm/
|
cd hero_vault_extension && npm run build
|
||||||
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
|
### Native
|
||||||
```sh
|
```sh
|
||||||
cargo check --workspace
|
cargo check --workspace --features kvstore/native
|
||||||
cargo test --workspace
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### WASM
|
### WASM (kvstore only)
|
||||||
```sh
|
```sh
|
||||||
make test-browser-all
|
cd kvstore
|
||||||
|
wasm-pack test --headless --firefox --features web
|
||||||
```
|
```
|
||||||
|
|
||||||
# Rhai Scripting System
|
# Rhai Scripting System
|
||||||
|
26
build.sh
26
build.sh
@ -17,22 +17,32 @@ cd "$(dirname "$0")/wasm_app" || exit 1
|
|||||||
wasm-pack build --target web
|
wasm-pack build --target web
|
||||||
echo -e "${GREEN}✓ WASM build successful!${RESET}"
|
echo -e "${GREEN}✓ WASM build successful!${RESET}"
|
||||||
|
|
||||||
# Step 2: Prepare the frontend extension
|
# Step 2: Build the frontend extension
|
||||||
echo -e "${BLUE}Preparing frontend extension...${RESET}"
|
echo -e "${BLUE}Building frontend extension...${RESET}"
|
||||||
cd ../crypto_vault_extension || exit 1
|
cd ../hero_vault_extension || exit 1
|
||||||
|
|
||||||
# Copy WASM files to the extension's public directory
|
# Copy WASM files to the extension's public directory
|
||||||
echo "Copying WASM files..."
|
echo "Copying WASM files..."
|
||||||
cp ../wasm_app/pkg/wasm_app* wasm/
|
mkdir -p public/wasm
|
||||||
cp ../wasm_app/pkg/*.d.ts wasm/
|
cp ../wasm_app/pkg/wasm_app* public/wasm/
|
||||||
cp ../wasm_app/pkg/*.js wasm/
|
cp ../wasm_app/pkg/*.d.ts public/wasm/
|
||||||
|
cp ../wasm_app/pkg/package.json public/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 -e "${GREEN}=== Build Complete ===${RESET}"
|
||||||
echo "Extension is ready in: $(pwd)"
|
echo "Extension is ready in: $(pwd)/dist"
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${BLUE}To load the extension in Chrome:${RESET}"
|
echo -e "${BLUE}To load the extension in Chrome:${RESET}"
|
||||||
echo "1. Go to chrome://extensions/"
|
echo "1. Go to chrome://extensions/"
|
||||||
echo "2. Enable Developer mode (toggle in top-right)"
|
echo "2. Enable Developer mode (toggle in top-right)"
|
||||||
echo "3. Click 'Load unpacked'"
|
echo "3. Click 'Load unpacked'"
|
||||||
echo "4. Select the $(pwd) directory"
|
echo "4. Select the 'dist' directory: $(pwd)/dist"
|
||||||
|
@ -6,9 +6,6 @@ let sessionTimeoutDuration = 15; // Default 15 seconds
|
|||||||
let sessionTimeoutId = null; // Background timer
|
let sessionTimeoutId = null; // Background timer
|
||||||
let popupPort = null; // Track popup connection
|
let popupPort = null; // Track popup connection
|
||||||
|
|
||||||
// SigSocket service instance
|
|
||||||
let sigSocketService = null;
|
|
||||||
|
|
||||||
// Utility function to convert Uint8Array to hex
|
// Utility function to convert Uint8Array to hex
|
||||||
function toHex(uint8Array) {
|
function toHex(uint8Array) {
|
||||||
return Array.from(uint8Array)
|
return Array.from(uint8Array)
|
||||||
@ -138,9 +135,8 @@ async function restoreSession() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import WASM module functions and SigSocket service
|
// Import WASM module functions
|
||||||
import init, * as wasmFunctions from './wasm/wasm_app.js';
|
import init, * as wasmFunctions from './wasm/wasm_app.js';
|
||||||
import SigSocketService from './background/sigsocket.js';
|
|
||||||
|
|
||||||
// Initialize WASM module
|
// Initialize WASM module
|
||||||
async function initVault() {
|
async function initVault() {
|
||||||
@ -155,13 +151,6 @@ async function initVault() {
|
|||||||
vault = wasmFunctions;
|
vault = wasmFunctions;
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
|
|
||||||
// Initialize SigSocket service
|
|
||||||
if (!sigSocketService) {
|
|
||||||
sigSocketService = new SigSocketService();
|
|
||||||
await sigSocketService.initialize(vault);
|
|
||||||
console.log('🔌 SigSocket service initialized');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to restore previous session
|
// Try to restore previous session
|
||||||
await restoreSession();
|
await restoreSession();
|
||||||
|
|
||||||
@ -183,20 +172,6 @@ const messageHandlers = {
|
|||||||
initSession: async (request) => {
|
initSession: async (request) => {
|
||||||
await vault.init_session(request.keyspace, request.password);
|
await vault.init_session(request.keyspace, request.password);
|
||||||
await sessionManager.save(request.keyspace);
|
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 };
|
return { success: true };
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -286,62 +261,6 @@ const messageHandlers = {
|
|||||||
await chrome.storage.local.set({ sessionTimeout: request.timeout });
|
await chrome.storage.local.set({ sessionTimeout: request.timeout });
|
||||||
resetSessionTimeout(); // Restart with new duration
|
resetSessionTimeout(); // Restart with new duration
|
||||||
return { success: true };
|
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 };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -383,11 +302,6 @@ chrome.runtime.onConnect.addListener((port) => {
|
|||||||
// Track popup connection
|
// Track popup connection
|
||||||
popupPort = port;
|
popupPort = port;
|
||||||
|
|
||||||
// Connect SigSocket service to popup
|
|
||||||
if (sigSocketService) {
|
|
||||||
sigSocketService.setPopupPort(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have an active session, ensure keep-alive is running
|
// If we have an active session, ensure keep-alive is running
|
||||||
if (currentSession) {
|
if (currentSession) {
|
||||||
startKeepAlive();
|
startKeepAlive();
|
||||||
@ -397,11 +311,6 @@ chrome.runtime.onConnect.addListener((port) => {
|
|||||||
// Popup closed, clear reference and stop keep-alive
|
// Popup closed, clear reference and stop keep-alive
|
||||||
popupPort = null;
|
popupPort = null;
|
||||||
stopKeepAlive();
|
stopKeepAlive();
|
||||||
|
|
||||||
// Disconnect SigSocket service from popup
|
|
||||||
if (sigSocketService) {
|
|
||||||
sigSocketService.setPopupPort(null);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -1,410 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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;
|
|
@ -1,75 +0,0 @@
|
|||||||
# 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
|
|
@ -1,232 +0,0 @@
|
|||||||
#!/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;
|
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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,8 +6,7 @@
|
|||||||
|
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"activeTab",
|
"activeTab"
|
||||||
"notifications"
|
|
||||||
],
|
],
|
||||||
|
|
||||||
"icons": {
|
"icons": {
|
||||||
|
@ -73,50 +73,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="vault-header">
|
||||||
<h2>Your Keypairs</h2>
|
<h2>Your Keypairs</h2>
|
||||||
<button id="toggleAddKeypairBtn" class="btn btn-primary">
|
<button id="toggleAddKeypairBtn" class="btn btn-primary">
|
||||||
|
@ -931,293 +931,4 @@ const verifySignature = () => performCryptoOperation({
|
|||||||
<span>${text}</span>
|
<span>${text}</span>
|
||||||
</div>`;
|
</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');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,443 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,183 +1069,4 @@ input::placeholder, textarea::placeholder {
|
|||||||
.verification-icon svg {
|
.verification-icon svg {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 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%);
|
|
||||||
}
|
}
|
@ -1,114 +0,0 @@
|
|||||||
# 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,42 +277,6 @@ export function is_unlocked() {
|
|||||||
return ret !== 0;
|
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
|
* Get all keypairs from the current session
|
||||||
* Returns an array of keypair objects with id, type, and metadata
|
* Returns an array of keypair objects with id, type, and metadata
|
||||||
@ -359,7 +323,7 @@ function passArray8ToWasm0(arg, malloc) {
|
|||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Sign message with current session (requires selected keypair)
|
* Sign message with current session
|
||||||
* @param {Uint8Array} message
|
* @param {Uint8Array} message
|
||||||
* @returns {Promise<any>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
@ -370,41 +334,6 @@ export function sign(message) {
|
|||||||
return ret;
|
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
|
* Verify a signature with the current session's selected keypair
|
||||||
* @param {Uint8Array} message
|
* @param {Uint8Array} message
|
||||||
@ -466,391 +395,24 @@ export function run_rhai(script) {
|
|||||||
return takeFromExternrefTable0(ret[0]);
|
return takeFromExternrefTable0(ret[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_34(arg0, arg1, arg2) {
|
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||||
wasm.closure174_externref_shim(arg0, arg1, arg2);
|
wasm.closure121_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_39(arg0, arg1) {
|
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||||
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__ha4436a3f79fb1a0f(arg0, arg1);
|
wasm.closure150_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_44(arg0, arg1, arg2) {
|
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||||
wasm.closure237_externref_shim(arg0, arg1, arg2);
|
wasm.closure227_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_49(arg0, arg1) {
|
function __wbg_adapter_138(arg0, arg1, arg2, arg3) {
|
||||||
wasm._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf148c54a4a246cea(arg0, arg1);
|
wasm.closure1879_externref_shim(arg0, arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 __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) {
|
async function __wbg_load(module, imports) {
|
||||||
if (typeof Response === 'function' && module instanceof Response) {
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||||
@ -897,9 +459,6 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.call(arg1, arg2);
|
const ret = arg0.call(arg1, arg2);
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments) };
|
}, 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) {
|
imports.wbg.__wbg_createObjectStore_d2f9e1016f4d81b9 = function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
const ret = arg0.createObjectStore(getStringFromWasm0(arg1, arg2), arg3);
|
||||||
return ret;
|
return ret;
|
||||||
@ -908,10 +467,6 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.crypto;
|
const ret = arg0.crypto;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_data_432d9c3df2630942 = function(arg0) {
|
|
||||||
const ret = arg0.data;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
|
||||||
console.error(arg0);
|
console.error(arg0);
|
||||||
};
|
};
|
||||||
@ -984,23 +539,10 @@ function __wbg_get_imports() {
|
|||||||
const ret = result;
|
const ret = result;
|
||||||
return ret;
|
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) {
|
imports.wbg.__wbg_length_52b6c4580c5ec934 = function(arg0) {
|
||||||
const ret = arg0.length;
|
const ret = arg0.length;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_log_c222819a41e063d3 = function(arg0) {
|
|
||||||
console.log(arg0);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
imports.wbg.__wbg_msCrypto_a61aeb35a24c1329 = function(arg0) {
|
||||||
const ret = arg0.msCrypto;
|
const ret = arg0.msCrypto;
|
||||||
return ret;
|
return ret;
|
||||||
@ -1016,7 +558,7 @@ function __wbg_get_imports() {
|
|||||||
const a = state0.a;
|
const a = state0.a;
|
||||||
state0.a = 0;
|
state0.a = 0;
|
||||||
try {
|
try {
|
||||||
return __wbg_adapter_207(a, state0.b, arg0, arg1);
|
return __wbg_adapter_138(a, state0.b, arg0, arg1);
|
||||||
} finally {
|
} finally {
|
||||||
state0.a = a;
|
state0.a = a;
|
||||||
}
|
}
|
||||||
@ -1035,10 +577,6 @@ function __wbg_get_imports() {
|
|||||||
const ret = new Array();
|
const ret = new Array();
|
||||||
return ret;
|
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) {
|
imports.wbg.__wbg_new_a12002a7f91c75be = function(arg0) {
|
||||||
const ret = new Uint8Array(arg0);
|
const ret = new Uint8Array(arg0);
|
||||||
return ret;
|
return ret;
|
||||||
@ -1071,12 +609,6 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
const ret = arg0.objectStore(getStringFromWasm0(arg1, arg2));
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments) };
|
}, 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) {
|
imports.wbg.__wbg_open_88b1390d99a7c691 = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
const ret = arg0.open(getStringFromWasm0(arg1, arg2));
|
||||||
return ret;
|
return ret;
|
||||||
@ -1111,10 +643,6 @@ function __wbg_get_imports() {
|
|||||||
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
imports.wbg.__wbg_randomFillSync_ac0988aba3254290 = function() { return handleError(function (arg0, arg1) {
|
||||||
arg0.randomFillSync(arg1);
|
arg0.randomFillSync(arg1);
|
||||||
}, arguments) };
|
}, arguments) };
|
||||||
imports.wbg.__wbg_readyState_7ef6e63c349899ed = function(arg0) {
|
|
||||||
const ret = arg0.readyState;
|
|
||||||
return ret;
|
|
||||||
};
|
|
||||||
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
imports.wbg.__wbg_require_60cc747a6bc5215a = function() { return handleError(function () {
|
||||||
const ret = module.require;
|
const ret = module.require;
|
||||||
return ret;
|
return ret;
|
||||||
@ -1127,38 +655,12 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.result;
|
const ret = arg0.result;
|
||||||
return ret;
|
return ret;
|
||||||
}, arguments) };
|
}, 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) {
|
imports.wbg.__wbg_set_65595bdd868b3009 = function(arg0, arg1, arg2) {
|
||||||
arg0.set(arg1, arg2 >>> 0);
|
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) {
|
imports.wbg.__wbg_setonerror_d7e3056cc6e56085 = function(arg0, arg1) {
|
||||||
arg0.onerror = 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) {
|
imports.wbg.__wbg_setonsuccess_afa464ee777a396d = function(arg0, arg1) {
|
||||||
arg0.onsuccess = arg1;
|
arg0.onsuccess = arg1;
|
||||||
};
|
};
|
||||||
@ -1193,10 +695,6 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.then(arg1);
|
const ret = arg0.then(arg1);
|
||||||
return ret;
|
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) {
|
imports.wbg.__wbg_transaction_d6d07c3c9963c49e = function() { return handleError(function (arg0, arg1, arg2) {
|
||||||
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
const ret = arg0.transaction(arg1, __wbindgen_enum_IdbTransactionMode[arg2]);
|
||||||
return ret;
|
return ret;
|
||||||
@ -1205,9 +703,6 @@ function __wbg_get_imports() {
|
|||||||
const ret = arg0.versions;
|
const ret = arg0.versions;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbg_warn_4ca3906c248c47c4 = function(arg0) {
|
|
||||||
console.warn(arg0);
|
|
||||||
};
|
|
||||||
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
imports.wbg.__wbindgen_cb_drop = function(arg0) {
|
||||||
const obj = arg0.original;
|
const obj = arg0.original;
|
||||||
if (obj.cnt-- == 1) {
|
if (obj.cnt-- == 1) {
|
||||||
@ -1217,40 +712,16 @@ function __wbg_get_imports() {
|
|||||||
const ret = false;
|
const ret = false;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper1015 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 309, __wbg_adapter_52);
|
const ret = makeMutClosure(arg0, arg1, 122, __wbg_adapter_32);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper1320 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper549 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 393, __wbg_adapter_55);
|
const ret = makeMutClosure(arg0, arg1, 151, __wbg_adapter_35);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper423 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper857 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_34);
|
const ret = makeMutClosure(arg0, arg1, 228, __wbg_adapter_38);
|
||||||
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;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||||
@ -1307,14 +778,6 @@ function __wbg_get_imports() {
|
|||||||
const ret = wasm.memory;
|
const ret = wasm.memory;
|
||||||
return ret;
|
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) {
|
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
|
||||||
const ret = getStringFromWasm0(arg0, arg1);
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
return ret;
|
return ret;
|
||||||
|
Binary file not shown.
@ -68,7 +68,7 @@ impl EvmClient {
|
|||||||
mut tx: provider::Transaction,
|
mut tx: provider::Transaction,
|
||||||
signer: &dyn crate::signer::Signer,
|
signer: &dyn crate::signer::Signer,
|
||||||
) -> Result<ethers_core::types::H256, EvmError> {
|
) -> Result<ethers_core::types::H256, EvmError> {
|
||||||
use ethers_core::types::{U256, H256};
|
use ethers_core::types::{U256, H256, Bytes, Address};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use crate::provider::{send_rpc, parse_signature_rs_v};
|
use crate::provider::{send_rpc, parse_signature_rs_v};
|
||||||
@ -131,7 +131,7 @@ impl EvmClient {
|
|||||||
|
|
||||||
// 3. Sign the RLP-encoded unsigned transaction
|
// 3. Sign the RLP-encoded unsigned transaction
|
||||||
let sig = signer.sign(&rlp_unsigned).await?;
|
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)
|
// 4. RLP encode signed transaction (EIP-155)
|
||||||
use rlp::RlpStream;
|
use rlp::RlpStream;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
//! Rhai bindings for EVM Client module
|
//! Rhai bindings for EVM Client module
|
||||||
//! Provides a single source of truth for scripting integration for EVM actions.
|
//! Provides a single source of truth for scripting integration for EVM actions.
|
||||||
|
|
||||||
use rhai::Engine;
|
use rhai::{Engine, Map};
|
||||||
pub use crate::EvmClient; // Ensure EvmClient is public and defined in lib.rs
|
pub use crate::EvmClient; // Ensure EvmClient is public and defined in lib.rs
|
||||||
|
|
||||||
/// Register EVM Client APIs with the Rhai scripting engine.
|
/// 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_type::<RhaiEvmClient>();
|
||||||
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
engine.register_fn("get_balance", RhaiEvmClient::get_balance);
|
||||||
// Register instance for scripts
|
// 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.
|
// Rhai does not support register_global_constant; pass the client as a parameter or use module scope.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
//! These use block_on for native, and should be adapted for WASM as needed.
|
//! These use block_on for native, and should be adapted for WASM as needed.
|
||||||
|
|
||||||
use crate::EvmClient;
|
use crate::EvmClient;
|
||||||
|
use rhai::Map;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_get_balance_real_address() {
|
async fn test_get_balance_real_address() {
|
||||||
|
use ethers_core::types::{Address, U256};
|
||||||
use evm_client::provider::get_balance;
|
use evm_client::provider::get_balance;
|
||||||
|
|
||||||
// Vitalik's address
|
// Vitalik's address
|
||||||
|
1
hero_vault_extension/.gitignore
vendored
1
hero_vault_extension/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
dist
|
|
205
hero_vault_extension/dist/assets/index-b58c7e43.js
vendored
Normal file
205
hero_vault_extension/dist/assets/index-b58c7e43.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
hero_vault_extension/dist/assets/wasm_app-bd9134aa.js
vendored
Normal file
2
hero_vault_extension/dist/assets/wasm_app-bd9134aa.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -130,7 +130,6 @@ store.put(&js_value, Some(&JsValue::from_str(key)))?.await
|
|||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub struct WasmStore;
|
pub struct WasmStore;
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl KVStore for WasmStore {
|
impl KVStore for WasmStore {
|
||||||
@ -140,16 +139,10 @@ impl KVStore for WasmStore {
|
|||||||
async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
|
async fn set(&self, _key: &str, _value: &[u8]) -> Result<()> {
|
||||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
}
|
}
|
||||||
async fn remove(&self, _key: &str) -> Result<()> {
|
async fn delete(&self, _key: &str) -> Result<()> {
|
||||||
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
}
|
}
|
||||||
async fn contains_key(&self, _key: &str) -> Result<bool> {
|
async fn exists(&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()))
|
Err(KVError::Other("WasmStore is only available on wasm32 targets".to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
[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"
|
|
@ -1,214 +0,0 @@
|
|||||||
# 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.
|
|
@ -1,218 +0,0 @@
|
|||||||
# 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
|
|
@ -1,133 +0,0 @@
|
|||||||
//! 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
|
|
||||||
}
|
|
@ -1,384 +0,0 @@
|
|||||||
//! 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
|||||||
//! 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))
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
//! # 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
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,232 +0,0 @@
|
|||||||
//! 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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,256 +0,0 @@
|
|||||||
//! 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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,549 +0,0 @@
|
|||||||
//! 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()))
|
|
||||||
}
|
|
@ -1,162 +0,0 @@
|
|||||||
//! 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);
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
//! 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");
|
|
||||||
}
|
|
@ -1,181 +0,0 @@
|
|||||||
#![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,6 +24,7 @@ use error::VaultError;
|
|||||||
pub use kvstore::traits::KVStore;
|
pub use kvstore::traits::KVStore;
|
||||||
|
|
||||||
use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20};
|
use crate::crypto::cipher::{decrypt_chacha20, encrypt_chacha20};
|
||||||
|
use signature::SignatureEncoding;
|
||||||
// TEMP: File-based debug logger for crypto troubleshooting
|
// TEMP: File-based debug logger for crypto troubleshooting
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
@ -216,7 +217,7 @@ impl<S: KVStore> Vault<S> {
|
|||||||
|
|
||||||
// --- Keypair Management APIs ---
|
// --- Keypair Management APIs ---
|
||||||
|
|
||||||
/// Create a default Secp256k1 keypair for client identity
|
/// Create a default Ed25519 keypair for client identity
|
||||||
/// This keypair is deterministically generated from the password and salt
|
/// This keypair is deterministically generated from the password and salt
|
||||||
/// and will always be the first keypair in the keyspace
|
/// and will always be the first keypair in the keyspace
|
||||||
async fn create_default_keypair(
|
async fn create_default_keypair(
|
||||||
@ -228,32 +229,26 @@ impl<S: KVStore> Vault<S> {
|
|||||||
// 1. Derive a deterministic seed using standard PBKDF2
|
// 1. Derive a deterministic seed using standard PBKDF2
|
||||||
let seed = kdf::keyspace_key(password, salt);
|
let seed = kdf::keyspace_key(password, salt);
|
||||||
|
|
||||||
// 2. Generate Secp256k1 keypair from the seed
|
// 2. Generate Ed25519 keypair from the seed
|
||||||
use k256::ecdsa::{SigningKey, VerifyingKey};
|
use ed25519_dalek::{SigningKey, VerifyingKey};
|
||||||
|
|
||||||
// Use the seed as the private key directly (32 bytes)
|
// Use the seed to create a deterministic keypair
|
||||||
let mut secret_key_bytes = [0u8; 32];
|
let signing = SigningKey::from_bytes(seed.as_slice().try_into().unwrap());
|
||||||
secret_key_bytes.copy_from_slice(&seed[..32]);
|
let verifying: VerifyingKey = (&signing).into();
|
||||||
|
|
||||||
// Create signing key
|
let priv_bytes = signing.to_bytes().to_vec();
|
||||||
let signing_key = SigningKey::from_bytes(&secret_key_bytes.into())
|
let pub_bytes = verifying.to_bytes().to_vec();
|
||||||
.map_err(|e| VaultError::Crypto(format!("Failed to create signing key: {}", e)))?;
|
|
||||||
|
|
||||||
// Get verifying key
|
// Create an ID for the default keypair
|
||||||
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);
|
let id = hex::encode(&pub_bytes);
|
||||||
|
|
||||||
// 3. Unlock keyspace to add the keypair
|
// 3. Unlock the keyspace to get its data
|
||||||
let mut data = self.unlock_keyspace(keyspace, password).await?;
|
let mut data = self.unlock_keyspace(keyspace, password).await?;
|
||||||
|
|
||||||
// 4. Create key entry
|
// 4. Add to keypairs (as the first entry)
|
||||||
let entry = KeyEntry {
|
let entry = KeyEntry {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
key_type: KeyType::Secp256k1,
|
key_type: KeyType::Ed25519,
|
||||||
private_key: priv_bytes,
|
private_key: priv_bytes,
|
||||||
public_key: pub_bytes,
|
public_key: pub_bytes,
|
||||||
metadata: Some(KeyMetadata {
|
metadata: Some(KeyMetadata {
|
||||||
@ -465,15 +460,14 @@ impl<S: KVStore> Vault<S> {
|
|||||||
Ok(sig.to_bytes().to_vec())
|
Ok(sig.to_bytes().to_vec())
|
||||||
}
|
}
|
||||||
KeyType::Secp256k1 => {
|
KeyType::Secp256k1 => {
|
||||||
use k256::ecdsa::{signature::Signer, SigningKey, Signature};
|
use k256::ecdsa::{signature::Signer, SigningKey};
|
||||||
let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| {
|
let arr: &[u8; 32] = key.private_key.as_slice().try_into().map_err(|_| {
|
||||||
VaultError::Crypto("Invalid secp256k1 private key length".to_string())
|
VaultError::Crypto("Invalid secp256k1 private key length".to_string())
|
||||||
})?;
|
})?;
|
||||||
let sk = SigningKey::from_bytes(arr.into())
|
let sk = SigningKey::from_bytes(arr.into())
|
||||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
let sig: Signature = sk.sign(message);
|
let sig: k256::ecdsa::DerSignature = sk.sign(message);
|
||||||
// Return compact signature (64 bytes) instead of DER format
|
Ok(sig.to_vec())
|
||||||
Ok(sig.to_bytes().to_vec())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -517,11 +511,7 @@ impl<S: KVStore> Vault<S> {
|
|||||||
use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
|
use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey};
|
||||||
let pk = VerifyingKey::from_sec1_bytes(&key.public_key)
|
let pk = VerifyingKey::from_sec1_bytes(&key.public_key)
|
||||||
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
// Use compact format (64 bytes) instead of DER
|
let sig = Signature::from_der(signature)
|
||||||
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()))?;
|
.map_err(|e| VaultError::Crypto(e.to_string()))?;
|
||||||
Ok(pk.verify(message, &sig).is_ok())
|
Ok(pk.verify(message, &sig).is_ok())
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use crate::session::SessionManager;
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn register_rhai_api<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static>(
|
pub fn register_rhai_api<S: kvstore::traits::KVStore + Send + Sync + Clone + 'static>(
|
||||||
engine: &mut Engine,
|
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_type::<RhaiSessionManager<S>>();
|
||||||
engine.register_fn("select_keypair", RhaiSessionManager::<S>::select_keypair);
|
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
|
// Use Mutex for interior mutability, &self is sufficient
|
||||||
self.inner.lock().unwrap().select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}"))
|
self.inner.lock().unwrap().select_keypair(&key_id).map_err(|e| format!("select_keypair error: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_default_keypair(&self) -> Result<(), String> {
|
pub fn select_default_keypair(&self) -> Result<(), String> {
|
||||||
self.inner.lock().unwrap().select_default_keypair()
|
self.inner.lock().unwrap().select_default_keypair()
|
||||||
.map_err(|e| format!("select_default_keypair error: {e}"))
|
.map_err(|e| format!("select_default_keypair error: {e}"))
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
//! All state is local to the SessionManager instance. No global state.
|
//! All state is local to the SessionManager instance. No global state.
|
||||||
|
|
||||||
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
|
use crate::{KVStore, KeyEntry, KeyspaceData, Vault, VaultError};
|
||||||
|
use std::collections::HashMap;
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
|
/// SessionManager: Ergonomic, stateful wrapper over the Vault stateless API.
|
||||||
@ -129,17 +130,17 @@ impl<S: KVStore + Send + Sync> SessionManager<S> {
|
|||||||
self.current_keyspace()
|
self.current_keyspace()
|
||||||
.and_then(|ks| ks.keypairs.first())
|
.and_then(|ks| ks.keypairs.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects the default keypair (first keypair) as the current keypair.
|
/// Selects the default keypair (first keypair) as the current keypair.
|
||||||
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
|
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
|
||||||
let default_id = self
|
let default_id = self
|
||||||
.default_keypair()
|
.default_keypair()
|
||||||
.map(|k| k.id.clone())
|
.map(|k| k.id.clone())
|
||||||
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
|
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
|
||||||
|
|
||||||
self.select_keypair(&default_id)
|
self.select_keypair(&default_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the current keypair is the default keypair (first keypair).
|
/// Returns true if the current keypair is the default keypair (first keypair).
|
||||||
pub fn is_default_keypair_selected(&self) -> bool {
|
pub fn is_default_keypair_selected(&self) -> bool {
|
||||||
match (self.current_keypair(), self.default_keypair()) {
|
match (self.current_keypair(), self.default_keypair()) {
|
||||||
@ -311,17 +312,17 @@ impl<S: KVStore> SessionManager<S> {
|
|||||||
self.current_keyspace()
|
self.current_keyspace()
|
||||||
.and_then(|ks| ks.keypairs.first())
|
.and_then(|ks| ks.keypairs.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Selects the default keypair (first keypair) as the current keypair.
|
/// Selects the default keypair (first keypair) as the current keypair.
|
||||||
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
|
pub fn select_default_keypair(&mut self) -> Result<(), VaultError> {
|
||||||
let default_id = self
|
let default_id = self
|
||||||
.default_keypair()
|
.default_keypair()
|
||||||
.map(|k| k.id.clone())
|
.map(|k| k.id.clone())
|
||||||
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
|
.ok_or_else(|| VaultError::Crypto("No default keypair found".to_string()))?;
|
||||||
|
|
||||||
self.select_keypair(&default_id)
|
self.select_keypair(&default_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the current keypair is the default keypair (first keypair).
|
/// Returns true if the current keypair is the default keypair (first keypair).
|
||||||
pub fn is_default_keypair_selected(&self) -> bool {
|
pub fn is_default_keypair_selected(&self) -> bool {
|
||||||
match (self.current_keypair(), self.default_keypair()) {
|
match (self.current_keypair(), self.default_keypair()) {
|
||||||
|
@ -26,8 +26,6 @@ async fn test_keypair_management_and_crypto() {
|
|||||||
vault.create_keyspace(keyspace, password, None).await.unwrap();
|
vault.create_keyspace(keyspace, password, None).await.unwrap();
|
||||||
|
|
||||||
debug!("after create_keyspace: keyspace={} password={}", keyspace, hex::encode(password));
|
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");
|
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;
|
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 {
|
match &key_id {
|
||||||
@ -40,7 +38,7 @@ async fn test_keypair_management_and_crypto() {
|
|||||||
|
|
||||||
debug!("before list_keypairs");
|
debug!("before list_keypairs");
|
||||||
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||||
assert_eq!(keys.len(), 3);
|
assert_eq!(keys.len(), 2);
|
||||||
|
|
||||||
debug!("before export Ed25519 keypair");
|
debug!("before export Ed25519 keypair");
|
||||||
let (priv_bytes, pub_bytes) = vault.export_keypair(keyspace, password, &key_id).await.unwrap();
|
let (priv_bytes, pub_bytes) = vault.export_keypair(keyspace, password, &key_id).await.unwrap();
|
||||||
@ -67,5 +65,5 @@ async fn test_keypair_management_and_crypto() {
|
|||||||
// Remove a keypair
|
// Remove a keypair
|
||||||
vault.remove_keypair(keyspace, password, &key_id).await.unwrap();
|
vault.remove_keypair(keyspace, password, &key_id).await.unwrap();
|
||||||
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
let keys = vault.list_keypairs(keyspace, password).await.unwrap();
|
||||||
assert_eq!(keys.len(), 2);
|
assert_eq!(keys.len(), 1);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ async fn session_manager_end_to_end() {
|
|||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
let tmp_dir = TempDir::new().expect("create temp dir");
|
let tmp_dir = TempDir::new().expect("create temp dir");
|
||||||
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("open NativeStore");
|
let store = NativeStore::open(tmp_dir.path().to_str().unwrap()).expect("open NativeStore");
|
||||||
let vault = Vault::new(store);
|
let mut vault = Vault::new(store);
|
||||||
let keyspace = "personal";
|
let keyspace = "personal";
|
||||||
let password = b"testpass";
|
let password = b"testpass";
|
||||||
|
|
||||||
|
@ -32,7 +32,8 @@ async fn test_session_manager_lock_unlock_keypairs_persistence() {
|
|||||||
|
|
||||||
// 3. List, store keys and names
|
// 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(), 3);
|
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!(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 == 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")));
|
assert!(keypairs_before.iter().any(|k| k.0 == id2 && k.3.as_ref().unwrap().name.as_deref() == Some("keypair-two")));
|
||||||
|
|
||||||
|
@ -12,11 +12,10 @@ web-sys = { version = "0.3", features = ["console"] }
|
|||||||
js-sys = "0.3"
|
js-sys = "0.3"
|
||||||
kvstore = { path = "../kvstore" }
|
kvstore = { path = "../kvstore" }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
base64 = "0.22"
|
|
||||||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
gloo-utils = "0.1"
|
gloo-utils = "0.1"
|
||||||
|
|
||||||
#
|
#
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
rhai = { version = "1.16", features = ["serde"] }
|
rhai = { version = "1.16", features = ["serde"] }
|
||||||
@ -24,7 +23,6 @@ wasm-bindgen-futures = "0.4"
|
|||||||
once_cell = "1.21"
|
once_cell = "1.21"
|
||||||
vault = { path = "../vault" }
|
vault = { path = "../vault" }
|
||||||
evm_client = { path = "../evm_client" }
|
evm_client = { path = "../evm_client" }
|
||||||
sigsocket_client = { path = "../sigsocket_client" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
wasm-bindgen-test = "0.3"
|
wasm-bindgen-test = "0.3"
|
||||||
|
@ -24,13 +24,8 @@ pub use vault::session_singleton::SESSION_MANAGER;
|
|||||||
|
|
||||||
// Include the keypair bindings module
|
// Include the keypair bindings module
|
||||||
mod vault_bindings;
|
mod vault_bindings;
|
||||||
mod sigsocket_bindings;
|
|
||||||
pub use vault_bindings::*;
|
pub use vault_bindings::*;
|
||||||
|
|
||||||
// Include the sigsocket module
|
|
||||||
mod sigsocket;
|
|
||||||
pub use sigsocket::*;
|
|
||||||
|
|
||||||
/// Initialize the scripting environment (must be called before run_rhai)
|
/// Initialize the scripting environment (must be called before run_rhai)
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn init_rhai_env() {
|
pub fn init_rhai_env() {
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
//! 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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
//! 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);
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
//! 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;
|
|
@ -1,528 +0,0 @@
|
|||||||
//! 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,41 +120,6 @@ 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
|
/// Get all keypairs from the current session
|
||||||
/// Returns an array of keypair objects with id, type, and metadata
|
/// Returns an array of keypair objects with id, type, and metadata
|
||||||
// #[wasm_bindgen]
|
// #[wasm_bindgen]
|
||||||
@ -249,7 +214,7 @@ pub async fn add_keypair(
|
|||||||
Ok(JsValue::from_str(&key_id))
|
Ok(JsValue::from_str(&key_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign message with current session (requires selected keypair)
|
/// Sign message with current session
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
|
pub async fn sign(message: &[u8]) -> Result<JsValue, JsValue> {
|
||||||
{
|
{
|
||||||
@ -270,79 +235,6 @@ 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
|
/// Verify a signature with the current session's selected keypair
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub async fn verify(message: &[u8], signature: &str) -> Result<JsValue, JsValue> {
|
pub async fn verify(message: &[u8], signature: &str) -> Result<JsValue, JsValue> {
|
||||||
|
Loading…
Reference in New Issue
Block a user