@anonympins/fingerprint 0.6.3 → 0.6.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.
@@ -111,5 +111,18 @@ export function createRedisStore(redisClient) {
111
111
  triggerFallback();
112
112
  }
113
113
  },
114
+ async clear() {
115
+ localStore.clear();
116
+ for (const timeout of localTimeouts.values()) {
117
+ clearTimeout(timeout);
118
+ }
119
+ localTimeouts.clear();
120
+ if (isDown) return;
121
+ try {
122
+ await redisClient.flushdb();
123
+ } catch (e) {
124
+ triggerFallback();
125
+ }
126
+ }
114
127
  };
115
128
  }
@@ -1,78 +1,81 @@
1
- /**
2
- * @file Creates a store adapter for Knex.js.
3
- * This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
4
- * It handles serialization of complex objects and TTL for automatic data expiration.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for a Knex.js client.
9
- *
10
- * **Note:** You must create the table yourself before using the store.
11
- * The table should have at least the following columns:
12
- * - `key` (string, primary key)
13
- * - `value` (text or json/jsonb)
14
- * - `expiresAt` (datetime or timestamp with time zone)
15
- *
16
- * Example schema for PostgreSQL:
17
- * ```sql
18
- * CREATE TABLE your_table_name (
19
- * "key" VARCHAR(255) PRIMARY KEY,
20
- * "value" TEXT NOT NULL,
21
- * "expiresAt" TIMESTAMPTZ
22
- * );
23
- * ```
24
- *
25
- * @param {import('knex').Knex} knex - An instance of the Knex client.
26
- * @param {string} [tableName='fingerprint_store'] - The name of the table to use.
27
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
28
- */
29
- export function createSqlStore(knex, tableName = 'fingerprint_store') {
30
- // Custom replacer/reviver to handle Set serialization, similar to the Redis store.
31
- const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
32
- const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
33
-
34
- return {
35
- async get(key) {
36
- const row = await knex(tableName).where('key', key).first();
37
- if (!row) return null;
38
-
39
- // Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
40
- if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
41
- await this.delete(key); // Clean up expired key.
42
- return null;
43
- }
44
-
45
- try {
46
- return JSON.parse(row.value, reviver);
47
- } catch (e) {
48
- // In case of malformed JSON, treat it as a miss.
49
- return null;
50
- }
51
- },
52
-
53
- async set(key, value, ttl) {
54
- const stringValue = JSON.stringify(value, replacer);
55
- const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
56
-
57
- // Use native upsert capabilities of Knex for different SQL dialects.
58
- await knex(tableName)
59
- .insert({ key, value: stringValue, expiresAt })
60
- .onConflict('key')
61
- .merge();
62
- },
63
-
64
- async has(key) {
65
- const row = await knex(tableName).where('key', key).first('key');
66
- if (!row) return false;
67
- // Also check for expiration here.
68
- if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
69
- return false;
70
- }
71
- return true;
72
- },
73
-
74
- async delete(key) {
75
- await knex(tableName).where('key', key).del();
76
- },
77
- };
1
+ /**
2
+ * @file Creates a store adapter for Knex.js.
3
+ * This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
4
+ * It handles serialization of complex objects and TTL for automatic data expiration.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for a Knex.js client.
9
+ *
10
+ * **Note:** You must create the table yourself before using the store.
11
+ * The table should have at least the following columns:
12
+ * - `key` (string, primary key)
13
+ * - `value` (text or json/jsonb)
14
+ * - `expiresAt` (datetime or timestamp with time zone)
15
+ *
16
+ * Example schema for PostgreSQL:
17
+ * ```sql
18
+ * CREATE TABLE your_table_name (
19
+ * "key" VARCHAR(255) PRIMARY KEY,
20
+ * "value" TEXT NOT NULL,
21
+ * "expiresAt" TIMESTAMPTZ
22
+ * );
23
+ * ```
24
+ *
25
+ * @param {import('knex').Knex} knex - An instance of the Knex client.
26
+ * @param {string} [tableName='fingerprint_store'] - The name of the table to use.
27
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
28
+ */
29
+ export function createSqlStore(knex, tableName = 'fingerprint_store') {
30
+ // Custom replacer/reviver to handle Set serialization, similar to the Redis store.
31
+ const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
32
+ const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
33
+
34
+ return {
35
+ async get(key) {
36
+ const row = await knex(tableName).where('key', key).first();
37
+ if (!row) return null;
38
+
39
+ // Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
40
+ if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
41
+ await this.delete(key); // Clean up expired key.
42
+ return null;
43
+ }
44
+
45
+ try {
46
+ return JSON.parse(row.value, reviver);
47
+ } catch (e) {
48
+ // In case of malformed JSON, treat it as a miss.
49
+ return null;
50
+ }
51
+ },
52
+
53
+ async set(key, value, ttl) {
54
+ const stringValue = JSON.stringify(value, replacer);
55
+ const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
56
+
57
+ // Use native upsert capabilities of Knex for different SQL dialects.
58
+ await knex(tableName)
59
+ .insert({ key, value: stringValue, expiresAt })
60
+ .onConflict('key')
61
+ .merge();
62
+ },
63
+
64
+ async has(key) {
65
+ const row = await knex(tableName).where('key', key).first('key');
66
+ if (!row) return false;
67
+ // Also check for expiration here.
68
+ if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
69
+ return false;
70
+ }
71
+ return true;
72
+ },
73
+
74
+ async delete(key) {
75
+ await knex(tableName).where('key', key).del();
76
+ },
77
+ async clear() {
78
+ await knex(tableName).del();
79
+ }
80
+ };
78
81
  }
