UNPKG

@fingerprintjs/fingerprintjs

Version:

Browser fingerprinting library with the highest accuracy and stability

1,256 lines (1,246 loc) 134 kB
/** * 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