876 lines
33 KiB
JavaScript
876 lines
33 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;
|
||
|
||
// Status monitoring
|
||
this.statusMonitorInterval = null;
|
||
this.lastKnownConnectionState = false;
|
||
}
|
||
|
||
/**
|
||
* 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);
|
||
}
|
||
|
||
// Restore any persisted pending requests
|
||
await this.restorePendingRequests();
|
||
|
||
console.log('🔌 SigSocket service initialized with WASM APIs');
|
||
}
|
||
|
||
/**
|
||
* Restore pending requests from persistent storage
|
||
* Only restore requests that match the current workspace
|
||
*/
|
||
async restorePendingRequests() {
|
||
try {
|
||
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
|
||
if (result.sigSocketPendingRequests && Array.isArray(result.sigSocketPendingRequests)) {
|
||
console.log(`🔄 Found ${result.sigSocketPendingRequests.length} stored requests`);
|
||
|
||
// Filter requests for current workspace only
|
||
const currentWorkspaceRequests = result.sigSocketPendingRequests.filter(request =>
|
||
request.target_public_key === this.connectedPublicKey
|
||
);
|
||
|
||
console.log(`🔄 Restoring ${currentWorkspaceRequests.length} requests for current workspace`);
|
||
|
||
// Add each workspace-specific request back to WASM storage
|
||
for (const request of currentWorkspaceRequests) {
|
||
try {
|
||
await this.wasmModule.SigSocketManager.add_pending_request(JSON.stringify(request.request || request));
|
||
console.log(`✅ Restored request: ${request.id || request.request?.id}`);
|
||
} catch (error) {
|
||
console.warn(`Failed to restore request ${request.id || request.request?.id}:`, error);
|
||
}
|
||
}
|
||
|
||
// Update badge after restoration
|
||
this.updateBadge();
|
||
}
|
||
} catch (error) {
|
||
console.warn('Failed to restore pending requests:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Persist pending requests to storage with workspace isolation
|
||
*/
|
||
async persistPendingRequests() {
|
||
try {
|
||
const requests = await this.getFilteredRequests();
|
||
|
||
// Get existing storage to merge with other workspaces
|
||
const result = await chrome.storage.local.get(['sigSocketPendingRequests']);
|
||
const existingRequests = result.sigSocketPendingRequests || [];
|
||
|
||
// Remove old requests for current workspace
|
||
const otherWorkspaceRequests = existingRequests.filter(request =>
|
||
request.target_public_key !== this.connectedPublicKey
|
||
);
|
||
|
||
// Combine with current workspace requests
|
||
const allRequests = [...otherWorkspaceRequests, ...requests];
|
||
|
||
await chrome.storage.local.set({ sigSocketPendingRequests: allRequests });
|
||
console.log(`💾 Persisted ${requests.length} requests for current workspace (${allRequests.length} total)`);
|
||
} catch (error) {
|
||
console.warn('Failed to persist pending requests:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Connect to SigSocket server using WASM APIs
|
||
* WASM handles all connection logic (reuse, switching, etc.)
|
||
* @param {string} workspaceId - The workspace/keyspace identifier
|
||
* @param {number} retryCount - Number of retry attempts (default: 3)
|
||
* @returns {Promise<boolean>} - True if connected successfully
|
||
*/
|
||
async connectToServer(workspaceId, retryCount = 3) {
|
||
for (let attempt = 1; attempt <= retryCount; attempt++) {
|
||
try {
|
||
if (!this.wasmModule?.SigSocketManager) {
|
||
throw new Error('WASM SigSocketManager not available');
|
||
}
|
||
|
||
console.log(`🔗 Requesting SigSocket connection for workspace: ${workspaceId} (attempt ${attempt}/${retryCount})`);
|
||
|
||
// Clean workspace switching
|
||
if (this.currentWorkspace && this.currentWorkspace !== workspaceId) {
|
||
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${workspaceId}`);
|
||
await this.cleanWorkspaceSwitch(workspaceId);
|
||
// Small delay to ensure clean state transition
|
||
await new Promise(resolve => setTimeout(resolve, 300));
|
||
}
|
||
|
||
// 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 = workspaceId; // Use the parameter we passed, not WASM response
|
||
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,
|
||
serverUrl: this.defaultServerUrl
|
||
});
|
||
|
||
// Validate that we have a public key if connected
|
||
if (this.isConnected && !this.connectedPublicKey) {
|
||
console.warn('⚠️ Connected but no public key received - this may cause request issues');
|
||
}
|
||
|
||
// Update badge to show current state
|
||
this.updateBadge();
|
||
|
||
if (this.isConnected) {
|
||
// Clean flow: Connect -> Restore workspace requests -> Update UI
|
||
console.log(`🔗 Connected to workspace: ${workspaceId}, restoring pending requests...`);
|
||
|
||
// 1. Restore requests for this specific workspace
|
||
await this.restorePendingRequests();
|
||
|
||
// 2. Update badge with current count
|
||
this.updateBadge();
|
||
|
||
console.log(`✅ Workspace ${workspaceId} ready with restored requests`);
|
||
return true;
|
||
}
|
||
|
||
// If not connected but no error, try again
|
||
if (attempt < retryCount) {
|
||
console.log(`⏳ Connection not established, retrying in 1 second...`);
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
}
|
||
|
||
} catch (error) {
|
||
// Check if this is an expected "no workspace" error during startup
|
||
const isExpectedStartupError = error.message &&
|
||
(error.message.includes('Workspace not found') ||
|
||
error.message.includes('no keypairs available'));
|
||
|
||
if (isExpectedStartupError && attempt === 1) {
|
||
console.log(`⏳ SigSocket connection attempt ${attempt}: No active workspace (expected after extension reload)`);
|
||
}
|
||
|
||
// Check if this is a public key related error
|
||
if (error.message && error.message.includes('public key')) {
|
||
console.error(`🔑 Public key error detected: ${error.message}`);
|
||
// For public key errors, don't retry immediately - might need workspace change
|
||
if (attempt === 1) {
|
||
console.log(`🔄 Public key error on first attempt, trying to disconnect and reconnect...`);
|
||
await this.disconnect();
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
}
|
||
}
|
||
|
||
if (attempt < retryCount) {
|
||
if (!isExpectedStartupError) {
|
||
console.log(`⏳ Retrying connection in 2 seconds...`);
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
} else {
|
||
// Final attempt failed
|
||
this.isConnected = false;
|
||
this.currentWorkspace = null;
|
||
this.connectedPublicKey = null;
|
||
|
||
if (isExpectedStartupError) {
|
||
console.log(`ℹ️ SigSocket connection failed: No active workspace. Will connect when user logs in.`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Handle events from the WASM SigSocket client
|
||
* This is called automatically when requests arrive
|
||
* @param {Object} event - Event from WASM layer
|
||
*/
|
||
async handleSigSocketEvent(event) {
|
||
console.log('📨 Received SigSocket event:', event);
|
||
|
||
if (event.type === 'sign_request') {
|
||
console.log(`🔐 New sign request: ${event.request_id} for workspace: ${this.currentWorkspace}`);
|
||
|
||
// Clean flow: Request arrives -> Store -> Persist -> Update UI
|
||
try {
|
||
// 1. Request is automatically stored in WASM (already done by WASM layer)
|
||
|
||
// 2. Persist to storage with workspace isolation
|
||
await this.persistPendingRequests();
|
||
|
||
// 3. Update badge count
|
||
this.updateBadge();
|
||
|
||
// 4. Show notification
|
||
this.showSignRequestNotification();
|
||
|
||
// 5. Notify popup if connected
|
||
this.notifyPopupOfNewRequest();
|
||
|
||
console.log(`✅ Request ${event.request_id} processed and stored for workspace: ${this.currentWorkspace}`);
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Failed to process request ${event.request_id}:`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
|
||
// Check if we're connected before attempting approval
|
||
if (!this.isConnected) {
|
||
console.warn(`⚠️ Not connected to SigSocket server, cannot approve request: ${requestId}`);
|
||
throw new Error('Not connected to SigSocket server');
|
||
}
|
||
|
||
// Verify we can approve this request
|
||
const canApprove = await this.canApproveRequest(requestId);
|
||
if (!canApprove) {
|
||
console.warn(`⚠️ Cannot approve request ${requestId} - keyspace may be locked or request not found`);
|
||
throw new Error('Cannot approve request - keyspace may be locked or request not found');
|
||
}
|
||
|
||
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}`);
|
||
|
||
// Clean flow: Approve -> Remove from storage -> Update UI
|
||
// 1. Remove from persistent storage (WASM already removed it)
|
||
await this.persistPendingRequests();
|
||
|
||
// 2. Update badge count
|
||
this.updateBadge();
|
||
|
||
// 3. Notify popup of updated state
|
||
this.notifyPopupOfRequestUpdate();
|
||
|
||
console.log(`✅ Request ${requestId} approved and cleaned up`);
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Failed to approve request ${requestId}:`, error);
|
||
|
||
// Check if this is a connection-related error
|
||
if (error.message && (error.message.includes('Connection not found') || error.message.includes('public key'))) {
|
||
console.error(`🔑 Connection/public key error during approval. Current state:`, {
|
||
connected: this.isConnected,
|
||
workspace: this.currentWorkspace,
|
||
publicKey: this.connectedPublicKey?.substring(0, 16) + '...'
|
||
});
|
||
|
||
// Try to reconnect for next time
|
||
if (this.currentWorkspace) {
|
||
console.log(`🔄 Attempting to reconnect to workspace: ${this.currentWorkspace}`);
|
||
setTimeout(() => this.connectToServer(this.currentWorkspace), 1000);
|
||
}
|
||
}
|
||
|
||
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}`);
|
||
|
||
// Clean flow: Reject -> Remove from storage -> Update UI
|
||
// 1. Remove from persistent storage (WASM already removed it)
|
||
await this.persistPendingRequests();
|
||
|
||
// 2. Update badge count
|
||
this.updateBadge();
|
||
|
||
// 3. Notify popup of updated state
|
||
this.notifyPopupOfRequestUpdate();
|
||
|
||
console.log(`✅ Request ${requestId} rejected and cleaned up`);
|
||
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 clickable notification for new sign request
|
||
* Call this AFTER the request has been stored and persisted
|
||
*/
|
||
async showSignRequestNotification() {
|
||
try {
|
||
if (chrome.notifications && chrome.notifications.create) {
|
||
// Small delay to ensure request is fully stored
|
||
await new Promise(resolve => setTimeout(resolve, 100));
|
||
|
||
console.log(`📢 Preparing notification for new signature request`);
|
||
|
||
// Check if keyspace is currently unlocked to customize message
|
||
let message = 'New signature request received. Click to review and approve.';
|
||
let title = 'SigSocket Sign Request';
|
||
|
||
// Try to determine if keyspace is locked
|
||
try {
|
||
const requests = await this.getPendingRequests();
|
||
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : false;
|
||
if (!canApprove) {
|
||
message = 'New signature request received. Click to unlock keyspace and approve.';
|
||
title = 'SigSocket Request';
|
||
}
|
||
} catch (error) {
|
||
// If we can't check, use generic message
|
||
message = 'New signature request received. Click to open extension.';
|
||
}
|
||
|
||
// Create clickable notification with unique ID
|
||
const notificationId = `sigsocket-request-${Date.now()}`;
|
||
|
||
const notificationOptions = {
|
||
type: 'basic',
|
||
iconUrl: 'icons/icon48.png',
|
||
title: title,
|
||
message: message,
|
||
requireInteraction: true // Keep notification visible until user interacts
|
||
};
|
||
|
||
console.log(`📢 Creating notification: ${notificationId}`, notificationOptions);
|
||
|
||
chrome.notifications.create(notificationId, notificationOptions, (createdId) => {
|
||
if (chrome.runtime.lastError) {
|
||
console.error('❌ Failed to create notification:', chrome.runtime.lastError);
|
||
} else {
|
||
console.log(`✅ Notification created successfully: ${createdId}`);
|
||
}
|
||
});
|
||
} 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.lastKnownConnectionState = false;
|
||
|
||
// Stop status monitoring
|
||
this.stopStatusMonitoring();
|
||
|
||
this.updateBadge();
|
||
|
||
console.log('🔌 SigSocket disconnection requested');
|
||
|
||
} catch (error) {
|
||
console.error('Failed to disconnect:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clear persisted pending requests from storage
|
||
*/
|
||
async clearPersistedRequests() {
|
||
try {
|
||
await chrome.storage.local.remove(['sigSocketPendingRequests']);
|
||
console.log('🗑️ Cleared persisted pending requests from storage');
|
||
} catch (error) {
|
||
console.warn('Failed to clear persisted requests:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clean workspace switch - clear current workspace requests only
|
||
*/
|
||
async cleanWorkspaceSwitch(newWorkspace) {
|
||
try {
|
||
console.log(`🔄 Clean workspace switch: ${this.currentWorkspace} -> ${newWorkspace}`);
|
||
|
||
// 1. Persist current workspace requests before switching
|
||
if (this.currentWorkspace && this.isConnected) {
|
||
await this.persistPendingRequests();
|
||
console.log(`💾 Saved requests for workspace: ${this.currentWorkspace}`);
|
||
}
|
||
|
||
// 2. Clear WASM state (will be restored for new workspace)
|
||
if (this.wasmModule?.SigSocketManager) {
|
||
await this.wasmModule.SigSocketManager.clear_pending_requests();
|
||
console.log('🧹 Cleared WASM request state');
|
||
}
|
||
|
||
// 3. Reset local state
|
||
this.currentWorkspace = null;
|
||
this.connectedPublicKey = null;
|
||
this.isConnected = false;
|
||
|
||
console.log('✅ Workspace switch cleanup completed');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to clean workspace switch:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get connection status with real connection verification
|
||
* @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
|
||
};
|
||
}
|
||
|
||
// Get WASM status first
|
||
const statusJson = await this.wasmModule.SigSocketManager.get_connection_status();
|
||
const status = JSON.parse(statusJson);
|
||
|
||
// Verify connection by trying to get requests (this will fail if not connected)
|
||
let actuallyConnected = false;
|
||
let requests = [];
|
||
|
||
try {
|
||
requests = await this.getPendingRequests();
|
||
// If we can get requests and WASM says connected, we're probably connected
|
||
actuallyConnected = status.is_connected && Array.isArray(requests);
|
||
} catch (error) {
|
||
// If getting requests fails, we're definitely not connected
|
||
console.warn('Connection verification failed:', error);
|
||
actuallyConnected = false;
|
||
}
|
||
|
||
// Update our internal state
|
||
this.isConnected = actuallyConnected;
|
||
|
||
if (status.connected_public_key && actuallyConnected) {
|
||
this.connectedPublicKey = status.connected_public_key;
|
||
} else {
|
||
this.connectedPublicKey = null;
|
||
}
|
||
|
||
// If we're disconnected, clear our workspace
|
||
if (!actuallyConnected) {
|
||
this.currentWorkspace = null;
|
||
}
|
||
|
||
const statusResult = {
|
||
isConnected: actuallyConnected,
|
||
workspace: this.currentWorkspace,
|
||
publicKey: status.connected_public_key,
|
||
pendingRequestCount: requests.length,
|
||
serverUrl: this.defaultServerUrl,
|
||
// Clean flow status indicators
|
||
cleanFlowReady: actuallyConnected && this.currentWorkspace && status.connected_public_key
|
||
};
|
||
|
||
console.log('📊 Clean flow status:', {
|
||
connected: statusResult.isConnected,
|
||
workspace: statusResult.workspace,
|
||
requestCount: statusResult.pendingRequestCount,
|
||
flowReady: statusResult.cleanFlowReady
|
||
});
|
||
|
||
return statusResult;
|
||
|
||
} catch (error) {
|
||
console.error('Failed to get status:', error);
|
||
// Clear state on error
|
||
this.isConnected = false;
|
||
this.currentWorkspace = null;
|
||
this.connectedPublicKey = null;
|
||
return {
|
||
isConnected: false,
|
||
workspace: null,
|
||
publicKey: null,
|
||
pendingRequestCount: 0,
|
||
serverUrl: this.defaultServerUrl
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Set the popup port for communication
|
||
* @param {chrome.runtime.Port|null} port - The popup port or null to disconnect
|
||
*/
|
||
setPopupPort(port) {
|
||
this.popupPort = port;
|
||
|
||
if (port) {
|
||
console.log('📱 Popup connected to SigSocket service');
|
||
|
||
// Immediately check connection status when popup opens
|
||
this.checkConnectionStatusNow();
|
||
|
||
// Start monitoring connection status when popup connects
|
||
this.startStatusMonitoring();
|
||
} else {
|
||
console.log('📱 Popup disconnected from SigSocket service');
|
||
// Stop monitoring when popup disconnects
|
||
this.stopStatusMonitoring();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Immediately check and update connection status
|
||
*/
|
||
async checkConnectionStatusNow() {
|
||
try {
|
||
// Force a fresh connection check
|
||
const currentStatus = await this.getStatusWithConnectionTest();
|
||
this.lastKnownConnectionState = currentStatus.isConnected;
|
||
|
||
// Notify popup of current status
|
||
this.notifyPopupOfStatusChange(currentStatus);
|
||
|
||
console.log(`🔍 Immediate status check: ${currentStatus.isConnected ? 'Connected' : 'Disconnected'}`);
|
||
} catch (error) {
|
||
console.warn('Failed to check connection status immediately:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get status with additional connection testing
|
||
*/
|
||
async getStatusWithConnectionTest() {
|
||
const status = await this.getStatus();
|
||
|
||
// If WASM claims we're connected, do an additional verification
|
||
if (status.isConnected) {
|
||
try {
|
||
// Try to get connection status again - if this fails, we're not really connected
|
||
const verifyJson = await this.wasmModule.SigSocketManager.get_connection_status();
|
||
const verifyStatus = JSON.parse(verifyJson);
|
||
|
||
if (!verifyStatus.is_connected) {
|
||
console.log('🔍 Connection verification failed - marking as disconnected');
|
||
status.isConnected = false;
|
||
this.isConnected = false;
|
||
this.currentWorkspace = null;
|
||
}
|
||
} catch (error) {
|
||
console.log('🔍 Connection test failed - marking as disconnected:', error.message);
|
||
status.isConnected = false;
|
||
this.isConnected = false;
|
||
this.currentWorkspace = null;
|
||
}
|
||
}
|
||
|
||
return status;
|
||
}
|
||
|
||
/**
|
||
* Start periodic status monitoring to detect connection changes
|
||
*/
|
||
startStatusMonitoring() {
|
||
// Clear any existing monitoring
|
||
if (this.statusMonitorInterval) {
|
||
clearInterval(this.statusMonitorInterval);
|
||
}
|
||
|
||
// Check status every 2 seconds when popup is open (more responsive)
|
||
this.statusMonitorInterval = setInterval(async () => {
|
||
if (this.popupPort) {
|
||
try {
|
||
const currentStatus = await this.getStatusWithConnectionTest();
|
||
|
||
// Check if connection status changed
|
||
if (currentStatus.isConnected !== this.lastKnownConnectionState) {
|
||
console.log(`🔄 Connection state changed: ${this.lastKnownConnectionState} -> ${currentStatus.isConnected}`);
|
||
this.lastKnownConnectionState = currentStatus.isConnected;
|
||
|
||
// Notify popup of status change
|
||
this.notifyPopupOfStatusChange(currentStatus);
|
||
}
|
||
} catch (error) {
|
||
console.warn('Status monitoring error:', error);
|
||
// On error, assume disconnected
|
||
if (this.lastKnownConnectionState !== false) {
|
||
console.log('🔄 Status monitoring error - marking as disconnected');
|
||
this.lastKnownConnectionState = false;
|
||
this.notifyPopupOfStatusChange({
|
||
isConnected: false,
|
||
workspace: null,
|
||
publicKey: null,
|
||
pendingRequestCount: 0,
|
||
serverUrl: this.defaultServerUrl
|
||
});
|
||
}
|
||
}
|
||
} else {
|
||
// Stop monitoring when popup is closed
|
||
this.stopStatusMonitoring();
|
||
}
|
||
}, 2000); // 2 seconds for better responsiveness
|
||
}
|
||
|
||
/**
|
||
* Stop status monitoring
|
||
*/
|
||
stopStatusMonitoring() {
|
||
if (this.statusMonitorInterval) {
|
||
clearInterval(this.statusMonitorInterval);
|
||
this.statusMonitorInterval = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Notify popup of connection status change
|
||
* @param {Object} status - Current connection status
|
||
*/
|
||
notifyPopupOfStatusChange(status) {
|
||
if (this.popupPort) {
|
||
this.popupPort.postMessage({
|
||
type: 'CONNECTION_STATUS_CHANGED',
|
||
status: status
|
||
});
|
||
console.log(`📡 Notified popup of connection status change: ${status.isConnected ? 'Connected' : 'Disconnected'}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Called when keyspace is unlocked - clean approach to show pending requests
|
||
*/
|
||
async onKeypaceUnlocked() {
|
||
try {
|
||
console.log('🔓 Keyspace unlocked - preparing to show pending requests');
|
||
|
||
// 1. Restore any persisted requests for this workspace
|
||
await this.restorePendingRequests();
|
||
|
||
// 2. Get current requests (includes restored + any new ones)
|
||
const requests = await this.getPendingRequests();
|
||
|
||
// 3. Check if we can approve requests (keyspace should be unlocked now)
|
||
const canApprove = requests.length > 0 ? await this.canApproveRequest(requests[0].id) : true;
|
||
|
||
// 4. Update badge with current count
|
||
this.updateBadge();
|
||
|
||
// 5. Notify popup if connected
|
||
if (this.popupPort) {
|
||
this.popupPort.postMessage({
|
||
type: 'KEYSPACE_UNLOCKED',
|
||
canApprove,
|
||
pendingRequests: requests
|
||
});
|
||
}
|
||
|
||
console.log(`🔓 Keyspace unlocked: ${requests.length} requests ready, canApprove: ${canApprove}`);
|
||
|
||
return requests;
|
||
|
||
} catch (error) {
|
||
console.error('Failed to handle keyspace unlock:', error);
|
||
return [];
|
||
}
|
||
}
|
||
}
|
||
|
||
// Export for use in background script
|
||
export default SigSocketService; |