@@ -92,8 +92,7 @@ describe('FingerprintEngine Challenge Validation', () => {
92
92
 
93
93
  // The client solves the challenge
94
94
  // --- FIX: Simulate the client creating the baseBlock for the solver ---
95
- const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-A:`;
96
- const baseBlock = new TextEncoder().encode(messageBase);
95
+ const baseBlock = new Uint8Array(challengeContext.baseBlock);
97
96
 
98
97
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
99
98
  // The memory challenge seed does not include the fingerprint.
@@ -153,7 +152,7 @@ describe('FingerprintEngine Challenge Validation', () => {
153
152
  // --- 2. Second request: Solution is submitted from "Machine B" ---
154
153
  // --- FIX: Simulate the client creating the baseBlock for the solver ---
155
154
  // The solver uses the fingerprint of the machine it's running on.
156
- const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:`;
155
+ const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:127.0.0.1::`;
157
156
  const baseBlock = new TextEncoder().encode(messageBase);
158
157
 
159
158
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
@@ -739,7 +739,7 @@ describe('Fingerprint & PoW Security Suite', () => {
739
739
  const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
740
740
 
741
741
  // The client constructs the base block and solves the challenge
742
- const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:`);
742
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
743
743
  const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTargetHex, () => {});
744
744
  const memSolution = await solveMemory(`:${nonce}:${clientSecret}`, memDifficulty);
745
745
 
