const ext = globalThis.browser ?? globalThis.chrome; const isNodeTestEnv = typeof module !== "undefined" && module.exports; const usePromiseAPI = typeof globalThis.browser !== "undefined"; function runtimeSend(message) { if (usePromiseAPI) { return ext.runtime.sendMessage(message); } return new Promise((resolve, reject) => { ext.runtime.sendMessage(message, (response) => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(response); }); }); } function isVisibleInput(input) { if (!(input instanceof HTMLInputElement)) { return false; } if (input.disabled || input.readOnly) { return false; } const style = window.getComputedStyle(input); if (style.display === "none" || style.visibility === "hidden") { return false; } return input.offsetParent !== null || style.position === "fixed"; } function normalizeRole(rawRole) { switch (String(rawRole || "").trim().toLowerCase()) { case "password": return "password"; default: return "username"; } } function describeFieldRole(input) { const type = String(input?.getAttribute?.("type") || "").toLowerCase(); if (type === "password") { return "password"; } const autocomplete = String(input?.autocomplete || "").toLowerCase(); if (autocomplete.includes("username") || autocomplete.includes("email")) { return "username"; } return "username"; } function isUsernameCandidate(input) { if (!isVisibleInput(input)) { return false; } return describeFieldRole(input) === "username"; } function isPasswordCandidate(input) { return isVisibleInput(input) && describeFieldRole(input) === "password"; } function dispatchFillEvents(input) { if (typeof InputEvent === "function") { input.dispatchEvent(new InputEvent("input", { bubbles: true, data: input.value, inputType: "insertText" })); } else { input.dispatchEvent(new Event("input", { bubbles: true })); } input.dispatchEvent(new Event("change", { bubbles: true })); } function setInputValue(input, value) { const prototype = Object.getPrototypeOf(input); const descriptor = prototype ? Object.getOwnPropertyDescriptor(prototype, "value") : null; if (descriptor?.set) { descriptor.set.call(input, value); return; } input.value = value; } function visibleInputs(scope) { return Array.from(scope.querySelectorAll("input")).filter(isVisibleInput); } function resolveFormInputs(anchorInput) { if (anchorInput?.form instanceof HTMLFormElement) { return visibleInputs(anchorInput.form); } return visibleInputs(document); } function firstVisiblePassword(scope) { return visibleInputs(scope).find(isPasswordCandidate) || null; } function firstVisibleUsername(scope) { return visibleInputs(scope).find(isUsernameCandidate) || null; } function associatedFieldsForAnchor(anchorInput) { const scopeInputs = resolveFormInputs(anchorInput); const passwordInput = scopeInputs.find(isPasswordCandidate) || firstVisiblePassword(document); const usernameInScope = scopeInputs.filter(isUsernameCandidate); let usernameInput = usernameInScope[0] || null; if (passwordInput && usernameInScope.length !== 0) { const priorSibling = usernameInScope.find((input) => typeof input.compareDocumentPosition === "function" && Boolean(input.compareDocumentPosition(passwordInput) & Node.DOCUMENT_POSITION_FOLLOWING) ); usernameInput = priorSibling || usernameInScope[0] || null; } if (!usernameInput) { usernameInput = firstVisibleUsername(document); } return { usernameInput, passwordInput }; } function buildFieldDescriptor(input, role) { if (!(input instanceof HTMLInputElement)) { return null; } const normalizedRole = normalizeRole(role || describeFieldRole(input)); const form = input.form instanceof HTMLFormElement ? input.form : null; const scope = form || document; const inputs = visibleInputs(scope); const fieldIndex = inputs.indexOf(input); const forms = Array.from(document.forms || []); return { role: normalizedRole, formIndex: form ? forms.indexOf(form) : -1, fieldIndex, id: String(input.id || ""), name: String(input.name || ""), autocomplete: String(input.autocomplete || "").toLowerCase() }; } function resolveFieldDescriptor(descriptor) { if (!descriptor || typeof descriptor !== "object") { return null; } const normalizedRole = normalizeRole(descriptor.role); const forms = Array.from(document.forms || []); const form = Number.isInteger(descriptor.formIndex) && descriptor.formIndex >= 0 ? forms[descriptor.formIndex] || null : null; const scope = form || document; const inputs = visibleInputs(scope); if (Number.isInteger(descriptor.fieldIndex) && descriptor.fieldIndex >= 0 && descriptor.fieldIndex < inputs.length) { const candidate = inputs[descriptor.fieldIndex]; if (describeFieldRole(candidate) === normalizedRole) { return candidate; } } if (descriptor.id) { const byID = scope.querySelector(`#${CSS.escape(descriptor.id)}`); if (byID instanceof HTMLInputElement && isVisibleInput(byID) && describeFieldRole(byID) === normalizedRole) { return byID; } } if (descriptor.name) { const byName = visibleInputs(scope).find((input) => input.name === descriptor.name && describeFieldRole(input) === normalizedRole); if (byName) { return byName; } } if (normalizedRole === "password") { return firstVisiblePassword(scope) || firstVisiblePassword(document); } return firstVisibleUsername(scope) || firstVisibleUsername(document); } function chooseFillTargets(targetDescriptor) { const anchorInput = resolveFieldDescriptor(targetDescriptor) || (document.activeElement instanceof HTMLInputElement ? document.activeElement : null); const associated = associatedFieldsForAnchor(anchorInput); if (normalizeRole(targetDescriptor?.role) === "password" && anchorInput instanceof HTMLInputElement) { return { usernameInput: associated.usernameInput, passwordInput: isPasswordCandidate(anchorInput) ? anchorInput : associated.passwordInput, anchorInput }; } if (normalizeRole(targetDescriptor?.role) === "username" && anchorInput instanceof HTMLInputElement) { return { usernameInput: isUsernameCandidate(anchorInput) ? anchorInput : associated.usernameInput, passwordInput: associated.passwordInput, anchorInput }; } return { usernameInput: associated.usernameInput, passwordInput: associated.passwordInput, anchorInput: anchorInput || associated.passwordInput || associated.usernameInput || null }; } function scanLoginFields() { const activeElement = document.activeElement instanceof HTMLInputElement ? document.activeElement : null; const activeUsable = activeElement && isVisibleInput(activeElement) ? activeElement : null; const targets = chooseFillTargets(buildFieldDescriptor(activeUsable, describeFieldRole(activeUsable))); const anchorInput = activeUsable || targets.passwordInput || targets.usernameInput; const focusTarget = buildFieldDescriptor(anchorInput, describeFieldRole(anchorInput)); const allVisible = visibleInputs(document); const roles = allVisible .filter((input) => isUsernameCandidate(input) || isPasswordCandidate(input)) .map((input) => { const descriptor = buildFieldDescriptor(input, describeFieldRole(input)); return `${descriptor.formIndex}:${descriptor.fieldIndex}:${descriptor.role}`; }); return { pageHasLoginForm: Boolean(targets.usernameInput || targets.passwordInput), usernameInput: targets.usernameInput, passwordInput: targets.passwordInput, anchorInput, focusTarget, signature: roles.join("|") }; } function fillCredential(credential, targetDescriptor) { const { passwordInput, usernameInput } = chooseFillTargets(targetDescriptor); if (usernameInput && credential.username) { usernameInput.focus(); setInputValue(usernameInput, credential.username); dispatchFillEvents(usernameInput); } if (passwordInput && credential.password) { passwordInput.focus(); setInputValue(passwordInput, credential.password); dispatchFillEvents(passwordInput); } if (!usernameInput && !passwordInput) { return { ok: false, error: "No fillable username or password fields were found." }; } return { ok: true }; } function domainLabel(rawURL) { try { return new URL(rawURL).host || ""; } catch (_error) { return ""; } } function inlineMatchSummary(match) { const parts = []; if (match.username) { parts.push(match.username); } if (match.url) { const host = domainLabel(match.url); if (host) { parts.push(host); } } if (Array.isArray(match.path) && match.path.length !== 0) { parts.push(match.path.join(" / ")); } return parts.join(" ยท ") || "No username"; } function shouldShowInlineOverlay(state, hasTarget, suppressed) { if (suppressed || !hasTarget) { return false; } return Boolean( state?.pageHasLoginForm && ( state?.pendingFill || (state?.configured && state?.success && !state?.status?.locked && Array.isArray(state?.matches) && state.matches.length > 0) ) ); } const contentTestExports = { normalizeRole, describeFieldRole, buildFieldDescriptor, resolveFieldDescriptor, chooseFillTargets, inlineMatchSummary, domainLabel, shouldShowInlineOverlay }; if (isNodeTestEnv) { module.exports = contentTestExports; } else { let pageState = { configured: true, success: true, matches: [], pageHasLoginForm: false, pendingFill: false, error: "", focusTarget: null }; let chooserOpen = false; let inlineSuppressed = false; let refreshTimer = null; let lastReportedSignature = ""; let lastReportedTarget = ""; const root = document.createElement("div"); root.id = "keepassgo-inline-root"; root.setAttribute("aria-live", "polite"); const shadow = root.attachShadow({ mode: "open" }); shadow.innerHTML = `
KeePassGO suggestions
Select a matching login for this field.
`; const dock = shadow.querySelector(".dock"); const trigger = shadow.querySelector(".trigger"); const meta = shadow.querySelector(".meta"); const matchList = shadow.querySelector(".match-list"); const panelCopy = shadow.querySelector(".panel-copy"); function ensureRootMounted() { if (!root.isConnected) { document.documentElement.appendChild(root); } } function currentTarget() { return chooseFillTargets(pageState.focusTarget).anchorInput; } function hideDock() { chooserOpen = false; dock.style.display = "none"; dock.dataset.open = "false"; } function positionDock() { const anchor = currentTarget(); if (!anchor || dock.style.display === "none") { return; } const rect = anchor.getBoundingClientRect(); const width = Math.min(340, Math.max(220, rect.width)); const left = Math.min(window.innerWidth - width - 12, Math.max(12, rect.left)); const top = Math.min(window.innerHeight - 16, rect.bottom + 8); dock.style.left = `${left}px`; dock.style.top = `${top}px`; dock.style.width = `${width}px`; } function renderMatches() { matchList.textContent = ""; if (pageState.pendingFill) { const pending = document.createElement("div"); pending.className = "empty"; pending.textContent = pageState.pendingMessage || "Approve or deny the fill request in KeePassGO."; matchList.appendChild(pending); return; } if (!Array.isArray(pageState.matches) || pageState.matches.length === 0) { const empty = document.createElement("div"); empty.className = "empty"; empty.textContent = pageState.error || "No matching entries were found for this page."; matchList.appendChild(empty); return; } for (const match of pageState.matches) { const row = document.createElement("button"); row.type = "button"; row.className = "match"; const title = document.createElement("strong"); title.textContent = match.title; const summary = document.createElement("span"); summary.className = "subtle"; summary.textContent = inlineMatchSummary(match); const quality = document.createElement("span"); quality.className = "pill"; quality.textContent = match.quality || "Candidate"; row.appendChild(title); row.appendChild(summary); row.appendChild(quality); row.addEventListener("click", async () => { row.disabled = true; inlineSuppressed = true; hideDock(); try { await runtimeSend({ type: "keepassgo-fill-entry", entryId: match.id, target: pageState.focusTarget }); } catch (_error) { pageState = { ...pageState, pendingFill: false, error: "KeePassGO could not fill this page." }; renderInlineState(); } finally { row.disabled = false; } }); matchList.appendChild(row); } } function renderInlineState() { const target = currentTarget(); const shouldShow = shouldShowInlineOverlay(pageState, Boolean(target), inlineSuppressed); if (!shouldShow) { hideDock(); return; } ensureRootMounted(); dock.style.display = "block"; trigger.dataset.tone = pageState.pendingFill ? "warning" : (pageState.error ? "error" : "ready"); if (pageState.pendingFill) { meta.textContent = "Approval needed in KeePassGO"; panelCopy.textContent = pageState.pendingMessage || "Approve or deny the fill request in KeePassGO."; } else { const count = Array.isArray(pageState.matches) ? pageState.matches.length : 0; meta.textContent = count === 1 ? "1 login ready" : `${count} logins ready`; panelCopy.textContent = "Select a matching login for this field."; } dock.dataset.open = chooserOpen ? "true" : "false"; renderMatches(); positionDock(); } function reportFieldState(force) { const scan = scanLoginFields(); pageState = { ...pageState, pageHasLoginForm: scan.pageHasLoginForm, focusTarget: scan.focusTarget }; renderInlineState(); const nextTarget = JSON.stringify(scan.focusTarget || null); if (!force && scan.signature === lastReportedSignature && nextTarget === lastReportedTarget) { return; } lastReportedSignature = scan.signature; lastReportedTarget = nextTarget; void runtimeSend({ type: "keepassgo-page-ready", force: Boolean(force), pageHasLoginForm: scan.pageHasLoginForm, focusTarget: scan.focusTarget, signature: scan.signature }).then((response) => { if (response && typeof response === "object" && !("success" in response && response.success === false)) { pageState = { ...pageState, ...response, pageHasLoginForm: Boolean(response.pageHasLoginForm), focusTarget: response.focusTarget || pageState.focusTarget }; renderInlineState(); } }).catch(() => null); } function scheduleRefresh(force) { if (refreshTimer !== null) { clearTimeout(refreshTimer); } refreshTimer = window.setTimeout(() => { refreshTimer = null; reportFieldState(force); }, force ? 0 : 120); } trigger.addEventListener("click", () => { chooserOpen = !chooserOpen; renderInlineState(); }); document.addEventListener("focusin", () => { scheduleRefresh(false); }); document.addEventListener("input", () => { scheduleRefresh(false); }, true); document.addEventListener("click", (event) => { if (!root.contains(event.target)) { chooserOpen = false; renderInlineState(); } }); window.addEventListener("scroll", () => { positionDock(); }, true); window.addEventListener("resize", () => { positionDock(); }); const observer = new MutationObserver((records) => { if (records.some((record) => record.type === "childList")) { scheduleRefresh(false); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); ext.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message?.type === "keepassgo-fill-credential") { try { sendResponse(fillCredential(message.credential || {}, message.target || pageState.focusTarget)); } catch (error) { sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }); } return false; } if (message?.type === "keepassgo-page-state") { pageState = { ...pageState, ...(message.state || {}) }; renderInlineState(); sendResponse({ ok: true }); return false; } if (message?.type === "keepassgo-page-scan") { const scan = scanLoginFields(); sendResponse({ pageHasLoginForm: scan.pageHasLoginForm, focusTarget: scan.focusTarget, signature: scan.signature }); return false; } return false; }); reportFieldState(true); }