@thumbmarkjs/thumbmarkjs 1.2.0 → 1.3.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 +5 -13
- package/dist/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +3 -10
- 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/mathml/index.d.ts +2 -0
- package/dist/types/components/webrtc/index.d.ts +2 -0
- package/dist/types/factory.d.ts +9 -0
- package/dist/types/functions/api.d.ts +39 -0
- package/dist/types/functions/index.d.ts +2 -46
- package/dist/types/options.d.ts +2 -0
- package/dist/types/utils/log.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/mathml/index.ts +149 -0
- package/src/components/webrtc/index.ts +126 -0
- package/src/factory.ts +12 -0
- package/src/functions/api.ts +130 -0
- package/src/functions/filterComponents.ts +0 -1
- package/src/functions/index.ts +39 -140
- package/src/options.ts +3 -1
- package/src/utils/log.ts +14 -13
|
@@ -4,47 +4,9 @@
|
|
|
4
4
|
* This module handles component collection, API calls, uniqueness scoring, and data filtering
|
|
5
5
|
* for the ThumbmarkJS browser fingerprinting library.
|
|
6
6
|
*
|
|
7
|
-
* Exports:
|
|
8
|
-
* - getThumbmark
|
|
9
|
-
* - getThumbmarkDataFromPromiseMap
|
|
10
|
-
* - resolveClientComponents
|
|
11
|
-
* - filterThumbmarkData
|
|
12
|
-
*
|
|
13
|
-
* Internal helpers and types are also defined here.
|
|
14
7
|
*/
|
|
15
8
|
import { optionsInterface } from "../options";
|
|
16
9
|
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
|
-
vpn: boolean;
|
|
30
|
-
bot: boolean;
|
|
31
|
-
datacenter: boolean;
|
|
32
|
-
danger_level: number;
|
|
33
|
-
};
|
|
34
|
-
uniqueness?: {
|
|
35
|
-
score: number | string;
|
|
36
|
-
};
|
|
37
|
-
timed_out?: boolean;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* API response structure
|
|
41
|
-
*/
|
|
42
|
-
interface apiResponse {
|
|
43
|
-
info?: infoInterface;
|
|
44
|
-
version?: string;
|
|
45
|
-
components?: componentInterface;
|
|
46
|
-
visitorId?: string;
|
|
47
|
-
}
|
|
48
10
|
/**
|
|
49
11
|
* Final thumbmark response structure
|
|
50
12
|
*/
|
|
@@ -56,16 +18,10 @@ interface thumbmarkResponse {
|
|
|
56
18
|
version: string;
|
|
57
19
|
thumbmark: string;
|
|
58
20
|
visitorId?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Only present if options.performance is true.
|
|
61
|
-
*/
|
|
62
21
|
elapsed?: any;
|
|
22
|
+
error?: string;
|
|
23
|
+
experimental?: componentInterface;
|
|
63
24
|
}
|
|
64
|
-
/**
|
|
65
|
-
* Calls the Thumbmark API with the given components, using caching and deduplication.
|
|
66
|
-
* Returns a promise for the API response or null on error.
|
|
67
|
-
*/
|
|
68
|
-
export declare const getApiPromise: (options: optionsInterface, components: componentInterface) => Promise<apiResponse | null>;
|
|
69
25
|
/**
|
|
70
26
|
* Main entry point: collects all components, optionally calls API, and returns thumbmark data.
|
|
71
27
|
*
|
package/dist/types/options.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface optionsInterface {
|
|
|
8
8
|
cache_api_call?: boolean;
|
|
9
9
|
performance?: boolean;
|
|
10
10
|
stabilize?: string[];
|
|
11
|
+
experimental?: boolean;
|
|
11
12
|
}
|
|
12
13
|
export declare const API_ENDPOINT = "https://api.thumbmarkjs.com";
|
|
13
14
|
export declare const defaultOptions: optionsInterface;
|
|
@@ -21,6 +22,7 @@ export declare let options: {
|
|
|
21
22
|
cache_api_call?: boolean | undefined;
|
|
22
23
|
performance?: boolean | undefined;
|
|
23
24
|
stabilize?: string[] | undefined;
|
|
25
|
+
experimental?: boolean | undefined;
|
|
24
26
|
};
|
|
25
27
|
/**
|
|
26
28
|
*
|
|
@@ -5,4 +5,4 @@ import { optionsInterface } from '../options';
|
|
|
5
5
|
* You can disable this by setting options.logging to false.
|
|
6
6
|
* @internal
|
|
7
7
|
*/
|
|
8
|
-
export declare function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface): Promise<void>;
|
|
8
|
+
export declare function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface, experimentalData?: componentInterface): Promise<void>;
|
package/package.json
CHANGED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { componentInterface } from '../../factory';
|
|
2
|
+
import { hash } from '../../utils/hash';
|
|
3
|
+
|
|
4
|
+
const BLACKBOARD_BOLD = ['\uD835\uDD04', '\uD835\uDD05', '\u212D', '\uD835\uDD07', '\uD835\uDD08', '\uD835\uDD09', '\uD835\uDD38', '\uD835\uDD39', '\u2102', '\uD835\uDD3B', '\uD835\uDD3C', '\uD835\uDD3D'];
|
|
5
|
+
const GREEK_SYMBOLS = ['\u03B2', '\u03C8', '\u03BB', '\u03B5', '\u03B6', '\u03B1', '\u03BE', '\u03BC', '\u03C1', '\u03C6', '\u03BA', '\u03C4', '\u03B7', '\u03C3', '\u03B9', '\u03C9', '\u03B3', '\u03BD', '\u03C7', '\u03B4', '\u03B8', '\u03C0', '\u03C5', '\u03BF'];
|
|
6
|
+
|
|
7
|
+
export default async function getMathML(): Promise<componentInterface | null> {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
try {
|
|
10
|
+
if (!isMathMLSupported()) {
|
|
11
|
+
resolve({
|
|
12
|
+
supported: false,
|
|
13
|
+
error: 'MathML not supported'
|
|
14
|
+
});
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const structures = [
|
|
19
|
+
createMathML('integral', '<msubsup><mo>\u222B</mo><mi>a</mi><mi>b</mi></msubsup><mfrac><mrow><mi>f</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mi>g</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac><mi>dx</mi>'),
|
|
20
|
+
createMathML('fraction', '<mfrac><mrow><mi>\u03C0</mi><mo>\u00D7</mo><msup><mi>r</mi><mn>2</mn></msup></mrow><mrow><mn>2</mn><mi>\u03C3</mi></mrow></mfrac>'),
|
|
21
|
+
createMathML('matrix', '<mo>[</mo><mtable><mtr><mtd><mi>\u03B1</mi></mtd><mtd><mi>\u03B2</mi></mtd></mtr><mtr><mtd><mi>\u03B3</mi></mtd><mtd><mi>\u03B4</mi></mtd></mtr></mtable><mo>]</mo>'),
|
|
22
|
+
createComplexNestedStructure(),
|
|
23
|
+
...createSymbolStructures()
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const measurements: any = {};
|
|
27
|
+
structures.forEach((struct, i) => {
|
|
28
|
+
measurements[`struct_${i}`] = measureMathMLStructure(struct);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
resolve({
|
|
32
|
+
//supported: true,
|
|
33
|
+
//measurements,
|
|
34
|
+
hash: hash(JSON.stringify(measurements))
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
} catch (error) {
|
|
38
|
+
resolve({
|
|
39
|
+
supported: false,
|
|
40
|
+
error: `MathML error: ${(error as Error).message}`
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isMathMLSupported(): boolean {
|
|
47
|
+
try {
|
|
48
|
+
const testElement = document.createElement('math');
|
|
49
|
+
testElement.innerHTML = '<mrow><mi>x</mi></mrow>';
|
|
50
|
+
testElement.style.position = 'absolute';
|
|
51
|
+
testElement.style.visibility = 'hidden';
|
|
52
|
+
|
|
53
|
+
document.body.appendChild(testElement);
|
|
54
|
+
const rect = testElement.getBoundingClientRect();
|
|
55
|
+
document.body.removeChild(testElement);
|
|
56
|
+
|
|
57
|
+
return rect.width > 0 && rect.height > 0;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createMathML(name: string, content: string): string {
|
|
64
|
+
return `<math><mrow>${content}</mrow></math>`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createComplexNestedStructure(): string {
|
|
68
|
+
let nestedContent = '<mo>\u220F</mo>'; // Product symbol (∏)
|
|
69
|
+
|
|
70
|
+
// Add all symbol combinations inside the main structure
|
|
71
|
+
BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
|
|
72
|
+
const startIdx = bbIndex * 2;
|
|
73
|
+
const greekSet = GREEK_SYMBOLS.slice(startIdx, startIdx + 2);
|
|
74
|
+
|
|
75
|
+
if (greekSet.length === 2) {
|
|
76
|
+
nestedContent += `<mmultiscripts><mi>${bbSymbol}</mi><none/><mi>${greekSet[1]}</mi><mprescripts></mprescripts><mi>${greekSet[0]}</mi><none/></mmultiscripts>`;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return createMathML('complex_nested',
|
|
81
|
+
`<munderover><mmultiscripts>${nestedContent}</mmultiscripts></munderover>`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function createSymbolStructures(): string[] {
|
|
86
|
+
const structures: string[] = [];
|
|
87
|
+
|
|
88
|
+
// Use blackboard bold as base symbols with Greek symbols as subscripts/superscripts
|
|
89
|
+
BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
|
|
90
|
+
// Get 2 Greek symbols for this blackboard bold symbol (lower left, top right)
|
|
91
|
+
const startIdx = bbIndex * 2;
|
|
92
|
+
const greekSet = GREEK_SYMBOLS.slice(startIdx, startIdx + 2);
|
|
93
|
+
|
|
94
|
+
if (greekSet.length === 2) {
|
|
95
|
+
structures.push(createMathML('combined',
|
|
96
|
+
`<mmultiscripts><mi>${bbSymbol}</mi><none/><mi>${greekSet[1]}</mi><mprescripts></mprescripts><mi>${greekSet[0]}</mi><none/></mmultiscripts>`
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return structures;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function measureMathMLStructure(mathml: string): any {
|
|
105
|
+
try {
|
|
106
|
+
const mathElement = document.createElement('math');
|
|
107
|
+
mathElement.innerHTML = mathml.replace(/<\/?math>/g, '');
|
|
108
|
+
mathElement.style.whiteSpace = 'nowrap';
|
|
109
|
+
mathElement.style.position = 'absolute';
|
|
110
|
+
mathElement.style.visibility = 'hidden';
|
|
111
|
+
mathElement.style.top = '-9999px';
|
|
112
|
+
|
|
113
|
+
document.body.appendChild(mathElement);
|
|
114
|
+
|
|
115
|
+
const rect = mathElement.getBoundingClientRect();
|
|
116
|
+
const computedStyle = window.getComputedStyle(mathElement);
|
|
117
|
+
|
|
118
|
+
const measurements = {
|
|
119
|
+
dimensions: {
|
|
120
|
+
width: rect.width,
|
|
121
|
+
height: rect.height,
|
|
122
|
+
|
|
123
|
+
},
|
|
124
|
+
fontInfo: {
|
|
125
|
+
fontFamily: computedStyle.fontFamily,
|
|
126
|
+
fontSize: computedStyle.fontSize,
|
|
127
|
+
fontWeight: computedStyle.fontWeight,
|
|
128
|
+
fontStyle: computedStyle.fontStyle,
|
|
129
|
+
lineHeight: computedStyle.lineHeight,
|
|
130
|
+
// Enhanced font properties for better system detection
|
|
131
|
+
fontVariant: computedStyle.fontVariant || 'normal',
|
|
132
|
+
fontStretch: computedStyle.fontStretch || 'normal',
|
|
133
|
+
fontSizeAdjust: computedStyle.fontSizeAdjust || 'none',
|
|
134
|
+
textRendering: computedStyle.textRendering || 'auto',
|
|
135
|
+
fontFeatureSettings: computedStyle.fontFeatureSettings || 'normal',
|
|
136
|
+
fontVariantNumeric: computedStyle.fontVariantNumeric || 'normal',
|
|
137
|
+
fontKerning: computedStyle.fontKerning || 'auto'
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
document.body.removeChild(mathElement);
|
|
142
|
+
return measurements;
|
|
143
|
+
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
error: (error as Error).message
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { componentInterface } from '../../factory';
|
|
2
|
+
import { hash } from '../../utils/hash';
|
|
3
|
+
|
|
4
|
+
export default async function getWebRTC(): Promise<componentInterface | null> {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
try {
|
|
7
|
+
// Check if WebRTC is supported
|
|
8
|
+
const RTCPeerConnection = (window as any).RTCPeerConnection || (window as any).webkitRTCPeerConnection || (window as any).mozRTCPeerConnection;
|
|
9
|
+
if (!RTCPeerConnection) {
|
|
10
|
+
resolve({
|
|
11
|
+
supported: false,
|
|
12
|
+
error: 'WebRTC not supported'
|
|
13
|
+
});
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const config = {
|
|
18
|
+
iceCandidatePoolSize: 1,
|
|
19
|
+
iceServers: []
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const connection = new RTCPeerConnection(config);
|
|
23
|
+
connection.createDataChannel(''); // trigger ICE gathering
|
|
24
|
+
|
|
25
|
+
const processOffer = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
|
|
28
|
+
const offer = await connection.createOffer(offerOptions);
|
|
29
|
+
await connection.setLocalDescription(offer);
|
|
30
|
+
|
|
31
|
+
const sdp = offer.sdp || '';
|
|
32
|
+
|
|
33
|
+
// Extract RTP extensions
|
|
34
|
+
const extensions = [...new Set((sdp.match(/extmap:\d+ [^\n\r]+/g) || []).map((x: string) => x.replace(/extmap:\d+ /, '')))].sort();
|
|
35
|
+
|
|
36
|
+
// Extract codec information
|
|
37
|
+
const getDescriptors = (mediaType: string) => {
|
|
38
|
+
const match = sdp.match(new RegExp(`m=${mediaType} [^\\s]+ [^\\s]+ ([^\\n\\r]+)`));
|
|
39
|
+
return match ? match[1].split(' ') : [];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const constructDescriptions = (mediaType: string, descriptors: string[]) => {
|
|
43
|
+
return descriptors.map(descriptor => {
|
|
44
|
+
const matcher = new RegExp(`(rtpmap|fmtp|rtcp-fb):${descriptor} (.+)`, 'g');
|
|
45
|
+
const matches = [...sdp.matchAll(matcher)];
|
|
46
|
+
if (!matches.length) return null;
|
|
47
|
+
|
|
48
|
+
const description: any = {};
|
|
49
|
+
matches.forEach(match => {
|
|
50
|
+
const [_, type, data] = match;
|
|
51
|
+
const parts = data.split('/');
|
|
52
|
+
if (type === 'rtpmap') {
|
|
53
|
+
description.mimeType = `${mediaType}/${parts[0]}`;
|
|
54
|
+
description.clockRate = +parts[1];
|
|
55
|
+
if (mediaType === 'audio') description.channels = +parts[2] || 1;
|
|
56
|
+
} else if (type === 'rtcp-fb') {
|
|
57
|
+
description.feedbackSupport = description.feedbackSupport || [];
|
|
58
|
+
description.feedbackSupport.push(data);
|
|
59
|
+
} else if (type === 'fmtp') {
|
|
60
|
+
description.sdpFmtpLine = data;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return description;
|
|
64
|
+
}).filter(Boolean);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const codecsSdp = {
|
|
68
|
+
audio: constructDescriptions('audio', getDescriptors('audio')),
|
|
69
|
+
video: constructDescriptions('video', getDescriptors('video'))
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Set up for ICE candidate collection with timeout
|
|
73
|
+
const result = await new Promise<componentInterface>((resolveResult) => {
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
connection.removeEventListener('icecandidate', onIceCandidate);
|
|
76
|
+
connection.close();
|
|
77
|
+
resolveResult({
|
|
78
|
+
supported: true,
|
|
79
|
+
codecsSdp,
|
|
80
|
+
extensions: extensions as string[],
|
|
81
|
+
timeout: true
|
|
82
|
+
});
|
|
83
|
+
}, 3000);
|
|
84
|
+
|
|
85
|
+
const onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
|
|
86
|
+
const candidateObj = event.candidate;
|
|
87
|
+
if (!candidateObj || !candidateObj.candidate) return;
|
|
88
|
+
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
connection.removeEventListener('icecandidate', onIceCandidate);
|
|
91
|
+
connection.close();
|
|
92
|
+
|
|
93
|
+
resolveResult({
|
|
94
|
+
supported: true,
|
|
95
|
+
codecsSdp,
|
|
96
|
+
extensions: extensions as string[],
|
|
97
|
+
candidateType: candidateObj.type || ''
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
connection.addEventListener('icecandidate', onIceCandidate);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
resolve({
|
|
105
|
+
hash: hash(JSON.stringify(result)),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
connection.close();
|
|
110
|
+
resolve({
|
|
111
|
+
supported: true,
|
|
112
|
+
error: `WebRTC offer failed: ${(error as Error).message}`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
processOffer();
|
|
118
|
+
|
|
119
|
+
} catch (error) {
|
|
120
|
+
resolve({
|
|
121
|
+
supported: false,
|
|
122
|
+
error: `WebRTC error: ${(error as Error).message}`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
package/src/factory.ts
CHANGED
|
@@ -20,6 +20,10 @@ import getScreen from "./components/screen";
|
|
|
20
20
|
import getSystem from "./components/system";
|
|
21
21
|
import getWebGL from "./components/webgl";
|
|
22
22
|
|
|
23
|
+
// Import experimental component functions
|
|
24
|
+
import getWebRTC from "./components/webrtc";
|
|
25
|
+
import getMathML from "./components/mathml";
|
|
26
|
+
|
|
23
27
|
/**
|
|
24
28
|
* @description key->function map of built-in components. Do not call the function here.
|
|
25
29
|
*/
|
|
@@ -37,6 +41,14 @@ export const tm_component_promises = {
|
|
|
37
41
|
'webgl': getWebGL
|
|
38
42
|
};
|
|
39
43
|
|
|
44
|
+
/**
|
|
45
|
+
* @description key->function map of experimental components. Only resolved during logging.
|
|
46
|
+
*/
|
|
47
|
+
export const tm_experimental_component_promises = {
|
|
48
|
+
'webrtc': getWebRTC,
|
|
49
|
+
'mathml': getMathML
|
|
50
|
+
};
|
|
51
|
+
|
|
40
52
|
// the component interface is the form of the JSON object the function's promise must return
|
|
41
53
|
export interface componentInterface {
|
|
42
54
|
[key: string]: string | string[] | number | boolean | componentInterface;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { optionsInterface, API_ENDPOINT } from '../options';
|
|
2
|
+
import { componentInterface } from '../factory';
|
|
3
|
+
import { getVisitorId, setVisitorId } from '../utils/visitorId';
|
|
4
|
+
import { getVersion } from "../utils/version";
|
|
5
|
+
import { hash } from '../utils/hash';
|
|
6
|
+
|
|
7
|
+
// ===================== Types & Interfaces =====================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Info returned from the API (IP, classification, uniqueness, etc)
|
|
11
|
+
*/
|
|
12
|
+
export interface infoInterface {
|
|
13
|
+
ip_address?: {
|
|
14
|
+
ip_address: string,
|
|
15
|
+
ip_identifier: string,
|
|
16
|
+
autonomous_system_number: number,
|
|
17
|
+
ip_version: 'v6' | 'v4',
|
|
18
|
+
},
|
|
19
|
+
classification?: {
|
|
20
|
+
tor: boolean,
|
|
21
|
+
vpn: boolean,
|
|
22
|
+
bot: boolean,
|
|
23
|
+
datacenter: boolean,
|
|
24
|
+
danger_level: number, // 5 is highest and should be blocked. 0 is no danger.
|
|
25
|
+
},
|
|
26
|
+
uniqueness?: {
|
|
27
|
+
score: number | string
|
|
28
|
+
},
|
|
29
|
+
timed_out?: boolean; // added for timeout handling
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* API response structure
|
|
34
|
+
*/
|
|
35
|
+
interface apiResponse {
|
|
36
|
+
info?: infoInterface;
|
|
37
|
+
version?: string;
|
|
38
|
+
components?: componentInterface;
|
|
39
|
+
visitorId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ===================== API Call Logic =====================
|
|
43
|
+
|
|
44
|
+
let currentApiPromise: Promise<apiResponse> | null = null;
|
|
45
|
+
let apiPromiseResult: apiResponse | null = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Calls the Thumbmark API with the given components, using caching and deduplication.
|
|
49
|
+
* Returns a promise for the API response or null on error.
|
|
50
|
+
*/
|
|
51
|
+
export const getApiPromise = (
|
|
52
|
+
options: optionsInterface,
|
|
53
|
+
components: componentInterface
|
|
54
|
+
): Promise<apiResponse | null> => {
|
|
55
|
+
// 1. If a result is already cached and caching is enabled, return it.
|
|
56
|
+
if (options.cache_api_call && apiPromiseResult) {
|
|
57
|
+
return Promise.resolve(apiPromiseResult);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. If a request is already in flight, return that promise to prevent duplicate calls.
|
|
61
|
+
if (currentApiPromise) {
|
|
62
|
+
return currentApiPromise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Otherwise, initiate a new API call with timeout.
|
|
66
|
+
const endpoint = `${API_ENDPOINT}/thumbmark`;
|
|
67
|
+
const visitorId = getVisitorId();
|
|
68
|
+
const requestBody: any = {
|
|
69
|
+
components,
|
|
70
|
+
options,
|
|
71
|
+
clientHash: hash(JSON.stringify(components)),
|
|
72
|
+
version: getVersion()
|
|
73
|
+
};
|
|
74
|
+
if (visitorId) {
|
|
75
|
+
requestBody.visitorId = visitorId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fetchPromise = fetch(endpoint, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'x-api-key': options.api_key!,
|
|
82
|
+
'Authorization': 'custom-authorized',
|
|
83
|
+
'Content-Type': 'application/json',
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(requestBody),
|
|
86
|
+
})
|
|
87
|
+
.then(response => {
|
|
88
|
+
// Handle HTTP errors that aren't network errors
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
if (response.status === 403) {
|
|
91
|
+
throw new Error('INVALID_API_KEY');
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
94
|
+
}
|
|
95
|
+
return response.json();
|
|
96
|
+
})
|
|
97
|
+
.then(data => {
|
|
98
|
+
// Handle visitor ID from server response
|
|
99
|
+
if (data.visitorId && data.visitorId !== visitorId) {
|
|
100
|
+
setVisitorId(data.visitorId);
|
|
101
|
+
}
|
|
102
|
+
apiPromiseResult = data; // Cache the successful result
|
|
103
|
+
currentApiPromise = null; // Clear the in-flight promise
|
|
104
|
+
return data;
|
|
105
|
+
})
|
|
106
|
+
.catch(error => {
|
|
107
|
+
console.error('Error fetching pro data', error);
|
|
108
|
+
currentApiPromise = null; // Also clear the in-flight promise on error
|
|
109
|
+
// For 403 errors, propagate the error instead of returning null
|
|
110
|
+
if (error.message === 'INVALID_API_KEY') {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
// Return null instead of a string to prevent downstream crashes
|
|
114
|
+
return null;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Timeout logic
|
|
118
|
+
const timeoutMs = options.timeout || 5000;
|
|
119
|
+
const timeoutPromise = new Promise<apiResponse>((resolve) => {
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
resolve({
|
|
122
|
+
info: { timed_out: true },
|
|
123
|
+
version: getVersion(),
|
|
124
|
+
});
|
|
125
|
+
}, timeoutMs);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
currentApiPromise = Promise.race([fetchPromise, timeoutPromise]);
|
|
129
|
+
return currentApiPromise;
|
|
130
|
+
};
|
|
@@ -47,7 +47,6 @@ export function filterThumbmarkData(
|
|
|
47
47
|
if (!rules) continue;
|
|
48
48
|
|
|
49
49
|
for (const rule of rules) {
|
|
50
|
-
// FIX: Use the 'in' operator as a type guard to check if 'browsers' exists.
|
|
51
50
|
// A rule applies to all browsers if the 'browsers' key is not present.
|
|
52
51
|
const appliesToAllBrowsers = !('browsers' in rule);
|
|
53
52
|
|