@cap.js/widget 0.1.25 → 0.1.27

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,600 +1,604 @@
1
- (function () {
2
- const WASM_VERSION = "0.0.6";
3
-
4
- const capFetch = function () {
5
- if (window?.CAP_CUSTOM_FETCH) {
6
- return window.CAP_CUSTOM_FETCH(...arguments);
7
- }
8
- return fetch(...arguments);
9
- };
10
-
11
- if (!window.CAP_CUSTOM_WASM_URL) {
12
- // preloads the wasm files to save up time on solve
13
- [
14
- `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
15
- `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`,
16
- ].forEach((url) => {
17
- const link = document.createElement("link");
18
- link.rel = "prefetch";
19
- link.href = url;
20
- link.as = url.endsWith(".wasm") ? "fetch" : "script";
21
- document.head.appendChild(link);
22
- });
23
- }
24
-
25
- class CapWidget extends HTMLElement {
26
- #workerUrl = "";
27
- #resetTimer = null;
28
- #workersCount = navigator.hardwareConcurrency || 8;
29
- token = null;
30
- #shadow;
31
- #div;
32
- #host;
33
- #solving = false;
34
- #eventHandlers;
35
-
36
- getI18nText(key, defaultValue) {
37
- return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue;
38
- }
39
-
40
- static get observedAttributes() {
41
- return [
42
- "onsolve",
43
- "onprogress",
44
- "onreset",
45
- "onerror",
46
- "data-cap-worker-count",
47
- "data-cap-i18n-initial-state",
48
- "[cap]",
49
- ];
50
- }
51
-
52
- constructor() {
53
- super();
54
- if (this.#eventHandlers) {
55
- this.#eventHandlers.forEach((handler, eventName) => {
56
- this.removeEventListener(eventName.slice(2), handler);
57
- });
58
- }
59
-
60
- this.#eventHandlers = new Map();
61
- this.boundHandleProgress = this.handleProgress.bind(this);
62
- this.boundHandleSolve = this.handleSolve.bind(this);
63
- this.boundHandleError = this.handleError.bind(this);
64
- this.boundHandleReset = this.handleReset.bind(this);
65
- }
66
-
67
- initialize() {
68
- this.#workerUrl = URL.createObjectURL(
69
- // MARK: worker injection
70
- // this placeholder will be replaced with the actual worker by the build script
71
-
72
- new Blob([`%%workerScript%%`], {
73
- type: "application/javascript",
74
- })
75
- );
76
- }
77
-
78
- attributeChangedCallback(name, _, value) {
79
- if (name.startsWith("on")) {
80
- const eventName = name.slice(2);
81
- const oldHandler = this.#eventHandlers.get(name);
82
- if (oldHandler) {
83
- this.removeEventListener(eventName, oldHandler);
84
- }
85
-
86
- if (value) {
87
- const handler = (event) => {
88
- const callback = this.getAttribute(name);
89
- if (typeof window[callback] === "function") {
90
- window[callback].call(this, event);
91
- }
92
- };
93
- this.#eventHandlers.set(name, handler);
94
- this.addEventListener(eventName, handler);
95
- }
96
- }
97
-
98
- if (name === "data-cap-worker-count") {
99
- this.setWorkersCount(parseInt(value));
100
- }
101
-
102
- if (
103
- name === "data-cap-i18n-initial-state" &&
104
- this.#div &&
105
- this.#div?.querySelector("p")?.innerText
106
- ) {
107
- this.#div.querySelector("p").innerText = this.getI18nText(
108
- "initial-state",
109
- "I'm a human"
110
- );
111
- }
112
- }
113
-
114
- async connectedCallback() {
115
- this.#host = this;
116
- this.#shadow = this.attachShadow({ mode: "open" });
117
- this.#div = document.createElement("div");
118
- this.createUI();
119
- this.addEventListeners();
120
- await this.initialize();
121
- this.#div.removeAttribute("disabled");
122
-
123
- const workers = this.getAttribute("data-cap-worker-count");
124
- const parsedWorkers = workers ? parseInt(workers, 10) : null;
125
- this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
126
- const fieldName =
127
- this.getAttribute("data-cap-hidden-field-name") || "cap-token";
128
- this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
129
- }
130
-
131
- async solve() {
132
- if (this.#solving) {
133
- return;
134
- }
135
-
136
- try {
137
- this.#solving = true;
138
- this.updateUI(
139
- "verifying",
140
- this.getI18nText("verifying-label", "Verifying..."),
141
- true
142
- );
143
-
144
- this.#div.setAttribute(
145
- "aria-label",
146
- this.getI18nText(
147
- "verifying-aria-label",
148
- "Verifying you're a human, please wait"
149
- )
150
- );
151
-
152
- this.dispatchEvent("progress", { progress: 0 });
153
-
154
- try {
155
- const apiEndpoint = this.getAttribute("data-cap-api-endpoint");
156
- if (!apiEndpoint) throw new Error("Missing API endpoint");
157
-
158
- const { challenge, token } = await (
159
- await capFetch(`${apiEndpoint}challenge`, {
160
- method: "POST",
161
- })
162
- ).json();
163
-
164
- let challenges = challenge;
165
-
166
- if (!Array.isArray(challenges)) {
167
- function prng(seed, length) {
168
- function fnv1a(str) {
169
- let hash = 2166136261;
170
- for (let i = 0; i < str.length; i++) {
171
- hash ^= str.charCodeAt(i);
172
- hash +=
173
- (hash << 1) +
174
- (hash << 4) +
175
- (hash << 7) +
176
- (hash << 8) +
177
- (hash << 24);
178
- }
179
- return hash >>> 0;
180
- }
181
-
182
- let state = fnv1a(seed);
183
- let result = "";
184
-
185
- function next() {
186
- state ^= state << 13;
187
- state ^= state >>> 17;
188
- state ^= state << 5;
189
- return state >>> 0;
190
- }
191
-
192
- while (result.length < length) {
193
- const rnd = next();
194
- result += rnd.toString(16).padStart(8, "0");
195
- }
196
-
197
- return result.substring(0, length);
198
- }
199
-
200
- let i = 0;
201
-
202
- challenges = Array.from({ length: challenge.c }, () => {
203
- i = i + 1;
204
-
205
- return [
206
- prng(`${token}${i}`, challenge.s),
207
- prng(`${token}${i}d`, challenge.d),
208
- ];
209
- });
210
- }
211
-
212
- const solutions = await this.solveChallenges(challenges);
213
-
214
- const resp = await (
215
- await capFetch(`${apiEndpoint}redeem`, {
216
- method: "POST",
217
- body: JSON.stringify({ token, solutions }),
218
- headers: { "Content-Type": "application/json" },
219
- })
220
- ).json();
221
-
222
- this.dispatchEvent("progress", { progress: 100 });
223
-
224
- if (!resp.success) throw new Error("Invalid solution");
225
- const fieldName =
226
- this.getAttribute("data-cap-hidden-field-name") || "cap-token";
227
- if (this.querySelector(`input[name='${fieldName}']`)) {
228
- this.querySelector(`input[name='${fieldName}']`).value = resp.token;
229
- }
230
-
231
- this.dispatchEvent("solve", { token: resp.token });
232
- this.token = resp.token;
233
-
234
- if (this.#resetTimer) clearTimeout(this.#resetTimer);
235
- const expiresIn = new Date(resp.expires).getTime() - Date.now();
236
- if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
237
- this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
238
- } else {
239
- this.error("Invalid expiration time");
240
- }
241
-
242
- this.#div.setAttribute(
243
- "aria-label",
244
- this.getI18nText(
245
- "verified-aria-label",
246
- "We have verified you're a human, you may now continue"
247
- )
248
- );
249
-
250
- return { success: true, token: this.token };
251
- } catch (err) {
252
- this.#div.setAttribute(
253
- "aria-label",
254
- this.getI18nText(
255
- "error-aria-label",
256
- "An error occurred, please try again"
257
- )
258
- );
259
- this.error(err.message);
260
- throw err;
261
- }
262
- } finally {
263
- this.#solving = false;
264
- }
265
- }
266
-
267
- async solveChallenges(challenge) {
268
- const total = challenge.length;
269
- let completed = 0;
270
-
271
- const workers = Array(this.#workersCount)
272
- .fill(null)
273
- .map(() => {
274
- try {
275
- return new Worker(this.#workerUrl);
276
- } catch (error) {
277
- console.error("[cap] Failed to create worker:", error);
278
- throw new Error("Worker creation failed");
279
- }
280
- });
281
-
282
- const solveSingleChallenge = ([salt, target], workerId) =>
283
- new Promise((resolve, reject) => {
284
- const worker = workers[workerId];
285
- if (!worker) {
286
- reject(new Error("Worker not available"));
287
- return;
288
- }
289
-
290
- const timeout = setTimeout(() => {
291
- try {
292
- worker.terminate();
293
- workers[workerId] = new Worker(this.#workerUrl);
294
- } catch (error) {
295
- console.error(
296
- "[cap] error terminating/recreating worker:",
297
- error
298
- );
299
- }
300
- reject(new Error("Worker timeout"));
301
- }, 30000);
302
-
303
- worker.onmessage = ({ data }) => {
304
- if (!data.found) return;
305
- clearTimeout(timeout);
306
- completed++;
307
- this.dispatchEvent("progress", {
308
- progress: Math.round((completed / total) * 100),
309
- });
310
-
311
- resolve(data.nonce);
312
- };
313
-
314
- worker.onerror = (err) => {
315
- clearTimeout(timeout);
316
- this.error(`Error in worker: ${err.message || err}`);
317
- reject(err);
318
- };
319
-
320
- worker.postMessage({
321
- salt,
322
- target,
323
- wasmUrl:
324
- window.CAP_CUSTOM_WASM_URL ||
325
- `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
326
- });
327
- });
328
-
329
- const results = [];
330
- try {
331
- for (let i = 0; i < challenge.length; i += this.#workersCount) {
332
- const chunk = challenge.slice(
333
- i,
334
- Math.min(i + this.#workersCount, challenge.length)
335
- );
336
- const chunkResults = await Promise.all(
337
- chunk.map((c, idx) => solveSingleChallenge(c, idx))
338
- );
339
- results.push(...chunkResults);
340
- }
341
- } finally {
342
- workers.forEach((w) => {
343
- if (w) {
344
- try {
345
- w.terminate();
346
- } catch (error) {
347
- console.error("[cap] error terminating worker:", error);
348
- }
349
- }
350
- });
351
- }
352
-
353
- return results;
354
- }
355
-
356
- setWorkersCount(workers) {
357
- const parsedWorkers = parseInt(workers, 10);
358
- const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16);
359
- this.#workersCount =
360
- !isNaN(parsedWorkers) &&
361
- parsedWorkers > 0 &&
362
- parsedWorkers <= maxWorkers
363
- ? parsedWorkers
364
- : navigator.hardwareConcurrency || 8;
365
- }
366
-
367
- createUI() {
368
- this.#div.classList.add("captcha");
369
- this.#div.setAttribute("role", "button");
370
- this.#div.setAttribute("tabindex", "0");
371
- this.#div.setAttribute(
372
- "aria-label",
373
- this.getI18nText("verify-aria-label", "Click to verify you're a human")
374
- );
375
- this.#div.setAttribute("aria-live", "polite");
376
- this.#div.setAttribute("disabled", "true");
377
- this.#div.innerHTML = `<div class="checkbox" part="checkbox"></div><p part="label">${this.getI18nText(
378
- "initial-state",
379
- "I'm a human"
380
- )}</p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener">Cap</a>`;
381
-
382
- this.#shadow.innerHTML = `<style>.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, 230px);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}.captcha[data-state=verifying]
1
+ (() => {
2
+ const WASM_VERSION = "0.0.6";
3
+
4
+ const capFetch = (...args) => {
5
+ if (window?.CAP_CUSTOM_FETCH) {
6
+ return window.CAP_CUSTOM_FETCH(...args);
7
+ }
8
+ return fetch(...args);
9
+ };
10
+
11
+ function prng(seed, length) {
12
+ function fnv1a(str) {
13
+ let hash = 2166136261;
14
+ for (let i = 0; i < str.length; i++) {
15
+ hash ^= str.charCodeAt(i);
16
+ hash +=
17
+ (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
18
+ }
19
+ return hash >>> 0;
20
+ }
21
+
22
+ let state = fnv1a(seed);
23
+ let result = "";
24
+
25
+ function next() {
26
+ state ^= state << 13;
27
+ state ^= state >>> 17;
28
+ state ^= state << 5;
29
+ return state >>> 0;
30
+ }
31
+
32
+ while (result.length < length) {
33
+ const rnd = next();
34
+ result += rnd.toString(16).padStart(8, "0");
35
+ }
36
+
37
+ return result.substring(0, length);
38
+ }
39
+
40
+ if (!window.CAP_CUSTOM_WASM_URL) {
41
+ // preloads the wasm files to save up time on solve
42
+ [
43
+ `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
44
+ `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm_bg.wasm`,
45
+ ].forEach((url) => {
46
+ const link = document.createElement("link");
47
+ link.rel = "prefetch";
48
+ link.href = url;
49
+ link.as = url.endsWith(".wasm") ? "fetch" : "script";
50
+ document.head.appendChild(link);
51
+ });
52
+ }
53
+
54
+ class CapWidget extends HTMLElement {
55
+ #workerUrl = "";
56
+ #resetTimer = null;
57
+ #workersCount = navigator.hardwareConcurrency || 8;
58
+ token = null;
59
+ #shadow;
60
+ #div;
61
+ #host;
62
+ #solving = false;
63
+ #eventHandlers;
64
+
65
+ getI18nText(key, defaultValue) {
66
+ return this.getAttribute(`data-cap-i18n-${key}`) || defaultValue;
67
+ }
68
+
69
+ static get observedAttributes() {
70
+ return [
71
+ "onsolve",
72
+ "onprogress",
73
+ "onreset",
74
+ "onerror",
75
+ "data-cap-worker-count",
76
+ "data-cap-i18n-initial-state",
77
+ "[cap]",
78
+ ];
79
+ }
80
+
81
+ constructor() {
82
+ super();
83
+ if (this.#eventHandlers) {
84
+ this.#eventHandlers.forEach((handler, eventName) => {
85
+ this.removeEventListener(eventName.slice(2), handler);
86
+ });
87
+ }
88
+
89
+ this.#eventHandlers = new Map();
90
+ this.boundHandleProgress = this.handleProgress.bind(this);
91
+ this.boundHandleSolve = this.handleSolve.bind(this);
92
+ this.boundHandleError = this.handleError.bind(this);
93
+ this.boundHandleReset = this.handleReset.bind(this);
94
+ }
95
+
96
+ initialize() {
97
+ this.#workerUrl = URL.createObjectURL(
98
+ // MARK: worker injection
99
+ // this placeholder will be replaced with the actual worker by the build script
100
+
101
+ new Blob([`%%workerScript%%`], {
102
+ type: "application/javascript",
103
+ }),
104
+ );
105
+ }
106
+
107
+ attributeChangedCallback(name, _, value) {
108
+ if (name.startsWith("on")) {
109
+ const eventName = name.slice(2);
110
+ const oldHandler = this.#eventHandlers.get(name);
111
+ if (oldHandler) {
112
+ this.removeEventListener(eventName, oldHandler);
113
+ }
114
+
115
+ if (value) {
116
+ const handler = (event) => {
117
+ const callback = this.getAttribute(name);
118
+ if (typeof window[callback] === "function") {
119
+ window[callback].call(this, event);
120
+ }
121
+ };
122
+ this.#eventHandlers.set(name, handler);
123
+ this.addEventListener(eventName, handler);
124
+ }
125
+ }
126
+
127
+ if (name === "data-cap-worker-count") {
128
+ this.setWorkersCount(parseInt(value));
129
+ }
130
+
131
+ if (
132
+ name === "data-cap-i18n-initial-state" &&
133
+ this.#div &&
134
+ this.#div?.querySelector("p")?.innerText
135
+ ) {
136
+ this.#div.querySelector("p").innerText = this.getI18nText(
137
+ "initial-state",
138
+ "I'm a human",
139
+ );
140
+ }
141
+ }
142
+
143
+ async connectedCallback() {
144
+ this.#host = this;
145
+ this.#shadow = this.attachShadow({ mode: "open" });
146
+ this.#div = document.createElement("div");
147
+ this.createUI();
148
+ this.addEventListeners();
149
+ await this.initialize();
150
+ this.#div.removeAttribute("disabled");
151
+
152
+ const workers = this.getAttribute("data-cap-worker-count");
153
+ const parsedWorkers = workers ? parseInt(workers, 10) : null;
154
+ this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
155
+ const fieldName =
156
+ this.getAttribute("data-cap-hidden-field-name") || "cap-token";
157
+ this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
158
+ }
159
+
160
+ async solve() {
161
+ if (this.#solving) {
162
+ return;
163
+ }
164
+
165
+ try {
166
+ this.#solving = true;
167
+ this.updateUI(
168
+ "verifying",
169
+ this.getI18nText("verifying-label", "Verifying..."),
170
+ true,
171
+ );
172
+
173
+ this.#div.setAttribute(
174
+ "aria-label",
175
+ this.getI18nText(
176
+ "verifying-aria-label",
177
+ "Verifying you're a human, please wait",
178
+ ),
179
+ );
180
+
181
+ this.dispatchEvent("progress", { progress: 0 });
182
+
183
+ try {
184
+ let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
185
+
186
+ if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
187
+ apiEndpoint = "/";
188
+ } else if (!apiEndpoint)
189
+ throw new Error(
190
+ "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
191
+ );
192
+
193
+ const { challenge, token } = await (
194
+ await capFetch(`${apiEndpoint}challenge`, {
195
+ method: "POST",
196
+ })
197
+ ).json();
198
+
199
+ let challenges = challenge;
200
+
201
+ if (!Array.isArray(challenges)) {
202
+ let i = 0;
203
+
204
+ challenges = Array.from({ length: challenge.c }, () => {
205
+ i = i + 1;
206
+
207
+ return [
208
+ prng(`${token}${i}`, challenge.s),
209
+ prng(`${token}${i}d`, challenge.d),
210
+ ];
211
+ });
212
+ }
213
+
214
+ const solutions = await this.solveChallenges(challenges);
215
+
216
+ const resp = await (
217
+ await capFetch(`${apiEndpoint}redeem`, {
218
+ method: "POST",
219
+ body: JSON.stringify({ token, solutions }),
220
+ headers: { "Content-Type": "application/json" },
221
+ })
222
+ ).json();
223
+
224
+ this.dispatchEvent("progress", { progress: 100 });
225
+
226
+ if (!resp.success) throw new Error("Invalid solution");
227
+ const fieldName =
228
+ this.getAttribute("data-cap-hidden-field-name") || "cap-token";
229
+ if (this.querySelector(`input[name='${fieldName}']`)) {
230
+ this.querySelector(`input[name='${fieldName}']`).value = resp.token;
231
+ }
232
+
233
+ this.dispatchEvent("solve", { token: resp.token });
234
+ this.token = resp.token;
235
+
236
+ if (this.#resetTimer) clearTimeout(this.#resetTimer);
237
+ const expiresIn = new Date(resp.expires).getTime() - Date.now();
238
+ if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
239
+ this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
240
+ } else {
241
+ this.error("Invalid expiration time");
242
+ }
243
+
244
+ this.#div.setAttribute(
245
+ "aria-label",
246
+ this.getI18nText(
247
+ "verified-aria-label",
248
+ "We have verified you're a human, you may now continue",
249
+ ),
250
+ );
251
+
252
+ return { success: true, token: this.token };
253
+ } catch (err) {
254
+ this.#div.setAttribute(
255
+ "aria-label",
256
+ this.getI18nText(
257
+ "error-aria-label",
258
+ "An error occurred, please try again",
259
+ ),
260
+ );
261
+ this.error(err.message);
262
+ throw err;
263
+ }
264
+ } finally {
265
+ this.#solving = false;
266
+ }
267
+ }
268
+
269
+ async solveChallenges(challenge) {
270
+ const total = challenge.length;
271
+ let completed = 0;
272
+
273
+ const workers = Array(this.#workersCount)
274
+ .fill(null)
275
+ .map(() => {
276
+ try {
277
+ return new Worker(this.#workerUrl);
278
+ } catch (error) {
279
+ console.error("[cap] Failed to create worker:", error);
280
+ throw new Error("Worker creation failed");
281
+ }
282
+ });
283
+
284
+ const solveSingleChallenge = ([salt, target], workerId) =>
285
+ new Promise((resolve, reject) => {
286
+ const worker = workers[workerId];
287
+ if (!worker) {
288
+ reject(new Error("Worker not available"));
289
+ return;
290
+ }
291
+
292
+ const timeout = setTimeout(() => {
293
+ try {
294
+ worker.terminate();
295
+ workers[workerId] = new Worker(this.#workerUrl);
296
+ } catch (error) {
297
+ console.error(
298
+ "[cap] error terminating/recreating worker:",
299
+ error,
300
+ );
301
+ }
302
+ reject(new Error("Worker timeout"));
303
+ }, 30000);
304
+
305
+ worker.onmessage = ({ data }) => {
306
+ if (!data.found) return;
307
+ clearTimeout(timeout);
308
+ completed++;
309
+ this.dispatchEvent("progress", {
310
+ progress: Math.round((completed / total) * 100),
311
+ });
312
+
313
+ resolve(data.nonce);
314
+ };
315
+
316
+ worker.onerror = (err) => {
317
+ clearTimeout(timeout);
318
+ this.error(`Error in worker: ${err.message || err}`);
319
+ reject(err);
320
+ };
321
+
322
+ worker.postMessage({
323
+ salt,
324
+ target,
325
+ wasmUrl:
326
+ window.CAP_CUSTOM_WASM_URL ||
327
+ `https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/cap_wasm.min.js`,
328
+ });
329
+ });
330
+
331
+ const results = [];
332
+ try {
333
+ for (let i = 0; i < challenge.length; i += this.#workersCount) {
334
+ const chunk = challenge.slice(
335
+ i,
336
+ Math.min(i + this.#workersCount, challenge.length),
337
+ );
338
+ const chunkResults = await Promise.all(
339
+ chunk.map((c, idx) => solveSingleChallenge(c, idx)),
340
+ );
341
+ results.push(...chunkResults);
342
+ }
343
+ } finally {
344
+ workers.forEach((w) => {
345
+ if (w) {
346
+ try {
347
+ w.terminate();
348
+ } catch (error) {
349
+ console.error("[cap] error terminating worker:", error);
350
+ }
351
+ }
352
+ });
353
+ }
354
+
355
+ return results;
356
+ }
357
+
358
+ setWorkersCount(workers) {
359
+ const parsedWorkers = parseInt(workers, 10);
360
+ const maxWorkers = Math.min(navigator.hardwareConcurrency || 8, 16);
361
+ this.#workersCount =
362
+ !Number.isNaN(parsedWorkers) &&
363
+ parsedWorkers > 0 &&
364
+ parsedWorkers <= maxWorkers
365
+ ? parsedWorkers
366
+ : navigator.hardwareConcurrency || 8;
367
+ }
368
+
369
+ createUI() {
370
+ this.#div.classList.add("captcha");
371
+ this.#div.setAttribute("role", "button");
372
+ this.#div.setAttribute("tabindex", "0");
373
+ this.#div.setAttribute(
374
+ "aria-label",
375
+ this.getI18nText("verify-aria-label", "Click to verify you're a human"),
376
+ );
377
+ this.#div.setAttribute("aria-live", "polite");
378
+ this.#div.setAttribute("disabled", "true");
379
+ this.#div.innerHTML = `<div class="checkbox" part="checkbox"></div><p part="label">${this.getI18nText(
380
+ "initial-state",
381
+ "I'm a human",
382
+ )}</p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener">Cap</a>`;
383
+
384
+ 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, 230px);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}.captcha[data-state=verifying]
383
385
  .checkbox{background: none;display:flex;align-items:center;justify-content:center;transform: scale(1.1);border: none;border-radius: 50%;background: conic-gradient(var(--cap-spinner-color,#000) 0%, var(--cap-spinner-color,#000) var(--progress, 0%), var(--cap-spinner-background-color,#eee) var(--progress, 0%), var(--cap-spinner-background-color,#eee) 100%);position: relative;}.captcha[data-state=verifying] .checkbox::after {content: "";background-color: var(--cap-background,#fdfdfd);width: calc(100% - var(--cap-spinner-thickness,5px));height: calc(100% - var(--cap-spinner-thickness,5px));border-radius: 50%;margin:calc(var(--cap-spinner-thickness,5px) / 2)}.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=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[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:var(--cap-credits-font-size,12px);color:var(--cap-color,#212121);opacity:var(--cap-opacity-hover,0.8);text-underline-offset: 1.5px;}</style>`;
384
386
 
385
- this.#shadow.appendChild(this.#div);
386
- }
387
-
388
- addEventListeners() {
389
- if (!this.#div) return;
390
-
391
- this.#div.querySelector("a").addEventListener("click", (e) => {
392
- e.stopPropagation();
393
- e.preventDefault();
394
- window.open("https://capjs.js.org", "_blank");
395
- });
396
-
397
- this.#div.addEventListener("click", () => {
398
- if (!this.#div.hasAttribute("disabled")) this.solve();
399
- });
400
-
401
- this.#div.addEventListener("keydown", (e) => {
402
- if (
403
- (e.key === "Enter" || e.key === " ") &&
404
- !this.#div.hasAttribute("disabled")
405
- ) {
406
- e.preventDefault();
407
- e.stopPropagation();
408
- this.solve();
409
- }
410
- });
411
-
412
- this.addEventListener("progress", this.boundHandleProgress);
413
- this.addEventListener("solve", this.boundHandleSolve);
414
- this.addEventListener("error", this.boundHandleError);
415
- this.addEventListener("reset", this.boundHandleReset);
416
- }
417
-
418
- updateUI(state, text, disabled = false) {
419
- if (!this.#div) return;
420
-
421
- this.#div.setAttribute("data-state", state);
422
-
423
- this.#div.querySelector("p").innerText = text;
424
-
425
- if (disabled) {
426
- this.#div.setAttribute("disabled", "true");
427
- } else {
428
- this.#div.removeAttribute("disabled");
429
- }
430
- }
431
-
432
- handleProgress(event) {
433
- if (!this.#div) return;
434
-
435
- const progressElement = this.#div.querySelector("p");
436
- const checkboxElement = this.#div.querySelector(".checkbox");
437
-
438
- if (progressElement && checkboxElement) {
439
- checkboxElement.style.setProperty(
440
- "--progress",
441
- `${event.detail.progress}%`
442
- );
443
- progressElement.innerText = `${this.getI18nText(
444
- "verifying-label",
445
- "Verifying..."
446
- )} ${event.detail.progress}%`;
447
- }
448
- this.executeAttributeCode("onprogress", event);
449
- }
450
-
451
- handleSolve(event) {
452
- this.updateUI(
453
- "done",
454
- this.getI18nText("solved-label", "You're a human"),
455
- true
456
- );
457
- this.executeAttributeCode("onsolve", event);
458
- }
459
-
460
- handleError(event) {
461
- this.updateUI(
462
- "error",
463
- this.getI18nText("error-label", "Error. Try again.")
464
- );
465
- this.executeAttributeCode("onerror", event);
466
- }
467
-
468
- handleReset(event) {
469
- this.updateUI("", this.getI18nText("initial-state", "I'm a human"));
470
- this.executeAttributeCode("onreset", event);
471
- }
472
-
473
- executeAttributeCode(attributeName, event) {
474
- const code = this.getAttribute(attributeName);
475
- if (!code) {
476
- return;
477
- }
478
-
479
- new Function("event", code).call(this, event);
480
- }
481
-
482
- error(message = "Unknown error") {
483
- console.error("[cap]", message);
484
- this.dispatchEvent("error", { isCap: true, message });
485
- }
486
-
487
- dispatchEvent(eventName, detail = {}) {
488
- const event = new CustomEvent(eventName, {
489
- bubbles: true,
490
- composed: true,
491
- detail,
492
- });
493
- super.dispatchEvent(event);
494
- }
495
-
496
- reset() {
497
- if (this.#resetTimer) {
498
- clearTimeout(this.#resetTimer);
499
- this.#resetTimer = null;
500
- }
501
- this.dispatchEvent("reset");
502
- this.token = null;
503
- const fieldName =
504
- this.getAttribute("data-cap-hidden-field-name") || "cap-token";
505
- if (this.querySelector(`input[name='${fieldName}']`)) {
506
- this.querySelector(`input[name='${fieldName}']`).value = "";
507
- }
508
- }
509
-
510
- get tokenValue() {
511
- return this.token;
512
- }
513
-
514
- disconnectedCallback() {
515
- this.removeEventListener("progress", this.boundHandleProgress);
516
- this.removeEventListener("solve", this.boundHandleSolve);
517
- this.removeEventListener("error", this.boundHandleError);
518
- this.removeEventListener("reset", this.boundHandleReset);
519
-
520
- this.#eventHandlers.forEach((handler, eventName) => {
521
- this.removeEventListener(eventName.slice(2), handler);
522
- });
523
- this.#eventHandlers.clear();
524
-
525
- if (this.#shadow) {
526
- this.#shadow.innerHTML = "";
527
- }
528
-
529
- this.reset();
530
- this.cleanup();
531
- }
532
-
533
- cleanup() {
534
- if (this.#resetTimer) {
535
- clearTimeout(this.#resetTimer);
536
- this.#resetTimer = null;
537
- }
538
-
539
- if (this.#workerUrl) {
540
- URL.revokeObjectURL(this.#workerUrl);
541
- this.#workerUrl = "";
542
- }
543
- }
544
- }
545
-
546
- // MARK: Invisible
547
- class Cap {
548
- constructor(config = {}, el) {
549
- let widget = el || document.createElement("cap-widget");
550
-
551
- Object.entries(config).forEach(([a, b]) => {
552
- widget.setAttribute(a, b);
553
- });
554
-
555
- if (config.apiEndpoint) {
556
- widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint);
557
- } else {
558
- widget.remove();
559
- throw new Error("Missing API endpoint");
560
- }
561
-
562
- this.widget = widget;
563
- this.solve = this.widget.solve.bind(this.widget);
564
- this.reset = this.widget.reset.bind(this.widget);
565
- this.addEventListener = this.widget.addEventListener.bind(this.widget);
566
-
567
- Object.defineProperty(this, "token", {
568
- get: () => widget.token,
569
- configurable: true,
570
- enumerable: true,
571
- });
572
-
573
- if (!el) {
574
- widget.style.display = "none";
575
- document.documentElement.appendChild(widget);
576
- }
577
- }
578
- }
579
- window.Cap = Cap;
580
-
581
- if (!customElements.get("cap-widget") && !window?.CAP_DONT_SKIP_REDEFINE) {
582
- customElements.define("cap-widget", CapWidget);
583
- } else {
584
- console.warn(
585
- "[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true"
586
- );
587
- }
588
-
589
- if (typeof exports === "object" && typeof module !== "undefined") {
590
- module.exports = Cap;
591
- } else if (typeof define === "function" && define.amd) {
592
- define([], function () {
593
- return Cap;
594
- });
595
- }
596
-
597
- if (typeof exports !== "undefined") {
598
- exports.default = Cap;
599
- }
387
+ this.#shadow.appendChild(this.#div);
388
+ }
389
+
390
+ addEventListeners() {
391
+ if (!this.#div) return;
392
+
393
+ this.#div.querySelector("a").addEventListener("click", (e) => {
394
+ e.stopPropagation();
395
+ e.preventDefault();
396
+ window.open("https://capjs.js.org", "_blank");
397
+ });
398
+
399
+ this.#div.addEventListener("click", () => {
400
+ if (!this.#div.hasAttribute("disabled")) this.solve();
401
+ });
402
+
403
+ this.#div.addEventListener("keydown", (e) => {
404
+ if (
405
+ (e.key === "Enter" || e.key === " ") &&
406
+ !this.#div.hasAttribute("disabled")
407
+ ) {
408
+ e.preventDefault();
409
+ e.stopPropagation();
410
+ this.solve();
411
+ }
412
+ });
413
+
414
+ this.addEventListener("progress", this.boundHandleProgress);
415
+ this.addEventListener("solve", this.boundHandleSolve);
416
+ this.addEventListener("error", this.boundHandleError);
417
+ this.addEventListener("reset", this.boundHandleReset);
418
+ }
419
+
420
+ updateUI(state, text, disabled = false) {
421
+ if (!this.#div) return;
422
+
423
+ this.#div.setAttribute("data-state", state);
424
+
425
+ this.#div.querySelector("p").innerText = text;
426
+
427
+ if (disabled) {
428
+ this.#div.setAttribute("disabled", "true");
429
+ } else {
430
+ this.#div.removeAttribute("disabled");
431
+ }
432
+ }
433
+
434
+ handleProgress(event) {
435
+ if (!this.#div) return;
436
+
437
+ const progressElement = this.#div.querySelector("p");
438
+ const checkboxElement = this.#div.querySelector(".checkbox");
439
+
440
+ if (progressElement && checkboxElement) {
441
+ checkboxElement.style.setProperty(
442
+ "--progress",
443
+ `${event.detail.progress}%`,
444
+ );
445
+ progressElement.innerText = `${this.getI18nText(
446
+ "verifying-label",
447
+ "Verifying...",
448
+ )} ${event.detail.progress}%`;
449
+ }
450
+ this.executeAttributeCode("onprogress", event);
451
+ }
452
+
453
+ handleSolve(event) {
454
+ this.updateUI(
455
+ "done",
456
+ this.getI18nText("solved-label", "You're a human"),
457
+ true,
458
+ );
459
+ this.executeAttributeCode("onsolve", event);
460
+ }
461
+
462
+ handleError(event) {
463
+ this.updateUI(
464
+ "error",
465
+ this.getI18nText("error-label", "Error. Try again."),
466
+ );
467
+ this.executeAttributeCode("onerror", event);
468
+ }
469
+
470
+ handleReset(event) {
471
+ this.updateUI("", this.getI18nText("initial-state", "I'm a human"));
472
+ this.executeAttributeCode("onreset", event);
473
+ }
474
+
475
+ executeAttributeCode(attributeName, event) {
476
+ const code = this.getAttribute(attributeName);
477
+ if (!code) {
478
+ return;
479
+ }
480
+
481
+ new Function("event", code).call(this, event);
482
+ }
483
+
484
+ error(message = "Unknown error") {
485
+ console.error("[cap]", message);
486
+ this.dispatchEvent("error", { isCap: true, message });
487
+ }
488
+
489
+ dispatchEvent(eventName, detail = {}) {
490
+ const event = new CustomEvent(eventName, {
491
+ bubbles: true,
492
+ composed: true,
493
+ detail,
494
+ });
495
+ super.dispatchEvent(event);
496
+ }
497
+
498
+ reset() {
499
+ if (this.#resetTimer) {
500
+ clearTimeout(this.#resetTimer);
501
+ this.#resetTimer = null;
502
+ }
503
+ this.dispatchEvent("reset");
504
+ this.token = null;
505
+ const fieldName =
506
+ this.getAttribute("data-cap-hidden-field-name") || "cap-token";
507
+ if (this.querySelector(`input[name='${fieldName}']`)) {
508
+ this.querySelector(`input[name='${fieldName}']`).value = "";
509
+ }
510
+ }
511
+
512
+ get tokenValue() {
513
+ return this.token;
514
+ }
515
+
516
+ disconnectedCallback() {
517
+ this.removeEventListener("progress", this.boundHandleProgress);
518
+ this.removeEventListener("solve", this.boundHandleSolve);
519
+ this.removeEventListener("error", this.boundHandleError);
520
+ this.removeEventListener("reset", this.boundHandleReset);
521
+
522
+ this.#eventHandlers.forEach((handler, eventName) => {
523
+ this.removeEventListener(eventName.slice(2), handler);
524
+ });
525
+ this.#eventHandlers.clear();
526
+
527
+ if (this.#shadow) {
528
+ this.#shadow.innerHTML = "";
529
+ }
530
+
531
+ this.reset();
532
+ this.cleanup();
533
+ }
534
+
535
+ cleanup() {
536
+ if (this.#resetTimer) {
537
+ clearTimeout(this.#resetTimer);
538
+ this.#resetTimer = null;
539
+ }
540
+
541
+ if (this.#workerUrl) {
542
+ URL.revokeObjectURL(this.#workerUrl);
543
+ this.#workerUrl = "";
544
+ }
545
+ }
546
+ }
547
+
548
+ // MARK: Invisible
549
+ class Cap {
550
+ constructor(config = {}, el) {
551
+ const widget = el || document.createElement("cap-widget");
552
+
553
+ Object.entries(config).forEach(([a, b]) => {
554
+ widget.setAttribute(a, b);
555
+ });
556
+
557
+ if (!config.apiEndpoint && !window?.CAP_CUSTOM_FETCH) {
558
+ widget.remove();
559
+ throw new Error(
560
+ "Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
561
+ );
562
+ }
563
+
564
+ if (config.apiEndpoint) {
565
+ widget.setAttribute("data-cap-api-endpoint", config.apiEndpoint);
566
+ }
567
+
568
+ this.widget = widget;
569
+ this.solve = this.widget.solve.bind(this.widget);
570
+ this.reset = this.widget.reset.bind(this.widget);
571
+ this.addEventListener = this.widget.addEventListener.bind(this.widget);
572
+
573
+ Object.defineProperty(this, "token", {
574
+ get: () => widget.token,
575
+ configurable: true,
576
+ enumerable: true,
577
+ });
578
+
579
+ if (!el) {
580
+ widget.style.display = "none";
581
+ document.documentElement.appendChild(widget);
582
+ }
583
+ }
584
+ }
585
+ window.Cap = Cap;
586
+
587
+ if (!customElements.get("cap-widget") && !window?.CAP_DONT_SKIP_REDEFINE) {
588
+ customElements.define("cap-widget", CapWidget);
589
+ } else {
590
+ console.warn(
591
+ "[cap] the cap-widget element has already been defined, skipping re-defining it.\nto prevent this, set window.CAP_DONT_SKIP_REDEFINE to true",
592
+ );
593
+ }
594
+
595
+ if (typeof exports === "object" && typeof module !== "undefined") {
596
+ module.exports = Cap;
597
+ } else if (typeof define === "function" && define.amd) {
598
+ define([], () => Cap);
599
+ }
600
+
601
+ if (typeof exports !== "undefined") {
602
+ exports.default = Cap;
603
+ }
600
604
  })();