UNPKG

capacitor-biometric-authentication

Version:

Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!

338 lines (287 loc) 9.67 kB
import { BiometricAuthAdapter, BiometricAuthConfiguration, BiometricAuthOptions, BiometricAuthResult, BiometricAuthState, BiometricErrorCode, BiometryType } from './types'; import { PlatformDetector } from './platform-detector'; export class BiometricAuthCore { private static instance: BiometricAuthCore; private config: BiometricAuthConfiguration = { adapter: 'auto', debug: false, sessionDuration: 3600, // 1 hour in seconds (consistent with web.ts) }; private sessionTimeoutId: ReturnType<typeof setTimeout> | null = null; private adapters = new Map<string, BiometricAuthAdapter>(); private currentAdapter: BiometricAuthAdapter | null = null; private state: BiometricAuthState = { isAuthenticated: false }; private platformDetector = PlatformDetector.getInstance(); private subscribers = new Set<(state: BiometricAuthState) => void>(); // Store initialization promise to prevent race conditions // Methods can await this to ensure adapter is ready before use private initPromise: Promise<void>; private constructor() { // Store the promise so we can await it in methods this.initPromise = this.initialize().catch(err => { if (this.config.debug) { console.warn('[BiometricAuth] Initialization failed:', err); } }); } /** * Ensures the adapter is initialized before use. * This prevents race conditions where methods are called before * the async initialization completes. */ private async ensureInitialized(): Promise<void> { await this.initPromise; } static getInstance(): BiometricAuthCore { if (!BiometricAuthCore.instance) { BiometricAuthCore.instance = new BiometricAuthCore(); } return BiometricAuthCore.instance; } private async initialize() { // Detect platform and load appropriate adapter const platformInfo = this.platformDetector.detect(); if (this.config.debug) { console.warn('[BiometricAuth] Platform detected:', platformInfo); } // Load adapter based on platform await this.loadAdapter(platformInfo.name); } private async loadAdapter(platform: string) { try { // Try custom adapters first if (this.config.customAdapters?.[platform]) { this.currentAdapter = this.config.customAdapters[platform]; return; } // Try to load from registered adapters if (this.adapters.has(platform)) { this.currentAdapter = this.adapters.get(platform)!; return; } // Dynamic import based on platform switch (platform) { case 'web': const { WebAdapter } = await import('../adapters/WebAdapter'); this.currentAdapter = new WebAdapter(); break; case 'ios': case 'android': // Check if Capacitor is available if (this.platformDetector.detect().isCapacitor) { const { CapacitorAdapter } = await import('../adapters/CapacitorAdapter'); this.currentAdapter = new CapacitorAdapter(); } else if (this.platformDetector.detect().isCordova) { // For Cordova, we might need a separate adapter in the future throw new Error('Cordova support not yet implemented. Please use Capacitor for native biometric authentication.'); } break; case 'electron': const { ElectronAdapter } = await import('../adapters/ElectronAdapter'); this.currentAdapter = new ElectronAdapter(); break; default: throw new Error(`Platform ${platform} not supported`); } } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] Failed to load adapter:', error); } // Fallback to web adapter if available if (platform !== 'web' && this.platformDetector.detect().isWeb) { const { WebAdapter } = await import('../adapters/WebAdapter'); this.currentAdapter = new WebAdapter(); } } } configure(config: Partial<BiometricAuthConfiguration>) { this.config = { ...this.config, ...config }; // Re-initialize if adapter changed if (config.adapter && config.adapter !== 'auto') { this.loadAdapter(config.adapter); } } registerAdapter(name: string, adapter: BiometricAuthAdapter) { this.adapters.set(name, adapter); } async isAvailable(): Promise<boolean> { await this.ensureInitialized(); if (!this.currentAdapter) { return false; } try { return await this.currentAdapter.isAvailable(); } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] isAvailable error:', error); } return false; } } async getSupportedBiometrics(): Promise<BiometryType[]> { await this.ensureInitialized(); if (!this.currentAdapter) { return []; } try { return await this.currentAdapter.getSupportedBiometrics(); } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] getSupportedBiometrics error:', error); } return []; } } async authenticate(options?: BiometricAuthOptions): Promise<BiometricAuthResult> { await this.ensureInitialized(); if (!this.currentAdapter) { return { success: false, error: { code: BiometricErrorCode.PLATFORM_NOT_SUPPORTED, message: 'No biometric adapter available for this platform' } }; } try { const result = await this.currentAdapter.authenticate(options); if (result.success) { this.updateState({ isAuthenticated: true, sessionId: result.sessionId, lastAuthTime: Date.now(), biometryType: result.biometryType, error: undefined }); // Set up session timeout if (this.config.sessionDuration && this.config.sessionDuration > 0) { // Clear any existing timeout to prevent memory leaks if (this.sessionTimeoutId !== null) { clearTimeout(this.sessionTimeoutId); } // Convert seconds to milliseconds for setTimeout this.sessionTimeoutId = setTimeout(() => { this.logout(); }, this.config.sessionDuration * 1000); } } else { this.updateState({ isAuthenticated: false, error: result.error }); } return result; } catch (error) { const errorResult: BiometricAuthResult = { success: false, error: { code: BiometricErrorCode.UNKNOWN_ERROR, message: error instanceof Error ? error.message : 'Unknown error occurred', details: error } }; this.updateState({ isAuthenticated: false, error: errorResult.error }); return errorResult; } } async deleteCredentials(): Promise<void> { await this.ensureInitialized(); if (!this.currentAdapter) { throw new Error('No biometric adapter available'); } await this.currentAdapter.deleteCredentials(); this.logout(); } async hasCredentials(): Promise<boolean> { await this.ensureInitialized(); if (!this.currentAdapter) { return false; } try { return await this.currentAdapter.hasCredentials(); } catch (error) { if (this.config.debug) { console.warn('[BiometricAuth] hasCredentials error:', error); } return false; } } logout() { // Clear session timeout to prevent memory leaks if (this.sessionTimeoutId !== null) { clearTimeout(this.sessionTimeoutId); this.sessionTimeoutId = null; } this.updateState({ isAuthenticated: false, sessionId: undefined, lastAuthTime: undefined, biometryType: undefined, error: undefined }); } getState(): BiometricAuthState { return { ...this.state }; } isAuthenticated(): boolean { if (!this.state.isAuthenticated || !this.state.lastAuthTime) { return false; } // Check if session is still valid (sessionDuration is in seconds) if (this.config.sessionDuration && this.config.sessionDuration > 0) { const elapsedMs = Date.now() - this.state.lastAuthTime; const sessionDurationMs = this.config.sessionDuration * 1000; if (elapsedMs > sessionDurationMs) { this.logout(); return false; } } return true; } subscribe(callback: (state: BiometricAuthState) => void): () => void { this.subscribers.add(callback); // Return unsubscribe function return () => { this.subscribers.delete(callback); }; } private updateState(newState: Partial<BiometricAuthState>) { this.state = { ...this.state, ...newState }; // Notify subscribers this.subscribers.forEach(callback => { callback(this.getState()); }); } // Utility methods for common use cases async requireAuthentication(callback: () => void | Promise<void>, options?: BiometricAuthOptions) { if (!this.isAuthenticated()) { const result = await this.authenticate(options); if (!result.success) { throw new Error(result.error?.message || 'Authentication failed'); } } return callback(); } async withAuthentication<T>(callback: () => T | Promise<T>, options?: BiometricAuthOptions): Promise<T> { if (!this.isAuthenticated()) { const result = await this.authenticate(options); if (!result.success) { throw new Error(result.error?.message || 'Authentication failed'); } } return callback(); } }