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/index.ts
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
import { CaptchaConfig, VerifyResponse, ServiceError } from './types';
|
|
2
|
+
import { collectFingerprint } from './fingerprint';
|
|
3
|
+
import { detectHeadless } from './headless';
|
|
4
|
+
import { collectTimingData } from './timing';
|
|
5
|
+
import { TrajectoryRecorder } from './trajectory';
|
|
6
|
+
import { computePoW } from './pow';
|
|
7
|
+
import { checkIntegrity, setFunctionRefs } from './integrity';
|
|
8
|
+
import { CaptchaAPI } from './api';
|
|
9
|
+
import { CaptchaContainer } from './ui/container';
|
|
10
|
+
import { injectStyles } from './ui/styles';
|
|
11
|
+
import { InvisibleChallenge } from './challenges/invisible';
|
|
12
|
+
import { setLocale, detectLocale, t, Locale } from './i18n';
|
|
13
|
+
import { setTheme, CaptchaTheme } from './theme';
|
|
14
|
+
import { initReporter, reportError, destroyReporter } from './reporter';
|
|
15
|
+
import type { DeepProbeResult } from './deep_probes';
|
|
16
|
+
import { PassiveSignalCollector } from './passive_signals';
|
|
17
|
+
import { NativeMonitor } from './native_monitor';
|
|
18
|
+
import { SessionPersist } from './session_persist';
|
|
19
|
+
import { sdkLogger } from './logger';
|
|
20
|
+
|
|
21
|
+
export type { DeepProbeResult };
|
|
22
|
+
|
|
23
|
+
export { DegradationManager, DegradationConfig } from './degradation';
|
|
24
|
+
|
|
25
|
+
export class EngageLabCaptcha {
|
|
26
|
+
private config: CaptchaConfig;
|
|
27
|
+
private api: CaptchaAPI;
|
|
28
|
+
private container = new CaptchaContainer();
|
|
29
|
+
private sessionId = '';
|
|
30
|
+
private sdkInitTime: number;
|
|
31
|
+
private pageTrajectory = new TrajectoryRecorder();
|
|
32
|
+
private passiveCollector = new PassiveSignalCollector();
|
|
33
|
+
private nativeMonitor = new NativeMonitor();
|
|
34
|
+
private sessionPersist = new SessionPersist();
|
|
35
|
+
private escalationDepth = 0;
|
|
36
|
+
private cachedCollectedData: { fingerprint: any; timingData: any; integrity: any; powResult: any } | null = null;
|
|
37
|
+
private static readonly MAX_ESCALATION_DEPTH = 5;
|
|
38
|
+
|
|
39
|
+
constructor(config: CaptchaConfig) {
|
|
40
|
+
this.config = {
|
|
41
|
+
mode: 'popup',
|
|
42
|
+
lang: 'zh-CN',
|
|
43
|
+
...config,
|
|
44
|
+
};
|
|
45
|
+
if (this.config.debug) {
|
|
46
|
+
sdkLogger.enableDebug();
|
|
47
|
+
}
|
|
48
|
+
this.api = new CaptchaAPI(config.apiBase, config.siteKey);
|
|
49
|
+
sdkLogger.configure(config.apiBase, config.siteKey);
|
|
50
|
+
if (this.config.debug) {
|
|
51
|
+
sdkLogger.startAutoFlush(15000);
|
|
52
|
+
}
|
|
53
|
+
this.sdkInitTime = performance.now();
|
|
54
|
+
|
|
55
|
+
const locale = (this.config.lang as Locale) || detectLocale();
|
|
56
|
+
setLocale(locale);
|
|
57
|
+
|
|
58
|
+
if (this.config.theme) {
|
|
59
|
+
setTheme(this.config.theme as CaptchaTheme);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
injectStyles();
|
|
63
|
+
setFunctionRefs(collectFingerprint, detectHeadless, computePoW);
|
|
64
|
+
|
|
65
|
+
initReporter(config.apiBase, config.siteKey);
|
|
66
|
+
|
|
67
|
+
this.pageTrajectory.start(document.body);
|
|
68
|
+
this.passiveCollector.start();
|
|
69
|
+
if (!this.config.disableEvalPatching) {
|
|
70
|
+
this.nativeMonitor.start();
|
|
71
|
+
}
|
|
72
|
+
this.sessionPersist.load().catch(() => {});
|
|
73
|
+
this.config.onReady?.();
|
|
74
|
+
|
|
75
|
+
sdkLogger.info(
|
|
76
|
+
'SDK',
|
|
77
|
+
`Initialized: siteKey=${config.siteKey?.substring(0, 8)}..., mode=${this.config.mode}, lang=${this.config.lang}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async execute(challengeType?: string): Promise<void> {
|
|
82
|
+
sdkLogger.info('SDK', `Execute called: challengeType=${challengeType || 'auto'}`);
|
|
83
|
+
try {
|
|
84
|
+
this.escalationDepth = 0;
|
|
85
|
+
this.cachedCollectedData = null;
|
|
86
|
+
const initResp = await this.api.init(this.config.siteKey, this.config.entityHash);
|
|
87
|
+
this.sessionId = initResp.sessionId;
|
|
88
|
+
sdkLogger.setSessionId(this.sessionId);
|
|
89
|
+
|
|
90
|
+
const type = challengeType || initResp.challengeType || 'invisible';
|
|
91
|
+
sdkLogger.info(
|
|
92
|
+
'SDK',
|
|
93
|
+
`Init response: sessionId=${this.sessionId?.substring(0, 8)}, type=${type}`,
|
|
94
|
+
);
|
|
95
|
+
await this.runChallenge(type, initResp);
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
sdkLogger.error('SDK', 'Execute failed', err?.message || err);
|
|
98
|
+
reportError(err instanceof Error ? err : new Error(err.message || 'Unknown error'));
|
|
99
|
+
if (err instanceof ServiceError) {
|
|
100
|
+
this.handleServiceUnavailable(err);
|
|
101
|
+
} else {
|
|
102
|
+
this.container.showResult(false, t('fail'));
|
|
103
|
+
this.config.onFail?.({ code: 'error', message: err.message || 'Unknown error' });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private async runChallenge(type: string, initResp: any): Promise<void> {
|
|
109
|
+
sdkLogger.info('SDK', `runChallenge enter: type=${type}`);
|
|
110
|
+
try {
|
|
111
|
+
switch (type) {
|
|
112
|
+
case 'invisible': return await this.runInvisible(initResp);
|
|
113
|
+
case 'slider': return await this.runSlider(initResp);
|
|
114
|
+
case 'spatial': return await this.runSpatial(initResp);
|
|
115
|
+
case 'rotate': return await this.runRotate(initResp);
|
|
116
|
+
case 'icons': return await this.runIcons(initResp);
|
|
117
|
+
case 'dragsort': return await this.runDragSort(initResp);
|
|
118
|
+
case 'grid': return await this.runGrid(initResp);
|
|
119
|
+
case 'physics': return await this.runPhysics(initResp);
|
|
120
|
+
default: return await this.runInvisible(initResp);
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
sdkLogger.debug('SDK', `runChallenge exit: type=${type}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
reset() {
|
|
128
|
+
sdkLogger.info('SDK', 'reset()');
|
|
129
|
+
this.container.close();
|
|
130
|
+
this.sessionId = '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
destroy() {
|
|
134
|
+
sdkLogger.info('SDK', 'destroy()');
|
|
135
|
+
this.passiveCollector.stop();
|
|
136
|
+
this.nativeMonitor.disable();
|
|
137
|
+
this.pageTrajectory.stop();
|
|
138
|
+
this.container.close();
|
|
139
|
+
destroyReporter();
|
|
140
|
+
sdkLogger.destroy();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private async collectAllData(initResp: any) {
|
|
144
|
+
if (this.cachedCollectedData && this.escalationDepth > 0) {
|
|
145
|
+
sdkLogger.debug('SDK', `collectAllData: reusing cached data for escalation depth=${this.escalationDepth}`);
|
|
146
|
+
return {
|
|
147
|
+
...this.cachedCollectedData,
|
|
148
|
+
timingData: { ...this.cachedCollectedData.timingData, verificationComplete: performance.now() },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const start = performance.now();
|
|
153
|
+
const loadDeepProbes = import('./deep_probes').then(m => m.runDeepProbes());
|
|
154
|
+
const [fingerprint, headlessSignals, timingData, integrity, powResult, deepProbes] = await Promise.all([
|
|
155
|
+
collectFingerprint(this.config.fingerprint),
|
|
156
|
+
detectHeadless(),
|
|
157
|
+
Promise.resolve(collectTimingData(this.sdkInitTime)),
|
|
158
|
+
Promise.resolve(checkIntegrity()),
|
|
159
|
+
computePoW(initResp.powChallenge, initResp.powDifficulty),
|
|
160
|
+
loadDeepProbes,
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
fingerprint.headlessSignals = headlessSignals;
|
|
164
|
+
fingerprint.deep_probes = deepProbes;
|
|
165
|
+
fingerprint.passive_signals = this.passiveCollector.getResults();
|
|
166
|
+
fingerprint.native_traces = this.nativeMonitor.getTraces();
|
|
167
|
+
fingerprint.session_count = this.sessionPersist.getData().sessionCount;
|
|
168
|
+
timingData.verificationComplete = performance.now();
|
|
169
|
+
|
|
170
|
+
sdkLogger.debug('SDK', `collectAllData completed in ${(performance.now() - start).toFixed(1)}ms`, {
|
|
171
|
+
headlessSignals: headlessSignals.filter(s => s.detected).length,
|
|
172
|
+
deepProbes: deepProbes.filter(p => p.detected).length,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const result = { fingerprint, timingData, integrity, powResult };
|
|
176
|
+
this.cachedCollectedData = result;
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async runInvisible(initResp: any) {
|
|
181
|
+
this.container.showLoading();
|
|
182
|
+
|
|
183
|
+
const { fingerprint, timingData, integrity, powResult } = await this.collectAllData(initResp);
|
|
184
|
+
|
|
185
|
+
const result = await this.api.verify(this.sessionId, {
|
|
186
|
+
challengeType: 'invisible',
|
|
187
|
+
fingerprint,
|
|
188
|
+
trajectory: this.pageTrajectory.getPoints().slice(-200),
|
|
189
|
+
powResult,
|
|
190
|
+
answer: { challengeType: 'invisible', passtime: performance.now() - this.sdkInitTime },
|
|
191
|
+
timing: timingData,
|
|
192
|
+
integrity,
|
|
193
|
+
entityHash: this.config.entityHash,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
this.handleResult(result, initResp);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private async runSlider(initResp: any) {
|
|
200
|
+
const { fingerprint, timingData, integrity, powResult } = await this.collectAllData(initResp);
|
|
201
|
+
|
|
202
|
+
const targetX = initResp.challengeConfig?.targetX ?? Math.random() * 60 + 20;
|
|
203
|
+
const { SliderChallenge } = await import('./challenges/slider');
|
|
204
|
+
const slider = new SliderChallenge();
|
|
205
|
+
const { element, promise } = slider.create(targetX);
|
|
206
|
+
this.container.show(element, () => { slider.destroy(); this.config.onClose?.(); });
|
|
207
|
+
|
|
208
|
+
const sliderResult = await promise;
|
|
209
|
+
timingData.verificationComplete = performance.now();
|
|
210
|
+
this.container.showLoading();
|
|
211
|
+
|
|
212
|
+
const result = await this.api.verify(this.sessionId, {
|
|
213
|
+
challengeType: 'slider',
|
|
214
|
+
fingerprint,
|
|
215
|
+
trajectory: sliderResult.trajectory,
|
|
216
|
+
powResult,
|
|
217
|
+
answer: { challengeType: 'slider', sliderX: sliderResult.sliderX, passtime: sliderResult.passtime },
|
|
218
|
+
timing: timingData,
|
|
219
|
+
integrity,
|
|
220
|
+
entityHash: this.config.entityHash,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
this.handleResult(result, initResp);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private async runSpatial(initResp: any) {
|
|
227
|
+
this.container.showLoading();
|
|
228
|
+
const [challengeData, collected] = await Promise.all([
|
|
229
|
+
this.api.getChallenge(this.sessionId, 'spatial'),
|
|
230
|
+
this.collectAllData(initResp),
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
const { SpatialChallenge } = await import('./challenges/spatial');
|
|
234
|
+
const spatial = new SpatialChallenge();
|
|
235
|
+
const { element, promise } = spatial.create(
|
|
236
|
+
challengeData.imageBase64, challengeData.question, challengeData.options);
|
|
237
|
+
this.container.show(element, () => this.config.onClose?.());
|
|
238
|
+
|
|
239
|
+
const spatialResult = await promise;
|
|
240
|
+
collected.timingData.verificationComplete = performance.now();
|
|
241
|
+
this.container.showLoading();
|
|
242
|
+
|
|
243
|
+
const result = await this.api.verify(this.sessionId, {
|
|
244
|
+
challengeType: 'spatial',
|
|
245
|
+
fingerprint: collected.fingerprint,
|
|
246
|
+
trajectory: this.pageTrajectory.getPoints().slice(-100),
|
|
247
|
+
powResult: collected.powResult,
|
|
248
|
+
answer: { challengeType: 'spatial', spatialAnswerIndex: spatialResult.answerIndex, passtime: spatialResult.passtime },
|
|
249
|
+
timing: collected.timingData,
|
|
250
|
+
integrity: collected.integrity,
|
|
251
|
+
entityHash: this.config.entityHash,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this.handleResult(result, initResp);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async runRotate(initResp: any) {
|
|
258
|
+
this.container.showLoading();
|
|
259
|
+
const [challengeData, collected] = await Promise.all([
|
|
260
|
+
this.api.getChallenge(this.sessionId, 'rotate'),
|
|
261
|
+
this.collectAllData(initResp),
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
const { RotateChallenge } = await import('./challenges/rotate');
|
|
265
|
+
const rotate = new RotateChallenge();
|
|
266
|
+
const { element, promise } = rotate.create(challengeData.imageBase64);
|
|
267
|
+
this.container.show(element, () => this.config.onClose?.());
|
|
268
|
+
|
|
269
|
+
const rotateResult = await promise;
|
|
270
|
+
collected.timingData.verificationComplete = performance.now();
|
|
271
|
+
this.container.showLoading();
|
|
272
|
+
|
|
273
|
+
const result = await this.api.verify(this.sessionId, {
|
|
274
|
+
challengeType: 'rotate',
|
|
275
|
+
fingerprint: collected.fingerprint,
|
|
276
|
+
trajectory: rotateResult.trajectory,
|
|
277
|
+
powResult: collected.powResult,
|
|
278
|
+
answer: { challengeType: 'rotate', rotateAngle: rotateResult.angle, passtime: rotateResult.passtime },
|
|
279
|
+
timing: collected.timingData,
|
|
280
|
+
integrity: collected.integrity,
|
|
281
|
+
entityHash: this.config.entityHash,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
this.handleResult(result, initResp);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async runIcons(initResp: any) {
|
|
288
|
+
this.container.showLoading();
|
|
289
|
+
const [challengeData, collected] = await Promise.all([
|
|
290
|
+
this.api.getChallenge(this.sessionId, 'icons'),
|
|
291
|
+
this.collectAllData(initResp),
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
const { IconChallenge } = await import('./challenges/icons');
|
|
295
|
+
const icons = new IconChallenge();
|
|
296
|
+
const { element, promise } = icons.create(
|
|
297
|
+
challengeData.imageBase64, challengeData.instruction, challengeData.iconCount || 9);
|
|
298
|
+
this.container.show(element, () => this.config.onClose?.());
|
|
299
|
+
|
|
300
|
+
const iconResult = await promise;
|
|
301
|
+
collected.timingData.verificationComplete = performance.now();
|
|
302
|
+
this.container.showLoading();
|
|
303
|
+
|
|
304
|
+
const result = await this.api.verify(this.sessionId, {
|
|
305
|
+
challengeType: 'icons',
|
|
306
|
+
fingerprint: collected.fingerprint,
|
|
307
|
+
trajectory: this.pageTrajectory.getPoints().slice(-100),
|
|
308
|
+
powResult: collected.powResult,
|
|
309
|
+
answer: { challengeType: 'icons', iconClicks: iconResult.clicks, passtime: iconResult.passtime },
|
|
310
|
+
timing: collected.timingData,
|
|
311
|
+
integrity: collected.integrity,
|
|
312
|
+
entityHash: this.config.entityHash,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
this.handleResult(result, initResp);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private async runDragSort(initResp: any) {
|
|
319
|
+
this.container.showLoading();
|
|
320
|
+
const [challengeData, collected] = await Promise.all([
|
|
321
|
+
this.api.getChallenge(this.sessionId, 'dragsort'),
|
|
322
|
+
this.collectAllData(initResp),
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const { DragSortChallenge } = await import('./challenges/dragsort');
|
|
326
|
+
const challenge = new DragSortChallenge();
|
|
327
|
+
const { element, getResult } = challenge.create(
|
|
328
|
+
challengeData.imageBase64, challengeData.instruction, challengeData.itemCount || 4);
|
|
329
|
+
|
|
330
|
+
const submitWrapper = document.createElement('div');
|
|
331
|
+
submitWrapper.style.cssText = 'width:100%;max-width:380px;margin:12px auto 0;display:flex;gap:8px;justify-content:center;';
|
|
332
|
+
|
|
333
|
+
const confirmBtn = document.createElement('button');
|
|
334
|
+
confirmBtn.textContent = t('dragsort.confirm');
|
|
335
|
+
confirmBtn.style.cssText = 'padding:8px 24px;background:#1890ff;color:white;border:none;border-radius:6px;cursor:pointer;font-size:14px;';
|
|
336
|
+
submitWrapper.appendChild(confirmBtn);
|
|
337
|
+
|
|
338
|
+
const container = document.createElement('div');
|
|
339
|
+
container.appendChild(element);
|
|
340
|
+
container.appendChild(submitWrapper);
|
|
341
|
+
this.container.show(container, () => { challenge.destroy(); this.config.onClose?.(); });
|
|
342
|
+
|
|
343
|
+
const dragResult = await new Promise<any>((resolve) => {
|
|
344
|
+
confirmBtn.addEventListener('click', () => resolve(getResult()));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
collected.timingData.verificationComplete = performance.now();
|
|
348
|
+
this.container.showLoading();
|
|
349
|
+
|
|
350
|
+
const result = await this.api.verify(this.sessionId, {
|
|
351
|
+
challengeType: 'dragsort',
|
|
352
|
+
fingerprint: collected.fingerprint,
|
|
353
|
+
trajectory: dragResult.trajectory,
|
|
354
|
+
powResult: collected.powResult,
|
|
355
|
+
answer: {
|
|
356
|
+
challengeType: 'dragsort',
|
|
357
|
+
dragSortOrder: dragResult.sortOrder,
|
|
358
|
+
dragEvents: dragResult.dragEvents,
|
|
359
|
+
passtime: dragResult.passtime,
|
|
360
|
+
},
|
|
361
|
+
timing: collected.timingData,
|
|
362
|
+
integrity: collected.integrity,
|
|
363
|
+
entityHash: this.config.entityHash,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
this.handleResult(result, initResp);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async runGrid(initResp: any) {
|
|
370
|
+
this.container.showLoading();
|
|
371
|
+
const [challengeData, collected] = await Promise.all([
|
|
372
|
+
this.api.getChallenge(this.sessionId, 'grid'),
|
|
373
|
+
this.collectAllData(initResp),
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
const { GridChallenge } = await import('./challenges/grid');
|
|
377
|
+
const challenge = new GridChallenge();
|
|
378
|
+
const { element, getResult } = challenge.create(
|
|
379
|
+
challengeData.imageBase64, challengeData.instruction, challengeData.gridSize || 3);
|
|
380
|
+
|
|
381
|
+
const submitWrapper = document.createElement('div');
|
|
382
|
+
submitWrapper.style.cssText = 'width:100%;max-width:380px;margin:12px auto 0;display:flex;gap:8px;justify-content:center;';
|
|
383
|
+
|
|
384
|
+
const confirmBtn = document.createElement('button');
|
|
385
|
+
confirmBtn.textContent = t('grid.confirm');
|
|
386
|
+
confirmBtn.style.cssText = 'padding:8px 24px;background:#1890ff;color:white;border:none;border-radius:6px;cursor:pointer;font-size:14px;';
|
|
387
|
+
submitWrapper.appendChild(confirmBtn);
|
|
388
|
+
|
|
389
|
+
const outerContainer = document.createElement('div');
|
|
390
|
+
outerContainer.appendChild(element);
|
|
391
|
+
outerContainer.appendChild(submitWrapper);
|
|
392
|
+
this.container.show(outerContainer, () => { challenge.destroy(); this.config.onClose?.(); });
|
|
393
|
+
|
|
394
|
+
const gridResult = await new Promise<any>((resolve) => {
|
|
395
|
+
confirmBtn.addEventListener('click', () => resolve(getResult()));
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
collected.timingData.verificationComplete = performance.now();
|
|
399
|
+
this.container.showLoading();
|
|
400
|
+
|
|
401
|
+
const result = await this.api.verify(this.sessionId, {
|
|
402
|
+
challengeType: 'grid',
|
|
403
|
+
fingerprint: collected.fingerprint,
|
|
404
|
+
trajectory: this.pageTrajectory.getPoints().slice(-100),
|
|
405
|
+
powResult: collected.powResult,
|
|
406
|
+
answer: {
|
|
407
|
+
challengeType: 'grid',
|
|
408
|
+
gridSelectedCells: gridResult.selectedCells,
|
|
409
|
+
passtime: gridResult.passtime,
|
|
410
|
+
},
|
|
411
|
+
timing: collected.timingData,
|
|
412
|
+
integrity: collected.integrity,
|
|
413
|
+
entityHash: this.config.entityHash,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
this.handleResult(result, initResp);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private async runPhysics(initResp: any) {
|
|
420
|
+
this.container.showLoading();
|
|
421
|
+
const [challengeData, collected] = await Promise.all([
|
|
422
|
+
this.api.getChallenge(this.sessionId, 'physics'),
|
|
423
|
+
this.collectAllData(initResp),
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
const { PhysicsChallenge } = await import('./challenges/physics');
|
|
427
|
+
const challenge = new PhysicsChallenge();
|
|
428
|
+
|
|
429
|
+
let physicsConfig = challengeData.challengeConfig || challengeData;
|
|
430
|
+
if (typeof challengeData.configJson === 'string') {
|
|
431
|
+
try {
|
|
432
|
+
physicsConfig = { ...JSON.parse(challengeData.configJson), variant: challengeData.variant };
|
|
433
|
+
} catch { /* use fallback */ }
|
|
434
|
+
}
|
|
435
|
+
const previewImg = challengeData.previewBase64 || challengeData.previewImage || '';
|
|
436
|
+
|
|
437
|
+
const { element, promise } = challenge.create(physicsConfig, previewImg);
|
|
438
|
+
this.container.show(element, () => { challenge.destroy(); this.config.onClose?.(); });
|
|
439
|
+
|
|
440
|
+
const physicsResult = await promise;
|
|
441
|
+
collected.timingData.verificationComplete = performance.now();
|
|
442
|
+
this.container.showLoading();
|
|
443
|
+
|
|
444
|
+
const result = await this.api.verify(this.sessionId, {
|
|
445
|
+
challengeType: 'physics',
|
|
446
|
+
fingerprint: collected.fingerprint,
|
|
447
|
+
trajectory: this.pageTrajectory.getPoints().slice(-100),
|
|
448
|
+
powResult: collected.powResult,
|
|
449
|
+
answer: {
|
|
450
|
+
challengeType: 'physics',
|
|
451
|
+
physicsAnswer: physicsResult.answer,
|
|
452
|
+
interactionEvents: physicsResult.interactionEvents,
|
|
453
|
+
passtime: physicsResult.passtime,
|
|
454
|
+
},
|
|
455
|
+
timing: collected.timingData,
|
|
456
|
+
integrity: collected.integrity,
|
|
457
|
+
entityHash: this.config.entityHash,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
this.handleResult(result, initResp);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private handleResult(result: VerifyResponse, initResp: any) {
|
|
464
|
+
sdkLogger.info(
|
|
465
|
+
'SDK',
|
|
466
|
+
`Verify result: success=${result.success}, decision=${result.decision}, score=${result.score}, degraded=${result.degraded}, nextChallenge=${result.nextChallenge}`,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
if (result.decision === 'service_unavailable' || (result.degraded && !result.success)) {
|
|
470
|
+
sdkLogger.warn('SDK', 'handleResult: service unavailable — notifying developer');
|
|
471
|
+
this.handleServiceUnavailable(
|
|
472
|
+
new ServiceError('service_unavailable', 'Risk engine is temporarily unavailable'),
|
|
473
|
+
);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (result.success || result.decision === 'pass') {
|
|
478
|
+
sdkLogger.info('SDK', 'handleResult: pass — showing success');
|
|
479
|
+
this.container.showResult(true, t('success'));
|
|
480
|
+
this.sessionPersist.recordVerify(this.sessionId).catch(() => {});
|
|
481
|
+
this.config.onSuccess?.(result.validateToken, result.riskLabels || []);
|
|
482
|
+
} else if (result.nextChallenge) {
|
|
483
|
+
this.escalationDepth++;
|
|
484
|
+
if (this.escalationDepth > EngageLabCaptcha.MAX_ESCALATION_DEPTH) {
|
|
485
|
+
sdkLogger.warn('SDK', `handleResult: escalation depth exceeded (${this.escalationDepth}), giving up`);
|
|
486
|
+
this.container.showResult(false, t('fail'));
|
|
487
|
+
this.config.onFail?.({ code: 'escalation_limit', message: 'Too many challenge attempts' });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
sdkLogger.info('SDK', `handleResult: challenge — next=${result.nextChallenge} (depth=${this.escalationDepth})`);
|
|
491
|
+
this.container.close();
|
|
492
|
+
this.runChallenge(result.nextChallenge, initResp).catch((err) => {
|
|
493
|
+
sdkLogger.error('SDK', `Challenge escalation failed: ${err}`);
|
|
494
|
+
if (err instanceof ServiceError) {
|
|
495
|
+
this.handleServiceUnavailable(err);
|
|
496
|
+
} else {
|
|
497
|
+
this.container.showResult(false, t('fail'));
|
|
498
|
+
this.config.onFail?.({ code: 'challenge_error', message: String(err) });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
} else {
|
|
502
|
+
sdkLogger.warn('SDK', 'handleResult: deny');
|
|
503
|
+
this.container.showResult(false, t('fail'));
|
|
504
|
+
this.config.onFail?.({ code: 'denied', message: `Risk score: ${result.score}` });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private handleServiceUnavailable(err: ServiceError) {
|
|
509
|
+
this.container.close();
|
|
510
|
+
const error = { code: err.code, message: err.message };
|
|
511
|
+
if (this.config.onError) {
|
|
512
|
+
sdkLogger.info('SDK', 'Service unavailable — calling onError callback');
|
|
513
|
+
this.config.onError(error);
|
|
514
|
+
} else {
|
|
515
|
+
sdkLogger.info('SDK', 'Service unavailable — no onError callback, falling back to onFail');
|
|
516
|
+
this.config.onFail?.(error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export { registerWebComponent, EngageLabCaptchaElement } from './web-component';
|
|
522
|
+
|
|
523
|
+
if (typeof window !== 'undefined') {
|
|
524
|
+
(window as any).EngageLabCaptcha = EngageLabCaptcha;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export default EngageLabCaptcha;
|
package/src/integrity.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { IntegrityData } from './types';
|
|
2
|
+
import { sdkLogger } from './logger';
|
|
3
|
+
|
|
4
|
+
export function checkIntegrity(): IntegrityData {
|
|
5
|
+
const patchedFunctions: string[] = [];
|
|
6
|
+
|
|
7
|
+
const fnChecks = [
|
|
8
|
+
{ name: 'fetch', fn: window.fetch },
|
|
9
|
+
{ name: 'XMLHttpRequest.send', fn: XMLHttpRequest.prototype.send },
|
|
10
|
+
{ name: 'XMLHttpRequest.open', fn: XMLHttpRequest.prototype.open },
|
|
11
|
+
{ name: 'canvas.toDataURL', fn: HTMLCanvasElement.prototype.toDataURL },
|
|
12
|
+
{ name: 'canvas.getContext', fn: HTMLCanvasElement.prototype.getContext },
|
|
13
|
+
{ name: 'Date.now', fn: Date.now },
|
|
14
|
+
{ name: 'performance.now', fn: performance.now },
|
|
15
|
+
{ name: 'Math.random', fn: Math.random },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
for (const { name, fn } of fnChecks) {
|
|
19
|
+
if (fn) {
|
|
20
|
+
try {
|
|
21
|
+
const str = Function.prototype.toString.call(fn);
|
|
22
|
+
if (!str.includes('[native code]') && !str.includes('{ [native code] }')) {
|
|
23
|
+
patchedFunctions.push(name);
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
patchedFunctions.push(name + '_error');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const devtoolsDetected = detectDevTools();
|
|
32
|
+
|
|
33
|
+
let sdkHash = '';
|
|
34
|
+
try {
|
|
35
|
+
const criticalCode = [
|
|
36
|
+
collectFingerprint_ref.toString().length,
|
|
37
|
+
detectHeadless_ref.toString().length,
|
|
38
|
+
computePoW_ref.toString().length,
|
|
39
|
+
].join(':');
|
|
40
|
+
sdkHash = simpleHash(criticalCode);
|
|
41
|
+
} catch {
|
|
42
|
+
sdkHash = 'unknown';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const out: IntegrityData = { sdkHash, patchedFunctions, devtoolsDetected };
|
|
46
|
+
sdkLogger.info('Integrity', 'Check complete', {
|
|
47
|
+
sdkHash,
|
|
48
|
+
patchedCount: patchedFunctions.length,
|
|
49
|
+
patchedFunctions,
|
|
50
|
+
devtoolsDetected,
|
|
51
|
+
});
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function detectDevTools(): boolean {
|
|
56
|
+
let detected = false;
|
|
57
|
+
|
|
58
|
+
// Method 1: debugger timing
|
|
59
|
+
const t1 = performance.now();
|
|
60
|
+
// Method 2: console object check
|
|
61
|
+
const element = new Image();
|
|
62
|
+
Object.defineProperty(element, 'id', {
|
|
63
|
+
get: function () { detected = true; return ''; }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Only logs when devtools is open
|
|
68
|
+
console.debug(element);
|
|
69
|
+
} catch {}
|
|
70
|
+
|
|
71
|
+
// Method 3: window size discrepancy (F12 docked)
|
|
72
|
+
const widthThreshold = window.outerWidth - window.innerWidth > 160;
|
|
73
|
+
const heightThreshold = window.outerHeight - window.innerHeight > 160;
|
|
74
|
+
if (widthThreshold || heightThreshold) {
|
|
75
|
+
detected = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return detected;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function simpleHash(str: string): string {
|
|
82
|
+
let hash = 0x811c9dc5;
|
|
83
|
+
for (let i = 0; i < str.length; i++) {
|
|
84
|
+
hash ^= str.charCodeAt(i);
|
|
85
|
+
hash = (hash * 0x01000193) >>> 0;
|
|
86
|
+
}
|
|
87
|
+
return hash.toString(16);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// References to critical functions for SDK hash computation.
|
|
91
|
+
// These will be set by the main module during initialization.
|
|
92
|
+
let collectFingerprint_ref: Function = () => {};
|
|
93
|
+
let detectHeadless_ref: Function = () => {};
|
|
94
|
+
let computePoW_ref: Function = () => {};
|
|
95
|
+
|
|
96
|
+
export function setFunctionRefs(fp: Function, hl: Function, pow: Function) {
|
|
97
|
+
collectFingerprint_ref = fp;
|
|
98
|
+
detectHeadless_ref = hl;
|
|
99
|
+
computePoW_ref = pow;
|
|
100
|
+
}
|