Compare commits

..
Author SHA1 Message Date
7a06fb1e5f Beta/v2.0.8 (#2992)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: FandacqkaCorp <david@davidyloreto.com>
Co-authored-by: FandacqkaCorp <275647464+FandacqkaCorp@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:25:55 -05:00
Graham Steffaniak 45cd854ed7 fix make dev build error (#2970) 2026-09-18 09:29:19 -05:00
ce066e5d5b Beta/v2.0.7 (#2967)
Co-authored-by: Matt Van Horn <mvanhorn@users.noreply.github.com>
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: zumbiepig <121742281+zumbiepig@users.noreply.github.com>
2026-09-17 17:31:22 -05:00
175 changed files with 15738 additions and 1989 deletions

No files matched your search

+16
View File
@@ -88,6 +88,8 @@ jobs:
dockerfile: ./_docker/Dockerfile.playwright-previews
- name: jwt
dockerfile: ./_docker/Dockerfile.playwright-jwt
- name: performance
dockerfile: ./_docker/Dockerfile.playwright-performance
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -124,6 +126,20 @@ jobs:
type=gha,scope=main-playwright-base
cache-to: |
type=gha,mode=max,scope=${{ github.ref_name }}-playwright-base
- name: Extract listing performance results
if: matrix.name == 'performance' && success()
run: |
mkdir -p perf-artifact
cid=$(docker create "${{ steps.build.outputs.imageid }}")
docker cp "$cid:/app/frontend/test-results/listing-performance-perf/results.json" perf-artifact/results.json
docker rm "$cid"
- name: Upload listing performance results
if: matrix.name == 'performance' && success()
uses: actions/upload-artifact@v4
with:
name: listing-performance-results
path: perf-artifact/results.json
retention-days: 7
push_pr_to_registry:
name: Build docker image
runs-on: ubuntu-24.04-arm
+4 -2
View File
@@ -22,8 +22,10 @@ jobs:
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- working-directory: backend
run: go tool golangci-lint run --path-prefix=backend
- name: Lint backend
run: |
GOLANGCI_LINT="$(cd backend/build && go tool -n golangci-lint)"
cd backend && "$GOLANGCI_LINT" run --path-prefix=backend
format-backend:
runs-on: ubuntu-24.04-arm
steps:
+11 -1
View File
@@ -6,6 +6,7 @@
*.bak
*.log
*.mjs
!frontend/scripts/serve-perf-dashboard.mjs
_old
rice-box.go
.idea/
@@ -19,7 +20,11 @@ frontend/tsconfig.tsbuildinfo
/backend/dist
/frontend/dist
/frontend/pkg
/frontend/.cache
/frontend/test-results
# Playwright's default HTML report output dir (written when a config enables the
# "html" reporter). Generated output, never committed.
/frontend/playwright-report
/frontend/playwright-files
/frontend/screenshots
/frontend/generated
@@ -27,6 +32,11 @@ frontend/.venv-icons
frontend/.tmp-icons-*
/frontend/tests/playwright-files/demo*
/frontend/loginAuth.json
/frontend/tests/playwright/performance/results.json
# Generated perf report artifacts. NOTE: perf-baseline.json is intentionally NOT
# ignored — it is the committed chromium baseline that CI gates against.
/frontend/tests/playwright/performance/report.json
/frontend/tests/playwright/performance/report.md
/frontend/sharePrepStorage.json
/backend/vendor
/backend/*.cov
@@ -58,7 +68,7 @@ yarn-error.log*
*.sln
*.sw*
bin/
build/
/build/
*__debug*
backend/custom.css
+20 -1
View File
@@ -2,8 +2,27 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v2.0.7
## v2.0.8
**Security**:
- [Critical] A forged JWT could authenticate as any known `belongsTo`. Auth signing keys are now persisted in the application database and JWT validation fails closed when no key is configured. (GHSA-8f9r-wg7w-pfw) (#2987) Thanks @d3do-23 and @whoamis3c.
- [Medium] Public upload shares with replacements disabled (`allowReplacements=false`) now reject overwrites when clients send `override=true`. (GHSA-3846-gh75-gp3m) Thanks @d3do-23
- [Medium] OnlyOffice document-server callbacks now verify the shared `integrations.office.secret` JWT (HS256), reject unsigned callback bodies when a secret is configured, and require the callback document key to match the cached editor session for the target path (fail closed on cache miss or path lookup failure).
**New Features**:
- add "Upload only what's missing" to the upload conflict prompt (#2985) (#2553)
**Notes**:
- Improved UI responsiveness for larger directories and firefox, marginal improvement to memory (#1773) (#2879)
- `/api/resources/download` and `/public/api/resources/download` now return HTTP 404 when the requested file or directory does not exist, instead of 500 (#2981)
- `GET /api/resources` returns 400 when the `path` query parameter is missing or empty instead of 500 (#2801)
**Bugfixes**:
- Public share folder and multi-file ZIP downloads were empty for anonymous visitors when the source used deny-by-default or path access rules (#2631) (#2365)
- 401 on pdf download button action (#2978)
- OnlyOffice saves could no-op with HTTP 200 when JWT was enabled: callback POST bodies wrapped in `token` were ignored and JWT parsing omitted fields such as `url` required to download the updated document from the Document Server.
## v2.0.7
**New Features**:
- Add cli init CLI command (#2957)
+3 -1
View File
@@ -1,5 +1,7 @@
FROM node:22-slim
WORKDIR /app/frontend
RUN npm i @playwright/test oauth2-mock-server @types/node
COPY frontend/package-lock.json /tmp/package-lock.json
RUN PW_VERSION=$(node -p "require('/tmp/package-lock.json').packages['node_modules/@playwright/test'].version") \
&& npm i "@playwright/test@${PW_VERSION}" oauth2-mock-server @types/node
RUN npx playwright install --with-deps chromium
+12
View File
@@ -0,0 +1,12 @@
# Local listing-performance only (see docker-compose local-playwright-performance).
# CI uses ghcr.io/gtsteffaniak/playwright-base:chromium + Dockerfile.playwright-performance.
FROM node:22-slim
WORKDIR /app/frontend
COPY frontend/package-lock.json /tmp/package-lock.json
RUN PW_VERSION=$(node -p "require('/tmp/package-lock.json').packages['node_modules/@playwright/test'].version") \
&& npm i "@playwright/test@${PW_VERSION}"
ARG PLAYWRIGHT_BROWSERS=chromium,firefox,webkit
ENV PLAYWRIGHT_BROWSERS=${PLAYWRIGHT_BROWSERS}
RUN bash -ec 'set -euo pipefail; for b in ${PLAYWRIGHT_BROWSERS//,/ }; do npx playwright install --with-deps "$b"; done'
+52
View File
@@ -0,0 +1,52 @@
# CI: same ghcr.io playwright-base:chromium + GHA cache as screenshots/previews (chromium only).
# Local all-browser runs: make playwright-perf-base + compose PLAYWRIGHT_BASE_IMAGE.
ARG PLAYWRIGHT_BASE_IMAGE=ghcr.io/gtsteffaniak/playwright-base:chromium
FROM ${PLAYWRIGHT_BASE_IMAGE} AS perf
WORKDIR /app
COPY [ "./_docker/src/performance/", "./" ]
WORKDIR /app/frontend
COPY [ "./frontend/tests/", "./tests" ]
COPY [ "./frontend/playwright.performance.config.ts", "./playwright.config.ts" ]
# The base image's /app/frontend/package.json is generated by `npm i` and has no
# "type" field, so /app/frontend is a CommonJS package. Playwright transpiles the
# suite accordingly (emitting `exports.x = ...`), but its ESM loader reparses these
# files as ES modules because their syntax is unambiguous ESM. The result is
# "ReferenceError: exports is not defined in ES module scope" before a single test
# runs. The perf harness is real ESM, so declare it as such — this matches
# frontend/package.json in the repo (also "type": "module").
RUN node -e 'const fs=require("fs");const p="package.json";const j=JSON.parse(fs.readFileSync(p,"utf8"));j.type="module";fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n")'
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
ENV CI=true
ENV PERF_BASE_URL=http://127.0.0.1/
ENV PERF_SCALES=100,1000,10000
ENV PERF_WORKERS=6
ENV PERF_TEST_TIMEOUT_MS=480000
# CI runs a single pass per scenario (N=1) for fast PR feedback; local runs
# default to N=3 and take the median. The baseline records the worker count and
# this setting, so an incomparable comparison is reported rather than silently
# producing a bogus regression.
ENV PERF_REPEATS=1
# Stamped into the baseline environment fingerprint so a mismatch is detectable.
ENV PERF_IMAGE_TAG=playwright-base-chromium
# Lets the harness report container-specific remedies (e.g. rebuild the base
# image) instead of host-oriented advice when a browser is missing.
ENV PERF_IN_CONTAINER=1
# CI / make test-playwright-performance (chromium only — same base as other Playwright images)
FROM perf AS ci
ENV PERF_BROWSERS=chromium
# The committed baseline (frontend/tests/playwright/performance/perf-baseline.json)
# is copied in by the `COPY ./frontend/tests/` layer above. This step exits
# non-zero when a gated metric regresses beyond its tolerance, which is what
# makes the performance check gate the PR.
RUN ./filebrowser & sleep 5 && cd ../frontend && npx playwright test -c playwright.config.ts
+36
View File
@@ -14,6 +14,42 @@ services:
build:
dockerfile: ./_docker/Dockerfile.playwright-screenshots
context: ../
local-playwright-performance:
container_name: local-playwright-performance
working_dir: /app/backend
volumes:
- ../frontend/test-results/playwright-performance:/app/frontend/test-results/playwright-performance
- ../frontend/test-results/listing-performance-perf:/app/frontend/test-results/listing-performance-perf
- ../frontend/tests/playwright/performance:/out/report
environment:
PERF_RESULTS_DIR: /app/frontend/test-results/listing-performance-perf
PERF_REPORT_DIR: /out/report
PERF_GREP: ${PERF_GREP:-}
PERF_BROWSERS: ${PERF_BROWSERS:-}
# Local runs repeat each scenario and take the median to average out noise.
# CI overrides this to 1 for fast feedback.
PERF_REPEATS: ${PERF_REPEATS:-3}
# Set to 1 to rewrite the committed chromium baseline from this run.
PERF_UPDATE_BASELINE: ${PERF_UPDATE_BASELINE:-0}
# Local comparisons are advisory unless explicitly enforced.
PERF_ENFORCE_BASELINE: ${PERF_ENFORCE_BASELINE:-0}
PERF_IMAGE_TAG: ${PERF_IMAGE_TAG:-local}
PERF_IN_CONTAINER: "1"
# When PERF_UPDATE_BASELINE=1, write through the mounted report dir (host:
# frontend/tests/playwright/performance/perf-baseline.json).
PERF_BASELINE_PATH: /out/report/perf-baseline.json
entrypoint:
[
"/bin/sh",
"-c",
"./filebrowser & sleep 5 && cd ../frontend && npx playwright test -c playwright.config.ts $${PERF_GREP:+-g \"$$PERF_GREP\"}",
]
build:
dockerfile: ./_docker/Dockerfile.playwright-performance
target: perf
context: ../
args:
PLAYWRIGHT_BASE_IMAGE: ${PLAYWRIGHT_BASE_IMAGE:-filebrowser-playwright-perf-base:latest}
nginx-proxy-auth:
image: nginx
container_name: nginx-proxy-auth
@@ -0,0 +1,19 @@
server:
<<: *server-main
http:
<<: *http-main
frontend:
<<: *frontend-main
userDefaults:
preview:
image: true
popup: true
video: true
audio: true
models: true
office: true
highQuality: true
account:
permissions:
share: true
@@ -0,0 +1,13 @@
frontend-main: &frontend-main
name: "Graham's Filebrowser"
disableDefaultLinks: true
styling:
disableEventThemes: true
customThemes:
no-rounded:
description: "No rounded corners" # The description of the theme to display in the UI.
css: "no-rounded.css" # The css file path and filename to use for the theme.
externalLinks:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
@@ -0,0 +1,24 @@
http-main: &http-main
port: 80
baseURL: "/"
server-main: &server-main
logging:
- levels: "warning|error"
apiLevels: disabled
sources:
- path: "../frontend/tests/playwright-files"
name: "playwright + files"
config:
defaultEnabled: true
defaultPermissions:
view: true
download: true
modify: true
create: true
- path: "."
name: "docker"
config:
defaultEnabled: true
rules:
- folderPath: "/"
viewable: true
+252
View File
@@ -0,0 +1,252 @@
module github.com/gtsteffaniak/filebrowser/backend/build
go 1.27.0
require (
4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
charm.land/lipgloss/v2 v2.0.6 // indirect
codeberg.org/chavacava/garif v0.2.1 // indirect
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/exhaustruct/v5 v5.2.0 // indirect
dev.gaijin.team/go/golib v0.8.1 // indirect
github.com/4meepo/tagalign v1.4.4 // indirect
github.com/Abirdcfly/dupword v0.1.8 // indirect
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
github.com/AlwxSin/noinlineerr v1.0.6 // indirect
github.com/Antonboom/errname v1.1.2 // indirect
github.com/Antonboom/nilnil v1.1.2 // indirect
github.com/Antonboom/testifylint v1.6.4 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect
github.com/Djarvur/go-err113 v0.1.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/air-verse/air v1.67.4 // indirect
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
github.com/alecthomas/go-check-sumtype v0.4.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.2.0 // indirect
github.com/andybalholm/brotli v1.2.4 // indirect
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bep/godartsass/v2 v2.5.0 // indirect
github.com/bep/golibsass v1.2.0 // indirect
github.com/bits-and-blooms/bitset v1.25.0 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
github.com/bombsimon/wsl/v5 v5.9.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
github.com/butuzov/ireturn v0.4.1 // indirect
github.com/butuzov/mirror v1.3.3 // 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.11 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 // indirect
github.com/charmbracelet/x/ansi v0.11.8 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.14.0 // indirect
github.com/dave/dst v0.28.0 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dlclark/regexp2/v2 v2.8.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.8 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v1.0.1 // indirect
github.com/go-critic/go-critic v0.15.0 // indirect
github.com/go-openapi/jsonpointer v1.0.1 // indirect
github.com/go-openapi/jsonreference v1.0.2 // indirect
github.com/go-openapi/spec v1.0.1 // indirect
github.com/go-openapi/swag/conv v0.29.2 // indirect
github.com/go-openapi/swag/jsonutils v0.29.2 // indirect
github.com/go-openapi/swag/loading v0.29.2 // indirect
github.com/go-openapi/swag/pools v0.29.2 // indirect
github.com/go-openapi/swag/stringutils v0.29.2 // indirect
github.com/go-openapi/swag/typeutils v0.29.2 // indirect
github.com/go-openapi/swag/yamlutils v0.29.2 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
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.5.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godoc-lint/godoc-lint v0.11.4 // indirect
github.com/gofrs/flock v0.13.1 // indirect
github.com/gohugoio/hashstructure v1.1.0 // indirect
github.com/gohugoio/hugo v0.165.0 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
github.com/golangci/go-printf-func-name v0.1.1 // indirect
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
github.com/golangci/golangci-lint/v2 v2.13.2 // 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/rowserrcheck v0.0.0-20260901154044-241134db7241 // 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.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.2 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.11.0 // indirect
github.com/jjti/go-spancheck v0.6.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
github.com/kisielk/errcheck v1.20.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.15 // indirect
github.com/ldez/exptostd v0.4.5 // indirect
github.com/ldez/gomoddirectives v0.10.0 // indirect
github.com/ldez/grignotin v0.10.1 // indirect
github.com/ldez/structtags v0.6.1 // indirect
github.com/ldez/tagliatelle v0.8.0 // indirect
github.com/ldez/usetesting v0.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
github.com/manuelarte/funcorder v0.6.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.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.30 // indirect
github.com/mgechev/revive v1.16.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.13.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/prometheus/client_golang v1.24.1 // indirect
github.com/prometheus/client_model v0.6.3 // indirect
github.com/prometheus/common v0.71.0 // indirect
github.com/prometheus/procfs v0.22.0 // 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.3.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.16.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
github.com/securego/gosec/v2 v2.29.0 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.5.1 // indirect
github.com/sourcegraph/go-diff v0.9.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.12.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/tetafro/godot v1.5.6 // indirect
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
github.com/timonwong/loggercheck v0.12.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/urfave/cli/v2 v2.27.7 // indirect
github.com/uudashr/gocognit v1.2.1 // indirect
github.com/uudashr/iface v1.5.1 // indirect
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // 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.14.0 // indirect
go-simpler.org/sloglint v0.12.0 // indirect
go.augendre.info/arangolint v0.4.0 // indirect
go.augendre.info/fatcontext v0.10.1 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba // indirect
golang.org/x/mod v0.41.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/tools v0.50.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.8.1 // indirect
mvdan.cc/gofumpt v0.12.0 // indirect
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
tool (
github.com/air-verse/air
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
github.com/swaggo/swag/cmd/swag
)
// air (via Hugo) and golangci-lint need different github.com/gobwas/glob APIs; pin Hugo 0.164 + glob 0.2.3.
exclude (
github.com/alecthomas/go-check-sumtype v0.5.0
github.com/gobwas/glob v1.0.0
github.com/gohugoio/hugo v0.166.0
)
+736
View File
@@ -0,0 +1,736 @@
4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM=
codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84=
codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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/exhaustruct/v5 v5.2.0 h1:HZB49Fshn8qHJCi9VzYtrcnR1jbLteRHXfnQz8Adams=
dev.gaijin.team/go/exhaustruct/v5 v5.2.0/go.mod h1:5IshpqhgGdKIrF8lBmi3M6YKmI7T3hMzAL9nrflko88=
dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
github.com/4meepo/tagalign v1.4.4 h1:052JDGj2pBORc6NkD30dDXpsUpoYszDeUwr9/5YZuVA=
github.com/4meepo/tagalign v1.4.4/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
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.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8=
github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
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/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck=
github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
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/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PFL3qh0=
github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.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/air-verse/air v1.67.4 h1:pkWBQues0094VMgUZ9MboNfS1vyCBEXeU6QxuD6/PwU=
github.com/air-verse/air v1.67.4/go.mod h1:XumHRkXbIg8CU5FqhQHgQC4oxQW/OQLwUMQAWln8cZs=
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.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/go-check-sumtype v0.4.0 h1:gd4mE4QsA62sXlnFIs8GUv8/0xH3DOUqTkYQR1l2v28=
github.com/alecthomas/go-check-sumtype v0.4.0/go.mod h1:aYYllI8IXNjo7ZYUzI2+wyLRECMg3zo+8CZj9JTBAWU=
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/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.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
github.com/andybalholm/brotli v1.2.4 h1:649LN6qF+/7JJYNN3Wdu4Dt5PS6eC/yP/Go3Bb0/AHI=
github.com/andybalholm/brotli v1.2.4/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
github.com/bep/golocales v0.2.0 h1:4H1H5UPw3ainpj5zykeEfiMRQngyaIC/t+I4Dvn+fvE=
github.com/bep/golocales v0.2.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE=
github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0=
github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU=
github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY=
github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps=
github.com/bep/imagemeta v1.0.0 h1:8JZmahegn5p1M9CpFmga7LoSxErpu1uFfpSzMUNju7A=
github.com/bep/imagemeta v1.0.0/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
github.com/bep/lazycache v0.8.1 h1:ko6ASLjkPxyV5DMWoNNZ8B2M0weyjqXX8IZkjBoBtvg=
github.com/bep/lazycache v0.8.1/go.mod h1:pbEiFsZoq7cLXvrTll0AHOPEurB1aGGxx4jKjOtlx9w=
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
github.com/bep/overlayfs v0.11.0 h1:aymHDGC0CHpvn0XvTfgpK6skCp16oMi+tdUF32l6pPs=
github.com/bep/overlayfs v0.11.0/go.mod h1:L+ggdoKm+Y7Xb4a1osd+/LOPG4qsY62snqRqJH5Mspc=
github.com/bep/textandbinarywriter v0.1.0 h1:KXmXsRN2Uhwhm1G3e/snM8+5SPQBJrCEpIosdIBR3po=
github.com/bep/textandbinarywriter v0.1.0/go.mod h1:dAcHveajlWWU7PXhp6Dn4PIAYDg2H13Huif9xMS2w8w=
github.com/bep/tmc v0.6.0 h1:5zWy4L+3gS+Kk8czzLC4g7ETaC3wkX9ZtTRdAdL8V4s=
github.com/bep/tmc v0.6.0/go.mod h1:SNHxc3o2WSNMAYqJcAO0rxFY+pbhZzMwjIHe5xaAue0=
github.com/bits-and-blooms/bitset v1.25.0 h1:0Ro0qF4abCkM6SqWPVj29sFhAbMPAZpaDD7xhJ10beM=
github.com/bits-and-blooms/bitset v1.25.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
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.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.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU=
github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM=
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.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
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/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.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 h1:/hSCZO1puetbGd2JikHKN0nAcb2mFevzxJElFoduhaw=
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16/go.mod h1:JFvmQPZh3uAu7PpSZU91bcR0BrIy24HhW+0EsW1uGxE=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/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.14.0 h1:h6AcLqmjIOBgojhtzY2CvBnA6RawPTkBHgtMvYD5YZ8=
github.com/daixiang0/gci v0.14.0/go.mod h1:w9E+SWQ4aPQ+xYPUdqitGDoXpT4mayqOfk7Szz2U6wQ=
github.com/dave/dst v0.28.0 h1:EIEuRC5XmVKuZF2OeyMahXX9ARVt6Xxr8vI0WtIY7K8=
github.com/dave/dst v0.28.0/go.mod h1:C4mfimBeSHfsgskhR/mUYSE05s2ih+PW6g9FP3c49tE=
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/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.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw=
github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
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/evanw/esbuild v0.28.2 h1:A2uETn4jrQTcXaT/shwTDTYBxDjl7fV7nXmUrJxfA2w=
github.com/evanw/esbuild v0.28.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
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.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II=
github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo=
github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
github.com/ghostiam/protogetter v1.0.1 h1:q4LLTx2KJHJpxrfO3BniUDWoFkoqWD/UlHhClBlUlLM=
github.com/ghostiam/protogetter v1.0.1/go.mod h1:50Fh5yZfs6SFGrd1FlmT04NcguyEOxyjNK5zGD6/EvM=
github.com/go-critic/go-critic v0.15.0 h1:MDUZIQKytIl+vPEc0Igw51m4uLTdi1ZGz2jatEqNs1g=
github.com/go-critic/go-critic v0.15.0/go.mod h1:3BPrw8nHATGm3o2n37cssOmJzpySGKCdY8JWkCzxgKQ=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc=
github.com/go-openapi/jsonreference v1.0.2/go.mod h1:TbUNSOo+fcorZjFaNoiSDSoaNnnZtqJtLGR1PuvE/Cs=
github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk=
github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0=
github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q=
github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k=
github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM=
github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY=
github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI=
github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c=
github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc=
github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk=
github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI=
github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I=
github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE=
github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM=
github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs=
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs=
github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8=
github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=
github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
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.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/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godoc-lint/godoc-lint v0.11.4 h1:N9zUYVJ/OkznBbrBsU/zODvua64EUplMBLQWStO3+1M=
github.com/godoc-lint/godoc-lint v0.11.4/go.mod h1:7usTXoxS0QYIi5usTSCPwR0w/6xLjcz450shXjLn3t4=
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
github.com/gohugoio/gift v0.2.0 h1:vA31pP0rTVmBxBrhpY3WEt+4zM4g+1sDqYeemwsYeqc=
github.com/gohugoio/gift v0.2.0/go.mod h1:1Mrm5CjF33KpD749Dwj+UAjWZ3LC6cBXGuTMa5XwoP4=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM=
github.com/gohugoio/hashstructure v1.1.0 h1:38yUfZBca6qXSbUpteLhjDGLNskclHaguFBYpjaRjf4=
github.com/gohugoio/hashstructure v1.1.0/go.mod h1:Pz8dcwjZs6FBKWu9x/ZIChrTHIM175zfUJK0KLvC1z8=
github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/SficorCU=
github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
github.com/gohugoio/hugo v0.165.0 h1:E9uRFnDw3E+Nv69OU8vsD1YLI3+tRcSHhs82y4fBXdc=
github.com/gohugoio/hugo v0.165.0/go.mod h1:mOASrT3UGLvAXPdRuY6zUH9SoKBy7VoMQIzMFf4CMi8=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0/go.mod h1:9LJNfKWFmhEJ7HW0in5znezMwH+FYMBIhNZ3VWtRcRs=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJGtbtlgkYNCs4TnIlZJh8vHgnAiofrI=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0/go.mod h1:ob9PCHy/ocsQhTz68uxhyInaYCbbVNpOOrJkIoSeD+8=
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/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4=
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
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-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg=
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA=
github.com/golangci/golangci-lint/v2 v2.13.2 h1:bCyq3E4vo9qwzifjpzJqYndEt7Ncva80qkk8G2B4TXU=
github.com/golangci/golangci-lint/v2 v2.13.2/go.mod h1:5xaMd1kAxV7GSBPEyngw8gnnjoRocqW9UVKdBR6627w=
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/rowserrcheck v0.0.0-20260901154044-241134db7241 h1:0kPqfqytQizeQ9WBk487NcsGcRdS8YhHeUhXxkfEAhA=
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241/go.mod h1:lczq5oJ8PxpMESQG6dO3iFYTQPhpqoMOYw//n2FEnHA=
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/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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.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=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
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.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.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=
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
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.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
github.com/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.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/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
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.10.0 h1:Zb23rfMMkG21B6pZ9OfESzemByP5Qr7YvWcXQZE/S3Q=
github.com/ldez/gomoddirectives v0.10.0/go.mod h1:W9uh0721Vc7aaVoiMXTpKIveLmtHC9uaOSiHM4OK5a8=
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.8.0 h1:QczqVPa22JdT40p3m5JTTVmPMv5wDORqdomJn6/Pnn0=
github.com/ldez/tagliatelle v0.8.0/go.mod h1:K9n83Wv0wyVq3JYF9WI28BX8xXhDBVP3thHJ0ENuiEs=
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/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
github.com/lucasb-eyer/go-colorful v1.4.1/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/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
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.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
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/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
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=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mgechev/revive v1.16.0 h1:9OZVO8xOPUwIYynplHXv7liMllPQ/BTNZLp07sM/Im0=
github.com/mgechev/revive v1.16.0/go.mod h1:nT854jMmvQsS/qqxEaMPM4pJSUpxhcPoHM788ZxDvdQ=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
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.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
github.com/nishanths/exhaustive v0.13.0 h1:Rof0axOSNQaaPaPmLK5RxcRrJV2Vcv+vfUmuRy+2EAM=
github.com/nishanths/exhaustive v0.13.0/go.mod h1:3/adQjJgvD/UFHHSYBjpK9Wbz8DOvf0FBtzx+zI0Rlo=
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.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8=
github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
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=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
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.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
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=
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
github.com/raeperd/recvcheck v0.3.1 h1:p7nZMxiCWkdUtY2i3o+pv1h4fJCR5gpvqAXr9326hEY=
github.com/raeperd/recvcheck v0.3.1/go.mod h1:CbE7iRocQ4uc71JwpW/CPSh4BShnkre4A9Gb508RypU=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA=
github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
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.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/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.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
github.com/securego/gosec/v2 v2.29.0 h1:pF2HSLcnY5voqpxQumEe0O5YAAbvF4Syu4qeIewpLyc=
github.com/securego/gosec/v2 v2.29.0/go.mod h1:47C7ej+L2bC1NOxteU2aCGnCi4cZaoKzRt5y9plIvuY=
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/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
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/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.9.0 h1:7xcVsayWHhvxZGrqt1o89vvBApCa4nyCgO7tNxDHJ+Q=
github.com/sourcegraph/go-diff v0.9.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
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.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.5/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.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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.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.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
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.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tdewolff/minify/v2 v2.24.16 h1:1d+IHElvmYM2zJtuykMK/esiqc4H/7pqPKAa7jpSUUU=
github.com/tdewolff/minify/v2 v2.24.16/go.mod h1:G6yep6HFo3u2eoOcIVBKSNwpoW2wXvPjvnFRj3w1zlc=
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
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.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g=
github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs=
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/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
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.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA=
github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw=
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4=
github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
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=
github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
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.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
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.10.1 h1:HCS/+eob32cM0mEtmZM6NiXdqvS5KiZmsR+fCubTpbc=
go.augendre.info/fatcontext v0.10.1/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
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.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
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/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-20260908205506-85c1c2202aba h1:ZoZmFkcuQ289xJilBm9Ck3W+/okrhXc4LUpbtKFQE4k=
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba/go.mod h1:qkMnvgP8C7+xkci04txL2SJFc/0PGqnkf6gs5JUC43E=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
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=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w=
honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+30 -3
View File
@@ -1,7 +1,9 @@
package cmd
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -170,6 +172,12 @@ func migrateFromBoltToSQLite() error {
}
}()
// Migrate auth signing key from legacy Bolt settings
logger.Info("Migrating auth signing key...")
if err := migrateAuthSigningKey(oldDB, sqlStore); err != nil {
return fmt.Errorf("failed to migrate auth signing key: %w", err)
}
// Migrate users (bolt User.ID is stored as SQLite users.user_id)
logger.Info("Migrating users...")
if err := migrateUsers(oldDB, sqlStore); err != nil {
@@ -209,6 +217,22 @@ func migrateFromBoltToSQLite() error {
return nil
}
func migrateAuthSigningKey(oldDB *storm.DB, sqlStore *sqldb.SQLStore) error {
key, err := state.AuthSigningKeyFromBoltDB(oldDB)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
logger.Info(" No legacy auth signing key in Bolt settings")
return nil
}
return fmt.Errorf("read legacy auth signing key: %w", err)
}
if err := sqlStore.SaveSetting("auth.signingKey", base64.StdEncoding.EncodeToString([]byte(key))); err != nil {
return fmt.Errorf("save auth signing key: %w", err)
}
logger.Info(" ✓ Migrated auth signing key from legacy database")
return nil
}
// normalizeUserScopesBeforeSQLite copies scopes from FrontendScopes into BackendScopes when the Bolt
// record only had JSON key "scopes" (FrontendUser) and nothing under backendScopes — CreateUser
// persists only BackendScopes in SQLite user_data.
@@ -283,6 +307,9 @@ func migrateUsers(oldDB *storm.DB, sqlStore *sqldb.SQLStore) error {
if err := sqlStore.CreateUser(user); err != nil {
return fmt.Errorf("failed to save user %s (bolt id: %d): %w", user.Username, boltID, err)
}
if err := state.BackfillUserTokenHashesOnStore(sqlStore, user); err != nil {
return fmt.Errorf("failed to backfill token hashes for user %s: %w", user.Username, err)
}
}
if promoted > 0 {
@@ -436,20 +463,20 @@ func migrateAccessRules(oldDB *storm.DB, sqlStore *sqldb.SQLStore) error {
// Migrate revoked tokens
for tokenHash := range storage.RevokedTokens {
err := sqlStore.SaveRevokedToken(tokenHash)
err := sqlStore.SaveRevokedToken(tokenHash, 0)
if err != nil {
return fmt.Errorf("failed to save revoked token: %w", err)
}
}
logger.Infof(" ✓ Migrated %d revoked tokens", len(storage.RevokedTokens))
// Migrate hashed tokens (bolt stored owner user id)
// Migrate hashed tokens (bolt stored owner user id); legacy rows are named API tokens.
for tokenHash, userID := range storage.HashedTokens {
if userID == 0 {
logger.Warningf(" skipping hashed token: invalid user id 0")
continue
}
err := sqlStore.SaveHashedToken(tokenHash, uint64(userID))
err := sqlStore.SaveHashedToken(tokenHash, uint64(userID), false)
if err != nil {
return fmt.Errorf("failed to save hashed token: %w", err)
}
@@ -0,0 +1,23 @@
http:
port: 8080
baseURL: "/"
server:
logging:
- levels: "info|warning|error"
apiFilter: "disabled"
sources:
- path: "../frontend/tests/playwright-files"
name: "playwright + files"
config:
defaultEnabled: true
defaultPermissions:
view: true
download: true
modify: true
create: true
auth:
methods:
password:
enabled: true
minLength: 5
signup: false
+6 -219
View File
@@ -19,7 +19,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/gtsteffaniak/go-cache v1.1.0
github.com/gtsteffaniak/go-ffmpeg v0.5.2
github.com/gtsteffaniak/go-ffmpeg v1.0.0
github.com/gtsteffaniak/go-logger v1.1.0
github.com/kovidgoyal/imaging v1.8.23
github.com/mattn/go-sqlite3 v1.14.52
@@ -42,89 +42,16 @@ require (
)
require (
4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
charm.land/lipgloss/v2 v2.0.6 // indirect
codeberg.org/chavacava/garif v0.2.1 // indirect
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/exhaustruct/v5 v5.2.0 // indirect
dev.gaijin.team/go/golib v0.8.1 // indirect
github.com/4meepo/tagalign v1.4.4 // indirect
github.com/Abirdcfly/dupword v0.1.8 // indirect
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
github.com/AlwxSin/noinlineerr v1.0.6 // indirect
github.com/Antonboom/errname v1.1.2 // indirect
github.com/Antonboom/nilnil v1.1.2 // indirect
github.com/Antonboom/testifylint v1.6.4 // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect
github.com/Djarvur/go-err113 v0.1.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/MirrexOne/unqueryvet v1.5.4 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/air-verse/air v1.67.4 // indirect
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
github.com/alecthomas/go-check-sumtype v0.4.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.2.0 // indirect
github.com/andybalholm/brotli v1.2.4 // indirect
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bep/godartsass/v2 v2.5.0 // indirect
github.com/bep/golibsass v1.2.0 // indirect
github.com/bep/helpers v0.12.0 // indirect
github.com/bits-and-blooms/bitset v1.25.0 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
github.com/bombsimon/wsl/v5 v5.9.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
github.com/butuzov/ireturn v0.4.1 // indirect
github.com/butuzov/mirror v1.3.3 // 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.11 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 // indirect
github.com/charmbracelet/x/ansi v0.11.8 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.14.0 // indirect
github.com/dave/dst v0.28.0 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dlclark/regexp2/v2 v2.8.0 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dustin/go-humanize v1.1.0 // indirect
github.com/ebitengine/purego v0.11.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.8 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/ghostiam/protogetter v1.0.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-critic/go-critic v0.15.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
github.com/go-openapi/jsonpointer v1.0.1 // indirect
@@ -139,182 +66,42 @@ require (
github.com/go-openapi/swag/yamlutils v0.29.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
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.5.0 // indirect
github.com/go-webauthn/x v0.3.1 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godoc-lint/godoc-lint v0.11.4 // indirect
github.com/gofrs/flock v0.13.1 // indirect
github.com/gohugoio/hashstructure v1.1.0 // indirect
github.com/gohugoio/hugo v0.166.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/geo v0.0.0-20260818125358-b200a1149890 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
github.com/golangci/go-printf-func-name v0.1.1 // indirect
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
github.com/golangci/golangci-lint/v2 v2.13.2 // 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/rowserrcheck v0.0.0-20260901154044-241134db7241 // indirect
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-tpm v0.9.8 // 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.2 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-version v1.9.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.11.0 // indirect
github.com/jjti/go-spancheck v0.6.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
github.com/kisielk/errcheck v1.20.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
github.com/kovidgoyal/go-shm v1.0.0 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.15 // indirect
github.com/ldez/exptostd v0.4.5 // indirect
github.com/ldez/gomoddirectives v0.10.0 // indirect
github.com/ldez/grignotin v0.10.1 // indirect
github.com/ldez/structtags v0.6.1 // indirect
github.com/ldez/tagliatelle v0.8.0 // indirect
github.com/ldez/usetesting v0.5.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leodido/go-urn v1.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
github.com/manuelarte/funcorder v0.6.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.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.30 // indirect
github.com/mgechev/revive v1.16.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/nishanths/exhaustive v0.13.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.24.1 // indirect
github.com/prometheus/client_model v0.6.3 // indirect
github.com/prometheus/common v0.71.0 // indirect
github.com/prometheus/procfs v0.22.0 // 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.3.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.16.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
github.com/securego/gosec/v2 v2.29.0 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.5.1 // indirect
github.com/sourcegraph/go-diff v0.9.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/tetafro/godot v1.5.6 // indirect
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
github.com/timonwong/loggercheck v0.12.0 // indirect
github.com/tinylib/msgp v1.6.4 // 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/urfave/cli/v2 v2.27.7 // indirect
github.com/uudashr/gocognit v1.2.1 // indirect
github.com/uudashr/iface v1.5.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v1.2.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // 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.14.0 // indirect
go-simpler.org/sloglint v0.12.0 // indirect
go.augendre.info/arangolint v0.4.0 // indirect
go.augendre.info/fatcontext v0.10.1 // indirect
go.etcd.io/bbolt v1.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.28.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/text v0.42.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.8.1 // indirect
modernc.org/libc v1.76.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
mvdan.cc/gofumpt v0.12.0 // indirect
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
tool (
github.com/air-verse/air
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
github.com/swaggo/swag/cmd/swag
)
exclude (
github.com/alecthomas/go-check-sumtype v0.5.0
github.com/gobwas/glob v1.0.0
github.com/gtsteffaniak/go-ffmpeg v1.0.0
github.com/gtsteffaniak/go-ffmpeg v1.1.0
github.com/gtsteffaniak/go-ffmpeg v1.2.0
)
+18 -619
View File
@@ -1,201 +1,33 @@
4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
codeberg.org/chavacava/garif v0.2.1 h1:K9oYxSlvlXrHXyW26Z4q61bTpJDo1wbvXcYKar/F/LM=
codeberg.org/chavacava/garif v0.2.1/go.mod h1:oHnDSmc0f9K1MeE+MQD/yjkiIB5Xsn5y3S9Dg96Xk84=
codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI=
codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
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/exhaustruct/v5 v5.2.0 h1:HZB49Fshn8qHJCi9VzYtrcnR1jbLteRHXfnQz8Adams=
dev.gaijin.team/go/exhaustruct/v5 v5.2.0/go.mod h1:5IshpqhgGdKIrF8lBmi3M6YKmI7T3hMzAL9nrflko88=
dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
github.com/4meepo/tagalign v1.4.4 h1:052JDGj2pBORc6NkD30dDXpsUpoYszDeUwr9/5YZuVA=
github.com/4meepo/tagalign v1.4.4/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
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.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8=
github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
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/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck=
github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4=
github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
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/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PFL3qh0=
github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8=
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
github.com/Masterminds/semver/v3 v3.5.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/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
github.com/air-verse/air v1.67.4 h1:pkWBQues0094VMgUZ9MboNfS1vyCBEXeU6QxuD6/PwU=
github.com/air-verse/air v1.67.4/go.mod h1:XumHRkXbIg8CU5FqhQHgQC4oxQW/OQLwUMQAWln8cZs=
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.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/go-check-sumtype v0.4.0 h1:gd4mE4QsA62sXlnFIs8GUv8/0xH3DOUqTkYQR1l2v28=
github.com/alecthomas/go-check-sumtype v0.4.0/go.mod h1:aYYllI8IXNjo7ZYUzI2+wyLRECMg3zo+8CZj9JTBAWU=
github.com/alecthomas/kong v1.16.1 h1:ixhCt93XkJ98kGposQ54+bl0IK6XwqB40AsMynU7Z8E=
github.com/alecthomas/kong v1.16.1/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
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/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
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.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
github.com/andybalholm/brotli v1.2.4 h1:649LN6qF+/7JJYNN3Wdu4Dt5PS6eC/yP/Go3Bb0/AHI=
github.com/andybalholm/brotli v1.2.4/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM=
github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM=
github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
github.com/bep/golocales v0.2.0 h1:4H1H5UPw3ainpj5zykeEfiMRQngyaIC/t+I4Dvn+fvE=
github.com/bep/golocales v0.2.0/go.mod h1:Hl78nje8mNL3LzLeJvYN9NsIZgyFJGrGfvgO9r1+mwE=
github.com/bep/goportabletext v0.2.0 h1:CZ9f8jADBWqHwBymQiJJPCTSV/tHSA+PYzlUf86Yze0=
github.com/bep/goportabletext v0.2.0/go.mod h1:xDeA5+qcgKzJq6Q6XjAiBKtxLD3Yn7f6XP4joD3J3qU=
github.com/bep/helpers v0.12.0 h1:tD6V2DQW0B+FUynF2etR/106S/TO9akm+vA/Hk24GxY=
github.com/bep/helpers v0.12.0/go.mod h1:PfE7MGdA8sSQ19nyDh4tYbs5rAlStlJaDI21f/fnNps=
github.com/bep/imagemeta v1.0.0 h1:8JZmahegn5p1M9CpFmga7LoSxErpu1uFfpSzMUNju7A=
github.com/bep/imagemeta v1.0.0/go.mod h1:+Hlp195TfZpzsqCxtDKTG6eWdyz2+F2V/oCYfr3CZKA=
github.com/bep/lazycache v0.9.0 h1:JLpAJGotKPaacb7dgv0+qBLneQjcMhJ3g9MvEhDyZUg=
github.com/bep/lazycache v0.9.0/go.mod h1:E3yeEfnmWUNNuIIR/EX6srqQwsvAFHSLQ8tR+jClATg=
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
github.com/bep/overlayfs v0.11.0 h1:aymHDGC0CHpvn0XvTfgpK6skCp16oMi+tdUF32l6pPs=
github.com/bep/overlayfs v0.11.0/go.mod h1:L+ggdoKm+Y7Xb4a1osd+/LOPG4qsY62snqRqJH5Mspc=
github.com/bep/textandbinarywriter v0.1.0 h1:KXmXsRN2Uhwhm1G3e/snM8+5SPQBJrCEpIosdIBR3po=
github.com/bep/textandbinarywriter v0.1.0/go.mod h1:dAcHveajlWWU7PXhp6Dn4PIAYDg2H13Huif9xMS2w8w=
github.com/bep/tmc v0.7.0 h1:e0axmUwy+5EGQ6qk4wiOyDFuTqn3vpnoXIrxEG+yCX0=
github.com/bep/tmc v0.7.0/go.mod h1:jNqktL5Su+ScC51fw6urQRvP9Dwr1hVVGr9a3rwuGb4=
github.com/bits-and-blooms/bitset v1.25.0 h1:0Ro0qF4abCkM6SqWPVj29sFhAbMPAZpaDD7xhJ10beM=
github.com/bits-and-blooms/bitset v1.25.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
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.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.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU=
github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
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/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.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk=
github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16 h1:/hSCZO1puetbGd2JikHKN0nAcb2mFevzxJElFoduhaw=
github.com/charmbracelet/ultraviolet v0.0.0-20260910203606-6c9e17dc7a16/go.mod h1:JFvmQPZh3uAu7PpSZU91bcR0BrIy24HhW+0EsW1uGxE=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/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.14.0 h1:h6AcLqmjIOBgojhtzY2CvBnA6RawPTkBHgtMvYD5YZ8=
github.com/daixiang0/gci v0.14.0/go.mod h1:w9E+SWQ4aPQ+xYPUdqitGDoXpT4mayqOfk7Szz2U6wQ=
github.com/dave/dst v0.28.0 h1:EIEuRC5XmVKuZF2OeyMahXX9ARVt6Xxr8vI0WtIY7K8=
github.com/dave/dst v0.28.0/go.mod h1:C4mfimBeSHfsgskhR/mUYSE05s2ih+PW6g9FP3c49tE=
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2/v2 v2.8.0 h1:CekDhPLGfm+GAJmPVFIG+5dqMIQPkyHJll7BbdneDfw=
github.com/dlclark/regexp2/v2 v2.8.0/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
@@ -213,43 +45,20 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg=
github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI=
github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI=
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
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/evanoberholster/imagemeta v1.0.0 h1:FlSihlsjkWIoS83wmgPOP3CbAxlRpxM8jQGZgLu+MWQ=
github.com/evanoberholster/imagemeta v1.0.0/go.mod h1:2ITZJjD1ygzWIhepvn8coRvpaFcnDNk3E6hx6v7z8DU=
github.com/evanw/esbuild v0.28.2 h1:A2uETn4jrQTcXaT/shwTDTYBxDjl7fV7nXmUrJxfA2w=
github.com/evanw/esbuild v0.28.2/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
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.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II=
github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo=
github.com/fxamacker/cbor/v2 v2.9.4/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/gen2brain/go-fitz v1.28.2 h1:845G85N5TUgnq5oDqyYrW0JvehAkeo35UkkK2dJtW1M=
github.com/gen2brain/go-fitz v1.28.2/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/getkin/kin-openapi v0.149.0 h1:ZbhmVJ4yq5RZDUsyP8lcBcGMsjsaTqXEFt6isdtMDfA=
github.com/getkin/kin-openapi v0.149.0/go.mod h1:1+BHDzstro+P5CKtPy1X4PfofnFgmRe6uvMy9+r9fKY=
github.com/ghostiam/protogetter v1.0.1 h1:q4LLTx2KJHJpxrfO3BniUDWoFkoqWD/UlHhClBlUlLM=
github.com/ghostiam/protogetter v1.0.1/go.mod h1:50Fh5yZfs6SFGrd1FlmT04NcguyEOxyjNK5zGD6/EvM=
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-critic/go-critic v0.15.0 h1:MDUZIQKytIl+vPEc0Igw51m4uLTdi1ZGz2jatEqNs1g=
github.com/go-critic/go-critic v0.15.0/go.mod h1:3BPrw8nHATGm3o2n37cssOmJzpySGKCdY8JWkCzxgKQ=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
@@ -260,8 +69,6 @@ github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc=
@@ -297,63 +104,14 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w=
github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8=
github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU=
github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s=
github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw=
github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4=
github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ=
github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw=
github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY=
github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco=
github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4=
github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA=
github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA=
github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk=
github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus=
github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8=
github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw=
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.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.18.1 h1:KaQw6M+ODLvxHwddyeo6zFhhmicfLup/BClhigB6A+0=
github.com/go-webauthn/webauthn v0.18.1/go.mod h1:s4rZTQnKHWxIh6G3yEGqlxvtiLceA1jigll8FpqSgQ8=
github.com/go-webauthn/x v0.3.1 h1:1ff37z3XfmTTomkhlURgGizLIDyOvPgTt2t9nlzKLRo=
github.com/go-webauthn/x v0.3.1/go.mod h1:ZInxAynYXfBPvvm5gzKZ7geBlL23K71xASMgohHl/Rg=
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/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
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/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godoc-lint/godoc-lint v0.11.4 h1:N9zUYVJ/OkznBbrBsU/zODvua64EUplMBLQWStO3+1M=
github.com/godoc-lint/godoc-lint v0.11.4/go.mod h1:7usTXoxS0QYIi5usTSCPwR0w/6xLjcz450shXjLn3t4=
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
github.com/gohugoio/gift v0.2.0 h1:vA31pP0rTVmBxBrhpY3WEt+4zM4g+1sDqYeemwsYeqc=
github.com/gohugoio/gift v0.2.0/go.mod h1:1Mrm5CjF33KpD749Dwj+UAjWZ3LC6cBXGuTMa5XwoP4=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM=
github.com/gohugoio/hashstructure v1.1.0 h1:38yUfZBca6qXSbUpteLhjDGLNskclHaguFBYpjaRjf4=
github.com/gohugoio/hashstructure v1.1.0/go.mod h1:Pz8dcwjZs6FBKWu9x/ZIChrTHIM175zfUJK0KLvC1z8=
github.com/gohugoio/httpcache v0.9.0 h1:f3Jb93RAP+qlmK7Ma5uGfwJslYDqTfzYJ6AeOT7nDe8=
github.com/gohugoio/httpcache v0.9.0/go.mod h1:YQsVn7oB30TG1QO9HoHc3Xq8dDQDVOdxBc0p7CYgbhE=
github.com/gohugoio/hugo v0.166.0 h1:MNGqY2FQQYofvHYMxdnvfnR/Sun+S6FDm7hd70qEG/4=
github.com/gohugoio/hugo v0.166.0/go.mod h1:jRSQi5aSXDeU2s2oJM1Z3eMRcaEuDZvWCrfjM71tFqE=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0 h1:I/n6v7VImJ3aISLnn73JAHXyjcQsMVvbguQPTk9Ehus=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.7.0/go.mod h1:9LJNfKWFmhEJ7HW0in5znezMwH+FYMBIhNZ3VWtRcRs=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0 h1:p13Q0DBCrBRpJGtbtlgkYNCs4TnIlZJh8vHgnAiofrI=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.5.0/go.mod h1:ob9PCHy/ocsQhTz68uxhyInaYCbbVNpOOrJkIoSeD+8=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -365,40 +123,14 @@ github.com/golang/geo v0.0.0-20260818125358-b200a1149890 h1:m+G0ip1+N4CF0ex34Seo
github.com/golang/geo v0.0.0-20260818125358-b200a1149890/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
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/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
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/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4=
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
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-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg=
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA=
github.com/golangci/golangci-lint/v2 v2.13.2 h1:bCyq3E4vo9qwzifjpzJqYndEt7Ncva80qkk8G2B4TXU=
github.com/golangci/golangci-lint/v2 v2.13.2/go.mod h1:5xaMd1kAxV7GSBPEyngw8gnnjoRocqW9UVKdBR6627w=
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/rowserrcheck v0.0.0-20260901154044-241134db7241 h1:0kPqfqytQizeQ9WBk487NcsGcRdS8YhHeUhXxkfEAhA=
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241/go.mod h1:lczq5oJ8PxpMESQG6dO3iFYTQPhpqoMOYw//n2FEnHA=
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/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
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.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
@@ -409,43 +141,18 @@ github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFe
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
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/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
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.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.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=
github.com/gtsteffaniak/go-cache v1.1.0 h1:ni/2cJpCxpGmaMZd92iTDIGOy+yUcMcxQuW7bSKhkCE=
github.com/gtsteffaniak/go-cache v1.1.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
github.com/gtsteffaniak/go-ffmpeg v0.5.2 h1:KVnKM3zewY9yeZVd59Hb0/10gSbhYf01yRM5RftXGEU=
github.com/gtsteffaniak/go-ffmpeg v0.5.2/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
github.com/gtsteffaniak/go-ffmpeg v1.0.0 h1:dUWvlZmWnkiujDtIMLGwrpfSAOnM+IOtk93HXF6HR0U=
github.com/gtsteffaniak/go-ffmpeg v1.0.0/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
github.com/gtsteffaniak/go-logger v1.1.0 h1:baFds+iibuLLNpNLwygz32bDqdsunI6dGWcdxwMIVKI=
github.com/gtsteffaniak/go-logger v1.1.0/go.mod h1:rapapTju4D7bwt6h4JOdZNUXDjnp0WkTBd6TN5yWglI=
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo=
github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw=
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.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@@ -458,24 +165,8 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
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.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0=
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok=
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
@@ -483,369 +174,97 @@ github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT
github.com/kovidgoyal/imaging v1.8.23 h1:4WsboyQ/E8tic2kX49P93Rvscg33SPsCwTBG5EZZF54=
github.com/kovidgoyal/imaging v1.8.23/go.mod h1:k3Iot3H0v2rSWXIxQISfT3e9wIryqzAHLHCuhG7z9lM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.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/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
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.10.0 h1:Zb23rfMMkG21B6pZ9OfESzemByP5Qr7YvWcXQZE/S3Q=
github.com/ldez/gomoddirectives v0.10.0/go.mod h1:W9uh0721Vc7aaVoiMXTpKIveLmtHC9uaOSiHM4OK5a8=
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.8.0 h1:QczqVPa22JdT40p3m5JTTVmPMv5wDORqdomJn6/Pnn0=
github.com/ldez/tagliatelle v0.8.0/go.mod h1:K9n83Wv0wyVq3JYF9WI28BX8xXhDBVP3thHJ0ENuiEs=
github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc=
github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ=
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY=
github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA=
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
github.com/lucasb-eyer/go-colorful v1.4.1/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/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE=
github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
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.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
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/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
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=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.30 h1:+KUuiDA4fF0R1p5FeueHefjDm+GIM+kWfFnDjybOPgk=
github.com/mattn/go-runewidth v0.0.30/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mgechev/revive v1.16.0 h1:9OZVO8xOPUwIYynplHXv7liMllPQ/BTNZLp07sM/Im0=
github.com/mgechev/revive v1.16.0/go.mod h1:nT854jMmvQsS/qqxEaMPM4pJSUpxhcPoHM788ZxDvdQ=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
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.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
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/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
github.com/nishanths/exhaustive v0.13.0 h1:Rof0axOSNQaaPaPmLK5RxcRrJV2Vcv+vfUmuRy+2EAM=
github.com/nishanths/exhaustive v0.13.0/go.mod h1:3/adQjJgvD/UFHHSYBjpK9Wbz8DOvf0FBtzx+zI0Rlo=
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.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8=
github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c=
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
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=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
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.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
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=
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
github.com/raeperd/recvcheck v0.3.1 h1:p7nZMxiCWkdUtY2i3o+pv1h4fJCR5gpvqAXr9326hEY=
github.com/raeperd/recvcheck v0.3.1/go.mod h1:CbE7iRocQ4uc71JwpW/CPSh4BShnkre4A9Gb508RypU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
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/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA=
github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU=
github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg=
github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
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.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/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.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
github.com/securego/gosec/v2 v2.29.0 h1:pF2HSLcnY5voqpxQumEe0O5YAAbvF4Syu4qeIewpLyc=
github.com/securego/gosec/v2 v2.29.0/go.mod h1:47C7ej+L2bC1NOxteU2aCGnCi4cZaoKzRt5y9plIvuY=
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/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
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/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.9.0 h1:7xcVsayWHhvxZGrqt1o89vvBApCa4nyCgO7tNxDHJ+Q=
github.com/sourcegraph/go-diff v0.9.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
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.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.5/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.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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.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.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
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.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
github.com/tdewolff/parse/v2 v2.8.16/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ=
github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
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.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g=
github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
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/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
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.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA=
github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
github.com/xo/terminfo v1.2.0 h1:d0ZTOCpuGE0lwSAOs0zcJjwz3jWQyqcRt9XbGJpCOl4=
github.com/xo/terminfo v1.2.0/go.mod h1:lGzkSo8Fe7IRh/w+Gqz7n5mDog4FVXCj3gy/DYTBqio=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
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=
github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4=
github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw=
github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
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.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo=
go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE=
go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
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.10.1 h1:HCS/+eob32cM0mEtmZM6NiXdqvS5KiZmsR+fCubTpbc=
go.augendre.info/fatcontext v0.10.1/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
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/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
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-20260908205506-85c1c2202aba h1:ZoZmFkcuQ289xJilBm9Ck3W+/okrhXc4LUpbtKFQE4k=
golang.org/x/exp/typeparams v0.0.0-20260908205506-85c1c2202aba/go.mod h1:qkMnvgP8C7+xkci04txL2SJFc/0PGqnkf6gs5JUC43E=
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
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.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=
golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -861,8 +280,8 @@ golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
@@ -870,40 +289,28 @@ golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
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=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.8.1 h1:+JKf3xJ1ni4CwrhVg4/pqsfPGP6vNAXcKbMXJodYx3w=
honnef.co/go/tools v0.8.1/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.35.2 h1:JPAIttQRHdY7aRdr04+iTW7Sx+6OSZcmKJ0OZl/tNaA=
@@ -932,11 +339,3 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
mvdan.cc/gofumpt v0.12.0 h1:1Lbudkz2kpM9Cjz2pL4M19u7q+GaEhCTNf7N9mfpcho=
mvdan.cc/gofumpt v0.12.0/go.mod h1:SmBHHrljiZu/uoypeKup3rFzP6eoC9UwCp2iH5E3jZA=
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+2 -7
View File
@@ -53,14 +53,9 @@ func MakeSignedTokenAPI(user *users.User, name string, duration time.Duration, p
}
if !minimal {
claim.Permissions = users.SanitizeTokenPermissions(perms)
claim.BelongsTo = user.ID
}
signedClaims := jwt.Claims(claim)
if minimal {
// Sign only standard JWT claims. The full AuthToken struct always JSON-marshals
// a zero Permissions object, which bloats the token past WebDAV client limits.
signedClaims = claim.MinimalAuthToken
}
// Always sign minimal JWT claims only; identity is resolved via hashed_tokens lookup.
signedClaims := jwt.Claims(claim.MinimalAuthToken)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, signedClaims)
tokenString, err := token.SignedString([]byte(settings.Config.Auth.Key))
if err != nil {
+24
View File
@@ -0,0 +1,24 @@
package auth
import (
"fmt"
"github.com/golang-jwt/jwt/v4"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
)
// JWTSigningKeyBytes returns the HMAC key or an error if it is unset (empty key must never verify).
func JWTSigningKeyBytes() ([]byte, error) {
key := settings.Config.Auth.Key
if len(key) == 0 {
return nil, fmt.Errorf("auth signing key is not configured")
}
return []byte(key), nil
}
// JWTSigningKeyFunc is the keyFunc for FileBrowser-issued session and API JWTs.
func JWTSigningKeyFunc() jwt.Keyfunc {
return func(token *jwt.Token) (interface{}, error) {
return JWTSigningKeyBytes()
}
}
+33
View File
@@ -0,0 +1,33 @@
package auth
import (
"testing"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
)
func TestJWTSigningKeyFuncRejectsEmptyKey(t *testing.T) {
orig := settings.Config.Auth.Key
settings.Config.Auth.Key = ""
t.Cleanup(func() { settings.Config.Auth.Key = orig })
_, err := JWTSigningKeyFunc()(nil)
if err == nil {
t.Fatal("expected error for empty signing key")
}
}
func TestJWTSigningKeyFuncAcceptsConfiguredKey(t *testing.T) {
orig := settings.Config.Auth.Key
settings.Config.Auth.Key = "test-key"
t.Cleanup(func() { settings.Config.Auth.Key = orig })
key, err := JWTSigningKeyFunc()(nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
keyBytes, ok := key.([]byte)
if !ok || string(keyBytes) != "test-key" {
t.Fatalf("got key %T %q", key, key)
}
}
+1 -3
View File
@@ -63,9 +63,7 @@ func invalidSignatureSpelling(token string) string {
}
func parseSessionToken(tokenString string) error {
keyFunc := func(token *jwt.Token) (interface{}, error) {
return []byte(settings.Config.Auth.Key), nil
}
keyFunc := JWTSigningKeyFunc()
var tk users.AuthToken
_, err := jwt.ParseWithClaims(tokenString, &tk, keyFunc)
return err
+7 -14
View File
@@ -27,31 +27,24 @@ func TestMinimalSignedTokenLengthUnder256(t *testing.T) {
assertMinimalJWTClaims(t, tokenString)
}
func TestCustomizedTokenEmbedsPermissionsAndIsLonger(t *testing.T) {
func TestCustomizedTokenMetadataRetainsPermissions(t *testing.T) {
origKey := settings.Config.Auth.Key
settings.Config.Auth.Key = "test-signing-key-for-length-check"
t.Cleanup(func() { settings.Config.Auth.Key = origKey })
user := &users.User{ID: 42, FrontendUser: users.FrontendUser{Username: "webdavuser"}}
minimal, _, err := MakeSignedTokenAPI(user, "min", time.Hour*24*365, users.Permissions{}, true)
_, claim, err := MakeSignedTokenAPI(user, "custom", time.Hour*24*365, users.Permissions{Api: true}, false)
if err != nil {
t.Fatal(err)
}
custom, _, err := MakeSignedTokenAPI(user, "custom", time.Hour*24*365, users.Permissions{Api: true}, false)
if !claim.Permissions.Api {
t.Fatalf("expected api permission in returned metadata: %+v", claim.Permissions)
}
tokenString, _, err := MakeSignedTokenAPI(user, "custom2", time.Hour*24*365, users.Permissions{Api: true}, false)
if err != nil {
t.Fatal(err)
}
if len(custom) <= len(minimal) {
t.Fatalf("custom token should be longer than minimal: custom=%d minimal=%d", len(custom), len(minimal))
}
claims := jwt.MapClaims{}
if _, _, err := jwt.NewParser().ParseUnverified(custom, claims); err != nil {
t.Fatalf("parse custom token: %v", err)
}
if _, ok := claims["Permissions"]; !ok {
t.Fatal("custom token payload should include Permissions claim")
}
assertMinimalJWTClaims(t, tokenString)
}
func assertMinimalJWTClaims(t *testing.T, tokenString string) {
+200 -38
View File
@@ -61,15 +61,25 @@ type FrontendAccessRule struct {
// GroupMap maps group names to a set of usernames.
type GroupMap map[string]StringSet
// HashedTokenInfo describes a registered bearer token owner and type.
type HashedTokenInfo struct {
UserID uint64
IsSession bool
}
// sessionTokenRetireGrace is how long a rotated session token stays usable after
// renewal. In-flight requests still carrying the previous cookie must not 401.
const sessionTokenRetireGrace = 2 * time.Minute
// Storage manages access rules and group membership.
type Storage struct {
mux sync.RWMutex
AllRules SourceRuleMap // AllRules[sourcePath][indexPath] - in-memory authoritative state
Groups GroupMap // key: group name, value: set of usernames - in-memory authoritative state
RevokedTokens map[string]struct{} // set of revoked token hashes - in-memory authoritative state
HashedTokens map[string]uint64 // maps token hash → owner user id - in-memory authoritative state
Users *users.Storage // Reference to users storage
sqlStore SQLPersister // SQL store for persistence
AllRules SourceRuleMap // AllRules[sourcePath][indexPath] - in-memory authoritative state
Groups GroupMap // key: group name, value: set of usernames - in-memory authoritative state
RevokedTokens map[string]int64 // token hash → revocation unix time (0 = immediate) - in-memory authoritative state
HashedTokens map[string]HashedTokenInfo // maps token hash → owner/type - in-memory authoritative state
Users *users.Storage // Reference to users storage
sqlStore SQLPersister // SQL store for persistence
}
// SQLPersister interface for SQL persistence operations
@@ -78,8 +88,11 @@ type SQLPersister interface {
DeleteAccessRule(source, path string) error
SaveGroup(name string, members StringSet) error
DeleteGroup(name string) error
SaveRevokedToken(tokenHash string) error
SaveHashedToken(tokenHash string, userID uint64) error
SaveRevokedToken(tokenHash string, revokedAt int64) error
PersistImmediateTokenRevocation(tokenHash string) error
PersistTokenRetirement(tokenHash string, revokedAt int64, pruned []string) error
DeleteRevokedToken(tokenHash string) error
SaveHashedToken(tokenHash string, userID uint64, isSession bool) error
DeleteHashedToken(tokenHash string) error
DeleteHashedTokensByUserID(userID uint64) error
}
@@ -127,8 +140,8 @@ func NewStorage(usersStore *users.Storage) *Storage {
var s = &Storage{
AllRules: make(SourceRuleMap),
Groups: make(GroupMap),
RevokedTokens: make(map[string]struct{}),
HashedTokens: make(map[string]uint64),
RevokedTokens: make(map[string]int64),
HashedTokens: make(map[string]HashedTokenInfo),
Users: usersStore,
}
return s
@@ -1300,61 +1313,210 @@ func (s *Storage) UpdateRulePath(sourcePath string, oldPath, newPath utils.Index
return nil
}
// RevokeToken adds a token hash to the revoked list and persists to DB.
func (s *Storage) RevokeToken(tokenHash string) error {
tokenHash = utils.HashSHA256(tokenHash)
// RevokeToken immediately invalidates a token: the owner mapping is removed and
// the hash is persisted as revoked with timestamp 0 (no grace window).
func (s *Storage) RevokeToken(tokenString string) error {
tokenHash := utils.HashSHA256(tokenString)
var rollback tokenRevokeRollback
s.mux.Lock()
s.RevokedTokens[tokenHash] = struct{}{}
rollback.capture(s, tokenHash)
s.RevokedTokens[tokenHash] = 0
delete(s.HashedTokens, tokenHash)
sqlStore := s.sqlStore
s.mux.Unlock()
if sqlStore != nil {
_ = sqlStore.DeleteHashedToken(tokenHash)
_ = sqlStore.SaveRevokedToken(tokenHash)
if sqlStore == nil {
return nil
}
if err := sqlStore.PersistImmediateTokenRevocation(tokenHash); err != nil {
s.mux.Lock()
rollback.apply(s)
s.mux.Unlock()
return err
}
return nil
}
// IsTokenRevoked checks if a token hash is in the revoked list (memory-only read).
func (s *Storage) IsTokenRevoked(tokenHash string) bool {
s.mux.RLock()
defer s.mux.RUnlock()
tokenHash = utils.HashSHA256(tokenHash)
_, exists := s.RevokedTokens[tokenHash]
return exists
// RetireToken schedules a rotated session token for revocation after
// sessionTokenRetireGrace. The owner mapping is kept during the grace window so
// in-flight requests still carrying the previous cookie stay authenticated.
func (s *Storage) RetireToken(tokenString string) error {
tokenHash := utils.HashSHA256(tokenString)
now := time.Now()
var rollback tokenRetireRollback
s.mux.Lock()
rollback.capture(s, tokenHash, now)
s.RevokedTokens[tokenHash] = now.Unix()
pruned := s.pruneRevocationsNL(now)
sqlStore := s.sqlStore
s.mux.Unlock()
if sqlStore == nil {
return nil
}
if err := sqlStore.PersistTokenRetirement(tokenHash, now.Unix(), pruned); err != nil {
s.mux.Lock()
rollback.apply(s)
s.mux.Unlock()
return err
}
return nil
}
// AddApiToken maps a token string hash to an owner user id (for minimal JWTs).
type tokenRevokeRollback struct {
tokenHash string
hadHashed bool
hashed HashedTokenInfo
hadRevoked bool
revokedAt int64
}
func (rb *tokenRevokeRollback) capture(s *Storage, tokenHash string) {
rb.tokenHash = tokenHash
if info, ok := s.HashedTokens[tokenHash]; ok {
rb.hadHashed = true
rb.hashed = info
}
if at, ok := s.RevokedTokens[tokenHash]; ok {
rb.hadRevoked = true
rb.revokedAt = at
}
}
func (rb *tokenRevokeRollback) apply(s *Storage) {
if rb.hadHashed {
s.HashedTokens[rb.tokenHash] = rb.hashed
} else {
delete(s.HashedTokens, rb.tokenHash)
}
if rb.hadRevoked {
s.RevokedTokens[rb.tokenHash] = rb.revokedAt
} else {
delete(s.RevokedTokens, rb.tokenHash)
}
}
type tokenRetireRollback struct {
tokenHash string
hadTokenRevoked bool
tokenRevokedAt int64
prunedRevoked map[string]int64
prunedHashed map[string]HashedTokenInfo
}
func (rb *tokenRetireRollback) capture(s *Storage, tokenHash string, now time.Time) {
rb.tokenHash = tokenHash
if at, ok := s.RevokedTokens[tokenHash]; ok {
rb.hadTokenRevoked = true
rb.tokenRevokedAt = at
}
revokedAfter := maps.Clone(s.RevokedTokens)
revokedAfter[tokenHash] = now.Unix()
rb.prunedRevoked = make(map[string]int64)
rb.prunedHashed = make(map[string]HashedTokenInfo)
for hash, revokedAt := range revokedAfter {
if revokedAt != 0 && now.Sub(time.Unix(revokedAt, 0)) < sessionTokenRetireGrace {
continue
}
if at, ok := s.RevokedTokens[hash]; ok {
rb.prunedRevoked[hash] = at
}
if info, ok := s.HashedTokens[hash]; ok {
rb.prunedHashed[hash] = info
}
}
}
func (rb *tokenRetireRollback) apply(s *Storage) {
if rb.hadTokenRevoked {
s.RevokedTokens[rb.tokenHash] = rb.tokenRevokedAt
} else {
delete(s.RevokedTokens, rb.tokenHash)
}
for hash, at := range rb.prunedRevoked {
s.RevokedTokens[hash] = at
}
for hash, info := range rb.prunedHashed {
s.HashedTokens[hash] = info
}
}
// pruneRevocationsNL drops revocation records that no longer need to be retained
// and returns their hashes so the caller can clean up SQL. Immediate revocations
// (timestamp 0) and retirements past the grace window no longer resolve a user
// because their owner mapping is removed here. Caller must hold s.mux.
func (s *Storage) pruneRevocationsNL(now time.Time) []string {
var pruned []string
for hash, revokedAt := range s.RevokedTokens {
if revokedAt == 0 || now.Sub(time.Unix(revokedAt, 0)) >= sessionTokenRetireGrace {
delete(s.RevokedTokens, hash)
delete(s.HashedTokens, hash)
pruned = append(pruned, hash)
}
}
return pruned
}
// IsTokenRevoked reports whether a token is revoked. Retired session tokens are
// only considered revoked once the grace window has elapsed.
func (s *Storage) IsTokenRevoked(tokenString string) bool {
tokenHash := utils.HashSHA256(tokenString)
s.mux.RLock()
revokedAt, exists := s.RevokedTokens[tokenHash]
s.mux.RUnlock()
if !exists {
return false
}
if revokedAt == 0 {
return true
}
return time.Since(time.Unix(revokedAt, 0)) >= sessionTokenRetireGrace
}
// AddApiToken maps a named API token string hash to an owner user id.
func (s *Storage) AddApiToken(tokenString string, userID uint64) error {
return s.addHashedToken(tokenString, userID, false)
}
// AddSessionToken maps a session bearer token string hash to an owner user id.
func (s *Storage) AddSessionToken(tokenString string, userID uint64) error {
return s.addHashedToken(tokenString, userID, true)
}
func (s *Storage) addHashedToken(tokenString string, userID uint64, isSession bool) error {
tokenHash := utils.HashSHA256(tokenString)
s.mux.Lock()
s.HashedTokens[tokenHash] = userID
s.HashedTokens[tokenHash] = HashedTokenInfo{UserID: userID, IsSession: isSession}
sqlStore := s.sqlStore
s.mux.Unlock()
if sqlStore != nil {
return sqlStore.SaveHashedToken(tokenHash, userID)
return sqlStore.SaveHashedToken(tokenHash, userID, isSession)
}
return nil
}
// GetUserIDFromToken retrieves the owner user id for a given token string (memory read; SQL populated at startup).
func (s *Storage) GetUserIDFromToken(tokenString string) (uint64, bool) {
info, ok := s.GetHashedTokenInfo(tokenString)
return info.UserID, ok
}
// GetHashedTokenInfo retrieves owner/type metadata for a given token string.
func (s *Storage) GetHashedTokenInfo(tokenString string) (HashedTokenInfo, bool) {
s.mux.RLock()
defer s.mux.RUnlock()
tokenHash := utils.HashSHA256(tokenString)
uid, exists := s.HashedTokens[tokenHash]
return uid, exists
info, exists := s.HashedTokens[tokenHash]
return info, exists
}
// GetRevokedTokens returns a copy of all revoked token hashes.
func (s *Storage) GetRevokedTokens() map[string]struct{} {
// GetRevokedTokens returns a copy of the revocation map (hash → revocation unix time).
func (s *Storage) GetRevokedTokens() map[string]int64 {
s.mux.RLock()
defer s.mux.RUnlock()
// Return a copy to prevent external modification
result := make(map[string]struct{}, len(s.RevokedTokens))
for k := range s.RevokedTokens {
result[k] = struct{}{}
result := make(map[string]int64, len(s.RevokedTokens))
for k, v := range s.RevokedTokens {
result[k] = v
}
return result
}
@@ -1372,15 +1534,15 @@ func (s *Storage) RemoveApiToken(tokenString string) error {
return nil
}
// RemoveHashedTokensForUser removes every minimal-JWT hash → user id mapping for that owner.
// RemoveHashedTokensForUser removes every bearer-token hash → user id mapping for that owner.
// Call when deleting a user so recycled usernames never resolve leftover token hashes to a new id.
func (s *Storage) RemoveHashedTokensForUser(userID uint64) error {
if userID == 0 {
return nil
}
s.mux.Lock()
for hash, uid := range s.HashedTokens {
if uid == userID {
for hash, info := range s.HashedTokens {
if info.UserID == userID {
delete(s.HashedTokens, hash)
}
}
@@ -0,0 +1,133 @@
package access_test
import (
"errors"
"testing"
"time"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
)
type failingRevokePersister struct{}
func (failingRevokePersister) SaveAccessRule(string, string, *access.AccessRule) error { return nil }
func (failingRevokePersister) DeleteAccessRule(string, string) error { return nil }
func (failingRevokePersister) SaveGroup(string, access.StringSet) error { return nil }
func (failingRevokePersister) DeleteGroup(string) error { return nil }
func (failingRevokePersister) SaveRevokedToken(string, int64) error { return nil }
func (failingRevokePersister) PersistImmediateTokenRevocation(string) error {
return errors.New("simulated revocation persistence failure")
}
func (failingRevokePersister) PersistTokenRetirement(string, int64, []string) error {
return errors.New("simulated retirement persistence failure")
}
func (failingRevokePersister) DeleteRevokedToken(string) error { return nil }
func (failingRevokePersister) SaveHashedToken(string, uint64, bool) error { return nil }
func (failingRevokePersister) DeleteHashedToken(string) error { return nil }
func (failingRevokePersister) DeleteHashedTokensByUserID(uint64) error { return nil }
func TestSessionAndApiTokenMetadata(t *testing.T) {
store, _ := createTestStorage(t)
if err := store.AddSessionToken("session-token", 42); err != nil {
t.Fatal(err)
}
sessionInfo, ok := store.GetHashedTokenInfo("session-token")
if !ok {
t.Fatal("expected session token mapping")
}
if !sessionInfo.IsSession || sessionInfo.UserID != 42 {
t.Fatalf("session token info = %+v, want user 42 session=true", sessionInfo)
}
if err := store.AddApiToken("api-token", 42); err != nil {
t.Fatal(err)
}
apiInfo, ok := store.GetHashedTokenInfo("api-token")
if !ok {
t.Fatal("expected api token mapping")
}
if apiInfo.IsSession {
t.Fatalf("api token info = %+v, want session=false", apiInfo)
}
}
func TestRevokeTokenRollsBackMemoryOnPersistenceFailure(t *testing.T) {
store, _ := createTestStorage(t)
store.SetSQLStore(failingRevokePersister{})
if err := store.AddSessionToken("tok", 1); err != nil {
t.Fatal(err)
}
if err := store.RevokeToken("tok"); err == nil {
t.Fatal("expected RevokeToken to propagate persistence failure")
}
if store.IsTokenRevoked("tok") {
t.Fatal("failed revocation must not leave in-memory revoked state")
}
if _, ok := store.GetHashedTokenInfo("tok"); !ok {
t.Fatal("failed revocation must restore owner mapping")
}
}
func TestRetireTokenRollsBackMemoryOnPersistenceFailure(t *testing.T) {
store, _ := createTestStorage(t)
store.SetSQLStore(failingRevokePersister{})
if err := store.AddSessionToken("tok", 1); err != nil {
t.Fatal(err)
}
if err := store.RetireToken("tok"); err == nil {
t.Fatal("expected RetireToken to propagate persistence failure")
}
if store.IsTokenRevoked("tok") {
t.Fatal("failed retirement must not leave in-memory revoked state")
}
if _, ok := store.GetHashedTokenInfo("tok"); !ok {
t.Fatal("failed retirement must keep owner mapping")
}
}
func TestRevokeTokenIsImmediate(t *testing.T) {
store, _ := createTestStorage(t)
if err := store.AddSessionToken("tok", 1); err != nil {
t.Fatal(err)
}
if store.IsTokenRevoked("tok") {
t.Fatal("token must not be revoked before RevokeToken")
}
if err := store.RevokeToken("tok"); err != nil {
t.Fatal(err)
}
if !store.IsTokenRevoked("tok") {
t.Fatal("RevokeToken must take effect immediately")
}
if _, ok := store.GetHashedTokenInfo("tok"); ok {
t.Fatal("immediate revocation must remove the owner mapping")
}
}
func TestRetireTokenHonorsGraceWindow(t *testing.T) {
store, _ := createTestStorage(t)
if err := store.AddSessionToken("tok", 1); err != nil {
t.Fatal(err)
}
if err := store.RetireToken("tok"); err != nil {
t.Fatal(err)
}
if store.IsTokenRevoked("tok") {
t.Fatal("retired token must remain valid during the grace window")
}
if _, ok := store.GetHashedTokenInfo("tok"); !ok {
t.Fatal("retired token mapping must remain during the grace window")
}
// Simulate the grace window elapsing.
store.RevokedTokens[utils.HashSHA256("tok")] = time.Now().Add(-3 * time.Minute).Unix()
if !store.IsTokenRevoked("tok") {
t.Fatal("retired token must be revoked once the grace window elapses")
}
}
+53 -2
View File
@@ -6,7 +6,7 @@ import (
)
// currentSchemaVersion is the SQLite schema marker for this codebase.
const currentSchemaVersion = 2
const currentSchemaVersion = 3
// Schema creates all tables for the SQLite database
func createSchema(db *sql.DB) error {
@@ -77,7 +77,8 @@ func createSchema(db *sql.DB) error {
-- Hashed tokens (minimal JWT → owner user_id)
CREATE TABLE IF NOT EXISTS hashed_tokens (
token_hash TEXT PRIMARY KEY,
user_id TEXT NOT NULL
user_id TEXT NOT NULL,
is_session INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_hashed_tokens_user_id ON hashed_tokens(user_id);
@@ -185,6 +186,10 @@ func runMigrations(db *sql.DB, fromVersion int) error {
if err := normalizeLegacyShareTokens(db); err != nil {
return err
}
case 3:
if err := addHashedTokenSessionColumn(db); err != nil {
return err
}
default:
return fmt.Errorf("unknown schema version: %d", v)
}
@@ -198,3 +203,49 @@ func runMigrations(db *sql.DB, fromVersion int) error {
return nil
}
// addHashedTokenSessionColumn adds the is_session flag to pre-existing
// hashed_tokens tables. Existing rows are treated as non-session (API) tokens.
func addHashedTokenSessionColumn(db *sql.DB) error {
hasColumn, err := tableHasColumn(db, "hashed_tokens", "is_session")
if err != nil {
return err
}
if hasColumn {
return nil
}
if _, err := db.Exec(`ALTER TABLE hashed_tokens ADD COLUMN is_session INTEGER NOT NULL DEFAULT 0`); err != nil {
return fmt.Errorf("failed to add hashed_tokens.is_session: %w", err)
}
return nil
}
// tableHasColumn reports whether table/column exists in the current schema.
func tableHasColumn(db *sql.DB, table, column string) (bool, error) {
rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
if err != nil {
return false, fmt.Errorf("failed to inspect table %s: %w", table, err)
}
defer rows.Close()
for rows.Next() {
var (
cid int
name string
ctype string
notNull int
dfltValue sql.NullString
pk int
)
if err := rows.Scan(&cid, &name, &ctype, &notNull, &dfltValue, &pk); err != nil {
return false, fmt.Errorf("failed to scan table_info(%s): %w", table, err)
}
if name == column {
return true, nil
}
}
if err := rows.Err(); err != nil {
return false, fmt.Errorf("error iterating table_info(%s): %w", table, err)
}
return false, nil
}
+6 -1
View File
@@ -3,9 +3,14 @@ package sqldb
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
)
// ErrSettingNotFound is returned by GetSetting when the key has no persisted row.
// Callers must treat this as absence, not as an I/O failure.
var ErrSettingNotFound = errors.New("setting not found")
// Settings SQL operations
// GetSetting retrieves a setting by key
@@ -16,7 +21,7 @@ func (s *SQLStore) GetSetting(key string) ([]byte, error) {
err := s.db.QueryRow(query, key).Scan(&value)
if err == sql.ErrNoRows {
return nil, fmt.Errorf("setting not found: %s", key)
return nil, fmt.Errorf("%w: %s", ErrSettingNotFound, key)
}
if err != nil {
return nil, fmt.Errorf("failed to get setting: %w", err)
+107 -32
View File
@@ -1,21 +1,101 @@
package sqldb
import (
"database/sql"
"fmt"
"strconv"
"time"
)
// SaveRevokedToken adds a token hash to the revoked tokens table
func (s *SQLStore) SaveRevokedToken(tokenHash string) error {
query := `INSERT OR IGNORE INTO revoked_tokens (token_hash, revoked_at) VALUES (?, ?)`
_, err := s.db.Exec(query, tokenHash, time.Now().Unix())
func execSaveRevokedToken(exec sqlExecutor, tokenHash string, revokedAt int64) error {
query := `INSERT OR REPLACE INTO revoked_tokens (token_hash, revoked_at) VALUES (?, ?)`
_, err := exec.Exec(query, tokenHash, revokedAt)
if err != nil {
return fmt.Errorf("failed to save revoked token: %w", err)
}
return nil
}
func execDeleteHashedToken(exec sqlExecutor, tokenHash string) error {
query := `DELETE FROM hashed_tokens WHERE token_hash = ?`
_, err := exec.Exec(query, tokenHash)
if err != nil {
return fmt.Errorf("failed to delete hashed token: %w", err)
}
return nil
}
func execDeleteRevokedToken(exec sqlExecutor, tokenHash string) error {
query := `DELETE FROM revoked_tokens WHERE token_hash = ?`
_, err := exec.Exec(query, tokenHash)
if err != nil {
return fmt.Errorf("failed to delete revoked token: %w", err)
}
return nil
}
type sqlExecutor interface {
Exec(query string, args ...any) (sql.Result, error)
}
// HashedTokenRecord is a persisted bearer-token hash → owner mapping.
type HashedTokenRecord struct {
UserID uint64
IsSession bool
}
// PersistImmediateTokenRevocation records an immediate revocation and removes the
// owner mapping in one transaction (revocation row first, then hashed_tokens).
func (s *SQLStore) PersistImmediateTokenRevocation(tokenHash string) error {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("begin immediate token revocation transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
if err := execSaveRevokedToken(tx, tokenHash, 0); err != nil {
return err
}
if err := execDeleteHashedToken(tx, tokenHash); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit immediate token revocation transaction: %w", err)
}
return nil
}
// PersistTokenRetirement records a grace-window revocation and removes expired
// revocation rows and owner mappings atomically.
func (s *SQLStore) PersistTokenRetirement(tokenHash string, revokedAt int64, pruned []string) error {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("begin token retirement transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
if err := execSaveRevokedToken(tx, tokenHash, revokedAt); err != nil {
return err
}
for _, hash := range pruned {
if err := execDeleteHashedToken(tx, hash); err != nil {
return err
}
if err := execDeleteRevokedToken(tx, hash); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit token retirement transaction: %w", err)
}
return nil
}
// SaveRevokedToken persists a revocation. revokedAt is the Unix timestamp of the
// revocation; 0 marks an immediate revocation (no grace window).
func (s *SQLStore) SaveRevokedToken(tokenHash string, revokedAt int64) error {
return execSaveRevokedToken(s.db, tokenHash, revokedAt)
}
// IsTokenRevoked checks if a token hash is in the revoked tokens table
func (s *SQLStore) IsTokenRevoked(tokenHash string) (bool, error) {
query := `SELECT 1 FROM revoked_tokens WHERE token_hash = ?`
@@ -30,22 +110,24 @@ func (s *SQLStore) IsTokenRevoked(tokenHash string) (bool, error) {
return true, nil
}
// GetAllRevokedTokens retrieves all revoked token hashes
func (s *SQLStore) GetAllRevokedTokens() (map[string]struct{}, error) {
query := `SELECT token_hash FROM revoked_tokens`
// GetAllRevokedTokens retrieves all revoked token hashes mapped to their
// revocation timestamp (0 = immediate revocation).
func (s *SQLStore) GetAllRevokedTokens() (map[string]int64, error) {
query := `SELECT token_hash, revoked_at FROM revoked_tokens`
rows, err := s.db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to get revoked tokens: %w", err)
}
defer rows.Close()
revokedTokens := make(map[string]struct{})
revokedTokens := make(map[string]int64)
for rows.Next() {
var tokenHash string
if err := rows.Scan(&tokenHash); err != nil {
var revokedAt int64
if err := rows.Scan(&tokenHash, &revokedAt); err != nil {
return nil, fmt.Errorf("failed to scan revoked token: %w", err)
}
revokedTokens[tokenHash] = struct{}{}
revokedTokens[tokenHash] = revokedAt
}
if err := rows.Err(); err != nil {
@@ -57,18 +139,15 @@ func (s *SQLStore) GetAllRevokedTokens() (map[string]struct{}, error) {
// DeleteRevokedToken removes a token hash from the revoked tokens table
func (s *SQLStore) DeleteRevokedToken(tokenHash string) error {
query := `DELETE FROM revoked_tokens WHERE token_hash = ?`
_, err := s.db.Exec(query, tokenHash)
if err != nil {
return fmt.Errorf("failed to delete revoked token: %w", err)
}
return nil
return execDeleteRevokedToken(s.db, tokenHash)
}
// SaveHashedToken saves a token hash to owner user_id mapping (decimal text).
func (s *SQLStore) SaveHashedToken(tokenHash string, userID uint64) error {
query := `INSERT OR REPLACE INTO hashed_tokens (token_hash, user_id) VALUES (?, ?)`
_, err := s.db.Exec(query, tokenHash, strconv.FormatUint(userID, 10))
// isSession records that the bearer token is a FileBrowser session token rather
// than a named API token, so auth can reject non-session tokens without caps.
func (s *SQLStore) SaveHashedToken(tokenHash string, userID uint64, isSession bool) error {
query := `INSERT OR REPLACE INTO hashed_tokens (token_hash, user_id, is_session) VALUES (?, ?, ?)`
_, err := s.db.Exec(query, tokenHash, strconv.FormatUint(userID, 10), isSession)
if err != nil {
return fmt.Errorf("failed to save hashed token: %w", err)
}
@@ -89,26 +168,27 @@ func (s *SQLStore) GetUserIDByTokenHash(tokenHash string) (uint64, error) {
return strconv.ParseUint(idStr, 10, 64)
}
// GetAllHashedTokens retrieves all token hash → owner user_id mappings.
func (s *SQLStore) GetAllHashedTokens() (map[string]uint64, error) {
query := `SELECT token_hash, user_id FROM hashed_tokens`
// GetAllHashedTokens retrieves all token hash → owner mappings.
func (s *SQLStore) GetAllHashedTokens() (map[string]HashedTokenRecord, error) {
query := `SELECT token_hash, user_id, is_session FROM hashed_tokens`
rows, err := s.db.Query(query)
if err != nil {
return nil, fmt.Errorf("failed to get hashed tokens: %w", err)
}
defer rows.Close()
hashedTokens := make(map[string]uint64)
hashedTokens := make(map[string]HashedTokenRecord)
for rows.Next() {
var tokenHash, idStr string
if err := rows.Scan(&tokenHash, &idStr); err != nil {
var isSession bool
if err := rows.Scan(&tokenHash, &idStr, &isSession); err != nil {
return nil, fmt.Errorf("failed to scan hashed token: %w", err)
}
uid, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid user_id in hashed_tokens: %w", err)
}
hashedTokens[tokenHash] = uid
hashedTokens[tokenHash] = HashedTokenRecord{UserID: uid, IsSession: isSession}
}
if err := rows.Err(); err != nil {
@@ -120,12 +200,7 @@ func (s *SQLStore) GetAllHashedTokens() (map[string]uint64, error) {
// DeleteHashedToken removes a token hash mapping
func (s *SQLStore) DeleteHashedToken(tokenHash string) error {
query := `DELETE FROM hashed_tokens WHERE token_hash = ?`
_, err := s.db.Exec(query, tokenHash)
if err != nil {
return fmt.Errorf("failed to delete hashed token: %w", err)
}
return nil
return execDeleteHashedToken(s.db, tokenHash)
}
// DeleteHashedTokensByUserID removes all token hashes for an owner user id.
@@ -0,0 +1,65 @@
package sqldb
import (
"errors"
"path/filepath"
"testing"
)
func TestMigrationAddsHashedTokenSessionColumn(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "legacy-tokens.db")
store, _, err := NewSQLStore(dbPath)
if err != nil {
t.Fatalf("NewSQLStore: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
// Recreate the pre-v3 hashed_tokens table without is_session.
if _, err = store.db.Exec(`DROP TABLE hashed_tokens`); err != nil {
t.Fatalf("drop hashed_tokens: %v", err)
}
if _, err = store.db.Exec(`CREATE TABLE hashed_tokens (token_hash TEXT PRIMARY KEY, user_id TEXT NOT NULL)`); err != nil {
t.Fatalf("create legacy hashed_tokens: %v", err)
}
if _, err = store.db.Exec(`INSERT INTO hashed_tokens (token_hash, user_id) VALUES ('legacy-hash', '7')`); err != nil {
t.Fatalf("insert legacy hashed token: %v", err)
}
if _, err = store.db.Exec(`UPDATE schema_version SET version = 2`); err != nil {
t.Fatalf("set schema version: %v", err)
}
if err = runMigrations(store.db, 2); err != nil {
t.Fatalf("runMigrations: %v", err)
}
if err = store.SaveHashedToken("new-hash", 8, true); err != nil {
t.Fatalf("SaveHashedToken after migration: %v", err)
}
records, err := store.GetAllHashedTokens()
if err != nil {
t.Fatalf("GetAllHashedTokens: %v", err)
}
legacy, ok := records["legacy-hash"]
if !ok || legacy.UserID != 7 {
t.Fatalf("legacy mapping = %+v, ok=%v; want user 7", legacy, ok)
}
if legacy.IsSession {
t.Fatal("legacy rows must migrate as non-session tokens")
}
if !records["new-hash"].IsSession {
t.Fatal("session flag was not persisted")
}
}
func TestGetSettingMissingReturnsSentinel(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "settings.db")
store, _, err := NewSQLStore(dbPath)
if err != nil {
t.Fatalf("NewSQLStore: %v", err)
}
t.Cleanup(func() { _ = store.Close() })
if _, err := store.GetSetting("does.not.exist"); !errors.Is(err, ErrSettingNotFound) {
t.Fatalf("GetSetting missing error = %v, want ErrSettingNotFound", err)
}
}
@@ -26,5 +26,5 @@ func HasAnyGlobalPermission(p Permissions) bool {
// IsMinimalApiToken reports whether the token inherits the owner's globals at auth time
// rather than embedding its own global permission caps in the JWT.
func IsMinimalApiToken(t AuthToken) bool {
return t.BelongsTo == 0 || !HasAnyGlobalPermission(t.Permissions)
return !HasAnyGlobalPermission(t.Permissions)
}
@@ -41,10 +41,10 @@ func TestIsMinimalApiToken(t *testing.T) {
if !IsMinimalApiToken(AuthToken{}) {
t.Fatal("empty token should be minimal")
}
if IsMinimalApiToken(AuthToken{BelongsTo: 1, Permissions: Permissions{Admin: true}}) {
if IsMinimalApiToken(AuthToken{Permissions: Permissions{Admin: true}}) {
t.Fatal("custom token with global caps should not be minimal")
}
if !IsMinimalApiToken(AuthToken{BelongsTo: 1, Permissions: Permissions{Modify: true}}) {
if !IsMinimalApiToken(AuthToken{Permissions: Permissions{Modify: true}}) {
t.Fatal("token without global caps should be minimal")
}
}
+1 -1
View File
@@ -77,7 +77,7 @@ type AuthToken struct {
Token string `json:"token,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
BelongsTo uint64 `json:"belongsTo,omitempty"` // numeric user id in JWT claims (bolt-era small ids still work)
BelongsTo uint64 `json:"belongsTo,omitempty"` // legacy stored metadata only; identity is hashed_tokens lookup
IssuedAt int64 `json:"issuedAt,omitempty"`
ExpiresAt int64 `json:"expiresAt,omitempty"`
Permissions Permissions `json:"Permissions,omitempty"`
+32
View File
@@ -1,6 +1,8 @@
package state
import (
"fmt"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
@@ -112,6 +114,36 @@ func AddApiToken(tokenString string, userID uint64) error {
return accessDb.AddApiToken(tokenString, userID)
}
// RegisterSessionToken maps a session bearer JWT to its owner user id.
func RegisterSessionToken(tokenString string, userID uint64) error {
if accessDb == nil {
return fmt.Errorf("access storage not available")
}
return accessDb.AddSessionToken(tokenString, userID)
}
// RetireSessionToken schedules a rotated session token for revocation after a
// short grace window so in-flight requests carrying the old cookie still work.
func RetireSessionToken(tokenString string) error {
if accessDb == nil {
return fmt.Errorf("access storage not available")
}
return accessDb.RetireToken(tokenString)
}
// HashedTokenOwner returns the owner id and session type for a registered bearer
// token hash. ok is false when the hash has no mapping (unknown or already revoked).
func HashedTokenOwner(tokenString string) (userID uint64, isSession bool, ok bool) {
if accessDb == nil {
return 0, false, false
}
info, found := accessDb.GetHashedTokenInfo(tokenString)
if !found {
return 0, false, false
}
return info.UserID, info.IsSession, true
}
func RemoveApiToken(tokenString string) error {
return accessDb.RemoveApiToken(tokenString)
}
+119
View File
@@ -0,0 +1,119 @@
package state
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqldb"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
"github.com/gtsteffaniak/go-logger/logger"
)
const authSigningKeySetting = "auth.signingKey"
// errAuthSigningKeyAbsent marks a missing persisted signing key. It is distinct
// from a corrupt row, which must abort startup instead of being overwritten.
var errAuthSigningKeyAbsent = errors.New("auth signing key not persisted")
// InitAuthSigningKey loads or creates the JWT HMAC secret and persists it in SQLite settings.
func InitAuthSigningKey() error {
if sqlDb == nil {
return fmt.Errorf("database not initialized")
}
key, err := resolveAuthSigningKey(settings.Config.Auth.Key)
if err != nil {
return err
}
settings.Config.Auth.Key = key
return nil
}
// resolveAuthSigningKey returns the key to use without mutating config until the
// key is known to be valid. Only a missing persisted row counts as absence; a
// malformed row or an unreadable legacy database is a hard error so an existing
// key is never silently replaced (which would invalidate all issued JWTs).
func resolveAuthSigningKey(explicitKey string) (string, error) {
storedKey, storedErr := loadPersistedAuthSigningKey()
switch {
case storedErr == nil && storedKey != "":
if explicitKey == "" {
return storedKey, nil
}
if explicitKey != storedKey {
logger.Warning("auth signing key from config/env differs from value stored in the application database; using config/env for this process")
}
return explicitKey, nil
case errors.Is(storedErr, errAuthSigningKeyAbsent):
// No persisted key yet: persist the configured key or fall back below.
default:
return "", fmt.Errorf("load persisted auth signing key: %w", storedErr)
}
if explicitKey != "" {
if err := persistAuthSigningKey(explicitKey); err != nil {
return "", fmt.Errorf("persist auth signing key: %w", err)
}
return explicitKey, nil
}
boltPath := settings.Config.Server.DatabaseV2.MigrateFrom
legacyKey, legacyErr := ReadAuthSigningKeyFromBoltFile(boltPath)
switch {
case legacyErr == nil && legacyKey != "":
if err := persistAuthSigningKey(legacyKey); err != nil {
return "", fmt.Errorf("persist auth signing key: %w", err)
}
logger.Info("Recovered auth signing key from legacy database file")
return legacyKey, nil
case errors.Is(legacyErr, os.ErrNotExist):
// No legacy key to recover.
default:
return "", fmt.Errorf("read legacy auth signing key: %w", legacyErr)
}
generated := utils.GenerateKey()
if generated == "" {
return "", fmt.Errorf("failed to generate auth signing key")
}
if err := persistAuthSigningKey(generated); err != nil {
return "", fmt.Errorf("persist auth signing key: %w", err)
}
logger.Info("Generated a new auth signing key (stored in the application database)")
return generated, nil
}
func loadPersistedAuthSigningKey() (string, error) {
raw, err := sqlDb.GetSetting(authSigningKeySetting)
if err != nil {
if errors.Is(err, sqldb.ErrSettingNotFound) {
return "", errAuthSigningKeyAbsent
}
return "", err
}
var encoded string
if err = json.Unmarshal(raw, &encoded); err != nil {
return "", fmt.Errorf("parse auth signing key: %w", err)
}
if encoded == "" {
return "", errors.New("empty auth signing key in database")
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
// Legacy row: raw secret string (may be invalid UTF-8).
return encoded, nil
}
if len(decoded) == 0 {
return "", errors.New("empty auth signing key in database")
}
return string(decoded), nil
}
func persistAuthSigningKey(key string) error {
encoded := base64.StdEncoding.EncodeToString([]byte(key))
return sqlDb.SaveSetting(authSigningKeySetting, encoded)
}
@@ -0,0 +1,111 @@
package state
import (
"os"
"path/filepath"
"testing"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqldb"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
)
// newAuthSigningKeyTestStore opens an isolated SQLite store and points the
// package-level sqlDb at it.
func newAuthSigningKeyTestStore(t *testing.T) *sqldb.SQLStore {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "test.sqlite")
store, _, err := sqldb.NewSQLStoreWithOptions(dbPath, sqldb.NewSQLStoreOpts{SkipQuickSetup: true})
if err != nil {
t.Fatal(err)
}
sqlDb = store
t.Cleanup(func() {
_ = store.Close()
sqlDb = nil
})
return store
}
// preserveAuthKey restores settings.Config.Auth.Key after the test so ordering
// between tests cannot leak a generated key.
func preserveAuthKey(t *testing.T) {
t.Helper()
original := settings.Config.Auth.Key
t.Cleanup(func() { settings.Config.Auth.Key = original })
}
func TestInitAuthSigningKeyPersistsAndReloads(t *testing.T) {
newAuthSigningKeyTestStore(t)
preserveAuthKey(t)
settings.Config.Auth.Key = ""
if err := InitAuthSigningKey(); err != nil {
t.Fatal(err)
}
first := settings.Config.Auth.Key
if first == "" {
t.Fatal("expected generated key")
}
settings.Config.Auth.Key = ""
if err := InitAuthSigningKey(); err != nil {
t.Fatal(err)
}
if settings.Config.Auth.Key != first {
t.Fatalf("expected same key after reload, got different values")
}
}
func TestInitAuthSigningKeyRejectsMalformedPersistedRow(t *testing.T) {
store := newAuthSigningKeyTestStore(t)
preserveAuthKey(t)
// A corrupt row must abort startup instead of being overwritten with a new key.
if _, err := store.DB().Exec(
`INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`,
authSigningKeySetting, []byte("{not-json"),
); err != nil {
t.Fatal(err)
}
settings.Config.Auth.Key = "explicit-config-key"
err := InitAuthSigningKey()
if err == nil {
t.Fatal("expected error for malformed persisted signing key")
}
if settings.Config.Auth.Key != "explicit-config-key" {
t.Fatalf("config key was mutated on failure: %q", settings.Config.Auth.Key)
}
raw, getErr := store.GetSetting(authSigningKeySetting)
if getErr != nil {
t.Fatal(getErr)
}
if string(raw) != "{not-json" {
t.Fatalf("malformed row was overwritten: %q", string(raw))
}
}
func TestInitAuthSigningKeyRejectsLegacyReadFailure(t *testing.T) {
newAuthSigningKeyTestStore(t)
preserveAuthKey(t)
originalMigrateFrom := settings.Config.Server.DatabaseV2.MigrateFrom
t.Cleanup(func() { settings.Config.Server.DatabaseV2.MigrateFrom = originalMigrateFrom })
// A file that exists but is not a valid Bolt database must not be treated as
// an absent key (which would generate and persist a replacement).
corruptPath := filepath.Join(t.TempDir(), "corrupt.db")
if err := os.WriteFile(corruptPath, []byte("this is not a bolt database"), 0o600); err != nil {
t.Fatal(err)
}
settings.Config.Auth.Key = ""
settings.Config.Server.DatabaseV2.MigrateFrom = corruptPath
if err := InitAuthSigningKey(); err == nil {
t.Fatal("expected error for unreadable legacy database")
}
if settings.Config.Auth.Key != "" {
t.Fatalf("signing key was generated despite legacy read failure: %q", settings.Config.Auth.Key)
}
}
+54
View File
@@ -0,0 +1,54 @@
package state
import (
"os"
storm "github.com/asdine/storm/v3"
)
// boltSettingsAuth is the legacy Bolt "config"/"settings" document shape we need for migration.
type boltSettingsAuth struct {
Auth struct {
Key string `json:"key"`
} `json:"auth"`
}
// AuthSigningKeyFromBoltDB reads auth.key from an open legacy Bolt database.
func AuthSigningKeyFromBoltDB(db *storm.DB) (string, error) {
if db == nil {
return "", os.ErrNotExist
}
var set boltSettingsAuth
if err := db.Get("config", "settings", &set); err != nil {
if err == storm.ErrNotFound {
return "", os.ErrNotExist
}
return "", err
}
if set.Auth.Key == "" {
return "", os.ErrNotExist
}
return set.Auth.Key, nil
}
// ReadAuthSigningKeyFromBoltFile reads the legacy BoltDB settings auth key (config/settings bucket).
func ReadAuthSigningKeyFromBoltFile(path string) (string, error) {
if path == "" {
return "", os.ErrNotExist
}
info, err := os.Stat(path)
if err != nil {
return "", err
}
if info.Size() == 0 {
return "", os.ErrNotExist
}
db, err := storm.Open(path)
if err != nil {
return "", err
}
defer db.Close()
return AuthSigningKeyFromBoltDB(db)
}
@@ -0,0 +1,83 @@
package state
import (
"fmt"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/sqldb"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/go-logger/logger"
)
// BackfillHashedTokensFromUserRecords registers SHA256(raw JWT) → user_id for stored API tokens.
func BackfillHashedTokensFromUserRecords() error {
if sqlDb == nil {
return nil
}
list, err := sqlDb.ListUsers()
if err != nil {
return err
}
added := 0
for _, user := range list {
if user == nil || user.ID == 0 {
continue
}
n, err := backfillUserTokenHashes(user)
if err != nil {
return fmt.Errorf("backfill hashed token for user %s: %w", user.Username, err)
}
added += n
}
if added > 0 {
logger.Infof("Backfilled %d hashed token mapping(s) from user API token records", added)
}
return nil
}
// BackfillUserTokenHashesOnStore writes hash mappings for a user's stored token strings (migration helper).
func BackfillUserTokenHashesOnStore(store *sqldb.SQLStore, user *users.User) error {
if store == nil || user == nil || user.ID == 0 {
return nil
}
for _, tok := range user.Tokens {
raw := tok.Token
if raw == "" {
raw = tok.Key
}
if raw == "" {
continue
}
if err := store.SaveHashedToken(utils.HashSHA256(raw), user.ID, false); err != nil {
return err
}
}
return nil
}
// backfillUserTokenHashes maps each stored named API token to its owner. A
// persistence failure is returned so initialization aborts instead of leaving a
// mapping that silently stops working after restart.
func backfillUserTokenHashes(user *users.User) (int, error) {
if accessDb == nil || user == nil || user.ID == 0 {
return 0, nil
}
added := 0
for _, tok := range user.Tokens {
raw := tok.Token
if raw == "" {
raw = tok.Key
}
if raw == "" {
continue
}
if _, ok := accessDb.GetUserIDFromToken(raw); ok {
continue
}
if err := AddApiToken(raw, user.ID); err != nil {
return added, err
}
added++
}
return added, nil
}
@@ -0,0 +1,49 @@
package state
import (
"errors"
"path/filepath"
"testing"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
)
// failingTokenPersister fails only SaveHashedToken so backfill error handling can
// be exercised without a live database failure.
type failingTokenPersister struct {
access.SQLPersister
}
func (failingTokenPersister) SaveHashedToken(string, uint64, bool) error {
return errors.New("simulated hashed token persistence failure")
}
func TestBackfillHashedTokensPropagatesPersistenceFailure(t *testing.T) {
t.Setenv("FILEBROWSER_ONLYOFFICE_SECRET", "")
settings.Initialize("../../../_docker/src/noauth/backend/config.yaml")
settings.Env.IsPlaywright = true
dbPath := filepath.Join(t.TempDir(), "filebrowser.sqlite")
if _, err := Initialize(dbPath); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = Close() })
const username = "backfill-user"
user := &users.User{FrontendUser: users.FrontendUser{Username: username}}
if err := CreateUser(user, "password"); err != nil {
t.Fatal(err)
}
if err := AddUserToken(username, users.AuthToken{Name: "ci-key", Token: "raw-jwt-token"}); err != nil {
t.Fatal(err)
}
// Force persistence of the backfilled mapping to fail.
accessDb.SetSQLStore(failingTokenPersister{})
if err := BackfillHashedTokensFromUserRecords(); err == nil {
t.Fatal("expected backfill to propagate hashed token persistence failure")
}
}
-2
View File
@@ -17,8 +17,6 @@ func QuickSetup() error {
settings.Env.IsFirstLoad = true
}
settings.Config.Auth.Key = utils.GenerateKey()
passwordAuth := settings.Config.Auth.Methods.PasswordAuth.Enabled
noAuth := settings.Config.Auth.Methods.NoAuth
+17 -3
View File
@@ -67,6 +67,10 @@ func initialize(dbPath string) (bool, error) {
return existingDb, fmt.Errorf("failed to initialize source access defaults: %w", err)
}
if err = InitAuthSigningKey(); err != nil {
return existingDb, fmt.Errorf("failed to initialize auth signing key: %w", err)
}
var userCount int
if countErr := sqlDb.DB().QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount); countErr != nil {
return existingDb, fmt.Errorf("failed to count users: %w", countErr)
@@ -112,8 +116,8 @@ func initialize(dbPath string) (bool, error) {
accessDb = &access.Storage{
AllRules: make(access.SourceRuleMap),
Groups: make(access.GroupMap),
RevokedTokens: make(map[string]struct{}),
HashedTokens: make(map[string]uint64),
RevokedTokens: make(map[string]int64),
HashedTokens: make(map[string]access.HashedTokenInfo),
}
allRules, err := sqlDb.GetAllAccessRules()
@@ -141,11 +145,21 @@ func initialize(dbPath string) (bool, error) {
if err != nil {
return existingDb, fmt.Errorf("failed to load hashed tokens: %w", err)
}
accessDb.HashedTokens = hashedTokens
accessDb.HashedTokens = make(map[string]access.HashedTokenInfo, len(hashedTokens))
for hash, record := range hashedTokens {
accessDb.HashedTokens[hash] = access.HashedTokenInfo{
UserID: record.UserID,
IsSession: record.IsSession,
}
}
logger.Debugf("Loaded %d hashed tokens", len(hashedTokens))
accessDb.SetSQLStore(sqlDb)
if err = BackfillHashedTokensFromUserRecords(); err != nil {
return existingDb, fmt.Errorf("failed to backfill hashed tokens: %w", err)
}
err = auth.InitializeEncryption()
if err != nil {
return existingDb, fmt.Errorf("failed to initialize auth encryption: %w", err)
+1 -13
View File
@@ -93,7 +93,7 @@ func copyPinnedItems(in users.PinnedItems) users.PinnedItems {
return out
}
// GetUserByID retrieves a user by stable numeric id (JWT belongsTo, admin APIs).
// GetUserByID retrieves a user by stable numeric id (admin APIs, hashed_tokens lookup).
func GetUserByID(id uint64) (users.User, error) {
if id == 0 {
return users.User{}, errors.ErrNotExist
@@ -164,18 +164,6 @@ func GetAllUsers() ([]users.User, error) {
return out, nil
}
// UserFromAPIToken resolves the user for a validated API JWT: numeric belongsTo id, or minimal tokens
// (hash → user id). Usernames are not used so a reused login name never inherits a previous account's API access.
func UserFromAPIToken(tk users.AuthToken, rawToken string) (users.User, error) {
if tk.BelongsTo != 0 {
return GetUserByID(tk.BelongsTo)
}
if uid, ok := accessDb.GetUserIDFromToken(rawToken); ok {
return GetUserByID(uid)
}
return users.User{}, errors.ErrNotExist
}
// UserForShareOwner resolves the user who owns a share link.
func UserForShareOwner(link share.Share) (users.User, error) {
if link.UserID == 0 {
+33
View File
@@ -44,3 +44,36 @@ func GenerateRandomSearchTerms(numTerms int) []string {
}
return searchTerms
}
// Seeded variants used by mock listing generation so performance runs are
// byte-for-byte reproducible. Adding a seeded random source here rather than
// mutating the global rand keeps existing callers untouched.
var mockTerms = []string{
"hi", "test", "other", "name",
"cool", "things", "more", "items",
}
var mockExtensions = []string{
".txt", ".mp3", ".mov", ".doc",
".mp4", ".bak", ".zip", ".jpg",
}
// GetRandomTermSeeded returns a term from an explicit random source.
func GetRandomTermSeeded(r *rand.Rand) string {
return mockTerms[r.Intn(len(mockTerms))]
}
// GetRandomExtensionSeeded returns an extension from an explicit random source.
func GetRandomExtensionSeeded(r *rand.Rand) string {
return mockExtensions[r.Intn(len(mockExtensions))]
}
// GenerateRandomPathSeeded builds a path from an explicit random source.
func GenerateRandomPathSeeded(r *rand.Rand, levels int) string {
dirName := "srv"
for i := 0; i < levels; i++ {
dirName += "/" + GetRandomTermSeeded(r)
}
return dirName
}
+19 -15
View File
@@ -86,7 +86,7 @@ func TestCreateApiTokenCustomizedWithNoEffectiveCapsKeepsPermissionsClaim(t *tes
setupTestEnv(t)
setTestAuthKey(t)
// User can create API tokens but lacks admin; explicit customized token keeps Permissions in JWT.
// User can create API tokens but lacks admin; caps are stored in user.Tokens metadata, not JWT.
user := createApiTokenTestUser(t, "tokenuser2", users.Permissions{Api: true})
rec := invokeCreateApiToken(t, user, "/auth/token?name=custom&days=365&minimal=false&permissions=admin")
if rec.Code != http.StatusOK {
@@ -94,16 +94,18 @@ func TestCreateApiTokenCustomizedWithNoEffectiveCapsKeepsPermissionsClaim(t *tes
}
token := decodeCreatedToken(t, rec.Body.Bytes())
claims := jwt.MapClaims{}
if _, _, err := jwt.NewParser().ParseUnverified(token, claims); err != nil {
t.Fatalf("parse token: %v", err)
assertHandlerMinimalJWTClaims(t, token)
got, err := state.GetUserByUsername(user.Username)
if err != nil {
t.Fatal(err)
}
perms, ok := claims["Permissions"].(map[string]interface{})
stored, ok := got.Tokens["custom"]
if !ok {
t.Fatalf("customized token should include Permissions claim, got %v", claims)
t.Fatal("expected custom token metadata on user")
}
if perms["admin"] != false {
t.Fatalf("expected admin false in customized token, got %v", perms["admin"])
if stored.Permissions.Admin {
t.Fatalf("expected admin false in stored caps, got %+v", stored.Permissions)
}
}
@@ -118,16 +120,18 @@ func TestCreateApiTokenCustomizedWithEffectiveCaps(t *testing.T) {
}
token := decodeCreatedToken(t, rec.Body.Bytes())
claims := jwt.MapClaims{}
if _, _, err := jwt.NewParser().ParseUnverified(token, claims); err != nil {
t.Fatalf("parse token: %v", err)
assertHandlerMinimalJWTClaims(t, token)
got, err := state.GetUserByUsername(user.Username)
if err != nil {
t.Fatal(err)
}
perms, ok := claims["Permissions"].(map[string]interface{})
stored, ok := got.Tokens["custom"]
if !ok {
t.Fatalf("expected Permissions claim in customized token, got %v", claims)
t.Fatal("expected custom token metadata on user")
}
if perms["admin"] != true || perms["api"] != true {
t.Fatalf("expected admin and api true, got %v", perms)
if !stored.Permissions.Admin || !stored.Permissions.Api {
t.Fatalf("expected admin and api in stored caps, got %+v", stored.Permissions)
}
}
+6 -4
View File
@@ -538,8 +538,9 @@ func addFile(source string, path string, d *Context, tarWriter *tar.Writer, zipW
return fmt.Errorf("source %s is not available", source)
}
permUser := accessCheckUsername(d)
// Check access control directly for each file and silently skip if access is denied
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), d.User.Username) {
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), permUser) {
return nil // Silently skip this file/folder
}
@@ -578,7 +579,7 @@ func addFile(source string, path string, d *Context, tarWriter *tar.Writer, zipW
indexRelPath := utils.JoinPathAsUnix(path, relPath)
indexRelPath = filepath.ToSlash(indexRelPath)
indexPath := utils.IndexPathFromNormalized(indexRelPath, true)
if !state.AccessPermitted(idx.Path, indexPath, d.User.Username) {
if !state.AccessPermitted(idx.Path, indexPath, permUser) {
if fileInfo.IsDir() {
return filepath.SkipDir
}
@@ -754,7 +755,7 @@ func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *Context, s
}
realPath, _, err := idx.GetRealPath(fileList[0])
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to get real path for %s: %v", fileList[0], err)
return realPathErrStatus(err), err
}
algo := r.URL.Query().Get("algo")
@@ -1269,8 +1270,9 @@ func computeArchiveSize(source string, fileList []string, d *Context) (int64, er
return 0, fmt.Errorf("source %s is not available", source)
}
permUser := accessCheckUsername(d)
for _, path := range fileList {
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), d.User.Username) {
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(path, true), permUser) {
continue
}
realPath, isDir, err := idx.GetRealPath(path)
+12 -14
View File
@@ -20,7 +20,6 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/errors"
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
"github.com/gtsteffaniak/go-logger/logger"
)
@@ -206,7 +205,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, erro
if d.User.HasPasskeyMFA() && d.User.TOTPSecret == "" {
return http.StatusForbidden, errors.ErrPasskeyMFARequired
}
status, err := printToken(w, r, d.User)
status, err := printToken(w, r, d.User, "")
if err != nil || status != 0 {
return status, err
}
@@ -338,12 +337,12 @@ func signupHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, err
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/auth/renew [post]
func renewHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, error) {
return printToken(w, r, d.User)
return printToken(w, r, d.User, d.Token)
}
func printToken(w http.ResponseWriter, r *http.Request, user *users.User) (int, error) {
func printToken(w http.ResponseWriter, r *http.Request, user *users.User, priorToken string) (int, error) {
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
tokenString, err := replaceSessionToken(priorToken, user)
if err != nil {
if strings.Contains(err.Error(), "key already exists with same name") {
return http.StatusConflict, err
@@ -351,8 +350,6 @@ func printToken(w http.ResponseWriter, r *http.Request, user *users.User) (int,
return 401, errors.ErrUnauthorized
}
// Add 30 minutes buffer so expired token doesn't get automatically deleted by the browser
// This allows backend to identify expired sessions and provide better user feedback
expiresTime := time.Now().Add(expires).Add(time.Minute * 30)
SetSessionCookie(w, r, tokenString, expiresTime)
@@ -485,17 +482,18 @@ func SetSessionCookie(w http.ResponseWriter, r *http.Request, token string, expi
http.SetCookie(w, sessionCookie(r, token, expiresTime, 0, http.SameSiteStrictMode))
}
// applyNamedApiTokenGlobalCaps intersects owner globals with JWT global caps for named custom API tokens.
// Session WEB_TOKEN_* tokens and minimal API tokens (no global caps in claims) keep full owner globals.
func applyNamedApiTokenGlobalCaps(user *users.User, tk users.AuthToken, tokenName string) {
if user == nil {
// applyNamedApiTokenGlobalCaps intersects owner globals with stored caps for named custom API tokens.
// Session tokens never reach this function; they are resolved as IsSession and keep full owner globals.
func applyNamedApiTokenGlobalCaps(user *users.User, tokenName string) {
if user == nil || user.Tokens == nil {
return
}
if strings.HasPrefix(tokenName, "WEB_TOKEN") {
stored, ok := user.Tokens[tokenName]
if !ok {
return
}
if tk.BelongsTo == 0 || !users.HasAnyGlobalPermission(tk.Permissions) {
if !users.HasAnyGlobalPermission(stored.Permissions) {
return
}
user.Permissions = users.IntersectGlobalPermissions(user.Permissions, tk.Permissions)
user.Permissions = users.IntersectGlobalPermissions(user.Permissions, stored.Permissions)
}
+17 -14
View File
@@ -178,10 +178,7 @@ func TestApplyNamedApiTokenGlobalCaps(t *testing.T) {
t.Run("session token unchanged", func(t *testing.T) {
user := *owner
applyNamedApiTokenGlobalCaps(&user, users.AuthToken{
BelongsTo: 1,
Permissions: users.Permissions{Admin: false},
}, "WEB_TOKEN_abcd")
applyNamedApiTokenGlobalCaps(&user, "WEB_TOKEN_abcd")
if !user.Permissions.Admin {
t.Fatal("session token should keep DB admin")
}
@@ -189,7 +186,10 @@ func TestApplyNamedApiTokenGlobalCaps(t *testing.T) {
t.Run("minimal api token unchanged", func(t *testing.T) {
user := *owner
applyNamedApiTokenGlobalCaps(&user, users.AuthToken{}, "my-key")
user.Tokens = map[string]users.AuthToken{
"my-key": {Name: "my-key", Permissions: users.Permissions{}},
}
applyNamedApiTokenGlobalCaps(&user, "my-key")
if !user.Permissions.Admin {
t.Fatal("minimal token should keep DB admin")
}
@@ -197,17 +197,20 @@ func TestApplyNamedApiTokenGlobalCaps(t *testing.T) {
t.Run("custom api token caps globals", func(t *testing.T) {
user := *owner
applyNamedApiTokenGlobalCaps(&user, users.AuthToken{
BelongsTo: 1,
Permissions: users.Permissions{
Admin: false,
Api: true,
Share: true,
Modify: true,
user.Tokens = map[string]users.AuthToken{
"customized": {
Name: "customized",
Permissions: users.Permissions{
Admin: false,
Api: true,
Share: true,
Modify: true,
},
},
}, "customized")
}
applyNamedApiTokenGlobalCaps(&user, "customized")
if user.Permissions.Admin {
t.Fatal("custom token should cap admin from JWT")
t.Fatal("custom token should cap admin from stored metadata")
}
if !user.Permissions.Api || !user.Permissions.Share {
t.Fatalf("expected api and share, got %#v", user.Permissions)
+10 -2
View File
@@ -250,9 +250,17 @@ func RawFilesHandler(w http.ResponseWriter, r *http.Request, d *Context, source
if idx == nil {
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", source)
}
_, isDir, err := idx.GetRealPath(firstFilePath)
var isDir bool
if d.Share.Hash != "" {
if d.Share.Path == "" {
return http.StatusForbidden, fmt.Errorf("share has no path scope")
}
_, isDir, err = idx.GetRealPathScoped(d.Share.Path, firstFilePath)
} else {
_, isDir, err = idx.GetRealPath(firstFilePath)
}
if err != nil {
return http.StatusInternalServerError, err
return realPathErrStatus(err), err
}
if len(fileList) == 1 && !isDir {
+311
View File
@@ -0,0 +1,311 @@
package web
import (
"archive/zip"
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/gtsteffaniak/filebrowser/backend/internal/app"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
)
func setupShareArchiveDownloadTest(t *testing.T) (sourceRoot string, sourceName string, owner *users.User) {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "share-dl-test.sqlite")
if _, err := state.Initialize(dbPath); err != nil {
t.Fatal(err)
}
app.MustWireServices(state.Default())
t.Cleanup(func() { state.Close() })
sourceRoot = t.TempDir()
sharedDir := filepath.Join(sourceRoot, "shared")
if err := os.MkdirAll(sharedDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sharedDir, "hello.txt"), []byte("share-zip-contents"), 0o644); err != nil {
t.Fatal(err)
}
sourceName = "data"
settings.Config.Server.SourceMap = map[string]*settings.Source{
sourceRoot: {
Path: sourceRoot,
Name: sourceName,
Config: settings.SourceConfig{
DenyByDefault: true,
},
},
}
settings.Config.Server.NameToSource = map[string]*settings.Source{
sourceName: settings.Config.Server.SourceMap[sourceRoot],
}
settings.InitializeUserResolvers()
owner = &users.User{
ID: 1,
FrontendUser: users.FrontendUser{
Username: "owner",
},
}
if err := state.CreateUser(owner, ""); err != nil {
t.Fatal(err)
}
if err := state.AllowUser(sourceRoot, utils.IndexPathFromNormalized("/shared", true), "owner"); err != nil {
t.Fatal(err)
}
indexing.SetTestIndex(sourceName, sourceRoot)
t.Cleanup(func() { indexing.ClearTestIndices() })
return sourceRoot, sourceName, owner
}
func shareArchiveContext(owner *users.User, sharePath string) *Context {
return &Context{
User: &users.User{
FrontendUser: users.FrontendUser{Username: users.AnonymousUserName},
},
ShareUser: owner,
Share: share.Share{
ShareColumns: share.ShareColumns{
Hash: "testshare",
Path: sharePath,
},
},
}
}
func readZipFileNames(data []byte) []string {
r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil
}
names := make([]string, 0, len(r.File))
for _, f := range r.File {
names = append(names, f.Name)
}
return names
}
func TestCreateZipPublicShareUsesShareOwnerAccessRules(t *testing.T) {
_, sourceName, owner := setupShareArchiveDownloadTest(t)
d := shareArchiveContext(owner, "/shared")
var buf bytes.Buffer
if err := createZip(d, sourceName, &buf, "/shared"); err != nil {
t.Fatalf("createZip: %v", err)
}
names := readZipFileNames(buf.Bytes())
if len(names) == 0 {
t.Fatal("expected non-empty zip for public share archive (owner has allow rule)")
}
found := false
for _, n := range names {
if n == "shared/hello.txt" || n == "hello.txt" {
found = true
break
}
}
if !found {
t.Fatalf("zip entries %v should include shared file", names)
}
// Without ShareUser, anonymous is denied under denyByDefault → empty archive.
dBroken := &Context{
User: &users.User{
FrontendUser: users.FrontendUser{Username: users.AnonymousUserName},
},
Share: share.Share{
ShareColumns: share.ShareColumns{
Hash: "testshare",
Path: "/shared",
},
},
}
var emptyBuf bytes.Buffer
if err := createZip(dBroken, sourceName, &emptyBuf, "/shared"); err != nil {
t.Fatalf("createZip: %v", err)
}
if len(readZipFileNames(emptyBuf.Bytes())) != 0 {
t.Fatal("expected empty zip when access is evaluated as anonymous only")
}
}
func TestComputeArchiveSizePublicShareUsesShareOwner(t *testing.T) {
_, sourceName, _ := setupShareArchiveDownloadTest(t)
dBroken := &Context{
User: &users.User{
FrontendUser: users.FrontendUser{Username: users.AnonymousUserName},
},
Share: share.Share{
ShareColumns: share.ShareColumns{
Hash: "testshare",
Path: "/shared",
},
},
}
sizeBroken, err := computeArchiveSize(sourceName, []string{"/shared"}, dBroken)
if err != nil {
t.Fatalf("computeArchiveSize: %v", err)
}
if sizeBroken != 0 {
t.Fatalf("expected zero estimated size for anonymous-only access, got %d", sizeBroken)
}
}
func TestBuildAndStreamArchivePublicShareHEADProducesArchive(t *testing.T) {
_, sourceName, owner := setupShareArchiveDownloadTest(t)
settings.Config.Server.CacheDir = t.TempDir()
if err := os.MkdirAll(settings.DownloadCacheDir(), 0o755); err != nil {
t.Fatal(err)
}
settings.Config.Server.MaxArchiveSizeGB = 0
d := shareArchiveContext(owner, "/shared")
req := httptest.NewRequest(http.MethodHead, "/?algo=zip", nil)
rec := httptest.NewRecorder()
status, err := BuildAndStreamArchive(rec, req, d, sourceName, []string{"/shared"})
if err != nil {
t.Fatalf("BuildAndStreamArchive HEAD: status=%d err=%v", status, err)
}
cl := rec.Header().Get("Content-Length")
if cl == "" || cl == "0" {
t.Fatalf("expected non-zero Content-Length on HEAD archive, got %q", cl)
}
token := rec.Header().Get("X-Archive-Token")
if token == "" {
t.Fatal("expected X-Archive-Token for chunked archive session")
}
}
func TestRawFilesHandlerShareDirectoryDownloadNotEmptyZip(t *testing.T) {
sourceRoot, sourceName, owner := setupShareArchiveDownloadTest(t)
settings.Config.Server.NameToSource[sourceName].Path = sourceRoot
d := shareArchiveContext(owner, "/shared")
req := httptest.NewRequest(http.MethodGet, "/public/api/resources/download?hash=testshare&file=%2F&algo=zip", nil)
rec := httptest.NewRecorder()
status, err := RawFilesHandler(rec, req, d, sourceName, []string{"/shared"})
if err != nil {
t.Fatalf("RawFilesHandler: status=%d err=%v", status, err)
}
if status != http.StatusOK && status != 0 {
t.Fatalf("unexpected status %d", status)
}
body, readErr := io.ReadAll(rec.Body)
if readErr != nil {
t.Fatal(readErr)
}
if len(readZipFileNames(body)) == 0 {
t.Fatal("expected non-empty zip body from share directory download")
}
}
func TestRawFilesHandler_MissingFileReturns404(t *testing.T) {
sourceRoot, sourceName, _ := setupShareArchiveDownloadTest(t)
ownerUser := users.User{
FrontendUser: users.FrontendUser{Username: "owner"},
BackendScopes: []users.BackendScope{{
Path: sourceRoot,
Scope: "/",
Permissions: users.SourceFilePermissions{
View: true, Download: true, Modify: true, Create: true, Delete: true,
},
}},
}
users.SyncBackendSourcePermissionsMap(&ownerUser)
d := &Context{User: &ownerUser}
req := httptest.NewRequest(http.MethodGet, "/api/resources/download?source="+sourceName+"&file=/shared/missing.txt", nil)
rec := httptest.NewRecorder()
status, err := RawFilesHandler(rec, req, d, sourceName, []string{"/shared/missing.txt"})
if err == nil {
t.Fatal("expected error for missing file")
}
if status != http.StatusNotFound {
t.Fatalf("expected 404, got status=%d err=%v", status, err)
}
}
func TestPublicDownload_MissingFileReturns404(t *testing.T) {
sourceRoot, _, owner := setupShareArchiveDownloadTest(t)
d := shareArchiveContext(owner, "/shared")
d.Share.SourcePath = sourceRoot
req := httptest.NewRequest(http.MethodGet, "/public/api/resources/download?hash=testshare&file=missing.txt", nil)
rec := httptest.NewRecorder()
status, err := publicDownloadHandler(rec, req, d)
if err == nil {
t.Fatal("expected error for missing file")
}
if status != http.StatusNotFound {
t.Fatalf("expected 404, got status=%d err=%v", status, err)
}
msg := err.Error()
if !strings.Contains(msg, "error processing filelist") {
t.Fatalf("expected masked share error, got %q", msg)
}
if strings.Contains(msg, sourceRoot) {
t.Fatalf("share error should not leak host path, got %q", msg)
}
}
func TestBuildAndStreamArchive_MissingPathReturns404(t *testing.T) {
_, sourceName, owner := setupShareArchiveDownloadTest(t)
d := shareArchiveContext(owner, "/shared")
req := httptest.NewRequest(http.MethodGet, "/?algo=zip", nil)
rec := httptest.NewRecorder()
status, err := BuildAndStreamArchive(rec, req, d, sourceName, []string{"/shared/missing.txt"})
if err == nil {
t.Fatal("expected error for missing archive path")
}
if status != http.StatusNotFound {
t.Fatalf("expected 404, got status=%d err=%v", status, err)
}
}
func TestServeSingleFile_MissingScopedPathReturns404(t *testing.T) {
sourceRoot, sourceName, _ := setupShareArchiveDownloadTest(t)
ownerUser := users.User{
FrontendUser: users.FrontendUser{Username: "owner"},
BackendScopes: []users.BackendScope{{
Path: sourceRoot,
Scope: "/",
Permissions: users.SourceFilePermissions{
View: true, Download: true, Modify: true, Create: true, Delete: true,
},
}},
}
users.SyncBackendSourcePermissionsMap(&ownerUser)
d := &Context{User: &ownerUser}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
status, err := ServeSingleFile(rec, req, d, sourceName, "/shared/missing.txt", "missing.txt", ServeSingleFileOptions{})
if err == nil {
t.Fatal("expected error for missing file")
}
if status != http.StatusNotFound {
t.Fatalf("expected 404, got status=%d err=%v", status, err)
}
}
@@ -0,0 +1 @@
hi
+22 -2
View File
@@ -53,6 +53,18 @@ func effectiveFilePerms(d *Context, sourceName string) (users.SourceFilePermissi
return share.EffectiveFilePermissions(d.User, link, sourceName)
}
// accessCheckUsername is the username used for filesystem access-rule checks.
// Public share routes evaluate rules as the share owner, not the anonymous visitor.
func accessCheckUsername(d *Context) string {
if d == nil || d.User == nil {
return ""
}
if d.Share.Hash != "" && d.ShareUser != nil {
return d.ShareUser.Username
}
return d.User.Username
}
// HttpResponse is the standard JSON error/success envelope.
type HttpResponse struct {
Status int `json:"status,omitempty"`
@@ -130,9 +142,9 @@ func ErrToStatus(err error) int {
return http.StatusForbidden
case errors.Is(err, libErrors.ErrPathEscapesScope):
return http.StatusForbidden
case os.IsNotExist(err), err == libErrors.ErrNotExist:
case os.IsNotExist(err), errors.Is(err, os.ErrNotExist), err == libErrors.ErrNotExist:
return http.StatusNotFound
case os.IsExist(err), err == libErrors.ErrExist:
case os.IsExist(err), errors.Is(err, os.ErrExist), err == libErrors.ErrExist:
return http.StatusConflict
case errors.Is(err, libErrors.ErrPermissionDenied):
return http.StatusForbidden
@@ -147,6 +159,14 @@ func ErrToStatus(err error) int {
}
}
// realPathErrStatus maps filesystem path resolution errors to HTTP status codes for download/view/archive paths.
func realPathErrStatus(err error) int {
if errors.Is(err, libErrors.ErrPathEscapesScope) {
return http.StatusForbidden
}
return ErrToStatus(err)
}
// RenderJSON writes a JSON response, optionally gzip-compressed.
func RenderJSON(w http.ResponseWriter, r *http.Request, data interface{}, statusCode ...int) (int, error) {
code := http.StatusOK
+2
View File
@@ -164,6 +164,8 @@ func configureHTTPRouter(router, api, publicRoutes, publicApi *http.ServeMux) {
api.HandleFunc("GET /events", withUser(SSEHandler))
if settings.Env.IsDevMode {
api.HandleFunc("GET /inspect-index", inspectIndex)
}
if settings.Env.IsDevMode || settings.Env.IsPlaywright {
api.HandleFunc("GET /mock-data", mockData)
}
@@ -0,0 +1,34 @@
package web
import (
"fmt"
"net/http"
"os"
"path/filepath"
"testing"
libErrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
)
func TestRealPathErrStatus(t *testing.T) {
if realPathErrStatus(libErrors.ErrPathEscapesScope) != http.StatusForbidden {
t.Fatal("expected 403 for ErrPathEscapesScope")
}
if realPathErrStatus(os.ErrNotExist) != http.StatusNotFound {
t.Fatal("expected 404 for ErrNotExist")
}
if realPathErrStatus(fmt.Errorf("disk failure")) != http.StatusInternalServerError {
t.Fatal("expected 500 for unknown error")
}
dir := t.TempDir()
missing := filepath.Join(dir, "gone")
_, _, err := iteminfo.ResolveSymlinks(missing)
if err == nil {
t.Fatal("expected resolve error")
}
if realPathErrStatus(err) != http.StatusNotFound {
t.Fatalf("expected 404 for ResolveSymlinks missing file, got %d (%v)", realPathErrStatus(err), err)
}
}
+37 -29
View File
@@ -190,9 +190,7 @@ func extractUserFromExpiredToken(r *http.Request, data *requestContext) *users.U
return &userValue
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return []byte(settings.Config.Auth.Key), nil
}
keyFunc := auth.JWTSigningKeyFunc()
tokenString, err := ExtractToken(r)
if err != nil {
@@ -216,11 +214,7 @@ func extractUserFromExpiredToken(r *http.Request, data *requestContext) *users.U
if state.IsTokenRevoked(tokenString) {
return nil
}
var user *users.User
userValue, err := state.UserFromAPIToken(tk, tokenString)
if err == nil {
user = &userValue
}
user, err := resolveBearerTokenUser(tokenString)
if err != nil {
logger.Errorf("Failed to get user from token: %v", err)
return nil
@@ -233,6 +227,30 @@ func extractUserFromExpiredToken(r *http.Request, data *requestContext) *users.U
return user
}
// resolveBearerTokenUser maps a validated bearer JWT to its owner via the
// hashed_tokens registry. Session tokens get full owner permissions; named API
// tokens must have stored metadata, otherwise the request is rejected rather
// than granted the owner's uncapped permissions.
func resolveBearerTokenUser(rawToken string) (*users.User, error) {
ownerID, isSession, ok := state.HashedTokenOwner(rawToken)
if !ok {
return nil, fmt.Errorf("token is invalid or revoked")
}
userValue, err := state.GetUserByID(ownerID)
if err != nil {
return nil, err
}
if isSession {
return &userValue, nil
}
tokenName, ok := state.TokenNameForRawToken(&userValue, rawToken)
if !ok {
return nil, fmt.Errorf("token has no permission metadata")
}
applyNamedApiTokenGlobalCaps(&userValue, tokenName)
return &userValue, nil
}
// withOrWithoutUserHelper is a middleware that tries to authenticate a user.
// If authentication is successful, the user is added to the request context.
// If authentication fails, the request continues without a user.
@@ -344,16 +362,13 @@ func LoginHelper(disableOtp bool, fn handleFunc) handleFunc {
}
// Check if request has a valid admin token first
if tokenStr, err := ExtractToken(r); err == nil && tokenStr != "" {
keyFunc := func(token *jwt.Token) (interface{}, error) {
return []byte(settings.Config.Auth.Key), nil
}
keyFunc := auth.JWTSigningKeyFunc()
var tk users.AuthToken
if token, err := jwt.ParseWithClaims(tokenStr, &tk, keyFunc); err == nil && token.Valid {
if !state.IsTokenRevoked( tokenStr) {
userValue, err := state.UserFromAPIToken(tk, tokenStr)
if !state.IsTokenRevoked(tokenStr) {
userValue, err := resolveBearerTokenUser(tokenStr)
if err == nil && userValue.Permissions.Admin {
u := userValue
d.User = &u
d.User = userValue
return fn(w, r, d)
}
}
@@ -431,9 +446,7 @@ func withUserHelper(fn handleFunc) handleFunc {
proxyUser := r.Header.Get(settings.Config.Auth.Methods.ProxyAuth.Header)
isProxyUser := settings.Config.Auth.Methods.ProxyAuth.Enabled && proxyUser != ""
keyFunc := func(token *jwt.Token) (interface{}, error) {
return []byte(settings.Config.Auth.Key), nil
}
keyFunc := auth.JWTSigningKeyFunc()
if data.Token == "" {
var err error
data.Token, err = ExtractToken(r)
@@ -462,17 +475,12 @@ func withUserHelper(fn handleFunc) handleFunc {
if tk.RegisteredClaims.ExpiresAt == nil {
return http.StatusUnauthorized, fmt.Errorf("token is invalid or revoked")
}
userValue, err := state.UserFromAPIToken(tk, data.Token)
if err == nil {
data.User = &userValue
}
userValue, err := resolveBearerTokenUser(data.Token)
if err != nil {
logger.Errorf("Failed to get user from token: %v", err)
return http.StatusUnauthorized, fmt.Errorf("token is invalid or revoked")
}
if tokenName, ok := state.TokenNameForRawToken(data.User, data.Token); ok {
applyNamedApiTokenGlobalCaps(data.User, tk, tokenName)
}
data.User = userValue
// Set cookie. Some clients like gvfs relies on it for concurrent uploads
if tk.RegisteredClaims.ExpiresAt != nil {
@@ -519,13 +527,13 @@ func getJwtUser(w http.ResponseWriter, r *http.Request, data *requestContext, fn
// Generate a FileBrowser session token for JWT users if they don't have one
if data.Token == "" {
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
tokenString, err := mintAndRegisterSessionToken(user)
if err != nil {
logger.Errorf("Failed to generate token for JWT user %s: %v", username, err)
return http.StatusInternalServerError, fmt.Errorf("failed to generate token")
}
data.Token = tokenString
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
SetSessionCookie(w, r, tokenString, time.Now().Add(expires).Add(time.Minute*30))
}
@@ -552,13 +560,13 @@ func getProxyUser(w http.ResponseWriter, r *http.Request, data *requestContext,
}
// Generate a token for proxy users if they don't have one
if data.Token == "" {
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
tokenString, err := mintAndRegisterSessionToken(user)
if err != nil {
logger.Errorf("Failed to generate token for proxy user %s: %v", proxyUser, err)
return http.StatusInternalServerError, fmt.Errorf("failed to generate token")
}
data.Token = tokenString
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
SetSessionCookie(w, r, tokenString, time.Now().Add(expires).Add(time.Minute*30))
}
// Call the handler function, passing in the context (or return OK if no handler)
+18 -12
View File
@@ -50,6 +50,21 @@ func setupTestEnv(t *testing.T) {
mockFileInfoFaster(t) // Mock FileInfoFasterFunc for this test
}
func testSessionToken(t *testing.T, user *users.User, duration time.Duration) string {
t.Helper()
if duration == 0 {
duration = time.Hour * 2
}
tokenString, _, err := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), duration, user.Permissions, true)
if err != nil {
t.Fatalf("MakeSignedTokenAPI: %v", err)
}
if err := state.RegisterSessionToken(tokenString, user.ID); err != nil {
t.Fatalf("RegisterSessionToken: %v", err)
}
return tokenString
}
func mockFileInfoFaster(t *testing.T) {
// Backup the original function
originalFileInfoFaster := FileInfoFasterFunc
@@ -123,10 +138,7 @@ func TestWithAdminHelper(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tokenString, _, err := auth.MakeSignedTokenAPI(tc.user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*2, tc.user.Permissions, false)
if err != nil {
t.Fatalf("Error making token for request: %v", err)
}
tokenString := testSessionToken(t, tc.user, time.Hour*2)
handler := withAdmin(mockHandler)
@@ -173,10 +185,7 @@ func TestPublicShare_RejectsRevokedJWT(t *testing.T) {
settings.Config.Auth.Key = "key"
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
tokenString, _, err := auth.MakeSignedTokenAPI(victim, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*2, victim.Permissions, false)
if err != nil {
t.Fatalf("failed to issue token: %v", err)
}
tokenString := testSessionToken(t, victim, time.Hour*2)
shareLink := &share.Share{
ShareSettings: share.ShareSettings{
@@ -311,10 +320,7 @@ func issueExtractUserTestToken(t *testing.T, duration time.Duration) (*users.Use
settings.Config.Auth.Key = "key"
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
tokenString, _, err := auth.MakeSignedTokenAPI(&user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), duration, user.Permissions, false)
if err != nil {
t.Fatalf("failed to issue token: %v", err)
}
tokenString := testSessionToken(t, &user, duration)
return &user, tokenString
}
+1 -3
View File
@@ -13,7 +13,6 @@ import (
"github.com/coreos/go-oidc/v3/oidc"
"github.com/gtsteffaniak/filebrowser/backend/internal/activity"
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/go-logger/logger"
@@ -383,8 +382,7 @@ func loginWithOidcUser(w http.ResponseWriter, r *http.Request, username string,
}
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
// Generate a signed token for the user
tokenString, _, err2 := auth.MakeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions, false)
tokenString, err2 := mintAndRegisterSessionToken(user)
if err2 != nil {
// Handle potential errors during token generation
if strings.Contains(err2.Error(), "key already exists with same name") {
+131 -84
View File
@@ -1,7 +1,6 @@
package web
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -469,6 +468,10 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *Contex
}
path = cleanPath
if err := validateOnlyOfficeCallbackKey(source, path, user, data); err != nil {
return returnOnlyOfficeError(w, r, 400, err.Error())
}
// Handle document closure - clean up document key cache
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
data.Status == onlyOfficeStatusDocumentClosedWithNoChanges {
@@ -705,18 +708,11 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *Contex
// parseOnlyOfficeCallbackFromJWT extracts callback data from JWT in Authorization header
func parseOnlyOfficeCallbackFromJWT(r *http.Request) (*OnlyOfficeCallback, error) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return nil, errors.New("missing Authorization header")
jwtToken := onlyOfficeCallbackBearerToken(r.Header.Get("Authorization"))
if jwtToken == "" {
return nil, errors.New("missing OnlyOffice callback JWT in Authorization header")
}
if !strings.HasPrefix(authHeader, "Bearer ") {
return nil, errors.New("invalid Authorization header format")
}
jwtToken := strings.TrimPrefix(authHeader, "Bearer ")
return parseOnlyOfficeJWT(jwtToken)
return parseOnlyOfficeCallbackToken(jwtToken)
}
// parseOnlyOfficeCallbackFromJSON extracts callback data from JSON request body
@@ -725,15 +721,134 @@ func parseOnlyOfficeCallbackFromJSON(r *http.Request) (*OnlyOfficeCallback, erro
if err != nil {
return nil, fmt.Errorf("failed to read request body: %v", err)
}
var data OnlyOfficeCallback
err = json.Unmarshal(body, &data)
if err != nil {
return nil, fmt.Errorf("failed to parse JSON: %v", err)
if len(body) == 0 {
return nil, errors.New("empty callback body")
}
secret := settings.Config.Integrations.OnlyOffice.Secret
if secret != "" {
var wrapper struct {
Token string `json:"token"`
}
if err := json.Unmarshal(body, &wrapper); err == nil && wrapper.Token != "" {
return parseOnlyOfficeCallbackToken(wrapper.Token)
}
if headerToken := onlyOfficeCallbackBearerToken(r.Header.Get("Authorization")); headerToken != "" {
return parseOnlyOfficeCallbackToken(headerToken)
}
return nil, errors.New("unsigned callback rejected when OnlyOffice JWT secret is configured")
}
var data OnlyOfficeCallback
if err := json.Unmarshal(body, &data); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %v", err)
}
if data.Key == "" {
return nil, errors.New("missing document key in callback JSON")
}
return &data, nil
}
func onlyOfficeCallbackBearerToken(authHeader string) string {
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
return ""
}
return strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer "))
}
// parseOnlyOfficeCallbackToken verifies (when secret is configured) and decodes an OnlyOffice callback JWT.
func parseOnlyOfficeCallbackToken(tokenString string) (*OnlyOfficeCallback, error) {
secret := settings.Config.Integrations.OnlyOffice.Secret
var claims jwt.MapClaims
if secret != "" {
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
if t.Method != jwt.SigningMethodHS256 {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, fmt.Errorf("invalid OnlyOffice callback JWT: %w", err)
}
if !token.Valid {
return nil, errors.New("invalid OnlyOffice callback JWT: token is not valid")
}
mapClaims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("invalid OnlyOffice callback JWT claims")
}
claims = mapClaims
} else {
parser := jwt.NewParser()
token, _, err := parser.ParseUnverified(tokenString, jwt.MapClaims{})
if err != nil {
return nil, fmt.Errorf("failed to parse OnlyOffice callback JWT: %w", err)
}
mapClaims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, errors.New("invalid OnlyOffice callback JWT claims")
}
claims = mapClaims
}
callback, err := onlyOfficeCallbackFromClaims(claims)
if err != nil {
return nil, err
}
if settings.Config.Integrations.OnlyOffice.Secret != "" && callback.Status == 0 {
return nil, errors.New("missing callback status in OnlyOffice JWT payload")
}
return callback, nil
}
func onlyOfficeCallbackFromClaims(claims jwt.MapClaims) (*OnlyOfficeCallback, error) {
payload, ok := claims["payload"].(map[string]interface{})
if !ok {
payload = map[string]interface{}(claims)
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to encode callback payload: %w", err)
}
var callback OnlyOfficeCallback
if err := json.Unmarshal(payloadBytes, &callback); err != nil {
return nil, fmt.Errorf("failed to decode callback payload: %w", err)
}
if callback.Key == "" {
return nil, errors.New("missing document key in callback payload")
}
return &callback, nil
}
// validateOnlyOfficeCallbackKey ensures the callback document key matches the active editor session.
// Fails closed when the path cannot be resolved or no editor session key is cached for that file.
func validateOnlyOfficeCallbackKey(source, path string, user *users.User, data *OnlyOfficeCallback) error {
if data.Key == "" {
return errors.New("missing document key in callback")
}
fi, err := files.FileInfoFaster(utils.FileOptions{
Path: path,
Source: source,
Expand: false,
FollowSymlinks: true,
}, user)
if err != nil {
return fmt.Errorf("could not resolve document for callback: %w", err)
}
if fi == nil || fi.RealPath == "" {
return errors.New("could not resolve document path for callback")
}
expectedKey, err := GetOnlyOfficeId(fi.RealPath)
if err != nil {
return errors.New("unknown or expired OnlyOffice editor session for document")
}
if expectedKey != data.Key {
return fmt.Errorf("document key mismatch for path %s", path)
}
return nil
}
func GetOnlyOfficeId(realpath string) (string, error) {
// error is intentionally ignored in order treat errors
// the same as a cache-miss
@@ -761,74 +876,6 @@ func deleteOfficeId(source, path string, user *users.User) {
utils.OnlyOfficeCache.Delete(key)
}
// parseOnlyOfficeJWT parses the JWT token from OnlyOffice callback
func parseOnlyOfficeJWT(tokenString string) (*OnlyOfficeCallback, error) {
// Parse the JWT token without signature verification since OnlyOffice uses different signing
// We'll parse it manually to avoid signature validation issues
parts := strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid JWT format")
}
// Decode the payload (second part) with fallback to standard base64
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
// Fallback to standard base64 decoding
payloadBytes, err = base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return nil, fmt.Errorf("failed to decode JWT payload: %v", err)
}
}
var claims jwt.MapClaims
err = json.Unmarshal(payloadBytes, &claims)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JWT claims: %v", err)
}
// Extract payload from claims with fallback
payload, ok := claims["payload"].(map[string]interface{})
if !ok {
// Fallback: try to use claims directly if no payload wrapper
payload = map[string]interface{}(claims)
}
// Convert to OnlyOfficeCallback struct with safe type assertions
callback := &OnlyOfficeCallback{}
// Extract key with validation
if key, ok := payload["key"].(string); ok && key != "" {
callback.Key = key
} else {
logger.Warningf("OnlyOffice callback: missing or empty key in JWT payload")
}
// Extract status with validation
if status, ok := payload["status"].(float64); ok {
callback.Status = int(status)
} else {
logger.Warningf("OnlyOffice callback: missing or invalid status in JWT payload")
callback.Status = 0 // Default to unknown status
}
// Extract users with safe array handling
if users, ok := payload["users"].([]interface{}); ok {
callback.Users = make([]string, 0, len(users))
for _, user := range users {
if userStr, ok := user.(string); ok && userStr != "" {
callback.Users = append(callback.Users, userStr)
}
}
}
// Validate essential fields
if callback.Key == "" {
return nil, fmt.Errorf("missing document key in JWT payload")
}
return callback, nil
}
// returnOnlyOfficeSuccess returns a success response to OnlyOffice server
func returnOnlyOfficeSuccess(w http.ResponseWriter, r *http.Request) (int, error) {
resp := map[string]int{
@@ -0,0 +1,200 @@
package web
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/files"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
)
func signOnlyOfficeCallbackJWT(secret string, payload map[string]interface{}) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"payload": payload,
})
signed, err := token.SignedString([]byte(secret))
if err != nil {
panic(err)
}
return signed
}
func TestParseOnlyOfficeCallbackTokenVerifiesAndMapsFields(t *testing.T) {
orig := settings.Config.Integrations.OnlyOffice
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
const secret = "onlyoffice-test-secret"
settings.Config.Integrations.OnlyOffice.Secret = secret
docURL := "http://office.example/cache/files/doc/output.docx"
token := signOnlyOfficeCallbackJWT(secret, map[string]interface{}{
"key": "doc-key-1",
"status": 2,
"url": docURL,
"users": []string{"user-1"},
})
callback, err := parseOnlyOfficeCallbackToken(token)
if err != nil {
t.Fatalf("parseOnlyOfficeCallbackToken() error = %v", err)
}
if callback.Key != "doc-key-1" {
t.Errorf("Key = %q, want doc-key-1", callback.Key)
}
if callback.Status != 2 {
t.Errorf("Status = %d, want 2", callback.Status)
}
if callback.URL != docURL {
t.Errorf("URL = %q, want %q", callback.URL, docURL)
}
if len(callback.Users) != 1 || callback.Users[0] != "user-1" {
t.Errorf("Users = %v, want [user-1]", callback.Users)
}
}
func TestParseOnlyOfficeCallbackTokenRejectsInvalidSignature(t *testing.T) {
orig := settings.Config.Integrations.OnlyOffice
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
settings.Config.Integrations.OnlyOffice.Secret = "expected-secret"
token := signOnlyOfficeCallbackJWT("other-secret", map[string]interface{}{
"key": "doc-key-1",
"status": 2,
})
_, err := parseOnlyOfficeCallbackToken(token)
if err == nil {
t.Fatal("expected error for invalid signature")
}
}
func TestParseOnlyOfficeCallbackFromJSONTokenBody(t *testing.T) {
orig := settings.Config.Integrations.OnlyOffice
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
const secret = "onlyoffice-test-secret"
settings.Config.Integrations.OnlyOffice.Secret = secret
token := signOnlyOfficeCallbackJWT(secret, map[string]interface{}{
"key": "doc-key-2",
"status": 1,
"url": "http://office.example/cache/save.docx",
})
body, err := json.Marshal(map[string]string{"token": token})
if err != nil {
t.Fatalf("marshal: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/api/office/callback", bytes.NewReader(body))
callback, err := parseOnlyOfficeCallbackFromJSON(req)
if err != nil {
t.Fatalf("parseOnlyOfficeCallbackFromJSON() error = %v", err)
}
if callback.Key != "doc-key-2" || callback.Status != 1 {
t.Fatalf("callback = %+v, want key doc-key-2 status 1", callback)
}
if callback.URL == "" {
t.Fatal("expected URL from JWT payload")
}
}
func TestParseOnlyOfficeCallbackFromJSONRejectsUnsignedWhenSecretSet(t *testing.T) {
orig := settings.Config.Integrations.OnlyOffice
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
settings.Config.Integrations.OnlyOffice.Secret = "onlyoffice-test-secret"
body := []byte(`{"key":"doc-key","status":2,"url":"http://office.example/x"}`)
req := httptest.NewRequest(http.MethodPost, "/api/office/callback", bytes.NewReader(body))
_, err := parseOnlyOfficeCallbackFromJSON(req)
if err == nil {
t.Fatal("expected error for unsigned callback body")
}
if !strings.Contains(err.Error(), "unsigned callback rejected") {
t.Fatalf("error = %v, want unsigned callback rejection", err)
}
}
func TestParseOnlyOfficeCallbackFromJSONPlainWhenNoSecret(t *testing.T) {
orig := settings.Config.Integrations.OnlyOffice
t.Cleanup(func() { settings.Config.Integrations.OnlyOffice = orig })
settings.Config.Integrations.OnlyOffice.Secret = ""
body := []byte(`{"key":"doc-key","status":2,"url":"http://office.example/x"}`)
req := httptest.NewRequest(http.MethodPost, "/api/office/callback", bytes.NewReader(body))
callback, err := parseOnlyOfficeCallbackFromJSON(req)
if err != nil {
t.Fatalf("parseOnlyOfficeCallbackFromJSON() error = %v", err)
}
if callback.Key != "doc-key" || callback.Status != 2 {
t.Fatalf("callback = %+v", callback)
}
}
func TestValidateOnlyOfficeCallbackKey(t *testing.T) {
const realPath = "/data/docs/report.docx"
const cacheKey = "oo-session-key"
user := &users.User{FrontendUser: users.FrontendUser{Username: "alice"}}
origFunc := files.FileInfoFasterFunc
t.Cleanup(func() { files.FileInfoFasterFunc = origFunc })
t.Run("match", func(t *testing.T) {
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
return &iteminfo.ExtendedFileInfo{RealPath: realPath}, nil
}
utils.OnlyOfficeCache.Set(realPath, cacheKey)
t.Cleanup(func() { utils.OnlyOfficeCache.Delete(realPath) })
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: cacheKey})
if err != nil {
t.Fatalf("validateOnlyOfficeCallbackKey() error = %v", err)
}
})
t.Run("mismatch", func(t *testing.T) {
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
return &iteminfo.ExtendedFileInfo{RealPath: realPath}, nil
}
utils.OnlyOfficeCache.Set(realPath, cacheKey)
t.Cleanup(func() { utils.OnlyOfficeCache.Delete(realPath) })
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: "other-key"})
if err == nil {
t.Fatal("expected document key mismatch error")
}
})
t.Run("cache miss", func(t *testing.T) {
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
return &iteminfo.ExtendedFileInfo{RealPath: realPath}, nil
}
utils.OnlyOfficeCache.Delete(realPath)
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: cacheKey})
if err == nil {
t.Fatal("expected error for cache miss")
}
})
t.Run("file lookup failure", func(t *testing.T) {
files.FileInfoFasterFunc = func(utils.FileOptions, *users.User) (*iteminfo.ExtendedFileInfo, error) {
return nil, fmt.Errorf("not found")
}
err := validateOnlyOfficeCallbackKey("source", "/report.docx", user, &OnlyOfficeCallback{Key: cacheKey})
if err == nil {
t.Fatal("expected error when file lookup fails")
}
})
}
+21 -3
View File
@@ -135,11 +135,20 @@ func validateMoveOperation(src, dst string, isSrcDir bool) error {
// @Param metadata query string false "When true, run audio/video metadata extraction, subtitles, and directory media batch processing"
// @Param checksum query string false "Optional checksum validation"
// @Success 200 {object} iteminfo.FileInfo "Resource metadata"
// @Failure 400 {object} map[string]string "Missing or invalid path query parameter"
// @Failure 404 {object} map[string]string "Resource not found"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/resources [get]
func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, error) {
if _, ok := r.URL.Query()["path"]; !ok {
return http.StatusBadRequest, fmt.Errorf("path query parameter is required")
}
path := r.URL.Query().Get("path")
cleanPath, err := utils.SanitizePath(path)
if err != nil {
return http.StatusBadRequest, err
}
path = cleanPath
source := r.URL.Query().Get("source")
filePerms, err := effectiveFilePerms(d, source)
if err != nil {
@@ -1056,7 +1065,8 @@ func publicUploadHandler(w http.ResponseWriter, r *http.Request, d *Context) (in
if d.Share.ShareType != "upload" && !d.Share.AllowCreate {
return http.StatusForbidden, fmt.Errorf("uploading is disabled for this share")
}
if !d.Share.AllowReplacements && r.URL.Query().Get("action") == "override" {
q := r.URL.Query()
if !d.Share.AllowReplacements && (q.Get("action") == "override" || q.Get("override") == "true") {
return http.StatusForbidden, fmt.Errorf("cannot overwrite files for this share")
}
sourceInfo, ok := settings.Config.Server.SourceMap[d.Share.SourcePath]
@@ -1064,7 +1074,6 @@ func publicUploadHandler(w http.ResponseWriter, r *http.Request, d *Context) (in
return http.StatusNotFound, fmt.Errorf("source not found")
}
source := sourceInfo.Name
q := r.URL.Query()
q.Set("source", source)
q.Set("path", d.IndexPath)
r.URL.RawQuery = q.Encode()
@@ -1647,7 +1656,16 @@ func mockData(w http.ResponseWriter, r *http.Request) {
if err != nil || err2 != nil {
return
}
mockDir := indexing.CreateMockData(NumDirs, numFiles)
// Optional explicit seed; 0 means derive deterministically from the shape.
var seed int64
if s := r.URL.Query().Get("seed"); s != "" {
parsed, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return
}
seed = parsed
}
mockDir := indexing.CreateMockDataSeeded(NumDirs, numFiles, seed)
RenderJSON(w, r, mockDir) // nolint:errcheck
}
+41
View File
@@ -0,0 +1,41 @@
package web
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
)
func TestResourceGetHandler_MissingOrEmptyPathReturns400(t *testing.T) {
d := &Context{
User: &users.User{
FrontendUser: users.FrontendUser{
Username: "admin",
Permissions: users.Permissions{Admin: true},
},
},
}
cases := []struct {
name string
query string
}{
{"missing path", "source=srv"},
{"empty path", "source=srv&path="},
{"path traversal", "source=srv&path=../../etc/passwd"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/resources?"+tc.query, nil)
status, err := resourceGetHandler(httptest.NewRecorder(), req, d)
if err == nil {
t.Fatal("expected error")
}
if status != http.StatusBadRequest {
t.Fatalf("expected 400, got %d (%v)", status, err)
}
})
}
}
@@ -0,0 +1,55 @@
package web
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/share"
)
func TestPublicUploadHandlerDeniesOverrideWhenReplacementsDisallowed(t *testing.T) {
t.Parallel()
d := &Context{
Share: share.Share{
ShareSettings: share.ShareSettings{
FrontendShareInfo: share.FrontendShareInfo{
ShareType: "upload",
AllowReplacements: false,
},
},
},
}
for _, query := range []string{"override=true", "action=override"} {
req := httptest.NewRequest(http.MethodPost, "/public/api/resources?"+query, nil)
status, err := publicUploadHandler(httptest.NewRecorder(), req, d)
if status != http.StatusForbidden {
t.Fatalf("query %q: expected 403, got %d", query, status)
}
if err == nil || err.Error() != "cannot overwrite files for this share" {
t.Fatalf("query %q: unexpected err: %v", query, err)
}
}
}
func TestPublicUploadHandlerAllowsOverrideQueryWhenReplacementsAllowed(t *testing.T) {
t.Parallel()
d := &Context{
Share: share.Share{
ShareSettings: share.ShareSettings{
FrontendShareInfo: share.FrontendShareInfo{
ShareType: "upload",
AllowReplacements: true,
},
},
},
}
req := httptest.NewRequest(http.MethodPost, "/public/api/resources?override=true", nil)
status, _ := publicUploadHandler(httptest.NewRecorder(), req, d)
if status == http.StatusForbidden {
t.Fatal("expected override guard not to forbid when AllowReplacements is true")
}
}
+51
View File
@@ -0,0 +1,51 @@
package web
import (
"fmt"
"time"
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
"github.com/gtsteffaniak/go-logger/logger"
)
// mintAndRegisterSessionToken creates a minimal session JWT and registers its hash for auth lookup.
func mintAndRegisterSessionToken(user *users.User) (string, error) {
if user == nil || user.ID == 0 {
return "", fmt.Errorf("invalid user for session token")
}
expires := time.Hour * time.Duration(settings.Config.Auth.TokenExpirationHours)
name := "WEB_TOKEN_" + utils.InsecureRandomIdentifier(4)
tokenString, _, err := auth.MakeSignedTokenAPI(user, name, expires, user.Permissions, true)
if err != nil {
return "", err
}
if err := state.RegisterSessionToken(tokenString, user.ID); err != nil {
return "", err
}
return tokenString, nil
}
// replaceSessionToken mints and registers the replacement before retiring the
// prior token. If minting or registration fails the current session is left
// untouched. The prior token is retired with a grace window (not revoked
// immediately) so requests already in flight with the old cookie stay valid.
func replaceSessionToken(oldToken string, user *users.User) (string, error) {
newToken, err := mintAndRegisterSessionToken(user)
if err != nil {
return "", err
}
if oldToken == "" || oldToken == newToken {
return newToken, nil
}
if err := state.RetireSessionToken(oldToken); err != nil {
if cleanupErr := state.RemoveApiToken(newToken); cleanupErr != nil {
logger.Errorf("failed to roll back replacement session token: %v", cleanupErr)
}
return "", err
}
return newToken, nil
}
@@ -0,0 +1,52 @@
package web
import (
"testing"
"time"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
)
func TestReplaceSessionTokenKeepsOldTokenWhenMintFails(t *testing.T) {
setupTestEnv(t)
user := createTokenAuthUser(t, "rotation-user", users.Permissions{Api: true})
oldToken := testSessionToken(t, user, time.Hour)
invalidUser := *user
invalidUser.ID = 0
if _, err := replaceSessionToken(oldToken, &invalidUser); err == nil {
t.Fatal("expected mint failure for user without an id")
}
if state.IsTokenRevoked(oldToken) {
t.Fatal("old token must not be retired when replacement minting fails")
}
if _, _, ok := state.HashedTokenOwner(oldToken); !ok {
t.Fatal("old token mapping must survive a failed replacement")
}
}
func TestReplaceSessionTokenRetiresOldWithGrace(t *testing.T) {
setupTestEnv(t)
user := createTokenAuthUser(t, "rotation-user-2", users.Permissions{Api: true})
oldToken := testSessionToken(t, user, time.Hour)
newToken, err := replaceSessionToken(oldToken, user)
if err != nil {
t.Fatal(err)
}
if newToken == oldToken {
t.Fatal("expected a new session token")
}
if _, isSession, ok := state.HashedTokenOwner(newToken); !ok || !isSession {
t.Fatalf("new token must be registered as a session: ok=%v isSession=%v", ok, isSession)
}
// The Playwright failures came from revoking the old token while in-flight
// requests still carried it. It must stay valid during the grace window.
if state.IsTokenRevoked(oldToken) {
t.Fatal("old token must remain valid during the retirement grace window")
}
if _, _, ok := state.HashedTokenOwner(oldToken); !ok {
t.Fatal("old token mapping must remain during the grace window")
}
}
+2
View File
@@ -277,6 +277,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"passkeyAvailable": settings.Config.Auth.Methods.PasskeyAuth.Enabled,
"passkeyLoginButtonText": settings.Config.Auth.Methods.PasskeyAuth.LoginButtonText,
"disablePWAInstall": settings.Config.Frontend.DisablePWAInstall,
"devMode": settings.Env.IsDevMode,
"playwrightTest": settings.Env.IsPlaywright,
}
cspNonce, err := utils.CSPNonce()
+3 -10
View File
@@ -14,7 +14,6 @@ import (
"time"
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
liberrors "github.com/gtsteffaniak/filebrowser/backend/internal/errors"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
@@ -424,10 +423,7 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
if idx == nil {
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", source)
}
permUser := d.User.Username
if d.Share.Hash != "" {
permUser = d.ShareUser.Username
}
permUser := accessCheckUsername(d)
if !state.AccessPermitted(idx.Path, utils.IndexPathFromNormalized(scopedFilePath, true), permUser) {
logger.Debugf("user %s denied access to path %s", permUser, scopedFilePath)
@@ -450,10 +446,7 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
realPath, _, err := idx.GetRealPathScoped(bound, scopedFilePath)
if err != nil {
if errors.Is(err, liberrors.ErrPathEscapesScope) {
return http.StatusForbidden, err
}
return http.StatusInternalServerError, err
return realPathErrStatus(err), err
}
isOnlyOffice := IsOnlyOfficeCompatibleFile(displayFileName) && settings.Config.Integrations.OnlyOffice.Url != ""
@@ -472,7 +465,7 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
SendOnlyOfficeLogEvent(logContext, "ERROR", "download",
fmt.Sprintf("OnlyOffice download failed - could not open file: %s - %v", scopedFilePath, err))
}
return http.StatusInternalServerError, err
return realPathErrStatus(err), err
}
defer fd.Close()
+96
View File
@@ -0,0 +1,96 @@
package web
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
)
func createTokenAuthUser(t *testing.T, username string, perms users.Permissions) *users.User {
t.Helper()
u := &users.User{FrontendUser: users.FrontendUser{Username: username}}
if err := state.CreateUser(u, ""); err != nil {
t.Fatal(err)
}
stored, err := state.GetUserByUsername(username)
if err != nil {
t.Fatal(err)
}
stored.Permissions = perms
if err = state.UpdateUser(&stored, "", "permissions"); err != nil {
t.Fatal(err)
}
reloaded, err := state.GetUserByUsername(username)
if err != nil {
t.Fatal(err)
}
return &reloaded
}
func cookieRequest(t *testing.T, handler http.HandlerFunc, token string) *httptest.ResponseRecorder {
t.Helper()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/users", http.NoBody)
req.AddCookie(&http.Cookie{Name: "filebrowser_quantum_jwt", Value: token})
handler(rec, req)
return rec
}
// A non-session bearer token whose hash has no stored permission metadata must be
// rejected rather than granted the owner's uncapped permissions.
func TestApiTokenWithoutMetadataRejected(t *testing.T) {
setupTestEnv(t)
originalAuthKey := settings.Config.Auth.Key
settings.Config.Auth.Key = "key"
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
user := createTokenAuthUser(t, "metadata-less-admin", users.Permissions{Admin: true, Api: true})
tokenString, _, err := auth.MakeSignedTokenAPI(user, "orphan-key", time.Hour, users.Permissions{}, true)
if err != nil {
t.Fatal(err)
}
// Register the hash mapping without any user.Tokens metadata.
if err := state.AddApiToken(tokenString, user.ID); err != nil {
t.Fatal(err)
}
rec := cookieRequest(t, withAdmin(mockHandler), tokenString)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("metadata-less API token: got status %d, want %d", rec.Code, http.StatusUnauthorized)
}
}
// A named API token with stored caps must have those caps applied at auth time.
func TestApiTokenCapsAppliedFromStoredMetadata(t *testing.T) {
setupTestEnv(t)
originalAuthKey := settings.Config.Auth.Key
settings.Config.Auth.Key = "key"
t.Cleanup(func() { settings.Config.Auth.Key = originalAuthKey })
user := createTokenAuthUser(t, "capped-admin", users.Permissions{Admin: true, Api: true})
// The token cap excludes admin, so the resolved user must lose admin.
tokenString, tokenMeta, err := auth.MakeSignedTokenAPI(user, "capped-key", time.Hour, users.Permissions{Share: true}, false)
if err != nil {
t.Fatal(err)
}
tokenMeta.Name = "capped-key"
tokenMeta.Token = tokenString
if err := state.AddUserToken(user.Username, tokenMeta); err != nil {
t.Fatal(err)
}
if err := state.AddApiToken(tokenString, user.ID); err != nil {
t.Fatal(err)
}
rec := cookieRequest(t, withAdmin(mockHandler), tokenString)
if rec.Code != http.StatusForbidden {
t.Fatalf("capped API token: got status %d, want %d", rec.Code, http.StatusForbidden)
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ func finishPasskeyLoginHandler(w http.ResponseWriter, r *http.Request, d *Contex
}
d.User = user
status, err := printToken(w, r, d.User)
status, err := printToken(w, r, d.User, "")
if err != nil || status != 0 {
return status, err
}
+2 -2
View File
@@ -139,11 +139,11 @@ func PathWithinRoot(root, path string) error {
func ResolveSymlinks(path string) (string, bool, error) {
resolvedPath, err := filepath.EvalSymlinks(path)
if err != nil {
return path, false, fmt.Errorf("could not resolve symlinks for %s: %v", path, err)
return path, false, fmt.Errorf("could not resolve symlinks for %s: %w", path, err)
}
info, err := os.Lstat(resolvedPath)
if err != nil {
return resolvedPath, false, fmt.Errorf("could not stat resolved path %s: %v", resolvedPath, err)
return resolvedPath, false, fmt.Errorf("could not stat resolved path %s: %w", resolvedPath, err)
}
return resolvedPath, IsDirectory(info), nil
}
@@ -0,0 +1,21 @@
package iteminfo
import (
"errors"
"os"
"path/filepath"
"testing"
)
func TestResolveSymlinks_NotExistIsWrappable(t *testing.T) {
dir := t.TempDir()
missing := filepath.Join(dir, "does-not-exist")
_, _, err := ResolveSymlinks(missing)
if err == nil {
t.Fatal("expected error for missing path")
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("expected errors.Is(ErrNotExist), got %v", err)
}
}
+113 -17
View File
@@ -2,6 +2,9 @@ package indexing
import (
"math/rand"
"mime"
"path/filepath"
"strings"
"time"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
@@ -9,18 +12,20 @@ import (
)
func (idx *Index) CreateMockData(numDirs, numFilesPerDir int) {
r := rand.New(rand.NewSource(int64(numDirs*1_000_003 + numFilesPerDir)))
for i := 0; i < numDirs; i++ {
dirPath := utils.GenerateRandomPath(rand.Intn(3) + 1)
dirPath := utils.GenerateRandomPathSeeded(r, r.Intn(3)+1)
files := []iteminfo.ExtendedItemInfo{}
// Simulating files and directories with ExtendedItemInfo
for j := 0; j < numFilesPerDir; j++ {
name := "file-" + utils.GetRandomTermSeeded(r) + utils.GetRandomExtensionSeeded(r)
newFile := iteminfo.ExtendedItemInfo{
ItemInfo: iteminfo.ItemInfo{
Name: "file-" + utils.GetRandomTerm() + utils.GetRandomExtension(),
Size: rand.Int63n(1000),
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour),
Type: "blob",
Name: name,
Size: mockSizeFor(name, r),
ModTime: time.Now().Add(-time.Duration(r.Intn(100)) * time.Hour),
Type: mockMimeType(name),
},
}
files = append(files, newFile)
@@ -34,29 +39,120 @@ func (idx *Index) CreateMockData(numDirs, numFilesPerDir int) {
}
}
// CreateMockData builds a deterministic listing for performance measurement.
//
// Determinism matters: mock row names, sizes and mtimes feed directly into DOM
// text length and JSON payload size, so unseeded randomness added real variance
// between runs and made payload metrics impossible to baseline. The seed is
// derived from the requested shape, so the same numDirs/numFiles always yields
// identical output. Pass a non-zero seed to override.
func CreateMockData(numDirs, numFilesPerDir int) iteminfo.FileInfo {
return CreateMockDataSeeded(numDirs, numFilesPerDir, 0)
}
// CreateMockDataSeeded is CreateMockData with an explicit seed.
func CreateMockDataSeeded(numDirs, numFilesPerDir int, seed int64) iteminfo.FileInfo {
if seed == 0 {
seed = MockDataSeed(numDirs, numFilesPerDir)
}
// Fixed time base: using time.Now() here made every row's ModTime differ
// between runs, which changed formatted cell text and thus DOM payload.
baseTime := time.Date(2024, time.January, 1, 0, 0, 0, 0, time.UTC)
r := rand.New(rand.NewSource(seed))
dir := iteminfo.FileInfo{}
dir.Path = "/here/is/your/mock/dir"
// Folders must genuinely be directories. The frontend decides whether a row
// is navigable purely from `item.type === "directory"`, so emitting
// file-shaped names with a file MIME type here produced rows that looked
// like folders in the payload but behaved as files in the UI.
for i := 0; i < numDirs; i++ {
newFile := iteminfo.ItemInfo{
Name: "file-" + utils.GetRandomTerm() + utils.GetRandomExtension(),
Size: rand.Int63n(1000), // Random size
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour), // Random mod time
Type: "blob",
}
dir.Folders = append(dir.Folders, newFile)
dir.Folders = append(dir.Folders, iteminfo.ItemInfo{
Name: "folder-" + utils.GetRandomTermSeeded(r),
Size: 0,
ModTime: baseTime.Add(-time.Duration(r.Intn(100)) * time.Hour),
Type: "directory",
})
}
// Simulating files and directories with ExtendedItemInfo
// Files carry their real MIME type so the listing exercises preview
// eligibility, type-based icons and media handling.
for j := 0; j < numFilesPerDir; j++ {
name := "file-" + utils.GetRandomTermSeeded(r) + utils.GetRandomExtensionSeeded(r)
newFile := iteminfo.ExtendedItemInfo{
ItemInfo: iteminfo.ItemInfo{
Name: "file-" + utils.GetRandomTerm() + utils.GetRandomExtension(),
Size: rand.Int63n(1000),
ModTime: time.Now().Add(-time.Duration(rand.Intn(100)) * time.Hour),
Type: "blob",
Name: name,
Size: mockSizeFor(name, r),
ModTime: baseTime.Add(-time.Duration(r.Intn(100)) * time.Hour),
Type: mockMimeType(name),
},
}
dir.Files = append(dir.Files, newFile)
}
return dir
}
// mockMimeType resolves a MIME type from a filename's extension.
//
// Extension-only by design. Real indexing uses ItemInfo.DetectType, which
// additionally sniffs file headers to disambiguate extensions like `.ts`; mock
// rows have no file on disk, so sniffing would just return "blob" and undo the
// point of typing them at all. A plain extension lookup is enough to exercise
// the type-dependent UI paths (icons, preview eligibility, size formatting).
//
// The charset parameter is stripped so the values match what indexed items
// carry: mime.TypeByExtension(".txt") returns "text/plain; charset=utf-8",
// whereas a real indexed item has "text/plain".
//
// Fixed table first: mock types must not depend on host mailcap/shared-mime-info.
var mockMimeTypes = map[string]string{
".txt": "text/plain",
".mp3": "audio/mpeg",
".mov": "video/quicktime",
".doc": "application/msword",
".mp4": "video/mp4",
".bak": "application/x-trash",
".zip": "application/zip",
".jpg": "image/jpeg",
}
func mockMimeType(name string) string {
ext := strings.ToLower(filepath.Ext(name))
if m, ok := mockMimeTypes[ext]; ok {
return m
}
if m := strings.Split(mime.TypeByExtension(ext), ";")[0]; m != "" {
return m
}
return "blob"
}
// mockSizeFor returns a plausible size for a mock file.
//
// Sizes are drawn from the real size bucket for the extension rather than a
// flat 0-999 range, so the listing exercises realistic formatting: a `.txt`
// renders in bytes while a `.mp4` or `.zip` renders in MB/GB. A flat range made
// every cell show a near-identical tiny size.
func mockSizeFor(name string, r *rand.Rand) int64 {
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".jpg", ".mp3", ".doc":
// Hundreds of KB to a few MB.
return 50_000 + r.Int63n(4_000_000)
case ".mp4", ".mov":
// Tens to hundreds of MB.
return 10_000_000 + r.Int63n(500_000_000)
case ".zip", ".bak":
// KB to tens of MB.
return 1_000 + r.Int63n(20_000_000)
default:
// Small text-like files.
return 10 + r.Int63n(50_000)
}
}
// MockDataSeed derives a stable seed from the requested listing shape.
func MockDataSeed(numDirs, numFilesPerDir int) int64 {
return int64(numDirs)*1_000_003 + int64(numFilesPerDir)
}
+202
View File
@@ -0,0 +1,202 @@
package indexing
import (
"encoding/json"
"strings"
"testing"
)
// The performance harness baselines JSON payload size and DOM text length, so
// mock listing generation must be byte-for-byte reproducible for a given shape.
func TestCreateMockDataDeterministic(t *testing.T) {
first := CreateMockData(25, 25)
second := CreateMockData(25, 25)
firstJSON, err := json.Marshal(first)
if err != nil {
t.Fatalf("marshal first: %v", err)
}
secondJSON, err := json.Marshal(second)
if err != nil {
t.Fatalf("marshal second: %v", err)
}
if string(firstJSON) != string(secondJSON) {
t.Fatalf("mock data is not deterministic:\n first=%s\nsecond=%s",
truncate(firstJSON), truncate(secondJSON))
}
}
func TestCreateMockDataShape(t *testing.T) {
got := CreateMockData(7, 11)
if len(got.Folders) != 7 {
t.Errorf("folders = %d, want 7", len(got.Folders))
}
if len(got.Files) != 11 {
t.Errorf("files = %d, want 11", len(got.Files))
}
}
func TestCreateMockDataDifferentShapesDiffer(t *testing.T) {
a, err := json.Marshal(CreateMockData(10, 10))
if err != nil {
t.Fatalf("marshal: %v", err)
}
b, err := json.Marshal(CreateMockData(11, 11))
if err != nil {
t.Fatalf("marshal: %v", err)
}
if string(a) == string(b) {
t.Fatal("different shapes produced identical output")
}
}
func TestCreateMockDataExplicitSeed(t *testing.T) {
a, err := json.Marshal(CreateMockDataSeeded(10, 10, 12345))
if err != nil {
t.Fatalf("marshal: %v", err)
}
b, err := json.Marshal(CreateMockDataSeeded(10, 10, 12345))
if err != nil {
t.Fatalf("marshal: %v", err)
}
if string(a) != string(b) {
t.Fatal("explicit seed did not produce identical output")
}
c, err := json.Marshal(CreateMockDataSeeded(10, 10, 999))
if err != nil {
t.Fatalf("marshal: %v", err)
}
if string(a) == string(c) {
t.Fatal("different seeds produced identical output")
}
}
func truncate(b []byte) string {
if len(b) > 200 {
return string(b[:200]) + "..."
}
return string(b)
}
// Mock rows previously all carried Type "blob", so the listing never exercised
// preview eligibility, type-based icons, or media bubbling. File types must now
// be resolved from the extension exactly as real indexing does.
func TestCreateMockDataResolvesMimeTypes(t *testing.T) {
got := CreateMockData(400, 400)
allowed := map[string]bool{
"text/plain": true,
"audio/mpeg": true,
"video/quicktime": true,
"application/msword": true,
"video/mp4": true,
"application/x-trash": true,
"application/zip": true,
"image/jpeg": true,
}
seen := map[string]int{}
if len(got.Files) == 0 {
t.Fatal("expected files")
}
for _, f := range got.Files {
if f.Type == "blob" || f.Type == "" {
t.Fatalf("%s resolved to %q; expected a real MIME type", f.Name, f.Type)
}
if !allowed[f.Type] {
t.Fatalf("%s resolved to unexpected type %q", f.Name, f.Type)
}
seen[f.Type]++
}
// All eight mock extensions should appear across 400 sampled files.
if len(seen) != len(allowed) {
t.Errorf("saw %d distinct types, want %d: %v", len(seen), len(allowed), seen)
}
}
// Sizes should be plausible per extension so the listing renders mixed units
// (bytes vs MB/GB) rather than a column of near-identical tiny values.
func TestCreateMockDataRealisticSizes(t *testing.T) {
got := CreateMockData(400, 400)
var sawLarge, sawSmall bool
inspect := func(name string, size int64) {
switch {
case strings.HasSuffix(name, ".mp4"), strings.HasSuffix(name, ".mov"):
if size < 10_000_000 {
t.Errorf("%s size %d unexpectedly small for video", name, size)
}
sawLarge = true
case strings.HasSuffix(name, ".txt"):
if size > 50_000 {
t.Errorf("%s size %d unexpectedly large for text", name, size)
}
sawSmall = true
}
}
for _, f := range got.Files {
inspect(f.Name, int64(f.Size))
}
if !sawLarge {
t.Error("expected at least one large (video) file")
}
if !sawSmall {
t.Error("expected at least one small (text) file")
}
}
// Folder rows must be real directories. The frontend decides whether a row is
// navigable from `type === "directory"`, so emitting file MIME types here made
// rows that looked like folders in the payload behave as files in the UI.
func TestCreateMockDataFolderRowsAreDirectories(t *testing.T) {
got := CreateMockData(20, 5)
if len(got.Folders) != 20 {
t.Fatalf("folders = %d, want 20", len(got.Folders))
}
for _, f := range got.Folders {
if f.Type != "directory" {
t.Errorf("folder row %s got type %q, want \"directory\"", f.Name, f.Type)
}
if f.Size != 0 {
t.Errorf("folder row %s got size %d, want 0", f.Name, f.Size)
}
}
}
// Mock rows have no file on disk, so header sniffing would degrade to "blob"
// and undo the point of typing them. Types come from the extension alone.
func TestMockMimeTypeFromExtension(t *testing.T) {
cases := map[string]string{
"file-a.txt": "text/plain",
"file-a.mp3": "audio/mpeg",
"file-a.mov": "video/quicktime",
"file-a.doc": "application/msword",
"file-a.mp4": "video/mp4",
"file-a.zip": "application/zip",
"file-a.jpg": "image/jpeg",
// Unknown or absent extensions fall back rather than erroring.
"file-a.unknownext": "blob",
"file-a": "blob",
}
for name, want := range cases {
if got := mockMimeType(name); got != want {
t.Errorf("mockMimeType(%q) = %q, want %q", name, got, want)
}
}
}
// The charset parameter must be stripped so mock rows match the types real
// indexing produces (mime.TypeByExtension(".txt") appends "; charset=utf-8").
func TestMockMimeTypeStripsCharset(t *testing.T) {
if got := mockMimeType("file-a.txt"); strings.Contains(got, ";") || strings.Contains(got, "charset") {
t.Errorf("mockMimeType returned %q; expected the charset parameter stripped", got)
}
// The extension is matched case-insensitively.
if got := mockMimeType("FILE-A.JPG"); got != "image/jpeg" {
t.Errorf("mockMimeType(\"FILE-A.JPG\") = %q, want \"image/jpeg\"", got)
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ type AuthCommon struct {
type Auth struct {
TokenExpirationHours int `json:"tokenExpirationHours"` // time in hours each web UI session token is valid for. Default is 2 hours.
Methods LoginMethods `json:"methods"`
Key string `json:"key"` // secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
Key string `json:"key"` // secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.
AdminUsername string `json:"adminUsername"` // secret: the username of the admin user. If not set, the default is "admin".
AdminPassword string `json:"adminPassword"` // secret: the password of the admin user. If not set, the default is "admin".
TotpSecret string `json:"totpSecret"` // secret: secret used to encrypt TOTP secrets
+11 -2
View File
@@ -1847,6 +1847,15 @@ const docTemplate = `{
"$ref": "#/definitions/iteminfo.FileInfo"
}
},
"400": {
"description": "Missing or invalid path query parameter",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Resource not found",
"schema": {
@@ -5501,7 +5510,7 @@ const docTemplate = `{
"type": "string"
},
"key": {
"description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.",
"description": "secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.",
"type": "string"
},
"methods": {
@@ -7179,7 +7188,7 @@ const docTemplate = `{
"$ref": "#/definitions/users.Permissions"
},
"belongsTo": {
"description": "numeric user id in JWT claims (bolt-era small ids still work)",
"description": "legacy stored metadata only; identity is hashed_tokens lookup",
"type": "integer"
},
"expiresAt": {
+11 -2
View File
@@ -1836,6 +1836,15 @@
"$ref": "#/definitions/iteminfo.FileInfo"
}
},
"400": {
"description": "Missing or invalid path query parameter",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Resource not found",
"schema": {
@@ -5490,7 +5499,7 @@
"type": "string"
},
"key": {
"description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.",
"description": "secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.",
"type": "string"
},
"methods": {
@@ -7168,7 +7177,7 @@
"$ref": "#/definitions/users.Permissions"
},
"belongsTo": {
"description": "numeric user id in JWT claims (bolt-era small ids still work)",
"description": "legacy stored metadata only; identity is hashed_tokens lookup",
"type": "integer"
},
"expiresAt": {
+9 -3
View File
@@ -271,8 +271,8 @@ definitions:
is "admin".'
type: string
key:
description: 'secret: the key used to sign the JWT tokens. If not set, a random
key will be generated.'
description: 'secret: HMAC key for JWT tokens. If unset in config/env, one
is generated and stored in the application database.'
type: string
methods:
$ref: '#/definitions/settings.LoginMethods'
@@ -1509,7 +1509,7 @@ definitions:
Permissions:
$ref: '#/definitions/users.Permissions'
belongsTo:
description: numeric user id in JWT claims (bolt-era small ids still work)
description: legacy stored metadata only; identity is hashed_tokens lookup
type: integer
expiresAt:
type: integer
@@ -3601,6 +3601,12 @@ paths:
description: Resource metadata
schema:
$ref: '#/definitions/iteminfo.FileInfo'
"400":
description: Missing or invalid path query parameter
schema:
additionalProperties:
type: string
type: object
"404":
description: Resource not found
schema:
+93 -567
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -8,7 +8,7 @@
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
},
"scripts": {
"build": "rm -rf ../backend/internal/web/embed/* && vite build && cp -r ../backend/internal/web/dist/* ../backend/internal/web/embed",
"build": "mkdir -p ../backend/internal/web/embed && rm -rf ../backend/internal/web/embed/* && vite build && cp -r ../backend/internal/web/dist/* ../backend/internal/web/embed",
"build:docker": "vite build",
"watch": "vite build --watch",
"typecheck": "vue-tsc -p ./tsconfig.json --noEmit",
+82
View File
@@ -0,0 +1,82 @@
import { defineConfig, devices } from "@playwright/test";
const baseURL =
process.env.PERF_BASE_URL ??
(process.env.CI ? "http://127.0.0.1/" : "http://127.0.0.1:8080/");
const scaleCount = (process.env.PERF_SCALES ?? "100,1000,10000")
.split(",")
.filter((s) => s.trim()).length;
/**
* Default worker count.
*
* Kept low deliberately: multiple browsers hammering the same single-process
* backend inflates every timing, and the baseline records the worker count so a
* mismatch is reported rather than silently corrupting comparisons.
*/
const defaultWorkers = Math.min(Math.max(scaleCount, 1), 3);
const ALL_BROWSERS = [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
] as const;
/**
* Browser selection.
*
* CI sets `PERF_BROWSERS=chromium` (see Dockerfile.playwright-performance, `ci`
* target) because only chromium results are baseline-gated. Locally the default
* is all three, for cross-browser comparison. A comma-separated list narrows
* the set, which is what the local smoke runs and the `perf-smoke` target use.
*/
const requested = (process.env.PERF_BROWSERS ?? "")
.split(",")
.map((b) => b.trim().toLowerCase())
.filter(Boolean);
const projects =
requested.length > 0
? ALL_BROWSERS.filter((b) => requested.includes(b.name))
: [...ALL_BROWSERS];
export default defineConfig({
globalSetup: "./tests/playwright/performance/performance-global-setup.ts",
globalTeardown: "./tests/playwright/performance/performance-global-teardown.ts",
outputDir: "test-results/playwright-performance",
timeout: Number(process.env.PERF_TEST_TIMEOUT_MS ?? "300000"),
testDir: "./tests/playwright/performance",
testMatch: /listing-performance\.spec\.ts/,
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
workers: Number(process.env.PERF_WORKERS ?? defaultWorkers),
reporter: [
["list"],
[
"json",
{ outputFile: "test-results/playwright-performance/playwright-report.json" },
],
],
use: {
actionTimeout: 60_000,
storageState: "loginAuth.json",
baseURL,
// Playwright Tracing works on chromium, firefox and webkit, unlike
// `browser.startTracing` (a Chromium-only low-level API used inside the
// scenarios for flame charts). Enabled with PERF_PLAYWRIGHT_TRACE=1.
trace: process.env.PERF_PLAYWRIGHT_TRACE === "1" ? "on" : "off",
video: "off",
locale: "en-US",
},
projects,
webServer: process.env.CI
? undefined
: {
command:
"cd ../backend && FILEBROWSER_PLAYWRIGHT_TEST=true ./filebrowser -c config.playwright-performance.yaml",
url: `${baseURL}api/health`,
reuseExistingServer: true,
timeout: 120_000,
},
});
+1 -1
View File
@@ -131,7 +131,7 @@ auth:
rpId: "" # the Relying Party ID; if empty, auto-derived from base URL host
rpOrigins: # allowed origins; if empty, auto-derived from base URL
loginButtonText: "" # custom text for the passkey login button
key: "" # secret: the key used to sign the JWT tokens. If not set, a random key will be generated.
key: "" # secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.
adminUsername: "admin" # secret: the username of the admin user. If not set, the default is "admin".
adminPassword: "admin" # secret: the password of the admin user. If not set, the default is "admin".
totpSecret: "" # secret: secret used to encrypt TOTP secrets
+105
View File
@@ -0,0 +1,105 @@
import { createServer } from "node:http";
import { existsSync, statSync } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const frontendDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const perfRoot = path.join(frontendDir, "tests/playwright/performance");
const port = Number(process.env.PERF_DASHBOARD_PORT ?? "9323");
const host = process.env.PERF_DASHBOARD_HOST ?? "127.0.0.1";
const mimeTypes = {
".html": "text/html; charset=utf-8",
".json": "application/json; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
};
const chartBundle = path.join(
frontendDir,
"node_modules/chart.js/dist/chart.umd.min.js",
);
async function resolveFile(urlPath) {
let rel = decodeURIComponent(urlPath.split("?")[0]);
if (rel === "/" || rel === "") {
rel = "/dashboard/report.html";
}
if (rel === "/dashboard/chart.umd.min.js" || rel.endsWith("/chart.umd.min.js")) {
return existsSync(chartBundle) ? chartBundle : null;
}
let file = path.normalize(path.join(perfRoot, rel));
if (!file.startsWith(perfRoot)) {
return null;
}
if (!existsSync(file)) {
return null;
}
const info = await stat(file);
if (info.isDirectory()) {
// The dashboard has a single entry point; there is no index.html alias.
const report = path.join(file, "report.html");
if (existsSync(report) && !statSync(report).isDirectory()) {
file = report;
} else {
return null;
}
}
return file;
}
const server = createServer(async (req, res) => {
try {
const file = await resolveFile(req.url ?? "/");
if (!file) {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end(
"Not found. Run make perf-check first, then open /dashboard/\n",
);
return;
}
const body = await readFile(file);
const ext = path.extname(file);
res.writeHead(200, {
"Content-Type": mimeTypes[ext] ?? "application/octet-stream",
"Cache-Control": "no-store",
});
res.end(body);
} catch (err) {
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end(String(err));
}
});
function printUrls(base) {
console.log("");
console.log("Listing performance dashboard");
console.log(` ${base}/dashboard/report.html`);
console.log(` ${base}/results.json`);
console.log("");
}
server.on("error", (err) => {
const base = `http://${host}:${port}`;
if (err.code === "EADDRINUSE") {
printUrls(base);
console.log(
`Port ${port} is already in use — a dashboard server is probably still running.`,
);
console.log("Open the URLs above in your browser, or stop the old server:");
console.log(` fuser -k ${port}/tcp`);
console.log(` PERF_DASHBOARD_PORT=9400 make perf-dashboard`);
console.log("");
process.exit(0);
}
console.error(err);
process.exit(1);
});
server.listen(port, host, () => {
const base = `http://${host}:${port}`;
printUrls(base);
console.log(`Serving: ${perfRoot}`);
console.log("Press Ctrl+C to stop.");
});
+93
View File
@@ -18,6 +18,65 @@ import { invalidateDirMetadataCache } from '@/utils/metadataCache.js'
export { fetchPreviewImage } from '@/utils/previewRequests'
const VIEW_TOKEN_TTL_SECONDS = 15 * 60;
const MOCK_DATA_SOURCE = 'mockData';
const MOCK_DATA_DEFAULT_COUNT = 1000;
const MOCK_DATA_MAX_COUNT = 100_000;
function isMockDataSource(source) {
return !!(globalVars.devMode || globalVars.playwrightTest) && source === MOCK_DATA_SOURCE;
}
function parseMockDataCount(value, defaultCount) {
if (value === undefined || value === null || value === '') {
return defaultCount;
}
const n = parseInt(String(value), 10);
if (!Number.isFinite(n) || n < 0) {
return defaultCount;
}
return Math.min(n, MOCK_DATA_MAX_COUNT);
}
function getMockDataCounts() {
const query = state.route?.query ?? {};
const seedRaw = query.seed;
const seed =
seedRaw === undefined || seedRaw === null || seedRaw === ''
? undefined
: String(seedRaw);
return {
numDirs: parseMockDataCount(query.numDirs, MOCK_DATA_DEFAULT_COUNT),
numFiles: parseMockDataCount(query.numFiles, MOCK_DATA_DEFAULT_COUNT),
seed,
};
}
function normalizeMockListing(raw) {
const data = { ...raw };
data.type = 'directory';
data.source = MOCK_DATA_SOURCE;
data.path = '/';
data.name = MOCK_DATA_SOURCE;
// The backend already types folder rows as 'directory'; this stays as a
// defensive default so a partial/older mock payload still renders as folders.
if (Array.isArray(data.folders)) {
data.folders = data.folders.map((folder) => ({
...folder,
type: folder.type || 'directory',
}));
}
return data;
}
async function fetchMockDataListing(numDirs, numFiles, seed) {
const apiPath = getApiPath('mock-data', {
numDirs: String(numDirs),
numFiles: String(numFiles),
seed,
});
const res = await fetchURL(apiPath);
return res.json();
}
function chunkUploadApiPath(apiPath, headers) {
if (headers["X-File-Chunk-Offset"] === undefined) {
@@ -53,6 +112,22 @@ export async function fetchFiles(source, path, content = false, metadata = false
throw new Error('no source provided')
}
try {
if (isMockDataSource(source)) {
if (content || metadata) {
throw new Error('mockData does not support file content or metadata');
}
const normalizedPath = path === '' ? '/' : path;
if (normalizedPath !== '/') {
const err = new Error('mockData only supports the root listing');
err.status = 404;
throw err;
}
const { numDirs, numFiles, seed } = getMockDataCounts();
const raw = await fetchMockDataListing(numDirs, numFiles, seed);
const adjusted = adjustedData(normalizeMockListing(raw));
cacheViewTokenFromListing(adjusted);
return adjusted;
}
const apiPath = getApiPath('resources', {
path: path,
source: source,
@@ -71,6 +146,24 @@ export async function fetchFiles(source, path, content = false, metadata = false
}
}
/**
* Lists a directory as a Map of name -> { size, type } without showing error toasts.
* Returns null when the directory can't be listed (e.g. it doesn't exist yet), so
* callers can treat every item in it as missing.
*/
export async function listDirectoryEntries(source, path) {
try {
const res = await fetchURL(getApiPath('resources', { path, source }))
const data = adjustedData(await res.json())
if (data.type !== 'directory') {
return null
}
return new Map(data.items.map((item) => [item.name, { size: item.size, type: item.type }]))
} catch {
return null
}
}
export async function getItems(source, path, only = "") {
if (!source || source === undefined || source === null) {
throw new Error('no source provided')
@@ -0,0 +1,92 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
const { fetchURLMock, stateMock } = vi.hoisted(() => ({
fetchURLMock: vi.fn(),
stateMock: {
route: { path: "/files/mockData", query: {} },
sessionId: "test-session",
shareInfo: { subPath: "", hash: "", token: "" },
},
}));
vi.mock("@/utils/constants", () => ({
globalVars: {
baseURL: "/",
externalUrl: "",
devMode: true,
playwrightTest: false,
},
}));
vi.mock("@/store", () => ({
getters: { isShare: () => false },
mutations: {},
state: stateMock,
}));
vi.mock("@/notify", () => ({
notify: { showError: vi.fn() },
}));
vi.mock("./utils", async (importOriginal) => {
const actual = await importOriginal();
return {
...actual,
fetchURL: (...args) => fetchURLMock(...args),
};
});
import { fetchFiles } from "@/api/resources";
function mockListingResponse(numDirs, numFiles) {
return {
json: async () => ({
folders: Array.from({ length: numDirs }, (_, i) => ({ name: `folder-${i}` })),
files: Array.from({ length: numFiles }, (_, i) => ({
name: `file-${i}.txt`,
type: "blob",
})),
}),
};
}
describe("fetchFiles mockData source", () => {
beforeEach(() => {
fetchURLMock.mockReset();
stateMock.route.query = {};
});
it("calls mock-data with default counts of 1000", async () => {
fetchURLMock.mockResolvedValue(mockListingResponse(2, 1));
const result = await fetchFiles("mockData", "/");
expect(fetchURLMock).toHaveBeenCalledTimes(1);
const url = fetchURLMock.mock.calls[0][0];
expect(url).toContain("api/mock-data");
expect(url).toContain("numDirs=1000");
expect(url).toContain("numFiles=1000");
expect(result.type).toBe("directory");
expect(result.source).toBe("mockData");
expect(result.items).toHaveLength(3);
expect(result.items[0].type).toBe("directory");
});
it("passes numDirs and numFiles from route query", async () => {
stateMock.route.query = { numDirs: "10", numFiles: "5" };
fetchURLMock.mockResolvedValue(mockListingResponse(10, 5));
await fetchFiles("mockData", "/");
const url = fetchURLMock.mock.calls[0][0];
expect(url).toContain("numDirs=10");
expect(url).toContain("numFiles=5");
});
it("rejects non-root paths", async () => {
await expect(fetchFiles("mockData", "/sub")).rejects.toMatchObject({
status: 404,
});
expect(fetchURLMock).not.toHaveBeenCalled();
});
});
+1 -1
View File
@@ -531,7 +531,7 @@ export default {
return this.showCentered || this.isMobileDevice || !this.posX || !this.posY;
},
isMobileDevice() {
return state.isMobile;
return getters.isMobile();
},
isDarkMode() {
return getters.isDarkMode();
@@ -69,7 +69,7 @@ export default {
},
computed: {
isMobile() {
return state.isMobile;
return getters.isMobile();
},
isDarkMode() {
return getters.isDarkMode();
@@ -117,7 +117,7 @@ export default {
return "arrow_upward";
},
quickDownloadEnabled() {
if (state.isMobile) {
if (getters.isMobile()) {
return false
}
if (getters.isShare()) {
+11 -31
View File
@@ -9,7 +9,6 @@
hiddenFile: isHiddenNotSelected && this && !this.isDraggedOver,
'half-selected': isDraggedOver,
'drag-hover': isDraggedOver,
'out-of-view': !isInView && !isSelected,
}"
:id="getID"
role="button"
@@ -96,7 +95,6 @@
hiddenFile: isHiddenNotSelected && this && !this.isDraggedOver,
'half-selected': isDraggedOver,
'drag-hover': isDraggedOver,
'out-of-view': !isInView && !isSelected,
}"
:id="getID"
role="button"
@@ -176,7 +174,6 @@ export default {
data() {
return {
isThumbnailInView: false,
isInView: false,
touches: 0,
touchStartX: 0,
touchStartY: 0,
@@ -369,26 +366,20 @@ export default {
},
},
mounted() {
// Set up IntersectionObserver for lazy-loading thumbnails
// Note: dragend listener moved to parent ListingView for better performance
if (!this.hasPreview) return;
this.observer = new IntersectionObserver(this.handleIntersect, {
root: null,
rootMargin: "500px", // Reduced from 1500px for better performance
rootMargin: "500px",
threshold: 0,
});
// Use $nextTick to ensure $el is available and is an Element
this.$nextTick(() => {
if (this.$el && this.$el instanceof Element) {
this.observer.observe(this.$el);
const rect = this.$el.getBoundingClientRect();
const isInViewport = rect.top < window.innerHeight + 500 && rect.bottom > -500;
if (isInViewport && this.hasPreview) {
this.isThumbnailInView = true;
this.isInView = true;
}
}
});
// Note: dragend listener moved to parent ListingView for better performance
},
beforeUnmount() {
// Clean up observer
@@ -491,13 +482,13 @@ export default {
* @param {IntersectionObserver} observer
*/
handleIntersect(entries) {
entries.forEach((entry) => {
// Update both view state and thumbnail state
this.isInView = entry.isIntersecting;
if (entry.isIntersecting) {
this.isThumbnailInView = true;
for (const entry of entries) {
if (!entry.isIntersecting) continue;
this.isThumbnailInView = true;
if (this.observer) {
this.observer.unobserve(entry.target);
}
});
}
},
/** @param {DragEvent} event */
dragLeave(event) {
@@ -857,18 +848,7 @@ export default {
border-color: color-mix(in srgb, var(--primaryColor) 35%, transparent);
}
/* Disable transitions and hide content for out-of-view items for better performance */
.listing-item.out-of-view {
transition: none !important;
}
.listing-item.out-of-view * {
transition: none !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* Ensure items maintain their height even when content is hidden */
/* Ensure items maintain their height while offscreen content is skipped */
.listing-item > div {
min-height: 1em; /* Forces layout calculation even with hidden content */
}
@@ -60,14 +60,14 @@ export default {
if (!this.sourceInfo || this.sourceInfo.type === "3d") return;
const { source, path, size, url, modified } = this.sourceInfo;
setImageLoaded(source, path, size, modified, url);
if (!state.isMobile) {
if (!getters.isMobile()) {
this.$nextTick(() => this.positionPopup());
}
},
updateCursorPosition(event) {
this.cursorX = event.clientX;
this.cursorY = event.clientY;
if (!state.isMobile) this.positionPopup();
if (!getters.isMobile()) this.positionPopup();
},
positionPopup() {
if (!this.sourceInfo) return;
@@ -84,7 +84,7 @@ export default {
const minLeft = getters.isSidebarVisible() ? 320 : padding;
const minTop = padding + 100;
if (state.isMobile) {
if (getters.isMobile()) {
this.popupStyle = {
top: "50%",
left: "5%",
@@ -1,9 +1,25 @@
<template>
<div class="card-content">
<p>{{ $t("prompts.replaceMessage") }}</p>
<template v-if="allowSkip">
<p>{{ $t("prompts.uploadConflictMessage") }}</p>
<p>
<strong>{{ $t("prompts.optionLabel", { name: $t("prompts.uploadMissing") }) }}</strong>
{{ $t("prompts.uploadMissingInfo") }}
</p>
<p v-if="canReplace">
<strong>{{ $t("prompts.optionLabel", { name: $t("general.replace") }) }}</strong>
{{ $t("prompts.uploadReplaceInfo") }}
</p>
<p v-if="allowRename">
<strong>{{ $t("prompts.optionLabel", { name: $t("general.rename") }) }}</strong>
{{ $t("prompts.uploadRenameInfo") }}
</p>
</template>
<p v-else>{{ $t("prompts.replaceMessage") }}</p>
</div>
<div class="card-actions">
<button
v-if="allowRename"
type="button"
class="button button--flat button--blue"
:id="canReplace ? undefined : 'focus-prompt'"
@@ -13,6 +29,16 @@
>
{{ $t("general.rename") }}
</button>
<button
v-if="allowSkip"
type="button"
class="button button--flat button--blue"
@click="(event) => currentPrompt.confirm(event, 'skip')"
:aria-label="$t('prompts.uploadMissing')"
:title="$t('prompts.uploadMissing')"
>
{{ $t("prompts.uploadMissing") }}
</button>
<button
v-if="canReplace"
type="button"
@@ -36,6 +62,12 @@ export default {
currentPrompt() {
return getters.currentPrompt();
},
allowRename() {
return this.currentPrompt.props?.allowRename !== false;
},
allowSkip() {
return this.currentPrompt.props?.allowSkip === true;
},
isSameFile() {
return this.currentPrompt.props?.isSameFile === true;
},
+9 -2
View File
@@ -181,7 +181,7 @@
<script>
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { readAllDirectoryEntries, uploadManager } from "@/utils/upload";
import { mutations, state } from "@/store";
import { getters, mutations, state } from "@/store";
import { notify } from "@/notify";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import ProgressBar from "@/components/ProgressBar.vue";
@@ -267,14 +267,21 @@ export default {
}
};
const handleConflict = (resolver) => {
const handleConflict = (resolver, options = {}) => {
conflictResolver = resolver;
mutations.showPrompt({
name: "replace-rename",
pinned: true,
props: {
allowSkip: !getters.isShare(),
allowRename: options.allowRename !== false,
...(getters.isShare() ? {} : { title: i18n.global.t("prompts.uploadConflictTitle") }),
},
confirm: (_event, option) => {
if (option === "overwrite") {
resolveConflict(true);
} else if (option === "skip") {
resolveConflict({ skip: true });
} else if (option === "rename") {
showRenamePrompt();
} else {
@@ -652,7 +652,9 @@ export default {
display: flex;
flex-direction: column;
width: 100%;
min-width: 13em;
min-width: 0;
max-width: 100%;
box-sizing: border-box;
}
.expand-dropdown-anchor {
+15 -4
View File
@@ -197,12 +197,16 @@
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: 2.5em;
font-size: 1.5em;
color: #fff;
text-align: center;
}
#previewer .preview .info .title {
margin-bottom: 1em;
max-width: 100%;
}
#previewer .preview .info .title i {
@@ -211,20 +215,27 @@
font-size: 4em;
}
#previewer .preview .info a {
display: inline-block;
#previewer .preview .info .preview-buttons .button--flat {
display: inline-flex;
flex-direction: column;
align-items: center;
text-align: center;
}
#previewer .preview .info a:hover {
#previewer .preview .info .preview-buttons .button--flat:hover {
background-color: rgba(255, 255, 255, 0.2);
}
#previewer .preview .info a i {
#previewer .preview .info .preview-buttons .button--flat i {
display: block;
margin-bottom: 4px;
font-size: 1.3em;
}
#previewer .preview .info .preview-buttons .button--flat.preview-action--icon-only i {
margin-bottom: 0;
}
#previewer .pdf {
width: 100%;
height: 100%;
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "لا شيء للعرض هنا...",
"cannotAccesDir": "يتعذر الوصول إلى المجلد",
"newFolderMessage": "اكتب اسم المجلد الجديد.",
"newFolderSuccess": "تم إنشاء المجلد بنجاح!"
"newFolderSuccess": "تم إنشاء المجلد بنجاح!",
"mobilePdfPreviewHint": "لا يمكن لمتصفحك عرض ملف PDF هذا داخل معاينة التطبيق."
},
"help": {
"functionKeys": "مفاتيح الوظائف",
@@ -163,6 +164,15 @@
"dragAndDrop": "سحب الملفات وإفلاتها هنا",
"completed": "مكتمل",
"conflictsDetected": "التعارضات المكتشفة",
"uploadMissing": "رفع ما ينقص فقط",
"optionLabel": "{name}:",
"uploadConflictTitle": "الملفات موجودة بالفعل",
"uploadConflictMessage": "بعض هذه الملفات موجودة بالفعل في هذا المجلد. ماذا تريد أن تفعل؟",
"uploadMissingInfo": "لا تُرفع الملفات المكتملة (بنفس الحجم) مرة أخرى، وتُستبدل الملفات الفارغة، وتُحفظ الملفات ذات الحجم المختلف بلاحقة _01 و_02... لا يُحذف أي شيء.",
"uploadReplaceInfo": "يستبدل كل ملف موجود بالملفات التي تقوم برفعها.",
"uploadRenameInfo": "يرفع المجلد باسم جديد.",
"uploadRenamed": "{count} ملف (ملفات) موجودة بحجم مختلف وتم رفعها بلاحقة _01 و_02... بدلاً من استبدالها",
"uploadSkipped": "تم تخطي {count} ملف (ملفات) مرفوعة مسبقاً بنفس الحجم",
"downloadsLimit": "حد عدد التنزيلات",
"maxBandwidth": "أقصى نطاق ترددي للتنزيل (كيلو بايت/ثانية)",
"shareThemeColor": "مشاركة لون السمة (قيمة CSS)",
+11 -1
View File
@@ -222,7 +222,8 @@
"sameFolder": "Не може да се премести в същата папка",
"cannotAccesDir": "Няма достъп до папката",
"newFolderMessage": "Напишете името на новата папка.",
"newFolderSuccess": "Папката беше създадена успешно!"
"newFolderSuccess": "Папката беше създадена успешно!",
"mobilePdfPreviewHint": "Вашият браузър не може да покаже този PDF файл в прегледа на приложението."
},
"help": {
"functionKeys": "Функционални клавиши",
@@ -431,6 +432,15 @@
"optionalPassword": "Незадължителна парола",
"deleted": "Изтрито успешно!",
"conflictsDetected": "Открити конфликти",
"uploadMissing": "Качи само липсващото",
"optionLabel": "{name}:",
"uploadConflictTitle": "Файловете вече съществуват",
"uploadConflictMessage": "Някои от тези файлове вече съществуват в тази папка. Какво искате да направите?",
"uploadMissingInfo": "Файловете, които вече са пълни (същия размер), не се качват отново, празните се заменят, а файловете с различен размер се запазват с наставка _01, _02... Нищо не се изтрива.",
"uploadReplaceInfo": "Презаписва всеки съществуващ файл с качваните от вас.",
"uploadRenameInfo": "Качва папката с ново име.",
"uploadRenamed": "{count} файл(а) вече съществуваха с различен размер и бяха качени с наставка _01, _02... вместо да бъдат заменени",
"uploadSkipped": "{count} файл(а) вече качени със същия размер бяха пропуснати",
"copyPasteConfirm": "Потвърждение за копиране/поставяне",
"copyItemQuestion": "Копиране на този елемент на новото местоположение?",
"moveItemQuestion": "Преместване на този елемент на новото местоположение?",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "Zde není nic k vidění...",
"cannotAccesDir": "Nelze otevřít složku",
"newFolderMessage": "Zadejte název nové složky.",
"newFolderSuccess": "Složka byla úspěšně vytvořena!"
"newFolderSuccess": "Složka byla úspěšně vytvořena!",
"mobilePdfPreviewHint": "Váš prohlížeč nedokáže zobrazit tento soubor PDF v náhledu aplikace."
},
"help": {
"functionKeys": "Funkční Klávesy",
@@ -233,6 +234,15 @@
"dragAndDrop": "Přetáhněte sem soubory",
"completed": "Dokončeno",
"conflictsDetected": "Zjištěné konflikty",
"uploadMissing": "Nahrát jen to, co chybí",
"optionLabel": "{name}:",
"uploadConflictTitle": "Soubory již existují",
"uploadConflictMessage": "Některé z těchto souborů již v této složce existují. Co chcete udělat?",
"uploadMissingInfo": "Soubory, které jsou již kompletní (stejná velikost), se znovu nenahrávají, prázdné se nahradí a soubory s jinou velikostí se uloží s příponou _01, _02... Nic se nemaže.",
"uploadReplaceInfo": "Přepíše všechny existující soubory těmi, které nahráváte.",
"uploadRenameInfo": "Nahraje složku pod novým názvem.",
"uploadRenamed": "Souborů: {count} již existovalo s jinou velikostí a bylo nahráno s příponou _01, _02... místo nahrazení",
"uploadSkipped": "Přeskočeno souborů: {count} (již nahrané se stejnou velikostí)",
"downloadsLimit": "Limit počtu stažení",
"maxBandwidth": "Maximální šířka pásma stahování (kbps)",
"shareThemeColor": "Sdílet barvu motivu (hodnota CSS)",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "Hier gibt es nichts zu sehen...",
"cannotAccesDir": "Auf den Ordner kann nicht zugegriffen werden",
"newFolderMessage": "Geben Sie den Namen des neuen Ordners ein.",
"newFolderSuccess": "Ordner erfolgreich erstellt!"
"newFolderSuccess": "Ordner erfolgreich erstellt!",
"mobilePdfPreviewHint": "Ihr Browser kann diese PDF-Datei in der App-Vorschau nicht anzeigen."
},
"help": {
"functionKeys": "Funktionstasten",
@@ -163,6 +164,15 @@
"dragAndDrop": "Dateien hierher ziehen und ablegen",
"completed": "Abgeschlossen",
"conflictsDetected": "Erkannte Widersprüche",
"uploadMissing": "Nur Fehlendes hochladen",
"optionLabel": "{name}:",
"uploadConflictTitle": "Dateien existieren bereits",
"uploadConflictMessage": "Einige dieser Dateien existieren bereits in diesem Ordner. Was möchten Sie tun?",
"uploadMissingInfo": "Bereits vollständige Dateien (gleiche Größe) werden nicht erneut hochgeladen, leere Dateien werden ersetzt, und Dateien mit anderer Größe werden mit dem Suffix _01, _02... gespeichert. Es wird nichts gelöscht.",
"uploadReplaceInfo": "Überschreibt jede vorhandene Datei mit den Dateien, die Sie hochladen.",
"uploadRenameInfo": "Lädt den Ordner unter einem neuen Namen hoch.",
"uploadRenamed": "{count} Datei(en) existierten bereits mit anderer Größe und wurden mit dem Suffix _01, _02... hochgeladen, statt sie zu ersetzen",
"uploadSkipped": "{count} bereits hochgeladene Datei(en) mit gleicher Größe wurden übersprungen",
"downloadsLimit": "Limit für die Anzahl der Downloads",
"maxBandwidth": "Maximale Download-Bandbreite (kbps)",
"shareThemeColor": "Themenfarbe freigeben (CSS-Wert)",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "Δεν υπάρχει τίποτα να δείξω εδώ...",
"cannotAccesDir": "Δεν είναι δυνατή η πρόσβαση στο φάκελο",
"newFolderMessage": "Γράψτε το όνομα του νέου φακέλου.",
"newFolderSuccess": "Ο φάκελος δημιουργήθηκε με επιτυχία!"
"newFolderSuccess": "Ο φάκελος δημιουργήθηκε με επιτυχία!",
"mobilePdfPreviewHint": "Ο περιηγητής σας δεν μπορεί να εμφανίσει αυτό το αρχείο PDF στην προεπισκόπηση της εφαρμογής."
},
"help": {
"functionKeys": "Πλήκτρα Λειτουργιών",
@@ -163,6 +164,15 @@
"dragAndDrop": "Σύρετε και αφήστε αρχεία εδώ",
"completed": "Ολοκληρωμένο",
"conflictsDetected": "Ανιχνεύονται συγκρούσεις",
"uploadMissing": "Ανέβασμα μόνο όσων λείπουν",
"optionLabel": "{name}:",
"uploadConflictTitle": "Τα αρχεία υπάρχουν ήδη",
"uploadConflictMessage": "Μερικά από αυτά τα αρχεία υπάρχουν ήδη σε αυτόν τον φάκελο. Τι θέλετε να κάνετε;",
"uploadMissingInfo": "Τα αρχεία που είναι ήδη πλήρη (ίδιο μέγεθος) δεν ανεβαίνουν ξανά, τα κενά αντικαθίστανται και τα αρχεία με διαφορετικό μέγεθος αποθηκεύονται με κατάληξη _01, _02... Τίποτα δεν διαγράφεται.",
"uploadReplaceInfo": "Αντικαθιστά κάθε υπάρχον αρχείο με αυτά που ανεβάζετε.",
"uploadRenameInfo": "Ανεβάζει τον φάκελο με νέο όνομα.",
"uploadRenamed": "{count} αρχείο(α) υπήρχαν ήδη με διαφορετικό μέγεθος και ανέβηκαν με κατάληξη _01, _02... αντί να αντικατασταθούν",
"uploadSkipped": "Παραλείφθηκαν {count} αρχείο(α) που είχαν ήδη ανέβει με το ίδιο μέγεθος",
"downloadsLimit": "Όριο αριθμού λήψεων",
"maxBandwidth": "Μέγιστο εύρος ζώνης λήψης (kbps)",
"shareThemeColor": "Μοιραστείτε το χρώμα του θέματος (τιμή CSS)",
+10
View File
@@ -216,6 +216,7 @@
"sortBySize": "Sort by size",
"sortByDuration": "Sort by duration",
"noPreview": "Preview is not available for this file.",
"mobilePdfPreviewHint": "Your browser cannot display this PDF inside the app preview.",
"noDownloadAccess": "Download is not available.",
"newFile": "New file{suffix}",
"newFolder": "New folder{suffix}",
@@ -431,6 +432,15 @@
"optionalPassword": "Optional password",
"deleted": "Deleted successfully!",
"conflictsDetected": "Conflicts detected",
"uploadMissing": "Upload only what's missing",
"optionLabel": "{name}:",
"uploadConflictTitle": "Files already exist",
"uploadConflictMessage": "Some of these files already exist in this folder. What do you want to do?",
"uploadMissingInfo": "Files that are already complete (same size) are not uploaded again, empty files are replaced, and files with a different size are saved with a _01, _02... suffix. Nothing is deleted.",
"uploadReplaceInfo": "Overwrites every existing file with the ones you are uploading.",
"uploadRenameInfo": "Uploads the folder under a new name.",
"uploadRenamed": "{count} file(s) with a different size already existed and were uploaded with a _01, _02... suffix instead of replacing them",
"uploadSkipped": "{count} file(s) already uploaded with the same size were skipped",
"copyPasteConfirm": "Copy/Paste Confirmation",
"copyItemQuestion": "Copy this item to the new location?",
"moveItemQuestion": "Move this item to the new location?",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "No hay nada aquí...",
"cannotAccesDir": "No se puede acceder a la carpeta",
"newFolderMessage": "Escribe el nombre de la nueva carpeta.",
"newFolderSuccess": "¡La carpeta se ha creado correctamente!"
"newFolderSuccess": "¡La carpeta se ha creado correctamente!",
"mobilePdfPreviewHint": "Tu navegador no puede mostrar este PDF en la vista previa de la aplicación."
},
"help": {
"functionKeys": "Teclas de función",
@@ -169,6 +170,15 @@
"dragAndDrop": "Arrastra y suelta los archivos aquí",
"completed": "Completado",
"conflictsDetected": "Se detectaron conflictos",
"uploadMissing": "Subir solo lo que falta",
"optionLabel": "{name}:",
"uploadConflictTitle": "Ya existen archivos",
"uploadConflictMessage": "Algunos de estos archivos ya existen en la carpeta. ¿Qué quieres hacer?",
"uploadMissingInfo": "No vuelve a subir los archivos que ya están completos (mismo tamaño), reemplaza los que están vacíos y guarda con sufijo _01, _02... los que tienen otro tamaño. No se borra nada.",
"uploadReplaceInfo": "Sobrescribe todos los archivos existentes con los que estás subiendo.",
"uploadRenameInfo": "Sube la carpeta con otro nombre.",
"uploadRenamed": "{count} archivo(s) ya existían con otro tamaño y se subieron con sufijo _01, _02... en lugar de reemplazarlos",
"uploadSkipped": "Se omitieron {count} archivo(s) que ya estaban subidos con el mismo tamaño",
"renameMessage": "Escribe un nuevo nombre:",
"renameMessageInvalid": "El nombre no puede tener \"/\" o \"\\\"",
"deleteMessageMultiple": "¿Quieres borrar estos {count} archivos?",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "Rien à voir ici...",
"cannotAccesDir": "Impossible d'accéder au dossier",
"newFolderMessage": "Indiquez le nom du nouveau dossier.",
"newFolderSuccess": "Le dossier a été créé avec succès !"
"newFolderSuccess": "Le dossier a été créé avec succès !",
"mobilePdfPreviewHint": "Votre navigateur ne peut pas afficher ce fichier PDF dans l'aperçu de l'application."
},
"help": {
"functionKeys": "Touches de Fonction",
@@ -163,6 +164,15 @@
"dragAndDrop": "Glisser-déposer des fichiers ici",
"completed": "Terminé",
"conflictsDetected": "Conflits détectés",
"uploadMissing": "Importer uniquement ce qui manque",
"optionLabel": "{name} :",
"uploadConflictTitle": "Les fichiers existent déjà",
"uploadConflictMessage": "Certains de ces fichiers existent déjà dans ce dossier. Que voulez-vous faire ?",
"uploadMissingInfo": "Les fichiers déjà complets (même taille) ne sont pas importés à nouveau, les fichiers vides sont remplacés et les fichiers de taille différente sont enregistrés avec le suffixe _01, _02... Rien n'est supprimé.",
"uploadReplaceInfo": "Écrase chaque fichier existant avec ceux que vous importez.",
"uploadRenameInfo": "Importe le dossier sous un nouveau nom.",
"uploadRenamed": "{count} fichier(s) existaient déjà avec une taille différente et ont été importés avec le suffixe _01, _02... au lieu d'être remplacés",
"uploadSkipped": "{count} fichier(s) déjà importé(s) avec la même taille ont été ignoré(s)",
"downloadsLimit": "Limite du nombre de téléchargements",
"maxBandwidth": "Largeur de bande maximale de téléchargement (kbps)",
"shareThemeColor": "Partager la couleur du thème (valeur CSS)",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "אין מה להראות כאן...",
"cannotAccesDir": "לא ניתן לגשת לתיקייה",
"newFolderMessage": "הזן את שם התיקייה החדשה.",
"newFolderSuccess": "התיקייה נוצרה בהצלחה!"
"newFolderSuccess": "התיקייה נוצרה בהצלחה!",
"mobilePdfPreviewHint": "הדפדפן שלך אינו יכול להציג קובץ PDF זה בתוך תצוגה מקדימה של האפליקציה."
},
"help": {
"functionKeys": "מקשי פונקציה",
@@ -163,6 +164,15 @@
"dragAndDrop": "גרור ושחרר קבצים לכאן",
"completed": "הושלם",
"conflictsDetected": "סכסוכים שזוהו",
"uploadMissing": "העלה רק את מה שחסר",
"optionLabel": "{name}:",
"uploadConflictTitle": "הקבצים כבר קיימים",
"uploadConflictMessage": "חלק מהקבצים האלה כבר קיימים בתיקייה זו. מה ברצונך לעשות?",
"uploadMissingInfo": "קבצים שכבר שלמים (באותו גודל) לא יועלו שוב, קבצים ריקים יוחלפו, וקבצים בגודל שונה יישמרו עם הסיומת _01, _02... שום דבר לא נמחק.",
"uploadReplaceInfo": "מחליף כל קובץ קיים בקבצים שאתה מעלה.",
"uploadRenameInfo": "מעלה את התיקייה בשם חדש.",
"uploadRenamed": "{count} קבצים כבר היו קיימים בגודל שונה והועלו עם הסיומת _01, _02... במקום להחליף אותם",
"uploadSkipped": "{count} קבצים שכבר הועלו באותו גודל דולגו",
"downloadsLimit": "הגבלת מספר ההורדות",
"maxBandwidth": "רוחב פס הורדה מרבי (kbps)",
"shareThemeColor": "שתף צבע נושא (ערך CSS)",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "Itt nincs mit mutatni...",
"cannotAccesDir": "Nem lehet hozzáférni a mappához",
"newFolderMessage": "Írja be az új mappa nevét.",
"newFolderSuccess": "A mappa létrehozása sikeresen befejeződött!"
"newFolderSuccess": "A mappa létrehozása sikeresen befejeződött!",
"mobilePdfPreviewHint": "A böngészője nem tudja megjeleníteni ezt a PDF-fájlt az alkalmazás előnézetében."
},
"help": {
"functionKeys": "Funkcióbillentyűk",
@@ -163,6 +164,15 @@
"dragAndDrop": "Húzza ide a fájlokat",
"completed": "Befejezett",
"conflictsDetected": "Feltárt konfliktusok",
"uploadMissing": "Csak a hiányzók feltöltése",
"optionLabel": "{name}:",
"uploadConflictTitle": "A fájlok már léteznek",
"uploadConflictMessage": "Ezek közül a fájlok közül néhány már létezik ebben a mappában. Mit szeretne tenni?",
"uploadMissingInfo": "A már teljes fájlok (azonos méret) nem kerülnek újra feltöltésre, az üres fájlok lecserélődnek, a más méretű fájlok pedig _01, _02... utótaggal kerülnek mentésre. Semmi sem törlődik.",
"uploadReplaceInfo": "Felülír minden meglévő fájlt a feltöltött fájlokkal.",
"uploadRenameInfo": "A mappát új néven tölti fel.",
"uploadRenamed": "{count} fájl már létezett más mérettel, ezért csere helyett _01, _02... utótaggal lett feltöltve",
"uploadSkipped": "{count} már feltöltött, azonos méretű fájl kihagyva",
"downloadsLimit": "Letöltések száma limit",
"maxBandwidth": "Maximális letöltési sávszélesség (kbps)",
"shareThemeColor": "Téma színének megosztása (CSS érték)",
+11 -1
View File
@@ -222,7 +222,8 @@
"sameFolder": "Impossibile spostare nella stessa cartella",
"cannotAccesDir": "Impossibile accedere alla cartella",
"newFolderMessage": "Inserisci il nome della nuova cartella.",
"newFolderSuccess": "La cartella è stata creata con successo!"
"newFolderSuccess": "La cartella è stata creata con successo!",
"mobilePdfPreviewHint": "Il tuo browser non è in grado di visualizzare questo PDF nell'anteprima dell'app."
},
"help": {
"functionKeys": "Tasti funzione",
@@ -430,6 +431,15 @@
"optionalPassword": "Password opzionale",
"deleted": "Eliminato con successo!",
"conflictsDetected": "Conflitti rilevati",
"uploadMissing": "Carica solo ciò che manca",
"optionLabel": "{name}:",
"uploadConflictTitle": "I file esistono già",
"uploadConflictMessage": "Alcuni di questi file esistono già in questa cartella. Cosa vuoi fare?",
"uploadMissingInfo": "I file già completi (stessa dimensione) non vengono caricati di nuovo, i file vuoti vengono sostituiti e i file con dimensione diversa vengono salvati con il suffisso _01, _02... Nulla viene eliminato.",
"uploadReplaceInfo": "Sovrascrive ogni file esistente con quelli che stai caricando.",
"uploadRenameInfo": "Carica la cartella con un nuovo nome.",
"uploadRenamed": "{count} file esistevano già con una dimensione diversa e sono stati caricati con il suffisso _01, _02... invece di sostituirli",
"uploadSkipped": "{count} file già caricati con la stessa dimensione sono stati ignorati",
"copyPasteConfirm": "Conferma Copia/Incolla",
"copyItemQuestion": "Copiare questo elemento nella nuova posizione?",
"moveItemQuestion": "Spostare questo elemento nella nuova posizione?",
+11 -1
View File
@@ -58,7 +58,8 @@
"lonely": "ここには何もない...。",
"cannotAccesDir": "フォルダにアクセスできません",
"newFolderMessage": "新しいフォルダの名前を入力してください。",
"newFolderSuccess": "フォルダが正常に作成されました!"
"newFolderSuccess": "フォルダが正常に作成されました!",
"mobilePdfPreviewHint": "お使いのブラウザでは、アプリプレビュー内でこのPDFを表示できません。"
},
"help": {
"functionKeys": "ファンクションキー",
@@ -163,6 +164,15 @@
"dragAndDrop": "ここにファイルをドラッグ&ドロップしてください",
"completed": "完了",
"conflictsDetected": "競合が検出されました",
"uploadMissing": "不足分のみアップロード",
"optionLabel": "{name}:",
"uploadConflictTitle": "ファイルは既に存在します",
"uploadConflictMessage": "これらのファイルの一部は、このフォルダーに既に存在します。どうしますか?",
"uploadMissingInfo": "既に完全なファイル(同じサイズ)は再アップロードされず、空のファイルは置き換えられ、サイズが異なるファイルは接尾辞 _01、_02... を付けて保存されます。何も削除されません。",
"uploadReplaceInfo": "既存のすべてのファイルを、アップロードするファイルで上書きします。",
"uploadRenameInfo": "フォルダーを新しい名前でアップロードします。",
"uploadRenamed": "サイズが異なる{count}個のファイルが既に存在していたため、置き換えずに接尾辞 _01、_02... を付けてアップロードしました",
"uploadSkipped": "同じサイズで既にアップロード済みの{count}個のファイルをスキップしました",
"downloadsLimit": "ダウンロード回数制限",
"maxBandwidth": "最大ダウンロード帯域幅(kbps)",
"shareThemeColor": "共有テーマカラー(CSS値)",
Loaded 100 of 175 files, more files were not shown because too many files have changed in this diff. Show more