@cap.js/widget 0.1.35 → 0.1.37

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 CHANGED
@@ -1,613 +1,870 @@
1
1
  (() => {
2
- const WASM_VERSION = "0.0.6";
2
+ const WASM_VERSION = "0.0.6";
3
3
 
4
- if (typeof window === "undefined") {
4
+ if (typeof window === "undefined") {
5
5
  return;
6
6
  }
7
7
 
8
- const capFetch = (...args) => {
9
- if (window?.CAP_CUSTOM_FETCH) {
10
- return window.CAP_CUSTOM_FETCH(...args);
11
- }
12
- return fetch(...args);
13
- };
14
-
15
- function prng(seed, length) {
16
- function fnv1a(str) {
17
- let hash = 2166136261;
18
- for (let i = 0; i < str.length; i++) {
19
- hash ^= str.charCodeAt(i);
20
- hash +=
21
- (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
22
- }
23
- return hash >>> 0;
24
- }
25
-
26
- let state = fnv1a(seed);
27
- let result = "";
28
-
29
- function next() {
30
- state ^= state << 13;
31
- state ^= state >>> 17;
32
- state ^= state << 5;
33
- return state >>> 0;
34
- }
35
-
36
- while (result.length < length) {
37
- const rnd = next();
38
- result += rnd.toString(16).padStart(8, "0");
39
- }
40
-
41
- return result.substring(0, length);
42
- }
43
-
44
- if (!window.CAP_CUSTOM_WASM_URL) {
45
- // preloads the wasm files to save up time on solve
46
- [
47
- `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
48
- `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`,
49
- ].forEach((url) => {
50
- const link = document.createElement("link");
51
- link.rel = "prefetch";
52
- link.href = url;
53
- link.as = url.endsWith(".wasm") ? "fetch" : "script";
54
- document.head.appendChild(link);
55
- });
56
- }
57
-
58
- class CapWidget extends HTMLElement {
59
- #workerUrl = "";
60
- #resetTimer = null;
61
- #workersCount = navigator.hardwareConcurrency || 8;
62
- token = null;
63
- #shadow;
64
- #div;
65
- #host;
66
- #solving = false;
67
- #eventHandlers;
68
-
69
- getI18nText(key, defaultValue) {
70
- return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue;
71
- }
72
-
73
- static get observedAttributes() {
74
- return [
75
- "onsolve",
76
- "onprogress",
77
- "onreset",
78
- "onerror",
79
- "data-cap-worker-count",
80
- "data-cap-i18n-initial-state",
81
- "[cap]",
82
- ];
83
- }
84
-
85
- constructor() {
86
- super();
87
- if (this.#eventHandlers) {
88
- this.#eventHandlers.forEach((handler, eventName) => {
89
- this.removeEventListener(eventName.slice(2), handler);
90
- });
91
- }
92
-
93
- this.#eventHandlers = new Map();
94
- this.boundHandleProgress = this.handleProgress.bind(this);
95
- this.boundHandleSolve = this.handleSolve.bind(this);
96
- this.boundHandleError = this.handleError.bind(this);
97
- this.boundHandleReset = this.handleReset.bind(this);
98
- }
99
-
100
- initialize() {
101
- this.#workerUrl = URL.createObjectURL(
102
- // this placeholder will be replaced with the actual worker by the build script
103
-
104
- new Blob([`%%workerScript%%`], {
105
- type: "application/javascript",
106
- }),
107
- );
108
- }
109
-
110
- attributeChangedCallback(name, _, value) {
111
- if (name.startsWith("on")) {
112
- const eventName = name.slice(2);
113
- const oldHandler = this.#eventHandlers.get(name);
114
- if (oldHandler) {
115
- this.removeEventListener(eventName, oldHandler);
116
- }
117
-
118
- if (value) {
119
- const handler = (event) => {
120
- const callback = this.getAttribute(name);
121
- if (typeof window[callback] === "function") {
122
- window[callback].call(this, event);
123
- }
124
- };
125
- this.#eventHandlers.set(name, handler);
126
- this.addEventListener(eventName, handler);
127
- }
128
- }
129
-
130
- if (name === "data-cap-worker-count") {
131
- this.setWorkersCount(parseInt(value, 10));
132
- }
133
-
134
- if (
135
- name === "data-cap-i18n-initial-state" &&
136
- this.#div &&
137
- this.#div?.querySelector("p")?.innerText
138
- ) {
139
- this.#div.querySelector("p").innerText = this.getI18nText(
140
- "initial-state",
141
- "Verify you're human",
142
- );
143
- }
144
- }
145
-
146
- async connectedCallback() {
147
- this.#host = this;
148
- this.#shadow = this.attachShadow({ mode: "open" });
149
- this.#div = document.createElement("div");
150
- this.createUI();
151
- this.addEventListeners();
152
- this.initialize();
153
- this.#div.removeAttribute("disabled");
154
-
155
- const workers = this.getAttribute("data-cap-worker-count");
156
- const parsedWorkers = workers ? parseInt(workers, 10) : null;
157
- this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
158
- const fieldName =
159
- this.getAttribute("data-cap-hidden-field-name") || "cap-token";
160
- this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
161
- }
162
-
163
- async solve() {
164
- if (this.#solving) {
165
- return;
166
- }
167
-
168
- try {
169
- this.#solving = true;
170
- this.updateUI(
171
- "verifying",
172
- this.getI18nText("verifying-label", "Verifying..."),
173
- true,
174
- );
175
-
176
- this.#div.setAttribute(
177
- "aria-label",
178
- this.getI18nText(
179
- "verifying-aria-label",
180
- "Verifying you're a human, please wait",
181
- ),
182
- );
183
-
184
- this.dispatchEvent("progress", { progress: 0 });
185
-
186
- try {
187
- let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
188
-
189
- if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
190
- apiEndpoint = "/";
191
- } else if (!apiEndpoint)
192
- throw new Error(
193
- "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
194
- );
195
-
196
- if (!apiEndpoint.endsWith("/")) {
8
+ const capFetch = (u, _conf = {}) => {
9
+ const conf = {
10
+ ..._conf,
11
+ headers: {
12
+ ...(_conf.headers || {}),
13
+ "Cap-Stamp": btoa(
14
+ String.fromCharCode(
15
+ ...[[Date.now()]].map((n) => [n >> 24, n >> 16, n >> 8, n[0]].map((x) => x & 255))[0],
16
+ ),
17
+ ).replace(/=/g, ""),
18
+ "Cap-Solver": `0,${WASM_VERSION}`,
19
+ },
20
+ };
21
+
22
+ if (window?.CAP_CUSTOM_FETCH) {
23
+ return window.CAP_CUSTOM_FETCH(u, conf);
24
+ }
25
+
26
+ return fetch(u, conf);
27
+ };
28
+
29
+ function prng(seed, length) {
30
+ function fnv1a(str) {
31
+ let hash = 2166136261;
32
+ for (let i = 0; i < str.length; i++) {
33
+ hash ^= str.charCodeAt(i);
34
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
35
+ }
36
+ return hash >>> 0;
37
+ }
38
+
39
+ let state = fnv1a(seed);
40
+ let result = "";
41
+
42
+ function next() {
43
+ state ^= state << 13;
44
+ state ^= state >>> 17;
45
+ state ^= state << 5;
46
+ return state >>> 0;
47
+ }
48
+
49
+ while (result.length < length) {
50
+ const rnd = next();
51
+ result += rnd.toString(16).padStart(8, "0");
52
+ }
53
+
54
+ return result.substring(0, length);
55
+ }
56
+
57
+ async function runInstrumentationChallenge(instrBytes) {
58
+ const b64ToUint8 = (b64) => {
59
+ const bin = atob(b64);
60
+ const arr = new Uint8Array(bin.length);
61
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
62
+ return arr;
63
+ };
64
+
65
+ var compressed = b64ToUint8(instrBytes);
66
+
67
+ const scriptText = await new Promise(function (resolve, reject) {
68
+ try {
69
+ var ds = new DecompressionStream("deflate-raw");
70
+ var writer = ds.writable.getWriter();
71
+ var reader = ds.readable.getReader();
72
+ var chunks = [];
73
+ function pump(res) {
74
+ if (res.done) {
75
+ var len = 0,
76
+ off = 0;
77
+ for (var i = 0; i < chunks.length; i++) len += chunks[i].length;
78
+ var out = new Uint8Array(len);
79
+ for (var i = 0; i < chunks.length; i++) {
80
+ out.set(chunks[i], off);
81
+ off += chunks[i].length;
82
+ }
83
+ resolve(new TextDecoder().decode(out));
84
+ } else {
85
+ chunks.push(res.value);
86
+ reader.read().then(pump).catch(reject);
87
+ }
88
+ }
89
+ reader.read().then(pump).catch(reject);
90
+ writer
91
+ .write(compressed)
92
+ .then(function () {
93
+ writer.close();
94
+ })
95
+ .catch(reject);
96
+ } catch (e) {
97
+ reject(e);
98
+ }
99
+ });
100
+
101
+ return new Promise(function (resolve) {
102
+ var timeout = setTimeout(function () {
103
+ cleanup();
104
+ resolve({ __timeout: true });
105
+ }, 20000);
106
+
107
+ var iframe = document.createElement("iframe");
108
+ iframe.setAttribute("sandbox", "allow-scripts");
109
+ iframe.setAttribute("aria-hidden", "true");
110
+ iframe.style.cssText =
111
+ "position:absolute;width:1px;height:1px;top:-9999px;left:-9999px;border:none;opacity:0;pointer-events:none;";
112
+
113
+ var resolved = false;
114
+ function cleanup() {
115
+ if (resolved) return;
116
+ resolved = true;
117
+ clearTimeout(timeout);
118
+ window.removeEventListener("message", handler);
119
+ if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
120
+ }
121
+
122
+ function handler(ev) {
123
+ var d = ev.data;
124
+ if (!d || typeof d !== "object") return;
125
+ if (d.type === "cap:instr") {
126
+ cleanup();
127
+ if (d.blocked) {
128
+ resolve({ __blocked: true, blockReason: d.blockReason || "automated_browser" });
129
+ } else if (d.result) {
130
+ resolve(d.result);
131
+ } else {
132
+ resolve({ __timeout: true });
133
+ }
134
+ } else if (d.type === "cap:error") {
135
+ cleanup();
136
+ resolve({ __timeout: true });
137
+ }
138
+ }
139
+
140
+ window.addEventListener("message", handler);
141
+
142
+ iframe.srcdoc =
143
+ '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><script>' +
144
+ scriptText +
145
+ "\n</scr" +
146
+ "ipt></body></html>";
147
+
148
+ document.body.appendChild(iframe);
149
+ });
150
+ }
151
+
152
+ let wasmModulePromise = null;
153
+
154
+ const getWasmModule = () => {
155
+ if (wasmModulePromise) return wasmModulePromise;
156
+
157
+ const wasmUrl =
158
+ window.CAP_CUSTOM_WASM_URL ||
159
+ `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`;
160
+
161
+ wasmModulePromise = fetch(wasmUrl)
162
+ .then((r) => {
163
+ if (!r.ok) throw new Error(`Failed to fetch wasm: ${r.status}`);
164
+ return r.arrayBuffer();
165
+ })
166
+ .then((buf) => WebAssembly.compile(buf))
167
+ .catch((e) => {
168
+ wasmModulePromise = null;
169
+ throw e;
170
+ });
171
+
172
+ return wasmModulePromise;
173
+ };
174
+
175
+ if (typeof WebAssembly === "object" && typeof WebAssembly.compile === "function") {
176
+ getWasmModule().catch(() => {});
177
+ }
178
+
179
+ const prefersReducedMotion = () =>
180
+ window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
181
+
182
+ class CapWidget extends HTMLElement {
183
+ #workerUrl = "";
184
+ #resetTimer = null;
185
+ #workersCount = navigator.hardwareConcurrency || 8;
186
+ token = null;
187
+ #shadow;
188
+ #div;
189
+ #host;
190
+ #solving = false;
191
+ #eventHandlers;
192
+
193
+ getI18nText(key, defaultValue) {
194
+ return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue;
195
+ }
196
+
197
+ static get observedAttributes() {
198
+ return [
199
+ "onsolve",
200
+ "onprogress",
201
+ "onreset",
202
+ "onerror",
203
+ "data-cap-worker-count",
204
+ "data-cap-i18n-initial-state",
205
+ ];
206
+ }
207
+
208
+ constructor() {
209
+ super();
210
+ if (this.#eventHandlers) {
211
+ this.#eventHandlers.forEach((handler, eventName) => {
212
+ this.removeEventListener(eventName.slice(2), handler);
213
+ });
214
+ }
215
+
216
+ this.#eventHandlers = new Map();
217
+ this.boundHandleProgress = this.handleProgress.bind(this);
218
+ this.boundHandleSolve = this.handleSolve.bind(this);
219
+ this.boundHandleError = this.handleError.bind(this);
220
+ this.boundHandleReset = this.handleReset.bind(this);
221
+ }
222
+
223
+ initialize() {
224
+ this.#workerUrl = URL.createObjectURL(
225
+ // this placeholder will be replaced with the actual worker by the build script
226
+
227
+ new Blob([`%%workerScript%%`], {
228
+ type: "application/javascript",
229
+ }),
230
+ );
231
+ }
232
+
233
+ attributeChangedCallback(name, _, value) {
234
+ if (name.startsWith("on")) {
235
+ const eventName = name.slice(2);
236
+ const oldHandler = this.#eventHandlers.get(name);
237
+ if (oldHandler) {
238
+ this.removeEventListener(eventName, oldHandler);
239
+ }
240
+
241
+ if (value) {
242
+ const handler = (event) => {
243
+ const callback = this.getAttribute(name);
244
+ if (typeof window[callback] === "function") {
245
+ window[callback].call(this, event);
246
+ }
247
+ };
248
+ this.#eventHandlers.set(name, handler);
249
+ this.addEventListener(eventName, handler);
250
+ }
251
+ }
252
+
253
+ if (name === "data-cap-worker-count") {
254
+ this.setWorkersCount(parseInt(value, 10));
255
+ }
256
+
257
+ if (
258
+ name === "data-cap-i18n-initial-state" &&
259
+ this.#div &&
260
+ this.#div?.querySelector(".label.active")
261
+ ) {
262
+ this.animateLabel(this.getI18nText("initial-state", "Verify you're human"));
263
+ }
264
+ }
265
+
266
+ async connectedCallback() {
267
+ this.#host = this;
268
+ this.#shadow = this.attachShadow({ mode: "open" });
269
+ this.#div = document.createElement("div");
270
+ this.createUI();
271
+ this.addEventListeners();
272
+ this.initialize();
273
+ this.#div.removeAttribute("disabled");
274
+
275
+ const workers = this.getAttribute("data-cap-worker-count");
276
+ const parsedWorkers = workers ? parseInt(workers, 10) : null;
277
+ this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
278
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
279
+ this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
280
+ }
281
+
282
+ async solve() {
283
+ if (this.#solving) {
284
+ return;
285
+ }
286
+
287
+ try {
288
+ this.#solving = true;
289
+ this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true);
290
+
291
+ this.#div.setAttribute(
292
+ "aria-label",
293
+ this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait"),
294
+ );
295
+
296
+ this.dispatchEvent("progress", { progress: 0 });
297
+
298
+ try {
299
+ let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
300
+
301
+ if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
302
+ apiEndpoint = "/";
303
+ } else if (!apiEndpoint)
304
+ throw new Error(
305
+ "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
306
+ );
307
+
308
+ if (!apiEndpoint.endsWith("/")) {
197
309
  apiEndpoint += "/";
198
310
  }
199
311
 
200
- const { challenge, token } = await (
201
- await capFetch(`${apiEndpoint}challenge`, {
202
- method: "POST",
203
- })
204
- ).json();
205
-
206
- let challenges = challenge;
207
-
208
- if (!Array.isArray(challenges)) {
209
- let i = 0;
210
-
211
- challenges = Array.from({ length: challenge.c }, () => {
212
- i = i + 1;
213
-
214
- return [
215
- prng(`${token}${i}`, challenge.s),
216
- prng(`${token}${i}d`, challenge.d),
217
- ];
218
- });
219
- }
220
-
221
- const solutions = await this.solveChallenges(challenges);
222
-
223
- const resp = await (
224
- await capFetch(`${apiEndpoint}redeem`, {
225
- method: "POST",
226
- body: JSON.stringify({ token, solutions }),
227
- headers: { "Content-Type": "application/json" },
228
- })
229
- ).json();
230
-
231
- this.dispatchEvent("progress", { progress: 100 });
232
-
233
- if (!resp.success) throw new Error("Invalid solution");
234
- const fieldName =
235
- this.getAttribute("data-cap-hidden-field-name") || "cap-token";
236
- if (this.querySelector(`input[name='${fieldName}']`)) {
237
- this.querySelector(`input[name='${fieldName}']`).value = resp.token;
238
- }
239
-
240
- this.dispatchEvent("solve", { token: resp.token });
241
- this.token = resp.token;
242
-
243
- if (this.#resetTimer) clearTimeout(this.#resetTimer);
244
- const expiresIn = new Date(resp.expires).getTime() - Date.now();
245
- if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
246
- this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
247
- } else {
248
- this.error("Invalid expiration time");
249
- }
250
-
251
- this.#div.setAttribute(
252
- "aria-label",
253
- this.getI18nText(
254
- "verified-aria-label",
255
- "We have verified you're a human, you may now continue",
256
- ),
257
- );
258
-
259
- return { success: true, token: this.token };
260
- } catch (err) {
261
- this.#div.setAttribute(
262
- "aria-label",
263
- this.getI18nText(
264
- "error-aria-label",
265
- "An error occurred, please try again",
266
- ),
267
- );
268
- this.error(err.message);
269
- throw err;
270
- }
271
- } finally {
272
- this.#solving = false;
273
- }
274
- }
275
-
276
- async solveChallenges(challenge) {
277
- const total = challenge.length;
278
- let completed = 0;
279
-
280
- const workers = Array(this.#workersCount)
281
- .fill(null)
282
- .map(() => {
283
- try {
284
- return new Worker(this.#workerUrl);
285
- } catch (error) {
286
- console.error("[cap] Failed to create worker:", error);
287
- throw new Error("Worker creation failed");
288
- }
289
- });
290
-
291
- const solveSingleChallenge = ([salt, target], workerId) =>
292
- new Promise((resolve, reject) => {
293
- const worker = workers[workerId];
294
- if (!worker) {
295
- reject(new Error("Worker not available"));
296
- return;
297
- }
298
-
299
- worker.onmessage = ({ data }) => {
300
- if (!data.found) return;
301
-
302
- completed++;
303
- this.dispatchEvent("progress", {
304
- progress: Math.round((completed / total) * 100),
305
- });
306
-
307
- resolve(data.nonce);
308
- };
309
-
310
- worker.onerror = (err) => {
311
- this.error(`Error in worker: ${err.message || err}`);
312
- reject(err);
313
- };
314
-
315
- worker.postMessage({
316
- salt,
317
- target,
318
- wasmUrl:
319
- window.CAP_CUSTOM_WASM_URL ||
320
- `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
321
- });
322
-
323
- if (
324
- typeof WebAssembly !== "object" ||
325
- typeof WebAssembly?.instantiate !== "function"
326
- ) {
327
- if (this.#shadow.querySelector(".warning")) return;
328
- const warningEl = document.createElement("div");
329
- warningEl.className = "warning";
330
- 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;padding-bottom:calc(var(--cap-border-radius,14px) + 5px);user-select:none;margin-bottom: -35.5px;opacity: 0;transition: margin-bottom .3s,opacity .3s;`;
331
- warningEl.innerText =
332
- this.getI18nText("wasm-disabled", "Enable WASM for significantly faster solving");
333
- this.#shadow.insertBefore(warningEl, this.#shadow.firstChild);
334
-
335
- setTimeout(() => {
336
- warningEl.style.marginBottom = `calc(-1 * var(--cap-border-radius, 14px))`
337
- warningEl.style.opacity = 1;
338
- }, 10);
339
- }
340
- });
341
-
342
- const results = [];
343
- try {
344
- for (let i = 0; i < challenge.length; i += this.#workersCount) {
345
- const chunk = challenge.slice(
346
- i,
347
- Math.min(i + this.#workersCount, challenge.length),
348
- );
349
- const chunkResults = await Promise.all(
350
- chunk.map((c, idx) => solveSingleChallenge(c, idx)),
351
- );
352
- results.push(...chunkResults);
353
- }
354
- } finally {
355
- workers.forEach((w) => {
356
- if (w) {
357
- try {
358
- w.terminate();
359
- } catch (error) {
360
- console.error("[cap] error terminating worker:", error);
361
- }
362
- }
363
- });
364
- }
365
-
366
- return results;
367
- }
368
-
369
- setWorkersCount(workers) {
370
- const parsedWorkers = parseInt(workers, 10);
371
- const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16);
372
- this.#workersCount =
373
- !Number.isNaN(parsedWorkers) &&
374
- parsedWorkers > 0 &&
375
- parsedWorkers <= maxWorkers
376
- ? parsedWorkers
377
- : navigator.hardwareConcurrency || 8;
378
- }
379
-
380
- createUI() {
381
- this.#div.classList.add("captcha");
382
- this.#div.setAttribute("role", "button");
383
- this.#div.setAttribute("tabindex", "0");
384
- this.#div.setAttribute(
385
- "aria-label",
386
- this.getI18nText("verify-aria-label", "Click to verify you're a human"),
387
- );
388
- this.#div.setAttribute("aria-live", "polite");
389
- this.#div.setAttribute("disabled", "true");
390
- 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">${this.getI18nText(
391
- "initial-state",
392
- "Verify you're human",
393
- )}</p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener">Cap</a>`;
394
-
395
- this.#shadow.innerHTML = `<style${window.CAP_CSS_NONCE ? ` nonce=${window.CAP_CSS_NONCE}` : ""}>.captcha,.captcha * {box-sizing:border-box;}.captcha{background-color:var(--cap-background,#fdfdfd);border:1px solid var(--cap-border-color,#dddddd8f);border-radius:var(--cap-border-radius,14px);user-select:none;height:var(--cap-widget-height, 58px);width:var(--cap-widget-width, 260px);display:flex;align-items:center;padding:var(--cap-widget-padding,14px);gap:var(--cap-gap,15px);cursor:pointer;transition:filter .2s,transform .2s;position:relative;-webkit-tap-highlight-color:rgba(255,255,255,0);overflow:hidden;color:var(--cap-color,#212121)}.captcha:hover{filter:brightness(98%)}.checkbox{width:var(--cap-checkbox-size,25px);height:var(--cap-checkbox-size,25px);border:var(--cap-checkbox-border,1px solid #aaaaaad1);border-radius:var(--cap-checkbox-border-radius,6px);background-color:var(--cap-checkbox-background,#fafafa91);transition:opacity .2s;margin-top:var(--cap-checkbox-margin,2px);margin-bottom:var(--cap-checkbox-margin,2px)}.captcha *{font-family:var(--cap-font,system,-apple-system,"BlinkMacSystemFont",".SFNSText-Regular","San Francisco","Roboto","Segoe UI","Helvetica Neue","Lucida Grande","Ubuntu","arial",sans-serif)}.captcha p{margin:0;font-weight:500;font-size:15px;user-select:none;transition:opacity .2s}.checkbox .progress-ring{display:none;width:100%;height:100%;transform:rotate(-90deg)}.checkbox .progress-ring-bg{fill:none;stroke:var(--cap-spinner-background-color,#eee);stroke-width:var(--cap-spinner-thickness,3)}.checkbox .progress-ring-circle{fill:none;stroke:var(--cap-spinner-color,#000);stroke-width:var(--cap-spinner-thickness,3);stroke-linecap:round;stroke-dasharray:87.96;stroke-dashoffset:87.96;transition:stroke-dashoffset 0.3s ease}.captcha[data-state=verifying] .checkbox{background:none;display:flex;align-items:center;justify-content:center;transform:scale(1.1);border:none;border-radius:50%;background-color:transparent}.captcha[data-state=verifying] .checkbox .progress-ring{display:block}.captcha[data-state=done] .checkbox{border:1px solid transparent;background-image:var(--cap-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"));background-size:cover}.captcha[data-state=done] .checkbox .progress-ring{display:none}.captcha[data-state=error] .checkbox{border:1px solid transparent;background-image:var(--cap-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"));background-size:cover}.captcha[data-state=error] .checkbox .progress-ring{display:none}.captcha[disabled]{cursor:not-allowed}.captcha[disabled][data-state=verifying]{cursor:progress}.captcha[disabled][data-state=done]{cursor:default}.captcha .credits{position:absolute;bottom:10px;right:10px;font-size:12px;color:var(--cap-color,#212121);opacity:0.8;text-underline-offset: 1.5px;}</style>`;
396
-
397
- this.#shadow.appendChild(this.#div);
398
- }
399
-
400
- addEventListeners() {
401
- if (!this.#div) return;
402
-
403
- this.#div.querySelector("a").addEventListener("click", (e) => {
404
- e.stopPropagation();
405
- e.preventDefault();
406
- window.open("https://capjs.js.org", "_blank");
407
- });
408
-
409
- this.#div.addEventListener("click", () => {
410
- if (!this.#div.hasAttribute("disabled")) this.solve();
411
- });
412
-
413
- this.#div.addEventListener("keydown", (e) => {
414
- if (
415
- (e.key === "Enter" || e.key === " ") &&
416
- !this.#div.hasAttribute("disabled")
417
- ) {
418
- e.preventDefault();
419
- e.stopPropagation();
420
- this.solve();
421
- }
422
- });
423
-
424
- this.addEventListener("progress", this.boundHandleProgress);
425
- this.addEventListener("solve", this.boundHandleSolve);
426
- this.addEventListener("error", this.boundHandleError);
427
- this.addEventListener("reset", this.boundHandleReset);
428
- }
429
-
430
- updateUI(state, text, disabled = false) {
431
- if (!this.#div) return;
432
-
433
- this.#div.setAttribute("data-state", state);
434
-
435
- this.#div.querySelector("p").innerText = text;
436
-
437
- if (disabled) {
438
- this.#div.setAttribute("disabled", "true");
439
- } else {
440
- this.#div.removeAttribute("disabled");
441
- }
442
- }
443
-
444
- handleProgress(event) {
445
- if (!this.#div) return;
446
-
447
- const progressElement = this.#div.querySelector("p");
448
- const progressCircle = this.#div.querySelector(".progress-ring-circle");
449
-
450
- if (progressElement && progressCircle) {
451
- const circumference = 2 * Math.PI * 14;
452
- const offset = circumference - (event.detail.progress / 100) * circumference;
453
- progressCircle.style.strokeDashoffset = offset;
454
- progressElement.innerText = `${this.getI18nText(
455
- "verifying-label",
456
- "Verifying...",
457
- )} ${event.detail.progress}%`;
458
- }
459
- this.executeAttributeCode("onprogress", event);
460
- }
461
-
462
- handleSolve(event) {
463
- this.updateUI(
464
- "done",
465
- this.getI18nText("solved-label", "You're a human"),
466
- true,
467
- );
468
- this.executeAttributeCode("onsolve", event);
469
- }
470
-
471
- handleError(event) {
472
- this.updateUI(
473
- "error",
474
- this.getI18nText("error-label", "Error. Try again."),
475
- );
476
- this.executeAttributeCode("onerror", event);
477
- }
478
-
479
- handleReset(event) {
480
- this.updateUI("", this.getI18nText("initial-state", "I'm a human"));
481
- this.executeAttributeCode("onreset", event);
482
- }
483
-
484
- executeAttributeCode(attributeName, event) {
485
- const code = this.getAttribute(attributeName);
486
- if (!code) {
487
- return;
488
- }
489
-
490
- new Function("event", code).call(this, event);
491
- }
492
-
493
- error(message = "Unknown error") {
494
- console.error("[cap]", message);
495
- this.dispatchEvent("error", { isCap: true, message });
496
- }
497
-
498
- dispatchEvent(eventName, detail = {}) {
499
- const event = new CustomEvent(eventName, {
500
- bubbles: true,
501
- composed: true,
502
- detail,
503
- });
504
- super.dispatchEvent(event);
505
- }
506
-
507
- reset() {
508
- if (this.#resetTimer) {
509
- clearTimeout(this.#resetTimer);
510
- this.#resetTimer = null;
511
- }
512
- this.dispatchEvent("reset");
513
- this.token = null;
514
- const fieldName =
515
- this.getAttribute("data-cap-hidden-field-name") || "cap-token";
516
- if (this.querySelector(`input[name='${fieldName}']`)) {
517
- this.querySelector(`input[name='${fieldName}']`).value = "";
518
- }
519
- }
520
-
521
- get tokenValue() {
522
- return this.token;
523
- }
524
-
525
- disconnectedCallback() {
526
- this.removeEventListener("progress", this.boundHandleProgress);
527
- this.removeEventListener("solve", this.boundHandleSolve);
528
- this.removeEventListener("error", this.boundHandleError);
529
- this.removeEventListener("reset", this.boundHandleReset);
530
-
531
- this.#eventHandlers.forEach((handler, eventName) => {
532
- this.removeEventListener(eventName.slice(2), handler);
533
- });
534
- this.#eventHandlers.clear();
535
-
536
- if (this.#shadow) {
537
- this.#shadow.innerHTML = "";
538
- }
539
-
540
- this.reset();
541
- this.cleanup();
542
- }
543
-
544
- cleanup() {
545
- if (this.#resetTimer) {
546
- clearTimeout(this.#resetTimer);
547
- this.#resetTimer = null;
548
- }
549
-
550
- if (this.#workerUrl) {
551
- URL.revokeObjectURL(this.#workerUrl);
552
- this.#workerUrl = "";
553
- }
554
- }
555
- }
556
-
557
- // MARK: Invisible
558
- class Cap {
559
- constructor(config = {}, el) {
560
- const widget = el || document.createElement("cap-widget");
561
-
562
- Object.entries(config).forEach(([a, b]) => {
563
- widget.setAttribute(a, b);
564
- });
565
-
566
- if (!config.apiEndpoint && !window?.CAP_CUSTOM_FETCH) {
567
- widget.remove();
568
- throw new Error(
569
- "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
570
- );
571
- }
572
-
573
- if (config.apiEndpoint) {
574
- widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint);
575
- }
576
-
577
- this.widget = widget;
578
- this.solve = this.widget.solve.bind(this.widget);
579
- this.reset = this.widget.reset.bind(this.widget);
580
- this.addEventListener = this.widget.addEventListener.bind(this.widget);
581
-
582
- Object.defineProperty(this, "token", {
583
- get: () => widget.token,
584
- configurable: true,
585
- enumerable: true,
586
- });
587
-
588
- if (!el) {
589
- widget.style.display = "none";
590
- document.documentElement.appendChild(widget);
591
- }
592
- }
593
- }
594
- window.Cap = Cap;
595
-
596
- if (!customElements.get("cap-widget") && !window?.CAP_DONT_SKIP_REDEFINE) {
597
- customElements.define("cap-widget", CapWidget);
598
- } else {
599
- console.warn(
600
- "[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true",
601
- );
602
- }
603
-
604
- if (typeof exports === "object" && typeof module !== "undefined") {
605
- module.exports = Cap;
606
- } else if (typeof define === "function" && define.amd) {
607
- define([], () => Cap);
608
- }
609
-
610
- if (typeof exports !== "undefined") {
611
- exports.default = Cap;
612
- }
312
+ const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
313
+ method: "POST",
314
+ });
315
+
316
+ let challengeResp;
317
+ try {
318
+ challengeResp = await challengeRaw.json();
319
+ } catch (parseErr) {
320
+ throw new Error("Failed to parse challenge response from server");
321
+ }
322
+
323
+ if (challengeResp.error) {
324
+ throw new Error(challengeResp.error);
325
+ }
326
+
327
+ const { challenge, token } = challengeResp;
328
+
329
+ let challenges = challenge;
330
+
331
+ if (!Array.isArray(challenges)) {
332
+ let i = 0;
333
+
334
+ challenges = Array.from({ length: challenge.c }, () => {
335
+ i = i + 1;
336
+
337
+ return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
338
+ });
339
+ }
340
+
341
+ const instrPromise = challengeResp.instrumentation
342
+ ? runInstrumentationChallenge(challengeResp.instrumentation)
343
+ : Promise.resolve(null);
344
+
345
+ const powPromise = this.solveChallenges(challenges);
346
+
347
+ const instrErrorPromise = instrPromise.then((result) => {
348
+ if (result && result.__timeout) return result;
349
+ return null;
350
+ });
351
+
352
+ const instrEarlyError = await Promise.race([
353
+ instrErrorPromise,
354
+ powPromise.then(() => null),
355
+ ]);
356
+
357
+ if (instrEarlyError && instrEarlyError.__timeout) {
358
+ const errMsg = "Instrumentation timeout please try again later";
359
+ this.updateUIBlocked(this.getI18nText("error-label", "Error"), true);
360
+ this.#div.setAttribute(
361
+ "aria-label",
362
+ this.getI18nText("error-aria-label", "An error occurred, please try again"),
363
+ );
364
+ this.removeEventListener("error", this.boundHandleError);
365
+ const errEvent = new CustomEvent("error", {
366
+ bubbles: true,
367
+ composed: true,
368
+ detail: { isCap: true, message: errMsg },
369
+ });
370
+ super.dispatchEvent(errEvent);
371
+ this.addEventListener("error", this.boundHandleError);
372
+ this.executeAttributeCode("onerror", errEvent);
373
+ console.error("[cap]", errMsg);
374
+ this.#solving = false;
375
+ return;
376
+ }
377
+
378
+ const [solutions, instrOut] = await Promise.all([powPromise, instrPromise]);
379
+
380
+ if (instrOut?.__timeout || instrOut?.__blocked) {
381
+ this.updateUIBlocked(
382
+ this.getI18nText("error-label", "Error"),
383
+ instrOut && instrOut.__blocked,
384
+ );
385
+ this.#div.setAttribute(
386
+ "aria-label",
387
+ this.getI18nText("error-aria-label", "An error occurred, please try again"),
388
+ );
389
+
390
+ this.removeEventListener("error", this.boundHandleError);
391
+ const errEvent = new CustomEvent("error", {
392
+ bubbles: true,
393
+ composed: true,
394
+ detail: { isCap: true, message: "Instrumentation failed" },
395
+ });
396
+ super.dispatchEvent(errEvent);
397
+ this.addEventListener("error", this.boundHandleError);
398
+
399
+ this.executeAttributeCode("onerror", errEvent);
400
+ console.error("[cap]", "Instrumentation failed");
401
+ this.#solving = false;
402
+ return;
403
+ }
404
+
405
+ const redeemResponse = await capFetch(`${apiEndpoint}redeem`, {
406
+ method: "POST",
407
+ body: JSON.stringify({
408
+ token,
409
+ solutions,
410
+ ...(instrOut && { instr: instrOut }),
411
+ }),
412
+ headers: { "Content-Type": "application/json" },
413
+ });
414
+
415
+ let resp;
416
+ try {
417
+ resp = await redeemResponse.json();
418
+ } catch {
419
+ throw new Error("Failed to parse server response");
420
+ }
421
+
422
+ this.dispatchEvent("progress", { progress: 100 });
423
+ if (!resp.success) throw new Error(resp.error || "Invalid solution");
424
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
425
+ if (this.querySelector(`input[name='${fieldName}']`)) {
426
+ this.querySelector(`input[name='${fieldName}']`).value = resp.token;
427
+ }
428
+
429
+ this.dispatchEvent("solve", { token: resp.token });
430
+ this.token = resp.token;
431
+
432
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
433
+ const expiresIn = new Date(resp.expires).getTime() - Date.now();
434
+ if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
435
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
436
+ } else {
437
+ this.error("Invalid expiration time");
438
+ }
439
+
440
+ this.#div.setAttribute(
441
+ "aria-label",
442
+ this.getI18nText(
443
+ "verified-aria-label",
444
+ "We have verified you're a human, you may now continue",
445
+ ),
446
+ );
447
+
448
+ return { success: true, token: this.token };
449
+ } catch (err) {
450
+ this.#div.setAttribute(
451
+ "aria-label",
452
+ this.getI18nText("error-aria-label", "An error occurred, please try again"),
453
+ );
454
+ this.error(err.message);
455
+ throw err;
456
+ }
457
+ } finally {
458
+ this.#solving = false;
459
+ }
460
+ }
461
+
462
+ async solveChallenges(challenges) {
463
+ const total = challenges.length;
464
+ let completed = 0;
465
+
466
+ let wasmModule = null;
467
+ const wasmSupported =
468
+ typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
469
+
470
+ if (wasmSupported) {
471
+ try {
472
+ wasmModule = await getWasmModule();
473
+ } catch (e) {
474
+ console.warn("[cap] wasm unavailable, falling back to JS solver:", e);
475
+ }
476
+ }
477
+
478
+ if (!wasmSupported) {
479
+ if (!this.#shadow.querySelector(".warning")) {
480
+ const warningEl = document.createElement("div");
481
+ warningEl.className = "warning";
482
+ 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;`;
483
+ warningEl.innerText = this.getI18nText(
484
+ "wasm-disabled",
485
+ "Enable WASM for significantly faster solving",
486
+ );
487
+ this.#shadow.insertBefore(warningEl, this.#shadow.firstChild);
488
+ setTimeout(() => {
489
+ warningEl.style.marginBottom = `calc(-1 * var(--cap-border-radius, 14px))`;
490
+ warningEl.style.opacity = 1;
491
+ }, 10);
492
+ }
493
+ }
494
+
495
+ const workers = Array(this.#workersCount)
496
+ .fill(null)
497
+ .map(() => {
498
+ try {
499
+ return new Worker(this.#workerUrl);
500
+ } catch (error) {
501
+ console.error("[cap] Failed to create worker:", error);
502
+ throw new Error("Worker creation failed");
503
+ }
504
+ });
505
+
506
+ const solveSingleChallenge = ([salt, target], workerId) =>
507
+ new Promise((resolve, reject) => {
508
+ const worker = workers[workerId];
509
+ if (!worker) {
510
+ reject(new Error("Worker not available"));
511
+ return;
512
+ }
513
+
514
+ worker.onmessage = ({ data }) => {
515
+ if (!data.found) return;
516
+
517
+ completed++;
518
+ this.dispatchEvent("progress", {
519
+ progress: Math.round((completed / total) * 100),
520
+ });
521
+
522
+ resolve(data.nonce);
523
+ };
524
+
525
+ worker.onerror = (err) => {
526
+ this.error(`Error in worker: ${err.message || err}`);
527
+ reject(err);
528
+ };
529
+
530
+ if (wasmModule) {
531
+ worker.postMessage({ salt, target, wasmModule }, []);
532
+ } else {
533
+ worker.postMessage({ salt, target });
534
+ }
535
+ });
536
+
537
+ const results = [];
538
+ try {
539
+ for (let i = 0; i < challenges.length; i += this.#workersCount) {
540
+ const chunk = challenges.slice(i, Math.min(i + this.#workersCount, challenges.length));
541
+ const chunkResults = await Promise.all(
542
+ chunk.map((c, idx) => solveSingleChallenge(c, idx)),
543
+ );
544
+ results.push(...chunkResults);
545
+ }
546
+ } finally {
547
+ workers.forEach((w) => {
548
+ if (w) {
549
+ try {
550
+ w.terminate();
551
+ } catch (error) {
552
+ console.error("[cap] error terminating worker:", error);
553
+ }
554
+ }
555
+ });
556
+ }
557
+
558
+ return results;
559
+ }
560
+
561
+ setWorkersCount(workers) {
562
+ const parsedWorkers = parseInt(workers, 10);
563
+ const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16);
564
+ this.#workersCount =
565
+ !Number.isNaN(parsedWorkers) && parsedWorkers > 0 && parsedWorkers <= maxWorkers
566
+ ? parsedWorkers
567
+ : navigator.hardwareConcurrency || 8;
568
+ }
569
+
570
+ createUI() {
571
+ this.#div.classList.add("captcha");
572
+ this.#div.setAttribute("role", "button");
573
+ this.#div.setAttribute("tabindex", "0");
574
+ this.#div.setAttribute(
575
+ "aria-label",
576
+ this.getI18nText("verify-aria-label", "Click to verify you're a human"),
577
+ );
578
+ this.#div.setAttribute("aria-live", "polite");
579
+ this.#div.setAttribute("disabled", "true");
580
+ 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(
581
+ "initial-state",
582
+ "Verify you're human",
583
+ )}</span></p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener" title="Secured by Cap: Self-hosted CAPTCHA for the modern web.">Cap</a>`;
584
+
585
+ this.#shadow.innerHTML = `<style${window.CAP_CSS_NONCE ? ` nonce=${window.CAP_CSS_NONCE}` : ""}>%%capCSS%%</style>`;
586
+
587
+ this.#shadow.appendChild(this.#div);
588
+ }
589
+
590
+ addEventListeners() {
591
+ if (!this.#div) return;
592
+
593
+ this.#div.querySelector("a").addEventListener("click", (e) => {
594
+ e.stopPropagation();
595
+ e.preventDefault();
596
+ window.open("https://capjs.js.org", "_blank");
597
+ });
598
+
599
+ this.#div.addEventListener("click", () => {
600
+ if (!this.#div.hasAttribute("disabled")) this.solve();
601
+ });
602
+
603
+ this.#div.addEventListener("keydown", (e) => {
604
+ if ((e.key === "Enter" || e.key === " ") && !this.#div.hasAttribute("disabled")) {
605
+ e.preventDefault();
606
+ e.stopPropagation();
607
+ this.solve();
608
+ }
609
+ });
610
+
611
+ this.addEventListener("progress", this.boundHandleProgress);
612
+ this.addEventListener("solve", this.boundHandleSolve);
613
+ this.addEventListener("error", this.boundHandleError);
614
+ this.addEventListener("reset", this.boundHandleReset);
615
+ }
616
+
617
+ animateLabel(text) {
618
+ if (!this.#div) return;
619
+ const wrapper = this.#div.querySelector(".label-wrapper");
620
+ if (!wrapper) return;
621
+
622
+ if (prefersReducedMotion()) {
623
+ const current = wrapper.querySelector(".label.active");
624
+ if (current) {
625
+ current.textContent = text;
626
+ } else {
627
+ const span = document.createElement("span");
628
+ span.className = "label active";
629
+ span.textContent = text;
630
+ wrapper.appendChild(span);
631
+ }
632
+ return;
633
+ }
634
+
635
+ const current = wrapper.querySelector(".label.active");
636
+
637
+ const next = document.createElement("span");
638
+ next.className = "label";
639
+ next.textContent = text;
640
+ wrapper.appendChild(next);
641
+
642
+ void next.offsetWidth;
643
+
644
+ next.classList.add("active");
645
+ if (current) {
646
+ current.classList.remove("active");
647
+ current.classList.add("exit");
648
+ current.addEventListener("transitionend", () => current.remove(), { once: true });
649
+ }
650
+ }
651
+
652
+ updateUI(state, text, disabled = false) {
653
+ if (!this.#div) return;
654
+
655
+ this.#div.setAttribute("data-state", state);
656
+
657
+ this.animateLabel(text);
658
+
659
+ if (disabled) {
660
+ this.#div.setAttribute("disabled", "true");
661
+ } else {
662
+ this.#div.removeAttribute("disabled");
663
+ }
664
+ }
665
+
666
+ updateUIBlocked(label, showTroubleshooting = false) {
667
+ if (!this.#div) return;
668
+
669
+ this.#div.setAttribute("data-state", "error");
670
+ this.#div.removeAttribute("disabled");
671
+
672
+ const wrapper = this.#div.querySelector(".label-wrapper");
673
+ if (!wrapper) return;
674
+
675
+ const troubleshootingUrl =
676
+ this.getAttribute("data-cap-troubleshooting-url") ||
677
+ "https://capjs.js.org/guide/troubleshooting/instrumentation.html";
678
+
679
+ const current = wrapper.querySelector(".label.active");
680
+ const next = document.createElement("span");
681
+ next.className = "label";
682
+ next.innerHTML = showTroubleshooting
683
+ ? `${label} · <a class="cap-troubleshoot-link" href="${troubleshootingUrl}" target="_blank" rel="noopener">${this.getI18nText("troubleshooting-label", "Troubleshoot")}</a>`
684
+ : label;
685
+ wrapper.appendChild(next);
686
+
687
+ void next.offsetWidth;
688
+ next.classList.add("active");
689
+ if (current) {
690
+ current.classList.remove("active");
691
+ current.classList.add("exit");
692
+ current.addEventListener("transitionend", () => current.remove(), { once: true });
693
+ }
694
+
695
+ const link = next.querySelector(".cap-troubleshoot-link");
696
+ if (link) {
697
+ link.addEventListener("click", (e) => {
698
+ e.stopPropagation();
699
+ });
700
+ }
701
+ }
702
+
703
+ handleProgress(event) {
704
+ if (!this.#div) return;
705
+
706
+ const progressCircle = this.#div.querySelector(".progress-ring-circle");
707
+
708
+ if (progressCircle) {
709
+ const circumference = 2 * Math.PI * 14;
710
+ const offset = circumference - (event.detail.progress / 100) * circumference;
711
+ progressCircle.style.strokeDashoffset = offset;
712
+ }
713
+
714
+ const wrapper = this.#div.querySelector(".label-wrapper");
715
+ if (wrapper) {
716
+ const activeLabel = wrapper.querySelector(".label.active");
717
+ if (activeLabel) {
718
+ activeLabel.textContent = `${this.getI18nText("verifying-label", "Verifying...")} ${event.detail.progress}%`;
719
+ }
720
+ }
721
+
722
+ this.executeAttributeCode("onprogress", event);
723
+ }
724
+
725
+ handleSolve(event) {
726
+ this.updateUI("done", this.getI18nText("solved-label", "You're a human"), true);
727
+ this.executeAttributeCode("onsolve", event);
728
+ }
729
+
730
+ handleError(event) {
731
+ this.updateUI("error", this.getI18nText("error-label", "Error. Try again."));
732
+ this.executeAttributeCode("onerror", event);
733
+ }
734
+
735
+ handleReset(event) {
736
+ this.updateUI("", this.getI18nText("initial-state", "I'm a human"));
737
+ this.executeAttributeCode("onreset", event);
738
+ }
739
+
740
+ executeAttributeCode(attributeName, event) {
741
+ const code = this.getAttribute(attributeName);
742
+ if (!code) {
743
+ return;
744
+ }
745
+
746
+ console.error("[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.");
747
+
748
+ new Function("event", code).call(this, event);
749
+ }
750
+
751
+ error(message = "Unknown error") {
752
+ console.error("[cap]", message);
753
+ this.dispatchEvent("error", { isCap: true, message });
754
+ }
755
+
756
+ dispatchEvent(eventName, detail = {}) {
757
+ const event = new CustomEvent(eventName, {
758
+ bubbles: true,
759
+ composed: true,
760
+ detail,
761
+ });
762
+ super.dispatchEvent(event);
763
+ }
764
+
765
+ reset() {
766
+ if (this.#resetTimer) {
767
+ clearTimeout(this.#resetTimer);
768
+ this.#resetTimer = null;
769
+ }
770
+ this.dispatchEvent("reset");
771
+ this.token = null;
772
+ const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
773
+ if (this.querySelector(`input[name='${fieldName}']`)) {
774
+ this.querySelector(`input[name='${fieldName}']`).value = "";
775
+ }
776
+ }
777
+
778
+ get tokenValue() {
779
+ return this.token;
780
+ }
781
+
782
+ disconnectedCallback() {
783
+ this.removeEventListener("progress", this.boundHandleProgress);
784
+ this.removeEventListener("solve", this.boundHandleSolve);
785
+ this.removeEventListener("error", this.boundHandleError);
786
+ this.removeEventListener("reset", this.boundHandleReset);
787
+
788
+ this.#eventHandlers.forEach((handler, eventName) => {
789
+ this.removeEventListener(eventName.slice(2), handler);
790
+ });
791
+ this.#eventHandlers.clear();
792
+
793
+ if (this.#shadow) {
794
+ this.#shadow.innerHTML = "";
795
+ }
796
+
797
+ this.reset();
798
+ this.cleanup();
799
+ }
800
+
801
+ cleanup() {
802
+ if (this.#resetTimer) {
803
+ clearTimeout(this.#resetTimer);
804
+ this.#resetTimer = null;
805
+ }
806
+
807
+ if (this.#workerUrl) {
808
+ URL.revokeObjectURL(this.#workerUrl);
809
+ this.#workerUrl = "";
810
+ }
811
+ }
812
+ }
813
+
814
+ class Cap {
815
+ constructor(config = {}, el) {
816
+ const widget = el || document.createElement("cap-widget");
817
+
818
+ Object.entries(config).forEach(([a, b]) => {
819
+ widget.setAttribute(a, b);
820
+ });
821
+
822
+ if (!config.apiEndpoint && !window?.CAP_CUSTOM_FETCH) {
823
+ widget.remove();
824
+ throw new Error(
825
+ "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
826
+ );
827
+ }
828
+
829
+ if (config.apiEndpoint) {
830
+ widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint);
831
+ }
832
+
833
+ this.widget = widget;
834
+ this.solve = this.widget.solve.bind(this.widget);
835
+ this.reset = this.widget.reset.bind(this.widget);
836
+ this.addEventListener = this.widget.addEventListener.bind(this.widget);
837
+
838
+ Object.defineProperty(this, "token", {
839
+ get: () => widget.token,
840
+ configurable: true,
841
+ enumerable: true,
842
+ });
843
+
844
+ if (!el) {
845
+ widget.style.display = "none";
846
+ document.documentElement.appendChild(widget);
847
+ }
848
+ }
849
+ }
850
+
851
+ window.Cap = Cap;
852
+
853
+ if (!customElements.get("cap-widget") && !window?.CAP_DONT_SKIP_REDEFINE) {
854
+ customElements.define("cap-widget", CapWidget);
855
+ } else if (customElements.get("cap-widget")) {
856
+ console.warn(
857
+ "[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true",
858
+ );
859
+ }
860
+
861
+ if (typeof exports === "object" && typeof module !== "undefined") {
862
+ module.exports = Cap;
863
+ } else if (typeof define === "function" && define.amd) {
864
+ define([], () => Cap);
865
+ }
866
+
867
+ if (typeof exports !== "undefined") {
868
+ exports.default = Cap;
869
+ }
613
870
  })();