├── .yarnrc ├── example ├── src │ ├── lib │ │ ├── constants.ts │ │ ├── enums.ts │ │ ├── components │ │ │ ├── sections │ │ │ │ ├── About.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── ClientData.svelte │ │ │ │ ├── Footer.svelte │ │ │ │ ├── Visitor.svelte │ │ │ │ ├── Durations.svelte │ │ │ │ ├── Errors.svelte │ │ │ │ └── UsageExample.svelte │ │ │ └── ui │ │ │ │ ├── ToggleIcon.svelte │ │ │ │ ├── NoJSInfo.svelte │ │ │ │ ├── index.ts │ │ │ │ ├── Accordion.svelte │ │ │ │ ├── ErrorBadge.svelte │ │ │ │ ├── GoToTop.svelte │ │ │ │ ├── VisitorInfo.svelte │ │ │ │ ├── AboutInfo.svelte │ │ │ │ ├── Badges.svelte │ │ │ │ ├── VisitMapSlider.svelte │ │ │ │ └── VisitorRow.svelte │ │ ├── utils │ │ │ ├── fetch.ts │ │ │ └── common.ts │ │ ├── services │ │ │ ├── mixvisit.ts │ │ │ └── visitor.ts │ │ ├── hooks │ │ │ └── tooltip.ts │ │ ├── api │ │ │ └── location.ts │ │ └── types.ts │ ├── routes │ │ ├── +layout.svelte │ │ ├── +layout.js │ │ └── +page.svelte │ ├── app.d.ts │ └── app.html ├── .gitignore ├── svelte.config.js ├── static │ └── _app │ │ ├── location-marker.svg │ │ ├── vite.svg │ │ └── main.css ├── vite.config.ts ├── tsconfig.json └── package.json ├── packages └── mixvisit-lite │ ├── src │ ├── types │ │ ├── index.ts │ │ ├── parameters │ │ │ ├── canvas.ts │ │ │ ├── index.ts │ │ │ ├── screenFrame.ts │ │ │ ├── webgpu.ts │ │ │ └── webgl.ts │ │ ├── utils.ts │ │ └── base.ts │ ├── client-parameters │ │ ├── java.ts │ │ ├── activeX.ts │ │ ├── colorDepth.ts │ │ ├── devicePixelRatio.ts │ │ ├── openDatabase.ts │ │ ├── globalPrivacyControl.ts │ │ ├── battery.ts │ │ ├── bluetooth.ts │ │ ├── network.ts │ │ ├── navigatorProperties.ts │ │ ├── sessionStorage.ts │ │ ├── architecture.ts │ │ ├── localStorage.ts │ │ ├── audioBaseLatency.ts │ │ ├── hdr.ts │ │ ├── forcedColors.ts │ │ ├── invertedColors.ts │ │ ├── reducedMotion.ts │ │ ├── scheduling.ts │ │ ├── flash.ts │ │ ├── reducedTransparency.ts │ │ ├── colorGamut.ts │ │ ├── indexedDB.ts │ │ ├── silverlight.ts │ │ ├── cookiesEnabled.ts │ │ ├── fileAPIs.ts │ │ ├── webkitAPIs.ts │ │ ├── mediaCapabilities.ts │ │ ├── storageQuota.ts │ │ ├── computedStyleProperties.ts │ │ ├── symbolProperties.ts │ │ ├── colorSpaceSupport.ts │ │ ├── canvas │ │ │ ├── utils.ts │ │ │ ├── index.ts │ │ │ ├── images.ts │ │ │ └── detect.ts │ │ ├── contrastPreference.ts │ │ ├── monochromeDepth.ts │ │ ├── speechSynthesis.ts │ │ ├── touchSupport.ts │ │ ├── fontRendering.ts │ │ ├── timezone.ts │ │ ├── screen-frame │ │ │ ├── index.ts │ │ │ ├── utils.ts │ │ │ └── screenFrame.ts │ │ ├── systemInfo.ts │ │ ├── screenResolution.ts │ │ ├── drmSupport.ts │ │ ├── vendorFlavors.ts │ │ ├── cssSupport.ts │ │ ├── webgpu │ │ │ ├── apiStatuses.ts │ │ │ ├── index.ts │ │ │ └── webgpuParams.ts │ │ ├── buildInObjects.ts │ │ ├── hdcp.ts │ │ ├── webgl │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── intl.ts │ │ ├── client-rects │ │ │ ├── index.ts │ │ │ └── utils.ts │ │ ├── math.ts │ │ ├── mediaDecodingCapabilities.ts │ │ ├── navigator.ts │ │ ├── index.ts │ │ ├── audio │ │ │ └── utils.ts │ │ └── fonts.ts │ ├── contextual-client-parameters │ │ ├── globalObjects.ts │ │ ├── network.ts │ │ ├── battery.ts │ │ ├── memory.ts │ │ ├── index.ts │ │ ├── screen.ts │ │ ├── location.ts │ │ ├── devToolsDetector.ts │ │ ├── performance.ts │ │ ├── webrtc.ts │ │ └── geolocation.ts │ ├── utils │ │ ├── enums.ts │ │ ├── load.ts │ │ ├── browser.ts │ │ └── constants.ts │ ├── index.ts │ ├── MixVisit.ts │ └── global.d.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── vite.config.js │ ├── package.json │ └── README.md ├── .editorconfig ├── .gitignore ├── .prettierignore ├── .prettierrc ├── package.json ├── LICENSE ├── README.md └── CODE_OF_CONDUCT.md /.yarnrc: -------------------------------------------------------------------------------- 1 | --engines-node >=20.9.0 -------------------------------------------------------------------------------- /example/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL_STORAGE_VARS = { VISITOR_DATA: 'visitorData' }; 2 | -------------------------------------------------------------------------------- /example/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/src/routes/+layout.js: -------------------------------------------------------------------------------- 1 | // This can be false if you're using a fallback (i.e. SPA mode) 2 | export const prerender = true; 3 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parameters'; 2 | export * from './base'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/java.ts: -------------------------------------------------------------------------------- 1 | export function isJavaEnabled(): boolean { 2 | return navigator.javaEnabled(); 3 | } 4 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/activeX.ts: -------------------------------------------------------------------------------- 1 | export function isHaveActiveX(): boolean { 2 | return !!window.ActiveXObject; 3 | } 4 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/colorDepth.ts: -------------------------------------------------------------------------------- 1 | export function getColorDepth(): number { 2 | return window.screen.colorDepth; 3 | } 4 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/types/parameters/canvas.ts: -------------------------------------------------------------------------------- 1 | export type CanvasContext = WebGLRenderingContext & { readonly canvas: HTMLCanvasElement }; 2 | -------------------------------------------------------------------------------- /example/src/lib/enums.ts: -------------------------------------------------------------------------------- 1 | export const enum VisitStatus { 2 | Prev = 'PREVIOUS VISIT', 3 | Curr = 'CURRENT VISIT', 4 | First = 'FIRST VISIT', 5 | } 6 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/devicePixelRatio.ts: -------------------------------------------------------------------------------- 1 | export function getDevicePixelRatio(): number { 2 | return window.devicePixelRatio; 3 | } 4 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/openDatabase.ts: -------------------------------------------------------------------------------- 1 | export function isHaveOpenDatabase(): boolean | null { 2 | return !!window.openDatabase; 3 | } 4 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/types/parameters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './canvas'; 2 | export * from './webgl'; 3 | export * from './webgpu'; 4 | export * from './screenFrame'; 5 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/globalPrivacyControl.ts: -------------------------------------------------------------------------------- 1 | export function getGlobalPrivacyControl(): boolean | null { 2 | return navigator.globalPrivacyControl ?? null; 3 | } 4 | -------------------------------------------------------------------------------- /example/src/lib/components/sections/About.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/battery.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export function isHaveBatteryAPI(): boolean { 4 | return !!hasProperty(navigator, 'getBattery'); 5 | } 6 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/bluetooth.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export function isHaveBluetoothAPI(): boolean { 4 | return hasProperty(navigator, 'bluetooth'); 5 | } 6 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/network.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export async function isHaveNetworkAPI(): Promise { 4 | return hasProperty(navigator, 'connection'); 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig site: https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/types/parameters/screenFrame.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The order matches the CSS side order: top, right, bottom, left 3 | */ 4 | export type FrameSize = [ 5 | number | null, 6 | number | null, 7 | number | null, 8 | number | null, 9 | ]; 10 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/navigatorProperties.ts: -------------------------------------------------------------------------------- 1 | export function getNavigatorProperties(): string[] { 2 | const result: string[] = []; 3 | 4 | for (const key in window.navigator) { 5 | result.push(key); 6 | } 7 | 8 | return result; 9 | } 10 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/sessionStorage.ts: -------------------------------------------------------------------------------- 1 | export function isHaveSessionStorage(): boolean { 2 | try { 3 | return !!window.sessionStorage; 4 | } catch (err) { 5 | /* SecurityError when referencing it means it exists */ 6 | return true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/architecture.ts: -------------------------------------------------------------------------------- 1 | export function getArchitecture(): number { 2 | const float32 = new Float32Array(1); 3 | const uint8 = new Uint8Array(float32.buffer); 4 | float32[0] = Infinity; 5 | float32[0] -= float32[0]; 6 | 7 | return uint8[3]; 8 | } 9 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type Values = T[keyof T]; 2 | export type MaybePromise = Promise | T; 3 | export type UnwrapPromise = T extends PromiseLike ? Awaited : T; 4 | 5 | export type Nullable = { 6 | [P in keyof T]: T[P] | null; 7 | }; 8 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | /dist 9 | 10 | # OS 11 | .DS_Store 12 | Thumbs.db 13 | 14 | # Env 15 | .env 16 | .env.* 17 | !.env.example 18 | !.env.test 19 | 20 | # Vite 21 | vite.config.js.timestamp-* 22 | vite.config.ts.timestamp-* 23 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/localStorage.ts: -------------------------------------------------------------------------------- 1 | // https://bugzilla.mozilla.org/show_bug.cgi?id=781447 2 | export function isHaveLocalStorage(): boolean { 3 | try { 4 | return !!window.localStorage; 5 | } catch (err) { 6 | /* SecurityError when referencing it means it exists */ 7 | return true; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | export default { 5 | preprocess: vitePreprocess(), 6 | kit: { 7 | adapter: adapter({ 8 | pages: 'dist', 9 | assets: 'dist', 10 | strict: true, 11 | }), 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /example/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/globalObjects.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export function getGlobalObjects(): string[] { 4 | const result: string[] = []; 5 | 6 | for (const key in window) { 7 | if (hasProperty(window, key)) { 8 | result.push(key); 9 | } 10 | } 11 | 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/static/_app/location-marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/audioBaseLatency.ts: -------------------------------------------------------------------------------- 1 | export function getAudioContextBaseLatency(): number | null { 2 | // @ts-ignore ts(2551) 3 | const AudioContext = window.AudioContext || window.webkitAudioContext; 4 | if (!AudioContext) { 5 | return null; 6 | } 7 | 8 | const audioContext = new AudioContext(); 9 | 10 | return audioContext.baseLatency ?? null; 11 | } 12 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ESNext", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "moduleResolution": "bundler", 8 | "types": ["@webgpu/types"], 9 | }, 10 | "include": [ 11 | "src/**/*", 12 | "./vite.config.js", 13 | ], 14 | "exclude": [ 15 | "node_modules", 16 | "dist", 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | resolve: { 7 | alias: { 8 | '@components': '/src/lib/components', 9 | '@api': '/src/lib/api', 10 | '@hooks': '/src/lib/hooks', 11 | '@services': '/src/lib/services', 12 | '@utils': '/src/lib/utils', 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/hdr.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueryMatcher } from '../utils/helpers'; 2 | 3 | export function isHDR(): boolean | null { 4 | const matchForDynamicRange = mediaQueryMatcher('dynamic-range') as (value: string) => boolean; 5 | 6 | if (matchForDynamicRange('high')) { 7 | return true; 8 | } 9 | 10 | if (matchForDynamicRange('standard')) { 11 | return false; 12 | } 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /example/src/lib/components/sections/index.ts: -------------------------------------------------------------------------------- 1 | export { default as About } from './About.svelte'; 2 | export { default as ClientData } from './ClientData.svelte'; 3 | export { default as Durations } from './Durations.svelte'; 4 | export { default as Errors } from './Errors.svelte'; 5 | export { default as Footer } from './Footer.svelte'; 6 | export { default as UsageExample } from './UsageExample.svelte'; 7 | export { default as Visitor } from './Visitor.svelte'; 8 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/utils/enums.ts: -------------------------------------------------------------------------------- 1 | export const enum CanvasImageStatus { 2 | UNSUPPORTED = 'unsupported', 3 | SKIPPED = 'skipped', 4 | UNSTABLE = 'unstable', 5 | } 6 | 7 | export enum ErrorType { 8 | TIMEOUT = 'TimeoutError', 9 | INTERNAL = 'InternalError', 10 | RESPONSE = 'ResponseError', 11 | } 12 | 13 | export const enum ContrastPreferenceStatus { 14 | LESS = -1, 15 | NONE = 0, 16 | MORE = 1, 17 | FORCED_COLORS = 10, 18 | } 19 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/forcedColors.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueryMatcher } from '../utils/helpers'; 2 | 3 | export function areColorsForced(): boolean | null { 4 | const matchForForcedColors = mediaQueryMatcher('forced-colors') as (value: string) => boolean; 5 | 6 | if (matchForForcedColors('active')) { 7 | return true; 8 | } 9 | 10 | if (matchForForcedColors('none')) { 11 | return false; 12 | } 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | build/ 7 | out/ 8 | 9 | # Log files 10 | *.log 11 | 12 | # Temporary files 13 | tmp/ 14 | temp/ 15 | 16 | # Configuration files 17 | # *.config.js 18 | # *.config.ts 19 | *.json 20 | *.yml 21 | *.yaml 22 | 23 | # VSCode settings 24 | .vscode/ 25 | 26 | # Compiled files 27 | *.d.ts 28 | 29 | # Ignore specific files and directories 30 | public/ 31 | *.min.js 32 | *.bundle.js 33 | *.map -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/invertedColors.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueryMatcher } from '../utils/helpers'; 2 | 3 | export function areColorsInverted(): boolean | null { 4 | const matchForInvertedColors = mediaQueryMatcher('inverted-colors') as (value: string) => boolean; 5 | 6 | if (matchForInvertedColors('inverted')) { 7 | return true; 8 | } 9 | 10 | if (matchForInvertedColors('none')) { 11 | return false; 12 | } 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/reducedMotion.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueryMatcher } from '../utils/helpers'; 2 | 3 | export function isMotionReduced(): boolean | null { 4 | const matchForReducedMotion = mediaQueryMatcher('prefers-reduced-motion') as (value: string) => boolean; 5 | 6 | if (matchForReducedMotion('reduce')) { 7 | return true; 8 | } 9 | 10 | if (matchForReducedMotion('no-preference')) { 11 | return false; 12 | } 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/scheduling.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | type SchedulingData = { 4 | isInputPending: boolean | null; 5 | }; 6 | 7 | export function schedulingData(): SchedulingData | null { 8 | if (!hasProperty(navigator, 'scheduling')) { 9 | return null; 10 | } 11 | 12 | return { 13 | isInputPending: navigator.scheduling.isInputPending 14 | ? navigator.scheduling.isInputPending() 15 | : null, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/ToggleIcon.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /example/src/lib/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | export async function fetchJSON(request: string): Promise { 2 | try { 3 | const response = await fetch(request); 4 | const contentType = response.headers.get('content-type'); 5 | if (!(contentType && contentType.includes('application/json'))) { 6 | throw new TypeError("Oops, we haven't got JSON!"); 7 | } 8 | 9 | return await response.json(); 10 | } catch (error) { 11 | console.error('Error:', error); 12 | 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/flash.ts: -------------------------------------------------------------------------------- 1 | export function isHaveFlash(): boolean { 2 | const { mimeTypes } = navigator; 3 | let flash: boolean = false; 4 | 5 | try { 6 | flash = new ActiveXObject('ShockwaveFlash.ShockwaveFlash'); 7 | } catch (err) { 8 | if ( 9 | mimeTypes 10 | && mimeTypes['application/x-shockwave-flash'] 11 | && mimeTypes['application/x-shockwave-flash'].enabledPlugin 12 | ) { 13 | flash = true; 14 | } 15 | } 16 | 17 | return flash; 18 | } 19 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/reducedTransparency.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueryMatcher } from '../utils/helpers'; 2 | 3 | export function isTransparencyReduced(): boolean | null { 4 | const matchForReducedTransparency = mediaQueryMatcher('prefers-reduced-transparency') as (value: string) => boolean; 5 | 6 | if (matchForReducedTransparency('reduce')) { 7 | return true; 8 | } 9 | 10 | if (matchForReducedTransparency('no-preference')) { 11 | return false; 12 | } 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/colorGamut.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueryMatcher } from '../utils/helpers'; 2 | 3 | export type ColorGamut = 'srgb' | 'p3' | 'rec2020'; 4 | 5 | export function getColorGamut(): ColorGamut | null { 6 | // rec2020 includes p3 and p3 includes srgb 7 | for (const gamut of ['srgb', 'p3', 'rec2020'] as const) { 8 | const isMatchesTrue = mediaQueryMatcher('color-gamut', gamut) as boolean; 9 | if (isMatchesTrue) { 10 | return gamut; 11 | } 12 | } 13 | 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/NoJSInfo.svelte: -------------------------------------------------------------------------------- 1 | JavaScript is disabled 2 |
3 |

