Files
WRNexusJS/packages/auth/assets/client/auth.js
T

735 lines
29 KiB
JavaScript

/* 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*)wrn-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, allowCrossOrigin = false) {
if (!target) return false;
const url = new URL(target, location.href);
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) return false;
if (url.origin !== location.origin) {
if (!allowCrossOrigin) return false;
location.assign(url.href);
return true;
}
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);
// The same-origin verification endpoint resolves and validates the final
// destination, including cross-app SSO handoffs. Never SPA-route those.
if (typeof result.redirect === "string" && navigate(result.redirect, true)) return;
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(--wrn-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("wrn:success", onSuccess);
recoveryCleanups.set(element, () => {
download?.removeEventListener("click", onDownload);
element.removeEventListener("wrn: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("wrn:success", onSuccess);
element.addEventListener("wrn:error", onError);
back?.addEventListener("click", onBack);
flowCleanups.set(element, () => {
element.removeEventListener("wrn:success", onSuccess);
element.removeEventListener("wrn: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("wrn:error", onError);
flowCleanups.set(element, () => element.removeEventListener("wrn: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 radios = Array.from(form?.querySelectorAll("input[type=radio][name=method]") || []);
const status = form?.querySelector("[data-auth-mfa-status]");
const resendGroup = form?.querySelector("[data-auth-mfa-resend-group]");
const resendButton = form?.querySelector("button[data-auth-mfa-resend]");
const resendDeadlines = new Map();
let resendTimer;
const saved = readMfaState();
const allowed = Array.isArray(saved?.requires?.mfa) ? saved.requires.mfa : [];
const challenges = new Map();
const pending = new Map();
let expired = false;
let disposed = false;
let expiryTimer;
const expiresAt = Number(saved?.mfaExpiresAt);
const expire = () => {
if (disposed) return;
expired = true;
window.clearTimeout(expiryTimer);
sessionStorage.removeItem(MFA_STORAGE_KEY);
if (tokenInput) tokenInput.value = "";
form?.querySelectorAll("input, button").forEach((control) => {
control.disabled = true;
});
// A form response's finalizer or hydration may re-enable buttons. Hide the
// entire stale form instead of leaving a second apparently usable action.
element.dataset.mfaExpired = "true";
if (form) form.hidden = true;
setMessage(status, "");
form?.querySelectorAll("[data-error]").forEach((error) => {
error.textContent = "";
});
const expiredMessage = element.querySelector("[data-auth-mfa-expired]");
if (expiredMessage) expiredMessage.hidden = false;
const restart = element.querySelector("[data-auth-mfa-restart]");
if (restart) restart.hidden = false;
};
const checkExpiry = () => {
// Old stored challenges have no verifiable deadline: require fresh sign-in.
if (saved?.mfaToken && (!Number.isFinite(expiresAt) || expiresAt <= Date.now())) expire();
return expired;
};
const restoreToken = () => {
checkExpiry();
if (!expired && tokenInput && !tokenInput.value && saved?.mfaToken)
tokenInput.value = String(saved.mfaToken);
};
const method = () => radios.find((radio) => radio.checked && !radio.disabled)?.value;
const isOtp = (value) => ["email-otp", "sms-otp"].includes(value);
const updateResend = () => {
window.clearTimeout(resendTimer);
const selected = method();
if (resendGroup) resendGroup.hidden = !isOtp(selected) || expired;
if (!resendButton || !isOtp(selected) || expired || disposed) return;
const seconds = Math.max(
0,
Math.ceil(((resendDeadlines.get(selected) || 0) - Date.now()) / 1000),
);
resendButton.disabled = pending.has(selected) || seconds > 0;
resendButton.textContent = seconds > 0 ? "Resend code in " + seconds + "s" : "Resend code";
if (seconds > 0) resendTimer = window.setTimeout(updateResend, 1000);
};
for (const radio of radios) {
radio.disabled = allowed.length > 0 && !allowed.includes(radio.value);
const label = radio.closest("label");
if (label) label.hidden = radio.disabled;
}
const initial =
radios.find((radio) => !radio.disabled && radio.value === element.dataset.mfaInitialMethod) ||
radios.find((radio) => !radio.disabled);
for (const radio of radios) radio.checked = radio === initial;
restoreToken();
const sendCode = async (selected, resend = false) => {
if (disposed || expired || !isOtp(selected)) return;
if (resend && (pending.has(selected) || (resendDeadlines.get(selected) || 0) > Date.now()))
return;
restoreToken();
if (expired) return;
if (!tokenInput?.value) {
setMessage(status, "Sign-in verification has expired. Start again.", true);
const restart = element.querySelector("[data-auth-mfa-restart]");
if (restart) restart.hidden = false;
return;
}
const previous = challenges.get(selected);
if (previous?.expiresAt && Number(previous.expiresAt) <= Date.now())
challenges.delete(selected);
if (!pending.has(selected) && (resend || !challenges.has(selected))) {
pending.set(
selected,
json(element.dataset.mfaOtpAction || "/api/auth/mfa/otp", {
mfaToken: tokenInput.value,
method: selected,
resend,
}),
);
}
if (method() === selected) {
setFormBusy(form, true);
setMessage(status, "Sending a one-time code…");
}
try {
const result = (!resend && challenges.get(selected)) || (await pending.get(selected));
if (disposed || expired) return;
challenges.set(selected, result);
if (!resendDeadlines.has(selected) || resend)
resendDeadlines.set(selected, Date.now() + (Number(result.retryAfter) || 60) * 1000);
if (method() !== selected) return;
if (challengeInput) challengeInput.value = String(result.id || "");
if (resend) {
form?.querySelectorAll("[data-auth-mfa-pin] input, [name=code]").forEach((input) => {
input.value = "";
input.dispatchEvent(new window.Event("input", { bubbles: true }));
});
form?.querySelectorAll("[data-error]").forEach((error) => {
error.textContent = "";
});
}
setMessage(
status,
selected === "email-otp"
? "An email code has been requested. Check your Inbox and Spam folder."
: "An SMS code has been requested. It may take a moment to arrive.",
);
} catch (error) {
if (!disposed && !expired && method() === selected)
setMessage(
status,
error instanceof Error ? error.message : "Unable to send a one-time code",
true,
);
} finally {
pending.delete(selected);
if (!disposed && !expired && method() === selected) setFormBusy(form, false);
updateResend();
}
};
const updateMethod = () => {
if (checkExpiry()) return;
const selected = method();
setFormBusy(form, false);
if (challengeInput) challengeInput.value = "";
const code = form?.querySelector("[name=code]");
if (code) code.value = "";
const error = form?.querySelector("[data-error=code]");
if (error) error.textContent = "";
setMessage(status, "");
updateResend();
for (const [selector, active] of [
["[data-auth-mfa-pin]", selected !== "recovery-code"],
["[data-auth-mfa-recovery]", selected === "recovery-code"],
]) {
const group = form?.querySelector(selector);
if (!group) continue;
group.hidden = !active;
for (const input of group.querySelectorAll("input")) {
input.disabled = !active;
input.value = "";
input.dispatchEvent(new window.Event("input", { bubbles: true }));
}
}
if (isOtp(selected)) void sendCode(selected);
};
const onMethodChange = (event) => {
if (radios.includes(event.target)) updateMethod();
};
const onResend = (event) => {
event.preventDefault();
void sendCode(method(), true);
};
const onSubmit = (event) => {
restoreToken();
const code = form?.querySelector("[name=code]");
const active = form?.querySelector(
method() === "recovery-code" ? "[name=recoveryCode]" : "[name=otpCode]",
);
if (code && active) code.value = active.value;
if (expired || !method() || (isOtp(method()) && !challengeInput?.value)) {
event.preventDefault();
event.stopImmediatePropagation();
if (!expired && isOtp(method())) void sendCode(method());
}
};
const onSuccess = (event) => {
if (event.target === form) sessionStorage.removeItem(MFA_STORAGE_KEY);
};
const onError = (event) => {
if (event.target !== form || event.detail?.code !== "mfa-transaction-expired") return;
expire();
};
updateMethod();
if (!expired && Number.isFinite(expiresAt)) {
expiryTimer = window.setTimeout(expire, Math.max(0, expiresAt - Date.now()));
}
// Capture native change before the shared Radio emits its component output.
form?.addEventListener("change", onMethodChange, true);
form?.addEventListener("submit", onSubmit, true);
resendButton?.addEventListener("click", onResend);
element.addEventListener("wrn:success", onSuccess);
element.addEventListener("wrn:error", onError);
flowCleanups.set(element, () => {
disposed = true;
window.clearTimeout(expiryTimer);
window.clearTimeout(resendTimer);
resendButton?.removeEventListener("click", onResend);
form?.removeEventListener("change", onMethodChange, true);
form?.removeEventListener("submit", onSubmit, true);
element.removeEventListener("wrn:success", onSuccess);
element.removeEventListener("wrn:error", onError);
});
}
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("wrn:success", onSuccess);
flowCleanups.set(element, () => element.removeEventListener("wrn: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 });
})();