@anonympins/fingerprint 0.6.7 → 0.7.1

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 (63) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +3 -7
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/public/fp.wasm +0 -0
  7. package/src/js/build-client.js +9 -11
  8. package/src/js/dynamic-wasm.js +7 -7
  9. package/src/js/fingerprint.builder.js +37 -38
  10. package/src/js/fingerprint.client.js +236 -152
  11. package/src/js/fingerprint.client.obfuscated.js +1 -1
  12. package/src/js/fingerprint.js +517 -101
  13. package/src/js/fingerprint.utils.js +213 -213
  14. package/src/js/library.js +355 -384
  15. package/src/js/mongodb-store.js +1 -1
  16. package/src/js/optimization.worker.js +27 -27
  17. package/src/js/pow.solver.inline.js +206 -62
  18. package/src/js/pow.solver.js +235 -101
  19. package/src/js/pow.worker.js +48 -26
  20. package/src/js/redis-store.js +1 -1
  21. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  22. package/src/js/tests/fingerprint.test.js +101 -40
  23. package/src/js/tests/ip-reputation.test.js +1 -0
  24. package/src/js/upow-model-task.js +14 -15
  25. package/src/php/AutoTuner.php +34 -34
  26. package/src/php/Challenge/ChallengeUtils.php +204 -32
  27. package/src/php/Config/SecurityProfiles.php +21 -0
  28. package/src/php/DirectFingerprint.php +190 -145
  29. package/src/php/FingerprintBuilder.php +29 -30
  30. package/src/php/FingerprintClient.php +24 -25
  31. package/src/php/FingerprintEngine.php +1836 -1880
  32. package/src/php/Ja3AnomalyDetector.php +258 -227
  33. package/src/php/Optimization/FunctionRegistry.php +7 -7
  34. package/src/php/Optimization/Optimization.php +24 -24
  35. package/src/php/Optimization/OptimizationOperators.php +28 -27
  36. package/src/php/Optimization/ProblemInitializers.php +52 -52
  37. package/src/php/ProblemManager.php +31 -34
  38. package/src/php/RequestContext.php +8 -1
  39. package/src/php/Store/IStore.php +7 -8
  40. package/src/php/Store/InMemoryStore.php +66 -66
  41. package/src/php/Store/MongoDbStore.php +5 -5
  42. package/src/php/Store/RedisStore.php +5 -5
  43. package/src/php/Store/StoreManager.php +35 -35
  44. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  45. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  46. package/src/php/Tests/FingerprintEngineTest.php +662 -693
  47. package/src/php/Tests/IpReputationTest.php +10 -0
  48. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  49. package/src/php/Tests/MetricsTest.php +97 -97
  50. package/src/php/Tests/ProblemManagerTest.php +376 -376
  51. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  52. package/src/php/Tests/RequestUtilsTest.php +471 -471
  53. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  54. package/src/php/Tests/config/ed25519_key.json +3 -3
  55. package/src/php/Utils/Env.php +49 -49
  56. package/src/php/Utils/MaliciousPatterns.php +74 -74
  57. package/src/php/Utils/MetricsManager.php +209 -209
  58. package/src/php/Utils/RequestUtils.php +236 -19
  59. package/src/php/Utils/TLSClientHelloParser.php +369 -212
  60. package/src/php/WordPress/WpDbStore.php +155 -0
  61. package/src/php/WordPress/fingerprint-wordpress.php +217 -0
  62. package/src/php/WordPress/package.php +123 -0
  63. package/src/php/bin/auto-tune.php +118 -118
