@thumbmarkjs/thumbmarkjs 0.19.0 → 0.20.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.
@@ -0,0 +1,118 @@
1
+ import {componentInterface, getComponentPromises, timeoutInstance} from '../factory'
2
+ import {hash} from '../utils/hash'
3
+ import {raceAll, raceAllPerformance} from '../utils/raceAll'
4
+ import {options} from './options'
5
+ import * as packageJson from '../../package.json'
6
+
7
+ export function getVersion(): string {
8
+ return packageJson.version
9
+ }
10
+
11
+ export async function getFingerprintData(): Promise<componentInterface> {
12
+ try {
13
+ const promiseMap: Record<string, Promise<componentInterface>> = getComponentPromises()
14
+ const keys: string[] = Object.keys(promiseMap)
15
+ const promises: Promise<componentInterface>[] = Object.values(promiseMap)
16
+ const resolvedValues: (componentInterface | undefined)[] = await raceAll(promises, options?.timeout || 1000, timeoutInstance);
17
+ const validValues: componentInterface[] = resolvedValues.filter((value): value is componentInterface => value !== undefined);
18
+ const resolvedComponents: Record<string, componentInterface> = {};
19
+ validValues.forEach((value, index) => {
20
+ resolvedComponents[keys[index]] = value
21
+ })
22
+ return filterFingerprintData(resolvedComponents, options.exclude || [], options.include || [], "")
23
+ }
24
+ catch (error) {
25
+ throw error
26
+ }
27
+ }
28
+
29
+ /**
30
+ * This function filters the fingerprint data based on the exclude and include list
31
+ * @param {componentInterface} obj - components objects from main componentInterface
32
+ * @param {string[]} excludeList - elements to exclude from components objects (e.g : 'canvas', 'system.browser')
33
+ * @param {string[]} includeList - elements to only include from components objects (e.g : 'canvas', 'system.browser')
34
+ * @param {string} path - auto-increment path iterating on key objects from components objects
35
+ * @returns {componentInterface} result - returns the final object before hashing in order to get fingerprint
36
+ */
37
+ export function filterFingerprintData(obj: componentInterface, excludeList: string[], includeList: string[], path: string = ""): componentInterface {
38
+ const result: componentInterface = {};
39
+
40
+ for (const [key, value] of Object.entries(obj)) {
41
+ const currentPath = path + key + ".";
42
+
43
+ if (typeof value === "object" && !Array.isArray(value)) {
44
+ const filtered = filterFingerprintData(value, excludeList, includeList, currentPath);
45
+ if (Object.keys(filtered).length > 0) {
46
+ result[key] = filtered;
47
+ }
48
+ } else {
49
+ const isExcluded = excludeList.some(exclusion => currentPath.startsWith(exclusion));
50
+ const isIncluded = includeList.some(inclusion => currentPath.startsWith(inclusion));
51
+
52
+ if (!isExcluded || isIncluded) {
53
+ result[key] = value;
54
+ }
55
+ }
56
+ }
57
+
58
+ return result;
59
+ }
60
+
61
+ export async function getFingerprint(includeData?: false): Promise<string>
62
+ export async function getFingerprint(includeData: true): Promise<{ hash: string, data: componentInterface }>
63
+ export async function getFingerprint(includeData?: boolean): Promise<string | { hash: string, data: componentInterface }> {
64
+ try {
65
+ const fingerprintData = await getFingerprintData()
66
+ const thisHash = hash(JSON.stringify(fingerprintData))
67
+ if (Math.random() < 0.001 && options.logging) logFingerprintData(thisHash, fingerprintData)
68
+ if (includeData) {
69
+ return { hash: thisHash.toString(), data: fingerprintData}
70
+ } else {
71
+ return thisHash.toString()
72
+ }
73
+ } catch (error) {
74
+ throw error
75
+ }
76
+ }
77
+
78
+ export async function getFingerprintPerformance() {
79
+ try {
80
+ const promiseMap = getComponentPromises()
81
+ const keys = Object.keys(promiseMap)
82
+ const promises = Object.values(promiseMap)
83
+ const resolvedValues = await raceAllPerformance(promises, options?.timeout || 1000, timeoutInstance )
84
+ const resolvedComponents: { [key: string]: any } = {
85
+ elapsed: {}
86
+ }
87
+ resolvedValues.forEach((value, index) => {
88
+ resolvedComponents[keys[index]] = value.value
89
+ resolvedComponents["elapsed"][keys[index]] = value.elapsed
90
+ });
91
+ return resolvedComponents
92
+ }
93
+ catch (error) {
94
+ throw error
95
+ }
96
+ }
97
+
98
+ // Function to log the fingerprint data
99
+ async function logFingerprintData(thisHash: string, fingerprintData: componentInterface) {
100
+ const url = 'https://logging.thumbmarkjs.com/v1/log'
101
+ const payload = {
102
+ thumbmark: thisHash,
103
+ components: fingerprintData,
104
+ version: getVersion()
105
+ };
106
+ if (!sessionStorage.getItem("_tmjs_l")) {
107
+ sessionStorage.setItem("_tmjs_l", "1")
108
+ try {
109
+ await fetch(url, {
110
+ method: 'POST',
111
+ headers: {
112
+ 'Content-Type': 'application/json'
113
+ },
114
+ body: JSON.stringify(payload)
115
+ });
116
+ } catch { } // do nothing
117
+ }
118
+ }
@@ -0,0 +1,26 @@
1
+ export interface optionsInterface {
2
+ exclude: string[],
3
+ include: string[],
4
+ webgl_runs?: number,
5
+ canvas_runs?: number,
6
+ permissions_to_check?: PermissionName[], // new option
7
+ retries?: number, // new option
8
+ timeout?: number, // new option
9
+ logging: boolean
10
+ }
11
+
12
+ export let options: optionsInterface = {
13
+ exclude: [],
14
+ include: [],
15
+ logging: true,
16
+ }
17
+
18
+ export function setOption<K extends keyof optionsInterface>(key: K, value: optionsInterface[K]) {
19
+ if (!['include', 'exclude', 'permissions_to_check', 'retries', 'timeout', 'logging'].includes(key))
20
+ throw new Error('Unknown option ' + key)
21
+ if (['include', 'exclude', 'permissions_to_check'].includes(key) && !(Array.isArray(value) && value.every(item => typeof item === 'string')) )
22
+ throw new Error('The value of the include, exclude and permissions_to_check must be an array of strings');
23
+ if ([ 'retries', 'timeout'].includes(key) && typeof value !== 'number')
24
+ throw new Error('The value of retries must be a number');
25
+ options[key] = value;
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { getFingerprint, getFingerprintData, getFingerprintPerformance, getVersion } from './fingerprint/functions'
2
+ import { setOption } from './fingerprint/options'
3
+ import { includeComponent } from './factory'
4
+ import './components'
5
+
6
+ export { setOption, getVersion, getFingerprint, getFingerprintData, getFingerprintPerformance, includeComponent }
@@ -0,0 +1,38 @@
1
+ export function getCommonPixels(images: ImageData[], width: number, height: number ): ImageData {
2
+ let finalData: number[] = [];
3
+ for (let i = 0; i < images[0].data.length; i++) {
4
+ let indice: number[] = [];
5
+ for (let u = 0; u < images.length; u++) {
6
+ indice.push(images[u].data[i]);
7
+ }
8
+ finalData.push(getMostFrequent(indice));
9
+ }
10
+
11
+ const pixelData = finalData;
12
+ const pixelArray = new Uint8ClampedArray(pixelData);
13
+ return new ImageData(pixelArray, width, height);
14
+ }
15
+
16
+ function getMostFrequent(arr: number[]): number {
17
+ if (arr.length === 0) {
18
+ return 0; // Handle empty array case
19
+ }
20
+
21
+ const frequencyMap: { [key: number]: number } = {};
22
+
23
+ // Count occurrences of each number in the array
24
+ for (const num of arr) {
25
+ frequencyMap[num] = (frequencyMap[num] || 0) + 1;
26
+ }
27
+
28
+ let mostFrequent: number = arr[0];
29
+
30
+ // Find the number with the highest frequency
31
+ for (const num in frequencyMap) {
32
+ if (frequencyMap[num] > frequencyMap[mostFrequent]) {
33
+ mostFrequent = parseInt(num, 10);
34
+ }
35
+ }
36
+
37
+ return mostFrequent;
38
+ }
@@ -0,0 +1,35 @@
1
+ export async function ephemeralIFrame(callback: ({ iframe }: { iframe: Document }) => void): Promise<any> {
2
+
3
+ while (!document.body) {
4
+ await wait(50)
5
+ }
6
+ const iframe = document.createElement('iframe')
7
+ iframe.setAttribute('frameBorder', '0')
8
+
9
+ const style = iframe.style
10
+ style.setProperty('position','fixed');
11
+ style.setProperty('display', 'block', 'important')
12
+ style.setProperty('visibility', 'visible')
13
+ style.setProperty('border', '0');
14
+ style.setProperty('opacity','0');
15
+
16
+ iframe.src = 'about:blank'
17
+ document.body.appendChild(iframe)
18
+
19
+ const iframeDocument = iframe.contentDocument || iframe.contentWindow?.document;
20
+ if (!iframeDocument) {
21
+ throw new Error('Iframe document is not accessible');
22
+ }
23
+
24
+ // Execute the callback function with access to the iframe's document
25
+ callback({ iframe: iframeDocument });
26
+
27
+ // Clean up after running the callback
28
+ setTimeout(() => {
29
+ document.body.removeChild(iframe);
30
+ }, 0);
31
+ }
32
+
33
+ export function wait<T = void>(durationMs: number, resolveWith?: T): Promise<T> {
34
+ return new Promise((resolve) => setTimeout(resolve, durationMs, resolveWith))
35
+ }
@@ -0,0 +1,39 @@
1
+ function mostFrequentValue(arr: any[]): any | null {
2
+ if (arr.length === 0) {
3
+ return null; // Return null for an empty array
4
+ }
5
+
6
+ const frequencyMap: { [key: string]: number } = {};
7
+
8
+ // Count occurrences of each element in the array
9
+ arr.forEach((element) => {
10
+ const key = String(element);
11
+ frequencyMap[key] = (frequencyMap[key] || 0) + 1;
12
+ });
13
+
14
+ let mostFrequent: any = arr[0]; // Assume the first element is the most frequent
15
+ let highestFrequency = 1; // Frequency of the assumed most frequent element
16
+
17
+ // Find the element with the highest frequency
18
+ Object.keys(frequencyMap).forEach((key) => {
19
+ if (frequencyMap[key] > highestFrequency) {
20
+ mostFrequent = key;
21
+ highestFrequency = frequencyMap[key];
22
+ }
23
+ });
24
+
25
+ return mostFrequent;
26
+ }
27
+
28
+ export function mostFrequentValuesInArrayOfDictionaries(arr: { [key: string]: any }[], keys: string[]): { [key: string]: any } {
29
+ const result: { [key: string]: any } = {};
30
+
31
+ keys.forEach((key) => {
32
+ const valuesForKey = arr.map((obj) => (key in obj ? obj[key] : undefined)).filter((val) => val !== undefined);
33
+ const mostFrequentValueForKey = mostFrequentValue(valuesForKey);
34
+ if (mostFrequentValueForKey)
35
+ result[key] = mostFrequentValueForKey;
36
+ });
37
+
38
+ return result;
39
+ }
@@ -0,0 +1,198 @@
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
+
6
+ function encodeUtf8(text: string): ArrayBuffer {
7
+ const encoder = new TextEncoder();
8
+ return encoder.encode(text).buffer;
9
+ }
10
+
11
+ function fmix (input : number) : number {
12
+ input ^= (input >>> 16)
13
+ input = Math.imul(input, 0x85ebca6b)
14
+ input ^= (input >>> 13)
15
+ input = Math.imul(input, 0xc2b2ae35)
16
+ input ^= (input >>> 16)
17
+
18
+ return (input >>> 0)
19
+ }
20
+
21
+ const C = new Uint32Array([
22
+ 0x239b961b,
23
+ 0xab0e9789,
24
+ 0x38b34ae5,
25
+ 0xa1e38b93
26
+ ])
27
+
28
+ function rotl (m : number, n : number) : number {
29
+ return (m << n) | (m >>> (32 - n))
30
+ }
31
+
32
+ function body (key : ArrayBuffer, hash : Uint32Array) {
33
+ const blocks = (key.byteLength / 16) | 0
34
+ const view32 = new Uint32Array(key, 0, blocks * 4)
35
+
36
+ for (let i = 0; i < blocks; i++) {
37
+ const k = view32.subarray(i * 4, (i + 1) * 4)
38
+
39
+ k[0] = Math.imul(k[0], C[0])
40
+ k[0] = rotl(k[0], 15)
41
+ k[0] = Math.imul(k[0], C[1])
42
+
43
+ hash[0] = (hash[0] ^ k[0])
44
+ hash[0] = rotl(hash[0], 19)
45
+ hash[0] = (hash[0] + hash[1])
46
+ hash[0] = Math.imul(hash[0], 5) + 0x561ccd1b
47
+
48
+ k[1] = Math.imul(k[1], C[1])
49
+ k[1] = rotl(k[1], 16)
50
+ k[1] = Math.imul(k[1], C[2])
51
+
52
+ hash[1] = (hash[1] ^ k[1])
53
+ hash[1] = rotl(hash[1], 17)
54
+ hash[1] = (hash[1] + hash[2])
55
+ hash[1] = Math.imul(hash[1], 5) + 0x0bcaa747
56
+
57
+ k[2] = Math.imul(k[2], C[2])
58
+ k[2] = rotl(k[2], 17)
59
+ k[2] = Math.imul(k[2], C[3])
60
+
61
+ hash[2] = (hash[2] ^ k[2])
62
+ hash[2] = rotl(hash[2], 15)
63
+ hash[2] = (hash[2] + hash[3])
64
+ hash[2] = Math.imul(hash[2], 5) + 0x96cd1c35
65
+
66
+ k[3] = Math.imul(k[3], C[3])
67
+ k[3] = rotl(k[3], 18)
68
+ k[3] = Math.imul(k[3], C[0])
69
+
70
+ hash[3] = (hash[3] ^ k[3])
71
+ hash[3] = rotl(hash[3], 13)
72
+ hash[3] = (hash[3] + hash[0])
73
+ hash[3] = Math.imul(hash[3], 5) + 0x32ac3b17
74
+ }
75
+ }
76
+
77
+ function tail (key : ArrayBuffer, hash : Uint32Array) {
78
+ const blocks = (key.byteLength / 16) | 0
79
+ const reminder = (key.byteLength % 16)
80
+
81
+ const k = new Uint32Array(4)
82
+ const tail = new Uint8Array(key, blocks * 16, reminder)
83
+
84
+ switch (reminder) {
85
+ case 15:
86
+ k[3] = (k[3] ^ (tail[14] << 16))
87
+ // fallthrough
88
+ case 14:
89
+ k[3] = (k[3] ^ (tail[13] << 8))
90
+ // fallthrough
91
+ case 13:
92
+ k[3] = (k[3] ^ (tail[12] << 0))
93
+
94
+ k[3] = Math.imul(k[3], C[3])
95
+ k[3] = rotl(k[3], 18)
96
+ k[3] = Math.imul(k[3], C[0])
97
+ hash[3] = (hash[3] ^ k[3])
98
+ // fallthrough
99
+ case 12:
100
+ k[2] = (k[2] ^ (tail[11] << 24))
101
+ // fallthrough
102
+ case 11:
103
+ k[2] = (k[2] ^ (tail[10] << 16))
104
+ // fallthrough
105
+ case 10:
106
+ k[2] = (k[2] ^ (tail[9] << 8))
107
+ // fallthrough
108
+ case 9:
109
+ k[2] = (k[2] ^ (tail[8] << 0))
110
+
111
+ k[2] = Math.imul(k[2], C[2])
112
+ k[2] = rotl(k[2], 17)
113
+ k[2] = Math.imul(k[2], C[3])
114
+ hash[2] = (hash[2] ^ k[2])
115
+ // fallthrough
116
+ case 8:
117
+ k[1] = (k[1] ^ (tail[7] << 24))
118
+ // fallthrough
119
+ case 7:
120
+ k[1] = (k[1] ^ (tail[6] << 16))
121
+ // fallthrough
122
+ case 6:
123
+ k[1] = (k[1] ^ (tail[5] << 8))
124
+ // fallthrough
125
+ case 5:
126
+ k[1] = (k[1] ^ (tail[4] << 0))
127
+
128
+ k[1] = Math.imul(k[1], C[1])
129
+ k[1] = rotl(k[1], 16)
130
+ k[1] = Math.imul(k[1], C[2])
131
+ hash[1] = (hash[1] ^ k[1])
132
+ // fallthrough
133
+ case 4:
134
+ k[0] = (k[0] ^ (tail[3] << 24))
135
+ // fallthrough
136
+ case 3:
137
+ k[0] = (k[0] ^ (tail[2] << 16))
138
+ // fallthrough
139
+ case 2:
140
+ k[0] = (k[0] ^ (tail[1] << 8))
141
+ // fallthrough
142
+ case 1:
143
+ k[0] = (k[0] ^ (tail[0] << 0))
144
+
145
+ k[0] = Math.imul(k[0], C[0])
146
+ k[0] = rotl(k[0], 15)
147
+ k[0] = Math.imul(k[0], C[1])
148
+ hash[0] = (hash[0] ^ k[0])
149
+ }
150
+ }
151
+
152
+ function finalize (key : ArrayBuffer, hash : Uint32Array) {
153
+ hash[0] = (hash[0] ^ key.byteLength)
154
+ hash[1] = (hash[1] ^ key.byteLength)
155
+ hash[2] = (hash[2] ^ key.byteLength)
156
+ hash[3] = (hash[3] ^ key.byteLength)
157
+
158
+ hash[0] = (hash[0] + hash[1]) | 0
159
+ hash[0] = (hash[0] + hash[2]) | 0
160
+ hash[0] = (hash[0] + hash[3]) | 0
161
+
162
+ hash[1] = (hash[1] + hash[0]) | 0
163
+ hash[2] = (hash[2] + hash[0]) | 0
164
+ hash[3] = (hash[3] + hash[0]) | 0
165
+
166
+ hash[0] = fmix(hash[0])
167
+ hash[1] = fmix(hash[1])
168
+ hash[2] = fmix(hash[2])
169
+ hash[3] = fmix(hash[3])
170
+
171
+ hash[0] = (hash[0] + hash[1]) | 0
172
+ hash[0] = (hash[0] + hash[2]) | 0
173
+ hash[0] = (hash[0] + hash[3]) | 0
174
+
175
+ hash[1] = (hash[1] + hash[0]) | 0
176
+ hash[2] = (hash[2] + hash[0]) | 0
177
+ hash[3] = (hash[3] + hash[0]) | 0
178
+ }
179
+
180
+ export function hash (key : ArrayBuffer | string, seed = 0) : string {
181
+ seed = (seed ? (seed | 0) : 0)
182
+
183
+ if (typeof key === 'string') {
184
+ key = encodeUtf8(key)
185
+ }
186
+
187
+ if (!(key instanceof ArrayBuffer)) {
188
+ throw new TypeError('Expected key to be ArrayBuffer or string')
189
+ }
190
+
191
+ const hash = new Uint32Array([seed, seed, seed, seed])
192
+
193
+ body(key, hash)
194
+ tail(key, hash)
195
+ finalize(key, hash)
196
+ const byteArray = new Uint8Array(hash.buffer);
197
+ return Array.from(byteArray).map(byte => byte.toString(16).padStart(2, '0')).join('');
198
+ }
@@ -0,0 +1,15 @@
1
+ export function imageDataToDataURL(imageData: ImageData): string {
2
+ const canvas = document.createElement('canvas');
3
+ const context = canvas.getContext('2d');
4
+
5
+ if (!context) {
6
+ throw new Error('Canvas context not supported');
7
+ }
8
+
9
+ canvas.width = imageData.width;
10
+ canvas.height = imageData.height;
11
+
12
+ context.putImageData(imageData, 0, 0);
13
+
14
+ return canvas.toDataURL();
15
+ }
@@ -0,0 +1,45 @@
1
+ import { componentInterface } from '../factory'
2
+
3
+ type DelayedPromise<T> = Promise<T>;
4
+
5
+ export function delay<T>(t: number, val: T): DelayedPromise<T> {
6
+ return new Promise<T>((resolve) => {
7
+ setTimeout(() => resolve(val), t);
8
+ });
9
+ }
10
+
11
+
12
+ export interface RaceResult<T> {
13
+ value: T;
14
+ elapsed?: number;
15
+ }
16
+
17
+ export function raceAllPerformance<T>(
18
+ promises: Promise<T>[],
19
+ timeoutTime: number,
20
+ timeoutVal: T
21
+ ): Promise<RaceResult<T>[]> {
22
+ return Promise.all(
23
+ promises.map((p) => {
24
+ const startTime = performance.now();
25
+ return Promise.race([
26
+ p.then((value) => ({
27
+ value,
28
+ elapsed: performance.now() - startTime,
29
+ })),
30
+ delay(timeoutTime, timeoutVal).then((value) => ({
31
+ value,
32
+ elapsed: performance.now() - startTime,
33
+ })),
34
+ ]);
35
+ })
36
+ );
37
+ }
38
+
39
+
40
+
41
+ export function raceAll<T>(promises: Promise<T>[], timeoutTime: number, timeoutVal: T): Promise<(T | undefined)[]> {
42
+ return Promise.all(promises.map((p) => {
43
+ return Promise.race([p, delay(timeoutTime, timeoutVal)]);
44
+ }));
45
+ }