@chdlabs/my-shield 0.3.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 CHANGED
@@ -16,7 +16,7 @@
16
16
  - **Docker & Gateway Protection**: Docker bridge gateways (`172.17.0.1`) and loopback addresses (`127.0.0.1`, `::1`) are permanently immune from bans, preventing accidental container isolation.
17
17
  - **Mission-Critical Route Protection**: First-class `shield.strictBotGuard()` to protect `/login`, `/checkout`, and `/register` against credential stuffing, carding (card testing), and automated account spam.
18
18
  - **Dual-Mode Behavioral Biometrics**: Calibrated Ridge decision boundary (`0.50`) supporting non-blocking `'audit'` mode (for searches and queries) alongside strict `'block'` mode (for auth and checkout).
19
- - **First-Party Client SDK (`@chdlabs/my-shield/client`)**: Full-featured `ShieldClient` with pointer freshness guards ($t_{\text{idle}} \le 2.5\text{s}$), buffer flushes on consumption, and automatic `fetch`/`Axios` interceptors.
19
+ - **First-Party Client SDK (`@chdlabs/my-shield/client`)**: Unified `ShieldClient` and `PointerTelemetryCollector` with mobile touch support (`pointerdown`, `touchmove`, tap dwell kinetics), pointer freshness guards ($t_{\text{idle}} \le 2.5\text{s}$), buffer flushes on consumption, and automatic `fetch`/`Axios` interceptors.
20
20
  - **Pluggable Threat Persistence**: Zero-overhead in-memory adapter, local JSON file persistence (`FileStorageAdapter`), or universal database hooks (`PostgreSQL`, `MySQL`, `Redis`, `Prisma`).
21
21
  - **Built-in Admin OTP / MFA Challenge Gate**: Standalone, dark-mode OTP verification portal (`shield.otpGate()`) with timing-safe HMAC validation and brute-force protection.
22
22
  - **Silent Honeypot Decoys**: Static trap routes (`/.env`, `/wp-login.php`, `/.git/config`, etc.) return a silent `200 OK` decoy response and instantly blacklist scanners.
