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,147 @@
1
+ import { t } from '../i18n';
2
+
3
+ export interface GridResult {
4
+ selectedCells: number[];
5
+ clickSequence: { cell: number; timestamp: number }[];
6
+ passtime: number;
7
+ }
8
+
9
+ export class GridChallenge {
10
+ private startTime = 0;
11
+ private selected = new Set<number>();
12
+ private clickSequence: { cell: number; timestamp: number }[] = [];
13
+ private cellElements: HTMLElement[] = [];
14
+
15
+ create(imageBase64: string, instruction: string, gridSize: number): {
16
+ element: HTMLElement;
17
+ getResult: () => GridResult;
18
+ } {
19
+ const wrapper = document.createElement('div');
20
+ wrapper.style.cssText = 'width:100%;max-width:380px;margin:0 auto;';
21
+
22
+ const instructionEl = document.createElement('div');
23
+ instructionEl.style.cssText = 'font-size:14px;color:#333;margin-bottom:10px;text-align:center;font-weight:500;';
24
+ instructionEl.textContent = instruction;
25
+ wrapper.appendChild(instructionEl);
26
+
27
+ const gridWrapper = document.createElement('div');
28
+ gridWrapper.style.cssText = 'position:relative;width:100%;';
29
+ wrapper.appendChild(gridWrapper);
30
+
31
+ const img = document.createElement('img');
32
+ img.src = `data:image/png;base64,${imageBase64}`;
33
+ img.style.cssText = 'width:100%;display:block;border-radius:8px;user-select:none;';
34
+ gridWrapper.appendChild(img);
35
+
36
+ const overlay = document.createElement('div');
37
+ overlay.style.cssText = `
38
+ position:absolute;top:0;left:0;width:100%;height:100%;
39
+ display:grid;grid-template-columns:repeat(${gridSize},1fr);
40
+ grid-template-rows:repeat(${gridSize},1fr);gap:2px;padding:2px;
41
+ border-radius:8px;overflow:hidden;
42
+ `;
43
+ overlay.setAttribute('role', 'grid');
44
+ overlay.setAttribute('aria-label', instruction);
45
+ gridWrapper.appendChild(overlay);
46
+
47
+ this.cellElements = [];
48
+ const totalCells = gridSize * gridSize;
49
+
50
+ for (let i = 0; i < totalCells; i++) {
51
+ const cell = document.createElement('div');
52
+ cell.style.cssText = `
53
+ cursor:pointer;border:2px solid transparent;border-radius:4px;
54
+ transition:background 0.15s,border-color 0.15s;
55
+ display:flex;align-items:center;justify-content:center;
56
+ `;
57
+ cell.dataset.cell = String(i);
58
+ cell.setAttribute('role', 'gridcell');
59
+ cell.setAttribute('aria-selected', 'false');
60
+ cell.setAttribute('tabindex', i === 0 ? '0' : '-1');
61
+ cell.setAttribute('aria-label', `Cell ${Math.floor(i / gridSize) + 1}-${(i % gridSize) + 1}`);
62
+
63
+ cell.addEventListener('click', () => this.toggleCell(i, cell));
64
+ cell.addEventListener('keydown', (e: KeyboardEvent) => {
65
+ let nextIdx = i;
66
+ if (e.key === 'ArrowRight') nextIdx = Math.min(i + 1, totalCells - 1);
67
+ else if (e.key === 'ArrowLeft') nextIdx = Math.max(i - 1, 0);
68
+ else if (e.key === 'ArrowDown') nextIdx = Math.min(i + gridSize, totalCells - 1);
69
+ else if (e.key === 'ArrowUp') nextIdx = Math.max(i - gridSize, 0);
70
+ else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.toggleCell(i, cell); return; }
71
+ else return;
72
+ e.preventDefault();
73
+ const nextCell = this.cellElements[nextIdx];
74
+ if (nextCell) {
75
+ cell.setAttribute('tabindex', '-1');
76
+ nextCell.setAttribute('tabindex', '0');
77
+ nextCell.focus();
78
+ }
79
+ });
80
+ cell.addEventListener('mouseenter', () => {
81
+ if (!this.selected.has(i)) {
82
+ cell.style.background = 'rgba(24,144,255,0.1)';
83
+ }
84
+ });
85
+ cell.addEventListener('mouseleave', () => {
86
+ if (!this.selected.has(i)) {
87
+ cell.style.background = 'transparent';
88
+ }
89
+ });
90
+
91
+ overlay.appendChild(cell);
92
+ this.cellElements.push(cell);
93
+ }
94
+
95
+ const hint = document.createElement('div');
96
+ hint.style.cssText = 'font-size:12px;color:#999;text-align:center;margin-top:8px;';
97
+ hint.textContent = `已选 0 个`;
98
+ wrapper.appendChild(hint);
99
+
100
+ const updateHint = () => {
101
+ hint.textContent = `已选 ${this.selected.size} 个`;
102
+ };
103
+
104
+ const origToggle = this.toggleCell.bind(this);
105
+ this.toggleCell = (idx: number, el: HTMLElement) => {
106
+ origToggle(idx, el);
107
+ updateHint();
108
+ };
109
+
110
+ this.startTime = Date.now();
111
+
112
+ return {
113
+ element: wrapper,
114
+ getResult: () => ({
115
+ selectedCells: Array.from(this.selected).sort((a, b) => a - b),
116
+ clickSequence: this.clickSequence,
117
+ passtime: Date.now() - this.startTime,
118
+ }),
119
+ };
120
+ }
121
+
122
+ private toggleCell(index: number, el: HTMLElement): void {
123
+ if (this.selected.has(index)) {
124
+ this.selected.delete(index);
125
+ el.style.background = 'transparent';
126
+ el.style.borderColor = 'transparent';
127
+ el.innerHTML = '';
128
+ el.setAttribute('aria-selected', 'false');
129
+ } else {
130
+ this.selected.add(index);
131
+ el.style.background = 'rgba(24,144,255,0.25)';
132
+ el.style.borderColor = '#1890ff';
133
+ el.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" fill="#1890ff"/></svg>';
134
+ el.setAttribute('aria-selected', 'true');
135
+ }
136
+
137
+ this.clickSequence.push({
138
+ cell: index,
139
+ timestamp: Date.now(),
140
+ });
141
+ }
142
+
143
+ destroy(): void {
144
+ this.cellElements = [];
145
+ this.selected.clear();
146
+ }
147
+ }
@@ -0,0 +1,106 @@
1
+ import { t } from '../i18n';
2
+
3
+ export interface IconResult {
4
+ clicks: { x: number; y: number; timestamp: number; index: number }[];
5
+ passtime: number;
6
+ }
7
+
8
+ export class IconChallenge {
9
+ private resolve?: (result: IconResult) => void;
10
+ private clicks: { x: number; y: number; timestamp: number; index: number }[] = [];
11
+ private startTime = 0;
12
+
13
+ create(imageBase64: string, instruction: string, iconCount: number): {
14
+ element: HTMLElement;
15
+ promise: Promise<IconResult>;
16
+ } {
17
+ this.clicks = [];
18
+ this.startTime = performance.now();
19
+
20
+ const wrapper = document.createElement('div');
21
+ const title = document.createElement('div');
22
+ title.className = 'elc-title';
23
+ title.textContent = instruction;
24
+
25
+ const imgContainer = document.createElement('div');
26
+ imgContainer.className = 'elc-image-container';
27
+ imgContainer.style.position = 'relative';
28
+ imgContainer.style.display = 'inline-block';
29
+
30
+ const img = document.createElement('img');
31
+ img.src = `data:image/png;base64,${imageBase64}`;
32
+ img.alt = t('challenge.imageAlt');
33
+ img.draggable = false;
34
+ img.style.display = 'block';
35
+ img.setAttribute('role', 'img');
36
+ imgContainer.appendChild(img);
37
+
38
+ const markerLayer = document.createElement('div');
39
+ markerLayer.style.cssText = 'position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;';
40
+ imgContainer.appendChild(markerLayer);
41
+
42
+ const clickInfo = document.createElement('div');
43
+ clickInfo.style.cssText = 'text-align:center;font-size:13px;color:#888;margin:8px 0;';
44
+ clickInfo.textContent = `已选择 0 个`;
45
+
46
+ const promise = new Promise<IconResult>((resolve) => {
47
+ this.resolve = resolve;
48
+ });
49
+
50
+ img.addEventListener('click', (e) => {
51
+ const rect = img.getBoundingClientRect();
52
+ const x = e.clientX - rect.left;
53
+ const y = e.clientY - rect.top;
54
+ const relX = x / rect.width;
55
+ const relY = y / rect.height;
56
+
57
+ this.clicks.push({
58
+ x: relX,
59
+ y: relY,
60
+ timestamp: performance.now(),
61
+ index: this.clicks.length,
62
+ });
63
+
64
+ const marker = document.createElement('div');
65
+ marker.style.cssText = `
66
+ position:absolute; left:${relX * 100}%; top:${relY * 100}%;
67
+ width:28px; height:28px; margin:-14px 0 0 -14px;
68
+ background:#4CAF50; color:#fff; border-radius:50%;
69
+ display:flex; align-items:center; justify-content:center;
70
+ font-size:12px; font-weight:bold; box-shadow:0 2px 6px rgba(0,0,0,0.3);
71
+ pointer-events:none;
72
+ `;
73
+ marker.textContent = String(this.clicks.length);
74
+ markerLayer.appendChild(marker);
75
+
76
+ clickInfo.textContent = `已选择 ${this.clicks.length} 个`;
77
+ });
78
+
79
+ const submitBtn = document.createElement('button');
80
+ submitBtn.className = 'elc-submit';
81
+ submitBtn.textContent = t('icons.confirm');
82
+ submitBtn.addEventListener('click', () => {
83
+ this.resolve?.({
84
+ clicks: this.clicks,
85
+ passtime: performance.now() - this.startTime,
86
+ });
87
+ });
88
+
89
+ const resetBtn = document.createElement('button');
90
+ resetBtn.style.cssText = 'width:100%;padding:8px;margin-top:8px;border:1px solid #ddd;border-radius:8px;background:#fff;cursor:pointer;font-size:13px;color:#666;';
91
+ resetBtn.textContent = t('icons.reset');
92
+ resetBtn.addEventListener('click', () => {
93
+ this.clicks = [];
94
+ markerLayer.innerHTML = '';
95
+ clickInfo.textContent = '已选择 0 个';
96
+ });
97
+
98
+ wrapper.appendChild(title);
99
+ wrapper.appendChild(imgContainer);
100
+ wrapper.appendChild(clickInfo);
101
+ wrapper.appendChild(submitBtn);
102
+ wrapper.appendChild(resetBtn);
103
+
104
+ return { element: wrapper, promise };
105
+ }
106
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Invisible (one-click) verification mode.
3
+ * Zero interaction - runs fingerprint + PoW + behavior scoring in background.
4
+ */
5
+ import { t } from '../i18n';
6
+
7
+ export class InvisibleChallenge {
8
+ private element: HTMLElement | null = null;
9
+ private verified = false;
10
+
11
+ createTrigger(onClick: () => void): HTMLElement {
12
+ this.element = document.createElement('div');
13
+ this.element.className = 'elc-invisible-btn';
14
+ this.element.setAttribute('role', 'checkbox');
15
+ this.element.setAttribute('aria-checked', 'false');
16
+ this.element.setAttribute('aria-label', t('invisible.button'));
17
+ this.element.setAttribute('tabindex', '0');
18
+ this.element.innerHTML = `
19
+ <div class="elc-check" aria-hidden="true"></div>
20
+ <span>${t('invisible.button')}</span>
21
+ `;
22
+ this.element.addEventListener('click', onClick);
23
+ this.element.addEventListener('keydown', (e: KeyboardEvent) => {
24
+ if (e.key === 'Enter' || e.key === ' ') {
25
+ e.preventDefault();
26
+ onClick();
27
+ }
28
+ });
29
+ return this.element;
30
+ }
31
+
32
+ showLoading() {
33
+ if (!this.element) return;
34
+ const check = this.element.querySelector('.elc-check') as HTMLElement;
35
+ if (check) {
36
+ check.innerHTML = '<div class="elc-loading-spinner" style="width:18px;height:18px;border-width:2px;"></div>';
37
+ }
38
+ }
39
+
40
+ showSuccess() {
41
+ this.verified = true;
42
+ if (!this.element) return;
43
+ this.element.classList.add('verified');
44
+ this.element.setAttribute('aria-checked', 'true');
45
+ const check = this.element.querySelector('.elc-check') as HTMLElement;
46
+ if (check) check.innerHTML = '✓';
47
+ }
48
+
49
+ showFail() {
50
+ if (!this.element) return;
51
+ const check = this.element.querySelector('.elc-check') as HTMLElement;
52
+ if (check) {
53
+ check.innerHTML = '✗';
54
+ check.style.color = '#f44336';
55
+ }
56
+ }
57
+ }