4 | It appears that JavaScript is currently disabled or not supported in your browser. MixVisit requires JavaScript to 5 | function properly, as it relies on advanced client-side fingerprinting technologies including Canvas Fingerprinting 6 | and real-time IP detection. 7 |

8 |

Please enable JavaScript in your browser settings to take full advantage of MixVisit’s capabilities.

9 |
10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "all", 6 | "printWidth": 120, 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "useTabs": false, 10 | "endOfLine": "lf", 11 | "plugins": ["prettier-plugin-svelte"], 12 | "overrides": [ 13 | { 14 | "files": "*.svelte", 15 | "options": { 16 | "parser": "svelte" 17 | } 18 | }, 19 | { 20 | "files": "*.ts", 21 | "options": { 22 | "parser": "typescript" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /example/src/lib/components/sections/ClientData.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | {#if clientData} 14 | 15 | 16 | 17 | {/if} 18 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/indexedDB.ts: -------------------------------------------------------------------------------- 1 | import { isEdgeHTML, isTrident } from '../utils/browser'; 2 | 3 | export function isHaveIndexedDB(): boolean | null { 4 | // IE and Edge don't allow accessing indexedDB in private mode, therefore IE and Edge will have different 5 | // visitor identifier in normal and private modes. 6 | if (isTrident() || isEdgeHTML()) { 7 | return null; 8 | } 9 | 10 | try { 11 | return !!window.indexedDB; 12 | } catch (err) { 13 | /* SecurityError when referencing it means it exists */ 14 | return true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/silverlight.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty, TDef } from '../utils/helpers'; 2 | 3 | export function isHaveSilverlight(): boolean { 4 | if (hasProperty(window, 'ActiveXObject')) { 5 | try { 6 | // eslint-disable-next-line no-new 7 | new window.ActiveXObject('AgControl.AgControl'); 8 | 9 | return true; 10 | } catch (err) { 11 | return false; 12 | } 13 | } 14 | 15 | const mimeType = navigator.mimeTypes['application/x-silverlight-2']; 16 | 17 | return !TDef.isUndefined(mimeType) && !TDef.isNull(mimeType.enabledPlugin); 18 | } 19 | -------------------------------------------------------------------------------- /example/src/lib/components/sections/Footer.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

9 | Copyright © {productYears} MixVisitJS. All rights reserved. 10 | MIT - License 11 |

12 |
13 | 14 | 20 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/cookiesEnabled.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/cookies.js Taken from here 3 | */ 4 | export function areCookiesEnabled(): boolean { 5 | try { 6 | // Create cookie 7 | document.cookie = 'cookietest=1; SameSite=Strict;'; 8 | const result = document.cookie.indexOf('cookietest=') !== -1; 9 | 10 | // Delete cookie 11 | document.cookie = 'cookietest=1; SameSite=Strict; expires=Thu, 01-Jan-1970 00:00:01 GMT'; 12 | 13 | return result; 14 | } catch (err) { 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/fileAPIs.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export function getFileAPIsInfo(): string[] { 4 | const fileAPIs = [ 5 | 'FileReader', 6 | 'FileList', 7 | 'File', 8 | 'FileSystemDirectoryHandle', 9 | 'FileSystemFileHandle', 10 | 'FileSystemHandle', 11 | 'FileSystemWritableFileStream', 12 | 'showOpenFilePicker', 13 | 'showSaveFilePicker', 14 | 'webkitRequestFileSystem', 15 | 'webkitResolveLocalFileSystemURL', 16 | ] as const; 17 | 18 | return fileAPIs.filter((api) => hasProperty(window, api)); 19 | } 20 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/webkitAPIs.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export function getWebKitAPIsInfo(): string[] { 4 | const webkitAPIs = [ 5 | 'onwebkitanimationend', 6 | 'onwebkitanimationiteration', 7 | 'onwebkitanimationstart', 8 | 'onwebkittransitionend', 9 | 'onwebkitfullscreenchange', 10 | 'onwebkitfullscreenerror', 11 | 'webkitMatchesSelector', 12 | 'webkitRequestFullScreen', 13 | 'webkitRequestFullscreen', 14 | ] as const; 15 | 16 | return webkitAPIs.filter((api) => hasProperty(document.documentElement, api)); 17 | } 18 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/index.ts: -------------------------------------------------------------------------------- 1 | import { MixVisit } from './MixVisit'; 2 | 3 | export type { ClientParameters } from './client-parameters'; 4 | export type { ContextualClientParameters } from './contextual-client-parameters'; 5 | export type { 6 | ClientData, 7 | LoadOptions, 8 | MixVisitInterface, 9 | ParametersResult, 10 | Result, 11 | } from './types'; 12 | 13 | export { getFonts } from './client-parameters/fonts'; 14 | export { loadParameters } from './utils/load'; 15 | export { withIframe, type WithIframeProps } from './utils/helpers'; 16 | 17 | export { MixVisit }; 18 | export default MixVisit; 19 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/mediaCapabilities.ts: -------------------------------------------------------------------------------- 1 | export function getMediaCapabilities(): MediaTrackSupportedConstraints | null { 2 | if (!(navigator.mediaDevices && navigator.mediaDevices.getSupportedConstraints)) { 3 | console.warn('MediaDevices API is not supported'); 4 | 5 | return null; 6 | } 7 | 8 | const result: MediaTrackSupportedConstraints = {}; 9 | const supportedConstraints = navigator.mediaDevices.getSupportedConstraints(); 10 | 11 | for (const constraint in supportedConstraints) { 12 | result[constraint] = supportedConstraints[constraint]; 13 | } 14 | 15 | return result; 16 | } 17 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/network.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export function getNetworkData(): Connection | null { 4 | if (hasProperty(navigator, 'connection')) { 5 | const { connection } = navigator; 6 | 7 | return { 8 | type: connection.type ?? null, 9 | effectiveType: connection.effectiveType ?? null, 10 | downlink: connection.downlink ?? null, 11 | downlinkMax: connection.downlinkMax ?? null, 12 | rtt: connection.rtt ?? null, 13 | saveData: connection.saveData ?? null, 14 | }; 15 | } 16 | 17 | return null; 18 | } 19 | -------------------------------------------------------------------------------- /example/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | MixVisitJS - JS Fingerprint to identify & track devices 9 | 10 | %sveltekit.head% 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/storageQuota.ts: -------------------------------------------------------------------------------- 1 | export async function getStorageQuota(): Promise { 2 | if (navigator.storage && navigator.storage.estimate) { 3 | try { 4 | const estimate = await navigator.storage.estimate(); 5 | 6 | return { 7 | quota: estimate.quota, 8 | usage: estimate.usage, 9 | } as StorageEstimate; 10 | } catch (err) { 11 | console.error('Error when retrieving storage information: ', err); 12 | 13 | return null; 14 | } 15 | } 16 | 17 | console.warn('The storage API is not supported'); 18 | 19 | return null; 20 | } 21 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/index.ts: -------------------------------------------------------------------------------- 1 | export { default as AboutInfo } from './AboutInfo.svelte'; 2 | export { default as Accordion } from './Accordion.svelte'; 3 | export { default as Badges } from './Badges.svelte'; 4 | export { default as ErrorBadge } from './ErrorBadge.svelte'; 5 | export { default as GoToTop } from './GoToTop.svelte'; 6 | export { default as NoJSInfo } from './NoJSInfo.svelte'; 7 | export { default as ToggleIcon } from './ToggleIcon.svelte'; 8 | export { default as VisitMapSlider } from './VisitMapSlider.svelte'; 9 | export { default as VisitorInfo } from './VisitorInfo.svelte'; 10 | export { default as VisitorRow } from './VisitorRow.svelte'; 11 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/battery.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | type BatteryInfo = { 4 | charging: boolean; 5 | chargingTime: number; 6 | dischargingTime: number; 7 | level: number; 8 | }; 9 | 10 | export async function getBatteryData(): Promise { 11 | if (hasProperty(navigator, 'getBattery')) { 12 | const battery = await navigator.getBattery(); 13 | 14 | return { 15 | charging: battery.charging, 16 | chargingTime: battery.chargingTime, 17 | dischargingTime: battery.dischargingTime, 18 | level: battery.level, 19 | }; 20 | } 21 | 22 | return null; 23 | } 24 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/computedStyleProperties.ts: -------------------------------------------------------------------------------- 1 | export function getComputedStyleProperties(): string[] { 2 | const div = document.createElement('div'); 3 | document.body.appendChild(div); 4 | 5 | const computedStyle = window.getComputedStyle(div); 6 | 7 | const properties: string[] = []; 8 | for (let index = 0; index < computedStyle.length; index++) { 9 | properties.push(computedStyle[index]); 10 | } 11 | 12 | for (const prop in computedStyle) { 13 | if (!properties.includes(prop) && Number.isNaN(Number(prop))) { 14 | properties.push(prop); 15 | } 16 | } 17 | 18 | document.body.removeChild(div); 19 | 20 | return properties.sort(); 21 | } 22 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/symbolProperties.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export function getSymbolPropertiesInfo(): string[] { 4 | const symbolProperties = [ 5 | 'length', 6 | 'name', 7 | 'prototype', 8 | 'for', 9 | 'keyFor', 10 | 'asyncIterator', 11 | 'hasInstance', 12 | 'isConcatSpreadable', 13 | 'iterator', 14 | 'match', 15 | 'matchAll', 16 | 'replace', 17 | 'search', 18 | 'species', 19 | 'split', 20 | 'toPrimitive', 21 | 'toStringTag', 22 | 'unscopables', 23 | 'dispose', 24 | ] as const; 25 | 26 | return symbolProperties.filter((prop) => hasProperty(Symbol, prop)); 27 | } 28 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler", 13 | "paths": { 14 | "$lib/*": ["./src/lib/*"], 15 | "@components/*": ["./src/lib/components/*"], 16 | "@api/*": ["./src/lib/api/*"], 17 | "@hooks/*": ["./src/lib/hooks/*"], 18 | "@services/*": ["./src/lib/services/*"], 19 | "@utils/*": ["./src/lib/utils/*"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/vite.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { resolve } from 'node:path'; 3 | 4 | import { defineConfig } from 'vite'; 5 | import dts from 'vite-plugin-dts'; 6 | 7 | export default defineConfig({ 8 | plugins: [dts({ rollupTypes: true })], 9 | build: { 10 | target: 'esnext', 11 | lib: { 12 | entry: resolve(__dirname, 'src/index.ts'), 13 | name: 'MV', 14 | formats: [ 15 | 'cjs', 16 | 'es', 17 | 'umd', 18 | ], 19 | fileName: (format) => `index.${format}.js`, 20 | }, 21 | rollupOptions: { 22 | output: { 23 | exports: 'named', 24 | }, 25 | }, 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/colorSpaceSupport.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | type ColorSpaceSupport = { 4 | srgb: boolean; 5 | p3: boolean; 6 | rec2020: boolean; 7 | }; 8 | 9 | export function getColorSpaceSupport(): ColorSpaceSupport { 10 | const colorSpaces = ['srgb', 'p3', 'rec2020']; 11 | const result = {} as ColorSpaceSupport; 12 | 13 | if (hasProperty(window, 'matchMedia')) { 14 | colorSpaces.forEach((space) => { 15 | result[space] = window.matchMedia(`(color-gamut: ${space})`).matches; 16 | }); 17 | } else { 18 | colorSpaces.forEach((space) => { 19 | result[space] = false; 20 | }); 21 | } 22 | 23 | return result; 24 | } 25 | -------------------------------------------------------------------------------- /example/src/lib/services/mixvisit.ts: -------------------------------------------------------------------------------- 1 | import { MixVisit, type ClientData } from '@mix-visit/lite'; 2 | 3 | import type { MixVisitResult } from '$lib/types'; 4 | 5 | /** 6 | * Retrieves client data using the MixVisit library. 7 | * 8 | * @returns {Promise} A promise that resolves to an object 9 | * containing the fingerprint hash, load time, and client data. 10 | */ 11 | export async function getMixVisitClientData(): Promise { 12 | const mixvisit = new MixVisit(); 13 | await mixvisit.load({ timeout: 1000, exclude: ['webrtc'] }); 14 | 15 | const data: ClientData = mixvisit.get(); 16 | const { fingerprintHash, loadTime } = mixvisit; 17 | 18 | return { 19 | fingerprintHash, 20 | loadTime, 21 | data, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/canvas/utils.ts: -------------------------------------------------------------------------------- 1 | export function createCanvas(): readonly [HTMLCanvasElement, CanvasRenderingContext2D | null] { 2 | const canvas = document.createElement('canvas'); 3 | canvas.width = 1; 4 | canvas.height = 1; 5 | 6 | return [canvas, canvas.getContext('2d')] as const; 7 | } 8 | 9 | export function isWindingSupported(context: CanvasRenderingContext2D): boolean { 10 | context.rect(0, 0, 10, 10); 11 | context.rect(2, 2, 6, 6); 12 | 13 | return !context.isPointInPath(5, 5, 'evenodd'); 14 | } 15 | 16 | export function isCanvasSupported(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D | null): boolean { 17 | return !!(context && canvas.toDataURL) && canvas.toDataURL().indexOf('data:image/png;base64') !== -1; 18 | } 19 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/memory.ts: -------------------------------------------------------------------------------- 1 | interface PerformanceMemory { 2 | jsHeapSizeLimit: number; 3 | totalJSHeapSize: number; 4 | usedJSHeapSize: number; 5 | } 6 | 7 | interface ExtendedPerformance extends Performance { 8 | memory?: PerformanceMemory; 9 | } 10 | 11 | export function getMemoryInfo(): PerformanceMemory | null { 12 | const { performance }: { performance: ExtendedPerformance } = window; 13 | 14 | if (!(performance && performance.memory)) { 15 | console.warn('The memory API is not supported'); 16 | 17 | return null; 18 | } 19 | 20 | return { 21 | jsHeapSizeLimit: performance.memory.jsHeapSizeLimit, 22 | totalJSHeapSize: performance.memory.totalJSHeapSize, 23 | usedJSHeapSize: performance.memory.usedJSHeapSize, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mixvisit", 3 | "engines": { 4 | "node": ">=20.9.0" 5 | }, 6 | "private": true, 7 | "type": "module", 8 | "packageManager": "yarn@1.22.22", 9 | "workspaces": [ 10 | "packages/*", 11 | "example" 12 | ], 13 | "scripts": { 14 | "build": "yarn workspaces run build", 15 | "build:example": "yarn workspace @mix-visit/example run build", 16 | "build:lite": "yarn workspace @mix-visit/lite run build", 17 | "dev:example": "yarn workspace @mix-visit/example run dev", 18 | "dev:lite": "yarn workspace @mix-visit/lite run dev", 19 | "prettier": "prettier . --write" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^22.15.3", 23 | "prettier": "^3.5.3", 24 | "prettier-plugin-svelte": "^3.3.3", 25 | "typescript": "^5.8.3", 26 | "vite": "^6.3.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/contrastPreference.ts: -------------------------------------------------------------------------------- 1 | import { ContrastPreferenceStatus } from '../utils/enums'; 2 | import { mediaQueryMatcher } from '../utils/helpers'; 3 | 4 | export function getContrastPreference(): number | null { 5 | const matchForPrefersContrast = mediaQueryMatcher('prefers-contrast') as (value: string) => boolean; 6 | 7 | if (matchForPrefersContrast('no-preference')) { 8 | return ContrastPreferenceStatus.NONE; 9 | } 10 | 11 | if (matchForPrefersContrast('high') || matchForPrefersContrast('more')) { 12 | return ContrastPreferenceStatus.MORE; 13 | } 14 | 15 | if (matchForPrefersContrast('low') || matchForPrefersContrast('less')) { 16 | return ContrastPreferenceStatus.LESS; 17 | } 18 | 19 | if (matchForPrefersContrast('forced')) { 20 | return ContrastPreferenceStatus.FORCED_COLORS; 21 | } 22 | 23 | return null; 24 | } 25 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/monochromeDepth.ts: -------------------------------------------------------------------------------- 1 | import { mediaQueryMatcher } from '../utils/helpers'; 2 | 3 | /** 4 | * If the display is monochrome (e.g. black&white), the value will be ≥0 and will mean the number of bits per pixel. 5 | * If the display is not monochrome, the returned value will be 0. 6 | * If the browser doesn't support this feature, the returned value will be null. 7 | */ 8 | export function getMonochromeDepth(): number | null { 9 | const maxValueToCheck = 100; 10 | const mediaIsNotSupport = !mediaQueryMatcher('min-monochrome', '0') as boolean; 11 | if (mediaIsNotSupport) { 12 | return null; 13 | } 14 | 15 | for (let i = 0; i <= maxValueToCheck; ++i) { 16 | const isMatchesTrue = mediaQueryMatcher('max-monochrome', i.toString()) as boolean; 17 | if (isMatchesTrue) { 18 | return i; 19 | } 20 | } 21 | 22 | throw new Error('Too high value'); 23 | } 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mix-visit/example", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "engines": { 7 | "node": ">=20.9.0" 8 | }, 9 | "packageManager": "yarn@1.22.22", 10 | "scripts": { 11 | "dev": "vite dev", 12 | "build": "vite build", 13 | "preview": "vite preview", 14 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 15 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 16 | }, 17 | "dependencies": { 18 | "@mix-visit/lite": "1.0.14", 19 | "leaflet": "^1.9.4", 20 | "tippy.js": "^6.3.7" 21 | }, 22 | "devDependencies": { 23 | "@iconify/svelte": "^4.0.2", 24 | "@sveltejs/adapter-static": "^3.0.8", 25 | "@sveltejs/kit": "^2.20.7", 26 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 27 | "@types/leaflet": "^1.9.12", 28 | "svelte": "^5.28.2", 29 | "svelte-check": "^4.1.6", 30 | "svelte-highlight": "^7.8.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/Accordion.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 | 20 | 21 | {#if title} 22 |

{title}

23 | {/if} 24 |
25 |
26 | 27 |
28 |
29 | 30 | 48 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/speechSynthesis.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | export async function getSpeechSynthesisVoices(): Promise { 4 | if (hasProperty(window, 'speechSynthesis')) { 5 | return new Promise((resolve) => { 6 | let voices = window.speechSynthesis.getVoices(); 7 | 8 | if (voices.length) { 9 | resolve(transformVoices(voices)); 10 | } else { 11 | window.speechSynthesis.onvoiceschanged = () => { 12 | voices = window.speechSynthesis.getVoices(); 13 | resolve(transformVoices(voices)); 14 | }; 15 | } 16 | }); 17 | } 18 | 19 | return null; 20 | } 21 | 22 | function transformVoices(voices: SpeechSynthesisVoice[]): SpeechSynthesisVoice[] { 23 | return voices.map((voice) => ({ 24 | default: voice.default, 25 | lang: voice.lang, 26 | localService: voice.localService, 27 | name: voice.name, 28 | voiceURI: voice.voiceURI, 29 | })); 30 | } 31 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/touchSupport.ts: -------------------------------------------------------------------------------- 1 | import { toInt, hasProperty } from '../utils/helpers'; 2 | 3 | type TouchSupport = { 4 | maxTouchPoints: number; 5 | touchEvent: boolean; 6 | touchStart: boolean; 7 | }; 8 | 9 | /** 10 | * This is a crude and primitive touch screen detection. 11 | * It's not possible to currently reliably detect the availability of a touch screen with a JS, 12 | * without actually subscribing to a touch event. 13 | */ 14 | export function getTouchSupport(): TouchSupport { 15 | let maxTouchPoints = 0; 16 | let touchEvent: boolean; 17 | 18 | if (navigator.maxTouchPoints !== undefined) { 19 | maxTouchPoints = toInt(navigator.maxTouchPoints); 20 | } 21 | 22 | try { 23 | document.createEvent('TouchEvent'); 24 | touchEvent = true; 25 | } catch { 26 | touchEvent = false; 27 | } 28 | 29 | const touchStart = hasProperty(window, 'ontouchstart'); 30 | 31 | return { 32 | maxTouchPoints, 33 | touchEvent, 34 | touchStart, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/index.ts: -------------------------------------------------------------------------------- 1 | import { getBatteryData } from './battery'; 2 | import { isDevToolsOpened } from './devToolsDetector'; 3 | import { getGeolocation } from './geolocation'; 4 | import { getGlobalObjects } from './globalObjects'; 5 | import { getLocation } from './location'; 6 | import { getMemoryInfo } from './memory'; 7 | import { getNetworkData } from './network'; 8 | import { getPerformanceData } from './performance'; 9 | import { getScreenData } from './screen'; 10 | import { getWebrtcIPs } from './webrtc'; 11 | 12 | export type ContextualClientParameters = typeof contextualClientParameters; 13 | 14 | export const contextualClientParameters = { 15 | globalObjests: getGlobalObjects, 16 | devToolsOpen: isDevToolsOpened, 17 | screen: getScreenData, 18 | location: getLocation, 19 | geolocation: getGeolocation, 20 | memory: getMemoryInfo, 21 | performance: getPerformanceData, 22 | networkInfo: getNetworkData, 23 | batteryInfo: getBatteryData, 24 | webrtc: getWebrtcIPs, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/fontRendering.ts: -------------------------------------------------------------------------------- 1 | type FontRendering = { 2 | font: 'Consolas' | 'Windows sans-serif' | 'Windows serif' | 'mix Consolas'; 3 | offsetWidth: number; 4 | offsetHeight: number; 5 | }[]; 6 | 7 | export function getFontRendering(): FontRendering { 8 | const fonts = ['Consolas', 'Windows sans-serif', 'Windows serif', 'mix Consolas'] as const; 9 | 10 | const element = document.createElement('span'); 11 | element.style.position = 'absolute'; 12 | element.style.left = '-9999px'; 13 | element.style.fontSize = '12px'; 14 | element.style.lineHeight = 'normal'; 15 | element.textContent = 'abcdefghijklmnopqrstuvwxyz0123456789'; 16 | 17 | document.body.appendChild(element); 18 | 19 | const result = fonts.map((font) => { 20 | element.style.fontFamily = font; 21 | 22 | return { 23 | font, 24 | offsetWidth: element.offsetWidth, 25 | offsetHeight: element.offsetHeight, 26 | }; 27 | }); 28 | 29 | document.body.removeChild(element); 30 | 31 | return result; 32 | } 33 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/types/parameters/webgpu.ts: -------------------------------------------------------------------------------- 1 | export type WebGPUInfo = { 2 | statuses: WebGPUAPI; 3 | supportedAdapters?: WebGPUSupportedAdapters; 4 | }; 5 | 6 | export type WebGPUAPI = { 7 | api?: boolean; 8 | adapter?: boolean; 9 | device?: boolean; 10 | context?: boolean; 11 | compat?: boolean; 12 | offscreen?: boolean; 13 | twoD?: boolean; 14 | }; 15 | 16 | export type WebGPUSupportedAdapters = { 17 | fallback: WebGPUParams | null; 18 | highPerformance: WebGPUParams | null; 19 | }; 20 | 21 | export type WebGPUParams = { 22 | info: GPUAdapterInfo; 23 | limits: GPUSupportedLimits; 24 | features: string[]; 25 | textureFormatCapabilities: string[] | null; 26 | flags: { 27 | isCompatibilityMode: boolean; 28 | isFallbackAdapter: boolean; 29 | } 30 | }; 31 | 32 | export type WebGPUSupportedAdaptersParam = { 33 | fallback: AdaptersOption; 34 | highPerformance: AdaptersOption; 35 | }; 36 | 37 | export type AdaptersOption = { 38 | powerPreference: 'high-performance' | 'low-power'; 39 | forceFallbackAdapter?: boolean; 40 | }; 41 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/ErrorBadge.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | 40 | 47 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/timezone.ts: -------------------------------------------------------------------------------- 1 | import { toFloat } from '../utils/helpers'; 2 | 3 | export function getTimezone(): string { 4 | const DateTimeFormat = window.Intl?.DateTimeFormat; 5 | if (DateTimeFormat) { 6 | const timezone = new DateTimeFormat().resolvedOptions().timeZone; 7 | if (timezone) { 8 | return timezone; 9 | } 10 | } 11 | 12 | // For browsers that don't support timezone names 13 | const currentYear = new Date().getFullYear(); 14 | 15 | // The timezone offset may change over time due to daylight saving time (DST) shifts. 16 | // The non-DST timezone offset is used as the result timezone offset. 17 | // Since the DST season differs in the northern and the southern hemispheres, 18 | // both January and July timezones offsets are considered. 19 | 20 | // The minus is intentional because the JS offset is opposite to the real offset 21 | const offset = -Math.max( 22 | toFloat(new Date(currentYear, 0, 1).getTimezoneOffset()), 23 | toFloat(new Date(currentYear, 6, 1).getTimezoneOffset()), 24 | ); 25 | 26 | return `UTC${offset >= 0 ? '+' : ''}${offset}`; 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 Vitalij Ryndin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/screen-frame/index.ts: -------------------------------------------------------------------------------- 1 | import { getBaseScreenFrame } from './screenFrame'; 2 | import { processSize } from './utils'; 3 | import type { FrameSize } from '../../types'; 4 | import { isSafariWebKit, isWebKit, isWebKit616OrNewer } from '../../utils/browser'; 5 | 6 | /** 7 | * Sometimes the available screen resolution changes a bit, e.g. 1900x1440 → 1900x1439. 8 | * A possible reason: macOS Dock shrinks to fit more icons when there is too little space. 9 | * The rounding is used to mitigate the difference 10 | */ 11 | export async function getScreenFrame(): Promise { 12 | // The frame width is always 0 in private mode of Safari 17, so the frame is not used in Safari 17. 13 | if (isWebKit() && isWebKit616OrNewer() && isSafariWebKit()) { 14 | return null; 15 | } 16 | 17 | const frameSize = await getBaseScreenFrame(); 18 | 19 | // In fact, such code is used to avoid TypeScript issues without using `as` 20 | return [ 21 | processSize(frameSize[0]), 22 | processSize(frameSize[1]), 23 | processSize(frameSize[2]), 24 | processSize(frameSize[3]), 25 | ]; 26 | } 27 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/systemInfo.ts: -------------------------------------------------------------------------------- 1 | type SystemInfo = { 2 | colors: string[]; 3 | fonts: string[]; 4 | }; 5 | 6 | export function getSystemInfo(): SystemInfo | null { 7 | const systemFonts = ['serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui'] as const; 8 | const systemColors = [ 9 | 'ActiveText', 10 | 'ButtonFace', 11 | 'ButtonText', 12 | 'Canvas', 13 | 'CanvasText', 14 | 'Field', 15 | 'FieldText', 16 | 'GrayText', 17 | 'Highlight', 18 | 'HighlightText', 19 | 'LinkText', 20 | 'Mark', 21 | 'MarkText', 22 | 'VisitedText', 23 | ] as const; 24 | 25 | const canvas = document.createElement('canvas'); 26 | const ctx = canvas.getContext('2d'); 27 | 28 | if (!ctx) { 29 | console.warn('Failed to get canvas context'); 30 | 31 | return null; 32 | } 33 | 34 | const colors = systemColors.map((color) => { 35 | ctx.fillStyle = color; 36 | 37 | return ctx.fillStyle; 38 | }); 39 | 40 | const fonts = systemFonts.map((font) => { 41 | ctx.font = `12px ${font}`; 42 | 43 | return ctx.font; 44 | }); 45 | 46 | canvas.remove(); 47 | 48 | return { colors, fonts }; 49 | } 50 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/screenResolution.ts: -------------------------------------------------------------------------------- 1 | import { isSafariWebKit, isWebKit, isWebKit616OrNewer } from '../utils/browser'; 2 | import { replaceNaN, toInt } from '../utils/helpers'; 3 | 4 | type ScreenResolution = [number | null, number | null]; 5 | 6 | /** 7 | * A version of the entropy source with stabilization to make it suitable for static fingerprinting. 8 | * The window resolution is always the document size in private mode of Safari 17, 9 | * so the window resolution is not used in Safari 17. 10 | */ 11 | export function getScreenResolution(): ScreenResolution | null { 12 | // the window resolution is not used in Safari 17 13 | const isNewSafari = isWebKit() && isWebKit616OrNewer() && isSafariWebKit(); 14 | if (isNewSafari) { 15 | return null; 16 | } 17 | 18 | // Some browsers return screen resolution as strings, e.g. "1200", instead of a number, e.g. 1200. 19 | const parseDimension = (value: unknown) => replaceNaN(toInt(value), null); 20 | 21 | const dimensions = [ 22 | parseDimension(window.screen.width), 23 | parseDimension(window.screen.height), 24 | ] as ScreenResolution; 25 | 26 | dimensions.sort().reverse(); 27 | 28 | return dimensions; 29 | } 30 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/screen.ts: -------------------------------------------------------------------------------- 1 | type WindowSize = { 2 | width: number; 3 | height: number; 4 | }; 5 | 6 | type Screen = { 7 | colorDepth: number; 8 | pixelDepth: number; 9 | height: number; 10 | width: number; 11 | availHeight: number; 12 | availWidth: number; 13 | availTop: number; 14 | availLeft: number; 15 | windowSize: WindowSize; 16 | }; 17 | 18 | export function getScreenData() { 19 | const result = {} as Screen; 20 | 21 | const screenFields = [ 22 | 'colorDepth', 23 | 'pixelDepth', 24 | 'height', 25 | 'width', 26 | 'availHeight', 27 | 'availWidth', 28 | 'availTop', 29 | 'availLeft', 30 | 'windowSize', 31 | ] as const; 32 | 33 | screenFields.forEach((field) => { 34 | if (field === 'windowSize') { 35 | result.windowSize = getWindowSize(); 36 | } else if (window.screen[field] || Number.isInteger(window.screen[field])) { 37 | result[field] = window.screen[field]; 38 | } 39 | }); 40 | 41 | return result; 42 | } 43 | 44 | function getWindowSize(): WindowSize { 45 | return { 46 | width: document.body?.clientWidth || window.innerWidth, 47 | height: document.body?.clientHeight || window.innerHeight, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /example/src/lib/hooks/tooltip.ts: -------------------------------------------------------------------------------- 1 | import tippy from 'tippy.js'; 2 | import type { Instance, Props } from 'tippy.js'; 3 | 4 | /** 5 | * Creates a new tippy.js tooltip on the given element. 6 | * 7 | * @param {HTMLElement} node - The element to render the tooltip on. 8 | * @param {string | Partial} content - The content to render in the tooltip. If a string, the content is set to the string. If an object, the options are passed directly to the tippy.js constructor. 9 | * @returns {{ update: (content: string | Partial) => void, destroy: () => void }} 10 | * An object with two methods: `update` and `destroy`. `update` updates the content of the tooltip with new content, and `destroy` removes the tooltip from the DOM. 11 | */ 12 | export function tooltip( 13 | node: HTMLElement, 14 | content: string | Partial, 15 | ): { update: (content: string | Partial) => void; destroy: () => void } { 16 | const instance: Instance = tippy(node, typeof content === 'string' ? { content } : content); 17 | 18 | return { 19 | update(newContent: string | Partial) { 20 | instance.setProps(typeof newContent === 'string' ? { content: newContent } : newContent); 21 | }, 22 | destroy() { 23 | instance.destroy(); 24 | }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/GoToTop.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | 51 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/drmSupport.ts: -------------------------------------------------------------------------------- 1 | type DRMSupportInfo = { 2 | supported: boolean; 3 | systems?: { 4 | 'com.widevine.alpha': boolean; 5 | 'com.microsoft.playready': boolean; 6 | 'com.apple.fps.1_0': boolean; 7 | 'org.w3.clearkey': boolean; 8 | }; 9 | }; 10 | 11 | export async function getDRMSupportInfo(): Promise { 12 | if (!navigator.requestMediaKeySystemAccess) { 13 | return { supported: false }; 14 | } 15 | 16 | const drmSystems = ['com.widevine.alpha', 'com.microsoft.playready', 'com.apple.fps.1_0', 'org.w3.clearkey'] as const; 17 | const config = [ 18 | { 19 | initDataTypes: ['cenc'], 20 | audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }], 21 | videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }], 22 | }, 23 | ]; 24 | 25 | try { 26 | const results = await Promise.all( 27 | drmSystems.map((system) => navigator 28 | .requestMediaKeySystemAccess(system, config) 29 | .then(() => ({ [system]: true })) 30 | .catch(() => ({ [system]: false }))), 31 | ); 32 | 33 | return { 34 | supported: true, 35 | systems: Object.assign({}, ...results), 36 | }; 37 | } catch (err) { 38 | return { supported: false }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/vendorFlavors.ts: -------------------------------------------------------------------------------- 1 | const vendorFlavorFields = [ 2 | // Blink and some browsers on iOS 3 | 'chrome', 4 | // Safari on macOS 5 | 'safari', 6 | // Chrome on iOS (checked in 85 on 13 and 87 on 14) 7 | '__crWeb', 8 | '__gCrWeb', 9 | // Yandex Browser on iOS, macOS and Android (checked in 21.2 on iOS 14, macOS and Android) 10 | 'yandex', 11 | // Yandex Browser on iOS (checked in 21.2 on 14) 12 | '__yb', 13 | '__ybro', 14 | // Firefox on iOS (checked in 32 on 14) 15 | '__firefox__', 16 | // Edge on iOS (checked in 46 on 14) 17 | '__edgeTrackingPreventionStatistics', 18 | 'webkit', 19 | // Opera Touch on iOS (checked in 2.6 on 14) 20 | 'oprt', 21 | // Samsung Internet on Android (checked in 11.1) 22 | 'samsungAr', 23 | // UC Browser on Android (checked in 12.10 and 13.0) 24 | 'ucweb', 25 | 'UCShellJava', 26 | // Puffin on Android (checked in 9.0) 27 | 'puffinDevice', 28 | ] as const; 29 | 30 | export function getVendorFlavors(): string[] { 31 | const flavors: string[] = []; 32 | 33 | for (const key of vendorFlavorFields) { 34 | const value = (window as unknown as Record)[key]; 35 | if (value && typeof value === 'object') { 36 | flavors.push(key); 37 | } 38 | } 39 | 40 | return flavors.sort(); 41 | } 42 | -------------------------------------------------------------------------------- /example/src/lib/api/location.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { TDef } from '@utils/common'; 4 | import { fetchJSON } from '@utils/fetch'; 5 | 6 | import type { LocationDataRaw, LocationData } from '$lib/types'; 7 | 8 | /** 9 | * Gets the location data of the visitor. 10 | * 11 | * @returns The location data in the object format or null if the data can't be retrieved. 12 | */ 13 | export const getLocationData = async (): Promise => { 14 | try { 15 | const location = await fetchJSON('https://ipgeo.iphey.com/'); 16 | if (!location) { 17 | return null; 18 | } 19 | 20 | return Object.keys(location).reduce((result, key) => { 21 | if (['isEu', 'metroCode', 'regionCode', 'airport'].includes(key)) { 22 | return result; 23 | } 24 | 25 | if (key === 'timezone' && location[key]?.name) { 26 | result[key] = location[key].name; 27 | 28 | return result; 29 | } 30 | 31 | if (!TDef.isUndefined(location[key as keyof LocationDataRaw])) { 32 | (result as any)[key] = location[key as keyof LocationData]; 33 | } 34 | 35 | return result; 36 | }, {} as LocationData); 37 | } catch (err) { 38 | console.error('Error in getLocationData(): ', err); 39 | 40 | return null; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/VisitorInfo.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | {#if visitorInfo} 25 |
26 | {#each pickEntries(visitorInfo, sortedFields) as [field, value]} 27 | 28 | {/each} 29 |
30 | {/if} 31 | 32 | 50 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/cssSupport.ts: -------------------------------------------------------------------------------- 1 | const cssProperties = { 2 | 'accent-color': ['initial'], 3 | 'anchor-name': ['--tooltip'], 4 | 'border-end-end-radius': ['initial'], 5 | color: ['light-dark'], 6 | fill: ['context-fill', 'context-stroke'], 7 | float: ['inline-start', 'inline-end'], 8 | 'font-size': ['1cap', '1rcap'], 9 | 'grid-template-rows': ['subgrid'], 10 | 'paint-order': ['normal', 'stroke', 'markers', 'fill', 'revert'], 11 | stroke: ['context-fill', 'context-stroke'], 12 | 'text-decoration': ['spelling-error'], 13 | 'text-wrap': ['pretty'], 14 | 'transform-box': ['stroke-box'], 15 | } as const; 16 | 17 | type CSSSupport = { 18 | [K in keyof typeof cssProperties]: { 19 | [V in (typeof cssProperties)[K][number]]: boolean; 20 | }; 21 | }; 22 | 23 | export function getCSSSupport(): CSSSupport { 24 | const element = document.createElement('div'); 25 | document.body.appendChild(element); 26 | 27 | const result = {} as CSSSupport; 28 | 29 | for (const [property, values] of Object.entries(cssProperties)) { 30 | result[property] = {}; 31 | for (const value of values) { 32 | element.style[property] = value; 33 | result[property][value] = element.style[property] === value; 34 | } 35 | } 36 | 37 | document.body.removeChild(element); 38 | 39 | return result; 40 | } 41 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/webgpu/apiStatuses.ts: -------------------------------------------------------------------------------- 1 | import type { WebGPUAPI } from '../../types'; 2 | 3 | export async function getWebgGPUAPI(): Promise { 4 | const res: WebGPUAPI = {}; 5 | 6 | const canvas = document.createElement('canvas'); 7 | const offscreen = !!canvas.transferControlToOffscreen; 8 | const offscreenCanvas = offscreen && canvas.transferControlToOffscreen(); 9 | 10 | try { 11 | if (navigator.gpu) { 12 | res.api = true; 13 | } 14 | 15 | const adapter = await navigator.gpu.requestAdapter(); 16 | if (adapter) { 17 | res.adapter = true; 18 | 19 | const compatAdapter = await navigator.gpu.requestAdapter({ compatibilityMode: true }); 20 | res.compat = !!compatAdapter; 21 | 22 | const device = await adapter.requestDevice(); 23 | if (device) { 24 | res.device = true; 25 | if (offscreenCanvas) { 26 | res.context = !!offscreenCanvas.getContext('webgpu'); 27 | } 28 | } 29 | } 30 | } catch (err) { 31 | console.error(err); 32 | } 33 | 34 | try { 35 | const newOffscreenCanvas = new OffscreenCanvas(300, 150); 36 | const ctx = newOffscreenCanvas.getContext('2d'); 37 | res.offscreen = true; 38 | res.twoD = !!ctx; 39 | } catch (err) { 40 | console.error(err); 41 | } 42 | 43 | return res; 44 | } 45 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/types/base.ts: -------------------------------------------------------------------------------- 1 | import type { UnwrapPromise } from './utils'; 2 | import type { ClientParameters } from '../client-parameters'; 3 | import type { ContextualClientParameters } from '../contextual-client-parameters'; 4 | 5 | export type ParametersResult = { 6 | value?: T; 7 | duration?: number; 8 | error?: { 9 | code: string; 10 | message: string; 11 | }; 12 | }; 13 | 14 | export type UnwrappedParameters> = { 15 | [K in keyof T]: UnwrapPromise>; 16 | }; 17 | 18 | export type Result = { 19 | [K in keyof T]: T[K] extends Promise | infer R 20 | ? ParametersResult 21 | : never; 22 | }; 23 | 24 | export type LoadOptions = { 25 | exclude?: string[]; 26 | only?: string[]; 27 | timeout?: number; 28 | }; 29 | 30 | export type ClientData = Result> & Result>; 31 | export type GetterResults = 32 | | ClientData[keyof ClientData]['value'] 33 | | ClientData[keyof ClientData]['error'] 34 | | Partial 35 | | null; 36 | 37 | export interface MixVisitInterface { 38 | loadTime: number | null; 39 | fingerprintHash: string | null; 40 | load(options?: LoadOptions): Promise; 41 | get(key?: keyof ClientData | (keyof ClientData)[]): GetterResults; 42 | } 43 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/buildInObjects.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty, TDef } from '../utils/helpers'; 2 | 3 | type BuiltInObjectsInfo = { 4 | [K in (typeof builtInObjects)[number]]: string[]; 5 | }; 6 | 7 | const builtInObjects = [ 8 | 'AbortSignal', 9 | 'Array', 10 | 'ArrayBuffer', 11 | 'Atomics', 12 | 'BigInt', 13 | 'Boolean', 14 | 'Date', 15 | 'Document', 16 | 'Element', 17 | 'Error', 18 | 'Function', 19 | 'GPU', 20 | 'Intl', 21 | 'JSON', 22 | 'Map', 23 | 'Math', 24 | 'Navigation', 25 | 'Navigator', 26 | 'Number', 27 | 'Object', 28 | 'PerformanceNavigationTiming', 29 | 'Promise', 30 | 'Proxy', 31 | 'RTCRtpReceiver', 32 | 'ReadableStream', 33 | 'Reflect', 34 | 'RegExp', 35 | 'SVGAElement', 36 | 'Set', 37 | 'ShadowRoot', 38 | 'String', 39 | 'Symbol', 40 | 'WeakMap', 41 | 'WeakSet', 42 | 'WebAssembly', 43 | 'WebSocketStream', 44 | ] as const; 45 | 46 | export function getBuiltInObjectsInfo(): BuiltInObjectsInfo { 47 | const result = {} as BuiltInObjectsInfo; 48 | 49 | for (const objName of builtInObjects) { 50 | if (hasProperty(window, objName)) { 51 | const obj = window[objName]; 52 | const properties = Object.getOwnPropertyNames(obj); 53 | const methods = properties.filter((prop) => TDef.isFunc(obj[prop])); 54 | result[objName] = methods; 55 | } 56 | } 57 | 58 | return result; 59 | } 60 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/webgpu/index.ts: -------------------------------------------------------------------------------- 1 | import { getWebgGPUAPI } from './apiStatuses'; 2 | import { getWebGPUParams } from './webgpuParams'; 3 | import type { 4 | WebGPUInfo, 5 | WebGPUSupportedAdapters, 6 | WebGPUSupportedAdaptersParam, 7 | } from '../../types'; 8 | 9 | const requestAdapterOptions: WebGPUSupportedAdaptersParam = { 10 | fallback: { 11 | powerPreference: 'low-power', 12 | forceFallbackAdapter: true, 13 | }, 14 | highPerformance: { powerPreference: 'high-performance' }, 15 | } as const; 16 | 17 | export async function getWebGPU(): Promise { 18 | try { 19 | const res: WebGPUInfo = { 20 | statuses: null, 21 | }; 22 | 23 | res.statuses = await getWebgGPUAPI(); 24 | 25 | if (!res.statuses.adapter) { 26 | return res; 27 | } 28 | 29 | res.supportedAdapters = {} as WebGPUSupportedAdapters; 30 | const supportedAdaptersPromises: Promise[] = []; 31 | 32 | for (const [type, option] of Object.entries(requestAdapterOptions)) { 33 | const promise = getWebGPUParams(option).then((params) => { 34 | res.supportedAdapters[type] = params; 35 | }); 36 | 37 | supportedAdaptersPromises.push(promise); 38 | } 39 | 40 | await Promise.all(supportedAdaptersPromises); 41 | 42 | return res; 43 | } catch (err) { 44 | console.error(err); 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mix-visit/lite", 3 | "version": "1.0.14", 4 | "description": "Fingerprint library for generating client fingerprint", 5 | "license": "MIT", 6 | "type": "module", 7 | "engines": { 8 | "node": ">=20.9.0" 9 | }, 10 | "packageManager": "yarn@1.22.22", 11 | "types": "./dist/index.es.d.ts", 12 | "main": "./dist/index.cjs.js", 13 | "unpkg": "./dist/index.umd.js", 14 | "module": "./dist/index.es.js", 15 | "exports": { 16 | ".": { 17 | "require": "./dist/index.cjs.js", 18 | "import": "./dist/index.es.js" 19 | }, 20 | "./package.json": "./package.json" 21 | }, 22 | "files": [ 23 | "dist" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/mixvisit-service/mixvisit.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/mixvisit-service/mixvisit/issues" 31 | }, 32 | "homepage": "https://mixvisit.com/", 33 | "keywords": [ 34 | "fingerprint", 35 | "fraud detection", 36 | "browser", 37 | "identification", 38 | "fingerprinting", 39 | "browser fingerprint", 40 | "device fingerprint", 41 | "privacy" 42 | ], 43 | "scripts": { 44 | "dev": "vite build --watch", 45 | "build": "vite build" 46 | }, 47 | "dependencies": {}, 48 | "devDependencies": { 49 | "@webgpu/types": "^0.1.43", 50 | "vite-plugin-dts": "^3.8.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/screen-frame/utils.ts: -------------------------------------------------------------------------------- 1 | import type { FrameSize } from '../../types'; 2 | import { replaceNaN, round, toFloat } from '../../utils/helpers'; 3 | 4 | export function processSize(sideSize: FrameSize[number]): number | null { 5 | const roundingPrecision = 10; 6 | 7 | return sideSize === null 8 | ? null 9 | : round(sideSize, roundingPrecision); 10 | } 11 | 12 | export function getCurrentScreenFrame(): FrameSize { 13 | // Some browsers return screen resolution as strings, e.g. "1200", instead of a number, e.g. 1200. 14 | // Some browsers (IE, Edge ≤18) don't provide `screen.availLeft` and `screen.availTop`. 15 | // The property values are replaced with 0 in such cases to not lose the entropy from `screen.availWidth` and `screen.availHeight` 16 | return [ 17 | replaceNaN(toFloat(window.screen.availTop), null), 18 | replaceNaN(toFloat(window.screen.width) - toFloat(window.screen.availWidth) - replaceNaN(toFloat(window.screen.availLeft), 0), null), 19 | replaceNaN(toFloat(window.screen.height) - toFloat(window.screen.availHeight) - replaceNaN(toFloat(window.screen.availTop), 0), null), 20 | replaceNaN(toFloat(window.screen.availLeft), null), 21 | ]; 22 | } 23 | 24 | export function isFrameSizeNull(frameSize: FrameSize): boolean { 25 | for (let i = 0; i < 4; ++i) { 26 | if (frameSize[i]) { 27 | return false; 28 | } 29 | } 30 | 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/AboutInfo.svelte: -------------------------------------------------------------------------------- 1 | Looking for a reliable way to determine the IP fingerprint of a device? 2 |
3 |

MixVisit offers an advanced solution that includes IP fingerprinting to identify and track devices accurately.

4 |

5 | Our service goes beyond traditional methods by incorporating Canvas Fingerprinting, ensuring higher precision in 6 | distinguishing devices. For those concerned about privacy, MixVisit provides the Canvas Fingerprint Defender, 7 | safeguarding user anonymity while maintaining robust tracking capabilities. As a reliable fingerprint js 8 | alternative, MixVisit utilizes state-of-the-art technology to deliver unparalleled results. 9 |

10 |

11 | By analyzing various device attributes, we generate unique IP fingerprints that help in fraud detection, user 12 | authentication, and more. Additionally, the service shows your location, adding an extra layer of insight for better 13 | tracking and security. MixVisit's seamless integration makes it easy for businesses to enhance their security 14 | measures without compromising on user experience. 15 |

16 |

17 | Trust MixVisit to provide comprehensive device identification and protection against spoofing. Upgrade your tracking 18 | capabilities today with MixVisit’s IP fingerprinting and Canvas Fingerprint Defender. 19 |

20 |
21 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/location.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | import { fetcher, hasProperty } from '../utils/helpers'; 3 | 4 | type LocationDataRaw = { 5 | ip: string; 6 | asn: number; 7 | org: string; 8 | city: string; 9 | country: string; 10 | countryName: string; 11 | continent: string; 12 | latitude: string; 13 | longitude: string; 14 | airport: string; 15 | region: string; 16 | isEu: boolean; 17 | metroCode: string; 18 | regionCode: string; 19 | timezone: { 20 | name: string; 21 | }, 22 | languages: string; 23 | }; 24 | 25 | type LocationData = Omit & { timezone: string; }; 26 | 27 | export async function getLocation(): Promise { 28 | try { 29 | const location = await fetcher('https://ipgeo.myip.link/'); 30 | if (!location) { 31 | return null; 32 | } 33 | 34 | return Object.entries(location).reduce((result, [key, value]) => { 35 | if (!['isEu', 'metroCode', 'regionCode', 'airport'].includes(key)) { 36 | const isTimezoneField = key === 'timezone' && typeof value === 'object' && hasProperty(value, 'name'); 37 | result[key] = isTimezoneField ? value.name : value; 38 | } 39 | 40 | return result; 41 | }, {} as LocationData); 42 | } catch (err) { 43 | console.error(err); 44 | 45 | return null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/static/_app/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/hdcp.ts: -------------------------------------------------------------------------------- 1 | const HDCPVersions = [ 2 | '1.0', 3 | '1.1', 4 | '1.2', 5 | '1.3', 6 | '1.4', 7 | '2.0', 8 | '2.1', 9 | '2.2', 10 | '2.3', 11 | ] as const; 12 | 13 | const supportedConfig = [{ 14 | videoCapabilities: [{ 15 | contentType: 'video/webm; codecs="vp09.00.10.08"', 16 | robustness: 'SW_SECURE_DECODE', 17 | }], 18 | }]; 19 | 20 | type HDCPStatusResults = Record; 21 | 22 | export async function getHDCPPolicyCheck(): Promise { 23 | try { 24 | const mediaKeySystemAccess = await navigator.requestMediaKeySystemAccess('com.widevine.alpha', supportedConfig); 25 | const mediaKeys = await mediaKeySystemAccess.createMediaKeys(); 26 | 27 | if (!('getStatusForPolicy' in mediaKeys)) { 28 | return 'HDCP Policy Check API is not available'; 29 | } 30 | 31 | const result = {} as HDCPStatusResults; 32 | const HDCPVersionsPromises: Promise[] = []; 33 | 34 | for (const ver of HDCPVersions) { 35 | const statusForPolicyPromise = (mediaKeys as any).getStatusForPolicy({ minHdcpVersion: ver }) as Promise; 36 | const promise = statusForPolicyPromise.then((status) => { 37 | result[ver] = status !== 'usable' ? status : 'available'; 38 | }); 39 | 40 | HDCPVersionsPromises.push(promise); 41 | } 42 | 43 | await Promise.all(HDCPVersionsPromises); 44 | 45 | return result; 46 | } catch (err) { 47 | console.log(err); 48 | 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/canvas/index.ts: -------------------------------------------------------------------------------- 1 | import { detectCanvasFingerprinting } from './detect'; 2 | import { renderImages } from './images'; 3 | import { isWindingSupported, createCanvas, isCanvasSupported } from './utils'; 4 | import { isSafariWebKit, isWebKit, isWebKit616OrNewer } from '../../utils/browser'; 5 | import { CanvasImageStatus } from '../../utils/enums'; 6 | 7 | interface CanvasFingerprint { 8 | spoofing: boolean; 9 | winding: boolean; 10 | geometry: string; 11 | text: string; 12 | } 13 | 14 | export async function getCanvasFingerprint(): Promise { 15 | let winding = false; 16 | let spoofing = false; 17 | let geometry: string; 18 | let text: string; 19 | 20 | const [canvas, context] = createCanvas(); 21 | 22 | if (!isCanvasSupported(canvas, context)) { 23 | geometry = CanvasImageStatus.UNSUPPORTED; 24 | text = CanvasImageStatus.UNSUPPORTED; 25 | } else { 26 | winding = isWindingSupported(context); 27 | 28 | // Checks if the current browser is known for applying anti-fingerprinting measures in all or some critical modes 29 | if (isWebKit() && isWebKit616OrNewer() && isSafariWebKit()) { 30 | geometry = CanvasImageStatus.SKIPPED; 31 | text = CanvasImageStatus.SKIPPED; 32 | } else { 33 | const { diffIndexes } = await detectCanvasFingerprinting() ?? {}; 34 | spoofing = !!diffIndexes && !!diffIndexes.join(''); 35 | 36 | [geometry, text] = await renderImages(canvas, context); 37 | } 38 | } 39 | 40 | return { 41 | spoofing, 42 | winding, 43 | geometry, 44 | text, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /example/src/lib/components/ui/Badges.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 9 | github 10 | 11 | 12 | 13 | npm 14 | 15 | 16 | 17 | typescript 18 | 19 | 20 | {#if packageVersion} 21 | 22 | release 23 | 24 | {/if} 25 | 26 | 27 | license 28 | 29 |
30 | 31 | 44 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/devToolsDetector.ts: -------------------------------------------------------------------------------- 1 | export async function isDevToolsOpened(): Promise { 2 | return (await isDevToolsOpenedDebuggerChecking()) || isDevToolsOpenScreenMismatchChecking(); 3 | } 4 | 5 | function isDevToolsOpenedDebuggerChecking(): Promise { 6 | let devtoolsOpen = false; 7 | 8 | const worker = new Worker( 9 | URL.createObjectURL( 10 | new Blob( 11 | [ 12 | ` 13 | "use strict"; 14 | onmessage = () => { 15 | postMessage({ isOpenBeat: true }); 16 | debugger; 17 | postMessage({ isOpenBeat: false }); 18 | }; 19 | `, 20 | ], 21 | { type: 'text/javascript' }, 22 | ), 23 | ), 24 | ); 25 | 26 | return new Promise((resolve) => { 27 | let timeout: any; 28 | 29 | worker.onmessage = (event: MessageEvent<{ isOpenBeat: boolean }>) => { 30 | if (event.data.isOpenBeat) { 31 | timeout = setTimeout(() => { 32 | devtoolsOpen = true; 33 | worker.terminate(); 34 | resolve(devtoolsOpen); 35 | }, 100); 36 | } else { 37 | clearTimeout(timeout); 38 | devtoolsOpen = false; 39 | worker.terminate(); 40 | resolve(devtoolsOpen); 41 | } 42 | }; 43 | 44 | worker.postMessage({}); 45 | }); 46 | } 47 | 48 | function isDevToolsOpenScreenMismatchChecking(): boolean { 49 | const threshold = 160; 50 | const hasWidthDiscrepancy = window.outerWidth - window.innerWidth > threshold; 51 | const hasHeightDiscrepancy = window.outerHeight - window.innerHeight > threshold; 52 | 53 | return hasWidthDiscrepancy || hasHeightDiscrepancy; 54 | } 55 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/webgl/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getSupportedContexts, 3 | getCanvasImageHash, 4 | getWebGLBasicParams, 5 | getWebGLExtendedParams, 6 | } from './canvas'; 7 | import type { WebGLInfo } from '../../types'; 8 | 9 | export function getWebGL(): WebGLInfo | null { 10 | const res: WebGLInfo = { 11 | webglImageHash: '', 12 | supportedWebGLContexts: [], 13 | }; 14 | 15 | const canvas = document.createElement('canvas'); 16 | canvas.width = 256; 17 | canvas.height = 128; 18 | 19 | const supportedContexts = getSupportedContexts(canvas); 20 | if (!supportedContexts.length) { 21 | return null; 22 | } 23 | 24 | const [someContext] = supportedContexts; 25 | res.webglImageHash = getCanvasImageHash(canvas, someContext.context); 26 | 27 | for (const contextInfo of supportedContexts) { 28 | const basics = getWebGLBasicParams(contextInfo); 29 | const { 30 | contextAttributes = null, 31 | shaderPrecisions = null, 32 | supportedFunctions = null, 33 | extensions = null, 34 | vertexShader = null, 35 | transformFeedback = null, 36 | rasterizer = null, 37 | fragmentShader = null, 38 | framebuffer = null, 39 | textures = null, 40 | uniformBuffers = null, 41 | } = getWebGLExtendedParams(contextInfo.context) || {}; 42 | 43 | res.supportedWebGLContexts.push({ 44 | basics, 45 | contextAttributes, 46 | shaderPrecisions, 47 | vertexShader, 48 | transformFeedback, 49 | rasterizer, 50 | fragmentShader, 51 | framebuffer, 52 | textures, 53 | uniformBuffers, 54 | extensions, 55 | supportedFunctions, 56 | }); 57 | } 58 | 59 | return res; 60 | } 61 | -------------------------------------------------------------------------------- /example/src/lib/components/sections/Visitor.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 | {#if visits && visitorInfo} 35 | 36 |
37 | 38 | 39 |
40 |
41 | {/if} 42 | 43 | 55 | -------------------------------------------------------------------------------- /example/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import type { ClientData } from '@mix-visit/lite'; 2 | import { VisitStatus } from './enums'; 3 | 4 | export type VisitorData = { 5 | visitorID: string; 6 | location: LocationData; 7 | }; 8 | 9 | export type VisitorStorageData = VisitorData & { 10 | time: string; 11 | }; 12 | 13 | export type VisitData = { 14 | country: string; 15 | city: string; 16 | lat: number; 17 | lng: number; 18 | time: string; 19 | when: string; 20 | visitStatus: (typeof VisitStatus)[keyof typeof VisitStatus]; 21 | isIncognito: null; 22 | }; 23 | 24 | export type VisitorInfo = { 25 | personalId: string; 26 | ip: string; 27 | location: string; 28 | ipAddress: number; 29 | geolocations: number; 30 | visitCounter: number; 31 | isIncognito: null; 32 | incognitoCounter: null; 33 | }; 34 | 35 | export type LocationData = { 36 | ip: string; 37 | asn: number; 38 | org: string; 39 | city: string; 40 | country: string; 41 | countryName: string; 42 | continent: string; 43 | latitude: string; 44 | longitude: string; 45 | region: string; 46 | timezone: string; 47 | languages: string; 48 | }; 49 | 50 | export type LocationDataRaw = { 51 | ip: string; 52 | asn: number; 53 | org: string; 54 | city: string; 55 | country: string; 56 | countryName: string; 57 | continent: string; 58 | latitude: string; 59 | longitude: string; 60 | airport: string; 61 | region: string; 62 | regionCode: string; 63 | timezone: { 64 | name: string; 65 | }; 66 | languages: string; 67 | }; 68 | 69 | export type GroupedError = { 70 | code: string; 71 | message: string; 72 | params: string[]; 73 | }; 74 | 75 | export type MixVisitResult = { 76 | fingerprintHash: string | null; 77 | loadTime: number | null; 78 | data: ClientData | null; 79 | }; 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MixVisitJS 2 | 3 | > Fingerprint library for generating client fingerprint 4 | 5 | ## Demo 6 | 7 | You can visit [mixvisit.com](https://mixvisit.com) (or [iphey.com](https://iphey.com)) for check our demo example 8 | 9 | ## Usage 10 | 11 | ```javascript 12 | import { MixVisit } from '@mix-visit/lite'; 13 | 14 | const mixvisit = new MixVisit(); 15 | await mixvisit.load(); 16 | 17 | console.log('time on miliseconds:>> ', mixvisit.loadTime); 18 | console.log('fingerprintHash :>> ', mixvisit.fingerprintHash); 19 | console.log('allResults :>> ', mixvisit.get()); 20 | console.log('platform :>> ', mixvisit.get('platform')); 21 | ``` 22 | 23 | ## Project info 24 | 25 | The project uses [yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) to manage all projects, share code between them, and other capabilities. 26 | 27 | Current workspace names for projects: 28 | - packages/lite - @mix-visit/lite 29 | - example - @mix-visit/example 30 | 31 | ## Commands 32 | 33 | `yarn install` - installs all project dependencies
34 | `yarn build` - builds all projects
35 | `yarn build:example` - build only example playground site
36 | `yarn build:lite` - build only MixVisit lib
37 | `yarn dev:example` - running vite dev with enabling HMR for example playground site
38 | `yarn dev:lite` - running vite dev with enabling HMR for MixVisit lib 39 | 40 | The build results are stored mainly in the `./dist` folder. 41 | 42 | For commands specific to a project, use `yarn workspace`: 43 | 44 | - `yarn workspace add ` - installs a package for the specified project 45 | - `yarn workspace run ` - runs a script for the specified project 46 | 47 | Example of installing the vite package for package/lite: 48 | `yarn workspace @mix-visit/lite add vite` 49 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/intl.ts: -------------------------------------------------------------------------------- 1 | import { TDef } from '../utils/helpers'; 2 | 3 | type IntlData = { 4 | dateTimeOptions: Intl.ResolvedDateTimeFormatOptions | null; 5 | numberFormatOptions: Intl.ResolvedNumberFormatOptions | null; 6 | collatorOptions: Intl.ResolvedCollatorOptions | null; 7 | pluralRulesOptions: Intl.ResolvedPluralRulesOptions | null; 8 | relativeTimeFormatOptions: Intl.ResolvedRelativeTimeFormatOptions | null; 9 | }; 10 | 11 | type Constructors = typeof Intl.DateTimeFormat 12 | | typeof Intl.NumberFormat 13 | | typeof Intl.Collator 14 | | typeof Intl.PluralRules 15 | | typeof Intl.RelativeTimeFormat; 16 | 17 | type ResolvedOptions = 18 | T extends typeof Intl.DateTimeFormat 19 | ? Intl.ResolvedDateTimeFormatOptions 20 | : T extends typeof Intl.NumberFormat 21 | ? Intl.ResolvedNumberFormatOptions 22 | : T extends typeof Intl.Collator 23 | ? Intl.ResolvedCollatorOptions 24 | : T extends typeof Intl.PluralRules 25 | ? Intl.ResolvedPluralRulesOptions 26 | : T extends typeof Intl.RelativeTimeFormat 27 | ? Intl.ResolvedRelativeTimeFormatOptions 28 | : never; 29 | 30 | export function getIntlData(): IntlData | null { 31 | if (TDef.isUndefined(Intl)) { 32 | return null; 33 | } 34 | 35 | return { 36 | dateTimeOptions: getIntlObjectOptions(Intl.DateTimeFormat), 37 | numberFormatOptions: getIntlObjectOptions(Intl.NumberFormat), 38 | collatorOptions: getIntlObjectOptions(Intl.Collator), 39 | pluralRulesOptions: getIntlObjectOptions(Intl.PluralRules), 40 | relativeTimeFormatOptions: getIntlObjectOptions(Intl.RelativeTimeFormat), 41 | }; 42 | } 43 | 44 | function getIntlObjectOptions(intlConstructor: T): ResolvedOptions { 45 | const intlObject = new (intlConstructor as any)(); 46 | 47 | return intlObject.resolvedOptions() as ResolvedOptions; 48 | } 49 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/client-rects/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { createClientRectElement, ELEMENTS } from './utils'; 4 | import { withIframe } from '../../utils/helpers'; 5 | 6 | type ClientRect = { 7 | bottom: number; 8 | height: number; 9 | left: number; 10 | right: number; 11 | top: number; 12 | width: number; 13 | x: number; 14 | y: number; 15 | }; 16 | 17 | export async function getClientRects(): Promise { 18 | return withIframe({ action: createElementsAndGetClientRects }); 19 | } 20 | 21 | async function createElementsAndGetClientRects( 22 | iframe: HTMLIFrameElement, 23 | iframeWindow: typeof window, 24 | ): Promise { 25 | const res: ClientRect[] = []; 26 | 27 | iframe.style.width = '700px'; 28 | iframe.style.height = '600px'; 29 | 30 | const iframeDocument = iframeWindow.document; 31 | const { head: iframeHead, body: iframeBody } = iframeDocument; 32 | 33 | // created elements 34 | const elementsResult = Object.values(ELEMENTS) 35 | .map(createClientRectElement) 36 | .join(''); 37 | 38 | const style = iframeDocument.createElement('style'); 39 | style.textContent = ` 40 | caption { 41 | border: solid 2px darkred; 42 | font-size: 20.99px; 43 | margin-left: 20.8px; 44 | } 45 | `; 46 | iframeHead.appendChild(style); 47 | iframeBody.innerHTML = elementsResult; 48 | 49 | const elementsArr = Array.from(iframeBody.children); 50 | 51 | for (const el of elementsArr) { 52 | const { 53 | bottom, top, 54 | left, right, 55 | width, height, 56 | x, y, 57 | } = el.getBoundingClientRect(); 58 | 59 | res.push({ 60 | bottom, 61 | top, 62 | left, 63 | right, 64 | width, 65 | height, 66 | x, 67 | y, 68 | }); 69 | } 70 | 71 | return res; 72 | } 73 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/performance.ts: -------------------------------------------------------------------------------- 1 | type PerformanceData = { 2 | commitLoadTime: number; 3 | finishDocumentLoadTime: number; 4 | finishLoadTime: number; 5 | firstPaintAfterLoadTime: number; 6 | firstPaintTime: number; 7 | navigationType: string; 8 | npnNegotiatedProtocol: string; 9 | requestTime: number; 10 | startLoadTime: number; 11 | wasAlternateProtocolAvailable: boolean; 12 | wasFetchedViaSpdy: boolean; 13 | wasNpnNegotiated: boolean; 14 | }; 15 | 16 | export function getPerformanceData(): PerformanceData { 17 | const { 18 | performance: { navigation, timing }, 19 | } = window; 20 | 21 | const navigationEntries = performance.getEntriesByType('navigation')[0] as PerformanceResourceTiming; 22 | const paintEntries = performance.getEntriesByType('paint'); 23 | 24 | return { 25 | commitLoadTime: timing.responseStart, 26 | finishDocumentLoadTime: timing.domContentLoadedEventEnd, 27 | finishLoadTime: timing.loadEventEnd, 28 | firstPaintAfterLoadTime: paintEntries.length > 0 ? paintEntries[0].startTime : 0, 29 | firstPaintTime: paintEntries.length > 0 ? paintEntries[0].startTime + timing.navigationStart : 0, 30 | navigationType: ['navigate', 'reload', 'back_forward', 'prerender'][navigation.type], 31 | npnNegotiatedProtocol: navigationEntries ? navigationEntries.nextHopProtocol : 'unknown', 32 | requestTime: timing.requestStart, 33 | startLoadTime: timing.fetchStart, 34 | wasAlternateProtocolAvailable: navigationEntries 35 | ? (navigationEntries as any).alternateProtocolUsage === 'used' 36 | : false, 37 | wasFetchedViaSpdy: navigationEntries 38 | ? navigationEntries.nextHopProtocol.includes('h2') || navigationEntries.nextHopProtocol.includes('h3') 39 | : false, 40 | wasNpnNegotiated: navigationEntries ? navigationEntries.nextHopProtocol !== 'http/1.1' : false, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /example/src/lib/components/sections/Durations.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | 19 |
20 | {#each durationsData.sort( (a, b) => (sortOrder === 'desc' ? b.duration - a.duration : a.duration - b.duration), ) as { name, duration }} 21 |
22 | {name} 23 | {duration} ms 24 |
25 | {/each} 26 |
27 |
28 | 29 | 66 | -------------------------------------------------------------------------------- /example/static/_app/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: dark; 7 | color: hsla(0, 0%, 100%, 0.87); 8 | background-color: hsl(0, 0%, 14%); 9 | 10 | text-rendering: optimizeLegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | *, 16 | *::before, 17 | *::after { 18 | box-sizing: border-box; 19 | } 20 | 21 | #app { 22 | width: 80rem; 23 | height: 100vh; 24 | margin: 0 auto; 25 | padding: 0; 26 | display: flex; 27 | flex-direction: column; 28 | } 29 | 30 | footer { 31 | font-size: 0.8rem; 32 | padding-top: 1.25rem; 33 | position: relative; 34 | bottom: 0; 35 | width: 100%; 36 | } 37 | 38 | body { 39 | display: flex; 40 | margin: 0; 41 | } 42 | 43 | main { 44 | padding: 2rem; 45 | flex: 1; 46 | } 47 | 48 | h1.lib-title { 49 | font-family: Open Sans; 50 | font-size: 5rem; 51 | line-height: 1.1; 52 | margin: 0; 53 | background-image: linear-gradient(45deg, hsl(57, 83%, 68%), hsl(343, 56%, 57%)); 54 | -webkit-background-clip: text; 55 | -webkit-text-fill-color: transparent; 56 | -moz-background-clip: text; 57 | -moz-text-fill-color: transparent; 58 | } 59 | 60 | h1 { 61 | font-size: 2.2rem; 62 | } 63 | 64 | code { 65 | border-radius: 0.5rem; 66 | font-size: 0.8rem; 67 | } 68 | 69 | pre { 70 | display: grid; 71 | margin: 0; 72 | } 73 | 74 | a { 75 | font-weight: 500; 76 | color: hsl(14, 59%, 65%); 77 | text-decoration: inherit; 78 | } 79 | 80 | a:hover { 81 | color: hsl(36, 69%, 66%); 82 | } 83 | 84 | .leaflet-control-attribution { 85 | display: none; 86 | } 87 | 88 | .description { 89 | font-size: 0.8rem; 90 | color: hsl(0, 0%, 70%); 91 | } 92 | 93 | @media screen and (max-width: 690px) { 94 | #app { 95 | width: -webkit-fill-available; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /example/src/lib/components/sections/Errors.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 25 |
26 | {#each errors as { code, message, params }} 27 |
28 | {params.join(', ')} 29 | 30 |
31 | {/each} 32 |
33 |
34 | 35 | 72 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/screen-frame/screenFrame.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentScreenFrame, isFrameSizeNull } from './utils'; 2 | import type { FrameSize } from '../../types'; 3 | import { exitFullscreen, getFullscreenElement } from '../../utils/helpers'; 4 | 5 | let screenFrameBackup: Readonly | null = null; 6 | let screenFrameSizeTimeoutId: number | null = null; 7 | 8 | export async function getBaseScreenFrame(): Promise { 9 | watchScreenFrame(); 10 | 11 | let frameSize = getCurrentScreenFrame(); 12 | 13 | if (isFrameSizeNull(frameSize)) { 14 | if (screenFrameBackup) { 15 | return [...screenFrameBackup]; 16 | } 17 | 18 | if (getFullscreenElement()) { 19 | // Some browsers set the screen frame to zero when programmatic fullscreen is on. 20 | // There is a chance of getting a non-zero frame after exiting the fullscreen. 21 | await exitFullscreen(); 22 | frameSize = getCurrentScreenFrame(); 23 | } 24 | } 25 | 26 | if (!isFrameSizeNull(frameSize)) { 27 | screenFrameBackup = frameSize; 28 | } 29 | 30 | return frameSize; 31 | } 32 | 33 | /** 34 | * Starts watching the screen frame size. When a non-zero size appears, the size is saved and the watch is stopped. 35 | * Later, when `getScreenFrame` runs, it will return the saved non-zero size if the current size is null. 36 | * This trick is required to mitigate the fact that the screen frame turns null in some cases. 37 | */ 38 | export function watchScreenFrame(): void { 39 | if (screenFrameSizeTimeoutId !== null) { 40 | return; 41 | } 42 | 43 | const screenFrameCheckInterval = 2500; 44 | 45 | const checkScreenFrame = () => { 46 | const frameSize = getCurrentScreenFrame(); 47 | if (isFrameSizeNull(frameSize)) { 48 | screenFrameSizeTimeoutId = (setTimeout as typeof window.setTimeout)(checkScreenFrame, screenFrameCheckInterval); 49 | } else { 50 | screenFrameBackup = frameSize; 51 | screenFrameSizeTimeoutId = null; 52 | } 53 | }; 54 | 55 | checkScreenFrame(); 56 | } 57 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/webrtc.ts: -------------------------------------------------------------------------------- 1 | type IceServerParam = { 2 | iceServer: string; 3 | }; 4 | 5 | type IPsResult = { 6 | public: string[]; 7 | private: string[]; 8 | allRes: string[]; 9 | log: string; 10 | }; 11 | 12 | export async function getWebrtcIPs( 13 | param: IceServerParam = { iceServer: 'stun.l.google.com:19302' }, 14 | timeoutMs: number = 4000 15 | ): Promise { 16 | const ips = new Set(); 17 | const servers = { iceServers: [{ urls: `stun:${param.iceServer}` }] }; 18 | const pc = new RTCPeerConnection(servers); 19 | 20 | try { 21 | pc.createDataChannel(''); 22 | const offer = await pc.createOffer(); 23 | await pc.setLocalDescription(offer); 24 | 25 | await new Promise((resolve) => { 26 | const timeout = setTimeout(() => { 27 | resolve(); 28 | }, timeoutMs); 29 | 30 | pc.onicegatheringstatechange = () => { 31 | if (pc.iceGatheringState === 'complete') { 32 | clearTimeout(timeout); 33 | resolve(); 34 | } 35 | }; 36 | }); 37 | 38 | const sdp = pc.localDescription?.sdp || ''; 39 | const ipMatches = [...sdp.matchAll(/(?:c=IN IP4|c=IN IP6) ([\d.a-fA-F:]+)/g)]; 40 | 41 | for (const match of ipMatches) { 42 | const ip = match[1]; 43 | if (ip && !ips.has(ip)) { 44 | ips.add(ip); 45 | } 46 | } 47 | 48 | const result: IPsResult = { 49 | public: [], 50 | private: [], 51 | allRes: Array.from(ips), 52 | log: sdp, 53 | }; 54 | 55 | for (const ip of ips) { 56 | if (isPrivateIP(ip)) { 57 | result.private.push(ip); 58 | } else if (!ip.endsWith('.local')) { 59 | result.public.push(ip); 60 | } 61 | } 62 | 63 | return result; 64 | } catch (error) { 65 | throw error; 66 | } finally { 67 | pc.close(); 68 | } 69 | } 70 | 71 | function isPrivateIP(ip: string): boolean { 72 | return [ 73 | /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, 74 | /^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/, 75 | /^192\.168\.\d{1,3}\.\d{1,3}$/, 76 | /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, 77 | ].some((regex) => regex.test(ip)); 78 | } 79 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://gitlab.torproject.org/legacy/trac/-/issues/13018 3 | * @see https://bugzilla.mozilla.org/show_bug.cgi?id=531915 4 | */ 5 | export function getMathResults(): Record { 6 | const fallbackFn = () => 0; 7 | 8 | // Native operations 9 | const acos = Math.acos || fallbackFn; 10 | const acosh = Math.acosh || fallbackFn; 11 | const asin = Math.asin || fallbackFn; 12 | const asinh = Math.asinh || fallbackFn; 13 | const atanh = Math.atanh || fallbackFn; 14 | const atan = Math.atan || fallbackFn; 15 | const sin = Math.sin || fallbackFn; 16 | const sinh = Math.sinh || fallbackFn; 17 | const cos = Math.cos || fallbackFn; 18 | const cosh = Math.cosh || fallbackFn; 19 | const tan = Math.tan || fallbackFn; 20 | const tanh = Math.tanh || fallbackFn; 21 | const exp = Math.exp || fallbackFn; 22 | const expm1 = Math.expm1 || fallbackFn; 23 | const log1p = Math.log1p || fallbackFn; 24 | 25 | // Operation polyfills 26 | const powPI = (value: number) => Math.PI ** value; 27 | const acoshPf = (value: number) => Math.log(value + Math.sqrt(value * value - 1)); 28 | const asinhPf = (value: number) => Math.log(value + Math.sqrt(value * value + 1)); 29 | const atanhPf = (value: number) => Math.log((1 + value) / (1 - value)) / 2; 30 | const sinhPf = (value: number) => Math.exp(value) - 1 / Math.exp(value) / 2; 31 | const coshPf = (value: number) => (Math.exp(value) + 1 / Math.exp(value)) / 2; 32 | const expm1Pf = (value: number) => Math.exp(value) - 1; 33 | const tanhPf = (value: number) => (Math.exp(2 * value) - 1) / (Math.exp(2 * value) + 1); 34 | const log1pPf = (value: number) => Math.log(1 + value); 35 | 36 | return { 37 | acos: acos(0.123124234234234242), 38 | acosh: acosh(1e308), 39 | acoshPf: acoshPf(1e154), 40 | asin: asin(0.123124234234234242), 41 | asinh: asinh(1), 42 | asinhPf: asinhPf(1), 43 | atanh: atanh(0.5), 44 | atanhPf: atanhPf(0.5), 45 | atan: atan(0.5), 46 | sin: sin(-1e300), 47 | sinh: sinh(1), 48 | sinhPf: sinhPf(1), 49 | cos: cos(10.000000000123), 50 | cosh: cosh(1), 51 | coshPf: coshPf(1), 52 | tan: tan(-1e300), 53 | tanh: tanh(1), 54 | tanhPf: tanhPf(1), 55 | exp: exp(1), 56 | expm1: expm1(1), 57 | expm1Pf: expm1Pf(1), 58 | log1p: log1p(10), 59 | log1pPf: log1pPf(10), 60 | powPI: powPI(-100), 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/webgl/utils.ts: -------------------------------------------------------------------------------- 1 | import type { CanvasContext } from '../../types'; 2 | 3 | export function getBestFloatPrecision(ctx: CanvasContext, shaderType: string): readonly [number, number, number] | null { 4 | const high = ctx.getShaderPrecisionFormat(ctx[shaderType], ctx.HIGH_FLOAT); 5 | const medium = ctx.getShaderPrecisionFormat(ctx[shaderType], ctx.MEDIUM_FLOAT); 6 | 7 | if (!(high || medium)) { 8 | return null; 9 | } 10 | 11 | let best = high; 12 | if (!high.precision) { 13 | best = medium; 14 | } 15 | 16 | return [best.precision, best.rangeMin, best.rangeMax] as const; 17 | } 18 | 19 | export function getFloatIntPrecision(ctx: CanvasContext): string | null { 20 | const highFloatPrecisionInfo = ctx.getShaderPrecisionFormat(ctx.FRAGMENT_SHADER, ctx.HIGH_FLOAT); 21 | const highIntPrecisionInfo = ctx.getShaderPrecisionFormat(ctx.FRAGMENT_SHADER, ctx.HIGH_INT); 22 | 23 | if (!(highFloatPrecisionInfo && highIntPrecisionInfo)) { 24 | return null; 25 | } 26 | 27 | const floatStrResult = highFloatPrecisionInfo.precision !== 0 ? 'highp/' : 'mediump/'; 28 | const intStrResult = highIntPrecisionInfo.rangeMax !== 0 ? 'highp' : 'lowp'; 29 | 30 | return `${floatStrResult}${intStrResult}`; 31 | } 32 | 33 | export function getMaxAnisotropy(ctx: CanvasContext): number | null { 34 | const ext = ctx.getExtension('EXT_texture_filter_anisotropic') 35 | || ctx.getExtension('WEBKIT_EXT_texture_filter_anisotropic') 36 | || ctx.getExtension('MOZ_EXT_texture_filter_anisotropic'); 37 | 38 | if (!ext) { 39 | return null; 40 | } 41 | 42 | let max: number = ctx.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT); 43 | // See Canary bug: https://code.google.com/p/chromium/issues/detail?id=117450 44 | if (max === 0) { 45 | max = 2; 46 | } 47 | 48 | return max; 49 | } 50 | 51 | export function getParamFromObject(ctx: CanvasContext, param: string): unknown[] | null { 52 | const parameterResult = ctx.getParameter(ctx[param]); 53 | 54 | return parameterResult ? Object.values(parameterResult) : null; 55 | } 56 | 57 | export function getParamsAndReturnArray(ctx: CanvasContext, params: string[]): unknown[] | null { 58 | const collectedParams = params.map((param) => ctx.getParameter(ctx[param])); 59 | const hasValidParameter = collectedParams.some((param) => param !== undefined && param !== null); 60 | 61 | return hasValidParameter ? collectedParams : null; 62 | } 63 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/README.md: -------------------------------------------------------------------------------- 1 | # MixVisitJS 2 | 3 | > Fingerprint library for generating client fingerprint 4 | 5 | ## Demo 6 | 7 | You can visit [mixvisit.com](https://mixvisit.com) for check our demo example 8 | 9 | ## Install 10 | 11 | At first install MixvisitJS package 12 | 13 | ```bash 14 | # with npm package manager 15 | npm install @mix-visit/lite 16 | 17 | # with yarn package manager 18 | yarn add @mix-visit/lite 19 | 20 | # with pnpm package manager 21 | pnpm add @mix-visit/lite 22 | ``` 23 | 24 | ## Usage 25 | 26 | After installation, you can use the module. Import the main `MixVisit` class and the `ClientData` type. 27 | 28 | ```typescript 29 | import { MixVisit, type ClientData } from '@mix-visit/lite'; 30 | ``` 31 | 32 | First, create an instance of `MixVisit` for usage. 33 | 34 | ```typescript 35 | const mixvisit = new MixVisit(); 36 | ``` 37 | 38 | For example, collect all data except `geolocation`, with a timeout of 700 ms for asynchronous parameters. Then, use `get` method to retrieve all collected client data and save it in the `data` variable. 39 | 40 | ```typescript 41 | await mixvisit.load({ exclude: ['geolocation'], timeout: 700 }); 42 | const data: ClientData = mixvisit.get(); 43 | ``` 44 | 45 | Suppose that after we need this `geolocation` parameter, we also need the `webrtc` parameter that was not loaded due to a timeout. The `load` method with the `only` option executes with a maximum timeout of 12 seconds. This option loads only the specified parameters. After that, when requesting all data, the `geolocation` and `webrtc` parameters will be included. 46 | 47 | ```typescript 48 | await mixvisit.load({ only: ['geolocation', 'webrtc'] }); 49 | const extendedData = mixvisit.get(); 50 | ``` 51 | 52 | After you have loaded the parameters using the `load` method, the properties `loadTime` and `fingerprintHash` become available. 53 | The `loadTime` property stores the time of the last `load` function execution. The `fingerprintHash` property contains the generated fingerprint hash. 54 | 55 | ```typescript 56 | const loadTime = mixvisit.loadTime; 57 | const hash = mixvisit.fingerprintHash; 58 | ``` 59 | 60 | You can also retrieve the loaded parameters using the `get()` method. If you want to retrieve all client data, call the `get()` method without options. If you need a specific parameter, pass its name as a string. To get multiple parameters, provide an array with the required parameters. 61 | 62 | ```typescript 63 | const allClientData = mixvisit.get(); 64 | const isDevToolsOpen = mixvisit.get('devToolsOpen'); 65 | const { location, geolocation, webrtc } = mixvisit.get(['location', 'geolocation', 'webrtc']); 66 | ``` 67 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/canvas/images.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | import { CanvasImageStatus } from '../../utils/enums'; 4 | import { wait } from '../../utils/helpers'; 5 | 6 | export async function renderImages( 7 | canvas: HTMLCanvasElement, 8 | context: CanvasRenderingContext2D, 9 | ): Promise<[geometry: string, text: string]> { 10 | renderTextImage(canvas, context); 11 | 12 | await wait(0); 13 | 14 | const textImage1 = canvas.toDataURL(); 15 | const textImage2 = canvas.toDataURL(); 16 | 17 | // The canvas is excluded from the fingerprint in this case 18 | if (textImage1 !== textImage2) { 19 | return [CanvasImageStatus.UNSTABLE, CanvasImageStatus.UNSTABLE]; 20 | } 21 | 22 | // Text is unstable. Therefore it's extracted into a separate image 23 | renderGeometryImage(canvas, context); 24 | 25 | await wait(0); 26 | const geometryImage = canvas.toDataURL(); 27 | 28 | return [geometryImage, textImage1]; 29 | } 30 | 31 | export function renderTextImageForDetectSpoofing(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D | null) { 32 | canvas.width = 21; 33 | canvas.height = 120; 34 | 35 | context.fillStyle = '#66666666'; 36 | context.fillRect(0, 0, 21, 120); 37 | context.font = '8pt Times New Roman'; 38 | context.fillText('H', 6, 14); 39 | } 40 | 41 | function renderTextImage(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { 42 | canvas.width = 240; 43 | canvas.height = 60; 44 | 45 | context.textBaseline = 'alphabetic'; 46 | context.fillStyle = '#f60'; 47 | context.fillRect(100, 1, 62, 20); 48 | 49 | context.fillStyle = '#069'; 50 | context.font = '11pt "Times New Roman"'; 51 | 52 | const printedText = `Cwm fjordbank gly ${String.fromCharCode(55357, 56835) /* 😃 */}`; 53 | context.fillText(printedText, 2, 15); 54 | context.fillStyle = 'rgba(102, 204, 0, 0.2)'; 55 | context.font = '18pt Arial'; 56 | context.fillText(printedText, 4, 45); 57 | } 58 | 59 | function renderGeometryImage(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { 60 | canvas.width = 122; 61 | canvas.height = 110; 62 | 63 | context.globalCompositeOperation = 'multiply'; 64 | 65 | for (const [color, x, y] of [ 66 | ['#f2f', 40, 40], 67 | ['#2ff', 80, 40], 68 | ['#ff2', 60, 80], 69 | ] as const) { 70 | context.fillStyle = color; 71 | context.beginPath(); 72 | context.arc(x, y, 40, 0, Math.PI * 2, true); 73 | context.closePath(); 74 | context.fill(); 75 | } 76 | 77 | context.fillStyle = '#f9c'; 78 | context.arc(60, 60, 60, 0, Math.PI * 2, true); 79 | context.arc(60, 60, 20, 0, Math.PI * 2, true); 80 | context.fill('evenodd'); 81 | } 82 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/webgpu/webgpuParams.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-empty */ 2 | 3 | import type { AdaptersOption, WebGPUParams } from '../../types'; 4 | import { TEXTURE_FORMATS } from '../../utils/constants'; 5 | 6 | export async function getWebGPUParams(options: AdaptersOption): Promise { 7 | try { 8 | const adapter = await navigator.gpu.requestAdapter(options); 9 | if (!adapter) { 10 | return null; 11 | } 12 | 13 | const features: string[] = Array.from(adapter.features); 14 | const { limits: limitsObject } = adapter; 15 | const [device, adapterInfo] = await Promise.all([ 16 | adapter.requestDevice({ requiredFeatures: features as Iterable }), 17 | adapter.requestAdapterInfo(), 18 | ]); 19 | 20 | const textureFormatCapabilities = await getSupportedFormats(device); 21 | 22 | const limits = {} as GPUSupportedLimits; 23 | for (const key in limitsObject) { 24 | limits[key] = limitsObject[key]; 25 | } 26 | 27 | const info = {} as GPUAdapterInfo; 28 | for (const key in adapterInfo) { 29 | info[key] = adapterInfo[key]; 30 | } 31 | 32 | return { 33 | info, 34 | limits, 35 | features, 36 | textureFormatCapabilities, 37 | flags: { 38 | isCompatibilityMode: adapter.isCompatibilityMode, 39 | isFallbackAdapter: adapter.isFallbackAdapter, 40 | }, 41 | }; 42 | } catch (err) { 43 | console.error(err); 44 | 45 | return null; 46 | } 47 | } 48 | 49 | async function getSupportedFormats(device: GPUDevice): Promise { 50 | try { 51 | const res: string[] = []; 52 | 53 | for (const format of TEXTURE_FORMATS) { 54 | try { 55 | let width = 1; 56 | let height = 1; 57 | 58 | if (format.startsWith('bc') || format.startsWith('e')) { 59 | width = 4; 60 | height = 4; 61 | } 62 | 63 | if (format.startsWith('astc')) { 64 | const match = format.match(/(\d+)x(\d+)/); 65 | if (match) { 66 | width = parseInt(match[1], 10); 67 | height = parseInt(match[2], 10); 68 | } 69 | } 70 | 71 | const GPUTexture = (GPUTextureUsage as ExtendedGPUTextureUsage); 72 | 73 | device.createTexture({ 74 | size: [width, height, 1], 75 | format, 76 | usage: GPUTexture.SAMPLED 77 | | GPUTexture.OUTPUT_ATTACHMENT 78 | | GPUTexture.STORAGE 79 | | GPUTexture.COPY_SRC 80 | | GPUTexture.COPY_DST, 81 | }); 82 | 83 | res.push(format); 84 | } catch (err) { } 85 | } 86 | 87 | return res; 88 | } catch (err) { 89 | console.error(err); 90 | 91 | return null; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/MixVisit.ts: -------------------------------------------------------------------------------- 1 | import { clientParameters } from './client-parameters'; 2 | import type { ClientParameters } from './client-parameters'; 3 | import { contextualClientParameters } from './contextual-client-parameters'; 4 | import type { ContextualClientParameters } from './contextual-client-parameters'; 5 | import type { 6 | ClientData, 7 | GetterResults, 8 | LoadOptions, 9 | MixVisitInterface, 10 | } from './types'; 11 | import { x64 } from './utils/hashing'; 12 | import { 13 | cloneDeep, 14 | hasProperty, 15 | removeFields, 16 | TDef, 17 | } from './utils/helpers'; 18 | import { loadParameters } from './utils/load'; 19 | 20 | export class MixVisit implements MixVisitInterface { 21 | public loadTime: number | null = null; 22 | 23 | public fingerprintHash: string | null = null; 24 | 25 | private cache: ClientData | null = null; 26 | 27 | public async load(options?: LoadOptions): Promise { 28 | try { 29 | const startTime = Date.now(); 30 | 31 | const [clientParametersResult, contextualClientParametersResult] = await Promise.all([ 32 | loadParameters(clientParameters, options), 33 | loadParameters(contextualClientParameters, options), 34 | ]); 35 | 36 | const endTime = Date.now(); 37 | this.loadTime = endTime - startTime; 38 | 39 | const results: ClientData = { 40 | ...clientParametersResult, 41 | ...contextualClientParametersResult, 42 | }; 43 | 44 | const clientParametersWithoutDuration = removeFields(clientParametersResult, ['duration']); 45 | const strForHashing = JSON.stringify(clientParametersWithoutDuration); 46 | 47 | const isFirstLoad = !this.cache; 48 | const newCache = isFirstLoad ? {} : cloneDeep(this.cache); 49 | 50 | // Update or load clientData to cache 51 | Object.assign(newCache, results); 52 | 53 | this.cache = newCache; 54 | this.fingerprintHash = x64.hash128(strForHashing); 55 | } catch (err) { 56 | console.error(err); 57 | } 58 | } 59 | 60 | public get(param?: keyof ClientData | (keyof ClientData)[]): GetterResults { 61 | if (!this.cache) { 62 | return null; 63 | } 64 | 65 | if (this.isClientDataKey(param)) { 66 | return this.cache[param].error ?? this.cache[param].value; 67 | } 68 | 69 | if (this.isArrayOfClientDataKey(param)) { 70 | return Object.fromEntries(param.map((key) => [key, this.cache[key]])); 71 | } 72 | 73 | return this.cache; 74 | } 75 | 76 | private isClientDataKey(key: unknown): key is keyof ClientData { 77 | return key && TDef.isString(key) && hasProperty(this.cache, key as string); 78 | } 79 | 80 | private isArrayOfClientDataKey(keys: unknown): keys is (keyof ClientData)[] { 81 | return keys && TDef.isArray(keys) && (keys as string[]).every((key) => this.isClientDataKey(key)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/mediaDecodingCapabilities.ts: -------------------------------------------------------------------------------- 1 | import { hasProperty } from '../utils/helpers'; 2 | 3 | interface MediaDecodingCapabilities extends MediaCapabilitiesInfo { 4 | keySystemAccess?: MediaKeySystemAccess | null; 5 | } 6 | 7 | export async function getMediaDecodingCapabilities(): Promise { 8 | if (!hasProperty(navigator, 'mediaCapabilities')) { 9 | return []; 10 | } 11 | 12 | const videoConfigurations = [ 13 | { 14 | type: 'file', 15 | video: { 16 | contentType: 'video/mp4; codecs="avc1.42E01E"', 17 | width: 1920, 18 | height: 1080, 19 | bitrate: 2646242, 20 | framerate: 30, 21 | }, 22 | }, 23 | { 24 | type: 'file', 25 | video: { 26 | contentType: 'video/webm; codecs="vp9"', 27 | width: 1920, 28 | height: 1080, 29 | bitrate: 2646242, 30 | framerate: 30, 31 | }, 32 | }, 33 | { 34 | type: 'file', 35 | video: { 36 | contentType: 'video/ogg; codecs="theora"', 37 | width: 1280, 38 | height: 720, 39 | bitrate: 1500000, 40 | framerate: 30, 41 | }, 42 | }, 43 | { 44 | type: 'media-source', 45 | video: { 46 | contentType: 'video/mp4; codecs="avc1.42E01E"', 47 | width: 1920, 48 | height: 1080, 49 | bitrate: 2646242, 50 | framerate: 30, 51 | }, 52 | }, 53 | { 54 | type: 'media-source', 55 | video: { 56 | contentType: 'video/webm; codecs="vp9"', 57 | width: 1920, 58 | height: 1080, 59 | bitrate: 2646242, 60 | framerate: 30, 61 | }, 62 | }, 63 | { 64 | type: 'media-source', 65 | video: { 66 | contentType: 'video/ogg; codecs="theora"', 67 | width: 1280, 68 | height: 720, 69 | bitrate: 1500000, 70 | framerate: 30, 71 | }, 72 | }, 73 | ] as const; 74 | 75 | const result = await Promise.all( 76 | videoConfigurations.map(async (config) => { 77 | try { 78 | const decodingInfo: MediaDecodingCapabilities = await navigator.mediaCapabilities.decodingInfo(config); 79 | 80 | return { 81 | contentType: config.video.contentType, 82 | powerEfficient: decodingInfo.powerEfficient, 83 | smooth: decodingInfo.smooth, 84 | supported: decodingInfo.supported, 85 | keySystemAccess: decodingInfo.keySystemAccess, 86 | }; 87 | } catch (err) { 88 | return { 89 | contentType: config.video.contentType, 90 | powerEfficient: false, 91 | smooth: false, 92 | supported: false, 93 | keySystemAccess: null, 94 | }; 95 | } 96 | }), 97 | ); 98 | 99 | return result; 100 | } 101 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/canvas/detect.ts: -------------------------------------------------------------------------------- 1 | import { renderTextImageForDetectSpoofing } from './images'; 2 | import { createCanvas, isCanvasSupported } from './utils'; 3 | import { isBlink } from '../../utils/browser'; 4 | 5 | type DetectResult = { 6 | diffIndexes: number[]; 7 | diffValues: number[]; 8 | significantPixelIndexes: number[]; 9 | significantPixelValues: number[][]; 10 | }; 11 | 12 | export async function detectCanvasFingerprinting(): Promise { 13 | const [originalCanvas, originalContext] = createCanvas(); 14 | const [testCanvas, testContext] = createCanvas(); 15 | 16 | if (!( 17 | isBlink() 18 | && isCanvasSupported(originalCanvas, originalContext) 19 | && testContext 20 | )) { 21 | return null; 22 | } 23 | 24 | const canvasWidth = 21; 25 | const canvasHeight = 120; 26 | const ignorePixels = [ 27 | 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 153, 154, 155, 158, 159, 28 | 174, 175, 176, 179, 180, 195, 196, 197, 198, 199, 200, 201, 216, 217, 218, 29 | 221, 222, 237, 238, 239, 242, 243, 258, 259, 260, 263, 264, 278, 279, 280, 30 | 281, 282, 283, 284, 285, 286, 287, 156, 157, 160, 219, 220, 31 | ]; 32 | 33 | originalCanvas.width = canvasWidth; 34 | originalCanvas.height = canvasHeight; 35 | testCanvas.width = canvasWidth; 36 | testCanvas.height = canvasHeight; 37 | 38 | renderTextImageForDetectSpoofing(originalCanvas, originalContext); 39 | 40 | const imageData = originalCanvas.toDataURL(); 41 | 42 | return new Promise((resolve) => { 43 | const image = new Image(); 44 | 45 | image.onload = () => { 46 | const diffIndexes = []; 47 | const diffValues = []; 48 | 49 | testContext.drawImage(image, 0, 0); 50 | 51 | const originalImageData = originalContext.getImageData(0, 0, originalCanvas.width, originalCanvas.height); 52 | const testImageData = testContext.getImageData(0, 0, testCanvas.width, testCanvas.height); 53 | 54 | const originalPixels = Array.from(originalImageData.data); 55 | const testPixels = Array.from(testImageData.data); 56 | 57 | let uniquePixelIndexes = []; 58 | originalPixels.forEach((pixel, index) => { 59 | if (pixel !== 102) { 60 | uniquePixelIndexes.push(Math.floor(index / 4)); 61 | } 62 | }); 63 | 64 | uniquePixelIndexes = [...new Set(uniquePixelIndexes)]; 65 | 66 | const significantPixelIndexes = uniquePixelIndexes.filter((index) => !ignorePixels.includes(index)); 67 | const significantPixelValues = significantPixelIndexes.map( 68 | (index) => [index * 4, index * 4 + 1, index * 4 + 2, index * 4 + 3].map((i) => originalPixels[i]), 69 | ); 70 | 71 | testPixels.forEach((pixel, index) => { 72 | if (pixel !== originalPixels[index]) { 73 | diffIndexes.push(index); 74 | diffValues.push(pixel - originalPixels[index]); 75 | } 76 | }); 77 | 78 | resolve({ 79 | diffIndexes, 80 | diffValues, 81 | significantPixelIndexes, 82 | significantPixelValues, 83 | }); 84 | }; 85 | 86 | image.onerror = () => resolve(null); 87 | image.src = imageData; 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /example/src/lib/utils/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Gets the type of a value. 3 | * 4 | * @param value - The value to get the type for. 5 | * @returns The type of the value. 6 | */ 7 | export const type = (value: any): string => { 8 | const matches = Object.prototype.toString.call(value).match(/^\[object (\S+?)\]$/) || []; 9 | 10 | return matches[1]?.toLowerCase() || 'undefined'; 11 | }; 12 | 13 | export const TDef = { 14 | isString: (value: any) => type(value) === 'string', 15 | isNumber: (value: any) => type(value) === 'number', 16 | isNumberFinity: (value: any) => Number.isFinite(value), 17 | isNegativeInfinity: (value: any) => value === Number.NEGATIVE_INFINITY, 18 | isPositiveInfinity: (value: any) => value === Number.POSITIVE_INFINITY, 19 | isNaN: (value: any) => Number.isNaN(value), 20 | isObject: (value: any) => type(value) === 'object', 21 | isArray: (value: any) => type(value) === 'array', 22 | isBoolean: (value: any) => type(value) === 'boolean', 23 | isSymbol: (value: any) => type(value) === 'symbol', 24 | isUndefined: (value: any) => type(value) === 'undefined', 25 | isNull: (value: any) => type(value) === 'null', 26 | isDate: (value: any) => type(value) === 'date', 27 | isBigIng: (value: any) => type(value) === 'bigint', 28 | isMap: (value: any) => type(value) === 'map', 29 | isSet: (value: any) => type(value) === 'set', 30 | isWeakMap: (value: any) => type(value) === 'weakmap', 31 | isWeakSet: (value: any) => type(value) === 'weakset', 32 | isRegExp: (value: any) => type(value) === 'regexp', 33 | isFunc: (value: any) => type(value) === 'function', 34 | isError: (value: any) => type(value) === 'error', 35 | isNil: (value: any) => TDef.isUndefined(value) || TDef.isNull(value), 36 | }; 37 | 38 | /** 39 | * Format a date string as `DD MMM YYYY HH:MM`. 40 | * 41 | * @param date - A date string in any format supported by the Date constructor. 42 | * @returns A string in the format `DD MMM YYYY HH:MM`. 43 | */ 44 | export function formatDate(date: string): string { 45 | const monthNames = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] as const; 46 | const givenDate = new Date(date); 47 | 48 | const day = String(givenDate.getDate()).padStart(2, '0'); 49 | const month = monthNames[givenDate.getMonth()]; 50 | const year = givenDate.getFullYear(); 51 | const hours = String(givenDate.getHours()).padStart(2, '0'); 52 | const minutes = String(givenDate.getMinutes()).padStart(2, '0'); 53 | 54 | return `${day} ${month} ${year} ${hours}:${minutes}`; 55 | } 56 | 57 | /** 58 | * Format a date string as a string indicating how many days ago it was. 59 | * 60 | * @param date - A date string in any format supported by the Date constructor. 61 | * @returns A string like 'TODAY', 'YESTERDAY', or a number followed by 'DAYS AGO'. 62 | */ 63 | export function formatDateDifference(date: string): string { 64 | const today = new Date(); 65 | const givenDate = new Date(date); 66 | today.setHours(0, 0, 0, 0); 67 | givenDate.setHours(0, 0, 0, 0); 68 | 69 | const diffTime = today.getTime() - givenDate.getTime(); 70 | const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); 71 | 72 | switch (diffDays) { 73 | case 0: 74 | return 'TODAY'; 75 | case 1: 76 | return 'YESTERDAY'; 77 | default: 78 | return `${diffDays} DAYS AGO`; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/utils/load.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-promise-executor-return */ 2 | /* eslint-disable no-await-in-loop */ 3 | 4 | import { ErrorType } from './enums'; 5 | import { TDef } from './helpers'; 6 | import type { ClientParameters } from '../client-parameters'; 7 | import type { ContextualClientParameters } from '../contextual-client-parameters'; 8 | import type { LoadOptions, Result, UnwrappedParameters } from '../types'; 9 | 10 | type UnknownParameters = Record unknown>; 11 | type Parameters = ClientParameters | ContextualClientParameters | UnknownParameters; 12 | 13 | type UnwrapClient = T extends ClientParameters ? UnwrappedParameters : never; 14 | type UnwrapContextual = T extends ContextualClientParameters ? UnwrappedParameters : never; 15 | type UnwrapUnknowParams = T extends UnknownParameters ? UnwrappedParameters : never; 16 | 17 | type LoadResult = UnwrapClient | UnwrapContextual | UnwrapUnknowParams; 18 | 19 | const DEFAULT_TIMEOUT = 2000; 20 | const TIMEOUT_FOR_ONLY_OPTION = 12000; 21 | 22 | export async function loadParameters( 23 | parameters: Parameters, 24 | options?: LoadOptions, 25 | ): Promise>> { 26 | const result = {} as Result>; 27 | const { only, exclude, timeout } = options || {}; 28 | 29 | // Mutually exclusive check 30 | if (only && exclude) { 31 | throw new Error('Cannot use both "only" and "exclude" options'); 32 | } 33 | 34 | const requiredTimeout = only && !timeout 35 | ? TIMEOUT_FOR_ONLY_OPTION 36 | : timeout || DEFAULT_TIMEOUT; 37 | 38 | // Determine the list of keys to process 39 | const parameterKeys = only 40 | ? only.filter((key) => TDef.isFunc(parameters[key])) 41 | : Object.keys(parameters).filter((key) => TDef.isFunc(parameters[key]) && !exclude?.includes(key)); 42 | 43 | for (const key of parameterKeys) { 44 | try { 45 | const fn = parameters[key]; 46 | const start = Date.now(); 47 | 48 | let value: any; 49 | 50 | if (fn.constructor.name === 'AsyncFunction') { 51 | // Handle asynchronous functions with timeout 52 | value = await Promise.race([ 53 | fn(), 54 | new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), requiredTimeout)), 55 | ]); 56 | } else { 57 | // Handle synchronous functions 58 | value = fn(); 59 | 60 | // Ensure that if the result is still a promise, we resolve it 61 | if (typeof value?.then === 'function') { 62 | value = await value; 63 | } 64 | } 65 | 66 | result[key] = { value, duration: Date.now() - start }; 67 | } catch (err) { 68 | const isTimeout = err.message === 'Timeout'; 69 | result[key] = { 70 | error: createError( 71 | isTimeout ? ErrorType.TIMEOUT : ErrorType.INTERNAL, 72 | isTimeout ? `Timeout exceeded by ${requiredTimeout} ms` : err.message, 73 | ), 74 | }; 75 | } 76 | } 77 | 78 | return result; 79 | } 80 | 81 | function createError(errorType: ErrorType.INTERNAL | ErrorType.TIMEOUT, message?: string): { code: string; message: string } { 82 | if (errorType === ErrorType.TIMEOUT) { 83 | return { 84 | code: ErrorType.TIMEOUT, 85 | message: message || 'Timeout exceeded', 86 | }; 87 | } 88 | 89 | return { 90 | code: ErrorType.INTERNAL, 91 | message: `An unexpected error occurred while collecting parameter data.${message ? ` Error: ${message}` : ''}`, 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/contextual-client-parameters/geolocation.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | type Geolocation = { 4 | statuses: { 5 | api: boolean; 6 | permission?: 'granted' | 'denied' | 'prompt'; 7 | }; 8 | geolocationPosition?: { 9 | accuracy: number; 10 | altitude: number | null; 11 | altitudeAccuracy: number | null; 12 | heading: number | null; 13 | latitude: number; 14 | longitude: number; 15 | speed: number | null; 16 | timestamp: number; 17 | }; 18 | error?: { 19 | code: number; 20 | message: string; 21 | }; 22 | }; 23 | 24 | type GeolocationError = { 25 | success: false; 26 | error: { 27 | code: number; 28 | message: string; 29 | }; 30 | }; 31 | 32 | type GeolocationPositionResults = GeolocationPosition | GeolocationError; 33 | 34 | const geoOptions = { 35 | enableHighAccuracy: true, 36 | maximumAge: 0, 37 | timeout: 10 * 1000, 38 | }; 39 | 40 | export async function getGeolocation(): Promise { 41 | try { 42 | const res: Geolocation = { 43 | statuses: { 44 | api: true, 45 | }, 46 | }; 47 | 48 | if (!navigator.geolocation) { 49 | res.statuses.api = false; 50 | 51 | return res; 52 | } 53 | 54 | const permission = await navigator.permissions.query({ name: 'geolocation' }); 55 | res.statuses.permission = permission.state; 56 | 57 | if (permission.state === 'denied') { 58 | return res; 59 | } 60 | 61 | const geolocationResult = await getGeolocationPosition(); 62 | handleGeolocationData(res, geolocationResult); 63 | 64 | return res; 65 | } catch (err) { 66 | console.error(err); 67 | 68 | return null; 69 | } 70 | } 71 | 72 | async function getGeolocationPosition(): Promise { 73 | return new Promise((resolve) => { 74 | const rejector = (err) => { 75 | resolve({ 76 | success: false, 77 | error: { 78 | code: err.code, 79 | message: err.message, 80 | }, 81 | }); 82 | }; 83 | 84 | navigator.geolocation.getCurrentPosition(resolve, rejector, geoOptions); 85 | }); 86 | } 87 | 88 | function handleGeolocationData(result: Geolocation, geolocationPosition: GeolocationPositionResults): void { 89 | if (isGeolocationPosition(geolocationPosition)) { 90 | result.geolocationPosition = { 91 | timestamp: geolocationPosition.timestamp, 92 | latitude: geolocationPosition.coords.latitude, 93 | longitude: geolocationPosition.coords.longitude, 94 | accuracy: geolocationPosition.coords.accuracy, 95 | altitude: geolocationPosition.coords.altitude, 96 | altitudeAccuracy: geolocationPosition.coords.altitudeAccuracy, 97 | heading: geolocationPosition.coords.heading, 98 | speed: geolocationPosition.coords.speed, 99 | }; 100 | } else if (isGeolocationError(geolocationPosition)) { 101 | // error.code can be: 102 | // 0: unknown error 103 | // 1: permission denied 104 | // 2: position unavailable (error response from location provider) 105 | // 3: timed out 106 | result.error = geolocationPosition.error; 107 | } else { 108 | console.error('Unknown result type from getGeolocationData() :>>', geolocationPosition); 109 | } 110 | } 111 | 112 | function isGeolocationPosition(obj: any): obj is GeolocationPosition { 113 | return !!(obj as GeolocationPosition).coords; 114 | } 115 | 116 | function isGeolocationError(obj: any): obj is GeolocationError { 117 | return !!(obj as GeolocationError).error; 118 | } 119 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/client-parameters/navigator.ts: -------------------------------------------------------------------------------- 1 | import { TDef } from '../utils/helpers'; 2 | 3 | type NavigatorInfo = { 4 | userAgent: string; 5 | appName: string; 6 | appVersion: string; 7 | appCodeName: string; 8 | deviceMemory?: number; 9 | hardwareConcurrency: number; 10 | platform: string; 11 | product: string; 12 | language: string; 13 | languages: string[]; 14 | oscpu?: string; 15 | cpuClass?: string; 16 | productSub: string; 17 | vendorSub: string; 18 | vendor: string; 19 | maxTouchPoints: number; 20 | doNotTrack: string | null; 21 | pdfViewerEnabled: boolean; 22 | cookieEnabled: boolean; 23 | onLine: boolean; 24 | webdriver: boolean; 25 | plugins: PluginData[]; 26 | mimeTypes: Record; 27 | userAgentData?: UADataValues; 28 | highEntropyValues?: UADataValues; 29 | }; 30 | 31 | type MimeTypes = { 32 | description: string; 33 | suffixes: string; 34 | enabledPlugin: string; 35 | }; 36 | 37 | type PluginMimeTypeData = { 38 | type: string; 39 | suffixes: string; 40 | }; 41 | 42 | type PluginData = { 43 | name: string; 44 | description: string; 45 | filename: string; 46 | mimeTypes: PluginMimeTypeData[]; 47 | }; 48 | 49 | export async function getNavigatorInfo(): Promise { 50 | const result = {} as NavigatorInfo; 51 | const { plugins, mimeTypes, userAgentData } = navigator; 52 | 53 | const navigatorProps = [ 54 | 'userAgent', 55 | 'appName', 56 | 'appVersion', 57 | 'appCodeName', 58 | 'deviceMemory', 59 | 'hardwareConcurrency', 60 | 'platform', 61 | 'product', 62 | 'language', 63 | 'languages', 64 | 'oscpu', 65 | 'cpuClass', 66 | 'productSub', 67 | 'vendorSub', 68 | 'vendor', 69 | 'maxTouchPoints', 70 | 'doNotTrack', 71 | 'pdfViewerEnabled', 72 | 'cookieEnabled', 73 | 'onLine', 74 | 'webdriver', 75 | 'userAgentData', 76 | ] as const; 77 | 78 | const highEntropyValuesProps = [ 79 | 'architecture', 80 | 'bitness', 81 | 'brands', 82 | 'mobile', 83 | 'model', 84 | 'platform', 85 | 'platformVersion', 86 | 'uaFullVersion', 87 | 'wow64', 88 | 'fullVersionList', 89 | ]; 90 | 91 | // Default properties 92 | 93 | for (const key of navigatorProps) { 94 | if (!TDef.isUndefined(navigator[key])) { 95 | result[key as string] = navigator[key]; 96 | } 97 | } 98 | 99 | // MimeTypes 100 | 101 | result.mimeTypes = mimeTypes ? {} : null; 102 | 103 | for (const mimeType of mimeTypes) { 104 | result.mimeTypes[mimeType.type] = { 105 | description: mimeType.description, 106 | suffixes: mimeType.suffixes, 107 | enabledPlugin: mimeType.enabledPlugin.name, 108 | }; 109 | } 110 | 111 | // Plugins 112 | 113 | result.plugins = plugins ? [] : null; 114 | 115 | // Safari 10 doesn't support iterating navigator.plugins with for...of 116 | for (let i = 0; i < plugins?.length; ++i) { 117 | const plugin = plugins[i]; 118 | if (!plugin) { 119 | continue; 120 | } 121 | 122 | const mimeTypesArr: PluginMimeTypeData[] = []; 123 | for (let j = 0; j < plugin.length; ++j) { 124 | const mimeType = plugin[j]; 125 | mimeTypesArr.push({ 126 | type: mimeType.type, 127 | suffixes: mimeType.suffixes, 128 | }); 129 | } 130 | 131 | result.plugins.push({ 132 | name: plugin.name, 133 | description: plugin.description, 134 | filename: plugin.filename, 135 | mimeTypes: mimeTypesArr, 136 | }); 137 | } 138 | 139 | // High Entropy Values 140 | 141 | result.highEntropyValues = (await userAgentData?.getHighEntropyValues(highEntropyValuesProps)) ?? null; 142 | 143 | return result; 144 | } 145 | -------------------------------------------------------------------------------- /packages/mixvisit-lite/src/global.d.ts: -------------------------------------------------------------------------------- 1 | /* Browser APIs not described by TypeScript */ 2 | 3 | interface Window { 4 | openDatabase?(...args: unknown[]): void; 5 | ApplePaySession?: ApplePaySessionConstructor; 6 | __fpjs_d_m?: unknown; 7 | } 8 | 9 | interface Navigator { 10 | userAgentData?: NavigatorUAData; 11 | globalPrivacyControl?: boolean; 12 | oscpu?: string; 13 | userLanguage?: string; 14 | browserLanguage?: string; 15 | systemLanguage?: string; 16 | deviceMemory?: number; 17 | cpuClass?: string; 18 | connection?: Connection; 19 | mozConnection?: Connection; 20 | webkitConnection?: Connection; 21 | readonly msMaxTouchPoints?: number; 22 | scheduling?: Scheduling; 23 | getBattery: () => Promise; 24 | } 25 | 26 | interface NavigatorUABrandVersion { 27 | readonly brand: string; 28 | readonly version: string; 29 | } 30 | 31 | interface UALowEntropyJSON { 32 | readonly brands: NavigatorUABrandVersion[]; 33 | readonly mobile: boolean; 34 | readonly platform: string; 35 | } 36 | 37 | interface UADataValues { 38 | readonly brands?: NavigatorUABrandVersion[]; 39 | readonly mobile?: boolean; 40 | readonly platform?: string; 41 | readonly architecture?: string; 42 | readonly bitness?: string; 43 | readonly formFactor?: string[]; 44 | readonly model?: string; 45 | readonly platformVersion?: string; 46 | /** @deprecated in favour of fullVersionList */ 47 | readonly uaFullVersion?: string; 48 | readonly fullVersionList?: NavigatorUABrandVersion[]; 49 | readonly wow64?: boolean; 50 | } 51 | 52 | interface NavigatorUAData extends UALowEntropyJSON { 53 | getHighEntropyValues(hints: string[]): Promise; 54 | toJSON(): UALowEntropyJSON; 55 | } 56 | 57 | interface Document { 58 | msFullscreenElement?: typeof document.fullscreenElement; 59 | mozFullScreenElement?: typeof document.fullscreenElement; 60 | webkitFullscreenElement?: typeof document.fullscreenElement; 61 | msExitFullscreen?: typeof document.exitFullscreen; 62 | mozCancelFullScreen?: typeof document.exitFullscreen; 63 | webkitExitFullscreen?: typeof document.exitFullscreen; 64 | } 65 | 66 | interface Screen { 67 | availLeft?: number; 68 | availTop?: number; 69 | } 70 | 71 | interface Element { 72 | webkitRequestFullscreen?: typeof Element.prototype.requestFullscreen; 73 | } 74 | 75 | interface CSSStyleDeclaration { 76 | zoom: string; 77 | textSizeAdjust: string; 78 | } 79 | 80 | interface ApplePaySessionConstructor { 81 | new(version: number, request: Record): never; 82 | canMakePayments(): boolean; 83 | } 84 | 85 | interface HTMLAnchorElement { 86 | attributionSourceId?: number; 87 | attributionsourceid?: number | string; 88 | } 89 | 90 | interface GPURequestAdapterOptions { 91 | compatibilityMode?: boolean; 92 | } 93 | 94 | interface GPUAdapter { 95 | readonly isCompatibilityMode: boolean; 96 | } 97 | 98 | type ExtendedGPUTextureUsage = typeof GPUTextureUsage & { 99 | readonly SAMPLED: GPUFlagsConstant; 100 | readonly OUTPUT_ATTACHMENT: GPUFlagsConstant; 101 | readonly STORAGE: GPUFlagsConstant; 102 | }; 103 | 104 | type Connection = { 105 | type?: string; 106 | effectiveType?: string; 107 | downlink?: number; 108 | downlinkMax?: number; 109 | rtt?: number; 110 | saveData?: boolean; 111 | onchange?: () => void; 112 | ontypechange?: () => void; 113 | }; 114 | 115 | type Scheduling = { 116 | isInputPending?: (options?: { includeContinuous?: boolean }) => boolean; 117 | }; 118 | 119 | type BatteryManager = { 120 | charging: boolean; 121 | chargingTime: number; 122 | dischargingTime: number; 123 | level: number; 124 | onchargingchange: ((this: BatteryManager, ev: Event) => any) | null; 125 | onchargingtimechange: ((this: BatteryManager, ev: Event) => any) | null; 126 | ondischargingtimechange: ((this: BatteryManager, ev: Event) => any) | null; 127 | onlevelchange: ((this: BatteryManager, ev: Event) => any) | null; 128 | }; 129 | -------------------------------------------------------------------------------- /example/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 | 89 | {@html highlightStyle} 90 | 91 | 92 |
93 |

MixVisitJS

94 |

JS Fingerprint to identify & track devices

95 | 96 | 97 | 101 | 102 | {#if jsEnabled} 103 | {#if status === 'loaded'} 104 | 105 | 106 | 107 | 108 | {#if durationsData.length} 109 | 110 | {/if} 111 | {#if errorsData.length} 112 | 113 | {/if} 114 | 115 | {:else if status === 'not loaded'} 116 |

Loading ...

117 | {:else} 118 |

Something wrong

119 | {/if} 120 | {/if} 121 |
122 | 123 |