@maggidev/captchashield 1.0.0

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/index.mjs ADDED
@@ -0,0 +1,611 @@
1
+ // src/errors.ts
2
+ var CaptchaShieldError = class _CaptchaShieldError extends Error {
3
+ constructor(message, originalError) {
4
+ super(`[CaptchaShield] ${message}`);
5
+ this.originalError = originalError;
6
+ this.name = "CaptchaShieldError";
7
+ const maybeCaptureStackTrace = Error.captureStackTrace;
8
+ maybeCaptureStackTrace?.(this, _CaptchaShieldError);
9
+ }
10
+ };
11
+
12
+ // src/cookies.ts
13
+ var COOKIE_NAME_RE = /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/;
14
+ function assertValidCookieName(name) {
15
+ if (!name || !COOKIE_NAME_RE.test(name)) {
16
+ throw new CaptchaShieldError(
17
+ `Invalid cookie.name "${name}": must be a valid RFC 6265 token (printable ASCII, no separators or control characters).`
18
+ );
19
+ }
20
+ }
21
+ function assertValidCookieAttributeValue(value, attr) {
22
+ if (value.includes(";")) {
23
+ throw new CaptchaShieldError(
24
+ `Invalid cookie.${attr} "${value}": must not contain semicolons.`
25
+ );
26
+ }
27
+ }
28
+ function hasCookie(name) {
29
+ if (typeof document === "undefined") return false;
30
+ return document.cookie.split(";").some((item) => item.trim().startsWith(`${name}=`));
31
+ }
32
+ function setCookie(options, value) {
33
+ assertValidCookieName(options.name);
34
+ if (options.domain) assertValidCookieAttributeValue(options.domain, "domain");
35
+ assertValidCookieAttributeValue(options.path, "path");
36
+ const attributes = [
37
+ `path=${options.path}`,
38
+ `max-age=${options.maxAgeSeconds}`,
39
+ options.domain ? `domain=${options.domain}` : "",
40
+ options.secure ? "secure" : "",
41
+ options.sameSite ? `samesite=${options.sameSite}` : ""
42
+ ].filter(Boolean).join("; ");
43
+ document.cookie = `${options.name}=${encodeURIComponent(value)}; ${attributes}`;
44
+ }
45
+ function clearCookie(options) {
46
+ assertValidCookieName(options.name);
47
+ if (options.domain) assertValidCookieAttributeValue(options.domain, "domain");
48
+ assertValidCookieAttributeValue(options.path, "path");
49
+ const attributes = [
50
+ `expires=Thu, 01 Jan 1970 00:00:00 GMT`,
51
+ `path=${options.path}`,
52
+ options.domain ? `domain=${options.domain}` : "",
53
+ options.secure ? "secure" : "",
54
+ options.sameSite ? `samesite=${options.sameSite}` : ""
55
+ ].filter(Boolean).join("; ");
56
+ document.cookie = `${options.name}=; ${attributes}`;
57
+ }
58
+ function deriveCookieName(baseName, options) {
59
+ if (!options.useScopePrefix) return baseName;
60
+ const scope = options.scopeId ?? deriveScopeId();
61
+ return scope ? `${baseName}_${scope}` : baseName;
62
+ }
63
+ function deriveScopeId() {
64
+ if (typeof window === "undefined") return void 0;
65
+ try {
66
+ return window.location.hostname.replace(/\./g, "_");
67
+ } catch {
68
+ return void 0;
69
+ }
70
+ }
71
+
72
+ // src/renderer.ts
73
+ function renderDefaultModal({ challengeContainer, config, close }) {
74
+ const overlay = document.createElement("div");
75
+ const panel = document.createElement("div");
76
+ const header = document.createElement("div");
77
+ const title = document.createElement("h2");
78
+ const closeButton = document.createElement("button");
79
+ const body = document.createElement("p");
80
+ const helper = document.createElement("p");
81
+ overlay.setAttribute("role", "presentation");
82
+ overlay.className = config.modal.styles.overlayClass;
83
+ overlay.setAttribute("data-captcha-shield", "overlay");
84
+ panel.className = config.modal.styles.panelClass;
85
+ panel.setAttribute("role", "dialog");
86
+ panel.setAttribute("aria-modal", "true");
87
+ panel.setAttribute("aria-label", config.modal.ariaLabel);
88
+ panel.setAttribute("data-captcha-shield", "panel");
89
+ header.className = "captcha-shield__header";
90
+ title.className = config.modal.styles.titleClass;
91
+ title.textContent = config.modal.copy.title;
92
+ closeButton.type = "button";
93
+ closeButton.className = "captcha-shield__close";
94
+ closeButton.setAttribute("aria-label", "Close verification dialog");
95
+ closeButton.textContent = "Close";
96
+ closeButton.addEventListener("click", close);
97
+ body.className = config.modal.styles.bodyClass;
98
+ body.textContent = config.modal.copy.body;
99
+ helper.className = config.modal.styles.helperClass;
100
+ helper.textContent = config.modal.copy.helperText;
101
+ header.appendChild(title);
102
+ header.appendChild(closeButton);
103
+ panel.appendChild(header);
104
+ panel.appendChild(body);
105
+ panel.appendChild(challengeContainer);
106
+ panel.appendChild(helper);
107
+ overlay.appendChild(panel);
108
+ let injectedStyle = null;
109
+ if (config.modal.injectDefaultStyle) {
110
+ injectedStyle = injectStyle(defaultStyleSheet(config.modal.styles.customCss));
111
+ } else if (config.modal.styles.customCss.trim().length > 0) {
112
+ injectedStyle = injectStyle(config.modal.styles.customCss);
113
+ }
114
+ return {
115
+ root: overlay,
116
+ destroy: () => {
117
+ overlay.remove();
118
+ injectedStyle?.remove();
119
+ }
120
+ };
121
+ }
122
+ function injectStyle(css) {
123
+ if (!css.trim()) return null;
124
+ const style = document.createElement("style");
125
+ style.setAttribute("data-captcha-shield-style", "true");
126
+ style.textContent = css;
127
+ document.head.appendChild(style);
128
+ return style;
129
+ }
130
+ function defaultStyleSheet(customCss) {
131
+ const base = `
132
+ .captcha-shield__overlay { position: fixed; inset: 0; background: rgba(15, 23, 42, 0.48); display: flex; align-items: center; justify-content: center; padding: 20px; z-index: 9999; }
133
+ .captcha-shield__panel { width: min(480px, 100%); background: #ffffff; color: #111827; border: 1px solid #d4d4d8; border-radius: 10px; box-shadow: 0 6px 24px rgba(15, 23, 42, 0.12); padding: 20px; font-family: inherit; display: flex; flex-direction: column; gap: 16px; }
134
+ .captcha-shield__header { display: flex; align-items: start; justify-content: space-between; gap: 16px; }
135
+ .captcha-shield__title { margin: 0; font-size: 1.125rem; line-height: 1.35; font-weight: 650; }
136
+ .captcha-shield__close { appearance: none; border: 1px solid #d4d4d8; background: #ffffff; color: #374151; border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 0.875rem; font-weight: 600; cursor: pointer; }
137
+ .captcha-shield__close:hover { background: #f4f4f5; }
138
+ .captcha-shield__body { margin: 0; line-height: 1.6; color: #1f2937; }
139
+ .captcha-shield__helper { margin: 0; font-size: 0.9375rem; color: #52525b; }
140
+ [data-captcha-shield="challenge"] { min-height: 70px; display: flex; align-items: center; justify-content: center; }
141
+ `;
142
+ return `${base}${customCss ?? ""}`;
143
+ }
144
+
145
+ // src/validation.ts
146
+ var ABSOLUTE_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
147
+ var LOCALHOST_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
148
+ function validateRequestEndpoint(endpoint, fieldName) {
149
+ const trimmed = endpoint.trim();
150
+ if (!trimmed) {
151
+ throw new CaptchaShieldError(`Configuration field "${fieldName}" cannot be empty.`);
152
+ }
153
+ if (trimmed.startsWith("//")) {
154
+ throw new CaptchaShieldError(`Configuration field "${fieldName}" must not use protocol-relative URLs.`);
155
+ }
156
+ if (!ABSOLUTE_URL_PATTERN.test(trimmed)) {
157
+ return trimmed;
158
+ }
159
+ let url;
160
+ try {
161
+ url = new URL(trimmed);
162
+ } catch {
163
+ throw new CaptchaShieldError(`Configuration field "${fieldName}" must be a valid URL.`);
164
+ }
165
+ if (url.protocol === "https:") {
166
+ return url.toString();
167
+ }
168
+ if (url.protocol === "http:" && LOCALHOST_HOSTNAMES.has(url.hostname)) {
169
+ return url.toString();
170
+ }
171
+ throw new CaptchaShieldError(
172
+ `Configuration field "${fieldName}" must use HTTPS. Plain HTTP is only allowed for localhost development.`
173
+ );
174
+ }
175
+ function validateTurnstileScriptUrl(scriptUrl) {
176
+ let url;
177
+ try {
178
+ url = new URL(scriptUrl);
179
+ } catch {
180
+ throw new CaptchaShieldError("turnstileScriptUrl must be a valid absolute URL.");
181
+ }
182
+ if (url.protocol !== "https:") {
183
+ throw new CaptchaShieldError("turnstileScriptUrl must use HTTPS.");
184
+ }
185
+ if (url.hostname !== "challenges.cloudflare.com" || !url.pathname.startsWith("/turnstile/")) {
186
+ throw new CaptchaShieldError(
187
+ "turnstileScriptUrl must point to the official Cloudflare Turnstile host."
188
+ );
189
+ }
190
+ return url.toString();
191
+ }
192
+
193
+ // src/turnstile.ts
194
+ var turnstileLoaders = /* @__PURE__ */ new Map();
195
+ function ensureTurnstile(scriptUrl, integrity) {
196
+ requireDom();
197
+ if (window.turnstile) {
198
+ assertTurnstile(integrity);
199
+ return Promise.resolve(window.turnstile);
200
+ }
201
+ const existing = turnstileLoaders.get(scriptUrl);
202
+ if (existing) return existing;
203
+ const loader = loadTurnstileScript(scriptUrl, integrity).catch((err) => {
204
+ turnstileLoaders.delete(scriptUrl);
205
+ throw err;
206
+ });
207
+ turnstileLoaders.set(scriptUrl, loader);
208
+ return loader;
209
+ }
210
+ function loadTurnstileScript(scriptUrl, integrity) {
211
+ return new Promise((resolve, reject) => {
212
+ const script = document.createElement("script");
213
+ script.src = validateTurnstileScriptUrl(scriptUrl);
214
+ script.async = true;
215
+ if (integrity.scriptIntegrity) {
216
+ script.integrity = integrity.scriptIntegrity;
217
+ script.crossOrigin = "anonymous";
218
+ }
219
+ script.onload = () => {
220
+ if (window.turnstile) {
221
+ try {
222
+ assertTurnstile(integrity);
223
+ resolve(window.turnstile);
224
+ } catch (err) {
225
+ reject(err);
226
+ }
227
+ } else {
228
+ reject(new CaptchaShieldError('Turnstile script loaded but "window.turnstile" is missing.'));
229
+ }
230
+ };
231
+ script.onerror = () => reject(new CaptchaShieldError(`Failed to load Turnstile script from ${scriptUrl}`));
232
+ document.head.appendChild(script);
233
+ });
234
+ }
235
+ function assertTurnstile(integrity) {
236
+ if (!integrity.verifyTurnstileGlobal) return;
237
+ const t = window.turnstile;
238
+ if (!t || typeof t.render !== "function") {
239
+ throw new CaptchaShieldError("Global integrity check failed: window.turnstile.render is missing.");
240
+ }
241
+ }
242
+ function requireDom() {
243
+ if (typeof window === "undefined" || typeof document === "undefined") {
244
+ throw new CaptchaShieldError("Library requires a browser DOM environment.");
245
+ }
246
+ }
247
+
248
+ // src/network.ts
249
+ async function fetchWithTimeout(url, init, timeoutMs) {
250
+ const timeoutController = new AbortController();
251
+ const merged = mergeSignals([init.signal, timeoutController.signal].filter(Boolean));
252
+ const timer = setTimeout(() => timeoutController.abort(new CaptchaShieldError("Request timed out")), timeoutMs);
253
+ try {
254
+ const signal = merged ? merged.signal : init.signal;
255
+ return await fetch(url, { ...init, signal });
256
+ } finally {
257
+ clearTimeout(timer);
258
+ merged?.cleanup();
259
+ }
260
+ }
261
+ function isExpectedStatus(status, expected) {
262
+ if (typeof expected === "function") {
263
+ return expected(status);
264
+ }
265
+ return status === expected;
266
+ }
267
+ function mergeSignals(signals) {
268
+ if (signals.length === 0) return void 0;
269
+ const controller = new AbortController();
270
+ const abortHandler = (event) => {
271
+ const signal = event.target;
272
+ controller.abort(signal?.reason);
273
+ };
274
+ signals.forEach((sig) => {
275
+ if (sig.aborted) {
276
+ if (!controller.signal.aborted) controller.abort(sig.reason);
277
+ } else {
278
+ sig.addEventListener("abort", abortHandler, { once: true });
279
+ }
280
+ });
281
+ const cleanup = () => {
282
+ signals.forEach((sig) => sig.removeEventListener("abort", abortHandler));
283
+ };
284
+ if (controller.signal.aborted) {
285
+ cleanup();
286
+ return { signal: controller.signal, cleanup: () => {
287
+ } };
288
+ }
289
+ controller.signal.addEventListener("abort", cleanup, { once: true });
290
+ return { signal: controller.signal, cleanup };
291
+ }
292
+
293
+ // src/verification.ts
294
+ async function runStatusCheck(status) {
295
+ if (!status.endpoint) return;
296
+ const response = await fetchWithTimeout(status.endpoint, { method: "GET" }, status.timeoutMs);
297
+ if (!isExpectedStatus(response.status, status.expectedStatus)) {
298
+ throw new CaptchaShieldError(`Status check failed with status: ${response.status}`);
299
+ }
300
+ }
301
+ async function verifyTokenWithServer(token, verify) {
302
+ if (!verify.endpoint) return true;
303
+ let lastError = null;
304
+ for (let attempt = 0; attempt <= verify.retries; attempt++) {
305
+ try {
306
+ const response = await requestVerification(token, verify);
307
+ if (isExpectedStatus(response.status, verify.expectedStatus)) {
308
+ return true;
309
+ }
310
+ lastError = new CaptchaShieldError(`Verification failed with status: ${response.status}`);
311
+ } catch (err) {
312
+ lastError = err instanceof Error ? err : new CaptchaShieldError(String(err));
313
+ }
314
+ }
315
+ if (lastError) {
316
+ throw lastError;
317
+ }
318
+ return false;
319
+ }
320
+ async function requestVerification(token, verify) {
321
+ const init = {
322
+ method: verify.method,
323
+ headers: verify.headers
324
+ };
325
+ const body = verify.buildBody(token);
326
+ if (body) {
327
+ init.body = body;
328
+ }
329
+ return fetchWithTimeout(verify.endpoint ?? "", init, verify.timeoutMs);
330
+ }
331
+
332
+ // src/shield.ts
333
+ var DEFAULT_SCRIPT_URL = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
334
+ var DEFAULT_COOKIE_NAME = "captchaShieldVerified";
335
+ var DEFAULT_MODAL_COPY = {
336
+ title: "Please verify you are human",
337
+ body: "Complete the Cloudflare Turnstile check to continue. A short-lived cookie will skip this step until it expires.",
338
+ helperText: "No data is stored beyond the verification cookie and the Turnstile token."
339
+ };
340
+ var DEFAULT_MODAL_STYLES = {
341
+ overlayClass: "captcha-shield__overlay",
342
+ panelClass: "captcha-shield__panel",
343
+ titleClass: "captcha-shield__title",
344
+ bodyClass: "captcha-shield__body",
345
+ helperClass: "captcha-shield__helper",
346
+ customCss: ""
347
+ };
348
+ var DEFAULT_MODAL = {
349
+ copy: DEFAULT_MODAL_COPY,
350
+ styles: DEFAULT_MODAL_STYLES,
351
+ ariaLabel: "Human verification dialog",
352
+ closeOnVerify: true,
353
+ injectDefaultStyle: true
354
+ };
355
+ var DEFAULT_COOKIE = {
356
+ name: DEFAULT_COOKIE_NAME,
357
+ maxAgeSeconds: 60 * 60 * 24,
358
+ path: "/",
359
+ sameSite: "Lax",
360
+ secure: true,
361
+ scopeId: void 0,
362
+ useScopePrefix: false,
363
+ trustClientCookie: false
364
+ };
365
+ var DEFAULT_VERIFY = {
366
+ endpoint: void 0,
367
+ method: "POST",
368
+ headers: { "content-type": "application/json" },
369
+ timeoutMs: 5e3,
370
+ retries: 1,
371
+ buildBody: (token) => JSON.stringify({ token }),
372
+ expectedStatus: (status) => status >= 200 && status < 300
373
+ };
374
+ var DEFAULT_STATUS = {
375
+ endpoint: void 0,
376
+ timeoutMs: 3e3,
377
+ expectedStatus: (status) => status >= 200 && status < 400
378
+ };
379
+ var DEFAULT_INTEGRITY = {
380
+ scriptIntegrity: void 0,
381
+ verifyTurnstileGlobal: true,
382
+ enforceChallengePresence: true,
383
+ monitorChallengeRemoval: false
384
+ };
385
+ function createCaptchaShield(config) {
386
+ const resolved = resolveConfig(config);
387
+ let verified = false;
388
+ let token = null;
389
+ let rendererHandle = null;
390
+ let challengeTarget = null;
391
+ let widgetId = null;
392
+ let opening = null;
393
+ let lastTurnstile = null;
394
+ let verifying = null;
395
+ let integrityWatch = null;
396
+ let currentOpenId = 0;
397
+ const isAlreadyVerified = () => verified || resolved.cookie.trustClientCookie && hasCookie(resolved.cookie.name);
398
+ const close = () => {
399
+ currentOpenId++;
400
+ if (widgetId && lastTurnstile?.remove) {
401
+ lastTurnstile.remove(widgetId);
402
+ widgetId = null;
403
+ }
404
+ if (rendererHandle) {
405
+ rendererHandle.destroy?.();
406
+ rendererHandle.root.remove();
407
+ rendererHandle = null;
408
+ }
409
+ if (integrityWatch) {
410
+ integrityWatch.stop();
411
+ integrityWatch = null;
412
+ }
413
+ challengeTarget = null;
414
+ };
415
+ const reset = () => {
416
+ token = null;
417
+ verified = false;
418
+ clearCookie(resolved.cookie);
419
+ if (widgetId && lastTurnstile?.reset) {
420
+ lastTurnstile.reset(widgetId);
421
+ }
422
+ widgetId = null;
423
+ };
424
+ const destroy = () => {
425
+ reset();
426
+ close();
427
+ };
428
+ const renderModal = (turnstile) => {
429
+ const challengeContainer = document.createElement("div");
430
+ challengeContainer.setAttribute("data-captcha-shield", "challenge");
431
+ const handle = resolved.render ? resolved.render({ challengeContainer, config: resolved, close }) : renderDefaultModal({ challengeContainer, config: resolved, close });
432
+ if (!handle.root.contains(challengeContainer)) {
433
+ handle.root.appendChild(challengeContainer);
434
+ }
435
+ document.body.appendChild(handle.root);
436
+ lastTurnstile = turnstile;
437
+ rendererHandle = handle;
438
+ challengeTarget = challengeContainer;
439
+ if (resolved.integrity.monitorChallengeRemoval) {
440
+ integrityWatch = startIntegrityWatch(challengeContainer, () => {
441
+ handleError("Challenge container was removed. Possible tampering detected.");
442
+ destroy();
443
+ });
444
+ }
445
+ };
446
+ const open = async () => {
447
+ const cookieVerified = resolved.cookie.trustClientCookie && hasCookie(resolved.cookie.name);
448
+ if (verified || cookieVerified) {
449
+ verified = true;
450
+ return { status: "already-verified", reason: cookieVerified ? "cookie" : "session" };
451
+ }
452
+ if (opening) {
453
+ return opening;
454
+ }
455
+ const myId = ++currentOpenId;
456
+ opening = (async () => {
457
+ requireDom();
458
+ await runStatusCheck(resolved.statusCheck);
459
+ if (myId !== currentOpenId) throw new CaptchaShieldError("Operation cancelled by close()");
460
+ const turnstile = await ensureTurnstile(resolved.turnstileScriptUrl, resolved.integrity);
461
+ if (myId !== currentOpenId) throw new CaptchaShieldError("Operation cancelled by close()");
462
+ renderModal(turnstile);
463
+ if (!challengeTarget) {
464
+ if (resolved.integrity.enforceChallengePresence) {
465
+ throw new CaptchaShieldError("Failed to mount a challenge container.");
466
+ }
467
+ challengeTarget = document.createElement("div");
468
+ }
469
+ widgetId = turnstile.render(challengeTarget, {
470
+ sitekey: resolved.siteKey,
471
+ action: resolved.action,
472
+ cData: resolved.cData,
473
+ callback: handleVerified,
474
+ "error-callback": (message) => handleError(message ?? "Turnstile error"),
475
+ "timeout-callback": () => handleError("Turnstile timed out")
476
+ });
477
+ return { status: "rendered" };
478
+ })();
479
+ try {
480
+ return await opening;
481
+ } finally {
482
+ opening = null;
483
+ }
484
+ };
485
+ const handleVerified = (turnstileToken) => {
486
+ token = turnstileToken;
487
+ const finalize = () => {
488
+ verified = true;
489
+ if (resolved.cookie.trustClientCookie) {
490
+ setCookie(resolved.cookie, "1");
491
+ }
492
+ resolved.onVerified?.(turnstileToken);
493
+ if (resolved.modal.closeOnVerify) {
494
+ close();
495
+ }
496
+ };
497
+ const resetWidget = () => {
498
+ if (widgetId && lastTurnstile?.reset) {
499
+ lastTurnstile.reset(widgetId);
500
+ }
501
+ };
502
+ const executeVerification = async () => {
503
+ if (!resolved.verify.endpoint) {
504
+ finalize();
505
+ return;
506
+ }
507
+ const ok = await verifyTokenWithServer(turnstileToken, resolved.verify);
508
+ if (!ok) {
509
+ resetWidget();
510
+ handleError("Server verification rejected token");
511
+ return;
512
+ }
513
+ finalize();
514
+ };
515
+ if (!verifying) {
516
+ verifying = executeVerification().catch((err) => {
517
+ resetWidget();
518
+ handleError(normalizeErrorMessage(err));
519
+ }).finally(() => {
520
+ verifying = null;
521
+ });
522
+ }
523
+ };
524
+ const handleError = (message) => {
525
+ resolved.onError?.(new CaptchaShieldError(message));
526
+ };
527
+ return {
528
+ open,
529
+ close,
530
+ reset,
531
+ destroy,
532
+ isVerified: () => isAlreadyVerified(),
533
+ getToken: () => token
534
+ };
535
+ }
536
+ function resolveConfig(config) {
537
+ if (!config.siteKey) {
538
+ throw new CaptchaShieldError('Configuration missing required "siteKey".');
539
+ }
540
+ const modalCopy = { ...DEFAULT_MODAL_COPY, ...config.modal?.copy ?? {} };
541
+ const modalStyles = { ...DEFAULT_MODAL_STYLES, ...config.modal?.styles ?? {} };
542
+ const modal = {
543
+ ...DEFAULT_MODAL,
544
+ ...config.modal ?? {},
545
+ copy: modalCopy,
546
+ styles: modalStyles
547
+ };
548
+ const cookieBase = config.cookie ?? {};
549
+ const cookie = { ...DEFAULT_COOKIE, ...cookieBase };
550
+ cookie.name = deriveCookieName(cookieBase.name ?? DEFAULT_COOKIE_NAME, cookie);
551
+ const verify = { ...DEFAULT_VERIFY, ...config.verify ?? {} };
552
+ const statusCheck = { ...DEFAULT_STATUS, ...config.statusCheck ?? {} };
553
+ const integrity = { ...DEFAULT_INTEGRITY, ...config.integrity ?? {} };
554
+ if (config.verify?.method && config.verify.method !== "POST") {
555
+ throw new CaptchaShieldError('verify.method only supports "POST".');
556
+ }
557
+ const turnstileScriptUrl = validateTurnstileScriptUrl(config.turnstileScriptUrl ?? DEFAULT_SCRIPT_URL);
558
+ if (verify.endpoint) {
559
+ verify.endpoint = validateRequestEndpoint(verify.endpoint, "verify.endpoint");
560
+ }
561
+ if (statusCheck.endpoint) {
562
+ statusCheck.endpoint = validateRequestEndpoint(statusCheck.endpoint, "statusCheck.endpoint");
563
+ }
564
+ return {
565
+ siteKey: config.siteKey,
566
+ action: config.action,
567
+ cData: config.cData,
568
+ turnstileScriptUrl,
569
+ modal,
570
+ cookie,
571
+ verify,
572
+ statusCheck,
573
+ integrity,
574
+ render: config.render,
575
+ onVerified: config.onVerified,
576
+ onError: config.onError
577
+ };
578
+ }
579
+ function startIntegrityWatch(element, onTamper) {
580
+ if (!element.isConnected) {
581
+ onTamper();
582
+ return { stop: () => void 0 };
583
+ }
584
+ const observer = new MutationObserver(() => {
585
+ if (!element.isConnected) {
586
+ onTamper();
587
+ observer.disconnect();
588
+ }
589
+ });
590
+ observer.observe(document.body, { childList: true, subtree: true });
591
+ return {
592
+ observer,
593
+ stop: () => observer.disconnect()
594
+ };
595
+ }
596
+ function normalizeErrorMessage(error) {
597
+ if (error instanceof Error) {
598
+ return error.message.replace(/^\[CaptchaShield\]\s*/, "");
599
+ }
600
+ return String(error);
601
+ }
602
+ export {
603
+ CaptchaShieldError,
604
+ DEFAULT_COOKIE_NAME,
605
+ DEFAULT_SCRIPT_URL,
606
+ clearCookie,
607
+ createCaptchaShield,
608
+ hasCookie,
609
+ setCookie
610
+ };
611
+ //# sourceMappingURL=index.mjs.map