@thumbmarkjs/thumbmarkjs 1.0.0-rc.2 → 1.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +5 -5
- package/dist/thumbmark.esm.js +1 -1
- package/dist/thumbmark.esm.js.map +1 -1
- package/dist/thumbmark.umd.js +1 -1
- package/dist/thumbmark.umd.js.map +1 -1
- package/dist/types/components/audio/index.d.ts +2 -0
- package/dist/types/components/canvas/index.d.ts +3 -0
- package/dist/types/components/fonts/index.d.ts +4 -0
- package/dist/types/components/hardware/index.d.ts +2 -0
- package/dist/types/components/locales/index.d.ts +2 -0
- package/dist/types/components/math/index.d.ts +2 -0
- package/dist/types/components/permissions/index.d.ts +3 -0
- package/dist/types/components/plugins/index.d.ts +2 -0
- package/dist/types/components/screen/index.d.ts +2 -0
- package/dist/types/components/system/browser.d.ts +7 -0
- package/dist/types/components/system/index.d.ts +2 -0
- package/dist/types/components/webgl/index.d.ts +2 -0
- package/dist/types/factory.d.ts +51 -0
- package/dist/types/functions/filterComponents.d.ts +11 -0
- package/dist/types/functions/index.d.ts +87 -0
- package/dist/types/functions/legacy_functions.d.ts +27 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/options.d.ts +28 -0
- package/dist/types/thumbmark.d.ts +26 -0
- package/dist/types/utils/commonPixels.d.ts +1 -0
- package/dist/types/utils/ephemeralIFrame.d.ts +4 -0
- package/dist/types/utils/getMostFrequent.d.ts +5 -0
- package/dist/types/utils/hash.d.ts +5 -0
- package/dist/types/utils/imageDataToDataURL.d.ts +1 -0
- package/dist/types/utils/log.d.ts +8 -0
- package/dist/types/utils/raceAll.d.ts +9 -0
- package/dist/types/utils/version.d.ts +4 -0
- package/package.json +1 -1
- package/src/components/audio/index.ts +1 -2
- package/src/components/canvas/index.ts +6 -9
- package/src/components/plugins/index.ts +26 -17
- package/src/components/screen/index.ts +19 -8
- package/src/components/system/browser.test.ts +113 -0
- package/src/components/system/browser.ts +76 -44
- package/src/components/system/index.ts +4 -2
- package/src/functions/functions.test.ts +1 -1
- package/src/functions/index.ts +18 -55
- package/src/index.ts +2 -1
- package/src/options.ts +4 -2
- package/src/thumbmark.ts +2 -1
- package/src/utils/log.ts +34 -0
- package/src/utils/version.ts +8 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThumbmarkJS: Main fingerprinting and API logic
|
|
3
|
+
*
|
|
4
|
+
* This module handles component collection, API calls, uniqueness scoring, and data filtering
|
|
5
|
+
* for the ThumbmarkJS browser fingerprinting library.
|
|
6
|
+
*
|
|
7
|
+
* Exports:
|
|
8
|
+
* - getThumbmark
|
|
9
|
+
* - getThumbmarkDataFromPromiseMap
|
|
10
|
+
* - resolveClientComponents
|
|
11
|
+
* - filterThumbmarkData
|
|
12
|
+
*
|
|
13
|
+
* Internal helpers and types are also defined here.
|
|
14
|
+
*/
|
|
15
|
+
import { optionsInterface } from "../options";
|
|
16
|
+
import { componentInterface, includeComponent as globalIncludeComponent } from "../factory";
|
|
17
|
+
/**
|
|
18
|
+
* Info returned from the API (IP, classification, uniqueness, etc)
|
|
19
|
+
*/
|
|
20
|
+
interface infoInterface {
|
|
21
|
+
ip_address?: {
|
|
22
|
+
ip_address: string;
|
|
23
|
+
ip_identifier: string;
|
|
24
|
+
autonomous_system_number: number;
|
|
25
|
+
ip_version: 'v6' | 'v4';
|
|
26
|
+
};
|
|
27
|
+
classification?: {
|
|
28
|
+
tor: boolean;
|
|
29
|
+
proxy: boolean;
|
|
30
|
+
datacenter: boolean;
|
|
31
|
+
danger_level: number;
|
|
32
|
+
};
|
|
33
|
+
uniqueness?: {
|
|
34
|
+
score: number | string;
|
|
35
|
+
};
|
|
36
|
+
timed_out?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* API response structure
|
|
40
|
+
*/
|
|
41
|
+
interface apiResponse {
|
|
42
|
+
thumbmark?: string;
|
|
43
|
+
info?: infoInterface;
|
|
44
|
+
version?: string;
|
|
45
|
+
components?: componentInterface;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Final thumbmark response structure
|
|
49
|
+
*/
|
|
50
|
+
interface thumbmarkResponse {
|
|
51
|
+
components: componentInterface;
|
|
52
|
+
info: {
|
|
53
|
+
[key: string]: any;
|
|
54
|
+
};
|
|
55
|
+
version: string;
|
|
56
|
+
thumbmark: string;
|
|
57
|
+
/**
|
|
58
|
+
* Only present if options.performance is true.
|
|
59
|
+
*/
|
|
60
|
+
elapsed?: any;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Calls the Thumbmark API with the given components, using caching and deduplication.
|
|
64
|
+
* Returns a promise for the API response or null on error.
|
|
65
|
+
*/
|
|
66
|
+
export declare const getApiPromise: (options: optionsInterface, components: componentInterface) => Promise<apiResponse | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Main entry point: collects all components, optionally calls API, and returns thumbmark data.
|
|
69
|
+
*
|
|
70
|
+
* @param options - Options for fingerprinting and API
|
|
71
|
+
* @returns thumbmarkResponse (elapsed is present only if options.performance is true)
|
|
72
|
+
*/
|
|
73
|
+
export declare function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse>;
|
|
74
|
+
/**
|
|
75
|
+
* Resolves and times all filtered component promises from a component function map.
|
|
76
|
+
*
|
|
77
|
+
* @param comps - Map of component functions
|
|
78
|
+
* @param options - Options for filtering and timing
|
|
79
|
+
* @returns Object with elapsed times and filtered resolved components
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveClientComponents(comps: {
|
|
82
|
+
[key: string]: (options?: optionsInterface) => Promise<componentInterface | null>;
|
|
83
|
+
}, options?: optionsInterface): Promise<{
|
|
84
|
+
elapsed: Record<string, number>;
|
|
85
|
+
resolvedComponents: componentInterface;
|
|
86
|
+
}>;
|
|
87
|
+
export { globalIncludeComponent as includeComponent };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is here to support legacy implementations.
|
|
3
|
+
* Eventually, these functions will be removed to keep the library small.
|
|
4
|
+
*/
|
|
5
|
+
import { componentInterface } from '../factory';
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* @deprecated
|
|
9
|
+
*/
|
|
10
|
+
export declare function getFingerprintData(): Promise<componentInterface>;
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* @param includeData boolean
|
|
14
|
+
* @deprecated this function is going to be removed. use getThumbmark or Thumbmark class instead.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getFingerprint(includeData?: false): Promise<string>;
|
|
17
|
+
export declare function getFingerprint(includeData: true): Promise<{
|
|
18
|
+
hash: string;
|
|
19
|
+
data: componentInterface;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
*
|
|
23
|
+
* @deprecated use Thumbmark or getThumbmark instead with options
|
|
24
|
+
*/
|
|
25
|
+
export declare function getFingerprintPerformance(): Promise<{
|
|
26
|
+
elapsed: Record<string, number>;
|
|
27
|
+
}>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { getFingerprint, getFingerprintData, getFingerprintPerformance } from './functions/legacy_functions';
|
|
2
|
+
import { getThumbmark } from './functions';
|
|
3
|
+
import { getVersion } from './utils/version';
|
|
4
|
+
import { setOption } from './options';
|
|
5
|
+
import { includeComponent } from './factory';
|
|
6
|
+
import { Thumbmark } from './thumbmark';
|
|
7
|
+
export { Thumbmark, getThumbmark, getVersion, setOption, getFingerprint, getFingerprintData, getFingerprintPerformance, includeComponent };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface optionsInterface {
|
|
2
|
+
exclude?: string[];
|
|
3
|
+
include?: string[];
|
|
4
|
+
permissions_to_check?: PermissionName[];
|
|
5
|
+
timeout?: number;
|
|
6
|
+
logging?: boolean;
|
|
7
|
+
api_key?: string;
|
|
8
|
+
cache_api_call?: boolean;
|
|
9
|
+
performance?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare const API_ENDPOINT = "https://api.thumbmarkjs.com";
|
|
12
|
+
export declare const defaultOptions: optionsInterface;
|
|
13
|
+
export declare let options: {
|
|
14
|
+
exclude?: string[] | undefined;
|
|
15
|
+
include?: string[] | undefined;
|
|
16
|
+
permissions_to_check?: PermissionName[] | undefined;
|
|
17
|
+
timeout?: number | undefined;
|
|
18
|
+
logging?: boolean | undefined;
|
|
19
|
+
api_key?: string | undefined;
|
|
20
|
+
cache_api_call?: boolean | undefined;
|
|
21
|
+
performance?: boolean | undefined;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
*
|
|
25
|
+
* @param key @deprecated this function will be removed
|
|
26
|
+
* @param value
|
|
27
|
+
*/
|
|
28
|
+
export declare function setOption<K extends keyof optionsInterface>(key: K, value: optionsInterface[K]): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { optionsInterface } from "./options";
|
|
2
|
+
import { componentInterface } from "./factory";
|
|
3
|
+
/**
|
|
4
|
+
* A client for generating thumbmarks with a persistent configuration.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Thumbmark {
|
|
7
|
+
private options;
|
|
8
|
+
/**
|
|
9
|
+
* Creates a new Thumbmarker client instance.
|
|
10
|
+
* @param options - Default configuration options for this instance.
|
|
11
|
+
*/
|
|
12
|
+
constructor(options?: optionsInterface);
|
|
13
|
+
/**
|
|
14
|
+
* Generates a thumbmark using the instance's configuration.
|
|
15
|
+
* @param overrideOptions - Options to override for this specific call.
|
|
16
|
+
* @returns The thumbmark result.
|
|
17
|
+
*/
|
|
18
|
+
get(overrideOptions?: optionsInterface): Promise<any>;
|
|
19
|
+
getVersion(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Register a custom component to be included in the fingerprint.
|
|
22
|
+
* @param key - The component name
|
|
23
|
+
* @param fn - The component function
|
|
24
|
+
*/
|
|
25
|
+
includeComponent(key: string, fn: (options?: optionsInterface) => Promise<componentInterface | null>): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getCommonPixels(images: ImageData[], width: number, height: number): ImageData;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function imageDataToDataURL(imageData: ImageData): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { componentInterface } from '../factory';
|
|
2
|
+
import { optionsInterface } from '../options';
|
|
3
|
+
/**
|
|
4
|
+
* Logs thumbmark data to remote logging endpoint (only once per session)
|
|
5
|
+
* You can disable this by setting options.logging to false.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export declare function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface): Promise<void>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type DelayedPromise<T> = Promise<T>;
|
|
2
|
+
export declare function delay<T>(t: number, val: T): DelayedPromise<T>;
|
|
3
|
+
export interface RaceResult<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
elapsed?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function raceAllPerformance<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<RaceResult<T>[]>;
|
|
8
|
+
export declare function raceAll<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<(T | undefined)[]>;
|
|
9
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { componentInterface, componentFunctionInterface, includeComponent } from '../../factory'
|
|
2
|
-
import { optionsInterface } from '../../options';
|
|
3
2
|
import { getBrowser } from '../system/browser'
|
|
4
3
|
|
|
5
|
-
export default async function getAudio(
|
|
4
|
+
export default async function getAudio(): Promise<componentInterface | null> {
|
|
6
5
|
const browser = getBrowser()
|
|
7
6
|
if (!['SamsungBrowser', 'Safari'].includes(browser.name))
|
|
8
7
|
return createAudioFingerprint()
|
|
@@ -4,11 +4,6 @@ import { hash } from '../../utils/hash';
|
|
|
4
4
|
import { getBrowser } from '../system/browser';
|
|
5
5
|
import { optionsInterface } from '../../options';
|
|
6
6
|
|
|
7
|
-
const browser = getBrowser();
|
|
8
|
-
const name = browser.name.toLowerCase();
|
|
9
|
-
const ver = browser.version.split('.')[0] || '0';
|
|
10
|
-
const majorVer = parseInt(ver, 10);
|
|
11
|
-
|
|
12
7
|
const _RUNS = 3;
|
|
13
8
|
|
|
14
9
|
/**
|
|
@@ -20,9 +15,12 @@ const _RUNS = 3;
|
|
|
20
15
|
const _WIDTH = 280;
|
|
21
16
|
const _HEIGHT = 20;
|
|
22
17
|
|
|
23
|
-
export default async function getCanvas(
|
|
24
|
-
const browser = getBrowser()
|
|
25
|
-
|
|
18
|
+
export default async function getCanvas(): Promise<componentInterface | null> {
|
|
19
|
+
const browser = getBrowser();
|
|
20
|
+
const name = browser.name.toLowerCase();
|
|
21
|
+
const ver = browser.version.split('.')[0] || '0';
|
|
22
|
+
const majorVer = parseInt(ver, 10);
|
|
23
|
+
if (!['firefox', 'brave'].includes(name) && !(name === 'safari' && majorVer >= 17))
|
|
26
24
|
return generateCanvasFingerprint()
|
|
27
25
|
return null;
|
|
28
26
|
}
|
|
@@ -30,7 +28,6 @@ export default async function getCanvas(options?: optionsInterface): Promise<com
|
|
|
30
28
|
|
|
31
29
|
export function generateCanvasFingerprint(): Promise<componentInterface> {
|
|
32
30
|
const canvas = document.createElement('canvas');
|
|
33
|
-
const ctx = canvas.getContext('2d');
|
|
34
31
|
|
|
35
32
|
return new Promise((resolve) => {
|
|
36
33
|
/**
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory'
|
|
2
|
+
import { getBrowser } from '../system/browser';
|
|
2
3
|
|
|
3
|
-
export default function getPlugins(): Promise<componentInterface> {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
export default async function getPlugins(): Promise<componentInterface | null> {
|
|
5
|
+
const browser = getBrowser();
|
|
6
|
+
const name = browser.name.toLowerCase();
|
|
7
|
+
|
|
8
|
+
// Brave will scramble the plugins list, so not including it.
|
|
9
|
+
if (['brave'].includes(name)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const plugins: string[] = [];
|
|
14
|
+
|
|
15
|
+
if (navigator.plugins) {
|
|
16
|
+
for (let i = 0; i < navigator.plugins.length; i++) {
|
|
17
|
+
const plugin = navigator.plugins[i];
|
|
18
|
+
plugins.push([plugin.name, plugin.filename, plugin.description ].join("|"));
|
|
11
19
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
resolve(
|
|
24
|
+
{
|
|
25
|
+
'plugins': plugins
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory';
|
|
2
|
+
import { isMobileUserAgent } from '../system/browser';
|
|
2
3
|
|
|
3
4
|
export default function getScreen(): Promise<componentInterface> {
|
|
4
5
|
return new Promise((resolve) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const result: componentInterface = {
|
|
7
|
+
'is_touchscreen': navigator.maxTouchPoints > 0,
|
|
8
|
+
'maxTouchPoints': navigator.maxTouchPoints,
|
|
9
|
+
'colorDepth': screen.colorDepth,
|
|
10
|
+
'mediaMatches': matchMedias(),
|
|
11
|
+
};
|
|
12
|
+
if (isMobileUserAgent() && navigator.maxTouchPoints > 0) {
|
|
13
|
+
result['resolution'] = screenResolution()
|
|
14
|
+
}
|
|
15
|
+
resolve(result);
|
|
13
16
|
});
|
|
14
17
|
}
|
|
15
18
|
|
|
19
|
+
function screenResolution() {
|
|
20
|
+
const w = window.screen.width;
|
|
21
|
+
const h = window.screen.height;
|
|
22
|
+
const longer = Math.max(w, h).toString();
|
|
23
|
+
const shorter = Math.min(w, h).toString();
|
|
24
|
+
return `${longer}x${shorter}`
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
function matchMedias(): string[] {
|
|
17
28
|
let results: string[] = [];
|
|
18
29
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getBrowser } from './browser';
|
|
2
|
+
|
|
3
|
+
describe('getBrowser', () => {
|
|
4
|
+
const originalNavigator = global.navigator;
|
|
5
|
+
|
|
6
|
+
function mockUserAgent(ua: string) {
|
|
7
|
+
Object.defineProperty(global, 'navigator', {
|
|
8
|
+
value: { userAgent: ua },
|
|
9
|
+
configurable: true,
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
// Restore original navigator
|
|
15
|
+
Object.defineProperty(global, 'navigator', {
|
|
16
|
+
value: originalNavigator,
|
|
17
|
+
configurable: true,
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Authoritative user agent test cases from DeviceAtlas
|
|
22
|
+
const cases = [
|
|
23
|
+
// Chrome (Windows)
|
|
24
|
+
{
|
|
25
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36',
|
|
26
|
+
expected: { name: 'Chrome', version: '120.0.6099.110' },
|
|
27
|
+
},
|
|
28
|
+
// Firefox (Windows)
|
|
29
|
+
{
|
|
30
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
|
|
31
|
+
expected: { name: 'Firefox', version: '120.0' },
|
|
32
|
+
},
|
|
33
|
+
// Edge (Windows)
|
|
34
|
+
{
|
|
35
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36 Edg/120.0.2210.61',
|
|
36
|
+
expected: { name: 'Edge', version: '120.0.2210.61' },
|
|
37
|
+
},
|
|
38
|
+
// Safari (macOS)
|
|
39
|
+
{
|
|
40
|
+
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15',
|
|
41
|
+
expected: { name: 'Safari', version: '17.1' },
|
|
42
|
+
},
|
|
43
|
+
// Chrome (macOS)
|
|
44
|
+
{
|
|
45
|
+
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36',
|
|
46
|
+
expected: { name: 'Chrome', version: '120.0.6099.110' },
|
|
47
|
+
},
|
|
48
|
+
// Firefox (macOS)
|
|
49
|
+
{
|
|
50
|
+
ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0',
|
|
51
|
+
expected: { name: 'Firefox', version: '120.0' },
|
|
52
|
+
},
|
|
53
|
+
// Chrome (Android)
|
|
54
|
+
{
|
|
55
|
+
ua: 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Mobile Safari/537.36',
|
|
56
|
+
expected: { name: 'Chrome', version: '120.0.6099.110' },
|
|
57
|
+
},
|
|
58
|
+
// Samsung Internet (Android)
|
|
59
|
+
{
|
|
60
|
+
ua: 'Mozilla/5.0 (Linux; Android 13; SAMSUNG SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/23.0 Chrome/120.0.6099.110 Mobile Safari/537.36',
|
|
61
|
+
expected: { name: 'SamsungBrowser', version: '23.0' },
|
|
62
|
+
},
|
|
63
|
+
// Firefox (Android)
|
|
64
|
+
{
|
|
65
|
+
ua: 'Mozilla/5.0 (Android 13; Mobile; rv:120.0) Gecko/120.0 Firefox/120.0',
|
|
66
|
+
expected: { name: 'Firefox', version: '120.0' },
|
|
67
|
+
},
|
|
68
|
+
// Safari (iPhone)
|
|
69
|
+
{
|
|
70
|
+
ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1',
|
|
71
|
+
expected: { name: 'Safari', version: '17.1' },
|
|
72
|
+
},
|
|
73
|
+
// Chrome (iPhone)
|
|
74
|
+
{
|
|
75
|
+
ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.6099.110 Mobile/15E148 Safari/604.1',
|
|
76
|
+
expected: { name: 'Chrome', version: '120.0.6099.110' },
|
|
77
|
+
},
|
|
78
|
+
// Firefox (iPhone)
|
|
79
|
+
{
|
|
80
|
+
ua: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/120.0 Mobile/15E148 Safari/604.1',
|
|
81
|
+
expected: { name: 'Firefox', version: '120.0' },
|
|
82
|
+
},
|
|
83
|
+
// Opera (Windows)
|
|
84
|
+
{
|
|
85
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36 OPR/107.0.5045.36',
|
|
86
|
+
expected: { name: 'Opera', version: '107.0.5045.36' },
|
|
87
|
+
},
|
|
88
|
+
// Vivaldi (Windows)
|
|
89
|
+
{
|
|
90
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36 Vivaldi/6.4.3160.49',
|
|
91
|
+
expected: { name: 'Vivaldi', version: '6.4.3160.49' },
|
|
92
|
+
},
|
|
93
|
+
// Brave (Windows)
|
|
94
|
+
{
|
|
95
|
+
ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.110 Safari/537.36 Brave/1.62.153',
|
|
96
|
+
expected: { name: 'Brave', version: '1.62.153' },
|
|
97
|
+
},
|
|
98
|
+
// Unknown
|
|
99
|
+
{
|
|
100
|
+
ua: 'SomeRandomUserAgent/1.0',
|
|
101
|
+
expected: { name: 'SomeRandomUserAgent', version: '1.0' },
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
cases.forEach(({ ua, expected }) => {
|
|
106
|
+
it(`should detect ${expected.name} ${expected.version} from UA: ${ua}`, () => {
|
|
107
|
+
mockUserAgent(ua);
|
|
108
|
+
const result = getBrowser();
|
|
109
|
+
expect(result.name.toLowerCase()).toBe(expected.name.toLowerCase());
|
|
110
|
+
expect(result.version).toContain(expected.version.split('.')[0]); // loose match for version
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -12,52 +12,84 @@ export function getBrowser(): BrowserResult {
|
|
|
12
12
|
version: "unknown"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
15
|
+
const ua = navigator.userAgent;
|
|
16
|
+
// DeviceAtlas authoritative regex order and patterns
|
|
17
|
+
const regexes = [
|
|
18
|
+
// Samsung Internet (Android)
|
|
19
|
+
/(?<name>SamsungBrowser)\/(?<version>\d+(?:\.\d+)+)/,
|
|
20
|
+
// Edge (Chromium, Android, iOS)
|
|
21
|
+
/(?<name>EdgA|EdgiOS|Edg)\/(?<version>\d+(?:\.\d+)+)/,
|
|
22
|
+
// Opera (OPR, OPX, Opera Mini, Opera Mobi)
|
|
23
|
+
/(?<name>OPR|OPX)\/(?<version>\d+(?:\.\d+)+)/,
|
|
24
|
+
/Opera[\s\/](?<version>\d+(?:\.\d+)+)/,
|
|
25
|
+
/Opera Mini\/(?<version>\d+(?:\.\d+)+)/,
|
|
26
|
+
/Opera Mobi\/(?<version>\d+(?:\.\d+)+)/,
|
|
27
|
+
// Vivaldi
|
|
28
|
+
/(?<name>Vivaldi)\/(?<version>\d+(?:\.\d+)+)/,
|
|
29
|
+
// Brave (Brave/1.62.153)
|
|
30
|
+
/(?<name>Brave)\/(?<version>\d+(?:\.\d+)+)/,
|
|
31
|
+
// Chrome iOS (CriOS)
|
|
32
|
+
/(?<name>CriOS)\/(?<version>\d+(?:\.\d+)+)/,
|
|
33
|
+
// Firefox iOS (FxiOS)
|
|
34
|
+
/(?<name>FxiOS)\/(?<version>\d+(?:\.\d+)+)/,
|
|
35
|
+
// Chrome, Chromium (desktop & Android)
|
|
36
|
+
/(?<name>Chrome|Chromium)\/(?<version>\d+(?:\.\d+)+)/,
|
|
37
|
+
// Firefox (desktop & Android)
|
|
38
|
+
/(?<name>Firefox|Waterfox|Iceweasel|IceCat)\/(?<version>\d+(?:\.\d+)+)/,
|
|
39
|
+
// Safari (desktop & iOS): prefer Version/x.y if present, else Safari/x.y
|
|
40
|
+
/Version\/(?<version1>[\d.]+).*Safari\/[\d.]+|(?<name>Safari)\/(?<version2>[\d.]+)/,
|
|
41
|
+
// Internet Explorer, IE Mobile
|
|
42
|
+
/(?<name>MSIE|Trident|IEMobile).+?(?<version>\d+(?:\.\d+)+)/,
|
|
43
|
+
// Other browsers that use the format "BrowserName/version"
|
|
44
|
+
/(?<name>[A-Za-z]+)\/(?<version>\d+(?:\.\d+)+)/,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Map UA tokens to canonical browser names
|
|
48
|
+
const browserNameMap: { [key: string]: string } = {
|
|
49
|
+
'edg': 'Edge',
|
|
50
|
+
'edga': 'Edge',
|
|
51
|
+
'edgios': 'Edge',
|
|
52
|
+
'opr': 'Opera',
|
|
53
|
+
'opx': 'Opera',
|
|
54
|
+
'crios': 'Chrome',
|
|
55
|
+
'fxios': 'Firefox',
|
|
56
|
+
'samsung': 'SamsungBrowser',
|
|
57
|
+
'vivaldi': 'Vivaldi',
|
|
58
|
+
'brave': 'Brave',
|
|
59
|
+
};
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
for (const regex of regexes) {
|
|
62
|
+
const match = ua.match(regex);
|
|
63
|
+
if (match) {
|
|
64
|
+
let name = match.groups?.name;
|
|
65
|
+
let version = match.groups?.version || match.groups?.version1 || match.groups?.version2;
|
|
66
|
+
// For Safari, if Version/x.y matched, set name to Safari
|
|
67
|
+
if (!name && (match.groups?.version1 || match.groups?.version2)) name = 'Safari';
|
|
68
|
+
// Fallbacks for legacy Opera/Opera Mini/Opera Mobi
|
|
69
|
+
if (!name && regex.source.includes('Opera Mini')) name = 'Opera Mini';
|
|
70
|
+
if (!name && regex.source.includes('Opera Mobi')) name = 'Opera Mobi';
|
|
71
|
+
if (!name && regex.source.includes('Opera')) name = 'Opera';
|
|
72
|
+
// Fallback for generic [A-Za-z]+/version
|
|
73
|
+
if (!name && match[1]) name = match[1];
|
|
74
|
+
if (!version && match[2]) version = match[2];
|
|
75
|
+
if (name) {
|
|
76
|
+
const canonical = browserNameMap[name.toLowerCase()] || name;
|
|
77
|
+
return { name: canonical, version: version || 'unknown' };
|
|
54
78
|
}
|
|
55
79
|
}
|
|
56
|
-
|
|
57
|
-
// If no match is found, return unknown
|
|
58
|
-
return {
|
|
59
|
-
name: "unknown",
|
|
60
|
-
version: "unknown"
|
|
61
|
-
};
|
|
62
80
|
}
|
|
81
|
+
return {
|
|
82
|
+
name: "unknown",
|
|
83
|
+
version: "unknown"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Utility function to detect if a user agent is a mobile device (DeviceAtlas-style)
|
|
88
|
+
export function isMobileUserAgent(): boolean {
|
|
89
|
+
if (typeof navigator === 'undefined' || !navigator.userAgent) return false;
|
|
90
|
+
const ua = navigator.userAgent;
|
|
91
|
+
// Exclude iPad from 'mobile' (treat as tablet)
|
|
92
|
+
return /Mobi|Android|iPhone|iPod|IEMobile|Opera Mini|Opera Mobi|webOS|BlackBerry|Windows Phone/i.test(ua)
|
|
93
|
+
&& !/iPad/i.test(ua);
|
|
94
|
+
}
|
|
63
95
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { componentInterface
|
|
2
|
-
import { getBrowser } from './browser'
|
|
1
|
+
import { componentInterface } from '../../factory';
|
|
2
|
+
import { getBrowser, isMobileUserAgent } from './browser'
|
|
3
3
|
|
|
4
4
|
export default function getSystem(): Promise<componentInterface> {
|
|
5
5
|
return new Promise((resolve) => {
|
|
6
6
|
const browser = getBrowser()
|
|
7
|
+
const ua = navigator.userAgent;
|
|
7
8
|
|
|
8
9
|
const result: componentInterface = {
|
|
9
10
|
'platform': window.navigator.platform,
|
|
@@ -12,6 +13,7 @@ export default function getSystem(): Promise<componentInterface> {
|
|
|
12
13
|
'useragent': navigator.userAgent,
|
|
13
14
|
'hardwareConcurrency': navigator.hardwareConcurrency,
|
|
14
15
|
'browser': {'name': browser.name, 'version': browser.version },
|
|
16
|
+
'mobile': isMobileUserAgent(),
|
|
15
17
|
}
|
|
16
18
|
// Safari handles these differently in an iFrame so removing them from components
|
|
17
19
|
if (browser.name.toLowerCase() !== 'safari') {
|