@thumbmarkjs/thumbmarkjs 0.20.5 → 1.0.0-rc.0
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/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +110 -16
- 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/package.json +8 -2
- package/src/components/audio/{audio.ts → index.ts} +11 -8
- package/src/components/canvas/{canvas.ts → index.ts} +12 -8
- package/src/components/fonts/{fonts.ts → index.ts} +10 -5
- package/src/components/hardware/{hardware.ts → index.ts} +3 -5
- package/src/components/locales/{locales.ts → index.ts} +2 -4
- package/src/components/math/index.ts +28 -0
- package/src/components/permissions/{permissions.ts → index.ts} +7 -4
- package/src/components/plugins/{plugins.ts → index.ts} +2 -4
- package/src/components/screen/{screen.ts → index.ts} +2 -4
- package/src/components/system/{system.ts → index.ts} +12 -10
- package/src/components/webgl/{webgl.ts → index.ts} +3 -5
- package/src/factory.ts +40 -29
- package/src/fingerprint/functions.test.ts +12 -7
- package/src/fingerprint/functions.ts +248 -118
- package/src/fingerprint/legacy_functions.ts +54 -0
- package/src/fingerprint/options.ts +19 -8
- package/src/index.ts +12 -3
- package/src/thumbmark.ts +41 -0
- package/src/{declarations.d.ts → types/global.d.ts} +1 -1
- package/src/utils/filterComponents.ts +42 -0
- package/src/utils/raceAll.ts +0 -2
- package/src/components/index.ts +0 -17
- package/src/components/math/math.ts +0 -39
- /package/src/components/canvas/{canvas.test.ts → index.test.ts} +0 -0
|
@@ -2,6 +2,7 @@ import { componentInterface, includeComponent } from '../../factory';
|
|
|
2
2
|
import { getCommonPixels } from '../../utils/commonPixels';
|
|
3
3
|
import { hash } from '../../utils/hash';
|
|
4
4
|
import { getBrowser } from '../system/browser';
|
|
5
|
+
import { optionsInterface } from '../../fingerprint/options';
|
|
5
6
|
|
|
6
7
|
const browser = getBrowser();
|
|
7
8
|
const name = browser.name.toLowerCase();
|
|
@@ -19,7 +20,15 @@ const _RUNS = 3;
|
|
|
19
20
|
const _WIDTH = 280;
|
|
20
21
|
const _HEIGHT = 20;
|
|
21
22
|
|
|
22
|
-
export default function
|
|
23
|
+
export default async function getCanvas(options?: optionsInterface): Promise<componentInterface | null> {
|
|
24
|
+
const browser = getBrowser()
|
|
25
|
+
if (name !== 'firefox' && !(name === 'safari' && majorVer >= 17))
|
|
26
|
+
return generateCanvasFingerprint()
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
export function generateCanvasFingerprint(): Promise<componentInterface> {
|
|
23
32
|
const canvas = document.createElement('canvas');
|
|
24
33
|
const ctx = canvas.getContext('2d');
|
|
25
34
|
|
|
@@ -33,7 +42,7 @@ export default function generateCanvasFingerprint(): Promise<componentInterface>
|
|
|
33
42
|
const commonImageData = getCommonPixels(imageDatas, _WIDTH, _HEIGHT);
|
|
34
43
|
|
|
35
44
|
resolve({
|
|
36
|
-
|
|
45
|
+
commonPixelsHash: hash(commonImageData.data.toString()).toString(),
|
|
37
46
|
});
|
|
38
47
|
});
|
|
39
48
|
}
|
|
@@ -85,9 +94,4 @@ function generateCanvasImageData(): ImageData {
|
|
|
85
94
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
86
95
|
// Return data URL of the canvas
|
|
87
96
|
return imageData;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// In Safari from version 17 in private and normal modes canvas differs
|
|
91
|
-
if (name !== 'firefox' && !(name === 'safari' && majorVer === 17)) {
|
|
92
|
-
includeComponent('canvas', generateCanvasFingerprint);
|
|
93
|
-
}
|
|
97
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory'
|
|
2
2
|
import { ephemeralIFrame } from '../../utils/ephemeralIFrame'
|
|
3
3
|
import { getBrowser } from '../system/browser'
|
|
4
|
+
import { optionsInterface } from '../../fingerprint/options'
|
|
4
5
|
|
|
5
6
|
interface FontMetrics {[k: string]: number}
|
|
6
7
|
|
|
@@ -98,7 +99,14 @@ const availableFonts = [
|
|
|
98
99
|
|
|
99
100
|
const baseFonts = ['monospace', 'sans-serif', 'serif'];
|
|
100
101
|
|
|
101
|
-
export default function
|
|
102
|
+
export default async function getFonts(options?: optionsInterface): Promise<componentInterface | null> {
|
|
103
|
+
const browser = getBrowser()
|
|
104
|
+
if (!['Firefox'].includes(browser.name))
|
|
105
|
+
return getFontMetrics()
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getFontMetrics(): Promise<componentInterface> {
|
|
102
110
|
|
|
103
111
|
return new Promise((resolve, reject) => {
|
|
104
112
|
try {
|
|
@@ -139,7 +147,4 @@ function measureSingleFont(ctx: CanvasRenderingContext2D | null, font: string):
|
|
|
139
147
|
const defaultFont = ctx.font; // Store default font
|
|
140
148
|
ctx.font = `72px ${font}`; // Set a default font size
|
|
141
149
|
return ctx.measureText(text).width;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (getBrowser().name != 'Firefox')
|
|
145
|
-
includeComponent('fonts', getFontMetrics);
|
|
150
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory'
|
|
2
2
|
|
|
3
|
-
function
|
|
3
|
+
export default function getHardware(): Promise<componentInterface> {
|
|
4
4
|
return new Promise((resolve, reject) => {
|
|
5
|
-
const deviceMemory = (navigator.deviceMemory !== undefined) ? navigator.deviceMemory : 0
|
|
5
|
+
const deviceMemory = ((navigator as any).deviceMemory !== undefined) ? (navigator as any).deviceMemory : 0
|
|
6
6
|
const memoryInfo = (window.performance && (window.performance as any).memory ) ? (window.performance as any).memory : 0
|
|
7
7
|
resolve(
|
|
8
8
|
{
|
|
@@ -64,6 +64,4 @@ function getArchitecture(): number {
|
|
|
64
64
|
f[0] = f[0] - f[0];
|
|
65
65
|
|
|
66
66
|
return u8[3];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
includeComponent('hardware', getHardwareInfo);
|
|
67
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory'
|
|
2
2
|
|
|
3
|
-
function getLocales(): Promise<componentInterface> {
|
|
3
|
+
export default function getLocales(): Promise<componentInterface> {
|
|
4
4
|
return new Promise((resolve) => {
|
|
5
5
|
resolve(
|
|
6
6
|
{
|
|
@@ -18,6 +18,4 @@ function getUserLanguage(): string {
|
|
|
18
18
|
|
|
19
19
|
function getUserTimezone(): string {
|
|
20
20
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
includeComponent('locales', getLocales);
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { componentInterface, includeComponent } from '../../factory'
|
|
2
|
+
|
|
3
|
+
const integrate = (f: (x: number) => number, a: number, b: number, n: number): number => {
|
|
4
|
+
const h = (b - a) / n;
|
|
5
|
+
let sum = 0;
|
|
6
|
+
for (let i = 0; i < n; i++) {
|
|
7
|
+
const x = a + (i + 0.5) * h;
|
|
8
|
+
sum += f(x);
|
|
9
|
+
}
|
|
10
|
+
return sum * h;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function getMath(): Promise<componentInterface> {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
resolve(
|
|
16
|
+
{
|
|
17
|
+
'acos': Math.acos(0.5),
|
|
18
|
+
'asin': integrate(Math.asin, -1, 1, 97),
|
|
19
|
+
'cos': integrate(Math.cos, 0, Math.PI, 97),
|
|
20
|
+
'largeCos': Math.cos(1e20),
|
|
21
|
+
'largeSin': Math.sin(1e20),
|
|
22
|
+
'largeTan': Math.tan(1e20),
|
|
23
|
+
'sin': integrate(Math.sin, -Math.PI, Math.PI, 97),
|
|
24
|
+
'tan': integrate(Math.tan, 0, 2 * Math.PI, 97),
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory';
|
|
2
2
|
import { mostFrequentValuesInArrayOfDictionaries } from '../../utils/getMostFrequent';
|
|
3
3
|
import { options } from '../../fingerprint/options';
|
|
4
|
+
import { getBrowser } from '../system/browser';
|
|
4
5
|
|
|
5
6
|
let permission_keys: PermissionName[];
|
|
6
7
|
function initializePermissionKeys() {
|
|
@@ -27,8 +28,12 @@ function initializePermissionKeys() {
|
|
|
27
28
|
] as PermissionName[];
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
export default async function
|
|
31
|
+
export default async function getPermissions(): Promise<componentInterface> {
|
|
31
32
|
initializePermissionKeys();
|
|
33
|
+
const browser = getBrowser();
|
|
34
|
+
if (browser.name.toLowerCase() === 'safari') { // removing from Safari due to iFrame handling
|
|
35
|
+
permission_keys = permission_keys.filter((key) => !['camera', 'geolocation', 'microphone'].includes(key));
|
|
36
|
+
}
|
|
32
37
|
const permissionPromises: Promise<componentInterface>[] = Array.from({length: options?.retries || 3}, () => getBrowserPermissionsOnce() );
|
|
33
38
|
return Promise.all(permissionPromises).then((resolvedPermissions) => {
|
|
34
39
|
const permission = mostFrequentValuesInArrayOfDictionaries(resolvedPermissions, permission_keys);
|
|
@@ -53,6 +58,4 @@ async function getBrowserPermissionsOnce(): Promise<componentInterface> {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
return permissionStatus;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
includeComponent("permissions", getBrowserPermissions);
|
|
61
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory'
|
|
2
2
|
|
|
3
|
-
export default function
|
|
3
|
+
export default function getPlugins(): Promise<componentInterface> {
|
|
4
4
|
const plugins: string[] = [];
|
|
5
5
|
|
|
6
6
|
if (navigator.plugins) {
|
|
@@ -17,6 +17,4 @@ export default function getInstalledPlugins(): Promise<componentInterface> {
|
|
|
17
17
|
}
|
|
18
18
|
);
|
|
19
19
|
});
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
includeComponent('plugins', getInstalledPlugins);
|
|
20
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory';
|
|
2
2
|
|
|
3
|
-
function
|
|
3
|
+
export default function getScreen(): Promise<componentInterface> {
|
|
4
4
|
return new Promise((resolve) => {
|
|
5
5
|
resolve(
|
|
6
6
|
{
|
|
@@ -41,6 +41,4 @@ function matchMedias(): string[] {
|
|
|
41
41
|
})
|
|
42
42
|
});
|
|
43
43
|
return results;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
includeComponent('screen', screenDetails);
|
|
44
|
+
}
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { componentInterface, includeComponent } from '../../factory';
|
|
2
2
|
import { getBrowser } from './browser'
|
|
3
3
|
|
|
4
|
-
function
|
|
4
|
+
export default function getSystem(): Promise<componentInterface> {
|
|
5
5
|
return new Promise((resolve) => {
|
|
6
6
|
const browser = getBrowser()
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
const result: componentInterface = {
|
|
8
9
|
'platform': window.navigator.platform,
|
|
9
|
-
'cookieEnabled': window.navigator.cookieEnabled,
|
|
10
10
|
'productSub': navigator.productSub,
|
|
11
11
|
'product': navigator.product,
|
|
12
12
|
'useragent': navigator.userAgent,
|
|
13
13
|
'hardwareConcurrency': navigator.hardwareConcurrency,
|
|
14
14
|
'browser': {'name': browser.name, 'version': browser.version },
|
|
15
|
-
|
|
15
|
+
}
|
|
16
|
+
// Safari handles these differently in an iFrame so removing them from components
|
|
17
|
+
if (browser.name.toLowerCase() !== 'safari') {
|
|
18
|
+
result['applePayVersion'] = getApplePayVersion();
|
|
19
|
+
result['cookieEnabled'] = window.navigator.cookieEnabled;
|
|
20
|
+
}
|
|
21
|
+
resolve(result);
|
|
16
22
|
});
|
|
17
|
-
}
|
|
18
|
-
}
|
|
23
|
+
};
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* @returns applePayCanMakePayments: boolean, applePayMaxSupportedVersion: number
|
|
@@ -34,7 +39,4 @@ function getApplePayVersion(): number {
|
|
|
34
39
|
}
|
|
35
40
|
}
|
|
36
41
|
return 0
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
includeComponent('system', getSystemDetails);
|
|
40
|
-
|
|
42
|
+
}
|
|
@@ -16,7 +16,7 @@ function initializeCanvasAndWebGL() {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
async function
|
|
19
|
+
export default async function getWebGL(): Promise<componentInterface> {
|
|
20
20
|
initializeCanvasAndWebGL();
|
|
21
21
|
|
|
22
22
|
try {
|
|
@@ -32,7 +32,7 @@ async function createWebGLFingerprint(): Promise<componentInterface> {
|
|
|
32
32
|
//const imageData = createWebGLImageData()
|
|
33
33
|
|
|
34
34
|
return {
|
|
35
|
-
'
|
|
35
|
+
'commonPixelsHash': hash(commonImageData.data.toString()).toString(),
|
|
36
36
|
}
|
|
37
37
|
} catch (error) {
|
|
38
38
|
return {
|
|
@@ -143,6 +143,4 @@ function createWebGLImageData(): ImageData {
|
|
|
143
143
|
gl.clearColor(0.0, 0.0, 0.0, 0.0);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
includeComponent('webgl', createWebGLFingerprint);
|
|
146
|
+
}
|
package/src/factory.ts
CHANGED
|
@@ -4,21 +4,53 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { optionsInterface } from './fingerprint/options';
|
|
8
|
+
//import { getComponentPromises } from './fingerprint/tm_functions';
|
|
9
|
+
|
|
10
|
+
// Import all built-in component functions
|
|
11
|
+
import getAudio from "./components/audio";
|
|
12
|
+
import getCanvas from "./components/canvas";
|
|
13
|
+
import getFonts from "./components/fonts";
|
|
14
|
+
import getHardware from "./components/hardware";
|
|
15
|
+
import getLocales from "./components/locales";
|
|
16
|
+
import getMath from "./components/math";
|
|
17
|
+
import getPermissions from "./components/permissions";
|
|
18
|
+
import getPlugins from "./components/plugins";
|
|
19
|
+
import getScreen from "./components/screen";
|
|
20
|
+
import getSystem from "./components/system";
|
|
21
|
+
import getWebGL from "./components/webgl";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @description key->function map of built-in components. Do not call the function here.
|
|
25
|
+
*/
|
|
26
|
+
export const tm_component_promises = {
|
|
27
|
+
'audio': getAudio,
|
|
28
|
+
'canvas': getCanvas,
|
|
29
|
+
'fonts': getFonts,
|
|
30
|
+
'hardware': getHardware,
|
|
31
|
+
'locales': getLocales,
|
|
32
|
+
'math': getMath,
|
|
33
|
+
'permissions': getPermissions,
|
|
34
|
+
'plugins': getPlugins,
|
|
35
|
+
'screen': getScreen,
|
|
36
|
+
'system': getSystem,
|
|
37
|
+
'webgl': getWebGL
|
|
38
|
+
};
|
|
8
39
|
|
|
9
40
|
// the component interface is the form of the JSON object the function's promise must return
|
|
10
41
|
export interface componentInterface {
|
|
11
42
|
[key: string]: string | string[] | number | boolean | componentInterface;
|
|
12
|
-
}
|
|
43
|
+
};
|
|
13
44
|
|
|
14
45
|
|
|
15
46
|
// The component function's interface is simply the promise of the above
|
|
16
47
|
export interface componentFunctionInterface {
|
|
17
|
-
(): Promise<componentInterface>;
|
|
48
|
+
(options?: optionsInterface): Promise<componentInterface | null>;
|
|
18
49
|
}
|
|
19
50
|
|
|
20
51
|
// components include a dictionary of name: function.
|
|
21
|
-
|
|
52
|
+
// Renamed to customComponents for clarity; this is for user-registered components.
|
|
53
|
+
export const customComponents: {[name: string]: componentFunctionInterface | null} = {};
|
|
22
54
|
|
|
23
55
|
//In case a promise time-outs, this is what we use as the value in place
|
|
24
56
|
export const timeoutInstance: componentInterface = {
|
|
@@ -30,29 +62,8 @@ export const timeoutInstance: componentInterface = {
|
|
|
30
62
|
* in the fingerprint.
|
|
31
63
|
* @param {string} name - the name identifier of the component
|
|
32
64
|
* @param {componentFunctionInterface} creationFunction - the function that implements the component
|
|
33
|
-
* @returns
|
|
65
|
+
* @returns nothing
|
|
34
66
|
*/
|
|
35
|
-
export const includeComponent = (name:string, creationFunction: componentFunctionInterface) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The function turns the map of component functions to a map of Promises when called
|
|
42
|
-
* @returns {[name: string]: <Promise>componentInterface}
|
|
43
|
-
*/
|
|
44
|
-
export const getComponentPromises = () => {
|
|
45
|
-
return Object.fromEntries(
|
|
46
|
-
Object.entries(components)
|
|
47
|
-
.filter(([key]) => {
|
|
48
|
-
return !options?.exclude?.includes(key)}
|
|
49
|
-
)
|
|
50
|
-
.filter(([key]) => {
|
|
51
|
-
return options?.include?.some(e => e.includes('.'))
|
|
52
|
-
? options?.include?.some(e => e.startsWith(key))
|
|
53
|
-
: options?.include?.length === 0 || options?.include?.includes(key)
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
.map(([key, value]) => [key, value()])
|
|
57
|
-
);
|
|
58
|
-
}
|
|
67
|
+
export const includeComponent = (name:string, creationFunction: componentFunctionInterface, options?: optionsInterface) => {
|
|
68
|
+
customComponents[name] = creationFunction;
|
|
69
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {componentInterface} from '../factory'
|
|
2
|
-
import {
|
|
1
|
+
import { componentInterface } from '../factory'
|
|
2
|
+
import { filterThumbmarkData } from '../utils/filterComponents'
|
|
3
|
+
import { defaultOptions } from './options';
|
|
3
4
|
|
|
4
5
|
const test_components: componentInterface = {
|
|
5
6
|
'one': '1',
|
|
@@ -9,26 +10,30 @@ const test_components: componentInterface = {
|
|
|
9
10
|
|
|
10
11
|
describe('component filtering tests', () => {
|
|
11
12
|
test("excluding top level works", () => {
|
|
12
|
-
expect(
|
|
13
|
+
expect(filterThumbmarkData(test_components,
|
|
14
|
+
{ ...defaultOptions, ...{ exclude: ['one']} }
|
|
15
|
+
)).toMatchObject({
|
|
13
16
|
'two': 2, 'three': {'a': true, 'b': false}
|
|
14
17
|
})
|
|
15
18
|
});
|
|
16
19
|
test("including top level works", () => {
|
|
17
|
-
expect(
|
|
20
|
+
expect(filterThumbmarkData(test_components, { ...defaultOptions, ...{ include: ['one', 'two']} })).toMatchObject({
|
|
18
21
|
'one': '1', 'two': 2
|
|
19
22
|
})
|
|
20
23
|
});
|
|
21
24
|
test("excluding low-level works", () => {
|
|
22
|
-
expect(
|
|
25
|
+
expect(filterThumbmarkData(test_components,
|
|
26
|
+
{ ...defaultOptions, ...{ exclude: ['two', 'three.a']} }
|
|
27
|
+
)).toMatchObject({
|
|
23
28
|
'one': '1',
|
|
24
29
|
'three': {'b': false}
|
|
25
30
|
})
|
|
26
31
|
});
|
|
27
32
|
test("including low-level works", () => {
|
|
28
|
-
expect(
|
|
33
|
+
expect(filterThumbmarkData(test_components,
|
|
34
|
+
{ ...defaultOptions, ...{ include: ['one', 'three.b']} })).toMatchObject({
|
|
29
35
|
'one': '1',
|
|
30
36
|
'three': {'b': false}
|
|
31
37
|
})
|
|
32
38
|
});
|
|
33
|
-
|
|
34
39
|
});
|