@@ -1,27 +1,49 @@
1
- /**
2
- * @file @/pow.worker.js
3
- * @description Web Worker dédié à la résolution du challenge CPU.
4
- * Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
5
- */
6
-
7
- self.onmessage = async (event) => {
8
- const { message, target } = event.data;
9
- let solution = 0;
10
- const encoder = new TextEncoder();
11
-
12
- // La boucle de calcul intensive est isolée dans ce worker.
13
- while (true) {
14
- const currentMessage = `${message}:${solution}`;
15
- const data = encoder.encode(currentMessage);
16
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
17
- const hashArray = Array.from(new Uint8Array(hashBuffer));
18
- const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
19
-
20
- if (BigInt('0x' + hashHex) < target) {
21
- // Une fois la solution trouvée, on la renvoie au thread principal.
22
- self.postMessage({ solution });
23
- return;
24
- }
25
- solution++;
26
- }
1
+ /**
2
+ * @file @/pow.worker.js
3
+ * @description Dedicated Web Worker for CPU challenge solving.
4
+ * Executes on a separate thread to avoid blocking the main UI thread.
5
+ */
6
+
7
+ self.onmessage = async (event) => {
8
+ const { message, baseBlock, target } = event.data;
9
+ const cpuTarget = BigInt(target);
10
+ let solution = 0;
11
+ const encoder = new TextEncoder();
12
+ if (baseBlock) {
13
+ // Mode solveCpuTargetInline (Uint8Array base block)
14
+ const blockArray = new Uint8Array(baseBlock);
15
+ while (true) {
16
+ const solutionBytes = encoder.encode(String(solution));
17
+ const finalBlock = new Uint8Array(blockArray.length + solutionBytes.length);
18
+ finalBlock.set(blockArray);
19
+ finalBlock.set(solutionBytes, blockArray.length);
20
+ const hashBuffer = await crypto.subtle.digest('SHA-256', finalBlock);
21
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
22
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
23
+
24
+ if (BigInt('0x' + hashHex) < cpuTarget) {
25
+ self.postMessage({ type: 'success', solution });
26
+ return;
27
+ }
28
+ solution++;
29
+ if (solution % 25000 === 0) {
30
+ self.postMessage({ type: 'progress', solution });
31
+ }
32
+ }
33
+ } else {
34
+ // Mode solveCpuTarget standard/legacy (Message string)
35
+ while (true) {
36
+ const currentMessage = `${message}:${solution}`;
37
+ const data = encoder.encode(currentMessage);
38
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
39
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
40
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
41
+
42
+ if (BigInt('0x' + hashHex) < cpuTarget) {
43
+ self.postMessage({ solution });
44
+ return;
45
+ }
46
+ solution++;
47
+ }
48
+ }
27
49
  };
@@ -35,7 +35,7 @@ export function createRedisStore(redisClient) {
35
35
  reconnecting = false;
36
36
  clearInterval(interval);
37
37
  } catch (err) {
38
- // Continue de tenter la reconnexion toutes les 5 secondes
38
+ // Keep retrying connection every 5 seconds
39
39
  }
40
40
  }, 5000);
41
41
  if (interval.unref) interval.unref();
