@anonympins/fingerprint 0.2.1 → 0.2.2

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/library.js CHANGED
@@ -1583,29 +1583,104 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
1583
1583
  * @returns {Array<{solution: object, objectives: number[]}>} Le front de Pareto des configurations optimales.
1584
1584
  */
1585
1585
  Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
1586
- const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context); // eslint-disable-line
1586
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
1587
1587
 
1588
1588
  // Un "individu" est un objet de configuration complet
1589
1589
  const createIndividual = () => ({
1590
1590
  thresholds: {
1591
- low: 10 + Math.random() * 30, // 10-40
1592
- medium: 40 + Math.random() * 30, // 40-70
1593
- high: 70 + Math.random() * 25, // 70-95
1591
+ low: 15 + Math.random() * 20, // 15-35
1592
+ medium: 40 + Math.random() * 25, // 40-65
1593
+ high: 70 + Math.random() * 20, // 70-90
1594
1594
  },
1595
1595
  weights: {
1596
1596
  historyScore: Math.random(),
1597
1597
  rotationScore: Math.random(),
1598
1598
  headerAnomalyScore: Math.random(),
1599
- requestPatternScore: Math.random(),
1599
+ requestPatternScore: 0.5 + Math.random(), // Donner plus d'importance aux patterns
1600
1600
  inconsistencyScore: Math.random(),
1601
1601
  honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1602
+ behaviorScore: Math.random(),
1603
+ crossLayerInconsistencyScore: Math.random(),
1604
+ timeInconsistencyScore: Math.random(),
1602
1605
  },
1603
- // On pourrait aussi faire muter les `patterns` ici
1606
+ patterns: {
1607
+ velocityThreshold: 100 + Math.random() * 400, // 100-500ms
1608
+ velocityWeight: 10 + Math.random() * 40,
1609
+ burstThreshold: 300 + Math.random() * 700, // 300-1000ms
1610
+ burstWeight: 20 + Math.random() * 40,
1611
+ scrapeThreshold: 500 + Math.random() * 1000, // 500-1500ms
1612
+ scrapeWeight: 15 + Math.random() * 35,
1613
+ sequenceLength: 3 + Math.floor(Math.random() * 3), // 3-5
1614
+ sequenceWeight: 20 + Math.random() * 50,
1615
+ regularityThreshold: 50 + Math.random() * 200, // 50-250ms
1616
+ regularityWeight: 20 + Math.random() * 40,
1617
+ decayFactor: 0.85 + Math.random() * 0.14, // 0.85-0.99
1618
+ inactivityReset: 15000 + Math.random() * 45000, // 15s-60s
1619
+ }
1604
1620
  });
1605
1621
 
1606
1622
  // Le crossover et la mutation doivent maintenant opérer sur des objets complexes
1607
- const crossover = (c1, c2) => { /* ... logique de croisement pour les objets de config ... */ return c1; }; // eslint-disable-line
1608
- const mutate = (c) => { /* ... logique de mutation pour les objets de config ... */ return c; }; // eslint-disable-line
1623
+ const crossover = (c1, c2) => {
1624
+ const child = JSON.parse(JSON.stringify(c1)); // Deep copy
1625
+ // Croisement pour chaque groupe de paramètres
1626
+ for (const key in child.thresholds) {
1627
+ child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
1628
+ }
1629
+ for (const key in child.weights) {
1630
+ if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
1631
+ child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
1632
+ }
1633
+ }
1634
+ for (const key in child.patterns) {
1635
+ child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
1636
+ }
1637
+ return child;
1638
+ };
1639
+
1640
+ const mutate = (c) => {
1641
+ const newConfig = JSON.parse(JSON.stringify(c));
1642
+
1643
+ // --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
1644
+ // On donne plus de poids à la mutation des 'patterns' et des 'weights',
1645
+ // car ils ont un impact plus direct sur la détection que les seuils.
1646
+ const sections = [
1647
+ { name: 'patterns', weight: 0.5 }, // 50% de chance
1648
+ { name: 'weights', weight: 0.35 }, // 35% de chance
1649
+ { name: 'thresholds', weight: 0.15 } // 15% de chance
1650
+ ];
1651
+ const rand = Math.random();
1652
+ let cumulativeWeight = 0;
1653
+ let sectionToMutate = 'patterns'; // Fallback
1654
+ for (const section of sections) {
1655
+ cumulativeWeight += section.weight;
1656
+ if (rand < cumulativeWeight) {
1657
+ sectionToMutate = section.name;
1658
+ break;
1659
+ }
1660
+ }
1661
+
1662
+ const keys = Object.keys(newConfig[sectionToMutate]);
1663
+ const keyToMutate = keys[Math.floor(Math.random() * keys.length)];
1664
+
1665
+ if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
1666
+
1667
+ // Appliquer une mutation avec une amplitude variable
1668
+ const mutationAmount = (Math.random() - 0.5) * 0.4; // +/- 20%
1669
+ newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
1670
+
1671
+ // S'assurer que les valeurs restent dans des limites raisonnables
1672
+ if (sectionToMutate === 'weights') {
1673
+ newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
1674
+ }
1675
+ if (keyToMutate === 'decayFactor') {
1676
+ newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
1677
+ }
1678
+ if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
1679
+ newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
1680
+ }
1681
+
1682
+ return newConfig;
1683
+ };
1609
1684
 
