UNPKG

capacitor-biometric-authentication

Version:

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

1,207 lines (1,193 loc) 45.1 kB
export { CapacitorAdapter } from './adapters/CapacitorAdapter'; /** * Unified error codes for biometric authentication * Using UPPER_CASE convention for enum values (more conventional) */ var BiometricErrorCode; (function (BiometricErrorCode) { /** Authentication attempt failed */ BiometricErrorCode["AUTHENTICATION_FAILED"] = "AUTHENTICATION_FAILED"; /** User cancelled the authentication */ BiometricErrorCode["USER_CANCELLED"] = "USER_CANCELLED"; /** System cancelled the authentication */ BiometricErrorCode["SYSTEM_CANCELLED"] = "SYSTEM_CANCELLED"; /** Biometric hardware not available */ BiometricErrorCode["NOT_AVAILABLE"] = "NOT_AVAILABLE"; /** Biometric hardware unavailable (legacy alias) */ BiometricErrorCode["BIOMETRIC_UNAVAILABLE"] = "BIOMETRIC_UNAVAILABLE"; /** Permission denied by user */ BiometricErrorCode["PERMISSION_DENIED"] = "PERMISSION_DENIED"; /** User is locked out due to too many failed attempts */ BiometricErrorCode["LOCKED_OUT"] = "LOCKED_OUT"; /** Lockout (legacy alias) */ BiometricErrorCode["LOCKOUT"] = "LOCKOUT"; /** Invalid context for authentication */ BiometricErrorCode["INVALID_CONTEXT"] = "INVALID_CONTEXT"; /** No biometrics enrolled on device */ BiometricErrorCode["NOT_ENROLLED"] = "NOT_ENROLLED"; /** Authentication timed out */ BiometricErrorCode["TIMEOUT"] = "TIMEOUT"; /** Platform not supported */ BiometricErrorCode["PLATFORM_NOT_SUPPORTED"] = "PLATFORM_NOT_SUPPORTED"; /** Unknown error occurred */ BiometricErrorCode["UNKNOWN"] = "UNKNOWN"; /** Unknown error (legacy alias) */ BiometricErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR"; })(BiometricErrorCode || (BiometricErrorCode = {})); /** * Reasons why biometric authentication is unavailable */ var BiometricUnavailableReason; (function (BiometricUnavailableReason) { /** Device doesn't have biometric hardware */ BiometricUnavailableReason["NO_HARDWARE"] = "noHardware"; /** Biometric hardware is unavailable */ BiometricUnavailableReason["HARDWARE_UNAVAILABLE"] = "hardwareUnavailable"; /** No biometrics enrolled on device */ BiometricUnavailableReason["NO_ENROLLED_BIOMETRICS"] = "noEnrolledBiometrics"; /** User denied permission */ BiometricUnavailableReason["PERMISSION_DENIED"] = "permissionDenied"; /** Biometric not supported on this platform */ BiometricUnavailableReason["NOT_SUPPORTED"] = "notSupported"; /** User locked out due to failed attempts */ BiometricUnavailableReason["LOCKED_OUT"] = "lockedOut"; /** User disabled biometrics */ BiometricUnavailableReason["USER_DISABLED"] = "userDisabled"; })(BiometricUnavailableReason || (BiometricUnavailableReason = {})); /** * Map legacy camelCase error codes to UPPER_CASE * Used for backward compatibility */ ({ authenticationFailed: BiometricErrorCode.AUTHENTICATION_FAILED, userCancelled: BiometricErrorCode.USER_CANCELLED, systemCancelled: BiometricErrorCode.SYSTEM_CANCELLED, notAvailable: BiometricErrorCode.NOT_AVAILABLE, permissionDenied: BiometricErrorCode.PERMISSION_DENIED, lockedOut: BiometricErrorCode.LOCKED_OUT, invalidContext: BiometricErrorCode.INVALID_CONTEXT, notEnrolled: BiometricErrorCode.NOT_ENROLLED, timeout: BiometricErrorCode.TIMEOUT, unknown: BiometricErrorCode.UNKNOWN, }); /** * Types of biometric authentication available */ var BiometryType; (function (BiometryType) { /** Fingerprint scanner */ BiometryType["FINGERPRINT"] = "fingerprint"; /** Apple Face ID */ BiometryType["FACE_ID"] = "faceId"; /** Apple Touch ID */ BiometryType["TOUCH_ID"] = "touchId"; /** Iris scanner */ BiometryType["IRIS"] = "iris"; /** Generic face authentication (Android) */ BiometryType["FACE_AUTHENTICATION"] = "faceAuthentication"; /** Multiple biometric types available */ BiometryType["MULTIPLE"] = "multiple"; /** Passcode/PIN fallback */ BiometryType["PASSCODE"] = "passcode"; /** Pattern lock (Android) */ BiometryType["PATTERN"] = "pattern"; /** PIN code */ BiometryType["PIN"] = "pin"; /** Unknown biometric type */ BiometryType["UNKNOWN"] = "unknown"; })(BiometryType || (BiometryType = {})); /** * Fallback authentication methods */ var FallbackMethod; (function (FallbackMethod) { FallbackMethod["PASSCODE"] = "passcode"; FallbackMethod["PASSWORD"] = "password"; FallbackMethod["PATTERN"] = "pattern"; FallbackMethod["PIN"] = "pin"; FallbackMethod["SECURITY_QUESTION"] = "securityQuestion"; })(FallbackMethod || (FallbackMethod = {})); class PlatformDetector { constructor() { this.platformInfo = null; } static getInstance() { if (!PlatformDetector.instance) { PlatformDetector.instance = new PlatformDetector(); } return PlatformDetector.instance; } detect() { var _a, _b; if (this.platformInfo) { return this.platformInfo; } const info = { name: 'unknown', isCapacitor: false, isReactNative: false, isCordova: false, isWeb: false, isIOS: false, isAndroid: false, isElectron: false }; // Check if we're in a browser environment if (typeof window !== 'undefined' && typeof document !== 'undefined') { info.isWeb = true; // Check for Capacitor if (window.Capacitor) { info.isCapacitor = true; const capacitor = window.Capacitor; const platform = (_a = capacitor === null || capacitor === void 0 ? void 0 : capacitor.getPlatform) === null || _a === void 0 ? void 0 : _a.call(capacitor); if (platform) { info.name = platform; info.isIOS = platform === 'ios'; info.isAndroid = platform === 'android'; info.isWeb = platform === 'web'; } } // Check for Cordova else if (window.cordova || window.phonegap) { info.isCordova = true; info.name = 'cordova'; const userAgent = navigator.userAgent || navigator.vendor || window.opera || ''; if (/android/i.test(userAgent)) { info.isAndroid = true; info.name = 'android'; } else if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { info.isIOS = true; info.name = 'ios'; } } // Check for Electron else if (((_b = window.process) === null || _b === void 0 ? void 0 : _b.type) === 'renderer' || navigator.userAgent.indexOf('Electron') !== -1) { info.isElectron = true; info.name = 'electron'; } // Default to web else { info.name = 'web'; } } // Node.js environment else if (typeof process !== 'undefined' && process.versions && process.versions.node) { info.name = 'node'; } // Get version info if available if (info.isCapacitor && typeof window !== 'undefined') { const capacitor = window.Capacitor; if (capacitor === null || capacitor === void 0 ? void 0 : capacitor.version) { info.version = capacitor.version; } } this.platformInfo = info; return info; } isSupported() { const info = this.detect(); return info.isWeb || info.isCapacitor || info.isCordova; } getPlatformName() { return this.detect().name; } isNativePlatform() { const info = this.detect(); return (info.isIOS || info.isAndroid) && (info.isCapacitor || info.isCordova); } } class BiometricAuthCore { constructor() { this.config = { adapter: 'auto', debug: false, sessionDuration: 3600, // 1 hour in seconds (consistent with web.ts) }; this.sessionTimeoutId = null; this.adapters = new Map(); this.currentAdapter = null; this.state = { isAuthenticated: false }; this.platformDetector = PlatformDetector.getInstance(); this.subscribers = new Set(); // 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. */ async ensureInitialized() { await this.initPromise; } static getInstance() { if (!BiometricAuthCore.instance) { BiometricAuthCore.instance = new BiometricAuthCore(); } return BiometricAuthCore.instance; } 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); } async loadAdapter(platform) { var _a; try { // Try custom adapters first if ((_a = this.config.customAdapters) === null || _a === void 0 ? void 0 : _a[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 Promise.resolve().then(function () { return WebAdapter$1; }); 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 Promise.resolve().then(function () { return ElectronAdapter$1; }); 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 Promise.resolve().then(function () { return WebAdapter$1; }); this.currentAdapter = new WebAdapter(); } } } configure(config) { this.config = Object.assign(Object.assign({}, this.config), config); // Re-initialize if adapter changed if (config.adapter && config.adapter !== 'auto') { this.loadAdapter(config.adapter); } } registerAdapter(name, adapter) { this.adapters.set(name, adapter); } async isAvailable() { 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() { 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) { 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 = { 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() { await this.ensureInitialized(); if (!this.currentAdapter) { throw new Error('No biometric adapter available'); } await this.currentAdapter.deleteCredentials(); this.logout(); } async hasCredentials() { 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() { return Object.assign({}, this.state); } isAuthenticated() { 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) { this.subscribers.add(callback); // Return unsubscribe function return () => { this.subscribers.delete(callback); }; } updateState(newState) { this.state = Object.assign(Object.assign({}, this.state), newState); // Notify subscribers this.subscribers.forEach(callback => { callback(this.getState()); }); } // Utility methods for common use cases async requireAuthentication(callback, options) { var _a; if (!this.isAuthenticated()) { const result = await this.authenticate(options); if (!result.success) { throw new Error(((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) || 'Authentication failed'); } } return callback(); } async withAuthentication(callback, options) { var _a; if (!this.isAuthenticated()) { const result = await this.authenticate(options); if (!result.success) { throw new Error(((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) || 'Authentication failed'); } } return callback(); } } /** * Unified encoding utilities for biometric authentication * * This module consolidates base64/base64url encoding functions * that were previously duplicated across multiple files. */ /** * Convert ArrayBuffer to standard base64 string */ function arrayBufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } /** * Convert ArrayBuffer to base64url string (WebAuthn standard) * Base64url is URL-safe: uses - instead of +, _ instead of /, no padding */ function arrayBufferToBase64URL(buffer) { return arrayBufferToBase64(buffer) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } /** * Convert base64 string to ArrayBuffer */ function base64ToArrayBuffer(base64) { const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes.buffer; } /** * Generate a cryptographically secure random session ID */ function generateSessionId() { const array = new Uint8Array(16); crypto.getRandomValues(array); return arrayBufferToBase64URL(array.buffer); } /** * Unified error handling utilities for biometric authentication * * This module consolidates error mapping logic from multiple adapters * to provide consistent error handling across platforms. */ /** * Map a WebAuthn DOMException to a BiometricError */ function mapDOMException(error) { switch (error.name) { case 'NotAllowedError': return { code: BiometricErrorCode.USER_CANCELLED, message: 'User cancelled the authentication', details: error, }; case 'AbortError': return { code: BiometricErrorCode.USER_CANCELLED, message: 'Authentication was aborted', details: error, }; case 'SecurityError': return { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Security error during authentication', details: error, }; case 'InvalidStateError': return { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Invalid state for authentication', details: error, }; case 'NotSupportedError': return { code: BiometricErrorCode.BIOMETRIC_UNAVAILABLE, message: 'WebAuthn is not supported', details: error, }; case 'TimeoutError': return { code: BiometricErrorCode.TIMEOUT, message: 'Authentication timed out', details: error, }; case 'ConstraintError': return { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Authenticator constraint not satisfied', details: error, }; default: return { code: BiometricErrorCode.UNKNOWN_ERROR, message: error.message || 'An unknown DOM error occurred', details: error, }; } } /** * Map a generic Error to a BiometricError */ function mapGenericError(error) { const message = error.message.toLowerCase(); // Check for common error patterns if (message.includes('cancelled') || message.includes('canceled')) { return { code: BiometricErrorCode.USER_CANCELLED, message: 'User cancelled the operation', details: error, }; } if (message.includes('timeout') || message.includes('timed out')) { return { code: BiometricErrorCode.TIMEOUT, message: 'Operation timed out', details: error, }; } if (message.includes('not available') || message.includes('unavailable')) { return { code: BiometricErrorCode.BIOMETRIC_UNAVAILABLE, message: 'Biometric authentication is not available', details: error, }; } if (message.includes('not supported')) { return { code: BiometricErrorCode.PLATFORM_NOT_SUPPORTED, message: 'Operation is not supported on this platform', details: error, }; } if (message.includes('not enrolled') || message.includes('no biometrics')) { return { code: BiometricErrorCode.NOT_ENROLLED, message: 'No biometrics enrolled on this device', details: error, }; } if (message.includes('locked out') || message.includes('lockout')) { return { code: BiometricErrorCode.LOCKED_OUT, message: 'Biometric authentication is locked out due to too many attempts', details: error, }; } return { code: BiometricErrorCode.UNKNOWN_ERROR, message: error.message || 'An unknown error occurred', details: error, }; } /** * Map any unknown error to a BiometricError */ function mapWebAuthnError(error) { if (error instanceof DOMException) { return mapDOMException(error); } if (error instanceof Error) { return mapGenericError(error); } // Handle string errors if (typeof error === 'string') { return { code: BiometricErrorCode.UNKNOWN_ERROR, message: error, details: error, }; } // Handle completely unknown errors return { code: BiometricErrorCode.UNKNOWN_ERROR, message: 'An unknown error occurred', details: error, }; } /** * Map a native platform error (iOS/Android/Windows) to a BiometricError */ function mapNativeError(error, platform) { const message = error.message.toLowerCase(); // Windows/Electron specific error messages if (platform === 'windows' || platform === 'electron') { // Windows Hello cancellation if (message.includes('cancelled') || message.includes('canceled') || message.includes('user refused')) { return { code: BiometricErrorCode.USER_CANCELLED, message: 'User cancelled Windows Hello authentication', details: error, }; } // Windows Hello not configured if (message.includes('not configured') || message.includes('not set up')) { return { code: BiometricErrorCode.NOT_ENROLLED, message: 'Windows Hello is not configured', details: error, }; } // Windows Hello not available if (message.includes('not available') || message.includes('not supported')) { return { code: BiometricErrorCode.NOT_AVAILABLE, message: 'Windows Hello is not available on this device', details: error, }; } // Windows Hello lockout if (message.includes('locked') || message.includes('too many attempts')) { return { code: BiometricErrorCode.LOCKED_OUT, message: 'Windows Hello is locked due to too many attempts', details: error, }; } } // iOS specific error messages if (platform === 'ios') { if (message.includes('user cancel') || message.includes('userCancel')) { return { code: BiometricErrorCode.USER_CANCELLED, message: 'User cancelled authentication', details: error, }; } if (message.includes('passcode not set') || message.includes('passcodeNotSet')) { return { code: BiometricErrorCode.NOT_ENROLLED, message: 'Passcode is not set on this device', details: error, }; } if (message.includes('biometry not available') || message.includes('biometryNotAvailable')) { return { code: BiometricErrorCode.NOT_AVAILABLE, message: 'Biometry is not available on this device', details: error, }; } if (message.includes('biometry not enrolled') || message.includes('biometryNotEnrolled')) { return { code: BiometricErrorCode.NOT_ENROLLED, message: 'No biometrics are enrolled on this device', details: error, }; } if (message.includes('biometry lockout') || message.includes('biometryLockout')) { return { code: BiometricErrorCode.LOCKED_OUT, message: 'Biometric authentication is locked out', details: error, }; } } // Android specific error messages if (platform === 'android') { if (message.includes('ERROR_USER_CANCELED') || message.includes('user canceled')) { return { code: BiometricErrorCode.USER_CANCELLED, message: 'User cancelled authentication', details: error, }; } if (message.includes('ERROR_NO_BIOMETRICS') || message.includes('no biometrics')) { return { code: BiometricErrorCode.NOT_ENROLLED, message: 'No biometrics are enrolled on this device', details: error, }; } if (message.includes('ERROR_HW_NOT_PRESENT') || message.includes('hw not present')) { return { code: BiometricErrorCode.NOT_AVAILABLE, message: 'Biometric hardware is not available', details: error, }; } if (message.includes('ERROR_HW_UNAVAILABLE') || message.includes('hw unavailable')) { return { code: BiometricErrorCode.NOT_AVAILABLE, message: 'Biometric hardware is currently unavailable', details: error, }; } if (message.includes('ERROR_LOCKOUT') || message.includes('lockout')) { return { code: BiometricErrorCode.LOCKED_OUT, message: 'Biometric authentication is locked out', details: error, }; } if (message.includes('ERROR_NEGATIVE_BUTTON') || message.includes('negative button')) { return { code: BiometricErrorCode.USER_CANCELLED, message: 'User pressed the negative button', details: error, }; } } // Fall back to generic error mapping return mapGenericError(error); } /** * Create a failed BiometricAuthResult from an error */ function createErrorResult(error, platform) { let biometricError; if (platform && error instanceof Error) { biometricError = mapNativeError(error, platform); } else { biometricError = mapWebAuthnError(error); } return { success: false, error: biometricError, }; } class WebAdapter { constructor() { this.platform = 'web'; this.credentials = new Map(); // Set default Relying Party info this.rpId = window.location.hostname; this.rpName = document.title || 'Biometric Authentication'; } async isAvailable() { // Check if WebAuthn is supported if (!window.PublicKeyCredential) { return false; } // Check if platform authenticator is available try { const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); return available; } catch (_a) { return false; } } async getSupportedBiometrics() { if (!(await this.isAvailable())) { return []; } // WebAuthn doesn't provide specific biometry types // Return generic "multiple" as modern devices support various methods return [BiometryType.MULTIPLE]; } async authenticate(options) { var _a; try { // Check if WebAuthn is available if (!(await this.isAvailable())) { return { success: false, error: { code: BiometricErrorCode.BIOMETRIC_UNAVAILABLE, message: 'WebAuthn is not available on this device' } }; } const webOptions = ((_a = options === null || options === void 0 ? void 0 : options.platform) === null || _a === void 0 ? void 0 : _a.web) || {}; // Try to get existing credential first const existingCredential = await this.getExistingCredential(webOptions); if (existingCredential) { return { success: true, biometryType: BiometryType.MULTIPLE, sessionId: generateSessionId(), platform: 'web' }; } // If no existing credential, create a new one const credential = await this.createCredential((options === null || options === void 0 ? void 0 : options.reason) || 'Authentication required', webOptions); if (credential) { // Store credential for future use const credentialId = arrayBufferToBase64(credential.rawId); this.credentials.set(credentialId, credential); this.saveCredentialId(credentialId); return { success: true, biometryType: BiometryType.MULTIPLE, sessionId: generateSessionId(), platform: 'web' }; } return { success: false, error: { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Failed to authenticate' } }; } catch (error) { return createErrorResult(error); } } async deleteCredentials() { this.credentials.clear(); localStorage.removeItem('biometric_credential_ids'); } async hasCredentials() { const storedIds = this.getStoredCredentialIds(); return storedIds.length > 0; } async getExistingCredential(options) { const storedIds = this.getStoredCredentialIds(); if (storedIds.length === 0) { return null; } try { const challenge = options.challenge || crypto.getRandomValues(new Uint8Array(32)); const publicKeyOptions = { challenge, rpId: options.rpId || this.rpId, timeout: options.timeout || 60000, userVerification: options.userVerification || 'preferred', allowCredentials: storedIds.map(id => ({ id: base64ToArrayBuffer(id), type: 'public-key' })) }; const credential = await navigator.credentials.get({ publicKey: publicKeyOptions }); return credential; } catch (_a) { return null; } } async createCredential(_reason, options) { try { const challenge = options.challenge || crypto.getRandomValues(new Uint8Array(32)); const userId = crypto.getRandomValues(new Uint8Array(32)); const publicKeyOptions = { challenge, rp: { id: options.rpId || this.rpId, name: options.rpName || this.rpName }, user: { id: userId, name: 'user@' + this.rpId, displayName: 'User' }, pubKeyCredParams: [ { type: 'public-key', alg: -7 }, // ES256 { type: 'public-key', alg: -257 } // RS256 ], authenticatorSelection: options.authenticatorSelection || { authenticatorAttachment: 'platform', userVerification: 'preferred', requireResidentKey: false, residentKey: 'discouraged' }, timeout: options.timeout || 60000, attestation: options.attestation || 'none' }; const credential = await navigator.credentials.create({ publicKey: publicKeyOptions }); return credential; } catch (_a) { return null; } } getStoredCredentialIds() { const stored = localStorage.getItem('biometric_credential_ids'); if (!stored) { return []; } try { return JSON.parse(stored); } catch (_a) { return []; } } saveCredentialId(id) { const existing = this.getStoredCredentialIds(); if (!existing.includes(id)) { existing.push(id); localStorage.setItem('biometric_credential_ids', JSON.stringify(existing)); } } } var WebAdapter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, WebAdapter: WebAdapter }); // Create singleton instance const biometricAuth = BiometricAuthCore.getInstance(); // Export the main API (provider-less, like Zustand) const BiometricAuth = { // Core methods configure: (config) => biometricAuth.configure(config), isAvailable: () => biometricAuth.isAvailable(), getSupportedBiometrics: () => biometricAuth.getSupportedBiometrics(), authenticate: (options) => biometricAuth.authenticate(options), deleteCredentials: () => biometricAuth.deleteCredentials(), hasCredentials: () => biometricAuth.hasCredentials(), // State management logout: () => biometricAuth.logout(), getState: () => biometricAuth.getState(), isAuthenticated: () => biometricAuth.isAuthenticated(), subscribe: (callback) => biometricAuth.subscribe(callback), // Utility methods requireAuthentication: (callback, options) => biometricAuth.requireAuthentication(callback, options), withAuthentication: (callback, options) => biometricAuth.withAuthentication(callback, options), // Advanced usage registerAdapter: (name, adapter) => biometricAuth.registerAdapter(name, adapter), }; // NOTE: Web plugin registration has been REMOVED // // The BiometricAuth package now works as follows: // - On NATIVE (Android/iOS): Uses Capacitor.Plugins.BiometricAuth which is auto-registered by native layer // - On WEB: Uses WebAdapter which implements WebAuthn API directly // // DO NOT add registerPlugin() calls here - it causes ".then() is not implemented" errors // on native platforms because it creates a conflicting proxy over the already-registered native plugin /** * Electron Adapter for biometric authentication * Supports: * - macOS: Touch ID via Electron's systemPreferences API * - Windows: Windows Hello via WebAuthn API */ class ElectronAdapter { constructor() { this.platform = 'electron'; this.windowsHelloAvailable = null; // Electron-specific initialization } /** * Check if Windows Hello is available */ async checkWindowsHello() { // Cache the result since this check can be expensive if (this.windowsHelloAvailable !== null) { return this.windowsHelloAvailable; } try { // Check if PublicKeyCredential API is available (WebAuthn) if (typeof PublicKeyCredential !== 'undefined' && typeof PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable === 'function') { this.windowsHelloAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); return this.windowsHelloAvailable; } } catch (_a) { // Windows Hello not available } this.windowsHelloAvailable = false; return false; } async isAvailable() { try { // Check if we're in Electron main or renderer process if (typeof process !== 'undefined' && process.versions && process.versions.electron) { // In Electron, we can use TouchID on macOS if (process.platform === 'darwin') { // eslint-disable-next-line @typescript-eslint/no-require-imports const electronModule = require('electron'); const { systemPreferences } = electronModule.remote || electronModule; return systemPreferences.canPromptTouchID(); } // On Windows, check for Windows Hello via WebAuthn if (process.platform === 'win32') { return await this.checkWindowsHello(); } return false; } return false; } catch (_a) { return false; } } async getSupportedBiometrics() { if (!(await this.isAvailable())) { return []; } // On macOS, we support Touch ID if (process.platform === 'darwin') { return [BiometryType.TOUCH_ID]; } // On Windows, Windows Hello supports multiple biometric types if (process.platform === 'win32') { // Windows Hello can use fingerprint, face, or PIN // We report FINGERPRINT as the primary type, but it could also be face recognition return [BiometryType.FINGERPRINT, BiometryType.FACE_ID]; } return []; } /** * Authenticate using Windows Hello via WebAuthn API */ async authenticateWithWindowsHello(options) { var _a, _b; try { // Generate a random challenge const challenge = new Uint8Array(32); crypto.getRandomValues(challenge); // Get WebAuthn options from the options object const webAuthnGet = (_a = options === null || options === void 0 ? void 0 : options.webAuthnOptions) === null || _a === void 0 ? void 0 : _a.get; const platformWeb = (_b = options === null || options === void 0 ? void 0 : options.platform) === null || _b === void 0 ? void 0 : _b.web; // Create a credential request for platform authenticator (Windows Hello) const publicKeyCredentialRequestOptions = { challenge: challenge.buffer, timeout: (webAuthnGet === null || webAuthnGet === void 0 ? void 0 : webAuthnGet.timeout) || (platformWeb === null || platformWeb === void 0 ? void 0 : platformWeb.timeout) || (options === null || options === void 0 ? void 0 : options.sessionTimeout) || 60000, userVerification: 'required', rpId: (webAuthnGet === null || webAuthnGet === void 0 ? void 0 : webAuthnGet.rpId) || (platformWeb === null || platformWeb === void 0 ? void 0 : platformWeb.rpId) || (typeof window !== 'undefined' ? window.location.hostname : 'localhost'), }; // Request authentication const credential = await navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions, }); if (!credential) { return { success: false, error: { code: BiometricErrorCode.AUTHENTICATION_FAILED, message: 'Windows Hello authentication failed - no credential returned' } }; } // Authentication successful return { success: true, biometryType: BiometryType.FINGERPRINT, // Windows Hello primary type sessionId: generateSessionId(), platform: 'electron', }; } catch (error) { return createErrorResult(error, 'windows'); } } async authenticate(options) { try { if (!(await this.isAvailable())) { return { success: false, error: { code: BiometricErrorCode.BIOMETRIC_UNAVAILABLE, message: 'Biometric authentication is not available' } }; } // macOS Touch ID if (process.platform === 'darwin') { // eslint-disable-next-line @typescript-eslint/no-require-imports const electronModule = require('electron'); const { systemPreferences } = electronModule.remote || electronModule; try { await systemPreferences.promptTouchID((options === null || options === void 0 ? void 0 : options.reason) || 'authenticate with Touch ID'); return { success: true, biometryType: BiometryType.TOUCH_ID, sessionId: generateSessionId(), platform: 'electron' }; } catch (touchIdError) { return createErrorResult(touchIdError, 'electron'); } } // Windows Hello if (process.platform === 'win32') { return await this.authenticateWithWindowsHello(options); } return { success: false, error: { code: BiometricErrorCode.PLATFORM_NOT_SUPPORTED, message: 'Platform not supported' } }; } catch (error) { return createErrorResult(error, 'electron'); } } async deleteCredentials() { // Electron doesn't store biometric credentials // This is a no-op } async hasCredentials() { // In Electron, we don't store credentials // Return true if biometrics are available return await this.isAvailable(); } } var ElectronAdapter$1 = /*#__PURE__*/Object.freeze({ __proto__: null, ElectronAdapter: ElectronAdapter }); export { BiometricAuth, BiometricAuthCore, BiometricErrorCode, PlatformDetector, WebAdapter, BiometricAuth as default }; //# sourceMappingURL=web.js.map