@cap.js/widget 0.1.36 → 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 +17 -0
- package/src/cap.js +726 -93
- package/src/worker.js +21 -7
- package/wasm-hashes.min.js +317 -360
package/src/cap.js
CHANGED
|
@@ -54,6 +54,104 @@
|
|
|
54
54
|
return result.substring(0, length);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
async function runInstrumentationChallenge(instrBytes) {
|
|
58
|
+
const b64ToUint8 = (b64) => {
|
|
59
|
+
const bin = atob(b64);
|
|
60
|
+
const arr = new Uint8Array(bin.length);
|
|
61
|
+
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
|
62
|
+
return arr;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
var compressed = b64ToUint8(instrBytes);
|
|
66
|
+
|
|
67
|
+
const scriptText = await new Promise((resolve, reject) => {
|
|
68
|
+
try {
|
|
69
|
+
var ds = new DecompressionStream("deflate-raw");
|
|
70
|
+
var writer = ds.writable.getWriter();
|
|
71
|
+
var reader = ds.readable.getReader();
|
|
72
|
+
var chunks = [];
|
|
73
|
+
function pump(res) {
|
|
74
|
+
if (res.done) {
|
|
75
|
+
var len = 0,
|
|
76
|
+
off = 0;
|
|
77
|
+
for (var i = 0; i < chunks.length; i++) len += chunks[i].length;
|
|
78
|
+
var out = new Uint8Array(len);
|
|
79
|
+
for (var i = 0; i < chunks.length; i++) {
|
|
80
|
+
out.set(chunks[i], off);
|
|
81
|
+
off += chunks[i].length;
|
|
82
|
+
}
|
|
83
|
+
resolve(new TextDecoder().decode(out));
|
|
84
|
+
} else {
|
|
85
|
+
chunks.push(res.value);
|
|
86
|
+
reader.read().then(pump).catch(reject);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
reader.read().then(pump).catch(reject);
|
|
90
|
+
writer
|
|
91
|
+
.write(compressed)
|
|
92
|
+
.then(() => {
|
|
93
|
+
writer.close();
|
|
94
|
+
})
|
|
95
|
+
.catch(reject);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
reject(e);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
var timeout = setTimeout(() => {
|
|
103
|
+
cleanup();
|
|
104
|
+
resolve({ __timeout: true });
|
|
105
|
+
}, 20000);
|
|
106
|
+
|
|
107
|
+
var iframe = document.createElement("iframe");
|
|
108
|
+
iframe.setAttribute("sandbox", "allow-scripts");
|
|
109
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
110
|
+
iframe.style.cssText =
|
|
111
|
+
"position:absolute;width:1px;height:1px;top:-9999px;left:-9999px;border:none;opacity:0;pointer-events:none;";
|
|
112
|
+
|
|
113
|
+
var resolved = false;
|
|
114
|
+
function cleanup() {
|
|
115
|
+
if (resolved) return;
|
|
116
|
+
resolved = true;
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
window.removeEventListener("message", handler);
|
|
119
|
+
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function handler(ev) {
|
|
123
|
+
var d = ev.data;
|
|
124
|
+
if (!d || typeof d !== "object") return;
|
|
125
|
+
if (d.type === "cap:instr") {
|
|
126
|
+
cleanup();
|
|
127
|
+
if (d.blocked) {
|
|
128
|
+
resolve({
|
|
129
|
+
__blocked: true,
|
|
130
|
+
blockReason: d.blockReason || "automated_browser",
|
|
131
|
+
});
|
|
132
|
+
} else if (d.result) {
|
|
133
|
+
resolve(d.result);
|
|
134
|
+
} else {
|
|
135
|
+
resolve({ __timeout: true });
|
|
136
|
+
}
|
|
137
|
+
} else if (d.type === "cap:error") {
|
|
138
|
+
cleanup();
|
|
139
|
+
resolve({ __timeout: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
window.addEventListener("message", handler);
|
|
144
|
+
|
|
145
|
+
iframe.srcdoc =
|
|
146
|
+
'<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><script>' +
|
|
147
|
+
scriptText +
|
|
148
|
+
"\n</scr" +
|
|
149
|
+
"ipt></body></html>";
|
|
150
|
+
|
|
151
|
+
document.body.appendChild(iframe);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
57
155
|
let wasmModulePromise = null;
|
|
58
156
|
|
|
59
157
|
const getWasmModule = () => {
|
|
@@ -84,8 +182,382 @@
|
|
|
84
182
|
const prefersReducedMotion = () =>
|
|
85
183
|
window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
86
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
|
+
|
|
87
560
|
class CapWidget extends HTMLElement {
|
|
88
|
-
#workerUrl = "";
|
|
89
561
|
#resetTimer = null;
|
|
90
562
|
#workersCount = navigator.hardwareConcurrency || 8;
|
|
91
563
|
token = null;
|
|
@@ -126,13 +598,7 @@
|
|
|
126
598
|
}
|
|
127
599
|
|
|
128
600
|
initialize() {
|
|
129
|
-
|
|
130
|
-
// this placeholder will be replaced with the actual worker by the build script
|
|
131
|
-
|
|
132
|
-
new Blob([`%%workerScript%%`], {
|
|
133
|
-
type: "application/javascript",
|
|
134
|
-
}),
|
|
135
|
-
);
|
|
601
|
+
_getSharedWorkerUrl();
|
|
136
602
|
}
|
|
137
603
|
|
|
138
604
|
attributeChangedCallback(name, _, value) {
|
|
@@ -182,6 +648,15 @@
|
|
|
182
648
|
this.setWorkersCount(parsedWorkers || navigator.hardwareConcurrency || 8);
|
|
183
649
|
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
|
|
184
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
|
+
}
|
|
185
660
|
}
|
|
186
661
|
|
|
187
662
|
async solve() {
|
|
@@ -192,59 +667,209 @@
|
|
|
192
667
|
try {
|
|
193
668
|
this.#solving = true;
|
|
194
669
|
this.updateUI("verifying", this.getI18nText("verifying-label", "Verifying..."), true);
|
|
195
|
-
|
|
196
670
|
this.#div.setAttribute(
|
|
197
671
|
"aria-label",
|
|
198
672
|
this.getI18nText("verifying-aria-label", "Verifying you're a human, please wait"),
|
|
199
673
|
);
|
|
200
|
-
|
|
201
674
|
this.dispatchEvent("progress", { progress: 0 });
|
|
202
675
|
|
|
203
676
|
try {
|
|
204
677
|
let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
|
|
205
|
-
|
|
206
678
|
if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) {
|
|
207
679
|
apiEndpoint = "/";
|
|
208
|
-
} else if (!apiEndpoint)
|
|
680
|
+
} else if (!apiEndpoint) {
|
|
209
681
|
throw new Error(
|
|
210
682
|
"Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
|
|
211
683
|
);
|
|
684
|
+
}
|
|
685
|
+
if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
|
|
686
|
+
|
|
687
|
+
let solutions;
|
|
688
|
+
let challengeResp;
|
|
689
|
+
|
|
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
|
+
);
|
|
212
716
|
|
|
213
|
-
|
|
214
|
-
|
|
717
|
+
_resetSpeculativeState();
|
|
718
|
+
this.#solving = false;
|
|
719
|
+
return;
|
|
215
720
|
}
|
|
216
721
|
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
}
|
|
740
|
+
|
|
741
|
+
speculative.pendingPromotion = this.#workersCount;
|
|
742
|
+
if (speculative.promoteFn) {
|
|
743
|
+
speculative.promoteFn(this.#workersCount);
|
|
744
|
+
}
|
|
745
|
+
|
|
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
|
+
}
|
|
766
|
+
|
|
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
|
+
}
|
|
797
|
+
|
|
798
|
+
solutions = speculative.results;
|
|
799
|
+
challengeResp = speculative.challengeResp;
|
|
800
|
+
this.dispatchEvent("progress", { progress: 100 });
|
|
801
|
+
} else {
|
|
802
|
+
const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
|
|
219
803
|
method: "POST",
|
|
220
|
-
})
|
|
221
|
-
|
|
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
|
+
}
|
|
222
821
|
|
|
223
|
-
|
|
822
|
+
solutions = await this.solveChallenges(challenges);
|
|
823
|
+
}
|
|
224
824
|
|
|
225
|
-
|
|
226
|
-
|
|
825
|
+
const instrPromise = challengeResp.instrumentation
|
|
826
|
+
? runInstrumentationChallenge(challengeResp.instrumentation)
|
|
827
|
+
: Promise.resolve(null);
|
|
227
828
|
|
|
228
|
-
|
|
229
|
-
i = i + 1;
|
|
829
|
+
const instrOut = await instrPromise;
|
|
230
830
|
|
|
231
|
-
|
|
831
|
+
if (instrOut?.__timeout || instrOut?.__blocked) {
|
|
832
|
+
this.updateUIBlocked(this.getI18nText("error-label", "Error"), instrOut?.__blocked);
|
|
833
|
+
this.#div.setAttribute(
|
|
834
|
+
"aria-label",
|
|
835
|
+
this.getI18nText("error-aria-label", "An error occurred, please try again"),
|
|
836
|
+
);
|
|
837
|
+
this.removeEventListener("error", this.boundHandleError);
|
|
838
|
+
const errEvent = new CustomEvent("error", {
|
|
839
|
+
bubbles: true,
|
|
840
|
+
composed: true,
|
|
841
|
+
detail: { isCap: true, message: "Instrumentation failed" },
|
|
232
842
|
});
|
|
843
|
+
super.dispatchEvent(errEvent);
|
|
844
|
+
this.addEventListener("error", this.boundHandleError);
|
|
845
|
+
this.executeAttributeCode("onerror", errEvent);
|
|
846
|
+
console.error("[cap]", "Instrumentation failed");
|
|
847
|
+
this.#solving = false;
|
|
848
|
+
return;
|
|
233
849
|
}
|
|
234
850
|
|
|
235
|
-
const
|
|
851
|
+
const { token } = challengeResp;
|
|
236
852
|
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
853
|
+
const redeemResponse = await capFetch(`${apiEndpoint}redeem`, {
|
|
854
|
+
method: "POST",
|
|
855
|
+
body: JSON.stringify({
|
|
856
|
+
token,
|
|
857
|
+
solutions,
|
|
858
|
+
...(instrOut && { instr: instrOut }),
|
|
859
|
+
}),
|
|
860
|
+
headers: { "Content-Type": "application/json" },
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
let resp;
|
|
864
|
+
try {
|
|
865
|
+
resp = await redeemResponse.json();
|
|
866
|
+
} catch {
|
|
867
|
+
throw new Error("Failed to parse server response");
|
|
868
|
+
}
|
|
244
869
|
|
|
245
870
|
this.dispatchEvent("progress", { progress: 100 });
|
|
871
|
+
if (!resp.success) throw new Error(resp.error || "Invalid solution");
|
|
246
872
|
|
|
247
|
-
if (!resp.success) throw new Error("Invalid solution");
|
|
248
873
|
const fieldName = this.getAttribute("data-cap-hidden-field-name") || "cap-token";
|
|
249
874
|
if (this.querySelector(`input[name='${fieldName}']`)) {
|
|
250
875
|
this.querySelector(`input[name='${fieldName}']`).value = resp.token;
|
|
@@ -253,6 +878,8 @@
|
|
|
253
878
|
this.dispatchEvent("solve", { token: resp.token });
|
|
254
879
|
this.token = resp.token;
|
|
255
880
|
|
|
881
|
+
_resetSpeculativeState();
|
|
882
|
+
|
|
256
883
|
if (this.#resetTimer) clearTimeout(this.#resetTimer);
|
|
257
884
|
const expiresIn = new Date(resp.expires).getTime() - Date.now();
|
|
258
885
|
if (expiresIn > 0 && expiresIn < 24 * 60 * 60 * 1000) {
|
|
@@ -287,6 +914,9 @@
|
|
|
287
914
|
const total = challenges.length;
|
|
288
915
|
let completed = 0;
|
|
289
916
|
|
|
917
|
+
const speculativeHead = 0;
|
|
918
|
+
const remaining = total - speculativeHead;
|
|
919
|
+
|
|
290
920
|
let wasmModule = null;
|
|
291
921
|
const wasmSupported =
|
|
292
922
|
typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function";
|
|
@@ -316,67 +946,31 @@
|
|
|
316
946
|
}
|
|
317
947
|
}
|
|
318
948
|
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
return new Worker(this.#workerUrl);
|
|
324
|
-
} catch (error) {
|
|
325
|
-
console.error("[cap] Failed to create worker:", error);
|
|
326
|
-
throw new Error("Worker creation failed");
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
const solveSingleChallenge = ([salt, target], workerId) =>
|
|
331
|
-
new Promise((resolve, reject) => {
|
|
332
|
-
const worker = workers[workerId];
|
|
333
|
-
if (!worker) {
|
|
334
|
-
reject(new Error("Worker not available"));
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
worker.onmessage = ({ data }) => {
|
|
339
|
-
if (!data.found) return;
|
|
340
|
-
|
|
341
|
-
completed++;
|
|
342
|
-
this.dispatchEvent("progress", {
|
|
343
|
-
progress: Math.round((completed / total) * 100),
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
resolve(data.nonce);
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
worker.onerror = (err) => {
|
|
350
|
-
this.error(`Error in worker: ${err.message || err}`);
|
|
351
|
-
reject(err);
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
if (wasmModule) {
|
|
355
|
-
worker.postMessage({ salt, target, wasmModule }, []);
|
|
356
|
-
} else {
|
|
357
|
-
worker.postMessage({ salt, target });
|
|
358
|
-
}
|
|
359
|
-
});
|
|
949
|
+
const pool = new WorkerPool(this.#workersCount);
|
|
950
|
+
pool.setWasm(wasmModule);
|
|
951
|
+
pool._ensureSize(this.#workersCount);
|
|
360
952
|
|
|
361
953
|
const results = [];
|
|
362
954
|
try {
|
|
363
955
|
for (let i = 0; i < challenges.length; i += this.#workersCount) {
|
|
364
956
|
const chunk = challenges.slice(i, Math.min(i + this.#workersCount, challenges.length));
|
|
365
957
|
const chunkResults = await Promise.all(
|
|
366
|
-
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
|
+
),
|
|
367
969
|
);
|
|
368
970
|
results.push(...chunkResults);
|
|
369
971
|
}
|
|
370
972
|
} finally {
|
|
371
|
-
|
|
372
|
-
if (w) {
|
|
373
|
-
try {
|
|
374
|
-
w.terminate();
|
|
375
|
-
} catch (error) {
|
|
376
|
-
console.error("[cap] error terminating worker:", error);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
});
|
|
973
|
+
pool.terminate();
|
|
380
974
|
}
|
|
381
975
|
|
|
382
976
|
return results;
|
|
@@ -406,9 +1000,7 @@
|
|
|
406
1000
|
"Verify you're human",
|
|
407
1001
|
)}</span></p><a part="attribution" aria-label="Secured by Cap" href="https://capjs.js.org/" class="credits" target="_blank" rel="follow noopener" title="Secured by Cap: Self-hosted CAPTCHA for the modern web.">Cap</a>`;
|
|
408
1002
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
this.#shadow.innerHTML = `<style${window.CAP_CSS_NONCE ? ` nonce=${window.CAP_CSS_NONCE}` : ""}>${css}</style>`;
|
|
1003
|
+
this.#shadow.innerHTML = `<style${window.CAP_CSS_NONCE ? ` nonce=${window.CAP_CSS_NONCE}` : ""}>%%capCSS%%</style>`;
|
|
412
1004
|
|
|
413
1005
|
this.#shadow.appendChild(this.#div);
|
|
414
1006
|
}
|
|
@@ -471,7 +1063,9 @@
|
|
|
471
1063
|
if (current) {
|
|
472
1064
|
current.classList.remove("active");
|
|
473
1065
|
current.classList.add("exit");
|
|
474
|
-
current.addEventListener("transitionend", () => current.remove(), {
|
|
1066
|
+
current.addEventListener("transitionend", () => current.remove(), {
|
|
1067
|
+
once: true,
|
|
1068
|
+
});
|
|
475
1069
|
}
|
|
476
1070
|
}
|
|
477
1071
|
|
|
@@ -489,6 +1083,46 @@
|
|
|
489
1083
|
}
|
|
490
1084
|
}
|
|
491
1085
|
|
|
1086
|
+
updateUIBlocked(label, showTroubleshooting = false) {
|
|
1087
|
+
if (!this.#div) return;
|
|
1088
|
+
|
|
1089
|
+
this.#div.setAttribute("data-state", "error");
|
|
1090
|
+
this.#div.removeAttribute("disabled");
|
|
1091
|
+
|
|
1092
|
+
const wrapper = this.#div.querySelector(".label-wrapper");
|
|
1093
|
+
if (!wrapper) return;
|
|
1094
|
+
|
|
1095
|
+
const troubleshootingUrl =
|
|
1096
|
+
this.getAttribute("data-cap-troubleshooting-url") ||
|
|
1097
|
+
"https://capjs.js.org/guide/troubleshooting/instrumentation.html";
|
|
1098
|
+
|
|
1099
|
+
const current = wrapper.querySelector(".label.active");
|
|
1100
|
+
const next = document.createElement("span");
|
|
1101
|
+
next.className = "label";
|
|
1102
|
+
next.innerHTML = showTroubleshooting
|
|
1103
|
+
? `${label} · <a class="cap-troubleshoot-link" href="${troubleshootingUrl}" target="_blank" rel="noopener">${this.getI18nText("troubleshooting-label", "Troubleshoot")}</a>`
|
|
1104
|
+
: label;
|
|
1105
|
+
wrapper.appendChild(next);
|
|
1106
|
+
|
|
1107
|
+
void next.offsetWidth;
|
|
1108
|
+
next.classList.add("active");
|
|
1109
|
+
if (current) {
|
|
1110
|
+
current.classList.remove("active");
|
|
1111
|
+
current.classList.add("exit");
|
|
1112
|
+
current.addEventListener("transitionend", () => current.remove(), {
|
|
1113
|
+
once: true,
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const link = next.querySelector(".cap-troubleshoot-link");
|
|
1118
|
+
if (link) {
|
|
1119
|
+
console.log("linkblud")
|
|
1120
|
+
link.addEventListener("click", (e) => {
|
|
1121
|
+
e.stopPropagation();
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
492
1126
|
handleProgress(event) {
|
|
493
1127
|
if (!this.#div) return;
|
|
494
1128
|
|
|
@@ -532,6 +1166,10 @@
|
|
|
532
1166
|
return;
|
|
533
1167
|
}
|
|
534
1168
|
|
|
1169
|
+
console.error(
|
|
1170
|
+
"[cap] using `onxxx='…'` is strongly discouraged and will be deprecated soon. please use `addEventListener` callbacks instead.",
|
|
1171
|
+
);
|
|
1172
|
+
|
|
535
1173
|
new Function("event", code).call(this, event);
|
|
536
1174
|
}
|
|
537
1175
|
|
|
@@ -590,11 +1228,6 @@
|
|
|
590
1228
|
clearTimeout(this.#resetTimer);
|
|
591
1229
|
this.#resetTimer = null;
|
|
592
1230
|
}
|
|
593
|
-
|
|
594
|
-
if (this.#workerUrl) {
|
|
595
|
-
URL.revokeObjectURL(this.#workerUrl);
|
|
596
|
-
this.#workerUrl = "";
|
|
597
|
-
}
|
|
598
1231
|
}
|
|
599
1232
|
}
|
|
600
1233
|
|