1610
1685
  return Optimization.geneticAlgorithmMultiObjective(
1611
1686
  createIndividual,
package/mongodb-store.js CHANGED
@@ -1,53 +1,53 @@
1
- /**
2
- * @file Creates a store adapter for MongoDB.
3
- * This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
4
- * for automatic expiration of documents.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for a MongoDB collection.
9
- * It's recommended to pass the `db` object and let the adapter handle the collection.
10
- *
11
- * **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
12
- * In the mongo shell, run:
13
- * `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
14
- *
15
- * @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
16
- * @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
17
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
18
- */
19
- export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
20
- const collection = db.collection(collectionName);
21
-
22
- return {
23
- async get(key) {
24
- const doc = await collection.findOne({ _id: key });
25
- // The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
26
- return doc ? doc.value : null;
27
- },
28
- async set(key, value, ttl) {
29
- const doc = {
30
- _id: key,
31
- value: value,
32
- };
33
-
34
- if (ttl && ttl > 0) {
35
- // Set the expiration date for the TTL index.
36
- doc.expiresAt = new Date(Date.now() + ttl * 1000);
37
- }
38
-
39
- await collection.updateOne(
40
- { _id: key },
41
- { $set: doc },
42
- { upsert: true }
43
- );
44
- },
45
- async has(key) {
46
- const count = await collection.countDocuments({ _id: key });
47
- return count > 0;
48
- },
49
- async delete(key) {
50
- await collection.deleteOne({ _id: key });
51
- },
52
- };
1
+ /**
2
+ * @file Creates a store adapter for MongoDB.
3
+ * This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
4
+ * for automatic expiration of documents.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for a MongoDB collection.
9
+ * It's recommended to pass the `db` object and let the adapter handle the collection.
10
+ *
11
+ * **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
12
+ * In the mongo shell, run:
13
+ * `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
14
+ *
15
+ * @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
16
+ * @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
17
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
18
+ */
19
+ export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
20
+ const collection = db.collection(collectionName);
21
+
22
+ return {
23
+ async get(key) {
24
+ const doc = await collection.findOne({ _id: key });
25
+ // The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
26
+ return doc ? doc.value : null;
27
+ },
28
+ async set(key, value, ttl) {
29
+ const doc = {
30
+ _id: key,
31
+ value: value,
32
+ };
33
+
34
+ if (ttl && ttl > 0) {
35
+ // Set the expiration date for the TTL index.
36
+ doc.expiresAt = new Date(Date.now() + ttl * 1000);
37
+ }
38
+
39
+ await collection.updateOne(
40
+ { _id: key },
41
+ { $set: doc },
42
+ { upsert: true }
43
+ );
44
+ },
45
+ async has(key) {
46
+ const count = await collection.countDocuments({ _id: key });
47
+ return count > 0;
48
+ },
49
+ async delete(key) {
50
+ await collection.deleteOne({ _id: key });
51
+ },
52
+ };
53
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anonympins/fingerprint",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
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
5
  "main": "fingerprint.js",
6
6
  "type": "module",
package/pow.solver.js CHANGED
@@ -339,14 +339,50 @@ export async function solveOptimizationTask(initialPopulation, generations) {
339
339
  return population;
340
340
  }
341
341
 
342
+ /**
343
+ * @class ChallengeSolution
344
+ * @description Encapsule une solution de challenge et fournit des méthodes pour la manipuler.
345
+ * @private
346
+ */
347
+ class ChallengeSolution {
348
+ constructor(type, nonce, rawSolution) {
349
+ this.type = type;
350
+ this.nonce = nonce;
351
+ this.rawSolution = rawSolution;
352
+ }
353
+
354
+ /**
355
+ * Applique les paramètres de la solution à un objet URL.
356
+ * @param {URL} url - L'objet URL à modifier.
357
+ */
358
+ applyToUrl(url) {
359
+ url.searchParams.set('pow_type', this.type);
360
+ url.searchParams.set('pow_nonce', this.nonce);
361
+
362
+ // Logique de formatage spécifique à chaque type de challenge
363
+ if (this.type === 'cpu_mem' || this.type === 'cpu_mem_inline' || this.type === 'cpu_target') {
364
+ Object.entries(this.rawSolution).forEach(([key, value]) => {
365
+ url.searchParams.set(`pow_solution_${key}`, String(value));
366
+ });
367
+ } else if (this.type === 'useful_work_task') {
368
+ url.searchParams.set('pow_solution_work_result', JSON.stringify(this.rawSolution.work_result));
369
+ url.searchParams.set('pow_problem_id', this.rawSolution.problem_id);
370
+ } else {
371
+ // Pour les cas simples comme 'tsp' où la solution est une seule valeur
372
+ url.searchParams.set('pow_solution', JSON.stringify(this.rawSolution));
373
+ }
374
+ }
375
+ }
376
+
342
377
  /**
343
378
  * Fonction principale qui reçoit un objet challenge et le résout.
344
379
  * @param {object} challenge - L'objet challenge reçu du serveur.
345
- * @returns {Promise<object>} Un objet contenant la ou les solutions.
380
+ * @param {string} [fingerprint=''] - L'empreinte de l'appareil qui résout le challenge.
381
+ * @returns {Promise<ChallengeSolution>} Un objet `ChallengeSolution` encapsulant le résultat.
346
382
  */
347
383
  export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
348
384
  const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
349
- const solutions = {};
385
+ let rawSolution = {};
350
386
 
351
387
  switch (type) {
352
388
  case 'cpu_target':
@@ -357,7 +393,7 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
357
393
  const target = cpuTarget; // Keep variable name for consistency below
358
394
  // Pour ce challenge simple, le baseBlock est juste le nonce.
359
395
  const baseBlockBytes = new TextEncoder().encode(nonce + ":");
360
- solutions.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
396
+ rawSolution.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
361
397
  break;
362
398
  case 'cpu_mem':
363
399
  // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
@@ -371,8 +407,8 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
371
407
  })(),
