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