engagelab-captcha-sdk 1.0.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/captcha-sdk.esm.js +1 -0
- package/dist/captcha-sdk.umd.js +1 -0
- package/dist/types/api.d.ts +12 -0
- package/dist/types/challenges/dragsort.d.ts +24 -0
- package/dist/types/challenges/grid.d.ts +20 -0
- package/dist/types/challenges/icons.d.ts +18 -0
- package/dist/types/challenges/invisible.d.ts +8 -0
- package/dist/types/challenges/physics.d.ts +40 -0
- package/dist/types/challenges/rotate.d.ts +14 -0
- package/dist/types/challenges/slider.d.ts +17 -0
- package/dist/types/challenges/spatial.d.ts +12 -0
- package/dist/types/compat/hcaptcha.d.ts +1 -0
- package/dist/types/compat/recaptcha.d.ts +1 -0
- package/dist/types/crypto.d.ts +11 -0
- package/dist/types/deep_probes.d.ts +13 -0
- package/dist/types/degradation.d.ts +27 -0
- package/dist/types/device_attestation.d.ts +19 -0
- package/dist/types/fingerprint.d.ts +2 -0
- package/dist/types/headless.d.ts +2 -0
- package/dist/types/i18n.d.ts +7 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/integrity.d.ts +3 -0
- package/dist/types/logger.d.ts +49 -0
- package/dist/types/native_monitor.d.ts +21 -0
- package/dist/types/passive_signals.d.ts +119 -0
- package/dist/types/pow.d.ts +2 -0
- package/dist/types/reporter.d.ts +8 -0
- package/dist/types/session_persist.d.ts +18 -0
- package/dist/types/theme.d.ts +11 -0
- package/dist/types/timing.d.ts +2 -0
- package/dist/types/trajectory.d.ts +18 -0
- package/dist/types/types.d.ts +182 -0
- package/dist/types/ui/container.d.ts +20 -0
- package/dist/types/ui/styles.d.ts +1 -0
- package/dist/types/wasm_core.d.ts +44 -0
- package/dist/types/web-component.d.ts +11 -0
- package/package.json +22 -0
- package/rollup.config.mjs +62 -0
- package/src/api.ts +180 -0
- package/src/challenges/dragsort.ts +168 -0
- package/src/challenges/grid.ts +147 -0
- package/src/challenges/icons.ts +106 -0
- package/src/challenges/invisible.ts +57 -0
- package/src/challenges/physics.ts +437 -0
- package/src/challenges/rotate.ts +81 -0
- package/src/challenges/slider.ts +168 -0
- package/src/challenges/spatial.ts +91 -0
- package/src/compat/hcaptcha.ts +112 -0
- package/src/compat/recaptcha.ts +108 -0
- package/src/crypto.ts +69 -0
- package/src/deep_probes.ts +690 -0
- package/src/degradation.ts +113 -0
- package/src/device_attestation.ts +109 -0
- package/src/fingerprint.ts +247 -0
- package/src/headless.ts +233 -0
- package/src/i18n.ts +455 -0
- package/src/index.ts +527 -0
- package/src/integrity.ts +100 -0
- package/src/logger.ts +170 -0
- package/src/native_monitor.ts +100 -0
- package/src/passive_signals.ts +544 -0
- package/src/pow.ts +120 -0
- package/src/reporter.ts +75 -0
- package/src/session_persist.ts +90 -0
- package/src/theme.ts +41 -0
- package/src/timing.ts +79 -0
- package/src/trajectory.ts +110 -0
- package/src/types.ts +199 -0
- package/src/ui/container.ts +161 -0
- package/src/ui/styles.ts +153 -0
- package/src/wasm_core.ts +189 -0
- package/src/web-component.ts +103 -0
- package/tsconfig.json +18 -0
package/src/reporter.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error reporting and performance monitoring for the SDK.
|
|
3
|
+
* Collects exceptions and performance metrics, sends via beacon.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface ErrorReport {
|
|
7
|
+
type: 'error' | 'perf';
|
|
8
|
+
timestamp: number;
|
|
9
|
+
message: string;
|
|
10
|
+
stack?: string;
|
|
11
|
+
context?: Record<string, string | number>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let reportEndpoint = '';
|
|
15
|
+
let siteKey = '';
|
|
16
|
+
const buffer: ErrorReport[] = [];
|
|
17
|
+
const MAX_BUFFER = 20;
|
|
18
|
+
const FLUSH_INTERVAL = 30000;
|
|
19
|
+
let flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
20
|
+
|
|
21
|
+
export function initReporter(endpoint: string, key: string): void {
|
|
22
|
+
reportEndpoint = endpoint;
|
|
23
|
+
siteKey = key;
|
|
24
|
+
if (!flushTimer) {
|
|
25
|
+
flushTimer = setInterval(flush, FLUSH_INTERVAL);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function reportError(error: Error, context?: Record<string, string | number>): void {
|
|
30
|
+
buffer.push({
|
|
31
|
+
type: 'error',
|
|
32
|
+
timestamp: Date.now(),
|
|
33
|
+
message: error.message,
|
|
34
|
+
stack: error.stack?.substring(0, 500),
|
|
35
|
+
context,
|
|
36
|
+
});
|
|
37
|
+
if (buffer.length >= MAX_BUFFER) flush();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function reportPerf(metric: string, durationMs: number): void {
|
|
41
|
+
buffer.push({
|
|
42
|
+
type: 'perf',
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
message: metric,
|
|
45
|
+
context: { durationMs },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function flush(): void {
|
|
50
|
+
if (!reportEndpoint || buffer.length === 0) return;
|
|
51
|
+
|
|
52
|
+
const payload = JSON.stringify({
|
|
53
|
+
siteKey,
|
|
54
|
+
reports: buffer.splice(0, MAX_BUFFER),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (navigator.sendBeacon) {
|
|
58
|
+
navigator.sendBeacon(reportEndpoint + '/v1/sdk/report', payload);
|
|
59
|
+
} else {
|
|
60
|
+
fetch(reportEndpoint + '/v1/sdk/report', {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
body: payload,
|
|
63
|
+
headers: { 'Content-Type': 'application/json' },
|
|
64
|
+
keepalive: true,
|
|
65
|
+
}).catch(() => {});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function destroyReporter(): void {
|
|
70
|
+
if (flushTimer) {
|
|
71
|
+
clearInterval(flushTimer);
|
|
72
|
+
flushTimer = null;
|
|
73
|
+
}
|
|
74
|
+
flush();
|
|
75
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK Session Persistence — store session metadata in IndexedDB for cross-page context.
|
|
3
|
+
*
|
|
4
|
+
* Persists: sessionCount, lastVerifyTime, fingerprintCache hash.
|
|
5
|
+
* Used to report returning-user signals to the risk engine.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DB_NAME = 'engagelab_captcha';
|
|
9
|
+
const STORE_NAME = 'session';
|
|
10
|
+
const DB_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
export interface SessionData {
|
|
13
|
+
sessionCount: number;
|
|
14
|
+
lastVerifyTime: number;
|
|
15
|
+
fingerprintHash: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function openDb(): Promise<IDBDatabase> {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
if (typeof indexedDB === 'undefined') {
|
|
21
|
+
reject(new Error('IndexedDB not available'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
25
|
+
request.onupgradeneeded = () => {
|
|
26
|
+
const db = request.result;
|
|
27
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
28
|
+
db.createObjectStore(STORE_NAME);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
request.onsuccess = () => resolve(request.result);
|
|
32
|
+
request.onerror = () => reject(request.error);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getItem(db: IDBDatabase, key: string): Promise<any> {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
39
|
+
const store = tx.objectStore(STORE_NAME);
|
|
40
|
+
const req = store.get(key);
|
|
41
|
+
req.onsuccess = () => resolve(req.result);
|
|
42
|
+
req.onerror = () => reject(req.error);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setItem(db: IDBDatabase, key: string, value: any): Promise<void> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
49
|
+
const store = tx.objectStore(STORE_NAME);
|
|
50
|
+
const req = store.put(value, key);
|
|
51
|
+
req.onsuccess = () => resolve();
|
|
52
|
+
req.onerror = () => reject(req.error);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class SessionPersist {
|
|
57
|
+
private db: IDBDatabase | null = null;
|
|
58
|
+
private data: SessionData = { sessionCount: 0, lastVerifyTime: 0, fingerprintHash: '' };
|
|
59
|
+
|
|
60
|
+
async load(): Promise<SessionData> {
|
|
61
|
+
try {
|
|
62
|
+
this.db = await openDb();
|
|
63
|
+
const stored = await getItem(this.db, 'session_data');
|
|
64
|
+
if (stored) {
|
|
65
|
+
this.data = { ...this.data, ...stored };
|
|
66
|
+
}
|
|
67
|
+
this.data.sessionCount++;
|
|
68
|
+
await setItem(this.db, 'session_data', this.data);
|
|
69
|
+
} catch {
|
|
70
|
+
// Silently degrade — persistence is not critical
|
|
71
|
+
}
|
|
72
|
+
return this.data;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async recordVerify(fingerprintHash: string): Promise<void> {
|
|
76
|
+
this.data.lastVerifyTime = Date.now();
|
|
77
|
+
this.data.fingerprintHash = fingerprintHash;
|
|
78
|
+
try {
|
|
79
|
+
if (this.db) {
|
|
80
|
+
await setItem(this.db, 'session_data', this.data);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Silently degrade
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getData(): SessionData {
|
|
88
|
+
return { ...this.data };
|
|
89
|
+
}
|
|
90
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface CaptchaTheme {
|
|
2
|
+
primaryColor?: string;
|
|
3
|
+
successColor?: string;
|
|
4
|
+
errorColor?: string;
|
|
5
|
+
backgroundColor?: string;
|
|
6
|
+
textColor?: string;
|
|
7
|
+
borderRadius?: number;
|
|
8
|
+
fontFamily?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_THEME: Required<CaptchaTheme> = {
|
|
12
|
+
primaryColor: '#1890ff',
|
|
13
|
+
successColor: '#52c41a',
|
|
14
|
+
errorColor: '#ff4d4f',
|
|
15
|
+
backgroundColor: '#ffffff',
|
|
16
|
+
textColor: '#333333',
|
|
17
|
+
borderRadius: 8,
|
|
18
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let activeTheme: Required<CaptchaTheme> = { ...DEFAULT_THEME };
|
|
22
|
+
|
|
23
|
+
export function setTheme(theme: CaptchaTheme): void {
|
|
24
|
+
activeTheme = { ...DEFAULT_THEME, ...theme };
|
|
25
|
+
applyThemeToDOM();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getTheme(): Required<CaptchaTheme> {
|
|
29
|
+
return activeTheme;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyThemeToDOM(): void {
|
|
33
|
+
const root = document.documentElement;
|
|
34
|
+
root.style.setProperty('--captcha-primary', activeTheme.primaryColor);
|
|
35
|
+
root.style.setProperty('--captcha-success', activeTheme.successColor);
|
|
36
|
+
root.style.setProperty('--captcha-error', activeTheme.errorColor);
|
|
37
|
+
root.style.setProperty('--captcha-bg', activeTheme.backgroundColor);
|
|
38
|
+
root.style.setProperty('--captcha-text', activeTheme.textColor);
|
|
39
|
+
root.style.setProperty('--captcha-radius', activeTheme.borderRadius + 'px');
|
|
40
|
+
root.style.setProperty('--captcha-font', activeTheme.fontFamily);
|
|
41
|
+
}
|
package/src/timing.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { TimingData } from './types';
|
|
2
|
+
import { sdkLogger } from './logger';
|
|
3
|
+
|
|
4
|
+
export function collectTimingData(sdkInitTime: number): TimingData {
|
|
5
|
+
const measurements = runJSBenchmarks();
|
|
6
|
+
|
|
7
|
+
let precision = 0;
|
|
8
|
+
const times: number[] = [];
|
|
9
|
+
for (let i = 0; i < 20; i++) {
|
|
10
|
+
const t1 = performance.now();
|
|
11
|
+
const t2 = performance.now();
|
|
12
|
+
if (t2 > t1) times.push(t2 - t1);
|
|
13
|
+
}
|
|
14
|
+
if (times.length > 0) {
|
|
15
|
+
precision = times.reduce((a, b) => a + b, 0) / times.length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const data: TimingData = {
|
|
19
|
+
domContentLoaded: performance.timing?.domContentLoadedEventEnd || 0,
|
|
20
|
+
sdkInitTime,
|
|
21
|
+
interactionStart: 0,
|
|
22
|
+
verificationComplete: 0,
|
|
23
|
+
jsMeasurements: measurements,
|
|
24
|
+
performanceNowPrecision: precision,
|
|
25
|
+
};
|
|
26
|
+
sdkLogger.debug('Timing', 'Collected', {
|
|
27
|
+
domContentLoaded: data.domContentLoaded,
|
|
28
|
+
performanceNowPrecision: precision,
|
|
29
|
+
benchmarks: measurements.map(m => `${m.operation}=${m.durationMs.toFixed(2)}ms`),
|
|
30
|
+
});
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runJSBenchmarks(): { operation: string; durationMs: number }[] {
|
|
35
|
+
const results: { operation: string; durationMs: number }[] = [];
|
|
36
|
+
|
|
37
|
+
// DOM operation benchmark
|
|
38
|
+
const t1 = performance.now();
|
|
39
|
+
const div = document.createElement('div');
|
|
40
|
+
for (let i = 0; i < 100; i++) {
|
|
41
|
+
div.setAttribute('data-test', String(i));
|
|
42
|
+
div.innerHTML = `<span>${i}</span>`;
|
|
43
|
+
}
|
|
44
|
+
results.push({ operation: 'dom_operation', durationMs: performance.now() - t1 });
|
|
45
|
+
|
|
46
|
+
// Canvas draw benchmark
|
|
47
|
+
const t2 = performance.now();
|
|
48
|
+
const canvas = document.createElement('canvas');
|
|
49
|
+
canvas.width = 100;
|
|
50
|
+
canvas.height = 100;
|
|
51
|
+
const ctx = canvas.getContext('2d');
|
|
52
|
+
if (ctx) {
|
|
53
|
+
for (let i = 0; i < 50; i++) {
|
|
54
|
+
ctx.fillStyle = `rgb(${i * 5},${i * 3},${i * 4})`;
|
|
55
|
+
ctx.fillRect(i, i, 50, 50);
|
|
56
|
+
ctx.arc(50, 50, 30, 0, Math.PI * 2);
|
|
57
|
+
ctx.stroke();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
results.push({ operation: 'canvas_draw', durationMs: performance.now() - t2 });
|
|
61
|
+
|
|
62
|
+
// Regex benchmark
|
|
63
|
+
const t3 = performance.now();
|
|
64
|
+
const text = 'Lorem ipsum dolor sit amet '.repeat(100);
|
|
65
|
+
for (let i = 0; i < 50; i++) {
|
|
66
|
+
/\b\w{5,}\b/g.exec(text);
|
|
67
|
+
}
|
|
68
|
+
results.push({ operation: 'regex_match', durationMs: performance.now() - t3 });
|
|
69
|
+
|
|
70
|
+
// JSON benchmark
|
|
71
|
+
const t4 = performance.now();
|
|
72
|
+
const obj = { a: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `item_${i}` })) };
|
|
73
|
+
for (let i = 0; i < 10; i++) {
|
|
74
|
+
JSON.parse(JSON.stringify(obj));
|
|
75
|
+
}
|
|
76
|
+
results.push({ operation: 'json_serialize', durationMs: performance.now() - t4 });
|
|
77
|
+
|
|
78
|
+
return results;
|
|
79
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { TrajectoryPoint } from './types';
|
|
2
|
+
import { sdkLogger } from './logger';
|
|
3
|
+
|
|
4
|
+
export class TrajectoryRecorder {
|
|
5
|
+
private points: TrajectoryPoint[] = [];
|
|
6
|
+
private recording = false;
|
|
7
|
+
private element: HTMLElement | null = null;
|
|
8
|
+
private handlers: { type: string; fn: EventListener }[] = [];
|
|
9
|
+
|
|
10
|
+
start(element: HTMLElement) {
|
|
11
|
+
this.points = [];
|
|
12
|
+
this.recording = true;
|
|
13
|
+
this.element = element;
|
|
14
|
+
this.bindEvents();
|
|
15
|
+
sdkLogger.info('Trajectory', 'Recording start', { tag: element.tagName });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
stop(): TrajectoryPoint[] {
|
|
19
|
+
this.recording = false;
|
|
20
|
+
this.unbindEvents();
|
|
21
|
+
sdkLogger.info('Trajectory', 'Recording stop', { pointCount: this.points.length });
|
|
22
|
+
return [...this.points];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
getPoints(): TrajectoryPoint[] {
|
|
26
|
+
return [...this.points];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private bindEvents() {
|
|
30
|
+
const el = this.element || document;
|
|
31
|
+
|
|
32
|
+
const mouseMove = (e: Event) => this.onMouseMove(e as MouseEvent);
|
|
33
|
+
const mouseDown = (e: Event) => this.onMouseDown(e as MouseEvent);
|
|
34
|
+
const mouseUp = (e: Event) => this.onMouseUp(e as MouseEvent);
|
|
35
|
+
const touchStart = (e: Event) => this.onTouchEvent(e as TouchEvent, 'touchstart');
|
|
36
|
+
const touchMove = (e: Event) => this.onTouchEvent(e as TouchEvent, 'touchmove');
|
|
37
|
+
const touchEnd = (e: Event) => this.onTouchEvent(e as TouchEvent, 'touchend');
|
|
38
|
+
const click = (e: Event) => this.onClick(e as MouseEvent);
|
|
39
|
+
|
|
40
|
+
el.addEventListener('mousemove', mouseMove, { passive: true });
|
|
41
|
+
el.addEventListener('mousedown', mouseDown, { passive: true });
|
|
42
|
+
el.addEventListener('mouseup', mouseUp, { passive: true });
|
|
43
|
+
el.addEventListener('touchstart', touchStart, { passive: true });
|
|
44
|
+
el.addEventListener('touchmove', touchMove, { passive: true });
|
|
45
|
+
el.addEventListener('touchend', touchEnd, { passive: true });
|
|
46
|
+
el.addEventListener('click', click, { passive: true });
|
|
47
|
+
|
|
48
|
+
this.handlers = [
|
|
49
|
+
{ type: 'mousemove', fn: mouseMove },
|
|
50
|
+
{ type: 'mousedown', fn: mouseDown },
|
|
51
|
+
{ type: 'mouseup', fn: mouseUp },
|
|
52
|
+
{ type: 'touchstart', fn: touchStart },
|
|
53
|
+
{ type: 'touchmove', fn: touchMove },
|
|
54
|
+
{ type: 'touchend', fn: touchEnd },
|
|
55
|
+
{ type: 'click', fn: click },
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private unbindEvents() {
|
|
60
|
+
const el = this.element || document;
|
|
61
|
+
for (const h of this.handlers) {
|
|
62
|
+
el.removeEventListener(h.type, h.fn);
|
|
63
|
+
}
|
|
64
|
+
this.handlers = [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private addPoint(x: number, y: number, eventType: string, isTrusted: boolean,
|
|
68
|
+
movementX = 0, movementY = 0, buttons = 0, force?: number, radiusX?: number, radiusY?: number) {
|
|
69
|
+
if (!this.recording) return;
|
|
70
|
+
this.points.push({
|
|
71
|
+
x: Math.round(x * 100) / 100,
|
|
72
|
+
y: Math.round(y * 100) / 100,
|
|
73
|
+
timestamp: performance.now(),
|
|
74
|
+
eventType,
|
|
75
|
+
isTrusted,
|
|
76
|
+
movementX,
|
|
77
|
+
movementY,
|
|
78
|
+
buttons,
|
|
79
|
+
force,
|
|
80
|
+
radiusX,
|
|
81
|
+
radiusY,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private onMouseMove(e: MouseEvent) {
|
|
86
|
+
this.addPoint(e.clientX, e.clientY, 'mousemove', e.isTrusted, e.movementX, e.movementY, e.buttons);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private onMouseDown(e: MouseEvent) {
|
|
90
|
+
this.addPoint(e.clientX, e.clientY, 'mousedown', e.isTrusted, 0, 0, e.buttons);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private onMouseUp(e: MouseEvent) {
|
|
94
|
+
this.addPoint(e.clientX, e.clientY, 'mouseup', e.isTrusted, 0, 0, e.buttons);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private onClick(e: MouseEvent) {
|
|
98
|
+
this.addPoint(e.clientX, e.clientY, 'click', e.isTrusted);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private onTouchEvent(e: TouchEvent, eventType: string) {
|
|
102
|
+
const touch = e.touches[0] || e.changedTouches[0];
|
|
103
|
+
if (touch) {
|
|
104
|
+
this.addPoint(
|
|
105
|
+
touch.clientX, touch.clientY, eventType, e.isTrusted,
|
|
106
|
+
0, 0, 0, (touch as any).force, (touch as any).radiusX, (touch as any).radiusY,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { DeepProbeResult } from './deep_probes';
|
|
2
|
+
import type { PassiveSignalResult } from './passive_signals';
|
|
3
|
+
|
|
4
|
+
export interface CaptchaConfig {
|
|
5
|
+
siteKey: string;
|
|
6
|
+
apiBase: string;
|
|
7
|
+
/** When true, enables verbose SDK logging (same effect as localStorage engagelab_captcha_debug). */
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
mode?: 'popup' | 'inline' | 'invisible';
|
|
10
|
+
lang?: string;
|
|
11
|
+
entityHash?: string;
|
|
12
|
+
theme?: ThemeConfig;
|
|
13
|
+
fingerprint?: FingerprintOptions;
|
|
14
|
+
/** Disable eval/Function.constructor wrapping (e.g. to avoid HMR conflicts). Default: false */
|
|
15
|
+
disableEvalPatching?: boolean;
|
|
16
|
+
onReady?: () => void;
|
|
17
|
+
onSuccess?: (token: string, riskLabels: string[]) => void;
|
|
18
|
+
onFail?: (error: CaptchaError) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Called when the CAPTCHA service is unavailable (risk engine down, HTTP 503, etc.).
|
|
21
|
+
* The developer should implement their own degradation strategy here.
|
|
22
|
+
* If not provided, falls back to onFail with code="service_unavailable".
|
|
23
|
+
*
|
|
24
|
+
* Recommended actions:
|
|
25
|
+
* - Allow the user through for low-risk business scenarios
|
|
26
|
+
* - Block the user for high-risk scenarios (e.g. payment)
|
|
27
|
+
* - Switch to an alternative verification method (e.g. SMS verification)
|
|
28
|
+
*/
|
|
29
|
+
onError?: (error: CaptchaError) => void;
|
|
30
|
+
onClose?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FingerprintOptions {
|
|
34
|
+
/** Audio fingerprint via OfflineAudioContext (no mic, no permission needed). Default: true */
|
|
35
|
+
audio?: boolean;
|
|
36
|
+
/** Canvas fingerprint. Default: true */
|
|
37
|
+
canvas?: boolean;
|
|
38
|
+
/** WebGL renderer/vendor fingerprint. Default: true */
|
|
39
|
+
webgl?: boolean;
|
|
40
|
+
/** Font detection via DOM measurement. Default: true */
|
|
41
|
+
fonts?: boolean;
|
|
42
|
+
/** WebRTC local IP detection. Default: true */
|
|
43
|
+
webrtc?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ThemeConfig {
|
|
47
|
+
primaryColor?: string;
|
|
48
|
+
successColor?: string;
|
|
49
|
+
errorColor?: string;
|
|
50
|
+
backgroundColor?: string;
|
|
51
|
+
textColor?: string;
|
|
52
|
+
borderRadius?: number;
|
|
53
|
+
fontFamily?: string;
|
|
54
|
+
darkMode?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CaptchaError {
|
|
58
|
+
code: string;
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Thrown when the CAPTCHA backend service is unavailable (HTTP 503 or
|
|
64
|
+
* verify returns decision="service_unavailable"). The developer should
|
|
65
|
+
* implement their own degradation strategy in the onError callback.
|
|
66
|
+
*/
|
|
67
|
+
export class ServiceError extends Error {
|
|
68
|
+
public readonly code: string;
|
|
69
|
+
constructor(code: string, message: string) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = 'ServiceError';
|
|
72
|
+
this.code = code;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface Fingerprint {
|
|
77
|
+
canvasHash: string;
|
|
78
|
+
webglRenderer: string;
|
|
79
|
+
webglVendor: string;
|
|
80
|
+
webglHash: string;
|
|
81
|
+
audioHash: string;
|
|
82
|
+
screenWidth: number;
|
|
83
|
+
screenHeight: number;
|
|
84
|
+
colorDepth: number;
|
|
85
|
+
devicePixelRatio: number;
|
|
86
|
+
timezone: string;
|
|
87
|
+
timezoneOffset: number;
|
|
88
|
+
language: string;
|
|
89
|
+
languages: string[];
|
|
90
|
+
platform: string;
|
|
91
|
+
hardwareConcurrency: number;
|
|
92
|
+
deviceMemory: number;
|
|
93
|
+
maxTouchPoints: number;
|
|
94
|
+
fonts: string[];
|
|
95
|
+
pluginCount: number;
|
|
96
|
+
webdriver: boolean;
|
|
97
|
+
hasChromeRuntime: boolean;
|
|
98
|
+
connectionType: string;
|
|
99
|
+
screenAvailWidth: number;
|
|
100
|
+
screenAvailHeight: number;
|
|
101
|
+
outerWidth: number;
|
|
102
|
+
outerHeight: number;
|
|
103
|
+
innerWidth: number;
|
|
104
|
+
innerHeight: number;
|
|
105
|
+
headlessSignals: HeadlessSignal[];
|
|
106
|
+
mathFp: MathFingerprint;
|
|
107
|
+
storageInfo: StorageInfo;
|
|
108
|
+
/** Deep environment probes (snake_case key matches server env analyzer). */
|
|
109
|
+
deep_probes?: DeepProbeResult[];
|
|
110
|
+
/** Background behavioral signals collected before challenge interaction. */
|
|
111
|
+
passive_signals?: PassiveSignalResult;
|
|
112
|
+
/** Stack trace records from wrapped native functions. */
|
|
113
|
+
native_traces?: NativeTrace[];
|
|
114
|
+
/** Number of SDK sessions from this browser (persisted via IndexedDB). */
|
|
115
|
+
session_count?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface NativeTrace {
|
|
119
|
+
fn: string;
|
|
120
|
+
stack: string;
|
|
121
|
+
timestamp: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface HeadlessSignal {
|
|
125
|
+
name: string;
|
|
126
|
+
detected: boolean;
|
|
127
|
+
value: string;
|
|
128
|
+
weight: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface MathFingerprint {
|
|
132
|
+
tanValue: number;
|
|
133
|
+
atan2Value: number;
|
|
134
|
+
logValue: number;
|
|
135
|
+
sinValue: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface StorageInfo {
|
|
139
|
+
localStorage: boolean;
|
|
140
|
+
sessionStorage: boolean;
|
|
141
|
+
indexedDb: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface TrajectoryPoint {
|
|
145
|
+
x: number;
|
|
146
|
+
y: number;
|
|
147
|
+
timestamp: number;
|
|
148
|
+
eventType: string;
|
|
149
|
+
isTrusted: boolean;
|
|
150
|
+
movementX: number;
|
|
151
|
+
movementY: number;
|
|
152
|
+
buttons: number;
|
|
153
|
+
force?: number;
|
|
154
|
+
radiusX?: number;
|
|
155
|
+
radiusY?: number;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface PowResult {
|
|
159
|
+
challenge: string;
|
|
160
|
+
nonce: string;
|
|
161
|
+
difficulty: number;
|
|
162
|
+
computationTimeMs: number;
|
|
163
|
+
iterations: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface TimingData {
|
|
167
|
+
domContentLoaded: number;
|
|
168
|
+
sdkInitTime: number;
|
|
169
|
+
interactionStart: number;
|
|
170
|
+
verificationComplete: number;
|
|
171
|
+
jsMeasurements: { operation: string; durationMs: number }[];
|
|
172
|
+
performanceNowPrecision: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface IntegrityData {
|
|
176
|
+
sdkHash: string;
|
|
177
|
+
patchedFunctions: string[];
|
|
178
|
+
devtoolsDetected: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface InitResponse {
|
|
182
|
+
sessionId: string;
|
|
183
|
+
challengeType: string;
|
|
184
|
+
powDifficulty: number;
|
|
185
|
+
powChallenge: string;
|
|
186
|
+
challengeConfig: any;
|
|
187
|
+
publicKey: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface VerifyResponse {
|
|
191
|
+
success: boolean;
|
|
192
|
+
decision: string;
|
|
193
|
+
score: number;
|
|
194
|
+
riskLabels: string[];
|
|
195
|
+
validateToken: string;
|
|
196
|
+
layerResults?: any[];
|
|
197
|
+
nextChallenge?: string;
|
|
198
|
+
degraded?: boolean;
|
|
199
|
+
}
|