372
408
  solveMemory(memSeed, memDifficulty)
373
409
  ]);
374
- solutions.cpu = cpuSol;
375
- solutions.mem = memSol;
410
+ rawSolution.cpu = cpuSol;
411
+ rawSolution.mem = memSol;
376
412
  break;
377
413
  case 'cpu_mem_inline':
378
414
  // Version inline pour compatibilité HTML avec IP incluse
@@ -385,28 +421,28 @@ export async function solveChallenge(challenge, fingerprint = '') { // The finge
385
421
  })(),
386
422
  solveMemory(memSeedInline, memDifficulty)
387
423
  ]);
388
- solutions.cpu = cpuSolInline;
389
- solutions.mem = memSolInline;
424
+ rawSolution.cpu = cpuSolInline;
425
+ rawSolution.mem = memSolInline;
390
426
  break;
391
427
  case 'tsp':
392
428
  const tspResult = await solveTsp(cities, targetMaxDistance);
393
- solutions.tsp = tspResult.path;
394
- solutions.distance = tspResult.distance;
429
+ // Pour ce challenge, la solution est juste le chemin.
430
+ rawSolution = tspResult.path;
395
431
  break;
396
432
  case 'optimization_task':
397
433
  const finalPopulation = await solveOptimizationTask(optimizationTask.population, optimizationTask.generations);
398
- solutions.population = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
434
+ rawSolution = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
399
435
  break;
400
436
  case 'useful_work_task':
401
437
  const workResult = await solveUsefulWorkTask(usefulWorkTask.task);
402
- solutions.work_result = workResult;
403
- solutions.problem_id = usefulWorkTask.problemId;
438
+ rawSolution.work_result = workResult;
439
+ rawSolution.problem_id = usefulWorkTask.problemId;
404
440
  break;
