@anonympins/fingerprint 0.7.3 → 0.7.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.
@@ -159,9 +159,14 @@ const ClientLibrary = {
159
159
  * @param {object} [detail={}] - The data to include in the event's detail property.
160
160
  */
161
161
  _dispatchEvent(eventName, detail = {}) {
162
- if (typeof window === 'undefined' || typeof CustomEvent === 'undefined') return;
163
- const event = new CustomEvent(`fingerprint:${eventName}`, { detail });
164
- window.dispatchEvent(event);
162
+ if (typeof window === 'undefined') return;
163
+ const CustomEventCtor = window.CustomEvent || (typeof CustomEvent !== 'undefined' ? CustomEvent : null);
164
+ if (!CustomEventCtor) return;
165
+ try {
166
+ const event = new CustomEventCtor(`fingerprint:${eventName}`, { detail });
167
+ window.dispatchEvent(event);
168
+ } catch (e) {
169
+ }
165
170
  },
166
171
 
167
172
  /**
@@ -190,10 +195,20 @@ const ClientLibrary = {
190
195
  const canvas2d = typeof CanvasRenderingContext2D !== 'undefined' ? CanvasRenderingContext2D : win.CanvasRenderingContext2D;
191
196
  const webgl = typeof WebGLRenderingContext !== 'undefined' ? WebGLRenderingContext : win.WebGLRenderingContext;
192
197
 
198
+ const isFetchTampered = () => {
199
+ if (ClientLibrary._isFetchPatched) {
200
+ if (ClientLibrary._originalFetch) {
201
+ return !Function.prototype.toString.call(ClientLibrary._originalFetch).includes('[native code]');
202
+ }
203
+ return false;
204
+ }
205
+ return checkNative(win, 'fetch');
206
+ };
207
+
193
208
  return checkNative(htmlCanvas?.prototype, 'toDataURL') ||
194
209
  checkNative(canvas2d?.prototype, 'getImageData') ||
195
210
  checkNative(webgl?.prototype, 'getParameter') ||
196
- checkNative(win, 'fetch');
211
+ isFetchTampered();
197
212
  },
198
213
 
199
214
  /**
@@ -397,54 +412,80 @@ const ClientLibrary = {
397
412
  },
398
413
 
399
414
  /**
400
- * Injects invisible phantom interactive elements to trap bots (focus/hover).
415
+ * Injects invisible phantom interactive elements and forms within a closed Shadow DOM container to trap bots (focus/hover/click).
401
416
  */
402
417
  injectPhantomTraps() {
403
- if (typeof document === 'undefined') return;
418
+ if (typeof document === 'undefined' || !document.body) return;
419
+
420
+ const host = document.createElement('div');
421
+ host.setAttribute('aria-hidden', 'true');
422
+ host.style.position = 'absolute';
423
+ host.style.left = '-9999px';
424
+ host.style.top = '-9999px';
425
+ host.style.width = '0';
426
+ host.style.height = '0';
427
+ host.style.overflow = 'hidden';
428
+
429
+ const shadow = typeof host.attachShadow === 'function'
430
+ ? host.attachShadow({ mode: 'closed' })
431
+ : host;
432
+
433
+ const style = document.createElement('style');
434
+ style.textContent = `
435
+ :host {
436
+ position: absolute;
437
+ left: -9999px;
438
+ top: -9999px;
439
+ width: 0;
440
+ height: 0;
441
+ overflow: hidden;
442
+ }
443
+ a, input, button, form {
444
+ opacity: 0.001;
445
+ pointer-events: auto;
446
+ }
447
+ `;
448
+ shadow.appendChild(style);
404
449
 
405
- // Create a phantom interactive element
450
+ const triggerTrap = () => {
451
+ this.onHoneypotTrigger();
452
+ };
453
+
454
+ // 1. Phantom interactive link
406
455
  const phantom = document.createElement('a');
407
456
  phantom.href = '#';
408
457
  // Random deceptive name to attract automated link/form parsers
409
458
  const phantomNames = ['sys-session-recovery', 'auth-token-refresh', 'debug-console-login', 'admin-portal-access', 'security-bypass-bypass', 'recovery-key-session', 'api-key-test', 'client-secrets-access'];
410
459
  phantom.id = phantomNames[Math.floor(secureRandom() * phantomNames.length)] + '-' + genRandStr(6);
411
460
  phantom.className = genRandStr(8);
412
- phantom.tabIndex = 0; // In natural tab flow
413
- phantom.setAttribute('aria-hidden', 'true'); // Hidden from legitimate screen readers
414
-
415
- // Invisible yet interactive style (1px x 1px, nearly transparent)
416
- phantom.style.position = 'fixed';
417
- phantom.style.top = '1px';
418
- phantom.style.left = '1px';
419
- phantom.style.width = '1px';
420
- phantom.style.height = '1px';
421
- phantom.style.opacity = '0.001';
422
- phantom.style.zIndex = '99999';
423
- phantom.style.overflow = 'hidden';
424
- phantom.style.pointerEvents = 'auto';
425
-
426
- const triggerTrap = () => {
427
- this.onHoneypotTrigger();
428
- };
429
-
461
+ phantom.tabIndex = 0; // In natural tab flow for crawler parsers
430
462
  phantom.addEventListener('focus', triggerTrap, { passive: true });
431
463
  phantom.addEventListener('mouseover', triggerTrap, { passive: true });
432
-
433
- // Polymorphic DOM insertion of the phantom trap
434
- const nestingOptions = [
435
- () => document.body.appendChild(phantom),
436
- () => {
437
- const wrapper = document.createElement(secureRandom() > 0.5 ? 'span' : 'div');
438
- wrapper.className = genRandStr(8);
439
- wrapper.style.position = 'absolute';
440
- wrapper.style.width = '0';
441
- wrapper.style.height = '0';
442
- wrapper.style.overflow = 'hidden';
443
- wrapper.appendChild(phantom);
444
- document.body.appendChild(wrapper);
445
- }
446
- ];
447
- nestingOptions[Math.floor(secureRandom() * nestingOptions.length)]();
464
+ phantom.addEventListener('click', (e) => {
465
+ if (e && typeof e.preventDefault === 'function') e.preventDefault();
466
+ triggerTrap();
467
+ }, { passive: false });
468
+
469
+ // 2. Deceptive hidden form with bait input
470
+ const phantomForm = document.createElement('form');
471
+ phantomForm.action = '#';
472
+ phantomForm.tabIndex = -1;
473
+ const inputNames = ['session_token', 'admin_pwd', 'secret_key', 'auth_bypass'];
474
+ const phantomInput = document.createElement('input');
475
+ phantomInput.type = 'text';
476
+ phantomInput.name = inputNames[Math.floor(secureRandom() * inputNames.length)] + '_' + genRandStr(4);
477
+ phantomInput.tabIndex = 0;
478
+ phantomInput.autocomplete = 'off';
479
+ phantomInput.addEventListener('focus', triggerTrap, { passive: true });
480
+ phantomInput.addEventListener('input', triggerTrap, { passive: true });
481
+ phantomInput.addEventListener('click', triggerTrap, { passive: true });
482
+ phantomInput.addEventListener('change', triggerTrap, { passive: true });
483
+
484
+ phantomForm.appendChild(phantomInput);
485
+ shadow.appendChild(phantom);
486
+ shadow.appendChild(phantomForm);
487
+
488
+ document.body.appendChild(host);
448
489
  },
449
490
 
450
491
  /**
@@ -785,7 +826,7 @@ const ClientLibrary = {
785
826
  });
786
827
 
787
828
  // 3. Generate trap inputs inside a closed Shadow DOM
788
- if (typeof document !== 'undefined' && honeypotFieldNames.length > 0) {
829
+ if (typeof document !== 'undefined' && honeypotFieldNames.length > 0 && document.body) {
789
830
  const host = document.createElement('div');
790
831
  host.setAttribute('aria-hidden', 'true');
791
832
  host.style.position = 'absolute';
@@ -793,7 +834,9 @@ const ClientLibrary = {
793
834
  host.style.height = '0';
794
835
  host.style.overflow = 'hidden';
795
836
 
796
- const shadow = host.attachShadow({ mode: 'closed' });
837
+ const shadow = typeof host.attachShadow === 'function'
838
+ ? host.attachShadow({ mode: 'closed' })
839
+ : host;
797
840
 
798
841
  // Randomize classes, CSS variables, and layout tags
799
842
  const wrapperClass = genRandStr(10);
@@ -843,6 +886,7 @@ const ClientLibrary = {
843
886
  input.addEventListener('input', trigger, { passive: true });
844
887
  input.addEventListener('change', trigger, { passive: true });
845
888
  input.addEventListener('focus', trigger, { passive: true });
889
+ input.addEventListener('click', trigger, { passive: true });
846
890
 
847
891
  // Polymorphic nesting of label and input
848
892
  const nestingType = Math.floor(secureRandom() * 3);
@@ -949,38 +993,118 @@ const ClientLibrary = {
949
993
  },
950
994
 
951
995
  /**
952
- * Retrieves collected client behavioral metrics.
953
- * Call before submitting sensitive requests.
954
- * @returns {ClientBehaviorMetrics}
996
+ * Évalue le comportement côté client en local sans transmettre de données brutes.
997
+ * Retourne 1 si le comportement est humain/légitime, 0 en cas de suspicion/bot.
998
+ * @returns {number} 1 ou 0
955
999
  */
956
- getClientBehaviorMetrics() {
957
- // Add history length as a behavioral signal.
958
- metrics.historyLength = window.history.length;
1000
+ evaluateBehavior() {
1001
+ let score = 0;
1002
+ if (metrics.honeypotInteraction) return 100;
1003
+ if (ClientLibrary.detectTamperedPrototypes()) score += 80;
1004
+
1005
+ // Vérification du rendu graphique
1006
+ if (metrics.rendering) {
1007
+ if (metrics.rendering.offscreenAnom) return 0;
1008
+ const fps = parseFloat(metrics.rendering.fps || 0);
1009
+ const jitter = parseFloat(metrics.rendering.jitter || 0);
1010
+ if (fps > 250 || (fps > 0 && fps < 15)) score += 50;
1011
+ if (jitter > 6.0) score += Math.min(80, (jitter - 6.0) * 10);
1012
+ }
959
1013
 
960
- // Attach collection timestamp for replay detection
961
- metrics.clicksHistory = clicksHistory;
962
- metrics.clientTimestamp = Date.now();
1014
+ // Mouvements de souris robotiques
1015
+ if (mouseMovementsHistory.length >= 3) {
1016
+ let totalDist = 0;
1017
+ let pauses = 0;
1018
+ const segments = [];
1019
+ for (let i = 1; i < mouseMovementsHistory.length; i++) {
1020
+ const p1 = mouseMovementsHistory[i - 1];
1021
+ const p2 = mouseMovementsHistory[i];
1022
+ const dx = p2.x - p1.x;
1023
+ const dy = p2.y - p1.y;
1024
+ const dt = p2.t - p1.t;
1025
+ const dist = Math.sqrt(dx * dx + dy * dy);
1026
+ if (dt > 0) segments.push(dist / dt);
1027
+ totalDist += dist;
1028
+ if (dt > 100 && dist < 5) pauses++;
1029
+ }
1030
+ if (segments.length >= 2) {
1031
+ const totalTime = mouseMovementsHistory[mouseMovementsHistory.length - 1].t - mouseMovementsHistory[0].t;
1032
+ const avgSpeed = totalTime > 0 ? totalDist / totalTime : 0;
1033
+ const pStart = mouseMovementsHistory[0];
1034
+ const pEnd = mouseMovementsHistory[mouseMovementsHistory.length - 1];
1035
+ const straightDist = Math.sqrt(Math.pow(pEnd.x - pStart.x, 2) + Math.pow(pEnd.y - pStart.y, 2));
1036
+ const straightness = totalDist > 0 ? straightDist / totalDist : 1;
1037
+ if (avgSpeed > 3.0) score += 25;
1038
+ if (straightness > 0.95) score += 30;
1039
+ if (pauses === 0 && segments.length > 20) score += 15;
1040
+ }
1041
+ }
963
1042
 
964
- metrics.touchMovementsHistory = touchMovementsHistory;
965
- if (ClientLibrary.detectTamperedPrototypes()) {
966
- metrics.prototypeTampered = true;
1043
+ // Analyse des événements tactiles
1044
+ if (touchMovementsHistory.length >= 3) {
1045
+ let totalDist = 0;
1046
+ let totalPressure = 0;
1047
+ let totalRadius = 0;
1048
+ for (const pt of touchMovementsHistory) {
1049
+ totalPressure += pt.p || 0;
1050
+ totalRadius += pt.r || 0;
1051
+ }
1052
+ const avgPressure = totalPressure / touchMovementsHistory.length;
1053
+ const avgRadius = totalRadius / touchMovementsHistory.length;
1054
+ let sqDiffPressure = 0;
1055
+ let sqDiffRadius = 0;
1056
+ for (const pt of touchMovementsHistory) {
1057
+ sqDiffPressure += Math.pow((pt.p || 0) - avgPressure, 2);
1058
+ sqDiffRadius += Math.pow((pt.r || 0) - avgRadius, 2);
1059
+ }
1060
+ if (avgPressure > 0 && (sqDiffPressure / touchMovementsHistory.length) === 0) {
1061
+ score += 30;
1062
+ }
1063
+ if (avgRadius > 0 && (sqDiffRadius / touchMovementsHistory.length) === 0) {
1064
+ score += 30;
1065
+ }
967
1066
  }
968
- metrics.motionVariance = metrics.motionVariance ?? -1;
969
- // Include mouse trajectory history for server-side evaluation
970
- metrics.mouseMovementsHistory = mouseMovementsHistory;
971
-
972
- // NOUVEAU: Keystroke dynamics metrics (dwell and flight times)
973
- metrics.keystrokeDwellTimes = keystrokeDwellTimes;
974
- metrics.keystrokeFlightTimes = keystrokeFlightTimes;
975
-
976
- // Calculate mean keystroke latency
977
- if (keystrokeLatencies.length > 0) {
978
- const sum = keystrokeLatencies.reduce((a, b) => a + b, 0);
979
- metrics.keystrokeLatency = sum / keystrokeLatencies.length;
980
- } else {
981
- metrics.keystrokeLatency = 0;
1067
+
1068
+ // Détection de rack / ferme de téléphones mobiles immobiles
1069
+ if (typeof window !== 'undefined' && (window.navigator?.userAgent || '').includes('Mobile')) {
1070
+ if (metrics.motionVariance === 0) return 0;
982
1071
  }
983
- return metrics;
1072
+
1073
+ // Variance des clics ultra-précise (bot de clic)
1074
+ if (clicksHistory.length >= 3) {
1075
+ const targets = {};
1076
+ for (const c of clicksHistory) {
1077
+ if (!c.targetId) continue;
1078
+ if (!targets[c.targetId]) targets[c.targetId] = [];
1079
+ targets[c.targetId].push(c);
1080
+ }
1081
+ for (const id in targets) {
1082
+ const clks = targets[id];
1083
+ if (clks.length >= 3) {
1084
+ const mx = clks.reduce((s, c) => s + c.x, 0) / clks.length;
1085
+ const my = clks.reduce((s, c) => s + c.y, 0) / clks.length;
1086
+ const v = clks.reduce((s, c) => s + Math.pow(c.x - mx, 2) + Math.pow(c.y - my, 2), 0) / clks.length;
1087
+ if (v < 1.0) return 0;
1088
+ }
1089
+ }
1090
+ }
1091
+
1092
+ // Dynamique des touches de clavier robotique
1093
+ if (keystrokeDwellTimes.length >= 5) {
1094
+ const mean = keystrokeDwellTimes.reduce((a, b) => a + b, 0) / keystrokeDwellTimes.length;
1095
+ const variance = keystrokeDwellTimes.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / keystrokeDwellTimes.length;
1096
+ if (Math.sqrt(variance) < 2.0 || mean < 15.0) return 0;
1097
+ }
1098
+
1099
+ return 1;
1100
+ },
1101
+
1102
+ /**
1103
+ * Récupère le résultat comportemental sous forme binaire (1 = valide/humain, 0 = suspect).
1104
+ * @returns {number} 1 ou 0
1105
+ */
1106
+ getClientBehaviorMetrics() {
1107
+ return this.evaluateBehavior();
984
1108
  },
985
1109
 
986
1110
  /**
@@ -991,14 +1115,13 @@ const ClientLibrary = {
991
1115
  */
992
1116
  async protectedFetch(resource, options = {}) {
993
1117
  const fp = this.getDeviceFingerprint();
994
- const rawBehavior = this.getClientBehaviorMetrics();
995
1118
  const zkpProof = await this.generateZkpProof(fp); // Generate ZKP proof
996
- const behavior = await signMetrics(rawBehavior);
1119
+ const behavior = this.getClientBehaviorMetrics();
997
1120
 
998
1121
  const headers = new Headers(options.headers || {});
999
1122
  headers.set('X-ZKP-Proof', zkpProof); // Add ZKP proof to headers
1000
1123
  headers.set('X-Device-Fingerprint', fp);
1001
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
1124
+ headers.set('X-Behavior-Metrics', String(behavior));
1002
1125
 
1003
1126
  options.headers = headers;
1004
1127
  return fetch(resource, options);
@@ -1008,6 +1131,8 @@ const ClientLibrary = {
1008
1131
  _isFetchPatched: false,
1009
1132
  _interceptorChain: [],
1010
1133
  // Store original fetch bound to window to prevent illegal invocation errors
1134
+ _targetDomains: [],
1135
+ _swRegistration: null,
1011
1136
  _originalFetch: (typeof window !== 'undefined') ? window.fetch.bind(window) : null,
1012
1137
 
1013
1138
  /**
@@ -1045,10 +1170,78 @@ const ClientLibrary = {
1045
1170
  */
1046
1171
  onHoneypotTrigger() {
1047
1172
  metrics.honeypotInteraction = true;
1173
+ if (this.workerPath) {
1174
+ this.syncServiceWorkerCredentials();
1175
+ }
1048
1176
  // Dispatch event allowing host applications to react
1049
1177
  this._dispatchEvent('honeypotTriggered');
1050
1178
  },
1051
1179
 
1180
+ /**
1181
+ * Registers and manages the Service Worker for transparent HTTPS request decoration.
1182
+ * @param {string} workerPath - Path to the worker script.
1183
+ * @param {string[]} [targetDomains=[]] - Domains to intercept.
1184
+ * @param {string} [scope] - Service Worker registration scope.
1185
+ * @returns {Promise<ServiceWorkerRegistration|null>}
1186
+ */
1187
+ async initServiceWorker(workerPath, targetDomains = [], scope = undefined) {
1188
+ if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) {
1189
+ console.warn('[Fingerprint] Service Worker is not supported in this browser.');
1190
+ return null;
1191
+ }
1192
+
1193
+ try {
1194
+ const registerOptions = scope ? { scope } : {};
1195
+ const registration = await navigator.serviceWorker.register(workerPath, registerOptions);
1196
+ this._swRegistration = registration;
1197
+
1198
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
1199
+ this.syncServiceWorkerCredentials(targetDomains);
1200
+ });
1201
+
1202
+ await navigator.serviceWorker.ready;
1203
+ this.syncServiceWorkerCredentials(targetDomains);
1204
+
1205
+ if (typeof window !== 'undefined') {
1206
+ window.addEventListener('focus', () => this.syncServiceWorkerCredentials(targetDomains), { passive: true });
1207
+ document.addEventListener('visibilitychange', () => {
1208
+ if (document.visibilityState === 'visible') {
1209
+ this.syncServiceWorkerCredentials(targetDomains);
1210
+ }
1211
+ }, { passive: true });
1212
+ }
1213
+
1214
+ console.log('[Fingerprint] Network Service Worker active and synchronized.');
1215
+ return registration;
1216
+ } catch (error) {
1217
+ console.warn('[Fingerprint] Failed to register Service Worker:', error);
1218
+ return null;
1219
+ }
1220
+ },
1221
+
1222
+ /**
1223
+ * Synchronizes device fingerprint and behavior metrics with the active Service Worker.
1224
+ * @param {string[]} [targetDomains=[]]
1225
+ */
1226
+ syncServiceWorkerCredentials(targetDomains = []) {
1227
+ if (typeof navigator === 'undefined' || !navigator.serviceWorker) return;
1228
+ const domains = (Array.isArray(targetDomains) && targetDomains.length > 0)
1229
+ ? targetDomains
1230
+ : (this._targetDomains || []);
1231
+ const payload = {
1232
+ type: 'UPDATE_CREDENTIALS',
1233
+ fp: this.getDeviceFingerprint(),
1234
+ behavior: this.getClientBehaviorMetrics(),
1235
+ targetDomains: domains
1236
+ };
1237
+
1238
+ if (navigator.serviceWorker.controller) {
1239
+ navigator.serviceWorker.controller.postMessage(payload);
1240
+ } else if (this._swRegistration && this._swRegistration.active) {
1241
+ this._swRegistration.active.postMessage(payload);
1242
+ }
1243
+ },
1244
+
1052
1245
  /**
1053
1246
  * Initializes the fingerprinting fetch interceptor.
1054
1247
  * Appends to interceptor chain without clobbering existing handlers.
@@ -1071,11 +1264,10 @@ const ClientLibrary = {
1071
1264
 
1072
1265
  if (shouldProtect) {
1073
1266
  const fp = this.getDeviceFingerprint();
1074
- const rawBehavior = this.getClientBehaviorMetrics();
1075
- const behavior = await signMetrics(rawBehavior);
1267
+ const behavior = this.getClientBehaviorMetrics();
1076
1268
  const headers = new Headers(options.headers || {});
1077
1269
  headers.set('X-Device-Fingerprint', fp);
1078
- headers.set('X-Behavior-Metrics', JSON.stringify(behavior));
1270
+ headers.set('X-Behavior-Metrics', String(behavior));
1079
1271
  options.headers = headers;
1080
1272
  }
1081
1273
 
@@ -1092,7 +1284,7 @@ const ClientLibrary = {
1092
1284
  * @private
1093
1285
  */
1094
1286
  injectTrapLinks(urls) {
1095
- if (!urls || urls.length === 0 || typeof document === 'undefined') {
1287
+ if (!urls || urls.length === 0 || typeof document === 'undefined' || !document.body) {
1096
1288
  return;
1097
1289
  }
1098
1290
 
@@ -1103,7 +1295,9 @@ const ClientLibrary = {
1103
1295
  host.style.height = '0';
1104
1296
  host.style.overflow = 'hidden';
1105
1297
 
1106
- const shadow = host.attachShadow({ mode: 'closed' });
1298
+ const shadow = typeof host.attachShadow === 'function'
1299
+ ? host.attachShadow({ mode: 'closed' })
1300
+ : host;
1107
1301
 
1108
1302
  // Randomize classes, CSS variables, and layout tags
1109
1303
  const wrapperClass = genRandStr(10);
@@ -1167,10 +1361,13 @@ const ClientLibrary = {
1167
1361
  link.innerHTML = `<span>&gt; ${i + 1}</span>`;
1168
1362
  }
1169
1363
 
1170
- const trigger = () => {
1364
+ const trigger = (e) => {
1365
+ if (e && typeof e.preventDefault === 'function') {
1366
+ e.preventDefault();
1367
+ }
1171
1368
  this.onHoneypotTrigger();
1172
1369
  };
1173
- link.addEventListener('click', trigger, { passive: true });
1370
+ link.addEventListener('click', trigger, { passive: false });
1174
1371
  link.addEventListener('focus', trigger, { passive: true });
1175
1372
  link.addEventListener('mouseover', trigger, { passive: true });
1176
1373
 
@@ -1193,10 +1390,20 @@ const ClientLibrary = {
1193
1390
  * @param {Response} response - Initial response (status 404/challenge).
1194
1391
  * @param {RequestInfo} resource - Original request resource.
1195
1392
  * @param {RequestInit} options - Original request options.
1393
+ * @param {number} [maxRetries=3] - Maximum allowed challenge retry attempts.
1196
1394
  * @returns {Promise<Response>} - Retried response.
1197
1395
  * @private
1198
1396
  */
1199
- async solveChallengeAndRetry(response, resource, options) {
1397
+ async solveChallengeAndRetry(response, resource, options = {}, maxRetries = 3) {
1398
+ const effectiveMaxRetries = (options && typeof options._powMaxRetries === 'number')
1399
+ ? options._powMaxRetries
1400
+ : (typeof maxRetries === 'number' ? maxRetries : 3);
1401
+ const currentRetries = (options && options._powRetryCount) || (resource && resource._powRetryCount) || 0;
1402
+ if (currentRetries >= effectiveMaxRetries) {
1403
+ console.warn(`[Fingerprint] Challenge retry limit reached (${effectiveMaxRetries}). Aborting challenge retry loop.`);
1404
+ return response;
1405
+ }
1406
+
1200
1407
  if (!response || response.status !== 404 || !response.headers?.get?.('content-type')?.includes('application/json') || response.bodyUsed) {
1201
1408
  return response;
1202
1409
  }
@@ -1223,8 +1430,16 @@ const ClientLibrary = {
1223
1430
  // Attach solver fingerprint to retry request
1224
1431
  url.searchParams.set('pow_fp', solverFp);
1225
1432
 
1226
- // Use fetch to retry with appropriate execution context
1227
- return window.fetch(url.toString(), options);
1433
+ // Use fetch to retry with incremented retry count to prevent infinite challenge loops
1434
+ const retryOptions = {
1435
+ ...options,
1436
+ _powRetryCount: currentRetries + 1,
1437
+ _powMaxRetries: effectiveMaxRetries
1438
+ };
1439
+ const fetchFn = (typeof window !== 'undefined' && typeof window.fetch === 'function')
1440
+ ? window.fetch
1441
+ : fetch;
1442
+ return fetchFn(url.toString(), retryOptions);
1228
1443
  } catch (e) {
1229
1444
  console.error('[Fingerprint] Failed to solve or retry challenge:', e);
1230
1445
  return response; // Retourne la réponse 429 originale en cas d'échec
@@ -1249,6 +1464,7 @@ const ClientLibrary = {
1249
1464
  honeypots = [],
1250
1465
  trapUrls = [], // Trap URLs
1251
1466
  wasmPath,
1467
+ worker = false,
1252
1468
  workerPath, // NOUVEAU
1253
1469
  fetch: fetchConfig = {}
1254
1470
  } = config;
@@ -1262,8 +1478,30 @@ const ClientLibrary = {
1262
1478
  if (mouse) {
1263
1479
  this.startMouseEntropyTracker();
1264
1480
  }
1265
- if (workerPath) {
1266
- this.workerPath = workerPath;
1481
+
1482
+ // Instantiation du Service Worker via initializeClient
1483
+ let resolvedWorkerPath = null;
1484
+ let workerScope = undefined;
1485
+ let workerTargetDomains = fetchConfig.targetDomains || [];
1486
+
1487
+ if (typeof worker === 'string') {
1488
+ resolvedWorkerPath = worker;
1489
+ } else if (typeof worker === 'object' && worker !== null) {
1490
+ resolvedWorkerPath = worker.path || '/fingerprint.worker.js';
1491
+ workerScope = worker.scope;
1492
+ if (Array.isArray(worker.targetDomains)) {
1493
+ workerTargetDomains = worker.targetDomains;
1494
+ }
1495
+ } else if (worker === true) {
1496
+ resolvedWorkerPath = '/fingerprint.worker.js';
1497
+ } else if (workerPath) {
1498
+ resolvedWorkerPath = workerPath;
1499
+ }
1500
+
1501
+ if (resolvedWorkerPath) {
1502
+ this.workerPath = resolvedWorkerPath;
1503
+ this._targetDomains = workerTargetDomains;
1504
+ this.initServiceWorker(resolvedWorkerPath, workerTargetDomains, workerScope);
1267
1505
  }
1268
1506
  if (keystrokes) {
1269
1507
  this.startKeystrokeDynamicsTracker();
@@ -1294,16 +1532,17 @@ const ClientLibrary = {
1294
1532
  if (trapUrls.length > 0) {
1295
1533
  this.injectTrapLinks(trapUrls);
1296
1534
  }
1297
- // Enable fetch interception if configured
1298
- if (config.fetch) {
1535
+ // Enable fetch monkey-patching only if explicitly configured and no Service Worker is used
1536
+ if (config.fetch && !resolvedWorkerPath) {
1299
1537
  this.initializeFetch(fetchConfig.targetDomains);
1300
1538
 
1301
1539
  // Add challenge resolution interceptor
1302
1540
  if (fetchConfig.handleChallenges !== false) {
1541
+ const maxRetries = typeof fetchConfig.maxRetries === 'number' ? fetchConfig.maxRetries : 3;
1303
1542
  this.addFetchInterceptor(async (resource, options, next) => {
1304
1543
  const originalResponse = await next(resource, options);
1305
1544
  // Clone response to prevent draining the body stream for original caller
1306
- return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
1545
+ return this.solveChallengeAndRetry(originalResponse.clone(), resource, options, maxRetries);
1307
1546
  });
1308
1547
  }
1309
1548
  }
@@ -1521,6 +1760,8 @@ export const generateZkpProof = ClientLibrary.generateZkpProof.bind(ClientLibrar
1521
1760
  export const initializeSpace = ClientLibrary.initializeSpace.bind(ClientLibrary);
1522
1761
  export const readSpaceBlock = ClientLibrary.readSpaceBlock.bind(ClientLibrary);
1523
1762
  export const solveSpaceChallenge = ClientLibrary.solveSpaceChallenge.bind(ClientLibrary);
1763
+ export const initServiceWorker = ClientLibrary.initServiceWorker.bind(ClientLibrary);
1764
+ export const syncServiceWorkerCredentials = ClientLibrary.syncServiceWorkerCredentials.bind(ClientLibrary);
1524
1765
 
1525
1766
  // Export the internal object for testing purposes
1526
1767
  export default ClientLibrary;