@quadible/web-sdk
Version:
The web sdk for Quadible's behavioral authentication service.
456 lines • 18.8 kB
JavaScript
;
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);
})();