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/package.json CHANGED
@@ -1,16 +1,55 @@
1
1
  {
2
2
  "name": "fpscanner",
3
- "version": "0.2.0",
4
- "description": "Detect bots using fingerprinting",
5
- "main": "src/fpScanner.js",
3
+ "version": "0.9.2",
4
+ "description": "A lightweight browser fingerprinting and bot detection library with encryption, obfuscation, and cross-context validation",
5
+ "main": "dist/fpScanner.cjs.js",
6
+ "module": "dist/fpScanner.es.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "fpscanner": "bin/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "src",
14
+ "bin",
15
+ "scripts"
16
+ ],
6
17
  "scripts": {
7
- "test": "./node_modules/.bin/mocha --recursive test"
18
+ "build": "vite build && tsc --emitDeclarationOnly",
19
+ "build:vite": "vite build",
20
+ "build:dev": "node bin/cli.js build --key=dev-key --no-obfuscate",
21
+ "build:prod": "node bin/cli.js build",
22
+ "build:prod:plain": "node bin/cli.js build --no-obfuscate",
23
+ "build:obfuscate": "node bin/cli.js build --key=dev-key",
24
+ "watch": "concurrently \"FP_ENCRYPTION_KEY=dev-key vite build --watch\" \"tsc --watch --emitDeclarationOnly\"",
25
+ "dev": "vite",
26
+ "dev:build": "npm run build:dev && vite",
27
+ "dev:obfuscate": "npm run build:obfuscate && vite",
28
+ "test": "npm run test:playwright",
29
+ "test:vitest": "vitest",
30
+ "test:playwright": "npm run build:obfuscate && npx playwright test",
31
+ "test:playwright:headed": "npm run build:obfuscate && npx playwright test --headed"
8
32
  },
9
33
  "repository": {
10
34
  "type": "git",
11
- "url": "https://github.com/antoinevastel/fpscanner.git"
35
+ "url": "git+https://github.com/antoinevastel/fpscanner.git"
12
36
  },
13
- "keywords": [],
37
+ "keywords": [
38
+ "fingerprinting",
39
+ "browser-fingerprint",
40
+ "bot-detection",
41
+ "automation-detection",
42
+ "fraud-detection",
43
+ "selenium",
44
+ "puppeteer",
45
+ "playwright",
46
+ "webdriver",
47
+ "headless",
48
+ "anti-bot",
49
+ "device-fingerprint",
50
+ "browser-detection",
51
+ "security"
52
+ ],
14
53
  "author": "antoinevastel <antoine.vastel@gmail.com>",
15
54
  "license": "MIT",
16
55
  "bugs": {
@@ -18,16 +57,20 @@
18
57
  },
19
58
  "homepage": "https://github.com/antoinevastel/fpscanner",
20
59
  "dependencies": {
21
- "ua-parser-js": "^0.7.17"
60
+ "ua-parser-js": "^0.7.18",
61
+ "javascript-obfuscator": "^5.1.0",
62
+ "terser": "^5.46.0"
22
63
  },
23
64
  "devDependencies": {
24
- "chai": "^4.1.2",
25
- "fpcollect": "^0.5.0",
26
- "karma": "^2.0.0",
27
- "karma-chai": "^0.1.0",
28
- "karma-chrome-launcher": "^2.2.0",
29
- "karma-mocha": "^1.3.0",
30
- "mocha": "^5.0.4",
31
- "puppeteer": "^1.1.1"
65
+ "@playwright/test": "^1.57.0",
66
+ "@vitest/ui": "^4.0.16",
67
+ "chai": "^4.2.0",
68
+ "concurrently": "^9.2.1",
69
+ "fpcollect": "^1.0.4",
70
+ "mocha": "^5.2.0",
71
+ "puppeteer": "^1.9.0",
72
+ "typescript": "^5.9.3",
73
+ "vite": "^7.3.0",
74
+ "vitest": "^4.0.16"
32
75
  }
