@aientrophy/sdk 0.2.0 → 0.3.0

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.
@@ -7,33 +7,100 @@ const CONFIG = {
7
7
  RSA_PUBLIC_KEY: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqbBCvDqyUtfhgI/Lo5pDtn1phwA6qczAyp8N1ZgQ68OTxgbJUGiXV6N67p15bBdME1R4va51P2Czq2IbRby/N+GHTciTaXvmusV8IjnIOGtKlGxWCuKIPrCS+rGjGA2j1irxBbNpqItltFxjhxBGOzsyQAF6LNIz5IKA7sC6cSH8zUyLuFrV96udc801Zc4nCJG64ZljNbDvlVbJJZ6ex5OLLS6AnrVAXpjEDR/MI/I8JvAPe/psHj6EpXgvKrBYMBOH3jzQRzMFoR79jXyGNgPjdy0A+f6RVuEG8H5sUXKeyy+cFvaT+pm6h+t6RPED11tCOwQxYZ2pExhhPxDGeQIDAQAB"
8
8
  };
9
9
  class CryptoUtils {
10
- // Base64
10
+ // 2 = ECDH, 1 = RSA
11
11
  /**
12
- * Initialize session key if not exists
12
+ * Initialize session key using ECDH + HKDF (PFS) or RSA fallback.
13
13
  */
14
14
  static async init() {
15
15
  if (this.sessionKey) return;
16
- this.sessionKey = await window.crypto.subtle.generateKey(
16
+ try {
17
+ await this.initECDH();
18
+ } catch (e) {
19
+ await this.initRSA();
20
+ }
21
+ }
22
+ /**
23
+ * ECDH + HKDF key exchange — provides Perfect Forward Secrecy.
24
+ *
25
+ * 1. Generate ephemeral ECDH key pair (P-256)
26
+ * 2. Fetch server's ephemeral ECDH public key from /key-exchange
27
+ * 3. Derive shared secret via ECDH
28
+ * 4. Derive AES-256-GCM key via HKDF-SHA256
29
+ */
30
+ static async initECDH() {
31
+ const keyPair = await window.crypto.subtle.generateKey(
32
+ { name: "ECDH", namedCurve: "P-256" },
33
+ true,
34
+ ["deriveBits"]
35
+ );
36
+ const clientPubRaw = await window.crypto.subtle.exportKey("raw", keyPair.publicKey);
37
+ this.ecdhPublicKeyBase64 = this.arrayBufferToBase64(clientPubRaw);
38
+ const endpoint = window.__securitySDKEndpoint || "";
39
+ const baseUrl = endpoint.replace(/\/events$/, "");
40
+ const res = await fetch(`${baseUrl}/key-exchange`, {
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
+ body: JSON.stringify({ clientPublicKey: this.ecdhPublicKeyBase64 })
44
+ });
45
+ if (!res.ok) throw new Error("Key exchange failed");
46
+ const { serverPublicKey, salt } = await res.json();
47
+ const serverPubBytes = this.base64ToArrayBuffer(serverPublicKey);
48
+ const serverPubKey = await window.crypto.subtle.importKey(
49
+ "raw",
50
+ serverPubBytes,
51
+ { name: "ECDH", namedCurve: "P-256" },
52
+ false,
53
+ []
54
+ );
55
+ const sharedBits = await window.crypto.subtle.deriveBits(
56
+ { name: "ECDH", public: serverPubKey },
57
+ keyPair.privateKey,
58
+ 256
59
+ );
60
+ const hkdfKey = await window.crypto.subtle.importKey(
61
+ "raw",
62
+ sharedBits,
63
+ "HKDF",
64
+ false,
65
+ ["deriveKey"]
66
+ );
67
+ const saltBytes = this.base64ToArrayBuffer(salt);
68
+ this.sessionKey = await window.crypto.subtle.deriveKey(
17
69
  {
18
- name: "AES-GCM",
19
- length: 256
70
+ name: "HKDF",
71
+ hash: "SHA-256",
72
+ salt: saltBytes,
73
+ info: new TextEncoder().encode("aientrophy-session-enc")
20
74
  },
75
+ hkdfKey,
76
+ { name: "AES-GCM", length: 256 },
21
77
  true,
22
78
  ["encrypt", "decrypt"]
23
79
  );
24
- const publicKey = await this.importPublicKey(CONFIG.RSA_PUBLIC_KEY);
80
+ this.protocolVersion = 2;
81
+ }
82
+ /**
83
+ * RSA-OAEP key exchange (legacy fallback, no PFS).
84
+ */
85
+ static async initRSA() {
86
+ this.sessionKey = await window.crypto.subtle.generateKey(
87
+ { name: "AES-GCM", length: 256 },
88
+ true,
89
+ ["encrypt", "decrypt"]
90
+ );
91
+ const publicKey = await this.importRSAPublicKey(CONFIG.RSA_PUBLIC_KEY);
25
92
  const rawSessionKey = await window.crypto.subtle.exportKey("raw", this.sessionKey);
26
93
  const encryptedKeyBuffer = await window.crypto.subtle.encrypt(
27
- {
28
- name: "RSA-OAEP"
29
- },
94
+ { name: "RSA-OAEP" },
30
95
  publicKey,
31
96
  rawSessionKey
32
97
  );
33
98
  this.encryptedSessionKey = this.arrayBufferToBase64(encryptedKeyBuffer);
99
+ this.protocolVersion = 1;
34
100
  }
35
101
  /**
36
- * Encrypt Payload using AES-GCM
102
+ * Encrypt Payload using AES-GCM.
103
+ * Returns envelope with version indicator for server-side dispatch.
37
104
  */
38
105
  static async encrypt(data) {
39
106
  if (!this.sessionKey) await this.init();
@@ -41,10 +108,7 @@ class CryptoUtils {
41
108
  const encodedData = new TextEncoder().encode(jsonString);
42
109
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
43
110
  const encryptedBuffer = await window.crypto.subtle.encrypt(
44
- {
45
- name: "AES-GCM",
46
- iv
47
- },
111
+ { name: "AES-GCM", iv },
48
112
  this.sessionKey,
49
113
  encodedData
50
114
  );
@@ -52,12 +116,16 @@ class CryptoUtils {
52
116
  const tagLength = 16;
53
117
  const ciphertext = encryptedBytes.slice(0, encryptedBytes.length - tagLength);
54
118
  const tag = encryptedBytes.slice(encryptedBytes.length - tagLength);
55
- return {
56
- ek: this.encryptedSessionKey,
119
+ const envelope = {
120
+ ek: this.protocolVersion === 2 ? this.ecdhPublicKeyBase64 : this.encryptedSessionKey,
57
121
  d: this.arrayBufferToBase64(ciphertext.buffer),
58
122
  iv: this.arrayBufferToBase64(iv.buffer),
59
123
  tag: this.arrayBufferToBase64(tag.buffer)
60
124
  };
125
+ if (this.protocolVersion === 2) {
126
+ envelope.v = 2;
127
+ }
128
+ return envelope;
61
129
  }
62
130
  /**
63
131
  * Decrypt Response using AES-GCM
@@ -71,26 +139,27 @@ class CryptoUtils {
71
139
  combinedBuffer.set(new Uint8Array(ciphertext), 0);
72
140
  combinedBuffer.set(new Uint8Array(tag), ciphertext.byteLength);
73
141
  const decryptedBuffer = await window.crypto.subtle.decrypt(
74
- {
75
- name: "AES-GCM",
76
- iv
77
- },
142
+ { name: "AES-GCM", iv },
78
143
  this.sessionKey,
79
144
  combinedBuffer
80
145
  );
81
146
  const decodedString = new TextDecoder().decode(decryptedBuffer);
82
147
  return JSON.parse(decodedString);
83
148
  }
84
- static async importPublicKey(pemContent) {
149
+ /**
150
+ * Store the endpoint URL for ECDH key exchange.
151
+ * Called from Transmitter.setConfig().
152
+ */
153
+ static setEndpoint(endpoint) {
154
+ window.__securitySDKEndpoint = endpoint;
155
+ }
156
+ static async importRSAPublicKey(pemContent) {
85
157
  const binaryDerString = window.atob(pemContent);
86
158
  const binaryDer = this.str2ab(binaryDerString);
87
159
  return await window.crypto.subtle.importKey(
88
160
  "spki",
89
161
  binaryDer,
90
- {
91
- name: "RSA-OAEP",
92
- hash: "SHA-256"
93
- },
162
+ { name: "RSA-OAEP", hash: "SHA-256" },
94
163
  true,
95
164
  ["encrypt"]
96
165
  );
@@ -123,7 +192,10 @@ class CryptoUtils {
123
192
  }
124
193
  }
125
194
  __publicField(CryptoUtils, "sessionKey", null);
195
+ __publicField(CryptoUtils, "ecdhPublicKeyBase64", null);
126
196
  __publicField(CryptoUtils, "encryptedSessionKey", null);
197
+ // RSA fallback
198
+ __publicField(CryptoUtils, "protocolVersion", 2);
127
199
  const THREAT_EVENTS = /* @__PURE__ */ new Set([
128
200
  "rapid_click",
129
201
  "honeypot_triggered",
@@ -136,7 +208,10 @@ const THREAT_EVENTS = /* @__PURE__ */ new Set([
136
208
  "input_metrics",
137
209
  "keystroke_dynamics",
138
210
  "environmental_integrity",
139
- "init_signals"
211
+ "init_signals",
212
+ "automation_detected",
213
+ "cross_context_mismatch",
214
+ "headless_probe"
140
215
  ]);
141
216
  class CallbackManager {
142
217
  constructor() {
@@ -256,11 +331,16 @@ class Transmitter {
256
331
  __publicField(this, "encryptionRequired", false);
257
332
  __publicField(this, "currentNonce", "");
258
333
  __publicField(this, "buffer", []);
259
- __publicField(this, "batchSize", 10);
260
- __publicField(this, "flushInterval", 1e3);
334
+ __publicField(this, "batchSize", 20);
335
+ __publicField(this, "flushInterval", 5e3);
261
336
  __publicField(this, "intervalId");
262
337
  __publicField(this, "responseHandler", null);
263
338
  __publicField(this, "callbackManager", null);
339
+ __publicField(this, "flushing", false);
340
+ __publicField(this, "pendingFlush", false);
341
+ __publicField(this, "backoffMs", 0);
342
+ __publicField(this, "maxBackoffMs", 6e4);
343
+ __publicField(this, "_lastBackoffStart", 0);
264
344
  this.startFlushInterval();
265
345
  }
266
346
  setConfig(config) {
@@ -269,6 +349,7 @@ class Transmitter {
269
349
  this.clientKey = config.clientKey;
270
350
  this.encryptionRequired = ((_a = config.serverConfig) == null ? void 0 : _a.encryptionRequired) ?? false;
271
351
  this.currentNonce = ((_b = config.serverConfig) == null ? void 0 : _b.initialNonce) ?? "";
352
+ CryptoUtils.setEndpoint(config.endpoint);
272
353
  }
273
354
  setResponseHandler(handler) {
274
355
  this.responseHandler = handler;
@@ -293,6 +374,17 @@ class Transmitter {
293
374
  }
294
375
  async flush() {
295
376
  if (this.buffer.length === 0 || !this.endpoint) return;
377
+ if (this.backoffMs > 0) {
378
+ const now = Date.now();
379
+ if (!this._lastBackoffStart) this._lastBackoffStart = now;
380
+ if (now - this._lastBackoffStart < this.backoffMs) return;
381
+ this._lastBackoffStart = 0;
382
+ }
383
+ if (this.flushing) {
384
+ this.pendingFlush = true;
385
+ return;
386
+ }
387
+ this.flushing = true;
296
388
  const rawPayload = this.buffer;
297
389
  this.buffer = [];
298
390
  try {
@@ -311,12 +403,20 @@ class Transmitter {
311
403
  requestHeaders["x-request-nonce"] = this.currentNonce;
312
404
  this.currentNonce = "";
313
405
  }
314
- fetch(this.endpoint, {
315
- method: "POST",
316
- body: payload,
317
- headers: requestHeaders,
318
- keepalive: true
319
- }).then(async (res) => {
406
+ try {
407
+ const res = await fetch(this.endpoint, {
408
+ method: "POST",
409
+ body: payload,
410
+ headers: requestHeaders,
411
+ keepalive: true
412
+ });
413
+ if (res.status === 429) {
414
+ this.buffer = rawPayload.concat(this.buffer).slice(0, 500);
415
+ this.backoffMs = this.backoffMs === 0 ? 5e3 : Math.min(this.backoffMs * 2, this.maxBackoffMs);
416
+ this._lastBackoffStart = Date.now();
417
+ return;
418
+ }
419
+ this.backoffMs = 0;
320
420
  if (res.ok && this.callbackManager) {
321
421
  this.callbackManager.emit("allow", { action: "allow" });
322
422
  }
@@ -336,14 +436,14 @@ class Transmitter {
336
436
  } catch (e) {
337
437
  }
338
438
  }
339
- }).catch((err) => {
439
+ } catch (err) {
340
440
  if (this.callbackManager) {
341
441
  this.callbackManager.emit("error", {
342
442
  code: "NETWORK_ERROR",
343
443
  message: "Failed to send security events"
344
444
  });
345
445
  }
346
- });
446
+ }
347
447
  } catch (e) {
348
448
  if (this.callbackManager) {
349
449
  this.callbackManager.emit("error", {
@@ -351,6 +451,12 @@ class Transmitter {
351
451
  message: "Failed to prepare security payload"
352
452
  });
353
453
  }
454
+ } finally {
455
+ this.flushing = false;
456
+ if (this.pendingFlush) {
457
+ this.pendingFlush = false;
458
+ this.flush();
459
+ }
354
460
  }
355
461
  }
356
462
  stop() {
@@ -366,149 +472,19 @@ class Transmitter {
366
472
  }, this.flushInterval);
367
473
  }
368
474
  }
369
- class EventCollector {
370
- constructor(transmitter) {
371
- __publicField(this, "transmitter");
372
- __publicField(this, "listeners", {});
373
- this.transmitter = transmitter;
374
- }
375
- start() {
376
- this.attachListeners();
377
- }
378
- stop() {
379
- this.detachListeners();
380
- }
381
- attachListeners() {
382
- this.listeners["mousemove"] = this.throttle((e) => {
383
- const mouseEvent = e;
384
- this.transmitter.send("mousemove", {
385
- x: mouseEvent.clientX,
386
- y: mouseEvent.clientY,
387
- timestamp: Date.now()
388
- });
389
- }, 100);
390
- this.listeners["click"] = (e) => {
391
- const mouseEvent = e;
392
- this.transmitter.send("click", {
393
- x: mouseEvent.clientX,
394
- y: mouseEvent.clientY,
395
- target: mouseEvent.target.tagName,
396
- timestamp: Date.now()
397
- });
398
- };
399
- this.listeners["keydown"] = (e) => {
400
- const keyEvent = e;
401
- this.transmitter.send("keydown", {
402
- key: keyEvent.key,
403
- timestamp: Date.now()
404
- });
405
- };
406
- window.addEventListener("mousemove", this.listeners["mousemove"]);
407
- window.addEventListener("click", this.listeners["click"]);
408
- window.addEventListener("keydown", this.listeners["keydown"]);
409
- }
410
- detachListeners() {
411
- if (this.listeners["mousemove"]) {
412
- window.removeEventListener("mousemove", this.listeners["mousemove"]);
413
- }
414
- if (this.listeners["click"]) {
415
- window.removeEventListener("click", this.listeners["click"]);
416
- }
417
- if (this.listeners["keydown"]) {
418
- window.removeEventListener("keydown", this.listeners["keydown"]);
419
- }
420
- }
421
- throttle(func, limit) {
422
- let inThrottle;
423
- return function(...args) {
424
- if (!inThrottle) {
425
- func.apply(this, args);
426
- inThrottle = true;
427
- setTimeout(() => inThrottle = false, limit);
428
- }
429
- };
430
- }
431
- }
432
475
  class BehaviorCollector {
433
476
  constructor(transmitter) {
477
+ // @ts-ignore kept for future extension (mouse/keyboard-외 행동 수집)
434
478
  __publicField(this, "transmitter");
435
- __publicField(this, "trajectoryBuffer", []);
436
- __publicField(this, "keystrokeBuffer", []);
437
479
  __publicField(this, "isCollecting", false);
438
- __publicField(this, "flushInterval", 3e3);
439
- // Send batch every 3 seconds
440
- __publicField(this, "flushTimer");
441
- __publicField(this, "lastMouseMoveTime", 0);
442
- __publicField(this, "onMouseMove", (e) => {
443
- const now = Date.now();
444
- if (now - this.lastMouseMoveTime > 50) {
445
- this.trajectoryBuffer.push({
446
- x: e.clientX,
447
- y: e.clientY,
448
- t: now
449
- });
450
- this.lastMouseMoveTime = now;
451
- }
452
- });
453
- __publicField(this, "onKeyDown", (e) => {
454
- this.keystrokeBuffer.push({
455
- code: e.code,
456
- t: Date.now(),
457
- type: "down"
458
- });
459
- });
460
- __publicField(this, "onKeyUp", (e) => {
461
- this.keystrokeBuffer.push({
462
- code: e.code,
463
- t: Date.now(),
464
- type: "up"
465
- });
466
- });
467
480
  this.transmitter = transmitter;
468
481
  }
469
482
  start() {
470
483
  if (this.isCollecting) return;
471
484
  this.isCollecting = true;
472
- this.attachListeners();
473
- this.startFlushTimer();
474
485
  }
475
486
  stop() {
476
487
  this.isCollecting = false;
477
- this.detachListeners();
478
- this.stopFlushTimer();
479
- this.flush();
480
- }
481
- attachListeners() {
482
- window.addEventListener("mousemove", this.onMouseMove);
483
- window.addEventListener("keydown", this.onKeyDown);
484
- window.addEventListener("keyup", this.onKeyUp);
485
- }
486
- detachListeners() {
487
- window.removeEventListener("mousemove", this.onMouseMove);
488
- window.removeEventListener("keydown", this.onKeyDown);
489
- window.removeEventListener("keyup", this.onKeyUp);
490
- }
491
- startFlushTimer() {
492
- this.flushTimer = setInterval(() => {
493
- this.flush();
494
- }, this.flushInterval);
495
- }
496
- stopFlushTimer() {
497
- if (this.flushTimer) {
498
- clearInterval(this.flushTimer);
499
- this.flushTimer = null;
500
- }
501
- }
502
- flush() {
503
- if (this.trajectoryBuffer.length === 0 && this.keystrokeBuffer.length === 0) return;
504
- const data = {
505
- trajectory: [...this.trajectoryBuffer],
506
- keystrokes: [...this.keystrokeBuffer],
507
- url: window.location.href
508
- };
509
- this.trajectoryBuffer = [];
510
- this.keystrokeBuffer = [];
511
- this.transmitter.send("behavior_data", data);
512
488
  }
513
489
  }
514
490
  class RapidClickDetector {
@@ -603,10 +579,14 @@ class MouseTracker {
603
579
  __publicField(this, "transmitter");
604
580
  __publicField(this, "wasmService");
605
581
  __publicField(this, "buffer", []);
606
- __publicField(this, "BUFFER_SIZE", 10);
607
- __publicField(this, "SAMPLE_INTERVAL", 50);
608
- // ms
582
+ __publicField(this, "BUFFER_SIZE", 50);
583
+ // 50 points for accurate entropy (was 10)
584
+ __publicField(this, "SAMPLE_INTERVAL", 100);
585
+ // 100ms between samples (was 50)
586
+ __publicField(this, "SEND_COOLDOWN", 5e3);
587
+ // 5s between server transmissions
609
588
  __publicField(this, "lastSampleTime", 0);
589
+ __publicField(this, "lastSendTime", 0);
610
590
  __publicField(this, "handleMouseMove", (e) => {
611
591
  const now = Date.now();
612
592
  if (now - this.lastSampleTime < this.SAMPLE_INTERVAL) return;
@@ -614,7 +594,10 @@ class MouseTracker {
614
594
  this.buffer.push({ x: e.clientX, y: e.clientY, time: now });
615
595
  if (this.buffer.length > this.BUFFER_SIZE) {
616
596
  this.buffer.shift();
597
+ }
598
+ if (this.buffer.length >= this.BUFFER_SIZE && now - this.lastSendTime >= this.SEND_COOLDOWN) {
617
599
  this.analyze();
600
+ this.lastSendTime = now;
618
601
  }
619
602
  });
620
603
  this.transmitter = transmitter;
@@ -631,15 +614,17 @@ class MouseTracker {
631
614
  }
632
615
  }
633
616
  analyze() {
634
- if (this.buffer.length < 5) return;
617
+ if (this.buffer.length < 20) return;
635
618
  const entropy = this.wasmService.calculateEntropy(this.buffer);
636
619
  const speedVariance = this.calculateSpeedVariance();
620
+ const rawTrajectory = this.buffer.map((p) => ({ x: p.x, y: p.y, t: p.time }));
637
621
  this.transmitter.send("behavior_metrics", {
638
622
  entropy,
639
623
  speedVariance,
640
- timestamp: Date.now()
624
+ sampleSize: this.buffer.length,
625
+ timestamp: Date.now(),
626
+ rawTrajectory
641
627
  });
642
- this.buffer = [];
643
628
  }
644
629
  // Speed Variance is simple enough to keep in JS or move to Wasm later.
645
630
  calculateSpeedVariance() {
@@ -660,8 +645,6 @@ class MouseTracker {
660
645
  }
661
646
  }
662
647
  class InputTracker {
663
- // Buffer for raw input events - currently unused but kept for potential future raw data analysis
664
- // private inputBuffer: any[] = [];
665
648
  constructor(transmitter) {
666
649
  __publicField(this, "lastInteractionTime", 0);
667
650
  __publicField(this, "lastInteractionType", "none");
@@ -676,6 +659,8 @@ class InputTracker {
676
659
  // Suspicious focus tracking
677
660
  __publicField(this, "suspiciousFocusCount", 0);
678
661
  __publicField(this, "suspiciousFocusResetTimer", null);
662
+ // Raw keystroke buffer for ML training
663
+ __publicField(this, "rawKeystrokes", []);
679
664
  this.transmitter = transmitter;
680
665
  this.isMobile = this.detectMobile();
681
666
  }
@@ -700,6 +685,7 @@ class InputTracker {
700
685
  this.checkFocusIntegrity();
701
686
  this.flightTimes = [];
702
687
  this.dwellTimes = [];
688
+ this.rawKeystrokes = [];
703
689
  this.lastKeyUpTime = 0;
704
690
  }
705
691
  }, { capture: true });
@@ -708,6 +694,7 @@ class InputTracker {
708
694
  if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") return;
709
695
  const now = Date.now();
710
696
  this.keyPressTimes.set(e.code, now);
697
+ this.rawKeystrokes.push({ code: e.code, t: now, type: "down" });
711
698
  if (this.lastKeyUpTime > 0) {
712
699
  const flight = now - this.lastKeyUpTime;
713
700
  if (flight < 2e3) {
@@ -720,6 +707,7 @@ class InputTracker {
720
707
  if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") return;
721
708
  const now = Date.now();
722
709
  this.lastKeyUpTime = now;
710
+ this.rawKeystrokes.push({ code: e.code, t: now, type: "up" });
723
711
  const startTime = this.keyPressTimes.get(e.code);
724
712
  if (startTime) {
725
713
  const dwell = now - startTime;
@@ -740,7 +728,7 @@ class InputTracker {
740
728
  checkFocusIntegrity() {
741
729
  const now = Date.now();
742
730
  const timeDiff = now - this.lastInteractionTime;
743
- if (performance.now() < 2e3) return;
731
+ if (performance.now() < 5e3) return;
744
732
  if (timeDiff > 200) {
745
733
  this.suspiciousFocusCount++;
746
734
  clearTimeout(this.suspiciousFocusResetTimer);
@@ -754,15 +742,6 @@ class InputTracker {
754
742
  consecutiveCount: this.suspiciousFocusCount,
755
743
  timestamp: now
756
744
  });
757
- if (this.suspiciousFocusCount >= 3) {
758
- this.transmitter.send("integrity_violation", {
759
- type: "programmatic_focus",
760
- details: `${this.suspiciousFocusCount} consecutive input focuses without click/tab`,
761
- consecutiveCount: this.suspiciousFocusCount,
762
- timestamp: now
763
- });
764
- this.suspiciousFocusCount = 0;
765
- }
766
745
  } else {
767
746
  this.suspiciousFocusCount = 0;
768
747
  }
@@ -784,10 +763,13 @@ class InputTracker {
784
763
  variance: varDwell
785
764
  },
786
765
  isMobile: this.isMobile,
787
- timestamp: Date.now()
766
+ timestamp: Date.now(),
767
+ rawKeystrokes: [...this.rawKeystrokes]
768
+ // ML training data (~10-20 entries, ~800B)
788
769
  });
789
770
  this.flightTimes = [];
790
771
  this.dwellTimes = [];
772
+ this.rawKeystrokes = [];
791
773
  }
792
774
  average(data) {
793
775
  if (data.length === 0) return 0;
@@ -862,22 +844,32 @@ class WasmService {
862
844
  }
863
845
  }
864
846
  class CanvasFingerprinter {
865
- constructor(transmitter, wasmService) {
847
+ // wasmService kept in constructor signature for backward compatibility
848
+ // but no longer used — fingerprint hashing upgraded to SHA-256 via Web Crypto
849
+ constructor(transmitter, _wasmService) {
866
850
  __publicField(this, "transmitter");
867
- __publicField(this, "wasmService");
868
851
  __publicField(this, "hasRun", false);
869
852
  this.transmitter = transmitter;
870
- this.wasmService = wasmService;
871
853
  }
872
854
  start() {
873
855
  if (this.hasRun || typeof document === "undefined") return;
874
- setTimeout(() => {
875
- const fingerprint = this.generateFingerprint();
876
- this.transmitter.send("fingerprint_collected", { hash: fingerprint });
856
+ setTimeout(async () => {
857
+ const canvasHash = await this.generateFingerprint();
858
+ const webglFp = this.collectWebGLFingerprint();
859
+ const audioFp = this.collectAudioFingerprint();
860
+ this.transmitter.send("fingerprint_collected", {
861
+ hash: canvasHash,
862
+ webgl: webglFp,
863
+ audio: audioFp
864
+ });
877
865
  this.hasRun = true;
878
866
  }, 500);
879
867
  }
880
- generateFingerprint() {
868
+ /**
869
+ * Generate canvas fingerprint using SHA-256 (upgraded from 32-bit FNV hash).
870
+ * SHA-256 eliminates collision risk at scale (2^128 vs ~77K for 32-bit).
871
+ */
872
+ async generateFingerprint() {
881
873
  try {
882
874
  const canvas = document.createElement("canvas");
883
875
  const ctx = canvas.getContext("2d");
@@ -908,18 +900,75 @@ class CanvasFingerprinter {
908
900
  ctx.closePath();
909
901
  ctx.fill();
910
902
  const dataUrl = canvas.toDataURL();
911
- const hash = this.wasmService.simpleHash(dataUrl);
912
- return Math.abs(hash).toString(16);
903
+ const hash = await this.sha256(dataUrl);
904
+ return hash;
913
905
  } catch (e) {
914
906
  return "error";
915
907
  }
916
908
  }
909
+ /**
910
+ * Compute SHA-256 hex digest using Web Crypto API.
911
+ */
912
+ async sha256(data) {
913
+ const encoded = new TextEncoder().encode(data);
914
+ const hashBuffer = await window.crypto.subtle.digest("SHA-256", encoded);
915
+ const hashArray = new Uint8Array(hashBuffer);
916
+ return Array.from(hashArray).map((b) => b.toString(16).padStart(2, "0")).join("");
917
+ }
918
+ /**
919
+ * Collect WebGL fingerprint data for cross-signal consistency validation.
920
+ * Includes renderer, vendor, and hardware parameter profile.
921
+ */
922
+ collectWebGLFingerprint() {
923
+ try {
924
+ const canvas = document.createElement("canvas");
925
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
926
+ if (!gl) return { available: false };
927
+ const g = gl;
928
+ const result = { available: true };
929
+ const debugInfo = g.getExtension("WEBGL_debug_renderer_info");
930
+ if (debugInfo) {
931
+ result.renderer = g.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
932
+ result.vendor = g.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
933
+ }
934
+ result.maxTextureSize = g.getParameter(g.MAX_TEXTURE_SIZE);
935
+ result.maxViewportDims = Array.from(g.getParameter(g.MAX_VIEWPORT_DIMS));
936
+ result.maxRenderbufferSize = g.getParameter(g.MAX_RENDERBUFFER_SIZE);
937
+ result.maxVertexAttribs = g.getParameter(g.MAX_VERTEX_ATTRIBS);
938
+ result.extensions = (g.getSupportedExtensions() || []).length;
939
+ return result;
940
+ } catch (e) {
941
+ return { available: false, error: true };
942
+ }
943
+ }
944
+ /**
945
+ * AudioContext fingerprint — the audio stack produces unique output
946
+ * per hardware/OS/browser combination, even for silent oscillators.
947
+ */
948
+ collectAudioFingerprint() {
949
+ try {
950
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
951
+ if (!AudioCtx) return { available: false };
952
+ const ctx = new AudioCtx();
953
+ const result = {
954
+ available: true,
955
+ sampleRate: ctx.sampleRate,
956
+ state: ctx.state,
957
+ maxChannels: ctx.destination.maxChannelCount
958
+ };
959
+ ctx.close();
960
+ return result;
961
+ } catch (e) {
962
+ return { available: false };
963
+ }
964
+ }
917
965
  }
918
966
  class ChallengeHandler {
919
967
  constructor(endpoint) {
920
968
  __publicField(this, "originalFetch");
921
969
  __publicField(this, "endpoint");
922
970
  __publicField(this, "callbackManager", null);
971
+ __publicField(this, "challengeInProgress", null);
923
972
  this.endpoint = endpoint;
924
973
  this.originalFetch = window.fetch.bind(window);
925
974
  }
@@ -946,7 +995,13 @@ class ChallengeHandler {
946
995
  return this.originalFetch(input, init);
947
996
  }
948
997
  }
949
- const passed = await this.showCaptchaModal();
998
+ if (!this.challengeInProgress) {
999
+ this.challengeInProgress = this.showCaptchaModal().finally(() => {
1000
+ this.challengeInProgress = null;
1001
+ });
1002
+ } else {
1003
+ }
1004
+ const passed = await this.challengeInProgress;
950
1005
  if (passed) {
951
1006
  return this.originalFetch(input, init);
952
1007
  } else {
@@ -1160,10 +1215,1315 @@ class InvisibleInteraction {
1160
1215
  return null;
1161
1216
  }
1162
1217
  }
1218
+ class AutomationDetector {
1219
+ constructor(transmitter) {
1220
+ __publicField(this, "transmitter");
1221
+ __publicField(this, "hasRun", false);
1222
+ this.transmitter = transmitter;
1223
+ }
1224
+ start() {
1225
+ if (this.hasRun || typeof window === "undefined") return;
1226
+ setTimeout(() => this.detect(), 1500);
1227
+ this.hasRun = true;
1228
+ }
1229
+ detect() {
1230
+ var _a, _b;
1231
+ const signals = {};
1232
+ let score = 0;
1233
+ if (navigator.webdriver === true) {
1234
+ signals.webdriver = true;
1235
+ score += 40;
1236
+ }
1237
+ try {
1238
+ const cdpResult = this.detectCDP();
1239
+ if (cdpResult) {
1240
+ signals.cdp = true;
1241
+ score += 50;
1242
+ }
1243
+ } catch (e) {
1244
+ }
1245
+ const globals = this.detectAutomationGlobals();
1246
+ if (globals.length > 0) {
1247
+ signals.automationGlobals = globals;
1248
+ score += 30;
1249
+ }
1250
+ const webglInfo = this.checkWebGL();
1251
+ if (webglInfo.suspicious) {
1252
+ signals.webgl = webglInfo;
1253
+ score += 25;
1254
+ }
1255
+ const inconsistencies = this.checkInconsistencies();
1256
+ if (inconsistencies.length > 0) {
1257
+ signals.inconsistencies = inconsistencies;
1258
+ score += 15;
1259
+ }
1260
+ const headlessSignals = this.checkHeadless();
1261
+ if (headlessSignals.score > 0) {
1262
+ signals.headless = headlessSignals.flags;
1263
+ score += headlessSignals.score;
1264
+ }
1265
+ if (score > 0) {
1266
+ this.transmitter.send("automation_detected", {
1267
+ signals,
1268
+ score,
1269
+ timestamp: Date.now()
1270
+ });
1271
+ }
1272
+ this.transmitter.send("init_signals", {
1273
+ webdriver: !!navigator.webdriver,
1274
+ plugins: ((_a = navigator.plugins) == null ? void 0 : _a.length) ?? 0,
1275
+ languages: ((_b = navigator.languages) == null ? void 0 : _b.length) ?? 0,
1276
+ platform: navigator.platform,
1277
+ hardwareConcurrency: navigator.hardwareConcurrency,
1278
+ deviceMemory: navigator.deviceMemory,
1279
+ maxTouchPoints: navigator.maxTouchPoints,
1280
+ webglRenderer: webglInfo.renderer,
1281
+ webglVendor: webglInfo.vendor,
1282
+ timestamp: Date.now()
1283
+ });
1284
+ }
1285
+ /**
1286
+ * CDP serialization detection.
1287
+ * When CDP's Runtime domain is active (Playwright/Puppeteer),
1288
+ * console.log triggers serialization of objects, invoking getters.
1289
+ * In normal browsers without DevTools open, getters are NOT invoked.
1290
+ */
1291
+ detectCDP() {
1292
+ let detected = false;
1293
+ const orig = console.debug;
1294
+ console.debug = orig;
1295
+ return detected;
1296
+ }
1297
+ /**
1298
+ * Check for automation framework globals injected into window.
1299
+ */
1300
+ detectAutomationGlobals() {
1301
+ const found = [];
1302
+ const checks = [
1303
+ // Selenium
1304
+ ["selenium", () => !!window._selenium],
1305
+ ["selenium_unwrapped", () => !!document.__selenium_unwrapped],
1306
+ ["driver_evaluate", () => !!document.__driver_evaluate],
1307
+ ["webdriver_evaluate", () => !!document.__webdriver_evaluate],
1308
+ ["driver_unwrapped", () => !!document.__driver_unwrapped],
1309
+ ["fxdriver", () => !!document.__fxdriver_evaluate],
1310
+ // Playwright
1311
+ ["playwright", () => !!window.__playwright],
1312
+ ["playwright_binding", () => !!window.__playwright__binding__],
1313
+ ["pwInitScripts", () => !!window.__pwInitScripts],
1314
+ // Puppeteer
1315
+ ["puppeteer", () => !!window.__puppeteer_evaluation_script__],
1316
+ // PhantomJS
1317
+ ["phantom", () => !!window.callPhantom || !!window._phantom],
1318
+ // Nightmare
1319
+ ["nightmare", () => !!window.__nightmare],
1320
+ // Generic CDP
1321
+ ["cdc", () => {
1322
+ return Object.keys(document).some((k) => k.startsWith("cdc_") || k.startsWith("$cdc_"));
1323
+ }]
1324
+ ];
1325
+ for (const [name, check] of checks) {
1326
+ try {
1327
+ if (check()) found.push(name);
1328
+ } catch (e) {
1329
+ }
1330
+ }
1331
+ return found;
1332
+ }
1333
+ /**
1334
+ * Check WebGL renderer for headless indicators.
1335
+ * SwiftShader, Mesa OffScreen, and ANGLE (software) are headless markers.
1336
+ */
1337
+ checkWebGL() {
1338
+ try {
1339
+ const canvas = document.createElement("canvas");
1340
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
1341
+ if (!gl) return { suspicious: false, renderer: "unavailable", vendor: "unavailable" };
1342
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
1343
+ if (!debugInfo) return { suspicious: false, renderer: "no_debug_info", vendor: "no_debug_info" };
1344
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "";
1345
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "";
1346
+ const suspicious = /SwiftShader/i.test(renderer) || /Mesa OffScreen/i.test(renderer) || /llvmpipe/i.test(renderer);
1347
+ return { suspicious, renderer, vendor };
1348
+ } catch (e) {
1349
+ return { suspicious: false, renderer: "error", vendor: "error" };
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Cross-signal consistency checks.
1354
+ * Compares multiple browser properties that should be correlated.
1355
+ */
1356
+ checkInconsistencies() {
1357
+ const issues = [];
1358
+ try {
1359
+ const ua = navigator.userAgent;
1360
+ const platform = navigator.platform;
1361
+ if (/Windows/.test(ua) && /Mac/.test(platform)) {
1362
+ issues.push("ua_platform_mismatch_win_mac");
1363
+ }
1364
+ if (/Macintosh/.test(ua) && /Win/.test(platform)) {
1365
+ issues.push("ua_platform_mismatch_mac_win");
1366
+ }
1367
+ if (/Mobile|Android|iPhone/.test(ua) && navigator.maxTouchPoints === 0) {
1368
+ issues.push("mobile_ua_no_touch");
1369
+ }
1370
+ if (/Chrome/.test(ua) && navigator.plugins && navigator.plugins.length === 0) {
1371
+ issues.push("chrome_no_plugins");
1372
+ }
1373
+ if (!navigator.languages || navigator.languages.length === 0) {
1374
+ issues.push("no_languages");
1375
+ }
1376
+ } catch (e) {
1377
+ }
1378
+ return issues;
1379
+ }
1380
+ /**
1381
+ * Additional headless Chrome detection signals.
1382
+ */
1383
+ checkHeadless() {
1384
+ const flags = [];
1385
+ let score = 0;
1386
+ try {
1387
+ if (/HeadlessChrome/.test(navigator.userAgent)) {
1388
+ flags.push("headless_ua");
1389
+ score += 40;
1390
+ }
1391
+ if (/Chrome/.test(navigator.userAgent) && !window.chrome) {
1392
+ flags.push("chrome_ua_no_chrome_obj");
1393
+ score += 15;
1394
+ }
1395
+ if (window.screen && window.screen.width === 800 && window.screen.height === 600) {
1396
+ flags.push("default_screen_size");
1397
+ score += 10;
1398
+ }
1399
+ if ("Notification" in window) {
1400
+ const perm = window.Notification.permission;
1401
+ if (perm === "denied" && navigator.userAgent.includes("Chrome")) {
1402
+ flags.push("notification_denied_default");
1403
+ score += 5;
1404
+ }
1405
+ }
1406
+ const conn = navigator.connection;
1407
+ if (conn && conn.rtt === 0) {
1408
+ flags.push("zero_rtt");
1409
+ score += 10;
1410
+ }
1411
+ } catch (e) {
1412
+ }
1413
+ return { score, flags };
1414
+ }
1415
+ }
1416
+ class SessionAnalyzer {
1417
+ constructor(transmitter) {
1418
+ __publicField(this, "transmitter");
1419
+ __publicField(this, "sessionStart", 0);
1420
+ __publicField(this, "pageTransitions", []);
1421
+ __publicField(this, "interactionTimestamps", []);
1422
+ __publicField(this, "lastPageUrl", "");
1423
+ __publicField(this, "formStartTimes", /* @__PURE__ */ new Map());
1424
+ // @ts-ignore
1425
+ __publicField(this, "reportInterval", null);
1426
+ this.transmitter = transmitter;
1427
+ }
1428
+ start() {
1429
+ if (typeof window === "undefined") return;
1430
+ this.sessionStart = Date.now();
1431
+ this.lastPageUrl = window.location.href;
1432
+ const events = ["mousedown", "keydown", "touchstart", "scroll"];
1433
+ events.forEach((evt) => {
1434
+ document.addEventListener(evt, () => {
1435
+ this.interactionTimestamps.push(Date.now());
1436
+ if (this.interactionTimestamps.length > 500) {
1437
+ this.interactionTimestamps.shift();
1438
+ }
1439
+ }, { passive: true, capture: true });
1440
+ });
1441
+ this.observeNavigation();
1442
+ this.observeForms();
1443
+ this.reportInterval = setInterval(() => this.report(), 15e3);
1444
+ }
1445
+ observeNavigation() {
1446
+ const origPushState = history.pushState;
1447
+ const origReplaceState = history.replaceState;
1448
+ const self = this;
1449
+ history.pushState = function(data, unused, url) {
1450
+ origPushState.call(this, data, unused, url);
1451
+ self.onPageTransition();
1452
+ };
1453
+ history.replaceState = function(data, unused, url) {
1454
+ origReplaceState.call(this, data, unused, url);
1455
+ self.onPageTransition();
1456
+ };
1457
+ window.addEventListener("popstate", () => this.onPageTransition());
1458
+ }
1459
+ onPageTransition() {
1460
+ const now = Date.now();
1461
+ const newUrl = window.location.href;
1462
+ if (newUrl !== this.lastPageUrl) {
1463
+ this.pageTransitions.push(now);
1464
+ this.lastPageUrl = newUrl;
1465
+ }
1466
+ }
1467
+ observeForms() {
1468
+ document.addEventListener("focus", (e) => {
1469
+ const target = e.target;
1470
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") {
1471
+ const form = target.closest("form");
1472
+ const formId = (form == null ? void 0 : form.id) || (form == null ? void 0 : form.action) || "anonymous_form";
1473
+ if (!this.formStartTimes.has(formId)) {
1474
+ this.formStartTimes.set(formId, Date.now());
1475
+ }
1476
+ }
1477
+ }, { capture: true });
1478
+ document.addEventListener("submit", (e) => {
1479
+ const form = e.target;
1480
+ const formId = form.id || form.action || "anonymous_form";
1481
+ const startTime = this.formStartTimes.get(formId);
1482
+ if (startTime) {
1483
+ const duration = Date.now() - startTime;
1484
+ this.transmitter.send("session_metrics", {
1485
+ type: "form_completion",
1486
+ formId,
1487
+ duration,
1488
+ timestamp: Date.now()
1489
+ });
1490
+ this.formStartTimes.delete(formId);
1491
+ }
1492
+ }, { capture: true });
1493
+ }
1494
+ report() {
1495
+ const now = Date.now();
1496
+ const sessionDuration = now - this.sessionStart;
1497
+ if (this.interactionTimestamps.length < 10) return;
1498
+ const intervals = this.calculateIntervals(this.interactionTimestamps);
1499
+ const timingMetrics = this.analyzeTimingDistribution(intervals);
1500
+ const pageMetrics = this.analyzePageTransitions();
1501
+ this.transmitter.send("session_metrics", {
1502
+ type: "session_analysis",
1503
+ sessionDuration,
1504
+ totalInteractions: this.interactionTimestamps.length,
1505
+ pageTransitions: this.pageTransitions.length,
1506
+ timing: timingMetrics,
1507
+ pages: pageMetrics,
1508
+ timestamp: now
1509
+ });
1510
+ }
1511
+ calculateIntervals(timestamps) {
1512
+ const intervals = [];
1513
+ for (let i = 1; i < timestamps.length; i++) {
1514
+ intervals.push(timestamps[i] - timestamps[i - 1]);
1515
+ }
1516
+ return intervals;
1517
+ }
1518
+ analyzeTimingDistribution(intervals) {
1519
+ if (intervals.length < 5) return { samples: intervals.length };
1520
+ const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
1521
+ const variance = intervals.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / intervals.length;
1522
+ const stdDev = Math.sqrt(variance);
1523
+ const cv = mean > 0 ? stdDev / mean : 0;
1524
+ const entropy = this.shannonEntropy(intervals, 50);
1525
+ const autocorr = this.autocorrelation(intervals);
1526
+ return {
1527
+ mean: Math.round(mean),
1528
+ stdDev: Math.round(stdDev),
1529
+ cv: parseFloat(cv.toFixed(4)),
1530
+ entropy: parseFloat(entropy.toFixed(4)),
1531
+ autocorrelation: parseFloat(autocorr.toFixed(4)),
1532
+ samples: intervals.length
1533
+ };
1534
+ }
1535
+ analyzePageTransitions() {
1536
+ if (this.pageTransitions.length < 2) {
1537
+ return { count: this.pageTransitions.length };
1538
+ }
1539
+ const intervals = this.calculateIntervals(this.pageTransitions);
1540
+ const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
1541
+ const variance = intervals.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / intervals.length;
1542
+ const cv = mean > 0 ? Math.sqrt(variance) / mean : 0;
1543
+ return {
1544
+ count: this.pageTransitions.length,
1545
+ avgInterval: Math.round(mean),
1546
+ cv: parseFloat(cv.toFixed(4))
1547
+ };
1548
+ }
1549
+ /**
1550
+ * Shannon entropy of intervals binned into buckets.
1551
+ * Low entropy = predictable/regular timing = bot-like.
1552
+ */
1553
+ shannonEntropy(values, binSize) {
1554
+ const bins = /* @__PURE__ */ new Map();
1555
+ for (const v of values) {
1556
+ const bin = Math.floor(v / binSize);
1557
+ bins.set(bin, (bins.get(bin) || 0) + 1);
1558
+ }
1559
+ const total = values.length;
1560
+ let entropy = 0;
1561
+ for (const count of bins.values()) {
1562
+ const p = count / total;
1563
+ if (p > 0) entropy -= p * Math.log2(p);
1564
+ }
1565
+ return entropy;
1566
+ }
1567
+ /**
1568
+ * Autocorrelation at lag-1.
1569
+ * Human timing: moderate positive autocorrelation (motor control inertia).
1570
+ * Random delays: near-zero autocorrelation.
1571
+ */
1572
+ autocorrelation(values) {
1573
+ if (values.length < 3) return 0;
1574
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
1575
+ let numerator = 0;
1576
+ let denominator = 0;
1577
+ for (let i = 0; i < values.length - 1; i++) {
1578
+ numerator += (values[i] - mean) * (values[i + 1] - mean);
1579
+ }
1580
+ for (let i = 0; i < values.length; i++) {
1581
+ denominator += Math.pow(values[i] - mean, 2);
1582
+ }
1583
+ return denominator === 0 ? 0 : numerator / denominator;
1584
+ }
1585
+ }
1586
+ class TabNavigationAnalyzer {
1587
+ // Also report when 20+ Tabs accumulated
1588
+ constructor(transmitter) {
1589
+ __publicField(this, "transmitter");
1590
+ // Tab event tracking
1591
+ __publicField(this, "tabTimestamps", []);
1592
+ __publicField(this, "shiftTabCount", 0);
1593
+ __publicField(this, "tabCount", 0);
1594
+ __publicField(this, "isShiftDown", false);
1595
+ // Burst detection
1596
+ __publicField(this, "BURST_THRESHOLD_MS", 80);
1597
+ // Consecutive Tabs under 80ms = burst
1598
+ // Reporting
1599
+ __publicField(this, "reportInterval", null);
1600
+ __publicField(this, "REPORT_INTERVAL_MS", 1e4);
1601
+ // Report every 10 seconds
1602
+ __publicField(this, "MIN_TABS_TO_REPORT", 5);
1603
+ // Need at least 5 Tabs to analyze
1604
+ __publicField(this, "TAB_TRIGGER_COUNT", 20);
1605
+ __publicField(this, "onKeyDown", (e) => {
1606
+ if (e.key === "Shift") {
1607
+ this.isShiftDown = true;
1608
+ return;
1609
+ }
1610
+ if (e.key === "Tab") {
1611
+ const now = Date.now();
1612
+ if (this.isShiftDown) {
1613
+ this.shiftTabCount++;
1614
+ } else {
1615
+ this.tabCount++;
1616
+ this.tabTimestamps.push(now);
1617
+ if (this.tabTimestamps.length > 100) {
1618
+ this.tabTimestamps.shift();
1619
+ }
1620
+ }
1621
+ if (this.tabCount + this.shiftTabCount >= this.TAB_TRIGGER_COUNT) {
1622
+ this.report();
1623
+ }
1624
+ }
1625
+ });
1626
+ __publicField(this, "onKeyUp", (e) => {
1627
+ if (e.key === "Shift") {
1628
+ this.isShiftDown = false;
1629
+ }
1630
+ });
1631
+ this.transmitter = transmitter;
1632
+ }
1633
+ start() {
1634
+ if (typeof document === "undefined") return;
1635
+ document.addEventListener("keydown", this.onKeyDown, { passive: true, capture: true });
1636
+ document.addEventListener("keyup", this.onKeyUp, { passive: true, capture: true });
1637
+ this.reportInterval = setInterval(() => this.report(), this.REPORT_INTERVAL_MS);
1638
+ }
1639
+ stop() {
1640
+ if (typeof document === "undefined") return;
1641
+ document.removeEventListener("keydown", this.onKeyDown, { capture: true });
1642
+ document.removeEventListener("keyup", this.onKeyUp, { capture: true });
1643
+ if (this.reportInterval) {
1644
+ clearInterval(this.reportInterval);
1645
+ this.reportInterval = null;
1646
+ }
1647
+ this.report();
1648
+ }
1649
+ report() {
1650
+ const totalTabs = this.tabCount + this.shiftTabCount;
1651
+ if (totalTabs < this.MIN_TABS_TO_REPORT) return;
1652
+ const intervals = this.calculateIntervals(this.tabTimestamps);
1653
+ if (intervals.length < 3) {
1654
+ this.resetBuffers();
1655
+ return;
1656
+ }
1657
+ const intervalStats = this.analyzeIntervals(intervals);
1658
+ const burstStats = this.analyzeBursts(intervals);
1659
+ const shiftTabRatio = totalTabs > 0 ? this.shiftTabCount / totalTabs : 0;
1660
+ this.transmitter.send("tab_navigation_metrics", {
1661
+ tabCount: this.tabCount,
1662
+ shiftTabCount: this.shiftTabCount,
1663
+ shiftTabRatio: parseFloat(shiftTabRatio.toFixed(4)),
1664
+ intervals: intervalStats,
1665
+ bursts: burstStats,
1666
+ timestamp: Date.now()
1667
+ });
1668
+ this.resetBuffers();
1669
+ }
1670
+ resetBuffers() {
1671
+ this.tabTimestamps = [];
1672
+ this.tabCount = 0;
1673
+ this.shiftTabCount = 0;
1674
+ }
1675
+ calculateIntervals(timestamps) {
1676
+ const intervals = [];
1677
+ for (let i = 1; i < timestamps.length; i++) {
1678
+ const interval = timestamps[i] - timestamps[i - 1];
1679
+ if (interval < 5e3) {
1680
+ intervals.push(interval);
1681
+ }
1682
+ }
1683
+ return intervals;
1684
+ }
1685
+ analyzeIntervals(intervals) {
1686
+ if (intervals.length === 0) {
1687
+ return { avg: 0, variance: 0, cv: 0, min: 0, samples: 0 };
1688
+ }
1689
+ const avg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
1690
+ const variance = intervals.reduce((a, b) => a + Math.pow(b - avg, 2), 0) / intervals.length;
1691
+ const stdDev = Math.sqrt(variance);
1692
+ const cv = avg > 0 ? stdDev / avg : 0;
1693
+ const min = Math.min(...intervals);
1694
+ return {
1695
+ avg: parseFloat(avg.toFixed(2)),
1696
+ variance: parseFloat(variance.toFixed(2)),
1697
+ cv: parseFloat(cv.toFixed(4)),
1698
+ min,
1699
+ samples: intervals.length
1700
+ };
1701
+ }
1702
+ analyzeBursts(intervals) {
1703
+ let burstCount = 0;
1704
+ let currentBurstLength = 0;
1705
+ let totalBurstLength = 0;
1706
+ let totalBurstInterval = 0;
1707
+ let burstIntervalCount = 0;
1708
+ for (const interval of intervals) {
1709
+ if (interval < this.BURST_THRESHOLD_MS) {
1710
+ currentBurstLength++;
1711
+ totalBurstInterval += interval;
1712
+ burstIntervalCount++;
1713
+ } else {
1714
+ if (currentBurstLength >= 3) {
1715
+ burstCount++;
1716
+ totalBurstLength += currentBurstLength;
1717
+ }
1718
+ currentBurstLength = 0;
1719
+ }
1720
+ }
1721
+ if (currentBurstLength >= 3) {
1722
+ burstCount++;
1723
+ totalBurstLength += currentBurstLength;
1724
+ }
1725
+ return {
1726
+ count: burstCount,
1727
+ avgLength: burstCount > 0 ? parseFloat((totalBurstLength / burstCount).toFixed(1)) : 0,
1728
+ avgInterval: burstIntervalCount > 0 ? parseFloat((totalBurstInterval / burstIntervalCount).toFixed(2)) : 0
1729
+ };
1730
+ }
1731
+ }
1732
+ class CloudEnvironmentDetector {
1733
+ constructor(transmitter) {
1734
+ __publicField(this, "transmitter");
1735
+ __publicField(this, "hasRun", false);
1736
+ this.transmitter = transmitter;
1737
+ }
1738
+ start() {
1739
+ if (this.hasRun || typeof window === "undefined") return;
1740
+ this.hasRun = true;
1741
+ setTimeout(() => this.detect(), 2e3);
1742
+ }
1743
+ async detect() {
1744
+ try {
1745
+ const gpu = this.analyzeGPU();
1746
+ const hardware = this.analyzeHardware();
1747
+ const screen2 = this.analyzeScreen();
1748
+ const media = await this.analyzeMedia();
1749
+ const performance2 = this.benchmarkCanvas();
1750
+ let vmIndicatorCount = 0;
1751
+ if (gpu.category === "virtual_gpu" || gpu.category === "software_render" || gpu.category === "cloud_gpu") {
1752
+ vmIndicatorCount += 2;
1753
+ }
1754
+ if (hardware.cores <= 2) vmIndicatorCount++;
1755
+ if (hardware.memory !== null && hardware.memory <= 2) vmIndicatorCount++;
1756
+ if (hardware.colorDepth === 24) vmIndicatorCount++;
1757
+ if (hardware.pixelRatio === 1) vmIndicatorCount++;
1758
+ if (screen2.noTaskbar) vmIndicatorCount++;
1759
+ if (screen2.isVMResolution) vmIndicatorCount++;
1760
+ if (media.cameras === 0 && media.microphones === 0) vmIndicatorCount++;
1761
+ if (!media.hasBluetooth && !media.hasUSB) vmIndicatorCount++;
1762
+ if (performance2.canvasRenderTime > 50) vmIndicatorCount++;
1763
+ const { classification, confidence } = this.classify(gpu, hardware, vmIndicatorCount);
1764
+ this.transmitter.send("cloud_environment", {
1765
+ classification,
1766
+ confidence,
1767
+ signals: {
1768
+ gpu: { renderer: gpu.renderer, vendor: gpu.vendor, category: gpu.category },
1769
+ hardware,
1770
+ screen: screen2,
1771
+ media,
1772
+ performance: performance2
1773
+ },
1774
+ vmIndicatorCount,
1775
+ timestamp: Date.now()
1776
+ });
1777
+ } catch (e) {
1778
+ }
1779
+ }
1780
+ /**
1781
+ * Analyze WebGL renderer to identify virtual GPUs.
1782
+ */
1783
+ analyzeGPU() {
1784
+ try {
1785
+ const canvas = document.createElement("canvas");
1786
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
1787
+ if (!gl) return { renderer: "unavailable", vendor: "unavailable", category: "unknown" };
1788
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
1789
+ if (!debugInfo) return { renderer: "no_debug_info", vendor: "no_debug_info", category: "unknown" };
1790
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || "";
1791
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || "";
1792
+ const category = this.categorizeGPU(renderer, vendor);
1793
+ return { renderer, vendor, category };
1794
+ } catch (e) {
1795
+ return { renderer: "error", vendor: "error", category: "unknown" };
1796
+ }
1797
+ }
1798
+ /**
1799
+ * Categorize GPU renderer string into environment type.
1800
+ */
1801
+ categorizeGPU(renderer, vendor) {
1802
+ const r = renderer.toLowerCase();
1803
+ const v = vendor.toLowerCase();
1804
+ if (/swiftshader|llvmpipe|mesa offscreen|softpipe/.test(r)) {
1805
+ return "software_render";
1806
+ }
1807
+ if (/svga3d|svga|vmware/.test(r)) return "virtual_gpu";
1808
+ if (/virtualbox/.test(r)) return "virtual_gpu";
1809
+ if (/parallels/.test(r)) return "virtual_gpu";
1810
+ if (/qxl|virtio|red hat|spice/.test(r)) return "virtual_gpu";
1811
+ if (/hyper-v|microsoft basic render/i.test(r)) return "virtual_gpu";
1812
+ if (/citrix/.test(r)) return "virtual_gpu";
1813
+ if (/amazon|elastic|aws/.test(r) || /amazon|elastic|aws/.test(v)) return "cloud_gpu";
1814
+ if (/google cloud|gce/.test(r) || /google cloud|gce/.test(v)) return "cloud_gpu";
1815
+ if (/azure/.test(r) || /azure/.test(v)) return "cloud_gpu";
1816
+ return "physical";
1817
+ }
1818
+ /**
1819
+ * Collect hardware characteristics.
1820
+ */
1821
+ analyzeHardware() {
1822
+ return {
1823
+ cores: navigator.hardwareConcurrency || 0,
1824
+ memory: navigator.deviceMemory ?? null,
1825
+ colorDepth: screen.colorDepth,
1826
+ pixelRatio: window.devicePixelRatio || 1
1827
+ };
1828
+ }
1829
+ /**
1830
+ * Analyze screen properties for VM indicators.
1831
+ */
1832
+ analyzeScreen() {
1833
+ const width = screen.width;
1834
+ const height = screen.height;
1835
+ const availHeight = screen.availHeight;
1836
+ const noTaskbar = height === availHeight && height === window.innerHeight;
1837
+ const vmResolutions = [
1838
+ [800, 600],
1839
+ [1024, 768],
1840
+ [1280, 1024],
1841
+ [1280, 800],
1842
+ [1920, 1080]
1843
+ // Also common physical, but combined with other signals
1844
+ ];
1845
+ const isVMResolution = vmResolutions.some(
1846
+ ([w, h]) => width === w && height === h && !(w === 1920 && h === 1080)
1847
+ );
1848
+ return { width, height, availHeight, noTaskbar, isVMResolution };
1849
+ }
1850
+ /**
1851
+ * Check media device availability (cameras, microphones, peripherals).
1852
+ */
1853
+ async analyzeMedia() {
1854
+ let cameras = -1;
1855
+ let microphones = -1;
1856
+ try {
1857
+ if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
1858
+ const devices = await navigator.mediaDevices.enumerateDevices();
1859
+ cameras = devices.filter((d) => d.kind === "videoinput").length;
1860
+ microphones = devices.filter((d) => d.kind === "audioinput").length;
1861
+ }
1862
+ } catch (e) {
1863
+ }
1864
+ const hasBluetooth = !!navigator.bluetooth;
1865
+ const hasUSB = !!navigator.usb;
1866
+ let hasBattery = null;
1867
+ try {
1868
+ if (navigator.getBattery) {
1869
+ const battery = await navigator.getBattery();
1870
+ hasBattery = !(battery.charging && battery.level === 1 && battery.chargingTime === 0);
1871
+ }
1872
+ } catch (e) {
1873
+ }
1874
+ return { cameras, microphones, hasBluetooth, hasUSB, hasBattery };
1875
+ }
1876
+ /**
1877
+ * Quick canvas rendering benchmark.
1878
+ * Software rendering (VM without GPU) is significantly slower.
1879
+ */
1880
+ benchmarkCanvas() {
1881
+ try {
1882
+ const canvas = document.createElement("canvas");
1883
+ canvas.width = 200;
1884
+ canvas.height = 200;
1885
+ const ctx = canvas.getContext("2d");
1886
+ if (!ctx) return { canvasRenderTime: -1 };
1887
+ const start = performance.now();
1888
+ for (let i = 0; i < 500; i++) {
1889
+ ctx.fillStyle = `rgb(${i % 256},${i * 3 % 256},${i * 7 % 256})`;
1890
+ ctx.fillRect(i * 13 % 200, i * 7 % 200, 10, 10);
1891
+ }
1892
+ ctx.getImageData(0, 0, 1, 1);
1893
+ const elapsed = performance.now() - start;
1894
+ return { canvasRenderTime: Math.round(elapsed * 100) / 100 };
1895
+ } catch (e) {
1896
+ return { canvasRenderTime: -1 };
1897
+ }
1898
+ }
1899
+ /**
1900
+ * Classify the environment based on collected signals.
1901
+ */
1902
+ classify(gpu, hardware, vmIndicatorCount) {
1903
+ if (gpu.category === "virtual_gpu") {
1904
+ if (hardware.colorDepth === 24) {
1905
+ return { classification: "rdp_likely", confidence: 0.85 };
1906
+ }
1907
+ return { classification: "vm_detected", confidence: 0.95 };
1908
+ }
1909
+ if (gpu.category === "cloud_gpu") {
1910
+ return { classification: "cloud_likely", confidence: 0.9 };
1911
+ }
1912
+ if (gpu.category === "software_render") {
1913
+ return { classification: "vm_detected", confidence: 0.9 };
1914
+ }
1915
+ if (vmIndicatorCount >= 5) {
1916
+ return { classification: "vm_likely", confidence: 0.8 };
1917
+ }
1918
+ if (vmIndicatorCount >= 3) {
1919
+ return { classification: "vm_likely", confidence: 0.6 };
1920
+ }
1921
+ return { classification: "physical", confidence: 1 - vmIndicatorCount * 0.1 };
1922
+ }
1923
+ }
1924
+ class CrossContextDetector {
1925
+ constructor(transmitter) {
1926
+ __publicField(this, "transmitter");
1927
+ __publicField(this, "hasRun", false);
1928
+ this.transmitter = transmitter;
1929
+ }
1930
+ start() {
1931
+ if (this.hasRun || typeof window === "undefined") return;
1932
+ this.hasRun = true;
1933
+ setTimeout(() => this.detect(), 2500);
1934
+ }
1935
+ async detect() {
1936
+ var _a, _b;
1937
+ const signals = {};
1938
+ let score = 0;
1939
+ const main = {
1940
+ webdriver: !!navigator.webdriver,
1941
+ languages: ((_a = navigator.languages) == null ? void 0 : _a.join(",")) || "",
1942
+ platform: navigator.platform || "",
1943
+ hardwareConcurrency: navigator.hardwareConcurrency || 0,
1944
+ userAgent: navigator.userAgent || "",
1945
+ pluginsLength: ((_b = navigator.plugins) == null ? void 0 : _b.length) ?? 0
1946
+ };
1947
+ const iframeResult = this.checkIframe(main);
1948
+ if (iframeResult.score > 0) {
1949
+ signals.iframe = iframeResult.mismatches;
1950
+ score += iframeResult.score;
1951
+ }
1952
+ try {
1953
+ const workerResult = await this.checkWorker(main);
1954
+ if (workerResult.score > 0) {
1955
+ signals.worker = workerResult.mismatches;
1956
+ score += workerResult.score;
1957
+ }
1958
+ } catch (_e) {
1959
+ }
1960
+ if (score > 0) {
1961
+ this.transmitter.send("cross_context_mismatch", {
1962
+ signals,
1963
+ score,
1964
+ timestamp: Date.now()
1965
+ });
1966
+ }
1967
+ }
1968
+ /**
1969
+ * Create a sandboxed iframe and compare navigator values.
1970
+ */
1971
+ checkIframe(main) {
1972
+ var _a, _b;
1973
+ const mismatches = [];
1974
+ let score = 0;
1975
+ try {
1976
+ const iframe = document.createElement("iframe");
1977
+ iframe.style.display = "none";
1978
+ iframe.sandbox = "allow-same-origin";
1979
+ document.body.appendChild(iframe);
1980
+ const iframeWindow = iframe.contentWindow;
1981
+ if (!iframeWindow) {
1982
+ document.body.removeChild(iframe);
1983
+ return { score: 0, mismatches: [] };
1984
+ }
1985
+ const iframeNav = iframeWindow.navigator;
1986
+ if (iframeNav.webdriver !== navigator.webdriver) {
1987
+ mismatches.push("webdriver_mismatch");
1988
+ score += 40;
1989
+ }
1990
+ if (!navigator.webdriver && iframeNav.webdriver === true) {
1991
+ mismatches.push("webdriver_spoofed_in_main");
1992
+ score += 30;
1993
+ }
1994
+ const iframeLangs = ((_a = iframeNav.languages) == null ? void 0 : _a.join(",")) || "";
1995
+ if (iframeLangs !== main.languages) {
1996
+ mismatches.push("languages_mismatch");
1997
+ score += 15;
1998
+ }
1999
+ if (iframeNav.platform !== main.platform) {
2000
+ mismatches.push("platform_mismatch");
2001
+ score += 15;
2002
+ }
2003
+ const iframePlugins = ((_b = iframeNav.plugins) == null ? void 0 : _b.length) ?? 0;
2004
+ if (iframePlugins !== main.pluginsLength) {
2005
+ mismatches.push("plugins_mismatch");
2006
+ score += 10;
2007
+ }
2008
+ document.body.removeChild(iframe);
2009
+ } catch (_e) {
2010
+ }
2011
+ return { score, mismatches };
2012
+ }
2013
+ /**
2014
+ * Spin up a WebWorker and compare navigator values.
2015
+ * Workers have their own navigator which is harder to spoof.
2016
+ */
2017
+ checkWorker(main) {
2018
+ return new Promise((resolve) => {
2019
+ try {
2020
+ const workerCode = `
2021
+ self.onmessage = function() {
2022
+ var nav = self.navigator;
2023
+ self.postMessage({
2024
+ webdriver: !!nav.webdriver,
2025
+ languages: (nav.languages || []).join(','),
2026
+ platform: nav.platform || '',
2027
+ hardwareConcurrency: nav.hardwareConcurrency || 0,
2028
+ userAgent: nav.userAgent || ''
2029
+ });
2030
+ };
2031
+ `;
2032
+ const blob = new Blob([workerCode], { type: "application/javascript" });
2033
+ const url = URL.createObjectURL(blob);
2034
+ const worker = new Worker(url);
2035
+ const timeout = setTimeout(() => {
2036
+ worker.terminate();
2037
+ URL.revokeObjectURL(url);
2038
+ resolve({ score: 0, mismatches: [] });
2039
+ }, 3e3);
2040
+ worker.onmessage = (e) => {
2041
+ clearTimeout(timeout);
2042
+ worker.terminate();
2043
+ URL.revokeObjectURL(url);
2044
+ const w = e.data;
2045
+ const mismatches = [];
2046
+ let score = 0;
2047
+ if (w.webdriver !== main.webdriver) {
2048
+ mismatches.push("worker_webdriver_mismatch");
2049
+ score += 35;
2050
+ }
2051
+ if (w.languages !== main.languages) {
2052
+ mismatches.push("worker_languages_mismatch");
2053
+ score += 15;
2054
+ }
2055
+ if (w.platform !== main.platform) {
2056
+ mismatches.push("worker_platform_mismatch");
2057
+ score += 15;
2058
+ }
2059
+ if (w.hardwareConcurrency !== main.hardwareConcurrency) {
2060
+ mismatches.push("worker_concurrency_mismatch");
2061
+ score += 10;
2062
+ }
2063
+ if (w.userAgent !== main.userAgent) {
2064
+ mismatches.push("worker_ua_mismatch");
2065
+ score += 20;
2066
+ }
2067
+ resolve({ score, mismatches });
2068
+ };
2069
+ worker.onerror = () => {
2070
+ clearTimeout(timeout);
2071
+ worker.terminate();
2072
+ URL.revokeObjectURL(url);
2073
+ resolve({ score: 0, mismatches: [] });
2074
+ };
2075
+ worker.postMessage("check");
2076
+ } catch (_e) {
2077
+ resolve({ score: 0, mismatches: [] });
2078
+ }
2079
+ });
2080
+ }
2081
+ }
2082
+ class HeadlessProbeDetector {
2083
+ constructor(transmitter) {
2084
+ __publicField(this, "transmitter");
2085
+ __publicField(this, "hasRun", false);
2086
+ this.transmitter = transmitter;
2087
+ }
2088
+ start() {
2089
+ if (this.hasRun || typeof window === "undefined") return;
2090
+ this.hasRun = true;
2091
+ setTimeout(() => this.detect(), 2e3);
2092
+ }
2093
+ async detect() {
2094
+ const signals = {};
2095
+ let score = 0;
2096
+ const brokenImg = await this.checkBrokenImage();
2097
+ if (brokenImg.suspicious) {
2098
+ signals.brokenImage = brokenImg;
2099
+ score += 25;
2100
+ }
2101
+ const clientHints = await this.checkClientHints();
2102
+ if (clientHints.suspicious) {
2103
+ signals.clientHints = clientHints;
2104
+ score += 20;
2105
+ }
2106
+ const cdpMouse = this.checkCDPMouseSignature();
2107
+ if (cdpMouse.suspicious) {
2108
+ signals.cdpMouse = cdpMouse;
2109
+ score += 30;
2110
+ }
2111
+ const hairline = this.checkHairlineSupport();
2112
+ if (hairline.suspicious) {
2113
+ signals.hairline = hairline;
2114
+ score += 10;
2115
+ }
2116
+ const extraFrameworks = this.checkExtraFrameworks();
2117
+ if (extraFrameworks.length > 0) {
2118
+ signals.extraFrameworks = extraFrameworks;
2119
+ score += 20;
2120
+ }
2121
+ const workerCDP = await this.checkWorkerCDP();
2122
+ if (workerCDP.suspicious) {
2123
+ signals.workerCDP = true;
2124
+ score += 35;
2125
+ }
2126
+ if (score > 0) {
2127
+ this.transmitter.send("headless_probe", {
2128
+ signals,
2129
+ score,
2130
+ timestamp: Date.now()
2131
+ });
2132
+ }
2133
+ }
2134
+ /**
2135
+ * Broken image dimensions test.
2136
+ * Normal browsers render a placeholder for broken images (typically 16x16 or 24x24).
2137
+ * Headless Chrome returns 0x0.
2138
+ */
2139
+ checkBrokenImage() {
2140
+ return new Promise((resolve) => {
2141
+ try {
2142
+ const img = document.createElement("img");
2143
+ img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==___invalid";
2144
+ const timeout = setTimeout(() => {
2145
+ resolve({ suspicious: false, width: -1, height: -1 });
2146
+ }, 2e3);
2147
+ img.onerror = () => {
2148
+ clearTimeout(timeout);
2149
+ const w = img.width;
2150
+ const h = img.height;
2151
+ resolve({
2152
+ suspicious: w === 0 && h === 0,
2153
+ width: w,
2154
+ height: h
2155
+ });
2156
+ };
2157
+ img.onload = () => {
2158
+ clearTimeout(timeout);
2159
+ resolve({ suspicious: false, width: img.width, height: img.height });
2160
+ };
2161
+ img.style.position = "absolute";
2162
+ img.style.left = "-9999px";
2163
+ document.body.appendChild(img);
2164
+ setTimeout(() => {
2165
+ try {
2166
+ document.body.removeChild(img);
2167
+ } catch (_e) {
2168
+ }
2169
+ }, 100);
2170
+ } catch (_e) {
2171
+ resolve({ suspicious: false, width: -1, height: -1 });
2172
+ }
2173
+ });
2174
+ }
2175
+ /**
2176
+ * Client Hints consistency check.
2177
+ * navigator.userAgentData (if available) should match UA string.
2178
+ * Bots that spoof UA often forget to update Client Hints.
2179
+ */
2180
+ async checkClientHints() {
2181
+ const mismatches = [];
2182
+ try {
2183
+ const uad = navigator.userAgentData;
2184
+ if (!uad) return { suspicious: false, mismatches: [] };
2185
+ const ua = navigator.userAgent;
2186
+ const brands = uad.brands || [];
2187
+ const brandNames = brands.map((b) => b.brand);
2188
+ if (/Chrome\/\d/.test(ua) && !brandNames.some((b) => /Chromium|Google Chrome|Chrome/.test(b))) {
2189
+ mismatches.push("ua_chrome_no_brand");
2190
+ }
2191
+ if (uad.platform) {
2192
+ if (/Windows/.test(ua) && uad.platform !== "Windows") {
2193
+ mismatches.push("ua_windows_brand_mismatch");
2194
+ }
2195
+ if (/Macintosh/.test(ua) && uad.platform !== "macOS") {
2196
+ mismatches.push("ua_mac_brand_mismatch");
2197
+ }
2198
+ if (/Linux/.test(ua) && !/Linux|Android|ChromeOS/.test(uad.platform)) {
2199
+ mismatches.push("ua_linux_brand_mismatch");
2200
+ }
2201
+ }
2202
+ if (/Mobile/.test(ua) && uad.mobile === false) {
2203
+ mismatches.push("ua_mobile_brand_mismatch");
2204
+ }
2205
+ } catch (_e) {
2206
+ }
2207
+ return { suspicious: mismatches.length > 0, mismatches };
2208
+ }
2209
+ /**
2210
+ * CDP mouse event signature detection.
2211
+ * CDP's Input.dispatchMouseEvent creates events where:
2212
+ * - movementX/Y are always 0 (no real mouse delta)
2213
+ * - screenX/Y may not match clientX/Y + window offset
2214
+ * - Multiple rapid mousemove without mousedown have consistent offsets
2215
+ *
2216
+ * We collect a few mouse events and check for CDP patterns.
2217
+ */
2218
+ checkCDPMouseSignature() {
2219
+ let zeroMovementCount = 0;
2220
+ let totalEvents = 0;
2221
+ let suspicious = false;
2222
+ try {
2223
+ const events = [];
2224
+ const handler = (e) => {
2225
+ events.push(e);
2226
+ totalEvents++;
2227
+ if (e.movementX === 0 && e.movementY === 0) {
2228
+ zeroMovementCount++;
2229
+ }
2230
+ };
2231
+ document.addEventListener("mousemove", handler, { passive: true });
2232
+ setTimeout(() => {
2233
+ document.removeEventListener("mousemove", handler);
2234
+ }, 5e3);
2235
+ if (totalEvents > 10 && zeroMovementCount / totalEvents > 0.9) {
2236
+ suspicious = true;
2237
+ }
2238
+ } catch (_e) {
2239
+ }
2240
+ return { suspicious, zeroMovementCount, totalEvents };
2241
+ }
2242
+ /**
2243
+ * Hairline (0.5px border) support test.
2244
+ * HiDPI devices support 0.5px borders. Headless Chrome may not.
2245
+ */
2246
+ checkHairlineSupport() {
2247
+ try {
2248
+ const div = document.createElement("div");
2249
+ div.style.border = ".5px solid transparent";
2250
+ div.style.position = "absolute";
2251
+ div.style.left = "-9999px";
2252
+ document.body.appendChild(div);
2253
+ const height = div.offsetHeight;
2254
+ document.body.removeChild(div);
2255
+ return { suspicious: height === 0, offsetHeight: height };
2256
+ } catch (_e) {
2257
+ return { suspicious: false, offsetHeight: -1 };
2258
+ }
2259
+ }
2260
+ /**
2261
+ * Additional automation framework globals.
2262
+ */
2263
+ checkExtraFrameworks() {
2264
+ const found = [];
2265
+ try {
2266
+ const ext = window.external;
2267
+ if (ext && typeof ext.toString === "function") {
2268
+ try {
2269
+ const extStr = ext.toString();
2270
+ if (/Sequentum/i.test(extStr)) found.push("sequentum");
2271
+ } catch (_e) {
2272
+ }
2273
+ }
2274
+ const webdriverKeys = [
2275
+ "__webdriver_script_fn",
2276
+ "__webdriver_script_func",
2277
+ "__webdriver_evaluate",
2278
+ "__selenium_evaluate",
2279
+ "__fxdriver_evaluate",
2280
+ "__driver_evaluate",
2281
+ "__webdriver_unwrapped",
2282
+ "__selenium_unwrapped",
2283
+ "__fxdriver_unwrapped",
2284
+ "__driver_unwrapped",
2285
+ "_Selenium_IDE_Recorder",
2286
+ "_selenium",
2287
+ "calledSelenium",
2288
+ "_WEBDRIVER_ELEM_CACHE",
2289
+ "ChromeDriverw",
2290
+ "driver-hierarchical",
2291
+ "__$webdriverAsyncExecutor",
2292
+ "__lastWatirAlert",
2293
+ "__lastWatirConfirm",
2294
+ "__lastWatirPrompt",
2295
+ "_WEBDRIVER_ELEM_CACHE",
2296
+ "webdriver",
2297
+ "_phantom",
2298
+ "__nightmare",
2299
+ "_selenium"
2300
+ ];
2301
+ for (const key of webdriverKeys) {
2302
+ if (key in document || key in window) {
2303
+ found.push(`global_${key}`);
2304
+ }
2305
+ }
2306
+ const navKeys = Object.getOwnPropertyNames(navigator);
2307
+ for (const key of navKeys) {
2308
+ if (/webdriver|selenium|puppeteer|playwright|phantom/i.test(key)) {
2309
+ found.push(`nav_${key}`);
2310
+ }
2311
+ }
2312
+ } catch (_e) {
2313
+ }
2314
+ return found;
2315
+ }
2316
+ /**
2317
+ * CDP detection within WebWorker context.
2318
+ * Same getter-trap technique but inside a Worker where it's harder to intercept.
2319
+ */
2320
+ checkWorkerCDP() {
2321
+ return new Promise((resolve) => {
2322
+ try {
2323
+ const workerCode = `
2324
+ self.onmessage = function() {
2325
+ var detected = false;
2326
+ try {
2327
+ var marker = {};
2328
+ Object.defineProperty(marker, 'stack', {
2329
+ get: function() { detected = true; return ''; }
2330
+ });
2331
+ console.debug(marker);
2332
+ } catch(e) {}
2333
+ self.postMessage({ cdp: detected });
2334
+ };
2335
+ `;
2336
+ const blob = new Blob([workerCode], { type: "application/javascript" });
2337
+ const url = URL.createObjectURL(blob);
2338
+ const worker = new Worker(url);
2339
+ const timeout = setTimeout(() => {
2340
+ worker.terminate();
2341
+ URL.revokeObjectURL(url);
2342
+ resolve({ suspicious: false });
2343
+ }, 3e3);
2344
+ worker.onmessage = (e) => {
2345
+ clearTimeout(timeout);
2346
+ worker.terminate();
2347
+ URL.revokeObjectURL(url);
2348
+ resolve({ suspicious: e.data.cdp === true });
2349
+ };
2350
+ worker.onerror = () => {
2351
+ clearTimeout(timeout);
2352
+ worker.terminate();
2353
+ URL.revokeObjectURL(url);
2354
+ resolve({ suspicious: false });
2355
+ };
2356
+ worker.postMessage("check");
2357
+ } catch (_e) {
2358
+ resolve({ suspicious: false });
2359
+ }
2360
+ });
2361
+ }
2362
+ }
2363
+ const COOKIE_NAME = "__aie_token";
2364
+ const RENEWAL_MARGIN_MS = 5 * 60 * 1e3;
2365
+ class CrawlProtect {
2366
+ constructor(config) {
2367
+ __publicField(this, "config");
2368
+ __publicField(this, "renewTimer", null);
2369
+ __publicField(this, "verified", false);
2370
+ this.config = config;
2371
+ }
2372
+ async start() {
2373
+ const existing = this.getToken();
2374
+ if (existing && !this.isExpired(existing)) {
2375
+ this.verified = true;
2376
+ this.scheduleRenewal(existing);
2377
+ this.revealContent();
2378
+ if (this.config.debug) ;
2379
+ return true;
2380
+ }
2381
+ this.hideContent();
2382
+ const success = await this.solveChallenge();
2383
+ if (success) {
2384
+ this.verified = true;
2385
+ this.revealContent();
2386
+ } else {
2387
+ if (this.config.insertHoneypot) {
2388
+ this.insertHoneypotData();
2389
+ }
2390
+ }
2391
+ return success;
2392
+ }
2393
+ isVerified() {
2394
+ return this.verified;
2395
+ }
2396
+ stop() {
2397
+ if (this.renewTimer) {
2398
+ clearTimeout(this.renewTimer);
2399
+ this.renewTimer = null;
2400
+ }
2401
+ }
2402
+ // ── Challenge Flow ──
2403
+ async solveChallenge() {
2404
+ try {
2405
+ const challengeRes = await fetch(`${this.config.apiBase}/crawl-protect/challenge`, {
2406
+ method: "POST",
2407
+ headers: { "Content-Type": "application/json", "x-client-id": this.config.clientId }
2408
+ });
2409
+ if (!challengeRes.ok) return false;
2410
+ const { id, challenges } = await challengeRes.json();
2411
+ const solutions = [];
2412
+ for (const ch of challenges) {
2413
+ if (ch.type === "math") {
2414
+ const result = ch.op === "add" ? ch.a + ch.b : ch.a * ch.b;
2415
+ solutions.push(String(result));
2416
+ } else if (ch.type === "crypto") {
2417
+ const hash = await this.sha256(ch.nonce);
2418
+ solutions.push(hash.slice(0, 8));
2419
+ }
2420
+ }
2421
+ const solution = solutions.join(":");
2422
+ const fp = await this.getFingerprint();
2423
+ const verifyRes = await fetch(`${this.config.apiBase}/crawl-protect/verify`, {
2424
+ method: "POST",
2425
+ headers: { "Content-Type": "application/json", "x-client-id": this.config.clientId },
2426
+ body: JSON.stringify({ id, solution, fingerprint: fp }),
2427
+ credentials: "include"
2428
+ // Accept cookie
2429
+ });
2430
+ if (!verifyRes.ok) return false;
2431
+ const data = await verifyRes.json();
2432
+ if (data.success && data.token) {
2433
+ this.setToken(data.token);
2434
+ this.scheduleRenewal(data.token);
2435
+ if (this.config.debug) ;
2436
+ return true;
2437
+ }
2438
+ return false;
2439
+ } catch (e) {
2440
+ if (this.config.debug) ;
2441
+ return false;
2442
+ }
2443
+ }
2444
+ // ── Content Protection (FE-only mode) ──
2445
+ hideContent() {
2446
+ var _a;
2447
+ if (!((_a = this.config.protectSelectors) == null ? void 0 : _a.length)) return;
2448
+ for (const selector of this.config.protectSelectors) {
2449
+ const elements = document.querySelectorAll(selector);
2450
+ elements.forEach((el) => {
2451
+ el.dataset.aieOrigDisplay = el.style.display;
2452
+ el.style.display = "none";
2453
+ });
2454
+ }
2455
+ }
2456
+ revealContent() {
2457
+ var _a;
2458
+ if (!((_a = this.config.protectSelectors) == null ? void 0 : _a.length)) return;
2459
+ for (const selector of this.config.protectSelectors) {
2460
+ const elements = document.querySelectorAll(selector);
2461
+ elements.forEach((el) => {
2462
+ el.style.display = el.dataset.aieOrigDisplay || "";
2463
+ delete el.dataset.aieOrigDisplay;
2464
+ });
2465
+ }
2466
+ }
2467
+ insertHoneypotData() {
2468
+ const honeypot = document.createElement("div");
2469
+ honeypot.style.cssText = "position:absolute;left:-9999px;top:-9999px;opacity:0;pointer-events:none;";
2470
+ honeypot.setAttribute("aria-hidden", "true");
2471
+ honeypot.innerHTML = `
2472
+ <span class="price" data-aie-honeypot="1">₩999,999,999</span>
2473
+ <span class="stock" data-aie-honeypot="1">재고: 0개</span>
2474
+ <a href="mailto:honeypot@aientrophy.com" data-aie-honeypot="1">contact</a>
2475
+ `;
2476
+ document.body.appendChild(honeypot);
2477
+ }
2478
+ // ── Token Management ──
2479
+ getToken() {
2480
+ const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));
2481
+ return match ? decodeURIComponent(match[1]) : null;
2482
+ }
2483
+ setToken(token) {
2484
+ const expires = new Date(Date.now() + 30 * 60 * 1e3).toUTCString();
2485
+ document.cookie = `${COOKIE_NAME}=${encodeURIComponent(token)};path=/;expires=${expires};SameSite=None;Secure`;
2486
+ }
2487
+ isExpired(token) {
2488
+ try {
2489
+ const [payloadStr] = token.split(".");
2490
+ const payload = JSON.parse(atob(payloadStr.replace(/-/g, "+").replace(/_/g, "/")));
2491
+ return Date.now() > payload.exp;
2492
+ } catch {
2493
+ return true;
2494
+ }
2495
+ }
2496
+ scheduleRenewal(token) {
2497
+ try {
2498
+ const [payloadStr] = token.split(".");
2499
+ const payload = JSON.parse(atob(payloadStr.replace(/-/g, "+").replace(/_/g, "/")));
2500
+ const renewAt = payload.exp - RENEWAL_MARGIN_MS;
2501
+ const delay = renewAt - Date.now();
2502
+ if (delay > 0) {
2503
+ this.renewTimer = setTimeout(() => this.solveChallenge(), delay);
2504
+ }
2505
+ } catch {
2506
+ }
2507
+ }
2508
+ // ── Crypto Helpers ──
2509
+ async sha256(data) {
2510
+ const encoder = new TextEncoder();
2511
+ const buffer = await crypto.subtle.digest("SHA-256", encoder.encode(data));
2512
+ return Array.from(new Uint8Array(buffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
2513
+ }
2514
+ async getFingerprint() {
2515
+ const parts = [
2516
+ navigator.userAgent,
2517
+ navigator.language,
2518
+ screen.width + "x" + screen.height,
2519
+ Intl.DateTimeFormat().resolvedOptions().timeZone
2520
+ ];
2521
+ return this.sha256(parts.join("|"));
2522
+ }
2523
+ }
1163
2524
  class SecuritySDK {
1164
2525
  constructor() {
1165
2526
  __publicField(this, "transmitter");
1166
- __publicField(this, "collector");
1167
2527
  __publicField(this, "behaviorCollector");
1168
2528
  // @ts-ignore
1169
2529
  __publicField(this, "rapidClickDetector");
@@ -1175,6 +2535,13 @@ class SecuritySDK {
1175
2535
  // @ts-ignore
1176
2536
  __publicField(this, "challengeHandler");
1177
2537
  __publicField(this, "invisibleInteraction");
2538
+ __publicField(this, "automationDetector");
2539
+ __publicField(this, "sessionAnalyzer");
2540
+ __publicField(this, "tabNavigationAnalyzer");
2541
+ __publicField(this, "cloudEnvironmentDetector");
2542
+ __publicField(this, "crossContextDetector");
2543
+ __publicField(this, "headlessProbeDetector");
2544
+ __publicField(this, "crawlProtect", null);
1178
2545
  __publicField(this, "initialized", false);
1179
2546
  __publicField(this, "wasmService");
1180
2547
  __publicField(this, "callbackManager");
@@ -1182,7 +2549,6 @@ class SecuritySDK {
1182
2549
  this.wasmService = new WasmService();
1183
2550
  this.callbackManager = new CallbackManager();
1184
2551
  this.transmitter.setCallbackManager(this.callbackManager);
1185
- this.collector = new EventCollector(this.transmitter);
1186
2552
  this.behaviorCollector = new BehaviorCollector(this.transmitter);
1187
2553
  this.rapidClickDetector = new RapidClickDetector(this.transmitter);
1188
2554
  this.honeypot = new Honeypot(this.transmitter);
@@ -1190,6 +2556,12 @@ class SecuritySDK {
1190
2556
  this.canvasFingerprinter = new CanvasFingerprinter(this.transmitter, this.wasmService);
1191
2557
  this.inputTracker = new InputTracker(this.transmitter);
1192
2558
  this.invisibleInteraction = new InvisibleInteraction(this.transmitter);
2559
+ this.automationDetector = new AutomationDetector(this.transmitter);
2560
+ this.sessionAnalyzer = new SessionAnalyzer(this.transmitter);
2561
+ this.tabNavigationAnalyzer = new TabNavigationAnalyzer(this.transmitter);
2562
+ this.cloudEnvironmentDetector = new CloudEnvironmentDetector(this.transmitter);
2563
+ this.crossContextDetector = new CrossContextDetector(this.transmitter);
2564
+ this.headlessProbeDetector = new HeadlessProbeDetector(this.transmitter);
1193
2565
  }
1194
2566
  async init(config) {
1195
2567
  var _a, _b;
@@ -1213,13 +2585,25 @@ class SecuritySDK {
1213
2585
  if (devtoolsEnabled) {
1214
2586
  import("../anti-debug-CRuvY4WC.js").then(({ AntiDebug }) => {
1215
2587
  AntiDebug.start();
1216
- import("../console-BRZJJX1_.js").then(({ ConsoleDetector }) => {
2588
+ import("../console-DbZZ4Ctg.js").then(({ ConsoleDetector }) => {
1217
2589
  new ConsoleDetector(() => {
1218
2590
  this.transmitter.send("devtools_open", { detected: true });
1219
2591
  }).start();
1220
2592
  });
1221
2593
  });
1222
2594
  }
2595
+ if (config.crawlProtect) {
2596
+ this.crawlProtect = new CrawlProtect({
2597
+ apiBase: baseApiUrl,
2598
+ clientId: config.clientKey,
2599
+ protectSelectors: config.crawlProtect.protectSelectors,
2600
+ insertHoneypot: config.crawlProtect.insertHoneypot,
2601
+ debug: config.debug
2602
+ });
2603
+ this.crawlProtect.start().catch((err) => {
2604
+ if (config.debug) ;
2605
+ });
2606
+ }
1223
2607
  this.initialized = true;
1224
2608
  if (config.debug) {
1225
2609
  }
@@ -1243,13 +2627,18 @@ class SecuritySDK {
1243
2627
  this.callbackManager.once(event, handler);
1244
2628
  }
1245
2629
  startDetectors() {
1246
- this.collector.start();
1247
2630
  this.behaviorCollector.start();
1248
2631
  this.honeypot.start();
1249
2632
  this.mouseTracker.start();
1250
2633
  this.canvasFingerprinter.start();
1251
2634
  this.inputTracker.start();
1252
2635
  this.invisibleInteraction.start();
2636
+ this.automationDetector.start();
2637
+ this.sessionAnalyzer.start();
2638
+ this.tabNavigationAnalyzer.start();
2639
+ this.cloudEnvironmentDetector.start();
2640
+ this.crossContextDetector.start();
2641
+ this.headlessProbeDetector.start();
1253
2642
  }
1254
2643
  }
1255
2644
  const securitySDK = new SecuritySDK();