cap-widget 0.1.58 → 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 +10 -0
- package/cap.min.js +1 -1
- package/package.json +1 -1
- package/src/cap-floating.js +1 -3
- package/src/cap.js +404 -175
- package/src/i18n/translations.mjs +869 -0
- package/src/worker.js +123 -43
- package/cap.compat.min.js +0 -408
- package/src/i18n/translations.js +0 -87
package/src/cap.js
CHANGED
|
@@ -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, vp: [window.innerWidth, window.innerHeight] });
|
|
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;
|
|
@@ -326,6 +374,7 @@
|
|
|
326
374
|
const SPECULATIVE_DELAY_MS = 2500;
|
|
327
375
|
const SPECULATIVE_WORKERS = 1;
|
|
328
376
|
const SPECULATIVE_YIELD_MS = 120;
|
|
377
|
+
const MAX_IDLE_REFRESHES = 6;
|
|
329
378
|
|
|
330
379
|
let _sharedWorkerUrl = null;
|
|
331
380
|
|
|
@@ -510,6 +559,8 @@
|
|
|
510
559
|
#interactionHandler = null;
|
|
511
560
|
#i18n = null;
|
|
512
561
|
#abort = null;
|
|
562
|
+
#idleRefreshes = 0;
|
|
563
|
+
#refreshOnVisible = false;
|
|
513
564
|
|
|
514
565
|
get #hasHaptics() {
|
|
515
566
|
return (
|
|
@@ -532,6 +583,9 @@
|
|
|
532
583
|
pendingPromotion: null,
|
|
533
584
|
token: null,
|
|
534
585
|
tokenExpires: null,
|
|
586
|
+
fetchP: null,
|
|
587
|
+
refreshTimer: null,
|
|
588
|
+
redeemError: null,
|
|
535
589
|
|
|
536
590
|
notify() {
|
|
537
591
|
for (const fn of this._listeners) fn();
|
|
@@ -549,10 +603,136 @@
|
|
|
549
603
|
}
|
|
550
604
|
|
|
551
605
|
#resetSpeculativeState() {
|
|
606
|
+
this.#dropChallenge(this.#speculative);
|
|
552
607
|
this.#speculative = this.#makeSpeculativeState();
|
|
553
608
|
this.#attachInteractionListeners();
|
|
554
609
|
}
|
|
555
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
|
+
|
|
556
736
|
#detachInteractionListeners() {
|
|
557
737
|
if (this.#interactionHandler) {
|
|
558
738
|
window.removeEventListener("mousemove", this.#interactionHandler);
|
|
@@ -566,6 +746,7 @@
|
|
|
566
746
|
this.#detachInteractionListeners();
|
|
567
747
|
const handler = () => {
|
|
568
748
|
this.#detachInteractionListeners();
|
|
749
|
+
this.#idleRefreshes = 0;
|
|
569
750
|
this.#onFirstInteraction();
|
|
570
751
|
};
|
|
571
752
|
this.#interactionHandler = handler;
|
|
@@ -589,6 +770,7 @@
|
|
|
589
770
|
}
|
|
590
771
|
|
|
591
772
|
#onFirstInteraction() {
|
|
773
|
+
this.#prefetch();
|
|
592
774
|
if (this.#speculative.state !== "idle") return;
|
|
593
775
|
if (!this.#isVisible()) return;
|
|
594
776
|
this.#speculative.state = "waiting";
|
|
@@ -598,43 +780,26 @@
|
|
|
598
780
|
}
|
|
599
781
|
|
|
600
782
|
async #beginSpeculativeSolve() {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
783
|
+
const spec = this.#speculative;
|
|
784
|
+
if (spec?.state !== "waiting") return;
|
|
785
|
+
spec.state = "fetching";
|
|
786
|
+
spec._t0 = performance.now();
|
|
604
787
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
apiEndpoint = "/";
|
|
608
|
-
}
|
|
609
|
-
if (!apiEndpoint) {
|
|
610
|
-
this.#speculative.state = "idle";
|
|
788
|
+
if (!this.#endpoint()) {
|
|
789
|
+
spec.state = "idle";
|
|
611
790
|
return;
|
|
612
791
|
}
|
|
613
|
-
if (!apiEndpoint.endsWith("/")) apiEndpoint += "/";
|
|
614
792
|
|
|
615
793
|
try {
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
signal: this.#abort?.signal,
|
|
619
|
-
});
|
|
620
|
-
let resp;
|
|
621
|
-
try {
|
|
622
|
-
resp = await raw.json();
|
|
623
|
-
} catch {
|
|
624
|
-
throw new Error("Failed to parse speculative challenge response");
|
|
625
|
-
}
|
|
626
|
-
if (resp.error) throw new Error(resp.error);
|
|
627
|
-
if (!this.#speculative) return;
|
|
628
|
-
|
|
629
|
-
resp._apiEndpoint = apiEndpoint;
|
|
630
|
-
this.#speculative.challengeResp = resp;
|
|
794
|
+
const resp = await this.#fetchChallenge();
|
|
795
|
+
if (this.#speculative !== spec) return;
|
|
631
796
|
|
|
632
797
|
if (resp.format === 2 && Array.isArray(resp.challenges)) {
|
|
633
798
|
if (resp.challenges.some((ch) => ch?.protocol === "hashwx")) {
|
|
634
799
|
getHashwxModule().catch(() => {});
|
|
635
800
|
}
|
|
636
|
-
|
|
637
|
-
|
|
801
|
+
spec.state = "idle";
|
|
802
|
+
spec.notify();
|
|
638
803
|
return;
|
|
639
804
|
}
|
|
640
805
|
|
|
@@ -650,24 +815,25 @@
|
|
|
650
815
|
];
|
|
651
816
|
});
|
|
652
817
|
}
|
|
653
|
-
|
|
654
|
-
|
|
818
|
+
spec.challenges = challenges;
|
|
819
|
+
spec.state = "solving";
|
|
655
820
|
|
|
656
|
-
|
|
821
|
+
spec.solvePromise = this.#speculativeSolveAll(
|
|
657
822
|
challenges,
|
|
823
|
+
spec,
|
|
658
824
|
).catch(() => {
|
|
659
|
-
if (
|
|
660
|
-
|
|
661
|
-
|
|
825
|
+
if (this.#speculative !== spec) return;
|
|
826
|
+
spec.state = "error";
|
|
827
|
+
spec.notify();
|
|
662
828
|
});
|
|
663
829
|
} catch {
|
|
664
|
-
if (
|
|
665
|
-
|
|
666
|
-
|
|
830
|
+
if (this.#speculative !== spec) return;
|
|
831
|
+
spec.state = "error";
|
|
832
|
+
spec.notify();
|
|
667
833
|
}
|
|
668
834
|
}
|
|
669
835
|
|
|
670
|
-
async #speculativeSolveAll(challenges) {
|
|
836
|
+
async #speculativeSolveAll(challenges, spec) {
|
|
671
837
|
_getSharedWorkerUrl();
|
|
672
838
|
|
|
673
839
|
let wasmModule = null;
|
|
@@ -675,7 +841,7 @@
|
|
|
675
841
|
wasmModule = await getWasmModule();
|
|
676
842
|
} catch {}
|
|
677
843
|
|
|
678
|
-
if (
|
|
844
|
+
if (this.#speculative !== spec) return [];
|
|
679
845
|
if (!this.#speculativePool) {
|
|
680
846
|
this.#speculativePool = new WorkerPool(1);
|
|
681
847
|
this.#speculativePool._spawn();
|
|
@@ -688,7 +854,7 @@
|
|
|
688
854
|
let concurrency = SPECULATIVE_WORKERS;
|
|
689
855
|
let promoted = false;
|
|
690
856
|
|
|
691
|
-
|
|
857
|
+
spec.promoteFn = (fullCount) => {
|
|
692
858
|
if (promoted || !this.#speculativePool) return;
|
|
693
859
|
promoted = true;
|
|
694
860
|
concurrency = fullCount;
|
|
@@ -696,15 +862,15 @@
|
|
|
696
862
|
this.#speculativePool._ensureSize(fullCount);
|
|
697
863
|
};
|
|
698
864
|
|
|
699
|
-
if (
|
|
700
|
-
|
|
701
|
-
|
|
865
|
+
if (spec.pendingPromotion !== null) {
|
|
866
|
+
spec.promoteFn(spec.pendingPromotion);
|
|
867
|
+
spec.pendingPromotion = null;
|
|
702
868
|
}
|
|
703
869
|
|
|
704
870
|
let nextIndex = 0;
|
|
705
871
|
|
|
706
872
|
while (nextIndex < total) {
|
|
707
|
-
if (
|
|
873
|
+
if (this.#speculative !== spec || !this.#speculativePool) return results;
|
|
708
874
|
const batchSize = concurrency;
|
|
709
875
|
const batch = [];
|
|
710
876
|
const batchIndices = [];
|
|
@@ -722,7 +888,7 @@
|
|
|
722
888
|
this.#speculativePool
|
|
723
889
|
.run(challenge[0], challenge[1])
|
|
724
890
|
.then((nonce) => {
|
|
725
|
-
|
|
891
|
+
spec.completedCount++;
|
|
726
892
|
return nonce;
|
|
727
893
|
}),
|
|
728
894
|
),
|
|
@@ -739,30 +905,43 @@
|
|
|
739
905
|
}
|
|
740
906
|
}
|
|
741
907
|
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
this.#speculativeRedeem(results);
|
|
908
|
+
if (this.#speculative !== spec) return results;
|
|
909
|
+
spec.results = results;
|
|
910
|
+
spec.state = "redeeming";
|
|
911
|
+
this.#speculativeRedeem(results, spec);
|
|
746
912
|
return results;
|
|
747
913
|
}
|
|
748
914
|
|
|
749
|
-
async #speculativeRedeem(solutions) {
|
|
915
|
+
async #speculativeRedeem(solutions, spec) {
|
|
916
|
+
let answered = false;
|
|
750
917
|
try {
|
|
751
|
-
if (
|
|
752
|
-
const challengeResp =
|
|
918
|
+
if (this.#speculative !== spec) return;
|
|
919
|
+
const challengeResp = spec.challengeResp;
|
|
753
920
|
const apiEndpoint = challengeResp._apiEndpoint;
|
|
754
921
|
if (!apiEndpoint)
|
|
755
922
|
throw _err("missing_endpoint", "speculative redeem: missing apiEndpoint");
|
|
756
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
|
+
|
|
757
938
|
let instrOut = null;
|
|
758
|
-
if (
|
|
759
|
-
instrOut = await
|
|
760
|
-
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
this.#speculative.state = "done";
|
|
765
|
-
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();
|
|
766
945
|
return;
|
|
767
946
|
}
|
|
768
947
|
}
|
|
@@ -778,26 +957,28 @@
|
|
|
778
957
|
signal: this.#abort?.signal,
|
|
779
958
|
});
|
|
780
959
|
|
|
960
|
+
answered = true;
|
|
781
961
|
let resp;
|
|
782
962
|
try {
|
|
783
963
|
resp = await redeemRaw.json();
|
|
784
964
|
} catch {
|
|
785
|
-
throw new Error("Failed to parse
|
|
965
|
+
throw new Error("Failed to parse server response");
|
|
786
966
|
}
|
|
787
967
|
|
|
788
|
-
if (
|
|
968
|
+
if (this.#speculative !== spec) return;
|
|
789
969
|
if (!resp.success)
|
|
790
|
-
throw new Error(resp.error || "
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
} catch {
|
|
798
|
-
if (
|
|
799
|
-
|
|
800
|
-
|
|
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();
|
|
801
982
|
}
|
|
802
983
|
}
|
|
803
984
|
|
|
@@ -970,11 +1151,19 @@
|
|
|
970
1151
|
|
|
971
1152
|
log.debug(T(), `widget ready (workers=${this.#workersCount}, haptics=${this.#hasHaptics})`);
|
|
972
1153
|
this.#attachInteractionListeners();
|
|
1154
|
+
this.#prefetch();
|
|
973
1155
|
this.#updateValidity();
|
|
974
1156
|
|
|
975
1157
|
this.addEventListener("invalid", this.#handleInvalid);
|
|
1158
|
+
document.addEventListener("visibilitychange", this.#handleVisibility);
|
|
976
1159
|
}
|
|
977
1160
|
|
|
1161
|
+
#handleVisibility = () => {
|
|
1162
|
+
if (document.hidden || !this.#refreshOnVisible) return;
|
|
1163
|
+
this.#refreshOnVisible = false;
|
|
1164
|
+
this.#prefetch();
|
|
1165
|
+
};
|
|
1166
|
+
|
|
978
1167
|
#handleInvalid = () => {
|
|
979
1168
|
if (!this.#div) return;
|
|
980
1169
|
try {
|
|
@@ -995,6 +1184,8 @@
|
|
|
995
1184
|
|
|
996
1185
|
this.#enforceCredits();
|
|
997
1186
|
const _solveT0 = performance.now();
|
|
1187
|
+
let usedResp = null;
|
|
1188
|
+
let reported = null;
|
|
998
1189
|
const signal = this.#abort?.signal;
|
|
999
1190
|
log.debug(T("solve"), "starting");
|
|
1000
1191
|
|
|
@@ -1038,6 +1229,10 @@
|
|
|
1038
1229
|
return this.#commitSpeculativeToken();
|
|
1039
1230
|
}
|
|
1040
1231
|
|
|
1232
|
+
if (this.#speculative.redeemError) {
|
|
1233
|
+
throw _err("invalid_solution", this.#speculative.redeemError);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1041
1236
|
if (this.#speculative.state === "done") {
|
|
1042
1237
|
solutions = this.#speculative.results;
|
|
1043
1238
|
challengeResp = this.#speculative.challengeResp;
|
|
@@ -1096,11 +1291,12 @@
|
|
|
1096
1291
|
this.#speculative.challengeResp?.format === 2 &&
|
|
1097
1292
|
Array.isArray(this.#speculative.challengeResp.challenges)
|
|
1098
1293
|
) {
|
|
1099
|
-
challengeResp = this.#
|
|
1100
|
-
|
|
1294
|
+
challengeResp = this.#takeChallenge();
|
|
1295
|
+
usedResp = challengeResp;
|
|
1101
1296
|
solutions = await this.solveChallengesV2(
|
|
1102
1297
|
challengeResp.challenges,
|
|
1103
1298
|
signal,
|
|
1299
|
+
challengeResp,
|
|
1104
1300
|
);
|
|
1105
1301
|
} else {
|
|
1106
1302
|
if (this.#speculative.state !== "done") {
|
|
@@ -1115,27 +1311,22 @@
|
|
|
1115
1311
|
return this.#commitSpeculativeToken();
|
|
1116
1312
|
}
|
|
1117
1313
|
|
|
1314
|
+
if (this.#speculative.redeemError) {
|
|
1315
|
+
throw _err("invalid_solution", this.#speculative.redeemError);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1118
1318
|
solutions = this.#speculative.results;
|
|
1119
1319
|
challengeResp = this.#speculative.challengeResp;
|
|
1120
1320
|
this.dispatchEvent("progress", { progress: 100 });
|
|
1121
1321
|
}
|
|
1122
1322
|
} else {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
challengeResp = cached;
|
|
1126
|
-
this.#speculative.challengeResp = null;
|
|
1127
|
-
} else {
|
|
1128
|
-
const challengeRaw = await capFetch(`${apiEndpoint}challenge`, {
|
|
1129
|
-
method: "POST",
|
|
1130
|
-
signal,
|
|
1131
|
-
});
|
|
1132
|
-
try {
|
|
1133
|
-
challengeResp = await challengeRaw.json();
|
|
1134
|
-
} catch {
|
|
1135
|
-
throw _err("challenge_parse_error", "Failed to parse challenge response from server");
|
|
1136
|
-
}
|
|
1137
|
-
if (challengeResp.error) throw _err("network_error", challengeResp.error);
|
|
1323
|
+
if (!this.#speculative.challengeResp) {
|
|
1324
|
+
await this.#fetchChallenge(signal);
|
|
1138
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;
|
|
1139
1330
|
|
|
1140
1331
|
if (
|
|
1141
1332
|
challengeResp.format === 2 &&
|
|
@@ -1144,6 +1335,7 @@
|
|
|
1144
1335
|
solutions = await this.solveChallengesV2(
|
|
1145
1336
|
challengeResp.challenges,
|
|
1146
1337
|
signal,
|
|
1338
|
+
challengeResp,
|
|
1147
1339
|
);
|
|
1148
1340
|
} else {
|
|
1149
1341
|
const { challenge, token } = challengeResp;
|
|
@@ -1162,29 +1354,47 @@
|
|
|
1162
1354
|
}
|
|
1163
1355
|
}
|
|
1164
1356
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const instrOut = await instrPromise;
|
|
1357
|
+
usedResp = challengeResp;
|
|
1358
|
+
const v2 = challengeResp.format === 2;
|
|
1359
|
+
const instrOut = v2 ? null : await this.#finalizeInstr(challengeResp);
|
|
1170
1360
|
if (signal?.aborted || !this.#speculative) return;
|
|
1171
1361
|
|
|
1172
|
-
|
|
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) {
|
|
1173
1376
|
capFetch(`${apiEndpoint}redeem`, {
|
|
1174
1377
|
method: "POST",
|
|
1175
1378
|
body: JSON.stringify({
|
|
1176
1379
|
token: challengeResp.token,
|
|
1177
1380
|
solutions,
|
|
1178
|
-
...(
|
|
1179
|
-
...(
|
|
1381
|
+
...(!v2 && instrFail.__error && { instr_error: instrFail.__error }),
|
|
1382
|
+
...(!v2 && instrFail.__blocked && { instr_blocked: true }),
|
|
1383
|
+
...(!v2 && instrFail.__timeout && { instr_timeout: true }),
|
|
1180
1384
|
}),
|
|
1181
1385
|
headers: { "Content-Type": "application/json" },
|
|
1182
1386
|
signal,
|
|
1183
1387
|
}).catch(() => {});
|
|
1184
1388
|
|
|
1389
|
+
const csp = instrFail.__error === "csp_blocked";
|
|
1185
1390
|
this.updateUIBlocked(
|
|
1186
|
-
|
|
1187
|
-
|
|
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,
|
|
1188
1398
|
);
|
|
1189
1399
|
this.#trigger.setAttribute(
|
|
1190
1400
|
"aria-label",
|
|
@@ -1193,10 +1403,18 @@
|
|
|
1193
1403
|
"An error occurred, please try again",
|
|
1194
1404
|
),
|
|
1195
1405
|
);
|
|
1196
|
-
const instrCode =
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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";
|
|
1200
1418
|
this.removeEventListener("error", this.boundHandleError);
|
|
1201
1419
|
const errEvent = new CustomEvent("error", {
|
|
1202
1420
|
bubbles: true,
|
|
@@ -1206,9 +1424,15 @@
|
|
|
1206
1424
|
super.dispatchEvent(errEvent);
|
|
1207
1425
|
this.addEventListener("error", this.boundHandleError);
|
|
1208
1426
|
this.executeAttributeCode("onerror", errEvent);
|
|
1209
|
-
log.error(
|
|
1210
|
-
|
|
1211
|
-
|
|
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;
|
|
1212
1436
|
}
|
|
1213
1437
|
|
|
1214
1438
|
const { token } = challengeResp;
|
|
@@ -1265,6 +1489,7 @@
|
|
|
1265
1489
|
return { success: true, token: this.token };
|
|
1266
1490
|
} catch (err) {
|
|
1267
1491
|
if (signal?.aborted || !this.#speculative) return;
|
|
1492
|
+
if (err === reported) throw err;
|
|
1268
1493
|
this.#trigger.setAttribute(
|
|
1269
1494
|
"aria-label",
|
|
1270
1495
|
this.getI18nText(
|
|
@@ -1273,14 +1498,17 @@
|
|
|
1273
1498
|
),
|
|
1274
1499
|
);
|
|
1275
1500
|
this.error(err.message, err.code || "solve_failed");
|
|
1501
|
+
this.#resetSpeculativeState();
|
|
1276
1502
|
throw err;
|
|
1277
1503
|
}
|
|
1278
1504
|
} finally {
|
|
1505
|
+
usedResp?._instr?.stop();
|
|
1279
1506
|
this.#solving = false;
|
|
1280
1507
|
}
|
|
1281
1508
|
}
|
|
1282
1509
|
|
|
1283
|
-
async solveChallengesV2(challenges, signal) {
|
|
1510
|
+
async solveChallengesV2(challenges, signal, resp) {
|
|
1511
|
+
const owner = resp ?? { format: 2, challenges };
|
|
1284
1512
|
const total = challenges.length;
|
|
1285
1513
|
let completed = 0;
|
|
1286
1514
|
|
|
@@ -1496,8 +1724,9 @@
|
|
|
1496
1724
|
|
|
1497
1725
|
if (ch.protocol === "hashwx") return null;
|
|
1498
1726
|
|
|
1499
|
-
return
|
|
1500
|
-
if (out?.
|
|
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 };
|
|
1501
1730
|
else if (out?.__blocked) solutions[idx] = { blocked: true };
|
|
1502
1731
|
else solutions[idx] = { instr: out };
|
|
1503
1732
|
inFlight[idx] = 0;
|
|
@@ -1655,8 +1884,6 @@
|
|
|
1655
1884
|
e.preventDefault();
|
|
1656
1885
|
window.open(
|
|
1657
1886
|
`https://trycap.dev/?${new URLSearchParams(
|
|
1658
|
-
// this attribution is only for our plausible analytics
|
|
1659
|
-
// instance. no personal data is collected.
|
|
1660
1887
|
{
|
|
1661
1888
|
utm_source: "cap_widget",
|
|
1662
1889
|
utm_medium: "referral",
|
|
@@ -1673,7 +1900,7 @@
|
|
|
1673
1900
|
});
|
|
1674
1901
|
|
|
1675
1902
|
this.#trigger.addEventListener("click", () => {
|
|
1676
|
-
if (!this.#trigger.hasAttribute("disabled")) this.solve();
|
|
1903
|
+
if (!this.#trigger.hasAttribute("disabled")) this.solve().catch(() => {});
|
|
1677
1904
|
});
|
|
1678
1905
|
this.#trigger.addEventListener("mousedown", () => {
|
|
1679
1906
|
if (!this.#trigger.hasAttribute("disabled") && this.#hasHaptics) {
|
|
@@ -1689,7 +1916,7 @@
|
|
|
1689
1916
|
) {
|
|
1690
1917
|
e.preventDefault();
|
|
1691
1918
|
e.stopPropagation();
|
|
1692
|
-
this.solve();
|
|
1919
|
+
this.solve().catch(() => {});
|
|
1693
1920
|
}
|
|
1694
1921
|
});
|
|
1695
1922
|
|
|
@@ -1914,6 +2141,7 @@
|
|
|
1914
2141
|
|
|
1915
2142
|
disconnectedCallback() {
|
|
1916
2143
|
this.#abort?.abort();
|
|
2144
|
+
document.removeEventListener("visibilitychange", this.#handleVisibility);
|
|
1917
2145
|
this.removeEventListener("progress", this.boundHandleProgress);
|
|
1918
2146
|
this.removeEventListener("solve", this.boundHandleSolve);
|
|
1919
2147
|
this.removeEventListener("error", this.boundHandleError);
|
|
@@ -1948,6 +2176,7 @@
|
|
|
1948
2176
|
this.#speculativePool = null;
|
|
1949
2177
|
}
|
|
1950
2178
|
if (this.#speculative) {
|
|
2179
|
+
this.#dropChallenge(this.#speculative);
|
|
1951
2180
|
this.#speculative.state = "error";
|
|
1952
2181
|
this.#speculative.notify();
|
|
1953
2182
|
this.#speculative = null;
|