@keverdjs/fraud-sdk-angular 1.0.0 → 2.0.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.
- package/dist/index.d.mts +182 -25
- package/dist/index.d.ts +182 -25
- package/dist/index.js +3662 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3664 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var core = require('@angular/core');
|
|
4
4
|
var rxjs = require('rxjs');
|
|
5
|
+
var operators = require('rxjs/operators');
|
|
5
6
|
var common = require('@angular/common');
|
|
6
7
|
|
|
7
8
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -12,29 +13,3684 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
12
13
|
result = (decorator(result)) || result;
|
|
13
14
|
return result;
|
|
14
15
|
};
|
|
15
|
-
|
|
16
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
17
|
+
|
|
18
|
+
// ../fraud-sdk/src/collectors/device.ts
|
|
19
|
+
var DeviceCollector = class {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.cachedDeviceInfo = null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Collect all device information and generate fingerprint
|
|
25
|
+
*/
|
|
26
|
+
collect() {
|
|
27
|
+
if (this.cachedDeviceInfo) {
|
|
28
|
+
return this.cachedDeviceInfo;
|
|
29
|
+
}
|
|
30
|
+
const fingerprint = this.generateDeviceFingerprint();
|
|
31
|
+
const deviceId = this.generateDeviceId(fingerprint);
|
|
32
|
+
this.cachedDeviceInfo = {
|
|
33
|
+
deviceId,
|
|
34
|
+
fingerprint,
|
|
35
|
+
manufacturer: this.getManufacturer(),
|
|
36
|
+
model: this.getModel(),
|
|
37
|
+
brand: this.getBrand(),
|
|
38
|
+
device: this.getDevice(),
|
|
39
|
+
product: this.getProduct(),
|
|
40
|
+
hardware: this.getHardware(),
|
|
41
|
+
sdkVersion: "1.0.0",
|
|
42
|
+
osVersion: this.getOSVersion(),
|
|
43
|
+
screenWidth: String(screen.width),
|
|
44
|
+
screenHeight: String(screen.height),
|
|
45
|
+
screenDensity: this.getScreenDensity(),
|
|
46
|
+
locale: navigator.language || navigator.languages?.[0] || "en",
|
|
47
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
48
|
+
};
|
|
49
|
+
return this.cachedDeviceInfo;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Generate a stable device fingerprint using multiple browser characteristics
|
|
53
|
+
* Returns SHA-256 hash (64 hex characters) as required by backend
|
|
54
|
+
*/
|
|
55
|
+
generateDeviceFingerprint() {
|
|
56
|
+
const components = [];
|
|
57
|
+
const canvasFingerprint = this.getCanvasFingerprint();
|
|
58
|
+
if (canvasFingerprint) components.push(canvasFingerprint);
|
|
59
|
+
const webglFingerprint = this.getWebGLFingerprint();
|
|
60
|
+
if (webglFingerprint) components.push(webglFingerprint);
|
|
61
|
+
components.push(navigator.userAgent);
|
|
62
|
+
components.push(navigator.language || navigator.languages?.[0] || "");
|
|
63
|
+
components.push(`${screen.width}x${screen.height}x${screen.colorDepth}`);
|
|
64
|
+
components.push(String((/* @__PURE__ */ new Date()).getTimezoneOffset()));
|
|
65
|
+
components.push(navigator.platform);
|
|
66
|
+
components.push(String(navigator.hardwareConcurrency || 0));
|
|
67
|
+
if ("deviceMemory" in navigator) {
|
|
68
|
+
components.push(String(navigator.deviceMemory));
|
|
69
|
+
}
|
|
70
|
+
if ("maxTouchPoints" in navigator) {
|
|
71
|
+
components.push(String(navigator.maxTouchPoints));
|
|
72
|
+
}
|
|
73
|
+
if (navigator.plugins && navigator.plugins.length > 0) {
|
|
74
|
+
const pluginNames = Array.from(navigator.plugins).slice(0, 3).map((p) => p.name).join(",");
|
|
75
|
+
components.push(pluginNames);
|
|
76
|
+
}
|
|
77
|
+
const combined = components.join("|");
|
|
78
|
+
return this.hashString(combined);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Generate canvas fingerprint
|
|
82
|
+
*/
|
|
83
|
+
getCanvasFingerprint() {
|
|
84
|
+
try {
|
|
85
|
+
const canvas = document.createElement("canvas");
|
|
86
|
+
const ctx = canvas.getContext("2d");
|
|
87
|
+
if (!ctx) return "";
|
|
88
|
+
canvas.width = 200;
|
|
89
|
+
canvas.height = 50;
|
|
90
|
+
ctx.textBaseline = "top";
|
|
91
|
+
ctx.font = "14px Arial";
|
|
92
|
+
ctx.fillStyle = "#f60";
|
|
93
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
94
|
+
ctx.fillStyle = "#069";
|
|
95
|
+
ctx.fillText("KeverdFingerprint", 2, 15);
|
|
96
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
97
|
+
ctx.fillText("KeverdFingerprint", 4, 17);
|
|
98
|
+
return this.hashString(canvas.toDataURL());
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Generate WebGL fingerprint
|
|
105
|
+
*/
|
|
106
|
+
getWebGLFingerprint() {
|
|
107
|
+
try {
|
|
108
|
+
const canvas = document.createElement("canvas");
|
|
109
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
110
|
+
if (!gl) return "";
|
|
111
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
112
|
+
if (debugInfo) {
|
|
113
|
+
const vendor2 = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
114
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
115
|
+
return this.hashString(`${vendor2}|${renderer}`);
|
|
116
|
+
}
|
|
117
|
+
const version = gl.getParameter(gl.VERSION);
|
|
118
|
+
const vendor = gl.getParameter(gl.VENDOR);
|
|
119
|
+
return this.hashString(`${version}|${vendor}`);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generate a stable device ID from fingerprint
|
|
126
|
+
*/
|
|
127
|
+
generateDeviceId(fingerprint) {
|
|
128
|
+
return fingerprint.substring(0, 32);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get manufacturer from user agent
|
|
132
|
+
*/
|
|
133
|
+
getManufacturer() {
|
|
134
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
135
|
+
if (ua.includes("iphone") || ua.includes("ipad")) return "Apple";
|
|
136
|
+
if (ua.includes("android")) {
|
|
137
|
+
const match = ua.match(/(?:^|\s)([a-z]+)(?:\s|$)/);
|
|
138
|
+
return match ? match[1].charAt(0).toUpperCase() + match[1].slice(1) : void 0;
|
|
139
|
+
}
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get model from user agent
|
|
144
|
+
*/
|
|
145
|
+
getModel() {
|
|
146
|
+
const ua = navigator.userAgent;
|
|
147
|
+
const match = ua.match(/(iPhone|iPad|Android)[\s\/]+([\w]+)/i);
|
|
148
|
+
return match ? match[2] : void 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Get brand from user agent
|
|
152
|
+
*/
|
|
153
|
+
getBrand() {
|
|
154
|
+
return this.getManufacturer();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get device type
|
|
158
|
+
*/
|
|
159
|
+
getDevice() {
|
|
160
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
161
|
+
if (ua.includes("mobile")) return "mobile";
|
|
162
|
+
if (ua.includes("tablet")) return "tablet";
|
|
163
|
+
return "desktop";
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get product name
|
|
167
|
+
*/
|
|
168
|
+
getProduct() {
|
|
169
|
+
const ua = navigator.userAgent;
|
|
170
|
+
const match = ua.match(/(iPhone|iPad|Android|Windows|Mac|Linux)/i);
|
|
171
|
+
return match ? match[1] : void 0;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Get hardware info
|
|
175
|
+
*/
|
|
176
|
+
getHardware() {
|
|
177
|
+
const ua = navigator.userAgent;
|
|
178
|
+
const match = ua.match(/\(([^)]+)\)/);
|
|
179
|
+
return match ? match[1] : void 0;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get OS version
|
|
183
|
+
*/
|
|
184
|
+
getOSVersion() {
|
|
185
|
+
const ua = navigator.userAgent;
|
|
186
|
+
const patterns = [
|
|
187
|
+
/OS\s+([\d_]+)/i,
|
|
188
|
+
// iOS
|
|
189
|
+
/Android\s+([\d.]+)/i,
|
|
190
|
+
// Android
|
|
191
|
+
/Windows\s+([\d.]+)/i,
|
|
192
|
+
// Windows
|
|
193
|
+
/Mac\s+OS\s+X\s+([\d_]+)/i,
|
|
194
|
+
// macOS
|
|
195
|
+
/Linux/i
|
|
196
|
+
// Linux
|
|
197
|
+
];
|
|
198
|
+
for (const pattern of patterns) {
|
|
199
|
+
const match = ua.match(pattern);
|
|
200
|
+
if (match) {
|
|
201
|
+
return match[1]?.replace(/_/g, ".") || match[0];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return void 0;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get screen density
|
|
208
|
+
*/
|
|
209
|
+
getScreenDensity() {
|
|
210
|
+
if (window.devicePixelRatio) {
|
|
211
|
+
return String(window.devicePixelRatio);
|
|
212
|
+
}
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Hash a string using SHA-256-like algorithm (64 hex characters)
|
|
217
|
+
* Backend expects SHA-256 format (64 hex chars)
|
|
218
|
+
*/
|
|
219
|
+
hashString(str) {
|
|
220
|
+
return this.sha256LikeHash(str);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* SHA-256-like hash function (synchronous, deterministic)
|
|
224
|
+
* Produces 64-character hex string matching SHA-256 format
|
|
225
|
+
*/
|
|
226
|
+
sha256LikeHash(str) {
|
|
227
|
+
const hashes = [];
|
|
228
|
+
let h1 = 5381;
|
|
229
|
+
for (let i = 0; i < str.length; i++) {
|
|
230
|
+
h1 = (h1 << 5) + h1 + str.charCodeAt(i);
|
|
231
|
+
h1 = h1 & 4294967295;
|
|
232
|
+
}
|
|
233
|
+
hashes.push(h1);
|
|
234
|
+
let h2 = 0;
|
|
235
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
236
|
+
h2 = (h2 << 7) - h2 + str.charCodeAt(i);
|
|
237
|
+
h2 = h2 & 4294967295;
|
|
238
|
+
}
|
|
239
|
+
hashes.push(h2);
|
|
240
|
+
let h3 = 0;
|
|
241
|
+
for (let i = 0; i < str.length; i++) {
|
|
242
|
+
h3 = h3 ^ str.charCodeAt(i) << i % 4 * 8;
|
|
243
|
+
h3 = h3 & 4294967295;
|
|
244
|
+
}
|
|
245
|
+
hashes.push(h3);
|
|
246
|
+
let h4 = 0;
|
|
247
|
+
for (let i = 0; i < str.length; i++) {
|
|
248
|
+
h4 = h4 * 31 + str.charCodeAt(i) & 4294967295;
|
|
249
|
+
}
|
|
250
|
+
hashes.push(h4);
|
|
251
|
+
let h5 = 0;
|
|
252
|
+
for (let i = 0; i < str.length; i++) {
|
|
253
|
+
const rotated = (str.charCodeAt(i) << i % 16 | str.charCodeAt(i) >>> 32 - i % 16) & 4294967295;
|
|
254
|
+
h5 = h5 + rotated & 4294967295;
|
|
255
|
+
}
|
|
256
|
+
hashes.push(h5);
|
|
257
|
+
let h6 = 2166136261;
|
|
258
|
+
for (let i = 0; i < str.length; i++) {
|
|
259
|
+
h6 = (h6 ^ str.charCodeAt(i)) * 16777619;
|
|
260
|
+
h6 = h6 & 4294967295;
|
|
261
|
+
}
|
|
262
|
+
hashes.push(h6);
|
|
263
|
+
let h7 = 0;
|
|
264
|
+
for (let i = 0; i < str.length; i++) {
|
|
265
|
+
h7 = h7 + str.charCodeAt(i) * (i + 1) & 4294967295;
|
|
266
|
+
}
|
|
267
|
+
hashes.push(h7);
|
|
268
|
+
let h8 = 0;
|
|
269
|
+
for (let i = 0; i < str.length; i += 2) {
|
|
270
|
+
const combined2 = str.charCodeAt(i) + (str.charCodeAt(i + 1) || 0) * 256;
|
|
271
|
+
h8 = (h8 << 3) - h8 + combined2;
|
|
272
|
+
h8 = h8 & 4294967295;
|
|
273
|
+
}
|
|
274
|
+
hashes.push(h8);
|
|
275
|
+
const hexParts = hashes.map((h) => Math.abs(h).toString(16).padStart(8, "0"));
|
|
276
|
+
const combined = hexParts.join("");
|
|
277
|
+
return combined.substring(0, 64).padEnd(64, "0");
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Clear cached device info (useful for testing)
|
|
281
|
+
*/
|
|
282
|
+
clearCache() {
|
|
283
|
+
this.cachedDeviceInfo = null;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// ../fraud-sdk/src/collectors/kinematic-engine.ts
|
|
288
|
+
var VELOCITY_SPIKE_THRESHOLD = 2.5;
|
|
289
|
+
var WINDOW_LIMIT = 200;
|
|
290
|
+
var KinematicEngine = class {
|
|
291
|
+
constructor(options = {}) {
|
|
292
|
+
this.featureVectors = [];
|
|
293
|
+
this.pointerQueue = [];
|
|
294
|
+
this.lastVelocity = 0;
|
|
295
|
+
this.lastAcceleration = 0;
|
|
296
|
+
this.lastAngle = 0;
|
|
297
|
+
this.lastPoint = null;
|
|
298
|
+
this.secondLastPoint = null;
|
|
299
|
+
this.rafId = null;
|
|
300
|
+
this.maxSwipeVelocity = 0;
|
|
301
|
+
this.isActive = false;
|
|
302
|
+
// Enhanced tracking
|
|
303
|
+
this.clickCount = 0;
|
|
304
|
+
this.rightClickCount = 0;
|
|
305
|
+
this.doubleClickCount = 0;
|
|
306
|
+
this.totalMouseDistance = 0;
|
|
307
|
+
this.lastClickTime = 0;
|
|
308
|
+
this.scrollDistance = 0;
|
|
309
|
+
this.lastScrollTop = 0;
|
|
310
|
+
this.touchEventCount = 0;
|
|
311
|
+
this.mouseMoveHandler = (event) => this.enqueuePoint(event, "move");
|
|
312
|
+
this.mouseDownHandler = (event) => {
|
|
313
|
+
this.enqueuePoint(event, "down");
|
|
314
|
+
this.handleClick(event);
|
|
315
|
+
};
|
|
316
|
+
this.mouseUpHandler = (event) => this.enqueuePoint(event, "up");
|
|
317
|
+
this.contextMenuHandler = (event) => this.handleRightClick(event);
|
|
318
|
+
this.scrollHandler = () => this.handleScroll();
|
|
319
|
+
this.touchStartHandler = () => this.handleTouch();
|
|
320
|
+
this.touchMoveHandler = () => this.handleTouch();
|
|
321
|
+
this.touchEndHandler = () => this.handleTouch();
|
|
322
|
+
this.onEvent = options.onEvent;
|
|
323
|
+
this.maskValue = options.maskValue ?? -1;
|
|
324
|
+
}
|
|
325
|
+
start() {
|
|
326
|
+
if (this.isActive || typeof document === "undefined") return;
|
|
327
|
+
document.addEventListener("mousemove", this.mouseMoveHandler, { passive: true });
|
|
328
|
+
document.addEventListener("mousedown", this.mouseDownHandler, { passive: true });
|
|
329
|
+
document.addEventListener("mouseup", this.mouseUpHandler, { passive: true });
|
|
330
|
+
document.addEventListener("contextmenu", this.contextMenuHandler, { passive: true });
|
|
331
|
+
window.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
332
|
+
document.addEventListener("touchstart", this.touchStartHandler, { passive: true });
|
|
333
|
+
document.addEventListener("touchmove", this.touchMoveHandler, { passive: true });
|
|
334
|
+
document.addEventListener("touchend", this.touchEndHandler, { passive: true });
|
|
335
|
+
this.lastScrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
336
|
+
this.isActive = true;
|
|
337
|
+
}
|
|
338
|
+
stop() {
|
|
339
|
+
if (!this.isActive || typeof document === "undefined") return;
|
|
340
|
+
document.removeEventListener("mousemove", this.mouseMoveHandler);
|
|
341
|
+
document.removeEventListener("mousedown", this.mouseDownHandler);
|
|
342
|
+
document.removeEventListener("mouseup", this.mouseUpHandler);
|
|
343
|
+
document.removeEventListener("contextmenu", this.contextMenuHandler);
|
|
344
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
345
|
+
document.removeEventListener("touchstart", this.touchStartHandler);
|
|
346
|
+
document.removeEventListener("touchmove", this.touchMoveHandler);
|
|
347
|
+
document.removeEventListener("touchend", this.touchEndHandler);
|
|
348
|
+
if (this.rafId !== null) {
|
|
349
|
+
cancelAnimationFrame(this.rafId);
|
|
350
|
+
this.rafId = null;
|
|
351
|
+
}
|
|
352
|
+
this.isActive = false;
|
|
353
|
+
}
|
|
354
|
+
getVectors() {
|
|
355
|
+
return this.featureVectors;
|
|
356
|
+
}
|
|
357
|
+
getSuspiciousSwipeVelocity() {
|
|
358
|
+
return this.maxSwipeVelocity;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Get enhanced mouse/touch signals
|
|
362
|
+
*/
|
|
363
|
+
getMouseSignals() {
|
|
364
|
+
const averageVelocity = this.featureVectors.length > 0 ? this.featureVectors.reduce((sum, v) => sum + v.velocity, 0) / this.featureVectors.length : 0;
|
|
365
|
+
const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
366
|
+
const scrollDiff = Math.abs(currentScrollTop - this.lastScrollTop);
|
|
367
|
+
const scrollDirection = currentScrollTop > this.lastScrollTop ? "down" : "up";
|
|
368
|
+
return {
|
|
369
|
+
totalDistance: this.totalMouseDistance,
|
|
370
|
+
averageVelocity,
|
|
371
|
+
clickCount: this.clickCount,
|
|
372
|
+
rightClickCount: this.rightClickCount,
|
|
373
|
+
doubleClickCount: this.doubleClickCount,
|
|
374
|
+
scrollDistance: this.scrollDistance + scrollDiff,
|
|
375
|
+
scrollDirection,
|
|
376
|
+
touchEvents: this.touchEventCount
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
reset() {
|
|
380
|
+
this.featureVectors = [];
|
|
381
|
+
this.pointerQueue = [];
|
|
382
|
+
this.lastVelocity = 0;
|
|
383
|
+
this.lastAcceleration = 0;
|
|
384
|
+
this.lastAngle = 0;
|
|
385
|
+
this.lastPoint = null;
|
|
386
|
+
this.secondLastPoint = null;
|
|
387
|
+
this.maxSwipeVelocity = 0;
|
|
388
|
+
this.clickCount = 0;
|
|
389
|
+
this.rightClickCount = 0;
|
|
390
|
+
this.doubleClickCount = 0;
|
|
391
|
+
this.totalMouseDistance = 0;
|
|
392
|
+
this.lastClickTime = 0;
|
|
393
|
+
this.scrollDistance = 0;
|
|
394
|
+
this.lastScrollTop = 0;
|
|
395
|
+
this.touchEventCount = 0;
|
|
396
|
+
}
|
|
397
|
+
enqueuePoint(event, type) {
|
|
398
|
+
const point = {
|
|
399
|
+
x: event.clientX,
|
|
400
|
+
y: event.clientY,
|
|
401
|
+
timestamp: performance.now(),
|
|
402
|
+
type
|
|
403
|
+
};
|
|
404
|
+
this.pointerQueue.push(point);
|
|
405
|
+
if (this.pointerQueue.length > WINDOW_LIMIT) {
|
|
406
|
+
this.pointerQueue.shift();
|
|
407
|
+
}
|
|
408
|
+
if (this.onEvent) {
|
|
409
|
+
this.onEvent(type === "move" ? "mousemove" : type === "down" ? "mousedown" : "mouseup");
|
|
410
|
+
}
|
|
411
|
+
this.scheduleProcess();
|
|
412
|
+
}
|
|
413
|
+
scheduleProcess() {
|
|
414
|
+
if (this.rafId !== null) return;
|
|
415
|
+
this.rafId = requestAnimationFrame(() => {
|
|
416
|
+
this.rafId = null;
|
|
417
|
+
this.processQueue();
|
|
418
|
+
if (this.pointerQueue.length > 0) {
|
|
419
|
+
this.scheduleProcess();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
processQueue() {
|
|
424
|
+
while (this.pointerQueue.length > 0) {
|
|
425
|
+
const point = this.pointerQueue.shift();
|
|
426
|
+
this.computeFeatures(point);
|
|
427
|
+
this.secondLastPoint = this.lastPoint;
|
|
428
|
+
this.lastPoint = point;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
computeFeatures(point) {
|
|
432
|
+
if (!this.lastPoint) {
|
|
433
|
+
this.lastPoint = point;
|
|
434
|
+
this.lastAngle = 0;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const dt = point.timestamp - this.lastPoint.timestamp;
|
|
438
|
+
if (dt <= 0) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const dx = point.x - this.lastPoint.x;
|
|
442
|
+
const dy = point.y - this.lastPoint.y;
|
|
443
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
444
|
+
this.totalMouseDistance += distance;
|
|
445
|
+
const velocity = distance / dt;
|
|
446
|
+
const acceleration = (velocity - this.lastVelocity) / dt;
|
|
447
|
+
const jerk = (acceleration - this.lastAcceleration) / dt;
|
|
448
|
+
const angle = Math.atan2(dy, dx);
|
|
449
|
+
const angularVelocity = this.normalizeAngle(angle - this.lastAngle) / dt;
|
|
450
|
+
const curvature = this.calculateCurvature(point);
|
|
451
|
+
this.featureVectors.push({
|
|
452
|
+
timestamp: point.timestamp,
|
|
453
|
+
velocity,
|
|
454
|
+
acceleration,
|
|
455
|
+
jerk,
|
|
456
|
+
curvature,
|
|
457
|
+
angularVelocity
|
|
458
|
+
});
|
|
459
|
+
this.maxSwipeVelocity = Math.max(this.maxSwipeVelocity, velocity);
|
|
460
|
+
if (velocity > VELOCITY_SPIKE_THRESHOLD) {
|
|
461
|
+
this.featureVectors.push({
|
|
462
|
+
timestamp: point.timestamp,
|
|
463
|
+
velocity,
|
|
464
|
+
acceleration,
|
|
465
|
+
jerk,
|
|
466
|
+
curvature,
|
|
467
|
+
angularVelocity: angularVelocity * 1.2
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (this.featureVectors.length > WINDOW_LIMIT) {
|
|
471
|
+
this.featureVectors.splice(0, this.featureVectors.length - WINDOW_LIMIT);
|
|
472
|
+
}
|
|
473
|
+
this.lastVelocity = velocity;
|
|
474
|
+
this.lastAcceleration = acceleration;
|
|
475
|
+
this.lastAngle = angle;
|
|
476
|
+
}
|
|
477
|
+
calculateCurvature(point) {
|
|
478
|
+
if (!this.lastPoint || !this.secondLastPoint) return 0;
|
|
479
|
+
const p0 = this.secondLastPoint;
|
|
480
|
+
const p1 = this.lastPoint;
|
|
481
|
+
const p2 = point;
|
|
482
|
+
const chordLength = Math.hypot(p2.x - p0.x, p2.y - p0.y);
|
|
483
|
+
const pathLength = Math.hypot(p1.x - p0.x, p1.y - p0.y) + Math.hypot(p2.x - p1.x, p2.y - p1.y);
|
|
484
|
+
if (chordLength === 0) return 0;
|
|
485
|
+
return (pathLength - chordLength) / chordLength;
|
|
486
|
+
}
|
|
487
|
+
normalizeAngle(angleDiff) {
|
|
488
|
+
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
|
|
489
|
+
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
|
|
490
|
+
return angleDiff;
|
|
491
|
+
}
|
|
492
|
+
handleClick(_event) {
|
|
493
|
+
const now = performance.now();
|
|
494
|
+
if (now - this.lastClickTime < 500) {
|
|
495
|
+
this.doubleClickCount++;
|
|
496
|
+
} else {
|
|
497
|
+
this.clickCount++;
|
|
498
|
+
}
|
|
499
|
+
this.lastClickTime = now;
|
|
500
|
+
}
|
|
501
|
+
handleRightClick(_event) {
|
|
502
|
+
this.rightClickCount++;
|
|
503
|
+
}
|
|
504
|
+
handleScroll() {
|
|
505
|
+
const currentScrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
506
|
+
const scrollDiff = Math.abs(currentScrollTop - this.lastScrollTop);
|
|
507
|
+
this.scrollDistance += scrollDiff;
|
|
508
|
+
this.lastScrollTop = currentScrollTop;
|
|
509
|
+
}
|
|
510
|
+
handleTouch() {
|
|
511
|
+
this.touchEventCount++;
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// ../fraud-sdk/src/collectors/keystroke-monitor.ts
|
|
516
|
+
var MAX_BUFFER = 200;
|
|
517
|
+
var KeystrokeMonitor = class {
|
|
518
|
+
constructor(options = {}) {
|
|
519
|
+
this.keyDownTimes = /* @__PURE__ */ new Map();
|
|
520
|
+
this.lastKeyUpTime = null;
|
|
521
|
+
this.lastKeyDownTime = null;
|
|
522
|
+
this.featureVectors = [];
|
|
523
|
+
this.dwellTimes = [];
|
|
524
|
+
this.flightTimes = [];
|
|
525
|
+
this.digraphLatencies = [];
|
|
526
|
+
this.isActive = false;
|
|
527
|
+
// Enhanced tracking
|
|
528
|
+
this.keydownCount = 0;
|
|
529
|
+
this.keyupCount = 0;
|
|
530
|
+
this.specialKeyCount = 0;
|
|
531
|
+
this.lastKeystrokeTime = null;
|
|
532
|
+
this.pauseCount = 0;
|
|
533
|
+
this.PAUSE_THRESHOLD = 2e3;
|
|
534
|
+
// 2 seconds
|
|
535
|
+
this.keystrokeTimestamps = [];
|
|
536
|
+
this.keyDownHandler = (event) => this.handleKeyDown(event);
|
|
537
|
+
this.keyUpHandler = (event) => this.handleKeyUp(event);
|
|
538
|
+
this.onEvent = options.onEvent;
|
|
539
|
+
}
|
|
540
|
+
start() {
|
|
541
|
+
if (this.isActive || typeof document === "undefined") return;
|
|
542
|
+
document.addEventListener("keydown", this.keyDownHandler, { passive: true, capture: true });
|
|
543
|
+
document.addEventListener("keyup", this.keyUpHandler, { passive: true, capture: true });
|
|
544
|
+
this.isActive = true;
|
|
545
|
+
}
|
|
546
|
+
stop() {
|
|
547
|
+
if (!this.isActive || typeof document === "undefined") return;
|
|
548
|
+
document.removeEventListener("keydown", this.keyDownHandler, true);
|
|
549
|
+
document.removeEventListener("keyup", this.keyUpHandler, true);
|
|
550
|
+
this.isActive = false;
|
|
551
|
+
}
|
|
552
|
+
getVectors() {
|
|
553
|
+
return this.featureVectors;
|
|
554
|
+
}
|
|
555
|
+
getDwellTimes(limit = 50) {
|
|
556
|
+
return this.dwellTimes.slice(-limit);
|
|
557
|
+
}
|
|
558
|
+
getFlightTimes(limit = 50) {
|
|
559
|
+
return this.flightTimes.slice(-limit);
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get enhanced keyboard signals
|
|
563
|
+
*/
|
|
564
|
+
getKeyboardSignals() {
|
|
565
|
+
let typingSpeed = 0;
|
|
566
|
+
if (this.keystrokeTimestamps.length > 1) {
|
|
567
|
+
const timeSpan = this.keystrokeTimestamps[this.keystrokeTimestamps.length - 1] - this.keystrokeTimestamps[0];
|
|
568
|
+
const minutes = timeSpan / 6e4;
|
|
569
|
+
if (minutes > 0) {
|
|
570
|
+
typingSpeed = this.keystrokeTimestamps.length / minutes;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const totalKeystrokes = this.keydownCount;
|
|
574
|
+
const backspaceRatio = totalKeystrokes > 0 ? this.getBackspaceCount() / totalKeystrokes : 0;
|
|
575
|
+
return {
|
|
576
|
+
keydownCount: this.keydownCount,
|
|
577
|
+
keyupCount: this.keyupCount,
|
|
578
|
+
specialKeyCount: this.specialKeyCount,
|
|
579
|
+
typingSpeed,
|
|
580
|
+
pauseCount: this.pauseCount,
|
|
581
|
+
backspaceRatio
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
getBackspaceCount() {
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
587
|
+
reset() {
|
|
588
|
+
this.keyDownTimes.clear();
|
|
589
|
+
this.lastKeyUpTime = null;
|
|
590
|
+
this.lastKeyDownTime = null;
|
|
591
|
+
this.featureVectors = [];
|
|
592
|
+
this.dwellTimes = [];
|
|
593
|
+
this.flightTimes = [];
|
|
594
|
+
this.digraphLatencies = [];
|
|
595
|
+
this.keydownCount = 0;
|
|
596
|
+
this.keyupCount = 0;
|
|
597
|
+
this.specialKeyCount = 0;
|
|
598
|
+
this.lastKeystrokeTime = null;
|
|
599
|
+
this.pauseCount = 0;
|
|
600
|
+
this.keystrokeTimestamps = [];
|
|
601
|
+
}
|
|
602
|
+
handleKeyDown(event) {
|
|
603
|
+
if (!this.isTargetField(event)) return;
|
|
604
|
+
const now = performance.now();
|
|
605
|
+
this.keydownCount++;
|
|
606
|
+
this.keystrokeTimestamps.push(now);
|
|
607
|
+
if (event.ctrlKey || event.altKey || event.shiftKey || event.metaKey) {
|
|
608
|
+
this.specialKeyCount++;
|
|
609
|
+
}
|
|
610
|
+
if (this.lastKeystrokeTime !== null && now - this.lastKeystrokeTime > this.PAUSE_THRESHOLD) {
|
|
611
|
+
this.pauseCount++;
|
|
612
|
+
}
|
|
613
|
+
this.lastKeystrokeTime = now;
|
|
614
|
+
if (this.lastKeyUpTime !== null) {
|
|
615
|
+
const flight = now - this.lastKeyUpTime;
|
|
616
|
+
this.flightTimes.push(flight);
|
|
617
|
+
this.appendVector({ dwellTime: -1, flightTime: flight, digraphLatency: -1, timestamp: now });
|
|
618
|
+
}
|
|
619
|
+
if (this.lastKeyDownTime !== null) {
|
|
620
|
+
const digraphLatency = now - this.lastKeyDownTime;
|
|
621
|
+
this.digraphLatencies.push(digraphLatency);
|
|
622
|
+
this.appendVector({ dwellTime: -1, flightTime: -1, digraphLatency, timestamp: now });
|
|
623
|
+
}
|
|
624
|
+
this.lastKeyDownTime = now;
|
|
625
|
+
this.keyDownTimes.set(event.code, now);
|
|
626
|
+
if (this.onEvent) this.onEvent("keydown");
|
|
627
|
+
}
|
|
628
|
+
handleKeyUp(event) {
|
|
629
|
+
if (!this.isTargetField(event)) return;
|
|
630
|
+
const now = performance.now();
|
|
631
|
+
this.keyupCount++;
|
|
632
|
+
const start = this.keyDownTimes.get(event.code);
|
|
633
|
+
if (start !== void 0) {
|
|
634
|
+
const dwell = now - start;
|
|
635
|
+
this.dwellTimes.push(dwell);
|
|
636
|
+
this.appendVector({ dwellTime: dwell, flightTime: -1, digraphLatency: -1, timestamp: now });
|
|
637
|
+
this.keyDownTimes.delete(event.code);
|
|
638
|
+
}
|
|
639
|
+
this.lastKeyUpTime = now;
|
|
640
|
+
if (this.onEvent) this.onEvent("keyup");
|
|
641
|
+
}
|
|
642
|
+
appendVector(vector) {
|
|
643
|
+
this.featureVectors.push(vector);
|
|
644
|
+
if (this.featureVectors.length > MAX_BUFFER) {
|
|
645
|
+
this.featureVectors.splice(0, this.featureVectors.length - MAX_BUFFER);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
isTargetField(event) {
|
|
649
|
+
const target = event.target;
|
|
650
|
+
if (!target) return false;
|
|
651
|
+
const isEditable = target.isContentEditable || ["INPUT", "TEXTAREA"].includes(target.tagName);
|
|
652
|
+
return isEditable;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// ../fraud-sdk/src/collectors/behavioral.ts
|
|
657
|
+
var WINDOW_SIZE = 50;
|
|
658
|
+
var MASK_VALUE = -1;
|
|
659
|
+
var FEATURE_NAMES = [
|
|
660
|
+
"velocity",
|
|
661
|
+
"acceleration",
|
|
662
|
+
"jerk",
|
|
663
|
+
"curvature",
|
|
664
|
+
"angularVelocity",
|
|
665
|
+
"dwell",
|
|
666
|
+
"flight",
|
|
667
|
+
"digraphLatency"
|
|
668
|
+
];
|
|
669
|
+
var BehavioralCollector = class {
|
|
670
|
+
constructor() {
|
|
671
|
+
this.isActive = false;
|
|
672
|
+
this.sessionEvents = [];
|
|
673
|
+
this.trackEvent = (event) => {
|
|
674
|
+
this.sessionEvents.push({ type: event, timestamp: performance.now() });
|
|
675
|
+
if (this.sessionEvents.length > 500) {
|
|
676
|
+
this.sessionEvents.shift();
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
this.kinematicEngine = new KinematicEngine({ onEvent: this.trackEvent });
|
|
680
|
+
this.keystrokeMonitor = new KeystrokeMonitor({ onEvent: this.trackEvent });
|
|
681
|
+
}
|
|
682
|
+
start() {
|
|
683
|
+
if (this.isActive) return;
|
|
684
|
+
this.kinematicEngine.start();
|
|
685
|
+
this.keystrokeMonitor.start();
|
|
686
|
+
this.isActive = true;
|
|
687
|
+
}
|
|
688
|
+
stop() {
|
|
689
|
+
if (!this.isActive) return;
|
|
690
|
+
this.kinematicEngine.stop();
|
|
691
|
+
this.keystrokeMonitor.stop();
|
|
692
|
+
this.isActive = false;
|
|
693
|
+
}
|
|
694
|
+
reset() {
|
|
695
|
+
this.sessionEvents = [];
|
|
696
|
+
this.kinematicEngine.reset();
|
|
697
|
+
this.keystrokeMonitor.reset();
|
|
698
|
+
}
|
|
699
|
+
getData() {
|
|
700
|
+
const kinematicVectors = this.kinematicEngine.getVectors();
|
|
701
|
+
const keystrokeVectors = this.keystrokeMonitor.getVectors();
|
|
702
|
+
const merged = [
|
|
703
|
+
...kinematicVectors.map((v) => ({
|
|
704
|
+
timestamp: v.timestamp,
|
|
705
|
+
values: [
|
|
706
|
+
v.velocity,
|
|
707
|
+
v.acceleration,
|
|
708
|
+
v.jerk,
|
|
709
|
+
v.curvature,
|
|
710
|
+
v.angularVelocity,
|
|
711
|
+
MASK_VALUE,
|
|
712
|
+
MASK_VALUE,
|
|
713
|
+
MASK_VALUE
|
|
714
|
+
]
|
|
715
|
+
})),
|
|
716
|
+
...keystrokeVectors.map((v) => ({
|
|
717
|
+
timestamp: v.timestamp,
|
|
718
|
+
values: [
|
|
719
|
+
MASK_VALUE,
|
|
720
|
+
MASK_VALUE,
|
|
721
|
+
MASK_VALUE,
|
|
722
|
+
MASK_VALUE,
|
|
723
|
+
MASK_VALUE,
|
|
724
|
+
v.dwellTime,
|
|
725
|
+
v.flightTime,
|
|
726
|
+
v.digraphLatency
|
|
727
|
+
]
|
|
728
|
+
}))
|
|
729
|
+
].sort((a, b) => a.timestamp - b.timestamp);
|
|
730
|
+
const sequence = this.buildSequence(merged);
|
|
731
|
+
const suspiciousSwipeVelocity = this.kinematicEngine.getSuspiciousSwipeVelocity();
|
|
732
|
+
const entropy = this.calculateSessionEntropy();
|
|
733
|
+
const dwellTimes = this.keystrokeMonitor.getDwellTimes();
|
|
734
|
+
const flightTimes = this.keystrokeMonitor.getFlightTimes();
|
|
735
|
+
return {
|
|
736
|
+
typing_dwell_ms: dwellTimes,
|
|
737
|
+
typing_flight_ms: flightTimes,
|
|
738
|
+
swipe_velocity: suspiciousSwipeVelocity,
|
|
739
|
+
suspicious_swipe_velocity: suspiciousSwipeVelocity,
|
|
740
|
+
session_entropy: entropy,
|
|
741
|
+
behavioral_vectors: sequence
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
buildSequence(vectors) {
|
|
745
|
+
const padded = [];
|
|
746
|
+
const startIndex = Math.max(0, vectors.length - WINDOW_SIZE);
|
|
747
|
+
const window2 = vectors.slice(startIndex);
|
|
748
|
+
for (const vector of window2) {
|
|
749
|
+
padded.push(vector.values);
|
|
750
|
+
}
|
|
751
|
+
while (padded.length < WINDOW_SIZE) {
|
|
752
|
+
padded.unshift(new Array(FEATURE_NAMES.length).fill(MASK_VALUE));
|
|
753
|
+
}
|
|
754
|
+
return {
|
|
755
|
+
featureNames: [...FEATURE_NAMES],
|
|
756
|
+
windowSize: WINDOW_SIZE,
|
|
757
|
+
maskValue: MASK_VALUE,
|
|
758
|
+
sequence: padded
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
calculateSessionEntropy() {
|
|
762
|
+
if (this.sessionEvents.length === 0) return 0;
|
|
763
|
+
const counts = {};
|
|
764
|
+
this.sessionEvents.forEach((evt) => {
|
|
765
|
+
counts[evt.type] = (counts[evt.type] || 0) + 1;
|
|
766
|
+
});
|
|
767
|
+
const total = this.sessionEvents.length;
|
|
768
|
+
return Object.values(counts).reduce((entropy, count) => {
|
|
769
|
+
const probability = count / total;
|
|
770
|
+
return probability > 0 ? entropy - probability * Math.log2(probability) : entropy;
|
|
771
|
+
}, 0);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Get mouse signals from kinematic engine
|
|
775
|
+
*/
|
|
776
|
+
getMouseSignals() {
|
|
777
|
+
return this.kinematicEngine.getMouseSignals();
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Get keyboard signals from keystroke monitor
|
|
781
|
+
*/
|
|
782
|
+
getKeyboardSignals() {
|
|
783
|
+
return this.keystrokeMonitor.getKeyboardSignals();
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// ../fraud-sdk/src/collectors/canvas-fingerprint.ts
|
|
788
|
+
var CanvasFingerprintCollector = class {
|
|
789
|
+
constructor() {
|
|
790
|
+
this.cachedFingerprint = null;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Generate canvas fingerprint
|
|
794
|
+
* Returns null if canvas is not available
|
|
795
|
+
*/
|
|
796
|
+
collect() {
|
|
797
|
+
if (this.cachedFingerprint) {
|
|
798
|
+
return this.cachedFingerprint;
|
|
799
|
+
}
|
|
800
|
+
const startTime = performance.now();
|
|
801
|
+
try {
|
|
802
|
+
const canvas = document.createElement("canvas");
|
|
803
|
+
canvas.width = 200;
|
|
804
|
+
canvas.height = 50;
|
|
805
|
+
const ctx = canvas.getContext("2d");
|
|
806
|
+
if (!ctx) {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
ctx.textBaseline = "top";
|
|
810
|
+
ctx.font = "14px Arial";
|
|
811
|
+
ctx.textBaseline = "alphabetic";
|
|
812
|
+
ctx.fillStyle = "#f60";
|
|
813
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
814
|
+
ctx.fillStyle = "#069";
|
|
815
|
+
ctx.fillText("KeverdFingerprint", 2, 15);
|
|
816
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
817
|
+
ctx.fillText("KeverdFingerprint", 4, 17);
|
|
818
|
+
const gradient = ctx.createLinearGradient(0, 0, 200, 50);
|
|
819
|
+
gradient.addColorStop(0, "rgba(255, 0, 0, 0.5)");
|
|
820
|
+
gradient.addColorStop(1, "rgba(0, 0, 255, 0.5)");
|
|
821
|
+
ctx.fillStyle = gradient;
|
|
822
|
+
ctx.fillRect(0, 0, 200, 50);
|
|
823
|
+
ctx.strokeStyle = "#000";
|
|
824
|
+
ctx.lineWidth = 2;
|
|
825
|
+
ctx.beginPath();
|
|
826
|
+
ctx.arc(100, 25, 20, 0, Math.PI * 2);
|
|
827
|
+
ctx.stroke();
|
|
828
|
+
ctx.font = "12px Verdana";
|
|
829
|
+
ctx.fillStyle = "#000";
|
|
830
|
+
ctx.fillText("CanvasFP", 150, 30);
|
|
831
|
+
const dataURL = canvas.toDataURL();
|
|
832
|
+
const hash = this.hashString(dataURL);
|
|
833
|
+
const duration = performance.now() - startTime;
|
|
834
|
+
this.cachedFingerprint = {
|
|
835
|
+
value: hash,
|
|
836
|
+
duration: Math.round(duration * 100) / 100
|
|
837
|
+
};
|
|
838
|
+
return this.cachedFingerprint;
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.warn("[Keverd SDK] Canvas fingerprinting failed:", error);
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Hash a string to create a stable fingerprint
|
|
846
|
+
*/
|
|
847
|
+
hashString(str) {
|
|
848
|
+
let hash = 0;
|
|
849
|
+
for (let i = 0; i < str.length; i++) {
|
|
850
|
+
const char = str.charCodeAt(i);
|
|
851
|
+
hash = (hash << 5) - hash + char;
|
|
852
|
+
hash = hash & hash;
|
|
853
|
+
}
|
|
854
|
+
return Math.abs(hash).toString(16).padStart(8, "0");
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Clear cached fingerprint
|
|
858
|
+
*/
|
|
859
|
+
clearCache() {
|
|
860
|
+
this.cachedFingerprint = null;
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
// ../fraud-sdk/src/collectors/webgl-fingerprint.ts
|
|
865
|
+
var WebGLFingerprintCollector = class {
|
|
866
|
+
constructor() {
|
|
867
|
+
this.cachedFingerprint = null;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Generate WebGL fingerprint
|
|
871
|
+
* Returns null if WebGL is not available
|
|
872
|
+
*/
|
|
873
|
+
collect() {
|
|
874
|
+
if (this.cachedFingerprint) {
|
|
875
|
+
return this.cachedFingerprint;
|
|
876
|
+
}
|
|
877
|
+
const startTime = performance.now();
|
|
878
|
+
try {
|
|
879
|
+
const canvas = document.createElement("canvas");
|
|
880
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
881
|
+
if (!gl) {
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
885
|
+
let vendor = null;
|
|
886
|
+
let renderer = null;
|
|
887
|
+
if (debugInfo) {
|
|
888
|
+
vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || null;
|
|
889
|
+
renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || null;
|
|
890
|
+
} else {
|
|
891
|
+
vendor = gl.getParameter(gl.VENDOR) || null;
|
|
892
|
+
renderer = gl.getParameter(gl.RENDERER) || null;
|
|
893
|
+
}
|
|
894
|
+
const version = gl.getParameter(gl.VERSION) || null;
|
|
895
|
+
const shadingLanguageVersion = gl.getParameter(gl.SHADING_LANGUAGE_VERSION) || null;
|
|
896
|
+
const extensions = [];
|
|
897
|
+
try {
|
|
898
|
+
const supportedExtensions = gl.getSupportedExtensions();
|
|
899
|
+
if (supportedExtensions) {
|
|
900
|
+
extensions.push(...supportedExtensions);
|
|
901
|
+
}
|
|
902
|
+
} catch (e) {
|
|
903
|
+
}
|
|
904
|
+
const parameters = {
|
|
905
|
+
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
|
|
906
|
+
maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
|
|
907
|
+
maxViewportDims: gl.getParameter(gl.MAX_VIEWPORT_DIMS),
|
|
908
|
+
maxTextureImageUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
|
|
909
|
+
maxCombinedTextureImageUnits: gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS),
|
|
910
|
+
maxFragmentUniformVectors: gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS),
|
|
911
|
+
maxVaryingVectors: gl.getParameter(gl.MAX_VARYING_VECTORS),
|
|
912
|
+
aliasedLineWidthRange: gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE),
|
|
913
|
+
aliasedPointSizeRange: gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE)
|
|
914
|
+
};
|
|
915
|
+
const precision = {};
|
|
916
|
+
try {
|
|
917
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
918
|
+
if (vertexShader) {
|
|
919
|
+
gl.shaderSource(vertexShader, "precision mediump float;");
|
|
920
|
+
gl.compileShader(vertexShader);
|
|
921
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
922
|
+
if (fragmentShader) {
|
|
923
|
+
gl.shaderSource(fragmentShader, "precision mediump float;");
|
|
924
|
+
gl.compileShader(fragmentShader);
|
|
925
|
+
precision.float = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT)?.precision?.toString() || "unknown";
|
|
926
|
+
precision.int = gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_INT)?.precision?.toString() || "unknown";
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
} catch (e) {
|
|
930
|
+
}
|
|
931
|
+
const duration = performance.now() - startTime;
|
|
932
|
+
this.cachedFingerprint = {
|
|
933
|
+
vendor,
|
|
934
|
+
renderer,
|
|
935
|
+
version,
|
|
936
|
+
shadingLanguageVersion,
|
|
937
|
+
extensions: extensions.sort(),
|
|
938
|
+
// Sort for consistency
|
|
939
|
+
parameters,
|
|
940
|
+
precision,
|
|
941
|
+
duration: Math.round(duration * 100) / 100
|
|
942
|
+
};
|
|
943
|
+
return this.cachedFingerprint;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
console.warn("[Keverd SDK] WebGL fingerprinting failed:", error);
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Clear cached fingerprint
|
|
951
|
+
*/
|
|
952
|
+
clearCache() {
|
|
953
|
+
this.cachedFingerprint = null;
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// ../fraud-sdk/src/collectors/audio-fingerprint.ts
|
|
958
|
+
var AudioFingerprintCollector = class {
|
|
959
|
+
constructor() {
|
|
960
|
+
this.cachedFingerprint = null;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Generate audio fingerprint
|
|
964
|
+
* Returns null if audio context is not available
|
|
965
|
+
*/
|
|
966
|
+
async collect() {
|
|
967
|
+
if (this.cachedFingerprint) {
|
|
968
|
+
return this.cachedFingerprint;
|
|
969
|
+
}
|
|
970
|
+
const startTime = performance.now();
|
|
971
|
+
try {
|
|
972
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext;
|
|
973
|
+
if (!AudioContext) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
const context = new AudioContext();
|
|
977
|
+
const oscillator = context.createOscillator();
|
|
978
|
+
const analyser = context.createAnalyser();
|
|
979
|
+
const gainNode = context.createGain();
|
|
980
|
+
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
|
|
981
|
+
analyser.fftSize = 2048;
|
|
982
|
+
analyser.smoothingTimeConstant = 0.8;
|
|
983
|
+
oscillator.connect(analyser);
|
|
984
|
+
analyser.connect(scriptProcessor);
|
|
985
|
+
scriptProcessor.connect(gainNode);
|
|
986
|
+
gainNode.connect(context.destination);
|
|
987
|
+
oscillator.type = "triangle";
|
|
988
|
+
oscillator.frequency.value = 1e4;
|
|
989
|
+
return new Promise((resolve) => {
|
|
990
|
+
const audioData = [];
|
|
991
|
+
let sampleCount = 0;
|
|
992
|
+
const maxSamples = 5;
|
|
993
|
+
scriptProcessor.onaudioprocess = (event) => {
|
|
994
|
+
const inputBuffer = event.inputBuffer;
|
|
995
|
+
const inputData = inputBuffer.getChannelData(0);
|
|
996
|
+
let sum = 0;
|
|
997
|
+
for (let i = 0; i < inputData.length; i++) {
|
|
998
|
+
sum += inputData[i] * inputData[i];
|
|
999
|
+
}
|
|
1000
|
+
const rms = Math.sqrt(sum / inputData.length);
|
|
1001
|
+
audioData.push(rms);
|
|
1002
|
+
sampleCount++;
|
|
1003
|
+
if (sampleCount >= maxSamples) {
|
|
1004
|
+
oscillator.stop();
|
|
1005
|
+
context.close();
|
|
1006
|
+
const fingerprint = this.calculateFingerprint(audioData);
|
|
1007
|
+
const duration = performance.now() - startTime;
|
|
1008
|
+
this.cachedFingerprint = {
|
|
1009
|
+
value: fingerprint,
|
|
1010
|
+
duration: Math.round(duration * 100) / 100
|
|
1011
|
+
};
|
|
1012
|
+
resolve(this.cachedFingerprint);
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
oscillator.start(0);
|
|
1016
|
+
gainNode.gain.value = 0;
|
|
1017
|
+
setTimeout(() => {
|
|
1018
|
+
if (!this.cachedFingerprint) {
|
|
1019
|
+
oscillator.stop();
|
|
1020
|
+
context.close();
|
|
1021
|
+
const fallbackFingerprint = this.getFallbackFingerprint(context);
|
|
1022
|
+
const duration = performance.now() - startTime;
|
|
1023
|
+
this.cachedFingerprint = {
|
|
1024
|
+
value: fallbackFingerprint,
|
|
1025
|
+
duration: Math.round(duration * 100) / 100
|
|
1026
|
+
};
|
|
1027
|
+
resolve(this.cachedFingerprint);
|
|
1028
|
+
}
|
|
1029
|
+
}, 1e3);
|
|
1030
|
+
});
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
console.warn("[Keverd SDK] Audio fingerprinting failed:", error);
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Calculate fingerprint from audio data
|
|
1038
|
+
*/
|
|
1039
|
+
calculateFingerprint(audioData) {
|
|
1040
|
+
if (audioData.length === 0) {
|
|
1041
|
+
return 0;
|
|
1042
|
+
}
|
|
1043
|
+
const sum = audioData.reduce((a, b) => a + b, 0);
|
|
1044
|
+
const mean = sum / audioData.length;
|
|
1045
|
+
const variance = audioData.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / audioData.length;
|
|
1046
|
+
const stdDev = Math.sqrt(variance);
|
|
1047
|
+
const fingerprint = mean * 1e6 + stdDev * 1e3 + audioData.length;
|
|
1048
|
+
return Math.round(fingerprint * 100) / 100;
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Get fallback fingerprint when audio processing fails
|
|
1052
|
+
*/
|
|
1053
|
+
getFallbackFingerprint(context) {
|
|
1054
|
+
const sampleRate = context.sampleRate || 44100;
|
|
1055
|
+
const state = context.state || "unknown";
|
|
1056
|
+
const hash = (sampleRate.toString() + state).split("").reduce((acc, char) => {
|
|
1057
|
+
return (acc << 5) - acc + char.charCodeAt(0);
|
|
1058
|
+
}, 0);
|
|
1059
|
+
return Math.abs(hash);
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Clear cached fingerprint
|
|
1063
|
+
*/
|
|
1064
|
+
clearCache() {
|
|
1065
|
+
this.cachedFingerprint = null;
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
// ../fraud-sdk/src/collectors/font-detection.ts
|
|
1070
|
+
var FontDetectionCollector = class {
|
|
1071
|
+
constructor() {
|
|
1072
|
+
this.cachedFingerprint = null;
|
|
1073
|
+
// Common fonts to test
|
|
1074
|
+
this.testFonts = [
|
|
1075
|
+
"Arial",
|
|
1076
|
+
"Verdana",
|
|
1077
|
+
"Times New Roman",
|
|
1078
|
+
"Courier New",
|
|
1079
|
+
"Georgia",
|
|
1080
|
+
"Palatino",
|
|
1081
|
+
"Garamond",
|
|
1082
|
+
"Bookman",
|
|
1083
|
+
"Comic Sans MS",
|
|
1084
|
+
"Trebuchet MS",
|
|
1085
|
+
"Arial Black",
|
|
1086
|
+
"Impact",
|
|
1087
|
+
"Tahoma",
|
|
1088
|
+
"Lucida Console",
|
|
1089
|
+
"Lucida Sans Unicode",
|
|
1090
|
+
"MS Sans Serif",
|
|
1091
|
+
"MS Serif",
|
|
1092
|
+
"Symbol",
|
|
1093
|
+
"Webdings",
|
|
1094
|
+
"Wingdings",
|
|
1095
|
+
"Monaco",
|
|
1096
|
+
"Menlo",
|
|
1097
|
+
"Consolas",
|
|
1098
|
+
"Courier",
|
|
1099
|
+
"Helvetica",
|
|
1100
|
+
"Helvetica Neue",
|
|
1101
|
+
"Roboto",
|
|
1102
|
+
"Open Sans",
|
|
1103
|
+
"Lato",
|
|
1104
|
+
"Montserrat"
|
|
1105
|
+
];
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Detect available fonts
|
|
1109
|
+
* Returns null if detection fails
|
|
1110
|
+
*/
|
|
1111
|
+
collect() {
|
|
1112
|
+
if (this.cachedFingerprint) {
|
|
1113
|
+
return this.cachedFingerprint;
|
|
1114
|
+
}
|
|
1115
|
+
const startTime = performance.now();
|
|
1116
|
+
try {
|
|
1117
|
+
const canvas = document.createElement("canvas");
|
|
1118
|
+
const ctx = canvas.getContext("2d");
|
|
1119
|
+
if (!ctx) {
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
const baseFont = "monospace";
|
|
1123
|
+
const testString = "mmmmmmmmmmlli";
|
|
1124
|
+
const testSize = "72px";
|
|
1125
|
+
ctx.font = `${testSize} ${baseFont}`;
|
|
1126
|
+
const baseWidth = ctx.measureText(testString).width;
|
|
1127
|
+
const availableFonts = [];
|
|
1128
|
+
const missingFonts = [];
|
|
1129
|
+
for (const font of this.testFonts) {
|
|
1130
|
+
ctx.font = `${testSize} ${font}, ${baseFont}`;
|
|
1131
|
+
const width = ctx.measureText(testString).width;
|
|
1132
|
+
if (Math.abs(width - baseWidth) > 1) {
|
|
1133
|
+
availableFonts.push(font);
|
|
1134
|
+
} else {
|
|
1135
|
+
missingFonts.push(font);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
const duration = performance.now() - startTime;
|
|
1139
|
+
this.cachedFingerprint = {
|
|
1140
|
+
availableFonts: availableFonts.sort(),
|
|
1141
|
+
missingFonts: missingFonts.sort(),
|
|
1142
|
+
duration: Math.round(duration * 100) / 100
|
|
1143
|
+
};
|
|
1144
|
+
return this.cachedFingerprint;
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
console.warn("[Keverd SDK] Font detection failed:", error);
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Clear cached fingerprint
|
|
1152
|
+
*/
|
|
1153
|
+
clearCache() {
|
|
1154
|
+
this.cachedFingerprint = null;
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
|
|
1158
|
+
// ../fraud-sdk/src/collectors/media-devices.ts
|
|
1159
|
+
var MediaDevicesCollector = class {
|
|
1160
|
+
constructor() {
|
|
1161
|
+
this.cachedFingerprint = null;
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Collect media device information
|
|
1165
|
+
* Returns null if MediaDevices API is not available
|
|
1166
|
+
*/
|
|
1167
|
+
async collect() {
|
|
1168
|
+
if (this.cachedFingerprint) {
|
|
1169
|
+
return this.cachedFingerprint;
|
|
1170
|
+
}
|
|
1171
|
+
const startTime = performance.now();
|
|
1172
|
+
try {
|
|
1173
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
let hasPermission = false;
|
|
1177
|
+
try {
|
|
1178
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
|
1179
|
+
hasPermission = true;
|
|
1180
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
hasPermission = false;
|
|
1183
|
+
}
|
|
1184
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
1185
|
+
const audioInputs = [];
|
|
1186
|
+
const audioOutputs = [];
|
|
1187
|
+
const videoInputs = [];
|
|
1188
|
+
devices.forEach((device) => {
|
|
1189
|
+
const sanitizedDevice = {
|
|
1190
|
+
deviceId: device.deviceId,
|
|
1191
|
+
kind: device.kind,
|
|
1192
|
+
label: hasPermission ? device.label : "",
|
|
1193
|
+
// Only include label if permission granted
|
|
1194
|
+
groupId: device.groupId,
|
|
1195
|
+
toJSON: () => ({
|
|
1196
|
+
deviceId: device.deviceId,
|
|
1197
|
+
kind: device.kind,
|
|
1198
|
+
label: hasPermission ? device.label : "",
|
|
1199
|
+
groupId: device.groupId
|
|
1200
|
+
})
|
|
1201
|
+
};
|
|
1202
|
+
switch (device.kind) {
|
|
1203
|
+
case "audioinput":
|
|
1204
|
+
audioInputs.push(sanitizedDevice);
|
|
1205
|
+
break;
|
|
1206
|
+
case "audiooutput":
|
|
1207
|
+
audioOutputs.push(sanitizedDevice);
|
|
1208
|
+
break;
|
|
1209
|
+
case "videoinput":
|
|
1210
|
+
videoInputs.push(sanitizedDevice);
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
const duration = performance.now() - startTime;
|
|
1215
|
+
this.cachedFingerprint = {
|
|
1216
|
+
audioInputs,
|
|
1217
|
+
audioOutputs,
|
|
1218
|
+
videoInputs,
|
|
1219
|
+
hasPermission,
|
|
1220
|
+
duration: Math.round(duration * 100) / 100
|
|
1221
|
+
};
|
|
1222
|
+
return this.cachedFingerprint;
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
console.warn("[Keverd SDK] Media devices enumeration failed:", error);
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Clear cached fingerprint
|
|
1230
|
+
*/
|
|
1231
|
+
clearCache() {
|
|
1232
|
+
this.cachedFingerprint = null;
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
// ../fraud-sdk/src/collectors/screen-display.ts
|
|
1237
|
+
var ScreenDisplayCollector = class {
|
|
1238
|
+
constructor() {
|
|
1239
|
+
this.cachedFingerprint = null;
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Collect screen and display information
|
|
1243
|
+
*/
|
|
1244
|
+
collect() {
|
|
1245
|
+
if (this.cachedFingerprint) {
|
|
1246
|
+
return this.cachedFingerprint;
|
|
1247
|
+
}
|
|
1248
|
+
const startTime = performance.now();
|
|
1249
|
+
const fingerprint = {
|
|
1250
|
+
width: screen.width,
|
|
1251
|
+
height: screen.height,
|
|
1252
|
+
availWidth: screen.availWidth,
|
|
1253
|
+
availHeight: screen.availHeight,
|
|
1254
|
+
colorDepth: screen.colorDepth,
|
|
1255
|
+
pixelDepth: screen.pixelDepth,
|
|
1256
|
+
pixelRatio: window.devicePixelRatio || 1,
|
|
1257
|
+
orientation: this.getOrientation(),
|
|
1258
|
+
orientationAngle: this.getOrientationAngle(),
|
|
1259
|
+
duration: 0
|
|
1260
|
+
};
|
|
1261
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1262
|
+
this.cachedFingerprint = fingerprint;
|
|
1263
|
+
return fingerprint;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Get screen orientation
|
|
1267
|
+
*/
|
|
1268
|
+
getOrientation() {
|
|
1269
|
+
if ("orientation" in screen) {
|
|
1270
|
+
const orientation = screen.orientation;
|
|
1271
|
+
if (orientation && orientation.type) {
|
|
1272
|
+
return orientation.type;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
if ("orientation" in window) {
|
|
1276
|
+
const angle = window.orientation;
|
|
1277
|
+
if (angle !== void 0) {
|
|
1278
|
+
if (angle === 0 || angle === 180) {
|
|
1279
|
+
return "portrait";
|
|
1280
|
+
} else if (angle === 90 || angle === -90) {
|
|
1281
|
+
return "landscape";
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (window.matchMedia) {
|
|
1286
|
+
if (window.matchMedia("(orientation: portrait)").matches) {
|
|
1287
|
+
return "portrait";
|
|
1288
|
+
} else if (window.matchMedia("(orientation: landscape)").matches) {
|
|
1289
|
+
return "landscape";
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Get screen orientation angle
|
|
1296
|
+
*/
|
|
1297
|
+
getOrientationAngle() {
|
|
1298
|
+
if ("orientation" in screen) {
|
|
1299
|
+
const orientation = screen.orientation;
|
|
1300
|
+
if (orientation && typeof orientation.angle === "number") {
|
|
1301
|
+
return orientation.angle;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if ("orientation" in window) {
|
|
1305
|
+
const angle = window.orientation;
|
|
1306
|
+
if (typeof angle === "number") {
|
|
1307
|
+
return angle;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Clear cached fingerprint
|
|
1314
|
+
*/
|
|
1315
|
+
clearCache() {
|
|
1316
|
+
this.cachedFingerprint = null;
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
// ../fraud-sdk/src/collectors/timezone-locale.ts
|
|
1321
|
+
var TimezoneLocaleCollector = class {
|
|
1322
|
+
constructor() {
|
|
1323
|
+
this.cachedFingerprint = null;
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Collect timezone and locale information
|
|
1327
|
+
*/
|
|
1328
|
+
collect() {
|
|
1329
|
+
if (this.cachedFingerprint) {
|
|
1330
|
+
return this.cachedFingerprint;
|
|
1331
|
+
}
|
|
1332
|
+
const startTime = performance.now();
|
|
1333
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1334
|
+
const timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
1335
|
+
const resolvedOptions = Intl.DateTimeFormat().resolvedOptions();
|
|
1336
|
+
const locale = resolvedOptions.locale;
|
|
1337
|
+
const locales = navigator.languages || [navigator.language];
|
|
1338
|
+
const dateFormat = this.getDateFormat();
|
|
1339
|
+
const timeFormat = this.getTimeFormat();
|
|
1340
|
+
const numberFormat = this.getNumberFormat();
|
|
1341
|
+
const calendar = resolvedOptions.calendar || null;
|
|
1342
|
+
const fingerprint = {
|
|
1343
|
+
timezone,
|
|
1344
|
+
timezoneOffset,
|
|
1345
|
+
locale,
|
|
1346
|
+
locales: locales.slice(0, 5),
|
|
1347
|
+
// Limit to first 5 locales
|
|
1348
|
+
dateFormat,
|
|
1349
|
+
timeFormat,
|
|
1350
|
+
numberFormat,
|
|
1351
|
+
calendar,
|
|
1352
|
+
duration: 0
|
|
1353
|
+
};
|
|
1354
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1355
|
+
this.cachedFingerprint = fingerprint;
|
|
1356
|
+
return fingerprint;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Get date format preference
|
|
1360
|
+
*/
|
|
1361
|
+
getDateFormat() {
|
|
1362
|
+
try {
|
|
1363
|
+
const formatter = new Intl.DateTimeFormat();
|
|
1364
|
+
const sample = formatter.format(new Date(2023, 0, 15));
|
|
1365
|
+
if (sample.includes("/")) {
|
|
1366
|
+
return "numeric";
|
|
1367
|
+
} else if (sample.includes("-")) {
|
|
1368
|
+
return "iso";
|
|
1369
|
+
} else if (sample.includes(".")) {
|
|
1370
|
+
return "european";
|
|
1371
|
+
}
|
|
1372
|
+
return "unknown";
|
|
1373
|
+
} catch {
|
|
1374
|
+
return "unknown";
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Get time format preference (12h vs 24h)
|
|
1379
|
+
*/
|
|
1380
|
+
getTimeFormat() {
|
|
1381
|
+
try {
|
|
1382
|
+
const formatter = new Intl.DateTimeFormat(void 0, {
|
|
1383
|
+
hour: "numeric",
|
|
1384
|
+
minute: "numeric"
|
|
1385
|
+
});
|
|
1386
|
+
const sample = formatter.format(new Date(2023, 0, 15, 13, 30));
|
|
1387
|
+
if (sample.toLowerCase().includes("am") || sample.toLowerCase().includes("pm")) {
|
|
1388
|
+
return "12h";
|
|
1389
|
+
}
|
|
1390
|
+
return "24h";
|
|
1391
|
+
} catch {
|
|
1392
|
+
return "unknown";
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/**
|
|
1396
|
+
* Get number format preference
|
|
1397
|
+
*/
|
|
1398
|
+
getNumberFormat() {
|
|
1399
|
+
try {
|
|
1400
|
+
const formatter = new Intl.NumberFormat();
|
|
1401
|
+
const sample = formatter.format(1234.56);
|
|
1402
|
+
if (sample.includes(",")) {
|
|
1403
|
+
return "european";
|
|
1404
|
+
} else if (sample.includes(".")) {
|
|
1405
|
+
return "american";
|
|
1406
|
+
}
|
|
1407
|
+
return "unknown";
|
|
1408
|
+
} catch {
|
|
1409
|
+
return "unknown";
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Clear cached fingerprint
|
|
1414
|
+
*/
|
|
1415
|
+
clearCache() {
|
|
1416
|
+
this.cachedFingerprint = null;
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
// ../fraud-sdk/src/collectors/plugin-mime.ts
|
|
1421
|
+
var PluginMimeCollector = class {
|
|
1422
|
+
constructor() {
|
|
1423
|
+
this.cachedFingerprint = null;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Collect plugin and MIME type information
|
|
1427
|
+
* Returns null if plugins are not available (modern browsers)
|
|
1428
|
+
*/
|
|
1429
|
+
collect() {
|
|
1430
|
+
if (this.cachedFingerprint) {
|
|
1431
|
+
return this.cachedFingerprint;
|
|
1432
|
+
}
|
|
1433
|
+
const startTime = performance.now();
|
|
1434
|
+
try {
|
|
1435
|
+
if (!navigator.plugins || navigator.plugins.length === 0) {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
const plugins = [];
|
|
1439
|
+
const allMimeTypes = /* @__PURE__ */ new Set();
|
|
1440
|
+
for (let i = 0; i < Math.min(navigator.plugins.length, 10); i++) {
|
|
1441
|
+
const plugin = navigator.plugins[i];
|
|
1442
|
+
if (!plugin) continue;
|
|
1443
|
+
const mimeTypes = [];
|
|
1444
|
+
for (let j = 0; j < Math.min(plugin.length, 5); j++) {
|
|
1445
|
+
const mimeType = plugin[j];
|
|
1446
|
+
if (mimeType && mimeType.type) {
|
|
1447
|
+
mimeTypes.push(mimeType.type);
|
|
1448
|
+
allMimeTypes.add(mimeType.type);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
plugins.push({
|
|
1452
|
+
name: plugin.name || "Unknown",
|
|
1453
|
+
description: plugin.description || "",
|
|
1454
|
+
mimeTypes
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
const fingerprint = {
|
|
1458
|
+
plugins,
|
|
1459
|
+
mimeTypes: Array.from(allMimeTypes).sort(),
|
|
1460
|
+
duration: 0
|
|
1461
|
+
};
|
|
1462
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1463
|
+
this.cachedFingerprint = fingerprint;
|
|
1464
|
+
return fingerprint;
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
console.warn("[Keverd SDK] Plugin/MIME collection failed:", error);
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Clear cached fingerprint
|
|
1472
|
+
*/
|
|
1473
|
+
clearCache() {
|
|
1474
|
+
this.cachedFingerprint = null;
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
// ../fraud-sdk/src/collectors/battery-api.ts
|
|
1479
|
+
var BatteryAPICollector = class {
|
|
1480
|
+
constructor() {
|
|
1481
|
+
this.cachedFingerprint = null;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Collect battery information
|
|
1485
|
+
* Returns null if Battery API is not available
|
|
1486
|
+
*/
|
|
1487
|
+
async collect() {
|
|
1488
|
+
if (this.cachedFingerprint) {
|
|
1489
|
+
return this.cachedFingerprint;
|
|
1490
|
+
}
|
|
1491
|
+
const startTime = performance.now();
|
|
1492
|
+
try {
|
|
1493
|
+
const batteryManager = await this.getBatteryManager();
|
|
1494
|
+
if (!batteryManager) {
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
const fingerprint = {
|
|
1498
|
+
level: batteryManager.level,
|
|
1499
|
+
charging: batteryManager.charging,
|
|
1500
|
+
chargingTime: batteryManager.chargingTime,
|
|
1501
|
+
dischargingTime: batteryManager.dischargingTime,
|
|
1502
|
+
duration: 0
|
|
1503
|
+
};
|
|
1504
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1505
|
+
this.cachedFingerprint = fingerprint;
|
|
1506
|
+
return fingerprint;
|
|
1507
|
+
} catch (error) {
|
|
1508
|
+
return null;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Get battery manager from various API locations
|
|
1513
|
+
*/
|
|
1514
|
+
async getBatteryManager() {
|
|
1515
|
+
if ("getBattery" in navigator) {
|
|
1516
|
+
try {
|
|
1517
|
+
return await navigator.getBattery();
|
|
1518
|
+
} catch (e) {
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if ("webkitGetBattery" in navigator) {
|
|
1522
|
+
try {
|
|
1523
|
+
return await navigator.webkitGetBattery();
|
|
1524
|
+
} catch (e) {
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
if ("mozGetBattery" in navigator) {
|
|
1528
|
+
try {
|
|
1529
|
+
return await navigator.mozGetBattery();
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Clear cached fingerprint
|
|
1537
|
+
*/
|
|
1538
|
+
clearCache() {
|
|
1539
|
+
this.cachedFingerprint = null;
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// ../fraud-sdk/src/collectors/storage-database.ts
|
|
1544
|
+
var StorageDatabaseCollector = class {
|
|
1545
|
+
constructor() {
|
|
1546
|
+
this.cachedFingerprint = null;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Collect storage and database information
|
|
1550
|
+
*/
|
|
1551
|
+
async collect() {
|
|
1552
|
+
if (this.cachedFingerprint) {
|
|
1553
|
+
return this.cachedFingerprint;
|
|
1554
|
+
}
|
|
1555
|
+
const startTime = performance.now();
|
|
1556
|
+
const fingerprint = {
|
|
1557
|
+
localStorage: await this.checkLocalStorage(),
|
|
1558
|
+
sessionStorage: await this.checkSessionStorage(),
|
|
1559
|
+
indexedDB: this.checkIndexedDB(),
|
|
1560
|
+
webSQL: this.checkWebSQL(),
|
|
1561
|
+
duration: 0
|
|
1562
|
+
};
|
|
1563
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1564
|
+
this.cachedFingerprint = fingerprint;
|
|
1565
|
+
return fingerprint;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Check localStorage availability and quota
|
|
1569
|
+
*/
|
|
1570
|
+
async checkLocalStorage() {
|
|
1571
|
+
let available = false;
|
|
1572
|
+
let quota = null;
|
|
1573
|
+
let usage = null;
|
|
1574
|
+
try {
|
|
1575
|
+
const test = "__localStorage_test__";
|
|
1576
|
+
localStorage.setItem(test, test);
|
|
1577
|
+
localStorage.removeItem(test);
|
|
1578
|
+
available = true;
|
|
1579
|
+
if ("storage" in navigator && "estimate" in navigator.storage) {
|
|
1580
|
+
try {
|
|
1581
|
+
const estimate = await navigator.storage.estimate();
|
|
1582
|
+
if (estimate) {
|
|
1583
|
+
quota = estimate.quota || null;
|
|
1584
|
+
usage = estimate.usage || null;
|
|
1585
|
+
}
|
|
1586
|
+
} catch (e) {
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
available = false;
|
|
1591
|
+
}
|
|
1592
|
+
return { available, quota, usage };
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Check sessionStorage availability and quota
|
|
1596
|
+
*/
|
|
1597
|
+
async checkSessionStorage() {
|
|
1598
|
+
let available = false;
|
|
1599
|
+
let quota = null;
|
|
1600
|
+
let usage = null;
|
|
1601
|
+
try {
|
|
1602
|
+
const test = "__sessionStorage_test__";
|
|
1603
|
+
sessionStorage.setItem(test, test);
|
|
1604
|
+
sessionStorage.removeItem(test);
|
|
1605
|
+
available = true;
|
|
1606
|
+
if ("storage" in navigator && "estimate" in navigator.storage) {
|
|
1607
|
+
try {
|
|
1608
|
+
const estimate = await navigator.storage.estimate();
|
|
1609
|
+
if (estimate) {
|
|
1610
|
+
quota = estimate.quota || null;
|
|
1611
|
+
usage = estimate.usage || null;
|
|
1612
|
+
}
|
|
1613
|
+
} catch (e) {
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
} catch (e) {
|
|
1617
|
+
available = false;
|
|
1618
|
+
}
|
|
1619
|
+
return { available, quota, usage };
|
|
1620
|
+
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Check IndexedDB availability
|
|
1623
|
+
*/
|
|
1624
|
+
checkIndexedDB() {
|
|
1625
|
+
return {
|
|
1626
|
+
available: "indexedDB" in window && !!window.indexedDB
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Check WebSQL availability (deprecated but still in some browsers)
|
|
1631
|
+
*/
|
|
1632
|
+
checkWebSQL() {
|
|
1633
|
+
return {
|
|
1634
|
+
available: "openDatabase" in window && !!window.openDatabase
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Clear cached fingerprint
|
|
1639
|
+
*/
|
|
1640
|
+
clearCache() {
|
|
1641
|
+
this.cachedFingerprint = null;
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
|
|
1645
|
+
// ../fraud-sdk/src/collectors/cpu-memory.ts
|
|
1646
|
+
var CPUMemoryCollector = class {
|
|
1647
|
+
constructor() {
|
|
1648
|
+
this.cachedFingerprint = null;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Collect CPU and memory information
|
|
1652
|
+
*/
|
|
1653
|
+
collect() {
|
|
1654
|
+
if (this.cachedFingerprint) {
|
|
1655
|
+
return this.cachedFingerprint;
|
|
1656
|
+
}
|
|
1657
|
+
const startTime = performance.now();
|
|
1658
|
+
const fingerprint = {
|
|
1659
|
+
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
1660
|
+
deviceMemory: this.getDeviceMemory(),
|
|
1661
|
+
performanceMemory: this.getPerformanceMemory(),
|
|
1662
|
+
duration: 0
|
|
1663
|
+
};
|
|
1664
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1665
|
+
this.cachedFingerprint = fingerprint;
|
|
1666
|
+
return fingerprint;
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Get device memory (if available)
|
|
1670
|
+
*/
|
|
1671
|
+
getDeviceMemory() {
|
|
1672
|
+
if ("deviceMemory" in navigator && navigator.deviceMemory) {
|
|
1673
|
+
return navigator.deviceMemory;
|
|
1674
|
+
}
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Get performance memory information (Chrome-specific)
|
|
1679
|
+
*/
|
|
1680
|
+
getPerformanceMemory() {
|
|
1681
|
+
if ("memory" in performance) {
|
|
1682
|
+
const memory = performance.memory;
|
|
1683
|
+
return {
|
|
1684
|
+
jsHeapSizeLimit: memory.jsHeapSizeLimit || null,
|
|
1685
|
+
totalJSHeapSize: memory.totalJSHeapSize || null,
|
|
1686
|
+
usedJSHeapSize: memory.usedJSHeapSize || null
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
return {
|
|
1690
|
+
jsHeapSizeLimit: null,
|
|
1691
|
+
totalJSHeapSize: null,
|
|
1692
|
+
usedJSHeapSize: null
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Clear cached fingerprint
|
|
1697
|
+
*/
|
|
1698
|
+
clearCache() {
|
|
1699
|
+
this.cachedFingerprint = null;
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
// ../fraud-sdk/src/collectors/touch-pointer.ts
|
|
1704
|
+
var TouchPointerCollector = class {
|
|
1705
|
+
constructor() {
|
|
1706
|
+
this.cachedFingerprint = null;
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Collect touch and pointer information
|
|
1710
|
+
*/
|
|
1711
|
+
collect() {
|
|
1712
|
+
if (this.cachedFingerprint) {
|
|
1713
|
+
return this.cachedFingerprint;
|
|
1714
|
+
}
|
|
1715
|
+
const startTime = performance.now();
|
|
1716
|
+
const fingerprint = {
|
|
1717
|
+
maxTouchPoints: navigator.maxTouchPoints || 0,
|
|
1718
|
+
touchSupport: this.checkTouchSupport(),
|
|
1719
|
+
pointerSupport: this.checkPointerSupport(),
|
|
1720
|
+
pointerTypes: this.getPointerTypes(),
|
|
1721
|
+
duration: 0
|
|
1722
|
+
};
|
|
1723
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1724
|
+
this.cachedFingerprint = fingerprint;
|
|
1725
|
+
return fingerprint;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Check if touch events are supported
|
|
1729
|
+
*/
|
|
1730
|
+
checkTouchSupport() {
|
|
1731
|
+
return "ontouchstart" in window || navigator.maxTouchPoints > 0 || !!window.DocumentTouch && document instanceof window.DocumentTouch;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Check if Pointer Events are supported
|
|
1735
|
+
*/
|
|
1736
|
+
checkPointerSupport() {
|
|
1737
|
+
return "PointerEvent" in window || "MSPointerEvent" in window;
|
|
1738
|
+
}
|
|
1739
|
+
/**
|
|
1740
|
+
* Get available pointer types
|
|
1741
|
+
*/
|
|
1742
|
+
getPointerTypes() {
|
|
1743
|
+
const types = [];
|
|
1744
|
+
if ("PointerEvent" in window) {
|
|
1745
|
+
if (navigator.maxTouchPoints > 0) {
|
|
1746
|
+
types.push("touch");
|
|
1747
|
+
}
|
|
1748
|
+
types.push("mouse");
|
|
1749
|
+
if ("pen" in navigator) {
|
|
1750
|
+
types.push("pen");
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return types;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* Clear cached fingerprint
|
|
1757
|
+
*/
|
|
1758
|
+
clearCache() {
|
|
1759
|
+
this.cachedFingerprint = null;
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
// ../fraud-sdk/src/collectors/permissions-api.ts
|
|
1764
|
+
var PermissionsAPICollector = class {
|
|
1765
|
+
constructor() {
|
|
1766
|
+
this.cachedFingerprint = null;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Collect permission states
|
|
1770
|
+
* Returns null if Permissions API is not available
|
|
1771
|
+
*/
|
|
1772
|
+
async collect() {
|
|
1773
|
+
if (this.cachedFingerprint) {
|
|
1774
|
+
return this.cachedFingerprint;
|
|
1775
|
+
}
|
|
1776
|
+
const startTime = performance.now();
|
|
1777
|
+
try {
|
|
1778
|
+
if (!("permissions" in navigator)) {
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
const permissions = navigator.permissions;
|
|
1782
|
+
const fingerprint = {
|
|
1783
|
+
camera: await this.queryPermission(permissions, "camera"),
|
|
1784
|
+
microphone: await this.queryPermission(permissions, "microphone"),
|
|
1785
|
+
notifications: await this.queryPermission(permissions, "notifications"),
|
|
1786
|
+
geolocation: await this.queryPermission(permissions, "geolocation"),
|
|
1787
|
+
persistentStorage: await this.queryPermission(permissions, "persistent-storage"),
|
|
1788
|
+
duration: 0
|
|
1789
|
+
};
|
|
1790
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1791
|
+
this.cachedFingerprint = fingerprint;
|
|
1792
|
+
return fingerprint;
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
console.warn("[Keverd SDK] Permissions API collection failed:", error);
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Query a specific permission
|
|
1800
|
+
*/
|
|
1801
|
+
async queryPermission(permissions, name) {
|
|
1802
|
+
try {
|
|
1803
|
+
const result = await permissions.query({ name });
|
|
1804
|
+
return result.state || null;
|
|
1805
|
+
} catch (e) {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Clear cached fingerprint
|
|
1811
|
+
*/
|
|
1812
|
+
clearCache() {
|
|
1813
|
+
this.cachedFingerprint = null;
|
|
1814
|
+
}
|
|
1815
|
+
};
|
|
1816
|
+
|
|
1817
|
+
// ../fraud-sdk/src/collectors/webrtc-ip.ts
|
|
1818
|
+
var WebRTCIPCollector = class {
|
|
1819
|
+
constructor() {
|
|
1820
|
+
this.cachedFingerprint = null;
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Collect WebRTC IP information
|
|
1824
|
+
* Returns null if WebRTC is not available or fails
|
|
1825
|
+
*/
|
|
1826
|
+
async collect() {
|
|
1827
|
+
if (this.cachedFingerprint) {
|
|
1828
|
+
return this.cachedFingerprint;
|
|
1829
|
+
}
|
|
1830
|
+
const startTime = performance.now();
|
|
1831
|
+
try {
|
|
1832
|
+
const RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
|
|
1833
|
+
if (!RTCPeerConnection) {
|
|
1834
|
+
return null;
|
|
1835
|
+
}
|
|
1836
|
+
const localIPs = [];
|
|
1837
|
+
let publicIP = null;
|
|
1838
|
+
const pc = new RTCPeerConnection({
|
|
1839
|
+
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
|
1840
|
+
});
|
|
1841
|
+
pc.onicecandidate = (event) => {
|
|
1842
|
+
if (event.candidate) {
|
|
1843
|
+
const candidate = event.candidate.candidate;
|
|
1844
|
+
const ipMatch = candidate.match(/([0-9]{1,3}\.){3}[0-9]{1,3}/);
|
|
1845
|
+
if (ipMatch) {
|
|
1846
|
+
const ip = ipMatch[0];
|
|
1847
|
+
if (this.isLocalIP(ip)) {
|
|
1848
|
+
if (!localIPs.includes(ip)) {
|
|
1849
|
+
localIPs.push(ip);
|
|
1850
|
+
}
|
|
1851
|
+
} else {
|
|
1852
|
+
publicIP = ip;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
};
|
|
1857
|
+
pc.createDataChannel("");
|
|
1858
|
+
const offer = await pc.createOffer();
|
|
1859
|
+
await pc.setLocalDescription(offer);
|
|
1860
|
+
await new Promise((resolve) => {
|
|
1861
|
+
const timeout = setTimeout(() => {
|
|
1862
|
+
resolve();
|
|
1863
|
+
}, 2e3);
|
|
1864
|
+
pc.onicegatheringstatechange = () => {
|
|
1865
|
+
if (pc.iceGatheringState === "complete") {
|
|
1866
|
+
clearTimeout(timeout);
|
|
1867
|
+
resolve();
|
|
1868
|
+
}
|
|
1869
|
+
};
|
|
1870
|
+
if (pc.iceGatheringState === "complete") {
|
|
1871
|
+
clearTimeout(timeout);
|
|
1872
|
+
resolve();
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
pc.close();
|
|
1876
|
+
const fingerprint = {
|
|
1877
|
+
localIPs: localIPs.sort(),
|
|
1878
|
+
publicIP,
|
|
1879
|
+
duration: 0
|
|
1880
|
+
};
|
|
1881
|
+
fingerprint.duration = Math.round((performance.now() - startTime) * 100) / 100;
|
|
1882
|
+
this.cachedFingerprint = fingerprint;
|
|
1883
|
+
return fingerprint;
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Check if an IP is a local/private IP
|
|
1890
|
+
*/
|
|
1891
|
+
isLocalIP(ip) {
|
|
1892
|
+
if (ip === "127.0.0.1" || ip === "::1") {
|
|
1893
|
+
return true;
|
|
1894
|
+
}
|
|
1895
|
+
const parts = ip.split(".");
|
|
1896
|
+
if (parts.length === 4) {
|
|
1897
|
+
const first = parseInt(parts[0], 10);
|
|
1898
|
+
const second = parseInt(parts[1], 10);
|
|
1899
|
+
if (first === 10) {
|
|
1900
|
+
return true;
|
|
1901
|
+
}
|
|
1902
|
+
if (first === 172 && second >= 16 && second <= 31) {
|
|
1903
|
+
return true;
|
|
1904
|
+
}
|
|
1905
|
+
if (first === 192 && second === 168) {
|
|
1906
|
+
return true;
|
|
1907
|
+
}
|
|
1908
|
+
if (first === 169 && second === 254) {
|
|
1909
|
+
return true;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1914
|
+
/**
|
|
1915
|
+
* Clear cached fingerprint
|
|
1916
|
+
*/
|
|
1917
|
+
clearCache() {
|
|
1918
|
+
this.cachedFingerprint = null;
|
|
1919
|
+
}
|
|
1920
|
+
};
|
|
1921
|
+
|
|
1922
|
+
// ../fraud-sdk/src/collectors/visitor-id-generator.ts
|
|
1923
|
+
var VisitorIDGenerator = class {
|
|
1924
|
+
/**
|
|
1925
|
+
* Generate a stable visitor ID from fingerprint components
|
|
1926
|
+
* Uses SHA-256-like hashing for consistency
|
|
1927
|
+
*/
|
|
1928
|
+
generate(components) {
|
|
1929
|
+
const identifiers = [];
|
|
1930
|
+
if (components.canvas?.value) {
|
|
1931
|
+
identifiers.push(`canvas:${components.canvas.value}`);
|
|
1932
|
+
}
|
|
1933
|
+
if (components.webgl) {
|
|
1934
|
+
if (components.webgl.vendor) {
|
|
1935
|
+
identifiers.push(`webgl_vendor:${components.webgl.vendor}`);
|
|
1936
|
+
}
|
|
1937
|
+
if (components.webgl.renderer) {
|
|
1938
|
+
identifiers.push(`webgl_renderer:${components.webgl.renderer}`);
|
|
1939
|
+
}
|
|
1940
|
+
if (components.webgl.extensions.length > 0) {
|
|
1941
|
+
identifiers.push(`webgl_ext:${components.webgl.extensions.slice(0, 10).join(",")}`);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
if (components.audio?.value) {
|
|
1945
|
+
identifiers.push(`audio:${components.audio.value}`);
|
|
1946
|
+
}
|
|
1947
|
+
if (components.screen) {
|
|
1948
|
+
identifiers.push(`screen:${components.screen.width}x${components.screen.height}x${components.screen.pixelRatio}`);
|
|
1949
|
+
identifiers.push(`color:${components.screen.colorDepth}`);
|
|
1950
|
+
}
|
|
1951
|
+
if (components.timezone) {
|
|
1952
|
+
identifiers.push(`tz:${components.timezone.timezone}`);
|
|
1953
|
+
identifiers.push(`locale:${components.timezone.locale}`);
|
|
1954
|
+
}
|
|
1955
|
+
if (components.cpu) {
|
|
1956
|
+
identifiers.push(`cpu:${components.cpu.hardwareConcurrency}`);
|
|
1957
|
+
if (components.cpu.deviceMemory) {
|
|
1958
|
+
identifiers.push(`memory:${components.cpu.deviceMemory}`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
if (components.touch) {
|
|
1962
|
+
identifiers.push(`touch:${components.touch.maxTouchPoints}`);
|
|
1963
|
+
}
|
|
1964
|
+
identifiers.push(`ua:${navigator.userAgent}`);
|
|
1965
|
+
identifiers.push(`lang:${navigator.language}`);
|
|
1966
|
+
identifiers.push(`platform:${navigator.platform}`);
|
|
1967
|
+
if (components.fonts && components.fonts.availableFonts.length > 0) {
|
|
1968
|
+
identifiers.push(`fonts:${components.fonts.availableFonts.slice(0, 20).join(",")}`);
|
|
1969
|
+
}
|
|
1970
|
+
const combined = identifiers.join("|");
|
|
1971
|
+
return this.sha256Hash(combined);
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Calculate confidence score based on component availability
|
|
1975
|
+
* More components = higher confidence
|
|
1976
|
+
*/
|
|
1977
|
+
calculateConfidence(components) {
|
|
1978
|
+
let score = 0;
|
|
1979
|
+
let maxScore = 0;
|
|
1980
|
+
const weights = {
|
|
1981
|
+
canvas: 20,
|
|
1982
|
+
webgl: 20,
|
|
1983
|
+
audio: 15,
|
|
1984
|
+
screen: 10,
|
|
1985
|
+
timezone: 10,
|
|
1986
|
+
cpu: 10,
|
|
1987
|
+
fonts: 10,
|
|
1988
|
+
touch: 5,
|
|
1989
|
+
storage: 5,
|
|
1990
|
+
permissions: 5,
|
|
1991
|
+
webrtc: 5,
|
|
1992
|
+
mediaDevices: 5,
|
|
1993
|
+
battery: 3,
|
|
1994
|
+
plugins: 3
|
|
1995
|
+
};
|
|
1996
|
+
Object.keys(weights).forEach((key) => {
|
|
1997
|
+
maxScore += weights[key];
|
|
1998
|
+
if (components[key]) {
|
|
1999
|
+
score += weights[key];
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
return maxScore > 0 ? Math.min(score / maxScore, 1) : 0;
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* SHA-256-like hash function
|
|
2006
|
+
* Produces a 64-character hex string (matching SHA-256 format)
|
|
2007
|
+
*/
|
|
2008
|
+
sha256Hash(input) {
|
|
2009
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
2010
|
+
return this.sha256LikeHash(input);
|
|
2011
|
+
}
|
|
2012
|
+
return this.sha256LikeHash(input);
|
|
2013
|
+
}
|
|
2014
|
+
/**
|
|
2015
|
+
* SHA-256-like hash (synchronous, deterministic)
|
|
2016
|
+
* Produces 64-character hex string
|
|
2017
|
+
*/
|
|
2018
|
+
sha256LikeHash(str) {
|
|
2019
|
+
const hashes = [];
|
|
2020
|
+
let h1 = 5381;
|
|
2021
|
+
for (let i = 0; i < str.length; i++) {
|
|
2022
|
+
h1 = (h1 << 5) + h1 + str.charCodeAt(i);
|
|
2023
|
+
h1 = h1 & 4294967295;
|
|
2024
|
+
}
|
|
2025
|
+
hashes.push(h1);
|
|
2026
|
+
let h2 = 0;
|
|
2027
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
2028
|
+
h2 = (h2 << 7) - h2 + str.charCodeAt(i);
|
|
2029
|
+
h2 = h2 & 4294967295;
|
|
2030
|
+
}
|
|
2031
|
+
hashes.push(h2);
|
|
2032
|
+
let h3 = 0;
|
|
2033
|
+
for (let i = 0; i < str.length; i++) {
|
|
2034
|
+
h3 = h3 ^ str.charCodeAt(i) << i % 4 * 8;
|
|
2035
|
+
h3 = h3 & 4294967295;
|
|
2036
|
+
}
|
|
2037
|
+
hashes.push(h3);
|
|
2038
|
+
let h4 = 0;
|
|
2039
|
+
for (let i = 0; i < str.length; i++) {
|
|
2040
|
+
h4 = h4 * 31 + str.charCodeAt(i) & 4294967295;
|
|
2041
|
+
}
|
|
2042
|
+
hashes.push(h4);
|
|
2043
|
+
let h5 = 0;
|
|
2044
|
+
for (let i = 0; i < str.length; i++) {
|
|
2045
|
+
const rotated = (str.charCodeAt(i) << i % 16 | str.charCodeAt(i) >>> 32 - i % 16) & 4294967295;
|
|
2046
|
+
h5 = h5 + rotated & 4294967295;
|
|
2047
|
+
}
|
|
2048
|
+
hashes.push(h5);
|
|
2049
|
+
let h6 = 2166136261;
|
|
2050
|
+
for (let i = 0; i < str.length; i++) {
|
|
2051
|
+
h6 = (h6 ^ str.charCodeAt(i)) * 16777619;
|
|
2052
|
+
h6 = h6 & 4294967295;
|
|
2053
|
+
}
|
|
2054
|
+
hashes.push(h6);
|
|
2055
|
+
let h7 = 0;
|
|
2056
|
+
for (let i = 0; i < str.length; i++) {
|
|
2057
|
+
h7 = h7 + str.charCodeAt(i) * (i + 1) & 4294967295;
|
|
2058
|
+
}
|
|
2059
|
+
hashes.push(h7);
|
|
2060
|
+
let h8 = 0;
|
|
2061
|
+
for (let i = 0; i < str.length; i += 2) {
|
|
2062
|
+
const combined2 = str.charCodeAt(i) + (str.charCodeAt(i + 1) || 0) * 256;
|
|
2063
|
+
h8 = (h8 << 3) - h8 + combined2;
|
|
2064
|
+
h8 = h8 & 4294967295;
|
|
2065
|
+
}
|
|
2066
|
+
hashes.push(h8);
|
|
2067
|
+
const hexParts = hashes.map((h) => Math.abs(h).toString(16).padStart(8, "0"));
|
|
2068
|
+
const combined = hexParts.join("");
|
|
2069
|
+
return combined.substring(0, 64).padEnd(64, "0");
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
|
|
2073
|
+
// ../fraud-sdk/src/collectors/fingerprint-manager.ts
|
|
2074
|
+
var FingerprintManager = class {
|
|
2075
|
+
constructor() {
|
|
2076
|
+
this.cachedData = null;
|
|
2077
|
+
this.collectionStartTime = 0;
|
|
2078
|
+
this.collectors = {
|
|
2079
|
+
canvas: new CanvasFingerprintCollector(),
|
|
2080
|
+
webgl: new WebGLFingerprintCollector(),
|
|
2081
|
+
audio: new AudioFingerprintCollector(),
|
|
2082
|
+
fonts: new FontDetectionCollector(),
|
|
2083
|
+
mediaDevices: new MediaDevicesCollector(),
|
|
2084
|
+
screen: new ScreenDisplayCollector(),
|
|
2085
|
+
timezone: new TimezoneLocaleCollector(),
|
|
2086
|
+
plugins: new PluginMimeCollector(),
|
|
2087
|
+
battery: new BatteryAPICollector(),
|
|
2088
|
+
storage: new StorageDatabaseCollector(),
|
|
2089
|
+
cpu: new CPUMemoryCollector(),
|
|
2090
|
+
touch: new TouchPointerCollector(),
|
|
2091
|
+
permissions: new PermissionsAPICollector(),
|
|
2092
|
+
webrtc: new WebRTCIPCollector()
|
|
2093
|
+
};
|
|
2094
|
+
this.visitorIDGenerator = new VisitorIDGenerator();
|
|
2095
|
+
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Collect all fingerprint data
|
|
2098
|
+
* This is the main method that replaces FingerprintJS.get()
|
|
2099
|
+
*/
|
|
2100
|
+
async collect() {
|
|
2101
|
+
if (this.cachedData) {
|
|
2102
|
+
return this.cachedData;
|
|
2103
|
+
}
|
|
2104
|
+
this.collectionStartTime = performance.now();
|
|
2105
|
+
const components = {};
|
|
2106
|
+
try {
|
|
2107
|
+
components.canvas = this.collectors.canvas.collect() || void 0;
|
|
2108
|
+
} catch (e) {
|
|
2109
|
+
console.warn("[Keverd SDK] Canvas collection failed:", e);
|
|
2110
|
+
}
|
|
2111
|
+
try {
|
|
2112
|
+
components.webgl = this.collectors.webgl.collect() || void 0;
|
|
2113
|
+
} catch (e) {
|
|
2114
|
+
console.warn("[Keverd SDK] WebGL collection failed:", e);
|
|
2115
|
+
}
|
|
2116
|
+
try {
|
|
2117
|
+
components.fonts = this.collectors.fonts.collect() || void 0;
|
|
2118
|
+
} catch (e) {
|
|
2119
|
+
console.warn("[Keverd SDK] Font detection failed:", e);
|
|
2120
|
+
}
|
|
2121
|
+
try {
|
|
2122
|
+
components.screen = this.collectors.screen.collect();
|
|
2123
|
+
} catch (e) {
|
|
2124
|
+
console.warn("[Keverd SDK] Screen collection failed:", e);
|
|
2125
|
+
}
|
|
2126
|
+
try {
|
|
2127
|
+
components.timezone = this.collectors.timezone.collect();
|
|
2128
|
+
} catch (e) {
|
|
2129
|
+
console.warn("[Keverd SDK] Timezone collection failed:", e);
|
|
2130
|
+
}
|
|
2131
|
+
try {
|
|
2132
|
+
components.plugins = this.collectors.plugins.collect() || void 0;
|
|
2133
|
+
} catch (e) {
|
|
2134
|
+
console.warn("[Keverd SDK] Plugin collection failed:", e);
|
|
2135
|
+
}
|
|
2136
|
+
try {
|
|
2137
|
+
components.cpu = this.collectors.cpu.collect();
|
|
2138
|
+
} catch (e) {
|
|
2139
|
+
console.warn("[Keverd SDK] CPU collection failed:", e);
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
components.touch = this.collectors.touch.collect();
|
|
2143
|
+
} catch (e) {
|
|
2144
|
+
console.warn("[Keverd SDK] Touch collection failed:", e);
|
|
2145
|
+
}
|
|
2146
|
+
try {
|
|
2147
|
+
components.audio = await this.collectors.audio.collect() || void 0;
|
|
2148
|
+
} catch (e) {
|
|
2149
|
+
console.warn("[Keverd SDK] Audio collection failed:", e);
|
|
2150
|
+
}
|
|
2151
|
+
try {
|
|
2152
|
+
components.mediaDevices = await this.collectors.mediaDevices.collect() || void 0;
|
|
2153
|
+
} catch (e) {
|
|
2154
|
+
console.warn("[Keverd SDK] Media devices collection failed:", e);
|
|
2155
|
+
}
|
|
2156
|
+
try {
|
|
2157
|
+
components.battery = await this.collectors.battery.collect() || void 0;
|
|
2158
|
+
} catch (e) {
|
|
2159
|
+
console.warn("[Keverd SDK] Battery collection failed:", e);
|
|
2160
|
+
}
|
|
2161
|
+
try {
|
|
2162
|
+
components.storage = await this.collectors.storage.collect();
|
|
2163
|
+
} catch (e) {
|
|
2164
|
+
console.warn("[Keverd SDK] Storage collection failed:", e);
|
|
2165
|
+
}
|
|
2166
|
+
try {
|
|
2167
|
+
components.permissions = await this.collectors.permissions.collect() || void 0;
|
|
2168
|
+
} catch (e) {
|
|
2169
|
+
console.warn("[Keverd SDK] Permissions collection failed:", e);
|
|
2170
|
+
}
|
|
2171
|
+
try {
|
|
2172
|
+
components.webrtc = await this.collectors.webrtc.collect() || void 0;
|
|
2173
|
+
} catch (e) {
|
|
2174
|
+
console.warn("[Keverd SDK] WebRTC collection failed:", e);
|
|
2175
|
+
}
|
|
2176
|
+
const visitorId = this.visitorIDGenerator.generate(components);
|
|
2177
|
+
const confidence = this.visitorIDGenerator.calculateConfidence(components);
|
|
2178
|
+
this.cachedData = {
|
|
2179
|
+
visitorId,
|
|
2180
|
+
confidence: Math.round(confidence * 1e3) / 1e3,
|
|
2181
|
+
// Round to 3 decimal places
|
|
2182
|
+
components,
|
|
2183
|
+
version: "1.0.0",
|
|
2184
|
+
// SDK version
|
|
2185
|
+
timestamp: Date.now()
|
|
2186
|
+
};
|
|
2187
|
+
return this.cachedData;
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* Get visitor ID only (faster, uses cached data if available)
|
|
2191
|
+
*/
|
|
2192
|
+
async getVisitorId() {
|
|
2193
|
+
const data = await this.collect();
|
|
2194
|
+
return data.visitorId;
|
|
2195
|
+
}
|
|
2196
|
+
/**
|
|
2197
|
+
* Clear all cached data
|
|
2198
|
+
*/
|
|
2199
|
+
clearCache() {
|
|
2200
|
+
this.cachedData = null;
|
|
2201
|
+
Object.values(this.collectors).forEach((collector) => {
|
|
2202
|
+
if (collector && typeof collector.clearCache === "function") {
|
|
2203
|
+
collector.clearCache();
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Get collection statistics
|
|
2209
|
+
*/
|
|
2210
|
+
getStats() {
|
|
2211
|
+
if (!this.cachedData) {
|
|
2212
|
+
return {
|
|
2213
|
+
componentsCollected: 0,
|
|
2214
|
+
totalComponents: 13,
|
|
2215
|
+
collectionTime: 0
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
const componentsCollected = Object.keys(this.cachedData.components).length;
|
|
2219
|
+
const collectionTime = performance.now() - this.collectionStartTime;
|
|
2220
|
+
return {
|
|
2221
|
+
componentsCollected,
|
|
2222
|
+
totalComponents: 13,
|
|
2223
|
+
collectionTime: Math.round(collectionTime * 100) / 100
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
// ../fraud-sdk/src/collectors/privacy-signals.ts
|
|
2229
|
+
var PrivacySignalCollector = class {
|
|
2230
|
+
constructor() {
|
|
2231
|
+
this.cachedSignals = null;
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Collect all privacy signals
|
|
2235
|
+
*/
|
|
2236
|
+
collect() {
|
|
2237
|
+
if (this.cachedSignals) {
|
|
2238
|
+
return this.cachedSignals;
|
|
2239
|
+
}
|
|
2240
|
+
const signals = {
|
|
2241
|
+
isIncognito: this.detectIncognito(),
|
|
2242
|
+
isVPN: false,
|
|
2243
|
+
// VPN detection requires backend IP analysis
|
|
2244
|
+
isAutomated: this.detectAutomation(),
|
|
2245
|
+
hasAdBlocker: this.detectAdBlocker()
|
|
2246
|
+
};
|
|
2247
|
+
this.cachedSignals = signals;
|
|
2248
|
+
return signals;
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Detect incognito/private mode
|
|
2252
|
+
* Uses multiple detection methods for accuracy
|
|
2253
|
+
*/
|
|
2254
|
+
detectIncognito() {
|
|
2255
|
+
try {
|
|
2256
|
+
if ("storage" in navigator && "estimate" in navigator.storage) {
|
|
2257
|
+
try {
|
|
2258
|
+
navigator.storage.estimate().then(() => {
|
|
2259
|
+
}).catch(() => {
|
|
2260
|
+
});
|
|
2261
|
+
} catch {
|
|
2262
|
+
return true;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
if ("storage" in navigator && "estimate" in navigator.storage) {
|
|
2266
|
+
}
|
|
2267
|
+
if ("getBattery" in navigator) {
|
|
2268
|
+
}
|
|
2269
|
+
if (window.chrome && window.chrome.runtime && !window.chrome.runtime.onConnect) {
|
|
2270
|
+
return true;
|
|
2271
|
+
}
|
|
2272
|
+
try {
|
|
2273
|
+
const testKey = "__incognito_test__";
|
|
2274
|
+
localStorage.setItem(testKey, "test");
|
|
2275
|
+
localStorage.removeItem(testKey);
|
|
2276
|
+
} catch {
|
|
2277
|
+
return true;
|
|
2278
|
+
}
|
|
2279
|
+
return false;
|
|
2280
|
+
} catch (error) {
|
|
2281
|
+
return false;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Detect automation/bot frameworks
|
|
2286
|
+
*/
|
|
2287
|
+
detectAutomation() {
|
|
2288
|
+
try {
|
|
2289
|
+
if (navigator.webdriver === true) {
|
|
2290
|
+
return true;
|
|
2291
|
+
}
|
|
2292
|
+
if (window.chrome && window.chrome.runtime && window.chrome.runtime.onConnect) {
|
|
2293
|
+
const hasDevTools = window.chrome.runtime.onConnect.hasListeners();
|
|
2294
|
+
if (hasDevTools) {
|
|
2295
|
+
return true;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
if (!window.chrome && !window.safari) {
|
|
2299
|
+
if (!window.Notification || !window.DeviceMotionEvent) {
|
|
2300
|
+
return true;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
try {
|
|
2304
|
+
const canvas = document.createElement("canvas");
|
|
2305
|
+
const ctx = canvas.getContext("2d");
|
|
2306
|
+
if (ctx) {
|
|
2307
|
+
ctx.textBaseline = "top";
|
|
2308
|
+
ctx.font = "14px Arial";
|
|
2309
|
+
ctx.fillText("Automation test", 2, 2);
|
|
2310
|
+
const fingerprint = canvas.toDataURL();
|
|
2311
|
+
if (fingerprint.length < 100) {
|
|
2312
|
+
return true;
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
} catch {
|
|
2316
|
+
}
|
|
2317
|
+
if (window.__nightmare || window.__phantomas || window.Buffer) {
|
|
2318
|
+
return true;
|
|
2319
|
+
}
|
|
2320
|
+
return false;
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
return false;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Detect ad blocker
|
|
2327
|
+
*/
|
|
2328
|
+
detectAdBlocker() {
|
|
2329
|
+
try {
|
|
2330
|
+
const testDiv = document.createElement("div");
|
|
2331
|
+
testDiv.innerHTML = " ";
|
|
2332
|
+
testDiv.className = "adsbox";
|
|
2333
|
+
testDiv.style.position = "absolute";
|
|
2334
|
+
testDiv.style.left = "-9999px";
|
|
2335
|
+
document.body.appendChild(testDiv);
|
|
2336
|
+
setTimeout(() => {
|
|
2337
|
+
const isBlocked = testDiv.offsetHeight === 0 || testDiv.offsetWidth === 0;
|
|
2338
|
+
document.body.removeChild(testDiv);
|
|
2339
|
+
return isBlocked;
|
|
2340
|
+
}, 100);
|
|
2341
|
+
const scripts = Array.from(document.getElementsByTagName("script"));
|
|
2342
|
+
const blockedCount = scripts.filter((script) => {
|
|
2343
|
+
return script.src && script.src.includes("ads") && !script.textContent;
|
|
2344
|
+
}).length;
|
|
2345
|
+
if (blockedCount > 0) {
|
|
2346
|
+
return true;
|
|
2347
|
+
}
|
|
2348
|
+
if (window.uBlock || window.adblock || window.BlockAdBlock) {
|
|
2349
|
+
return true;
|
|
2350
|
+
}
|
|
2351
|
+
return false;
|
|
2352
|
+
} catch (error) {
|
|
2353
|
+
return false;
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* Clear cached signals (useful for testing)
|
|
2358
|
+
*/
|
|
2359
|
+
clearCache() {
|
|
2360
|
+
this.cachedSignals = null;
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
2364
|
+
// ../fraud-sdk/src/collectors/browser-capabilities.ts
|
|
2365
|
+
var BrowserCapabilityCollector = class {
|
|
2366
|
+
constructor() {
|
|
2367
|
+
this.cachedCapabilities = null;
|
|
2368
|
+
}
|
|
2369
|
+
/**
|
|
2370
|
+
* Collect all browser capabilities
|
|
2371
|
+
*/
|
|
2372
|
+
collect() {
|
|
2373
|
+
if (this.cachedCapabilities) {
|
|
2374
|
+
return this.cachedCapabilities;
|
|
2375
|
+
}
|
|
2376
|
+
const capabilities = {
|
|
2377
|
+
hasWebGL: this.checkWebGL(),
|
|
2378
|
+
hasCanvas: this.checkCanvas(),
|
|
2379
|
+
hasWebRTC: this.checkWebRTC(),
|
|
2380
|
+
hasServiceWorker: this.checkServiceWorker(),
|
|
2381
|
+
hasIndexedDB: this.checkIndexedDB(),
|
|
2382
|
+
hasLocalStorage: this.checkLocalStorage(),
|
|
2383
|
+
hasSessionStorage: this.checkSessionStorage(),
|
|
2384
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
2385
|
+
doNotTrack: navigator.doNotTrack === "1" || navigator.doNotTrack === "yes"
|
|
2386
|
+
};
|
|
2387
|
+
this.cachedCapabilities = capabilities;
|
|
2388
|
+
return capabilities;
|
|
2389
|
+
}
|
|
2390
|
+
checkWebGL() {
|
|
2391
|
+
try {
|
|
2392
|
+
const canvas = document.createElement("canvas");
|
|
2393
|
+
return !!(canvas.getContext("webgl") || canvas.getContext("experimental-webgl"));
|
|
2394
|
+
} catch {
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
checkCanvas() {
|
|
2399
|
+
try {
|
|
2400
|
+
const canvas = document.createElement("canvas");
|
|
2401
|
+
return !!canvas.getContext("2d");
|
|
2402
|
+
} catch {
|
|
2403
|
+
return false;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
checkWebRTC() {
|
|
2407
|
+
return !!((window.RTCPeerConnection || // @ts-ignore
|
|
2408
|
+
window.webkitRTCPeerConnection || // @ts-ignore
|
|
2409
|
+
window.mozRTCPeerConnection) && navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
|
|
2410
|
+
}
|
|
2411
|
+
checkServiceWorker() {
|
|
2412
|
+
return "serviceWorker" in navigator;
|
|
2413
|
+
}
|
|
2414
|
+
checkIndexedDB() {
|
|
2415
|
+
return "indexedDB" in window;
|
|
2416
|
+
}
|
|
2417
|
+
checkLocalStorage() {
|
|
2418
|
+
try {
|
|
2419
|
+
const test = "__localStorage_test__";
|
|
2420
|
+
localStorage.setItem(test, test);
|
|
2421
|
+
localStorage.removeItem(test);
|
|
2422
|
+
return true;
|
|
2423
|
+
} catch {
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
checkSessionStorage() {
|
|
2428
|
+
try {
|
|
2429
|
+
const test = "__sessionStorage_test__";
|
|
2430
|
+
sessionStorage.setItem(test, test);
|
|
2431
|
+
sessionStorage.removeItem(test);
|
|
2432
|
+
return true;
|
|
2433
|
+
} catch {
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
/**
|
|
2438
|
+
* Clear cached capabilities (useful for testing)
|
|
2439
|
+
*/
|
|
2440
|
+
clearCache() {
|
|
2441
|
+
this.cachedCapabilities = null;
|
|
2442
|
+
}
|
|
2443
|
+
};
|
|
2444
|
+
|
|
2445
|
+
// ../fraud-sdk/src/collectors/hardware-signals.ts
|
|
2446
|
+
var HardwareSignalCollector = class {
|
|
2447
|
+
constructor() {
|
|
2448
|
+
this.cachedSignals = null;
|
|
2449
|
+
}
|
|
2450
|
+
/**
|
|
2451
|
+
* Collect all hardware signals
|
|
2452
|
+
*/
|
|
2453
|
+
collect() {
|
|
2454
|
+
if (this.cachedSignals) {
|
|
2455
|
+
return this.cachedSignals;
|
|
2456
|
+
}
|
|
2457
|
+
const signals = {
|
|
2458
|
+
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
2459
|
+
deviceMemory: this.getDeviceMemory(),
|
|
2460
|
+
maxTouchPoints: navigator.maxTouchPoints || 0,
|
|
2461
|
+
connectionType: this.getConnectionType(),
|
|
2462
|
+
effectiveType: this.getEffectiveType(),
|
|
2463
|
+
downlink: this.getDownlink(),
|
|
2464
|
+
rtt: this.getRTT(),
|
|
2465
|
+
saveData: this.getSaveData()
|
|
2466
|
+
};
|
|
2467
|
+
this.cachedSignals = signals;
|
|
2468
|
+
return signals;
|
|
2469
|
+
}
|
|
2470
|
+
getDeviceMemory() {
|
|
2471
|
+
if ("deviceMemory" in navigator && navigator.deviceMemory) {
|
|
2472
|
+
return navigator.deviceMemory;
|
|
2473
|
+
}
|
|
2474
|
+
return null;
|
|
2475
|
+
}
|
|
2476
|
+
getConnectionType() {
|
|
2477
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
2478
|
+
if (connection && connection.type) {
|
|
2479
|
+
return connection.type;
|
|
2480
|
+
}
|
|
2481
|
+
return null;
|
|
2482
|
+
}
|
|
2483
|
+
getEffectiveType() {
|
|
2484
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
2485
|
+
if (connection && connection.effectiveType) {
|
|
2486
|
+
return connection.effectiveType;
|
|
2487
|
+
}
|
|
2488
|
+
return null;
|
|
2489
|
+
}
|
|
2490
|
+
getDownlink() {
|
|
2491
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
2492
|
+
if (connection && connection.downlink) {
|
|
2493
|
+
return connection.downlink;
|
|
2494
|
+
}
|
|
2495
|
+
return null;
|
|
2496
|
+
}
|
|
2497
|
+
getRTT() {
|
|
2498
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
2499
|
+
if (connection && connection.rtt) {
|
|
2500
|
+
return connection.rtt;
|
|
2501
|
+
}
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
getSaveData() {
|
|
2505
|
+
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
2506
|
+
if (connection && connection.saveData !== void 0) {
|
|
2507
|
+
return connection.saveData;
|
|
2508
|
+
}
|
|
2509
|
+
return null;
|
|
2510
|
+
}
|
|
2511
|
+
/**
|
|
2512
|
+
* Clear cached signals (useful for testing)
|
|
2513
|
+
*/
|
|
2514
|
+
clearCache() {
|
|
2515
|
+
this.cachedSignals = null;
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
// ../fraud-sdk/src/collectors/form-interactions.ts
|
|
2520
|
+
var FormInteractionCollector = class {
|
|
2521
|
+
constructor() {
|
|
2522
|
+
this.focusCount = 0;
|
|
2523
|
+
this.blurCount = 0;
|
|
2524
|
+
this.copyCount = 0;
|
|
2525
|
+
this.pasteCount = 0;
|
|
2526
|
+
this.cutCount = 0;
|
|
2527
|
+
this.fieldFocusOrder = [];
|
|
2528
|
+
this.fieldFocusTimes = /* @__PURE__ */ new Map();
|
|
2529
|
+
this.backspaceCount = 0;
|
|
2530
|
+
this.deleteCount = 0;
|
|
2531
|
+
this.autofillDetected = false;
|
|
2532
|
+
this.autocompleteDetected = false;
|
|
2533
|
+
this.isActive = false;
|
|
2534
|
+
this.focusHandler = (event) => this.handleFocus(event);
|
|
2535
|
+
this.blurHandler = (event) => this.handleBlur(event);
|
|
2536
|
+
this.copyHandler = (event) => this.handleCopy(event);
|
|
2537
|
+
this.pasteHandler = (event) => this.handlePaste(event);
|
|
2538
|
+
this.cutHandler = (event) => this.handleCut(event);
|
|
2539
|
+
this.keydownHandler = (event) => this.handleKeyDown(event);
|
|
2540
|
+
this.inputHandler = (event) => this.handleInput(event);
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Start collecting form interactions
|
|
2544
|
+
*/
|
|
2545
|
+
start() {
|
|
2546
|
+
if (this.isActive || typeof document === "undefined") return;
|
|
2547
|
+
document.addEventListener("focus", this.focusHandler, true);
|
|
2548
|
+
document.addEventListener("blur", this.blurHandler, true);
|
|
2549
|
+
document.addEventListener("copy", this.copyHandler, true);
|
|
2550
|
+
document.addEventListener("paste", this.pasteHandler, true);
|
|
2551
|
+
document.addEventListener("cut", this.cutHandler, true);
|
|
2552
|
+
document.addEventListener("keydown", this.keydownHandler, true);
|
|
2553
|
+
document.addEventListener("input", this.inputHandler, true);
|
|
2554
|
+
this.isActive = true;
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Stop collecting form interactions
|
|
2558
|
+
*/
|
|
2559
|
+
stop() {
|
|
2560
|
+
if (!this.isActive || typeof document === "undefined") return;
|
|
2561
|
+
document.removeEventListener("focus", this.focusHandler, true);
|
|
2562
|
+
document.removeEventListener("blur", this.blurHandler, true);
|
|
2563
|
+
document.removeEventListener("copy", this.copyHandler, true);
|
|
2564
|
+
document.removeEventListener("paste", this.pasteHandler, true);
|
|
2565
|
+
document.removeEventListener("cut", this.cutHandler, true);
|
|
2566
|
+
document.removeEventListener("keydown", this.keydownHandler, true);
|
|
2567
|
+
document.removeEventListener("input", this.inputHandler, true);
|
|
2568
|
+
this.isActive = false;
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* Get collected form interaction data
|
|
2572
|
+
*/
|
|
2573
|
+
getData() {
|
|
2574
|
+
const timePerField = {};
|
|
2575
|
+
this.fieldFocusTimes.forEach((time, fieldId) => {
|
|
2576
|
+
timePerField[fieldId] = time;
|
|
2577
|
+
});
|
|
2578
|
+
return {
|
|
2579
|
+
focusCount: this.focusCount,
|
|
2580
|
+
blurCount: this.blurCount,
|
|
2581
|
+
copyCount: this.copyCount,
|
|
2582
|
+
pasteCount: this.pasteCount,
|
|
2583
|
+
cutCount: this.cutCount,
|
|
2584
|
+
autofillDetected: this.autofillDetected,
|
|
2585
|
+
autocompleteDetected: this.autocompleteDetected,
|
|
2586
|
+
fieldFocusOrder: [...this.fieldFocusOrder],
|
|
2587
|
+
timePerField,
|
|
2588
|
+
backspaceCount: this.backspaceCount,
|
|
2589
|
+
deleteCount: this.deleteCount
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* Reset all collected data
|
|
2594
|
+
*/
|
|
2595
|
+
reset() {
|
|
2596
|
+
this.focusCount = 0;
|
|
2597
|
+
this.blurCount = 0;
|
|
2598
|
+
this.copyCount = 0;
|
|
2599
|
+
this.pasteCount = 0;
|
|
2600
|
+
this.cutCount = 0;
|
|
2601
|
+
this.fieldFocusOrder = [];
|
|
2602
|
+
this.fieldFocusTimes.clear();
|
|
2603
|
+
this.backspaceCount = 0;
|
|
2604
|
+
this.deleteCount = 0;
|
|
2605
|
+
this.autofillDetected = false;
|
|
2606
|
+
this.autocompleteDetected = false;
|
|
2607
|
+
}
|
|
2608
|
+
handleFocus(event) {
|
|
2609
|
+
const target = event.target;
|
|
2610
|
+
if (!this.isFormField(target)) return;
|
|
2611
|
+
this.focusCount++;
|
|
2612
|
+
const fieldId = this.getFieldId(target);
|
|
2613
|
+
if (fieldId && !this.fieldFocusOrder.includes(fieldId)) {
|
|
2614
|
+
this.fieldFocusOrder.push(fieldId);
|
|
2615
|
+
}
|
|
2616
|
+
this.fieldFocusTimes.set(fieldId || "unknown", Date.now());
|
|
2617
|
+
}
|
|
2618
|
+
handleBlur(event) {
|
|
2619
|
+
const target = event.target;
|
|
2620
|
+
if (!this.isFormField(target)) return;
|
|
2621
|
+
this.blurCount++;
|
|
2622
|
+
const fieldId = this.getFieldId(target);
|
|
2623
|
+
if (fieldId) {
|
|
2624
|
+
const focusTime = this.fieldFocusTimes.get(fieldId) || Date.now();
|
|
2625
|
+
const timeSpent = Date.now() - focusTime;
|
|
2626
|
+
this.fieldFocusTimes.set(fieldId, timeSpent);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
handleCopy(event) {
|
|
2630
|
+
const target = event.target;
|
|
2631
|
+
if (this.isFormField(target)) {
|
|
2632
|
+
this.copyCount++;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
handlePaste(event) {
|
|
2636
|
+
const target = event.target;
|
|
2637
|
+
if (this.isFormField(target)) {
|
|
2638
|
+
this.pasteCount++;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
handleCut(event) {
|
|
2642
|
+
const target = event.target;
|
|
2643
|
+
if (this.isFormField(target)) {
|
|
2644
|
+
this.cutCount++;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
handleKeyDown(event) {
|
|
2648
|
+
const target = event.target;
|
|
2649
|
+
if (!this.isFormField(target)) return;
|
|
2650
|
+
if (event.key === "Backspace") {
|
|
2651
|
+
this.backspaceCount++;
|
|
2652
|
+
} else if (event.key === "Delete") {
|
|
2653
|
+
this.deleteCount++;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
handleInput(event) {
|
|
2657
|
+
const target = event.target;
|
|
2658
|
+
if (!this.isFormField(target)) return;
|
|
2659
|
+
const value = target.value;
|
|
2660
|
+
if (value && target.type === "password") {
|
|
2661
|
+
this.autofillDetected = true;
|
|
2662
|
+
}
|
|
2663
|
+
if (target.hasAttribute("autocomplete") && target.getAttribute("autocomplete") !== "off") {
|
|
2664
|
+
this.autocompleteDetected = true;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
isFormField(element) {
|
|
2668
|
+
if (!element) return false;
|
|
2669
|
+
const tagName = element.tagName.toLowerCase();
|
|
2670
|
+
return tagName === "input" || tagName === "textarea" || tagName === "select" || element.isContentEditable;
|
|
2671
|
+
}
|
|
2672
|
+
getFieldId(element) {
|
|
2673
|
+
if (element.id) return element.id;
|
|
2674
|
+
if ("name" in element && element.name) {
|
|
2675
|
+
return element.name;
|
|
2676
|
+
}
|
|
2677
|
+
if (element.getAttribute("data-field-id")) {
|
|
2678
|
+
return element.getAttribute("data-field-id");
|
|
2679
|
+
}
|
|
2680
|
+
return null;
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
|
|
2684
|
+
// ../fraud-sdk/src/collectors/page-interactions.ts
|
|
2685
|
+
var PageInteractionCollector = class {
|
|
16
2686
|
constructor() {
|
|
2687
|
+
this.pageLoadTime = 0;
|
|
2688
|
+
this.timeToFirstInteraction = 0;
|
|
2689
|
+
this.startTime = Date.now();
|
|
2690
|
+
this.firstInteractionTime = null;
|
|
2691
|
+
this.maxScrollDepth = 0;
|
|
2692
|
+
this.clickCount = 0;
|
|
2693
|
+
this.linkClickCount = 0;
|
|
2694
|
+
this.imageViewCount = 0;
|
|
2695
|
+
this.videoPlayCount = 0;
|
|
2696
|
+
this.isActive = false;
|
|
2697
|
+
this.clickHandler = (event) => this.handleClick(event);
|
|
2698
|
+
this.scrollHandler = () => this.handleScroll();
|
|
2699
|
+
this.loadHandler = () => this.handleLoad();
|
|
2700
|
+
this.imageLoadHandler = () => this.handleImageLoad();
|
|
2701
|
+
this.videoPlayHandler = () => this.handleVideoPlay();
|
|
2702
|
+
this.startTime = performance.now();
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* Start collecting page interactions
|
|
2706
|
+
*/
|
|
2707
|
+
start() {
|
|
2708
|
+
if (this.isActive || typeof document === "undefined") return;
|
|
2709
|
+
if (document.readyState === "complete") {
|
|
2710
|
+
this.pageLoadTime = performance.now() - this.startTime;
|
|
2711
|
+
} else {
|
|
2712
|
+
window.addEventListener("load", this.loadHandler);
|
|
2713
|
+
}
|
|
2714
|
+
document.addEventListener("click", this.clickHandler, true);
|
|
2715
|
+
window.addEventListener("scroll", this.scrollHandler, { passive: true });
|
|
2716
|
+
document.addEventListener("load", this.imageLoadHandler, true);
|
|
2717
|
+
document.addEventListener("play", this.videoPlayHandler, true);
|
|
2718
|
+
this.isActive = true;
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* Stop collecting page interactions
|
|
2722
|
+
*/
|
|
2723
|
+
stop() {
|
|
2724
|
+
if (!this.isActive || typeof document === "undefined") return;
|
|
2725
|
+
window.removeEventListener("load", this.loadHandler);
|
|
2726
|
+
document.removeEventListener("click", this.clickHandler, true);
|
|
2727
|
+
window.removeEventListener("scroll", this.scrollHandler);
|
|
2728
|
+
document.removeEventListener("load", this.imageLoadHandler, true);
|
|
2729
|
+
document.removeEventListener("play", this.videoPlayHandler, true);
|
|
2730
|
+
this.isActive = false;
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Get collected page interaction data
|
|
2734
|
+
*/
|
|
2735
|
+
getData() {
|
|
2736
|
+
const timeOnPage = Date.now() - this.startTime;
|
|
2737
|
+
const scrollDepth = this.calculateScrollDepth();
|
|
2738
|
+
return {
|
|
2739
|
+
pageLoadTime: this.pageLoadTime,
|
|
2740
|
+
timeToFirstInteraction: this.firstInteractionTime ? this.firstInteractionTime - this.startTime : 0,
|
|
2741
|
+
timeOnPage,
|
|
2742
|
+
scrollDepth,
|
|
2743
|
+
clickCount: this.clickCount,
|
|
2744
|
+
linkClickCount: this.linkClickCount,
|
|
2745
|
+
imageViewCount: this.imageViewCount,
|
|
2746
|
+
videoPlayCount: this.videoPlayCount
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Reset all collected data
|
|
2751
|
+
*/
|
|
2752
|
+
reset() {
|
|
2753
|
+
this.pageLoadTime = 0;
|
|
2754
|
+
this.timeToFirstInteraction = 0;
|
|
2755
|
+
this.startTime = Date.now();
|
|
2756
|
+
this.firstInteractionTime = null;
|
|
2757
|
+
this.maxScrollDepth = 0;
|
|
2758
|
+
this.clickCount = 0;
|
|
2759
|
+
this.linkClickCount = 0;
|
|
2760
|
+
this.imageViewCount = 0;
|
|
2761
|
+
this.videoPlayCount = 0;
|
|
2762
|
+
}
|
|
2763
|
+
handleLoad() {
|
|
2764
|
+
this.pageLoadTime = performance.now() - this.startTime;
|
|
2765
|
+
}
|
|
2766
|
+
handleClick(event) {
|
|
2767
|
+
if (!this.firstInteractionTime) {
|
|
2768
|
+
this.firstInteractionTime = performance.now();
|
|
2769
|
+
}
|
|
2770
|
+
this.clickCount++;
|
|
2771
|
+
const target = event.target;
|
|
2772
|
+
if (target.tagName.toLowerCase() === "a" || target.closest("a")) {
|
|
2773
|
+
this.linkClickCount++;
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
handleScroll() {
|
|
2777
|
+
const scrollDepth = this.calculateScrollDepth();
|
|
2778
|
+
this.maxScrollDepth = Math.max(this.maxScrollDepth, scrollDepth);
|
|
2779
|
+
}
|
|
2780
|
+
handleImageLoad() {
|
|
2781
|
+
this.imageViewCount++;
|
|
2782
|
+
}
|
|
2783
|
+
handleVideoPlay() {
|
|
2784
|
+
this.videoPlayCount++;
|
|
2785
|
+
}
|
|
2786
|
+
calculateScrollDepth() {
|
|
2787
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
2788
|
+
return 0;
|
|
2789
|
+
}
|
|
2790
|
+
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
2791
|
+
const documentHeight = Math.max(
|
|
2792
|
+
document.body.scrollHeight,
|
|
2793
|
+
document.body.offsetHeight,
|
|
2794
|
+
document.documentElement.clientHeight,
|
|
2795
|
+
document.documentElement.scrollHeight,
|
|
2796
|
+
document.documentElement.offsetHeight
|
|
2797
|
+
);
|
|
2798
|
+
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
2799
|
+
if (documentHeight === 0) return 0;
|
|
2800
|
+
const scrolled = scrollTop + windowHeight;
|
|
2801
|
+
return Math.min(scrolled / documentHeight, 1);
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
|
|
2805
|
+
// ../fraud-sdk/src/core/sdk.ts
|
|
2806
|
+
var DEFAULT_ENDPOINT = "https://api.keverd.com";
|
|
2807
|
+
var KeverdSDK = class {
|
|
2808
|
+
// Maximum 10 minutes of collection
|
|
2809
|
+
constructor() {
|
|
2810
|
+
this.config = null;
|
|
2811
|
+
this.isInitialized = false;
|
|
2812
|
+
this.sessionId = null;
|
|
2813
|
+
this.ghostInterval = null;
|
|
2814
|
+
this.ghostStartTime = null;
|
|
2815
|
+
this.ghostSignalCount = 0;
|
|
2816
|
+
this.MIN_GHOST_SIGNALS = 10;
|
|
2817
|
+
// Collect at least 10 signals (5 minutes of data)
|
|
2818
|
+
this.MAX_GHOST_COLLECTION_TIME = 10 * 60 * 1e3;
|
|
2819
|
+
this.deviceCollector = new DeviceCollector();
|
|
2820
|
+
this.behavioralCollector = new BehavioralCollector();
|
|
2821
|
+
this.fingerprintManager = new FingerprintManager();
|
|
2822
|
+
this.privacySignalCollector = new PrivacySignalCollector();
|
|
2823
|
+
this.browserCapabilityCollector = new BrowserCapabilityCollector();
|
|
2824
|
+
this.hardwareSignalCollector = new HardwareSignalCollector();
|
|
2825
|
+
this.formInteractionCollector = new FormInteractionCollector();
|
|
2826
|
+
this.pageInteractionCollector = new PageInteractionCollector();
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* Initialize the SDK with configuration
|
|
2830
|
+
*/
|
|
2831
|
+
init(config) {
|
|
2832
|
+
if (this.isInitialized) {
|
|
2833
|
+
if (this.config?.debug) {
|
|
2834
|
+
console.warn("[Keverd SDK] Already initialized");
|
|
2835
|
+
}
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
if (typeof config === "string") {
|
|
2839
|
+
this.config = {
|
|
2840
|
+
apiKey: config,
|
|
2841
|
+
debug: false
|
|
2842
|
+
};
|
|
2843
|
+
} else {
|
|
2844
|
+
this.config = {
|
|
2845
|
+
debug: false,
|
|
2846
|
+
...config
|
|
2847
|
+
};
|
|
2848
|
+
}
|
|
2849
|
+
this.behavioralCollector.start();
|
|
2850
|
+
this.formInteractionCollector.start();
|
|
2851
|
+
this.pageInteractionCollector.start();
|
|
2852
|
+
this.sessionId = this.generateSessionId();
|
|
2853
|
+
this.isInitialized = true;
|
|
2854
|
+
if (this.config.debug) {
|
|
2855
|
+
console.log("[Keverd SDK] Initialized successfully");
|
|
2856
|
+
}
|
|
2857
|
+
this.startSession().catch(() => {
|
|
2858
|
+
});
|
|
2859
|
+
this.startGhostSignalCollection();
|
|
2860
|
+
}
|
|
2861
|
+
startGhostSignalCollection() {
|
|
2862
|
+
if (this.ghostInterval) return;
|
|
2863
|
+
if (!this.config) return;
|
|
2864
|
+
this.ghostStartTime = Date.now();
|
|
2865
|
+
this.ghostSignalCount = 0;
|
|
2866
|
+
const intervalMs = 3e4;
|
|
2867
|
+
const sendGhostSignal = () => {
|
|
2868
|
+
const now = Date.now();
|
|
2869
|
+
const totalCollectionTime = this.ghostStartTime ? now - this.ghostStartTime : 0;
|
|
2870
|
+
const hasEnoughSignals = this.ghostSignalCount >= this.MIN_GHOST_SIGNALS;
|
|
2871
|
+
const exceededMaxTime = totalCollectionTime >= this.MAX_GHOST_COLLECTION_TIME;
|
|
2872
|
+
if (hasEnoughSignals || exceededMaxTime) {
|
|
2873
|
+
if (this.config?.debug) {
|
|
2874
|
+
console.log(`[Keverd SDK] Ghost collection complete: ${this.ghostSignalCount} signals, ${Math.round(totalCollectionTime / 1e3)}s elapsed`);
|
|
2875
|
+
}
|
|
2876
|
+
this.stopGhostSignalCollection();
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const durationMs = this.ghostStartTime ? now - this.ghostStartTime : intervalMs;
|
|
2880
|
+
this.sendGhostSignals(durationMs).catch((err) => {
|
|
2881
|
+
if (this.config?.debug) {
|
|
2882
|
+
console.debug("[Keverd SDK] Ghost signals upload failed:", err);
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
this.ghostSignalCount++;
|
|
2886
|
+
this.ghostStartTime = now;
|
|
2887
|
+
};
|
|
2888
|
+
setTimeout(() => {
|
|
2889
|
+
sendGhostSignal();
|
|
2890
|
+
this.ghostInterval = setInterval(() => {
|
|
2891
|
+
sendGhostSignal();
|
|
2892
|
+
}, intervalMs);
|
|
2893
|
+
}, intervalMs);
|
|
2894
|
+
}
|
|
2895
|
+
stopGhostSignalCollection() {
|
|
2896
|
+
if (this.ghostInterval) {
|
|
2897
|
+
clearInterval(this.ghostInterval);
|
|
2898
|
+
this.ghostInterval = null;
|
|
2899
|
+
this.ghostStartTime = null;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
async sendGhostSignals(durationMs) {
|
|
2903
|
+
if (!this.isInitialized || !this.config) return;
|
|
2904
|
+
const deviceInfo = this.deviceCollector.collect();
|
|
2905
|
+
const behavioralData = this.behavioralCollector.getData();
|
|
2906
|
+
const fingerprintData = await this.fingerprintManager.collect();
|
|
2907
|
+
const fingerprintjsData = {
|
|
2908
|
+
visitorId: fingerprintData.visitorId,
|
|
2909
|
+
confidence: fingerprintData.confidence,
|
|
2910
|
+
components: fingerprintData.components
|
|
2911
|
+
};
|
|
2912
|
+
const privacySignals = this.privacySignalCollector.collect();
|
|
2913
|
+
const browserCapabilities = this.browserCapabilityCollector.collect();
|
|
2914
|
+
const hardwareSignals = this.hardwareSignalCollector.collect();
|
|
2915
|
+
const formInteractions = this.formInteractionCollector.getData();
|
|
2916
|
+
const mouseSignals = this.behavioralCollector.getMouseSignals();
|
|
2917
|
+
const keyboardSignals = this.behavioralCollector.getKeyboardSignals();
|
|
2918
|
+
const pageInteractions = this.pageInteractionCollector.getData();
|
|
2919
|
+
const networkSignals = {
|
|
2920
|
+
connectionType: hardwareSignals.connectionType || void 0,
|
|
2921
|
+
effectiveType: hardwareSignals.effectiveType || void 0,
|
|
2922
|
+
downlink: hardwareSignals.downlink || void 0,
|
|
2923
|
+
rtt: hardwareSignals.rtt || void 0,
|
|
2924
|
+
saveData: hardwareSignals.saveData || void 0
|
|
2925
|
+
};
|
|
2926
|
+
const sessionInfo = {
|
|
2927
|
+
sessionId: this.sessionId || void 0,
|
|
2928
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2929
|
+
};
|
|
2930
|
+
const request = {
|
|
2931
|
+
userId: this.config.userId,
|
|
2932
|
+
device: deviceInfo,
|
|
2933
|
+
session: sessionInfo,
|
|
2934
|
+
behavioral: behavioralData,
|
|
2935
|
+
duration_ms: durationMs,
|
|
2936
|
+
fingerprintjs: fingerprintjsData || void 0,
|
|
2937
|
+
privacySignals,
|
|
2938
|
+
browserCapabilities,
|
|
2939
|
+
hardwareSignals,
|
|
2940
|
+
formInteractions,
|
|
2941
|
+
mouseSignals,
|
|
2942
|
+
keyboardSignals,
|
|
2943
|
+
pageInteractions,
|
|
2944
|
+
networkSignals,
|
|
2945
|
+
useCase: "ghost"
|
|
2946
|
+
};
|
|
2947
|
+
const url = `${DEFAULT_ENDPOINT}/fingerprint/ghost`;
|
|
2948
|
+
const headers = {
|
|
2949
|
+
"Content-Type": "application/json",
|
|
2950
|
+
"X-SDK-Source": "javascript"
|
|
2951
|
+
};
|
|
2952
|
+
const apiKey = this.config.apiKey;
|
|
2953
|
+
if (apiKey) {
|
|
2954
|
+
headers["x-keverd-key"] = apiKey;
|
|
2955
|
+
headers["X-API-KEY"] = apiKey;
|
|
2956
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
2957
|
+
}
|
|
2958
|
+
await fetch(url, {
|
|
2959
|
+
method: "POST",
|
|
2960
|
+
headers,
|
|
2961
|
+
body: JSON.stringify(request)
|
|
2962
|
+
}).catch(() => {
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Get visitor data (fingerprint and risk assessment)
|
|
2967
|
+
* This is the main method for getting risk scores
|
|
2968
|
+
*/
|
|
2969
|
+
async getVisitorData() {
|
|
2970
|
+
if (!this.isInitialized || !this.config) {
|
|
2971
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
2972
|
+
}
|
|
2973
|
+
try {
|
|
2974
|
+
const deviceInfo = this.deviceCollector.collect();
|
|
2975
|
+
const behavioralData = this.behavioralCollector.getData();
|
|
2976
|
+
const fingerprintData = await this.fingerprintManager.collect();
|
|
2977
|
+
const fingerprintjsData = {
|
|
2978
|
+
visitorId: fingerprintData.visitorId,
|
|
2979
|
+
confidence: fingerprintData.confidence,
|
|
2980
|
+
components: fingerprintData.components
|
|
2981
|
+
};
|
|
2982
|
+
const privacySignals = this.privacySignalCollector.collect();
|
|
2983
|
+
const browserCapabilities = this.browserCapabilityCollector.collect();
|
|
2984
|
+
const hardwareSignals = this.hardwareSignalCollector.collect();
|
|
2985
|
+
const formInteractions = this.formInteractionCollector.getData();
|
|
2986
|
+
const mouseSignals = this.behavioralCollector.getMouseSignals();
|
|
2987
|
+
const keyboardSignals = this.behavioralCollector.getKeyboardSignals();
|
|
2988
|
+
const pageInteractions = this.pageInteractionCollector.getData();
|
|
2989
|
+
const networkSignals = {
|
|
2990
|
+
connectionType: hardwareSignals.connectionType || void 0,
|
|
2991
|
+
effectiveType: hardwareSignals.effectiveType || void 0,
|
|
2992
|
+
downlink: hardwareSignals.downlink || void 0,
|
|
2993
|
+
rtt: hardwareSignals.rtt || void 0,
|
|
2994
|
+
saveData: hardwareSignals.saveData || void 0
|
|
2995
|
+
};
|
|
2996
|
+
const sessionInfo = {
|
|
2997
|
+
sessionId: this.sessionId || void 0,
|
|
2998
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2999
|
+
};
|
|
3000
|
+
const request = {
|
|
3001
|
+
userId: this.config.userId,
|
|
3002
|
+
device: deviceInfo,
|
|
3003
|
+
session: sessionInfo,
|
|
3004
|
+
behavioral: behavioralData,
|
|
3005
|
+
fingerprintjs: fingerprintjsData || void 0,
|
|
3006
|
+
privacySignals,
|
|
3007
|
+
browserCapabilities,
|
|
3008
|
+
hardwareSignals,
|
|
3009
|
+
formInteractions,
|
|
3010
|
+
mouseSignals,
|
|
3011
|
+
keyboardSignals,
|
|
3012
|
+
pageInteractions,
|
|
3013
|
+
networkSignals,
|
|
3014
|
+
useCase: "general"
|
|
3015
|
+
// Note: sim field is NOT included for web SDKs
|
|
3016
|
+
};
|
|
3017
|
+
const response = await this.sendFingerprintRequest(request);
|
|
3018
|
+
return response;
|
|
3019
|
+
} catch (error) {
|
|
3020
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
3021
|
+
if (this.config.debug) {
|
|
3022
|
+
console.error("[Keverd SDK] Error getting visitor data:", error);
|
|
3023
|
+
}
|
|
3024
|
+
throw new Error(`Failed to get visitor data: ${errorMessage}`);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Create transaction ID with risk assessment (legacy method)
|
|
3029
|
+
* For new implementations, use getVisitorData() instead
|
|
3030
|
+
*/
|
|
3031
|
+
async createTransactionID(_metadata) {
|
|
3032
|
+
const response = await this.getVisitorData();
|
|
3033
|
+
return response.session_id;
|
|
3034
|
+
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Verify user identity during login attempts
|
|
3037
|
+
*/
|
|
3038
|
+
async verifyLogin(userId, metadata) {
|
|
3039
|
+
if (!this.isInitialized || !this.config) {
|
|
3040
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
3041
|
+
}
|
|
3042
|
+
const response = await this.sendVerifyRequest("login", { userId, metadata });
|
|
3043
|
+
return response;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Verify user during checkout/payment
|
|
3047
|
+
*/
|
|
3048
|
+
async verifyCheckout(amount, currency, metadata) {
|
|
3049
|
+
if (!this.isInitialized || !this.config) {
|
|
3050
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
3051
|
+
}
|
|
3052
|
+
const response = await this.sendVerifyRequest("checkout", { amount, currency, metadata });
|
|
3053
|
+
return response;
|
|
3054
|
+
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Detect fake/bot registrations
|
|
3057
|
+
*/
|
|
3058
|
+
async verifyRegistration(metadata) {
|
|
3059
|
+
if (!this.isInitialized || !this.config) {
|
|
3060
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
3061
|
+
}
|
|
3062
|
+
const response = await this.sendVerifyRequest("registration", { metadata });
|
|
3063
|
+
return response;
|
|
3064
|
+
}
|
|
3065
|
+
/**
|
|
3066
|
+
* Detect account takeover attempts during password reset
|
|
3067
|
+
*/
|
|
3068
|
+
async verifyPasswordReset(email, metadata) {
|
|
3069
|
+
if (!this.isInitialized || !this.config) {
|
|
3070
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
3071
|
+
}
|
|
3072
|
+
const response = await this.sendVerifyRequest("password_reset", { email, metadata });
|
|
3073
|
+
return response;
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Verify sensitive account changes (email, phone, password)
|
|
3077
|
+
*/
|
|
3078
|
+
async verifyAccountChange(changeType, metadata) {
|
|
3079
|
+
if (!this.isInitialized || !this.config) {
|
|
3080
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
3081
|
+
}
|
|
3082
|
+
const response = await this.sendVerifyRequest("account_change", { changeType, metadata });
|
|
3083
|
+
return response;
|
|
3084
|
+
}
|
|
3085
|
+
/**
|
|
3086
|
+
* Helper to send verify request to use-case-specific endpoint
|
|
3087
|
+
*/
|
|
3088
|
+
async sendVerifyRequest(useCase, options) {
|
|
3089
|
+
if (!this.config) {
|
|
3090
|
+
throw new Error("SDK not initialized");
|
|
3091
|
+
}
|
|
3092
|
+
const deviceInfo = this.deviceCollector.collect();
|
|
3093
|
+
const behavioralData = this.behavioralCollector.getData();
|
|
3094
|
+
const fingerprintData = await this.fingerprintManager.collect();
|
|
3095
|
+
const fingerprintjsData = {
|
|
3096
|
+
visitorId: fingerprintData.visitorId,
|
|
3097
|
+
confidence: fingerprintData.confidence,
|
|
3098
|
+
components: fingerprintData.components
|
|
3099
|
+
};
|
|
3100
|
+
const privacySignals = this.privacySignalCollector.collect();
|
|
3101
|
+
const browserCapabilities = this.browserCapabilityCollector.collect();
|
|
3102
|
+
const hardwareSignals = this.hardwareSignalCollector.collect();
|
|
3103
|
+
const formInteractions = this.formInteractionCollector.getData();
|
|
3104
|
+
const mouseSignals = this.behavioralCollector.getMouseSignals();
|
|
3105
|
+
const keyboardSignals = this.behavioralCollector.getKeyboardSignals();
|
|
3106
|
+
const pageInteractions = this.pageInteractionCollector.getData();
|
|
3107
|
+
const networkSignals = {
|
|
3108
|
+
connectionType: hardwareSignals.connectionType || void 0,
|
|
3109
|
+
effectiveType: hardwareSignals.effectiveType || void 0,
|
|
3110
|
+
downlink: hardwareSignals.downlink || void 0,
|
|
3111
|
+
rtt: hardwareSignals.rtt || void 0,
|
|
3112
|
+
saveData: hardwareSignals.saveData || void 0
|
|
3113
|
+
};
|
|
3114
|
+
const sessionInfo = {
|
|
3115
|
+
sessionId: this.sessionId || void 0,
|
|
3116
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3117
|
+
};
|
|
3118
|
+
const request = {
|
|
3119
|
+
userId: options.userId || this.config.userId,
|
|
3120
|
+
device: deviceInfo,
|
|
3121
|
+
session: sessionInfo,
|
|
3122
|
+
behavioral: behavioralData,
|
|
3123
|
+
fingerprintjs: fingerprintjsData || void 0,
|
|
3124
|
+
privacySignals,
|
|
3125
|
+
browserCapabilities,
|
|
3126
|
+
hardwareSignals,
|
|
3127
|
+
formInteractions,
|
|
3128
|
+
mouseSignals,
|
|
3129
|
+
keyboardSignals,
|
|
3130
|
+
pageInteractions,
|
|
3131
|
+
networkSignals,
|
|
3132
|
+
useCase
|
|
3133
|
+
};
|
|
3134
|
+
if (options.metadata) {
|
|
3135
|
+
Object.assign(request, options.metadata);
|
|
3136
|
+
}
|
|
3137
|
+
if (options.amount !== void 0) {
|
|
3138
|
+
request.amount = options.amount;
|
|
3139
|
+
}
|
|
3140
|
+
if (options.currency) {
|
|
3141
|
+
request.currency = options.currency;
|
|
3142
|
+
}
|
|
3143
|
+
if (options.email) {
|
|
3144
|
+
request.email = options.email;
|
|
3145
|
+
}
|
|
3146
|
+
if (options.changeType) {
|
|
3147
|
+
request.changeType = options.changeType;
|
|
3148
|
+
}
|
|
3149
|
+
const url = `${DEFAULT_ENDPOINT}/fingerprint/verify/${useCase}`;
|
|
3150
|
+
const headers = {
|
|
3151
|
+
"Content-Type": "application/json",
|
|
3152
|
+
"X-SDK-Source": "javascript"
|
|
3153
|
+
};
|
|
3154
|
+
const apiKey = this.config.apiKey;
|
|
3155
|
+
if (apiKey) {
|
|
3156
|
+
headers["x-keverd-key"] = apiKey;
|
|
3157
|
+
headers["X-API-KEY"] = apiKey;
|
|
3158
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3159
|
+
}
|
|
3160
|
+
const response = await fetch(url, {
|
|
3161
|
+
method: "POST",
|
|
3162
|
+
headers,
|
|
3163
|
+
body: JSON.stringify(request)
|
|
3164
|
+
});
|
|
3165
|
+
if (!response.ok) {
|
|
3166
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
3167
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
3168
|
+
}
|
|
3169
|
+
const data = await response.json();
|
|
3170
|
+
return data;
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Send fingerprint request to backend
|
|
3174
|
+
*/
|
|
3175
|
+
async sendFingerprintRequest(request) {
|
|
3176
|
+
if (!this.config) {
|
|
3177
|
+
throw new Error("SDK not initialized");
|
|
3178
|
+
}
|
|
3179
|
+
const url = `${DEFAULT_ENDPOINT}/fingerprint/score`;
|
|
3180
|
+
const headers = {
|
|
3181
|
+
"Content-Type": "application/json",
|
|
3182
|
+
"X-SDK-Source": "javascript"
|
|
3183
|
+
// Identify SDK source for backend analytics
|
|
3184
|
+
};
|
|
3185
|
+
const apiKey = this.config.apiKey;
|
|
3186
|
+
if (apiKey) {
|
|
3187
|
+
headers["x-keverd-key"] = apiKey;
|
|
3188
|
+
headers["X-API-KEY"] = apiKey;
|
|
3189
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3190
|
+
}
|
|
3191
|
+
const response = await fetch(url, {
|
|
3192
|
+
method: "POST",
|
|
3193
|
+
headers,
|
|
3194
|
+
body: JSON.stringify(request)
|
|
3195
|
+
});
|
|
3196
|
+
if (!response.ok) {
|
|
3197
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
3198
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
3199
|
+
}
|
|
3200
|
+
const data = await response.json();
|
|
3201
|
+
return data;
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Generate a unique session ID
|
|
3205
|
+
*/
|
|
3206
|
+
generateSessionId() {
|
|
3207
|
+
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Detect browser name from user agent
|
|
3211
|
+
*/
|
|
3212
|
+
_detectBrowser() {
|
|
3213
|
+
const ua = navigator.userAgent;
|
|
3214
|
+
if (ua.includes("Chrome")) return "Chrome";
|
|
3215
|
+
if (ua.includes("Firefox")) return "Firefox";
|
|
3216
|
+
if (ua.includes("Safari") && !ua.includes("Chrome")) return "Safari";
|
|
3217
|
+
if (ua.includes("Edge")) return "Edge";
|
|
3218
|
+
if (ua.includes("Opera")) return "Opera";
|
|
3219
|
+
return void 0;
|
|
3220
|
+
}
|
|
3221
|
+
/**
|
|
3222
|
+
* Detect OS from user agent
|
|
3223
|
+
*/
|
|
3224
|
+
_detectOS() {
|
|
3225
|
+
const ua = navigator.userAgent;
|
|
3226
|
+
if (ua.includes("Windows")) return "Windows";
|
|
3227
|
+
if (ua.includes("Mac OS")) return "macOS";
|
|
3228
|
+
if (ua.includes("Linux")) return "Linux";
|
|
3229
|
+
if (ua.includes("Android")) return "Android";
|
|
3230
|
+
if (ua.includes("iOS") || ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
|
|
3231
|
+
return void 0;
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Start a new session (called automatically on init, but can be called manually)
|
|
3235
|
+
*/
|
|
3236
|
+
async startSession(userId, deviceHash, metadata) {
|
|
3237
|
+
if (!this.isInitialized || !this.config) {
|
|
3238
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
3239
|
+
}
|
|
3240
|
+
if (!this.sessionId) {
|
|
3241
|
+
this.sessionId = this.generateSessionId();
|
|
3242
|
+
}
|
|
3243
|
+
try {
|
|
3244
|
+
const url = `${DEFAULT_ENDPOINT}/dashboard/sessions/start`;
|
|
3245
|
+
const headers = {
|
|
3246
|
+
"Content-Type": "application/json"
|
|
3247
|
+
};
|
|
3248
|
+
const apiKey = this.config.apiKey;
|
|
3249
|
+
if (apiKey) {
|
|
3250
|
+
headers["x-keverd-key"] = apiKey;
|
|
3251
|
+
headers["X-API-KEY"] = apiKey;
|
|
3252
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3253
|
+
}
|
|
3254
|
+
const deviceInfo = this.deviceCollector.collect();
|
|
3255
|
+
await fetch(url, {
|
|
3256
|
+
method: "POST",
|
|
3257
|
+
headers,
|
|
3258
|
+
body: JSON.stringify({
|
|
3259
|
+
session_id: this.sessionId,
|
|
3260
|
+
user_id: userId || this.config.userId,
|
|
3261
|
+
device_hash: deviceHash || deviceInfo.fingerprint,
|
|
3262
|
+
metadata: metadata || {},
|
|
3263
|
+
user_agent: navigator.userAgent,
|
|
3264
|
+
browser: this._detectBrowser(),
|
|
3265
|
+
os: this._detectOS(),
|
|
3266
|
+
platform: "web",
|
|
3267
|
+
sdk_type: "web",
|
|
3268
|
+
sdk_source: "javascript"
|
|
3269
|
+
})
|
|
3270
|
+
});
|
|
3271
|
+
if (this.config.debug) {
|
|
3272
|
+
console.log(`[Keverd SDK] Session started: ${this.sessionId}`);
|
|
3273
|
+
}
|
|
3274
|
+
} catch (error) {
|
|
3275
|
+
if (this.config.debug) {
|
|
3276
|
+
console.warn("[Keverd SDK] Failed to start session on server:", error);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
/**
|
|
3281
|
+
* End the current session
|
|
3282
|
+
*/
|
|
3283
|
+
async endSession() {
|
|
3284
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
try {
|
|
3288
|
+
const url = `${DEFAULT_ENDPOINT}/dashboard/sessions/${this.sessionId}/end`;
|
|
3289
|
+
const headers = {
|
|
3290
|
+
"Content-Type": "application/json"
|
|
3291
|
+
};
|
|
3292
|
+
const apiKey = this.config.apiKey;
|
|
3293
|
+
if (apiKey) {
|
|
3294
|
+
headers["x-keverd-key"] = apiKey;
|
|
3295
|
+
headers["X-API-KEY"] = apiKey;
|
|
3296
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3297
|
+
}
|
|
3298
|
+
await fetch(url, {
|
|
3299
|
+
method: "POST",
|
|
3300
|
+
headers
|
|
3301
|
+
});
|
|
3302
|
+
if (this.config.debug) {
|
|
3303
|
+
console.log(`[Keverd SDK] Session ended: ${this.sessionId}`);
|
|
3304
|
+
}
|
|
3305
|
+
} catch (error) {
|
|
3306
|
+
if (this.config.debug) {
|
|
3307
|
+
console.warn("[Keverd SDK] Failed to end session on server:", error);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Pause the current session (e.g., when app goes to background)
|
|
3313
|
+
*/
|
|
3314
|
+
async pauseSession() {
|
|
3315
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
try {
|
|
3319
|
+
const url = `${DEFAULT_ENDPOINT}/dashboard/sessions/${this.sessionId}/pause`;
|
|
3320
|
+
const headers = {
|
|
3321
|
+
"Content-Type": "application/json"
|
|
3322
|
+
};
|
|
3323
|
+
const apiKey = this.config.apiKey;
|
|
3324
|
+
if (apiKey) {
|
|
3325
|
+
headers["x-keverd-key"] = apiKey;
|
|
3326
|
+
headers["X-API-KEY"] = apiKey;
|
|
3327
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3328
|
+
}
|
|
3329
|
+
await fetch(url, {
|
|
3330
|
+
method: "POST",
|
|
3331
|
+
headers
|
|
3332
|
+
});
|
|
3333
|
+
if (this.config.debug) {
|
|
3334
|
+
console.log(`[Keverd SDK] Session paused: ${this.sessionId}`);
|
|
3335
|
+
}
|
|
3336
|
+
} catch (error) {
|
|
3337
|
+
if (this.config.debug) {
|
|
3338
|
+
console.warn("[Keverd SDK] Failed to pause session on server:", error);
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
/**
|
|
3343
|
+
* Resume a paused session (e.g., when app comes to foreground)
|
|
3344
|
+
*/
|
|
3345
|
+
async resumeSession() {
|
|
3346
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
try {
|
|
3350
|
+
const url = `${DEFAULT_ENDPOINT}/dashboard/sessions/${this.sessionId}/resume`;
|
|
3351
|
+
const headers = {
|
|
3352
|
+
"Content-Type": "application/json"
|
|
3353
|
+
};
|
|
3354
|
+
const apiKey = this.config.apiKey;
|
|
3355
|
+
if (apiKey) {
|
|
3356
|
+
headers["x-keverd-key"] = apiKey;
|
|
3357
|
+
headers["X-API-KEY"] = apiKey;
|
|
3358
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3359
|
+
}
|
|
3360
|
+
await fetch(url, {
|
|
3361
|
+
method: "POST",
|
|
3362
|
+
headers
|
|
3363
|
+
});
|
|
3364
|
+
if (this.config.debug) {
|
|
3365
|
+
console.log(`[Keverd SDK] Session resumed: ${this.sessionId}`);
|
|
3366
|
+
}
|
|
3367
|
+
} catch (error) {
|
|
3368
|
+
if (this.config.debug) {
|
|
3369
|
+
console.warn("[Keverd SDK] Failed to resume session on server:", error);
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
/**
|
|
3374
|
+
* Get current session status
|
|
3375
|
+
*/
|
|
3376
|
+
async getSessionStatus() {
|
|
3377
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
3378
|
+
return null;
|
|
3379
|
+
}
|
|
3380
|
+
try {
|
|
3381
|
+
const url = `${DEFAULT_ENDPOINT}/dashboard/sessions/${this.sessionId}/status`;
|
|
3382
|
+
const headers = {};
|
|
3383
|
+
const apiKey = this.config.apiKey;
|
|
3384
|
+
if (apiKey) {
|
|
3385
|
+
headers["x-keverd-key"] = apiKey;
|
|
3386
|
+
headers["X-API-KEY"] = apiKey;
|
|
3387
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
3388
|
+
}
|
|
3389
|
+
const response = await fetch(url, {
|
|
3390
|
+
method: "GET",
|
|
3391
|
+
headers
|
|
3392
|
+
});
|
|
3393
|
+
if (!response.ok) {
|
|
3394
|
+
return null;
|
|
3395
|
+
}
|
|
3396
|
+
return await response.json();
|
|
3397
|
+
} catch (error) {
|
|
3398
|
+
if (this.config.debug) {
|
|
3399
|
+
console.warn("[Keverd SDK] Failed to get session status:", error);
|
|
3400
|
+
}
|
|
3401
|
+
return null;
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Destroy the SDK instance
|
|
3406
|
+
*/
|
|
3407
|
+
async destroy() {
|
|
3408
|
+
if (this.sessionId) {
|
|
3409
|
+
await this.endSession();
|
|
3410
|
+
}
|
|
3411
|
+
this.behavioralCollector.stop();
|
|
3412
|
+
this.formInteractionCollector.stop();
|
|
3413
|
+
this.pageInteractionCollector.stop();
|
|
3414
|
+
this.fingerprintManager.clearCache();
|
|
3415
|
+
this.isInitialized = false;
|
|
3416
|
+
this.stopGhostSignalCollection();
|
|
3417
|
+
this.ghostSignalCount = 0;
|
|
3418
|
+
if (this.config?.debug) {
|
|
3419
|
+
console.log("[Keverd SDK] Destroyed");
|
|
3420
|
+
}
|
|
17
3421
|
this.config = null;
|
|
3422
|
+
this.sessionId = null;
|
|
3423
|
+
}
|
|
3424
|
+
/**
|
|
3425
|
+
* Check if SDK is initialized and ready to use
|
|
3426
|
+
*/
|
|
3427
|
+
isReady() {
|
|
3428
|
+
return this.isInitialized && this.config !== null;
|
|
18
3429
|
}
|
|
3430
|
+
};
|
|
3431
|
+
var Keverd = new KeverdSDK();
|
|
3432
|
+
|
|
3433
|
+
// src/services/keverd.service.ts
|
|
3434
|
+
exports.KeverdService = class KeverdService {
|
|
3435
|
+
constructor(config) {
|
|
3436
|
+
this.config = null;
|
|
3437
|
+
this.isInitialized = false;
|
|
3438
|
+
if (config) {
|
|
3439
|
+
this.init(config);
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Initialize the SDK with configuration
|
|
3444
|
+
* Wraps the web SDK initialization
|
|
3445
|
+
*/
|
|
19
3446
|
init(config) {
|
|
20
|
-
this.
|
|
3447
|
+
if (this.isInitialized) {
|
|
3448
|
+
if (this.config?.debug) {
|
|
3449
|
+
console.warn("[Keverd SDK] Already initialized");
|
|
3450
|
+
}
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
if (!config.apiKey) {
|
|
3454
|
+
throw new Error("Keverd SDK: apiKey is required");
|
|
3455
|
+
}
|
|
3456
|
+
this.config = {
|
|
3457
|
+
debug: false,
|
|
3458
|
+
...config
|
|
3459
|
+
};
|
|
3460
|
+
Keverd.init({
|
|
3461
|
+
apiKey: this.config.apiKey,
|
|
3462
|
+
userId: this.config.userId,
|
|
3463
|
+
debug: this.config.debug || false
|
|
3464
|
+
});
|
|
3465
|
+
this.isInitialized = true;
|
|
3466
|
+
if (this.config.debug) {
|
|
3467
|
+
console.log("[Keverd SDK] Initialized successfully");
|
|
3468
|
+
}
|
|
3469
|
+
Keverd.startSession().catch(() => {
|
|
3470
|
+
});
|
|
21
3471
|
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Get visitor data (fingerprint and risk assessment)
|
|
3474
|
+
* Returns an RxJS Observable
|
|
3475
|
+
*/
|
|
22
3476
|
getVisitorData(options) {
|
|
23
|
-
|
|
3477
|
+
if (!this.isInitialized || !this.config) {
|
|
3478
|
+
const error = {
|
|
3479
|
+
message: "Keverd SDK not initialized. Call init() first.",
|
|
3480
|
+
code: "SDK_NOT_INITIALIZED"
|
|
3481
|
+
};
|
|
3482
|
+
return rxjs.throwError(() => error);
|
|
3483
|
+
}
|
|
3484
|
+
return rxjs.from(this.collectAndSendFingerprint(options)).pipe(
|
|
3485
|
+
operators.map((response) => this.transformResponse(response)),
|
|
3486
|
+
operators.catchError((error) => {
|
|
3487
|
+
const keverdError = {
|
|
3488
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
3489
|
+
code: "FETCH_ERROR"
|
|
3490
|
+
};
|
|
3491
|
+
if (this.config?.debug) {
|
|
3492
|
+
console.error("[Keverd SDK] Error getting visitor data:", error);
|
|
3493
|
+
}
|
|
3494
|
+
return rxjs.throwError(() => keverdError);
|
|
3495
|
+
})
|
|
3496
|
+
);
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Collect data and send fingerprint request
|
|
3500
|
+
* Uses the web SDK which includes all enhanced signals including FingerprintJS
|
|
3501
|
+
*/
|
|
3502
|
+
async collectAndSendFingerprint(_options) {
|
|
3503
|
+
if (!this.config) {
|
|
3504
|
+
throw new Error("SDK not initialized");
|
|
3505
|
+
}
|
|
3506
|
+
return await Keverd.getVisitorData();
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Transform API response to visitor data format
|
|
3510
|
+
*/
|
|
3511
|
+
transformResponse(response) {
|
|
3512
|
+
return {
|
|
3513
|
+
visitorId: response.requestId,
|
|
3514
|
+
riskScore: response.risk_score,
|
|
3515
|
+
score: response.score,
|
|
3516
|
+
action: response.action,
|
|
3517
|
+
reasons: response.reason || [],
|
|
3518
|
+
sessionId: response.session_id,
|
|
3519
|
+
requestId: response.requestId,
|
|
3520
|
+
simSwapEngine: response.sim_swap_engine,
|
|
3521
|
+
confidence: response.confidence || 1 - response.score,
|
|
3522
|
+
// Enhanced fields
|
|
3523
|
+
fingerprintjsVisitorId: response.fingerprintjs_visitor_id,
|
|
3524
|
+
privacySignals: response.privacy_signals ? {
|
|
3525
|
+
isIncognito: response.privacy_signals.is_incognito,
|
|
3526
|
+
isVPN: response.privacy_signals.is_vpn,
|
|
3527
|
+
isAutomated: response.privacy_signals.is_automated,
|
|
3528
|
+
hasAdBlocker: response.privacy_signals.has_ad_blocker
|
|
3529
|
+
} : void 0,
|
|
3530
|
+
deviceMatch: response.device_match,
|
|
3531
|
+
fraudProbability: response.fraud_probability,
|
|
3532
|
+
recommendedAction: response.recommended_action
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
/**
|
|
3536
|
+
* Generate a session ID
|
|
3537
|
+
*/
|
|
3538
|
+
generateSessionId() {
|
|
3539
|
+
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
3540
|
+
}
|
|
3541
|
+
/**
|
|
3542
|
+
* Start a new session (called automatically on init, but can be called manually)
|
|
3543
|
+
*/
|
|
3544
|
+
startSession(userId, deviceHash, metadata) {
|
|
3545
|
+
if (!this.isInitialized || !this.config) {
|
|
3546
|
+
return rxjs.throwError(() => ({
|
|
3547
|
+
message: "Keverd SDK not initialized. Call init() first.",
|
|
3548
|
+
code: "SDK_NOT_INITIALIZED"
|
|
3549
|
+
}));
|
|
3550
|
+
}
|
|
3551
|
+
return rxjs.from(Keverd.startSession(userId, deviceHash, metadata)).pipe(
|
|
3552
|
+
operators.catchError((error) => {
|
|
3553
|
+
if (this.config?.debug) {
|
|
3554
|
+
console.warn("[Keverd SDK] Failed to start session:", error);
|
|
3555
|
+
}
|
|
3556
|
+
return rxjs.throwError(() => error);
|
|
3557
|
+
})
|
|
3558
|
+
);
|
|
3559
|
+
}
|
|
3560
|
+
/**
|
|
3561
|
+
* End the current session
|
|
3562
|
+
*/
|
|
3563
|
+
endSession() {
|
|
3564
|
+
if (!this.isInitialized || !this.config) {
|
|
3565
|
+
return rxjs.throwError(() => ({
|
|
3566
|
+
message: "Keverd SDK not initialized. Call init() first.",
|
|
3567
|
+
code: "SDK_NOT_INITIALIZED"
|
|
3568
|
+
}));
|
|
3569
|
+
}
|
|
3570
|
+
return rxjs.from(Keverd.endSession()).pipe(
|
|
3571
|
+
operators.catchError((error) => {
|
|
3572
|
+
if (this.config?.debug) {
|
|
3573
|
+
console.warn("[Keverd SDK] Failed to end session:", error);
|
|
3574
|
+
}
|
|
3575
|
+
return rxjs.throwError(() => error);
|
|
3576
|
+
})
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
/**
|
|
3580
|
+
* Pause the current session (e.g., when app goes to background)
|
|
3581
|
+
*/
|
|
3582
|
+
pauseSession() {
|
|
3583
|
+
if (!this.isInitialized || !this.config) {
|
|
3584
|
+
return rxjs.throwError(() => ({
|
|
3585
|
+
message: "Keverd SDK not initialized. Call init() first.",
|
|
3586
|
+
code: "SDK_NOT_INITIALIZED"
|
|
3587
|
+
}));
|
|
3588
|
+
}
|
|
3589
|
+
return rxjs.from(Keverd.pauseSession()).pipe(
|
|
3590
|
+
operators.catchError((error) => {
|
|
3591
|
+
if (this.config?.debug) {
|
|
3592
|
+
console.warn("[Keverd SDK] Failed to pause session:", error);
|
|
3593
|
+
}
|
|
3594
|
+
return rxjs.throwError(() => error);
|
|
3595
|
+
})
|
|
3596
|
+
);
|
|
3597
|
+
}
|
|
3598
|
+
/**
|
|
3599
|
+
* Resume a paused session (e.g., when app comes to foreground)
|
|
3600
|
+
*/
|
|
3601
|
+
resumeSession() {
|
|
3602
|
+
if (!this.isInitialized || !this.config) {
|
|
3603
|
+
return rxjs.throwError(() => ({
|
|
3604
|
+
message: "Keverd SDK not initialized. Call init() first.",
|
|
3605
|
+
code: "SDK_NOT_INITIALIZED"
|
|
3606
|
+
}));
|
|
3607
|
+
}
|
|
3608
|
+
return rxjs.from(Keverd.resumeSession()).pipe(
|
|
3609
|
+
operators.catchError((error) => {
|
|
3610
|
+
if (this.config?.debug) {
|
|
3611
|
+
console.warn("[Keverd SDK] Failed to resume session:", error);
|
|
3612
|
+
}
|
|
3613
|
+
return rxjs.throwError(() => error);
|
|
3614
|
+
})
|
|
3615
|
+
);
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Get current session status
|
|
3619
|
+
*/
|
|
3620
|
+
getSessionStatus() {
|
|
3621
|
+
if (!this.isInitialized || !this.config) {
|
|
3622
|
+
return rxjs.throwError(() => ({
|
|
3623
|
+
message: "Keverd SDK not initialized. Call init() first.",
|
|
3624
|
+
code: "SDK_NOT_INITIALIZED"
|
|
3625
|
+
}));
|
|
3626
|
+
}
|
|
3627
|
+
return rxjs.from(Keverd.getSessionStatus()).pipe(
|
|
3628
|
+
operators.catchError((error) => {
|
|
3629
|
+
if (this.config?.debug) {
|
|
3630
|
+
console.warn("[Keverd SDK] Failed to get session status:", error);
|
|
3631
|
+
}
|
|
3632
|
+
return rxjs.throwError(() => error);
|
|
3633
|
+
})
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Destroy the SDK instance
|
|
3638
|
+
*/
|
|
3639
|
+
destroy() {
|
|
3640
|
+
if (this.isInitialized) {
|
|
3641
|
+
Keverd.endSession().catch(() => {
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
Keverd.destroy();
|
|
3645
|
+
this.isInitialized = false;
|
|
3646
|
+
this.config = null;
|
|
3647
|
+
}
|
|
3648
|
+
/**
|
|
3649
|
+
* Get current configuration
|
|
3650
|
+
*/
|
|
3651
|
+
getConfig() {
|
|
3652
|
+
return this.config;
|
|
3653
|
+
}
|
|
3654
|
+
/**
|
|
3655
|
+
* Check if SDK is initialized
|
|
3656
|
+
*/
|
|
3657
|
+
isReady() {
|
|
3658
|
+
return this.isInitialized;
|
|
24
3659
|
}
|
|
25
3660
|
};
|
|
26
3661
|
exports.KeverdService = __decorateClass([
|
|
27
3662
|
core.Injectable({
|
|
28
3663
|
providedIn: "root"
|
|
29
|
-
})
|
|
3664
|
+
}),
|
|
3665
|
+
__decorateParam(0, core.Optional()),
|
|
3666
|
+
__decorateParam(0, core.Inject("KEVERD_CONFIG"))
|
|
30
3667
|
], exports.KeverdService);
|
|
31
3668
|
exports.KeverdModule = class KeverdModule {
|
|
3669
|
+
/**
|
|
3670
|
+
* Configure the module with SDK settings
|
|
3671
|
+
* Call this in your app.module.ts imports array
|
|
3672
|
+
*
|
|
3673
|
+
* @example
|
|
3674
|
+
* ```typescript
|
|
3675
|
+
* @NgModule({
|
|
3676
|
+
* imports: [
|
|
3677
|
+
* KeverdModule.forRoot({
|
|
3678
|
+
* apiKey: 'your-api-key',
|
|
3679
|
+
* })
|
|
3680
|
+
* ]
|
|
3681
|
+
* })
|
|
3682
|
+
* export class AppModule {}
|
|
3683
|
+
* ```
|
|
3684
|
+
*/
|
|
32
3685
|
static forRoot(config) {
|
|
33
3686
|
return {
|
|
34
3687
|
ngModule: exports.KeverdModule,
|
|
35
3688
|
providers: [
|
|
36
|
-
|
|
37
|
-
|
|
3689
|
+
exports.KeverdService,
|
|
3690
|
+
{
|
|
3691
|
+
provide: "KEVERD_CONFIG",
|
|
3692
|
+
useValue: config
|
|
3693
|
+
}
|
|
38
3694
|
]
|
|
39
3695
|
};
|
|
40
3696
|
}
|