fpscanner 0.2.0 → 0.9.2

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 (168) hide show
  1. package/README.md +639 -55
  2. package/bin/cli.js +216 -0
  3. package/dist/crypto-helpers.d.ts +19 -0
  4. package/dist/crypto-helpers.d.ts.map +1 -0
  5. package/dist/detections/hasCDP.d.ts +3 -0
  6. package/dist/detections/hasCDP.d.ts.map +1 -0
  7. package/dist/detections/hasContextMismatch.d.ts +3 -0
  8. package/dist/detections/hasContextMismatch.d.ts.map +1 -0
  9. package/dist/detections/hasHeadlessChromeScreenResolution.d.ts +3 -0
  10. package/dist/detections/hasHeadlessChromeScreenResolution.d.ts.map +1 -0
  11. package/dist/detections/hasHighCPUCount.d.ts +3 -0
  12. package/dist/detections/hasHighCPUCount.d.ts.map +1 -0
  13. package/dist/detections/hasImpossibleDeviceMemory.d.ts +3 -0
  14. package/dist/detections/hasImpossibleDeviceMemory.d.ts.map +1 -0
  15. package/dist/detections/hasMismatchPlatformIframe.d.ts +3 -0
  16. package/dist/detections/hasMismatchPlatformIframe.d.ts.map +1 -0
  17. package/dist/detections/hasMismatchPlatformWorker.d.ts +3 -0
  18. package/dist/detections/hasMismatchPlatformWorker.d.ts.map +1 -0
  19. package/dist/detections/hasMismatchWebGLInWorker.d.ts +3 -0
  20. package/dist/detections/hasMismatchWebGLInWorker.d.ts.map +1 -0
  21. package/dist/detections/hasMissingChromeObject.d.ts +3 -0
  22. package/dist/detections/hasMissingChromeObject.d.ts.map +1 -0
  23. package/dist/detections/hasPlaywright.d.ts +3 -0
  24. package/dist/detections/hasPlaywright.d.ts.map +1 -0
  25. package/dist/detections/hasSeleniumProperty.d.ts +3 -0
  26. package/dist/detections/hasSeleniumProperty.d.ts.map +1 -0
  27. package/dist/detections/hasSwiftshaderRenderer.d.ts +3 -0
  28. package/dist/detections/hasSwiftshaderRenderer.d.ts.map +1 -0
  29. package/dist/detections/hasUTCTimezone.d.ts +3 -0
  30. package/dist/detections/hasUTCTimezone.d.ts.map +1 -0
  31. package/dist/detections/hasWebdriver.d.ts +3 -0
  32. package/dist/detections/hasWebdriver.d.ts.map +1 -0
  33. package/dist/detections/hasWebdriverIframe.d.ts +3 -0
  34. package/dist/detections/hasWebdriverIframe.d.ts.map +1 -0
  35. package/dist/detections/hasWebdriverWorker.d.ts +3 -0
  36. package/dist/detections/hasWebdriverWorker.d.ts.map +1 -0
  37. package/dist/detections/hasWebdriverWritable.d.ts +3 -0
  38. package/dist/detections/hasWebdriverWritable.d.ts.map +1 -0
  39. package/dist/fpScanner.cjs.js +31 -0
  40. package/dist/fpScanner.es.js +1066 -0
  41. package/dist/index.d.ts +39 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/signals/browserExtensions.d.ts +5 -0
  44. package/dist/signals/browserExtensions.d.ts.map +1 -0
  45. package/dist/signals/browserFeatures.d.ts +14 -0
  46. package/dist/signals/browserFeatures.d.ts.map +1 -0
  47. package/dist/signals/canvas.d.ts +6 -0
  48. package/dist/signals/canvas.d.ts.map +1 -0
  49. package/dist/signals/cdp.d.ts +2 -0
  50. package/dist/signals/cdp.d.ts.map +1 -0
  51. package/dist/signals/cpuCount.d.ts +2 -0
  52. package/dist/signals/cpuCount.d.ts.map +1 -0
  53. package/dist/signals/etsl.d.ts +2 -0
  54. package/dist/signals/etsl.d.ts.map +1 -0
  55. package/dist/signals/highEntropyValues.d.ts +11 -0
  56. package/dist/signals/highEntropyValues.d.ts.map +1 -0
  57. package/dist/signals/iframe.d.ts +9 -0
  58. package/dist/signals/iframe.d.ts.map +1 -0
  59. package/dist/signals/internationalization.d.ts +5 -0
  60. package/dist/signals/internationalization.d.ts.map +1 -0
  61. package/dist/signals/languages.d.ts +5 -0
  62. package/dist/signals/languages.d.ts.map +1 -0
  63. package/dist/signals/maths.d.ts +2 -0
  64. package/dist/signals/maths.d.ts.map +1 -0
  65. package/dist/signals/mediaCodecs.d.ts +11 -0
  66. package/dist/signals/mediaCodecs.d.ts.map +1 -0
  67. package/dist/signals/mediaQueries.d.ts +13 -0
  68. package/dist/signals/mediaQueries.d.ts.map +1 -0
  69. package/dist/signals/memory.d.ts +2 -0
  70. package/dist/signals/memory.d.ts.map +1 -0
  71. package/dist/signals/multimediaDevices.d.ts +2 -0
  72. package/dist/signals/multimediaDevices.d.ts.map +1 -0
  73. package/dist/signals/navigatorPropertyDescriptors.d.ts +2 -0
  74. package/dist/signals/navigatorPropertyDescriptors.d.ts.map +1 -0
  75. package/dist/signals/nonce.d.ts +2 -0
  76. package/dist/signals/nonce.d.ts.map +1 -0
  77. package/dist/signals/platform.d.ts +2 -0
  78. package/dist/signals/platform.d.ts.map +1 -0
  79. package/dist/signals/playwright.d.ts +2 -0
  80. package/dist/signals/playwright.d.ts.map +1 -0
  81. package/dist/signals/plugins.d.ts +9 -0
  82. package/dist/signals/plugins.d.ts.map +1 -0
  83. package/dist/signals/screenResolution.d.ts +12 -0
  84. package/dist/signals/screenResolution.d.ts.map +1 -0
  85. package/dist/signals/seleniumProperties.d.ts +2 -0
  86. package/dist/signals/seleniumProperties.d.ts.map +1 -0
  87. package/dist/signals/time.d.ts +2 -0
  88. package/dist/signals/time.d.ts.map +1 -0
  89. package/dist/signals/toSourceError.d.ts +5 -0
  90. package/dist/signals/toSourceError.d.ts.map +1 -0
  91. package/dist/signals/url.d.ts +2 -0
  92. package/dist/signals/url.d.ts.map +1 -0
  93. package/dist/signals/userAgent.d.ts +2 -0
  94. package/dist/signals/userAgent.d.ts.map +1 -0
  95. package/dist/signals/utils.d.ts +11 -0
  96. package/dist/signals/utils.d.ts.map +1 -0
  97. package/dist/signals/webGL.d.ts +5 -0
  98. package/dist/signals/webGL.d.ts.map +1 -0
  99. package/dist/signals/webdriver.d.ts +2 -0
  100. package/dist/signals/webdriver.d.ts.map +1 -0
  101. package/dist/signals/webdriverWritable.d.ts +2 -0
  102. package/dist/signals/webdriverWritable.d.ts.map +1 -0
  103. package/dist/signals/webgpu.d.ts +7 -0
  104. package/dist/signals/webgpu.d.ts.map +1 -0
  105. package/dist/signals/worker.d.ts +2 -0
  106. package/dist/signals/worker.d.ts.map +1 -0
  107. package/dist/types.d.ts +207 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/package.json +58 -15
  110. package/scripts/build-custom.js +246 -0
  111. package/src/crypto-helpers.ts +50 -0
  112. package/src/detections/hasCDP.ts +5 -0
  113. package/src/detections/hasContextMismatch.ts +19 -0
  114. package/src/detections/hasHeadlessChromeScreenResolution.ts +10 -0
  115. package/src/detections/hasHighCPUCount.ts +9 -0
  116. package/src/detections/hasImpossibleDeviceMemory.ts +9 -0
  117. package/src/detections/hasMismatchPlatformIframe.ts +10 -0
  118. package/src/detections/hasMismatchPlatformWorker.ts +10 -0
  119. package/src/detections/hasMismatchWebGLInWorker.ts +13 -0
  120. package/src/detections/hasMissingChromeObject.ts +6 -0
  121. package/src/detections/hasPlaywright.ts +5 -0
  122. package/src/detections/hasSeleniumProperty.ts +5 -0
  123. package/src/detections/hasSwiftshaderRenderer.ts +5 -0
  124. package/src/detections/hasUTCTimezone.ts +5 -0
  125. package/src/detections/hasWebdriver.ts +5 -0
  126. package/src/detections/hasWebdriverIframe.ts +5 -0
  127. package/src/detections/hasWebdriverWorker.ts +5 -0
  128. package/src/detections/hasWebdriverWritable.ts +5 -0
  129. package/src/globals.d.ts +10 -0
  130. package/src/index.ts +644 -0
  131. package/src/signals/browserExtensions.ts +57 -0
  132. package/src/signals/browserFeatures.ts +24 -0
  133. package/src/signals/canvas.ts +84 -0
  134. package/src/signals/cdp.ts +18 -0
  135. package/src/signals/cpuCount.ts +5 -0
  136. package/src/signals/etsl.ts +3 -0
  137. package/src/signals/highEntropyValues.ts +48 -0
  138. package/src/signals/iframe.ts +34 -0
  139. package/src/signals/internationalization.ts +24 -0
  140. package/src/signals/languages.ts +6 -0
  141. package/src/signals/maths.ts +30 -0
  142. package/src/signals/mediaCodecs.ts +120 -0
  143. package/src/signals/mediaQueries.ts +85 -0
  144. package/src/signals/memory.ts +5 -0
  145. package/src/signals/multimediaDevices.ts +34 -0
  146. package/src/signals/navigatorPropertyDescriptors.ts +17 -0
  147. package/src/signals/nonce.ts +3 -0
  148. package/src/signals/platform.ts +3 -0
  149. package/src/signals/playwright.ts +3 -0
  150. package/src/signals/plugins.ts +70 -0
  151. package/src/signals/screenResolution.ts +15 -0
  152. package/src/signals/seleniumProperties.ts +40 -0
  153. package/src/signals/time.ts +3 -0
  154. package/src/signals/toSourceError.ts +27 -0
  155. package/src/signals/url.ts +3 -0
  156. package/src/signals/userAgent.ts +3 -0
  157. package/src/signals/utils.ts +29 -0
  158. package/src/signals/webGL.ts +28 -0
  159. package/src/signals/webdriver.ts +3 -0
  160. package/src/signals/webdriverWritable.ts +15 -0
  161. package/src/signals/webgpu.ts +28 -0
  162. package/src/signals/worker.ts +77 -0
  163. package/src/types.ts +237 -0
  164. package/.babelrc +0 -3
  165. package/.travis.yml +0 -17
  166. package/src/fpScanner.js +0 -222
  167. package/test/test.html +0 -11
  168. package/test/test.js +0 -116
