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.
- package/README.md +639 -55
- package/bin/cli.js +216 -0
- package/dist/crypto-helpers.d.ts +19 -0
- package/dist/crypto-helpers.d.ts.map +1 -0
- package/dist/detections/hasCDP.d.ts +3 -0
- package/dist/detections/hasCDP.d.ts.map +1 -0
- package/dist/detections/hasContextMismatch.d.ts +3 -0
- package/dist/detections/hasContextMismatch.d.ts.map +1 -0
- package/dist/detections/hasHeadlessChromeScreenResolution.d.ts +3 -0
- package/dist/detections/hasHeadlessChromeScreenResolution.d.ts.map +1 -0
- package/dist/detections/hasHighCPUCount.d.ts +3 -0
- package/dist/detections/hasHighCPUCount.d.ts.map +1 -0
- package/dist/detections/hasImpossibleDeviceMemory.d.ts +3 -0
- package/dist/detections/hasImpossibleDeviceMemory.d.ts.map +1 -0
- package/dist/detections/hasMismatchPlatformIframe.d.ts +3 -0
- package/dist/detections/hasMismatchPlatformIframe.d.ts.map +1 -0
- package/dist/detections/hasMismatchPlatformWorker.d.ts +3 -0
- package/dist/detections/hasMismatchPlatformWorker.d.ts.map +1 -0
- package/dist/detections/hasMismatchWebGLInWorker.d.ts +3 -0
- package/dist/detections/hasMismatchWebGLInWorker.d.ts.map +1 -0
- package/dist/detections/hasMissingChromeObject.d.ts +3 -0
- package/dist/detections/hasMissingChromeObject.d.ts.map +1 -0
- package/dist/detections/hasPlaywright.d.ts +3 -0
- package/dist/detections/hasPlaywright.d.ts.map +1 -0
- package/dist/detections/hasSeleniumProperty.d.ts +3 -0
- package/dist/detections/hasSeleniumProperty.d.ts.map +1 -0
- package/dist/detections/hasSwiftshaderRenderer.d.ts +3 -0
- package/dist/detections/hasSwiftshaderRenderer.d.ts.map +1 -0
- package/dist/detections/hasUTCTimezone.d.ts +3 -0
- package/dist/detections/hasUTCTimezone.d.ts.map +1 -0
- package/dist/detections/hasWebdriver.d.ts +3 -0
- package/dist/detections/hasWebdriver.d.ts.map +1 -0
- package/dist/detections/hasWebdriverIframe.d.ts +3 -0
- package/dist/detections/hasWebdriverIframe.d.ts.map +1 -0
- package/dist/detections/hasWebdriverWorker.d.ts +3 -0
- package/dist/detections/hasWebdriverWorker.d.ts.map +1 -0
- package/dist/detections/hasWebdriverWritable.d.ts +3 -0
- package/dist/detections/hasWebdriverWritable.d.ts.map +1 -0
- package/dist/fpScanner.cjs.js +31 -0
- package/dist/fpScanner.es.js +1066 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/signals/browserExtensions.d.ts +5 -0
- package/dist/signals/browserExtensions.d.ts.map +1 -0
- package/dist/signals/browserFeatures.d.ts +14 -0
- package/dist/signals/browserFeatures.d.ts.map +1 -0
- package/dist/signals/canvas.d.ts +6 -0
- package/dist/signals/canvas.d.ts.map +1 -0
- package/dist/signals/cdp.d.ts +2 -0
- package/dist/signals/cdp.d.ts.map +1 -0
- package/dist/signals/cpuCount.d.ts +2 -0
- package/dist/signals/cpuCount.d.ts.map +1 -0
- package/dist/signals/etsl.d.ts +2 -0
- package/dist/signals/etsl.d.ts.map +1 -0
- package/dist/signals/highEntropyValues.d.ts +11 -0
- package/dist/signals/highEntropyValues.d.ts.map +1 -0
- package/dist/signals/iframe.d.ts +9 -0
- package/dist/signals/iframe.d.ts.map +1 -0
- package/dist/signals/internationalization.d.ts +5 -0
- package/dist/signals/internationalization.d.ts.map +1 -0
- package/dist/signals/languages.d.ts +5 -0
- package/dist/signals/languages.d.ts.map +1 -0
- package/dist/signals/maths.d.ts +2 -0
- package/dist/signals/maths.d.ts.map +1 -0
- package/dist/signals/mediaCodecs.d.ts +11 -0
- package/dist/signals/mediaCodecs.d.ts.map +1 -0
- package/dist/signals/mediaQueries.d.ts +13 -0
- package/dist/signals/mediaQueries.d.ts.map +1 -0
- package/dist/signals/memory.d.ts +2 -0
- package/dist/signals/memory.d.ts.map +1 -0
- package/dist/signals/multimediaDevices.d.ts +2 -0
- package/dist/signals/multimediaDevices.d.ts.map +1 -0
- package/dist/signals/navigatorPropertyDescriptors.d.ts +2 -0
- package/dist/signals/navigatorPropertyDescriptors.d.ts.map +1 -0
- package/dist/signals/nonce.d.ts +2 -0
- package/dist/signals/nonce.d.ts.map +1 -0
- package/dist/signals/platform.d.ts +2 -0
- package/dist/signals/platform.d.ts.map +1 -0
- package/dist/signals/playwright.d.ts +2 -0
- package/dist/signals/playwright.d.ts.map +1 -0
- package/dist/signals/plugins.d.ts +9 -0
- package/dist/signals/plugins.d.ts.map +1 -0
- package/dist/signals/screenResolution.d.ts +12 -0
- package/dist/signals/screenResolution.d.ts.map +1 -0
- package/dist/signals/seleniumProperties.d.ts +2 -0
- package/dist/signals/seleniumProperties.d.ts.map +1 -0
- package/dist/signals/time.d.ts +2 -0
- package/dist/signals/time.d.ts.map +1 -0
- package/dist/signals/toSourceError.d.ts +5 -0
- package/dist/signals/toSourceError.d.ts.map +1 -0
- package/dist/signals/url.d.ts +2 -0
- package/dist/signals/url.d.ts.map +1 -0
- package/dist/signals/userAgent.d.ts +2 -0
- package/dist/signals/userAgent.d.ts.map +1 -0
- package/dist/signals/utils.d.ts +11 -0
- package/dist/signals/utils.d.ts.map +1 -0
- package/dist/signals/webGL.d.ts +5 -0
- package/dist/signals/webGL.d.ts.map +1 -0
- package/dist/signals/webdriver.d.ts +2 -0
- package/dist/signals/webdriver.d.ts.map +1 -0
- package/dist/signals/webdriverWritable.d.ts +2 -0
- package/dist/signals/webdriverWritable.d.ts.map +1 -0
- package/dist/signals/webgpu.d.ts +7 -0
- package/dist/signals/webgpu.d.ts.map +1 -0
- package/dist/signals/worker.d.ts +2 -0
- package/dist/signals/worker.d.ts.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +58 -15
- package/scripts/build-custom.js +246 -0
- package/src/crypto-helpers.ts +50 -0
- package/src/detections/hasCDP.ts +5 -0
- package/src/detections/hasContextMismatch.ts +19 -0
- package/src/detections/hasHeadlessChromeScreenResolution.ts +10 -0
- package/src/detections/hasHighCPUCount.ts +9 -0
- package/src/detections/hasImpossibleDeviceMemory.ts +9 -0
- package/src/detections/hasMismatchPlatformIframe.ts +10 -0
- package/src/detections/hasMismatchPlatformWorker.ts +10 -0
- package/src/detections/hasMismatchWebGLInWorker.ts +13 -0
- package/src/detections/hasMissingChromeObject.ts +6 -0
- package/src/detections/hasPlaywright.ts +5 -0
- package/src/detections/hasSeleniumProperty.ts +5 -0
- package/src/detections/hasSwiftshaderRenderer.ts +5 -0
- package/src/detections/hasUTCTimezone.ts +5 -0
- package/src/detections/hasWebdriver.ts +5 -0
- package/src/detections/hasWebdriverIframe.ts +5 -0
- package/src/detections/hasWebdriverWorker.ts +5 -0
- package/src/detections/hasWebdriverWritable.ts +5 -0
- package/src/globals.d.ts +10 -0
- package/src/index.ts +644 -0
- package/src/signals/browserExtensions.ts +57 -0
- package/src/signals/browserFeatures.ts +24 -0
- package/src/signals/canvas.ts +84 -0
- package/src/signals/cdp.ts +18 -0
- package/src/signals/cpuCount.ts +5 -0
- package/src/signals/etsl.ts +3 -0
- package/src/signals/highEntropyValues.ts +48 -0
- package/src/signals/iframe.ts +34 -0
- package/src/signals/internationalization.ts +24 -0
- package/src/signals/languages.ts +6 -0
- package/src/signals/maths.ts +30 -0
- package/src/signals/mediaCodecs.ts +120 -0
- package/src/signals/mediaQueries.ts +85 -0
- package/src/signals/memory.ts +5 -0
- package/src/signals/multimediaDevices.ts +34 -0
- package/src/signals/navigatorPropertyDescriptors.ts +17 -0
- package/src/signals/nonce.ts +3 -0
- package/src/signals/platform.ts +3 -0
- package/src/signals/playwright.ts +3 -0
- package/src/signals/plugins.ts +70 -0
- package/src/signals/screenResolution.ts +15 -0
- package/src/signals/seleniumProperties.ts +40 -0
- package/src/signals/time.ts +3 -0
- package/src/signals/toSourceError.ts +27 -0
- package/src/signals/url.ts +3 -0
- package/src/signals/userAgent.ts +3 -0
- package/src/signals/utils.ts +29 -0
- package/src/signals/webGL.ts +28 -0
- package/src/signals/webdriver.ts +3 -0
- package/src/signals/webdriverWritable.ts +15 -0
- package/src/signals/webgpu.ts +28 -0
- package/src/signals/worker.ts +77 -0
- package/src/types.ts +237 -0
- package/.babelrc +0 -3
- package/.travis.yml +0 -17
- package/src/fpScanner.js +0 -222
- package/test/test.html +0 -11
- package/test/test.js +0 -116
package/package.json
CHANGED
|
@@ -1,16 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fpscanner",
|
|
3
|
-
"version": "0.2
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "
|
|
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
|
-
"
|
|
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.
|
|
60
|
+
"ua-parser-js": "^0.7.18",
|
|
61
|
+
"javascript-obfuscator": "^5.1.0",
|
|
62
|
+
"terser": "^5.46.0"
|
|
22
63
|
},
|
|
23
64
|
"devDependencies": {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
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,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 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
|
+
}
|
package/src/globals.d.ts
ADDED
|
@@ -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;
|