UNPKG

@neabyte/touchid

Version:

Native macOS Touch ID authentication with device identification, TTL caching, and TypeScript support. Features hardware UUID, device serial, and biometric type detection.

324 lines (323 loc) 11.8 kB
import { TouchIDError } from './types/touchid.js'; import { TouchIDEventEmitterImpl } from './utils/event-emitter.js'; import { fileURLToPath } from 'url'; import path from 'path'; import os from 'node:os'; function isMacOs() { return os.platform() === 'darwin'; } function validateOptions(options) { if (options.ttl !== undefined && (options.ttl < 1000 || options.ttl > 300000)) { return { valid: false, error: 'TTL must be between 1000 and 300000 milliseconds' }; } if (options.reason !== undefined && options.reason.trim().length === 0) { return { valid: false, error: 'Reason cannot be empty' }; } return { valid: true }; } export class TouchIDService { nativeModule = null; initialized = false; initPromise = null; eventEmitter; constructor() { this.eventEmitter = new TouchIDEventEmitterImpl(); this.init(); } get events() { return this.eventEmitter; } async init() { if (this.initPromise) { return this.initPromise; } this.initPromise = this._init(); return this.initPromise; } async _init() { const startTime = Date.now(); this.eventEmitter.emit('initialization:start', { timestamp: startTime }); if (!isMacOs()) { console.warn('⚠️ Touch ID is only available on macOS - skipping native module load'); this.nativeModule = null; this.initialized = true; this.eventEmitter.emit('initialization:error', { timestamp: Date.now(), error: 'Touch ID is only available on macOS' }); return; } try { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const nativePath = path.join(__dirname, '../build/Release/touchid-native'); const { createRequire } = await import('module'); const require = createRequire(import.meta.url); this.nativeModule = require(nativePath); this.initialized = true; this.eventEmitter.emit('initialization:complete', { timestamp: Date.now(), duration: Date.now() - startTime }); } catch (error) { console.warn('Touch ID native module not available:', error); this.nativeModule = null; this.initialized = true; this.eventEmitter.emit('initialization:error', { timestamp: Date.now(), error: error instanceof Error ? error.message : 'Unknown initialization error' }); } } async waitForInit() { while (!this.initialized) { await new Promise(resolve => setTimeout(resolve, 10)); } } async isAvailable() { if (!isMacOs()) { console.warn('⚠️ Touch ID is only available on macOS'); this.eventEmitter.emit('device:unavailable', { timestamp: Date.now(), reason: 'Touch ID is only available on macOS' }); return false; } await this.waitForInit(); if (!this.nativeModule) { this.eventEmitter.emit('device:unavailable', { timestamp: Date.now(), reason: 'Touch ID native module not available' }); return false; } try { if (typeof this.nativeModule.canPromptTouchID === 'function') { const available = await this.nativeModule.canPromptTouchID(); if (available) { this.eventEmitter.emit('device:available', { timestamp: Date.now(), biometryType: 'TouchID' }); } else { this.eventEmitter.emit('device:unavailable', { timestamp: Date.now(), reason: 'Touch ID not available on this device' }); } return available; } else { throw new Error('canPromptTouchID function not available'); } } catch (error) { console.error('Error checking Touch ID availability:', error); this.eventEmitter.emit('device:unavailable', { timestamp: Date.now(), reason: error instanceof Error ? error.message : 'Unknown error' }); return false; } } async getBiometricInfo() { if (!isMacOs()) { console.warn('⚠️ Touch ID is only available on macOS'); const unsupportedPlatform = 'Unsupported Platform'; return { biometricsAvailable: false, biometryType: 'Unsupported', deviceSerial: unsupportedPlatform, deviceModel: unsupportedPlatform, systemVersion: unsupportedPlatform, hardwareUUID: unsupportedPlatform }; } await this.waitForInit(); if (!this.nativeModule) { const emptyValue = ''; return { biometricsAvailable: false, biometryType: 'None', deviceSerial: emptyValue, deviceModel: emptyValue, systemVersion: emptyValue, hardwareUUID: emptyValue }; } try { if (typeof this.nativeModule.getBiometricInfo === 'function') { return await this.nativeModule.getBiometricInfo(); } else { throw new Error('getBiometricInfo function not available'); } } catch (error) { console.error('Error getting biometric info:', error); const errorValue = 'Error'; return { biometricsAvailable: false, biometryType: 'None', deviceSerial: errorValue, deviceModel: errorValue, systemVersion: errorValue, hardwareUUID: errorValue }; } } async authenticate(options = {}) { const startTime = Date.now(); const method = options.method || 'direct'; const reason = options.reason || 'Authenticate with Touch ID'; this.eventEmitter.emit('authentication:start', { timestamp: startTime, method, reason }); if (!isMacOs()) { console.warn('⚠️ Touch ID is only available on macOS'); const duration = Date.now() - startTime; this.eventEmitter.emit('authentication:failure', { timestamp: Date.now(), method, error: TouchIDError.NOT_AVAILABLE, duration }); return { success: false, error: TouchIDError.NOT_AVAILABLE }; } const validation = validateOptions(options); if (!validation.valid) { const duration = Date.now() - startTime; this.eventEmitter.emit('authentication:failure', { timestamp: Date.now(), method, error: validation.error, duration }); return { success: false, error: validation.error }; } await this.waitForInit(); if (!this.nativeModule) { const duration = Date.now() - startTime; this.eventEmitter.emit('authentication:failure', { timestamp: Date.now(), method, error: 'Touch ID native module not available', duration }); return { success: false, error: 'Touch ID native module not available' }; } try { const ttl = options.ttl || 30000; if (typeof this.nativeModule.promptTouchID !== 'function') { throw new Error('promptTouchID function not available'); } const result = await this.nativeModule.promptTouchID({ reason, method, ttl }); const duration = Date.now() - startTime; if (result.success) { if (method === 'cached') { this.eventEmitter.emit('cache:created', { timestamp: Date.now(), ttl }); } this.eventEmitter.emit('authentication:success', { timestamp: Date.now(), method, duration, data: result.data }); } else { this.eventEmitter.emit('authentication:failure', { timestamp: Date.now(), method, error: result.error || 'Unknown error', duration }); } return { success: true, data: result.data }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; let mappedError = errorMessage; if (errorMessage.includes('not available')) { mappedError = TouchIDError.NOT_AVAILABLE; } else if (errorMessage.includes('not enrolled')) { mappedError = TouchIDError.NOT_ENROLLED; } else if (errorMessage.includes('locked out')) { mappedError = TouchIDError.LOCKOUT; this.eventEmitter.emit('device:lockout', { timestamp: Date.now(), duration: 300000, reason: mappedError }); } else if (errorMessage.includes('cancelled') || errorMessage.includes('canceled')) { mappedError = TouchIDError.USER_CANCEL; const duration = Date.now() - startTime; this.eventEmitter.emit('authentication:cancel', { timestamp: Date.now(), method, duration }); } else if (errorMessage.includes('passcode not set')) { mappedError = TouchIDError.PASSCODE_NOT_SET; } else if (errorMessage.includes('not interactive')) { mappedError = TouchIDError.NOT_INTERACTIVE; } const duration = Date.now() - startTime; this.eventEmitter.emit('authentication:failure', { timestamp: Date.now(), method, error: mappedError, duration }); return { success: false, error: mappedError }; } } async test() { const available = await this.isAvailable(); if (available) { const result = await this.authenticate({ reason: 'Test Touch ID authentication' }); if (!result.success) { throw new Error(`Touch ID test failed: ${result.error}`); } } else { throw new Error('Touch ID is not available on this device'); } } } export const createTouchIDService = () => { return new TouchIDService(); }; export const touchID = createTouchIDService();