guardian-risk-browser 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -14
- package/dist/index.cjs +177 -6
- package/dist/index.d.cts +65 -11
- package/dist/index.d.ts +65 -11
- package/dist/index.js +175 -7
- package/package.json +14 -4
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,31 +6,44 @@
|
|
|
6
6
|
npm install guardian-risk guardian-risk-browser
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Browser-side behavioral signal collection for [guardian-risk](https://www.npmjs.com/package/guardian-risk).
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## Planned signals
|
|
11
|
+
## Signals
|
|
14
12
|
|
|
15
13
|
| Signal | Source |
|
|
16
14
|
|--------|--------|
|
|
17
|
-
| `mouseLinearity` |
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
15
|
+
| `mouseLinearity` | Pointer movement linearity (0–1) |
|
|
16
|
+
| `hasPointerActivity` | Mouse or touch activity detected |
|
|
17
|
+
| `keystrokeCount` | Key events in sample window |
|
|
18
|
+
| `headlessUA` | User-agent heuristics |
|
|
21
19
|
|
|
22
|
-
## Usage
|
|
20
|
+
## Usage
|
|
23
21
|
|
|
24
22
|
```typescript
|
|
25
23
|
import { Guardian } from 'guardian-risk';
|
|
26
|
-
import { browserPlugin,
|
|
24
|
+
import { browserPlugin, BrowserCollector } from 'guardian-risk-browser';
|
|
27
25
|
|
|
28
26
|
const guardian = new Guardian().use(browserPlugin());
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
const
|
|
28
|
+
const collector = new BrowserCollector();
|
|
29
|
+
const stop = collector.start();
|
|
30
|
+
// ... user interacts ...
|
|
31
|
+
collector.applyTo(guardian);
|
|
32
|
+
stop();
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
##
|
|
35
|
+
## Security notes
|
|
36
|
+
|
|
37
|
+
- **All browser signals are client-controlled** — attackers can spoof or omit them.
|
|
38
|
+
- Use for **defense in depth** only; never as sole auth or blocking factor.
|
|
39
|
+
- Pair with server-side signals (IP rate limits, session age, VPN checks).
|
|
40
|
+
- Mobile users: `hasPointerActivity` includes touch events.
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
- `browserPlugin()` — registers default behavioral rules
|
|
45
|
+
- `BrowserCollector` — tracks pointer/keyboard activity
|
|
46
|
+
- `collectSignals(guardian, options?)` — timed sampling helper
|
|
47
|
+
- `computeMouseLinearity(points)` — standalone metric
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
See [SECURITY.md](../../SECURITY.md).
|
package/dist/index.cjs
CHANGED
|
@@ -1,19 +1,190 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/collector.ts
|
|
4
|
+
function computeMouseLinearity(points) {
|
|
5
|
+
if (points.length < 3) {
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
const first = points[0];
|
|
9
|
+
const last = points[points.length - 1];
|
|
10
|
+
const straightDistance = distance(first, last);
|
|
11
|
+
if (straightDistance === 0) {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
let pathDistance = 0;
|
|
15
|
+
for (let i = 1; i < points.length; i++) {
|
|
16
|
+
pathDistance += distance(points[i - 1], points[i]);
|
|
17
|
+
}
|
|
18
|
+
if (pathDistance === 0) {
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const linearity = straightDistance / pathDistance;
|
|
22
|
+
return Math.min(1, Math.max(0, linearity));
|
|
23
|
+
}
|
|
24
|
+
function distance(a, b) {
|
|
25
|
+
const dx = b.x - a.x;
|
|
26
|
+
const dy = b.y - a.y;
|
|
27
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
28
|
+
}
|
|
29
|
+
var BrowserCollector = class {
|
|
30
|
+
maxMouseSamples;
|
|
31
|
+
target;
|
|
32
|
+
mousePoints = [];
|
|
33
|
+
clickTimes = [];
|
|
34
|
+
keyStrokeCount = 0;
|
|
35
|
+
pointerActivity = false;
|
|
36
|
+
startedAt = 0;
|
|
37
|
+
listeners = [];
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
this.maxMouseSamples = options.maxMouseSamples ?? 100;
|
|
40
|
+
this.target = options.target ?? getDefaultTarget();
|
|
41
|
+
}
|
|
42
|
+
/** Start listening for user events. Returns a stop function. */
|
|
43
|
+
start() {
|
|
44
|
+
if (!this.target) {
|
|
45
|
+
return () => {
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
this.startedAt = Date.now();
|
|
49
|
+
const onMouseMove = (event) => {
|
|
50
|
+
const e = event;
|
|
51
|
+
this.mousePoints.push({ x: e.clientX, y: e.clientY, t: Date.now() });
|
|
52
|
+
if (this.mousePoints.length > this.maxMouseSamples) {
|
|
53
|
+
this.mousePoints.shift();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const onClick = () => {
|
|
57
|
+
this.clickTimes.push(Date.now());
|
|
58
|
+
};
|
|
59
|
+
const onKeyDown = () => {
|
|
60
|
+
this.keyStrokeCount += 1;
|
|
61
|
+
};
|
|
62
|
+
const onPointer = () => {
|
|
63
|
+
this.pointerActivity = true;
|
|
64
|
+
};
|
|
65
|
+
this.addListener("mousemove", onMouseMove);
|
|
66
|
+
this.addListener("click", onClick);
|
|
67
|
+
this.addListener("keydown", onKeyDown);
|
|
68
|
+
this.addListener("pointerdown", onPointer);
|
|
69
|
+
this.addListener("touchstart", onPointer);
|
|
70
|
+
return () => this.stop();
|
|
71
|
+
}
|
|
72
|
+
stop() {
|
|
73
|
+
if (!this.target) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const { type, handler } of this.listeners) {
|
|
77
|
+
this.target.removeEventListener(type, handler);
|
|
78
|
+
}
|
|
79
|
+
this.listeners = [];
|
|
80
|
+
}
|
|
81
|
+
/** Build a snapshot of collected signals. */
|
|
82
|
+
getSnapshot() {
|
|
83
|
+
const intervals = [];
|
|
84
|
+
for (let i = 1; i < this.clickTimes.length; i++) {
|
|
85
|
+
intervals.push(this.clickTimes[i] - this.clickTimes[i - 1]);
|
|
86
|
+
}
|
|
87
|
+
const avgClickIntervalMs = intervals.length > 0 ? intervals.reduce((sum, n) => sum + n, 0) / intervals.length : 0;
|
|
88
|
+
return {
|
|
89
|
+
mouseSampleCount: this.mousePoints.length,
|
|
90
|
+
mouseLinearity: computeMouseLinearity(this.mousePoints),
|
|
91
|
+
clickCount: this.clickTimes.length,
|
|
92
|
+
avgClickIntervalMs,
|
|
93
|
+
keyStrokeCount: this.keyStrokeCount,
|
|
94
|
+
hasPointerActivity: this.pointerActivity || this.mousePoints.length > 0,
|
|
95
|
+
collectionDurationMs: this.startedAt > 0 ? Date.now() - this.startedAt : 0
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Push collected signals onto a Guardian instance. */
|
|
99
|
+
applyTo(guardian) {
|
|
100
|
+
const snapshot = this.getSnapshot();
|
|
101
|
+
return guardian.signal("mouseSampleCount", snapshot.mouseSampleCount).signal("mouseLinearity", round(snapshot.mouseLinearity, 4)).signal("clickCount", snapshot.clickCount).signal("avgClickIntervalMs", Math.round(snapshot.avgClickIntervalMs)).signal("keyStrokeCount", snapshot.keyStrokeCount).signal("hasPointerActivity", snapshot.hasPointerActivity).signal("collectionDurationMs", snapshot.collectionDurationMs).signal("signalSource", "browser");
|
|
102
|
+
}
|
|
103
|
+
addListener(type, handler) {
|
|
104
|
+
if (!this.target) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.target.addEventListener(type, handler, { passive: true });
|
|
108
|
+
this.listeners.push({ type, handler });
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
function getDefaultTarget() {
|
|
112
|
+
if (typeof document !== "undefined") {
|
|
113
|
+
return document;
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function round(value, digits) {
|
|
118
|
+
const factor = 10 ** digits;
|
|
119
|
+
return Math.round(value * factor) / factor;
|
|
120
|
+
}
|
|
121
|
+
|
|
3
122
|
// src/index.ts
|
|
4
123
|
function browserPlugin(options = {}) {
|
|
5
|
-
const {
|
|
124
|
+
const { autoStart = false, ...collectorOptions } = options;
|
|
6
125
|
return {
|
|
7
126
|
name: "guardian-risk-browser",
|
|
8
|
-
install(
|
|
127
|
+
install(guardian) {
|
|
128
|
+
guardian.ruleGroup({
|
|
129
|
+
name: "behavior",
|
|
130
|
+
maxScore: 50,
|
|
131
|
+
rules: [
|
|
132
|
+
{
|
|
133
|
+
name: "LinearMouse",
|
|
134
|
+
reason: "Mouse movement is unnaturally linear",
|
|
135
|
+
when: (s) => s.mouseLinearity > 0.92,
|
|
136
|
+
score: 25
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: "NoMouseActivity",
|
|
140
|
+
reason: "No pointer or mouse activity detected before submit",
|
|
141
|
+
when: (s) => s.mouseSampleCount === 0 && s.hasPointerActivity !== true,
|
|
142
|
+
score: 20
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "RoboticClicks",
|
|
146
|
+
reason: "Click timing is unnaturally regular",
|
|
147
|
+
when: (s) => {
|
|
148
|
+
const interval = s.avgClickIntervalMs;
|
|
149
|
+
const count = s.clickCount;
|
|
150
|
+
return count >= 3 && interval > 0 && interval < 50;
|
|
151
|
+
},
|
|
152
|
+
score: 30
|
|
153
|
+
}
|
|
154
|
+
]
|
|
155
|
+
});
|
|
156
|
+
if (autoStart && typeof document !== "undefined") {
|
|
157
|
+
const collector = new BrowserCollector(collectorOptions);
|
|
158
|
+
const stop = collector.start();
|
|
159
|
+
guardian.beforeAnalyze(({ guardian: g }) => {
|
|
160
|
+
collector.applyTo(g);
|
|
161
|
+
stop();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
9
164
|
}
|
|
10
165
|
};
|
|
11
166
|
}
|
|
12
|
-
function
|
|
13
|
-
return
|
|
167
|
+
function createBrowserCollector(options = {}) {
|
|
168
|
+
return new BrowserCollector(options);
|
|
169
|
+
}
|
|
170
|
+
function collectSignals(guardian, options = {}) {
|
|
171
|
+
const { durationMs = 0, ...collectorOptions } = options;
|
|
172
|
+
const collector = new BrowserCollector(collectorOptions);
|
|
173
|
+
const stop = collector.start();
|
|
174
|
+
const finish = () => {
|
|
175
|
+
stop();
|
|
176
|
+
return collector.applyTo(guardian);
|
|
177
|
+
};
|
|
178
|
+
if (durationMs <= 0) {
|
|
179
|
+
return Promise.resolve(finish());
|
|
180
|
+
}
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
setTimeout(() => resolve(finish()), durationMs);
|
|
183
|
+
});
|
|
14
184
|
}
|
|
15
185
|
|
|
186
|
+
exports.BrowserCollector = BrowserCollector;
|
|
16
187
|
exports.browserPlugin = browserPlugin;
|
|
17
188
|
exports.collectSignals = collectSignals;
|
|
18
|
-
|
|
19
|
-
|
|
189
|
+
exports.computeMouseLinearity = computeMouseLinearity;
|
|
190
|
+
exports.createBrowserCollector = createBrowserCollector;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,21 +1,75 @@
|
|
|
1
1
|
import * as guardian_risk from 'guardian-risk';
|
|
2
2
|
import { Plugin } from 'guardian-risk';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
interface
|
|
6
|
-
|
|
7
|
-
readonly
|
|
4
|
+
/** Collected behavioral signals from the browser. */
|
|
5
|
+
interface BrowserSignalSnapshot {
|
|
6
|
+
readonly mouseSampleCount: number;
|
|
7
|
+
readonly mouseLinearity: number;
|
|
8
|
+
readonly clickCount: number;
|
|
9
|
+
readonly avgClickIntervalMs: number;
|
|
10
|
+
readonly keyStrokeCount: number;
|
|
11
|
+
readonly hasPointerActivity: boolean;
|
|
12
|
+
readonly collectionDurationMs: number;
|
|
8
13
|
}
|
|
14
|
+
/** Options for browser signal collection. */
|
|
15
|
+
interface BrowserCollectorOptions {
|
|
16
|
+
/** Max mouse samples to retain. */
|
|
17
|
+
readonly maxMouseSamples?: number;
|
|
18
|
+
/** Target element or document root. */
|
|
19
|
+
readonly target?: EventTarget;
|
|
20
|
+
}
|
|
21
|
+
interface Point {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
t: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute how linear a mouse path is (0 = erratic, 1 = perfectly straight).
|
|
28
|
+
* High values often indicate scripted movement.
|
|
29
|
+
*/
|
|
30
|
+
declare function computeMouseLinearity(points: readonly Point[]): number;
|
|
9
31
|
/**
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
32
|
+
* Collects mouse, click, and keyboard behavioral signals in the browser.
|
|
33
|
+
*/
|
|
34
|
+
declare class BrowserCollector {
|
|
35
|
+
private readonly maxMouseSamples;
|
|
36
|
+
private readonly target;
|
|
37
|
+
private readonly mousePoints;
|
|
38
|
+
private readonly clickTimes;
|
|
39
|
+
private keyStrokeCount;
|
|
40
|
+
private pointerActivity;
|
|
41
|
+
private startedAt;
|
|
42
|
+
private listeners;
|
|
43
|
+
constructor(options?: BrowserCollectorOptions);
|
|
44
|
+
/** Start listening for user events. Returns a stop function. */
|
|
45
|
+
start(): () => void;
|
|
46
|
+
stop(): void;
|
|
47
|
+
/** Build a snapshot of collected signals. */
|
|
48
|
+
getSnapshot(): BrowserSignalSnapshot;
|
|
49
|
+
/** Push collected signals onto a Guardian instance. */
|
|
50
|
+
applyTo(guardian: guardian_risk.Guardian): guardian_risk.Guardian;
|
|
51
|
+
private addListener;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Options for the browser plugin. */
|
|
55
|
+
interface BrowserPluginOptions extends BrowserCollectorOptions {
|
|
56
|
+
/** Auto-start collection on install (browser only). */
|
|
57
|
+
readonly autoStart?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Browser plugin — behavioral rules with a capped `behavior` group.
|
|
61
|
+
* Use {@link createBrowserCollector} to collect signals before analyze.
|
|
14
62
|
*/
|
|
15
63
|
declare function browserPlugin(options?: BrowserPluginOptions): Plugin;
|
|
16
64
|
/**
|
|
17
|
-
*
|
|
65
|
+
* Create a browser signal collector. Call `start()`, then `applyTo(guardian)` before analyze.
|
|
66
|
+
*/
|
|
67
|
+
declare function createBrowserCollector(options?: BrowserCollectorOptions): BrowserCollector;
|
|
68
|
+
/**
|
|
69
|
+
* Collect signals then apply to Guardian. Optional `durationMs` to wait before sampling.
|
|
18
70
|
*/
|
|
19
|
-
declare function collectSignals(guardian: guardian_risk.Guardian
|
|
71
|
+
declare function collectSignals(guardian: guardian_risk.Guardian, options?: BrowserCollectorOptions & {
|
|
72
|
+
durationMs?: number;
|
|
73
|
+
}): Promise<guardian_risk.Guardian>;
|
|
20
74
|
|
|
21
|
-
export { type BrowserPluginOptions, browserPlugin, collectSignals };
|
|
75
|
+
export { BrowserCollector, type BrowserCollectorOptions, type BrowserPluginOptions, type BrowserSignalSnapshot, browserPlugin, collectSignals, computeMouseLinearity, createBrowserCollector };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,75 @@
|
|
|
1
1
|
import * as guardian_risk from 'guardian-risk';
|
|
2
2
|
import { Plugin } from 'guardian-risk';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
interface
|
|
6
|
-
|
|
7
|
-
readonly
|
|
4
|
+
/** Collected behavioral signals from the browser. */
|
|
5
|
+
interface BrowserSignalSnapshot {
|
|
6
|
+
readonly mouseSampleCount: number;
|
|
7
|
+
readonly mouseLinearity: number;
|
|
8
|
+
readonly clickCount: number;
|
|
9
|
+
readonly avgClickIntervalMs: number;
|
|
10
|
+
readonly keyStrokeCount: number;
|
|
11
|
+
readonly hasPointerActivity: boolean;
|
|
12
|
+
readonly collectionDurationMs: number;
|
|
8
13
|
}
|
|
14
|
+
/** Options for browser signal collection. */
|
|
15
|
+
interface BrowserCollectorOptions {
|
|
16
|
+
/** Max mouse samples to retain. */
|
|
17
|
+
readonly maxMouseSamples?: number;
|
|
18
|
+
/** Target element or document root. */
|
|
19
|
+
readonly target?: EventTarget;
|
|
20
|
+
}
|
|
21
|
+
interface Point {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
t: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute how linear a mouse path is (0 = erratic, 1 = perfectly straight).
|
|
28
|
+
* High values often indicate scripted movement.
|
|
29
|
+
*/
|
|
30
|
+
declare function computeMouseLinearity(points: readonly Point[]): number;
|
|
9
31
|
/**
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
32
|
+
* Collects mouse, click, and keyboard behavioral signals in the browser.
|
|
33
|
+
*/
|
|
34
|
+
declare class BrowserCollector {
|
|
35
|
+
private readonly maxMouseSamples;
|
|
36
|
+
private readonly target;
|
|
37
|
+
private readonly mousePoints;
|
|
38
|
+
private readonly clickTimes;
|
|
39
|
+
private keyStrokeCount;
|
|
40
|
+
private pointerActivity;
|
|
41
|
+
private startedAt;
|
|
42
|
+
private listeners;
|
|
43
|
+
constructor(options?: BrowserCollectorOptions);
|
|
44
|
+
/** Start listening for user events. Returns a stop function. */
|
|
45
|
+
start(): () => void;
|
|
46
|
+
stop(): void;
|
|
47
|
+
/** Build a snapshot of collected signals. */
|
|
48
|
+
getSnapshot(): BrowserSignalSnapshot;
|
|
49
|
+
/** Push collected signals onto a Guardian instance. */
|
|
50
|
+
applyTo(guardian: guardian_risk.Guardian): guardian_risk.Guardian;
|
|
51
|
+
private addListener;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Options for the browser plugin. */
|
|
55
|
+
interface BrowserPluginOptions extends BrowserCollectorOptions {
|
|
56
|
+
/** Auto-start collection on install (browser only). */
|
|
57
|
+
readonly autoStart?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Browser plugin — behavioral rules with a capped `behavior` group.
|
|
61
|
+
* Use {@link createBrowserCollector} to collect signals before analyze.
|
|
14
62
|
*/
|
|
15
63
|
declare function browserPlugin(options?: BrowserPluginOptions): Plugin;
|
|
16
64
|
/**
|
|
17
|
-
*
|
|
65
|
+
* Create a browser signal collector. Call `start()`, then `applyTo(guardian)` before analyze.
|
|
66
|
+
*/
|
|
67
|
+
declare function createBrowserCollector(options?: BrowserCollectorOptions): BrowserCollector;
|
|
68
|
+
/**
|
|
69
|
+
* Collect signals then apply to Guardian. Optional `durationMs` to wait before sampling.
|
|
18
70
|
*/
|
|
19
|
-
declare function collectSignals(guardian: guardian_risk.Guardian
|
|
71
|
+
declare function collectSignals(guardian: guardian_risk.Guardian, options?: BrowserCollectorOptions & {
|
|
72
|
+
durationMs?: number;
|
|
73
|
+
}): Promise<guardian_risk.Guardian>;
|
|
20
74
|
|
|
21
|
-
export { type BrowserPluginOptions, browserPlugin, collectSignals };
|
|
75
|
+
export { BrowserCollector, type BrowserCollectorOptions, type BrowserPluginOptions, type BrowserSignalSnapshot, browserPlugin, collectSignals, computeMouseLinearity, createBrowserCollector };
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,184 @@
|
|
|
1
|
+
// src/collector.ts
|
|
2
|
+
function computeMouseLinearity(points) {
|
|
3
|
+
if (points.length < 3) {
|
|
4
|
+
return 0;
|
|
5
|
+
}
|
|
6
|
+
const first = points[0];
|
|
7
|
+
const last = points[points.length - 1];
|
|
8
|
+
const straightDistance = distance(first, last);
|
|
9
|
+
if (straightDistance === 0) {
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
let pathDistance = 0;
|
|
13
|
+
for (let i = 1; i < points.length; i++) {
|
|
14
|
+
pathDistance += distance(points[i - 1], points[i]);
|
|
15
|
+
}
|
|
16
|
+
if (pathDistance === 0) {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
const linearity = straightDistance / pathDistance;
|
|
20
|
+
return Math.min(1, Math.max(0, linearity));
|
|
21
|
+
}
|
|
22
|
+
function distance(a, b) {
|
|
23
|
+
const dx = b.x - a.x;
|
|
24
|
+
const dy = b.y - a.y;
|
|
25
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
26
|
+
}
|
|
27
|
+
var BrowserCollector = class {
|
|
28
|
+
maxMouseSamples;
|
|
29
|
+
target;
|
|
30
|
+
mousePoints = [];
|
|
31
|
+
clickTimes = [];
|
|
32
|
+
keyStrokeCount = 0;
|
|
33
|
+
pointerActivity = false;
|
|
34
|
+
startedAt = 0;
|
|
35
|
+
listeners = [];
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
this.maxMouseSamples = options.maxMouseSamples ?? 100;
|
|
38
|
+
this.target = options.target ?? getDefaultTarget();
|
|
39
|
+
}
|
|
40
|
+
/** Start listening for user events. Returns a stop function. */
|
|
41
|
+
start() {
|
|
42
|
+
if (!this.target) {
|
|
43
|
+
return () => {
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
this.startedAt = Date.now();
|
|
47
|
+
const onMouseMove = (event) => {
|
|
48
|
+
const e = event;
|
|
49
|
+
this.mousePoints.push({ x: e.clientX, y: e.clientY, t: Date.now() });
|
|
50
|
+
if (this.mousePoints.length > this.maxMouseSamples) {
|
|
51
|
+
this.mousePoints.shift();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const onClick = () => {
|
|
55
|
+
this.clickTimes.push(Date.now());
|
|
56
|
+
};
|
|
57
|
+
const onKeyDown = () => {
|
|
58
|
+
this.keyStrokeCount += 1;
|
|
59
|
+
};
|
|
60
|
+
const onPointer = () => {
|
|
61
|
+
this.pointerActivity = true;
|
|
62
|
+
};
|
|
63
|
+
this.addListener("mousemove", onMouseMove);
|
|
64
|
+
this.addListener("click", onClick);
|
|
65
|
+
this.addListener("keydown", onKeyDown);
|
|
66
|
+
this.addListener("pointerdown", onPointer);
|
|
67
|
+
this.addListener("touchstart", onPointer);
|
|
68
|
+
return () => this.stop();
|
|
69
|
+
}
|
|
70
|
+
stop() {
|
|
71
|
+
if (!this.target) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (const { type, handler } of this.listeners) {
|
|
75
|
+
this.target.removeEventListener(type, handler);
|
|
76
|
+
}
|
|
77
|
+
this.listeners = [];
|
|
78
|
+
}
|
|
79
|
+
/** Build a snapshot of collected signals. */
|
|
80
|
+
getSnapshot() {
|
|
81
|
+
const intervals = [];
|
|
82
|
+
for (let i = 1; i < this.clickTimes.length; i++) {
|
|
83
|
+
intervals.push(this.clickTimes[i] - this.clickTimes[i - 1]);
|
|
84
|
+
}
|
|
85
|
+
const avgClickIntervalMs = intervals.length > 0 ? intervals.reduce((sum, n) => sum + n, 0) / intervals.length : 0;
|
|
86
|
+
return {
|
|
87
|
+
mouseSampleCount: this.mousePoints.length,
|
|
88
|
+
mouseLinearity: computeMouseLinearity(this.mousePoints),
|
|
89
|
+
clickCount: this.clickTimes.length,
|
|
90
|
+
avgClickIntervalMs,
|
|
91
|
+
keyStrokeCount: this.keyStrokeCount,
|
|
92
|
+
hasPointerActivity: this.pointerActivity || this.mousePoints.length > 0,
|
|
93
|
+
collectionDurationMs: this.startedAt > 0 ? Date.now() - this.startedAt : 0
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Push collected signals onto a Guardian instance. */
|
|
97
|
+
applyTo(guardian) {
|
|
98
|
+
const snapshot = this.getSnapshot();
|
|
99
|
+
return guardian.signal("mouseSampleCount", snapshot.mouseSampleCount).signal("mouseLinearity", round(snapshot.mouseLinearity, 4)).signal("clickCount", snapshot.clickCount).signal("avgClickIntervalMs", Math.round(snapshot.avgClickIntervalMs)).signal("keyStrokeCount", snapshot.keyStrokeCount).signal("hasPointerActivity", snapshot.hasPointerActivity).signal("collectionDurationMs", snapshot.collectionDurationMs).signal("signalSource", "browser");
|
|
100
|
+
}
|
|
101
|
+
addListener(type, handler) {
|
|
102
|
+
if (!this.target) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.target.addEventListener(type, handler, { passive: true });
|
|
106
|
+
this.listeners.push({ type, handler });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
function getDefaultTarget() {
|
|
110
|
+
if (typeof document !== "undefined") {
|
|
111
|
+
return document;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
function round(value, digits) {
|
|
116
|
+
const factor = 10 ** digits;
|
|
117
|
+
return Math.round(value * factor) / factor;
|
|
118
|
+
}
|
|
119
|
+
|
|
1
120
|
// src/index.ts
|
|
2
121
|
function browserPlugin(options = {}) {
|
|
3
|
-
const {
|
|
122
|
+
const { autoStart = false, ...collectorOptions } = options;
|
|
4
123
|
return {
|
|
5
124
|
name: "guardian-risk-browser",
|
|
6
|
-
install(
|
|
125
|
+
install(guardian) {
|
|
126
|
+
guardian.ruleGroup({
|
|
127
|
+
name: "behavior",
|
|
128
|
+
maxScore: 50,
|
|
129
|
+
rules: [
|
|
130
|
+
{
|
|
131
|
+
name: "LinearMouse",
|
|
132
|
+
reason: "Mouse movement is unnaturally linear",
|
|
133
|
+
when: (s) => s.mouseLinearity > 0.92,
|
|
134
|
+
score: 25
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "NoMouseActivity",
|
|
138
|
+
reason: "No pointer or mouse activity detected before submit",
|
|
139
|
+
when: (s) => s.mouseSampleCount === 0 && s.hasPointerActivity !== true,
|
|
140
|
+
score: 20
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "RoboticClicks",
|
|
144
|
+
reason: "Click timing is unnaturally regular",
|
|
145
|
+
when: (s) => {
|
|
146
|
+
const interval = s.avgClickIntervalMs;
|
|
147
|
+
const count = s.clickCount;
|
|
148
|
+
return count >= 3 && interval > 0 && interval < 50;
|
|
149
|
+
},
|
|
150
|
+
score: 30
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
});
|
|
154
|
+
if (autoStart && typeof document !== "undefined") {
|
|
155
|
+
const collector = new BrowserCollector(collectorOptions);
|
|
156
|
+
const stop = collector.start();
|
|
157
|
+
guardian.beforeAnalyze(({ guardian: g }) => {
|
|
158
|
+
collector.applyTo(g);
|
|
159
|
+
stop();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
7
162
|
}
|
|
8
163
|
};
|
|
9
164
|
}
|
|
10
|
-
function
|
|
11
|
-
return
|
|
165
|
+
function createBrowserCollector(options = {}) {
|
|
166
|
+
return new BrowserCollector(options);
|
|
167
|
+
}
|
|
168
|
+
function collectSignals(guardian, options = {}) {
|
|
169
|
+
const { durationMs = 0, ...collectorOptions } = options;
|
|
170
|
+
const collector = new BrowserCollector(collectorOptions);
|
|
171
|
+
const stop = collector.start();
|
|
172
|
+
const finish = () => {
|
|
173
|
+
stop();
|
|
174
|
+
return collector.applyTo(guardian);
|
|
175
|
+
};
|
|
176
|
+
if (durationMs <= 0) {
|
|
177
|
+
return Promise.resolve(finish());
|
|
178
|
+
}
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
setTimeout(() => resolve(finish()), durationMs);
|
|
181
|
+
});
|
|
12
182
|
}
|
|
13
183
|
|
|
14
|
-
export { browserPlugin, collectSignals };
|
|
15
|
-
//# sourceMappingURL=index.js.map
|
|
16
|
-
//# sourceMappingURL=index.js.map
|
|
184
|
+
export { BrowserCollector, browserPlugin, collectSignals, computeMouseLinearity, createBrowserCollector };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardian-risk-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Browser plugin for guardian-risk — collects behavioral and fingerprint signals",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -20,10 +20,16 @@
|
|
|
20
20
|
},
|
|
21
21
|
"sideEffects": false,
|
|
22
22
|
"files": [
|
|
23
|
-
"dist",
|
|
23
|
+
"dist/index.js",
|
|
24
|
+
"dist/index.cjs",
|
|
25
|
+
"dist/index.d.ts",
|
|
26
|
+
"dist/index.d.cts",
|
|
24
27
|
"README.md",
|
|
25
28
|
"LICENSE"
|
|
26
29
|
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
27
33
|
"keywords": [
|
|
28
34
|
"guardian-risk",
|
|
29
35
|
"guardian-risk-browser",
|
|
@@ -32,6 +38,7 @@
|
|
|
32
38
|
"bot-detection",
|
|
33
39
|
"risk"
|
|
34
40
|
],
|
|
41
|
+
"author": "himanshuusinghh",
|
|
35
42
|
"license": "MIT",
|
|
36
43
|
"repository": {
|
|
37
44
|
"type": "git",
|
|
@@ -42,19 +49,22 @@
|
|
|
42
49
|
"bugs": {
|
|
43
50
|
"url": "https://github.com/himanshu6306singh/guardian-risk/issues"
|
|
44
51
|
},
|
|
52
|
+
"security": "https://github.com/himanshu6306singh/guardian-risk/security/policy",
|
|
45
53
|
"publishConfig": {
|
|
46
54
|
"access": "public"
|
|
47
55
|
},
|
|
48
56
|
"peerDependencies": {
|
|
49
|
-
"guardian-risk": "^0.
|
|
57
|
+
"guardian-risk": "^0.3.0"
|
|
50
58
|
},
|
|
51
59
|
"devDependencies": {
|
|
52
60
|
"tsup": "^8.3.5",
|
|
53
61
|
"typescript": "^5.7.2",
|
|
54
|
-
"
|
|
62
|
+
"vitest": "^4.1.9",
|
|
63
|
+
"guardian-risk": "0.3.0"
|
|
55
64
|
},
|
|
56
65
|
"scripts": {
|
|
57
66
|
"build": "tsup",
|
|
67
|
+
"test": "vitest run",
|
|
58
68
|
"typecheck": "tsc --noEmit"
|
|
59
69
|
}
|
|
60
70
|
}
|
package/dist/index.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAcO,SAAS,aAAA,CAAc,OAAA,GAAgC,EAAC,EAAW;AACxE,EAAA,MAAM,EAAE,SAAA,GAAY,UAAA,EAAW,GAAI,OAAA;AAEnC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,uBAAA;AAAA,IACN,QAAQ,SAAA,EAAW;AACZ,IAEP;AAAA,GACF;AACF;AAKO,SAAS,eACd,QAAA,EACkC;AAClC,EAAA,OAAO,SACJ,MAAA,CAAO,cAAA,EAAgB,SAAS,CAAA,CAChC,MAAA,CAAO,iBAAiB,MAAM,CAAA;AACnC","file":"index.cjs","sourcesContent":["import type { Plugin } from 'guardian-risk';\n\n/** Options for the browser plugin (stub). */\nexport interface BrowserPluginOptions {\n /** DOM element to attach behavioral collectors. Defaults to document. */\n readonly container?: string;\n}\n\n/**\n * Browser plugin for guardian-risk.\n *\n * @stub Future versions will collect mouse, keyboard, and fingerprint signals\n * in the page and add them to Guardian before analysis.\n */\nexport function browserPlugin(options: BrowserPluginOptions = {}): Plugin {\n const { container = 'document' } = options;\n\n return {\n name: 'guardian-risk-browser',\n install(_guardian) {\n void container;\n // Stub: collectors will attach listeners and call guardian.signal()\n },\n };\n}\n\n/**\n * @stub Future helper to start client-side signal collection.\n */\nexport function collectSignals(\n guardian: import('guardian-risk').Guardian,\n): import('guardian-risk').Guardian {\n return guardian\n .signal('signalSource', 'browser')\n .signal('browserPlugin', 'stub');\n}\n"]}
|
package/dist/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";AAcO,SAAS,aAAA,CAAc,OAAA,GAAgC,EAAC,EAAW;AACxE,EAAA,MAAM,EAAE,SAAA,GAAY,UAAA,EAAW,GAAI,OAAA;AAEnC,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,uBAAA;AAAA,IACN,QAAQ,SAAA,EAAW;AACZ,IAEP;AAAA,GACF;AACF;AAKO,SAAS,eACd,QAAA,EACkC;AAClC,EAAA,OAAO,SACJ,MAAA,CAAO,cAAA,EAAgB,SAAS,CAAA,CAChC,MAAA,CAAO,iBAAiB,MAAM,CAAA;AACnC","file":"index.js","sourcesContent":["import type { Plugin } from 'guardian-risk';\n\n/** Options for the browser plugin (stub). */\nexport interface BrowserPluginOptions {\n /** DOM element to attach behavioral collectors. Defaults to document. */\n readonly container?: string;\n}\n\n/**\n * Browser plugin for guardian-risk.\n *\n * @stub Future versions will collect mouse, keyboard, and fingerprint signals\n * in the page and add them to Guardian before analysis.\n */\nexport function browserPlugin(options: BrowserPluginOptions = {}): Plugin {\n const { container = 'document' } = options;\n\n return {\n name: 'guardian-risk-browser',\n install(_guardian) {\n void container;\n // Stub: collectors will attach listeners and call guardian.signal()\n },\n };\n}\n\n/**\n * @stub Future helper to start client-side signal collection.\n */\nexport function collectSignals(\n guardian: import('guardian-risk').Guardian,\n): import('guardian-risk').Guardian {\n return guardian\n .signal('signalSource', 'browser')\n .signal('browserPlugin', 'stub');\n}\n"]}
|