@anonympins/fingerprint 0.1.1 → 0.1.3

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.
package/README.md CHANGED
@@ -97,6 +97,7 @@ const securityConfig = {
97
97
  ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
98
98
  challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
99
99
  deviceIdCookieMaxAge: undefined, // By default, it's a session cookie. Set a value in ms for a persistent cookie.
100
+ challengePagePath: './path/to/your/custom-challenge-page.html', // (Optional) Path to a custom HTML template for the challenge page.
100
101
  verbose: true, // set to true to log for fingerprint detection output
101
102
  patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
102
103
  velocityThreshold: 800, // ms between requests to be considered "fast"
@@ -199,7 +200,50 @@ app.use((req, res, next) => {
199
200
 
200
201
  app.listen(3000, () => console.log('Server started on port 3000'));
201
202
  ```
203
+ ### Customizing the Challenge Page
202
204
 
205
+ You can provide your own HTML template for the Proof-of-Work challenge page to maintain a consistent user experience with your brand.
206
+
207
+ 1. **Configuration**: In your `securityConfig`, specify the path to your template file using the `challengePagePath` option.
208
+
209
+ ```javascript
210
+ const securityConfig = {
211
+ // ... other options
212
+ challengePagePath: './path/to/your/custom-challenge-page.html',
213
+ };
214
+ ```
215
+
216
+ 2. **Template Placeholders**: Your HTML file **must** contain the following placeholders. The system will replace them with the dynamic JavaScript code required to run the challenge.
217
+
218
+ * `<!-- FINGERPRINT_SOLVER_SCRIPT -->`: This will be replaced by the script that contains the logic for solving the CPU and memory challenges.
219
+ * `<!-- FINGERPRINT_CHALLENGE_SCRIPT -->`: This will be replaced by the script that initiates the challenge with the specific parameters for the current request (nonce, difficulty, etc.).
220
+ * `<!-- FINGERPRINT_TRAPS -->`: This will be replaced by hidden "honeypot" links designed to trap simple bots. This placeholder is crucial for an effective defense.
221
+
222
+ #### Example Custom HTML Template
223
+
224
+ Here is a basic example of what your `custom-challenge-page.html` could look like:
225
+
226
+ ```html
227
+ <!DOCTYPE html>
228
+ <html lang="en">
229
+ <head>
230
+ <meta charset="UTF-8">
231
+ <title>Security Verification</title>
232
+ <style>
233
+ body { font-family: sans-serif; text-align: center; padding-top: 50px; }
234
+ h1 { color: #333; }
235
+ </style>
236
+ </head>
237
+ <body>
238
+ <h1>Please wait while we verify your connection...</h1>
239
+ <div id="loader" style="margin:20px;">⚙️ Initializing verification...</div>
240
+
241
+ <script><!-- FINGERPRINT_SOLVER_SCRIPT --></script>
242
+ <script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script>
243
+ <!-- FINGERPRINT_TRAPS -->
244
+ </body>
245
+ </html>
246
+ ```
203
247
  ## Public API
204
248
 
205
249
  In addition to the main middleware, several functions are exported to allow for more advanced integrations.
@@ -238,9 +238,9 @@ const ClientLibrary = {
238
238
 
239
239
  _isFetchPatched: false,
240
240
  _interceptorChain: [],
241
- // On "lie" (bind) la fonction fetch à son contexte d'origine (window) pour éviter les erreurs "Illegal invocation".
242
- // C'est la correction clé pour le problème que vous rencontrez.
243
- _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : () => Promise.reject(new Error('fetch is not available')),
241
+ // On stocke la fonction fetch originale et on la lie à son contexte (window)
242
+ // pour éviter les erreurs "Illegal invocation" si une autre lib la modifie.
243
+ _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
244
244
 
245
245
  /**
246
246
  * Adds an interceptor function to the `fetch` chain.
@@ -256,7 +256,7 @@ const ClientLibrary = {
256
256
  },
257
257
 
258
258
  patchGlobalFetch() {
259
- if (this._isFetchPatched || typeof window === 'undefined') return;
259
+ if (this._isFetchPatched || !this._originalFetch) return;
260
260
 
261
261
  this._isFetchPatched = true;
262
262
  window.fetch = (resource, options) => {
package/fingerprint.js CHANGED
@@ -1,4 +1,3 @@
1
- // C:/Dev/games.primals.net/src/utils/fingerprint.js
2
1
  import crypto from "node:crypto";
3
2
  import { BlockList } from "node:net";
4
3
  import dns from "node:dns/promises";
@@ -30,17 +29,19 @@ const getPowSolverCode = () => {
30
29
  try {
31
30
  const __filename = fileURLToPath(import.meta.url);
32
31
  const __dirname = dirname(__filename);
33
- const solverPath = join(__dirname, 'pow.solver.js');
32
+ const solverPath = join(__dirname, 'pow.solver.inline.js'); // Use the inline version
34
33
  return readFileSync(solverPath, 'utf-8');
35
34
  } catch (error) {
36
35
  console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
37
36
  // Fallback inline code if file cannot be loaded
38
37
  return `(function(global){
39
- async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
38
+ async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){ const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
40
39
  const cpuTarget = BigInt(target);
41
40
  let cpuSolution = 0;
41
+ const ipPart = clientIp || '';
42
42
  while(true){
43
- const msg = clientSecret ? clientIp+':'+nonce+':'+cpuSolution+':'+clientSecret : clientIp+':'+nonce+':'+cpuSolution;
43
+ // When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
44
+ const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
44
45
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
45
46
  const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
46
47
  if(BigInt('0x'+hashHex) < cpuTarget) break;
@@ -95,9 +96,39 @@ const getPowSolverCode = () => {
95
96
  const solutionDistance=evaluatePathDistance(cities,solutionPath);
96
97
  return{path:solutionPath,distance:solutionDistance};
97
98
  }
99
+ async function solveChallenge(challenge) {
100
+ const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
101
+ const solutions = {};
102
+
103
+ switch (type) {
104
+ case 'cpu_target':
105
+ solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
106
+ break;
107
+ case 'cpu_mem':
108
+ case 'cpu_mem_inline':
109
+ const memSeed = nonce + ":" + clientSecret;
110
+ const [cpuSol, memSol] = await Promise.all([
111
+ solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
112
+ solveMemory(memSeed, memDifficulty)
113
+ ]);
114
+ solutions.cpu = cpuSol;
115
+ solutions.mem = memSol;
116
+ break;
117
+ case 'tsp':
118
+ const tspResult = await solveTsp(cities, targetMaxDistance);
119
+ solutions.tsp = tspResult.path;
120
+ solutions.distance = tspResult.distance;
121
+ break;
122
+ default:
123
+ throw new Error(\`Unknown challenge type: \${type}\`);
124
+ }
125
+
126
+ return solutions;
127
+ }
98
128
  global.solveCpuChallengeInline=solveCpuTargetInline;
99
129
  global.solveMemoryChallenge=solveMemory;
100
130
  global.solveTspChallenge=solveTsp;
131
+ global.solveChallenge=solveChallenge;
101
132
  })(typeof window!=='undefined'?window:global);`;
102
133
  }
103
134
  };
@@ -1224,48 +1255,61 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1224
1255
  * @param {string} clientIp - The client's IP address.
1225
1256
  * @returns {string} HTML content.
1226
1257
  */
1227
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret) {
1258
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
1228
1259
  const { nonce, target, path } = cpuChallengeDetails;
1229
1260
  const solverCode = getPowSolverCode();
1230
- return `
1231
- <html><head><title>Advanced Security Check</title></head>
1232
- <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
1233
- <h1>Enhanced Verification... (Level 2)</h1>
1234
- <p>Your activity requires an additional security check. This may take a few moments.</p>
1235
- <div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div>
1236
- <script>${solverCode}</script>
1237
- <script>
1238
- async function solve() {
1239
- const nonce = "${nonce}";
1240
- const path = "${path}";
1241
- const clientSecret = "${clientSecret}";
1242
- const clientIp = "${clientIp}";
1243
- const cpuTarget = BigInt("0x${target}");
1244
- const memDifficulty = ${memoryDifficulty};
1245
1261
 
1246
- // --- CPU Challenge ---
1247
- document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1248
- const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, (progress) => {
1249
- // Optional progress callback
1250
- });
1262
+ const challengeScript = `
1263
+ async function solve() {
1264
+ const nonce = "${nonce}";
1265
+ const path = "${path}";
1266
+ const clientSecret = "${clientSecret}";
1267
+ const clientIp = "${clientIp}";
1268
+ const cpuTarget = BigInt("0x${target}");
1269
+ const memDifficulty = ${memoryDifficulty};
1270
+
1271
+ // --- CPU Challenge ---
1272
+ document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1273
+ const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, (progress) => {
1274
+ // Optional progress callback
1275
+ });
1251
1276
 
1252
- // --- Memory Challenge ---
1253
- document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
1254
- await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1277
+ // --- Memory Challenge ---
1278
+ document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
1279
+ await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1255
1280
 
1256
- let memSolution = 0;
1257
- try {
1258
- const memSeed = nonce + ":" + clientSecret;
1259
- memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
1260
- } catch(e) {
1261
- document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1262
- return;
1263
- }
1264
- window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1265
- }
1266
- solve();
1267
- </script>
1268
- </body></html>`;
1281
+ let memSolution = 0;
1282
+ try {
1283
+ const memSeed = nonce + ":" + clientSecret;
1284
+ memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
1285
+ } catch(e) {
1286
+ document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1287
+ return;
1288
+ }
1289
+ window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1290
+ }
1291
+ solve();
1292
+ `;
1293
+
1294
+ let htmlTemplate;
1295
+ const customTemplatePath = securityConfig?.challengePagePath;
1296
+
1297
+ if (customTemplatePath) {
1298
+ try {
1299
+ htmlTemplate = readFileSync(customTemplatePath, 'utf-8');
1300
+ } catch (error) {
1301
+ console.warn(`[Fingerprint] Could not load custom challenge page at '${customTemplatePath}'. Falling back to default. Error: ${error.message}`);
1302
+ }
1303
+ }
1304
+
1305
+ if (!htmlTemplate) {
1306
+ htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`;
1307
+ }
1308
+
1309
+ return htmlTemplate
1310
+ .replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
1311
+ .replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript)
1312
+ .replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
1269
1313
  }
1270
1314
 
1271
1315
  /**
@@ -1276,20 +1320,19 @@ export function verifyCpuTargetPoWAndGenerateTicket(
1276
1320
  ticketMaxAge, // NOUVEAU: Durée de validité du ticket configurable
1277
1321
  nonce,
1278
1322
  solution,
1279
- suspicionFactor,
1280
1323
  clientSecret, // Le secret est maintenant requis
1324
+ target, // La cible est maintenant passée directement
1281
1325
  ) {
1282
- const target = calculateTarget(suspicionFactor);
1283
1326
  const message = clientSecret
1284
- ? `${clientIp}:${nonce}:${solution}:${clientSecret}`
1285
- : `${clientIp}:${nonce}:${solution}`;
1327
+ ? `${nonce}:${solution}:${clientSecret}` // FIX: Ne pas inclure l'IP si un secret client est utilisé
1328
+ : `${clientIp}:${nonce}:${solution}`; // L'IP est utilisée uniquement pour les challenges sans secret (plus anciens/simples)
1286
1329
  const hash = crypto
1287
1330
  .createHash("sha256")
1288
1331
  .update(message)
1289
1332
  .digest("hex");
1290
1333
  const hashAsInt = BigInt("0x" + hash);
1291
1334
 
1292
- if (hashAsInt < target) {
1335
+ if (hashAsInt < BigInt("0x" + target)) {
1293
1336
  // The comparison is direct with native BigInts
1294
1337
  // The proof is valid, generate the ticket
1295
1338
  const expiry = Date.now() + (ticketMaxAge || 3600000); // Utilise la durée passée ou un fallback.
@@ -1304,7 +1347,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
1304
1347
  }
1305
1348
 
1306
1349
  const staticExtensions = new RegExp(
1307
- "\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest)$",
1350
+ "\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
1308
1351
  "i",
1309
1352
  );
1310
1353
  const isStaticResource = (path) => staticExtensions.test(path);
@@ -1381,22 +1424,28 @@ function determineOptimalTicketTtl(suspicionScore) {
1381
1424
  * @private
1382
1425
  */
1383
1426
  function isMalicious(str) {
1427
+ if (typeof str !== 'string') return false;
1384
1428
  // Regex pour les injections SQL et NoSQL de base
1385
1429
  // Ajout de la détection des injections basées sur le temps (SLEEP, BENCHMARK, WAITFOR) et d'autres commandes dangereuses.
1386
- const injectionRegex = /(\$ne|' OR '1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
1430
+ const injectionRegex = /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
1387
1431
  // Regex pour les injections plus avancées
1388
1432
  const log4ShellRegex = /\$\{jndi:(ldap|rmi|dns):/i;
1389
1433
  const sstiRegex = /\{\{.*\}\}|\{%.*%\}/; // Détecte les syntaxes de type Jinja2, Twig, etc.
1390
1434
  const xxeRegex = /<!ENTITY\s+.*SYSTEM/i;
1391
1435
  const pathTraversalRegex = /(\.\.\/|\.\.\\)/;
1392
- // NOUVEAU : Regex pour les injections de commandes basiques.
1393
- // Cible les séparateurs de commandes et les backticks d'exécution.
1394
- const commandInjectionRegex = /(&&|\|\||;|\n|`)/;
1436
+ // Regex pour les injections de commandes.
1437
+ // Elle détecte deux cas :
1438
+ // 1. L'utilisation de backticks `` pour l'exécution de commandes.
1439
+ // 2. Des commandes dangereuses (comme rm, whoami) précédées par un séparateur de commande (;, &&, ||, |)
1440
+ // pour éviter les faux positifs sur des phrases comme "A normal command like ls -la".
1441
+ const commandInjectionRegex = /`.*`|[\n;&|]\s*(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i;
1395
1442
 
1396
1443
  return injectionRegex.test(str) || log4ShellRegex.test(str) || sstiRegex.test(str) || xxeRegex.test(str) || pathTraversalRegex.test(str) || commandInjectionRegex.test(str);
1397
1444
  }
1398
1445
 
1399
1446
  // --- Middleware Proof-of-Work (Le péage) ---
1447
+ export { isMalicious };
1448
+
1400
1449
  export class FingerprintEngine {
1401
1450
  constructor(securityConfig) {
1402
1451
  const isProduction = process.env.NODE_ENV === 'production';
@@ -1580,18 +1629,20 @@ export class FingerprintEngine {
1580
1629
  let isValid = false;
1581
1630
  const challengeContext = await store.get(`secret:${pow_nonce}`);
1582
1631
  let ticket = null;
1632
+ // Déclarer optimalTtl ici avec une valeur par défaut
1633
+ let optimalTtl = this.securityConfig.ticketMaxAge || 3600000;
1583
1634
 
1584
1635
  if (challengeContext) {
1585
- const optimalTtl = determineOptimalTicketTtl(preliminaryScore);
1636
+ optimalTtl = determineOptimalTicketTtl(preliminaryScore);
1586
1637
  this._log('Challenge context found, verifying solution', { optimalTtl });
1587
1638
 
1588
1639
  if (pow_type === "cpu_target") {
1589
1640
  // On passe la durée de vie du ticket configurée
1590
- ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution, null, challengeContext.clientSecret, challengeContext.cpuTarget);
1641
+ ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution, challengeContext.clientSecret, challengeContext.cpuTarget);
1591
1642
  isValid = ticket !== null;
1592
1643
  this._log('CPU target challenge verification', { isValid });
1593
1644
  } else if (pow_type === "cpu_mem") {
1594
- const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution_cpu, null, challengeContext.clientSecret, challengeContext.cpuTarget);
1645
+ const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget);
1595
1646
  const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
1596
1647
  isValid = cpuTicket !== null && isMemValid;
1597
1648
  if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
@@ -1605,18 +1656,40 @@ export class FingerprintEngine {
1605
1656
  this._log('Challenge context not found or expired', { pow_nonce });
1606
1657
  }
1607
1658
 
1659
+ console.log({isValid})
1608
1660
  if (isValid) {
1609
1661
  // La solution est valide. On supprime le secret et on redirige.
1610
1662
  await store.delete(`secret:${pow_nonce}`);
1611
- this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: this.securityConfig.ticketMaxAge || 3600000 });
1663
+ this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: optimalTtl });
1612
1664
 
1613
1665
  if (logger) {
1614
1666
  logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now() });
1615
1667
  }
1616
1668
 
1669
+ // NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
1670
+ // 1. On part du chemin original stocké, qui peut contenir des query params.
1671
+ const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
1672
+
1673
+ console.log({originalUrl})
1674
+ // 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
1675
+ const finalSearchParams = new URLSearchParams(requestContext.query);
1676
+
1677
+ // 3. On supprime uniquement les paramètres liés au challenge.
1678
+ finalSearchParams.delete('pow_type');
1679
+ finalSearchParams.delete('pow_nonce');
1680
+ finalSearchParams.delete('pow_solution');
1681
+ finalSearchParams.delete('pow_solution_cpu');
1682
+ finalSearchParams.delete('pow_solution_mem');
1683
+
1684
+ // 4. On reconstruit le chemin final.
1685
+ const finalQueryString = finalSearchParams.toString();
1686
+ const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
1687
+ this._log('Redirecting to clean path', { finalRedirectPath });
1688
+
1689
+ console.log({finalRedirectPath})
1617
1690
  return {
1618
1691
  action: 'redirect',
1619
- path: path,
1692
+ path: finalRedirectPath,
1620
1693
  score: 0, // Le score n'est pas pertinent ici, on a passé le test.
1621
1694
  vector: { challenge_solved: 100 },
1622
1695
  cookie: {
@@ -1625,7 +1698,7 @@ export class FingerprintEngine {
1625
1698
  options: {
1626
1699
  httpOnly: true,
1627
1700
  secure: this.isProduction, // Le maxAge est déjà inclus dans le ticket, mais on le met aussi sur le cookie
1628
- maxAge: this.securityConfig.ticketMaxAge || 3600000,
1701
+ maxAge: optimalTtl,
1629
1702
  }
1630
1703
  }
1631
1704
  };
@@ -1791,7 +1864,8 @@ export class FingerprintEngine {
1791
1864
  await store.set(`secret:${nonce}`, {
1792
1865
  clientSecret,
1793
1866
  cpuTarget: cpuChallengeDetails.target,
1794
- memDifficulty: memDifficulty
1867
+ memDifficulty: memDifficulty,
1868
+ originalPath: path, // *** FIX: Store the original path ***
1795
1869
  }, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
1796
1870
 
1797
1871
  // Associate the current challenge nonce with the device for trap URL verification later.
@@ -1811,7 +1885,7 @@ export class FingerprintEngine {
1811
1885
  }
1812
1886
 
1813
1887
  // Check if the request is an API request to return a JSON challenge
1814
- const isApi = this.securityConfig.thresholds?.isApiRequest?.(requestContext);
1888
+ const isApi = requestContext.rawReq && this.securityConfig.thresholds?.isApiRequest?.(requestContext.rawReq);
1815
1889
 
1816
1890
  if (isApi) {
1817
1891
  // For API clients, send a JSON response with challenge details.
@@ -1828,8 +1902,7 @@ export class FingerprintEngine {
1828
1902
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
1829
1903
  } else {
1830
1904
  // For browsers, send the HTML page.
1831
- const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`;
1832
- const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret).replace('</body>', `${trapContainer}</body>`);
1905
+ const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer);
1833
1906
  this._log('Browser challenge page generated', {
1834
1907
  pageLength: page.length,
1835
1908
  hasTrapContainer: true
@@ -2090,7 +2163,6 @@ export const powMiddleware = (securityConfig) => {
2090
2163
  */
2091
2164
  export const __internal = {
2092
2165
  getDeviceHash,
2093
- isMalicious,
2094
2166
  getSuspicionVector,
2095
2167
  cyrb53, // Export for testing
2096
2168
  FingerprintBuilder, // Export for testing
package/package.json CHANGED
@@ -1,76 +1,76 @@
1
- {
2
- "name": "@anonympins/fingerprint",
3
- "version": "0.1.1",
4
- "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
- "main": "fingerprint.js",
6
- "type": "module",
7
- "engines": {
8
- "node": ">=20.0.0"
9
- },
10
- "scripts": {
11
- "test": "vitest run --reporter=verbose"
12
- },
13
- "files": [
14
- "fingerprint.js",
15
- "fingerprint.client.js",
16
- "fingerprint.builder.js",
17
- "pow.solver.js",
18
- "library.js",
19
- "redis-store.js",
20
- "mongodb-store.js",
21
- "sql-store.js",
22
- "README.md",
23
- "LICENSE"
24
- ],
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/anonympins/fingerprint.git"
28
- },
29
- "keywords": [
30
- "fingerprint",
31
- "bot",
32
- "anti-bot",
33
- "security",
34
- "express",
35
- "middleware",
36
- "proof-of-work",
37
- "pow",
38
- "rate-limiting",
39
- "mitigation",
40
- "captcha",
41
- "dns"
42
- ],
43
- "author": "anonympins",
44
- "license": "MIT",
45
- "bugs": {
46
- "url": "https://github.com/anonympins/fingerprint/issues"
47
- },
48
- "homepage": "https://github.com/anonympins/fingerprint#readme",
49
- "devDependencies": {
50
- "body-parser": "^1.20.2",
51
- "cookie-parser": "^1.4.6",
52
- "express": "^4.18.2",
53
- "prom-client": "^15.1.2",
54
- "vitest": "^4.1.11"
55
- },
56
- "peerDependencies": {
57
- "ioredis": "^5.3.2",
58
- "knex": ">=3.0.0",
59
- "mongodb": "^6.3.0",
60
- "sqlite3": "^5.1.7"
61
- },
62
- "peerDependenciesMeta": {
63
- "ioredis": {
64
- "optional": true
65
- },
66
- "mongodb": {
67
- "optional": true
68
- },
69
- "knex": {
70
- "optional": true
71
- },
72
- "sqlite3": {
73
- "optional": true
74
- }
75
- }
76
- }
1
+ {
2
+ "name": "@anonympins/fingerprint",
3
+ "version": "0.1.3",
4
+ "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
+ "main": "fingerprint.js",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20.0.0"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run --reporter=verbose"
12
+ },
13
+ "files": [
14
+ "fingerprint.js",
15
+ "fingerprint.client.js",
16
+ "fingerprint.builder.js",
17
+ "pow.solver.js",
18
+ "library.js",
19
+ "redis-store.js",
20
+ "mongodb-store.js",
21
+ "sql-store.js",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/anonympins/fingerprint.git"
28
+ },
29
+ "keywords": [
30
+ "fingerprint",
31
+ "bot",
32
+ "anti-bot",
33
+ "security",
34
+ "express",
35
+ "middleware",
36
+ "proof-of-work",
37
+ "pow",
38
+ "rate-limiting",
39
+ "mitigation",
40
+ "captcha",
41
+ "dns"
42
+ ],
43
+ "author": "anonympins",
44
+ "license": "MIT",
45
+ "bugs": {
46
+ "url": "https://github.com/anonympins/fingerprint/issues"
47
+ },
48
+ "homepage": "https://github.com/anonympins/fingerprint#readme",
49
+ "devDependencies": {
50
+ "body-parser": "^1.20.2",
51
+ "cookie-parser": "^1.4.6",
52
+ "express": "^4.18.2",
53
+ "prom-client": "^15.1.2",
54
+ "vitest": "^4.1.11"
55
+ },
56
+ "peerDependencies": {
57
+ "ioredis": "^5.3.2",
58
+ "knex": ">=3.0.0",
59
+ "mongodb": "^6.3.0",
60
+ "sqlite3": "^5.1.7"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "ioredis": {
64
+ "optional": true
65
+ },
66
+ "mongodb": {
67
+ "optional": true
68
+ },
69
+ "knex": {
70
+ "optional": true
71
+ },
72
+ "sqlite3": {
73
+ "optional": true
74
+ }
75
+ }
76
+ }
package/pow.solver.js CHANGED
@@ -1,214 +1,224 @@
1
- /**
2
- * @file @/pow.solver.js
3
- * @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
4
- * Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
5
- */
6
-
7
- 'use strict';
8
-
9
- /**
10
- * Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
11
- * @param {string} clientIp - L'adresse IP du client.
12
- * @param {string} nonce - Le nonce du challenge.
13
- * @param {bigint} target - La cible à atteindre.
14
- * @param {string} clientSecret - Le secret client (optionnel).
15
- * @param {Function} progressCallback - Callback pour les mises à jour de progression.
16
- * @returns {Promise<number>} La solution (un nombre entier).
17
- */
18
- export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
19
- const cpuTarget = BigInt(target);
20
- let cpuSolution = 0;
21
- while (true) {
22
- const msg = clientSecret
23
- ? `${clientIp}:${nonce}:${cpuSolution}:${clientSecret}`
24
- : `${clientIp}:${nonce}:${cpuSolution}`;
25
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
26
- const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
27
- if (BigInt('0x' + hashHex) < cpuTarget) break;
28
- cpuSolution++;
29
- if (cpuSolution % 100000 === 0) {
30
- await new Promise(r => setTimeout(r, 0));
31
- if (progressCallback) progressCallback(cpuSolution);
32
- }
33
- }
34
- return cpuSolution;
35
- }
36
-
37
- /**
38
- * Résout un challenge CPU basé sur une cible (version Web Worker).
39
- * @param {string} message - Le message à hasher (ex: `ip:nonce:solution:secret`).
40
- * @param {bigint} target - La cible à atteindre.
41
- * @returns {Promise<number>} La solution (un nombre entier).
42
- */
43
- export async function solveCpuTarget(message, target) {
44
- // Vérifie si les Web Workers sont supportés par le navigateur.
45
- if (typeof(Worker) === "undefined") {
46
- console.warn("Web Workers not supported. Falling back to main thread calculation (UI may freeze).");
47
- // Ici, on pourrait remettre l'ancienne implémentation comme solution de secours.
48
- // Pour la clarté, nous supposons que les workers sont disponibles.
49
- throw new Error("Web Worker support is required for CPU challenges.");
50
- }
51
-
52
- return new Promise((resolve, reject) => {
53
- // Crée un worker à partir du script dédié. Le chemin doit être accessible publiquement.
54
- // Assurez-vous que `pow.worker.js` est servi par votre serveur statique.
55
- const worker = new Worker('./pow.worker.js');
56
-
57
- worker.onmessage = (event) => {
58
- resolve(event.data.solution);
59
- worker.terminate(); // Nettoie le worker une fois le travail terminé.
60
- };
61
-
62
- worker.onerror = (error) => {
63
- reject(error);
64
- worker.terminate();
65
- };
66
-
67
- // Envoie les données du challenge au worker pour qu'il commence le calcul.
68
- worker.postMessage({ message, target });
69
- });
70
- }
71
-
72
- /**
73
- * Résout un challenge basé sur la mémoire.
74
- * @param {string} seed - La graine pour l'initialisation de la mémoire.
75
- * @param {number} difficulty - La difficulté (en Mo).
76
- * @returns {Promise<number>} La solution (nombre entier).
77
- */
78
- export async function solveMemory(seed, difficulty) {
79
- const size = difficulty * 1024 * 1024;
80
- const buffer = new Uint32Array(size / 4);
81
- let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
82
- for (let i = 0; i < buffer.length; i++) {
83
- buffer[i] = (h = Math.imul(h ^ i, 1597334677));
84
- }
85
- let solution = 0;
86
- const iterations = size / 16;
87
- let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
88
- for (let i = 0; i < iterations; i++) {
89
- addr = buffer[addr] % buffer.length;
90
- solution ^= addr;
91
- }
92
- return solution;
93
- }
94
-
95
- /**
96
- * Résout un challenge de type "Problème du Voyageur de Commerce" (TSP).
97
- * NOTE: Ceci est une implémentation simple (heuristique du plus proche voisin) et n'est pas garantie
98
- * de trouver la solution optimale, mais elle est suffisante pour un challenge.
99
- * @param {Array<{x: number, y: number}>} cities - Les coordonnées des villes.
100
- * @param {number} targetMaxDistance - La distance maximale acceptable.
101
- * @returns {Promise<{path: number[], distance: number}>} Le chemin et la distance.
102
- */
103
- export async function solveTsp(cities, targetMaxDistance) {
104
- // Utility function to calculate the distance between two cities
105
- function distance(city1, city2) {
106
- return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
107
- }
108
-
109
- // Utility function to evaluate the total distance of a path
110
- function evaluatePathDistance(cities, path) {
111
- let totalDistance = 0;
112
- for (let i = 0; i < path.length - 1; i++) {
113
- totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
114
- }
115
- totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
116
- return totalDistance;
117
- }
118
-
119
- // Solveur simple du TSP (heuristique du plus proche voisin)
120
- function solveTspNearestNeighbor(cities) {
121
- const numCities = cities.length;
122
- if (numCities === 0) return [];
123
-
124
- let currentPath = [];
125
- let visited = new Array(numCities).fill(false);
126
-
127
- let currentCityIndex = 0; // Always start with the first city for reproducibility
128
- currentPath.push(currentCityIndex);
129
- visited[currentCityIndex] = true;
130
-
131
- for (let i = 1; i < numCities; i++) {
132
- let nearestCityIndex = -1;
133
- let minDistance = Infinity;
134
-
135
- for (let j = 0; j < numCities; j++) {
136
- if (!visited[j]) {
137
- const dist = distance(cities[currentCityIndex], cities[j]);
138
- if (dist < minDistance) {
139
- minDistance = dist;
140
- nearestCityIndex = j;
141
- }
142
- }
143
- }
144
- currentCityIndex = nearestCityIndex;
145
- currentPath.push(currentCityIndex);
146
- visited[currentCityIndex] = true;
147
- }
148
- return currentPath;
149
- }
150
-
151
- // To avoid freezing the browser, yield the thread from time to time
152
- await new Promise(resolve => setTimeout(resolve, 10));
153
- const solutionPath = solveTspNearestNeighbor(cities);
154
- const solutionDistance = evaluatePathDistance(cities, solutionPath);
155
-
156
- return { path: solutionPath, distance: solutionDistance };
157
- }
158
-
159
- /**
160
- * Fonction principale qui reçoit un objet challenge et le résout.
161
- * @param {object} challenge - L'objet challenge reçu du serveur.
162
- * @returns {Promise<object>} Un objet contenant la ou les solutions.
163
- */
164
- export async function solveChallenge(challenge) {
165
- const { type, nonce, clientSecret, target, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
166
- const solutions = {};
167
-
168
- switch (type) {
169
- case 'cpu_target':
170
- const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
171
- solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
172
- break;
173
- case 'cpu_mem':
174
- // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
175
- const baseMessageCombined = `${nonce}:${clientSecret}`;
176
- const memSeed = `:${nonce}:${clientSecret}`;
177
- const [cpuSol, memSol] = await Promise.all([
178
- // On utilise une version de solveCpuTarget qui n'a pas besoin de l'IP
179
- solveCpuTargetInline(null, nonce, target, clientSecret),
180
- solveMemory(memSeed, memDifficulty)
181
- ]);
182
- solutions.cpu = cpuSol;
183
- solutions.mem = memSol;
184
- break;
185
- case 'cpu_mem_inline':
186
- // Version inline pour compatibilité HTML avec IP incluse
187
- const memSeedInline = `${nonce}:${clientSecret}`;
188
- const [cpuSolInline, memSolInline] = await Promise.all([
189
- solveCpuTargetInline(clientIp, nonce, target, clientSecret),
190
- solveMemory(memSeedInline, memDifficulty)
191
- ]);
192
- solutions.cpu = cpuSolInline;
193
- solutions.mem = memSolInline;
194
- break;
195
- case 'tsp':
196
- const tspResult = await solveTsp(cities, targetMaxDistance);
197
- solutions.tsp = tspResult.path;
198
- solutions.distance = tspResult.distance;
199
- break;
200
- default:
201
- throw new Error(`Unknown challenge type: ${type}`);
202
- }
203
-
204
- return solutions;
205
- }
206
-
207
- // --- Compatibilité pour l'injection directe dans le HTML ---
208
- // Si le script est chargé dans un navigateur (window existe), on attache les fonctions nécessaires à window.
209
- if (typeof window !== 'undefined') {
210
- window.solveCpuChallengeInline = solveCpuTargetInline;
211
- window.solveMemoryChallenge = solveMemory;
212
- window.solveTspChallenge = solveTsp;
213
- window.solveChallenge = solveChallenge;
1
+ /**
2
+ * @file @/pow.solver.js
3
+ * @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
4
+ * Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
11
+ * @param {string} clientIp - L'adresse IP du client.
12
+ * @param {string} nonce - Le nonce du challenge.
13
+ * @param {bigint} target - La cible à atteindre.
14
+ * @param {string} clientSecret - Le secret client (optionnel).
15
+ * @param {Function} progressCallback - Callback pour les mises à jour de progression.
16
+ * @returns {Promise<number>} La solution (un nombre entier).
17
+ */
18
+ export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
19
+ const cpuTarget = BigInt('0x' + target);
20
+ let cpuSolution = 0;
21
+ const ipPart = clientIp || ''; // Use empty string if IP is null/undefined
22
+ while (true) { // When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
23
+ const msg = clientSecret ?
24
+ `${nonce}:${cpuSolution}:${clientSecret}` :
25
+ `${ipPart}:${nonce}:${cpuSolution}`;
26
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
27
+ const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
28
+ if (BigInt('0x' + hashHex) < cpuTarget) break;
29
+ cpuSolution++;
30
+ if (cpuSolution % 100000 === 0) {
31
+ await new Promise(r => setTimeout(r, 0));
32
+ if (progressCallback) progressCallback(cpuSolution);
33
+ }
34
+ }
35
+ return cpuSolution;
36
+ }
37
+
38
+ /**
39
+ * Résout un challenge CPU basé sur une cible (version Web Worker).
40
+ * @param {string} message - Le message à hasher (ex: `ip:nonce:solution:secret`).
41
+ * @param {bigint} target - La cible à atteindre.
42
+ * @returns {Promise<number>} La solution (un nombre entier).
43
+ */
44
+ export async function solveCpuTarget(message, target) {
45
+ // Vérifie si les Web Workers sont supportés par le navigateur.
46
+ if (typeof(Worker) === "undefined") {
47
+ console.warn("Web Workers not supported. Falling back to main thread calculation (UI may freeze).");
48
+ // Ici, on pourrait remettre l'ancienne implémentation comme solution de secours.
49
+ // Pour la clarté, nous supposons que les workers sont disponibles.
50
+ throw new Error("Web Worker support is required for CPU challenges.");
51
+ }
52
+
53
+ return new Promise((resolve, reject) => {
54
+ // Crée un worker à partir du script dédié. Le chemin doit être accessible publiquement.
55
+ // Assurez-vous que `pow.worker.js` est servi par votre serveur statique.
56
+ const worker = new Worker('./pow.worker.js');
57
+
58
+ worker.onmessage = (event) => {
59
+ resolve(event.data.solution);
60
+ worker.terminate(); // Nettoie le worker une fois le travail terminé.
61
+ };
62
+
63
+ worker.onerror = (error) => {
64
+ reject(error);
65
+ worker.terminate();
66
+ };
67
+
68
+ // Envoie les données du challenge au worker pour qu'il commence le calcul.
69
+ worker.postMessage({ message, target });
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Résout un challenge basé sur la mémoire.
75
+ * @param {string} seed - La graine pour l'initialisation de la mémoire.
76
+ * @param {number} difficulty - La difficulté (en Mo).
77
+ * @returns {Promise<number>} La solution (nombre entier).
78
+ */
79
+ export async function solveMemory(seed, difficulty) {
80
+ const size = difficulty * 1024 * 1024;
81
+ const buffer = new Uint32Array(size / 4);
82
+ let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
83
+ for (let i = 0; i < buffer.length; i++) {
84
+ buffer[i] = (h = Math.imul(h ^ i, 1597334677));
85
+ }
86
+ let solution = 0;
87
+ const iterations = size / 16;
88
+ let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
89
+ for (let i = 0; i < iterations; i++) {
90
+ addr = buffer[addr] % buffer.length;
91
+ solution ^= addr;
92
+ }
93
+ return solution;
94
+ }
95
+
96
+ /**
97
+ * Résout un challenge de type "Problème du Voyageur de Commerce" (TSP).
98
+ * NOTE: Ceci est une implémentation simple (heuristique du plus proche voisin) et n'est pas garantie
99
+ * de trouver la solution optimale, mais elle est suffisante pour un challenge.
100
+ * @param {Array<{x: number, y: number}>} cities - Les coordonnées des villes.
101
+ * @param {number} targetMaxDistance - La distance maximale acceptable.
102
+ * @returns {Promise<{path: number[], distance: number}>} Le chemin et la distance.
103
+ */
104
+ export async function solveTsp(cities, targetMaxDistance) {
105
+ // Utility function to calculate the distance between two cities
106
+ function distance(city1, city2) {
107
+ return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
108
+ }
109
+
110
+ // Utility function to evaluate the total distance of a path
111
+ function evaluatePathDistance(cities, path) {
112
+ let totalDistance = 0;
113
+ for (let i = 0; i < path.length - 1; i++) {
114
+ totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
115
+ }
116
+ totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
117
+ return totalDistance;
118
+ }
119
+
120
+ // Solveur simple du TSP (heuristique du plus proche voisin)
121
+ function solveTspNearestNeighbor(cities) {
122
+ const numCities = cities.length;
123
+ if (numCities === 0) return [];
124
+
125
+ let currentPath = [];
126
+ let visited = new Array(numCities).fill(false);
127
+
128
+ let currentCityIndex = 0; // Always start with the first city for reproducibility
129
+ currentPath.push(currentCityIndex);
130
+ visited[currentCityIndex] = true;
131
+
132
+ for (let i = 1; i < numCities; i++) {
133
+ let nearestCityIndex = -1;
134
+ let minDistance = Infinity;
135
+
136
+ for (let j = 0; j < numCities; j++) {
137
+ if (!visited[j]) {
138
+ const dist = distance(cities[currentCityIndex], cities[j]);
139
+ if (dist < minDistance) {
140
+ minDistance = dist;
141
+ nearestCityIndex = j;
142
+ }
143
+ }
144
+ }
145
+ currentCityIndex = nearestCityIndex;
146
+ currentPath.push(currentCityIndex);
147
+ visited[currentCityIndex] = true;
148
+ }
149
+ return currentPath;
150
+ }
151
+
152
+ // To avoid freezing the browser, yield the thread from time to time
153
+ await new Promise(resolve => setTimeout(resolve, 10));
154
+ const solutionPath = solveTspNearestNeighbor(cities);
155
+ const solutionDistance = evaluatePathDistance(cities, solutionPath);
156
+
157
+ return { path: solutionPath, distance: solutionDistance };
158
+ }
159
+
160
+ /**
161
+ * Fonction principale qui reçoit un objet challenge et le résout.
162
+ * @param {object} challenge - L'objet challenge reçu du serveur.
163
+ * @returns {Promise<object>} Un objet contenant la ou les solutions.
164
+ */
165
+ export async function solveChallenge(challenge) {
166
+ const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
167
+ const solutions = {};
168
+
169
+ switch (type) {
170
+ case 'cpu_target':
171
+ const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
172
+ if (!cpuTarget) {
173
+ throw new Error("Challenge data is missing 'cpuTarget' property.");
174
+ }
175
+ const target = cpuTarget; // Keep variable name for consistency below
176
+ solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
177
+ break;
178
+ case 'cpu_mem':
179
+ // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
180
+ const baseMessageCombined = `${nonce}:${clientSecret}`;
181
+ const memSeed = `:${nonce}:${clientSecret}`;
182
+ const [cpuSol, memSol] = await Promise.all([
183
+ (async () => {
184
+ if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
185
+ return solveCpuTargetInline(null, nonce, cpuTarget, clientSecret);
186
+ })(),
187
+ solveMemory(memSeed, memDifficulty)
188
+ ]);
189
+ solutions.cpu = cpuSol;
190
+ solutions.mem = memSol;
191
+ break;
192
+ case 'cpu_mem_inline':
193
+ // Version inline pour compatibilité HTML avec IP incluse
194
+ const memSeedInline = `${nonce}:${clientSecret}`;
195
+ const [cpuSolInline, memSolInline] = await Promise.all([
196
+ (async () => {
197
+ if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
198
+ return solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
199
+ })(),
200
+ solveMemory(memSeedInline, memDifficulty)
201
+ ]);
202
+ solutions.cpu = cpuSolInline;
203
+ solutions.mem = memSolInline;
204
+ break;
205
+ case 'tsp':
206
+ const tspResult = await solveTsp(cities, targetMaxDistance);
207
+ solutions.tsp = tspResult.path;
208
+ solutions.distance = tspResult.distance;
209
+ break;
210
+ default:
211
+ throw new Error(`Unknown challenge type: ${type}`);
212
+ }
213
+
214
+ return solutions;
215
+ }
216
+
217
+ // --- Compatibilité pour l'injection directe dans le HTML ---
218
+ // Si le script est chargé dans un navigateur (window existe), on attache les fonctions nécessaires à window.
219
+ if (typeof window !== 'undefined') {
220
+ window.solveCpuChallengeInline = solveCpuTargetInline;
221
+ window.solveMemoryChallenge = solveMemory;
222
+ window.solveTspChallenge = solveTsp;
223
+ window.solveChallenge = solveChallenge;
214
224
  }