ribaunt 0.2.2 → 0.2.4
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/README.md +113 -10
- package/demo/lib/ribaunt-solver.wasm +0 -0
- package/demo/lib/solver-worker.js +174 -0
- package/demo/lib/solver-worker.js.map +1 -0
- package/demo/lib/solver.js +135 -0
- package/demo/lib/solver.js.map +1 -0
- package/demo/lib/wasm-solver.js +296 -0
- package/demo/lib/wasm-solver.js.map +1 -0
- package/demo/lib/widget-browser.js +9 -0
- package/demo/lib/widget-browser.js.map +1 -0
- package/demo/lib/widget.js +942 -0
- package/demo/lib/widget.js.map +1 -0
- package/demo/lib/worker-client.js +128 -0
- package/demo/lib/worker-client.js.map +1 -0
- package/dist/cjs/index.d.ts +48 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +196 -17
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/risk.d.ts +124 -0
- package/dist/cjs/risk.d.ts.map +1 -0
- package/dist/cjs/risk.js +213 -0
- package/dist/cjs/risk.js.map +1 -0
- package/dist/index.d.ts +48 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +193 -16
- package/dist/index.js.map +1 -1
- package/dist/ribaunt-solver.wasm +0 -0
- package/dist/risk.d.ts +124 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +202 -0
- package/dist/risk.js.map +1 -0
- package/dist/solver-worker.d.ts.map +1 -1
- package/dist/solver-worker.js +166 -7
- package/dist/solver-worker.js.map +1 -1
- package/dist/solver.d.ts +1 -0
- package/dist/solver.d.ts.map +1 -1
- package/dist/solver.js +38 -3
- package/dist/solver.js.map +1 -1
- package/dist/wasm-solver.d.ts +29 -0
- package/dist/wasm-solver.d.ts.map +1 -0
- package/dist/wasm-solver.js +296 -0
- package/dist/wasm-solver.js.map +1 -0
- package/dist/widget-browser.d.ts +3 -2
- package/dist/widget-browser.d.ts.map +1 -1
- package/dist/widget-browser.js +1 -1
- package/dist/widget-browser.js.map +1 -1
- package/dist/widget-react.d.ts +4 -1
- package/dist/widget-react.d.ts.map +1 -1
- package/dist/widget-react.js +102 -14
- package/dist/widget-react.js.map +1 -1
- package/dist/widget.d.ts +42 -14
- package/dist/widget.d.ts.map +1 -1
- package/dist/widget.js +254 -12
- package/dist/widget.js.map +1 -1
- package/dist/worker-client.d.ts +7 -1
- package/dist/worker-client.d.ts.map +1 -1
- package/dist/worker-client.js +69 -19
- package/dist/worker-client.js.map +1 -1
- package/package.json +22 -9
- package/context7.json +0 -4
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ribaunt CAPTCHA Widget - Web Component
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```html
|
|
6
|
+
* <ribaunt-widget
|
|
7
|
+
* challenge-endpoint="/api/challenge"
|
|
8
|
+
* verify-endpoint="/api/verify"
|
|
9
|
+
* auto-verify="true"
|
|
10
|
+
* show-warning="true"
|
|
11
|
+
* warning-message="Custom warning message"
|
|
12
|
+
* show-progress="false"
|
|
13
|
+
* ></ribaunt-widget>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Or in React/Vue/Svelte:
|
|
17
|
+
* ```jsx
|
|
18
|
+
* <ribaunt-widget
|
|
19
|
+
* challengeEndpoint="/api/challenge"
|
|
20
|
+
* verifyEndpoint="/api/verify"
|
|
21
|
+
* />
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Events:
|
|
25
|
+
* - verify: Fired when verification is complete
|
|
26
|
+
* - error: Fired when an error occurs
|
|
27
|
+
* - state-change: Fired when widget state changes
|
|
28
|
+
*/
|
|
29
|
+
import { calibrateBrowser } from './solver.js';
|
|
30
|
+
import { solveChallengeWithWorker, WorkerUnavailableError, } from './worker-client.js';
|
|
31
|
+
const WIDGET_STYLES = `
|
|
32
|
+
/* Widget Container Styles */
|
|
33
|
+
:host,
|
|
34
|
+
:host * {
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.captcha {
|
|
39
|
+
background-color: var(--ribaunt-background, #fdfdfd);
|
|
40
|
+
border: 1px solid var(--ribaunt-border-color, #dddddd8f);
|
|
41
|
+
border-radius: var(--ribaunt-border-radius, 14px);
|
|
42
|
+
user-select: none;
|
|
43
|
+
height: var(--ribaunt-widget-height, 58px);
|
|
44
|
+
width: var(--ribaunt-widget-width, 230px);
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
padding: var(--ribaunt-widget-padding, 14px);
|
|
48
|
+
gap: var(--ribaunt-gap, 15px);
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
transition: filter .2s, transform .2s;
|
|
51
|
+
position: relative;
|
|
52
|
+
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
|
|
53
|
+
overflow: hidden;
|
|
54
|
+
color: var(--ribaunt-color, #212121);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
:host([disabled]) .captcha {
|
|
58
|
+
cursor: not-allowed;
|
|
59
|
+
opacity: 0.72;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:host([disabled]) .captcha:hover {
|
|
63
|
+
filter: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
:host([disabled]) .captcha[data-state=fetching],
|
|
67
|
+
:host([disabled]) .captcha[data-state=solving],
|
|
68
|
+
:host([disabled]) .captcha[data-state=verifying] {
|
|
69
|
+
cursor: progress;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
:host([disabled]) .captcha[data-state=done] {
|
|
73
|
+
cursor: default;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.captcha:hover {
|
|
77
|
+
filter: brightness(98%);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.captcha:focus-visible {
|
|
81
|
+
outline: 3px solid var(--ribaunt-focus-color, #1677ff);
|
|
82
|
+
outline-offset: 3px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Checkbox Styles */
|
|
86
|
+
.checkbox {
|
|
87
|
+
width: var(--ribaunt-checkbox-size, 25px);
|
|
88
|
+
height: var(--ribaunt-checkbox-size, 25px);
|
|
89
|
+
border: var(--ribaunt-checkbox-border, 1px solid #aaaaaad1);
|
|
90
|
+
border-radius: var(--ribaunt-checkbox-border-radius, 6px);
|
|
91
|
+
background-color: var(--ribaunt-checkbox-background, #fafafa91);
|
|
92
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
93
|
+
margin-top: var(--ribaunt-checkbox-margin, 2px);
|
|
94
|
+
margin-bottom: var(--ribaunt-checkbox-margin, 2px);
|
|
95
|
+
flex-shrink: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Content group: spinner + label, side by side */
|
|
99
|
+
.captcha .content {
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: var(--ribaunt-gap, 15px);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Font Family */
|
|
106
|
+
.captcha * {
|
|
107
|
+
font-family: var(--ribaunt-font, system, -apple-system, "BlinkMacSystemFont", ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", "Ubuntu", "arial", sans-serif);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Keyframes */
|
|
111
|
+
@keyframes ribaunt-spin {
|
|
112
|
+
from { transform: scale(1.1) rotate(0deg); }
|
|
113
|
+
to { transform: scale(1.1) rotate(360deg); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@keyframes ribaunt-pop {
|
|
117
|
+
0% { transform: scale(1); }
|
|
118
|
+
60% { transform: scale(1.12); }
|
|
119
|
+
100% { transform: scale(1); }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Label Text */
|
|
123
|
+
.captcha p {
|
|
124
|
+
margin: 0;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
font-size: 15px;
|
|
127
|
+
user-select: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Verifying State */
|
|
131
|
+
.captcha[data-state=fetching] .checkbox,
|
|
132
|
+
.captcha[data-state=solving] .checkbox,
|
|
133
|
+
.captcha[data-state=verifying] .checkbox {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
border: none;
|
|
138
|
+
border-radius: 50%;
|
|
139
|
+
background: conic-gradient(
|
|
140
|
+
var(--ribaunt-spinner-color, #000) 0%,
|
|
141
|
+
var(--ribaunt-spinner-color, #000) var(--progress, 0%),
|
|
142
|
+
var(--ribaunt-spinner-background-color, #eee) var(--progress, 0%),
|
|
143
|
+
var(--ribaunt-spinner-background-color, #eee) 100%
|
|
144
|
+
);
|
|
145
|
+
position: relative;
|
|
146
|
+
animation: ribaunt-spin 2.5s linear infinite;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.captcha[data-state=fetching] .checkbox::after,
|
|
150
|
+
.captcha[data-state=solving] .checkbox::after,
|
|
151
|
+
.captcha[data-state=verifying] .checkbox::after {
|
|
152
|
+
content: "";
|
|
153
|
+
background-color: var(--ribaunt-background, #fdfdfd);
|
|
154
|
+
width: calc(100% - var(--ribaunt-spinner-thickness, 5px));
|
|
155
|
+
height: calc(100% - var(--ribaunt-spinner-thickness, 5px));
|
|
156
|
+
border-radius: 50%;
|
|
157
|
+
margin: calc(var(--ribaunt-spinner-thickness, 5px) / 2);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Done/Success State */
|
|
161
|
+
.captcha[data-state=done] .checkbox {
|
|
162
|
+
border: 1px solid transparent;
|
|
163
|
+
background-image: var(--ribaunt-checkmark, url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cstyle%3E%40keyframes%20anim%7B0%25%7Bstroke-dashoffset%3A23.21320343017578px%7Dto%7Bstroke-dashoffset%3A0%7D%7D%3C%2Fstyle%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2300a67d%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22m5%2012%205%205L20%207%22%20style%3D%22stroke-dashoffset%3A0%3Bstroke-dasharray%3A23.21320343017578px%3Banimation%3Aanim%20.5s%20ease%22%2F%3E%3C%2Fsvg%3E"));
|
|
164
|
+
background-size: cover;
|
|
165
|
+
animation: ribaunt-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
166
|
+
box-shadow: 0 0 0 3px rgba(0, 166, 125, 0.25);
|
|
167
|
+
transition: box-shadow 0.5s ease-out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* Error State */
|
|
171
|
+
.captcha[data-state=error] .checkbox {
|
|
172
|
+
border: 1px solid transparent;
|
|
173
|
+
background-image: var(--ribaunt-error-cross, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 24 24'%3E%3Cpath fill='%23f55b50' d='M11 15h2v2h-2zm0-8h2v6h-2zm1-5C6.47 2 2 6.5 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m0 18a8 8 0 0 1-8-8a8 8 0 0 1 8-8a8 8 0 0 1 8 8a8 8 0 0 1-8 8'/%3E%3C/svg%3E"));
|
|
174
|
+
background-size: cover;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Logo */
|
|
178
|
+
.logo {
|
|
179
|
+
position: absolute;
|
|
180
|
+
bottom: 8px;
|
|
181
|
+
right: 8px;
|
|
182
|
+
width: 20px;
|
|
183
|
+
height: auto;
|
|
184
|
+
opacity: 0.6;
|
|
185
|
+
transition: opacity 0.2s, color 0.2s;
|
|
186
|
+
color: var(--ribaunt-logo-color, #666);
|
|
187
|
+
pointer-events: auto;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.logo svg {
|
|
191
|
+
width: 100%;
|
|
192
|
+
height: auto;
|
|
193
|
+
display: block;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/* Logo color adapts to theme */
|
|
197
|
+
@media (prefers-color-scheme: dark) {
|
|
198
|
+
.captcha {
|
|
199
|
+
background-color: var(--ribaunt-background, #171717);
|
|
200
|
+
border-color: var(--ribaunt-border-color, #454545);
|
|
201
|
+
color: var(--ribaunt-color, #f4f4f4);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.checkbox {
|
|
205
|
+
background-color: var(--ribaunt-checkbox-background, #242424);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.logo {
|
|
209
|
+
color: var(--ribaunt-logo-color, #999);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.captcha[data-state=fetching] .checkbox::after,
|
|
213
|
+
.captcha[data-state=solving] .checkbox::after,
|
|
214
|
+
.captcha[data-state=verifying] .checkbox::after {
|
|
215
|
+
background-color: var(--ribaunt-background, #171717);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.captcha[data-state=fetching] .checkbox,
|
|
219
|
+
.captcha[data-state=solving] .checkbox,
|
|
220
|
+
.captcha[data-state=verifying] .checkbox {
|
|
221
|
+
background: conic-gradient(
|
|
222
|
+
var(--ribaunt-spinner-color, #eee) 0%,
|
|
223
|
+
var(--ribaunt-spinner-color, #eee) var(--progress, 0%),
|
|
224
|
+
var(--ribaunt-spinner-background-color, #333) var(--progress, 0%),
|
|
225
|
+
var(--ribaunt-spinner-background-color, #333) 100%
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Secondary "no percent" spinner: 12 fixed radial bars with a single smooth
|
|
231
|
+
opacity pulse (show-progress="false"). The container never rotates and
|
|
232
|
+
each bar carries one static transform; the only animation is a symmetric
|
|
233
|
+
opacity pulse shared by all bars via a per-bar delay.
|
|
234
|
+
Like the conic loader, the checkbox box morphs into a round spinner on
|
|
235
|
+
entering the loading states: the base .checkbox transition animates the
|
|
236
|
+
square -> circle (border-radius), border removal and transparency, so
|
|
237
|
+
both loaders transforms look identical. Reduced motion kills all shell
|
|
238
|
+
animation (transition: none) while the bars keep their static position.
|
|
239
|
+
The loader group (spinner + label) is left-aligned and shares a single
|
|
240
|
+
color: the bars draw with currentColor, which resolves to the same color
|
|
241
|
+
the "Loading..." text uses. The logo is absolutely positioned, so the
|
|
242
|
+
content group is the only in-flow flex child and sits flush at the left. */
|
|
243
|
+
.captcha[data-state=fetching] .content.bars,
|
|
244
|
+
.captcha[data-state=solving] .content.bars,
|
|
245
|
+
.captcha[data-state=verifying] .content.bars {
|
|
246
|
+
gap: 12px;
|
|
247
|
+
flex-shrink: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.captcha[data-state=fetching] .content.bars .checkbox,
|
|
251
|
+
.captcha[data-state=solving] .content.bars .checkbox,
|
|
252
|
+
.captcha[data-state=verifying] .content.bars .checkbox {
|
|
253
|
+
margin: 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.captcha[data-state=fetching] .content.bars p,
|
|
257
|
+
.captcha[data-state=solving] .content.bars p,
|
|
258
|
+
.captcha[data-state=verifying] .content.bars p {
|
|
259
|
+
flex-shrink: 0;
|
|
260
|
+
line-height: 1.2;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.captcha[data-state=fetching] .checkbox.bars,
|
|
264
|
+
.captcha[data-state=solving] .checkbox.bars,
|
|
265
|
+
.captcha[data-state=verifying] .checkbox.bars {
|
|
266
|
+
animation: none;
|
|
267
|
+
background: none;
|
|
268
|
+
border: none;
|
|
269
|
+
border-radius: 50%;
|
|
270
|
+
position: relative;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.captcha[data-state=fetching] .checkbox.bars::after,
|
|
274
|
+
.captcha[data-state=solving] .checkbox.bars::after,
|
|
275
|
+
.captcha[data-state=verifying] .checkbox.bars::after {
|
|
276
|
+
display: none;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Bars pivot around the checkbox center: each bar's own center is placed at
|
|
280
|
+
the checkbox center (left/top 50% minus half the bar's size), so the static
|
|
281
|
+
rotate(angle) translate(146%) orbits every bar around the exact center. */
|
|
282
|
+
.captcha[data-state=fetching] .checkbox.bars .bar,
|
|
283
|
+
.captcha[data-state=solving] .checkbox.bars .bar,
|
|
284
|
+
.captcha[data-state=verifying] .checkbox.bars .bar {
|
|
285
|
+
animation: ribaunt-bars-pulse 1.2s ease-in-out infinite;
|
|
286
|
+
animation-delay: calc(var(--bar-index, 0) * -0.1s);
|
|
287
|
+
background: currentColor;
|
|
288
|
+
border-radius: 6px;
|
|
289
|
+
height: 8%;
|
|
290
|
+
left: 50%;
|
|
291
|
+
margin-left: -12%;
|
|
292
|
+
margin-top: -4%;
|
|
293
|
+
position: absolute;
|
|
294
|
+
top: 50%;
|
|
295
|
+
width: 24%;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(1),
|
|
299
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(1),
|
|
300
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(1) {
|
|
301
|
+
--bar-index: 0;
|
|
302
|
+
transform: rotate(0.0001deg) translate(146%);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(2),
|
|
306
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(2),
|
|
307
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(2) {
|
|
308
|
+
--bar-index: 1;
|
|
309
|
+
transform: rotate(30deg) translate(146%);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(3),
|
|
313
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(3),
|
|
314
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(3) {
|
|
315
|
+
--bar-index: 2;
|
|
316
|
+
transform: rotate(60deg) translate(146%);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(4),
|
|
320
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(4),
|
|
321
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(4) {
|
|
322
|
+
--bar-index: 3;
|
|
323
|
+
transform: rotate(90deg) translate(146%);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(5),
|
|
327
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(5),
|
|
328
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(5) {
|
|
329
|
+
--bar-index: 4;
|
|
330
|
+
transform: rotate(120deg) translate(146%);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(6),
|
|
334
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(6),
|
|
335
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(6) {
|
|
336
|
+
--bar-index: 5;
|
|
337
|
+
transform: rotate(150deg) translate(146%);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(7),
|
|
341
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(7),
|
|
342
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(7) {
|
|
343
|
+
--bar-index: 6;
|
|
344
|
+
transform: rotate(180deg) translate(146%);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(8),
|
|
348
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(8),
|
|
349
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(8) {
|
|
350
|
+
--bar-index: 7;
|
|
351
|
+
transform: rotate(210deg) translate(146%);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(9),
|
|
355
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(9),
|
|
356
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(9) {
|
|
357
|
+
--bar-index: 8;
|
|
358
|
+
transform: rotate(240deg) translate(146%);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(10),
|
|
362
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(10),
|
|
363
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(10) {
|
|
364
|
+
--bar-index: 9;
|
|
365
|
+
transform: rotate(270deg) translate(146%);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(11),
|
|
369
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(11),
|
|
370
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(11) {
|
|
371
|
+
--bar-index: 10;
|
|
372
|
+
transform: rotate(300deg) translate(146%);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.captcha[data-state=fetching] .checkbox.bars .bar:nth-child(12),
|
|
376
|
+
.captcha[data-state=solving] .checkbox.bars .bar:nth-child(12),
|
|
377
|
+
.captcha[data-state=verifying] .checkbox.bars .bar:nth-child(12) {
|
|
378
|
+
--bar-index: 11;
|
|
379
|
+
transform: rotate(330deg) translate(146%);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@keyframes ribaunt-bars-pulse {
|
|
383
|
+
0%,
|
|
384
|
+
100% {
|
|
385
|
+
opacity: 1;
|
|
386
|
+
}
|
|
387
|
+
50% {
|
|
388
|
+
opacity: 0.15;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
@media (prefers-reduced-motion: reduce) {
|
|
393
|
+
.captcha,
|
|
394
|
+
.checkbox,
|
|
395
|
+
.bar,
|
|
396
|
+
.warning,
|
|
397
|
+
.logo {
|
|
398
|
+
transition: none !important;
|
|
399
|
+
animation: none !important;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* Warning Message */
|
|
404
|
+
.warning {
|
|
405
|
+
width: var(--ribaunt-widget-width, 230px);
|
|
406
|
+
background: rgb(237, 56, 46);
|
|
407
|
+
color: white;
|
|
408
|
+
padding: 4px 6px;
|
|
409
|
+
padding-bottom: calc(var(--ribaunt-border-radius, 14px) + 5px);
|
|
410
|
+
font-size: 10px;
|
|
411
|
+
box-sizing: border-box;
|
|
412
|
+
font-family: system-ui;
|
|
413
|
+
border-top-left-radius: 8px;
|
|
414
|
+
border-top-right-radius: 8px;
|
|
415
|
+
text-align: center;
|
|
416
|
+
user-select: none;
|
|
417
|
+
margin-bottom: -35.5px;
|
|
418
|
+
opacity: 0;
|
|
419
|
+
transition: margin-bottom .3s, opacity .3s;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.warning.visible {
|
|
423
|
+
margin-bottom: calc(-1 * var(--ribaunt-border-radius, 14px));
|
|
424
|
+
opacity: 1;
|
|
425
|
+
}
|
|
426
|
+
`;
|
|
427
|
+
const RIBAUNT_LOGO = `
|
|
428
|
+
<svg width="500" height="384" viewBox="0 0 500 384" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
429
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M368.357 122.188H236.618L187.59 0H450.972L500 122.188L434.14 286.295H302.478L368.357 122.188Z" fill="currentColor"/>
|
|
430
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M236.619 122.188L341.575 383.702H209.913L170.796 286.199L131.663 383.702H0L104.957 122.188H236.619Z" fill="currentColor"/>
|
|
431
|
+
</svg>
|
|
432
|
+
`;
|
|
433
|
+
const BARS_MARKUP = '<div class="bar"></div>'.repeat(12);
|
|
434
|
+
function parseTokenArray(value) {
|
|
435
|
+
if (!Array.isArray(value)) {
|
|
436
|
+
throw new Error('Challenge response must be an array of token strings');
|
|
437
|
+
}
|
|
438
|
+
if (value.length === 0) {
|
|
439
|
+
throw new Error('No challenge tokens available');
|
|
440
|
+
}
|
|
441
|
+
if (!value.every((entry) => typeof entry === 'string' && entry.length > 0)) {
|
|
442
|
+
throw new Error('Challenge response contains invalid token values');
|
|
443
|
+
}
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Error carrying a machine-readable widget code so consumers can classify
|
|
448
|
+
* failures without depending on human-readable messages.
|
|
449
|
+
*/
|
|
450
|
+
export class WidgetError extends Error {
|
|
451
|
+
constructor(code, message) {
|
|
452
|
+
super(message);
|
|
453
|
+
this.name = 'WidgetError';
|
|
454
|
+
this.code = code;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function parseChallengeTokens(payload) {
|
|
458
|
+
if (Array.isArray(payload)) {
|
|
459
|
+
return parseTokenArray(payload);
|
|
460
|
+
}
|
|
461
|
+
if (!payload || typeof payload !== 'object') {
|
|
462
|
+
throw new Error('Challenge response must be an object or array of token strings');
|
|
463
|
+
}
|
|
464
|
+
const record = payload;
|
|
465
|
+
if ('challenges' in record) {
|
|
466
|
+
return parseTokenArray(record.challenges);
|
|
467
|
+
}
|
|
468
|
+
if ('tokens' in record) {
|
|
469
|
+
return parseTokenArray(record.tokens);
|
|
470
|
+
}
|
|
471
|
+
throw new Error('Challenge response must include "challenges" or "tokens"');
|
|
472
|
+
}
|
|
473
|
+
export class RibauntWidget extends HTMLElement {
|
|
474
|
+
static get observedAttributes() {
|
|
475
|
+
return [
|
|
476
|
+
'challenge-endpoint',
|
|
477
|
+
'verify-endpoint',
|
|
478
|
+
'auto-verify',
|
|
479
|
+
'show-warning',
|
|
480
|
+
'warning-message',
|
|
481
|
+
'solve-timeout',
|
|
482
|
+
'worker-mode',
|
|
483
|
+
'wasm-mode',
|
|
484
|
+
'challenge-method',
|
|
485
|
+
'calibrate',
|
|
486
|
+
'show-progress',
|
|
487
|
+
'disabled',
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
constructor() {
|
|
491
|
+
super();
|
|
492
|
+
this.state = 'initial';
|
|
493
|
+
this.progress = 0;
|
|
494
|
+
this.timeoutError = false;
|
|
495
|
+
this.captchaElement = null;
|
|
496
|
+
this.checkboxElement = null;
|
|
497
|
+
this.messageElement = null;
|
|
498
|
+
this.warningElement = null;
|
|
499
|
+
this.logoElement = null;
|
|
500
|
+
this.autoVerifyStarted = false;
|
|
501
|
+
this.attemptController = null;
|
|
502
|
+
this.progressAnimFrame = null;
|
|
503
|
+
this.warnedWorkerMode = null;
|
|
504
|
+
this.warnedWasmMode = null;
|
|
505
|
+
this.shadow = this.attachShadow({ mode: 'open' });
|
|
506
|
+
this.handleClick = this.handleClick.bind(this);
|
|
507
|
+
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
508
|
+
this.handleLogoClick = this.handleLogoClick.bind(this);
|
|
509
|
+
}
|
|
510
|
+
connectedCallback() {
|
|
511
|
+
this.render();
|
|
512
|
+
this.dispatchStateChange();
|
|
513
|
+
this.maybeAutoVerify();
|
|
514
|
+
}
|
|
515
|
+
disconnectedCallback() {
|
|
516
|
+
this.cancelAttempt();
|
|
517
|
+
if (this.progressAnimFrame !== null) {
|
|
518
|
+
cancelAnimationFrame(this.progressAnimFrame);
|
|
519
|
+
this.progressAnimFrame = null;
|
|
520
|
+
}
|
|
521
|
+
this.removeEventListeners();
|
|
522
|
+
}
|
|
523
|
+
attributeChangedCallback(name, oldValue, newValue) {
|
|
524
|
+
if (oldValue !== newValue) {
|
|
525
|
+
this.render();
|
|
526
|
+
if (name === 'auto-verify') {
|
|
527
|
+
this.maybeAutoVerify();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
render() {
|
|
532
|
+
if (this.progressAnimFrame !== null) {
|
|
533
|
+
cancelAnimationFrame(this.progressAnimFrame);
|
|
534
|
+
this.progressAnimFrame = null;
|
|
535
|
+
}
|
|
536
|
+
this.removeEventListeners();
|
|
537
|
+
const showWarning = this.hasAttribute('show-warning') && this.getAttribute('show-warning') !== 'false';
|
|
538
|
+
const warningMessage = this.getAttribute('warning-message') || 'Enable WASM for significantly faster solving';
|
|
539
|
+
const disabled = this.isDisabled();
|
|
540
|
+
const hideProgress = this.isProgressHidden();
|
|
541
|
+
this.shadow.innerHTML = `
|
|
542
|
+
<style>${WIDGET_STYLES}</style>
|
|
543
|
+
<div>
|
|
544
|
+
${showWarning ? '<div class="warning"></div>' : ''}
|
|
545
|
+
<div class="captcha" data-state="${this.state}" role="button" tabindex="${disabled ? '-1' : '0'}" aria-disabled="${disabled}" aria-label="${this.getMessage()}" aria-busy="${this.isBusy()}">
|
|
546
|
+
<div class="content${hideProgress ? ' bars' : ''}">
|
|
547
|
+
<div class="checkbox${hideProgress ? ' bars' : ''}">${hideProgress ? BARS_MARKUP : ''}</div>
|
|
548
|
+
<p role="status" aria-live="polite">${this.getMessage()}</p>
|
|
549
|
+
</div>
|
|
550
|
+
<a class="logo" href="https://ribaunt.com" target="_blank" rel="noopener noreferrer" aria-label="Powered by Ribaunt">
|
|
551
|
+
${RIBAUNT_LOGO}
|
|
552
|
+
</a>
|
|
553
|
+
</div>
|
|
554
|
+
</div>
|
|
555
|
+
`;
|
|
556
|
+
this.captchaElement = this.shadow.querySelector('.captcha');
|
|
557
|
+
this.checkboxElement = this.shadow.querySelector('.checkbox');
|
|
558
|
+
this.messageElement = this.shadow.querySelector('p');
|
|
559
|
+
this.warningElement = this.shadow.querySelector('.warning');
|
|
560
|
+
this.logoElement = this.shadow.querySelector('.logo');
|
|
561
|
+
if (showWarning && this.warningElement) {
|
|
562
|
+
this.warningElement.textContent = warningMessage;
|
|
563
|
+
setTimeout(() => {
|
|
564
|
+
this.warningElement?.classList.add('visible');
|
|
565
|
+
}, 0);
|
|
566
|
+
}
|
|
567
|
+
// Update progress CSS variable if verifying
|
|
568
|
+
if (this.isBusy() && this.captchaElement) {
|
|
569
|
+
this.captchaElement.style.setProperty('--progress', `${this.progress}%`);
|
|
570
|
+
}
|
|
571
|
+
this.attachEventListeners();
|
|
572
|
+
}
|
|
573
|
+
attachEventListeners() {
|
|
574
|
+
if (this.captchaElement) {
|
|
575
|
+
this.captchaElement.addEventListener('click', this.handleClick);
|
|
576
|
+
this.captchaElement.addEventListener('keydown', this.handleKeyDown);
|
|
577
|
+
}
|
|
578
|
+
if (this.logoElement) {
|
|
579
|
+
this.logoElement.addEventListener('click', this.handleLogoClick);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
removeEventListeners() {
|
|
583
|
+
if (this.captchaElement) {
|
|
584
|
+
this.captchaElement.removeEventListener('click', this.handleClick);
|
|
585
|
+
this.captchaElement.removeEventListener('keydown', this.handleKeyDown);
|
|
586
|
+
}
|
|
587
|
+
if (this.logoElement) {
|
|
588
|
+
this.logoElement.removeEventListener('click', this.handleLogoClick);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
handleLogoClick(event) {
|
|
592
|
+
event.stopPropagation();
|
|
593
|
+
}
|
|
594
|
+
handleClick() {
|
|
595
|
+
if (this.isDisabled())
|
|
596
|
+
return;
|
|
597
|
+
if (this.state !== 'initial' && this.state !== 'error')
|
|
598
|
+
return;
|
|
599
|
+
this.verify();
|
|
600
|
+
}
|
|
601
|
+
handleKeyDown(e) {
|
|
602
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
603
|
+
e.preventDefault();
|
|
604
|
+
this.handleClick();
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
getMessage() {
|
|
608
|
+
if (this.isProgressHidden() && this.isBusy()) {
|
|
609
|
+
return 'Loading...';
|
|
610
|
+
}
|
|
611
|
+
switch (this.state) {
|
|
612
|
+
case 'initial':
|
|
613
|
+
return "I'm a human";
|
|
614
|
+
case 'fetching':
|
|
615
|
+
return '';
|
|
616
|
+
case 'solving':
|
|
617
|
+
return `Solving... ${this.progress}%`;
|
|
618
|
+
case 'verifying':
|
|
619
|
+
return 'Verifying...';
|
|
620
|
+
case 'done':
|
|
621
|
+
return "You're a human";
|
|
622
|
+
case 'error':
|
|
623
|
+
return this.timeoutError ? 'Timed out.' : 'Error. Try again.';
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* When `show-progress="false"`, the primary progress UI (conic spinner +
|
|
628
|
+
* percentage) is replaced by the secondary plain bars spinner and a static
|
|
629
|
+
* "Loading..." label. Progress is still tracked and reported via events.
|
|
630
|
+
*/
|
|
631
|
+
isProgressHidden() {
|
|
632
|
+
return this.hasAttribute('show-progress') && this.getAttribute('show-progress') === 'false';
|
|
633
|
+
}
|
|
634
|
+
isBusy() {
|
|
635
|
+
return this.state === 'fetching' || this.state === 'solving' || this.state === 'verifying';
|
|
636
|
+
}
|
|
637
|
+
getWorkerMode() {
|
|
638
|
+
const value = this.getAttribute('worker-mode');
|
|
639
|
+
if (value === 'required' || value === 'disabled' || value === 'preferred')
|
|
640
|
+
return value;
|
|
641
|
+
if (value !== null && this.warnedWorkerMode !== value) {
|
|
642
|
+
this.warnedWorkerMode = value;
|
|
643
|
+
console.warn(`[ribaunt] Unknown worker-mode "${value}"; falling back to "preferred".`);
|
|
644
|
+
}
|
|
645
|
+
return 'preferred';
|
|
646
|
+
}
|
|
647
|
+
getWasmMode() {
|
|
648
|
+
const value = this.getAttribute('wasm-mode');
|
|
649
|
+
if (value === 'disabled')
|
|
650
|
+
return 'disabled';
|
|
651
|
+
if (value === 'preferred' || value === null)
|
|
652
|
+
return 'preferred';
|
|
653
|
+
if (this.warnedWasmMode !== value) {
|
|
654
|
+
this.warnedWasmMode = value;
|
|
655
|
+
console.warn(`[ribaunt] Unknown wasm-mode "${value}"; falling back to "preferred".`);
|
|
656
|
+
}
|
|
657
|
+
return 'preferred';
|
|
658
|
+
}
|
|
659
|
+
shouldCalibrate() {
|
|
660
|
+
return this.hasAttribute('calibrate') && this.getAttribute('calibrate') !== 'false';
|
|
661
|
+
}
|
|
662
|
+
getChallengeMethod() {
|
|
663
|
+
return this.getAttribute('challenge-method')?.toUpperCase() === 'POST' ? 'POST' : 'GET';
|
|
664
|
+
}
|
|
665
|
+
getSolveTimeoutMs() {
|
|
666
|
+
const raw = this.getAttribute('solve-timeout');
|
|
667
|
+
if (!raw) {
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
const value = Number(raw);
|
|
671
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
672
|
+
return undefined;
|
|
673
|
+
}
|
|
674
|
+
return Math.floor(value);
|
|
675
|
+
}
|
|
676
|
+
isDisabled() {
|
|
677
|
+
return this.hasAttribute('disabled') && this.getAttribute('disabled') !== 'false';
|
|
678
|
+
}
|
|
679
|
+
shouldAutoVerify() {
|
|
680
|
+
return this.hasAttribute('auto-verify') && this.getAttribute('auto-verify') !== 'false';
|
|
681
|
+
}
|
|
682
|
+
maybeAutoVerify() {
|
|
683
|
+
if (!this.isConnected || this.autoVerifyStarted || !this.shouldAutoVerify()) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
this.autoVerifyStarted = true;
|
|
687
|
+
queueMicrotask(() => {
|
|
688
|
+
this.startVerification();
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
setState(newState) {
|
|
692
|
+
if (newState !== 'error') {
|
|
693
|
+
this.timeoutError = false;
|
|
694
|
+
}
|
|
695
|
+
this.state = newState;
|
|
696
|
+
if (this.captchaElement) {
|
|
697
|
+
this.captchaElement.setAttribute('data-state', this.state);
|
|
698
|
+
this.captchaElement.setAttribute('aria-label', this.getMessage());
|
|
699
|
+
this.captchaElement.tabIndex = this.isDisabled() ? -1 : 0;
|
|
700
|
+
this.captchaElement.setAttribute('aria-disabled', String(this.isDisabled()));
|
|
701
|
+
this.captchaElement.setAttribute('aria-busy', String(this.isBusy()));
|
|
702
|
+
}
|
|
703
|
+
if (this.messageElement) {
|
|
704
|
+
this.messageElement.textContent = this.getMessage();
|
|
705
|
+
}
|
|
706
|
+
this.dispatchStateChange();
|
|
707
|
+
}
|
|
708
|
+
dispatchStateChange() {
|
|
709
|
+
// Dispatch state change event
|
|
710
|
+
this.dispatchEvent(new CustomEvent('state-change', {
|
|
711
|
+
detail: {
|
|
712
|
+
state: this.state,
|
|
713
|
+
phase: this.state,
|
|
714
|
+
progress: this.progress,
|
|
715
|
+
},
|
|
716
|
+
bubbles: true,
|
|
717
|
+
composed: true,
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
setProgress(value) {
|
|
721
|
+
const target = Math.round(value * 10) / 10;
|
|
722
|
+
if (this.progressAnimFrame !== null) {
|
|
723
|
+
cancelAnimationFrame(this.progressAnimFrame);
|
|
724
|
+
}
|
|
725
|
+
const start = this.progress;
|
|
726
|
+
this.progress = target;
|
|
727
|
+
// Progress stays internal (events) in the no-percent mode; nothing to paint.
|
|
728
|
+
if (this.isProgressHidden()) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (this.state !== 'solving') {
|
|
732
|
+
if (this.captchaElement) {
|
|
733
|
+
this.captchaElement.style.setProperty('--progress', `${target}%`);
|
|
734
|
+
}
|
|
735
|
+
if (this.messageElement) {
|
|
736
|
+
this.messageElement.textContent = this.getMessage();
|
|
737
|
+
}
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const startTime = performance.now();
|
|
741
|
+
const duration = 120;
|
|
742
|
+
const animate = (now) => {
|
|
743
|
+
const elapsed = now - startTime;
|
|
744
|
+
const t = Math.min(elapsed / duration, 1);
|
|
745
|
+
const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
|
|
746
|
+
const current = Math.round((start + (target - start) * eased) * 10) / 10;
|
|
747
|
+
this.progress = current;
|
|
748
|
+
if (this.captchaElement) {
|
|
749
|
+
this.captchaElement.style.setProperty('--progress', `${current}%`);
|
|
750
|
+
}
|
|
751
|
+
if (this.messageElement) {
|
|
752
|
+
this.messageElement.textContent = this.getMessage();
|
|
753
|
+
}
|
|
754
|
+
if (t < 1) {
|
|
755
|
+
this.progressAnimFrame = requestAnimationFrame(animate);
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
this.progressAnimFrame = null;
|
|
759
|
+
this.progress = target;
|
|
760
|
+
if (this.captchaElement) {
|
|
761
|
+
this.captchaElement.style.setProperty('--progress', `${target}%`);
|
|
762
|
+
}
|
|
763
|
+
if (this.messageElement) {
|
|
764
|
+
this.messageElement.textContent = this.getMessage();
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
this.progressAnimFrame = requestAnimationFrame(animate);
|
|
769
|
+
}
|
|
770
|
+
cancelAttempt() {
|
|
771
|
+
this.attemptController?.abort();
|
|
772
|
+
this.attemptController = null;
|
|
773
|
+
}
|
|
774
|
+
getErrorCode(error) {
|
|
775
|
+
if (error instanceof WidgetError)
|
|
776
|
+
return error.code;
|
|
777
|
+
if (error instanceof WorkerUnavailableError)
|
|
778
|
+
return 'worker-unavailable';
|
|
779
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
780
|
+
return this.timeoutError ? 'timeout' : 'aborted';
|
|
781
|
+
}
|
|
782
|
+
if (!(error instanceof Error))
|
|
783
|
+
return 'unknown';
|
|
784
|
+
// Legacy string matching for errors thrown without a code.
|
|
785
|
+
if (error.message === 'Failed to fetch challenge')
|
|
786
|
+
return 'challenge-fetch-failed';
|
|
787
|
+
if (error.message === 'Verification failed')
|
|
788
|
+
return 'verification-failed';
|
|
789
|
+
if (error.message.startsWith('Challenge response') || error.message === 'No challenge tokens available') {
|
|
790
|
+
return 'invalid-challenge';
|
|
791
|
+
}
|
|
792
|
+
if (error.message.includes('solve') || error.message.includes('challenge'))
|
|
793
|
+
return 'solve-failed';
|
|
794
|
+
return 'unknown';
|
|
795
|
+
}
|
|
796
|
+
async verify() {
|
|
797
|
+
this.cancelAttempt();
|
|
798
|
+
const controller = new AbortController();
|
|
799
|
+
this.attemptController = controller;
|
|
800
|
+
this.setProgress(0);
|
|
801
|
+
const timeoutMs = this.getSolveTimeoutMs();
|
|
802
|
+
const timeoutHandle = timeoutMs
|
|
803
|
+
? setTimeout(() => {
|
|
804
|
+
this.timeoutError = true;
|
|
805
|
+
controller.abort();
|
|
806
|
+
}, timeoutMs)
|
|
807
|
+
: undefined;
|
|
808
|
+
try {
|
|
809
|
+
const challengeEndpoint = this.getAttribute('challenge-endpoint');
|
|
810
|
+
const verifyEndpoint = this.getAttribute('verify-endpoint');
|
|
811
|
+
this.setState('fetching');
|
|
812
|
+
let tokens = [];
|
|
813
|
+
if (challengeEndpoint) {
|
|
814
|
+
const method = this.getChallengeMethod();
|
|
815
|
+
let calibration;
|
|
816
|
+
if (method === 'POST' && this.shouldCalibrate()) {
|
|
817
|
+
calibration = await calibrateBrowser();
|
|
818
|
+
}
|
|
819
|
+
const request = method === 'POST'
|
|
820
|
+
? {
|
|
821
|
+
method,
|
|
822
|
+
headers: { 'Content-Type': 'application/json' },
|
|
823
|
+
body: JSON.stringify(calibration ? { calibration } : {}),
|
|
824
|
+
signal: controller.signal,
|
|
825
|
+
}
|
|
826
|
+
: { signal: controller.signal };
|
|
827
|
+
const response = await fetch(challengeEndpoint, request);
|
|
828
|
+
if (!response.ok)
|
|
829
|
+
throw new WidgetError('challenge-fetch-failed', 'Failed to fetch challenge');
|
|
830
|
+
const data = await response.json();
|
|
831
|
+
try {
|
|
832
|
+
tokens = parseChallengeTokens(data);
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
throw new WidgetError('invalid-challenge', error instanceof Error ? error.message : String(error));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (tokens.length === 0) {
|
|
839
|
+
throw new WidgetError('invalid-challenge', 'No challenge tokens available');
|
|
840
|
+
}
|
|
841
|
+
this.setState('solving');
|
|
842
|
+
const solutions = await solveChallengeWithWorker(tokens, (progress) => {
|
|
843
|
+
this.setProgress(progress);
|
|
844
|
+
}, controller.signal, this.getWorkerMode(), this.getWasmMode(), (backend) => {
|
|
845
|
+
try {
|
|
846
|
+
this.dispatchEvent(new CustomEvent('solver-backend', {
|
|
847
|
+
detail: { backend, phase: 'solving' },
|
|
848
|
+
bubbles: true,
|
|
849
|
+
composed: true,
|
|
850
|
+
}));
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
// telemetry must never break solving
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
this.setState('verifying');
|
|
857
|
+
if (verifyEndpoint) {
|
|
858
|
+
const response = await fetch(verifyEndpoint, {
|
|
859
|
+
method: 'POST',
|
|
860
|
+
headers: { 'Content-Type': 'application/json' },
|
|
861
|
+
body: JSON.stringify({ tokens, solutions }),
|
|
862
|
+
signal: controller.signal,
|
|
863
|
+
});
|
|
864
|
+
if (!response.ok) {
|
|
865
|
+
throw new WidgetError('verification-failed', 'Verification failed');
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
this.setState('done');
|
|
869
|
+
this.dispatchEvent(new CustomEvent('verify', {
|
|
870
|
+
detail: {
|
|
871
|
+
solutions,
|
|
872
|
+
phase: 'done',
|
|
873
|
+
progress: 100,
|
|
874
|
+
},
|
|
875
|
+
bubbles: true,
|
|
876
|
+
composed: true,
|
|
877
|
+
}));
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
if (controller.signal.aborted && this.attemptController !== controller)
|
|
881
|
+
return;
|
|
882
|
+
if (!this.timeoutError) {
|
|
883
|
+
this.timeoutError = error instanceof DOMException
|
|
884
|
+
&& error.name === 'AbortError'
|
|
885
|
+
&& timeoutMs !== undefined;
|
|
886
|
+
}
|
|
887
|
+
const code = this.getErrorCode(error);
|
|
888
|
+
this.setState('error');
|
|
889
|
+
this.dispatchEvent(new CustomEvent('error', {
|
|
890
|
+
detail: {
|
|
891
|
+
error: this.timeoutError
|
|
892
|
+
? 'Timed out.'
|
|
893
|
+
: (error instanceof Error ? error.message : String(error)),
|
|
894
|
+
code,
|
|
895
|
+
timeout: this.timeoutError,
|
|
896
|
+
phase: 'error',
|
|
897
|
+
},
|
|
898
|
+
bubbles: true,
|
|
899
|
+
composed: true,
|
|
900
|
+
}));
|
|
901
|
+
}
|
|
902
|
+
finally {
|
|
903
|
+
if (timeoutHandle !== undefined) {
|
|
904
|
+
clearTimeout(timeoutHandle);
|
|
905
|
+
}
|
|
906
|
+
if (this.attemptController === controller) {
|
|
907
|
+
this.attemptController = null;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Public API: Reset the widget to initial state
|
|
913
|
+
*/
|
|
914
|
+
reset() {
|
|
915
|
+
this.cancelAttempt();
|
|
916
|
+
this.timeoutError = false;
|
|
917
|
+
this.setState('initial');
|
|
918
|
+
this.setProgress(0);
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Public API: Get current state
|
|
922
|
+
*/
|
|
923
|
+
getState() {
|
|
924
|
+
return this.state;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Public API: Programmatically trigger verification
|
|
928
|
+
*/
|
|
929
|
+
startVerification() {
|
|
930
|
+
if (!this.isDisabled() && (this.state === 'initial' || this.state === 'error')) {
|
|
931
|
+
this.verify();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// Register the custom element
|
|
936
|
+
/* v8 ignore next -- environment guard for SSR/repeated imports */
|
|
937
|
+
if (typeof window !== 'undefined' && typeof customElements !== 'undefined' && !customElements.get('ribaunt-widget')) {
|
|
938
|
+
customElements.define('ribaunt-widget', RibauntWidget);
|
|
939
|
+
}
|
|
940
|
+
// Export for use in TypeScript
|
|
941
|
+
export default RibauntWidget;
|
|
942
|
+
//# sourceMappingURL=widget.js.map
|