@surbee/cipher 0.1.0 → 0.2.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.
@@ -1,527 +0,0 @@
1
- /**
2
- * Behavioral checks
3
- *
4
- * Analyzes mouse, keyboard, and interaction patterns to detect bots.
5
- * All checks in this file are offline (no API required).
6
- */
7
-
8
- import type { CheckResult, BehavioralMetrics } from '../types';
9
-
10
- /**
11
- * Check: Low Interaction
12
- *
13
- * Detects minimal mouse/keyboard activity relative to survey duration.
14
- * Bots often have very few interaction events.
15
- */
16
- export function checkLowInteraction(metrics?: BehavioralMetrics): CheckResult {
17
- if (!metrics) {
18
- return {
19
- checkId: 'low_interaction',
20
- passed: true,
21
- score: 0,
22
- details: 'No behavioral data available',
23
- };
24
- }
25
-
26
- const durationSeconds = (metrics.duration || 1) / 1000;
27
- const totalInteractions =
28
- metrics.mouseMovementCount +
29
- metrics.keypressCount +
30
- metrics.scrollEventCount;
31
-
32
- // Expect at least 1 interaction per second on average
33
- const interactionsPerSecond = totalInteractions / durationSeconds;
34
-
35
- if (interactionsPerSecond < 0.1) {
36
- return {
37
- checkId: 'low_interaction',
38
- passed: false,
39
- score: 1.0,
40
- details: 'Almost no user interaction detected',
41
- data: { interactionsPerSecond, totalInteractions, durationSeconds },
42
- };
43
- }
44
-
45
- if (interactionsPerSecond < 0.5) {
46
- return {
47
- checkId: 'low_interaction',
48
- passed: true,
49
- score: 0.6,
50
- details: 'Below average interaction rate',
51
- data: { interactionsPerSecond },
52
- };
53
- }
54
-
55
- return {
56
- checkId: 'low_interaction',
57
- passed: true,
58
- score: 0,
59
- data: { interactionsPerSecond },
60
- };
61
- }
62
-
63
- /**
64
- * Check: Excessive Paste
65
- *
66
- * Detects heavy copy-paste behavior which might indicate:
67
- * - AI-generated content being pasted
68
- * - Copying from other sources
69
- * - Bot automation
70
- */
71
- export function checkExcessivePaste(metrics?: BehavioralMetrics): CheckResult {
72
- if (!metrics) {
73
- return {
74
- checkId: 'excessive_paste',
75
- passed: true,
76
- score: 0,
77
- details: 'No behavioral data available',
78
- };
79
- }
80
-
81
- const { pasteEvents, keypressCount } = metrics;
82
-
83
- // If more pastes than manual keypresses, very suspicious
84
- if (pasteEvents > 0 && keypressCount === 0) {
85
- return {
86
- checkId: 'excessive_paste',
87
- passed: false,
88
- score: 1.0,
89
- details: 'All content was pasted, no typing detected',
90
- data: { pasteEvents, keypressCount },
91
- };
92
- }
93
-
94
- // Ratio of paste events to keypresses
95
- const pasteRatio = keypressCount > 0 ? pasteEvents / keypressCount : 0;
96
-
97
- if (pasteRatio > 0.5) {
98
- return {
99
- checkId: 'excessive_paste',
100
- passed: false,
101
- score: 0.8,
102
- details: 'High paste-to-typing ratio',
103
- data: { pasteRatio, pasteEvents },
104
- };
105
- }
106
-
107
- if (pasteRatio > 0.2) {
108
- return {
109
- checkId: 'excessive_paste',
110
- passed: true,
111
- score: 0.4,
112
- details: 'Some paste events detected',
113
- data: { pasteRatio },
114
- };
115
- }
116
-
117
- return {
118
- checkId: 'excessive_paste',
119
- passed: true,
120
- score: 0,
121
- data: { pasteEvents },
122
- };
123
- }
124
-
125
- /**
126
- * Check: Pointer Velocity Spikes
127
- *
128
- * Detects unnatural mouse movement patterns:
129
- * - Teleporting (instant jumps)
130
- * - Perfectly linear movement
131
- * - Inhuman speeds
132
- */
133
- export function checkPointerSpikes(metrics?: BehavioralMetrics): CheckResult {
134
- if (!metrics?.mouseMovements || metrics.mouseMovements.length < 10) {
135
- return {
136
- checkId: 'pointer_spikes',
137
- passed: true,
138
- score: 0,
139
- details: 'Insufficient mouse data',
140
- };
141
- }
142
-
143
- const velocities = metrics.mouseMovements
144
- .map(m => m.velocity)
145
- .filter(v => v > 0);
146
-
147
- if (velocities.length === 0) {
148
- return {
149
- checkId: 'pointer_spikes',
150
- passed: true,
151
- score: 0,
152
- details: 'No velocity data',
153
- };
154
- }
155
-
156
- // Check for velocity spikes (inhuman speeds > 50 pixels/ms)
157
- const spikeThreshold = 50;
158
- const spikes = velocities.filter(v => v > spikeThreshold);
159
- const spikeRatio = spikes.length / velocities.length;
160
-
161
- if (spikeRatio > 0.3) {
162
- return {
163
- checkId: 'pointer_spikes',
164
- passed: false,
165
- score: 0.9,
166
- details: 'Many unnatural mouse speed spikes detected',
167
- data: { spikeRatio, spikeCount: spikes.length },
168
- };
169
- }
170
-
171
- if (spikeRatio > 0.1) {
172
- return {
173
- checkId: 'pointer_spikes',
174
- passed: true,
175
- score: 0.4,
176
- details: 'Some unusual mouse movements',
177
- data: { spikeRatio },
178
- };
179
- }
180
-
181
- return {
182
- checkId: 'pointer_spikes',
183
- passed: true,
184
- score: 0,
185
- data: { spikeRatio },
186
- };
187
- }
188
-
189
- /**
190
- * Check: Robotic Typing
191
- *
192
- * Detects uniform keystroke timing that suggests automation.
193
- * Humans have natural variation in typing rhythm.
194
- */
195
- export function checkRoboticTyping(metrics?: BehavioralMetrics): CheckResult {
196
- if (!metrics?.keystrokeDynamics || metrics.keystrokeDynamics.length < 10) {
197
- return {
198
- checkId: 'robotic_typing',
199
- passed: true,
200
- score: 0,
201
- details: 'Insufficient keystroke data',
202
- };
203
- }
204
-
205
- const dwellTimes = metrics.keystrokeDynamics.map(k => k.dwell).filter(d => d > 0 && d < 1000);
206
-
207
- if (dwellTimes.length < 5) {
208
- return {
209
- checkId: 'robotic_typing',
210
- passed: true,
211
- score: 0,
212
- details: 'Not enough valid keystroke data',
213
- };
214
- }
215
-
216
- // Calculate coefficient of variation
217
- const mean = dwellTimes.reduce((a, b) => a + b, 0) / dwellTimes.length;
218
- const variance = dwellTimes.reduce((sum, t) => sum + Math.pow(t - mean, 2), 0) / dwellTimes.length;
219
- const stddev = Math.sqrt(variance);
220
- const cv = stddev / mean;
221
-
222
- // Humans typically have CV > 0.25 for keystroke timing
223
- if (cv < 0.08) {
224
- return {
225
- checkId: 'robotic_typing',
226
- passed: false,
227
- score: 1.0,
228
- details: 'Keystroke timing is machine-like uniform',
229
- data: { cv, mean, stddev },
230
- };
231
- }
232
-
233
- if (cv < 0.15) {
234
- return {
235
- checkId: 'robotic_typing',
236
- passed: true,
237
- score: 0.5,
238
- details: 'Lower than typical keystroke variation',
239
- data: { cv },
240
- };
241
- }
242
-
243
- return {
244
- checkId: 'robotic_typing',
245
- passed: true,
246
- score: 0,
247
- data: { cv },
248
- };
249
- }
250
-
251
- /**
252
- * Check: Mouse Teleporting
253
- *
254
- * Detects large instant mouse jumps that are physically impossible.
255
- */
256
- export function checkMouseTeleporting(metrics?: BehavioralMetrics): CheckResult {
257
- if (!metrics?.mouseMovements || metrics.mouseMovements.length < 5) {
258
- return {
259
- checkId: 'mouse_teleporting',
260
- passed: true,
261
- score: 0,
262
- details: 'Insufficient mouse data',
263
- };
264
- }
265
-
266
- let teleportCount = 0;
267
- const movements = metrics.mouseMovements;
268
-
269
- for (let i = 1; i < movements.length; i++) {
270
- const prev = movements[i - 1];
271
- const curr = movements[i];
272
- const dx = curr.x - prev.x;
273
- const dy = curr.y - prev.y;
274
- const distance = Math.sqrt(dx * dx + dy * dy);
275
- const dt = curr.t - prev.t;
276
-
277
- // If moved > 500px in < 10ms, it's a teleport
278
- if (distance > 500 && dt < 10) {
279
- teleportCount++;
280
- }
281
- }
282
-
283
- const teleportRatio = teleportCount / movements.length;
284
-
285
- if (teleportRatio > 0.2) {
286
- return {
287
- checkId: 'mouse_teleporting',
288
- passed: false,
289
- score: 0.9,
290
- details: 'Frequent mouse teleportation detected',
291
- data: { teleportCount, teleportRatio },
292
- };
293
- }
294
-
295
- if (teleportRatio > 0.05) {
296
- return {
297
- checkId: 'mouse_teleporting',
298
- passed: true,
299
- score: 0.4,
300
- details: 'Some mouse teleportation detected',
301
- data: { teleportCount },
302
- };
303
- }
304
-
305
- return {
306
- checkId: 'mouse_teleporting',
307
- passed: true,
308
- score: 0,
309
- data: { teleportCount },
310
- };
311
- }
312
-
313
- /**
314
- * Check: No Corrections
315
- *
316
- * Detects perfect typing with no backspaces or corrections.
317
- * Humans naturally make typos and correct them.
318
- */
319
- export function checkNoCorrections(metrics?: BehavioralMetrics): CheckResult {
320
- if (!metrics || metrics.keypressCount < 20) {
321
- return {
322
- checkId: 'no_corrections',
323
- passed: true,
324
- score: 0,
325
- details: 'Insufficient typing data',
326
- };
327
- }
328
-
329
- const { backspaceCount, keypressCount } = metrics;
330
- const correctionRatio = backspaceCount / keypressCount;
331
-
332
- // Typical humans have ~5-15% backspace rate
333
- if (backspaceCount === 0 && keypressCount > 50) {
334
- return {
335
- checkId: 'no_corrections',
336
- passed: false,
337
- score: 0.8,
338
- details: 'No typing corrections despite significant text entry',
339
- data: { backspaceCount, keypressCount },
340
- };
341
- }
342
-
343
- if (correctionRatio < 0.01 && keypressCount > 30) {
344
- return {
345
- checkId: 'no_corrections',
346
- passed: true,
347
- score: 0.5,
348
- details: 'Very few typing corrections',
349
- data: { correctionRatio },
350
- };
351
- }
352
-
353
- return {
354
- checkId: 'no_corrections',
355
- passed: true,
356
- score: 0,
357
- data: { correctionRatio },
358
- };
359
- }
360
-
361
- /**
362
- * Check: Hover Behavior
363
- *
364
- * Analyzes mouse hover patterns before clicks.
365
- * Humans typically hover before clicking; bots often don't.
366
- */
367
- export function checkHoverBehavior(metrics?: BehavioralMetrics): CheckResult {
368
- if (!metrics?.mouseClicks || metrics.mouseClicks.length < 3) {
369
- return {
370
- checkId: 'hover_behavior',
371
- passed: true,
372
- score: 0,
373
- details: 'Insufficient click data',
374
- };
375
- }
376
-
377
- const clicksWithHover = metrics.mouseClicks.filter(c => c.hadHover);
378
- const hoverRatio = clicksWithHover.length / metrics.mouseClicks.length;
379
-
380
- // Humans typically hover before ~70%+ of clicks
381
- if (hoverRatio < 0.2) {
382
- return {
383
- checkId: 'hover_behavior',
384
- passed: false,
385
- score: 0.8,
386
- details: 'Clicks without natural hover behavior',
387
- data: { hoverRatio, totalClicks: metrics.mouseClicks.length },
388
- };
389
- }
390
-
391
- if (hoverRatio < 0.4) {
392
- return {
393
- checkId: 'hover_behavior',
394
- passed: true,
395
- score: 0.4,
396
- details: 'Lower than typical hover-before-click rate',
397
- data: { hoverRatio },
398
- };
399
- }
400
-
401
- return {
402
- checkId: 'hover_behavior',
403
- passed: true,
404
- score: 0,
405
- data: { hoverRatio },
406
- };
407
- }
408
-
409
- /**
410
- * Check: Scroll Patterns
411
- *
412
- * Analyzes scrolling behavior for signs of automation.
413
- */
414
- export function checkScrollPatterns(metrics?: BehavioralMetrics): CheckResult {
415
- if (!metrics?.scrollEvents || metrics.scrollEvents.length < 5) {
416
- return {
417
- checkId: 'scroll_patterns',
418
- passed: true,
419
- score: 0,
420
- details: 'Insufficient scroll data',
421
- };
422
- }
423
-
424
- const velocities = metrics.scrollEvents.map(s => Math.abs(s.velocity)).filter(v => v > 0);
425
-
426
- if (velocities.length < 3) {
427
- return {
428
- checkId: 'scroll_patterns',
429
- passed: true,
430
- score: 0,
431
- };
432
- }
433
-
434
- // Check for uniform scroll velocity (robotic)
435
- const mean = velocities.reduce((a, b) => a + b, 0) / velocities.length;
436
- const variance = velocities.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / velocities.length;
437
- const cv = Math.sqrt(variance) / mean;
438
-
439
- if (cv < 0.1) {
440
- return {
441
- checkId: 'scroll_patterns',
442
- passed: false,
443
- score: 0.7,
444
- details: 'Unnaturally uniform scroll pattern',
445
- data: { cv },
446
- };
447
- }
448
-
449
- return {
450
- checkId: 'scroll_patterns',
451
- passed: true,
452
- score: 0,
453
- data: { cv },
454
- };
455
- }
456
-
457
- /**
458
- * Check: Mouse Acceleration
459
- *
460
- * Analyzes natural mouse acceleration patterns.
461
- * Real mice have gradual acceleration/deceleration.
462
- */
463
- export function checkMouseAcceleration(metrics?: BehavioralMetrics): CheckResult {
464
- if (!metrics?.mouseMovements || metrics.mouseMovements.length < 20) {
465
- return {
466
- checkId: 'mouse_acceleration',
467
- passed: true,
468
- score: 0,
469
- details: 'Insufficient mouse data',
470
- };
471
- }
472
-
473
- const velocities = metrics.mouseMovements.map(m => m.velocity).filter(v => v > 0);
474
-
475
- if (velocities.length < 10) {
476
- return {
477
- checkId: 'mouse_acceleration',
478
- passed: true,
479
- score: 0,
480
- };
481
- }
482
-
483
- // Calculate acceleration (change in velocity)
484
- const accelerations: number[] = [];
485
- for (let i = 1; i < velocities.length; i++) {
486
- accelerations.push(Math.abs(velocities[i] - velocities[i - 1]));
487
- }
488
-
489
- // Check if acceleration is too uniform
490
- const meanAcc = accelerations.reduce((a, b) => a + b, 0) / accelerations.length;
491
- const varianceAcc = accelerations.reduce((sum, a) => sum + Math.pow(a - meanAcc, 2), 0) / accelerations.length;
492
- const cvAcc = Math.sqrt(varianceAcc) / meanAcc;
493
-
494
- if (cvAcc < 0.2) {
495
- return {
496
- checkId: 'mouse_acceleration',
497
- passed: true,
498
- score: 0.5,
499
- details: 'Lower than typical acceleration variation',
500
- data: { cvAcc },
501
- };
502
- }
503
-
504
- return {
505
- checkId: 'mouse_acceleration',
506
- passed: true,
507
- score: 0,
508
- data: { cvAcc },
509
- };
510
- }
511
-
512
- /**
513
- * Run all behavioral checks
514
- */
515
- export function runBehavioralChecks(metrics?: BehavioralMetrics): CheckResult[] {
516
- return [
517
- checkLowInteraction(metrics),
518
- checkExcessivePaste(metrics),
519
- checkPointerSpikes(metrics),
520
- checkRoboticTyping(metrics),
521
- checkMouseTeleporting(metrics),
522
- checkNoCorrections(metrics),
523
- checkHoverBehavior(metrics),
524
- checkScrollPatterns(metrics),
525
- checkMouseAcceleration(metrics),
526
- ];
527
- }