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.
Files changed (60) hide show
  1. package/README.md +113 -10
  2. package/demo/lib/ribaunt-solver.wasm +0 -0
  3. package/demo/lib/solver-worker.js +174 -0
  4. package/demo/lib/solver-worker.js.map +1 -0
  5. package/demo/lib/solver.js +135 -0
  6. package/demo/lib/solver.js.map +1 -0
  7. package/demo/lib/wasm-solver.js +296 -0
  8. package/demo/lib/wasm-solver.js.map +1 -0
  9. package/demo/lib/widget-browser.js +9 -0
  10. package/demo/lib/widget-browser.js.map +1 -0
  11. package/demo/lib/widget.js +942 -0
  12. package/demo/lib/widget.js.map +1 -0
  13. package/demo/lib/worker-client.js +128 -0
  14. package/demo/lib/worker-client.js.map +1 -0
  15. package/dist/cjs/index.d.ts +48 -3
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js +196 -17
  18. package/dist/cjs/index.js.map +1 -1
  19. package/dist/cjs/risk.d.ts +124 -0
  20. package/dist/cjs/risk.d.ts.map +1 -0
  21. package/dist/cjs/risk.js +213 -0
  22. package/dist/cjs/risk.js.map +1 -0
  23. package/dist/index.d.ts +48 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +193 -16
  26. package/dist/index.js.map +1 -1
  27. package/dist/ribaunt-solver.wasm +0 -0
  28. package/dist/risk.d.ts +124 -0
  29. package/dist/risk.d.ts.map +1 -0
  30. package/dist/risk.js +202 -0
  31. package/dist/risk.js.map +1 -0
  32. package/dist/solver-worker.d.ts.map +1 -1
  33. package/dist/solver-worker.js +166 -7
  34. package/dist/solver-worker.js.map +1 -1
  35. package/dist/solver.d.ts +1 -0
  36. package/dist/solver.d.ts.map +1 -1
  37. package/dist/solver.js +38 -3
  38. package/dist/solver.js.map +1 -1
  39. package/dist/wasm-solver.d.ts +29 -0
  40. package/dist/wasm-solver.d.ts.map +1 -0
  41. package/dist/wasm-solver.js +296 -0
  42. package/dist/wasm-solver.js.map +1 -0
  43. package/dist/widget-browser.d.ts +3 -2
  44. package/dist/widget-browser.d.ts.map +1 -1
  45. package/dist/widget-browser.js +1 -1
  46. package/dist/widget-browser.js.map +1 -1
  47. package/dist/widget-react.d.ts +4 -1
  48. package/dist/widget-react.d.ts.map +1 -1
  49. package/dist/widget-react.js +102 -14
  50. package/dist/widget-react.js.map +1 -1
  51. package/dist/widget.d.ts +42 -14
  52. package/dist/widget.d.ts.map +1 -1
  53. package/dist/widget.js +254 -12
  54. package/dist/widget.js.map +1 -1
  55. package/dist/worker-client.d.ts +7 -1
  56. package/dist/worker-client.d.ts.map +1 -1
  57. package/dist/worker-client.js +69 -19
  58. package/dist/worker-client.js.map +1 -1
  59. package/package.json +22 -9
  60. 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