/* global Audio, CSS, CustomEvent, Element, HTMLElement, MutationObserver, URL, clearInterval, clearTimeout, document, fetch, window */ (function () { "use strict"; var RUNTIME_KEY = "__wrnexusCaptchaRuntime"; var existingRuntime = window[RUNTIME_KEY]; if (existingRuntime && typeof existingRuntime.scan === "function") { window.__wrnexusRuntimes = window.__wrnexusRuntimes || {}; window.__wrnexusRuntimes.captcha = { mount: existingRuntime.scan, unmount: existingRuntime.unmount || function () {}, }; existingRuntime.scan(document); return; } var states = new WeakMap(); var scriptPromises = new Map(); var instanceCounter = 0; function bool(value, fallback) { if (value === undefined || value === null || value === "") return fallback; return value === true || value === "true" || value === "1"; } function numberInRange(value, fallback, minimum, maximum) { var parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.max(minimum, Math.min(maximum, Math.round(parsed))); } function normalizeSize(value, compact) { if (compact) return "compact"; var normalized = String(value || "normal") .trim() .toLowerCase(); if (normalized === "compact" || normalized === "small" || normalized === "sm") return "compact"; if (normalized === "big" || normalized === "large" || normalized === "lg") return "big"; return "normal"; } function commaList(value) { return String(value || "") .split(",") .map(function (item) { return item.trim().toLowerCase(); }) .filter(Boolean); } function text(root, selector, value) { var element = root.querySelector(selector); if (element) element.textContent = value == null ? "" : String(value); } function show(element, visible) { if (!element) return; element.hidden = !visible; } function setBusy(root, busy) { root.setAttribute("aria-busy", busy ? "true" : "false"); } function config(root) { var data = root.dataset; var responseField = data.captchaResponseField || data.captchaName || "wrn-captcha-response"; var compact = bool(data.captchaCompact, false); var size = normalizeSize(data.captchaSize, compact); return { provider: data.captchaProvider || "self-hosted", siteKey: data.captchaSiteKey || "", type: data.captchaType || "alphanumeric", action: data.captchaAction || "form-submit", presentation: data.captchaPresentation || "visual", difficulty: data.captchaDifficulty || "normal", disturbance: numberInRange(data.captchaDisturbance, 50, 25, 75), imageStyle: data.captchaImageStyle || "random", allowedStyles: commaList(data.captchaAllowedStyles), excludedStyles: commaList(data.captchaExcludedStyles), randomizeStyle: bool(data.captchaRandomizeStyle, false), locale: data.captchaLocale || "en", size: size, endpoint: data.captchaEndpoint || "/__wrnexus/captcha/challenge", verifyEndpoint: data.captchaVerifyEndpoint || "/__wrnexus/captcha/verify", responseField: responseField, autoLoad: bool(data.captchaAutoLoad, true), autoVerify: bool(data.captchaAutoVerify, false), showVerify: bool(data.captchaShowVerify, true), showRefresh: bool(data.captchaShowRefresh, true), showAudio: bool(data.captchaShowAudio, true), showListen: bool(data.captchaShowListen, true), showStatus: bool(data.captchaShowStatus, true), disabled: bool(data.captchaDisabled, false), required: bool(data.captchaRequired, true), resetOnError: bool(data.captchaResetOnError, true), compact: compact, requiredMessage: data.captchaRequiredMessage || "Please complete the security check.", incorrectMessage: data.captchaIncorrectMessage || "That answer was not correct. Try again.", expiredMessage: data.captchaExpiredMessage || "This challenge expired. Load a new one.", networkMessage: data.captchaNetworkMessage || "The verification service is unavailable. Try again.", }; } function eventDetail(state, extra) { var challenge = state.challenge; return { component: "Captcha", provider: state.config.provider, type: challenge && challenge.type ? challenge.type : state.config.type, action: state.config.action, disturbance: state.config.disturbance, imageStyle: challenge && challenge.metadata && challenge.metadata.imageStyle ? challenge.metadata.imageStyle : state.config.imageStyle, requestedImageStyle: challenge && challenge.metadata && challenge.metadata.requestedImageStyle ? challenge.metadata.requestedImageStyle : state.config.imageStyle, imageStylePool: challenge && challenge.metadata && challenge.metadata.imageStylePool ? challenge.metadata.imageStylePool : state.config.allowedStyles, size: state.config.size, status: state.status, challengeId: challenge && challenge.id ? challenge.id : "", responseToken: state.responseToken, expiresAt: challenge && challenge.expiresAt ? challenge.expiresAt : null, extra: extra || null, }; } function emit(state, name, extra) { state.root.dispatchEvent( new CustomEvent(name, { bubbles: true, detail: eventDetail(state, extra), }), ); } function setResponseToken(state, token) { state.responseToken = token || ""; var input = state.root.querySelector("[data-captcha-response]"); if (input) { input.name = state.config.responseField; input.value = state.responseToken; } } function isNotRobot(state) { return Boolean( (state.challenge && state.challenge.type === "not-robot") || (!state.challenge && state.config.type === "not-robot"), ); } function updateNotRobotState(state) { var panel = state.root.querySelector("[data-captcha-not-robot]"); var button = state.root.querySelector("[data-captcha-not-robot-button]"); var empty = state.root.querySelector("[data-captcha-not-robot-empty]"); var spinner = state.root.querySelector("[data-captcha-not-robot-spinner]"); var check = state.root.querySelector("[data-captcha-not-robot-check]"); var control = state.root.querySelector("[data-captcha-not-robot-control]"); var label = state.root.querySelector("[data-captcha-not-robot-label]"); var active = isNotRobot(state); var pending = state.notRobotPending || state.status === "verifying"; var verified = state.status === "verified"; show(panel, active && state.status !== "loading" && state.status !== "idle"); if (!active) return; if (button) { button.disabled = state.config.disabled || pending || verified || state.status === "expired"; button.setAttribute("aria-pressed", verified ? "true" : "false"); } show(empty, !pending && !verified); show(spinner, pending && !verified); show(check, verified); if (control) control.dataset.verified = verified ? "true" : "false"; if (label) { label.textContent = verified ? "Verified" : pending ? "Checking…" : "I'm not a robot"; } } function setStatus(state, status, message) { state.status = status; state.root.dataset.captchaStatus = status; setBusy(state.root, status === "loading" || status === "verifying"); var loading = state.root.querySelector("[data-captcha-loading]"); var challenge = state.root.querySelector("[data-captcha-challenge]"); var success = state.root.querySelector("[data-captcha-success]"); var error = state.root.querySelector("[data-captcha-error]"); var badge = state.root.querySelector("[data-captcha-verified-badge]"); var verifyButton = state.root.querySelector("[data-captcha-verify]"); var verifyIcon = state.root.querySelector("[data-captcha-verify-icon]"); var verifySpinner = state.root.querySelector("[data-captcha-verify-spinner]"); var verifyLabel = state.root.querySelector("[data-captcha-verify-label]"); var answer = state.root.querySelector("[data-captcha-answer]"); show(loading, status === "loading"); show( challenge, status !== "loading" && status !== "idle" && state.config.provider !== "turnstile" && state.config.provider !== "recaptcha" && state.config.provider !== "hcaptcha", ); show(badge, status === "verified"); if (state.config.showStatus) { show(success, status === "verified"); show(error, Boolean(message) && status !== "verified"); } else { show(success, false); show(error, false); } if (message && status === "verified") text(state.root, "[data-captcha-success-message]", message); if (message && status !== "verified") text(state.root, "[data-captcha-error-message]", message); if (answer) { answer.disabled = state.config.disabled || status === "verified" || status === "expired" || status === "loading" || status === "verifying"; answer.setAttribute("aria-invalid", status === "incorrect" ? "true" : "false"); } if (verifyButton) { verifyButton.disabled = state.config.disabled || status === "loading" || status === "verifying" || status === "verified" || status === "expired"; } show(verifyIcon, status !== "verifying"); show(verifySpinner, status === "verifying"); if (verifyLabel) verifyLabel.textContent = status === "verifying" ? "Verifying…" : "Verify"; updateNotRobotState(state); updateControls(state); } function clearTimer(state) { if (state.timer) { window.clearInterval(state.timer); state.timer = null; } } function updateCountdown(state) { var countdown = state.root.querySelector("[data-captcha-countdown]"); if (!countdown || !state.challenge || !state.challenge.expiresAt) { show(countdown, false); return; } var remaining = Math.max(0, Math.ceil((Number(state.challenge.expiresAt) - Date.now()) / 1000)); countdown.textContent = remaining + "s"; show(countdown, remaining > 0 && state.status !== "verified"); if (remaining <= 0 && state.status !== "verified" && state.status !== "expired") { clearTimer(state); setResponseToken(state, ""); setStatus(state, "expired", state.config.expiredMessage); emit(state, "expired"); } } function startTimer(state) { clearTimer(state); if (!state.challenge || !state.challenge.expiresAt) return; updateCountdown(state); state.timer = window.setInterval(function () { updateCountdown(state); }, 1000); } function resetUi(state) { if (state.audioPlayer) { state.audioPlayer.pause(); state.audioPlayer.removeAttribute("src"); state.audioPlayer.load(); state.audioPlayer = null; } if (state.notRobotTimer) { window.clearTimeout(state.notRobotTimer); state.notRobotTimer = null; } state.notRobotPending = false; state.challengeLoadedAt = 0; state.answer = ""; state.selections = []; state.challenge = null; setResponseToken(state, ""); var answer = state.root.querySelector("[data-captcha-answer]"); var items = state.root.querySelector("[data-captcha-items]"); var imageWrap = state.root.querySelector("[data-captcha-image-wrap]"); var providerMount = state.root.querySelector("[data-captcha-provider-mount]"); var honeypot = state.root.querySelector("[data-captcha-honeypot]"); var notRobot = state.root.querySelector("[data-captcha-not-robot]"); if (answer) answer.value = ""; if (items) items.replaceChildren(); show(items, false); show(imageWrap, false); show(notRobot, false); if (honeypot) { honeypot.value = ""; honeypot.name = ""; } if ( providerMount && state.config.provider !== "turnstile" && state.config.provider !== "recaptcha" && state.config.provider !== "hcaptcha" ) { providerMount.replaceChildren(); show(providerMount, false); } } function updateControls(state) { var challenge = state.challenge; var audio = state.root.querySelector("[data-captcha-audio]"); var audioAlternative = state.root.querySelector("[data-captcha-audio-alternative]"); var refresh = state.root.querySelector("[data-captcha-refresh]"); var verify = state.root.querySelector("[data-captcha-verify]"); var footer = state.root.querySelector("[data-captcha-footer]"); var isExternal = state.config.provider === "turnstile" || state.config.provider === "recaptcha" || state.config.provider === "hcaptcha"; var notRobot = isNotRobot(state); var locked = state.config.disabled || state.status === "loading" || state.status === "verifying" || state.notRobotPending; show(footer, !notRobot); show( audio, !notRobot && state.config.showAudio && state.config.showListen && Boolean(challenge && challenge.audioUrl) && state.status !== "verified", ); show( audioAlternative, !notRobot && state.config.showAudio && Boolean(challenge && challenge.type === "image") && state.status !== "verified", ); show(refresh, !notRobot && state.config.showRefresh && state.status !== "verified"); show(verify, !notRobot && state.config.showVerify && !isExternal); if (audio) audio.disabled = locked; if (audioAlternative) audioAlternative.disabled = locked; if (refresh) refresh.disabled = locked; if (verify) verify.disabled = locked || state.status === "verified" || state.status === "expired"; } function renderItems(state, challenge) { var container = state.root.querySelector("[data-captcha-items]"); var template = state.root.querySelector("[data-captcha-item-template]"); if ( !container || !template || !Array.isArray(challenge.items) || challenge.items.length === 0 ) { show(container, false); return; } container.replaceChildren(); container.setAttribute("aria-label", challenge.prompt || "Select matching images"); challenge.items.forEach(function (item, index) { var fragment = template.content.cloneNode(true); var button = fragment.querySelector("button"); var image = fragment.querySelector("[data-captcha-item-image]"); if (!button || !image) return; button.dataset.captchaItemId = item.id; button.setAttribute("aria-label", item.alt || "Challenge tile " + (index + 1)); image.src = item.image; image.alt = ""; button.addEventListener("click", function () { toggleItem(state, item.id, button); }); container.appendChild(fragment); }); show(container, true); } function toggleItem(state, itemId, button) { if (state.config.disabled || state.status !== "ready") return; var index = state.selections.indexOf(itemId); var selected = index >= 0; if (selected) { state.selections.splice(index, 1); } else { var maximum = Number( state.challenge && state.challenge.maxSelections ? state.challenge.maxSelections : 0, ); if (maximum > 0 && state.selections.length >= maximum) { var removed = state.selections.shift(); var previousButton = state.root.querySelector( '[data-captcha-item-id="' + CSS.escape(removed) + '"]', ); if (previousButton) { previousButton.setAttribute("aria-pressed", "false"); show(previousButton.querySelector("[data-captcha-item-check]"), false); } } state.selections.push(itemId); } var nowSelected = state.selections.indexOf(itemId) >= 0; button.setAttribute("aria-pressed", nowSelected ? "true" : "false"); show(button.querySelector("[data-captcha-item-check]"), nowSelected); emit(state, "input", { selections: state.selections.slice() }); var minimum = Number( state.challenge && state.challenge.minSelections ? state.challenge.minSelections : 0, ); if (state.config.autoVerify && minimum > 0 && state.selections.length >= minimum) verify(state); } function renderChallenge(state, challenge) { state.challenge = challenge; state.challengeLoadedAt = Date.now(); state.notRobotPending = false; state.answer = ""; state.selections = []; var notRobot = challenge.type === "not-robot"; state.root.dataset.captchaType = challenge.type || state.config.type; state.root.dataset.captchaResolvedImageStyle = challenge.metadata && challenge.metadata.imageStyle ? String(challenge.metadata.imageStyle) : state.config.imageStyle; text( state.root, "[data-captcha-prompt]", challenge.prompt || "Complete the security challenge.", ); var promptRow = state.root.querySelector("[data-captcha-prompt-row]"); var notRobotPanel = state.root.querySelector("[data-captcha-not-robot]"); show(promptRow, !notRobot); show(notRobotPanel, notRobot); var imageWrap = state.root.querySelector("[data-captcha-image-wrap]"); var image = state.root.querySelector("[data-captcha-image]"); if (!notRobot && challenge.image && image) { image.src = challenge.image; show(imageWrap, true); } else { if (image) image.removeAttribute("src"); show(imageWrap, false); } if (notRobot) { var items = state.root.querySelector("[data-captcha-items]"); if (items) items.replaceChildren(); show(items, false); } else { renderItems(state, challenge); } var answerWrap = state.root.querySelector("[data-captcha-answer-wrap]"); var answer = state.root.querySelector("[data-captcha-answer]"); var needsAnswer = !notRobot && !challenge.items && challenge.inputMode !== "none"; show(answerWrap, needsAnswer); if (answer) { answer.value = ""; answer.inputMode = challenge.inputMode === "numeric" ? "numeric" : "text"; answer.setAttribute("aria-invalid", "false"); } var honeypot = state.root.querySelector("[data-captcha-honeypot]"); if (honeypot) honeypot.name = challenge.honeypotField || ""; setStatus(state, "ready", ""); startTimer(state); emit(state, "challenge", challenge); emit(state, "ready"); if ( state.config.autoVerify && !notRobot && challenge.inputMode === "none" && !challenge.items ) { window.setTimeout(function () { verify(state); }, 0); } } async function createChallenge(state, requestedPresentation) { if (state.config.disabled) return; if ( state.config.provider === "turnstile" || state.config.provider === "recaptcha" || state.config.provider === "hcaptcha" ) { await mountExternal(state); return; } clearTimer(state); resetUi(state); setStatus(state, "loading", ""); try { var response = await fetch(state.config.endpoint, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", accept: "application/json", }, body: JSON.stringify({ siteKey: state.config.siteKey, action: state.config.action, type: state.config.type, presentation: requestedPresentation || state.config.presentation, difficulty: state.config.difficulty, disturbance: state.config.disturbance, imageStyle: state.config.imageStyle, allowedStyles: state.config.allowedStyles, excludedStyles: state.config.excludedStyles, randomizeStyle: state.config.randomizeStyle, locale: state.config.locale, responseField: state.config.responseField, }), }); var result = await response.json().catch(function () { return null; }); if (!response.ok || !result || !result.id) { throw new Error(result && result.message ? result.message : "Challenge request failed"); } renderChallenge(state, result); } catch (error) { setStatus(state, "network-error", state.config.networkMessage); emit(state, "error", { error: error instanceof Error ? error.message : String(error) }); } } async function verify(state) { state.notRobotPending = false; if ( state.config.disabled || state.status === "loading" || state.status === "verifying" || state.status === "expired" || state.status === "verified" ) { updateNotRobotState(state); updateControls(state); return; } if (!state.challenge) { await createChallenge(state); return; } var answerInput = state.root.querySelector("[data-captcha-answer]"); var honeypotInput = state.root.querySelector("[data-captcha-honeypot]"); state.answer = answerInput ? answerInput.value : ""; setStatus(state, "verifying", ""); emit(state, "verify"); try { var response = await fetch(state.challenge.verifyUrl || state.config.verifyEndpoint, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", accept: "application/json", }, body: JSON.stringify({ challengeId: state.challenge.id, action: state.config.action, answer: state.answer, selections: state.selections, honeypot: honeypotInput ? honeypotInput.value : "", timingToken: state.challenge.timingToken || "", }), }); var result = await response.json().catch(function () { return null; }); if (!response.ok || !result || !result.success) { setResponseToken(state, ""); var expired = Boolean(result && result.code === "expired"); setStatus( state, expired ? "expired" : "incorrect", expired ? state.config.expiredMessage : result && result.message ? result.message : state.config.incorrectMessage, ); emit(state, "failure", result || null); return; } clearTimer(state); setResponseToken(state, result.responseToken || ""); setStatus(state, "verified", "Verification completed."); emit(state, "success", result); } catch (error) { setStatus(state, "network-error", state.config.networkMessage); emit(state, "error", { error: error instanceof Error ? error.message : String(error) }); } } function verifyNotRobot(state) { if (state.config.disabled || state.notRobotPending || state.status !== "ready") return; if (!state.challenge || state.challenge.type !== "not-robot") { createChallenge(state); return; } var configuredMinimum = Number( state.challenge.metadata && state.challenge.metadata.minCompletionMs !== undefined ? state.challenge.metadata.minCompletionMs : 800, ); var minimum = Number.isFinite(configuredMinimum) ? Math.max(0, configuredMinimum) : 800; var elapsed = Math.max(0, Date.now() - state.challengeLoadedAt); var remaining = Math.max(0, minimum - elapsed); state.notRobotPending = true; updateNotRobotState(state); updateControls(state); emit(state, "input", { checked: true }); var complete = function () { state.notRobotTimer = null; state.notRobotPending = false; verify(state); }; if (remaining > 0) state.notRobotTimer = window.setTimeout(complete, remaining); else complete(); } function providerDefinition(provider) { if (provider === "turnstile") { return { url: "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit", globalName: "turnstile", }; } if (provider === "recaptcha") { return { url: "https://www.google.com/recaptcha/api.js?render=explicit", globalName: "grecaptcha", }; } if (provider === "hcaptcha") { return { url: "https://js.hcaptcha.com/1/api.js?render=explicit", globalName: "hcaptcha", }; } return null; } function loadProviderScript(definition) { if (window[definition.globalName]) return Promise.resolve(window[definition.globalName]); if (scriptPromises.has(definition.url)) return scriptPromises.get(definition.url); var promise = new Promise(function (resolve, reject) { var script = document.querySelector('script[src="' + definition.url + '"]'); if (!script) { script = document.createElement("script"); script.src = definition.url; script.async = true; script.defer = true; document.head.appendChild(script); } script.addEventListener( "load", function () { if (window[definition.globalName]) resolve(window[definition.globalName]); else reject(new Error("CAPTCHA provider did not initialize")); }, { once: true }, ); script.addEventListener( "error", function () { reject(new Error("CAPTCHA provider script failed")); }, { once: true }, ); }); scriptPromises.set(definition.url, promise); return promise; } async function mountExternal(state) { var definition = providerDefinition(state.config.provider); if (!definition) return; if (!state.config.siteKey) { setStatus(state, "provider-error", "A public site key is required."); return; } var mount = state.root.querySelector("[data-captcha-provider-mount]"); if (!mount) return; show(mount, true); setStatus(state, "loading", ""); try { var api = await loadProviderScript(definition); if (state.externalWidgetId !== null && typeof api.reset === "function") { setResponseToken(state, ""); api.reset(state.externalWidgetId); setStatus(state, "ready", ""); emit(state, "ready"); return; } mount.replaceChildren(); state.externalApi = api; state.externalWidgetId = api.render(mount, { sitekey: state.config.siteKey, theme: "auto", size: state.config.size === "compact" ? "compact" : state.config.provider === "turnstile" ? "flexible" : "normal", action: state.config.action, callback: function (token) { setResponseToken(state, token); setStatus(state, "verified", "Verification completed."); emit(state, "success"); }, "expired-callback": function () { setResponseToken(state, ""); setStatus(state, "expired", state.config.expiredMessage); emit(state, "expired"); }, "error-callback": function (providerError) { setResponseToken(state, ""); setStatus(state, "provider-error", state.config.networkMessage); emit(state, "error", { error: providerError || "provider-error" }); }, }); setStatus(state, "ready", ""); emit(state, "ready"); } catch (error) { setStatus(state, "provider-error", state.config.networkMessage); emit(state, "error", { error: error instanceof Error ? error.message : String(error) }); } } function playAudio(state) { if (!state.challenge || !state.challenge.audioUrl || state.config.disabled) return; if (state.audioPlayer) { state.audioPlayer.pause(); state.audioPlayer.currentTime = 0; } var audio = new Audio(); state.audioPlayer = audio; audio.preload = "auto"; audio.volume = 1; audio.src = new URL(state.challenge.audioUrl, window.location.href).href; audio.addEventListener( "ended", function () { if (state.audioPlayer === audio) state.audioPlayer = null; emit(state, "audioEnd"); emit(state, "audio-end"); }, { once: true }, ); audio.addEventListener( "error", function () { if (state.audioPlayer === audio) state.audioPlayer = null; var mediaError = audio.error; setStatus( state, "network-error", "The audio challenge could not be played. Load a new challenge and try again.", ); emit(state, "error", { code: "audio-playback-failed", audioUrl: audio.src, mediaErrorCode: mediaError ? mediaError.code : null, }); }, { once: true }, ); audio.load(); audio .play() .then(function () { emit(state, "audioStart"); emit(state, "audio-start"); }) .catch(function (error) { if (state.audioPlayer === audio) state.audioPlayer = null; setStatus( state, "network-error", "The audio challenge could not be played. Check the audio endpoint and try again.", ); emit(state, "error", { code: "audio-playback-rejected", audioUrl: audio.src, error: error instanceof Error ? error.message : String(error), }); }); } function onFormSubmit(state, event) { if (!state.config.required || state.responseToken) return; event.preventDefault(); event.stopImmediatePropagation(); setStatus(state, "incorrect", state.config.requiredMessage); emit(state, "failure", { code: "missing-input" }); var target = state.root.querySelector("[data-captcha-answer]") || state.root.querySelector("[data-captcha-not-robot-button]") || state.root.querySelector("[data-captcha-verify]"); if (target && typeof target.focus === "function") target.focus(); } function initialize(root) { if (!(root instanceof HTMLElement) || states.has(root)) return; if (!root.id) { instanceCounter += 1; root.id = "wrn-captcha-" + instanceCounter; } var state = { root: root, config: config(root), challenge: null, answer: "", selections: [], responseToken: "", status: "idle", timer: null, notRobotTimer: null, notRobotPending: false, challengeLoadedAt: 0, externalApi: null, externalWidgetId: null, audioPlayer: null, form: root.closest("form"), formSubmitListener: null, formSuccessListener: null, formErrorListener: null, }; states.set(root, state); root.dataset.captchaSize = state.config.size; var response = root.querySelector("[data-captcha-response]"); if (response) response.name = state.config.responseField; var answer = root.querySelector("[data-captcha-answer]"); var verifyButton = root.querySelector("[data-captcha-verify]"); var refreshButton = root.querySelector("[data-captcha-refresh]"); var audioButton = root.querySelector("[data-captcha-audio]"); var audioAlternative = root.querySelector("[data-captcha-audio-alternative]"); var notRobotButton = root.querySelector("[data-captcha-not-robot-button]"); if (answer) { answer.addEventListener("input", function () { state.answer = answer.value; emit(state, "input", { answerLength: state.answer.length }); }); answer.addEventListener("keydown", function (event) { if (event.key === "Enter") { event.preventDefault(); verify(state); } }); } if (verifyButton) verifyButton.addEventListener("click", function () { verify(state); }); if (notRobotButton) notRobotButton.addEventListener("click", function () { verifyNotRobot(state); }); if (refreshButton) refreshButton.addEventListener("click", function () { emit(state, "refresh"); createChallenge(state, state.config.presentation); }); if (audioButton) audioButton.addEventListener("click", function () { playAudio(state); }); if (audioAlternative) audioAlternative.addEventListener("click", function () { emit(state, "refresh", { presentation: "audio" }); createChallenge(state, "audio"); }); root.addEventListener("captcha-reset", function () { emit(state, "refresh"); createChallenge(state, state.config.presentation); }); if (state.form) { state.formSubmitListener = function (event) { onFormSubmit(state, event); }; state.formSuccessListener = function () { createChallenge(state, state.config.presentation); }; state.formErrorListener = function (event) { // Verified response tokens are single-use. The receiving API may // consume one before another form field fails, so retry with a fresh // challenge instead of reusing an already-consumed token. var detail = event && event.detail ? event.detail : {}; var captchaFailed = detail.code === "missing-input" || detail.code === "invalid-input" || detail.code === "expired" || detail.code === "already-used" || detail.code === "action-mismatch" || detail.code === "hostname-mismatch" || detail.code === "session-mismatch"; if (state.config.resetOnError || captchaFailed) createChallenge(state, state.config.presentation); }; state.form.addEventListener("submit", state.formSubmitListener, true); state.form.addEventListener("wire:success", state.formSuccessListener); state.form.addEventListener("wire:error", state.formErrorListener); } updateNotRobotState(state); updateControls(state); if (state.config.disabled) setStatus(state, "disabled", ""); else if (state.config.autoLoad) createChallenge(state, state.config.presentation); } function scan(scope) { var host = scope || document; if (host instanceof Element && host.matches("[data-wrn-captcha]")) initialize(host); host.querySelectorAll("[data-wrn-captcha]").forEach(initialize); } function cleanupRoot(root) { var state = states.get(root); if (!state) return; if (state.timer) clearInterval(state.timer); if (state.notRobotTimer) clearTimeout(state.notRobotTimer); if (state.audioPlayer) { try { state.audioPlayer.pause(); state.audioPlayer.src = ""; } catch { // Best-effort cleanup for browser audio implementations. } } if (state.form && state.formSubmitListener) { state.form.removeEventListener("submit", state.formSubmitListener, true); } if (state.form && state.formSuccessListener) { state.form.removeEventListener("wire:success", state.formSuccessListener); } if (state.form && state.formErrorListener) { state.form.removeEventListener("wire:error", state.formErrorListener); } try { if (state.externalApi && state.externalWidgetId != null) { if (typeof state.externalApi.remove === "function") state.externalApi.remove(state.externalWidgetId); else if (typeof state.externalApi.reset === "function") state.externalApi.reset(state.externalWidgetId); } } catch { // Third-party providers may reject cleanup after navigation. } states.delete(root); } function unmount(scope) { var host = scope || document; var roots = []; if (host instanceof Element && host.matches("[data-wrn-captcha]")) roots.push(host); if (host.querySelectorAll) host.querySelectorAll("[data-wrn-captcha]").forEach(function (root) { roots.push(root); }); roots.forEach(cleanupRoot); } var runtime = { scan: scan, mount: scan, unmount: unmount, reset: function (element) { var root = typeof element === "string" ? document.querySelector(element) : element; var state = root ? states.get(root) : null; if (state) createChallenge(state, state.config.presentation); }, verify: function (element) { var root = typeof element === "string" ? document.querySelector(element) : element; var state = root ? states.get(root) : null; return state ? verify(state) : Promise.resolve(); }, }; window[RUNTIME_KEY] = runtime; window.__wrnexusRuntimes = window.__wrnexusRuntimes || {}; window.__wrnexusRuntimes.captcha = { mount: scan, unmount: unmount }; if (document.readyState === "loading") { document.addEventListener( "DOMContentLoaded", function () { scan(document); }, { once: true }, ); } else { scan(document); } new MutationObserver(function (records) { records.forEach(function (record) { record.addedNodes.forEach(function (node) { if (node.nodeType === 1) scan(node); }); }); }).observe(document.documentElement, { childList: true, subtree: true }); })();