@toffee-at/sdk 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/.turbo/turbo-build.log +23 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.global.js +1 -0
- package/dist/index.js +1 -0
- package/package.json +31 -0
- package/src/core.ts +196 -0
- package/src/detect/automation.ts +34 -0
- package/src/detect/behavioral.ts +382 -0
- package/src/detect/features.ts +126 -0
- package/src/detect/fingerprint.ts +115 -0
- package/src/detect/headless.ts +44 -0
- package/src/detect/index.ts +202 -0
- package/src/detect/navigator.ts +102 -0
- package/src/detect/types.ts +52 -0
- package/src/detect/user-agent.ts +21 -0
- package/src/index.ts +22 -0
- package/src/track/elements.ts +105 -0
- package/src/track/index.ts +15 -0
- package/src/track/page.ts +43 -0
- package/src/track/session.ts +19 -0
- package/src/track/types.ts +19 -0
- package/src/transport/batch.ts +119 -0
- package/src/transport/index.ts +60 -0
- package/tests/features.test.ts +94 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +10 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const BATCH_INTERVAL_MS = 5000;
|
|
2
|
+
const BATCH_MAX_SIZE = 20;
|
|
3
|
+
|
|
4
|
+
export interface EventBatch {
|
|
5
|
+
siteId: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
events: unknown[];
|
|
8
|
+
metadata?: Record<string, string>;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class BatchQueue {
|
|
13
|
+
private queue: unknown[] = [];
|
|
14
|
+
private timer: ReturnType<typeof setInterval> | null = null;
|
|
15
|
+
private endpoint: string;
|
|
16
|
+
private siteId: string;
|
|
17
|
+
private sessionId: string;
|
|
18
|
+
private apiKey: string;
|
|
19
|
+
private metadata?: Record<string, string>;
|
|
20
|
+
private onClassification?: (classifications: Record<string, { classification: string; probabilities: { human: number; bot: number; agent: number } }>) => void;
|
|
21
|
+
|
|
22
|
+
constructor(opts: {
|
|
23
|
+
endpoint: string;
|
|
24
|
+
siteId: string;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
apiKey: string;
|
|
27
|
+
onClassification?: (classifications: Record<string, { classification: string; probabilities: { human: number; bot: number; agent: number } }>) => void;
|
|
28
|
+
}) {
|
|
29
|
+
this.endpoint = opts.endpoint;
|
|
30
|
+
this.siteId = opts.siteId;
|
|
31
|
+
this.sessionId = opts.sessionId;
|
|
32
|
+
this.apiKey = opts.apiKey;
|
|
33
|
+
this.onClassification = opts.onClassification;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
start() {
|
|
37
|
+
this.timer = setInterval(() => this.flush(), BATCH_INTERVAL_MS);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
stop() {
|
|
41
|
+
if (this.timer) {
|
|
42
|
+
clearInterval(this.timer);
|
|
43
|
+
this.timer = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
setMetadata(metadata: Record<string, string>) {
|
|
48
|
+
this.metadata = metadata;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
push(event: unknown) {
|
|
52
|
+
this.queue.push(event);
|
|
53
|
+
if (this.queue.length >= BATCH_MAX_SIZE) {
|
|
54
|
+
this.flush();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
flush() {
|
|
59
|
+
if (this.queue.length === 0) return;
|
|
60
|
+
|
|
61
|
+
const batch: EventBatch = {
|
|
62
|
+
siteId: this.siteId,
|
|
63
|
+
sessionId: this.sessionId,
|
|
64
|
+
events: this.queue.splice(0),
|
|
65
|
+
metadata: this.metadata,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const body = JSON.stringify(batch);
|
|
70
|
+
this.send(body);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
flushBeacon() {
|
|
74
|
+
if (this.queue.length === 0) return;
|
|
75
|
+
|
|
76
|
+
const batch: EventBatch = {
|
|
77
|
+
siteId: this.siteId,
|
|
78
|
+
sessionId: this.sessionId,
|
|
79
|
+
events: this.queue.splice(0),
|
|
80
|
+
metadata: this.metadata,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const body = JSON.stringify(batch);
|
|
85
|
+
// sendBeacon doesn't support custom headers, so append apiKey as query param
|
|
86
|
+
const url = `${this.endpoint}?apiKey=${encodeURIComponent(this.apiKey)}`;
|
|
87
|
+
navigator.sendBeacon(url, body);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private send(body: string) {
|
|
91
|
+
try {
|
|
92
|
+
fetch(this.endpoint, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
'X-Api-Key': this.apiKey,
|
|
97
|
+
},
|
|
98
|
+
body,
|
|
99
|
+
keepalive: true,
|
|
100
|
+
})
|
|
101
|
+
.then((resp) => {
|
|
102
|
+
if (resp.ok && this.onClassification) {
|
|
103
|
+
return resp.json();
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
})
|
|
107
|
+
.then((data) => {
|
|
108
|
+
if (data?.classifications && this.onClassification) {
|
|
109
|
+
this.onClassification(data.classifications);
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
.catch(() => {
|
|
113
|
+
// Network error — SDK continues with heuristic fallback
|
|
114
|
+
});
|
|
115
|
+
} catch {
|
|
116
|
+
// silently drop
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BatchQueue } from './batch';
|
|
2
|
+
|
|
3
|
+
export function createTransport(opts: {
|
|
4
|
+
endpoint: string;
|
|
5
|
+
siteId: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
onClassification?: (classifications: Record<string, { classification: string; probabilities: { human: number; bot: number; agent: number } }>) => void;
|
|
9
|
+
}) {
|
|
10
|
+
const batch = new BatchQueue(opts);
|
|
11
|
+
batch.start();
|
|
12
|
+
|
|
13
|
+
let sessionStart = Date.now();
|
|
14
|
+
let pageCount = 0;
|
|
15
|
+
let interactionCount = 0;
|
|
16
|
+
|
|
17
|
+
const onVisibilityChange = () => {
|
|
18
|
+
if (document.visibilityState === 'hidden') {
|
|
19
|
+
emitSessionEnd();
|
|
20
|
+
batch.flushBeacon();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const onPageHide = () => {
|
|
25
|
+
emitSessionEnd();
|
|
26
|
+
batch.flushBeacon();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function emitSessionEnd() {
|
|
30
|
+
batch.push({
|
|
31
|
+
type: 'session_end',
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
url: location.href,
|
|
34
|
+
duration: Date.now() - sessionStart,
|
|
35
|
+
pageCount,
|
|
36
|
+
interactionCount,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
41
|
+
window.addEventListener('pagehide', onPageHide);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
push: (event: unknown) => {
|
|
45
|
+
const e = event as { type?: string };
|
|
46
|
+
if (e.type === 'pageview') pageCount++;
|
|
47
|
+
if (e.type === 'interaction') interactionCount++;
|
|
48
|
+
batch.push(event);
|
|
49
|
+
},
|
|
50
|
+
setMetadata: (metadata: Record<string, string>) => batch.setMetadata(metadata),
|
|
51
|
+
flush: () => batch.flush(),
|
|
52
|
+
destroy: () => {
|
|
53
|
+
batch.stop();
|
|
54
|
+
emitSessionEnd();
|
|
55
|
+
batch.flush();
|
|
56
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
57
|
+
window.removeEventListener('pagehide', onPageHide);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractModelFeatures } from '../src/detect/features';
|
|
3
|
+
|
|
4
|
+
describe('extractModelFeatures', () => {
|
|
5
|
+
it('returns zeros for empty state', () => {
|
|
6
|
+
const result = extractModelFeatures({
|
|
7
|
+
mousePoints: [],
|
|
8
|
+
clicks: [],
|
|
9
|
+
clickDurations: [],
|
|
10
|
+
scrollTimestamps: [],
|
|
11
|
+
keyTimings: [],
|
|
12
|
+
allEventTimestamps: [],
|
|
13
|
+
});
|
|
14
|
+
expect(result.action_burst_ratio).toBe(0);
|
|
15
|
+
expect(result.click_duration_std).toBe(0);
|
|
16
|
+
expect(result.inter_event_entropy).toBe(0);
|
|
17
|
+
expect(result.mouse_teleportation).toBe(0);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('detects mouse teleportation (jumps > 100px)', () => {
|
|
21
|
+
const result = extractModelFeatures({
|
|
22
|
+
mousePoints: [
|
|
23
|
+
{ x: 0, y: 0, t: 0 },
|
|
24
|
+
{ x: 200, y: 0, t: 100 },
|
|
25
|
+
{ x: 210, y: 0, t: 200 },
|
|
26
|
+
{ x: 500, y: 0, t: 300 },
|
|
27
|
+
],
|
|
28
|
+
clicks: [],
|
|
29
|
+
clickDurations: [],
|
|
30
|
+
scrollTimestamps: [],
|
|
31
|
+
keyTimings: [],
|
|
32
|
+
allEventTimestamps: [],
|
|
33
|
+
});
|
|
34
|
+
expect(result.mouse_teleportation).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('computes action burst ratio', () => {
|
|
38
|
+
const result = extractModelFeatures({
|
|
39
|
+
mousePoints: [],
|
|
40
|
+
clicks: [{ x: 0, y: 0, t: 100 }, { x: 0, y: 0, t: 200 }],
|
|
41
|
+
clickDurations: [],
|
|
42
|
+
scrollTimestamps: [300],
|
|
43
|
+
keyTimings: [400],
|
|
44
|
+
allEventTimestamps: [],
|
|
45
|
+
});
|
|
46
|
+
expect(result.action_burst_ratio).toBe(1.0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('computes click duration std', () => {
|
|
50
|
+
const result = extractModelFeatures({
|
|
51
|
+
mousePoints: [],
|
|
52
|
+
clicks: [],
|
|
53
|
+
clickDurations: [100, 100, 100],
|
|
54
|
+
scrollTimestamps: [],
|
|
55
|
+
keyTimings: [],
|
|
56
|
+
allEventTimestamps: [],
|
|
57
|
+
});
|
|
58
|
+
expect(result.click_duration_std).toBe(0);
|
|
59
|
+
|
|
60
|
+
const result2 = extractModelFeatures({
|
|
61
|
+
mousePoints: [],
|
|
62
|
+
clicks: [],
|
|
63
|
+
clickDurations: [50, 150],
|
|
64
|
+
scrollTimestamps: [],
|
|
65
|
+
keyTimings: [],
|
|
66
|
+
allEventTimestamps: [],
|
|
67
|
+
});
|
|
68
|
+
expect(result2.click_duration_std).toBe(50);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('computes inter-event entropy > 0 for varied gaps', () => {
|
|
72
|
+
const result = extractModelFeatures({
|
|
73
|
+
mousePoints: [],
|
|
74
|
+
clicks: [],
|
|
75
|
+
clickDurations: [],
|
|
76
|
+
scrollTimestamps: [],
|
|
77
|
+
keyTimings: [],
|
|
78
|
+
allEventTimestamps: [0, 10, 50, 200, 201, 500, 2000, 2001, 5000],
|
|
79
|
+
});
|
|
80
|
+
expect(result.inter_event_entropy).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns entropy 0 for uniform gaps', () => {
|
|
84
|
+
const result = extractModelFeatures({
|
|
85
|
+
mousePoints: [],
|
|
86
|
+
clicks: [],
|
|
87
|
+
clickDurations: [],
|
|
88
|
+
scrollTimestamps: [],
|
|
89
|
+
keyTimings: [],
|
|
90
|
+
allEventTimestamps: [0, 100, 200, 300, 400],
|
|
91
|
+
});
|
|
92
|
+
expect(result.inter_event_entropy).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
});
|
package/tsconfig.json
ADDED