@purespectrum1/purescore-client 0.7.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 +55 -0
- package/lib/application/auto-init.d.ts +44 -0
- package/lib/application/auto-init.js +245 -0
- package/lib/application/device-identifier.d.ts +116 -0
- package/lib/application/device-identifier.js +169 -0
- package/lib/domain/models/crosscutting/device-intelligence.d.ts +11 -0
- package/lib/domain/models/crosscutting/device-intelligence.js +18 -0
- package/lib/domain/models/crosscutting/index.d.ts +1 -0
- package/lib/domain/models/crosscutting/index.js +17 -0
- package/lib/domain/protocols/cache/get-storage.d.ts +8 -0
- package/lib/domain/protocols/cache/get-storage.js +2 -0
- package/lib/domain/protocols/cache/index.d.ts +3 -0
- package/lib/domain/protocols/cache/index.js +19 -0
- package/lib/domain/protocols/cache/remove-storage.d.ts +6 -0
- package/lib/domain/protocols/cache/remove-storage.js +2 -0
- package/lib/domain/protocols/cache/set-storage.d.ts +7 -0
- package/lib/domain/protocols/cache/set-storage.js +2 -0
- package/lib/domain/protocols/http/http-client.d.ts +23 -0
- package/lib/domain/protocols/http/http-client.js +13 -0
- package/lib/domain/protocols/services/behavioural-biometrics/cursor-behavior.d.ts +63 -0
- package/lib/domain/protocols/services/behavioural-biometrics/cursor-behavior.js +2 -0
- package/lib/domain/protocols/services/behavioural-biometrics/index.d.ts +3 -0
- package/lib/domain/protocols/services/behavioural-biometrics/index.js +19 -0
- package/lib/domain/protocols/services/behavioural-biometrics/keystorke-metrics.d.ts +35 -0
- package/lib/domain/protocols/services/behavioural-biometrics/keystorke-metrics.js +2 -0
- package/lib/domain/protocols/services/behavioural-biometrics/touch-behavior.d.ts +88 -0
- package/lib/domain/protocols/services/behavioural-biometrics/touch-behavior.js +2 -0
- package/lib/domain/protocols/services/device-fingerprint/display.d.ts +22 -0
- package/lib/domain/protocols/services/device-fingerprint/display.js +2 -0
- package/lib/domain/protocols/services/device-fingerprint/index.d.ts +3 -0
- package/lib/domain/protocols/services/device-fingerprint/index.js +19 -0
- package/lib/domain/protocols/services/device-fingerprint/navigator.d.ts +63 -0
- package/lib/domain/protocols/services/device-fingerprint/navigator.js +2 -0
- package/lib/domain/protocols/services/device-fingerprint/utils.d.ts +40 -0
- package/lib/domain/protocols/services/device-fingerprint/utils.js +2 -0
- package/lib/domain/protocols/services/device-identifier.d.ts +96 -0
- package/lib/domain/protocols/services/device-identifier.js +2 -0
- package/lib/domain/protocols/services/device-intelligence.d.ts +5 -0
- package/lib/domain/protocols/services/device-intelligence.js +2 -0
- package/lib/domain/protocols/services/listeners.d.ts +9 -0
- package/lib/domain/protocols/services/listeners.js +2 -0
- package/lib/domain/protocols/services/single-script-injection/auto-init.d.ts +120 -0
- package/lib/domain/protocols/services/single-script-injection/auto-init.js +2 -0
- package/lib/domain/protocols/services/single-script-injection/index.d.ts +1 -0
- package/lib/domain/protocols/services/single-script-injection/index.js +17 -0
- package/lib/domain/services/behavioural-biometrics/collector.d.ts +94 -0
- package/lib/domain/services/behavioural-biometrics/collector.js +229 -0
- package/lib/domain/services/behavioural-biometrics/cursor-behavior.d.ts +87 -0
- package/lib/domain/services/behavioural-biometrics/cursor-behavior.js +369 -0
- package/lib/domain/services/behavioural-biometrics/index.d.ts +4 -0
- package/lib/domain/services/behavioural-biometrics/index.js +20 -0
- package/lib/domain/services/behavioural-biometrics/keystroke-metrics.d.ts +60 -0
- package/lib/domain/services/behavioural-biometrics/keystroke-metrics.js +213 -0
- package/lib/domain/services/behavioural-biometrics/touch-behavior.d.ts +125 -0
- package/lib/domain/services/behavioural-biometrics/touch-behavior.js +413 -0
- package/lib/domain/services/device-fingerprint/audio.d.ts +14 -0
- package/lib/domain/services/device-fingerprint/audio.js +109 -0
- package/lib/domain/services/device-fingerprint/canvas.d.ts +10 -0
- package/lib/domain/services/device-fingerprint/canvas.js +33 -0
- package/lib/domain/services/device-fingerprint/display.d.ts +49 -0
- package/lib/domain/services/device-fingerprint/display.js +88 -0
- package/lib/domain/services/device-fingerprint/hash.d.ts +14 -0
- package/lib/domain/services/device-fingerprint/hash.js +60 -0
- package/lib/domain/services/device-fingerprint/index.d.ts +8 -0
- package/lib/domain/services/device-fingerprint/index.js +24 -0
- package/lib/domain/services/device-fingerprint/navigator.d.ts +110 -0
- package/lib/domain/services/device-fingerprint/navigator.js +213 -0
- package/lib/domain/services/device-fingerprint/pointer-capability.d.ts +14 -0
- package/lib/domain/services/device-fingerprint/pointer-capability.js +42 -0
- package/lib/domain/services/device-fingerprint/storage.d.ts +44 -0
- package/lib/domain/services/device-fingerprint/storage.js +64 -0
- package/lib/domain/services/device-fingerprint/utils.d.ts +42 -0
- package/lib/domain/services/device-fingerprint/utils.js +95 -0
- package/lib/domain/services/device-intelligence.d.ts +10 -0
- package/lib/domain/services/device-intelligence.js +59 -0
- package/lib/domain/services/single-script-injection/checkpoint-scheduler.d.ts +37 -0
- package/lib/domain/services/single-script-injection/checkpoint-scheduler.js +94 -0
- package/lib/domain/services/single-script-injection/index.d.ts +3 -0
- package/lib/domain/services/single-script-injection/index.js +19 -0
- package/lib/domain/services/single-script-injection/response-delivery.d.ts +47 -0
- package/lib/domain/services/single-script-injection/response-delivery.js +115 -0
- package/lib/domain/services/single-script-injection/schedule-store.d.ts +69 -0
- package/lib/domain/services/single-script-injection/schedule-store.js +146 -0
- package/lib/domain/services/snapshot-store.d.ts +19 -0
- package/lib/domain/services/snapshot-store.js +36 -0
- package/lib/domain/services/spa-navigation.d.ts +15 -0
- package/lib/domain/services/spa-navigation.js +40 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.js +27 -0
- package/lib/infra/cache/index.d.ts +3 -0
- package/lib/infra/cache/index.js +19 -0
- package/lib/infra/cache/local-storage-adapter.d.ts +8 -0
- package/lib/infra/cache/local-storage-adapter.js +14 -0
- package/lib/infra/cache/session-storage-adapter.d.ts +8 -0
- package/lib/infra/cache/session-storage-adapter.js +14 -0
- package/lib/infra/cache/web-storage-adapter.d.ts +13 -0
- package/lib/infra/cache/web-storage-adapter.js +33 -0
- package/lib/infra/http/axios-http-client.d.ts +4 -0
- package/lib/infra/http/axios-http-client.js +39 -0
- package/lib/validation/builder.d.ts +43 -0
- package/lib/validation/builder.js +85 -0
- package/lib/validation/composite.d.ts +13 -0
- package/lib/validation/composite.js +23 -0
- package/lib/validation/errors/index.d.ts +2 -0
- package/lib/validation/errors/index.js +18 -0
- package/lib/validation/errors/invalid-field-error.d.ts +7 -0
- package/lib/validation/errors/invalid-field-error.js +14 -0
- package/lib/validation/errors/required-field-error.d.ts +3 -0
- package/lib/validation/errors/required-field-error.js +10 -0
- package/lib/validation/index.d.ts +5 -0
- package/lib/validation/index.js +21 -0
- package/lib/validation/protocols/index.d.ts +1 -0
- package/lib/validation/protocols/index.js +17 -0
- package/lib/validation/protocols/validator.d.ts +11 -0
- package/lib/validation/protocols/validator.js +2 -0
- package/lib/validation/validators/behavioural-signal.d.ts +27 -0
- package/lib/validation/validators/behavioural-signal.js +56 -0
- package/lib/validation/validators/country-code.d.ts +14 -0
- package/lib/validation/validators/country-code.js +24 -0
- package/lib/validation/validators/global-function.d.ts +14 -0
- package/lib/validation/validators/global-function.js +29 -0
- package/lib/validation/validators/index.d.ts +9 -0
- package/lib/validation/validators/index.js +25 -0
- package/lib/validation/validators/interpolated-template.d.ts +14 -0
- package/lib/validation/validators/interpolated-template.js +28 -0
- package/lib/validation/validators/json-object.d.ts +14 -0
- package/lib/validation/validators/json-object.js +33 -0
- package/lib/validation/validators/language-code.d.ts +13 -0
- package/lib/validation/validators/language-code.js +23 -0
- package/lib/validation/validators/positive-number.d.ts +13 -0
- package/lib/validation/validators/positive-number.js +24 -0
- package/lib/validation/validators/required-string.d.ts +10 -0
- package/lib/validation/validators/required-string.js +19 -0
- package/lib/validation/validators/schedule-record.d.ts +26 -0
- package/lib/validation/validators/schedule-record.js +33 -0
- package/package.json +51 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CursorBehavior = void 0;
|
|
4
|
+
class CursorBehavior {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.movements = [];
|
|
7
|
+
this.velocities = [];
|
|
8
|
+
this.accelerations = [];
|
|
9
|
+
this.scrolls = [];
|
|
10
|
+
this.lastPoint = null;
|
|
11
|
+
this.lastVelocity = null;
|
|
12
|
+
this.lastSampleTime = 0;
|
|
13
|
+
this.angleChangeCount = 0;
|
|
14
|
+
this.lastAngle = null;
|
|
15
|
+
this.hoverStart = null;
|
|
16
|
+
this.isTracking = false;
|
|
17
|
+
this.sampleRate = 50;
|
|
18
|
+
this.significantAngleChange = Math.PI / 6;
|
|
19
|
+
this.maxDataPoints = 500;
|
|
20
|
+
this.pruneInterval = 50;
|
|
21
|
+
this.pruneCounter = 0;
|
|
22
|
+
this.mouseMoveHandler = this.handleMouseMove.bind(this);
|
|
23
|
+
this.mouseDownHandler = this.handleMouseDown.bind(this);
|
|
24
|
+
this.wheelHandler = this.handleWheel.bind(this);
|
|
25
|
+
this.scrollHandler = this.handleScroll.bind(this);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start tracking mouse behavior
|
|
29
|
+
*/
|
|
30
|
+
startTracking(maxDataPoints) {
|
|
31
|
+
if (this.isTracking || typeof window === 'undefined') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.maxDataPoints = maxDataPoints !== null && maxDataPoints !== void 0 ? maxDataPoints : 1000;
|
|
35
|
+
this.resetData();
|
|
36
|
+
this.isTracking = true;
|
|
37
|
+
window.addEventListener('mousemove', this.mouseMoveHandler);
|
|
38
|
+
window.addEventListener('mousedown', this.mouseDownHandler);
|
|
39
|
+
window.addEventListener('wheel', this.wheelHandler, { passive: true });
|
|
40
|
+
window.addEventListener('scroll', this.scrollHandler, { passive: true });
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Stops tracking cursor behavior without clearing collected data.
|
|
44
|
+
*/
|
|
45
|
+
stopTracking() {
|
|
46
|
+
if (!this.isTracking)
|
|
47
|
+
return;
|
|
48
|
+
this.isTracking = false;
|
|
49
|
+
window.removeEventListener('mousemove', this.mouseMoveHandler);
|
|
50
|
+
window.removeEventListener('mousedown', this.mouseDownHandler);
|
|
51
|
+
window.removeEventListener('wheel', this.wheelHandler);
|
|
52
|
+
window.removeEventListener('scroll', this.scrollHandler);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns true if this tracker currently has its DOM listeners attached.
|
|
56
|
+
*/
|
|
57
|
+
hasActiveListeners() {
|
|
58
|
+
return this.isTracking;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Snapshot of raw event buffers for cross-page persistence.
|
|
62
|
+
*/
|
|
63
|
+
getState() {
|
|
64
|
+
return {
|
|
65
|
+
movements: this.movements,
|
|
66
|
+
velocities: this.velocities,
|
|
67
|
+
accelerations: this.accelerations,
|
|
68
|
+
scrolls: this.scrolls,
|
|
69
|
+
angleChangeCount: this.angleChangeCount,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Restores raw event buffers from a snapshot. Transient fields (lastPoint,
|
|
74
|
+
* lastVelocity, hoverStart, etc.) are not restored — they only make sense
|
|
75
|
+
* within a continuous interaction sequence on a single page.
|
|
76
|
+
*/
|
|
77
|
+
restoreState(state) {
|
|
78
|
+
if (state.movements)
|
|
79
|
+
this.movements = state.movements;
|
|
80
|
+
if (state.velocities)
|
|
81
|
+
this.velocities = state.velocities;
|
|
82
|
+
if (state.accelerations)
|
|
83
|
+
this.accelerations = state.accelerations;
|
|
84
|
+
if (state.scrolls)
|
|
85
|
+
this.scrolls = state.scrolls;
|
|
86
|
+
if (typeof state.angleChangeCount === 'number')
|
|
87
|
+
this.angleChangeCount = state.angleChangeCount;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Reset all collected data
|
|
91
|
+
*/
|
|
92
|
+
resetData() {
|
|
93
|
+
this.movements = [];
|
|
94
|
+
this.velocities = [];
|
|
95
|
+
this.accelerations = [];
|
|
96
|
+
this.scrolls = [];
|
|
97
|
+
this.lastPoint = null;
|
|
98
|
+
this.lastVelocity = null;
|
|
99
|
+
this.lastSampleTime = 0;
|
|
100
|
+
this.angleChangeCount = 0;
|
|
101
|
+
this.lastAngle = null;
|
|
102
|
+
this.hoverStart = null;
|
|
103
|
+
this.pruneCounter = 0;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get current behavioral metrics
|
|
107
|
+
*/
|
|
108
|
+
getCursorBehaviorBiometrics() {
|
|
109
|
+
const averageSpeed = this.calculateAverageSpeed();
|
|
110
|
+
return {
|
|
111
|
+
movementMetrics: {
|
|
112
|
+
averageSpeed: averageSpeed,
|
|
113
|
+
maxSpeed: this.calculateMaxSpeed(),
|
|
114
|
+
minSpeed: this.calculateMinSpeed(),
|
|
115
|
+
speedStdDev: this.calculateSpeedStdDev(averageSpeed),
|
|
116
|
+
},
|
|
117
|
+
accelerationMetrics: {
|
|
118
|
+
averageAcceleration: this.calculateAverageAcceleration(),
|
|
119
|
+
maxAcceleration: this.calculateMaxAcceleration(),
|
|
120
|
+
decelerationCount: this.countDecelerations(),
|
|
121
|
+
accelerationCount: this.countAccelerations(),
|
|
122
|
+
},
|
|
123
|
+
trajectoryMetrics: {
|
|
124
|
+
averageCurvature: this.calculateAverageCurvature(),
|
|
125
|
+
straightLinePercentage: this.calculateStraightLinePercentage(),
|
|
126
|
+
averageJerkiness: this.calculateAverageJerkiness(),
|
|
127
|
+
angleChanges: this.angleChangeCount,
|
|
128
|
+
},
|
|
129
|
+
scrollMetrics: {
|
|
130
|
+
totalScrollDistance: this.calculateTotalScrollDistance(),
|
|
131
|
+
averageScrollVelocity: this.calculateAverageScrollVelocity(),
|
|
132
|
+
wheelScrollCount: this.scrolls.filter(s => s.type === 'wheel').length,
|
|
133
|
+
dragScrollCount: this.scrolls.filter(s => s.type === 'drag').length,
|
|
134
|
+
},
|
|
135
|
+
sessionInfo: {
|
|
136
|
+
totalMovements: this.movements.length,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
handleMouseMove(event) {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
// Apply sample rate throttling
|
|
143
|
+
if (now - this.lastSampleTime < this.sampleRate) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.lastSampleTime = now;
|
|
147
|
+
const point = {
|
|
148
|
+
x: event.clientX,
|
|
149
|
+
y: event.clientY,
|
|
150
|
+
timestamp: now
|
|
151
|
+
};
|
|
152
|
+
this.movements.push(point);
|
|
153
|
+
// Calculate velocity and acceleration
|
|
154
|
+
if (this.lastPoint) {
|
|
155
|
+
const velocity = this.calculateVelocity(this.lastPoint, point);
|
|
156
|
+
this.velocities.push(velocity);
|
|
157
|
+
// Track angle changes
|
|
158
|
+
const angle = Math.atan2(velocity.vy, velocity.vx);
|
|
159
|
+
if (this.lastAngle !== null) {
|
|
160
|
+
const angleDiff = Math.abs(angle - this.lastAngle);
|
|
161
|
+
if (angleDiff > this.significantAngleChange) {
|
|
162
|
+
this.angleChangeCount++;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
this.lastAngle = angle;
|
|
166
|
+
if (this.lastVelocity) {
|
|
167
|
+
const acceleration = this.calculateAcceleration(this.lastVelocity, velocity, point.timestamp - this.lastPoint.timestamp);
|
|
168
|
+
this.accelerations.push(acceleration);
|
|
169
|
+
}
|
|
170
|
+
this.lastVelocity = velocity;
|
|
171
|
+
}
|
|
172
|
+
this.lastPoint = point;
|
|
173
|
+
// Update hover tracking
|
|
174
|
+
if (!this.hoverStart) {
|
|
175
|
+
this.hoverStart = point;
|
|
176
|
+
}
|
|
177
|
+
// Batch prune every N events
|
|
178
|
+
if (++this.pruneCounter >= this.pruneInterval) {
|
|
179
|
+
this.pruneCounter = 0;
|
|
180
|
+
this.pruneOldData();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
;
|
|
184
|
+
handleMouseDown(event) {
|
|
185
|
+
// Track when user starts hovering before clicking
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
this.hoverStart = {
|
|
188
|
+
x: event.clientX,
|
|
189
|
+
y: event.clientY,
|
|
190
|
+
timestamp: now
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
;
|
|
194
|
+
handleWheel(event) {
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
const scrollEvent = {
|
|
197
|
+
deltaX: event.deltaX,
|
|
198
|
+
deltaY: event.deltaY,
|
|
199
|
+
timestamp: now,
|
|
200
|
+
velocity: Math.sqrt(event.deltaX ** 2 + event.deltaY ** 2),
|
|
201
|
+
type: 'wheel'
|
|
202
|
+
};
|
|
203
|
+
this.scrolls.push(scrollEvent);
|
|
204
|
+
}
|
|
205
|
+
;
|
|
206
|
+
handleScroll(event) {
|
|
207
|
+
// This detects programmatic or drag scrolling
|
|
208
|
+
const now = Date.now();
|
|
209
|
+
// Avoid duplicate events from wheel
|
|
210
|
+
if (this.scrolls.length > 0) {
|
|
211
|
+
const lastScroll = this.scrolls[this.scrolls.length - 1];
|
|
212
|
+
if (now - lastScroll.timestamp < 50) {
|
|
213
|
+
return; // Too close to last scroll event, likely duplicate
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const scrollEvent = {
|
|
217
|
+
deltaX: 0,
|
|
218
|
+
deltaY: 0,
|
|
219
|
+
timestamp: now,
|
|
220
|
+
velocity: 0,
|
|
221
|
+
type: 'drag'
|
|
222
|
+
};
|
|
223
|
+
this.scrolls.push(scrollEvent);
|
|
224
|
+
}
|
|
225
|
+
;
|
|
226
|
+
calculateVelocity(p1, p2) {
|
|
227
|
+
const dt = p2.timestamp - p1.timestamp;
|
|
228
|
+
if (dt === 0) {
|
|
229
|
+
return { vx: 0, vy: 0, speed: 0 };
|
|
230
|
+
}
|
|
231
|
+
const vx = (p2.x - p1.x) / dt;
|
|
232
|
+
const vy = (p2.y - p1.y) / dt;
|
|
233
|
+
const speed = Math.sqrt(vx ** 2 + vy ** 2);
|
|
234
|
+
return { vx, vy, speed };
|
|
235
|
+
}
|
|
236
|
+
calculateAcceleration(v1, v2, dt) {
|
|
237
|
+
if (dt === 0) {
|
|
238
|
+
return { ax: 0, ay: 0, magnitude: 0 };
|
|
239
|
+
}
|
|
240
|
+
const ax = (v2.vx - v1.vx) / dt;
|
|
241
|
+
const ay = (v2.vy - v1.vy) / dt;
|
|
242
|
+
const magnitude = Math.sqrt(ax ** 2 + ay ** 2);
|
|
243
|
+
return { ax, ay, magnitude };
|
|
244
|
+
}
|
|
245
|
+
calculateDistance(p1, p2) {
|
|
246
|
+
return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
|
|
247
|
+
}
|
|
248
|
+
pruneOldData() {
|
|
249
|
+
if (this.movements.length > this.maxDataPoints) {
|
|
250
|
+
this.movements = this.movements.slice(-this.maxDataPoints);
|
|
251
|
+
}
|
|
252
|
+
if (this.velocities.length > this.maxDataPoints) {
|
|
253
|
+
this.velocities = this.velocities.slice(-this.maxDataPoints);
|
|
254
|
+
}
|
|
255
|
+
if (this.accelerations.length > this.maxDataPoints) {
|
|
256
|
+
this.accelerations = this.accelerations.slice(-this.maxDataPoints);
|
|
257
|
+
}
|
|
258
|
+
if (this.scrolls.length > this.maxDataPoints) {
|
|
259
|
+
this.scrolls = this.scrolls.slice(-this.maxDataPoints);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Metric calculation methods
|
|
263
|
+
calculateAverageSpeed() {
|
|
264
|
+
if (this.velocities.length === 0)
|
|
265
|
+
return null;
|
|
266
|
+
const sum = this.velocities.reduce((acc, v) => acc + v.speed, 0);
|
|
267
|
+
return sum / this.velocities.length;
|
|
268
|
+
}
|
|
269
|
+
calculateMaxSpeed() {
|
|
270
|
+
if (this.velocities.length === 0)
|
|
271
|
+
return null;
|
|
272
|
+
return Math.max(...this.velocities.map(v => v.speed));
|
|
273
|
+
}
|
|
274
|
+
calculateMinSpeed() {
|
|
275
|
+
if (this.velocities.length === 0)
|
|
276
|
+
return null;
|
|
277
|
+
return Math.min(...this.velocities.map(v => v.speed));
|
|
278
|
+
}
|
|
279
|
+
calculateSpeedStdDev(averageSpeed) {
|
|
280
|
+
if (this.velocities.length < 2 || averageSpeed === null)
|
|
281
|
+
return null;
|
|
282
|
+
const squaredDiffs = this.velocities.map(v => (v.speed - averageSpeed) ** 2);
|
|
283
|
+
const variance = squaredDiffs.reduce((acc, val) => acc + val, 0) / this.velocities.length;
|
|
284
|
+
return Math.sqrt(variance);
|
|
285
|
+
}
|
|
286
|
+
calculateAverageAcceleration() {
|
|
287
|
+
if (this.accelerations.length === 0)
|
|
288
|
+
return null;
|
|
289
|
+
const sum = this.accelerations.reduce((acc, a) => acc + a.magnitude, 0);
|
|
290
|
+
return sum / this.accelerations.length;
|
|
291
|
+
}
|
|
292
|
+
calculateMaxAcceleration() {
|
|
293
|
+
if (this.accelerations.length === 0)
|
|
294
|
+
return null;
|
|
295
|
+
return Math.max(...this.accelerations.map(a => a.magnitude));
|
|
296
|
+
}
|
|
297
|
+
countDecelerations() {
|
|
298
|
+
let count = 0;
|
|
299
|
+
for (let i = 1; i < this.velocities.length; i++) {
|
|
300
|
+
if (this.velocities[i].speed < this.velocities[i - 1].speed) {
|
|
301
|
+
count++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return count;
|
|
305
|
+
}
|
|
306
|
+
countAccelerations() {
|
|
307
|
+
let count = 0;
|
|
308
|
+
for (let i = 1; i < this.velocities.length; i++) {
|
|
309
|
+
if (this.velocities[i].speed > this.velocities[i - 1].speed) {
|
|
310
|
+
count++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return count;
|
|
314
|
+
}
|
|
315
|
+
calculateAverageCurvature() {
|
|
316
|
+
if (this.movements.length < 3)
|
|
317
|
+
return null;
|
|
318
|
+
let totalCurvature = 0;
|
|
319
|
+
let count = 0;
|
|
320
|
+
for (let i = 1; i < this.movements.length - 1; i++) {
|
|
321
|
+
const p1 = this.movements[i - 1];
|
|
322
|
+
const p2 = this.movements[i];
|
|
323
|
+
const p3 = this.movements[i + 1];
|
|
324
|
+
const angle1 = Math.atan2(p2.y - p1.y, p2.x - p1.x);
|
|
325
|
+
const angle2 = Math.atan2(p3.y - p2.y, p3.x - p2.x);
|
|
326
|
+
let curvature = Math.abs(angle2 - angle1);
|
|
327
|
+
if (curvature > Math.PI) {
|
|
328
|
+
curvature = 2 * Math.PI - curvature;
|
|
329
|
+
}
|
|
330
|
+
totalCurvature += curvature;
|
|
331
|
+
count++;
|
|
332
|
+
}
|
|
333
|
+
return count > 0 ? totalCurvature / count : null;
|
|
334
|
+
}
|
|
335
|
+
calculateStraightLinePercentage() {
|
|
336
|
+
if (this.movements.length < 3)
|
|
337
|
+
return null;
|
|
338
|
+
const start = this.movements[0];
|
|
339
|
+
const end = this.movements[this.movements.length - 1];
|
|
340
|
+
const directDistance = this.calculateDistance(start, end);
|
|
341
|
+
let pathDistance = 0;
|
|
342
|
+
for (let i = 1; i < this.movements.length; i++) {
|
|
343
|
+
pathDistance += this.calculateDistance(this.movements[i - 1], this.movements[i]);
|
|
344
|
+
}
|
|
345
|
+
if (pathDistance === 0)
|
|
346
|
+
return null;
|
|
347
|
+
return directDistance / pathDistance;
|
|
348
|
+
}
|
|
349
|
+
calculateAverageJerkiness() {
|
|
350
|
+
if (this.accelerations.length < 2)
|
|
351
|
+
return null;
|
|
352
|
+
let totalJerk = 0;
|
|
353
|
+
for (let i = 1; i < this.accelerations.length; i++) {
|
|
354
|
+
const da = this.accelerations[i].magnitude - this.accelerations[i - 1].magnitude;
|
|
355
|
+
totalJerk += Math.abs(da);
|
|
356
|
+
}
|
|
357
|
+
return totalJerk / (this.accelerations.length - 1);
|
|
358
|
+
}
|
|
359
|
+
calculateTotalScrollDistance() {
|
|
360
|
+
return this.scrolls.reduce((acc, s) => acc + Math.sqrt(s.deltaX ** 2 + s.deltaY ** 2), 0);
|
|
361
|
+
}
|
|
362
|
+
calculateAverageScrollVelocity() {
|
|
363
|
+
if (this.scrolls.length === 0)
|
|
364
|
+
return null;
|
|
365
|
+
const sum = this.scrolls.reduce((acc, s) => acc + s.velocity, 0);
|
|
366
|
+
return sum / this.scrolls.length;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
exports.CursorBehavior = CursorBehavior;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./collector"), exports);
|
|
18
|
+
__exportStar(require("./cursor-behavior"), exports);
|
|
19
|
+
__exportStar(require("./keystroke-metrics"), exports);
|
|
20
|
+
__exportStar(require("./touch-behavior"), exports);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { KeyLog, KeyboardMetric } from '../../protocols/services/behavioural-biometrics/keystorke-metrics';
|
|
2
|
+
export declare class KeyboardMetrics {
|
|
3
|
+
private keyHistory;
|
|
4
|
+
private activeKeys;
|
|
5
|
+
private isTracking;
|
|
6
|
+
private startTime;
|
|
7
|
+
private backspaceCount;
|
|
8
|
+
private deleteCount;
|
|
9
|
+
private shiftCount;
|
|
10
|
+
private totalKeyCount;
|
|
11
|
+
private keyDownHandler;
|
|
12
|
+
private keyUpHandler;
|
|
13
|
+
private maxDataPoints;
|
|
14
|
+
private readonly pruneInterval;
|
|
15
|
+
private pruneCounter;
|
|
16
|
+
constructor();
|
|
17
|
+
startTracking(maxDataPoints?: number): void;
|
|
18
|
+
stopTracking(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if this tracker currently has its DOM listeners attached.
|
|
21
|
+
*/
|
|
22
|
+
hasActiveListeners(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Snapshot of raw event buffers + counters for cross-page persistence.
|
|
25
|
+
* `startTime` is included so typing speed is computed over the full session,
|
|
26
|
+
* not just the duration on the current page.
|
|
27
|
+
*/
|
|
28
|
+
getState(): {
|
|
29
|
+
keyHistory: KeyLog[];
|
|
30
|
+
backspaceCount: number;
|
|
31
|
+
deleteCount: number;
|
|
32
|
+
shiftCount: number;
|
|
33
|
+
totalKeyCount: number;
|
|
34
|
+
startTime: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Restores raw event buffers + counters from a snapshot. Transient state
|
|
38
|
+
* (activeKeys map for in-progress key presses) is not restored.
|
|
39
|
+
*/
|
|
40
|
+
restoreState(state: {
|
|
41
|
+
keyHistory?: KeyLog[];
|
|
42
|
+
backspaceCount?: number;
|
|
43
|
+
deleteCount?: number;
|
|
44
|
+
shiftCount?: number;
|
|
45
|
+
totalKeyCount?: number;
|
|
46
|
+
startTime?: number;
|
|
47
|
+
}): void;
|
|
48
|
+
resetData(): void;
|
|
49
|
+
private handleKeyDown;
|
|
50
|
+
private handleKeyUp;
|
|
51
|
+
private pruneOldData;
|
|
52
|
+
/**
|
|
53
|
+
* Metric Calculations
|
|
54
|
+
*/
|
|
55
|
+
getKeyboardMetrics(): KeyboardMetric;
|
|
56
|
+
private calculateDwellTimes;
|
|
57
|
+
private calculateFlightTimes;
|
|
58
|
+
private calculateTypingSpeed;
|
|
59
|
+
private countQuickCorrections;
|
|
60
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyboardMetrics = void 0;
|
|
4
|
+
class KeyboardMetrics {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.keyHistory = [];
|
|
7
|
+
this.activeKeys = new Map();
|
|
8
|
+
this.isTracking = false;
|
|
9
|
+
this.startTime = 0;
|
|
10
|
+
this.backspaceCount = 0;
|
|
11
|
+
this.deleteCount = 0;
|
|
12
|
+
this.shiftCount = 0;
|
|
13
|
+
this.totalKeyCount = 0;
|
|
14
|
+
this.maxDataPoints = 1000;
|
|
15
|
+
this.pruneInterval = 50;
|
|
16
|
+
this.pruneCounter = 0;
|
|
17
|
+
this.keyDownHandler = this.handleKeyDown.bind(this);
|
|
18
|
+
this.keyUpHandler = this.handleKeyUp.bind(this);
|
|
19
|
+
}
|
|
20
|
+
startTracking(maxDataPoints) {
|
|
21
|
+
if (this.isTracking || typeof document === 'undefined')
|
|
22
|
+
return;
|
|
23
|
+
this.maxDataPoints = maxDataPoints !== null && maxDataPoints !== void 0 ? maxDataPoints : 1000;
|
|
24
|
+
this.resetData();
|
|
25
|
+
this.isTracking = true;
|
|
26
|
+
this.startTime = Date.now();
|
|
27
|
+
document.addEventListener('keydown', this.keyDownHandler);
|
|
28
|
+
document.addEventListener('keyup', this.keyUpHandler);
|
|
29
|
+
}
|
|
30
|
+
stopTracking() {
|
|
31
|
+
if (!this.isTracking)
|
|
32
|
+
return;
|
|
33
|
+
document.removeEventListener('keydown', this.keyDownHandler);
|
|
34
|
+
document.removeEventListener('keyup', this.keyUpHandler);
|
|
35
|
+
this.isTracking = false;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if this tracker currently has its DOM listeners attached.
|
|
39
|
+
*/
|
|
40
|
+
hasActiveListeners() {
|
|
41
|
+
return this.isTracking;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Snapshot of raw event buffers + counters for cross-page persistence.
|
|
45
|
+
* `startTime` is included so typing speed is computed over the full session,
|
|
46
|
+
* not just the duration on the current page.
|
|
47
|
+
*/
|
|
48
|
+
getState() {
|
|
49
|
+
return {
|
|
50
|
+
keyHistory: this.keyHistory,
|
|
51
|
+
backspaceCount: this.backspaceCount,
|
|
52
|
+
deleteCount: this.deleteCount,
|
|
53
|
+
shiftCount: this.shiftCount,
|
|
54
|
+
totalKeyCount: this.totalKeyCount,
|
|
55
|
+
startTime: this.startTime,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Restores raw event buffers + counters from a snapshot. Transient state
|
|
60
|
+
* (activeKeys map for in-progress key presses) is not restored.
|
|
61
|
+
*/
|
|
62
|
+
restoreState(state) {
|
|
63
|
+
if (state.keyHistory)
|
|
64
|
+
this.keyHistory = state.keyHistory;
|
|
65
|
+
if (typeof state.backspaceCount === 'number')
|
|
66
|
+
this.backspaceCount = state.backspaceCount;
|
|
67
|
+
if (typeof state.deleteCount === 'number')
|
|
68
|
+
this.deleteCount = state.deleteCount;
|
|
69
|
+
if (typeof state.shiftCount === 'number')
|
|
70
|
+
this.shiftCount = state.shiftCount;
|
|
71
|
+
if (typeof state.totalKeyCount === 'number')
|
|
72
|
+
this.totalKeyCount = state.totalKeyCount;
|
|
73
|
+
if (typeof state.startTime === 'number')
|
|
74
|
+
this.startTime = state.startTime;
|
|
75
|
+
}
|
|
76
|
+
resetData() {
|
|
77
|
+
this.keyHistory = [];
|
|
78
|
+
this.activeKeys.clear();
|
|
79
|
+
this.backspaceCount = 0;
|
|
80
|
+
this.deleteCount = 0;
|
|
81
|
+
this.shiftCount = 0;
|
|
82
|
+
this.totalKeyCount = 0;
|
|
83
|
+
this.pruneCounter = 0;
|
|
84
|
+
this.startTime = Date.now();
|
|
85
|
+
}
|
|
86
|
+
handleKeyDown(event) {
|
|
87
|
+
if (event.repeat)
|
|
88
|
+
return;
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
this.activeKeys.set(event.key, now);
|
|
91
|
+
this.totalKeyCount++;
|
|
92
|
+
if (event.key === 'Backspace')
|
|
93
|
+
this.backspaceCount++;
|
|
94
|
+
if (event.key === 'Delete')
|
|
95
|
+
this.deleteCount++;
|
|
96
|
+
if (event.key === 'Shift')
|
|
97
|
+
this.shiftCount++;
|
|
98
|
+
}
|
|
99
|
+
handleKeyUp(event) {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const pressTime = this.activeKeys.get(event.key);
|
|
102
|
+
if (pressTime) {
|
|
103
|
+
const duration = now - pressTime;
|
|
104
|
+
this.keyHistory.push({
|
|
105
|
+
key: event.key,
|
|
106
|
+
pressTime,
|
|
107
|
+
releaseTime: now,
|
|
108
|
+
duration
|
|
109
|
+
});
|
|
110
|
+
this.activeKeys.delete(event.key);
|
|
111
|
+
}
|
|
112
|
+
if (++this.pruneCounter >= this.pruneInterval) {
|
|
113
|
+
this.pruneCounter = 0;
|
|
114
|
+
this.pruneOldData();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
pruneOldData() {
|
|
118
|
+
if (this.keyHistory.length > this.maxDataPoints) {
|
|
119
|
+
this.keyHistory = this.keyHistory.slice(-this.maxDataPoints);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Metric Calculations
|
|
124
|
+
*/
|
|
125
|
+
getKeyboardMetrics() {
|
|
126
|
+
const dwellTimes = this.calculateDwellTimes();
|
|
127
|
+
const flightTimes = this.calculateFlightTimes();
|
|
128
|
+
const typingSpeed = this.calculateTypingSpeed();
|
|
129
|
+
const totalCorrections = this.backspaceCount + this.deleteCount;
|
|
130
|
+
const correctionRate = this.totalKeyCount > 0 ? (totalCorrections / this.totalKeyCount) : null;
|
|
131
|
+
// Error Rate Calculation:
|
|
132
|
+
// Ratio of corrections to the total intended characters produced
|
|
133
|
+
const allChars = Math.max(0, this.totalKeyCount - (2 * totalCorrections));
|
|
134
|
+
const errorRate = (allChars + totalCorrections) > 0
|
|
135
|
+
? (totalCorrections / (allChars + totalCorrections))
|
|
136
|
+
: null;
|
|
137
|
+
return {
|
|
138
|
+
dwellTimes,
|
|
139
|
+
flightTimes,
|
|
140
|
+
statistics: {
|
|
141
|
+
backspaceCount: this.backspaceCount,
|
|
142
|
+
shiftKeyUsage: this.shiftCount,
|
|
143
|
+
deleteKeyCount: this.deleteCount,
|
|
144
|
+
},
|
|
145
|
+
errors: {
|
|
146
|
+
totalErrors: totalCorrections,
|
|
147
|
+
quickCorrections: this.countQuickCorrections(),
|
|
148
|
+
errorRate: errorRate,
|
|
149
|
+
correctionRate: correctionRate
|
|
150
|
+
},
|
|
151
|
+
typingSpeed
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
calculateDwellTimes() {
|
|
155
|
+
const map = new Map();
|
|
156
|
+
this.keyHistory.forEach(k => {
|
|
157
|
+
const stats = map.get(k.key) || { total: 0, count: 0, last: 0 };
|
|
158
|
+
stats.total += k.duration;
|
|
159
|
+
stats.count += 1;
|
|
160
|
+
stats.last = k.duration;
|
|
161
|
+
map.set(k.key, stats);
|
|
162
|
+
});
|
|
163
|
+
return Array.from(map.entries()).map(([key, stats]) => ({
|
|
164
|
+
key,
|
|
165
|
+
averageDuration: stats.total / stats.count,
|
|
166
|
+
duration: stats.last,
|
|
167
|
+
count: stats.count
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
calculateFlightTimes() {
|
|
171
|
+
const flightMap = new Map();
|
|
172
|
+
for (let i = 1; i < this.keyHistory.length; i++) {
|
|
173
|
+
const from = this.keyHistory[i - 1];
|
|
174
|
+
const to = this.keyHistory[i];
|
|
175
|
+
const flightDuration = to.pressTime - from.releaseTime;
|
|
176
|
+
const pairKey = `${from.key}->${to.key}`;
|
|
177
|
+
const stats = flightMap.get(pairKey) || { total: 0, count: 0, last: 0 };
|
|
178
|
+
stats.total += flightDuration;
|
|
179
|
+
stats.count += 1;
|
|
180
|
+
stats.last = flightDuration;
|
|
181
|
+
flightMap.set(pairKey, stats);
|
|
182
|
+
}
|
|
183
|
+
return Array.from(flightMap.entries()).map(([pair, stats]) => {
|
|
184
|
+
const [fromKey, toKey] = pair.split('->');
|
|
185
|
+
return {
|
|
186
|
+
fromKey,
|
|
187
|
+
toKey,
|
|
188
|
+
averageDuration: stats.total / stats.count,
|
|
189
|
+
duration: stats.last,
|
|
190
|
+
count: stats.count
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
calculateTypingSpeed() {
|
|
195
|
+
if (this.totalKeyCount === 0)
|
|
196
|
+
return null;
|
|
197
|
+
const elapsedMinutes = (Date.now() - this.startTime) / 60000;
|
|
198
|
+
if (elapsedMinutes <= 0)
|
|
199
|
+
return null;
|
|
200
|
+
return (this.totalKeyCount / 5) / elapsedMinutes;
|
|
201
|
+
}
|
|
202
|
+
countQuickCorrections() {
|
|
203
|
+
let quickCorrections = 0;
|
|
204
|
+
for (let i = 1; i < this.keyHistory.length; i++) {
|
|
205
|
+
const isCorrective = this.keyHistory[i].key === 'Backspace' || this.keyHistory[i].key === 'Delete';
|
|
206
|
+
if (isCorrective && (this.keyHistory[i].pressTime - this.keyHistory[i - 1].releaseTime < 500)) {
|
|
207
|
+
quickCorrections++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return quickCorrections;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
exports.KeyboardMetrics = KeyboardMetrics;
|