Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1f30f5936 |
@@ -4,9 +4,7 @@ const isNodeTestEnv = typeof module !== "undefined" && module.exports;
|
|||||||
const usePromiseAPI = typeof globalThis.browser !== "undefined";
|
const usePromiseAPI = typeof globalThis.browser !== "undefined";
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
bearerToken: "",
|
bearerToken: "",
|
||||||
bestMatchOnly: false,
|
bestMatchOnly: false
|
||||||
requireSchemeMatch: false,
|
|
||||||
sortResults: "quality"
|
|
||||||
};
|
};
|
||||||
const pageStatePrefix = "keepassgo-page-state:";
|
const pageStatePrefix = "keepassgo-page-state:";
|
||||||
const matchCacheTTL = 30 * 1000;
|
const matchCacheTTL = 30 * 1000;
|
||||||
@@ -176,20 +174,13 @@ function connectNative(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const stored = await storageGet(["bearerToken", "bestMatchOnly", "requireSchemeMatch", "sortResults"]);
|
const stored = await storageGet(["bearerToken", "bestMatchOnly"]);
|
||||||
const sortResults = normalizeSortResults(stored.sortResults);
|
|
||||||
return {
|
return {
|
||||||
bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim(),
|
bearerToken: (stored.bearerToken || defaultSettings.bearerToken).trim(),
|
||||||
bestMatchOnly: Boolean(stored.bestMatchOnly ?? defaultSettings.bestMatchOnly),
|
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) {
|
function supportsPageStateURL(rawURL) {
|
||||||
return typeof rawURL === "string" && /^https?:\/\//i.test(rawURL);
|
return typeof rawURL === "string" && /^https?:\/\//i.test(rawURL);
|
||||||
}
|
}
|
||||||
@@ -249,21 +240,6 @@ 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) {
|
function qualityRank(quality) {
|
||||||
switch (String(quality || "").trim().toLowerCase()) {
|
switch (String(quality || "").trim().toLowerCase()) {
|
||||||
case "exact":
|
case "exact":
|
||||||
@@ -277,67 +253,17 @@ function qualityRank(quality) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareMatchText(left, right) {
|
function applyBestMatchOnly(matches, enabled) {
|
||||||
return String(left || "").localeCompare(String(right || ""), undefined, { sensitivity: "base" });
|
if (!Array.isArray(matches) || !enabled) {
|
||||||
}
|
return Array.isArray(matches) ? [...matches] : [];
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
if (matches.length === 0) {
|
||||||
|
|
||||||
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
const bestRank = Math.min(...matches.map((match) => qualityRank(match?.quality)));
|
const bestRank = Math.min(...matches.map((match) => qualityRank(match?.quality)));
|
||||||
return matches.filter((match) => qualityRank(match?.quality) === bestRank);
|
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) {
|
function defaultObservedTitle(observed) {
|
||||||
if (observed?.title) {
|
if (observed?.title) {
|
||||||
return observed.title;
|
return observed.title;
|
||||||
@@ -723,11 +649,6 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
|
|||||||
bearerToken: settings.bearerToken,
|
bearerToken: settings.bearerToken,
|
||||||
url: resolvedURL
|
url: resolvedURL
|
||||||
});
|
});
|
||||||
const filteredMatches = applyMatchControls(
|
|
||||||
Array.isArray(matches?.matches) ? matches.matches : [],
|
|
||||||
settings,
|
|
||||||
resolvedURL
|
|
||||||
);
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
@@ -737,7 +658,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
|
|||||||
pendingMessage: tokenPendingApprovalCount(matches?.status ?? state.status) > 0
|
pendingMessage: tokenPendingApprovalCount(matches?.status ?? state.status) > 0
|
||||||
? approvalHintForState(state) || "Approve or deny the browser fill request in KeePassGO."
|
? approvalHintForState(state) || "Approve or deny the browser fill request in KeePassGO."
|
||||||
: "",
|
: "",
|
||||||
matches: filteredMatches,
|
matches: applyBestMatchOnly(matches?.matches, settings.bestMatchOnly),
|
||||||
error: matches?.error ?? "",
|
error: matches?.error ?? "",
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now()
|
||||||
};
|
};
|
||||||
@@ -918,7 +839,7 @@ async function saveObservedLogin(tabId, selectedMatch = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const backgroundTestExports = {
|
const backgroundTestExports = {
|
||||||
applyMatchControls,
|
applyBestMatchOnly,
|
||||||
normalizePageState,
|
normalizePageState,
|
||||||
actionPresentationForState,
|
actionPresentationForState,
|
||||||
shouldReuseMatches,
|
shouldReuseMatches,
|
||||||
@@ -960,30 +881,22 @@ if (isNodeTestEnv) {
|
|||||||
case "keepassgo-save-settings":
|
case "keepassgo-save-settings":
|
||||||
await storageSet({
|
await storageSet({
|
||||||
bearerToken: String(message.settings?.bearerToken || "").trim(),
|
bearerToken: String(message.settings?.bearerToken || "").trim(),
|
||||||
bestMatchOnly: Boolean(message.settings?.bestMatchOnly),
|
bestMatchOnly: Boolean(message.settings?.bestMatchOnly)
|
||||||
requireSchemeMatch: Boolean(message.settings?.requireSchemeMatch),
|
|
||||||
sortResults: normalizeSortResults(message.settings?.sortResults)
|
|
||||||
});
|
});
|
||||||
await refreshActivePage({ force: true }).catch(() => null);
|
await refreshActivePage({ force: true }).catch(() => null);
|
||||||
sendResponse({ success: true });
|
sendResponse({ success: true });
|
||||||
return;
|
return;
|
||||||
case "keepassgo-search-logins": {
|
case "keepassgo-search-logins": {
|
||||||
const settings = await loadSettings();
|
const settings = await loadSettings();
|
||||||
const page = await activePageContext();
|
|
||||||
const response = await connectNative({
|
const response = await connectNative({
|
||||||
action: "search-logins",
|
action: "search-logins",
|
||||||
bearerToken: settings.bearerToken,
|
bearerToken: settings.bearerToken,
|
||||||
query: String(message?.query || "").trim()
|
query: String(message?.query || "").trim()
|
||||||
});
|
});
|
||||||
const searchResults = applyMatchControls(
|
|
||||||
Array.isArray(response?.searchResults) ? response.searchResults : [],
|
|
||||||
settings,
|
|
||||||
page.url
|
|
||||||
);
|
|
||||||
sendResponse({
|
sendResponse({
|
||||||
success: Boolean(response?.success),
|
success: Boolean(response?.success),
|
||||||
error: response?.error || "",
|
error: response?.error || "",
|
||||||
results: searchResults,
|
results: applyBestMatchOnly(response?.searchResults, settings.bestMatchOnly),
|
||||||
status: response?.status ?? null
|
status: response?.status ?? null
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -70,8 +70,6 @@ test("shouldContinueWatchingState keeps polling locked login pages", () => {
|
|||||||
test("default settings include a blank bearer token that can be overridden by harness patching", () => {
|
test("default settings include a blank bearer token that can be overridden by harness patching", () => {
|
||||||
assert.equal(background.defaultSettings.bearerToken, "");
|
assert.equal(background.defaultSettings.bearerToken, "");
|
||||||
assert.equal(background.defaultSettings.bestMatchOnly, false);
|
assert.equal(background.defaultSettings.bestMatchOnly, false);
|
||||||
assert.equal(background.defaultSettings.requireSchemeMatch, false);
|
|
||||||
assert.equal(background.defaultSettings.sortResults, "quality");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("savePlanForObservedLogin prefers updating an exact username match", () => {
|
test("savePlanForObservedLogin prefers updating an exact username match", () => {
|
||||||
@@ -133,44 +131,21 @@ test("savePlanForObservedLogin falls back to saving into the current page group"
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("applyMatchControls keeps only the strongest quality band when best-match-only is enabled", () => {
|
test("applyBestMatchOnly keeps only the strongest quality band when enabled", () => {
|
||||||
const filtered = background.applyMatchControls([
|
const filtered = background.applyBestMatchOnly([
|
||||||
{ id: "livingston", title: "Livingston Dell", quality: "exact" },
|
{ id: "livingston", title: "Livingston Dell", quality: "exact" },
|
||||||
{ id: "rusty", title: "Rusty Ryan", quality: "host" },
|
{ id: "rusty", title: "Rusty Ryan", quality: "host" },
|
||||||
{ id: "linus", title: "Linus Caldwell", quality: "scheme" }
|
{ id: "linus", title: "Linus Caldwell", quality: "scheme" }
|
||||||
], {
|
], true);
|
||||||
bestMatchOnly: true,
|
|
||||||
requireSchemeMatch: false,
|
|
||||||
sortResults: "quality"
|
|
||||||
}, "https://vault.example.invalid/login");
|
|
||||||
|
|
||||||
assert.deepEqual(filtered.map((match) => match.id), ["livingston"]);
|
assert.deepEqual(filtered.map((match) => match.id), ["livingston"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("applyMatchControls removes explicit scheme mismatches but keeps scheme-less matches", () => {
|
test("applyBestMatchOnly preserves all matches when disabled", () => {
|
||||||
const filtered = background.applyMatchControls([
|
const filtered = background.applyBestMatchOnly([
|
||||||
{ id: "yen", title: "The Amazing Yen", url: "https://vault.example.invalid/login", quality: "exact" },
|
{ id: "livingston", title: "Livingston Dell", quality: "exact" },
|
||||||
{ id: "saul", title: "Saul Bloom", url: "http://vault.example.invalid/login", quality: "exact" },
|
{ id: "rusty", title: "Rusty Ryan", quality: "host" }
|
||||||
{ id: "basher", title: "Basher Tarr", url: "vault.example.invalid", quality: "host" }
|
], false);
|
||||||
], {
|
|
||||||
bestMatchOnly: false,
|
|
||||||
requireSchemeMatch: true,
|
|
||||||
sortResults: "quality"
|
|
||||||
}, "https://vault.example.invalid/login");
|
|
||||||
|
|
||||||
assert.deepEqual(filtered.map((match) => match.id), ["yen", "basher"]);
|
assert.deepEqual(filtered.map((match) => match.id), ["livingston", "rusty"]);
|
||||||
});
|
|
||||||
|
|
||||||
test("applyMatchControls sorts by path when requested", () => {
|
|
||||||
const filtered = background.applyMatchControls([
|
|
||||||
{ id: "linus", title: "Linus Caldwell", path: ["Crew", "Inside"] },
|
|
||||||
{ id: "rusty", title: "Rusty Ryan", path: ["Crew", "Casino"] },
|
|
||||||
{ id: "danny", title: "Danny Ocean", path: ["Crew"] }
|
|
||||||
], {
|
|
||||||
bestMatchOnly: false,
|
|
||||||
requireSchemeMatch: false,
|
|
||||||
sortResults: "path"
|
|
||||||
}, "https://vault.example.invalid/login");
|
|
||||||
|
|
||||||
assert.deepEqual(filtered.map((match) => match.id), ["danny", "rusty", "linus"]);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,18 +25,6 @@
|
|||||||
<input id="best-match-only" name="best-match-only" type="checkbox">
|
<input id="best-match-only" name="best-match-only" type="checkbox">
|
||||||
<span>Best match only</span>
|
<span>Best match only</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="checkbox-row">
|
|
||||||
<input id="require-scheme-match" name="require-scheme-match" type="checkbox">
|
|
||||||
<span>Require current scheme</span>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Sort results</span>
|
|
||||||
<select id="sort-results" name="sort-results">
|
|
||||||
<option value="quality">KeePassGO match quality</option>
|
|
||||||
<option value="title">Title</option>
|
|
||||||
<option value="path">Path</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="submit">Save</button>
|
<button type="submit">Save</button>
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ async function loadSettings() {
|
|||||||
}
|
}
|
||||||
document.getElementById("bearer-token").value = response.settings.bearerToken || "";
|
document.getElementById("bearer-token").value = response.settings.bearerToken || "";
|
||||||
document.getElementById("best-match-only").checked = Boolean(response.settings.bestMatchOnly);
|
document.getElementById("best-match-only").checked = Boolean(response.settings.bestMatchOnly);
|
||||||
document.getElementById("require-scheme-match").checked = Boolean(response.settings.requireSchemeMatch);
|
|
||||||
document.getElementById("sort-results").value = response.settings.sortResults || "quality";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings(event) {
|
async function saveSettings(event) {
|
||||||
@@ -37,9 +35,7 @@ async function saveSettings(event) {
|
|||||||
type: "keepassgo-save-settings",
|
type: "keepassgo-save-settings",
|
||||||
settings: {
|
settings: {
|
||||||
bearerToken: document.getElementById("bearer-token").value,
|
bearerToken: document.getElementById("bearer-token").value,
|
||||||
bestMatchOnly: document.getElementById("best-match-only").checked,
|
bestMatchOnly: document.getElementById("best-match-only").checked
|
||||||
requireSchemeMatch: document.getElementById("require-scheme-match").checked,
|
|
||||||
sortResults: document.getElementById("sort-results").value
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!response?.success) {
|
if (!response?.success) {
|
||||||
|
|||||||
@@ -177,8 +177,7 @@ label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea {
|
||||||
select {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
|
|||||||
@@ -103,9 +103,8 @@ User story:
|
|||||||
access.
|
access.
|
||||||
- Browser matching must treat common KeePass data conventions as real browser
|
- Browser matching must treat common KeePass data conventions as real browser
|
||||||
targets, not just the primary `URL` field.
|
targets, not just the primary `URL` field.
|
||||||
- Users need explicit control over how aggressive browser matching should be so
|
- Users who prefer narrow suggestions can ask the extension to show only the
|
||||||
they can prefer narrow page suggestions or broader candidate lists without
|
strongest match quality returned by KeePassGO.
|
||||||
reconfiguring KeePassGO itself.
|
|
||||||
|
|
||||||
Expected behavior:
|
Expected behavior:
|
||||||
|
|
||||||
@@ -117,20 +116,9 @@ Expected behavior:
|
|||||||
- scheme-less host values such as `gitlab.com`
|
- scheme-less host values such as `gitlab.com`
|
||||||
- custom URL fields such as `URL1`, `URL2`, and similar KeePass-style URL
|
- custom URL fields such as `URL1`, `URL2`, and similar KeePass-style URL
|
||||||
slots
|
slots
|
||||||
- The extension settings page exposes browser match controls for:
|
- The extension settings page exposes `Best match only`.
|
||||||
- `Best match only`
|
- When `Best match only` is enabled, page suggestions and popup search results
|
||||||
- `Require current scheme`
|
only show the strongest quality band returned by KeePassGO.
|
||||||
- `Sort results`
|
|
||||||
- `Best match only` limits page suggestions to the strongest quality band
|
|
||||||
returned by KeePassGO instead of showing every candidate for the host.
|
|
||||||
- `Require current scheme` hides `http` credentials on `https` pages and
|
|
||||||
vice versa when an entry stores an explicit scheme, while still allowing
|
|
||||||
scheme-less host entries to match either page.
|
|
||||||
- `Sort results` affects both page suggestions and popup search results so the
|
|
||||||
user can prefer:
|
|
||||||
- KeePassGO match quality first
|
|
||||||
- title order
|
|
||||||
- path order
|
|
||||||
|
|
||||||
## Locked Vault Workflow
|
## Locked Vault Workflow
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user