@@ -821,7 +821,7 @@ describe('Fingerprint & PoW Security Suite', () => {
821
821
  const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
822
822
 
823
823
  // The server stores the challenge context. We need to ensure the `baseBlock` is present for verification.
824
- const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:`);
824
+ const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
825
825
  await inMemoryStore.set(`secret:${nonce}`, {
826
826
  clientSecret: clientSecret,
827
827
  cpuTarget: cpuTargetHex,
@@ -27,7 +27,7 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
27
27
  const solutions = [];
28
28
 
29
29
  for (let idx = 0; idx < 64; idx++) {
30
- let x = Math.fround(numericSeed + idx * 0.015);
30
+ let x = Math.fround((numericSeed + idx * 0.015) % 1); // Ensure x starts in [0, 1)
31
31
  for (let i = 0; i < iterations; i++) {
32
32
  x = Math.fround(rFloat * x * Math.fround(1.0 - x));
33
33
  }
@@ -53,19 +53,21 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
53
53
  const solutions = [];
54
54
 
55
55
  for (let idx = 0; idx < 64; idx++) {
56
- let x = Math.fround(numericSeed + idx * 0.015);
56
+ let x = Math.fround((numericSeed + idx * 0.015) % 1); // Ensure x starts in [0, 1)
57
57
  for (let i = 0; i < iterations; i++) {
58
58
  x = Math.fround(rFloat * x * Math.fround(1.0 - x));
59
59
  }
60
60
  solutions.push(x.toFixed(6));
61
61
  }
62
62
 
63
- // Tamper with the 12th channel (one of the default sample indices [0, 12, 35, 57])
63
+ // Tamper with one of the dynamically derived sample indices
64
+ const sampleIndices = GpuPowSolver.deriveSampleIndices('127.0.0.1', 'gpu-pow-salt');
65
+ const tamperedIndex = sampleIndices[0];
64
66
  const tamperedSolutions = [...solutions];
65
- const originalVal = parseFloat(tamperedSolutions[12]);
66
- tamperedSolutions[12] = (originalVal + 0.0002).toFixed(6); // exceeding 1e-4 tolerance
67
-
68
- expect(GpuPowSolver.verify(seed, iterations, tamperedSolutions.join(','))).toBe(false);
67
+ const originalVal = parseFloat(tamperedSolutions[tamperedIndex]);
68
+ tamperedSolutions[tamperedIndex] = (originalVal + 0.1).toFixed(6); // exceeding tolerance
69
+ const verificationResult = GpuPowSolver.verify(seed, iterations, tamperedSolutions.join(','), '127.0.0.1', 'gpu-pow-salt');
70
+ expect(verificationResult).toBe(false);
69
71
  });
70
72
 
71
73
  it('should fallback to WebGL2 if WebGPU is not supported', async () => {
@@ -53,6 +53,47 @@ class AutoTuner
53
53
  $this->onCleanup = $options['onCleanup'] ?? null;
54
54
  $this->savePath = $options['savePath'] ?? null;
55
55
  $this->validationTolerance = $options['validationTolerance'] ?? 0.15;
56
+
57
+ if (isset($this->securityConfig['logger']) && is_callable($this->securityConfig['logger']) && !isset($this->securityConfig['logger_wrapped'])) {
58
+ $originalLogger = $this->securityConfig['logger'];
59
+ $maxPercentage = $this->securityConfig['autotuning']['maxDensityPercentage'] ?? 0.02;
60
+
61
+ $this->securityConfig['logger'] = function (array $log) use ($originalLogger, $maxPercentage) {
62
+ $ip = $log['clientIp'] ?? $log['ip'] ?? null;
63
+ $subnet = $ip ? RequestUtils::getIpSubnet($ip, 24, 48) : null;
64
+ $fp = $log['deviceHash'] ?? $log['fingerprint'] ?? $log['deviceFingerprint'] ?? null;
65
+ $stableFp = $fp ? RequestUtils::extractStablePart($fp) : null;
66
+
67
+ if (count($this->trafficData) > 0) {
68
+ $total = count($this->trafficData);
69
+ $ipMatchCount = 0;
70
+ $fpMatchCount = 0;
71
+ $matchedKey = null;
72
+
73
+ foreach ($this->trafficData as $key => $existingLog) {
74
+ $logIp = $existingLog['clientIp'] ?? $existingLog['ip'] ?? null;
75
+ $logSubnet = $logIp ? RequestUtils::getIpSubnet($logIp, 24, 48) : null;
76
+ $logFp = $existingLog['deviceHash'] ?? $existingLog['fingerprint'] ?? $existingLog['deviceFingerprint'] ?? null;
77
+ $logStableFp = $logFp ? RequestUtils::extractStablePart($logFp) : null;
78
+
79
+ $isIpMatch = $subnet && $logSubnet === $subnet;
80
+ $isFpMatch = $stableFp && $logStableFp === $stableFp;
81
+ if ($isIpMatch) $ipMatchCount++;
82
+ if ($isFpMatch) $fpMatchCount++;
83
+ if ($isIpMatch || $isFpMatch) $matchedKey = $key;
84
+ }
85
+ if (($subnet && ($ipMatchCount / $total) > $maxPercentage) || ($stableFp && ($fpMatchCount / $total) > $maxPercentage)) {
86
+ if ($matchedKey !== null) {
87
+ $this->trafficData[$matchedKey]['instancesCount'] = ($this->trafficData[$matchedKey]['instancesCount'] ?? 1) + 1;
88
+ $this->trafficData[$matchedKey]['weight'] = ($this->trafficData[$matchedKey]['weight'] ?? 1.0) + 1.0;
89
+ }
90
+ return;
91
+ }
92
+ }
93
+ $originalLogger($log);
94
+ };
95
+ $this->securityConfig['logger_wrapped'] = true;
96
+ }
56
97
  }
57
98
 
58
99
  /**
@@ -32,12 +32,33 @@ class ChallengeUtils
32
32
  return abs($hash % 1000000) / 1000000;
33
33
  }
34
34
 
35
- public static function verifyGpuPow(string $seed, int $iterations, string $solution, array $sampleIndices = [0, 12, 35, 57]): bool
35
+ public static function deriveSampleIndices(string $clientIp, string $secret): array
36
+ {
37
+ $timeWindow = (int)floor(time() / (60 * 5));
38
+ $message = "{$clientIp}:{$timeWindow}";
39
+ $hmacHex = hash_hmac('sha256', $message, $secret ?: 'gpu-pow-salt');
40
+ $hmacBytes = array_values(unpack('C*', hex2bin($hmacHex)));
41
+
42
+ $indices = [];
43
+ for ($i = 0; $i < 4; $i++) {
44
+ $offset = ($i * 2) % count($hmacBytes);
45
+ $value = ($hmacBytes[$offset] << 8) | $hmacBytes[($offset + 1) % count($hmacBytes)];
46
+ $idx = $value % 64;
47
+ while (in_array($idx, $indices, true)) {
48
+ $idx = ($idx + 1) % 64;
49
+ }
50
+ $indices[] = $idx;
51
+ }
52
+ return $indices;
53
+ }
54
+
55
+ public static function verifyGpuPow(string $seed, int $iterations, string $solution, string $clientIp = '127.0.0.1', string $secret = 'gpu-pow-salt'): bool
36
56
  {
37
57
  $values = explode(',', $solution);
38
58
  if (count($values) !== 64) {
39
59
  return false;
40
60
  }
61
+ $sampleIndices = self::deriveSampleIndices($clientIp, $secret);
41
62
  $numericSeed = self::hashSeedToFloat($seed);
42
63
  $r = 3.9999;
43
64
  foreach ($sampleIndices as $idx) {
@@ -203,9 +224,22 @@ class ChallengeUtils
203
224
  return ['error' => 'Missing signature'];
204
225
  }
205
226
 
206
- // Ban the ZKP public key for 30 days
207
- $store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
208
- return ['status' => 'synchronized'];
227
+ $peersKey = "fed-peers:{$zkpY}";
228
+ $reportedPeers = $store->get($peersKey) ?: [];
229
+ if (!is_array($reportedPeers)) {
230
+ $reportedPeers = [];
231
+ }
232
+ if (!in_array($clientIp, $reportedPeers, true)) {
233
+ $reportedPeers[] = $clientIp;
234
+ $store->set($peersKey, $reportedPeers, 86400 * 30);
235
+ }
236
+
237
+ $threshold = $config['federationConsensusThreshold'] ?? 3;
238
+ if (count($reportedPeers) >= $threshold) {
239
+ $store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
240
+ return ['status' => 'synchronized', 'banned' => true];
241
+ }
242
+ return ['status' => 'synchronized', 'banned' => false, 'reportsCount' => count($reportedPeers)];
209
243
  }
210
244
 
211
245
  $nodeId = $params['node_id'] ?? '';
@@ -633,14 +667,14 @@ class ChallengeUtils
633
667
  /**
634
668
  * Crée le bloc de données de base pour le challenge CPU.
635
669
  */
636
- public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint): string
670
+ public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint, string $clientIp = '', string $tlsSessionId = ''): string
637
671
  {
638
672
  $parts = explode('|', $fingerprint);
639
673
  $filteredParts = array_filter($parts);
640
674
  sort($filteredParts);
641
675
  $sortedFingerprint = implode('|', $filteredParts);
642
676
 
643
- return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:";
677
+ return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:{$clientIp}:{$tlsSessionId}:";
644
678
  }
645
679
 
646
680
  /**
@@ -58,6 +58,7 @@ class SecurityProfiles
58
58
  'decayFactor' => 0.9,
59
59
  'inactivityReset' => 5000,
60
60
  ],
61
+ 'filterWhitelist' => 85.0, // Stratégie d'inspection modérée pour les IP/chemins en liste blanche
61
62
  'wasm' => true,
62
63
  ],
63
64
 
@@ -108,6 +109,7 @@ class SecurityProfiles
108
109
  ],
109
110
  'challengeNewDevices' => true, // Challenge all new devices
110
111
  'wasm' => true,
112
+ 'filterWhitelist' => true, // Tout comportement d'attaque certain bypass immédiatement la liste blanche
111
113
  ],
112
114
 
113
115
  /**
@@ -155,6 +157,7 @@ class SecurityProfiles
155
157
  ],
156
158
  // This would be a callable in PHP, but for now, we represent its intent.
157
159
  'isApiRequest' => 'req.path.startsWith("/api/") || req.headers.accept?.includes("application/json")',
160
+ 'filterWhitelist' => 75.0, // Seuil bas pour parer au vol de clés/tokens API légitimes
158
161
  'wasm' => true,
159
162
  ],
160
163
 
@@ -204,6 +207,7 @@ class SecurityProfiles
204
207
  'decayFactor' => 0.92,
205
208
  'inactivityReset' => 10000,
206
209
  ],
210
+ 'filterWhitelist' => 90.0, // Très tolérant, n'inspecte que si le score est presque au blocage
207
211
  'wasm' => true,
208
212
  ],
209
213
 
@@ -257,6 +261,7 @@ class SecurityProfiles
257
261
  // This would be a callable in PHP, but for now, we represent its intent.
258
262
  'isApiRequest' => 'req.path.startsWith("/api/cart") || req.path.startsWith("/api/stock") || req.path.startsWith("/api/checkout")',
259
263
  'wasm' => true,
264
+ 'filterWhitelist' => true, // Tolérance zéro pour le scraping / scalping distribué
260
265
  ],
261
266
  ];
262
267
 
@@ -237,6 +237,9 @@
237
237
  $this->logger = isset($securityConfig['logger']) && is_callable($securityConfig['logger']) ? new Logger($securityConfig['logger']) : null;
238
238
  $this->dryRun = $securityConfig['dryRun'] ?? false;
239
239
  $this->validateConfig($securityConfig);
240
+ if ($this->securityConfig['reset'] ?? false) {
241
+ $this->resetStore();
242
+ }
240
243
  }
241
244
 
242
245
  /**
@@ -252,6 +255,18 @@
252
255
  $this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
253
256
  }
254
257
 
258
+ /**
259
+ * Réinitialise le store de persistance actif.
260
+ */
261
+ public function resetStore(): void
262
+ {
263
+ $store = StoreManager::getStore();
264
+ if (method_exists($store, 'clear')) {
265
+ $store->clear();
266
+ }
267
+ $this->log('Store has been reset/cleared.');
268
+ }
269
+
255
270
  private function validateConfig(array $config): void
256
271
  {
257
272
  if (empty($config)) {
@@ -266,7 +281,7 @@
266
281
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
267
282
  'similarityThreshold', 'summary', 'description',
268
283
  'ed25519_private_key', 'ed25519_public_key',
269
- 'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret'
284
+ 'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset'
270
285
  ];
271
286
 
272
287
  if (empty($config['weights'])) {
@@ -497,28 +512,50 @@
497
512
  */
498
513
  private function checkAllowlists(RequestContext $context): bool
499
514
  {
515
+ $whitelisted = false;
516
+ $type = '';
517
+
500
518
  if ($this->isIpInAllowlist($context->clientIp)) {
501
- $this->log('IP in allowlist - allowing request', ['clientIp' => $context->clientIp]);
502
- return true;
503
- }
504
- if ($this->isPathInAllowlist($context->path)) {
505
- $this->log('Path in allowlist - allowing request', ['path' => $context->path]);
506
- return true;
507
- }
508
- if ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
509
- $this->log('Host and path in allowlist - allowing request', ['host' => $context->getHeader('host'), 'path' => $context->path]);
510
- return true;
511
- }
512
- // NOUVEAU: Vérifier la liste blanche GraphQL
513
- if ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
514
- $this->log('GraphQL operation in allowlist - allowing request', ['operation' => "{$context->graphqlOperation['type']}:{$context->graphqlOperation['name']}"]);
515
- return true;
516
- }
517
- if ($this->verifyWhitelistedBot($context)) {
518
- $this->log('Whitelisted bot verified - allowing request', ['clientIp' => $context->clientIp]);
519
+ $whitelisted = true;
520
+ $type = 'allowlist';
521
+ } elseif ($this->isPathInAllowlist($context->path)) {
522
+ $whitelisted = true;
523
+ $type = 'path_allowlist';
524
+ } elseif ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
525
+ $whitelisted = true;
526
+ $type = 'host_path_allowlist';
527
+ } elseif ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
528
+ $whitelisted = true;
529
+ $type = 'graphql_operation_allowlist';
530
+ } elseif ($this->verifyWhitelistedBot($context)) {
531
+ $whitelisted = true;
532
+ $type = 'bot';
533
+ }
534
+
535
+ if ($whitelisted) {
536
+ $filterWhitelist = $this->securityConfig['filterWhitelist'] ?? false;
537
+ $bypassWhitelist = false;
538
+ if ($filterWhitelist === true) {
539
+ $bypassWhitelist = $this->hasCertainAttack($context);
540
+ } elseif (is_numeric($filterWhitelist)) {
541
+ if ($context->preCalculatedScore === null) {
542
+ $suspicionVector = [];
543
+ $context->preCalculatedVector = $this->getSuspicionVector($context, $suspicionVector);
544
+ $context->preCalculatedScore = $this->calculateFinalScore($context->preCalculatedVector);
545
+ }
546
+ if ($context->preCalculatedScore > $filterWhitelist) {
547
+ $bypassWhitelist = true;
548
+ }
549
+ }
550
+
551
+ if ($bypassWhitelist) {
552
+ $this->log('Whitelisted request exceeds filter threshold - bypassing whitelist bypass', ['clientIp' => $context->clientIp, 'path' => $context->path]);
553
+ return false;
554
+ }
555
+ $this->log("IP/Path in allowlist ({$type}) - allowing request", ['clientIp' => $context->clientIp, 'path' => $context->path]);
519
556
  return true;
520
557
  }
521
-
558
+
522
559
  return false;
523
560
  }
524
561
 
@@ -908,6 +945,23 @@
908
945
  }
909
946
  }
910
947
 
948
+ /**
949
+ * Évalue si la requête présente des caractéristiques d'attaque flagrantes ou a déclenché un honeypot.
950
+ */
951
+ private function hasCertainAttack(RequestContext $context): bool
952
+ {
953
+ $honeypotConfig = $this->securityConfig['honeypot'] ?? [];
954
+ $honeypot = RequestUtils::getHoneypotScore($context, $honeypotConfig);
955
+ if (($honeypot['honeypotScore'] ?? 0.0) >= 100.0) {
956
+ return true;
957
+ }
958
+ $bot = RequestUtils::getBotScore($context);
959
+ if (($bot['botScore'] ?? 0.0) >= 100.0) {
960
+ return true;
961
+ }
962
+ return false;
963
+ }
964
+
911
965
  /**
912
966
  * Traite une requête entrante et retourne une décision.
913
967
  * @param RequestContext $context Le contexte de la requête.
@@ -1032,6 +1086,28 @@
1032
1086
  $problemManager = \Anonympins\Fingerprint\ProblemManager::getInstance($configPath, $store);
1033
1087
  // FIX: La solution est directement le $workResult, pas une sous-propriété.
1034
1088
  $problemManager->integrateSolution($problemId, $workResult);
1089
+
1090
+ // Si le problème résolu est l'auto-tuning de sécurité et que l'auto-tuning est activé,
1091
+ // on applique directement la meilleure solution calculée au moteur en direct.
1092
+ if ($problemId === 'security_auto_tuning' && ($this->securityConfig['autotuning']['enabled'] ?? false)) {
1093
+ $paretoFront = $workResult['paretoFront'] ?? null;
1094
+ if (is_array($paretoFront) && !empty($paretoFront)) {
1095
+ $bestSolution = $paretoFront[0];
1096
+ $minDistance = sqrt(pow((float)$bestSolution['objectives'][0], 2) + pow((float)$bestSolution['objectives'][1], 2));
1097
+ for ($i = 1; $i < count($paretoFront); $i++) {
1098
+ $distance = sqrt(pow((float)$paretoFront[$i]['objectives'][0], 2) + pow((float)$paretoFront[$i]['objectives'][1], 2));
1099
+ if ($distance < $minDistance) {
1100
+ $minDistance = $distance;
1101
+ $bestSolution = $paretoFront[$i];
1102
+ }
1103
+ }
1104
+ if (isset($bestSolution['solution'])) {
1105
+ $this->updateConfig($bestSolution['solution']);
1106
+ $this->log('Useful Work auto-tuning applied successfully to live config.');
1107
+ }
1108
+ }
1109
+ }
1110
+
1035
1111
  $isValid = true;
1036
1112
  // FIX: Générer un vrai ticket pour uPoW, comme pour un PoW normal.
1037
1113
  $ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000; // 1 heure par défaut
@@ -1113,8 +1189,8 @@
1113
1189
  return $decision;
1114
1190
  }
1115
1191
 
1116
- $suspicionVector = $this->getSuspicionVector($context, $suspicionVector);
1117
- $finalScore = $this->calculateFinalScore($suspicionVector);
1192
+ $suspicionVector = $context->preCalculatedVector ?? $this->getSuspicionVector($context, $suspicionVector);
1193
+ $finalScore = $context->preCalculatedScore ?? $this->calculateFinalScore($suspicionVector);
1118
1194
  $this->log('Suspicion vector and final score calculated', [
1119
1195
  'finalScore' => round($finalScore, 2),
1120
1196
  'vector' => $suspicionVector
@@ -1173,10 +1249,21 @@
1173
1249
  // 1. Valider cryptographiquement la preuve avant de bannir/diffuser
1174
1250
  if (ChallengeUtils::verifyZkpProof($zkpY, $zkpT, $zkpS)) {
1175
1251
  // 2. Dédoublonner : Ne diffuser que si la clé n'est pas déjà bannie
1176
- if (!$store->has("banned-zkp-y:{$zkpY}")) {
1252
+ $peersKey = "fed-peers:{$zkpY}";
1253
+ $reportedPeers = $store->get($peersKey) ?: [];
1254
+ if (!is_array($reportedPeers)) {
1255
+ $reportedPeers = [];
1256
+ }
1257
+ if (!in_array('local', $reportedPeers, true)) {
1258
+ $reportedPeers[] = 'local';
1259
+ $store->set($peersKey, $reportedPeers, 86400 * 30);
1260
+ }
1261
+
1262
+ $threshold = $this->securityConfig['federationConsensusThreshold'] ?? 3;
1263
+ if (count($reportedPeers) >= $threshold) {
1177
1264
  $store->set("banned-zkp-y:{$zkpY}", true, 86400 * 30);
1178
- $this->broadcastBannedZkp($zkpY);
1179
1265
  }
1266
+ $this->broadcastBannedZkp($zkpY);
1180
1267
  }
1181
1268
  }
1182
1269
 
@@ -1354,7 +1441,8 @@
1354
1441
  $memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
1355
1442
 
1356
1443
  $originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
1357
- $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
1444
+ $tlsSessionId = $context->tlsSessionId ?? '';
1445
+ $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $context->clientIp, $tlsSessionId);
1358
1446
 
1359
1447
  $challengeContext = [
1360
1448
  'clientSecret' => $clientSecret,