@aientrophy/sdk 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{console-BRZJJX1_.js → console-DbZZ4Ctg.js} +7 -0
- package/dist/{console-D3Kd9M_6.cjs → console-aIpHQlgP.cjs} +1 -1
- package/dist/core.js +1 -1
- package/dist/index.d.ts +21 -0
- package/dist/npm/index.cjs.js +1 -1
- package/dist/npm/index.es.js +1568 -192
- package/dist/sri-hashes.json +9 -0
- package/dist/types/challenge/ChallengeHandler.d.ts +1 -0
- package/dist/types/collection/BehaviorCollector.d.ts +0 -13
- package/dist/types/core/index.d.ts +14 -1
- package/dist/types/detectors/AutomationDetector.d.ts +43 -0
- package/dist/types/detectors/CanvasFingerprinter.d.ts +19 -2
- package/dist/types/detectors/CloudEnvironmentDetector.d.ts +51 -0
- package/dist/types/detectors/CrossContextDetector.d.ts +34 -0
- package/dist/types/detectors/HeadlessProbeDetector.d.ts +66 -0
- package/dist/types/detectors/InputTracker.d.ts +1 -0
- package/dist/types/detectors/MouseTracker.d.ts +2 -0
- package/dist/types/detectors/SessionAnalyzer.d.ts +39 -0
- package/dist/types/detectors/TabNavigationAnalyzer.d.ts +36 -0
- package/dist/types/detectors/console.d.ts +4 -0
- package/dist/types/npm/index.d.ts +1 -0
- package/dist/types/protection/CrawlProtect.d.ts +38 -0
- package/dist/types/protection/CryptoUtils.d.ts +38 -3
- package/dist/types/transport/index.d.ts +3 -0
- package/package.json +2 -2
- package/dist/types/collector/index.d.ts +0 -11
package/dist/npm/index.es.js
CHANGED
|
@@ -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
|
-
//
|
|
10
|
+
// 2 = ECDH, 1 = RSA
|
|
11
11
|
/**
|
|
12
|
-
* Initialize session key
|
|
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
|
-
|
|
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: "
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
260
|
-
__publicField(this, "flushInterval",
|
|
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",
|
|
621
|
-
|
|
622
|
-
|
|
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 <
|
|
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
|
-
|
|
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() <
|
|
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
|
-
|
|
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
|
|
890
|
-
this.
|
|
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
|
-
|
|
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.
|
|
926
|
-
return
|
|
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
|
-
|
|
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-
|
|
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();
|
|
@@ -1306,6 +2681,7 @@ class Aientrophy {
|
|
|
1306
2681
|
clientKey: this.config.clientKey,
|
|
1307
2682
|
debug: this.config.debug,
|
|
1308
2683
|
callbacks: this.config.callbacks,
|
|
2684
|
+
crawlProtect: this.config.crawlProtect,
|
|
1309
2685
|
serverConfig
|
|
1310
2686
|
});
|
|
1311
2687
|
}
|