@aientrophy/sdk 0.2.1 → 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,13 +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", 3e3);
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);
264
339
  __publicField(this, "flushing", false);
265
340
  __publicField(this, "pendingFlush", false);
341
+ __publicField(this, "backoffMs", 0);
342
+ __publicField(this, "maxBackoffMs", 6e4);
343
+ __publicField(this, "_lastBackoffStart", 0);
266
344
  this.startFlushInterval();
267
345
  }
268
346
  setConfig(config) {
@@ -271,6 +349,7 @@ class Transmitter {
271
349
  this.clientKey = config.clientKey;
272
350
  this.encryptionRequired = ((_a = config.serverConfig) == null ? void 0 : _a.encryptionRequired) ?? false;
273
351
  this.currentNonce = ((_b = config.serverConfig) == null ? void 0 : _b.initialNonce) ?? "";
352
+ CryptoUtils.setEndpoint(config.endpoint);
274
353
  }
275
354
  setResponseHandler(handler) {
276
355
  this.responseHandler = handler;
@@ -295,6 +374,12 @@ class Transmitter {
295
374
  }
296
375
  async flush() {
297
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
+ }
298
383
  if (this.flushing) {
299
384
  this.pendingFlush = true;
300
385
  return;
@@ -325,6 +410,13 @@ class Transmitter {
325
410
  headers: requestHeaders,
326
411
  keepalive: true
327
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;
328
420
  if (res.ok && this.callbackManager) {
329
421
  this.callbackManager.emit("allow", { action: "allow" });
330
422
  }
@@ -380,149 +472,19 @@ class Transmitter {
380
472
  }, this.flushInterval);
381
473
  }
382
474
  }
383
- class EventCollector {
384
- constructor(transmitter) {
385
- __publicField(this, "transmitter");
386
- __publicField(this, "listeners", {});
387
- this.transmitter = transmitter;
388
- }
389
- start() {
390
- this.attachListeners();
391
- }
392
- stop() {
393
- this.detachListeners();
394
- }
395
- attachListeners() {
396
- this.listeners["mousemove"] = this.throttle((e) => {
397
- const mouseEvent = e;
398
- this.transmitter.send("mousemove", {
399
- x: mouseEvent.clientX,
400
- y: mouseEvent.clientY,
401
- timestamp: Date.now()
402
- });
403
- }, 100);
404
- this.listeners["click"] = (e) => {
405
- const mouseEvent = e;
406
- this.transmitter.send("click", {
407
- x: mouseEvent.clientX,
408
- y: mouseEvent.clientY,
409
- target: mouseEvent.target.tagName,
410
- timestamp: Date.now()
411
- });
412
- };
413
- this.listeners["keydown"] = (e) => {
414
- const keyEvent = e;
415
- this.transmitter.send("keydown", {
416
- key: keyEvent.key,
417
- timestamp: Date.now()
418
- });
419
- };
420
- window.addEventListener("mousemove", this.listeners["mousemove"]);
421
- window.addEventListener("click", this.listeners["click"]);
422
- window.addEventListener("keydown", this.listeners["keydown"]);
423
- }
424
- detachListeners() {
425
- if (this.listeners["mousemove"]) {
426
- window.removeEventListener("mousemove", this.listeners["mousemove"]);
427
- }
428
- if (this.listeners["click"]) {
429
- window.removeEventListener("click", this.listeners["click"]);
430
- }
431
- if (this.listeners["keydown"]) {
432
- window.removeEventListener("keydown", this.listeners["keydown"]);
433
- }
434
- }
435
- throttle(func, limit) {
436
- let inThrottle;
437
- return function(...args) {
438
- if (!inThrottle) {
439
- func.apply(this, args);
440
- inThrottle = true;
441
- setTimeout(() => inThrottle = false, limit);
442
- }
443
- };
444
- }
445
- }
446
475
  class BehaviorCollector {
447
476
  constructor(transmitter) {
477
+ // @ts-ignore kept for future extension (mouse/keyboard-외 행동 수집)
448
478
  __publicField(this, "transmitter");
449
- __publicField(this, "trajectoryBuffer", []);
450
- __publicField(this, "keystrokeBuffer", []);
451
479
  __publicField(this, "isCollecting", false);
452
- __publicField(this, "flushInterval", 3e3);
453
- // Send batch every 3 seconds
454
- __publicField(this, "flushTimer");
455
- __publicField(this, "lastMouseMoveTime", 0);
456
- __publicField(this, "onMouseMove", (e) => {
457
- const now = Date.now();
458
- if (now - this.lastMouseMoveTime > 50) {
459
- this.trajectoryBuffer.push({
460
- x: e.clientX,
461
- y: e.clientY,
462
- t: now
463
- });
464
- this.lastMouseMoveTime = now;
465
- }
466
- });
467
- __publicField(this, "onKeyDown", (e) => {
468
- this.keystrokeBuffer.push({
469
- code: e.code,
470
- t: Date.now(),
471
- type: "down"
472
- });
473
- });
474
- __publicField(this, "onKeyUp", (e) => {
475
- this.keystrokeBuffer.push({
476
- code: e.code,
477
- t: Date.now(),
478
- type: "up"
479
- });
480
- });
481
480
  this.transmitter = transmitter;
482
481
  }
483
482
  start() {
484
483
  if (this.isCollecting) return;
485
484
  this.isCollecting = true;
486
- this.attachListeners();
487
- this.startFlushTimer();
488
485
  }
489
486
  stop() {
490
487
  this.isCollecting = false;
491
- this.detachListeners();
492
- this.stopFlushTimer();
493
- this.flush();
494
- }
495
- attachListeners() {
496
- window.addEventListener("mousemove", this.onMouseMove);
497
- window.addEventListener("keydown", this.onKeyDown);
498
- window.addEventListener("keyup", this.onKeyUp);
499
- }
500
- detachListeners() {
501
- window.removeEventListener("mousemove", this.onMouseMove);
502
- window.removeEventListener("keydown", this.onKeyDown);
503
- window.removeEventListener("keyup", this.onKeyUp);
504
- }
505
- startFlushTimer() {
506
- this.flushTimer = setInterval(() => {
507
- this.flush();
508
- }, this.flushInterval);
509
- }
510
- stopFlushTimer() {
511
- if (this.flushTimer) {
512
- clearInterval(this.flushTimer);
513
- this.flushTimer = null;
514
- }
515
- }
516
- flush() {
517
- if (this.trajectoryBuffer.length === 0 && this.keystrokeBuffer.length === 0) return;
518
- const data = {
519
- trajectory: [...this.trajectoryBuffer],
520
- keystrokes: [...this.keystrokeBuffer],
521
- url: window.location.href
522
- };
523
- this.trajectoryBuffer = [];
524
- this.keystrokeBuffer = [];
525
- this.transmitter.send("behavior_data", data);
526
488
  }
527
489
  }
528
490
  class RapidClickDetector {
@@ -617,10 +579,14 @@ class MouseTracker {
617
579
  __publicField(this, "transmitter");
618
580
  __publicField(this, "wasmService");
619
581
  __publicField(this, "buffer", []);
620
- __publicField(this, "BUFFER_SIZE", 10);
621
- __publicField(this, "SAMPLE_INTERVAL", 50);
622
- // 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
623
588
  __publicField(this, "lastSampleTime", 0);
589
+ __publicField(this, "lastSendTime", 0);
624
590
  __publicField(this, "handleMouseMove", (e) => {
625
591
  const now = Date.now();
626
592
  if (now - this.lastSampleTime < this.SAMPLE_INTERVAL) return;
@@ -628,7 +594,10 @@ class MouseTracker {
628
594
  this.buffer.push({ x: e.clientX, y: e.clientY, time: now });
629
595
  if (this.buffer.length > this.BUFFER_SIZE) {
630
596
  this.buffer.shift();
597
+ }
598
+ if (this.buffer.length >= this.BUFFER_SIZE && now - this.lastSendTime >= this.SEND_COOLDOWN) {
631
599
  this.analyze();
600
+ this.lastSendTime = now;
632
601
  }
633
602
  });
634
603
  this.transmitter = transmitter;
@@ -645,15 +614,17 @@ class MouseTracker {
645
614
  }
646
615
  }
647
616
  analyze() {
648
- if (this.buffer.length < 5) return;
617
+ if (this.buffer.length < 20) return;
649
618
  const entropy = this.wasmService.calculateEntropy(this.buffer);
650
619
  const speedVariance = this.calculateSpeedVariance();
620
+ const rawTrajectory = this.buffer.map((p) => ({ x: p.x, y: p.y, t: p.time }));
651
621
  this.transmitter.send("behavior_metrics", {
652
622
  entropy,
653
623
  speedVariance,
654
- timestamp: Date.now()
624
+ sampleSize: this.buffer.length,
625
+ timestamp: Date.now(),
626
+ rawTrajectory
655
627
  });
656
- this.buffer = [];
657
628
  }
658
629
  // Speed Variance is simple enough to keep in JS or move to Wasm later.
659
630
  calculateSpeedVariance() {
@@ -674,8 +645,6 @@ class MouseTracker {
674
645
  }
675
646
  }
676
647
  class InputTracker {
677
- // Buffer for raw input events - currently unused but kept for potential future raw data analysis
678
- // private inputBuffer: any[] = [];
679
648
  constructor(transmitter) {
680
649
  __publicField(this, "lastInteractionTime", 0);
681
650
  __publicField(this, "lastInteractionType", "none");
@@ -690,6 +659,8 @@ class InputTracker {
690
659
  // Suspicious focus tracking
691
660
  __publicField(this, "suspiciousFocusCount", 0);
692
661
  __publicField(this, "suspiciousFocusResetTimer", null);
662
+ // Raw keystroke buffer for ML training
663
+ __publicField(this, "rawKeystrokes", []);
693
664
  this.transmitter = transmitter;
694
665
  this.isMobile = this.detectMobile();
695
666
  }
@@ -714,6 +685,7 @@ class InputTracker {
714
685
  this.checkFocusIntegrity();
715
686
  this.flightTimes = [];
716
687
  this.dwellTimes = [];
688
+ this.rawKeystrokes = [];
717
689
  this.lastKeyUpTime = 0;
718
690
  }
719
691
  }, { capture: true });
@@ -722,6 +694,7 @@ class InputTracker {
722
694
  if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") return;
723
695
  const now = Date.now();
724
696
  this.keyPressTimes.set(e.code, now);
697
+ this.rawKeystrokes.push({ code: e.code, t: now, type: "down" });
725
698
  if (this.lastKeyUpTime > 0) {
726
699
  const flight = now - this.lastKeyUpTime;
727
700
  if (flight < 2e3) {
@@ -734,6 +707,7 @@ class InputTracker {
734
707
  if (target.tagName !== "INPUT" && target.tagName !== "TEXTAREA") return;
735
708
  const now = Date.now();
736
709
  this.lastKeyUpTime = now;
710
+ this.rawKeystrokes.push({ code: e.code, t: now, type: "up" });
737
711
  const startTime = this.keyPressTimes.get(e.code);
738
712
  if (startTime) {
739
713
  const dwell = now - startTime;
@@ -754,7 +728,7 @@ class InputTracker {
754
728
  checkFocusIntegrity() {
755
729
  const now = Date.now();
756
730
  const timeDiff = now - this.lastInteractionTime;
757
- if (performance.now() < 2e3) return;
731
+ if (performance.now() < 5e3) return;
758
732
  if (timeDiff > 200) {
759
733
  this.suspiciousFocusCount++;
760
734
  clearTimeout(this.suspiciousFocusResetTimer);
@@ -768,15 +742,6 @@ class InputTracker {
768
742
  consecutiveCount: this.suspiciousFocusCount,
769
743
  timestamp: now
770
744
  });
771
- if (this.suspiciousFocusCount >= 3) {
772
- this.transmitter.send("integrity_violation", {
773
- type: "programmatic_focus",
774
- details: `${this.suspiciousFocusCount} consecutive input focuses without click/tab`,
775
- consecutiveCount: this.suspiciousFocusCount,
776
- timestamp: now
777
- });
778
- this.suspiciousFocusCount = 0;
779
- }
780
745
  } else {
781
746
  this.suspiciousFocusCount = 0;
782
747
  }
@@ -798,10 +763,13 @@ class InputTracker {
798
763
  variance: varDwell
799
764
  },
800
765
  isMobile: this.isMobile,
801
- timestamp: Date.now()
766
+ timestamp: Date.now(),
767
+ rawKeystrokes: [...this.rawKeystrokes]
768
+ // ML training data (~10-20 entries, ~800B)
802
769
  });
803
770
  this.flightTimes = [];
804
771
  this.dwellTimes = [];
772
+ this.rawKeystrokes = [];
805
773
  }
806
774
  average(data) {
807
775
  if (data.length === 0) return 0;
@@ -876,22 +844,32 @@ class WasmService {
876
844
  }
877
845
  }
878
846
  class CanvasFingerprinter {
879
- 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) {
880
850
  __publicField(this, "transmitter");
881
- __publicField(this, "wasmService");
882
851
  __publicField(this, "hasRun", false);
883
852
  this.transmitter = transmitter;
884
- this.wasmService = wasmService;
885
853
  }
886
854
  start() {
887
855
  if (this.hasRun || typeof document === "undefined") return;
888
- setTimeout(() => {
889
- const fingerprint = this.generateFingerprint();
890
- 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
+ });
891
865
  this.hasRun = true;
892
866
  }, 500);
893
867
  }
894
- 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() {
895
873
  try {
896
874
  const canvas = document.createElement("canvas");
897
875
  const ctx = canvas.getContext("2d");
@@ -922,18 +900,75 @@ class CanvasFingerprinter {
922
900
  ctx.closePath();
923
901
  ctx.fill();
924
902
  const dataUrl = canvas.toDataURL();
925
- const hash = this.wasmService.simpleHash(dataUrl);
926
- return Math.abs(hash).toString(16);
903
+ const hash = await this.sha256(dataUrl);
904
+ return hash;
927
905
  } catch (e) {
928
906
  return "error";
929
907
  }
930
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
+ }
931
965
  }
932
966
  class ChallengeHandler {
933
967
  constructor(endpoint) {
934
968
  __publicField(this, "originalFetch");
935
969
  __publicField(this, "endpoint");
936
970
  __publicField(this, "callbackManager", null);
971
+ __publicField(this, "challengeInProgress", null);
937
972
  this.endpoint = endpoint;
938
973
  this.originalFetch = window.fetch.bind(window);
939
974
  }
@@ -960,7 +995,13 @@ class ChallengeHandler {
960
995
  return this.originalFetch(input, init);
961
996
  }
962
997
  }
963
- 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;
964
1005
  if (passed) {
965
1006
  return this.originalFetch(input, init);
966
1007
  } else {
@@ -1174,10 +1215,1315 @@ class InvisibleInteraction {
1174
1215
  return null;
1175
1216
  }
1176
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
+ }
1177
2524
  class SecuritySDK {
1178
2525
  constructor() {
1179
2526
  __publicField(this, "transmitter");
1180
- __publicField(this, "collector");
1181
2527
  __publicField(this, "behaviorCollector");
1182
2528
  // @ts-ignore
1183
2529
  __publicField(this, "rapidClickDetector");
@@ -1189,6 +2535,13 @@ class SecuritySDK {
1189
2535
  // @ts-ignore
1190
2536
  __publicField(this, "challengeHandler");
1191
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);
1192
2545
  __publicField(this, "initialized", false);
1193
2546
  __publicField(this, "wasmService");
1194
2547
  __publicField(this, "callbackManager");
@@ -1196,7 +2549,6 @@ class SecuritySDK {
1196
2549
  this.wasmService = new WasmService();
1197
2550
  this.callbackManager = new CallbackManager();
1198
2551
  this.transmitter.setCallbackManager(this.callbackManager);
1199
- this.collector = new EventCollector(this.transmitter);
1200
2552
  this.behaviorCollector = new BehaviorCollector(this.transmitter);
1201
2553
  this.rapidClickDetector = new RapidClickDetector(this.transmitter);
1202
2554
  this.honeypot = new Honeypot(this.transmitter);
@@ -1204,6 +2556,12 @@ class SecuritySDK {
1204
2556
  this.canvasFingerprinter = new CanvasFingerprinter(this.transmitter, this.wasmService);
1205
2557
  this.inputTracker = new InputTracker(this.transmitter);
1206
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);
1207
2565
  }
1208
2566
  async init(config) {
1209
2567
  var _a, _b;
@@ -1227,13 +2585,25 @@ class SecuritySDK {
1227
2585
  if (devtoolsEnabled) {
1228
2586
  import("../anti-debug-CRuvY4WC.js").then(({ AntiDebug }) => {
1229
2587
  AntiDebug.start();
1230
- import("../console-BRZJJX1_.js").then(({ ConsoleDetector }) => {
2588
+ import("../console-DbZZ4Ctg.js").then(({ ConsoleDetector }) => {
1231
2589
  new ConsoleDetector(() => {
1232
2590
  this.transmitter.send("devtools_open", { detected: true });
1233
2591
  }).start();
1234
2592
  });
1235
2593
  });
1236
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
+ }
1237
2607
  this.initialized = true;
1238
2608
  if (config.debug) {
1239
2609
  }
@@ -1257,13 +2627,18 @@ class SecuritySDK {
1257
2627
  this.callbackManager.once(event, handler);
1258
2628
  }
1259
2629
  startDetectors() {
1260
- this.collector.start();
1261
2630
  this.behaviorCollector.start();
1262
2631
  this.honeypot.start();
1263
2632
  this.mouseTracker.start();
1264
2633
  this.canvasFingerprinter.start();
1265
2634
  this.inputTracker.start();
1266
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();
1267
2642
  }
1268
2643
  }
1269
2644
  const securitySDK = new SecuritySDK();