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.
Files changed (73) hide show
  1. package/dist/captcha-sdk.esm.js +1 -0
  2. package/dist/captcha-sdk.umd.js +1 -0
  3. package/dist/types/api.d.ts +12 -0
  4. package/dist/types/challenges/dragsort.d.ts +24 -0
  5. package/dist/types/challenges/grid.d.ts +20 -0
  6. package/dist/types/challenges/icons.d.ts +18 -0
  7. package/dist/types/challenges/invisible.d.ts +8 -0
  8. package/dist/types/challenges/physics.d.ts +40 -0
  9. package/dist/types/challenges/rotate.d.ts +14 -0
  10. package/dist/types/challenges/slider.d.ts +17 -0
  11. package/dist/types/challenges/spatial.d.ts +12 -0
  12. package/dist/types/compat/hcaptcha.d.ts +1 -0
  13. package/dist/types/compat/recaptcha.d.ts +1 -0
  14. package/dist/types/crypto.d.ts +11 -0
  15. package/dist/types/deep_probes.d.ts +13 -0
  16. package/dist/types/degradation.d.ts +27 -0
  17. package/dist/types/device_attestation.d.ts +19 -0
  18. package/dist/types/fingerprint.d.ts +2 -0
  19. package/dist/types/headless.d.ts +2 -0
  20. package/dist/types/i18n.d.ts +7 -0
  21. package/dist/types/index.d.ts +36 -0
  22. package/dist/types/integrity.d.ts +3 -0
  23. package/dist/types/logger.d.ts +49 -0
  24. package/dist/types/native_monitor.d.ts +21 -0
  25. package/dist/types/passive_signals.d.ts +119 -0
  26. package/dist/types/pow.d.ts +2 -0
  27. package/dist/types/reporter.d.ts +8 -0
  28. package/dist/types/session_persist.d.ts +18 -0
  29. package/dist/types/theme.d.ts +11 -0
  30. package/dist/types/timing.d.ts +2 -0
  31. package/dist/types/trajectory.d.ts +18 -0
  32. package/dist/types/types.d.ts +182 -0
  33. package/dist/types/ui/container.d.ts +20 -0
  34. package/dist/types/ui/styles.d.ts +1 -0
  35. package/dist/types/wasm_core.d.ts +44 -0
  36. package/dist/types/web-component.d.ts +11 -0
  37. package/package.json +22 -0
  38. package/rollup.config.mjs +62 -0
  39. package/src/api.ts +180 -0
  40. package/src/challenges/dragsort.ts +168 -0
  41. package/src/challenges/grid.ts +147 -0
  42. package/src/challenges/icons.ts +106 -0
  43. package/src/challenges/invisible.ts +57 -0
  44. package/src/challenges/physics.ts +437 -0
  45. package/src/challenges/rotate.ts +81 -0
  46. package/src/challenges/slider.ts +168 -0
  47. package/src/challenges/spatial.ts +91 -0
  48. package/src/compat/hcaptcha.ts +112 -0
  49. package/src/compat/recaptcha.ts +108 -0
  50. package/src/crypto.ts +69 -0
  51. package/src/deep_probes.ts +690 -0
  52. package/src/degradation.ts +113 -0
  53. package/src/device_attestation.ts +109 -0
  54. package/src/fingerprint.ts +247 -0
  55. package/src/headless.ts +233 -0
  56. package/src/i18n.ts +455 -0
  57. package/src/index.ts +527 -0
  58. package/src/integrity.ts +100 -0
  59. package/src/logger.ts +170 -0
  60. package/src/native_monitor.ts +100 -0
  61. package/src/passive_signals.ts +544 -0
  62. package/src/pow.ts +120 -0
  63. package/src/reporter.ts +75 -0
  64. package/src/session_persist.ts +90 -0
  65. package/src/theme.ts +41 -0
  66. package/src/timing.ts +79 -0
  67. package/src/trajectory.ts +110 -0
  68. package/src/types.ts +199 -0
  69. package/src/ui/container.ts +161 -0
  70. package/src/ui/styles.ts +153 -0
  71. package/src/wasm_core.ts +189 -0
  72. package/src/web-component.ts +103 -0
  73. package/tsconfig.json +18 -0
