guardian-risk 0.2.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,17 @@
2
2
 
3
3
  Configurable risk decision engine for TypeScript. Evaluate signals against rules and get an explainable risk score.
4
4
 
5
+ **Production-ready** at `0.3.x` — zero runtime dependencies, hardened validation, async hooks, and official plugins for Express, Redis, VPN, browser, and logging.
6
+
5
7
  ## Install
6
8
 
7
9
  ```bash
8
10
  npm install guardian-risk
9
11
  ```
10
12
 
11
- ## Usage
13
+ ## Usage (core only)
14
+
15
+ No plugins required — sync `analyze()` works when no hooks are registered:
12
16
 
13
17
  ```typescript
14
18
  import { Guardian } from 'guardian-risk';
@@ -41,32 +45,49 @@ Install **core first**, then add only the plugins you need:
41
45
  | Package | Install | Purpose |
42
46
  |---------|---------|---------|
43
47
  | **guardian-risk** (this) | `npm i guardian-risk` | Core engine |
44
- | guardian-risk-express | `npm i guardian-risk-express` | Express request signals |
45
- | guardian-risk-browser | `npm i guardian-risk-browser` | Browser behavioral signals |
46
- | guardian-risk-redis | `npm i guardian-risk-redis` | Redis session counters |
48
+ | guardian-risk-express | `npm i guardian-risk-express` | Express middleware + validated IP |
49
+ | guardian-risk-redis | `npm i guardian-risk-redis` | Redis session counters + rate limits |
47
50
  | guardian-risk-vpn | `npm i guardian-risk-vpn` | VPN / proxy / Tor detection |
51
+ | guardian-risk-browser | `npm i guardian-risk-browser` | Browser behavioral signals |
48
52
  | guardian-risk-logger | `npm i guardian-risk-logger` | Audit logging |
49
53
 
50
54
  ```bash
51
- # Example: Express + VPN + Logger stack
52
- npm install guardian-risk guardian-risk-express guardian-risk-vpn guardian-risk-logger
55
+ npm install guardian-risk guardian-risk-express guardian-risk-redis guardian-risk-vpn guardian-risk-logger
56
+ npm install ioredis # optional, required for Redis in production
53
57
  ```
54
58
 
59
+ ### Production Express example
60
+
55
61
  ```typescript
62
+ import express from 'express';
56
63
  import { Guardian } from 'guardian-risk';
57
- import { expressPlugin } from 'guardian-risk-express';
58
- import { vpnPlugin } from 'guardian-risk-vpn';
59
- import { loggerPlugin, analyzeAndLog } from 'guardian-risk-logger';
60
-
61
- const guardian = new Guardian()
62
- .use(expressPlugin())
63
- .use(vpnPlugin())
64
- .use(loggerPlugin());
65
-
66
- analyzeAndLog(guardian);
64
+ import { expressPlugin, guardianMiddleware } from 'guardian-risk-express';
65
+ import { redisPlugin } from 'guardian-risk-redis';
66
+ import { vpnPlugin, StaticIpProvider } from 'guardian-risk-vpn';
67
+ import { loggerPlugin } from 'guardian-risk-logger';
68
+
69
+ const app = express();
70
+ app.set('trust proxy', 1);
71
+
72
+ const template = new Guardian()
73
+ .use(expressPlugin({ trustProxy: true }))
74
+ .use(redisPlugin({ url: process.env.REDIS_URL, allowInMemoryFallback: false }))
75
+ .use(vpnPlugin({ provider: new StaticIpProvider({}), vpnScore: 25 }))
76
+ .use(loggerPlugin({ minScore: 20 }))
77
+ .rule({ name: 'Burst', when: (s) => (s.requestsInWindow as number) > 30, score: 40 });
78
+
79
+ app.get('/health', (_req, res) => res.json({ ok: true }));
80
+
81
+ app.use(
82
+ guardianMiddleware(template, {
83
+ blockAboveScore: 80,
84
+ onAnalyzeError: 'block',
85
+ exposeBlockDetails: false,
86
+ }),
87
+ );
67
88
  ```
68
89
 
69
- > Plugin packages are early stubsAPIs may change before `1.0.0`.
90
+ > Use your own IP intelligence provider in production (MaxMind, IPinfo, etc.) not the dev-only `IpApiProvider`.
70
91
 
71
92
  ## Plugins API
72
93
 
@@ -76,11 +97,39 @@ import type { Plugin } from 'guardian-risk';
76
97
  const myPlugin: Plugin = {
77
98
  name: 'my-plugin',
78
99
  install(guardian) {
79
- guardian.rule({ name: 'Custom', when: () => true, score: 10 });
100
+ guardian.beforeAnalyze(async ({ guardian: g }) => {
101
+ g.signal('customSignal', true);
102
+ });
80
103
  },
81
104
  };
82
105
 
83
- new Guardian().use(myPlugin);
106
+ await new Guardian().use(myPlugin).analyzeAsync();
107
+ ```
108
+
109
+ ## Typed signals & presets
110
+
111
+ ```typescript
112
+ import { defineSignals, applyRules, botDetectionRules } from 'guardian-risk';
113
+
114
+ const bot = defineSignals<{ mouseLinearity: number; headlessUA: boolean }>();
115
+
116
+ const guardian = applyRules(bot.create(), botDetectionRules)
117
+ .signal('mouseLinearity', 0.95)
118
+ .signal('headlessUA', true);
119
+
120
+ const report = await guardian.analyzeAsync();
121
+ ```
122
+
123
+ ## Rule groups
124
+
125
+ ```typescript
126
+ guardian.ruleGroup({
127
+ name: 'login',
128
+ maxScore: 40,
129
+ rules: [
130
+ { name: 'BruteForce', when: (s) => (s.loginAttempts as number) > 5, score: 45 },
131
+ ],
132
+ });
84
133
  ```
