diff --git a/browser/extension/background.js b/browser/extension/background.js index 4ff07ad..4572290 100644 --- a/browser/extension/background.js +++ b/browser/extension/background.js @@ -3,7 +3,8 @@ const nativeHost = "com.keepassgo.browser"; const isNodeTestEnv = typeof module !== "undefined" && module.exports; const usePromiseAPI = typeof globalThis.browser !== "undefined"; const defaultSettings = { - bearerToken: "" + bearerToken: "", + bestMatchOnly: false }; const pageStatePrefix = "keepassgo-page-state:"; const matchCacheTTL = 30 * 1000; @@ -173,9 +174,10 @@ function connectNative(message) { } async function loadSettings() { - const stored = await storageGet(["bearerToken"]); + const stored = await storageGet(["bearerToken", "bestMatchOnly"]); return { - bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim() + bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim(), + bestMatchOnly: Boolean(stored.bestMatchOnly ?? defaultSettings.bestMatchOnly) }; } @@ -238,6 +240,30 @@ function matchHost(rawURL) { } } +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; @@ -632,7 +658,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) { pendingMessage: tokenPendingApprovalCount(matches?.status ?? state.status) > 0 ? approvalHintForState(state) || "Approve or deny the browser fill request in KeePassGO." : "", - matches: Array.isArray(matches?.matches) ? matches.matches : [], + matches: applyBestMatchOnly(matches?.matches, settings.bestMatchOnly), error: matches?.error ?? "", updatedAt: Date.now() }; @@ -813,6 +839,7 @@ async function saveObservedLogin(tabId, selectedMatch = null) { } const backgroundTestExports = { + applyBestMatchOnly, normalizePageState, actionPresentationForState, shouldReuseMatches, @@ -853,7 +880,8 @@ if (isNodeTestEnv) { return; case "keepassgo-save-settings": await storageSet({ - bearerToken: String(message.settings?.bearerToken || "").trim() + bearerToken: String(message.settings?.bearerToken || "").trim(), + bestMatchOnly: Boolean(message.settings?.bestMatchOnly) }); await refreshActivePage({ force: true }).catch(() => null); sendResponse({ success: true }); @@ -868,7 +896,7 @@ if (isNodeTestEnv) { sendResponse({ success: Boolean(response?.success), error: response?.error || "", - results: Array.isArray(response?.searchResults) ? response.searchResults : [], + results: applyBestMatchOnly(response?.searchResults, settings.bestMatchOnly), status: response?.status ?? null }); return; diff --git a/browser/extension/background.test.cjs b/browser/extension/background.test.cjs index b030502..3bae90e 100644 --- a/browser/extension/background.test.cjs +++ b/browser/extension/background.test.cjs @@ -69,6 +69,7 @@ test("shouldContinueWatchingState keeps polling locked login pages", () => { test("default settings include a blank bearer token that can be overridden by harness patching", () => { assert.equal(background.defaultSettings.bearerToken, ""); + assert.equal(background.defaultSettings.bestMatchOnly, false); }); test("savePlanForObservedLogin prefers updating an exact username match", () => { @@ -129,3 +130,22 @@ test("savePlanForObservedLogin falls back to saving into the current page group" url: "https://vault.example.invalid/login" }); }); + +test("applyBestMatchOnly keeps only the strongest quality band when enabled", () => { + const filtered = background.applyBestMatchOnly([ + { id: "livingston", title: "Livingston Dell", quality: "exact" }, + { id: "rusty", title: "Rusty Ryan", quality: "host" }, + { id: "linus", title: "Linus Caldwell", quality: "scheme" } + ], true); + + assert.deepEqual(filtered.map((match) => match.id), ["livingston"]); +}); + +test("applyBestMatchOnly preserves all matches when disabled", () => { + const filtered = background.applyBestMatchOnly([ + { id: "livingston", title: "Livingston Dell", quality: "exact" }, + { id: "rusty", title: "Rusty Ryan", quality: "host" } + ], false); + + assert.deepEqual(filtered.map((match) => match.id), ["livingston", "rusty"]); +}); diff --git a/browser/extension/options.html b/browser/extension/options.html index 1f9a6b1..6bd602c 100644 --- a/browser/extension/options.html +++ b/browser/extension/options.html @@ -19,6 +19,13 @@ API token +