package/src/index.ts ADDED
@@ -0,0 +1,644 @@
1
+ // Import all signals
2
+ import { webdriver } from './signals/webdriver';
3
+ import { userAgent } from './signals/userAgent';
4
+ import { platform } from './signals/platform';
5
+ import { cdp } from './signals/cdp';
6
+ import { webGL } from './signals/webGL';
7
+ import { playwright } from './signals/playwright';
8
+ import { cpuCount } from './signals/cpuCount';
9
+ import { maths } from './signals/maths';
10
+ import { memory } from './signals/memory';
11
+ import { etsl } from './signals/etsl';
12
+ import { internationalization } from './signals/internationalization';
13
+ import { screenResolution } from './signals/screenResolution';
14
+ import { languages } from './signals/languages';
15
+ import { webgpu } from './signals/webgpu';
16
+ import { hasSeleniumProperties } from './signals/seleniumProperties';
17
+ import { webdriverWritable } from './signals/webdriverWritable';
18
+ import { highEntropyValues } from './signals/highEntropyValues';
19
+ import { plugins } from './signals/plugins';
20
+ import { multimediaDevices } from './signals/multimediaDevices';
21
+ import { iframe } from './signals/iframe';
22
+ import { worker } from './signals/worker';
23
+ import { toSourceError } from './signals/toSourceError';
24
+ import { mediaCodecs } from './signals/mediaCodecs';
25
+ import { canvas } from './signals/canvas';
26
+ import { navigatorPropertyDescriptors } from './signals/navigatorPropertyDescriptors';
27
+ import { nonce } from './signals/nonce';
28
+ import { time } from './signals/time';
29
+ import { pageURL } from './signals/url';
30
+ import { hasContextMismatch } from './detections/hasContextMismatch';
31
+ import { browserExtensions } from './signals/browserExtensions';
32
+ import { browserFeatures } from './signals/browserFeatures';
33
+ import { mediaQueries } from './signals/mediaQueries';
34
+
35
+ // Fast Bot Detection tests
36
+ import { hasHeadlessChromeScreenResolution } from './detections/hasHeadlessChromeScreenResolution';
37
+ import { hasWebdriver } from './detections/hasWebdriver';
38
+ import { hasSeleniumProperty } from './detections/hasSeleniumProperty';
39
+ import { hasCDP } from './detections/hasCDP';
40
+ import { hasPlaywright } from './detections/hasPlaywright';
41
+ import { hasImpossibleDeviceMemory } from './detections/hasImpossibleDeviceMemory';
42
+ import { hasHighCPUCount } from './detections/hasHighCPUCount';
43
+ import { hasMissingChromeObject } from './detections/hasMissingChromeObject';
44
+ import { hasWebdriverIframe } from './detections/hasWebdriverIframe';
45
+ import { hasWebdriverWorker } from './detections/hasWebdriverWorker';
46
+ import { hasMismatchWebGLInWorker } from './detections/hasMismatchWebGLInWorker';
47
+ import { hasMismatchPlatformWorker } from './detections/hasMismatchPlatformWorker';
48
+ import { hasMismatchPlatformIframe } from './detections/hasMismatchPlatformIframe';
49
+ import { hasWebdriverWritable } from './detections/hasWebdriverWritable';
50
+ import { hasSwiftshaderRenderer } from './detections/hasSwiftshaderRenderer';
51
+ import { hasUTCTimezone } from './detections/hasUTCTimezone';
52
+
53
+ import { ERROR, HIGH, INIT, LOW, MEDIUM, SKIPPED, hashCode } from './signals/utils';
54
+ import { encryptString } from './crypto-helpers';
55
+ import { Fingerprint, FastBotDetectionDetails, DetectionRule, CollectFingerprintOptions } from './types';
56
+
57
+
58
+ class FingerprintScanner {
59
+ private fingerprint: Fingerprint;
60
+
61
+ constructor() {
62
+ this.fingerprint = {
63
+ signals: {
64
+ // Automation/Bot detection signals
65
+ automation: {
66
+ webdriver: INIT,
67
+ webdriverWritable: INIT,
68
+ selenium: INIT,
69
+ cdp: INIT,
70
+ playwright: INIT,
71
+ navigatorPropertyDescriptors: INIT,
72
+ },
73
+ // Device hardware characteristics
74
+ device: {
75
+ cpuCount: INIT,
76
+ memory: INIT,
77
+ platform: INIT,
78
+ screenResolution: {
79
+ width: INIT,
80
+ height: INIT,
81
+ pixelDepth: INIT,
82
+ colorDepth: INIT,
83
+ availableWidth: INIT,
84
+ availableHeight: INIT,
85
+ innerWidth: INIT,
86
+ innerHeight: INIT,
87
+ hasMultipleDisplays: INIT,
88
+ },
89
+ multimediaDevices: {
90
+ speakers: INIT,
91
+ microphones: INIT,
92
+ webcams: INIT,
93
+ },
94
+ mediaQueries: {
95
+ prefersColorScheme: INIT,
96
+ prefersReducedMotion: INIT,
97
+ prefersReducedTransparency: INIT,
98
+ colorGamut: INIT,
99
+ pointer: INIT,
100
+ anyPointer: INIT,
101
+ hover: INIT,
102
+ anyHover: INIT,
103
+ colorDepth: INIT,
104
+ },
105
+ },
106
+ // Browser identity & features
107
+ browser: {
108
+ userAgent: INIT,
109
+ features: {
110
+ bitmask: INIT,
111
+ chrome: INIT,
112
+ brave: INIT,
113
+ applePaySupport: INIT,
114
+ opera: INIT,
115
+ serial: INIT,
116
+ attachShadow: INIT,
117
+ caches: INIT,
118
+ webAssembly: INIT,
119
+ buffer: INIT,
120
+ showModalDialog: INIT,
121
+ },
122
+ plugins: {
123
+ isValidPluginArray: INIT,
124
+ pluginCount: INIT,
125
+ pluginNamesHash: INIT,
126
+ pluginConsistency1: INIT,
127
+ pluginOverflow: INIT,
128
+ },
129
+ extensions: {
130
+ bitmask: INIT,
131
+ extensions: INIT,
132
+ },
133
+ highEntropyValues: {
134
+ architecture: INIT,
135
+ bitness: INIT,
136
+ brands: INIT,
137
+ mobile: INIT,
138
+ model: INIT,
139
+ platform: INIT,
140
+ platformVersion: INIT,
141
+ uaFullVersion: INIT,
142
+ },
143
+ etsl: INIT,
144
+ maths: INIT,
145
+ toSourceError: {
146
+ toSourceError: INIT,
147
+ hasToSource: INIT,
148
+ },
149
+ },
150
+ // Graphics & rendering
151
+ graphics: {
152
+ webGL: {
153
+ vendor: INIT,
154
+ renderer: INIT,
155
+ },
156
+ webgpu: {
157
+ vendor: INIT,
158
+ architecture: INIT,
159
+ device: INIT,
160
+ description: INIT,
161
+ },
162
+ canvas: {
163
+ hasModifiedCanvas: INIT,
164
+ canvasFingerprint: INIT,
165
+ },
166
+ },
167
+ // Media codecs (at root level)
168
+ codecs: {
169
+ audioCanPlayTypeHash: INIT,
170
+ videoCanPlayTypeHash: INIT,
171
+ audioMediaSourceHash: INIT,
172
+ videoMediaSourceHash: INIT,
173
+ rtcAudioCapabilitiesHash: INIT,
174
+ rtcVideoCapabilitiesHash: INIT,
175
+ hasMediaSource: INIT,
176
+ },
177
+ // Locale & internationalization
178
+ locale: {
179
+ internationalization: {
180
+ timezone: INIT,
181
+ localeLanguage: INIT,
182
+ },
183
+ languages: {
184
+ languages: INIT,
185
+ language: INIT,
186
+ },
187
+ },
188
+ // Isolated execution contexts
189
+ contexts: {
190
+ iframe: {
191
+ webdriver: INIT,
192
+ userAgent: INIT,
193
+ platform: INIT,
194
+ memory: INIT,
195
+ cpuCount: INIT,
196
+ language: INIT,
197
+ },
198
+ webWorker: {
199
+ webdriver: INIT,
200
+ userAgent: INIT,
201
+ platform: INIT,
202
+ memory: INIT,
203
+ cpuCount: INIT,
204
+ language: INIT,
205
+ vendor: INIT,
206
+ renderer: INIT,
207
+ },
208
+ },
209
+ },
210
+ fsid: INIT,
211
+ nonce: INIT,
212
+ time: INIT,
213
+ url: INIT,
214
+ fastBotDetection: false,
215
+ fastBotDetectionDetails: {
216
+ headlessChromeScreenResolution: { detected: false, severity: 'high' },
217
+ hasWebdriver: { detected: false, severity: 'high' },
218
+ hasWebdriverWritable: { detected: false, severity: 'high' },
219
+ hasSeleniumProperty: { detected: false, severity: 'high' },
220
+ hasCDP: { detected: false, severity: 'high' },
221
+ hasPlaywright: { detected: false, severity: 'high' },
222
+ hasImpossibleDeviceMemory: { detected: false, severity: 'high' },
223
+ hasHighCPUCount: { detected: false, severity: 'high' },
224
+ hasMissingChromeObject: { detected: false, severity: 'high' },
225
+ hasWebdriverIframe: { detected: false, severity: 'high' },
226
+ hasWebdriverWorker: { detected: false, severity: 'high' },
227
+ hasMismatchWebGLInWorker: { detected: false, severity: 'high' },
228
+ hasMismatchPlatformIframe: { detected: false, severity: 'high' },
229
+ hasMismatchPlatformWorker: { detected: false, severity: 'high' },
230
+ hasSwiftshaderRenderer: { detected: false, severity: 'low' },
231
+ hasUTCTimezone: { detected: false, severity: 'medium' },
232
+ },
233
+ };
234
+ }
235
+
236
+ private async collectSignal(signal: () => any) {
237
+ try {
238
+ return await signal();
239
+ } catch (e) {
240
+ return ERROR;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Generate a JA4-inspired fingerprint scanner ID
246
+ * Format: FS1_<det>_<auto>_<dev>_<brw>_<gfx>_<cod>_<loc>_<ctx>
247
+ *
248
+ * Each section is delimited by '_', allowing partial matching.
249
+ * Sections use the pattern: <bitmask>h<hash> where applicable.
250
+ * Bitmasks are extensible - new boolean fields are appended without breaking existing positions.
251
+ *
252
+ * Sections:
253
+ * - det: fastBotDetectionDetails bitmask (14 bits: headlessChromeScreenResolution, hasWebdriver,
254
+ * hasWebdriverWritable, hasSeleniumProperty, hasCDP, hasPlaywright, hasImpossibleDeviceMemory,
255
+ * hasHighCPUCount, hasMissingChromeObject, hasWebdriverIframe, hasWebdriverWorker,
256
+ * hasMismatchWebGLInWorker, hasMismatchPlatformIframe, hasMismatchPlatformWorker)
257
+ * - auto: automation bitmask (5 bits: webdriver, webdriverWritable, selenium, cdp, playwright) + hash
258
+ * - dev: WIDTHxHEIGHT + cpu + mem + device bitmask + hash of all device signals
259
+ * - brw: features.bitmask + extensions.bitmask + plugins bitmask (3 bits) + hash of browser signals
260
+ * - gfx: canvas bitmask (1 bit: hasModifiedCanvas) + hash of all graphics signals
261
+ * - cod: codecs bitmask (1 bit: hasMediaSource) + hash of all codec hashes
262
+ * - loc: language code (2 chars) + language count + hash of locale signals
263
+ * - ctx: context mismatch bitmask (2 bits: iframe, worker) + hash of all context signals
264
+ */
265
+ private generateFingerprintScannerId(): string {
266
+ try {
267
+ const s = this.fingerprint.signals;
268
+ const det = this.fingerprint.fastBotDetectionDetails;
269
+
270
+ // Section 1: Version
271
+ const version = 'FS1';
272
+
273
+ // Section 2: Detection bitmask - all 14 fastBotDetectionDetails booleans
274
+ // Order matches FastBotDetectionDetails interface for consistency
275
+ const detBitmask = [
276
+ det.headlessChromeScreenResolution.detected,
277
+ det.hasWebdriver.detected,
278
+ det.hasWebdriverWritable.detected,
279
+ det.hasSeleniumProperty.detected,
280
+ det.hasCDP.detected,
281
+ det.hasPlaywright.detected,
282
+ det.hasImpossibleDeviceMemory.detected,
283
+ det.hasHighCPUCount.detected,
284
+ det.hasMissingChromeObject.detected,
285
+ det.hasWebdriverIframe.detected,
286
+ det.hasWebdriverWorker.detected,
287
+ det.hasMismatchWebGLInWorker.detected,
288
+ det.hasMismatchPlatformIframe.detected,
289
+ det.hasMismatchPlatformWorker.detected,
290
+ det.hasSwiftshaderRenderer.detected,
291
+ det.hasUTCTimezone.detected,
292
+ // Add other detection rules output here
293
+ ].map(b => b ? '1' : '0').join('');
294
+ const detSection = detBitmask;
295
+
296
+ // Section 3: Automation - bitmask + hash of non-boolean fields
297
+ const autoBitmask = [
298
+ s.automation.webdriver === true,
299
+ s.automation.webdriverWritable === true,
300
+ s.automation.selenium === true,
301
+ s.automation.cdp === true,
302
+ s.automation.playwright === true,
303
+ ].map(b => b ? '1' : '0').join('');
304
+ const autoHash = hashCode(String(s.automation.navigatorPropertyDescriptors)).slice(0, 4);
305
+ const autoSection = `${autoBitmask}h${autoHash}`;
306
+
307
+ // Section 4: Device - screen dims + cpu + mem + bitmask + hash
308
+ const width = typeof s.device.screenResolution.width === 'number' ? s.device.screenResolution.width : 0;
309
+ const height = typeof s.device.screenResolution.height === 'number' ? s.device.screenResolution.height : 0;
310
+ const cpu = typeof s.device.cpuCount === 'number' ? String(s.device.cpuCount).padStart(2, '0') : '00';
311
+ const mem = typeof s.device.memory === 'number' ? String(Math.round(s.device.memory)).padStart(2, '0') : '00';
312
+ const devBitmask = [
313
+ s.device.screenResolution.hasMultipleDisplays === true,
314
+ s.device.mediaQueries.prefersReducedMotion === true,
315
+ s.device.mediaQueries.prefersReducedTransparency === true,
316
+ s.device.mediaQueries.hover === true,
317
+ s.device.mediaQueries.anyHover === true,
318
+ ].map(b => b ? '1' : '0').join('');
319
+ const devStr = [
320
+ s.device.platform,
321
+ s.device.screenResolution.pixelDepth,
322
+ s.device.screenResolution.colorDepth,
323
+ s.device.multimediaDevices.speakers,
324
+ s.device.multimediaDevices.microphones,
325
+ s.device.multimediaDevices.webcams,
326
+ s.device.mediaQueries.prefersColorScheme,
327
+ s.device.mediaQueries.colorGamut,
328
+ s.device.mediaQueries.pointer,
329
+ s.device.mediaQueries.anyPointer,
330
+ s.device.mediaQueries.colorDepth,
331
+ ].map(v => String(v)).join('|');
332
+ const devHash = hashCode(devStr).slice(0, 6);
333
+ const devSection = `${width}x${height}c${cpu}m${mem}b${devBitmask}h${devHash}`;
334
+
335
+ // Section 5: Browser - use existing bitmasks + plugins bitmask + hash
336
+ const featuresBitmask = typeof s.browser.features.bitmask === 'string' ? s.browser.features.bitmask : '0000000000';
337
+ const extensionsBitmask = typeof s.browser.extensions.bitmask === 'string' ? s.browser.extensions.bitmask : '00000000';
338
+ const pluginsBitmask = [
339
+ s.browser.plugins.isValidPluginArray === true,
340
+ s.browser.plugins.pluginConsistency1 === true,
341
+ s.browser.plugins.pluginOverflow === true,
342
+ s.browser.toSourceError.hasToSource === true,
343
+ ].map(b => b ? '1' : '0').join('');
344
+ const brwStr = [
345
+ s.browser.userAgent,
346
+ s.browser.etsl,
347
+ s.browser.maths,
348
+ s.browser.plugins.pluginCount,
349
+ s.browser.plugins.pluginNamesHash,
350
+ s.browser.toSourceError.toSourceError,
351
+ s.browser.highEntropyValues.architecture,
352
+ s.browser.highEntropyValues.bitness,
353
+ s.browser.highEntropyValues.platform,
354
+ s.browser.highEntropyValues.platformVersion,
355
+ s.browser.highEntropyValues.uaFullVersion,
356
+ s.browser.highEntropyValues.mobile,
357
+ ].map(v => String(v)).join('|');
358
+ const brwHash = hashCode(brwStr).slice(0, 6);
359
+ const brwSection = `f${featuresBitmask}e${extensionsBitmask}p${pluginsBitmask}h${brwHash}`;
360
+
361
+ // Section 6: Graphics - bitmask + hash
362
+ const gfxBitmask = [
363
+ s.graphics.canvas.hasModifiedCanvas === true,
364
+ ].map(b => b ? '1' : '0').join('');
365
+ const gfxStr = [
366
+ s.graphics.webGL.vendor,
367
+ s.graphics.webGL.renderer,
368
+ s.graphics.webgpu.vendor,
369
+ s.graphics.webgpu.architecture,
370
+ s.graphics.webgpu.device,
371
+ s.graphics.webgpu.description,
372
+ s.graphics.canvas.canvasFingerprint,
373
+ ].map(v => String(v)).join('|');
374
+ const gfxHash = hashCode(gfxStr).slice(0, 6);
375
+ const gfxSection = `${gfxBitmask}h${gfxHash}`;
376
+
377
+ // Section 7: Codecs - bitmask + hash of all codec hashes
378
+ const codBitmask = [
379
+ s.codecs.hasMediaSource === true,
380
+ ].map(b => b ? '1' : '0').join('');
381
+ const codStr = [
382
+ s.codecs.audioCanPlayTypeHash,
383
+ s.codecs.videoCanPlayTypeHash,
384
+ s.codecs.audioMediaSourceHash,
385
+ s.codecs.videoMediaSourceHash,
386
+ s.codecs.rtcAudioCapabilitiesHash,
387
+ s.codecs.rtcVideoCapabilitiesHash,
388
+ ].map(v => String(v)).join('|');
389
+ const codHash = hashCode(codStr).slice(0, 6);
390
+ const codSection = `${codBitmask}h${codHash}`;
391
+
392
+ // Section 8: Locale - language code + count + timezone + hash
393
+ const primaryLang = typeof s.locale.languages.language === 'string'
394
+ ? s.locale.languages.language.slice(0, 2).toLowerCase()
395
+ : 'xx';
396
+ const langCount = Array.isArray(s.locale.languages.languages) ? s.locale.languages.languages.length : 0;
397
+ // Sanitize timezone: replace / and spaces with - for fingerprint compatibility
398
+ const rawTimezone = typeof s.locale.internationalization.timezone === 'string'
399
+ ? s.locale.internationalization.timezone
400
+ : 'unknown';
401
+ const sanitizedTimezone = rawTimezone.replace(/[\/\s]/g, '-');
402
+ const locStr = [
403
+ s.locale.internationalization.timezone,
404
+ s.locale.internationalization.localeLanguage,
405
+ Array.isArray(s.locale.languages.languages) ? s.locale.languages.languages.join(',') : s.locale.languages.languages,
406
+ s.locale.languages.language,
407
+ ].map(v => String(v)).join('|');
408
+ const locHash = hashCode(locStr).slice(0, 4);
409
+ const locSection = `${primaryLang}${langCount}t${sanitizedTimezone}_h${locHash}`;
410
+
411
+ // Section 9: Contexts - mismatch bitmask + hash of all context signals
412
+ const ctxBitmask = [
413
+ hasContextMismatch(this.fingerprint, 'iframe'),
414
+ hasContextMismatch(this.fingerprint, 'worker'),
415
+ s.contexts.iframe.webdriver === true,
416
+ s.contexts.webWorker.webdriver === true,
417
+ ].map(b => b ? '1' : '0').join('');
418
+ const ctxStr = [
419
+ s.contexts.iframe.userAgent,
420
+ s.contexts.iframe.platform,
421
+ s.contexts.iframe.memory,
422
+ s.contexts.iframe.cpuCount,
423
+ s.contexts.iframe.language,
424
+ s.contexts.webWorker.userAgent,
425
+ s.contexts.webWorker.platform,
426
+ s.contexts.webWorker.memory,
427
+ s.contexts.webWorker.cpuCount,
428
+ s.contexts.webWorker.language,
429
+ s.contexts.webWorker.vendor,
430
+ s.contexts.webWorker.renderer,
431
+ ].map(v => String(v)).join('|');
432
+ const ctxHash = hashCode(ctxStr).slice(0, 6);
433
+ const ctxSection = `${ctxBitmask}h${ctxHash}`;
434
+
435
+ return [
436
+ version,
437
+ detSection,
438
+ autoSection,
439
+ devSection,
440
+ brwSection,
441
+ gfxSection,
442
+ codSection,
443
+ locSection,
444
+ ctxSection,
445
+ ].join('_');
446
+ } catch (e) {
447
+ console.error('Error generating fingerprint scanner id', e);
448
+ return ERROR;
449
+ }
450
+ }
451
+
452
+ private async encryptFingerprint(fingerprint: string) {
453
+ // Key is injected at build time via Vite's define option
454
+ // Customers run: npx fpscanner build --key=their-key
455
+ const key = __FP_ENCRYPTION_KEY__;
456
+ const enc = await encryptString(JSON.stringify(fingerprint), key);
457
+
458
+ return enc;
459
+ }
460
+
461
+ /**
462
+ * Detection rules with name and severity.
463
+ * All rules are currently HIGH severity as they indicate bot-like behavior.
464
+ */
465
+ private getDetectionRules(): DetectionRule[] {
466
+ return [
467
+ { name: 'headlessChromeScreenResolution', severity: HIGH, test: hasHeadlessChromeScreenResolution },
468
+ { name: 'hasWebdriver', severity: HIGH, test: hasWebdriver },
469
+ { name: 'hasWebdriverWritable', severity: HIGH, test: hasWebdriverWritable },
470
+ { name: 'hasSeleniumProperty', severity: HIGH, test: hasSeleniumProperty },
471
+ { name: 'hasCDP', severity: HIGH, test: hasCDP },
472
+ { name: 'hasPlaywright', severity: HIGH, test: hasPlaywright },
473
+ { name: 'hasImpossibleDeviceMemory', severity: HIGH, test: hasImpossibleDeviceMemory },
474
+ { name: 'hasHighCPUCount', severity: HIGH, test: hasHighCPUCount },
475
+ { name: 'hasMissingChromeObject', severity: HIGH, test: hasMissingChromeObject },
476
+ { name: 'hasWebdriverIframe', severity: HIGH, test: hasWebdriverIframe },
477
+ { name: 'hasWebdriverWorker', severity: HIGH, test: hasWebdriverWorker },
478
+ { name: 'hasMismatchWebGLInWorker', severity: HIGH, test: hasMismatchWebGLInWorker },
479
+ { name: 'hasMismatchPlatformIframe', severity: HIGH, test: hasMismatchPlatformIframe },
480
+ { name: 'hasMismatchPlatformWorker', severity: HIGH, test: hasMismatchPlatformWorker },
481
+ { name: 'hasSwiftshaderRenderer', severity: LOW, test: hasSwiftshaderRenderer },
482
+ { name: 'hasUTCTimezone', severity: MEDIUM, test: hasUTCTimezone },
483
+ ];
484
+ }
485
+
486
+ private runDetectionRules(): FastBotDetectionDetails {
487
+ const rules = this.getDetectionRules();
488
+ const results: FastBotDetectionDetails = {
489
+ headlessChromeScreenResolution: { detected: false, severity: 'high' },
490
+ hasWebdriver: { detected: false, severity: 'high' },
491
+ hasWebdriverWritable: { detected: false, severity: 'high' },
492
+ hasSeleniumProperty: { detected: false, severity: 'high' },
493
+ hasCDP: { detected: false, severity: 'high' },
494
+ hasPlaywright: { detected: false, severity: 'high' },
495
+ hasImpossibleDeviceMemory: { detected: false, severity: 'high' },
496
+ hasHighCPUCount: { detected: false, severity: 'high' },
497
+ hasMissingChromeObject: { detected: false, severity: 'high' },
498
+ hasWebdriverIframe: { detected: false, severity: 'high' },
499
+ hasWebdriverWorker: { detected: false, severity: 'high' },
500
+ hasMismatchWebGLInWorker: { detected: false, severity: 'high' },
501
+ hasMismatchPlatformIframe: { detected: false, severity: 'high' },
502
+ hasMismatchPlatformWorker: { detected: false, severity: 'high' },
503
+ hasSwiftshaderRenderer: { detected: false, severity: 'low' },
504
+ hasUTCTimezone: { detected: false, severity: 'medium' },
505
+ };
506
+
507
+ for (const rule of rules) {
508
+ try {
509
+ const detected = rule.test(this.fingerprint);
510
+ (results as any)[rule.name] = { detected, severity: rule.severity };
511
+ } catch (e) {
512
+ (results as any)[rule.name] = { detected: false, severity: rule.severity };
513
+ }
514
+ }
515
+
516
+ return results;
517
+ }
518
+
519
+ async collectFingerprint(options: CollectFingerprintOptions = { encrypt: true }) {
520
+ const { encrypt = true, skipWorker = false } = options;
521
+ const s = this.fingerprint.signals;
522
+
523
+ // Define all signal collection tasks to run in parallel
524
+ const signalTasks = {
525
+ // Automation signals
526
+ webdriver: this.collectSignal(webdriver),
527
+ webdriverWritable: this.collectSignal(webdriverWritable),
528
+ selenium: this.collectSignal(hasSeleniumProperties),
529
+ cdp: this.collectSignal(cdp),
530
+ playwright: this.collectSignal(playwright),
531
+ navigatorPropertyDescriptors: this.collectSignal(navigatorPropertyDescriptors),
532
+ // Device signals
533
+ cpuCount: this.collectSignal(cpuCount),
534
+ memory: this.collectSignal(memory),
535
+ platform: this.collectSignal(platform),
536
+ screenResolution: this.collectSignal(screenResolution),
537
+ multimediaDevices: this.collectSignal(multimediaDevices),
538
+ mediaQueries: this.collectSignal(mediaQueries),
539
+ // Browser signals
540
+ userAgent: this.collectSignal(userAgent),
541
+ browserFeatures: this.collectSignal(browserFeatures),
542
+ plugins: this.collectSignal(plugins),
543
+ browserExtensions: this.collectSignal(browserExtensions),
544
+ highEntropyValues: this.collectSignal(highEntropyValues),
545
+ etsl: this.collectSignal(etsl),
546
+ maths: this.collectSignal(maths),
547
+ toSourceError: this.collectSignal(toSourceError),
548
+ // Graphics signals
549
+ webGL: this.collectSignal(webGL),
550
+ webgpu: this.collectSignal(webgpu),
551
+ canvas: this.collectSignal(canvas),
552
+ // Codecs
553
+ mediaCodecs: this.collectSignal(mediaCodecs),
554
+ // Locale signals
555
+ internationalization: this.collectSignal(internationalization),
556
+ languages: this.collectSignal(languages),
557
+ // Context signals
558
+ iframe: this.collectSignal(iframe),
559
+ webWorker: skipWorker
560
+ ? Promise.resolve({
561
+ webdriver: SKIPPED,
562
+ userAgent: SKIPPED,
563
+ platform: SKIPPED,
564
+ memory: SKIPPED,
565
+ cpuCount: SKIPPED,
566
+ language: SKIPPED,
567
+ vendor: SKIPPED,
568
+ renderer: SKIPPED,
569
+ })
570
+ : this.collectSignal(worker),
571
+ // Meta signals
572
+ nonce: this.collectSignal(nonce),
573
+ time: this.collectSignal(time),
574
+ url: this.collectSignal(pageURL),
575
+ };
576
+
577
+ // Run all signal collections in parallel
578
+ const keys = Object.keys(signalTasks) as (keyof typeof signalTasks)[];
579
+ const results = await Promise.all(Object.values(signalTasks));
580
+ const r = Object.fromEntries(keys.map((key, i) => [key, results[i]])) as Record<keyof typeof signalTasks, any>;
581
+
582
+ // Assign results to fingerprint structure
583
+ // Automation
584
+ s.automation.webdriver = r.webdriver;
585
+ s.automation.webdriverWritable = r.webdriverWritable;
586
+ s.automation.selenium = r.selenium;
587
+ s.automation.cdp = r.cdp;
588
+ s.automation.playwright = r.playwright;
589
+ s.automation.navigatorPropertyDescriptors = r.navigatorPropertyDescriptors;
590
+ // Device
591
+ s.device.cpuCount = r.cpuCount;
592
+ s.device.memory = r.memory;
593
+ s.device.platform = r.platform;
594
+ s.device.screenResolution = r.screenResolution;
595
+ s.device.multimediaDevices = r.multimediaDevices;
596
+ s.device.mediaQueries = r.mediaQueries;
597
+ // Browser
598
+ s.browser.userAgent = r.userAgent;
599
+ s.browser.features = r.browserFeatures;
600
+ s.browser.plugins = r.plugins;
601
+ s.browser.extensions = r.browserExtensions;
602
+ s.browser.highEntropyValues = r.highEntropyValues;
603
+ s.browser.etsl = r.etsl;
604
+ s.browser.maths = r.maths;
605
+ s.browser.toSourceError = r.toSourceError;
606
+ // Graphics
607
+ s.graphics.webGL = r.webGL;
608
+ s.graphics.webgpu = r.webgpu;
609
+ s.graphics.canvas = r.canvas;
610
+ // Codecs
611
+ s.codecs = r.mediaCodecs;
612
+ // Locale
613
+ s.locale.internationalization = r.internationalization;
614
+ s.locale.languages = r.languages;
615
+ // Contexts
616
+ s.contexts.iframe = r.iframe;
617
+ s.contexts.webWorker = r.webWorker;
618
+ // Meta
619
+ this.fingerprint.nonce = r.nonce;
620
+ this.fingerprint.time = r.time;
621
+ this.fingerprint.url = r.url;
622
+
623
+ // Run detection rules (needed for fsid generation)
624
+ this.fingerprint.fastBotDetectionDetails = this.runDetectionRules();
625
+
626
+ // fastBotDetection = true if any detection rule was triggered
627
+ this.fingerprint.fastBotDetection = Object.values(this.fingerprint.fastBotDetectionDetails)
628
+ .some(result => result.detected);
629
+
630
+ // Generate fsid after all signals and detections are collected
631
+ this.fingerprint.fsid = this.generateFingerprintScannerId();
632
+
633
+ if (encrypt) {
634
+ const encryptedFingerprint = await this.encryptFingerprint(JSON.stringify(this.fingerprint));
635
+ return encryptedFingerprint;
636
+ }
637
+
638
+ // Return the raw fingerprint if no encryption is requested
639
+ return this.fingerprint;
640
+ }
641
+ }
642
+
643
+ export default FingerprintScanner;
644
+ export * from './types';