@anonympins/fingerprint 0.5.2 → 0.5.5

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 (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/package.json +1 -1
  3. package/src/js/build-client.js +67 -67
  4. package/src/js/fingerprint.client.js +280 -38
  5. package/src/js/fingerprint.client.obfuscated.js +1 -1
  6. package/src/js/fingerprint.js +474 -163
  7. package/src/js/fingerprint.utils.js +213 -213
  8. package/src/js/library.js +24 -8
  9. package/src/js/mongodb-store.js +165 -79
  10. package/src/js/obfuscation.worker.js +23 -0
  11. package/src/js/pow.solver.inline.js +509 -14
  12. package/src/js/pow.solver.js +350 -32
  13. package/src/js/problem-manager.js +1 -1
  14. package/src/js/redis-store.js +114 -42
  15. package/src/js/tests/cross_parity.test.js +175 -0
  16. package/src/js/tests/dns-circuit-breaker.test.js +55 -0
  17. package/src/js/tests/fingerprint.client.init.test.js +143 -143
  18. package/src/js/tests/fingerprint.client.test.js +128 -128
  19. package/src/js/tests/fingerprint.test.js +57 -2
  20. package/src/js/tests/ip-reputation.test.js +1 -1
  21. package/src/js/tests/obfuscation.test.js +48 -0
  22. package/src/js/tests/pow.solver.test.js +5 -2
  23. package/src/js/tests/usefulWorkSolver.test.js +162 -0
  24. package/src/php/AutoTuner.php +42 -0
  25. package/src/php/Challenge/ChallengeUtils.php +21 -8
  26. package/src/php/FingerprintClient.php +22 -5
  27. package/src/php/FingerprintEngine.php +233 -57
  28. package/src/php/Optimization/Optimization.php +23 -16
  29. package/src/php/Optimization/OptimizationOperators.php +27 -0
  30. package/src/php/ProblemManager.php +1 -1
  31. package/src/php/Store/MongoDbStore.php +184 -104
  32. package/src/php/Store/RedisStore.php +91 -11
  33. package/src/php/Tests/ChallengeUtilsTest.php +285 -285
  34. package/src/php/Tests/DnsCircuitBreakerTest.php +106 -0
  35. package/src/php/Tests/IpReputationTest.php +1 -1
  36. package/src/php/Tests/PowTest.php +1 -1
  37. package/src/php/Tests/RequestUtilsTest.php +29 -0
  38. package/src/php/Tests/TLSClientHelloParserTest.php +177 -117
  39. package/src/php/Utils/BigInt.php +202 -202
  40. package/src/php/Utils/RequestUtils.php +74 -21
  41. package/src/php/Utils/TLSClientHelloParser.php +212 -116
@@ -1,129 +1,129 @@
1
- /**
2
- * @vitest-environment jsdom
3
- */
4
-
5
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
6
- import ClientLibrary from '../fingerprint.client.js';
7
-
8
- describe('ClientLibrary WASM Integration', () => {
9
- beforeEach(() => {
10
- // Réinitialise le cache et les mocks avant chaque test
11
- ClientLibrary._resetCache();
12
- vi.spyOn(console, 'log').mockImplementation(() => {});
13
- vi.spyOn(console, 'warn').mockImplementation(() => {});
14
-
15
- // Supprime les mocks globaux pour éviter les fuites entre les tests
16
- delete window.createFingerprintModule;
17
- });
18
-
19
- afterEach(() => {
20
- vi.restoreAllMocks();
21
- });
22
-
23
- it('should successfully load WASM module and switch hasher', async () => {
24
- // 1. Simuler un module WASM fonctionnel
25
- const mockWasmModule = {
26
- _hash_string: vi.fn((str) => 99999), // Un mock qui retourne une valeur distincte
27
- };
28
-
29
- // 2. Simuler le chargement du script et l'initialisation du module
30
- // On attache les fonctions de simulation à `window` car c'est ce que le code cherche
31
- window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
32
-
33
- // On simule l'injection du script en appelant directement `onload`
34
- vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
35
- // Simule le chargement réussi du script
36
- script.onload();
37
- return script;
38
- });
39
-
40
- // 3. Appeler la fonction d'initialisation
41
- await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
42
-
43
- // 4. Vérifier que le hasher a été remplacé
44
- const wasmHash = ClientLibrary._hasher("test");
45
- expect(wasmHash).toBe(99999);
46
- expect(mockWasmModule._hash_string).toHaveBeenCalledWith("test");
47
- expect(console.log).toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
48
- });
49
-
50
- it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
51
- // 1. Simuler un échec de chargement du script
52
- vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
53
- script.onerror(new Error('Script loading failed'));
54
- return script;
55
- });
56
-
57
- // 2. Appeler la fonction d'initialisation
58
- await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
59
-
60
- // 3. Vérifier que le hasher est toujours l'implémentation JS
61
- const jsHash = ClientLibrary._hasher("test");
62
- const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
63
-
64
- expect(jsHash).toBe(originalJsHash);
65
- expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'), expect.any(Error));
66
- });
67
-
68
- it('should gracefully fall back if WASM module does not export _hash_string', async () => {
69
- // 1. Simuler un module WASM malformé (sans la fonction attendue)
70
- const mockWasmModule = {};
71
- window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
72
-
73
- vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
74
- script.onload();
75
- return script;
76
- });
77
-
78
- // 2. Appeler la fonction d'initialisation
79
- await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
80
-
81
- // 3. Vérifier le fallback
82
- const jsHash = ClientLibrary._hasher("test");
83
- const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
84
- expect(jsHash).toBe(originalJsHash);
85
- // L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
86
- expect(console.warn).toHaveBeenCalledWith(
87
- expect.stringContaining('WASM module failed to load'),
88
- expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
89
- );
90
- });
91
-
92
- it('should successfully load raw WASM and use standalone polymorphic hasher', async () => {
93
- const mockWasmInstance = {
94
- exports: {
95
- memory: { buffer: new ArrayBuffer(65536) },
96
- hash: vi.fn().mockReturnValue(123456)
97
- }
98
- };
99
-
100
- global.WebAssembly = {
101
- compile: vi.fn().mockResolvedValue({}),
102
- instantiate: vi.fn().mockResolvedValue(mockWasmInstance)
103
- };
104
-
105
- global.fetch = vi.fn().mockResolvedValue({
106
- arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(100))
107
- });
108
-
109
- await ClientLibrary.initializeWasm('/fp.wasm');
110
-
111
- const hash = ClientLibrary._hasher("test");
112
- expect(hash).toBe(123456);
113
- expect(mockWasmInstance.exports.hash).toHaveBeenCalledWith(0, 4);
114
- });
115
-
116
- it('should use the default JS hasher if WASM is not configured', async () => {
117
- // Ensure no WASM path is provided in the config
118
- ClientLibrary.initializeClient({});
119
-
120
- // Verify that the active hasher is the original JS implementation
121
- const jsHash = ClientLibrary._hasher("another test string");
122
- const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("another test string");
123
-
124
- expect(jsHash).toBe(originalJsHash);
125
- // Ensure no WASM-related console logs or warnings were made
126
- expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
127
- expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'));
128
- });
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+
5
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
6
+ import ClientLibrary from '../fingerprint.client.js';
7
+
8
+ describe('ClientLibrary WASM Integration', () => {
9
+ beforeEach(() => {
10
+ // Réinitialise le cache et les mocks avant chaque test
11
+ ClientLibrary._resetCache();
12
+ vi.spyOn(console, 'log').mockImplementation(() => {});
13
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
14
+
15
+ // Supprime les mocks globaux pour éviter les fuites entre les tests
16
+ delete window.createFingerprintModule;
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.restoreAllMocks();
21
+ });
22
+
23
+ it('should successfully load WASM module and switch hasher', async () => {
24
+ // 1. Simuler un module WASM fonctionnel
25
+ const mockWasmModule = {
26
+ _hash_string: vi.fn((str) => 99999), // Un mock qui retourne une valeur distincte
27
+ };
28
+
29
+ // 2. Simuler le chargement du script et l'initialisation du module
30
+ // On attache les fonctions de simulation à `window` car c'est ce que le code cherche
31
+ window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
32
+
33
+ // On simule l'injection du script en appelant directement `onload`
34
+ vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
35
+ // Simule le chargement réussi du script
36
+ script.onload();
37
+ return script;
38
+ });
39
+
40
+ // 3. Appeler la fonction d'initialisation
41
+ await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
42
+
43
+ // 4. Vérifier que le hasher a été remplacé
44
+ const wasmHash = ClientLibrary._hasher("test");
45
+ expect(wasmHash).toBe(99999);
46
+ expect(mockWasmModule._hash_string).toHaveBeenCalledWith("test");
47
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
48
+ });
49
+
50
+ it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
51
+ // 1. Simuler un échec de chargement du script
52
+ vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
53
+ script.onerror(new Error('Script loading failed'));
54
+ return script;
55
+ });
56
+
57
+ // 2. Appeler la fonction d'initialisation
58
+ await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
59
+
60
+ // 3. Vérifier que le hasher est toujours l'implémentation JS
61
+ const jsHash = ClientLibrary._hasher("test");
62
+ const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
63
+
64
+ expect(jsHash).toBe(originalJsHash);
65
+ expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'), expect.any(Error));
66
+ });
67
+
68
+ it('should gracefully fall back if WASM module does not export _hash_string', async () => {
69
+ // 1. Simuler un module WASM malformé (sans la fonction attendue)
70
+ const mockWasmModule = {};
71
+ window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
72
+
73
+ vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
74
+ script.onload();
75
+ return script;
76
+ });
77
+
78
+ // 2. Appeler la fonction d'initialisation
79
+ await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
80
+
81
+ // 3. Vérifier le fallback
82
+ const jsHash = ClientLibrary._hasher("test");
83
+ const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
84
+ expect(jsHash).toBe(originalJsHash);
85
+ // L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
86
+ expect(console.warn).toHaveBeenCalledWith(
87
+ expect.stringContaining('WASM module failed to load'),
88
+ expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
89
+ );
90
+ });
91
+
92
+ it('should successfully load raw WASM and use standalone polymorphic hasher', async () => {
93
+ const mockWasmInstance = {
94
+ exports: {
95
+ memory: { buffer: new ArrayBuffer(65536) },
96
+ hash: vi.fn().mockReturnValue(123456)
97
+ }
98
+ };
99
+
100
+ global.WebAssembly = {
101
+ compile: vi.fn().mockResolvedValue({}),
102
+ instantiate: vi.fn().mockResolvedValue(mockWasmInstance)
103
+ };
104
+
105
+ global.fetch = vi.fn().mockResolvedValue({
106
+ arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(100))
107
+ });
108
+
109
+ await ClientLibrary.initializeWasm('/fp.wasm');
110
+
111
+ const hash = ClientLibrary._hasher("test");
112
+ expect(hash).toBe(123456);
113
+ expect(mockWasmInstance.exports.hash).toHaveBeenCalledWith(0, 4);
114
+ });
115
+
116
+ it('should use the default JS hasher if WASM is not configured', async () => {
117
+ // Ensure no WASM path is provided in the config
118
+ ClientLibrary.initializeClient({});
119
+
120
+ // Verify that the active hasher is the original JS implementation
121
+ const jsHash = ClientLibrary._hasher("another test string");
122
+ const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("another test string");
123
+
124
+ expect(jsHash).toBe(originalJsHash);
125
+ // Ensure no WASM-related console logs or warnings were made
126
+ expect(console.log).not.toHaveBeenCalledWith(expect.stringContaining('WASM module loaded successfully'));
127
+ expect(console.warn).not.toHaveBeenCalledWith(expect.stringContaining('WASM module failed to load'));
128
+ });
129
129
  });
