@cap.js/widget 0.1.37 → 0.1.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cap.compat.min.js +408 -1
- package/cap.d.ts +12 -48
- package/cap.min.js +1 -1
- package/package.json +18 -18
- package/src/cap-floating.js +14 -16
- package/src/cap.css +4 -0
- package/src/cap.js +568 -153
- package/wasm-hashes.min.js +317 -360
package/src/cap.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const WASM_VERSION = "0.0.6";
|
|
3
|
+
const hasHaptics =
|
|
4
|
+
"vibrate" in navigator && !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3
5
|
|
|
4
6
|
if (typeof window === "undefined") {
|
|
5
7
|
return;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
const capFetch = (u,
|
|
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
|
-
|
|
10
|
+
const capFetch = (u, conf = {}) => {
|
|
22
11
|
if (window?.CAP_CUSTOM_FETCH) {
|
|
23
12
|
return window.CAP_CUSTOM_FETCH(u, conf);
|
|
24
13
|
}
|
|
@@ -64,7 +53,7 @@
|
|
|
64
53
|
|
|
65
54
|
var compressed = b64ToUint8(instrBytes);
|
|
66
55
|
|
|
67
|
-
const scriptText = await new Promise(
|
|
56
|
+
const scriptText = await new Promise((resolve, reject) => {
|
|
68
57
|
try {
|
|
69
58
|
var ds = new DecompressionStream("deflate-raw");
|
|
70
59
|
var writer = ds.writable.getWriter();
|
|
@@ -89,7 +78,7 @@
|
|
|
89
78
|
reader.read().then(pump).catch(reject);
|
|
90
79
|
writer
|
|
91
80
|
.write(compressed)
|
|
92
|
-
.then(
|
|
81
|
+
.then(() => {
|
|
93
82
|
writer.close();
|
|
94
83
|
})
|
|
95
84
|
.catch(reject);
|
|
@@ -98,8 +87,8 @@
|
|
|
98
87
|
}
|
|
99
88
|
});
|
|
100
89
|
|
|
101
|
-
return new Promise(
|
|
102
|
-
var timeout = setTimeout(
|
|
90
|
+
return new Promise((resolve) => {
|
|
91
|
+
var timeout = setTimeout(() => {
|
|
103
92
|
cleanup();
|
|
104
93
|
resolve({ __timeout: true });
|
|
105
94
|
}, 20000);
|
|
@@ -125,7 +114,10 @@
|
|
|
125
114
|
if (d.type === "cap:instr") {
|
|
126
115
|
cleanup();
|
|
127
116
|
if (d.blocked) {
|
|
128
|
-
resolve({
|
|
117
|
+
resolve({
|
|
118
|
+
__blocked: true,
|
|
119
|
+
blockReason: d.blockReason || "automated_browser",
|
|
120
|
+
});
|
|
129
121
|
} else if (d.result) {
|
|
130
122
|
resolve(d.result);
|
|
131
123
|
} else {
|
|
@@ -179,8 +171,382 @@
|
|
|
179
171
|
const prefersReducedMotion = () =>
|
|
180
172
|
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
181
173
|
|
|
174
|
+
const SPECULATIVE_DELAY_MS = 2500;
|
|
175
|
+
const SPECULATIVE_WORKERS = 1;
|
|
176
|
+
const SPECULATIVE_YIELD_MS = 120;
|
|
177
|
+
|
|
178
|
+
const speculative = {
|
|
179
|
+
state: "idle",
|
|
180
|
+
challengeResp: null,
|
|
181
|
+
challenges: null,
|
|
182
|
+
results: [],
|
|
183
|
+
completedCount: 0,
|
|
184
|
+
solvePromise: null,
|
|
185
|
+
promoteFn: null,
|
|
186
|
+
_listeners: [],
|
|
187
|
+
pendingPromotion: null,
|
|
188
|
+
token: null,
|
|
189
|
+
tokenExpires: null,
|
|
190
|
+
|
|
191
|
+
notify() {
|
|
192
|
+
for (const fn of this._listeners) fn();
|
|
193
|
+
this._listeners = [];
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
onSettled(fn) {
|
|
197
|
+
if (this.state === "done" || this.state === "error") {
|
|
198
|
+
fn();
|
|
199
|
+
} else {
|
|
200
|
+
this._listeners.push(fn);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
function _resetSpeculativeState() {
|
|
206
|
+
speculative.state = "idle";
|
|
207
|
+
speculative.challengeResp = null;
|
|
208
|
+
speculative.challenges = null;
|
|
209
|
+
speculative.results = [];
|
|
210
|
+
speculative.completedCount = 0;
|
|
211
|
+
speculative.solvePromise = null;
|
|
212
|
+
speculative.promoteFn = null;
|
|
213
|
+
speculative.pendingPromotion = null;
|
|
214
|
+
speculative._listeners = [];
|
|
215
|
+
speculative.token = null;
|
|
216
|
+
speculative.tokenExpires = null;
|
|
217
|
+
|
|
218
|
+
_attachInteractionListeners();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let _speculativeTimer = null;
|
|
222
|
+
|
|
223
|
+
function _onFirstInteraction() {
|
|
224
|
+
if (speculative.state !== "idle") return;
|
|
225
|
+
speculative.state = "waiting";
|
|
226
|
+
|
|
227
|
+
_speculativeTimer = setTimeout(() => {
|
|
228
|
+
_beginSpeculativeSolve();
|
|
229
|
+
}, SPECULATIVE_DELAY_MS);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let _currentInteractionHandler = null;
|
|
233
|
+
|
|
234
|
+
function _detachInteractionListeners() {
|
|
235
|
+
if (_currentInteractionHandler) {
|
|
236
|
+
window.removeEventListener("mousemove", _currentInteractionHandler);
|
|
237
|
+
window.removeEventListener("touchstart", _currentInteractionHandler);
|
|
238
|
+
window.removeEventListener("keydown", _currentInteractionHandler);
|
|
239
|
+
_currentInteractionHandler = null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function _attachInteractionListeners() {
|
|
244
|
+
_detachInteractionListeners();
|
|
245
|
+
|
|
246
|
+
const handler = () => {
|
|
247
|
+
_detachInteractionListeners();
|
|
248
|
+
_onFirstInteraction();
|
|
249
|
+
};
|
|
250
|
+
_currentInteractionHandler = handler;
|
|
251
|
+
window.addEventListener("mousemove", handler, { passive: true });
|
|
252
|
+
window.addEventListener("touchstart", handler, { passive: true });
|
|
253
|
+
window.addEventListener("keydown", handler, { passive: true });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
_attachInteractionListeners();
|
|
257
|
+
|
|
258
|
+
async function _beginSpeculativeSolve() {
|
|
259
|
+
if (speculative.state !== "waiting") return;
|
|
260
|
+
speculative.state = "fetching";
|
|
261
|
+
|
|
262
|
+
const widget = document.querySelector("cap-widget");
|
|
263
|
+
if (!widget) {
|
|
264
|
+
speculative.state = "idle";
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let apiEndpoint = widget.getAttribute("data-cap-api-endpoint");
|
|
269
|
+
if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
|
|
270
|
+
apiEndpoint = "/";
|
|
271
|
+
}
|
|
272
|
+
if (!apiEndpoint) {
|
|
273
|
+
speculative.state = "idle";
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const raw = await capFetch(`${apiEndpoint}challenge`, { method: "POST" });
|
|
280
|
+
let resp;
|
|
281
|
+
try {
|
|
282
|
+
resp = await raw.json();
|
|
283
|
+
} catch {
|
|
284
|
+
throw new Error("Failed to parse speculative challenge response");
|
|
285
|
+
}
|
|
286
|
+
if (resp.error) throw new Error(resp.error);
|
|
287
|
+
|
|
288
|
+
resp._apiEndpoint = apiEndpoint;
|
|
289
|
+
speculative.challengeResp = resp;
|
|
290
|
+
|
|
291
|
+
const { challenge, token } = resp;
|
|
292
|
+
let challenges = challenge;
|
|
293
|
+
if (!Array.isArray(challenges)) {
|
|
294
|
+
let i = 0;
|
|
295
|
+
challenges = Array.from({ length: challenge.c }, () => {
|
|
296
|
+
i++;
|
|
297
|
+
return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
speculative.challenges = challenges;
|
|
301
|
+
speculative.state = "solving";
|
|
302
|
+
|
|
303
|
+
speculative.solvePromise = _speculativeSolveAll(challenges);
|
|
304
|
+
} catch (e) {
|
|
305
|
+
console.warn("[cap] speculative challenge fetch failed:", e);
|
|
306
|
+
speculative.state = "error";
|
|
307
|
+
speculative.notify();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function _speculativeSolveAll(challenges) {
|
|
312
|
+
_getSharedWorkerUrl();
|
|
313
|
+
|
|
314
|
+
let wasmModule = null;
|
|
315
|
+
try {
|
|
316
|
+
wasmModule = await getWasmModule();
|
|
317
|
+
} catch {}
|
|
318
|
+
|
|
319
|
+
_speculativePool.setWasm(wasmModule);
|
|
320
|
+
|
|
321
|
+
const total = challenges.length;
|
|
322
|
+
const results = new Array(total);
|
|
323
|
+
|
|
324
|
+
let concurrency = SPECULATIVE_WORKERS;
|
|
325
|
+
let promoted = false;
|
|
326
|
+
|
|
327
|
+
speculative.promoteFn = (fullCount) => {
|
|
328
|
+
if (promoted) return;
|
|
329
|
+
promoted = true;
|
|
330
|
+
concurrency = fullCount;
|
|
331
|
+
_speculativePool._size = fullCount;
|
|
332
|
+
_speculativePool._ensureSize(fullCount);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
if (speculative.pendingPromotion !== null) {
|
|
336
|
+
speculative.promoteFn(speculative.pendingPromotion);
|
|
337
|
+
speculative.pendingPromotion = null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let nextIndex = 0;
|
|
341
|
+
|
|
342
|
+
while (nextIndex < total) {
|
|
343
|
+
const batchSize = concurrency;
|
|
344
|
+
const batch = [];
|
|
345
|
+
const batchIndices = [];
|
|
346
|
+
|
|
347
|
+
for (let i = 0; i < batchSize && nextIndex < total; i++) {
|
|
348
|
+
batchIndices.push(nextIndex);
|
|
349
|
+
batch.push(challenges[nextIndex]);
|
|
350
|
+
nextIndex++;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
_speculativePool._ensureSize(Math.max(concurrency, batchSize));
|
|
354
|
+
|
|
355
|
+
const batchResults = await Promise.all(
|
|
356
|
+
batch.map((challenge) =>
|
|
357
|
+
_speculativePool.run(challenge[0], challenge[1]).then((nonce) => {
|
|
358
|
+
speculative.completedCount++;
|
|
359
|
+
return nonce;
|
|
360
|
+
}),
|
|
361
|
+
),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < batchIndices.length; i++) {
|
|
365
|
+
results[batchIndices[i]] = batchResults[i];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!promoted && nextIndex < total) {
|
|
369
|
+
await new Promise((resolve) => setTimeout(resolve, SPECULATIVE_YIELD_MS));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
speculative.results = results;
|
|
374
|
+
speculative.state = "redeeming";
|
|
375
|
+
_speculativeRedeem(results);
|
|
376
|
+
return results;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function _speculativeRedeem(solutions) {
|
|
380
|
+
try {
|
|
381
|
+
const challengeResp = speculative.challengeResp;
|
|
382
|
+
const apiEndpoint = challengeResp._apiEndpoint;
|
|
383
|
+
if (!apiEndpoint) throw new Error("[cap] speculative redeem: missing apiEndpoint");
|
|
384
|
+
|
|
385
|
+
let instrOut = null;
|
|
386
|
+
if (challengeResp.instrumentation) {
|
|
387
|
+
instrOut = await runInstrumentationChallenge(challengeResp.instrumentation);
|
|
388
|
+
if (instrOut?.__timeout || instrOut?.__blocked) {
|
|
389
|
+
speculative.state = "done";
|
|
390
|
+
speculative.notify();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const redeemRaw = await capFetch(`${apiEndpoint}redeem`, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
body: JSON.stringify({
|
|
398
|
+
token: challengeResp.token,
|
|
399
|
+
solutions,
|
|
400
|
+
...(instrOut && { instr: instrOut }),
|
|
401
|
+
}),
|
|
402
|
+
headers: { "Content-Type": "application/json" },
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
let resp;
|
|
406
|
+
try {
|
|
407
|
+
resp = await redeemRaw.json();
|
|
408
|
+
} catch {
|
|
409
|
+
throw new Error("Failed to parse speculative redeem response");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!resp.success) throw new Error(resp.error || "Speculative redeem failed");
|
|
413
|
+
|
|
414
|
+
speculative.token = resp.token;
|
|
415
|
+
speculative.tokenExpires = new Date(resp.expires).getTime();
|
|
416
|
+
speculative.state = "done";
|
|
417
|
+
speculative.notify();
|
|
418
|
+
} catch (e) {
|
|
419
|
+
console.warn("[cap] speculative redeem failed (will redo on click):", e);
|
|
420
|
+
speculative.state = "done";
|
|
421
|
+
speculative.notify();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let _sharedWorkerUrl = null;
|
|
426
|
+
|
|
427
|
+
function _getSharedWorkerUrl() {
|
|
428
|
+
if (_sharedWorkerUrl) return _sharedWorkerUrl;
|
|
429
|
+
|
|
430
|
+
_sharedWorkerUrl = URL.createObjectURL(
|
|
431
|
+
new Blob([`%%workerScript%%`], { type: "application/javascript" }),
|
|
432
|
+
);
|
|
433
|
+
return _sharedWorkerUrl;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
class WorkerPool {
|
|
437
|
+
constructor(size) {
|
|
438
|
+
this._size = size;
|
|
439
|
+
this._workers = [];
|
|
440
|
+
this._idle = [];
|
|
441
|
+
this._queue = [];
|
|
442
|
+
this._wasmModule = null;
|
|
443
|
+
this._spawnFailures = 0;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
setWasm(wasmModule) {
|
|
447
|
+
this._wasmModule = wasmModule;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
_spawn() {
|
|
451
|
+
const url = _getSharedWorkerUrl();
|
|
452
|
+
const w = new Worker(url);
|
|
453
|
+
w._busy = false;
|
|
454
|
+
this._workers.push(w);
|
|
455
|
+
this._idle.push(w);
|
|
456
|
+
return w;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_replaceWorker(deadWorker) {
|
|
460
|
+
const idx = this._workers.indexOf(deadWorker);
|
|
461
|
+
if (idx !== -1) this._workers.splice(idx, 1);
|
|
462
|
+
const idleIdx = this._idle.indexOf(deadWorker);
|
|
463
|
+
if (idleIdx !== -1) this._idle.splice(idleIdx, 1);
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
deadWorker.terminate();
|
|
467
|
+
} catch {}
|
|
468
|
+
|
|
469
|
+
this._spawnFailures++;
|
|
470
|
+
if (this._spawnFailures > 3) {
|
|
471
|
+
console.error("[cap] worker spawn failed repeatedly, not retrying");
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return this._spawn();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
_ensureSize(n) {
|
|
479
|
+
while (this._workers.length < n) this._spawn();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
run(salt, target) {
|
|
483
|
+
return new Promise((resolve, reject) => {
|
|
484
|
+
this._queue.push({ salt, target, resolve, reject });
|
|
485
|
+
this._dispatch();
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_dispatch() {
|
|
490
|
+
while (this._idle.length > 0 && this._queue.length > 0) {
|
|
491
|
+
const worker = this._idle.shift();
|
|
492
|
+
const { salt, target, resolve, reject } = this._queue.shift();
|
|
493
|
+
|
|
494
|
+
let settled = false;
|
|
495
|
+
|
|
496
|
+
const onMessage = ({ data }) => {
|
|
497
|
+
if (settled) return;
|
|
498
|
+
settled = true;
|
|
499
|
+
worker.removeEventListener("message", onMessage);
|
|
500
|
+
worker.removeEventListener("error", onError);
|
|
501
|
+
this._spawnFailures = 0;
|
|
502
|
+
this._idle.push(worker);
|
|
503
|
+
if (!data.found) {
|
|
504
|
+
reject(new Error(data.error || "worker failed"));
|
|
505
|
+
} else {
|
|
506
|
+
resolve(data.nonce);
|
|
507
|
+
}
|
|
508
|
+
this._dispatch();
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const onError = (err) => {
|
|
512
|
+
if (settled) return;
|
|
513
|
+
settled = true;
|
|
514
|
+
worker.removeEventListener("message", onMessage);
|
|
515
|
+
worker.removeEventListener("error", onError);
|
|
516
|
+
const replacement = this._replaceWorker(worker);
|
|
517
|
+
reject(err);
|
|
518
|
+
if (replacement) {
|
|
519
|
+
this._dispatch();
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
worker.addEventListener("message", onMessage);
|
|
524
|
+
worker.addEventListener("error", onError);
|
|
525
|
+
|
|
526
|
+
if (this._wasmModule) {
|
|
527
|
+
worker.postMessage({ salt, target, wasmModule: this._wasmModule }, []);
|
|
528
|
+
} else {
|
|
529
|
+
worker.postMessage({ salt, target });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
terminate() {
|
|
535
|
+
for (const w of this._workers) {
|
|
536
|
+
try {
|
|
537
|
+
w.terminate();
|
|
538
|
+
} catch {}
|
|
539
|
+
}
|
|
540
|
+
this._workers = [];
|
|
541
|
+
this._idle = [];
|
|
542
|
+
this._queue = [];
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const _speculativePool = new WorkerPool(1);
|
|
547
|
+
_speculativePool._spawn();
|
|
548
|
+
|
|
182
549
|
class CapWidget extends HTMLElement {
|
|
183
|
-
#workerUrl = "";
|
|
184
550
|
#resetTimer = null;
|
|
185
551
|
#workersCount = navigator.hardwareConcurrency || 8;
|
|
186
552
|
token = null;
|
|
@@ -221,13 +587,7 @@
|
|
|
221
587
|
}
|
|
222
588
|
|
|
223
589
|
initialize() {
|
|
224
|
-
|
|
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
|
-
);
|
|
590
|
+
_getSharedWorkerUrl();
|
|
231
591
|
}
|
|
232
592
|
|
|
233
593
|
attributeChangedCallback(name, _, value) {
|
|
@@ -277,6 +637,15 @@
|
|
|
277
637
|
this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
|
|
278
638
|
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
|
|
279
639
|
this.#host.innerHTML = `<input type="hidden" name="${fieldName}">`;
|
|
640
|
+
|
|
641
|
+
if (speculative.state === "idle" || speculative.state === "waiting") {
|
|
642
|
+
if (_speculativeTimer) {
|
|
643
|
+
clearTimeout(_speculativeTimer);
|
|
644
|
+
_speculativeTimer = null;
|
|
645
|
+
}
|
|
646
|
+
speculative.state = "waiting";
|
|
647
|
+
_speculativeTimer = setTimeout(() => _beginSpeculativeSolve(), SPECULATIVE_DELAY_MS);
|
|
648
|
+
}
|
|
280
649
|
}
|
|
281
650
|
|
|
282
651
|
async solve() {
|
|
@@ -287,106 +656,177 @@
|
|
|
287
656
|
try {
|
|
288
657
|
this.#solving = true;
|
|
289
658
|
this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true);
|
|
290
|
-
|
|
291
659
|
this.#div.setAttribute(
|
|
292
660
|
"aria-label",
|
|
293
661
|
this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait"),
|
|
294
662
|
);
|
|
295
|
-
|
|
296
663
|
this.dispatchEvent("progress", { progress: 0 });
|
|
297
664
|
|
|
665
|
+
if (hasHaptics) navigator.vibrate(5);
|
|
666
|
+
|
|
298
667
|
try {
|
|
299
668
|
let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
|
|
300
|
-
|
|
301
669
|
if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
|
|
302
670
|
apiEndpoint = "/";
|
|
303
|
-
} else if (!apiEndpoint)
|
|
671
|
+
} else if (!apiEndpoint) {
|
|
304
672
|
throw new Error(
|
|
305
673
|
"Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
|
|
306
674
|
);
|
|
307
|
-
|
|
308
|
-
if (!apiEndpoint.endsWith("/")) {
|
|
309
|
-
apiEndpoint += "/";
|
|
310
675
|
}
|
|
676
|
+
if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
|
|
311
677
|
|
|
312
|
-
|
|
313
|
-
method: "POST",
|
|
314
|
-
});
|
|
315
|
-
|
|
678
|
+
let solutions;
|
|
316
679
|
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
680
|
|
|
323
|
-
if (
|
|
324
|
-
|
|
681
|
+
if (
|
|
682
|
+
speculative.state === "done" &&
|
|
683
|
+
speculative.token &&
|
|
684
|
+
speculative.tokenExpires &&
|
|
685
|
+
Date.now() < speculative.tokenExpires
|
|
686
|
+
) {
|
|
687
|
+
this.dispatchEvent("progress", { progress: 100 });
|
|
688
|
+
|
|
689
|
+
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
|
|
690
|
+
if (this.querySelector(`input[name='${fieldName}']`)) {
|
|
691
|
+
this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
|
|
692
|
+
}
|
|
693
|
+
this.dispatchEvent("solve", { token: speculative.token });
|
|
694
|
+
this.token = speculative.token;
|
|
695
|
+
|
|
696
|
+
const expiresIn = speculative.tokenExpires - Date.now();
|
|
697
|
+
if (this.#resetTimer) clearTimeout(this.#resetTimer);
|
|
698
|
+
this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
|
|
699
|
+
|
|
700
|
+
this.#div.setAttribute(
|
|
701
|
+
"aria-label",
|
|
702
|
+
this.getI18nText(
|
|
703
|
+
"verified-aria-label",
|
|
704
|
+
"We have verified you're a human, you may now continue",
|
|
705
|
+
),
|
|
706
|
+
);
|
|
707
|
+
if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
|
|
708
|
+
|
|
709
|
+
_resetSpeculativeState();
|
|
710
|
+
this.#solving = false;
|
|
711
|
+
return;
|
|
325
712
|
}
|
|
326
713
|
|
|
327
|
-
|
|
714
|
+
if (speculative.state === "done") {
|
|
715
|
+
solutions = speculative.results;
|
|
716
|
+
challengeResp = speculative.challengeResp;
|
|
717
|
+
this.dispatchEvent("progress", { progress: 100 });
|
|
718
|
+
} else if (
|
|
719
|
+
speculative.state === "solving" ||
|
|
720
|
+
speculative.state === "redeeming" ||
|
|
721
|
+
speculative.state === "fetching" ||
|
|
722
|
+
speculative.state === "waiting"
|
|
723
|
+
) {
|
|
724
|
+
if (speculative.state === "waiting") {
|
|
725
|
+
if (_speculativeTimer) {
|
|
726
|
+
clearTimeout(_speculativeTimer);
|
|
727
|
+
_speculativeTimer = null;
|
|
728
|
+
}
|
|
729
|
+
speculative.state = "waiting";
|
|
730
|
+
_beginSpeculativeSolve();
|
|
731
|
+
}
|
|
328
732
|
|
|
329
|
-
|
|
733
|
+
speculative.pendingPromotion = this.#workersCount;
|
|
734
|
+
if (speculative.promoteFn) {
|
|
735
|
+
speculative.promoteFn(this.#workersCount);
|
|
736
|
+
}
|
|
330
737
|
|
|
331
|
-
|
|
332
|
-
|
|
738
|
+
const progressInterval = setInterval(() => {
|
|
739
|
+
if (speculative.state !== "solving" && speculative.state !== "redeeming") {
|
|
740
|
+
clearInterval(progressInterval);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const total = speculative.challenges ? speculative.challenges.length : 1;
|
|
744
|
+
const done = speculative.completedCount;
|
|
745
|
+
const visual =
|
|
746
|
+
speculative.state === "redeeming"
|
|
747
|
+
? 99
|
|
748
|
+
: Math.min(98, Math.round((done / total) * 100));
|
|
749
|
+
this.dispatchEvent("progress", { progress: visual });
|
|
750
|
+
}, 150);
|
|
751
|
+
|
|
752
|
+
await new Promise((resolve) => speculative.onSettled(resolve));
|
|
753
|
+
clearInterval(progressInterval);
|
|
754
|
+
|
|
755
|
+
if (speculative.state !== "done") {
|
|
756
|
+
throw new Error("Speculative solve failed – please try again");
|
|
757
|
+
}
|
|
333
758
|
|
|
334
|
-
|
|
335
|
-
|
|
759
|
+
if (
|
|
760
|
+
speculative.token &&
|
|
761
|
+
speculative.tokenExpires &&
|
|
762
|
+
Date.now() < speculative.tokenExpires
|
|
763
|
+
) {
|
|
764
|
+
this.dispatchEvent("progress", { progress: 100 });
|
|
765
|
+
|
|
766
|
+
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
|
|
767
|
+
if (this.querySelector(`input[name='${fieldName}']`)) {
|
|
768
|
+
this.querySelector(`input[name='${fieldName}']`).value = speculative.token;
|
|
769
|
+
}
|
|
770
|
+
this.dispatchEvent("solve", { token: speculative.token });
|
|
771
|
+
this.token = speculative.token;
|
|
772
|
+
|
|
773
|
+
const expiresIn = speculative.tokenExpires - Date.now();
|
|
774
|
+
if (this.#resetTimer) clearTimeout(this.#resetTimer);
|
|
775
|
+
this.#resetTimer = setTimeout(() => this.reset(), expiresIn);
|
|
776
|
+
|
|
777
|
+
this.#div.setAttribute(
|
|
778
|
+
"aria-label",
|
|
779
|
+
this.getI18nText(
|
|
780
|
+
"verified-aria-label",
|
|
781
|
+
"We have verified you're a human, you may now continue",
|
|
782
|
+
),
|
|
783
|
+
);
|
|
784
|
+
if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
|
|
785
|
+
|
|
786
|
+
_resetSpeculativeState();
|
|
787
|
+
this.#solving = false;
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
336
790
|
|
|
337
|
-
|
|
791
|
+
solutions = speculative.results;
|
|
792
|
+
challengeResp = speculative.challengeResp;
|
|
793
|
+
this.dispatchEvent("progress", { progress: 100 });
|
|
794
|
+
} else {
|
|
795
|
+
const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
|
|
796
|
+
method: "POST",
|
|
338
797
|
});
|
|
798
|
+
try {
|
|
799
|
+
challengeResp = await challengeRaw.json();
|
|
800
|
+
} catch {
|
|
801
|
+
throw new Error("Failed to parse challenge response from server");
|
|
802
|
+
}
|
|
803
|
+
if (challengeResp.error) throw new Error(challengeResp.error);
|
|
804
|
+
|
|
805
|
+
const { challenge, token } = challengeResp;
|
|
806
|
+
let challenges = challenge;
|
|
807
|
+
if (!Array.isArray(challenges)) {
|
|
808
|
+
let i = 0;
|
|
809
|
+
challenges = Array.from({ length: challenge.c }, () => {
|
|
810
|
+
i++;
|
|
811
|
+
return [prng(`${token}${i}`, challenge.s), prng(`${token}${i}d`, challenge.d)];
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
solutions = await this.solveChallenges(challenges);
|
|
339
816
|
}
|
|
340
817
|
|
|
341
818
|
const instrPromise = challengeResp.instrumentation
|
|
342
819
|
? runInstrumentationChallenge(challengeResp.instrumentation)
|
|
343
820
|
: Promise.resolve(null);
|
|
344
821
|
|
|
345
|
-
const
|
|
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]);
|
|
822
|
+
const instrOut = await instrPromise;
|
|
379
823
|
|
|
380
824
|
if (instrOut?.__timeout || instrOut?.__blocked) {
|
|
381
|
-
this.updateUIBlocked(
|
|
382
|
-
this.getI18nText("error-label", "Error"),
|
|
383
|
-
instrOut && instrOut.__blocked,
|
|
384
|
-
);
|
|
825
|
+
this.updateUIBlocked(this.getI18nText("error-label", "Error"), instrOut?.__blocked);
|
|
385
826
|
this.#div.setAttribute(
|
|
386
827
|
"aria-label",
|
|
387
828
|
this.getI18nText("error-aria-label", "An error occurred, please try again"),
|
|
388
829
|
);
|
|
389
|
-
|
|
390
830
|
this.removeEventListener("error", this.boundHandleError);
|
|
391
831
|
const errEvent = new CustomEvent("error", {
|
|
392
832
|
bubbles: true,
|
|
@@ -395,13 +835,14 @@
|
|
|
395
835
|
});
|
|
396
836
|
super.dispatchEvent(errEvent);
|
|
397
837
|
this.addEventListener("error", this.boundHandleError);
|
|
398
|
-
|
|
399
838
|
this.executeAttributeCode("onerror", errEvent);
|
|
400
839
|
console.error("[cap]", "Instrumentation failed");
|
|
401
840
|
this.#solving = false;
|
|
402
841
|
return;
|
|
403
842
|
}
|
|
404
843
|
|
|
844
|
+
const { token } = challengeResp;
|
|
845
|
+
|
|
405
846
|
const redeemResponse = await capFetch(`${apiEndpoint}redeem`, {
|
|
406
847
|
method: "POST",
|
|
407
848
|
body: JSON.stringify({
|
|
@@ -421,6 +862,7 @@
|
|
|
421
862
|
|
|
422
863
|
this.dispatchEvent("progress", { progress: 100 });
|
|
423
864
|
if (!resp.success) throw new Error(resp.error || "Invalid solution");
|
|
865
|
+
|
|
424
866
|
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
|
|
425
867
|
if (this.querySelector(`input[name='${fieldName}']`)) {
|
|
426
868
|
this.querySelector(`input[name='${fieldName}']`).value = resp.token;
|
|
@@ -429,6 +871,8 @@
|
|
|
429
871
|
this.dispatchEvent("solve", { token: resp.token });
|
|
430
872
|
this.token = resp.token;
|
|
431
873
|
|
|
874
|
+
_resetSpeculativeState();
|
|
875
|
+
|
|
432
876
|
if (this.#resetTimer) clearTimeout(this.#resetTimer);
|
|
433
877
|
const expiresIn = new Date(resp.expires).getTime() - Date.now();
|
|
434
878
|
if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
|
|
@@ -444,6 +888,7 @@
|
|
|
444
888
|
"We have verified you're a human, you may now continue",
|
|
445
889
|
),
|
|
446
890
|
);
|
|
891
|
+
if (hasHaptics) navigator.vibrate([10, 50, 20, 30, 40]);
|
|
447
892
|
|
|
448
893
|
return { success: true, token: this.token };
|
|
449
894
|
} catch (err) {
|
|
@@ -463,6 +908,8 @@
|
|
|
463
908
|
const total = challenges.length;
|
|
464
909
|
let completed = 0;
|
|
465
910
|
|
|
911
|
+
const speculativeHead = 0;
|
|
912
|
+
|
|
466
913
|
let wasmModule = null;
|
|
467
914
|
const wasmSupported =
|
|
468
915
|
typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
|
|
@@ -492,67 +939,31 @@
|
|
|
492
939
|
}
|
|
493
940
|
}
|
|
494
941
|
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
});
|
|
942
|
+
const pool = new WorkerPool(this.#workersCount);
|
|
943
|
+
pool.setWasm(wasmModule);
|
|
944
|
+
pool._ensureSize(this.#workersCount);
|
|
536
945
|
|
|
537
946
|
const results = [];
|
|
538
947
|
try {
|
|
539
948
|
for (let i = 0; i < challenges.length; i += this.#workersCount) {
|
|
540
949
|
const chunk = challenges.slice(i, Math.min(i + this.#workersCount, challenges.length));
|
|
541
950
|
const chunkResults = await Promise.all(
|
|
542
|
-
chunk.map((
|
|
951
|
+
chunk.map(([salt, target]) =>
|
|
952
|
+
pool.run(salt, target).then((nonce) => {
|
|
953
|
+
completed++;
|
|
954
|
+
const visual = Math.min(
|
|
955
|
+
99,
|
|
956
|
+
Math.round(((speculativeHead + completed) / total) * 100),
|
|
957
|
+
);
|
|
958
|
+
this.dispatchEvent("progress", { progress: visual });
|
|
959
|
+
return nonce;
|
|
960
|
+
}),
|
|
961
|
+
),
|
|
543
962
|
);
|
|
544
963
|
results.push(...chunkResults);
|
|
545
964
|
}
|
|
546
965
|
} finally {
|
|
547
|
-
|
|
548
|
-
if (w) {
|
|
549
|
-
try {
|
|
550
|
-
w.terminate();
|
|
551
|
-
} catch (error) {
|
|
552
|
-
console.error("[cap] error terminating worker:", error);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
});
|
|
966
|
+
pool.terminate();
|
|
556
967
|
}
|
|
557
968
|
|
|
558
969
|
return results;
|
|
@@ -645,7 +1056,9 @@
|
|
|
645
1056
|
if (current) {
|
|
646
1057
|
current.classList.remove("active");
|
|
647
1058
|
current.classList.add("exit");
|
|
648
|
-
current.addEventListener("transitionend", () => current.remove(), {
|
|
1059
|
+
current.addEventListener("transitionend", () => current.remove(), {
|
|
1060
|
+
once: true,
|
|
1061
|
+
});
|
|
649
1062
|
}
|
|
650
1063
|
}
|
|
651
1064
|
|
|
@@ -689,11 +1102,14 @@
|
|
|
689
1102
|
if (current) {
|
|
690
1103
|
current.classList.remove("active");
|
|
691
1104
|
current.classList.add("exit");
|
|
692
|
-
current.addEventListener("transitionend", () => current.remove(), {
|
|
1105
|
+
current.addEventListener("transitionend", () => current.remove(), {
|
|
1106
|
+
once: true,
|
|
1107
|
+
});
|
|
693
1108
|
}
|
|
694
1109
|
|
|
695
1110
|
const link = next.querySelector(".cap-troubleshoot-link");
|
|
696
1111
|
if (link) {
|
|
1112
|
+
console.log("linkblud");
|
|
697
1113
|
link.addEventListener("click", (e) => {
|
|
698
1114
|
e.stopPropagation();
|
|
699
1115
|
});
|
|
@@ -715,7 +1131,7 @@
|
|
|
715
1131
|
if (wrapper) {
|
|
716
1132
|
const activeLabel = wrapper.querySelector(".label.active");
|
|
717
1133
|
if (activeLabel) {
|
|
718
|
-
activeLabel.textContent = `${this.getI18nText("verifying-label", "Verifying...")}
|
|
1134
|
+
activeLabel.textContent = `${this.getI18nText("verifying-label", "Verifying...")}`;
|
|
719
1135
|
}
|
|
720
1136
|
}
|
|
721
1137
|
|
|
@@ -730,6 +1146,8 @@
|
|
|
730
1146
|
handleError(event) {
|
|
731
1147
|
this.updateUI("error", this.getI18nText("error-label", "Error. Try again."));
|
|
732
1148
|
this.executeAttributeCode("onerror", event);
|
|
1149
|
+
|
|
1150
|
+
if (hasHaptics) navigator.vibrate([10, 40, 10]);
|
|
733
1151
|
}
|
|
734
1152
|
|
|
735
1153
|
handleReset(event) {
|
|
@@ -742,8 +1160,10 @@
|
|
|
742
1160
|
if (!code) {
|
|
743
1161
|
return;
|
|
744
1162
|
}
|
|
745
|
-
|
|
746
|
-
console.error(
|
|
1163
|
+
|
|
1164
|
+
console.error(
|
|
1165
|
+
"[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.",
|
|
1166
|
+
);
|
|
747
1167
|
|
|
748
1168
|
new Function("event", code).call(this, event);
|
|
749
1169
|
}
|
|
@@ -803,11 +1223,6 @@
|
|
|
803
1223
|
clearTimeout(this.#resetTimer);
|
|
804
1224
|
this.#resetTimer = null;
|
|
805
1225
|
}
|
|
806
|
-
|
|
807
|
-
if (this.#workerUrl) {
|
|
808
|
-
URL.revokeObjectURL(this.#workerUrl);
|
|
809
|
-
this.#workerUrl = "";
|
|
810
|
-
}
|
|
811
1226
|
}
|
|
812
1227
|
}
|
|
813
1228
|
|