const s = document.createElement("script"); s.src = chrome.runtime.getURL("inject.js"); s.onload = () => s.remove(); (document.documentElement || document.head).appendChild(s); // --- Password prompt (closed shadow DOM, isolated context) --- function showPasswordPrompt(title, needsConfirm) { return new Promise((resolve) => { const host = document.createElement("div"); host.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483647"; const shadow = host.attachShadow({ mode: "closed" }); shadow.innerHTML = `
`; const cleanup = () => host.remove(); shadow.querySelector(".title").textContent = title; const pw = shadow.querySelector(".pw"); const pw2 = shadow.querySelector(".pw2"); const errEl = shadow.querySelector(".err"); const submit = () => { if (!pw.value) { errEl.textContent = "Password required"; errEl.style.display = ""; return; } if (needsConfirm && pw.value !== pw2.value) { errEl.textContent = "Passwords do not match"; errEl.style.display = ""; return; } const val = pw.value; cleanup(); resolve(val); }; shadow.querySelector(".ok").onclick = submit; shadow.querySelector(".cancel").onclick = () => { cleanup(); resolve(null); }; shadow.querySelector(".overlay").onclick = () => { cleanup(); resolve(null); }; const onKey = (e) => { if (e.key === "Enter") submit(); }; pw.addEventListener("keydown", onKey); if (pw2) pw2.addEventListener("keydown", onKey); document.body.appendChild(host); pw.focus(); }); } // --- Message relay with auth handling --- async function sendToHost(msg) { const response = await chrome.runtime.sendMessage(msg); if (chrome.runtime.lastError) throw new Error(chrome.runtime.lastError.message); return response; } async function handleRequest(action, payload, rpId) { const msg = { type: "VWEBAUTHN_REQUEST", action, payload }; if (rpId) msg.rpId = rpId; // No-auth actions pass through directly if (action === "list" || action === "status" || action === "ping") { return sendToHost(msg); } // Try with session first (no password) let response = await sendToHost(msg); // If session worked, done if (response.success) return response; // Need password — check if first-time setup const isSessionError = response.error?.includes("session") || response.error?.includes("Session") || response.error?.includes("Password or session"); if (!isSessionError) return response; // real error, don't retry let statusResp; try { statusResp = await sendToHost({ type: "VWEBAUTHN_REQUEST", action: "status", payload: {} }); } catch { return response; } const needsSetup = statusResp.success && statusResp.data?.needsSetup; const title = needsSetup ? "Virtual WebAuthn — Set Password" : `Virtual WebAuthn — ${action === "create" ? "Create Credential" : "Authenticate"}`; // Retry loop — allow 3 password attempts for (let attempt = 0; attempt < 3; attempt++) { const password = await showPasswordPrompt( attempt > 0 ? "Wrong password — try again" : title, needsSetup, ); if (!password) return { success: false, error: "Password prompt cancelled" }; msg.password = password; const retry = await sendToHost(msg); if (retry.success || !retry.error?.includes("password")) return retry; } return { success: false, error: "Too many failed attempts" }; } window.addEventListener("message", async (event) => { if (event.source !== window || event.data?.type !== "VWEBAUTHN_REQUEST") return; const { id, action, payload } = event.data; try { const response = await handleRequest(action, payload, event.data.rpId); window.postMessage({ type: "VWEBAUTHN_RESPONSE", id, ...response }, "*"); } catch (error) { window.postMessage({ type: "VWEBAUTHN_RESPONSE", id, success: false, error: error.message }, "*"); } });