@axeptio/behavior-detection 1.1.0 → 1.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.
@@ -15,6 +15,9 @@ export declare class BehaviorDetector {
15
15
  private confidenceScore;
16
16
  private readonly CONFIDENCE_TARGET;
17
17
  private readonly CONFIDENCE_DECAY;
18
+ private scoreMemory;
19
+ private scoreFloor;
20
+ private lastEventTime;
18
21
  constructor(tickOptions?: TickOptions);
19
22
  /**
20
23
  * Add a detection strategy
@@ -46,6 +46,7 @@ module.exports = __toCommonJS(index_exports);
46
46
  // dist/esm/behavior-detector.js
47
47
  var BehaviorDetector = class {
48
48
  constructor(tickOptions) {
49
+ var _a, _b, _c, _d;
49
50
  this.strategies = /* @__PURE__ */ new Map();
50
51
  this.isTracking = false;
51
52
  this.isPausedByVisibility = false;
@@ -56,12 +57,21 @@ var BehaviorDetector = class {
56
57
  this.confidenceScore = 0;
57
58
  this.CONFIDENCE_TARGET = 1;
58
59
  this.CONFIDENCE_DECAY = 0.95;
60
+ this.scoreFloor = 1;
61
+ this.lastEventTime = 0;
59
62
  if (tickOptions === null || tickOptions === void 0 ? void 0 : tickOptions.interval) {
60
63
  this.tickIntervalMs = tickOptions.interval;
61
64
  }
62
65
  if ((tickOptions === null || tickOptions === void 0 ? void 0 : tickOptions.pauseOnHidden) !== void 0) {
63
66
  this.pauseOnHidden = tickOptions.pauseOnHidden;
64
67
  }
68
+ const mem = tickOptions === null || tickOptions === void 0 ? void 0 : tickOptions.scoreMemory;
69
+ this.scoreMemory = {
70
+ enabled: (_a = mem === null || mem === void 0 ? void 0 : mem.enabled) !== null && _a !== void 0 ? _a : true,
71
+ inactivityThreshold: (_b = mem === null || mem === void 0 ? void 0 : mem.inactivityThreshold) !== null && _b !== void 0 ? _b : 2e3,
72
+ maxRecovery: (_c = mem === null || mem === void 0 ? void 0 : mem.maxRecovery) !== null && _c !== void 0 ? _c : 0.1,
73
+ recoveryPerEvent: (_d = mem === null || mem === void 0 ? void 0 : mem.recoveryPerEvent) !== null && _d !== void 0 ? _d : 0.02
74
+ };
65
75
  }
66
76
  /**
67
77
  * Add a detection strategy
@@ -90,6 +100,10 @@ var BehaviorDetector = class {
90
100
  this.confidenceScore *= this.CONFIDENCE_DECAY;
91
101
  const contribution = event.weight * strategyWeight;
92
102
  this.confidenceScore = Math.min(this.CONFIDENCE_TARGET, this.confidenceScore + contribution);
103
+ this.lastEventTime = Date.now();
104
+ if (this.scoreMemory.enabled) {
105
+ this.scoreFloor = Math.min(1, this.scoreFloor + this.scoreMemory.recoveryPerEvent);
106
+ }
93
107
  }
94
108
  /**
95
109
  * Remove a detection strategy
@@ -170,6 +184,8 @@ var BehaviorDetector = class {
170
184
  */
171
185
  reset() {
172
186
  this.confidenceScore = 0;
187
+ this.scoreFloor = 1;
188
+ this.lastEventTime = 0;
173
189
  for (const [_, config] of this.strategies) {
174
190
  config.strategy.reset();
175
191
  }
@@ -179,8 +195,19 @@ var BehaviorDetector = class {
179
195
  */
180
196
  async score(options = {}) {
181
197
  const breakdown = this.calculateStrategyScore();
198
+ let effectiveScore = breakdown.overall;
199
+ if (this.scoreMemory.enabled && this.lastEventTime > 0) {
200
+ if (effectiveScore < this.scoreFloor) {
201
+ this.scoreFloor = effectiveScore;
202
+ }
203
+ const timeSinceEvent = Date.now() - this.lastEventTime;
204
+ if (timeSinceEvent > this.scoreMemory.inactivityThreshold) {
205
+ const cap = this.scoreFloor + this.scoreMemory.maxRecovery;
206
+ effectiveScore = Math.min(effectiveScore, cap);
207
+ }
208
+ }
182
209
  const result = {
183
- score: breakdown.overall
210
+ score: effectiveScore
184
211
  };
185
212
  if (options.breakdown) {
186
213
  result.breakdown = breakdown;
@@ -398,7 +425,7 @@ var MouseStrategy = class extends BaseStrategy {
398
425
  this.lastPosition = null;
399
426
  this.lastAngle = 0;
400
427
  this.cumulativeAngle = 0;
401
- this.rollingWindowMs = 5e3;
428
+ this.rollingWindowMs = 3e4;
402
429
  this.listener = null;
403
430
  this.leaveListener = null;
404
431
  this.enterListener = null;
@@ -681,7 +708,7 @@ var ScrollStrategy = class extends BaseStrategy {
681
708
  this.velocitySeries = [];
682
709
  this.lastScrollY = null;
683
710
  this.lastTimestamp = 0;
684
- this.rollingWindowMs = 5e3;
711
+ this.rollingWindowMs = 3e4;
685
712
  this.documentHeight = 1;
686
713
  this.listener = null;
687
714
  this.isActive = false;
@@ -1431,6 +1458,33 @@ var AUTOMATION_GLOBALS = [
1431
1458
  "__fxdriver_unwrapped",
1432
1459
  "__selenium_unwrapped"
1433
1460
  ];
1461
+ function getWebGLRenderer(gl) {
1462
+ const renderer = gl.getParameter(gl.RENDERER);
1463
+ if (renderer && !/^(WebKit WebGL|Mozilla|Google Inc\.)$/i.test(renderer)) {
1464
+ return renderer;
1465
+ }
1466
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
1467
+ if (debugInfo) {
1468
+ return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
1469
+ }
1470
+ return renderer !== null && renderer !== void 0 ? renderer : void 0;
1471
+ }
1472
+ function withWebGLContext(fn) {
1473
+ try {
1474
+ const canvas = document.createElement("canvas");
1475
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
1476
+ if (!gl)
1477
+ return { hasWebGL: false, result: void 0 };
1478
+ try {
1479
+ return { hasWebGL: true, result: fn(gl) };
1480
+ } finally {
1481
+ const loseCtx = gl.getExtension("WEBGL_lose_context");
1482
+ loseCtx === null || loseCtx === void 0 ? void 0 : loseCtx.loseContext();
1483
+ }
1484
+ } catch (_a) {
1485
+ return { hasWebGL: false, result: void 0 };
1486
+ }
1487
+ }
1434
1488
  var EnvironmentStrategy = class extends BaseStrategy {
1435
1489
  constructor() {
1436
1490
  super(...arguments);
@@ -1447,7 +1501,9 @@ var EnvironmentStrategy = class extends BaseStrategy {
1447
1501
  this.data = null;
1448
1502
  }
1449
1503
  onTick(_timestamp) {
1450
- this.captureEnvironment();
1504
+ if (!this.data) {
1505
+ this.captureEnvironment();
1506
+ }
1451
1507
  }
1452
1508
  score() {
1453
1509
  if (!this.data)
@@ -1545,18 +1601,11 @@ var EnvironmentStrategy = class extends BaseStrategy {
1545
1601
  const hasCDPInjection = detectedCDPKeys.length > 0;
1546
1602
  const hasNoPlugins = !isMobile && navigator.plugins.length === 0;
1547
1603
  let hasSoftwareRenderer = false;
1548
- try {
1549
- const canvas = document.createElement("canvas");
1550
- const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
1551
- if (gl) {
1552
- const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
1553
- if (debugInfo) {
1554
- const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
1555
- hasSoftwareRenderer = /SwiftShader|Software|LLVMpipe/i.test(renderer);
1556
- this._webglRenderer = renderer;
1557
- }
1558
- }
1559
- } catch (_b) {
1604
+ const webglResult = withWebGLContext((gl) => getWebGLRenderer(gl));
1605
+ if (webglResult.hasWebGL && webglResult.result) {
1606
+ const renderer = webglResult.result;
1607
+ hasSoftwareRenderer = /SwiftShader|Software|LLVMpipe/i.test(renderer);
1608
+ this._webglRenderer = renderer;
1560
1609
  }
1561
1610
  return {
1562
1611
  isWebdriver,
@@ -1572,21 +1621,9 @@ var EnvironmentStrategy = class extends BaseStrategy {
1572
1621
  }
1573
1622
  captureEnvironment() {
1574
1623
  try {
1575
- let hasWebGL = false;
1576
- let webglRenderer;
1577
- try {
1578
- const canvas = document.createElement("canvas");
1579
- const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
1580
- hasWebGL = !!gl;
1581
- if (gl) {
1582
- const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
1583
- if (debugInfo) {
1584
- webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
1585
- }
1586
- }
1587
- } catch (e) {
1588
- hasWebGL = false;
1589
- }
1624
+ const webgl = withWebGLContext((gl) => getWebGLRenderer(gl));
1625
+ const hasWebGL = webgl.hasWebGL;
1626
+ const webglRenderer = webgl.result;
1590
1627
  const hasWebRTC = !!(window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection);
1591
1628
  const isMobile = this.isMobileDevice();
1592
1629
  const windowWidth = window.innerWidth;
@@ -15,6 +15,9 @@ export declare class BehaviorDetector {
15
15
  private confidenceScore;
16
16
  private readonly CONFIDENCE_TARGET;
17
17
  private readonly CONFIDENCE_DECAY;
18
+ private scoreMemory;
19
+ private scoreFloor;
20
+ private lastEventTime;
18
21
  constructor(tickOptions?: TickOptions);
19
22
  /**
20
23
  * Add a detection strategy
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export class BehaviorDetector {
6
6
  constructor(tickOptions) {
7
+ var _a, _b, _c, _d;
7
8
  // Strategy mode
8
9
  this.strategies = new Map();
9
10
  this.isTracking = false;
@@ -16,12 +17,22 @@ export class BehaviorDetector {
16
17
  this.confidenceScore = 0;
17
18
  this.CONFIDENCE_TARGET = 1.0; // Target confidence for reliable classification
18
19
  this.CONFIDENCE_DECAY = 0.95; // Per event decay to prevent infinite growth
20
+ this.scoreFloor = 1.0;
21
+ this.lastEventTime = 0;
19
22
  if (tickOptions === null || tickOptions === void 0 ? void 0 : tickOptions.interval) {
20
23
  this.tickIntervalMs = tickOptions.interval;
21
24
  }
22
25
  if ((tickOptions === null || tickOptions === void 0 ? void 0 : tickOptions.pauseOnHidden) !== undefined) {
23
26
  this.pauseOnHidden = tickOptions.pauseOnHidden;
24
27
  }
28
+ // Score memory defaults
29
+ const mem = tickOptions === null || tickOptions === void 0 ? void 0 : tickOptions.scoreMemory;
30
+ this.scoreMemory = {
31
+ enabled: (_a = mem === null || mem === void 0 ? void 0 : mem.enabled) !== null && _a !== void 0 ? _a : true,
32
+ inactivityThreshold: (_b = mem === null || mem === void 0 ? void 0 : mem.inactivityThreshold) !== null && _b !== void 0 ? _b : 2000,
33
+ maxRecovery: (_c = mem === null || mem === void 0 ? void 0 : mem.maxRecovery) !== null && _c !== void 0 ? _c : 0.1,
34
+ recoveryPerEvent: (_d = mem === null || mem === void 0 ? void 0 : mem.recoveryPerEvent) !== null && _d !== void 0 ? _d : 0.02,
35
+ };
25
36
  }
26
37
  /**
27
38
  * Add a detection strategy
@@ -55,6 +66,11 @@ export class BehaviorDetector {
55
66
  // eventWeight (0-1) * strategyWeight (e.g. 0.3 for mouse) = contribution
56
67
  const contribution = event.weight * strategyWeight;
57
68
  this.confidenceScore = Math.min(this.CONFIDENCE_TARGET, this.confidenceScore + contribution);
69
+ // Score memory: track activity and slowly raise the floor
70
+ this.lastEventTime = Date.now();
71
+ if (this.scoreMemory.enabled) {
72
+ this.scoreFloor = Math.min(1.0, this.scoreFloor + this.scoreMemory.recoveryPerEvent);
73
+ }
58
74
  }
59
75
  /**
60
76
  * Remove a detection strategy
@@ -144,6 +160,8 @@ export class BehaviorDetector {
144
160
  */
145
161
  reset() {
146
162
  this.confidenceScore = 0;
163
+ this.scoreFloor = 1.0;
164
+ this.lastEventTime = 0;
147
165
  for (const [_, config] of this.strategies) {
148
166
  config.strategy.reset();
149
167
  }
@@ -153,8 +171,22 @@ export class BehaviorDetector {
153
171
  */
154
172
  async score(options = {}) {
155
173
  const breakdown = this.calculateStrategyScore();
174
+ let effectiveScore = breakdown.overall;
175
+ // Score memory: cap recovery during inactivity
176
+ if (this.scoreMemory.enabled && this.lastEventTime > 0) {
177
+ // Track the lowest score observed
178
+ if (effectiveScore < this.scoreFloor) {
179
+ this.scoreFloor = effectiveScore;
180
+ }
181
+ // If inactive, prevent the score from recovering freely
182
+ const timeSinceEvent = Date.now() - this.lastEventTime;
183
+ if (timeSinceEvent > this.scoreMemory.inactivityThreshold) {
184
+ const cap = this.scoreFloor + this.scoreMemory.maxRecovery;
185
+ effectiveScore = Math.min(effectiveScore, cap);
186
+ }
187
+ }
156
188
  const result = {
157
- score: breakdown.overall,
189
+ score: effectiveScore,
158
190
  };
159
191
  if (options.breakdown) {
160
192
  result.breakdown = breakdown;
@@ -32,7 +32,7 @@
32
32
  * ```
33
33
  */
34
34
  export { BehaviorDetector } from './behavior-detector.js';
35
- export type { DetectionStrategy, StrategyConfig } from './strategy.js';
35
+ export type { DetectionStrategy, StrategyConfig, ScoreMemoryOptions } from './strategy.js';
36
36
  export { Mouse, Scroll, Click, Tap, Keyboard, Environment, Resize, Timing, Visibility, MouseStrategy, ScrollStrategy, ClickStrategy, TapStrategy, KeyboardStrategy, EnvironmentStrategy, ResizeStrategy, TimingStrategy, VisibilityStrategy, } from './strategies/index.js';
37
37
  export type { BehaviorSettings, ScoreOptions, ScoreResult, ScoreBreakdown, TrackedEvent, EventType, ScoringFunction, } from './types.js';
38
38
  export { DEFAULT_SETTINGS } from './types.js';
@@ -30,6 +30,46 @@ const AUTOMATION_GLOBALS = [
30
30
  '__fxdriver_unwrapped',
31
31
  '__selenium_unwrapped',
32
32
  ];
33
+ /**
34
+ * Get the WebGL renderer string without triggering deprecation warnings.
35
+ * Modern browsers (Firefox 128+, Chrome 113+) return the unmasked renderer
36
+ * directly from gl.RENDERER. Falls back to the deprecated
37
+ * WEBGL_debug_renderer_info extension for older browsers.
38
+ */
39
+ function getWebGLRenderer(gl) {
40
+ // Modern browsers: gl.RENDERER now returns the unmasked string directly
41
+ const renderer = gl.getParameter(gl.RENDERER);
42
+ if (renderer && !/^(WebKit WebGL|Mozilla|Google Inc\.)$/i.test(renderer)) {
43
+ return renderer;
44
+ }
45
+ // Fallback for older browsers that return a generic RENDERER string
46
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
47
+ if (debugInfo) {
48
+ return gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
49
+ }
50
+ return renderer !== null && renderer !== void 0 ? renderer : undefined;
51
+ }
52
+ /**
53
+ * Create a temporary WebGL context, run a callback, then release it.
54
+ */
55
+ function withWebGLContext(fn) {
56
+ try {
57
+ const canvas = document.createElement('canvas');
58
+ const gl = (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
59
+ if (!gl)
60
+ return { hasWebGL: false, result: undefined };
61
+ try {
62
+ return { hasWebGL: true, result: fn(gl) };
63
+ }
64
+ finally {
65
+ const loseCtx = gl.getExtension('WEBGL_lose_context');
66
+ loseCtx === null || loseCtx === void 0 ? void 0 : loseCtx.loseContext();
67
+ }
68
+ }
69
+ catch (_a) {
70
+ return { hasWebGL: false, result: undefined };
71
+ }
72
+ }
33
73
  export class EnvironmentStrategy extends BaseStrategy {
34
74
  constructor() {
35
75
  super(...arguments);
@@ -47,8 +87,11 @@ export class EnvironmentStrategy extends BaseStrategy {
47
87
  this.data = null;
48
88
  }
49
89
  onTick(_timestamp) {
50
- // Re-capture environment periodically to detect changes
51
- this.captureEnvironment();
90
+ // Only capture once - environment data is static and creating
91
+ // WebGL contexts on every tick leaks GPU resources
92
+ if (!this.data) {
93
+ this.captureEnvironment();
94
+ }
52
95
  }
53
96
  score() {
54
97
  if (!this.data)
@@ -179,23 +222,14 @@ export class EnvironmentStrategy extends BaseStrategy {
179
222
  const hasNoPlugins = !isMobile && navigator.plugins.length === 0;
180
223
  // 7. Software renderer detection (SwiftShader, ANGLE without GPU)
181
224
  let hasSoftwareRenderer = false;
182
- try {
183
- const canvas = document.createElement('canvas');
184
- const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
185
- if (gl) {
186
- const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
187
- if (debugInfo) {
188
- const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
189
- // SwiftShader = headless Chrome software renderer
190
- // ANGLE without real GPU = suspicious
191
- hasSoftwareRenderer = /SwiftShader|Software|LLVMpipe/i.test(renderer);
192
- // Store for debugging
193
- this._webglRenderer = renderer;
194
- }
195
- }
196
- }
197
- catch (_b) {
198
- // WebGL not available
225
+ const webglResult = withWebGLContext((gl) => getWebGLRenderer(gl));
226
+ if (webglResult.hasWebGL && webglResult.result) {
227
+ const renderer = webglResult.result;
228
+ // SwiftShader = headless Chrome software renderer
229
+ // ANGLE without real GPU = suspicious
230
+ hasSoftwareRenderer = /SwiftShader|Software|LLVMpipe/i.test(renderer);
231
+ // Store for debugging
232
+ this._webglRenderer = renderer;
199
233
  }
200
234
  return {
201
235
  isWebdriver,
@@ -212,22 +246,9 @@ export class EnvironmentStrategy extends BaseStrategy {
212
246
  captureEnvironment() {
213
247
  try {
214
248
  // WebGL detection
215
- let hasWebGL = false;
216
- let webglRenderer;
217
- try {
218
- const canvas = document.createElement('canvas');
219
- const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
220
- hasWebGL = !!gl;
221
- if (gl) {
222
- const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
223
- if (debugInfo) {
224
- webglRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
225
- }
226
- }
227
- }
228
- catch (e) {
229
- hasWebGL = false;
230
- }
249
+ const webgl = withWebGLContext((gl) => getWebGLRenderer(gl));
250
+ const hasWebGL = webgl.hasWebGL;
251
+ const webglRenderer = webgl.result;
231
252
  // WebRTC detection
232
253
  const hasWebRTC = !!(window.RTCPeerConnection ||
233
254
  window.mozRTCPeerConnection ||
@@ -20,7 +20,7 @@ export class MouseStrategy extends BaseStrategy {
20
20
  this.lastPosition = null;
21
21
  this.lastAngle = 0;
22
22
  this.cumulativeAngle = 0;
23
- this.rollingWindowMs = 5000;
23
+ this.rollingWindowMs = 30000;
24
24
  this.listener = null;
25
25
  this.leaveListener = null;
26
26
  this.enterListener = null;
@@ -14,7 +14,7 @@ export class ScrollStrategy extends BaseStrategy {
14
14
  this.velocitySeries = [];
15
15
  this.lastScrollY = null;
16
16
  this.lastTimestamp = 0;
17
- this.rollingWindowMs = 5000;
17
+ this.rollingWindowMs = 30000;
18
18
  this.documentHeight = 1;
19
19
  this.listener = null;
20
20
  this.isActive = false;
@@ -104,4 +104,37 @@ export interface TickOptions {
104
104
  * Default: true (recommended for better performance and battery life)
105
105
  */
106
106
  pauseOnHidden?: boolean;
107
+ /**
108
+ * Score memory options to prevent score recovery during inactivity.
109
+ * When enabled, the detector remembers the lowest score observed and prevents
110
+ * the score from jumping back up when behavioral data expires from rolling windows.
111
+ * Default: enabled with sensible defaults
112
+ */
113
+ scoreMemory?: ScoreMemoryOptions;
114
+ }
115
+ export interface ScoreMemoryOptions {
116
+ /**
117
+ * Enable score memory (default: true)
118
+ * When enabled, the detector tracks the lowest score observed and caps
119
+ * score recovery during periods of inactivity.
120
+ */
121
+ enabled: boolean;
122
+ /**
123
+ * Milliseconds of inactivity before the score cap kicks in (default: 2000)
124
+ * If no strategy events are received for this duration, the score cannot
125
+ * recover freely — it's capped near the lowest observed score.
126
+ */
127
+ inactivityThreshold?: number;
128
+ /**
129
+ * Maximum score increase allowed above the floor during inactivity (default: 0.1)
130
+ * During inactivity, effective score = min(rawScore, floor + maxRecovery)
131
+ */
132
+ maxRecovery?: number;
133
+ /**
134
+ * How much the floor rises per strategy event (default: 0.02)
135
+ * Each incoming event nudges the floor upward, allowing genuine human
136
+ * activity to eventually recover the score. At 0.02, it takes ~15 events
137
+ * to recover 0.3 points of score.
138
+ */
139
+ recoveryPerEvent?: number;
107
140
  }
package/dist/index.d.ts CHANGED
@@ -32,7 +32,7 @@
32
32
  * ```
33
33
  */
34
34
  export { BehaviorDetector } from './behavior-detector.js';
35
- export type { DetectionStrategy, StrategyConfig } from './strategy.js';
35
+ export type { DetectionStrategy, StrategyConfig, ScoreMemoryOptions } from './strategy.js';
36
36
  export { Mouse, Scroll, Click, Tap, Keyboard, Environment, Resize, Timing, Visibility, MouseStrategy, ScrollStrategy, ClickStrategy, TapStrategy, KeyboardStrategy, EnvironmentStrategy, ResizeStrategy, TimingStrategy, VisibilityStrategy, } from './strategies/index.js';
37
37
  export type { BehaviorSettings, ScoreOptions, ScoreResult, ScoreBreakdown, TrackedEvent, EventType, ScoringFunction, } from './types.js';
38
38
  export { DEFAULT_SETTINGS } from './types.js';
@@ -104,4 +104,37 @@ export interface TickOptions {
104
104
  * Default: true (recommended for better performance and battery life)
105
105
  */
106
106
  pauseOnHidden?: boolean;
107
+ /**
108
+ * Score memory options to prevent score recovery during inactivity.
109
+ * When enabled, the detector remembers the lowest score observed and prevents
110
+ * the score from jumping back up when behavioral data expires from rolling windows.
111
+ * Default: enabled with sensible defaults
112
+ */
113
+ scoreMemory?: ScoreMemoryOptions;
114
+ }
115
+ export interface ScoreMemoryOptions {
116
+ /**
117
+ * Enable score memory (default: true)
118
+ * When enabled, the detector tracks the lowest score observed and caps
119
+ * score recovery during periods of inactivity.
120
+ */
121
+ enabled: boolean;
122
+ /**
123
+ * Milliseconds of inactivity before the score cap kicks in (default: 2000)
124
+ * If no strategy events are received for this duration, the score cannot
125
+ * recover freely — it's capped near the lowest observed score.
126
+ */
127
+ inactivityThreshold?: number;
128
+ /**
129
+ * Maximum score increase allowed above the floor during inactivity (default: 0.1)
130
+ * During inactivity, effective score = min(rawScore, floor + maxRecovery)
131
+ */
132
+ maxRecovery?: number;
133
+ /**
134
+ * How much the floor rises per strategy event (default: 0.02)
135
+ * Each incoming event nudges the floor upward, allowing genuine human
136
+ * activity to eventually recover the score. At 0.02, it takes ~15 events
137
+ * to recover 0.3 points of score.
138
+ */
139
+ recoveryPerEvent?: number;
107
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axeptio/behavior-detection",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Lightweight behavior detection library to assess human likelihood of user sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.cjs",