class CryptoVaultError extends Error { constructor(message, code, retryable = false, userMessage = null) { super(message); this.name = 'CryptoVaultError'; this.code = code; this.retryable = retryable; this.userMessage = userMessage || message; this.timestamp = Date.now(); } } const ERROR_CODES = { NETWORK_ERROR: 'NETWORK_ERROR', TIMEOUT_ERROR: 'TIMEOUT_ERROR', SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', INVALID_PASSWORD: 'INVALID_PASSWORD', SESSION_EXPIRED: 'SESSION_EXPIRED', UNAUTHORIZED: 'UNAUTHORIZED', CRYPTO_ERROR: 'CRYPTO_ERROR', INVALID_SIGNATURE: 'INVALID_SIGNATURE', ENCRYPTION_FAILED: 'ENCRYPTION_FAILED', INVALID_INPUT: 'INVALID_INPUT', MISSING_KEYPAIR: 'MISSING_KEYPAIR', INVALID_FORMAT: 'INVALID_FORMAT', WASM_ERROR: 'WASM_ERROR', STORAGE_ERROR: 'STORAGE_ERROR', UNKNOWN_ERROR: 'UNKNOWN_ERROR' }; const ERROR_MESSAGES = { [ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.', [ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.', [ERROR_CODES.SERVICE_UNAVAILABLE]: 'Service is temporarily unavailable. Please try again later.', [ERROR_CODES.INVALID_PASSWORD]: 'Invalid password. Please check your password and try again.', [ERROR_CODES.SESSION_EXPIRED]: 'Your session has expired. Please log in again.', [ERROR_CODES.UNAUTHORIZED]: 'You are not authorized to perform this action.', [ERROR_CODES.CRYPTO_ERROR]: 'Cryptographic operation failed. Please try again.', [ERROR_CODES.INVALID_SIGNATURE]: 'Invalid signature. Please verify your input.', [ERROR_CODES.ENCRYPTION_FAILED]: 'Encryption failed. Please try again.', [ERROR_CODES.INVALID_INPUT]: 'Invalid input. Please check your data and try again.', [ERROR_CODES.MISSING_KEYPAIR]: 'No keypair selected. Please select a keypair first.', [ERROR_CODES.INVALID_FORMAT]: 'Invalid data format. Please check your input.', [ERROR_CODES.WASM_ERROR]: 'System error occurred. Please refresh and try again.', [ERROR_CODES.STORAGE_ERROR]: 'Storage error occurred. Please try again.', [ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.' }; const RETRYABLE_ERRORS = new Set([ ERROR_CODES.NETWORK_ERROR, ERROR_CODES.TIMEOUT_ERROR, ERROR_CODES.SERVICE_UNAVAILABLE, ERROR_CODES.WASM_ERROR, ERROR_CODES.STORAGE_ERROR ]); function classifyError(error) { const errorMessage = getErrorMessage(error); if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) { return new CryptoVaultError( errorMessage, ERROR_CODES.NETWORK_ERROR, true, ERROR_MESSAGES[ERROR_CODES.NETWORK_ERROR] ); } if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) { return new CryptoVaultError( errorMessage, ERROR_CODES.INVALID_PASSWORD, false, ERROR_MESSAGES[ERROR_CODES.INVALID_PASSWORD] ); } if (errorMessage.includes('session') || errorMessage.includes('not unlocked') || errorMessage.includes('expired')) { return new CryptoVaultError( errorMessage, ERROR_CODES.SESSION_EXPIRED, false, ERROR_MESSAGES[ERROR_CODES.SESSION_EXPIRED] ); } if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) { return new CryptoVaultError( errorMessage, ERROR_CODES.CRYPTO_ERROR, false, 'Invalid password or corrupted data. Please check your password.' ); } if (errorMessage.includes('Crypto error') || errorMessage.includes('encryption')) { return new CryptoVaultError( errorMessage, ERROR_CODES.CRYPTO_ERROR, false, ERROR_MESSAGES[ERROR_CODES.CRYPTO_ERROR] ); } if (errorMessage.includes('No keypair selected')) { return new CryptoVaultError( errorMessage, ERROR_CODES.MISSING_KEYPAIR, false, ERROR_MESSAGES[ERROR_CODES.MISSING_KEYPAIR] ); } if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) { return new CryptoVaultError( errorMessage, ERROR_CODES.WASM_ERROR, true, ERROR_MESSAGES[ERROR_CODES.WASM_ERROR] ); } return new CryptoVaultError( errorMessage, ERROR_CODES.UNKNOWN_ERROR, false, ERROR_MESSAGES[ERROR_CODES.UNKNOWN_ERROR] ); } function getErrorMessage(error) { if (!error) return 'Unknown error'; if (typeof error === 'string') { return error.trim(); } if (error instanceof Error) { return error.message; } if (error.error) { return getErrorMessage(error.error); } if (error.message) { return error.message; } if (typeof error === 'object') { try { const stringified = JSON.stringify(error); if (stringified && stringified !== '{}') { return stringified; } } catch (e) { // Silently handle JSON stringify errors } } return 'Unknown error'; } async function withRetry(operation, options = {}) { const { maxRetries = 3, baseDelay = 1000, maxDelay = 10000, backoffFactor = 2, onRetry = null } = options; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await operation(); } catch (error) { const classifiedError = classifyError(error); lastError = classifiedError; if (attempt === maxRetries || !classifiedError.retryable) { throw classifiedError; } const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay); if (onRetry) { onRetry(attempt + 1, delay, classifiedError); } await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } async function executeOperation(operation, options = {}) { const { loadingElement = null, successMessage = null, showRetryProgress = false, onProgress = null } = options; if (loadingElement) { setButtonLoading(loadingElement, true); } try { const result = await withRetry(operation, { ...options, onRetry: (attempt, delay, error) => { if (showRetryProgress && onProgress) { onProgress(`Retrying... (${attempt}/${options.maxRetries || 3})`); } if (options.onRetry) { options.onRetry(attempt, delay, error); } } }); if (successMessage) { showToast(successMessage, 'success'); } return result; } catch (error) { showToast(error.userMessage || error.message, 'error'); throw error; } finally { if (loadingElement) { setButtonLoading(loadingElement, false); } } } window.CryptoVaultError = CryptoVaultError; window.ERROR_CODES = ERROR_CODES; window.classifyError = classifyError; window.getErrorMessage = getErrorMessage; window.withRetry = withRetry; window.executeOperation = executeOperation;