// webauthn_bridge.js // Bridges Flutter Web to @simplewebauthn/browser for Passkey WebAuthn registration and authentication (function () { window.WebAuthnBridge = { isSupported: function () { try { if (typeof window.PublicKeyCredential !== 'undefined') { return true; } if (window.SimpleWebAuthnBrowser && typeof window.SimpleWebAuthnBrowser.browserSupportsWebAuthn === 'function') { return window.SimpleWebAuthnBrowser.browserSupportsWebAuthn(); } return false; } catch (e) { return false; } }, getDeviceLabel: function () { try { var ua = navigator.userAgent; var browser = 'Browser'; if (ua.indexOf('Firefox') > -1) browser = 'Firefox'; else if (ua.indexOf('SamsungBrowser') > -1) browser = 'Samsung Internet'; else if (ua.indexOf('Opera') > -1 || ua.indexOf('OPR') > -1) browser = 'Opera'; else if (ua.indexOf('Edg') > -1) browser = 'Edge'; else if (ua.indexOf('Chrome') > -1) browser = 'Chrome'; else if (ua.indexOf('Safari') > -1) browser = 'Safari'; var os = 'Web'; if (ua.indexOf('Win') > -1) os = 'Windows'; else if (ua.indexOf('Mac') > -1) os = 'macOS'; else if (ua.indexOf('Android') > -1) os = 'Android'; else if (ua.indexOf('iPhone') > -1 || ua.indexOf('iPad') > -1) os = 'iOS'; else if (ua.indexOf('Linux') > -1) os = 'Linux'; return browser + ' on ' + os; } catch (e) { return 'Web Browser'; } }, _toBase64Url: function (val) { if (!val) return ''; if (typeof val === 'string') return val; if (Array.isArray(val) || val instanceof Uint8Array) { var binary = ''; var bytes = (val instanceof Uint8Array) ? val : new Uint8Array(val); for (var i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } if (val.data && (Array.isArray(val.data) || val.data instanceof Uint8Array)) { return this._toBase64Url(val.data); } return String(val); }, _unwrapOptions: function (options) { if (!options) return {}; // Parse recursively if string while (typeof options === 'string') { try { options = JSON.parse(options); } catch (e) { console.warn('[WebAuthnBridge] Failed to parse options string:', e); return {}; } } // Check if wrapped in common response envelopes if (options.publicKey) { options = (typeof options.publicKey === 'string') ? JSON.parse(options.publicKey) : options.publicKey; } else if (options.options) { options = (typeof options.options === 'string') ? JSON.parse(options.options) : options.options; } else if (options.creation_options) { options = (typeof options.creation_options === 'string') ? JSON.parse(options.creation_options) : options.creation_options; } else if (options.request_options) { options = (typeof options.request_options === 'string') ? JSON.parse(options.request_options) : options.request_options; } else if (options.data && typeof options.data === 'object' && !options.challenge) { var sub = options.data.options || options.data.publicKey || options.data.creation_options || options.data; options = (typeof sub === 'string') ? JSON.parse(sub) : sub; } // Ensure challenge is string if (options.challenge) { options.challenge = this._toBase64Url(options.challenge); } // Normalize RP if (!options.rp || typeof options.rp !== 'object') { options.rp = { id: window.location.hostname, name: 'VeloxIQ' }; } else { if (!options.rp.id) options.rp.id = window.location.hostname; if (!options.rp.name) options.rp.name = 'VeloxIQ'; } // Normalize User (for registration) if (!options.user || typeof options.user !== 'object') { if (options.user_id || options.userId) { options.user = { id: this._toBase64Url(options.user_id || options.userId), name: options.user_name || options.userName || options.name || 'User', displayName: options.user_display_name || options.displayName || 'User' }; } } else { if (options.user.id) { options.user.id = this._toBase64Url(options.user.id); } else { options.user.id = this._toBase64Url('user_' + Date.now()); } if (!options.user.name) options.user.name = 'User'; if (!options.user.displayName) options.user.displayName = options.user.name; } // Normalize pubKeyCredParams if (options.pub_key_cred_params && !options.pubKeyCredParams) { options.pubKeyCredParams = options.pub_key_cred_params; } if (!options.pubKeyCredParams || !Array.isArray(options.pubKeyCredParams) || options.pubKeyCredParams.length === 0) { options.pubKeyCredParams = [ { alg: -7, type: 'public-key' }, { alg: -257, type: 'public-key' } ]; } // Normalize authenticatorSelection if (options.authenticator_selection && !options.authenticatorSelection) { options.authenticatorSelection = options.authenticator_selection; } if (options.authenticatorSelection) { if (options.authenticatorSelection.user_verification && !options.authenticatorSelection.userVerification) { options.authenticatorSelection.userVerification = options.authenticatorSelection.user_verification; } if (options.authenticatorSelection.resident_key && !options.authenticatorSelection.residentKey) { options.authenticatorSelection.residentKey = options.authenticatorSelection.resident_key; } } // Normalize excludeCredentials var rawExclude = options.excludeCredentials || options.exclude_credentials; if (Array.isArray(rawExclude)) { var cleanExclude = []; for (var i = 0; i < rawExclude.length; i++) { var item = rawExclude[i]; if (item) { var rawId = item.id || item.credential_id || item.credentialId; if (rawId) { cleanExclude.push({ type: item.type || 'public-key', id: this._toBase64Url(rawId), transports: item.transports || [] }); } } } options.excludeCredentials = cleanExclude; } // Normalize allowCredentials var rawAllow = options.allowCredentials || options.allow_credentials; if (Array.isArray(rawAllow)) { var cleanAllow = []; for (var j = 0; j < rawAllow.length; j++) { var aItem = rawAllow[j]; if (aItem) { var aRawId = aItem.id || aItem.credential_id || aItem.credentialId; if (aRawId) { cleanAllow.push({ type: aItem.type || 'public-key', id: this._toBase64Url(aRawId), transports: aItem.transports || [] }); } } } options.allowCredentials = cleanAllow; } return options; }, startRegistration: async function (optionsJsonOrStr) { if (!window.SimpleWebAuthnBrowser) { throw new Error('SimpleWebAuthnBrowser library is not loaded'); } var options = this._unwrapOptions(optionsJsonOrStr); console.log('[WebAuthnBridge] Normalized registration options:', options); if (!options.challenge) { throw new Error('Missing challenge in WebAuthn registration options.'); } var resp = await window.SimpleWebAuthnBrowser.startRegistration({ optionsJSON: options }); return JSON.stringify(resp); }, startAuthentication: async function (optionsJsonOrStr) { if (!window.SimpleWebAuthnBrowser) { throw new Error('SimpleWebAuthnBrowser library is not loaded'); } var options = this._unwrapOptions(optionsJsonOrStr); console.log('[WebAuthnBridge] Normalized authentication options:', options); if (!options.challenge) { throw new Error('Missing challenge in WebAuthn authentication options.'); } var resp = await window.SimpleWebAuthnBrowser.startAuthentication({ optionsJSON: options }); return JSON.stringify(resp); } }; })();