405
441
  default:
406
442
  throw new Error(`Unknown challenge type: ${type}`);
407
443
  }
408
444
 
409
- return solutions;
445
+ return new ChallengeSolution(type, nonce, rawSolution);
410
446
  }
411
447
 
412
448
  // --- Compatibilité pour l'injection directe dans le HTML ---
package/pow.worker.js CHANGED
@@ -1,27 +1,27 @@
1
- /**
2
- * @file @/pow.worker.js
3
- * @description Web Worker dédié à la résolution du challenge CPU.
4
- * Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
5
- */
6
-
7
- self.onmessage = async (event) => {
8
- const { message, target } = event.data;
9
- let solution = 0;
10
- const encoder = new TextEncoder();
11
-
12
- // La boucle de calcul intensive est isolée dans ce worker.
13
- while (true) {
14
- const currentMessage = `${message}:${solution}`;
15
- const data = encoder.encode(currentMessage);
16
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
17
- const hashArray = Array.from(new Uint8Array(hashBuffer));
18
- const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
19
-
20
- if (BigInt('0x' + hashHex) < target) {
21
- // Une fois la solution trouvée, on la renvoie au thread principal.
22
- self.postMessage({ solution });
23
- return;
24
- }
25
- solution++;
26
- }
1
+ /**
2
+ * @file @/pow.worker.js
3
+ * @description Web Worker dédié à la résolution du challenge CPU.
4
+ * Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
5
+ */
6
+
7
+ self.onmessage = async (event) => {
8
+ const { message, target } = event.data;
9
+ let solution = 0;
10
+ const encoder = new TextEncoder();
11
+
12
+ // La boucle de calcul intensive est isolée dans ce worker.
13
+ while (true) {
14
+ const currentMessage = `${message}:${solution}`;
15
+ const data = encoder.encode(currentMessage);
16
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
17
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
18
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
19
+
20
+ if (BigInt('0x' + hashHex) < target) {
21
+ // Une fois la solution trouvée, on la renvoie au thread principal.
22
+ self.postMessage({ solution });
23
+ return;
24
+ }
25
+ solution++;
26
+ }
27
27
  };
@@ -169,6 +169,78 @@ class ProblemManager {
169
169
  }
170
170
  this.saveProblems();
171
171
  }
