Add browser match controls
ci / lint-test (pull_request) Successful in 7m19s
ci / build (pull_request) Successful in 7m13s

This commit is contained in:
Joe Julian
2026-04-23 23:10:05 -07:00
parent 2269944702
commit 0538cd2feb
6 changed files with 238 additions and 8 deletions
+121 -6
View File
@@ -3,7 +3,10 @@ const nativeHost = "com.keepassgo.browser";
const isNodeTestEnv = typeof module !== "undefined" && module.exports;
const usePromiseAPI = typeof globalThis.browser !== "undefined";
const defaultSettings = {
bearerToken: ""
bearerToken: "",
bestMatchOnly: false,
requireSchemeMatch: false,
sortResults: "quality"
};
const pageStatePrefix = "keepassgo-page-state:";
const matchCacheTTL = 30 * 1000;
@@ -173,12 +176,20 @@ function connectNative(message) {
}
async function loadSettings() {
const stored = await storageGet(["bearerToken"]);
const stored = await storageGet(["bearerToken", "bestMatchOnly", "requireSchemeMatch", "sortResults"]);
const sortResults = normalizeSortResults(stored.sortResults);
return {
bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim()
bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim(),
bestMatchOnly: Boolean(stored.bestMatchOnly ?? defaultSettings.bestMatchOnly),
requireSchemeMatch: Boolean(stored.requireSchemeMatch ?? defaultSettings.requireSchemeMatch),
sortResults
};
}
function normalizeSortResults(value) {
return ["quality", "title", "path"].includes(value) ? value : defaultSettings.sortResults;
}
function supportsPageStateURL(rawURL) {
return typeof rawURL === "string" && /^https?:\/\//i.test(rawURL);
}
@@ -238,6 +249,95 @@ function matchHost(rawURL) {
}
}
function matchScheme(rawURL) {
if (typeof rawURL !== "string") {
return "";
}
const trimmed = rawURL.trim();
if (!trimmed) {
return "";
}
try {
return new URL(trimmed).protocol.toLowerCase();
} catch (_error) {
return "";
}
}
function qualityRank(quality) {
switch (String(quality || "").trim().toLowerCase()) {
case "exact":
return 0;
case "scheme":
return 1;
case "host":
return 2;
default:
return 3;
}
}
function compareMatchText(left, right) {
return String(left || "").localeCompare(String(right || ""), undefined, { sensitivity: "base" });
}
function sortMatches(matches, sortResults) {
const sorted = [...matches];
switch (normalizeSortResults(sortResults)) {
case "title":
sorted.sort((left, right) =>
compareMatchText(left?.title, right?.title) ||
compareMatchText(left?.username, right?.username) ||
compareMatchText(left?.path?.join("/"), right?.path?.join("/"))
);
return sorted;
case "path":
sorted.sort((left, right) =>
compareMatchText(left?.path?.join("/"), right?.path?.join("/")) ||
compareMatchText(left?.title, right?.title) ||
compareMatchText(left?.username, right?.username)
);
return sorted;
default:
sorted.sort((left, right) =>
qualityRank(left?.quality) - qualityRank(right?.quality) ||
compareMatchText(left?.title, right?.title) ||
compareMatchText(left?.username, right?.username)
);
return sorted;
}
}
function filterSchemeMatches(matches, pageURL) {
const pageScheme = matchScheme(pageURL);
if (!pageScheme) {
return [...matches];
}
return matches.filter((match) => {
const entryScheme = matchScheme(match?.url);
return !entryScheme || entryScheme === pageScheme;
});
}
function filterBestQuality(matches) {
if (!Array.isArray(matches) || matches.length === 0) {
return [];
}
const bestRank = Math.min(...matches.map((match) => qualityRank(match?.quality)));
return matches.filter((match) => qualityRank(match?.quality) === bestRank);
}
function applyMatchControls(matches, settings, pageURL) {
let filtered = Array.isArray(matches) ? [...matches] : [];
if (settings?.requireSchemeMatch) {
filtered = filterSchemeMatches(filtered, pageURL);
}
if (settings?.bestMatchOnly) {
filtered = filterBestQuality(filtered);
}
return sortMatches(filtered, settings?.sortResults);
}
function defaultObservedTitle(observed) {
if (observed?.title) {
return observed.title;
@@ -623,6 +723,11 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
bearerToken: settings.bearerToken,
url: resolvedURL
});
const filteredMatches = applyMatchControls(
Array.isArray(matches?.matches) ? matches.matches : [],
settings,
resolvedURL
);
state = {
...state,
@@ -632,7 +737,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: filteredMatches,
error: matches?.error ?? "",
updatedAt: Date.now()
};
@@ -813,6 +918,7 @@ async function saveObservedLogin(tabId, selectedMatch = null) {
}
const backgroundTestExports = {
applyMatchControls,
normalizePageState,
actionPresentationForState,
shouldReuseMatches,
@@ -853,22 +959,31 @@ 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),
requireSchemeMatch: Boolean(message.settings?.requireSchemeMatch),
sortResults: normalizeSortResults(message.settings?.sortResults)
});
await refreshActivePage({ force: true }).catch(() => null);
sendResponse({ success: true });
return;
case "keepassgo-search-logins": {
const settings = await loadSettings();
const page = await activePageContext();
const response = await connectNative({
action: "search-logins",
bearerToken: settings.bearerToken,
query: String(message?.query || "").trim()
});
const searchResults = applyMatchControls(
Array.isArray(response?.searchResults) ? response.searchResults : [],
settings,
page.url
);
sendResponse({
success: Boolean(response?.success),
error: response?.error || "",
results: Array.isArray(response?.searchResults) ? response.searchResults : [],
results: searchResults,
status: response?.status ?? null
});
return;