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 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 }; 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 = `