diff --git a/.eslintrc.js b/.eslintrc.js index bd006b4d..fcd19ebe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,12 @@ module.exports = { node: true, es2022: true, }, + ignorePatterns: [ + 'node_modules/', + 'dist/', + 'dist-electron/', + '*.d.ts' + ], extends: [ "plugin:vue/vue3-recommended", "eslint:recommended", diff --git a/doc/secure-storage-implementation.md b/doc/secure-storage-implementation.md index e1fabd4d..5d217c0d 100644 --- a/doc/secure-storage-implementation.md +++ b/doc/secure-storage-implementation.md @@ -2,19 +2,18 @@ ## Overview -This document outlines the implementation of secure storage for the TimeSafari app using Capacitor solutions. The implementation focuses on: +This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on: 1. **Platform-Specific Storage Solutions**: - - Web: wa-sqlite with IndexedDB backend - - iOS: SQLCipher with Keychain integration - - Android: SQLCipher with Keystore integration - - Electron: SQLite with secure storage + - Web: SQLite with IndexedDB backend (absurd-sql) + - Electron: SQLite with Node.js backend + - Native: (Planned) SQLCipher with platform-specific secure storage 2. **Key Features**: - - Encrypted storage using SQLCipher - - Platform-specific security features - - Migration support from existing implementations + - SQLite-based storage using absurd-sql for web + - Platform-specific service factory pattern - Consistent API across platforms + - Migration support from Dexie.js ## Quick Start @@ -22,1388 +21,168 @@ This document outlines the implementation of secure storage for the TimeSafari a ```bash # Core dependencies -npm install @capacitor-community/sqlite@6.0.0 -npm install @wa-sqlite/sql.js@0.8.12 -npm install @wa-sqlite/sql.js-httpvfs@0.8.12 +npm install @jlongster/sql.js +npm install absurd-sql -# Platform-specific dependencies -npm install @capacitor/preferences@6.0.2 -npm install @capacitor-community/biometric-auth@5.0.0 +# Platform-specific dependencies (for future native support) +npm install @capacitor/preferences +npm install @capacitor-community/biometric-auth ``` ### 2. Basic Usage ```typescript -// src/services/storage/StorageService.ts -import { PlatformServiceFactory } from '../PlatformServiceFactory'; -import { StorageError, StorageErrorCodes } from './errors/StorageError'; - -export class StorageService { - private static instance: StorageService; - private platformService: PlatformService; - - private constructor() { - this.platformService = PlatformServiceFactory.create(); - } - - static getInstance(): StorageService { - if (!StorageService.instance) { - StorageService.instance = new StorageService(); - } - return StorageService.instance; - } - - async initialize(): Promise { - try { - // Initialize databases - await this.platformService.openSecretDatabase(); - await this.platformService.openAccountsDatabase(); - - // Check for migration - if (await this.platformService.needsMigration()) { - await this.handleMigration(); - } - } catch (error) { - throw new StorageError( - 'Failed to initialize storage service', - StorageErrorCodes.INITIALIZATION_FAILED, - error - ); - } - } - - private async handleMigration(): Promise { - try { - // Show migration UI - const shouldMigrate = await this.showMigrationPrompt(); - if (!shouldMigrate) return; - - // Perform migration - await this.platformService.performMigration(); - - // Verify migration - await this.verifyMigration(); - } catch (error) { - // Handle migration failure - await this.handleMigrationError(error); - } - } - - // Example: Adding an account - async addAccount(account: Account): Promise { - try { - await this.platformService.addAccount(account); - } catch (error) { - if (error instanceof StorageError) { - throw error; - } - throw new StorageError( - 'Failed to add account', - StorageErrorCodes.QUERY_FAILED, - error - ); - } - } - - // Example: Retrieving an account - async getAccountByDid(did: string): Promise { - try { - return await this.platformService.getAccountByDid(did); - } catch (error) { - if (error instanceof StorageError) { - throw error; - } - throw new StorageError( - 'Failed to retrieve account', - StorageErrorCodes.QUERY_FAILED, - error - ); - } - } -} +// Using the platform service +import { PlatformServiceFactory } from '../services/PlatformServiceFactory'; + +// Get platform-specific service instance +const platformService = PlatformServiceFactory.getInstance(); + +// Example database operations +async function example() { + try { + // Query example + const result = await platformService.dbQuery( + "SELECT * FROM accounts WHERE did = ?", + [did] + ); -// Usage example: -const storageService = StorageService.getInstance(); -await storageService.initialize(); + // Execute example + await platformService.dbExec( + "INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)", + [did, publicKeyHex] + ); -try { - const account = await storageService.getAccountByDid('did:example:123'); - if (!account) { - await storageService.addAccount({ - did: 'did:example:123', - publicKeyHex: '0x123...', - // ... other account properties - }); + } catch (error) { + console.error('Database operation failed:', error); } -} catch (error) { - if (error instanceof StorageError) { - console.error(`Storage error: ${error.code}`, error.message); - } else { - console.error('Unexpected error:', error); - } -} -``` - -#### A. Modifying Code - -When converting from Dexie.js to SQL-based implementation, follow these patterns: - -1. **Database Access Pattern** - ```typescript - // Before (Dexie) - const result = await db.table.where("field").equals(value).first(); - - // After (SQL) - const platform = PlatformServiceFactory.getInstance(); - const result = await platform.dbQuery( - "SELECT * FROM table WHERE field = ?", - [value] - ); - ``` - -2. **Update Operations** - ```typescript - // Before (Dexie) - await db.table.where("id").equals(id).modify(changes); - - // After (SQL) - const { sql, params } = generateUpdateStatement( - changes, - "table", - "id = ?", - [id] - ); - await platform.dbExec(sql, params); - ``` - -3. **Insert Operations** - ```typescript - // Before (Dexie) - await db.table.add(item); - - // After (SQL) - const columns = Object.keys(item); - const values = Object.values(item); - const placeholders = values.map(() => '?').join(', '); - const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`; - await platform.dbExec(sql, values); - ``` - -4. **Delete Operations** - ```typescript - // Before (Dexie) - await db.table.where("id").equals(id).delete(); - - // After (SQL) - await platform.dbExec("DELETE FROM table WHERE id = ?", [id]); - ``` - -5. **Result Processing** - ```typescript - // Before (Dexie) - const items = await db.table.toArray(); - - // After (SQL) - const result = await platform.dbQuery("SELECT * FROM table"); - const items = mapColumnsToValues(result.columns, result.values); - ``` - -Key Considerations: -- Use the `generateUpdateStatement` helper for update operations -- Use the `mapColumnsToValues` helper for processing query results - -Example Migration: -```typescript -// Before (Dexie) -export async function updateSettings(settings: Settings): Promise { - await db.settings.put(settings); -} - -// After (SQL) -export async function updateSettings(settings: Settings): Promise { - const platform = PlatformServiceFactory.getInstance(); - const { sql, params } = generateUpdateStatement( - settings, - "settings", - "id = ?", - [settings.id] - ); - await platform.dbExec(sql, params); } ``` -Remember to: -- Create database access code to use the platform service, putting it in front of the Dexie version -- Instead of removing Dexie-specific code, keep it. - - - For creates & updates & deletes, the duplicate code is fine. - - - For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if it's true then use that result instead of the SQL code's result. - -- Test thoroughly after migration -- Consider data migration needs, and warn if there are any potential migration problems - ### 3. Platform Detection ```typescript -// src/services/storage/PlatformDetection.ts -import { Capacitor } from '@capacitor/core'; -import { StorageError, StorageErrorCodes } from './errors/StorageError'; - -export class PlatformDetection { - static isNativePlatform(): boolean { - return Capacitor.isNativePlatform(); - } - - static getPlatform(): 'ios' | 'android' | 'web' | 'electron' { - if (Capacitor.isNativePlatform()) { - return Capacitor.getPlatform() as 'ios' | 'android'; +// src/services/PlatformServiceFactory.ts +export class PlatformServiceFactory { + static getInstance(): PlatformService { + if (process.env.ELECTRON) { + // Electron platform + return new ElectronPlatformService(); + } else { + // Web platform (default) + return new AbsurdSqlDatabaseService(); } - return window.electron ? 'electron' : 'web'; - } - - static async getCapabilities(): Promise { - try { - const platform = this.getPlatform(); - - return { - hasFileSystem: platform !== 'web', - hasSecureStorage: platform !== 'web', - hasBiometrics: await this.checkBiometrics(), - isIOS: platform === 'ios', - isAndroid: platform === 'android' - }; - } catch (error) { - throw new StorageError( - 'Failed to detect platform capabilities', - StorageErrorCodes.INITIALIZATION_FAILED, - error - ); - } - } - - private static async checkBiometrics(): Promise { - if (!this.isNativePlatform()) return false; - - try { - const { BiometricAuth } = await import('@capacitor-community/biometric-auth'); - const available = await BiometricAuth.isAvailable(); - return available.has; - } catch (error) { - console.warn('Biometric check failed:', error); - return false; - } - } -} - -// Usage example: -try { - const capabilities = await PlatformDetection.getCapabilities(); - if (capabilities.hasSecureStorage) { - // Use platform-specific secure storage - await initializeSecureStorage(); - } else { - // Fall back to web storage - await initializeWebStorage(); - } -} catch (error) { - if (error instanceof StorageError) { - console.error(`Platform detection error: ${error.code}`, error.message); - } else { - console.error('Unexpected error during platform detection:', error); } } ``` -### 4. Platform-Specific Implementations - -#### Web Platform (wa-sqlite) - -```typescript -// src/services/platforms/web/WebSQLiteService.ts -export class WebSQLiteService implements PlatformService { - private db: SQLite.Database | null = null; - private vfs: IDBBatchAtomicVFS | null = null; - private initialized = false; - - async initialize(): Promise { - if (this.initialized) return; - - try { - // 1. Initialize SQLite - const sqlite3 = await this.initializeSQLite(); - - // 2. Set up VFS - await this.setupVFS(sqlite3); - - // 3. Open database - await this.openDatabase(); - - // 4. Set up schema - await this.setupSchema(); - - // 5. Optimize performance - await this.optimizePerformance(); - - this.initialized = true; - } catch (error) { - throw new StorageError( - 'Failed to initialize web SQLite', - StorageErrorCodes.INITIALIZATION_FAILED, - error - ); - } - } - - private async initializeSQLite(): Promise { - try { - return await SQLite.init({ - locateFile: file => `https://cdn.jsdelivr.net/npm/@wa-sqlite/sql.js@0.8.12/dist/${file}` - }); - } catch (error) { - throw new StorageError( - 'Failed to load SQLite WebAssembly', - StorageErrorCodes.WASM_LOAD_FAILED, - error - ); - } - } - - private async setupVFS(sqlite3: SQLite.SqlJsStatic): Promise { - try { - this.vfs = new IDBBatchAtomicVFS('timesafari'); - await this.vfs.registerVFS(sqlite3); - } catch (error) { - throw new StorageError( - 'Failed to set up IndexedDB VFS', - StorageErrorCodes.VFS_SETUP_FAILED, - error - ); - } - } - - async openDatabase(): Promise { - if (!this.vfs) { - throw new StorageError( - 'VFS not initialized', - StorageErrorCodes.INITIALIZATION_FAILED - ); - } - - try { - this.db = await this.vfs.openDatabase('timesafari.db'); - await this.setupPragmas(); - } catch (error) { - throw new StorageError( - 'Failed to open database', - StorageErrorCodes.INITIALIZATION_FAILED, - error - ); - } - } - - private async setupPragmas(): Promise { - if (!this.db) return; - - try { - await this.db.exec(` - PRAGMA journal_mode = WAL; - PRAGMA synchronous = NORMAL; - PRAGMA foreign_keys = ON; - PRAGMA busy_timeout = 5000; - `); - } catch (error) { - throw new StorageError( - 'Failed to set up database pragmas', - StorageErrorCodes.INITIALIZATION_FAILED, - error - ); - } - } - - async close(): Promise { - if (this.db) { - await this.db.close(); - this.db = null; - } - this.initialized = false; - } -} - -// Migration strategy for web platform -export class WebMigrationService { - async migrate(): Promise { - // 1. Check prerequisites - await this.checkPrerequisites(); - - // 2. Create backup - const backup = await this.createBackup(); - - // 3. Perform migration - try { - await this.performMigration(backup); - } catch (error) { - // 4. Handle failure - await this.handleMigrationFailure(error, backup); - } - - // 5. Verify migration - await this.verifyMigration(backup); - } - - private async checkPrerequisites(): Promise { - // Check IndexedDB availability - if (!window.indexedDB) { - throw new StorageError( - 'IndexedDB not available', - StorageErrorCodes.INITIALIZATION_FAILED - ); - } - - // Check storage quota - const quota = await navigator.storage.estimate(); - if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) { - throw new StorageError( - 'Insufficient storage space', - StorageErrorCodes.INITIALIZATION_FAILED - ); - } - } +### 4. Current Implementation Details - private async createBackup(): Promise { - const backup = { - timestamp: Date.now(), - accounts: await this.dexieDB.accounts.toArray(), - settings: await this.dexieDB.settings.toArray(), - contacts: await this.dexieDB.contacts.toArray() - }; +#### Web Platform (AbsurdSqlDatabaseService) - // Store backup in IndexedDB - await this.storeBackup(backup); - - return backup; - } -} -``` - -#### Native Platform (iOS/Android) +The web platform uses absurd-sql with IndexedDB backend: ```typescript -// src/services/platforms/native/NativeSQLiteService.ts -export class NativeSQLiteService implements PlatformService { - private db: SQLiteConnection | null = null; - private initialized = false; - - async initialize(): Promise { - if (this.initialized) return; +// src/services/AbsurdSqlDatabaseService.ts +export class AbsurdSqlDatabaseService implements PlatformService { + private static instance: AbsurdSqlDatabaseService | null = null; + private db: AbsurdSqlDatabase | null = null; + private initialized: boolean = false; - try { - // 1. Check platform capabilities - await this.checkPlatformCapabilities(); - - // 2. Initialize SQLite with encryption - await this.initializeEncryptedDatabase(); - - // 3. Set up schema - await this.setupSchema(); - - this.initialized = true; - } catch (error) { - throw new StorageError( - 'Failed to initialize native SQLite', - StorageErrorCodes.INITIALIZATION_FAILED, - error - ); + // Singleton pattern + static getInstance(): AbsurdSqlDatabaseService { + if (!AbsurdSqlDatabaseService.instance) { + AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService(); } + return AbsurdSqlDatabaseService.instance; } - private async checkPlatformCapabilities(): Promise { - const { Capacitor } = await import('@capacitor/core'); - if (!Capacitor.isNativePlatform()) { - throw new StorageError( - 'Not running on native platform', - StorageErrorCodes.INITIALIZATION_FAILED - ); - } - } - - private async initializeEncryptedDatabase(): Promise { - const { SQLite } = await import('@capacitor-community/sqlite'); - this.db = await SQLite.createConnection( - 'timesafari', - false, - 'encryption', - 1, - false - ); - await this.db.open(); - } - - async setupSchema(): Promise { - if (!this.db) { - throw new StorageError( - 'Database not initialized', - StorageErrorCodes.INITIALIZATION_FAILED - ); - } - - try { - await this.db.execute(` - CREATE TABLE IF NOT EXISTS accounts ( - did TEXT PRIMARY KEY, - public_key_hex TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS contacts ( - did TEXT PRIMARY KEY, - name TEXT NOT NULL, - public_key_hex TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ); - `); - } catch (error) { - throw new StorageError( - 'Failed to set up database schema', - StorageErrorCodes.INITIALIZATION_FAILED, - error - ); - } + // Database operations + async dbQuery(sql: string, params: unknown[] = []): Promise { + await this.waitForInitialization(); + return this.queueOperation("query", sql, params); } - async close(): Promise { - if (this.db) { - await this.db.close(); - this.db = null; - } - this.initialized = false; + async dbExec(sql: string, params: unknown[] = []): Promise { + await this.waitForInitialization(); + await this.queueOperation("run", sql, params); } } ``` -### 5. Error Handling - -```typescript -// src/services/storage/errors/StorageError.ts -export enum StorageErrorCodes { - INITIALIZATION_FAILED = 'STORAGE_INIT_FAILED', - QUERY_FAILED = 'STORAGE_QUERY_FAILED', - MIGRATION_FAILED = 'STORAGE_MIGRATION_FAILED', - ENCRYPTION_FAILED = 'STORAGE_ENCRYPTION_FAILED', - DECRYPTION_FAILED = 'STORAGE_DECRYPTION_FAILED', - INVALID_DATA = 'STORAGE_INVALID_DATA', - DATABASE_CORRUPTED = 'STORAGE_DB_CORRUPTED', - INSUFFICIENT_PERMISSIONS = 'STORAGE_INSUFFICIENT_PERMISSIONS', - STORAGE_FULL = 'STORAGE_FULL', - CONCURRENT_ACCESS = 'STORAGE_CONCURRENT_ACCESS' -} - -export class StorageError extends Error { - constructor( - message: string, - public code: StorageErrorCodes, - public originalError?: unknown - ) { - super(message); - this.name = 'StorageError'; - } - - static isStorageError(error: unknown): error is StorageError { - return error instanceof StorageError; - } - - static fromUnknown(error: unknown, context: string): StorageError { - if (this.isStorageError(error)) { - return error; - } - return new StorageError( - `${context}: ${error instanceof Error ? error.message : String(error)}`, - StorageErrorCodes.QUERY_FAILED, - error - ); - } -} - -// Error recovery strategies -export class StorageErrorRecovery { - static async handleError(error: StorageError): Promise { - switch (error.code) { - case StorageErrorCodes.DATABASE_CORRUPTED: - await this.handleCorruptedDatabase(); - break; - case StorageErrorCodes.STORAGE_FULL: - await this.handleStorageFull(); - break; - case StorageErrorCodes.CONCURRENT_ACCESS: - await this.handleConcurrentAccess(); - break; - default: - throw error; // Re-throw unhandled errors - } - } - - private static async handleCorruptedDatabase(): Promise { - // 1. Attempt to repair - try { - await this.repairDatabase(); - } catch { - // 2. If repair fails, restore from backup - await this.restoreFromBackup(); - } - } - - private static async handleStorageFull(): Promise { - // 1. Clean up temporary files - await this.cleanupTempFiles(); - - // 2. If still full, notify user - const isStillFull = await this.checkStorageFull(); - if (isStillFull) { - throw new StorageError( - 'Storage is full. Please free up space.', - StorageErrorCodes.STORAGE_FULL - ); - } - } +Key features: +- Uses absurd-sql for SQLite in the browser +- Implements operation queuing for thread safety +- Handles initialization and connection management +- Provides consistent API across platforms - private static async handleConcurrentAccess(): Promise { - // Implement retry logic with exponential backoff - await this.retryWithBackoff(async () => { - // Attempt operation again - }); - } -} -``` +### 5. Migration from Dexie.js -### 6. Testing Strategy +The current implementation supports gradual migration from Dexie.js: ```typescript -// src/services/storage/__tests__/StorageService.test.ts -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { StorageService } from '../StorageService'; -import { StorageError, StorageErrorCodes } from '../errors/StorageError'; -import { PlatformDetection } from '../PlatformDetection'; - -describe('StorageService', () => { - let storageService: StorageService; - - beforeEach(async () => { - storageService = StorageService.getInstance(); - await storageService.initialize(); - }); - - afterEach(async () => { - // Clean up test data - await cleanupTestData(); - }); - - describe('Account Operations', () => { - it('should add and retrieve an account', async () => { - const account = { - did: 'did:test:123', - publicKeyHex: '0x123...', - // ... other properties - }; - - await storageService.addAccount(account); - const retrieved = await storageService.getAccountByDid(account.did); - - expect(retrieved).toBeDefined(); - expect(retrieved?.did).toBe(account.did); - }); - - it('should handle duplicate accounts', async () => { - const account = { - did: 'did:test:123', - publicKeyHex: '0x123...', - }; - - await storageService.addAccount(account); - - await expect( - storageService.addAccount(account) - ).rejects.toThrow(StorageError); - }); - }); - - describe('Error Handling', () => { - it('should handle database corruption', async () => { - // Simulate database corruption - await simulateDatabaseCorruption(); - - await expect( - storageService.getAccountByDid('did:test:123') - ).rejects.toThrow(StorageError); - - // Verify recovery - const recovered = await storageService.getAccountByDid('did:test:123'); - expect(recovered).toBeDefined(); - }); - - it('should handle concurrent access', async () => { - const promises = Array(5).fill(null).map(() => - storageService.addAccount({ - did: `did:test:${Math.random()}`, - publicKeyHex: '0x123...', - }) - ); - - const results = await Promise.allSettled(promises); - const errors = results.filter(r => r.status === 'rejected'); - - expect(errors.length).toBeLessThan(promises.length); - }); - }); - - describe('Platform-Specific Tests', () => { - it('should use correct storage implementation', async () => { - const capabilities = await PlatformDetection.getCapabilities(); - - if (capabilities.hasSecureStorage) { - // Verify native storage implementation - expect(storageService.getImplementation()).toBe('native'); - } else { - // Verify web storage implementation - expect(storageService.getImplementation()).toBe('web'); - } - }); - - it('should handle platform transitions', async () => { - // Simulate platform change (e.g., web to native) - await simulatePlatformChange(); - - // Verify data persistence - const account = await storageService.getAccountByDid('did:test:123'); - expect(account).toBeDefined(); - }); - }); -}); - -// Helper functions for testing -async function cleanupTestData(): Promise { - // Implementation -} +// Example of dual-storage pattern +async function getAccount(did: string): Promise { + // Try SQLite first + const platform = PlatformServiceFactory.getInstance(); + let account = await platform.dbQuery( + "SELECT * FROM accounts WHERE did = ?", + [did] + ); -async function simulateDatabaseCorruption(): Promise { - // Implementation -} + // Fallback to Dexie if needed + if (USE_DEXIE_DB && !account) { + account = await db.accounts.get(did); + } -async function simulatePlatformChange(): Promise { - // Implementation + return account; } ``` -#### Additional Platform-Specific Tests - -```typescript -// src/services/storage/__tests__/WebSQLiteService.spec.ts -import { WebSQLiteService } from '../platforms/web/WebSQLiteService'; -import { StorageError, StorageErrorCodes } from '../errors/StorageError'; - -describe('WebSQLiteService', () => { - let service: WebSQLiteService; - - beforeEach(async () => { - service = new WebSQLiteService(); - await service.initialize(); - }); - - afterEach(async () => { - await service.close(); - }); - - it('should initialize successfully', async () => { - expect(service.isInitialized()).toBe(true); - }); - - it('should handle IndexedDB errors', async () => { - // Mock IndexedDB failure - const mockIndexedDB = jest.spyOn(window, 'indexedDB', 'get'); - mockIndexedDB.mockImplementation(() => undefined); - - await expect(service.initialize()).rejects.toThrow( - new StorageError( - 'IndexedDB not available', - StorageErrorCodes.INITIALIZATION_FAILED - ) - ); - }); - - it('should migrate data correctly', async () => { - // Set up test data - const testAccount = createTestAccount(); - await dexieDB.accounts.add(testAccount); - - // Perform migration - await service.migrate(); - - // Verify migration - const migratedAccount = await service.getAccountByDid(testAccount.did); - expect(migratedAccount).toEqual(testAccount); - }); -}); - -// Integration tests -describe('StorageService Integration', () => { - it('should handle concurrent access', async () => { - const service1 = StorageService.getInstance(); - const service2 = StorageService.getInstance(); - - // Simulate concurrent access - const [result1, result2] = await Promise.all([ - service1.addAccount(testAccount1), - service2.addAccount(testAccount2) - ]); - - // Verify both operations succeeded - expect(result1).toBeDefined(); - expect(result2).toBeDefined(); - }); - - it('should recover from errors', async () => { - const service = StorageService.getInstance(); - - // Simulate database corruption - await simulateDatabaseCorruption(); - - // Attempt recovery - await service.recover(); - - // Verify data integrity - const accounts = await service.getAllAccounts(); - expect(accounts).toBeDefined(); - }); -}); -``` - -### 7. Troubleshooting Guide - -#### Detailed Recovery Procedures - -1. **Database Corruption Recovery** - ```typescript - async recoverFromCorruption(): Promise { - // 1. Stop all database operations - await this.stopDatabaseOperations(); - - // 2. Create backup of corrupted database - const backup = await this.createEmergencyBackup(); - - // 3. Attempt repair - try { - await this.repairDatabase(); - } catch (error) { - // 4. Restore from backup if repair fails - await this.restoreFromBackup(backup); - } - } - ``` - -2. **Migration Recovery** - ```typescript - async recoverFromFailedMigration(): Promise { - // 1. Identify migration stage - const stage = await this.getMigrationStage(); - - // 2. Execute appropriate recovery - switch (stage) { - case 'backup': - await this.recoverFromBackupStage(); - break; - case 'migration': - await this.recoverFromMigrationStage(); - break; - case 'verification': - await this.recoverFromVerificationStage(); - break; - } - } - ``` - -3. **Performance Troubleshooting** - - Monitor database size and growth - - Check query performance with EXPLAIN - - Review indexing strategy - - Monitor memory usage - - Check for connection leaks - ## Success Criteria 1. **Functionality** - - [ ] All CRUD operations work correctly - - [ ] Migration process completes successfully - - [ ] Error handling works as expected - - [ ] Platform-specific features function correctly + - [x] Basic CRUD operations work correctly + - [x] Platform service factory pattern implemented + - [x] Error handling in place + - [ ] Native platform support (planned) 2. **Performance** - - [ ] Database operations complete within acceptable time - - [ ] Memory usage remains stable - - [ ] IndexedDB quota usage is monitored - - [ ] Concurrent operations work correctly + - [x] Database operations complete within acceptable time + - [x] Operation queuing for thread safety + - [x] Proper initialization handling + - [ ] Performance monitoring (planned) 3. **Security** - - [ ] Data is properly encrypted - - [ ] Keys are securely stored - - [ ] Platform-specific security features work - - [ ] No sensitive data leaks + - [x] Basic data integrity + - [ ] Encryption (planned for native platforms) + - [ ] Secure key storage (planned) + - [ ] Platform-specific security features (planned) 4. **Testing** - - [ ] All unit tests pass - - [ ] Integration tests complete successfully - - [ ] Edge cases are handled - - [ ] Error recovery works as expected - -## Appendix - -### A. Database Schema - -```sql --- Accounts Table -CREATE TABLE accounts ( - did TEXT PRIMARY KEY, - public_key_hex TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL -); - --- Settings Table -CREATE TABLE settings ( - key TEXT PRIMARY KEY, - value TEXT NOT NULL, - updated_at INTEGER NOT NULL -); - --- Contacts Table -CREATE TABLE contacts ( - did TEXT PRIMARY KEY, - name TEXT NOT NULL, - public_key_hex TEXT NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL -); - --- Indexes -CREATE INDEX idx_accounts_created_at ON accounts(created_at); -CREATE INDEX idx_contacts_created_at ON contacts(created_at); -CREATE INDEX idx_settings_updated_at ON settings(updated_at); -``` - -### B. Error Codes Reference - -| Code | Description | Recovery Action | -|------|-------------|-----------------| -| `STORAGE_INIT_FAILED` | Database initialization failed | Check permissions, storage space | -| `STORAGE_QUERY_FAILED` | Database query failed | Verify query, check connection | -| `STORAGE_MIGRATION_FAILED` | Data migration failed | Use backup, manual migration | -| `STORAGE_ENCRYPTION_FAILED` | Data encryption failed | Check key management | -| `STORAGE_DECRYPTION_FAILED` | Data decryption failed | Verify encryption key | -| `STORAGE_INVALID_DATA` | Invalid data format | Validate input data | -| `STORAGE_DB_CORRUPTED` | Database corruption detected | Use backup, repair | -| `STORAGE_INSUFFICIENT_PERMISSIONS` | Missing required permissions | Request permissions | -| `STORAGE_FULL` | Storage quota exceeded | Clean up, increase quota | -| `STORAGE_CONCURRENT_ACCESS` | Concurrent access conflict | Implement retry logic | - -### C. Platform Capabilities Matrix - -| Feature | Web | iOS | Android | Electron | -|---------|-----|-----|---------|----------| -| SQLite | wa-sqlite | SQLCipher | SQLCipher | SQLite | -| Encryption | SQLCipher | SQLCipher | SQLCipher | SQLCipher | -| Secure Storage | IndexedDB | Keychain | Keystore | Secure Storage | -| Biometrics | No | Yes | Yes | No | -| File System | Limited | Full | Full | Full | -| Background Sync | No | Yes | Yes | Yes | -| Storage Quota | Yes | No | No | No | -| Multi-tab Support | Yes | N/A | N/A | Yes | - -### D. Usage Examples - -#### Before (Using Dexie.js) - -```typescript -// src/services/storage/legacy/AccountService.ts -import Dexie from 'dexie'; - -class AccountDatabase extends Dexie { - accounts: Dexie.Table; - settings: Dexie.Table; - contacts: Dexie.Table; - - constructor() { - super('TimeSafariDB'); - this.version(1).stores({ - accounts: 'did, publicKeyHex, createdAt, updatedAt', - settings: 'key, value, updatedAt', - contacts: 'did, name, publicKeyHex, createdAt, updatedAt' - }); - } -} - -export class AccountService { - private db: AccountDatabase; - - constructor() { - this.db = new AccountDatabase(); - } - - // Account Management - async addAccount(account: Account): Promise { - try { - await this.db.accounts.add(account); - } catch (error) { - if (error instanceof Dexie.ConstraintError) { - throw new Error('Account already exists'); - } - throw error; - } - } - - async getAccount(did: string): Promise { - return await this.db.accounts.get(did); - } - - // Settings Management - async updateSetting(key: string, value: string): Promise { - await this.db.settings.put({ - key, - value, - updatedAt: Date.now() - }); - } - - async getSetting(key: string): Promise { - const setting = await this.db.settings.get(key); - return setting?.value; - } - - // Contact Management - async addContact(contact: Contact): Promise { - await this.db.contacts.add(contact); - } - - async getContacts(): Promise { - return await this.db.contacts.toArray(); - } -} - -// Usage Example -const accountService = new AccountService(); - -// Add an account -await accountService.addAccount({ - did: 'did:example:123', - publicKeyHex: '0x123...', - createdAt: Date.now(), - updatedAt: Date.now() -}); - -// Update settings -await accountService.updateSetting('theme', 'dark'); - -// Add a contact -await accountService.addContact({ - did: 'did:example:456', - name: 'Alice', - publicKeyHex: '0x456...', - createdAt: Date.now(), - updatedAt: Date.now() -}); -``` - -#### After (Using Platform Service) - -```typescript -// src/services/storage/AccountService.ts -import { StorageService } from './StorageService'; -import { StorageError, StorageErrorCodes } from './errors/StorageError'; -import { PlatformDetection } from './PlatformDetection'; - -export class AccountService { - private static instance: AccountService; - private storageService: StorageService; - - private constructor() { - this.storageService = StorageService.getInstance(); - } - - static getInstance(): AccountService { - if (!AccountService.instance) { - AccountService.instance = new AccountService(); - } - return AccountService.instance; - } - - async initialize(): Promise { - try { - // Initialize storage with platform-specific implementation - await this.storageService.initialize(); - - // Check for migration if needed - if (await this.storageService.needsMigration()) { - await this.handleMigration(); - } - } catch (error) { - throw StorageError.fromUnknown(error, 'Failed to initialize account service'); - } - } - - // Account Management with Platform-Specific Features - async addAccount(account: Account): Promise { - try { - // Check platform capabilities - const capabilities = await PlatformDetection.getCapabilities(); - - // Add platform-specific metadata - const enhancedAccount = { - ...account, - platform: capabilities.isIOS ? 'ios' : - capabilities.isAndroid ? 'android' : - capabilities.hasSecureStorage ? 'electron' : 'web', - secureStorage: capabilities.hasSecureStorage, - biometricsEnabled: capabilities.hasBiometrics - }; - - await this.storageService.addAccount(enhancedAccount); - - // If platform supports biometrics, offer to enable it - if (capabilities.hasBiometrics) { - await this.offerBiometricSetup(account.did); - } - } catch (error) { - if (error instanceof StorageError) { - throw error; - } - throw new StorageError( - 'Failed to add account', - StorageErrorCodes.QUERY_FAILED, - error - ); - } - } - - async getAccount(did: string): Promise { - try { - const account = await this.storageService.getAccountByDid(did); - - // Verify account integrity - if (account) { - await this.verifyAccountIntegrity(account); - } - - return account; - } catch (error) { - throw StorageError.fromUnknown(error, `Failed to get account ${did}`); - } - } - - // Settings Management with Encryption - async updateSetting(key: string, value: string): Promise { - try { - const capabilities = await PlatformDetection.getCapabilities(); - - // Encrypt sensitive settings if platform supports it - const processedValue = capabilities.hasSecureStorage ? - await this.encryptSetting(value) : value; - - await this.storageService.updateSettings({ - key, - value: processedValue, - updatedAt: Date.now() - }); - } catch (error) { - throw StorageError.fromUnknown(error, `Failed to update setting ${key}`); - } - } - - async getSetting(key: string): Promise { - try { - const setting = await this.storageService.getAccountSettings(key); - - if (setting?.value) { - const capabilities = await PlatformDetection.getCapabilities(); - - // Decrypt if the setting was encrypted - return capabilities.hasSecureStorage ? - await this.decryptSetting(setting.value) : - setting.value; - } - - return undefined; - } catch (error) { - throw StorageError.fromUnknown(error, `Failed to get setting ${key}`); - } - } - - // Contact Management with Platform Integration - async addContact(contact: Contact): Promise { - try { - const capabilities = await PlatformDetection.getCapabilities(); - - // Add platform-specific features - const enhancedContact = { - ...contact, - platform: capabilities.isIOS ? 'ios' : - capabilities.isAndroid ? 'android' : - capabilities.hasSecureStorage ? 'electron' : 'web', - syncEnabled: capabilities.hasBackgroundSync - }; - - await this.storageService.addContact(enhancedContact); - - // If platform supports background sync, schedule contact sync - if (capabilities.hasBackgroundSync) { - await this.scheduleContactSync(contact.did); - } - } catch (error) { - throw StorageError.fromUnknown(error, 'Failed to add contact'); - } - } - - async getContacts(): Promise { - try { - const contacts = await this.storageService.getAllContacts(); - - // Verify contact data integrity - await Promise.all(contacts.map(contact => - this.verifyContactIntegrity(contact) - )); - - return contacts; - } catch (error) { - throw StorageError.fromUnknown(error, 'Failed to get contacts'); - } - } - - // Platform-Specific Helper Methods - private async offerBiometricSetup(did: string): Promise { - const { BiometricAuth } = await import('@capacitor-community/biometric-auth'); - const available = await BiometricAuth.isAvailable(); - - if (available.has) { - // Show biometric setup prompt - // Implementation depends on UI framework - } - } - - private async verifyAccountIntegrity(account: Account): Promise { - // Verify account data integrity - // Implementation depends on security requirements - } - - private async verifyContactIntegrity(contact: Contact): Promise { - // Verify contact data integrity - // Implementation depends on security requirements - } - - private async encryptSetting(value: string): Promise { - // Encrypt sensitive settings - // Implementation depends on encryption requirements - return value; // Placeholder - } - - private async decryptSetting(value: string): Promise { - // Decrypt sensitive settings - // Implementation depends on encryption requirements - return value; // Placeholder - } - - private async scheduleContactSync(did: string): Promise { - // Schedule background sync for contacts - // Implementation depends on platform capabilities - } -} - -// Usage Example -const accountService = AccountService.getInstance(); - -// Initialize with platform detection -await accountService.initialize(); - -try { - // Add an account with platform-specific features - await accountService.addAccount({ - did: 'did:example:123', - publicKeyHex: '0x123...', - createdAt: Date.now(), - updatedAt: Date.now() - }); - - // Update settings with encryption if available - await accountService.updateSetting('theme', 'dark'); - await accountService.updateSetting('apiKey', 'sensitive-data'); - - // Add a contact with platform integration - await accountService.addContact({ - did: 'did:example:456', - name: 'Alice', - publicKeyHex: '0x456...', - createdAt: Date.now(), - updatedAt: Date.now() - }); - - // Retrieve data with integrity verification - const account = await accountService.getAccount('did:example:123'); - const contacts = await accountService.getContacts(); - const theme = await accountService.getSetting('theme'); - - console.log('Account:', account); - console.log('Contacts:', contacts); - console.log('Theme:', theme); -} catch (error) { - if (error instanceof StorageError) { - console.error(`Storage error: ${error.code}`, error.message); - } else { - console.error('Unexpected error:', error); - } -} -``` - -Key improvements in the new implementation: - -1. **Platform Awareness**: - - Automatically detects platform capabilities - - Uses platform-specific features (biometrics, secure storage) - - Handles platform transitions gracefully - -2. **Enhanced Security**: - - Encrypts sensitive data when platform supports it - - Verifies data integrity - - Uses platform-specific secure storage - -3. **Better Error Handling**: - - Consistent error types and codes - - Platform-specific error recovery - - Detailed error messages - -4. **Migration Support**: - - Automatic migration detection - - Data integrity verification - - Backup and recovery - -5. **Platform Integration**: - - Background sync for contacts - - Biometric authentication - - Secure storage for sensitive data - -6. **Type Safety**: - - Strong typing throughout - - Platform capability type checking - - Error type narrowing - -7. **Singleton Pattern**: - - Single instance management - - Consistent state across the app - - Resource sharing - -8. **Extensibility**: - - Easy to add new platform features - - Modular design - - Clear separation of concerns \ No newline at end of file + - [x] Basic unit tests + - [ ] Comprehensive integration tests (planned) + - [ ] Platform-specific tests (planned) + - [ ] Migration tests (planned) + +## Next Steps + +1. **Native Platform Support** + - Implement SQLCipher for iOS/Android + - Add platform-specific secure storage + - Implement biometric authentication + +2. **Enhanced Security** + - Add encryption for sensitive data + - Implement secure key storage + - Add platform-specific security features + +3. **Testing and Monitoring** + - Add comprehensive test coverage + - Implement performance monitoring + - Add error tracking and analytics + +4. **Documentation** + - Add API documentation + - Create migration guides + - Document security measures \ No newline at end of file diff --git a/doc/storage-implementation-checklist.md b/doc/storage-implementation-checklist.md index cfcda815..dec776ac 100644 --- a/doc/storage-implementation-checklist.md +++ b/doc/storage-implementation-checklist.md @@ -3,45 +3,46 @@ ## Core Services ### 1. Storage Service Layer -- [ ] Create base `StorageService` interface - - [ ] Define common methods for all platforms - - [ ] Add platform-specific method signatures - - [ ] Include error handling types - - [ ] Add migration support methods - -- [ ] Implement platform-specific services - - [ ] `WebSQLiteService` (absurd-sql) - - [ ] Database initialization - - [ ] VFS setup with IndexedDB backend - - [ ] Connection management - - [ ] Query builder - - [ ] `NativeSQLiteService` (iOS/Android) +- [x] Create base `PlatformService` interface + - [x] Define common methods for all platforms + - [x] Add platform-specific method signatures + - [x] Include error handling types + - [x] Add migration support methods + +- [x] Implement platform-specific services + - [x] `AbsurdSqlDatabaseService` (web) + - [x] Database initialization + - [x] VFS setup with IndexedDB backend + - [x] Connection management + - [x] Operation queuing + - [ ] `NativeSQLiteService` (iOS/Android) (planned) - [ ] SQLCipher integration - [ ] Native bridge setup - [ ] File system access - - [ ] `ElectronSQLiteService` + - [ ] `ElectronSQLiteService` (planned) - [ ] Node SQLite integration - [ ] IPC communication - [ ] File system access ### 2. Migration Services -- [ ] Implement `MigrationService` - - [ ] Backup creation - - [ ] Data verification - - [ ] Rollback procedures - - [ ] Progress tracking -- [ ] Create `MigrationUI` components +- [x] Implement basic migration support + - [x] Dual-storage pattern (SQLite + Dexie) + - [x] Basic data verification + - [ ] Rollback procedures (planned) + - [ ] Progress tracking (planned) +- [ ] Create `MigrationUI` components (planned) - [ ] Progress indicators - [ ] Error handling - [ ] User notifications - [ ] Manual triggers ### 3. Security Layer -- [ ] Implement `EncryptionService` +- [x] Basic data integrity +- [ ] Implement `EncryptionService` (planned) - [ ] Key management - [ ] Encryption/decryption - [ ] Secure storage -- [ ] Add `BiometricService` +- [ ] Add `BiometricService` (planned) - [ ] Platform detection - [ ] Authentication flow - [ ] Fallback mechanisms @@ -49,18 +50,19 @@ ## Platform-Specific Implementation ### Web Platform -- [ ] Setup absurd-sql - - [ ] Install dependencies +- [x] Setup absurd-sql + - [x] Install dependencies ```json { "@jlongster/sql.js": "^1.8.0", "absurd-sql": "^1.8.0" } ``` - - [ ] Configure VFS with IndexedDB backend - - [ ] Setup worker threads - - [ ] Implement connection pooling - - [ ] Configure database pragmas + - [x] Configure VFS with IndexedDB backend + - [x] Setup worker threads + - [x] Implement operation queuing + - [x] Configure database pragmas + ```sql PRAGMA journal_mode=MEMORY; PRAGMA synchronous=NORMAL; @@ -68,19 +70,19 @@ PRAGMA busy_timeout=5000; ``` -- [ ] Update build configuration - - [ ] Modify `vite.config.ts` - - [ ] Add worker configuration - - [ ] Update chunk splitting - - [ ] Configure asset handling +- [x] Update build configuration + - [x] Modify `vite.config.ts` + - [x] Add worker configuration + - [x] Update chunk splitting + - [x] Configure asset handling -- [ ] Implement IndexedDB fallback - - [ ] Create fallback service - - [ ] Add data synchronization - - [ ] Handle quota exceeded - - [ ] Implement atomic operations +- [x] Implement IndexedDB backend + - [x] Create database service + - [x] Add operation queuing + - [x] Handle initialization + - [x] Implement atomic operations -### iOS Platform +### iOS Platform (Planned) - [ ] Setup SQLCipher - [ ] Install pod dependencies - [ ] Configure encryption @@ -93,7 +95,7 @@ - [ ] Configure backup - [ ] Setup app groups -### Android Platform +### Android Platform (Planned) - [ ] Setup SQLCipher - [ ] Add Gradle dependencies - [ ] Configure encryption @@ -106,7 +108,7 @@ - [ ] Configure backup - [ ] Setup file provider -### Electron Platform +### Electron Platform (Planned) - [ ] Setup Node SQLite - [ ] Install dependencies - [ ] Configure IPC @@ -122,7 +124,8 @@ ## Data Models and Types ### 1. Database Schema -- [ ] Define tables +- [x] Define tables + ```sql -- Accounts table CREATE TABLE accounts ( @@ -155,13 +158,14 @@ CREATE INDEX idx_settings_updated_at ON settings(updated_at); ``` -- [ ] Create indexes -- [ ] Define constraints -- [ ] Add triggers -- [ ] Setup migrations +- [x] Create indexes +- [x] Define constraints +- [ ] Add triggers (planned) +- [ ] Setup migrations (planned) ### 2. Type Definitions -- [ ] Create interfaces + +- [x] Create interfaces ```typescript interface Account { did: string; @@ -185,28 +189,28 @@ } ``` -- [ ] Add validation -- [ ] Create DTOs -- [ ] Define enums -- [ ] Add type guards +- [x] Add validation +- [x] Create DTOs +- [x] Define enums +- [x] Add type guards ## UI Components -### 1. Migration UI +### 1. Migration UI (Planned) - [ ] Create components - [ ] `MigrationProgress.vue` - [ ] `MigrationError.vue` - [ ] `MigrationSettings.vue` - [ ] `MigrationStatus.vue` -### 2. Settings UI +### 2. Settings UI (Planned) - [ ] Update components - [ ] Add storage settings - [ ] Add migration controls - [ ] Add backup options - [ ] Add security settings -### 3. Error Handling UI +### 3. Error Handling UI (Planned) - [ ] Create components - [ ] `StorageError.vue` - [ ] `QuotaExceeded.vue` @@ -216,20 +220,20 @@ ## Testing ### 1. Unit Tests -- [ ] Test services - - [ ] Storage service tests - - [ ] Migration service tests - - [ ] Security service tests - - [ ] Platform detection tests +- [x] Basic service tests + - [x] Platform service tests + - [x] Database operation tests + - [ ] Security service tests (planned) + - [ ] Platform detection tests (planned) -### 2. Integration Tests +### 2. Integration Tests (Planned) - [ ] Test migrations - [ ] Web platform tests - [ ] iOS platform tests - [ ] Android platform tests - [ ] Electron platform tests -### 3. E2E Tests +### 3. E2E Tests (Planned) - [ ] Test workflows - [ ] Account management - [ ] Settings management @@ -239,12 +243,12 @@ ## Documentation ### 1. Technical Documentation -- [ ] Update architecture docs -- [ ] Add API documentation -- [ ] Create migration guides -- [ ] Document security measures +- [x] Update architecture docs +- [x] Add API documentation +- [ ] Create migration guides (planned) +- [ ] Document security measures (planned) -### 2. User Documentation +### 2. User Documentation (Planned) - [ ] Update user guides - [ ] Add troubleshooting guides - [ ] Create FAQ @@ -253,18 +257,18 @@ ## Deployment ### 1. Build Process -- [ ] Update build scripts -- [ ] Add platform-specific builds -- [ ] Configure CI/CD -- [ ] Setup automated testing +- [x] Update build scripts +- [x] Add platform-specific builds +- [ ] Configure CI/CD (planned) +- [ ] Setup automated testing (planned) -### 2. Release Process +### 2. Release Process (Planned) - [ ] Create release checklist - [ ] Add version management - [ ] Setup rollback procedures - [ ] Configure monitoring -## Monitoring and Analytics +## Monitoring and Analytics (Planned) ### 1. Error Tracking - [ ] Setup error logging @@ -278,7 +282,7 @@ - [ ] Monitor performance - [ ] Collect user feedback -## Security Audit +## Security Audit (Planned) ### 1. Code Review - [ ] Review encryption @@ -295,29 +299,31 @@ ## Success Criteria ### 1. Performance -- [ ] Query response time < 100ms -- [ ] Migration time < 5s per 1000 records -- [ ] Storage overhead < 10% -- [ ] Memory usage < 50MB -- [ ] Atomic operations complete successfully -- [ ] Transaction performance meets requirements +- [x] Query response time < 100ms +- [x] Operation queuing for thread safety +- [x] Proper initialization handling +- [ ] Migration time < 5s per 1000 records (planned) +- [ ] Storage overhead < 10% (planned) +- [ ] Memory usage < 50MB (planned) ### 2. Reliability -- [ ] 99.9% uptime -- [ ] Zero data loss -- [ ] Automatic recovery -- [ ] Backup verification -- [ ] Transaction atomicity -- [ ] Data consistency +- [x] Basic data integrity +- [x] Operation queuing +- [ ] Automatic recovery (planned) +- [ ] Backup verification (planned) +- [ ] Transaction atomicity (planned) +- [ ] Data consistency (planned) ### 3. Security -- [ ] AES-256 encryption -- [ ] Secure key storage -- [ ] Access control -- [ ] Audit logging +- [x] Basic data integrity +- [ ] AES-256 encryption (planned) +- [ ] Secure key storage (planned) +- [ ] Access control (planned) +- [ ] Audit logging (planned) ### 4. User Experience -- [ ] Smooth migration -- [ ] Clear error messages -- [ ] Progress indicators -- [ ] Recovery options \ No newline at end of file +- [x] Basic database operations +- [ ] Smooth migration (planned) +- [ ] Clear error messages (planned) +- [ ] Progress indicators (planned) +- [ ] Recovery options (planned) \ No newline at end of file