@anonympins/fingerprint 0.7.4 → 0.8.0

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 (64) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/anonympins-bot-mitigation-pow.zip +0 -0
  5. package/src/js/dynamic-wasm.js +218 -218
  6. package/src/js/fingerprint.client.js +1784 -1738
  7. package/src/js/fingerprint.client.obfuscated.js +1 -1
  8. package/src/js/fingerprint.js +489 -99
  9. package/src/js/fingerprint.utils.js +252 -2
  10. package/src/js/pow.worker.js +2 -2
  11. package/src/js/tests/cross_parity.test.js +3 -3
  12. package/src/js/tests/dns-circuit-breaker.test.js +6 -6
  13. package/src/js/tests/dynamic-wasm.test.js +77 -77
  14. package/src/js/tests/fingerprint.builder.test.js +76 -78
  15. package/src/js/tests/fingerprint.client.init.test.js +78 -0
  16. package/src/js/tests/fingerprint.client.test.js +16 -17
  17. package/src/js/tests/fingerprint.test.js +37 -12
  18. package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
  19. package/src/js/tests/library.test.js +94 -95
  20. package/src/js/tests/obfuscation.test.js +8 -8
  21. package/src/js/tests/quicFingerprint.test.js +47 -0
  22. package/src/js/tests/tcpFingerprint.test.js +4 -4
  23. package/src/js/tests/threatIntel.test.js +40 -1
  24. package/src/js/upow-model-task.js +155 -112
  25. package/src/php/Challenge/ChallengeUtils.php +392 -86
  26. package/src/php/Config/SecurityProfiles.php +79 -61
  27. package/src/php/DirectFingerprint.php +55 -8
  28. package/src/php/FingerprintBuilder.php +13 -13
  29. package/src/php/FingerprintClient.php +6 -6
  30. package/src/php/FingerprintEngine.php +2122 -1690
  31. package/src/php/Ja3AnomalyDetector.php +22 -22
  32. package/src/php/Optimization/FunctionRegistry.php +4 -4
  33. package/src/php/Optimization/Optimization.php +6 -6
  34. package/src/php/RequestContext.php +11 -11
  35. package/src/php/Store/InMemoryStore.php +96 -66
  36. package/src/php/Store/MongoDbStore.php +2 -2
  37. package/src/php/Store/RedisStore.php +1 -1
  38. package/src/php/Store/StoreManager.php +48 -35
  39. package/src/php/Tests/ChallengeUtilsTest.php +31 -0
  40. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  41. package/src/php/Tests/FingerprintClientTest.php +70 -70
  42. package/src/php/Tests/PowTest.php +37 -39
  43. package/src/php/Tests/QuicFingerprintTest.php +115 -53
  44. package/src/php/Tests/ThreatIntelTest.php +34 -0
  45. package/src/php/Utils/BigInt.php +202 -202
  46. package/src/php/Utils/BlockList.php +13 -13
  47. package/src/php/Utils/Logger.php +19 -1
  48. package/src/php/Utils/MaliciousPatterns.php +6 -6
  49. package/src/php/Utils/MetricsManager.php +209 -209
  50. package/src/php/Utils/RequestUtils.php +362 -1
  51. package/src/php/Utils/TLSClientHelloParser.php +385 -369
  52. package/src/php/WordPress/WpDbStore.php +7 -7
  53. package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
  54. package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
  55. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
  56. package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
  57. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  58. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
  59. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  60. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
  61. package/src/php/WordPress/package.php +80 -101
  62. package/src/php/WordPress/readme.txt +21 -5
  63. package/src/php/bin/auto-tune.php +1 -1
  64. package/public/fingerprint-anti-bot.zip +0 -0
