devn-bot-detector-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1007 @@
1
+ import { DEFAULT_SESSION_DURATION_MS, SITE_KEY_HEADER, API_PATHS, TELEMETRY_VERSION, utf8ByteLength, MAX_TELEMETRY_BYTES, DEFAULT_TELEMETRY_INTERVAL_MS } from 'devn-bot-detector-core';
2
+
3
+ // src/BotDetector.ts
4
+
5
+ // src/utils.ts
6
+ function isBrowser() {
7
+ return typeof window !== "undefined" && typeof document !== "undefined";
8
+ }
9
+ function nowMs() {
10
+ return Date.now();
11
+ }
12
+ function perfNow() {
13
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
14
+ }
15
+ function secureId(prefix, bytes = 16) {
16
+ const arr = new Uint8Array(bytes);
17
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
18
+ crypto.getRandomValues(arr);
19
+ } else {
20
+ throw new Error("Secure random generation is unavailable");
21
+ }
22
+ let hex = "";
23
+ for (let i = 0; i < arr.length; i++) {
24
+ hex += arr[i].toString(16).padStart(2, "0");
25
+ }
26
+ return `${prefix}_${hex}`;
27
+ }
28
+ function debugLog(enabled, ...args) {
29
+ if (enabled && typeof console !== "undefined" && typeof console.debug === "function") {
30
+ console.debug("[bot-detector-client]", ...args);
31
+ }
32
+ }
33
+ var IntervalTracker = class {
34
+ constructor() {
35
+ this.lastAt = null;
36
+ this.sum = 0;
37
+ this.count = 0;
38
+ this.min = null;
39
+ this.max = null;
40
+ this.sumSq = 0;
41
+ }
42
+ mark(t) {
43
+ if (this.lastAt != null) {
44
+ const interval = t - this.lastAt;
45
+ if (interval >= 0) {
46
+ this.sum += interval;
47
+ this.sumSq += interval * interval;
48
+ this.count += 1;
49
+ this.min = this.min == null ? interval : Math.min(this.min, interval);
50
+ this.max = this.max == null ? interval : Math.max(this.max, interval);
51
+ }
52
+ }
53
+ this.lastAt = t;
54
+ }
55
+ average() {
56
+ return this.count === 0 ? null : this.sum / this.count;
57
+ }
58
+ minInterval() {
59
+ return this.min;
60
+ }
61
+ maxInterval() {
62
+ return this.max;
63
+ }
64
+ variance() {
65
+ if (this.count < 2) return null;
66
+ const mean = this.sum / this.count;
67
+ return this.sumSq / this.count - mean * mean;
68
+ }
69
+ reset() {
70
+ this.lastAt = null;
71
+ this.sum = 0;
72
+ this.count = 0;
73
+ this.min = null;
74
+ this.max = null;
75
+ this.sumSq = 0;
76
+ }
77
+ };
78
+
79
+ // src/collectors/browser.ts
80
+ function getWebglInfo() {
81
+ try {
82
+ if (typeof document === "undefined") return { renderer: null, vendor: null };
83
+ if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.includes("jsdom")) {
84
+ return { renderer: null, vendor: null };
85
+ }
86
+ const canvas = document.createElement("canvas");
87
+ if (typeof canvas.getContext !== "function") return { renderer: null, vendor: null };
88
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
89
+ if (!gl) return { renderer: null, vendor: null };
90
+ const ext = gl.getExtension("WEBGL_debug_renderer_info");
91
+ if (!ext) return { renderer: null, vendor: null };
92
+ const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL);
93
+ const vendor = gl.getParameter(ext.UNMASKED_VENDOR_WEBGL);
94
+ return {
95
+ renderer: typeof renderer === "string" ? renderer.slice(0, 128) : null,
96
+ vendor: typeof vendor === "string" ? vendor.slice(0, 128) : null
97
+ };
98
+ } catch {
99
+ return { renderer: null, vendor: null };
100
+ }
101
+ }
102
+ function collectBrowserSignals() {
103
+ if (!isBrowser()) {
104
+ return {
105
+ userAgent: "",
106
+ language: "",
107
+ languagesLength: 0,
108
+ timezone: "UTC",
109
+ timezoneOffset: 0,
110
+ platform: "",
111
+ screenWidth: 0,
112
+ screenHeight: 0,
113
+ colorDepth: 0,
114
+ devicePixelRatio: 1,
115
+ hardwareConcurrency: null,
116
+ maxTouchPoints: 0,
117
+ cookieEnabled: false,
118
+ webdriver: false
119
+ };
120
+ }
121
+ const nav = navigator;
122
+ let timezone = "UTC";
123
+ try {
124
+ timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
125
+ } catch {
126
+ }
127
+ const webgl = getWebglInfo();
128
+ const hasChrome = Boolean(window.chrome);
129
+ const pluginsCount = typeof nav.plugins !== "undefined" ? nav.plugins.length : 0;
130
+ const screenOrientation = typeof screen !== "undefined" && screen.orientation && screen.orientation.type ? screen.orientation.type : null;
131
+ return {
132
+ userAgent: (nav.userAgent || "").slice(0, 512),
133
+ language: (nav.language || "").slice(0, 32),
134
+ languagesLength: Array.isArray(nav.languages) ? nav.languages.length : 0,
135
+ timezone: timezone.slice(0, 64),
136
+ timezoneOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
137
+ platform: (nav.platform || "").slice(0, 64),
138
+ screenWidth: typeof screen !== "undefined" ? screen.width || 0 : 0,
139
+ screenHeight: typeof screen !== "undefined" ? screen.height || 0 : 0,
140
+ colorDepth: typeof screen !== "undefined" ? screen.colorDepth || 0 : 0,
141
+ devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
142
+ hardwareConcurrency: typeof nav.hardwareConcurrency === "number" ? nav.hardwareConcurrency : null,
143
+ maxTouchPoints: nav.maxTouchPoints || 0,
144
+ cookieEnabled: Boolean(nav.cookieEnabled),
145
+ webdriver: Boolean(nav.webdriver),
146
+ webglRenderer: webgl.renderer,
147
+ webglVendor: webgl.vendor,
148
+ hasChrome,
149
+ pluginsCount,
150
+ screenOrientation
151
+ };
152
+ }
153
+
154
+ // src/collectors/pointer.ts
155
+ var IDLE_GAP_MS = 1500;
156
+ var PointerCollector = class {
157
+ constructor() {
158
+ this.listening = false;
159
+ this.eventCount = 0;
160
+ this.movementDistance = 0;
161
+ this.directionChanges = 0;
162
+ this.idlePeriods = 0;
163
+ this.clickCount = 0;
164
+ this.activeDuration = 0;
165
+ this.lastX = null;
166
+ this.lastY = null;
167
+ this.lastAngle = null;
168
+ this.lastEventAt = null;
169
+ this.activityStartedAt = null;
170
+ this.intervals = new IntervalTracker();
171
+ this.onMove = (e) => {
172
+ const t = perfNow();
173
+ const x = e.clientX;
174
+ const y = e.clientY;
175
+ this.eventCount += 1;
176
+ this.intervals.mark(t);
177
+ if (this.lastEventAt != null && t - this.lastEventAt > IDLE_GAP_MS) {
178
+ this.idlePeriods += 1;
179
+ if (this.activityStartedAt != null) {
180
+ this.activeDuration += this.lastEventAt - this.activityStartedAt;
181
+ }
182
+ this.activityStartedAt = t;
183
+ } else if (this.activityStartedAt == null) {
184
+ this.activityStartedAt = t;
185
+ }
186
+ if (this.lastX != null && this.lastY != null) {
187
+ const dx = x - this.lastX;
188
+ const dy = y - this.lastY;
189
+ const dist = Math.hypot(dx, dy);
190
+ this.movementDistance += dist;
191
+ if (dist > 2) {
192
+ const angle = Math.atan2(dy, dx);
193
+ if (this.lastAngle != null) {
194
+ let delta = Math.abs(angle - this.lastAngle);
195
+ if (delta > Math.PI) delta = 2 * Math.PI - delta;
196
+ if (delta > 0.6) this.directionChanges += 1;
197
+ }
198
+ this.lastAngle = angle;
199
+ }
200
+ }
201
+ this.lastX = x;
202
+ this.lastY = y;
203
+ this.lastEventAt = t;
204
+ };
205
+ this.onClick = () => {
206
+ this.clickCount += 1;
207
+ this.eventCount += 1;
208
+ };
209
+ }
210
+ start() {
211
+ if (this.listening || !isBrowser()) return;
212
+ this.listening = true;
213
+ window.addEventListener("pointermove", this.onMove, { passive: true });
214
+ window.addEventListener("click", this.onClick, { passive: true });
215
+ }
216
+ stop() {
217
+ if (!this.listening || !isBrowser()) return;
218
+ this.listening = false;
219
+ window.removeEventListener("pointermove", this.onMove);
220
+ window.removeEventListener("click", this.onClick);
221
+ this.flushActive();
222
+ }
223
+ snapshot() {
224
+ this.flushActive();
225
+ return {
226
+ eventCount: this.eventCount,
227
+ movementDistance: Math.round(this.movementDistance),
228
+ averageInterval: this.intervals.average(),
229
+ minInterval: this.intervals.minInterval(),
230
+ maxInterval: this.intervals.maxInterval(),
231
+ directionChanges: this.directionChanges,
232
+ idlePeriods: this.idlePeriods,
233
+ activeDuration: Math.round(this.activeDuration),
234
+ clickCount: this.clickCount
235
+ };
236
+ }
237
+ reset() {
238
+ this.eventCount = 0;
239
+ this.movementDistance = 0;
240
+ this.directionChanges = 0;
241
+ this.idlePeriods = 0;
242
+ this.clickCount = 0;
243
+ this.activeDuration = 0;
244
+ this.lastX = null;
245
+ this.lastY = null;
246
+ this.lastAngle = null;
247
+ this.lastEventAt = null;
248
+ this.activityStartedAt = null;
249
+ this.intervals.reset();
250
+ }
251
+ flushActive() {
252
+ if (this.activityStartedAt != null && this.lastEventAt != null) {
253
+ this.activeDuration += this.lastEventAt - this.activityStartedAt;
254
+ this.activityStartedAt = this.lastEventAt;
255
+ }
256
+ }
257
+ };
258
+
259
+ // src/collectors/keyboard.ts
260
+ var KeyboardCollector = class {
261
+ constructor() {
262
+ this.listening = false;
263
+ this.keydownCount = 0;
264
+ this.keyupCount = 0;
265
+ this.pasteCount = 0;
266
+ this.intervals = new IntervalTracker();
267
+ this.onKeyDown = () => {
268
+ this.keydownCount += 1;
269
+ this.intervals.mark(perfNow());
270
+ };
271
+ this.onKeyUp = () => {
272
+ this.keyupCount += 1;
273
+ };
274
+ this.onPaste = () => {
275
+ this.pasteCount += 1;
276
+ };
277
+ }
278
+ start() {
279
+ if (this.listening || !isBrowser()) return;
280
+ this.listening = true;
281
+ window.addEventListener("keydown", this.onKeyDown, { passive: true });
282
+ window.addEventListener("keyup", this.onKeyUp, { passive: true });
283
+ window.addEventListener("paste", this.onPaste, { passive: true });
284
+ }
285
+ stop() {
286
+ if (!this.listening || !isBrowser()) return;
287
+ this.listening = false;
288
+ window.removeEventListener("keydown", this.onKeyDown);
289
+ window.removeEventListener("keyup", this.onKeyUp);
290
+ window.removeEventListener("paste", this.onPaste);
291
+ }
292
+ snapshot() {
293
+ return {
294
+ keydownCount: this.keydownCount,
295
+ keyupCount: this.keyupCount,
296
+ averageInterval: this.intervals.average(),
297
+ minInterval: this.intervals.minInterval(),
298
+ maxInterval: this.intervals.maxInterval(),
299
+ timingVariance: this.intervals.variance(),
300
+ pasteCount: this.pasteCount
301
+ };
302
+ }
303
+ reset() {
304
+ this.keydownCount = 0;
305
+ this.keyupCount = 0;
306
+ this.pasteCount = 0;
307
+ this.intervals.reset();
308
+ }
309
+ };
310
+
311
+ // src/collectors/touch.ts
312
+ var TouchCollector = class {
313
+ constructor() {
314
+ this.listening = false;
315
+ this.touchCount = 0;
316
+ this.multiTouchCount = 0;
317
+ this.activeDuration = 0;
318
+ this.touchStartedAt = null;
319
+ this.intervals = new IntervalTracker();
320
+ this.onStart = (e) => {
321
+ const t = perfNow();
322
+ this.touchCount += 1;
323
+ this.intervals.mark(t);
324
+ this.touchStartedAt = t;
325
+ const touches = e.touches?.length ?? 0;
326
+ if (touches > 1) this.multiTouchCount += 1;
327
+ };
328
+ this.onEnd = () => {
329
+ if (this.touchStartedAt != null) {
330
+ this.activeDuration += perfNow() - this.touchStartedAt;
331
+ this.touchStartedAt = null;
332
+ }
333
+ };
334
+ }
335
+ start() {
336
+ if (this.listening || !isBrowser()) return;
337
+ this.listening = true;
338
+ window.addEventListener("touchstart", this.onStart, { passive: true });
339
+ window.addEventListener("touchend", this.onEnd, { passive: true });
340
+ window.addEventListener("touchcancel", this.onEnd, { passive: true });
341
+ }
342
+ stop() {
343
+ if (!this.listening || !isBrowser()) return;
344
+ this.listening = false;
345
+ window.removeEventListener("touchstart", this.onStart);
346
+ window.removeEventListener("touchend", this.onEnd);
347
+ window.removeEventListener("touchcancel", this.onEnd);
348
+ }
349
+ snapshot() {
350
+ return {
351
+ touchCount: this.touchCount,
352
+ averageInterval: this.intervals.average(),
353
+ activeDuration: Math.round(this.activeDuration),
354
+ multiTouchCount: this.multiTouchCount
355
+ };
356
+ }
357
+ reset() {
358
+ this.touchCount = 0;
359
+ this.multiTouchCount = 0;
360
+ this.activeDuration = 0;
361
+ this.touchStartedAt = null;
362
+ this.intervals.reset();
363
+ }
364
+ };
365
+
366
+ // src/collectors/scroll.ts
367
+ var ScrollCollector = class {
368
+ constructor() {
369
+ this.listening = false;
370
+ this.scrollCount = 0;
371
+ this.totalDistance = 0;
372
+ this.directionChanges = 0;
373
+ this.activeDuration = 0;
374
+ this.lastY = null;
375
+ this.lastDir = null;
376
+ this.lastAt = null;
377
+ this.activityStartedAt = null;
378
+ this.intervals = new IntervalTracker();
379
+ this.scheduled = false;
380
+ this.onScroll = () => {
381
+ if (this.scheduled) return;
382
+ this.scheduled = true;
383
+ const run = () => {
384
+ this.scheduled = false;
385
+ this.handleScroll();
386
+ };
387
+ if (typeof requestAnimationFrame === "function") {
388
+ requestAnimationFrame(run);
389
+ } else {
390
+ setTimeout(run, 16);
391
+ }
392
+ };
393
+ }
394
+ handleScroll() {
395
+ const t = perfNow();
396
+ const y = typeof window !== "undefined" ? window.scrollY || 0 : 0;
397
+ this.scrollCount += 1;
398
+ this.intervals.mark(t);
399
+ if (this.lastY != null) {
400
+ const dy = y - this.lastY;
401
+ this.totalDistance += Math.abs(dy);
402
+ const dir = dy === 0 ? 0 : dy > 0 ? 1 : -1;
403
+ if (dir !== 0 && this.lastDir != null && dir !== this.lastDir) {
404
+ this.directionChanges += 1;
405
+ }
406
+ if (dir !== 0) this.lastDir = dir;
407
+ }
408
+ if (this.activityStartedAt == null) this.activityStartedAt = t;
409
+ if (this.lastAt != null && t - this.lastAt > 800) {
410
+ this.activeDuration += this.lastAt - this.activityStartedAt;
411
+ this.activityStartedAt = t;
412
+ }
413
+ this.lastY = y;
414
+ this.lastAt = t;
415
+ }
416
+ start() {
417
+ if (this.listening || !isBrowser()) return;
418
+ this.listening = true;
419
+ window.addEventListener("scroll", this.onScroll, { passive: true });
420
+ }
421
+ stop() {
422
+ if (!this.listening || !isBrowser()) return;
423
+ this.listening = false;
424
+ window.removeEventListener("scroll", this.onScroll);
425
+ if (this.activityStartedAt != null && this.lastAt != null) {
426
+ this.activeDuration += this.lastAt - this.activityStartedAt;
427
+ this.activityStartedAt = this.lastAt;
428
+ }
429
+ }
430
+ snapshot() {
431
+ return {
432
+ scrollCount: this.scrollCount,
433
+ averageInterval: this.intervals.average(),
434
+ totalDistance: Math.round(this.totalDistance),
435
+ directionChanges: this.directionChanges,
436
+ activeDuration: Math.round(this.activeDuration)
437
+ };
438
+ }
439
+ reset() {
440
+ this.scrollCount = 0;
441
+ this.totalDistance = 0;
442
+ this.directionChanges = 0;
443
+ this.activeDuration = 0;
444
+ this.lastY = null;
445
+ this.lastDir = null;
446
+ this.lastAt = null;
447
+ this.activityStartedAt = null;
448
+ this.intervals.reset();
449
+ }
450
+ };
451
+
452
+ // src/collectors/focus.ts
453
+ var FocusCollector = class {
454
+ constructor() {
455
+ this.listening = false;
456
+ this.focusCount = 0;
457
+ this.blurCount = 0;
458
+ this.onFocus = () => {
459
+ this.focusCount += 1;
460
+ };
461
+ this.onBlur = () => {
462
+ this.blurCount += 1;
463
+ };
464
+ }
465
+ start() {
466
+ if (this.listening || !isBrowser()) return;
467
+ this.listening = true;
468
+ window.addEventListener("focus", this.onFocus);
469
+ window.addEventListener("blur", this.onBlur);
470
+ }
471
+ stop() {
472
+ if (!this.listening || !isBrowser()) return;
473
+ this.listening = false;
474
+ window.removeEventListener("focus", this.onFocus);
475
+ window.removeEventListener("blur", this.onBlur);
476
+ }
477
+ snapshot() {
478
+ return {
479
+ focusCount: this.focusCount,
480
+ blurCount: this.blurCount
481
+ };
482
+ }
483
+ reset() {
484
+ this.focusCount = 0;
485
+ this.blurCount = 0;
486
+ }
487
+ };
488
+
489
+ // src/collectors/visibility.ts
490
+ var VisibilityCollector = class {
491
+ constructor() {
492
+ this.listening = false;
493
+ this.visibilityChanges = 0;
494
+ this.visibleDuration = 0;
495
+ this.hiddenDuration = 0;
496
+ this.segmentStartedAt = nowMs();
497
+ this.currentlyHidden = false;
498
+ this.onChange = () => {
499
+ const t = nowMs();
500
+ const elapsed = t - this.segmentStartedAt;
501
+ if (this.currentlyHidden) this.hiddenDuration += elapsed;
502
+ else this.visibleDuration += elapsed;
503
+ this.currentlyHidden = document.visibilityState === "hidden";
504
+ this.segmentStartedAt = t;
505
+ this.visibilityChanges += 1;
506
+ };
507
+ }
508
+ start() {
509
+ if (this.listening || !isBrowser()) return;
510
+ this.listening = true;
511
+ this.currentlyHidden = document.visibilityState === "hidden";
512
+ this.segmentStartedAt = nowMs();
513
+ document.addEventListener("visibilitychange", this.onChange);
514
+ }
515
+ stop() {
516
+ if (!this.listening || !isBrowser()) return;
517
+ this.listening = false;
518
+ document.removeEventListener("visibilitychange", this.onChange);
519
+ this.flush();
520
+ }
521
+ snapshot() {
522
+ this.flush();
523
+ return {
524
+ visibilityChanges: this.visibilityChanges,
525
+ visibleDuration: Math.round(this.visibleDuration),
526
+ hiddenDuration: Math.round(this.hiddenDuration)
527
+ };
528
+ }
529
+ reset() {
530
+ this.visibilityChanges = 0;
531
+ this.visibleDuration = 0;
532
+ this.hiddenDuration = 0;
533
+ this.segmentStartedAt = nowMs();
534
+ this.currentlyHidden = isBrowser() ? document.visibilityState === "hidden" : false;
535
+ }
536
+ flush() {
537
+ const t = nowMs();
538
+ const elapsed = t - this.segmentStartedAt;
539
+ if (this.currentlyHidden) this.hiddenDuration += elapsed;
540
+ else this.visibleDuration += elapsed;
541
+ this.segmentStartedAt = t;
542
+ }
543
+ };
544
+
545
+ // src/collectors/interaction.ts
546
+ var InteractionCollector = class {
547
+ constructor() {
548
+ this.listening = false;
549
+ this.pageLoadTime = nowMs();
550
+ this.firstInteractionTime = null;
551
+ this.clickCount = 0;
552
+ this.interactionCount = 0;
553
+ this.formInteractionCount = 0;
554
+ this.scrollCount = 0;
555
+ this.focusCount = 0;
556
+ this.markInteraction = () => {
557
+ if (this.firstInteractionTime == null) {
558
+ this.firstInteractionTime = nowMs();
559
+ }
560
+ this.interactionCount += 1;
561
+ };
562
+ this.onClick = () => {
563
+ this.clickCount += 1;
564
+ this.markInteraction();
565
+ };
566
+ this.onKey = () => {
567
+ this.markInteraction();
568
+ };
569
+ this.onTouch = () => {
570
+ this.markInteraction();
571
+ };
572
+ this.onScroll = () => {
573
+ this.scrollCount += 1;
574
+ this.markInteraction();
575
+ };
576
+ this.onFocusIn = (e) => {
577
+ this.focusCount += 1;
578
+ const target = e.target;
579
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) {
580
+ this.formInteractionCount += 1;
581
+ }
582
+ };
583
+ }
584
+ start() {
585
+ if (this.listening || !isBrowser()) return;
586
+ this.listening = true;
587
+ this.pageLoadTime = nowMs();
588
+ window.addEventListener("click", this.onClick, { passive: true });
589
+ window.addEventListener("keydown", this.onKey, { passive: true });
590
+ window.addEventListener("touchstart", this.onTouch, { passive: true });
591
+ window.addEventListener("scroll", this.onScroll, { passive: true });
592
+ document.addEventListener("focusin", this.onFocusIn, { passive: true });
593
+ }
594
+ stop() {
595
+ if (!this.listening || !isBrowser()) return;
596
+ this.listening = false;
597
+ window.removeEventListener("click", this.onClick);
598
+ window.removeEventListener("keydown", this.onKey);
599
+ window.removeEventListener("touchstart", this.onTouch);
600
+ window.removeEventListener("scroll", this.onScroll);
601
+ document.removeEventListener("focusin", this.onFocusIn);
602
+ }
603
+ snapshot() {
604
+ return {
605
+ pageLoadTime: this.pageLoadTime,
606
+ firstInteractionTime: this.firstInteractionTime,
607
+ timeToFirstInteraction: this.firstInteractionTime != null ? this.firstInteractionTime - this.pageLoadTime : null,
608
+ clickCount: this.clickCount,
609
+ interactionCount: this.interactionCount,
610
+ formInteractionCount: this.formInteractionCount,
611
+ scrollCount: this.scrollCount,
612
+ focusCount: this.focusCount
613
+ };
614
+ }
615
+ reset() {
616
+ this.pageLoadTime = nowMs();
617
+ this.firstInteractionTime = null;
618
+ this.clickCount = 0;
619
+ this.interactionCount = 0;
620
+ this.formInteractionCount = 0;
621
+ this.scrollCount = 0;
622
+ this.focusCount = 0;
623
+ }
624
+ };
625
+ function createLocalSession(sessionDurationMs = DEFAULT_SESSION_DURATION_MS) {
626
+ const createdAt = nowMs();
627
+ return {
628
+ sessionId: secureId("ses"),
629
+ nonce: secureId("nonce", 12),
630
+ createdAt,
631
+ expiresAt: createdAt + sessionDurationMs
632
+ };
633
+ }
634
+ function isSessionExpired(session, now = nowMs()) {
635
+ return now >= session.expiresAt;
636
+ }
637
+ function rotateNonce(session) {
638
+ return {
639
+ ...session,
640
+ nonce: secureId("nonce", 12)
641
+ };
642
+ }
643
+ function wait(ms) {
644
+ return new Promise((resolve) => setTimeout(resolve, ms));
645
+ }
646
+ var HttpTransport = class {
647
+ constructor(options) {
648
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
649
+ this.siteKey = options.siteKey;
650
+ this.timeoutMs = options.timeoutMs ?? 1e4;
651
+ this.headersProvider = options.headers;
652
+ this.fetchFn = options.fetchFn ?? ((...args) => fetch(...args));
653
+ this.credentials = options.credentials ?? "omit";
654
+ this.retries = Math.max(0, options.retries ?? 0);
655
+ this.retryDelayMs = options.retryDelayMs ?? 300;
656
+ }
657
+ async post(path, body) {
658
+ return this.executeWithRetry("POST", path, body);
659
+ }
660
+ async get(path) {
661
+ return this.executeWithRetry("GET", path);
662
+ }
663
+ async resolveHeaders() {
664
+ if (!this.headersProvider) return {};
665
+ if (typeof this.headersProvider === "function") {
666
+ return await this.headersProvider() ?? {};
667
+ }
668
+ return this.headersProvider;
669
+ }
670
+ async executeWithRetry(method, path, body) {
671
+ let lastError;
672
+ for (let attempt = 0; attempt <= this.retries; attempt++) {
673
+ try {
674
+ return await this.request(method, path, body);
675
+ } catch (err) {
676
+ lastError = err;
677
+ if (attempt < this.retries) {
678
+ const delay = this.retryDelayMs * Math.pow(2, attempt);
679
+ await wait(delay);
680
+ }
681
+ }
682
+ }
683
+ throw lastError;
684
+ }
685
+ async request(method, path, body) {
686
+ const controller = new AbortController();
687
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
688
+ try {
689
+ const customHeaders = await this.resolveHeaders();
690
+ const res = await this.fetchFn(`${this.baseUrl}${path}`, {
691
+ method,
692
+ headers: {
693
+ "Content-Type": "application/json",
694
+ [SITE_KEY_HEADER]: this.siteKey,
695
+ ...customHeaders
696
+ },
697
+ body: body === void 0 ? void 0 : JSON.stringify(body),
698
+ signal: controller.signal,
699
+ credentials: this.credentials
700
+ });
701
+ const text = await res.text();
702
+ let data = null;
703
+ if (text) {
704
+ try {
705
+ data = JSON.parse(text);
706
+ } catch {
707
+ throw new Error(`Invalid JSON response (${res.status})`);
708
+ }
709
+ }
710
+ if (!res.ok) {
711
+ const message = typeof data === "object" && data !== null && "error" in data && typeof data.error?.message === "string" ? data.error.message : `HTTP ${res.status}`;
712
+ throw new Error(message);
713
+ }
714
+ return data;
715
+ } finally {
716
+ clearTimeout(timer);
717
+ }
718
+ }
719
+ };
720
+
721
+ // src/BotDetector.ts
722
+ function resolveConfig(config) {
723
+ if (!config.siteKey) throw new Error("BotDetector requires siteKey");
724
+ if (!config.endpoint) throw new Error("BotDetector requires endpoint");
725
+ return {
726
+ siteKey: config.siteKey,
727
+ endpoint: config.endpoint.replace(/\/$/, ""),
728
+ autoStart: config.autoStart ?? true,
729
+ telemetryInterval: config.telemetryInterval ?? DEFAULT_TELEMETRY_INTERVAL_MS,
730
+ sessionDuration: config.sessionDuration ?? DEFAULT_SESSION_DURATION_MS,
731
+ collectPointer: config.collectPointer ?? true,
732
+ collectKeyboardTiming: config.collectKeyboardTiming ?? true,
733
+ collectTouch: config.collectTouch ?? true,
734
+ collectScroll: config.collectScroll ?? true,
735
+ collectFocus: config.collectFocus ?? true,
736
+ collectVisibility: config.collectVisibility ?? true,
737
+ collectBrowser: config.collectBrowser ?? true,
738
+ failOpen: config.failOpen ?? true,
739
+ maxTelemetryBytes: config.maxTelemetryBytes ?? MAX_TELEMETRY_BYTES,
740
+ debug: config.debug ?? false,
741
+ sampleRate: Math.max(0, Math.min(1, config.sampleRate ?? 1)),
742
+ headers: config.headers,
743
+ fetchFn: config.fetchFn,
744
+ credentials: config.credentials,
745
+ timeoutMs: config.timeoutMs,
746
+ retries: config.retries,
747
+ retryDelayMs: config.retryDelayMs,
748
+ onSessionCreated: config.onSessionCreated,
749
+ onCheck: config.onCheck,
750
+ onError: config.onError
751
+ };
752
+ }
753
+ var BotDetector = class {
754
+ constructor(config) {
755
+ this.pointer = new PointerCollector();
756
+ this.keyboard = new KeyboardCollector();
757
+ this.touch = new TouchCollector();
758
+ this.scroll = new ScrollCollector();
759
+ this.focus = new FocusCollector();
760
+ this.visibility = new VisibilityCollector();
761
+ this.interaction = new InteractionCollector();
762
+ this.session = null;
763
+ this.sequence = 0;
764
+ this.running = false;
765
+ this.paused = false;
766
+ this.destroyed = false;
767
+ this.flushTimer = null;
768
+ this.startPromise = null;
769
+ this.config = resolveConfig(config);
770
+ this.transport = new HttpTransport({
771
+ baseUrl: this.config.endpoint,
772
+ siteKey: this.config.siteKey,
773
+ headers: this.config.headers,
774
+ fetchFn: this.config.fetchFn,
775
+ credentials: this.config.credentials,
776
+ timeoutMs: this.config.timeoutMs,
777
+ retries: this.config.retries,
778
+ retryDelayMs: this.config.retryDelayMs
779
+ });
780
+ if (this.config.autoStart && isBrowser()) {
781
+ void this.start();
782
+ }
783
+ }
784
+ async start() {
785
+ if (this.destroyed) throw new Error("BotDetector has been destroyed");
786
+ if (this.running) return;
787
+ if (this.startPromise) return this.startPromise;
788
+ this.startPromise = this.doStart();
789
+ try {
790
+ await this.startPromise;
791
+ } finally {
792
+ this.startPromise = null;
793
+ }
794
+ }
795
+ async doStart() {
796
+ if (!isBrowser()) {
797
+ debugLog(this.config.debug, "start() outside browser \u2014 collectors skipped");
798
+ return;
799
+ }
800
+ this.attachCollectors();
801
+ this.running = true;
802
+ await this.ensureSession();
803
+ this.scheduleFlush();
804
+ debugLog(this.config.debug, "started", { sessionId: this.session?.sessionId });
805
+ }
806
+ /** Temporarily pause active collectors (e.g. background tab or navigation). */
807
+ pause() {
808
+ if (!this.running || this.paused || this.destroyed) return;
809
+ this.detachCollectors();
810
+ this.paused = true;
811
+ debugLog(this.config.debug, "paused");
812
+ }
813
+ /** Resume collecting signals after pausing. */
814
+ resume() {
815
+ if (!this.running || !this.paused || this.destroyed) return;
816
+ this.attachCollectors();
817
+ this.paused = false;
818
+ debugLog(this.config.debug, "resumed");
819
+ }
820
+ isPaused() {
821
+ return this.paused;
822
+ }
823
+ isRunning() {
824
+ return this.running && !this.destroyed;
825
+ }
826
+ /**
827
+ * Collect telemetry and analyze risk via the configured endpoint.
828
+ * On network failure with failOpen=true, returns a monitor decision.
829
+ */
830
+ async check(options) {
831
+ if (this.destroyed) throw new Error("BotDetector has been destroyed");
832
+ if (!this.running) await this.start();
833
+ if (this.config.sampleRate < 1 && Math.random() > this.config.sampleRate) {
834
+ debugLog(this.config.debug, "skipped check due to sampleRate");
835
+ const sampled = this.fallbackResult("allow", 95);
836
+ this.config.onCheck?.(sampled);
837
+ return sampled;
838
+ }
839
+ try {
840
+ await this.ensureSession();
841
+ const session = this.session;
842
+ if (!session) throw new Error("No session");
843
+ this.sequence += 1;
844
+ const telemetry = this.getTelemetry();
845
+ const body = {
846
+ sessionId: session.sessionId,
847
+ sequence: this.sequence,
848
+ nonce: session.nonce,
849
+ timestamp: nowMs(),
850
+ hostname: isBrowser() ? location.hostname : "",
851
+ telemetry
852
+ };
853
+ if (options?.action) body.action = options.action;
854
+ if (options?.metadata) body.metadata = options.metadata;
855
+ debugLog(this.config.debug, "analyze request", {
856
+ sessionId: session.sessionId,
857
+ sequence: this.sequence,
858
+ action: options?.action
859
+ });
860
+ const result = await this.transport.post(API_PATHS.analyze, body);
861
+ this.session = rotateNonce(session);
862
+ this.config.onCheck?.(result);
863
+ return result;
864
+ } catch (err) {
865
+ const errorObj = err instanceof Error ? err : new Error(String(err));
866
+ this.config.onError?.(errorObj);
867
+ debugLog(this.config.debug, "check failed", errorObj.message);
868
+ if (this.config.failOpen) {
869
+ const fallback = this.fallbackResult("monitor");
870
+ this.config.onCheck?.(fallback);
871
+ return fallback;
872
+ }
873
+ throw err;
874
+ }
875
+ }
876
+ /** Shorthand for check({ action, metadata }). */
877
+ async checkAction(action, metadata) {
878
+ return this.check({ action, metadata });
879
+ }
880
+ getSession() {
881
+ return this.session;
882
+ }
883
+ getTelemetry() {
884
+ const telemetry = {
885
+ version: TELEMETRY_VERSION,
886
+ ...this.config.collectBrowser ? { browser: collectBrowserSignals() } : {},
887
+ ...this.config.collectPointer ? { pointer: this.pointer.snapshot() } : {},
888
+ ...this.config.collectKeyboardTiming ? { keyboard: this.keyboard.snapshot() } : {},
889
+ ...this.config.collectTouch ? { touch: this.touch.snapshot() } : {},
890
+ ...this.config.collectScroll ? { scroll: this.scroll.snapshot() } : {},
891
+ ...this.config.collectFocus ? { focus: this.focus.snapshot() } : {},
892
+ ...this.config.collectVisibility ? { visibility: this.visibility.snapshot() } : {},
893
+ interaction: this.interaction.snapshot()
894
+ };
895
+ return this.enforceSize(telemetry);
896
+ }
897
+ reset() {
898
+ this.pointer.reset();
899
+ this.keyboard.reset();
900
+ this.touch.reset();
901
+ this.scroll.reset();
902
+ this.focus.reset();
903
+ this.visibility.reset();
904
+ this.interaction.reset();
905
+ this.sequence = 0;
906
+ }
907
+ /** Remove listeners, timers, and clear state. */
908
+ destroy() {
909
+ this.destroyed = true;
910
+ this.running = false;
911
+ this.clearFlush();
912
+ this.detachCollectors();
913
+ this.session = null;
914
+ this.reset();
915
+ debugLog(this.config.debug, "destroyed");
916
+ }
917
+ attachCollectors() {
918
+ if (this.config.collectPointer) this.pointer.start();
919
+ if (this.config.collectKeyboardTiming) this.keyboard.start();
920
+ if (this.config.collectTouch) this.touch.start();
921
+ if (this.config.collectScroll) this.scroll.start();
922
+ if (this.config.collectFocus) this.focus.start();
923
+ if (this.config.collectVisibility) this.visibility.start();
924
+ this.interaction.start();
925
+ }
926
+ detachCollectors() {
927
+ this.pointer.stop();
928
+ this.keyboard.stop();
929
+ this.touch.stop();
930
+ this.scroll.stop();
931
+ this.focus.stop();
932
+ this.visibility.stop();
933
+ this.interaction.stop();
934
+ }
935
+ async ensureSession() {
936
+ if (this.session && !isSessionExpired(this.session)) return;
937
+ try {
938
+ const response = await this.transport.post(API_PATHS.session, {
939
+ hostname: isBrowser() ? location.hostname : "",
940
+ userAgent: isBrowser() ? navigator.userAgent.slice(0, 512) : "",
941
+ timestamp: nowMs()
942
+ });
943
+ this.session = {
944
+ sessionId: response.sessionId,
945
+ nonce: response.nonce,
946
+ createdAt: nowMs(),
947
+ expiresAt: Date.parse(response.expiresAt) || nowMs() + this.config.sessionDuration
948
+ };
949
+ this.sequence = 0;
950
+ debugLog(this.config.debug, "session created", { sessionId: this.session.sessionId });
951
+ this.config.onSessionCreated?.(this.session);
952
+ } catch (err) {
953
+ debugLog(this.config.debug, "session API failed, using local session");
954
+ this.session = createLocalSession(this.config.sessionDuration);
955
+ this.sequence = 0;
956
+ this.config.onSessionCreated?.(this.session);
957
+ if (!this.config.failOpen) throw err;
958
+ }
959
+ }
960
+ scheduleFlush() {
961
+ this.clearFlush();
962
+ if (this.config.telemetryInterval <= 0) return;
963
+ this.flushTimer = setInterval(() => {
964
+ void this.getTelemetry();
965
+ }, this.config.telemetryInterval);
966
+ }
967
+ clearFlush() {
968
+ if (this.flushTimer != null) {
969
+ clearInterval(this.flushTimer);
970
+ this.flushTimer = null;
971
+ }
972
+ }
973
+ enforceSize(telemetry) {
974
+ const fits = (value) => utf8ByteLength(JSON.stringify(value)) <= this.config.maxTelemetryBytes;
975
+ if (fits(telemetry)) return telemetry;
976
+ const candidates = [
977
+ {
978
+ version: TELEMETRY_VERSION,
979
+ interaction: telemetry.interaction,
980
+ browser: telemetry.browser
981
+ },
982
+ {
983
+ version: TELEMETRY_VERSION,
984
+ interaction: telemetry.interaction
985
+ },
986
+ { version: TELEMETRY_VERSION }
987
+ ];
988
+ for (const candidate of candidates) {
989
+ if (fits(candidate)) return candidate;
990
+ }
991
+ return { version: TELEMETRY_VERSION };
992
+ }
993
+ fallbackResult(decision, score) {
994
+ return {
995
+ requestId: `req_fallback_${nowMs()}`,
996
+ score: score ?? (decision === "monitor" ? 55 : decision === "allow" ? 90 : 50),
997
+ confidence: 0.3,
998
+ decision,
999
+ riskLevel: decision === "allow" ? "low" : "medium",
1000
+ timestamp: nowMs()
1001
+ };
1002
+ }
1003
+ };
1004
+
1005
+ export { BotDetector, FocusCollector, HttpTransport, InteractionCollector, KeyboardCollector, PointerCollector, ScrollCollector, TouchCollector, VisibilityCollector, collectBrowserSignals, createLocalSession, isSessionExpired };
1006
+ //# sourceMappingURL=index.js.map
1007
+ //# sourceMappingURL=index.js.map