` → the vendored bundle."""
def __init__(self):
page = _PAGE.encode()
+ creep_page = _CREEP_PAGE.encode()
vendor = _VENDOR
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self): # noqa: N802
- if self.path == "/" or self.path.startswith("/?"):
+ p = self.path.split("?")[0]
+ if p == "/":
body, ctype = page, "text/html; charset=utf-8"
+ elif p == "/creepjs":
+ body, ctype = creep_page, "text/html; charset=utf-8"
else:
- f = vendor / Path(self.path.lstrip("/")).name
+ f = vendor / Path(p.lstrip("/")).name
if not f.is_file():
self.send_error(404); return
body = f.read_bytes()
@@ -99,6 +141,10 @@ class _DetectorSite:
def url(self):
return f"http://127.0.0.1:{self.port}/"
+ @property
+ def creep_url(self):
+ return f"http://127.0.0.1:{self.port}/creepjs"
+
def close(self):
self._srv.shutdown()
@@ -111,7 +157,7 @@ def detector_site():
def _run_detectors(firefox_binary, url):
- """Launch the binary, load the page, return (botd, fp, err)."""
+ """Launch the binary, load the page, return (botd, fp, fps, err)."""
with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser:
page = browser.new_page()
page.goto(url, wait_until="load", timeout=45000)
@@ -121,16 +167,48 @@ def _run_detectors(firefox_binary, url):
)
botd = page.evaluate("() => window.__botd")
fp = page.evaluate("() => window.__fp")
+ fps = page.evaluate("() => window.__fps")
err = page.evaluate("() => window.__err")
- return botd, fp, err
+ return botd, fp, fps, err
+
+
+def _run_creepjs(firefox_binary, creep_url):
+ """Launch the binary, run CreepJS fully offline, return its headless result."""
+ _EV = """() => {
+ const f = window.Fingerprint;
+ if (!f || !f.headless) return { ready: false };
+ const h = f.headless;
+ return {
+ ready: true,
+ headlessRating: h.headlessRating,
+ stealthRating: h.stealthRating,
+ likeHeadlessRating: h.likeHeadlessRating,
+ headless: h.headless || {},
+ stealth: h.stealth || {},
+ totalLies: (f.lies && f.lies.totalLies) || 0,
+ };
+ }"""
+ with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser:
+ page = browser.new_page()
+ # truly offline: abort every non-loopback request (CreepJS's optional
+ # crowd-comparison POST to arh.antoinevastel.com never runs).
+ page.route(
+ "**/*",
+ lambda r: r.abort() if "127.0.0.1" not in r.request.url else r.continue_(),
+ )
+ page.goto(creep_url, wait_until="domcontentloaded", timeout=45000)
+ page.wait_for_function(
+ "() => !!(window.Fingerprint && window.Fingerprint.headless)",
+ timeout=60000,
+ )
+ return page.evaluate(_EV)
@pytest.mark.e2e
def test_botd_no_detector_flags_automation(firefox_binary, detector_site):
"""The real BotD must not flag the build — aggregate AND every one of its
- individual detectors (webDriver/userAgent/appVersion/plugins/process/... ).
- """
- botd, _fp, err = _run_detectors(firefox_binary, detector_site.url)
+ individual detectors (webDriver/userAgent/appVersion/plugins/process/...)."""
+ botd, _fp, _fps, err = _run_detectors(firefox_binary, detector_site.url)
assert botd is not None, f"BotD produced no result (err:{err!r})"
assert botd.get("bot") is False, (
f"BotD aggregate flagged a bot: botKind={botd.get('botKind')!r}"
@@ -146,8 +224,8 @@ def test_fingerprintjs_visitorid_stable_across_launches(firefox_binary, detector
"""FingerprintJS visitorId must be present and identical across two fresh
launches with the same seed — a real browser is stable; an over-randomized
spoof drifts (and a drifting fingerprint is itself a bot tell)."""
- _b1, fp1, err1 = _run_detectors(firefox_binary, detector_site.url)
- _b2, fp2, err2 = _run_detectors(firefox_binary, detector_site.url)
+ _b1, fp1, _f1, err1 = _run_detectors(firefox_binary, detector_site.url)
+ _b2, fp2, _f2, err2 = _run_detectors(firefox_binary, detector_site.url)
assert fp1 and fp1.get("visitorId"), f"no visitorId on run 1 (err:{err1!r})"
assert fp2 and fp2.get("visitorId"), f"no visitorId on run 2 (err:{err2!r})"
assert fp1["visitorId"] == fp2["visitorId"], (
@@ -159,13 +237,58 @@ def test_fingerprintjs_visitorid_stable_across_launches(firefox_binary, detector
@pytest.mark.e2e
def test_fingerprintjs_collects_rich_fingerprint(firefox_binary, detector_site):
"""FingerprintJS must collect a RICH component surface (a real browser
- exposes many signals; a stripped/blocked surface is itself suspicious).
- We don't assert zero errored components (some are legitimately unsupported
- per browser), only that the surface is substantial and the id computed."""
- _b, fp, err = _run_detectors(firefox_binary, detector_site.url)
+ exposes many signals; a stripped/blocked surface is itself suspicious)."""
+ _b, fp, _f, err = _run_detectors(firefox_binary, detector_site.url)
assert fp and fp.get("visitorId"), f"FingerprintJS produced no id (err:{err!r})"
keys = fp.get("componentKeys") or []
assert len(keys) >= 15, (
f"FingerprintJS collected only {len(keys)} components — surface too thin "
f"(suppressed signals are themselves a tell): {keys}"
)
+
+
+@pytest.mark.e2e
+def test_fpscanner_no_automation_rules(firefox_binary, detector_site):
+ """fpscanner's engine-agnostic bot rules (webdriver/selenium/bot-UA/platform/
+ timezone/language) must all be clean. The Chrome/GPU-only rules are ignored
+ on purpose (see module docstring) — they false-red on a software-WebGL host."""
+ _b, _fp, fps, err = _run_detectors(firefox_binary, detector_site.url)
+ assert fps is not None, f"fpscanner produced no result (err:{err!r})"
+ details = fps.get("details") or {}
+ assert details, f"fpscanner returned no detection details (err:{err!r})"
+ flagged = [
+ k for k in _FPSCANNER_AGNOSTIC
+ if details.get(k) and details[k].get("detected")
+ ]
+ assert not flagged, (
+ f"fpscanner flagged automation on engine-agnostic rules: {flagged} "
+ f"(full details: { {k: v for k, v in details.items() if v.get('detected')} })"
+ )
+
+
+@pytest.mark.e2e
+def test_creepjs_headless_and_proxy_clean(firefox_binary, detector_site):
+ """CreepJS (Firefox-aware) must see no headless tell and no JS-proxy stealth
+ tell. ``headlessRating`` aggregates webDriverIsOn + headless-UA checks (all
+ GPU-independent). The proxy/runtime stealth sub-signals (hasIframeProxy,
+ hasToStringProxy, hasBadChromeRuntime) must be false — a spoof implemented
+ with a JS Proxy is exactly what CreepJS catches. stealthRating/totalLies/
+ likeHeadlessRating are GPU/theme-sensitive, so we log them, not assert."""
+ r = _run_creepjs(firefox_binary, detector_site.creep_url)
+ assert r and r.get("ready"), f"CreepJS never populated window.Fingerprint: {r!r}"
+ print(
+ f"[creepjs] headlessRating={r['headlessRating']} stealthRating={r['stealthRating']} "
+ f"likeHeadlessRating={r['likeHeadlessRating']} totalLies={r['totalLies']} "
+ f"headless={r['headless']} stealth={r['stealth']}"
+ )
+ assert r["headlessRating"] == 0, (
+ f"CreepJS headless tells fired: headless={r['headless']} "
+ f"(headlessRating={r['headlessRating']})"
+ )
+ stealth = r.get("stealth") or {}
+ proxy_tells = {
+ k: stealth.get(k)
+ for k in ("hasIframeProxy", "hasToStringProxy", "hasBadChromeRuntime")
+ if stealth.get(k)
+ }
+ assert not proxy_tells, f"CreepJS JS-proxy stealth tells fired: {proxy_tells}"
diff --git a/tests/vendor/README.md b/tests/vendor/README.md
index 8b4ae4a..b252d8b 100644
--- a/tests/vendor/README.md
+++ b/tests/vendor/README.md
@@ -6,13 +6,19 @@ on a dev box and on a GitHub runner (no external CDN at test time — Firefox
tracking-protection blocks the openfpcdn.io CDN anyway, and we want CI offline).
They are served from a localhost HTTP server and loaded into the patched Firefox;
-the tests assert the REAL detectors don't flag the stealth build (BotD: `bot===false`)
-and that the fingerprint is stable (FingerprintJS: same `visitorId` across launches).
+the tests assert the REAL detectors don't flag the stealth build (BotD: `bot===false`;
+fpscanner: engine-agnostic rules clean; CreepJS: `headlessRating===0` + no JS-proxy
+tells) and that the fingerprint is stable (FingerprintJS: same `visitorId` across
+launches). CreepJS runs fully offline — the tests abort every non-loopback request,
+so its optional crowd-comparison POST never fires and the verdict is computed locally.
| File | Package | Version | Source | License |
|---|---|---|---|---|
| `botd-2.0.0.esm.js` | `@fingerprintjs/botd` | 2.0.0 | https://cdn.jsdelivr.net/npm/@fingerprintjs/botd@2.0.0/dist/botd.esm.js | MIT |
| `fingerprintjs-5.2.0.umd.min.js` | `@fingerprintjs/fingerprintjs` | 5.2.0 | https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@5.2.0/dist/fp.umd.min.js | MIT |
+| `fpscanner-1.0.6.es.js` | `fpscanner` | 1.0.6 | https://cdn.jsdelivr.net/npm/fpscanner@1.0.6/dist/fpScanner.es.js | MIT |
+| `creepjs-10aa672.js` | `abrahamjuliot/creepjs` | git `10aa6724` | https://raw.githubusercontent.com/abrahamjuliot/creepjs/10aa6724cd33a1015db1574211890518cd04f0cc/docs/creep.js | MIT |
-Both are MIT (Copyright © FingerprintJS, Inc.). To update: download the pinned
-dist from jsdelivr, drop it here, and bump the version in the filename + this table.
+All MIT (FingerprintJS Inc. / Antoine Vastel / Abraham Juliot). To update: download
+the pinned dist (jsdelivr for npm packages, raw.githubusercontent for CreepJS at a
+commit SHA), drop it here, and bump the version in the filename + this table.
diff --git a/tests/vendor/creepjs-10aa672.js b/tests/vendor/creepjs-10aa672.js
new file mode 100644
index 0000000..1e920f1
--- /dev/null
+++ b/tests/vendor/creepjs-10aa672.js
@@ -0,0 +1,9710 @@
+(function () {
+ 'use strict';
+
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
+ // @ts-expect-error
+ const IS_WORKER_SCOPE = !self.document && self.WorkerGlobalScope;
+ // Detect Browser
+ function getEngine() {
+ const x = [].constructor;
+ try {
+ (-1).toFixed(-1);
+ }
+ catch (err) {
+ return err.message.length + (x + '').split(x.name).join('').length;
+ }
+ }
+ const ENGINE_IDENTIFIER = getEngine();
+ const IS_BLINK = ENGINE_IDENTIFIER == 80;
+ const IS_GECKO = ENGINE_IDENTIFIER == 58;
+ const IS_WEBKIT = ENGINE_IDENTIFIER == 77;
+ const JS_ENGINE = ({
+ 80: 'V8',
+ 58: 'SpiderMonkey',
+ 77: 'JavaScriptCore',
+ })[ENGINE_IDENTIFIER] || null;
+ const LIKE_BRAVE = IS_BLINK && 'flat' in Array.prototype /* Chrome 69 */ && !('ReportingObserver' in self /* Brave */);
+ function braveBrowser() {
+ const brave = ('brave' in navigator &&
+ // @ts-ignore
+ Object.getPrototypeOf(navigator.brave).constructor.name == 'Brave' &&
+ // @ts-ignore
+ navigator.brave.isBrave.toString() == 'function isBrave() { [native code] }');
+ return brave;
+ }
+ function getBraveMode() {
+ const mode = {
+ unknown: false,
+ allow: false,
+ standard: false,
+ strict: false,
+ };
+ try {
+ // strict mode adds float frequency data AnalyserNode
+ const strictMode = () => {
+ try {
+ window.OfflineAudioContext = (
+ // @ts-ignore
+ OfflineAudioContext || webkitOfflineAudioContext);
+ }
+ catch (err) { }
+ if (!window.OfflineAudioContext) {
+ return false;
+ }
+ const context = new OfflineAudioContext(1, 1, 44100);
+ const analyser = context.createAnalyser();
+ const data = new Float32Array(analyser.frequencyBinCount);
+ analyser.getFloatFrequencyData(data);
+ const strict = new Set(data).size > 1; // native only has -Infinity
+ return strict;
+ };
+ if (strictMode()) {
+ mode.strict = true;
+ return mode;
+ }
+ // standard and strict mode do not have chrome plugins
+ const chromePlugins = /(Chrom(e|ium)|Microsoft Edge) PDF (Plugin|Viewer)/;
+ const pluginsList = [...navigator.plugins];
+ const hasChromePlugins = pluginsList
+ .filter((plugin) => chromePlugins.test(plugin.name)).length == 2;
+ if (pluginsList.length && !hasChromePlugins) {
+ mode.standard = true;
+ return mode;
+ }
+ mode.allow = true;
+ return mode;
+ }
+ catch (e) {
+ mode.unknown = true;
+ return mode;
+ }
+ }
+ const getBraveUnprotectedParameters = (parameters) => {
+ const blocked = new Set([
+ 'FRAGMENT_SHADER.HIGH_FLOAT.precision',
+ 'FRAGMENT_SHADER.HIGH_FLOAT.rangeMax',
+ 'FRAGMENT_SHADER.HIGH_FLOAT.rangeMin',
+ 'FRAGMENT_SHADER.HIGH_INT.precision',
+ 'FRAGMENT_SHADER.HIGH_INT.rangeMax',
+ 'FRAGMENT_SHADER.HIGH_INT.rangeMin',
+ 'FRAGMENT_SHADER.LOW_FLOAT.precision',
+ 'FRAGMENT_SHADER.LOW_FLOAT.rangeMax',
+ 'FRAGMENT_SHADER.LOW_FLOAT.rangeMin',
+ 'FRAGMENT_SHADER.MEDIUM_FLOAT.precision',
+ 'FRAGMENT_SHADER.MEDIUM_FLOAT.rangeMax',
+ 'FRAGMENT_SHADER.MEDIUM_FLOAT.rangeMin',
+ 'MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS',
+ 'MAX_COMBINED_UNIFORM_BLOCKS',
+ 'MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS',
+ 'MAX_DRAW_BUFFERS_WEBGL',
+ 'MAX_FRAGMENT_INPUT_COMPONENTS',
+ 'MAX_FRAGMENT_UNIFORM_BLOCKS',
+ 'MAX_FRAGMENT_UNIFORM_COMPONENTS',
+ 'MAX_TEXTURE_MAX_ANISOTROPY_EXT',
+ 'MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS',
+ 'MAX_UNIFORM_BUFFER_BINDINGS',
+ 'MAX_VARYING_COMPONENTS',
+ 'MAX_VERTEX_OUTPUT_COMPONENTS',
+ 'MAX_VERTEX_UNIFORM_BLOCKS',
+ 'MAX_VERTEX_UNIFORM_COMPONENTS',
+ 'SHADING_LANGUAGE_VERSION',
+ 'UNMASKED_RENDERER_WEBGL',
+ 'UNMASKED_VENDOR_WEBGL',
+ 'VERSION',
+ 'VERTEX_SHADER.HIGH_FLOAT.precision',
+ 'VERTEX_SHADER.HIGH_FLOAT.rangeMax',
+ 'VERTEX_SHADER.HIGH_FLOAT.rangeMin',
+ 'VERTEX_SHADER.HIGH_INT.precision',
+ 'VERTEX_SHADER.HIGH_INT.rangeMax',
+ 'VERTEX_SHADER.HIGH_INT.rangeMin',
+ 'VERTEX_SHADER.LOW_FLOAT.precision',
+ 'VERTEX_SHADER.LOW_FLOAT.rangeMax',
+ 'VERTEX_SHADER.LOW_FLOAT.rangeMin',
+ 'VERTEX_SHADER.MEDIUM_FLOAT.precision',
+ 'VERTEX_SHADER.MEDIUM_FLOAT.rangeMax',
+ 'VERTEX_SHADER.MEDIUM_FLOAT.rangeMin',
+ ]);
+ const safeParameters = Object.keys(parameters).reduce((acc, curr) => {
+ if (blocked.has(curr)) {
+ return acc;
+ }
+ acc[curr] = parameters[curr];
+ return acc;
+ }, {});
+ return safeParameters;
+ };
+ // system
+ const getOS = (userAgent) => {
+ const os = (
+ // order is important
+ /windows phone/ig.test(userAgent) ? 'Windows Phone' :
+ /win(dows|16|32|64|95|98|nt)|wow64/ig.test(userAgent) ? 'Windows' :
+ /android/ig.test(userAgent) ? 'Android' :
+ /cros/ig.test(userAgent) ? 'Chrome OS' :
+ /linux/ig.test(userAgent) ? 'Linux' :
+ /ipad/ig.test(userAgent) ? 'iPad' :
+ /iphone/ig.test(userAgent) ? 'iPhone' :
+ /ipod/ig.test(userAgent) ? 'iPod' :
+ /ios/ig.test(userAgent) ? 'iOS' :
+ /mac/ig.test(userAgent) ? 'Mac' :
+ 'Other');
+ return os;
+ };
+ function getReportedPlatform(userAgent, platform) {
+ // user agent os lie
+ const userAgentOS = (
+ // order is important
+ /win(dows|16|32|64|95|98|nt)|wow64/ig.test(userAgent) ? "Windows" /* PlatformClassifier.WINDOWS */ :
+ /android|linux|cros/ig.test(userAgent) ? "Linux" /* PlatformClassifier.LINUX */ :
+ /(i(os|p(ad|hone|od)))|mac/ig.test(userAgent) ? "Apple" /* PlatformClassifier.APPLE */ :
+ "Other" /* PlatformClassifier.OTHER */);
+ if (!platform)
+ return [userAgentOS];
+ const platformOS = (
+ // order is important
+ /win/ig.test(platform) ? "Windows" /* PlatformClassifier.WINDOWS */ :
+ /android|arm|linux/ig.test(platform) ? "Linux" /* PlatformClassifier.LINUX */ :
+ /(i(os|p(ad|hone|od)))|mac/ig.test(platform) ? "Apple" /* PlatformClassifier.APPLE */ :
+ "Other" /* PlatformClassifier.OTHER */);
+ return [userAgentOS, platformOS];
+ }
+ const { userAgent: navUserAgent, platform: navPlatform } = self.navigator || {};
+ const [USER_AGENT_OS, PLATFORM_OS] = getReportedPlatform(navUserAgent, navPlatform);
+ const decryptUserAgent = ({ ua, os, isBrave }) => {
+ const apple = /ipad|iphone|ipod|ios|mac/ig.test(os);
+ const isOpera = /OPR\//g.test(ua);
+ const isVivaldi = /Vivaldi/g.test(ua);
+ const isDuckDuckGo = /DuckDuckGo/g.test(ua);
+ const isYandex = /YaBrowser/g.test(ua);
+ const paleMoon = ua.match(/(palemoon)\/(\d+)./i);
+ const edge = ua.match(/(edgios|edg|edge|edga)\/(\d+)./i);
+ const edgios = edge && /edgios/i.test(edge[1]);
+ const chromium = ua.match(/(crios|chrome)\/(\d+)./i);
+ const firefox = ua.match(/(fxios|firefox)\/(\d+)./i);
+ const likeSafari = (/AppleWebKit/g.test(ua) &&
+ /Safari/g.test(ua));
+ const safari = (likeSafari &&
+ !firefox &&
+ !chromium &&
+ !edge &&
+ ua.match(/(version)\/(\d+)\.(\d|\.)+\s(mobile|safari)/i));
+ if (chromium) {
+ const browser = chromium[1];
+ const version = chromium[2];
+ const like = (isOpera ? ' Opera' :
+ isVivaldi ? ' Vivaldi' :
+ isDuckDuckGo ? ' DuckDuckGo' :
+ isYandex ? ' Yandex' :
+ edge ? ' Edge' :
+ isBrave ? ' Brave' : '');
+ return `${browser} ${version}${like}`;
+ }
+ else if (edgios) {
+ const browser = edge[1];
+ const version = edge[2];
+ return `${browser} ${version}`;
+ }
+ else if (firefox) {
+ const browser = paleMoon ? paleMoon[1] : firefox[1];
+ const version = paleMoon ? paleMoon[2] : firefox[2];
+ return `${browser} ${version}`;
+ }
+ else if (apple && safari) {
+ const browser = 'Safari';
+ const version = safari[2];
+ return `${browser} ${version}`;
+ }
+ return 'unknown';
+ };
+ const getUserAgentPlatform = ({ userAgent, excludeBuild = true }) => {
+ if (!userAgent) {
+ return 'unknown';
+ }
+ // patterns
+ const nonPlatformParenthesis = /\((khtml|unlike|vizio|like gec|internal dummy|org\.eclipse|openssl|ipv6|via translate|safari|cardamon).+|xt\d+\)/ig;
+ const parenthesis = /\((.+)\)/;
+ const android = /((android).+)/i;
+ const androidNoise = /^(linux|[a-z]|wv|mobile|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|windows|(rv:|trident|webview|iemobile).+/i;
+ const androidBuild = /build\/.+\s|\sbuild\/.+/i;
+ const androidRelease = /android( |-)\d+/i;
+ const windows = /((windows).+)/i;
+ const windowsNoise = /^(windows|ms(-|)office|microsoft|compatible|[a-z]|x64|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|(rv:|outlook|ms(-|)office|microsoft|trident|\.net|msie|httrack|media center|infopath|aol|opera|iemobile|webbrowser).+/i;
+ const windows64bitCPU = /w(ow|in)64/i;
+ const cros = /cros/i;
+ const crosNoise = /^([a-z]|x11|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|(rv:|trident).+/i;
+ const crosBuild = /\d+\.\d+\.\d+/i;
+ const linux = /linux|x11|ubuntu|debian/i;
+ const linuxNoise = /^([a-z]|x11|unknown|compatible|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|(rv:|java|oracle|\+http|http|unknown|mozilla|konqueror|valve).+/i;
+ const apple = /(cpu iphone|cpu os|iphone os|mac os|macos|intel os|ppc mac).+/i;
+ const appleNoise = /^([a-z]|macintosh|compatible|mimic|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2}|rv|\d+\.\d+)$|(rv:|silk|valve).+/i;
+ const appleRelease = /(ppc |intel |)(mac|mac |)os (x |x|)(\d{2}(_|\.)\d{1,2}|\d{2,})/i;
+ const otherOS = /((symbianos|nokia|blackberry|morphos|mac).+)|\/linux|freebsd|symbos|series \d+|win\d+|unix|hp-ux|bsdi|bsd|x86_64/i;
+ const isDevice = (list, device) => list.filter((x) => device.test(x)).length;
+ userAgent = userAgent.trim().replace(/\s{2,}/, ' ').replace(nonPlatformParenthesis, '');
+ if (parenthesis.test(userAgent)) {
+ const platformSection = userAgent.match(parenthesis)[0];
+ const identifiers = platformSection.slice(1, -1).replace(/,/g, ';').split(';').map((x) => x.trim());
+ if (isDevice(identifiers, android)) {
+ return identifiers
+ // @ts-ignore
+ .map((x) => androidRelease.test(x) ? androidRelease.exec(x)[0].replace('-', ' ') : x)
+ .filter((x) => !(androidNoise.test(x)))
+ .join(' ')
+ .replace((excludeBuild ? androidBuild : ''), '')
+ .trim().replace(/\s{2,}/, ' ');
+ }
+ else if (isDevice(identifiers, windows)) {
+ return identifiers
+ .filter((x) => !(windowsNoise.test(x)))
+ .join(' ')
+ .replace(/\sNT (\d+\.\d+)/, (match, version) => {
+ return (version == '10.0' ? ' 10' :
+ version == '6.3' ? ' 8.1' :
+ version == '6.2' ? ' 8' :
+ version == '6.1' ? ' 7' :
+ version == '6.0' ? ' Vista' :
+ version == '5.2' ? ' XP Pro' :
+ version == '5.1' ? ' XP' :
+ version == '5.0' ? ' 2000' :
+ version == '4.0' ? match :
+ ' ' + version);
+ })
+ .replace(windows64bitCPU, '(64-bit)')
+ .trim().replace(/\s{2,}/, ' ');
+ }
+ else if (isDevice(identifiers, cros)) {
+ return identifiers
+ .filter((x) => !(crosNoise.test(x)))
+ .join(' ')
+ .replace((excludeBuild ? crosBuild : ''), '')
+ .trim().replace(/\s{2,}/, ' ');
+ }
+ else if (isDevice(identifiers, linux)) {
+ return identifiers
+ .filter((x) => !(linuxNoise.test(x)))
+ .join(' ')
+ .trim().replace(/\s{2,}/, ' ');
+ }
+ else if (isDevice(identifiers, apple)) {
+ return identifiers
+ .map((x) => {
+ if (appleRelease.test(x)) {
+ // @ts-ignore
+ const release = appleRelease.exec(x)[0];
+ const versionMap = {
+ '10_7': 'Lion',
+ '10_8': 'Mountain Lion',
+ '10_9': 'Mavericks',
+ '10_10': 'Yosemite',
+ '10_11': 'El Capitan',
+ '10_12': 'Sierra',
+ '10_13': 'High Sierra',
+ '10_14': 'Mojave',
+ '10_15': 'Catalina',
+ '11': 'Big Sur',
+ '12': 'Monterey',
+ '13': 'Ventura',
+ };
+ const version = ((/(\d{2}(_|\.)\d{1,2}|\d{2,})/.exec(release) || [])[0] ||
+ '').replace(/\./g, '_');
+ const isOSX = /^10/.test(version);
+ const id = isOSX ? version : (/^\d{2,}/.exec(version) || [])[0];
+ const codeName = versionMap[id];
+ return codeName ? `macOS ${codeName}` : release;
+ }
+ return x;
+ })
+ .filter((x) => !(appleNoise.test(x)))
+ .join(' ')
+ .replace(/\slike mac.+/ig, '')
+ .trim().replace(/\s{2,}/, ' ');
+ }
+ else {
+ const other = identifiers.filter((x) => otherOS.test(x));
+ if (other.length) {
+ return other.join(' ').trim().replace(/\s{2,}/, ' ');
+ }
+ return identifiers.join(' ');
+ }
+ }
+ else {
+ return 'unknown';
+ }
+ };
+ const computeWindowsRelease = ({ platform, platformVersion, fontPlatformVersion }) => {
+ if ((platform != 'Windows') || !(IS_BLINK && CSS.supports('accent-color', 'initial'))) {
+ return;
+ }
+ const platformVersionNumber = +(/(\d+)\./.exec(platformVersion) || [])[1];
+ // https://github.com/WICG/ua-client-hints/issues/220#issuecomment-870858413
+ // https://docs.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11
+ // https://docs.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-guidance
+ const release = {
+ '0.1.0': '7',
+ '0.2.0': '8',
+ '0.3.0': '8.1',
+ '1.0.0': '10 (1507)',
+ '2.0.0': '10 (1511)',
+ '3.0.0': '10 (1607)',
+ '4.0.0': '10 (1703)',
+ '5.0.0': '10 (1709)',
+ '6.0.0': '10 (1803)',
+ '7.0.0': '10 (1809)',
+ '8.0.0': '10 (1903|1909)',
+ '10.0.0': '10 (2004|20H2|21H1)',
+ '11.0.0': '10',
+ '12.0.0': '10',
+ };
+ const oldFontPlatformVersionNumber = (/7|8\.1|8/.exec(fontPlatformVersion) || [])[0];
+ const version = (platformVersionNumber >= 13 ? '11' :
+ platformVersionNumber == 0 && oldFontPlatformVersionNumber ? oldFontPlatformVersionNumber :
+ (release[platformVersion] || 'Unknown'));
+ return (`Windows ${version} [${platformVersion}]`);
+ };
+ // attempt restore from User-Agent Reduction
+ const isUAPostReduction = (userAgent) => {
+ const matcher = /Mozilla\/5\.0 \((Macintosh; Intel Mac OS X 10_15_7|Windows NT 10\.0; Win64; x64|(X11; (CrOS|Linux) x86_64)|(Linux; Android 10(; K|)))\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0( Mobile|) Safari\/537\.36/;
+ const unifiedPlatform = (matcher.exec(userAgent) || [])[1];
+ return IS_BLINK && !!unifiedPlatform;
+ };
+ const createPerformanceLogger = () => {
+ const log = {};
+ let total = 0;
+ return {
+ logTestResult: ({ test, passed, time = 0 }) => {
+ total += time;
+ const timeString = `${time.toFixed(2)}ms`;
+ log[test] = timeString;
+ const color = passed ? '#4cca9f' : 'lightcoral';
+ const result = passed ? 'passed' : 'failed';
+ const symbol = passed ? '✔' : '-';
+ return console.log(`%c${symbol}${time ? ` (${timeString})` : ''} ${test} ${result}`, `color:${color}`);
+ },
+ getLog: () => log,
+ getTotal: () => total,
+ };
+ };
+ const performanceLogger = createPerformanceLogger();
+ const { logTestResult } = performanceLogger;
+ const createTimer = () => {
+ let start = 0;
+ const log = [];
+ return {
+ stop: () => {
+ if (start) {
+ log.push(performance.now() - start);
+ return log.reduce((acc, n) => acc += n, 0);
+ }
+ return start;
+ },
+ start: () => {
+ start = performance.now();
+ return start;
+ },
+ };
+ };
+ const queueEvent = (timer, delay = 0) => {
+ timer.stop();
+ return new Promise((resolve) => setTimeout(() => resolve(timer.start()), delay))
+ .catch((e) => { });
+ };
+ const formatEmojiSet = (emojiSet, limit = 3) => {
+ const maxLen = (limit * 2) + 3;
+ const list = (emojiSet || []);
+ return list.length > maxLen ? `${emojiSet.slice(0, limit).join('')}...${emojiSet.slice(-limit).join('')}` :
+ list.join('');
+ };
+ const EMOJIS = [
+ [128512], [9786], [129333, 8205, 9794, 65039], [9832], [9784], [9895], [8265], [8505], [127987, 65039, 8205, 9895, 65039], [129394], [9785], [9760], [129489, 8205, 129456], [129487, 8205, 9794, 65039], [9975], [129489, 8205, 129309, 8205, 129489], [9752], [9968], [9961], [9972], [9992], [9201], [9928], [9730], [9969], [9731], [9732], [9976], [9823], [9937], [9000], [9993], [9999],
+ [128105, 8205, 10084, 65039, 8205, 128139, 8205, 128104],
+ [128104, 8205, 128105, 8205, 128103, 8205, 128102],
+ [128104, 8205, 128105, 8205, 128102],
+ // android 11
+ [128512],
+ [169], [174], [8482],
+ [128065, 65039, 8205, 128488, 65039],
+ // other
+ [10002], [9986], [9935], [9874], [9876], [9881], [9939], [9879], [9904], [9905], [9888], [9762], [9763], [11014], [8599], [10145], [11013], [9883], [10017], [10013], [9766], [9654], [9197], [9199], [9167], [9792], [9794], [10006], [12336], [9877], [9884], [10004], [10035], [10055], [9724], [9642], [10083], [10084], [9996], [9757], [9997], [10052], [9878], [8618], [9775], [9770], [9774], [9745], [10036], [127344], [127359],
+ ].map((emojiCode) => String.fromCodePoint(...emojiCode));
+ const CSS_FONT_FAMILY = `
+ 'Segoe Fluent Icons',
+ 'Ink Free',
+ 'Bahnschrift',
+ 'Segoe MDL2 Assets',
+ 'HoloLens MDL2 Assets',
+ 'Leelawadee UI',
+ 'Javanese Text',
+ 'Segoe UI Emoji',
+ 'Aldhabi',
+ 'Gadugi',
+ 'Myanmar Text',
+ 'Nirmala UI',
+ 'Lucida Console',
+ 'Cambria Math',
+ 'Bai Jamjuree',
+ 'Chakra Petch',
+ 'Charmonman',
+ 'Fahkwang',
+ 'K2D',
+ 'Kodchasan',
+ 'KoHo',
+ 'Sarabun',
+ 'Srisakdi',
+ 'Galvji',
+ 'MuktaMahee Regular',
+ 'InaiMathi Bold',
+ 'American Typewriter Semibold',
+ 'Futura Bold',
+ 'SignPainter-HouseScript Semibold',
+ 'PingFang HK Light',
+ 'Kohinoor Devanagari Medium',
+ 'Luminari',
+ 'Geneva',
+ 'Helvetica Neue',
+ 'Droid Sans Mono',
+ 'Dancing Script',
+ 'Roboto',
+ 'Ubuntu',
+ 'Liberation Mono',
+ 'Source Code Pro',
+ 'DejaVu Sans',
+ 'OpenSymbol',
+ 'Chilanka',
+ 'Cousine',
+ 'Arimo',
+ 'Jomolhari',
+ 'MONO',
+ 'Noto Color Emoji',
+ sans-serif !important
+`;
+ const hashSlice = (x) => !x ? x : x.slice(0, 8);
+ function getGpuBrand(gpu) {
+ if (!gpu)
+ return null;
+ const gpuBrandMatcher = /(adreno|amd|apple|intel|llvm|mali|microsoft|nvidia|parallels|powervr|samsung|swiftshader|virtualbox|vmware)/i;
+ const brand = (/radeon/i.test(gpu) ? 'AMD' :
+ /geforce/i.test(gpu) ? 'NVIDIA' :
+ (gpuBrandMatcher.exec(gpu)?.[0] || 'other').toLocaleUpperCase());
+ return brand;
+ }
+ // collect fingerprints for analysis
+ const Analysis = {};
+ // use if needed to stable fingerprint
+ const LowerEntropy = {
+ AUDIO: false,
+ CANVAS: false,
+ FONTS: false,
+ SCREEN: false,
+ TIME_ZONE: false,
+ WEBGL: false,
+ };
+
+ // template views
+ function patch(oldEl, newEl, fn) {
+ if (!oldEl)
+ return null;
+ oldEl.parentNode?.replaceChild(newEl, oldEl);
+ return typeof fn === 'function' ? fn() : true;
+ }
+ function html(templateStr, ...expressionSet) {
+ const template = document.createElement('template');
+ template.innerHTML = templateStr.map((s, i) => `${s}${expressionSet[i] || ''}`).join('');
+ return document.importNode(template.content, true);
+ }
+ // template helpers
+ const HTMLNote = {
+ UNKNOWN: 'unknown ',
+ UNSUPPORTED: 'unsupported ',
+ BLOCKED: 'blocked ',
+ LIED: 'lied ',
+ SECRET: 'secret ',
+ };
+ const count = (arr) => arr && arr.constructor.name === 'Array' ? '' + (arr.length) : '0';
+ const getDiffs = ({ stringA, stringB, charDiff = false, decorate = (diff) => `[${diff}]` }) => {
+ if (!stringA || !stringB)
+ return;
+ const splitter = charDiff ? '' : ' ';
+ const listA = ('' + stringA).split(splitter);
+ const listB = ('' + stringB).split(splitter);
+ const listBWithDiffs = listB.map((x, i) => {
+ const matcher = listA[i];
+ const match = x == matcher;
+ return !match ? decorate(x) : x;
+ });
+ return listBWithDiffs.join(splitter);
+ };
+ // modal component
+ const modal = (name, result, linkname = 'details') => {
+ if (!result.length) {
+ return '';
+ }
+ return `
+
+
+ ${linkname}
+
+
+
+ ×
+ ${result}
+
+
+ `;
+ };
+
+ const createErrorsCaptured = () => {
+ const errors = [];
+ return {
+ getErrors: () => errors,
+ captureError: (error, customMessage = '') => {
+ const type = {
+ Error: true,
+ EvalError: true,
+ InternalError: true,
+ RangeError: true,
+ ReferenceError: true,
+ SyntaxError: true,
+ TypeError: true,
+ URIError: true,
+ InvalidStateError: true,
+ SecurityError: true,
+ };
+ const hasInnerSpace = (s) => /.+(\s).+/g.test(s); // ignore AOPR noise
+ console.error(error); // log error to educate
+ const { name, message } = error;
+ const trustedMessage = (!hasInnerSpace(message) ? undefined :
+ !customMessage ? message :
+ `${message} [${customMessage}]`);
+ const trustedName = type[name] ? name : undefined;
+ errors.push({ trustedName, trustedMessage });
+ return undefined;
+ },
+ };
+ };
+ const errorsCaptured = createErrorsCaptured();
+ const { captureError } = errorsCaptured;
+ const attempt = (fn, customMessage = '') => {
+ try {
+ return fn();
+ }
+ catch (error) {
+ if (customMessage) {
+ return captureError(error, customMessage);
+ }
+ return captureError(error);
+ }
+ };
+ const caniuse = (fn, objChainList = [], args = [], method = false) => {
+ let api;
+ try {
+ api = fn();
+ }
+ catch (error) {
+ return undefined;
+ }
+ let i;
+ const len = objChainList.length;
+ let chain = api;
+ try {
+ for (i = 0; i < len; i++) {
+ const obj = objChainList[i];
+ chain = chain[obj];
+ }
+ }
+ catch (error) {
+ return undefined;
+ }
+ return (method && args.length ? chain.apply(api, args) :
+ method && !args.length ? chain.apply(api) :
+ chain);
+ };
+ // Log performance time
+ const timer = (logStart) => {
+ let start = 0;
+ try {
+ start = performance.now();
+ }
+ catch (error) {
+ captureError(error);
+ }
+ return (logEnd) => {
+ let end = 0;
+ try {
+ end = performance.now() - start;
+ logEnd && console.log(`${logEnd}: ${end / 1000} seconds`);
+ return end;
+ }
+ catch (error) {
+ captureError(error);
+ return 0;
+ }
+ };
+ };
+ const getCapturedErrors = () => ({ data: errorsCaptured.getErrors() });
+
+ /* eslint-disable new-cap */
+ /* eslint-disable no-unused-vars */
+ // warm up while we detect lies
+ try {
+ speechSynthesis.getVoices();
+ }
+ catch (err) { }
+ // Collect lies detected
+ function createLieRecords() {
+ const records = {};
+ return {
+ getRecords: () => records,
+ documentLie: (name, lie) => {
+ const isArray = lie instanceof Array;
+ if (records[name]) {
+ if (isArray) {
+ return (records[name] = [...records[name], ...lie]);
+ }
+ return records[name].push(lie);
+ }
+ return isArray ? (records[name] = lie) : (records[name] = [lie]);
+ },
+ };
+ }
+ const lieRecords = createLieRecords();
+ const { documentLie } = lieRecords;
+ const GHOST = `
+ height: 100vh;
+ width: 100vw;
+ position: absolute;
+ left:-10000px;
+ visibility: hidden;
+`;
+ function getRandomValues() {
+ return (String.fromCharCode(Math.random() * 26 + 97) +
+ Math.random().toString(36).slice(-7));
+ }
+ function getBehemothIframe(win) {
+ try {
+ if (!IS_BLINK)
+ return win;
+ const div = win.document.createElement('div');
+ div.setAttribute('id', getRandomValues());
+ div.setAttribute('style', GHOST);
+ div.innerHTML = `
`;
+ win.document.body.appendChild(div);
+ const iframe = [...[...div.childNodes][0].childNodes][0];
+ if (!iframe)
+ return null;
+ const { contentWindow } = iframe || {};
+ if (!contentWindow)
+ return null;
+ const div2 = contentWindow.document.createElement('div');
+ div2.innerHTML = `
`;
+ contentWindow.document.body.appendChild(div2);
+ const iframe2 = [...[...div2.childNodes][0].childNodes][0];
+ return iframe2.contentWindow;
+ }
+ catch (error) {
+ captureError(error, 'client blocked behemoth iframe');
+ return win;
+ }
+ }
+ const RAND = getRandomValues();
+ const HAS_REFLECT = 'Reflect' in self;
+ function isTypeError(err) {
+ return err.constructor.name == 'TypeError';
+ }
+ function failsTypeError({ spawnErr, withStack, final }) {
+ try {
+ spawnErr();
+ throw Error();
+ }
+ catch (err) {
+ if (!isTypeError(err))
+ return true;
+ return withStack ? withStack(err) : false;
+ }
+ finally {
+ final && final();
+ }
+ }
+ function failsWithError(fn) {
+ try {
+ fn();
+ return false;
+ }
+ catch (err) {
+ return true;
+ }
+ }
+ function hasKnownToString(name) {
+ return {
+ [`function ${name}() { [native code] }`]: true,
+ [`function get ${name}() { [native code] }`]: true,
+ [`function () { [native code] }`]: true,
+ [`function ${name}() {${'\n'} [native code]${'\n'}}`]: true,
+ [`function get ${name}() {${'\n'} [native code]${'\n'}}`]: true,
+ [`function () {${'\n'} [native code]${'\n'}}`]: true,
+ };
+ }
+ function hasValidStack(err, reg, i = 1) {
+ if (i === 0)
+ return reg.test(err.message);
+ return reg.test(err.stack.split('\n')[i]);
+ }
+ const AT_FUNCTION = /at Function\.toString /;
+ const AT_OBJECT = /at Object\.toString/;
+ const FUNCTION_INSTANCE = /at (Function\.)?\[Symbol.hasInstance\]/; // useful if < Chrome 102
+ const PROXY_INSTANCE = /at (Proxy\.)?\[Symbol.hasInstance\]/; // useful if < Chrome 102
+ const STRICT_MODE = /strict mode/;
+ function queryLies({ scope, apiFunction, proto, obj, lieProps, }) {
+ if (typeof apiFunction != 'function') {
+ return {
+ lied: 0,
+ lieTypes: [],
+ };
+ }
+ const name = apiFunction.name.replace(/get\s/, '');
+ const objName = obj?.name;
+ const nativeProto = Object.getPrototypeOf(apiFunction);
+ let lies = {
+ // custom lie string names
+ ['failed illegal error']: !!obj && failsTypeError({
+ spawnErr: () => obj.prototype[name],
+ }),
+ ['failed undefined properties']: (!!obj && /^(screen|navigator)$/i.test(objName) && !!(Object.getOwnPropertyDescriptor(self[objName.toLowerCase()], name) || (HAS_REFLECT &&
+ Reflect.getOwnPropertyDescriptor(self[objName.toLowerCase()], name)))),
+ ['failed call interface error']: failsTypeError({
+ spawnErr: () => {
+ // @ts-expect-error
+ new apiFunction();
+ apiFunction.call(proto);
+ },
+ }),
+ ['failed apply interface error']: failsTypeError({
+ spawnErr: () => {
+ // @ts-expect-error
+ new apiFunction();
+ apiFunction.apply(proto);
+ },
+ }),
+ ['failed new instance error']: failsTypeError({
+ // @ts-expect-error
+ spawnErr: () => new apiFunction(),
+ }),
+ ['failed class extends error']: !IS_WEBKIT && failsTypeError({
+ spawnErr: () => {
+ // @ts-expect-error
+ class Fake extends apiFunction {
+ }
+ },
+ }),
+ ['failed null conversion error']: failsTypeError({
+ spawnErr: () => Object.setPrototypeOf(apiFunction, null).toString(),
+ final: () => Object.setPrototypeOf(apiFunction, nativeProto),
+ }),
+ ['failed toString']: (!hasKnownToString(name)[scope.Function.prototype.toString.call(apiFunction)] ||
+ !hasKnownToString('toString')[scope.Function.prototype.toString.call(apiFunction.toString)]),
+ ['failed "prototype" in function']: 'prototype' in apiFunction,
+ ['failed descriptor']: !!(Object.getOwnPropertyDescriptor(apiFunction, 'arguments') ||
+ Reflect.getOwnPropertyDescriptor(apiFunction, 'arguments') ||
+ Object.getOwnPropertyDescriptor(apiFunction, 'caller') ||
+ Reflect.getOwnPropertyDescriptor(apiFunction, 'caller') ||
+ Object.getOwnPropertyDescriptor(apiFunction, 'prototype') ||
+ Reflect.getOwnPropertyDescriptor(apiFunction, 'prototype') ||
+ Object.getOwnPropertyDescriptor(apiFunction, 'toString') ||
+ Reflect.getOwnPropertyDescriptor(apiFunction, 'toString')),
+ ['failed own property']: !!(apiFunction.hasOwnProperty('arguments') ||
+ apiFunction.hasOwnProperty('caller') ||
+ apiFunction.hasOwnProperty('prototype') ||
+ apiFunction.hasOwnProperty('toString')),
+ ['failed descriptor keys']: (Object.keys(Object.getOwnPropertyDescriptors(apiFunction)).sort().toString() != 'length,name'),
+ ['failed own property names']: (Object.getOwnPropertyNames(apiFunction).sort().toString() != 'length,name'),
+ ['failed own keys names']: HAS_REFLECT && (Reflect.ownKeys(apiFunction).sort().toString() != 'length,name'),
+ // Proxy Detection
+ ['failed object toString error']: (failsTypeError({
+ spawnErr: () => Object.create(apiFunction).toString(),
+ withStack: (err) => IS_BLINK && !hasValidStack(err, AT_FUNCTION),
+ }) ||
+ failsTypeError({
+ spawnErr: () => Object.create(new Proxy(apiFunction, {})).toString(),
+ withStack: (err) => IS_BLINK && !hasValidStack(err, AT_OBJECT),
+ })),
+ ['failed at incompatible proxy error']: failsTypeError({
+ spawnErr: () => {
+ apiFunction.arguments;
+ apiFunction.caller;
+ },
+ withStack: (err) => IS_GECKO && !hasValidStack(err, STRICT_MODE, 0),
+ }),
+ ['failed at toString incompatible proxy error']: failsTypeError({
+ spawnErr: () => {
+ apiFunction.toString.arguments;
+ apiFunction.toString.caller;
+ },
+ withStack: (err) => IS_GECKO && !hasValidStack(err, STRICT_MODE, 0),
+ }),
+ ['failed at too much recursion error']: failsTypeError({
+ spawnErr: () => {
+ Object.setPrototypeOf(apiFunction, Object.create(apiFunction)).toString();
+ },
+ final: () => Object.setPrototypeOf(apiFunction, nativeProto),
+ }),
+ };
+ // conditionally increase difficulty
+ const detectProxies = (name == 'toString' ||
+ !!lieProps['Function.toString'] ||
+ !!lieProps['Permissions.query']);
+ if (detectProxies) {
+ const proxy1 = new Proxy(apiFunction, {});
+ const proxy2 = new Proxy(apiFunction, {});
+ const proxy3 = new Proxy(apiFunction, {});
+ lies = {
+ ...lies,
+ // Advanced Proxy Detection
+ ['failed at too much recursion __proto__ error']: !failsTypeError({
+ spawnErr: () => {
+ // @ts-expect-error
+ apiFunction.__proto__ = proxy;
+ apiFunction++;
+ },
+ final: () => Object.setPrototypeOf(apiFunction, nativeProto),
+ }),
+ ['failed at chain cycle error']: !failsTypeError({
+ spawnErr: () => {
+ Object.setPrototypeOf(proxy1, Object.create(proxy1)).toString();
+ },
+ final: () => Object.setPrototypeOf(proxy1, nativeProto),
+ }),
+ ['failed at chain cycle __proto__ error']: !failsTypeError({
+ spawnErr: () => {
+ // @ts-expect-error
+ proxy2.__proto__ = proxy2;
+ proxy2++;
+ },
+ final: () => Object.setPrototypeOf(proxy2, nativeProto),
+ }),
+ ['failed at reflect set proto']: HAS_REFLECT && failsTypeError({
+ spawnErr: () => {
+ Reflect.setPrototypeOf(apiFunction, Object.create(apiFunction));
+ RAND in apiFunction;
+ throw new TypeError();
+ },
+ final: () => Object.setPrototypeOf(apiFunction, nativeProto),
+ }),
+ ['failed at reflect set proto proxy']: HAS_REFLECT && !failsTypeError({
+ spawnErr: () => {
+ Reflect.setPrototypeOf(proxy3, Object.create(proxy3));
+ RAND in proxy3;
+ },
+ final: () => Object.setPrototypeOf(proxy3, nativeProto),
+ }),
+ ['failed at instanceof check error']: IS_BLINK && (failsTypeError({
+ spawnErr: () => {
+ apiFunction instanceof apiFunction;
+ },
+ withStack: (err) => !hasValidStack(err, FUNCTION_INSTANCE),
+ }) ||
+ failsTypeError({
+ spawnErr: () => {
+ const proxy = new Proxy(apiFunction, {});
+ proxy instanceof proxy;
+ },
+ withStack: (err) => !hasValidStack(err, PROXY_INSTANCE),
+ })),
+ ['failed at define properties']: IS_BLINK && HAS_REFLECT && failsWithError(() => {
+ Object.defineProperty(apiFunction, '', { configurable: true }).toString();
+ Reflect.deleteProperty(apiFunction, '');
+ }),
+ };
+ }
+ const lieTypes = Object.keys(lies).filter((key) => !!lies[key]);
+ return {
+ lied: lieTypes.length,
+ lieTypes,
+ };
+ }
+ function createLieDetector(scope) {
+ const isSupported = (obj) => typeof obj != 'undefined' && !!obj;
+ const props = {}; // lie list and detail
+ const propsSearched = []; // list of properties searched
+ return {
+ getProps: () => props,
+ getPropsSearched: () => propsSearched,
+ searchLies: (fn, config) => {
+ const { target, ignore } = config || {};
+ let obj;
+ // check if api is blocked or not supported
+ try {
+ obj = fn();
+ if (!isSupported(obj)) {
+ return;
+ }
+ }
+ catch (error) {
+ return;
+ }
+ const interfaceObject = !!obj.prototype ? obj.prototype : obj;
+ [...new Set([
+ ...Object.getOwnPropertyNames(interfaceObject),
+ ...Object.keys(interfaceObject), // backup
+ ])].sort().forEach((name) => {
+ const skip = (name == 'constructor' ||
+ (target && !new Set(target).has(name)) ||
+ (ignore && new Set(ignore).has(name)));
+ if (skip)
+ return;
+ const objectNameString = /\s(.+)\]/;
+ const apiName = `${obj.name ? obj.name :
+ objectNameString.test(obj) ? objectNameString.exec(obj)?.[1] :
+ undefined}.${name}`;
+ propsSearched.push(apiName);
+ try {
+ const proto = obj.prototype ? obj.prototype : obj;
+ let res; // response from getLies
+ // search if function
+ try {
+ const apiFunction = proto[name]; // may trigger TypeError
+ if (typeof apiFunction == 'function') {
+ res = queryLies({
+ scope,
+ apiFunction: proto[name],
+ proto,
+ obj: null,
+ lieProps: props,
+ });
+ if (res.lied) {
+ documentLie(apiName, res.lieTypes);
+ return (props[apiName] = res.lieTypes);
+ }
+ return;
+ }
+ // since there is no TypeError and the typeof is not a function,
+ // handle invalid values and ignore name, length, and constants
+ if (name != 'name' &&
+ name != 'length' &&
+ name[0] !== name[0].toUpperCase()) {
+ const lie = ['failed descriptor.value undefined'];
+ documentLie(apiName, lie);
+ return (props[apiName] = lie);
+ }
+ }
+ catch (error) { }
+ // else search getter function
+ // @ts-ignore
+ const getterFunction = Object.getOwnPropertyDescriptor(proto, name).get;
+ res = queryLies({
+ scope,
+ apiFunction: getterFunction,
+ proto,
+ obj,
+ lieProps: props,
+ }); // send the obj for special tests
+ if (res.lied) {
+ documentLie(apiName, res.lieTypes);
+ return (props[apiName] = res.lieTypes);
+ }
+ return;
+ }
+ catch (error) {
+ const lie = `failed prototype test execution`;
+ documentLie(apiName, lie);
+ return (props[apiName] = [lie]);
+ }
+ });
+ },
+ };
+ }
+ function getPhantomIframe() {
+ if (IS_WORKER_SCOPE)
+ return { iframeWindow: self };
+ try {
+ const numberOfIframes = self.length;
+ const frag = new DocumentFragment();
+ const div = document.createElement('div');
+ const id = getRandomValues();
+ div.setAttribute('id', id);
+ frag.appendChild(div);
+ div.innerHTML = `
`;
+ document.body.appendChild(frag);
+ const iframeWindow = self[numberOfIframes];
+ const phantomWindow = getBehemothIframe(iframeWindow);
+ return { iframeWindow: phantomWindow || self, div };
+ }
+ catch (error) {
+ captureError(error, 'client blocked phantom iframe');
+ return { iframeWindow: self };
+ }
+ }
+ const { iframeWindow: PHANTOM_DARKNESS, div: PARENT_PHANTOM } = getPhantomIframe() || {};
+ function getPrototypeLies(scope) {
+ const lieDetector = createLieDetector(scope);
+ const { searchLies, } = lieDetector;
+ // search lies: remove target to search all properties
+ // test Function.toString first to determine the depth of the search
+ searchLies(() => Function, {
+ target: [
+ 'toString',
+ ],
+ ignore: [
+ 'caller',
+ 'arguments',
+ ],
+ });
+ // other APIs
+ searchLies(() => AnalyserNode);
+ searchLies(() => AudioBuffer, {
+ target: [
+ 'copyFromChannel',
+ 'getChannelData',
+ ],
+ });
+ searchLies(() => BiquadFilterNode, {
+ target: [
+ 'getFrequencyResponse',
+ ],
+ });
+ searchLies(() => CanvasRenderingContext2D, {
+ target: [
+ 'getImageData',
+ 'getLineDash',
+ 'isPointInPath',
+ 'isPointInStroke',
+ 'measureText',
+ 'quadraticCurveTo',
+ 'fillText',
+ 'strokeText',
+ 'font',
+ ],
+ });
+ searchLies(() => CSSStyleDeclaration, {
+ target: [
+ 'setProperty',
+ ],
+ });
+ // @ts-expect-error
+ searchLies(() => CSS2Properties, {
+ target: [
+ 'setProperty',
+ ],
+ });
+ searchLies(() => Date, {
+ target: [
+ 'getDate',
+ 'getDay',
+ 'getFullYear',
+ 'getHours',
+ 'getMinutes',
+ 'getMonth',
+ 'getTime',
+ 'getTimezoneOffset',
+ 'setDate',
+ 'setFullYear',
+ 'setHours',
+ 'setMilliseconds',
+ 'setMonth',
+ 'setSeconds',
+ 'setTime',
+ 'toDateString',
+ 'toJSON',
+ 'toLocaleDateString',
+ 'toLocaleString',
+ 'toLocaleTimeString',
+ 'toString',
+ 'toTimeString',
+ 'valueOf',
+ ],
+ });
+ // @ts-expect-error if not supported
+ searchLies(() => GPU, {
+ target: [
+ 'requestAdapter',
+ ],
+ });
+ // @ts-expect-error if not supported
+ searchLies(() => GPUAdapter, {
+ target: [
+ 'requestAdapterInfo',
+ ],
+ });
+ searchLies(() => Intl.DateTimeFormat, {
+ target: [
+ 'format',
+ 'formatRange',
+ 'formatToParts',
+ 'resolvedOptions',
+ ],
+ });
+ searchLies(() => Document, {
+ target: [
+ 'createElement',
+ 'createElementNS',
+ 'getElementById',
+ 'getElementsByClassName',
+ 'getElementsByName',
+ 'getElementsByTagName',
+ 'getElementsByTagNameNS',
+ 'referrer',
+ 'write',
+ 'writeln',
+ ],
+ ignore: [
+ // Gecko
+ 'onreadystatechange',
+ 'onmouseenter',
+ 'onmouseleave',
+ ],
+ });
+ searchLies(() => DOMRect);
+ searchLies(() => DOMRectReadOnly);
+ searchLies(() => Element, {
+ target: [
+ 'append',
+ 'appendChild',
+ 'getBoundingClientRect',
+ 'getClientRects',
+ 'insertAdjacentElement',
+ 'insertAdjacentHTML',
+ 'insertAdjacentText',
+ 'insertBefore',
+ 'prepend',
+ 'replaceChild',
+ 'replaceWith',
+ 'setAttribute',
+ ],
+ });
+ searchLies(() => FontFace, {
+ target: [
+ 'family',
+ 'load',
+ 'status',
+ ],
+ });
+ searchLies(() => HTMLCanvasElement);
+ searchLies(() => HTMLElement, {
+ target: [
+ 'clientHeight',
+ 'clientWidth',
+ 'offsetHeight',
+ 'offsetWidth',
+ 'scrollHeight',
+ 'scrollWidth',
+ ],
+ ignore: [
+ // Gecko
+ 'onmouseenter',
+ 'onmouseleave',
+ ],
+ });
+ searchLies(() => HTMLIFrameElement, {
+ target: [
+ 'contentDocument',
+ 'contentWindow',
+ ],
+ });
+ searchLies(() => IntersectionObserverEntry, {
+ target: [
+ 'boundingClientRect',
+ 'intersectionRect',
+ 'rootBounds',
+ ],
+ });
+ searchLies(() => Math, {
+ target: [
+ 'acos',
+ 'acosh',
+ 'asinh',
+ 'atan',
+ 'atan2',
+ 'atanh',
+ 'cbrt',
+ 'cos',
+ 'cosh',
+ 'exp',
+ 'expm1',
+ 'log',
+ 'log10',
+ 'log1p',
+ 'sin',
+ 'sinh',
+ 'sqrt',
+ 'tan',
+ 'tanh',
+ ],
+ });
+ searchLies(() => MediaDevices, {
+ target: [
+ 'enumerateDevices',
+ 'getDisplayMedia',
+ 'getUserMedia',
+ ],
+ });
+ searchLies(() => Navigator, {
+ target: [
+ 'appCodeName',
+ 'appName',
+ 'appVersion',
+ 'buildID',
+ 'connection',
+ 'deviceMemory',
+ 'getBattery',
+ 'getGamepads',
+ 'getVRDisplays',
+ 'hardwareConcurrency',
+ 'language',
+ 'languages',
+ 'maxTouchPoints',
+ 'mimeTypes',
+ 'oscpu',
+ 'platform',
+ 'plugins',
+ 'product',
+ 'productSub',
+ 'sendBeacon',
+ 'serviceWorker',
+ 'storage',
+ 'userAgent',
+ 'vendor',
+ 'vendorSub',
+ 'webdriver',
+ 'gpu',
+ ],
+ });
+ searchLies(() => Node, {
+ target: [
+ 'appendChild',
+ 'insertBefore',
+ 'replaceChild',
+ ],
+ });
+ // @ts-expect-error
+ searchLies(() => OffscreenCanvas, {
+ target: [
+ 'convertToBlob',
+ 'getContext',
+ ],
+ });
+ // @ts-expect-error
+ searchLies(() => OffscreenCanvasRenderingContext2D, {
+ target: [
+ 'getImageData',
+ 'getLineDash',
+ 'isPointInPath',
+ 'isPointInStroke',
+ 'measureText',
+ 'quadraticCurveTo',
+ 'font',
+ ],
+ });
+ searchLies(() => Permissions, {
+ target: [
+ 'query',
+ ],
+ });
+ searchLies(() => Range, {
+ target: [
+ 'getBoundingClientRect',
+ 'getClientRects',
+ ],
+ });
+ // @ts-expect-error
+ searchLies(() => Intl.RelativeTimeFormat, {
+ target: [
+ 'resolvedOptions',
+ ],
+ });
+ searchLies(() => Screen);
+ searchLies(() => speechSynthesis, {
+ target: [
+ 'getVoices',
+ ],
+ });
+ searchLies(() => String, {
+ target: [
+ 'fromCodePoint',
+ ],
+ });
+ searchLies(() => StorageManager, {
+ target: [
+ 'estimate',
+ ],
+ });
+ searchLies(() => SVGRect);
+ searchLies(() => SVGRectElement, {
+ target: [
+ 'getBBox',
+ ],
+ });
+ searchLies(() => SVGTextContentElement, {
+ target: [
+ 'getExtentOfChar',
+ 'getSubStringLength',
+ 'getComputedTextLength',
+ ],
+ });
+ searchLies(() => TextMetrics);
+ searchLies(() => WebGLRenderingContext, {
+ target: [
+ 'bufferData',
+ 'getParameter',
+ 'readPixels',
+ ],
+ });
+ searchLies(() => WebGL2RenderingContext, {
+ target: [
+ 'bufferData',
+ 'getParameter',
+ 'readPixels',
+ ],
+ });
+ /* potential targets:
+ RTCPeerConnection
+ Plugin
+ PluginArray
+ MimeType
+ MimeTypeArray
+ Worker
+ History
+ */
+ // return lies list and detail
+ const props = lieDetector.getProps();
+ const propsSearched = lieDetector.getPropsSearched();
+ return {
+ lieDetector,
+ lieList: Object.keys(props).sort(),
+ lieDetail: props,
+ lieCount: Object.keys(props).reduce((acc, key) => acc + props[key].length, 0),
+ propsSearched,
+ };
+ }
+ // start program
+ const start = performance.now();
+ const { lieDetector, lieList, lieDetail,
+ // lieCount,
+ propsSearched, } = getPrototypeLies(PHANTOM_DARKNESS); // execute and destructure the list and detail
+ // disregard Function.prototype.toString lies when determining if the API can be trusted
+ const getNonFunctionToStringLies = (x) => !x ? x : x.filter((x) => !/object toString|toString incompatible proxy/.test(x)).length;
+ let lieProps;
+ let prototypeLies;
+ let PROTO_BENCHMARK = 0;
+ if (!IS_WORKER_SCOPE) {
+ lieProps = (() => {
+ const props = lieDetector.getProps();
+ return Object.keys(props).reduce((acc, key) => {
+ acc[key] = getNonFunctionToStringLies(props[key]);
+ return acc;
+ }, {});
+ })();
+ prototypeLies = JSON.parse(JSON.stringify(lieDetail));
+ const perf = performance.now() - start;
+ PROTO_BENCHMARK = +perf.toFixed(2);
+ const message = `${propsSearched.length} API properties analyzed in ${PROTO_BENCHMARK}ms (${lieList.length} corrupted)`;
+ setTimeout(() => console.log(message), 3000);
+ }
+ const getPluginLies = (plugins, mimeTypes) => {
+ const lies = []; // collect lie types
+ const pluginsOwnPropertyNames = Object.getOwnPropertyNames(plugins).filter((name) => isNaN(+name));
+ const mimeTypesOwnPropertyNames = Object.getOwnPropertyNames(mimeTypes).filter((name) => isNaN(+name));
+ // cast to array
+ const pluginsList = [...plugins];
+ const mimeTypesList = [...mimeTypes];
+ // get initial trusted mimeType names
+ const trustedMimeTypes = new Set(mimeTypesOwnPropertyNames);
+ // get initial trusted plugin names
+ const excludeDuplicates = (arr) => [...new Set(arr)];
+ const mimeTypeEnabledPlugins = excludeDuplicates(mimeTypesList.map((mimeType) => mimeType.enabledPlugin));
+ const trustedPluginNames = new Set(pluginsOwnPropertyNames);
+ const mimeTypeEnabledPluginsNames = mimeTypeEnabledPlugins.map((plugin) => plugin && plugin.name);
+ const trustedPluginNamesArray = [...trustedPluginNames];
+ trustedPluginNamesArray.forEach((name) => {
+ const validName = new Set(mimeTypeEnabledPluginsNames).has(name);
+ if (!validName) {
+ trustedPluginNames.delete(name);
+ }
+ });
+ // 3. Expect MimeType object in plugins
+ const invalidPlugins = pluginsList.filter((plugin) => {
+ try {
+ const validMimeType = Object.getPrototypeOf(plugin[0]).constructor.name == 'MimeType';
+ if (!validMimeType) {
+ trustedPluginNames.delete(plugin.name);
+ }
+ return !validMimeType;
+ }
+ catch (error) {
+ trustedPluginNames.delete(plugin.name);
+ return true; // sign of tampering
+ }
+ });
+ if (invalidPlugins.length) {
+ lies.push('missing mimetype');
+ }
+ // 4. Expect valid MimeType(s) in plugin
+ const pluginMimeTypes = pluginsList
+ .map((plugin) => Object.values(plugin)).flat();
+ const pluginMimeTypesNames = pluginMimeTypes.map((mimetype) => mimetype.type);
+ pluginMimeTypesNames.forEach((name) => {
+ const validName = trustedMimeTypes.has(name);
+ if (!validName) {
+ trustedMimeTypes.delete(name);
+ }
+ });
+ pluginsList.forEach((plugin) => {
+ const pluginMimeTypes = Object.values(plugin).map((mimetype) => mimetype.type);
+ return pluginMimeTypes.forEach((mimetype) => {
+ if (!trustedMimeTypes.has(mimetype)) {
+ lies.push('invalid mimetype');
+ return trustedPluginNames.delete(plugin.name);
+ }
+ return;
+ });
+ });
+ return {
+ validPlugins: pluginsList.filter((plugin) => trustedPluginNames.has(plugin.name)),
+ validMimeTypes: mimeTypesList.filter((mimeType) => trustedMimeTypes.has(mimeType.type)),
+ lies: [...new Set(lies)], // remove duplicates
+ };
+ };
+ const getLies = () => {
+ const records = lieRecords.getRecords();
+ const totalLies = Object.keys(records).reduce((acc, key) => {
+ acc += records[key].length;
+ return acc;
+ }, 0);
+ return { data: records, totalLies };
+ };
+
+ // Detect proxy behavior
+ const proxyBehavior = (x) => typeof x == 'function' ? true : false;
+ const GIBBERS = /[cC]f|[jJ][bcdfghlmprsty]|[qQ][bcdfghjklmnpsty]|[vV][bfhjkmpt]|[xX][dkrz]|[yY]y|[zZ][fr]|[cCxXzZ]j|[bBfFgGjJkKpPvVqQtTwWyYzZ]q|[cCfFgGjJpPqQwW]v|[jJqQvV]w|[bBcCdDfFgGhHjJkKmMpPqQsSvVwWxXzZ]x|[bBfFhHjJkKmMpPqQ]z/g;
+ // Detect gibberish
+ const gibberish = (str, { strict = false } = {}) => {
+ if (!str)
+ return [];
+ // test letter case sequence
+ const letterCaseSequenceGibbers = [];
+ const tests = [
+ /([A-Z]{3,}[a-z])/g, // ABCd
+ /([a-z][A-Z]{3,})/g, // aBCD
+ /([a-z][A-Z]{2,}[a-z])/g, // aBC...z
+ /([a-z][\d]{2,}[a-z])/g, // a##...b
+ /([A-Z][\d]{2,}[a-z])/g, // A##...b
+ /([a-z][\d]{2,}[A-Z])/g, // a##...B
+ ];
+ tests.forEach((regExp) => {
+ const match = str.match(regExp);
+ if (match) {
+ return letterCaseSequenceGibbers.push(match.join(', '));
+ }
+ return;
+ });
+ // test letter sequence
+ const letterSequenceGibbers = [];
+ const clean = str.replace(/\d|\W|_/g, ' ').replace(/\s+/g, ' ').trim().split(' ').join('_');
+ const len = clean.length;
+ const arr = [...clean];
+ arr.forEach((char, index) => {
+ const nextIndex = index + 1;
+ const nextChar = arr[nextIndex];
+ const isWordSequence = nextChar !== '_' && char !== '_' && nextIndex !== len;
+ if (isWordSequence) {
+ const combo = char + nextChar;
+ if (GIBBERS.test(combo))
+ letterSequenceGibbers.push(combo);
+ }
+ });
+ const gibbers = [
+ // ignore sequence if less than 3 exist
+ ...(!strict && (letterSequenceGibbers.length < 3) ? [] : letterSequenceGibbers),
+ ...(!strict && (letterCaseSequenceGibbers.length < 4) ? [] : letterCaseSequenceGibbers),
+ ];
+ const allow = [
+ // known gibbers
+ 'bz',
+ 'cf',
+ 'fx',
+ 'mx',
+ 'vb',
+ 'xd',
+ 'gx',
+ 'PCIe',
+ 'vm',
+ 'NVIDIAGa',
+ ];
+ return gibbers.filter((x) => !allow.includes(x));
+ };
+ // WebGL Renderer helpers
+ function compressWebGLRenderer(x) {
+ if (!x)
+ return;
+ return ('' + x)
+ .replace(/ANGLE \(|\sDirect3D.+|\sD3D.+|\svs_.+\)|\((DRM|POLARIS|LLVM).+|Mesa.+|(ATI|INTEL)-.+|Metal\s-\s.+|NVIDIA\s[\d|\.]+/ig, '')
+ .replace(/(\s(ti|\d{1,2}GB|super)$)/ig, '')
+ .replace(/\s{2,}/g, ' ')
+ .trim()
+ .replace(/((r|g)(t|)(x|s|\d) |Graphics |GeForce |Radeon (HD |Pro |))(\d+)/i, (...args) => {
+ return `${args[1]}${args[6][0]}${args[6].slice(1).replace(/\d/g, '0')}s`;
+ });
+ }
+ const getWebGLRendererParts = (x) => {
+ const knownParts = [
+ 'AMD',
+ 'ANGLE',
+ 'ASUS',
+ 'ATI',
+ 'ATI Radeon',
+ 'ATI Technologies Inc',
+ 'Adreno',
+ 'Android Emulator',
+ 'Apple',
+ 'Apple GPU',
+ 'Apple M1',
+ 'Chipset',
+ 'D3D11',
+ 'Direct3D',
+ 'Express Chipset',
+ 'GeForce',
+ 'Generation',
+ 'Generic Renderer',
+ 'Google',
+ 'Google SwiftShader',
+ 'Graphics',
+ 'Graphics Media Accelerator',
+ 'HD Graphics Family',
+ 'Intel',
+ 'Intel(R) HD Graphics',
+ 'Intel(R) UHD Graphics',
+ 'Iris',
+ 'KBL Graphics',
+ 'Mali',
+ 'Mesa',
+ 'Mesa DRI',
+ 'Metal',
+ 'Microsoft',
+ 'Microsoft Basic Render Driver',
+ 'Microsoft Corporation',
+ 'NVIDIA',
+ 'NVIDIA Corporation',
+ 'NVIDIAGameReadyD3D',
+ 'OpenGL',
+ 'OpenGL Engine',
+ 'Open Source Technology Center',
+ 'Parallels',
+ 'Parallels Display Adapter',
+ 'PCIe',
+ 'Plus Graphics',
+ 'PowerVR',
+ 'Pro Graphics',
+ 'Quadro',
+ 'Radeon',
+ 'Radeon Pro',
+ 'Radeon Pro Vega',
+ 'Samsung',
+ 'SSE2',
+ 'VMware',
+ 'VMware SVGA 3D',
+ 'Vega',
+ 'VirtualBox',
+ 'VirtualBox Graphics Adapter',
+ 'Vulkan',
+ 'Xe Graphics',
+ 'llvmpipe',
+ ];
+ const parts = [...knownParts].filter((name) => ('' + x).includes(name));
+ return [...new Set(parts)].sort().join(', ');
+ };
+ const getWebGLRendererConfidence = (x) => {
+ if (!x) {
+ return;
+ }
+ const parts = getWebGLRendererParts(x);
+ const hasKnownParts = parts.length;
+ const hasBlankSpaceNoise = /\s{2,}|^\s|\s$/.test(x);
+ const hasBrokenAngleStructure = /^ANGLE/.test(x) && !(/^ANGLE \((.+)\)/.exec(x) || [])[1];
+ // https://chromium.googlesource.com/angle/angle/+/83fa18905d8fed4f394e4f30140a83a3e76b1577/src/gpu_info_util/SystemInfo.cpp
+ // https://chromium.googlesource.com/angle/angle/+/83fa18905d8fed4f394e4f30140a83a3e76b1577/src/gpu_info_util/SystemInfo.h
+ // https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/gl/gl_version_info.cc
+ /*
+ const knownVendors = [
+ 'AMD',
+ 'ARM',
+ 'Broadcom',
+ 'Google',
+ 'ImgTec',
+ 'Intel',
+ 'Kazan',
+ 'NVIDIA',
+ 'Qualcomm',
+ 'VeriSilicon',
+ 'Vivante',
+ 'VMWare',
+ 'Apple',
+ 'Unknown'
+ ]
+ const angle = {
+ vendorId: (/^ANGLE \(([^,]+),/.exec(x)||[])[1] || knownVendors.find(vendor => x.includes(vendor)),
+ deviceId: (
+ (x.match(/,/g)||[]).length == 2 ? (/^ANGLE \(([^,]+), ([^,]+)[,|\)]/.exec(x)||[])[2] :
+ (/^ANGLE \(([^,]+), ([^,]+)[,|\)]/.exec(x)||[])[1] || (/^ANGLE \((.+)\)$/.exec(x)||[])[1]
+ ).replace(/\sDirect3D.+/, '')
+ }
+ */
+ const gibbers = gibberish(x, { strict: true }).join(', ');
+ const valid = (hasKnownParts && !hasBlankSpaceNoise && !hasBrokenAngleStructure);
+ const confidence = (valid && !gibbers.length ? 'high' :
+ valid && gibbers.length ? 'moderate' :
+ 'low');
+ const grade = (confidence == 'high' ? 'A' :
+ confidence == 'moderate' ? 'C' :
+ 'F');
+ const warnings = new Set([
+ (hasBlankSpaceNoise ? 'found extra spaces' : undefined),
+ (hasBrokenAngleStructure ? 'broken angle structure' : undefined),
+ ]);
+ warnings.delete(undefined);
+ return {
+ parts,
+ warnings: [...warnings],
+ gibbers,
+ confidence,
+ grade,
+ };
+ };
+ // Collect trash values
+ const createTrashBin = () => {
+ const bin = [];
+ return {
+ getBin: () => bin,
+ sendToTrash: (name, val, response = undefined) => {
+ const proxyLike = proxyBehavior(val);
+ const value = !proxyLike ? val : 'proxy behavior detected';
+ bin.push({ name, value });
+ return response;
+ },
+ };
+ };
+ const trashBin = createTrashBin();
+ const { sendToTrash } = trashBin;
+ const getTrash = () => ({ trashBin: trashBin.getBin() });
+
+ function isFontOSBad(userAgentOS, fonts) {
+ if (!userAgentOS || !fonts || !fonts.length)
+ return false;
+ const fontMap = fonts.reduce((acc, x) => {
+ acc[x] = true;
+ return acc;
+ }, {});
+ const isLikeWindows = ('Cambria Math' in fontMap ||
+ 'Nirmala UI' in fontMap ||
+ 'Leelawadee UI' in fontMap ||
+ 'HoloLens MDL2 Assets' in fontMap ||
+ 'Segoe Fluent Icons' in fontMap);
+ const isLikeApple = ('Helvetica Neue' in fontMap ||
+ 'Luminari' in fontMap ||
+ 'PingFang HK Light' in fontMap ||
+ 'InaiMathi Bold' in fontMap ||
+ 'Galvji' in fontMap ||
+ 'Chakra Petch' in fontMap);
+ const isLikeLinux = ('Arimo' in fontMap ||
+ 'MONO' in fontMap ||
+ 'Ubuntu' in fontMap ||
+ 'Noto Color Emoji' in fontMap ||
+ 'Dancing Script' in fontMap ||
+ 'Droid Sans Mono' in fontMap);
+ if (isLikeWindows && userAgentOS != "Windows" /* PlatformClassifier.WINDOWS */) {
+ return true;
+ }
+ else if (isLikeApple && userAgentOS != "Apple" /* PlatformClassifier.APPLE */) {
+ return true;
+ }
+ else if (isLikeLinux && userAgentOS != "Linux" /* PlatformClassifier.LINUX */) {
+ return true;
+ }
+ return false;
+ }
+
+ let WORKER_TYPE = '';
+ let WORKER_NAME = '';
+ async function spawnWorker() {
+ const ask = (fn) => {
+ try {
+ return fn();
+ }
+ catch (e) {
+ return;
+ }
+ };
+ function getWorkerPrototypeLies(scope) {
+ const lieDetector = createLieDetector(scope);
+ const { searchLies, } = lieDetector;
+ searchLies(() => Function, {
+ target: [
+ 'toString',
+ ],
+ ignore: [
+ 'caller',
+ 'arguments',
+ ],
+ });
+ // @ts-expect-error
+ searchLies(() => WorkerNavigator, {
+ target: [
+ 'deviceMemory',
+ 'hardwareConcurrency',
+ 'language',
+ 'languages',
+ 'platform',
+ 'userAgent',
+ ],
+ });
+ // return lies list and detail
+ const props = lieDetector.getProps();
+ const propsSearched = lieDetector.getPropsSearched();
+ return {
+ lieDetector,
+ lieList: Object.keys(props).sort(),
+ lieDetail: props,
+ lieCount: Object.keys(props).reduce((acc, key) => acc + props[key].length, 0),
+ propsSearched,
+ };
+ }
+ const getUserAgentData = async (navigator) => {
+ if (!('userAgentData' in navigator)) {
+ return;
+ }
+ const data = await navigator.userAgentData.getHighEntropyValues(['platform', 'platformVersion', 'architecture', 'bitness', 'model', 'uaFullVersion']);
+ const { brands, mobile } = navigator.userAgentData || {};
+ const compressedBrands = (brands, captureVersion = false) => brands
+ .filter((obj) => !/Not/.test(obj.brand)).map((obj) => `${obj.brand}${captureVersion ? ` ${obj.version}` : ''}`);
+ const removeChromium = (brands) => (brands.length > 1 ? brands.filter((brand) => !/Chromium/.test(brand)) : brands);
+ // compress brands
+ if (!data.brands) {
+ data.brands = brands;
+ }
+ data.brandsVersion = compressedBrands(data.brands, true);
+ data.brands = compressedBrands(data.brands);
+ data.brandsVersion = removeChromium(data.brandsVersion);
+ data.brands = removeChromium(data.brands);
+ if (!data.mobile) {
+ data.mobile = mobile;
+ }
+ const dataSorted = Object.keys(data).sort().reduce((acc, key) => {
+ acc[key] = data[key];
+ return acc;
+ }, {});
+ return dataSorted;
+ };
+ const getWebglData = () => ask(() => {
+ // @ts-ignore
+ const canvasOffscreenWebgl = new OffscreenCanvas(256, 256);
+ const contextWebgl = canvasOffscreenWebgl.getContext('webgl');
+ const rendererInfo = contextWebgl.getExtension('WEBGL_debug_renderer_info');
+ return {
+ webglVendor: contextWebgl.getParameter(rendererInfo.UNMASKED_VENDOR_WEBGL),
+ webglRenderer: contextWebgl.getParameter(rendererInfo.UNMASKED_RENDERER_WEBGL),
+ };
+ });
+ const computeTimezoneOffset = () => {
+ const date = new Date().getDate();
+ const month = new Date().getMonth();
+ // @ts-ignore
+ const year = Date().split ` `[3]; // current year
+ const format = (n) => ('' + n).length == 1 ? `0${n}` : n;
+ const dateString = `${month + 1}/${format(date)}/${year}`;
+ const dateStringUTC = `${year}-${format(month + 1)}-${format(date)}`;
+ // @ts-ignore
+ const utc = Date.parse(new Date(dateString));
+ const now = +new Date(dateStringUTC);
+ return +(((utc - now) / 60000).toFixed(0));
+ };
+ const getLocale = () => {
+ const constructors = [
+ 'Collator',
+ 'DateTimeFormat',
+ 'DisplayNames',
+ 'ListFormat',
+ 'NumberFormat',
+ 'PluralRules',
+ 'RelativeTimeFormat',
+ ];
+ // @ts-ignore
+ const locale = constructors.reduce((acc, name) => {
+ try {
+ const obj = new Intl[name];
+ if (!obj) {
+ return acc;
+ }
+ const { locale } = obj.resolvedOptions() || {};
+ return [...acc, locale];
+ }
+ catch (error) {
+ return acc;
+ }
+ }, []);
+ return [...new Set(locale)];
+ };
+ const getWorkerData = async () => {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const userAgentData = await getUserAgentData(navigator).catch((error) => console.error(error));
+ // webgl
+ const { webglVendor, webglRenderer } = getWebglData() || {};
+ // timezone & locale
+ const timezoneOffset = computeTimezoneOffset();
+ // eslint-disable-next-line new-cap
+ const timezoneLocation = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ const locale = getLocale();
+ // navigator
+ const { hardwareConcurrency, language, languages, platform, userAgent,
+ // @ts-expect-error
+ deviceMemory, } = navigator || {};
+ // prototype lies
+ await queueEvent(timer);
+ const {
+ // lieDetector: lieProps,
+ lieList, lieDetail,
+ // lieCount,
+ // propsSearched,
+ } = getWorkerPrototypeLies(self); // execute and destructure the list and detail
+ // const prototypeLies = JSON.parse(JSON.stringify(lieDetail))
+ const protoLieLen = lieList.length;
+ // match engine locale to system locale to determine if locale entropy is trusty
+ let systemCurrencyLocale;
+ const lang = ('' + language).split(',')[0];
+ try {
+ systemCurrencyLocale = (1).toLocaleString((lang || undefined), {
+ style: 'currency',
+ currency: 'USD',
+ currencyDisplay: 'name',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ });
+ }
+ catch (e) { }
+ const engineCurrencyLocale = (1).toLocaleString(undefined, {
+ style: 'currency',
+ currency: 'USD',
+ currencyDisplay: 'name',
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 0,
+ });
+ const localeEntropyIsTrusty = engineCurrencyLocale == systemCurrencyLocale;
+ const localeIntlEntropyIsTrusty = new Set(('' + language).split(',')).has('' + locale);
+ const { href, pathname } = self.location || {};
+ const locationPathNameLie = (!href ||
+ !pathname ||
+ !/^\/(docs|creepjs|public)|\/creep.js$/.test(pathname) ||
+ !new RegExp(`${pathname}$`).test(href));
+ return {
+ lied: protoLieLen || +locationPathNameLie,
+ lies: {
+ proto: protoLieLen ? lieDetail : false,
+ },
+ locale: '' + locale,
+ systemCurrencyLocale,
+ engineCurrencyLocale,
+ localeEntropyIsTrusty,
+ localeIntlEntropyIsTrusty,
+ timezoneOffset,
+ timezoneLocation,
+ deviceMemory,
+ hardwareConcurrency,
+ language,
+ languages: '' + languages,
+ platform,
+ userAgent,
+ webglRenderer,
+ webglVendor,
+ userAgentData,
+ };
+ };
+ // Compute and communicate from worker scope
+ const onEvent = (eventType, fn) => addEventListener(eventType, fn);
+ const send = (source) => {
+ return getWorkerData().then((data) => source.postMessage(data));
+ };
+ if (IS_WORKER_SCOPE) {
+ globalThis.ServiceWorkerGlobalScope ? onEvent('message', (e) => send(e.source)) :
+ globalThis.SharedWorkerGlobalScope ? onEvent('connect', (e) => send(e.ports[0])) :
+ send(self); // DedicatedWorkerGlobalScope
+ }
+ return IS_WORKER_SCOPE ? 0 /* Scope.WORKER */ : 1 /* Scope.WINDOW */;
+ }
+ async function getBestWorkerScope() {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const ask = (fn) => {
+ try {
+ return fn();
+ }
+ catch (e) {
+ return;
+ }
+ };
+ const hasConstructor = (x, name) => x && x.__proto__.constructor.name == name;
+ const getDedicatedWorker = ({ scriptSource }) => new Promise((resolve) => {
+ const giveUpOnWorker = setTimeout(() => {
+ return resolve(null);
+ }, 3000);
+ const dedicatedWorker = ask(() => new Worker(scriptSource));
+ if (!hasConstructor(dedicatedWorker, 'Worker'))
+ return resolve(null);
+ dedicatedWorker.onmessage = (event) => {
+ dedicatedWorker.terminate();
+ clearTimeout(giveUpOnWorker);
+ return resolve(event.data);
+ };
+ });
+ const getSharedWorker = ({ scriptSource }) => new Promise((resolve) => {
+ const giveUpOnWorker = setTimeout(() => {
+ return resolve(null);
+ }, 3000);
+ const sharedWorker = ask(() => new SharedWorker(scriptSource));
+ if (!hasConstructor(sharedWorker, 'SharedWorker'))
+ return resolve(null);
+ sharedWorker.port.start();
+ sharedWorker.port.onmessage = (event) => {
+ sharedWorker.port.close();
+ clearTimeout(giveUpOnWorker);
+ return resolve(event.data);
+ };
+ });
+ const getServiceWorker = ({ scriptSource }) => new Promise((resolve) => {
+ const giveUpOnWorker = setTimeout(() => {
+ return resolve(null);
+ }, 4000);
+ if (!ask(() => navigator.serviceWorker.register))
+ return resolve(null);
+ return navigator.serviceWorker.register(scriptSource).then((registration) => {
+ if (!hasConstructor(registration, 'ServiceWorkerRegistration'))
+ return resolve(null);
+ return navigator.serviceWorker.ready.then((registration) => {
+ // @ts-ignore
+ registration.active.postMessage(undefined);
+ navigator.serviceWorker.onmessage = (event) => {
+ registration.unregister();
+ clearTimeout(giveUpOnWorker);
+ return resolve(event.data);
+ };
+ });
+ }).catch((error) => {
+ console.error(error);
+ clearTimeout(giveUpOnWorker);
+ return resolve(null);
+ });
+ });
+ const scriptSource = './creep.js';
+ WORKER_NAME = 'ServiceWorkerGlobalScope';
+ WORKER_TYPE = 'service'; // loads fast but is not available in frames
+ let workerScope = await getServiceWorker({ scriptSource }).catch((error) => {
+ captureError(error);
+ console.error(error.message);
+ return;
+ });
+ if (!(workerScope || {}).userAgent) {
+ WORKER_NAME = 'SharedWorkerGlobalScope';
+ WORKER_TYPE = 'shared'; // no support in Safari, iOS, and Chrome Android
+ workerScope = await getSharedWorker({ scriptSource }).catch((error) => {
+ captureError(error);
+ console.error(error.message);
+ return;
+ });
+ }
+ if (!(workerScope || {}).userAgent) {
+ WORKER_NAME = 'DedicatedWorkerGlobalScope';
+ WORKER_TYPE = 'dedicated'; // device emulators can easily spoof dedicated scope
+ workerScope = await getDedicatedWorker({ scriptSource }).catch((error) => {
+ captureError(error);
+ console.error(error.message);
+ return;
+ });
+ }
+ if (!(workerScope || {}).userAgent) {
+ return;
+ }
+ workerScope.system = getOS(workerScope.userAgent);
+ workerScope.device = getUserAgentPlatform({ userAgent: workerScope.userAgent });
+ // detect lies
+ const { system, userAgent, userAgentData, platform, deviceMemory, hardwareConcurrency, } = workerScope || {};
+ // navigator lies
+ // skip language and languages to respect valid engine language switching bug in Chrome
+ // these are more likely navigator lies, so don't trigger lied worker scope
+ const workerScopeMatchLie = 'does not match worker scope';
+ if (platform != navigator.platform) {
+ documentLie('Navigator.platform', workerScopeMatchLie);
+ }
+ if (userAgent != navigator.userAgent) {
+ documentLie('Navigator.userAgent', workerScopeMatchLie);
+ }
+ if (hardwareConcurrency && (hardwareConcurrency != navigator.hardwareConcurrency)) {
+ documentLie('Navigator.hardwareConcurrency', workerScopeMatchLie);
+ }
+ // @ts-ignore
+ if (deviceMemory && (deviceMemory != navigator.deviceMemory)) {
+ documentLie('Navigator.deviceMemory', workerScopeMatchLie);
+ }
+ // prototype lies
+ if (workerScope.lies.proto) {
+ const { proto } = workerScope.lies;
+ const keys = Object.keys(proto);
+ keys.forEach((key) => {
+ const api = `WorkerGlobalScope.${key}`;
+ const lies = proto[key];
+ lies.forEach((lie) => documentLie(api, lie));
+ });
+ }
+ // user agent os lie
+ const [userAgentOS, platformOS] = getReportedPlatform(userAgent, platform);
+ if (userAgentOS != platformOS) {
+ workerScope.lied = true;
+ workerScope.lies.os = `${platformOS} platform and ${userAgentOS} user agent do not match`;
+ documentLie('WorkerGlobalScope', workerScope.lies.os);
+ }
+ // user agent engine lie
+ const decryptedName = decryptUserAgent({
+ ua: userAgent,
+ os: system,
+ isBrave: false, // default false since we are only looking for JS runtime and version
+ });
+ const userAgentEngine = ((/safari/i.test(decryptedName) || /iphone|ipad/i.test(userAgent)) ? 'JavaScriptCore' :
+ /firefox/i.test(userAgent) ? 'SpiderMonkey' :
+ /chrome/i.test(userAgent) ? 'V8' :
+ undefined);
+ if (userAgentEngine != JS_ENGINE) {
+ workerScope.lied = true;
+ workerScope.lies.engine = `${JS_ENGINE} JS runtime and ${userAgentEngine} user agent do not match`;
+ documentLie('WorkerGlobalScope', workerScope.lies.engine);
+ }
+ // user agent version lie
+ const getVersion = (x) => (/\d+/.exec(x) || [])[0];
+ const userAgentVersion = getVersion(decryptedName);
+ const userAgentDataVersion = getVersion(userAgentData ? userAgentData.uaFullVersion : '');
+ const versionSupported = userAgentDataVersion && userAgentVersion;
+ const versionMatch = userAgentDataVersion == userAgentVersion;
+ if (versionSupported && !versionMatch) {
+ workerScope.lied = true;
+ workerScope.lies.version = `userAgentData version ${userAgentDataVersion} and user agent version ${userAgentVersion} do not match`;
+ documentLie('WorkerGlobalScope', workerScope.lies.version);
+ }
+ // platformVersion lie
+ const FEATURE_CASE = IS_BLINK && CSS.supports('accent-color: initial');
+ const getPlatformVersionLie = (device, userAgentData) => {
+ if (!/windows|mac/i.test(device) || !userAgentData?.platformVersion) {
+ return false;
+ }
+ if (userAgentData.platform == 'macOS') {
+ return FEATURE_CASE ? /_/.test(userAgentData.platformVersion) : false;
+ }
+ const reportedVersionNumber = (/windows ([\d|\.]+)/i.exec(device) || [])[1];
+ const windows10OrHigherReport = +reportedVersionNumber == 10;
+ const { platformVersion } = userAgentData;
+ const versionMap = {
+ '6.1': '7',
+ '6.2': '8',
+ '6.3': '8.1',
+ '10.0': '10',
+ };
+ const version = versionMap[platformVersion];
+ if (!FEATURE_CASE && version) {
+ return version != reportedVersionNumber;
+ }
+ const parts = platformVersion.split('.');
+ if (parts.length != 3)
+ return true;
+ const windows10OrHigherPlatform = +parts[0] > 0;
+ return ((windows10OrHigherPlatform && !windows10OrHigherReport) ||
+ (!windows10OrHigherPlatform && windows10OrHigherReport));
+ };
+ const windowsVersionLie = getPlatformVersionLie(workerScope.device, userAgentData);
+ if (windowsVersionLie) {
+ workerScope.lied = true;
+ workerScope.lies.platformVersion = `platform version is fake`;
+ documentLie('WorkerGlobalScope', workerScope.lies.platformVersion);
+ }
+ // capture userAgent version
+ workerScope.userAgentVersion = userAgentVersion;
+ workerScope.userAgentDataVersion = userAgentDataVersion;
+ workerScope.userAgentEngine = userAgentEngine;
+ const gpu = {
+ ...(getWebGLRendererConfidence(workerScope.webglRenderer) || {}),
+ compressedGPU: compressWebGLRenderer(workerScope.webglRenderer),
+ };
+ logTestResult({ time: timer.stop(), test: `${WORKER_TYPE} worker`, passed: true });
+ return {
+ ...workerScope,
+ gpu,
+ uaPostReduction: isUAPostReduction(workerScope.userAgent),
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'worker', passed: false });
+ captureError(error, 'workers failed or blocked by client');
+ return;
+ }
+ }
+ function workerScopeHTML(fp) {
+ if (!fp.workerScope) {
+ return `
+
+
Worker
+
lang/timezone:
+
${HTMLNote.BLOCKED}
+
gpu:
+
${HTMLNote.BLOCKED}
+
+
+
userAgent:
+
${HTMLNote.BLOCKED}
+
device:
+
${HTMLNote.BLOCKED}
+
userAgentData:
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { workerScope: data } = fp;
+ const { lied, locale, systemCurrencyLocale, engineCurrencyLocale, localeEntropyIsTrusty, localeIntlEntropyIsTrusty, timezoneOffset, timezoneLocation, deviceMemory, hardwareConcurrency, language,
+ // languages,
+ platform, userAgent, uaPostReduction, webglRenderer, webglVendor, gpu, userAgentData, system, device, $hash, } = data || {};
+ const { parts, warnings, gibbers, confidence, grade: confidenceGrade, compressedGPU, } = gpu || {};
+ return `
+ ${performanceLogger.getLog()[`${WORKER_TYPE} worker`]}
+ ${WORKER_NAME || ''}
+
+
+
+
Worker ${hashSlice($hash)}
+
lang/timezone:
+
+ ${localeEntropyIsTrusty ? `${language} (${systemCurrencyLocale})` :
+ `${language} (${engineCurrencyLocale} )`}
+ ${locale === language ? '' : localeIntlEntropyIsTrusty ? ` ${locale}` :
+ ` ${locale} `}
+ ${timezoneLocation} (${'' + timezoneOffset})
+
+
+
${confidence ? `confidence: ${confidence} ` : ''}gpu:
+
+ ${webglVendor ? webglVendor : ''}
+ ${webglRenderer ? ` ${webglRenderer}` : HTMLNote.UNSUPPORTED}
+
+
+
+
+
+
userAgent:${!uaPostReduction ? '' : `ua reduction `}
+
+
${userAgent || HTMLNote.UNSUPPORTED}
+
+
+
device:
+
+ ${`${system}${platform ? ` (${platform})` : ''}`}
+ ${device ? ` ${device}` : HTMLNote.BLOCKED}
+ ${hardwareConcurrency && deviceMemory ? ` cores: ${hardwareConcurrency}, ram: ${deviceMemory}` :
+ hardwareConcurrency && !deviceMemory ? ` cores: ${hardwareConcurrency}` :
+ !hardwareConcurrency && deviceMemory ? ` ram: ${deviceMemory}` : ''}
+
+
+
userAgentData:
+
+
+ ${((userAgentData) => {
+ const { architecture, bitness, brandsVersion, uaFullVersion, mobile, model, platformVersion, platform, } = userAgentData || {};
+ // @ts-ignore
+ const windowsRelease = computeWindowsRelease({ platform, platformVersion });
+ return !userAgentData ? HTMLNote.UNSUPPORTED : `
+ ${(brandsVersion || []).join(',')}${uaFullVersion ? ` (${uaFullVersion})` : ''}
+ ${windowsRelease || `${platform} ${platformVersion}`} ${architecture ? `${architecture}${bitness ? `_${bitness}` : ''}` : ''}
+ ${model ? ` ${model}` : ''}
+ ${mobile ? ' mobile' : ''}
+ `;
+ })(userAgentData)}
+
+
+
+
+ `;
+ }
+
+ // https://stackoverflow.com/a/22429679
+ const hashMini = (x) => {
+ const json = `${JSON.stringify(x)}`;
+ const hash = json.split('').reduce((hash, char, i) => {
+ return Math.imul(31, hash) + json.charCodeAt(i) | 0;
+ }, 0x811c9dc5);
+ return ('0000000' + (hash >>> 0).toString(16)).substr(-8);
+ };
+ // instance id
+ const instanceId = (String.fromCharCode(Math.random() * 26 + 97) +
+ Math.random().toString(36).slice(-7));
+ // https://stackoverflow.com/a/53490958
+ // https://stackoverflow.com/a/43383990
+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
+ const hashify = (x, algorithm = 'SHA-256') => {
+ const json = `${JSON.stringify(x)}`;
+ const jsonBuffer = new TextEncoder().encode(json);
+ return crypto.subtle.digest(algorithm, jsonBuffer).then((hashBuffer) => {
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map((b) => ('00' + b.toString(16)).slice(-2)).join('');
+ return hashHex;
+ });
+ };
+ const getFuzzyHash = async (fp) => {
+ // requires update log (below) when adding new keys to fp
+ const metricKeys = [
+ 'canvas2d.dataURI',
+ 'canvas2d.emojiSet',
+ 'canvas2d.emojiURI',
+ 'canvas2d.liedTextMetrics',
+ 'canvas2d.mods',
+ 'canvas2d.paintURI',
+ 'canvas2d.paintCpuURI',
+ 'canvas2d.textMetricsSystemSum',
+ 'canvas2d.textURI',
+ 'canvasWebgl.dataURI',
+ 'canvasWebgl.dataURI2',
+ 'canvasWebgl.extensions',
+ 'canvasWebgl.gpu',
+ 'canvasWebgl.parameterOrExtensionLie',
+ 'canvasWebgl.parameters',
+ 'canvasWebgl.pixels',
+ 'canvasWebgl.pixels2',
+ 'capturedErrors.data',
+ 'clientRects.domrectSystemSum',
+ 'clientRects.elementBoundingClientRect',
+ 'clientRects.elementClientRects',
+ 'clientRects.emojiSet',
+ 'clientRects.rangeBoundingClientRect',
+ 'clientRects.rangeClientRects',
+ 'consoleErrors.errors',
+ 'css.computedStyle',
+ 'css.system',
+ 'cssMedia.matchMediaCSS',
+ 'cssMedia.mediaCSS',
+ 'cssMedia.screenQuery',
+ 'features.cssFeatures',
+ 'features.cssVersion',
+ 'features.jsFeatures',
+ 'features.jsFeaturesKeys',
+ 'features.jsVersion',
+ 'features.version',
+ 'features.versionRange',
+ 'features.windowFeatures',
+ 'features.windowVersion',
+ 'fonts.apps',
+ 'fonts.emojiSet',
+ 'fonts.fontFaceLoadFonts',
+ 'fonts.pixelSizeSystemSum',
+ 'fonts.platformVersion',
+ 'headless.chromium',
+ 'headless.headless',
+ 'headless.headlessRating',
+ 'headless.likeHeadless',
+ 'headless.likeHeadlessRating',
+ 'headless.platformEstimate',
+ 'headless.stealth',
+ 'headless.stealthRating',
+ 'headless.systemFonts',
+ 'htmlElementVersion.keys',
+ 'intl.dateTimeFormat',
+ 'intl.displayNames',
+ 'intl.listFormat',
+ 'intl.locale',
+ 'intl.numberFormat',
+ 'intl.pluralRules',
+ 'intl.relativeTimeFormat',
+ 'lies.data',
+ 'lies.totalLies',
+ 'maths.data',
+ 'media.mimeTypes',
+ 'navigator.appVersion',
+ 'navigator.bluetoothAvailability',
+ 'navigator.device',
+ 'navigator.deviceMemory',
+ 'navigator.doNotTrack',
+ 'navigator.globalPrivacyControl',
+ 'navigator.hardwareConcurrency',
+ 'navigator.language',
+ 'navigator.maxTouchPoints',
+ 'navigator.mimeTypes',
+ 'navigator.oscpu',
+ 'navigator.permissions',
+ 'navigator.platform',
+ 'navigator.plugins',
+ 'navigator.properties',
+ 'navigator.system',
+ 'navigator.uaPostReduction',
+ 'navigator.userAgent',
+ 'navigator.userAgentData',
+ 'navigator.userAgentParsed',
+ 'navigator.vendor',
+ 'navigator.webgpu',
+ 'offlineAudioContext.binsSample',
+ 'offlineAudioContext.compressorGainReduction',
+ 'offlineAudioContext.copySample',
+ 'offlineAudioContext.floatFrequencyDataSum',
+ 'offlineAudioContext.floatTimeDomainDataSum',
+ 'offlineAudioContext.noise',
+ 'offlineAudioContext.sampleSum',
+ 'offlineAudioContext.totalUniqueSamples',
+ 'offlineAudioContext.values',
+ 'resistance.engine',
+ 'resistance.extension',
+ 'resistance.extensionHashPattern',
+ 'resistance.mode',
+ 'resistance.privacy',
+ 'resistance.security',
+ 'screen.availHeight',
+ 'screen.availWidth',
+ 'screen.colorDepth',
+ 'screen.height',
+ 'screen.pixelDepth',
+ 'screen.touch',
+ 'screen.width',
+ 'svg.bBox',
+ 'svg.computedTextLength',
+ 'svg.emojiSet',
+ 'svg.extentOfChar',
+ 'svg.subStringLength',
+ 'svg.svgrectSystemSum',
+ 'timezone.location',
+ 'timezone.locationEpoch',
+ 'timezone.locationMeasured',
+ 'timezone.offset',
+ 'timezone.offsetComputed',
+ 'timezone.zone',
+ 'trash.trashBin',
+ 'voices.defaultVoiceLang',
+ 'voices.defaultVoiceName',
+ 'voices.languages',
+ 'voices.local',
+ 'voices.remote',
+ 'windowFeatures.apple',
+ 'windowFeatures.keys',
+ 'windowFeatures.moz',
+ 'windowFeatures.webkit',
+ 'workerScope.device',
+ 'workerScope.deviceMemory',
+ 'workerScope.engineCurrencyLocale',
+ 'workerScope.gpu',
+ 'workerScope.hardwareConcurrency',
+ 'workerScope.language',
+ 'workerScope.languages',
+ 'workerScope.lies',
+ 'workerScope.locale',
+ 'workerScope.localeEntropyIsTrusty',
+ 'workerScope.localeIntlEntropyIsTrusty',
+ 'workerScope.platform',
+ 'workerScope.system',
+ 'workerScope.systemCurrencyLocale',
+ 'workerScope.timezoneLocation',
+ 'workerScope.timezoneOffset',
+ 'workerScope.uaPostReduction',
+ 'workerScope.userAgent',
+ 'workerScope.userAgentData',
+ 'workerScope.userAgentDataVersion',
+ 'workerScope.userAgentEngine',
+ 'workerScope.userAgentVersion',
+ 'workerScope.webglRenderer',
+ 'workerScope.webglVendor',
+ ];
+ // construct map of all metrics
+ const metricsAll = Object.keys(fp).sort().reduce((acc, sectionKey) => {
+ const section = fp[sectionKey];
+ const sectionMetrics = Object.keys(section || {}).sort().reduce((acc, key) => {
+ if (key == '$hash' || key == 'lied') {
+ return acc;
+ }
+ return { ...acc, [`${sectionKey}.${key}`]: section[key] };
+ }, {});
+ return { ...acc, ...sectionMetrics };
+ }, {});
+ // reduce to 64 bins
+ const maxBins = 64;
+ const metricKeysReported = Object.keys(metricsAll);
+ const binSize = Math.ceil(metricKeys.length / maxBins);
+ // update log
+ const devMode = window.location.host != 'abrahamjuliot.github.io';
+ if (devMode && ('' + metricKeysReported != '' + metricKeys)) {
+ const newKeys = metricKeysReported.filter((key) => !metricKeys.includes(key));
+ const oldKeys = metricKeys.filter((key) => !metricKeysReported.includes(key));
+ if (newKeys.length || oldKeys.length) {
+ newKeys.length && console.warn('added fuzzy key(s):\n', newKeys.join('\n'));
+ oldKeys.length && console.warn('removed fuzzy key(s):\n', oldKeys.join('\n'));
+ console.groupCollapsed('update keys for accurate fuzzy hashing:');
+ console.log(metricKeysReported.map((x) => `'${x}',`).join('\n'));
+ console.groupEnd();
+ }
+ }
+ // compute fuzzy fingerprint master
+ const fuzzyFpMaster = metricKeys.reduce((acc, key, index) => {
+ if (!index || ((index % binSize) == 0)) {
+ const keySet = metricKeys.slice(index, index + binSize);
+ return { ...acc, ['' + keySet]: keySet.map((key) => metricsAll[key]) };
+ }
+ return acc;
+ }, {});
+ // hash each bin
+ await Promise.all(Object.keys(fuzzyFpMaster).map((key) => hashify(fuzzyFpMaster[key]).then((hash) => {
+ fuzzyFpMaster[key] = hash; // swap values for hash
+ return hash;
+ })));
+ // create fuzzy hash
+ const fuzzyBits = 64;
+ const fuzzyFingerprint = Object.keys(fuzzyFpMaster)
+ .map((key) => fuzzyFpMaster[key][0])
+ .join('')
+ .padEnd(fuzzyBits, '0');
+ return fuzzyFingerprint;
+ };
+
+ const KnownAudio = {
+ // Blink/WebKit
+ [-20.538286209106445]: [
+ 124.0434488439787,
+ 124.04344968475198,
+ 124.04347527516074,
+ 124.04347503720783,
+ 124.04347657808103,
+ ],
+ [-20.538288116455078]: [
+ 124.04347518575378,
+ 124.04347527516074,
+ 124.04344884395687,
+ 124.04344968475198,
+ 124.04347657808103,
+ 124.04347730590962, // pattern (rare)
+ 124.0434765110258, // pattern (rare)
+ 124.04347656317987, // pattern (rare)
+ 124.04375314689969, // pattern (rare)
+ // WebKit
+ 124.0434485301812,
+ 124.0434496849557,
+ 124.043453265891,
+ 124.04345734833623,
+ 124.04345808873768,
+ ],
+ [-20.535268783569336]: [
+ // Android/Linux
+ 124.080722568091,
+ 124.08072256811283,
+ 124.08072766105033,
+ 124.08072787802666,
+ 124.08072787804849,
+ 124.08074500028306,
+ 124.0807470110085,
+ 124.08075528279005,
+ 124.08075643483608,
+ ],
+ // Gecko
+ [-31.502187728881836]: [35.74996626004577],
+ [-31.502185821533203]: [35.74996031448245, 35.7499681673944, 35.749968223273754],
+ [-31.50218963623047]: [35.74996031448245],
+ [-31.509262084960938]: [35.7383295930922, 35.73833402246237],
+ // WebKit
+ [-29.837873458862305]: [35.10892717540264, 35.10892752557993],
+ [-29.83786964416504]: [35.10893232002854, 35.10893253237009],
+ };
+ const AUDIO_TRAP = Math.random();
+ async function hasFakeAudio() {
+ const context = new OfflineAudioContext(1, 100, 44100);
+ const oscillator = context.createOscillator();
+ oscillator.frequency.value = 0;
+ oscillator.start(0);
+ context.startRendering();
+ return new Promise((resolve) => {
+ context.oncomplete = (event) => {
+ const channelData = event.renderedBuffer.getChannelData?.(0);
+ if (!channelData)
+ resolve(false);
+ resolve('' + [...new Set(channelData)] !== '0');
+ };
+ }).finally(() => oscillator.disconnect());
+ }
+ async function getOfflineAudioContext() {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ try {
+ // @ts-expect-error if unsupported
+ window.OfflineAudioContext = OfflineAudioContext || webkitOfflineAudioContext;
+ }
+ catch (err) { }
+ if (!window.OfflineAudioContext) {
+ logTestResult({ test: 'audio', passed: false });
+ return;
+ }
+ // detect lies
+ const channelDataLie = lieProps['AudioBuffer.getChannelData'];
+ const copyFromChannelLie = lieProps['AudioBuffer.copyFromChannel'];
+ let lied = (channelDataLie || copyFromChannelLie) || false;
+ const bufferLen = 5000;
+ const context = new OfflineAudioContext(1, bufferLen, 44100);
+ const analyser = context.createAnalyser();
+ const oscillator = context.createOscillator();
+ const dynamicsCompressor = context.createDynamicsCompressor();
+ const biquadFilter = context.createBiquadFilter();
+ // detect lie
+ const dataArray = new Float32Array(analyser.frequencyBinCount);
+ analyser.getFloatFrequencyData?.(dataArray);
+ const floatFrequencyUniqueDataSize = new Set(dataArray).size;
+ if (floatFrequencyUniqueDataSize > 1) {
+ lied = true;
+ const floatFrequencyDataLie = `expected -Infinity (silence) and got ${floatFrequencyUniqueDataSize} frequencies`;
+ documentLie(`AnalyserNode.getFloatFrequencyData`, floatFrequencyDataLie);
+ }
+ const values = {
+ ['AnalyserNode.channelCount']: attempt(() => analyser.channelCount),
+ ['AnalyserNode.channelCountMode']: attempt(() => analyser.channelCountMode),
+ ['AnalyserNode.channelInterpretation']: attempt(() => analyser.channelInterpretation),
+ ['AnalyserNode.context.sampleRate']: attempt(() => analyser.context.sampleRate),
+ ['AnalyserNode.fftSize']: attempt(() => analyser.fftSize),
+ ['AnalyserNode.frequencyBinCount']: attempt(() => analyser.frequencyBinCount),
+ ['AnalyserNode.maxDecibels']: attempt(() => analyser.maxDecibels),
+ ['AnalyserNode.minDecibels']: attempt(() => analyser.minDecibels),
+ ['AnalyserNode.numberOfInputs']: attempt(() => analyser.numberOfInputs),
+ ['AnalyserNode.numberOfOutputs']: attempt(() => analyser.numberOfOutputs),
+ ['AnalyserNode.smoothingTimeConstant']: attempt(() => analyser.smoothingTimeConstant),
+ ['AnalyserNode.context.listener.forwardX.maxValue']: attempt(() => {
+ return caniuse(() => analyser.context.listener.forwardX.maxValue);
+ }),
+ ['BiquadFilterNode.gain.maxValue']: attempt(() => biquadFilter.gain.maxValue),
+ ['BiquadFilterNode.frequency.defaultValue']: attempt(() => biquadFilter.frequency.defaultValue),
+ ['BiquadFilterNode.frequency.maxValue']: attempt(() => biquadFilter.frequency.maxValue),
+ ['DynamicsCompressorNode.attack.defaultValue']: attempt(() => dynamicsCompressor.attack.defaultValue),
+ ['DynamicsCompressorNode.knee.defaultValue']: attempt(() => dynamicsCompressor.knee.defaultValue),
+ ['DynamicsCompressorNode.knee.maxValue']: attempt(() => dynamicsCompressor.knee.maxValue),
+ ['DynamicsCompressorNode.ratio.defaultValue']: attempt(() => dynamicsCompressor.ratio.defaultValue),
+ ['DynamicsCompressorNode.ratio.maxValue']: attempt(() => dynamicsCompressor.ratio.maxValue),
+ ['DynamicsCompressorNode.release.defaultValue']: attempt(() => dynamicsCompressor.release.defaultValue),
+ ['DynamicsCompressorNode.release.maxValue']: attempt(() => dynamicsCompressor.release.maxValue),
+ ['DynamicsCompressorNode.threshold.defaultValue']: attempt(() => dynamicsCompressor.threshold.defaultValue),
+ ['DynamicsCompressorNode.threshold.minValue']: attempt(() => dynamicsCompressor.threshold.minValue),
+ ['OscillatorNode.detune.maxValue']: attempt(() => oscillator.detune.maxValue),
+ ['OscillatorNode.detune.minValue']: attempt(() => oscillator.detune.minValue),
+ ['OscillatorNode.frequency.defaultValue']: attempt(() => oscillator.frequency.defaultValue),
+ ['OscillatorNode.frequency.maxValue']: attempt(() => oscillator.frequency.maxValue),
+ ['OscillatorNode.frequency.minValue']: attempt(() => oscillator.frequency.minValue),
+ };
+ const getRenderedBuffer = (context) => (new Promise((resolve) => {
+ const analyser = context.createAnalyser();
+ const oscillator = context.createOscillator();
+ const dynamicsCompressor = context.createDynamicsCompressor();
+ try {
+ oscillator.type = 'triangle';
+ oscillator.frequency.value = 10000;
+ dynamicsCompressor.threshold.value = -50;
+ dynamicsCompressor.knee.value = 40;
+ dynamicsCompressor.attack.value = 0;
+ }
+ catch (err) { }
+ oscillator.connect(dynamicsCompressor);
+ dynamicsCompressor.connect(analyser);
+ dynamicsCompressor.connect(context.destination);
+ oscillator.start(0);
+ context.startRendering();
+ return context.addEventListener('complete', (event) => {
+ try {
+ dynamicsCompressor.disconnect();
+ oscillator.disconnect();
+ const floatFrequencyData = new Float32Array(analyser.frequencyBinCount);
+ analyser.getFloatFrequencyData?.(floatFrequencyData);
+ const floatTimeDomainData = new Float32Array(analyser.fftSize);
+ if ('getFloatTimeDomainData' in analyser) {
+ analyser.getFloatTimeDomainData(floatTimeDomainData);
+ }
+ return resolve({
+ floatFrequencyData,
+ floatTimeDomainData,
+ buffer: event.renderedBuffer,
+ compressorGainReduction: (
+ // @ts-expect-error if unsupported
+ dynamicsCompressor.reduction.value || // webkit
+ dynamicsCompressor.reduction),
+ });
+ }
+ catch (error) {
+ return resolve(null);
+ }
+ });
+ }));
+ await queueEvent(timer);
+ const [audioData, audioIsFake,] = await Promise.all([
+ getRenderedBuffer(new OfflineAudioContext(1, bufferLen, 44100)),
+ hasFakeAudio().catch(() => false),
+ ]);
+ const { floatFrequencyData, floatTimeDomainData, buffer, compressorGainReduction, } = audioData || {};
+ await queueEvent(timer);
+ const getSnapshot = (arr, start, end) => {
+ const collection = [];
+ for (let i = start; i < end; i++) {
+ collection.push(arr[i]);
+ }
+ return collection;
+ };
+ const getSum = (arr) => !arr ? 0 : [...arr]
+ .reduce((acc, curr) => (acc += Math.abs(curr)), 0);
+ const floatFrequencyDataSum = getSum(floatFrequencyData);
+ const floatTimeDomainDataSum = getSum(floatTimeDomainData);
+ const copy = new Float32Array(bufferLen);
+ let bins = new Float32Array();
+ if (buffer) {
+ buffer.copyFromChannel?.(copy, 0);
+ bins = buffer.getChannelData?.(0) || [];
+ }
+ const copySample = getSnapshot([...copy], 4500, 4600);
+ const binsSample = getSnapshot([...bins], 4500, 4600);
+ const sampleSum = getSum(getSnapshot([...bins], 4500, bufferLen));
+ // detect lies
+ if (audioIsFake) {
+ lied = true;
+ documentLie('AudioBuffer', 'audio is fake');
+ }
+ // sample matching
+ const matching = '' + binsSample == '' + copySample;
+ const copyFromChannelSupported = ('copyFromChannel' in AudioBuffer.prototype);
+ if (copyFromChannelSupported && !matching) {
+ lied = true;
+ const audioSampleLie = 'getChannelData and copyFromChannel samples mismatch';
+ documentLie('AudioBuffer', audioSampleLie);
+ }
+ // sample uniqueness
+ const totalUniqueSamples = new Set([...bins]).size;
+ if (totalUniqueSamples == bufferLen) {
+ const audioUniquenessTrash = `${totalUniqueSamples} unique samples of ${bufferLen} is too high`;
+ sendToTrash('AudioBuffer', audioUniquenessTrash);
+ }
+ // sample noise factor
+ const getRandFromRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
+ const getCopyFrom = (rand, buffer, copy) => {
+ const { length } = buffer;
+ const max = 20;
+ const start = getRandFromRange(275, length - (max + 1));
+ const mid = start + max / 2;
+ const end = start + max;
+ buffer.getChannelData(0)[start] = rand;
+ buffer.getChannelData(0)[mid] = rand;
+ buffer.getChannelData(0)[end] = rand;
+ buffer.copyFromChannel(copy, 0);
+ const attack = [
+ buffer.getChannelData(0)[start] === 0 ? Math.random() : 0,
+ buffer.getChannelData(0)[mid] === 0 ? Math.random() : 0,
+ buffer.getChannelData(0)[end] === 0 ? Math.random() : 0,
+ ];
+ return [...new Set([...buffer.getChannelData(0), ...copy, ...attack])].filter((x) => x !== 0);
+ };
+ const getCopyTo = (rand, buffer, copy) => {
+ buffer.copyToChannel(copy.map(() => rand), 0);
+ const frequency = buffer.getChannelData(0)[0];
+ const dataAttacked = [...buffer.getChannelData(0)]
+ .map((x) => x !== frequency || !x ? Math.random() : x);
+ return dataAttacked.filter((x) => x !== frequency);
+ };
+ const getNoiseFactor = () => {
+ const length = 2000;
+ try {
+ const result = [...new Set([
+ ...getCopyFrom(AUDIO_TRAP, new AudioBuffer({ length, sampleRate: 44100 }), new Float32Array(length)),
+ ...getCopyTo(AUDIO_TRAP, new AudioBuffer({ length, sampleRate: 44100 }), new Float32Array(length)),
+ ])];
+ return +(result.length !== 1 &&
+ result.reduce((acc, n) => acc += +n, 0));
+ }
+ catch (error) {
+ console.error(error);
+ return 0;
+ }
+ };
+ const noiseFactor = getNoiseFactor();
+ const noise = (noiseFactor || [...new Set(bins.slice(0, 100))]
+ .reduce((acc, n) => acc += n, 0));
+ // Locked Patterns
+ const known = {
+ /* BLINK */
+ // 124.04347527516074/124.04347518575378
+ '-20.538286209106445,164537.64796829224,502.5999283068122': [124.04347527516074],
+ '-20.538288116455078,164537.64796829224,502.5999283068122': [124.04347527516074],
+ '-20.538288116455078,164537.64795303345,502.5999283068122': [
+ 124.04347527516074,
+ 124.04347518575378,
+ // sus:
+ 124.04347519320436,
+ 124.04347523045726,
+ ],
+ '-20.538286209106445,164537.64805984497,502.5999283068122': [124.04347527516074],
+ '-20.538288116455078,164537.64805984497,502.5999283068122': [
+ 124.04347527516074,
+ 124.04347518575378,
+ // sus
+ 124.04347520065494,
+ 124.04347523790784,
+ 124.043475252809,
+ 124.04347526025958,
+ 124.04347522300668,
+ 124.04347523045726,
+ 124.04347524535842,
+ ],
+ // 124.04344884395687
+ '-20.538288116455078,164881.9727935791,502.59990317908887': [124.04344884395687],
+ '-20.538288116455078,164881.9729309082,502.59990317908887': [124.04344884395687],
+ // 124.0434488439787
+ '-20.538286209106445,164882.2082748413,502.59990317911434': [124.0434488439787],
+ '-20.538288116455078,164882.20836639404,502.59990317911434': [124.0434488439787],
+ // 124.04344968475198
+ '-20.538286209106445,164863.45319366455,502.5999033495791': [124.04344968475198],
+ '-20.538288116455078,164863.45319366455,502.5999033495791': [
+ 124.04344968475198,
+ 124.04375314689969, // rare
+ // sus
+ 124.04341541208123,
+ ],
+ // 124.04347503720783 (rare)
+ '-20.538288116455078,164531.82670593262,502.59992767886797': [
+ 124.04347503720783,
+ // sus
+ 124.04347494780086,
+ 124.04347495525144,
+ 124.04347499250434,
+ 124.0434750074055,
+ ],
+ // 124.04347657808103
+ '-20.538286209106445,164540.1567993164,502.59992209258417': [124.04347657808103],
+ '-20.538288116455078,164540.1567993164,502.59992209258417': [
+ 124.04347657808103,
+ 124.0434765110258, // rare
+ 124.04347656317987, // rare
+ // sus
+ 124.04347657063045,
+ 124.04378004022874,
+ ],
+ '-20.538288116455078,164540.1580810547,502.59992209258417': [124.04347657808103],
+ // 124.080722568091/124.04347730590962 (rare)
+ '-20.535268783569336,164940.360786438,502.69695458233764': [124.080722568091],
+ '-20.538288116455078,164538.55073928833,502.5999307175407': [124.04347730590962],
+ // Android/Linux
+ '-20.535268783569336,164948.14596557617,502.6969545823631': [124.08072256811283],
+ '-20.535268783569336,164926.65912628174,502.6969610930064': [124.08072766105033],
+ '-20.535268783569336,164932.96168518066,502.69696179985476': [124.08072787802666],
+ '-20.535268783569336,164931.54252624512,502.6969617998802': [124.08072787804849],
+ '-20.535268783569336,164591.9659729004,502.6969925059784': [124.08074500028306],
+ '-20.535268783569336,164590.4111480713,502.6969947774742': [124.0807470110085],
+ '-20.535268783569336,164590.41115570068,502.6969947774742': [124.0807470110085],
+ '-20.535268783569336,164593.64263916016,502.69700490119067': [124.08075528279005],
+ '-20.535268783569336,164595.0285797119,502.69700578315314': [124.08075643483608],
+ // sus
+ '-20.538288116455078,164860.96576690674,502.6075748118915': [124.0434496279413],
+ '-20.538288116455078,164860.9938583374,502.6073723861407': [124.04344962817413],
+ '-20.538288116455078,164862.14078521729,502.59991004130643': [124.04345734833623],
+ '-20.538288116455078,164534.50047683716,502.61542110471055': [124.04347520368174],
+ '-20.538288116455078,164535.1324043274,502.6079200572931': [124.04347521997988],
+ '-20.538288116455078,164535.51135635376,502.60633126448374': [124.04347522952594],
+ /* GECKO */
+ '-31.509262084960938,167722.6894454956,148.42717787250876': [35.7383295930922],
+ '-31.509262084960938,167728.72756958008,148.427184343338': [35.73833402246237],
+ '-31.50218963623047,167721.27517700195,148.47537828609347': [35.74996031448245],
+ '-31.502185821533203,167727.52931976318,148.47542023658752': [35.7499681673944],
+ '-31.502185821533203,167700.7530517578,148.475412953645': [35.749968223273754],
+ '-31.502187728881836,167697.23177337646,148.47541113197803': [35.74996626004577],
+ /* WEBKIT */
+ '-20.538288116455078,164873.80361557007,502.59989904452596': [124.0434485301812],
+ '-20.538288116455078,164863.47760391235,502.5999033453372': [124.0434496849557],
+ '-20.538288116455078,164876.62466049194,502.5998911961724': [124.043453265891],
+ '-20.538288116455078,164862.14879989624,502.59991004130643': [124.04345734833623],
+ '-20.538288116455078,164896.54167175293,502.5999054916465': [124.04345808873768],
+ '-29.837873458862305,163206.43050384521,0': [35.10892717540264],
+ '-29.837873458862305,163224.69785308838,0': [35.10892752557993],
+ '-29.83786964416504,163209.17245483398,0': [35.10893232002854],
+ '-29.83786964416504,163202.77336883545,0': [35.10893253237009],
+ };
+ if (noise) {
+ lied = true;
+ documentLie('AudioBuffer', 'sample noise detected');
+ }
+ const pattern = '' + [
+ compressorGainReduction,
+ floatFrequencyDataSum,
+ floatTimeDomainDataSum,
+ ];
+ const knownPattern = known[pattern];
+ if (knownPattern && !knownPattern.includes(sampleSum)) {
+ LowerEntropy.AUDIO = true;
+ sendToTrash('AudioBuffer', 'suspicious frequency data');
+ }
+ logTestResult({ time: timer.stop(), test: 'audio', passed: true });
+ return {
+ totalUniqueSamples,
+ compressorGainReduction,
+ floatFrequencyDataSum,
+ floatTimeDomainDataSum,
+ sampleSum,
+ binsSample,
+ copySample: copyFromChannelSupported ? copySample : [undefined],
+ values,
+ noise,
+ lied,
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'audio', passed: false });
+ captureError(error, 'OfflineAudioContext failed or blocked by client');
+ return;
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ function audioHTML(fp) {
+ if (!fp.offlineAudioContext) {
+ return `
+
Audio
+
sum: ${HTMLNote.BLOCKED}
+
gain: ${HTMLNote.BLOCKED}
+
freq: ${HTMLNote.BLOCKED}
+
time: ${HTMLNote.BLOCKED}
+
trap: ${HTMLNote.BLOCKED}
+
unique: ${HTMLNote.BLOCKED}
+
data: ${HTMLNote.BLOCKED}
+
copy: ${HTMLNote.BLOCKED}
+
values: ${HTMLNote.BLOCKED}
+
`;
+ }
+ const { offlineAudioContext: { $hash, totalUniqueSamples, compressorGainReduction, floatFrequencyDataSum, floatTimeDomainDataSum, sampleSum, binsSample, copySample, lied, noise, values, }, } = fp;
+ const knownSums = KnownAudio[compressorGainReduction] || [];
+ const validAudio = sampleSum && compressorGainReduction && knownSums.length;
+ const matchesKnownAudio = knownSums.includes(sampleSum);
+ return `
+
+
${performanceLogger.getLog().audio}
+
Audio ${hashSlice($hash)}
+
sum: ${!sampleSum ? HTMLNote.BLOCKED : (!validAudio || matchesKnownAudio) ? sampleSum : getDiffs({
+ stringA: knownSums[0],
+ stringB: sampleSum,
+ charDiff: true,
+ decorate: (diff) => `${diff} `,
+ })}
+
gain: ${compressorGainReduction || HTMLNote.BLOCKED}
+
freq: ${floatFrequencyDataSum || HTMLNote.BLOCKED}
+
time: ${floatTimeDomainDataSum || HTMLNote.UNSUPPORTED}
+
trap: ${!noise ? AUDIO_TRAP : getDiffs({
+ stringA: AUDIO_TRAP,
+ stringB: noise,
+ charDiff: true,
+ decorate: (diff) => `${diff} `,
+ })}
+
unique: ${totalUniqueSamples}
+
data:${'' + binsSample[0] == 'undefined' ? ` ${HTMLNote.BLOCKED}` :
+ `${hashMini(binsSample)} `}
+
copy:${'' + copySample[0] == 'undefined' ? ` ${HTMLNote.BLOCKED}` :
+ `${hashMini(copySample)} `}
+
values: ${modal('creep-offline-audio-context', Object.keys(values).map((key) => `
${key}: ${values[key]}
`).join(''), hashMini(values))}
+
+ `;
+ }
+
+ // inspired by https://arkenfox.github.io/TZP/tests/canvasnoise.html
+ let pixelImageRandom = '';
+ const getPixelMods = () => {
+ const pattern1 = [];
+ const pattern2 = [];
+ const len = 8; // canvas dimensions
+ const alpha = 255;
+ const visualMultiplier = 5;
+ try {
+ // create 2 canvas contexts
+ const options = {
+ willReadFrequently: true,
+ desynchronized: true,
+ };
+ const canvasDisplay1 = document.createElement('canvas');
+ const canvasDisplay2 = document.createElement('canvas');
+ const canvas1 = document.createElement('canvas');
+ const canvas2 = document.createElement('canvas');
+ const contextDisplay1 = canvasDisplay1.getContext('2d', options);
+ const contextDisplay2 = canvasDisplay2.getContext('2d', options);
+ const context1 = canvas1.getContext('2d', options);
+ const context2 = canvas2.getContext('2d', options);
+ if (!contextDisplay1 || !contextDisplay2 || !context1 || !context2) {
+ throw new Error('canvas context blocked');
+ }
+ // set the dimensions
+ canvasDisplay1.width = len * visualMultiplier;
+ canvasDisplay1.height = len * visualMultiplier;
+ canvasDisplay2.width = len * visualMultiplier;
+ canvasDisplay2.height = len * visualMultiplier;
+ canvas1.width = len;
+ canvas1.height = len;
+ canvas2.width = len;
+ canvas2.height = len;
+ [...Array(len)].forEach((e, x) => [...Array(len)].forEach((e, y) => {
+ const red = ~~(Math.random() * 256);
+ const green = ~~(Math.random() * 256);
+ const blue = ~~(Math.random() * 256);
+ const colors = `${red}, ${green}, ${blue}, ${alpha}`;
+ context1.fillStyle = `rgba(${colors})`;
+ context1.fillRect(x, y, 1, 1);
+ // capture data in visuals
+ contextDisplay1.fillStyle = `rgba(${colors})`;
+ contextDisplay1.fillRect(x * visualMultiplier, y * visualMultiplier, 1 * visualMultiplier, 1 * visualMultiplier);
+ return pattern1.push(colors); // collect the pixel pattern
+ }));
+ [...Array(len)].forEach((e, x) => [...Array(len)].forEach((e, y) => {
+ // get context1 pixel data and mirror to context2
+ const { data: [red, green, blue, alpha], } = context1.getImageData(x, y, 1, 1) || {};
+ const colors = `${red}, ${green}, ${blue}, ${alpha}`;
+ context2.fillStyle = `rgba(${colors})`;
+ context2.fillRect(x, y, 1, 1);
+ // capture noise in visuals
+ const { data: [red2, green2, blue2, alpha2], } = context2.getImageData(x, y, 1, 1) || {};
+ const colorsDisplay = `
+ ${red != red2 ? red2 : 255},
+ ${green != green2 ? green2 : 255},
+ ${blue != blue2 ? blue2 : 255},
+ ${alpha != alpha2 ? alpha2 : 1}
+ `;
+ contextDisplay2.fillStyle = `rgba(${colorsDisplay})`;
+ contextDisplay2.fillRect(x * visualMultiplier, y * visualMultiplier, 1 * visualMultiplier, 1 * visualMultiplier);
+ return pattern2.push(colors); // collect the pixel pattern
+ }));
+ // compare the pattern collections and collect diffs
+ const patternDiffs = [];
+ const rgbaChannels = new Set();
+ [...Array(pattern1.length)].forEach((e, i) => {
+ const pixelColor1 = pattern1[i];
+ const pixelColor2 = pattern2[i];
+ if (pixelColor1 != pixelColor2) {
+ const rgbaValues1 = pixelColor1.split(',');
+ const rgbaValues2 = pixelColor2.split(',');
+ const colors = [
+ rgbaValues1[0] != rgbaValues2[0] ? 'r' : '',
+ rgbaValues1[1] != rgbaValues2[1] ? 'g' : '',
+ rgbaValues1[2] != rgbaValues2[2] ? 'b' : '',
+ rgbaValues1[3] != rgbaValues2[3] ? 'a' : '',
+ ].join('');
+ rgbaChannels.add(colors);
+ patternDiffs.push([i, colors]);
+ }
+ });
+ pixelImageRandom = canvasDisplay1.toDataURL(); // template use only
+ const pixelImage = canvasDisplay2.toDataURL();
+ const rgba = rgbaChannels.size ? [...rgbaChannels].sort().join(', ') : undefined;
+ const pixels = patternDiffs.length || undefined;
+ return { rgba, pixels, pixelImage };
+ }
+ catch (error) {
+ return console.error(error);
+ }
+ };
+ // based on and inspired by https://github.com/antoinevastel/picasso-like-canvas-fingerprinting
+ const paintCanvas = ({ canvas, context, strokeText = false, cssFontFamily = '', area = { width: 50, height: 50 }, rounds = 10, maxShadowBlur = 50, seed = 500, offset = 2001000001, multiplier = 15000, }) => {
+ if (!context) {
+ return;
+ }
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ canvas.width = area.width;
+ canvas.height = area.height;
+ if (canvas.style) {
+ canvas.style.display = 'none';
+ }
+ const createPicassoSeed = ({ seed, offset, multiplier }) => {
+ let current = Number(seed) % Number(offset);
+ const getNextSeed = () => {
+ current = (Number(multiplier) * current) % Number(offset);
+ return current;
+ };
+ return {
+ getNextSeed,
+ };
+ };
+ const picassoSeed = createPicassoSeed({ seed, offset, multiplier });
+ const { getNextSeed } = picassoSeed;
+ const patchSeed = (current, offset, maxBound, computeFloat) => {
+ const result = (((current - 1) / offset) * (maxBound || 1)) || 0;
+ return computeFloat ? result : Math.floor(result);
+ };
+ const addRandomCanvasGradient = (context, offset, area, colors, getNextSeed) => {
+ const { width, height } = area;
+ const canvasGradient = context.createRadialGradient(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width));
+ canvasGradient.addColorStop(0, colors[patchSeed(getNextSeed(), offset, colors.length)]);
+ canvasGradient.addColorStop(1, colors[patchSeed(getNextSeed(), offset, colors.length)]);
+ context.fillStyle = canvasGradient;
+ };
+ const colors = [
+ '#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6',
+ '#E6B333', '#3366E6', '#999966', '#99FF99', '#B34D4D',
+ '#80B300', '#809900', '#E6B3B3', '#6680B3', '#66991A',
+ '#FF99E6', '#CCFF1A', '#FF1A66', '#E6331A', '#33FFCC',
+ '#66994D', '#B366CC', '#4D8000', '#B33300', '#CC80CC',
+ '#66664D', '#991AFF', '#E666FF', '#4DB3FF', '#1AB399',
+ '#E666B3', '#33991A', '#CC9999', '#B3B31A', '#00E680',
+ '#4D8066', '#809980', '#E6FF80', '#1AFF33', '#999933',
+ '#FF3380', '#CCCC00', '#66E64D', '#4D80CC', '#9900B3',
+ '#E64D66', '#4DB380', '#FF4D4D', '#99E6E6', '#6666FF',
+ ];
+ const drawOutlineOfText = (context, offset, area, getNextSeed) => {
+ const { width, height } = area;
+ const fontSize = 2.99;
+ context.font = `${height / fontSize}px ${cssFontFamily.replace(/!important/gm, '')}`;
+ context.strokeText('👾A', patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width));
+ };
+ const createCircularArc = (context, offset, area, getNextSeed) => {
+ const { width, height } = area;
+ context.beginPath();
+ context.arc(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, Math.min(width, height)), patchSeed(getNextSeed(), offset, 2 * Math.PI, true), patchSeed(getNextSeed(), offset, 2 * Math.PI, true));
+ context.stroke();
+ };
+ const createBezierCurve = (context, offset, area, getNextSeed) => {
+ const { width, height } = area;
+ context.beginPath();
+ context.moveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height));
+ context.bezierCurveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height));
+ context.stroke();
+ };
+ const createQuadraticCurve = (context, offset, area, getNextSeed) => {
+ const { width, height } = area;
+ context.beginPath();
+ context.moveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height));
+ context.quadraticCurveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height));
+ context.stroke();
+ };
+ const createEllipticalArc = (context, offset, area, getNextSeed) => {
+ if (!('ellipse' in context)) {
+ return;
+ }
+ const { width, height } = area;
+ context.beginPath();
+ context.ellipse(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, Math.floor(width / 2)), patchSeed(getNextSeed(), offset, Math.floor(height / 2)), patchSeed(getNextSeed(), offset, 2 * Math.PI, true), patchSeed(getNextSeed(), offset, 2 * Math.PI, true), patchSeed(getNextSeed(), offset, 2 * Math.PI, true));
+ context.stroke();
+ };
+ const methods = [
+ createCircularArc,
+ createBezierCurve,
+ createQuadraticCurve,
+ ];
+ if (!IS_WEBKIT)
+ methods.push(createEllipticalArc); // unstable in webkit
+ if (strokeText)
+ methods.push(drawOutlineOfText);
+ [...Array(rounds)].forEach((x) => {
+ addRandomCanvasGradient(context, offset, area, colors, getNextSeed);
+ context.shadowBlur = patchSeed(getNextSeed(), offset, maxShadowBlur, true);
+ context.shadowColor = colors[patchSeed(getNextSeed(), offset, colors.length)];
+ const nextMethod = methods[patchSeed(getNextSeed(), offset, methods.length)];
+ nextMethod(context, offset, area, getNextSeed);
+ context.fill();
+ });
+ return;
+ };
+ async function getCanvas2d() {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const dataLie = lieProps['HTMLCanvasElement.toDataURL'];
+ const contextLie = lieProps['HTMLCanvasElement.getContext'];
+ const imageDataLie = (lieProps['CanvasRenderingContext2D.fillText'] ||
+ lieProps['CanvasRenderingContext2D.font'] ||
+ lieProps['CanvasRenderingContext2D.getImageData'] ||
+ lieProps['CanvasRenderingContext2D.strokeText']);
+ const codePointLie = lieProps['String.fromCodePoint'];
+ let textMetricsLie = (lieProps['CanvasRenderingContext2D.measureText'] ||
+ lieProps['TextMetrics.actualBoundingBoxAscent'] ||
+ lieProps['TextMetrics.actualBoundingBoxDescent'] ||
+ lieProps['TextMetrics.actualBoundingBoxLeft'] ||
+ lieProps['TextMetrics.actualBoundingBoxRight'] ||
+ lieProps['TextMetrics.fontBoundingBoxAscent'] ||
+ lieProps['TextMetrics.fontBoundingBoxDescent'] ||
+ lieProps['TextMetrics.width']);
+ let lied = (dataLie ||
+ contextLie ||
+ imageDataLie ||
+ textMetricsLie ||
+ codePointLie) || false;
+ // create canvas context
+ let win = window;
+ if (!LIKE_BRAVE && PHANTOM_DARKNESS) {
+ win = PHANTOM_DARKNESS;
+ }
+ const doc = win.document;
+ const canvas = doc.createElement('canvas');
+ const context = canvas.getContext('2d');
+ const canvasCPU = doc.createElement('canvas');
+ const contextCPU = canvasCPU.getContext('2d', {
+ desynchronized: true,
+ willReadFrequently: true,
+ });
+ if (!context) {
+ throw new Error('canvas context blocked');
+ }
+ await queueEvent(timer);
+ const imageSizeMax = IS_WEBKIT ? 50 : 75; // webkit is unstable
+ paintCanvas({
+ canvas,
+ context,
+ strokeText: true,
+ cssFontFamily: CSS_FONT_FAMILY,
+ area: { width: imageSizeMax, height: imageSizeMax },
+ rounds: 10,
+ });
+ const dataURI = canvas.toDataURL();
+ await queueEvent(timer);
+ const mods = getPixelMods();
+ // TextMetrics: get emoji set and system
+ await queueEvent(timer);
+ context.font = `10px ${CSS_FONT_FAMILY.replace(/!important/gm, '')}`;
+ const pattern = new Set();
+ const emojiSet = EMOJIS.reduce((emojiSet, emoji) => {
+ const { actualBoundingBoxAscent, actualBoundingBoxDescent, actualBoundingBoxLeft, actualBoundingBoxRight, fontBoundingBoxAscent, fontBoundingBoxDescent, width, } = context.measureText(emoji) || {};
+ const dimensions = [
+ actualBoundingBoxAscent,
+ actualBoundingBoxDescent,
+ actualBoundingBoxLeft,
+ actualBoundingBoxRight,
+ fontBoundingBoxAscent,
+ fontBoundingBoxDescent,
+ width,
+ ].join(',');
+ if (!pattern.has(dimensions)) {
+ pattern.add(dimensions);
+ emojiSet.add(emoji);
+ }
+ return emojiSet;
+ }, new Set());
+ // textMetrics System Sum
+ const textMetricsSystemSum = 0.00001 * [...pattern].map((x) => {
+ return x.split(',').reduce((acc, x) => acc += (+x || 0), 0);
+ }).reduce((acc, x) => acc += x, 0);
+ // Paint
+ const maxSize = 75;
+ await queueEvent(timer);
+ paintCanvas({
+ canvas,
+ context,
+ area: { width: maxSize, height: maxSize },
+ }); // clears image
+ const paintURI = canvas.toDataURL();
+ // Paint with CPU
+ await queueEvent(timer);
+ paintCanvas({
+ canvas: canvasCPU,
+ context: contextCPU,
+ area: { width: maxSize, height: maxSize },
+ }); // clears image
+ const paintCpuURI = canvasCPU.toDataURL();
+ // Text
+ context.restore();
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ canvas.width = 50;
+ canvas.height = 50;
+ context.font = `50px ${CSS_FONT_FAMILY.replace(/!important/gm, '')}`;
+ context.fillText('A', 7, 37);
+ const textURI = canvas.toDataURL();
+ // Emoji
+ context.restore();
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ canvas.width = 50;
+ canvas.height = 50;
+ context.font = `35px ${CSS_FONT_FAMILY.replace(/!important/gm, '')}`;
+ context.fillText('👾', 0, 37);
+ const emojiURI = canvas.toDataURL();
+ // lies
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ if ((mods && mods.pixels) || !!Math.max(...context.getImageData(0, 0, 8, 8).data)) {
+ lied = true;
+ documentLie(`CanvasRenderingContext2D.getImageData`, `pixel data modified`);
+ }
+ // verify low entropy image data
+ canvas.width = 2;
+ canvas.height = 2;
+ context.fillStyle = '#000';
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ context.fillStyle = '#fff';
+ context.fillRect(2, 2, 1, 1);
+ context.beginPath();
+ context.arc(0, 0, 2, 0, 1, true);
+ context.closePath();
+ context.fill();
+ const imageDataLowEntropy = context.getImageData(0, 0, 2, 2).data.join('');
+ const KnownImageData = {
+ BLINK: [
+ '255255255255178178178255246246246255555555255',
+ '255255255255192192192255240240240255484848255',
+ '255255255255177177177255246246246255535353255',
+ '255255255255128128128255191191191255646464255',
+ '255255255255178178178255247247247255565656255', // ?
+ '255255255255174174174255242242242255474747255',
+ '255255255255229229229255127127127255686868255',
+ '255255255255192192192255244244244255535353255',
+ ],
+ GECKO: [
+ '255255255255191191191255207207207255646464255',
+ '255255255255192192192255240240240255484848255',
+ '255255255255191191191255239239239255646464255',
+ '255255255255191191191255223223223255606060255', // ?
+ '255255255255171171171255223223223255606060255', // ?
+ '255255255255188188188255245245245255525252255',
+ ],
+ WEBKIT: [
+ '255255255255185185185255233233233255474747255',
+ '255255255255185185185255229229229255474747255',
+ '255255255255185185185255218218218255474747255',
+ '255255255255192192192255240240240255484848255',
+ '255255255255178178178255247247247255565656255',
+ '255255255255178178178255247247247255565656255',
+ '255255255255192192192255240240240255484848255',
+ '255255255255186186186255218218218255464646255',
+ ],
+ };
+ Analysis.imageDataLowEntropy = imageDataLowEntropy;
+ if (IS_BLINK && !KnownImageData.BLINK.includes(imageDataLowEntropy)) {
+ LowerEntropy.CANVAS = true;
+ }
+ else if (IS_GECKO && !KnownImageData.GECKO.includes(imageDataLowEntropy)) {
+ LowerEntropy.CANVAS = true;
+ }
+ else if (IS_WEBKIT && !KnownImageData.WEBKIT.includes(imageDataLowEntropy)) {
+ LowerEntropy.CANVAS = true;
+ }
+ if (LowerEntropy.CANVAS) {
+ sendToTrash('CanvasRenderingContext2D.getImageData', 'suspicious pixel data');
+ }
+ const getTextMetricsFloatLie = (context) => {
+ const isFloat = (n) => n % 1 !== 0;
+ const { actualBoundingBoxAscent: abba, actualBoundingBoxDescent: abbd, actualBoundingBoxLeft: abbl, actualBoundingBoxRight: abbr, fontBoundingBoxAscent: fbba, fontBoundingBoxDescent: fbbd,
+ // width: w,
+ } = context.measureText('') || {};
+ const lied = [
+ abba,
+ abbd,
+ abbl,
+ abbr,
+ fbba,
+ fbbd,
+ ].find((x) => isFloat((x || 0)));
+ return lied;
+ };
+ await queueEvent(timer);
+ if (getTextMetricsFloatLie(context)) {
+ textMetricsLie = true;
+ lied = true;
+ documentLie('CanvasRenderingContext2D.measureText', 'metric noise detected');
+ }
+ logTestResult({ time: timer.stop(), test: 'canvas 2d', passed: true });
+ return {
+ dataURI,
+ paintURI,
+ paintCpuURI,
+ textURI,
+ emojiURI,
+ mods,
+ textMetricsSystemSum,
+ liedTextMetrics: textMetricsLie,
+ emojiSet: [...emojiSet],
+ lied,
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'canvas 2d', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function canvasHTML(fp) {
+ if (!fp.canvas2d) {
+ return `
+
+
Canvas 2d ${HTMLNote.BLOCKED}
+
data: ${HTMLNote.BLOCKED}
+
rendering:
+
${HTMLNote.BLOCKED}
+
${HTMLNote.BLOCKED}
+
textMetrics:
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { canvas2d: { lied, dataURI, paintURI, paintCpuURI, textURI, emojiURI, mods, emojiSet, textMetricsSystemSum, $hash, }, } = fp;
+ const { pixels, rgba, pixelImage } = mods || {};
+ const modPercent = pixels ? Math.round((pixels / 400) * 100) : 0;
+ const hash = {
+ dataURI: hashMini(dataURI),
+ textURI: hashMini(textURI),
+ emojiURI: hashMini(emojiURI),
+ paintURI: hashMini(paintURI),
+ paintCpuURI: hashMini(paintCpuURI),
+ };
+ const dataTemplate = `
+ ${textURI ? `
` : ''}
+ text: ${!textURI ? HTMLNote.BLOCKED : hash.textURI}
+
+
+ ${emojiURI ? `
` : ''}
+ emoji: ${!emojiURI ? HTMLNote.BLOCKED : hash.emojiURI}
+
+
+ ${paintURI ? `
` : ''}
+ paint (GPU): ${!paintURI ? HTMLNote.BLOCKED : hash.paintURI}
+
+
+ ${paintCpuURI ? `
` : ''}
+ paint (CPU): ${!paintCpuURI ? HTMLNote.BLOCKED : hash.paintCpuURI}
+
+
+ ${dataURI ? `
` : ''}
+ combined: ${!dataURI ? HTMLNote.BLOCKED : hash.dataURI}
+ `;
+ // rgba: "b, g, gb, r, rb, rg, rgb"
+ const rgbaHTML = !rgba ? rgba : rgba.split(', ').map((s) => s.split('').map((c) => {
+ const css = {
+ r: 'red',
+ g: 'green',
+ b: 'blue',
+ };
+ return ` `;
+ }).join('')).join(' ');
+ const emojiHelpTitle = `CanvasRenderingContext2D.measureText()\nhash: ${hashMini(emojiSet)}\n${emojiSet.map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`;
+ return `
+
+
+
${performanceLogger.getLog()['canvas 2d']}
+
Canvas 2d ${hashSlice($hash)}
+
data: ${modal('creep-canvas-data', dataTemplate, hashMini({
+ dataURI,
+ pixelImage,
+ paintURI,
+ paintCpuURI,
+ textURI,
+ emojiURI,
+ }))}
+
rendering: ${rgba ? `${modPercent}% rgba noise ${rgbaHTML}` : ''}
+
+ ${textURI ? `
` : ''}
+ ${emojiURI ? `
` : ''}
+ ${paintCpuURI ? `
` : ''}
+ ${dataURI ? `
` : ''}
+
+
+
textMetrics:
+
+ ${textMetricsSystemSum || HTMLNote.UNSUPPORTED}
+
+ ${formatEmojiSet(emojiSet)}
+
+
+
+ `;
+ }
+
+ function getCSS() {
+ const computeStyle = (type, { require: [captureError] }) => {
+ try {
+ // get CSSStyleDeclaration
+ const cssStyleDeclaration = (type == 'getComputedStyle' ? getComputedStyle(document.body) :
+ type == 'HTMLElement.style' ? document.body.style :
+ // @ts-ignore
+ type == 'CSSRuleList.style' ? document.styleSheets[0].cssRules[0].style :
+ undefined);
+ if (!cssStyleDeclaration) {
+ throw new TypeError('invalid argument string');
+ }
+ // get properties
+ const proto = Object.getPrototypeOf(cssStyleDeclaration);
+ const prototypeProperties = Object.getOwnPropertyNames(proto);
+ const ownEnumerablePropertyNames = [];
+ const cssVar = /^--.*$/;
+ Object.keys(cssStyleDeclaration).forEach((key) => {
+ const numericKey = !isNaN(+key);
+ const value = cssStyleDeclaration[key];
+ const customPropKey = cssVar.test(key);
+ const customPropValue = cssVar.test(value);
+ if (numericKey && !customPropValue) {
+ return ownEnumerablePropertyNames.push(value);
+ }
+ else if (!numericKey && !customPropKey) {
+ return ownEnumerablePropertyNames.push(key);
+ }
+ return;
+ });
+ // get properties in prototype chain (required only in chrome)
+ const propertiesInPrototypeChain = {};
+ const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
+ const uncapitalize = (str) => str.charAt(0).toLowerCase() + str.slice(1);
+ const removeFirstChar = (str) => str.slice(1);
+ const caps = /[A-Z]/g;
+ ownEnumerablePropertyNames.forEach((key) => {
+ if (propertiesInPrototypeChain[key]) {
+ return;
+ }
+ // determine attribute type
+ const isNamedAttribute = key.indexOf('-') > -1;
+ const isAliasAttribute = caps.test(key);
+ // reduce key for computation
+ const firstChar = key.charAt(0);
+ const isPrefixedName = isNamedAttribute && firstChar == '-';
+ const isCapitalizedAlias = isAliasAttribute && firstChar == firstChar.toUpperCase();
+ key = (isPrefixedName ? removeFirstChar(key) :
+ isCapitalizedAlias ? uncapitalize(key) :
+ key);
+ // find counterpart in CSSStyleDeclaration object or its prototype chain
+ if (isNamedAttribute) {
+ const aliasAttribute = key.split('-').map((word, index) => index == 0 ? word : capitalize(word)).join('');
+ if (aliasAttribute in cssStyleDeclaration) {
+ propertiesInPrototypeChain[aliasAttribute] = true;
+ }
+ else if (capitalize(aliasAttribute) in cssStyleDeclaration) {
+ propertiesInPrototypeChain[capitalize(aliasAttribute)] = true;
+ }
+ }
+ else if (isAliasAttribute) {
+ const namedAttribute = key.replace(caps, (char) => '-' + char.toLowerCase());
+ if (namedAttribute in cssStyleDeclaration) {
+ propertiesInPrototypeChain[namedAttribute] = true;
+ }
+ else if (`-${namedAttribute}` in cssStyleDeclaration) {
+ propertiesInPrototypeChain[`-${namedAttribute}`] = true;
+ }
+ }
+ return;
+ });
+ // compile keys
+ const keys = [
+ ...new Set([
+ ...prototypeProperties,
+ ...ownEnumerablePropertyNames,
+ ...Object.keys(propertiesInPrototypeChain),
+ ]),
+ ];
+ // @ts-ignore
+ const interfaceName = ('' + proto).match(/\[object (.+)\]/)[1];
+ return { keys, interfaceName };
+ }
+ catch (error) {
+ captureError(error);
+ return;
+ }
+ };
+ const getSystemStyles = (el) => {
+ try {
+ const colors = [
+ 'ActiveBorder',
+ 'ActiveCaption',
+ 'ActiveText',
+ 'AppWorkspace',
+ 'Background',
+ 'ButtonBorder',
+ 'ButtonFace',
+ 'ButtonHighlight',
+ 'ButtonShadow',
+ 'ButtonText',
+ 'Canvas',
+ 'CanvasText',
+ 'CaptionText',
+ 'Field',
+ 'FieldText',
+ 'GrayText',
+ 'Highlight',
+ 'HighlightText',
+ 'InactiveBorder',
+ 'InactiveCaption',
+ 'InactiveCaptionText',
+ 'InfoBackground',
+ 'InfoText',
+ 'LinkText',
+ 'Mark',
+ 'MarkText',
+ 'Menu',
+ 'MenuText',
+ 'Scrollbar',
+ 'ThreeDDarkShadow',
+ 'ThreeDFace',
+ 'ThreeDHighlight',
+ 'ThreeDLightShadow',
+ 'ThreeDShadow',
+ 'VisitedText',
+ 'Window',
+ 'WindowFrame',
+ 'WindowText',
+ ];
+ const fonts = [
+ 'caption',
+ 'icon',
+ 'menu',
+ 'message-box',
+ 'small-caption',
+ 'status-bar',
+ ];
+ const getStyles = (el) => ({
+ colors: colors.map((color) => {
+ el.setAttribute('style', `background-color: ${color} !important`);
+ return {
+ [color]: getComputedStyle(el).backgroundColor,
+ };
+ }),
+ fonts: fonts.map((font) => {
+ el.setAttribute('style', `font: ${font} !important`);
+ const computedStyle = getComputedStyle(el);
+ return {
+ [font]: `${computedStyle.fontSize} ${computedStyle.fontFamily}`,
+ };
+ }),
+ });
+ if (!el) {
+ el = document.createElement('div');
+ document.body.append(el);
+ const systemStyles = getStyles(el);
+ el.parentNode.removeChild(el);
+ return systemStyles;
+ }
+ return getStyles(el);
+ }
+ catch (error) {
+ captureError(error);
+ return;
+ }
+ };
+ try {
+ const timer = createTimer();
+ timer.start();
+ const computedStyle = computeStyle('getComputedStyle', { require: [captureError] });
+ const system = getSystemStyles(PARENT_PHANTOM);
+ logTestResult({ time: timer.stop(), test: 'computed style', passed: true });
+ return {
+ computedStyle,
+ system,
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'computed style', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function cssHTML(fp) {
+ if (!fp.css) {
+ return `
+
+
Computed Style
+
keys (0): ${HTMLNote.BLOCKED}
+
system styles: ${HTMLNote.BLOCKED}
+
+
`;
+ }
+ const { css: data, } = fp;
+ const { $hash, computedStyle, system, } = data;
+ const colorsLen = system.colors.length;
+ const gradientColors = system.colors.map((color, index) => {
+ const name = Object.values(color)[0];
+ return (index == 0 ? `${name}, ${name} ${((index + 1) / colorsLen * 100).toFixed(2)}%` :
+ index == colorsLen - 1 ? `${name} ${((index - 1) / colorsLen * 100).toFixed(2)}%, ${name} 100%` :
+ `${name} ${(index / colorsLen * 100).toFixed(2)}%, ${name} ${((index + 1) / colorsLen * 100).toFixed(2)}%`);
+ });
+ const id = 'creep-css-style-declaration-version';
+ return `
+
+
${performanceLogger.getLog()['computed style']}
+
Computed Style ${hashSlice($hash)}
+
keys (${!computedStyle ? '0' : count(computedStyle.keys)}): ${!computedStyle ? HTMLNote.BLOCKED :
+ modal('creep-computed-style', computedStyle.keys.join(', '), hashMini(computedStyle))}
+
system styles: ${system && system.colors ? modal(`${id}-system-styles`, [
+ ...system.colors.map((color) => {
+ const key = Object.keys(color)[0];
+ const val = color[key];
+ return `
+
${key}: ${val}
+ `;
+ }),
+ ...system.fonts.map((font) => {
+ const key = Object.keys(font)[0];
+ const val = font[key];
+ return `
+
${key}: ${val}
+ `;
+ }),
+ ].join(''), hashMini(system)) : HTMLNote.BLOCKED}
+
+
+
+ `;
+ }
+
+ function getCSSMedia() {
+ const gcd = (a, b) => b == 0 ? a : gcd(b, a % b);
+ const getAspectRatio = (width, height) => {
+ const r = gcd(width, height);
+ const aspectRatio = `${width / r}/${height / r}`;
+ return aspectRatio;
+ };
+ const query = ({ body, type, rangeStart, rangeLen }) => {
+ const html = [...Array(rangeLen)].map((slot, i) => {
+ i += rangeStart;
+ return `@media(device-${type}:${i}px){body{--device-${type}:${i};}}`;
+ }).join('');
+ body.innerHTML = ``;
+ const style = getComputedStyle(body);
+ return style.getPropertyValue(`--device-${type}`).trim();
+ };
+ const getScreenMedia = ({ body, width, height }) => {
+ let widthMatch = query({ body, type: 'width', rangeStart: width, rangeLen: 1 });
+ let heightMatch = query({ body, type: 'height', rangeStart: height, rangeLen: 1 });
+ if (widthMatch && heightMatch) {
+ return { width, height };
+ }
+ const rangeLen = 1000;
+ [...Array(10)].find((slot, i) => {
+ if (!widthMatch) {
+ widthMatch = query({ body, type: 'width', rangeStart: i * rangeLen, rangeLen });
+ }
+ if (!heightMatch) {
+ heightMatch = query({ body, type: 'height', rangeStart: i * rangeLen, rangeLen });
+ }
+ return widthMatch && heightMatch;
+ });
+ return { width: +widthMatch, height: +heightMatch };
+ };
+ try {
+ const timer = createTimer();
+ timer.start();
+ const win = PHANTOM_DARKNESS.window;
+ const { body } = win.document;
+ const { width, availWidth, height, availHeight } = win.screen;
+ const noTaskbar = !(width - availWidth || height - availHeight);
+ if (screen.width !== width || (width > 800 && noTaskbar)) {
+ LowerEntropy.IFRAME_SCREEN = true;
+ }
+ const deviceAspectRatio = getAspectRatio(width, height);
+ const matchMediaCSS = {
+ ['prefers-reduced-motion']: (win.matchMedia('(prefers-reduced-motion: no-preference)').matches ? 'no-preference' :
+ win.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' : undefined),
+ ['prefers-color-scheme']: (
+ // prefer main window
+ matchMedia('(prefers-color-scheme: light)').matches ? 'light' :
+ matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : undefined),
+ monochrome: (win.matchMedia('(monochrome)').matches ? 'monochrome' :
+ win.matchMedia('(monochrome: 0)').matches ? 'non-monochrome' : undefined),
+ ['inverted-colors']: (win.matchMedia('(inverted-colors: inverted)').matches ? 'inverted' :
+ win.matchMedia('(inverted-colors: none)').matches ? 'none' : undefined),
+ ['forced-colors']: (win.matchMedia('(forced-colors: none)').matches ? 'none' :
+ win.matchMedia('(forced-colors: active)').matches ? 'active' : undefined),
+ ['any-hover']: (win.matchMedia('(any-hover: hover)').matches ? 'hover' :
+ win.matchMedia('(any-hover: none)').matches ? 'none' : undefined),
+ hover: (win.matchMedia('(hover: hover)').matches ? 'hover' :
+ win.matchMedia('(hover: none)').matches ? 'none' : undefined),
+ ['any-pointer']: (win.matchMedia('(any-pointer: fine)').matches ? 'fine' :
+ win.matchMedia('(any-pointer: coarse)').matches ? 'coarse' :
+ win.matchMedia('(any-pointer: none)').matches ? 'none' : undefined),
+ pointer: (win.matchMedia('(pointer: fine)').matches ? 'fine' :
+ win.matchMedia('(pointer: coarse)').matches ? 'coarse' :
+ win.matchMedia('(pointer: none)').matches ? 'none' : undefined),
+ ['device-aspect-ratio']: (win.matchMedia(`(device-aspect-ratio: ${deviceAspectRatio})`).matches ? deviceAspectRatio : undefined),
+ ['device-screen']: (win.matchMedia(`(device-width: ${width}px) and (device-height: ${height}px)`).matches ? `${width} x ${height}` : undefined),
+ ['display-mode']: (win.matchMedia('(display-mode: fullscreen)').matches ? 'fullscreen' :
+ win.matchMedia('(display-mode: standalone)').matches ? 'standalone' :
+ win.matchMedia('(display-mode: minimal-ui)').matches ? 'minimal-ui' :
+ win.matchMedia('(display-mode: browser)').matches ? 'browser' : undefined),
+ ['color-gamut']: (win.matchMedia('(color-gamut: rec2020)').matches ? 'rec2020' :
+ win.matchMedia('(color-gamut: p3)').matches ? 'p3' :
+ win.matchMedia('(color-gamut: srgb)').matches ? 'srgb' : undefined),
+ orientation: (
+ // prefer main window
+ matchMedia('(orientation: landscape)').matches ? 'landscape' :
+ matchMedia('(orientation: portrait)').matches ? 'portrait' : undefined),
+ };
+ body.innerHTML = `
+
+ `;
+ const style = getComputedStyle(body);
+ const mediaCSS = {
+ ['prefers-reduced-motion']: style.getPropertyValue('--prefers-reduced-motion').trim() || undefined,
+ ['prefers-color-scheme']: style.getPropertyValue('--prefers-color-scheme').trim() || undefined,
+ monochrome: style.getPropertyValue('--monochrome').trim() || undefined,
+ ['inverted-colors']: style.getPropertyValue('--inverted-colors').trim() || undefined,
+ ['forced-colors']: style.getPropertyValue('--forced-colors').trim() || undefined,
+ ['any-hover']: style.getPropertyValue('--any-hover').trim() || undefined,
+ hover: style.getPropertyValue('--hover').trim() || undefined,
+ ['any-pointer']: style.getPropertyValue('--any-pointer').trim() || undefined,
+ pointer: style.getPropertyValue('--pointer').trim() || undefined,
+ ['device-aspect-ratio']: style.getPropertyValue('--device-aspect-ratio').trim() || undefined,
+ ['device-screen']: style.getPropertyValue('--device-screen').trim() || undefined,
+ ['display-mode']: style.getPropertyValue('--display-mode').trim() || undefined,
+ ['color-gamut']: style.getPropertyValue('--color-gamut').trim() || undefined,
+ orientation: style.getPropertyValue('--orientation').trim() || undefined,
+ };
+ // get screen query
+ const screenQuery = getScreenMedia({ body, width, height });
+ logTestResult({ time: timer.stop(), test: 'css media', passed: true });
+ return { mediaCSS, matchMediaCSS, screenQuery };
+ }
+ catch (error) {
+ logTestResult({ test: 'css media', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function cssMediaHTML(fp) {
+ if (!fp.css) {
+ return `
+
+
CSS Media Queries
+
@media: ${HTMLNote.BLOCKED}
+
matchMedia: ${HTMLNote.BLOCKED}
+
touch device: ${HTMLNote.BLOCKED}
+
screen query: ${HTMLNote.BLOCKED}
+
`;
+ }
+ const { cssMedia: data, } = fp;
+ const { $hash, mediaCSS, matchMediaCSS, screenQuery, } = data;
+ return `
+
+
${performanceLogger.getLog()['css media']}
+
CSS Media Queries ${hashSlice($hash)}
+
@media: ${!mediaCSS || !Object.keys(mediaCSS).filter((key) => !!mediaCSS[key]).length ?
+ HTMLNote.BLOCKED :
+ modal('creep-css-media', `@media ${Object.keys(mediaCSS).map((key) => `${key}: ${mediaCSS[key] || HTMLNote.UNSUPPORTED}`).join(' ')}`, hashMini(mediaCSS))}
+
matchMedia: ${!matchMediaCSS || !Object.keys(matchMediaCSS).filter((key) => !!matchMediaCSS[key]).length ?
+ HTMLNote.BLOCKED :
+ modal('creep-css-match-media', `matchMedia ${Object.keys(matchMediaCSS).map((key) => `${key}: ${matchMediaCSS[key] || HTMLNote.UNSUPPORTED}`).join(' ')}`, hashMini(matchMediaCSS))}
+
touch device: ${!mediaCSS ? HTMLNote.BLOCKED : mediaCSS['any-pointer'] == 'coarse' ? true : HTMLNote.UNKNOWN}
+
screen query:
+ ${!screenQuery ? HTMLNote.BLOCKED : `${screenQuery.width} x ${screenQuery.height}`}
+
+
+ `;
+ }
+
+ function getHTMLElementVersion() {
+ try {
+ const timer = createTimer();
+ timer.start();
+ const keys = [];
+ // eslint-disable-next-line guard-for-in
+ for (const key in document.documentElement) {
+ keys.push(key);
+ }
+ logTestResult({ time: timer.stop(), test: 'html element', passed: true });
+ return { keys };
+ }
+ catch (error) {
+ logTestResult({ test: 'html element', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function htmlElementVersionHTML(fp) {
+ if (!fp.htmlElementVersion) {
+ return `
+
+
HTMLElement
+
keys (0): ${HTMLNote.Blocked}
+
`;
+ }
+ const { htmlElementVersion: { $hash, keys, }, } = fp;
+ return `
+
+
${performanceLogger.getLog()['html element']}
+
HTMLElement ${hashSlice($hash)}
+
keys (${count(keys)}): ${keys && keys.length ? modal('creep-html-element-version', keys.join(', ')) : HTMLNote.Blocked}
+
+ `;
+ }
+
+ function getRectSum(rect) {
+ return Object.keys(rect).reduce((acc, key) => acc += rect[key], 0) / 100000000;
+ }
+ // inspired by
+ // https://privacycheck.sec.lrz.de/active/fp_gcr/fp_getclientrects.html
+ // https://privacycheck.sec.lrz.de/active/fp_e/fp_emoji.html
+ async function getClientRects() {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const toNativeObject = (domRect) => {
+ return {
+ bottom: domRect.bottom,
+ height: domRect.height,
+ left: domRect.left,
+ right: domRect.right,
+ width: domRect.width,
+ top: domRect.top,
+ x: domRect.x,
+ y: domRect.y,
+ };
+ };
+ let lied = (lieProps['Element.getClientRects'] ||
+ lieProps['Element.getBoundingClientRect'] ||
+ lieProps['Range.getClientRects'] ||
+ lieProps['Range.getBoundingClientRect'] ||
+ lieProps['String.fromCodePoint']) || false;
+ const DOC = (PHANTOM_DARKNESS &&
+ PHANTOM_DARKNESS.document &&
+ PHANTOM_DARKNESS.document.body ? PHANTOM_DARKNESS.document :
+ document);
+ const getBestRect = (el) => {
+ let range;
+ if (!lieProps['Element.getClientRects']) {
+ return el.getClientRects()[0];
+ }
+ else if (!lieProps['Element.getBoundingClientRect']) {
+ return el.getBoundingClientRect();
+ }
+ else if (!lieProps['Range.getClientRects']) {
+ range = DOC.createRange();
+ range.selectNode(el);
+ return range.getClientRects()[0];
+ }
+ range = DOC.createRange();
+ range.selectNode(el);
+ return range.getBoundingClientRect();
+ };
+ const rectsId = `${instanceId}-client-rects-div`;
+ const divElement = document.createElement('div');
+ divElement.setAttribute('id', rectsId);
+ DOC.body.appendChild(divElement);
+ patch(divElement, html `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${EMOJIS.map((emoji) => {
+ return `
${emoji}
`;
+ }).join('')}
+
+
+ `);
+ // get emoji set and system
+ const pattern = new Set();
+ await queueEvent(timer);
+ const emojiElems = [...DOC.getElementsByClassName('domrect-emoji')];
+ const emojiSet = emojiElems.reduce((emojiSet, el, i) => {
+ const emoji = EMOJIS[i];
+ const { height, width } = getBestRect(el);
+ const dimensions = `${width},${height}`;
+ if (!pattern.has(dimensions)) {
+ pattern.add(dimensions);
+ emojiSet.add(emoji);
+ }
+ return emojiSet;
+ }, new Set());
+ const domrectSystemSum = 0.00001 * [...pattern].map((x) => {
+ return x.split(',').reduce((acc, x) => acc += (+x || 0), 0);
+ }).reduce((acc, x) => acc += x, 0);
+ // get clientRects
+ const range = document.createRange();
+ const rectElems = DOC.getElementsByClassName('rects');
+ const elementClientRects = [...rectElems].map((el) => {
+ return toNativeObject(el.getClientRects()[0]);
+ });
+ const elementBoundingClientRect = [...rectElems].map((el) => {
+ return toNativeObject(el.getBoundingClientRect());
+ });
+ const rangeClientRects = [...rectElems].map((el) => {
+ range.selectNode(el);
+ return toNativeObject(range.getClientRects()[0]);
+ });
+ const rangeBoundingClientRect = [...rectElems].map((el) => {
+ range.selectNode(el);
+ return toNativeObject(range.getBoundingClientRect());
+ });
+ // detect failed shift calculation
+ // inspired by https://arkenfox.github.io/TZP
+ const rect4 = [...rectElems][3];
+ const { top: initialTop } = elementClientRects[3];
+ rect4.classList.add('shift-dom-rect');
+ const { top: shiftedTop } = toNativeObject(rect4.getClientRects()[0]);
+ rect4.classList.remove('shift-dom-rect');
+ const { top: unshiftedTop } = toNativeObject(rect4.getClientRects()[0]);
+ const diff = initialTop - shiftedTop;
+ const unshiftLie = diff != (unshiftedTop - shiftedTop);
+ if (unshiftLie) {
+ lied = true;
+ documentLie('Element.getClientRects', 'failed unshift calculation');
+ }
+ // detect failed math calculation lie
+ let mathLie = false;
+ elementClientRects.forEach((rect) => {
+ const { right, left, width, bottom, top, height, x, y } = rect;
+ if (right - left != width ||
+ bottom - top != height ||
+ right - x != width ||
+ bottom - y != height) {
+ lied = true;
+ mathLie = true;
+ }
+ return;
+ });
+ if (mathLie) {
+ documentLie('Element.getClientRects', 'failed math calculation');
+ }
+ // detect equal elements mismatch lie
+ const { right: right1, left: left1 } = elementClientRects[10];
+ const { right: right2, left: left2 } = elementClientRects[11];
+ if (right1 != right2 || left1 != left2) {
+ documentLie('Element.getClientRects', 'equal elements mismatch');
+ lied = true;
+ }
+ // detect unknown rotate dimensions
+ const knownEl = [...DOC.getElementsByClassName('rect-known')][0];
+ const knownDimensions = toNativeObject(knownEl.getClientRects()[0]);
+ const knownHash = hashMini(knownDimensions);
+ if (IS_BLINK) {
+ if (devicePixelRatio === 1 && knownHash !== '9d9215cc') {
+ documentLie('Element.getClientRects', 'unknown rotate dimensions');
+ lied = true;
+ }
+ }
+ else if (IS_GECKO) {
+ const Rotate = {
+ 'e38453f0': true, // 100, etc
+ };
+ if (!Rotate[knownHash]) {
+ documentLie('Element.getClientRects', 'unknown rotate dimensions');
+ lied = true;
+ }
+ }
+ // detect ghost dimensions
+ const ghostEl = [...DOC.getElementsByClassName('rect-ghost')][0];
+ const ghostDimensions = toNativeObject(ghostEl.getClientRects()[0]);
+ const hasGhostDimensions = Object.keys(ghostDimensions)
+ .some((key) => ghostDimensions[key] !== 0);
+ if (hasGhostDimensions) {
+ documentLie('Element.getClientRects', 'unknown ghost dimensions');
+ lied = true;
+ }
+ DOC.body.removeChild(DOC.getElementById(rectsId));
+ logTestResult({ time: timer.stop(), test: 'rects', passed: true });
+ return {
+ elementClientRects,
+ elementBoundingClientRect,
+ rangeClientRects,
+ rangeBoundingClientRect,
+ emojiSet: [...emojiSet],
+ domrectSystemSum,
+ lied,
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'rects', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ function clientRectsHTML(fp) {
+ if (!fp.clientRects) {
+ return `
+
+
DOMRect
+
elems A: ${HTMLNote.BLOCKED}
+
elems B: ${HTMLNote.BLOCKED}
+
range A: ${HTMLNote.BLOCKED}
+
range B: ${HTMLNote.BLOCKED}
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { clientRects: { $hash, elementClientRects, elementBoundingClientRect, rangeClientRects, rangeBoundingClientRect, emojiSet, domrectSystemSum, lied, }, } = fp;
+ const computeDiffs = (rects) => {
+ if (!rects || !rects.length) {
+ return;
+ }
+ const expectedSum = rects.reduce((acc, rect) => {
+ const { right, left, width, bottom, top, height } = rect;
+ const expected = {
+ width: right - left,
+ height: bottom - top,
+ right: left + width,
+ left: right - width,
+ bottom: top + height,
+ top: bottom - height,
+ x: right - width,
+ y: bottom - height,
+ };
+ return acc += getRectSum(expected);
+ }, 0);
+ const actualSum = rects.reduce((acc, rect) => acc += getRectSum(rect), 0);
+ return getDiffs({
+ stringA: actualSum,
+ stringB: expectedSum,
+ charDiff: true,
+ decorate: (diff) => `${diff} `,
+ });
+ };
+ const helpTitle = `Element.getClientRects()\nhash: ${hashMini(emojiSet)}\n${emojiSet.map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`;
+ return `
+
+
${performanceLogger.getLog().rects}
+
DOMRect ${hashSlice($hash)}
+
elems A: ${computeDiffs(elementClientRects)}
+
elems B: ${computeDiffs(elementBoundingClientRect)}
+
range A: ${computeDiffs(rangeClientRects)}
+
range B: ${computeDiffs(rangeBoundingClientRect)}
+
+ ${domrectSystemSum || HTMLNote.UNSUPPORTED}
+ ${formatEmojiSet(emojiSet)}
+
+
+ `;
+ }
+
+ function getErrors(errFns) {
+ const errors = [];
+ let i;
+ const len = errFns.length;
+ for (i = 0; i < len; i++) {
+ try {
+ errFns[i]();
+ }
+ catch (err) {
+ errors.push(err.message);
+ }
+ }
+ return errors;
+ }
+ function getConsoleErrors() {
+ try {
+ const timer = createTimer();
+ timer.start();
+ const errorTests = [
+ () => new Function('alert(")')(),
+ () => new Function('const foo;foo.bar')(),
+ () => new Function('null.bar')(),
+ () => new Function('abc.xyz = 123')(),
+ () => new Function('const foo;foo.bar')(),
+ () => new Function('(1).toString(1000)')(),
+ () => new Function('[...undefined].length')(),
+ () => new Function('var x = new Array(-1)')(),
+ () => new Function('const a=1; const a=2;')(),
+ ];
+ const errors = getErrors(errorTests);
+ logTestResult({ time: timer.stop(), test: 'console errors', passed: true });
+ return { errors };
+ }
+ catch (error) {
+ logTestResult({ test: 'console errors', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function consoleErrorsHTML(fp) {
+ if (!fp.consoleErrors) {
+ return `
+
+
Error
+
results: ${HTMLNote.BLOCKED}
+
`;
+ }
+ const { consoleErrors: { $hash, errors, }, } = fp;
+ const results = Object.keys(errors).map((key) => {
+ const value = errors[key];
+ return `${+key + 1}: ${value}`;
+ });
+ return `
+
+
${performanceLogger.getLog()['console errors']}
+
Error ${hashSlice($hash)}
+
results: ${modal('creep-console-errors', results.join(' '))}
+
+ `;
+ }
+
+ /*
+ Steps to update:
+ 0. get beta release desktop/mobile
+ 1. get diffs from template
+ 2. update feature list
+ 3. update stable features object
+ */
+ const getStableFeatures = () => ({
+ 'Chrome': {
+ version: 115,
+ windowKeys: `Object, Function, Array, Number, parseFloat, parseInt, Infinity, NaN, undefined, Boolean, String, Symbol, Date, Promise, RegExp, Error, AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, globalThis, JSON, Math, Intl, ArrayBuffer, Atomics, Uint8Array, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array, Uint8ClampedArray, BigUint64Array, BigInt64Array, DataView, Map, BigInt, Set, WeakMap, WeakSet, Proxy, Reflect, FinalizationRegistry, WeakRef, decodeURI, decodeURIComponent, encodeURI, encodeURIComponent, escape, unescape, eval, isFinite, isNaN, console, Option, Image, Audio, webkitURL, webkitRTCPeerConnection, webkitMediaStream, WebKitMutationObserver, WebKitCSSMatrix, XSLTProcessor, XPathResult, XPathExpression, XPathEvaluator, XMLSerializer, XMLHttpRequestUpload, XMLHttpRequestEventTarget, XMLHttpRequest, XMLDocument, WritableStreamDefaultWriter, WritableStreamDefaultController, WritableStream, Worker, Window, WheelEvent, WebSocket, WebGLVertexArrayObject, WebGLUniformLocation, WebGLTransformFeedback, WebGLTexture, WebGLSync, WebGLShaderPrecisionFormat, WebGLShader, WebGLSampler, WebGLRenderingContext, WebGLRenderbuffer, WebGLQuery, WebGLProgram, WebGLFramebuffer, WebGLContextEvent, WebGLBuffer, WebGLActiveInfo, WebGL2RenderingContext, WaveShaperNode, VisualViewport, VirtualKeyboardGeometryChangeEvent, ValidityState, VTTCue, UserActivation, URLSearchParams, URLPattern, URL, UIEvent, TrustedTypePolicyFactory, TrustedTypePolicy, TrustedScriptURL, TrustedScript, TrustedHTML, TreeWalker, TransitionEvent, TransformStreamDefaultController, TransformStream, TrackEvent, TouchList, TouchEvent, Touch, TimeRanges, TextTrackList, TextTrackCueList, TextTrackCue, TextTrack, TextMetrics, TextEvent, TextEncoderStream, TextEncoder, TextDecoderStream, TextDecoder, Text, TaskSignal, TaskPriorityChangeEvent, TaskController, TaskAttributionTiming, SyncManager, SubmitEvent, StyleSheetList, StyleSheet, StylePropertyMapReadOnly, StylePropertyMap, StorageEvent, Storage, StereoPannerNode, StaticRange, SourceBufferList, SourceBuffer, ShadowRoot, Selection, SecurityPolicyViolationEvent, ScriptProcessorNode, ScreenOrientation, Screen, Scheduling, Scheduler, SVGViewElement, SVGUseElement, SVGUnitTypes, SVGTransformList, SVGTransform, SVGTitleElement, SVGTextPositioningElement, SVGTextPathElement, SVGTextElement, SVGTextContentElement, SVGTSpanElement, SVGSymbolElement, SVGSwitchElement, SVGStyleElement, SVGStringList, SVGStopElement, SVGSetElement, SVGScriptElement, SVGSVGElement, SVGRectElement, SVGRect, SVGRadialGradientElement, SVGPreserveAspectRatio, SVGPolylineElement, SVGPolygonElement, SVGPointList, SVGPoint, SVGPatternElement, SVGPathElement, SVGNumberList, SVGNumber, SVGMetadataElement, SVGMatrix, SVGMaskElement, SVGMarkerElement, SVGMPathElement, SVGLinearGradientElement, SVGLineElement, SVGLengthList, SVGLength, SVGImageElement, SVGGraphicsElement, SVGGradientElement, SVGGeometryElement, SVGGElement, SVGForeignObjectElement, SVGFilterElement, SVGFETurbulenceElement, SVGFETileElement, SVGFESpotLightElement, SVGFESpecularLightingElement, SVGFEPointLightElement, SVGFEOffsetElement, SVGFEMorphologyElement, SVGFEMergeNodeElement, SVGFEMergeElement, SVGFEImageElement, SVGFEGaussianBlurElement, SVGFEFuncRElement, SVGFEFuncGElement, SVGFEFuncBElement, SVGFEFuncAElement, SVGFEFloodElement, SVGFEDropShadowElement, SVGFEDistantLightElement, SVGFEDisplacementMapElement, SVGFEDiffuseLightingElement, SVGFEConvolveMatrixElement, SVGFECompositeElement, SVGFEComponentTransferElement, SVGFEColorMatrixElement, SVGFEBlendElement, SVGEllipseElement, SVGElement, SVGDescElement, SVGDefsElement, SVGComponentTransferFunctionElement, SVGClipPathElement, SVGCircleElement, SVGAnimationElement, SVGAnimatedTransformList, SVGAnimatedString, SVGAnimatedRect, SVGAnimatedPreserveAspectRatio, SVGAnimatedNumberList, SVGAnimatedNumber, SVGAnimatedLengthList, SVGAnimatedLength, SVGAnimatedInteger, SVGAnimatedEnumeration, SVGAnimatedBoolean, SVGAnimatedAngle, SVGAnimateTransformElement, SVGAnimateMotionElement, SVGAnimateElement, SVGAngle, SVGAElement, Response, ResizeObserverSize, ResizeObserverEntry, ResizeObserver, Request, ReportingObserver, ReadableStreamDefaultReader, ReadableStreamDefaultController, ReadableStreamBYOBRequest, ReadableStreamBYOBReader, ReadableStream, ReadableByteStreamController, Range, RadioNodeList, RTCTrackEvent, RTCStatsReport, RTCSessionDescription, RTCSctpTransport, RTCRtpTransceiver, RTCRtpSender, RTCRtpReceiver, RTCPeerConnectionIceEvent, RTCPeerConnectionIceErrorEvent, RTCPeerConnection, RTCIceTransport, RTCIceCandidate, RTCErrorEvent, RTCError, RTCEncodedVideoFrame, RTCEncodedAudioFrame, RTCDtlsTransport, RTCDataChannelEvent, RTCDataChannel, RTCDTMFToneChangeEvent, RTCDTMFSender, RTCCertificate, PromiseRejectionEvent, ProgressEvent, Profiler, ProcessingInstruction, PopStateEvent, PointerEvent, PluginArray, Plugin, PictureInPictureWindow, PictureInPictureEvent, PeriodicWave, PerformanceTiming, PerformanceServerTiming, PerformanceResourceTiming, PerformancePaintTiming, PerformanceObserverEntryList, PerformanceObserver, PerformanceNavigationTiming, PerformanceNavigation, PerformanceMeasure, PerformanceMark, PerformanceLongTaskTiming, PerformanceEventTiming, PerformanceEntry, PerformanceElementTiming, Performance, Path2D, PannerNode, PageTransitionEvent, OverconstrainedError, OscillatorNode, OffscreenCanvasRenderingContext2D, OffscreenCanvas, OfflineAudioContext, OfflineAudioCompletionEvent, NodeList, NodeIterator, NodeFilter, Node, NetworkInformation, Navigator, NavigationTransition, NavigationHistoryEntry, NavigationDestination, NavigationCurrentEntryChangeEvent, Navigation, NavigateEvent, NamedNodeMap, MutationRecord, MutationObserver, MutationEvent, MouseEvent, MimeTypeArray, MimeType, MessagePort, MessageEvent, MessageChannel, MediaStreamTrackProcessor, MediaStreamTrackGenerator, MediaStreamTrackEvent, MediaStreamTrack, MediaStreamEvent, MediaStreamAudioSourceNode, MediaStreamAudioDestinationNode, MediaStream, MediaSourceHandle, MediaSource, MediaRecorder, MediaQueryListEvent, MediaQueryList, MediaList, MediaError, MediaEncryptedEvent, MediaElementAudioSourceNode, MediaCapabilities, Location, LayoutShiftAttribution, LayoutShift, LargestContentfulPaint, KeyframeEffect, KeyboardEvent, IntersectionObserverEntry, IntersectionObserver, InputEvent, InputDeviceInfo, InputDeviceCapabilities, ImageData, ImageCapture, ImageBitmapRenderingContext, ImageBitmap, IdleDeadline, IIRFilterNode, IDBVersionChangeEvent, IDBTransaction, IDBRequest, IDBOpenDBRequest, IDBObjectStore, IDBKeyRange, IDBIndex, IDBFactory, IDBDatabase, IDBCursorWithValue, IDBCursor, History, Headers, HashChangeEvent, HTMLVideoElement, HTMLUnknownElement, HTMLUListElement, HTMLTrackElement, HTMLTitleElement, HTMLTimeElement, HTMLTextAreaElement, HTMLTemplateElement, HTMLTableSectionElement, HTMLTableRowElement, HTMLTableElement, HTMLTableColElement, HTMLTableCellElement, HTMLTableCaptionElement, HTMLStyleElement, HTMLSpanElement, HTMLSourceElement, HTMLSlotElement, HTMLSelectElement, HTMLScriptElement, HTMLQuoteElement, HTMLProgressElement, HTMLPreElement, HTMLPictureElement, HTMLParamElement, HTMLParagraphElement, HTMLOutputElement, HTMLOptionsCollection, HTMLOptionElement, HTMLOptGroupElement, HTMLObjectElement, HTMLOListElement, HTMLModElement, HTMLMeterElement, HTMLMetaElement, HTMLMenuElement, HTMLMediaElement, HTMLMarqueeElement, HTMLMapElement, HTMLLinkElement, HTMLLegendElement, HTMLLabelElement, HTMLLIElement, HTMLInputElement, HTMLImageElement, HTMLIFrameElement, HTMLHtmlElement, HTMLHeadingElement, HTMLHeadElement, HTMLHRElement, HTMLFrameSetElement, HTMLFrameElement, HTMLFormElement, HTMLFormControlsCollection, HTMLFontElement, HTMLFieldSetElement, HTMLEmbedElement, HTMLElement, HTMLDocument, HTMLDivElement, HTMLDirectoryElement, HTMLDialogElement, HTMLDetailsElement, HTMLDataListElement, HTMLDataElement, HTMLDListElement, HTMLCollection, HTMLCanvasElement, HTMLButtonElement, HTMLBodyElement, HTMLBaseElement, HTMLBRElement, HTMLAudioElement, HTMLAreaElement, HTMLAnchorElement, HTMLAllCollection, GeolocationPositionError, GeolocationPosition, GeolocationCoordinates, Geolocation, GamepadHapticActuator, GamepadEvent, GamepadButton, Gamepad, GainNode, FormDataEvent, FormData, FontFaceSetLoadEvent, FontFace, FocusEvent, FileReader, FileList, File, FeaturePolicy, External, EventTarget, EventSource, EventCounts, Event, ErrorEvent, ElementInternals, Element, DynamicsCompressorNode, DragEvent, DocumentType, DocumentFragment, Document, DelayNode, DecompressionStream, DataTransferItemList, DataTransferItem, DataTransfer, DOMTokenList, DOMStringMap, DOMStringList, DOMRectReadOnly, DOMRectList, DOMRect, DOMQuad, DOMPointReadOnly, DOMPoint, DOMParser, DOMMatrixReadOnly, DOMMatrix, DOMImplementation, DOMException, DOMError, CustomStateSet, CustomEvent, CustomElementRegistry, Crypto, CountQueuingStrategy, ConvolverNode, ConstantSourceNode, CompressionStream, CompositionEvent, Comment, CloseEvent, ClipboardEvent, CharacterData, ChannelSplitterNode, ChannelMergerNode, CanvasRenderingContext2D, CanvasPattern, CanvasGradient, CanvasCaptureMediaStreamTrack, CSSVariableReferenceValue, CSSUnparsedValue, CSSUnitValue, CSSTranslate, CSSTransformValue, CSSTransformComponent, CSSSupportsRule, CSSStyleValue, CSSStyleSheet, CSSStyleRule, CSSStyleDeclaration, CSSSkewY, CSSSkewX, CSSSkew, CSSScale, CSSRuleList, CSSRule, CSSRotate, CSSPropertyRule, CSSPositionValue, CSSPerspective, CSSPageRule, CSSNumericValue, CSSNumericArray, CSSNamespaceRule, CSSMediaRule, CSSMatrixComponent, CSSMathValue, CSSMathSum, CSSMathProduct, CSSMathNegate, CSSMathMin, CSSMathMax, CSSMathInvert, CSSMathClamp, CSSLayerStatementRule, CSSLayerBlockRule, CSSKeywordValue, CSSKeyframesRule, CSSKeyframeRule, CSSImportRule, CSSImageValue, CSSGroupingRule, CSSFontPaletteValuesRule, CSSFontFaceRule, CSSCounterStyleRule, CSSContainerRule, CSSConditionRule, CSS, CDATASection, ByteLengthQueuingStrategy, BroadcastChannel, BlobEvent, Blob, BiquadFilterNode, BeforeUnloadEvent, BeforeInstallPromptEvent, BaseAudioContext, BarProp, AudioWorkletNode, AudioSinkInfo, AudioScheduledSourceNode, AudioProcessingEvent, AudioParamMap, AudioParam, AudioNode, AudioListener, AudioDestinationNode, AudioContext, AudioBufferSourceNode, AudioBuffer, Attr, AnimationEvent, AnimationEffect, Animation, AnalyserNode, AbstractRange, AbortSignal, AbortController, window, self, document, name, location, customElements, history, navigation, locationbar, menubar, personalbar, scrollbars, statusbar, toolbar, status, closed, frames, length, top, opener, parent, frameElement, navigator, origin, external, screen, innerWidth, innerHeight, scrollX, pageXOffset, scrollY, pageYOffset, visualViewport, screenX, screenY, outerWidth, outerHeight, devicePixelRatio, event, clientInformation, offscreenBuffering, screenLeft, screenTop, styleMedia, onsearch, isSecureContext, trustedTypes, performance, onappinstalled, onbeforeinstallprompt, crypto, indexedDB, sessionStorage, localStorage, onbeforexrselect, onabort, onbeforeinput, onblur, oncancel, oncanplay, oncanplaythrough, onchange, onclick, onclose, oncontextlost, oncontextmenu, oncontextrestored, oncuechange, ondblclick, ondrag, ondragend, ondragenter, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onerror, onfocus, onformdata, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmouseenter, onmouseleave, onmousemove, onmouseout, onmouseover, onmouseup, onmousewheel, onpause, onplay, onplaying, onprogress, onratechange, onreset, onresize, onscroll, onsecuritypolicyviolation, onseeked, onseeking, onselect, onslotchange, onstalled, onsubmit, onsuspend, ontimeupdate, ontoggle, onvolumechange, onwaiting, onwebkitanimationend, onwebkitanimationiteration, onwebkitanimationstart, onwebkittransitionend, onwheel, onauxclick, ongotpointercapture, onlostpointercapture, onpointerdown, onpointermove, onpointerrawupdate, onpointerup, onpointercancel, onpointerover, onpointerout, onpointerenter, onpointerleave, onselectstart, onselectionchange, onanimationend, onanimationiteration, onanimationstart, ontransitionrun, ontransitionstart, ontransitionend, ontransitioncancel, onafterprint, onbeforeprint, onbeforeunload, onhashchange, onlanguagechange, onmessage, onmessageerror, onoffline, ononline, onpagehide, onpageshow, onpopstate, onrejectionhandled, onstorage, onunhandledrejection, onunload, crossOriginIsolated, scheduler, alert, atob, blur, btoa, cancelAnimationFrame, cancelIdleCallback, captureEvents, clearInterval, clearTimeout, close, confirm, createImageBitmap, fetch, find, focus, getComputedStyle, getSelection, matchMedia, moveBy, moveTo, open, postMessage, print, prompt, queueMicrotask, releaseEvents, reportError, requestAnimationFrame, requestIdleCallback, resizeBy, resizeTo, scroll, scrollBy, scrollTo, setInterval, setTimeout, stop, structuredClone, webkitCancelAnimationFrame, webkitRequestAnimationFrame, chrome, WebAssembly, credentialless, caches, cookieStore, ondevicemotion, ondeviceorientation, ondeviceorientationabsolute, launchQueue, onbeforematch, onbeforetoggle, AbsoluteOrientationSensor, Accelerometer, AudioWorklet, BatteryManager, Cache, CacheStorage, Clipboard, ClipboardItem, CookieChangeEvent, CookieStore, CookieStoreManager, Credential, CredentialsContainer, CryptoKey, DeviceMotionEvent, DeviceMotionEventAcceleration, DeviceMotionEventRotationRate, DeviceOrientationEvent, FederatedCredential, GravitySensor, Gyroscope, Keyboard, KeyboardLayoutMap, LinearAccelerationSensor, Lock, LockManager, MIDIAccess, MIDIConnectionEvent, MIDIInput, MIDIInputMap, MIDIMessageEvent, MIDIOutput, MIDIOutputMap, MIDIPort, MediaDeviceInfo, MediaDevices, MediaKeyMessageEvent, MediaKeySession, MediaKeyStatusMap, MediaKeySystemAccess, MediaKeys, NavigationPreloadManager, NavigatorManagedData, OrientationSensor, PasswordCredential, RelativeOrientationSensor, Sanitizer, ScreenDetailed, ScreenDetails, Sensor, SensorErrorEvent, ServiceWorker, ServiceWorkerContainer, ServiceWorkerRegistration, StorageManager, SubtleCrypto, VirtualKeyboard, WebTransport, WebTransportBidirectionalStream, WebTransportDatagramDuplexStream, WebTransportError, Worklet, XRDOMOverlayState, XRLayer, XRWebGLBinding, AudioData, EncodedAudioChunk, EncodedVideoChunk, ImageTrack, ImageTrackList, VideoColorSpace, VideoFrame, AudioDecoder, AudioEncoder, ImageDecoder, VideoDecoder, VideoEncoder, AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, AuthenticatorResponse, PublicKeyCredential, Bluetooth, BluetoothCharacteristicProperties, BluetoothDevice, BluetoothRemoteGATTCharacteristic, BluetoothRemoteGATTDescriptor, BluetoothRemoteGATTServer, BluetoothRemoteGATTService, CaptureController, EyeDropper, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, FileSystemWritableFileStream, FontData, FragmentDirective, GPU, GPUAdapter, GPUAdapterInfo, GPUBindGroup, GPUBindGroupLayout, GPUBuffer, GPUBufferUsage, GPUCanvasContext, GPUColorWrite, GPUCommandBuffer, GPUCommandEncoder, GPUCompilationInfo, GPUCompilationMessage, GPUComputePassEncoder, GPUComputePipeline, GPUDevice, GPUDeviceLostInfo, GPUError, GPUExternalTexture, GPUInternalError, GPUMapMode, GPUOutOfMemoryError, GPUPipelineError, GPUPipelineLayout, GPUQuerySet, GPUQueue, GPURenderBundle, GPURenderBundleEncoder, GPURenderPassEncoder, GPURenderPipeline, GPUSampler, GPUShaderModule, GPUShaderStage, GPUSupportedFeatures, GPUSupportedLimits, GPUTexture, GPUTextureUsage, GPUTextureView, GPUUncapturedErrorEvent, GPUValidationError, HID, HIDConnectionEvent, HIDDevice, HIDInputReportEvent, IdentityCredential, IdleDetector, LaunchParams, LaunchQueue, OTPCredential, PaymentAddress, PaymentRequest, PaymentResponse, PaymentMethodChangeEvent, Presentation, PresentationAvailability, PresentationConnection, PresentationConnectionAvailableEvent, PresentationConnectionCloseEvent, PresentationConnectionList, PresentationReceiver, PresentationRequest, Serial, SerialPort, ToggleEvent, USB, USBAlternateInterface, USBConfiguration, USBConnectionEvent, USBDevice, USBEndpoint, USBInTransferResult, USBInterface, USBIsochronousInTransferPacket, USBIsochronousInTransferResult, USBIsochronousOutTransferPacket, USBIsochronousOutTransferResult, USBOutTransferResult, WakeLock, WakeLockSentinel, WindowControlsOverlay, WindowControlsOverlayGeometryChangeEvent, XRAnchor, XRAnchorSet, XRBoundedReferenceSpace, XRCPUDepthInformation, XRCamera, XRDepthInformation, XRFrame, XRHitTestResult, XRHitTestSource, XRInputSource, XRInputSourceArray, XRInputSourceEvent, XRInputSourcesChangeEvent, XRLightEstimate, XRLightProbe, XRPose, XRRay, XRReferenceSpace, XRReferenceSpaceEvent, XRRenderState, XRRigidTransform, XRSession, XRSessionEvent, XRSpace, XRSystem, XRTransientInputHitTestResult, XRTransientInputHitTestSource, XRView, XRViewerPose, XRViewport, XRWebGLDepthInformation, XRWebGLLayer, getScreenDetails, queryLocalFonts, showDirectoryPicker, showOpenFilePicker, showSaveFilePicker, originAgentCluster, speechSynthesis, oncontentvisibilityautostatechange, onscrollend, AnimationPlaybackEvent, AnimationTimeline, CSSAnimation, CSSTransition, DocumentTimeline, BackgroundFetchManager, BackgroundFetchRecord, BackgroundFetchRegistration, BluetoothUUID, BrowserCaptureMediaStreamTrack, CropTarget, ContentVisibilityAutoStateChangeEvent, DelegatedInkTrailPresenter, Ink, Highlight, HighlightRegistry, MathMLElement, MediaMetadata, MediaSession, NavigatorUAData, Notification, PaymentManager, PaymentRequestUpdateEvent, PeriodicSyncManager, PermissionStatus, Permissions, PushManager, PushSubscription, PushSubscriptionOptions, RemotePlayback, SharedWorker, SpeechSynthesisErrorEvent, SpeechSynthesisEvent, SpeechSynthesisUtterance, VideoPlaybackQuality, ViewTransition, webkitSpeechGrammar, webkitSpeechGrammarList, webkitSpeechRecognition, webkitSpeechRecognitionError, webkitSpeechRecognitionEvent, openDatabase, webkitRequestFileSystem, webkitResolveLocalFileSystemURL`,
+ cssKeys: `cssText, length, parentRule, cssFloat, getPropertyPriority, getPropertyValue, item, removeProperty, setProperty, constructor, accent-color, align-content, align-items, align-self, alignment-baseline, animation-composition, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, app-region, appearance, backdrop-filter, backface-visibility, background-attachment, background-blend-mode, background-clip, background-color, background-image, background-origin, background-position, background-repeat, background-size, baseline-shift, baseline-source, block-size, border-block-end-color, border-block-end-style, border-block-end-width, border-block-start-color, border-block-start-style, border-block-start-width, border-bottom-color, border-bottom-left-radius, border-bottom-right-radius, border-bottom-style, border-bottom-width, border-collapse, border-end-end-radius, border-end-start-radius, border-image-outset, border-image-repeat, border-image-slice, border-image-source, border-image-width, border-inline-end-color, border-inline-end-style, border-inline-end-width, border-inline-start-color, border-inline-start-style, border-inline-start-width, border-left-color, border-left-style, border-left-width, border-right-color, border-right-style, border-right-width, border-start-end-radius, border-start-start-radius, border-top-color, border-top-left-radius, border-top-right-radius, border-top-style, border-top-width, bottom, box-shadow, box-sizing, break-after, break-before, break-inside, buffered-rendering, caption-side, caret-color, clear, clip, clip-path, clip-rule, color, color-interpolation, color-interpolation-filters, color-rendering, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-span, column-width, contain-intrinsic-block-size, contain-intrinsic-height, contain-intrinsic-inline-size, contain-intrinsic-size, contain-intrinsic-width, container-name, container-type, content, cursor, cx, cy, d, direction, display, dominant-baseline, empty-cells, fill, fill-opacity, fill-rule, filter, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, float, flood-color, flood-opacity, font-family, font-kerning, font-optical-sizing, font-palette, font-size, font-stretch, font-style, font-synthesis-small-caps, font-synthesis-style, font-synthesis-weight, font-variant, font-variant-alternates, font-variant-caps, font-variant-east-asian, font-variant-ligatures, font-variant-numeric, font-weight, grid-auto-columns, grid-auto-flow, grid-auto-rows, grid-column-end, grid-column-start, grid-row-end, grid-row-start, grid-template-areas, grid-template-columns, grid-template-rows, height, hyphenate-character, hyphenate-limit-chars, hyphens, image-orientation, image-rendering, initial-letter, inline-size, inset-block-end, inset-block-start, inset-inline-end, inset-inline-start, isolation, justify-content, justify-items, justify-self, left, letter-spacing, lighting-color, line-break, line-height, list-style-image, list-style-position, list-style-type, margin-block-end, margin-block-start, margin-bottom, margin-inline-end, margin-inline-start, margin-left, margin-right, margin-top, marker-end, marker-mid, marker-start, mask-type, math-depth, math-shift, math-style, max-block-size, max-height, max-inline-size, max-width, min-block-size, min-height, min-inline-size, min-width, mix-blend-mode, object-fit, object-position, object-view-box, offset-distance, offset-path, offset-rotate, opacity, order, orphans, outline-color, outline-offset, outline-style, outline-width, overflow-anchor, overflow-clip-margin, overflow-wrap, overflow-x, overflow-y, overscroll-behavior-block, overscroll-behavior-inline, padding-block-end, padding-block-start, padding-bottom, padding-inline-end, padding-inline-start, padding-left, padding-right, padding-top, paint-order, perspective, perspective-origin, pointer-events, position, r, resize, right, rotate, row-gap, ruby-position, rx, ry, scale, scroll-behavior, scroll-margin-block-end, scroll-margin-block-start, scroll-margin-inline-end, scroll-margin-inline-start, scroll-padding-block-end, scroll-padding-block-start, scroll-padding-inline-end, scroll-padding-inline-start, scrollbar-gutter, shape-image-threshold, shape-margin, shape-outside, shape-rendering, speak, stop-color, stop-opacity, stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap, stroke-linejoin, stroke-miterlimit, stroke-opacity, stroke-width, tab-size, table-layout, text-align, text-align-last, text-anchor, text-decoration, text-decoration-color, text-decoration-line, text-decoration-skip-ink, text-decoration-style, text-emphasis-color, text-emphasis-position, text-emphasis-style, text-indent, text-overflow, text-rendering, text-shadow, text-size-adjust, text-transform, text-underline-position, text-wrap, top, touch-action, transform, transform-origin, transform-style, transition-delay, transition-duration, transition-property, transition-timing-function, translate, unicode-bidi, user-select, vector-effect, vertical-align, view-transition-name, visibility, white-space-collapse, widows, width, will-change, word-break, word-spacing, writing-mode, x, y, z-index, zoom, -webkit-border-horizontal-spacing, -webkit-border-image, -webkit-border-vertical-spacing, -webkit-box-align, -webkit-box-decoration-break, -webkit-box-direction, -webkit-box-flex, -webkit-box-ordinal-group, -webkit-box-orient, -webkit-box-pack, -webkit-box-reflect, -webkit-font-smoothing, -webkit-highlight, -webkit-line-break, -webkit-line-clamp, -webkit-locale, -webkit-mask-box-image, -webkit-mask-box-image-outset, -webkit-mask-box-image-repeat, -webkit-mask-box-image-slice, -webkit-mask-box-image-source, -webkit-mask-box-image-width, -webkit-mask-clip, -webkit-mask-composite, -webkit-mask-image, -webkit-mask-origin, -webkit-mask-position, -webkit-mask-repeat, -webkit-mask-size, -webkit-print-color-adjust, -webkit-rtl-ordering, -webkit-tap-highlight-color, -webkit-text-combine, -webkit-text-decorations-in-effect, -webkit-text-fill-color, -webkit-text-orientation, -webkit-text-security, -webkit-text-stroke-color, -webkit-text-stroke-width, -webkit-user-drag, -webkit-user-modify, -webkit-writing-mode, accentColor, additiveSymbols, alignContent, alignItems, alignSelf, alignmentBaseline, all, animation, animationComposition, animationDelay, animationDirection, animationDuration, animationFillMode, animationIterationCount, animationName, animationPlayState, animationTimingFunction, appRegion, ascentOverride, aspectRatio, backdropFilter, backfaceVisibility, background, backgroundAttachment, backgroundBlendMode, backgroundClip, backgroundColor, backgroundImage, backgroundOrigin, backgroundPosition, backgroundPositionX, backgroundPositionY, backgroundRepeat, backgroundRepeatX, backgroundRepeatY, backgroundSize, basePalette, baselineShift, baselineSource, blockSize, border, borderBlock, borderBlockColor, borderBlockEnd, borderBlockEndColor, borderBlockEndStyle, borderBlockEndWidth, borderBlockStart, borderBlockStartColor, borderBlockStartStyle, borderBlockStartWidth, borderBlockStyle, borderBlockWidth, borderBottom, borderBottomColor, borderBottomLeftRadius, borderBottomRightRadius, borderBottomStyle, borderBottomWidth, borderCollapse, borderColor, borderEndEndRadius, borderEndStartRadius, borderImage, borderImageOutset, borderImageRepeat, borderImageSlice, borderImageSource, borderImageWidth, borderInline, borderInlineColor, borderInlineEnd, borderInlineEndColor, borderInlineEndStyle, borderInlineEndWidth, borderInlineStart, borderInlineStartColor, borderInlineStartStyle, borderInlineStartWidth, borderInlineStyle, borderInlineWidth, borderLeft, borderLeftColor, borderLeftStyle, borderLeftWidth, borderRadius, borderRight, borderRightColor, borderRightStyle, borderRightWidth, borderSpacing, borderStartEndRadius, borderStartStartRadius, borderStyle, borderTop, borderTopColor, borderTopLeftRadius, borderTopRightRadius, borderTopStyle, borderTopWidth, borderWidth, boxShadow, boxSizing, breakAfter, breakBefore, breakInside, bufferedRendering, captionSide, caretColor, clipPath, clipRule, colorInterpolation, colorInterpolationFilters, colorRendering, colorScheme, columnCount, columnFill, columnGap, columnRule, columnRuleColor, columnRuleStyle, columnRuleWidth, columnSpan, columnWidth, columns, contain, containIntrinsicBlockSize, containIntrinsicHeight, containIntrinsicInlineSize, containIntrinsicSize, containIntrinsicWidth, container, containerName, containerType, contentVisibility, counterIncrement, counterReset, counterSet, descentOverride, dominantBaseline, emptyCells, fallback, fillOpacity, fillRule, flex, flexBasis, flexDirection, flexFlow, flexGrow, flexShrink, flexWrap, floodColor, floodOpacity, font, fontDisplay, fontFamily, fontFeatureSettings, fontKerning, fontOpticalSizing, fontPalette, fontSize, fontStretch, fontStyle, fontSynthesis, fontSynthesisSmallCaps, fontSynthesisStyle, fontSynthesisWeight, fontVariant, fontVariantAlternates, fontVariantCaps, fontVariantEastAsian, fontVariantLigatures, fontVariantNumeric, fontVariationSettings, fontWeight, forcedColorAdjust, gap, grid, gridArea, gridAutoColumns, gridAutoFlow, gridAutoRows, gridColumn, gridColumnEnd, gridColumnGap, gridColumnStart, gridGap, gridRow, gridRowEnd, gridRowGap, gridRowStart, gridTemplate, gridTemplateAreas, gridTemplateColumns, gridTemplateRows, hyphenateCharacter, hyphenateLimitChars, imageOrientation, imageRendering, inherits, initialLetter, initialValue, inlineSize, inset, insetBlock, insetBlockEnd, insetBlockStart, insetInline, insetInlineEnd, insetInlineStart, justifyContent, justifyItems, justifySelf, letterSpacing, lightingColor, lineBreak, lineGapOverride, lineHeight, listStyle, listStyleImage, listStylePosition, listStyleType, margin, marginBlock, marginBlockEnd, marginBlockStart, marginBottom, marginInline, marginInlineEnd, marginInlineStart, marginLeft, marginRight, marginTop, marker, markerEnd, markerMid, markerStart, mask, maskType, mathDepth, mathShift, mathStyle, maxBlockSize, maxHeight, maxInlineSize, maxWidth, minBlockSize, minHeight, minInlineSize, minWidth, mixBlendMode, negative, objectFit, objectPosition, objectViewBox, offset, offsetDistance, offsetPath, offsetRotate, outline, outlineColor, outlineOffset, outlineStyle, outlineWidth, overflow, overflowAnchor, overflowClipMargin, overflowWrap, overflowX, overflowY, overrideColors, overscrollBehavior, overscrollBehaviorBlock, overscrollBehaviorInline, overscrollBehaviorX, overscrollBehaviorY, pad, padding, paddingBlock, paddingBlockEnd, paddingBlockStart, paddingBottom, paddingInline, paddingInlineEnd, paddingInlineStart, paddingLeft, paddingRight, paddingTop, page, pageBreakAfter, pageBreakBefore, pageBreakInside, pageOrientation, paintOrder, perspectiveOrigin, placeContent, placeItems, placeSelf, pointerEvents, prefix, quotes, range, rowGap, rubyPosition, scrollBehavior, scrollMargin, scrollMarginBlock, scrollMarginBlockEnd, scrollMarginBlockStart, scrollMarginBottom, scrollMarginInline, scrollMarginInlineEnd, scrollMarginInlineStart, scrollMarginLeft, scrollMarginRight, scrollMarginTop, scrollPadding, scrollPaddingBlock, scrollPaddingBlockEnd, scrollPaddingBlockStart, scrollPaddingBottom, scrollPaddingInline, scrollPaddingInlineEnd, scrollPaddingInlineStart, scrollPaddingLeft, scrollPaddingRight, scrollPaddingTop, scrollSnapAlign, scrollSnapStop, scrollSnapType, scrollbarGutter, shapeImageThreshold, shapeMargin, shapeOutside, shapeRendering, size, sizeAdjust, speakAs, src, stopColor, stopOpacity, strokeDasharray, strokeDashoffset, strokeLinecap, strokeLinejoin, strokeMiterlimit, strokeOpacity, strokeWidth, suffix, symbols, syntax, system, tabSize, tableLayout, textAlign, textAlignLast, textAnchor, textCombineUpright, textDecoration, textDecorationColor, textDecorationLine, textDecorationSkipInk, textDecorationStyle, textDecorationThickness, textEmphasis, textEmphasisColor, textEmphasisPosition, textEmphasisStyle, textIndent, textOrientation, textOverflow, textRendering, textShadow, textSizeAdjust, textTransform, textUnderlineOffset, textUnderlinePosition, textWrap, touchAction, transformBox, transformOrigin, transformStyle, transition, transitionDelay, transitionDuration, transitionProperty, transitionTimingFunction, unicodeBidi, unicodeRange, userSelect, vectorEffect, verticalAlign, viewTransitionName, webkitAlignContent, webkitAlignItems, webkitAlignSelf, webkitAnimation, webkitAnimationDelay, webkitAnimationDirection, webkitAnimationDuration, webkitAnimationFillMode, webkitAnimationIterationCount, webkitAnimationName, webkitAnimationPlayState, webkitAnimationTimingFunction, webkitAppRegion, webkitAppearance, webkitBackfaceVisibility, webkitBackgroundClip, webkitBackgroundOrigin, webkitBackgroundSize, webkitBorderAfter, webkitBorderAfterColor, webkitBorderAfterStyle, webkitBorderAfterWidth, webkitBorderBefore, webkitBorderBeforeColor, webkitBorderBeforeStyle, webkitBorderBeforeWidth, webkitBorderBottomLeftRadius, webkitBorderBottomRightRadius, webkitBorderEnd, webkitBorderEndColor, webkitBorderEndStyle, webkitBorderEndWidth, webkitBorderHorizontalSpacing, webkitBorderImage, webkitBorderRadius, webkitBorderStart, webkitBorderStartColor, webkitBorderStartStyle, webkitBorderStartWidth, webkitBorderTopLeftRadius, webkitBorderTopRightRadius, webkitBorderVerticalSpacing, webkitBoxAlign, webkitBoxDecorationBreak, webkitBoxDirection, webkitBoxFlex, webkitBoxOrdinalGroup, webkitBoxOrient, webkitBoxPack, webkitBoxReflect, webkitBoxShadow, webkitBoxSizing, webkitClipPath, webkitColumnBreakAfter, webkitColumnBreakBefore, webkitColumnBreakInside, webkitColumnCount, webkitColumnGap, webkitColumnRule, webkitColumnRuleColor, webkitColumnRuleStyle, webkitColumnRuleWidth, webkitColumnSpan, webkitColumnWidth, webkitColumns, webkitFilter, webkitFlex, webkitFlexBasis, webkitFlexDirection, webkitFlexFlow, webkitFlexGrow, webkitFlexShrink, webkitFlexWrap, webkitFontFeatureSettings, webkitFontSmoothing, webkitHighlight, webkitHyphenateCharacter, webkitJustifyContent, webkitLineBreak, webkitLineClamp, webkitLocale, webkitLogicalHeight, webkitLogicalWidth, webkitMarginAfter, webkitMarginBefore, webkitMarginEnd, webkitMarginStart, webkitMask, webkitMaskBoxImage, webkitMaskBoxImageOutset, webkitMaskBoxImageRepeat, webkitMaskBoxImageSlice, webkitMaskBoxImageSource, webkitMaskBoxImageWidth, webkitMaskClip, webkitMaskComposite, webkitMaskImage, webkitMaskOrigin, webkitMaskPosition, webkitMaskPositionX, webkitMaskPositionY, webkitMaskRepeat, webkitMaskRepeatX, webkitMaskRepeatY, webkitMaskSize, webkitMaxLogicalHeight, webkitMaxLogicalWidth, webkitMinLogicalHeight, webkitMinLogicalWidth, webkitOpacity, webkitOrder, webkitPaddingAfter, webkitPaddingBefore, webkitPaddingEnd, webkitPaddingStart, webkitPerspective, webkitPerspectiveOrigin, webkitPerspectiveOriginX, webkitPerspectiveOriginY, webkitPrintColorAdjust, webkitRtlOrdering, webkitRubyPosition, webkitShapeImageThreshold, webkitShapeMargin, webkitShapeOutside, webkitTapHighlightColor, webkitTextCombine, webkitTextDecorationsInEffect, webkitTextEmphasis, webkitTextEmphasisColor, webkitTextEmphasisPosition, webkitTextEmphasisStyle, webkitTextFillColor, webkitTextOrientation, webkitTextSecurity, webkitTextSizeAdjust, webkitTextStroke, webkitTextStrokeColor, webkitTextStrokeWidth, webkitTransform, webkitTransformOrigin, webkitTransformOriginX, webkitTransformOriginY, webkitTransformOriginZ, webkitTransformStyle, webkitTransition, webkitTransitionDelay, webkitTransitionDuration, webkitTransitionProperty, webkitTransitionTimingFunction, webkitUserDrag, webkitUserModify, webkitUserSelect, webkitWritingMode, whiteSpace, whiteSpaceCollapse, willChange, wordBreak, wordSpacing, wordWrap, writingMode, zIndex, additive-symbols, ascent-override, aspect-ratio, background-position-x, background-position-y, background-repeat-x, background-repeat-y, base-palette, border-block, border-block-color, border-block-end, border-block-start, border-block-style, border-block-width, border-bottom, border-color, border-image, border-inline, border-inline-color, border-inline-end, border-inline-start, border-inline-style, border-inline-width, border-left, border-radius, border-right, border-spacing, border-style, border-top, border-width, color-scheme, column-fill, column-rule, content-visibility, counter-increment, counter-reset, counter-set, descent-override, flex-flow, font-display, font-feature-settings, font-synthesis, font-variation-settings, forced-color-adjust, grid-area, grid-column, grid-column-gap, grid-gap, grid-row, grid-row-gap, grid-template, initial-value, inset-block, inset-inline, line-gap-override, list-style, margin-block, margin-inline, override-colors, overscroll-behavior, overscroll-behavior-x, overscroll-behavior-y, padding-block, padding-inline, page-break-after, page-break-before, page-break-inside, page-orientation, place-content, place-items, place-self, scroll-margin, scroll-margin-block, scroll-margin-bottom, scroll-margin-inline, scroll-margin-left, scroll-margin-right, scroll-margin-top, scroll-padding, scroll-padding-block, scroll-padding-bottom, scroll-padding-inline, scroll-padding-left, scroll-padding-right, scroll-padding-top, scroll-snap-align, scroll-snap-stop, scroll-snap-type, size-adjust, speak-as, text-combine-upright, text-decoration-thickness, text-emphasis, text-orientation, text-underline-offset, transform-box, unicode-range, -webkit-align-content, -webkit-align-items, -webkit-align-self, -webkit-animation, -webkit-animation-delay, -webkit-animation-direction, -webkit-animation-duration, -webkit-animation-fill-mode, -webkit-animation-iteration-count, -webkit-animation-name, -webkit-animation-play-state, -webkit-animation-timing-function, -webkit-app-region, -webkit-appearance, -webkit-backface-visibility, -webkit-background-clip, -webkit-background-origin, -webkit-background-size, -webkit-border-after, -webkit-border-after-color, -webkit-border-after-style, -webkit-border-after-width, -webkit-border-before, -webkit-border-before-color, -webkit-border-before-style, -webkit-border-before-width, -webkit-border-bottom-left-radius, -webkit-border-bottom-right-radius, -webkit-border-end, -webkit-border-end-color, -webkit-border-end-style, -webkit-border-end-width, -webkit-border-radius, -webkit-border-start, -webkit-border-start-color, -webkit-border-start-style, -webkit-border-start-width, -webkit-border-top-left-radius, -webkit-border-top-right-radius, -webkit-box-shadow, -webkit-box-sizing, -webkit-clip-path, -webkit-column-break-after, -webkit-column-break-before, -webkit-column-break-inside, -webkit-column-count, -webkit-column-gap, -webkit-column-rule, -webkit-column-rule-color, -webkit-column-rule-style, -webkit-column-rule-width, -webkit-column-span, -webkit-column-width, -webkit-columns, -webkit-filter, -webkit-flex, -webkit-flex-basis, -webkit-flex-direction, -webkit-flex-flow, -webkit-flex-grow, -webkit-flex-shrink, -webkit-flex-wrap, -webkit-font-feature-settings, -webkit-hyphenate-character, -webkit-justify-content, -webkit-logical-height, -webkit-logical-width, -webkit-margin-after, -webkit-margin-before, -webkit-margin-end, -webkit-margin-start, -webkit-mask, -webkit-mask-position-x, -webkit-mask-position-y, -webkit-mask-repeat-x, -webkit-mask-repeat-y, -webkit-max-logical-height, -webkit-max-logical-width, -webkit-min-logical-height, -webkit-min-logical-width, -webkit-opacity, -webkit-order, -webkit-padding-after, -webkit-padding-before, -webkit-padding-end, -webkit-padding-start, -webkit-perspective, -webkit-perspective-origin, -webkit-perspective-origin-x, -webkit-perspective-origin-y, -webkit-ruby-position, -webkit-shape-image-threshold, -webkit-shape-margin, -webkit-shape-outside, -webkit-text-emphasis, -webkit-text-emphasis-color, -webkit-text-emphasis-position, -webkit-text-emphasis-style, -webkit-text-size-adjust, -webkit-text-stroke, -webkit-transform, -webkit-transform-origin, -webkit-transform-origin-x, -webkit-transform-origin-y, -webkit-transform-origin-z, -webkit-transform-style, -webkit-transition, -webkit-transition-delay, -webkit-transition-duration, -webkit-transition-property, -webkit-transition-timing-function, -webkit-user-select, white-space, word-wrap`,
+ jsKeys: 'Object.assign, Object.getOwnPropertyDescriptor, Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.hasOwn, Object.is, Object.preventExtensions, Object.seal, Object.create, Object.defineProperties, Object.defineProperty, Object.freeze, Object.getPrototypeOf, Object.setPrototypeOf, Object.isExtensible, Object.isFrozen, Object.isSealed, Object.keys, Object.entries, Object.fromEntries, Object.values, Object.__defineGetter__, Object.__defineSetter__, Object.hasOwnProperty, Object.__lookupGetter__, Object.__lookupSetter__, Object.isPrototypeOf, Object.propertyIsEnumerable, Object.toString, Object.valueOf, Object.__proto__, Object.toLocaleString, Function.apply, Function.bind, Function.call, Function.toString, Boolean.toString, Boolean.valueOf, Symbol.for, Symbol.keyFor, Symbol.asyncIterator, Symbol.hasInstance, Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.matchAll, Symbol.replace, Symbol.search, Symbol.species, Symbol.split, Symbol.toPrimitive, Symbol.toStringTag, Symbol.unscopables, Symbol.toString, Symbol.valueOf, Symbol.description, Error.captureStackTrace, Error.stackTraceLimit, Error.message, Error.toString, Number.isFinite, Number.isInteger, Number.isNaN, Number.isSafeInteger, Number.parseFloat, Number.parseInt, Number.MAX_VALUE, Number.MIN_VALUE, Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.EPSILON, Number.toExponential, Number.toFixed, Number.toPrecision, Number.toString, Number.valueOf, Number.toLocaleString, BigInt.asUintN, BigInt.asIntN, BigInt.toLocaleString, BigInt.toString, BigInt.valueOf, Math.abs, Math.acos, Math.acosh, Math.asin, Math.asinh, Math.atan, Math.atanh, Math.atan2, Math.ceil, Math.cbrt, Math.expm1, Math.clz32, Math.cos, Math.cosh, Math.exp, Math.floor, Math.fround, Math.hypot, Math.imul, Math.log, Math.log1p, Math.log2, Math.log10, Math.max, Math.min, Math.pow, Math.random, Math.round, Math.sign, Math.sin, Math.sinh, Math.sqrt, Math.tan, Math.tanh, Math.trunc, Math.E, Math.LN10, Math.LN2, Math.LOG10E, Math.LOG2E, Math.PI, Math.SQRT1_2, Math.SQRT2, Date.now, Date.parse, Date.UTC, Date.toString, Date.toDateString, Date.toTimeString, Date.toISOString, Date.toUTCString, Date.toGMTString, Date.getDate, Date.setDate, Date.getDay, Date.getFullYear, Date.setFullYear, Date.getHours, Date.setHours, Date.getMilliseconds, Date.setMilliseconds, Date.getMinutes, Date.setMinutes, Date.getMonth, Date.setMonth, Date.getSeconds, Date.setSeconds, Date.getTime, Date.setTime, Date.getTimezoneOffset, Date.getUTCDate, Date.setUTCDate, Date.getUTCDay, Date.getUTCFullYear, Date.setUTCFullYear, Date.getUTCHours, Date.setUTCHours, Date.getUTCMilliseconds, Date.setUTCMilliseconds, Date.getUTCMinutes, Date.setUTCMinutes, Date.getUTCMonth, Date.setUTCMonth, Date.getUTCSeconds, Date.setUTCSeconds, Date.valueOf, Date.getYear, Date.setYear, Date.toJSON, Date.toLocaleString, Date.toLocaleDateString, Date.toLocaleTimeString, String.fromCharCode, String.fromCodePoint, String.raw, String.anchor, String.at, String.big, String.blink, String.bold, String.charAt, String.charCodeAt, String.codePointAt, String.concat, String.endsWith, String.fontcolor, String.fontsize, String.fixed, String.includes, String.indexOf, String.italics, String.lastIndexOf, String.link, String.localeCompare, String.match, String.matchAll, String.normalize, String.padEnd, String.padStart, String.repeat, String.replace, String.replaceAll, String.search, String.slice, String.small, String.split, String.strike, String.sub, String.substr, String.substring, String.sup, String.startsWith, String.toString, String.trim, String.trimStart, String.trimLeft, String.trimEnd, String.trimRight, String.toLocaleLowerCase, String.toLocaleUpperCase, String.toLowerCase, String.toUpperCase, String.valueOf, String.isWellFormed, String.toWellFormed, RegExp.input, RegExp.$_, RegExp.lastMatch, RegExp.$&, RegExp.lastParen, RegExp.$+, RegExp.leftContext, RegExp.$`, RegExp.rightContext, RegExp.$\', RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$6, RegExp.$7, RegExp.$8, RegExp.$9, RegExp.exec, RegExp.dotAll, RegExp.flags, RegExp.global, RegExp.hasIndices, RegExp.ignoreCase, RegExp.multiline, RegExp.source, RegExp.sticky, RegExp.unicode, RegExp.compile, RegExp.toString, RegExp.test, RegExp.unicodeSets, Array.isArray, Array.from, Array.of, Array.at, Array.concat, Array.copyWithin, Array.fill, Array.find, Array.findIndex, Array.findLast, Array.findLastIndex, Array.lastIndexOf, Array.pop, Array.push, Array.reverse, Array.shift, Array.unshift, Array.slice, Array.sort, Array.splice, Array.includes, Array.indexOf, Array.join, Array.keys, Array.entries, Array.values, Array.forEach, Array.filter, Array.flat, Array.flatMap, Array.map, Array.every, Array.some, Array.reduce, Array.reduceRight, Array.toLocaleString, Array.toString, Array.toReversed, Array.toSorted, Array.toSpliced, Array.with, Map.get, Map.set, Map.has, Map.delete, Map.clear, Map.entries, Map.forEach, Map.keys, Map.size, Map.values, Set.has, Set.add, Set.delete, Set.clear, Set.entries, Set.forEach, Set.size, Set.values, Set.keys, WeakMap.delete, WeakMap.get, WeakMap.set, WeakMap.has, WeakSet.delete, WeakSet.has, WeakSet.add, Atomics.load, Atomics.store, Atomics.add, Atomics.sub, Atomics.and, Atomics.or, Atomics.xor, Atomics.exchange, Atomics.compareExchange, Atomics.isLockFree, Atomics.wait, Atomics.waitAsync, Atomics.notify, JSON.parse, JSON.stringify, JSON.rawJSON, JSON.isRawJSON, Promise.all, Promise.allSettled, Promise.any, Promise.race, Promise.resolve, Promise.reject, Promise.then, Promise.catch, Promise.finally, Reflect.defineProperty, Reflect.deleteProperty, Reflect.apply, Reflect.construct, Reflect.get, Reflect.getOwnPropertyDescriptor, Reflect.getPrototypeOf, Reflect.has, Reflect.isExtensible, Reflect.ownKeys, Reflect.preventExtensions, Reflect.set, Reflect.setPrototypeOf, Proxy.revocable, Intl.getCanonicalLocales, Intl.supportedValuesOf, Intl.DateTimeFormat, Intl.NumberFormat, Intl.Collator, Intl.v8BreakIterator, Intl.PluralRules, Intl.RelativeTimeFormat, Intl.ListFormat, Intl.Locale, Intl.DisplayNames, Intl.Segmenter, WebAssembly.compile, WebAssembly.validate, WebAssembly.instantiate, WebAssembly.compileStreaming, WebAssembly.instantiateStreaming, WebAssembly.Module, WebAssembly.Instance, WebAssembly.Table, WebAssembly.Memory, WebAssembly.Global, WebAssembly.Tag, WebAssembly.Exception, WebAssembly.CompileError, WebAssembly.LinkError, WebAssembly.RuntimeError, Document.implementation, Document.URL, Document.documentURI, Document.compatMode, Document.characterSet, Document.charset, Document.inputEncoding, Document.contentType, Document.doctype, Document.documentElement, Document.xmlEncoding, Document.xmlVersion, Document.xmlStandalone, Document.domain, Document.referrer, Document.cookie, Document.lastModified, Document.readyState, Document.title, Document.dir, Document.body, Document.head, Document.images, Document.embeds, Document.plugins, Document.links, Document.forms, Document.scripts, Document.currentScript, Document.defaultView, Document.designMode, Document.onreadystatechange, Document.anchors, Document.applets, Document.fgColor, Document.linkColor, Document.vlinkColor, Document.alinkColor, Document.bgColor, Document.all, Document.scrollingElement, Document.onpointerlockchange, Document.onpointerlockerror, Document.hidden, Document.visibilityState, Document.wasDiscarded, Document.prerendering, Document.featurePolicy, Document.webkitVisibilityState, Document.webkitHidden, Document.onbeforecopy, Document.onbeforecut, Document.onbeforepaste, Document.onfreeze, Document.onprerenderingchange, Document.onresume, Document.onsearch, Document.onvisibilitychange, Document.fullscreenEnabled, Document.fullscreen, Document.onfullscreenchange, Document.onfullscreenerror, Document.webkitIsFullScreen, Document.webkitCurrentFullScreenElement, Document.webkitFullscreenEnabled, Document.webkitFullscreenElement, Document.onwebkitfullscreenchange, Document.onwebkitfullscreenerror, Document.rootElement, Document.pictureInPictureEnabled, Document.pictureInPictureElement, Document.onbeforexrselect, Document.onabort, Document.onbeforeinput, Document.onblur, Document.oncancel, Document.oncanplay, Document.oncanplaythrough, Document.onchange, Document.onclick, Document.onclose, Document.oncontextlost, Document.oncontextmenu, Document.oncontextrestored, Document.oncuechange, Document.ondblclick, Document.ondrag, Document.ondragend, Document.ondragenter, Document.ondragleave, Document.ondragover, Document.ondragstart, Document.ondrop, Document.ondurationchange, Document.onemptied, Document.onended, Document.onerror, Document.onfocus, Document.onformdata, Document.oninput, Document.oninvalid, Document.onkeydown, Document.onkeypress, Document.onkeyup, Document.onload, Document.onloadeddata, Document.onloadedmetadata, Document.onloadstart, Document.onmousedown, Document.onmouseenter, Document.onmouseleave, Document.onmousemove, Document.onmouseout, Document.onmouseover, Document.onmouseup, Document.onmousewheel, Document.onpause, Document.onplay, Document.onplaying, Document.onprogress, Document.onratechange, Document.onreset, Document.onresize, Document.onscroll, Document.onsecuritypolicyviolation, Document.onseeked, Document.onseeking, Document.onselect, Document.onslotchange, Document.onstalled, Document.onsubmit, Document.onsuspend, Document.ontimeupdate, Document.ontoggle, Document.onvolumechange, Document.onwaiting, Document.onwebkitanimationend, Document.onwebkitanimationiteration, Document.onwebkitanimationstart, Document.onwebkittransitionend, Document.onwheel, Document.onauxclick, Document.ongotpointercapture, Document.onlostpointercapture, Document.onpointerdown, Document.onpointermove, Document.onpointerrawupdate, Document.onpointerup, Document.onpointercancel, Document.onpointerover, Document.onpointerout, Document.onpointerenter, Document.onpointerleave, Document.onselectstart, Document.onselectionchange, Document.onanimationend, Document.onanimationiteration, Document.onanimationstart, Document.ontransitionrun, Document.ontransitionstart, Document.ontransitionend, Document.ontransitioncancel, Document.oncopy, Document.oncut, Document.onpaste, Document.children, Document.firstElementChild, Document.lastElementChild, Document.childElementCount, Document.activeElement, Document.styleSheets, Document.pointerLockElement, Document.fullscreenElement, Document.adoptedStyleSheets, Document.fonts, Document.adoptNode, Document.append, Document.captureEvents, Document.caretRangeFromPoint, Document.clear, Document.close, Document.createAttribute, Document.createAttributeNS, Document.createCDATASection, Document.createComment, Document.createDocumentFragment, Document.createElement, Document.createElementNS, Document.createEvent, Document.createExpression, Document.createNSResolver, Document.createNodeIterator, Document.createProcessingInstruction, Document.createRange, Document.createTextNode, Document.createTreeWalker, Document.elementFromPoint, Document.elementsFromPoint, Document.evaluate, Document.execCommand, Document.exitFullscreen, Document.exitPictureInPicture, Document.exitPointerLock, Document.getElementById, Document.getElementsByClassName, Document.getElementsByName, Document.getElementsByTagName, Document.getElementsByTagNameNS, Document.getSelection, Document.hasFocus, Document.importNode, Document.open, Document.prepend, Document.queryCommandEnabled, Document.queryCommandIndeterm, Document.queryCommandState, Document.queryCommandSupported, Document.queryCommandValue, Document.querySelector, Document.querySelectorAll, Document.releaseEvents, Document.replaceChildren, Document.webkitCancelFullScreen, Document.webkitExitFullscreen, Document.write, Document.writeln, Document.fragmentDirective, Document.onbeforematch, Document.onbeforetoggle, Document.timeline, Document.oncontentvisibilityautostatechange, Document.onscrollend, Document.getAnimations, Document.startViewTransition, Element.namespaceURI, Element.prefix, Element.localName, Element.tagName, Element.id, Element.className, Element.classList, Element.slot, Element.attributes, Element.shadowRoot, Element.part, Element.assignedSlot, Element.innerHTML, Element.outerHTML, Element.scrollTop, Element.scrollLeft, Element.scrollWidth, Element.scrollHeight, Element.clientTop, Element.clientLeft, Element.clientWidth, Element.clientHeight, Element.onbeforecopy, Element.onbeforecut, Element.onbeforepaste, Element.onsearch, Element.elementTiming, Element.onfullscreenchange, Element.onfullscreenerror, Element.onwebkitfullscreenchange, Element.onwebkitfullscreenerror, Element.role, Element.ariaAtomic, Element.ariaAutoComplete, Element.ariaBusy, Element.ariaBrailleLabel, Element.ariaBrailleRoleDescription, Element.ariaChecked, Element.ariaColCount, Element.ariaColIndex, Element.ariaColSpan, Element.ariaCurrent, Element.ariaDescription, Element.ariaDisabled, Element.ariaExpanded, Element.ariaHasPopup, Element.ariaHidden, Element.ariaInvalid, Element.ariaKeyShortcuts, Element.ariaLabel, Element.ariaLevel, Element.ariaLive, Element.ariaModal, Element.ariaMultiLine, Element.ariaMultiSelectable, Element.ariaOrientation, Element.ariaPlaceholder, Element.ariaPosInSet, Element.ariaPressed, Element.ariaReadOnly, Element.ariaRelevant, Element.ariaRequired, Element.ariaRoleDescription, Element.ariaRowCount, Element.ariaRowIndex, Element.ariaRowSpan, Element.ariaSelected, Element.ariaSetSize, Element.ariaSort, Element.ariaValueMax, Element.ariaValueMin, Element.ariaValueNow, Element.ariaValueText, Element.children, Element.firstElementChild, Element.lastElementChild, Element.childElementCount, Element.previousElementSibling, Element.nextElementSibling, Element.after, Element.animate, Element.append, Element.attachShadow, Element.before, Element.closest, Element.computedStyleMap, Element.getAttribute, Element.getAttributeNS, Element.getAttributeNames, Element.getAttributeNode, Element.getAttributeNodeNS, Element.getBoundingClientRect, Element.getClientRects, Element.getElementsByClassName, Element.getElementsByTagName, Element.getElementsByTagNameNS, Element.getInnerHTML, Element.hasAttribute, Element.hasAttributeNS, Element.hasAttributes, Element.hasPointerCapture, Element.insertAdjacentElement, Element.insertAdjacentHTML, Element.insertAdjacentText, Element.matches, Element.prepend, Element.querySelector, Element.querySelectorAll, Element.releasePointerCapture, Element.remove, Element.removeAttribute, Element.removeAttributeNS, Element.removeAttributeNode, Element.replaceChildren, Element.replaceWith, Element.requestFullscreen, Element.requestPointerLock, Element.scroll, Element.scrollBy, Element.scrollIntoView, Element.scrollIntoViewIfNeeded, Element.scrollTo, Element.setAttribute, Element.setAttributeNS, Element.setAttributeNode, Element.setAttributeNodeNS, Element.setPointerCapture, Element.toggleAttribute, Element.webkitMatchesSelector, Element.webkitRequestFullScreen, Element.webkitRequestFullscreen, Element.checkVisibility, Element.getAnimations, Element.setHTML',
+ },
+ 'Firefox': {
+ version: 112,
+ windowKeys: `undefined, globalThis, Array, Boolean, JSON, Date, Math, Number, String, RegExp, Error, InternalError, AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, ArrayBuffer, Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, Uint8ClampedArray, BigInt64Array, BigUint64Array, BigInt, Proxy, WeakMap, Set, DataView, Symbol, Intl, Reflect, WeakSet, Atomics, WebAssembly, FinalizationRegistry, WeakRef, NaN, Infinity, isNaN, isFinite, parseFloat, parseInt, escape, unescape, decodeURI, encodeURI, decodeURIComponent, encodeURIComponent, CryptoKey, FocusEvent, CSSRuleList, MediaStreamTrackAudioSourceNode, SVGGeometryElement, SVGElement, SVGPatternElement, WebSocket, HTMLAllCollection, UIEvent, PageTransitionEvent, AuthenticatorAssertionResponse, ScreenOrientation, MediaCapabilitiesInfo, SVGImageElement, NodeIterator, SVGFEOffsetElement, AnimationPlaybackEvent, MessageChannel, TextDecoderStream, ShadowRoot, SVGAnimatedNumberList, SVGEllipseElement, DOMStringMap, AudioWorkletNode, TextMetrics, SVGPointList, SVGSymbolElement, DocumentType, StorageEvent, SVGAnimatedLength, PerformanceObserver, WebGLSampler, PushSubscription, CustomElementRegistry, SVGUnitTypes, SVGFEMorphologyElement, NodeFilter, File, Geolocation, ProcessingInstruction, AudioScheduledSourceNode, FileReader, IDBObjectStore, SVGStringList, HTMLParagraphElement, IDBDatabase, SVGLinearGradientElement, Animation, HTMLIFrameElement, HTMLTableSectionElement, Worker, TimeRanges, Navigator, InputEvent, GamepadHapticActuator, SVGMatrix, Worklet, SVGFEMergeNodeElement, DOMTokenList, MediaQueryListEvent, HTMLParamElement, MessagePort, SVGLengthList, ResizeObserverSize, TextEncoderStream, PromiseRejectionEvent, SVGTransformList, DOMPoint, SVGTextElement, WebGL2RenderingContext, PluginArray, IDBVersionChangeEvent, FontFaceSetLoadEvent, CSSStyleDeclaration, SVGGraphicsElement, HTMLOListElement, HTMLTextAreaElement, Storage, XPathEvaluator, MouseScrollEvent, HTMLCanvasElement, HTMLBodyElement, HTMLCollection, HTMLHtmlElement, MediaList, HTMLAudioElement, IDBFactory, SVGAnimatedTransformList, MimeType, SVGAnimatedPreserveAspectRatio, HTMLFrameElement, HTMLLegendElement, HTMLMapElement, SVGAnimateMotionElement, HTMLFrameSetElement, CSSGroupingRule, Clipboard, IIRFilterNode, ReadableStreamDefaultController, FileSystemFileHandle, HTMLElement, CSSConditionRule, CSS, SVGFEComponentTransferElement, DOMRectList, AudioWorklet, SourceBuffer, HTMLStyleElement, DocumentTimeline, IDBKeyRange, DOMRequest, PerformanceTiming, GeolocationPosition, SVGTextPositioningElement, OfflineResourceList, IDBOpenDBRequest, SVGFEFuncGElement, MouseEvent, FontFaceSet, OffscreenCanvasRenderingContext2D, OscillatorNode, SpeechSynthesisUtterance, AudioContext, FileSystemEntry, PaintRequest, SVGFEConvolveMatrixElement, ChannelSplitterNode, AudioBufferSourceNode, CaretPosition, AbortSignal, HTMLBRElement, XSLTProcessor, SVGFESpecularLightingElement, mozRTCPeerConnection, XMLSerializer, StorageManager, HTMLImageElement, WebGLQuery, FileSystemHandle, Permissions, BaseAudioContext, MediaStream, History, DOMStringList, StereoPannerNode, ReadableStreamDefaultReader, HTMLDListElement, MutationEvent, SVGComponentTransferFunctionElement, Notification, DynamicsCompressorNode, LockManager, Option, HTMLMeterElement, RTCPeerConnection, CloseEvent, AudioBuffer, ByteLengthQueuingStrategy, SVGFECompositeElement, HTMLTrackElement, ServiceWorkerContainer, MediaStreamTrack, WebGLContextEvent, WritableStreamDefaultController, SVGPathElement, BarProp, PerformanceObserverEntryList, RTCRtpReceiver, StyleSheet, WebGLUniformLocation, HTMLMetaElement, CanvasPattern, OffscreenCanvas, SVGTransform, SVGTextContentElement, PerformanceServerTiming, TrackEvent, XPathExpression, AnimationTimeline, MediaError, HTMLAnchorElement, XMLHttpRequest, SecurityPolicyViolationEvent, CSSMozDocumentRule, CSSImportRule, SVGLength, WaveShaperNode, RTCTrackEvent, RTCRtpSender, CSSAnimation, CSSFontPaletteValuesRule, AnimationEffect, CSSContainerRule, RTCStatsReport, SourceBufferList, HTMLHeadingElement, CanvasCaptureMediaStream, HTMLOptionElement, SVGSVGElement, WebGLActiveInfo, MIDIPort, HTMLUListElement, XPathResult, SVGUseElement, Credential, PublicKeyCredential, DOMQuad, Selection, HTMLDataElement, CSSLayerStatementRule, WebGLShader, Location, MIDIAccess, MediaRecorderErrorEvent, SVGFEGaussianBlurElement, MediaStreamEvent, CSSFontFeatureValuesRule, AbortController, RTCIceCandidate, HTMLLabelElement, PerformanceMeasure, HTMLDirectoryElement, SVGStopElement, PermissionStatus, PerformancePaintTiming, FileSystemFileEntry, SVGMarkerElement, console, SharedWorker, WebGLVertexArrayObject, HTMLOptionsCollection, HTMLTitleElement, TreeWalker, CompositionEvent, IDBCursor, TransformStream, PerformanceNavigation, Blob, SpeechSynthesisErrorEvent, CSSStyleSheet, HTMLUnknownElement, KeyEvent, HTMLOptGroupElement, CanvasGradient, AnalyserNode, Element, AnimationEvent, HTMLFieldSetElement, MediaSession, MutationObserver, SVGAnimateTransformElement, OfflineAudioContext, CacheStorage, MediaKeyStatusMap, GamepadEvent, RTCPeerConnectionIceEvent, AudioParam, AbstractRange, TextEncoder, FileSystemDirectoryHandle, CSSSupportsRule, SVGPreserveAspectRatio, PerformanceEntry, mozRTCSessionDescription, VideoPlaybackQuality, HTMLTableRowElement, HTMLFontElement, MediaKeys, DataTransfer, CSSPageRule, SVGAngle, WebGLFramebuffer, WebGLRenderbuffer, Directory, HTMLAreaElement, MimeTypeArray, NamedNodeMap, CSSKeyframeRule, XMLDocument, HTMLSlotElement, MediaDevices, IDBTransaction, HTMLModElement, MediaKeyError, SVGFEFloodElement, DOMParser, HTMLScriptElement, ReadableStreamBYOBReader, HTMLDataListElement, MediaElementAudioSourceNode, PeriodicWave, DragEvent, SVGStyleElement, SubtleCrypto, TransformStreamDefaultController, MessageEvent, WebGLProgram, SVGSetElement, WebGLShaderPrecisionFormat, ErrorEvent, NodeList, GamepadButton, MediaDeviceInfo, HTMLMediaElement, DeviceMotionEvent, ImageData, Range, PushSubscriptionOptions, OfflineAudioCompletionEvent, DOMPointReadOnly, DocumentFragment, Attr, BroadcastChannel, HTMLLinkElement, DOMMatrixReadOnly, AuthenticatorAttestationResponse, IdleDeadline, ImageBitmapRenderingContext, MediaKeySystemAccess, SVGPoint, SVGFEDropShadowElement, SVGFEDiffuseLightingElement, SVGRadialGradientElement, RTCDataChannelEvent, Headers, FileSystemDirectoryReader, SVGFEDisplacementMapElement, SVGDefsElement, SVGFEDistantLightElement, HTMLFormControlsCollection, WebGLRenderingContext, HTMLTableElement, SVGNumber, SVGAnimatedInteger, VisualViewport, SpeechSynthesisVoice, WheelEvent, SVGAnimatedNumber, GamepadPose, ResizeObserver, TextDecoder, FormDataEvent, FileSystem, IntersectionObserverEntry, SVGPolylineElement, Text, CanvasRenderingContext2D, CSSFontFaceRule, SVGGradientElement, WritableStream, SVGNumberList, SpeechSynthesisEvent, WritableStreamDefaultWriter, SVGFilterElement, URL, SVGRect, SVGFEImageElement, AudioDestinationNode, IDBRequest, MathMLElement, HTMLTimeElement, TextTrackCue, RadioNodeList, SVGTSpanElement, SVGAnimatedLengthList, SVGAnimatedString, HTMLSourceElement, Lock, ProgressEvent, PopupBlockedEvent, ValidityState, WebKitCSSMatrix, AudioListener, HTMLFormElement, PerformanceEventTiming, HTMLProgressElement, Gamepad, MediaKeySession, MediaStreamAudioSourceNode, CSSRule, HTMLPreElement, webkitURL, AuthenticatorResponse, HTMLEmbedElement, HTMLDivElement, MediaStreamAudioDestinationNode, CredentialsContainer, SVGScriptElement, MutationRecord, ConvolverNode, SVGFEPointLightElement, Screen, ClipboardEvent, SVGFETurbulenceElement, SVGFEBlendElement, GeolocationCoordinates, TextTrackList, FontFace, HTMLInputElement, Request, SVGGElement, SVGClipPathElement, TimeEvent, TextTrackCueList, BiquadFilterNode, SVGTitleElement, SVGFETileElement, SVGFEFuncRElement, HTMLButtonElement, Path2D, HTMLTableCellElement, StaticRange, SVGLineElement, CSSCounterStyleRule, HTMLQuoteElement, AudioParamMap, DeviceOrientationEvent, IDBCursorWithValue, MediaKeyMessageEvent, SVGAnimatedEnumeration, MIDIOutput, HTMLHRElement, ImageBitmap, CSSStyleRule, MIDIOutputMap, SVGAElement, ElementInternals, VTTCue, MediaMetadata, PannerNode, SVGForeignObjectElement, SVGSwitchElement, HTMLVideoElement, MediaEncryptedEvent, EventSource, SVGAnimateElement, DOMException, CSSTransition, Image, VTTRegion, CharacterData, SVGFEFuncAElement, SVGDescElement, SubmitEvent, RTCSessionDescription, SVGAnimatedBoolean, StyleSheetList, HTMLTableColElement, HTMLMarqueeElement, CSSKeyframesRule, SVGMetadataElement, MIDIInputMap, PushManager, GainNode, DOMRect, SVGMaskElement, HTMLMenuElement, RTCDTMFToneChangeEvent, IDBIndex, CSSMediaRule, HashChangeEvent, ServiceWorker, SVGFEFuncBElement, BlobEvent, DOMImplementation, GeolocationPositionError, SVGMPathElement, SVGFEColorMatrixElement, KeyboardEvent, HTMLLIElement, RTCCertificate, SpeechSynthesis, ReadableStreamBYOBRequest, DelayNode, XMLHttpRequestEventTarget, PopStateEvent, Cache, ScrollAreaEvent, RTCDtlsTransport, SVGTextPathElement, XMLHttpRequestUpload, HTMLOutputElement, MediaRecorder, PaintRequestList, SVGRectElement, AudioNode, DOMMatrix, MediaSource, FormData, NavigationPreloadManager, HTMLTableCaptionElement, CustomEvent, MediaCapabilities, SVGFEMergeElement, MediaStreamTrackEvent, Audio, CSS2Properties, FileList, SVGAnimatedRect, FileSystemWritableFileStream, ReadableStream, HTMLPictureElement, HTMLSelectElement, AudioProcessingEvent, PerformanceResourceTiming, Plugin, Crypto, CSSLayerBlockRule, ConstantSourceNode, HTMLSpanElement, DataTransferItem, ServiceWorkerRegistration, WebGLTransformFeedback, SVGViewElement, CSSNamespaceRule, URLSearchParams, WebGLBuffer, MediaQueryList, PointerEvent, SVGPolygonElement, KeyframeEffect, RTCDTMFSender, ResizeObserverEntry, SVGCircleElement, SVGAnimationElement, WebGLTexture, DOMRectReadOnly, WebGLSync, IntersectionObserver, MIDIMessageEvent, ReadableByteStreamController, HTMLBaseElement, CountQueuingStrategy, mozRTCIceCandidate, DataTransferItemList, HTMLHeadElement, CDATASection, HTMLDialogElement, HTMLTemplateElement, RTCDataChannel, PerformanceMark, SVGFESpotLightElement, BeforeUnloadEvent, MIDIConnectionEvent, RTCRtpTransceiver, TextTrack, TransitionEvent, HTMLDetailsElement, Comment, HTMLObjectElement, ChannelMergerNode, SVGAnimatedAngle, Response, FileSystemDirectoryEntry, MIDIInput, ScriptProcessorNode, Function, Object, eval, EventTarget, Window, close, stop, focus, blur, open, alert, confirm, prompt, print, postMessage, captureEvents, releaseEvents, getSelection, getComputedStyle, matchMedia, moveTo, moveBy, resizeTo, resizeBy, scroll, scrollTo, scrollBy, getDefaultComputedStyle, scrollByLines, scrollByPages, sizeToContent, updateCommands, find, dump, setResizable, requestIdleCallback, cancelIdleCallback, requestAnimationFrame, cancelAnimationFrame, reportError, btoa, atob, setTimeout, clearTimeout, setInterval, clearInterval, queueMicrotask, createImageBitmap, structuredClone, fetch, self, name, history, customElements, locationbar, menubar, personalbar, scrollbars, statusbar, toolbar, status, closed, event, frames, length, opener, parent, frameElement, navigator, clientInformation, external, applicationCache, screen, innerWidth, innerHeight, scrollX, pageXOffset, scrollY, pageYOffset, screenLeft, screenTop, screenX, screenY, outerWidth, outerHeight, performance, mozInnerScreenX, mozInnerScreenY, devicePixelRatio, scrollMaxX, scrollMaxY, fullScreen, ondevicemotion, ondeviceorientation, ondeviceorientationabsolute, InstallTrigger, visualViewport, crypto, onabort, onblur, onfocus, onauxclick, onbeforeinput, oncanplay, oncanplaythrough, onchange, onclick, onclose, oncontextmenu, oncopy, oncuechange, oncut, ondblclick, ondrag, ondragend, ondragenter, ondragexit, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onformdata, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmouseenter, onmouseleave, onmousemove, onmouseout, onmouseover, onmouseup, onwheel, onpaste, onpause, onplay, onplaying, onprogress, onratechange, onreset, onresize, onscroll, onscrollend, onsecuritypolicyviolation, onseeked, onseeking, onselect, onslotchange, onstalled, onsubmit, onsuspend, ontimeupdate, onvolumechange, onwaiting, onselectstart, onselectionchange, ontoggle, onpointercancel, onpointerdown, onpointerup, onpointermove, onpointerout, onpointerover, onpointerenter, onpointerleave, ongotpointercapture, onlostpointercapture, onmozfullscreenchange, onmozfullscreenerror, onanimationcancel, onanimationend, onanimationiteration, onanimationstart, ontransitioncancel, ontransitionend, ontransitionrun, ontransitionstart, onwebkitanimationend, onwebkitanimationiteration, onwebkitanimationstart, onwebkittransitionend, onerror, speechSynthesis, onafterprint, onbeforeprint, onbeforeunload, onhashchange, onlanguagechange, onmessage, onmessageerror, onoffline, ononline, onpagehide, onpageshow, onpopstate, onrejectionhandled, onstorage, onunhandledrejection, onunload, ongamepadconnected, ongamepaddisconnected, localStorage, origin, crossOriginIsolated, isSecureContext, indexedDB, caches, sessionStorage, window, document, location, top, netscape, Node, Document, HTMLDocument, EventCounts, Map, Promise, Event`,
+ cssKeys: `alignContent, align-content, alignItems, align-items, alignSelf, align-self, aspectRatio, aspect-ratio, backfaceVisibility, backface-visibility, borderCollapse, border-collapse, borderImageRepeat, border-image-repeat, boxDecorationBreak, box-decoration-break, boxSizing, box-sizing, breakInside, break-inside, captionSide, caption-side, clear, colorInterpolation, color-interpolation, colorInterpolationFilters, color-interpolation-filters, columnCount, column-count, columnFill, column-fill, columnSpan, column-span, contain, containerType, container-type, direction, display, dominantBaseline, dominant-baseline, emptyCells, empty-cells, flexDirection, flex-direction, flexWrap, flex-wrap, cssFloat, float, fontKerning, font-kerning, fontLanguageOverride, font-language-override, fontOpticalSizing, font-optical-sizing, fontSizeAdjust, font-size-adjust, fontStretch, font-stretch, fontStyle, font-style, fontVariantCaps, font-variant-caps, fontVariantEastAsian, font-variant-east-asian, fontVariantLigatures, font-variant-ligatures, fontVariantNumeric, font-variant-numeric, fontVariantPosition, font-variant-position, fontWeight, font-weight, gridAutoFlow, grid-auto-flow, hyphens, imageOrientation, image-orientation, imageRendering, image-rendering, imeMode, ime-mode, isolation, justifyContent, justify-content, justifyItems, justify-items, justifySelf, justify-self, lineBreak, line-break, listStylePosition, list-style-position, maskType, mask-type, mixBlendMode, mix-blend-mode, MozBoxAlign, -moz-box-align, MozBoxDirection, -moz-box-direction, MozBoxOrient, -moz-box-orient, MozBoxPack, -moz-box-pack, MozFloatEdge, -moz-float-edge, MozOrient, -moz-orient, MozTextSizeAdjust, -moz-text-size-adjust, MozUserFocus, -moz-user-focus, MozUserInput, -moz-user-input, MozUserModify, -moz-user-modify, MozWindowDragging, -moz-window-dragging, objectFit, object-fit, offsetRotate, offset-rotate, outlineStyle, outline-style, overflowAnchor, overflow-anchor, overflowWrap, overflow-wrap, paintOrder, paint-order, pointerEvents, pointer-events, position, printColorAdjust, print-color-adjust, resize, rubyAlign, ruby-align, rubyPosition, ruby-position, scrollBehavior, scroll-behavior, scrollSnapAlign, scroll-snap-align, scrollSnapStop, scroll-snap-stop, scrollSnapType, scroll-snap-type, scrollbarGutter, scrollbar-gutter, scrollbarWidth, scrollbar-width, shapeRendering, shape-rendering, strokeLinecap, stroke-linecap, strokeLinejoin, stroke-linejoin, tableLayout, table-layout, textAlign, text-align, textAlignLast, text-align-last, textAnchor, text-anchor, textCombineUpright, text-combine-upright, textDecorationLine, text-decoration-line, textDecorationSkipInk, text-decoration-skip-ink, textDecorationStyle, text-decoration-style, textEmphasisPosition, text-emphasis-position, textJustify, text-justify, textOrientation, text-orientation, textRendering, text-rendering, textTransform, text-transform, textUnderlinePosition, text-underline-position, touchAction, touch-action, transformBox, transform-box, transformStyle, transform-style, unicodeBidi, unicode-bidi, userSelect, user-select, vectorEffect, vector-effect, visibility, webkitLineClamp, WebkitLineClamp, -webkit-line-clamp, whiteSpace, white-space, wordBreak, word-break, writingMode, writing-mode, zIndex, z-index, appearance, MozForceBrokenImageIcon, -moz-force-broken-image-icon, breakAfter, break-after, breakBefore, break-before, clipRule, clip-rule, fillRule, fill-rule, fillOpacity, fill-opacity, strokeOpacity, stroke-opacity, fontSynthesisSmallCaps, font-synthesis-small-caps, fontSynthesisStyle, font-synthesis-style, fontSynthesisWeight, font-synthesis-weight, MozBoxOrdinalGroup, -moz-box-ordinal-group, order, flexGrow, flex-grow, flexShrink, flex-shrink, MozBoxFlex, -moz-box-flex, strokeMiterlimit, stroke-miterlimit, overflowBlock, overflow-block, overflowInline, overflow-inline, overflowX, overflow-x, overflowY, overflow-y, overscrollBehaviorBlock, overscroll-behavior-block, overscrollBehaviorInline, overscroll-behavior-inline, overscrollBehaviorX, overscroll-behavior-x, overscrollBehaviorY, overscroll-behavior-y, floodOpacity, flood-opacity, opacity, shapeImageThreshold, shape-image-threshold, stopOpacity, stop-opacity, borderBlockEndStyle, border-block-end-style, borderBlockStartStyle, border-block-start-style, borderBottomStyle, border-bottom-style, borderInlineEndStyle, border-inline-end-style, borderInlineStartStyle, border-inline-start-style, borderLeftStyle, border-left-style, borderRightStyle, border-right-style, borderTopStyle, border-top-style, columnRuleStyle, column-rule-style, accentColor, accent-color, animationDelay, animation-delay, animationDirection, animation-direction, animationDuration, animation-duration, animationFillMode, animation-fill-mode, animationIterationCount, animation-iteration-count, animationName, animation-name, animationPlayState, animation-play-state, animationTimingFunction, animation-timing-function, backdropFilter, backdrop-filter, backgroundAttachment, background-attachment, backgroundBlendMode, background-blend-mode, backgroundClip, background-clip, backgroundImage, background-image, backgroundOrigin, background-origin, backgroundPositionX, background-position-x, backgroundPositionY, background-position-y, backgroundRepeat, background-repeat, backgroundSize, background-size, borderImageOutset, border-image-outset, borderImageSlice, border-image-slice, borderImageWidth, border-image-width, borderSpacing, border-spacing, boxShadow, box-shadow, caretColor, caret-color, clip, clipPath, clip-path, color, colorScheme, color-scheme, columnWidth, column-width, containerName, container-name, content, counterIncrement, counter-increment, counterReset, counter-reset, counterSet, counter-set, cursor, d, filter, flexBasis, flex-basis, fontFamily, font-family, fontFeatureSettings, font-feature-settings, fontPalette, font-palette, fontSize, font-size, fontVariantAlternates, font-variant-alternates, fontVariationSettings, font-variation-settings, gridTemplateAreas, grid-template-areas, hyphenateCharacter, hyphenate-character, letterSpacing, letter-spacing, lineHeight, line-height, listStyleType, list-style-type, maskClip, mask-clip, maskComposite, mask-composite, maskImage, mask-image, maskMode, mask-mode, maskOrigin, mask-origin, maskPositionX, mask-position-x, maskPositionY, mask-position-y, maskRepeat, mask-repeat, maskSize, mask-size, offsetPath, offset-path, page, perspective, quotes, rotate, scale, scrollbarColor, scrollbar-color, shapeOutside, shape-outside, strokeDasharray, stroke-dasharray, strokeDashoffset, stroke-dashoffset, strokeWidth, stroke-width, tabSize, tab-size, textDecorationThickness, text-decoration-thickness, textEmphasisStyle, text-emphasis-style, textOverflow, text-overflow, textShadow, text-shadow, transitionDelay, transition-delay, transitionDuration, transition-duration, transitionProperty, transition-property, transitionTimingFunction, transition-timing-function, translate, verticalAlign, vertical-align, willChange, will-change, wordSpacing, word-spacing, objectPosition, object-position, perspectiveOrigin, perspective-origin, offsetAnchor, offset-anchor, fill, stroke, transformOrigin, transform-origin, gridTemplateColumns, grid-template-columns, gridTemplateRows, grid-template-rows, borderImageSource, border-image-source, listStyleImage, list-style-image, gridAutoColumns, grid-auto-columns, gridAutoRows, grid-auto-rows, transform, columnGap, column-gap, rowGap, row-gap, markerEnd, marker-end, markerMid, marker-mid, markerStart, marker-start, containIntrinsicBlockSize, contain-intrinsic-block-size, containIntrinsicHeight, contain-intrinsic-height, containIntrinsicInlineSize, contain-intrinsic-inline-size, containIntrinsicWidth, contain-intrinsic-width, gridColumnEnd, grid-column-end, gridColumnStart, grid-column-start, gridRowEnd, grid-row-end, gridRowStart, grid-row-start, maxBlockSize, max-block-size, maxHeight, max-height, maxInlineSize, max-inline-size, maxWidth, max-width, cx, cy, offsetDistance, offset-distance, textIndent, text-indent, x, y, borderBottomLeftRadius, border-bottom-left-radius, borderBottomRightRadius, border-bottom-right-radius, borderEndEndRadius, border-end-end-radius, borderEndStartRadius, border-end-start-radius, borderStartEndRadius, border-start-end-radius, borderStartStartRadius, border-start-start-radius, borderTopLeftRadius, border-top-left-radius, borderTopRightRadius, border-top-right-radius, blockSize, block-size, height, inlineSize, inline-size, minBlockSize, min-block-size, minHeight, min-height, minInlineSize, min-inline-size, minWidth, min-width, width, paddingBlockEnd, padding-block-end, paddingBlockStart, padding-block-start, paddingBottom, padding-bottom, paddingInlineEnd, padding-inline-end, paddingInlineStart, padding-inline-start, paddingLeft, padding-left, paddingRight, padding-right, paddingTop, padding-top, r, shapeMargin, shape-margin, rx, ry, scrollPaddingBlockEnd, scroll-padding-block-end, scrollPaddingBlockStart, scroll-padding-block-start, scrollPaddingBottom, scroll-padding-bottom, scrollPaddingInlineEnd, scroll-padding-inline-end, scrollPaddingInlineStart, scroll-padding-inline-start, scrollPaddingLeft, scroll-padding-left, scrollPaddingRight, scroll-padding-right, scrollPaddingTop, scroll-padding-top, borderBlockEndWidth, border-block-end-width, borderBlockStartWidth, border-block-start-width, borderBottomWidth, border-bottom-width, borderInlineEndWidth, border-inline-end-width, borderInlineStartWidth, border-inline-start-width, borderLeftWidth, border-left-width, borderRightWidth, border-right-width, borderTopWidth, border-top-width, columnRuleWidth, column-rule-width, outlineWidth, outline-width, webkitTextStrokeWidth, WebkitTextStrokeWidth, -webkit-text-stroke-width, outlineOffset, outline-offset, overflowClipMargin, overflow-clip-margin, scrollMarginBlockEnd, scroll-margin-block-end, scrollMarginBlockStart, scroll-margin-block-start, scrollMarginBottom, scroll-margin-bottom, scrollMarginInlineEnd, scroll-margin-inline-end, scrollMarginInlineStart, scroll-margin-inline-start, scrollMarginLeft, scroll-margin-left, scrollMarginRight, scroll-margin-right, scrollMarginTop, scroll-margin-top, bottom, insetBlockEnd, inset-block-end, insetBlockStart, inset-block-start, insetInlineEnd, inset-inline-end, insetInlineStart, inset-inline-start, left, marginBlockEnd, margin-block-end, marginBlockStart, margin-block-start, marginBottom, margin-bottom, marginInlineEnd, margin-inline-end, marginInlineStart, margin-inline-start, marginLeft, margin-left, marginRight, margin-right, marginTop, margin-top, right, textUnderlineOffset, text-underline-offset, top, backgroundColor, background-color, borderBlockEndColor, border-block-end-color, borderBlockStartColor, border-block-start-color, borderBottomColor, border-bottom-color, borderInlineEndColor, border-inline-end-color, borderInlineStartColor, border-inline-start-color, borderLeftColor, border-left-color, borderRightColor, border-right-color, borderTopColor, border-top-color, columnRuleColor, column-rule-color, floodColor, flood-color, lightingColor, lighting-color, outlineColor, outline-color, stopColor, stop-color, textDecorationColor, text-decoration-color, textEmphasisColor, text-emphasis-color, webkitTextFillColor, WebkitTextFillColor, -webkit-text-fill-color, webkitTextStrokeColor, WebkitTextStrokeColor, -webkit-text-stroke-color, background, backgroundPosition, background-position, borderColor, border-color, borderStyle, border-style, borderWidth, border-width, borderTop, border-top, borderRight, border-right, borderBottom, border-bottom, borderLeft, border-left, borderBlockStart, border-block-start, borderBlockEnd, border-block-end, borderInlineStart, border-inline-start, borderInlineEnd, border-inline-end, border, borderRadius, border-radius, borderImage, border-image, borderBlockWidth, border-block-width, borderBlockStyle, border-block-style, borderBlockColor, border-block-color, borderInlineWidth, border-inline-width, borderInlineStyle, border-inline-style, borderInlineColor, border-inline-color, borderBlock, border-block, borderInline, border-inline, overflow, overscrollBehavior, overscroll-behavior, container, pageBreakBefore, page-break-before, pageBreakAfter, page-break-after, pageBreakInside, page-break-inside, offset, columns, columnRule, column-rule, font, fontVariant, font-variant, fontSynthesis, font-synthesis, marker, textEmphasis, text-emphasis, webkitTextStroke, WebkitTextStroke, -webkit-text-stroke, listStyle, list-style, margin, marginBlock, margin-block, marginInline, margin-inline, scrollMargin, scroll-margin, scrollMarginBlock, scroll-margin-block, scrollMarginInline, scroll-margin-inline, outline, padding, paddingBlock, padding-block, paddingInline, padding-inline, scrollPadding, scroll-padding, scrollPaddingBlock, scroll-padding-block, scrollPaddingInline, scroll-padding-inline, flexFlow, flex-flow, flex, gap, gridRow, grid-row, gridColumn, grid-column, gridArea, grid-area, gridTemplate, grid-template, grid, placeContent, place-content, placeSelf, place-self, placeItems, place-items, inset, insetBlock, inset-block, insetInline, inset-inline, containIntrinsicSize, contain-intrinsic-size, mask, maskPosition, mask-position, textDecoration, text-decoration, transition, animation, all, webkitBackgroundClip, WebkitBackgroundClip, -webkit-background-clip, webkitBackgroundOrigin, WebkitBackgroundOrigin, -webkit-background-origin, webkitBackgroundSize, WebkitBackgroundSize, -webkit-background-size, MozBorderStartColor, -moz-border-start-color, MozBorderStartStyle, -moz-border-start-style, MozBorderStartWidth, -moz-border-start-width, MozBorderEndColor, -moz-border-end-color, MozBorderEndStyle, -moz-border-end-style, MozBorderEndWidth, -moz-border-end-width, webkitBorderTopLeftRadius, WebkitBorderTopLeftRadius, -webkit-border-top-left-radius, webkitBorderTopRightRadius, WebkitBorderTopRightRadius, -webkit-border-top-right-radius, webkitBorderBottomRightRadius, WebkitBorderBottomRightRadius, -webkit-border-bottom-right-radius, webkitBorderBottomLeftRadius, WebkitBorderBottomLeftRadius, -webkit-border-bottom-left-radius, MozTransform, -moz-transform, webkitTransform, WebkitTransform, -webkit-transform, MozPerspective, -moz-perspective, webkitPerspective, WebkitPerspective, -webkit-perspective, MozPerspectiveOrigin, -moz-perspective-origin, webkitPerspectiveOrigin, WebkitPerspectiveOrigin, -webkit-perspective-origin, MozBackfaceVisibility, -moz-backface-visibility, webkitBackfaceVisibility, WebkitBackfaceVisibility, -webkit-backface-visibility, MozTransformStyle, -moz-transform-style, webkitTransformStyle, WebkitTransformStyle, -webkit-transform-style, MozTransformOrigin, -moz-transform-origin, webkitTransformOrigin, WebkitTransformOrigin, -webkit-transform-origin, MozAppearance, -moz-appearance, webkitAppearance, WebkitAppearance, -webkit-appearance, webkitBoxShadow, WebkitBoxShadow, -webkit-box-shadow, webkitFilter, WebkitFilter, -webkit-filter, MozFontFeatureSettings, -moz-font-feature-settings, MozFontLanguageOverride, -moz-font-language-override, colorAdjust, color-adjust, MozHyphens, -moz-hyphens, webkitTextSizeAdjust, WebkitTextSizeAdjust, -webkit-text-size-adjust, wordWrap, word-wrap, MozTabSize, -moz-tab-size, MozMarginStart, -moz-margin-start, MozMarginEnd, -moz-margin-end, MozPaddingStart, -moz-padding-start, MozPaddingEnd, -moz-padding-end, webkitFlexDirection, WebkitFlexDirection, -webkit-flex-direction, webkitFlexWrap, WebkitFlexWrap, -webkit-flex-wrap, webkitJustifyContent, WebkitJustifyContent, -webkit-justify-content, webkitAlignContent, WebkitAlignContent, -webkit-align-content, webkitAlignItems, WebkitAlignItems, -webkit-align-items, webkitFlexGrow, WebkitFlexGrow, -webkit-flex-grow, webkitFlexShrink, WebkitFlexShrink, -webkit-flex-shrink, webkitAlignSelf, WebkitAlignSelf, -webkit-align-self, webkitOrder, WebkitOrder, -webkit-order, webkitFlexBasis, WebkitFlexBasis, -webkit-flex-basis, MozBoxSizing, -moz-box-sizing, webkitBoxSizing, WebkitBoxSizing, -webkit-box-sizing, gridColumnGap, grid-column-gap, gridRowGap, grid-row-gap, webkitClipPath, WebkitClipPath, -webkit-clip-path, webkitMaskRepeat, WebkitMaskRepeat, -webkit-mask-repeat, webkitMaskPositionX, WebkitMaskPositionX, -webkit-mask-position-x, webkitMaskPositionY, WebkitMaskPositionY, -webkit-mask-position-y, webkitMaskClip, WebkitMaskClip, -webkit-mask-clip, webkitMaskOrigin, WebkitMaskOrigin, -webkit-mask-origin, webkitMaskSize, WebkitMaskSize, -webkit-mask-size, webkitMaskComposite, WebkitMaskComposite, -webkit-mask-composite, webkitMaskImage, WebkitMaskImage, -webkit-mask-image, MozUserSelect, -moz-user-select, webkitUserSelect, WebkitUserSelect, -webkit-user-select, MozTransitionDuration, -moz-transition-duration, webkitTransitionDuration, WebkitTransitionDuration, -webkit-transition-duration, MozTransitionTimingFunction, -moz-transition-timing-function, webkitTransitionTimingFunction, WebkitTransitionTimingFunction, -webkit-transition-timing-function, MozTransitionProperty, -moz-transition-property, webkitTransitionProperty, WebkitTransitionProperty, -webkit-transition-property, MozTransitionDelay, -moz-transition-delay, webkitTransitionDelay, WebkitTransitionDelay, -webkit-transition-delay, MozAnimationName, -moz-animation-name, webkitAnimationName, WebkitAnimationName, -webkit-animation-name, MozAnimationDuration, -moz-animation-duration, webkitAnimationDuration, WebkitAnimationDuration, -webkit-animation-duration, MozAnimationTimingFunction, -moz-animation-timing-function, webkitAnimationTimingFunction, WebkitAnimationTimingFunction, -webkit-animation-timing-function, MozAnimationIterationCount, -moz-animation-iteration-count, webkitAnimationIterationCount, WebkitAnimationIterationCount, -webkit-animation-iteration-count, MozAnimationDirection, -moz-animation-direction, webkitAnimationDirection, WebkitAnimationDirection, -webkit-animation-direction, MozAnimationPlayState, -moz-animation-play-state, webkitAnimationPlayState, WebkitAnimationPlayState, -webkit-animation-play-state, MozAnimationFillMode, -moz-animation-fill-mode, webkitAnimationFillMode, WebkitAnimationFillMode, -webkit-animation-fill-mode, MozAnimationDelay, -moz-animation-delay, webkitAnimationDelay, WebkitAnimationDelay, -webkit-animation-delay, webkitBoxAlign, WebkitBoxAlign, -webkit-box-align, webkitBoxDirection, WebkitBoxDirection, -webkit-box-direction, webkitBoxFlex, WebkitBoxFlex, -webkit-box-flex, webkitBoxOrient, WebkitBoxOrient, -webkit-box-orient, webkitBoxPack, WebkitBoxPack, -webkit-box-pack, webkitBoxOrdinalGroup, WebkitBoxOrdinalGroup, -webkit-box-ordinal-group, MozBorderStart, -moz-border-start, MozBorderEnd, -moz-border-end, webkitBorderRadius, WebkitBorderRadius, -webkit-border-radius, MozBorderImage, -moz-border-image, webkitBorderImage, WebkitBorderImage, -webkit-border-image, webkitFlexFlow, WebkitFlexFlow, -webkit-flex-flow, webkitFlex, WebkitFlex, -webkit-flex, gridGap, grid-gap, webkitMask, WebkitMask, -webkit-mask, webkitMaskPosition, WebkitMaskPosition, -webkit-mask-position, MozTransition, -moz-transition, webkitTransition, WebkitTransition, -webkit-transition, MozAnimation, -moz-animation, webkitAnimation, WebkitAnimation, -webkit-animation, constructor`,
+ jsKeys: 'Object.assign, Object.getPrototypeOf, Object.setPrototypeOf, Object.getOwnPropertyDescriptor, Object.getOwnPropertyDescriptors, Object.keys, Object.values, Object.entries, Object.is, Object.defineProperty, Object.defineProperties, Object.create, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.isExtensible, Object.preventExtensions, Object.freeze, Object.isFrozen, Object.seal, Object.isSealed, Object.fromEntries, Object.hasOwn, Object.toString, Object.toLocaleString, Object.valueOf, Object.hasOwnProperty, Object.isPrototypeOf, Object.propertyIsEnumerable, Object.__defineGetter__, Object.__defineSetter__, Object.__lookupGetter__, Object.__lookupSetter__, Object.__proto__, Function.toString, Function.apply, Function.call, Function.bind, Boolean.toString, Boolean.valueOf, Symbol.for, Symbol.keyFor, Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.replace, Symbol.search, Symbol.species, Symbol.hasInstance, Symbol.split, Symbol.toPrimitive, Symbol.toStringTag, Symbol.unscopables, Symbol.asyncIterator, Symbol.matchAll, Symbol.toString, Symbol.valueOf, Symbol.description, Error.toString, Error.message, Error.stack, Number.isFinite, Number.isInteger, Number.isNaN, Number.isSafeInteger, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.EPSILON, Number.parseInt, Number.parseFloat, Number.NaN, Number.toString, Number.toLocaleString, Number.valueOf, Number.toFixed, Number.toExponential, Number.toPrecision, BigInt.asUintN, BigInt.asIntN, BigInt.valueOf, BigInt.toString, BigInt.toLocaleString, Math.abs, Math.acos, Math.asin, Math.atan, Math.atan2, Math.ceil, Math.clz32, Math.cos, Math.exp, Math.floor, Math.imul, Math.fround, Math.log, Math.max, Math.min, Math.pow, Math.random, Math.round, Math.sin, Math.sqrt, Math.tan, Math.log10, Math.log2, Math.log1p, Math.expm1, Math.cosh, Math.sinh, Math.tanh, Math.acosh, Math.asinh, Math.atanh, Math.hypot, Math.trunc, Math.sign, Math.cbrt, Math.E, Math.LOG2E, Math.LOG10E, Math.LN2, Math.LN10, Math.PI, Math.SQRT2, Math.SQRT1_2, Date.UTC, Date.parse, Date.now, Date.getTime, Date.getTimezoneOffset, Date.getYear, Date.getFullYear, Date.getUTCFullYear, Date.getMonth, Date.getUTCMonth, Date.getDate, Date.getUTCDate, Date.getDay, Date.getUTCDay, Date.getHours, Date.getUTCHours, Date.getMinutes, Date.getUTCMinutes, Date.getSeconds, Date.getUTCSeconds, Date.getMilliseconds, Date.getUTCMilliseconds, Date.setTime, Date.setYear, Date.setFullYear, Date.setUTCFullYear, Date.setMonth, Date.setUTCMonth, Date.setDate, Date.setUTCDate, Date.setHours, Date.setUTCHours, Date.setMinutes, Date.setUTCMinutes, Date.setSeconds, Date.setUTCSeconds, Date.setMilliseconds, Date.setUTCMilliseconds, Date.toUTCString, Date.toLocaleString, Date.toLocaleDateString, Date.toLocaleTimeString, Date.toDateString, Date.toTimeString, Date.toISOString, Date.toJSON, Date.toString, Date.valueOf, Date.toGMTString, String.fromCharCode, String.fromCodePoint, String.raw, String.toString, String.valueOf, String.toLowerCase, String.toUpperCase, String.charAt, String.charCodeAt, String.substring, String.padStart, String.padEnd, String.codePointAt, String.includes, String.indexOf, String.lastIndexOf, String.startsWith, String.endsWith, String.trim, String.trimStart, String.trimEnd, String.toLocaleLowerCase, String.toLocaleUpperCase, String.localeCompare, String.repeat, String.normalize, String.match, String.matchAll, String.search, String.replace, String.replaceAll, String.split, String.substr, String.concat, String.slice, String.at, String.bold, String.italics, String.fixed, String.strike, String.small, String.big, String.blink, String.sup, String.sub, String.anchor, String.link, String.fontcolor, String.fontsize, String.trimLeft, String.trimRight, RegExp.input, RegExp.lastMatch, RegExp.lastParen, RegExp.leftContext, RegExp.rightContext, RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$6, RegExp.$7, RegExp.$8, RegExp.$9, RegExp.$_, RegExp.$&, RegExp.$+, RegExp.$`, RegExp.$\', RegExp.toString, RegExp.compile, RegExp.exec, RegExp.test, RegExp.flags, RegExp.hasIndices, RegExp.global, RegExp.ignoreCase, RegExp.multiline, RegExp.dotAll, RegExp.source, RegExp.sticky, RegExp.unicode, Array.isArray, Array.from, Array.of, Array.toString, Array.toLocaleString, Array.join, Array.reverse, Array.sort, Array.push, Array.pop, Array.shift, Array.unshift, Array.splice, Array.concat, Array.slice, Array.lastIndexOf, Array.indexOf, Array.forEach, Array.map, Array.filter, Array.reduce, Array.reduceRight, Array.some, Array.every, Array.find, Array.findIndex, Array.copyWithin, Array.fill, Array.entries, Array.keys, Array.values, Array.includes, Array.flatMap, Array.flat, Array.at, Array.findLast, Array.findLastIndex, Map.get, Map.has, Map.set, Map.delete, Map.keys, Map.values, Map.clear, Map.forEach, Map.entries, Map.size, Set.has, Set.add, Set.delete, Set.entries, Set.clear, Set.forEach, Set.values, Set.keys, Set.size, WeakMap.has, WeakMap.get, WeakMap.delete, WeakMap.set, WeakSet.add, WeakSet.delete, WeakSet.has, Atomics.compareExchange, Atomics.load, Atomics.store, Atomics.exchange, Atomics.add, Atomics.sub, Atomics.and, Atomics.or, Atomics.xor, Atomics.isLockFree, Atomics.wait, Atomics.notify, Atomics.wake, JSON.parse, JSON.stringify, Promise.all, Promise.allSettled, Promise.any, Promise.race, Promise.reject, Promise.resolve, Promise.then, Promise.catch, Promise.finally, Reflect.apply, Reflect.construct, Reflect.defineProperty, Reflect.deleteProperty, Reflect.get, Reflect.getOwnPropertyDescriptor, Reflect.getPrototypeOf, Reflect.has, Reflect.isExtensible, Reflect.ownKeys, Reflect.preventExtensions, Reflect.set, Reflect.setPrototypeOf, Proxy.revocable, Intl.getCanonicalLocales, Intl.supportedValuesOf, Intl.Collator, Intl.DateTimeFormat, Intl.DisplayNames, Intl.ListFormat, Intl.Locale, Intl.NumberFormat, Intl.PluralRules, Intl.RelativeTimeFormat, WebAssembly.compile, WebAssembly.instantiate, WebAssembly.validate, WebAssembly.compileStreaming, WebAssembly.instantiateStreaming, WebAssembly.Module, WebAssembly.Instance, WebAssembly.Memory, WebAssembly.Table, WebAssembly.Global, WebAssembly.CompileError, WebAssembly.LinkError, WebAssembly.RuntimeError, WebAssembly.Tag, WebAssembly.Exception, Document.getElementsByTagName, Document.getElementsByTagNameNS, Document.getElementsByClassName, Document.getElementById, Document.createElement, Document.createElementNS, Document.createDocumentFragment, Document.createTextNode, Document.createComment, Document.createProcessingInstruction, Document.importNode, Document.adoptNode, Document.createEvent, Document.createRange, Document.createNodeIterator, Document.createTreeWalker, Document.createCDATASection, Document.createAttribute, Document.createAttributeNS, Document.getElementsByName, Document.open, Document.close, Document.write, Document.writeln, Document.hasFocus, Document.execCommand, Document.queryCommandEnabled, Document.queryCommandIndeterm, Document.queryCommandState, Document.queryCommandSupported, Document.queryCommandValue, Document.releaseCapture, Document.mozSetImageElement, Document.clear, Document.captureEvents, Document.releaseEvents, Document.exitFullscreen, Document.mozCancelFullScreen, Document.exitPointerLock, Document.enableStyleSheetsForSet, Document.caretPositionFromPoint, Document.querySelector, Document.querySelectorAll, Document.getSelection, Document.hasStorageAccess, Document.requestStorageAccess, Document.elementFromPoint, Document.elementsFromPoint, Document.getAnimations, Document.prepend, Document.append, Document.replaceChildren, Document.createExpression, Document.createNSResolver, Document.evaluate, Document.implementation, Document.URL, Document.documentURI, Document.compatMode, Document.characterSet, Document.charset, Document.inputEncoding, Document.contentType, Document.doctype, Document.documentElement, Document.domain, Document.referrer, Document.cookie, Document.lastModified, Document.readyState, Document.title, Document.dir, Document.body, Document.head, Document.images, Document.embeds, Document.plugins, Document.links, Document.forms, Document.scripts, Document.defaultView, Document.designMode, Document.onreadystatechange, Document.onbeforescriptexecute, Document.onafterscriptexecute, Document.currentScript, Document.fgColor, Document.linkColor, Document.vlinkColor, Document.alinkColor, Document.bgColor, Document.anchors, Document.applets, Document.all, Document.fullscreen, Document.mozFullScreen, Document.fullscreenEnabled, Document.mozFullScreenEnabled, Document.onfullscreenchange, Document.onfullscreenerror, Document.onpointerlockchange, Document.onpointerlockerror, Document.hidden, Document.visibilityState, Document.onvisibilitychange, Document.selectedStyleSheetSet, Document.lastStyleSheetSet, Document.preferredStyleSheetSet, Document.styleSheetSets, Document.scrollingElement, Document.timeline, Document.rootElement, Document.activeElement, Document.styleSheets, Document.pointerLockElement, Document.fullscreenElement, Document.mozFullScreenElement, Document.adoptedStyleSheets, Document.fonts, Document.onabort, Document.onblur, Document.onfocus, Document.onauxclick, Document.onbeforeinput, Document.oncanplay, Document.oncanplaythrough, Document.onchange, Document.onclick, Document.onclose, Document.oncontextmenu, Document.oncopy, Document.oncuechange, Document.oncut, Document.ondblclick, Document.ondrag, Document.ondragend, Document.ondragenter, Document.ondragexit, Document.ondragleave, Document.ondragover, Document.ondragstart, Document.ondrop, Document.ondurationchange, Document.onemptied, Document.onended, Document.onformdata, Document.oninput, Document.oninvalid, Document.onkeydown, Document.onkeypress, Document.onkeyup, Document.onload, Document.onloadeddata, Document.onloadedmetadata, Document.onloadstart, Document.onmousedown, Document.onmouseenter, Document.onmouseleave, Document.onmousemove, Document.onmouseout, Document.onmouseover, Document.onmouseup, Document.onwheel, Document.onpaste, Document.onpause, Document.onplay, Document.onplaying, Document.onprogress, Document.onratechange, Document.onreset, Document.onresize, Document.onscroll, Document.onscrollend, Document.onsecuritypolicyviolation, Document.onseeked, Document.onseeking, Document.onselect, Document.onslotchange, Document.onstalled, Document.onsubmit, Document.onsuspend, Document.ontimeupdate, Document.onvolumechange, Document.onwaiting, Document.onselectstart, Document.onselectionchange, Document.ontoggle, Document.onpointercancel, Document.onpointerdown, Document.onpointerup, Document.onpointermove, Document.onpointerout, Document.onpointerover, Document.onpointerenter, Document.onpointerleave, Document.ongotpointercapture, Document.onlostpointercapture, Document.onmozfullscreenchange, Document.onmozfullscreenerror, Document.onanimationcancel, Document.onanimationend, Document.onanimationiteration, Document.onanimationstart, Document.ontransitioncancel, Document.ontransitionend, Document.ontransitionrun, Document.ontransitionstart, Document.onwebkitanimationend, Document.onwebkitanimationiteration, Document.onwebkitanimationstart, Document.onwebkittransitionend, Document.onerror, Document.children, Document.firstElementChild, Document.lastElementChild, Document.childElementCount, Element.getAttributeNames, Element.getAttribute, Element.getAttributeNS, Element.toggleAttribute, Element.setAttribute, Element.setAttributeNS, Element.removeAttribute, Element.removeAttributeNS, Element.hasAttribute, Element.hasAttributeNS, Element.hasAttributes, Element.closest, Element.matches, Element.webkitMatchesSelector, Element.getElementsByTagName, Element.getElementsByTagNameNS, Element.getElementsByClassName, Element.insertAdjacentElement, Element.insertAdjacentText, Element.mozMatchesSelector, Element.setPointerCapture, Element.releasePointerCapture, Element.hasPointerCapture, Element.setCapture, Element.releaseCapture, Element.getAttributeNode, Element.setAttributeNode, Element.removeAttributeNode, Element.getAttributeNodeNS, Element.setAttributeNodeNS, Element.getClientRects, Element.getBoundingClientRect, Element.checkVisibility, Element.scrollIntoView, Element.scroll, Element.scrollTo, Element.scrollBy, Element.insertAdjacentHTML, Element.querySelector, Element.querySelectorAll, Element.attachShadow, Element.requestFullscreen, Element.mozRequestFullScreen, Element.requestPointerLock, Element.animate, Element.getAnimations, Element.before, Element.after, Element.replaceWith, Element.remove, Element.prepend, Element.append, Element.replaceChildren, Element.namespaceURI, Element.prefix, Element.localName, Element.tagName, Element.id, Element.className, Element.classList, Element.part, Element.attributes, Element.scrollTop, Element.scrollLeft, Element.scrollWidth, Element.scrollHeight, Element.clientTop, Element.clientLeft, Element.clientWidth, Element.clientHeight, Element.scrollTopMax, Element.scrollLeftMax, Element.innerHTML, Element.outerHTML, Element.shadowRoot, Element.assignedSlot, Element.slot, Element.onfullscreenchange, Element.onfullscreenerror, Element.previousElementSibling, Element.nextElementSibling, Element.children, Element.firstElementChild, Element.lastElementChild, Element.childElementCount',
+ },
+ });
+ // @ts-ignore
+ const getListDiff = ({ oldList, newList, removeCamelCase = false } = {}) => {
+ const oldSet = new Set(oldList);
+ const newSet = new Set(newList);
+ newList.forEach((x) => oldSet.delete(x));
+ oldList.forEach((x) => newSet.delete(x));
+ const camelCase = /[a-z][A-Z]/;
+ return {
+ removed: !removeCamelCase ? [...oldSet] : [...oldSet].filter((key) => !camelCase.test(key)),
+ added: !removeCamelCase ? [...newSet] : [...newSet].filter((key) => !camelCase.test(key)),
+ };
+ };
+ const BROWSER = (IS_BLINK ? 'Chrome' : IS_GECKO ? 'Firefox' : '');
+ const getEngineMaps = (browser) => {
+ // Blink
+ const blinkJS = {
+ '76': ['Document.onsecuritypolicyviolation', 'Promise.allSettled'],
+ '77': ['Document.onformdata', 'Document.onpointerrawupdate'],
+ '78': ['Element.elementTiming'],
+ '79': ['Document.onanimationend', 'Document.onanimationiteration', 'Document.onanimationstart', 'Document.ontransitionend'],
+ '80': ['!Document.registerElement', '!Element.createShadowRoot', '!Element.getDestinationInsertionPoints'],
+ '81': ['Document.onwebkitanimationend', 'Document.onwebkitanimationiteration', 'Document.onwebkitanimationstart', 'Document.onwebkittransitionend', 'Element.ariaAtomic', 'Element.ariaAutoComplete', 'Element.ariaBusy', 'Element.ariaChecked', 'Element.ariaColCount', 'Element.ariaColIndex', 'Element.ariaColSpan', 'Element.ariaCurrent', 'Element.ariaDisabled', 'Element.ariaExpanded', 'Element.ariaHasPopup', 'Element.ariaHidden', 'Element.ariaKeyShortcuts', 'Element.ariaLabel', 'Element.ariaLevel', 'Element.ariaLive', 'Element.ariaModal', 'Element.ariaMultiLine', 'Element.ariaMultiSelectable', 'Element.ariaOrientation', 'Element.ariaPlaceholder', 'Element.ariaPosInSet', 'Element.ariaPressed', 'Element.ariaReadOnly', 'Element.ariaRelevant', 'Element.ariaRequired', 'Element.ariaRoleDescription', 'Element.ariaRowCount', 'Element.ariaRowIndex', 'Element.ariaRowSpan', 'Element.ariaSelected', 'Element.ariaSort', 'Element.ariaValueMax', 'Element.ariaValueMin', 'Element.ariaValueNow', 'Element.ariaValueText', 'Intl.DisplayNames'],
+ '83': ['Element.ariaDescription', 'Element.onbeforexrselect'],
+ '84': ['Document.getAnimations', 'Document.timeline', 'Element.ariaSetSize', 'Element.getAnimations'],
+ '85': ['Promise.any', 'String.replaceAll'],
+ '86': ['Document.fragmentDirective', 'Document.replaceChildren', 'Element.replaceChildren', '!Atomics.wake'],
+ '87-89': ['Atomics.waitAsync', 'Document.ontransitioncancel', 'Document.ontransitionrun', 'Document.ontransitionstart', 'Intl.Segmenter'],
+ '90': ['Document.onbeforexrselect', 'RegExp.hasIndices', '!Element.onbeforexrselect'],
+ '91': ['Element.getInnerHTML'],
+ '92': ['Array.at', 'String.at'],
+ '93': ['Error.cause', 'Object.hasOwn'],
+ '94': ['!Error.cause', 'Object.hasOwn'],
+ '95-96': ['WebAssembly.Exception', 'WebAssembly.Tag'],
+ '97-98': ['Array.findLast', 'Array.findLastIndex', 'Document.onslotchange'],
+ '99-101': ['Intl.supportedValuesOf', 'Document.oncontextlost', 'Document.oncontextrestored'],
+ '102': ['Element.ariaInvalid', 'Document.onbeforematch'],
+ '103-106': ['Element.role'],
+ '107-109': ['Element.ariaBrailleLabel', 'Element.ariaBrailleRoleDescription'],
+ '110': ['Array.toReversed', 'Array.toSorted', 'Array.toSpliced', 'Array.with'],
+ '111': ['String.isWellFormed', 'String.toWellFormed', 'Document.startViewTransition'],
+ '112-113': ['RegExp.unicodeSets'],
+ '114-115': ['JSON.rawJSON', 'JSON.isRawJSON'],
+ };
+ const blinkCSS = {
+ '76': ['backdrop-filter'],
+ '77-80': ['overscroll-behavior-block', 'overscroll-behavior-inline'],
+ '81': ['color-scheme', 'image-orientation'],
+ '83': ['contain-intrinsic-size'],
+ '84': ['appearance', 'ruby-position'],
+ '85-86': ['content-visibility', 'counter-set', 'inherits', 'initial-value', 'page-orientation', 'syntax'],
+ '87': ['ascent-override', 'border-block', 'border-block-color', 'border-block-style', 'border-block-width', 'border-inline', 'border-inline-color', 'border-inline-style', 'border-inline-width', 'descent-override', 'inset', 'inset-block', 'inset-block-end', 'inset-block-start', 'inset-inline', 'inset-inline-end', 'inset-inline-start', 'line-gap-override', 'margin-block', 'margin-inline', 'padding-block', 'padding-inline', 'text-decoration-thickness', 'text-underline-offset'],
+ '88': ['aspect-ratio'],
+ '89': ['border-end-end-radius', 'border-end-start-radius', 'border-start-end-radius', 'border-start-start-radius', 'forced-color-adjust'],
+ '90': ['overflow-clip-margin'],
+ '91': ['additive-symbols', 'fallback', 'negative', 'pad', 'prefix', 'range', 'speak-as', 'suffix', 'symbols', 'system'],
+ '92': ['size-adjust'],
+ '93': ['accent-color'],
+ '94': ['scrollbar-gutter'],
+ '95-96': ['app-region', 'contain-intrinsic-block-size', 'contain-intrinsic-height', 'contain-intrinsic-inline-size', 'contain-intrinsic-width'],
+ '97-98': ['font-synthesis-small-caps', 'font-synthesis-style', 'font-synthesis-weight', 'font-synthesis'],
+ '99-100': ['text-emphasis-color', 'text-emphasis-position', 'text-emphasis-style', 'text-emphasis'],
+ '101-103': ['font-palette', 'base-palette', 'override-colors'],
+ '104': ['object-view-box'],
+ '105': ['container-name', 'container-type', 'container'],
+ '106-107': ['hyphenate-character'],
+ '108': ['hyphenate-character', '!orientation', '!max-zoom', '!min-zoom', '!user-zoom'],
+ '109': ['hyphenate-limit-chars', 'math-depth', 'math-shift', 'math-style'],
+ '110': ['initial-letter'],
+ '111-113': ['baseline-source', 'font-variant-alternates', 'view-transition-name'],
+ '114-115': ['text-wrap', 'white-space-collapse'],
+ };
+ const blinkWindow = {
+ '80': ['CompressionStream', 'DecompressionStream', 'FeaturePolicy', 'FragmentDirective', 'PeriodicSyncManager', 'VideoPlaybackQuality'],
+ '81': ['SubmitEvent', 'XRHitTestResult', 'XRHitTestSource', 'XRRay', 'XRTransientInputHitTestResult', 'XRTransientInputHitTestSource'],
+ '83': ['BarcodeDetector', 'XRDOMOverlayState', 'XRSystem'],
+ '84': ['AnimationPlaybackEvent', 'AnimationTimeline', 'CSSAnimation', 'CSSTransition', 'DocumentTimeline', 'FinalizationRegistry', 'LayoutShiftAttribution', 'ResizeObserverSize', 'WakeLock', 'WakeLockSentinel', 'WeakRef', 'XRLayer'],
+ '85': ['AggregateError', 'CSSPropertyRule', 'EventCounts', 'XRAnchor', 'XRAnchorSet'],
+ '86': ['RTCEncodedAudioFrame', 'RTCEncodedVideoFrame'],
+ '87': ['CookieChangeEvent', 'CookieStore', 'CookieStoreManager', 'Scheduling'],
+ '88': ['Scheduling', '!BarcodeDetector'],
+ '89': ['ReadableByteStreamController', 'ReadableStreamBYOBReader', 'ReadableStreamBYOBRequest', 'ReadableStreamDefaultController', 'XRWebGLBinding'],
+ '90': ['AbstractRange', 'CustomStateSet', 'NavigatorUAData', 'XRCPUDepthInformation', 'XRDepthInformation', 'XRLightEstimate', 'XRLightProbe', 'XRWebGLDepthInformation'],
+ '91': ['CSSCounterStyleRule', 'GravitySensor', 'NavigatorManagedData'],
+ '92': ['CSSCounterStyleRule', '!SharedArrayBuffer'],
+ '93': ['WritableStreamDefaultController'],
+ '94': ['AudioData', 'AudioDecoder', 'AudioEncoder', 'EncodedAudioChunk', 'EncodedVideoChunk', 'IdleDetector', 'ImageDecoder', 'ImageTrack', 'ImageTrackList', 'VideoColorSpace', 'VideoDecoder', 'VideoEncoder', 'VideoFrame', 'MediaStreamTrackGenerator', 'MediaStreamTrackProcessor', 'Profiler', 'VirtualKeyboard', 'DelegatedInkTrailPresenter', 'Ink', 'Scheduler', 'TaskController', 'TaskPriorityChangeEvent', 'TaskSignal', 'VirtualKeyboardGeometryChangeEvent'],
+ '95-96': ['URLPattern'],
+ '97-98': ['WebTransport', 'WebTransportBidirectionalStream', 'WebTransportDatagramDuplexStream', 'WebTransportError'],
+ '99': ['CanvasFilter', 'CSSLayerBlockRule', 'CSSLayerStatementRule'],
+ '100': ['CSSMathClamp'],
+ '101-104': ['CSSFontPaletteValuesRule'],
+ '105-106': ['CSSContainerRule'],
+ '107-108': ['XRCamera'],
+ '109': ['MathMLElement'],
+ '110': ['AudioSinkInfo'],
+ '111-112': ['ViewTransition'],
+ '113-115': ['ViewTransition', '!CanvasFilter'],
+ };
+ // Gecko
+ const geckoJS = {
+ '71': ['Promise.allSettled'],
+ '72-73': ['Document.onformdata', 'Element.part'],
+ '74': ['!Array.toSource', '!Boolean.toSource', '!Date.toSource', '!Error.toSource', '!Function.toSource', '!Intl.toSource', '!JSON.toSource', '!Math.toSource', '!Number.toSource', '!Object.toSource', '!RegExp.toSource', '!String.toSource', '!WebAssembly.toSource'],
+ '75-76': ['Document.getAnimations', 'Document.timeline', 'Element.getAnimations', 'Intl.Locale'],
+ '77': ['String.replaceAll'],
+ '78': ['Atomics.add', 'Atomics.and', 'Atomics.compareExchange', 'Atomics.exchange', 'Atomics.isLockFree', 'Atomics.load', 'Atomics.notify', 'Atomics.or', 'Atomics.store', 'Atomics.sub', 'Atomics.wait', 'Atomics.wake', 'Atomics.xor', 'Document.replaceChildren', 'Element.replaceChildren', 'Intl.ListFormat', 'RegExp.dotAll'],
+ '79-84': ['Promise.any'],
+ '85': ['!Document.onshow', 'Promise.any'],
+ '86': ['Intl.DisplayNames'],
+ '87': ['Document.onbeforeinput'],
+ '88-89': ['RegExp.hasIndices'],
+ '90-91': ['Array.at', 'String.at'],
+ '92': ['Object.hasOwn'],
+ '93-99': ['Intl.supportedValuesOf', 'Document.onsecuritypolicyviolation', 'Document.onslotchange'],
+ '100': ['WebAssembly.Tag', 'WebAssembly.Exception'],
+ '101-103': ['Document.adoptedStyleSheets'],
+ '104-108': ['Array.findLast', 'Array.findLastIndex'],
+ '109-112': ['Document.onscrollend'],
+ };
+ // {"added":[],"removed":[]} {"added":["Document.onscrollend"],"removed":[]} {"added":["onscrollend"],"removed":[]}
+ const geckoCSS = {
+ '71': ['-moz-column-span'],
+ '72': ['offset', 'offset-anchor', 'offset-distance', 'offset-path', 'offset-rotate', 'rotate', 'scale', 'translate'],
+ '73': ['overscroll-behavior-block', 'overscroll-behavior-inline'],
+ '74-79': ['!-moz-stack-sizing', 'text-underline-position'],
+ '80-88': ['appearance'],
+ '89-90': ['!-moz-outline-radius', '!-moz-outline-radius-bottomleft', '!-moz-outline-radius-bottomright', '!-moz-outline-radius-topleft', '!-moz-outline-radius-topright', 'aspect-ratio'],
+ '91': ['tab-size'],
+ '92-95': ['accent-color'],
+ '96': ['color-scheme'],
+ '97': ['print-color-adjust', 'scrollbar-gutter', 'd'],
+ '98-101': ['hyphenate-character'],
+ '102': ['overflow-clip-margin'],
+ '103-106': ['scroll-snap-stop'],
+ '107-108': ['backdrop-filter', 'font-palette', 'contain-intrinsic-block-size', 'contain-intrinsic-height', 'contain-intrinsic-inline-size', 'contain-intrinsic-width', 'contain-intrinsic-size'],
+ '109': ['-webkit-clip-path'],
+ '110': ['container-type', 'container-name', 'page', 'container'],
+ '111': ['font-synthesis-small-caps', 'font-synthesis-style', 'font-synthesis-weight'],
+ '112': ['font-synthesis-small-caps', '!-moz-image-region'],
+ };
+ const geckoWindow = {
+ // disregard: 'reportError','onsecuritypolicyviolation','onslotchange'
+ '71': ['MathMLElement', '!SVGZoomAndPan'],
+ '72-73': ['!BatteryManager', 'FormDataEvent', 'Geolocation', 'GeolocationCoordinates', 'GeolocationPosition', 'GeolocationPositionError', '!mozPaintCount'],
+ '74': ['FormDataEvent', '!uneval'],
+ '75': ['AnimationTimeline', 'CSSAnimation', 'CSSTransition', 'DocumentTimeline', 'SubmitEvent'],
+ '76-77': ['AudioParamMap', 'AudioWorklet', 'AudioWorkletNode', 'Worklet'],
+ '78': ['Atomics'],
+ '79-81': ['AggregateError', 'FinalizationRegistry'],
+ '82': ['MediaMetadata', 'MediaSession', 'Sanitizer'],
+ '83': ['MediaMetadata', 'MediaSession', '!Sanitizer'],
+ '84': ['PerformancePaintTiming'],
+ '85-86': ['PerformancePaintTiming', '!HTMLMenuItemElement', '!onshow'],
+ '87': ['onbeforeinput'],
+ '88': ['onbeforeinput', '!VisualViewport'],
+ '89-92': ['!ondevicelight', '!ondeviceproximity', '!onuserproximity'],
+ '93-95': ['ElementInternals'],
+ '96': ['Lock', 'LockManager'],
+ '97': ['CSSLayerBlockRule', 'CSSLayerStatementRule'],
+ '98': ['HTMLDialogElement'],
+ '99': ['NavigationPreloadManager'],
+ '100-104': ['WritableStream'],
+ '105-106': ['TextDecoderStream', 'OffscreenCanvasRenderingContext2D', 'OffscreenCanvas', 'TextEncoderStream'],
+ '107-109': ['CSSFontPaletteValuesRule'],
+ '110': ['CSSContainerRule'],
+ '111': ['FileSystemFileHandle', 'FileSystemDirectoryHandle'],
+ '112': ['FileSystemFileHandle', '!U2F'],
+ };
+ const IS_BLINK = browser == 'Chrome';
+ const IS_GECKO = browser == 'Firefox';
+ const css = (IS_BLINK ? blinkCSS : IS_GECKO ? geckoCSS : {});
+ const win = (IS_BLINK ? blinkWindow : IS_GECKO ? geckoWindow : {});
+ const js = (IS_BLINK ? blinkJS : IS_GECKO ? geckoJS : {});
+ return {
+ css,
+ win,
+ js,
+ };
+ };
+ const getJSCoreFeatures = (win) => {
+ const globalObjects = [
+ 'Object',
+ 'Function',
+ 'Boolean',
+ 'Symbol',
+ 'Error',
+ 'Number',
+ 'BigInt',
+ 'Math',
+ 'Date',
+ 'String',
+ 'RegExp',
+ 'Array',
+ 'Map',
+ 'Set',
+ 'WeakMap',
+ 'WeakSet',
+ 'Atomics',
+ 'JSON',
+ 'Promise',
+ 'Reflect',
+ 'Proxy',
+ 'Intl',
+ 'WebAssembly',
+ 'Document',
+ 'Element',
+ ];
+ try {
+ // @ts-ignore
+ const features = globalObjects.reduce((acc, name) => {
+ const ignore = ['name', 'length', 'constructor', 'prototype', 'arguments', 'caller'];
+ const descriptorKeys = Object.keys(Object.getOwnPropertyDescriptors(win[name] || {}));
+ const descriptorProtoKeys = Object.keys(Object.getOwnPropertyDescriptors((win[name] || {}).prototype || {}));
+ const uniques = [...new Set([...descriptorKeys, ...descriptorProtoKeys].filter((key) => !ignore.includes(key)))];
+ const keys = uniques.map((key) => `${name}.${key}`);
+ return [...acc, ...keys];
+ }, []);
+ return features;
+ }
+ catch (error) {
+ console.error(error);
+ return [];
+ }
+ };
+ // @ts-ignore
+ const versionSort = (x) => x.sort((a, b) => /\d+/.exec(a)[0] - /\d+/.exec(b)[0]).reverse();
+ const getVersionLie = (vReport, version, forgivenessOffset = 0) => {
+ const stable = getStableFeatures();
+ const { version: maxVersion } = stable[BROWSER] || {};
+ const validMetrics = vReport && version;
+ if (!validMetrics) {
+ return {};
+ }
+ const [vStart, vEnd] = version ? version.split('-') : [];
+ const vMax = (vEnd || vStart);
+ const reportIsTooHigh = +vReport > (+vMax + forgivenessOffset);
+ const reportIsTooLow = +vReport < (+vStart - forgivenessOffset);
+ const reportIsOff = (reportIsTooHigh || reportIsTooLow);
+ const versionIsAboveMax = ((+vMax == maxVersion) &&
+ (+vReport > maxVersion));
+ const liedVersion = !versionIsAboveMax && reportIsOff;
+ const distance = !liedVersion ? 0 : (Math.abs(vReport - (reportIsTooLow ? vStart : vMax)));
+ return { liedVersion, distance };
+ };
+ async function getEngineFeatures({ cssComputed, navigatorComputed, windowFeaturesComputed, }) {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const win = PHANTOM_DARKNESS ? PHANTOM_DARKNESS : window;
+ if (!cssComputed || !windowFeaturesComputed) {
+ logTestResult({ test: 'features', passed: false });
+ return;
+ }
+ const jsFeaturesKeys = getJSCoreFeatures(win);
+ const { keys: computedStyleKeys } = cssComputed.computedStyle || {};
+ const { keys: windowFeaturesKeys } = windowFeaturesComputed || {};
+ const { userAgentParsed: decryptedName } = navigatorComputed || {};
+ const isNative = (win, x) => (/\[native code\]/.test(win[x] + '') &&
+ 'prototype' in win[x] &&
+ win[x].prototype.constructor.name === x);
+ // @ts-ignore
+ const getFeatures = ({ context, allKeys, engineMap, checkNative = false } = {}) => {
+ const allKeysSet = new Set(allKeys);
+ const features = new Set();
+ // @ts-ignore
+ const match = Object.keys(engineMap || {}).reduce((acc, key) => {
+ const version = engineMap[key];
+ const versionLen = version.length;
+ const featureLen = version.filter((prop) => {
+ const removedFromVersion = prop.charAt(0) == '!';
+ if (removedFromVersion) {
+ const propName = prop.slice(1);
+ return !allKeysSet.has(propName) && features.add(prop);
+ }
+ return (allKeysSet.has(prop) &&
+ (checkNative ? isNative(context, prop) : true) &&
+ features.add(prop));
+ }).length;
+ return versionLen == featureLen ? [...acc, key] : acc;
+ }, []);
+ const version = versionSort(match)[0];
+ return {
+ version,
+ features,
+ };
+ };
+ // engine maps
+ const { css: engineMapCSS, win: engineMapWindow, js: engineMapJS, } = getEngineMaps(BROWSER);
+ // css version
+ const { version: cssVersion, features: cssFeatures, } = getFeatures({
+ context: win,
+ allKeys: computedStyleKeys,
+ engineMap: engineMapCSS,
+ });
+ // window version
+ const { version: windowVersion, features: windowFeatures, } = getFeatures({
+ context: win,
+ allKeys: windowFeaturesKeys,
+ engineMap: engineMapWindow,
+ checkNative: true,
+ });
+ // js version
+ const { version: jsVersion, features: jsFeatures, } = getFeatures({
+ context: win,
+ allKeys: jsFeaturesKeys,
+ engineMap: engineMapJS,
+ });
+ // determine version based on 3 factors
+ const getVersionFromRange = (range, versionCollection) => {
+ const exactVersion = versionCollection.find((version) => version && !/-/.test(version));
+ if (exactVersion) {
+ return exactVersion;
+ }
+ const len = range.length;
+ const first = range[0];
+ const last = range[len - 1];
+ return (!len ? '' :
+ len == 1 ? first :
+ `${last}-${first}`);
+ };
+ const versionSet = new Set([
+ cssVersion,
+ windowVersion,
+ jsVersion,
+ ]);
+ versionSet.delete(undefined);
+ const versionRange = versionSort([...versionSet].reduce((acc, x) => [...acc, ...x.split('-')], []));
+ const version = getVersionFromRange(versionRange, [cssVersion, windowVersion, jsVersion]);
+ const vReport = (/\d+/.exec(decryptedName) || [])[0];
+ const { liedVersion: liedCSS, distance: distanceCSS, } = getVersionLie(vReport, cssVersion);
+ const { liedVersion: liedJS, distance: distanceJS, } = getVersionLie(vReport, jsVersion);
+ const { liedVersion: liedWindow, distance: distanceWindow, } = getVersionLie(vReport, windowVersion);
+ if (liedCSS) {
+ sendToTrash('userAgent', `v${vReport} failed v${cssVersion} CSS features`);
+ if (distanceCSS > 1) {
+ documentLie(`Navigator.userAgent`, `v${vReport} failed CSS features by ${distanceCSS} versions`);
+ }
+ }
+ if (liedJS) {
+ sendToTrash('userAgent', `v${vReport} failed v${jsVersion} JS features`);
+ if (distanceJS > 2) {
+ documentLie(`Navigator.userAgent`, `v${vReport} failed JS features by ${distanceJS} versions`);
+ }
+ }
+ if (liedWindow) {
+ sendToTrash('userAgent', `v${vReport} failed v${windowVersion} Window features`);
+ if (distanceWindow > 3) {
+ documentLie(`Navigator.userAgent`, `v${vReport} failed Window features by ${distanceWindow} versions`);
+ }
+ }
+ logTestResult({ time: timer.stop(), test: 'features', passed: true });
+ return {
+ versionRange,
+ version,
+ cssVersion,
+ windowVersion,
+ jsVersion,
+ cssFeatures: [...cssFeatures],
+ windowFeatures: [...windowFeatures],
+ jsFeatures: [...jsFeatures],
+ jsFeaturesKeys,
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'features', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function featuresHTML(fp) {
+ if (!fp.features) {
+ return `
+
+
Features: ${HTMLNote.UNKNOWN}
+
JS/DOM: ${HTMLNote.UNKNOWN}
+
+
+
CSS: ${HTMLNote.UNKNOWN}
+
Window: ${HTMLNote.UNKNOWN}
+
`;
+ }
+ const { versionRange, version, cssVersion, jsVersion, windowVersion, cssFeatures, windowFeatures, jsFeatures, jsFeaturesKeys, } = fp.features || {};
+ const { keys: windowFeaturesKeys } = fp.windowFeatures || {};
+ const { keys: computedStyleKeys } = fp.css.computedStyle || {};
+ const { userAgentVersion } = fp.workerScope || {};
+ const { css: engineMapCSS, win: engineMapWindow, js: engineMapJS, } = getEngineMaps(BROWSER);
+ // logger
+ const shouldLogFeatures = (browser, version, userAgentVersion) => {
+ const shouldLog = userAgentVersion > version;
+ return shouldLog;
+ };
+ const log = ({ features, name, diff }) => {
+ console.groupCollapsed(`%c ${name} Features %c-${diff.removed.length} %c+${diff.added.length}`, 'color: #4cc1f9', 'color: Salmon', 'color: MediumAquaMarine');
+ Object.keys(diff).forEach((key) => {
+ console.log(`%c${key}:`, `color: ${key == 'added' ? 'MediumAquaMarine' : 'Salmon'}`);
+ return console.log(diff[key].join('\n'));
+ });
+ console.log(features.join(', '));
+ return console.groupEnd();
+ };
+ // modal
+ const report = { computedStyleKeys, windowFeaturesKeys, jsFeaturesKeys };
+ const getModal = ({ id, engineMap, features, browser, report, userAgentVersion }) => {
+ // capture diffs from stable release
+ const stable = getStableFeatures();
+ const { windowKeys, cssKeys, jsKeys, version } = stable[browser] || {};
+ const logger = shouldLogFeatures(browser, version, userAgentVersion);
+ let diff = null;
+ if (id == 'css') {
+ const { computedStyleKeys } = report;
+ if (cssKeys) {
+ diff = getListDiff({
+ oldList: cssKeys.split(', '),
+ newList: computedStyleKeys,
+ removeCamelCase: true,
+ });
+ }
+ if (logger) {
+ console.log(`computing ${browser} ${userAgentVersion} diffs from ${browser} ${version}...`);
+ Analysis.featuresCSS = diff;
+ log({ features: computedStyleKeys, name: 'CSS', diff });
+ }
+ }
+ else if (id == 'window') {
+ const { windowFeaturesKeys } = report;
+ if (windowKeys) {
+ diff = getListDiff({
+ oldList: windowKeys.split(', '),
+ newList: windowFeaturesKeys,
+ });
+ }
+ if (logger) {
+ Analysis.featuresWindow = diff;
+ log({ features: windowFeaturesKeys, name: 'Window', diff });
+ }
+ }
+ else if (id == 'js') {
+ const { jsFeaturesKeys } = report;
+ if (jsKeys) {
+ diff = getListDiff({
+ oldList: jsKeys.split(', '),
+ newList: jsFeaturesKeys,
+ });
+ }
+ if (logger) {
+ Analysis.featuresJS = diff;
+ log({ features: jsFeaturesKeys, name: 'JS', diff });
+ }
+ }
+ const header = !version || !diff || (!diff.added.length && !diff.removed.length) ? '' : `
+ diffs from ${version} :
+
+ ${diff && diff.added.length ?
+ diff.added.map((key) => `
${key}
`).join('') : ''}
+ ${diff && diff.removed.length ?
+ diff.removed.map((key) => `
${key}
`).join('') : ''}
+
+
+ `;
+ return modal(`creep-features-${id}`, header + versionSort(Object.keys(engineMap)).map((key) => {
+ return `
+ ${key} : ${engineMap[key].map((prop) => {
+ return `${prop} `;
+ }).join(' ')}
+ `;
+ }).join(' '), hashMini([...features]));
+ };
+ Analysis.featuresVersion = +userAgentVersion || 0;
+ const cssModal = getModal({
+ id: 'css',
+ engineMap: engineMapCSS,
+ features: new Set(cssFeatures),
+ browser: BROWSER,
+ report,
+ userAgentVersion,
+ });
+ const windowModal = getModal({
+ id: 'window',
+ engineMap: engineMapWindow,
+ features: new Set(windowFeatures),
+ browser: BROWSER,
+ report,
+ userAgentVersion,
+ });
+ const jsModal = getModal({
+ id: 'js',
+ engineMap: engineMapJS,
+ features: new Set(jsFeatures),
+ browser: BROWSER,
+ report,
+ userAgentVersion,
+ });
+ const getIcon = (name) => ` `;
+ const browserIcon = (!BROWSER ? '' :
+ /chrome/i.test(BROWSER) ? getIcon('chrome') :
+ /firefox/i.test(BROWSER) ? getIcon('firefox') :
+ '');
+ return `
+
+ ${performanceLogger.getLog().features}
+
+
Features: ${versionRange.length ? `${browserIcon}${version}+` : HTMLNote.UNKNOWN}
+
JS/DOM: ${jsVersion ? `${jsModal} (v${jsVersion})` : HTMLNote.UNKNOWN}
+
+
+
CSS: ${cssVersion ? `${cssModal} (v${cssVersion})` : HTMLNote.UNKNOWN}
+
Window: ${windowVersion ? `${windowModal} (v${windowVersion})` : HTMLNote.UNKNOWN}
+
+ `;
+ }
+
+ // inspired by Lalit Patel's fontdetect.js
+ // https://www.lalit.org/wordpress/wp-content/uploads/2008/05/fontdetect.js?ver=0.3
+ const WindowsFonts = {
+ // https://docs.microsoft.com/en-us/typography/fonts/windows_11_font_list
+ '7': [
+ 'Cambria Math',
+ 'Lucida Console',
+ ],
+ '8': [
+ 'Aldhabi',
+ 'Gadugi',
+ 'Myanmar Text',
+ 'Nirmala UI',
+ ],
+ '8.1': [
+ 'Leelawadee UI',
+ 'Javanese Text',
+ 'Segoe UI Emoji',
+ ],
+ '10': [
+ 'HoloLens MDL2 Assets', // 10 (v1507) +
+ 'Segoe MDL2 Assets', // 10 (v1507) +
+ 'Bahnschrift', // 10 (v1709) +-
+ 'Ink Free', // 10 (v1803) +-
+ ],
+ '11': ['Segoe Fluent Icons'],
+ };
+ const MacOSFonts = {
+ // Mavericks and below
+ '10.9': [
+ 'Helvetica Neue',
+ 'Geneva', // mac (not iOS)
+ ],
+ // Yosemite
+ '10.10': [
+ 'Kohinoor Devanagari Medium',
+ 'Luminari',
+ ],
+ // El Capitan
+ '10.11': [
+ 'PingFang HK Light',
+ ],
+ // Sierra: https://support.apple.com/en-ie/HT206872
+ '10.12': [
+ 'American Typewriter Semibold',
+ 'Futura Bold',
+ 'SignPainter-HouseScript Semibold',
+ ],
+ // High Sierra: https://support.apple.com/en-me/HT207962
+ // Mojave: https://support.apple.com/en-us/HT208968
+ '10.13-10.14': [
+ 'InaiMathi Bold',
+ ],
+ // Catalina: https://support.apple.com/en-us/HT210192
+ // Big Sur: https://support.apple.com/en-sg/HT211240
+ '10.15-11': [
+ 'Galvji',
+ 'MuktaMahee Regular',
+ ],
+ // Monterey: https://support.apple.com/en-us/HT212587
+ '12': [
+ 'Noto Sans Gunjala Gondi Regular',
+ 'Noto Sans Masaram Gondi Regular',
+ 'Noto Serif Yezidi Regular'
+ ],
+ // Ventura: https://support.apple.com/en-us/HT213266
+ '13': [
+ 'Apple SD Gothic Neo ExtraBold',
+ 'STIX Two Math Regular',
+ 'STIX Two Text Regular',
+ 'Noto Sans Canadian Aboriginal Regular',
+ ],
+ };
+ const DesktopAppFonts = {
+ // docs.microsoft.com/en-us/typography/font-list/ms-outlook
+ 'Microsoft Outlook': ['MS Outlook'],
+ // https://community.adobe.com/t5/postscript-discussions/zwadobef-font/m-p/3730427#M785
+ 'Adobe Acrobat': ['ZWAdobeF'],
+ // https://wiki.documentfoundation.org/Fonts
+ 'LibreOffice': [
+ 'Amiri',
+ 'KACSTOffice',
+ 'Liberation Mono',
+ 'Source Code Pro',
+ ],
+ // https://superuser.com/a/611804
+ 'OpenOffice': [
+ 'DejaVu Sans',
+ 'Gentium Book Basic',
+ 'OpenSymbol',
+ ],
+ };
+ const APPLE_FONTS = Object.keys(MacOSFonts).map((key) => MacOSFonts[key]).flat();
+ const WINDOWS_FONTS = Object.keys(WindowsFonts).map((key) => WindowsFonts[key]).flat();
+ const DESKTOP_APP_FONTS = (Object.keys(DesktopAppFonts).map((key) => DesktopAppFonts[key]).flat());
+ const LINUX_FONTS = [
+ 'Arimo', // ubuntu, chrome os
+ 'Chilanka', // ubuntu (not TB)
+ 'Cousine', // ubuntu, chrome os
+ 'Jomolhari', // chrome os
+ 'MONO', // ubuntu, chrome os (not TB)
+ 'Noto Color Emoji', // Linux
+ 'Ubuntu', // ubuntu (not TB)
+ ];
+ const ANDROID_FONTS = [
+ 'Dancing Script', // android
+ 'Droid Sans Mono', // Android
+ 'Roboto', // Android, Chrome OS
+ ];
+ const FONT_LIST = [
+ ...APPLE_FONTS,
+ ...WINDOWS_FONTS,
+ ...LINUX_FONTS,
+ ...ANDROID_FONTS,
+ ...DESKTOP_APP_FONTS,
+ ].sort();
+ async function getFonts() {
+ const getPixelEmojis = ({ doc, id, emojis }) => {
+ try {
+ patch(doc.getElementById(id), html `
+
+
+ ${emojis.map((emoji) => {
+ return `
${emoji}
`;
+ }).join('')}
+
+ `);
+ // get emoji set and system
+ const getEmojiDimensions = (style) => {
+ return {
+ width: style.inlineSize,
+ height: style.blockSize,
+ };
+ };
+ const pattern = new Set();
+ const emojiElems = [...doc.getElementsByClassName('pixel-emoji')];
+ const emojiSet = emojiElems.reduce((emojiSet, el, i) => {
+ const style = getComputedStyle(el);
+ const emoji = emojis[i];
+ const { height, width } = getEmojiDimensions(style);
+ const dimensions = `${width},${height}`;
+ if (!pattern.has(dimensions)) {
+ pattern.add(dimensions);
+ emojiSet.add(emoji);
+ }
+ return emojiSet;
+ }, new Set());
+ const pixelToNumber = (pixels) => +(pixels.replace('px', ''));
+ const pixelSizeSystemSum = 0.00001 * [...pattern].map((x) => {
+ return x.split(',').map((x) => pixelToNumber(x)).reduce((acc, x) => acc += (+x || 0), 0);
+ }).reduce((acc, x) => acc += x, 0);
+ doc.body.removeChild(doc.getElementById('pixel-emoji-container'));
+ return {
+ emojiSet: [...emojiSet],
+ pixelSizeSystemSum,
+ };
+ }
+ catch (error) {
+ console.error(error);
+ return {
+ emojiSet: [],
+ pixelSizeSystemSum: 0,
+ };
+ }
+ };
+ const getFontFaceLoadFonts = async (fontList) => {
+ try {
+ let fontsChecked = [];
+ if (!document.fonts.check(`0px "${getRandomValues()}"`)) {
+ fontsChecked = fontList.reduce((acc, font) => {
+ const found = document.fonts.check(`0px "${font}"`);
+ if (found)
+ acc.push(font);
+ return acc;
+ }, []);
+ }
+ const fontFaceList = fontList.map((font) => new FontFace(font, `local("${font}")`));
+ const responseCollection = await Promise
+ .allSettled(fontFaceList.map((font) => font.load()));
+ const fontsLoaded = responseCollection.reduce((acc, font) => {
+ if (font.status == 'fulfilled') {
+ acc.push(font.value.family);
+ }
+ return acc;
+ }, []);
+ return [...new Set([...fontsChecked, ...fontsLoaded])].sort();
+ }
+ catch (error) {
+ console.error(error);
+ return [];
+ }
+ };
+ const getPlatformVersion = (fonts) => {
+ const getWindows = ({ fonts, fontMap }) => {
+ const fontVersion = {
+ ['11']: fontMap['11'].find((x) => fonts.includes(x)),
+ ['10']: fontMap['10'].find((x) => fonts.includes(x)),
+ ['8.1']: fontMap['8.1'].find((x) => fonts.includes(x)),
+ ['8']: fontMap['8'].find((x) => fonts.includes(x)),
+ // require complete set of Windows 7 fonts
+ ['7']: fontMap['7'].filter((x) => fonts.includes(x)).length == fontMap['7'].length,
+ };
+ const hash = ('' + Object.keys(fontVersion).sort().filter((key) => !!fontVersion[key]));
+ const hashMap = {
+ '10,11,7,8,8.1': '11',
+ '10,7,8,8.1': '10',
+ '7,8,8.1': '8.1',
+ '11,7,8,8.1': '8.1', // missing 10
+ '7,8': '8',
+ '10,7,8': '8', // missing 8.1
+ '10,11,7,8': '8', // missing 8.1
+ '7': '7',
+ '7,8.1': '7',
+ '10,7,8.1': '7', // missing 8
+ '10,11,7,8.1': '7', // missing 8
+ };
+ const version = hashMap[hash];
+ return version ? `Windows ${version}` : undefined;
+ };
+ const getMacOS = ({ fonts, fontMap }) => {
+ const fontVersion = {
+ ['13']: fontMap['13'].find((x) => fonts.includes(x)),
+ ['12']: fontMap['12'].find((x) => fonts.includes(x)),
+ ['10.15-11']: fontMap['10.15-11'].find((x) => fonts.includes(x)),
+ ['10.13-10.14']: fontMap['10.13-10.14'].find((x) => fonts.includes(x)),
+ ['10.12']: fontMap['10.12'].find((x) => fonts.includes(x)),
+ ['10.11']: fontMap['10.11'].find((x) => fonts.includes(x)),
+ ['10.10']: fontMap['10.10'].find((x) => fonts.includes(x)),
+ // require complete set of 10.9 fonts
+ ['10.9']: fontMap['10.9'].filter((x) => fonts.includes(x)).length == fontMap['10.9'].length,
+ };
+ const hash = ('' + Object.keys(fontVersion).sort().filter((key) => !!fontVersion[key]));
+ const hashMap = {
+ '10.10,10.11,10.12,10.13-10.14,10.15-11,10.9,12,13': 'Ventura',
+ '10.10,10.11,10.12,10.13-10.14,10.15-11,10.9,12': 'Monterey',
+ '10.10,10.11,10.12,10.13-10.14,10.15-11,10.9': '10.15-11',
+ '10.10,10.11,10.12,10.13-10.14,10.9': '10.13-10.14',
+ '10.10,10.11,10.12,10.9': 'Sierra', // 10.12
+ '10.10,10.11,10.9': 'El Capitan', // 10.11
+ '10.10,10.9': 'Yosemite', // 10.10
+ '10.9': 'Mavericks', // 10.9
+ };
+ const version = hashMap[hash];
+ return version ? `macOS ${version}` : undefined;
+ };
+ return (getWindows({ fonts, fontMap: WindowsFonts }) ||
+ getMacOS({ fonts, fontMap: MacOSFonts }));
+ };
+ const getDesktopApps = (fonts) => {
+ // @ts-ignore
+ const apps = Object.keys(DesktopAppFonts).reduce((acc, key) => {
+ const appFontSet = DesktopAppFonts[key];
+ const match = appFontSet.filter((x) => fonts.includes(x)).length == appFontSet.length;
+ return match ? [...acc, key] : acc;
+ }, []);
+ return apps;
+ };
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const doc = (PHANTOM_DARKNESS &&
+ PHANTOM_DARKNESS.document &&
+ PHANTOM_DARKNESS.document.body ? PHANTOM_DARKNESS.document :
+ document);
+ const id = `font-fingerprint`;
+ const div = doc.createElement('div');
+ div.setAttribute('id', id);
+ doc.body.appendChild(div);
+ const { emojiSet, pixelSizeSystemSum, } = getPixelEmojis({
+ doc,
+ id,
+ emojis: EMOJIS,
+ }) || {};
+ const fontList = FONT_LIST;
+ const fontFaceLoadFonts = await getFontFaceLoadFonts(fontList);
+ const platformVersion = getPlatformVersion(fontFaceLoadFonts);
+ const apps = getDesktopApps(fontFaceLoadFonts);
+ // detect lies
+ const lied = (lieProps['FontFace.load'] ||
+ lieProps['FontFace.family'] ||
+ lieProps['FontFace.status'] ||
+ lieProps['String.fromCodePoint'] ||
+ lieProps['CSSStyleDeclaration.setProperty'] ||
+ lieProps['CSS2Properties.setProperty']) || false;
+ if (isFontOSBad(USER_AGENT_OS, fontFaceLoadFonts)) {
+ LowerEntropy.FONTS = true,
+ Analysis.FontOsIsBad = true;
+ sendToTrash('platform', `${USER_AGENT_OS} system and fonts are uncommon`);
+ }
+ logTestResult({ time: timer.stop(), test: 'fonts', passed: true });
+ return {
+ fontFaceLoadFonts,
+ platformVersion,
+ apps,
+ emojiSet,
+ pixelSizeSystemSum,
+ lied,
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'fonts', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function fontsHTML(fp) {
+ if (!fp.fonts) {
+ return `
+
+
Fonts
+
load (0):
+
apps:${HTMLNote.BLOCKED}
+
${HTMLNote.BLOCKED}
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { fonts: { $hash, fontFaceLoadFonts, platformVersion, apps, emojiSet, pixelSizeSystemSum, lied, }, } = fp;
+ const icon = {
+ 'Linux': ' ',
+ 'Apple': ' ',
+ 'Windows': ' ',
+ 'Android': ' ',
+ 'CrOS': ' ',
+ };
+ const blockHelpTitle = `FontFace.load()\nCSSStyleDeclaration.setProperty()\nblock-size\ninline-size\nhash: ${hashMini(emojiSet)}\n${(emojiSet || []).map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`;
+ return `
+
+
${performanceLogger.getLog().fonts}
+
Fonts ${hashSlice($hash)}
+
load (${fontFaceLoadFonts ? count(fontFaceLoadFonts) : '0'}/${'' + FONT_LIST.length}): ${`Like ${platformVersion}` || ((fonts) => {
+ return !(fonts || []).length ? '' : ((('' + fonts).match(/Lucida Console/) || []).length ? `${icon.Windows}Like Windows` :
+ (('' + fonts).match(/Droid Sans Mono|Noto Color Emoji|Roboto/g) || []).length == 3 ? `${icon.Linux}${icon.Android}Like Linux Android` :
+ (('' + fonts).match(/Droid Sans Mono|Roboto/g) || []).length == 2 ? `${icon.Android}Like Android` :
+ (('' + fonts).match(/Noto Color Emoji|Roboto/g) || []).length == 2 ? `${icon.CrOS}Like Chrome OS` :
+ (('' + fonts).match(/Noto Color Emoji/) || []).length ? `${icon.Linux}Like Linux` :
+ (('' + fonts).match(/Arimo/) || []).length ? `${icon.Linux}Like Linux` :
+ (('' + fonts).match(/Helvetica Neue/g) || []).length == 2 ? `${icon.Apple}Like Apple` :
+ `${(fonts || [])[0]}...`);
+ })(fontFaceLoadFonts)}
+
apps: ${(apps || []).length ? apps.join(', ') : HTMLNote.UNSUPPORTED}
+
+ ${fontFaceLoadFonts.join(', ') || HTMLNote.UNSUPPORTED}
+
+
+
+ ${pixelSizeSystemSum || HTMLNote.UNSUPPORTED}
+ ${formatEmojiSet(emojiSet)}
+
+
+
+ `;
+ }
+
+ function getPlatformEstimate() {
+ if (!IS_BLINK)
+ return [];
+ const v80 = 'getVideoPlaybackQuality' in HTMLVideoElement.prototype;
+ const v81 = CSS.supports('color-scheme: initial');
+ const v84 = CSS.supports('appearance: initial');
+ const v86 = 'DisplayNames' in Intl;
+ const v88 = CSS.supports('aspect-ratio: initial');
+ const v89 = CSS.supports('border-end-end-radius: initial');
+ const v95 = 'randomUUID' in Crypto.prototype;
+ const hasBarcodeDetector = 'BarcodeDetector' in window;
+ // @ts-expect-error if not supported
+ const hasDownlinkMax = 'downlinkMax' in (window.NetworkInformation?.prototype || {});
+ const hasContentIndex = 'ContentIndex' in window;
+ const hasContactsManager = 'ContactsManager' in window;
+ const hasEyeDropper = 'EyeDropper' in window;
+ const hasFileSystemWritableFileStream = 'FileSystemWritableFileStream' in window;
+ const hasHid = 'HID' in window && 'HIDDevice' in window;
+ const hasSerialPort = 'SerialPort' in window && 'Serial' in window;
+ const hasSharedWorker = 'SharedWorker' in window;
+ const hasTouch = 'ontouchstart' in Window && 'TouchEvent' in window;
+ const hasAppBadge = 'setAppBadge' in Navigator.prototype;
+ const hasFeature = (version, condition) => {
+ return (version ? [condition] : []);
+ };
+ const estimate = {
+ ["Android" /* Platform.ANDROID */]: [
+ ...hasFeature(v88, hasBarcodeDetector),
+ ...hasFeature(v84, hasContentIndex),
+ ...hasFeature(v80, hasContactsManager),
+ hasDownlinkMax,
+ ...hasFeature(v95, !hasEyeDropper),
+ ...hasFeature(v86, !hasFileSystemWritableFileStream),
+ ...hasFeature(v89, !hasHid),
+ ...hasFeature(v89, !hasSerialPort),
+ !hasSharedWorker,
+ hasTouch,
+ ...hasFeature(v81, !hasAppBadge),
+ ],
+ ["Chrome OS" /* Platform.CHROME_OS */]: [
+ ...hasFeature(v88, hasBarcodeDetector),
+ ...hasFeature(v84, !hasContentIndex),
+ ...hasFeature(v80, !hasContactsManager),
+ hasDownlinkMax,
+ ...hasFeature(v95, hasEyeDropper),
+ ...hasFeature(v86, hasFileSystemWritableFileStream),
+ ...hasFeature(v89, hasHid),
+ ...hasFeature(v89, hasSerialPort),
+ hasSharedWorker,
+ hasTouch || !hasTouch,
+ ...hasFeature(v81, !hasAppBadge),
+ ],
+ ["Windows" /* Platform.WINDOWS */]: [
+ ...hasFeature(v88, !hasBarcodeDetector),
+ ...hasFeature(v84, !hasContentIndex),
+ ...hasFeature(v80, !hasContactsManager),
+ !hasDownlinkMax,
+ ...hasFeature(v95, hasEyeDropper),
+ ...hasFeature(v86, hasFileSystemWritableFileStream),
+ ...hasFeature(v89, hasHid),
+ ...hasFeature(v89, hasSerialPort),
+ hasSharedWorker,
+ hasTouch || !hasTouch,
+ ...hasFeature(v81, hasAppBadge),
+ ],
+ ["Mac" /* Platform.MAC */]: [
+ ...hasFeature(v88, hasBarcodeDetector),
+ ...hasFeature(v84, !hasContentIndex),
+ ...hasFeature(v80, !hasContactsManager),
+ !hasDownlinkMax,
+ ...hasFeature(v95, hasEyeDropper),
+ ...hasFeature(v86, hasFileSystemWritableFileStream),
+ ...hasFeature(v89, hasHid),
+ ...hasFeature(v89, hasSerialPort),
+ hasSharedWorker,
+ !hasTouch,
+ ...hasFeature(v81, hasAppBadge),
+ ],
+ ["Linux" /* Platform.LINUX */]: [
+ ...hasFeature(v88, !hasBarcodeDetector),
+ ...hasFeature(v84, !hasContentIndex),
+ ...hasFeature(v80, !hasContactsManager),
+ !hasDownlinkMax,
+ ...hasFeature(v95, hasEyeDropper),
+ ...hasFeature(v86, hasFileSystemWritableFileStream),
+ ...hasFeature(v89, hasHid),
+ ...hasFeature(v89, hasSerialPort),
+ hasSharedWorker,
+ !hasTouch || !hasTouch,
+ ...hasFeature(v81, !hasAppBadge),
+ ],
+ };
+ // Chrome only features
+ const headlessEstimate = {
+ noContentIndex: v84 && !hasContentIndex,
+ noContactsManager: v80 && !hasContactsManager,
+ noDownlinkMax: !hasDownlinkMax,
+ };
+ const scores = Object.keys(estimate).reduce((acc, key) => {
+ const list = estimate[key];
+ const score = +((list.filter((x) => x).length / list.length).toFixed(2));
+ acc[key] = score;
+ return acc;
+ }, {});
+ const platform = Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b);
+ const highestScore = scores[platform];
+ return [scores, highestScore, headlessEstimate];
+ }
+
+ const SYSTEM_FONTS = [
+ 'caption',
+ 'icon',
+ 'menu',
+ 'message-box',
+ 'small-caption',
+ 'status-bar',
+ ];
+
+ const GeckoFonts = {
+ '-apple-system': "Mac" /* Platform.MAC */,
+ 'Segoe UI': "Windows" /* Platform.WINDOWS */,
+ 'Tahoma': "Windows" /* Platform.WINDOWS */,
+ 'Yu Gothic UI': "Windows" /* Platform.WINDOWS */,
+ 'Microsoft JhengHei UI': "Windows" /* Platform.WINDOWS */,
+ 'Microsoft YaHei UI': "Windows" /* Platform.WINDOWS */,
+ 'Meiryo UI': "Windows" /* Platform.WINDOWS */,
+ 'Cantarell': "Linux" /* Platform.LINUX */,
+ 'Ubuntu': "Linux" /* Platform.LINUX */,
+ 'Sans': "Linux" /* Platform.LINUX */,
+ 'sans-serif': "Linux" /* Platform.LINUX */,
+ 'Fira Sans': "Linux" /* Platform.LINUX */,
+ 'Roboto': "Android" /* Platform.ANDROID */,
+ };
+ function getSystemFonts() {
+ const { body } = document;
+ const el = document.createElement('div');
+ body.appendChild(el);
+ try {
+ const systemFonts = String([
+ ...SYSTEM_FONTS.reduce((acc, font) => {
+ el.setAttribute('style', `font: ${font} !important`);
+ return acc.add(getComputedStyle(el).fontFamily);
+ }, new Set()),
+ ]);
+ const geckoPlatform = GeckoFonts[systemFonts];
+ return GeckoFonts[systemFonts] ? `${systemFonts}:${geckoPlatform}` : systemFonts;
+ }
+ catch (err) {
+ return '';
+ }
+ finally {
+ body.removeChild(el);
+ }
+ }
+
+ /* eslint-disable new-cap */
+ async function getHeadlessFeatures({ webgl, workerScope, }) {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const mimeTypes = Object.keys({ ...navigator.mimeTypes });
+ const systemFonts = getSystemFonts();
+ const [scores, highestScore, headlessEstimate] = getPlatformEstimate();
+ const data = {
+ chromium: IS_BLINK,
+ likeHeadless: {
+ noChrome: IS_BLINK && !('chrome' in window),
+ hasPermissionsBug: (IS_BLINK &&
+ 'permissions' in navigator &&
+ await (async () => {
+ const res = await navigator.permissions.query({ name: 'notifications' });
+ return (res.state == 'prompt' &&
+ 'Notification' in window &&
+ Notification.permission === 'denied');
+ })()),
+ noPlugins: IS_BLINK && navigator.plugins.length === 0,
+ noMimeTypes: IS_BLINK && mimeTypes.length === 0,
+ notificationIsDenied: (IS_BLINK &&
+ 'Notification' in window &&
+ (Notification.permission == 'denied')),
+ hasKnownBgColor: IS_BLINK && (() => {
+ let rendered = PARENT_PHANTOM;
+ if (!PARENT_PHANTOM) {
+ rendered = document.createElement('div');
+ document.body.appendChild(rendered);
+ }
+ if (!rendered)
+ return false;
+ rendered.setAttribute('style', `background-color: ActiveText`);
+ const { backgroundColor: activeText } = getComputedStyle(rendered) || [];
+ if (!PARENT_PHANTOM) {
+ document.body.removeChild(rendered);
+ }
+ return activeText === 'rgb(255, 0, 0)';
+ })(),
+ prefersLightColor: matchMedia('(prefers-color-scheme: light)').matches,
+ uaDataIsBlank: ('userAgentData' in navigator && (
+ // @ts-expect-error if userAgentData is null
+ navigator.userAgentData?.platform === '' ||
+ // @ts-expect-error if userAgentData is null
+ await navigator.userAgentData.getHighEntropyValues(['platform']).platform === '')),
+ pdfIsDisabled: ('pdfViewerEnabled' in navigator && navigator.pdfViewerEnabled === false),
+ noTaskbar: (screen.height === screen.availHeight &&
+ screen.width === screen.availWidth),
+ hasVvpScreenRes: ((innerWidth === screen.width && outerHeight === screen.height) || ('visualViewport' in window &&
+ // @ts-expect-error if unsupported
+ (visualViewport.width === screen.width && visualViewport.height === screen.height))),
+ hasSwiftShader: /SwiftShader/.test(workerScope?.webglRenderer),
+ noWebShare: IS_BLINK && CSS.supports('accent-color: initial') && (!('share' in navigator) || !('canShare' in navigator)),
+ noContentIndex: !!headlessEstimate?.noContentIndex,
+ noContactsManager: !!headlessEstimate?.noContactsManager,
+ noDownlinkMax: !!headlessEstimate?.noDownlinkMax,
+ },
+ headless: {
+ webDriverIsOn: ((CSS.supports('border-end-end-radius: initial') && navigator.webdriver === undefined) ||
+ !!navigator.webdriver ||
+ !!lieProps['Navigator.webdriver']),
+ hasHeadlessUA: (/HeadlessChrome/.test(navigator.userAgent) ||
+ /HeadlessChrome/.test(navigator.appVersion)),
+ hasHeadlessWorkerUA: !!workerScope && (/HeadlessChrome/.test(workerScope.userAgent)),
+ },
+ stealth: {
+ hasIframeProxy: (() => {
+ try {
+ const iframe = document.createElement('iframe');
+ iframe.srcdoc = instanceId;
+ return !!iframe.contentWindow;
+ }
+ catch (err) {
+ return true;
+ }
+ })(),
+ hasHighChromeIndex: (() => {
+ const key = 'chrome';
+ const highIndexRange = -50;
+ return (Object.keys(window).slice(highIndexRange).includes(key) &&
+ Object.getOwnPropertyNames(window).slice(highIndexRange).includes(key));
+ })(),
+ hasBadChromeRuntime: (() => {
+ // @ts-expect-error if unsupported
+ if (!('chrome' in window && 'runtime' in chrome)) {
+ return false;
+ }
+ try {
+ // @ts-expect-error if unsupported
+ if ('prototype' in chrome.runtime.sendMessage ||
+ // @ts-expect-error if unsupported
+ 'prototype' in chrome.runtime.connect) {
+ return true;
+ }
+ // @ts-expect-error if unsupported
+ new chrome.runtime.sendMessage;
+ // @ts-expect-error if unsupported
+ new chrome.runtime.connect;
+ return true;
+ }
+ catch (err) {
+ return err.constructor.name != 'TypeError' ? true : false;
+ }
+ })(),
+ hasToStringProxy: (!!lieProps['Function.toString']),
+ hasBadWebGL: (() => {
+ const { UNMASKED_RENDERER_WEBGL: gpu } = webgl?.parameters || {};
+ const { webglRenderer: workerGPU } = workerScope || {};
+ return (gpu && workerGPU && (gpu !== workerGPU));
+ })(),
+ },
+ };
+ const { likeHeadless, headless, stealth } = data;
+ const likeHeadlessKeys = Object.keys(likeHeadless);
+ const headlessKeys = Object.keys(headless);
+ const stealthKeys = Object.keys(stealth);
+ const likeHeadlessRating = +((likeHeadlessKeys.filter((key) => likeHeadless[key]).length / likeHeadlessKeys.length) * 100).toFixed(0);
+ const headlessRating = +((headlessKeys.filter((key) => headless[key]).length / headlessKeys.length) * 100).toFixed(0);
+ const stealthRating = +((stealthKeys.filter((key) => stealth[key]).length / stealthKeys.length) * 100).toFixed(0);
+ logTestResult({ time: timer.stop(), test: 'headless', passed: true });
+ return {
+ ...data,
+ likeHeadlessRating,
+ headlessRating,
+ stealthRating,
+ systemFonts,
+ platformEstimate: [scores, highestScore],
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'headless', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function headlessFeaturesHTML(fp) {
+ if (!fp.headless) {
+ return `
+
+
Headless
+
chromium: ${HTMLNote.BLOCKED}
+
0% like headless: ${HTMLNote.BLOCKED}
+
0% headless: ${HTMLNote.BLOCKED}
+
0% stealth: ${HTMLNote.BLOCKED}
+
platform hints:
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { headless: data, } = fp;
+ const { $hash, chromium, likeHeadless, likeHeadlessRating, headless, headlessRating, stealth, stealthRating, systemFonts, platformEstimate, } = data || {};
+ const [scores, highestScore] = platformEstimate || [];
+ const IconMap = {
+ ["Android" /* Platform.ANDROID */]: ` `,
+ ["Chrome OS" /* Platform.CHROME_OS */]: ` `,
+ ["Windows" /* Platform.WINDOWS */]: ` `,
+ ["Mac" /* Platform.MAC */]: ` `,
+ ["Linux" /* Platform.LINUX */]: ` `,
+ };
+ const scoreKeys = Object.keys(scores || {});
+ const platformTemplate = !scores ? '' : `
+ ${scoreKeys.map((key) => (scores[key] * 100).toFixed(0)).join(':')}
+ ${scoreKeys.map((key) => {
+ const score = scores[key];
+ const style = `
+ filter: opacity(${score == highestScore ? 100 : 15}%);
+ `;
+ return `${IconMap[key]} `;
+ }).join('')}
+ `;
+ return `
+
+
+
${performanceLogger.getLog().headless}
+
Headless ${hashSlice($hash)}
+
chromium: ${'' + chromium}
+
${'' + likeHeadlessRating}% like headless: ${modal('creep-like-headless', 'Like Headless ' +
+ Object.keys(likeHeadless).map((key) => `${key}: ${'' + likeHeadless[key]}`).join(' '), hashMini(likeHeadless))}
+
${'' + headlessRating}% headless: ${modal('creep-headless', 'Headless ' +
+ Object.keys(headless).map((key) => `${key}: ${'' + headless[key]}`).join(' '), hashMini(headless))}
+
${'' + stealthRating}% stealth: ${modal('creep-stealth', 'Stealth ' +
+ Object.keys(stealth).map((key) => `${key}: ${'' + stealth[key]}`).join(' '), hashMini(stealth))}
+
platform hints:
+
+ ${systemFonts ? `
${systemFonts}
` : ''}
+ ${platformTemplate ? `
${platformTemplate}
` : ''}
+
+
`;
+ }
+
+ async function getIntl() {
+ const getLocale = (intl) => {
+ const constructors = [
+ 'Collator',
+ 'DateTimeFormat',
+ 'DisplayNames',
+ 'ListFormat',
+ 'NumberFormat',
+ 'PluralRules',
+ 'RelativeTimeFormat',
+ ];
+ // @ts-ignore
+ const locale = constructors.reduce((acc, name) => {
+ try {
+ const obj = new intl[name];
+ if (!obj) {
+ return acc;
+ }
+ const { locale } = obj.resolvedOptions() || {};
+ return [...acc, locale];
+ }
+ catch (error) {
+ return acc;
+ }
+ }, []);
+ return [...new Set(locale)];
+ };
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const lied = (lieProps['Intl.Collator.resolvedOptions'] ||
+ lieProps['Intl.DateTimeFormat.resolvedOptions'] ||
+ lieProps['Intl.DisplayNames.resolvedOptions'] ||
+ lieProps['Intl.ListFormat.resolvedOptions'] ||
+ lieProps['Intl.NumberFormat.resolvedOptions'] ||
+ lieProps['Intl.PluralRules.resolvedOptions'] ||
+ lieProps['Intl.RelativeTimeFormat.resolvedOptions']) || false;
+ const dateTimeFormat = caniuse(() => {
+ return new Intl.DateTimeFormat(undefined, {
+ month: 'long',
+ timeZoneName: 'long',
+ }).format(963644400000);
+ });
+ const displayNames = caniuse(() => {
+ return new Intl.DisplayNames(undefined, {
+ type: 'language',
+ }).of('en-US');
+ });
+ const listFormat = caniuse(() => {
+ // @ts-ignore
+ return new Intl.ListFormat(undefined, {
+ style: 'long',
+ type: 'disjunction',
+ }).format(['0', '1']);
+ });
+ const numberFormat = caniuse(() => {
+ return new Intl.NumberFormat(undefined, {
+ notation: 'compact',
+ compactDisplay: 'long',
+ }).format(21000000);
+ });
+ const pluralRules = caniuse(() => {
+ return new Intl.PluralRules().select(1);
+ });
+ const relativeTimeFormat = caniuse(() => {
+ return new Intl.RelativeTimeFormat(undefined, {
+ localeMatcher: 'best fit',
+ numeric: 'auto',
+ style: 'long',
+ }).format(1, 'year');
+ });
+ const locale = getLocale(Intl);
+ logTestResult({ time: timer.stop(), test: 'intl', passed: true });
+ return {
+ dateTimeFormat,
+ displayNames,
+ listFormat,
+ numberFormat,
+ pluralRules,
+ relativeTimeFormat,
+ locale: '' + locale,
+ lied,
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'intl', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function intlHTML(fp) {
+ if (!fp.htmlElementVersion) {
+ return `
+
+
Intl
+
locale: ${HTMLNote.Blocked}
+
date: ${HTMLNote.Blocked}
+
display: ${HTMLNote.Blocked}
+
list: ${HTMLNote.Blocked}
+
number: ${HTMLNote.Blocked}
+
plural: ${HTMLNote.Blocked}
+
relative: ${HTMLNote.Blocked}
+
`;
+ }
+ const { $hash, dateTimeFormat, displayNames, listFormat, numberFormat, pluralRules, relativeTimeFormat, locale, lied, } = fp.intl || {};
+ return `
+
+
${performanceLogger.getLog().intl}
+
Intl ${hashSlice($hash)}
+
+ ${[
+ locale,
+ dateTimeFormat,
+ displayNames,
+ numberFormat,
+ relativeTimeFormat,
+ listFormat,
+ pluralRules,
+ ].join(' ')}
+
+
+ `;
+ }
+
+ function getMaths() {
+ try {
+ const timer = createTimer();
+ timer.start();
+ // detect failed math equality lie
+ const check = [
+ 'acos',
+ 'acosh',
+ 'asin',
+ 'asinh',
+ 'atan',
+ 'atanh',
+ 'atan2',
+ 'cbrt',
+ 'cos',
+ 'cosh',
+ 'expm1',
+ 'exp',
+ 'hypot',
+ 'log',
+ 'log1p',
+ 'log10',
+ 'sin',
+ 'sinh',
+ 'sqrt',
+ 'tan',
+ 'tanh',
+ 'pow',
+ ];
+ let lied = false;
+ check.forEach((prop) => {
+ if (!!lieProps[`Math.${prop}`]) {
+ lied = true;
+ }
+ const test = (prop == 'cos' ? [1e308] :
+ prop == 'acos' || prop == 'asin' || prop == 'atanh' ? [0.5] :
+ prop == 'pow' || prop == 'atan2' ? [Math.PI, 2] :
+ [Math.PI]);
+ const res1 = Math[prop](...test);
+ const res2 = Math[prop](...test);
+ const matching = isNaN(res1) && isNaN(res2) ? true : res1 == res2;
+ if (!matching) {
+ lied = true;
+ const mathLie = `expected x and got y`;
+ documentLie(`Math.${prop}`, mathLie);
+ }
+ return;
+ });
+ const n = 0.123;
+ const bigN = 5.860847362277284e+38;
+ const fns = [
+ ['acos', [n], `acos(${n})`, 1.4474840516030247, NaN, NaN, 1.4474840516030245],
+ ['acos', [Math.SQRT1_2], 'acos(Math.SQRT1_2)', 0.7853981633974483, NaN, NaN, NaN],
+ ['acosh', [1e308], 'acosh(1e308)', 709.889355822726, NaN, NaN, NaN],
+ ['acosh', [Math.PI], 'acosh(Math.PI)', 1.811526272460853, NaN, NaN, NaN],
+ ['acosh', [Math.SQRT2], 'acosh(Math.SQRT2)', 0.881373587019543, NaN, NaN, 0.8813735870195432],
+ ['asin', [n], `asin(${n})`, 0.12331227519187199, NaN, NaN, NaN],
+ ['asinh', [1e300], 'asinh(1e308)', 691.4686750787736, NaN, NaN, NaN],
+ ['asinh', [Math.PI], 'asinh(Math.PI)', 1.8622957433108482, NaN, NaN, NaN],
+ ['atan', [2], 'atan(2)', 1.1071487177940904, NaN, NaN, 1.1071487177940906],
+ ['atan', [Math.PI], 'atan(Math.PI)', 1.2626272556789115, NaN, NaN, NaN],
+ ['atanh', [0.5], 'atanh(0.5)', 0.5493061443340548, NaN, NaN, 0.5493061443340549],
+ ['atan2', [1e-310, 2], 'atan2(1e-310, 2)', 5e-311, NaN, NaN, NaN],
+ ['atan2', [Math.PI, 2], 'atan2(Math.PI)', 1.0038848218538872, NaN, NaN, NaN],
+ ['cbrt', [100], 'cbrt(100)', 4.641588833612779, NaN, NaN, NaN],
+ ['cbrt', [Math.PI], 'cbrt(Math.PI)', 1.4645918875615231, NaN, NaN, 1.4645918875615234],
+ ['cos', [n], `cos(${n})`, 0.9924450321351935, NaN, NaN, NaN],
+ ['cos', [Math.PI], 'cos(Math.PI)', -1, NaN, NaN, NaN],
+ ['cos', [bigN], `cos(${bigN})`, -0.10868049424995659, NaN, -0.9779661551196617, NaN],
+ ['cos', [-1e308], 'cos(-1e308)', -0.8913089376870335, NaN, 0.99970162388838, NaN],
+ ['cos', [13 * Math.E], 'cos(13*Math.E)', -0.7108118501064331, -0.7108118501064332, NaN, NaN],
+ ['cos', [57 * Math.E], 'cos(57*Math.E)', -0.536911695749024, -0.5369116957490239, NaN, NaN],
+ ['cos', [21 * Math.LN2], 'cos(21*Math.LN2)', -0.4067775970251724, -0.40677759702517235, -0.6534063185820197, NaN],
+ ['cos', [51 * Math.LN2], 'cos(51*Math.LN2)', -0.7017203400855446, -0.7017203400855445, NaN, NaN],
+ ['cos', [21 * Math.LOG2E], 'cos(21*Math.LOG2E)', 0.4362848063618998, 0.43628480636189976, NaN, NaN],
+ ['cos', [25 * Math.SQRT2], 'cos(25*Math.SQRT2)', -0.6982689820462377, -0.6982689820462376, NaN, NaN],
+ ['cos', [50 * Math.SQRT1_2], 'cos(50*Math.SQRT1_2)', -0.6982689820462377, -0.6982689820462376, NaN, NaN],
+ ['cos', [21 * Math.SQRT1_2], 'cos(21*Math.SQRT1_2)', -0.6534063185820198, NaN, NaN, NaN],
+ ['cos', [17 * Math.LOG10E], 'cos(17*Math.LOG10E)', 0.4537557425982784, 0.45375574259827833, NaN, NaN],
+ ['cos', [2 * Math.LOG10E], 'cos(2*Math.LOG10E)', 0.6459044007438142, NaN, 0.6459044007438141, NaN],
+ ['cosh', [1], 'cosh(1)', 1.5430806348152437, NaN, NaN, NaN],
+ ['cosh', [Math.PI], 'cosh(Math.PI)', 11.591953275521519, NaN, NaN, NaN],
+ ['cosh', [492 * Math.LOG2E], 'cosh(492*Math.LOG2E)', 9.199870313877772e+307, 9.199870313877774e+307, NaN, NaN],
+ ['cosh', [502 * Math.SQRT2], 'cosh(502*Math.SQRT2)', 1.0469199669023138e+308, 1.046919966902314e+308, NaN, NaN],
+ ['expm1', [1], 'expm1(1)', 1.718281828459045, NaN, NaN, 1.7182818284590453],
+ ['expm1', [Math.PI], 'expm1(Math.PI)', 22.140692632779267, NaN, NaN, NaN],
+ ['exp', [n], `exp(${n})`, 1.1308844209474893, NaN, NaN, NaN],
+ ['exp', [Math.PI], 'exp(Math.PI)', 23.140692632779267, NaN, NaN, NaN],
+ ['hypot', [1, 2, 3, 4, 5, 6], 'hypot(1, 2, 3, 4, 5, 6)', 9.539392014169456, NaN, NaN, NaN],
+ ['hypot', [bigN, bigN], `hypot(${bigN}, ${bigN})`, 8.288489826731116e+38, 8.288489826731114e+38, NaN, NaN],
+ ['hypot', [2 * Math.E, -100], 'hypot(2*Math.E, -100)', 100.14767208675259, 100.14767208675258, NaN, NaN],
+ ['hypot', [6 * Math.PI, -100], 'hypot(6*Math.PI, -100)', 101.76102278593319, 101.7610227859332, NaN, NaN],
+ ['hypot', [2 * Math.LN2, -100], 'hypot(2*Math.LN2, -100)', 100.0096085986525, 100.00960859865252, NaN, NaN],
+ ['hypot', [Math.LOG2E, -100], 'hypot(Math.LOG2E, -100)', 100.01040630344929, 100.01040630344927, NaN, NaN],
+ ['hypot', [Math.SQRT2, -100], 'hypot(Math.SQRT2, -100)', 100.00999950004999, 100.00999950005, NaN, NaN],
+ ['hypot', [Math.SQRT1_2, -100], 'hypot(Math.SQRT1_2, -100)', 100.0024999687508, 100.00249996875078, NaN, NaN],
+ ['hypot', [2 * Math.LOG10E, -100], 'hypot(2*Math.LOG10E, -100)', 100.00377216279416, 100.00377216279418, NaN, NaN],
+ ['log', [n], `log(${n})`, -2.0955709236097197, NaN, NaN, NaN],
+ ['log', [Math.PI], 'log(Math.PI)', 1.1447298858494002, NaN, NaN, NaN],
+ ['log1p', [n], `log1p(${n})`, 0.11600367575630613, NaN, NaN, NaN],
+ ['log1p', [Math.PI], 'log1p(Math.PI)', 1.4210804127942926, NaN, NaN, NaN],
+ ['log10', [n], `log10(${n})`, -0.9100948885606021, NaN, NaN, NaN],
+ ['log10', [Math.PI], 'log10(Math.PI)', 0.4971498726941338, 0.49714987269413385, NaN, NaN],
+ ['log10', [Math.E], 'log10(Math.E)', 0.4342944819032518, NaN, NaN, NaN],
+ ['log10', [34 * Math.E], 'log10(34*Math.E)', 1.9657733989455068, 1.965773398945507, NaN, NaN],
+ ['log10', [Math.LN2], 'log10(Math.LN2)', -0.1591745389548616, NaN, NaN, NaN],
+ ['log10', [11 * Math.LN2], 'log10(11*Math.LN2)', 0.8822181462033634, 0.8822181462033635, NaN, NaN],
+ ['log10', [Math.LOG2E], 'log10(Math.LOG2E)', 0.15917453895486158, NaN, NaN, NaN],
+ ['log10', [43 * Math.LOG2E], 'log10(43*Math.LOG2E)', 1.792642994534448, 1.7926429945344482, NaN, NaN],
+ ['log10', [Math.LOG10E], 'log10(Math.LOG10E)', -0.36221568869946325, NaN, NaN, NaN],
+ ['log10', [7 * Math.LOG10E], 'log10(7*Math.LOG10E)', 0.4828823513147936, 0.48288235131479357, NaN, NaN],
+ ['log10', [Math.SQRT1_2], 'log10(Math.SQRT1_2)', -0.15051499783199057, NaN, NaN, NaN],
+ ['log10', [2 * Math.SQRT1_2], 'log10(2*Math.SQRT1_2)', 0.1505149978319906, 0.15051499783199063, NaN, NaN],
+ ['log10', [Math.SQRT2], 'log10(Math.SQRT2)', 0.1505149978319906, 0.15051499783199063, NaN, NaN],
+ ['sin', [bigN], `sin(${bigN})`, 0.994076732536068, NaN, -0.20876350121720488, NaN],
+ ['sin', [Math.PI], 'sin(Math.PI)', 1.2246467991473532e-16, NaN, 1.2246063538223773e-16, NaN],
+ ['sin', [39 * Math.E], 'sin(39*Math.E)', -0.7181630308570677, -0.7181630308570678, NaN, NaN],
+ ['sin', [35 * Math.LN2], 'sin(35*Math.LN2)', -0.7659964138980511, -0.765996413898051, NaN, NaN],
+ ['sin', [110 * Math.LOG2E], 'sin(110*Math.LOG2E)', 0.9989410140273756, 0.9989410140273757, NaN, NaN],
+ ['sin', [7 * Math.LOG10E], 'sin(7*Math.LOG10E)', 0.10135692924965616, 0.10135692924965614, NaN, NaN],
+ ['sin', [35 * Math.SQRT1_2], 'sin(35*Math.SQRT1_2)', -0.3746357547858202, -0.37463575478582023, NaN, NaN],
+ ['sin', [21 * Math.SQRT2], 'sin(21*Math.SQRT2)', -0.9892668187780498, -0.9892668187780497, NaN, NaN],
+ ['sinh', [1], 'sinh(1)', 1.1752011936438014, NaN, NaN, NaN],
+ ['sinh', [Math.PI], 'sinh(Math.PI)', 11.548739357257748, NaN, NaN, 11.548739357257746],
+ ['sinh', [Math.E], 'sinh(Math.E)', 7.544137102816975, NaN, NaN, NaN],
+ ['sinh', [Math.LN2], 'sinh(Math.LN2)', 0.75, NaN, NaN, NaN],
+ ['sinh', [Math.LOG2E], 'sinh(Math.LOG2E)', 1.9978980091062795, NaN, NaN, NaN],
+ ['sinh', [492 * Math.LOG2E], 'sinh(492*Math.LOG2E)', 9.199870313877772e+307, 9.199870313877774e+307, NaN, NaN],
+ ['sinh', [Math.LOG10E], 'sinh(Math.LOG10E)', 0.44807597941469024, NaN, NaN, NaN],
+ ['sinh', [Math.SQRT1_2], 'sinh(Math.SQRT1_2)', 0.7675231451261164, NaN, NaN, NaN],
+ ['sinh', [Math.SQRT2], 'sinh(Math.SQRT2)', 1.935066822174357, NaN, NaN, 1.9350668221743568],
+ ['sinh', [502 * Math.SQRT2], 'sinh(502*Math.SQRT2)', 1.0469199669023138e+308, 1.046919966902314e+308, NaN, NaN],
+ ['sqrt', [n], `sqrt(${n})`, 0.3507135583350036, NaN, NaN, NaN],
+ ['sqrt', [Math.PI], 'sqrt(Math.PI)', 1.7724538509055159, NaN, NaN, NaN],
+ ['tan', [-1e308], 'tan(-1e308)', 0.5086861259107568, NaN, NaN, 0.5086861259107567],
+ ['tan', [Math.PI], 'tan(Math.PI)', -1.2246467991473532e-16, NaN, NaN, NaN],
+ ['tan', [6 * Math.E], 'tan(6*Math.E)', 0.6866761546452431, 0.686676154645243, NaN, NaN],
+ ['tan', [6 * Math.LN2], 'tan(6*Math.LN2)', 1.6182817135715877, 1.618281713571588, NaN, 1.6182817135715875],
+ ['tan', [10 * Math.LOG2E], 'tan(10*Math.LOG2E)', -3.3537128705376014, -3.353712870537601, NaN, -3.353712870537602],
+ ['tan', [17 * Math.SQRT2], 'tan(17*Math.SQRT2)', -1.9222955461799982, -1.922295546179998, NaN, NaN],
+ ['tan', [34 * Math.SQRT1_2], 'tan(34*Math.SQRT1_2)', -1.9222955461799982, -1.922295546179998, NaN, NaN],
+ ['tan', [10 * Math.LOG10E], 'tan(10*Math.LOG10E)', 2.5824856130712432, 2.5824856130712437, NaN, NaN],
+ ['tanh', [n], `tanh(${n})`, 0.12238344189440875, NaN, NaN, 0.12238344189440876],
+ ['tanh', [Math.PI], 'tanh(Math.PI)', 0.99627207622075, NaN, NaN, NaN],
+ ['pow', [n, -100], `pow(${n}, -100)`, 1.022089333584519e+91, 1.0220893335845176e+91, NaN, NaN],
+ ['pow', [Math.PI, -100], 'pow(Math.PI, -100)', 1.9275814160560204e-50, 1.9275814160560185e-50, NaN, 1.9275814160560206e-50],
+ ['pow', [Math.E, -100], 'pow(Math.E, -100)', 3.7200759760208555e-44, 3.720075976020851e-44, NaN, NaN],
+ ['pow', [Math.LN2, -100], 'pow(Math.LN2, -100)', 8269017203802394, 8269017203802410, NaN, NaN],
+ ['pow', [Math.LN10, -100], 'pow(Math.LN10, -100)', 6.003867926738829e-37, 6.003867926738811e-37, NaN, NaN],
+ ['pow', [Math.LOG2E, -100], 'pow(Math.LOG2E, -100)', 1.20933355845501e-16, 1.2093335584550061e-16, NaN, NaN],
+ ['pow', [Math.LOG10E, -100], 'pow(Math.LOG10E, -100)', 1.6655929347585958e+36, 1.665592934758592e+36, NaN, 1.6655929347585955e+36],
+ ['pow', [Math.SQRT1_2, -100], 'pow(Math.SQRT1_2, -100)', 1125899906842616.2, 1125899906842611.5, NaN, NaN],
+ ['pow', [Math.SQRT2, -100], 'pow(Math.SQRT2, -100)', 8.881784197001191e-16, 8.881784197001154e-16, NaN, NaN],
+ ['polyfill', [2e-3 ** -100], 'polyfill pow(2e-3, -100)', 7.888609052210102e+269, 7.888609052210126e+269, NaN, NaN],
+ ];
+ const data = {};
+ fns.forEach((fn) => {
+ data[fn[2]] = attempt(() => {
+ // @ts-ignore
+ const result = fn[0] != 'polyfill' ? Math[fn[0]](...fn[1]) : fn[1];
+ const chrome = result == fn[3];
+ const firefox = fn[4] ? result == fn[4] : false;
+ const torBrowser = fn[5] ? result == fn[5] : false;
+ const safari = fn[6] ? result == fn[6] : false;
+ return { result, chrome, firefox, torBrowser, safari };
+ });
+ });
+ logTestResult({ time: timer.stop(), test: 'math', passed: true });
+ return { data, lied };
+ }
+ catch (error) {
+ logTestResult({ test: 'math', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function mathsHTML(fp) {
+ if (!fp.maths) {
+ return `
+
+
Math
+
results: ${HTMLNote.Blocked}
+
+
+
`;
+ }
+ const { maths: { data, $hash, lied, }, } = fp;
+ const header = `
+
+
+ C - Chromium
+ F - Firefox
+ T - Tor Browser
+ S - Safari
+
`;
+ const results = Object.keys(data).map((key) => {
+ const value = data[key];
+ const { chrome, firefox, torBrowser, safari } = value;
+ return `
+ ${chrome ? 'C ' : '- '}${firefox ? 'F ' : '- '}${torBrowser ? 'T ' : '- '}${safari ? 'S ' : '- '} ${key}`;
+ });
+ return `
+
+
${performanceLogger.getLog().math}
+
Math ${hashSlice($hash)}
+
results: ${!data ? HTMLNote.Blocked :
+ modal('creep-maths', header + results.join(' '))}
+
+ `;
+ }
+
+ // inspired by
+ // - https://privacycheck.sec.lrz.de/active/fp_cpt/fp_can_play_type.html
+ // - https://arkenfox.github.io/TZP
+ const getMimeTypeShortList = () => [
+ 'audio/ogg; codecs="vorbis"',
+ 'audio/mpeg',
+ 'audio/mpegurl',
+ 'audio/wav; codecs="1"',
+ 'audio/x-m4a',
+ 'audio/aac',
+ 'video/ogg; codecs="theora"',
+ 'video/quicktime',
+ 'video/mp4; codecs="avc1.42E01E"',
+ 'video/webm; codecs="vp8"',
+ 'video/webm; codecs="vp9"',
+ 'video/x-matroska',
+ ].sort();
+ async function getMedia() {
+ const getMimeTypes = () => {
+ try {
+ const mimeTypes = getMimeTypeShortList();
+ const videoEl = document.createElement('video');
+ const audioEl = new Audio();
+ const isMediaRecorderSupported = 'MediaRecorder' in window;
+ const types = mimeTypes.reduce((acc, type) => {
+ const data = {
+ mimeType: type,
+ audioPlayType: audioEl.canPlayType(type),
+ videoPlayType: videoEl.canPlayType(type),
+ mediaSource: MediaSource.isTypeSupported(type),
+ mediaRecorder: isMediaRecorderSupported ? MediaRecorder.isTypeSupported(type) : false,
+ };
+ if (!data.audioPlayType && !data.videoPlayType && !data.mediaSource && !data.mediaRecorder) {
+ return acc;
+ }
+ // @ts-ignore
+ acc.push(data);
+ return acc;
+ }, []);
+ return types;
+ }
+ catch (error) {
+ return;
+ }
+ };
+ try {
+ const timer = createTimer();
+ timer.start();
+ const mimeTypes = getMimeTypes();
+ logTestResult({ time: timer.stop(), test: 'media', passed: true });
+ return { mimeTypes };
+ }
+ catch (error) {
+ logTestResult({ test: 'media', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function mediaHTML(fp) {
+ if (!fp.media) {
+ return `
+
+
Media
+
mimes (0): ${HTMLNote.BLOCKED}
+
+ `;
+ }
+ const { media: { mimeTypes, $hash, }, } = fp;
+ const header = `
+
+
+ audioPlayType
+ videoPlayType
+ mediaSource
+ mediaRecorder
+ P (Probably)
+ M (Maybe)
+ T (True)
+
+ `;
+ const invalidMimeTypes = !mimeTypes || !mimeTypes.length;
+ const mimes = invalidMimeTypes ? undefined : mimeTypes.map((type) => {
+ const { mimeType, audioPlayType, videoPlayType, mediaSource, mediaRecorder } = type;
+ return `
+ ${audioPlayType == 'probably' ? 'P ' : audioPlayType == 'maybe' ? 'M ' : '- '}${videoPlayType == 'probably' ? 'P ' : videoPlayType == 'maybe' ? 'M ' : '- '}${mediaSource ? 'T ' : '- '}${mediaRecorder ? 'T ' : '- '}: ${mimeType}
+ `;
+ });
+ const mimesListLen = getMimeTypeShortList().length;
+ return `
+
+
${performanceLogger.getLog().media}
+
Media ${hashSlice($hash)}
+
mimes (${count(mimeTypes)}/${mimesListLen}): ${invalidMimeTypes ? HTMLNote.BLOCKED :
+ modal('creep-media-mimeTypes', header + mimes.join(' '), hashMini(mimeTypes))}
+
+ `;
+ }
+
+ // special thanks to https://arh.antoinevastel.com for inspiration
+ async function getNavigator(workerScope) {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ let lied = (lieProps['Navigator.appVersion'] ||
+ lieProps['Navigator.deviceMemory'] ||
+ lieProps['Navigator.doNotTrack'] ||
+ lieProps['Navigator.hardwareConcurrency'] ||
+ lieProps['Navigator.language'] ||
+ lieProps['Navigator.languages'] ||
+ lieProps['Navigator.maxTouchPoints'] ||
+ lieProps['Navigator.oscpu'] ||
+ lieProps['Navigator.platform'] ||
+ lieProps['Navigator.userAgent'] ||
+ lieProps['Navigator.vendor'] ||
+ lieProps['Navigator.plugins'] ||
+ lieProps['Navigator.mimeTypes']) || false;
+ const credibleUserAgent = ('chrome' in window ? navigator.userAgent.includes(navigator.appVersion) : true);
+ const data = {
+ platform: attempt(() => {
+ const { platform } = navigator;
+ const systems = ['win', 'linux', 'mac', 'arm', 'pike', 'linux', 'iphone', 'ipad', 'ipod', 'android', 'x11'];
+ const trusted = typeof platform == 'string' && systems.filter((val) => platform.toLowerCase().includes(val))[0];
+ if (!trusted) {
+ sendToTrash(`platform`, `${platform} is unusual`);
+ }
+ // user agent os lie
+ if (USER_AGENT_OS !== PLATFORM_OS) {
+ lied = true;
+ documentLie(`Navigator.platform`, `${PLATFORM_OS} platform and ${USER_AGENT_OS} user agent do not match`);
+ }
+ if (platform != workerScope.platform) {
+ lied = true; // documented in the worker source
+ }
+ return platform;
+ }),
+ system: attempt(() => getOS(navigator.userAgent), 'userAgent system failed'),
+ userAgentParsed: await attempt(async () => {
+ const reportedUserAgent = caniuse(() => navigator.userAgent);
+ const reportedSystem = getOS(reportedUserAgent);
+ const isBrave = await braveBrowser();
+ const report = decryptUserAgent({
+ ua: reportedUserAgent,
+ os: reportedSystem,
+ isBrave,
+ });
+ return report;
+ }),
+ device: attempt(() => getUserAgentPlatform({ userAgent: navigator.userAgent }), 'userAgent device failed'),
+ userAgent: attempt(() => {
+ const { userAgent } = navigator;
+ if (!credibleUserAgent) {
+ sendToTrash('userAgent', `${userAgent} does not match appVersion`);
+ }
+ if (/\s{2,}|^\s|\s$/g.test(userAgent)) {
+ sendToTrash('userAgent', `extra spaces detected`);
+ }
+ const gibbers = gibberish(userAgent);
+ if (!!gibbers.length) {
+ sendToTrash(`userAgent is gibberish`, userAgent);
+ }
+ if (userAgent != workerScope.userAgent) {
+ lied = true; // documented in the worker source
+ }
+ return userAgent.trim().replace(/\s{2,}/, ' ');
+ }, 'userAgent failed'),
+ uaPostReduction: isUAPostReduction((navigator || {}).userAgent),
+ appVersion: attempt(() => {
+ const { appVersion } = navigator;
+ if (!credibleUserAgent) {
+ sendToTrash('appVersion', `${appVersion} does not match userAgent`);
+ }
+ if ('appVersion' in navigator && !appVersion) {
+ sendToTrash('appVersion', 'Living Standard property returned falsy value');
+ }
+ if (/\s{2,}|^\s|\s$/g.test(appVersion)) {
+ sendToTrash('appVersion', `extra spaces detected`);
+ }
+ return appVersion.trim().replace(/\s{2,}/, ' ');
+ }, 'appVersion failed'),
+ deviceMemory: attempt(() => {
+ if (!('deviceMemory' in navigator)) {
+ return undefined;
+ }
+ // @ts-ignore
+ const { deviceMemory } = navigator;
+ const trusted = {
+ '0.25': true,
+ '0.5': true,
+ '1': true,
+ '2': true,
+ '4': true,
+ '8': true,
+ '16': true,
+ '32': true,
+ };
+ if (!trusted[deviceMemory]) {
+ sendToTrash('deviceMemory', `${deviceMemory} is not a valid value [0.25, 0.5, 1, 2, 4, 8, 16, 32]`);
+ }
+ // @ts-expect-error memory is undefined if not supported
+ const memory = performance?.memory?.jsHeapSizeLimit || null;
+ const memoryInGigabytes = memory ? +(memory / 1073741824).toFixed(1) : 0;
+ if (memoryInGigabytes > deviceMemory) {
+ sendToTrash('deviceMemory', `available memory ${memoryInGigabytes}GB is greater than device memory ${deviceMemory}GB`);
+ }
+ if (deviceMemory !== workerScope.deviceMemory) {
+ lied = true; // documented in the worker source
+ }
+ return deviceMemory;
+ }, 'deviceMemory failed'),
+ doNotTrack: attempt(() => {
+ const { doNotTrack } = navigator;
+ const trusted = {
+ '1': !0,
+ 'true': !0,
+ 'yes': !0,
+ '0': !0,
+ 'false': !0,
+ 'no': !0,
+ 'unspecified': !0,
+ 'null': !0,
+ 'undefined': !0,
+ };
+ if (!trusted[doNotTrack]) {
+ sendToTrash('doNotTrack - unusual result', doNotTrack);
+ }
+ return doNotTrack;
+ }, 'doNotTrack failed'),
+ globalPrivacyControl: attempt(() => {
+ if (!('globalPrivacyControl' in navigator)) {
+ return undefined;
+ }
+ // @ts-ignore
+ const { globalPrivacyControl } = navigator;
+ const trusted = {
+ '1': !0,
+ 'true': !0,
+ 'yes': !0,
+ '0': !0,
+ 'false': !0,
+ 'no': !0,
+ 'unspecified': !0,
+ 'null': !0,
+ 'undefined': !0,
+ };
+ if (!trusted[globalPrivacyControl]) {
+ sendToTrash('globalPrivacyControl - unusual result', globalPrivacyControl);
+ }
+ return globalPrivacyControl;
+ }, 'globalPrivacyControl failed'),
+ hardwareConcurrency: attempt(() => {
+ if (!('hardwareConcurrency' in navigator)) {
+ return undefined;
+ }
+ const { hardwareConcurrency } = navigator;
+ if (hardwareConcurrency !== workerScope.hardwareConcurrency) {
+ lied = true; // documented in the worker source
+ }
+ return hardwareConcurrency;
+ }, 'hardwareConcurrency failed'),
+ language: attempt(() => {
+ const { language, languages } = navigator;
+ if (language && languages) {
+ // @ts-ignore
+ const lang = /^.{0,2}/g.exec(language)[0];
+ // @ts-ignore
+ const langs = /^.{0,2}/g.exec(languages[0])[0];
+ if (langs != lang) {
+ sendToTrash('language/languages', `${[language, languages].join(' ')} mismatch`);
+ }
+ return `${languages.join(', ')} (${language})`;
+ }
+ if (language != workerScope.language) {
+ lied = true;
+ documentLie(`Navigator.language`, `${language} does not match worker scope`);
+ }
+ if (languages !== workerScope.languages) {
+ lied = true;
+ documentLie(`Navigator.languages`, `${languages} does not match worker scope`);
+ }
+ return `${language} ${languages}`;
+ }, 'language(s) failed'),
+ maxTouchPoints: attempt(() => {
+ if (!('maxTouchPoints' in navigator)) {
+ return null;
+ }
+ return navigator.maxTouchPoints;
+ }, 'maxTouchPoints failed'),
+ vendor: attempt(() => navigator.vendor, 'vendor failed'),
+ mimeTypes: attempt(() => {
+ const { mimeTypes } = navigator;
+ return mimeTypes ? [...mimeTypes].map((m) => m.type) : [];
+ }, 'mimeTypes failed'),
+ // @ts-ignore
+ oscpu: attempt(() => navigator.oscpu, 'oscpu failed'),
+ plugins: attempt(() => {
+ // https://html.spec.whatwg.org/multipage/system-state.html#pdf-viewing-support
+ const { plugins } = navigator;
+ if (!(plugins instanceof PluginArray)) {
+ return;
+ }
+ const response = plugins ? [...plugins]
+ .map((p) => ({
+ name: p.name,
+ description: p.description,
+ filename: p.filename,
+ // @ts-ignore
+ version: p.version,
+ })) : [];
+ const { lies } = getPluginLies(plugins, navigator.mimeTypes);
+ if (lies.length) {
+ lied = true;
+ lies.forEach((lie) => {
+ return documentLie(`Navigator.plugins`, lie);
+ });
+ }
+ if (response.length) {
+ response.forEach((plugin) => {
+ const { name, description } = plugin;
+ const nameGibbers = gibberish(name);
+ const descriptionGibbers = gibberish(description);
+ if (nameGibbers.length) {
+ sendToTrash(`plugin name is gibberish`, name);
+ }
+ if (descriptionGibbers.length) {
+ sendToTrash(`plugin description is gibberish`, description);
+ }
+ return;
+ });
+ }
+ return response;
+ }, 'plugins failed'),
+ properties: attempt(() => {
+ const keys = Object.keys(Object.getPrototypeOf(navigator));
+ return keys;
+ }, 'navigator keys failed'),
+ };
+ const getUserAgentData = () => attempt(() => {
+ // @ts-ignore
+ if (!navigator.userAgentData ||
+ // @ts-ignore
+ !navigator.userAgentData.getHighEntropyValues) {
+ return;
+ }
+ // @ts-ignore
+ return navigator.userAgentData.getHighEntropyValues(['platform', 'platformVersion', 'architecture', 'bitness', 'model', 'uaFullVersion']).then((data) => {
+ // @ts-ignore
+ const { brands, mobile } = navigator.userAgentData || {};
+ const compressedBrands = (brands, captureVersion = false) => brands
+ .filter((obj) => !/Not/.test(obj.brand)).map((obj) => `${obj.brand}${captureVersion ? ` ${obj.version}` : ''}`);
+ const removeChromium = (brands) => (brands.length > 1 ? brands.filter((brand) => !/Chromium/.test(brand)) : brands);
+ // compress brands
+ if (!data.brands) {
+ data.brands = brands;
+ }
+ data.brandsVersion = compressedBrands(data.brands, true);
+ data.brands = compressedBrands(data.brands);
+ data.brandsVersion = removeChromium(data.brandsVersion);
+ data.brands = removeChromium(data.brands);
+ if (!data.mobile) {
+ data.mobile = mobile;
+ }
+ const dataSorted = Object.keys(data).sort().reduce((acc, key) => {
+ acc[key] = data[key];
+ return acc;
+ }, {});
+ return dataSorted;
+ });
+ }, 'userAgentData failed');
+ const getBluetoothAvailability = () => attempt(() => {
+ if (!('bluetooth' in navigator) ||
+ // @ts-ignore
+ !navigator.bluetooth ||
+ // @ts-ignore
+ !navigator.bluetooth.getAvailability) {
+ return undefined;
+ }
+ // @ts-ignore
+ return navigator.bluetooth.getAvailability();
+ }, 'bluetoothAvailability failed');
+ const getPermissions = () => attempt(() => {
+ const getPermissionState = (name) => navigator.permissions.query({ name })
+ .then((res) => ({ name, state: res.state }))
+ .catch((error) => ({ name, state: 'unknown' }));
+ // https://w3c.github.io/permissions/#permission-registry
+ const permissions = !('permissions' in navigator) ? undefined : Promise.all([
+ getPermissionState('accelerometer'),
+ getPermissionState('ambient-light-sensor'),
+ getPermissionState('background-fetch'),
+ getPermissionState('background-sync'),
+ getPermissionState('bluetooth'),
+ getPermissionState('camera'),
+ getPermissionState('clipboard'),
+ getPermissionState('device-info'),
+ getPermissionState('display-capture'),
+ getPermissionState('gamepad'),
+ getPermissionState('geolocation'),
+ getPermissionState('gyroscope'),
+ getPermissionState('magnetometer'),
+ getPermissionState('microphone'),
+ getPermissionState('midi'),
+ getPermissionState('nfc'),
+ getPermissionState('notifications'),
+ getPermissionState('persistent-storage'),
+ getPermissionState('push'),
+ getPermissionState('screen-wake-lock'),
+ getPermissionState('speaker'),
+ getPermissionState('speaker-selection'),
+ ]).then((permissions) => permissions.reduce((acc, perm) => {
+ const { state, name } = perm || {};
+ if (acc[state]) {
+ acc[state].push(name);
+ return acc;
+ }
+ acc[state] = [name];
+ return acc;
+ }, {})).catch((error) => console.error(error));
+ return permissions;
+ }, 'permissions failed');
+ const getWebGpu = () => attempt(() => {
+ if (!('gpu' in navigator)) {
+ return;
+ }
+ // @ts-expect-error if unsupported
+ return navigator.gpu.requestAdapter().then((adapter) => {
+ if (!adapter)
+ return;
+ const { limits = {}, features = [] } = adapter || {};
+ // @ts-expect-error if unsupported
+ const handleInfo = (info) => {
+ const { architecture, description, device, vendor } = info;
+ const adapterInfo = [vendor, architecture, description, device];
+ const featureValues = [...features.values()];
+ const limitsData = ((limits) => {
+ const data = {};
+ // eslint-disable-next-line guard-for-in
+ for (const prop in limits) {
+ data[prop] = limits[prop];
+ }
+ return data;
+ })(limits);
+ Analysis.webGpuAdapter = adapterInfo;
+ Analysis.webGpuFeatures = featureValues;
+ Analysis.webGpuLimits = hashMini(limitsData);
+ return {
+ adapterInfo,
+ limits: limitsData,
+ };
+ };
+ const { info } = adapter;
+ return info ? handleInfo(info) : adapter.requestAdapterInfo().then(handleInfo);
+ });
+ }, 'webgpu failed');
+ await queueEvent(timer);
+ return Promise.all([
+ getUserAgentData(),
+ getBluetoothAvailability(),
+ getPermissions(),
+ getWebGpu(),
+ ]).then(([userAgentData, bluetoothAvailability, permissions, webgpu,]) => {
+ logTestResult({ time: timer.stop(), test: 'navigator', passed: true });
+ return {
+ ...data,
+ userAgentData,
+ bluetoothAvailability,
+ permissions,
+ webgpu,
+ lied,
+ };
+ }).catch((error) => {
+ console.error(error);
+ logTestResult({ time: timer.stop(), test: 'navigator', passed: true });
+ return {
+ ...data,
+ lied,
+ };
+ });
+ }
+ catch (error) {
+ logTestResult({ test: 'navigator', passed: false });
+ captureError(error, 'Navigator failed or blocked by client');
+ return;
+ }
+ }
+ function navigatorHTML(fp) {
+ if (!fp.navigator) {
+ return `
+
+
Navigator
+
properties (0): ${HTMLNote.BLOCKED}
+
dnt: ${HTMLNote.BLOCKED}
+
gpc:${HTMLNote.BLOCKED}
+
lang: ${HTMLNote.BLOCKED}
+
mimeTypes (0): ${HTMLNote.BLOCKED}
+
permissions (0): ${HTMLNote.BLOCKED}
+
plugins (0): ${HTMLNote.BLOCKED}
+
vendor: ${HTMLNote.BLOCKED}
+
webgpu: ${HTMLNote.BLOCKED}
+
userAgentData:
+
${HTMLNote.BLOCKED}
+
+
+
device:
+
${HTMLNote.BLOCKED}
+
ua parsed: ${HTMLNote.BLOCKED}
+
userAgent:
+
${HTMLNote.BLOCKED}
+
appVersion:
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { navigator: { $hash, appVersion, deviceMemory, doNotTrack, globalPrivacyControl, hardwareConcurrency, language, maxTouchPoints, mimeTypes, oscpu, permissions, platform, plugins, properties, system, device, userAgent, uaPostReduction, userAgentData, userAgentParsed, vendor, bluetoothAvailability, webgpu, lied, }, } = fp;
+ const id = 'creep-navigator';
+ const blocked = {
+ ['null']: true,
+ ['undefined']: true,
+ ['']: true,
+ };
+ const permissionsKeys = Object.keys(permissions || {});
+ const permissionsGranted = (permissions && permissions.granted ? permissions.granted.length : 0);
+ return `
+ ${performanceLogger.getLog().navigator}
+
+
Navigator ${hashSlice($hash)}
+
properties (${count(properties)}): ${modal(`${id}-properties`, properties.join(', '), hashMini(properties))}
+
dnt: ${'' + doNotTrack}
+
gpc: ${'' + globalPrivacyControl == 'undefined' ? HTMLNote.UNSUPPORTED : '' + globalPrivacyControl}
+
lang: ${!blocked[language] ? language : HTMLNote.BLOCKED}
+
mimeTypes (${count(mimeTypes)}): ${!blocked['' + mimeTypes] ?
+ modal(`${id}-mimeTypes`, mimeTypes.join(' '), hashMini(mimeTypes)) :
+ HTMLNote.BLOCKED}
+
permissions (${'' + permissionsGranted}): ${!permissions || !permissionsKeys ? HTMLNote.UNSUPPORTED : modal('creep-permissions', permissionsKeys.map((key) => `
${key} : ${permissions[key].join(' ')}
`).join(''), hashMini(permissions))}
+
plugins (${count(plugins)}): ${!blocked['' + plugins] ?
+ modal(`${id}-plugins`, plugins.map((plugin) => plugin.name).join(' '), hashMini(plugins)) :
+ HTMLNote.BLOCKED}
+
vendor: ${!blocked[vendor] ? vendor : HTMLNote.BLOCKED}
+
webgpu: ${!webgpu ? HTMLNote.UNSUPPORTED :
+ modal(`${id}-webgpu`, ((webgpu) => {
+ const { adapterInfo, limits } = webgpu;
+ return `
+
+ Adapter ${adapterInfo.filter((x) => x).join(' ')}
+
+
+ Limits ${Object.keys(limits).map((x) => `${x}: ${limits[x]}`).join(' ')}
+
+ `;
+ })(webgpu), hashMini(webgpu))}
+
userAgentData:
+
+
+ ${((userAgentData) => {
+ const { architecture, bitness, brandsVersion, uaFullVersion, mobile, model, platformVersion, platform, } = userAgentData || {};
+ // @ts-ignore
+ const windowsRelease = computeWindowsRelease({ platform, platformVersion });
+ return !userAgentData ? HTMLNote.UNSUPPORTED : `
+ ${(brandsVersion || []).join(',')}${uaFullVersion ? ` (${uaFullVersion})` : ''}
+ ${windowsRelease || `${platform} ${platformVersion}`} ${architecture ? `${architecture}${bitness ? `_${bitness}` : ''}` : ''}
+ ${model ? ` ${model}` : ''}
+ ${mobile ? ' mobile' : ''}
+ `;
+ })(userAgentData)}
+
+
+
+
+
device:
+
+ ${oscpu ? oscpu : ''}
+ ${`${oscpu ? ' ' : ''}${system}${platform ? ` (${platform})` : ''}`}
+ ${device ? ` ${device}` : HTMLNote.BLOCKED}${hardwareConcurrency && deviceMemory ? ` cores: ${hardwareConcurrency}, ram: ${deviceMemory}` :
+ hardwareConcurrency && !deviceMemory ? ` cores: ${hardwareConcurrency}` :
+ !hardwareConcurrency && deviceMemory ? ` ram: ${deviceMemory}` : ''}${typeof maxTouchPoints != 'undefined' ? `, touch: ${'' + maxTouchPoints}` : ''}${bluetoothAvailability ? `, bluetooth` : ''}
+
+
ua parsed: ${userAgentParsed || HTMLNote.BLOCKED}
+
userAgent:${!uaPostReduction ? '' : `ua reduction `}
+
+
${userAgent || HTMLNote.BLOCKED}
+
+
appVersion:
+
+
${appVersion || HTMLNote.BLOCKED}
+
+
+ `;
+ }
+
+ async function getResistance() {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ const data = {
+ privacy: undefined,
+ security: undefined,
+ mode: undefined,
+ extension: undefined,
+ engine: (IS_BLINK ? 'Blink' :
+ IS_GECKO ? 'Gecko' :
+ ''),
+ };
+ // Firefox/Tor Browser
+ const regex = (n) => new RegExp(`${n}+$`);
+ const delay = (ms, baseNumber, baseDate) => new Promise((resolve) => setTimeout(() => {
+ const date = baseDate ? baseDate : +new Date();
+ // @ts-ignore
+ const value = regex(baseNumber).test(date) ? regex(baseNumber).exec(date)[0] : date;
+ return resolve(value);
+ }, ms));
+ const getTimerPrecision = async () => {
+ const baseDate = +new Date();
+ const baseNumber = +('' + baseDate).slice(-1);
+ const a = await delay(0, baseNumber, baseDate);
+ const b = await delay(1, baseNumber);
+ const c = await delay(2, baseNumber);
+ const d = await delay(3, baseNumber);
+ const e = await delay(4, baseNumber);
+ const f = await delay(5, baseNumber);
+ const g = await delay(6, baseNumber);
+ const h = await delay(7, baseNumber);
+ const i = await delay(8, baseNumber);
+ const j = await delay(9, baseNumber);
+ const lastCharA = ('' + a).slice(-1);
+ const lastCharB = ('' + b).slice(-1);
+ const lastCharC = ('' + c).slice(-1);
+ const lastCharD = ('' + d).slice(-1);
+ const lastCharE = ('' + e).slice(-1);
+ const lastCharF = ('' + f).slice(-1);
+ const lastCharG = ('' + g).slice(-1);
+ const lastCharH = ('' + h).slice(-1);
+ const lastCharI = ('' + i).slice(-1);
+ const lastCharJ = ('' + j).slice(-1);
+ const protection = (lastCharA == lastCharB &&
+ lastCharA == lastCharC &&
+ lastCharA == lastCharD &&
+ lastCharA == lastCharE &&
+ lastCharA == lastCharF &&
+ lastCharA == lastCharG &&
+ lastCharA == lastCharH &&
+ lastCharA == lastCharI &&
+ lastCharA == lastCharJ);
+ const baseLen = ('' + a).length;
+ const collection = [a, b, c, d, e, f, g, h, i, j];
+ return {
+ protection,
+ delays: collection.map((n) => ('' + n).length > baseLen ? ('' + n).slice(-baseLen) : n),
+ precision: protection ? Math.min(...collection.map((val) => ('' + val).length)) : undefined,
+ precisionValue: protection ? lastCharA : undefined,
+ };
+ };
+ const [isBrave, timerPrecision,] = await Promise.all([
+ braveBrowser(),
+ IS_BLINK ? undefined : getTimerPrecision(),
+ ]);
+ if (isBrave) {
+ const braveMode = getBraveMode();
+ data.privacy = 'Brave';
+ // @ts-ignore
+ data.security = {
+ 'FileSystemWritableFileStream': 'FileSystemWritableFileStream' in window,
+ 'Serial': 'Serial' in window,
+ 'ReportingObserver': 'ReportingObserver' in window,
+ };
+ data.mode = (braveMode.allow ? 'allow' :
+ braveMode.standard ? 'standard' :
+ braveMode.strict ? 'strict' :
+ '');
+ }
+ const { protection } = timerPrecision || {};
+ if (IS_GECKO && protection) {
+ const features = {
+ 'OfflineAudioContext': 'OfflineAudioContext' in window, // dom.webaudio.enabled
+ 'WebGL2RenderingContext': 'WebGL2RenderingContext' in window, // webgl.enable-webgl2
+ 'WebAssembly': 'WebAssembly' in window, // javascript.options.wasm
+ 'maxTouchPoints': 'maxTouchPoints' in navigator,
+ 'RTCRtpTransceiver': 'RTCRtpTransceiver' in window,
+ 'MediaDevices': 'MediaDevices' in window,
+ 'Credential': 'Credential' in window,
+ };
+ const featureKeys = Object.keys(features);
+ const targetSet = new Set([
+ 'RTCRtpTransceiver',
+ 'MediaDevices',
+ 'Credential',
+ ]);
+ const torBrowser = featureKeys.filter((key) => targetSet.has(key) && !features[key]).length == targetSet.size;
+ const safer = !features.WebAssembly;
+ data.privacy = torBrowser ? 'Tor Browser' : 'Firefox';
+ // @ts-ignore
+ data.security = {
+ 'reduceTimerPrecision': true,
+ ...features,
+ };
+ data.mode = (!torBrowser ? 'resistFingerprinting' :
+ safer ? 'safer' :
+ 'standard');
+ }
+ // extension
+ // - this technique gets a small sample of known lie patterns
+ // - patterns vary based on extensions settings, version, browser
+ const prototypeLiesLen = Object.keys(prototypeLies).length;
+ // patterns based on settings
+ const disabled = 'c767712b';
+ const pattern = {
+ noscript: {
+ contentDocumentHash: ['0b637a33', '37e2f32e', '318390d1'],
+ contentWindowHash: ['0b637a33', '37e2f32e', '318390d1'],
+ getContextHash: ['0b637a33', '081d6d1b', disabled],
+ },
+ trace: {
+ contentDocumentHash: ['ca9d9c2f'],
+ contentWindowHash: ['ca9d9c2f'],
+ createElementHash: ['77dea834'],
+ getElementByIdHash: ['77dea834'],
+ getImageDataHash: ['77dea834'],
+ toBlobHash: ['77dea834', disabled],
+ toDataURLHash: ['77dea834', disabled],
+ },
+ cydec: {
+ // [FF, FF Anti OFF, Chrome, Chrome Anti Off, no iframe Chrome, no iframe Chrome Anti Off]
+ contentDocumentHash: ['945b0c78', '15771efa', '403a1a21', '55e9b959'],
+ contentWindowHash: ['945b0c78', '15771efa', '403a1a21', '55e9b959'],
+ createElementHash: ['3dd86d6f', 'cc7cb598', '4237b44c', '1466aaf0', '0cb0c682', '73c662d9', '72b1ee2b', 'ae3d02c9'],
+ getElementByIdHash: ['3dd86d6f', 'cc7cb598', '4237b44c', '1466aaf0', '0cb0c682', '73c662d9', '72b1ee2b', 'ae3d02c9'],
+ getImageDataHash: ['044f14c2', 'db60d7f9', '15771efa', 'db60d7f9', '55e9b959'],
+ toBlobHash: ['044f14c2', '15771efa', 'afec348d', '55e9b959', '0dbbf456'],
+ toDataURLHash: ['ecb498d9', '15771efa', '6b838fb6', 'd19104ec', '6985d315', '55e9b959', 'fe88259f'],
+ },
+ canvasblocker: {
+ contentDocumentHash: ['98ec858e', 'dbbaf31f'],
+ contentWindowHash: ['98ec858e', 'dbbaf31f'],
+ appendHash: ['98ec858e', 'dbbaf31f'],
+ getImageDataHash: ['98ec858e', 'a2971888', 'dbbaf31f', disabled],
+ toBlobHash: ['9f1c3dfe', 'a2971888', 'dbbaf31f', disabled],
+ toDataURLHash: ['98ec858e', 'a2971888', 'dbbaf31f', disabled],
+ },
+ chameleon: {
+ appendHash: ['77dea834'],
+ insertAdjacentElementHash: ['77dea834'],
+ insertAdjacentHTMLHash: ['77dea834'],
+ insertAdjacentTextHash: ['77dea834'],
+ prependHash: ['77dea834'],
+ replaceWithHash: ['77dea834'],
+ appendChildHash: ['77dea834'],
+ insertBeforeHash: ['77dea834'],
+ replaceChildHash: ['77dea834'],
+ },
+ duckduckgo: {
+ toDataURLHash: ['fd00bf5d', '8ee7df22', disabled],
+ toBlobHash: ['fd00bf5d', '8ee7df22', disabled],
+ getImageDataHash: ['fd00bf5d', '8ee7df22', disabled],
+ getByteFrequencyDataHash: ['fd00bf5d', '8ee7df22', disabled],
+ getByteTimeDomainDataHash: ['fd00bf5d', '8ee7df22', disabled],
+ getFloatFrequencyDataHash: ['fd00bf5d', '8ee7df22', disabled],
+ getFloatTimeDomainDataHash: ['fd00bf5d', '8ee7df22', disabled],
+ copyFromChannelHash: ['fd00bf5d', '8ee7df22', disabled],
+ getChannelDataHash: ['fd00bf5d', '8ee7df22', disabled],
+ hardwareConcurrencyHash: ['dfd41ab4'],
+ availHeightHash: ['dfd41ab4'],
+ availLeftHash: ['dfd41ab4'],
+ availTopHash: ['dfd41ab4'],
+ availWidthHash: ['dfd41ab4'],
+ colorDepthHash: ['dfd41ab4'],
+ pixelDepthHash: ['dfd41ab4'],
+ },
+ // mode: Learn to block new trackers from your browsing
+ privacybadger: {
+ getImageDataHash: ['0cb0c682'],
+ toDataURLHash: ['0cb0c682'],
+ },
+ privacypossum: {
+ hardwareConcurrencyHash: ['452924d5'],
+ availWidthHash: ['452924d5'],
+ colorDepthHash: ['452924d5'],
+ },
+ jshelter: {
+ contentDocumentHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ contentWindowHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ appendHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ insertAdjacentElementHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ insertAdjacentHTMLHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ prependHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ replaceWithHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ appendChildHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ insertBeforeHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ replaceChildHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'],
+ hardwareConcurrencyHash: ['dfd41ab4'],
+ },
+ puppeteerExtra: {
+ contentDocumentHash: ['55e9b959'],
+ contentWindowHash: [
+ '55e9b959',
+ '50a281b5', // @2.10.0
+ ],
+ createElementHash: ['55e9b959'],
+ getElementByIdHash: ['55e9b959'],
+ appendHash: ['55e9b959'],
+ insertAdjacentElementHash: ['55e9b959'],
+ insertAdjacentHTMLHash: ['55e9b959'],
+ insertAdjacentTextHash: ['55e9b959'],
+ prependHash: ['55e9b959'],
+ replaceWithHash: ['55e9b959'],
+ appendChildHash: ['55e9b959'],
+ insertBeforeHash: ['55e9b959'],
+ replaceChildHash: ['55e9b959'],
+ getContextHash: ['55e9b959', disabled],
+ toDataURLHash: ['55e9b959', disabled],
+ toBlobHash: ['55e9b959', disabled],
+ getImageDataHash: ['55e9b959'],
+ hardwareConcurrencyHash: ['efbd4cf9', 'a63491fb', 'b011fd1c', '194ecf17', '55e9b959'],
+ },
+ fakeBrowser: {
+ appendChildHash: ['8dfec2ec', 'f43e6134'],
+ getContextHash: ['83b825ab', 'a63491fb'],
+ toDataURLHash: ['83b825ab', 'a63491fb'],
+ toBlobHash: ['83b825ab', 'a63491fb'],
+ getImageDataHash: ['83b825ab', 'a63491fb'],
+ hardwareConcurrencyHash: ['83b825ab', 'a63491fb'],
+ availHeightHash: ['83b825ab', 'a63491fb'],
+ availLeftHash: ['83b825ab', 'a63491fb'],
+ availTopHash: ['83b825ab', 'a63491fb'],
+ availWidthHash: ['83b825ab', 'a63491fb'],
+ colorDepthHash: ['83b825ab', 'a63491fb'],
+ pixelDepthHash: ['83b825ab', 'a63491fb'],
+ },
+ };
+ /*
+ Random User-Agent
+ User Agent Switcher and Manager
+ ScriptSafe
+ Windscribe
+ */
+ await queueEvent(timer);
+ const hash = {
+ // iframes
+ contentDocumentHash: hashMini(prototypeLies['HTMLIFrameElement.contentDocument']),
+ contentWindowHash: hashMini(prototypeLies['HTMLIFrameElement.contentWindow']),
+ createElementHash: hashMini(prototypeLies['Document.createElement']),
+ getElementByIdHash: hashMini(prototypeLies['Document.getElementById']),
+ appendHash: hashMini(prototypeLies['Element.append']),
+ insertAdjacentElementHash: hashMini(prototypeLies['Element.insertAdjacentElement']),
+ insertAdjacentHTMLHash: hashMini(prototypeLies['Element.insertAdjacentHTML']),
+ insertAdjacentTextHash: hashMini(prototypeLies['Element.insertAdjacentText']),
+ prependHash: hashMini(prototypeLies['Element.prepend']),
+ replaceWithHash: hashMini(prototypeLies['Element.replaceWith']),
+ appendChildHash: hashMini(prototypeLies['Node.appendChild']),
+ insertBeforeHash: hashMini(prototypeLies['Node.insertBefore']),
+ replaceChildHash: hashMini(prototypeLies['Node.replaceChild']),
+ // canvas
+ getContextHash: hashMini(prototypeLies['HTMLCanvasElement.getContext']),
+ toDataURLHash: hashMini(prototypeLies['HTMLCanvasElement.toDataURL']),
+ toBlobHash: hashMini(prototypeLies['HTMLCanvasElement.toBlob']),
+ getImageDataHash: hashMini(prototypeLies['CanvasRenderingContext2D.getImageData']),
+ // Audio
+ getByteFrequencyDataHash: hashMini(prototypeLies['AnalyserNode.getByteFrequencyData']),
+ getByteTimeDomainDataHash: hashMini(prototypeLies['AnalyserNode.getByteTimeDomainData']),
+ getFloatFrequencyDataHash: hashMini(prototypeLies['AnalyserNode.getFloatFrequencyData']),
+ getFloatTimeDomainDataHash: hashMini(prototypeLies['AnalyserNode.getFloatTimeDomainData']),
+ copyFromChannelHash: hashMini(prototypeLies['AudioBuffer.copyFromChannel']),
+ getChannelDataHash: hashMini(prototypeLies['AudioBuffer.getChannelData']),
+ // Hardware
+ hardwareConcurrencyHash: hashMini(prototypeLies['Navigator.hardwareConcurrency']),
+ // Screen
+ availHeightHash: hashMini(prototypeLies['Screen.availHeight']),
+ availLeftHash: hashMini(prototypeLies['Screen.availLeft']),
+ availTopHash: hashMini(prototypeLies['Screen.availTop']),
+ availWidthHash: hashMini(prototypeLies['Screen.availWidth']),
+ colorDepthHash: hashMini(prototypeLies['Screen.colorDepth']),
+ pixelDepthHash: hashMini(prototypeLies['Screen.pixelDepth']),
+ };
+ data.extensionHashPattern = Object.keys(hash).reduce((acc, key) => {
+ const val = hash[key];
+ if (val == disabled) {
+ return acc;
+ }
+ acc[key.replace('Hash', '')] = val;
+ return acc;
+ }, {});
+ const getExtension = ({ pattern, hash, prototypeLiesLen }) => {
+ const { noscript, trace, cydec, canvasblocker, chameleon, duckduckgo, privacybadger, privacypossum, jshelter, puppeteerExtra, fakeBrowser, } = pattern;
+ const disabled = 'c767712b';
+ if (prototypeLiesLen) {
+ if (prototypeLiesLen >= 7 &&
+ trace.contentDocumentHash.includes(hash.contentDocumentHash) &&
+ trace.contentWindowHash.includes(hash.contentWindowHash) &&
+ trace.createElementHash.includes(hash.createElementHash) &&
+ trace.getElementByIdHash.includes(hash.getElementByIdHash) &&
+ trace.toDataURLHash.includes(hash.toDataURLHash) &&
+ trace.toBlobHash.includes(hash.toBlobHash) &&
+ trace.getImageDataHash.includes(hash.getImageDataHash)) {
+ return 'Trace';
+ }
+ if (prototypeLiesLen >= 7 &&
+ cydec.contentDocumentHash.includes(hash.contentDocumentHash) &&
+ cydec.contentWindowHash.includes(hash.contentWindowHash) &&
+ cydec.createElementHash.includes(hash.createElementHash) &&
+ cydec.getElementByIdHash.includes(hash.getElementByIdHash) &&
+ cydec.toDataURLHash.includes(hash.toDataURLHash) &&
+ cydec.toBlobHash.includes(hash.toBlobHash) &&
+ cydec.getImageDataHash.includes(hash.getImageDataHash)) {
+ return 'CyDec';
+ }
+ if (prototypeLiesLen >= 6 &&
+ canvasblocker.contentDocumentHash.includes(hash.contentDocumentHash) &&
+ canvasblocker.contentWindowHash.includes(hash.contentWindowHash) &&
+ canvasblocker.appendHash.includes(hash.appendHash) &&
+ canvasblocker.toDataURLHash.includes(hash.toDataURLHash) &&
+ canvasblocker.toBlobHash.includes(hash.toBlobHash) &&
+ canvasblocker.getImageDataHash.includes(hash.getImageDataHash)) {
+ return 'CanvasBlocker';
+ }
+ if (prototypeLiesLen >= 9 &&
+ chameleon.appendHash.includes(hash.appendHash) &&
+ chameleon.insertAdjacentElementHash.includes(hash.insertAdjacentElementHash) &&
+ chameleon.insertAdjacentHTMLHash.includes(hash.insertAdjacentHTMLHash) &&
+ chameleon.insertAdjacentTextHash.includes(hash.insertAdjacentTextHash) &&
+ chameleon.prependHash.includes(hash.prependHash) &&
+ chameleon.replaceWithHash.includes(hash.replaceWithHash) &&
+ chameleon.appendChildHash.includes(hash.appendChildHash) &&
+ chameleon.insertBeforeHash.includes(hash.insertBeforeHash) &&
+ chameleon.replaceChildHash.includes(hash.replaceChildHash)) {
+ return 'Chameleon';
+ }
+ if (prototypeLiesLen >= 7 &&
+ duckduckgo.toDataURLHash.includes(hash.toDataURLHash) &&
+ duckduckgo.toBlobHash.includes(hash.toBlobHash) &&
+ duckduckgo.getImageDataHash.includes(hash.getImageDataHash) &&
+ duckduckgo.getByteFrequencyDataHash.includes(hash.getByteFrequencyDataHash) &&
+ duckduckgo.getByteTimeDomainDataHash.includes(hash.getByteTimeDomainDataHash) &&
+ duckduckgo.getFloatFrequencyDataHash.includes(hash.getFloatFrequencyDataHash) &&
+ duckduckgo.getFloatTimeDomainDataHash.includes(hash.getFloatTimeDomainDataHash) &&
+ duckduckgo.copyFromChannelHash.includes(hash.copyFromChannelHash) &&
+ duckduckgo.getChannelDataHash.includes(hash.getChannelDataHash) &&
+ duckduckgo.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash) &&
+ duckduckgo.availHeightHash.includes(hash.availHeightHash) &&
+ duckduckgo.availLeftHash.includes(hash.availLeftHash) &&
+ duckduckgo.availTopHash.includes(hash.availTopHash) &&
+ duckduckgo.availWidthHash.includes(hash.availWidthHash) &&
+ duckduckgo.colorDepthHash.includes(hash.colorDepthHash) &&
+ duckduckgo.pixelDepthHash.includes(hash.pixelDepthHash)) {
+ return 'DuckDuckGo';
+ }
+ if (prototypeLiesLen >= 2 &&
+ privacybadger.getImageDataHash.includes(hash.getImageDataHash) &&
+ privacybadger.toDataURLHash.includes(hash.toDataURLHash)) {
+ return 'Privacy Badger';
+ }
+ if (prototypeLiesLen >= 3 &&
+ privacypossum.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash) &&
+ privacypossum.availWidthHash.includes(hash.availWidthHash) &&
+ privacypossum.colorDepthHash.includes(hash.colorDepthHash)) {
+ return 'Privacy Possum';
+ }
+ if (prototypeLiesLen >= 2 &&
+ noscript.contentDocumentHash.includes(hash.contentDocumentHash) &&
+ noscript.contentWindowHash.includes(hash.contentDocumentHash) &&
+ noscript.getContextHash.includes(hash.getContextHash) &&
+ // distinguish NoScript from JShelter
+ hash.hardwareConcurrencyHash == disabled) {
+ return 'NoScript';
+ }
+ if (prototypeLiesLen >= 14 &&
+ jshelter.contentDocumentHash.includes(hash.contentDocumentHash) &&
+ jshelter.contentWindowHash.includes(hash.contentDocumentHash) &&
+ jshelter.appendHash.includes(hash.appendHash) &&
+ jshelter.insertAdjacentElementHash.includes(hash.insertAdjacentElementHash) &&
+ jshelter.insertAdjacentHTMLHash.includes(hash.insertAdjacentHTMLHash) &&
+ jshelter.prependHash.includes(hash.prependHash) &&
+ jshelter.replaceWithHash.includes(hash.replaceWithHash) &&
+ jshelter.appendChildHash.includes(hash.appendChildHash) &&
+ jshelter.insertBeforeHash.includes(hash.insertBeforeHash) &&
+ jshelter.replaceChildHash.includes(hash.replaceChildHash) &&
+ jshelter.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash)) {
+ return 'JShelter';
+ }
+ if (prototypeLiesLen >= 13 &&
+ puppeteerExtra.contentDocumentHash.includes(hash.contentDocumentHash) &&
+ puppeteerExtra.contentWindowHash.includes(hash.contentWindowHash) &&
+ puppeteerExtra.createElementHash.includes(hash.createElementHash) &&
+ puppeteerExtra.getElementByIdHash.includes(hash.getElementByIdHash) &&
+ puppeteerExtra.appendHash.includes(hash.appendHash) &&
+ puppeteerExtra.insertAdjacentElementHash.includes(hash.insertAdjacentElementHash) &&
+ puppeteerExtra.insertAdjacentHTMLHash.includes(hash.insertAdjacentHTMLHash) &&
+ puppeteerExtra.insertAdjacentTextHash.includes(hash.insertAdjacentTextHash) &&
+ puppeteerExtra.prependHash.includes(hash.prependHash) &&
+ puppeteerExtra.replaceWithHash.includes(hash.replaceWithHash) &&
+ puppeteerExtra.appendChildHash.includes(hash.appendChildHash) &&
+ puppeteerExtra.insertBeforeHash.includes(hash.insertBeforeHash) &&
+ puppeteerExtra.contentDocumentHash.includes(hash.contentDocumentHash) &&
+ puppeteerExtra.replaceChildHash.includes(hash.replaceChildHash) &&
+ puppeteerExtra.getContextHash.includes(hash.getContextHash) &&
+ puppeteerExtra.toDataURLHash.includes(hash.toDataURLHash) &&
+ puppeteerExtra.toBlobHash.includes(hash.toBlobHash) &&
+ puppeteerExtra.getImageDataHash.includes(hash.getImageDataHash) &&
+ puppeteerExtra.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash)) {
+ return 'puppeteer-extra';
+ }
+ if (prototypeLiesLen >= 12 &&
+ fakeBrowser.appendChildHash.includes(hash.appendChildHash) &&
+ fakeBrowser.getContextHash.includes(hash.getContextHash) &&
+ fakeBrowser.toDataURLHash.includes(hash.toDataURLHash) &&
+ fakeBrowser.toBlobHash.includes(hash.toBlobHash) &&
+ fakeBrowser.getImageDataHash.includes(hash.getImageDataHash) &&
+ fakeBrowser.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash) &&
+ fakeBrowser.availHeightHash.includes(hash.availHeightHash) &&
+ fakeBrowser.availLeftHash.includes(hash.availLeftHash) &&
+ fakeBrowser.availTopHash.includes(hash.availTopHash) &&
+ fakeBrowser.availWidthHash.includes(hash.availWidthHash) &&
+ fakeBrowser.colorDepthHash.includes(hash.colorDepthHash) &&
+ fakeBrowser.pixelDepthHash.includes(hash.pixelDepthHash)) {
+ return 'FakeBrowser';
+ }
+ return;
+ }
+ return;
+ };
+ // @ts-ignore
+ data.extension = getExtension({ pattern, hash, prototypeLiesLen });
+ logTestResult({ time: timer.stop(), test: 'resistance', passed: true });
+ return data;
+ }
+ catch (error) {
+ logTestResult({ test: 'resistance', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function resistanceHTML(fp) {
+ if (!fp.resistance) {
+ return `
+
+
Resistance
+
privacy: ${HTMLNote.BLOCKED}
+
security: ${HTMLNote.BLOCKED}
+
mode: ${HTMLNote.BLOCKED}
+
extension: ${HTMLNote.BLOCKED}
+
`;
+ }
+ const { resistance: data, } = fp;
+ const { $hash, privacy, security, mode, extension, extensionHashPattern, engine, } = data || {};
+ const securitySettings = !security || Object.keys(security).reduce((acc, curr) => {
+ if (security[curr]) {
+ acc[curr] = 'enabled';
+ return acc;
+ }
+ acc[curr] = 'disabled';
+ return acc;
+ }, {});
+ const browserIcon = (/brave/i.test(privacy) ? ' ' :
+ /tor/i.test(privacy) ? ' ' :
+ /firefox/i.test(privacy) ? ' ' :
+ '');
+ const extensionIcon = (/blink/i.test(engine) ? ' ' :
+ /gecko/i.test(engine) ? ' ' :
+ '');
+ return `
+
+
${performanceLogger.getLog().resistance}
+
Resistance ${hashSlice($hash)}
+
privacy: ${privacy ? `${browserIcon}${privacy}` : HTMLNote.UNKNOWN}
+
security: ${!security ? HTMLNote.UNKNOWN :
+ modal('creep-resistance', 'Security ' +
+ Object.keys(securitySettings).map((key) => `${key}: ${'' + securitySettings[key]}`).join(' '), hashMini(security))}
+
mode: ${mode || HTMLNote.UNKNOWN}
+
extension: ${!Object.keys(extensionHashPattern || {}).length ? HTMLNote.UNKNOWN :
+ modal('creep-extension', 'Pattern ' +
+ Object.keys(extensionHashPattern).map((key) => `${key}: ${'' + extensionHashPattern[key]}`).join(' '), (extension ? `${extensionIcon}${extension}` : hashMini(extensionHashPattern)))}
+
+ `;
+ }
+
+ function hasTouch() {
+ try {
+ return 'ontouchstart' in window && !!document.createEvent('TouchEvent');
+ }
+ catch (err) {
+ return false;
+ }
+ }
+ async function getScreen(log = true) {
+ try {
+ const timer = createTimer();
+ timer.start();
+ let lied = (lieProps['Screen.width'] ||
+ lieProps['Screen.height'] ||
+ lieProps['Screen.availWidth'] ||
+ lieProps['Screen.availHeight'] ||
+ lieProps['Screen.colorDepth'] ||
+ lieProps['Screen.pixelDepth']) || false;
+ const s = (window.screen || {});
+ const { width, height, availWidth, availHeight, colorDepth, pixelDepth, } = s;
+ const dpr = window.devicePixelRatio || 0;
+ const firefoxWithHighDPR = IS_GECKO && (dpr != 1);
+ if (!firefoxWithHighDPR) {
+ // firefox with high dpr requires floating point precision dimensions
+ const matchMediaLie = !matchMedia(`(device-width: ${width}px) and (device-height: ${height}px)`).matches;
+ if (matchMediaLie) {
+ lied = true;
+ documentLie('Screen', 'failed matchMedia');
+ }
+ }
+ const hasLiedDPR = !matchMedia(`(resolution: ${dpr}dppx)`).matches;
+ if (!IS_WEBKIT && hasLiedDPR) {
+ lied = true;
+ documentLie('Window.devicePixelRatio', 'lied dpr');
+ }
+ const noTaskbar = !(width - availWidth || height - availHeight);
+ if (width > 800 && noTaskbar) {
+ LowerEntropy.SCREEN = true;
+ }
+ const data = {
+ width,
+ height,
+ availWidth,
+ availHeight,
+ colorDepth,
+ pixelDepth,
+ touch: hasTouch(),
+ lied,
+ };
+ log && logTestResult({ time: timer.stop(), test: 'screen', passed: true });
+ return data;
+ }
+ catch (error) {
+ log && logTestResult({ test: 'screen', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function screenHTML(fp) {
+ if (!fp.screen) {
+ return `
+
+
Screen
+
...screen: ${HTMLNote.BLOCKED}
+
....avail: ${HTMLNote.BLOCKED}
+
touch: ${HTMLNote.BLOCKED}
+
depth: ${HTMLNote.BLOCKED}
+
viewport: ${HTMLNote.BLOCKED}
+
+
`;
+ }
+ const { screen: data, } = fp;
+ const { $hash } = data || {};
+ const perf = performanceLogger.getLog().screen;
+ const paintScreen = (event) => {
+ const el = document.getElementById('creep-resize');
+ if (!el) {
+ return;
+ }
+ removeEventListener('resize', paintScreen);
+ return getScreen(false).then((data) => {
+ requestAnimationFrame(() => patch(el, html `${resizeHTML(({ data, $hash, perf, paintScreen }))}`));
+ });
+ };
+ const resizeHTML = ({ data, $hash, perf, paintScreen }) => {
+ const { width, height, availWidth, availHeight, colorDepth, pixelDepth, touch, lied, } = data;
+ addEventListener('resize', paintScreen);
+ const s = (window.screen || {});
+ const { orientation } = s;
+ const { type: orientationType } = orientation || {};
+ const dpr = window.devicePixelRatio || undefined;
+ const { width: vVWidth, height: vVHeight } = (window.visualViewport || {});
+ const mediaOrientation = !window.matchMedia ? undefined : (matchMedia('(orientation: landscape)').matches ? 'landscape' :
+ matchMedia('(orientation: portrait)').matches ? 'portrait' : undefined);
+ const displayMode = !window.matchMedia ? undefined : (matchMedia('(display-mode: fullscreen)').matches ? 'fullscreen' :
+ matchMedia('(display-mode: standalone)').matches ? 'standalone' :
+ matchMedia('(display-mode: minimal-ui)').matches ? 'minimal-ui' :
+ matchMedia('(display-mode: browser)').matches ? 'browser' : undefined);
+ const getDeviceDimensions = (width, height, diameter = 180) => {
+ const aspectRatio = width / height;
+ const isPortrait = height > width;
+ const deviceWidth = isPortrait ? diameter * aspectRatio : diameter;
+ const deviceHeight = isPortrait ? diameter : diameter / aspectRatio;
+ return { deviceWidth, deviceHeight };
+ };
+ // const { deviceWidth, deviceHeight } = getDeviceDimensions(width, height)
+ const { deviceWidth: deviceInnerWidth, deviceHeight: deviceInnerHeight } = getDeviceDimensions(innerWidth, innerHeight);
+ const toFix = (n, nFix) => {
+ const d = +(1 + [...Array(nFix)].map((x) => 0).join(''));
+ return Math.round(n * d) / d;
+ };
+ const viewportTitle = `Window.outerWidth\nWindow.outerHeight\nWindow.innerWidth\nWindow.innerHeight\nVisualViewport.width\nVisualViewport.height\nWindow.matchMedia()\nScreenOrientation.type\nWindow.devicePixelRatio`;
+ return `
+
+
${perf}
+
Screen ${hashSlice($hash)}
+
...screen: ${width} x ${height}
+
....avail: ${availWidth} x ${availHeight}
+
touch: ${touch}
+
depth: ${colorDepth}|${pixelDepth}
+
viewport:
+
+
+
${outerWidth}
+
${innerWidth}
+
${toFix(vVWidth, 6)}
+
${outerHeight}
+
${innerHeight}
+
${toFix(vVHeight, 6)}
+
${displayMode}
+
${mediaOrientation}
+
${orientationType}
+
${dpr}
+
+
+
+ `;
+ };
+ return `
+ ${resizeHTML({ data, $hash, perf, paintScreen })}
+ `;
+ }
+
+ async function getVoices() {
+ // Don't run voice immediately. This is unstable
+ // wait a bit for services to load
+ await new Promise((resolve) => setTimeout(() => resolve(undefined), 50));
+ return new Promise(async (resolve) => {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ // use window since iframe is unstable in FF
+ const supported = 'speechSynthesis' in window;
+ supported && speechSynthesis.getVoices(); // warm up
+ if (!supported) {
+ logTestResult({ test: 'speech', passed: false });
+ return resolve(null);
+ }
+ const voicesLie = !!lieProps['SpeechSynthesis.getVoices'];
+ const giveUpOnVoices = setTimeout(() => {
+ logTestResult({ test: 'speech', passed: false });
+ return resolve(null);
+ }, 300);
+ const getVoices = () => {
+ const data = speechSynthesis.getVoices();
+ const localServiceDidLoad = (data || []).find((x) => x.localService);
+ if (!data || !data.length || (IS_BLINK && !localServiceDidLoad)) {
+ return;
+ }
+ clearTimeout(giveUpOnVoices);
+ // filter first occurrence of unique voiceURI data
+ const getUniques = (data, voiceURISet) => data
+ .filter((x) => {
+ const { voiceURI } = x;
+ if (!voiceURISet.has(voiceURI)) {
+ voiceURISet.add(voiceURI);
+ return true;
+ }
+ return false;
+ });
+ const dataUnique = getUniques(data, new Set());
+ // https://wicg.github.io/speech-api/#speechsynthesisvoice-attributes
+ const local = dataUnique.filter((x) => x.localService).map((x) => x.name);
+ const remote = dataUnique.filter((x) => !x.localService).map((x) => x.name);
+ const languages = [...new Set(dataUnique.map((x) => x.lang))];
+ const defaultLocalVoices = dataUnique.filter((x) => x.default && x.localService);
+ let defaultVoiceName = '';
+ let defaultVoiceLang = '';
+ if (defaultLocalVoices.length === 1) {
+ const { name, lang } = defaultLocalVoices[0];
+ defaultVoiceName = name;
+ defaultVoiceLang = (lang || '').replace(/_/, '-');
+ }
+ // eslint-disable-next-line new-cap
+ const { locale: localeLang } = Intl.DateTimeFormat().resolvedOptions();
+ if (defaultVoiceLang &&
+ defaultVoiceLang.split('-')[0] !== localeLang.split('-')[0]) {
+ // this is not trash
+ Analysis.voiceLangMismatch = true;
+ LowerEntropy.TIME_ZONE = true;
+ }
+ logTestResult({ time: timer.stop(), test: 'speech', passed: true });
+ return resolve({
+ local,
+ remote,
+ languages,
+ defaultVoiceName,
+ defaultVoiceLang,
+ lied: voicesLie,
+ });
+ };
+ getVoices();
+ if (speechSynthesis.addEventListener) {
+ return speechSynthesis.addEventListener('voiceschanged', getVoices);
+ }
+ speechSynthesis.onvoiceschanged = getVoices;
+ }
+ catch (error) {
+ logTestResult({ test: 'speech', passed: false });
+ captureError(error);
+ return resolve(null);
+ }
+ });
+ }
+ function voicesHTML(fp) {
+ if (!fp.voices) {
+ return `
+
+
Speech
+
local (0): ${HTMLNote.BLOCKED}
+
remote (0): ${HTMLNote.BLOCKED}
+
lang (0): ${HTMLNote.BLOCKED}
+
default:
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { voices: { $hash, local, remote, languages, defaultVoiceName, defaultVoiceLang, lied, }, } = fp;
+ const icon = {
+ 'Linux': ' ',
+ 'Apple': ' ',
+ 'Windows': ' ',
+ 'Android': ' ',
+ 'CrOS': ' ',
+ };
+ const system = {
+ 'Chrome OS': icon.CrOS,
+ 'Maged': icon.Apple,
+ 'Microsoft': icon.Windows,
+ 'English United States': icon.Android,
+ 'English (United States)': icon.Android,
+ };
+ const systemVoice = Object.keys(system).find((key) => local.find((voice) => voice.includes(key))) || '';
+ return `
+
+
${performanceLogger.getLog().speech}
+
Speech ${hashSlice($hash)}
+
local (${count(local)}): ${!local || !local.length ? HTMLNote.UNSUPPORTED :
+ modal('creep-voices-local', local.join(' '), `${system[systemVoice] || ''}${hashMini(local)}`)}
+
remote (${count(remote)}): ${!remote || !remote.length ? HTMLNote.UNSUPPORTED :
+ modal('creep-voices-remote', remote.join(' '), hashMini(remote))}
+
lang (${count(languages)}): ${!languages || !languages.length ? HTMLNote.BLOCKED :
+ languages.length == 1 ? languages[0] : modal('creep-voices-languages', languages.join(' '), hashMini(languages))}
+
default:
+
+ ${!defaultVoiceName ? HTMLNote.UNSUPPORTED :
+ `${defaultVoiceName}${defaultVoiceLang ? ` [${defaultVoiceLang}]` : ''}`}
+
+
+ `;
+ }
+
+ const GIGABYTE = 1073741824; // bytes
+ function getMaxCallStackSize() {
+ const fn = () => {
+ try {
+ return 1 + fn();
+ }
+ catch (err) {
+ return 1;
+ }
+ };
+ [...Array(10)].forEach(() => fn()); // stabilize
+ return fn();
+ }
+ // based on and inspired by
+ // https://github.com/Joe12387/OP-Fingerprinting-Script/blob/main/opfs.js#L443
+ function getTimingResolution() {
+ const maxRuns = 5000;
+ let valA = 1;
+ let valB = 1;
+ let res;
+ for (let i = 0; i < maxRuns; i++) {
+ const a = performance.now();
+ const b = performance.now();
+ if (a < b) {
+ res = b - a;
+ if (res > valA && res < valB) {
+ valB = res;
+ }
+ else if (res < valA) {
+ valB = valA;
+ valA = res;
+ }
+ }
+ }
+ return [valA, valB];
+ }
+ function getClientLitter() {
+ try {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ const iframeWindow = iframe.contentWindow;
+ const windowKeys = Object.getOwnPropertyNames(window);
+ const iframeKeys = Object.getOwnPropertyNames(iframeWindow);
+ document.body.removeChild(iframe);
+ const clientKeys = windowKeys.filter((x) => !iframeKeys.includes(x));
+ return clientKeys;
+ }
+ catch (err) {
+ return [];
+ }
+ }
+ function getClientCode() {
+ const names = Object.getOwnPropertyNames(window).slice(-50);
+ const [p1, p2] = (1).constructor.toString().split((1).constructor.name);
+ const isEngine = (fn) => {
+ return (typeof fn === 'function' &&
+ ('' + fn === p1 + fn.name + p2 || '' + fn === p1 + (fn.name || '').replace('get ', '') + p2));
+ };
+ const isClient = (key) => {
+ if (/_$/.test(key))
+ return true;
+ const d = Object.getOwnPropertyDescriptor(window, key);
+ if (!d)
+ return true;
+ return key === 'chrome' ? names.includes(key) : !isEngine(d.get || d.value);
+ };
+ return Object.keys(window)
+ .slice(-50)
+ .filter((x) => isClient(x));
+ }
+ async function getBattery() {
+ if (!('getBattery' in navigator))
+ return null;
+ // @ts-expect-error if not supported
+ return navigator.getBattery();
+ }
+ async function getStorage() {
+ if (!navigator?.storage?.estimate)
+ return null;
+ return Promise.all([
+ navigator.storage.estimate().then(({ quota }) => quota),
+ new Promise((resolve) => {
+ // @ts-expect-error if not supported
+ navigator.webkitTemporaryStorage.queryUsageAndQuota((_, quota) => {
+ resolve(quota);
+ });
+ }).catch(() => null),
+ ]).then(([quota1, quota2]) => (quota2 || quota1));
+ }
+ async function getScriptSize() {
+ let url = null;
+ try {
+ // @ts-expect-error if unsupported
+ url = document?.currentScript?.src || (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('creep.js', document.baseURI).href);
+ }
+ catch (err) { }
+ if (!url)
+ return null;
+ return fetch(url)
+ .then((res) => res.blob())
+ .then((blob) => blob.size)
+ .catch(() => null);
+ }
+ async function getStatus() {
+ const [batteryInfo, quotaA, quotaB, scriptSize, stackSize, timingRes, clientLitter,] = await Promise.all([
+ getBattery(),
+ getStorage(),
+ getStorage(),
+ getScriptSize(),
+ getMaxCallStackSize(),
+ getTimingResolution(),
+ [...new Set([...getClientLitter(), ...getClientCode()])].sort().slice(0, 50),
+ ]);
+ // BatteryManager
+ const { charging, chargingTime, dischargingTime, level, } = batteryInfo || {};
+ // MemoryInfo
+ // @ts-expect-error if not supported
+ const memory = performance?.memory?.jsHeapSizeLimit || null;
+ const memoryInGigabytes = memory ? +(memory / GIGABYTE).toFixed(2) : null;
+ // StorageManager
+ const quotaInGigabytes = quotaA ? +(+(quotaA) / GIGABYTE).toFixed(2) : null;
+ // Network Info
+ const { downlink, effectiveType, rtt, saveData, downlinkMax, type,
+ // @ts-expect-error if not supported
+ } = navigator?.connection || {};
+ const scripts = [
+ ...document.querySelectorAll('script'),
+ ].map((x) => x.src.replace(/^https?:\/\//, '')).slice(0, 10);
+ return {
+ charging,
+ chargingTime,
+ dischargingTime,
+ level,
+ memory,
+ memoryInGigabytes,
+ quota: quotaA,
+ quotaIsInsecure: quotaA !== quotaB,
+ quotaInGigabytes,
+ downlink,
+ effectiveType,
+ rtt,
+ saveData,
+ downlinkMax,
+ type,
+ stackSize,
+ timingRes,
+ clientLitter,
+ scripts,
+ scriptSize,
+ };
+ }
+ function statusHTML(status) {
+ if (!status) {
+ return `
+
+
Status
+
network:
+
${HTMLNote.BLOCKED}
+
+
+
battery:
+
${HTMLNote.BLOCKED}
+
+
+
available:
+
${HTMLNote.BLOCKED}
+
+ `;
+ }
+ const { charging, chargingTime, dischargingTime, level, memory, memoryInGigabytes, quota, quotaInGigabytes, downlink, effectiveType, rtt, saveData, downlinkMax, type, stackSize, timingRes, } = status;
+ const statusHash = hashMini({
+ memoryInGigabytes,
+ quotaInGigabytes,
+ timingRes,
+ rtt: rtt === 0 ? 0 : -1,
+ type,
+ });
+ return `
+
+
Status ${statusHash}
+
network:
+
${isNaN(Number(rtt)) ? HTMLNote.UNSUPPORTED : `
+
rtt: ${rtt}, downlink: ${downlink}${downlinkMax ? `, max: ${downlinkMax}` : ''}
+
effectiveType: ${effectiveType}
+
saveData: ${saveData}${type ? `, type: ${type}` : ''}
+ `}
+
+
+
+
battery:
+
${!level || isNaN(Number(level)) ? HTMLNote.UNSUPPORTED : `
+
level: ${level * 100}%
+
charging: ${charging}
+
charge time: ${chargingTime === Infinity ? 'discharging' :
+ chargingTime === 0 ? 'fully charged' :
+ `${+(chargingTime / 60).toFixed(1)} min.`}
+
discharge time: ${dischargingTime === Infinity ? 'charging' :
+ `${+(dischargingTime / 60).toFixed(1)} min.`}
+ `}
+
+
+
available:
+
+ ${quota ? `
storage: ${quotaInGigabytes}GB [${quota}]
` : ''}
+ ${memory ? `
memory: ${memoryInGigabytes}GB [${memory}]
` : ''}
+ ${timingRes ? `
timing res: ${timingRes.join(' ')}
` : ''}
+
stack: ${stackSize || HTMLNote.BLOCKED}
+
+
+ `;
+ }
+
+ async function getSVG() {
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ let lied = (lieProps['SVGRect.height'] ||
+ lieProps['SVGRect.width'] ||
+ lieProps['SVGRect.x'] ||
+ lieProps['SVGRect.y'] ||
+ lieProps['String.fromCodePoint'] ||
+ lieProps['SVGRectElement.getBBox'] ||
+ lieProps['SVGTextContentElement.getExtentOfChar'] ||
+ lieProps['SVGTextContentElement.getSubStringLength'] ||
+ lieProps['SVGTextContentElement.getComputedTextLength']) || false;
+ const doc = (PHANTOM_DARKNESS &&
+ PHANTOM_DARKNESS.document &&
+ PHANTOM_DARKNESS.document.body ? PHANTOM_DARKNESS.document :
+ document);
+ const divElement = document.createElement('div');
+ doc.body.appendChild(divElement);
+ // patch div
+ patch(divElement, html `
+
+
+
+
+ ${EMOJIS.map((emoji) => {
+ return `${emoji} `;
+ }).join('')}
+
+
+
+ `);
+ // SVG
+ const reduceToObject = (nativeObj) => {
+ const keys = Object.keys(nativeObj.__proto__);
+ return keys.reduce((acc, key) => {
+ const val = nativeObj[key];
+ const isMethod = typeof val == 'function';
+ return isMethod ? acc : { ...acc, [key]: val };
+ }, {});
+ };
+ const reduceToSum = (nativeObj) => {
+ const keys = Object.keys(nativeObj.__proto__);
+ return keys.reduce((acc, key) => {
+ const val = nativeObj[key];
+ return isNaN(val) ? acc : (acc += val);
+ }, 0);
+ };
+ const getObjectSum = (obj) => !obj ? 0 : Object.keys(obj).reduce((acc, key) => acc += Math.abs(obj[key]), 0);
+ // SVGRect
+ const svgBox = doc.getElementById('svgBox');
+ const bBox = reduceToObject(svgBox.getBBox());
+ // compute SVGRect emojis
+ const pattern = new Set();
+ const svgElems = [...svgBox.getElementsByClassName('svgrect-emoji')];
+ await queueEvent(timer);
+ const emojiSet = svgElems.reduce((emojiSet, el, i) => {
+ const emoji = EMOJIS[i];
+ const dimensions = '' + el.getComputedTextLength();
+ if (!pattern.has(dimensions)) {
+ pattern.add(dimensions);
+ emojiSet.add(emoji);
+ }
+ return emojiSet;
+ }, new Set());
+ // svgRect System Sum
+ const svgrectSystemSum = 0.00001 * [...pattern].map((x) => {
+ return x.split(',').reduce((acc, x) => acc += (+x || 0), 0);
+ }).reduce((acc, x) => acc += x, 0);
+ // detect failed shift calculation
+ const svgEmojiEl = svgElems[0];
+ const initial = svgEmojiEl.getComputedTextLength();
+ svgEmojiEl.classList.add('shift-svg');
+ const shifted = svgEmojiEl.getComputedTextLength();
+ svgEmojiEl.classList.remove('shift-svg');
+ const unshifted = svgEmojiEl.getComputedTextLength();
+ if ((initial - shifted) != (unshifted - shifted)) {
+ lied = true;
+ documentLie('SVGTextContentElement.getComputedTextLength', 'failed unshift calculation');
+ }
+ const data = {
+ bBox: getObjectSum(bBox),
+ extentOfChar: reduceToSum(svgElems[0].getExtentOfChar(EMOJIS[0])),
+ subStringLength: svgElems[0].getSubStringLength(0, 10),
+ computedTextLength: svgElems[0].getComputedTextLength(),
+ emojiSet: [...emojiSet],
+ svgrectSystemSum,
+ lied,
+ };
+ doc.body.removeChild(doc.getElementById('svg-container'));
+ logTestResult({ time: timer.stop(), test: 'svg', passed: true });
+ return data;
+ }
+ catch (error) {
+ logTestResult({ test: 'svg', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function svgHTML(fp) {
+ if (!fp.svg) {
+ return `
+
+
SVGRect
+
bBox: ${HTMLNote.BLOCKED}
+
char: ${HTMLNote.BLOCKED}
+
subs: ${HTMLNote.BLOCKED}
+
text: ${HTMLNote.BLOCKED}
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { svg: { $hash, bBox, subStringLength, extentOfChar, computedTextLength, emojiSet, svgrectSystemSum, lied, }, } = fp;
+ const divisor = 10000;
+ const helpTitle = `SVGTextContentElement.getComputedTextLength()\nhash: ${hashMini(emojiSet)}\n${emojiSet.map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`;
+ return `
+
+
${performanceLogger.getLog().svg}
+
SVGRect ${hashSlice($hash)}
+
bBox: ${bBox ? (bBox / divisor) : HTMLNote.BLOCKED}
+
char: ${extentOfChar ? (extentOfChar / divisor) : HTMLNote.BLOCKED}
+
subs: ${subStringLength ? (subStringLength / divisor) : HTMLNote.BLOCKED}
+
text: ${computedTextLength ? (computedTextLength / divisor) : HTMLNote.BLOCKED}
+
+ ${svgrectSystemSum || HTMLNote.UNSUPPORTED}
+ ${formatEmojiSet(emojiSet)}
+
+
+ `;
+ }
+
+ function getTimezone() {
+ // inspired by https://arkenfox.github.io/TZP
+ // https://github.com/vvo/tzdb/blob/master/time-zones-names.json
+ const cities = [
+ 'UTC',
+ 'GMT',
+ 'Etc/GMT+0',
+ 'Etc/GMT+1',
+ 'Etc/GMT+10',
+ 'Etc/GMT+11',
+ 'Etc/GMT+12',
+ 'Etc/GMT+2',
+ 'Etc/GMT+3',
+ 'Etc/GMT+4',
+ 'Etc/GMT+5',
+ 'Etc/GMT+6',
+ 'Etc/GMT+7',
+ 'Etc/GMT+8',
+ 'Etc/GMT+9',
+ 'Etc/GMT-1',
+ 'Etc/GMT-10',
+ 'Etc/GMT-11',
+ 'Etc/GMT-12',
+ 'Etc/GMT-13',
+ 'Etc/GMT-14',
+ 'Etc/GMT-2',
+ 'Etc/GMT-3',
+ 'Etc/GMT-4',
+ 'Etc/GMT-5',
+ 'Etc/GMT-6',
+ 'Etc/GMT-7',
+ 'Etc/GMT-8',
+ 'Etc/GMT-9',
+ 'Etc/GMT',
+ 'Africa/Abidjan',
+ 'Africa/Accra',
+ 'Africa/Addis_Ababa',
+ 'Africa/Algiers',
+ 'Africa/Asmara',
+ 'Africa/Bamako',
+ 'Africa/Bangui',
+ 'Africa/Banjul',
+ 'Africa/Bissau',
+ 'Africa/Blantyre',
+ 'Africa/Brazzaville',
+ 'Africa/Bujumbura',
+ 'Africa/Cairo',
+ 'Africa/Casablanca',
+ 'Africa/Ceuta',
+ 'Africa/Conakry',
+ 'Africa/Dakar',
+ 'Africa/Dar_es_Salaam',
+ 'Africa/Djibouti',
+ 'Africa/Douala',
+ 'Africa/El_Aaiun',
+ 'Africa/Freetown',
+ 'Africa/Gaborone',
+ 'Africa/Harare',
+ 'Africa/Johannesburg',
+ 'Africa/Juba',
+ 'Africa/Kampala',
+ 'Africa/Khartoum',
+ 'Africa/Kigali',
+ 'Africa/Kinshasa',
+ 'Africa/Lagos',
+ 'Africa/Libreville',
+ 'Africa/Lome',
+ 'Africa/Luanda',
+ 'Africa/Lubumbashi',
+ 'Africa/Lusaka',
+ 'Africa/Malabo',
+ 'Africa/Maputo',
+ 'Africa/Maseru',
+ 'Africa/Mbabane',
+ 'Africa/Mogadishu',
+ 'Africa/Monrovia',
+ 'Africa/Nairobi',
+ 'Africa/Ndjamena',
+ 'Africa/Niamey',
+ 'Africa/Nouakchott',
+ 'Africa/Ouagadougou',
+ 'Africa/Porto-Novo',
+ 'Africa/Sao_Tome',
+ 'Africa/Tripoli',
+ 'Africa/Tunis',
+ 'Africa/Windhoek',
+ 'America/Adak',
+ 'America/Anchorage',
+ 'America/Anguilla',
+ 'America/Antigua',
+ 'America/Araguaina',
+ 'America/Argentina/Buenos_Aires',
+ 'America/Argentina/Catamarca',
+ 'America/Argentina/Cordoba',
+ 'America/Argentina/Jujuy',
+ 'America/Argentina/La_Rioja',
+ 'America/Argentina/Mendoza',
+ 'America/Argentina/Rio_Gallegos',
+ 'America/Argentina/Salta',
+ 'America/Argentina/San_Juan',
+ 'America/Argentina/San_Luis',
+ 'America/Argentina/Tucuman',
+ 'America/Argentina/Ushuaia',
+ 'America/Aruba',
+ 'America/Asuncion',
+ 'America/Atikokan',
+ 'America/Bahia',
+ 'America/Bahia_Banderas',
+ 'America/Barbados',
+ 'America/Belem',
+ 'America/Belize',
+ 'America/Blanc-Sablon',
+ 'America/Boa_Vista',
+ 'America/Bogota',
+ 'America/Boise',
+ 'America/Cambridge_Bay',
+ 'America/Campo_Grande',
+ 'America/Cancun',
+ 'America/Caracas',
+ 'America/Cayenne',
+ 'America/Cayman',
+ 'America/Chicago',
+ 'America/Chihuahua',
+ 'America/Costa_Rica',
+ 'America/Creston',
+ 'America/Cuiaba',
+ 'America/Curacao',
+ 'America/Danmarkshavn',
+ 'America/Dawson',
+ 'America/Dawson_Creek',
+ 'America/Denver',
+ 'America/Detroit',
+ 'America/Dominica',
+ 'America/Edmonton',
+ 'America/Eirunepe',
+ 'America/El_Salvador',
+ 'America/Fort_Nelson',
+ 'America/Fortaleza',
+ 'America/Glace_Bay',
+ 'America/Godthab',
+ 'America/Goose_Bay',
+ 'America/Grand_Turk',
+ 'America/Grenada',
+ 'America/Guadeloupe',
+ 'America/Guatemala',
+ 'America/Guayaquil',
+ 'America/Guyana',
+ 'America/Halifax',
+ 'America/Havana',
+ 'America/Hermosillo',
+ 'America/Indiana/Indianapolis',
+ 'America/Indiana/Knox',
+ 'America/Indiana/Marengo',
+ 'America/Indiana/Petersburg',
+ 'America/Indiana/Tell_City',
+ 'America/Indiana/Vevay',
+ 'America/Indiana/Vincennes',
+ 'America/Indiana/Winamac',
+ 'America/Inuvik',
+ 'America/Iqaluit',
+ 'America/Jamaica',
+ 'America/Juneau',
+ 'America/Kentucky/Louisville',
+ 'America/Kentucky/Monticello',
+ 'America/Kralendijk',
+ 'America/La_Paz',
+ 'America/Lima',
+ 'America/Los_Angeles',
+ 'America/Lower_Princes',
+ 'America/Maceio',
+ 'America/Managua',
+ 'America/Manaus',
+ 'America/Marigot',
+ 'America/Martinique',
+ 'America/Matamoros',
+ 'America/Mazatlan',
+ 'America/Menominee',
+ 'America/Merida',
+ 'America/Metlakatla',
+ 'America/Mexico_City',
+ 'America/Miquelon',
+ 'America/Moncton',
+ 'America/Monterrey',
+ 'America/Montevideo',
+ 'America/Montserrat',
+ 'America/Nassau',
+ 'America/New_York',
+ 'America/Nipigon',
+ 'America/Nome',
+ 'America/Noronha',
+ 'America/North_Dakota/Beulah',
+ 'America/North_Dakota/Center',
+ 'America/North_Dakota/New_Salem',
+ 'America/Ojinaga',
+ 'America/Panama',
+ 'America/Pangnirtung',
+ 'America/Paramaribo',
+ 'America/Phoenix',
+ 'America/Port-au-Prince',
+ 'America/Port_of_Spain',
+ 'America/Porto_Velho',
+ 'America/Puerto_Rico',
+ 'America/Punta_Arenas',
+ 'America/Rainy_River',
+ 'America/Rankin_Inlet',
+ 'America/Recife',
+ 'America/Regina',
+ 'America/Resolute',
+ 'America/Rio_Branco',
+ 'America/Santarem',
+ 'America/Santiago',
+ 'America/Santo_Domingo',
+ 'America/Sao_Paulo',
+ 'America/Scoresbysund',
+ 'America/Sitka',
+ 'America/St_Barthelemy',
+ 'America/St_Johns',
+ 'America/St_Kitts',
+ 'America/St_Lucia',
+ 'America/St_Thomas',
+ 'America/St_Vincent',
+ 'America/Swift_Current',
+ 'America/Tegucigalpa',
+ 'America/Thule',
+ 'America/Thunder_Bay',
+ 'America/Tijuana',
+ 'America/Toronto',
+ 'America/Tortola',
+ 'America/Vancouver',
+ 'America/Whitehorse',
+ 'America/Winnipeg',
+ 'America/Yakutat',
+ 'America/Yellowknife',
+ 'Antarctica/Casey',
+ 'Antarctica/Davis',
+ 'Antarctica/DumontDUrville',
+ 'Antarctica/Macquarie',
+ 'Antarctica/Mawson',
+ 'Antarctica/McMurdo',
+ 'Antarctica/Palmer',
+ 'Antarctica/Rothera',
+ 'Antarctica/Syowa',
+ 'Antarctica/Troll',
+ 'Antarctica/Vostok',
+ 'Arctic/Longyearbyen',
+ 'Asia/Aden',
+ 'Asia/Almaty',
+ 'Asia/Amman',
+ 'Asia/Anadyr',
+ 'Asia/Aqtau',
+ 'Asia/Aqtobe',
+ 'Asia/Ashgabat',
+ 'Asia/Atyrau',
+ 'Asia/Baghdad',
+ 'Asia/Bahrain',
+ 'Asia/Baku',
+ 'Asia/Bangkok',
+ 'Asia/Barnaul',
+ 'Asia/Beirut',
+ 'Asia/Bishkek',
+ 'Asia/Brunei',
+ 'Asia/Calcutta',
+ 'Asia/Chita',
+ 'Asia/Choibalsan',
+ 'Asia/Colombo',
+ 'Asia/Damascus',
+ 'Asia/Dhaka',
+ 'Asia/Dili',
+ 'Asia/Dubai',
+ 'Asia/Dushanbe',
+ 'Asia/Famagusta',
+ 'Asia/Gaza',
+ 'Asia/Hebron',
+ 'Asia/Ho_Chi_Minh',
+ 'Asia/Hong_Kong',
+ 'Asia/Hovd',
+ 'Asia/Irkutsk',
+ 'Asia/Jakarta',
+ 'Asia/Jayapura',
+ 'Asia/Jerusalem',
+ 'Asia/Kabul',
+ 'Asia/Kamchatka',
+ 'Asia/Karachi',
+ 'Asia/Kathmandu',
+ 'Asia/Khandyga',
+ 'Asia/Kolkata',
+ 'Asia/Krasnoyarsk',
+ 'Asia/Kuala_Lumpur',
+ 'Asia/Kuching',
+ 'Asia/Kuwait',
+ 'Asia/Macau',
+ 'Asia/Magadan',
+ 'Asia/Makassar',
+ 'Asia/Manila',
+ 'Asia/Muscat',
+ 'Asia/Nicosia',
+ 'Asia/Novokuznetsk',
+ 'Asia/Novosibirsk',
+ 'Asia/Omsk',
+ 'Asia/Oral',
+ 'Asia/Phnom_Penh',
+ 'Asia/Pontianak',
+ 'Asia/Pyongyang',
+ 'Asia/Qatar',
+ 'Asia/Qostanay',
+ 'Asia/Qyzylorda',
+ 'Asia/Riyadh',
+ 'Asia/Sakhalin',
+ 'Asia/Samarkand',
+ 'Asia/Seoul',
+ 'Asia/Shanghai',
+ 'Asia/Singapore',
+ 'Asia/Srednekolymsk',
+ 'Asia/Taipei',
+ 'Asia/Tashkent',
+ 'Asia/Tbilisi',
+ 'Asia/Tehran',
+ 'Asia/Thimphu',
+ 'Asia/Tokyo',
+ 'Asia/Tomsk',
+ 'Asia/Ulaanbaatar',
+ 'Asia/Urumqi',
+ 'Asia/Ust-Nera',
+ 'Asia/Vientiane',
+ 'Asia/Vladivostok',
+ 'Asia/Yakutsk',
+ 'Asia/Yangon',
+ 'Asia/Yekaterinburg',
+ 'Asia/Yerevan',
+ 'Atlantic/Azores',
+ 'Atlantic/Bermuda',
+ 'Atlantic/Canary',
+ 'Atlantic/Cape_Verde',
+ 'Atlantic/Faroe',
+ 'Atlantic/Madeira',
+ 'Atlantic/Reykjavik',
+ 'Atlantic/South_Georgia',
+ 'Atlantic/St_Helena',
+ 'Atlantic/Stanley',
+ 'Australia/Adelaide',
+ 'Australia/Brisbane',
+ 'Australia/Broken_Hill',
+ 'Australia/Currie',
+ 'Australia/Darwin',
+ 'Australia/Eucla',
+ 'Australia/Hobart',
+ 'Australia/Lindeman',
+ 'Australia/Lord_Howe',
+ 'Australia/Melbourne',
+ 'Australia/Perth',
+ 'Australia/Sydney',
+ 'Europe/Amsterdam',
+ 'Europe/Andorra',
+ 'Europe/Astrakhan',
+ 'Europe/Athens',
+ 'Europe/Belgrade',
+ 'Europe/Berlin',
+ 'Europe/Bratislava',
+ 'Europe/Brussels',
+ 'Europe/Bucharest',
+ 'Europe/Budapest',
+ 'Europe/Busingen',
+ 'Europe/Chisinau',
+ 'Europe/Copenhagen',
+ 'Europe/Dublin',
+ 'Europe/Gibraltar',
+ 'Europe/Guernsey',
+ 'Europe/Helsinki',
+ 'Europe/Isle_of_Man',
+ 'Europe/Istanbul',
+ 'Europe/Jersey',
+ 'Europe/Kaliningrad',
+ 'Europe/Kiev',
+ 'Europe/Kirov',
+ 'Europe/Lisbon',
+ 'Europe/Ljubljana',
+ 'Europe/London',
+ 'Europe/Luxembourg',
+ 'Europe/Madrid',
+ 'Europe/Malta',
+ 'Europe/Mariehamn',
+ 'Europe/Minsk',
+ 'Europe/Monaco',
+ 'Europe/Moscow',
+ 'Europe/Oslo',
+ 'Europe/Paris',
+ 'Europe/Podgorica',
+ 'Europe/Prague',
+ 'Europe/Riga',
+ 'Europe/Rome',
+ 'Europe/Samara',
+ 'Europe/San_Marino',
+ 'Europe/Sarajevo',
+ 'Europe/Saratov',
+ 'Europe/Simferopol',
+ 'Europe/Skopje',
+ 'Europe/Sofia',
+ 'Europe/Stockholm',
+ 'Europe/Tallinn',
+ 'Europe/Tirane',
+ 'Europe/Ulyanovsk',
+ 'Europe/Uzhgorod',
+ 'Europe/Vaduz',
+ 'Europe/Vatican',
+ 'Europe/Vienna',
+ 'Europe/Vilnius',
+ 'Europe/Volgograd',
+ 'Europe/Warsaw',
+ 'Europe/Zagreb',
+ 'Europe/Zaporozhye',
+ 'Europe/Zurich',
+ 'Indian/Antananarivo',
+ 'Indian/Chagos',
+ 'Indian/Christmas',
+ 'Indian/Cocos',
+ 'Indian/Comoro',
+ 'Indian/Kerguelen',
+ 'Indian/Mahe',
+ 'Indian/Maldives',
+ 'Indian/Mauritius',
+ 'Indian/Mayotte',
+ 'Indian/Reunion',
+ 'Pacific/Apia',
+ 'Pacific/Auckland',
+ 'Pacific/Bougainville',
+ 'Pacific/Chatham',
+ 'Pacific/Chuuk',
+ 'Pacific/Easter',
+ 'Pacific/Efate',
+ 'Pacific/Enderbury',
+ 'Pacific/Fakaofo',
+ 'Pacific/Fiji',
+ 'Pacific/Funafuti',
+ 'Pacific/Galapagos',
+ 'Pacific/Gambier',
+ 'Pacific/Guadalcanal',
+ 'Pacific/Guam',
+ 'Pacific/Honolulu',
+ 'Pacific/Kiritimati',
+ 'Pacific/Kosrae',
+ 'Pacific/Kwajalein',
+ 'Pacific/Majuro',
+ 'Pacific/Marquesas',
+ 'Pacific/Midway',
+ 'Pacific/Nauru',
+ 'Pacific/Niue',
+ 'Pacific/Norfolk',
+ 'Pacific/Noumea',
+ 'Pacific/Pago_Pago',
+ 'Pacific/Palau',
+ 'Pacific/Pitcairn',
+ 'Pacific/Pohnpei',
+ 'Pacific/Port_Moresby',
+ 'Pacific/Rarotonga',
+ 'Pacific/Saipan',
+ 'Pacific/Tahiti',
+ 'Pacific/Tarawa',
+ 'Pacific/Tongatapu',
+ 'Pacific/Wake',
+ 'Pacific/Wallis',
+ ];
+ const getTimezoneOffset = () => {
+ const [year, month, day] = JSON.stringify(new Date())
+ .slice(1, 11)
+ .split('-');
+ const dateString = `${month}/${day}/${year}`;
+ const dateStringUTC = `${year}-${month}-${day}`;
+ const now = +new Date(dateString);
+ const utc = +new Date(dateStringUTC);
+ const offset = +((now - utc) / 60000);
+ return ~~offset;
+ };
+ const getTimezoneOffsetHistory = ({ year, city = null }) => {
+ const format = {
+ timeZone: '',
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ };
+ const minute = 60000;
+ let formatter;
+ let summer;
+ if (city) {
+ const options = {
+ ...format,
+ timeZone: city,
+ };
+ // @ts-ignore
+ formatter = new Intl.DateTimeFormat('en', options);
+ summer = +new Date(formatter.format(new Date(`7/1/${year}`)));
+ }
+ else {
+ summer = +new Date(`7/1/${year}`);
+ }
+ const summerUTCTime = +new Date(`${year}-07-01`);
+ const offset = (summer - summerUTCTime) / minute;
+ return offset;
+ };
+ const binarySearch = (list, fn) => {
+ const end = list.length;
+ const middle = Math.floor(end / 2);
+ const [left, right] = [list.slice(0, middle), list.slice(middle, end)];
+ const found = fn(left);
+ return end == 1 || found.length ? found : binarySearch(right, fn);
+ };
+ const decryptLocation = ({ year, timeZone }) => {
+ const system = getTimezoneOffsetHistory({ year });
+ const resolvedOptions = getTimezoneOffsetHistory({ year, city: timeZone });
+ const filter = (cities) => cities
+ .filter((city) => system == getTimezoneOffsetHistory({ year, city }));
+ // get city region set
+ const decryption = (system == resolvedOptions ? [timeZone] : binarySearch(cities, filter));
+ // reduce set to one city
+ const decrypted = (decryption.length == 1 && decryption[0] == timeZone ? timeZone : hashMini(decryption));
+ return decrypted;
+ };
+ const formatLocation = (x) => {
+ try {
+ return x.replace(/_/, ' ').split('/').join(', ');
+ }
+ catch (error) { }
+ return x;
+ };
+ try {
+ const timer = createTimer();
+ timer.start();
+ const lied = (lieProps['Date.getTimezoneOffset'] ||
+ lieProps['Intl.DateTimeFormat.resolvedOptions'] ||
+ lieProps['Intl.RelativeTimeFormat.resolvedOptions']) || false;
+ const year = 1113;
+ // eslint-disable-next-line new-cap
+ const { timeZone } = Intl.DateTimeFormat().resolvedOptions();
+ const decrypted = decryptLocation({ year, timeZone });
+ const locationEpoch = +new Date(new Date(`7/1/${year}`));
+ const notWithinParentheses = /.*\(|\).*/g;
+ const data = {
+ zone: ('' + new Date()).replace(notWithinParentheses, ''),
+ location: formatLocation(timeZone),
+ locationMeasured: formatLocation(decrypted),
+ locationEpoch,
+ offset: new Date().getTimezoneOffset(),
+ offsetComputed: getTimezoneOffset(),
+ lied,
+ };
+ logTestResult({ time: timer.stop(), test: 'timezone', passed: true });
+ return { ...data };
+ }
+ catch (error) {
+ logTestResult({ test: 'timezone', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function timezoneHTML(fp) {
+ if (!fp.timezone) {
+ return `
+
+
Timezone
+
${HTMLNote.BLOCKED}
+
`;
+ }
+ const { timezone: { $hash, zone, location, locationMeasured, locationEpoch, offset, offsetComputed, lied, }, } = fp;
+ return `
+
+
${performanceLogger.getLog().timezone}
+
Timezone ${hashSlice($hash)}
+
+ ${zone ? zone : ''}
+ ${location != locationMeasured ? locationMeasured : location}
+ ${locationEpoch}
+ ${offset != offsetComputed ? offsetComputed : offset}
+
+
+ `;
+ }
+
+ async function getCanvasWebgl() {
+ // use short list to improve performance
+ const getParamNames = () => [
+ // 'BLEND_EQUATION',
+ // 'BLEND_EQUATION_RGB',
+ // 'BLEND_EQUATION_ALPHA',
+ // 'BLEND_DST_RGB',
+ // 'BLEND_SRC_RGB',
+ // 'BLEND_DST_ALPHA',
+ // 'BLEND_SRC_ALPHA',
+ // 'BLEND_COLOR',
+ // 'CULL_FACE',
+ // 'BLEND',
+ // 'DITHER',
+ // 'STENCIL_TEST',
+ // 'DEPTH_TEST',
+ // 'SCISSOR_TEST',
+ // 'POLYGON_OFFSET_FILL',
+ // 'SAMPLE_ALPHA_TO_COVERAGE',
+ // 'SAMPLE_COVERAGE',
+ // 'LINE_WIDTH',
+ 'ALIASED_POINT_SIZE_RANGE',
+ 'ALIASED_LINE_WIDTH_RANGE',
+ // 'CULL_FACE_MODE',
+ // 'FRONT_FACE',
+ // 'DEPTH_RANGE',
+ // 'DEPTH_WRITEMASK',
+ // 'DEPTH_CLEAR_VALUE',
+ // 'DEPTH_FUNC',
+ // 'STENCIL_CLEAR_VALUE',
+ // 'STENCIL_FUNC',
+ // 'STENCIL_FAIL',
+ // 'STENCIL_PASS_DEPTH_FAIL',
+ // 'STENCIL_PASS_DEPTH_PASS',
+ // 'STENCIL_REF',
+ 'STENCIL_VALUE_MASK',
+ 'STENCIL_WRITEMASK',
+ // 'STENCIL_BACK_FUNC',
+ // 'STENCIL_BACK_FAIL',
+ // 'STENCIL_BACK_PASS_DEPTH_FAIL',
+ // 'STENCIL_BACK_PASS_DEPTH_PASS',
+ // 'STENCIL_BACK_REF',
+ 'STENCIL_BACK_VALUE_MASK',
+ 'STENCIL_BACK_WRITEMASK',
+ // 'VIEWPORT',
+ // 'SCISSOR_BOX',
+ // 'COLOR_CLEAR_VALUE',
+ // 'COLOR_WRITEMASK',
+ // 'UNPACK_ALIGNMENT',
+ // 'PACK_ALIGNMENT',
+ 'MAX_TEXTURE_SIZE',
+ 'MAX_VIEWPORT_DIMS',
+ 'SUBPIXEL_BITS',
+ // 'RED_BITS',
+ // 'GREEN_BITS',
+ // 'BLUE_BITS',
+ // 'ALPHA_BITS',
+ // 'DEPTH_BITS',
+ // 'STENCIL_BITS',
+ // 'POLYGON_OFFSET_UNITS',
+ // 'POLYGON_OFFSET_FACTOR',
+ // 'SAMPLE_BUFFERS',
+ // 'SAMPLES',
+ // 'SAMPLE_COVERAGE_VALUE',
+ // 'SAMPLE_COVERAGE_INVERT',
+ // 'COMPRESSED_TEXTURE_FORMATS',
+ // 'GENERATE_MIPMAP_HINT',
+ 'MAX_VERTEX_ATTRIBS',
+ 'MAX_VERTEX_UNIFORM_VECTORS',
+ 'MAX_VARYING_VECTORS',
+ 'MAX_COMBINED_TEXTURE_IMAGE_UNITS',
+ 'MAX_VERTEX_TEXTURE_IMAGE_UNITS',
+ 'MAX_TEXTURE_IMAGE_UNITS',
+ 'MAX_FRAGMENT_UNIFORM_VECTORS',
+ 'SHADING_LANGUAGE_VERSION',
+ 'VENDOR',
+ 'RENDERER',
+ 'VERSION',
+ 'MAX_CUBE_MAP_TEXTURE_SIZE',
+ // 'ACTIVE_TEXTURE',
+ // 'IMPLEMENTATION_COLOR_READ_TYPE',
+ // 'IMPLEMENTATION_COLOR_READ_FORMAT',
+ 'MAX_RENDERBUFFER_SIZE',
+ // 'UNPACK_FLIP_Y_WEBGL',
+ // 'UNPACK_PREMULTIPLY_ALPHA_WEBGL',
+ // 'UNPACK_COLORSPACE_CONVERSION_WEBGL',
+ // 'READ_BUFFER',
+ // 'UNPACK_ROW_LENGTH',
+ // 'UNPACK_SKIP_ROWS',
+ // 'UNPACK_SKIP_PIXELS',
+ // 'PACK_ROW_LENGTH',
+ // 'PACK_SKIP_ROWS',
+ // 'PACK_SKIP_PIXELS',
+ // 'UNPACK_SKIP_IMAGES',
+ // 'UNPACK_IMAGE_HEIGHT',
+ 'MAX_3D_TEXTURE_SIZE',
+ 'MAX_ELEMENTS_VERTICES',
+ 'MAX_ELEMENTS_INDICES',
+ 'MAX_TEXTURE_LOD_BIAS',
+ 'MAX_DRAW_BUFFERS',
+ // 'DRAW_BUFFER0',
+ // 'DRAW_BUFFER1',
+ // 'DRAW_BUFFER2',
+ // 'DRAW_BUFFER3',
+ // 'DRAW_BUFFER4',
+ // 'DRAW_BUFFER5',
+ // 'DRAW_BUFFER6',
+ // 'DRAW_BUFFER7',
+ 'MAX_FRAGMENT_UNIFORM_COMPONENTS',
+ 'MAX_VERTEX_UNIFORM_COMPONENTS',
+ // 'FRAGMENT_SHADER_DERIVATIVE_HINT',
+ 'MAX_ARRAY_TEXTURE_LAYERS',
+ // 'MIN_PROGRAM_TEXEL_OFFSET',
+ 'MAX_PROGRAM_TEXEL_OFFSET',
+ 'MAX_VARYING_COMPONENTS',
+ 'MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS',
+ // 'RASTERIZER_DISCARD',
+ 'MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS',
+ 'MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS',
+ 'MAX_COLOR_ATTACHMENTS',
+ 'MAX_SAMPLES',
+ 'MAX_VERTEX_UNIFORM_BLOCKS',
+ 'MAX_FRAGMENT_UNIFORM_BLOCKS',
+ 'MAX_COMBINED_UNIFORM_BLOCKS',
+ 'MAX_UNIFORM_BUFFER_BINDINGS',
+ 'MAX_UNIFORM_BLOCK_SIZE',
+ 'MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS',
+ 'MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS',
+ // 'UNIFORM_BUFFER_OFFSET_ALIGNMENT',
+ 'MAX_VERTEX_OUTPUT_COMPONENTS',
+ 'MAX_FRAGMENT_INPUT_COMPONENTS',
+ 'MAX_SERVER_WAIT_TIMEOUT',
+ // 'TRANSFORM_FEEDBACK_PAUSED',
+ // 'TRANSFORM_FEEDBACK_ACTIVE',
+ 'MAX_ELEMENT_INDEX',
+ 'MAX_CLIENT_WAIT_TIMEOUT_WEBGL',
+ ].sort();
+ const draw = (gl) => {
+ const isSafari15AndAbove = ('BigInt64Array' in window &&
+ IS_WEBKIT &&
+ !/(Cr|Fx)iOS/.test(navigator.userAgent));
+ if (!gl || isSafari15AndAbove) {
+ return;
+ }
+ // gl.clearColor(0.47, 0.7, 0.78, 1)
+ gl.clear(gl.COLOR_BUFFER_BIT);
+ // based on https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js
+ const vertexPosBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
+ const vertices = new Float32Array([-0.9, -0.7, 0, 0.8, -0.7, 0, 0, 0.5, 0]);
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
+ // create program
+ const program = gl.createProgram();
+ // compile and attach vertex shader
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
+ gl.shaderSource(vertexShader, `
+ attribute vec2 attrVertex;
+ varying vec2 varyinTexCoordinate;
+ uniform vec2 uniformOffset;
+ void main(){
+ varyinTexCoordinate = attrVertex + uniformOffset;
+ gl_Position = vec4(attrVertex, 0, 1);
+ }
+ `);
+ gl.compileShader(vertexShader);
+ gl.attachShader(program, vertexShader);
+ // compile and attach fragment shader
+ const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
+ gl.shaderSource(fragmentShader, `
+ precision mediump float;
+ varying vec2 varyinTexCoordinate;
+ void main() {
+ gl_FragColor = vec4(varyinTexCoordinate, 1, 1);
+ }
+ `);
+ gl.compileShader(fragmentShader);
+ gl.attachShader(program, fragmentShader);
+ // use program
+ const componentSize = 3;
+ gl.linkProgram(program);
+ gl.useProgram(program);
+ program.vertexPosAttrib = gl.getAttribLocation(program, 'attrVertex');
+ program.offsetUniform = gl.getUniformLocation(program, 'uniformOffset');
+ gl.enableVertexAttribArray(program.vertexPosArray);
+ gl.vertexAttribPointer(program.vertexPosAttrib, componentSize, gl.FLOAT, false, 0, 0);
+ gl.uniform2f(program.offsetUniform, 1, 1);
+ // draw
+ const numOfIndices = 3;
+ gl.drawArrays(gl.LINE_LOOP, 0, numOfIndices);
+ return gl;
+ };
+ try {
+ const timer = createTimer();
+ await queueEvent(timer);
+ // detect lies
+ const dataLie = lieProps['HTMLCanvasElement.toDataURL'];
+ const contextLie = lieProps['HTMLCanvasElement.getContext'];
+ const parameterOrExtensionLie = (lieProps['WebGLRenderingContext.getParameter'] ||
+ lieProps['WebGL2RenderingContext.getParameter'] ||
+ lieProps['WebGLRenderingContext.getExtension'] ||
+ lieProps['WebGL2RenderingContext.getExtension']);
+ const lied = (dataLie ||
+ contextLie ||
+ parameterOrExtensionLie ||
+ lieProps['WebGLRenderingContext.getSupportedExtensions'] ||
+ lieProps['WebGL2RenderingContext.getSupportedExtensions']) || false;
+ // create canvas context
+ let win = window;
+ if (!LIKE_BRAVE && PHANTOM_DARKNESS) {
+ win = PHANTOM_DARKNESS;
+ }
+ const doc = win.document;
+ let canvas;
+ let canvas2;
+ if ('OffscreenCanvas' in window) {
+ // @ts-ignore OffscreenCanvas
+ canvas = new win.OffscreenCanvas(256, 256);
+ // @ts-ignore OffscreenCanvas
+ canvas2 = new win.OffscreenCanvas(256, 256);
+ }
+ else {
+ canvas = doc.createElement('canvas');
+ canvas2 = doc.createElement('canvas');
+ }
+ const getContext = (canvas, contextType) => {
+ try {
+ if (contextType == 'webgl2') {
+ return (canvas.getContext('webgl2') ||
+ canvas.getContext('experimental-webgl2'));
+ }
+ return (canvas.getContext('webgl') ||
+ canvas.getContext('experimental-webgl') ||
+ canvas.getContext('moz-webgl') ||
+ canvas.getContext('webkit-3d'));
+ }
+ catch (error) {
+ return;
+ }
+ };
+ const gl = getContext(canvas, 'webgl');
+ const gl2 = getContext(canvas2, 'webgl2');
+ if (!gl) {
+ logTestResult({ test: 'webgl', passed: false });
+ return;
+ }
+ // helpers
+ const getShaderPrecisionFormat = (gl, shaderType) => {
+ if (!gl) {
+ return;
+ }
+ const LOW_FLOAT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.LOW_FLOAT));
+ const MEDIUM_FLOAT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.MEDIUM_FLOAT));
+ const HIGH_FLOAT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.HIGH_FLOAT));
+ const HIGH_INT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.HIGH_INT));
+ return {
+ LOW_FLOAT,
+ MEDIUM_FLOAT,
+ HIGH_FLOAT,
+ HIGH_INT,
+ };
+ };
+ const getShaderData = (name, shader) => {
+ const data = {};
+ // eslint-disable-next-line guard-for-in
+ for (const prop in shader) {
+ const obj = shader[prop];
+ data[name + '.' + prop + '.precision'] = obj ? attempt(() => obj.precision) : undefined;
+ data[name + '.' + prop + '.rangeMax'] = obj ? attempt(() => obj.rangeMax) : undefined;
+ data[name + '.' + prop + '.rangeMin'] = obj ? attempt(() => obj.rangeMin) : undefined;
+ }
+ return data;
+ };
+ const getMaxAnisotropy = (gl) => {
+ if (!gl) {
+ return;
+ }
+ const ext = (gl.getExtension('EXT_texture_filter_anisotropic') ||
+ gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
+ gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic'));
+ return ext ? gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT) : undefined;
+ };
+ const getParams = (gl) => {
+ if (!gl) {
+ return {};
+ }
+ const pnamesShortList = new Set(getParamNames());
+ const pnames = Object.getOwnPropertyNames(Object.getPrototypeOf(gl))
+ // .filter(prop => prop.toUpperCase() == prop) // global test
+ .filter((name) => pnamesShortList.has(name));
+ return pnames.reduce((acc, name) => {
+ const val = gl.getParameter(gl[name]);
+ if (!!val && 'buffer' in Object.getPrototypeOf(val)) {
+ acc[name] = [...val];
+ }
+ else {
+ acc[name] = val;
+ }
+ return acc;
+ }, {});
+ };
+ const getUnmasked = (gl) => {
+ const ext = !!gl ? gl.getExtension('WEBGL_debug_renderer_info') : null;
+ return !ext ? {} : {
+ UNMASKED_VENDOR_WEBGL: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL),
+ UNMASKED_RENDERER_WEBGL: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL),
+ };
+ };
+ const getSupportedExtensions = (gl) => {
+ if (!gl) {
+ return [];
+ }
+ const ext = attempt(() => gl.getSupportedExtensions());
+ if (!ext) {
+ return [];
+ }
+ return ext;
+ };
+ const getWebGLData = (gl, contextType) => {
+ if (!gl) {
+ return {
+ dataURI: undefined,
+ pixels: undefined,
+ };
+ }
+ try {
+ draw(gl);
+ const { drawingBufferWidth, drawingBufferHeight } = gl;
+ let dataURI = '';
+ if (gl.canvas.constructor.name === 'OffscreenCanvas') {
+ const canvas = document.createElement('canvas');
+ draw(getContext(canvas, contextType));
+ dataURI = canvas.toDataURL();
+ }
+ else {
+ dataURI = gl.canvas.toDataURL();
+ }
+ // reduce excessive reads to improve performance
+ const width = drawingBufferWidth / 15;
+ const height = drawingBufferHeight / 6;
+ const pixels = new Uint8Array(width * height * 4);
+ try {
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+ }
+ catch (error) {
+ return {
+ dataURI,
+ pixels: undefined,
+ };
+ }
+ // console.log([...pixels].filter(x => !!x)) // test read
+ return {
+ dataURI,
+ pixels: [...pixels],
+ };
+ }
+ catch (error) {
+ return captureError(error);
+ }
+ };
+ // get data
+ await queueEvent(timer);
+ const params = { ...getParams(gl), ...getUnmasked(gl) };
+ const params2 = { ...getParams(gl2), ...getUnmasked(gl2) };
+ const VersionParam = {
+ ALIASED_LINE_WIDTH_RANGE: true,
+ SHADING_LANGUAGE_VERSION: true,
+ VERSION: true,
+ };
+ const mismatch = Object.keys(params2)
+ .filter((key) => !!params[key] && !VersionParam[key] && ('' + params[key] != '' + params2[key]));
+ if (mismatch.length) {
+ sendToTrash('webgl/webgl2 mirrored params mismatch', mismatch.toString());
+ }
+ await queueEvent(timer);
+ const { dataURI, pixels } = getWebGLData(gl, 'webgl') || {};
+ const { dataURI: dataURI2, pixels: pixels2 } = getWebGLData(gl2, 'webgl2') || {};
+ const data = {
+ extensions: [...getSupportedExtensions(gl), ...getSupportedExtensions(gl2)],
+ pixels,
+ pixels2,
+ dataURI,
+ dataURI2,
+ parameters: {
+ ...{ ...params, ...params2 },
+ ...{
+ antialias: gl.getContextAttributes() ? gl.getContextAttributes().antialias : undefined,
+ MAX_VIEWPORT_DIMS: attempt(() => [...gl.getParameter(gl.MAX_VIEWPORT_DIMS)]),
+ MAX_TEXTURE_MAX_ANISOTROPY_EXT: getMaxAnisotropy(gl),
+ ...getShaderData('VERTEX_SHADER', getShaderPrecisionFormat(gl, 'VERTEX_SHADER')),
+ ...getShaderData('FRAGMENT_SHADER', getShaderPrecisionFormat(gl, 'FRAGMENT_SHADER')),
+ MAX_DRAW_BUFFERS_WEBGL: attempt(() => {
+ const buffers = gl.getExtension('WEBGL_draw_buffers');
+ return buffers ? gl.getParameter(buffers.MAX_DRAW_BUFFERS_WEBGL) : undefined;
+ }),
+ },
+ },
+ parameterOrExtensionLie,
+ lied,
+ };
+ // Firewall
+ const brandCapabilities = ['00b72507', '00c1b42d', '00fe1ec9', '02b3eea3', '0461d3de', '0463627d', '057857ac', '0586e20b', '0639a81a', '087d5759', '08847ba5', '0b2d4333', '0cdb985d', '0e058699', '0eb2fc19', '0f39d057', '0f840379', '0fc123c7', '101e0582', '12e92e62', '12f8ac14', '1453d59a', '149a1efa', '166dc7c8', '16c481a6', '171831c5', '177cc258', '18579e83', '19594666', '1b251fd7', '1bfd326c', '1e8a9a79', '1ff7c7e7', '2048bc5a', '2259b706', '22d0f2cf', '230d6a0d', '23d1ce20', '2402c3d2', '24306836', '258789d0', '25a760b8', '25f9385d', '27938830', '27db292c', '2b80fd96', '2bb488da', '2c04c2eb', '2d15287f', '2f014c41', '2f582ed9', '300ee927', '33bc5492', '34270469', '3660b71f', '3740c4c7', '3999a5e1', '39ead506', '3a91d0d6', '3b724916', '3bf321b8', '3c546144', '3f9ef44c', '3fea1100', '3ff82303', '4027d193', '402e1064', '4065cd69', '43038e3d', '4503e771', '461f97e1', '464d51ac', '467b99a5', '482c81b2', '48af038f', '4962ada1', '49bf7358', '4c9e8f5d', '502c402c', '508d1625', '52e348ba', '534002ab', '5582debe', '55d3aa56', '55e821f7', '581f3282', '5831d5fd', '58871380', '58fdc720', '5a5658f1', '5a90a5f8', '5aea1af1', '5b6a17aa', '5bef9a39', '5ca55292', '5d786cef', '5ddb9237', '5ee41456', '61178f2a', '61ca8e23', '61d9464e', '61eecaae', '623c3bfd', '6248d9e3', '6294d84e', '62bf7ef1', '6346cf49', '6357365c', '66628310', '668f0f93', '66d992e8', '67995996', '6843ebbf', '6864dcb0', '6951838b', '696e1548', '698c5c2e', '6a75ae3b', '6aa1ff7e', '6b07d4f8', '6b290cd4', '6c168801', '6dfae3cb', '6e806ffc', '6edf1720', '6f81cbe7', '70859bdb', '70a095b1', '7238c5dd', '7360ebd1', '741688e4', '74daf866', '78640859', '79284c47', '794f8929', '795e5c95', '79a57aa9', '7aa13573', '7b2e5242', '7b811cdd', '7ec0ea6b', '801d73af', '802e2547', '81b9cd29', '8219e1a4', '82a9a2f1', '8428fc8e', '849ccb64', '8541aa4c', '85479b99', '8bd0b91b', '8d371161', '903c8847', '917871e7', '98aeaba9', '99b1a1c6', '99ef2c3b', '9b67b7dc', '9c6df98c', '9c814c1b', '9e2b5e94', '9fd76352', 'a1c808d5', 'a22788f8', 'a2383001', 'a26e9aa9', 'a397a568', 'a3f9ee34', 'a4b988da', 'a4d34176', 'a581f55e', 'a5a477ae', 'a9640880', 'a97d3858', 'aa73f3a4', 'ab40bece', 'ac4d4ba8', 'ad01a422', 'ade75c4f', 'ae2c4777', 'afa583bc', 'b10c2a85', 'b224cc7c', 'b2d6fc98', 'b362c2f5', 'b467620a', 'b4d40dcc', 'b504662d', 'b50edd99', 'b5494027', 'b62321c3', 'b8961d15', 'b8ea6e7f', 'bb77a469', 'bc0f9686', 'bcf7315f', 'be2dfaea', 'beffda26', 'bf06317e', 'bf610cdb', 'bfe1c212', 'c00582e9', 'c026469d', 'c04889b1', 'c04b0635', 'c04e374a', 'c05f7596', 'c07307c6', 'c092fdf8', 'c25dd065', 'c2bce496', 'c5e9a883', 'c79634c2', 'c7e37ca0', 'c93b5366', 'c9bc4ffd', 'cba1878b', 'cbeade8c', 'ce2e3d16', 'cefb72ca', 'cf9643e6', 'cfd20274', 'd05a66eb', 'd09c1c07', 'd1e76c89', 'd2172943', 'd2dc2474', 'd498797d', 'd6bf35ad', 'd734ea08', 'd860ff42', 'd8bd9e5a', 'd913dafa', 'd970d345', 'dbdbe7a4', 'dc271c35', 'dcd9a29e', 'dd67b076', 'de793ead', 'ded74044', 'df9daeb6', 'e10339b3', 'e142d1f9', 'e155c47e', 'e15afab0', 'e16bb1bb', 'e316e4c0', 'e3eff92a', 'e4569a5b', 'e574bef6', 'e5962ba3', 'e6464c9f', 'e68b5c4e', 'e796b84e', 'e8694547', 'e965d180', 'e965d541', 'e9bdc904', 'e9dbb8d5', 'ea54d525', 'ea59b343', 'ea7f90ea', 'ea8f5ad0', 'eaa13804', 'eb799d34', 'ec050bb6', 'ec928655', 'eed2e5e1', 'ef8f5db1', 'f0d5a3c7', 'f1077334', 'f221fef5', 'f2293447', 'f33d918e', 'f3c6ea11', 'f51056a1', 'f51cab9a', 'f573bb34', 'f5d19934', 'f7451c92', 'f8e65486', 'f9714b3d', 'fa994f33', 'fafa14c0', 'fc37fe1f', 'fca66520', 'fe0997b6'];
+ const capabilities = [-1056897629, -1056946782, -1073719331, -1147160399, -1147160553, -1147168724, -1147419751, -1147419753, -1147419775, -1147427826, -1147451883, -1147451901, -1147464169, -1147464177, -1147488144, -1147602934, -1147643759, -1147643872, -1147765274, -1148326739, -1148335070, -1148572354, -1148678631, -1148680509, -1148713259, -1164279890, -1164800191, -1164800478, -1332029332, -133757475, -1342154787, -134823971, -16746546, -1878102921, -1878111124, -1962893370, -1962919974, -1962928178, -2130164162, -2130164382, -2130164388, -2130164546, -2130172573, -2130659912, -2145933648, -2145941977, -2145958228, -2145966414, -2145966441, -2145966529, -2145966535, -2145966545, -2145970658, -2145974343, -2145974380, -2145974489, -2145974596, -2145974598, -2145974612, -2145974637, -2145974657, -2145974729, -2146187766, -2146232338, -2146232480, -2146232503, -2146232590, -2146232723, -2146232724, -2146236588, -2146236703, -2146237020, -2146251619, -2146251641, -2146251681, -2146253671, -2146253693, -2146277218, -2146286438, -2146286463, -2146286583, -2146319268, -2146376065, -2146379955, -2146384003, -2146384011, -2146384027, -2146384034, -2146384120, -2146384281, -2146398568, -2146400384, -2146400556, -2146400620, -2146401928, -2146417027, -2146526795, -2146526934, -2147125544, -2147128275, -2147133747, -2147133749, -2147133760, -2147134974, -2147136328, -2147142429, -2147287810, -2147287811, -2147287820, -2147287834, -2147287835, -2147287854, -2147291718, -2147291820, -2147293058, -2147295768, -2147295822, -2147295823, -2147295849, -2147295857, -2147300019, -2147304193, -2147304219, -2147306321, -2147316382, -2147316383, -2147333118, -2147336998, -2147337003, -2147337012, -2147337022, -2147344686, -2147346747, -2147361652, -2147361731, -2147361769, -2147361774, -2147361775, -2147361778, -2147361792, -2147362760, -2147365698, -2147365730, -2147365759, -2147365760, -2147365827, -2147365863, -2147373914, -2147373984, -2147374032, -2147374080, -2147378041, -2147378146, -2147382130, -2147382221, -2147382251, -2147382270, -2147382272, -2147383246, -2147385825, -2147385849, -2147386292, -2147386326, -2147387335, -2147387364, -2147389930, -2147389937, -2147389951, -2147390461, -2147394188, -2147394251, -2147394484, -2147400057, -2147406798, -2147407643, -2147407821, -2147410938, -2147410941, -2147414733, -2147414956, -2147414987, -2147415037, -2147429201, -2147429223, -2147439020, -2147440422, -2147447111, -2147447122, -2147447126, -2147447137, -2147447149, -2147447157, -2147447161, -2147447163, -2147447873, -2147447892, -2147447896, -2147447928, -2147448592, -2147453701, -2147453767, -2147453768, -2147459031, -2147461169, -2147466956, -2147466972, -2147467172, -2147470173, -2147475351, -2147475352, -638494755, -671082546, -677558160, -999987216, 1099536, 1099644, 1147714426, 1197075, 1229835, 1508998, 1509050, 1610618841, 184555483, 2146590728, 2147305224, 2147361749, 2147440438, 2147475085, 2147479181, 21667, 349912, 351513, 83625, 998804992, 998911268, 999148597, 999156922];
+ const webglParams = !data.parameters ? undefined : [
+ ...new Set(Object.values(data.parameters)
+ .filter((val) => val && typeof val != 'string')
+ .flat()
+ .map((val) => Number(val))),
+ ].sort((a, b) => (a - b));
+ const gpuBrand = getGpuBrand(data.parameters?.UNMASKED_RENDERER_WEBGL);
+ const webglParamsStr = '' + webglParams;
+ const webglBrandCapabilities = !gpuBrand || !webglParamsStr ? undefined : hashMini([gpuBrand, webglParamsStr]);
+ const webglCapabilities = !webglParams ? undefined : webglParams.reduce((acc, val, i) => acc ^ (+val + i), 0);
+ Analysis.webglParams = webglParamsStr;
+ Analysis.webglBrandCapabilities = webglBrandCapabilities;
+ Analysis.webglCapabilities = webglCapabilities;
+ const hasSusGpu = webglBrandCapabilities && !brandCapabilities.includes(webglBrandCapabilities);
+ const hasSusCapabilities = webglCapabilities && !capabilities.includes(webglCapabilities);
+ if (hasSusGpu) {
+ LowerEntropy.WEBGL = true;
+ sendToTrash('WebGLRenderingContext.getParameter', 'suspicious gpu');
+ }
+ if (hasSusCapabilities) {
+ LowerEntropy.WEBGL = true;
+ sendToTrash('WebGLRenderingContext.getParameter', 'suspicious capabilities');
+ }
+ logTestResult({ time: timer.stop(), test: 'webgl', passed: true });
+ return {
+ ...data,
+ gpu: {
+ ...(getWebGLRendererConfidence((data.parameters || {}).UNMASKED_RENDERER_WEBGL) || {}),
+ compressedGPU: compressWebGLRenderer((data.parameters || {}).UNMASKED_RENDERER_WEBGL),
+ },
+ };
+ }
+ catch (error) {
+ logTestResult({ test: 'webgl', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function webglHTML(fp) {
+ if (!fp.canvasWebgl) {
+ return `
+
+
WebGL
+
images: ${HTMLNote.BLOCKED}
+
pixels: ${HTMLNote.BLOCKED}
+
params (0): ${HTMLNote.BLOCKED}
+
exts (0): ${HTMLNote.BLOCKED}
+
gpu:
+
${HTMLNote.BLOCKED}
+
+
`;
+ }
+ const { canvasWebgl: data } = fp;
+ const id = 'creep-canvas-webgl';
+ const { $hash, dataURI, dataURI2, pixels, pixels2, lied, extensions, parameters, gpu, } = data || {};
+ const { parts, warnings, gibbers, confidence, grade: confidenceGrade, compressedGPU, } = gpu || {};
+ const paramKeys = parameters ? Object.keys(parameters).sort() : [];
+ return `
+
+
+
${performanceLogger.getLog().webgl}
+
WebGL ${hashSlice($hash)}
+
images:${!dataURI ? ' ' + HTMLNote.BLOCKED : `${hashMini(dataURI)} ${!dataURI2 || dataURI == dataURI2 ? '' : `${hashMini(dataURI2)} `}`}
+
pixels:${!pixels ? ' ' + HTMLNote.BLOCKED : `${hashSlice(pixels)} ${!pixels2 || pixels == pixels2 ? '' : `${hashSlice(pixels2)} `}`}
+
params (${count(paramKeys)}): ${!paramKeys.length ? HTMLNote.BLOCKED :
+ modal(`${id}-parameters`, paramKeys.map((key) => `${key}: ${parameters[key]}`).join(' '), hashMini(parameters))}
+
exts (${count(extensions)}): ${!extensions.length ? HTMLNote.BLOCKED :
+ modal(`${id}-extensions`, extensions.sort().join(' '), hashMini(extensions))}
+
+
gpu:${confidence ? `confidence: ${confidence} ` : ''}
+
+
+ ${parameters.UNMASKED_VENDOR_WEBGL ? parameters.UNMASKED_VENDOR_WEBGL : ''}
+ ${!parameters.UNMASKED_RENDERER_WEBGL ? HTMLNote.BLOCKED : ` ${parameters.UNMASKED_RENDERER_WEBGL}`}
+
+
+ ${!dataURI ? '
' : `
`}
+
+ `;
+ }
+
+ async function getWebRTCDevices() {
+ if (!navigator?.mediaDevices?.enumerateDevices)
+ return null;
+ return navigator.mediaDevices.enumerateDevices().then((devices) => {
+ return devices.map((device) => device.kind).sort();
+ });
+ }
+ const getExtensions = (sdp) => {
+ const extensions = (('' + sdp).match(/extmap:\d+ [^\n|\r]+/g) || [])
+ .map((x) => x.replace(/extmap:[^\s]+ /, ''));
+ return [...new Set(extensions)].sort();
+ };
+ const createCounter = () => {
+ let counter = 0;
+ return {
+ increment: () => counter += 1,
+ getValue: () => counter,
+ };
+ };
+ // https://webrtchacks.com/sdp-anatomy/
+ // https://tools.ietf.org/id/draft-ietf-rtcweb-sdp-08.html
+ const constructDescriptions = ({ mediaType, sdp, sdpDescriptors, rtxCounter }) => {
+ if (!('' + sdpDescriptors)) {
+ return;
+ }
+ return sdpDescriptors.reduce((descriptionAcc, descriptor) => {
+ const matcher = `(rtpmap|fmtp|rtcp-fb):${descriptor} (.+)`;
+ const formats = (sdp.match(new RegExp(matcher, 'g')) || []);
+ if (!('' + formats)) {
+ return descriptionAcc;
+ }
+ const isRtxCodec = ('' + formats).includes(' rtx/');
+ if (isRtxCodec) {
+ if (rtxCounter.getValue()) {
+ return descriptionAcc;
+ }
+ rtxCounter.increment();
+ }
+ const getLineData = (x) => x.replace(/[^\s]+ /, '');
+ const description = formats.reduce((acc, x) => {
+ const rawData = getLineData(x);
+ const data = rawData.split('/');
+ const codec = data[0];
+ const description = {};
+ if (x.includes('rtpmap')) {
+ if (mediaType == 'audio') {
+ description.channels = (+data[2]) || 1;
+ }
+ description.mimeType = `${mediaType}/${codec}`;
+ description.clockRates = [+data[1]];
+ return {
+ ...acc,
+ ...description,
+ };
+ }
+ else if (x.includes('rtcp-fb')) {
+ return {
+ ...acc,
+ feedbackSupport: [...(acc.feedbackSupport || []), rawData],
+ };
+ }
+ else if (isRtxCodec) {
+ return acc; // no sdpFmtpLine
+ }
+ return { ...acc, sdpFmtpLine: [...rawData.split(';')] };
+ }, {});
+ let shouldMerge = false;
+ const mergerAcc = descriptionAcc.map((x) => {
+ shouldMerge = x.mimeType == description.mimeType;
+ if (shouldMerge) {
+ if (x.feedbackSupport) {
+ x.feedbackSupport = [
+ ...new Set([...x.feedbackSupport, ...description.feedbackSupport]),
+ ];
+ }
+ if (x.sdpFmtpLine) {
+ x.sdpFmtpLine = [
+ ...new Set([...x.sdpFmtpLine, ...description.sdpFmtpLine]),
+ ];
+ }
+ return {
+ ...x,
+ clockRates: [
+ ...new Set([...x.clockRates, ...description.clockRates]),
+ ],
+ };
+ }
+ return x;
+ });
+ if (shouldMerge) {
+ return mergerAcc;
+ }
+ return [...descriptionAcc, description];
+ }, []);
+ };
+ const getCapabilities = (sdp) => {
+ const videoDescriptors = ((/m=video [^\s]+ [^\s]+ ([^\n|\r]+)/.exec(sdp) || [])[1] || '').split(' ');
+ const audioDescriptors = ((/m=audio [^\s]+ [^\s]+ ([^\n|\r]+)/.exec(sdp) || [])[1] || '').split(' ');
+ const rtxCounter = createCounter();
+ return {
+ audio: constructDescriptions({
+ mediaType: 'audio',
+ sdp,
+ sdpDescriptors: audioDescriptors,
+ rtxCounter,
+ }),
+ video: constructDescriptions({
+ mediaType: 'video',
+ sdp,
+ sdpDescriptors: videoDescriptors,
+ rtxCounter,
+ }),
+ };
+ };
+ const getIPAddress = (sdp) => {
+ const blocked = '0.0.0.0';
+ const candidateEncoding = /((udp|tcp)\s)((\d|\w)+\s)((\d|\w|(\.|\:))+)(?=\s)/ig;
+ const connectionLineEncoding = /(c=IN\s)(.+)\s/ig;
+ const connectionLineIpAddress = ((sdp.match(connectionLineEncoding) || [])[0] || '').trim().split(' ')[2];
+ if (connectionLineIpAddress && (connectionLineIpAddress != blocked)) {
+ return connectionLineIpAddress;
+ }
+ const candidateIpAddress = ((sdp.match(candidateEncoding) || [])[0] || '').split(' ')[2];
+ return candidateIpAddress && (candidateIpAddress != blocked) ? candidateIpAddress : undefined;
+ };
+ async function getWebRTCData() {
+ return new Promise(async (resolve) => {
+ if (!window.RTCPeerConnection) {
+ return resolve(null);
+ }
+ const config = {
+ iceCandidatePoolSize: 1,
+ iceServers: [
+ {
+ urls: [
+ 'stun:stun4.l.google.com:19302',
+ 'stun:stun3.l.google.com:19302',
+ // 'stun:stun2.l.google.com:19302',
+ // 'stun:stun1.l.google.com:19302',
+ // 'stun:stun.l.google.com:19302',
+ ],
+ },
+ ],
+ };
+ const connection = new RTCPeerConnection(config);
+ connection.createDataChannel('');
+ const options = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 };
+ const offer = await connection.createOffer(options);
+ connection.setLocalDescription(offer);
+ const { sdp } = offer || {};
+ const extensions = getExtensions(sdp);
+ const codecsSdp = getCapabilities(sdp);
+ let iceCandidate = '';
+ let foundation = '';
+ const giveUpOnIPAddress = setTimeout(() => {
+ connection.removeEventListener('icecandidate', computeCandidate);
+ connection.close();
+ if (sdp) {
+ return resolve({
+ codecsSdp,
+ extensions,
+ foundation,
+ iceCandidate,
+ });
+ }
+ return resolve(null);
+ }, 3000);
+ const computeCandidate = (event) => {
+ const { candidate, foundation: foundationProp } = event.candidate || {};
+ if (!candidate) {
+ return;
+ }
+ if (!iceCandidate) {
+ iceCandidate = candidate;
+ foundation = (/^candidate:([\w]+)/.exec(candidate) || [])[1] || '';
+ }
+ const { sdp } = connection.localDescription || {};
+ const address = getIPAddress(sdp);
+ if (!address) {
+ return;
+ }
+ const knownInterface = {
+ 842163049: 'public interface',
+ 2268587630: 'WireGuard',
+ };
+ connection.removeEventListener('icecandidate', computeCandidate);
+ clearTimeout(giveUpOnIPAddress);
+ connection.close();
+ return resolve({
+ codecsSdp,
+ extensions,
+ foundation: knownInterface[foundation] || foundation,
+ foundationProp,
+ iceCandidate,
+ address,
+ stunConnection: candidate,
+ });
+ };
+ connection.addEventListener('icecandidate', computeCandidate);
+ });
+ }
+ function webrtcHTML(webRTC, mediaDevices) {
+ if (!webRTC && !mediaDevices) {
+ return `
+
+
WebRTC
+
host connection:
+
${HTMLNote.BLOCKED}
+
foundation/ip:
+
${HTMLNote.BLOCKED}
+
+
+
sdp capabilities: ${HTMLNote.BLOCKED}
+
stun connection:
+
${HTMLNote.BLOCKED}
+
devices (0): ${HTMLNote.BLOCKED}
+
${HTMLNote.BLOCKED}
+
+ `;
+ }
+ const { codecsSdp, extensions, foundation, foundationProp, iceCandidate, address, stunConnection, } = webRTC || {};
+ const { audio, video } = codecsSdp || {};
+ const id = 'creep-webrtc';
+ const webRTCHash = hashMini({
+ codecsSdp,
+ extensions,
+ foundation,
+ foundationProp,
+ address,
+ mediaDevices,
+ });
+ const deviceMap = {
+ 'audioinput': 'mic',
+ 'audiooutput': 'audio',
+ 'videoinput': 'webcam',
+ };
+ const feedbackId = {
+ 'ccm fir': 'Codec Control Message Full Intra Request (ccm fir)',
+ 'goog-remb': 'Google\'s Receiver Estimated Maximum Bitrate (goog-remb)',
+ 'nack': 'Negative ACKs (nack)',
+ 'nack pli': 'Picture loss Indication and NACK (nack pli)',
+ 'transport-cc': 'Transport Wide Congestion Control (transport-cc)',
+ };
+ const replaceIndex = ({ list, index, replacement }) => [
+ ...list.slice(0, index),
+ replacement,
+ ...list.slice(index + 1),
+ ];
+ const mediaDevicesByType = (mediaDevices || []).reduce((acc, x) => {
+ const deviceType = deviceMap[x] || x;
+ if (!acc.includes(deviceType)) {
+ return (acc = [...acc, deviceType]);
+ }
+ else if (!deviceType.includes('dual') && (acc.filter((x) => x == deviceType) || []).length == 1) {
+ return (acc = replaceIndex({
+ list: acc,
+ index: acc.indexOf(deviceType),
+ replacement: `dual ${deviceType}`,
+ }));
+ }
+ return (acc = [...acc, deviceType]);
+ }, []);
+ const getModalTemplate = (list) => (list || []).map((x) => {
+ return `
+ ${x.mimeType}
+ Clock Rates: ${x.clockRates.sort((a, b) => b - a).join(', ')}
+ ${x.channels > 1 ? ` Channels: ${x.channels}` : ''}
+ ${x.sdpFmtpLine ? ` Format Specific Parameters: - ${x.sdpFmtpLine.sort().map((x) => x.replace('=', ': ')).join(' - ')}` : ''}
+ ${x.feedbackSupport ? ` Feedback Support: - ${x.feedbackSupport.map((x) => {
+ return feedbackId[x] || x;
+ }).sort().join(' - ')}` : ''}
+ `;
+ }).join(' ');
+ return `
+
+
WebRTC ${webRTCHash}
+
host connection:
+
${iceCandidate || HTMLNote.BLOCKED}
+
foundation/ip:
+
+
${foundation ? `type & base ip: ${foundation}` : HTMLNote.UNSUPPORTED}
+
${address ? `ip: ${address}` : HTMLNote.BLOCKED}
+
+
+
+
sdp capabilities: ${!codecsSdp ? HTMLNote.BLOCKED :
+ modal(`${id}-sdp-capabilities`, getModalTemplate(audio) +
+ ' ' + getModalTemplate(video) +
+ 'extensions ' + extensions.join(' '), hashMini({ audio, video, extensions }))}
+
stun connection:
+
${stunConnection || HTMLNote.BLOCKED}
+
devices (${count(mediaDevices)}):
+
${!mediaDevices || !mediaDevices.length ? HTMLNote.BLOCKED :
+ mediaDevicesByType.join(', ')}
+
+
+ `;
+ }
+
+ function getWindowFeatures() {
+ try {
+ const timer = createTimer();
+ timer.start();
+ const win = PHANTOM_DARKNESS || window;
+ let keys = Object.getOwnPropertyNames(win)
+ .filter((key) => !/_|\d{3,}/.test(key)); // clear out known ddg noise
+ // if Firefox, remove the 'Event' key and push to end for consistent order
+ // and disregard keys known to be missing in RFP mode
+ const firefoxKeyMovedByInspect = 'Event';
+ const varyingKeysMissingInRFP = ['PerformanceNavigationTiming', 'Performance'];
+ if (IS_GECKO) {
+ const index = keys.indexOf(firefoxKeyMovedByInspect);
+ if (index != -1) {
+ keys = keys.slice(0, index).concat(keys.slice(index + 1));
+ keys = [...keys, firefoxKeyMovedByInspect];
+ }
+ varyingKeysMissingInRFP.forEach((key) => {
+ const index = keys.indexOf(key);
+ if (index != -1) {
+ keys = keys.slice(0, index).concat(keys.slice(index + 1));
+ }
+ return keys;
+ });
+ }
+ const moz = keys.filter((key) => (/moz/i).test(key)).length;
+ const webkit = keys.filter((key) => (/webkit/i).test(key)).length;
+ const apple = keys.filter((key) => (/apple/i).test(key)).length;
+ const data = { keys, apple, moz, webkit };
+ logTestResult({ time: timer.stop(), test: 'window', passed: true });
+ return { ...data };
+ }
+ catch (error) {
+ logTestResult({ test: 'window', passed: false });
+ captureError(error);
+ return;
+ }
+ }
+ function windowFeaturesHTML(fp) {
+ if (!fp.windowFeatures) {
+ return `
+
+
Window
+
keys (0): ${HTMLNote.BLOCKED}
+
`;
+ }
+ const { windowFeatures: { $hash, keys, }, } = fp;
+ return `
+
+
${performanceLogger.getLog().window}
+
Window ${hashSlice($hash)}
+
keys (${count(keys)}): ${keys && keys.length ? modal('creep-iframe-content-window-version', keys.join(', ')) : HTMLNote.BLOCKED}
+
+ `;
+ }
+
+ !async function () {
+ const scope = await spawnWorker();
+ if (scope == 0 /* Scope.WORKER */) {
+ return;
+ }
+ const isBrave = IS_BLINK ? await braveBrowser() : false;
+ const braveMode = isBrave ? getBraveMode() : {};
+ const braveFingerprintingBlocking = isBrave && (braveMode.standard || braveMode.strict);
+ const fingerprint = async () => {
+ const timeStart = timer();
+ const fingerprintTimeStart = timer();
+ // @ts-ignore
+ const [workerScopeComputed, voicesComputed, offlineAudioContextComputed, canvasWebglComputed, canvas2dComputed, windowFeaturesComputed, htmlElementVersionComputed, cssComputed, cssMediaComputed, screenComputed, mathsComputed, consoleErrorsComputed, timezoneComputed, clientRectsComputed, fontsComputed, mediaComputed, svgComputed, resistanceComputed, intlComputed,] = await Promise.all([
+ getBestWorkerScope(),
+ getVoices(),
+ getOfflineAudioContext(),
+ getCanvasWebgl(),
+ getCanvas2d(),
+ getWindowFeatures(),
+ getHTMLElementVersion(),
+ getCSS(),
+ getCSSMedia(),
+ getScreen(),
+ getMaths(),
+ getConsoleErrors(),
+ getTimezone(),
+ getClientRects(),
+ getFonts(),
+ getMedia(),
+ getSVG(),
+ getResistance(),
+ getIntl(),
+ ]).catch((error) => console.error(error.message));
+ const navigatorComputed = await getNavigator(workerScopeComputed)
+ .catch((error) => console.error(error.message));
+ // @ts-ignore
+ const [headlessComputed, featuresComputed,] = await Promise.all([
+ getHeadlessFeatures({
+ webgl: canvasWebglComputed,
+ workerScope: workerScopeComputed,
+ }),
+ getEngineFeatures({
+ cssComputed,
+ navigatorComputed,
+ windowFeaturesComputed,
+ }),
+ ]).catch((error) => console.error(error.message));
+ // @ts-ignore
+ const [liesComputed, trashComputed, capturedErrorsComputed,] = await Promise.all([
+ getLies(),
+ getTrash(),
+ getCapturedErrors(),
+ ]).catch((error) => console.error(error.message));
+ const fingerprintTimeEnd = fingerprintTimeStart();
+ console.log(`Fingerprinting complete in ${(fingerprintTimeEnd).toFixed(2)}ms`);
+ // GPU Prediction
+ const { parameters: gpuParameter } = canvasWebglComputed || {};
+ const reducedGPUParameters = {
+ ...(braveFingerprintingBlocking ? getBraveUnprotectedParameters(gpuParameter) :
+ gpuParameter),
+ RENDERER: undefined,
+ SHADING_LANGUAGE_VERSION: undefined,
+ UNMASKED_RENDERER_WEBGL: undefined,
+ UNMASKED_VENDOR_WEBGL: undefined,
+ VERSION: undefined,
+ VENDOR: undefined,
+ };
+ // Hashing
+ const hashStartTime = timer();
+ // @ts-ignore
+ const [windowHash, headlessHash, htmlHash, cssMediaHash, cssHash, styleHash, styleSystemHash, screenHash, voicesHash, canvas2dHash, canvas2dImageHash, canvas2dPaintHash, canvas2dTextHash, canvas2dEmojiHash, canvasWebglHash, canvasWebglImageHash, canvasWebglParametersHash, pixelsHash, pixels2Hash, mathsHash, consoleErrorsHash, timezoneHash, rectsHash, domRectHash, audioHash, fontsHash, workerHash, mediaHash, mimeTypesHash, navigatorHash, liesHash, trashHash, errorsHash, svgHash, resistanceHash, intlHash, featuresHash, deviceOfTimezoneHash,] = await Promise.all([
+ hashify(windowFeaturesComputed),
+ hashify(headlessComputed),
+ hashify((htmlElementVersionComputed || {}).keys),
+ hashify(cssMediaComputed),
+ hashify(cssComputed),
+ hashify((cssComputed || {}).computedStyle),
+ hashify((cssComputed || {}).system),
+ hashify(screenComputed),
+ hashify(voicesComputed),
+ hashify(canvas2dComputed),
+ hashify((canvas2dComputed || {}).dataURI),
+ hashify((canvas2dComputed || {}).paintURI),
+ hashify((canvas2dComputed || {}).textURI),
+ hashify((canvas2dComputed || {}).emojiURI),
+ hashify(canvasWebglComputed),
+ hashify((canvasWebglComputed || {}).dataURI),
+ hashify(reducedGPUParameters),
+ ((canvasWebglComputed || {}).pixels || []).length ? hashify(canvasWebglComputed.pixels) : undefined,
+ ((canvasWebglComputed || {}).pixels2 || []).length ? hashify(canvasWebglComputed.pixels2) : undefined,
+ hashify((mathsComputed || {}).data),
+ hashify((consoleErrorsComputed || {}).errors),
+ hashify(timezoneComputed),
+ hashify(clientRectsComputed),
+ hashify([
+ (clientRectsComputed || {}).elementBoundingClientRect,
+ (clientRectsComputed || {}).elementClientRects,
+ (clientRectsComputed || {}).rangeBoundingClientRect,
+ (clientRectsComputed || {}).rangeClientRects,
+ ]),
+ hashify(offlineAudioContextComputed),
+ hashify(fontsComputed),
+ hashify(workerScopeComputed),
+ hashify(mediaComputed),
+ hashify((mediaComputed || {}).mimeTypes),
+ hashify(navigatorComputed),
+ hashify(liesComputed),
+ hashify(trashComputed),
+ hashify(capturedErrorsComputed),
+ hashify(svgComputed),
+ hashify(resistanceComputed),
+ hashify(intlComputed),
+ hashify(featuresComputed),
+ hashify((() => {
+ const { bluetoothAvailability, device, deviceMemory, hardwareConcurrency, maxTouchPoints, oscpu, platform, system, userAgentData, } = navigatorComputed || {};
+ const { architecture, bitness, mobile, model, platform: uaPlatform, platformVersion, } = userAgentData || {};
+ const { 'any-pointer': anyPointer } = cssMediaComputed?.mediaCSS || {};
+ const { colorDepth, pixelDepth, height, width } = screenComputed || {};
+ const { location, locationEpoch, zone } = timezoneComputed || {};
+ const { deviceMemory: deviceMemoryWorker, hardwareConcurrency: hardwareConcurrencyWorker, gpu, platform: platformWorker, system: systemWorker, timezoneLocation: locationWorker, userAgentData: userAgentDataWorker, } = workerScopeComputed || {};
+ const { compressedGPU, confidence } = gpu || {};
+ const { architecture: architectureWorker, bitness: bitnessWorker, mobile: mobileWorker, model: modelWorker, platform: uaPlatformWorker, platformVersion: platformVersionWorker, } = userAgentDataWorker || {};
+ return [
+ anyPointer,
+ architecture,
+ architectureWorker,
+ bitness,
+ bitnessWorker,
+ bluetoothAvailability,
+ colorDepth,
+ ...(compressedGPU && confidence != 'low' ? [compressedGPU] : []),
+ device,
+ deviceMemory,
+ deviceMemoryWorker,
+ hardwareConcurrency,
+ hardwareConcurrencyWorker,
+ height,
+ location,
+ locationWorker,
+ locationEpoch,
+ maxTouchPoints,
+ mobile,
+ mobileWorker,
+ model,
+ modelWorker,
+ oscpu,
+ pixelDepth,
+ platform,
+ platformWorker,
+ platformVersion,
+ platformVersionWorker,
+ system,
+ systemWorker,
+ uaPlatform,
+ uaPlatformWorker,
+ width,
+ zone,
+ ];
+ })()),
+ ]).catch((error) => console.error(error.message));
+ // console.log(performance.now()-start)
+ const hashTimeEnd = hashStartTime();
+ const timeEnd = timeStart();
+ console.log(`Hashing complete in ${(hashTimeEnd).toFixed(2)}ms`);
+ if (PARENT_PHANTOM) {
+ // @ts-ignore
+ PARENT_PHANTOM.parentNode.removeChild(PARENT_PHANTOM);
+ }
+ const fingerprint = {
+ workerScope: !workerScopeComputed ? undefined : { ...workerScopeComputed, $hash: workerHash },
+ navigator: !navigatorComputed ? undefined : { ...navigatorComputed, $hash: navigatorHash },
+ windowFeatures: !windowFeaturesComputed ? undefined : { ...windowFeaturesComputed, $hash: windowHash },
+ headless: !headlessComputed ? undefined : { ...headlessComputed, $hash: headlessHash },
+ htmlElementVersion: !htmlElementVersionComputed ? undefined : { ...htmlElementVersionComputed, $hash: htmlHash },
+ cssMedia: !cssMediaComputed ? undefined : { ...cssMediaComputed, $hash: cssMediaHash },
+ css: !cssComputed ? undefined : { ...cssComputed, $hash: cssHash },
+ screen: !screenComputed ? undefined : { ...screenComputed, $hash: screenHash },
+ voices: !voicesComputed ? undefined : { ...voicesComputed, $hash: voicesHash },
+ media: !mediaComputed ? undefined : { ...mediaComputed, $hash: mediaHash },
+ canvas2d: !canvas2dComputed ? undefined : { ...canvas2dComputed, $hash: canvas2dHash },
+ canvasWebgl: !canvasWebglComputed ? undefined : { ...canvasWebglComputed, pixels: pixelsHash, pixels2: pixels2Hash, $hash: canvasWebglHash },
+ maths: !mathsComputed ? undefined : { ...mathsComputed, $hash: mathsHash },
+ consoleErrors: !consoleErrorsComputed ? undefined : { ...consoleErrorsComputed, $hash: consoleErrorsHash },
+ timezone: !timezoneComputed ? undefined : { ...timezoneComputed, $hash: timezoneHash },
+ clientRects: !clientRectsComputed ? undefined : { ...clientRectsComputed, $hash: rectsHash },
+ offlineAudioContext: !offlineAudioContextComputed ? undefined : { ...offlineAudioContextComputed, $hash: audioHash },
+ fonts: !fontsComputed ? undefined : { ...fontsComputed, $hash: fontsHash },
+ lies: !liesComputed ? undefined : { ...liesComputed, $hash: liesHash },
+ trash: !trashComputed ? undefined : { ...trashComputed, $hash: trashHash },
+ capturedErrors: !capturedErrorsComputed ? undefined : { ...capturedErrorsComputed, $hash: errorsHash },
+ svg: !svgComputed ? undefined : { ...svgComputed, $hash: svgHash },
+ resistance: !resistanceComputed ? undefined : { ...resistanceComputed, $hash: resistanceHash },
+ intl: !intlComputed ? undefined : { ...intlComputed, $hash: intlHash },
+ features: !featuresComputed ? undefined : { ...featuresComputed, $hash: featuresHash },
+ };
+ return {
+ fingerprint,
+ styleSystemHash,
+ styleHash,
+ domRectHash,
+ mimeTypesHash,
+ canvas2dImageHash,
+ canvasWebglImageHash,
+ canvas2dPaintHash,
+ canvas2dTextHash,
+ canvas2dEmojiHash,
+ canvasWebglParametersHash,
+ deviceOfTimezoneHash,
+ timeEnd,
+ };
+ };
+ // fingerprint and render
+ const [{ fingerprint: fp, styleSystemHash, styleHash, domRectHash, mimeTypesHash, canvas2dImageHash, canvas2dPaintHash, canvas2dTextHash, canvas2dEmojiHash, canvasWebglImageHash, canvasWebglParametersHash, deviceOfTimezoneHash, timeEnd, },] = await Promise.all([
+ fingerprint().catch((error) => console.error(error)) || {},
+ ]);
+ if (!fp) {
+ throw new Error('Fingerprint failed!');
+ }
+ console.log('%c✔ loose fingerprint passed', 'color:#4cca9f');
+ console.groupCollapsed('Loose Fingerprint');
+ console.log(fp);
+ console.groupEnd();
+ console.groupCollapsed('Loose Fingerprint JSON');
+ console.log('diff check at https://www.diffchecker.com/diff\n\n', JSON.stringify(fp, null, '\t'));
+ console.groupEnd();
+ const hardenEntropy = (workerScope, prop) => {
+ return (!workerScope ? prop :
+ (workerScope.localeEntropyIsTrusty && workerScope.localeIntlEntropyIsTrusty) ? prop :
+ undefined);
+ };
+ const privacyResistFingerprinting = (fp.resistance && /^(tor browser|firefox)$/i.test(fp.resistance.privacy));
+ // harden gpu
+ const hardenGPU = (canvasWebgl) => {
+ const { gpu: { confidence, compressedGPU } } = canvasWebgl;
+ return (confidence == 'low' ? {} : {
+ UNMASKED_RENDERER_WEBGL: compressedGPU,
+ UNMASKED_VENDOR_WEBGL: canvasWebgl.parameters.UNMASKED_VENDOR_WEBGL,
+ });
+ };
+ const creep = {
+ navigator: (!fp.navigator || fp.navigator.lied ? undefined : {
+ bluetoothAvailability: fp.navigator.bluetoothAvailability,
+ device: fp.navigator.device,
+ deviceMemory: fp.navigator.deviceMemory,
+ hardwareConcurrency: fp.navigator.hardwareConcurrency,
+ maxTouchPoints: fp.navigator.maxTouchPoints,
+ oscpu: fp.navigator.oscpu,
+ platform: fp.navigator.platform,
+ system: fp.navigator.system,
+ userAgentData: {
+ ...(fp.navigator.userAgentData || {}),
+ // loose
+ brandsVersion: undefined,
+ uaFullVersion: undefined,
+ },
+ vendor: fp.navigator.vendor,
+ }),
+ screen: (!fp.screen || fp.screen.lied || privacyResistFingerprinting || LowerEntropy.SCREEN ? undefined :
+ hardenEntropy(fp.workerScope, {
+ height: fp.screen.height,
+ width: fp.screen.width,
+ pixelDepth: fp.screen.pixelDepth,
+ colorDepth: fp.screen.colorDepth,
+ lied: fp.screen.lied,
+ })),
+ workerScope: !fp.workerScope || fp.workerScope.lied ? undefined : {
+ deviceMemory: (braveFingerprintingBlocking ? undefined : fp.workerScope.deviceMemory),
+ hardwareConcurrency: (braveFingerprintingBlocking ? undefined : fp.workerScope.hardwareConcurrency),
+ // system locale in blink
+ language: !LowerEntropy.TIME_ZONE ? fp.workerScope.language : undefined,
+ platform: fp.workerScope.platform,
+ system: fp.workerScope.system,
+ device: fp.workerScope.device,
+ timezoneLocation: (!LowerEntropy.TIME_ZONE ?
+ hardenEntropy(fp.workerScope, fp.workerScope.timezoneLocation) :
+ undefined),
+ webglRenderer: ((fp.workerScope.gpu.confidence != 'low') ? fp.workerScope.gpu.compressedGPU : undefined),
+ webglVendor: ((fp.workerScope.gpu.confidence != 'low') ? fp.workerScope.webglVendor : undefined),
+ userAgentData: {
+ ...fp.workerScope.userAgentData,
+ // loose
+ brandsVersion: undefined,
+ uaFullVersion: undefined,
+ },
+ },
+ media: fp.media,
+ canvas2d: ((canvas2d) => {
+ if (!canvas2d) {
+ return;
+ }
+ const { lied, liedTextMetrics } = canvas2d;
+ let data;
+ if (!lied) {
+ const { dataURI, paintURI, textURI, emojiURI } = canvas2d;
+ data = {
+ lied,
+ ...{ dataURI, paintURI, textURI, emojiURI },
+ };
+ }
+ if (!liedTextMetrics) {
+ const { textMetricsSystemSum, emojiSet } = canvas2d;
+ data = {
+ ...(data || {}),
+ ...{ textMetricsSystemSum, emojiSet },
+ };
+ }
+ return data;
+ })(fp.canvas2d),
+ canvasWebgl: (!fp.canvasWebgl || fp.canvasWebgl.lied || LowerEntropy.WEBGL) ? undefined : (braveFingerprintingBlocking ? {
+ parameters: {
+ ...getBraveUnprotectedParameters(fp.canvasWebgl.parameters),
+ ...hardenGPU(fp.canvasWebgl),
+ },
+ } : {
+ ...((gl, canvas2d) => {
+ if ((canvas2d && canvas2d.lied) || LowerEntropy.CANVAS) {
+ // distrust images
+ const { extensions, gpu, lied, parameterOrExtensionLie } = gl;
+ return {
+ extensions,
+ gpu,
+ lied,
+ parameterOrExtensionLie,
+ };
+ }
+ return gl;
+ })(fp.canvasWebgl, fp.canvas2d),
+ parameters: {
+ ...fp.canvasWebgl.parameters,
+ ...hardenGPU(fp.canvasWebgl),
+ },
+ }),
+ cssMedia: !fp.cssMedia ? undefined : {
+ reducedMotion: caniuse(() => fp.cssMedia.mediaCSS['prefers-reduced-motion']),
+ colorScheme: (braveFingerprintingBlocking ? undefined :
+ caniuse(() => fp.cssMedia.mediaCSS['prefers-color-scheme'])),
+ monochrome: caniuse(() => fp.cssMedia.mediaCSS.monochrome),
+ invertedColors: caniuse(() => fp.cssMedia.mediaCSS['inverted-colors']),
+ forcedColors: caniuse(() => fp.cssMedia.mediaCSS['forced-colors']),
+ anyHover: caniuse(() => fp.cssMedia.mediaCSS['any-hover']),
+ hover: caniuse(() => fp.cssMedia.mediaCSS.hover),
+ anyPointer: caniuse(() => fp.cssMedia.mediaCSS['any-pointer']),
+ pointer: caniuse(() => fp.cssMedia.mediaCSS.pointer),
+ colorGamut: caniuse(() => fp.cssMedia.mediaCSS['color-gamut']),
+ screenQuery: (privacyResistFingerprinting || (LowerEntropy.SCREEN || LowerEntropy.IFRAME_SCREEN) ?
+ undefined :
+ hardenEntropy(fp.workerScope, caniuse(() => fp.cssMedia.screenQuery))),
+ },
+ css: !fp.css ? undefined : fp.css.system.fonts,
+ timezone: !fp.timezone || fp.timezone.lied || LowerEntropy.TIME_ZONE ? undefined : {
+ locationMeasured: hardenEntropy(fp.workerScope, fp.timezone.locationMeasured),
+ lied: fp.timezone.lied,
+ },
+ offlineAudioContext: !fp.offlineAudioContext ? undefined : (fp.offlineAudioContext.lied || LowerEntropy.AUDIO ? undefined :
+ fp.offlineAudioContext),
+ fonts: !fp.fonts || fp.fonts.lied || LowerEntropy.FONTS ? undefined : fp.fonts.fontFaceLoadFonts,
+ forceRenew: 1737085481442,
+ };
+ console.log('%c✔ stable fingerprint passed', 'color:#4cca9f');
+ console.groupCollapsed('Stable Fingerprint');
+ console.log(creep);
+ console.groupEnd();
+ console.groupCollapsed('Stable Fingerprint JSON');
+ console.log('diff check at https://www.diffchecker.com/diff\n\n', JSON.stringify(creep, null, '\t'));
+ console.groupEnd();
+ const [fpHash, creepHash] = await Promise.all([hashify(fp), hashify(creep)]).catch((error) => {
+ console.error(error.message);
+ }) || [];
+ const blankFingerprint = '0000000000000000000000000000000000000000000000000000000000000000';
+ const el = document.getElementById('fingerprint-data');
+ patch(el, html `
+
+
+
+
+
WebRTC
+
host connection:
+
+ candidate:0000000000 1 udp 9353978903 93549af7-47d4-485c-a57a-751a3d213876.local 56518 typ host generation 0 ufrag bk84 network-cost 999
+
+
foundation/ip:
+
+
0000000000
+
000.000.000.000
+
+
+
+
capabilities:
+
stun connection:
+
+ candidate:0000000000 1 udp 9353978903 93549af7-47d4-485c-a57a-751a3d213876.local 56518 typ host generation 0 ufrag bk84 network-cost 999
+
+
devices (0):
+
mic, audio, webcam
+
+
+
+ ${timezoneHTML(fp)}
+ ${intlHTML(fp)}
+
+
+ ${headlessFeaturesHTML(fp)}
+ ${resistanceHTML(fp)}
+
+
${workerScopeHTML(fp)}
+
+ ${webglHTML(fp)}
+ ${screenHTML(fp)}
+
+
+ ${canvasHTML(fp)}
+ ${fontsHTML(fp)}
+
+
+ ${clientRectsHTML(fp)}
+ ${svgHTML(fp)}
+
+
+ ${audioHTML(fp)}
+ ${voicesHTML(fp)}
+ ${mediaHTML(fp)}
+
+
${featuresHTML(fp)}
+
+ ${cssMediaHTML(fp)}
+ ${cssHTML(fp)}
+
+
+
+ ${mathsHTML(fp)}
+ ${consoleErrorsHTML(fp)}
+
+
+ ${windowFeaturesHTML(fp)}
+ ${htmlElementVersionHTML(fp)}
+
+
+
${navigatorHTML(fp)}
+
+
+
+ `, async () => {
+ // send analysis fingerprint
+ Promise.all([
+ getWebRTCData(),
+ getWebRTCDevices(),
+ getStatus(),
+ ]).then(async (data) => {
+ const [webRTC, mediaDevices, status] = data || [];
+ patch(document.getElementById('webrtc-connection'), html `
+
+ ${webrtcHTML(webRTC, mediaDevices)}
+
+ `);
+ patch(document.getElementById('status-info'), html `
+
+ ${statusHTML(status)}
+
+ `);
+ }).catch((err) => console.error(err));
+ // expose results to the window
+ // @ts-expect-error does not exist
+ window.Fingerprint = JSON.parse(JSON.stringify(fp));
+ // @ts-expect-error does not exist
+ window.Creep = JSON.parse(JSON.stringify(creep));
+ const fuzzyFingerprint = await getFuzzyHash(fp);
+ const fuzzyFpEl = document.getElementById('fuzzy-fingerprint');
+ patch(fuzzyFpEl, html `
+
+
Fuzzy: ${fuzzyFingerprint}
+
+ `);
+ // Display fingerprint
+ const rand = (min, max) => Math.floor(Math.random() * (max - min + 1) + min);
+ setTimeout(() => {
+ patch(document.getElementById('creep-fingerprint'), html `
+ FP ID: ${creepHash?.split('').map((x, i) => {
+ return `${x} `;
+ }).join('')}
+ `);
+ }, 50);
+ });
+ }();
+
+})();
diff --git a/tests/vendor/fpscanner-1.0.6.es.js b/tests/vendor/fpscanner-1.0.6.es.js
new file mode 100644
index 0000000..4df30e4
--- /dev/null
+++ b/tests/vendor/fpscanner-1.0.6.es.js
@@ -0,0 +1,1253 @@
+function ie() {
+ return navigator.webdriver;
+}
+function ae() {
+ return navigator.userAgent;
+}
+function oe() {
+ return navigator.platform;
+}
+const l = "ERROR", r = "INIT", s = "NA", v = "SKIPPED", h = "high", S = "low", se = "medium";
+function f(t) {
+ let e = 0;
+ for (let n = 0, i = t.length; n < i; n++) {
+ let a = t.charCodeAt(n);
+ e = (e << 5) - e + a, e |= 0;
+ }
+ return e.toString(16).padStart(8, "0");
+}
+function d(t, e) {
+ for (const n in t)
+ t[n] = e;
+}
+function ce() {
+ return navigator.buildID === "20181001000000";
+}
+function le() {
+ try {
+ let t = !1;
+ const e = Error.prepareStackTrace;
+ Error.prepareStackTrace = function() {
+ return t = !0, e;
+ };
+ const n = new Error("");
+ return console.log(n), t;
+ } catch {
+ return l;
+ }
+}
+function ue() {
+ const t = {
+ vendor: r,
+ renderer: r
+ };
+ if (ce())
+ return d(t, s), t;
+ try {
+ var e = document.createElement("canvas"), n = e.getContext("webgl") || e.getContext("experimental-webgl");
+ n.getSupportedExtensions().indexOf("WEBGL_debug_renderer_info") >= 0 ? (t.vendor = n.getParameter(n.getExtension("WEBGL_debug_renderer_info").UNMASKED_VENDOR_WEBGL), t.renderer = n.getParameter(n.getExtension("WEBGL_debug_renderer_info").UNMASKED_RENDERER_WEBGL)) : d(t, s);
+ } catch {
+ d(t, l);
+ }
+ return t;
+}
+function de() {
+ return "__pwInitScripts" in window || "__playwright__binding__" in window;
+}
+function ge() {
+ return navigator.hardwareConcurrency || s;
+}
+function he() {
+ const t = [], e = 0.123456789;
+ return ["E", "LN10", "LN2", "LOG10E", "LOG2E", "PI", "SQRT1_2", "SQRT2"].forEach(function(a) {
+ try {
+ t.push(Math[a]);
+ } catch {
+ t.push(-1);
+ }
+ }), ["tan", "sin", "exp", "atan", "acosh", "asinh", "atanh", "expm1", "log1p", "sinh"].forEach(function(a) {
+ try {
+ t.push(Math[a](e));
+ } catch {
+ t.push(-1);
+ }
+ }), "sumPrecise" in Math ? t.push(Math.sumPrecise([1e20, 0.1, -1e20])) : t.push(-1), f(t.map(String).join(","));
+}
+function me() {
+ return navigator.deviceMemory || s;
+}
+function pe() {
+ return eval.toString().length;
+}
+function fe() {
+ const t = {
+ timezone: r,
+ localeLanguage: r
+ };
+ try {
+ if (typeof Intl < "u" && typeof Intl.DateTimeFormat < "u") {
+ const e = Intl.DateTimeFormat().resolvedOptions();
+ t.timezone = e.timeZone, t.localeLanguage = e.locale;
+ } else
+ t.timezone = s, t.localeLanguage = s;
+ } catch {
+ t.timezone = l, t.localeLanguage = l;
+ }
+ return t;
+}
+function ve() {
+ return {
+ width: window.screen.width,
+ height: window.screen.height,
+ pixelDepth: window.screen.pixelDepth,
+ colorDepth: window.screen.colorDepth,
+ availableWidth: window.screen.availWidth,
+ availableHeight: window.screen.availHeight,
+ innerWidth: window.innerWidth,
+ innerHeight: window.innerHeight,
+ hasMultipleDisplays: typeof screen.isExtended < "u" ? screen.isExtended : s
+ };
+}
+function ye() {
+ return {
+ languages: navigator.languages,
+ language: navigator.language
+ };
+}
+async function we() {
+ const t = {
+ vendor: r,
+ architecture: r,
+ device: r,
+ description: r
+ };
+ if ("gpu" in navigator)
+ try {
+ const e = await navigator.gpu.requestAdapter();
+ e && (t.vendor = e.info.vendor, t.architecture = e.info.architecture, t.device = e.info.device, t.description = e.info.description);
+ } catch {
+ d(t, l);
+ }
+ else
+ d(t, s);
+ return t;
+}
+function be() {
+ const t = [
+ "__driver_evaluate",
+ "__webdriver_evaluate",
+ "__selenium_evaluate",
+ "__fxdriver_evaluate",
+ "__driver_unwrapped",
+ "__webdriver_unwrapped",
+ "__selenium_unwrapped",
+ "__fxdriver_unwrapped",
+ "_Selenium_IDE_Recorder",
+ "_selenium",
+ "calledSelenium",
+ "$cdc_asdjflasutopfhvcZLmcfl_",
+ "$chrome_asyncScriptInfo",
+ "__$webdriverAsyncExecutor",
+ "webdriver",
+ "__webdriverFunc",
+ "domAutomation",
+ "domAutomationController",
+ "__lastWatirAlert",
+ "__lastWatirConfirm",
+ "__lastWatirPrompt",
+ "__webdriver_script_fn",
+ "_WEBDRIVER_ELEM_CACHE"
+ ];
+ let e = !1;
+ for (let n = 0; n < t.length; n++)
+ if (t[n] in window) {
+ e = !0;
+ break;
+ }
+ return e = e || !!document.__webdriver_script_fn || !!window.domAutomation || !!window.domAutomationController, e;
+}
+function Se() {
+ try {
+ const t = "webdriver", e = window.navigator;
+ if (!e[t] && !e.hasOwnProperty(t)) {
+ e[t] = 1;
+ const n = e[t] === 1;
+ return delete e[t], n;
+ }
+ return !0;
+ } catch {
+ return !1;
+ }
+}
+async function Ce() {
+ const t = window.navigator, e = {
+ architecture: r,
+ bitness: r,
+ brands: r,
+ mobile: r,
+ model: r,
+ platform: r,
+ platformVersion: r,
+ uaFullVersion: r
+ };
+ if ("userAgentData" in t)
+ try {
+ const n = await t.userAgentData.getHighEntropyValues([
+ "architecture",
+ "bitness",
+ "brands",
+ "mobile",
+ "model",
+ "platform",
+ "platformVersion",
+ "uaFullVersion"
+ ]);
+ e.architecture = n.architecture, e.bitness = n.bitness, e.brands = n.brands, e.mobile = n.mobile, e.model = n.model, e.platform = n.platform, e.platformVersion = n.platformVersion, e.uaFullVersion = n.uaFullVersion;
+ } catch {
+ d(e, l);
+ }
+ else
+ d(e, s);
+ return e;
+}
+function Ae() {
+ if (!navigator.plugins) return !1;
+ const t = typeof navigator.plugins.toString == "function" ? navigator.plugins.toString() : navigator.plugins.constructor && typeof navigator.plugins.constructor.toString == "function" ? navigator.plugins.constructor.toString() : typeof navigator.plugins;
+ return t === "[object PluginArray]" || t === "[object MSPluginsCollection]" || t === "[object HTMLPluginsCollection]";
+}
+function Pe() {
+ if (!navigator.plugins) return s;
+ const t = [];
+ for (let e = 0; e < navigator.plugins.length; e++)
+ t.push(navigator.plugins[e].name);
+ return f(t.join(","));
+}
+function ke() {
+ return navigator.plugins ? navigator.plugins.length : s;
+}
+function Me() {
+ if (!navigator.plugins) return s;
+ try {
+ return navigator.plugins[0] === navigator.plugins[0][0].enabledPlugin;
+ } catch {
+ return l;
+ }
+}
+function xe() {
+ if (!navigator.plugins) return s;
+ try {
+ return navigator.plugins.item(4294967296) !== navigator.plugins[0];
+ } catch {
+ return l;
+ }
+}
+function We() {
+ const t = {
+ isValidPluginArray: r,
+ pluginCount: r,
+ pluginNamesHash: r,
+ pluginConsistency1: r,
+ pluginOverflow: r
+ };
+ try {
+ t.isValidPluginArray = Ae(), t.pluginCount = ke(), t.pluginNamesHash = Pe(), t.pluginConsistency1 = Me(), t.pluginOverflow = xe();
+ } catch {
+ d(t, l);
+ }
+ return t;
+}
+async function Ee() {
+ return new Promise(async function(t) {
+ var e = {
+ audiooutput: 0,
+ audioinput: 0,
+ videoinput: 0
+ };
+ if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
+ const a = await navigator.mediaDevices.enumerateDevices();
+ if (typeof a < "u") {
+ for (var n = 0; n < a.length; n++) {
+ var i = a[n].kind;
+ e[i] = e[i] + 1;
+ }
+ return t({
+ speakers: e.audiooutput,
+ microphones: e.audioinput,
+ webcams: e.videoinput
+ });
+ } else
+ return d(e, s), t(e);
+ } else
+ return d(e, s), t(e);
+ });
+}
+function De() {
+ const t = {
+ webdriver: r,
+ userAgent: r,
+ platform: r,
+ memory: r,
+ cpuCount: r,
+ language: r
+ }, e = document.createElement("iframe");
+ let n = !1;
+ try {
+ e.style.display = "none", e.src = "about:blank", document.body.appendChild(e), n = !0;
+ const i = e.contentWindow?.navigator;
+ t.webdriver = i.webdriver ?? !1, t.userAgent = i.userAgent ?? s, t.platform = i.platform ?? s, t.memory = i.deviceMemory ?? s, t.cpuCount = i.hardwareConcurrency ?? s, t.language = i.language ?? s;
+ } catch {
+ d(t, l);
+ } finally {
+ if (n)
+ try {
+ document.body.removeChild(e);
+ } catch {
+ }
+ }
+ return t;
+}
+async function _e() {
+ return new Promise((t) => {
+ const e = {
+ vendor: r,
+ renderer: r,
+ userAgent: r,
+ language: r,
+ platform: r,
+ memory: r,
+ cpuCount: r
+ };
+ let n = null, i = null, a = null;
+ const g = () => {
+ a && clearTimeout(a), n && n.terminate(), i && URL.revokeObjectURL(i);
+ };
+ try {
+ const p = `var fingerprintWorker = {
+ userAgent: 'NA',
+ language: 'NA',
+ cpuCount: 'NA',
+ platform: 'NA',
+ memory: 'NA',
+ vendor: 'NA',
+ renderer: 'NA'
+ };
+ try {
+ fingerprintWorker.userAgent = navigator.userAgent;
+ fingerprintWorker.language = navigator.language;
+ fingerprintWorker.cpuCount = navigator.hardwareConcurrency;
+ fingerprintWorker.platform = navigator.platform;
+ if (typeof navigator.deviceMemory !== 'undefined') {
+ fingerprintWorker.memory = navigator.deviceMemory;
+ }
+
+ try {
+ if (typeof OffscreenCanvas === 'undefined') {
+ fingerprintWorker.vendor = 'NA';
+ fingerprintWorker.renderer = 'NA';
+ } else {
+ var canvas = new OffscreenCanvas(1, 1);
+ var gl = canvas.getContext('webgl');
+ var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
+ if (gl && !isFirefox) {
+ var glExt = gl.getExtension('WEBGL_debug_renderer_info');
+ if (glExt) {
+ fingerprintWorker.vendor = gl.getParameter(glExt.UNMASKED_VENDOR_WEBGL);
+ fingerprintWorker.renderer = gl.getParameter(glExt.UNMASKED_RENDERER_WEBGL);
+ } else {
+ fingerprintWorker.vendor = 'NA';
+ fingerprintWorker.renderer = 'NA';
+ }
+ } else {
+ fingerprintWorker.vendor = 'NA';
+ fingerprintWorker.renderer = 'NA';
+ }
+ }
+ } catch (_) {
+ fingerprintWorker.vendor = 'ERROR';
+ fingerprintWorker.renderer = 'ERROR';
+ }
+ self.postMessage(fingerprintWorker);
+ } catch (e) {
+ self.postMessage(fingerprintWorker);
+ }`, y = new Blob([p], { type: "application/javascript" });
+ i = URL.createObjectURL(y), n = new Worker(i), a = window.setTimeout(() => {
+ g(), d(e, l), t(e);
+ }, 2e3), n.onmessage = function(o) {
+ try {
+ const m = (w) => typeof w > "u" ? s : w;
+ e.vendor = m(o.data.vendor), e.renderer = m(o.data.renderer), e.userAgent = m(o.data.userAgent), e.language = m(o.data.language), e.platform = m(o.data.platform), e.memory = m(o.data.memory), e.cpuCount = m(o.data.cpuCount);
+ } catch {
+ d(e, l);
+ } finally {
+ g(), t(e);
+ }
+ }, n.onerror = function() {
+ g(), d(e, l), t(e);
+ };
+ } catch {
+ g(), d(e, l), t(e);
+ }
+ });
+}
+function Re() {
+ const t = {
+ toSourceError: r,
+ hasToSource: !1
+ };
+ try {
+ null.usdfsh;
+ } catch (e) {
+ t.toSourceError = e.toString();
+ }
+ try {
+ throw "xyz";
+ } catch (e) {
+ try {
+ e.toSource(), t.hasToSource = !0;
+ } catch {
+ t.hasToSource = !1;
+ }
+ }
+ return t;
+}
+const C = [
+ 'audio/mp4; codecs="mp4a.40.2"',
+ "audio/mpeg;",
+ 'audio/webm; codecs="vorbis"',
+ 'audio/ogg; codecs="vorbis"',
+ 'audio/wav; codecs="1"',
+ 'audio/ogg; codecs="speex"',
+ 'audio/ogg; codecs="flac"',
+ 'audio/3gpp; codecs="samr"'
+], A = [
+ 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"',
+ 'video/mp4; codecs="avc1.42E01E"',
+ 'video/mp4; codecs="avc1.58A01E"',
+ 'video/mp4; codecs="avc1.4D401E"',
+ 'video/mp4; codecs="avc1.64001E"',
+ 'video/mp4; codecs="mp4v.20.8"',
+ 'video/mp4; codecs="mp4v.20.240"',
+ 'video/webm; codecs="vp8"',
+ 'video/ogg; codecs="theora"',
+ 'video/ogg; codecs="dirac"',
+ 'video/3gpp; codecs="mp4v.20.8"',
+ 'video/x-matroska; codecs="theora"'
+];
+function P(t, e) {
+ const n = {};
+ try {
+ const i = document.createElement(e);
+ for (const a of t)
+ try {
+ n[a] = i.canPlayType(a) || null;
+ } catch {
+ n[a] = null;
+ }
+ } catch {
+ for (const i of t)
+ n[i] = null;
+ }
+ return n;
+}
+function k(t) {
+ const e = {}, n = window.MediaSource;
+ if (!n || typeof n.isTypeSupported != "function") {
+ for (const i of t)
+ e[i] = null;
+ return e;
+ }
+ for (const i of t)
+ try {
+ e[i] = n.isTypeSupported(i);
+ } catch {
+ e[i] = null;
+ }
+ return e;
+}
+function M(t) {
+ try {
+ const e = window.RTCRtpReceiver;
+ if (e && typeof e.getCapabilities == "function") {
+ const n = e.getCapabilities(t);
+ return f(JSON.stringify(n));
+ }
+ return s;
+ } catch {
+ return l;
+ }
+}
+function Ie() {
+ const t = {
+ audioCanPlayTypeHash: s,
+ videoCanPlayTypeHash: s,
+ audioMediaSourceHash: s,
+ videoMediaSourceHash: s,
+ rtcAudioCapabilitiesHash: s,
+ rtcVideoCapabilitiesHash: s,
+ hasMediaSource: !1
+ };
+ try {
+ t.hasMediaSource = !!window.MediaSource;
+ const e = P(C, "audio"), n = P(A, "video");
+ t.audioCanPlayTypeHash = f(JSON.stringify(e)), t.videoCanPlayTypeHash = f(JSON.stringify(n));
+ const i = k(C), a = k(A);
+ t.audioMediaSourceHash = f(JSON.stringify(i)), t.videoMediaSourceHash = f(JSON.stringify(a)), t.rtcAudioCapabilitiesHash = M("audio"), t.rtcVideoCapabilitiesHash = M("video");
+ } catch {
+ d(t, l);
+ }
+ return t;
+}
+async function Le() {
+ return new Promise((t) => {
+ try {
+ const e = new Image(), n = document.createElement("canvas").getContext("2d");
+ e.onload = () => {
+ n.drawImage(e, 0, 0), t(n.getImageData(0, 0, 1, 1).data.filter((i) => i === 0).length != 4);
+ }, e.onerror = () => {
+ t(l);
+ }, e.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=";
+ } catch {
+ t(l);
+ }
+ });
+}
+function Te() {
+ var t = document.createElement("canvas");
+ t.width = 400, t.height = 200, t.style.display = "inline";
+ var e = t.getContext("2d");
+ try {
+ return e.rect(0, 0, 10, 10), e.rect(2, 2, 6, 6), e.textBaseline = "alphabetic", e.fillStyle = "#f60", e.fillRect(125, 1, 62, 20), e.fillStyle = "#069", e.font = "11pt no-real-font-123", e.fillText("Cwm fjordbank glyphs vext quiz, 😃", 2, 15), e.fillStyle = "rgba(102, 204, 0, 0.2)", e.font = "18pt Arial", e.fillText("Cwm fjordbank glyphs vext quiz, 😃", 4, 45), e.globalCompositeOperation = "multiply", e.fillStyle = "rgb(255,0,255)", e.beginPath(), e.arc(50, 50, 50, 0, 2 * Math.PI, !0), e.closePath(), e.fill(), e.fillStyle = "rgb(0,255,255)", e.beginPath(), e.arc(100, 50, 50, 0, 2 * Math.PI, !0), e.closePath(), e.fill(), e.fillStyle = "rgb(255,255,0)", e.beginPath(), e.arc(75, 100, 50, 0, 2 * Math.PI, !0), e.closePath(), e.fill(), e.fillStyle = "rgb(255,0,255)", e.arc(75, 75, 75, 0, 2 * Math.PI, !0), e.arc(75, 75, 25, 0, 2 * Math.PI, !0), e.fill("evenodd"), f(t.toDataURL());
+ } catch {
+ return l;
+ }
+}
+async function Oe() {
+ const t = {
+ hasModifiedCanvas: r,
+ canvasFingerprint: r
+ };
+ return t.hasModifiedCanvas = await Le(), t.canvasFingerprint = Te(), t;
+}
+function He() {
+ const t = ["deviceMemory", "hardwareConcurrency", "language", "languages", "platform"], e = [];
+ for (const n of t) {
+ const i = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), n);
+ i && i.value ? e.push("1") : e.push("0");
+ }
+ return e.join("");
+}
+function ze() {
+ return Math.random().toString(36).substring(2, 15);
+}
+function Ue() {
+ return (/* @__PURE__ */ new Date()).getTime();
+}
+function Ne() {
+ return window.location.href;
+}
+function x(t, e) {
+ const n = t.signals;
+ return e === "iframe" ? n.contexts.iframe.webdriver !== n.automation.webdriver || n.contexts.iframe.userAgent !== n.browser.userAgent || n.contexts.iframe.platform !== n.device.platform || n.contexts.iframe.memory !== n.device.memory || n.contexts.iframe.cpuCount !== n.device.cpuCount : n.contexts.webWorker.webdriver !== n.automation.webdriver || n.contexts.webWorker.userAgent !== n.browser.userAgent || n.contexts.webWorker.platform !== n.device.platform || n.contexts.webWorker.memory !== n.device.memory || n.contexts.webWorker.cpuCount !== n.device.cpuCount;
+}
+function Fe() {
+ const t = {
+ bitmask: r,
+ extensions: []
+ }, e = document.body.hasAttribute("data-gr-ext-installed"), n = typeof window.ethereum < "u", i = document.getElementById("coupon-birds-drop-div") !== null, a = document.querySelector("deepl-input-controller") !== null, g = document.getElementById("monica-content-root") !== null, p = document.querySelector("chatgpt-sidebar") !== null, y = typeof window.__REQUESTLY__ < "u", o = Array.from(document.querySelectorAll("*")).filter((m) => m.tagName.toLowerCase().startsWith("veepn-")).length > 0;
+ return t.bitmask = [
+ e ? "1" : "0",
+ n ? "1" : "0",
+ i ? "1" : "0",
+ a ? "1" : "0",
+ g ? "1" : "0",
+ p ? "1" : "0",
+ y ? "1" : "0",
+ o ? "1" : "0"
+ ].join(""), e && t.extensions.push("grammarly"), n && t.extensions.push("metamask"), i && t.extensions.push("coupon-birds"), a && t.extensions.push("deepl"), g && t.extensions.push("monica-ai"), p && t.extensions.push("sider-ai"), y && t.extensions.push("requestly"), o && t.extensions.push("veepn"), t;
+}
+function c(t) {
+ try {
+ return t();
+ } catch {
+ return !1;
+ }
+}
+function Ge() {
+ const t = {
+ bitmask: r,
+ chrome: c(() => "chrome" in window),
+ brave: c(() => "brave" in navigator),
+ applePaySupport: c(() => "ApplePaySetup" in window),
+ opera: c(() => typeof window.opr < "u" || typeof window.onoperadetachedviewchange == "object"),
+ serial: c(() => window.navigator.serial !== void 0),
+ attachShadow: c(() => !!Element.prototype.attachShadow),
+ caches: c(() => !!window.caches),
+ webAssembly: c(() => !!window.WebAssembly && !!window.WebAssembly.instantiate),
+ buffer: c(() => "Buffer" in window),
+ showModalDialog: c(() => "showModalDialog" in window),
+ safari: c(() => "safari" in window),
+ webkitPrefixedFunction: c(() => "webkitCancelAnimationFrame" in window),
+ mozPrefixedFunction: c(() => "mozGetUserMedia" in navigator),
+ usb: c(() => typeof window.USB == "function"),
+ browserCapture: c(() => typeof window.BrowserCaptureMediaStreamTrack == "function"),
+ paymentRequestUpdateEvent: c(() => typeof window.PaymentRequestUpdateEvent == "function"),
+ pressureObserver: c(() => typeof window.PressureObserver == "function"),
+ audioSession: c(() => "audioSession" in navigator),
+ selectAudioOutput: c(() => typeof navigator < "u" && typeof navigator.mediaDevices < "u" && typeof navigator.mediaDevices.selectAudioOutput == "function"),
+ barcodeDetector: c(() => "BarcodeDetector" in window),
+ battery: c(() => "getBattery" in navigator),
+ devicePosture: c(() => "DevicePosture" in window),
+ documentPictureInPicture: c(() => "documentPictureInPicture" in window),
+ eyeDropper: c(() => "EyeDropper" in window),
+ editContext: c(() => "EditContext" in window),
+ fencedFrame: c(() => "FencedFrameConfig" in window),
+ sanitizer: c(() => "Sanitizer" in window),
+ otpCredential: c(() => "OTPCredential" in window),
+ sumPrecise: c(() => "sumPrecise" in Math)
+ }, e = Object.keys(t).filter((n) => n !== "bitmask").map((n) => t[n] ? "1" : "0").join("");
+ return t.bitmask = e, t;
+}
+function Ve() {
+ const t = {
+ prefersColorScheme: r,
+ prefersReducedMotion: r,
+ prefersReducedTransparency: r,
+ colorGamut: r,
+ pointer: r,
+ anyPointer: r,
+ hover: r,
+ anyHover: r,
+ colorDepth: r
+ };
+ try {
+ window.matchMedia("(prefers-color-scheme: dark)").matches ? t.prefersColorScheme = "dark" : window.matchMedia("(prefers-color-scheme: light)").matches ? t.prefersColorScheme = "light" : t.prefersColorScheme = null, t.prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches, t.prefersReducedTransparency = window.matchMedia("(prefers-reduced-transparency: reduce)").matches, window.matchMedia("(color-gamut: rec2020)").matches ? t.colorGamut = "rec2020" : window.matchMedia("(color-gamut: p3)").matches ? t.colorGamut = "p3" : window.matchMedia("(color-gamut: srgb)").matches ? t.colorGamut = "srgb" : t.colorGamut = null, window.matchMedia("(pointer: fine)").matches ? t.pointer = "fine" : window.matchMedia("(pointer: coarse)").matches ? t.pointer = "coarse" : window.matchMedia("(pointer: none)").matches ? t.pointer = "none" : t.pointer = null, window.matchMedia("(any-pointer: fine)").matches ? t.anyPointer = "fine" : window.matchMedia("(any-pointer: coarse)").matches ? t.anyPointer = "coarse" : window.matchMedia("(any-pointer: none)").matches ? t.anyPointer = "none" : t.anyPointer = null, t.hover = window.matchMedia("(hover: hover)").matches, t.anyHover = window.matchMedia("(any-hover: hover)").matches;
+ let e = 0;
+ for (let n = 0; n <= 16; n++)
+ window.matchMedia(`(color: ${n})`).matches && (e = n);
+ t.colorDepth = e;
+ } catch {
+ d(t, l);
+ }
+ return t;
+}
+async function Be() {
+ const t = {
+ layout: r,
+ layoutSize: r
+ };
+ if ("keyboard" in navigator && typeof navigator.keyboard.getLayoutMap < "u")
+ try {
+ const e = await navigator.keyboard.getLayoutMap();
+ t.layout = Array.from(
+ e.entries()
+ ).map(([n, i]) => `${n},${i}`).join(" "), t.layoutSize = e.size;
+ } catch {
+ d(t, l);
+ }
+ else
+ d(t, s);
+ return t;
+}
+async function je() {
+ const t = {
+ summarizerAvailability: r,
+ summarizerLanguageAvailability: r
+ };
+ if ("Summarizer" in window)
+ try {
+ t.summarizerAvailability = await window.Summarizer.availability(), t.summarizerLanguageAvailability = await window.Summarizer.availability({
+ expectedInputLanguages: [navigator.language]
+ });
+ } catch {
+ d(t, l);
+ }
+ else
+ d(t, s);
+ return t;
+}
+function $e(t) {
+ const e = t.signals.device.screenResolution;
+ return e.width === 800 && e.height === 600 || e.availableWidth === 800 && e.availableHeight === 600 || e.innerWidth === 800 && e.innerHeight === 600;
+}
+function Qe(t) {
+ return t.signals.automation.webdriver === !0;
+}
+function qe(t) {
+ return !!t.signals.automation.selenium;
+}
+function Ke(t) {
+ return t.signals.automation.cdp === !0;
+}
+function Je(t) {
+ return t.signals.automation.playwright === !0;
+}
+function Ye(t) {
+ return typeof t.signals.device.memory != "number" ? !1 : t.signals.device.memory > 32 || t.signals.device.memory < 0.25;
+}
+function Ze(t) {
+ return typeof t.signals.device.cpuCount != "number" ? !1 : t.signals.device.cpuCount > 70;
+}
+function Xe(t) {
+ return t.includes("Android") || t.includes("iPhone") || t.includes("iPod") || t.includes("iPad");
+}
+function et(t) {
+ const e = t.signals.browser.userAgent;
+ return typeof e != "string" || !e.includes("Chrome") || Xe(e) ? !1 : t.signals.browser.features.chrome === !1;
+}
+function tt(t) {
+ return t.signals.contexts.iframe.webdriver === !0;
+}
+function rt(t) {
+ return t.signals.contexts.webWorker.webdriver === !0;
+}
+function b(t) {
+ return typeof t != "string" || t.length === 0 ? !0 : t === s || t === l || t === v || t === r;
+}
+function nt(t) {
+ const e = t.signals.contexts.webWorker, n = t.signals.graphics.webGL;
+ return b(n.vendor) || b(n.renderer) || b(e.vendor) || b(e.renderer) ? !1 : e.vendor !== n.vendor || e.renderer !== n.renderer;
+}
+function it(t, e) {
+ const n = t.includes("iPad"), i = e.includes("iPad");
+ if (n === i)
+ return !1;
+ const a = (g) => g === "MacIntel" || g === "MacPPC";
+ return a(t) || a(e);
+}
+function at(t) {
+ if (t.signals.contexts.webWorker.platform === s || t.signals.contexts.webWorker.platform === l || t.signals.contexts.webWorker.platform === v)
+ return !1;
+ const e = t.signals.device.platform, n = t.signals.contexts.webWorker.platform;
+ return !(e === n || it(e, n));
+}
+function ot(t) {
+ return t.signals.contexts.iframe.platform === s || t.signals.contexts.iframe.platform === l ? !1 : t.signals.device.platform !== t.signals.contexts.iframe.platform;
+}
+function st(t) {
+ return t.signals.automation.webdriverWritable === !0;
+}
+function ct(t) {
+ return t.signals.graphics.webGL.renderer.includes("SwiftShader");
+}
+function lt(t) {
+ return t.signals.locale.internationalization.timezone === "UTC";
+}
+function ut(t) {
+ const e = t.signals.locale.languages.languages, n = t.signals.locale.languages.language;
+ return n && e && Array.isArray(e) && e.length > 0 ? e[0] !== n : !1;
+}
+function dt(t) {
+ return !!(t.signals.browser.features.chrome && t.signals.browser.etsl !== 33 || t.signals.browser.features.safari && t.signals.browser.etsl !== 37 || t.signals.browser.userAgent.includes("Firefox") && t.signals.browser.etsl !== 37);
+}
+function gt(t) {
+ return [
+ t.signals.browser.userAgent,
+ t.signals.contexts.iframe.userAgent,
+ t.signals.contexts.webWorker.userAgent
+ ].some((n) => /bot|headless/i.test(n.toLowerCase()));
+}
+function ht(t) {
+ const e = t.signals.graphics.webgpu, n = t.signals.graphics.webGL, i = t.signals.browser.userAgent;
+ return !!((n.vendor.includes("Apple") || n.renderer.includes("Apple")) && !i.includes("Mac") || e.vendor.includes("apple") && !i.includes("Mac") || e.vendor.includes("apple") && !n.renderer.includes("Apple"));
+}
+function mt(t) {
+ const e = t.signals.device.platform, n = t.signals.browser.userAgent, i = t.signals.browser.highEntropyValues.platform;
+ return !!(n.includes("Mac") && (e.includes("Win") || e.includes("Linux")) || n.includes("Windows") && (e.includes("Mac") || e.includes("Linux")) || n.includes("Linux") && (e.includes("Mac") || e.includes("Win")) || i !== l && i !== s && (i.includes("Mac") && (e.includes("Win") || e.includes("Linux")) || i.includes("Windows") && (e.includes("Mac") || e.includes("Linux")) || i.includes("Linux") && (e.includes("Mac") || e.includes("Win"))));
+}
+async function pt(t, e) {
+ const n = new TextEncoder().encode(e), i = new TextEncoder().encode(t), a = new Uint8Array(i.length);
+ for (let p = 0; p < i.length; p++)
+ a[p] = i[p] ^ n[p % n.length];
+ const g = String.fromCharCode(...a);
+ return btoa(g);
+}
+class vt {
+ constructor() {
+ this.fingerprint = {
+ signals: {
+ // Automation/Bot detection signals
+ automation: {
+ webdriver: r,
+ webdriverWritable: r,
+ selenium: r,
+ cdp: r,
+ playwright: r,
+ navigatorPropertyDescriptors: r
+ },
+ // Device hardware characteristics
+ device: {
+ cpuCount: r,
+ memory: r,
+ platform: r,
+ screenResolution: {
+ width: r,
+ height: r,
+ pixelDepth: r,
+ colorDepth: r,
+ availableWidth: r,
+ availableHeight: r,
+ innerWidth: r,
+ innerHeight: r,
+ hasMultipleDisplays: r
+ },
+ multimediaDevices: {
+ speakers: r,
+ microphones: r,
+ webcams: r
+ },
+ mediaQueries: {
+ prefersColorScheme: r,
+ prefersReducedMotion: r,
+ prefersReducedTransparency: r,
+ colorGamut: r,
+ pointer: r,
+ anyPointer: r,
+ hover: r,
+ anyHover: r,
+ colorDepth: r
+ },
+ keyboard: {
+ layout: r,
+ layoutSize: r
+ }
+ },
+ // Browser identity & features
+ browser: {
+ userAgent: r,
+ features: {
+ bitmask: r,
+ chrome: r,
+ brave: r,
+ applePaySupport: r,
+ opera: r,
+ serial: r,
+ attachShadow: r,
+ caches: r,
+ webAssembly: r,
+ buffer: r,
+ showModalDialog: r,
+ safari: r,
+ webkitPrefixedFunction: r,
+ mozPrefixedFunction: r,
+ usb: r,
+ browserCapture: r,
+ paymentRequestUpdateEvent: r,
+ pressureObserver: r,
+ audioSession: r,
+ selectAudioOutput: r,
+ barcodeDetector: r,
+ battery: r,
+ devicePosture: r,
+ documentPictureInPicture: r,
+ eyeDropper: r,
+ editContext: r,
+ fencedFrame: r,
+ sanitizer: r,
+ otpCredential: r
+ },
+ plugins: {
+ isValidPluginArray: r,
+ pluginCount: r,
+ pluginNamesHash: r,
+ pluginConsistency1: r,
+ pluginOverflow: r
+ },
+ extensions: {
+ bitmask: r,
+ extensions: r
+ },
+ highEntropyValues: {
+ architecture: r,
+ bitness: r,
+ brands: r,
+ mobile: r,
+ model: r,
+ platform: r,
+ platformVersion: r,
+ uaFullVersion: r
+ },
+ etsl: r,
+ maths: r,
+ toSourceError: {
+ toSourceError: r,
+ hasToSource: r
+ },
+ ai: {
+ summarizerAvailability: r,
+ summarizerLanguageAvailability: r
+ }
+ },
+ // Graphics & rendering
+ graphics: {
+ webGL: {
+ vendor: r,
+ renderer: r
+ },
+ webgpu: {
+ vendor: r,
+ architecture: r,
+ device: r,
+ description: r
+ },
+ canvas: {
+ hasModifiedCanvas: r,
+ canvasFingerprint: r
+ }
+ },
+ // Media codecs (at root level)
+ codecs: {
+ audioCanPlayTypeHash: r,
+ videoCanPlayTypeHash: r,
+ audioMediaSourceHash: r,
+ videoMediaSourceHash: r,
+ rtcAudioCapabilitiesHash: r,
+ rtcVideoCapabilitiesHash: r,
+ hasMediaSource: r
+ },
+ // Locale & internationalization
+ locale: {
+ internationalization: {
+ timezone: r,
+ localeLanguage: r
+ },
+ languages: {
+ languages: r,
+ language: r
+ }
+ },
+ // Isolated execution contexts
+ contexts: {
+ iframe: {
+ webdriver: r,
+ userAgent: r,
+ platform: r,
+ memory: r,
+ cpuCount: r,
+ language: r
+ },
+ webWorker: {
+ webdriver: r,
+ userAgent: r,
+ platform: r,
+ memory: r,
+ cpuCount: r,
+ language: r,
+ vendor: r,
+ renderer: r
+ }
+ }
+ },
+ fsid: r,
+ nonce: r,
+ time: r,
+ url: r,
+ fastBotDetection: !1,
+ fastBotDetectionDetails: {
+ headlessChromeScreenResolution: { detected: !1, severity: "high" },
+ hasWebdriver: { detected: !1, severity: "high" },
+ hasWebdriverWritable: { detected: !1, severity: "high" },
+ hasSeleniumProperty: { detected: !1, severity: "high" },
+ hasCDP: { detected: !1, severity: "high" },
+ hasPlaywright: { detected: !1, severity: "high" },
+ hasImpossibleDeviceMemory: { detected: !1, severity: "high" },
+ hasHighCPUCount: { detected: !1, severity: "high" },
+ hasMissingChromeObject: { detected: !1, severity: "high" },
+ hasWebdriverIframe: { detected: !1, severity: "high" },
+ hasWebdriverWorker: { detected: !1, severity: "high" },
+ hasMismatchWebGLInWorker: { detected: !1, severity: "high" },
+ hasMismatchPlatformIframe: { detected: !1, severity: "high" },
+ hasMismatchPlatformWorker: { detected: !1, severity: "high" },
+ hasSwiftshaderRenderer: { detected: !1, severity: "low" },
+ hasUTCTimezone: { detected: !1, severity: "medium" },
+ hasMismatchLanguages: { detected: !1, severity: "low" },
+ hasInconsistentEtsl: { detected: !1, severity: "high" },
+ hasBotUserAgent: { detected: !1, severity: "high" },
+ hasGPUMismatch: { detected: !1, severity: "high" },
+ hasPlatformMismatch: { detected: !1, severity: "high" }
+ }
+ };
+ }
+ async collectSignal(e) {
+ try {
+ return await e();
+ } catch {
+ return l;
+ }
+ }
+ /**
+ * Generate a JA4-inspired fingerprint scanner ID
+ * Format: FS1________
+ *
+ * Each section is delimited by '_', allowing partial matching.
+ * Sections use the pattern: h where applicable.
+ * Bitmasks are extensible - new boolean fields are appended without breaking existing positions.
+ *
+ * Sections:
+ * - det: fastBotDetectionDetails bitmask (21 bits: headlessChromeScreenResolution, hasWebdriver,
+ * hasWebdriverWritable, hasSeleniumProperty, hasCDP, hasPlaywright, hasImpossibleDeviceMemory,
+ * hasHighCPUCount, hasMissingChromeObject, hasWebdriverIframe, hasWebdriverWorker,
+ * hasMismatchWebGLInWorker, hasMismatchPlatformIframe, hasMismatchPlatformWorker,
+ * hasMismatchLanguages, hasInconsistentEtsl, hasBotUserAgent, hasGPUMismatch, hasPlatformMismatch)
+ * - auto: automation bitmask (5 bits: webdriver, webdriverWritable, selenium, cdp, playwright) + hash
+ * - dev: WIDTHxHEIGHT + cpu + mem + device bitmask + hash of all device signals
+ * - brw: features.bitmask + extensions.bitmask + plugins bitmask (3 bits) + hash of browser signals
+ * - gfx: canvas bitmask (1 bit: hasModifiedCanvas) + hash of all graphics signals
+ * - cod: codecs bitmask (1 bit: hasMediaSource) + hash of all codec hashes
+ * - loc: language code (2 chars) + language count + hash of locale signals
+ * - ctx: context mismatch bitmask (2 bits: iframe, worker) + hash of all context signals
+ */
+ generateFingerprintScannerId() {
+ try {
+ const e = this.fingerprint.signals, n = this.fingerprint.fastBotDetectionDetails, i = "FS1", g = [
+ n.headlessChromeScreenResolution.detected,
+ n.hasWebdriver.detected,
+ n.hasWebdriverWritable.detected,
+ n.hasSeleniumProperty.detected,
+ n.hasCDP.detected,
+ n.hasPlaywright.detected,
+ n.hasImpossibleDeviceMemory.detected,
+ n.hasHighCPUCount.detected,
+ n.hasMissingChromeObject.detected,
+ n.hasWebdriverIframe.detected,
+ n.hasWebdriverWorker.detected,
+ n.hasMismatchWebGLInWorker.detected,
+ n.hasMismatchPlatformIframe.detected,
+ n.hasMismatchPlatformWorker.detected,
+ n.hasSwiftshaderRenderer.detected,
+ n.hasUTCTimezone.detected,
+ n.hasMismatchLanguages.detected,
+ n.hasInconsistentEtsl.detected,
+ n.hasBotUserAgent.detected,
+ n.hasGPUMismatch.detected,
+ n.hasPlatformMismatch.detected
+ // Add other detection rules output here
+ ].map((u) => u ? "1" : "0").join(""), p = [
+ e.automation.webdriver === !0,
+ e.automation.webdriverWritable === !0,
+ e.automation.selenium === !0,
+ e.automation.cdp === !0,
+ e.automation.playwright === !0
+ ].map((u) => u ? "1" : "0").join(""), y = f(String(e.automation.navigatorPropertyDescriptors)).slice(0, 4), o = `${p}h${y}`, m = typeof e.device.screenResolution.width == "number" ? e.device.screenResolution.width : 0, w = typeof e.device.screenResolution.height == "number" ? e.device.screenResolution.height : 0, W = typeof e.device.cpuCount == "number" ? String(e.device.cpuCount).padStart(2, "0") : "00", E = typeof e.device.memory == "number" ? String(Math.round(e.device.memory)).padStart(2, "0") : "00", D = [
+ e.device.screenResolution.hasMultipleDisplays === !0,
+ e.device.mediaQueries.prefersReducedMotion === !0,
+ e.device.mediaQueries.prefersReducedTransparency === !0,
+ e.device.mediaQueries.hover === !0,
+ e.device.mediaQueries.anyHover === !0
+ ].map((u) => u ? "1" : "0").join(""), _ = [
+ e.device.platform,
+ e.device.screenResolution.pixelDepth,
+ e.device.screenResolution.colorDepth,
+ e.device.multimediaDevices.speakers,
+ e.device.multimediaDevices.microphones,
+ e.device.multimediaDevices.webcams,
+ e.device.mediaQueries.prefersColorScheme,
+ e.device.mediaQueries.colorGamut,
+ e.device.mediaQueries.pointer,
+ e.device.mediaQueries.anyPointer,
+ e.device.mediaQueries.colorDepth,
+ e.device.keyboard.layout,
+ e.device.keyboard.layoutSize
+ ].map((u) => String(u)).join("|"), R = f(_).slice(0, 6), I = `${m}x${w}c${W}m${E}b${D}h${R}`, L = typeof e.browser.features.bitmask == "string" ? e.browser.features.bitmask : "0".repeat(29), T = typeof e.browser.extensions.bitmask == "string" ? e.browser.extensions.bitmask : "0".repeat(8), O = [
+ e.browser.plugins.isValidPluginArray === !0,
+ e.browser.plugins.pluginConsistency1 === !0,
+ e.browser.plugins.pluginOverflow === !0,
+ e.browser.toSourceError.hasToSource === !0
+ ].map((u) => u ? "1" : "0").join(""), H = [
+ e.browser.userAgent,
+ e.browser.etsl,
+ e.browser.maths,
+ e.browser.plugins.pluginCount,
+ e.browser.plugins.pluginNamesHash,
+ e.browser.toSourceError.toSourceError,
+ e.browser.highEntropyValues.architecture,
+ e.browser.highEntropyValues.bitness,
+ e.browser.highEntropyValues.platform,
+ e.browser.highEntropyValues.platformVersion,
+ e.browser.highEntropyValues.uaFullVersion,
+ e.browser.highEntropyValues.mobile,
+ e.browser.ai.summarizerAvailability,
+ e.browser.ai.summarizerLanguageAvailability
+ ].map((u) => String(u)).join("|"), z = f(H).slice(0, 6), U = `f${L}e${T}p${O}h${z}`, N = [
+ e.graphics.canvas.hasModifiedCanvas === !0
+ ].map((u) => u ? "1" : "0").join(""), F = [
+ e.graphics.webGL.vendor,
+ e.graphics.webGL.renderer,
+ e.graphics.webgpu.vendor,
+ e.graphics.webgpu.architecture,
+ e.graphics.webgpu.device,
+ e.graphics.webgpu.description,
+ e.graphics.canvas.canvasFingerprint
+ ].map((u) => String(u)).join("|"), G = f(F).slice(0, 6), V = `${N}h${G}`, B = [
+ e.codecs.hasMediaSource === !0
+ ].map((u) => u ? "1" : "0").join(""), j = [
+ e.codecs.audioCanPlayTypeHash,
+ e.codecs.videoCanPlayTypeHash,
+ e.codecs.audioMediaSourceHash,
+ e.codecs.videoMediaSourceHash,
+ e.codecs.rtcAudioCapabilitiesHash,
+ e.codecs.rtcVideoCapabilitiesHash
+ ].map((u) => String(u)).join("|"), $ = f(j).slice(0, 6), Q = `${B}h${$}`, q = typeof e.locale.languages.language == "string" ? e.locale.languages.language.slice(0, 2).toLowerCase() : "xx", K = Array.isArray(e.locale.languages.languages) ? e.locale.languages.languages.length : 0, J = (typeof e.locale.internationalization.timezone == "string" ? e.locale.internationalization.timezone : "unknown").replace(/[\/\s]/g, "-"), Y = [
+ e.locale.internationalization.timezone,
+ e.locale.internationalization.localeLanguage,
+ Array.isArray(e.locale.languages.languages) ? e.locale.languages.languages.join(",") : e.locale.languages.languages,
+ e.locale.languages.language
+ ].map((u) => String(u)).join("|"), Z = f(Y).slice(0, 4), X = `${q}${K}t${J}_h${Z}`, ee = [
+ x(this.fingerprint, "iframe"),
+ x(this.fingerprint, "worker"),
+ e.contexts.iframe.webdriver === !0,
+ e.contexts.webWorker.webdriver === !0
+ ].map((u) => u ? "1" : "0").join(""), te = [
+ e.contexts.iframe.userAgent,
+ e.contexts.iframe.platform,
+ e.contexts.iframe.memory,
+ e.contexts.iframe.cpuCount,
+ e.contexts.iframe.language,
+ e.contexts.webWorker.userAgent,
+ e.contexts.webWorker.platform,
+ e.contexts.webWorker.memory,
+ e.contexts.webWorker.cpuCount,
+ e.contexts.webWorker.language,
+ e.contexts.webWorker.vendor,
+ e.contexts.webWorker.renderer
+ ].map((u) => String(u)).join("|"), re = f(te).slice(0, 6), ne = `${ee}h${re}`;
+ return [
+ i,
+ g,
+ o,
+ I,
+ U,
+ V,
+ Q,
+ X,
+ ne
+ ].join("_");
+ } catch (e) {
+ return console.error("Error generating fingerprint scanner id", e), l;
+ }
+ }
+ async encryptFingerprint(e) {
+ const n = "__DEFAULT_FPSCANNER_KEY__";
+ return n.length > 20 && n.indexOf("DEFAULT") > 0 && n.indexOf("FPSCANNER") > 0 && console.warn(
+ '[fpscanner] WARNING: Using default encryption key! Run "npx fpscanner build --key=your-secret-key" to inject your own key. See: https://github.com/antoinevastel/fpscanner#advanced-custom-builds'
+ ), await pt(JSON.stringify(e), n);
+ }
+ /**
+ * Detection rules with name and severity.
+ */
+ getDetectionRules() {
+ return [
+ { name: "headlessChromeScreenResolution", severity: h, test: $e },
+ { name: "hasWebdriver", severity: h, test: Qe },
+ { name: "hasWebdriverWritable", severity: h, test: st },
+ { name: "hasSeleniumProperty", severity: h, test: qe },
+ { name: "hasCDP", severity: h, test: Ke },
+ { name: "hasPlaywright", severity: h, test: Je },
+ { name: "hasImpossibleDeviceMemory", severity: h, test: Ye },
+ { name: "hasHighCPUCount", severity: h, test: Ze },
+ { name: "hasMissingChromeObject", severity: h, test: et },
+ { name: "hasWebdriverIframe", severity: h, test: tt },
+ { name: "hasWebdriverWorker", severity: h, test: rt },
+ { name: "hasMismatchWebGLInWorker", severity: h, test: nt },
+ { name: "hasMismatchPlatformIframe", severity: h, test: ot },
+ { name: "hasMismatchPlatformWorker", severity: h, test: at },
+ { name: "hasSwiftshaderRenderer", severity: S, test: ct },
+ { name: "hasUTCTimezone", severity: se, test: lt },
+ { name: "hasMismatchLanguages", severity: S, test: ut },
+ { name: "hasInconsistentEtsl", severity: h, test: dt },
+ { name: "hasBotUserAgent", severity: h, test: gt },
+ { name: "hasGPUMismatch", severity: h, test: ht },
+ { name: "hasPlatformMismatch", severity: h, test: mt }
+ ];
+ }
+ runDetectionRules() {
+ const e = this.getDetectionRules(), n = {
+ headlessChromeScreenResolution: { detected: !1, severity: "high" },
+ hasWebdriver: { detected: !1, severity: "high" },
+ hasWebdriverWritable: { detected: !1, severity: "high" },
+ hasSeleniumProperty: { detected: !1, severity: "high" },
+ hasCDP: { detected: !1, severity: "high" },
+ hasPlaywright: { detected: !1, severity: "high" },
+ hasImpossibleDeviceMemory: { detected: !1, severity: "high" },
+ hasHighCPUCount: { detected: !1, severity: "high" },
+ hasMissingChromeObject: { detected: !1, severity: "high" },
+ hasWebdriverIframe: { detected: !1, severity: "high" },
+ hasWebdriverWorker: { detected: !1, severity: "high" },
+ hasMismatchWebGLInWorker: { detected: !1, severity: "high" },
+ hasMismatchPlatformIframe: { detected: !1, severity: "high" },
+ hasMismatchPlatformWorker: { detected: !1, severity: "high" },
+ hasSwiftshaderRenderer: { detected: !1, severity: "low" },
+ hasUTCTimezone: { detected: !1, severity: "medium" },
+ hasMismatchLanguages: { detected: !1, severity: "low" },
+ hasInconsistentEtsl: { detected: !1, severity: "high" },
+ hasBotUserAgent: { detected: !1, severity: "high" },
+ hasGPUMismatch: { detected: !1, severity: "high" },
+ hasPlatformMismatch: { detected: !1, severity: "high" }
+ };
+ for (const i of e)
+ try {
+ const a = i.test(this.fingerprint);
+ n[i.name] = { detected: a, severity: i.severity };
+ } catch {
+ n[i.name] = { detected: !1, severity: i.severity };
+ }
+ return n;
+ }
+ async collectFingerprint(e = { encrypt: !0 }) {
+ const { encrypt: n = !0, skipWorker: i = !1 } = e, a = this.fingerprint.signals, g = {
+ // Automation signals
+ webdriver: this.collectSignal(ie),
+ webdriverWritable: this.collectSignal(Se),
+ selenium: this.collectSignal(be),
+ cdp: this.collectSignal(le),
+ playwright: this.collectSignal(de),
+ navigatorPropertyDescriptors: this.collectSignal(He),
+ // Device signals
+ cpuCount: this.collectSignal(ge),
+ memory: this.collectSignal(me),
+ platform: this.collectSignal(oe),
+ screenResolution: this.collectSignal(ve),
+ multimediaDevices: this.collectSignal(Ee),
+ mediaQueries: this.collectSignal(Ve),
+ keyboard: this.collectSignal(Be),
+ // Browser signals
+ userAgent: this.collectSignal(ae),
+ browserFeatures: this.collectSignal(Ge),
+ plugins: this.collectSignal(We),
+ browserExtensions: this.collectSignal(Fe),
+ highEntropyValues: this.collectSignal(Ce),
+ etsl: this.collectSignal(pe),
+ maths: this.collectSignal(he),
+ toSourceError: this.collectSignal(Re),
+ ai: this.collectSignal(je),
+ // Graphics signals
+ webGL: this.collectSignal(ue),
+ webgpu: this.collectSignal(we),
+ canvas: this.collectSignal(Oe),
+ // Codecs
+ mediaCodecs: this.collectSignal(Ie),
+ // Locale signals
+ internationalization: this.collectSignal(fe),
+ languages: this.collectSignal(ye),
+ // Context signals
+ iframe: this.collectSignal(De),
+ webWorker: i ? Promise.resolve({
+ webdriver: v,
+ userAgent: v,
+ platform: v,
+ memory: v,
+ cpuCount: v,
+ language: v,
+ vendor: v,
+ renderer: v
+ }) : this.collectSignal(_e),
+ // Meta signals
+ nonce: this.collectSignal(ze),
+ time: this.collectSignal(Ue),
+ url: this.collectSignal(Ne)
+ }, p = Object.keys(g), y = await Promise.all(Object.values(g)), o = Object.fromEntries(p.map((m, w) => [m, y[w]]));
+ return a.automation.webdriver = o.webdriver, a.automation.webdriverWritable = o.webdriverWritable, a.automation.selenium = o.selenium, a.automation.cdp = o.cdp, a.automation.playwright = o.playwright, a.automation.navigatorPropertyDescriptors = o.navigatorPropertyDescriptors, a.device.cpuCount = o.cpuCount, a.device.memory = o.memory, a.device.platform = o.platform, a.device.screenResolution = o.screenResolution, a.device.multimediaDevices = o.multimediaDevices, a.device.mediaQueries = o.mediaQueries, a.device.keyboard = o.keyboard, a.browser.userAgent = o.userAgent, a.browser.features = o.browserFeatures, a.browser.plugins = o.plugins, a.browser.extensions = o.browserExtensions, a.browser.highEntropyValues = o.highEntropyValues, a.browser.etsl = o.etsl, a.browser.maths = o.maths, a.browser.toSourceError = o.toSourceError, a.browser.ai = o.ai, a.graphics.webGL = o.webGL, a.graphics.webgpu = o.webgpu, a.graphics.canvas = o.canvas, a.codecs = o.mediaCodecs, a.locale.internationalization = o.internationalization, a.locale.languages = o.languages, a.contexts.iframe = o.iframe, a.contexts.webWorker = o.webWorker, this.fingerprint.nonce = o.nonce, this.fingerprint.time = o.time, this.fingerprint.url = o.url, this.fingerprint.fastBotDetectionDetails = this.runDetectionRules(), this.fingerprint.fastBotDetection = Object.values(this.fingerprint.fastBotDetectionDetails).some((m) => m.detected), this.fingerprint.fsid = this.generateFingerprintScannerId(), n ? await this.encryptFingerprint(JSON.stringify(this.fingerprint)) : this.fingerprint;
+ }
+}
+export {
+ vt as default
+};