abee-adi-core
Version:
Behavioral tracking and device fingerprinting library
1,306 lines (1,283 loc) • 128 kB
JavaScript
class ADIEventImplV1 {
constructor(code, immediate, ts, payload) {
this.v = "1";
this.c = code;
this.i = immediate;
this.t = ts;
this.p = payload;
this.u = {};
}
}
/* istanbul ignore file */
class ADIKeyPressed extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIKeyPressed.CODE, immediate, Date.now(), payload);
}
}
ADIKeyPressed.CODE = "A_KP";
class ADITouch extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADITouch.CODE, immediate, Date.now(), payload);
}
}
ADITouch.CODE = "A_T";
class ADIView extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIView.CODE, immediate, Date.now(), payload);
}
}
ADIView.CODE = "A_V";
class ADIElementView extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIElementView.CODE, immediate, Date.now(), payload);
}
}
ADIElementView.CODE = "A_EV";
class ADIDeviceInfo extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIDeviceInfo.CODE, immediate, Date.now(), payload);
}
}
ADIDeviceInfo.CODE = "A_DI";
class ADIChannelLogin extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIChannelLogin.CODE, immediate, Date.now(), payload);
}
}
ADIChannelLogin.CODE = "A_CL";
class ADIChannelLogOut extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIChannelLogOut.CODE, immediate, Date.now(), payload);
}
}
ADIChannelLogOut.CODE = "A_CLO";
class ADIChannelLoginFailed extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIChannelLoginFailed.CODE, immediate, Date.now(), payload);
}
}
ADIChannelLoginFailed.CODE = "A_CLF";
class ADIIntegration {
constructor(applicationKey, client, endpoint) {
this.endpoint = endpoint;
this.client = client;
this.applicationKey = applicationKey;
}
async sendEvent(event) {
// this.client.postForJSON(this.endpoints.adiURL, )
await this.client.postForText(this.endpoint.adiBaseURL, event, undefined, this.applicationKey);
console.log(`Sent event ${JSON.stringify(event)}`, event);
return;
}
async sendEvents(events) {
await this.client.postForText(this.endpoint.adiBaseURL, events, undefined, this.applicationKey);
console.log(`Sent events ${JSON.stringify(events)}`, events);
return;
}
}
/* istanbul ignore file */
class ApiClientBrowserFetch {
constructor() {
}
async postForText(url, json, token, applicationKey) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (token)
headers["Authorization"] = `Bearer ${token}`;
if (applicationKey)
headers["x-api-key"] = `${applicationKey}`;
const response = await fetch(`${url}`, {
method: 'POST',
headers: headers,
body: JSON.stringify(json)
});
if (!response.ok) {
throw new Error(response.statusText);
}
const result = await response.text();
return result;
}
async postForJSON(url, json, token, applicationKey) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (token)
headers["Authorization"] = `Bearer ${token}`;
if (applicationKey)
headers["x-api-key"] = `${applicationKey}`;
const response = await fetch(`${url}`, {
method: 'POST',
headers: headers,
body: JSON.stringify(json)
});
if (!response.ok) {
throw new Error(response.statusText);
}
const result = await response.json();
return result;
}
async getForText(url, token, applicationKey) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (token)
headers["Authorization"] = `Bearer ${token}`;
if (applicationKey)
headers["x-api-key"] = `${applicationKey}`;
const response = await fetch(`${url}`, {
method: 'GET',
headers: headers
});
if (!response.ok) {
throw new Error(response.statusText);
}
const result = await response.text();
return result;
}
async getForJSON(url, token, applicationKey) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (token)
headers["Authorization"] = `Bearer ${token}`;
if (applicationKey)
headers["x-api-key"] = `${applicationKey}`;
const response = await fetch(`${url}`, {
method: 'GET',
headers: headers
});
if (!response.ok) {
throw new Error(response.statusText);
}
const result = await response.json();
return result;
}
async deleteForJSON(url, json, token, applicationKey) {
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
if (token)
headers["Authorization"] = `Bearer ${token}`;
if (applicationKey)
headers["x-api-key"] = `${applicationKey}`;
const response = await fetch(`${url}`, {
method: 'DELETE',
headers: headers,
body: JSON.stringify(json)
});
if (!response.ok) {
throw new Error(response.statusText);
}
const result = await response.json();
return result;
}
}
/* istanbul ignore file */
const STORAGE_PROVIDER_DEFAULTS = {
JOURNEY_ID_NAME: "adiJourneyId",
DEVICE_ID_NAME: "adiDeviceId",
PERSONA_ID_NAME: "adiPersonaId",
COOKIE_EXPIRY_HRS: 2,
};
class DefaultProvider {
storeObject(_name, _obj) {
throw new Error("Must be implemented");
}
restoreObject(_name) {
throw new Error("Must be implemented");
}
storeString(_name, _obj) {
throw new Error("Must be implemented");
}
restoreString(_name) {
throw new Error("Must be implemented");
}
deleteItem(_name) {
throw new Error("Must be implemented");
}
getToken() {
throw new Error("Must be implemented");
}
setToken(_token) {
throw new Error("Must be implemented");
}
deleteToken() {
throw new Error("Must be implemented");
}
}
class LocalStorageProvider extends DefaultProvider {
storeObject(name, obj) {
if (typeof obj === 'object' && obj !== null) {
localStorage.setItem(name, JSON.stringify(obj));
}
}
restoreObject(name) {
const item = localStorage.getItem(name);
if (item != null)
return JSON.parse(item);
return;
}
storeString(name, obj) {
localStorage.setItem(name, obj);
}
restoreString(name) {
return localStorage.getItem(name) || undefined;
}
deleteItem(name) {
localStorage.removeItem(name);
}
}
class ADICore {
constructor() {
const client = new ApiClientBrowserFetch();
this._apiClient = client;
this._consentGranted = false;
// initialize patient
this.patient = {
id: {
deviceId: undefined,
journeyId: undefined,
personaId: undefined
}
};
this._delayedEvents = []; // array to store events for delayed/bulk sending
}
async boot(options) {
// async boot(applicationKey: string, gdprProvider: GDPRConsentProvider, personaProvider: PersonaProvider, fingerprintProvider:FingerprintProvider, storageProvider: StorageProvider):Promise<Patient>{
this._applicationKey = options.applicationKey;
this._gdprProvider = options.gdprProvider;
this._personaProvider = options.personaProvider;
this._fingerprintProvider = options.fingerprintProvider;
this._storageProvider = options.storageProvider;
this._apiClient = options.apiClient || this._apiClient;
this._endpoint = options.endpoint;
this._api = new ADIIntegration(this._applicationKey, this._apiClient, this._endpoint);
// make sure all obligatory options are set
if (!this._applicationKey)
throw new Error(`applicationKey is required`);
if (!this._fingerprintProvider)
throw new Error(`fingerprintProvider is required`);
if (!this._storageProvider)
throw new Error(`storageProvider is required`);
// ask gdpr provider if we can track user
await this._updateUserConsent();
// now let's try to update persona information
await this._updatePersona();
// we retrieve as much as possible data locally
await this._bootPatient();
// console.log(`Initialized with patient ${JSON.stringify(this.patient)}`);
return this.patient;
}
/**
* Will try to boot/restore as much as possible patient data locally (without exchanging data with any external provider).
* For journeyId when none is found in storage a new random one is created.
*/
async _bootPatient() {
// retrieve journeyId (if there is any)
this.patient.id.journeyId = this._storageProvider.restoreString(STORAGE_PROVIDER_DEFAULTS.JOURNEY_ID_NAME);
// when no journeyId is stored generate new
if (!this.patient.id.journeyId) {
this.patient.id.journeyId = `${Math.random().toString(36).substring(2, 12)}`;
this._storageProvider.storeString(STORAGE_PROVIDER_DEFAULTS.JOURNEY_ID_NAME, this.patient.id.journeyId);
}
// calculate device id
const deviceInfo = await this._fingerprintProvider(this._applicationKey, this.patient, this._apiClient);
this.patient.id.deviceId = deviceInfo.deviceId;
//put delayed event with device characteristics for later on (we can send this event only when patient consent is granted)
this._delayedEvents.push(new ADIDeviceInfo({ d: deviceInfo }, false));
// retrieve persona id
this.patient.id.personaId = this._storageProvider.restoreString(STORAGE_PROVIDER_DEFAULTS.PERSONA_ID_NAME);
}
/* istanbul ignore next */
/**
* Updates patient personaId from external personaProvider
*/
async _updatePersona() {
var _a;
if (this._consentGranted && this._personaProvider) {
// if there is a non empty response from personaProvider update personaId and store it in the storage for
// future use
const personaId = await this._personaProvider(this._applicationKey, this.patient, this._apiClient);
if (personaId) {
this.patient.id.personaId = personaId;
(_a = this._storageProvider) === null || _a === void 0 ? void 0 : _a.storeString(STORAGE_PROVIDER_DEFAULTS.PERSONA_ID_NAME, personaId);
}
}
}
/**
* Sets persona from external provider via direct call
* @param personaId
*/
async setPersona(personaId) {
this.patient.id.personaId = personaId;
// now as we received persona id we should check if we are still eligible to track user behaviour
await this._updateUserConsent();
// when consent granted do store persona for future uses
if (this._consentGranted)
this._storageProvider.storeString(STORAGE_PROVIDER_DEFAULTS.PERSONA_ID_NAME, personaId);
else
// no consent so abort and clean data
this._clean();
}
/**
* Revokes GDPR consent and cleans all data that was recorded
*/
revokeGDPRConsent() {
// we switch off user behaviour tracking
this._consentGranted = false;
// and we also clean data that is/was eventually recorded
this._clean();
}
/**
* method to capture user behaviour
* @param event
* @returns
*/
async capture(event) {
// do nothing when we are not allowed to track user behaviour
if (!this._consentGranted) {
return;
}
this._sendEvent(event);
}
setApiClient(apiClient) {
this._apiClient = apiClient;
}
;
/**
* Will ask external GDPR Provider for green light to track user behaviour. When no GDPR Provider is provided
* it is assumed that the consent is granted and eventual revoke of the consent shall be done by the revokeGDPRConsent() method.
*/
async _updateUserConsent() {
if (!this._gdprProvider) {
this._consentGranted = true;
return;
}
this._consentGranted = await this._gdprProvider(this._applicationKey, this.patient, this._apiClient);
}
_clean() {
this._storageProvider.deleteItem(STORAGE_PROVIDER_DEFAULTS.JOURNEY_ID_NAME);
this._storageProvider.deleteItem(STORAGE_PROVIDER_DEFAULTS.PERSONA_ID_NAME);
this._storageProvider.deleteItem(STORAGE_PROVIDER_DEFAULTS.DEVICE_ID_NAME);
// also clean patient data
this.patient.id.personaId = undefined;
this.patient.id.deviceId = undefined;
this.patient.id.journeyId = undefined;
}
async _sendEvent(event) {
// in case there are any delayed evends send them together with this event
const events = JSON.parse(JSON.stringify(this._delayedEvents));
// clear delayed messages
this._delayedEvents.length = 0;
// add current event
events.push(event);
// add patient data to events
events.forEach((event) => {
event.u = this.patient;
});
// send all to ADI
await this._api.sendEvents(events);
}
}
/* istanbul ignore file */
class ADIFinTransfer extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIFinTransfer.CODE, immediate, Date.now(), payload);
}
}
ADIFinTransfer.CODE = "A_FT";
class ADIFinTransferCreditCard extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIFinTransferCreditCard.CODE, immediate, Date.now(), payload);
}
}
ADIFinTransferCreditCard.CODE = "A_FTCC";
class ADIFinTransferSEPA extends ADIEventImplV1 {
constructor(payload, immediate) {
super(ADIFinTransferSEPA.CODE, immediate, Date.now(), payload);
}
}
ADIFinTransferSEPA.CODE = "A_FTSE";
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
var __assign = function() {
__assign = Object.assign || function __assign(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
function __generator(thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (g && (g = 0, op[0] && (_ = 0)), _) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
}
function __spreadArray(to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
/**
* FingerprintJS v3.4.2 - Copyright (c) FingerprintJS, Inc, 2023 (https://fingerprint.com)
* Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license.
*
* This software contains code from open-source projects:
* MurmurHash3 by Karan Lyons (https://github.com/karanlyons/murmurHash3.js)
*/
var version = "3.4.2";
function wait(durationMs, resolveWith) {
return new Promise(function (resolve) { return setTimeout(resolve, durationMs, resolveWith); });
}
function requestIdleCallbackIfAvailable(fallbackTimeout, deadlineTimeout) {
if (deadlineTimeout === void 0) { deadlineTimeout = Infinity; }
var requestIdleCallback = window.requestIdleCallback;
if (requestIdleCallback) {
// The function `requestIdleCallback` loses the binding to `window` here.
// `globalThis` isn't always equal `window` (see https://github.com/fingerprintjs/fingerprintjs/issues/683).
// Therefore, an error can occur. `call(window,` prevents the error.
return new Promise(function (resolve) { return requestIdleCallback.call(window, function () { return resolve(); }, { timeout: deadlineTimeout }); });
}
else {
return wait(Math.min(fallbackTimeout, deadlineTimeout));
}
}
function isPromise(value) {
return !!value && typeof value.then === 'function';
}
/**
* Calls a maybe asynchronous function without creating microtasks when the function is synchronous.
* Catches errors in both cases.
*
* If just you run a code like this:
* ```
* console.time('Action duration')
* await action()
* console.timeEnd('Action duration')
* ```
* The synchronous function time can be measured incorrectly because another microtask may run before the `await`
* returns the control back to the code.
*/
function awaitIfAsync(action, callback) {
try {
var returnedValue = action();
if (isPromise(returnedValue)) {
returnedValue.then(function (result) { return callback(true, result); }, function (error) { return callback(false, error); });
}
else {
callback(true, returnedValue);
}
}
catch (error) {
callback(false, error);
}
}
/**
* If you run many synchronous tasks without using this function, the JS main loop will be busy and asynchronous tasks
* (e.g. completing a network request, rendering the page) won't be able to happen.
* This function allows running many synchronous tasks such way that asynchronous tasks can run too in background.
*/
function mapWithBreaks(items, callback, loopReleaseInterval) {
if (loopReleaseInterval === void 0) { loopReleaseInterval = 16; }
return __awaiter(this, void 0, void 0, function () {
var results, lastLoopReleaseTime, i, now;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
results = Array(items.length);
lastLoopReleaseTime = Date.now();
i = 0;
_a.label = 1;
case 1:
if (!(i < items.length)) return [3 /*break*/, 4];
results[i] = callback(items[i], i);
now = Date.now();
if (!(now >= lastLoopReleaseTime + loopReleaseInterval)) return [3 /*break*/, 3];
lastLoopReleaseTime = now;
// Allows asynchronous actions and microtasks to happen
return [4 /*yield*/, wait(0)];
case 2:
// Allows asynchronous actions and microtasks to happen
_a.sent();
_a.label = 3;
case 3:
++i;
return [3 /*break*/, 1];
case 4: return [2 /*return*/, results];
}
});
});
}
/**
* Makes the given promise never emit an unhandled promise rejection console warning.
* The promise will still pass errors to the next promises.
*
* Otherwise, promise emits a console warning unless it has a `catch` listener.
*/
function suppressUnhandledRejectionWarning(promise) {
promise.then(undefined, function () { return undefined; });
}
/*
* Taken from https://github.com/karanlyons/murmurHash3.js/blob/a33d0723127e2e5415056c455f8aed2451ace208/murmurHash3.js
*/
//
// Given two 64bit ints (as an array of two 32bit ints) returns the two
// added together as a 64bit int (as an array of two 32bit ints).
//
function x64Add(m, n) {
m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff];
n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff];
var o = [0, 0, 0, 0];
o[3] += m[3] + n[3];
o[2] += o[3] >>> 16;
o[3] &= 0xffff;
o[2] += m[2] + n[2];
o[1] += o[2] >>> 16;
o[2] &= 0xffff;
o[1] += m[1] + n[1];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[0] += m[0] + n[0];
o[0] &= 0xffff;
return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]];
}
//
// Given two 64bit ints (as an array of two 32bit ints) returns the two
// multiplied together as a 64bit int (as an array of two 32bit ints).
//
function x64Multiply(m, n) {
m = [m[0] >>> 16, m[0] & 0xffff, m[1] >>> 16, m[1] & 0xffff];
n = [n[0] >>> 16, n[0] & 0xffff, n[1] >>> 16, n[1] & 0xffff];
var o = [0, 0, 0, 0];
o[3] += m[3] * n[3];
o[2] += o[3] >>> 16;
o[3] &= 0xffff;
o[2] += m[2] * n[3];
o[1] += o[2] >>> 16;
o[2] &= 0xffff;
o[2] += m[3] * n[2];
o[1] += o[2] >>> 16;
o[2] &= 0xffff;
o[1] += m[1] * n[3];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[1] += m[2] * n[2];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[1] += m[3] * n[1];
o[0] += o[1] >>> 16;
o[1] &= 0xffff;
o[0] += m[0] * n[3] + m[1] * n[2] + m[2] * n[1] + m[3] * n[0];
o[0] &= 0xffff;
return [(o[0] << 16) | o[1], (o[2] << 16) | o[3]];
}
//
// Given a 64bit int (as an array of two 32bit ints) and an int
// representing a number of bit positions, returns the 64bit int (as an
// array of two 32bit ints) rotated left by that number of positions.
//
function x64Rotl(m, n) {
n %= 64;
if (n === 32) {
return [m[1], m[0]];
}
else if (n < 32) {
return [(m[0] << n) | (m[1] >>> (32 - n)), (m[1] << n) | (m[0] >>> (32 - n))];
}
else {
n -= 32;
return [(m[1] << n) | (m[0] >>> (32 - n)), (m[0] << n) | (m[1] >>> (32 - n))];
}
}
//
// Given a 64bit int (as an array of two 32bit ints) and an int
// representing a number of bit positions, returns the 64bit int (as an
// array of two 32bit ints) shifted left by that number of positions.
//
function x64LeftShift(m, n) {
n %= 64;
if (n === 0) {
return m;
}
else if (n < 32) {
return [(m[0] << n) | (m[1] >>> (32 - n)), m[1] << n];
}
else {
return [m[1] << (n - 32), 0];
}
}
//
// Given two 64bit ints (as an array of two 32bit ints) returns the two
// xored together as a 64bit int (as an array of two 32bit ints).
//
function x64Xor(m, n) {
return [m[0] ^ n[0], m[1] ^ n[1]];
}
//
// Given a block, returns murmurHash3's final x64 mix of that block.
// (`[0, h[0] >>> 1]` is a 33 bit unsigned right shift. This is the
// only place where we need to right shift 64bit ints.)
//
function x64Fmix(h) {
h = x64Xor(h, [0, h[0] >>> 1]);
h = x64Multiply(h, [0xff51afd7, 0xed558ccd]);
h = x64Xor(h, [0, h[0] >>> 1]);
h = x64Multiply(h, [0xc4ceb9fe, 0x1a85ec53]);
h = x64Xor(h, [0, h[0] >>> 1]);
return h;
}
//
// Given a string and an optional seed as an int, returns a 128 bit
// hash using the x64 flavor of MurmurHash3, as an unsigned hex.
//
function x64hash128(key, seed) {
key = key || '';
seed = seed || 0;
var remainder = key.length % 16;
var bytes = key.length - remainder;
var h1 = [0, seed];
var h2 = [0, seed];
var k1 = [0, 0];
var k2 = [0, 0];
var c1 = [0x87c37b91, 0x114253d5];
var c2 = [0x4cf5ad43, 0x2745937f];
var i;
for (i = 0; i < bytes; i = i + 16) {
k1 = [
(key.charCodeAt(i + 4) & 0xff) |
((key.charCodeAt(i + 5) & 0xff) << 8) |
((key.charCodeAt(i + 6) & 0xff) << 16) |
((key.charCodeAt(i + 7) & 0xff) << 24),
(key.charCodeAt(i) & 0xff) |
((key.charCodeAt(i + 1) & 0xff) << 8) |
((key.charCodeAt(i + 2) & 0xff) << 16) |
((key.charCodeAt(i + 3) & 0xff) << 24),
];
k2 = [
(key.charCodeAt(i + 12) & 0xff) |
((key.charCodeAt(i + 13) & 0xff) << 8) |
((key.charCodeAt(i + 14) & 0xff) << 16) |
((key.charCodeAt(i + 15) & 0xff) << 24),
(key.charCodeAt(i + 8) & 0xff) |
((key.charCodeAt(i + 9) & 0xff) << 8) |
((key.charCodeAt(i + 10) & 0xff) << 16) |
((key.charCodeAt(i + 11) & 0xff) << 24),
];
k1 = x64Multiply(k1, c1);
k1 = x64Rotl(k1, 31);
k1 = x64Multiply(k1, c2);
h1 = x64Xor(h1, k1);
h1 = x64Rotl(h1, 27);
h1 = x64Add(h1, h2);
h1 = x64Add(x64Multiply(h1, [0, 5]), [0, 0x52dce729]);
k2 = x64Multiply(k2, c2);
k2 = x64Rotl(k2, 33);
k2 = x64Multiply(k2, c1);
h2 = x64Xor(h2, k2);
h2 = x64Rotl(h2, 31);
h2 = x64Add(h2, h1);
h2 = x64Add(x64Multiply(h2, [0, 5]), [0, 0x38495ab5]);
}
k1 = [0, 0];
k2 = [0, 0];
switch (remainder) {
case 15:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 14)], 48));
// fallthrough
case 14:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 13)], 40));
// fallthrough
case 13:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 12)], 32));
// fallthrough
case 12:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 11)], 24));
// fallthrough
case 11:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 10)], 16));
// fallthrough
case 10:
k2 = x64Xor(k2, x64LeftShift([0, key.charCodeAt(i + 9)], 8));
// fallthrough
case 9:
k2 = x64Xor(k2, [0, key.charCodeAt(i + 8)]);
k2 = x64Multiply(k2, c2);
k2 = x64Rotl(k2, 33);
k2 = x64Multiply(k2, c1);
h2 = x64Xor(h2, k2);
// fallthrough
case 8:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 7)], 56));
// fallthrough
case 7:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 6)], 48));
// fallthrough
case 6:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 5)], 40));
// fallthrough
case 5:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 4)], 32));
// fallthrough
case 4:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 3)], 24));
// fallthrough
case 3:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 2)], 16));
// fallthrough
case 2:
k1 = x64Xor(k1, x64LeftShift([0, key.charCodeAt(i + 1)], 8));
// fallthrough
case 1:
k1 = x64Xor(k1, [0, key.charCodeAt(i)]);
k1 = x64Multiply(k1, c1);
k1 = x64Rotl(k1, 31);
k1 = x64Multiply(k1, c2);
h1 = x64Xor(h1, k1);
// fallthrough
}
h1 = x64Xor(h1, [0, key.length]);
h2 = x64Xor(h2, [0, key.length]);
h1 = x64Add(h1, h2);
h2 = x64Add(h2, h1);
h1 = x64Fmix(h1);
h2 = x64Fmix(h2);
h1 = x64Add(h1, h2);
h2 = x64Add(h2, h1);
return (('00000000' + (h1[0] >>> 0).toString(16)).slice(-8) +
('00000000' + (h1[1] >>> 0).toString(16)).slice(-8) +
('00000000' + (h2[0] >>> 0).toString(16)).slice(-8) +
('00000000' + (h2[1] >>> 0).toString(16)).slice(-8));
}
/**
* Converts an error object to a plain object that can be used with `JSON.stringify`.
* If you just run `JSON.stringify(error)`, you'll get `'{}'`.
*/
function errorToObject(error) {
var _a;
return __assign({ name: error.name, message: error.message, stack: (_a = error.stack) === null || _a === void 0 ? void 0 : _a.split('\n') }, error);
}
/*
* This file contains functions to work with pure data only (no browser features, DOM, side effects, etc).
*/
/**
* Does the same as Array.prototype.includes but has better typing
*/
function includes(haystack, needle) {
for (var i = 0, l = haystack.length; i < l; ++i) {
if (haystack[i] === needle) {
return true;
}
}
return false;
}
/**
* Like `!includes()` but with proper typing
*/
function excludes(haystack, needle) {
return !includes(haystack, needle);
}
/**
* Be careful, NaN can return
*/
function toInt(value) {
return parseInt(value);
}
/**
* Be careful, NaN can return
*/
function toFloat(value) {
return parseFloat(value);
}
function replaceNaN(value, replacement) {
return typeof value === 'number' && isNaN(value) ? replacement : value;
}
function countTruthy(values) {
return values.reduce(function (sum, value) { return sum + (value ? 1 : 0); }, 0);
}
function round(value, base) {
if (base === void 0) { base = 1; }
if (Math.abs(base) >= 1) {
return Math.round(value / base) * base;
}
else {
// Sometimes when a number is multiplied by a small number, precision is lost,
// for example 1234 * 0.0001 === 0.12340000000000001, and it's more precise divide: 1234 / (1 / 0.0001) === 0.1234.
var counterBase = 1 / base;
return Math.round(value * counterBase) / counterBase;
}
}
/**
* Parses a CSS selector into tag name with HTML attributes.
* Only single element selector are supported (without operators like space, +, >, etc).
*
* Multiple values can be returned for each attribute. You decide how to handle them.
*/
function parseSimpleCssSelector(selector) {
var _a, _b;
var errorMessage = "Unexpected syntax '".concat(selector, "'");
var tagMatch = /^\s*([a-z-]*)(.*)$/i.exec(selector);
var tag = tagMatch[1] || undefined;
var attributes = {};
var partsRegex = /([.:#][\w-]+|\[.+?\])/gi;
var addAttribute = function (name, value) {
attributes[name] = attributes[name] || [];
attributes[name].push(value);
};
for (;;) {
var match = partsRegex.exec(tagMatch[2]);
if (!match) {
break;
}
var part = match[0];
switch (part[0]) {
case '.':
addAttribute('class', part.slice(1));
break;
case '#':
addAttribute('id', part.slice(1));
break;
case '[': {
var attributeMatch = /^\[([\w-]+)([~|^$*]?=("(.*?)"|([\w-]+)))?(\s+[is])?\]$/.exec(part);
if (attributeMatch) {
addAttribute(attributeMatch[1], (_b = (_a = attributeMatch[4]) !== null && _a !== void 0 ? _a : attributeMatch[5]) !== null && _b !== void 0 ? _b : '');
}
else {
throw new Error(errorMessage);
}
break;
}
default:
throw new Error(errorMessage);
}
}
return [tag, attributes];
}
function ensureErrorWithMessage(error) {
return error && typeof error === 'object' && 'message' in error ? error : { message: error };
}
function isFinalResultLoaded(loadResult) {
return typeof loadResult !== 'function';
}
/**
* Loads the given entropy source. Returns a function that gets an entropy component from the source.
*
* The result is returned synchronously to prevent `loadSources` from
* waiting for one source to load before getting the components from the other sources.
*/
function loadSource(source, sourceOptions) {
var sourceLoadPromise = new Promise(function (resolveLoad) {
var loadStartTime = Date.now();
// `awaitIfAsync` is used instead of just `await` in order to measure the duration of synchronous sources
// correctly (other microtasks won't affect the duration).
awaitIfAsync(source.bind(null, sourceOptions), function () {
var loadArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
loadArgs[_i] = arguments[_i];
}
var loadDuration = Date.now() - loadStartTime;
// Source loading failed
if (!loadArgs[0]) {
return resolveLoad(function () { return ({ error: ensureErrorWithMessage(loadArgs[1]), duration: loadDuration }); });
}
var loadResult = loadArgs[1];
// Source loaded with the final result
if (isFinalResultLoaded(loadResult)) {
return resolveLoad(function () { return ({ value: loadResult, duration: loadDuration }); });
}
// Source loaded with "get" stage
resolveLoad(function () {
return new Promise(function (resolveGet) {
var getStartTime = Date.now();
awaitIfAsync(loadResult, function () {
var getArgs = [];
for (var _i = 0; _i < arguments.length; _i++) {
getArgs[_i] = arguments[_i];
}
var duration = loadDuration + Date.now() - getStartTime;
// Source getting failed
if (!getArgs[0]) {
return resolveGet({ error: ensureErrorWithMessage(getArgs[1]), duration: duration });
}
// Source getting succeeded
resolveGet({ value: getArgs[1], duration: duration });
});
});
});
});
});
suppressUnhandledRejectionWarning(sourceLoadPromise);
return function getComponent() {
return sourceLoadPromise.then(function (finalizeSource) { return finalizeSource(); });
};
}
/**
* Loads the given entropy sources. Returns a function that collects the entropy components.
*
* The result is returned synchronously in order to allow start getting the components
* before the sources are loaded completely.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function loadSources(sources, sourceOptions, excludeSources) {
var includedSources = Object.keys(sources).filter(function (sourceKey) { return excludes(excludeSources, sourceKey); });
// Using `mapWithBreaks` allows asynchronous sources to complete between synchronous sources
// and measure the duration correctly
var sourceGettersPromise = mapWithBreaks(includedSources, function (sourceKey) {
return loadSource(sources[sourceKey], sourceOptions);
});
suppressUnhandledRejectionWarning(sourceGettersPromise);
return function getComponents() {
return __awaiter(this, void 0, void 0, function () {
var sourceGetters, componentPromises, componentArray, components, index;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, sourceGettersPromise];
case 1:
sourceGetters = _a.sent();
return [4 /*yield*/, mapWithBreaks(sourceGetters, function (sourceGetter) {
var componentPromise = sourceGetter();
suppressUnhandledRejectionWarning(componentPromise);
return componentPromise;
})];
case 2:
componentPromises = _a.sent();
return [4 /*yield*/, Promise.all(componentPromises)
// Keeping the component keys order the same as the source keys order
];
case 3:
componentArray = _a.sent();
components = {};
for (index = 0; index < includedSources.length; ++index) {
components[includedSources[index]] = componentArray[index];
}
return [2 /*return*/, components];
}
});
});
};
}
/*
* Functions to help with features that vary through browsers
*/
/**
* Checks whether the browser is based on Trident (the Internet Explorer engine) without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isTrident() {
var w = window;
var n = navigator;
// The properties are checked to be in IE 10, IE 11 and not to be in other browsers in October 2020
return (countTruthy([
'MSCSSMatrix' in w,
'msSetImmediate' in w,
'msIndexedDB' in w,
'msMaxTouchPoints' in n,
'msPointerEnabled' in n,
]) >= 4);
}
/**
* Checks whether the browser is based on EdgeHTML (the pre-Chromium Edge engine) without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isEdgeHTML() {
// Based on research in October 2020
var w = window;
var n = navigator;
return (countTruthy(['msWriteProfilerMark' in w, 'MSStream' in w, 'msLaunchUri' in n, 'msSaveBlob' in n]) >= 3 &&
!isTrident());
}
/**
* Checks whether the browser is based on Chromium without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isChromium() {
// Based on research in October 2020. Tested to detect Chromium 42-86.
var w = window;
var n = navigator;
return (countTruthy([
'webkitPersistentStorage' in n,
'webkitTemporaryStorage' in n,
n.vendor.indexOf('Google') === 0,
'webkitResolveLocalFileSystemURL' in w,
'BatteryManager' in w,
'webkitMediaStream' in w,
'webkitSpeechGrammar' in w,
]) >= 5);
}
/**
* Checks whether the browser is based on mobile or desktop Safari without using user-agent.
* All iOS browsers use WebKit (the Safari engine).
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isWebKit() {
// Based on research in September 2020
var w = window;
var n = navigator;
return (countTruthy([
'ApplePayError' in w,
'CSSPrimitiveValue' in w,
'Counter' in w,
n.vendor.indexOf('Apple') === 0,
'getStorageUpdates' in n,
'WebKitMediaKeys' in w,
]) >= 4);
}
/**
* Checks whether the WebKit browser is a desktop Safari.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isDesktopSafari() {
var w = window;
return (countTruthy([
'safari' in w,
!('DeviceMotionEvent' in w),
!('ongestureend' in w),
!('standalone' in navigator),
]) >= 3);
}
/**
* Checks whether the browser is based on Gecko (Firefox engine) without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isGecko() {
var _a, _b;
var w = window;
// Based on research in September 2020
return (countTruthy([
'buildID' in navigator,
'MozAppearance' in ((_b = (_a = document.documentElement) === null || _a === void 0 ? void 0 : _a.style) !== null && _b !== void 0 ? _b : {}),
'onmozfullscreenchange' in w,
'mozInnerScreenX' in w,
'CSSMozDocumentRule' in w,
'CanvasCaptureMediaStream' in w,
]) >= 4);
}
/**
* Checks whether the browser is based on Chromium version ≥86 without using user-agent.
* It doesn't check that the browser is based on Chromium, there is a separate function for this.
*/
function isChromium86OrNewer() {
// Checked in Chrome 85 vs Chrome 86 both on desktop and Android
var w = window;
return (countTruthy([
!('MediaSettingsRange' in w),
'RTCEncodedAudioFrame' in w,
'' + w.Intl === '[object Intl]',
'' + w.Reflect === '[object Reflect]',
]) >= 3);
}
/**
* Checks whether the browser is based on WebKit version ≥606 (Safari ≥12) without using user-agent.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* @link https://en.wikipedia.org/wiki/Safari_version_history#Release_history Safari-WebKit versions map
*/
function isWebKit606OrNewer() {
// Checked in Safari 9–14
var w = window;
return (countTruthy([
'DOMRectList' in w,
'RTCPeerConnectionIceEvent' in w,
'SVGGeometryElement' in w,
'ontransitioncancel' in w,
]) >= 3);
}
/**
* Checks whether the device is an iPad.
* It doesn't check that the engine is WebKit and that the WebKit isn't desktop.
*/
function isIPad() {
// Checked on:
// Safari on iPadOS (both mobile and desktop modes): 8, 11, 12, 13, 14
// Chrome on iPadOS (both mobile and desktop modes): 11, 12, 13, 14
// Safari on iOS (both mobile and desktop modes): 9, 10, 11, 12, 13, 14
// Chrome on iOS (both mobile and desktop modes): 9, 10, 11, 12, 13, 14
// Before iOS 13. Safari tampers the value in "request desktop site" mode since iOS 13.
if (navigator.platform === 'iPad') {
return true;
}
var s = screen;
var screenRatio = s.width / s.height;
return (countTruthy([
'MediaSource' in window,
!!Element.prototype.webkitRequestFullscreen,
// iPhone 4S that runs iOS 9 matches this. But it won't match the criteria above, so it won't be detected as iPad.
screenRatio > 0.65 && screenRatio < 1.53,
]) >= 2);
}
/**
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function getFullscreenElement() {
var d = document;
return d.fullscreenElement || d.msFullscreenElement || d.mozFullScreenElement || d.webkitFullscreenElement || null;
}
function exitFullscreen() {
var d = document;
// `call` is required because the function throws an error without a proper "this" context
return (d.exitFullscreen || d.msExitFullscreen || d.mozCancelFullScreen || d.webkitExitFullscreen).call(d);
}
/**
* Checks whether the device runs on Android without using user-agent.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isAndroid() {
var isItChromium = isChromium();
var isItGecko = isGecko();
// Only 2 browser engines are presented on Android.
// Actually, there is also Android 4.1 browser, but it's not worth detecting it at the moment.
if (!isItChromium && !isItGecko) {
return false;
}
var w = window;
// Chrome removes all words "Android" from `navigator` when desktop version is requested
// Firefox keeps "Android" in `navigator.appVersion` when desktop version is requested
return (countTruthy([
'onorientationchange' in w,
'orientation' in w,
isItChromium && !('SharedWorker' in w),
isItGecko && /android/i.test(navigator.appVersion),
]) >= 2);
}
/**
* A deep description: https://fingerprint.com/blog/audio-fingerprinting/
* Inspired by and based on https://github.com/cozylife/audio-fingerprint
*/
function getAudioFingerprint() {
var w = window;
var AudioContext = w.OfflineAudioContext || w.webkitOfflineAudioContext;
if (!AudioContext) {
return -2 /* SpecialFingerprint.NotSupported */;
}
// In some browsers, audio context always stays suspended unless the context is started in response to a user action
// (e.g. a click or a tap). It prevents audio fingerprint from being taken at an arbitrary moment of time.
// Such browsers are old and unpopular, so the audio fingerprinting is just skipped in them.
// See a similar case explanation at https://stackoverflow.com/questions/46363048/onaudioprocess-not-called-on-ios11#46534088
if (doesCurrentBrowserSuspendAudioContext()) {
return -1 /* SpecialFingerprint.KnownToSuspend */;
}
var hashFromIndex = 4500;
var hashToIndex = 5000;
var context = new AudioContext(1, hashToIndex, 44100);
var oscillator = context.createOscillator();
oscillator.type = 'triangle';
oscillator.frequency.value = 10000;
var compressor = context.createDynamicsCompressor();
compressor.threshold.value = -50;
compressor.knee.value = 40;
compressor.ratio.value = 12;
compressor.attack.value = 0;
compressor.release.value = 0.25;
oscillator.connect(compressor);
compressor.connect(context.destination);
oscillator.start(0);
var _a = startRenderingAudio(context), renderPromise = _a[0], finishRendering = _a[1];
var fingerprintPromise = renderPromise.then(function (buffer) { return getHash(buffer.getChannelData(0).subarray(hashFromIndex)); }, function (error) {
if (error.name === "timeout" /* InnerErrorName.Timeout */ || error.name === "suspended" /* InnerErrorName.Suspended */) {
return -3 /* SpecialFingerprint.Timeout */;
}
throw error;
});
// Suppresses the console error message in case when the fingerprint fails before requested
suppressUnhandledRejectionWarning(fingerprintPromise);
return function () {
finishRendering();
return fingerprintPromise;
};
}
/**
* Checks if the current browser is known to always suspend audio context
*/
function doesCurrentBrowserSuspendAudioContext() {
return isWebKit() && !isDesktopSafari() && !isWebKit606OrNewer();
}
/**
* Starts rendering the audio context.
* When the returned function is called, the render process starts finishing.
*/
function startRenderingAudio(context) {
var renderTryMaxCount = 3;
var renderRetryDelay = 500;
var runningMaxAwaitTime = 500;
var runningSufficientTime = 5000;
var finalize = function () { return undefined; };
var resultPromise = new Promise(function (resolve, reject) {
var isFinalized = false;
var renderTryCount = 0;
var startedRunningAt = 0;
context.oncomplete = function (event) { return resolve(event.renderedBuffer); };
var startRunningTimeout = function () {
setTimeout(function () { return reject(makeInnerError("timeout" /* InnerErrorName.Timeout */)); }, Math.min(runningMaxAwaitTime, start