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