const ext = globalThis.browser ?? globalThis.chrome; const nativeHost = "com.keepassgo.browser"; const isNodeTestEnv = typeof module !== "undefined" && module.exports; const usePromiseAPI = typeof globalThis.browser !== "undefined"; const defaultSettings = { bearerToken: "", bestMatchOnly: false }; const pageStatePrefix = "keepassgo-page-state:"; const matchCacheTTL = 30 * 1000; const pendingPollMillis = 1500; const pageStates = new Map(); const refreshJobs = new Map(); const pendingPollers = new Map(); function storageGet(keys) { if (usePromiseAPI) { return ext.storage.local.get(keys); } return new Promise((resolve, reject) => { ext.storage.local.get(keys, (value) => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(value); }); }); } function storageSet(value) { if (usePromiseAPI) { return ext.storage.local.set(value); } return new Promise((resolve, reject) => { ext.storage.local.set(value, () => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(); }); }); } function sessionArea() { return ext.storage?.session ?? null; } function sessionStorageGet(keys) { const area = sessionArea(); if (!area) { return Promise.resolve({}); } if (usePromiseAPI) { return area.get(keys).then((value) => value || {}); } return new Promise((resolve, reject) => { area.get(keys, (value) => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(value || {}); }); }); } function sessionStorageSet(value) { const area = sessionArea(); if (!area) { return Promise.resolve(); } if (usePromiseAPI) { return area.set(value); } return new Promise((resolve, reject) => { area.set(value, () => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(); }); }); } function sessionStorageRemove(keys) { const area = sessionArea(); if (!area) { return Promise.resolve(); } if (usePromiseAPI) { return area.remove(keys); } return new Promise((resolve, reject) => { area.remove(keys, () => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(); }); }); } function tabsQuery(query) { if (usePromiseAPI) { return ext.tabs.query(query); } return new Promise((resolve, reject) => { ext.tabs.query(query, (tabs) => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(tabs); }); }); } function tabsGet(tabId) { if (usePromiseAPI) { return ext.tabs.get(tabId); } return new Promise((resolve, reject) => { ext.tabs.get(tabId, (tab) => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(tab); }); }); } function tabsSendMessage(tabId, message) { if (usePromiseAPI) { return ext.tabs.sendMessage(tabId, message); } return new Promise((resolve, reject) => { ext.tabs.sendMessage(tabId, message, (response) => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(response); }); }); } function connectNative(message) { if (usePromiseAPI) { return ext.runtime.sendNativeMessage(nativeHost, message); } return new Promise((resolve, reject) => { ext.runtime.sendNativeMessage(nativeHost, message, (response) => { const error = ext.runtime.lastError; if (error) { reject(new Error(error.message)); return; } resolve(response); }); }); } async function loadSettings() { const stored = await storageGet(["bearerToken", "bestMatchOnly"]); return { bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim(), bestMatchOnly: Boolean(stored.bestMatchOnly ?? defaultSettings.bestMatchOnly) }; } function supportsPageStateURL(rawURL) { return typeof rawURL === "string" && /^https?:\/\//i.test(rawURL); } function pageStateKey(tabId) { return `${pageStatePrefix}${String(tabId)}`; } function cloneTarget(target) { return target && typeof target === "object" ? { ...target } : null; } function cloneSavePlan(plan) { if (!plan || typeof plan !== "object") { return null; } return { mode: plan.mode === "update" ? "update" : "save", entryId: typeof plan.entryId === "string" ? plan.entryId : "", title: typeof plan.title === "string" ? plan.title : "", path: Array.isArray(plan.path) ? [...plan.path] : [], username: typeof plan.username === "string" ? plan.username : "", password: typeof plan.password === "string" ? plan.password : "", url: typeof plan.url === "string" ? plan.url : "" }; } function normalizeObservedCredential(observed) { if (!observed || typeof observed !== "object") { return null; } const password = typeof observed.password === "string" ? observed.password.trim() : ""; const url = typeof observed.url === "string" ? observed.url.trim() : ""; if (!password || !url) { return null; } return { title: typeof observed.title === "string" ? observed.title.trim() : "", username: typeof observed.username === "string" ? observed.username.trim() : "", password, url }; } function matchHost(rawURL) { if (typeof rawURL !== "string") { return ""; } const trimmed = rawURL.trim(); if (!trimmed) { return ""; } try { return new URL(trimmed).hostname.toLowerCase(); } catch (_error) { return trimmed.replace(/^https?:\/\//i, "").replace(/\/.*$/, "").toLowerCase(); } } function qualityRank(quality) { switch (String(quality || "").trim().toLowerCase()) { case "exact": return 0; case "scheme": return 1; case "host": return 2; default: return 3; } } function applyBestMatchOnly(matches, enabled) { if (!Array.isArray(matches) || !enabled) { return Array.isArray(matches) ? [...matches] : []; } if (matches.length === 0) { return []; } const bestRank = Math.min(...matches.map((match) => qualityRank(match?.quality))); return matches.filter((match) => qualityRank(match?.quality) === bestRank); } function defaultObservedTitle(observed) { if (observed?.title) { return observed.title; } return matchHost(observed?.url) || "Browser Login"; } function savePlanForObservedLogin(observed, matches) { const normalized = normalizeObservedCredential(observed); if (!normalized) { return null; } const targetHost = matchHost(normalized.url); const exact = Array.isArray(matches) ? matches.find((match) => typeof match?.id === "string" && String(match?.username || "").trim().toLowerCase() === normalized.username.toLowerCase() && matchHost(match?.url || "") === targetHost ) : null; if (exact) { return { mode: "update", entryId: exact.id, title: exact.title || defaultObservedTitle(normalized), path: Array.isArray(exact.path) ? [...exact.path] : [], username: normalized.username, password: normalized.password, url: normalized.url }; } const fallbackPath = Array.isArray(matches) && matches.length > 0 && Array.isArray(matches[0]?.path) ? [...matches[0].path] : []; return { mode: "save", entryId: "", title: defaultObservedTitle(normalized), path: fallbackPath, username: normalized.username, password: normalized.password, url: normalized.url }; } function normalizePageState(state) { return { tabId: Number.isInteger(state?.tabId) ? state.tabId : null, pageUrl: typeof state?.pageUrl === "string" ? state.pageUrl : "", configured: Boolean(state?.configured), success: state?.success !== false, status: state?.status ?? null, matches: Array.isArray(state?.matches) ? state.matches : [], error: typeof state?.error === "string" ? state.error : "", pageHasLoginForm: Boolean(state?.pageHasLoginForm), signature: typeof state?.signature === "string" ? state.signature : "", focusTarget: cloneTarget(state?.focusTarget), pendingFill: Boolean(state?.pendingFill), pendingEntryId: typeof state?.pendingEntryId === "string" ? state.pendingEntryId : "", pendingTarget: cloneTarget(state?.pendingTarget), pendingMessage: typeof state?.pendingMessage === "string" ? state.pendingMessage : "", pendingSave: cloneSavePlan(state?.pendingSave), lastFilledEntryId: typeof state?.lastFilledEntryId === "string" ? state.lastFilledEntryId : "", updatedAt: Number.isFinite(state?.updatedAt) ? state.updatedAt : 0 }; } function defaultPageState(tabId, pageUrl) { return normalizePageState({ tabId, pageUrl, configured: true, success: true, status: null, matches: [], error: "", pageHasLoginForm: false, signature: "", focusTarget: null, pendingFill: false, pendingEntryId: "", pendingTarget: null, pendingMessage: "", pendingSave: null, lastFilledEntryId: "", updatedAt: 0 }); } async function getPageState(tabId, pageUrl) { if (!Number.isInteger(tabId)) { return defaultPageState(null, pageUrl || ""); } const existing = pageStates.get(tabId); if (existing && (!pageUrl || existing.pageUrl === pageUrl)) { return normalizePageState(existing); } const stored = await sessionStorageGet(pageStateKey(tabId)); const state = normalizePageState(stored[pageStateKey(tabId)] || defaultPageState(tabId, pageUrl || "")); if (pageUrl && state.pageUrl !== pageUrl) { return defaultPageState(tabId, pageUrl); } pageStates.set(tabId, state); return state; } async function setPageState(tabId, nextState) { const state = normalizePageState({ ...nextState, tabId }); if (!Number.isInteger(tabId)) { return state; } pageStates.set(tabId, state); await sessionStorageSet({ [pageStateKey(tabId)]: state }); await updateActionState(tabId, state); await notifyContentState(tabId, state); return state; } function clearPendingPoll(tabId) { const timer = pendingPollers.get(tabId); if (timer !== undefined) { clearTimeout(timer); pendingPollers.delete(tabId); } } async function clearPageState(tabId) { if (!Number.isInteger(tabId)) { return; } pageStates.delete(tabId); refreshJobs.delete(tabId); clearPendingPoll(tabId); await sessionStorageRemove(pageStateKey(tabId)); await clearActionState(tabId); } function describeError(error) { return error instanceof Error ? error.message : String(error || "Unknown error"); } function approvalHintForState(state) { if (!state.pendingFill) { return ""; } return state.pendingMessage || "Approve or deny the fill request in KeePassGO."; } function shouldContinueWatchingState(state) { if (!state?.pageHasLoginForm) { return false; } if (state?.pendingFill) { return true; } return Boolean(state?.status?.locked); } function schedulePendingPoll(tabId, pageUrl) { if (!Number.isInteger(tabId)) { return; } clearPendingPoll(tabId); const timer = setTimeout(() => { pendingPollers.delete(tabId); void refreshPageState(tabId, pageUrl, { force: true }).catch(() => null); }, pendingPollMillis); pendingPollers.set(tabId, timer); } async function notifyContentState(tabId, state) { if (!Number.isInteger(tabId)) { return; } try { await tabsSendMessage(tabId, { type: "keepassgo-page-state", state }); } catch (_error) { // Ignore pages without a ready content script. } } async function clearActionState(tabId) { if (!Number.isInteger(tabId) || !ext.action) { return; } await Promise.allSettled([ ext.action.setBadgeText({ tabId, text: "" }), ext.action.setTitle({ tabId, title: "KeePassGO Browser" }) ]); } function actionPresentationForState(state) { let badgeText = ""; let title = "KeePassGO Browser"; let color = "#255f4a"; if (state.pendingFill) { badgeText = "!"; color = "#9f5f0e"; title = approvalHintForState(state) || "KeePassGO approval needed for this page"; } else if (state.pendingSave) { badgeText = "S"; color = "#255f4a"; title = state.pendingSave.mode === "update" ? `KeePassGO can update ${state.pendingSave.title || "this login"}` : "KeePassGO can save the submitted login"; } else if (!state.configured) { title = "Configure KeePassGO Browser in extension settings"; } else if (!state.success) { badgeText = "!"; color = "#9f2f2f"; title = state.error || "KeePassGO is unavailable for this page"; } else if (state.status?.locked) { title = "Unlock KeePassGO to fill this page"; } else if (state.pageHasLoginForm && state.matches.length > 0) { badgeText = String(Math.min(state.matches.length, 9)); title = `KeePassGO found ${state.matches.length} matching entr${state.matches.length === 1 ? "y" : "ies"} on this page`; } else if (state.pageHasLoginForm) { title = "KeePassGO found no matching entries on this page"; } return { badgeText, title, color }; } async function updateActionState(tabId, state) { if (!Number.isInteger(tabId) || !ext.action) { return; } const presentation = actionPresentationForState(state); await Promise.allSettled([ ext.action.setBadgeText({ tabId, text: presentation.badgeText }), ext.action.setBadgeBackgroundColor({ tabId, color: presentation.color }), ext.action.setTitle({ tabId, title: presentation.title }) ]); } async function activePageContext() { const [tab] = await tabsQuery({ active: true, currentWindow: true }); return { tabId: Number.isInteger(tab?.id) ? tab.id : null, url: typeof tab?.url === "string" ? tab.url : "" }; } async function scanTabForLoginForm(tabId) { if (!Number.isInteger(tabId)) { return { pageHasLoginForm: false, focusTarget: null, signature: "" }; } try { const response = await tabsSendMessage(tabId, { type: "keepassgo-page-scan" }); return { pageHasLoginForm: Boolean(response?.pageHasLoginForm), focusTarget: cloneTarget(response?.focusTarget), signature: typeof response?.signature === "string" ? response.signature : "" }; } catch (_error) { return { pageHasLoginForm: false, focusTarget: null, signature: "" }; } } function shouldReuseMatches(state, force) { if (force || state.pendingFill) { return false; } if (!state.pageHasLoginForm || !Array.isArray(state.matches)) { return false; } return Date.now() - (state.updatedAt || 0) < matchCacheTTL; } function tokenPendingApprovalCount(status) { return Number(status?.tokenPendingApprovalCount || 0); } async function fetchStatus(settings) { if (!settings.bearerToken) { return { success: false, configured: false, status: null, error: "Set an API token in extension settings." }; } const status = await connectNative({ action: "status", bearerToken: settings.bearerToken }); return { success: Boolean(status?.success), configured: true, status: status?.status ?? null, error: status?.error ?? "" }; } async function refreshPageState(tabId, pageUrl, options = {}) { if (!Number.isInteger(tabId)) { return defaultPageState(null, pageUrl || ""); } const force = Boolean(options.force); const existingJob = refreshJobs.get(tabId); if (existingJob && !force) { return existingJob; } const job = (async () => { let resolvedURL = typeof pageUrl === "string" ? pageUrl : ""; if (!supportsPageStateURL(resolvedURL)) { const tab = await tabsGet(tabId).catch(() => null); resolvedURL = typeof tab?.url === "string" ? tab.url : resolvedURL; } if (!supportsPageStateURL(resolvedURL)) { await clearPageState(tabId); return defaultPageState(tabId, resolvedURL || ""); } let state = await getPageState(tabId, resolvedURL); if (state.pageUrl !== resolvedURL) { state = defaultPageState(tabId, resolvedURL); } const scan = typeof options.pageHasLoginForm === "boolean" ? { pageHasLoginForm: options.pageHasLoginForm, focusTarget: cloneTarget(options.focusTarget) || state.focusTarget, signature: typeof options.signature === "string" ? options.signature : state.signature } : await scanTabForLoginForm(tabId); state = { ...state, pageUrl: resolvedURL, pageHasLoginForm: scan.pageHasLoginForm, focusTarget: cloneTarget(scan.focusTarget) || state.focusTarget, signature: typeof scan.signature === "string" ? scan.signature : state.signature }; const settings = await loadSettings(); const statusInfo = await fetchStatus(settings).catch((error) => ({ success: false, configured: true, status: null, error: describeError(error) })); state = { ...state, configured: statusInfo.configured, success: statusInfo.success, status: statusInfo.status, pendingFill: state.pendingFill || tokenPendingApprovalCount(statusInfo.status) > 0, pendingMessage: tokenPendingApprovalCount(statusInfo.status) > 0 ? approvalHintForState(state) || "Approve or deny the browser fill request in KeePassGO." : "", error: statusInfo.error }; if (!statusInfo.configured || !statusInfo.success || statusInfo.status?.locked || !state.pageHasLoginForm) { state.matches = []; state.updatedAt = Date.now(); const saved = await setPageState(tabId, state); if (shouldContinueWatchingState(saved)) { schedulePendingPoll(tabId, resolvedURL); } else { clearPendingPoll(tabId); } return saved; } if (shouldReuseMatches(state, force)) { const saved = await setPageState(tabId, state); if (shouldContinueWatchingState(saved)) { schedulePendingPoll(tabId, resolvedURL); } else { clearPendingPoll(tabId); } return saved; } const matches = await connectNative({ action: "find-logins", bearerToken: settings.bearerToken, url: resolvedURL }); state = { ...state, success: Boolean(matches?.success), status: matches?.status ?? state.status, pendingFill: state.pendingFill || tokenPendingApprovalCount(matches?.status ?? state.status) > 0, pendingMessage: tokenPendingApprovalCount(matches?.status ?? state.status) > 0 ? approvalHintForState(state) || "Approve or deny the browser fill request in KeePassGO." : "", matches: applyBestMatchOnly(matches?.matches, settings.bestMatchOnly), error: matches?.error ?? "", updatedAt: Date.now() }; const saved = await setPageState(tabId, state); if (shouldContinueWatchingState(saved)) { schedulePendingPoll(tabId, resolvedURL); } else { clearPendingPoll(tabId); } return saved; })().finally(() => { if (refreshJobs.get(tabId) === job) { refreshJobs.delete(tabId); } }); refreshJobs.set(tabId, job); return job; } async function statusForPage(options = {}) { let page = await activePageContext(); if (Number.isInteger(options.tabId)) { const tab = await tabsGet(options.tabId).catch(() => null); page = { tabId: options.tabId, url: typeof tab?.url === "string" ? tab.url : "" }; } if (page.tabId == null) { return defaultPageState(null, page.url); } if (!options.force) { const cached = await getPageState(page.tabId, page.url); if (cached.pageUrl === page.url && cached.updatedAt && !cached.pendingFill) { return cached; } } return refreshPageState(page.tabId, page.url, options); } function matchedLoginCredentialRequest(settings, entryId, pageUrl) { return { action: "get-login", bearerToken: settings.bearerToken, entryId, url: pageUrl }; } function selectedLoginCredentialRequest(settings, entryId) { return { action: "get-login", bearerToken: settings.bearerToken, entryId }; } async function fillMatchedLogin(tabId, entryId) { const page = await loginFillPage(tabId); return fillLoginOnPage(tabId, entryId, page.url, matchedLoginCredentialRequest); } async function fillSelectedLogin(tabId, entryId) { const page = await loginFillPage(tabId); return fillLoginOnPage(tabId, entryId, page.url, selectedLoginCredentialRequest); } async function loginFillPage(tabId) { if (!Number.isInteger(tabId)) { throw new Error("No active tab is available."); } const tab = await tabsGet(tabId); const pageUrl = typeof tab?.url === "string" ? tab.url : ""; if (!supportsPageStateURL(pageUrl)) { throw new Error("This page cannot be filled."); } return { url: pageUrl }; } async function fillLoginOnPage(tabId, entryId, pageUrl, credentialRequest) { let state = await getPageState(tabId, pageUrl); state = await setPageState(tabId, { ...state, pageUrl, pendingFill: true, pendingEntryId: String(entryId || "").trim(), pendingTarget: cloneTarget(state.focusTarget), pendingMessage: "Approve or deny the browser fill request in KeePassGO.", error: "", updatedAt: Date.now() }); schedulePendingPoll(tabId, pageUrl); try { const settings = await loadSettings(); if (!settings.bearerToken) { throw new Error("API token is not configured."); } const response = await connectNative(credentialRequest(settings, entryId, pageUrl)); if (!response?.success || !response.credential) { throw new Error(response?.error || "KeePassGO did not return a credential."); } const fillResponse = await tabsSendMessage(tabId, { type: "keepassgo-fill-credential", credential: response.credential, target: state.pendingTarget }); if (!fillResponse?.ok) { throw new Error(fillResponse?.error || "The current page could not be filled."); } state = await setPageState(tabId, { ...state, pendingFill: false, pendingEntryId: "", pendingTarget: null, pendingMessage: "", lastFilledEntryId: String(entryId || "").trim(), error: "", updatedAt: Date.now() }); clearPendingPoll(tabId); return { credential: response.credential, pageUrl, state }; } catch (error) { state = await setPageState(tabId, { ...state, pendingFill: false, pendingEntryId: "", pendingTarget: null, pendingMessage: "", error: describeError(error), updatedAt: Date.now() }); clearPendingPoll(tabId); throw error; } } async function refreshActivePage(options = {}) { const page = await activePageContext(); if (page.tabId == null) { return defaultPageState(null, page.url); } return refreshPageState(page.tabId, page.url, options); } async function saveObservedLogin(tabId, selectedMatch = null) { if (!Number.isInteger(tabId)) { throw new Error("No active tab is available."); } const tab = await tabsGet(tabId); const pageUrl = typeof tab?.url === "string" ? tab.url : ""; let state = await getPageState(tabId, pageUrl); const pendingSave = cloneSavePlan(state.pendingSave); if (!pendingSave) { throw new Error("There is no pending login to save."); } const request = { action: "save-login", title: pendingSave.title, username: pendingSave.username, password: pendingSave.password, url: pendingSave.url }; if (selectedMatch && typeof selectedMatch === "object") { if (pendingSave.mode === "update" && typeof selectedMatch.id === "string" && selectedMatch.id.trim()) { request.entryId = selectedMatch.id.trim(); request.title = String(selectedMatch.title || pendingSave.title || "").trim(); } else if (Array.isArray(selectedMatch.path) && selectedMatch.path.length > 0) { request.path = [...selectedMatch.path]; } } else if (pendingSave.mode === "update" && pendingSave.entryId) { request.entryId = pendingSave.entryId; } else if (pendingSave.path.length > 0) { request.path = [...pendingSave.path]; } const settings = await loadSettings(); if (!settings.bearerToken) { throw new Error("API token is not configured."); } const response = await connectNative({ ...request, bearerToken: settings.bearerToken }); if (!response?.success) { throw new Error(response?.error || "KeePassGO did not save the submitted login."); } state = await setPageState(tabId, { ...state, pendingSave: null, error: "", updatedAt: Date.now() }); await refreshPageState(tabId, pageUrl, { force: true }); return { state }; } const backgroundTestExports = { applyBestMatchOnly, normalizePageState, actionPresentationForState, shouldReuseMatches, shouldContinueWatchingState, tokenPendingApprovalCount, savePlanForObservedLogin, matchedLoginCredentialRequest, selectedLoginCredentialRequest, defaultSettings }; if (isNodeTestEnv) { module.exports = backgroundTestExports; } else { ext.runtime.onMessage.addListener((message, sender, sendResponse) => { (async () => { switch (message?.type) { case "keepassgo-popup-state": sendResponse(await statusForPage({ force: Boolean(message.force), tabId: Number.isInteger(message?.tabId) ? message.tabId : null })); return; case "keepassgo-fill-entry": { const targetTabID = Number.isInteger(message?.tabId) ? message.tabId : (Number.isInteger(sender?.tab?.id) ? sender.tab.id : (await activePageContext()).tabId); if (Number.isInteger(targetTabID) && message.target) { const targetState = await getPageState(targetTabID, ""); await setPageState(targetTabID, { ...targetState, focusTarget: cloneTarget(message.target) }); } sendResponse({ success: true, ...(await fillMatchedLogin(targetTabID, message.entryId)) }); return; } case "keepassgo-fill-selected-entry": { const targetTabID = Number.isInteger(message?.tabId) ? message.tabId : (Number.isInteger(sender?.tab?.id) ? sender.tab.id : (await activePageContext()).tabId); sendResponse({ success: true, ...(await fillSelectedLogin(targetTabID, message.entryId)) }); return; } case "keepassgo-load-settings": sendResponse({ success: true, settings: await loadSettings() }); return; case "keepassgo-save-settings": await storageSet({ bearerToken: String(message.settings?.bearerToken || "").trim(), bestMatchOnly: Boolean(message.settings?.bestMatchOnly) }); await refreshActivePage({ force: true }).catch(() => null); sendResponse({ success: true }); return; case "keepassgo-search-logins": { const settings = await loadSettings(); const response = await connectNative({ action: "search-logins", bearerToken: settings.bearerToken, query: String(message?.query || "").trim() }); sendResponse({ success: Boolean(response?.success), error: response?.error || "", results: applyBestMatchOnly(response?.searchResults, settings.bestMatchOnly), status: response?.status ?? null }); return; } case "keepassgo-observed-login": if (Number.isInteger(sender?.tab?.id)) { const targetState = await getPageState(sender.tab.id, sender.tab.url || ""); const nextSave = savePlanForObservedLogin(message.observed, targetState.matches); sendResponse(await setPageState(sender.tab.id, { ...targetState, pendingSave: nextSave, updatedAt: Date.now() })); return; } sendResponse({ success: false, error: "No active tab is available." }); return; case "keepassgo-save-login": { const targetTabID = Number.isInteger(message?.tabId) ? message.tabId : (Number.isInteger(sender?.tab?.id) ? sender.tab.id : (await activePageContext()).tabId); const selectedMatch = message?.selectedMatch && typeof message.selectedMatch === "object" ? { id: String(message.selectedMatch.id || "").trim(), title: String(message.selectedMatch.title || "").trim(), path: Array.isArray(message.selectedMatch.path) ? message.selectedMatch.path : [] } : null; sendResponse({ success: true, ...(await saveObservedLogin(targetTabID, selectedMatch)) }); return; } case "keepassgo-page-ready": if (Number.isInteger(sender?.tab?.id)) { sendResponse(await refreshPageState(sender.tab.id, sender.tab.url, { force: Boolean(message.force), pageHasLoginForm: Boolean(message.pageHasLoginForm), focusTarget: cloneTarget(message.focusTarget), signature: typeof message.signature === "string" ? message.signature : "" })); return; } sendResponse(defaultPageState(null, "")); return; case "keepassgo-refresh-page-state": if (Number.isInteger(sender?.tab?.id)) { sendResponse(await refreshPageState(sender.tab.id, sender.tab.url, { force: true })); return; } sendResponse(defaultPageState(null, "")); return; default: sendResponse({ success: false, error: `Unsupported message ${message?.type || ""}`.trim() }); } })().catch((error) => { sendResponse({ success: false, error: describeError(error) }); }); return true; }); ext.tabs?.onActivated?.addListener(({ tabId }) => { void refreshPageState(tabId, "", { force: false }).catch(() => null); }); ext.tabs?.onUpdated?.addListener((tabId, changeInfo, tab) => { if (typeof changeInfo.url === "string") { void clearPageState(tabId).catch(() => null); } if (changeInfo.status === "complete") { void refreshPageState(tabId, tab?.url || changeInfo.url || "", { force: false }).catch(() => null); } }); ext.tabs?.onRemoved?.addListener((tabId) => { void clearPageState(tabId).catch(() => null); }); }