diff --git a/tests/test_detectors_e2e.py b/tests/test_detectors_e2e.py index 821a186..c37f67c 100644 --- a/tests/test_detectors_e2e.py +++ b/tests/test_detectors_e2e.py @@ -6,21 +6,32 @@ libraries and uses their FULL API surface: * BotD (@fingerprintjs/botd, MIT) — the client-side bot detector that FingerprintJS Pro itself uses. We assert the aggregate verdict (``detect().bot == False``) AND every one of its ~18 individual detectors - (``getDetections()``) returns ``bot == False``. The per-detector view is - why we could delete our hand-rolled ``test_botd_*`` mirrors — the real - library now covers each detector, with the same granularity. + (``getDetections()``) returns ``bot == False``. * FingerprintJS open-source (MIT) — ``get()`` must return a ``visitorId`` - that is STABLE across two fresh launches with the same seed (an - over-randomized spoof drifts), and a RICH component set (the fingerprint - surface is real, not a stub). + that is STABLE across two fresh launches with the same seed, and a RICH + component set (the fingerprint surface is real, not a stub). + * fpscanner (antoinevastel/fpscanner 1.0.6, MIT) — ``collectFingerprint()`` + runs ~21 bot-detection rules in the browser. We assert the **engine-agnostic** + subset (webdriver / selenium / bot-UA / platform / timezone / language) is + clean. We deliberately do NOT assert the Chrome/GPU-only rules (hasCDP, + hasPlaywright, hasSwiftshaderRenderer, hasMissingChromeObject, …): they're + trivially clean on Firefox, and the GPU ones can legitimately fire on a + software-WebGL CI host (Xvfb/llvmpipe) — asserting them would false-red. + * CreepJS (abrahamjuliot/creepjs, MIT, pinned) — the gold-standard Firefox-aware + headless/stealth/lie detector. It exposes its result on ``window.Fingerprint``. + We assert ``headlessRating == 0`` (webdriver + headless-UA tells) and the + JS-proxy stealth tells are absent. ``stealthRating`` / ``totalLies`` / + ``likeHeadlessRating`` are LOGGED, not hard-asserted, because some of their + sub-signals (hasBadWebGL, prefers-light-color) are GPU/theme-sensitive and + differ on a GPU-less CI host. Everything is hermetic: the libraries are vendored (tests/vendor/) and served -from a localhost HTTP server — no external CDN call (Firefox tracking-protection -blocks the CDN anyway) and no IP/network dependency. Runs identically on a dev -box and on a GitHub runner. +from a localhost HTTP server — no external CDN call. For CreepJS, every non-local +request is aborted, so its optional crowd-comparison POST never runs and the +verdict is computed purely locally. Runs identically on a dev box and a GH runner. -NOT covered: FingerprintJS *Pro* (commercial, server-side, IP/residential -analysis) — can't be self-hosted, stays the local realness gate. +NOT covered: FingerprintJS *Pro* (commercial, server-side) — stays the local +realness gate. """ from __future__ import annotations @@ -36,19 +47,33 @@ from invisible_playwright import InvisiblePlaywright _VENDOR = Path(__file__).parent / "vendor" _BOTD = "botd-2.0.0.esm.js" _FPJS = "fingerprintjs-5.2.0.umd.min.js" +_FPSCANNER = "fpscanner-1.0.6.es.js" +_CREEPJS = "creepjs-10aa672.js" # pinned abrahamjuliot/creepjs@10aa6724 + +# fpscanner rules that are MEANINGFUL on Firefox and GPU-independent — these must +# stay clean. The omitted rules are Chrome-only (hasCDP/hasPlaywright/ +# hasMissingChromeObject/hasHighCPUCount/hasImpossibleDeviceMemory/ +# headlessChromeScreenResolution) or GPU-sensitive on a software-WebGL CI host +# (hasSwiftshaderRenderer/hasGPUMismatch/hasMismatchWebGLInWorker). +_FPSCANNER_AGNOSTIC = [ + "hasWebdriver", "hasWebdriverIframe", "hasWebdriverWorker", "hasWebdriverWritable", + "hasSeleniumProperty", "hasBotUserAgent", "hasPlatformMismatch", + "hasMismatchLanguages", "hasUTCTimezone", "hasMismatchPlatformIframe", + "hasMismatchPlatformWorker", "hasInconsistentEtsl", +] _PAGE = f""" detectors

loading

""" +# CreepJS gets its own page: creep.js is a plain `defer` script that runs on load +# and populates window.Fingerprint. A minimal DOM is enough (the rich report DOM +# is only for the visual page, not the computation). +_CREEP_PAGE = f"""creep +
""" + class _DetectorSite: - """Localhost server: `/` → the page; `/` → the vendored bundle.""" + """Localhost server: `/` → BotD+FPJS+fpscanner page, `/creepjs` → CreepJS page, + `/` → 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 ` + + + + + `; + }; + + 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 ? `
` : ''} +
+
+
+ ${rgba ? `
` : ''} +
+
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}
+ +
${'' + 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}
+
+
${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 ` +
+
+
+
FP ID: Computing...
+
+
Fuzzy: ${blankFingerprint}
+
+
${(timeEnd || 0).toFixed(2)} ms
+
+
+
+
+ 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)}
+
+
+ Status +
network:
+
+
+
+
battery:
+
+
+
+
available:
+
+
+
+
+ Tests +
+ Workers +
Iframes +
Fonts +
Timezone +
Window Version +
Screen +
Prototype +
DOMRect +
Emojis +
Math +
Machine +
Chrome Extensions +
JS Proxy +
+
+
+ `, 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 +};