@@ -31,6 +31,7 @@ const {
31
31
  default_whitelist,
32
32
  stopThresholdAutoTuning,
33
33
  getCompositeDeviceHash,
34
+ sanitizeTrafficData,
34
35
  } = fingerprint;
35
36
  const { store, getRequestPatternScore, getDeviceHash } = __internal;
36
37
  const { getRenderingAnomalyScore } = __internal;
@@ -1083,6 +1084,35 @@ describe('Fingerprint & PoW Security Suite', () => {
1083
1084
  describe('Threshold Auto-Tuning', () => {
1084
1085
  let setIntervalSpy, clearIntervalSpy, consoleLogSpy;
1085
1086
 
1087
+ test('should sanitize traffic data based on stable hardware cluster', async () => {
1088
+ const trafficData = [];
1089
+ // Attacker rotating IPs and deviceIds but having the same hardware fingerprint
1090
+ for (let i = 0; i < 100; i++) {
1091
+ trafficData.push({
1092
+ type: 'trap_triggered',
1093
+ deviceId: `attacker-dev-${i}`,
1094
+ clientIp: `192.168.1.${i}`,
1095
+ fingerprint: 'gpu:spoofed-nvidia-gpu|cvs:spoofed-canvas-data'
1096
+ });
1097
+ }
1098
+
1099
+ // Unique legit users
1100
+ for (let i = 0; i < 10; i++) {
1101
+ trafficData.push({
1102
+ type: 'challenge_solved',
1103
+ deviceId: `legit-dev-${i}`,
1104
+ clientIp: `10.0.0.${i}`,
1105
+ fingerprint: `gpu:legit-gpu-${i}|cvs:legit-canvas-${i}`
1106
+ });
1107
+ }
1108
+
1109
+ const sanitized = sanitizeTrafficData(trafficData);
1110
+ const attackerLogs = sanitized.filter(log => log.deviceId.startsWith('attacker-dev-'));
1111
+
1112
+ // Total 110 logs. 2% limit = Math.max(3, 2) = 3.
1113
+ expect(attackerLogs.length).toBeLessThanOrEqual(3);
1114
+ });
1115
+
1086
1116
  beforeEach(() => {
1087
1117
  setIntervalSpy = vi.spyOn(global, 'setInterval');
1088
1118
  clearIntervalSpy = vi.spyOn(global, 'clearInterval');
@@ -2045,6 +2075,31 @@ describe('getTimeInconsistencyScore', () => {
2045
2075
  expect(timeInconsistencyScore).toBe(0);
2046
2076
  });
2047
2077
 
2078
+ it('should return 100 if the signature is invalid or missing when sessionHmacKey is negotiated', () => {
2079
+ const requestTimestamp = Date.now();
2080
+ const clientTimestamp = requestTimestamp - 100;
2081
+ const context = { requestTimestamp };
2082
+ const metrics = { clientTimestamp, signature: 'bad-signature' };
2083
+ const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
2084
+
2085
+ const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
2086
+ expect(timeInconsistencyScore).toBe(100);
2087
+ });
2088
+
2089
+ it('should return 0 if the signature is valid when sessionHmacKey is negotiated', () => {
2090
+ const requestTimestamp = Date.now();
2091
+ const clientTimestamp = requestTimestamp - 100;
2092
+ const context = { requestTimestamp };
2093
+ const deviceData = { sessionHmacKey: '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef' };
2094
+ const metrics = { clientTimestamp };
2095
+ const dataToSign = JSON.stringify(metrics);
2096
+ const signature = createHmac('sha256', Buffer.from(deviceData.sessionHmacKey, 'hex')).update(dataToSign).digest('hex');
2097
+ metrics.signature = signature;
2098
+
2099
+ const { timeInconsistencyScore } = __internal.getTimeInconsistencyScore(context, metrics, deviceData);
2100
+ expect(timeInconsistencyScore).toBe(0);
2101
+ });
2102
+
2048
2103
  it('should return a score > 0 for a replayed request (clientTimestamp significantly before requestTimestamp)', () => {
2049
2104
  const requestTimestamp = Date.now();
2050
2105
  const clientTimestamp = requestTimestamp - (REPLAY_THRESHOLD_MS + 1000); // 1 second beyond threshold
@@ -2305,9 +2360,9 @@ describe('Subnet Scoring (Node.js)', () => {
2305
2360
  // The new version handles other prefixes
2306
2361
  expect(getIpSubnet('10.20.30.40', 16)).toBe('10.20.0.0/16');
2307
2362
  // IPv6
2308
- expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:db8:abcd:0:0:0:0:0/48');
2363
+ expect(getIpSubnet('2001:db8:abcd:0012:0000:0000:0000:0001', 48)).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
2309
2364
  // The new version handles other prefixes
2310
- expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:e0a:129:0:0:0:0:0/48');
2365
+ expect(getIpSubnet('2a01:e0a:129:57c0:a1b2:c3d4:e5f6:a7b8', 64)).toBe('2a01:0e0a:0129:0000:0000:0000:0000:0000/48');
2311
2366
  // Invalid IPs
2312
2367
  expect(getIpSubnet('not-an-ip')).toBeNull();
2313
2368
  });
@@ -96,7 +96,7 @@ describe('IP Reputation Local System (Node.js)', () => {
96
96
  expect(ipv4Subnet).toBe('192.168.1.0/24');
97
97
 
98
98
  const ipv6Subnet = __internal.getIpSubnet('2001:db8:abcd:12::1', 24, 48);
99
- expect(ipv6Subnet).toBe('2001:db8:abcd:0:0:0:0:0/48');
99
+ expect(ipv6Subnet).toBe('2001:0db8:abcd:0000:0000:0000:0000:0000/48');
100
100
  });
101
101
 
102
102
  it('should calculate client hints inconsistency score correctly', async () => {
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __internal } from '../fingerprint.js';
3
+
4
+ describe('Système de Polymorphisme et Compilateur d\'Obfuscation', () => {
5
+
6
+ it('should generate a valid session mapping with randomized headers and globals', () => {
7
+ const mapping1 = __internal.generateSessionMapping();
8
+ const mapping2 = __internal.generateSessionMapping();
9
+
10
+ // Vérification de la présence des structures attendues
11
+ expect(mapping1).toBeDefined();
12
+ expect(mapping1.headers).toHaveProperty('x-device-fingerprint');
13
+ expect(mapping1.headers).toHaveProperty('x-behavior-metrics');
14
+ expect(mapping1.globals).toHaveProperty('ClientLibrary');
15
+ expect(mapping1.globals).toHaveProperty('getDeviceFingerprint');
16
+ expect(mapping1.globals).toHaveProperty('getClientBehaviorMetrics');
17
+ expect(mapping1.keys).toHaveProperty('ua');
18
+ expect(mapping1.wasmConstants).toHaveProperty('seed');
19
+
20
+ // Vérification de l'aléa (deux sessions consécutives doivent avoir des mappings uniques)
21
+ expect(mapping1.headers['x-device-fingerprint']).not.toBe(mapping2.headers['x-device-fingerprint']);
22
+ expect(mapping1.globals['ClientLibrary']).not.toBe(mapping2.globals['ClientLibrary']);
23
+ expect(mapping1.keys['ua']).not.toBe(mapping2.keys['ua']);
24
+ expect(mapping1.wasmConstants.seed).not.toBe(mapping2.wasmConstants.seed);
25
+ });
26
+
27
+ it('should compile and obfuscate client script asynchronously using the obfuscation worker', async () => {
28
+ const mapping = __internal.generateSessionMapping();
29
+
30
+ // On lance la compilation lourde qui instancie le Worker d'arrière-plan
31
+ const obfuscatedCode = await __internal.compilePolymorphicJs(mapping);
32
+
33
+ expect(obfuscatedCode).toBeDefined();
34
+ expect(typeof obfuscatedCode).toBe('string');
35
+ expect(obfuscatedCode.length).toBeGreaterThan(0);
36
+
37
+ // Vérification que les placeholders originaux ont bien été purgés du code client final
38
+ expect(obfuscatedCode).not.toContain('X-Device-Fingerprint');
39
+ expect(obfuscatedCode).not.toContain('X-Behavior-Metrics');
40
+ expect(obfuscatedCode).not.toContain('ClientLibrary');
41
+
42
+ // Vérification que le code est minifié/obfusqué (ne contient pas de commentaires de développement par exemple)
43
+ expect(obfuscatedCode).not.toContain('// Nom trompeur aléatoire pour attirer les analyseurs');
44
+
45
+ // La structure globale de javascript-obfuscator génère typiquement un tableau de chaînes au début
46
+ expect(obfuscatedCode).toMatch(/_0x/);
47
+ }, 15000); // Augmentation du timeout à 15s car l'obfuscation via Worker thread consomme du CPU
48
+ });
@@ -223,9 +223,12 @@ describe('Proof-of-Work Solvers', () => {
223
223
  const seed = 'test-wasm-seed';
224
224
  const difficulty = 4; // 4 MB
225
225
 
226
- const solution = await solveMemory(seed, difficulty);
226
+ const result = await solveMemory(seed, difficulty);
227
227
 
228
- expect(solution).toBe(777);
228
+ expect(result).toBeInstanceOf(Object);
229
+ expect(result.solution).toBe(777);
230
+ expect(result.merkleRoot).toBeDefined();
231
+ expect(result.proofs).toBeDefined();
229
232
  expect(mockWasmModule._malloc).toHaveBeenCalledTimes(1);
230
233
  expect(mockWasmModule._free).toHaveBeenCalledTimes(1);
231
234
  expect(mockWasmModule._solve_memory_challenge).toHaveBeenCalledWith(1234, difficulty);
@@ -0,0 +1,162 @@
1
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2
+ import { solveChallenge } from '../pow.solver.js'; // Assuming pow.solver.js is the ES module version
3
+
4
+ // Mock crypto.subtle for hashing operations
5
+ const mockSubtleDigest = vi.fn(async (algorithm, data) => {
6
+ // Return a consistent mock hash for testing purposes
7
+ const hash = new Uint8Array(32); // 32 bytes for SHA-256
8
+ for (let i = 0; i < hash.length; i++) {
9
+ hash[i] = i; // Simple predictable hash
10
+ }
11
+ return hash.buffer;
12
+ });
13
+
14
+ // Mock Math.random for deterministic optimization results
15
+ const mockMathRandom = vi.fn(() => 0.5);
16
+
17
+ // Mock IndexedDB for pospace challenges, even if not directly used by this test
18
+ const mockIndexedDB = {
19
+ open: vi.fn(() => ({
20
+ onupgradeneeded: null,
21
+ onsuccess: null,
22
+ onerror: null,
23
+ result: {
24
+ objectStoreNames: { contains: vi.fn(() => true) },
25
+ createObjectStore: vi.fn(),
26
+ transaction: vi.fn(() => ({
27
+ objectStore: vi.fn(() => ({
28
+ put: vi.fn(),
29
+ get: vi.fn(() => ({ onsuccess: null, result: null }))
30
+ }))
31
+ }))
32
+ }
33
+ }))
34
+ };
35
+
36
+ describe('Useful Work Solver for Optimization Challenges', () => {
37
+ let originalMathRandom;
38
+ let originalIndexedDB;
39
+
40
+ beforeEach(() => {
41
+ // Save original implementations
42
+ originalMathRandom = global.Math.random;
43
+ originalIndexedDB = global.indexedDB;
44
+
45
+ // Mock global objects
46
+ vi.stubGlobal('crypto', {
47
+ subtle: {
48
+ digest: mockSubtleDigest,
49
+ },
50
+ randomInt: vi.fn((min, max) => Math.floor(mockMathRandom() * (max - min) + min)), // Mock randomInt for crypto
51
+ randomBytes: vi.fn((length) => new Uint8Array(length).fill(0x01)), // Mock randomBytes for crypto
52
+ });
53
+ global.Math.random = mockMathRandom;
54
+ global.indexedDB = mockIndexedDB;
55
+
56
+ // Reset mocks
57
+ mockSubtleDigest.mockClear();
58
+ mockMathRandom.mockClear();
59
+ mockIndexedDB.open.mockClear();
60
+ });
61
+
62
+ afterEach(() => {
63
+ // Restore original implementations
64
+ vi.unstubAllGlobals();
65
+ global.Math.random = originalMathRandom;
66
+ global.indexedDB = originalIndexedDB;
67
+ });
68
+
69
+ it('should solve a "useful_work_task" with fraud.solve and return a Pareto front', async () => {
70
+ const mockChallenge = {
71
+ type: 'useful_work_task',
72
+ nonce: 'test-nonce-fraud',
73
+ usefulWorkTask: {
74
+ problemId: 'fraud_detection_challenge',
75
+ task: {
76
+ type: 'multi_objective_genetic_algorithm',
77
+ solverName: 'fraud.solve',
78
+ generations: 5, // Keep generations low for quick test
79
+ populationSize: 10, // Keep population low for quick test
80
+ payload: {
81
+ legitimateClicks: [
82
+ { fingerprint: 'fp1', timeToClick: 500, clickX: 10, clickY: 10, mouseEntropy: 0.8, scrollEvents: 5 },
83
+ { fingerprint: 'fp1', timeToClick: 600, clickX: 12, clickY: 12, mouseEntropy: 0.7, scrollEvents: 6 },
84
+ { fingerprint: 'fp2', timeToClick: 700, clickX: 20, clickY: 20, mouseEntropy: 0.9, scrollEvents: 7 },
85
+ ],
86
+ fraudulentClicks: [
87
+ { fingerprint: 'fp3', timeToClick: 50, clickX: 5, clickY: 5, mouseEntropy: 0.1, scrollEvents: 0 },
88
+ { fingerprint: 'fp3', timeToClick: 60, clickX: 6, clickY: 6, mouseEntropy: 0.2, scrollEvents: 1 },
89
+ { fingerprint: 'fp4', timeToClick: 100, clickX: 10, clickY: 10, mouseEntropy: 0.3, scrollEvents: 2 },
90
+ ],
91
+ },
92
+ },
93
+ },
94
+ };
95
+
96
+ const solution = await solveChallenge(mockChallenge);
97
+
98
+ // Assert the structure of the returned solution
99
+ expect(solution.type).toBe('useful_work_task');
100
+ expect(solution.nonce).toBe('test-nonce-fraud');
101
+ expect(solution.rawSolution).toHaveProperty('problem_id', 'fraud_detection_challenge');
102
+ expect(solution.rawSolution).toHaveProperty('work_result');
103
+
104
+ const workResult = solution.rawSolution.work_result;
105
+ expect(workResult).toHaveProperty('paretoFront');
106
+ expect(workResult.paretoFront).toBeInstanceOf(Array);
107
+ expect(workResult.paretoFront.length).toBeGreaterThan(0);
108
+
109
+ // Check the structure of a single solution in the Pareto front
110
+ const firstParetoSolution = workResult.paretoFront[0];
111
+ expect(firstParetoSolution).toHaveProperty('solution');
112
+ expect(firstParetoSolution.solution).toBeInstanceOf(Array); // fraud.solve returns an array of thresholds
113
+ expect(firstParetoSolution.solution.length).toBe(4); // [minTimeToClick, maxClickVariance, minMouseEntropy, minScrollEvents]
114
+ expect(firstParetoSolution).toHaveProperty('objectives');
115
+ expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [objective1, objective2]
116
+ expect(firstParetoSolution.objectives.length).toBe(2);
117
+ }, 10000); // Increase timeout for GA, even with low generations/population
118
+
119
+ it('should solve a "useful_work_task" with cpc.solve and return a Pareto front', async () => {
120
+ const mockChallenge = {
121
+ type: 'useful_work_task',
122
+ nonce: 'test-nonce-cpc',
123
+ usefulWorkTask: {
124
+ problemId: 'cpc_optimization_challenge',
125
+ task: {
126
+ type: 'multi_objective_genetic_algorithm',
127
+ solverName: 'cpc.solve',
128
+ generations: 5, // Keep generations low for quick test
129
+ populationSize: 10, // Keep population low for quick test
130
+ payload: {
131
+ platformParams: { optimalBaseCommission: 0.3, optimalBonusFactor: 0.1 },
132
+ website: { relevanceScore: 70 },
133
+ advertiser: { credits: 1000 },
134
+ estimatedImpressions: 500,
135
+ competingAds: [{ id: 'ad1' }, { id: 'ad2' }],
136
+ },
137
+ },
138
+ },
139
+ };
140
+
141
+ const solution = await solveChallenge(mockChallenge);
142
+
143
+ // Assert the structure of the returned solution
144
+ expect(solution.type).toBe('useful_work_task');
145
+ expect(solution.nonce).toBe('test-nonce-cpc');
146
+ expect(solution.rawSolution).toHaveProperty('problem_id', 'cpc_optimization_challenge');
147
+ expect(solution.rawSolution).toHaveProperty('work_result');
148
+
149
+ const workResult = solution.rawSolution.work_result;
150
+ expect(workResult).toHaveProperty('paretoFront');
151
+ expect(workResult.paretoFront).toBeInstanceOf(Array);
152
+ expect(workResult.paretoFront.length).toBeGreaterThan(0);
153
+
154
+ // Check the structure of a single solution in the Pareto front
155
+ const firstParetoSolution = workResult.paretoFront[0];
156
+ expect(firstParetoSolution).toHaveProperty('solution');
157
+ expect(typeof firstParetoSolution.solution).toBe('number'); // cpc.solve returns a single CPC value
158
+ expect(firstParetoSolution).toHaveProperty('objectives');
159
+ expect(firstParetoSolution.objectives).toBeInstanceOf(Array); // [platformRevenue, advertiserValue, marketImbalance]
160
+ expect(firstParetoSolution.objectives.length).toBe(3);
161
+ }, 10000);
162
+ });
@@ -30,6 +30,7 @@ class AutoTuner
30
30
  /** @var ?callable */
31
31
  private $onCleanup;
32
32
  private ?string $savePath;
33
+ private float $validationTolerance;
33
34
 
34
35
  /**
35
36
  * @var ?array<string, mixed> La dernière meilleure solution trouvée par l'optimiseur.
@@ -51,6 +52,7 @@ class AutoTuner
51
52
  $this->clearAfterTuning = $options['clearAfterTuning'] ?? false;
52
53
  $this->onCleanup = $options['onCleanup'] ?? null;
53
54
  $this->savePath = $options['savePath'] ?? null;
55
+ $this->validationTolerance = $options['validationTolerance'] ?? 0.15;
54
56
  }
55
57
 
56
58
  /**
@@ -211,6 +213,46 @@ class AutoTuner
211
213
  }
212
214
  };
213
215
 
216
+ // --- VALIDATION POST-CALCUL (PHP Rollback & Seuil de Tolérance) ---
217
+ $tempConfig = [
218
+ 'thresholds' => $this->securityConfig['thresholds'],
219
+ 'weights' => $this->securityConfig['weights'],
220
+ 'patterns' => $this->securityConfig['patterns'],
221
+ ];
222
+
223
+ $applyInertialUpdate($tempConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
224
+ $applyInertialUpdate($tempConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
225
+ $applyInertialUpdate($tempConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);
226
+
227
+ $evaluator = OptimizationOperators::createFullSecurityConfigEvaluator(['trafficData' => $sanitizedData]);
228
+ $currentObjectives = $evaluator($this->securityConfig);
229
+ $proposedObjectives = $evaluator($tempConfig);
230
+
231
+ $currentFPR = $currentObjectives[0];
232
+ $currentFNR = $currentObjectives[1];
233
+ $proposedFPR = $proposedObjectives[0];
234
+ $proposedFNR = $proposedObjectives[1];
235
+
236
+ $validationTolerance = $this->securityConfig['autotuning']['validationTolerance'] ?? $this->validationTolerance;
237
+
238
+ if ($proposedFPR > $currentFPR + $validationTolerance || $proposedFNR > $currentFNR + $validationTolerance) {
239
+ error_log(sprintf(
240
+ "[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: %.4f (Current: %.4f), Proposed FNR: %.4f (Current: %.4f)",
241
+ $proposedFPR, $currentFPR, $proposedFNR, $currentFNR
242
+ ));
243
+ if (isset($this->securityConfig['logger']) && is_callable($this->securityConfig['logger'])) {
244
+ call_user_func($this->securityConfig['logger'], [
245
+ 'type' => 'autotuning_instability_alert',
246
+ 'proposedFPR' => $proposedFPR,
247
+ 'currentFPR' => $currentFPR,
248
+ 'proposedFNR' => $proposedFNR,
249
+ 'currentFNR' => $currentFNR,
250
+ 'timestamp' => (int)(microtime(true) * 1000)
251
+ ]);
252
+ }
253
+ return; // Rollback
254
+ }
255
+
214
256
  $applyInertialUpdate($this->securityConfig['thresholds'], $newConfig['thresholds'], 'thresholds', $trafficConfidence);
215
257
  $applyInertialUpdate($this->securityConfig['weights'], $newConfig['weights'], 'weights', $trafficConfidence);
216
258
  $applyInertialUpdate($this->securityConfig['patterns'], $newConfig['patterns'], 'patterns', $trafficConfidence);