410 lines
13 KiB
JavaScript
410 lines
13 KiB
JavaScript
/**
|
|
* 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; |