@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.
Files changed (50) hide show
  1. package/README.md +9 -10
  2. package/dist/thumbmark.cjs.js +1 -1
  3. package/dist/thumbmark.cjs.js.map +1 -1
  4. package/dist/thumbmark.esm.d.ts +5 -5
  5. package/dist/thumbmark.esm.js +1 -1
  6. package/dist/thumbmark.esm.js.map +1 -1
  7. package/dist/thumbmark.umd.js +1 -1
  8. package/dist/thumbmark.umd.js.map +1 -1
  9. package/dist/types/components/audio/index.d.ts +2 -0
  10. package/dist/types/components/canvas/index.d.ts +3 -0
  11. package/dist/types/components/fonts/index.d.ts +4 -0
  12. package/dist/types/components/hardware/index.d.ts +2 -0
  13. package/dist/types/components/locales/index.d.ts +2 -0
  14. package/dist/types/components/math/index.d.ts +2 -0
  15. package/dist/types/components/permissions/index.d.ts +3 -0
  16. package/dist/types/components/plugins/index.d.ts +2 -0
  17. package/dist/types/components/screen/index.d.ts +2 -0
  18. package/dist/types/components/system/browser.d.ts +7 -0
  19. package/dist/types/components/system/index.d.ts +2 -0
  20. package/dist/types/components/webgl/index.d.ts +2 -0
  21. package/dist/types/factory.d.ts +51 -0
  22. package/dist/types/functions/filterComponents.d.ts +11 -0
  23. package/dist/types/functions/index.d.ts +87 -0
  24. package/dist/types/functions/legacy_functions.d.ts +27 -0
  25. package/dist/types/index.d.ts +7 -0
  26. package/dist/types/options.d.ts +28 -0
  27. package/dist/types/thumbmark.d.ts +26 -0
  28. package/dist/types/utils/commonPixels.d.ts +1 -0
  29. package/dist/types/utils/ephemeralIFrame.d.ts +4 -0
  30. package/dist/types/utils/getMostFrequent.d.ts +5 -0
  31. package/dist/types/utils/hash.d.ts +5 -0
  32. package/dist/types/utils/imageDataToDataURL.d.ts +1 -0
  33. package/dist/types/utils/log.d.ts +8 -0
  34. package/dist/types/utils/raceAll.d.ts +9 -0
  35. package/dist/types/utils/version.d.ts +4 -0
  36. package/package.json +1 -1
  37. package/src/components/audio/index.ts +1 -2
  38. package/src/components/canvas/index.ts +6 -9
  39. package/src/components/plugins/index.ts +26 -17
  40. package/src/components/screen/index.ts +19 -8
  41. package/src/components/system/browser.test.ts +113 -0
  42. package/src/components/system/browser.ts +76 -44
  43. package/src/components/system/index.ts +4 -2
  44. package/src/functions/functions.test.ts +1 -1
  45. package/src/functions/index.ts +18 -55
  46. package/src/index.ts +2 -1
  47. package/src/options.ts +4 -2
  48. package/src/thumbmark.ts +2 -1
  49. package/src/utils/log.ts +34 -0
  50. 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,4 @@
1
+ export declare function ephemeralIFrame(callback: ({ iframe }: {
2
+ iframe: Document;
3
+ }) => void): Promise<any>;
4
+ export declare function wait<T = void>(durationMs: number, resolveWith?: T): Promise<T>;
@@ -0,0 +1,5 @@
1
+ export declare function mostFrequentValuesInArrayOfDictionaries(arr: {
2
+ [key: string]: any;
3
+ }[], keys: string[]): {
4
+ [key: string]: any;
5
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This code is taken from https://github.com/LinusU/murmur-128/blob/master/index.js
3
+ * But instead of dependencies to encode-utf8 and fmix, I've implemented them here.
4
+ */
5
+ export declare function hash(key: ArrayBuffer | string, seed?: number): string;
@@ -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 {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns the current package version
3
+ */
4
+ export declare function getVersion(): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thumbmarkjs/thumbmarkjs",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.1.0-rc.1",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "./dist/thumbmark.cjs.js",
@@ -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(options?: optionsInterface): Promise<componentInterface | null> {
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(options?: optionsInterface): Promise<componentInterface | null> {
24
- const browser = getBrowser()
25
- if (name !== 'firefox' && !(name === 'safari' && majorVer >= 17))
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
- const plugins: string[] = [];
5
-
6
- if (navigator.plugins) {
7
- for (let i = 0; i < navigator.plugins.length; i++) {
8
- const plugin = navigator.plugins[i];
9
- plugins.push([plugin.name, plugin.filename, plugin.description ].join("|"));
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
- return new Promise((resolve) => {
14
- resolve(
15
- {
16
- 'plugins': plugins
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
- resolve(
6
- {
7
- 'is_touchscreen': navigator.maxTouchPoints > 0,
8
- 'maxTouchPoints': navigator.maxTouchPoints,
9
- 'colorDepth': screen.colorDepth,
10
- 'mediaMatches': matchMedias(),
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
- const ua = navigator.userAgent
16
- // Define some regular expressions to match different browsers and their versions
17
- const regexes = [
18
- // Samsung internet browser
19
- /(?<name>SamsungBrowser)\/(?<version>\d+(?:\.\d+)?)/,
20
- // Edge
21
- /(?<name>Edge|Edg)\/(?<version>\d+(?:\.\d+)?)/,
22
- // Chrome, Chromium, Opera, Vivaldi, Brave, etc.
23
- /(?<name>(?:Chrome|Chromium|OPR|Opera|Vivaldi|Brave))\/(?<version>\d+(?:\.\d+)?)/,
24
- // Firefox, Waterfox, etc.
25
- /(?<name>(?:Firefox|Waterfox|Iceweasel|IceCat))\/(?<version>\d+(?:\.\d+)?)/,
26
- // Safari, Mobile Safari, etc.
27
- /(?<name>Safari)\/(?<version>\d+(?:\.\d+)?)/,
28
- // Internet Explorer, IE Mobile, etc.
29
- /(?<name>MSIE|Trident|IEMobile).+?(?<version>\d+(?:\.\d+)?)/,
30
- // Samsung browser (Tizen format)
31
- /(?<name>samsung).*Version\/(?<version>\d+(?:\.\d+)?)/i,
32
- // Other browsers that use the format "BrowserName/version"
33
- /(?<name>[A-Za-z]+)\/(?<version>\d+(?:\.\d+)?)/,
34
- ];
35
-
36
- // Define a map for browser name translations
37
- const browserNameMap: { [key: string]: string } = {
38
- 'edg': 'Edge',
39
- 'opr': 'Opera',
40
- 'samsung': 'SamsungBrowser'
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
- // Loop through the regexes and try to find a match
44
- for (const regex of regexes) {
45
- const match = ua.match(regex);
46
- if (match && match.groups) {
47
- // Translate the browser name if necessary
48
- const name = browserNameMap[match.groups.name.toLowerCase()] || match.groups.name;
49
- // Return the browser name and version
50
- return {
51
- name: name,
52
- version: match.groups.version
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, includeComponent } from '../../factory';
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') {
@@ -1,5 +1,5 @@
1
1
  import { componentInterface } from '../factory'
2
- import { filterThumbmarkData } from '../utils/filterComponents'
2
+ import { filterThumbmarkData } from './filterComponents'
3
3
  import { defaultOptions } from '../options';
4
4
 
5
5
  const test_components: componentInterface = {