85
134
 
86
135
  ## API
@@ -88,26 +137,48 @@ new Guardian().use(myPlugin);
88
137
  | Method | Description |
89
138
  |--------|-------------|
90
139
  | `guardian.signal(key, value)` | Add a signal |
140
+ | `guardian.getSignal(key)` | Read a signal without modifying state |
91
141
  | `guardian.rule({ name, when, score, reason? })` | Register a rule |
142
+ | `guardian.ruleGroup({ name, maxScore, rules })` | Register capped rule group |
92
143
  | `guardian.use(plugin)` | Install a plugin (once per name) |
93
- | `guardian.analyze()` | Run evaluation, returns `RiskReport` |
144
+ | `guardian.beforeAnalyze(hook)` | Run hook before evaluation (async OK) |
145
+ | `guardian.afterAnalyze(hook)` | Run hook after report is built |
146
+ | `guardian.analyze()` | Sync analysis (only when no hooks registered) |
147
+ | `guardian.analyzeAsync(context?)` | Async analysis with lifecycle hooks |
148
+ | `guardian.fork()` | Clone rules/plugins for per-request use |
94
149
  | `guardian.reset()` | Clear signals (rules + plugins persist) |
95
150
  | `guardian.getInstalledPlugins()` | List installed plugin names |
96
151
 