@@ -1,79 +1,77 @@
1
- import {describe, expect, it} from 'vitest';
2
- import {FingerprintBuilder} from '../fingerprint.builder.js';
3
-
4
- describe('FingerprintBuilder.compare', () => {
5
-
6
- // Empreinte réaliste d'un utilisateur légitime (ex: Chrome sur Windows)
7
- // C'est l'empreinte qui serait stockée lors de la première visite.
8
- const realisticOriginalFp = new FingerprintBuilder()
9
- .add('cvs', 'mock-canvas-data-v1')
10
- .add('gpu', 'ANGLE (NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)')
11
- .add('hw', '16_8_0') // 16 cores, 8GB RAM, no touch
12
- .add('os', 'Win32')
13
- .add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36')
14
- .add('ja3', '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0')
15
- // En-têtes qui peuvent changer lors de la requête de résolution du challenge
16
- .add('cookie_keys', '_ga,session,device_id')
17
- .add('upgrade', '1')
18
- .toString();
19
-
20
- it('should return a similarity score of 1.0 for the same device with minor volatile changes', () => {
21
- // Scénario de succès : L'utilisateur résout un challenge.
22
- // L'empreinte du solveur est presque identique, mais certains en-têtes "volatils"
23
- // (comme la présence de cookies ou 'upgrade-insecure-requests') ont changé ou disparu.
24
- // La fonction `compare` est conçue pour ignorer ces clés volatiles.
25
- const realisticSolverFp = new FingerprintBuilder()
26
- .add('cvs', 'mock-canvas-data-v1') // Identique
27
- .add('gpu', 'ANGLE (NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)') // Identique
28
- .add('hw', '16_8_0') // Identique
29
- .add('os', 'Win32') // Identique
30
- .add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36') // Identique
31
- .add('ja3', '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0') // Identique
32
- // La clé 'cookie_keys' est absente, simulant une requête sans cookies.
33
- // La clé 'upgrade' est également absente.
34
- .toString();
35
-
36
- const similarity = FingerprintBuilder.compare(realisticOriginalFp, realisticSolverFp);
37
-
38
- // La similarité doit être de 1.0 car toutes les différences concernent des clés volatiles
39
- // qui sont ignorées par la comparaison.
40
- expect(similarity).toBe(1.0);
41
- });
42
-
43
- it('should return a low similarity score for two completely different devices', () => {
44
- // Scénario d'échec : Un attaquant a volé le cookie `device_id` et tente de
45
- // résoudre un challenge depuis une machine différente (ex: un serveur Linux avec Firefox).
46
- const differentSolverFp = new FingerprintBuilder()
47
- .add('cvs', 'different-canvas-data') // Différent
48
- .add('gpu', 'llvmpipe (LLVM 15.0.7, 256 bits)') // Différent (GPU de VM)
49
- .add('hw', '8_4_0') // Différent
50
- .add('os', 'Linux x86_64') // Différent
51
- .add('ua', 'Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0') // Différent
52
- .add('ja3', '771,49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,65281-11-10-35-16-5-13-51-45-43-27-23-17513,29-23-24,0') // Différent (JA3 de Firefox)
53
- .toString();
54
-
55
- const similarity = FingerprintBuilder.compare(realisticOriginalFp, differentSolverFp);
56
-
57
- // La similarité doit être très faible (proche de 0) car tous les signaux forts sont différents.
58
- expect(similarity).toBeLessThan(0.1);
59
- });
60
-
61
- it('should return 0 if one of the fingerprints is null or empty', () => {
62
- expect(FingerprintBuilder.compare(realisticOriginalFp, null)).toBe(0);
63
- expect(FingerprintBuilder.compare(null, realisticOriginalFp)).toBe(0);
64
- expect(FingerprintBuilder.compare(realisticOriginalFp, '')).toBe(0);
65
- expect(FingerprintBuilder.compare('', realisticOriginalFp)).toBe(0);
66
- expect(FingerprintBuilder.compare(null, null)).toBe(0);
67
- });
68
-
69
- it('should handle fingerprints with missing components gracefully', () => {
70
- const partialFp = new FingerprintBuilder()
71
- .add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36')
72
- .add('os', 'Win32')
73
- .toString(); // Manque GPU, canvas, etc.
74
-
75
- const similarity = FingerprintBuilder.compare(realisticOriginalFp, partialFp);
76
- expect(similarity).toBeGreaterThan(0);
77
- expect(similarity).toBeLessThan(1);
78
- });
1
+ import {describe, expect, it} from 'vitest';
2
+ import {FingerprintBuilder} from '../fingerprint.builder.js';
3
+
4
+ describe('FingerprintBuilder.compare', () => {
5
+
6
+ // Realistic fingerprint for a legitimate user (e.g. Chrome on Windows)
7
+ // Recorded during baseline initial visit.
8
+ const realisticOriginalFp = new FingerprintBuilder()
9
+ .add('cvs', 'mock-canvas-data-v1')
10
+ .add('gpu', 'ANGLE (NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)')
11
+ .add('hw', '16_8_0') // 16 cores, 8GB RAM, no touch
12
+ .add('os', 'Win32')
13
+ .add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36')
14
+ .add('ja3', '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0')
15
+ // Volatile headers that may legitimately change during challenge solving
16
+ .add('cookie_keys', '_ga,session,device_id')
17
+ .add('upgrade', '1')
18
+ .toString();
19
+
20
+ it('should return a similarity score of 1.0 for the same device with minor volatile changes', () => {
21
+ // Success scenario: user solves a challenge.
22
+ // Solver fingerprint is identical except for volatile headers
23
+ // (such as cookie presence or upgrade-insecure-requests) which compare ignores.
24
+ const realisticSolverFp = new FingerprintBuilder()
25
+ .add('cvs', 'mock-canvas-data-v1') // Identique
26
+ .add('gpu', 'ANGLE (NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0)') // Identique
27
+ .add('hw', '16_8_0') // Identique
28
+ .add('os', 'Win32') // Identique
29
+ .add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36') // Identique
30
+ .add('ja3', '771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0') // Identique
31
+ // 'cookie_keys' is omitted, simulating a request without cookies
32
+ // 'upgrade' header is also omitted
33
+ .toString();
34
+
35
+ const similarity = FingerprintBuilder.compare(realisticOriginalFp, realisticSolverFp);
36
+
37
+ // Similarity remains 1.0 because all differences reside in ignored volatile keys
38
+ expect(similarity).toBe(1.0);
39
+ });
40
+
41
+ it('should return a low similarity score for two completely different devices', () => {
42
+ // Failure scenario: attacker steals device_id cookie and attempts
43
+ // challenge resolution from a different machine (e.g. Linux VM with Firefox).
44
+ const differentSolverFp = new FingerprintBuilder()
45
+ .add('cvs', 'different-canvas-data') // Différent
46
+ .add('gpu', 'llvmpipe (LLVM 15.0.7, 256 bits)') // Différent (GPU de VM)
47
+ .add('hw', '8_4_0') // Différent
48
+ .add('os', 'Linux x86_64') // Différent
49
+ .add('ua', 'Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0') // Différent
50
+ .add('ja3', '771,49195-49199-52393-52392-49196-49200-49162-49161-49171-49172-156-157-47-53,65281-11-10-35-16-5-13-51-45-43-27-23-17513,29-23-24,0') // Différent (JA3 de Firefox)
51
+ .toString();
52
+
53
+ const similarity = FingerprintBuilder.compare(realisticOriginalFp, differentSolverFp);
54
+
55
+ // Similarity should be very low (close to 0) since invariant signals differ
56
+ expect(similarity).toBeLessThan(0.1);
57
+ });
58
+
59
+ it('should return 0 if one of the fingerprints is null or empty', () => {
60
+ expect(FingerprintBuilder.compare(realisticOriginalFp, null)).toBe(0);
61
+ expect(FingerprintBuilder.compare(null, realisticOriginalFp)).toBe(0);
62
+ expect(FingerprintBuilder.compare(realisticOriginalFp, '')).toBe(0);
63
+ expect(FingerprintBuilder.compare('', realisticOriginalFp)).toBe(0);
64
+ expect(FingerprintBuilder.compare(null, null)).toBe(0);
65
+ });
66
+
67
+ it('should handle fingerprints with missing components gracefully', () => {
68
+ const partialFp = new FingerprintBuilder()
69
+ .add('ua', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36')
70
+ .add('os', 'Win32')
71
+ .toString(); // Missing GPU, canvas, etc.
72
+
73
+ const similarity = FingerprintBuilder.compare(realisticOriginalFp, partialFp);
74
+ expect(similarity).toBeGreaterThan(0);
75
+ expect(similarity).toBeLessThan(1);
76
+ });
79
77
  });
@@ -21,6 +21,7 @@ if (typeof globalThis.crypto !== 'undefined') {
21
21
  Object.defineProperty(dom.window, 'crypto', { value: globalThis.crypto, writable: true, configurable: true });
22
22
  }
23
23
  Object.defineProperty(global, 'CustomEvent', { value: dom.window.CustomEvent, writable: true, configurable: true });
24
+ Object.defineProperty(global, 'Event', { value: dom.window.Event, writable: true, configurable: true });
24
25
 
25
26
  global.fetch = vi.fn(); // Mock global fetch
26
27
  window.fetch = (...args) => global.fetch(...args);
@@ -174,4 +175,81 @@ describe('ClientLibrary.initializeClient', () => {
174
175
  expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Challenge retry limit reached (3)'));
175
176
  });
176
177
 
178
+ it('should inject phantom traps within closed shadow root and fire trigger on interaction', () => {
179
+ const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
180
+ let capturedShadow = null;
181
+ const origAttachShadow = dom.window.Element.prototype.attachShadow;
182
+ vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
183
+ capturedShadow = origAttachShadow.call(this, init);
184
+ return capturedShadow;
185
+ });
186
+
187
+ // Ensure document.body exists
188
+ document.body.innerHTML = '';
189
+
190
+ ClientLibrary.injectPhantomTraps();
191
+
192
+ const host = document.body.lastElementChild;
193
+ expect(host).not.toBeNull();
194
+ expect(host.getAttribute('aria-hidden')).toBe('true');
195
+ // Under closed mode, host.shadowRoot is inaccessible (null) from external callers
196
+ expect(host.shadowRoot).toBeNull();
197
+ expect(capturedShadow).not.toBeNull();
198
+
199
+ // Dispatch events on attached phantom elements inside closed shadow root
200
+ const link = capturedShadow.querySelector('a');
201
+ expect(link).not.toBeNull();
202
+ link.dispatchEvent(new Event('focus'));
203
+ expect(onTriggerSpy).toHaveBeenCalled();
204
+ });
205
+
206
+ it('should inject trap links within closed shadow root and trigger on honeypot click', () => {
207
+ const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
208
+ let capturedShadow = null;
209
+ const origAttachShadow = dom.window.Element.prototype.attachShadow;
210
+ vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
211
+ capturedShadow = origAttachShadow.call(this, init);
212
+ return capturedShadow;
213
+ });
214
+
215
+ document.body.innerHTML = '';
216
+
217
+ ClientLibrary.injectTrapLinks(['/trap-url-1', '/trap-url-2']);
218
+
219
+ const host = document.body.lastElementChild;
220
+ expect(host).not.toBeNull();
221
+ expect(host.getAttribute('aria-hidden')).toBe('true');
222
+ expect(host.shadowRoot).toBeNull(); // Closed Shadow DOM verification
223
+ expect(capturedShadow).not.toBeNull();
224
+
225
+ const link = capturedShadow.querySelector('a');
226
+ expect(link).not.toBeNull();
227
+ link.dispatchEvent(new Event('click'));
228
+ expect(onTriggerSpy).toHaveBeenCalled();
229
+ });
230
+
231
+ it('should initialize honeypot form inputs within closed shadow root', () => {
232
+ const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
233
+ let capturedShadow = null;
234
+ const origAttachShadow = dom.window.Element.prototype.attachShadow;
235
+ vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
236
+ capturedShadow = origAttachShadow.call(this, init);
237
+ return capturedShadow;
238
+ });
239
+
240
+ document.body.innerHTML = '';
241
+
242
+ ClientLibrary.initializeHoneypots(['bot_field_trap']);
243
+
244
+ const host = document.body.lastElementChild;
245
+ expect(host).not.toBeNull();
246
+ expect(host.getAttribute('aria-hidden')).toBe('true');
247
+ expect(host.shadowRoot).toBeNull();
248
+ expect(capturedShadow).not.toBeNull();
249
+
250
+ const input = capturedShadow.querySelector('input');
251
+ expect(input).not.toBeNull();
252
+ input.dispatchEvent(new Event('input'));
253
+ expect(onTriggerSpy).toHaveBeenCalled();
254
+ });
177
255
  });
