capacitor-biometric-authentication
Version:
Framework-agnostic biometric authentication library. Works with React, Vue, Angular, or vanilla JS. No providers required!
1,186 lines (1,176 loc) • 62.3 kB
JavaScript
var BiometricAuth = (function (exports) {
'use strict';
/**
* Unified error codes for biometric authentication
* Using UPPER_CASE convention for enum values (more conventional)
*/
exports.BiometricErrorCode = void 0;
(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";
})(exports.BiometricErrorCode || (exports.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: exports.BiometricErrorCode.AUTHENTICATION_FAILED,
userCancelled: exports.BiometricErrorCode.USER_CANCELLED,
systemCancelled: exports.BiometricErrorCode.SYSTEM_CANCELLED,
notAvailable: exports.BiometricErrorCode.NOT_AVAILABLE,
permissionDenied: exports.BiometricErrorCode.PERMISSION_DENIED,
lockedOut: exports.BiometricErrorCode.LOCKED_OUT,
invalidContext: exports.BiometricErrorCode.INVALID_CONTEXT,
notEnrolled: exports.BiometricErrorCode.NOT_ENROLLED,
timeout: exports.BiometricErrorCode.TIMEOUT,
unknown: exports.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 Promise.resolve().then(function () { return CapacitorAdapter$1; });
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: exports.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: exports.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: exports.BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled the authentication',
details: error,
};
case 'AbortError':
return {
code: exports.BiometricErrorCode.USER_CANCELLED,
message: 'Authentication was aborted',
details: error,
};
case 'SecurityError':
return {
code: exports.BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Security error during authentication',
details: error,
};
case 'InvalidStateError':
return {
code: exports.BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Invalid state for authentication',
details: error,
};
case 'NotSupportedError':
return {
code: exports.BiometricErrorCode.BIOMETRIC_UNAVAILABLE,
message: 'WebAuthn is not supported',
details: error,
};
case 'TimeoutError':
return {
code: exports.BiometricErrorCode.TIMEOUT,
message: 'Authentication timed out',
details: error,
};
case 'ConstraintError':
return {
code: exports.BiometricErrorCode.AUTHENTICATION_FAILED,
message: 'Authenticator constraint not satisfied',
details: error,
};
default:
return {
code: exports.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: exports.BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled the operation',
details: error,
};
}
if (message.includes('timeout') || message.includes('timed out')) {
return {
code: exports.BiometricErrorCode.TIMEOUT,
message: 'Operation timed out',
details: error,
};
}
if (message.includes('not available') || message.includes('unavailable')) {
return {
code: exports.BiometricErrorCode.BIOMETRIC_UNAVAILABLE,
message: 'Biometric authentication is not available',
details: error,
};
}
if (message.includes('not supported')) {
return {
code: exports.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: exports.BiometricErrorCode.NOT_ENROLLED,
message: 'No biometrics enrolled on this device',
details: error,
};
}
if (message.includes('locked out') || message.includes('lockout')) {
return {
code: exports.BiometricErrorCode.LOCKED_OUT,
message: 'Biometric authentication is locked out due to too many attempts',
details: error,
};
}
return {
code: exports.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: exports.BiometricErrorCode.UNKNOWN_ERROR,
message: error,
details: error,
};
}
// Handle completely unknown errors
return {
code: exports.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: exports.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: exports.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: exports.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: exports.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: exports.BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled authentication',
details: error,
};
}
if (message.includes('passcode not set') || message.includes('passcodeNotSet')) {
return {
code: exports.BiometricErrorCode.NOT_ENROLLED,
message: 'Passcode is not set on this device',
details: error,
};
}
if (message.includes('biometry not available') || message.includes('biometryNotAvailable')) {
return {
code: exports.BiometricErrorCode.NOT_AVAILABLE,
message: 'Biometry is not available on this device',
details: error,
};
}
if (message.includes('biometry not enrolled') || message.includes('biometryNotEnrolled')) {
return {
code: exports.BiometricErrorCode.NOT_ENROLLED,
message: 'No biometrics are enrolled on this device',
details: error,
};
}
if (message.includes('biometry lockout') || message.includes('biometryLockout')) {
return {
code: exports.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: exports.BiometricErrorCode.USER_CANCELLED,
message: 'User cancelled authentication',
details: error,
};
}
if (message.includes('ERROR_NO_BIOMETRICS') || message.includes('no biometrics')) {
return {
code: exports.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: exports.BiometricErrorCode.NOT_AVAILABLE,
message: 'Biometric hardware is not available',
details: error,
};
}
if (message.includes('ERROR_HW_UNAVAILABLE') || message.includes('hw unavailable')) {
return {
code: exports.BiometricErrorCode.NOT_AVAILABLE,
message: 'Biometric hardware is currently unavailable',
details: error,
};
}
if (message.includes('ERROR_LOCKOUT') || message.includes('lockout')) {
return {
code: exports.BiometricErrorCode.LOCKED_OUT,
message: 'Biometric authentication is locked out',
details: error,
};
}
if (message.includes('ERROR_NEGATIVE_BUTTON') || message.includes('negative button')) {
return {
code: exports.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: exports.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: exports.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
});
class CapacitorAdapter {
constructor() {
this.platform = 'capacitor';
this.capacitorPlugin = null;
// IMPORTANT: Use a flag to track initialization instead of checking plugin truthiness
// Capacitor's plugin proxy intercepts ALL property access including truthiness checks
// which can trigger ".then() is not implemented" errors
this.pluginInitialized = false;
// Plugin will be loaded dynamically
}
async getPlugin() {
var _a, _b, _c;
// Use flag-based check instead of checking plugin truthiness
// to avoid triggering Capacitor proxy's property interception
if (this.pluginInitialized) {
return this.capacitorPlugin;
}
try {
// Try to get the registered Capacitor plugin
const capacitorCore = await import('@capacitor/core');
// Get Capacitor global object for platform detection
const capacitorGlobal = capacitorCore.Capacitor;
// Check if we're on a native platform (Android/iOS)
const isNative = (_b = (_a = capacitorGlobal === null || capacitorGlobal === void 0 ? void 0 : capacitorGlobal.isNativePlatform) === null || _a === void 0 ? void 0 : _a.call(capacitorGlobal)) !== null && _b !== void 0 ? _b : false;
if (isNative) {
// CRITICAL: On native platforms, the plugin is ALREADY registered by Capacitor
// DO NOT call registerPlugin() again - it creates a broken proxy that throws
// ".then() is not implemented" errors when JavaScript checks for Promise-like objects
// Instead, get the reference from Capacitor.Plugins which has the working native bridge
const nativePlugin = (_c = capacitorGlobal === null || capacitorGlobal === void 0 ? void 0 : capacitorGlobal.Plugins) === null || _c === void 0 ? void 0 : _c['BiometricAuth'];
if (nativePlugin) {
this.capacitorPlugin = nativePlugin;
this.pluginInitialized = true;
return this.capacitorPlugin;
}
// If not in Plugins yet, it might still be initializing - throw to retry later
throw new Error('Native BiometricAuth plugin not yet registered');
}
// WEB ONLY: Use registerPlugin to create a web implementation
// This is safe on web because there's no native plugin to conflict with
if (capacitorCore.registerPlugin) {
try {
this.capacitorPlugin = capacitorCore.registerPlugin('BiometricAuth');
this.pluginInitialized = true;
return this.capacitorPlugin;
}
catch (_d) {
// Continue to fallback
}
}
// Legacy support for older Capacitor versions (web only)
const legacyPlugins = capacitorCore.Plugins;
if (legacyPlugins === null || legacyPlugins === void 0 ? void 0 : legacyPlugins.BiometricAuth) {
this.capacitorPlugin = legacyPlugins.BiometricAuth;
this.pluginInitialized = true;
return this.capacitorPlugin;
}
// If not found in Plugins, try direct import
// This allows the plugin to work even if not properly registered
const BiometricAuthPlugin = window.BiometricAuthPlugin;
if (BiometricAuthPlugin) {
this.capacitorPlugin = BiometricAuthPlugin;
this.pluginInitialized = true;
return this.capacitorPlugin;
}
throw new Error('BiometricAuth Capacitor plugin not found');
}
catch (error) {
throw new Error('Failed to load Capacitor plugin: ' + error.message);
}
}
async isAvailable() {
try {
const plugin = await this.getPlugin();
const result = await plugin.isAvailable();
return result.isAvailable || false;
}
catch (_a) {
return false;
}
}
async getSupportedBiometrics() {
try {
const plugin = await this.getPlugin();
const result = await plugin.getSupportedBiometrics();
// Map Capacitor biometry types to our types
return (result.biometryTypes || []).map((type) => {
switch (type.toLowerCase()) {
case 'fingerprint':
return BiometryType.FINGERPRINT;
case 'faceid':
case 'face_id':
return BiometryType.FACE_ID;
case 'touchid':
case 'touch_id':
return BiometryType.TOUCH_ID;
case 'iris':
return BiometryType.IRIS;
default:
return BiometryType.UNKNOWN;
}
}).filter((type) => type !== BiometryType.UNKNOWN);
}
catch (_a) {
return [];
}
}
async authenticate(options) {
var _a, _b;
try {
const plugin = await this.getPlugin();
// Map our options to Capacitor plugin options
const capacitorOptions = Object.assign(Object.assign({ reason: (options === null || options === void 0 ? void 0 : options.reason) || 'Authenticate to continue', cancelTitle: options === null || options === void 0 ? void 0 : options.cancelTitle, fallbackTitle: options === null || options === void 0 ? void 0 : options.fallbackTitle, disableDeviceCredential: options === null || options === void 0 ? void 0 : options.disableDeviceCredential, maxAttempts: options === null || options === void 0 ? void 0 : options.maxAttempts, requireConfirmation: options === null || options === void 0 ? void 0 : options.requireConfirmation }, (((_a = options === null || options === void 0 ? void 0 : options.platform) === null || _a === void 0 ? void 0 : _a.android) || {})), (((_b = options === null || options === void 0 ? void 0 : options.platform) === null || _b === void 0 ? void 0 : _b.ios) || {}));
const result = await plugin.authenticate(capacitorOptions);
if (result.success) {
const biometryType = this.mapBiometryType(result.biometryType);
return {
success: true,
biometryType,
sessionId: this.generateSessionId(),
platform: 'capacitor'
};
}
else {
return {
success: false,
error: this.mapError(result.error)
};
}
}
catch (error) {
return {
success: false,
error: this.mapError(error)
};
}
}
async deleteCredentials() {
try {
const plugin = await this.getPlugin();
await plugin.deleteCredentials();
}
catch (_a) {
// Ignore errors when deleting credentials
}
}
async hasCredentials() {
try {
const plugin = await this.getPlugin();
// Check if the plugin has a hasCredentials method
if (typeof plugin.hasCredentials === 'function') {
const result = await plugin.hasCredentials();
return result.hasCredentials || false;
}
// Fallback: assume credentials exist if biometrics are available
return await this.isAvailable();
}
catch (_a) {
return false;
}
}
mapBiometryType(type) {
if (!type) {
return BiometryType.UNKNOWN;
}
switch (type.toLowerCase()) {
case 'fingerprint':
return BiometryType.FINGERPRINT;
case 'faceid':
case 'face_id':
return BiometryType.FACE_ID;
case 'touchid':
case 'touch_id':
return BiometryType.TOUCH_ID;
case 'iris':
return BiometryType.IRIS;
default:
return BiometryType.UNKNOWN;
}
}
mapError(error) {
let code = exports.BiometricErrorCode.UNKNOWN_ERROR;
let message = 'An unknown error occurred';
const errorObj = error;
if (errorObj === null || errorObj === void 0 ? void 0 : errorObj.code) {
switch (errorObj.code) {
case 'BIOMETRIC_UNAVAILABLE':
case 'UNAVAILABLE':
code = exports.BiometricErrorCode.BIOMETRIC_UNAVAILABLE;
message = errorObj.message || 'Biometric authentication is not