Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0538cd2feb | |||
| 2269944702 | |||
| 2ccd5bc337 |
+40
-1
@@ -39,6 +39,11 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install native build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -47,6 +52,7 @@ jobs:
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
zsh \
|
||||
python3 \
|
||||
pkg-config \
|
||||
libx11-dev \
|
||||
libx11-xcb-dev \
|
||||
@@ -58,6 +64,12 @@ jobs:
|
||||
libxcursor-dev \
|
||||
libxfixes-dev
|
||||
|
||||
- name: Install web-ext
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm install -g web-ext
|
||||
|
||||
- name: Lint
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -74,6 +86,12 @@ jobs:
|
||||
trap 'rm -rf -- "$state_dir"' EXIT
|
||||
KEEPASSGO_STATE_DIR="$state_dir" go test -tags nox11,nowayland,novulkan ./...
|
||||
|
||||
- name: Firefox extension lint
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
make browser-extension-firefox-lint
|
||||
|
||||
build:
|
||||
needs: lint-test
|
||||
runs-on: keepassgo-android
|
||||
@@ -92,6 +110,11 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
- name: Install native build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -100,6 +123,7 @@ jobs:
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
zsh \
|
||||
python3 \
|
||||
pkg-config \
|
||||
libx11-dev \
|
||||
libx11-xcb-dev \
|
||||
@@ -111,6 +135,12 @@ jobs:
|
||||
libxcursor-dev \
|
||||
libxfixes-dev
|
||||
|
||||
- name: Install web-ext
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm install -g web-ext
|
||||
|
||||
- name: Prepare dist directory
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -159,6 +189,13 @@ jobs:
|
||||
make apk-release RELEASE_SIGNKEY="$signkey_path" RELEASE_SIGNPASS_FILE="$signpass_path"
|
||||
cp build/keepassgo.apk "${DIST_DIR}/keepassgo.apk"
|
||||
|
||||
- name: Build Firefox extension
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
make browser-extension-firefox-build
|
||||
cp build/browser-extension/*.zip "${DIST_DIR}/"
|
||||
|
||||
- name: Upload CI artifacts
|
||||
uses: christopherhx/gitea-upload-artifact@v4
|
||||
env:
|
||||
@@ -171,6 +208,7 @@ jobs:
|
||||
dist/keepassgo-windows-amd64.exe
|
||||
dist/keepassgo-windows-arm64.exe
|
||||
dist/keepassgo.apk
|
||||
dist/*.zip
|
||||
retention-days: 30
|
||||
|
||||
- name: Publish release artifacts
|
||||
@@ -193,4 +231,5 @@ jobs:
|
||||
"${DIST_DIR}/keepassgo-linux-amd64" \
|
||||
"${DIST_DIR}/keepassgo-windows-amd64.exe" \
|
||||
"${DIST_DIR}/keepassgo-windows-arm64.exe" \
|
||||
"${DIST_DIR}/keepassgo.apk"
|
||||
"${DIST_DIR}/keepassgo.apk" \
|
||||
"${DIST_DIR}"/*.zip
|
||||
|
||||
@@ -136,6 +136,10 @@ These features are product requirements, not “nice to have” ideas.
|
||||
## Delivery Discipline
|
||||
|
||||
- Treat bug fixes as the highest-priority items in `TODO.md`.
|
||||
- Do not start a new feature while unrelated tracked or untracked local changes
|
||||
remain in the repo.
|
||||
- If previous work leaves unrelated uncommitted changes behind, stop and ask
|
||||
the user before continuing with the next feature.
|
||||
- Do not treat this product as complete until the stated requirements in this file are actually satisfied.
|
||||
- Do not stop at a “good checkpoint” or “meaningful tranche” when required product capabilities are still missing.
|
||||
- Continue iterating in test-first slices:
|
||||
|
||||
@@ -20,6 +20,9 @@ ARCH_PKG_TMPL ?= $(ARCH_PKG_DIR)/PKGBUILD.tmpl
|
||||
ARCH_PKGBUILD ?= $(ARCH_PKG_DIR)/PKGBUILD
|
||||
ARCH_PKGVER ?= $(shell printf 'r%s.%s' "$$(git rev-list --count HEAD 2>/dev/null || echo 0)" "$$(git rev-parse --short HEAD 2>/dev/null || echo dev)")
|
||||
ARCH_REPO_DIR ?= $(CURDIR)
|
||||
WEB_EXT ?= web-ext
|
||||
FIREFOX_EXTENSION_DIR ?= build/firefox-extension
|
||||
FIREFOX_EXTENSION_ARTIFACT_DIR ?= build/browser-extension
|
||||
|
||||
GOGIO_SIGN_FLAGS :=
|
||||
ifneq ($(strip $(SIGNKEY)),)
|
||||
@@ -44,7 +47,7 @@ CONTAINER_SIGNPASSFILE_MOUNT += -v "$(dir $(abspath $(SIGNPASS_FILE))):$(dir $(a
|
||||
CONTAINER_SIGN_ARGS += SIGNPASS_FILE="$(abspath $(SIGNPASS_FILE))"
|
||||
endif
|
||||
|
||||
.PHONY: apk apk-local apk-release apk-container apk-container-image archlinux-pkgbuild browser-bridge browser-extension-validate
|
||||
.PHONY: apk apk-local apk-release apk-container apk-container-image archlinux-pkgbuild browser-bridge browser-extension-validate browser-extension-firefox-dir browser-extension-firefox-lint browser-extension-firefox-build browser-extension-firefox-run browser-extension-firefox-sign
|
||||
apk:
|
||||
@if [ -x "$(JAVA_HOME)/bin/java" ] && "$(JAVA_HOME)/bin/java" -version 2>&1 | grep -q 'version "25'; then \
|
||||
$(MAKE) apk-local JAVA_HOME="$(JAVA_HOME)"; \
|
||||
@@ -132,6 +135,23 @@ archlinux-pkgbuild: $(ARCH_PKG_TMPL) Makefile
|
||||
browser-bridge:
|
||||
go build ./cmd/keepassgo-browser-bridge
|
||||
|
||||
browser-extension-firefox-dir:
|
||||
@mkdir -p "$(dir $(FIREFOX_EXTENSION_DIR))"
|
||||
@python3 scripts/prepare_firefox_extension.py "$(FIREFOX_EXTENSION_DIR)"
|
||||
|
||||
browser-extension-firefox-lint: browser-extension-firefox-dir
|
||||
$(WEB_EXT) lint --source-dir "$(FIREFOX_EXTENSION_DIR)"
|
||||
|
||||
browser-extension-firefox-build: browser-extension-firefox-dir
|
||||
@mkdir -p "$(FIREFOX_EXTENSION_ARTIFACT_DIR)"
|
||||
$(WEB_EXT) build --source-dir "$(FIREFOX_EXTENSION_DIR)" --artifacts-dir "$(FIREFOX_EXTENSION_ARTIFACT_DIR)"
|
||||
|
||||
browser-extension-firefox-run: browser-extension-firefox-dir
|
||||
$(WEB_EXT) run --source-dir "$(FIREFOX_EXTENSION_DIR)"
|
||||
|
||||
browser-extension-firefox-sign: browser-extension-firefox-dir
|
||||
$(WEB_EXT) sign --source-dir "$(FIREFOX_EXTENSION_DIR)"
|
||||
|
||||
browser-extension-validate:
|
||||
@command -v xvfb-run >/dev/null 2>&1 || { echo "xvfb-run is required"; exit 1; }
|
||||
@command -v firefox >/dev/null 2>&1 || { echo "firefox is required"; exit 1; }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -69,6 +69,9 @@ 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);
|
||||
assert.equal(background.defaultSettings.requireSchemeMatch, false);
|
||||
assert.equal(background.defaultSettings.sortResults, "quality");
|
||||
});
|
||||
|
||||
test("savePlanForObservedLogin prefers updating an exact username match", () => {
|
||||
@@ -129,3 +132,45 @@ test("savePlanForObservedLogin falls back to saving into the current page group"
|
||||
url: "https://vault.example.invalid/login"
|
||||
});
|
||||
});
|
||||
|
||||
test("applyMatchControls keeps only the strongest quality band when best-match-only is enabled", () => {
|
||||
const filtered = background.applyMatchControls([
|
||||
{ id: "livingston", title: "Livingston Dell", quality: "exact" },
|
||||
{ id: "rusty", title: "Rusty Ryan", quality: "host" },
|
||||
{ id: "linus", title: "Linus Caldwell", quality: "scheme" }
|
||||
], {
|
||||
bestMatchOnly: true,
|
||||
requireSchemeMatch: false,
|
||||
sortResults: "quality"
|
||||
}, "https://vault.example.invalid/login");
|
||||
|
||||
assert.deepEqual(filtered.map((match) => match.id), ["livingston"]);
|
||||
});
|
||||
|
||||
test("applyMatchControls removes explicit scheme mismatches but keeps scheme-less matches", () => {
|
||||
const filtered = background.applyMatchControls([
|
||||
{ id: "yen", title: "The Amazing Yen", url: "https://vault.example.invalid/login", quality: "exact" },
|
||||
{ id: "saul", title: "Saul Bloom", url: "http://vault.example.invalid/login", quality: "exact" },
|
||||
{ id: "basher", title: "Basher Tarr", url: "vault.example.invalid", quality: "host" }
|
||||
], {
|
||||
bestMatchOnly: false,
|
||||
requireSchemeMatch: true,
|
||||
sortResults: "quality"
|
||||
}, "https://vault.example.invalid/login");
|
||||
|
||||
assert.deepEqual(filtered.map((match) => match.id), ["yen", "basher"]);
|
||||
});
|
||||
|
||||
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"]);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,25 @@
|
||||
<span>API token</span>
|
||||
<textarea id="bearer-token" name="bearer-token" rows="6" spellcheck="false"></textarea>
|
||||
</label>
|
||||
<fieldset>
|
||||
<legend>Browser Matching</legend>
|
||||
<label class="checkbox-row">
|
||||
<input id="best-match-only" name="best-match-only" type="checkbox">
|
||||
<span>Best match only</span>
|
||||
</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>
|
||||
<div class="actions">
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,9 @@ async function loadSettings() {
|
||||
throw new Error(response?.error || "Could not load settings.");
|
||||
}
|
||||
document.getElementById("bearer-token").value = response.settings.bearerToken || "";
|
||||
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) {
|
||||
@@ -33,7 +36,10 @@ async function saveSettings(event) {
|
||||
const response = await runtimeSend({
|
||||
type: "keepassgo-save-settings",
|
||||
settings: {
|
||||
bearerToken: document.getElementById("bearer-token").value
|
||||
bearerToken: document.getElementById("bearer-token").value,
|
||||
bestMatchOnly: document.getElementById("best-match-only").checked,
|
||||
requireSchemeMatch: document.getElementById("require-scheme-match").checked,
|
||||
sortResults: document.getElementById("sort-results").value
|
||||
}
|
||||
});
|
||||
if (!response?.success) {
|
||||
|
||||
@@ -177,7 +177,8 @@ label {
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
@@ -187,6 +188,33 @@ textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0 6px;
|
||||
color: var(--ink-soft);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.checkbox-row input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
button,
|
||||
.link-button {
|
||||
padding: 10px 14px;
|
||||
|
||||
@@ -103,6 +103,9 @@ User story:
|
||||
access.
|
||||
- Browser matching must treat common KeePass data conventions as real browser
|
||||
targets, not just the primary `URL` field.
|
||||
- Users need explicit control over how aggressive browser matching should be so
|
||||
they can prefer narrow page suggestions or broader candidate lists without
|
||||
reconfiguring KeePassGO itself.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
@@ -114,6 +117,20 @@ Expected behavior:
|
||||
- scheme-less host values such as `gitlab.com`
|
||||
- custom URL fields such as `URL1`, `URL2`, and similar KeePass-style URL
|
||||
slots
|
||||
- The extension settings page exposes browser match controls for:
|
||||
- `Best match only`
|
||||
- `Require current scheme`
|
||||
- `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
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE_DIR = REPO_ROOT / "browser" / "extension"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Prepare a Firefox extension directory for web-ext.")
|
||||
parser.add_argument("output_dir", help="directory to write the prepared extension into")
|
||||
args = parser.parse_args()
|
||||
|
||||
output_dir = Path(args.output_dir).resolve()
|
||||
if output_dir.exists():
|
||||
shutil.rmtree(output_dir)
|
||||
shutil.copytree(SOURCE_DIR, output_dir)
|
||||
|
||||
manifest = json.loads((output_dir / "manifest.firefox.json").read_text(encoding="utf-8"))
|
||||
(output_dir / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user