@keverdjs/fraud-sdk-react 1.0.0 → 1.1.2
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 +53 -62
- package/dist/index.d.ts +53 -62
- package/dist/index.js +554 -221
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +554 -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)
|
|
@@ -585,6 +700,17 @@ var KeverdSDK = class {
|
|
|
585
700
|
throw keverdError;
|
|
586
701
|
}
|
|
587
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Extract origin and referrer from browser
|
|
705
|
+
*/
|
|
706
|
+
getOriginHeaders() {
|
|
707
|
+
if (typeof window === "undefined") {
|
|
708
|
+
return {};
|
|
709
|
+
}
|
|
710
|
+
const origin = window.location.origin || void 0;
|
|
711
|
+
const referrer = document.referrer || void 0;
|
|
712
|
+
return { origin, referrer };
|
|
713
|
+
}
|
|
588
714
|
/**
|
|
589
715
|
* Send fingerprint request to backend
|
|
590
716
|
*/
|
|
@@ -592,13 +718,19 @@ var KeverdSDK = class {
|
|
|
592
718
|
if (!this.config) {
|
|
593
719
|
throw new Error("SDK not initialized");
|
|
594
720
|
}
|
|
595
|
-
const
|
|
596
|
-
const url = `${endpoint}/fingerprint/score`;
|
|
721
|
+
const url = `${this.getDefaultEndpoint()}/fingerprint/score`;
|
|
597
722
|
const headers = {
|
|
598
723
|
"Content-Type": "application/json",
|
|
599
724
|
"X-SDK-Source": "react"
|
|
600
725
|
// Identify SDK source for backend analytics
|
|
601
726
|
};
|
|
727
|
+
const { origin, referrer } = this.getOriginHeaders();
|
|
728
|
+
if (origin) {
|
|
729
|
+
headers["X-Origin-URL"] = origin;
|
|
730
|
+
}
|
|
731
|
+
if (referrer) {
|
|
732
|
+
headers["X-Referrer-URL"] = referrer;
|
|
733
|
+
}
|
|
602
734
|
const apiKey = this.config.apiKey;
|
|
603
735
|
if (apiKey) {
|
|
604
736
|
headers["x-keverd-key"] = apiKey;
|
|
@@ -608,10 +740,17 @@ var KeverdSDK = class {
|
|
|
608
740
|
if (options?.tag === "sandbox") {
|
|
609
741
|
headers["X-Sandbox"] = "true";
|
|
610
742
|
}
|
|
743
|
+
const requestBody = {
|
|
744
|
+
...request,
|
|
745
|
+
session: {
|
|
746
|
+
...request.session || {},
|
|
747
|
+
sessionId: this.sessionId || request.session?.sessionId
|
|
748
|
+
}
|
|
749
|
+
};
|
|
611
750
|
const response = await fetch(url, {
|
|
612
751
|
method: "POST",
|
|
613
752
|
headers,
|
|
614
|
-
body: JSON.stringify(
|
|
753
|
+
body: JSON.stringify(requestBody)
|
|
615
754
|
});
|
|
616
755
|
if (!response.ok) {
|
|
617
756
|
const errorText = await response.text().catch(() => "Unknown error");
|
|
@@ -649,7 +788,7 @@ var KeverdSDK = class {
|
|
|
649
788
|
* Get default endpoint
|
|
650
789
|
*/
|
|
651
790
|
getDefaultEndpoint() {
|
|
652
|
-
return "https://
|
|
791
|
+
return "https://api.keverd.com";
|
|
653
792
|
}
|
|
654
793
|
/**
|
|
655
794
|
* Generate a session ID
|
|
@@ -657,10 +796,204 @@ var KeverdSDK = class {
|
|
|
657
796
|
generateSessionId() {
|
|
658
797
|
return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
659
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* Start a new session (called automatically on init, but can be called manually)
|
|
801
|
+
*/
|
|
802
|
+
async startSession(userId, deviceHash, metadata) {
|
|
803
|
+
if (!this.isInitialized || !this.config) {
|
|
804
|
+
throw new Error("Keverd SDK not initialized. Call init() first.");
|
|
805
|
+
}
|
|
806
|
+
if (!this.sessionId) {
|
|
807
|
+
this.sessionId = this.generateSessionId();
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/start`;
|
|
811
|
+
const headers = {
|
|
812
|
+
"Content-Type": "application/json"
|
|
813
|
+
};
|
|
814
|
+
const apiKey = this.config.apiKey;
|
|
815
|
+
if (apiKey) {
|
|
816
|
+
headers["x-keverd-key"] = apiKey;
|
|
817
|
+
headers["X-API-KEY"] = apiKey;
|
|
818
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
819
|
+
}
|
|
820
|
+
const deviceInfo = this.deviceCollector.collect();
|
|
821
|
+
await fetch(url, {
|
|
822
|
+
method: "POST",
|
|
823
|
+
headers,
|
|
824
|
+
body: JSON.stringify({
|
|
825
|
+
session_id: this.sessionId,
|
|
826
|
+
user_id: userId || this.config.userId,
|
|
827
|
+
device_hash: deviceHash || deviceInfo.fingerprint,
|
|
828
|
+
session_metadata: metadata || {},
|
|
829
|
+
user_agent: navigator.userAgent,
|
|
830
|
+
browser: this._detectBrowser(),
|
|
831
|
+
os: this._detectOS(),
|
|
832
|
+
platform: "web",
|
|
833
|
+
sdk_type: "web",
|
|
834
|
+
sdk_source: "react"
|
|
835
|
+
})
|
|
836
|
+
});
|
|
837
|
+
if (this.config.debug) {
|
|
838
|
+
console.log(`[Keverd SDK] Session started: ${this.sessionId}`);
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
if (this.config.debug) {
|
|
842
|
+
console.warn("[Keverd SDK] Failed to start session on server:", error);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* End the current session
|
|
848
|
+
*/
|
|
849
|
+
async endSession() {
|
|
850
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
try {
|
|
854
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/end`;
|
|
855
|
+
const headers = {
|
|
856
|
+
"Content-Type": "application/json"
|
|
857
|
+
};
|
|
858
|
+
const apiKey = this.config.apiKey;
|
|
859
|
+
if (apiKey) {
|
|
860
|
+
headers["x-keverd-key"] = apiKey;
|
|
861
|
+
headers["X-API-KEY"] = apiKey;
|
|
862
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
863
|
+
}
|
|
864
|
+
await fetch(url, {
|
|
865
|
+
method: "POST",
|
|
866
|
+
headers
|
|
867
|
+
});
|
|
868
|
+
if (this.config.debug) {
|
|
869
|
+
console.log(`[Keverd SDK] Session ended: ${this.sessionId}`);
|
|
870
|
+
}
|
|
871
|
+
} catch (error) {
|
|
872
|
+
if (this.config.debug) {
|
|
873
|
+
console.warn("[Keverd SDK] Failed to end session on server:", error);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Pause the current session (e.g., when app goes to background)
|
|
879
|
+
*/
|
|
880
|
+
async pauseSession() {
|
|
881
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/pause`;
|
|
886
|
+
const headers = {
|
|
887
|
+
"Content-Type": "application/json"
|
|
888
|
+
};
|
|
889
|
+
const apiKey = this.config.apiKey;
|
|
890
|
+
if (apiKey) {
|
|
891
|
+
headers["x-keverd-key"] = apiKey;
|
|
892
|
+
headers["X-API-KEY"] = apiKey;
|
|
893
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
894
|
+
}
|
|
895
|
+
await fetch(url, {
|
|
896
|
+
method: "POST",
|
|
897
|
+
headers
|
|
898
|
+
});
|
|
899
|
+
if (this.config.debug) {
|
|
900
|
+
console.log(`[Keverd SDK] Session paused: ${this.sessionId}`);
|
|
901
|
+
}
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (this.config.debug) {
|
|
904
|
+
console.warn("[Keverd SDK] Failed to pause session on server:", error);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Resume a paused session (e.g., when app comes to foreground)
|
|
910
|
+
*/
|
|
911
|
+
async resumeSession() {
|
|
912
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
try {
|
|
916
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/resume`;
|
|
917
|
+
const headers = {
|
|
918
|
+
"Content-Type": "application/json"
|
|
919
|
+
};
|
|
920
|
+
const apiKey = this.config.apiKey;
|
|
921
|
+
if (apiKey) {
|
|
922
|
+
headers["x-keverd-key"] = apiKey;
|
|
923
|
+
headers["X-API-KEY"] = apiKey;
|
|
924
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
925
|
+
}
|
|
926
|
+
await fetch(url, {
|
|
927
|
+
method: "POST",
|
|
928
|
+
headers
|
|
929
|
+
});
|
|
930
|
+
if (this.config.debug) {
|
|
931
|
+
console.log(`[Keverd SDK] Session resumed: ${this.sessionId}`);
|
|
932
|
+
}
|
|
933
|
+
} catch (error) {
|
|
934
|
+
if (this.config.debug) {
|
|
935
|
+
console.warn("[Keverd SDK] Failed to resume session on server:", error);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Get current session status
|
|
941
|
+
*/
|
|
942
|
+
async getSessionStatus() {
|
|
943
|
+
if (!this.isInitialized || !this.config || !this.sessionId) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
const url = `${this.getDefaultEndpoint()}/dashboard/sessions/${this.sessionId}/status`;
|
|
948
|
+
const headers = {};
|
|
949
|
+
const apiKey = this.config.apiKey;
|
|
950
|
+
if (apiKey) {
|
|
951
|
+
headers["x-keverd-key"] = apiKey;
|
|
952
|
+
headers["X-API-KEY"] = apiKey;
|
|
953
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
954
|
+
}
|
|
955
|
+
const response = await fetch(url, {
|
|
956
|
+
method: "GET",
|
|
957
|
+
headers
|
|
958
|
+
});
|
|
959
|
+
if (!response.ok) {
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
return await response.json();
|
|
963
|
+
} catch (error) {
|
|
964
|
+
if (this.config.debug) {
|
|
965
|
+
console.warn("[Keverd SDK] Failed to get session status:", error);
|
|
966
|
+
}
|
|
967
|
+
return null;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Helper methods for browser/OS detection
|
|
972
|
+
*/
|
|
973
|
+
_detectBrowser() {
|
|
974
|
+
const ua = navigator.userAgent;
|
|
975
|
+
if (ua.includes("Chrome") && !ua.includes("Edg")) return "Chrome";
|
|
976
|
+
if (ua.includes("Firefox")) return "Firefox";
|
|
977
|
+
if (ua.includes("Safari") && !ua.includes("Chrome")) return "Safari";
|
|
978
|
+
if (ua.includes("Edg")) return "Edge";
|
|
979
|
+
return "Unknown";
|
|
980
|
+
}
|
|
981
|
+
_detectOS() {
|
|
982
|
+
const ua = navigator.userAgent;
|
|
983
|
+
if (ua.includes("Windows")) return "Windows";
|
|
984
|
+
if (ua.includes("Mac")) return "macOS";
|
|
985
|
+
if (ua.includes("Linux")) return "Linux";
|
|
986
|
+
if (ua.includes("Android")) return "Android";
|
|
987
|
+
if (ua.includes("iOS") || ua.includes("iPhone") || ua.includes("iPad")) return "iOS";
|
|
988
|
+
return "Unknown";
|
|
989
|
+
}
|
|
660
990
|
/**
|
|
661
991
|
* Destroy the SDK instance
|
|
662
992
|
*/
|
|
663
|
-
destroy() {
|
|
993
|
+
async destroy() {
|
|
994
|
+
if (this.sessionId) {
|
|
995
|
+
await this.endSession();
|
|
996
|
+
}
|
|
664
997
|
const wasDebug = this.config?.debug;
|
|
665
998
|
this.behavioralCollector.stop();
|
|
666
999
|
this.isInitialized = false;
|
|
@@ -692,14 +1025,14 @@ function KeverdProvider({ loadOptions, children }) {
|
|
|
692
1025
|
if (!sdk.isReady()) {
|
|
693
1026
|
sdk.init({
|
|
694
1027
|
apiKey: loadOptions.apiKey,
|
|
695
|
-
endpoint: loadOptions.endpoint,
|
|
696
1028
|
debug: loadOptions.debug || false
|
|
697
1029
|
});
|
|
698
1030
|
setIsReady(true);
|
|
699
1031
|
}
|
|
700
1032
|
return () => {
|
|
701
1033
|
if (sdk.isReady()) {
|
|
702
|
-
sdk.destroy()
|
|
1034
|
+
sdk.destroy().catch(() => {
|
|
1035
|
+
});
|
|
703
1036
|
setIsReady(false);
|
|
704
1037
|
}
|
|
705
1038
|
};
|