@anonympins/fingerprint 0.2.0 → 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/README.md +60 -4
- package/fingerprint.builder.js +160 -103
- package/fingerprint.client.js +11 -22
- package/fingerprint.js +324 -355
- package/library.js +83 -8
- package/mongodb-store.js +52 -52
- package/optimization.worker.js +28 -0
- package/package.json +4 -1
- package/pow.solver.js +89 -33
- package/pow.worker.js +27 -0
- package/problem-manager.js +247 -0
- package/redis-store.js +42 -42
- package/sql-store.js +77 -77
package/README.md
CHANGED
|
@@ -63,6 +63,8 @@ export POW_SECRET="your_secret_key_of_at_least_32_characters"
|
|
|
63
63
|
|
|
64
64
|
The `powMiddleware` requires a configuration object defining the weights of suspicion indicators and the challenge trigger thresholds.
|
|
65
65
|
|
|
66
|
+
All following `securityConfig` parameters are optional.
|
|
67
|
+
|
|
66
68
|
```javascript
|
|
67
69
|
import express from 'express';
|
|
68
70
|
import bodyParser from 'body-parser';
|
|
@@ -98,6 +100,10 @@ const securityConfig = {
|
|
|
98
100
|
high: 75, // Score for a very difficult challenge
|
|
99
101
|
block: 95, // Score above which the request is blocked outright (HTTP 404)
|
|
100
102
|
},
|
|
103
|
+
cpu: {
|
|
104
|
+
minDifficultyBits: 8,
|
|
105
|
+
maxDifficultyBits: 24,
|
|
106
|
+
},
|
|
101
107
|
// (Optional) Configure the duration (in milliseconds) for various temporary data.
|
|
102
108
|
ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
|
|
103
109
|
challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
|
|
@@ -106,11 +112,15 @@ const securityConfig = {
|
|
|
106
112
|
verbose: process.env.NODE_ENV !== 'production', // Log detailed info in development, but not in production.
|
|
107
113
|
patterns: { // (Optional) Initial values for request pattern detection, optimized by auto-tuner if enabled.
|
|
108
114
|
velocityThreshold: 800, // ms between requests to be considered "fast"
|
|
109
|
-
burstThreshold: 1500,
|
|
115
|
+
burstThreshold: 1500, // ms for identical requests to be a "burst"
|
|
110
116
|
scrapeThreshold: 1000, // ms for sequential requests to be "scraping"
|
|
111
117
|
historySize: 10, // Number of requests to keep for pattern analysis
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
minSamples: 5, // Minimum number of timings to collect before statistical analysis.
|
|
119
|
+
regularityThreshold: 50, // Standard deviation (ms) below which behavior is "too regular".
|
|
120
|
+
benfordThreshold: 0.15, // Benford's Law deviation threshold above which the distribution is "unnatural".
|
|
121
|
+
patternWeight: 80, // Strong, one-time penalty when a pattern is detected.
|
|
122
|
+
decayFactor: 0.9, // Factor by which the pattern score decreases over time.
|
|
123
|
+
inactivityReset: 5000, // Time (ms) after which the pattern score is reset.
|
|
114
124
|
},
|
|
115
125
|
honeypot: {
|
|
116
126
|
// List of field names that are traps for bots.
|
|
@@ -152,6 +162,9 @@ const securityConfig = {
|
|
|
152
162
|
'203.0.113.0/24', // A partner's network range
|
|
153
163
|
'2001:db8::/32' // An IPv6 range
|
|
154
164
|
]},
|
|
165
|
+
{ type: 'hostname_allowlist', entries: [
|
|
166
|
+
'google.com', // A specific hostname
|
|
167
|
+
]},
|
|
155
168
|
// Option 2: DNS-verified bots (e.g., search engine crawlers).
|
|
156
169
|
// This uses a secure DNS lookup (reverse then forward) to verify the bot's identity.
|
|
157
170
|
// The result is cached per IP to avoid repeated DNS lookups.
|
|
@@ -169,7 +182,8 @@ const securityConfig = {
|
|
|
169
182
|
autotuning: {
|
|
170
183
|
trafficData: trafficData, // The data source for the genetic algorithm.
|
|
171
184
|
interval: 1800000, // Optimization cycle every 30 minutes (in ms).
|
|
172
|
-
minDataPoints: 200 // Minimum requests before starting an optimization cycle.
|
|
185
|
+
minDataPoints: 200, // Minimum requests before starting an optimization cycle.
|
|
186
|
+
maxDataPoints: 20000 // Minimum requests before starting an optimization cycle.
|
|
173
187
|
},
|
|
174
188
|
// Enables problem solving for suspicious activity (configurable in problems.config.json)
|
|
175
189
|
enableUsefulWork: true
|
|
@@ -536,6 +550,48 @@ While `powMiddleware` is convenient for Express, you can use the `FingerprintEng
|
|
|
536
550
|
|
|
537
551
|
The engine is a named export from the main module.
|
|
538
552
|
|
|
553
|
+
### Useful Proof-of-Work (`ProblemManager`)
|
|
554
|
+
|
|
555
|
+
Instead of issuing a generic Proof-of-Work, the system can dispatch a "useful" computational problem to a suspicious client. This allows harnessing the client's CPU cycles to solve complex problems (like optimization tasks) over time. This feature is managed by the `ProblemManager` class, which is enabled via the `enableUsefulWork: true` flag in the security configuration.
|
|
556
|
+
|
|
557
|
+
The `ProblemManager` reads its configuration from `problems.config.json`, which defines the problems to be solved, the type of work units, and the current state of the solutions.
|
|
558
|
+
|
|
559
|
+
While you typically won't interact with it directly, its methods are exported and can be used for monitoring or manual administration. The main instance is exported as `problemManager`.
|
|
560
|
+
|
|
561
|
+
#### `problemManager.dispatchWork(suspicionFactor)`
|
|
562
|
+
|
|
563
|
+
Selects a problem and generates a work unit for a client. The difficulty of the task (e.g., number of iterations) is scaled based on the client's `suspicionFactor`.
|
|
564
|
+
|
|
565
|
+
* **`suspicionFactor`** (`number`): A factor to adjust the difficulty of the work unit.
|
|
566
|
+
* **Returns**: (`object|null`) An object containing the `problemId` and the `task` to be sent to the client, or `null` if no problems are available.
|
|
567
|
+
|
|
568
|
+
#### `problemManager.integrateSolution(problemId, solutionData)`
|
|
569
|
+
|
|
570
|
+
Integrates a solution returned by a client into the problem's state. If the new solution is better than the existing one, it is saved as the new best solution.
|
|
571
|
+
|
|
572
|
+
* **`problemId`** (`string`): The ID of the problem being updated.
|
|
573
|
+
* **`solutionData`** (`object`): The solution data returned by the client (e.g., `{ solution, energy }`).
|
|
574
|
+
|
|
575
|
+
#### `problemManager.getBestSolutions([problemId])`
|
|
576
|
+
|
|
577
|
+
Retrieves the best solution currently known for one or all problems. This is useful for creating an API endpoint to view the progress of the distributed computation.
|
|
578
|
+
|
|
579
|
+
* **`problemId`** (`string`, optional): The ID of a specific problem.
|
|
580
|
+
* **Returns**: (`object|Array<object>|null`)
|
|
581
|
+
* If a `problemId` is provided, it returns an object with the best solution for that problem (`{ id, solution, score, lastUpdate }`).
|
|
582
|
+
* If no `problemId` is provided, it returns an array of these objects for all problems.
|
|
583
|
+
|
|
584
|
+
**Example: Creating an API endpoint to view solutions**
|
|
585
|
+
|
|
586
|
+
```javascript
|
|
587
|
+
import { problemManager } from './fingerprint.js'; // Adjust path
|
|
588
|
+
|
|
589
|
+
app.get('/api/problems/solutions', (req, res) => {
|
|
590
|
+
const solutions = problemManager.getBestSolutions();
|
|
591
|
+
res.json(solutions);
|
|
592
|
+
});
|
|
593
|
+
```
|
|
594
|
+
|
|
539
595
|
**Workflow:**
|
|
540
596
|
|
|
541
597
|
1. **Instantiate the Engine**: Create an instance with your `securityConfig`.
|
package/fingerprint.builder.js
CHANGED
|
@@ -1,104 +1,161 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
3
|
-
*/
|
|
4
|
-
export const cyrb53 = (str, seed = 0) => {
|
|
5
|
-
let h1 = 0xdeadbeef ^ seed,
|
|
6
|
-
h2 = 0x41c6ce57 ^ seed;
|
|
7
|
-
for (let i = 0, ch; i < str.length; i++) {
|
|
8
|
-
ch = str.charCodeAt(i);
|
|
9
|
-
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
10
|
-
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
11
|
-
}
|
|
12
|
-
h1 =
|
|
13
|
-
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
|
14
|
-
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
15
|
-
h2 =
|
|
16
|
-
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
17
|
-
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
18
|
-
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
23
|
-
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
24
|
-
*/
|
|
25
|
-
export class FingerprintBuilder {
|
|
26
|
-
constructor() {
|
|
27
|
-
this.components = new Map();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Ajoute un composant au hash global.
|
|
32
|
-
* @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
|
|
33
|
-
* @param {string|number|boolean} value - La valeur brute à hasher
|
|
34
|
-
*/
|
|
35
|
-
add(group, value) {
|
|
36
|
-
if (value === undefined || value === null) return this;
|
|
37
|
-
// On hash la valeur individuellement pour l'anonymiser et réduire sa taille
|
|
38
|
-
this.components.set(group, cyrb53(String(value)));
|
|
39
|
-
return this;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Adds a raw component without hashing it.
|
|
44
|
-
* Useful for metrics that need to be read on the server.
|
|
45
|
-
* @param {string} group - The name of the group.
|
|
46
|
-
* @param {string|number} value - The raw value.
|
|
47
|
-
*/
|
|
48
|
-
addRaw(group, value) {
|
|
49
|
-
if (value === undefined || value === null) return this;
|
|
50
|
-
this.components.set(group, value);
|
|
51
|
-
return this;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
3
|
+
*/
|
|
4
|
+
export const cyrb53 = (str, seed = 0) => {
|
|
5
|
+
let h1 = 0xdeadbeef ^ seed,
|
|
6
|
+
h2 = 0x41c6ce57 ^ seed;
|
|
7
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
8
|
+
ch = str.charCodeAt(i);
|
|
9
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
10
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
11
|
+
}
|
|
12
|
+
h1 =
|
|
13
|
+
Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
|
|
14
|
+
Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
15
|
+
h2 =
|
|
16
|
+
Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
|
|
17
|
+
Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
18
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
23
|
+
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
24
|
+
*/
|
|
25
|
+
export class FingerprintBuilder {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.components = new Map();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Ajoute un composant au hash global.
|
|
32
|
+
* @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
|
|
33
|
+
* @param {string|number|boolean} value - La valeur brute à hasher
|
|
34
|
+
*/
|
|
35
|
+
add(group, value) {
|
|
36
|
+
if (value === undefined || value === null) return this;
|
|
37
|
+
// On hash la valeur individuellement pour l'anonymiser et réduire sa taille
|
|
38
|
+
this.components.set(group, cyrb53(String(value)));
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds a raw component without hashing it.
|
|
44
|
+
* Useful for metrics that need to be read on the server.
|
|
45
|
+
* @param {string} group - The name of the group.
|
|
46
|
+
* @param {string|number} value - The raw value.
|
|
47
|
+
*/
|
|
48
|
+
addRaw(group, value) {
|
|
49
|
+
if (value === undefined || value === null) return this;
|
|
50
|
+
this.components.set(group, value);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Affiche les composants actuels dans la console.
|
|
56
|
+
* @param {string} [title='FingerprintBuilder Components'] - Un titre pour le log.
|
|
57
|
+
*/
|
|
58
|
+
log(title = 'FingerprintBuilder Components') {
|
|
59
|
+
console.log(`--- ${title} ---`);
|
|
60
|
+
const sortedComponents = Array.from(this.components.entries())
|
|
61
|
+
.sort((a, b) => a[0].localeCompare(b[0]));
|
|
62
|
+
|
|
63
|
+
console.table(Object.fromEntries(sortedComponents));
|
|
64
|
+
console.log(`Final string: ${this.toString()}`);
|
|
65
|
+
console.log(`---------------------------------${'-'.repeat(title.length)}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Génère la chaîne de signature finale.
|
|
70
|
+
* Trie les clés pour garantir un ordre déterministe.
|
|
71
|
+
*/
|
|
72
|
+
toString() {
|
|
73
|
+
return Array.from(this.components.entries())
|
|
74
|
+
.sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
|
|
75
|
+
.map(([key, hash]) => `${key}:${hash}`)
|
|
76
|
+
.join("|");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Adds a raw component without hashing it.
|
|
80
|
+
* Useful for metrics that need to be read on the server.
|
|
81
|
+
* @param {string} group - The name of the group.
|
|
82
|
+
* @param {string|number} value - The raw value.
|
|
83
|
+
*/
|
|
84
|
+
addRaw(group, value) {
|
|
85
|
+
if (value === undefined || value === null) return this;
|
|
86
|
+
this.components.set(group, value);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compares two fingerprints and returns a similarity score (0 to 1).
|
|
92
|
+
* Uses weights to give more importance to strong invariants (Canvas, GPU).
|
|
93
|
+
* @param {string} fpString1 - Fingerprint A
|
|
94
|
+
* @param {string} fpString2 - Fingerprint B
|
|
95
|
+
*/
|
|
96
|
+
static compare(fpString1, fpString2) {
|
|
97
|
+
if (!fpString1 || !fpString2) return 0;
|
|
98
|
+
|
|
99
|
+
const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
|
|
100
|
+
|
|
101
|
+
const map1 = parse(fpString1);
|
|
102
|
+
const map2 = parse(fpString2);
|
|
103
|
+
|
|
104
|
+
// Keys to ignore when comparing the initial request fingerprint with the challenge solver's fingerprint.
|
|
105
|
+
// Headers like Client-Hints (ch_*), cookie presence (cookie_keys), and upgrade-insecure-requests
|
|
106
|
+
// can vary or be absent on the subsequent request that submits the solution, especially after a redirect.
|
|
107
|
+
// By ignoring them, we focus the comparison on more stable identifiers like UA, JA3, GPU, etc.
|
|
108
|
+
const volatileKeys = new Set([
|
|
109
|
+
'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
|
|
110
|
+
'cookie_keys', 'upgrade',
|
|
111
|
+
// Also ignore network and http version as they can change between requests (e.g., proxy, protocol upgrade)
|
|
112
|
+
'network', 'http_ver',
|
|
113
|
+
// Ignore proxy-related headers as they are not stable client identifiers
|
|
114
|
+
'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
// Poids de "véracité" (Entropie/Stabilité)
|
|
118
|
+
// Les poids sont augmentés pour donner plus d'importance aux signaux forts.
|
|
119
|
+
const weights = {
|
|
120
|
+
// --- Signaux très forts (difficiles à usurper) ---
|
|
121
|
+
cvs: 5.0, // Canvas: Très haute entropie (Rendu unique du GPU/driver)
|
|
122
|
+
gpu: 4.0, // GPU: Haute entropie (Matériel spécifique)
|
|
123
|
+
ja3: 3.5, // JA3: Identifie la librairie TLS (très stable pour un client donné)
|
|
124
|
+
ua: 2.0, // User-Agent: Signal fort, bien que modifiable
|
|
125
|
+
|
|
126
|
+
// --- Signaux composites et dérivés ---
|
|
127
|
+
client_fp_hash: 3.0, // Le hash de l'empreinte client est un signal très fort.
|
|
128
|
+
browser: 1.5, // Le navigateur extrait du UA.
|
|
129
|
+
os_version: 1.5, // L'OS extrait du UA.
|
|
130
|
+
device_type: 1.0, // Le type d'appareil extrait du UA.
|
|
131
|
+
|
|
132
|
+
// --- Signaux moyens ---
|
|
133
|
+
hw: 1.5, // Hardware (CPU, RAM): Stabilité moyenne
|
|
134
|
+
scr: 1.0, // Screen: Stabilité moyenne
|
|
135
|
+
// 'os' est souvent la même chose que 'ch_platform', on peut le déprécier ou lui donner un poids faible.
|
|
136
|
+
os: 0.8, // OS (nav.platform): Assez stable
|
|
137
|
+
geo: 0.5, // Geo/Langue: Peut changer (VPN, voyage)
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let weightedMatches = 0;
|
|
141
|
+
let totalWeight = 0;
|
|
142
|
+
|
|
143
|
+
const allKeys = new Set([...map1.keys(), ...map2.keys()]);
|
|
144
|
+
|
|
145
|
+
allKeys.forEach((key) => {
|
|
146
|
+
// On ignore les clés volatiles pour cette comparaison spécifique.
|
|
147
|
+
if (volatileKeys.has(key)) return;
|
|
148
|
+
|
|
149
|
+
// On ne compare que les clés qui ont un poids défini.
|
|
150
|
+
const weight = weights[key];
|
|
151
|
+
if (!weight) return;
|
|
152
|
+
|
|
153
|
+
totalWeight += weight; // N'incrémenter que si la clé est pertinente.
|
|
154
|
+
if (map1.get(key) === map2.get(key)) {
|
|
155
|
+
weightedMatches += weight;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
|
|
160
|
+
}
|
|
104
161
|
}
|
package/fingerprint.client.js
CHANGED
|
@@ -335,7 +335,7 @@ const ClientLibrary = {
|
|
|
335
335
|
* @private
|
|
336
336
|
*/
|
|
337
337
|
async solveChallengeAndRetry(response, resource, options) {
|
|
338
|
-
if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json')) {
|
|
338
|
+
if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json') || response.bodyUsed) {
|
|
339
339
|
return response;
|
|
340
340
|
}
|
|
341
341
|
|
|
@@ -346,30 +346,18 @@ const ClientLibrary = {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
|
|
349
|
-
|
|
349
|
+
// L'empreinte de l'appareil qui résout le challenge est cruciale.
|
|
350
|
+
const solverFp = this.getDeviceFingerprint();
|
|
351
|
+
const solutionWrapper = await solveChallenge(challengeData.challenge, solverFp);
|
|
350
352
|
console.log('[Fingerprint] Challenge solved. Retrying original request.');
|
|
351
353
|
|
|
352
354
|
// Ajouter la solution aux paramètres de la requête pour le nouvel essai
|
|
353
355
|
const url = new URL((resource instanceof Request) ? resource.url : String(resource), window.location.origin);
|
|
354
|
-
//
|
|
355
|
-
|
|
356
|
-
url.searchParams.set('pow_nonce', challengeData.challenge.nonce);
|
|
357
|
-
|
|
358
|
-
// La solution est un objet { cpu: ..., mem: ... }. Le serveur attend pow_solution_cpu et pow_solution_mem.
|
|
359
|
-
Object.entries(solution).forEach(([key, value]) => {
|
|
360
|
-
url.searchParams.set(`pow_solution_${key}`, String(value));
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// Pour le challenge d'optimisation, la solution est un tableau d'objets
|
|
364
|
-
if (solution.population) {
|
|
365
|
-
url.searchParams.set('pow_solution_population', JSON.stringify(solution.population));
|
|
366
|
-
}
|
|
356
|
+
// La logique de formatage est maintenant cachée dans la classe ChallengeSolution.
|
|
357
|
+
solutionWrapper.applyToUrl(url);
|
|
367
358
|
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
url.searchParams.set('pow_solution_work_result', JSON.stringify(solution.work_result));
|
|
371
|
-
url.searchParams.set('pow_problem_id', solution.problem_id);
|
|
372
|
-
}
|
|
359
|
+
// On ajoute l'empreinte du solveur à la requête de réessai.
|
|
360
|
+
url.searchParams.set('pow_fp', solverFp);
|
|
373
361
|
|
|
374
362
|
// On utilise la chaîne d'intercepteurs pour la requête réessayée,
|
|
375
363
|
// ce qui garantit que le fetch original est appelé avec le bon contexte.
|
|
@@ -418,8 +406,9 @@ const ClientLibrary = {
|
|
|
418
406
|
// Ajoute l'intercepteur pour la résolution de challenge
|
|
419
407
|
if (fetchConfig.handleChallenges !== false) {
|
|
420
408
|
this.addFetchInterceptor(async (resource, options, next) => {
|
|
421
|
-
const
|
|
422
|
-
|
|
409
|
+
const originalResponse = await next(resource, options);
|
|
410
|
+
// On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
|
|
411
|
+
return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
|
|
423
412
|
});
|
|
424
413
|
}
|
|
425
414
|
}
|