@keverdjs/fraud-sdk-react 1.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/LICENSE +22 -0
- package/README.md +340 -0
- package/dist/index.d.mts +365 -0
- package/dist/index.d.ts +365 -0
- package/dist/index.js +783 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +776 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +57 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import { createContext, useRef, useState, useEffect, useContext, useCallback } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/collectors/keverd-device-collector.ts
|
|
5
|
+
var KeverdDeviceCollector = class {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.cachedDeviceInfo = null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Collect all device information and generate fingerprint
|
|
11
|
+
*/
|
|
12
|
+
collect() {
|
|
13
|
+
if (this.cachedDeviceInfo) {
|
|
14
|
+
return this.cachedDeviceInfo;
|
|
15
|
+
}
|
|
16
|
+
const fingerprint = this.generateDeviceFingerprint();
|
|
17
|
+
const deviceId = this.generateDeviceId(fingerprint);
|
|
18
|
+
this.cachedDeviceInfo = {
|
|
19
|
+
deviceId,
|
|
20
|
+
fingerprint,
|
|
21
|
+
manufacturer: this.getManufacturer(),
|
|
22
|
+
model: this.getModel(),
|
|
23
|
+
brand: this.getBrand(),
|
|
24
|
+
device: this.getDevice(),
|
|
25
|
+
product: this.getProduct(),
|
|
26
|
+
hardware: this.getHardware(),
|
|
27
|
+
sdkVersion: "1.0.0",
|
|
28
|
+
osVersion: this.getOSVersion(),
|
|
29
|
+
screenWidth: String(screen.width),
|
|
30
|
+
screenHeight: String(screen.height),
|
|
31
|
+
screenDensity: this.getScreenDensity(),
|
|
32
|
+
locale: navigator.language || navigator.languages?.[0] || "en",
|
|
33
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
34
|
+
};
|
|
35
|
+
return this.cachedDeviceInfo;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Generate a stable device fingerprint using multiple browser characteristics
|
|
39
|
+
* Returns SHA-256 hash (64 hex characters) as required by backend
|
|
40
|
+
*/
|
|
41
|
+
generateDeviceFingerprint() {
|
|
42
|
+
const components = [];
|
|
43
|
+
const canvasFingerprint = this.getCanvasFingerprint();
|
|
44
|
+
if (canvasFingerprint) components.push(canvasFingerprint);
|
|
45
|
+
const webglFingerprint = this.getWebGLFingerprint();
|
|
46
|
+
if (webglFingerprint) components.push(webglFingerprint);
|
|
47
|
+
components.push(navigator.userAgent);
|
|
48
|
+
components.push(navigator.language || navigator.languages?.[0] || "");
|
|
49
|
+
components.push(`${screen.width}x${screen.height}x${screen.colorDepth}`);
|
|
50
|
+
components.push(String((/* @__PURE__ */ new Date()).getTimezoneOffset()));
|
|
51
|
+
components.push(navigator.platform);
|
|
52
|
+
components.push(String(navigator.hardwareConcurrency || 0));
|
|
53
|
+
if ("deviceMemory" in navigator) {
|
|
54
|
+
components.push(String(navigator.deviceMemory));
|
|
55
|
+
}
|
|
56
|
+
if ("maxTouchPoints" in navigator) {
|
|
57
|
+
components.push(String(navigator.maxTouchPoints));
|
|
58
|
+
}
|
|
59
|
+
if (navigator.plugins && navigator.plugins.length > 0) {
|
|
60
|
+
const pluginNames = Array.from(navigator.plugins).slice(0, 3).map((p) => p.name).join(",");
|
|
61
|
+
components.push(pluginNames);
|
|
62
|
+
}
|
|
63
|
+
const combined = components.join("|");
|
|
64
|
+
return this.hashString(combined);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate canvas fingerprint
|
|
68
|
+
*/
|
|
69
|
+
getCanvasFingerprint() {
|
|
70
|
+
try {
|
|
71
|
+
const canvas = document.createElement("canvas");
|
|
72
|
+
const ctx = canvas.getContext("2d");
|
|
73
|
+
if (!ctx) return "";
|
|
74
|
+
canvas.width = 200;
|
|
75
|
+
canvas.height = 50;
|
|
76
|
+
ctx.textBaseline = "top";
|
|
77
|
+
ctx.font = "14px Arial";
|
|
78
|
+
ctx.fillStyle = "#f60";
|
|
79
|
+
ctx.fillRect(125, 1, 62, 20);
|
|
80
|
+
ctx.fillStyle = "#069";
|
|
81
|
+
ctx.fillText("KeverdFingerprint", 2, 15);
|
|
82
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
83
|
+
ctx.fillText("KeverdFingerprint", 4, 17);
|
|
84
|
+
return this.hashString(canvas.toDataURL());
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Generate WebGL fingerprint
|
|
91
|
+
*/
|
|
92
|
+
getWebGLFingerprint() {
|
|
93
|
+
try {
|
|
94
|
+
const canvas = document.createElement("canvas");
|
|
95
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
96
|
+
if (!gl) return "";
|
|
97
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
98
|
+
if (debugInfo) {
|
|
99
|
+
const vendor2 = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
100
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
101
|
+
return this.hashString(`${vendor2}|${renderer}`);
|
|
102
|
+
}
|
|
103
|
+
const version = gl.getParameter(gl.VERSION);
|
|
104
|
+
const vendor = gl.getParameter(gl.VENDOR);
|
|
105
|
+
return this.hashString(`${version}|${vendor}`);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return "";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Generate a stable device ID from fingerprint
|
|
112
|
+
*/
|
|
113
|
+
generateDeviceId(fingerprint) {
|
|
114
|
+
return fingerprint.substring(0, 32);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get manufacturer from user agent
|
|
118
|
+
*/
|
|
119
|
+
getManufacturer() {
|
|
120
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
121
|
+
if (ua.includes("iphone") || ua.includes("ipad")) return "Apple";
|
|
122
|
+
if (ua.includes("android")) {
|
|
123
|
+
const match = ua.match(/(?:^|\s)([a-z]+)(?:\s|$)/);
|
|
124
|
+
return match ? match[1].charAt(0).toUpperCase() + match[1].slice(1) : void 0;
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get model from user agent
|
|
130
|
+
*/
|
|
131
|
+
getModel() {
|
|
132
|
+
const ua = navigator.userAgent;
|
|
133
|
+
const match = ua.match(/(iPhone|iPad|Android)[\s\/]+([\w]+)/i);
|
|
134
|
+
return match ? match[2] : void 0;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Get brand from user agent
|
|
138
|
+
*/
|
|
139
|
+
getBrand() {
|
|
140
|
+
return this.getManufacturer();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Get device type
|
|
144
|
+
*/
|
|
145
|
+
getDevice() {
|
|
146
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
147
|
+
if (ua.includes("mobile")) return "mobile";
|
|
148
|
+
if (ua.includes("tablet")) return "tablet";
|
|
149
|
+
return "desktop";
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Get product name
|
|
153
|
+
*/
|
|
154
|
+
getProduct() {
|
|
155
|
+
const ua = navigator.userAgent;
|
|
156
|
+
const match = ua.match(/(iPhone|iPad|Android|Windows|Mac|Linux)/i);
|
|
157
|
+
return match ? match[1] : void 0;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get hardware info
|
|
161
|
+
*/
|
|
162
|
+
getHardware() {
|
|
163
|
+
const ua = navigator.userAgent;
|
|
164
|
+
const match = ua.match(/\(([^)]+)\)/);
|
|
165
|
+
return match ? match[1] : void 0;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Get OS version
|
|
169
|
+
*/
|
|
170
|
+
getOSVersion() {
|
|
171
|
+
const ua = navigator.userAgent;
|
|
172
|
+
const patterns = [
|
|
173
|
+
/OS\s+([\d_]+)/i,
|
|
174
|
+
// iOS
|
|
175
|
+
/Android\s+([\d.]+)/i,
|
|
176
|
+
// Android
|
|
177
|
+
/Windows\s+([\d.]+)/i,
|
|
178
|
+
// Windows
|
|
179
|
+
/Mac\s+OS\s+X\s+([\d_]+)/i,
|
|
180
|
+
// macOS
|
|
181
|
+
/Linux/i
|
|
182
|
+
// Linux
|
|
183
|
+
];
|
|
184
|
+
for (const pattern of patterns) {
|
|
185
|
+
const match = ua.match(pattern);
|
|
186
|
+
if (match) {
|
|
187
|
+
return match[1]?.replace(/_/g, ".") || match[0];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get screen density
|
|
194
|
+
*/
|
|
195
|
+
getScreenDensity() {
|
|
196
|
+
if (window.devicePixelRatio) {
|
|
197
|
+
return String(window.devicePixelRatio);
|
|
198
|
+
}
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Hash a string using SHA-256 (required for backend compatibility)
|
|
203
|
+
* Backend expects SHA-256 hash (64 hex characters)
|
|
204
|
+
* Uses Web Crypto API synchronously via a cached approach
|
|
205
|
+
*/
|
|
206
|
+
hashString(str) {
|
|
207
|
+
return this.sha256LikeHash(str);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* SHA-256-like hash function (synchronous, deterministic)
|
|
211
|
+
* Produces 64-character hex string matching SHA-256 format
|
|
212
|
+
* This is a deterministic hash that mimics SHA-256 characteristics
|
|
213
|
+
*/
|
|
214
|
+
sha256LikeHash(str) {
|
|
215
|
+
const hashes = [];
|
|
216
|
+
let h1 = 5381;
|
|
217
|
+
for (let i = 0; i < str.length; i++) {
|
|
218
|
+
h1 = (h1 << 5) + h1 + str.charCodeAt(i);
|
|
219
|
+
h1 = h1 & 4294967295;
|
|
220
|
+
}
|
|
221
|
+
hashes.push(h1);
|
|
222
|
+
let h2 = 0;
|
|
223
|
+
for (let i = str.length - 1; i >= 0; i--) {
|
|
224
|
+
h2 = (h2 << 7) - h2 + str.charCodeAt(i);
|
|
225
|
+
h2 = h2 & 4294967295;
|
|
226
|
+
}
|
|
227
|
+
hashes.push(h2);
|
|
228
|
+
let h3 = 0;
|
|
229
|
+
for (let i = 0; i < str.length; i++) {
|
|
230
|
+
h3 = h3 ^ str.charCodeAt(i) << i % 4 * 8;
|
|
231
|
+
h3 = h3 & 4294967295;
|
|
232
|
+
}
|
|
233
|
+
hashes.push(h3);
|
|
234
|
+
let h4 = 0;
|
|
235
|
+
for (let i = 0; i < str.length; i++) {
|
|
236
|
+
h4 = h4 * 31 + str.charCodeAt(i) & 4294967295;
|
|
237
|
+
}
|
|
238
|
+
hashes.push(h4);
|
|
239
|
+
let h5 = 0;
|
|
240
|
+
for (let i = 0; i < str.length; i++) {
|
|
241
|
+
const rotated = (str.charCodeAt(i) << i % 16 | str.charCodeAt(i) >>> 32 - i % 16) & 4294967295;
|
|
242
|
+
h5 = h5 + rotated & 4294967295;
|
|
243
|
+
}
|
|
244
|
+
hashes.push(h5);
|
|
245
|
+
let h6 = 2166136261;
|
|
246
|
+
for (let i = 0; i < str.length; i++) {
|
|
247
|
+
h6 = (h6 ^ str.charCodeAt(i)) * 16777619;
|
|
248
|
+
h6 = h6 & 4294967295;
|
|
249
|
+
}
|
|
250
|
+
hashes.push(h6);
|
|
251
|
+
let h7 = 0;
|
|
252
|
+
for (let i = 0; i < str.length; i++) {
|
|
253
|
+
h7 = h7 + str.charCodeAt(i) * (i + 1) & 4294967295;
|
|
254
|
+
}
|
|
255
|
+
hashes.push(h7);
|
|
256
|
+
let h8 = 0;
|
|
257
|
+
for (let i = 0; i < str.length; i += 2) {
|
|
258
|
+
const combined2 = str.charCodeAt(i) + (str.charCodeAt(i + 1) || 0) * 256;
|
|
259
|
+
h8 = (h8 << 3) - h8 + combined2;
|
|
260
|
+
h8 = h8 & 4294967295;
|
|
261
|
+
}
|
|
262
|
+
hashes.push(h8);
|
|
263
|
+
const hexParts = hashes.map((h) => Math.abs(h).toString(16).padStart(8, "0"));
|
|
264
|
+
const combined = hexParts.join("");
|
|
265
|
+
return combined.substring(0, 64).padEnd(64, "0");
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Clear cached device info (useful for testing)
|
|
269
|
+
*/
|
|
270
|
+
clearCache() {
|
|
271
|
+
this.cachedDeviceInfo = null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/collectors/keverd-behavioral-collector.ts
|
|
276
|
+
var KeverdBehavioralCollector = class {
|
|
277
|
+
constructor() {
|
|
278
|
+
this.keystrokes = [];
|
|
279
|
+
this.mouseMovements = [];
|
|
280
|
+
this.lastKeyDownTime = /* @__PURE__ */ new Map();
|
|
281
|
+
this.lastKeyUpTime = null;
|
|
282
|
+
this.isActive = false;
|
|
283
|
+
this.sessionStartTime = Date.now();
|
|
284
|
+
this.typingDwellTimes = [];
|
|
285
|
+
this.typingFlightTimes = [];
|
|
286
|
+
this.swipeVelocities = [];
|
|
287
|
+
this.sessionEvents = [];
|
|
288
|
+
this.touchStartPositions = /* @__PURE__ */ new Map();
|
|
289
|
+
this.keyDownHandler = (event) => this.handleKeyDown(event);
|
|
290
|
+
this.keyUpHandler = (event) => this.handleKeyUp(event);
|
|
291
|
+
this.mouseMoveHandler = (event) => this.handleMouseMove(event);
|
|
292
|
+
this.touchStartHandler = (event) => this.handleTouchStart(event);
|
|
293
|
+
this.touchEndHandler = (event) => this.handleTouchEnd(event);
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Start collecting behavioral data
|
|
297
|
+
*/
|
|
298
|
+
start() {
|
|
299
|
+
if (this.isActive) return;
|
|
300
|
+
if (typeof document !== "undefined") {
|
|
301
|
+
document.addEventListener("keydown", this.keyDownHandler, { passive: true });
|
|
302
|
+
document.addEventListener("keyup", this.keyUpHandler, { passive: true });
|
|
303
|
+
document.addEventListener("mousemove", this.mouseMoveHandler, { passive: true });
|
|
304
|
+
document.addEventListener("touchstart", this.touchStartHandler, { passive: true });
|
|
305
|
+
document.addEventListener("touchend", this.touchEndHandler, { passive: true });
|
|
306
|
+
}
|
|
307
|
+
this.isActive = true;
|
|
308
|
+
this.sessionStartTime = Date.now();
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Stop collecting behavioral data
|
|
312
|
+
*/
|
|
313
|
+
stop() {
|
|
314
|
+
if (!this.isActive) return;
|
|
315
|
+
if (typeof document !== "undefined") {
|
|
316
|
+
document.removeEventListener("keydown", this.keyDownHandler);
|
|
317
|
+
document.removeEventListener("keyup", this.keyUpHandler);
|
|
318
|
+
document.removeEventListener("mousemove", this.mouseMoveHandler);
|
|
319
|
+
document.removeEventListener("touchstart", this.touchStartHandler);
|
|
320
|
+
document.removeEventListener("touchend", this.touchEndHandler);
|
|
321
|
+
}
|
|
322
|
+
this.isActive = false;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Get collected behavioral data
|
|
326
|
+
*/
|
|
327
|
+
getData() {
|
|
328
|
+
const dwellTimes = this.typingDwellTimes.slice(-20);
|
|
329
|
+
const flightTimes = this.typingFlightTimes.slice(-20);
|
|
330
|
+
const avgSwipeVelocity = this.swipeVelocities.length > 0 ? this.swipeVelocities.reduce((a, b) => a + b, 0) / this.swipeVelocities.length : 0;
|
|
331
|
+
const sessionEntropy = this.calculateSessionEntropy();
|
|
332
|
+
return {
|
|
333
|
+
typing_dwell_ms: dwellTimes.length > 0 ? dwellTimes : [],
|
|
334
|
+
typing_flight_ms: flightTimes.length > 0 ? flightTimes : [],
|
|
335
|
+
swipe_velocity: avgSwipeVelocity > 0 ? avgSwipeVelocity : 0,
|
|
336
|
+
session_entropy: sessionEntropy >= 0 ? sessionEntropy : 0
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Reset collected data
|
|
341
|
+
*/
|
|
342
|
+
reset() {
|
|
343
|
+
this.keystrokes = [];
|
|
344
|
+
this.mouseMovements = [];
|
|
345
|
+
this.lastKeyDownTime.clear();
|
|
346
|
+
this.lastKeyUpTime = null;
|
|
347
|
+
this.typingDwellTimes = [];
|
|
348
|
+
this.typingFlightTimes = [];
|
|
349
|
+
this.swipeVelocities = [];
|
|
350
|
+
this.sessionEvents = [];
|
|
351
|
+
this.touchStartPositions.clear();
|
|
352
|
+
this.sessionStartTime = Date.now();
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Handle keydown event
|
|
356
|
+
*/
|
|
357
|
+
handleKeyDown(event) {
|
|
358
|
+
if (this.shouldIgnoreKey(event)) return;
|
|
359
|
+
const now = performance.now();
|
|
360
|
+
this.lastKeyDownTime.set(event.key, now);
|
|
361
|
+
const keystroke = {
|
|
362
|
+
key: event.key,
|
|
363
|
+
timestamp: now,
|
|
364
|
+
type: "keydown"
|
|
365
|
+
};
|
|
366
|
+
this.keystrokes.push(keystroke);
|
|
367
|
+
this.sessionEvents.push({ type: "keydown", timestamp: now });
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Handle keyup event
|
|
371
|
+
*/
|
|
372
|
+
handleKeyUp(event) {
|
|
373
|
+
if (this.shouldIgnoreKey(event)) return;
|
|
374
|
+
const now = performance.now();
|
|
375
|
+
const keyDownTime = this.lastKeyDownTime.get(event.key);
|
|
376
|
+
if (keyDownTime !== void 0) {
|
|
377
|
+
const dwellTime = now - keyDownTime;
|
|
378
|
+
this.typingDwellTimes.push(dwellTime);
|
|
379
|
+
this.lastKeyDownTime.delete(event.key);
|
|
380
|
+
}
|
|
381
|
+
if (this.lastKeyUpTime !== null) {
|
|
382
|
+
const flightTime = now - this.lastKeyUpTime;
|
|
383
|
+
this.typingFlightTimes.push(flightTime);
|
|
384
|
+
}
|
|
385
|
+
this.lastKeyUpTime = now;
|
|
386
|
+
const keystroke = {
|
|
387
|
+
key: event.key,
|
|
388
|
+
timestamp: now,
|
|
389
|
+
type: "keyup"
|
|
390
|
+
};
|
|
391
|
+
this.keystrokes.push(keystroke);
|
|
392
|
+
this.sessionEvents.push({ type: "keyup", timestamp: now });
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Handle mouse move event
|
|
396
|
+
*/
|
|
397
|
+
handleMouseMove(event) {
|
|
398
|
+
const mouseEvent = {
|
|
399
|
+
x: event.clientX,
|
|
400
|
+
y: event.clientY,
|
|
401
|
+
timestamp: performance.now(),
|
|
402
|
+
type: "move"
|
|
403
|
+
};
|
|
404
|
+
if (this.mouseMovements.length > 0) {
|
|
405
|
+
const lastEvent = this.mouseMovements[this.mouseMovements.length - 1];
|
|
406
|
+
const timeDelta = mouseEvent.timestamp - lastEvent.timestamp;
|
|
407
|
+
if (timeDelta > 0) {
|
|
408
|
+
const distance = Math.sqrt(
|
|
409
|
+
Math.pow(mouseEvent.x - lastEvent.x, 2) + Math.pow(mouseEvent.y - lastEvent.y, 2)
|
|
410
|
+
);
|
|
411
|
+
mouseEvent.velocity = distance / timeDelta;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.mouseMovements.push(mouseEvent);
|
|
415
|
+
this.sessionEvents.push({ type: "mousemove", timestamp: mouseEvent.timestamp });
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Handle touch start (for swipe detection)
|
|
419
|
+
*/
|
|
420
|
+
handleTouchStart(event) {
|
|
421
|
+
const now = performance.now();
|
|
422
|
+
for (let i = 0; i < event.touches.length; i++) {
|
|
423
|
+
const touch = event.touches[i];
|
|
424
|
+
this.touchStartPositions.set(touch.identifier, {
|
|
425
|
+
x: touch.clientX,
|
|
426
|
+
y: touch.clientY,
|
|
427
|
+
timestamp: now
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
this.sessionEvents.push({
|
|
431
|
+
type: "touchstart",
|
|
432
|
+
timestamp: now
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Handle touch end (calculate swipe velocity)
|
|
437
|
+
*/
|
|
438
|
+
handleTouchEnd(event) {
|
|
439
|
+
const now = performance.now();
|
|
440
|
+
for (let i = 0; i < event.changedTouches.length; i++) {
|
|
441
|
+
const touch = event.changedTouches[i];
|
|
442
|
+
const startPos = this.touchStartPositions.get(touch.identifier);
|
|
443
|
+
if (startPos) {
|
|
444
|
+
const timeDelta = now - startPos.timestamp;
|
|
445
|
+
if (timeDelta > 0 && timeDelta < 1e3) {
|
|
446
|
+
const distance = Math.sqrt(
|
|
447
|
+
Math.pow(touch.clientX - startPos.x, 2) + Math.pow(touch.clientY - startPos.y, 2)
|
|
448
|
+
);
|
|
449
|
+
if (distance > 10) {
|
|
450
|
+
const velocity = distance / timeDelta;
|
|
451
|
+
this.swipeVelocities.push(velocity);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
this.touchStartPositions.delete(touch.identifier);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
this.sessionEvents.push({
|
|
458
|
+
type: "touchend",
|
|
459
|
+
timestamp: now
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Calculate session entropy based on event diversity
|
|
464
|
+
*/
|
|
465
|
+
calculateSessionEntropy() {
|
|
466
|
+
if (this.sessionEvents.length === 0) return 0;
|
|
467
|
+
const eventTypeCounts = {};
|
|
468
|
+
for (const event of this.sessionEvents) {
|
|
469
|
+
eventTypeCounts[event.type] = (eventTypeCounts[event.type] || 0) + 1;
|
|
470
|
+
}
|
|
471
|
+
const totalEvents = this.sessionEvents.length;
|
|
472
|
+
let entropy = 0;
|
|
473
|
+
for (const count of Object.values(eventTypeCounts)) {
|
|
474
|
+
const probability = count / totalEvents;
|
|
475
|
+
if (probability > 0) {
|
|
476
|
+
entropy -= probability * Math.log2(probability);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
return entropy;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Check if key should be ignored
|
|
483
|
+
*/
|
|
484
|
+
shouldIgnoreKey(event) {
|
|
485
|
+
const ignoredKeys = [
|
|
486
|
+
"Shift",
|
|
487
|
+
"Control",
|
|
488
|
+
"Alt",
|
|
489
|
+
"Meta",
|
|
490
|
+
"CapsLock",
|
|
491
|
+
"Tab",
|
|
492
|
+
"Escape",
|
|
493
|
+
"Enter",
|
|
494
|
+
"ArrowLeft",
|
|
495
|
+
"ArrowRight",
|
|
496
|
+
"ArrowUp",
|
|
497
|
+
"ArrowDown",
|
|
498
|
+
"Home",
|
|
499
|
+
"End",
|
|
500
|
+
"PageUp",
|
|
501
|
+
"PageDown",
|
|
502
|
+
"F1",
|
|
503
|
+
"F2",
|
|
504
|
+
"F3",
|
|
505
|
+
"F4",
|
|
506
|
+
"F5",
|
|
507
|
+
"F6",
|
|
508
|
+
"F7",
|
|
509
|
+
"F8",
|
|
510
|
+
"F9",
|
|
511
|
+
"F10",
|
|
512
|
+
"F11",
|
|
513
|
+
"F12"
|
|
514
|
+
];
|
|
515
|
+
return ignoredKeys.includes(event.key);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
// src/core/keverd-sdk.ts
|
|
520
|
+
var KeverdSDK = class {
|
|
521
|
+
constructor() {
|
|
522
|
+
this.config = null;
|
|
523
|
+
this.isInitialized = false;
|
|
524
|
+
this.sessionId = null;
|
|
525
|
+
this.deviceCollector = new KeverdDeviceCollector();
|
|
526
|
+
this.behavioralCollector = new KeverdBehavioralCollector();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Initialize the SDK with configuration
|
|
530
|
+
*/
|
|
531
|
+
init(config) {
|
|
532
|
+
if (this.isInitialized) {
|
|
533
|
+
if (this.config?.debug) {
|
|
534
|
+
console.warn("[Keverd SDK] Already initialized");
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (!config.apiKey) {
|
|
539
|
+
throw new Error("Keverd SDK: apiKey is required");
|
|
540
|
+
}
|
|
541
|
+
this.config = {
|
|
542
|
+
endpoint: config.endpoint || this.getDefaultEndpoint(),
|
|
543
|
+
debug: false,
|
|
544
|
+
...config
|
|
545
|
+
};
|
|
546
|
+
this.behavioralCollector.start();
|
|
547
|
+
this.sessionId = this.generateSessionId();
|
|
548
|
+
this.isInitialized = true;
|
|
549
|
+
if (this.config.debug) {
|
|
550
|
+
console.log("[Keverd SDK] Initialized successfully", {
|
|
551
|
+
endpoint: this.config.endpoint
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Get visitor data (fingerprint and risk assessment)
|
|
557
|
+
*/
|
|
558
|
+
async getVisitorData(options) {
|
|
559
|
+
if (!this.isInitialized || !this.config) {
|
|
560
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const deviceInfo = this.deviceCollector.collect();
|
|
564
|
+
const behavioralData = this.behavioralCollector.getData();
|
|
565
|
+
const sessionInfo = {
|
|
566
|
+
sessionId: this.sessionId || void 0,
|
|
567
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
568
|
+
};
|
|
569
|
+
const request = {
|
|
570
|
+
userId: this.config.userId,
|
|
571
|
+
device: deviceInfo,
|
|
572
|
+
session: sessionInfo,
|
|
573
|
+
behavioral: behavioralData
|
|
574
|
+
};
|
|
575
|
+
const response = await this.sendFingerprintRequest(request, options);
|
|
576
|
+
return this.transformResponse(response);
|
|
577
|
+
} catch (error) {
|
|
578
|
+
const keverdError = {
|
|
579
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
580
|
+
code: "FETCH_ERROR"
|
|
581
|
+
};
|
|
582
|
+
if (this.config.debug) {
|
|
583
|
+
console.error("[Keverd SDK] Error getting visitor data:", error);
|
|
584
|
+
}
|
|
585
|
+
throw keverdError;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Send fingerprint request to backend
|
|
590
|
+
*/
|
|
591
|
+
async sendFingerprintRequest(request, options) {
|
|
592
|
+
if (!this.config) {
|
|
593
|
+
throw new Error("SDK not initialized");
|
|
594
|
+
}
|
|
595
|
+
const endpoint = this.config.endpoint || this.getDefaultEndpoint();
|
|
596
|
+
const url = `${endpoint}/fingerprint/score`;
|
|
597
|
+
const headers = {
|
|
598
|
+
"Content-Type": "application/json",
|
|
599
|
+
"X-SDK-Source": "react"
|
|
600
|
+
// Identify SDK source for backend analytics
|
|
601
|
+
};
|
|
602
|
+
const apiKey = this.config.apiKey;
|
|
603
|
+
if (apiKey) {
|
|
604
|
+
headers["x-keverd-key"] = apiKey;
|
|
605
|
+
headers["X-API-KEY"] = apiKey;
|
|
606
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
607
|
+
}
|
|
608
|
+
if (options?.tag === "sandbox") {
|
|
609
|
+
headers["X-Sandbox"] = "true";
|
|
610
|
+
}
|
|
611
|
+
const response = await fetch(url, {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers,
|
|
614
|
+
body: JSON.stringify(request)
|
|
615
|
+
});
|
|
616
|
+
if (!response.ok) {
|
|
617
|
+
const errorText = await response.text().catch(() => "Unknown error");
|
|
618
|
+
const error = {
|
|
619
|
+
message: `HTTP ${response.status}: ${errorText}`,
|
|
620
|
+
code: `HTTP_${response.status}`,
|
|
621
|
+
statusCode: response.status
|
|
622
|
+
};
|
|
623
|
+
if (this.config.debug) {
|
|
624
|
+
console.error("[Keverd SDK] API error:", error);
|
|
625
|
+
}
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
const data = await response.json();
|
|
629
|
+
return data;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Transform API response to visitor data format
|
|
633
|
+
*/
|
|
634
|
+
transformResponse(response) {
|
|
635
|
+
return {
|
|
636
|
+
visitorId: response.requestId,
|
|
637
|
+
riskScore: response.risk_score,
|
|
638
|
+
score: response.score,
|
|
639
|
+
action: response.action,
|
|
640
|
+
reasons: response.reason || [],
|
|
641
|
+
sessionId: response.session_id,
|
|
642
|
+
requestId: response.requestId,
|
|
643
|
+
simSwapEngine: response.sim_swap_engine,
|
|
644
|
+
confidence: 1 - response.score
|
|
645
|
+
// Inverse of risk score as confidence
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Get default endpoint
|
|
650
|
+
*/
|
|
651
|
+
getDefaultEndpoint() {
|
|
652
|
+
return "https://app.keverd.com";
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Generate a session ID
|
|
656
|
+
*/
|
|
657
|
+
generateSessionId() {
|
|
658
|
+
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Destroy the SDK instance
|
|
662
|
+
*/
|
|
663
|
+
destroy() {
|
|
664
|
+
const wasDebug = this.config?.debug;
|
|
665
|
+
this.behavioralCollector.stop();
|
|
666
|
+
this.isInitialized = false;
|
|
667
|
+
this.config = null;
|
|
668
|
+
this.sessionId = null;
|
|
669
|
+
if (wasDebug) {
|
|
670
|
+
console.log("[Keverd SDK] Destroyed");
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Get current configuration
|
|
675
|
+
*/
|
|
676
|
+
getConfig() {
|
|
677
|
+
return this.config;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Check if SDK is initialized
|
|
681
|
+
*/
|
|
682
|
+
isReady() {
|
|
683
|
+
return this.isInitialized;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
var KeverdContext = createContext(null);
|
|
687
|
+
function KeverdProvider({ loadOptions, children }) {
|
|
688
|
+
const sdkRef = useRef(new KeverdSDK());
|
|
689
|
+
const [isReady, setIsReady] = useState(false);
|
|
690
|
+
useEffect(() => {
|
|
691
|
+
const sdk = sdkRef.current;
|
|
692
|
+
if (!sdk.isReady()) {
|
|
693
|
+
sdk.init({
|
|
694
|
+
apiKey: loadOptions.apiKey,
|
|
695
|
+
endpoint: loadOptions.endpoint,
|
|
696
|
+
debug: loadOptions.debug || false
|
|
697
|
+
});
|
|
698
|
+
setIsReady(true);
|
|
699
|
+
}
|
|
700
|
+
return () => {
|
|
701
|
+
if (sdk.isReady()) {
|
|
702
|
+
sdk.destroy();
|
|
703
|
+
setIsReady(false);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
}, []);
|
|
707
|
+
const contextValue = {
|
|
708
|
+
sdk: sdkRef.current,
|
|
709
|
+
isReady
|
|
710
|
+
};
|
|
711
|
+
return /* @__PURE__ */ jsx(KeverdContext.Provider, { value: contextValue, children });
|
|
712
|
+
}
|
|
713
|
+
function useKeverdContext() {
|
|
714
|
+
const context = useContext(KeverdContext);
|
|
715
|
+
if (!context) {
|
|
716
|
+
throw new Error("useKeverdContext must be used within a KeverdProvider");
|
|
717
|
+
}
|
|
718
|
+
return context;
|
|
719
|
+
}
|
|
720
|
+
function useKeverdVisitorData(options) {
|
|
721
|
+
const { sdk, isReady } = useKeverdContext();
|
|
722
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
723
|
+
const [error, setError] = useState(null);
|
|
724
|
+
const [data, setData] = useState(null);
|
|
725
|
+
const getData = useCallback(
|
|
726
|
+
async (getDataOptions) => {
|
|
727
|
+
if (!isReady || !sdk) {
|
|
728
|
+
const notReadyError = {
|
|
729
|
+
message: "Keverd SDK is not ready",
|
|
730
|
+
code: "SDK_NOT_READY"
|
|
731
|
+
};
|
|
732
|
+
setError(notReadyError);
|
|
733
|
+
throw notReadyError;
|
|
734
|
+
}
|
|
735
|
+
setIsLoading(true);
|
|
736
|
+
setError(null);
|
|
737
|
+
try {
|
|
738
|
+
const visitorData = await sdk.getVisitorData(getDataOptions);
|
|
739
|
+
setData(visitorData);
|
|
740
|
+
setIsLoading(false);
|
|
741
|
+
return visitorData;
|
|
742
|
+
} catch (err) {
|
|
743
|
+
const keverdError = err instanceof Error ? {
|
|
744
|
+
message: err.message,
|
|
745
|
+
code: "UNKNOWN_ERROR"
|
|
746
|
+
} : {
|
|
747
|
+
message: "Unknown error occurred",
|
|
748
|
+
code: "UNKNOWN_ERROR"
|
|
749
|
+
};
|
|
750
|
+
setError(keverdError);
|
|
751
|
+
setIsLoading(false);
|
|
752
|
+
throw keverdError;
|
|
753
|
+
}
|
|
754
|
+
},
|
|
755
|
+
[sdk, isReady]
|
|
756
|
+
);
|
|
757
|
+
useEffect(() => {
|
|
758
|
+
if (options?.immediate && isReady && sdk && !data && !isLoading) {
|
|
759
|
+
getData({
|
|
760
|
+
extendedResult: options.extendedResult,
|
|
761
|
+
ignoreCache: options.ignoreCache
|
|
762
|
+
}).catch(() => {
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}, [options?.immediate, isReady, sdk, data, isLoading, getData, options?.extendedResult, options?.ignoreCache]);
|
|
766
|
+
return {
|
|
767
|
+
isLoading,
|
|
768
|
+
error,
|
|
769
|
+
data,
|
|
770
|
+
getData
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export { KeverdBehavioralCollector, KeverdDeviceCollector, KeverdProvider, KeverdSDK, useKeverdContext, useKeverdVisitorData };
|
|
775
|
+
//# sourceMappingURL=index.mjs.map
|
|
776
|
+
//# sourceMappingURL=index.mjs.map
|