cap-widget 0.1.46

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/src/cap.js ADDED
@@ -0,0 +1,1363 @@
1
+ (() => {
2
+ const WASM_VERSION = "0.0.6";
3
+ const _browserHasHaptics =
4
+ "vibrate" in navigator && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
5
+
6
+ if (typeof window === "undefined") {
7
+ return;
8
+ }
9
+
10
+ const capFetch = (u, conf = {}) => {
11
+ if (window?.CAP_CUSTOM_FETCH) {
12
+ return window.CAP_CUSTOM_FETCH(u, conf);
13
+ }
14
+
15
+ return fetch(u, conf);
16
+ };
17
+
18
+ function prng(seed, length) {
19
+ function fnv1a(str) {
20
+ let hash = 2166136261;
21
+ for (let i = 0; i < str.length; i++) {
22
+ hash ^= str.charCodeAt(i);
23
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
24
+ }
25
+ return hash >>> 0;
26
+ }
27
+
28
+ let state = fnv1a(seed);
29
+ let result = "";
30
+
31
+ function next() {
32
+ state ^= state << 13;
33
+ state ^= state >>> 17;
34
+ state ^= state << 5;
35
+ return state >>> 0;
36
+ }
37
+
38
+ while (result.length < length) {
39
+ const rnd = next();
40
+ result += rnd.toString(16).padStart(8, "0");
41
+ }
42
+
43
+ return result.substring(0, length);
44
+ }
45
+
46
+ async function runInstrumentationChallenge(instrBytes) {
47
+ const b64ToUint8 = (b64) => {
48
+ const bin = atob(b64);
49
+ const arr = new Uint8Array(bin.length);
50
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
51
+ return arr;
52
+ };
53
+
54
+ var compressed = b64ToUint8(instrBytes);
55
+
56
+ const scriptText = await new Promise((resolve, reject) => {
57
+ try {
58
+ var ds = new DecompressionStream("deflate-raw");
59
+ var writer = ds.writable.getWriter();
60
+ var reader = ds.readable.getReader();
61
+ var chunks = [];
62
+ function pump(res) {
63
+ if (res.done) {
64
+ var len = 0,
65
+ off = 0;
66
+ for (var i = 0; i < chunks.length; i++) len += chunks[i].length;
67
+ var out = new Uint8Array(len);
68
+ for (var i = 0; i < chunks.length; i++) {
69
+ out.set(chunks[i], off);
70
+ off += chunks[i].length;
71
+ }
72
+ resolve(new TextDecoder().decode(out));
73
+ } else {
74
+ chunks.push(res.value);
75
+ reader.read().then(pump).catch(reject);
76
+ }
77
+ }
78
+ reader.read().then(pump).catch(reject);
79
+ writer
80
+ .write(compressed)
81
+ .then(() => {
82
+ writer.close();
83
+ })
84
+ .catch(reject);
85
+ } catch (e) {
86
+ reject(e);
87
+ }
88
+ });
89
+
90
+ return new Promise((resolve) => {
91
+ var timeout = setTimeout(() => {
92
+ cleanup();
93
+ resolve({ __timeout: true });
94
+ }, 20000);
95
+
96
+ var iframe = document.createElement("iframe");
97
+ iframe.setAttribute("sandbox", "allow-scripts");
98
+ iframe.setAttribute("aria-hidden", "true");
99
+ iframe.style.cssText =
100
+ "position:absolute;width:1px;height:1px;top:-9999px;left:-9999px;border:none;opacity:0;pointer-events:none;";
101
+
102
+ var resolved = false;
103
+ function cleanup() {
104
+ if (resolved) return;
105
+ resolved = true;
106
+ clearTimeout(timeout);
107
+ window.removeEventListener("message", handler);
108
+ if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
109
+ }
110
+
111
+ function handler(ev) {
112
+ var d = ev.data;
113
+ if (!d || typeof d !== "object") return;
114
+ if (d.type === "cap:instr") {
115
+ cleanup();
116
+ if (d.blocked) {
117
+ resolve({
118
+ __blocked: true,
119
+ blockReason: d.blockReason || "automated_browser",
120
+ });
121
+ } else if (d.result) {
122
+ resolve(d.result);
123
+ } else {
124
+ resolve({ __timeout: true });
125
+ }
126
+ } else if (d.type === "cap:error") {
127
+ cleanup();
128
+ resolve({ __timeout: true });
129
+ }
130
+ }
131
+
132
+ window.addEventListener("message", handler);
133
+
134
+ iframe.srcdoc =
135
+ '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><script>' +
136
+ scriptText +
137
+ "\n</scr" +
138
+ "ipt></body></html>";
139
+
140
+ document.body.appendChild(iframe);
141
+ });
142
+ }
143
+
144
+ let wasmModulePromise = null;
145
+
146
+ const getWasmModule = () => {
147
+ if (wasmModulePromise) return wasmModulePromise;
148
+
149
+ const wasmUrl =
150
+ window.CAP_CUSTOM_WASM_URL ||
151
+ `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`;
152
+
153
+ wasmModulePromise = fetch(wasmUrl)
154
+ .then((r) => {
155
+ if (!r.ok) throw new Error(`Failed to fetch wasm: ${r.status}`);
156
+ return r.arrayBuffer();
157
+ })
158
+ .then((buf) => WebAssembly.compile(buf))
159
+ .catch((e) => {
160
+ wasmModulePromise = null;
161
+ throw e;
162
+ });
163
+
164
+ return wasmModulePromise;
165
+ };
166
+
167
+ if (typeof WebAssembly === "object" && typeof WebAssembly.compile === "function") {
168
+ getWasmModule().catch(() => {});
169
+ }
170
+
171
+ const prefersReducedMotion = () =>
172
+ window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
173
+
174
+ const SPECULATIVE_DELAY_MS = 2500;
175
+ const SPECULATIVE_WORKERS = 1;
176
+ const SPECULATIVE_YIELD_MS = 120;
177
+
178
+ let _sharedWorkerUrl = null;
179
+
180
+ function _getSharedWorkerUrl() {
181
+ if (_sharedWorkerUrl) return _sharedWorkerUrl;
182
+
183
+ _sharedWorkerUrl = URL.createObjectURL(
184
+ new Blob([`%%workerScript%%`], { type: "application/javascript" }),
185
+ );
186
+ return _sharedWorkerUrl;
187
+ }
188
+
189
+ class WorkerPool {
190
+ constructor(size) {
191
+ this._size = size;
192
+ this._workers = [];
193
+ this._idle = [];
194
+ this._queue = [];
195
+ this._wasmModule = null;
196
+ this._spawnFailures = 0;
197
+ }
198
+
199
+ setWasm(wasmModule) {
200
+ this._wasmModule = wasmModule;
201
+ }
202
+
203
+ _spawn() {
204
+ const url = _getSharedWorkerUrl();
205
+ const w = new Worker(url);
206
+ w._busy = false;
207
+ this._workers.push(w);
208
+ this._idle.push(w);
209
+ return w;
210
+ }
211
+
212
+ _replaceWorker(deadWorker) {
213
+ const idx = this._workers.indexOf(deadWorker);
214
+ if (idx !== -1) this._workers.splice(idx, 1);
215
+ const idleIdx = this._idle.indexOf(deadWorker);
216
+ if (idleIdx !== -1) this._idle.splice(idleIdx, 1);
217
+
218
+ try {
219
+ deadWorker.terminate();
220
+ } catch {}
221
+
222
+ this._spawnFailures++;
223
+ if (this._spawnFailures > 3) {
224
+ console.error("[cap] worker spawn failed repeatedly, not retrying");
225
+ return null;
226
+ }
227
+
228
+ return this._spawn();
229
+ }
230
+
231
+ _ensureSize(n) {
232
+ while (this._workers.length < n) this._spawn();
233
+ }
234
+
235
+ run(salt, target) {
236
+ return new Promise((resolve, reject) => {
237
+ this._queue.push({ salt, target, resolve, reject });
238
+ this._dispatch();
239
+ });
240
+ }
241
+
242
+ _dispatch() {
243
+ while (this._idle.length > 0 && this._queue.length > 0) {
244
+ const worker = this._idle.shift();
245
+ const { salt, target, resolve, reject } = this._queue.shift();
246
+
247
+ let settled = false;
248
+
249
+ const onMessage = ({ data }) => {
250
+ if (settled) return;
251
+ settled = true;
252
+ worker.removeEventListener("message", onMessage);
253
+ worker.removeEventListener("error", onError);
254
+ this._spawnFailures = 0;
255
+ this._idle.push(worker);
256
+ if (!data.found) {
257
+ reject(new Error(data.error || "worker failed"));
258
+ } else {
259
+ resolve(data.nonce);
260
+ }
261
+ this._dispatch();
262
+ };
263
+
264
+ const onError = (err) => {
265
+ if (settled) return;
266
+ settled = true;
267
+ worker.removeEventListener("message", onMessage);
268
+ worker.removeEventListener("error", onError);
269
+ const replacement = this._replaceWorker(worker);
270
+ reject(err);
271
+ if (replacement) {
272
+ this._dispatch();
273
+ }
274
+ };
275
+
276
+ worker.addEventListener("message", onMessage);
277
+ worker.addEventListener("error", onError);
278
+
279
+ if (this._wasmModule) {
280
+ worker.postMessage({ salt, target, wasmModule: this._wasmModule }, []);
281
+ } else {
282
+ worker.postMessage({ salt, target });
283
+ }
284
+ }
285
+ }
286
+
287
+ terminate() {
288
+ for (const w of this._workers) {
289
+ try {
290
+ w.terminate();
291
+ } catch {}
292
+ }
293
+ this._workers = [];
294
+ this._idle = [];
295
+ this._queue = [];
296
+ }
297
+ }
298
+
299
+ class CapWidget extends HTMLElement {
300
+ static formAssociated = true;
301
+ #resetTimer = null;
302
+ #workersCount = navigator.hardwareConcurrency || 8;
303
+ token = null;
304
+ #shadow;
305
+ #div;
306
+ #host;
307
+ #solving = false;
308
+ #eventHandlers;
309
+ #internals;
310
+
311
+ #speculative = null;
312
+ #speculativeTimer = null;
313
+ #speculativePool = null;
314
+ #interactionHandler = null;
315
+
316
+ get #hasHaptics() {
317
+ return (
318
+ _browserHasHaptics &&
319
+ !window.CAP_DISABLE_HAPTICS &&
320
+ !this.hasAttribute("data-cap-disable-haptics")
321
+ );
322
+ }
323
+
324
+ #makeSpeculativeState() {
325
+ return {
326
+ state: "idle",
327
+ challengeResp: null,
328
+ challenges: null,
329
+ results: [],
330
+ completedCount: 0,
331
+ solvePromise: null,
332
+ promoteFn: null,
333
+ _listeners: [],
334
+ pendingPromotion: null,
335
+ token: null,
336
+ tokenExpires: null,
337
+
338
+ notify() {
339
+ for (const fn of this._listeners) fn();
340
+ this._listeners = [];
341
+ },
342
+
343
+ onSettled(fn) {
344
+ if (this.state === "done" || this.state === "error") {
345
+ fn();
346
+ } else {
347
+ this._listeners.push(fn);
348
+ }
349
+ },
350
+ };
351
+ }
352
+
353
+ #resetSpeculativeState() {
354
+ this.#speculative = this.#makeSpeculativeState();
355
+ this.#attachInteractionListeners();
356
+ }
357
+
358
+ #detachInteractionListeners() {
359
+ if (this.#interactionHandler) {
360
+ window.removeEventListener("mousemove", this.#interactionHandler);
361
+ window.removeEventListener("touchstart", this.#interactionHandler);
362
+ window.removeEventListener("keydown", this.#interactionHandler);
363
+ this.#interactionHandler = null;
364
+ }
365
+ }
366
+
367
+ #attachInteractionListeners() {
368
+ this.#detachInteractionListeners();
369
+ const handler = () => {
370
+ this.#detachInteractionListeners();
371
+ this.#onFirstInteraction();
372
+ };
373
+ this.#interactionHandler = handler;
374
+ window.addEventListener("mousemove", handler, { passive: true });
375
+ window.addEventListener("touchstart", handler, { passive: true });
376
+ window.addEventListener("keydown", handler, { passive: true });
377
+ }
378
+
379
+ #isVisible() {
380
+ if (typeof this.checkVisibility === "function") {
381
+ return this.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true });
382
+ }
383
+ // Fallback: offsetParent is null for display:none; also check the style directly
384
+ return !!(this.offsetParent || this.getClientRects().length > 0);
385
+ }
386
+
387
+ #onFirstInteraction() {
388
+ if (this.#speculative.state !== "idle") return;
389
+ if (!this.#isVisible()) return;
390
+ this.#speculative.state = "waiting";
391
+ this.#speculativeTimer = setTimeout(() => {
392
+ this.#beginSpeculativeSolve();
393
+ }, SPECULATIVE_DELAY_MS);
394
+ }
395
+
396
+ async #beginSpeculativeSolve() {
397
+ if (this.#speculative.state !== "waiting") return;
398
+ this.#speculative.state = "fetching";
399
+
400
+ let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
401
+ if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
402
+ apiEndpoint = "/";
403
+ }
404
+ if (!apiEndpoint) {
405
+ this.#speculative.state = "idle";
406
+ return;
407
+ }
408
+ if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
409
+
410
+ try {
411
+ const raw = await capFetch(`${apiEndpoint}challenge`, { method: "POST" });
412
+ let resp;
413
+ try {
414
+ resp = await raw.json();
415
+ } catch {
416
+ throw new Error("Failed to parse speculative challenge response");
417
+ }
418
+ if (resp.error) throw new Error(resp.error);
419
+
420
+ resp._apiEndpoint = apiEndpoint;
421
+ this.#speculative.challengeResp = resp;
422
+
423
+ const { challenge, token } = resp;
424
+ let challenges = challenge;
425
+ if (!Array.isArray(challenges)) {
426
+ let i = 0;
427
+ challenges = Array.from({ length: challenge.c }, () => {
428
+ i++;
429
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
430
+ });
431
+ }
432
+ this.#speculative.challenges = challenges;
433
+ this.#speculative.state = "solving";
434
+
435
+ this.#speculative.solvePromise = this.#speculativeSolveAll(challenges);
436
+ } catch (e) {
437
+ console.warn("[cap] speculative challenge fetch failed:", e);
438
+ this.#speculative.state = "error";
439
+ this.#speculative.notify();
440
+ }
441
+ }
442
+
443
+ async #speculativeSolveAll(challenges) {
444
+ _getSharedWorkerUrl();
445
+
446
+ let wasmModule = null;
447
+ try {
448
+ wasmModule = await getWasmModule();
449
+ } catch {}
450
+
451
+ if (!this.#speculativePool) {
452
+ this.#speculativePool = new WorkerPool(1);
453
+ this.#speculativePool._spawn();
454
+ }
455
+ this.#speculativePool.setWasm(wasmModule);
456
+
457
+ const total = challenges.length;
458
+ const results = new Array(total);
459
+
460
+ let concurrency = SPECULATIVE_WORKERS;
461
+ let promoted = false;
462
+
463
+ this.#speculative.promoteFn = (fullCount) => {
464
+ if (promoted) return;
465
+ promoted = true;
466
+ concurrency = fullCount;
467
+ this.#speculativePool._size = fullCount;
468
+ this.#speculativePool._ensureSize(fullCount);
469
+ };
470
+
471
+ if (this.#speculative.pendingPromotion !== null) {
472
+ this.#speculative.promoteFn(this.#speculative.pendingPromotion);
473
+ this.#speculative.pendingPromotion = null;
474
+ }
475
+
476
+ let nextIndex = 0;
477
+
478
+ while (nextIndex < total) {
479
+ const batchSize = concurrency;
480
+ const batch = [];
481
+ const batchIndices = [];
482
+
483
+ for (let i = 0; i < batchSize && nextIndex < total; i++) {
484
+ batchIndices.push(nextIndex);
485
+ batch.push(challenges[nextIndex]);
486
+ nextIndex++;
487
+ }
488
+
489
+ this.#speculativePool._ensureSize(Math.max(concurrency, batchSize));
490
+
491
+ const batchResults = await Promise.all(
492
+ batch.map((challenge) =>
493
+ this.#speculativePool.run(challenge[0], challenge[1]).then((nonce) => {
494
+ this.#speculative.completedCount++;
495
+ return nonce;
496
+ }),
497
+ ),
498
+ );
499
+
500
+ for (let i = 0; i < batchIndices.length; i++) {
501
+ results[batchIndices[i]] = batchResults[i];
502
+ }
503
+
504
+ if (!promoted && nextIndex < total) {
505
+ await new Promise((resolve) => setTimeout(resolve, SPECULATIVE_YIELD_MS));
506
+ }
507
+ }
508
+
509
+ this.#speculative.results = results;
510
+ this.#speculative.state = "redeeming";
511
+ this.#speculativeRedeem(results);
512
+ return results;
513
+ }
514
+
515
+ async #speculativeRedeem(solutions) {
516
+ try {
517
+ const challengeResp = this.#speculative.challengeResp;
518
+ const apiEndpoint = challengeResp._apiEndpoint;
519
+ if (!apiEndpoint) throw new Error("[cap] speculative redeem: missing apiEndpoint");
520
+
521
+ let instrOut = null;
522
+ if (challengeResp.instrumentation) {
523
+ instrOut = await runInstrumentationChallenge(challengeResp.instrumentation);
524
+ if (instrOut?.__timeout || instrOut?.__blocked) {
525
+ this.#speculative.state = "done";
526
+ this.#speculative.notify();
527
+ return;
528
+ }
529
+ }
530
+
531
+ const redeemRaw = await capFetch(`${apiEndpoint}redeem`, {
532
+ method: "POST",
533
+ body: JSON.stringify({
534
+ token: challengeResp.token,
535
+ solutions,
536
+ ...(instrOut && { instr: instrOut }),
537
+ }),
538
+ headers: { "Content-Type": "application/json" },
539
+ });
540
+
541
+ let resp;
542
+ try {
543
+ resp = await redeemRaw.json();
544
+ } catch {
545
+ throw new Error("Failed to parse speculative redeem response");
546
+ }
547
+
548
+ if (!resp.success) throw new Error(resp.error || "Speculative redeem failed");
549
+
550
+ this.#speculative.token = resp.token;
551
+ this.#speculative.tokenExpires = new Date(resp.expires).getTime();
552
+ this.#speculative.state = "done";
553
+ this.#speculative.notify();
554
+ } catch (e) {
555
+ console.warn("[cap] speculative redeem failed (will redo on click):", e);
556
+ this.#speculative.state = "done";
557
+ this.#speculative.notify();
558
+ }
559
+ }
560
+
561
+ getI18nText(key, defaultValue) {
562
+ return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue;
563
+ }
564
+
565
+ static get observedAttributes() {
566
+ return [
567
+ "onsolve",
568
+ "onprogress",
569
+ "onreset",
570
+ "onerror",
571
+ "data-cap-worker-count",
572
+ "data-cap-i18n-initial-state",
573
+ "required",
574
+ ];
575
+ }
576
+
577
+ constructor() {
578
+ super();
579
+ if (this.#eventHandlers) {
580
+ this.#eventHandlers.forEach((handler, eventName) => {
581
+ this.removeEventListener(eventName.slice(2), handler);
582
+ });
583
+ }
584
+
585
+ this.#eventHandlers = new Map();
586
+ this.boundHandleProgress = this.handleProgress.bind(this);
587
+ this.boundHandleSolve = this.handleSolve.bind(this);
588
+ this.boundHandleError = this.handleError.bind(this);
589
+ this.boundHandleReset = this.handleReset.bind(this);
590
+
591
+ try {
592
+ this.#internals = this.attachInternals();
593
+ } catch {}
594
+ }
595
+
596
+ #updateValidity() {
597
+ if (!this.#internals?.setValidity) return;
598
+ if (this.hasAttribute("required") && !this.token) {
599
+ this.#internals.setValidity(
600
+ { valueMissing: true },
601
+ this.getI18nText("required-label", "Please verify you're human"),
602
+ this.#div || this,
603
+ );
604
+ } else {
605
+ this.#internals.setValidity({});
606
+ }
607
+ }
608
+
609
+ initialize() {
610
+ _getSharedWorkerUrl();
611
+ if (!this.#speculative) {
612
+ this.#speculative = this.#makeSpeculativeState();
613
+ }
614
+ if (!this.#speculativePool) {
615
+ this.#speculativePool = new WorkerPool(1);
616
+ this.#speculativePool._spawn();
617
+ }
618
+ }
619
+
620
+ attributeChangedCallback(name, _, value) {
621
+ if (name.startsWith("on")) {
622
+ const eventName = name.slice(2);
623
+ const oldHandler = this.#eventHandlers.get(name);
624
+ if (oldHandler) {
625
+ this.removeEventListener(eventName, oldHandler);
626
+ }
627
+
628
+ if (value) {
629
+ const handler = (event) => {
630
+ const callback = this.getAttribute(name);
631
+ if (typeof window[callback] === "function") {
632
+ window[callback].call(this, event);
633
+ }
634
+ };
635
+ this.#eventHandlers.set(name, handler);
636
+ this.addEventListener(eventName, handler);
637
+ }
638
+ }
639
+
640
+ if (name === "data-cap-worker-count") {
641
+ this.setWorkersCount(parseInt(value, 10));
642
+ }
643
+
644
+ if (
645
+ name === "data-cap-i18n-initial-state" &&
646
+ this.#div &&
647
+ this.#div?.querySelector(".label.active")
648
+ ) {
649
+ this.animateLabel(this.getI18nText("initial-state", "Verify you're human"));
650
+ }
651
+
652
+ if (name === "required") {
653
+ this.#updateValidity();
654
+ }
655
+ }
656
+
657
+ async connectedCallback() {
658
+ this.#host = this;
659
+ this.#shadow = this.attachShadow({ mode: "open" });
660
+ this.#div = document.createElement("div");
661
+ this.createUI();
662
+ this.addEventListeners();
663
+ this.initialize();
664
+ this.#div.removeAttribute("disabled");
665
+
666
+ const workers = this.getAttribute("data-cap-worker-count");
667
+ const parsedWorkers = workers ? parseInt(workers, 10) : null;
668
+ this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
669
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
670
+ this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
671
+
672
+ this.#attachInteractionListeners();
673
+ this.#updateValidity();
674
+
675
+ this.addEventListener("invalid", this.#handleInvalid);
676
+ }
677
+
678
+ #handleInvalid = () => {
679
+ if (!this.#div) return;
680
+ try {
681
+ this.scrollIntoView({ behavior: "smooth", block: "center" });
682
+ } catch {
683
+ this.scrollIntoView();
684
+ }
685
+ this.#div.classList.remove("invalid");
686
+ void this.#div.offsetWidth;
687
+ this.#div.classList.add("invalid");
688
+ setTimeout(() => this.#div?.classList.remove("invalid"), 1500);
689
+ };
690
+
691
+ async solve() {
692
+ if (this.#solving) {
693
+ return;
694
+ }
695
+
696
+ try {
697
+ this.#solving = true;
698
+ this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true);
699
+ this.#div.setAttribute(
700
+ "aria-label",
701
+ this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait"),
702
+ );
703
+ this.dispatchEvent("progress", { progress: 0 });
704
+
705
+ try {
706
+ let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
707
+ if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
708
+ apiEndpoint = "/";
709
+ } else if (!apiEndpoint) {
710
+ throw new Error(
711
+ "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
712
+ );
713
+ }
714
+ if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
715
+
716
+ let solutions;
717
+ let challengeResp;
718
+
719
+ if (
720
+ this.#speculative.state === "done" &&
721
+ this.#speculative.token &&
722
+ this.#speculative.tokenExpires &&
723
+ Date.now() < this.#speculative.tokenExpires
724
+ ) {
725
+ this.dispatchEvent("progress", { progress: 100 });
726
+
727
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
728
+ if (this.querySelector(`input[name='${fieldName}']`)) {
729
+ this.querySelector(`input[name='${fieldName}']`).value = this.#speculative.token;
730
+ }
731
+ this.dispatchEvent("solve", { token: this.#speculative.token });
732
+ this.token = this.#speculative.token;
733
+
734
+ const expiresIn = this.#speculative.tokenExpires - Date.now();
735
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
736
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
737
+
738
+ this.#div.setAttribute(
739
+ "aria-label",
740
+ this.getI18nText(
741
+ "verified-aria-label",
742
+ "We have verified you're a human, you may now continue",
743
+ ),
744
+ );
745
+ if (this.#hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
746
+
747
+ this.#resetSpeculativeState();
748
+ this.#solving = false;
749
+ return { success: true, token: this.token };
750
+ }
751
+
752
+ if (this.#speculative.state === "done") {
753
+ solutions = this.#speculative.results;
754
+ challengeResp = this.#speculative.challengeResp;
755
+ this.dispatchEvent("progress", { progress: 100 });
756
+ } else if (
757
+ this.#speculative.state === "solving" ||
758
+ this.#speculative.state === "redeeming" ||
759
+ this.#speculative.state === "fetching" ||
760
+ this.#speculative.state === "waiting"
761
+ ) {
762
+ if (this.#speculative.state === "waiting") {
763
+ if (this.#speculativeTimer) {
764
+ clearTimeout(this.#speculativeTimer);
765
+ this.#speculativeTimer = null;
766
+ }
767
+ this.#speculative.state = "waiting";
768
+ this.#beginSpeculativeSolve();
769
+ }
770
+
771
+ this.#speculative.pendingPromotion = this.#workersCount;
772
+ if (this.#speculative.promoteFn) {
773
+ this.#speculative.promoteFn(this.#workersCount);
774
+ }
775
+
776
+ const progressInterval = setInterval(() => {
777
+ const st = this.#speculative.state;
778
+ if (st === "done" || st === "error") {
779
+ clearInterval(progressInterval);
780
+ return;
781
+ }
782
+ const total = this.#speculative.challenges ? this.#speculative.challenges.length : 1;
783
+ const done = this.#speculative.completedCount;
784
+ const visual =
785
+ st === "redeeming"
786
+ ? 99
787
+ : st === "fetching" || st === "waiting"
788
+ ? 0
789
+ : Math.min(98, Math.round((done / total) * 100));
790
+ this.dispatchEvent("progress", { progress: visual });
791
+ }, 150);
792
+
793
+ await new Promise((resolve) => this.#speculative.onSettled(resolve));
794
+ clearInterval(progressInterval);
795
+
796
+ if (this.#speculative.state !== "done") {
797
+ throw new Error("Speculative solve failed – please try again");
798
+ }
799
+
800
+ if (
801
+ this.#speculative.token &&
802
+ this.#speculative.tokenExpires &&
803
+ Date.now() < this.#speculative.tokenExpires
804
+ ) {
805
+ this.dispatchEvent("progress", { progress: 100 });
806
+
807
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
808
+ if (this.querySelector(`input[name='${fieldName}']`)) {
809
+ this.querySelector(`input[name='${fieldName}']`).value = this.#speculative.token;
810
+ }
811
+ this.dispatchEvent("solve", { token: this.#speculative.token });
812
+ this.token = this.#speculative.token;
813
+
814
+ const expiresIn = this.#speculative.tokenExpires - Date.now();
815
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
816
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
817
+
818
+ this.#div.setAttribute(
819
+ "aria-label",
820
+ this.getI18nText(
821
+ "verified-aria-label",
822
+ "We have verified you're a human, you may now continue",
823
+ ),
824
+ );
825
+ if (this.#hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
826
+
827
+ this.#resetSpeculativeState();
828
+ this.#solving = false;
829
+ return { success: true, token: this.token };
830
+ }
831
+
832
+ solutions = this.#speculative.results;
833
+ challengeResp = this.#speculative.challengeResp;
834
+ this.dispatchEvent("progress", { progress: 100 });
835
+ } else {
836
+ const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
837
+ method: "POST",
838
+ });
839
+ try {
840
+ challengeResp = await challengeRaw.json();
841
+ } catch {
842
+ throw new Error("Failed to parse challenge response from server");
843
+ }
844
+ if (challengeResp.error) throw new Error(challengeResp.error);
845
+
846
+ const { challenge, token } = challengeResp;
847
+ let challenges = challenge;
848
+ if (!Array.isArray(challenges)) {
849
+ let i = 0;
850
+ challenges = Array.from({ length: challenge.c }, () => {
851
+ i++;
852
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
853
+ });
854
+ }
855
+
856
+ solutions = await this.solveChallenges(challenges);
857
+ }
858
+
859
+ const instrPromise = challengeResp.instrumentation
860
+ ? runInstrumentationChallenge(challengeResp.instrumentation)
861
+ : Promise.resolve(null);
862
+
863
+ const instrOut = await instrPromise;
864
+
865
+ if (instrOut?.__timeout || instrOut?.__blocked) {
866
+ this.updateUIBlocked(this.getI18nText("error-label", "Error"), instrOut?.__blocked);
867
+ this.#div.setAttribute(
868
+ "aria-label",
869
+ this.getI18nText("error-aria-label", "An error occurred, please try again"),
870
+ );
871
+ this.removeEventListener("error", this.boundHandleError);
872
+ const errEvent = new CustomEvent("error", {
873
+ bubbles: true,
874
+ composed: true,
875
+ detail: { isCap: true, message: "Instrumentation failed" },
876
+ });
877
+ super.dispatchEvent(errEvent);
878
+ this.addEventListener("error", this.boundHandleError);
879
+ this.executeAttributeCode("onerror", errEvent);
880
+ console.error("[cap]", "Instrumentation failed");
881
+ this.#solving = false;
882
+ return;
883
+ }
884
+
885
+ const { token } = challengeResp;
886
+
887
+ const redeemResponse = await capFetch(`${apiEndpoint}redeem`, {
888
+ method: "POST",
889
+ body: JSON.stringify({
890
+ token,
891
+ solutions,
892
+ ...(instrOut && { instr: instrOut }),
893
+ }),
894
+ headers: { "Content-Type": "application/json" },
895
+ });
896
+
897
+ let resp;
898
+ try {
899
+ resp = await redeemResponse.json();
900
+ } catch {
901
+ throw new Error("Failed to parse server response");
902
+ }
903
+
904
+ this.dispatchEvent("progress", { progress: 100 });
905
+ if (!resp.success) throw new Error(resp.error || "Invalid solution");
906
+
907
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
908
+ if (this.querySelector(`input[name='${fieldName}']`)) {
909
+ this.querySelector(`input[name='${fieldName}']`).value = resp.token;
910
+ }
911
+
912
+ this.dispatchEvent("solve", { token: resp.token });
913
+ this.token = resp.token;
914
+
915
+ this.#resetSpeculativeState();
916
+
917
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
918
+ const expiresIn = new Date(resp.expires).getTime() - Date.now();
919
+ if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
920
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
921
+ } else {
922
+ this.error("Invalid expiration time");
923
+ }
924
+
925
+ this.#div.setAttribute(
926
+ "aria-label",
927
+ this.getI18nText(
928
+ "verified-aria-label",
929
+ "We have verified you're a human, you may now continue",
930
+ ),
931
+ );
932
+ if (this.#hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
933
+
934
+ return { success: true, token: this.token };
935
+ } catch (err) {
936
+ this.#div.setAttribute(
937
+ "aria-label",
938
+ this.getI18nText("error-aria-label", "An error occurred, please try again"),
939
+ );
940
+ this.error(err.message);
941
+ throw err;
942
+ }
943
+ } finally {
944
+ this.#solving = false;
945
+ }
946
+ }
947
+
948
+ async solveChallenges(challenges) {
949
+ const total = challenges.length;
950
+ let completed = 0;
951
+
952
+ const speculativeHead = 0;
953
+
954
+ let wasmModule = null;
955
+ const wasmSupported =
956
+ typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
957
+
958
+ if (wasmSupported) {
959
+ try {
960
+ wasmModule = await getWasmModule();
961
+ } catch (e) {
962
+ console.warn("[cap] wasm unavailable, falling back to JS solver:", e);
963
+ }
964
+ }
965
+
966
+ if (!wasmSupported) {
967
+ if (!this.#shadow.querySelector(".warning")) {
968
+ const warningEl = document.createElement("div");
969
+ warningEl.className = "warning";
970
+ warningEl.style.cssText = `width:var(--cap-widget-width,230px);background:rgb(237,56,46);color:white;padding:4px 6px;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);font-size:10px;box-sizing:border-box;font-family:system-ui;border-top-left-radius:8px;border-top-right-radius:8px;text-align:center;user-select:none;margin-bottom:-35.5px;opacity:0;transition:margin-bottom .3s,opacity .3s;`;
971
+ warningEl.innerText = this.getI18nText(
972
+ "wasm-disabled",
973
+ "Enable WASM for significantly faster solving",
974
+ );
975
+ this.#shadow.insertBefore(warningEl, this.#shadow.firstChild);
976
+ setTimeout(() => {
977
+ warningEl.style.marginBottom = `calc(-1 * var(--cap-border-radius, 14px))`;
978
+ warningEl.style.opacity = 1;
979
+ }, 10);
980
+ }
981
+ }
982
+
983
+ const pool = new WorkerPool(this.#workersCount);
984
+ pool.setWasm(wasmModule);
985
+ pool._ensureSize(this.#workersCount);
986
+
987
+ const results = [];
988
+ try {
989
+ for (let i = 0; i < challenges.length; i += this.#workersCount) {
990
+ const chunk = challenges.slice(i, Math.min(i + this.#workersCount, challenges.length));
991
+ const chunkResults = await Promise.all(
992
+ chunk.map(([salt, target]) =>
993
+ pool.run(salt, target).then((nonce) => {
994
+ completed++;
995
+ const visual = Math.min(
996
+ 99,
997
+ Math.round(((speculativeHead + completed) / total) * 100),
998
+ );
999
+ this.dispatchEvent("progress", { progress: visual });
1000
+ return nonce;
1001
+ }),
1002
+ ),
1003
+ );
1004
+ results.push(...chunkResults);
1005
+ }
1006
+ } finally {
1007
+ pool.terminate();
1008
+ }
1009
+
1010
+ return results;
1011
+ }
1012
+
1013
+ setWorkersCount(workers) {
1014
+ const parsedWorkers = parseInt(workers, 10);
1015
+ const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16);
1016
+ this.#workersCount =
1017
+ !Number.isNaN(parsedWorkers) && parsedWorkers > 0 && parsedWorkers <= maxWorkers
1018
+ ? parsedWorkers
1019
+ : navigator.hardwareConcurrency || 8;
1020
+ }
1021
+
1022
+ createUI() {
1023
+ this.#div.classList.add("captcha");
1024
+ this.#div.setAttribute("role", "button");
1025
+ this.#div.setAttribute("tabindex", "0");
1026
+ this.#div.setAttribute(
1027
+ "aria-label",
1028
+ this.getI18nText("verify-aria-label", "Click to verify you're a human"),
1029
+ );
1030
+ this.#div.setAttribute("aria-live", "polite");
1031
+ this.#div.setAttribute("disabled", "true");
1032
+ this.#div.innerHTML = `<div class="checkbox" part="checkbox"><svg class="progress-ring" viewBox="0 0 32 32"><circle class="progress-ring-bg" cx="16" cy="16" r="14"></circle><circle class="progress-ring-circle" cx="16" cy="16" r="14"></circle></svg></div><p part="label" class="label-wrapper"><span class="label active">${this.getI18nText(
1033
+ "initial-state",
1034
+ "Verify you're human",
1035
+ )}</span></p><a part="attribution" aria-label="Secured by Cap" href="https://trycap.dev/?${new URLSearchParams(
1036
+ // this attribution is only for our plausible analytics
1037
+ // instancee. no personal data is collected.
1038
+ {
1039
+ utm_source: "cap_widget",
1040
+ utm_medium: "referral",
1041
+ utm_campaign: "widget",
1042
+ utm_content: window.CAP_DISABLE_WIDGET_REF ? "" : location.hostname,
1043
+ ref: window.CAP_DISABLE_WIDGET_REF ? "" : location.href || "",
1044
+ sub: window.CAP_DISABLE_WIDGET_REF ? "" : document.referrer || "",
1045
+ },
1046
+ ).toString()}" class="credits" target="_blank" title="Secured by Cap: The self-hosted CAPTCHA for the modern web.">Cap</a>`;
1047
+
1048
+ this.#shadow.innerHTML = `<style${window.CAP_CSS_NONCE ? ` nonce=${window.CAP_CSS_NONCE}` : ""}>%%capCSS%%</style>`;
1049
+
1050
+ this.#shadow.appendChild(this.#div);
1051
+ }
1052
+
1053
+ addEventListeners() {
1054
+ if (!this.#div) return;
1055
+
1056
+ this.#div.querySelector("a").addEventListener("click", (e) => {
1057
+ e.stopPropagation();
1058
+ e.preventDefault();
1059
+ window.open("https://trycap.dev", "_blank");
1060
+ });
1061
+
1062
+ this.#div.addEventListener("click", () => {
1063
+ if (!this.#div.hasAttribute("disabled")) this.solve();
1064
+ });
1065
+ this.#div.addEventListener("mousedown", () => {
1066
+ if (!this.#div.hasAttribute("disabled") && this.#hasHaptics) {
1067
+ navigator.vibrate(5);
1068
+ }
1069
+ });
1070
+
1071
+ this.#div.addEventListener("keydown", (e) => {
1072
+ if ((e.key === "Enter" || e.key === " ") && !this.#div.hasAttribute("disabled")) {
1073
+ e.preventDefault();
1074
+ e.stopPropagation();
1075
+ this.solve();
1076
+ }
1077
+ });
1078
+
1079
+ this.addEventListener("progress", this.boundHandleProgress);
1080
+ this.addEventListener("solve", this.boundHandleSolve);
1081
+ this.addEventListener("error", this.boundHandleError);
1082
+ this.addEventListener("reset", this.boundHandleReset);
1083
+ }
1084
+
1085
+ animateLabel(text) {
1086
+ if (!this.#div) return;
1087
+ const wrapper = this.#div.querySelector(".label-wrapper");
1088
+ if (!wrapper) return;
1089
+
1090
+ if (prefersReducedMotion()) {
1091
+ const current = wrapper.querySelector(".label.active");
1092
+ if (current) {
1093
+ current.textContent = text;
1094
+ } else {
1095
+ const span = document.createElement("span");
1096
+ span.className = "label active";
1097
+ span.textContent = text;
1098
+ wrapper.appendChild(span);
1099
+ }
1100
+ return;
1101
+ }
1102
+
1103
+ const current = wrapper.querySelector(".label.active");
1104
+
1105
+ const next = document.createElement("span");
1106
+ next.className = "label";
1107
+ next.textContent = text;
1108
+ wrapper.appendChild(next);
1109
+
1110
+ void next.offsetWidth;
1111
+
1112
+ next.classList.add("active");
1113
+ if (current) {
1114
+ current.classList.remove("active");
1115
+ current.classList.add("exit");
1116
+ current.addEventListener("transitionend", () => current.remove(), {
1117
+ once: true,
1118
+ });
1119
+ }
1120
+ }
1121
+
1122
+ updateUI(state, text, disabled = false) {
1123
+ if (!this.#div) return;
1124
+
1125
+ this.#div.setAttribute("data-state", state);
1126
+
1127
+ this.animateLabel(text);
1128
+
1129
+ if (disabled) {
1130
+ this.#div.setAttribute("disabled", "true");
1131
+ } else {
1132
+ this.#div.removeAttribute("disabled");
1133
+ }
1134
+ }
1135
+
1136
+ updateUIBlocked(label, showTroubleshooting = false) {
1137
+ if (!this.#div) return;
1138
+
1139
+ this.#div.setAttribute("data-state", "error");
1140
+ this.#div.removeAttribute("disabled");
1141
+
1142
+ const wrapper = this.#div.querySelector(".label-wrapper");
1143
+ if (!wrapper) return;
1144
+
1145
+ const troubleshootingUrl =
1146
+ this.getAttribute("data-cap-troubleshooting-url") ||
1147
+ "https://trycap.dev/guide/troubleshooting/instrumentation.html";
1148
+
1149
+ const current = wrapper.querySelector(".label.active");
1150
+ const next = document.createElement("span");
1151
+ next.className = "label";
1152
+ next.innerHTML = showTroubleshooting
1153
+ ? `${label} · <a class="cap-troubleshoot-link" href="${troubleshootingUrl}" target="_blank" rel="noopener">${this.getI18nText("troubleshooting-label", "Troubleshoot")}</a>`
1154
+ : label;
1155
+ wrapper.appendChild(next);
1156
+
1157
+ void next.offsetWidth;
1158
+ next.classList.add("active");
1159
+ if (current) {
1160
+ current.classList.remove("active");
1161
+ current.classList.add("exit");
1162
+ current.addEventListener("transitionend", () => current.remove(), {
1163
+ once: true,
1164
+ });
1165
+ }
1166
+
1167
+ const link = next.querySelector(".cap-troubleshoot-link");
1168
+ if (link) {
1169
+ link.addEventListener("click", (e) => {
1170
+ e.stopPropagation();
1171
+ });
1172
+ }
1173
+ }
1174
+
1175
+ handleProgress(event) {
1176
+ if (!this.#div) return;
1177
+
1178
+ const progressCircle = this.#div.querySelector(".progress-ring-circle");
1179
+
1180
+ if (progressCircle) {
1181
+ const circumference = 2 * Math.PI * 14;
1182
+ const offset = circumference - (event.detail.progress / 100) * circumference;
1183
+ progressCircle.style.strokeDashoffset = offset;
1184
+ }
1185
+
1186
+ const wrapper = this.#div.querySelector(".label-wrapper");
1187
+ if (wrapper) {
1188
+ const activeLabel = wrapper.querySelector(".label.active");
1189
+ if (activeLabel) {
1190
+ activeLabel.textContent = `${this.getI18nText("verifying-label", "Verifying...")}`;
1191
+ }
1192
+ }
1193
+
1194
+ this.executeAttributeCode("onprogress", event);
1195
+ }
1196
+
1197
+ handleSolve(event) {
1198
+ this.updateUI("done", this.getI18nText("solved-label", "You're a human"), true);
1199
+ this.executeAttributeCode("onsolve", event);
1200
+ this.#internals?.setValidity?.({});
1201
+ this.#div?.classList.remove("invalid");
1202
+ }
1203
+
1204
+ handleError(event) {
1205
+ this.updateUI("error", this.getI18nText("error-label", "Error. Try again."));
1206
+ this.executeAttributeCode("onerror", event);
1207
+
1208
+ if (this.#hasHaptics) navigator.vibrate([10, 40, 10]);
1209
+ }
1210
+
1211
+ handleReset(event) {
1212
+ this.updateUI("", this.getI18nText("initial-state", "I'm a human"));
1213
+ this.executeAttributeCode("onreset", event);
1214
+ this.#updateValidity();
1215
+ }
1216
+
1217
+ executeAttributeCode(attributeName, event) {
1218
+ const code = this.getAttribute(attributeName);
1219
+ if (!code) {
1220
+ return;
1221
+ }
1222
+
1223
+ console.error(
1224
+ "[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.",
1225
+ );
1226
+
1227
+ new Function("event", code).call(this, event);
1228
+ }
1229
+
1230
+ error(message = "Unknown error") {
1231
+ console.error("[cap]", message);
1232
+ this.dispatchEvent("error", { isCap: true, message });
1233
+ }
1234
+
1235
+ dispatchEvent(eventName, detail = {}) {
1236
+ const event = new CustomEvent(eventName, {
1237
+ bubbles: true,
1238
+ composed: true,
1239
+ detail,
1240
+ });
1241
+ super.dispatchEvent(event);
1242
+ }
1243
+
1244
+ reset() {
1245
+ if (this.#resetTimer) {
1246
+ clearTimeout(this.#resetTimer);
1247
+ this.#resetTimer = null;
1248
+ }
1249
+ this.token = null;
1250
+ this.dispatchEvent("reset");
1251
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
1252
+ if (this.querySelector(`input[name='${fieldName}']`)) {
1253
+ this.querySelector(`input[name='${fieldName}']`).value = "";
1254
+ }
1255
+ }
1256
+
1257
+ get tokenValue() {
1258
+ return this.token;
1259
+ }
1260
+
1261
+ disconnectedCallback() {
1262
+ this.removeEventListener("progress", this.boundHandleProgress);
1263
+ this.removeEventListener("solve", this.boundHandleSolve);
1264
+ this.removeEventListener("error", this.boundHandleError);
1265
+ this.removeEventListener("reset", this.boundHandleReset);
1266
+
1267
+ this.#eventHandlers.forEach((handler, eventName) => {
1268
+ this.removeEventListener(eventName.slice(2), handler);
1269
+ });
1270
+ this.#eventHandlers.clear();
1271
+
1272
+ if (this.#shadow) {
1273
+ this.#shadow.innerHTML = "";
1274
+ }
1275
+
1276
+ this.reset();
1277
+ this.cleanup();
1278
+ }
1279
+
1280
+ cleanup() {
1281
+ if (this.#resetTimer) {
1282
+ clearTimeout(this.#resetTimer);
1283
+ this.#resetTimer = null;
1284
+ }
1285
+
1286
+ this.#detachInteractionListeners();
1287
+ if (this.#speculativeTimer) {
1288
+ clearTimeout(this.#speculativeTimer);
1289
+ this.#speculativeTimer = null;
1290
+ }
1291
+ if (this.#speculativePool) {
1292
+ this.#speculativePool.terminate();
1293
+ this.#speculativePool = null;
1294
+ }
1295
+ if (this.#speculative) {
1296
+ this.#speculative.state = "error";
1297
+ this.#speculative.notify();
1298
+ this.#speculative = null;
1299
+ }
1300
+ }
1301
+ }
1302
+
1303
+ class Cap {
1304
+ constructor(config = {}, el) {
1305
+ const widget = el || document.createElement("cap-widget");
1306
+
1307
+ Object.entries(config).forEach(([a, b]) => {
1308
+ widget.setAttribute(a, b);
1309
+ });
1310
+
1311
+ if (!config.apiEndpoint && !window?.CAP_CUSTOM_FETCH) {
1312
+ widget.remove();
1313
+ throw new Error(
1314
+ "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
1315
+ );
1316
+ }
1317
+
1318
+ if (config.apiEndpoint) {
1319
+ widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint);
1320
+ }
1321
+
1322
+ if (!el && !widget.hasAttribute("data-cap-disable-haptics")) {
1323
+ widget.setAttribute("data-cap-disable-haptics", "");
1324
+ }
1325
+
1326
+ this.widget = widget;
1327
+ this.solve = this.widget.solve.bind(this.widget);
1328
+ this.reset = this.widget.reset.bind(this.widget);
1329
+ this.addEventListener = this.widget.addEventListener.bind(this.widget);
1330
+
1331
+ Object.defineProperty(this, "token", {
1332
+ get: () => widget.token,
1333
+ configurable: true,
1334
+ enumerable: true,
1335
+ });
1336
+
1337
+ if (!el) {
1338
+ widget.style.display = "none";
1339
+ document.documentElement.appendChild(widget);
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ window.Cap = Cap;
1345
+
1346
+ if (!customElements.get("cap-widget") && !window?.CAP_DONT_SKIP_REDEFINE) {
1347
+ customElements.define("cap-widget", CapWidget);
1348
+ } else if (customElements.get("cap-widget")) {
1349
+ console.warn(
1350
+ "[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true",
1351
+ );
1352
+ }
1353
+
1354
+ if (typeof exports === "object" && typeof module !== "undefined") {
1355
+ module.exports = Cap;
1356
+ } else if (typeof define === "function" && define.amd) {
1357
+ define([], () => Cap);
1358
+ }
1359
+
1360
+ if (typeof exports !== "undefined") {
1361
+ exports.default = Cap;
1362
+ }
1363
+ })();