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 }, "*");
}
});