@@ -187,7 +187,7 @@ app.get('/api/search', shield.behavioralGuard({ mode: 'audit' }), (req, res) =>
187
187
 
188
188
  Install `@chdlabs/my-shield` in your frontend application (or import `./client`).
189
189
 
190
- The client captures human pointer micro-movements as the user fills out the form and clicks "Submit" or "Pay Now". When `instrumentFetch()` is enabled, it automatically attaches the `x-shield-telemetry` header to your API requests:
190
+ The client captures human pointer micro-movements across desktop mice, trackpads, and mobile touchscreens (measuring both continuous trajectory gestures and button tap dwell kinetics). When `instrumentFetch()` is enabled, it automatically attaches the `x-shield-telemetry` header to your API requests:
191
191
 
192
192
  ```javascript
193
193
  // client/index.js (React, Vue, or Vanilla JS)
@@ -197,8 +197,10 @@ import { ShieldClient } from '@chdlabs/my-shield/client';
197
197
  const shieldClient = new ShieldClient({
198
198
  sampleIntervalMs: 10,
199
199
  windowLength: 128,
200
- minPoints: 15, // Requires at least 15 raw events to prevent flatlines
200
+ minPoints: 15, // Minimum raw events for continuous trajectories
201
201
  maxIdleMs: 2500, // Freshness Guard: Discards trajectory if pointer idle > 2.5s
202
+ captureTouch: true, // Enables mobile touch & pointer dynamics
203
+ minTapDwellMs: 20, // Distinguishes genuine human taps from instant 0ms bot clicks
202
204
  flushOnConsumption: true, // Prevents replaying identical trajectories
203
205
  autoInjectTripwire: true // Injects invisible honeypot decoy trap into DOM
204
206
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chdlabs/my-shield",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Zero-latency security shield middleware with Bloom filter, honeypots, 404 intelligence, and MiniRocket behavioral biometrics",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,22 +1,25 @@
1
1
  /**
2
2
  * @file src/client/collector.js
3
3
  * Browser Client Kinematics Telemetry Collector
4
- * Captures pointer dynamics and normalizes trajectories to equispaced kinematic series
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 MouseTelemetryCollector {
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
17
  * @param {number} [options.maxIdleMs=2500] Maximum acceptable idle gap before trajectory considered stale
18
- * @param {number} [options.minPoints=15] Minimum raw points required to avoid flat interpolation
18
+ * @param {number} [options.minPoints=15] Minimum raw points required for continuous trajectories
19
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
20
23
  */
21
24
  constructor(options = {}) {
22
25
  this.windowLength = options.windowLength || 128;
@@ -25,39 +28,87 @@ export class MouseTelemetryCollector {
25
28
  this.maxIdleMs = options.maxIdleMs ?? 2500;
26
29
  this.minPoints = options.minPoints ?? 15;
27
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
+
28
35
  this.buffer = [];
36
+ this.activePointers = new Map();
37
+ this.lastTapMetrics = null;
38
+ this.detectedPointerType = 'mouse';
29
39
  this.isListening = false;
30
- this._onMove = this._onMove.bind(this);
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);
31
51
  }
32
52
 
33
53
  /**
34
- * Start capturing mouse movement events
54
+ * Start capturing pointer and touch movement events
35
55
  */
36
56
  start() {
37
57
  if (this.isListening || typeof window === 'undefined') return;
38
- window.addEventListener('mousemove', this._onMove, { passive: true });
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
+
39
74
  this.isListening = true;
40
75
  }
41
76
 
42
77
  /**
43
- * Stop capturing mouse movement events
78
+ * Stop capturing pointer and touch movement events
44
79
  */
45
80
  stop() {
46
81
  if (!this.isListening || typeof window === 'undefined') return;
47
- window.removeEventListener('mousemove', this._onMove);
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();
48
97
  this.isListening = false;
49
98
  }
50
99
 
51
100
  /**
52
- * Event listener callback
101
+ * Record a raw point into the sliding buffer
53
102
  * @private
54
103
  */
55
- _onMove(e) {
104
+ _recordPoint(x, y, trusted, pointerType = 'mouse') {
105
+ this.detectedPointerType = pointerType;
56
106
  this.buffer.push({
57
- x: e.clientX,
58
- y: e.clientY,
107
+ x,
108
+ y,
59
109
  t: typeof performance !== 'undefined' ? performance.now() : Date.now(),
60
- trusted: e.isTrusted !== false
110
+ trusted: trusted !== false,
111
+ pointerType
61
112
  });
62
113
 
63
114
  if (this.buffer.length > this.maxBufferSize) {
@@ -66,71 +117,251 @@ export class MouseTelemetryCollector {
66
117
  }
67
118
 
68
119
  /**
69
- * Resamples trajectory to fixed length and derives kinematic velocity vector.
70
- * @returns {{ velocity: number[], isTrusted: boolean, timestamp: number } | null}
120
+ * Pointer Events callbacks
121
+ * @private
71
122
  */
72
- getPayload() {
73
- if (this.buffer.length < this.minPoints) return null;
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
+ }
136
+
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
+ }
74
141
 
142
+ _onPointerUp(e) {
143
+ const pointerId = e.pointerId ?? 0;
144
+ const start = this.activePointers.get(pointerId);
75
145
  const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
76
- const latest = this.buffer[this.buffer.length - 1];
146
+ const pointerType = e.pointerType || (start ? start.pointerType : 'touch');
77
147
 
78
- // 1. Freshness Guard: If pointer has been stationary for > maxIdleMs (e.g. 2.5s), discard stale vectors
79
- if (this.maxIdleMs > 0 && (now - latest.t > this.maxIdleMs)) {
80
- if (this.flushOnConsumption) this.buffer = [];
81
- return null;
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);
82
160
  }
83
161
 
84
- const requiredDuration = this.windowLength * this.sampleIntervalMs;
85
- const threshold = latest.t - requiredDuration;
86
- const points = this.buffer.filter(p => p.t >= threshold);
162
+ this._recordPoint(e.clientX, e.clientY, e.isTrusted !== false, pointerType);
163
+ }
87
164
 
88
- if (points.length < this.minPoints) return null;
165
+ _onPointerCancel(e) {
166
+ const pointerId = e.pointerId ?? 0;
167
+ this.activePointers.delete(pointerId);
168
+ }
89
169
 
90
- // Check synthetic dispatch
91
- const allTrusted = points.every(p => p.trusted === true);
170
+ /**
171
+ * Fallback Mouse Events callback
172
+ * @private
173
+ */
174
+ _onMouseMove(e) {
175
+ this._recordPoint(e.clientX, e.clientY, e.isTrusted !== false, 'mouse');
176
+ }
92
177
 
93
- const startTime = points[0].t;
94
- const L = this.windowLength;
95
- const dt = this.sampleIntervalMs;
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
+ }
96
197
 
97
- const resampledX = new Float64Array(L);
98
- const resampledY = new Float64Array(L);
99
- let rawIdx = 0;
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
+ }
100
203
 
101
- // 2. Equispaced linear interpolation
102
- for (let i = 0; i < L; i++) {
103
- const targetT = startTime + i * dt;
104
- while (rawIdx < points.length - 2 && points[rawIdx + 1].t < targetT) {
105
- rawIdx++;
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);
106
222
  }
107
- const p0 = points[rawIdx];
108
- const p1 = points[Math.min(rawIdx + 1, points.length - 1)];
109
- const segmentDt = p1.t - p0.t;
110
- const alpha = segmentDt > 0 ? Math.min(Math.max((targetT - p0.t) / segmentDt, 0), 1) : 0;
223
+ this._recordPoint(touch.clientX, touch.clientY, e.isTrusted !== false, 'touch');
224
+ }
225
+ }
111
226
 
112
- resampledX[i] = p0.x + alpha * (p1.x - p0.x);
113
- resampledY[i] = p0.y + alpha * (p1.y - p0.y);
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);
114
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;
281
+
282
+ resampledX[i] = p0.x + alpha * (p1.x - p0.x);
283
+ resampledY[i] = p0.y + alpha * (p1.y - p0.y);
284
+ }
115
285
 
116
- // 3. Velocity Derivation
117
- const velocity = new Float64Array(L);
118
- for (let i = 1; i < L; i++) {
119
- const dx = resampledX[i] - resampledX[i - 1];
120
- const dy = resampledY[i] - resampledY[i - 1];
121
- velocity[i] = Math.hypot(dx, dy) / dt;
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
+ }
122
308
  }
123
- velocity[0] = velocity[1];
124
309
 
125
- // 4. Buffer Flush on Consumption (Stops replay of identical kinematics across multiple clicks)
126
- if (this.flushOnConsumption) {
127
- this.buffer = [];
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;
128
358
  }
129
359
 
130
- return {
131
- velocity: Array.from(velocity),
132
- isTrusted: allTrusted,
133
- timestamp: Date.now()
134
- };
360
+ return null;
135
361
  }
136
362
  }
363
+
364
+ /**
365
+ * Backward compatibility alias
366
+ */
367
+ export const MouseTelemetryCollector = PointerTelemetryCollector;
@@ -7,7 +7,7 @@
7
7
  * @license MIT
8
8
  */
9
9
 
10
- export { MouseTelemetryCollector } from './collector.js';
10
+ export { PointerTelemetryCollector, MouseTelemetryCollector } from './collector.js';
11
11
  export { ShieldClient } from './shieldClient.js';
12
12
 
13
13
  /**
@@ -7,7 +7,7 @@
7
7
  * @author ChdLabs
8
8
  * @license MIT
9
9
  */
10
- import { MouseTelemetryCollector } from './collector.js';
10
+ import { PointerTelemetryCollector, MouseTelemetryCollector } from './collector.js';
11
11
  import { injectTripwire } from './index.js';
12
12
 
13
13
  export class ShieldClient {
@@ -19,10 +19,13 @@ export class ShieldClient {
19
19
  * @param {number} [options.minPoints=15] Require at least 15 events
20
20
  * @param {boolean} [options.flushOnConsumption=true] Flush buffer after reading payload
21
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
22
25
  */
23
26
  constructor(options = {}) {
24
27
  this.options = options;
25
- this.collector = new MouseTelemetryCollector(options);
28
+ this.collector = new PointerTelemetryCollector(options);
26
29
 
27
30
  if (options.autoInjectTripwire && typeof document !== 'undefined') {
28
31
  injectTripwire();
package/src/index.d.ts CHANGED
@@ -187,16 +187,23 @@ export class OTPGate {
187
187
 
188
188
  export function createOtpGate(options?: OtpGateOptions & { logger?: any }): RequestHandler;
189
189
 
190
- export interface MouseTelemetryCollectorOptions {
190
+ export type PointerType = 'mouse' | 'touch' | 'pen' | string;
191
+
192
+ export interface PointerTelemetryCollectorOptions {
191
193
  windowLength?: number;
192
194
  sampleIntervalMs?: number;
193
195
  maxBufferSize?: number;
194
196
  maxIdleMs?: number;
195
197
  minPoints?: number;
196
198
  flushOnConsumption?: boolean;
199
+ captureTouch?: boolean;
200
+ minTapDwellMs?: number;
201
+ maxTapDwellMs?: number;
197
202
  }
198
203
 
199
- export interface ShieldClientOptions extends MouseTelemetryCollectorOptions {
204
+ export type MouseTelemetryCollectorOptions = PointerTelemetryCollectorOptions;
205
+
206
+ export interface ShieldClientOptions extends PointerTelemetryCollectorOptions {
200
207
  autoInjectTripwire?: boolean;
201
208
  }
202
209
 
@@ -205,22 +212,35 @@ export interface InstrumentFetchOptions {
205
212
  endpoints?: string[];
206
213
  }
207
214
 
208
- export class MouseTelemetryCollector {
215
+ export interface PointerTelemetryPayload {
216
+ velocity: number[];
217
+ isTrusted: boolean;
218
+ timestamp: number;
219
+ pointerType?: PointerType;
220
+ source?: 'trajectory' | 'tap' | string;
221
+ dwellMs?: number;
222
+ jitter?: number;
223
+ }
224
+
225
+ export class PointerTelemetryCollector {
209
226
  windowLength: number;
210
227
  sampleIntervalMs: number;
211
228
  maxBufferSize: number;
212
- constructor(options?: MouseTelemetryCollectorOptions);
229
+ constructor(options?: PointerTelemetryCollectorOptions);
213
230
  start(): void;
214
231
  stop(): void;
215
- getPayload(): { velocity: number[]; isTrusted: boolean; timestamp: number } | null;
232
+ getPayload(): PointerTelemetryPayload | null;
216
233
  }
217
234
 
235
+ export const MouseTelemetryCollector: typeof PointerTelemetryCollector;
236
+ export type MouseTelemetryCollector = PointerTelemetryCollector;
237
+
218
238
  export class ShieldClient {
219
- collector: MouseTelemetryCollector;
239
+ collector: PointerTelemetryCollector;
220
240
  constructor(options?: ShieldClientOptions);
221
241
  start(): void;
222
242
  stop(): void;
223
- getPayload(): { velocity: number[]; isTrusted: boolean; timestamp: number } | null;
243
+ getPayload(): PointerTelemetryPayload | null;
224
244
  instrumentFetch(options?: InstrumentFetchOptions): void;
225
245
  instrumentAxios(axiosInstance: any, options?: InstrumentFetchOptions): void;
226
246
  }
@@ -242,6 +262,7 @@ export class Shield extends EventEmitter {
242
262
  strictBotGuard(options?: BehavioralGuardOptions): RequestHandler;
243
263
  otpGate(options?: OtpGateOptions): RequestHandler;
244
264
  verifyBiometrics(velocity: number[] | Float64Array): BehavioralVerificationResult;
265
+ verifyTapDwell(dwellMs: number): BehavioralVerificationResult;
245
266
 
246
267
  blockIP(ip: string, reason?: string, metadata?: Record<string, any>, customTtlMs?: number | null): void;
247
268
  unblockIP(ip: string): void;
package/src/index.js CHANGED
@@ -269,6 +269,14 @@ export class Shield extends EventEmitter {
269
269
  return this.behavioralShieldService.verify(velocityArray);
270
270
  }
271
271
 
272
+ /**
273
+ * Verify mobile tap dwell kinetics against calibrated human distribution
274
+ * @param {number} dwellMs
275
+ */
276
+ verifyTapDwell(dwellMs) {
277
+ return this.behavioralShieldService.verifyTapDwell(dwellMs);
278
+ }
279
+
272
280
  /**
273
281
  * Block an IP address immediately
274
282
  * @param {string} ip
@@ -448,7 +456,7 @@ export {
448
456
  createStorageAdapter
449
457
  } from './storage/index.js';
450
458
  export { ShieldClient } from './client/shieldClient.js';
451
- export { MouseTelemetryCollector } from './client/collector.js';
459
+ export { PointerTelemetryCollector, MouseTelemetryCollector } from './client/collector.js';
452
460
  export { injectTripwire } from './client/index.js';
453
461
 
454
462
  export default createShield;
@@ -76,6 +76,49 @@ export class BehavioralShieldService {
76
76
  return { ...result, latencyMs: latency };
77
77
  }
78
78
 
79
+ /**
80
+ * Evaluate mobile tap dwell kinetics against human physiological tap distribution
81
+ * @param {number} dwellMs Tap contact duration in milliseconds
82
+ * @returns {{ score: number, probability: number, isHuman: boolean, latencyMs: number, reason?: string }}
83
+ */
84
+ verifyTapDwell(dwellMs) {
85
+ const t0 = performance.now();
86
+ if (typeof dwellMs !== 'number' || !Number.isFinite(dwellMs) || dwellMs < 20) {
87
+ return {
88
+ score: -2.5,
89
+ probability: 0.05,
90
+ isHuman: false,
91
+ latencyMs: +(performance.now() - t0).toFixed(2),
92
+ reason: 'BOT_INSTANT_TAP'
93
+ };
94
+ }
95
+ if (dwellMs > 1500) {
96
+ return {
97
+ score: -1.2,
98
+ probability: 0.20,
99
+ isHuman: false,
100
+ latencyMs: +(performance.now() - t0).toFixed(2),
101
+ reason: 'ABNORMAL_TAP_DWELL'
102
+ };
103
+ }
104
+
105
+ // Calibrated log-normal human tap distribution (mean = ~90ms, std = 0.55)
106
+ const logVal = Math.log(dwellMs);
107
+ const mean = Math.log(90);
108
+ const std = 0.55;
109
+ const z = (logVal - mean) / std;
110
+ const score = +(2.0 - 0.8 * Math.abs(z)).toFixed(2);
111
+ const probability = +(1 / (1 + Math.exp(-score))).toFixed(2);
112
+ const latency = +(performance.now() - t0).toFixed(2);
113
+
114
+ return {
115
+ score,
116
+ probability,
117
+ isHuman: probability >= 0.50,
118
+ latencyMs: latency
119
+ };
120
+ }
121
+
79
122
  /**
80
123
  * Extract client IP safely from headers or socket
81
124
  * @param {Object} req
@@ -131,7 +174,7 @@ export class BehavioralShieldService {
131
174
  };
132
175
 
133
176
  const setVerdictHeader = (isHuman, confidence, score, reason) => {
134
- if (res.headersSent) return;
177
+ if (res.headersSent || typeof res.setHeader !== 'function') return;
135
178
  const headerVal = isHuman
136
179
  ? `HUMAN; confidence=${confidence.toFixed(2)}; score=${score.toFixed(2)}`
137
180
  : `BOT; confidence=${confidence.toFixed(2)}; score=${score.toFixed(2)}; reason=${reason || 'AUTOMATED_KINEMATICS_DETECTED'}`;
@@ -220,26 +263,32 @@ export class BehavioralShieldService {
220
263
  }
221
264
  this.nonceCache.set(nonce, true, replayWindowMs);
222
265
 
223
- // 5. MiniRocket & Dual Ridge Scoring
224
- const outcome = this.verify(payload.velocity);
266
+ // 5. Kinematics Scoring (MiniRocket or Touch Tap Dwell)
267
+ const isTap = payload.source === 'tap' && typeof payload.dwellMs === 'number';
268
+ const outcome = isTap ? this.verifyTapDwell(payload.dwellMs) : this.verify(payload.velocity);
225
269
  const ip = this.getClientIP(req);
226
270
  const isHuman = outcome.probability >= threshold;
271
+ const failReason = outcome.reason || 'AUTOMATED_KINEMATICS_DETECTED';
227
272
 
228
273
  req.shield = req.shield || {};
229
274
  req.shield.biometrics = {
230
275
  isHuman,
231
276
  humanConfidence: outcome.probability,
232
277
  latencyMs: outcome.latencyMs,
233
- score: outcome.score
278
+ score: outcome.score,
279
+ pointerType: payload.pointerType || 'mouse',
280
+ source: payload.source || 'trajectory'
234
281
  };
235
282
 
236
- setVerdictHeader(isHuman, outcome.probability, outcome.score, isHuman ? null : 'AUTOMATED_KINEMATICS_DETECTED');
283
+ setVerdictHeader(isHuman, outcome.probability, outcome.score, isHuman ? null : failReason);
237
284
 
238
285
  const verdictMeta = {
239
286
  isHuman,
240
287
  probability: outcome.probability,
241
288
  score: outcome.score,
242
289
  latencyMs: outcome.latencyMs,
290
+ pointerType: payload.pointerType || 'mouse',
291
+ source: payload.source || 'trajectory',
243
292
  ip,
244
293
  mode
245
294
  };
@@ -254,6 +303,8 @@ export class BehavioralShieldService {
254
303
  confidence: outcome.probability,
255
304
  score: outcome.score,
256
305
  latencyMs: outcome.latencyMs,
306
+ pointerType: payload.pointerType || 'mouse',
307
+ source: payload.source || 'trajectory',
257
308
  mode
258
309
  });
259
310
  }
@@ -268,10 +319,12 @@ export class BehavioralShieldService {
268
319
  this.shield.blockIP(ip, 'behavioral_bot_detected', {
269
320
  confidence: outcome.probability,
270
321
  latencyMs: outcome.latencyMs,
271
- score: outcome.score
322
+ score: outcome.score,
323
+ reason: failReason,
324
+ pointerType: payload.pointerType || 'mouse'
272
325
  });
273
326
  }
274
- return this._deny(req, res, 'AUTOMATED_KINEMATICS_DETECTED', onBotDetected, outcome);
327
+ return this._deny(req, res, failReason, onBotDetected, outcome);
275
328
  }
276
329
 
277
330
  next();