@@ -1,144 +1,147 @@
1
- import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
2
- import {JSDOM} from 'jsdom';
3
- import ClientLibrary from '../fingerprint.client.js';
4
-
5
- // --- Setup JSDOM Environment ---
6
- // Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
7
- // ici rend le test explicite et portable.
8
- const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
9
- url: 'http://localhost',
10
- });
11
-
12
- // In recent Node.js versions, some global properties like 'navigator', 'performance',
13
- // and 'screen' are read-only. To ensure our JSDOM environment works correctly across
14
- // all versions, we use Object.defineProperty to make these properties writable.
15
- Object.defineProperty(global, 'window', { value: dom.window, writable: true });
16
- Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
17
- Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
18
- Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
19
- Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
20
-
21
- global.fetch = vi.fn(); // Mock global fetch
22
- global.Headers = dom.window.Headers;
23
- global.Request = dom.window.Request;
24
- global.URL = dom.window.URL;
25
- global.TextEncoder = dom.window.TextEncoder;
26
-
27
-
28
- describe('ClientLibrary.initializeClient', () => {
29
-
30
- // On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
31
- let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
32
-
33
- beforeEach(() => {
34
- // Réinitialiser l'état du client avant chaque test
35
- ClientLibrary._resetCache();
36
-
37
- // Créer les espions sur les méthodes internes
38
- startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
39
- startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
40
- startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
41
- startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
42
- startRenderingSpy = vi.spyOn(ClientLibrary, 'startRenderingTracker');
43
- initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
44
- initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
45
- injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
46
- initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
47
- injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
48
- });
49
-
50
- afterEach(() => {
51
- // Restaurer les espions après chaque test pour ne pas affecter les autres tests
52
- vi.restoreAllMocks();
53
- });
54
-
55
- it('should enable all trackers by default', () => {
56
- ClientLibrary.initializeClient();
57
-
58
- expect(startMouseSpy).toHaveBeenCalled();
59
- expect(startKeystrokeSpy).toHaveBeenCalled();
60
- expect(startClickSpy).toHaveBeenCalled();
61
- expect(startTouchSpy).toHaveBeenCalled();
62
- expect(startRenderingSpy).toHaveBeenCalled();
63
- expect(injectPhantomTrapsSpy).toHaveBeenCalled();
64
- });
65
-
66
- it('should disable trackers when configured', () => {
67
- ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false, rendering: false });
68
-
69
- expect(startMouseSpy).not.toHaveBeenCalled();
70
- expect(startKeystrokeSpy).not.toHaveBeenCalled();
71
- expect(startClickSpy).not.toHaveBeenCalled();
72
- expect(startTouchSpy).not.toHaveBeenCalled();
73
- expect(startRenderingSpy).not.toHaveBeenCalled();
74
- });
75
-
76
- it('should initialize WASM if wasmPath is configured', () => {
77
- ClientLibrary.initializeClient({ wasmPath: '/fp.js' });
78
-
79
- expect(initWasmSpy).toHaveBeenCalledWith('/fp.js');
80
- });
81
-
82
- it('should disable phantom traps when configured', () => {
83
- ClientLibrary.initializeClient({ phantomTraps: false });
84
-
85
- expect(injectPhantomTrapsSpy).not.toHaveBeenCalled();
86
- });
87
-
88
- it('should initialize honeypots with the provided field names', () => {
89
- const honeypotFields = ['email_confirm', 'user_nickname'];
90
- ClientLibrary.initializeClient({ honeypots: honeypotFields });
91
-
92
- expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
93
- });
94
-
95
- it('should not initialize honeypots if the array is empty', () => {
96
- ClientLibrary.initializeClient({ honeypots: [] });
97
-
98
- expect(initHoneypotsSpy).not.toHaveBeenCalled();
99
- });
100
-
101
- it('should inject trap URLs when provided', () => {
102
- const urls = ['/trap1?sig=123', '/trap2?sig=456'];
103
- ClientLibrary.initializeClient({ trapUrls: urls });
104
-
105
- expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
106
- });
107
-
108
- it('should not inject trap URLs if the array is empty', () => {
109
- ClientLibrary.initializeClient({ trapUrls: [] });
110
-
111
- expect(injectTrapsSpy).not.toHaveBeenCalled();
112
- });
113
-
114
- it('should initialize fetch interception when fetch config is present', () => {
115
- const targetDomains = ['api.example.com'];
116
- ClientLibrary.initializeClient({ fetch: { targetDomains } });
117
-
118
- expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
119
- });
120
-
121
- it('should not initialize fetch interception if fetch config is absent', () => {
122
- ClientLibrary.initializeClient(); // No fetch config
123
-
124
- expect(initFetchSpy).not.toHaveBeenCalled();
125
- });
126
-
127
- it('should call all initializers correctly when a full config is provided', () => {
128
- const config = {
129
- mouse: true,
130
- keystrokes: true,
131
- honeypots: ['field1'],
132
- trapUrls: ['/trap1'],
133
- fetch: { targetDomains: ['api.com'] }
134
- };
135
- ClientLibrary.initializeClient(config);
136
-
137
- expect(startMouseSpy).toHaveBeenCalled();
138
- expect(startKeystrokeSpy).toHaveBeenCalled();
139
- expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
140
- expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
141
- expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
142
- });
143
-
1
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
2
+ import {JSDOM} from 'jsdom';
3
+ import ClientLibrary from '../fingerprint.client.js';
4
+
5
+ // --- Setup JSDOM Environment ---
6
+ // Vitest peut être configuré pour le faire automatiquement, mais le faire manuellement
7
+ // ici rend le test explicite et portable.
8
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
9
+ url: 'http://localhost',
10
+ });
11
+
12
+ // In recent Node.js versions, some global properties like 'navigator', 'performance',
13
+ // and 'screen' are read-only. To ensure our JSDOM environment works correctly across
14
+ // all versions, we use Object.defineProperty to make these properties writable.
15
+ Object.defineProperty(global, 'window', { value: dom.window, writable: true });
16
+ Object.defineProperty(global, 'document', { value: dom.window.document, writable: true });
17
+ Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
18
+ Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
19
+ Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
20
+
21
+ global.fetch = vi.fn(); // Mock global fetch
22
+ global.Headers = dom.window.Headers;
23
+ global.Request = dom.window.Request;
24
+ global.URL = dom.window.URL;
25
+ global.TextEncoder = dom.window.TextEncoder;
26
+
27
+
28
+ describe('ClientLibrary.initializeClient', () => {
29
+
30
+ // On utilise des espions (spies) pour vérifier si les méthodes internes sont appelées.
31
+ let startMouseSpy, startKeystrokeSpy, startClickSpy, startTouchSpy, startMotionSpy, startRenderingSpy, initWasmSpy, initHoneypotsSpy, injectTrapsSpy, initFetchSpy, injectPhantomTrapsSpy;
32
+
33
+ beforeEach(() => {
34
+ // Réinitialiser l'état du client avant chaque test
35
+ ClientLibrary._resetCache();
36
+
37
+ // Créer les espions sur les méthodes internes
38
+ startMouseSpy = vi.spyOn(ClientLibrary, 'startMouseEntropyTracker');
39
+ startKeystrokeSpy = vi.spyOn(ClientLibrary, 'startKeystrokeDynamicsTracker');
40
+ startClickSpy = vi.spyOn(ClientLibrary, 'startClickTracker');
41
+ startTouchSpy = vi.spyOn(ClientLibrary, 'startTouchEventTracker');
42
+ startMotionSpy = vi.spyOn(ClientLibrary, 'startMotionTracker');
43
+ startRenderingSpy = vi.spyOn(ClientLibrary, 'startRenderingTracker');
44
+ initWasmSpy = vi.spyOn(ClientLibrary, 'initializeWasm').mockResolvedValue(undefined);
45
+ initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
46
+ injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
47
+ initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
48
+ injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
49
+ });
50
+
51
+ afterEach(() => {
52
+ // Restaurer les espions après chaque test pour ne pas affecter les autres tests
53
+ vi.restoreAllMocks();
54
+ });
55
+
56
+ it('should enable all trackers by default', () => {
57
+ ClientLibrary.initializeClient();
58
+
59
+ expect(startMouseSpy).toHaveBeenCalled();
60
+ expect(startKeystrokeSpy).toHaveBeenCalled();
61
+ expect(startClickSpy).toHaveBeenCalled();
62
+ expect(startTouchSpy).toHaveBeenCalled();
63
+ expect(startMotionSpy).toHaveBeenCalled();
64
+ expect(startRenderingSpy).toHaveBeenCalled();
65
+ expect(injectPhantomTrapsSpy).toHaveBeenCalled();
66
+ });
67
+
68
+ it('should disable trackers when configured', () => {
69
+ ClientLibrary.initializeClient({ mouse: false, keystrokes: false, clicks: false, touches: false, motion: false, rendering: false });
70
+
71
+ expect(startMouseSpy).not.toHaveBeenCalled();
72
+ expect(startKeystrokeSpy).not.toHaveBeenCalled();
73
+ expect(startClickSpy).not.toHaveBeenCalled();
74
+ expect(startTouchSpy).not.toHaveBeenCalled();
75
+ expect(startMotionSpy).not.toHaveBeenCalled();
76
+ expect(startRenderingSpy).not.toHaveBeenCalled();
77
+ });
78
+
79
+ it('should initialize WASM if wasmPath is configured', () => {
80
+ ClientLibrary.initializeClient({ wasmPath: '/fp.js' });
81
+
82
+ expect(initWasmSpy).toHaveBeenCalledWith('/fp.js');
83
+ });
84
+
85
+ it('should disable phantom traps when configured', () => {
86
+ ClientLibrary.initializeClient({ phantomTraps: false });
87
+
88
+ expect(injectPhantomTrapsSpy).not.toHaveBeenCalled();
89
+ });
90
+
91
+ it('should initialize honeypots with the provided field names', () => {
92
+ const honeypotFields = ['email_confirm', 'user_nickname'];
93
+ ClientLibrary.initializeClient({ honeypots: honeypotFields });
94
+
95
+ expect(initHoneypotsSpy).toHaveBeenCalledWith(honeypotFields);
96
+ });
97
+
98
+ it('should not initialize honeypots if the array is empty', () => {
99
+ ClientLibrary.initializeClient({ honeypots: [] });
100
+
101
+ expect(initHoneypotsSpy).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it('should inject trap URLs when provided', () => {
105
+ const urls = ['/trap1?sig=123', '/trap2?sig=456'];
106
+ ClientLibrary.initializeClient({ trapUrls: urls });
107
+
108
+ expect(injectTrapsSpy).toHaveBeenCalledWith(urls);
109
+ });
110
+
111
+ it('should not inject trap URLs if the array is empty', () => {
112
+ ClientLibrary.initializeClient({ trapUrls: [] });
113
+
114
+ expect(injectTrapsSpy).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('should initialize fetch interception when fetch config is present', () => {
118
+ const targetDomains = ['api.example.com'];
119
+ ClientLibrary.initializeClient({ fetch: { targetDomains } });
120
+
121
+ expect(initFetchSpy).toHaveBeenCalledWith(targetDomains);
122
+ });
123
+
124
+ it('should not initialize fetch interception if fetch config is absent', () => {
125
+ ClientLibrary.initializeClient(); // No fetch config
126
+
127
+ expect(initFetchSpy).not.toHaveBeenCalled();
128
+ });
129
+
130
+ it('should call all initializers correctly when a full config is provided', () => {
131
+ const config = {
132
+ mouse: true,
133
+ keystrokes: true,
134
+ honeypots: ['field1'],
135
+ trapUrls: ['/trap1'],
136
+ fetch: { targetDomains: ['api.com'] }
137
+ };
138
+ ClientLibrary.initializeClient(config);
139
+
140
+ expect(startMouseSpy).toHaveBeenCalled();
141
+ expect(startKeystrokeSpy).toHaveBeenCalled();
142
+ expect(initHoneypotsSpy).toHaveBeenCalledWith(config.honeypots);
143
+ expect(injectTrapsSpy).toHaveBeenCalledWith(config.trapUrls);
144
+ expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
145
+ });
146
+
144
147
  });
@@ -665,38 +665,6 @@ describe('Fingerprint & PoW Security Suite', () => {
665
665
  expect(next, 'next() should have been called for a request with a valid ticket').toHaveBeenCalled();
666
666
  });
667
667
 
668
- test('should issue a new challenge (re-challenge) if suspicion score is high even with a valid ticket', async () => {
669
- vi.spyOn(__internal, 'getSuspicionVector').mockResolvedValue({
670
- historyScore: 80,
671
- rotationScore: 80,
672
- headerAnomalyScore: 80,
673
- inconsistencyScore: 80,
674
- requestPatternScore: 80,
675
- honeypotScore: 0
676
- });
677
-
678
- const ip = '127.0.0.1';
679
- const expiry = Date.now() + 3600000;
680
- const signature = createHmac("sha256", process.env.POW_SECRET || "fallback-dev-secret-32-chars-minimum").update(`${ip}:${expiry}`).digest("hex");
681
- const validTicket = `${expiry}:${signature}`;
682
-
683
- const req = { path: '/', ip, cookies: { pow_clearance: validTicket }, query: {}, headers: { 'user-agent': 'test-ua' } };
684
- let sentStatus, sentBody;
685
- const res = {
686
- status: (s) => { sentStatus = s; return res; },
687
- send: (b) => { sentBody = b; },
688
- cookie: vi.fn()
689
- };
690
- const next = vi.fn();
691
-
692
- req.fingerprint = {};
693
- await powMiddleware(securityConfig)(req, res, next);
694
-
695
- expect(next).not.toHaveBeenCalled();
696
- expect(sentStatus).toBe(404);
697
- expect(sentBody).toContain('Enhanced Verification');
698
- });
699
-
700
668
  test('should redirect after a valid PoW solution is provided', async () => {
701
669
  // --- 1. Setup: Define context and mock a suspicious score ---
702
670
  const ip = '127.0.0.1';
@@ -2401,20 +2369,47 @@ describe('Subnet Scoring (Node.js)', () => {
2401
2369
  // 2. Some high scores, few devices. The subnet is calculated from the context.
2402
2370
  // The key is hardcoded here to match what the implementation would store.
2403
2371
  await inMemoryStore.set('subnet:10.0.0.0/24', {
2404
- highScoreCount: 5, // score += 5 * 2 = 10
2405
- deviceIds: ['d1', 'd2'], // count < 10, score += 0
2372
+ highScoreCount: 5,
2373
+ deviceIds: ['d1', 'd2'],
2406
2374
  });
2407
2375
  ({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
2408
- expect(subnetScore).toBe(100);
2376
+ expect(subnetScore).toBeGreaterThan(0);
2377
+ expect(subnetScore).toBeLessThanOrEqual(45.0); // Ambient score capped in Medium zone
2409
2378
 
2410
- // 3. Many devices and high scores
2379
+ // 3. Many devices and high scores, ambient score still capped at 45.0 (Medium asymptote)
2411
2380
  const deviceIds = Array.from({ length: 20 }, (_, i) => `d${i}`);
2412
2381
  await inMemoryStore.set('subnet:10.0.0.0/24', {
2413
- highScoreCount: 30, // score += min(40, 30 * 2) = 40
2414
- deviceIds: deviceIds, // count = 20. score += (20-10)*5 = 50
2382
+ highScoreCount: 30,
2383
+ deviceIds: deviceIds,
2384
+ });
2385
+ ({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
2386
+ expect(subnetScore).toBeLessThanOrEqual(45.0);
2387
+ expect(subnetScore).toBeGreaterThanOrEqual(30.0);
2388
+
2389
+ // 4. Nearby attacker in /30 (+45 boost), escalates score into High territory
2390
+ await inMemoryStore.set('subnet:10.0.0.0/24', {
2391
+ highScoreCount: 30,
2392
+ deviceIds: deviceIds,
2393
+ attackerIps: [{ ip: '10.0.0.26', lastSeen: Date.now(), score: 85 }]
2415
2394
  });
2416
2395
  ({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
2417
- expect(subnetScore).toBe(94.4);
2396
+ expect(subnetScore).toBeGreaterThanOrEqual(80.0);
2397
+
2398
+ // 5. Exact IP match (+55 boost), reaches 90-100
2399
+ await inMemoryStore.set('subnet:10.0.0.0/24', {
2400
+ highScoreCount: 30,
2401
+ deviceIds: deviceIds,
2402
+ attackerIps: [{ ip: '10.0.0.25', lastSeen: Date.now(), score: 95 }]
2403
+ });
2404
+ ({ subnetScore } = await __internal.getSubnetScore(context, 'device-1'));
2405
+ expect(subnetScore).toBeGreaterThanOrEqual(90.0);
2406
+ });
2407
+
2408
+ it('getIpCommonPrefixLength should accurately calculate common prefix bits', () => {
2409
+ const { getIpCommonPrefixLength } = __internal;
2410
+ expect(getIpCommonPrefixLength('10.0.0.25', '10.0.0.25')).toBe(32);
2411
+ expect(getIpCommonPrefixLength('10.0.0.24', '10.0.0.25')).toBe(31);
2412
+ expect(getIpCommonPrefixLength('192.168.1.10', '192.168.2.10')).toBe(22);
2418
2413
  });
2419
2414
  });
2420
2415
 
@@ -2611,6 +2606,14 @@ describe('Additional Suspicion Vectors Coverage', () => {
2611
2606
  });
2612
2607
 
2613
2608
  describe('Ed25519 Asymmetric Tickets', () => {
2609
+ const localStore = {
2610
+ _map: new Map(),
2611
+ async get(key) { return this._map.get(key); },
2612
+ async set(key, value) { this._map.set(key, value); },
2613
+ async has(key) { return this._map.has(key); },
2614
+ async delete(key) { this._map.delete(key); },
2615
+ async clear() { this._map.clear(); }
2616
+ };
2614
2617
  const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
2615
2618
  privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
2616
2619
  publicKeyEncoding: { format: 'pem', type: 'spki' }
@@ -2618,7 +2621,9 @@ describe('Ed25519 Asymmetric Tickets', () => {
2618
2621
  const privateKeyPem = privateKey;
2619
2622
  const publicKeyPem = publicKey;
2620
2623
 
2621
- beforeEach(() => {
2624
+ beforeEach(async () => {
2625
+ await localStore.clear();
2626
+ configureStore(localStore);
2622
2627
  process.env.ED25519_PRIVATE_KEY = privateKeyPem;
2623
2628
  process.env.ED25519_PUBLIC_KEY = publicKeyPem;
2624
2629
  });
@@ -2697,6 +2702,62 @@ describe('Ed25519 Asymmetric Tickets', () => {
2697
2702
  expect(resPoll.requests[0].req_id).toBe('req-123');
2698
2703
  expect(resPoll.requests[0].block_idx).toBe(42);
2699
2704
  });
2705
+
2706
+ test('WebRTC P2P Signaling Flow (Node.js)', async () => {
2707
+ const clientIp = '127.0.0.1';
2708
+ const nodeIdA = 'node-a-webrtc';
2709
+ const nodeIdB = 'node-b-webrtc';
2710
+ const clientSecretA = 'secret-a-webrtc';
2711
+ const clientSecretB = 'secret-b-webrtc';
2712
+
2713
+ const store = __internal.store;
2714
+ await store.set(`secret:${nodeIdA}`, { clientSecret: clientSecretA });
2715
+ await store.set(`secret:${nodeIdB}`, { clientSecret: clientSecretB });
2716
+
2717
+ await __internal.registerCooperativeNode(clientIp, nodeIdA, 'seed-a');
2718
+ await __internal.registerCooperativeNode(clientIp, nodeIdB, 'seed-b');
2719
+
2720
+ // 1. Découverte d'un pair dans le sous-réseau
2721
+ const sigFind = createHash('sha256')
2722
+ .update(`${clientSecretA}:find_peer:${nodeIdA}`)
2723
+ .digest('hex');
2724
+ const findRes = await __internal.handleCooperativeRequest({
2725
+ coop_op: 'find_peer',
2726
+ node_id: nodeIdA,
2727
+ coop_sig: sigFind
2728
+ }, clientIp);
2729
+ expect(findRes.status).toBe('peer_found');
2730
+ expect(findRes.peer_id).toBe(nodeIdB);
2731
+
2732
+ // 2. Envoi d'une offre SDP WebRTC (A -> B)
2733
+ const offerData = '{"type":"offer","sdp":"v=0..."}';
2734
+ const sigSignal = createHash('sha256')
2735
+ .update(`${clientSecretA}:webrtc_signal:${nodeIdA}:${nodeIdB}:offer:${offerData}`)
2736
+ .digest('hex');
2737
+ const signalRes = await __internal.handleCooperativeRequest({
2738
+ coop_op: 'webrtc_signal',
2739
+ node_id: nodeIdA,
2740
+ target_peer_id: nodeIdB,
2741
+ signal_type: 'offer',
2742
+ signal_data: offerData,
2743
+ coop_sig: sigSignal
2744
+ }, clientIp);
2745
+ expect(signalRes.status).toBe('signal_queued');
2746
+
2747
+ // 3. Récupération des signaux par le nœud B
2748
+ const sigPoll = createHash('sha256')
2749
+ .update(`${clientSecretB}:poll_signals:${nodeIdB}`)
2750
+ .digest('hex');
2751
+ const pollRes = await __internal.handleCooperativeRequest({
2752
+ coop_op: 'poll_signals',
2753
+ node_id: nodeIdB,
2754
+ coop_sig: sigPoll
2755
+ }, clientIp);
2756
+ expect(pollRes.status).toBe('ok');
2757
+ expect(pollRes.signals.length).toBe(1);
2758
+ expect(pollRes.signals[0].from_peer_id).toBe(nodeIdA);
2759
+ expect(pollRes.signals[0].signal_type).toBe('offer');
2760
+ });
2700
2761
  });
2701
2762
 
2702
2763
  describe('Botnet Cluster Scoring (Node.js)', () => {
@@ -138,5 +138,6 @@ describe('IP Reputation Local System (Node.js)', () => {
138
138
  }
139
139
  score = await __internal.getSubnetScore(mockContext);
140
140
  expect(score.subnetScore).toBeGreaterThan(0);
141
+ expect(score.subnetScore).toBeLessThanOrEqual(45.0); // Ambient cap
141
142
  });
142
143
  });