@fingerprintjs/fingerprintjs
Version:
Browser fingerprinting library with the highest accuracy and stability
1,256 lines (1,246 loc) • 134 kB
JavaScript
/**
* FingerprintJS v4.2.1 - Copyright (c) FingerprintJS, Inc, 2024 (https://fingerprint.com)
*
* Licensed under Business Source License 1.1 https://mariadb.com/bsl11/
* Licensor: FingerprintJS, Inc.
* Licensed Work: FingerprintJS browser fingerprinting library
* Additional Use Grant: None
* Change Date: Four years from first release for the specific version.
* Change License: MIT, text at https://opensource.org/license/mit/ with the following copyright notice:
* Copyright 2015-present FingerprintJS, Inc.
*/
import { __awaiter, __generator, __assign, __spreadArray } from 'tslib';
var version = "4.2.1";
function wait(durationMs, resolveWith) {
return new Promise(function (resolve) { return setTimeout(resolve, durationMs, resolveWith); });
}
/**
* Allows asynchronous actions and microtasks to happen.
*/
function releaseEventLoop() {
return wait(0);
}
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; });
}
/*
* 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];
}
/**
* Converts a string to UTF8 bytes
*/
function getUTF8Bytes(input) {
// Benchmark: https://jsbench.me/b6klaaxgwq/1
// If you want to just count bytes, see solutions at https://jsbench.me/ehklab415e/1
var result = new Uint8Array(input.length);
for (var i = 0; i < input.length; i++) {
// `charCode` is faster than encoding, so we prefer that when it's possible
var charCode = input.charCodeAt(i);
// In case of non-ASCII symbols we use proper encoding
if (charCode > 127) {
return new TextEncoder().encode(input);
}
result[i] = charCode;
}
return result;
}
/*
* Based on https://github.com/karanlyons/murmurHash3.js/blob/a33d0723127e2e5415056c455f8aed2451ace208/murmurHash3.js
*/
/**
* Adds two 64-bit values (provided as tuples of 32-bit values)
* and updates (mutates) first value to write the result
*/
function x64Add(m, n) {
var m0 = m[0] >>> 16, m1 = m[0] & 0xffff, m2 = m[1] >>> 16, m3 = m[1] & 0xffff;
var n0 = n[0] >>> 16, n1 = n[0] & 0xffff, n2 = n[1] >>> 16, n3 = n[1] & 0xffff;
var o0 = 0, o1 = 0, o2 = 0, o3 = 0;
o3 += m3 + n3;
o2 += o3 >>> 16;
o3 &= 0xffff;
o2 += m2 + n2;
o1 += o2 >>> 16;
o2 &= 0xffff;
o1 += m1 + n1;
o0 += o1 >>> 16;
o1 &= 0xffff;
o0 += m0 + n0;
o0 &= 0xffff;
m[0] = (o0 << 16) | o1;
m[1] = (o2 << 16) | o3;
}
/**
* Multiplies two 64-bit values (provided as tuples of 32-bit values)
* and updates (mutates) first value to write the result
*/
function x64Multiply(m, n) {
var m0 = m[0] >>> 16, m1 = m[0] & 0xffff, m2 = m[1] >>> 16, m3 = m[1] & 0xffff;
var n0 = n[0] >>> 16, n1 = n[0] & 0xffff, n2 = n[1] >>> 16, n3 = n[1] & 0xffff;
var o0 = 0, o1 = 0, o2 = 0, o3 = 0;
o3 += m3 * n3;
o2 += o3 >>> 16;
o3 &= 0xffff;
o2 += m2 * n3;
o1 += o2 >>> 16;
o2 &= 0xffff;
o2 += m3 * n2;
o1 += o2 >>> 16;
o2 &= 0xffff;
o1 += m1 * n3;
o0 += o1 >>> 16;
o1 &= 0xffff;
o1 += m2 * n2;
o0 += o1 >>> 16;
o1 &= 0xffff;
o1 += m3 * n1;
o0 += o1 >>> 16;
o1 &= 0xffff;
o0 += m0 * n3 + m1 * n2 + m2 * n1 + m3 * n0;
o0 &= 0xffff;
m[0] = (o0 << 16) | o1;
m[1] = (o2 << 16) | o3;
}
/**
* Provides left rotation of the given int64 value (provided as tuple of two int32)
* by given number of bits. Result is written back to the value
*/
function x64Rotl(m, bits) {
var m0 = m[0];
bits %= 64;
if (bits === 32) {
m[0] = m[1];
m[1] = m0;
}
else if (bits < 32) {
m[0] = (m0 << bits) | (m[1] >>> (32 - bits));
m[1] = (m[1] << bits) | (m0 >>> (32 - bits));
}
else {
bits -= 32;
m[0] = (m[1] << bits) | (m0 >>> (32 - bits));
m[1] = (m0 << bits) | (m[1] >>> (32 - bits));
}
}
/**
* Provides a left shift of the given int32 value (provided as tuple of [0, int32])
* by given number of bits. Result is written back to the value
*/
function x64LeftShift(m, bits) {
bits %= 64;
if (bits === 0) {
return;
}
else if (bits < 32) {
m[0] = m[1] >>> (32 - bits);
m[1] = m[1] << bits;
}
else {
m[0] = m[1] << (bits - 32);
m[1] = 0;
}
}
/**
* Provides a XOR of the given int64 values(provided as tuple of two int32).
* Result is written back to the first value
*/
function x64Xor(m, n) {
m[0] ^= n[0];
m[1] ^= n[1];
}
var F1 = [0xff51afd7, 0xed558ccd];
var F2 = [0xc4ceb9fe, 0x1a85ec53];
/**
* Calculates murmurHash3's final x64 mix of that block and writes result back to the input value.
* (`[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) {
var shifted = [0, h[0] >>> 1];
x64Xor(h, shifted);
x64Multiply(h, F1);
shifted[1] = h[0] >>> 1;
x64Xor(h, shifted);
x64Multiply(h, F2);
shifted[1] = h[0] >>> 1;
x64Xor(h, shifted);
}
var C1 = [0x87c37b91, 0x114253d5];
var C2 = [0x4cf5ad43, 0x2745937f];
var M$1 = [0, 5];
var N1 = [0, 0x52dce729];
var N2 = [0, 0x38495ab5];
/**
* 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.
* All internal functions mutates passed value to achieve minimal memory allocations and GC load
*
* Benchmark https://jsbench.me/p4lkpaoabi/1
*/
function x64hash128(input, seed) {
var key = getUTF8Bytes(input);
seed = seed || 0;
var length = [0, key.length];
var remainder = length[1] % 16;
var bytes = length[1] - remainder;
var h1 = [0, seed];
var h2 = [0, seed];
var k1 = [0, 0];
var k2 = [0, 0];
var i;
for (i = 0; i < bytes; i = i + 16) {
k1[0] = key[i + 4] | (key[i + 5] << 8) | (key[i + 6] << 16) | (key[i + 7] << 24);
k1[1] = key[i] | (key[i + 1] << 8) | (key[i + 2] << 16) | (key[i + 3] << 24);
k2[0] = key[i + 12] | (key[i + 13] << 8) | (key[i + 14] << 16) | (key[i + 15] << 24);
k2[1] = key[i + 8] | (key[i + 9] << 8) | (key[i + 10] << 16) | (key[i + 11] << 24);
x64Multiply(k1, C1);
x64Rotl(k1, 31);
x64Multiply(k1, C2);
x64Xor(h1, k1);
x64Rotl(h1, 27);
x64Add(h1, h2);
x64Multiply(h1, M$1);
x64Add(h1, N1);
x64Multiply(k2, C2);
x64Rotl(k2, 33);
x64Multiply(k2, C1);
x64Xor(h2, k2);
x64Rotl(h2, 31);
x64Add(h2, h1);
x64Multiply(h2, M$1);
x64Add(h2, N2);
}
k1[0] = 0;
k1[1] = 0;
k2[0] = 0;
k2[1] = 0;
var val = [0, 0];
switch (remainder) {
case 15:
val[1] = key[i + 14];
x64LeftShift(val, 48);
x64Xor(k2, val);
// fallthrough
case 14:
val[1] = key[i + 13];
x64LeftShift(val, 40);
x64Xor(k2, val);
// fallthrough
case 13:
val[1] = key[i + 12];
x64LeftShift(val, 32);
x64Xor(k2, val);
// fallthrough
case 12:
val[1] = key[i + 11];
x64LeftShift(val, 24);
x64Xor(k2, val);
// fallthrough
case 11:
val[1] = key[i + 10];
x64LeftShift(val, 16);
x64Xor(k2, val);
// fallthrough
case 10:
val[1] = key[i + 9];
x64LeftShift(val, 8);
x64Xor(k2, val);
// fallthrough
case 9:
val[1] = key[i + 8];
x64Xor(k2, val);
x64Multiply(k2, C2);
x64Rotl(k2, 33);
x64Multiply(k2, C1);
x64Xor(h2, k2);
// fallthrough
case 8:
val[1] = key[i + 7];
x64LeftShift(val, 56);
x64Xor(k1, val);
// fallthrough
case 7:
val[1] = key[i + 6];
x64LeftShift(val, 48);
x64Xor(k1, val);
// fallthrough
case 6:
val[1] = key[i + 5];
x64LeftShift(val, 40);
x64Xor(k1, val);
// fallthrough
case 5:
val[1] = key[i + 4];
x64LeftShift(val, 32);
x64Xor(k1, val);
// fallthrough
case 4:
val[1] = key[i + 3];
x64LeftShift(val, 24);
x64Xor(k1, val);
// fallthrough
case 3:
val[1] = key[i + 2];
x64LeftShift(val, 16);
x64Xor(k1, val);
// fallthrough
case 2:
val[1] = key[i + 1];
x64LeftShift(val, 8);
x64Xor(k1, val);
// fallthrough
case 1:
val[1] = key[i];
x64Xor(k1, val);
x64Multiply(k1, C1);
x64Rotl(k1, 31);
x64Multiply(k1, C2);
x64Xor(h1, k1);
// fallthrough
}
x64Xor(h1, length);
x64Xor(h2, length);
x64Add(h1, h2);
x64Add(h2, h1);
x64Fmix(h1);
x64Fmix(h2);
x64Add(h1, 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);
}
function isFunctionNative(func) {
return /^function\s.*?\{\s*\[native code]\s*}$/.test(String(func));
}
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: 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: 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];
}
});
});
};
}
/**
* Modifies an entropy source by transforming its returned value with the given function.
* Keeps the source properties: sync/async, 1/2 stages.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function transformSource(source, transformValue) {
var transformLoadResult = function (loadResult) {
if (isFinalResultLoaded(loadResult)) {
return transformValue(loadResult);
}
return function () {
var getResult = loadResult();
if (isPromise(getResult)) {
return getResult.then(transformValue);
}
return transformValue(getResult);
};
};
return function (options) {
var loadResult = source(options);
if (isPromise(loadResult)) {
return loadResult.then(transformLoadResult);
}
return transformLoadResult(loadResult);
};
}
/*
* 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 this WebKit browser is a desktop browser.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function isDesktopWebKit() {
// Checked in Safari and DuckDuckGo
var w = window;
var HTMLElement = w.HTMLElement, Document = w.Document;
return (countTruthy([
'safari' in w,
!('ongestureend' in w),
!('TouchEvent' in w),
!('orientation' in w),
HTMLElement && !('autocapitalize' in HTMLElement.prototype),
Document && 'pointerLockElement' in Document.prototype,
]) >= 4);
}
/**
* Checks whether this WebKit browser is Safari.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* Warning! The function works properly only for Safari version 15 and newer.
*/
function isSafariWebKit() {
// Checked in Safari, Chrome, Firefox, Yandex, UC Browser, Opera, Edge and DuckDuckGo.
// iOS Safari and Chrome were checked on iOS 11-17. DuckDuckGo was checked on iOS 17 and macOS 14.
// Desktop Safari versions 12-17 were checked.
// The other browsers were checked on iOS 17; there was no chance to check them on the other OS versions.
var w = window;
if (!isFunctionNative(w.print)) {
return false; // Chrome, Firefox, Yandex, DuckDuckGo macOS, Edge
}
return (countTruthy([
// Incorrect in Safari <= 14 (iOS and macOS)
String(w.browser) === '[object WebPageNamespace]',
// Incorrect in desktop Safari and iOS Safari <= 15
'MicrodataExtractor' in w,
]) >= 1);
}
/**
* 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.
*
* @see https://en.wikipedia.org/wiki/Safari_version_history#Release_history Safari-WebKit versions map
*/
function isWebKit606OrNewer() {
// Checked in Safari 9–17
var w = window;
return (countTruthy([
'DOMRectList' in w,
'RTCPeerConnectionIceEvent' in w,
'SVGGeometryElement' in w,
'ontransitioncancel' in w,
]) >= 3);
}
/**
* Checks whether the browser is based on WebKit version ≥616 (Safari ≥17) without using user-agent.
* It doesn't check that the browser is based on WebKit, there is a separate function for this.
*
* @see https://developer.apple.com/documentation/safari-release-notes/safari-17-release-notes Safari 17 release notes
* @see https://tauri.app/v1/references/webview-versions/#webkit-versions-in-safari Safari-WebKit versions map
*/
function isWebKit616OrNewer() {
var w = window;
var n = navigator;
var CSS = w.CSS, HTMLButtonElement = w.HTMLButtonElement;
return (countTruthy([
!('getStorageUpdates' in n),
HTMLButtonElement && 'popover' in HTMLButtonElement.prototype,
'CSSCounterStyleRule' in w,
CSS.supports('font-size-adjust: ex-height 0.5'),
CSS.supports('text-transform: full-width'),
]) >= 4);
}
/**
* 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-17
// Chrome on iPadOS (both mobile and desktop modes): 11-17
// Safari on iOS (both mobile and desktop modes): 9-17
// Chrome on iOS (both mobile and desktop modes): 9-17
// 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 is not supported
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();
var w = window;
var n = navigator;
var c = 'connection';
// Chrome removes all words "Android" from `navigator` when desktop version is requested
// Firefox keeps "Android" in `navigator.appVersion` when desktop version is requested
if (isItChromium) {
return (countTruthy([
!('SharedWorker' in w),
// `typechange` is deprecated, but it's still present on Android (tested on Chrome Mobile 117)
// Removal proposal https://bugs.chromium.org/p/chromium/issues/detail?id=699892
// Note: this expression returns true on ChromeOS, so additional detectors are required to avoid false-positives
n[c] && 'ontypechange' in n[c],
!('sinkId' in new window.Audio()),
]) >= 2);
}
else if (isItGecko) {
return countTruthy(['onorientationchange' in w, 'orientation' in w, /android/i.test(navigator.appVersion)]) >= 2;
}
else {
// 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.
return false;
}
}
/**
* Creates and keeps an invisible iframe while the given function runs.
* The given function is called when the iframe is loaded and has a body.
* The iframe allows to measure DOM sizes inside itself.
*
* Notice: passing an initial HTML code doesn't work in IE.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function withIframe(action, initialHtml, domPollInterval) {
var _a, _b, _c;
if (domPollInterval === void 0) { domPollInterval = 50; }
return __awaiter(this, void 0, void 0, function () {
var d, iframe;
return __generator(this, function (_d) {
switch (_d.label) {
case 0:
d = document;
_d.label = 1;
case 1:
if (!!d.body) return [3 /*break*/, 3];
return [4 /*yield*/, wait(domPollInterval)];
case 2:
_d.sent();
return [3 /*break*/, 1];
case 3:
iframe = d.createElement('iframe');
_d.label = 4;
case 4:
_d.trys.push([4, , 10, 11]);
return [4 /*yield*/, new Promise(function (_resolve, _reject) {
var isComplete = false;
var resolve = function () {
isComplete = true;
_resolve();
};
var reject = function (error) {
isComplete = true;
_reject(error);
};
iframe.onload = resolve;
iframe.onerror = reject;
var style = iframe.style;
style.setProperty('display', 'block', 'important'); // Required for browsers to calculate the layout
style.position = 'absolute';
style.top = '0';
style.left = '0';
style.visibility = 'hidden';
if (initialHtml && 'srcdoc' in iframe) {
iframe.srcdoc = initialHtml;
}
else {
iframe.src = 'about:blank';
}
d.body.appendChild(iframe);
// WebKit in WeChat doesn't fire the iframe's `onload` for some reason.
// This code checks for the loading state manually.
// See https://github.com/fingerprintjs/fingerprintjs/issues/645
var checkReadyState = function () {
var _a, _b;
// The ready state may never become 'complete' in Firefox despite the 'load' event being fired.
// So an infinite setTimeout loop can happen without this check.
// See https://github.com/fingerprintjs/fingerprintjs/pull/716#issuecomment-986898796
if (isComplete) {
return;
}
// Make sure iframe.contentWindow and iframe.contentWindow.document are both loaded
// The contentWindow.document can miss in JSDOM (https://github.com/jsdom/jsdom).
if (((_b = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document) === null || _b === void 0 ? void 0 : _b.readyState) === 'complete') {
resolve();
}
else {
setTimeout(checkReadyState, 10);
}
};
checkReadyState();
})];
case 5:
_d.sent();
_d.label = 6;
case 6:
if (!!((_b = (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.document) === null || _b === void 0 ? void 0 : _b.body)) return [3 /*break*/, 8];
return [4 /*yield*/, wait(domPollInterval)];
case 7:
_d.sent();
return [3 /*break*/, 6];
case 8: return [4 /*yield*/, action(iframe, iframe.contentWindow)];
case 9: return [2 /*return*/, _d.sent()];
case 10:
(_c = iframe.parentNode) === null || _c === void 0 ? void 0 : _c.removeChild(iframe);
return [7 /*endfinally*/];
case 11: return [2 /*return*/];
}
});
});
}
/**
* Creates a DOM element that matches the given selector.
* Only single element selector are supported (without operators like space, +, >, etc).
*/
function selectorToElement(selector) {
var _a = parseSimpleCssSelector(selector), tag = _a[0], attributes = _a[1];
var element = document.createElement(tag !== null && tag !== void 0 ? tag : 'div');
for (var _i = 0, _b = Object.keys(attributes); _i < _b.length; _i++) {
var name_1 = _b[_i];
var value = attributes[name_1].join(' ');
// Changing the `style` attribute can cause a CSP error, therefore we change the `style.cssText` property.
// https://github.com/fingerprintjs/fingerprintjs/issues/733
if (name_1 === 'style') {
addStyleString(element.style, value);
}
else {
element.setAttribute(name_1, value);
}
}
return element;
}
/**
* Adds CSS styles from a string in such a way that doesn't trigger a CSP warning (unsafe-inline or unsafe-eval)
*/
function addStyleString(style, source) {
// We don't use `style.cssText` because browsers must block it when no `unsafe-eval` CSP is presented: https://csplite.com/csp145/#w3c_note
// Even though the browsers ignore this standard, we don't use `cssText` just in case.
for (var _i = 0, _a = source.split(';'); _i < _a.length; _i++) {
var property = _a[_i];
var match = /^\s*([\w-]+)\s*:\s*(.+?)(\s*!([\w-]+))?\s*$/.exec(property);
if (match) {
var name_2 = match[1], value = match[2], priority = match[4];
style.setProperty(name_2, value, priority || ''); // The last argument can't be undefined in IE11
}
}
}
/**
* The returned promise resolves when the tab becomes visible (in foreground).
* If the tab is already visible, resolves immediately.
*/
function whenDocumentVisible() {
return new Promise(function (resolve) {
var d = document;
var eventName = 'visibilitychange';
var handleVisibilityChange = function () {
if (!d.hidden) {
d.removeEventListener(eventName, handleVisibilityChange);
resolve();
}
};
d.addEventListener(eventName, handleVisibilityChange);
handleVisibilityChange();
});
}
var sampleRate = 44100;
var cloneCount = 40000;
var stabilizationPrecision = 6.2;
/**
* A version of the entropy source with stabilization to make it suitable for static fingerprinting.
* Audio signal is noised in private mode of Safari 17.
*/
function getAudioFingerprint() {
return __awaiter(this, void 0, void 0, function () {
var finish;
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getUnstableAudioFingerprint()];
case 1:
finish = _a.sent();
return [2 /*return*/, function () {
var rawFingerprint = finish();
return stabilize(rawFingerprint, stabilizationPrecision);
}];
}
});
});
}
/**
* A version of the entropy source without stabilization.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function getUnstableAudioFingerprint() {
return __awaiter(this, void 0, void 0, function () {
var fingerprintResult, timeoutPromise, fingerprintPromise;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
timeoutPromise = whenDocumentVisible().then(function () { return wait(500); });
fingerprintPromise = getBaseAudioFingerprint().then(function (result) { return (fingerprintResult = [true, result]); }, function (error) { return (fingerprintResult = [false, error]); });
return [4 /*yield*/, Promise.race([timeoutPromise, fingerprintPromise])];
case 1:
_a.sent();
return [2 /*return*/, function () {
if (!fingerprintResult) {
return -3 /* SpecialFingerprint.Timeout */;
}
if (!fingerprintResult[0]) {
throw fingerprintResult[1];
}
return fingerprintResult[1];
}];
}
});
});
}
function getBaseAudioFingerprint() {
return __awaiter(this, void 0, void 0, function () {
var w, AudioContext, baseSignal, context, sourceNode, clonedSignal, fingerprint;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
w = window;
AudioContext = w.OfflineAudioContext || w.webkitOfflineAudioContext;
if (!AudioContext) {
return [2 /*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 (doesBrowserSuspendAudioContext()) {
return [2 /*return*/, -1 /* SpecialFingerprint.KnownForSuspending */];
}
return [4 /*yield*/, getBaseSignal(AudioContext)];
case 1:
baseSignal = _a.sent();
if (!baseSignal) {
return [2 /*return*/, -3 /* SpecialFingerprint.Timeout */];
}
context = new AudioContext(1, baseSignal.length - 1 + cloneCount, sampleRate);
sourceNode = context.createBufferSource();
sourceNode.buffer = baseSignal;
sourceNode.loop = true;
sourceNode.loopStart = (baseSignal.length - 1) / sampleRate;
sourceNode.loopEnd = baseSignal.length / sampleRate;
sourceNode.connect(context.destination);
sourceNode.start();
return [4 /*yield*/, renderAudio(context)];
case 2:
clonedSignal = _a.sent();
if (!clonedSignal) {
return [2 /*return*/, -3 /* SpecialFingerprint.Timeout */];
}
fingerprint = extractFingerprint(baseSignal, clonedSignal.getChannelData(0).subarray(baseSignal.length - 1));
return [2 /*return*/, Math.abs(fingerprint)]; // The fingerprint is made positive to avoid confusion with the special fingerprints
}
});
});
}
/**
* Checks if the current browser is known for always suspending audio context.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function doesBrowserSuspendAudioContext() {
// Mobile Safari 11 and older
return isWebKit() && !isDesktopWebKit() && !isWebKit606OrNewer();
}
/**
* Produces an arbitrary audio signal
*/
function getBaseSignal(AudioContext) {
return __awaiter(this, void 0, void 0, function () {
var targetSampleIndex, context, oscillator, compressor, filter;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
targetSampleIndex = 3395;
context = new AudioContext(1, targetSampleIndex + 1, sampleRate);
oscillator = context.createOscillator();
oscillator.type = 'square';
oscillator.frequency.value = 1000;
compressor = context.createDynamicsCompressor();
compressor.threshold.value = -70;
compressor.knee.value = 40;
compressor.ratio.value = 12;
compressor.attack.value = 0;
compressor.release.value = 0.25;
filter = context.createBiquadFilter();
filter.type = 'allpass';
filter.frequency.value = 5.239622852977861;
filter.Q.value = 0.1;
oscillator.connect(compressor);
compressor.connect(filter);
filter.connect(context.destination);
oscillator.start(0);
return [4 /*yield*/, renderAudio(context)];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
}
/**
* Renders the given audio context with configured nodes.
* Returns `null` when the rendering runs out of attempts.
*
* Warning for package users:
* This function is out of Semantic Versioning, i.e. can change unexpectedly. Usage is at your own risk.
*/
function renderAudio(context) {
return new Promise(function (resolve, reject) {
var retryDelay = 200;
var attemptsLeft = 25;
context.oncomplete = function (event) { return resolve(event.renderedBuffer); };
var tryRender = function () {
try {
var renderingPromise = context.startRendering();
// `context.startRendering` has two APIs: Promise and callback, we check that it's really a promise just in case
if (isPromise(renderingPromise)) {
// Suppresses all unhandled rejections in case of scheduled redundant retries after successful rendering
suppressUnhandledRejectionWarning(renderingPromise);
}
// Sometimes the audio context doesn't start after calling `startRendering` (in addition to the cases where
// audio context doesn't start at all). A known case is starting an audio context when the browser tab is in
// background on iPhone. Retries usually help in this case.
if (context.state === 'suspended') {
// The audio context can reject starting until the tab is in foreground. Long fingerprint duration
// in background isn't a problem, therefore the retry attempts don't count in background. It can lead to
// a situation when a fingerprint takes very long time and finishes successfully. FYI, the audio context
// can be suspended when `document.hidden === false` and start running after a retry.
if (!document.hidden) {
attemptsLeft--;
}
if (attemptsLeft > 0) {
setTimeout(tryRender, retryDelay);
}
else {
resolve(null);
}
}
}
catch (error) {
reject(error);
}
};
tryRender();
});
}
function extractFingerprint(baseSignal, clonedSample) {
var fingerprint = undefined;
var needsDenoising = false;
for (var i = 0; i < clonedSample.length; i += Math.floor(clonedSample.length / 10)) {
if (clonedSample[i] === 0) ;
else if (fingerprint === undefined) {
fingerprint = clonedSample[i];
}
else if (fingerprint !== clonedSample[i]) {
needsDenoising = true;
break;
}
}
// The looped buffer source works incorrectly in old Safari versions (>14 desktop, >15 mobile).
// The looped signal contains only 0s. To fix it, the loop start should be `baseSignal.length - 1.00000000001` and
// the loop end should be `baseSignal.length + 0.00000000001` (there can be 10 or 11 0s after the point). But this
// solution breaks the looped signal in other browsers. Instead of checking the browser version, we check that the
// looped signals comprises only 0s, and if it does, we return the last value of the base signal, because old Safari
// versions don't add noise that we want to cancel.
if (fingerprint === undefined) {
fingerprint = baseSignal.getChannelData(0)[baseSignal.length - 1];
}
else if (needsDenoising) {
fingerprint = getMiddle(clone