@@ -7,12 +7,12 @@ import ClientLibrary from '../fingerprint.client.js';
7
7
 
8
8
  describe('ClientLibrary WASM Integration', () => {
9
9
  beforeEach(() => {
10
- // Réinitialise le cache et les mocks avant chaque test
10
+ // Reset cache and spies before each test
11
11
  ClientLibrary._resetCache();
12
12
  vi.spyOn(console, 'log').mockImplementation(() => {});
13
13
  vi.spyOn(console, 'warn').mockImplementation(() => {});
14
14
 
15
- // Supprime les mocks globaux pour éviter les fuites entre les tests
15
+ // Remove global mocks to prevent leakage across tests
16
16
  delete window.createFingerprintModule;
17
17
  });
18
18
 
@@ -21,28 +21,27 @@ describe('ClientLibrary WASM Integration', () => {
21
21
  });
22
22
 
23
23
  it('should successfully load WASM module and switch hasher', async () => {
24
- // 1. Simuler un module WASM fonctionnel
24
+ // 1. Mock functional WASM module
25
25
  const mockWasmModule = {
26
- _hash_string: vi.fn((str) => 99999), // Un mock qui retourne une valeur distincte
26
+ _hash_string: vi.fn((str) => 99999), // Distinct mock hash value
27
27
  _malloc: vi.fn().mockReturnValue(0),
28
28
  HEAPU8: new Uint8Array(4096)
29
29
  };
30
30
 
31
- // 2. Simuler le chargement du script et l'initialisation du module
32
- // On attache les fonctions de simulation à `window` car c'est ce que le code cherche
31
+ // 2. Mock script loading and module factory
33
32
  window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
34
33
 
35
- // On simule l'injection du script en appelant directement `onload`
34
+ // Mock script element injection with direct onload callback
36
35
  vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
37
- // Simule le chargement réussi du script
36
+ // Simulate successful script load
38
37
  script.onload();
39
38
  return script;
40
39
  });
41
40
 
42
- // 3. Appeler la fonction d'initialisation
41
+ // 3. Initialize WASM module
43
42
  await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
44
43
 
45
- // 4. Vérifier que le hasher a été remplacé
44
+ // 4. Verify hasher was switched
46
45
  const wasmHash = ClientLibrary._hasher("test");
47
46
  expect(wasmHash).toBe(99999);
48
47
  expect(mockWasmModule._hash_string).toHaveBeenCalledWith(0);
@@ -50,16 +49,16 @@ describe('ClientLibrary WASM Integration', () => {
50
49
  });
51
50
 
52
51
  it('should gracefully fall back to JS hasher if WASM module fails to load', async () => {
53
- // 1. Simuler un échec de chargement du script
52
+ // 1. Simulate script loading failure
54
53
  vi.spyOn(document.head, 'appendChild').mockImplementation((script) => {
55
54
  script.onerror(new Error('Script loading failed'));
56
55
  return script;
57
56
  });
58
57
 
59
- // 2. Appeler la fonction d'initialisation
58
+ // 2. Initialize WASM module
60
59
  await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
61
60
 
62
- // 3. Vérifier que le hasher est toujours l'implémentation JS
61
+ // 3. Verify hasher falls back to JS implementation
63
62
  const jsHash = ClientLibrary._hasher("test");
64
63
  const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
65
64
 
@@ -68,7 +67,7 @@ describe('ClientLibrary WASM Integration', () => {
68
67
  });
69
68
 
70
69
  it('should gracefully fall back if WASM module does not export _hash_string', async () => {
71
- // 1. Simuler un module WASM malformé (sans la fonction attendue)
70
+ // 1. Simulate malformed WASM module missing required export
72
71
  const mockWasmModule = {};
73
72
  window.createFingerprintModule = vi.fn().mockResolvedValue(mockWasmModule);
74
73
 
@@ -77,14 +76,14 @@ describe('ClientLibrary WASM Integration', () => {
77
76
  return script;
78
77
  });
79
78
 
80
- // 2. Appeler la fonction d'initialisation
79
+ // 2. Initialize WASM module
81
80
  await ClientLibrary.initializeWasm('/fake/path/to/fp.js');
82
81
 
83
- // 3. Vérifier le fallback
82
+ // 3. Verify fallback
84
83
  const jsHash = ClientLibrary._hasher("test");
85
84
  const originalJsHash = (await import('../fingerprint.builder.js')).cyrb53("test");
86
85
  expect(jsHash).toBe(originalJsHash);
87
- // L'assertion est maintenant plus précise : elle vérifie le message générique ET le message d'erreur spécifique.
86
+ // Verify both generic error log and specific missing export cause
88
87
  expect(console.warn).toHaveBeenCalledWith(
89
88
  expect.stringContaining('WASM module failed to load'),
90
89
  expect.objectContaining({ message: 'WASM module did not export _hash_string.' })
@@ -1621,18 +1621,6 @@ describe('Fingerprint & PoW Security Suite', () => {
1621
1621
  expect(behaviorScore).toBe(0);
1622
1622
  });
1623
1623
 
1624
- it('should return a score of 0 when x-behavior-metrics is 1 (human verified client-side)', () => {
1625
- const context = { headers: { 'x-behavior-metrics': '1' } };
1626
- const { behaviorScore } = getBehaviorScore(context);
1627
- expect(behaviorScore).toBe(0);
1628
- });
1629
-
1630
- it('should return a score of 100 when x-behavior-metrics is 0 (bot detected client-side)', () => {
1631
- const context = { headers: { 'x-behavior-metrics': '0' } };
1632
- const { behaviorScore } = getBehaviorScore(context);
1633
- expect(behaviorScore).toBe(100);
1634
- });
1635
-
1636
1624
  it('should return a score of 100 for honeypot interaction', () => {
1637
1625
  const metrics = { honeypotInteraction: true, mouseEntropy: 50, keystrokeLatency: 120 };
1638
1626
  const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
@@ -3199,5 +3187,42 @@ describe('IP Registration and Filtering', () => {
3199
3187
  expect(process.env.ED25519_PRIVATE_KEY).toBe(dummyKeys.privateKey);
3200
3188
  expect(process.env.ED25519_PUBLIC_KEY).toBe(dummyKeys.publicKey);
3201
3189
  });
3190
+
3191
+ it('should generate issuer PEM files directly via generateIssuerPemKeys', async () => {
3192
+ const fs = await import('node:fs');
3193
+ const os = await import('node:os');
3194
+ const path = await import('node:path');
3195
+ const tempDir = path.join(os.tmpdir(), `fp-issuer-keys-${Date.now()}`);
3196
+
3197
+ try {
3198
+ const keys = fingerprint.generateIssuerPemKeys(tempDir);
3199
+ expect(keys.privateKeyPath).toContain('issuer-private.pem');
3200
+ expect(keys.publicKeyPath).toContain('issuer-public.pem');
3201
+ expect(keys.privateKey).toContain('-----BEGIN PRIVATE KEY-----');
3202
+ expect(keys.publicKey).toContain('-----BEGIN PUBLIC KEY-----');
3203
+ expect(fs.existsSync(keys.privateKeyPath)).toBe(true);
3204
+ expect(fs.existsSync(keys.publicKeyPath)).toBe(true);
3205
+ } finally {
3206
+ fs.rmSync(tempDir, { recursive: true, force: true });
3207
+ }
3208
+ });
3209
+ });
3210
+
3211
+ describe('Closed-Shadow DOM Honeypot Traps (Feature 19)', () => {
3212
+ it('should generate closed declarative shadow DOM trap container for trap URLs', () => {
3213
+ const trapUrls = ['/trap-1', '/trap-2'];
3214
+ const markup = __internal.generateClosedShadowTraps(trapUrls);
3215
+
3216
+ expect(markup).toContain('aria-hidden="true"');
3217
+ expect(markup).toContain('shadowrootmode="closed"');
3218
+ expect(markup).toContain('shadowroot="closed"');
3219
+ expect(markup).toContain('href="/trap-1"');
3220
+ expect(markup).toContain('href="/trap-2"');
3221
+ });
3222
+
3223
+ it('should return an empty string if trapUrls array is empty or invalid', () => {
3224
+ expect(__internal.generateClosedShadowTraps([])).toBe('');
3225
+ expect(__internal.generateClosedShadowTraps(null)).toBe('');
3226
+ });
3202
3227
  });
3203
3228
  });