engagelab-captcha-sdk 1.0.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.
Files changed (73) hide show
  1. package/dist/captcha-sdk.esm.js +1 -0
  2. package/dist/captcha-sdk.umd.js +1 -0
  3. package/dist/types/api.d.ts +12 -0
  4. package/dist/types/challenges/dragsort.d.ts +24 -0
  5. package/dist/types/challenges/grid.d.ts +20 -0
  6. package/dist/types/challenges/icons.d.ts +18 -0
  7. package/dist/types/challenges/invisible.d.ts +8 -0
  8. package/dist/types/challenges/physics.d.ts +40 -0
  9. package/dist/types/challenges/rotate.d.ts +14 -0
  10. package/dist/types/challenges/slider.d.ts +17 -0
  11. package/dist/types/challenges/spatial.d.ts +12 -0
  12. package/dist/types/compat/hcaptcha.d.ts +1 -0
  13. package/dist/types/compat/recaptcha.d.ts +1 -0
  14. package/dist/types/crypto.d.ts +11 -0
  15. package/dist/types/deep_probes.d.ts +13 -0
  16. package/dist/types/degradation.d.ts +27 -0
  17. package/dist/types/device_attestation.d.ts +19 -0
  18. package/dist/types/fingerprint.d.ts +2 -0
  19. package/dist/types/headless.d.ts +2 -0
  20. package/dist/types/i18n.d.ts +7 -0
  21. package/dist/types/index.d.ts +36 -0
  22. package/dist/types/integrity.d.ts +3 -0
  23. package/dist/types/logger.d.ts +49 -0
  24. package/dist/types/native_monitor.d.ts +21 -0
  25. package/dist/types/passive_signals.d.ts +119 -0
  26. package/dist/types/pow.d.ts +2 -0
  27. package/dist/types/reporter.d.ts +8 -0
  28. package/dist/types/session_persist.d.ts +18 -0
  29. package/dist/types/theme.d.ts +11 -0
  30. package/dist/types/timing.d.ts +2 -0
  31. package/dist/types/trajectory.d.ts +18 -0
  32. package/dist/types/types.d.ts +182 -0
  33. package/dist/types/ui/container.d.ts +20 -0
  34. package/dist/types/ui/styles.d.ts +1 -0
  35. package/dist/types/wasm_core.d.ts +44 -0
  36. package/dist/types/web-component.d.ts +11 -0
  37. package/package.json +22 -0
  38. package/rollup.config.mjs +62 -0
  39. package/src/api.ts +180 -0
  40. package/src/challenges/dragsort.ts +168 -0
  41. package/src/challenges/grid.ts +147 -0
  42. package/src/challenges/icons.ts +106 -0
  43. package/src/challenges/invisible.ts +57 -0
  44. package/src/challenges/physics.ts +437 -0
  45. package/src/challenges/rotate.ts +81 -0
  46. package/src/challenges/slider.ts +168 -0
  47. package/src/challenges/spatial.ts +91 -0
  48. package/src/compat/hcaptcha.ts +112 -0
  49. package/src/compat/recaptcha.ts +108 -0
  50. package/src/crypto.ts +69 -0
  51. package/src/deep_probes.ts +690 -0
  52. package/src/degradation.ts +113 -0
  53. package/src/device_attestation.ts +109 -0
  54. package/src/fingerprint.ts +247 -0
  55. package/src/headless.ts +233 -0
  56. package/src/i18n.ts +455 -0
  57. package/src/index.ts +527 -0
  58. package/src/integrity.ts +100 -0
  59. package/src/logger.ts +170 -0
  60. package/src/native_monitor.ts +100 -0
  61. package/src/passive_signals.ts +544 -0
  62. package/src/pow.ts +120 -0
  63. package/src/reporter.ts +75 -0
  64. package/src/session_persist.ts +90 -0
  65. package/src/theme.ts +41 -0
  66. package/src/timing.ts +79 -0
  67. package/src/trajectory.ts +110 -0
  68. package/src/types.ts +199 -0
  69. package/src/ui/container.ts +161 -0
  70. package/src/ui/styles.ts +153 -0
  71. package/src/wasm_core.ts +189 -0
  72. package/src/web-component.ts +103 -0
  73. package/tsconfig.json +18 -0