172
+
173
+ /**
174
+ * S'assure qu'un problème a une solution initiale. Si non, en génère une.
175
+ * @param {object} problem - L'objet problème.
176
+ * @private
177
+ */
178
+ _ensureInitialSolution(problem) {
179
+ if (problem.state.bestSolution) {
180
+ return; // Une solution existe déjà
181
+ }
182
+
183
+ console.log(`[ProblemManager] Génération d'une solution initiale pour le problème ${problem.id}...`);
184
+
185
+ // On se base sur le type de problème pour générer une solution de base.
186
+ // Pour l'instant, on gère le cas le plus commun (TSP/points)
187
+ // qui utilise le recuit simulé.
188
+ switch (problem.workUnit.type) {
189
+ case 'simulated_annealing_iterations': {
190
+ // Pour un TSP, une solution initiale est un ordre des points.
191
+ // On prend l'ordre initial des points du payload.
192
+ const initialSolution = problem.payload.points;
193
+ if (initialSolution && Array.isArray(initialSolution)) {
194
+ // On calcule l'énergie (coût) de cette solution initiale.
195
+ const energy = Optimization.tsp.calculateEnergy(initialSolution);
196
+ problem.state.bestSolution = initialSolution;
197
+ problem.state.bestEnergy = energy;
198
+ problem.state.lastUpdate = new Date().toISOString();
199
+ console.log(`[ProblemManager] Solution initiale pour ${problem.id} générée avec une énergie de ${energy.toFixed(2)}.`);
200
+ this.saveProblems(); // On sauvegarde la nouvelle solution
201
+ }
202
+ break;
203
+ }
204
+ // D'autres types de problèmes (ex: algo génétique) pourraient être ajoutés ici.
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Récupère la meilleure solution actuellement connue pour un ou plusieurs problèmes.
210
+ * @param {string} [problemId] - L'ID optionnel du problème à consulter.
211
+ * Si non fourni, retourne les meilleures solutions pour tous les problèmes.
212
+ * @returns {object|Array<object>|null}
213
+ * - Si un `problemId` est fourni, retourne un objet `{ id, solution, score }` ou `null` si non trouvé.
214
+ * - Si aucun `problemId` n'est fourni, retourne un tableau de ces objets.
215
+ */
216
+ getBestSolutions(problemId) {
217
+ const problemsToProcess = problemId
218
+ ? this.problems.filter(p => p.id === problemId)
219
+ : this.problems;
220
+
221
+ problemsToProcess.forEach(p => this._ensureInitialSolution(p));
222
+
223
+ const formatSolution = (p) => {
224
+ // Après _ensureInitialSolution, on peut supposer que p.state existe.
225
+ if (!p || !p.state) return null;
226
+ return {
227
+ id: p.id,
228
+ solution: p.state.bestSolution,
229
+ // Gère les deux types de scores : 'energy' (recuit simulé) et 'fitness' (algo génétique)
230
+ score: p.state.bestEnergy !== undefined ? p.state.bestEnergy : p.state.bestFitness,
231
+ lastUpdate: p.state.lastUpdate,
232
+ };
233
+ };
234
+
235
+ if (problemId) {
236
+ const problem = this.problems.find(p => p.id === problemId);
237
+ return problem ? formatSolution(problem) : null; // Le filtrage initial a déjà fait le travail
238
+ }
239
+
240
+ // Retourne un aperçu pour tous les problèmes
241
+ return this.problems.map(formatSolution).filter(s => s && s.solution);
242
+ }
243
+
172
244
  }
173
245
 
174
246
  export { ProblemManager }; // Export the class for testing
package/redis-store.js CHANGED
@@ -1,43 +1,43 @@
1
- /**
2
- * @file Creates a store adapter for ioredis.
3
- * This adapter handles the serialization and deserialization of complex objects,
4
- * including the conversion of Set objects to arrays for storage in Redis.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for an ioredis client.
9
- * @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
10
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
11
- */
12
- export function createRedisStore(redisClient) {
13
- return {
14
- async get(key) {
15
- const value = await redisClient.get(key);
16
- if (!value) return null;
17
- // Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
18
- return JSON.parse(value, (k, v) => {
19
- if (k === 'ips' && Array.isArray(v)) {
20
- return new Set(v);
21
- }
22
- return v;
23
- });
24
- },
25
- async set(key, value, ttl) {
26
- // Use a replacer to convert Set objects into arrays before serialization.
27
- const stringValue = JSON.stringify(value, (k, v) => {
28
- if (v instanceof Set) {
29
- return Array.from(v);
30
- }
31
- return v;
32
- });
33
-
34
- if (ttl && ttl > 0) {
35
- await redisClient.set(key, stringValue, 'EX', ttl);
36
- } else {
37
- await redisClient.set(key, stringValue);
38
- }
39
- },
40
- async has(key) { return (await redisClient.exists(key)) === 1; },
41
- async delete(key) { await redisClient.del(key); },
42
- };
1
+ /**
2
+ * @file Creates a store adapter for ioredis.
3
+ * This adapter handles the serialization and deserialization of complex objects,
4
+ * including the conversion of Set objects to arrays for storage in Redis.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for an ioredis client.
9
+ * @param {import('ioredis').Redis} redisClient - An instance of the ioredis client.
10
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
11
+ */
12
+ export function createRedisStore(redisClient) {
13
+ return {
14
+ async get(key) {
15
+ const value = await redisClient.get(key);
16
+ if (!value) return null;
17
+ // Use a reviver to convert arrays back to Sets for specific keys like 'ips'.
18
+ return JSON.parse(value, (k, v) => {
19
+ if (k === 'ips' && Array.isArray(v)) {
20
+ return new Set(v);
21
+ }
22
+ return v;
23
+ });
24
+ },
25
+ async set(key, value, ttl) {
26
+ // Use a replacer to convert Set objects into arrays before serialization.
27
+ const stringValue = JSON.stringify(value, (k, v) => {
28
+ if (v instanceof Set) {
29
+ return Array.from(v);
30
+ }
31
+ return v;
32
+ });
33
+
34
+ if (ttl && ttl > 0) {
35
+ await redisClient.set(key, stringValue, 'EX', ttl);
36
+ } else {
37
+ await redisClient.set(key, stringValue);
38
+ }
39
+ },
40
+ async has(key) { return (await redisClient.exists(key)) === 1; },
41
+ async delete(key) { await redisClient.del(key); },
42
+ };
43
43
  }