cap-widget 0.1.57 → 0.1.59
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/cap-floating.min.js +1 -1
- package/cap.d.ts +11 -0
- package/cap.min.js +1 -1
- package/package.json +1 -1
- package/src/cap-floating.js +1 -3
- package/src/cap.js +630 -193
- package/src/i18n/translations.mjs +869 -0
- package/src/worker.js +300 -16
- package/cap.compat.min.js +0 -408
- package/src/i18n/translations.js +0 -87
package/src/cap.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
const WASM_VERSION = "0.0.
|
|
2
|
+
const WASM_VERSION = "0.0.8";
|
|
3
3
|
const _browserHasHaptics =
|
|
4
4
|
"vibrate" in navigator &&
|
|
5
5
|
!window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -165,76 +165,124 @@
|
|
|
165
165
|
return pako.inflateRaw(compressed);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
168
|
+
const INSTR_ERRORS = [
|
|
169
|
+
"csp_blocked",
|
|
170
|
+
"natives_tampered",
|
|
171
|
+
"env_failed",
|
|
172
|
+
"script_failed",
|
|
173
|
+
];
|
|
174
|
+
const CSP_DOCS = "https://trycap.dev/guide/widget.html#content-security-policy";
|
|
175
|
+
|
|
176
|
+
function startInstrumentation(instrBytes, host) {
|
|
177
|
+
let finished = false;
|
|
178
|
+
let ready = false;
|
|
179
|
+
let wantFinal = false;
|
|
180
|
+
let nonce = null;
|
|
181
|
+
let timer = null;
|
|
182
|
+
let iframe = null;
|
|
183
|
+
let settle;
|
|
184
|
+
let markReady;
|
|
185
|
+
const out = new Promise((resolve) => {
|
|
186
|
+
settle = resolve;
|
|
187
|
+
});
|
|
188
|
+
const session = {
|
|
189
|
+
observing: false,
|
|
190
|
+
result: null,
|
|
191
|
+
ready: new Promise((resolve) => {
|
|
192
|
+
markReady = resolve;
|
|
193
|
+
}),
|
|
174
194
|
};
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
const send = () =>
|
|
196
|
+
iframe?.contentWindow?.postMessage({ type: "cap:finalize", nonce }, "*");
|
|
197
|
+
const finish = (value) => {
|
|
198
|
+
if (finished) return;
|
|
199
|
+
finished = true;
|
|
200
|
+
session.result = value;
|
|
201
|
+
clearTimeout(timer);
|
|
202
|
+
window.removeEventListener("message", onMessage);
|
|
203
|
+
iframe?.remove();
|
|
204
|
+
markReady();
|
|
205
|
+
settle(value);
|
|
206
|
+
};
|
|
207
|
+
const onMessage = (ev) => {
|
|
208
|
+
if (!iframe?.contentWindow || ev.source !== iframe.contentWindow) return;
|
|
209
|
+
const d = ev.data;
|
|
210
|
+
if (!d || typeof d !== "object") return;
|
|
211
|
+
if (d.type === "cap:ready") {
|
|
212
|
+
ready = true;
|
|
213
|
+
nonce = d.nonce;
|
|
214
|
+
session.observing = d.observe === true;
|
|
215
|
+
markReady();
|
|
216
|
+
if (wantFinal) send();
|
|
217
|
+
} else if (d.type === "cap:instr") {
|
|
218
|
+
if (d.early === true) return;
|
|
219
|
+
finish(
|
|
220
|
+
d.blocked
|
|
221
|
+
? { __blocked: true, blockReason: d.blockReason || "automated_browser" }
|
|
222
|
+
: d.result || { __timeout: true },
|
|
223
|
+
);
|
|
224
|
+
} else if (d.type === "cap:error") {
|
|
225
|
+
finish({
|
|
226
|
+
__error: INSTR_ERRORS.includes(d.reason) ? d.reason : "script_failed",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
session.finalize = () => {
|
|
231
|
+
if (!finished) {
|
|
232
|
+
wantFinal = true;
|
|
233
|
+
timer ??= setTimeout(() => finish({ __timeout: true }), 20000);
|
|
234
|
+
if (ready) send();
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
};
|
|
238
|
+
session.stop = () => finish({ __timeout: true });
|
|
239
|
+
window.addEventListener("message", onMessage);
|
|
240
|
+
|
|
241
|
+
(async () => {
|
|
242
|
+
const bin = atob(instrBytes);
|
|
243
|
+
const bytes = new Uint8Array(bin.length);
|
|
244
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
245
|
+
const scriptText = new TextDecoder().decode(await _inflateRaw(bytes));
|
|
246
|
+
if (finished) return;
|
|
247
|
+
const isolated = window.CAP_ISOLATE === true;
|
|
248
|
+
iframe = document.createElement("iframe");
|
|
249
|
+
iframe.setAttribute(
|
|
250
|
+
"sandbox",
|
|
251
|
+
isolated ? "allow-scripts" : "allow-scripts allow-same-origin",
|
|
252
|
+
);
|
|
187
253
|
iframe.setAttribute("aria-hidden", "true");
|
|
254
|
+
iframe.setAttribute("tabindex", "-1");
|
|
188
255
|
iframe.style.cssText =
|
|
189
256
|
"position:absolute;width:1px;height:1px;top:-9999px;left:-9999px;border:none;opacity:0;pointer-events:none;";
|
|
190
|
-
|
|
191
|
-
var resolved = false;
|
|
192
|
-
function cleanup() {
|
|
193
|
-
if (resolved) return;
|
|
194
|
-
resolved = true;
|
|
195
|
-
clearTimeout(timeout);
|
|
196
|
-
window.removeEventListener("message", handler);
|
|
197
|
-
if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function handler(ev) {
|
|
201
|
-
if (!iframe.contentWindow || ev.source !== iframe.contentWindow) return;
|
|
202
|
-
var d = ev.data;
|
|
203
|
-
if (!d || typeof d !== "object") return;
|
|
204
|
-
if (d.type === "cap:instr") {
|
|
205
|
-
cleanup();
|
|
206
|
-
if (d.blocked) {
|
|
207
|
-
resolve({
|
|
208
|
-
__blocked: true,
|
|
209
|
-
blockReason: d.blockReason || "automated_browser",
|
|
210
|
-
});
|
|
211
|
-
} else if (d.result) {
|
|
212
|
-
resolve(d.result);
|
|
213
|
-
} else {
|
|
214
|
-
resolve({ __timeout: true });
|
|
215
|
-
}
|
|
216
|
-
} else if (d.type === "cap:error") {
|
|
217
|
-
cleanup();
|
|
218
|
-
resolve({ __timeout: true });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
window.addEventListener("message", handler);
|
|
223
|
-
|
|
224
257
|
const scriptNonce = window.CAP_SCRIPT_NONCE || window.CAP_CSS_NONCE;
|
|
225
258
|
const nonceAttr = scriptNonce
|
|
226
259
|
? ` nonce="${String(scriptNonce).replace(/"/g, """)}"`
|
|
227
260
|
: "";
|
|
228
|
-
iframe.srcdoc =
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
261
|
+
iframe.srcdoc = `<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><script${nonceAttr}>${scriptText}\n</scr${"ipt"}></body></html>`;
|
|
262
|
+
iframe.addEventListener("load", () => {
|
|
263
|
+
if (isolated) {
|
|
264
|
+
setTimeout(() => {
|
|
265
|
+
if (!ready && !finished) finish({ __error: "csp_blocked" });
|
|
266
|
+
}, 1000);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
let ran = true;
|
|
270
|
+
try {
|
|
271
|
+
const doc = iframe.contentDocument;
|
|
272
|
+
ran = !doc?.querySelector("script") || iframe.contentWindow.onload !== null;
|
|
273
|
+
} catch {}
|
|
274
|
+
if (ran) return;
|
|
275
|
+
setTimeout(() => {
|
|
276
|
+
if (!ready && !finished) finish({ __error: "csp_blocked" });
|
|
277
|
+
}, 250);
|
|
278
|
+
});
|
|
279
|
+
(host.host?.getClientRects().length === 0
|
|
280
|
+
? document.documentElement
|
|
281
|
+
: host
|
|
282
|
+
).appendChild(iframe);
|
|
283
|
+
})().catch(() => finish({ __timeout: true }));
|
|
284
|
+
|
|
285
|
+
return session;
|
|
238
286
|
}
|
|
239
287
|
|
|
240
288
|
let wasmModulePromise = null;
|
|
@@ -267,6 +315,52 @@
|
|
|
267
315
|
return wasmModulePromise;
|
|
268
316
|
};
|
|
269
317
|
|
|
318
|
+
const firstFulfilled = (tasks) =>
|
|
319
|
+
new Promise((resolve, reject) => {
|
|
320
|
+
let pending = tasks.length;
|
|
321
|
+
let firstError = null;
|
|
322
|
+
for (const task of tasks) {
|
|
323
|
+
task.then(resolve, (err) => {
|
|
324
|
+
if (!firstError) firstError = err;
|
|
325
|
+
pending--;
|
|
326
|
+
if (pending === 0) reject(firstError);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const MAX_HASHWX_DIFFICULTY = 1_000_000_000;
|
|
332
|
+
const MAX_HASHWX_NONCES_PER_HASH = 1_048_576;
|
|
333
|
+
|
|
334
|
+
let hashwxModulePromise = null;
|
|
335
|
+
|
|
336
|
+
const getHashwxModule = () => {
|
|
337
|
+
if (hashwxModulePromise) return hashwxModulePromise;
|
|
338
|
+
|
|
339
|
+
const wasmUrl =
|
|
340
|
+
window.CAP_CUSTOM_HASHWX_URL ||
|
|
341
|
+
`https://cdn.jsdelivr.net/npm/@cap.js/wasm@${WASM_VERSION}/browser/hashwx.wasm`;
|
|
342
|
+
|
|
343
|
+
const t0 = performance.now();
|
|
344
|
+
log.debug(T("wasm"), "fetching", wasmUrl);
|
|
345
|
+
hashwxModulePromise = fetch(wasmUrl)
|
|
346
|
+
.then((r) => {
|
|
347
|
+
if (!r.ok) throw new Error(`Failed to fetch hashwx wasm: ${r.status}`);
|
|
348
|
+
return r.arrayBuffer();
|
|
349
|
+
})
|
|
350
|
+
.then((buf) => WebAssembly.compile(buf))
|
|
351
|
+
.then((mod) => {
|
|
352
|
+
log.debug(T("wasm"), `hashwx ready in ${since(t0)}`);
|
|
353
|
+
return mod;
|
|
354
|
+
})
|
|
355
|
+
.catch((e) => {
|
|
356
|
+
hashwxModulePromise = null;
|
|
357
|
+
log.warn(T("wasm"), `hashwx load failed (${since(t0)}):`, e.message || e);
|
|
358
|
+
throw e;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return hashwxModulePromise;
|
|
362
|
+
};
|
|
363
|
+
|
|
270
364
|
if (
|
|
271
365
|
typeof WebAssembly === "object" &&
|
|
272
366
|
typeof WebAssembly.compile === "function"
|
|
@@ -280,6 +374,7 @@
|
|
|
280
374
|
const SPECULATIVE_DELAY_MS = 2500;
|
|
281
375
|
const SPECULATIVE_WORKERS = 1;
|
|
282
376
|
const SPECULATIVE_YIELD_MS = 120;
|
|
377
|
+
const MAX_IDLE_REFRESHES = 6;
|
|
283
378
|
|
|
284
379
|
let _sharedWorkerUrl = null;
|
|
285
380
|
|
|
@@ -292,6 +387,9 @@
|
|
|
292
387
|
return _sharedWorkerUrl;
|
|
293
388
|
}
|
|
294
389
|
|
|
390
|
+
const WORKER_STOP_GRACE_MS = 250;
|
|
391
|
+
const HASHWX_SETTLE_MS = 1000;
|
|
392
|
+
|
|
295
393
|
class WorkerPool {
|
|
296
394
|
constructor(size) {
|
|
297
395
|
this._size = size;
|
|
@@ -412,15 +510,31 @@
|
|
|
412
510
|
}
|
|
413
511
|
}
|
|
414
512
|
|
|
415
|
-
|
|
513
|
+
stopAll() {
|
|
416
514
|
for (const w of this._workers) {
|
|
417
515
|
try {
|
|
418
|
-
w.
|
|
516
|
+
w.postMessage({ kind: "stop" });
|
|
419
517
|
} catch {}
|
|
420
518
|
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
terminate() {
|
|
522
|
+
const workers = this._workers;
|
|
421
523
|
this._workers = [];
|
|
422
524
|
this._idle = [];
|
|
423
525
|
this._queue = [];
|
|
526
|
+
for (const w of workers) {
|
|
527
|
+
try {
|
|
528
|
+
w.postMessage({ kind: "stop" });
|
|
529
|
+
} catch {}
|
|
530
|
+
}
|
|
531
|
+
setTimeout(() => {
|
|
532
|
+
for (const w of workers) {
|
|
533
|
+
try {
|
|
534
|
+
w.terminate();
|
|
535
|
+
} catch {}
|
|
536
|
+
}
|
|
537
|
+
}, WORKER_STOP_GRACE_MS);
|
|
424
538
|
}
|
|
425
539
|
}
|
|
426
540
|
|
|
@@ -445,6 +559,8 @@
|
|
|
445
559
|
#interactionHandler = null;
|
|
446
560
|
#i18n = null;
|
|
447
561
|
#abort = null;
|
|
562
|
+
#idleRefreshes = 0;
|
|
563
|
+
#refreshOnVisible = false;
|
|
448
564
|
|
|
449
565
|
get #hasHaptics() {
|
|
450
566
|
return (
|
|
@@ -467,6 +583,9 @@
|
|
|
467
583
|
pendingPromotion: null,
|
|
468
584
|
token: null,
|
|
469
585
|
tokenExpires: null,
|
|
586
|
+
fetchP: null,
|
|
587
|
+
refreshTimer: null,
|
|
588
|
+
redeemError: null,
|
|
470
589
|
|
|
471
590
|
notify() {
|
|
472
591
|
for (const fn of this._listeners) fn();
|
|
@@ -484,10 +603,136 @@
|
|
|
484
603
|
}
|
|
485
604
|
|
|
486
605
|
#resetSpeculativeState() {
|
|
606
|
+
this.#dropChallenge(this.#speculative);
|
|
487
607
|
this.#speculative = this.#makeSpeculativeState();
|
|
488
608
|
this.#attachInteractionListeners();
|
|
489
609
|
}
|
|
490
610
|
|
|
611
|
+
#dropChallenge(spec) {
|
|
612
|
+
if (!spec) return;
|
|
613
|
+
clearTimeout(spec.refreshTimer);
|
|
614
|
+
spec.challengeResp?._instr?.stop();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#endpoint() {
|
|
618
|
+
let apiEndpoint = this.getAttribute("data-cap-api-endpoint");
|
|
619
|
+
if (!apiEndpoint && window?.CAP_CUSTOM_FETCH) apiEndpoint = "/";
|
|
620
|
+
if (!apiEndpoint) return null;
|
|
621
|
+
return apiEndpoint.endsWith("/") ? apiEndpoint : `${apiEndpoint}/`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
#instrBlob(resp) {
|
|
625
|
+
if (resp?.format === 2 && Array.isArray(resp.challenges)) {
|
|
626
|
+
return (
|
|
627
|
+
resp.challenges.find((ch) => ch?.protocol === "instrumentation")
|
|
628
|
+
?.payload?.blob ?? null
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return resp?.instrumentation ?? null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#startInstr(resp) {
|
|
635
|
+
const blob = this.#instrBlob(resp);
|
|
636
|
+
if (!blob || !this.#shadow) return null;
|
|
637
|
+
if (!resp._instr || resp._instr.result?.__timeout) {
|
|
638
|
+
resp._instr = startInstrumentation(blob, this.#shadow);
|
|
639
|
+
}
|
|
640
|
+
return resp._instr;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
#finalizeInstr(resp) {
|
|
644
|
+
const session = this.#startInstr(resp);
|
|
645
|
+
return session ? session.finalize() : Promise.resolve(null);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#fetchChallenge(signal) {
|
|
649
|
+
const spec = this.#speculative;
|
|
650
|
+
if (!spec) return Promise.reject(_err("aborted", "aborted"));
|
|
651
|
+
if (spec.fetchP) return spec.fetchP;
|
|
652
|
+
const apiEndpoint = this.#endpoint();
|
|
653
|
+
if (!apiEndpoint) {
|
|
654
|
+
return Promise.reject(
|
|
655
|
+
_err(
|
|
656
|
+
"missing_endpoint",
|
|
657
|
+
"Missing API endpoint. Either custom fetch or an API endpoint must be provided.",
|
|
658
|
+
),
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
const pending = (async () => {
|
|
662
|
+
const raw = await capFetch(`${apiEndpoint}challenge`, {
|
|
663
|
+
method: "POST",
|
|
664
|
+
signal: signal ?? this.#abort?.signal,
|
|
665
|
+
});
|
|
666
|
+
let resp;
|
|
667
|
+
try {
|
|
668
|
+
resp = await raw.json();
|
|
669
|
+
} catch {
|
|
670
|
+
throw _err("challenge_parse_error", "Failed to parse challenge response from server");
|
|
671
|
+
}
|
|
672
|
+
if (resp.error) throw _err("network_error", resp.error);
|
|
673
|
+
resp._apiEndpoint = apiEndpoint;
|
|
674
|
+
if (this.#speculative !== spec) return resp;
|
|
675
|
+
spec.challengeResp = resp;
|
|
676
|
+
this.#startInstr(resp);
|
|
677
|
+
this.#scheduleRefresh(spec, resp);
|
|
678
|
+
return resp;
|
|
679
|
+
})();
|
|
680
|
+
spec.fetchP = pending;
|
|
681
|
+
pending.catch(() => {
|
|
682
|
+
if (spec.fetchP === pending) spec.fetchP = null;
|
|
683
|
+
});
|
|
684
|
+
return pending;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
#scheduleRefresh(spec, resp) {
|
|
688
|
+
clearTimeout(spec.refreshTimer);
|
|
689
|
+
let ttl = Number.NaN;
|
|
690
|
+
try {
|
|
691
|
+
const claims = JSON.parse(
|
|
692
|
+
atob(resp.token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")),
|
|
693
|
+
);
|
|
694
|
+
ttl = Math.min(claims.exp, Number(resp.expires) || claims.exp) - claims.iat;
|
|
695
|
+
} catch {}
|
|
696
|
+
if (!(ttl > 0)) ttl = Number(resp.expires) - Date.now();
|
|
697
|
+
if (!(ttl > 60_000 && ttl < 86_400_000)) ttl = 300_000;
|
|
698
|
+
const hashwx =
|
|
699
|
+
resp.format === 2 &&
|
|
700
|
+
resp.challenges?.some?.((ch) => ch?.protocol === "hashwx");
|
|
701
|
+
const margin = Math.min(
|
|
702
|
+
ttl / 2,
|
|
703
|
+
Math.max(hashwx ? 120_000 : 30_000, ttl * (hashwx ? 0.3 : 0.15)),
|
|
704
|
+
);
|
|
705
|
+
spec.refreshTimer = setTimeout(() => {
|
|
706
|
+
if (this.#speculative !== spec || this.#solving || spec.token) return;
|
|
707
|
+
if (spec.challengeResp !== resp) return;
|
|
708
|
+
if (spec.state !== "idle" && spec.state !== "done") return;
|
|
709
|
+
this.#resetSpeculativeState();
|
|
710
|
+
if (this.#idleRefreshes >= MAX_IDLE_REFRESHES) return;
|
|
711
|
+
if (document.hidden) {
|
|
712
|
+
this.#refreshOnVisible = true;
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
this.#idleRefreshes++;
|
|
716
|
+
this.#prefetch();
|
|
717
|
+
}, ttl - margin);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#takeChallenge() {
|
|
721
|
+
const spec = this.#speculative;
|
|
722
|
+
const resp = spec.challengeResp;
|
|
723
|
+
spec.challengeResp = null;
|
|
724
|
+
spec.fetchP = null;
|
|
725
|
+
clearTimeout(spec.refreshTimer);
|
|
726
|
+
return resp;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
#prefetch() {
|
|
730
|
+
const spec = this.#speculative;
|
|
731
|
+
if (!spec || spec.challengeResp || spec.fetchP) return;
|
|
732
|
+
if (!this.#endpoint() || !this.#isVisible()) return;
|
|
733
|
+
this.#fetchChallenge().catch(() => {});
|
|
734
|
+
}
|
|
735
|
+
|
|
491
736
|
#detachInteractionListeners() {
|
|
492
737
|
if (this.#interactionHandler) {
|
|
493
738
|
window.removeEventListener("mousemove", this.#interactionHandler);
|
|
@@ -501,6 +746,7 @@
|
|
|
501
746
|
this.#detachInteractionListeners();
|
|
502
747
|
const handler = () => {
|
|
503
748
|
this.#detachInteractionListeners();
|
|
749
|
+
this.#idleRefreshes = 0;
|
|
504
750
|
this.#onFirstInteraction();
|
|
505
751
|
};
|
|
506
752
|
this.#interactionHandler = handler;
|
|
@@ -524,6 +770,7 @@
|
|
|
524
770
|
}
|
|
525
771
|
|
|
526
772
|
#onFirstInteraction() {
|
|
773
|
+
this.#prefetch();
|
|
527
774
|
if (this.#speculative.state !== "idle") return;
|
|
528
775
|
if (!this.#isVisible()) return;
|
|
529
776
|
this.#speculative.state = "waiting";
|
|
@@ -533,40 +780,26 @@
|
|
|
533
780
|
}
|
|
534
781
|
|
|
535
782
|
async #beginSpeculativeSolve() {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
783
|
+
const spec = this.#speculative;
|
|
784
|
+
if (spec?.state !== "waiting") return;
|
|
785
|
+
spec.state = "fetching";
|
|
786
|
+
spec._t0 = performance.now();
|
|
539
787
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
apiEndpoint = "/";
|
|
543
|
-
}
|
|
544
|
-
if (!apiEndpoint) {
|
|
545
|
-
this.#speculative.state = "idle";
|
|
788
|
+
if (!this.#endpoint()) {
|
|
789
|
+
spec.state = "idle";
|
|
546
790
|
return;
|
|
547
791
|
}
|
|
548
|
-
if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
|
|
549
792
|
|
|
550
793
|
try {
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
signal: this.#abort?.signal,
|
|
554
|
-
});
|
|
555
|
-
let resp;
|
|
556
|
-
try {
|
|
557
|
-
resp = await raw.json();
|
|
558
|
-
} catch {
|
|
559
|
-
throw new Error("Failed to parse speculative challenge response");
|
|
560
|
-
}
|
|
561
|
-
if (resp.error) throw new Error(resp.error);
|
|
562
|
-
if (!this.#speculative) return;
|
|
563
|
-
|
|
564
|
-
resp._apiEndpoint = apiEndpoint;
|
|
565
|
-
this.#speculative.challengeResp = resp;
|
|
794
|
+
const resp = await this.#fetchChallenge();
|
|
795
|
+
if (this.#speculative !== spec) return;
|
|
566
796
|
|
|
567
797
|
if (resp.format === 2 && Array.isArray(resp.challenges)) {
|
|
568
|
-
|
|
569
|
-
|
|
798
|
+
if (resp.challenges.some((ch) => ch?.protocol === "hashwx")) {
|
|
799
|
+
getHashwxModule().catch(() => {});
|
|
800
|
+
}
|
|
801
|
+
spec.state = "idle";
|
|
802
|
+
spec.notify();
|
|
570
803
|
return;
|
|
571
804
|
}
|
|
572
805
|
|
|
@@ -582,24 +815,25 @@
|
|
|
582
815
|
];
|
|
583
816
|
});
|
|
584
817
|
}
|
|
585
|
-
|
|
586
|
-
|
|
818
|
+
spec.challenges = challenges;
|
|
819
|
+
spec.state = "solving";
|
|
587
820
|
|
|
588
|
-
|
|
821
|
+
spec.solvePromise = this.#speculativeSolveAll(
|
|
589
822
|
challenges,
|
|
823
|
+
spec,
|
|
590
824
|
).catch(() => {
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
825
|
+
if (this.#speculative !== spec) return;
|
|
826
|
+
spec.state = "error";
|
|
827
|
+
spec.notify();
|
|
594
828
|
});
|
|
595
829
|
} catch {
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
|
|
830
|
+
if (this.#speculative !== spec) return;
|
|
831
|
+
spec.state = "error";
|
|
832
|
+
spec.notify();
|
|
599
833
|
}
|
|
600
834
|
}
|
|
601
835
|
|
|
602
|
-
async #speculativeSolveAll(challenges) {
|
|
836
|
+
async #speculativeSolveAll(challenges, spec) {
|
|
603
837
|
_getSharedWorkerUrl();
|
|
604
838
|
|
|
605
839
|
let wasmModule = null;
|
|
@@ -607,7 +841,7 @@
|
|
|
607
841
|
wasmModule = await getWasmModule();
|
|
608
842
|
} catch {}
|
|
609
843
|
|
|
610
|
-
if (
|
|
844
|
+
if (this.#speculative !== spec) return [];
|
|
611
845
|
if (!this.#speculativePool) {
|
|
612
846
|
this.#speculativePool = new WorkerPool(1);
|
|
613
847
|
this.#speculativePool._spawn();
|
|
@@ -620,7 +854,7 @@
|
|
|
620
854
|
let concurrency = SPECULATIVE_WORKERS;
|
|
621
855
|
let promoted = false;
|
|
622
856
|
|
|
623
|
-
|
|
857
|
+
spec.promoteFn = (fullCount) => {
|
|
624
858
|
if (promoted || !this.#speculativePool) return;
|
|
625
859
|
promoted = true;
|
|
626
860
|
concurrency = fullCount;
|
|
@@ -628,15 +862,15 @@
|
|
|
628
862
|
this.#speculativePool._ensureSize(fullCount);
|
|
629
863
|
};
|
|
630
864
|
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
865
|
+
if (spec.pendingPromotion !== null) {
|
|
866
|
+
spec.promoteFn(spec.pendingPromotion);
|
|
867
|
+
spec.pendingPromotion = null;
|
|
634
868
|
}
|
|
635
869
|
|
|
636
870
|
let nextIndex = 0;
|
|
637
871
|
|
|
638
872
|
while (nextIndex < total) {
|
|
639
|
-
if (
|
|
873
|
+
if (this.#speculative !== spec || !this.#speculativePool) return results;
|
|
640
874
|
const batchSize = concurrency;
|
|
641
875
|
const batch = [];
|
|
642
876
|
const batchIndices = [];
|
|
@@ -654,7 +888,7 @@
|
|
|
654
888
|
this.#speculativePool
|
|
655
889
|
.run(challenge[0], challenge[1])
|
|
656
890
|
.then((nonce) => {
|
|
657
|
-
|
|
891
|
+
spec.completedCount++;
|
|
658
892
|
return nonce;
|
|
659
893
|
}),
|
|
660
894
|
),
|
|
@@ -671,30 +905,43 @@
|
|
|
671
905
|
}
|
|
672
906
|
}
|
|
673
907
|
|
|
674
|
-
if (
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
this.#speculativeRedeem(results);
|
|
908
|
+
if (this.#speculative !== spec) return results;
|
|
909
|
+
spec.results = results;
|
|
910
|
+
spec.state = "redeeming";
|
|
911
|
+
this.#speculativeRedeem(results, spec);
|
|
678
912
|
return results;
|
|
679
913
|
}
|
|
680
914
|
|
|
681
|
-
async #speculativeRedeem(solutions) {
|
|
915
|
+
async #speculativeRedeem(solutions, spec) {
|
|
916
|
+
let answered = false;
|
|
682
917
|
try {
|
|
683
|
-
if (
|
|
684
|
-
const challengeResp =
|
|
918
|
+
if (this.#speculative !== spec) return;
|
|
919
|
+
const challengeResp = spec.challengeResp;
|
|
685
920
|
const apiEndpoint = challengeResp._apiEndpoint;
|
|
686
921
|
if (!apiEndpoint)
|
|
687
922
|
throw _err("missing_endpoint", "speculative redeem: missing apiEndpoint");
|
|
688
923
|
|
|
924
|
+
const session = this.#startInstr(challengeResp);
|
|
925
|
+
if (session) {
|
|
926
|
+
await Promise.race([
|
|
927
|
+
session.ready,
|
|
928
|
+
new Promise((resolve) => setTimeout(resolve, 5000)),
|
|
929
|
+
]);
|
|
930
|
+
if (this.#speculative !== spec) return;
|
|
931
|
+
}
|
|
932
|
+
if (session?.observing) {
|
|
933
|
+
spec.state = "done";
|
|
934
|
+
spec.notify();
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
|
|
689
938
|
let instrOut = null;
|
|
690
|
-
if (
|
|
691
|
-
instrOut = await
|
|
692
|
-
|
|
693
|
-
)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
this.#speculative.state = "done";
|
|
697
|
-
this.#speculative.notify();
|
|
939
|
+
if (session) {
|
|
940
|
+
instrOut = await session.finalize();
|
|
941
|
+
if (this.#speculative !== spec) return;
|
|
942
|
+
if (instrOut?.__timeout || instrOut?.__blocked || instrOut?.__error) {
|
|
943
|
+
spec.state = "done";
|
|
944
|
+
spec.notify();
|
|
698
945
|
return;
|
|
699
946
|
}
|
|
700
947
|
}
|
|
@@ -710,26 +957,28 @@
|
|
|
710
957
|
signal: this.#abort?.signal,
|
|
711
958
|
});
|
|
712
959
|
|
|
960
|
+
answered = true;
|
|
713
961
|
let resp;
|
|
714
962
|
try {
|
|
715
963
|
resp = await redeemRaw.json();
|
|
716
964
|
} catch {
|
|
717
|
-
throw new Error("Failed to parse
|
|
965
|
+
throw new Error("Failed to parse server response");
|
|
718
966
|
}
|
|
719
967
|
|
|
720
|
-
if (
|
|
968
|
+
if (this.#speculative !== spec) return;
|
|
721
969
|
if (!resp.success)
|
|
722
|
-
throw new Error(resp.error || "
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
} catch {
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
|
|
970
|
+
throw new Error(resp.error || "Invalid solution");
|
|
971
|
+
|
|
972
|
+
spec.token = resp.token;
|
|
973
|
+
spec.tokenExpires = new Date(resp.expires).getTime();
|
|
974
|
+
spec.state = "done";
|
|
975
|
+
spec._invisibleElapsed = spec._t0 ? since(spec._t0) : "?";
|
|
976
|
+
spec.notify();
|
|
977
|
+
} catch (err) {
|
|
978
|
+
if (this.#speculative !== spec) return;
|
|
979
|
+
if (answered) spec.redeemError = err?.message || "Invalid solution";
|
|
980
|
+
spec.state = "done";
|
|
981
|
+
spec.notify();
|
|
733
982
|
}
|
|
734
983
|
}
|
|
735
984
|
|
|
@@ -902,11 +1151,19 @@
|
|
|
902
1151
|
|
|
903
1152
|
log.debug(T(), `widget ready (workers=${this.#workersCount}, haptics=${this.#hasHaptics})`);
|
|
904
1153
|
this.#attachInteractionListeners();
|
|
1154
|
+
this.#prefetch();
|
|
905
1155
|
this.#updateValidity();
|
|
906
1156
|
|
|
907
1157
|
this.addEventListener("invalid", this.#handleInvalid);
|
|
1158
|
+
document.addEventListener("visibilitychange", this.#handleVisibility);
|
|
908
1159
|
}
|
|
909
1160
|
|
|
1161
|
+
#handleVisibility = () => {
|
|
1162
|
+
if (document.hidden || !this.#refreshOnVisible) return;
|
|
1163
|
+
this.#refreshOnVisible = false;
|
|
1164
|
+
this.#prefetch();
|
|
1165
|
+
};
|
|
1166
|
+
|
|
910
1167
|
#handleInvalid = () => {
|
|
911
1168
|
if (!this.#div) return;
|
|
912
1169
|
try {
|
|
@@ -927,6 +1184,8 @@
|
|
|
927
1184
|
|
|
928
1185
|
this.#enforceCredits();
|
|
929
1186
|
const _solveT0 = performance.now();
|
|
1187
|
+
let usedResp = null;
|
|
1188
|
+
let reported = null;
|
|
930
1189
|
const signal = this.#abort?.signal;
|
|
931
1190
|
log.debug(T("solve"), "starting");
|
|
932
1191
|
|
|
@@ -970,6 +1229,10 @@
|
|
|
970
1229
|
return this.#commitSpeculativeToken();
|
|
971
1230
|
}
|
|
972
1231
|
|
|
1232
|
+
if (this.#speculative.redeemError) {
|
|
1233
|
+
throw _err("invalid_solution", this.#speculative.redeemError);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
973
1236
|
if (this.#speculative.state === "done") {
|
|
974
1237
|
solutions = this.#speculative.results;
|
|
975
1238
|
challengeResp = this.#speculative.challengeResp;
|
|
@@ -1028,11 +1291,12 @@
|
|
|
1028
1291
|
this.#speculative.challengeResp?.format === 2 &&
|
|
1029
1292
|
Array.isArray(this.#speculative.challengeResp.challenges)
|
|
1030
1293
|
) {
|
|
1031
|
-
challengeResp = this.#
|
|
1032
|
-
|
|
1294
|
+
challengeResp = this.#takeChallenge();
|
|
1295
|
+
usedResp = challengeResp;
|
|
1033
1296
|
solutions = await this.solveChallengesV2(
|
|
1034
1297
|
challengeResp.challenges,
|
|
1035
1298
|
signal,
|
|
1299
|
+
challengeResp,
|
|
1036
1300
|
);
|
|
1037
1301
|
} else {
|
|
1038
1302
|
if (this.#speculative.state !== "done") {
|
|
@@ -1047,27 +1311,22 @@
|
|
|
1047
1311
|
return this.#commitSpeculativeToken();
|
|
1048
1312
|
}
|
|
1049
1313
|
|
|
1314
|
+
if (this.#speculative.redeemError) {
|
|
1315
|
+
throw _err("invalid_solution", this.#speculative.redeemError);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1050
1318
|
solutions = this.#speculative.results;
|
|
1051
1319
|
challengeResp = this.#speculative.challengeResp;
|
|
1052
1320
|
this.dispatchEvent("progress", { progress: 100 });
|
|
1053
1321
|
}
|
|
1054
1322
|
} else {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
challengeResp = cached;
|
|
1058
|
-
this.#speculative.challengeResp = null;
|
|
1059
|
-
} else {
|
|
1060
|
-
const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
|
|
1061
|
-
method: "POST",
|
|
1062
|
-
signal,
|
|
1063
|
-
});
|
|
1064
|
-
try {
|
|
1065
|
-
challengeResp = await challengeRaw.json();
|
|
1066
|
-
} catch {
|
|
1067
|
-
throw _err("challenge_parse_error", "Failed to parse challenge response from server");
|
|
1068
|
-
}
|
|
1069
|
-
if (challengeResp.error) throw _err("network_error", challengeResp.error);
|
|
1323
|
+
if (!this.#speculative.challengeResp) {
|
|
1324
|
+
await this.#fetchChallenge(signal);
|
|
1070
1325
|
}
|
|
1326
|
+
if (signal?.aborted || !this.#speculative) return;
|
|
1327
|
+
challengeResp = this.#takeChallenge();
|
|
1328
|
+
if (!challengeResp) throw _err("network_error", "Failed to fetch challenge");
|
|
1329
|
+
usedResp = challengeResp;
|
|
1071
1330
|
|
|
1072
1331
|
if (
|
|
1073
1332
|
challengeResp.format === 2 &&
|
|
@@ -1076,6 +1335,7 @@
|
|
|
1076
1335
|
solutions = await this.solveChallengesV2(
|
|
1077
1336
|
challengeResp.challenges,
|
|
1078
1337
|
signal,
|
|
1338
|
+
challengeResp,
|
|
1079
1339
|
);
|
|
1080
1340
|
} else {
|
|
1081
1341
|
const { challenge, token } = challengeResp;
|
|
@@ -1094,29 +1354,47 @@
|
|
|
1094
1354
|
}
|
|
1095
1355
|
}
|
|
1096
1356
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const instrOut = await instrPromise;
|
|
1357
|
+
usedResp = challengeResp;
|
|
1358
|
+
const v2 = challengeResp.format === 2;
|
|
1359
|
+
const instrOut = v2 ? null : await this.#finalizeInstr(challengeResp);
|
|
1102
1360
|
if (signal?.aborted || !this.#speculative) return;
|
|
1103
1361
|
|
|
1104
|
-
|
|
1362
|
+
const v2Fail = v2
|
|
1363
|
+
? solutions?.find?.((x) => x?.error || x?.timeout || x?.blocked)
|
|
1364
|
+
: null;
|
|
1365
|
+
const instrFail = v2Fail
|
|
1366
|
+
? {
|
|
1367
|
+
__error: v2Fail.error,
|
|
1368
|
+
__timeout: v2Fail.timeout,
|
|
1369
|
+
__blocked: v2Fail.blocked,
|
|
1370
|
+
}
|
|
1371
|
+
: instrOut?.__timeout || instrOut?.__blocked || instrOut?.__error
|
|
1372
|
+
? instrOut
|
|
1373
|
+
: null;
|
|
1374
|
+
|
|
1375
|
+
if (instrFail) {
|
|
1105
1376
|
capFetch(`${apiEndpoint}redeem`, {
|
|
1106
1377
|
method: "POST",
|
|
1107
1378
|
body: JSON.stringify({
|
|
1108
1379
|
token: challengeResp.token,
|
|
1109
1380
|
solutions,
|
|
1110
|
-
...(
|
|
1111
|
-
...(
|
|
1381
|
+
...(!v2 && instrFail.__error && { instr_error: instrFail.__error }),
|
|
1382
|
+
...(!v2 && instrFail.__blocked && { instr_blocked: true }),
|
|
1383
|
+
...(!v2 && instrFail.__timeout && { instr_timeout: true }),
|
|
1112
1384
|
}),
|
|
1113
1385
|
headers: { "Content-Type": "application/json" },
|
|
1114
1386
|
signal,
|
|
1115
1387
|
}).catch(() => {});
|
|
1116
1388
|
|
|
1389
|
+
const csp = instrFail.__error === "csp_blocked";
|
|
1117
1390
|
this.updateUIBlocked(
|
|
1118
|
-
|
|
1119
|
-
|
|
1391
|
+
csp
|
|
1392
|
+
? this.getI18nText(
|
|
1393
|
+
"csp-error-label",
|
|
1394
|
+
this.#i18n?.["error-label"] || "Blocked by this page",
|
|
1395
|
+
)
|
|
1396
|
+
: this.getI18nText("error-label", "Error"),
|
|
1397
|
+
!!instrFail.__blocked,
|
|
1120
1398
|
);
|
|
1121
1399
|
this.#trigger.setAttribute(
|
|
1122
1400
|
"aria-label",
|
|
@@ -1125,10 +1403,18 @@
|
|
|
1125
1403
|
"An error occurred, please try again",
|
|
1126
1404
|
),
|
|
1127
1405
|
);
|
|
1128
|
-
const instrCode =
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1406
|
+
const instrCode = instrFail.__error
|
|
1407
|
+
? `instr_${instrFail.__error}`
|
|
1408
|
+
: instrFail.__blocked
|
|
1409
|
+
? "instr_blocked"
|
|
1410
|
+
: "instr_timeout";
|
|
1411
|
+
const instrMsg = csp
|
|
1412
|
+
? "The page's Content-Security-Policy blocked Cap's instrumentation frame"
|
|
1413
|
+
: instrFail.__error
|
|
1414
|
+
? `Instrumentation failed (${instrFail.__error})`
|
|
1415
|
+
: instrFail.__blocked
|
|
1416
|
+
? `Instrumentation blocked (${instrFail.blockReason || "automated_browser"})`
|
|
1417
|
+
: "Instrumentation timed out";
|
|
1132
1418
|
this.removeEventListener("error", this.boundHandleError);
|
|
1133
1419
|
const errEvent = new CustomEvent("error", {
|
|
1134
1420
|
bubbles: true,
|
|
@@ -1138,9 +1424,15 @@
|
|
|
1138
1424
|
super.dispatchEvent(errEvent);
|
|
1139
1425
|
this.addEventListener("error", this.boundHandleError);
|
|
1140
1426
|
this.executeAttributeCode("onerror", errEvent);
|
|
1141
|
-
log.error(
|
|
1142
|
-
|
|
1143
|
-
|
|
1427
|
+
log.error(
|
|
1428
|
+
T("instr"),
|
|
1429
|
+
csp
|
|
1430
|
+
? `[${instrCode}] ${instrMsg}. Set window.CAP_SCRIPT_NONCE to the nonce in your script-src before the widget loads, see ${CSP_DOCS}`
|
|
1431
|
+
: `[${instrCode}] ${instrMsg}`,
|
|
1432
|
+
);
|
|
1433
|
+
this.#resetSpeculativeState();
|
|
1434
|
+
reported = _err(instrCode, instrMsg);
|
|
1435
|
+
throw reported;
|
|
1144
1436
|
}
|
|
1145
1437
|
|
|
1146
1438
|
const { token } = challengeResp;
|
|
@@ -1197,6 +1489,7 @@
|
|
|
1197
1489
|
return { success: true, token: this.token };
|
|
1198
1490
|
} catch (err) {
|
|
1199
1491
|
if (signal?.aborted || !this.#speculative) return;
|
|
1492
|
+
if (err === reported) throw err;
|
|
1200
1493
|
this.#trigger.setAttribute(
|
|
1201
1494
|
"aria-label",
|
|
1202
1495
|
this.getI18nText(
|
|
@@ -1205,14 +1498,17 @@
|
|
|
1205
1498
|
),
|
|
1206
1499
|
);
|
|
1207
1500
|
this.error(err.message, err.code || "solve_failed");
|
|
1501
|
+
this.#resetSpeculativeState();
|
|
1208
1502
|
throw err;
|
|
1209
1503
|
}
|
|
1210
1504
|
} finally {
|
|
1505
|
+
usedResp?._instr?.stop();
|
|
1211
1506
|
this.#solving = false;
|
|
1212
1507
|
}
|
|
1213
1508
|
}
|
|
1214
1509
|
|
|
1215
|
-
async solveChallengesV2(challenges, signal) {
|
|
1510
|
+
async solveChallengesV2(challenges, signal, resp) {
|
|
1511
|
+
const owner = resp ?? { format: 2, challenges };
|
|
1216
1512
|
const total = challenges.length;
|
|
1217
1513
|
let completed = 0;
|
|
1218
1514
|
|
|
@@ -1225,37 +1521,96 @@
|
|
|
1225
1521
|
!(
|
|
1226
1522
|
ch.protocol === "sha256-pow" ||
|
|
1227
1523
|
ch.protocol === "rsw" ||
|
|
1524
|
+
ch.protocol === "hashwx" ||
|
|
1228
1525
|
ch.protocol === "instrumentation"
|
|
1229
1526
|
)
|
|
1230
1527
|
) {
|
|
1231
|
-
// Unknown protocol = older widget on a newer server. Fall back to
|
|
1232
|
-
// erroring out -- the host can detect this and serve format-1.
|
|
1233
1528
|
throw _err("challenge_unsupported", `unsupported format-2 protocol '${ch?.protocol}'`);
|
|
1234
1529
|
}
|
|
1235
1530
|
}
|
|
1236
1531
|
|
|
1237
|
-
let
|
|
1532
|
+
let needsCapWasm = false;
|
|
1533
|
+
let hashwxCount = 0;
|
|
1534
|
+
for (const ch of challenges) {
|
|
1535
|
+
if (ch.protocol === "sha256-pow") needsCapWasm = true;
|
|
1536
|
+
else if (ch.protocol === "hashwx") {
|
|
1537
|
+
hashwxCount++;
|
|
1538
|
+
const { c, d, n } = ch.payload ?? {};
|
|
1539
|
+
if (
|
|
1540
|
+
typeof c !== "string" ||
|
|
1541
|
+
!/^[0-9a-f]{64}$/i.test(c) ||
|
|
1542
|
+
!Number.isInteger(d) ||
|
|
1543
|
+
d < 1 ||
|
|
1544
|
+
d > MAX_HASHWX_DIFFICULTY ||
|
|
1545
|
+
!Number.isInteger(n) ||
|
|
1546
|
+
n < 1 ||
|
|
1547
|
+
n > MAX_HASHWX_NONCES_PER_HASH
|
|
1548
|
+
) {
|
|
1549
|
+
throw _err("challenge_unsupported", "malformed hashwx challenge payload");
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
const needsHashwx = hashwxCount > 0;
|
|
1554
|
+
|
|
1238
1555
|
const wasmSupported =
|
|
1239
1556
|
typeof WebAssembly === "object" &&
|
|
1240
1557
|
typeof WebAssembly.instantiate === "function";
|
|
1241
|
-
|
|
1558
|
+
|
|
1559
|
+
let wasmModule = null;
|
|
1560
|
+
if (wasmSupported && needsCapWasm) {
|
|
1242
1561
|
try { wasmModule = await getWasmModule(); }
|
|
1243
1562
|
catch (e) { log.warn(T("wasm"), "unavailable, falling back to JS solver:", e.message || e); }
|
|
1244
1563
|
}
|
|
1245
1564
|
|
|
1565
|
+
let hashwxModule = null;
|
|
1566
|
+
if (needsHashwx) {
|
|
1567
|
+
if (!wasmSupported) {
|
|
1568
|
+
throw _err("challenge_unsupported", "hashwx requires WebAssembly");
|
|
1569
|
+
}
|
|
1570
|
+
hashwxModule = await getHashwxModule();
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1246
1573
|
const poolSize = Math.max(1, Math.min(this.#workersCount, challenges.length));
|
|
1247
1574
|
const pool = new WorkerPool(poolSize);
|
|
1248
1575
|
pool.setWasm(wasmModule);
|
|
1249
1576
|
pool._ensureSize(poolSize);
|
|
1250
1577
|
|
|
1578
|
+
const hashwxWorkers = Math.max(1, this.#workersCount);
|
|
1579
|
+
const hashwxQueued = hashwxCount > hashwxWorkers;
|
|
1580
|
+
const hashwxStripes = hashwxQueued ? 1 : hashwxWorkers;
|
|
1581
|
+
let hashwxPool = needsHashwx ? new WorkerPool(hashwxWorkers) : null;
|
|
1582
|
+
if (hashwxPool) hashwxPool._ensureSize(hashwxWorkers);
|
|
1583
|
+
|
|
1584
|
+
const reclaimStripes = async (stripes) => {
|
|
1585
|
+
hashwxPool.stopAll();
|
|
1586
|
+
let timer;
|
|
1587
|
+
const settled = await Promise.race([
|
|
1588
|
+
Promise.allSettled(stripes).then(() => true),
|
|
1589
|
+
new Promise((resolve) => {
|
|
1590
|
+
timer = setTimeout(() => resolve(false), HASHWX_SETTLE_MS);
|
|
1591
|
+
}),
|
|
1592
|
+
]);
|
|
1593
|
+
clearTimeout(timer);
|
|
1594
|
+
if (settled) return;
|
|
1595
|
+
log.warn(T("solve"), "hashwx workers did not stop in time, replacing the pool");
|
|
1596
|
+
hashwxPool.terminate();
|
|
1597
|
+
hashwxPool = new WorkerPool(hashwxWorkers);
|
|
1598
|
+
hashwxPool._ensureSize(hashwxWorkers);
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1251
1601
|
const TASK_TIMEOUT_MS = 60_000;
|
|
1252
|
-
const withTimeout = (promise, label) =>
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1602
|
+
const withTimeout = (promise, label) => {
|
|
1603
|
+
let timer;
|
|
1604
|
+
return Promise.race([
|
|
1605
|
+
promise,
|
|
1606
|
+
new Promise((_, rej) => {
|
|
1607
|
+
timer = setTimeout(
|
|
1608
|
+
() => rej(new Error(`[cap] ${label} timed out after ${TASK_TIMEOUT_MS}ms`)),
|
|
1609
|
+
TASK_TIMEOUT_MS,
|
|
1610
|
+
);
|
|
1611
|
+
}),
|
|
1612
|
+
]).finally(() => clearTimeout(timer));
|
|
1613
|
+
};
|
|
1259
1614
|
|
|
1260
1615
|
const inFlight = new Array(challenges.length).fill(0);
|
|
1261
1616
|
const emit = () => {
|
|
@@ -1264,9 +1619,82 @@
|
|
|
1264
1619
|
this.dispatchEvent("progress", { progress: visual });
|
|
1265
1620
|
};
|
|
1266
1621
|
|
|
1622
|
+
const solveHashwx = async (ch, idx) => {
|
|
1623
|
+
const { c, d, n } = ch.payload;
|
|
1624
|
+
const expected = Math.max(1, d);
|
|
1625
|
+
const striped = new Array(hashwxStripes).fill(0);
|
|
1626
|
+
const stripes = [];
|
|
1627
|
+
try {
|
|
1628
|
+
for (let s = 0; s < hashwxStripes; s++) {
|
|
1629
|
+
const stripe = s;
|
|
1630
|
+
const task = hashwxPool.runMsg(
|
|
1631
|
+
{
|
|
1632
|
+
kind: "hashwx",
|
|
1633
|
+
hashwxModule,
|
|
1634
|
+
c,
|
|
1635
|
+
d,
|
|
1636
|
+
n,
|
|
1637
|
+
workerIndex: stripe,
|
|
1638
|
+
workerCount: hashwxStripes,
|
|
1639
|
+
},
|
|
1640
|
+
(hashes) => {
|
|
1641
|
+
striped[stripe] = hashes;
|
|
1642
|
+
let sum = 0;
|
|
1643
|
+
for (const v of striped) sum += v;
|
|
1644
|
+
inFlight[idx] = Math.min(0.99, sum / expected);
|
|
1645
|
+
emit();
|
|
1646
|
+
},
|
|
1647
|
+
);
|
|
1648
|
+
task.catch(() => {});
|
|
1649
|
+
stripes.push(task);
|
|
1650
|
+
}
|
|
1651
|
+
const data = await withTimeout(
|
|
1652
|
+
firstFulfilled(stripes).catch((err) => {
|
|
1653
|
+
throw _err(
|
|
1654
|
+
"solve_failed",
|
|
1655
|
+
err?.message || "every hashwx worker failed",
|
|
1656
|
+
);
|
|
1657
|
+
}),
|
|
1658
|
+
`hashwx worker #${idx}`,
|
|
1659
|
+
);
|
|
1660
|
+
solutions[idx] = { nonce: data.nonce };
|
|
1661
|
+
log.debug(
|
|
1662
|
+
T("solve"),
|
|
1663
|
+
`hashwx #${idx}: ${data.hashes} hashes in ${data.durationMs}ms`,
|
|
1664
|
+
`compiled=${data.compiled}`,
|
|
1665
|
+
);
|
|
1666
|
+
inFlight[idx] = 0;
|
|
1667
|
+
completed++;
|
|
1668
|
+
emit();
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
await reclaimStripes(stripes);
|
|
1671
|
+
throw err;
|
|
1672
|
+
}
|
|
1673
|
+
return stripes;
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
const hashwxJobs = [];
|
|
1677
|
+
challenges.forEach((ch, idx) => {
|
|
1678
|
+
if (ch.protocol === "hashwx") hashwxJobs.push({ ch, idx });
|
|
1679
|
+
});
|
|
1680
|
+
const hashwxSequence = hashwxJobs.length
|
|
1681
|
+
? hashwxQueued
|
|
1682
|
+
? Promise.all(hashwxJobs.map((job) => solveHashwx(job.ch, job.idx)))
|
|
1683
|
+
: (async () => {
|
|
1684
|
+
for (let i = 0; i < hashwxJobs.length; i++) {
|
|
1685
|
+
const stripes = await solveHashwx(
|
|
1686
|
+
hashwxJobs[i].ch,
|
|
1687
|
+
hashwxJobs[i].idx,
|
|
1688
|
+
);
|
|
1689
|
+
if (i < hashwxJobs.length - 1) await reclaimStripes(stripes);
|
|
1690
|
+
}
|
|
1691
|
+
})()
|
|
1692
|
+
: null;
|
|
1693
|
+
|
|
1267
1694
|
try {
|
|
1268
|
-
await raceAbort(Promise.all(
|
|
1269
|
-
|
|
1695
|
+
await raceAbort(Promise.all([
|
|
1696
|
+
hashwxSequence,
|
|
1697
|
+
...challenges.map((ch, idx) => {
|
|
1270
1698
|
if (ch.protocol === "sha256-pow") {
|
|
1271
1699
|
return withTimeout(
|
|
1272
1700
|
pool.run(ch.payload.salt, ch.payload.target),
|
|
@@ -1293,9 +1721,12 @@
|
|
|
1293
1721
|
emit();
|
|
1294
1722
|
});
|
|
1295
1723
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1724
|
+
|
|
1725
|
+
if (ch.protocol === "hashwx") return null;
|
|
1726
|
+
|
|
1727
|
+
return this.#finalizeInstr(owner).then((out) => {
|
|
1728
|
+
if (out?.__error) solutions[idx] = { error: out.__error };
|
|
1729
|
+
else if (out?.__timeout) solutions[idx] = { timeout: true };
|
|
1299
1730
|
else if (out?.__blocked) solutions[idx] = { blocked: true };
|
|
1300
1731
|
else solutions[idx] = { instr: out };
|
|
1301
1732
|
inFlight[idx] = 0;
|
|
@@ -1303,9 +1734,10 @@
|
|
|
1303
1734
|
emit();
|
|
1304
1735
|
});
|
|
1305
1736
|
}),
|
|
1306
|
-
), signal);
|
|
1737
|
+
]), signal);
|
|
1307
1738
|
} finally {
|
|
1308
1739
|
pool.terminate();
|
|
1740
|
+
if (hashwxPool) hashwxPool.terminate();
|
|
1309
1741
|
}
|
|
1310
1742
|
|
|
1311
1743
|
return solutions;
|
|
@@ -1452,8 +1884,6 @@
|
|
|
1452
1884
|
e.preventDefault();
|
|
1453
1885
|
window.open(
|
|
1454
1886
|
`https://trycap.dev/?${new URLSearchParams(
|
|
1455
|
-
// this attribution is only for our plausible analytics
|
|
1456
|
-
// instance. no personal data is collected.
|
|
1457
1887
|
{
|
|
1458
1888
|
utm_source: "cap_widget",
|
|
1459
1889
|
utm_medium: "referral",
|
|
@@ -1470,7 +1900,7 @@
|
|
|
1470
1900
|
});
|
|
1471
1901
|
|
|
1472
1902
|
this.#trigger.addEventListener("click", () => {
|
|
1473
|
-
if (!this.#trigger.hasAttribute("disabled")) this.solve();
|
|
1903
|
+
if (!this.#trigger.hasAttribute("disabled")) this.solve().catch(() => {});
|
|
1474
1904
|
});
|
|
1475
1905
|
this.#trigger.addEventListener("mousedown", () => {
|
|
1476
1906
|
if (!this.#trigger.hasAttribute("disabled") && this.#hasHaptics) {
|
|
@@ -1486,7 +1916,7 @@
|
|
|
1486
1916
|
) {
|
|
1487
1917
|
e.preventDefault();
|
|
1488
1918
|
e.stopPropagation();
|
|
1489
|
-
this.solve();
|
|
1919
|
+
this.solve().catch(() => {});
|
|
1490
1920
|
}
|
|
1491
1921
|
});
|
|
1492
1922
|
|
|
@@ -1695,6 +2125,11 @@
|
|
|
1695
2125
|
clearTimeout(this.#resetTimer);
|
|
1696
2126
|
this.#resetTimer = null;
|
|
1697
2127
|
}
|
|
2128
|
+
if (this.#speculativeTimer) {
|
|
2129
|
+
clearTimeout(this.#speculativeTimer);
|
|
2130
|
+
this.#speculativeTimer = null;
|
|
2131
|
+
}
|
|
2132
|
+
if (this.#speculative) this.#resetSpeculativeState();
|
|
1698
2133
|
this.token = null;
|
|
1699
2134
|
this.dispatchEvent("reset");
|
|
1700
2135
|
this.#setToken("");
|
|
@@ -1706,6 +2141,7 @@
|
|
|
1706
2141
|
|
|
1707
2142
|
disconnectedCallback() {
|
|
1708
2143
|
this.#abort?.abort();
|
|
2144
|
+
document.removeEventListener("visibilitychange", this.#handleVisibility);
|
|
1709
2145
|
this.removeEventListener("progress", this.boundHandleProgress);
|
|
1710
2146
|
this.removeEventListener("solve", this.boundHandleSolve);
|
|
1711
2147
|
this.removeEventListener("error", this.boundHandleError);
|
|
@@ -1740,6 +2176,7 @@
|
|
|
1740
2176
|
this.#speculativePool = null;
|
|
1741
2177
|
}
|
|
1742
2178
|
if (this.#speculative) {
|
|
2179
|
+
this.#dropChallenge(this.#speculative);
|
|
1743
2180
|
this.#speculative.state = "error";
|
|
1744
2181
|
this.#speculative.notify();
|
|
1745
2182
|
this.#speculative = null;
|