@@ -0,0 +1,544 @@
1
+ /**
2
+ * Passive Signal Collection — detect bots through background behavioral analysis.
3
+ * These signals are collected BEFORE the user interacts with any challenge.
4
+ */
5
+
6
+ import { sdkLogger } from './logger';
7
+
8
+ export interface DeviceMotionResult {
9
+ sampleCount: number;
10
+ avgAcceleration: number;
11
+ stdAcceleration: number;
12
+ avgRotationRate: number;
13
+ stdRotationRate: number;
14
+ hasGravity: boolean;
15
+ }
16
+
17
+ export interface DeviceOrientationResult {
18
+ sampleCount: number;
19
+ avgGammaDelta: number;
20
+ avgBetaDelta: number;
21
+ hasData: boolean;
22
+ }
23
+
24
+ export interface PassiveSignalResult {
25
+ rafTiming: RafTimingResult;
26
+ hoverTrap: HoverTrapResult;
27
+ eventTiming: EventTimingResult;
28
+ scrollBehavior: ScrollBehaviorResult;
29
+ idlePattern: IdlePatternResult;
30
+ inputBehavior: InputBehaviorResult;
31
+ deviceMotion?: DeviceMotionResult;
32
+ deviceOrientation?: DeviceOrientationResult;
33
+ }
34
+
35
+ interface RafTimingResult {
36
+ avgInterval: number;
37
+ stdInterval: number;
38
+ minInterval: number;
39
+ maxInterval: number;
40
+ frameCount: number;
41
+ anomaly: boolean;
42
+ anomalyType: string;
43
+ }
44
+
45
+ interface HoverTrapResult {
46
+ triggered: boolean;
47
+ hoverCount: number;
48
+ mouseOverHidden: boolean;
49
+ }
50
+
51
+ interface EventTimingResult {
52
+ mousedownToMouseup: number[];
53
+ mousemoveToClick: number;
54
+ avgClickDuration: number;
55
+ hasNaturalVariance: boolean;
56
+ }
57
+
58
+ interface ScrollBehaviorResult {
59
+ scrollEvents: number;
60
+ hasInertia: boolean;
61
+ avgScrollDelta: number;
62
+ scrollPatternRegular: boolean;
63
+ }
64
+
65
+ interface IdlePatternResult {
66
+ idlePeriods: number;
67
+ avgIdleDuration: number;
68
+ hasNaturalPauses: boolean;
69
+ }
70
+
71
+ interface InputBehaviorResult {
72
+ keyPressIntervals: number[];
73
+ hasTypingRhythm: boolean;
74
+ avgKeyInterval: number;
75
+ keystrokeDynamics: KeystrokeDynamics;
76
+ }
77
+
78
+ interface KeystrokeDynamics {
79
+ dwellTimes: number[]; // How long each key is held (keydown→keyup)
80
+ flightTimes: number[]; // Time between consecutive keyup→keydown
81
+ avgDwell: number;
82
+ avgFlight: number;
83
+ dwellVariance: number;
84
+ flightVariance: number;
85
+ digraphLatencies: Record<string, number[]>; // Timing for common key pairs
86
+ }
87
+
88
+ export class PassiveSignalCollector {
89
+ private rafTimings: number[] = [];
90
+ private lastRafTime = 0;
91
+ private rafHandle = 0;
92
+ private mouseEvents: Array<{ type: string; time: number; x: number; y: number }> = [];
93
+ private scrollEvents: Array<{ time: number; deltaY: number }> = [];
94
+ private keyEvents: Array<{ time: number; key: string; type: 'down' | 'up' }> = [];
95
+ private hoverTrapTriggered = false;
96
+ private hoverTrapElement: HTMLElement | null = null;
97
+ private idlePeriods: number[] = [];
98
+ private lastActivityTime = 0;
99
+ private collecting = false;
100
+
101
+ // Device sensor data
102
+ private motionSamples: Array<{ accel: number; rotation: number }> = [];
103
+ private motionHasGravity = false;
104
+ private orientationDeltas: Array<{ gammaDelta: number; betaDelta: number }> = [];
105
+ private lastGamma: number | null = null;
106
+ private lastBeta: number | null = null;
107
+ private isMobileDevice = false;
108
+
109
+ start(): void {
110
+ if (this.collecting) return;
111
+ this.collecting = true;
112
+ this.lastActivityTime = performance.now();
113
+ sdkLogger.info('PassiveSignals', 'Collector start');
114
+
115
+ // 1. Start rAF timing measurement
116
+ this.measureRaf();
117
+
118
+ // 2. Set up hover trap
119
+ this.setupHoverTrap();
120
+
121
+ // 3. Attach event listeners
122
+ this.attachListeners();
123
+
124
+ // 4. Start device sensor collection (mobile only)
125
+ this.startDeviceSensors();
126
+ }
127
+
128
+ stop(): void {
129
+ this.collecting = false;
130
+ cancelAnimationFrame(this.rafHandle);
131
+ this.removeHoverTrap();
132
+ this.detachListeners();
133
+ this.stopDeviceSensors();
134
+ sdkLogger.info('PassiveSignals', 'Collector stop');
135
+ }
136
+
137
+ getResults(): PassiveSignalResult {
138
+ const rafTiming = this.analyzeRafTiming();
139
+ const hoverTrap = this.analyzeHoverTrap();
140
+ const eventTiming = this.analyzeEventTiming();
141
+ const scrollBehavior = this.analyzeScrollBehavior();
142
+ const idlePattern = this.analyzeIdlePattern();
143
+ const inputBehavior = this.analyzeInputBehavior();
144
+ const deviceMotion = this.analyzeDeviceMotion();
145
+ const deviceOrientation = this.analyzeDeviceOrientation();
146
+ sdkLogger.debug('PassiveSignals', 'Results summary', {
147
+ rafFrames: rafTiming.frameCount,
148
+ rafAnomaly: rafTiming.anomaly,
149
+ hoverTriggered: hoverTrap.triggered,
150
+ mouseEvents: this.mouseEvents.length,
151
+ scrollEvents: scrollBehavior.scrollEvents,
152
+ idlePeriods: idlePattern.idlePeriods,
153
+ motionSamples: deviceMotion?.sampleCount ?? 0,
154
+ orientationSamples: deviceOrientation?.sampleCount ?? 0,
155
+ });
156
+ const result: PassiveSignalResult = {
157
+ rafTiming,
158
+ hoverTrap,
159
+ eventTiming,
160
+ scrollBehavior,
161
+ idlePattern,
162
+ inputBehavior,
163
+ };
164
+ if (deviceMotion) result.deviceMotion = deviceMotion;
165
+ if (deviceOrientation) result.deviceOrientation = deviceOrientation;
166
+ return result;
167
+ }
168
+
169
+ // --- rAF Timing ---
170
+ private measureRaf(): void {
171
+ const measure = (now: number) => {
172
+ if (!this.collecting) return;
173
+ if (this.lastRafTime > 0) {
174
+ this.rafTimings.push(now - this.lastRafTime);
175
+ }
176
+ this.lastRafTime = now;
177
+ // Collect for up to 120 frames (~2 seconds at 60fps)
178
+ if (this.rafTimings.length < 120) {
179
+ this.rafHandle = requestAnimationFrame(measure);
180
+ }
181
+ };
182
+ this.rafHandle = requestAnimationFrame(measure);
183
+ }
184
+
185
+ private analyzeRafTiming(): RafTimingResult {
186
+ const t = this.rafTimings;
187
+ if (t.length < 10) {
188
+ return { avgInterval: 0, stdInterval: 0, minInterval: 0, maxInterval: 0, frameCount: 0, anomaly: false, anomalyType: 'insufficient_data' };
189
+ }
190
+
191
+ const avg = t.reduce((a, b) => a + b, 0) / t.length;
192
+ const std = Math.sqrt(t.reduce((s, v) => s + (v - avg) ** 2, 0) / t.length);
193
+ const min = Math.min(...t);
194
+ const max = Math.max(...t);
195
+
196
+ let anomaly = false;
197
+ let anomalyType = 'none';
198
+
199
+ // Real browser at 60fps: avg ~16.67ms, std ~1-3ms
200
+ // Headless/virtual: avg might be exact 16.67 with std < 0.1
201
+ // Or completely irregular (batch rendering)
202
+ if (std < 0.1 && t.length > 30) {
203
+ anomaly = true;
204
+ anomalyType = 'perfect_timing'; // Too perfect = virtual display
205
+ } else if (avg < 5) {
206
+ anomaly = true;
207
+ anomalyType = 'too_fast'; // Faster than any real display
208
+ } else if (avg > 50 && t.length > 20) {
209
+ anomaly = true;
210
+ anomalyType = 'too_slow'; // Below 20fps consistently
211
+ } else if (std > avg * 0.5 && t.length > 30) {
212
+ anomaly = true;
213
+ anomalyType = 'highly_irregular'; // No real vsync
214
+ }
215
+
216
+ return { avgInterval: avg, stdInterval: std, minInterval: min, maxInterval: max, frameCount: t.length, anomaly, anomalyType };
217
+ }
218
+
219
+ // --- CSS Hover Trap ---
220
+ private setupHoverTrap(): void {
221
+ // Create invisible elements that only real mouse movement would trigger
222
+ const trap = document.createElement('div');
223
+ trap.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:-1;opacity:0;';
224
+
225
+ // Inner trap element with pointer-events auto
226
+ const inner = document.createElement('div');
227
+ inner.style.cssText = 'position:absolute;top:10%;left:10%;width:80%;height:80%;pointer-events:auto;';
228
+
229
+ inner.addEventListener('mouseover', () => {
230
+ this.hoverTrapTriggered = true;
231
+ });
232
+
233
+ trap.appendChild(inner);
234
+ document.body.appendChild(trap);
235
+ this.hoverTrapElement = trap;
236
+ }
237
+
238
+ private removeHoverTrap(): void {
239
+ if (this.hoverTrapElement) {
240
+ document.body.removeChild(this.hoverTrapElement);
241
+ this.hoverTrapElement = null;
242
+ }
243
+ }
244
+
245
+ private analyzeHoverTrap(): HoverTrapResult {
246
+ const hoverEvents = this.mouseEvents.filter(e => e.type === 'mouseover');
247
+ return {
248
+ triggered: this.hoverTrapTriggered,
249
+ hoverCount: hoverEvents.length,
250
+ mouseOverHidden: this.hoverTrapTriggered,
251
+ };
252
+ }
253
+
254
+ // --- Event Listeners ---
255
+ private onMouseEvent = (e: MouseEvent) => {
256
+ if (!this.collecting) return;
257
+ const now = performance.now();
258
+ this.mouseEvents.push({ type: e.type, time: now, x: e.clientX, y: e.clientY });
259
+ this.recordActivity(now);
260
+ // Cap storage
261
+ if (this.mouseEvents.length > 500) this.mouseEvents.splice(0, 100);
262
+ };
263
+
264
+ private onScrollEvent = (e: Event) => {
265
+ if (!this.collecting) return;
266
+ const we = e as WheelEvent;
267
+ const now = performance.now();
268
+ this.scrollEvents.push({ time: now, deltaY: we.deltaY || 0 });
269
+ this.recordActivity(now);
270
+ if (this.scrollEvents.length > 200) this.scrollEvents.splice(0, 50);
271
+ };
272
+
273
+ private onKeyDown = (e: KeyboardEvent) => {
274
+ if (!this.collecting) return;
275
+ const now = performance.now();
276
+ const key = e.key.length === 1 ? 'char' : e.key;
277
+ this.keyEvents.push({ time: now, key, type: 'down' });
278
+ this.recordActivity(now);
279
+ if (this.keyEvents.length > 400) this.keyEvents.splice(0, 100);
280
+ };
281
+
282
+ private onKeyUp = (e: KeyboardEvent) => {
283
+ if (!this.collecting) return;
284
+ const now = performance.now();
285
+ const key = e.key.length === 1 ? 'char' : e.key;
286
+ this.keyEvents.push({ time: now, key, type: 'up' });
287
+ this.recordActivity(now);
288
+ if (this.keyEvents.length > 400) this.keyEvents.splice(0, 100);
289
+ };
290
+
291
+ private recordActivity(now: number): void {
292
+ const gap = now - this.lastActivityTime;
293
+ if (gap > 2000) {
294
+ this.idlePeriods.push(gap);
295
+ }
296
+ this.lastActivityTime = now;
297
+ }
298
+
299
+ private attachListeners(): void {
300
+ document.addEventListener('mousedown', this.onMouseEvent, { passive: true });
301
+ document.addEventListener('mouseup', this.onMouseEvent, { passive: true });
302
+ document.addEventListener('mousemove', this.onMouseEvent, { passive: true });
303
+ document.addEventListener('mouseover', this.onMouseEvent, { passive: true });
304
+ document.addEventListener('wheel', this.onScrollEvent, { passive: true });
305
+ document.addEventListener('keydown', this.onKeyDown, { passive: true });
306
+ document.addEventListener('keyup', this.onKeyUp, { passive: true });
307
+ }
308
+
309
+ private detachListeners(): void {
310
+ document.removeEventListener('mousedown', this.onMouseEvent);
311
+ document.removeEventListener('mouseup', this.onMouseEvent);
312
+ document.removeEventListener('mousemove', this.onMouseEvent);
313
+ document.removeEventListener('mouseover', this.onMouseEvent);
314
+ document.removeEventListener('wheel', this.onScrollEvent);
315
+ document.removeEventListener('keydown', this.onKeyDown);
316
+ document.removeEventListener('keyup', this.onKeyUp);
317
+ }
318
+
319
+ // --- Analysis Methods ---
320
+ private analyzeEventTiming(): EventTimingResult {
321
+ const downs = this.mouseEvents.filter(e => e.type === 'mousedown');
322
+ const ups = this.mouseEvents.filter(e => e.type === 'mouseup');
323
+
324
+ const clickDurations: number[] = [];
325
+ for (const down of downs) {
326
+ const up = ups.find(u => u.time > down.time && u.time - down.time < 1000);
327
+ if (up) clickDurations.push(up.time - down.time);
328
+ }
329
+
330
+ const moves = this.mouseEvents.filter(e => e.type === 'mousemove');
331
+ let moveThenClick = 0;
332
+ if (moves.length > 0 && downs.length > 0) {
333
+ const lastMove = moves[moves.length - 1];
334
+ const firstClick = downs[0];
335
+ moveThenClick = firstClick.time - lastMove.time;
336
+ }
337
+
338
+ const avgClick = clickDurations.length > 0
339
+ ? clickDurations.reduce((a, b) => a + b, 0) / clickDurations.length
340
+ : 0;
341
+
342
+ const hasVariance = clickDurations.length >= 3 &&
343
+ (Math.max(...clickDurations) - Math.min(...clickDurations)) > 20;
344
+
345
+ return {
346
+ mousedownToMouseup: clickDurations.slice(0, 10),
347
+ mousemoveToClick: moveThenClick,
348
+ avgClickDuration: avgClick,
349
+ hasNaturalVariance: hasVariance,
350
+ };
351
+ }
352
+
353
+ private analyzeScrollBehavior(): ScrollBehaviorResult {
354
+ const se = this.scrollEvents;
355
+ if (se.length < 3) {
356
+ return { scrollEvents: se.length, hasInertia: false, avgScrollDelta: 0, scrollPatternRegular: false };
357
+ }
358
+
359
+ const deltas = se.map(s => Math.abs(s.deltaY));
360
+ const avgDelta = deltas.reduce((a, b) => a + b, 0) / deltas.length;
361
+
362
+ // Check for inertia (decreasing deltas = natural scroll)
363
+ let hasInertia = false;
364
+ for (let i = 0; i < se.length - 3; i++) {
365
+ const d1 = Math.abs(se[i].deltaY);
366
+ const d2 = Math.abs(se[i + 1].deltaY);
367
+ const d3 = Math.abs(se[i + 2].deltaY);
368
+ if (d1 > d2 && d2 > d3 && d1 > 10) {
369
+ hasInertia = true;
370
+ break;
371
+ }
372
+ }
373
+
374
+ // Check regularity
375
+ const intervals = [];
376
+ for (let i = 1; i < se.length; i++) {
377
+ intervals.push(se[i].time - se[i - 1].time);
378
+ }
379
+ const ivAvg = intervals.reduce((a, b) => a + b, 0) / intervals.length;
380
+ const ivStd = Math.sqrt(intervals.reduce((s, v) => s + (v - ivAvg) ** 2, 0) / intervals.length);
381
+ const regular = ivAvg > 0 && ivStd / ivAvg < 0.1;
382
+
383
+ return { scrollEvents: se.length, hasInertia, avgScrollDelta: avgDelta, scrollPatternRegular: regular };
384
+ }
385
+
386
+ private analyzeIdlePattern(): IdlePatternResult {
387
+ const ip = this.idlePeriods;
388
+ const avgIdle = ip.length > 0 ? ip.reduce((a, b) => a + b, 0) / ip.length : 0;
389
+ return {
390
+ idlePeriods: ip.length,
391
+ avgIdleDuration: avgIdle,
392
+ hasNaturalPauses: ip.length >= 1 && avgIdle > 500,
393
+ };
394
+ }
395
+
396
+ private analyzeInputBehavior(): InputBehaviorResult {
397
+ const ke = this.keyEvents;
398
+ const intervals: number[] = [];
399
+ const downEvents = ke.filter(e => e.type === 'down');
400
+ for (let i = 1; i < downEvents.length; i++) {
401
+ intervals.push(downEvents[i].time - downEvents[i - 1].time);
402
+ }
403
+ const avg = intervals.length > 0 ? intervals.reduce((a, b) => a + b, 0) / intervals.length : 0;
404
+ const hasRhythm = intervals.length >= 5 && avg > 50 && avg < 500;
405
+
406
+ // Keystroke dynamics: dwell times (key hold duration) and flight times
407
+ const dwellTimes: number[] = [];
408
+ const flightTimes: number[] = [];
409
+ const digraphLatencies: Record<string, number[]> = {};
410
+
411
+ // Match keydown→keyup pairs for dwell time
412
+ const pendingDown: Map<string, number> = new Map();
413
+ const upHistory: Array<{ time: number; key: string }> = [];
414
+
415
+ for (const event of ke) {
416
+ if (event.type === 'down') {
417
+ pendingDown.set(event.key, event.time);
418
+ // Flight time: previous keyup → current keydown
419
+ if (upHistory.length > 0) {
420
+ const lastUp = upHistory[upHistory.length - 1];
421
+ const flight = event.time - lastUp.time;
422
+ if (flight > 0 && flight < 2000) {
423
+ flightTimes.push(flight);
424
+ // Digraph: pair of consecutive keys
425
+ const digraph = `${lastUp.key}_${event.key}`;
426
+ if (!digraphLatencies[digraph]) digraphLatencies[digraph] = [];
427
+ digraphLatencies[digraph].push(flight);
428
+ }
429
+ }
430
+ } else if (event.type === 'up') {
431
+ const downTime = pendingDown.get(event.key);
432
+ if (downTime !== undefined) {
433
+ const dwell = event.time - downTime;
434
+ if (dwell > 0 && dwell < 2000) {
435
+ dwellTimes.push(dwell);
436
+ }
437
+ pendingDown.delete(event.key);
438
+ }
439
+ upHistory.push({ time: event.time, key: event.key });
440
+ if (upHistory.length > 50) upHistory.splice(0, 25);
441
+ }
442
+ }
443
+
444
+ const mean = (arr: number[]) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
445
+ const variance = (arr: number[]) => {
446
+ if (arr.length < 2) return 0;
447
+ const m = mean(arr);
448
+ return arr.reduce((s, v) => s + (v - m) ** 2, 0) / arr.length;
449
+ };
450
+
451
+ return {
452
+ keyPressIntervals: intervals.slice(0, 20),
453
+ hasTypingRhythm: hasRhythm,
454
+ avgKeyInterval: avg,
455
+ keystrokeDynamics: {
456
+ dwellTimes: dwellTimes.slice(0, 30),
457
+ flightTimes: flightTimes.slice(0, 30),
458
+ avgDwell: mean(dwellTimes),
459
+ avgFlight: mean(flightTimes),
460
+ dwellVariance: variance(dwellTimes),
461
+ flightVariance: variance(flightTimes),
462
+ digraphLatencies,
463
+ },
464
+ };
465
+ }
466
+
467
+ // --- Device Sensor Collection ---
468
+ private onDeviceMotion = (e: DeviceMotionEvent) => {
469
+ if (!this.collecting) return;
470
+ const a = e.accelerationIncludingGravity;
471
+ const r = e.rotationRate;
472
+ if (a) {
473
+ const accelMag = Math.sqrt((a.x ?? 0) ** 2 + (a.y ?? 0) ** 2 + (a.z ?? 0) ** 2);
474
+ const rotMag = r ? Math.sqrt((r.alpha ?? 0) ** 2 + (r.beta ?? 0) ** 2 + (r.gamma ?? 0) ** 2) : 0;
475
+ this.motionSamples.push({ accel: accelMag, rotation: rotMag });
476
+ if (e.acceleration && (e.acceleration.x !== null || e.acceleration.y !== null)) {
477
+ this.motionHasGravity = true;
478
+ }
479
+ }
480
+ if (this.motionSamples.length > 200) this.motionSamples.splice(0, 50);
481
+ };
482
+
483
+ private onDeviceOrientation = (e: DeviceOrientationEvent) => {
484
+ if (!this.collecting) return;
485
+ const gamma = e.gamma ?? 0;
486
+ const beta = e.beta ?? 0;
487
+ if (this.lastGamma !== null && this.lastBeta !== null) {
488
+ this.orientationDeltas.push({
489
+ gammaDelta: Math.abs(gamma - this.lastGamma),
490
+ betaDelta: Math.abs(beta - this.lastBeta),
491
+ });
492
+ }
493
+ this.lastGamma = gamma;
494
+ this.lastBeta = beta;
495
+ if (this.orientationDeltas.length > 200) this.orientationDeltas.splice(0, 50);
496
+ };
497
+
498
+ private startDeviceSensors(): void {
499
+ this.isMobileDevice = navigator.maxTouchPoints > 0;
500
+ if (!this.isMobileDevice) return;
501
+ window.addEventListener('devicemotion', this.onDeviceMotion, { passive: true });
502
+ window.addEventListener('deviceorientation', this.onDeviceOrientation, { passive: true });
503
+ sdkLogger.debug('PassiveSignals', 'Device sensors started (mobile detected)');
504
+ }
505
+
506
+ private stopDeviceSensors(): void {
507
+ if (!this.isMobileDevice) return;
508
+ window.removeEventListener('devicemotion', this.onDeviceMotion);
509
+ window.removeEventListener('deviceorientation', this.onDeviceOrientation);
510
+ }
511
+
512
+ private analyzeDeviceMotion(): DeviceMotionResult | undefined {
513
+ if (!this.isMobileDevice) return undefined;
514
+ const samples = this.motionSamples;
515
+ const accels = samples.map(s => s.accel);
516
+ const rots = samples.map(s => s.rotation);
517
+ const mean = (arr: number[]) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
518
+ const std = (arr: number[]) => {
519
+ if (arr.length < 2) return 0;
520
+ const m = mean(arr);
521
+ return Math.sqrt(arr.reduce((s, v) => s + (v - m) ** 2, 0) / arr.length);
522
+ };
523
+ return {
524
+ sampleCount: samples.length,
525
+ avgAcceleration: mean(accels),
526
+ stdAcceleration: std(accels),
527
+ avgRotationRate: mean(rots),
528
+ stdRotationRate: std(rots),
529
+ hasGravity: this.motionHasGravity,
530
+ };
531
+ }
532
+
533
+ private analyzeDeviceOrientation(): DeviceOrientationResult | undefined {
534
+ if (!this.isMobileDevice) return undefined;
535
+ const deltas = this.orientationDeltas;
536
+ const mean = (arr: number[]) => arr.length > 0 ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
537
+ return {
538
+ sampleCount: deltas.length,
539
+ avgGammaDelta: mean(deltas.map(d => d.gammaDelta)),
540
+ avgBetaDelta: mean(deltas.map(d => d.betaDelta)),
541
+ hasData: deltas.length > 0,
542
+ };
543
+ }
544
+ }
package/src/pow.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { PowResult } from './types';
2
+ import { sdkLogger } from './logger';
3
+
4
+ export async function computePoW(challenge: string, difficulty: number): Promise<PowResult> {
5
+ sdkLogger.info('PoW', 'Computation start', {
6
+ difficulty,
7
+ challengeLen: challenge?.length ?? 0,
8
+ useWorker: typeof Worker !== 'undefined',
9
+ });
10
+ const result =
11
+ typeof Worker !== 'undefined'
12
+ ? await computeInWorker(challenge, difficulty)
13
+ : await computeInline(challenge, difficulty);
14
+ sdkLogger.info('PoW', 'Computation done', {
15
+ iterations: result.iterations,
16
+ timeMs: `${result.computationTimeMs.toFixed(1)}ms`,
17
+ difficulty: result.difficulty,
18
+ });
19
+ return result;
20
+ }
21
+
22
+ function computeInWorker(challenge: string, difficulty: number): Promise<PowResult> {
23
+ return new Promise((resolve, reject) => {
24
+ const workerCode = `
25
+ self.onmessage = async function(e) {
26
+ const { challenge, difficulty } = e.data;
27
+ const prefix = '0'.repeat(difficulty);
28
+ const start = performance.now();
29
+ let nonce = 0;
30
+ const encoder = new TextEncoder();
31
+
32
+ while (true) {
33
+ const data = encoder.encode(challenge + nonce.toString());
34
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
35
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
36
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
37
+
38
+ if (hashHex.startsWith(prefix)) {
39
+ self.postMessage({
40
+ nonce: nonce.toString(),
41
+ computationTimeMs: performance.now() - start,
42
+ iterations: nonce + 1,
43
+ });
44
+ return;
45
+ }
46
+ nonce++;
47
+ if (nonce > 10000000) {
48
+ self.postMessage({ error: 'timeout' });
49
+ return;
50
+ }
51
+ }
52
+ };
53
+ `;
54
+
55
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
56
+ const blobUrl = URL.createObjectURL(blob);
57
+ const worker = new Worker(blobUrl);
58
+
59
+ const cleanup = () => { worker.terminate(); URL.revokeObjectURL(blobUrl); };
60
+
61
+ const timeout = setTimeout(() => {
62
+ cleanup();
63
+ resolve(computeInline(challenge, difficulty));
64
+ }, 30000);
65
+
66
+ worker.onmessage = (e) => {
67
+ clearTimeout(timeout);
68
+ cleanup();
69
+ if (e.data.error) {
70
+ resolve(computeInline(challenge, difficulty));
71
+ } else {
72
+ resolve({
73
+ challenge,
74
+ nonce: e.data.nonce,
75
+ difficulty,
76
+ computationTimeMs: e.data.computationTimeMs,
77
+ iterations: e.data.iterations,
78
+ });
79
+ }
80
+ };
81
+
82
+ worker.onerror = () => {
83
+ clearTimeout(timeout);
84
+ cleanup();
85
+ resolve(computeInline(challenge, difficulty));
86
+ };
87
+
88
+ worker.postMessage({ challenge, difficulty });
89
+ });
90
+ }
91
+
92
+ async function computeInline(challenge: string, difficulty: number): Promise<PowResult> {
93
+ const prefix = '0'.repeat(difficulty);
94
+ const start = performance.now();
95
+ let nonce = 0;
96
+ const encoder = new TextEncoder();
97
+
98
+ while (nonce < 10000000) {
99
+ const data = encoder.encode(challenge + nonce.toString());
100
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
101
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
102
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
103
+
104
+ if (hashHex.startsWith(prefix)) {
105
+ return {
106
+ challenge,
107
+ nonce: nonce.toString(),
108
+ difficulty,
109
+ computationTimeMs: performance.now() - start,
110
+ iterations: nonce + 1,
111
+ };
112
+ }
113
+ nonce++;
114
+ if (nonce % 10000 === 0) {
115
+ await new Promise(r => setTimeout(r, 0));
116
+ }
117
+ }
118
+
119
+ return { challenge, nonce: '', difficulty, computationTimeMs: performance.now() - start, iterations: nonce };
120
+ }