/* global window, document, navigator, CustomEvent, HTMLElement, HTMLFormElement, MutationObserver, fetch, atob, btoa, Blob, URL, sessionStorage, localStorage, location, crypto */ (() => { if (window.WRNexusAuth) return; const mounted = new WeakSet(); const requestControllers = new WeakMap(); const cleanups = new WeakMap(); const recoveryMounted = new WeakSet(); const recoveryCleanups = new WeakMap(); const flowMounted = new WeakSet(); const flowCleanups = new WeakMap(); const MFA_STORAGE_KEY = "wrnexus.auth.mfa"; const DEVICE_STORAGE_KEY = "wrnexus.auth.device"; function fromBase64Url(value) { const input = String(value); if (!input || !/^[A-Za-z0-9_-]+$/.test(input) || input.length % 4 === 1) { throw new TypeError("Passkey data must be canonical Base64URL without padding"); } const normalized = input.replace(/-/g, "+").replace(/_/g, "/"); const binary = atob(normalized + "=".repeat((4 - (normalized.length % 4)) % 4)); const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0)); if (toBase64Url(bytes) !== input) { throw new TypeError("Passkey data must be canonical Base64URL without padding"); } return bytes; } function toBase64Url(value) { const bytes = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } function publicKeyCreationOptions(options) { return { ...options, challenge: fromBase64Url(options.challenge), user: { ...options.user, id: fromBase64Url(options.user.id) }, excludeCredentials: (options.excludeCredentials || []).map((item) => ({ ...item, id: fromBase64Url(item.id), })), }; } function publicKeyRequestOptions(options) { return { ...options, challenge: fromBase64Url(options.challenge), allowCredentials: (options.allowCredentials || []).map((item) => ({ ...item, id: fromBase64Url(item.id), })), }; } function credentialToJson(credential) { if (!credential) return null; const response = credential.response; const base = { id: credential.id, type: credential.type, rawId: toBase64Url(credential.rawId), authenticatorAttachment: credential.authenticatorAttachment, clientExtensionResults: credential.getClientExtensionResults ? credential.getClientExtensionResults() : {}, }; if (response && "attestationObject" in response) { return { ...base, response: { clientDataJSON: toBase64Url(response.clientDataJSON), attestationObject: toBase64Url(response.attestationObject), transports: response.getTransports ? response.getTransports() : [], }, }; } return { ...base, response: { clientDataJSON: toBase64Url(response.clientDataJSON), authenticatorData: toBase64Url(response.authenticatorData), signature: toBase64Url(response.signature), userHandle: response.userHandle ? toBase64Url(response.userHandle) : null, }, }; } function csrfHeaders() { const match = document.cookie.match(/(?:^|;\s*)wire-csrf=([^;]+)/); return match ? { "x-csrf-token": decodeURIComponent(match[1]) } : {}; } async function json(url, body, signal) { const response = await fetch(url, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json", accept: "application/json", ...csrfHeaders(), }, body: JSON.stringify(body), signal, }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload.ok === false) { const error = new Error( payload.message || payload.error || `Request failed (${response.status})`, ); error.status = response.status; error.payload = payload; throw error; } return payload; } function setState(element, state, message) { element.dataset.authState = state; element.setAttribute("aria-busy", state === "loading" ? "true" : "false"); const status = element.querySelector("[data-auth-status]"); if (status) { status.textContent = message || ""; status.hidden = !message; } const button = element.matches("button") ? element : element.querySelector("button"); if (button) button.disabled = state === "loading"; } function dispatch(element, name, detail) { element.dispatchEvent(new CustomEvent(name, { bubbles: true, detail })); } function navigate(target) { if (!target) return false; const url = new URL(target, location.href); if (url.origin !== location.origin) return false; if (window.__wrnexusNavigate) { window.__wrnexusNavigate(url.pathname + url.search + url.hash); } else { location.assign(url.href); } return true; } function randomDeviceId() { if (crypto.randomUUID) return crypto.randomUUID(); const bytes = crypto.getRandomValues(new Uint8Array(24)); return toBase64Url(bytes); } function deviceFingerprint() { try { let value = localStorage.getItem(DEVICE_STORAGE_KEY); if (!value) { value = randomDeviceId(); localStorage.setItem(DEVICE_STORAGE_KEY, value); } return value; } catch { return randomDeviceId(); } } function toggle(element, visible) { if (!element) return; element.hidden = !visible; element.classList.toggle("hidden", !visible); } function setMessage(element, message, isError = false) { if (!element) return; element.textContent = message || ""; element.hidden = !message; element.classList.toggle("hidden", !message); element.dataset.status = isError ? "error" : "success"; } function setFormBusy(form, busy) { form?.querySelectorAll("[type=submit]").forEach((button) => { button.disabled = busy; }); } async function registerPasskey(element) { setState(element, "loading", element.dataset.loadingMessage || "Preparing passkey…"); try { if (!window.PublicKeyCredential || !navigator.credentials || !window.AbortController) { throw new Error("Passkeys are not supported in this browser"); } const controller = new window.AbortController(); requestControllers.get(element)?.abort(); requestControllers.set(element, controller); const optionsPayload = await json( element.dataset.optionsEndpoint || "/api/auth/passkeys/register/options", {}, controller.signal, ); const credential = await navigator.credentials.create({ publicKey: publicKeyCreationOptions(optionsPayload.options), signal: controller.signal, }); const result = await json( element.dataset.verifyEndpoint || "/api/auth/passkeys/register/verify", { key: optionsPayload.key, response: credentialToJson(credential), name: element.dataset.passkeyName || "Passkey", }, controller.signal, ); setState(element, "success", element.dataset.successMessage || "Passkey added."); dispatch(element, "auth:passkey-registered", result); } catch (error) { if (error && error.name === "AbortError") return; setState( element, "error", error instanceof Error ? error.message : "Passkey registration failed", ); dispatch(element, "auth:error", { error }); } } async function authenticatePasskey(element) { setState(element, "loading", element.dataset.loadingMessage || "Waiting for your passkey…"); try { if (!window.PublicKeyCredential || !navigator.credentials || !window.AbortController) { throw new Error("Passkeys are not supported in this browser"); } const controller = new window.AbortController(); requestControllers.get(element)?.abort(); requestControllers.set(element, controller); const identifier = element.dataset.identifier || element.closest("form")?.querySelector("[name=identifier]")?.value || element.closest("[data-auth-sign-in]")?.querySelector("form [name=identifier]")?.value || undefined; const optionsPayload = await json( element.dataset.optionsEndpoint || "/api/auth/passkeys/login/options", { identifier }, controller.signal, ); const credential = await navigator.credentials.get({ publicKey: publicKeyRequestOptions(optionsPayload.options), signal: controller.signal, mediation: element.dataset.conditional === "true" ? "conditional" : "optional", }); const result = await json( element.dataset.verifyEndpoint || "/api/auth/passkeys/login/verify", { key: optionsPayload.key, response: credentialToJson(credential), }, controller.signal, ); setState(element, "success", element.dataset.successMessage || "Signed in."); dispatch(element, "auth:passkey-authenticated", result); if (element.dataset.redirect) navigate(element.dataset.redirect); } catch (error) { if (error && error.name === "AbortError") return; const detail = error?.payload || {}; if (detail.code === "mfa-required" && detail.mfaToken) { sessionStorage.setItem(MFA_STORAGE_KEY, JSON.stringify(detail)); dispatch(element, "auth:mfa-required", detail); navigate(element.dataset.mfaHref || "/two-factor"); return; } setState(element, "error", error instanceof Error ? error.message : "Passkey sign-in failed"); dispatch(element, "auth:error", { error, ...detail }); } } function recoveryCodes(element) { return Array.from(element.querySelectorAll("[data-recovery-code]")) .map((item) => String(item.textContent || "").trim()) .filter(Boolean); } function renderRecoveryCodes(element, codes) { const list = element.querySelector("[data-recovery-code-list]"); if (!list || !Array.isArray(codes)) return; list.textContent = ""; for (const code of codes) { const item = document.createElement("code"); item.dataset.recoveryCode = ""; item.className = "rounded bg-[var(--wire-color-surface)] px-2 py-1.5 text-center"; item.textContent = String(code); list.appendChild(item); } } function downloadRecoveryCodes(element) { const codes = recoveryCodes(element); if (!codes.length) return; const blob = new Blob([codes.join("\n") + "\n"], { type: "text/plain;charset=utf-8" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = element.dataset.recoveryFilename || "wrnexus-recovery-codes.txt"; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } function mountRecoveryCodes(element) { if (!(element instanceof HTMLElement) || recoveryMounted.has(element)) return; recoveryMounted.add(element); const download = element.querySelector("[data-recovery-download]"); const onDownload = () => downloadRecoveryCodes(element); const onSuccess = (event) => { if (event.target instanceof HTMLFormElement && event.detail?.codes) { renderRecoveryCodes(element, event.detail.codes); } }; download?.addEventListener("click", onDownload); element.addEventListener("wire:success", onSuccess); recoveryCleanups.set(element, () => { download?.removeEventListener("click", onDownload); element.removeEventListener("wire:success", onSuccess); }); } function mountOtpSignIn(element) { if (!(element instanceof HTMLElement) || flowMounted.has(element)) return; flowMounted.add(element); const requestForm = element.querySelector("[data-auth-otp-request]"); const completeForm = element.querySelector("[data-auth-otp-complete]"); const back = element.querySelector("[data-auth-otp-back]"); const onSuccess = (event) => { if (event.target !== requestForm || !event.detail?.id) return; const challenge = completeForm?.querySelector("[name=challengeId]"); if (challenge) challenge.value = String(event.detail.id); toggle(requestForm, false); toggle(completeForm, true); completeForm?.querySelector("[name=code]")?.focus(); dispatch(element, "auth:otp-requested", event.detail); }; const onError = (event) => { if (event.target !== completeForm) return; const detail = event.detail || {}; if (detail.code !== "mfa-required" || !detail.mfaToken) return; sessionStorage.setItem(MFA_STORAGE_KEY, JSON.stringify(detail)); dispatch(element, "auth:mfa-required", detail); navigate(element.dataset.mfaHref || "/two-factor"); }; const onBack = () => { toggle(completeForm, false); toggle(requestForm, true); requestForm?.querySelector("[name=identifier]")?.focus(); }; element.addEventListener("wire:success", onSuccess); element.addEventListener("wire:error", onError); back?.addEventListener("click", onBack); flowCleanups.set(element, () => { element.removeEventListener("wire:success", onSuccess); element.removeEventListener("wire:error", onError); back?.removeEventListener("click", onBack); }); } function mountSignIn(element) { if (!(element instanceof HTMLElement) || flowMounted.has(element)) return; flowMounted.add(element); const form = element.querySelector("form[data-schema]"); const fingerprintInput = form?.querySelector("[name=deviceFingerprint]"); const deviceNameInput = form?.querySelector("[name=deviceName]"); if (fingerprintInput && !fingerprintInput.value) fingerprintInput.value = deviceFingerprint(); if (deviceNameInput && !deviceNameInput.value) { deviceNameInput.value = String(navigator.userAgent || navigator.platform || "Browser").slice( 0, 120, ); } const onError = (event) => { if (!(event.target instanceof HTMLFormElement)) return; const detail = event.detail || {}; if (detail.code !== "mfa-required" || !detail.mfaToken) return; sessionStorage.setItem(MFA_STORAGE_KEY, JSON.stringify(detail)); dispatch(element, "auth:mfa-required", detail); navigate(element.dataset.mfaHref || "/two-factor"); }; element.addEventListener("wire:error", onError); flowCleanups.set(element, () => element.removeEventListener("wire:error", onError)); } function readMfaState() { try { return JSON.parse(sessionStorage.getItem(MFA_STORAGE_KEY) || "null"); } catch { return null; } } function mountMfaChallenge(element) { if (!(element instanceof HTMLElement) || flowMounted.has(element)) return; flowMounted.add(element); const form = element.querySelector("[data-auth-mfa-form]"); const tokenInput = form?.querySelector("[name=mfaToken]"); const challengeInput = form?.querySelector("[name=challengeId]"); const methodInput = form?.querySelector("[data-auth-mfa-method]"); const codeInput = form?.querySelector("[name=code]"); const status = form?.querySelector("[data-auth-mfa-status]"); const saved = readMfaState(); if (tokenInput && !tokenInput.value && saved?.mfaToken) { tokenInput.value = String(saved.mfaToken); } const allowed = Array.isArray(saved?.requires?.mfa) ? saved.requires.mfa : []; const initialMethod = element.dataset.mfaInitialMethod; if (methodInput && allowed.length) { Array.from(methodInput.options).forEach((option) => { option.disabled = !allowed.includes(option.value); option.hidden = option.disabled; }); const selected = allowed.includes(initialMethod) ? initialMethod : allowed.includes(methodInput.value) ? methodInput.value : allowed[0]; if (selected) methodInput.value = selected; } else if (methodInput && initialMethod) { methodInput.value = initialMethod; } const onMethodChange = () => { if (challengeInput) challengeInput.value = ""; setMessage(status, ""); if (codeInput) { codeInput.value = ""; codeInput.setAttribute( "inputmode", methodInput?.value === "recovery-code" ? "text" : "numeric", ); } }; const onSubmit = async (event) => { const method = methodInput?.value; if (!form || !["email-otp", "sms-otp"].includes(method) || challengeInput?.value) return; event.preventDefault(); event.stopImmediatePropagation(); if (!tokenInput?.value) { setMessage(status, "Sign-in verification has expired. Start again.", true); return; } setFormBusy(form, true); setMessage(status, "Sending a one-time code…"); try { const result = await json(element.dataset.mfaOtpAction || "/api/auth/mfa/otp", { mfaToken: tokenInput.value, method, }); if (challengeInput) challengeInput.value = String(result.id || ""); setMessage(status, "A one-time code has been sent."); codeInput?.focus(); } catch (error) { setMessage( status, error instanceof Error ? error.message : "Unable to send a one-time code", true, ); } finally { setFormBusy(form, false); } }; const onSuccess = (event) => { if (event.target !== form) return; sessionStorage.removeItem(MFA_STORAGE_KEY); }; onMethodChange(); methodInput?.addEventListener("change", onMethodChange); form?.addEventListener("submit", onSubmit, true); element.addEventListener("wire:success", onSuccess); flowCleanups.set(element, () => { methodInput?.removeEventListener("change", onMethodChange); form?.removeEventListener("submit", onSubmit, true); element.removeEventListener("wire:success", onSuccess); }); } function mountAuthenticatorSetup(element) { if (!(element instanceof HTMLElement) || flowMounted.has(element)) return; flowMounted.add(element); const startForm = element.querySelector("[data-auth-authenticator-start]"); const details = element.querySelector("[data-auth-authenticator-details]"); const confirmForm = element.querySelector("[data-auth-authenticator-confirm]"); const secret = element.querySelector("[data-auth-authenticator-secret]"); const uri = element.querySelector("[data-auth-authenticator-uri]"); const onSuccess = (event) => { if (event.target === startForm && event.detail?.credentialId) { const credential = confirmForm?.querySelector("[name=credentialId]"); if (credential) credential.value = String(event.detail.credentialId); if (secret) secret.textContent = String(event.detail.secret || ""); if (uri) uri.textContent = String(event.detail.uri || ""); toggle(startForm, false); toggle(details, true); confirmForm?.querySelector("[name=code]")?.focus(); dispatch(element, "auth:authenticator-created", event.detail); } if (event.target === confirmForm) { dispatch(element, "auth:authenticator-enabled", event.detail); } }; element.addEventListener("wire:success", onSuccess); flowCleanups.set(element, () => element.removeEventListener("wire:success", onSuccess)); } function mountElement(element) { if (!(element instanceof HTMLElement) || mounted.has(element)) return; mounted.add(element); const action = element.dataset.authPasskey; const button = element.matches("button") ? element : element.querySelector("button"); if (!button || !action) return; const handler = () => action === "register" ? registerPasskey(element) : authenticatePasskey(element); button.addEventListener("click", handler); cleanups.set(element, () => button.removeEventListener("click", handler)); } function mount(root = document) { if (root instanceof HTMLElement && root.matches("[data-auth-passkey]")) mountElement(root); root.querySelectorAll?.("[data-auth-passkey]").forEach(mountElement); if (root instanceof HTMLElement && root.matches("[data-auth-recovery-codes]")) mountRecoveryCodes(root); root.querySelectorAll?.("[data-auth-recovery-codes]").forEach(mountRecoveryCodes); if (root instanceof HTMLElement && root.matches("[data-auth-otp-sign-in]")) mountOtpSignIn(root); root.querySelectorAll?.("[data-auth-otp-sign-in]").forEach(mountOtpSignIn); if (root instanceof HTMLElement && root.matches("[data-auth-sign-in]")) mountSignIn(root); root.querySelectorAll?.("[data-auth-sign-in]").forEach(mountSignIn); if (root instanceof HTMLElement && root.matches("[data-auth-mfa-challenge]")) mountMfaChallenge(root); root.querySelectorAll?.("[data-auth-mfa-challenge]").forEach(mountMfaChallenge); if (root instanceof HTMLElement && root.matches("[data-auth-authenticator-setup]")) mountAuthenticatorSetup(root); root.querySelectorAll?.("[data-auth-authenticator-setup]").forEach(mountAuthenticatorSetup); } function unmount(root = document) { const elements = []; if (root instanceof HTMLElement && root.matches("[data-auth-passkey]")) elements.push(root); root.querySelectorAll?.("[data-auth-passkey]").forEach((element) => elements.push(element)); for (const element of elements) { requestControllers.get(element)?.abort(); requestControllers.delete(element); cleanups.get(element)?.(); cleanups.delete(element); mounted.delete(element); } const recoveryElements = []; if (root instanceof HTMLElement && root.matches("[data-auth-recovery-codes]")) recoveryElements.push(root); root .querySelectorAll?.("[data-auth-recovery-codes]") .forEach((element) => recoveryElements.push(element)); for (const element of recoveryElements) { recoveryCleanups.get(element)?.(); recoveryCleanups.delete(element); recoveryMounted.delete(element); } const flowElements = []; const selector = "[data-auth-otp-sign-in],[data-auth-sign-in],[data-auth-mfa-challenge],[data-auth-authenticator-setup]"; if (root instanceof HTMLElement && root.matches(selector)) flowElements.push(root); root.querySelectorAll?.(selector).forEach((element) => flowElements.push(element)); for (const element of flowElements) { flowCleanups.get(element)?.(); flowCleanups.delete(element); flowMounted.delete(element); } } window.WRNexusAuth = { mount, unmount, registerPasskey, authenticatePasskey }; window.__wrnexusRuntimes = window.__wrnexusRuntimes || {}; window.__wrnexusRuntimes.auth = { mount, unmount }; if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => mount(document), { once: true }); else mount(document); new MutationObserver((records) => { for (const record of records) for (const node of record.addedNodes) if (node instanceof HTMLElement) mount(node); }).observe(document.documentElement, { childList: true, subtree: true }); })();