33
76
  }
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ /**
8
+ * Build fpscanner with a custom encryption key and optional obfuscation.
9
+ * This script:
10
+ * 1. Runs Vite build with the key injected via environment variable
11
+ * 2. Generates TypeScript declarations
12
+ * 3. Optionally obfuscates the output
13
+ * 4. Minifies with Terser (when obfuscating)
14
+ * 5. Removes source maps (when obfuscating)
15
+ */
16
+ module.exports = async function build(args) {
17
+ // Parse arguments
18
+ const keyArg = args.find(a => a.startsWith('--key='));
19
+ const packageDirArg = args.find(a => a.startsWith('--package-dir='));
20
+ const skipObfuscation = args.includes('--no-obfuscate');
21
+
22
+ if (!keyArg) {
23
+ throw new Error('Missing --key argument');
24
+ }
25
+
26
+ const key = keyArg.split('=').slice(1).join('=');
27
+ const packageDir = packageDirArg
28
+ ? packageDirArg.split('=')[1]
29
+ : path.dirname(__dirname);
30
+
31
+ const distDir = path.join(packageDir, 'dist');
32
+ const files = ['fpScanner.es.js', 'fpScanner.cjs.js'];
33
+ const sentinel = '__DEFAULT_FPSCANNER_KEY__';
34
+
35
+ console.log('');
36
+ console.log('🔨 Building fpscanner with custom key...');
37
+ console.log(` Package: ${packageDir}`);
38
+ console.log(` Output: ${distDir}`);
39
+ console.log(` Obfuscation: ${skipObfuscation ? 'disabled' : 'enabled'}`);
40
+ console.log('');
41
+
42
+ // Check if dist files exist (consumer mode vs development mode)
43
+ const distExists = fs.existsSync(distDir) &&
44
+ files.some(file => fs.existsSync(path.join(distDir, file)));
45
+
46
+ if (!distExists) {
47
+ // Development mode: build from source first
48
+ console.log('📦 Step 0/5: Dist files not found, building from source...');
49
+ try {
50
+ execSync('npm run build:vite', {
51
+ cwd: packageDir,
52
+ stdio: 'inherit',
53
+ env: {
54
+ ...process.env,
55
+ FP_ENCRYPTION_KEY: key,
56
+ },
57
+ });
58
+ console.log('');
59
+ console.log('📝 Generating TypeScript declarations...');
60
+ execSync('npx tsc --emitDeclarationOnly', {
61
+ cwd: packageDir,
62
+ stdio: 'inherit',
63
+ });
64
+ } catch (err) {
65
+ throw new Error('Build from source failed');
66
+ }
67
+ console.log('');
68
+ }
69
+
70
+ // Step 1: Inject encryption key into pre-built dist files
71
+ console.log('📦 Step 1/5: Injecting encryption key...');
72
+
73
+ let keyInjected = false;
74
+ for (const file of files) {
75
+ const filePath = path.join(distDir, file);
76
+
77
+ if (!fs.existsSync(filePath)) {
78
+ console.log(` ⚠️ ${file} not found, skipping`);
79
+ continue;
80
+ }
81
+
82
+ let code = fs.readFileSync(filePath, 'utf8');
83
+
84
+ // Check if sentinel exists
85
+ if (!code.includes(sentinel)) {
86
+ console.log(` ⚠️ ${file} does not contain the default key sentinel, skipping`);
87
+ continue;
88
+ }
89
+
90
+ // Replace all occurrences of the sentinel with the actual key
91
+ const escapedSentinel = sentinel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ code = code.replace(new RegExp(`"${escapedSentinel}"`, 'g'), JSON.stringify(key));
93
+
94
+ fs.writeFileSync(filePath, code);
95
+ keyInjected = true;
96
+ console.log(` ✓ ${file}`);
97
+ }
98
+
99
+ if (!keyInjected) {
100
+ console.log(' ℹ️ Key already injected during build step');
101
+ }
102
+
103
+ // Step 2: Skip TypeScript declarations (already generated)
104
+ console.log('');
105
+ console.log('⏭️ Step 2/5: TypeScript declarations already present, skipping...');
106
+
107
+ // Step 3: Obfuscate (optional)
108
+ if (!skipObfuscation) {
109
+ console.log('');
110
+ console.log('🔒 Step 3/5: Obfuscating output...');
111
+
112
+ let JavaScriptObfuscator;
113
+ try {
114
+ JavaScriptObfuscator = require('javascript-obfuscator');
115
+ } catch (err) {
116
+ console.log(' ⚠️ javascript-obfuscator not installed, skipping obfuscation');
117
+ console.log(' To enable obfuscation, run: npm install --save-dev javascript-obfuscator');
118
+ skipObfuscation = true;
119
+ }
120
+
121
+ if (JavaScriptObfuscator) {
122
+ const files = ['fpScanner.es.js', 'fpScanner.cjs.js'];
123
+
124
+ for (const file of files) {
125
+ const filePath = path.join(distDir, file);
126
+
127
+ if (!fs.existsSync(filePath)) {
128
+ console.log(` ⚠️ ${file} not found, skipping`);
129
+ continue;
130
+ }
131
+
132
+ const code = fs.readFileSync(filePath, 'utf8');
133
+
134
+ const obfuscated = JavaScriptObfuscator.obfuscate(code, {
135
+ compact: true,
136
+ controlFlowFlattening: true,
137
+ controlFlowFlatteningThreshold: 0.4,
138
+ deadCodeInjection: true,
139
+ deadCodeInjectionThreshold: 0.1,
140
+ stringArray: true,
141
+ stringArrayThreshold: 0.95,
142
+ stringArrayEncoding: ['rc4'],
143
+ transformObjectKeys: true,
144
+ unicodeEscapeSequence: false,
145
+ // Preserve functionality
146
+ selfDefending: false,
147
+ disableConsoleOutput: false,
148
+ });
149
+
150
+ fs.writeFileSync(filePath, obfuscated.getObfuscatedCode());
151
+ console.log(` ✓ ${file}`);
152
+ }
153
+
154
+ // Step 4: Minify with Terser
155
+ console.log('');
156
+ console.log('📦 Step 4/5: Minifying with Terser...');
157
+
158
+ let terser;
159
+ try {
160
+ terser = require('terser');
161
+ } catch (err) {
162
+ console.log(' ⚠️ terser not installed, skipping minification');
163
+ console.log(' To enable minification, run: npm install --save-dev terser');
164
+ }
165
+
166
+ if (terser) {
167
+ for (const file of files) {
168
+ const filePath = path.join(distDir, file);
169
+
170
+ if (!fs.existsSync(filePath)) {
171
+ continue;
172
+ }
173
+
174
+ const code = fs.readFileSync(filePath, 'utf8');
175
+ const minified = await terser.minify(code, {
176
+ compress: {
177
+ drop_console: false,
178
+ dead_code: true,
179
+ unused: true,
180
+ },
181
+ mangle: {
182
+ toplevel: true,
183
+ },
184
+ format: {
185
+ comments: false,
186
+ },
187
+ });
188
+
189
+ if (minified.error) {
190
+ console.log(` ⚠️ Failed to minify ${file}: ${minified.error}`);
191
+ } else {
192
+ fs.writeFileSync(filePath, minified.code);
193
+ console.log(` ✓ ${file}`);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Step 5: Delete all source map files so DevTools can't show original source
199
+ console.log('');
200
+ console.log('🗑️ Step 5/5: Removing source maps...');
201
+
202
+ function deleteMapFiles(dir, prefix = '') {
203
+ if (!fs.existsSync(dir)) {
204
+ return;
205
+ }
206
+ const files = fs.readdirSync(dir);
207
+ for (const file of files) {
208
+ const fullPath = path.join(dir, file);
209
+ const stat = fs.statSync(fullPath);
210
+
211
+ if (stat.isDirectory()) {
212
+ deleteMapFiles(fullPath, prefix + file + '/');
213
+ } else if (file.endsWith('.map')) {
214
+ fs.unlinkSync(fullPath);
215
+ console.log(` ✓ Deleted ${prefix}${file}`);
216
+ }
217
+ }
218
+ }
219
+
220
+ deleteMapFiles(distDir);
221
+ }
222
+ } else {
223
+ console.log('');
224
+ console.log('⏭️ Steps 3-5/5: Skipping obfuscation, minification, and source map removal (--no-obfuscate)');
225
+ }
226
+
227
+ console.log('');
228
+ console.log('✅ Build complete!');
229
+ console.log('');
230
+ console.log(' Your custom fpscanner build is ready in:');
231
+ console.log(` ${distDir}`);
232
+ console.log('');
233
+ console.log(' Import it in your code:');
234
+ console.log(" import FingerprintScanner from 'fpscanner';");
235
+ console.log('');
236
+ };
237
+
238
+ // Allow running directly
239
+ if (require.main === module) {
240
+ const args = process.argv.slice(2);
241
+ module.exports(args)
242
+ .catch((err) => {
243
+ console.error('❌ Build failed:', err.message);
244
+ process.exit(1);
245
+ });
246
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Simple and fast XOR-based encryption/decryption
3
+ * Note: This is NOT cryptographically secure - use only for obfuscation
4
+ */
5
+
6
+ /**
7
+ * Encrypts a string using XOR cipher with the provided key
8
+ * @param plaintext - The string to encrypt
9
+ * @param key - The encryption key as a string
10
+ * @returns Encrypted string (base64 encoded)
11
+ */
12
+ export async function encryptString(plaintext: string, key: string): Promise<string> {
13
+ const keyBytes = new TextEncoder().encode(key);
14
+ const textBytes = new TextEncoder().encode(plaintext);
15
+ const encrypted = new Uint8Array(textBytes.length);
16
+
17
+ for (let i = 0; i < textBytes.length; i++) {
18
+ encrypted[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
19
+ }
20
+
21
+ // Convert to base64 for safe string representation
22
+ const binaryString = String.fromCharCode(...encrypted);
23
+ return btoa(binaryString);
24
+ }
25
+
26
+ /**
27
+ * Decrypts a string that was encrypted with encryptString
28
+ * @param ciphertext - The encrypted string (base64 encoded)
29
+ * @param key - The decryption key as a string (must match encryption key)
30
+ * @returns Decrypted string
31
+ */
32
+ export async function decryptString(ciphertext: string, key: string): Promise<string> {
33
+ // Decode from base64
34
+ const binaryString = atob(ciphertext);
35
+ const encrypted = new Uint8Array(binaryString.length);
36
+ for (let i = 0; i < binaryString.length; i++) {
37
+ encrypted[i] = binaryString.charCodeAt(i);
38
+ }
39
+
40
+ const keyBytes = new TextEncoder().encode(key);
41
+ const decrypted = new Uint8Array(encrypted.length);
42
+
43
+ // XOR is symmetric, so decryption is the same as encryption
44
+ for (let i = 0; i < encrypted.length; i++) {
45
+ decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];
46
+ }
47
+
48
+ return new TextDecoder().decode(decrypted);
49
+ }
50
+
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasCDP(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.automation.cdp === true;
5
+ }
@@ -0,0 +1,19 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ // Not used as a detection rule since, more like an indicator
4
+ export function hasContextMismatch(fingerprint: Fingerprint, context: 'iframe' | 'worker'): boolean {
5
+ const s = fingerprint.signals;
6
+ if (context === 'iframe') {
7
+ return s.contexts.iframe.webdriver !== s.automation.webdriver ||
8
+ s.contexts.iframe.userAgent !== s.browser.userAgent ||
9
+ s.contexts.iframe.platform !== s.device.platform ||
10
+ s.contexts.iframe.memory !== s.device.memory ||
11
+ s.contexts.iframe.cpuCount !== s.device.cpuCount;
12
+ } else {
13
+ return s.contexts.webWorker.webdriver !== s.automation.webdriver ||
14
+ s.contexts.webWorker.userAgent !== s.browser.userAgent ||
15
+ s.contexts.webWorker.platform !== s.device.platform ||
16
+ s.contexts.webWorker.memory !== s.device.memory ||
17
+ s.contexts.webWorker.cpuCount !== s.device.cpuCount;
18
+ }
19
+ }
@@ -0,0 +1,10 @@
1
+ import { Fingerprint } from '../types';
2
+
3
+ export function hasHeadlessChromeScreenResolution(fingerprint: Fingerprint) {
4
+ const screen = fingerprint.signals.device.screenResolution;
5
+ if (typeof screen.width !== 'number' || typeof screen.height !== 'number') {
6
+ return false;
7
+ }
8
+
9
+ return (screen.width === 600 && screen.height === 800) || (screen.availableWidth === 600 && screen.availableHeight === 800) || (screen.innerWidth === 600 && screen.innerHeight === 800);
10
+ }
@@ -0,0 +1,9 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasHighCPUCount(fingerprint: Fingerprint) {
4
+ if (typeof fingerprint.signals.device.cpuCount !== 'number') {
5
+ return false;
6
+ }
7
+
8
+ return fingerprint.signals.device.cpuCount > 70;
9
+ }
@@ -0,0 +1,9 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasImpossibleDeviceMemory(fingerprint: Fingerprint) {
4
+ if (typeof fingerprint.signals.device.memory !== 'number') {
5
+ return false;
6
+ }
7
+
8
+ return (fingerprint.signals.device.memory > 8 || fingerprint.signals.device.memory < 0.25);
9
+ }
@@ -0,0 +1,10 @@
1
+ import { Fingerprint } from "../types";
2
+ import { ERROR, NA } from "../signals/utils";
3
+
4
+ export function hasMismatchPlatformIframe(fingerprint: Fingerprint) {
5
+ if (fingerprint.signals.contexts.iframe.platform === NA || fingerprint.signals.contexts.iframe.platform === ERROR) {
6
+ return false;
7
+ }
8
+
9
+ return fingerprint.signals.device.platform !== fingerprint.signals.contexts.iframe.platform;
10
+ }
@@ -0,0 +1,10 @@
1
+ import { Fingerprint } from "../types";
2
+ import { ERROR, NA, SKIPPED } from "../signals/utils";
3
+
4
+ export function hasMismatchPlatformWorker(fingerprint: Fingerprint) {
5
+ if (fingerprint.signals.contexts.webWorker.platform === NA || fingerprint.signals.contexts.webWorker.platform === ERROR || fingerprint.signals.contexts.webWorker.platform === SKIPPED) {
6
+ return false;
7
+ }
8
+
9
+ return fingerprint.signals.device.platform !== fingerprint.signals.contexts.webWorker.platform;
10
+ }
@@ -0,0 +1,13 @@
1
+ import { Fingerprint } from "../types";
2
+ import { ERROR, NA, SKIPPED } from "../signals/utils";
3
+
4
+ export function hasMismatchWebGLInWorker(fingerprint: Fingerprint) {
5
+ const worker = fingerprint.signals.contexts.webWorker;
6
+ const webGL = fingerprint.signals.graphics.webGL;
7
+
8
+ if (worker.vendor === ERROR || worker.renderer === ERROR || webGL.vendor === NA || webGL.renderer === NA || worker.vendor === SKIPPED) {
9
+ return false;
10
+ }
11
+
12
+ return worker.vendor !== webGL.vendor || worker.renderer !== webGL.renderer;
13
+ }
@@ -0,0 +1,6 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasMissingChromeObject(fingerprint: Fingerprint) {
4
+ const userAgent = fingerprint.signals.browser.userAgent;
5
+ return fingerprint.signals.browser.features.chrome === false && typeof userAgent === 'string' && userAgent.includes('Chrome');
6
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasPlaywright(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.automation.playwright === true;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasSeleniumProperty(fingerprint: Fingerprint) {
4
+ return !!fingerprint.signals.automation.selenium;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasSwiftshaderRenderer(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.graphics.webGL.renderer.includes('SwiftShader');
5
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasUTCTimezone(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.locale.internationalization.timezone === 'UTC';
5
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasWebdriver(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.automation.webdriver === true;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasWebdriverIframe(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.contexts.iframe.webdriver === true;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasWebdriverWorker(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.contexts.webWorker.webdriver === true;
5
+ }
@@ -0,0 +1,5 @@
1
+ import { Fingerprint } from "../types";
2
+
3
+ export function hasWebdriverWritable(fingerprint: Fingerprint) {
4
+ return fingerprint.signals.automation.webdriverWritable === true;
5
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Build-time constant injected via Vite's define option.
3
+ * This is replaced with the actual encryption key during the build process.
4
+ *
5
+ * Customers provide their key via:
6
+ * - npx fpscanner build --key=their-key
7
+ * - FINGERPRINT_KEY environment variable
8
+ * - .env file with FINGERPRINT_KEY=their-key
9
+ */
10
+ declare const __FP_ENCRYPTION_KEY__: string;