152
+ ## Production checklist
153
+
154
+ 1. One **template** `Guardian` at startup; `fork()` or middleware per request
155
+ 2. `await analyzeAsync(req)` when plugins are installed
156
+ 3. `app.set('trust proxy', 1)` behind load balancers
157
+ 4. `REDIS_URL` set; `allowInMemoryFallback: false` in production
158
+ 5. Your own VPN/IP provider — not default external APIs
159
+ 6. `onAnalyzeError: 'block'` and `exposeBlockDetails: false` when blocking
160
+ 7. Browser/client signals are hints only — never sole auth factor
161
+
162
+ Full details: [SECURITY.md](https://github.com/himanshu6306singh/guardian-risk/blob/main/SECURITY.md)
163
+
97
164
  ## Security
98
165
 
99
166
  - **Zero runtime dependencies** — minimal supply chain risk
100
167
  - **No install scripts** — nothing runs on `npm install`
168
+ - String signals capped at 4 KB; `NaN`/`Infinity` rejected
101
169
  - Prototype pollution protection on signal keys
102
170
  - Rule `when()` errors isolated — engine stays stable
171
+ - Hook timeout (10s); rules/plugins locked during `analyzeAsync()`
172
+ - Deep-frozen matched rules in reports
103
173
  - Score bounds: ±10,000 per rule, ±1,000,000 total
104
- - Plugin `install()` failures throw `PluginInstallError` without registering
105
- - See [SECURITY.md](SECURITY.md) for vulnerability reporting
174
+
175
+ See [SECURITY.md](https://github.com/himanshu6306singh/guardian-risk/blob/main/SECURITY.md) for vulnerability reporting.
106
176
 
107
177
  ## Links
108
178
 
109
179
  - [GitHub monorepo](https://github.com/himanshu6306singh/guardian-risk)
110
- - [All packages guide](https://github.com/himanshu6306singh/guardian-risk/blob/main/ECOSYSTEM.md)
180
+ - [Migration guide](https://github.com/himanshu6306singh/guardian-risk/blob/main/MIGRATION.md)
181
+ - [All packages](https://github.com/himanshu6306singh/guardian-risk/blob/main/ECOSYSTEM.md)
111
182
 
112
183
  ## License
113
184
 
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var crypto = require('crypto');
4
+ var net = require('net');
4
5
 
5
6
  // src/constants/defaults.ts
6
7
  var DEFAULT_RISK_LEVELS = [
@@ -16,12 +17,38 @@ var MAX_SIGNALS = 1e3;
16
17
  var MAX_KEY_LENGTH = 256;
17
18
  var MAX_RULE_SCORE = 1e4;
18
19
  var MAX_TOTAL_SCORE = 1e6;
20
+ var MAX_SIGNAL_STRING_LENGTH = 4096;
21
+ var MAX_SESSION_ID_LENGTH = 128;
22
+ var SESSION_ID_PATTERN = /^[a-zA-Z0-9._-]+$/;
23
+ var HOOK_TIMEOUT_MS = 1e4;
19
24
  var BLOCKED_SIGNAL_KEYS = /* @__PURE__ */ new Set([
20
25
  "__proto__",
21
26
  "constructor",
22
27
  "prototype"
23
28
  ]);
24
29
 
30
+ // src/hooks/runHooks.ts
31
+ async function runHooks(hooks, context, timeoutMs = HOOK_TIMEOUT_MS) {
32
+ for (const hook of hooks) {
33
+ await runWithTimeout(() => hook(context), timeoutMs);
34
+ }
35
+ }
36
+ async function runWithTimeout(operation, timeoutMs) {
37
+ let timer;
38
+ const timeout = new Promise((_, reject) => {
39
+ timer = setTimeout(() => {
40
+ reject(new Error(`Guardian analyze hook timed out after ${timeoutMs}ms`));
41
+ }, timeoutMs);
42
+ });
43
+ try {
44
+ await Promise.race([Promise.resolve(operation()), timeout]);
45
+ } finally {
46
+ if (timer !== void 0) {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ }
51
+
25
52
  // src/utils/resolveLevel.ts
26
53
  function resolveLevel(score, thresholds = DEFAULT_RISK_LEVELS) {
27
54
  for (const threshold of thresholds) {
@@ -41,11 +68,14 @@ var ReportBuilder = class {
41
68
  build(score, matchedRules, thresholds, analyzedAt = (/* @__PURE__ */ new Date()).toISOString()) {
42
69
  const reasons = matchedRules.map((rule) => rule.reason);
43
70
  const level = resolveLevel(score, thresholds);
71
+ const frozenRules = matchedRules.map(
72
+ (rule) => Object.freeze({ ...rule })
73
+ );
44
74
  const report = {
45
75
  score,
46
76
  level,
47
77
  reasons: Object.freeze([...reasons]),
48
- matchedRules: Object.freeze([...matchedRules]),
78
+ matchedRules: Object.freeze(frozenRules),
49
79
  analyzedAt
50
80
  };
51
81
  return Object.freeze(report);
@@ -72,7 +102,8 @@ var RuleEvaluator = class {
72
102
  id: rule.id,
73
103
  name: rule.name,
74
104
  score: rule.score,
75
- reason: rule.reason ?? rule.name
105
+ reason: rule.reason ?? rule.name,
106
+ ...rule.group !== void 0 ? { group: rule.group } : {}
76
107
  });
77
108
  }
78
109
  }
@@ -83,10 +114,24 @@ var RuleEvaluator = class {
83
114
  // src/score/ScoreCalculator.ts
84
115
  var ScoreCalculator = class {
85
116
  /**
86
- * Sum scores from all matched rules, clamped to a safe maximum.
117
+ * Sum scores from matched rules, applying group caps when configured.
87
118
  */
88
- calculate(matchedRules) {
89
- const total = matchedRules.reduce((sum, rule) => sum + rule.score, 0);
119
+ calculate(matchedRules, groupCaps = []) {
120
+ const caps = new Map(groupCaps.map((cap) => [cap.name, cap.maxScore]));
121
+ const grouped = /* @__PURE__ */ new Map();
122
+ let ungroupedTotal = 0;
123
+ for (const rule of matchedRules) {
124
+ if (rule.group !== void 0) {
125
+ grouped.set(rule.group, (grouped.get(rule.group) ?? 0) + rule.score);
126
+ } else {
127
+ ungroupedTotal += rule.score;
128
+ }
129
+ }
130
+ let total = ungroupedTotal;
131
+ for (const [groupName, groupScore] of grouped) {
132
+ const cap = caps.get(groupName);
133
+ total += cap !== void 0 ? Math.min(groupScore, cap) : groupScore;
134
+ }
90
135
  if (!Number.isFinite(total)) {
91
136
  return 0;
92
137
  }
@@ -104,7 +149,13 @@ function validateSignalValue(value) {
104
149
  return true;
105
150
  }
106
151
  const type = typeof value;
107
- return type === "string" || type === "number" || type === "boolean";
152
+ if (type === "string") {
153
+ return value.length <= MAX_SIGNAL_STRING_LENGTH;
154
+ }
155
+ if (type === "number") {
156
+ return Number.isFinite(value);
157
+ }
158
+ return type === "boolean";
108
159
  }
109
160
  function validateSignalKey(key) {
110
161
  if (typeof key !== "string" || key.length === 0) {
@@ -149,6 +200,14 @@ function validateRuleInput(input) {
149
200
  throw new TypeError(`Rule description exceeds maximum length of ${MAX_KEY_LENGTH}`);
150
201
  }
151
202
  }
203
+ if (input.group !== void 0) {
204
+ if (typeof input.group !== "string" || input.group.trim().length === 0) {
205
+ throw new TypeError("Rule group must be a non-empty string");
206
+ }
207
+ if (input.group.length > MAX_KEY_LENGTH) {
208
+ throw new TypeError(`Rule group exceeds maximum length of ${MAX_KEY_LENGTH}`);
209
+ }
210
+ }
152
211
  }
153
212
  function validatePlugin(plugin) {
154
213
  if (typeof plugin.name !== "string" || plugin.name.trim().length === 0) {
@@ -174,6 +233,22 @@ function validateRiskLevels(levels) {
174
233
  }
175
234
  }
176
235
  }
236
+ function validateRuleGroupInput(input) {
237
+ if (typeof input.name !== "string" || input.name.trim().length === 0) {
238
+ throw new TypeError("Rule group name must be a non-empty string");
239
+ }
240
+ if (input.name.length > MAX_KEY_LENGTH) {
241
+ throw new TypeError(`Rule group name exceeds maximum length of ${MAX_KEY_LENGTH}`);
242
+ }
243
+ if (input.maxScore !== void 0) {
244
+ if (typeof input.maxScore !== "number" || !Number.isFinite(input.maxScore) || input.maxScore < 0) {
245
+ throw new TypeError("Rule group maxScore must be a non-negative finite number");
246
+ }
247
+ }
248
+ if (!Array.isArray(input.rules) || input.rules.length === 0) {
249
+ throw new TypeError("Rule group must contain at least one rule");
250
+ }
251
+ }
177
252
  function generateId() {
178
253
  return crypto.randomUUID();
179
254
  }
@@ -235,6 +310,7 @@ var RiskEngine = class {
235
310
  }
236
311
  deps;
237
312
  rules = [];
313
+ groupCaps = [];
238
314
  thresholds;
239
315
  /**
240
316
  * Register a rule for evaluation.
@@ -251,13 +327,30 @@ var RiskEngine = class {
251
327
  getRules() {
252
328
  return this.rules;
253
329
  }
330
+ /**
331
+ * Get configured per-group score caps.
332
+ */
333
+ getGroupCaps() {
334
+ return this.groupCaps;
335
+ }
336
+ /**
337
+ * Cap the combined score of matched rules in a group.
338
+ */
339
+ setGroupCap(name, maxScore) {
340
+ const existing = this.groupCaps.findIndex((cap) => cap.name === name);
341
+ if (existing >= 0) {
342
+ this.groupCaps[existing] = { name, maxScore };
343
+ return;
344
+ }
345
+ this.groupCaps.push({ name, maxScore });
346
+ }
254
347
  /**
255
348
  * Run the full risk analysis pipeline.
256
349
  */
257
350
  analyze() {
258
351
  const signals = this.deps.signalStore.getAll();
259
352
  const matchedRules = this.deps.ruleEvaluator.evaluate(this.rules, signals);
260
- const score = this.deps.scoreCalculator.calculate(matchedRules);
353
+ const score = this.deps.scoreCalculator.calculate(matchedRules, this.groupCaps);
261
354
  return this.deps.reportBuilder.build(score, matchedRules, this.thresholds);
262
355
  }
263
356
  };
@@ -275,7 +368,8 @@ var RuleBuilder = class {
275
368
  score: input.score,
276
369
  when: input.when,
277
370
  ...input.description !== void 0 ? { description: input.description } : {},
278
- ...input.reason !== void 0 ? { reason: input.reason } : {}
371
+ ...input.reason !== void 0 ? { reason: input.reason } : {},
372
+ ...input.group !== void 0 ? { group: input.group } : {}
279
373
  };
280
374
  return rule;
281
375
  }
@@ -319,6 +413,15 @@ var PluginRegistry = class {
319
413
  has(name) {
320
414
  return this.installed.has(name);
321
415
  }
416
+ /**
417
+ * Copy installed plugin names from a template (used by Guardian.fork).
418
+ * Does not call install() — rules and hooks are copied separately.
419
+ */
420
+ adoptInstalled(names) {
421
+ for (const name of names) {
422
+ this.installed.add(name);
423
+ }
424
+ }
322
425
  /**
323
426
  * Get names of all installed plugins in registration order.
324
427
  */
@@ -328,12 +431,17 @@ var PluginRegistry = class {
328
431
  };
329
432
 
330
433
  // src/engine/Guardian.ts
331
- var Guardian = class {
434
+ var Guardian = class _Guardian {
332
435
  signalStore;
333
436
  riskEngine;
334
437
  pluginRegistry = new PluginRegistry();
438
+ plugins = [];
439
+ beforeHooks = [];
440
+ afterHooks = [];
441
+ thresholds;
442
+ analyzing = false;
335
443
  constructor(config = {}) {
336
- const thresholds = config.levels ?? DEFAULT_RISK_LEVELS;
444
+ this.thresholds = config.levels !== void 0 ? [...config.levels] : DEFAULT_RISK_LEVELS;
337
445
  if (config.levels !== void 0) {
338
446
  validateRiskLevels(config.levels);
339
447
  }
@@ -344,7 +452,7 @@ var Guardian = class {
344
452
  scoreCalculator: new ScoreCalculator(),
345
453
  reportBuilder: new ReportBuilder()
346
454
  };
347
- this.riskEngine = new RiskEngine(deps, thresholds);
455
+ this.riskEngine = new RiskEngine(deps, this.thresholds);
348
456
  }
349
457
  /**
350
458
  * Add a signal value for risk evaluation.
@@ -353,21 +461,60 @@ var Guardian = class {
353
461
  this.signalStore.set(key, value);
354
462
  return this;
355
463
  }
464
+ /**
465
+ * Read a signal value without modifying state.
466
+ */
467
+ getSignal(key) {
468
+ return this.signalStore.get(key);
469
+ }
356
470
  /**
357
471
  * Register a rule. ID is auto-generated.
358
472
  */
359
473
  rule(input) {
474
+ this.assertNotAnalyzing("register rules");
360
475
  const rule = RuleBuilder.create(input);
361
476
  this.riskEngine.addRule(rule);
362
477
  return this;
363
478
  }
479
+ /**
480
+ * Register a named group of rules with an optional combined score cap.
481
+ */
482
+ ruleGroup(input) {
483
+ this.assertNotAnalyzing("register rule groups");
484
+ validateRuleGroupInput(input);
485
+ for (const ruleInput of input.rules) {
486
+ this.rule({ ...ruleInput, group: input.name });
487
+ }
488
+ if (input.maxScore !== void 0) {
489
+ this.riskEngine.setGroupCap(input.name, input.maxScore);
490
+ }
491
+ return this;
492
+ }
364
493
  /**
365
494
  * Install a plugin. Each plugin name may only be registered once.
366
495
  */
367
496
  use(plugin) {
497
+ this.assertNotAnalyzing("install plugins");
498
+ this.plugins.push(plugin);
368
499
  this.pluginRegistry.install(plugin, this);
369
500
  return this;
370
501
  }
502
+ /**
503
+ * Register a hook that runs before rule evaluation.
504
+ * Use for loading signals from requests, Redis, IP lookups, etc.
505
+ */
506
+ beforeAnalyze(hook) {
507
+ this.beforeHooks.push(hook);
508
+ return this;
509
+ }
510
+ /**
511
+ * Register a hook that runs after the report is built.
512
+ * Use for audit logging, metrics, or blocking responses.
513
+ */
514
+ afterAnalyze(hook) {
515
+ this.afterHooks.push(hook);
516
+ return this;
517
+ }
371
518
  /**
372
519
  * Returns names of installed plugins.
373
520
  */
@@ -375,22 +522,191 @@ var Guardian = class {
375
522
  return this.pluginRegistry.getInstalled();
376
523
  }
377
524
  /**
378
- * Run risk analysis and return an immutable report.
525
+ * Run risk analysis synchronously (skips lifecycle hooks).
526
+ * Prefer {@link analyzeAsync} when hooks are registered.
379
527
  */
380
528
  analyze() {
529
+ if (this.beforeHooks.length > 0 || this.afterHooks.length > 0) {
530
+ throw new Error(
531
+ "Guardian has analyze hooks registered. Use analyzeAsync() instead of analyze()."
532
+ );
533
+ }
381
534
  return this.riskEngine.analyze();
382
535
  }
383
536
  /**
384
- * Clear all signals. Rules and installed plugins persist across resets.
537
+ * Run lifecycle hooks, evaluate rules, and return an immutable report.
538
+ *
539
+ * @param context Optional caller context passed to hooks (e.g. Express `req`).
540
+ */
541
+ async analyzeAsync(context) {
542
+ this.analyzing = true;
543
+ try {
544
+ const analyzeContext = {
545
+ data: context,
546
+ guardian: this
547
+ };
548
+ await runHooks(this.beforeHooks, analyzeContext);
549
+ const report = this.riskEngine.analyze();
550
+ const afterContext = {
551
+ ...analyzeContext,
552
+ report
553
+ };
554
+ await runHooks(this.afterHooks, afterContext);
555
+ return report;
556
+ } finally {
557
+ this.analyzing = false;
558
+ }
559
+ }
560
+ /**
561
+ * Create an isolated copy sharing rules, plugins, and hooks.
562
+ * Each fork has its own signal store for safe concurrent use.
563
+ */
564
+ fork() {
565
+ const child = new _Guardian({ levels: [...this.thresholds] });
566
+ for (const rule of this.riskEngine.getRules()) {
567
+ child.riskEngine.addRule(rule);
568
+ }
569
+ for (const cap of this.riskEngine.getGroupCaps()) {
570
+ child.riskEngine.setGroupCap(cap.name, cap.maxScore);
571
+ }
572
+ child.plugins.push(...this.plugins);
573
+ child.pluginRegistry.adoptInstalled(this.pluginRegistry.getInstalled());
574
+ for (const hook of this.beforeHooks) {
575
+ child.beforeHooks.push(hook);
576
+ }
577
+ for (const hook of this.afterHooks) {
578
+ child.afterHooks.push(hook);
579
+ }
580
+ return child;
581
+ }
582
+ /**
583
+ * Clear all signals. Rules, plugins, and hooks persist across resets.
385
584
  */
386
585
  reset() {
387
586
  this.signalStore.clear();
388
587
  return this;
389
588
  }
589
+ assertNotAnalyzing(action) {
590
+ if (this.analyzing) {
591
+ throw new Error(`Cannot ${action} while analysis is in progress`);
592
+ }
593
+ }
390
594
  };
391
595
 
596
+ // src/guardian/defineSignals.ts
597
+ function defineSignals() {
598
+ return {
599
+ create(config) {
600
+ return new Guardian(config);
601
+ }
602
+ };
603
+ }
604
+
605
+ // src/presets/applyRules.ts
606
+ function applyRules(guardian, rules) {
607
+ for (const rule of rules) {
608
+ guardian.rule(rule);
609
+ }
610
+ return guardian;
611
+ }
612
+
613
+ // src/presets/botDetection.ts
614
+ var botDetectionRules = [
615
+ {
616
+ name: "LinearMouseMovement",
617
+ reason: "Mouse movement is unnaturally linear",
618
+ when: (s) => s.mouseLinearity > 0.9,
619
+ score: 25
620
+ },
621
+ {
622
+ name: "RequestBurst",
623
+ reason: "Unusually high request rate in short window",
624
+ when: (s) => s.requestBurst > 50,
625
+ score: 30
626
+ },
627
+ {
628
+ name: "HeadlessBrowser",
629
+ reason: "User agent indicates headless browser",
630
+ when: (s) => s.headlessUA === true,
631
+ score: 40
632
+ },
633
+ {
634
+ name: "NewSession",
635
+ reason: "Session is very new",
636
+ when: (s) => s.sessionAgeSeconds < 10,
637
+ score: 10
638
+ },
639
+ {
640
+ name: "HighRequestRate",
641
+ reason: "Too many requests per minute",
642
+ when: (s) => s.requestsPerMinute > 30,
643
+ score: 35
644
+ }
645
+ ];
646
+ var loginProtectionRules = [
647
+ {
648
+ name: "BruteForceAttempts",
649
+ reason: "Multiple failed login attempts",
650
+ when: (s) => s.loginAttempts > 5,
651
+ score: 45,
652
+ group: "login"
653
+ },
654
+ {
655
+ name: "RapidLoginBurst",
656
+ reason: "Login attempts in rapid succession",
657
+ when: (s) => s.requestsPerMinute > 10,
658
+ score: 25,
659
+ group: "login"
660
+ }
661
+ ];
662
+ function parseIpAddress(value) {
663
+ const trimmed = value.trim();
664
+ if (trimmed.length === 0 || trimmed.length > 45) {
665
+ return null;
666
+ }
667
+ return net.isIP(trimmed) === 0 ? null : trimmed;
668
+ }
669
+ function isPrivateIp(ip) {
670
+ const parsed = parseIpAddress(ip);
671
+ if (!parsed) {
672
+ return true;
673
+ }
674
+ if (net.isIP(parsed) === 4) {
675
+ if (parsed.startsWith("10.") || parsed.startsWith("127.") || parsed.startsWith("192.168.") || parsed.startsWith("169.254.") || parsed.startsWith("0.")) {
676
+ return true;
677
+ }
678
+ if (parsed.startsWith("172.")) {
679
+ const second = Number(parsed.split(".")[1]);
680
+ if (second >= 16 && second <= 31) {
681
+ return true;
682
+ }
683
+ }
684
+ if (parsed.startsWith("100.")) {
685
+ const second = Number(parsed.split(".")[1]);
686
+ if (second >= 64 && second <= 127) {
687
+ return true;
688
+ }
689
+ }
690
+ return false;
691
+ }
692
+ const lower = parsed.toLowerCase();
693
+ return lower === "::1" || lower.startsWith("fc") || lower.startsWith("fd") || lower.startsWith("fe80");
694
+ }
695
+ function sanitizeSessionId(value) {
696
+ const trimmed = value.trim();
697
+ if (trimmed.length === 0 || trimmed.length > MAX_SESSION_ID_LENGTH) {
698
+ return null;
699
+ }
700
+ if (!SESSION_ID_PATTERN.test(trimmed)) {
701
+ return null;
702
+ }
703
+ return trimmed;
704
+ }
705
+
392
706
  exports.DEFAULT_RISK_LEVELS = DEFAULT_RISK_LEVELS;
393
707
  exports.Guardian = Guardian;
708
+ exports.HOOK_TIMEOUT_MS = HOOK_TIMEOUT_MS;
709
+ exports.MAX_SIGNAL_STRING_LENGTH = MAX_SIGNAL_STRING_LENGTH;
394
710
  exports.PluginAlreadyInstalledError = PluginAlreadyInstalledError;
395
711
  exports.PluginInstallError = PluginInstallError;
396
712
  exports.PluginRegistry = PluginRegistry;
@@ -399,4 +715,11 @@ exports.RuleBuilder = RuleBuilder;
399
715
  exports.RuleEvaluator = RuleEvaluator;
400
716
  exports.ScoreCalculator = ScoreCalculator;
401
717
  exports.SignalStore = SignalStore;
718
+ exports.applyRules = applyRules;
719
+ exports.botDetectionRules = botDetectionRules;
720
+ exports.defineSignals = defineSignals;
721
+ exports.isPrivateIp = isPrivateIp;
722
+ exports.loginProtectionRules = loginProtectionRules;
723
+ exports.parseIpAddress = parseIpAddress;
402
724
  exports.resolveLevel = resolveLevel;
725
+ exports.sanitizeSessionId = sanitizeSessionId;