@chdlabs/my-shield 0.2.0 → 0.3.1
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 +212 -226
- package/package.json +5 -1
- package/src/admin/otpGate.js +370 -0
- package/src/client/collector.js +303 -53
- package/src/client/index.js +2 -1
- package/src/client/shieldClient.js +121 -0
- package/src/core/ipResolver.js +225 -0
- package/src/index.d.ts +152 -10
- package/src/index.js +113 -4
- package/src/middleware/behavioralGuard.js +172 -24
- package/src/middleware/honeypot.js +6 -4
- package/src/middleware/notFoundCollector.js +6 -4
- package/src/middleware/shieldGuard.js +3 -0
- package/src/storage/BaseAdapter.js +58 -0
- package/src/storage/CallbackStorageAdapter.js +69 -0
- package/src/storage/FileStorageAdapter.js +174 -0
- package/src/storage/MemoryAdapter.js +56 -0
- package/src/storage/index.js +53 -0
package/src/client/collector.js
CHANGED
|
@@ -1,57 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/client/collector.js
|
|
3
3
|
* Browser Client Kinematics Telemetry Collector
|
|
4
|
-
* Captures pointer dynamics
|
|
5
|
-
* for behavioral biometric classification. Zero external dependencies.
|
|
4
|
+
* Captures pointer and touch dynamics, measuring continuous trajectories and discrete
|
|
5
|
+
* tap dwell kinetics for behavioral biometric classification. Zero external dependencies.
|
|
6
6
|
*
|
|
7
7
|
* @author ChdLabs
|
|
8
8
|
* @license MIT
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export class
|
|
11
|
+
export class PointerTelemetryCollector {
|
|
12
12
|
/**
|
|
13
13
|
* @param {Object} [options={}]
|
|
14
14
|
* @param {number} [options.windowLength=128] Number of equispaced time points
|
|
15
15
|
* @param {number} [options.sampleIntervalMs=10] Equispaced time delta (10ms = 100Hz)
|
|
16
16
|
* @param {number} [options.maxBufferSize=500] Raw event sliding buffer size
|
|
17
|
+
* @param {number} [options.maxIdleMs=2500] Maximum acceptable idle gap before trajectory considered stale
|
|
18
|
+
* @param {number} [options.minPoints=15] Minimum raw points required for continuous trajectories
|
|
19
|
+
* @param {boolean} [options.flushOnConsumption=true] Flush buffer after getPayload to avoid replaying identical vectors
|
|
20
|
+
* @param {boolean} [options.captureTouch=true] Enable touch/pointer event capture on mobile devices
|
|
21
|
+
* @param {number} [options.minTapDwellMs=20] Minimum tap dwell time in ms to distinguish genuine human taps from instant bot clicks
|
|
22
|
+
* @param {number} [options.maxTapDwellMs=1500] Maximum acceptable tap dwell duration in ms
|
|
17
23
|
*/
|
|
18
24
|
constructor(options = {}) {
|
|
19
25
|
this.windowLength = options.windowLength || 128;
|
|
20
26
|
this.sampleIntervalMs = options.sampleIntervalMs || 10;
|
|
21
27
|
this.maxBufferSize = options.maxBufferSize || 500;
|
|
28
|
+
this.maxIdleMs = options.maxIdleMs ?? 2500;
|
|
29
|
+
this.minPoints = options.minPoints ?? 15;
|
|
30
|
+
this.flushOnConsumption = options.flushOnConsumption !== false;
|
|
31
|
+
this.captureTouch = options.captureTouch !== false;
|
|
32
|
+
this.minTapDwellMs = options.minTapDwellMs ?? 20;
|
|
33
|
+
this.maxTapDwellMs = options.maxTapDwellMs ?? 1500;
|
|
34
|
+
|
|
22
35
|
this.buffer = [];
|
|
36
|
+
this.activePointers = new Map();
|
|
37
|
+
this.lastTapMetrics = null;
|
|
38
|
+
this.detectedPointerType = 'mouse';
|
|
23
39
|
this.isListening = false;
|
|
24
|
-
|
|
40
|
+
|
|
41
|
+
this._onPointerDown = this._onPointerDown.bind(this);
|
|
42
|
+
this._onPointerMove = this._onPointerMove.bind(this);
|
|
43
|
+
this._onPointerUp = this._onPointerUp.bind(this);
|
|
44
|
+
this._onPointerCancel = this._onPointerCancel.bind(this);
|
|
45
|
+
|
|
46
|
+
this._onMouseMove = this._onMouseMove.bind(this);
|
|
47
|
+
this._onTouchStart = this._onTouchStart.bind(this);
|
|
48
|
+
this._onTouchMove = this._onTouchMove.bind(this);
|
|
49
|
+
this._onTouchEnd = this._onTouchEnd.bind(this);
|
|
50
|
+
this._onTouchCancel = this._onTouchCancel.bind(this);
|
|
25
51
|
}
|
|
26
52
|
|
|
27
53
|
/**
|
|
28
|
-
* Start capturing
|
|
54
|
+
* Start capturing pointer and touch movement events
|
|
29
55
|
*/
|
|
30
56
|
start() {
|
|
31
57
|
if (this.isListening || typeof window === 'undefined') return;
|
|
32
|
-
|
|
58
|
+
|
|
59
|
+
if (typeof window.PointerEvent !== 'undefined') {
|
|
60
|
+
window.addEventListener('pointerdown', this._onPointerDown, { passive: true });
|
|
61
|
+
window.addEventListener('pointermove', this._onPointerMove, { passive: true });
|
|
62
|
+
window.addEventListener('pointerup', this._onPointerUp, { passive: true });
|
|
63
|
+
window.addEventListener('pointercancel', this._onPointerCancel, { passive: true });
|
|
64
|
+
} else {
|
|
65
|
+
window.addEventListener('mousemove', this._onMouseMove, { passive: true });
|
|
66
|
+
if (this.captureTouch) {
|
|
67
|
+
window.addEventListener('touchstart', this._onTouchStart, { passive: true });
|
|
68
|
+
window.addEventListener('touchmove', this._onTouchMove, { passive: true });
|
|
69
|
+
window.addEventListener('touchend', this._onTouchEnd, { passive: true });
|
|
70
|
+
window.addEventListener('touchcancel', this._onTouchCancel, { passive: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
33
74
|
this.isListening = true;
|
|
34
75
|
}
|
|
35
76
|
|
|
36
77
|
/**
|
|
37
|
-
* Stop capturing
|
|
78
|
+
* Stop capturing pointer and touch movement events
|
|
38
79
|
*/
|
|
39
80
|
stop() {
|
|
40
81
|
if (!this.isListening || typeof window === 'undefined') return;
|
|
41
|
-
|
|
82
|
+
|
|
83
|
+
if (typeof window.PointerEvent !== 'undefined') {
|
|
84
|
+
window.removeEventListener('pointerdown', this._onPointerDown);
|
|
85
|
+
window.removeEventListener('pointermove', this._onPointerMove);
|
|
86
|
+
window.removeEventListener('pointerup', this._onPointerUp);
|
|
87
|
+
window.removeEventListener('pointercancel', this._onPointerCancel);
|
|
88
|
+
} else {
|
|
89
|
+
window.removeEventListener('mousemove', this._onMouseMove);
|
|
90
|
+
window.removeEventListener('touchstart', this._onTouchStart);
|
|
91
|
+
window.removeEventListener('touchmove', this._onTouchMove);
|
|
92
|
+
window.removeEventListener('touchend', this._onTouchEnd);
|
|
93
|
+
window.removeEventListener('touchcancel', this._onTouchCancel);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.activePointers.clear();
|
|
42
97
|
this.isListening = false;
|
|
43
98
|
}
|
|
44
99
|
|
|
45
100
|
/**
|
|
46
|
-
*
|
|
101
|
+
* Record a raw point into the sliding buffer
|
|
47
102
|
* @private
|
|
48
103
|
*/
|
|
49
|
-
|
|
104
|
+
_recordPoint(x, y, trusted, pointerType = 'mouse') {
|
|
105
|
+
this.detectedPointerType = pointerType;
|
|
50
106
|
this.buffer.push({
|
|
51
|
-
x
|
|
52
|
-
y
|
|
107
|
+
x,
|
|
108
|
+
y,
|
|
53
109
|
t: typeof performance !== 'undefined' ? performance.now() : Date.now(),
|
|
54
|
-
trusted:
|
|
110
|
+
trusted: trusted !== false,
|
|
111
|
+
pointerType
|
|
55
112
|
});
|
|
56
113
|
|
|
57
114
|
if (this.buffer.length > this.maxBufferSize) {
|
|
@@ -60,58 +117,251 @@ export class MouseTelemetryCollector {
|
|
|
60
117
|
}
|
|
61
118
|
|
|
62
119
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @
|
|
120
|
+
* Pointer Events callbacks
|
|
121
|
+
* @private
|
|
65
122
|
*/
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
123
|
+
_onPointerDown(e) {
|
|
124
|
+
const pointerType = e.pointerType || 'touch';
|
|
125
|
+
this.detectedPointerType = pointerType;
|
|
126
|
+
const pointerId = e.pointerId ?? 0;
|
|
127
|
+
this.activePointers.set(pointerId, {
|
|
128
|
+
startX: e.clientX,
|
|
129
|
+
startY: e.clientY,
|
|
130
|
+
startTime: typeof performance !== 'undefined' ? performance.now() : Date.now(),
|
|
131
|
+
trusted: e.isTrusted !== false,
|
|
132
|
+
pointerType
|
|
133
|
+
});
|
|
134
|
+
this._recordPoint(e.clientX, e.clientY, e.isTrusted !== false, pointerType);
|
|
135
|
+
}
|
|
69
136
|
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
137
|
+
_onPointerMove(e) {
|
|
138
|
+
const pointerType = e.pointerType || (this.activePointers.size > 0 ? 'touch' : 'mouse');
|
|
139
|
+
this._recordPoint(e.clientX, e.clientY, e.isTrusted !== false, pointerType);
|
|
140
|
+
}
|
|
73
141
|
|
|
74
|
-
|
|
142
|
+
_onPointerUp(e) {
|
|
143
|
+
const pointerId = e.pointerId ?? 0;
|
|
144
|
+
const start = this.activePointers.get(pointerId);
|
|
145
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
146
|
+
const pointerType = e.pointerType || (start ? start.pointerType : 'touch');
|
|
75
147
|
|
|
76
|
-
|
|
77
|
-
|
|
148
|
+
if (start) {
|
|
149
|
+
const dwellMs = Math.max(0, now - start.startTime);
|
|
150
|
+
const dx = Math.abs(e.clientX - start.startX);
|
|
151
|
+
const dy = Math.abs(e.clientY - start.startY);
|
|
152
|
+
this.lastTapMetrics = {
|
|
153
|
+
dwellMs,
|
|
154
|
+
jitter: Math.hypot(dx, dy),
|
|
155
|
+
pointerType: start.pointerType,
|
|
156
|
+
trusted: start.trusted && (e.isTrusted !== false),
|
|
157
|
+
timestamp: Date.now()
|
|
158
|
+
};
|
|
159
|
+
this.activePointers.delete(pointerId);
|
|
160
|
+
}
|
|
78
161
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const dt = this.sampleIntervalMs;
|
|
162
|
+
this._recordPoint(e.clientX, e.clientY, e.isTrusted !== false, pointerType);
|
|
163
|
+
}
|
|
82
164
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
165
|
+
_onPointerCancel(e) {
|
|
166
|
+
const pointerId = e.pointerId ?? 0;
|
|
167
|
+
this.activePointers.delete(pointerId);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Fallback Mouse Events callback
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
_onMouseMove(e) {
|
|
175
|
+
this._recordPoint(e.clientX, e.clientY, e.isTrusted !== false, 'mouse');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Fallback Touch Events callbacks
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
_onTouchStart(e) {
|
|
183
|
+
if (!e.changedTouches) return;
|
|
184
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
185
|
+
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
186
|
+
const touch = e.changedTouches[i];
|
|
187
|
+
this.activePointers.set(touch.identifier, {
|
|
188
|
+
startX: touch.clientX,
|
|
189
|
+
startY: touch.clientY,
|
|
190
|
+
startTime: now,
|
|
191
|
+
trusted: e.isTrusted !== false,
|
|
192
|
+
pointerType: 'touch'
|
|
193
|
+
});
|
|
194
|
+
this._recordPoint(touch.clientX, touch.clientY, e.isTrusted !== false, 'touch');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
86
197
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
198
|
+
_onTouchMove(e) {
|
|
199
|
+
if (e.touches && e.touches[0]) {
|
|
200
|
+
this._recordPoint(e.touches[0].clientX, e.touches[0].clientY, e.isTrusted !== false, 'touch');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
_onTouchEnd(e) {
|
|
205
|
+
if (!e.changedTouches) return;
|
|
206
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
207
|
+
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
208
|
+
const touch = e.changedTouches[i];
|
|
209
|
+
const start = this.activePointers.get(touch.identifier);
|
|
210
|
+
if (start) {
|
|
211
|
+
const dwellMs = Math.max(0, now - start.startTime);
|
|
212
|
+
const dx = Math.abs(touch.clientX - start.startX);
|
|
213
|
+
const dy = Math.abs(touch.clientY - start.startY);
|
|
214
|
+
this.lastTapMetrics = {
|
|
215
|
+
dwellMs,
|
|
216
|
+
jitter: Math.hypot(dx, dy),
|
|
217
|
+
pointerType: 'touch',
|
|
218
|
+
trusted: start.trusted && (e.isTrusted !== false),
|
|
219
|
+
timestamp: Date.now()
|
|
220
|
+
};
|
|
221
|
+
this.activePointers.delete(touch.identifier);
|
|
92
222
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
223
|
+
this._recordPoint(touch.clientX, touch.clientY, e.isTrusted !== false, 'touch');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
_onTouchCancel(e) {
|
|
228
|
+
if (!e.changedTouches) return;
|
|
229
|
+
for (let i = 0; i < e.changedTouches.length; i++) {
|
|
230
|
+
this.activePointers.delete(e.changedTouches[i].identifier);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resamples trajectory to fixed length and derives kinematic velocity vector,
|
|
236
|
+
* or produces tap dwell kinematics for mobile touch interactions.
|
|
237
|
+
* @returns {{ velocity: number[], isTrusted: boolean, timestamp: number, pointerType: string, source: string, dwellMs?: number, jitter?: number } | null}
|
|
238
|
+
*/
|
|
239
|
+
getPayload() {
|
|
240
|
+
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
241
|
+
|
|
242
|
+
// 1. Continuous Trajectory Path (>= minPoints from mouse movement or touch swipes)
|
|
243
|
+
if (this.buffer.length >= this.minPoints) {
|
|
244
|
+
const latest = this.buffer[this.buffer.length - 1];
|
|
245
|
+
|
|
246
|
+
// Freshness Guard: If pointer has been stationary for > maxIdleMs (e.g. 2.5s), discard stale vectors
|
|
247
|
+
if (this.maxIdleMs > 0 && (now - latest.t > this.maxIdleMs)) {
|
|
248
|
+
if (this.flushOnConsumption) {
|
|
249
|
+
this.buffer = [];
|
|
250
|
+
this.lastTapMetrics = null;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const requiredDuration = this.windowLength * this.sampleIntervalMs;
|
|
256
|
+
const threshold = latest.t - requiredDuration;
|
|
257
|
+
const points = this.buffer.filter(p => p.t >= threshold);
|
|
258
|
+
|
|
259
|
+
if (points.length >= this.minPoints) {
|
|
260
|
+
const allTrusted = points.every(p => p.trusted === true);
|
|
261
|
+
const detectedType = points[points.length - 1].pointerType || this.detectedPointerType || 'mouse';
|
|
262
|
+
|
|
263
|
+
const startTime = points[0].t;
|
|
264
|
+
const L = this.windowLength;
|
|
265
|
+
const dt = this.sampleIntervalMs;
|
|
266
|
+
|
|
267
|
+
const resampledX = new Float64Array(L);
|
|
268
|
+
const resampledY = new Float64Array(L);
|
|
269
|
+
let rawIdx = 0;
|
|
270
|
+
|
|
271
|
+
// Equispaced linear interpolation
|
|
272
|
+
for (let i = 0; i < L; i++) {
|
|
273
|
+
const targetT = startTime + i * dt;
|
|
274
|
+
while (rawIdx < points.length - 2 && points[rawIdx + 1].t < targetT) {
|
|
275
|
+
rawIdx++;
|
|
276
|
+
}
|
|
277
|
+
const p0 = points[rawIdx];
|
|
278
|
+
const p1 = points[Math.min(rawIdx + 1, points.length - 1)];
|
|
279
|
+
const segmentDt = p1.t - p0.t;
|
|
280
|
+
const alpha = segmentDt > 0 ? Math.min(Math.max((targetT - p0.t) / segmentDt, 0), 1) : 0;
|
|
97
281
|
|
|
98
|
-
|
|
99
|
-
|
|
282
|
+
resampledX[i] = p0.x + alpha * (p1.x - p0.x);
|
|
283
|
+
resampledY[i] = p0.y + alpha * (p1.y - p0.y);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Velocity Derivation
|
|
287
|
+
const velocity = new Float64Array(L);
|
|
288
|
+
for (let i = 1; i < L; i++) {
|
|
289
|
+
const dx = resampledX[i] - resampledX[i - 1];
|
|
290
|
+
const dy = resampledY[i] - resampledY[i - 1];
|
|
291
|
+
velocity[i] = Math.hypot(dx, dy) / dt;
|
|
292
|
+
}
|
|
293
|
+
velocity[0] = velocity[1];
|
|
294
|
+
|
|
295
|
+
if (this.flushOnConsumption) {
|
|
296
|
+
this.buffer = [];
|
|
297
|
+
this.lastTapMetrics = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
velocity: Array.from(velocity),
|
|
302
|
+
isTrusted: allTrusted,
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
pointerType: detectedType,
|
|
305
|
+
source: 'trajectory'
|
|
306
|
+
};
|
|
307
|
+
}
|
|
100
308
|
}
|
|
101
309
|
|
|
102
|
-
// 2.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
310
|
+
// 2. Discrete Mobile Tap Path (User tapped a button; insufficient continuous points but valid tap metrics)
|
|
311
|
+
if (this.lastTapMetrics) {
|
|
312
|
+
const tapAgeMs = Date.now() - this.lastTapMetrics.timestamp;
|
|
313
|
+
if (this.maxIdleMs > 0 && tapAgeMs > this.maxIdleMs) {
|
|
314
|
+
if (this.flushOnConsumption) {
|
|
315
|
+
this.buffer = [];
|
|
316
|
+
this.lastTapMetrics = null;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const dwell = this.lastTapMetrics.dwellMs;
|
|
322
|
+
const jitter = this.lastTapMetrics.jitter;
|
|
323
|
+
const pointerType = this.lastTapMetrics.pointerType || 'touch';
|
|
324
|
+
const isTrusted = this.lastTapMetrics.trusted;
|
|
325
|
+
|
|
326
|
+
// Synthesize calibrated micro-tremor velocity series matching the 128-point schema
|
|
327
|
+
const L = this.windowLength;
|
|
328
|
+
const velocity = new Float64Array(L);
|
|
329
|
+
const dwellSteps = Math.min(L, Math.max(3, Math.round(dwell / this.sampleIntervalMs)));
|
|
330
|
+
let v = 0.5 + Math.min(0.5, jitter / 5);
|
|
331
|
+
|
|
332
|
+
for (let t = 0; t < L; t++) {
|
|
333
|
+
if (t < dwellSteps) {
|
|
334
|
+
v += (Math.random() - 0.49) * 0.12;
|
|
335
|
+
velocity[t] = Math.max(0.1, v);
|
|
336
|
+
} else {
|
|
337
|
+
v += (Math.random() - 0.49) * 0.08;
|
|
338
|
+
velocity[t] = Math.max(0.05, v);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const payload = {
|
|
343
|
+
velocity: Array.from(velocity),
|
|
344
|
+
isTrusted,
|
|
345
|
+
timestamp: Date.now(),
|
|
346
|
+
pointerType,
|
|
347
|
+
source: 'tap',
|
|
348
|
+
dwellMs: +(dwell.toFixed(1)),
|
|
349
|
+
jitter: +(jitter.toFixed(2))
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
if (this.flushOnConsumption) {
|
|
353
|
+
this.buffer = [];
|
|
354
|
+
this.lastTapMetrics = null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return payload;
|
|
108
358
|
}
|
|
109
|
-
velocity[0] = velocity[1];
|
|
110
359
|
|
|
111
|
-
return
|
|
112
|
-
velocity: Array.from(velocity),
|
|
113
|
-
isTrusted: allTrusted,
|
|
114
|
-
timestamp: Date.now()
|
|
115
|
-
};
|
|
360
|
+
return null;
|
|
116
361
|
}
|
|
117
362
|
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Backward compatibility alias
|
|
366
|
+
*/
|
|
367
|
+
export const MouseTelemetryCollector = PointerTelemetryCollector;
|
package/src/client/index.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* @license MIT
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export { MouseTelemetryCollector } from './collector.js';
|
|
10
|
+
export { PointerTelemetryCollector, MouseTelemetryCollector } from './collector.js';
|
|
11
|
+
export { ShieldClient } from './shieldClient.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Injects invisible DOM tripwire decoy forms and links to trap automated scrapers
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ShieldClient - Browser Client SDK for Behavioral Biometrics & Tripwires
|
|
3
|
+
*
|
|
4
|
+
* Provides real-time mouse kinematics telemetry capture, freshness verification,
|
|
5
|
+
* replay protection, and transparent HTTP interceptors for fetch and Axios.
|
|
6
|
+
*
|
|
7
|
+
* @author ChdLabs
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
import { PointerTelemetryCollector, MouseTelemetryCollector } from './collector.js';
|
|
11
|
+
import { injectTripwire } from './index.js';
|
|
12
|
+
|
|
13
|
+
export class ShieldClient {
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} [options={}]
|
|
16
|
+
* @param {number} [options.windowLength=128]
|
|
17
|
+
* @param {number} [options.sampleIntervalMs=10]
|
|
18
|
+
* @param {number} [options.maxIdleMs=2500] Discard stale trajectory if pointer idle > 2.5s
|
|
19
|
+
* @param {number} [options.minPoints=15] Require at least 15 events
|
|
20
|
+
* @param {boolean} [options.flushOnConsumption=true] Flush buffer after reading payload
|
|
21
|
+
* @param {boolean} [options.autoInjectTripwire=false] Auto-inject decoy honeypot trap into DOM
|
|
22
|
+
* @param {boolean} [options.captureTouch=true] Enable touch/pointer event capture on mobile devices
|
|
23
|
+
* @param {number} [options.minTapDwellMs=20] Minimum tap dwell time in ms
|
|
24
|
+
* @param {number} [options.maxTapDwellMs=1500] Maximum acceptable tap dwell duration in ms
|
|
25
|
+
*/
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.collector = new PointerTelemetryCollector(options);
|
|
29
|
+
|
|
30
|
+
if (options.autoInjectTripwire && typeof document !== 'undefined') {
|
|
31
|
+
injectTripwire();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Begin recording pointer movements
|
|
37
|
+
*/
|
|
38
|
+
start() {
|
|
39
|
+
this.collector.start();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stop recording pointer movements
|
|
44
|
+
*/
|
|
45
|
+
stop() {
|
|
46
|
+
this.collector.stop();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract normalized 128-sample kinematic telemetry vector
|
|
51
|
+
* @returns {{ velocity: number[], isTrusted: boolean, timestamp: number } | null}
|
|
52
|
+
*/
|
|
53
|
+
getPayload() {
|
|
54
|
+
return this.collector.getPayload();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Transparently instrument window.fetch to attach x-shield-telemetry header
|
|
59
|
+
* @param {Object} [options={}]
|
|
60
|
+
* @param {string} [options.headerName='x-shield-telemetry']
|
|
61
|
+
* @param {string[]} [options.endpoints=['*']] URL patterns to match (e.g. ['/api/auth/*', '/checkout'])
|
|
62
|
+
*/
|
|
63
|
+
instrumentFetch({ headerName = 'x-shield-telemetry', endpoints = ['*'] } = {}) {
|
|
64
|
+
if (typeof window === 'undefined' || typeof window.fetch !== 'function') return;
|
|
65
|
+
const originalFetch = window.fetch;
|
|
66
|
+
const client = this;
|
|
67
|
+
|
|
68
|
+
window.fetch = async function(input, init = {}) {
|
|
69
|
+
let url = typeof input === 'string' ? input : (input?.url || '');
|
|
70
|
+
|
|
71
|
+
const isMatch = endpoints.some(pattern => {
|
|
72
|
+
if (pattern === '*' || pattern === '/*') return true;
|
|
73
|
+
if (pattern.endsWith('*')) return url.startsWith(pattern.slice(0, -1));
|
|
74
|
+
return url === pattern;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (isMatch) {
|
|
78
|
+
const payload = client.getPayload();
|
|
79
|
+
if (payload) {
|
|
80
|
+
const headers = new Headers(init.headers || {});
|
|
81
|
+
headers.set(headerName, JSON.stringify(payload));
|
|
82
|
+
init = { ...init, headers };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return originalFetch.call(this, input, init);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Transparently instrument an Axios instance to attach telemetry headers
|
|
92
|
+
* @param {Object} axiosInstance Axios instance
|
|
93
|
+
* @param {Object} [options={}]
|
|
94
|
+
* @param {string} [options.headerName='x-shield-telemetry']
|
|
95
|
+
* @param {string[]} [options.endpoints=['*']]
|
|
96
|
+
*/
|
|
97
|
+
instrumentAxios(axiosInstance, { headerName = 'x-shield-telemetry', endpoints = ['*'] } = {}) {
|
|
98
|
+
if (!axiosInstance || !axiosInstance.interceptors || !axiosInstance.interceptors.request) return;
|
|
99
|
+
const client = this;
|
|
100
|
+
|
|
101
|
+
axiosInstance.interceptors.request.use((config) => {
|
|
102
|
+
const url = config.url || '';
|
|
103
|
+
const isMatch = endpoints.some(pattern => {
|
|
104
|
+
if (pattern === '*' || pattern === '/*') return true;
|
|
105
|
+
if (pattern.endsWith('*')) return url.startsWith(pattern.slice(0, -1));
|
|
106
|
+
return url === pattern;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (isMatch) {
|
|
110
|
+
const payload = client.getPayload();
|
|
111
|
+
if (payload) {
|
|
112
|
+
config.headers = config.headers || {};
|
|
113
|
+
config.headers[headerName] = JSON.stringify(payload);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return config;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default ShieldClient;
|