UNPKG

@quadible/web-sdk

Version:

The web sdk for Quadible's behavioral authentication service.

456 lines 18.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const eventemitter2_1 = require("eventemitter2"); const API_1 = __importDefault(require("./API")); const DeviceCollector_1 = __importDefault(require("./collectors/DeviceCollector")); const EventCollector_1 = __importDefault(require("./collectors/EventCollector")); const GeolocationCollector_1 = __importDefault(require("./collectors/GeolocationCollector")); const KeyboardCollector_1 = __importDefault(require("./collectors/KeyboardCollector")); const MouseCollector_1 = __importDefault(require("./collectors/MouseCollector")); const StateCollector_1 = __importDefault(require("./collectors/StateCollector")); const TouchCollector_1 = __importDefault(require("./collectors/TouchCollector")); const DebugUI_1 = __importDefault(require("./common/DebugUI")); const FormData_1 = require("./common/FormData"); const VideoWorker_1 = __importDefault(require("./common/VideoWorker")); const WebcamDialog_1 = __importDefault(require("./common/WebcamDialog")); const WebcamSession_1 = __importDefault(require("./common/WebcamSession")); const sleep_1 = __importDefault(require("./common/sleep")); const spinnerHtml_1 = require("./common/spinnerHtml"); const Status_1 = __importDefault(require("./models/Status")); const DEFAULT_COMPRESSION = true; class BehavioralAuthSDK extends eventemitter2_1.EventEmitter2 { configuration; static getClientId() { BehavioralAuthSDK.ensureClientId(); return localStorage.getItem(BehavioralAuthSDK.cidKey); } static ensureClientId() { const cid = localStorage.getItem(BehavioralAuthSDK.cidKey); if (!cid) { localStorage.setItem(BehavioralAuthSDK.cidKey, BehavioralAuthSDK.createUUID()); } } static createUUID() { return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, a => (a ^ Math.random() * 16 >> a / 4).toString(16)); } static defaultApiKey = '#DEFAULT_API_KEY'; static version = '2.0.11'; static storageKey = '_bauthsdk_authorization'; static cidKey = '__q_cid'; static isRUMInitialized = false; api; messages = { defaultLockScreenMessage: `Making sure it's you...` }; videoWorker; webcamSession; collectors = [ new KeyboardCollector_1.default(), new StateCollector_1.default(), new TouchCollector_1.default(), new DeviceCollector_1.default(this), new MouseCollector_1.default(), new EventCollector_1.default(), new GeolocationCollector_1.default() ]; dataPushIntervalIndex; status = Status_1.default.Stopped; isFaceAuthenticated = false; isScreenLocked = false; lockScreenContainer = document.createElement('div'); lockScreenText = document.createElement('div'); unlockScheduleId; lastAuthResult = null; debugUI; constructor(configuration) { super(); this.configuration = configuration; this.configuration.compression = this.defaultCompressionValue(configuration); this.registerCollectorErrorListeners(); this.configuration.serviceUrl = this.configuration.serviceUrl || 'https://api.quadible.io'; this.api = new API_1.default(this.configuration); this.videoWorker = new VideoWorker_1.default(this.configuration.verbose, this.configuration.serviceUrl); this.videoWorker.on('error', error => this.emit('error', error)); this.webcamSession = new WebcamSession_1.default(this.api, this.videoWorker); this.debugUI = this.configuration.debug && new DebugUI_1.default(this); this.videoWorker.on('predictions', (data) => { this.emit("predictions" /* SDKEvent.LocalFrameAnalysis */, data); }); this.videoWorker.on('video-can-play', () => { this.log('Worker can play video stream.'); }); this.videoWorker.on('video-play-start', () => { this.log('Worker video started playing.'); }); this.webcamSession.on("face-auth-change" /* WebcamSessionEvent.FaceAuthenticationStatusChanged */, ({ status, lastAuthResult }) => { this.isFaceAuthenticated = status; this.lastAuthResult = lastAuthResult; this.updateScreenLockStatus(); this.updateLockScreenMessageBasedOnFaceAuthResult(); }); this.api.setCid(BehavioralAuthSDK.getClientId()); this.buildLockScreen(); if (!configuration.pushIntervalMs) { configuration.pushIntervalMs = 15e3; } this.on("face-enrollment" /* SDKEvent.FaceEnrollmentSuccessful */, this.startVideoAuthenticationLoopIfNeeded.bind(this)); } /** * Sets the default value for compression if there is no current value */ defaultCompressionValue(configuration) { return configuration.compression !== undefined ? configuration.compression : DEFAULT_COMPRESSION; } /** * Start collecting and pushing data to the server. */ async start(forceUseThisVersion = false) { if (this.status === Status_1.default.Stopped) { this.setStatus(Status_1.default.Starting); this.updateScreenLockStatus(); try { if (!forceUseThisVersion) { this.log('Checking for updates...'); const { version: configuredSdkVersion } = await this.api.getConfiguredSdkVersion(); if (BehavioralAuthSDK.version !== configuredSdkVersion) { this.log(`A different version is configured (${BehavioralAuthSDK.version} => ${configuredSdkVersion}). Getting...`); await this.loadVersion(configuredSdkVersion).catch(e => { this.log('Failed to load new version. Using current version.'); this.log(e); // If the different version was loaded, the status would be Stopped, // but now we need to set it manually. this.setStatus(Status_1.default.Stopped); }); this.log(`Starting (${window.BehavioralAuthSDK.version}) ...`); return await this.start(true); } this.log('SDK is up to date.'); } const config = await this.api.getRemoteConfig(); this.saveSessionInfo({ ...config }); await this.startAllAvailableCollectors(); this.startDataPushLoop(); this.setStatus(Status_1.default.Started); this.startVideoAuthenticationLoopIfNeeded(); if (!this.getSessionInfo().faceEnrollmentStatus && this.configuration.useWebcam) { this.assignPhotoToUserPopup({ clickOutsideToClose: true }).catch(e => this.emit("error" /* SDKEvent.Error */, new Error(`User canceled enrollment. (${e?.message} ${e?.stack})`))); } } catch (e) { this.emit("error" /* SDKEvent.Error */, e); this.setStatus(Status_1.default.Stopped); } } } /** * Stop collecting and pushing data to the server. */ async stop() { this.stopAllCollectors(); this.stopDataPushLoop(); this.webcamSession.stop(); this.setStatus(Status_1.default.Stopped); } /** * Clears all information from storage. */ clearSession() { if (this.status !== Status_1.default.Stopped) { this.stop(); } this.lastAuthResult = null; localStorage.removeItem(BehavioralAuthSDK.storageKey); } /** Deletes current user data. */ async deleteUser() { await this.api.deleteUser(); } setApiKey(apiKey) { this.configuration.apiKey = apiKey; } isCollecting() { return this.status === Status_1.default.Starting || this.status === Status_1.default.Started; } /** * Flushes any remaining data and asks the service to authenticate the user. */ async authenticate() { await this.flushAll(); return await this.api.authenticate(); } async assignPhotoToUserPopup(options = { clickOutsideToClose: true }) { await this.api.loadDependency('https://cdn.jsdelivr.net/npm/three@v0.156.1/build/three.min.js'); const dialog = new WebcamDialog_1.default({ serviceUrl: this.configuration.serviceUrl, videoWorker: await this.getVideoWorker(), ...options }); dialog.show(); return new Promise((resolve, reject) => { dialog.on('canceled', reject); dialog.on('submit', async (base64DataUrl) => { dialog.lockSubmit(); dialog.clearErrorMessage(); try { await this.assignPhotoToUser(base64DataUrl); dialog.dispose(); resolve(undefined); } finally { dialog.showErrorMessage('There was an error, please try again.'); dialog.unlockSubmit(); } }); }); } async assignPhotoToUser(base64DataUrl) { const result = await fetch(base64DataUrl); const formData = (0, FormData_1.createFormData)(); const fileBlob = await result.blob(); formData.append('file', fileBlob); const response = await this.api.postFile('/v1/face/register', formData); if (response.status !== 200) { throw new Error('Could not assign photo: ' + await response.text()); } const session = this.getSessionInfo(); session.faceEnrollmentStatus = true; this.saveSessionInfo(session); this.emit("face-enrollment" /* SDKEvent.FaceEnrollmentSuccessful */); } setUserDisplayName(displayName) { const session = this.getSessionInfo(); session.userDisplayName = displayName; this.saveSessionInfo(session); } getSessionInfo() { let sessionInfo = {}; const storedInfo = localStorage.getItem(BehavioralAuthSDK.storageKey); if (storedInfo) { try { sessionInfo = JSON.parse(storedInfo); } catch (error) { this.emit("warning" /* SDKEvent.Warn */, error); } } return sessionInfo; } async loadVersion(version) { await this.api.loadSdk(version); const newCtor = window.BehavioralAuthSDK; const newInstance = new newCtor(this.configuration); this.constructor = newCtor; this.__proto__ = newInstance.__proto__; const existingListeners = this._listeners; Object.assign(this, newInstance); for (const { event, listener } of existingListeners) { this.on(event, listener); } } /** Copy listeners to updated instance */ _listeners = []; on(event, listener) { this._listeners.push({ event, listener }); super.on(event, listener); return this; } off(event, listener) { this._listeners.splice(this._listeners.findIndex(o => o.event === event && listener === listener), 1); super.off(event, listener); return this; } registerCollectorErrorListeners() { for (const collector of this.collectors) { collector.on('error', error => this.emit('error', error)); } } buildLockScreen() { const container = document.createElement('div'); const spinner = document.createElement('div'); this.lockScreenText.innerText = 'Making sure it\'s you...'; this.lockScreenText.style.fontFamily = `'Work Sans', sans-serif`; spinner.innerHTML = spinnerHtml_1.spinnerHtml; spinner.querySelector('.qdbl-circle').style.margin = '65px auto 30px'; spinner.querySelector('.qdbl-circle').classList.add('white'); this.lockScreenText.style.textAlign = 'center'; container.append(spinner, this.lockScreenText); this.lockScreenContainer.classList.add('qdbl-lock-screen'); this.lockScreenContainer.append(container); Object.assign(container.style, { width: '200px', height: '200px', position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }); Object.assign(this.lockScreenContainer.style, { position: 'fixed', top: '0px', left: '0px', width: '100%', height: '100%', background: '#00000080', color: 'white', zIndex: 9998 }); } updateScreenLockStatus() { if (this.configuration.lockScreenOnAuthLost) { if (this.shouldLockScreen()) { this.lockScreen(); } else { this.scheduleUnlockScreen(); } } } shouldLockScreen() { return !this.isFaceAuthenticated && (this.configuration.lockScreenOnFaceLost || (!this.lastAuthResult || (!this.lastAuthResult.authenticated && this.lastAuthResult.detectedFaces))); } lockScreen() { if (!this.isScreenLocked) { this.isScreenLocked = true; this.unlockScheduleId = null; const elements = document.querySelectorAll('body > :not(.qdbl-lock-screen)'); const focusedElement = document.querySelector(':focus'); this.lockScreenText.innerText = this.messages.defaultLockScreenMessage; focusedElement?.blur(); elements.forEach((e) => { // e.style.filter = 'blur(10px)'; }); document.body.append(this.lockScreenContainer); } } async scheduleUnlockScreen() { if (this.isScreenLocked) { this.isScreenLocked = false; const unlockScheduleId = Math.random(); this.unlockScheduleId = unlockScheduleId; await (0, sleep_1.default)(2e3); if (this.unlockScheduleId === unlockScheduleId) { this.unlockScreen(); } this.lockScreenText.innerText = this.messages.defaultLockScreenMessage; } } unlockScreen() { this.lockScreenContainer.remove(); const elements = document.querySelectorAll('body > *'); elements.forEach((e) => { e.style.filter = 'none'; }); } async startVideoAuthenticationLoopIfNeeded() { if (this.shouldRunVideoAuthentication() && !this.webcamSession.isStarted) { this.webcamSession.start(); } } updateLockScreenMessageBasedOnFaceAuthResult() { if (!this.lastAuthResult.authenticated && this.lastAuthResult.detectedFaces) { this.lockScreenText.innerText = 'You are not allowed to access this account.'; } else if (this.lastAuthResult.authenticated && this.isFaceAuthenticated) { this.lockScreenText.innerText = `Welcome back, ${this.getSessionInfo().userDisplayName}!`; } else { this.lockScreenText.innerText = this.messages.defaultLockScreenMessage; } } shouldRunVideoAuthentication() { return this.status === Status_1.default.Started && this.configuration.useWebcam && this.getSessionInfo().faceEnrollmentStatus; } async getVideoWorker() { await this.videoWorker.init(); return this.videoWorker; } setStatus(status) { this.status = status; } stopAllCollectors() { for (const collector of this.collectors) { if (collector.isCollecting) { collector.stop?.(); } } } startDataPushLoop() { this.dataPushIntervalIndex = setInterval(this.flushAll.bind(this), this.configuration.pushIntervalMs); } async flushAll() { const allEvents = []; for (const collector of this.collectors) { if (collector.isCollecting) { const data = collector.flush(); if (data.length) { allEvents.push(...data.map(o => ({ kind: o.kind, timestamp: o.timestamp, payload: o }))); } } } if (allEvents.length) { this.api.pushEvents(allEvents); } } stopDataPushLoop() { clearInterval(this.dataPushIntervalIndex); } saveSessionInfo(sessionInfo) { localStorage.setItem(BehavioralAuthSDK.storageKey, JSON.stringify(sessionInfo)); } async startAllAvailableCollectors() { const sessionInfo = this.getSessionInfo(); const enabledCollectors = new Set(); for (const collector of sessionInfo.collectors) { enabledCollectors.add(collector.name); } for (const collector of this.collectors) { if (enabledCollectors.has(collector.name)) { this.log(`Using collector: ${collector.name}`); await this.tryStartCollectorIfAvailable(collector); } } // Flush immediately to have a session version near the start of the session. this.flushAll(); } async tryStartCollectorIfAvailable(collector) { try { if (await collector.isAvailable() && !collector.isCollecting) { await collector.start?.(); } } catch (e) { console.error(e); } } log(...args) { if (this.configuration.verbose) { console.debug(`[quadible] ${args.shift()}`, ...args); } } } exports.default = BehavioralAuthSDK; //# sourceMappingURL=BehavioralAuthSDK.js.map (function(){ if(typeof document === 'undefined') return; const styleEl = document.createElement('style'); styleEl.innerHTML = `.qdblsdk-debug-ui{width:400px;height:auto;background:#333;border-radius:4px;color:#fff;z-index:999999999999999;filter:none !important;position:fixed;top:0;right:0;font-family:monospace;padding:5px 10px;margin:10px}.qdblsdk-debug-ui .field-row span{color:aqua}`; document.body.append(styleEl); })();