@@ -0,0 +1,91 @@
1
+ import { t } from '../i18n';
2
+
3
+ export interface SpatialResult {
4
+ answerIndex: number;
5
+ passtime: number;
6
+ }
7
+
8
+ export class SpatialChallenge {
9
+ private resolve?: (result: SpatialResult) => void;
10
+ private startTime = 0;
11
+
12
+ create(imageBase64: string, question: string, options: string[]): {
13
+ element: HTMLElement;
14
+ promise: Promise<SpatialResult>;
15
+ } {
16
+ this.startTime = performance.now();
17
+
18
+ const wrapper = document.createElement('div');
19
+ const title = document.createElement('div');
20
+ title.className = 'elc-title';
21
+ title.textContent = t('spatial.title');
22
+
23
+ const imgContainer = document.createElement('div');
24
+ imgContainer.className = 'elc-image-container';
25
+ const img = document.createElement('img');
26
+ img.src = `data:image/png;base64,${imageBase64}`;
27
+ img.alt = t('challenge.imageAlt');
28
+ img.draggable = false;
29
+ imgContainer.appendChild(img);
30
+
31
+ const questionEl = document.createElement('div');
32
+ questionEl.className = 'elc-question';
33
+ questionEl.textContent = question;
34
+
35
+ const optionsEl = document.createElement('div');
36
+ optionsEl.className = 'elc-options';
37
+ optionsEl.setAttribute('role', 'radiogroup');
38
+ optionsEl.setAttribute('aria-label', question);
39
+
40
+ const promise = new Promise<SpatialResult>((resolve) => {
41
+ this.resolve = resolve;
42
+ });
43
+
44
+ options.forEach((opt, idx) => {
45
+ const btn = document.createElement('div');
46
+ btn.className = 'elc-option';
47
+ btn.setAttribute('role', 'radio');
48
+ btn.setAttribute('aria-checked', 'false');
49
+ btn.setAttribute('tabindex', idx === 0 ? '0' : '-1');
50
+ btn.textContent = `${String.fromCharCode(65 + idx)}. ${opt}`;
51
+
52
+ const selectOption = () => {
53
+ optionsEl.querySelectorAll('.elc-option').forEach(el => {
54
+ el.classList.remove('selected');
55
+ el.setAttribute('aria-checked', 'false');
56
+ el.setAttribute('tabindex', '-1');
57
+ });
58
+ btn.classList.add('selected');
59
+ btn.setAttribute('aria-checked', 'true');
60
+ btn.setAttribute('tabindex', '0');
61
+ btn.focus();
62
+ const passtime = performance.now() - this.startTime;
63
+ this.resolve?.({ answerIndex: idx, passtime });
64
+ };
65
+
66
+ btn.addEventListener('click', selectOption);
67
+ btn.addEventListener('keydown', (e: KeyboardEvent) => {
68
+ if (e.key === 'Enter' || e.key === ' ') {
69
+ e.preventDefault();
70
+ selectOption();
71
+ } else if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
72
+ e.preventDefault();
73
+ const next = optionsEl.children[(idx + 1) % options.length] as HTMLElement;
74
+ next?.focus();
75
+ } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
76
+ e.preventDefault();
77
+ const prev = optionsEl.children[(idx - 1 + options.length) % options.length] as HTMLElement;
78
+ prev?.focus();
79
+ }
80
+ });
81
+ optionsEl.appendChild(btn);
82
+ });
83
+
84
+ wrapper.appendChild(title);
85
+ wrapper.appendChild(imgContainer);
86
+ wrapper.appendChild(questionEl);
87
+ wrapper.appendChild(optionsEl);
88
+
89
+ return { element: wrapper, promise };
90
+ }
91
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * hCaptcha compatibility adapter.
3
+ *
4
+ * Registers `window.hcaptcha` with the standard API surface:
5
+ * hcaptcha.render(el, params), hcaptcha.execute(widgetId), hcaptcha.reset(widgetId),
6
+ * hcaptcha.getResponse(widgetId)
7
+ *
8
+ * All calls delegate to an internal EngageLabCaptcha instance.
9
+ */
10
+ import { EngageLabCaptcha } from '../index';
11
+ import type { CaptchaConfig } from '../types';
12
+
13
+ interface HCaptchaRenderParams {
14
+ sitekey?: string;
15
+ callback?: (token: string) => void;
16
+ 'expired-callback'?: () => void;
17
+ 'error-callback'?: (err: any) => void;
18
+ size?: 'invisible' | 'normal' | 'compact';
19
+ theme?: 'light' | 'dark';
20
+ }
21
+
22
+ interface HCaptchaApi {
23
+ render: (container: string | HTMLElement, params: HCaptchaRenderParams) => string;
24
+ execute: (widgetId?: string) => Promise<{ response: string }>;
25
+ reset: (widgetId?: string) => void;
26
+ getResponse: (widgetId?: string) => string;
27
+ }
28
+
29
+ const instances = new Map<string, { captcha: EngageLabCaptcha; params: HCaptchaRenderParams; lastToken: string }>();
30
+ let nextId = 0;
31
+
32
+ function createHCaptcha(apiBase: string): HCaptchaApi {
33
+ return {
34
+ render(container: string | HTMLElement, params: HCaptchaRenderParams): string {
35
+ const widgetId = `hc-${nextId++}`;
36
+ const siteKey = params.sitekey || '';
37
+
38
+ const config: CaptchaConfig = {
39
+ siteKey,
40
+ apiBase,
41
+ mode: params.size === 'invisible' ? 'invisible' : 'popup',
42
+ theme: params.theme === 'dark' ? { darkMode: true } : undefined,
43
+ onSuccess: (token) => {
44
+ const entry = instances.get(widgetId);
45
+ if (entry) entry.lastToken = token;
46
+ params.callback?.(token);
47
+ },
48
+ onFail: (err) => {
49
+ params['error-callback']?.(err);
50
+ },
51
+ };
52
+
53
+ const captcha = new EngageLabCaptcha(config);
54
+ instances.set(widgetId, { captcha, params, lastToken: '' });
55
+
56
+ if (params.size !== 'invisible') {
57
+ const el = typeof container === 'string'
58
+ ? document.getElementById(container)
59
+ : container;
60
+ if (el) {
61
+ const trigger = document.createElement('div');
62
+ trigger.style.cssText = 'cursor:pointer;display:inline-block;';
63
+ trigger.textContent = "I'm not a robot";
64
+ trigger.addEventListener('click', () => captcha.execute());
65
+ el.appendChild(trigger);
66
+ }
67
+ }
68
+
69
+ return widgetId;
70
+ },
71
+
72
+ execute(widgetId?: string): Promise<{ response: string }> {
73
+ const id = widgetId || Array.from(instances.keys())[0];
74
+ const entry = instances.get(id);
75
+ if (!entry) return Promise.reject(new Error('No hCaptcha widget found'));
76
+
77
+ return new Promise((resolve, reject) => {
78
+ const origCallback = entry.params.callback;
79
+ entry.params.callback = (token: string) => {
80
+ entry.lastToken = token;
81
+ origCallback?.(token);
82
+ resolve({ response: token });
83
+ };
84
+ const origError = entry.params['error-callback'];
85
+ entry.params['error-callback'] = (err: any) => {
86
+ origError?.(err);
87
+ reject(err);
88
+ };
89
+ entry.captcha.execute();
90
+ });
91
+ },
92
+
93
+ reset(widgetId?: string) {
94
+ const id = widgetId || Array.from(instances.keys())[0];
95
+ const entry = instances.get(id);
96
+ if (entry) {
97
+ entry.lastToken = '';
98
+ entry.captcha.reset();
99
+ }
100
+ },
101
+
102
+ getResponse(widgetId?: string): string {
103
+ const id = widgetId || Array.from(instances.keys())[0];
104
+ return instances.get(id)?.lastToken || '';
105
+ },
106
+ };
107
+ }
108
+
109
+ export function installHCaptchaCompat(apiBase: string): void {
110
+ if (typeof window === 'undefined') return;
111
+ (window as any).hcaptcha = createHCaptcha(apiBase);
112
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * reCAPTCHA v2/v3 compatibility adapter.
3
+ *
4
+ * Registers `window.grecaptcha` with the standard API surface:
5
+ * grecaptcha.ready(cb), grecaptcha.render(el, params), grecaptcha.execute(siteKey, opts),
6
+ * grecaptcha.reset(widgetId)
7
+ *
8
+ * All calls delegate to an internal EngageLabCaptcha instance.
9
+ */
10
+ import { EngageLabCaptcha } from '../index';
11
+ import type { CaptchaConfig } from '../types';
12
+
13
+ interface GrecaptchaRenderParams {
14
+ sitekey?: string;
15
+ callback?: (token: string) => void;
16
+ 'expired-callback'?: () => void;
17
+ 'error-callback'?: (err: any) => void;
18
+ size?: 'invisible' | 'normal' | 'compact';
19
+ theme?: 'light' | 'dark';
20
+ }
21
+
22
+ interface GrecaptchaApi {
23
+ ready: (cb: () => void) => void;
24
+ render: (container: string | HTMLElement, params: GrecaptchaRenderParams) => number;
25
+ execute: (siteKey?: string, opts?: { action?: string }) => Promise<string>;
26
+ reset: (widgetId?: number) => void;
27
+ }
28
+
29
+ const instances = new Map<number, { captcha: EngageLabCaptcha; params: GrecaptchaRenderParams }>();
30
+ let nextWidgetId = 0;
31
+
32
+ function createGrecaptcha(apiBase: string): GrecaptchaApi {
33
+ return {
34
+ ready(cb: () => void) {
35
+ if (typeof cb === 'function') cb();
36
+ },
37
+
38
+ render(container: string | HTMLElement, params: GrecaptchaRenderParams): number {
39
+ const widgetId = nextWidgetId++;
40
+ const siteKey = params.sitekey || '';
41
+
42
+ const config: CaptchaConfig = {
43
+ siteKey,
44
+ apiBase,
45
+ mode: params.size === 'invisible' ? 'invisible' : 'popup',
46
+ theme: params.theme === 'dark' ? { darkMode: true } : undefined,
47
+ onSuccess: (token) => {
48
+ params.callback?.(token);
49
+ },
50
+ onFail: (err) => {
51
+ params['error-callback']?.(err);
52
+ },
53
+ };
54
+
55
+ const captcha = new EngageLabCaptcha(config);
56
+ instances.set(widgetId, { captcha, params });
57
+
58
+ if (params.size !== 'invisible') {
59
+ const el = typeof container === 'string'
60
+ ? document.getElementById(container)
61
+ : container;
62
+ if (el) {
63
+ const trigger = document.createElement('div');
64
+ trigger.style.cssText = 'cursor:pointer;display:inline-block;';
65
+ trigger.textContent = "I'm not a robot";
66
+ trigger.addEventListener('click', () => captcha.execute());
67
+ el.appendChild(trigger);
68
+ }
69
+ }
70
+
71
+ return widgetId;
72
+ },
73
+
74
+ execute(siteKey?: string, _opts?: { action?: string }): Promise<string> {
75
+ const entry = siteKey
76
+ ? Array.from(instances.values()).find(e => e.params.sitekey === siteKey)
77
+ : instances.get(0);
78
+
79
+ if (!entry) {
80
+ return Promise.reject(new Error('No reCAPTCHA widget found'));
81
+ }
82
+
83
+ return new Promise((resolve, reject) => {
84
+ const origSuccess = entry.params.callback;
85
+ entry.params.callback = (token: string) => {
86
+ origSuccess?.(token);
87
+ resolve(token);
88
+ };
89
+ const origError = entry.params['error-callback'];
90
+ entry.params['error-callback'] = (err: any) => {
91
+ origError?.(err);
92
+ reject(err);
93
+ };
94
+ entry.captcha.execute();
95
+ });
96
+ },
97
+
98
+ reset(widgetId?: number) {
99
+ const entry = instances.get(widgetId ?? 0);
100
+ entry?.captcha.reset();
101
+ },
102
+ };
103
+ }
104
+
105
+ export function installRecaptchaCompat(apiBase: string): void {
106
+ if (typeof window === 'undefined') return;
107
+ (window as any).grecaptcha = createGrecaptcha(apiBase);
108
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Communication security: AES-256-GCM encryption + HMAC-SHA256 signing.
3
+ * MVP uses a simplified approach with server-provided key material.
4
+ */
5
+
6
+ export async function encryptPayload(data: any, sessionKey: string): Promise<{ encrypted: string; iv: string; hmac: string }> {
7
+ const plaintext = JSON.stringify(data);
8
+ const encoder = new TextEncoder();
9
+
10
+ const sessionKeyBytes = encoder.encode(sessionKey);
11
+ const hashBuffer = await crypto.subtle.digest('SHA-256', sessionKeyBytes);
12
+ const aesKeyMaterial = await crypto.subtle.importKey(
13
+ 'raw', hashBuffer,
14
+ { name: 'AES-GCM' }, false, ['encrypt']
15
+ );
16
+
17
+ const iv = crypto.getRandomValues(new Uint8Array(12));
18
+ const encrypted = await crypto.subtle.encrypt(
19
+ { name: 'AES-GCM', iv },
20
+ aesKeyMaterial,
21
+ encoder.encode(plaintext)
22
+ );
23
+
24
+ const hmacKey = await crypto.subtle.importKey(
25
+ 'raw', encoder.encode(sessionKey),
26
+ { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
27
+ );
28
+ const signature = await crypto.subtle.sign('HMAC', hmacKey, encrypted);
29
+
30
+ return {
31
+ encrypted: arrayBufferToBase64(encrypted),
32
+ iv: arrayBufferToBase64(iv),
33
+ hmac: arrayBufferToBase64(signature),
34
+ };
35
+ }
36
+
37
+ export async function createRequestSignature(
38
+ method: string, path: string, body: string, timestamp: number, nonce: string, secretKey: string,
39
+ ): Promise<string> {
40
+ const message = `${method}\n${path}\n${body}\n${timestamp}\n${nonce}`;
41
+ const encoder = new TextEncoder();
42
+ const key = await crypto.subtle.importKey(
43
+ 'raw', encoder.encode(secretKey),
44
+ { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
45
+ );
46
+ const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
47
+ return arrayBufferToBase64(sig);
48
+ }
49
+
50
+ export function generateNonce(): string {
51
+ const arr = crypto.getRandomValues(new Uint8Array(16));
52
+ return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
53
+ }
54
+
55
+ function arrayBufferToBase64(buffer: ArrayBuffer | ArrayBufferView): string {
56
+ let bytes: Uint8Array;
57
+ if (buffer instanceof Uint8Array) {
58
+ bytes = buffer;
59
+ } else if (buffer instanceof ArrayBuffer) {
60
+ bytes = new Uint8Array(buffer);
61
+ } else {
62
+ bytes = new Uint8Array((buffer as ArrayBufferView).buffer, (buffer as ArrayBufferView).byteOffset, (buffer as ArrayBufferView).byteLength);
63
+ }
64
+ let binary = '';
65
+ for (let i = 0; i < bytes.byteLength; i++) {
66
+ binary += String.fromCharCode(bytes[i]);
67
+ }
68
+ return btoa(binary);
69
+ }