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"; case "username": return "username"; default: return ""; } } function lowerJoined(values) { return values .map((value) => String(value || "").trim().toLowerCase()) .filter(Boolean) .join(" "); } function fieldHintText(input) { if (!input || typeof input !== "object") { return ""; } const labels = input.labels ? Array.from(input.labels).map((label) => label.textContent || "") : []; return lowerJoined([ input.getAttribute?.("type"), input.getAttribute?.("name"), input.getAttribute?.("id"), input.autocomplete, input.getAttribute?.("autocomplete"), input.getAttribute?.("placeholder"), input.getAttribute?.("aria-label"), ...labels ]); } function textLikeInputType(type) { switch (String(type || "").toLowerCase()) { case "": case "text": case "email": case "tel": case "number": return true; default: return false; } } function hintMatches(text, patterns) { return patterns.some((pattern) => pattern.test(text)); } function scopeHintText(scope) { const isDocumentScope = typeof document !== "undefined" && scope === document; if ((!scope || typeof scope !== "object") || (!isDocumentScope && typeof scope.querySelectorAll !== "function" && typeof scope.getAttribute !== "function")) { return ""; } const attrText = isDocumentScope ? "" : lowerJoined([ scope.getAttribute?.("id"), scope.getAttribute?.("name"), scope.getAttribute?.("class"), scope.getAttribute?.("action"), scope.getAttribute?.("aria-label") ]); const headingText = lowerJoined(Array.from(scope.querySelectorAll?.("button, h1, h2, h3, h4, legend, label, [role='button']") || []) .slice(0, 8) .map((element) => element.textContent || "")); return lowerJoined([attrText, headingText]); } function hasAuthFlowSignals(usernameInput, scope) { if (usernameInput) { return true; } return hintMatches(scopeHintText(scope), authScopePatterns); } const usernameHintPatterns = [ /\buser(name|id)?\b/, /\blog[\s_-]?in\b/, /\bsign[\s_-]?in\b/, /\bemail\b/, /\be-mail\b/, /\baccount\b/, /\bmember\b/, /\bidentifier\b/ ]; const nonLoginHintPatterns = [ /\bsearch\b/, /\bquery\b/, /\bfilter\b/, /\bcomment\b/, /\bmessage\b/, /\bcontact\b/, /\bcity\b/, /\bstate\b/, /\bpostal\b/, /\bzip\b/, /\bcoupon\b/, /\bpromo\b/, /\bnewsletter\b/, /\bsubscribe\b/ ]; const authScopePatterns = [ /\blog[\s_-]?in\b/, /\bsign[\s_-]?in\b/, /\bauth\b/, /\bpassword\b/, /\bpasscode\b/, /\b2fa\b/, /\btwo[\s-]?factor\b/, /\bverify\b/, /\baccount\b/ ]; function describeFieldRole(input) { const type = String(input?.getAttribute?.("type") || "").toLowerCase(); if (type === "password") { return "password"; } if (!textLikeInputType(type)) { return ""; } const hints = fieldHintText(input); if (!hints) { return ""; } if (hintMatches(hints, nonLoginHintPatterns)) { return ""; } if (hintMatches(hints, usernameHintPatterns)) { return "username"; } return ""; } 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 authFlowCandidate(anchorInput) { const scope = (typeof HTMLFormElement !== "undefined" && anchorInput?.form instanceof HTMLFormElement ? anchorInput.form : document); const scopeInputs = resolveFormInputs(anchorInput); const passwordInput = scopeInputs.find(isPasswordCandidate) || null; if (!passwordInput) { return null; } const associated = associatedFieldsForAnchor(anchorInput || passwordInput); if (!hasAuthFlowSignals(associated.usernameInput, scope)) { return null; } return { usernameInput: associated.usernameInput, passwordInput, anchorInput: anchorInput || passwordInput, scope }; } function loginCandidates() { const candidates = []; for (const passwordInput of visibleInputs(document).filter(isPasswordCandidate)) { const candidate = authFlowCandidate(passwordInput); if (!candidate) { continue; } if (candidates.some((existing) => existing.passwordInput === candidate.passwordInput)) { continue; } candidates.push(candidate); } return candidates; } 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 (typeof HTMLInputElement === "undefined" || !(input instanceof HTMLInputElement)) { return null; } const normalizedRole = normalizeRole(role || describeFieldRole(input)); const form = typeof HTMLFormElement !== "undefined" && 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); if (!normalizedRole) { return null; } 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 explicitRole = describeFieldRole(activeUsable); const activeTargets = activeUsable ? authFlowCandidate(activeUsable) : null; const candidates = loginCandidates(); const chosen = activeTargets || candidates[0] || null; const anchorInput = activeUsable || chosen?.passwordInput || chosen?.usernameInput || null; const focusRole = explicitRole || describeFieldRole(anchorInput); const focusTarget = anchorInput ? buildFieldDescriptor(anchorInput, focusRole) : null; const roles = candidates.map((candidate) => { const passwordDescriptor = buildFieldDescriptor(candidate.passwordInput, "password"); const usernameDescriptor = candidate.usernameInput ? buildFieldDescriptor(candidate.usernameInput, "username") : null; return `${passwordDescriptor.formIndex}:${passwordDescriptor.fieldIndex}:password:${usernameDescriptor ? `${usernameDescriptor.formIndex}:${usernameDescriptor.fieldIndex}` : "-"}`; }); return { pageHasLoginForm: Boolean(chosen), usernameInput: chosen?.usernameInput || null, passwordInput: chosen?.passwordInput || null, 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 submittedCredential(candidate, rawURL) { if (!candidate?.passwordInput) { return null; } const password = String(candidate.passwordInput.value || "").trim(); if (!password) { return null; } return { title: domainLabel(rawURL), username: String(candidate.usernameInput?.value || "").trim(), password, url: String(rawURL || "").trim() }; } 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, idleHidden) { if (suppressed || idleHidden || !hasTarget) { return false; } return Boolean( state?.pageHasLoginForm && ( state?.pendingFill || (state?.configured && state?.success && state?.status?.locked) || (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, fieldHintText, scopeHintText, hasAuthFlowSignals, authFlowCandidate, submittedCredential }; 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 inlineIdleHidden = false; let refreshTimer = null; let idleHideTimer = 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 clearIdleHideTimer() { if (idleHideTimer !== null) { clearTimeout(idleHideTimer); idleHideTimer = null; } } function refreshInlineLifetime(shouldShow) { clearIdleHideTimer(); if (!shouldShow || chooserOpen || pageState.pendingFill) { return; } idleHideTimer = window.setTimeout(() => { inlineIdleHidden = true; hideDock(); }, 15000); } 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, inlineIdleHidden); if (!shouldShow) { clearIdleHideTimer(); hideDock(); return; } ensureRootMounted(); dock.style.display = "block"; trigger.dataset.tone = pageState.pendingFill || pageState.status?.locked ? "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 if (pageState.status?.locked) { meta.textContent = "Unlock KeePassGO"; panelCopy.textContent = "Unlock KeePassGO to turn this field back into live login suggestions."; } 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(); refreshInlineLifetime(shouldShow); } function reportFieldState(force) { const scan = scanLoginFields(); const nextTarget = JSON.stringify(scan.focusTarget || null); if (scan.signature !== lastReportedSignature || nextTarget !== lastReportedTarget) { inlineIdleHidden = false; } pageState = { ...pageState, pageHasLoginForm: scan.pageHasLoginForm, focusTarget: scan.focusTarget }; renderInlineState(); 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("submit", (event) => { const form = event.target instanceof HTMLFormElement ? event.target : null; if (!form) { return; } const passwordInput = visibleInputs(form).find(isPasswordCandidate) || null; const candidate = passwordInput ? authFlowCandidate(passwordInput) : null; const observed = submittedCredential(candidate, window.location.href); if (!observed) { return; } void runtimeSend({ type: "keepassgo-observed-login", observed }).catch(() => null); }, 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); }