Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 586de0169d | |||
| de19f84305 | |||
| ff81ae633c | |||
| 270a950a72 | |||
| 18084b5e83 | |||
| ab9214af99 | |||
| d1f30f5936 | |||
| 2269944702 | |||
| 2ccd5bc337 | |||
| 9a9d9e7447 | |||
| 2c065a04a4 | |||
| f82ddf7435 | |||
| 14c9bc72f6 | |||
| 515eb730f0 | |||
| d60a8d2fbf | |||
| 4afbc3c933 | |||
| c7d35927f3 | |||
| a6340f5c9e | |||
| 0adf1b8826 | |||
| c517794182 | |||
| b511ab4dc0 | |||
| 7b06388712 | |||
| fea1a75cdf | |||
| 0dfaeef7bf | |||
| 92a7853258 | |||
| 14f22b4ebf | |||
| 4d972bfab0 | |||
| e005a42a3f | |||
| 58d6d510f9 | |||
| bb114cee16 | |||
| 2431467aa7 | |||
| c302c29d4f | |||
| 361d6dbe03 | |||
| a41e842a65 | |||
| 54398837e6 | |||
| 989b41735f |
@@ -45,8 +45,8 @@ Use this skill together with the installed `android-emulator-debug` skill. That
|
||||
## Build Workflow
|
||||
|
||||
1. Verify the JDK/SDK paths match the known working environment.
|
||||
2. Build with `make apk`.
|
||||
3. If `make apk` fails, inspect the effective `JAVA_HOME`, `ANDROID_SDK_ROOT`, and `ANDROID_NDK_ROOT` before changing code.
|
||||
2. Build with `make apk` for debug validation, or `make apk-release` when validating production signing behavior.
|
||||
3. If the build fails, inspect the effective `JAVA_HOME`, `ANDROID_SDK_ROOT`, and `ANDROID_NDK_ROOT` before changing code.
|
||||
4. If the problem is Android-only, avoid desktop-only conclusions from `go test ./...`.
|
||||
|
||||
Typical local build:
|
||||
@@ -55,6 +55,12 @@ Typical local build:
|
||||
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk
|
||||
```
|
||||
|
||||
Typical local release build:
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk-release
|
||||
```
|
||||
|
||||
## Emulator Workflow
|
||||
|
||||
1. Reuse an existing emulator session if one is already running.
|
||||
@@ -79,7 +85,7 @@ adb shell dumpsys window | rg 'mCurrentFocus|mFocusedApp'
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
- APK builds successfully with `make apk`.
|
||||
- APK builds successfully with the intended target: `make apk` for debug validation or `make apk-release` for release-signing validation.
|
||||
- App launches to `org.julianfamily.keepassgo/org.gioui.GioActivity`.
|
||||
- Screenshot shows the expected screen, not just a black frame.
|
||||
- `logcat` shows no app crash or Android runtime fatal error.
|
||||
|
||||
@@ -52,11 +52,17 @@ The installed package version must correspond to the committed source, not a dir
|
||||
Use the repo's known-good local JDK unless the environment already proves otherwise:
|
||||
|
||||
```sh
|
||||
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk
|
||||
JAVA_HOME=/usr/lib/jvm/java-25-openjdk make apk-release
|
||||
```
|
||||
|
||||
If that JDK is unavailable on the current host, use the working replacement already established for the machine and say so in the closeout.
|
||||
|
||||
- `ship it` must use the dedicated release keystore flow, not Gio's implicit debug or temporary signing path.
|
||||
- The default local release-signing paths are:
|
||||
`~/.config/keepassgo/android-release.keystore`
|
||||
`~/.config/keepassgo/android-release.pass`
|
||||
- If those files are unavailable, stop and fix signing instead of shipping a differently signed APK.
|
||||
|
||||
### 4. Zip The APK
|
||||
|
||||
- Create the ZIP under the globally required temporary secret-safe directory.
|
||||
|
||||
+62
-6
@@ -8,6 +8,9 @@ on:
|
||||
- "v*"
|
||||
- "release-*"
|
||||
- "[0-9]+.[0-9]+.[0-9]+*"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -16,7 +19,6 @@ env:
|
||||
GO_VERSION: "1.26.1"
|
||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||
ANDROID_NDK_ROOT: /opt/android-sdk/ndk
|
||||
JAVA_HOME: /usr/lib/jvm/java-21-openjdk-amd64
|
||||
DIST_DIR: dist
|
||||
|
||||
jobs:
|
||||
@@ -31,6 +33,17 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
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: |
|
||||
@@ -39,6 +52,7 @@ jobs:
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
zsh \
|
||||
python3 \
|
||||
pkg-config \
|
||||
libx11-dev \
|
||||
libx11-xcb-dev \
|
||||
@@ -50,13 +64,19 @@ 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: |
|
||||
set -euo pipefail
|
||||
state_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$state_dir"' EXIT
|
||||
KEEPASSGO_STATE_DIR="$state_dir" go tool golangci-lint run --build-tags nox11,nowayland,novulkan ./...
|
||||
KEEPASSGO_STATE_DIR="$state_dir" go tool golangci-lint run --timeout=10m --build-tags nox11,nowayland,novulkan ./...
|
||||
|
||||
- name: Test
|
||||
shell: bash
|
||||
@@ -66,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
|
||||
@@ -78,6 +104,17 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
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: |
|
||||
@@ -86,6 +123,7 @@ jobs:
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
zsh \
|
||||
python3 \
|
||||
pkg-config \
|
||||
libx11-dev \
|
||||
libx11-xcb-dev \
|
||||
@@ -97,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: |
|
||||
@@ -135,13 +179,23 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
signkey_path="$(mktemp)"
|
||||
trap 'rm -f -- "$signkey_path"' EXIT
|
||||
mkdir -p build/ci-signing
|
||||
signkey_path="$(pwd)/build/ci-signing/android-release.keystore"
|
||||
signpass_path="$(pwd)/build/ci-signing/android-release.pass"
|
||||
trap 'rm -f -- "$signkey_path" "$signpass_path"' EXIT
|
||||
printf '%s' '${{ secrets.APK_SIGNKEY_B64 }}' | base64 -d > "$signkey_path"
|
||||
printf '%s' '${{ secrets.APK_SIGNPASS }}' > "$signpass_path"
|
||||
export APP_VERSION="$(git describe --tags --always --dirty)"
|
||||
make apk SIGNKEY="$signkey_path" SIGNPASS='${{ secrets.APK_SIGNPASS }}'
|
||||
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:
|
||||
@@ -154,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
|
||||
@@ -176,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
|
||||
|
||||
+27
-16
@@ -1,19 +1,30 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: standard
|
||||
enable:
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
|
||||
linters-settings:
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gocyclo
|
||||
settings:
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
exclusions:
|
||||
generated: lax
|
||||
presets:
|
||||
- comments
|
||||
- common-false-positives
|
||||
- legacy
|
||||
- std-error-handling
|
||||
rules:
|
||||
- linters:
|
||||
- gocyclo
|
||||
path: _test\.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
|
||||
@@ -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:
|
||||
@@ -177,7 +181,7 @@ These features are product requirements, not “nice to have” ideas.
|
||||
local `ANDROID_NDK_ROOT=/opt/android-ndk`,
|
||||
CI `ANDROID_NDK_ROOT=/opt/android-sdk/ndk`,
|
||||
local `JAVA_HOME=/usr/lib/jvm/java-25-openjdk`,
|
||||
CI `JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64`.
|
||||
CI `JAVA_HOME` provided by `actions/setup-java` with Temurin 25.
|
||||
- Remember the known Android runtime regression:
|
||||
`gioui.org v0.9.0` produced a black screen on the `KeepassGoAPI35` emulator, while `gioui.org v0.8.0` rendered correctly. Treat Gio upgrades on Android as regression-sensitive and verify them on-device or in the emulator.
|
||||
- When validating an APK in the emulator, prefer the known KeePassGO setup:
|
||||
|
||||
@@ -6,17 +6,43 @@ Build the APK with:
|
||||
make apk
|
||||
```
|
||||
|
||||
Build the release-signed APK with:
|
||||
|
||||
```sh
|
||||
make apk-release
|
||||
```
|
||||
|
||||
`make apk` uses a local Java 25 install when `JAVA_HOME` points to one.
|
||||
If the host does not have a working Java 25 install, it falls back to the
|
||||
repo-managed Docker image in `packaging/docker/android-apk/`, which also builds
|
||||
with Java 25. CI provisions Java 25 directly in the build job so release builds
|
||||
use that same local path instead of nested Docker.
|
||||
|
||||
`make apk` remains a developer build path and may use Gio's default debug or
|
||||
ephemeral signing behavior if no explicit signing key is provided.
|
||||
`make apk-release` is the production-signing path and fails unless a dedicated
|
||||
release keystore and password file are present.
|
||||
|
||||
Environment:
|
||||
|
||||
- `ANDROID_SDK_ROOT` defaults to `/opt/android-sdk`.
|
||||
- `ANDROID_NDK_ROOT` defaults to `/opt/android-ndk`.
|
||||
- `JAVA_HOME` defaults to `/usr/lib/jvm/java-25-openjdk`.
|
||||
- `APK_BUILD_IMAGE` overrides the Docker image name used by `make apk-container`.
|
||||
- `APP_ID` overrides the Android application id.
|
||||
- `APP_VERSION` overrides the version shown inside KeePassGO itself.
|
||||
- `APK_OUT` overrides the output path.
|
||||
- `APK_VERSION` overrides the packaged app version.
|
||||
- `ANDROID_MIN_SDK` overrides the minimum supported Android SDK.
|
||||
- `ANDROID_TARGET_SDK` overrides the target Android SDK.
|
||||
- `SIGNPASS_FILE` provides the signing password by file instead of a command-line argument.
|
||||
- `RELEASE_SIGNKEY` overrides the release keystore path used by `make apk-release`.
|
||||
- `RELEASE_SIGNPASS_FILE` overrides the password file path used by `make apk-release`.
|
||||
|
||||
Default release-signing paths:
|
||||
|
||||
- `~/.config/keepassgo/android-release.keystore`
|
||||
- `~/.config/keepassgo/android-release.pass`
|
||||
|
||||
Installed machine prerequisites expected by this repo:
|
||||
|
||||
@@ -24,24 +50,28 @@ Installed machine prerequisites expected by this repo:
|
||||
- `android-sdk-build-tools`
|
||||
- `android-platform-35`
|
||||
- `android-sdk-platform-tools`
|
||||
- a working JDK install
|
||||
- a working Java 25 JDK install for `make apk-local`, or Docker for `make apk`
|
||||
|
||||
The repo tracks `gogio` as a Go tool, so the build runs through:
|
||||
The repo tracks `gogio` as a Go tool, and the local build runs through:
|
||||
|
||||
```sh
|
||||
go tool gogio -target android ./cmd/keepassgo ...
|
||||
```
|
||||
|
||||
The release target wraps `make apk` and injects explicit signing credentials so
|
||||
local release builds and CI use the same stable key without echoing the release
|
||||
password in build logs.
|
||||
|
||||
The Android build uses the branded icon asset at:
|
||||
|
||||
- `internal/assets/keepassgo-icon.png`
|
||||
|
||||
Note:
|
||||
|
||||
- Gio's Android doc currently references Java 1.8, but the Android build-tools
|
||||
installed on this machine (`d8` from build-tools 37) do not run on Java 8.
|
||||
- In this environment, KeePassGO's APK build requires a newer JDK runtime on
|
||||
`PATH`, which is why the repo defaults `JAVA_HOME` to `/usr/lib/jvm/java-25-openjdk`.
|
||||
- KeePassGO's documented Android build uses Java 25 locally and in CI.
|
||||
- If that host setup is unavailable, `make apk` falls back to the Docker image
|
||||
so the build still runs under Java 25 instead of encoding a newer host JDK as
|
||||
a requirement.
|
||||
- Android runtime testing on the `KeepassGoAPI35` emulator showed a black-screen
|
||||
regression with `gioui.org v0.9.0` while a stock Gio example and KeePassGO both
|
||||
rendered correctly with `gioui.org v0.8.0` on the same emulator and SDK/JDK
|
||||
|
||||
@@ -2,20 +2,28 @@ ANDROID_SDK_ROOT ?= /opt/android-sdk
|
||||
ANDROID_NDK_ROOT ?= /opt/android-ndk
|
||||
JAVA_HOME ?= /usr/lib/jvm/java-25-openjdk
|
||||
PATH := $(JAVA_HOME)/bin:$(ANDROID_SDK_ROOT)/cmdline-tools/latest/bin:$(ANDROID_SDK_ROOT)/platform-tools:$(PATH)
|
||||
APK_BUILD_IMAGE ?= keepassgo/android-apk-build:java25
|
||||
APP_ID ?= org.julianfamily.keepassgo
|
||||
APK_OUT ?= build/keepassgo.apk
|
||||
APK_VERSION ?= 0.1.0.1
|
||||
APP_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)
|
||||
GO_LDFLAGS ?= -X git.julianfamily.org/keepassgo/internal/appui.appVersion=$(APP_VERSION)
|
||||
APK_ARCH ?= arm64,amd64
|
||||
ANDROID_MIN_SDK ?= 28
|
||||
ANDROID_TARGET_SDK ?= 35
|
||||
SIGNKEY ?=
|
||||
SIGNPASS ?=
|
||||
SIGNPASS_FILE ?=
|
||||
RELEASE_SIGNKEY ?= $(HOME)/.config/keepassgo/android-release.keystore
|
||||
RELEASE_SIGNPASS_FILE ?= $(HOME)/.config/keepassgo/android-release.pass
|
||||
ARCH_PKG_DIR ?= packaging/archlinux/keepassgo-git
|
||||
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)),)
|
||||
@@ -25,8 +33,31 @@ ifneq ($(strip $(SIGNPASS)),)
|
||||
GOGIO_SIGN_FLAGS += -signpass $(SIGNPASS)
|
||||
endif
|
||||
|
||||
.PHONY: apk archlinux-pkgbuild browser-bridge browser-extension-validate
|
||||
apk: android/keepassgo-android.jar
|
||||
CONTAINER_SIGNKEY_MOUNT :=
|
||||
CONTAINER_SIGNPASSFILE_MOUNT :=
|
||||
CONTAINER_SIGN_ARGS :=
|
||||
ifneq ($(strip $(SIGNKEY)),)
|
||||
CONTAINER_SIGNKEY_MOUNT += -v "$(dir $(abspath $(SIGNKEY))):$(dir $(abspath $(SIGNKEY))):ro"
|
||||
CONTAINER_SIGN_ARGS += SIGNKEY="$(abspath $(SIGNKEY))"
|
||||
endif
|
||||
ifneq ($(strip $(SIGNPASS)),)
|
||||
CONTAINER_SIGN_ARGS += SIGNPASS="$(SIGNPASS)"
|
||||
endif
|
||||
ifneq ($(strip $(SIGNPASS_FILE)),)
|
||||
CONTAINER_SIGNPASSFILE_MOUNT += -v "$(dir $(abspath $(SIGNPASS_FILE))):$(dir $(abspath $(SIGNPASS_FILE))):ro"
|
||||
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 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)"; \
|
||||
else \
|
||||
echo "Using Dockerized Java 25 Android build because JAVA_HOME is not a working Java 25 install."; \
|
||||
$(MAKE) apk-container; \
|
||||
fi
|
||||
|
||||
apk-local: android/keepassgo-android.jar
|
||||
@test -x "$(JAVA_HOME)/bin/java" || { echo "JAVA_HOME must point to a working JDK install"; exit 1; }
|
||||
@test -d "$(ANDROID_SDK_ROOT)" || { echo "ANDROID_SDK_ROOT must point to an Android SDK install"; exit 1; }
|
||||
@test -d "$(ANDROID_NDK_ROOT)" || { echo "ANDROID_NDK_ROOT must point to an Android NDK install"; exit 1; }
|
||||
@@ -34,11 +65,18 @@ apk: android/keepassgo-android.jar
|
||||
@test -d "$(ANDROID_SDK_ROOT)/platforms/android-$(ANDROID_TARGET_SDK)" || { echo "Android platform android-$(ANDROID_TARGET_SDK) is missing"; exit 1; }
|
||||
@test -d "$(ANDROID_SDK_ROOT)/build-tools" || { echo "Android build-tools are missing"; exit 1; }
|
||||
@mkdir -p "$(dir $(APK_OUT))"
|
||||
@set -eu; \
|
||||
if [ -n "$(SIGNPASS_FILE)" ]; then \
|
||||
test -f "$(SIGNPASS_FILE)" || { echo "SIGNPASS_FILE does not exist: $(SIGNPASS_FILE)"; exit 1; }; \
|
||||
export GOGIO_SIGNPASS="$$(tr -d '\r\n' < "$(SIGNPASS_FILE)")"; \
|
||||
test -n "$$GOGIO_SIGNPASS" || { echo "SIGNPASS_FILE is empty: $(SIGNPASS_FILE)"; exit 1; }; \
|
||||
fi; \
|
||||
ANDROID_HOME="$(ANDROID_SDK_ROOT)" \
|
||||
ANDROID_SDK_ROOT="$(ANDROID_SDK_ROOT)" \
|
||||
ANDROID_NDK_ROOT="$(ANDROID_NDK_ROOT)" \
|
||||
JAVA_HOME="$(JAVA_HOME)" \
|
||||
go tool gogio -target android \
|
||||
-arch $(APK_ARCH) \
|
||||
-buildmode exe \
|
||||
-appid $(APP_ID) \
|
||||
-ldflags "$(GO_LDFLAGS)" \
|
||||
@@ -50,12 +88,39 @@ apk: android/keepassgo-android.jar
|
||||
-icon internal/assets/keepassgo-icon.png \
|
||||
./cmd/keepassgo
|
||||
|
||||
apk-release:
|
||||
@test -f "$(RELEASE_SIGNKEY)" || { echo "Release signing key not found at $(RELEASE_SIGNKEY)"; exit 1; }
|
||||
@test -f "$(RELEASE_SIGNPASS_FILE)" || { echo "Release signing password file not found at $(RELEASE_SIGNPASS_FILE)"; exit 1; }
|
||||
@$(MAKE) apk SIGNKEY="$(abspath $(RELEASE_SIGNKEY))" SIGNPASS_FILE="$(abspath $(RELEASE_SIGNPASS_FILE))" JAVA_HOME="$(JAVA_HOME)"
|
||||
|
||||
apk-container: apk-container-image
|
||||
@command -v docker >/dev/null 2>&1 || { echo "docker is required for apk-container"; exit 1; }
|
||||
@test -d "$(ANDROID_SDK_ROOT)" || { echo "ANDROID_SDK_ROOT must point to an Android SDK install"; exit 1; }
|
||||
@test -d "$(ANDROID_NDK_ROOT)" || { echo "ANDROID_NDK_ROOT must point to an Android NDK install"; exit 1; }
|
||||
docker run --rm \
|
||||
-u "$$(id -u):$$(id -g)" \
|
||||
-v "$(CURDIR):$(CURDIR)" \
|
||||
-w "$(CURDIR)" \
|
||||
-v "$(ANDROID_SDK_ROOT):$(ANDROID_SDK_ROOT)" \
|
||||
-v "$(ANDROID_NDK_ROOT):$(ANDROID_NDK_ROOT)" \
|
||||
$(CONTAINER_SIGNKEY_MOUNT) \
|
||||
$(CONTAINER_SIGNPASSFILE_MOUNT) \
|
||||
-e ANDROID_SDK_ROOT="$(ANDROID_SDK_ROOT)" \
|
||||
-e ANDROID_NDK_ROOT="$(ANDROID_NDK_ROOT)" \
|
||||
-e JAVA_HOME=/opt/java/openjdk \
|
||||
$(APK_BUILD_IMAGE) \
|
||||
make apk-local JAVA_HOME=/opt/java/openjdk $(CONTAINER_SIGN_ARGS)
|
||||
|
||||
apk-container-image:
|
||||
@command -v docker >/dev/null 2>&1 || { echo "docker is required for apk-container-image"; exit 1; }
|
||||
docker build --load -t $(APK_BUILD_IMAGE) packaging/docker/android-apk
|
||||
|
||||
android/keepassgo-android.jar: $(shell find androidsrc -type f | sort)
|
||||
@test -x "$(JAVA_HOME)/bin/javac" || { echo "JAVA_HOME must point to a working JDK install"; exit 1; }
|
||||
@test -f "$(ANDROID_SDK_ROOT)/platforms/android-$(ANDROID_TARGET_SDK)/android.jar" || { echo "Android platform android-$(ANDROID_TARGET_SDK) is missing"; exit 1; }
|
||||
@mkdir -p android
|
||||
@zsh -lc 'tmpdir=$$(mktemp -d); \
|
||||
trap '\''python3 -c "import shutil,sys; shutil.rmtree(sys.argv[1], ignore_errors=True)" "$$tmpdir"'\'' EXIT; \
|
||||
@sh -ec 'tmpdir=$$(mktemp -d); \
|
||||
trap "rm -rf $$tmpdir" EXIT; \
|
||||
"$(JAVA_HOME)/bin/javac" \
|
||||
-classpath "$(ANDROID_SDK_ROOT)/platforms/android-$(ANDROID_TARGET_SDK)/android.jar" \
|
||||
-d "$$tmpdir" \
|
||||
@@ -72,6 +137,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; }
|
||||
|
||||
@@ -90,10 +90,25 @@ go get -tool gioui.org/cmd/gogio@latest
|
||||
Package:
|
||||
|
||||
```bash
|
||||
go tool gogio -target android -icon internal/assets/keepassgo-icon.png ./cmd/keepassgo
|
||||
make apk
|
||||
```
|
||||
|
||||
You will need the Android SDK and NDK installed and configured for real device or release packaging.
|
||||
`make apk` prefers a local Java 25 install at `JAVA_HOME`. If that is not
|
||||
available, it falls back to the repo-managed Docker build image, which also
|
||||
uses Java 25. CI provisions Java 25 directly in the build job so release
|
||||
packaging follows that same local path. You still need the Android SDK and NDK
|
||||
installed and configured for real device or release packaging.
|
||||
|
||||
Release package:
|
||||
|
||||
```bash
|
||||
make apk-release
|
||||
```
|
||||
|
||||
`make apk-release` is the production-signing path. It requires a dedicated
|
||||
release keystore at `~/.config/keepassgo/android-release.keystore` and a
|
||||
password file at `~/.config/keepassgo/android-release.pass`, unless you
|
||||
override `RELEASE_SIGNKEY` and `RELEASE_SIGNPASS_FILE`.
|
||||
|
||||
## Automation
|
||||
|
||||
|
||||
@@ -130,6 +130,98 @@ These are important, but they should likely move behind a dedicated settings gea
|
||||
- Accessibility preferences:
|
||||
future display-density, contrast, reduced-motion, or keyboard-focus tuning should live under settings.
|
||||
|
||||
## Upstream Gap Review
|
||||
|
||||
This section tracks explicit feature gaps against the source-level behavior of:
|
||||
|
||||
- KeePass 2.57.1
|
||||
- KeePassHttp
|
||||
- Keepass2Android
|
||||
|
||||
These are not speculative enhancements. They are parity gaps relative to the
|
||||
stated product requirement to cover the practical feature surface of those
|
||||
upstream tools where it fits KeePassGO's security model.
|
||||
|
||||
### Stage 1
|
||||
|
||||
- Android autofill parity/completeness:
|
||||
close the remaining gaps in Android autofill behavior, including broader
|
||||
page/app detection coverage, stronger approval and visibility UX, and more
|
||||
reliable fill behavior across real-world apps and browsers.
|
||||
- Android fallback fill workflows:
|
||||
provide a non-autofill fallback comparable in usefulness to KP2A's keyboard
|
||||
and share-driven workflows for apps and browsers that do not cooperate with
|
||||
platform autofill.
|
||||
- Browser extension save/update:
|
||||
add the browser-side save/update-credential workflow after successful form
|
||||
submission, not only lookup and fill.
|
||||
- Search and matching controls:
|
||||
browser/API result behavior does not yet expose KeePassHttp-style controls
|
||||
such as best-match-only, scheme matching, and sort preferences as a finished
|
||||
product surface.
|
||||
- Unlock-request workflow:
|
||||
KeePassHttp has an explicit locked-database browser flow; KeePassGO still
|
||||
needs a polished browser-visible locked/unlock request experience.
|
||||
- Android share/intents:
|
||||
browser/app share-driven lookup and open flows comparable to KP2A are not
|
||||
implemented as a full user workflow.
|
||||
|
||||
### Stage 2
|
||||
|
||||
- OTP/TOTP:
|
||||
implement real OTP/TOTP support, including storage conventions compatible
|
||||
with common KeePass ecosystems and usable display/copy/fill workflows.
|
||||
- TOTP product surface:
|
||||
KP2A exposes TOTP directly in entry and list UX; KeePassGO does not.
|
||||
- Browser-returned field breadth:
|
||||
KeePassHttp can return string fields for browser consumers; KeePassGO does
|
||||
not yet have a finished policy and browser UX for rich field return.
|
||||
- Placeholder and field-reference parity:
|
||||
KeePass-style placeholder expansion, field references, and related command
|
||||
and URL override behavior are not implemented as a product surface.
|
||||
- Offline/work-offline flow:
|
||||
KP2A has explicit offline/cache-oriented remote-file workflows that are more
|
||||
mature than KeePassGO's current user-facing remote behavior.
|
||||
|
||||
### Stage 3
|
||||
|
||||
- Desktop automation:
|
||||
implement desktop login automation comparable in practical capability to
|
||||
KeePass auto-type, or replace it with a demonstrably superior workflow that
|
||||
covers global invocation, selected-entry invocation, window targeting, and
|
||||
field sequencing.
|
||||
- Trigger system:
|
||||
KeePass-style event/condition/action triggers are not implemented.
|
||||
- Import/export breadth:
|
||||
KDBX load/save exists, but KeePass-style breadth for CSV/XML/HTML and other
|
||||
exchange formats is still missing.
|
||||
- Multi-database lookup:
|
||||
KeePassHttp can search across all opened databases when configured; KeePassGO
|
||||
does not yet have an equivalent multi-vault lookup model.
|
||||
- Remote backend breadth:
|
||||
KP2A supports far more remote/file backends than KeePassGO currently does;
|
||||
KeePassGO is still effectively WebDAV-first.
|
||||
- Plugin/extensibility model:
|
||||
KeePassGO has integrations, but not a first-class plugin model comparable to
|
||||
KeePass.
|
||||
- Plugin ecosystem replacements:
|
||||
QR transfer, keyboard transport, and related mobile integration equivalents
|
||||
do not exist in KeePassGO.
|
||||
- Emergency and recovery utilities:
|
||||
emergency-sheet/key-file-backup style flows are not implemented.
|
||||
|
||||
### Immediate Product Questions
|
||||
|
||||
- Desktop automation:
|
||||
decide whether KeePassGO will implement true auto-type, or a different
|
||||
security model that still satisfies the practical workflows KeePass users
|
||||
expect.
|
||||
- OTP model:
|
||||
decide the canonical KeePassGO representation for TOTP/HOTP so desktop,
|
||||
Android, gRPC, and browser workflows can all target the same semantics.
|
||||
- Remote breadth:
|
||||
decide which non-WebDAV backends are in product scope after WebDAV.
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- The main workflow screens prioritize opening, browsing, copying, editing, and synchronizing credentials.
|
||||
@@ -269,7 +361,7 @@ Exit criteria:
|
||||
- Tests cover clear/reset transitions.
|
||||
- `go test ./...` passes.
|
||||
|
||||
### Segment 10: Template CRUD UI
|
||||
### Segment 10 (stage 3): Template CRUD UI
|
||||
|
||||
Scope:
|
||||
- Create template.
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
android:name="android.accessibilityservice"
|
||||
android:resource="@xml/keepassgo_accessibility_service" />
|
||||
</service>
|
||||
<activity
|
||||
android:name="org.julianfamily.keepassgo.KeePassGOAutofillPickerActivity"
|
||||
android:exported="false"
|
||||
android:label="Search KeePassGO" />
|
||||
<provider
|
||||
android:name="org.julianfamily.keepassgo.SharedVaultProvider"
|
||||
android:authorities="org.julianfamily.keepassgo.share"
|
||||
@@ -31,6 +35,11 @@
|
||||
android:name="org.julianfamily.keepassgo.SharedVaultImportActivity"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -41,6 +50,16 @@
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="http" />
|
||||
<data android:scheme="https" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="application/octet-stream" />
|
||||
<data android:mimeType="application/x-keepass2" />
|
||||
<data android:mimeType="application/vnd.keepass" />
|
||||
<data android:scheme="content" android:pathPattern=".*\\.kdbx" />
|
||||
<data android:scheme="file" android:pathPattern=".*\\.kdbx" />
|
||||
</intent-filter>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.JsonReader;
|
||||
import android.util.JsonWriter;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class AutofillBindingStore {
|
||||
private static final String TAG = "KeePassGOAutofill";
|
||||
|
||||
private AutofillBindingStore() {
|
||||
}
|
||||
|
||||
static String entryIDForTarget(Context context, String rawTarget) {
|
||||
Bindings bindings = read(context);
|
||||
return bindings.apps.getOrDefault(normalize(rawTarget), "");
|
||||
}
|
||||
|
||||
static void rememberBinding(Context context, String rawTarget, String entryID) {
|
||||
String target = normalize(rawTarget);
|
||||
if (target.isEmpty() || entryID == null || entryID.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Bindings bindings = read(context);
|
||||
bindings.updatedAt = Instant.now().toString();
|
||||
bindings.apps.put(target, entryID.trim());
|
||||
write(context, bindings);
|
||||
}
|
||||
|
||||
private static Bindings read(Context context) {
|
||||
File path = path(context);
|
||||
if (!path.isFile()) {
|
||||
return new Bindings();
|
||||
}
|
||||
try (JsonReader reader = new JsonReader(new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8))) {
|
||||
Bindings bindings = new Bindings();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
if ("updatedAt".equals(name)) {
|
||||
bindings.updatedAt = nextString(reader);
|
||||
continue;
|
||||
}
|
||||
if ("apps".equals(name)) {
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
bindings.apps.put(normalize(reader.nextName()), nextString(reader));
|
||||
}
|
||||
reader.endObject();
|
||||
continue;
|
||||
}
|
||||
reader.skipValue();
|
||||
}
|
||||
reader.endObject();
|
||||
return bindings;
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to read autofill bindings", err);
|
||||
return new Bindings();
|
||||
}
|
||||
}
|
||||
|
||||
private static void write(Context context, Bindings bindings) {
|
||||
File path = path(context);
|
||||
File parent = path.getParentFile();
|
||||
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||
Log.e(TAG, "failed to create autofill binding directory " + parent.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
try (JsonWriter writer = new JsonWriter(new OutputStreamWriter(new FileOutputStream(path, false), StandardCharsets.UTF_8))) {
|
||||
writer.setIndent(" ");
|
||||
writer.beginObject();
|
||||
writer.name("updatedAt").value(bindings.updatedAt);
|
||||
writer.name("apps");
|
||||
writer.beginObject();
|
||||
for (Map.Entry<String, String> entry : bindings.apps.entrySet()) {
|
||||
writer.name(entry.getKey()).value(entry.getValue());
|
||||
}
|
||||
writer.endObject();
|
||||
writer.endObject();
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to write autofill bindings", err);
|
||||
}
|
||||
}
|
||||
|
||||
private static String nextString(JsonReader reader) throws IOException {
|
||||
if (reader.peek() == android.util.JsonToken.NULL) {
|
||||
reader.nextNull();
|
||||
return "";
|
||||
}
|
||||
return reader.nextString();
|
||||
}
|
||||
|
||||
private static File path(Context context) {
|
||||
return new File(new File(context.getFilesDir(), "keepassgo"), "autofill-bindings.json");
|
||||
}
|
||||
|
||||
private static String normalize(String rawTarget) {
|
||||
return rawTarget == null ? "" : rawTarget.trim();
|
||||
}
|
||||
|
||||
private static final class Bindings {
|
||||
String updatedAt = "";
|
||||
final Map<String, String> apps = new LinkedHashMap<>();
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@@ -20,41 +21,79 @@ final class AutofillCacheStore {
|
||||
}
|
||||
|
||||
static Entry findBestMatch(Context context, String webDomain) {
|
||||
File cacheFile = findCacheFile(context);
|
||||
if (cacheFile == null) {
|
||||
Log.i(TAG, "autofill cache file not found");
|
||||
return null;
|
||||
}
|
||||
List<Entry> entries;
|
||||
try {
|
||||
entries = readEntries(cacheFile);
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to read autofill cache", err);
|
||||
return null;
|
||||
}
|
||||
List<Entry> entries = readEntries(context);
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
NormalizedTarget target = normalizeURL(webDomain);
|
||||
if (target.host.isEmpty()) {
|
||||
return fromMatcherEntry(AutofillTargetMatcher.findBestMatch(toMatcherEntries(entries), webDomain));
|
||||
}
|
||||
|
||||
static Entry findByID(Context context, String entryID) {
|
||||
if (entryID == null || entryID.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Entry> exactHost = new ArrayList<>();
|
||||
List<Entry> parentHost = new ArrayList<>();
|
||||
for (Entry entry : readEntries(context)) {
|
||||
if (entryID.equals(entry.id)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<Entry> chooserCandidates(Context context, String rawTarget) {
|
||||
List<Entry> entries = readEntries(context);
|
||||
if (entries.isEmpty()) {
|
||||
return entries;
|
||||
}
|
||||
Entry direct = fromMatcherEntry(AutofillTargetMatcher.findBestMatch(toMatcherEntries(entries), rawTarget));
|
||||
if (direct != null) {
|
||||
List<Entry> resolved = new ArrayList<>();
|
||||
resolved.add(direct);
|
||||
return resolved;
|
||||
}
|
||||
entries.sort(Comparator
|
||||
.comparing((Entry entry) -> entry.title.toLowerCase(Locale.US))
|
||||
.thenComparing(entry -> String.join("/", entry.path).toLowerCase(Locale.US))
|
||||
.thenComparing(entry -> entry.id));
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<AutofillTargetMatcher.Entry> toMatcherEntries(List<Entry> entries) {
|
||||
List<AutofillTargetMatcher.Entry> converted = new ArrayList<>(entries.size());
|
||||
for (Entry entry : entries) {
|
||||
if (entryMatchesHost(entry, target.host)) {
|
||||
exactHost.add(entry);
|
||||
continue;
|
||||
}
|
||||
if (entryMatchesParentHost(entry, target.host)) {
|
||||
parentHost.add(entry);
|
||||
}
|
||||
converted.add(new AutofillTargetMatcher.Entry(
|
||||
entry.id,
|
||||
entry.title,
|
||||
entry.username,
|
||||
entry.password,
|
||||
entry.host,
|
||||
entry.url,
|
||||
entry.targets,
|
||||
entry.path
|
||||
));
|
||||
}
|
||||
Entry matched = chooseEntry(target, exactHost);
|
||||
if (matched != null) {
|
||||
return matched;
|
||||
return converted;
|
||||
}
|
||||
|
||||
private static Entry fromMatcherEntry(AutofillTargetMatcher.Entry entry) {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return new Entry(entry.id, entry.title, entry.username, entry.password, entry.host, entry.url, entry.targets, entry.path);
|
||||
}
|
||||
|
||||
private static List<Entry> readEntries(Context context) {
|
||||
File cacheFile = findCacheFile(context);
|
||||
if (cacheFile == null) {
|
||||
Log.i(TAG, "autofill cache file not found");
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return readEntries(cacheFile);
|
||||
} catch (IOException err) {
|
||||
Log.e(TAG, "failed to read autofill cache", err);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return chooseEntry(target, parentHost);
|
||||
}
|
||||
|
||||
private static File findCacheFile(Context context) {
|
||||
@@ -103,16 +142,21 @@ final class AutofillCacheStore {
|
||||
}
|
||||
|
||||
private static Entry readEntry(JsonReader reader) throws IOException {
|
||||
String id = "";
|
||||
String title = "";
|
||||
String username = "";
|
||||
String password = "";
|
||||
String host = "";
|
||||
String url = "";
|
||||
List<String> targets = new ArrayList<>();
|
||||
List<String> path = new ArrayList<>();
|
||||
reader.beginObject();
|
||||
while (reader.hasNext()) {
|
||||
String name = reader.nextName();
|
||||
switch (name) {
|
||||
case "id":
|
||||
id = nextString(reader);
|
||||
break;
|
||||
case "title":
|
||||
title = nextString(reader);
|
||||
break;
|
||||
@@ -135,13 +179,20 @@ final class AutofillCacheStore {
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
case "path":
|
||||
reader.beginArray();
|
||||
while (reader.hasNext()) {
|
||||
path.add(nextString(reader));
|
||||
}
|
||||
reader.endArray();
|
||||
break;
|
||||
default:
|
||||
reader.skipValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
reader.endObject();
|
||||
return new Entry(title, username, password, host, url, targets);
|
||||
return new Entry(id, title, username, password, host, url, targets, path);
|
||||
}
|
||||
|
||||
private static String nextString(JsonReader reader) throws IOException {
|
||||
@@ -153,182 +204,28 @@ final class AutofillCacheStore {
|
||||
}
|
||||
|
||||
private static String normalizeHost(String raw) {
|
||||
return normalizeURL(raw).host;
|
||||
}
|
||||
|
||||
private static NormalizedTarget normalizeURL(String raw) {
|
||||
if (raw == null) {
|
||||
return new NormalizedTarget("", "", "");
|
||||
}
|
||||
String value = raw.trim().toLowerCase(Locale.US);
|
||||
if (value.startsWith("http://")) {
|
||||
value = value.substring("http://".length());
|
||||
} else if (value.startsWith("https://")) {
|
||||
value = value.substring("https://".length());
|
||||
}
|
||||
int slash = value.indexOf('/');
|
||||
if (slash >= 0) {
|
||||
value = value.substring(0, slash);
|
||||
}
|
||||
int colon = value.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
value = value.substring(0, colon);
|
||||
}
|
||||
String host = value;
|
||||
String path = "/";
|
||||
int schemeSep = raw.indexOf("://");
|
||||
String original = raw.trim();
|
||||
if (schemeSep < 0) {
|
||||
original = "https://" + original;
|
||||
}
|
||||
try {
|
||||
java.net.URI uri = java.net.URI.create(original);
|
||||
if (uri.getHost() != null) {
|
||||
host = uri.getHost().toLowerCase(Locale.US);
|
||||
}
|
||||
path = cleanPath(uri.getPath());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
path = "/";
|
||||
}
|
||||
return new NormalizedTarget(host, path, host + path);
|
||||
}
|
||||
|
||||
private static String cleanPath(String raw) {
|
||||
if (raw == null || raw.trim().isEmpty() || "/".equals(raw.trim())) {
|
||||
return "/";
|
||||
}
|
||||
String value = raw.trim();
|
||||
while (value.endsWith("/") && value.length() > 1) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
if (!value.startsWith("/")) {
|
||||
value = "/" + value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Entry chooseEntry(NormalizedTarget target, List<Entry> entries) {
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (entries.size() == 1) {
|
||||
return entries.get(0);
|
||||
}
|
||||
|
||||
List<Entry> exact = new ArrayList<>();
|
||||
List<Entry> prefix = new ArrayList<>();
|
||||
int bestPrefixLen = -1;
|
||||
for (Entry entry : entries) {
|
||||
MatchQuality quality = bestTargetMatch(entry, target);
|
||||
if (quality.exact) {
|
||||
exact.add(entry);
|
||||
continue;
|
||||
}
|
||||
if (quality.prefixLength <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (quality.prefixLength > bestPrefixLen) {
|
||||
prefix.clear();
|
||||
prefix.add(entry);
|
||||
bestPrefixLen = quality.prefixLength;
|
||||
} else if (quality.prefixLength == bestPrefixLen) {
|
||||
prefix.add(entry);
|
||||
}
|
||||
}
|
||||
if (exact.size() == 1) {
|
||||
return exact.get(0);
|
||||
}
|
||||
if (exact.size() > 1 || prefix.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return prefix.size() == 1 ? prefix.get(0) : null;
|
||||
}
|
||||
|
||||
private static boolean entryMatchesHost(Entry entry, String host) {
|
||||
for (NormalizedTarget target : entryTargets(entry)) {
|
||||
if (target.host.equals(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean entryMatchesParentHost(Entry entry, String host) {
|
||||
for (NormalizedTarget target : entryTargets(entry)) {
|
||||
if (!target.host.isEmpty() && host.endsWith("." + target.host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<NormalizedTarget> entryTargets(Entry entry) {
|
||||
List<String> rawTargets = entry.targets;
|
||||
if (rawTargets.isEmpty()) {
|
||||
rawTargets = new ArrayList<>();
|
||||
rawTargets.add(entry.url);
|
||||
}
|
||||
List<NormalizedTarget> targets = new ArrayList<>();
|
||||
for (String rawTarget : rawTargets) {
|
||||
NormalizedTarget target = normalizeURL(rawTarget);
|
||||
if (!target.host.isEmpty()) {
|
||||
targets.add(target);
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static MatchQuality bestTargetMatch(Entry entry, NormalizedTarget target) {
|
||||
int bestPrefixLen = -1;
|
||||
for (NormalizedTarget entryTarget : entryTargets(entry)) {
|
||||
if (entryTarget.url.equals(target.url)) {
|
||||
return new MatchQuality(true, 0);
|
||||
}
|
||||
if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) {
|
||||
bestPrefixLen = Math.max(bestPrefixLen, entryTarget.path.length());
|
||||
}
|
||||
}
|
||||
return new MatchQuality(false, bestPrefixLen);
|
||||
return AutofillTargetMatcher.normalize(raw).host;
|
||||
}
|
||||
|
||||
static final class Entry {
|
||||
final String id;
|
||||
final String title;
|
||||
final String username;
|
||||
final String password;
|
||||
final String host;
|
||||
final String url;
|
||||
final List<String> targets;
|
||||
final List<String> path;
|
||||
|
||||
Entry(String title, String username, String password, String host, String url, List<String> targets) {
|
||||
Entry(String id, String title, String username, String password, String host, String url, List<String> targets, List<String> path) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.host = host;
|
||||
this.url = url;
|
||||
this.targets = new ArrayList<>(targets);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MatchQuality {
|
||||
final boolean exact;
|
||||
final int prefixLength;
|
||||
|
||||
MatchQuality(boolean exact, int prefixLength) {
|
||||
this.exact = exact;
|
||||
this.prefixLength = prefixLength;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class NormalizedTarget {
|
||||
final String host;
|
||||
final String path;
|
||||
final String url;
|
||||
|
||||
NormalizedTarget(String host, String path, String url) {
|
||||
this.host = host;
|
||||
this.path = path;
|
||||
this.url = url;
|
||||
this.path = new ArrayList<>(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
final class AutofillFallbackTarget {
|
||||
private static final String APP_SCHEME = "androidapp://";
|
||||
|
||||
private AutofillFallbackTarget() {
|
||||
}
|
||||
|
||||
static String resolve(String packageName, String webDomain) {
|
||||
String domain = trim(webDomain);
|
||||
if (!domain.isEmpty()) {
|
||||
return domain;
|
||||
}
|
||||
String pkg = trim(packageName);
|
||||
if (pkg.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return APP_SCHEME + pkg;
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
final class AutofillTargetMatcher {
|
||||
private AutofillTargetMatcher() {
|
||||
}
|
||||
|
||||
static Entry findBestMatch(List<Entry> entries, String rawTarget) {
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
NormalizedTarget target = normalize(rawTarget);
|
||||
if (target.host.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Entry> exactHost = new ArrayList<>();
|
||||
List<Entry> parentHost = new ArrayList<>();
|
||||
for (Entry entry : entries) {
|
||||
if (entryMatchesHost(entry, target.host)) {
|
||||
exactHost.add(entry);
|
||||
continue;
|
||||
}
|
||||
if (entryMatchesParentHost(entry, target.host)) {
|
||||
parentHost.add(entry);
|
||||
}
|
||||
}
|
||||
Entry matched = chooseEntry(target, exactHost);
|
||||
if (matched != null) {
|
||||
return matched;
|
||||
}
|
||||
return chooseEntry(target, parentHost);
|
||||
}
|
||||
|
||||
static List<Entry> chooserCandidates(List<Entry> entries, String rawTarget) {
|
||||
if (entries == null || entries.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
Entry direct = findBestMatch(entries, rawTarget);
|
||||
if (direct != null) {
|
||||
List<Entry> resolved = new ArrayList<>();
|
||||
resolved.add(direct);
|
||||
return resolved;
|
||||
}
|
||||
NormalizedTarget target = normalize(rawTarget);
|
||||
List<Entry> exactHost = new ArrayList<>();
|
||||
List<Entry> parentHost = new ArrayList<>();
|
||||
for (Entry entry : entries) {
|
||||
if (entryMatchesHost(entry, target.host)) {
|
||||
exactHost.add(entry);
|
||||
continue;
|
||||
}
|
||||
if (entryMatchesParentHost(entry, target.host)) {
|
||||
parentHost.add(entry);
|
||||
}
|
||||
}
|
||||
if (!exactHost.isEmpty()) {
|
||||
return sortEntries(exactHost);
|
||||
}
|
||||
if (!parentHost.isEmpty()) {
|
||||
return sortEntries(parentHost);
|
||||
}
|
||||
return sortEntries(entries);
|
||||
}
|
||||
|
||||
static NormalizedTarget normalize(String raw) {
|
||||
if (raw == null) {
|
||||
return new NormalizedTarget("", "", "");
|
||||
}
|
||||
String trimmed = raw.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return new NormalizedTarget("", "", "");
|
||||
}
|
||||
String original = trimmed;
|
||||
String value = trimmed.toLowerCase(Locale.US);
|
||||
if (value.startsWith("http://")) {
|
||||
value = value.substring("http://".length());
|
||||
} else if (value.startsWith("https://")) {
|
||||
value = value.substring("https://".length());
|
||||
}
|
||||
int slash = value.indexOf('/');
|
||||
if (slash >= 0) {
|
||||
value = value.substring(0, slash);
|
||||
}
|
||||
int colon = value.indexOf(':');
|
||||
if (colon >= 0) {
|
||||
value = value.substring(0, colon);
|
||||
}
|
||||
String host = value;
|
||||
String path = "/";
|
||||
int schemeSep = original.indexOf("://");
|
||||
if (schemeSep < 0) {
|
||||
original = "https://" + original;
|
||||
}
|
||||
try {
|
||||
URI uri = URI.create(original);
|
||||
if (uri.getHost() != null) {
|
||||
host = uri.getHost().toLowerCase(Locale.US);
|
||||
}
|
||||
path = cleanPath(uri.getPath());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
path = "/";
|
||||
}
|
||||
return new NormalizedTarget(host, path, host + path);
|
||||
}
|
||||
|
||||
private static String cleanPath(String raw) {
|
||||
if (raw == null || raw.trim().isEmpty() || "/".equals(raw.trim())) {
|
||||
return "/";
|
||||
}
|
||||
String value = raw.trim();
|
||||
while (value.endsWith("/") && value.length() > 1) {
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
if (!value.startsWith("/")) {
|
||||
value = "/" + value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static Entry chooseEntry(NormalizedTarget target, List<Entry> entries) {
|
||||
if (entries.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (entries.size() == 1) {
|
||||
return entries.get(0);
|
||||
}
|
||||
List<Entry> exact = new ArrayList<>();
|
||||
List<Entry> prefix = new ArrayList<>();
|
||||
int bestPrefixLen = -1;
|
||||
for (Entry entry : entries) {
|
||||
MatchQuality quality = bestTargetMatch(entry, target);
|
||||
if (quality.exact) {
|
||||
exact.add(entry);
|
||||
continue;
|
||||
}
|
||||
if (quality.prefixLength <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (quality.prefixLength > bestPrefixLen) {
|
||||
prefix.clear();
|
||||
prefix.add(entry);
|
||||
bestPrefixLen = quality.prefixLength;
|
||||
} else if (quality.prefixLength == bestPrefixLen) {
|
||||
prefix.add(entry);
|
||||
}
|
||||
}
|
||||
if (exact.size() == 1) {
|
||||
return exact.get(0);
|
||||
}
|
||||
if (exact.size() > 1 || prefix.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return prefix.size() == 1 ? prefix.get(0) : null;
|
||||
}
|
||||
|
||||
private static boolean entryMatchesHost(Entry entry, String host) {
|
||||
for (NormalizedTarget target : entryTargets(entry)) {
|
||||
if (target.host.equals(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean entryMatchesParentHost(Entry entry, String host) {
|
||||
for (NormalizedTarget target : entryTargets(entry)) {
|
||||
if (!target.host.isEmpty() && host.endsWith("." + target.host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<NormalizedTarget> entryTargets(Entry entry) {
|
||||
List<String> rawTargets = entry.targets;
|
||||
if (rawTargets.isEmpty()) {
|
||||
rawTargets = new ArrayList<>();
|
||||
rawTargets.add(entry.url);
|
||||
}
|
||||
List<NormalizedTarget> targets = new ArrayList<>();
|
||||
for (String rawTarget : rawTargets) {
|
||||
NormalizedTarget target = normalize(rawTarget);
|
||||
if (!target.host.isEmpty()) {
|
||||
targets.add(target);
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
private static MatchQuality bestTargetMatch(Entry entry, NormalizedTarget target) {
|
||||
int bestPrefixLen = -1;
|
||||
for (NormalizedTarget entryTarget : entryTargets(entry)) {
|
||||
if (entryTarget.url.equals(target.url)) {
|
||||
return new MatchQuality(true, 0);
|
||||
}
|
||||
if (!"/".equals(entryTarget.path) && target.path.startsWith(entryTarget.path)) {
|
||||
bestPrefixLen = Math.max(bestPrefixLen, entryTarget.path.length());
|
||||
}
|
||||
}
|
||||
return new MatchQuality(false, bestPrefixLen);
|
||||
}
|
||||
|
||||
private static List<Entry> sortEntries(List<Entry> entries) {
|
||||
List<Entry> sorted = new ArrayList<>(entries);
|
||||
sorted.sort(Comparator
|
||||
.comparing((Entry entry) -> entry.title.toLowerCase(Locale.US))
|
||||
.thenComparing(entry -> String.join("/", entry.path).toLowerCase(Locale.US))
|
||||
.thenComparing(entry -> entry.id));
|
||||
return sorted;
|
||||
}
|
||||
|
||||
static final class NormalizedTarget {
|
||||
final String host;
|
||||
final String path;
|
||||
final String url;
|
||||
|
||||
NormalizedTarget(String host, String path, String url) {
|
||||
this.host = host;
|
||||
this.path = path;
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Entry {
|
||||
final String id;
|
||||
final String title;
|
||||
final String username;
|
||||
final String password;
|
||||
final String host;
|
||||
final String url;
|
||||
final List<String> targets;
|
||||
final List<String> path;
|
||||
|
||||
Entry(String id, String title, String username, String password, String host, String url, List<String> targets, List<String> path) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.host = host;
|
||||
this.url = url;
|
||||
this.targets = new ArrayList<>(targets);
|
||||
this.path = new ArrayList<>(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MatchQuality {
|
||||
final boolean exact;
|
||||
final int prefixLength;
|
||||
|
||||
MatchQuality(boolean exact, int prefixLength) {
|
||||
this.exact = exact;
|
||||
this.prefixLength = prefixLength;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import java.util.List;
|
||||
|
||||
public final class KeePassGOAccessibilityService extends AccessibilityService {
|
||||
private static final String TAG = "KeePassGOA11y";
|
||||
|
||||
private String lastFilledSignature = "";
|
||||
|
||||
@Override
|
||||
@@ -22,22 +21,17 @@ public final class KeePassGOAccessibilityService extends AccessibilityService {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
CharSequence packageName = root.getPackageName();
|
||||
if (packageName == null || !"com.android.chrome".contentEquals(packageName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChromeForm form = inspectChrome(root);
|
||||
ChromeForm form = inspectWindow(root);
|
||||
if (form == null || form.passwordField == null) {
|
||||
return;
|
||||
}
|
||||
AutofillCacheStore.Entry entry = AutofillCacheStore.findBestMatch(this, form.url);
|
||||
AutofillCacheStore.Entry entry = AutofillCacheStore.findBestMatch(this, form.matchTarget);
|
||||
if (entry == null) {
|
||||
Log.i(TAG, "no accessibility-fill match for " + form.url);
|
||||
Log.i(TAG, "no accessibility-fill match for " + form.matchTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
String signature = form.url + "|" + entry.username + "|" + nodeKey(form.passwordField);
|
||||
String signature = form.matchTarget + "|" + entry.username + "|" + nodeKey(form.passwordField);
|
||||
if (signature.equals(lastFilledSignature)) {
|
||||
return;
|
||||
}
|
||||
@@ -49,7 +43,7 @@ public final class KeePassGOAccessibilityService extends AccessibilityService {
|
||||
filled |= setNodeText(form.passwordField, entry.password);
|
||||
if (filled) {
|
||||
lastFilledSignature = signature;
|
||||
Log.i(TAG, "filled login form for " + form.url + " with " + entry.username);
|
||||
Log.i(TAG, "filled login form for " + form.matchTarget + " with " + entry.username);
|
||||
}
|
||||
} catch (Exception err) {
|
||||
Log.e(TAG, "accessibility fill failed", err);
|
||||
@@ -61,11 +55,11 @@ public final class KeePassGOAccessibilityService extends AccessibilityService {
|
||||
Log.i(TAG, "accessibility service interrupted");
|
||||
}
|
||||
|
||||
private static ChromeForm inspectChrome(AccessibilityNodeInfo root) {
|
||||
private static ChromeForm inspectWindow(AccessibilityNodeInfo root) {
|
||||
List<AccessibilityNodeInfo> editables = new ArrayList<>();
|
||||
ChromeForm form = new ChromeForm();
|
||||
walk(root, editables, form);
|
||||
if (form.url.isEmpty()) {
|
||||
if (form.matchTarget.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (form.passwordField == null) {
|
||||
@@ -111,7 +105,13 @@ public final class KeePassGOAccessibilityService extends AccessibilityService {
|
||||
if (viewID != null && viewID.toString().endsWith(":id/url_bar")) {
|
||||
CharSequence text = node.getText();
|
||||
if (text != null) {
|
||||
form.url = text.toString().trim();
|
||||
form.webDomain = text.toString().trim();
|
||||
}
|
||||
}
|
||||
if (form.packageName.isEmpty()) {
|
||||
CharSequence packageName = node.getPackageName();
|
||||
if (packageName != null) {
|
||||
form.packageName = packageName.toString().trim();
|
||||
}
|
||||
}
|
||||
if (node.isEditable()) {
|
||||
@@ -128,6 +128,7 @@ public final class KeePassGOAccessibilityService extends AccessibilityService {
|
||||
walk(child, editables, form);
|
||||
}
|
||||
}
|
||||
form.matchTarget = AutofillFallbackTarget.resolve(form.packageName, form.webDomain);
|
||||
}
|
||||
|
||||
private static boolean isPasswordNode(AccessibilityNodeInfo node) {
|
||||
@@ -188,7 +189,9 @@ public final class KeePassGOAccessibilityService extends AccessibilityService {
|
||||
}
|
||||
|
||||
private static final class ChromeForm {
|
||||
String url = "";
|
||||
String packageName = "";
|
||||
String webDomain = "";
|
||||
String matchTarget = "";
|
||||
AccessibilityNodeInfo usernameField;
|
||||
AccessibilityNodeInfo passwordField;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.autofill.AutofillId;
|
||||
import android.view.autofill.AutofillManager;
|
||||
import android.view.autofill.AutofillValue;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.service.autofill.Dataset;
|
||||
|
||||
public final class KeePassGOAutofillPickerActivity extends Activity {
|
||||
static final String EXTRA_TARGET = "org.julianfamily.keepassgo.extra.AUTOFILL_TARGET";
|
||||
static final String EXTRA_PACKAGE_NAME = "org.julianfamily.keepassgo.extra.AUTOFILL_PACKAGE";
|
||||
static final String EXTRA_USERNAME_ID = "org.julianfamily.keepassgo.extra.USERNAME_ID";
|
||||
static final String EXTRA_PASSWORD_ID = "org.julianfamily.keepassgo.extra.PASSWORD_ID";
|
||||
|
||||
private final List<AutofillCacheStore.Entry> allEntries = new ArrayList<>();
|
||||
private final List<AutofillCacheStore.Entry> visibleEntries = new ArrayList<>();
|
||||
private ArrayAdapter<String> adapter;
|
||||
private String matchTarget = "";
|
||||
private String packageName = "";
|
||||
private AutofillId usernameID;
|
||||
private AutofillId passwordID;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
matchTarget = intent.getStringExtra(EXTRA_TARGET);
|
||||
packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
|
||||
usernameID = intent.getParcelableExtra(EXTRA_USERNAME_ID, AutofillId.class);
|
||||
passwordID = intent.getParcelableExtra(EXTRA_PASSWORD_ID, AutofillId.class);
|
||||
|
||||
LinearLayout root = new LinearLayout(this);
|
||||
root.setOrientation(LinearLayout.VERTICAL);
|
||||
int padding = dp(16);
|
||||
root.setPadding(padding, padding, padding, padding);
|
||||
|
||||
TextView title = new TextView(this);
|
||||
title.setTextSize(TypedValue.COMPLEX_UNIT_SP, 20);
|
||||
title.setGravity(Gravity.START);
|
||||
title.setPadding(0, 0, 0, dp(8));
|
||||
title.setText(packageName == null || packageName.trim().isEmpty() ? "Search KeePassGO" : "Search KeePassGO for " + packageName);
|
||||
root.addView(title, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
EditText search = new EditText(this);
|
||||
search.setHint("Search entries");
|
||||
root.addView(search, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
|
||||
allEntries.clear();
|
||||
allEntries.addAll(AutofillCacheStore.chooserCandidates(this, matchTarget));
|
||||
visibleEntries.clear();
|
||||
visibleEntries.addAll(allEntries);
|
||||
|
||||
adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, labelsFor(visibleEntries));
|
||||
ListView list = new ListView(this);
|
||||
list.setAdapter(adapter);
|
||||
list.setOnItemClickListener(this::onEntrySelected);
|
||||
|
||||
TextView empty = new TextView(this);
|
||||
empty.setPadding(0, dp(16), 0, 0);
|
||||
empty.setText("No KeePassGO entries are available for autofill.");
|
||||
empty.setVisibility(allEntries.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
list.setVisibility(allEntries.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
root.addView(empty, new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
LinearLayout.LayoutParams listParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0
|
||||
);
|
||||
listParams.weight = 1f;
|
||||
listParams.topMargin = dp(12);
|
||||
root.addView(list, listParams);
|
||||
|
||||
search.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterEntries(s == null ? "" : s.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
}
|
||||
});
|
||||
|
||||
setContentView(root);
|
||||
}
|
||||
|
||||
private void filterEntries(String rawQuery) {
|
||||
String query = rawQuery == null ? "" : rawQuery.trim().toLowerCase(Locale.US);
|
||||
visibleEntries.clear();
|
||||
if (query.isEmpty()) {
|
||||
visibleEntries.addAll(allEntries);
|
||||
} else {
|
||||
for (AutofillCacheStore.Entry entry : allEntries) {
|
||||
if (entryLabel(entry).toLowerCase(Locale.US).contains(query)) {
|
||||
visibleEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
adapter.clear();
|
||||
adapter.addAll(labelsFor(visibleEntries));
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private void onEntrySelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
if (position < 0 || position >= visibleEntries.size() || passwordID == null) {
|
||||
setResult(Activity.RESULT_CANCELED);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
AutofillCacheStore.Entry entry = visibleEntries.get(position);
|
||||
if (matchTarget != null && matchTarget.startsWith("androidapp://")) {
|
||||
AutofillBindingStore.rememberBinding(this, matchTarget, entry.id);
|
||||
}
|
||||
|
||||
Dataset.Builder dataset = new Dataset.Builder();
|
||||
dataset.setId(entry.id);
|
||||
if (usernameID != null && entry.username != null && !entry.username.isEmpty()) {
|
||||
dataset.setValue(usernameID, AutofillValue.forText(entry.username));
|
||||
}
|
||||
dataset.setValue(passwordID, AutofillValue.forText(entry.password));
|
||||
|
||||
Intent result = new Intent();
|
||||
result.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset.build());
|
||||
setResult(Activity.RESULT_OK, result);
|
||||
finish();
|
||||
}
|
||||
|
||||
private static List<String> labelsFor(List<AutofillCacheStore.Entry> entries) {
|
||||
List<String> labels = new ArrayList<>(entries.size());
|
||||
for (AutofillCacheStore.Entry entry : entries) {
|
||||
labels.add(entryLabel(entry));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
private static String entryLabel(AutofillCacheStore.Entry entry) {
|
||||
StringBuilder label = new StringBuilder();
|
||||
label.append(entry.title == null || entry.title.trim().isEmpty() ? "Untitled entry" : entry.title.trim());
|
||||
if (entry.username != null && !entry.username.trim().isEmpty()) {
|
||||
label.append(" (").append(entry.username.trim()).append(")");
|
||||
}
|
||||
if (entry.path != null && !entry.path.isEmpty()) {
|
||||
label.append(" • ").append(String.join(" / ", entry.path));
|
||||
}
|
||||
return label.toString();
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.assist.AssistStructure;
|
||||
import android.content.Intent;
|
||||
import android.os.CancellationSignal;
|
||||
import android.service.autofill.AutofillService;
|
||||
import android.service.autofill.Dataset;
|
||||
@@ -66,29 +68,21 @@ public final class KeePassGOAutofillService extends AutofillService {
|
||||
return;
|
||||
}
|
||||
|
||||
AutofillCacheStore.Entry entry = AutofillCacheStore.findBestMatch(this, target.matchTarget);
|
||||
AutofillCacheStore.Entry entry = findBoundOrBestMatch(target.matchTarget);
|
||||
if (entry == null) {
|
||||
Log.i(TAG, "no autofill cache match");
|
||||
callback.onSuccess(null);
|
||||
FillResponse chooser = chooserResponse(target, fields);
|
||||
if (chooser == null) {
|
||||
Log.i(TAG, "no autofill cache match");
|
||||
callback.onSuccess(null);
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "returning chooser dataset for " + target.matchTarget);
|
||||
callback.onSuccess(chooser);
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "matched entry title=" + entry.title + " user=" + entry.username + " host=" + entry.host);
|
||||
|
||||
RemoteViews presentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
|
||||
presentation.setTextViewText(
|
||||
android.R.id.text1,
|
||||
entry.title + " (" + entry.username + ")"
|
||||
);
|
||||
|
||||
Dataset.Builder dataset = new Dataset.Builder(presentation);
|
||||
if (fields.usernameId != null) {
|
||||
dataset.setValue(fields.usernameId, AutofillValue.forText(entry.username));
|
||||
}
|
||||
dataset.setValue(fields.passwordId, AutofillValue.forText(entry.password));
|
||||
|
||||
FillResponse response = new FillResponse.Builder()
|
||||
.addDataset(dataset.build())
|
||||
.build();
|
||||
FillResponse response = directFillResponse(entry, fields);
|
||||
Log.i(TAG, "returning dataset");
|
||||
callback.onSuccess(response);
|
||||
} catch (Exception err) {
|
||||
@@ -103,6 +97,72 @@ public final class KeePassGOAutofillService extends AutofillService {
|
||||
callback.onSuccess();
|
||||
}
|
||||
|
||||
private AutofillCacheStore.Entry findBoundOrBestMatch(String matchTarget) {
|
||||
String entryID = AutofillBindingStore.entryIDForTarget(this, matchTarget);
|
||||
if (!entryID.isEmpty()) {
|
||||
AutofillCacheStore.Entry bound = AutofillCacheStore.findByID(this, entryID);
|
||||
if (bound != null) {
|
||||
return bound;
|
||||
}
|
||||
}
|
||||
return AutofillCacheStore.findBestMatch(this, matchTarget);
|
||||
}
|
||||
|
||||
private FillResponse directFillResponse(AutofillCacheStore.Entry entry, ParsedFields fields) {
|
||||
RemoteViews presentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
|
||||
presentation.setTextViewText(
|
||||
android.R.id.text1,
|
||||
entry.title + " (" + entry.username + ")"
|
||||
);
|
||||
|
||||
Dataset.Builder dataset = new Dataset.Builder(presentation);
|
||||
dataset.setId(entry.id);
|
||||
if (fields.usernameId != null) {
|
||||
dataset.setValue(fields.usernameId, AutofillValue.forText(entry.username));
|
||||
}
|
||||
dataset.setValue(fields.passwordId, AutofillValue.forText(entry.password));
|
||||
|
||||
return new FillResponse.Builder()
|
||||
.addDataset(dataset.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
private FillResponse chooserResponse(ParsedTarget target, ParsedFields fields) {
|
||||
if (fields.passwordId == null) {
|
||||
return null;
|
||||
}
|
||||
List<AutofillCacheStore.Entry> candidates = AutofillCacheStore.chooserCandidates(this, target.matchTarget);
|
||||
if (candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RemoteViews presentation = new RemoteViews(getPackageName(), android.R.layout.simple_list_item_1);
|
||||
presentation.setTextViewText(android.R.id.text1, "Search KeePassGO");
|
||||
|
||||
Intent intent = new Intent(this, KeePassGOAutofillPickerActivity.class);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_TARGET, target.matchTarget);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_PACKAGE_NAME, target.packageName);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_USERNAME_ID, fields.usernameId);
|
||||
intent.putExtra(KeePassGOAutofillPickerActivity.EXTRA_PASSWORD_ID, fields.passwordId);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
target.matchTarget.hashCode(),
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE
|
||||
);
|
||||
|
||||
AutofillId[] ids;
|
||||
if (fields.usernameId != null) {
|
||||
ids = new AutofillId[]{fields.usernameId, fields.passwordId};
|
||||
} else {
|
||||
ids = new AutofillId[]{fields.passwordId};
|
||||
}
|
||||
return new FillResponse.Builder()
|
||||
.setAuthentication(ids, pendingIntent.getIntentSender(), presentation)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static ParsedTarget parseWindow(AssistStructure structure, ParsedFields fields) {
|
||||
String domain = "";
|
||||
final int windowCount = structure.getWindowNodeCount();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
@@ -10,13 +11,18 @@ import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class SharedVaultImportActivity extends Activity {
|
||||
private static final String TAG = "KeePassGOImport";
|
||||
private static final String DEFAULT_NAME = "shared-vault.kdbx";
|
||||
private static final String PENDING_SHARED_VAULT = "pending-shared-vault.kdbx";
|
||||
private static final String PENDING_SHARED_VAULT_NAME = "pending-shared-vault-name.txt";
|
||||
private static final String PENDING_SHARED_LOOKUP = "pending-shared-lookup.txt";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle state) {
|
||||
@@ -36,6 +42,17 @@ public final class SharedVaultImportActivity extends Activity {
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
logIntent(intent);
|
||||
String sharedLookup = resolveSharedLookup(intent);
|
||||
if (!sharedLookup.isEmpty()) {
|
||||
try {
|
||||
persistPendingLookup(sharedLookup);
|
||||
Log.i(TAG, "queued shared lookup target");
|
||||
} catch (IOException | RuntimeException err) {
|
||||
Log.e(TAG, "failed to queue shared lookup target", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Uri uri = resolveSharedUri(intent);
|
||||
if (uri == null) {
|
||||
Log.i(TAG, "no shared vault URI on intent");
|
||||
@@ -55,21 +72,63 @@ public final class SharedVaultImportActivity extends Activity {
|
||||
}
|
||||
String action = intent.getAction();
|
||||
if (Intent.ACTION_SEND.equals(action)) {
|
||||
return intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
Uri extraStream = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (extraStream != null) {
|
||||
return extraStream;
|
||||
}
|
||||
}
|
||||
if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
|
||||
ArrayList<Uri> streams = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
if (streams != null && !streams.isEmpty()) {
|
||||
return streams.get(0);
|
||||
}
|
||||
}
|
||||
if (Intent.ACTION_VIEW.equals(action)) {
|
||||
return intent.getData();
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
ClipData clipData = intent.getClipData();
|
||||
if (clipData != null && clipData.getItemCount() > 0) {
|
||||
Uri clipUri = clipData.getItemAt(0).getUri();
|
||||
if (clipUri != null) {
|
||||
return clipUri;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveSharedLookup(Intent intent) {
|
||||
if (intent == null) {
|
||||
return "";
|
||||
}
|
||||
String action = intent.getAction();
|
||||
if (Intent.ACTION_SEND.equals(action) && "text/plain".equalsIgnoreCase(intent.getType())) {
|
||||
CharSequence extraText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
|
||||
if (extraText != null) {
|
||||
return extraText.toString().trim();
|
||||
}
|
||||
}
|
||||
if (Intent.ACTION_VIEW.equals(action)) {
|
||||
Uri data = intent.getData();
|
||||
if (data != null) {
|
||||
String scheme = data.getScheme();
|
||||
if ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme)) {
|
||||
return data.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private void persistPendingImport(Uri uri) throws IOException {
|
||||
File dir = new File(getFilesDir(), "keepassgo");
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
throw new IOException("failed to create " + dir.getAbsolutePath());
|
||||
}
|
||||
File pendingFile = new File(dir, "pending-shared-vault.kdbx");
|
||||
try (InputStream in = getContentResolver().openInputStream(uri)) {
|
||||
File pendingFile = new File(dir, PENDING_SHARED_VAULT);
|
||||
try (InputStream in = openSharedInputStream(uri)) {
|
||||
if (in == null) {
|
||||
throw new IOException("failed to open shared vault stream");
|
||||
}
|
||||
@@ -82,12 +141,34 @@ public final class SharedVaultImportActivity extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
File nameFile = new File(dir, "pending-shared-vault-name.txt");
|
||||
File nameFile = new File(dir, PENDING_SHARED_VAULT_NAME);
|
||||
try (FileOutputStream out = new FileOutputStream(nameFile, false)) {
|
||||
out.write(resolveDisplayName(uri).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private void persistPendingLookup(String lookup) throws IOException {
|
||||
File dir = new File(getFilesDir(), "keepassgo");
|
||||
if (!dir.exists() && !dir.mkdirs()) {
|
||||
throw new IOException("failed to create " + dir.getAbsolutePath());
|
||||
}
|
||||
File pendingFile = new File(dir, PENDING_SHARED_LOOKUP);
|
||||
try (FileOutputStream out = new FileOutputStream(pendingFile, false)) {
|
||||
out.write(lookup.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream openSharedInputStream(Uri uri) throws IOException {
|
||||
if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
String path = uri.getPath();
|
||||
if (path == null || path.trim().isEmpty()) {
|
||||
throw new IOException("file URI is missing a path");
|
||||
}
|
||||
return new FileInputStream(new File(path));
|
||||
}
|
||||
return getContentResolver().openInputStream(uri);
|
||||
}
|
||||
|
||||
private String resolveDisplayName(Uri uri) {
|
||||
String displayName = queryDisplayName(uri);
|
||||
if (!displayName.isEmpty()) {
|
||||
@@ -123,6 +204,20 @@ public final class SharedVaultImportActivity extends Activity {
|
||||
return "";
|
||||
}
|
||||
|
||||
private void logIntent(Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "intent action=" + intent.getAction()
|
||||
+ " type=" + intent.getType()
|
||||
+ " data=" + intent.getData()
|
||||
+ " flags=0x" + Integer.toHexString(intent.getFlags()));
|
||||
ClipData clipData = intent.getClipData();
|
||||
if (clipData != null) {
|
||||
Log.i(TAG, "intent clip items=" + clipData.getItemCount());
|
||||
}
|
||||
}
|
||||
|
||||
private void launchMainActivity() {
|
||||
Intent launch = new Intent();
|
||||
launch.setClassName(this, "org.gioui.GioActivity");
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class AutofillCacheStoreBehaviorTest {
|
||||
public static void main(String[] args) {
|
||||
testFindBestMatchUsesAndroidAppTargets();
|
||||
testChooserCandidatesCollapseToExactAndroidAppMatch();
|
||||
testChooserCandidatesStayScopedToExactHostMatches();
|
||||
testChooserCandidatesStayScopedToParentHostMatches();
|
||||
}
|
||||
|
||||
private static void testFindBestMatchUsesAndroidAppTargets() {
|
||||
List<AutofillTargetMatcher.Entry> entries = new ArrayList<>();
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"blink-entry",
|
||||
"Blink",
|
||||
"linuscaldwell",
|
||||
"bellagio-stack",
|
||||
"account.blinknetwork.com",
|
||||
"https://account.blinknetwork.com",
|
||||
Arrays.asList("https://account.blinknetwork.com", "androidapp://com.blinknetwork.mobile2"),
|
||||
Arrays.asList("Crew", "Apps")
|
||||
));
|
||||
|
||||
AutofillTargetMatcher.Entry got = AutofillTargetMatcher.findBestMatch(entries, "androidapp://com.blinknetwork.mobile2");
|
||||
if (got == null || !"blink-entry".equals(got.id)) {
|
||||
throw new AssertionError("findBestMatch(entries, androidapp target) = " + describe(got) + ", want blink-entry");
|
||||
}
|
||||
}
|
||||
|
||||
private static void testChooserCandidatesCollapseToExactAndroidAppMatch() {
|
||||
List<AutofillTargetMatcher.Entry> entries = new ArrayList<>();
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"blink-entry",
|
||||
"Blink",
|
||||
"linuscaldwell",
|
||||
"bellagio-stack",
|
||||
"account.blinknetwork.com",
|
||||
"https://account.blinknetwork.com",
|
||||
Arrays.asList("https://account.blinknetwork.com", "androidapp://com.blinknetwork.mobile2"),
|
||||
Arrays.asList("Crew", "Apps")
|
||||
));
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"night-fox-entry",
|
||||
"Night Fox",
|
||||
"nightfox",
|
||||
"vault-code",
|
||||
"gitlab.com",
|
||||
"https://gitlab.com",
|
||||
Arrays.asList("https://gitlab.com"),
|
||||
Arrays.asList("Crew", "Internet")
|
||||
));
|
||||
|
||||
List<AutofillTargetMatcher.Entry> got = AutofillTargetMatcher.chooserCandidates(entries, "androidapp://com.blinknetwork.mobile2");
|
||||
if (got.size() != 1 || !"blink-entry".equals(got.get(0).id)) {
|
||||
throw new AssertionError("chooserCandidates(entries, androidapp target) = " + describe(got) + ", want [blink-entry]");
|
||||
}
|
||||
}
|
||||
|
||||
private static void testChooserCandidatesStayScopedToExactHostMatches() {
|
||||
List<AutofillTargetMatcher.Entry> entries = new ArrayList<>();
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"bellagio-primary",
|
||||
"Bellagio Primary",
|
||||
"dannyocean",
|
||||
"vault-code",
|
||||
"bellagio.example.invalid",
|
||||
"https://bellagio.example.invalid/login",
|
||||
Arrays.asList("https://bellagio.example.invalid/login"),
|
||||
Arrays.asList("Crew", "Internet")
|
||||
));
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"bellagio-backup",
|
||||
"Bellagio Backup",
|
||||
"rustyryan",
|
||||
"backup-code",
|
||||
"bellagio.example.invalid",
|
||||
"https://bellagio.example.invalid/admin",
|
||||
Arrays.asList("https://bellagio.example.invalid/admin"),
|
||||
Arrays.asList("Crew", "Internet")
|
||||
));
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"night-fox-entry",
|
||||
"Night Fox",
|
||||
"nightfox",
|
||||
"vault-code",
|
||||
"gitlab.com",
|
||||
"https://gitlab.com",
|
||||
Arrays.asList("https://gitlab.com"),
|
||||
Arrays.asList("Crew", "Internet")
|
||||
));
|
||||
|
||||
List<AutofillTargetMatcher.Entry> got = AutofillTargetMatcher.chooserCandidates(entries, "https://bellagio.example.invalid/security");
|
||||
if (got.size() != 2 || !containsIDs(got, "bellagio-primary", "bellagio-backup")) {
|
||||
throw new AssertionError("chooserCandidates(entries, exact host) = " + describe(got) + ", want only Bellagio entries");
|
||||
}
|
||||
}
|
||||
|
||||
private static void testChooserCandidatesStayScopedToParentHostMatches() {
|
||||
List<AutofillTargetMatcher.Entry> entries = new ArrayList<>();
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"bellagio-parent",
|
||||
"Bellagio Parent",
|
||||
"linuscaldwell",
|
||||
"chip-stack",
|
||||
"example.invalid",
|
||||
"https://example.invalid/login",
|
||||
Arrays.asList("https://example.invalid/login"),
|
||||
Arrays.asList("Crew", "Internet")
|
||||
));
|
||||
entries.add(new AutofillTargetMatcher.Entry(
|
||||
"night-fox-entry",
|
||||
"Night Fox",
|
||||
"nightfox",
|
||||
"vault-code",
|
||||
"gitlab.com",
|
||||
"https://gitlab.com",
|
||||
Arrays.asList("https://gitlab.com"),
|
||||
Arrays.asList("Crew", "Internet")
|
||||
));
|
||||
|
||||
List<AutofillTargetMatcher.Entry> got = AutofillTargetMatcher.chooserCandidates(entries, "https://bellagio.example.invalid/security");
|
||||
if (got.size() != 1 || !"bellagio-parent".equals(got.get(0).id)) {
|
||||
throw new AssertionError("chooserCandidates(entries, parent host) = " + describe(got) + ", want [bellagio-parent]");
|
||||
}
|
||||
}
|
||||
|
||||
private static String describe(AutofillTargetMatcher.Entry entry) {
|
||||
if (entry == null) {
|
||||
return "null";
|
||||
}
|
||||
return entry.id;
|
||||
}
|
||||
|
||||
private static String describe(List<AutofillTargetMatcher.Entry> entries) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (AutofillTargetMatcher.Entry entry : entries) {
|
||||
ids.add(entry.id);
|
||||
}
|
||||
return ids.toString();
|
||||
}
|
||||
|
||||
private static boolean containsIDs(List<AutofillTargetMatcher.Entry> entries, String... wantIDs) {
|
||||
List<String> ids = new ArrayList<>();
|
||||
for (AutofillTargetMatcher.Entry entry : entries) {
|
||||
ids.add(entry.id);
|
||||
}
|
||||
return ids.containsAll(Arrays.asList(wantIDs)) && ids.size() == wantIDs.length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.julianfamily.keepassgo;
|
||||
|
||||
public final class AutofillFallbackTargetBehaviorTest {
|
||||
public static void main(String[] args) {
|
||||
testPrefersWebDomainWhenPresent();
|
||||
testFallsBackToAndroidAppPackage();
|
||||
testEmptyWhenNeitherSignalExists();
|
||||
}
|
||||
|
||||
private static void testPrefersWebDomainWhenPresent() {
|
||||
String got = AutofillFallbackTarget.resolve("com.android.chrome", "gitlab.com");
|
||||
if (!"gitlab.com".equals(got)) {
|
||||
throw new AssertionError("resolve(chrome, gitlab.com) = " + got + ", want gitlab.com");
|
||||
}
|
||||
}
|
||||
|
||||
private static void testFallsBackToAndroidAppPackage() {
|
||||
String got = AutofillFallbackTarget.resolve("com.blinknetwork.mobile2", "");
|
||||
if (!"androidapp://com.blinknetwork.mobile2".equals(got)) {
|
||||
throw new AssertionError("resolve(package-only) = " + got + ", want androidapp://com.blinknetwork.mobile2");
|
||||
}
|
||||
}
|
||||
|
||||
private static void testEmptyWhenNeitherSignalExists() {
|
||||
String got = AutofillFallbackTarget.resolve("", "");
|
||||
if (!"".equals(got)) {
|
||||
throw new AssertionError("resolve(empty) = " + got + ", want empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,6 +193,120 @@ 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,
|
||||
@@ -207,6 +323,7 @@ function normalizePageState(state) {
|
||||
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
|
||||
};
|
||||
@@ -228,6 +345,7 @@ function defaultPageState(tabId, pageUrl) {
|
||||
pendingEntryId: "",
|
||||
pendingTarget: null,
|
||||
pendingMessage: "",
|
||||
pendingSave: null,
|
||||
lastFilledEntryId: "",
|
||||
updatedAt: 0
|
||||
});
|
||||
@@ -292,6 +410,16 @@ function approvalHintForState(state) {
|
||||
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;
|
||||
@@ -337,6 +465,12 @@ function actionPresentationForState(state) {
|
||||
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) {
|
||||
@@ -492,7 +626,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
|
||||
state.matches = [];
|
||||
state.updatedAt = Date.now();
|
||||
const saved = await setPageState(tabId, state);
|
||||
if (saved.pendingFill) {
|
||||
if (shouldContinueWatchingState(saved)) {
|
||||
schedulePendingPoll(tabId, resolvedURL);
|
||||
} else {
|
||||
clearPendingPoll(tabId);
|
||||
@@ -502,7 +636,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
|
||||
|
||||
if (shouldReuseMatches(state, force)) {
|
||||
const saved = await setPageState(tabId, state);
|
||||
if (saved.pendingFill) {
|
||||
if (shouldContinueWatchingState(saved)) {
|
||||
schedulePendingPoll(tabId, resolvedURL);
|
||||
} else {
|
||||
clearPendingPoll(tabId);
|
||||
@@ -524,12 +658,12 @@ 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()
|
||||
};
|
||||
const saved = await setPageState(tabId, state);
|
||||
if (saved.pendingFill) {
|
||||
if (shouldContinueWatchingState(saved)) {
|
||||
schedulePendingPoll(tabId, resolvedURL);
|
||||
} else {
|
||||
clearPendingPoll(tabId);
|
||||
@@ -653,11 +787,65 @@ async function refreshActivePage(options = {}) {
|
||||
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,
|
||||
defaultSettings
|
||||
};
|
||||
|
||||
@@ -692,11 +880,54 @@ 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 });
|
||||
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, {
|
||||
|
||||
@@ -49,6 +49,103 @@ test("tokenPendingApprovalCount reads token-scoped approval state", () => {
|
||||
assert.equal(background.tokenPendingApprovalCount({}), 0);
|
||||
});
|
||||
|
||||
test("shouldContinueWatchingState keeps polling locked login pages", () => {
|
||||
assert.equal(background.shouldContinueWatchingState({
|
||||
pageHasLoginForm: true,
|
||||
pendingFill: false,
|
||||
status: { locked: true }
|
||||
}), true);
|
||||
assert.equal(background.shouldContinueWatchingState({
|
||||
pageHasLoginForm: true,
|
||||
pendingFill: true,
|
||||
status: { locked: false }
|
||||
}), true);
|
||||
assert.equal(background.shouldContinueWatchingState({
|
||||
pageHasLoginForm: true,
|
||||
pendingFill: false,
|
||||
status: { locked: false }
|
||||
}), false);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const plan = background.savePlanForObservedLogin({
|
||||
username: "dannyocean",
|
||||
password: "bellagio-safe",
|
||||
url: "https://vault.example.invalid/login"
|
||||
}, [
|
||||
{
|
||||
id: "vault-console",
|
||||
title: "Vault Console",
|
||||
username: "dannyocean",
|
||||
url: "vault.example.invalid",
|
||||
path: ["Crew", "Internet"]
|
||||
},
|
||||
{
|
||||
id: "bellagio-backup",
|
||||
title: "Bellagio Backup",
|
||||
username: "rustyryan",
|
||||
url: "vault.example.invalid",
|
||||
path: ["Crew", "Internet"]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(plan, {
|
||||
mode: "update",
|
||||
entryId: "vault-console",
|
||||
title: "Vault Console",
|
||||
path: ["Crew", "Internet"],
|
||||
username: "dannyocean",
|
||||
password: "bellagio-safe",
|
||||
url: "https://vault.example.invalid/login"
|
||||
});
|
||||
});
|
||||
|
||||
test("savePlanForObservedLogin falls back to saving into the current page group", () => {
|
||||
const plan = background.savePlanForObservedLogin({
|
||||
username: "linuscaldwell",
|
||||
password: "yellow-chip",
|
||||
url: "https://vault.example.invalid/login"
|
||||
}, [
|
||||
{
|
||||
id: "vault-console",
|
||||
title: "Vault Console",
|
||||
username: "dannyocean",
|
||||
url: "vault.example.invalid",
|
||||
path: ["Crew", "Internet"]
|
||||
}
|
||||
]);
|
||||
|
||||
assert.deepEqual(plan, {
|
||||
mode: "save",
|
||||
entryId: "",
|
||||
title: "vault.example.invalid",
|
||||
path: ["Crew", "Internet"],
|
||||
username: "linuscaldwell",
|
||||
password: "yellow-chip",
|
||||
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"]);
|
||||
});
|
||||
|
||||
+248
-25
@@ -36,21 +36,138 @@ function normalizeRole(rawRole) {
|
||||
switch (String(rawRole || "").trim().toLowerCase()) {
|
||||
case "password":
|
||||
return "password";
|
||||
default:
|
||||
case "username":
|
||||
return "username";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function lowerJoined(values) {
|
||||
return values
|
||||
.map((value) => String(value || "").trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function fieldHintText(input) {
|
||||
if (!input || typeof input !== "object") {
|
||||
return "";
|
||||
}
|
||||
const labels = input.labels ? Array.from(input.labels).map((label) => label.textContent || "") : [];
|
||||
return lowerJoined([
|
||||
input.getAttribute?.("type"),
|
||||
input.getAttribute?.("name"),
|
||||
input.getAttribute?.("id"),
|
||||
input.autocomplete,
|
||||
input.getAttribute?.("autocomplete"),
|
||||
input.getAttribute?.("placeholder"),
|
||||
input.getAttribute?.("aria-label"),
|
||||
...labels
|
||||
]);
|
||||
}
|
||||
|
||||
function textLikeInputType(type) {
|
||||
switch (String(type || "").toLowerCase()) {
|
||||
case "":
|
||||
case "text":
|
||||
case "email":
|
||||
case "tel":
|
||||
case "number":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hintMatches(text, patterns) {
|
||||
return patterns.some((pattern) => pattern.test(text));
|
||||
}
|
||||
|
||||
function scopeHintText(scope) {
|
||||
const isDocumentScope = typeof document !== "undefined" && scope === document;
|
||||
if ((!scope || typeof scope !== "object") || (!isDocumentScope && typeof scope.querySelectorAll !== "function" && typeof scope.getAttribute !== "function")) {
|
||||
return "";
|
||||
}
|
||||
const attrText = isDocumentScope ? "" : lowerJoined([
|
||||
scope.getAttribute?.("id"),
|
||||
scope.getAttribute?.("name"),
|
||||
scope.getAttribute?.("class"),
|
||||
scope.getAttribute?.("action"),
|
||||
scope.getAttribute?.("aria-label")
|
||||
]);
|
||||
const headingText = lowerJoined(Array.from(scope.querySelectorAll?.("button, h1, h2, h3, h4, legend, label, [role='button']") || [])
|
||||
.slice(0, 8)
|
||||
.map((element) => element.textContent || ""));
|
||||
return lowerJoined([attrText, headingText]);
|
||||
}
|
||||
|
||||
function hasAuthFlowSignals(usernameInput, scope) {
|
||||
if (usernameInput) {
|
||||
return true;
|
||||
}
|
||||
return hintMatches(scopeHintText(scope), authScopePatterns);
|
||||
}
|
||||
|
||||
const usernameHintPatterns = [
|
||||
/\buser(name|id)?\b/,
|
||||
/\blog[\s_-]?in\b/,
|
||||
/\bsign[\s_-]?in\b/,
|
||||
/\bemail\b/,
|
||||
/\be-mail\b/,
|
||||
/\baccount\b/,
|
||||
/\bmember\b/,
|
||||
/\bidentifier\b/
|
||||
];
|
||||
|
||||
const nonLoginHintPatterns = [
|
||||
/\bsearch\b/,
|
||||
/\bquery\b/,
|
||||
/\bfilter\b/,
|
||||
/\bcomment\b/,
|
||||
/\bmessage\b/,
|
||||
/\bcontact\b/,
|
||||
/\bcity\b/,
|
||||
/\bstate\b/,
|
||||
/\bpostal\b/,
|
||||
/\bzip\b/,
|
||||
/\bcoupon\b/,
|
||||
/\bpromo\b/,
|
||||
/\bnewsletter\b/,
|
||||
/\bsubscribe\b/
|
||||
];
|
||||
|
||||
const authScopePatterns = [
|
||||
/\blog[\s_-]?in\b/,
|
||||
/\bsign[\s_-]?in\b/,
|
||||
/\bauth\b/,
|
||||
/\bpassword\b/,
|
||||
/\bpasscode\b/,
|
||||
/\b2fa\b/,
|
||||
/\btwo[\s-]?factor\b/,
|
||||
/\bverify\b/,
|
||||
/\baccount\b/
|
||||
];
|
||||
|
||||
function describeFieldRole(input) {
|
||||
const type = String(input?.getAttribute?.("type") || "").toLowerCase();
|
||||
if (type === "password") {
|
||||
return "password";
|
||||
}
|
||||
const autocomplete = String(input?.autocomplete || "").toLowerCase();
|
||||
if (autocomplete.includes("username") || autocomplete.includes("email")) {
|
||||
if (!textLikeInputType(type)) {
|
||||
return "";
|
||||
}
|
||||
const hints = fieldHintText(input);
|
||||
if (!hints) {
|
||||
return "";
|
||||
}
|
||||
if (hintMatches(hints, nonLoginHintPatterns)) {
|
||||
return "";
|
||||
}
|
||||
if (hintMatches(hints, usernameHintPatterns)) {
|
||||
return "username";
|
||||
}
|
||||
return "username";
|
||||
return "";
|
||||
}
|
||||
|
||||
function isUsernameCandidate(input) {
|
||||
@@ -102,6 +219,40 @@ function firstVisibleUsername(scope) {
|
||||
return visibleInputs(scope).find(isUsernameCandidate) || null;
|
||||
}
|
||||
|
||||
function authFlowCandidate(anchorInput) {
|
||||
const scope = (typeof HTMLFormElement !== "undefined" && anchorInput?.form instanceof HTMLFormElement ? anchorInput.form : document);
|
||||
const scopeInputs = resolveFormInputs(anchorInput);
|
||||
const passwordInput = scopeInputs.find(isPasswordCandidate) || null;
|
||||
if (!passwordInput) {
|
||||
return null;
|
||||
}
|
||||
const associated = associatedFieldsForAnchor(anchorInput || passwordInput);
|
||||
if (!hasAuthFlowSignals(associated.usernameInput, scope)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
usernameInput: associated.usernameInput,
|
||||
passwordInput,
|
||||
anchorInput: anchorInput || passwordInput,
|
||||
scope
|
||||
};
|
||||
}
|
||||
|
||||
function loginCandidates() {
|
||||
const candidates = [];
|
||||
for (const passwordInput of visibleInputs(document).filter(isPasswordCandidate)) {
|
||||
const candidate = authFlowCandidate(passwordInput);
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
if (candidates.some((existing) => existing.passwordInput === candidate.passwordInput)) {
|
||||
continue;
|
||||
}
|
||||
candidates.push(candidate);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function associatedFieldsForAnchor(anchorInput) {
|
||||
const scopeInputs = resolveFormInputs(anchorInput);
|
||||
const passwordInput = scopeInputs.find(isPasswordCandidate) || firstVisiblePassword(document);
|
||||
@@ -121,11 +272,11 @@ function associatedFieldsForAnchor(anchorInput) {
|
||||
}
|
||||
|
||||
function buildFieldDescriptor(input, role) {
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
if (typeof HTMLInputElement === "undefined" || !(input instanceof HTMLInputElement)) {
|
||||
return null;
|
||||
}
|
||||
const normalizedRole = normalizeRole(role || describeFieldRole(input));
|
||||
const form = input.form instanceof HTMLFormElement ? input.form : null;
|
||||
const form = typeof HTMLFormElement !== "undefined" && input.form instanceof HTMLFormElement ? input.form : null;
|
||||
const scope = form || document;
|
||||
const inputs = visibleInputs(scope);
|
||||
const fieldIndex = inputs.indexOf(input);
|
||||
@@ -145,6 +296,9 @@ function resolveFieldDescriptor(descriptor) {
|
||||
return null;
|
||||
}
|
||||
const normalizedRole = normalizeRole(descriptor.role);
|
||||
if (!normalizedRole) {
|
||||
return null;
|
||||
}
|
||||
const forms = Array.from(document.forms || []);
|
||||
const form = Number.isInteger(descriptor.formIndex) && descriptor.formIndex >= 0 ? forms[descriptor.formIndex] || null : null;
|
||||
const scope = form || document;
|
||||
@@ -200,20 +354,22 @@ function chooseFillTargets(targetDescriptor) {
|
||||
function scanLoginFields() {
|
||||
const activeElement = document.activeElement instanceof HTMLInputElement ? document.activeElement : null;
|
||||
const activeUsable = activeElement && isVisibleInput(activeElement) ? activeElement : null;
|
||||
const targets = chooseFillTargets(buildFieldDescriptor(activeUsable, describeFieldRole(activeUsable)));
|
||||
const anchorInput = activeUsable || targets.passwordInput || targets.usernameInput;
|
||||
const focusTarget = buildFieldDescriptor(anchorInput, describeFieldRole(anchorInput));
|
||||
const allVisible = visibleInputs(document);
|
||||
const roles = allVisible
|
||||
.filter((input) => isUsernameCandidate(input) || isPasswordCandidate(input))
|
||||
.map((input) => {
|
||||
const descriptor = buildFieldDescriptor(input, describeFieldRole(input));
|
||||
return `${descriptor.formIndex}:${descriptor.fieldIndex}:${descriptor.role}`;
|
||||
});
|
||||
const explicitRole = describeFieldRole(activeUsable);
|
||||
const activeTargets = activeUsable ? authFlowCandidate(activeUsable) : null;
|
||||
const candidates = loginCandidates();
|
||||
const chosen = activeTargets || candidates[0] || null;
|
||||
const anchorInput = activeUsable || chosen?.passwordInput || chosen?.usernameInput || null;
|
||||
const focusRole = explicitRole || describeFieldRole(anchorInput);
|
||||
const focusTarget = anchorInput ? buildFieldDescriptor(anchorInput, focusRole) : null;
|
||||
const roles = candidates.map((candidate) => {
|
||||
const passwordDescriptor = buildFieldDescriptor(candidate.passwordInput, "password");
|
||||
const usernameDescriptor = candidate.usernameInput ? buildFieldDescriptor(candidate.usernameInput, "username") : null;
|
||||
return `${passwordDescriptor.formIndex}:${passwordDescriptor.fieldIndex}:password:${usernameDescriptor ? `${usernameDescriptor.formIndex}:${usernameDescriptor.fieldIndex}` : "-"}`;
|
||||
});
|
||||
return {
|
||||
pageHasLoginForm: Boolean(targets.usernameInput || targets.passwordInput),
|
||||
usernameInput: targets.usernameInput,
|
||||
passwordInput: targets.passwordInput,
|
||||
pageHasLoginForm: Boolean(chosen),
|
||||
usernameInput: chosen?.usernameInput || null,
|
||||
passwordInput: chosen?.passwordInput || null,
|
||||
anchorInput,
|
||||
focusTarget,
|
||||
signature: roles.join("|")
|
||||
@@ -240,6 +396,22 @@ function fillCredential(credential, targetDescriptor) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function submittedCredential(candidate, rawURL) {
|
||||
if (!candidate?.passwordInput) {
|
||||
return null;
|
||||
}
|
||||
const password = String(candidate.passwordInput.value || "").trim();
|
||||
if (!password) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
title: domainLabel(rawURL),
|
||||
username: String(candidate.usernameInput?.value || "").trim(),
|
||||
password,
|
||||
url: String(rawURL || "").trim()
|
||||
};
|
||||
}
|
||||
|
||||
function domainLabel(rawURL) {
|
||||
try {
|
||||
return new URL(rawURL).host || "";
|
||||
@@ -265,14 +437,15 @@ function inlineMatchSummary(match) {
|
||||
return parts.join(" · ") || "No username";
|
||||
}
|
||||
|
||||
function shouldShowInlineOverlay(state, hasTarget, suppressed) {
|
||||
if (suppressed || !hasTarget) {
|
||||
function shouldShowInlineOverlay(state, hasTarget, suppressed, idleHidden) {
|
||||
if (suppressed || idleHidden || !hasTarget) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(
|
||||
state?.pageHasLoginForm &&
|
||||
(
|
||||
state?.pendingFill ||
|
||||
(state?.configured && state?.success && state?.status?.locked) ||
|
||||
(state?.configured && state?.success && !state?.status?.locked && Array.isArray(state?.matches) && state.matches.length > 0)
|
||||
)
|
||||
);
|
||||
@@ -286,7 +459,12 @@ const contentTestExports = {
|
||||
chooseFillTargets,
|
||||
inlineMatchSummary,
|
||||
domainLabel,
|
||||
shouldShowInlineOverlay
|
||||
shouldShowInlineOverlay,
|
||||
fieldHintText,
|
||||
scopeHintText,
|
||||
hasAuthFlowSignals,
|
||||
authFlowCandidate,
|
||||
submittedCredential
|
||||
};
|
||||
|
||||
if (isNodeTestEnv) {
|
||||
@@ -303,7 +481,9 @@ if (isNodeTestEnv) {
|
||||
};
|
||||
let chooserOpen = false;
|
||||
let inlineSuppressed = false;
|
||||
let inlineIdleHidden = false;
|
||||
let refreshTimer = null;
|
||||
let idleHideTimer = null;
|
||||
let lastReportedSignature = "";
|
||||
let lastReportedTarget = "";
|
||||
|
||||
@@ -464,6 +644,24 @@ if (isNodeTestEnv) {
|
||||
dock.dataset.open = "false";
|
||||
}
|
||||
|
||||
function clearIdleHideTimer() {
|
||||
if (idleHideTimer !== null) {
|
||||
clearTimeout(idleHideTimer);
|
||||
idleHideTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshInlineLifetime(shouldShow) {
|
||||
clearIdleHideTimer();
|
||||
if (!shouldShow || chooserOpen || pageState.pendingFill) {
|
||||
return;
|
||||
}
|
||||
idleHideTimer = window.setTimeout(() => {
|
||||
inlineIdleHidden = true;
|
||||
hideDock();
|
||||
}, 15000);
|
||||
}
|
||||
|
||||
function positionDock() {
|
||||
const anchor = currentTarget();
|
||||
if (!anchor || dock.style.display === "none") {
|
||||
@@ -537,19 +735,23 @@ if (isNodeTestEnv) {
|
||||
|
||||
function renderInlineState() {
|
||||
const target = currentTarget();
|
||||
const shouldShow = shouldShowInlineOverlay(pageState, Boolean(target), inlineSuppressed);
|
||||
const shouldShow = shouldShowInlineOverlay(pageState, Boolean(target), inlineSuppressed, inlineIdleHidden);
|
||||
|
||||
if (!shouldShow) {
|
||||
clearIdleHideTimer();
|
||||
hideDock();
|
||||
return;
|
||||
}
|
||||
|
||||
ensureRootMounted();
|
||||
dock.style.display = "block";
|
||||
trigger.dataset.tone = pageState.pendingFill ? "warning" : (pageState.error ? "error" : "ready");
|
||||
trigger.dataset.tone = pageState.pendingFill || pageState.status?.locked ? "warning" : (pageState.error ? "error" : "ready");
|
||||
if (pageState.pendingFill) {
|
||||
meta.textContent = "Approval needed in KeePassGO";
|
||||
panelCopy.textContent = pageState.pendingMessage || "Approve or deny the fill request in KeePassGO.";
|
||||
} else if (pageState.status?.locked) {
|
||||
meta.textContent = "Unlock KeePassGO";
|
||||
panelCopy.textContent = "Unlock KeePassGO to turn this field back into live login suggestions.";
|
||||
} else {
|
||||
const count = Array.isArray(pageState.matches) ? pageState.matches.length : 0;
|
||||
meta.textContent = count === 1 ? "1 login ready" : `${count} logins ready`;
|
||||
@@ -558,17 +760,21 @@ if (isNodeTestEnv) {
|
||||
dock.dataset.open = chooserOpen ? "true" : "false";
|
||||
renderMatches();
|
||||
positionDock();
|
||||
refreshInlineLifetime(shouldShow);
|
||||
}
|
||||
|
||||
function reportFieldState(force) {
|
||||
const scan = scanLoginFields();
|
||||
const nextTarget = JSON.stringify(scan.focusTarget || null);
|
||||
if (scan.signature !== lastReportedSignature || nextTarget !== lastReportedTarget) {
|
||||
inlineIdleHidden = false;
|
||||
}
|
||||
pageState = {
|
||||
...pageState,
|
||||
pageHasLoginForm: scan.pageHasLoginForm,
|
||||
focusTarget: scan.focusTarget
|
||||
};
|
||||
renderInlineState();
|
||||
const nextTarget = JSON.stringify(scan.focusTarget || null);
|
||||
if (!force && scan.signature === lastReportedSignature && nextTarget === lastReportedTarget) {
|
||||
return;
|
||||
}
|
||||
@@ -616,6 +822,23 @@ if (isNodeTestEnv) {
|
||||
scheduleRefresh(false);
|
||||
}, true);
|
||||
|
||||
document.addEventListener("submit", (event) => {
|
||||
const form = event.target instanceof HTMLFormElement ? event.target : null;
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
const passwordInput = visibleInputs(form).find(isPasswordCandidate) || null;
|
||||
const candidate = passwordInput ? authFlowCandidate(passwordInput) : null;
|
||||
const observed = submittedCredential(candidate, window.location.href);
|
||||
if (!observed) {
|
||||
return;
|
||||
}
|
||||
void runtimeSend({
|
||||
type: "keepassgo-observed-login",
|
||||
observed
|
||||
}).catch(() => null);
|
||||
}, true);
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!root.contains(event.target)) {
|
||||
chooserOpen = false;
|
||||
|
||||
@@ -18,6 +18,68 @@ test("domainLabel tolerates invalid URLs", () => {
|
||||
assert.equal(content.domainLabel("not-a-url"), "");
|
||||
});
|
||||
|
||||
test("describeFieldRole only treats explicit account fields as usernames", () => {
|
||||
const loginField = {
|
||||
autocomplete: "username",
|
||||
labels: [],
|
||||
getAttribute(name) {
|
||||
const attrs = {
|
||||
type: "email",
|
||||
id: "crew-email",
|
||||
name: "email",
|
||||
placeholder: "Email address",
|
||||
"aria-label": "Email address"
|
||||
};
|
||||
return attrs[name] || "";
|
||||
}
|
||||
};
|
||||
const searchField = {
|
||||
autocomplete: "",
|
||||
labels: [],
|
||||
getAttribute(name) {
|
||||
const attrs = {
|
||||
type: "text",
|
||||
id: "site-search",
|
||||
name: "query",
|
||||
placeholder: "Search casino news",
|
||||
"aria-label": "Search"
|
||||
};
|
||||
return attrs[name] || "";
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal(content.describeFieldRole(loginField), "username");
|
||||
assert.equal(content.describeFieldRole(searchField), "");
|
||||
});
|
||||
|
||||
test("hasAuthFlowSignals rejects generic password scopes and accepts sign-in scopes", () => {
|
||||
const genericScope = {
|
||||
getAttribute() {
|
||||
return "";
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [{ textContent: "Confirm shipment" }];
|
||||
}
|
||||
};
|
||||
const signInScope = {
|
||||
getAttribute(name) {
|
||||
const attrs = {
|
||||
id: "signin-panel",
|
||||
name: "signin",
|
||||
action: "/session"
|
||||
};
|
||||
return attrs[name] || "";
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [{ textContent: "Sign in to the Bellagio vault" }];
|
||||
}
|
||||
};
|
||||
|
||||
assert.equal(content.hasAuthFlowSignals(null, genericScope), false);
|
||||
assert.equal(content.hasAuthFlowSignals(null, signInScope), true);
|
||||
assert.equal(content.hasAuthFlowSignals({ id: "danny-ocean" }, genericScope), true);
|
||||
});
|
||||
|
||||
test("shouldShowInlineOverlay hides the page overlay after it is suppressed", () => {
|
||||
const state = {
|
||||
pageHasLoginForm: true,
|
||||
@@ -29,5 +91,46 @@ test("shouldShowInlineOverlay hides the page overlay after it is suppressed", ()
|
||||
};
|
||||
|
||||
assert.equal(content.shouldShowInlineOverlay(state, true, false), true);
|
||||
assert.equal(content.shouldShowInlineOverlay(state, true, true), false);
|
||||
assert.equal(content.shouldShowInlineOverlay(state, true, true, false), false);
|
||||
});
|
||||
|
||||
test("shouldShowInlineOverlay stays visible for locked login pages", () => {
|
||||
const state = {
|
||||
pageHasLoginForm: true,
|
||||
configured: true,
|
||||
success: true,
|
||||
status: { locked: true },
|
||||
matches: [],
|
||||
pendingFill: false
|
||||
};
|
||||
|
||||
assert.equal(content.shouldShowInlineOverlay(state, true, false, false), true);
|
||||
});
|
||||
|
||||
test("shouldShowInlineOverlay hides the page overlay after idle expiry", () => {
|
||||
const state = {
|
||||
pageHasLoginForm: true,
|
||||
configured: true,
|
||||
success: true,
|
||||
status: { locked: false },
|
||||
matches: [{ id: "rusty-ryan" }],
|
||||
pendingFill: false
|
||||
};
|
||||
|
||||
assert.equal(content.shouldShowInlineOverlay(state, true, false, false), true);
|
||||
assert.equal(content.shouldShowInlineOverlay(state, true, false, true), false);
|
||||
});
|
||||
|
||||
test("submittedCredential captures the pending browser save payload from a login candidate", () => {
|
||||
const candidate = {
|
||||
usernameInput: { value: "linuscaldwell" },
|
||||
passwordInput: { value: "yellow-chip" }
|
||||
};
|
||||
|
||||
assert.deepEqual(content.submittedCredential(candidate, "https://bellagio.example.invalid/login"), {
|
||||
title: "bellagio.example.invalid",
|
||||
username: "linuscaldwell",
|
||||
password: "yellow-chip",
|
||||
url: "https://bellagio.example.invalid/login"
|
||||
});
|
||||
});
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
@@ -3,6 +3,13 @@
|
||||
"name": "KeePassGO Browser",
|
||||
"version": "0.1.0",
|
||||
"description": "Fill credentials from KeePassGO on sign-in pages.",
|
||||
"icons": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png",
|
||||
"48": "icons/icon-48.png",
|
||||
"96": "icons/icon-96.png",
|
||||
"128": "icons/icon-128.png"
|
||||
},
|
||||
"permissions": [
|
||||
"activeTab",
|
||||
"nativeMessaging",
|
||||
@@ -16,6 +23,10 @@
|
||||
},
|
||||
"browser_action": {
|
||||
"default_title": "KeePassGO Browser",
|
||||
"default_icon": {
|
||||
"16": "icons/icon-16.png",
|
||||
"32": "icons/icon-32.png"
|
||||
},
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"options_ui": {
|
||||
@@ -31,7 +42,14 @@
|
||||
],
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "browser@keepassgo.com"
|
||||
"id": "browser@keepassgo.com",
|
||||
"data_collection_permissions": {
|
||||
"required": ["authenticationInfo", "websiteActivity"]
|
||||
},
|
||||
"strict_min_version": "140.0"
|
||||
},
|
||||
"gecko_android": {
|
||||
"strict_min_version": "142.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
<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>
|
||||
</fieldset>
|
||||
<div class="actions">
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ 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);
|
||||
}
|
||||
|
||||
async function saveSettings(event) {
|
||||
@@ -33,7 +34,8 @@ 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
|
||||
}
|
||||
});
|
||||
if (!response?.success) {
|
||||
|
||||
@@ -20,10 +20,25 @@
|
||||
<p id="status-message" class="subtle">Checking KeePassGO.</p>
|
||||
</section>
|
||||
<p id="page-hint" class="inline-hint subtle">Loading page state.</p>
|
||||
<section id="save-card" class="save-card" hidden>
|
||||
<div>
|
||||
<h2>Save Submitted Login</h2>
|
||||
<p id="save-message" class="subtle">KeePassGO can save this login.</p>
|
||||
</div>
|
||||
<button id="save-action" type="button">Save Login</button>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Matches</h2>
|
||||
<div id="matches" class="match-list"></div>
|
||||
</section>
|
||||
<section class="search-section">
|
||||
<h2>Search Vault</h2>
|
||||
<form id="search-form" class="search-form">
|
||||
<input id="search-query" type="search" placeholder="Search entries" autocomplete="off">
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<div id="search-results" class="match-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
|
||||
+143
-17
@@ -43,21 +43,25 @@ function matchSubtitle(match) {
|
||||
return parts.join(" · ") || "No username";
|
||||
}
|
||||
|
||||
function renderMatches(state) {
|
||||
const root = document.getElementById("matches");
|
||||
function saveCardLabel(pendingSave) {
|
||||
return pendingSave?.mode === "update"
|
||||
? `Update ${pendingSave.title || "Login"}`
|
||||
: "Save Login";
|
||||
}
|
||||
|
||||
function renderMatchList(root, matches, options = {}) {
|
||||
const targetTabID = popupTabID();
|
||||
const emptyMessage = options.emptyMessage || "No matching entries.";
|
||||
root.textContent = "";
|
||||
if (!Array.isArray(state.matches) || state.matches.length === 0) {
|
||||
if (!Array.isArray(matches) || matches.length === 0) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "subtle";
|
||||
empty.textContent = state.pageHasLoginForm
|
||||
? "No matching entries for this page."
|
||||
: "No login fields detected on this page.";
|
||||
empty.textContent = emptyMessage;
|
||||
root.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const match of state.matches) {
|
||||
for (const match of matches) {
|
||||
const row = document.createElement("button");
|
||||
row.type = "button";
|
||||
row.className = "match-row";
|
||||
@@ -77,19 +81,23 @@ function renderMatches(state) {
|
||||
row.appendChild(quality);
|
||||
row.addEventListener("click", async () => {
|
||||
row.disabled = true;
|
||||
setStatus("Approval may be required", "KeePassGO will prompt if this token needs approval before fill.", "warning");
|
||||
try {
|
||||
const result = await runtimeSend({
|
||||
type: "keepassgo-fill-entry",
|
||||
entryId: match.id,
|
||||
tabId: targetTabID
|
||||
});
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || "Fill failed.");
|
||||
if (typeof options.onSelect === "function") {
|
||||
await options.onSelect(match, targetTabID);
|
||||
} else {
|
||||
setStatus("Approval may be required", "KeePassGO will prompt if this token needs approval before fill.", "warning");
|
||||
const result = await runtimeSend({
|
||||
type: "keepassgo-fill-entry",
|
||||
entryId: match.id,
|
||||
tabId: targetTabID
|
||||
});
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || "Fill failed.");
|
||||
}
|
||||
setStatus("Filled", `${match.title} was sent to the current page.`, "ready");
|
||||
}
|
||||
setStatus("Filled", `${match.title} was sent to the current page.`, "ready");
|
||||
} catch (error) {
|
||||
setStatus("Fill failed", error instanceof Error ? error.message : String(error), "error");
|
||||
setStatus(options.onSelect ? "Save failed" : "Fill failed", error instanceof Error ? error.message : String(error), "error");
|
||||
} finally {
|
||||
row.disabled = false;
|
||||
}
|
||||
@@ -98,6 +106,51 @@ function renderMatches(state) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderMatches(state) {
|
||||
const emptyMessage = state.pageHasLoginForm
|
||||
? "No matching entries for this page."
|
||||
: "No login fields detected on this page.";
|
||||
const root = document.getElementById("matches");
|
||||
if (state.pendingSave) {
|
||||
renderMatchList(root, state.matches, {
|
||||
emptyMessage,
|
||||
onSelect: async (match, targetTabID) => {
|
||||
const result = await runtimeSend({
|
||||
type: "keepassgo-save-login",
|
||||
tabId: targetTabID,
|
||||
selectedMatch: {
|
||||
id: match.id,
|
||||
title: match.title,
|
||||
path: Array.isArray(match.path) ? match.path : []
|
||||
}
|
||||
});
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || "Save failed.");
|
||||
}
|
||||
setStatus("Saved", `${state.pendingSave.title || "Login"} is now in KeePassGO.`, "ready");
|
||||
document.getElementById("save-card").hidden = true;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
renderMatchList(root, state.matches, { emptyMessage });
|
||||
}
|
||||
|
||||
function renderSearchResults(results, query) {
|
||||
const root = document.getElementById("search-results");
|
||||
if (!query) {
|
||||
root.textContent = "";
|
||||
const hint = document.createElement("p");
|
||||
hint.className = "subtle";
|
||||
hint.textContent = "Search all entries you can access with this token.";
|
||||
root.appendChild(hint);
|
||||
return;
|
||||
}
|
||||
renderMatchList(root, results, {
|
||||
emptyMessage: `No entries matched "${query}".`
|
||||
});
|
||||
}
|
||||
|
||||
function renderPageHint(state) {
|
||||
const hint = document.getElementById("page-hint");
|
||||
if (state.pendingFill) {
|
||||
@@ -115,6 +168,46 @@ function renderPageHint(state) {
|
||||
hint.textContent = "Open a sign-in page to see KeePassGO suggestions here.";
|
||||
}
|
||||
|
||||
function renderPendingSave(state) {
|
||||
const card = document.getElementById("save-card");
|
||||
const message = document.getElementById("save-message");
|
||||
const action = document.getElementById("save-action");
|
||||
const pendingSave = state.pendingSave;
|
||||
if (!pendingSave) {
|
||||
card.hidden = true;
|
||||
action.onclick = null;
|
||||
return;
|
||||
}
|
||||
card.hidden = false;
|
||||
action.textContent = saveCardLabel(pendingSave);
|
||||
if (pendingSave.mode === "update") {
|
||||
message.textContent = `KeePassGO can update ${pendingSave.title || "this login"} with the submitted password.`;
|
||||
} else if (Array.isArray(pendingSave.path) && pendingSave.path.length > 0) {
|
||||
message.textContent = `KeePassGO can save this login in ${pendingSave.path.join(" / ")}. Search the vault to choose a different group if needed.`;
|
||||
} else {
|
||||
message.textContent = "Search the vault below to choose a group for this submitted login.";
|
||||
}
|
||||
action.disabled = pendingSave.mode !== "update" && (!Array.isArray(pendingSave.path) || pendingSave.path.length === 0);
|
||||
action.onclick = async () => {
|
||||
action.disabled = true;
|
||||
try {
|
||||
const result = await runtimeSend({
|
||||
type: "keepassgo-save-login",
|
||||
tabId: popupTabID()
|
||||
});
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || "Save failed.");
|
||||
}
|
||||
setStatus("Saved", `${pendingSave.title || "Login"} is now in KeePassGO.`, "ready");
|
||||
card.hidden = true;
|
||||
} catch (error) {
|
||||
setStatus("Save failed", error instanceof Error ? error.message : String(error), "error");
|
||||
} finally {
|
||||
action.disabled = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function popupTabID() {
|
||||
const rawValue = new URLSearchParams(window.location.search).get("tabId");
|
||||
if (rawValue === null) {
|
||||
@@ -124,8 +217,38 @@ function popupTabID() {
|
||||
return Number.isInteger(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
async function searchVault(event) {
|
||||
event.preventDefault();
|
||||
const query = document.getElementById("search-query").value.trim();
|
||||
const resultsRoot = document.getElementById("search-results");
|
||||
if (!query) {
|
||||
renderSearchResults([], "");
|
||||
return;
|
||||
}
|
||||
resultsRoot.textContent = "";
|
||||
const loading = document.createElement("p");
|
||||
loading.className = "subtle";
|
||||
loading.textContent = "Searching KeePassGO…";
|
||||
resultsRoot.appendChild(loading);
|
||||
try {
|
||||
const response = await runtimeSend({
|
||||
type: "keepassgo-search-logins",
|
||||
query
|
||||
});
|
||||
if (!response?.success) {
|
||||
throw new Error(response?.error || "Search failed.");
|
||||
}
|
||||
renderSearchResults(Array.isArray(response.results) ? response.results : [], query);
|
||||
} catch (error) {
|
||||
renderSearchResults([], query);
|
||||
setStatus("Search failed", error instanceof Error ? error.message : String(error), "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
document.getElementById("search-form").addEventListener("submit", searchVault);
|
||||
renderSearchResults([], "");
|
||||
const state = await runtimeSend({
|
||||
type: "keepassgo-popup-state",
|
||||
force: true,
|
||||
@@ -133,6 +256,7 @@ async function main() {
|
||||
});
|
||||
document.getElementById("page-host").textContent = hostFromURL(state.pageUrl || "");
|
||||
renderPageHint(state);
|
||||
renderPendingSave(state);
|
||||
|
||||
if (!state.configured) {
|
||||
setStatus("Configure access", state.error || "Set the API token in extension settings.", "warning");
|
||||
@@ -158,6 +282,8 @@ async function main() {
|
||||
const count = Array.isArray(state.matches) ? state.matches.length : 0;
|
||||
if (!state.pageHasLoginForm) {
|
||||
setStatus("Ready", "KeePassGO is connected. Open a login form to check for matches.", "ready");
|
||||
} else if (state.pendingSave) {
|
||||
setStatus("Save submitted login", state.pendingSave.mode === "update" ? `Update ${state.pendingSave.title || "this login"} or pick a different target below.` : "Save this submitted login or search below to choose a target entry.", "ready");
|
||||
} else if (count === 0) {
|
||||
setStatus("Checked this page", "KeePassGO did not find a matching login for this form.", "ready");
|
||||
} else {
|
||||
|
||||
@@ -96,6 +96,29 @@ h2 {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.save-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
margin: 0 0 16px;
|
||||
border: 1px solid #c5dccf;
|
||||
border-radius: 12px;
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.match-row,
|
||||
button,
|
||||
.link-button {
|
||||
@@ -164,6 +187,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;
|
||||
|
||||
+4
-4
@@ -73,16 +73,16 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("ANDROID_NDK_ROOT must point to an Android NDK install")
|
||||
}
|
||||
if !isExecutable(filepath.Join(c.SDKRoot, "cmdline-tools", "latest", "bin", "sdkmanager")) {
|
||||
return fmt.Errorf("Android SDK cmdline-tools are missing")
|
||||
return fmt.Errorf("android SDK cmdline-tools are missing")
|
||||
}
|
||||
if !isDir(filepath.Join(c.SDKRoot, "platforms", "android-"+c.TargetSDK)) {
|
||||
return fmt.Errorf("Android platform android-%s is missing", c.TargetSDK)
|
||||
return fmt.Errorf("android platform android-%s is missing", c.TargetSDK)
|
||||
}
|
||||
if !isDir(filepath.Join(c.SDKRoot, "build-tools")) {
|
||||
return fmt.Errorf("Android build-tools are missing")
|
||||
return fmt.Errorf("android build-tools are missing")
|
||||
}
|
||||
if !isFile(c.IconPath) {
|
||||
return fmt.Errorf("Android icon asset is missing: %s", c.IconPath)
|
||||
return fmt.Errorf("android icon asset is missing: %s", c.IconPath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# Android Autofill
|
||||
|
||||
## App Target Matching
|
||||
|
||||
User story:
|
||||
|
||||
- When an entry carries an Android-specific target such as
|
||||
`androidapp://com.blinknetwork.mobile2`, KeePassGO should treat that as a
|
||||
first-class autofill target on Android.
|
||||
- If an exact app target exists, Android autofill should resolve that entry
|
||||
directly instead of falling back to a generic chooser for the whole cache.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- `AndroidApp*` custom fields exported into the autofill cache must match the
|
||||
Android package target used by the autofill and accessibility services.
|
||||
- The Android-side matcher must normalize `androidapp://...` targets the same
|
||||
way the Go cache builder does.
|
||||
- The chooser path should still collapse to a single direct result when there
|
||||
is one exact app-target match.
|
||||
|
||||
## Accessibility Fallback
|
||||
|
||||
User story:
|
||||
|
||||
- When Android accessibility fallback is needed, KeePassGO should not be
|
||||
limited to Chrome-only URL bar parsing.
|
||||
- Apps with stable package identities should still be fillable when an entry
|
||||
carries a matching `AndroidApp*` target.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- Accessibility fallback derives its match target from the web domain when one
|
||||
is available.
|
||||
- If no web domain is available, accessibility fallback uses the active app
|
||||
package as `androidapp://<package>`.
|
||||
- The fallback path can therefore fill supported apps that never expose a
|
||||
browser-style URL bar.
|
||||
|
||||
## Share-Driven Lookup
|
||||
|
||||
User story:
|
||||
|
||||
- When Android shares a login URL or a text snippet containing a login URL into
|
||||
KeePassGO, the app should open into a credential lookup flow instead of only
|
||||
supporting shared `.kdbx` imports.
|
||||
- If the vault is already open, the shared target should immediately narrow the
|
||||
entries view.
|
||||
- If the vault is not open yet, the shared target should survive startup and
|
||||
apply as soon as the vault is unlocked.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- Android share intents can queue a pending lookup target in addition to shared
|
||||
vault file imports.
|
||||
- KeePassGO normalizes the shared value into a search query that users can
|
||||
immediately act on.
|
||||
- The pending lookup is consumed once and does not keep reappearing on later
|
||||
launches.
|
||||
|
||||
## Chooser Relevance
|
||||
|
||||
User story:
|
||||
|
||||
- When Android autofill cannot resolve a single direct match, KeePassGO should
|
||||
still keep the chooser focused on entries that are relevant to the current
|
||||
site or app.
|
||||
- The picker should not fall back to an alphabetized dump of unrelated vault
|
||||
entries when KeePassGO already knows the current host or package target.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- If multiple entries exactly match the current web host or Android app target,
|
||||
the chooser shows only those relevant entries.
|
||||
- If there are no exact matches but there are parent-host matches, the chooser
|
||||
shows only those related entries.
|
||||
- KeePassGO falls back to the full chooser list only when it has no related
|
||||
host or app-target candidates at all.
|
||||
@@ -93,6 +93,77 @@ Chromium / Chrome:
|
||||
- Username and password fields get an inline KeePassGO affordance that opens a candidate chooser anchored to the focused field and keeps fills scoped to that field's form when possible.
|
||||
- If a fill request needs user approval, the extension keeps the pending state visible in both the page affordance and the popup until KeePassGO resolves it, using the token-scoped pending-approval count from the local gRPC API.
|
||||
|
||||
## Search And Matching
|
||||
|
||||
User story:
|
||||
|
||||
- When a page has no obvious match, the popup still lets the user search the
|
||||
vault without leaving the browser.
|
||||
- Search results must stay scoped to what the current API token can actually
|
||||
access.
|
||||
- Browser matching must treat common KeePass data conventions as real browser
|
||||
targets, not just the primary `URL` field.
|
||||
- Users who prefer narrow suggestions can ask the extension to show only the
|
||||
strongest match quality returned by KeePassGO.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- The popup exposes a `Search Vault` field that queries KeePassGO directly.
|
||||
- Search results use the same fill path as page matches.
|
||||
- Search never leaks entries outside the token's authorized group scope.
|
||||
- A browser match can come from:
|
||||
- the primary `URL` field
|
||||
- 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 `Best match only`.
|
||||
- When `Best match only` is enabled, page suggestions and popup search results
|
||||
only show the strongest quality band returned by KeePassGO.
|
||||
|
||||
## Locked Vault Workflow
|
||||
|
||||
User story:
|
||||
|
||||
- When the current page has a login form but KeePassGO is locked, the browser
|
||||
must still make that state visible on the page and in the popup.
|
||||
- Unlocking KeePassGO should not require the user to reopen the popup multiple
|
||||
times or reload the page before the extension becomes usable again.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- The popup shows a locked-state message instead of silently falling back to
|
||||
"no matches."
|
||||
- The inline page affordance stays visible on login forms while KeePassGO is
|
||||
locked and tells the user to unlock the vault.
|
||||
- After the vault is unlocked, the extension rechecks the page automatically
|
||||
and turns the locked affordance back into live matches without requiring a
|
||||
page reload.
|
||||
|
||||
## Save And Update Workflow
|
||||
|
||||
User story:
|
||||
|
||||
- After the user submits a login form, the browser extension should help store
|
||||
that credential instead of forcing the user back into KeePassGO manually.
|
||||
- If KeePassGO already has a matching entry for that site and username, the
|
||||
popup should offer an update.
|
||||
- If the user is creating a new login, the popup should let the user save it
|
||||
into a relevant vault group without leaving the browser.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
- Submitted login forms queue a pending browser save/update state for the
|
||||
active tab.
|
||||
- The popup shows that pending save/update state prominently instead of hiding
|
||||
it behind page matches alone.
|
||||
- When KeePassGO finds an exact browser match for the submitted username and
|
||||
site, the popup offers an `Update` action for that entry.
|
||||
- When there is no exact entry match, the popup offers a `Save` action using a
|
||||
relevant group path from the current page matches or a user-selected search
|
||||
result.
|
||||
- The browser save/update action writes through KeePassGO's existing secure
|
||||
gRPC mutation API and stays scoped to the browser token's allowed groups.
|
||||
|
||||
For extension-side regression checks, run:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# GUI Test Plan
|
||||
|
||||
This document splits GUI validation into human-owned and agent-owned coverage.
|
||||
The intent is that, together, both passes exercise every reachable GUI area
|
||||
without relying on one side to judge both functional correctness and real
|
||||
usability.
|
||||
|
||||
## Scope
|
||||
|
||||
- Desktop GUI
|
||||
- Android GUI
|
||||
- Shared workflow surfaces:
|
||||
lifecycle, unlock, list/detail/editor, settings, sync, API tokens, API audit,
|
||||
templates, recycle bin, About, and platform-specific integrations
|
||||
|
||||
## Ownership Model
|
||||
|
||||
- Human testing covers visual correctness, ergonomics, discoverability, focus,
|
||||
scrolling, tap targets, keyboard feel, and platform integration behavior.
|
||||
- Agent testing covers deterministic automation:
|
||||
build, install, launch, focus, test suite, lint, and release-path validation.
|
||||
|
||||
## Android Plan
|
||||
|
||||
### Coverage Matrix
|
||||
|
||||
| Area | Human | Agent |
|
||||
|---|---|---|
|
||||
| App launch and first render | Verify real rendering, layout, readability, no black screen | Verify emulator online, app installed, app focused |
|
||||
| Lifecycle screen | Exercise create/open/open-remote forms visually | Cover lifecycle logic through Go tests and release APK build |
|
||||
| Unlock flow | Verify password/key-file affordances, focus, error messaging | Covered by automated tests |
|
||||
| Vault list and navigation | Verify scrolling, selection, section switching, phone layout reachability | Covered by automated tests for list state/search/section behavior |
|
||||
| Entry detail | Verify readability, action placement, tap targets | Covered by automated tests for state transitions and copy/generate actions |
|
||||
| Entry editor | Verify field editing usability and save affordance | Covered by automated tests for save/update behavior |
|
||||
| Template views | Verify template list/editor reachability and copy | Covered by automated tests |
|
||||
| Recycle bin | Verify deleted-entry browsing/search UX | Covered by automated tests |
|
||||
| Attachments UI | Verify list/add/remove affordances visually | Covered by automated tests for attachment summaries and save paths |
|
||||
| Search | Verify placeholder, live filtering feel, clear behavior | Covered by automated tests for search semantics |
|
||||
| Settings | Verify toggles and summaries are understandable | Covered by automated tests for persistence |
|
||||
| Remote sync setup/dialog | Verify sheet/dialog layout, field order, source/direction controls | Covered by automated tests and release build |
|
||||
| Saved remote binding UI | Verify summaries are understandable and discoverable | Covered by automated tests |
|
||||
| API tokens / API audit | Verify navigation and dense-detail readability | Covered by automated tests |
|
||||
| About / informational screens | Verify copy, scroll, layout | Covered by automated tests for search disable state and section state |
|
||||
| Android-only share/import/file picker | Verify system integration actually works | Agent only verifies package/build/focus, not picker UX |
|
||||
| Android autofill entry point | Verify Android mechanism appears and is usable | Agent only verifies app/package/focus |
|
||||
|
||||
### Human Steps
|
||||
|
||||
1. Start from the currently installed `org.julianfamily.keepassgo` app on the
|
||||
running emulator or device.
|
||||
2. Verify launch lands on a rendered screen, not a black frame, blank frame, or
|
||||
frozen frame.
|
||||
3. On the lifecycle screen, inspect every visible action:
|
||||
`Create vault`, `Open vault`, `Open remote vault`, unlock-related controls,
|
||||
recent vaults, and any sync summaries.
|
||||
4. Create a throwaway vault or open a demo vault and verify the full unlock path
|
||||
is visually clear.
|
||||
5. After unlock, visit each top-level section you can reach in the Android UI:
|
||||
vault/group list, entry detail, entry editor, templates, recycle bin,
|
||||
settings, API tokens, API audit, About.
|
||||
6. In the main vault UI, verify:
|
||||
group navigation, entry selection, search, back behavior, scrolling, and any
|
||||
phone-only toggles or drawers.
|
||||
7. Open an entry and exercise every visible entry action:
|
||||
copy username, copy password, copy URL, reveal/hide password, generate
|
||||
password, save.
|
||||
8. Exercise one template flow:
|
||||
open template list, inspect template detail/editor, save or cancel.
|
||||
9. Exercise recycle-bin browsing and search.
|
||||
10. Open settings and inspect every visible toggle or summary card. Change at
|
||||
least one benign toggle and verify it sticks after leaving and returning.
|
||||
11. Open the sync UI and inspect:
|
||||
remote setup, saved-binding summary, direction/source choices,
|
||||
confirm/cancel behavior, and text summaries.
|
||||
12. Exercise one Android-native integration:
|
||||
shared-vault import, file picker open, or current-vault share.
|
||||
13. If Android autofill is in scope for current testing, open a target app/site
|
||||
and confirm the Android autofill entry point is offered and usable.
|
||||
14. Record failures with:
|
||||
exact screen, exact control, expected behavior, actual behavior, and whether
|
||||
it is Android-only or likely shared with desktop.
|
||||
|
||||
### Agent Steps Executed
|
||||
|
||||
1. Verified emulator availability with `adb devices -l`.
|
||||
2. Verified emulator Android version with
|
||||
`adb shell getprop ro.build.version.release`.
|
||||
3. Verified app package installed with
|
||||
`adb shell pm list packages org.julianfamily.keepassgo`.
|
||||
4. Verified app focus with
|
||||
`adb shell dumpsys window | rg 'mCurrentFocus|mFocusedApp'`.
|
||||
5. Ran `go test ./...`.
|
||||
6. Ran `go tool golangci-lint run ./...`.
|
||||
7. Verified release APK path through release workflow `v0.6.0`:
|
||||
PR run `60` succeeded,
|
||||
post-merge `main` run `61` succeeded,
|
||||
tag run `62` succeeded.
|
||||
|
||||
### Agent Results
|
||||
|
||||
- Emulator present: yes
|
||||
- Android version: `15`
|
||||
- App installed: yes
|
||||
- App focused:
|
||||
`org.julianfamily.keepassgo/org.gioui.GioActivity`
|
||||
- APK release build path: passed in release CI
|
||||
- Remaining Android risk:
|
||||
real visual usability and system-integration behavior
|
||||
|
||||
## Desktop Plan
|
||||
|
||||
### Coverage Matrix
|
||||
|
||||
| Area | Human | Agent |
|
||||
|---|---|---|
|
||||
| App launch and window presentation | Verify startup polish, focus, resize behavior, DPI/readability | Verify desktop binaries built in release CI |
|
||||
| Lifecycle screen | Verify desktop layout density, affordance clarity, recent vault usability | Covered by automated tests |
|
||||
| Unlock flow | Verify keyboard-first behavior, focus order, errors, lock/unlock loop | Covered by automated tests plus human keyboard feel |
|
||||
| Group browser and list pane | Verify density, selection clarity, scrolling, split behavior | Covered by automated tests for state/search |
|
||||
| Entry detail pane | Verify copy actions, reveal/hide, path context, readability | Covered by automated tests |
|
||||
| Entry editor | Verify field order, keyboard flow, save/cancel usability | Covered by automated tests |
|
||||
| Templates | Verify templates section is reachable and understandable | Covered by automated tests |
|
||||
| Recycle bin | Verify browsing, searching, deletion or recovery UX | Covered by automated tests |
|
||||
| Search | Verify keyboard search flow, placeholder correctness, result context | Covered by automated tests |
|
||||
| Vault save/save-as/lock | Verify menus, buttons, and shortcuts feel correct | Covered by automated tests and release builds |
|
||||
| Settings | Verify desktop layout, summaries, toggles, persistence feel | Covered by automated tests |
|
||||
| Remote sync UI | Verify dialog layout, wording, discoverability, advanced options | Covered by automated tests |
|
||||
| API tokens | Verify dense-detail presentation and policy editor usability | Covered by automated tests |
|
||||
| API audit | Verify search/filter/readability | Covered by automated tests |
|
||||
| Browser-extension-adjacent desktop UX | Verify visible status/help text and extension workflow discoverability | Agent validated release/build path; human should judge usability |
|
||||
| About and docs entry points | Verify copy and layout | Covered partly by tests; human judges presentation |
|
||||
|
||||
### Human Steps
|
||||
|
||||
1. Launch KeePassGO on desktop from the shipped binary or normal desktop entry.
|
||||
2. Inspect the lifecycle/open screen at normal window size and at a narrower
|
||||
width.
|
||||
3. Exercise create, open, save-as, lock, unlock, and reopen on a throwaway
|
||||
vault or demo vault.
|
||||
4. Use keyboard-first operation for at least one complete pass:
|
||||
tab order, enter or escape expectations, search focus, unlock focus, and
|
||||
editor save flow.
|
||||
5. After unlocking, visit every reachable primary section:
|
||||
vault list, entry detail, editor, templates, recycle bin, settings,
|
||||
API tokens, API audit, About.
|
||||
6. In the vault browser, verify:
|
||||
nested groups, path context, scrolling, selection state, and search results
|
||||
with path context.
|
||||
7. Open an entry and exercise all visible actions:
|
||||
copy username, password, URL, reveal/hide password, password generation,
|
||||
and save.
|
||||
8. Exercise at least one mutation flow:
|
||||
create entry, edit entry, move/delete entry, view recycle bin, and recover
|
||||
if available.
|
||||
9. Open settings and inspect all visible summaries and toggles.
|
||||
10. Open remote sync UI and inspect every visible mode:
|
||||
setup, saved binding, advanced sync, source or direction choices.
|
||||
11. Open API Tokens and API Audit even if you do not issue a real token, just
|
||||
to assess navigation and readability.
|
||||
12. If you use browser integration, verify the desktop-side flow is still
|
||||
understandable from the product UI and extension behavior.
|
||||
13. Record failures with:
|
||||
screen, control, expected behavior, actual behavior, and whether it is
|
||||
presentation-only or functional.
|
||||
|
||||
### Agent Steps Executed
|
||||
|
||||
1. Verified clean repo state on `main`.
|
||||
2. Ran `go test ./...`.
|
||||
3. Ran `go tool golangci-lint run ./...`.
|
||||
4. Verified post-merge `main` CI run `61` succeeded.
|
||||
5. Verified release tag run `62` succeeded.
|
||||
6. Verified release `v0.6.0` published.
|
||||
7. Verified release artifacts include:
|
||||
`keepassgo-linux-amd64`,
|
||||
`keepassgo-windows-amd64.exe`,
|
||||
`keepassgo-windows-arm64.exe`,
|
||||
and `keepassgo.apk`.
|
||||
|
||||
### Agent Results
|
||||
|
||||
- Automated logic/state coverage: pass
|
||||
- Desktop build coverage: pass
|
||||
- Release publication: pass
|
||||
- Remaining desktop risk:
|
||||
interaction quality, keyboard feel, dense-layout readability, and workflow
|
||||
discovery
|
||||
|
||||
## Reporting Template
|
||||
|
||||
Use this format when reporting findings from the human pass:
|
||||
|
||||
```md
|
||||
## Android
|
||||
- Screen:
|
||||
- Action:
|
||||
- Expected:
|
||||
- Actual:
|
||||
- Severity:
|
||||
|
||||
## Desktop
|
||||
- Screen:
|
||||
- Action:
|
||||
- Expected:
|
||||
- Actual:
|
||||
- Severity:
|
||||
```
|
||||
@@ -2,8 +2,12 @@ module git.julianfamily.org/keepassgo
|
||||
|
||||
go 1.26
|
||||
|
||||
replace gioui.org => git.julianfamily.org/joejulian/gio-patched v0.9.1-0.20260416220049-9bfa6bc1c2dc
|
||||
|
||||
replace gioui.org/cmd => git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260417040456-1762d36ddecc
|
||||
|
||||
require (
|
||||
gioui.org v0.8.0
|
||||
gioui.org v0.9.0
|
||||
gioui.org/x v0.8.0
|
||||
github.com/atotto/clipboard v0.1.4
|
||||
github.com/tobischo/gokeepasslib/v3 v3.6.2
|
||||
@@ -15,54 +19,69 @@ require (
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
codeberg.org/chavacava/garif v0.2.0 // indirect
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
|
||||
dev.gaijin.team/go/golib v0.6.0 // indirect
|
||||
gioui.org/cmd v0.8.0 // indirect
|
||||
gioui.org/shader v1.0.8 // indirect
|
||||
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect
|
||||
github.com/4meepo/tagalign v1.4.2 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.3 // indirect
|
||||
github.com/Antonboom/errname v1.0.0 // indirect
|
||||
github.com/Antonboom/nilnil v1.0.1 // indirect
|
||||
github.com/Antonboom/testifylint v1.5.2 // indirect
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect
|
||||
github.com/Crocmagnon/fatcontext v0.7.1 // indirect
|
||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
|
||||
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.0 // indirect
|
||||
github.com/4meepo/tagalign v1.4.3 // indirect
|
||||
github.com/Abirdcfly/dupword v0.1.7 // indirect
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
|
||||
github.com/AlwxSin/noinlineerr v1.0.5 // indirect
|
||||
github.com/Antonboom/errname v1.1.1 // indirect
|
||||
github.com/Antonboom/nilnil v1.1.1 // indirect
|
||||
github.com/Antonboom/testifylint v1.6.4 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/akavel/rsrc v0.10.1 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.5 // indirect
|
||||
github.com/alexkohler/prealloc v1.0.0 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
github.com/alexkohler/prealloc v1.1.0 // indirect
|
||||
github.com/alfatraining/structtag v1.0.0 // indirect
|
||||
github.com/alingse/asasalint v0.0.11 // indirect
|
||||
github.com/alingse/nilnesserr v0.1.2 // indirect
|
||||
github.com/ashanbrown/forbidigo v1.6.0 // indirect
|
||||
github.com/ashanbrown/makezero v1.2.0 // indirect
|
||||
github.com/alingse/nilnesserr v0.2.0 // indirect
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect
|
||||
github.com/ashanbrown/makezero/v2 v2.1.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.3 // indirect
|
||||
github.com/blizzy78/varnamelen v0.8.0 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.5.0 // indirect
|
||||
github.com/breml/bidichk v0.3.2 // indirect
|
||||
github.com/breml/errchkjson v0.4.0 // indirect
|
||||
github.com/butuzov/ireturn v0.3.1 // indirect
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
|
||||
github.com/bombsimon/wsl/v5 v5.6.0 // indirect
|
||||
github.com/breml/bidichk v0.3.3 // indirect
|
||||
github.com/breml/errchkjson v0.4.1 // indirect
|
||||
github.com/butuzov/ireturn v0.4.0 // indirect
|
||||
github.com/butuzov/mirror v1.3.0 // indirect
|
||||
github.com/catenacyber/perfsprint v0.8.2 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
|
||||
github.com/catenacyber/perfsprint v0.10.1 // indirect
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charithe/durationcheck v0.0.10 // indirect
|
||||
github.com/chavacava/garif v0.1.0 // indirect
|
||||
github.com/ckaznocha/intrange v0.3.0 // indirect
|
||||
github.com/charithe/durationcheck v0.0.11 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/ckaznocha/intrange v0.3.1 // indirect
|
||||
github.com/curioswitch/go-reassign v0.3.0 // indirect
|
||||
github.com/daixiang0/gci v0.13.5 // indirect
|
||||
github.com/daixiang0/gci v0.13.7 // indirect
|
||||
github.com/dave/dst v0.27.3 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingaikin/go-header v0.5.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ettle/strcase v0.2.0 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.5 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.6 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.9 // indirect
|
||||
github.com/go-critic/go-critic v0.12.0 // indirect
|
||||
github.com/ghostiam/protogetter v0.3.20 // indirect
|
||||
github.com/go-critic/go-critic v0.14.3 // indirect
|
||||
github.com/go-text/typesetting v0.3.0 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||
@@ -71,149 +90,153 @@ require (
|
||||
github.com/go-toolsmith/astp v1.1.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.1.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/godbus/dbus/v5 v5.0.6 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/godoc-lint/godoc-lint v0.11.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golangci/asciicheck v0.5.0 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.0 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.1 // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
|
||||
github.com/golangci/golangci-lint v1.64.8 // indirect
|
||||
github.com/golangci/misspell v0.6.0 // indirect
|
||||
github.com/golangci/plugin-module-register v0.1.1 // indirect
|
||||
github.com/golangci/golangci-lint/v2 v2.11.4 // indirect
|
||||
github.com/golangci/golines v0.15.0 // indirect
|
||||
github.com/golangci/misspell v0.8.0 // indirect
|
||||
github.com/golangci/plugin-module-register v0.1.2 // indirect
|
||||
github.com/golangci/revgrep v0.8.0 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.1.0 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.5.0 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
|
||||
github.com/hashicorp/go-version v1.7.0 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jgautheron/goconst v1.7.1 // indirect
|
||||
github.com/jgautheron/goconst v1.8.2 // indirect
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.4 // indirect
|
||||
github.com/jjti/go-spancheck v0.6.5 // indirect
|
||||
github.com/julz/importas v0.2.0 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
|
||||
github.com/kisielk/errcheck v1.9.0 // indirect
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
|
||||
github.com/kisielk/errcheck v1.10.0 // indirect
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
|
||||
github.com/kulti/thelper v0.6.3 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.10 // indirect
|
||||
github.com/kulti/thelper v0.7.1 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.15 // indirect
|
||||
github.com/lasiar/canonicalheader v1.1.2 // indirect
|
||||
github.com/ldez/exptostd v0.4.2 // indirect
|
||||
github.com/ldez/gomoddirectives v0.6.1 // indirect
|
||||
github.com/ldez/grignotin v0.9.0 // indirect
|
||||
github.com/ldez/tagliatelle v0.7.1 // indirect
|
||||
github.com/ldez/usetesting v0.4.2 // indirect
|
||||
github.com/ldez/exptostd v0.4.5 // indirect
|
||||
github.com/ldez/gomoddirectives v0.8.0 // indirect
|
||||
github.com/ldez/grignotin v0.10.1 // indirect
|
||||
github.com/ldez/structtags v0.6.1 // indirect
|
||||
github.com/ldez/tagliatelle v0.7.2 // indirect
|
||||
github.com/ldez/usetesting v0.5.0 // indirect
|
||||
github.com/leonklingele/grouper v1.1.2 // indirect
|
||||
github.com/macabu/inamedparam v0.1.3 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/macabu/inamedparam v0.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/maratori/testableexamples v1.0.0 // indirect
|
||||
github.com/maratori/testpackage v1.1.1 // indirect
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
|
||||
github.com/manuelarte/funcorder v0.5.0 // indirect
|
||||
github.com/maratori/testableexamples v1.0.1 // indirect
|
||||
github.com/maratori/testpackage v1.1.2 // indirect
|
||||
github.com/matoous/godox v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mgechev/revive v1.7.0 // indirect
|
||||
github.com/mgechev/revive v1.15.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moricho/tparallel v0.3.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/nishanths/exhaustive v0.12.0 // indirect
|
||||
github.com/nishanths/predeclared v0.2.2 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.19.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/nunnatsa/ginkgolinter v0.23.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polyfloyd/go-errorlint v1.7.1 // indirect
|
||||
github.com/prometheus/client_golang v1.12.1 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
|
||||
github.com/quasilyte/gogrep v0.5.0 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
|
||||
github.com/raeperd/recvcheck v0.2.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.3.5 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.4.1 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.22.2 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sivchari/tenv v1.12.1 // indirect
|
||||
github.com/sonatard/noctx v0.1.0 // indirect
|
||||
github.com/sonatard/noctx v0.5.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.12.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/tdakkota/asciicheck v0.4.1 // indirect
|
||||
github.com/tetafro/godot v1.5.0 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect
|
||||
github.com/timonwong/loggercheck v0.10.1 // indirect
|
||||
github.com/tetafro/godot v1.5.4 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
|
||||
github.com/timonwong/loggercheck v0.11.0 // indirect
|
||||
github.com/tobischo/argon2 v0.1.0 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
|
||||
github.com/ultraware/funlen v0.2.0 // indirect
|
||||
github.com/ultraware/whitespace v0.2.0 // indirect
|
||||
github.com/uudashr/gocognit v1.2.0 // indirect
|
||||
github.com/uudashr/iface v1.3.1 // indirect
|
||||
github.com/xen0n/gosmopolitan v1.2.2 // indirect
|
||||
github.com/uudashr/gocognit v1.2.1 // indirect
|
||||
github.com/uudashr/iface v1.4.1 // indirect
|
||||
github.com/xen0n/gosmopolitan v1.3.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yagipy/maintidx v1.0.0 // indirect
|
||||
github.com/yeya24/promlinter v0.3.0 // indirect
|
||||
github.com/ykadowak/zerologlint v0.1.5 // indirect
|
||||
gitlab.com/bosi/decorder v0.4.2 // indirect
|
||||
go-simpler.org/musttag v0.13.0 // indirect
|
||||
go-simpler.org/sloglint v0.9.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.24.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
|
||||
go-simpler.org/musttag v0.14.0 // indirect
|
||||
go-simpler.org/sloglint v0.11.1 // indirect
|
||||
go.augendre.info/arangolint v0.4.0 // indirect
|
||||
go.augendre.info/fatcontext v0.9.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
|
||||
golang.org/x/image v0.37.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated // indirect
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
mvdan.cc/gofumpt v0.7.0 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect
|
||||
honnef.co/go/tools v0.7.0 // indirect
|
||||
mvdan.cc/gofumpt v0.9.2 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
gioui.org/cmd/gogio
|
||||
github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
|
||||
)
|
||||
|
||||
@@ -34,73 +34,87 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY=
|
||||
codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ=
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
|
||||
dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo=
|
||||
dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
|
||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
|
||||
gioui.org v0.8.0 h1:QV5p5JvsmSmGiIXVYOKn6d9YDliTfjtLlVf5J+BZ9Pg=
|
||||
gioui.org v0.8.0/go.mod h1:vEMmpxMOd/iwJhXvGVIzWEbxMWhnMQ9aByOGQdlQ8rc=
|
||||
gioui.org/cmd v0.8.0 h1:oy5qOlc1UXcglc5HBCMZQELiIzQ2obhT98mw+SuWafQ=
|
||||
gioui.org/cmd v0.8.0/go.mod h1:wKLAyAgRR25VMYFzGX2Ecia0m0Td562wDcZ3LaPHPTI=
|
||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||
gioui.org/x v0.8.0 h1:RhIlQNOFKKn8D8FeaKKaXCo7vB3x+fq4VcD10HW/YpA=
|
||||
gioui.org/x v0.8.0/go.mod h1:aXtQb+kyqoUOjDl5/uMqAopjzVzMkeHBbMQOGT5KnSE=
|
||||
git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260417040456-1762d36ddecc h1:jyfCTx9wk/uLaEMkdKsg491C/kjfbG2EKAVTORhZxHo=
|
||||
git.julianfamily.org/joejulian/gio-cmd-patched v0.9.1-0.20260417040456-1762d36ddecc/go.mod h1:RBQfFU8JCgMjQ2wKU9DG3zMC38TnY97E5MKoBGhGl3s=
|
||||
git.julianfamily.org/joejulian/gio-patched v0.9.1-0.20260416220049-9bfa6bc1c2dc h1:p2AaZUAXa/ExPybNyeB05+GjTSZGA9lCfDpWz49IT5Y=
|
||||
git.julianfamily.org/joejulian/gio-patched v0.9.1-0.20260416220049-9bfa6bc1c2dc/go.mod h1:BdI7mF5DCa3kxlo3G93XHL7khtZnk1gu4335pExk8gs=
|
||||
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI=
|
||||
git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo=
|
||||
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
|
||||
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
|
||||
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
|
||||
github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw=
|
||||
github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA=
|
||||
github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI=
|
||||
github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs=
|
||||
github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0=
|
||||
github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk=
|
||||
github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8=
|
||||
github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8=
|
||||
github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
|
||||
github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ=
|
||||
github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4=
|
||||
github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
|
||||
github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
|
||||
github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=
|
||||
github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
|
||||
github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q=
|
||||
github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ=
|
||||
github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ=
|
||||
github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II=
|
||||
github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
|
||||
github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
|
||||
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM=
|
||||
github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM=
|
||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k=
|
||||
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg=
|
||||
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
|
||||
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
|
||||
github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
|
||||
github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
|
||||
github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
|
||||
github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU=
|
||||
github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw=
|
||||
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ=
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q=
|
||||
github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
|
||||
github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
|
||||
github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
|
||||
github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
|
||||
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
|
||||
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
|
||||
github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo=
|
||||
github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
|
||||
github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY=
|
||||
github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU=
|
||||
github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU=
|
||||
github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4=
|
||||
github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
|
||||
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo=
|
||||
github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c=
|
||||
github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE=
|
||||
github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -109,77 +123,95 @@ github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5
|
||||
github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo=
|
||||
github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M=
|
||||
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
||||
github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A=
|
||||
github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc=
|
||||
github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs=
|
||||
github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos=
|
||||
github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk=
|
||||
github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8=
|
||||
github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY=
|
||||
github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M=
|
||||
github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ=
|
||||
github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg=
|
||||
github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8=
|
||||
github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU=
|
||||
github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE=
|
||||
github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE=
|
||||
github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg=
|
||||
github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s=
|
||||
github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E=
|
||||
github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70=
|
||||
github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
|
||||
github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
|
||||
github.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw=
|
||||
github.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60=
|
||||
github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
|
||||
github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
|
||||
github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4=
|
||||
github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ=
|
||||
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
|
||||
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
|
||||
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
|
||||
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
|
||||
github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
|
||||
github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
|
||||
github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
|
||||
github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4 h1:UZdrvid2JFwnvPlUSEFlE794XZL4Jmrj8fuxfcLECJE=
|
||||
github.com/chromedp/cdproto v0.0.0-20250429231605-6ed5b53462d4/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||
github.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk=
|
||||
github.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=
|
||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY=
|
||||
github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo=
|
||||
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
|
||||
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs=
|
||||
github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88=
|
||||
github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c=
|
||||
github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk=
|
||||
github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ=
|
||||
github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ=
|
||||
github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY=
|
||||
github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc=
|
||||
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
|
||||
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
|
||||
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
|
||||
github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA=
|
||||
github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw=
|
||||
github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=
|
||||
github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
|
||||
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ=
|
||||
github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
|
||||
github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w=
|
||||
github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w=
|
||||
github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
|
||||
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
|
||||
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
|
||||
github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-json-experiment/json v0.0.0-20250417205406-170dfdcf87d1 h1:+VexzzkMLb1tnvpuQdGT/DicIRW7MN8ozsXqBMgp0Hk=
|
||||
github.com/go-json-experiment/json v0.0.0-20250417205406-170dfdcf87d1/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
@@ -218,22 +250,24 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi
|
||||
github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ=
|
||||
github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus=
|
||||
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
|
||||
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
|
||||
github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
|
||||
github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM=
|
||||
github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo=
|
||||
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -264,22 +298,28 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
|
||||
github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
|
||||
github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU=
|
||||
github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s=
|
||||
github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
|
||||
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
|
||||
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
|
||||
github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I=
|
||||
github.com/golangci/golangci-lint v1.64.8/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4=
|
||||
github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs=
|
||||
github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo=
|
||||
github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c=
|
||||
github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc=
|
||||
github.com/golangci/golangci-lint/v2 v2.11.4 h1:GK+UlZBN5y7rh2PBnHA93XLSX6RaF7uhzJQ3JwU1wuA=
|
||||
github.com/golangci/golangci-lint/v2 v2.11.4/go.mod h1:ODQDCASMA3VqfZYIbbQLpTRTzV7O/vjmIRF6u8NyFwI=
|
||||
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
|
||||
github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
|
||||
github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
|
||||
github.com/golangci/misspell v0.8.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg=
|
||||
github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg=
|
||||
github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
|
||||
github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
|
||||
github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
|
||||
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs=
|
||||
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ=
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
|
||||
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
|
||||
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -292,7 +332,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -306,25 +345,24 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=
|
||||
github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
|
||||
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
|
||||
github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
|
||||
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
|
||||
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
|
||||
github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM=
|
||||
github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8=
|
||||
github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk=
|
||||
github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY=
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk=
|
||||
github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A=
|
||||
github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU=
|
||||
github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA=
|
||||
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||
github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8=
|
||||
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
|
||||
@@ -333,8 +371,8 @@ github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1T
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
@@ -346,12 +384,12 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk=
|
||||
github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4=
|
||||
github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4=
|
||||
github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako=
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=
|
||||
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
|
||||
github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc=
|
||||
github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk=
|
||||
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
|
||||
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
@@ -363,15 +401,13 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ=
|
||||
github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM=
|
||||
github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
|
||||
github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
|
||||
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
|
||||
github.com/kisielk/errcheck v1.10.0 h1:Lvs/YAHP24YKg08LA8oDw2z9fJVme090RAXd90S+rrw=
|
||||
github.com/kisielk/errcheck v1.10.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
|
||||
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
|
||||
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@@ -382,34 +418,40 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs=
|
||||
github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I=
|
||||
github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs=
|
||||
github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY=
|
||||
github.com/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98=
|
||||
github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs=
|
||||
github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w=
|
||||
github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
|
||||
github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=
|
||||
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
|
||||
github.com/ldez/exptostd v0.4.2 h1:l5pOzHBz8mFOlbcifTxzfyYbgEmoUqjxLFHZkjlbHXs=
|
||||
github.com/ldez/exptostd v0.4.2/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ=
|
||||
github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc=
|
||||
github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs=
|
||||
github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow=
|
||||
github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk=
|
||||
github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk=
|
||||
github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I=
|
||||
github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA=
|
||||
github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ=
|
||||
github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
|
||||
github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
|
||||
github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk=
|
||||
github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q=
|
||||
github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
|
||||
github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
|
||||
github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
|
||||
github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY=
|
||||
github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk=
|
||||
github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI=
|
||||
github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
|
||||
github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
|
||||
github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
|
||||
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
|
||||
github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk=
|
||||
github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
|
||||
github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
|
||||
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI=
|
||||
github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE=
|
||||
github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04=
|
||||
github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww=
|
||||
github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM=
|
||||
github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8=
|
||||
github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA=
|
||||
github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
|
||||
github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
|
||||
github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
|
||||
github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc=
|
||||
github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4=
|
||||
github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
@@ -418,13 +460,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY=
|
||||
github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4=
|
||||
github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q=
|
||||
github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
@@ -436,6 +477,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI=
|
||||
github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
|
||||
@@ -444,14 +487,12 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK
|
||||
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
|
||||
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
|
||||
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
|
||||
github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4=
|
||||
github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
|
||||
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
|
||||
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
|
||||
github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8=
|
||||
github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
|
||||
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
|
||||
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
|
||||
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
|
||||
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
|
||||
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
|
||||
@@ -461,18 +502,13 @@ github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT9
|
||||
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA=
|
||||
github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
@@ -495,10 +531,10 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo=
|
||||
github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
|
||||
github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
|
||||
github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
|
||||
github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo=
|
||||
github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng=
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU=
|
||||
@@ -514,100 +550,94 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU=
|
||||
github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE=
|
||||
github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
|
||||
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
|
||||
github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
|
||||
github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
|
||||
github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
|
||||
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8=
|
||||
github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g=
|
||||
github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
|
||||
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
|
||||
github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08 h1:AoLtJX4WUtZkhhUUMFy3GgecAALp/Mb4S1iyQOA2s0U=
|
||||
github.com/securego/gosec/v2 v2.24.8-0.20260309165252-619ce2117e08/go.mod h1:+XLCJiRE95ga77XInNELh2M6zQP+PdqiT9Zpm0D9Wpk=
|
||||
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
|
||||
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
|
||||
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
|
||||
github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY=
|
||||
github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw=
|
||||
github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM=
|
||||
github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c=
|
||||
github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
|
||||
github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
|
||||
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
|
||||
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ=
|
||||
github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0=
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g=
|
||||
github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
|
||||
github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
|
||||
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
|
||||
github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag=
|
||||
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
|
||||
github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw=
|
||||
github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio=
|
||||
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg=
|
||||
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
|
||||
github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg=
|
||||
github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
|
||||
github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg=
|
||||
github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk=
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
|
||||
github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
|
||||
github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
|
||||
github.com/tobischo/argon2 v0.1.0 h1:mwAx/9DK/4rP0xzNifb/XMAf43dU3eG1B3aeF88qu4Y=
|
||||
github.com/tobischo/argon2 v0.1.0/go.mod h1:4NLmLFwhWPbT66nRZNgcktV/mibJ6fESoeEp43h9GRw=
|
||||
github.com/tobischo/gokeepasslib/v3 v3.6.2 h1:SJzzllmNe7iZLudLJ3Lzdm3pDb++AJqZlmqG+SR8bVc=
|
||||
github.com/tobischo/gokeepasslib/v3 v3.6.2/go.mod h1:ga7HFqG0TZSLNao/QOnV2+yngkrf5186saPxSQ1Xp7o=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
|
||||
github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||
github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI=
|
||||
github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA=
|
||||
github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g=
|
||||
github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
|
||||
github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=
|
||||
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
|
||||
github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U=
|
||||
github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg=
|
||||
github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU=
|
||||
github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg=
|
||||
github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
|
||||
github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
|
||||
github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU=
|
||||
github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
|
||||
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
|
||||
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM=
|
||||
github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk=
|
||||
github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs=
|
||||
@@ -625,10 +655,14 @@ gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
|
||||
gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8=
|
||||
go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ=
|
||||
go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28=
|
||||
go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE=
|
||||
go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM=
|
||||
go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE=
|
||||
go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww=
|
||||
go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
|
||||
go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
|
||||
go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s=
|
||||
go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ=
|
||||
go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
|
||||
go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
|
||||
go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE=
|
||||
go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -646,16 +680,14 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -665,8 +697,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -677,14 +709,14 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90 h1:kyPrwnEYXdME284bE7xgS9BPxhG7MCa5hw1/TpaTJVs=
|
||||
golang.org/x/exp/shiny v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:jqkJFnLVkS8zgKKY4+MOPCZtuZGw3hONUjhapUSwZ8c=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
|
||||
@@ -711,13 +743,11 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -752,14 +782,12 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -827,9 +855,7 @@ golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -839,9 +865,7 @@ golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
@@ -852,9 +876,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
@@ -894,7 +916,6 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
@@ -904,22 +925,17 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
|
||||
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
@@ -1027,7 +1043,6 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1037,12 +1052,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI=
|
||||
honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4=
|
||||
mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU=
|
||||
mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo=
|
||||
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U=
|
||||
mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ=
|
||||
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
|
||||
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
|
||||
mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4=
|
||||
mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s=
|
||||
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI=
|
||||
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
+108
-9
@@ -275,7 +275,7 @@ func (s *Server) FindBrowserLogins(ctx context.Context, req *keepassgov1.FindBro
|
||||
|
||||
var matches []rankedBrowserMatch
|
||||
for _, entry := range displayModel.Entries {
|
||||
quality, score := classifyBrowserEntryMatch(pageHost, entry.URL)
|
||||
quality, score := classifyBrowserEntry(pageHost, entry)
|
||||
if score == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -390,7 +390,7 @@ func (s *Server) GetBrowserCredential(ctx context.Context, req *keepassgov1.GetB
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
if _, score := classifyBrowserEntryMatch(pageHost, entry.URL); score == 0 {
|
||||
if _, score := classifyBrowserEntry(pageHost, entry); score == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "entry url does not match requested page")
|
||||
}
|
||||
}
|
||||
@@ -446,19 +446,22 @@ func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRe
|
||||
}
|
||||
displayModel := visibleModel(model)
|
||||
internalPath := expandClientPath(displayModel, req.GetPath())
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, internalPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
model = displayModel
|
||||
var entries []vault.Entry
|
||||
if strings.TrimSpace(req.GetQuery()) != "" {
|
||||
token, err := s.authenticateRequest(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := model.Search(req.GetQuery())
|
||||
entries = make([]vault.Entry, 0, len(results))
|
||||
for _, result := range results {
|
||||
entries = append(entries, result.Entry)
|
||||
entries, err = s.authorizedSearchEntries(ctx, model, token, internalPath, results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, internalPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = model.EntriesInPath(internalPath)
|
||||
}
|
||||
|
||||
@@ -472,6 +475,49 @@ func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRe
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) authorizedSearchEntries(ctx context.Context, model vault.Model, token apitokens.Token, path []string, results []vault.SearchResult) ([]vault.Entry, error) {
|
||||
entries := make([]vault.Entry, 0, len(results))
|
||||
var promptResource *apitokens.Resource
|
||||
for _, result := range results {
|
||||
entry := result.Entry
|
||||
if !hasPathPrefix(path, entry.Path) {
|
||||
continue
|
||||
}
|
||||
resource := apitokens.Resource{Kind: apitokens.ResourceGroup, Path: entry.Path}
|
||||
switch evaluateAuthorization(model, token, apitokens.OperationListEntries, resource) {
|
||||
case apitokens.DecisionAllow:
|
||||
entries = append(entries, entry)
|
||||
case apitokens.DecisionPrompt:
|
||||
if promptResource == nil {
|
||||
candidate := resource
|
||||
promptResource = &candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(entries) != 0 || promptResource == nil {
|
||||
return entries, nil
|
||||
}
|
||||
if _, err := s.authorizeResourceRequest(ctx, token, apitokens.OperationListEntries, *promptResource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorizedSearchEntriesWithinPath(path, promptResource.Path, results), nil
|
||||
}
|
||||
|
||||
func authorizedSearchEntriesWithinPath(requestPath, approvedPath []string, results []vault.SearchResult) []vault.Entry {
|
||||
entries := make([]vault.Entry, 0, len(results))
|
||||
for _, result := range results {
|
||||
entry := result.Entry
|
||||
if !hasPathPrefix(requestPath, entry.Path) {
|
||||
continue
|
||||
}
|
||||
if !hasPathPrefix(approvedPath, entry.Path) {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s *Server) ListGroups(ctx context.Context, req *keepassgov1.ListGroupsRequest) (*keepassgov1.ListGroupsResponse, error) {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
@@ -1063,6 +1109,52 @@ func normalizedBrowserEntryHost(raw string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func browserURLFieldKey(key string) bool {
|
||||
if len(key) <= len("URL") || !strings.EqualFold(key[:len("URL")], "URL") {
|
||||
return false
|
||||
}
|
||||
for _, r := range key[len("URL"):] {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func browserEntryURLs(entry vault.Entry) []string {
|
||||
urls := make([]string, 0, 1+len(entry.Fields))
|
||||
if raw := strings.TrimSpace(entry.URL); raw != "" {
|
||||
urls = append(urls, raw)
|
||||
}
|
||||
if len(entry.Fields) == 0 {
|
||||
return urls
|
||||
}
|
||||
keys := slices.Collect(maps.Keys(entry.Fields))
|
||||
slices.Sort(keys)
|
||||
for _, key := range keys {
|
||||
if !browserURLFieldKey(key) {
|
||||
continue
|
||||
}
|
||||
if raw := strings.TrimSpace(entry.Fields[key]); raw != "" {
|
||||
urls = append(urls, raw)
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func classifyBrowserEntry(pageHost string, entry vault.Entry) (string, int) {
|
||||
bestQuality := ""
|
||||
bestScore := 0
|
||||
for _, rawURL := range browserEntryURLs(entry) {
|
||||
quality, score := classifyBrowserEntryMatch(pageHost, rawURL)
|
||||
if score > bestScore {
|
||||
bestQuality = quality
|
||||
bestScore = score
|
||||
}
|
||||
}
|
||||
return bestQuality, bestScore
|
||||
}
|
||||
|
||||
func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
|
||||
entryHost := normalizedBrowserEntryHost(rawEntryURL)
|
||||
if entryHost == "" {
|
||||
@@ -1078,6 +1170,13 @@ func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
|
||||
}
|
||||
}
|
||||
|
||||
func hasPathPrefix(prefix, path []string) bool {
|
||||
if len(prefix) > len(path) {
|
||||
return false
|
||||
}
|
||||
return slices.Equal(prefix, path[:len(prefix)])
|
||||
}
|
||||
|
||||
func visibleModel(model vault.Model) vault.Model {
|
||||
out := model
|
||||
out.Entries = nil
|
||||
|
||||
@@ -294,6 +294,55 @@ func TestVaultServiceFindsBrowserLoginsForSchemeLessEntryURLs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultServiceFindsBrowserLoginsForCustomURLFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClientForModel(t, vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{
|
||||
ID: "night-fox-gitlab",
|
||||
Title: "Night Fox GitLab",
|
||||
Username: "nightfox",
|
||||
Password: "vault-code",
|
||||
Path: []string{"Root", "Internet"},
|
||||
Fields: map[string]string{
|
||||
"URL1": "gitlab.com",
|
||||
},
|
||||
},
|
||||
testAPITokenEntry(t,
|
||||
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
||||
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyUsername, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
||||
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationCopyPassword, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
|
||||
),
|
||||
},
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
resp, err := client.FindBrowserLogins(tokenContext(defaultTestTokenSecret), &keepassgov1.FindBrowserLoginsRequest{
|
||||
PageUrl: "https://gitlab.com/users/sign_in",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("FindBrowserLogins() error = %v", err)
|
||||
}
|
||||
if len(resp.Matches) != 1 {
|
||||
t.Fatalf("len(FindBrowserLogins().Matches) = %d, want 1", len(resp.Matches))
|
||||
}
|
||||
if resp.Matches[0].Id != "night-fox-gitlab" {
|
||||
t.Fatalf("FindBrowserLogins().Matches[0].Id = %q, want night-fox-gitlab", resp.Matches[0].Id)
|
||||
}
|
||||
|
||||
credential, err := client.GetBrowserCredential(tokenContext(defaultTestTokenSecret), &keepassgov1.GetBrowserCredentialRequest{
|
||||
Id: "night-fox-gitlab",
|
||||
PageUrl: "https://gitlab.com/users/sign_in",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBrowserCredential() error = %v", err)
|
||||
}
|
||||
if credential.GetId() != "night-fox-gitlab" {
|
||||
t.Fatalf("GetBrowserCredential().Id = %q, want night-fox-gitlab", credential.GetId())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultServiceFindsBrowserLoginsWithinAuthorizedGroupScope(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1203,6 +1252,51 @@ func TestVaultServiceListsEntriesForAuthorizedClients(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultServiceSearchesEntriesWithinAuthorizedScope(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client, _, cleanup := newTestClientForModel(t, vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{
|
||||
ID: "turk-codex",
|
||||
Title: "Turk Codex GitLab",
|
||||
Username: "basher",
|
||||
Password: "chip-stack",
|
||||
URL: "https://gitlab.com",
|
||||
Path: []string{"keepass", "Joe", "codex"},
|
||||
},
|
||||
{
|
||||
ID: "rusty-internet",
|
||||
Title: "Rusty Internet GitLab",
|
||||
Username: "rusty",
|
||||
Password: "bellagio-stack",
|
||||
URL: "https://gitlab.com",
|
||||
Path: []string{"keepass", "Joe", "Internet"},
|
||||
},
|
||||
testAPITokenEntry(t,
|
||||
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root", "Joe", "codex"}}},
|
||||
),
|
||||
},
|
||||
})
|
||||
defer cleanup()
|
||||
|
||||
resp, err := client.ListEntries(tokenContext(defaultTestTokenSecret), &keepassgov1.ListEntriesRequest{
|
||||
Query: "GitLab",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListEntries() error = %v", err)
|
||||
}
|
||||
if len(resp.Entries) != 1 {
|
||||
t.Fatalf("len(ListEntries().Entries) = %d, want 1", len(resp.Entries))
|
||||
}
|
||||
if got := resp.Entries[0].Id; got != "turk-codex" {
|
||||
t.Fatalf("ListEntries().Entries[0].Id = %q, want turk-codex", got)
|
||||
}
|
||||
if got := resp.Entries[0].Path; !slices.Equal(got, []string{"Joe", "codex"}) {
|
||||
t.Fatalf("ListEntries().Entries[0].Path = %v, want [Joe codex]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVaultServiceListsCreatesAndRenamesGroupsForAuthorizedClients(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
+15
-9
@@ -140,6 +140,7 @@ type statePaths struct {
|
||||
AutofillCachePath string
|
||||
PendingSharedVaultPath string
|
||||
PendingSharedVaultNamePath string
|
||||
PendingSharedLookupPath string
|
||||
}
|
||||
|
||||
type recentVaultRecord struct {
|
||||
@@ -474,6 +475,8 @@ type ui struct {
|
||||
autofillCachePath string
|
||||
pendingSharedVaultPath string
|
||||
pendingSharedVaultNamePath string
|
||||
pendingSharedLookupPath string
|
||||
pendingSharedLookupQuery string
|
||||
editingEntry bool
|
||||
syncDefaultSourceMode syncSourceMode
|
||||
syncDefaultDirection syncDirection
|
||||
@@ -557,10 +560,10 @@ func newUIWithSession(mode string, sess appstate.CurrentSession, paths ...stateP
|
||||
|
||||
func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths) *ui {
|
||||
th := material.NewTheme()
|
||||
th.Palette.Bg = bgColor
|
||||
th.Palette.Fg = color.NRGBA{R: 31, G: 29, B: 27, A: 255}
|
||||
th.Palette.ContrastBg = accentColor
|
||||
th.Palette.ContrastFg = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||
th.Bg = bgColor
|
||||
th.Fg = color.NRGBA{R: 31, G: 29, B: 27, A: 255}
|
||||
th.ContrastBg = accentColor
|
||||
th.ContrastFg = color.NRGBA{R: 255, G: 252, B: 247, A: 255}
|
||||
|
||||
u := &ui{
|
||||
mode: mode,
|
||||
@@ -656,6 +659,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
autofillCachePath: paths.AutofillCachePath,
|
||||
pendingSharedVaultPath: paths.PendingSharedVaultPath,
|
||||
pendingSharedVaultNamePath: paths.PendingSharedVaultNamePath,
|
||||
pendingSharedLookupPath: paths.PendingSharedLookupPath,
|
||||
recentVaultGroups: map[string][]string{},
|
||||
recentVaultUsedAt: map[string]time.Time{},
|
||||
lifecycleAdvancedHidden: true,
|
||||
@@ -704,6 +708,7 @@ func newUIWithState(mode string, sess appstate.CurrentSession, paths statePaths)
|
||||
u.showStatusMessage("Some saved remote sign-ins came from an older KeePassGO build. Reopen those remotes and save them in the vault to migrate them.")
|
||||
}
|
||||
u.consumePendingSharedVaultImport()
|
||||
u.consumePendingSharedLookup()
|
||||
u.restoreStartupLifecycleTarget()
|
||||
u.requestMasterPassFocus = u.hasSelectedLifecycleTarget()
|
||||
u.loadUIPreferences()
|
||||
@@ -785,6 +790,7 @@ func defaultStatePaths(stateDir string) statePaths {
|
||||
AutofillCachePath: filepath.Join(baseDir, "autofill-cache.json"),
|
||||
PendingSharedVaultPath: filepath.Join(baseDir, "pending-shared-vault.kdbx"),
|
||||
PendingSharedVaultNamePath: filepath.Join(baseDir, "pending-shared-vault-name.txt"),
|
||||
PendingSharedLookupPath: filepath.Join(baseDir, "pending-shared-lookup.txt"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1529,7 +1535,7 @@ func approvalFact(theme *material.Theme, title, primary, secondary string) layou
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(theme, unit.Sp(16), strings.TrimSpace(primary))
|
||||
lbl.Color = theme.Palette.Fg
|
||||
lbl.Color = theme.Fg
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -1572,7 +1578,7 @@ func (u *ui) aboutDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||
},
|
||||
func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(14), currentAppVersion())
|
||||
lbl.Color = u.theme.Palette.Fg
|
||||
lbl.Color = u.theme.Fg
|
||||
return lbl.Layout(gtx)
|
||||
},
|
||||
}
|
||||
@@ -1594,7 +1600,7 @@ func aboutFact(theme *material.Theme, title, primary, secondary string) layout.W
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(theme, unit.Sp(16), primary)
|
||||
lbl.Color = theme.Palette.Fg
|
||||
lbl.Color = theme.Fg
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -1684,7 +1690,7 @@ func (u *ui) listPanelSearchRow(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.Color = u.theme.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
})
|
||||
@@ -3144,7 +3150,7 @@ func syncDialogSummaryCard(gtx layout.Context, th *material.Theme, purpose syncD
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(th, unit.Sp(14), syncDialogSummaryText(purpose, source, direction))
|
||||
lbl.Color = th.Palette.Fg
|
||||
lbl.Color = th.Fg
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -4,8 +4,10 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
@@ -17,6 +19,8 @@ import (
|
||||
"git.julianfamily.org/keepassgo/internal/webdav"
|
||||
)
|
||||
|
||||
var pendingSharedLookupURLPattern = regexp.MustCompile(`https?://[^\s<>"']+`)
|
||||
|
||||
func (u *ui) createVaultAction() error {
|
||||
key, err := u.currentMasterKey()
|
||||
defer u.clearMasterPassword()
|
||||
@@ -78,6 +82,7 @@ func (u *ui) openVaultAction() error {
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
u.applyPendingSharedLookup()
|
||||
u.applyPendingLifecycleOpenIntent()
|
||||
return nil
|
||||
}
|
||||
@@ -120,6 +125,7 @@ func (u *ui) startOpenVaultAction() {
|
||||
u.loadSecuritySettingsFromSession()
|
||||
u.editingEntry = false
|
||||
u.filter()
|
||||
u.applyPendingSharedLookup()
|
||||
u.applyPendingLifecycleOpenIntent()
|
||||
return nil
|
||||
}, nil
|
||||
@@ -741,6 +747,49 @@ func (u *ui) consumePendingSharedVaultImport() {
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePendingSharedLookupQuery(raw string) string {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if match := pendingSharedLookupURLPattern.FindString(value); match != "" {
|
||||
value = match
|
||||
}
|
||||
if parsed, err := url.Parse(value); err == nil && strings.TrimSpace(parsed.Hostname()) != "" {
|
||||
return strings.ToLower(strings.TrimSpace(parsed.Hostname()))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (u *ui) consumePendingSharedLookup() {
|
||||
path := strings.TrimSpace(u.pendingSharedLookupPath)
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
u.state.ErrorMessage = fmt.Sprintf("shared lookup: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
_ = os.Remove(path)
|
||||
u.pendingSharedLookupQuery = normalizePendingSharedLookupQuery(string(data))
|
||||
u.applyPendingSharedLookup()
|
||||
}
|
||||
|
||||
func (u *ui) applyPendingSharedLookup() {
|
||||
query := strings.TrimSpace(u.pendingSharedLookupQuery)
|
||||
status, ok := u.state.Session.(sessionStatus)
|
||||
if query == "" || !ok || !status.HasVault() || status.IsLocked() {
|
||||
return
|
||||
}
|
||||
u.pendingSharedLookupQuery = ""
|
||||
u.state.Section = appstate.SectionEntries
|
||||
u.search.SetText(query)
|
||||
u.filter()
|
||||
}
|
||||
|
||||
func (u *ui) importSharedVaultBytesAction(name string, content []byte) error {
|
||||
target := u.importedVaultDestination(name)
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
|
||||
|
||||
@@ -8390,6 +8390,9 @@ func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
|
||||
if got := paths.PendingSharedVaultNamePath; got != filepath.Join(base, "pending-shared-vault-name.txt") {
|
||||
t.Fatalf("PendingSharedVaultNamePath = %q, want %q", got, filepath.Join(base, "pending-shared-vault-name.txt"))
|
||||
}
|
||||
if got := paths.PendingSharedLookupPath; got != filepath.Join(base, "pending-shared-lookup.txt") {
|
||||
t.Fatalf("PendingSharedLookupPath = %q, want %q", got, filepath.Join(base, "pending-shared-lookup.txt"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestImportedVaultDestinationUsesIncomingFilenameInsideDefaultDirectory(t *testing.T) {
|
||||
@@ -8520,6 +8523,95 @@ func TestUIConsumesPendingSharedVaultImportOnStartup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIConsumesPendingSharedLookupOnStartupWhenVaultIsAlreadyOpen(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
paths := statePaths{
|
||||
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
|
||||
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
|
||||
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||
PendingSharedLookupPath: filepath.Join(dir, "pending-shared-lookup.txt"),
|
||||
}
|
||||
if err := os.WriteFile(paths.PendingSharedLookupPath, []byte("https://bellagio.example.invalid/login\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(PendingSharedLookupPath) error = %v", err)
|
||||
}
|
||||
|
||||
u := newUIWithSession("phone", &uiSession{model: vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "bellagio-login", Title: "Bellagio", URL: "https://bellagio.example.invalid/login", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.example.invalid", Path: []string{"Crew", "Internet"}},
|
||||
},
|
||||
}}, paths)
|
||||
|
||||
if got := u.search.Text(); got != "bellagio.example.invalid" {
|
||||
t.Fatalf("search after pending shared lookup = %q, want %q", got, "bellagio.example.invalid")
|
||||
}
|
||||
if got := u.filteredTitles(); !slices.Equal(got, []string{"Bellagio"}) {
|
||||
t.Fatalf("filteredTitles() after pending shared lookup = %v, want [Bellagio]", got)
|
||||
}
|
||||
if _, err := os.Stat(paths.PendingSharedLookupPath); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("Stat(PendingSharedLookupPath) error = %v, want not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizePendingSharedLookupQueryExtractsURLFromTextSnippet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
raw := "Meet the crew at https://bellagio.example.invalid/login before the vault opens."
|
||||
if got := normalizePendingSharedLookupQuery(raw); got != "bellagio.example.invalid" {
|
||||
t.Fatalf("normalizePendingSharedLookupQuery() = %q, want %q", got, "bellagio.example.invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAppliesPendingSharedLookupAfterOpeningVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
paths := statePaths{
|
||||
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
|
||||
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
|
||||
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||
PendingSharedLookupPath: filepath.Join(dir, "pending-shared-lookup.txt"),
|
||||
}
|
||||
if err := os.WriteFile(paths.PendingSharedLookupPath, []byte("https://bellagio.example.invalid/login\n"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(PendingSharedLookupPath) error = %v", err)
|
||||
}
|
||||
|
||||
key := vault.MasterKey{Password: "correct horse battery staple"}
|
||||
vaultPath := filepath.Join(dir, "bellagio.kdbx")
|
||||
var encoded bytes.Buffer
|
||||
if err := vault.SaveKDBXWithKey(&encoded, vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "bellagio-login", Title: "Bellagio", URL: "https://bellagio.example.invalid/login", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "vault-console", Title: "Vault Console", URL: "https://vault.example.invalid", Path: []string{"Crew", "Internet"}},
|
||||
},
|
||||
}, key); err != nil {
|
||||
t.Fatalf("SaveKDBXWithKey() error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(vaultPath, encoded.Bytes(), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(vaultPath) error = %v", err)
|
||||
}
|
||||
|
||||
u := newUIWithState("phone", &session.Manager{}, paths)
|
||||
if got := u.search.Text(); got != "" {
|
||||
t.Fatalf("search before open with pending shared lookup = %q, want empty", got)
|
||||
}
|
||||
u.vaultPath.SetText(vaultPath)
|
||||
u.masterPassword.SetText(key.Password)
|
||||
if err := u.openVaultAction(); err != nil {
|
||||
t.Fatalf("openVaultAction() with pending shared lookup error = %v", err)
|
||||
}
|
||||
if got := u.search.Text(); got != "bellagio.example.invalid" {
|
||||
t.Fatalf("search after open with pending shared lookup = %q, want %q", got, "bellagio.example.invalid")
|
||||
}
|
||||
if got := u.filteredTitles(); !slices.Equal(got, []string{"Bellagio"}) {
|
||||
t.Fatalf("filteredTitles() after open with pending shared lookup = %v, want [Bellagio]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUICurrentShareableVaultPathUsesSelectedVaultPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -802,8 +802,8 @@ func (u *ui) prefillAdvancedSyncRemoteFromSavedBinding() {
|
||||
}
|
||||
|
||||
func (u *ui) syncDialogTitle() string {
|
||||
switch {
|
||||
case u.syncDialogPurpose == syncDialogPurposeRemoteSetup:
|
||||
switch u.syncDialogPurpose {
|
||||
case syncDialogPurposeRemoteSetup:
|
||||
if _, ok := u.selectedVaultRemoteBinding(); ok {
|
||||
return "Remote Sync Settings"
|
||||
}
|
||||
@@ -814,8 +814,8 @@ func (u *ui) syncDialogTitle() string {
|
||||
}
|
||||
|
||||
func (u *ui) syncDialogDescription() string {
|
||||
switch {
|
||||
case u.syncDialogPurpose == syncDialogPurposeRemoteSetup:
|
||||
switch u.syncDialogPurpose {
|
||||
case syncDialogPurposeRemoteSetup:
|
||||
if _, ok := u.selectedVaultRemoteBinding(); ok {
|
||||
return "Review or change this vault's saved WebDAV target, credentials, and sync mode."
|
||||
}
|
||||
@@ -826,8 +826,8 @@ func (u *ui) syncDialogDescription() string {
|
||||
}
|
||||
|
||||
func (u *ui) syncDialogConfirmButtonLabel() string {
|
||||
switch {
|
||||
case u.syncDialogPurpose == syncDialogPurposeRemoteSetup:
|
||||
switch u.syncDialogPurpose {
|
||||
case syncDialogPurposeRemoteSetup:
|
||||
if _, ok := u.selectedVaultRemoteBinding(); ok {
|
||||
return "Save Remote Sync Settings"
|
||||
}
|
||||
|
||||
@@ -372,7 +372,7 @@ func settingsSummaryCard(gtx layout.Context, th *material.Theme, title, body str
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(th, unit.Sp(13), body)
|
||||
lbl.Color = th.Palette.Fg
|
||||
lbl.Color = th.Fg
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -199,7 +199,7 @@ func (u *ui) syncPasswordField(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
field := func(gtx layout.Context) layout.Dimensions {
|
||||
editor := material.Editor(u.theme, &u.syncRemotePassword, "Remote Password")
|
||||
editor.Color = u.theme.Palette.Fg
|
||||
editor.Color = u.theme.Fg
|
||||
editor.HintColor = mutedColor
|
||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package autofillcache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type BindingsFile struct {
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
Apps map[string]string `json:"apps,omitempty"`
|
||||
}
|
||||
|
||||
func ReadBindings(path string) (BindingsFile, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return BindingsFile{}, nil
|
||||
}
|
||||
return BindingsFile{}, err
|
||||
}
|
||||
var bindings BindingsFile
|
||||
if err := json.Unmarshal(data, &bindings); err != nil {
|
||||
return BindingsFile{}, err
|
||||
}
|
||||
if bindings.Apps == nil {
|
||||
bindings.Apps = make(map[string]string)
|
||||
}
|
||||
return bindings, nil
|
||||
}
|
||||
|
||||
func RememberBinding(path, rawTarget, entryID string, now time.Time) error {
|
||||
bindings, err := ReadBindings(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bindings.Apps == nil {
|
||||
bindings.Apps = make(map[string]string)
|
||||
}
|
||||
target := strings.TrimSpace(rawTarget)
|
||||
id := strings.TrimSpace(entryID)
|
||||
if target == "" || id == "" {
|
||||
return nil
|
||||
}
|
||||
bindings.Apps[target] = id
|
||||
bindings.UpdatedAt = now.UTC().Format(time.RFC3339)
|
||||
data, err := json.MarshalIndent(bindings, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0o600)
|
||||
}
|
||||
|
||||
func ResolveWithBindings(cache File, bindings BindingsFile, rawTarget string) MatchResult {
|
||||
target := strings.TrimSpace(rawTarget)
|
||||
if entryID := strings.TrimSpace(bindings.Apps[target]); entryID != "" {
|
||||
for _, entry := range cache.Entries {
|
||||
if entry.ID == entryID {
|
||||
return MatchResult{Status: MatchStatusFound, Entry: entry}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Resolve(cache, rawTarget)
|
||||
}
|
||||
|
||||
func ChooserCandidates(cache File, rawTarget string) []Entry {
|
||||
if result := Resolve(cache, rawTarget); result.Status == MatchStatusFound {
|
||||
return []Entry{result.Entry}
|
||||
}
|
||||
candidates := append([]Entry(nil), cache.Entries...)
|
||||
slices.SortFunc(candidates, func(left, right Entry) int {
|
||||
if cmp := strings.Compare(strings.ToLower(strings.TrimSpace(left.Title)), strings.ToLower(strings.TrimSpace(right.Title))); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
if cmp := strings.Compare(strings.ToLower(strings.Join(left.Path, "/")), strings.ToLower(strings.Join(right.Path, "/"))); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
return strings.Compare(left.ID, right.ID)
|
||||
})
|
||||
return candidates
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package autofillcache
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestResolvePrefersLearnedAndroidAppBinding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := File{
|
||||
Entries: []Entry{
|
||||
{
|
||||
ID: "danny-ocean",
|
||||
Title: "Bellagio Vault",
|
||||
Username: "danny",
|
||||
Password: "secret1",
|
||||
URL: "https://bellagio.example.invalid/login",
|
||||
Host: "bellagio.example.invalid",
|
||||
},
|
||||
{
|
||||
ID: "rusty-ryan",
|
||||
Title: "Mirage Crew",
|
||||
Username: "rusty",
|
||||
Password: "secret2",
|
||||
URL: "https://mirage.example.invalid/login",
|
||||
Host: "mirage.example.invalid",
|
||||
},
|
||||
},
|
||||
}
|
||||
bindings := BindingsFile{
|
||||
Apps: map[string]string{
|
||||
"androidapp://com.samsung.android.shealth": "rusty-ryan",
|
||||
},
|
||||
}
|
||||
|
||||
got := ResolveWithBindings(cache, bindings, "androidapp://com.samsung.android.shealth")
|
||||
if got.Status != MatchStatusFound {
|
||||
t.Fatalf("ResolveWithBindings() status = %q, want found", got.Status)
|
||||
}
|
||||
if got.Entry.ID != "rusty-ryan" {
|
||||
t.Fatalf("ResolveWithBindings() entry = %q, want rusty-ryan", got.Entry.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChooserCandidatesFallBackToAllEntriesForUnknownAndroidApp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cache := File{
|
||||
Entries: []Entry{
|
||||
{
|
||||
ID: "basher-tarr",
|
||||
Title: "Bellagio Vault",
|
||||
Username: "basher",
|
||||
Password: "secret1",
|
||||
URL: "https://bellagio.example.invalid/login",
|
||||
Host: "bellagio.example.invalid",
|
||||
Path: []string{"Crew"},
|
||||
},
|
||||
{
|
||||
ID: "linus-caldwell",
|
||||
Title: "Bank Floor",
|
||||
Username: "linus",
|
||||
Password: "secret2",
|
||||
URL: "https://bank.example.invalid/sign-in",
|
||||
Host: "bank.example.invalid",
|
||||
Path: []string{"Operations"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got := ChooserCandidates(cache, "androidapp://com.samsung.android.shealth")
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len(ChooserCandidates()) = %d, want 2", len(got))
|
||||
}
|
||||
if got[0].ID != "linus-caldwell" || got[1].ID != "basher-tarr" {
|
||||
t.Fatalf("ChooserCandidates() = %#v, want title-sorted fallback candidates", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRememberBindingPersistsAndroidAppSelection(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
path := filepath.Join(t.TempDir(), "autofill-bindings.json")
|
||||
now := time.Date(2026, time.April, 13, 18, 0, 0, 0, time.UTC)
|
||||
|
||||
if err := RememberBinding(path, "androidapp://com.samsung.android.shealth", "saul-bloom", now); err != nil {
|
||||
t.Fatalf("RememberBinding() error = %v", err)
|
||||
}
|
||||
|
||||
got, err := ReadBindings(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadBindings() error = %v", err)
|
||||
}
|
||||
if got.UpdatedAt != now.UTC().Format(time.RFC3339) {
|
||||
t.Fatalf("UpdatedAt = %q, want %q", got.UpdatedAt, now.UTC().Format(time.RFC3339))
|
||||
}
|
||||
if got.Apps["androidapp://com.samsung.android.shealth"] != "saul-bloom" {
|
||||
t.Fatalf("binding = %#v, want samsung health -> saul-bloom", got.Apps)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@ package browserbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -28,19 +31,25 @@ const (
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Action string `json:"action"`
|
||||
BearerToken string `json:"bearerToken,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
EntryID string `json:"entryId,omitempty"`
|
||||
Action string `json:"action"`
|
||||
BearerToken string `json:"bearerToken,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
EntryID string `json:"entryId,omitempty"`
|
||||
Query string `json:"query,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
Path []string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
Matches []Match `json:"matches,omitempty"`
|
||||
Credential *Credential `json:"credential,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
Matches []Match `json:"matches,omitempty"`
|
||||
SearchResults []Match `json:"searchResults,omitempty"`
|
||||
Credential *Credential `json:"credential,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
@@ -77,11 +86,15 @@ type Connection struct {
|
||||
type Client interface {
|
||||
Status(context.Context) (*keepassgov1.GetSessionStatusResponse, error)
|
||||
FindBrowserLogins(context.Context, string) ([]*keepassgov1.BrowserLoginMatch, error)
|
||||
ListEntries(context.Context, []string, string) ([]*keepassgov1.Entry, error)
|
||||
GetBrowserCredential(context.Context, string, string) (*keepassgov1.GetBrowserCredentialResponse, error)
|
||||
UpsertEntry(context.Context, *keepassgov1.Entry) (*keepassgov1.Entry, error)
|
||||
}
|
||||
|
||||
type Browser string
|
||||
|
||||
type actionHandler func(context.Context, Client, Request, string) Response
|
||||
|
||||
const (
|
||||
BrowserFirefox Browser = "firefox"
|
||||
BrowserChrome Browser = "chrome"
|
||||
@@ -163,34 +176,70 @@ func HandleRequest(ctx context.Context, req Request, grpcAddr string, client Cli
|
||||
return Response{Success: false, Error: err.Error()}
|
||||
}
|
||||
action := strings.TrimSpace(req.Action)
|
||||
switch action {
|
||||
case "status":
|
||||
status, err := statusResponse(ctx, client, conn.GRPCAddress)
|
||||
if err != nil {
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: status, Version: responseVersion}
|
||||
case "find-logins":
|
||||
matches, err := findMatches(ctx, client, req.URL)
|
||||
if err != nil {
|
||||
if status := inferredActionStatus(conn.GRPCAddress, err); status != nil {
|
||||
return Response{Success: true, Status: status, Matches: nil, Version: responseVersion}
|
||||
}
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: availableStatus(conn.GRPCAddress), Matches: matches, Version: responseVersion}
|
||||
case "get-login":
|
||||
credential, err := loadCredential(ctx, client, req.EntryID, req.URL)
|
||||
if err != nil {
|
||||
if status := inferredActionStatus(conn.GRPCAddress, err); status != nil {
|
||||
return Response{Success: false, Error: err.Error(), Status: status}
|
||||
}
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(conn.GRPCAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: availableStatus(conn.GRPCAddress), Credential: credential, Version: responseVersion}
|
||||
default:
|
||||
handler, ok := actionHandlers[action]
|
||||
if !ok {
|
||||
return Response{Success: false, Error: fmt.Sprintf("unsupported action %q", action)}
|
||||
}
|
||||
return handler(ctx, client, req, conn.GRPCAddress)
|
||||
}
|
||||
|
||||
var actionHandlers = map[string]actionHandler{
|
||||
"status": handleStatusAction,
|
||||
"find-logins": handleFindLoginsAction,
|
||||
"search-logins": handleSearchLoginsAction,
|
||||
"get-login": handleGetLoginAction,
|
||||
"save-login": handleSaveLoginAction,
|
||||
}
|
||||
|
||||
func handleStatusAction(ctx context.Context, client Client, _ Request, grpcAddress string) Response {
|
||||
status, err := statusResponse(ctx, client, grpcAddress)
|
||||
if err != nil {
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(grpcAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: status, Version: responseVersion}
|
||||
}
|
||||
|
||||
func handleFindLoginsAction(ctx context.Context, client Client, req Request, grpcAddress string) Response {
|
||||
matches, err := findMatches(ctx, client, req.URL)
|
||||
if err != nil {
|
||||
if status := inferredActionStatus(grpcAddress, err); status != nil {
|
||||
return Response{Success: true, Status: status, Matches: nil, Version: responseVersion}
|
||||
}
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(grpcAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: availableStatus(grpcAddress), Matches: matches, Version: responseVersion}
|
||||
}
|
||||
|
||||
func handleSearchLoginsAction(ctx context.Context, client Client, req Request, grpcAddress string) Response {
|
||||
results, err := searchEntries(ctx, client, req.Query)
|
||||
if err != nil {
|
||||
if status := inferredActionStatus(grpcAddress, err); status != nil {
|
||||
return Response{Success: true, Status: status, SearchResults: nil, Version: responseVersion}
|
||||
}
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(grpcAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: availableStatus(grpcAddress), SearchResults: results, Version: responseVersion}
|
||||
}
|
||||
|
||||
func handleGetLoginAction(ctx context.Context, client Client, req Request, grpcAddress string) Response {
|
||||
credential, err := loadCredential(ctx, client, req.EntryID, req.URL)
|
||||
if err != nil {
|
||||
if status := inferredActionStatus(grpcAddress, err); status != nil {
|
||||
return Response{Success: false, Error: err.Error(), Status: status}
|
||||
}
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(grpcAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: availableStatus(grpcAddress), Credential: credential, Version: responseVersion}
|
||||
}
|
||||
|
||||
func handleSaveLoginAction(ctx context.Context, client Client, req Request, grpcAddress string) Response {
|
||||
if err := saveLogin(ctx, client, req); err != nil {
|
||||
if status := inferredActionStatus(grpcAddress, err); status != nil {
|
||||
return Response{Success: false, Error: err.Error(), Status: status}
|
||||
}
|
||||
return Response{Success: false, Error: err.Error(), Status: disconnectedStatus(grpcAddress)}
|
||||
}
|
||||
return Response{Success: true, Status: availableStatus(grpcAddress), Version: responseVersion}
|
||||
}
|
||||
|
||||
func disconnectedStatus(addr string) *Status {
|
||||
@@ -264,6 +313,113 @@ func loadCredential(ctx context.Context, client Client, entryID, rawURL string)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func saveLogin(ctx context.Context, client Client, req Request) error {
|
||||
if strings.TrimSpace(req.Password) == "" {
|
||||
return fmt.Errorf("browser save requires a password")
|
||||
}
|
||||
if strings.TrimSpace(req.EntryID) != "" {
|
||||
entries, err := client.ListEntries(ctx, nil, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing := findEntry(entries, req.EntryID)
|
||||
if existing == nil {
|
||||
return fmt.Errorf("entry %q was not found", strings.TrimSpace(req.EntryID))
|
||||
}
|
||||
entry := cloneEntry(existing)
|
||||
entry.Title = coalesceTitle(req.Title, existing.Title, req.URL)
|
||||
entry.Username = strings.TrimSpace(req.Username)
|
||||
entry.Password = strings.TrimSpace(req.Password)
|
||||
entry.Url = strings.TrimSpace(req.URL)
|
||||
_, err = client.UpsertEntry(ctx, entry)
|
||||
return err
|
||||
}
|
||||
path := append([]string(nil), req.Path...)
|
||||
if len(path) == 0 {
|
||||
return fmt.Errorf("browser save requires a target group path")
|
||||
}
|
||||
entry := &keepassgov1.Entry{
|
||||
Id: newBrowserEntryID(),
|
||||
Title: coalesceTitle(req.Title, "", req.URL),
|
||||
Username: strings.TrimSpace(req.Username),
|
||||
Password: strings.TrimSpace(req.Password),
|
||||
Url: strings.TrimSpace(req.URL),
|
||||
Path: path,
|
||||
Fields: map[string]string{},
|
||||
}
|
||||
_, err := client.UpsertEntry(ctx, entry)
|
||||
return err
|
||||
}
|
||||
|
||||
func findEntry(entries []*keepassgov1.Entry, id string) *keepassgov1.Entry {
|
||||
for _, entry := range entries {
|
||||
if entry.GetId() == strings.TrimSpace(id) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneEntry(entry *keepassgov1.Entry) *keepassgov1.Entry {
|
||||
if entry == nil {
|
||||
return &keepassgov1.Entry{Fields: map[string]string{}}
|
||||
}
|
||||
fields := make(map[string]string, len(entry.GetFields()))
|
||||
for key, value := range entry.GetFields() {
|
||||
fields[key] = value
|
||||
}
|
||||
return &keepassgov1.Entry{
|
||||
Id: entry.GetId(),
|
||||
Title: entry.GetTitle(),
|
||||
Username: entry.GetUsername(),
|
||||
Password: entry.GetPassword(),
|
||||
Url: entry.GetUrl(),
|
||||
Notes: entry.GetNotes(),
|
||||
Tags: append([]string(nil), entry.GetTags()...),
|
||||
Path: append([]string(nil), entry.GetPath()...),
|
||||
Fields: fields,
|
||||
}
|
||||
}
|
||||
|
||||
func coalesceTitle(title, fallback, rawURL string) string {
|
||||
if trimmed := strings.TrimSpace(title); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
if trimmed := strings.TrimSpace(fallback); trimmed != "" {
|
||||
return trimmed
|
||||
}
|
||||
if parsed, err := url.Parse(strings.TrimSpace(rawURL)); err == nil && strings.TrimSpace(parsed.Hostname()) != "" {
|
||||
return strings.ToLower(strings.TrimSpace(parsed.Hostname()))
|
||||
}
|
||||
return "Browser Login"
|
||||
}
|
||||
|
||||
func newBrowserEntryID() string {
|
||||
var buf [16]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return fmt.Sprintf("browser-%d", os.Getpid())
|
||||
}
|
||||
return hex.EncodeToString(buf[:])
|
||||
}
|
||||
|
||||
func searchEntries(ctx context.Context, client Client, query string) ([]Match, error) {
|
||||
resp, err := client.ListEntries(ctx, nil, strings.TrimSpace(query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]Match, 0, len(resp))
|
||||
for _, entry := range resp {
|
||||
out = append(out, Match{
|
||||
ID: entry.GetId(),
|
||||
Title: entry.GetTitle(),
|
||||
Username: entry.GetUsername(),
|
||||
URL: entry.GetUrl(),
|
||||
Path: append([]string(nil), entry.GetPath()...),
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func Manifest(browser Browser, binaryPath, extensionID string) (NativeHostManifest, error) {
|
||||
path := strings.TrimSpace(binaryPath)
|
||||
if path == "" {
|
||||
|
||||
@@ -149,6 +149,110 @@ func TestHandleRequestGetLogin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequestSearchLogins(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &fakeClient{
|
||||
entries: []*keepassgov1.Entry{
|
||||
{Id: "rusty-gitlab", Title: "Rusty GitLab", Username: "rustyryan", Url: "gitlab.com", Path: []string{"Joe", "Internet"}},
|
||||
},
|
||||
}
|
||||
resp := HandleRequest(context.Background(), Request{
|
||||
Action: "search-logins",
|
||||
BearerToken: "secret",
|
||||
Query: "GitLab",
|
||||
}, "", client)
|
||||
if !resp.Success {
|
||||
t.Fatalf("HandleRequest(search-logins) success = false, error = %q", resp.Error)
|
||||
}
|
||||
if len(resp.SearchResults) != 1 || resp.SearchResults[0].ID != "rusty-gitlab" {
|
||||
t.Fatalf("HandleRequest(search-logins).SearchResults = %#v, want rusty-gitlab", resp.SearchResults)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequestSaveLoginUpdatesExistingEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &fakeClient{
|
||||
entries: []*keepassgov1.Entry{
|
||||
{
|
||||
Id: "vault-console",
|
||||
Title: "Vault Console",
|
||||
Username: "dannyocean",
|
||||
Password: "old-password",
|
||||
Url: "https://vault.example.invalid/login",
|
||||
Path: []string{"Crew", "Internet"},
|
||||
Fields: map[string]string{
|
||||
"URL1": "vault.example.invalid",
|
||||
"X-Role": "inside-man",
|
||||
},
|
||||
Tags: []string{"vault"},
|
||||
Notes: "Original notes stay intact.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp := HandleRequest(context.Background(), Request{
|
||||
Action: "save-login",
|
||||
BearerToken: "secret",
|
||||
EntryID: "vault-console",
|
||||
Username: "dannyocean",
|
||||
Password: "new-password",
|
||||
URL: "https://vault.example.invalid/login",
|
||||
}, "", client)
|
||||
if !resp.Success {
|
||||
t.Fatalf("HandleRequest(save-login update) success = false, error = %q", resp.Error)
|
||||
}
|
||||
if client.upserted == nil {
|
||||
t.Fatal("HandleRequest(save-login update) did not upsert an entry")
|
||||
}
|
||||
if got := client.upserted.Id; got != "vault-console" {
|
||||
t.Fatalf("upserted.Id = %q, want vault-console", got)
|
||||
}
|
||||
if got := client.upserted.Password; got != "new-password" {
|
||||
t.Fatalf("upserted.Password = %q, want new-password", got)
|
||||
}
|
||||
if got := client.upserted.Fields["X-Role"]; got != "inside-man" {
|
||||
t.Fatalf("upserted.Fields[X-Role] = %q, want inside-man", got)
|
||||
}
|
||||
if got := client.upserted.Notes; got != "Original notes stay intact." {
|
||||
t.Fatalf("upserted.Notes = %q, want original notes", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequestSaveLoginCreatesNewEntryInChosenPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &fakeClient{}
|
||||
resp := HandleRequest(context.Background(), Request{
|
||||
Action: "save-login",
|
||||
BearerToken: "secret",
|
||||
Title: "Bellagio Login",
|
||||
Username: "linuscaldwell",
|
||||
Password: "yellow-chip",
|
||||
URL: "https://bellagio.example.invalid/login",
|
||||
Path: []string{"Crew", "Internet"},
|
||||
}, "", client)
|
||||
if !resp.Success {
|
||||
t.Fatalf("HandleRequest(save-login create) success = false, error = %q", resp.Error)
|
||||
}
|
||||
if client.upserted == nil {
|
||||
t.Fatal("HandleRequest(save-login create) did not upsert an entry")
|
||||
}
|
||||
if got := client.upserted.Title; got != "Bellagio Login" {
|
||||
t.Fatalf("upserted.Title = %q, want Bellagio Login", got)
|
||||
}
|
||||
if got := client.upserted.Username; got != "linuscaldwell" {
|
||||
t.Fatalf("upserted.Username = %q, want linuscaldwell", got)
|
||||
}
|
||||
if got := client.upserted.Path; !slices.Equal(got, []string{"Crew", "Internet"}) {
|
||||
t.Fatalf("upserted.Path = %v, want [Crew Internet]", got)
|
||||
}
|
||||
if got := client.upserted.Id; got == "" {
|
||||
t.Fatal("upserted.Id = empty, want generated id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRequestFindLoginsInfersLockedStatusFromRPC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -309,10 +413,14 @@ func TestEnsureNativeHostManifestsInstallsFirefoxAndDiscoveredChromium(t *testin
|
||||
type fakeClient struct {
|
||||
status *keepassgov1.GetSessionStatusResponse
|
||||
matches []*keepassgov1.BrowserLoginMatch
|
||||
entries []*keepassgov1.Entry
|
||||
credential *keepassgov1.GetBrowserCredentialResponse
|
||||
upserted *keepassgov1.Entry
|
||||
err error
|
||||
matchesErr error
|
||||
entriesErr error
|
||||
credentialErr error
|
||||
upsertErr error
|
||||
statusCalls int
|
||||
}
|
||||
|
||||
@@ -382,6 +490,16 @@ func (f *fakeClient) FindBrowserLogins(context.Context, string) ([]*keepassgov1.
|
||||
return f.matches, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) ListEntries(context.Context, []string, string) ([]*keepassgov1.Entry, error) {
|
||||
if f.entriesErr != nil {
|
||||
return nil, f.entriesErr
|
||||
}
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return f.entries, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) GetBrowserCredential(context.Context, string, string) (*keepassgov1.GetBrowserCredentialResponse, error) {
|
||||
if f.credentialErr != nil {
|
||||
return nil, f.credentialErr
|
||||
@@ -394,3 +512,11 @@ func (f *fakeClient) GetBrowserCredential(context.Context, string, string) (*kee
|
||||
}
|
||||
return f.credential, nil
|
||||
}
|
||||
|
||||
func (f *fakeClient) UpsertEntry(_ context.Context, entry *keepassgov1.Entry) (*keepassgov1.Entry, error) {
|
||||
if f.upsertErr != nil {
|
||||
return nil, f.upsertErr
|
||||
}
|
||||
f.upserted = entry
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
@@ -65,9 +65,28 @@ func (c *GRPCClient) FindBrowserLogins(ctx context.Context, pageURL string) ([]*
|
||||
return resp.GetMatches(), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListEntries(ctx context.Context, path []string, query string) ([]*keepassgov1.Entry, error) {
|
||||
resp, err := c.client.ListEntries(ctx, &keepassgov1.ListEntriesRequest{
|
||||
Path: append([]string(nil), path...),
|
||||
Query: strings.TrimSpace(query),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.GetEntries(), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) GetBrowserCredential(ctx context.Context, entryID, pageURL string) (*keepassgov1.GetBrowserCredentialResponse, error) {
|
||||
return c.client.GetBrowserCredential(ctx, &keepassgov1.GetBrowserCredentialRequest{
|
||||
Id: strings.TrimSpace(entryID),
|
||||
PageUrl: strings.TrimSpace(pageURL),
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GRPCClient) UpsertEntry(ctx context.Context, entry *keepassgov1.Entry) (*keepassgov1.Entry, error) {
|
||||
resp, err := c.client.UpsertEntry(ctx, &keepassgov1.UpsertEntryRequest{Entry: entry})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.GetEntry(), nil
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@ import "git.julianfamily.org/keepassgo/internal/vault"
|
||||
// HiddenRoot returns the single synthetic top-level vault group that should be
|
||||
// treated as an internal storage root rather than as a user-visible group.
|
||||
func HiddenRoot(model vault.Model) string {
|
||||
if !hasGroup(model.Groups, []string{KeepassRoot}) {
|
||||
return ""
|
||||
if hasGroup(model.Groups, []string{KeepassRoot}) {
|
||||
return KeepassRoot
|
||||
}
|
||||
return KeepassRoot
|
||||
if usesTopLevelRoot(model, KeepassRoot) {
|
||||
return KeepassRoot
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func hasGroup(groups [][]string, path []string) bool {
|
||||
|
||||
@@ -24,3 +24,20 @@ func TestHiddenRootIgnoresRecycleBin(t *testing.T) {
|
||||
t.Fatalf("HiddenRoot() = %q, want %q", got, "keepass")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHiddenRootFallsBackToEntryPathsWhenGroupsAreSparse(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
model := vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "rusty", Title: "Rusty GitLab", Path: []string{"keepass", "Joe", "Internet"}},
|
||||
},
|
||||
Groups: [][]string{
|
||||
{"Recycle Bin"},
|
||||
},
|
||||
}
|
||||
|
||||
if got := HiddenRoot(model); got != "keepass" {
|
||||
t.Fatalf("HiddenRoot() = %q, want %q", got, "keepass")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,16 @@ package() {
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/background.js"
|
||||
install -Dm644 browser/extension/content.js \
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/content.js"
|
||||
install -Dm644 browser/extension/icons/icon-16.png \
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/icons/icon-16.png"
|
||||
install -Dm644 browser/extension/icons/icon-32.png \
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/icons/icon-32.png"
|
||||
install -Dm644 browser/extension/icons/icon-48.png \
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/icons/icon-48.png"
|
||||
install -Dm644 browser/extension/icons/icon-96.png \
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/icons/icon-96.png"
|
||||
install -Dm644 browser/extension/icons/icon-128.png \
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/icons/icon-128.png"
|
||||
install -Dm644 browser/extension/manifest.chromium.json \
|
||||
"${pkgdir}/usr/share/keepassgo/browser-extension/manifest.chromium.json"
|
||||
install -Dm644 browser/extension/manifest.firefox.json \
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM golang:1.26-bookworm AS gobase
|
||||
|
||||
FROM eclipse-temurin:25-jdk
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
findutils \
|
||||
git \
|
||||
make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=gobase /usr/local/go /usr/local/go
|
||||
|
||||
ENV JAVA_HOME=/opt/java/openjdk
|
||||
ENV PATH=/usr/local/go/bin:${PATH}
|
||||
|
||||
WORKDIR /workspace
|
||||
@@ -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