@keverdjs/fraud-sdk-react 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/README.md +0 -16
- package/dist/index.d.mts +49 -62
- package/dist/index.d.ts +49 -62
- package/dist/index.js +536 -221
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +536 -221
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -2
package/dist/index.mjs
CHANGED
|
@@ -272,247 +272,363 @@ var KeverdDeviceCollector = class {
|
|
|
272
272
|
}
|
|
273
273
|
};
|
|
274
274
|
|
|
275
|
-
// src/collectors/
|
|
276
|
-
var
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
this.
|
|
275
|
+
// src/collectors/kinematic-engine.ts
|
|
276
|
+
var VELOCITY_SPIKE_THRESHOLD = 2.5;
|
|
277
|
+
var WINDOW_LIMIT = 200;
|
|
278
|
+
var KinematicEngine = class {
|
|
279
|
+
constructor(options = {}) {
|
|
280
|
+
this.featureVectors = [];
|
|
281
|
+
this.pointerQueue = [];
|
|
282
|
+
this.lastVelocity = 0;
|
|
283
|
+
this.lastAcceleration = 0;
|
|
284
|
+
this.lastAngle = 0;
|
|
285
|
+
this.lastPoint = null;
|
|
286
|
+
this.secondLastPoint = null;
|
|
287
|
+
this.rafId = null;
|
|
288
|
+
this.maxSwipeVelocity = 0;
|
|
289
|
+
this.isActive = false;
|
|
290
|
+
this.mouseMoveHandler = (event) => this.enqueuePoint(event, "move");
|
|
291
|
+
this.mouseDownHandler = (event) => this.enqueuePoint(event, "down");
|
|
292
|
+
this.mouseUpHandler = (event) => this.enqueuePoint(event, "up");
|
|
293
|
+
this.onEvent = options.onEvent;
|
|
294
|
+
this.maskValue = options.maskValue ?? -1;
|
|
295
|
+
}
|
|
296
|
+
start() {
|
|
297
|
+
if (this.isActive || typeof document === "undefined") return;
|
|
298
|
+
document.addEventListener("mousemove", this.mouseMoveHandler, { passive: true });
|
|
299
|
+
document.addEventListener("mousedown", this.mouseDownHandler, { passive: true });
|
|
300
|
+
document.addEventListener("mouseup", this.mouseUpHandler, { passive: true });
|
|
301
|
+
this.isActive = true;
|
|
302
|
+
}
|
|
303
|
+
stop() {
|
|
304
|
+
if (!this.isActive || typeof document === "undefined") return;
|
|
305
|
+
document.removeEventListener("mousemove", this.mouseMoveHandler);
|
|
306
|
+
document.removeEventListener("mousedown", this.mouseDownHandler);
|
|
307
|
+
document.removeEventListener("mouseup", this.mouseUpHandler);
|
|
308
|
+
if (this.rafId !== null) {
|
|
309
|
+
cancelAnimationFrame(this.rafId);
|
|
310
|
+
this.rafId = null;
|
|
311
|
+
}
|
|
312
|
+
this.isActive = false;
|
|
313
|
+
}
|
|
314
|
+
getVectors() {
|
|
315
|
+
return this.featureVectors;
|
|
316
|
+
}
|
|
317
|
+
getSuspiciousSwipeVelocity() {
|
|
318
|
+
return this.maxSwipeVelocity;
|
|
319
|
+
}
|
|
320
|
+
reset() {
|
|
321
|
+
this.featureVectors = [];
|
|
322
|
+
this.pointerQueue = [];
|
|
323
|
+
this.lastVelocity = 0;
|
|
324
|
+
this.lastAcceleration = 0;
|
|
325
|
+
this.lastAngle = 0;
|
|
326
|
+
this.lastPoint = null;
|
|
327
|
+
this.secondLastPoint = null;
|
|
328
|
+
this.maxSwipeVelocity = 0;
|
|
329
|
+
}
|
|
330
|
+
enqueuePoint(event, type) {
|
|
331
|
+
const point = {
|
|
332
|
+
x: event.clientX,
|
|
333
|
+
y: event.clientY,
|
|
334
|
+
timestamp: performance.now(),
|
|
335
|
+
type
|
|
336
|
+
};
|
|
337
|
+
this.pointerQueue.push(point);
|
|
338
|
+
if (this.pointerQueue.length > WINDOW_LIMIT) {
|
|
339
|
+
this.pointerQueue.shift();
|
|
340
|
+
}
|
|
341
|
+
if (this.onEvent) {
|
|
342
|
+
this.onEvent(type === "move" ? "mousemove" : type === "down" ? "mousedown" : "mouseup");
|
|
343
|
+
}
|
|
344
|
+
this.scheduleProcess();
|
|
345
|
+
}
|
|
346
|
+
scheduleProcess() {
|
|
347
|
+
if (this.rafId !== null) return;
|
|
348
|
+
this.rafId = requestAnimationFrame(() => {
|
|
349
|
+
this.rafId = null;
|
|
350
|
+
this.processQueue();
|
|
351
|
+
if (this.pointerQueue.length > 0) {
|
|
352
|
+
this.scheduleProcess();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
processQueue() {
|
|
357
|
+
while (this.pointerQueue.length > 0) {
|
|
358
|
+
const point = this.pointerQueue.shift();
|
|
359
|
+
this.computeFeatures(point);
|
|
360
|
+
this.secondLastPoint = this.lastPoint;
|
|
361
|
+
this.lastPoint = point;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
computeFeatures(point) {
|
|
365
|
+
if (!this.lastPoint) {
|
|
366
|
+
this.lastPoint = point;
|
|
367
|
+
this.lastAngle = 0;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const dt = point.timestamp - this.lastPoint.timestamp;
|
|
371
|
+
if (dt <= 0) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const dx = point.x - this.lastPoint.x;
|
|
375
|
+
const dy = point.y - this.lastPoint.y;
|
|
376
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
377
|
+
const velocity = distance / dt;
|
|
378
|
+
const acceleration = (velocity - this.lastVelocity) / dt;
|
|
379
|
+
const jerk = (acceleration - this.lastAcceleration) / dt;
|
|
380
|
+
const angle = Math.atan2(dy, dx);
|
|
381
|
+
const angularVelocity = this.normalizeAngle(angle - this.lastAngle) / dt;
|
|
382
|
+
const curvature = this.calculateCurvature(point);
|
|
383
|
+
this.featureVectors.push({
|
|
384
|
+
timestamp: point.timestamp,
|
|
385
|
+
velocity,
|
|
386
|
+
acceleration,
|
|
387
|
+
jerk,
|
|
388
|
+
curvature,
|
|
389
|
+
angularVelocity
|
|
390
|
+
});
|
|
391
|
+
this.maxSwipeVelocity = Math.max(this.maxSwipeVelocity, velocity);
|
|
392
|
+
if (velocity > VELOCITY_SPIKE_THRESHOLD) {
|
|
393
|
+
this.featureVectors.push({
|
|
394
|
+
timestamp: point.timestamp,
|
|
395
|
+
velocity,
|
|
396
|
+
acceleration,
|
|
397
|
+
jerk,
|
|
398
|
+
curvature,
|
|
399
|
+
angularVelocity: angularVelocity * 1.2
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
if (this.featureVectors.length > WINDOW_LIMIT) {
|
|
403
|
+
this.featureVectors.splice(0, this.featureVectors.length - WINDOW_LIMIT);
|
|
404
|
+
}
|
|
405
|
+
this.lastVelocity = velocity;
|
|
406
|
+
this.lastAcceleration = acceleration;
|
|
407
|
+
this.lastAngle = angle;
|
|
408
|
+
}
|
|
409
|
+
calculateCurvature(point) {
|
|
410
|
+
if (!this.lastPoint || !this.secondLastPoint) return 0;
|
|
411
|
+
const p0 = this.secondLastPoint;
|
|
412
|
+
const p1 = this.lastPoint;
|
|
413
|
+
const p2 = point;
|
|
414
|
+
const chordLength = Math.hypot(p2.x - p0.x, p2.y - p0.y);
|
|
415
|
+
const pathLength = Math.hypot(p1.x - p0.x, p1.y - p0.y) + Math.hypot(p2.x - p1.x, p2.y - p1.y);
|
|
416
|
+
if (chordLength === 0) return 0;
|
|
417
|
+
return (pathLength - chordLength) / chordLength;
|
|
418
|
+
}
|
|
419
|
+
normalizeAngle(angleDiff) {
|
|
420
|
+
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
|
|
421
|
+
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
|
|
422
|
+
return angleDiff;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
// src/collectors/keystroke-monitor.ts
|
|
427
|
+
var MAX_BUFFER = 200;
|
|
428
|
+
var KeystrokeMonitor = class {
|
|
429
|
+
constructor(options = {}) {
|
|
430
|
+
this.keyDownTimes = /* @__PURE__ */ new Map();
|
|
281
431
|
this.lastKeyUpTime = null;
|
|
432
|
+
this.lastKeyDownTime = null;
|
|
433
|
+
this.featureVectors = [];
|
|
434
|
+
this.dwellTimes = [];
|
|
435
|
+
this.flightTimes = [];
|
|
436
|
+
this.digraphLatencies = [];
|
|
282
437
|
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
438
|
this.keyDownHandler = (event) => this.handleKeyDown(event);
|
|
290
439
|
this.keyUpHandler = (event) => this.handleKeyUp(event);
|
|
291
|
-
this.
|
|
292
|
-
this.touchStartHandler = (event) => this.handleTouchStart(event);
|
|
293
|
-
this.touchEndHandler = (event) => this.handleTouchEnd(event);
|
|
440
|
+
this.onEvent = options.onEvent;
|
|
294
441
|
}
|
|
295
|
-
/**
|
|
296
|
-
* Start collecting behavioral data
|
|
297
|
-
*/
|
|
298
442
|
start() {
|
|
299
|
-
if (this.isActive) return;
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
}
|
|
443
|
+
if (this.isActive || typeof document === "undefined") return;
|
|
444
|
+
document.addEventListener("keydown", this.keyDownHandler, { passive: true, capture: true });
|
|
445
|
+
document.addEventListener("keyup", this.keyUpHandler, { passive: true, capture: true });
|
|
307
446
|
this.isActive = true;
|
|
308
|
-
this.sessionStartTime = Date.now();
|
|
309
447
|
}
|
|
310
|
-
/**
|
|
311
|
-
* Stop collecting behavioral data
|
|
312
|
-
*/
|
|
313
448
|
stop() {
|
|
314
|
-
if (!this.isActive) return;
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
}
|
|
449
|
+
if (!this.isActive || typeof document === "undefined") return;
|
|
450
|
+
document.removeEventListener("keydown", this.keyDownHandler, true);
|
|
451
|
+
document.removeEventListener("keyup", this.keyUpHandler, true);
|
|
322
452
|
this.isActive = false;
|
|
323
453
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
};
|
|
454
|
+
getVectors() {
|
|
455
|
+
return this.featureVectors;
|
|
456
|
+
}
|
|
457
|
+
getDwellTimes(limit = 50) {
|
|
458
|
+
return this.dwellTimes.slice(-limit);
|
|
459
|
+
}
|
|
460
|
+
getFlightTimes(limit = 50) {
|
|
461
|
+
return this.flightTimes.slice(-limit);
|
|
338
462
|
}
|
|
339
|
-
/**
|
|
340
|
-
* Reset collected data
|
|
341
|
-
*/
|
|
342
463
|
reset() {
|
|
343
|
-
this.
|
|
344
|
-
this.mouseMovements = [];
|
|
345
|
-
this.lastKeyDownTime.clear();
|
|
464
|
+
this.keyDownTimes.clear();
|
|
346
465
|
this.lastKeyUpTime = null;
|
|
347
|
-
this.
|
|
348
|
-
this.
|
|
349
|
-
this.
|
|
350
|
-
this.
|
|
351
|
-
this.
|
|
352
|
-
this.sessionStartTime = Date.now();
|
|
466
|
+
this.lastKeyDownTime = null;
|
|
467
|
+
this.featureVectors = [];
|
|
468
|
+
this.dwellTimes = [];
|
|
469
|
+
this.flightTimes = [];
|
|
470
|
+
this.digraphLatencies = [];
|
|
353
471
|
}
|
|
354
|
-
/**
|
|
355
|
-
* Handle keydown event
|
|
356
|
-
*/
|
|
357
472
|
handleKeyDown(event) {
|
|
358
|
-
if (this.
|
|
473
|
+
if (!this.isTargetField(event)) return;
|
|
359
474
|
const now = performance.now();
|
|
360
|
-
this.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
timestamp: now
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
475
|
+
if (this.lastKeyUpTime !== null) {
|
|
476
|
+
const flight = now - this.lastKeyUpTime;
|
|
477
|
+
this.flightTimes.push(flight);
|
|
478
|
+
this.appendVector({ dwellTime: -1, flightTime: flight, digraphLatency: -1, timestamp: now });
|
|
479
|
+
}
|
|
480
|
+
if (this.lastKeyDownTime !== null) {
|
|
481
|
+
const digraphLatency = now - this.lastKeyDownTime;
|
|
482
|
+
this.digraphLatencies.push(digraphLatency);
|
|
483
|
+
this.appendVector({ dwellTime: -1, flightTime: -1, digraphLatency, timestamp: now });
|
|
484
|
+
}
|
|
485
|
+
this.lastKeyDownTime = now;
|
|
486
|
+
this.keyDownTimes.set(event.code, now);
|
|
487
|
+
if (this.onEvent) this.onEvent("keydown");
|
|
368
488
|
}
|
|
369
|
-
/**
|
|
370
|
-
* Handle keyup event
|
|
371
|
-
*/
|
|
372
489
|
handleKeyUp(event) {
|
|
373
|
-
if (this.
|
|
490
|
+
if (!this.isTargetField(event)) return;
|
|
374
491
|
const now = performance.now();
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
377
|
-
const
|
|
378
|
-
this.
|
|
379
|
-
this.
|
|
380
|
-
|
|
381
|
-
if (this.lastKeyUpTime !== null) {
|
|
382
|
-
const flightTime = now - this.lastKeyUpTime;
|
|
383
|
-
this.typingFlightTimes.push(flightTime);
|
|
492
|
+
const start = this.keyDownTimes.get(event.code);
|
|
493
|
+
if (start !== void 0) {
|
|
494
|
+
const dwell = now - start;
|
|
495
|
+
this.dwellTimes.push(dwell);
|
|
496
|
+
this.appendVector({ dwellTime: dwell, flightTime: -1, digraphLatency: -1, timestamp: now });
|
|
497
|
+
this.keyDownTimes.delete(event.code);
|
|
384
498
|
}
|
|
385
499
|
this.lastKeyUpTime = now;
|
|
386
|
-
|
|
387
|
-
key: event.key,
|
|
388
|
-
timestamp: now,
|
|
389
|
-
type: "keyup"
|
|
390
|
-
};
|
|
391
|
-
this.keystrokes.push(keystroke);
|
|
392
|
-
this.sessionEvents.push({ type: "keyup", timestamp: now });
|
|
500
|
+
if (this.onEvent) this.onEvent("keyup");
|
|
393
501
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
}
|
|
502
|
+
appendVector(vector) {
|
|
503
|
+
this.featureVectors.push(vector);
|
|
504
|
+
if (this.featureVectors.length > MAX_BUFFER) {
|
|
505
|
+
this.featureVectors.splice(0, this.featureVectors.length - MAX_BUFFER);
|
|
413
506
|
}
|
|
414
|
-
this.mouseMovements.push(mouseEvent);
|
|
415
|
-
this.sessionEvents.push({ type: "mousemove", timestamp: mouseEvent.timestamp });
|
|
416
507
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
});
|
|
508
|
+
isTargetField(event) {
|
|
509
|
+
const target = event.target;
|
|
510
|
+
if (!target) return false;
|
|
511
|
+
const isEditable = target.isContentEditable || ["INPUT", "TEXTAREA"].includes(target.tagName);
|
|
512
|
+
return isEditable;
|
|
434
513
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/collectors/keverd-behavioral-collector.ts
|
|
517
|
+
var WINDOW_SIZE = 50;
|
|
518
|
+
var MASK_VALUE = -1;
|
|
519
|
+
var FEATURE_NAMES = [
|
|
520
|
+
"velocity",
|
|
521
|
+
"acceleration",
|
|
522
|
+
"jerk",
|
|
523
|
+
"curvature",
|
|
524
|
+
"angularVelocity",
|
|
525
|
+
"dwell",
|
|
526
|
+
"flight",
|
|
527
|
+
"digraphLatency"
|
|
528
|
+
];
|
|
529
|
+
var KeverdBehavioralCollector = class {
|
|
530
|
+
constructor() {
|
|
531
|
+
this.isActive = false;
|
|
532
|
+
this.sessionEvents = [];
|
|
533
|
+
this.trackEvent = (event) => {
|
|
534
|
+
this.sessionEvents.push({ type: event, timestamp: performance.now() });
|
|
535
|
+
if (this.sessionEvents.length > 500) {
|
|
536
|
+
this.sessionEvents.shift();
|
|
455
537
|
}
|
|
538
|
+
};
|
|
539
|
+
this.kinematicEngine = new KinematicEngine({ onEvent: this.trackEvent });
|
|
540
|
+
this.keystrokeMonitor = new KeystrokeMonitor({ onEvent: this.trackEvent });
|
|
541
|
+
}
|
|
542
|
+
start() {
|
|
543
|
+
if (this.isActive) return;
|
|
544
|
+
this.kinematicEngine.start();
|
|
545
|
+
this.keystrokeMonitor.start();
|
|
546
|
+
this.isActive = true;
|
|
547
|
+
}
|
|
548
|
+
stop() {
|
|
549
|
+
if (!this.isActive) return;
|
|
550
|
+
this.kinematicEngine.stop();
|
|
551
|
+
this.keystrokeMonitor.stop();
|
|
552
|
+
this.isActive = false;
|
|
553
|
+
}
|
|
554
|
+
reset() {
|
|
555
|
+
this.sessionEvents = [];
|
|
556
|
+
this.kinematicEngine.reset();
|
|
557
|
+
this.keystrokeMonitor.reset();
|
|
558
|
+
}
|
|
559
|
+
getData() {
|
|
560
|
+
const kinematicVectors = this.kinematicEngine.getVectors();
|
|
561
|
+
const keystrokeVectors = this.keystrokeMonitor.getVectors();
|
|
562
|
+
const merged = [
|
|
563
|
+
...kinematicVectors.map((v) => ({
|
|
564
|
+
timestamp: v.timestamp,
|
|
565
|
+
values: [
|
|
566
|
+
v.velocity,
|
|
567
|
+
v.acceleration,
|
|
568
|
+
v.jerk,
|
|
569
|
+
v.curvature,
|
|
570
|
+
v.angularVelocity,
|
|
571
|
+
MASK_VALUE,
|
|
572
|
+
MASK_VALUE,
|
|
573
|
+
MASK_VALUE
|
|
574
|
+
]
|
|
575
|
+
})),
|
|
576
|
+
...keystrokeVectors.map((v) => ({
|
|
577
|
+
timestamp: v.timestamp,
|
|
578
|
+
values: [
|
|
579
|
+
MASK_VALUE,
|
|
580
|
+
MASK_VALUE,
|
|
581
|
+
MASK_VALUE,
|
|
582
|
+
MASK_VALUE,
|
|
583
|
+
MASK_VALUE,
|
|
584
|
+
v.dwellTime,
|
|
585
|
+
v.flightTime,
|
|
586
|
+
v.digraphLatency
|
|
587
|
+
]
|
|
588
|
+
}))
|
|
589
|
+
].sort((a, b) => a.timestamp - b.timestamp);
|
|
590
|
+
const sequence = this.buildSequence(merged);
|
|
591
|
+
const suspiciousSwipeVelocity = this.kinematicEngine.getSuspiciousSwipeVelocity();
|
|
592
|
+
const entropy = this.calculateSessionEntropy();
|
|
593
|
+
const dwellTimes = this.keystrokeMonitor.getDwellTimes();
|
|
594
|
+
const flightTimes = this.keystrokeMonitor.getFlightTimes();
|
|
595
|
+
return {
|
|
596
|
+
typing_dwell_ms: dwellTimes,
|
|
597
|
+
typing_flight_ms: flightTimes,
|
|
598
|
+
swipe_velocity: suspiciousSwipeVelocity,
|
|
599
|
+
suspicious_swipe_velocity: suspiciousSwipeVelocity,
|
|
600
|
+
session_entropy: entropy,
|
|
601
|
+
behavioral_vectors: sequence
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
buildSequence(vectors) {
|
|
605
|
+
const padded = [];
|
|
606
|
+
const startIndex = Math.max(0, vectors.length - WINDOW_SIZE);
|
|
607
|
+
const window2 = vectors.slice(startIndex);
|
|
608
|
+
for (const vector of window2) {
|
|
609
|
+
padded.push(vector.values);
|
|
456
610
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
611
|
+
while (padded.length < WINDOW_SIZE) {
|
|
612
|
+
padded.unshift(new Array(FEATURE_NAMES.length).fill(MASK_VALUE));
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
featureNames: [...FEATURE_NAMES],
|
|
616
|
+
windowSize: WINDOW_SIZE,
|
|
617
|
+
maskValue: MASK_VALUE,
|
|
618
|
+
sequence: padded
|
|
619
|
+
};
|
|
461
620
|
}
|
|
462
|
-
/**
|
|
463
|
-
* Calculate session entropy based on event diversity
|
|
464
|
-
*/
|
|
465
621
|
calculateSessionEntropy() {
|
|
466
622
|
if (this.sessionEvents.length === 0) return 0;
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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);
|
|
623
|
+
const counts = {};
|
|
624
|
+
this.sessionEvents.forEach((evt) => {
|
|
625
|
+
counts[evt.type] = (counts[evt.type] || 0) + 1;
|
|
626
|
+
});
|
|
627
|
+
const total = this.sessionEvents.length;
|
|
628
|
+
return Object.values(counts).reduce((entropy, count) => {
|
|
629
|
+
const probability = count / total;
|
|
630
|
+
return probability > 0 ? entropy - probability * Math.log2(probability) : entropy;
|
|
631
|
+
}, 0);
|
|
516
632
|
}
|
|
517
633
|
};
|
|
518
634
|
|
|
@@ -539,7 +655,6 @@ var KeverdSDK = class {
|
|
|
539
655
|
throw new Error("Keverd SDK: apiKey is required");
|
|
540
656
|
}
|
|
541
657
|
this.config = {
|
|
542
|
-
endpoint: config.endpoint || this.getDefaultEndpoint(),
|
|
543
658
|
debug: false,
|
|
544
659
|
...config
|
|
545
660
|
};
|
|
@@ -547,10 +662,10 @@ var KeverdSDK = class {
|
|
|
547
662
|
this.sessionId = this.generateSessionId();
|
|
548
663
|
this.isInitialized = true;
|
|
549
664
|
if (this.config.debug) {
|
|
550
|
-
console.log("[Keverd SDK] Initialized successfully"
|
|
551
|
-
endpoint: this.config.endpoint
|
|
552
|
-
});
|
|
665
|
+
console.log("[Keverd SDK] Initialized successfully");
|
|
553
666
|
}
|
|
667
|
+
this.startSession().catch(() => {
|
|
668
|
+
});
|
|
554
669
|
}
|
|
555
670
|
/**
|
|
556
671
|
* Get visitor data (fingerprint and risk assessment)
|
|
@@ -592,8 +707,7 @@ var KeverdSDK = class {
|
|
|
592
707
|
if (!this.config) {
|
|
593
708
|
throw new Error("SDK not initialized");
|
|
594
709
|
}
|
|
595
|
-
const
|
|
596
|
-
const url = `${endpoint}/fingerprint/score`;
|
|
710
|
+
const url = `${this.getDefaultEndpoint()}/fingerprint/score`;
|
|
597
711
|
const headers = {
|
|
598
712
|
"Content-Type": "application/json",
|
|
599
713
|
"X-SDK-Source": "react"
|
|
@@ -608,10 +722,17 @@ var KeverdSDK = class {
|
|
|
608
722
|
if (options?.tag === "sandbox") {
|
|
609
723
|
headers["X-Sandbox"] = "true";
|
|
610
724
|
}
|
|
725
|
+
const requestBody = {
|
|
726
|
+
...request,
|
|
727
|
+
session: {
|
|
728
|
+
...request.session || {},
|
|
729
|
+
sessionId: this.sessionId || request.session?.sessionId
|
|
730
|
+
}
|
|
731
|
+
};
|
|
611
732
|
const response = await fetch(url, {
|
|
612
733
|
method: "POST",
|
|
613
734
|
headers,
|
|
614
|
-
body: JSON.stringify(
|
|
735
|
+
body: JSON.stringify(requestBody)
|
|
615
736
|
});
|
|
616
737
|
if (!response.ok) {
|
|
617
738
|
const errorText = await response.text().catch(() => "Unknown error");
|
|
@@ -649,7 +770,7 @@ var KeverdSDK = class {
|
|
|
649
770
|
* Get default endpoint
|
|
650
771
|
*/
|
|
651
772
|
getDefaultEndpoint() {
|
|
652
|
-
return "https://
|
|
773
|
+
return "https://api.keverd.com";
|
|
653
774
|
}
|
|
654
775
|
/**
|
|
655
776
|
* Generate a session ID
|
|
@@ -657,10 +778,204 @@ var KeverdSDK = class {
|
|
|
657
778
|
generateSessionId() {
|
|
658
779
|
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
659
780
|
}
|
|
781
|
+
/**
|
|
782
|
+
* Start a new session (called automatically on init, but can be called manually)
|
|
783
|
+
*/
|
|
784
|
+
async startSession(userId, deviceHash, metadata) {
|
|
785
|
+
if (!this.isInitialized || !this.config) {
|
|
786
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
787
|
+
}
|
|
788
|
+
if (!this.sessionId) {
|
|
789
|
+
this.sessionId = this.generateSessionId();
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/start`;
|
|
793
|
+
const headers = {
|
|
794
|
+
"Content-Type": "application/json"
|
|
795
|
+
};
|
|
796
|
+
const apiKey = this.config.apiKey;
|
|
797
|
+
if (apiKey) {
|
|
798
|
+
headers["x-keverd-key"] = apiKey;
|
|
799
|
+
headers["X-API-KEY"] = apiKey;
|
|
800
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
801
|
+
}
|
|
802
|
+
const deviceInfo = this.deviceCollector.collect();
|
|
803
|
+
await fetch(url, {
|
|
804
|
+
method: "POST",
|
|
805
|
+
headers,
|
|
806
|
+
body: JSON.stringify({
|
|
807
|
+
session_id: this.sessionId,
|
|
808
|
+
user_id: userId || this.config.userId,
|
|
809
|
+
device_hash: deviceHash || deviceInfo.fingerprint,
|
|
810
|
+
session_metadata: metadata || {},
|
|
811
|
+
user_agent: navigator.userAgent,
|
|
812
|
+
browser: this._detectBrowser(),
|
|
813
|
+
os: this._detectOS(),
|
|
814
|
+
platform: "web",
|
|
815
|
+
sdk_type: "web",
|
|
816
|
+
sdk_source: "react"
|
|
817
|
+
})
|
|
818
|
+
});
|
|
819
|
+
if (this.config.debug) {
|
|
820
|
+
console.log(`[Keverd SDK] Session started: ${this.sessionId}`);
|
|
821
|
+
}
|
|
822
|
+
} catch (error) {
|
|
823
|
+
if (this.config.debug) {
|
|
824
|
+
console.warn("[Keverd SDK] Failed to start session on server:", error);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* End the current session
|
|
830
|
+
*/
|
|
831
|
+
async endSession() {
|
|
832
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/end`;
|
|
837
|
+
const headers = {
|
|
838
|
+
"Content-Type": "application/json"
|
|
839
|
+
};
|
|
840
|
+
const apiKey = this.config.apiKey;
|
|
841
|
+
if (apiKey) {
|
|
842
|
+
headers["x-keverd-key"] = apiKey;
|
|
843
|
+
headers["X-API-KEY"] = apiKey;
|
|
844
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
845
|
+
}
|
|
846
|
+
await fetch(url, {
|
|
847
|
+
method: "POST",
|
|
848
|
+
headers
|
|
849
|
+
});
|
|
850
|
+
if (this.config.debug) {
|
|
851
|
+
console.log(`[Keverd SDK] Session ended: ${this.sessionId}`);
|
|
852
|
+
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
if (this.config.debug) {
|
|
855
|
+
console.warn("[Keverd SDK] Failed to end session on server:", error);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Pause the current session (e.g., when app goes to background)
|
|
861
|
+
*/
|
|
862
|
+
async pauseSession() {
|
|
863
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/pause`;
|
|
868
|
+
const headers = {
|
|
869
|
+
"Content-Type": "application/json"
|
|
870
|
+
};
|
|
871
|
+
const apiKey = this.config.apiKey;
|
|
872
|
+
if (apiKey) {
|
|
873
|
+
headers["x-keverd-key"] = apiKey;
|
|
874
|
+
headers["X-API-KEY"] = apiKey;
|
|
875
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
876
|
+
}
|
|
877
|
+
await fetch(url, {
|
|
878
|
+
method: "POST",
|
|
879
|
+
headers
|
|
880
|
+
});
|
|
881
|
+
if (this.config.debug) {
|
|
882
|
+
console.log(`[Keverd SDK] Session paused: ${this.sessionId}`);
|
|
883
|
+
}
|
|
884
|
+
} catch (error) {
|
|
885
|
+
if (this.config.debug) {
|
|
886
|
+
console.warn("[Keverd SDK] Failed to pause session on server:", error);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Resume a paused session (e.g., when app comes to foreground)
|
|
892
|
+
*/
|
|
893
|
+
async resumeSession() {
|
|
894
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
try {
|
|
898
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/resume`;
|
|
899
|
+
const headers = {
|
|
900
|
+
"Content-Type": "application/json"
|
|
901
|
+
};
|
|
902
|
+
const apiKey = this.config.apiKey;
|
|
903
|
+
if (apiKey) {
|
|
904
|
+
headers["x-keverd-key"] = apiKey;
|
|
905
|
+
headers["X-API-KEY"] = apiKey;
|
|
906
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
907
|
+
}
|
|
908
|
+
await fetch(url, {
|
|
909
|
+
method: "POST",
|
|
910
|
+
headers
|
|
911
|
+
});
|
|
912
|
+
if (this.config.debug) {
|
|
913
|
+
console.log(`[Keverd SDK] Session resumed: ${this.sessionId}`);
|
|
914
|
+
}
|
|
915
|
+
} catch (error) {
|
|
916
|
+
if (this.config.debug) {
|
|
917
|
+
console.warn("[Keverd SDK] Failed to resume session on server:", error);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Get current session status
|
|
923
|
+
*/
|
|
924
|
+
async getSessionStatus() {
|
|
925
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/status`;
|
|
930
|
+
const headers = {};
|
|
931
|
+
const apiKey = this.config.apiKey;
|
|
932
|
+
if (apiKey) {
|
|
933
|
+
headers["x-keverd-key"] = apiKey;
|
|
934
|
+
headers["X-API-KEY"] = apiKey;
|
|
935
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
936
|
+
}
|
|
937
|
+
const response = await fetch(url, {
|
|
938
|
+
method: "GET",
|
|
939
|
+
headers
|
|
940
|
+
});
|
|
941
|
+
if (!response.ok) {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
return await response.json();
|
|
945
|
+
} catch (error) {
|
|
946
|
+
if (this.config.debug) {
|
|
947
|
+
console.warn("[Keverd SDK] Failed to get session status:", error);
|
|
948
|
+
}
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Helper methods for browser/OS detection
|
|
954
|
+
*/
|
|
955
|
+
_detectBrowser() {
|
|
956
|
+
const ua = navigator.userAgent;
|
|
957
|
+
if (ua.includes("Chrome") && !ua.includes("Edg")) return "Chrome";
|
|
958
|
+
if (ua.includes("Firefox")) return "Firefox";
|
|
959
|
+
if (ua.includes("Safari") && !ua.includes("Chrome")) return "Safari";
|
|
960
|
+
if (ua.includes("Edg")) return "Edge";
|
|
961
|
+
return "Unknown";
|
|
962
|
+
}
|
|
963
|
+
_detectOS() {
|
|
964
|
+
const ua = navigator.userAgent;
|
|
965
|
+
if (ua.includes("Windows")) return "Windows";
|
|
966
|
+
if (ua.includes("Mac")) return "macOS";
|
|
967
|
+
if (ua.includes("Linux")) return "Linux";
|
|
968
|
+
if (ua.includes("Android")) return "Android";
|
|
969
|
+
if (ua.includes("iOS") || ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
|
|
970
|
+
return "Unknown";
|
|
971
|
+
}
|
|
660
972
|
/**
|
|
661
973
|
* Destroy the SDK instance
|
|
662
974
|
*/
|
|
663
|
-
destroy() {
|
|
975
|
+
async destroy() {
|
|
976
|
+
if (this.sessionId) {
|
|
977
|
+
await this.endSession();
|
|
978
|
+
}
|
|
664
979
|
const wasDebug = this.config?.debug;
|
|
665
980
|
this.behavioralCollector.stop();
|
|
666
981
|
this.isInitialized = false;
|
|
@@ -692,14 +1007,14 @@ function KeverdProvider({ loadOptions, children }) {
|
|
|
692
1007
|
if (!sdk.isReady()) {
|
|
693
1008
|
sdk.init({
|
|
694
1009
|
apiKey: loadOptions.apiKey,
|
|
695
|
-
endpoint: loadOptions.endpoint,
|
|
696
1010
|
debug: loadOptions.debug || false
|
|
697
1011
|
});
|
|
698
1012
|
setIsReady(true);
|
|
699
1013
|
}
|
|
700
1014
|
return () => {
|
|
701
1015
|
if (sdk.isReady()) {
|
|
702
|
-
sdk.destroy()
|
|
1016
|
+
sdk.destroy().catch(() => {
|
|
1017
|
+
});
|
|
703
1018
|
setIsReady(false);
|
|
704
1019
|
}
|
|
705
1020
|
};
|