@zaptcha/widget 1.0.0 → 1.0.2
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/dist/zaptcha-widget.js +745 -0
- package/package.json +10 -5
- package/src/zaptcha-widget.js +0 -485
- package/src/zaptcha.js +0 -249
- package/src/zaptcha_bg.wasm +0 -0
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "@zaptcha/widget",
|
|
3
|
-
"version": "1.0.
|
|
2
|
+
"name": "@zaptcha/widget",
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main":
|
|
5
|
+
"main": "./dist/zaptcha-widget.js",
|
|
6
6
|
"types": "./src/zaptcha.d.ts",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
8
|
+
"dist",
|
|
9
|
+
"src/zaptcha.d.ts"
|
|
9
10
|
],
|
|
10
11
|
"exports": {
|
|
11
|
-
".": "./
|
|
12
|
+
".": "./dist/zaptcha-widget.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "node scripts/build.js",
|
|
16
|
+
"prepare": "node scripts/build.js"
|
|
12
17
|
}
|
|
13
18
|
}
|
package/src/zaptcha-widget.js
DELETED
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* <zero-captcha> — CAPTCHA Web Component with WASM Proof-of-Work
|
|
3
|
-
*
|
|
4
|
-
* The worker is bundled inline — only this one file needs to be distributed.
|
|
5
|
-
* WASM paths are resolved automatically relative to this module's URL.
|
|
6
|
-
*
|
|
7
|
-
* Attributes:
|
|
8
|
-
* base-url="https://api.example.com" required — backend base URL
|
|
9
|
-
* config-id="AAAA" required — base64url-encoded u32
|
|
10
|
-
* wasm-glue="./pkg/zaptcha.js" optional — override WASM glue path
|
|
11
|
-
* wasm-bin="./pkg/zaptcha_bg.wasm" optional — override WASM binary path
|
|
12
|
-
*
|
|
13
|
-
* CSS custom properties (set on a parent element to theme the widget):
|
|
14
|
-
* --zaptcha-bg background color
|
|
15
|
-
* --zaptcha-border border color
|
|
16
|
-
* --zaptcha-border-focus focus ring color
|
|
17
|
-
* --zaptcha-text primary text color
|
|
18
|
-
* --zaptcha-text-muted muted / branding text color
|
|
19
|
-
* --zaptcha-accent spinner / interactive accent color
|
|
20
|
-
* --zaptcha-success checkmark fill color
|
|
21
|
-
* --zaptcha-shadow box shadow
|
|
22
|
-
* --zaptcha-radius border radius
|
|
23
|
-
*
|
|
24
|
-
* Events dispatched on host element:
|
|
25
|
-
* zaptcha-success -> detail: { token: string }
|
|
26
|
-
* zaptcha-fail -> detail: { reason: string }
|
|
27
|
-
* zaptcha-reset -> detail: {}
|
|
28
|
-
*
|
|
29
|
-
* Public methods:
|
|
30
|
-
* el.reset()
|
|
31
|
-
*
|
|
32
|
-
* Usage:
|
|
33
|
-
* <zero-captcha base-url="https://api.example.com" config-id="AAAA"></zero-captcha>
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
// ── Default WASM paths (relative to this module) ──────────────────────────
|
|
37
|
-
|
|
38
|
-
const _wasmGlue = new URL('./zaptcha.js', import.meta.url).href;
|
|
39
|
-
const _wasmBin = new URL('./zaptcha_bg.wasm', import.meta.url).href;
|
|
40
|
-
|
|
41
|
-
const WORKER_SRC = `
|
|
42
|
-
let wasmModule = null;
|
|
43
|
-
let wasmReady = false;
|
|
44
|
-
let wasmGlueUrl = null;
|
|
45
|
-
let wasmBinUrl = null;
|
|
46
|
-
|
|
47
|
-
async function initWasm() {
|
|
48
|
-
if (wasmReady) return;
|
|
49
|
-
if (!wasmGlueUrl) throw new Error("WASM paths not provided");
|
|
50
|
-
const mod = await import(wasmGlueUrl);
|
|
51
|
-
if (typeof mod.default === "function") await mod.default(wasmBinUrl);
|
|
52
|
-
wasmModule = mod;
|
|
53
|
-
wasmReady = true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function runSolve(baseUrl, configId) {
|
|
57
|
-
await initWasm();
|
|
58
|
-
|
|
59
|
-
const challengeRes = await fetch(baseUrl + "/" + configId + "/challenge");
|
|
60
|
-
if (!challengeRes.ok) throw new Error("Challenge request failed: HTTP " + challengeRes.status);
|
|
61
|
-
|
|
62
|
-
const { token, challenge_count, salt_length, difficulty } = await challengeRes.json();
|
|
63
|
-
|
|
64
|
-
self.__zaptchaProgressCb = (index, nonce) => {
|
|
65
|
-
self.postMessage({ type: "progress", index, nonce, total: challenge_count });
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
let solutions;
|
|
69
|
-
try {
|
|
70
|
-
solutions = wasmModule.solve_challenge(
|
|
71
|
-
token, difficulty, challenge_count, salt_length, self.__zaptchaProgressCb,
|
|
72
|
-
);
|
|
73
|
-
} finally {
|
|
74
|
-
delete self.__zaptchaProgressCb;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const redeemRes = await fetch(baseUrl + "/" + configId + "/redeem", {
|
|
78
|
-
method: "POST",
|
|
79
|
-
headers: { "Content-Type": "application/json" },
|
|
80
|
-
body: JSON.stringify({ token, solutions: Array.from(solutions) }),
|
|
81
|
-
});
|
|
82
|
-
if (!redeemRes.ok) throw new Error("Redeem request failed: HTTP " + redeemRes.status);
|
|
83
|
-
|
|
84
|
-
const { message } = await redeemRes.json();
|
|
85
|
-
if (!message) throw new Error("Server did not return a token");
|
|
86
|
-
return message;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
self.onmessage = async function ({ data }) {
|
|
90
|
-
if (data.type === "init") {
|
|
91
|
-
wasmGlueUrl = data.wasmGlue;
|
|
92
|
-
wasmBinUrl = data.wasmBin;
|
|
93
|
-
try {
|
|
94
|
-
await initWasm();
|
|
95
|
-
self.postMessage({ type: "ready" });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
self.postMessage({ type: "fail", reason: err.message });
|
|
98
|
-
}
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (data.type === "solve") {
|
|
103
|
-
try {
|
|
104
|
-
const token = await runSolve(data.baseUrl, data.configId);
|
|
105
|
-
self.postMessage({ type: "success", token });
|
|
106
|
-
} catch (err) {
|
|
107
|
-
self.postMessage({ type: "fail", reason: err.message });
|
|
108
|
-
}
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
`;
|
|
113
|
-
|
|
114
|
-
const STYLE = `
|
|
115
|
-
:host {
|
|
116
|
-
display: inline-block;
|
|
117
|
-
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
118
|
-
-webkit-font-smoothing: antialiased;
|
|
119
|
-
-moz-osx-font-smoothing: grayscale;
|
|
120
|
-
|
|
121
|
-
/*
|
|
122
|
-
* Map public --zaptcha-* custom properties to internal vars with
|
|
123
|
-
* sensible light-mode fallbacks.
|
|
124
|
-
*/
|
|
125
|
-
--zap-bg: var(--zaptcha-bg, #ffffff);
|
|
126
|
-
--zap-border: var(--zaptcha-border, #e5e7eb);
|
|
127
|
-
--zap-border-hover: var(--zaptcha-border-hover, #d1d5db);
|
|
128
|
-
--zap-border-focus: var(--zaptcha-border-focus, #4f46e5);
|
|
129
|
-
--zap-text: var(--zaptcha-text, #374151);
|
|
130
|
-
--zap-text-muted: var(--zaptcha-text-muted, #9ca3af);
|
|
131
|
-
--zap-accent: var(--zaptcha-accent, #4f46e5);
|
|
132
|
-
--zap-success: var(--zaptcha-success, #10b981);
|
|
133
|
-
--zap-shadow: var(--zaptcha-shadow, 0 2px 6px rgba(0,0,0,0.04));
|
|
134
|
-
--zap-radius: var(--zaptcha-radius, 8px);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.widget {
|
|
138
|
-
background: var(--zap-bg);
|
|
139
|
-
border: 1px solid var(--zap-border);
|
|
140
|
-
border-radius: var(--zap-radius);
|
|
141
|
-
box-shadow: var(--zap-shadow);
|
|
142
|
-
width: 210px; /* Слегка шире для баланса, но все еще очень компактно */
|
|
143
|
-
box-sizing: border-box;
|
|
144
|
-
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.widget:hover {
|
|
148
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.row {
|
|
152
|
-
display: flex;
|
|
153
|
-
align-items: center;
|
|
154
|
-
gap: 10px;
|
|
155
|
-
padding: 0 12px;
|
|
156
|
-
height: 48px;
|
|
157
|
-
cursor: pointer;
|
|
158
|
-
user-select: none;
|
|
159
|
-
box-sizing: border-box;
|
|
160
|
-
border-radius: var(--zap-radius);
|
|
161
|
-
outline: none;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
.row:focus-visible {
|
|
165
|
-
box-shadow: 0 0 0 2px var(--zap-bg), 0 0 0 4px var(--zap-border-focus);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.row[aria-checked="true"] {
|
|
169
|
-
cursor: default;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/* Checkbox */
|
|
173
|
-
.box {
|
|
174
|
-
width: 22px;
|
|
175
|
-
height: 22px;
|
|
176
|
-
flex-shrink: 0;
|
|
177
|
-
position: relative;
|
|
178
|
-
display: flex;
|
|
179
|
-
align-items: center;
|
|
180
|
-
justify-content: center;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
.box-border {
|
|
184
|
-
position: absolute;
|
|
185
|
-
inset: 0;
|
|
186
|
-
border: 1.5px solid var(--zap-border);
|
|
187
|
-
border-radius: 4px;
|
|
188
|
-
background: var(--zap-bg);
|
|
189
|
-
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.row:hover:not([aria-checked="true"]) .box-border {
|
|
193
|
-
border-color: var(--zap-border-hover);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.box.done .box-border {
|
|
197
|
-
background: var(--zap-success);
|
|
198
|
-
border-color: var(--zap-success);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
.box-tick { display: none; position: relative; z-index: 1; }
|
|
202
|
-
.box.done .box-tick { display: block; animation: zap-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
|
|
203
|
-
|
|
204
|
-
@keyframes zap-pop {
|
|
205
|
-
0% { transform: scale(0.5); opacity: 0; }
|
|
206
|
-
100% { transform: scale(1); opacity: 1; }
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/* Spinner */
|
|
210
|
-
.box-spinner { display: none; position: absolute; inset: 0; }
|
|
211
|
-
.box.spinning .box-border { border-color: transparent; background: transparent; }
|
|
212
|
-
.box.spinning .box-spinner {
|
|
213
|
-
display: flex;
|
|
214
|
-
align-items: center;
|
|
215
|
-
justify-content: center;
|
|
216
|
-
}
|
|
217
|
-
.box-spinner::after {
|
|
218
|
-
content: '';
|
|
219
|
-
display: block;
|
|
220
|
-
width: 18px;
|
|
221
|
-
height: 18px;
|
|
222
|
-
border: 2px solid var(--zap-border);
|
|
223
|
-
border-top-color: var(--zap-accent);
|
|
224
|
-
border-radius: 50%;
|
|
225
|
-
animation: zap-spin 0.6s linear infinite;
|
|
226
|
-
}
|
|
227
|
-
@keyframes zap-spin { to { transform: rotate(360deg); } }
|
|
228
|
-
|
|
229
|
-
/* Label */
|
|
230
|
-
.label {
|
|
231
|
-
color: var(--zap-text);
|
|
232
|
-
font-size: 13.5px;
|
|
233
|
-
font-weight: 500;
|
|
234
|
-
flex: 1;
|
|
235
|
-
white-space: nowrap;
|
|
236
|
-
overflow: hidden;
|
|
237
|
-
text-overflow: ellipsis;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/* Branding */
|
|
241
|
-
.branding {
|
|
242
|
-
display: flex;
|
|
243
|
-
flex-direction: column;
|
|
244
|
-
align-items: flex-end;
|
|
245
|
-
justify-content: center;
|
|
246
|
-
flex-shrink: 0;
|
|
247
|
-
gap: 1px;
|
|
248
|
-
}
|
|
249
|
-
.branding strong {
|
|
250
|
-
font-size: 9px;
|
|
251
|
-
font-weight: 700;
|
|
252
|
-
letter-spacing: 0.05em;
|
|
253
|
-
text-transform: uppercase;
|
|
254
|
-
color: var(--zap-text-muted);
|
|
255
|
-
}
|
|
256
|
-
.branding a {
|
|
257
|
-
font-size: 9px;
|
|
258
|
-
color: var(--zap-text-muted);
|
|
259
|
-
text-decoration: none;
|
|
260
|
-
transition: color 0.2s;
|
|
261
|
-
}
|
|
262
|
-
.branding a:hover {
|
|
263
|
-
color: var(--zap-text);
|
|
264
|
-
}
|
|
265
|
-
`;
|
|
266
|
-
|
|
267
|
-
class ZeroCaptcha extends HTMLElement {
|
|
268
|
-
|
|
269
|
-
static get observedAttributes() { return []; }
|
|
270
|
-
|
|
271
|
-
constructor() {
|
|
272
|
-
super();
|
|
273
|
-
this._shadow = this.attachShadow({ mode: "open" });
|
|
274
|
-
this._worker = null;
|
|
275
|
-
this._blobUrl = null;
|
|
276
|
-
/** @type {"idle"|"solving"|"done"|"error"} */
|
|
277
|
-
this._state = "idle";
|
|
278
|
-
this._progress = { done: 0, total: 1 };
|
|
279
|
-
this._render();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
connectedCallback() {
|
|
283
|
-
this._bindEvents();
|
|
284
|
-
this._startWorker();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
disconnectedCallback() {
|
|
288
|
-
this._destroyWorker();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
reset() {
|
|
292
|
-
this._destroyWorker();
|
|
293
|
-
this._state = "idle";
|
|
294
|
-
this._progress = { done: 0, total: 1 };
|
|
295
|
-
this._render();
|
|
296
|
-
this._bindEvents();
|
|
297
|
-
this._startWorker();
|
|
298
|
-
this.dispatchEvent(new CustomEvent("zaptcha-reset", { bubbles: true, detail: {} }));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
_startWorker() {
|
|
302
|
-
try {
|
|
303
|
-
const externalUrl = this.getAttribute("worker-url");
|
|
304
|
-
if (externalUrl) {
|
|
305
|
-
this._worker = new Worker(externalUrl, { type: "module" });
|
|
306
|
-
} else {
|
|
307
|
-
const blob = new Blob([WORKER_SRC], { type: "text/javascript" });
|
|
308
|
-
this._blobUrl = URL.createObjectURL(blob);
|
|
309
|
-
this._worker = new Worker(this._blobUrl, { type: "module" });
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
this._worker.onmessage = (e) => this._onWorkerMsg(e.data);
|
|
313
|
-
this._worker.onerror = (e) => this._onFail(`Worker error: ${e.message}`);
|
|
314
|
-
|
|
315
|
-
const wasmGlue = this.getAttribute("wasm-glue") ?? _wasmGlue;
|
|
316
|
-
const wasmBin = this.getAttribute("wasm-bin") ?? _wasmBin;
|
|
317
|
-
|
|
318
|
-
this._worker.postMessage({ type: "init", wasmGlue, wasmBin });
|
|
319
|
-
} catch (err) {
|
|
320
|
-
this._worker = null;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
_destroyWorker() {
|
|
325
|
-
this._worker?.terminate();
|
|
326
|
-
this._worker = null;
|
|
327
|
-
if (this._blobUrl) {
|
|
328
|
-
URL.revokeObjectURL(this._blobUrl);
|
|
329
|
-
this._blobUrl = null;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
_onWorkerMsg(msg) {
|
|
334
|
-
switch (msg.type) {
|
|
335
|
-
case "ready":
|
|
336
|
-
break;
|
|
337
|
-
|
|
338
|
-
case "progress":
|
|
339
|
-
this._progress.done = (msg.index ?? 0) + 1;
|
|
340
|
-
this._progress.total = msg.total ?? 1;
|
|
341
|
-
this.dispatchEvent(new CustomEvent("zaptcha-progress", {
|
|
342
|
-
bubbles: true,
|
|
343
|
-
composed: true,
|
|
344
|
-
detail: {
|
|
345
|
-
index: msg.index,
|
|
346
|
-
nonce: msg.nonce,
|
|
347
|
-
total: msg.total,
|
|
348
|
-
pct: Math.round(((msg.index ?? 0) + 1) / (msg.total ?? 1) * 100),
|
|
349
|
-
},
|
|
350
|
-
}));
|
|
351
|
-
break;
|
|
352
|
-
|
|
353
|
-
case "success":
|
|
354
|
-
this._state = "done";
|
|
355
|
-
this._render();
|
|
356
|
-
this.dispatchEvent(new CustomEvent("zaptcha-success", {
|
|
357
|
-
bubbles: true,
|
|
358
|
-
composed: true,
|
|
359
|
-
detail: { token: msg.token },
|
|
360
|
-
}));
|
|
361
|
-
break;
|
|
362
|
-
|
|
363
|
-
case "fail":
|
|
364
|
-
this._onFail(msg.reason || "Unknown error");
|
|
365
|
-
break;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
_render() {
|
|
371
|
-
const solving = this._state === "solving";
|
|
372
|
-
const done = this._state === "done";
|
|
373
|
-
|
|
374
|
-
const label = done ? "Verified" :
|
|
375
|
-
solving ? "Verifying\u2026" :
|
|
376
|
-
"I'm not a robot";
|
|
377
|
-
|
|
378
|
-
this._shadow.innerHTML = `
|
|
379
|
-
<style>${STYLE}</style>
|
|
380
|
-
<div class="widget" part="widget">
|
|
381
|
-
<div class="row"
|
|
382
|
-
id="row"
|
|
383
|
-
tabindex="${done ? -1 : 0}"
|
|
384
|
-
role="checkbox"
|
|
385
|
-
aria-checked="${done}"
|
|
386
|
-
aria-label="${label}">
|
|
387
|
-
|
|
388
|
-
<div class="box ${solving ? "spinning" : done ? "done" : ""}" id="box" part="checkbox">
|
|
389
|
-
<div class="box-border"></div>
|
|
390
|
-
<div class="box-spinner"></div>
|
|
391
|
-
<svg class="box-tick" width="12" height="9" viewBox="0 0 12 9" fill="none" aria-hidden="true">
|
|
392
|
-
<polyline points="1.5,4.5 4.5,7.5 10.5,1.5"
|
|
393
|
-
stroke="white" stroke-width="2"
|
|
394
|
-
stroke-linecap="round" stroke-linejoin="round"/>
|
|
395
|
-
</svg>
|
|
396
|
-
</div>
|
|
397
|
-
|
|
398
|
-
<span class="label" part="label">${label}</span>
|
|
399
|
-
|
|
400
|
-
<div class="branding" part="branding">
|
|
401
|
-
<strong>Zaptcha</strong>
|
|
402
|
-
<a href="#" tabindex="-1">Privacy</a>
|
|
403
|
-
</div>
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
406
|
-
`;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ── Event binding ───────────────────────────────────────────────────────
|
|
410
|
-
|
|
411
|
-
_bindEvents() {
|
|
412
|
-
const row = this._shadow.getElementById("row");
|
|
413
|
-
if (!row) return;
|
|
414
|
-
row.addEventListener("click", () => this._onRowClick());
|
|
415
|
-
row.addEventListener("keydown", (e) => {
|
|
416
|
-
if (e.key === " " || e.key === "Enter") {
|
|
417
|
-
e.preventDefault();
|
|
418
|
-
this._onRowClick();
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
_onRowClick() {
|
|
424
|
-
if (this._state !== "idle") return;
|
|
425
|
-
this._startSolve();
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// ── Solving flow ────────────────────────────────────────────────────────
|
|
429
|
-
|
|
430
|
-
_startSolve() {
|
|
431
|
-
this._state = "solving";
|
|
432
|
-
this._progress = { done: 0, total: 1 };
|
|
433
|
-
this._render();
|
|
434
|
-
|
|
435
|
-
if (!this._worker) {
|
|
436
|
-
this._onFail("Worker unavailable");
|
|
437
|
-
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
const baseUrl = this.getAttribute("base-url") ?? "";
|
|
441
|
-
const configId = this.getAttribute("config-id") ?? "";
|
|
442
|
-
|
|
443
|
-
if (!baseUrl || !configId) {
|
|
444
|
-
this._onFail("Attributes base-url and config-id are required");
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
this._worker.postMessage({ type: "solve", baseUrl, configId });
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
_onFail(reason) {
|
|
452
|
-
this._state = "error";
|
|
453
|
-
this._render();
|
|
454
|
-
|
|
455
|
-
this.dispatchEvent(new CustomEvent("zaptcha-fail", {
|
|
456
|
-
bubbles: true,
|
|
457
|
-
composed: true,
|
|
458
|
-
detail: { reason },
|
|
459
|
-
}));
|
|
460
|
-
|
|
461
|
-
setTimeout(() => this.reset(), 3000);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
customElements.define("zero-captcha", ZeroCaptcha);
|
|
466
|
-
export default ZeroCaptcha;
|
|
467
|
-
export { ZeroCaptcha };
|
|
468
|
-
|
|
469
|
-
// ── Standalone worker factory ─────────────────────────────────────────────
|
|
470
|
-
|
|
471
|
-
export function createWorker({ wasmGlue = _wasmGlue, wasmBin = _wasmBin } = {}) {
|
|
472
|
-
const blob = new Blob([WORKER_SRC], { type: "text/javascript" });
|
|
473
|
-
const blobUrl = URL.createObjectURL(blob);
|
|
474
|
-
const worker = new Worker(blobUrl, { type: "module" });
|
|
475
|
-
|
|
476
|
-
worker.addEventListener("message", function onReady(e) {
|
|
477
|
-
if (e.data?.type === "ready" || e.data?.type === "fail") {
|
|
478
|
-
URL.revokeObjectURL(blobUrl);
|
|
479
|
-
worker.removeEventListener("message", onReady);
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
worker.postMessage({ type: "init", wasmGlue, wasmBin });
|
|
484
|
-
return worker;
|
|
485
|
-
}
|