Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-08-23 12:24:57 -04:00

Compare commits

..
Author SHA1 Message Date
Andrew Nesbitt
6ed0c75ccd
Drop redundant upstream defaulting and cache PyPI href regex 2026-08-17 09:27:22 +01:00
Andrew Nesbitt
05ff7e99ec
Simplify NuGet service rewrite and trim upstream URLs 2026-08-16 22:21:01 +01:00
Andrew Nesbitt
d86bc633ac
Make built-in upstream URLs configurable 2026-08-16 22:18:21 +01:00
49 changed files with 2345 additions and 898 deletions

View file

@ -39,7 +39,7 @@ proxy/
│ │ └── queries.go # CRUD operations
│ ├── storage/ # Artifact file storage
│ │ ├── storage.go # Storage interface
│ │ └── blob.go # gocloud.dev/blob backends (file, S3, Azure)
│ │ └── filesystem.go # Local filesystem impl
│ ├── upstream/ # Upstream registry clients
│ │ ├── fetcher.go # HTTP artifact fetching
│ │ └── resolver.go # Download URL resolution
@ -72,7 +72,7 @@ Key types:
### `internal/storage`
Artifact file storage abstraction backed by `gocloud.dev/blob`. Supports local filesystem (`file://`), S3 (`s3://`), and Azure (`azblob://`) URLs.
Artifact file storage abstraction. Currently implements local filesystem storage. Designed to allow future backends (S3, GCS).
Interface:
```go

View file

@ -506,6 +506,8 @@ cooldown:
default: "3d"
```
See the [configuration reference](docs/configuration.md#upstream-registries) for every upstream key, environment variable, and default URL.
Run with config file:
```bash

View file

@ -275,10 +275,7 @@ func runServe() {
logger := setupLogger(cfg.Log.Level, cfg.Log.Format)
// Create and start server
srv, err := server.New(cfg, logger, server.BuildInfo{
Version: Version,
Commit: Commit,
})
srv, err := server.New(cfg, logger)
if err != nil {
logger.Error("failed to create server", "error", err)
os.Exit(1)

View file

@ -82,26 +82,74 @@ log:
access_log:
path: ""
# Upstream registry URLs and authentication
# Upstream URLs for built-in routes and authentication
upstream:
# npm registry URL
npm: "https://registry.npmjs.org"
# Maven repository URL (used by /maven endpoint)
maven: "https://repo1.maven.org/maven2"
# Gradle Plugin Portal Maven URL (fallback for plugin marker artifacts)
gradle_plugin_portal: "https://plugins.gradle.org/m2"
# Cargo sparse index URL
cargo: "https://index.crates.io"
# Cargo crate download URL
cargo_download: "https://static.crates.io/crates"
# RubyGems registry URL
gem: "https://rubygems.org"
# Go module proxy URL
go: "https://proxy.golang.org"
# Hex repository URL
hex: "https://repo.hex.pm"
# Hex API URL used for package timestamps
hex_api: "https://hex.pm"
# pub registry URL
pub: "https://pub.dev"
# PyPI index and API URL
pypi: "https://pypi.org"
# PyPI package download URL
pypi_download: "https://files.pythonhosted.org"
# Maven repository URL (used by /maven endpoint)
maven: "https://repo1.maven.org/maven2"
# Gradle Plugin Portal Maven URL (fallback for plugin marker artifacts)
gradle_plugin_portal: "https://plugins.gradle.org/m2"
# NuGet API URL
nuget: "https://api.nuget.org"
# NuGet search API URL
nuget_search: "https://azuresearch-usnc.nuget.org"
# Packagist API URL
composer: "https://packagist.org"
# Packagist repository URL
composer_repository: "https://repo.packagist.org"
# Conan registry URL
conan: "https://center.conan.io"
# Conda channel base URL
conda: "https://conda.anaconda.org"
# CRAN mirror URL
cran: "https://cloud.r-project.org"
# Julia package server URL
julia: "https://pkg.julialang.org"
# Debian/APT repository URL (used by /debian endpoint)
debian: "http://deb.debian.org/debian"
# RPM repository URL (used by /rpm endpoint)
rpm: "https://dl.fedoraproject.org/pub/fedora/linux"
# Named HTTP Helm chart repositories (used by /helm/{name}/)
# helm:
# bitnami: "https://charts.bitnami.com/bitnami"

View file

@ -134,16 +134,51 @@ Upstream retries and OCI authentication calls are separate `upstream` records, s
## Upstream Registries
Override default upstream registry URLs:
Each upstream used by a built-in package route can be set in YAML or JSON under `upstream`, or with its matching environment variable. Existing installations keep the same public upstreams by default. Trailing slashes are ignored.
| Config | Environment | Default |
|--------|-------------|---------|
| `upstream.npm` | `PROXY_UPSTREAM_NPM` | `https://registry.npmjs.org` |
| `upstream.cargo` | `PROXY_UPSTREAM_CARGO` | `https://index.crates.io` |
| `upstream.cargo_download` | `PROXY_UPSTREAM_CARGO_DOWNLOAD` | `https://static.crates.io/crates` |
| `upstream.gem` | `PROXY_UPSTREAM_GEM` | `https://rubygems.org` |
| `upstream.go` | `PROXY_UPSTREAM_GO` | `https://proxy.golang.org` |
| `upstream.hex` | `PROXY_UPSTREAM_HEX` | `https://repo.hex.pm` |
| `upstream.hex_api` | `PROXY_UPSTREAM_HEX_API` | `https://hex.pm` |
| `upstream.pub` | `PROXY_UPSTREAM_PUB` | `https://pub.dev` |
| `upstream.pypi` | `PROXY_UPSTREAM_PYPI` | `https://pypi.org` |
| `upstream.pypi_download` | `PROXY_UPSTREAM_PYPI_DOWNLOAD` | `https://files.pythonhosted.org` |
| `upstream.maven` | `PROXY_UPSTREAM_MAVEN` | `https://repo1.maven.org/maven2` |
| `upstream.gradle_plugin_portal` | `PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL` | `https://plugins.gradle.org/m2` |
| `upstream.nuget` | `PROXY_UPSTREAM_NUGET` | `https://api.nuget.org` |
| `upstream.nuget_search` | `PROXY_UPSTREAM_NUGET_SEARCH` | `https://azuresearch-usnc.nuget.org` |
| `upstream.composer` | `PROXY_UPSTREAM_COMPOSER` | `https://packagist.org` |
| `upstream.composer_repository` | `PROXY_UPSTREAM_COMPOSER_REPOSITORY` | `https://repo.packagist.org` |
| `upstream.conan` | `PROXY_UPSTREAM_CONAN` | `https://center.conan.io` |
| `upstream.conda` | `PROXY_UPSTREAM_CONDA` | `https://conda.anaconda.org` |
| `upstream.cran` | `PROXY_UPSTREAM_CRAN` | `https://cloud.r-project.org` |
| `upstream.julia` | `PROXY_UPSTREAM_JULIA` | `https://pkg.julialang.org` |
| `upstream.debian` | `PROXY_UPSTREAM_DEBIAN` | `http://deb.debian.org/debian` |
| `upstream.rpm` | `PROXY_UPSTREAM_RPM` | `https://dl.fedoraproject.org/pub/fedora/linux` |
For protocols that use separate metadata and download services, configure both values. They may point to the same endpoint when chaining proxies:
```yaml
upstream:
npm: "https://registry.npmjs.org"
maven: "https://repo1.maven.org/maven2"
gradle_plugin_portal: "https://plugins.gradle.org/m2"
cargo: "https://index.crates.io"
cargo_download: "https://static.crates.io/crates"
pypi: "https://upstream-proxy.example.com/pypi"
pypi_download: "https://upstream-proxy.example.com/pypi"
nuget: "https://upstream-proxy.example.com/nuget"
nuget_search: "https://upstream-proxy.example.com/nuget"
composer: "https://upstream-proxy.example.com/composer"
composer_repository: "https://upstream-proxy.example.com/composer"
```
`upstream.hex_api` is used for cooldown timestamps and must expose Hex's `/api/packages/{name}` JSON endpoint.
Helm HTTP repositories and additional OCI registries are configured as named maps rather than single URLs:
```yaml
upstream:
# Named HTTP Helm chart repositories, served at /helm/{name}/.
helm:
bitnami: "https://charts.bitnami.com/bitnami"

161
go.mod
View file

@ -1,6 +1,6 @@
module github.com/git-pkgs/proxy
go 1.26.0
go 1.25.6
toolchain go1.26.6
@ -10,10 +10,9 @@ require (
github.com/git-pkgs/archives v0.5.1
github.com/git-pkgs/cooldown v0.1.1
github.com/git-pkgs/enrichment v0.6.5
github.com/git-pkgs/integrity v0.1.1
github.com/git-pkgs/magic v0.2.0
github.com/git-pkgs/purl v0.1.17
github.com/git-pkgs/registries v0.8.1
github.com/git-pkgs/purl v0.1.16
github.com/git-pkgs/registries v0.7.0
github.com/git-pkgs/spdx v0.3.1
github.com/git-pkgs/vers v0.3.1
github.com/git-pkgs/vulns v0.2.2
@ -32,48 +31,45 @@ require (
)
require (
4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
charm.land/lipgloss/v2 v2.0.6 // indirect
cloud.google.com/go/auth v0.21.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
codeberg.org/chavacava/garif v0.2.0 // indirect
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/exhaustruct/v5 v5.0.3 // indirect
dev.gaijin.team/go/golib v0.8.1 // indirect
dev.gaijin.team/go/golib v0.6.0 // indirect
github.com/4meepo/tagalign v1.4.3 // indirect
github.com/Abirdcfly/dupword v0.1.8 // indirect
github.com/Abirdcfly/dupword v0.1.7 // 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/AlwxSin/noinlineerr v1.0.5 // indirect
github.com/Antonboom/errname v1.1.1 // indirect
github.com/Antonboom/nilnil v1.1.1 // indirect
github.com/Antonboom/testifylint v1.6.4 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.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/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MirrexOne/unqueryvet v1.5.3 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
github.com/alexkohler/prealloc v1.1.0 // indirect
github.com/alexkohler/prealloc v1.0.2 // 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/anchore/go-struct-converter v0.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect
github.com/ashanbrown/makezero/v2 v2.1.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.9 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.20 // indirect
@ -93,49 +89,48 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect
github.com/aws/smithy-go v1.26.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
github.com/bombsimon/wsl/v5 v5.9.0 // indirect
github.com/bombsimon/wsl/v5 v5.6.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
github.com/butuzov/ireturn v0.4.1 // indirect
github.com/butuzov/mirror v1.3.3 // indirect
github.com/butuzov/ireturn v0.4.0 // indirect
github.com/butuzov/mirror v1.3.0 // indirect
github.com/catenacyber/perfsprint v0.10.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/cenk/backoff v2.2.1+incompatible // 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-20260811164956-006e29f97886 // 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/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.7 // indirect
github.com/dave/dst v0.27.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dlclark/regexp2/v2 v2.2.1 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ecosyste-ms/ecosystems-go v0.4.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.8 // indirect
github.com/firefart/nonamedreturns v1.0.6 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.21 // indirect
github.com/ghostiam/protogetter v0.3.20 // indirect
github.com/git-pkgs/packageurl-go v0.3.1 // indirect
github.com/git-pkgs/pom v0.1.5 // indirect
github.com/github/go-spdx/v2 v2.7.0 // indirect
github.com/go-critic/go-critic v0.14.4 // indirect
github.com/go-critic/go-critic v0.14.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
@ -156,82 +151,83 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // 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.1 // indirect
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
github.com/golangci/golangci-lint/v2 v2.10.1 // 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-20260419091836-c5f79b8a11ba // 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/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/wire v0.7.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.18 // indirect
github.com/googleapis/gax-go/v2 v2.23.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.19.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/go-version v1.8.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.11.0 // indirect
github.com/jgautheron/goconst v1.8.2 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.5 // indirect
github.com/josharian/intern v1.0.0 // 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/kisielk/errcheck v1.9.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/kylelemons/godebug v1.1.0 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
github.com/ldez/exptostd v0.4.5 // indirect
github.com/ldez/gomoddirectives v0.9.0 // indirect
github.com/ldez/gomoddirectives v0.8.0 // indirect
github.com/ldez/grignotin v0.10.1 // indirect
github.com/ldez/structtags v0.6.1 // indirect
github.com/ldez/tagliatelle v0.7.2 // indirect
github.com/ldez/usetesting v0.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lucasb-eyer/go-colorful v1.4.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
github.com/manuelarte/funcorder v0.6.0 // indirect
github.com/manuelarte/funcorder v0.5.0 // indirect
github.com/maratori/testableexamples v1.0.1 // indirect
github.com/maratori/testpackage v1.1.2 // indirect
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect
github.com/mgechev/revive v1.15.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mgechev/revive v1.14.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // 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.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.24.0 // indirect
github.com/nunnatsa/ginkgolinter v0.23.0 // indirect
github.com/oapi-codegen/nullable v1.2.0 // indirect
github.com/oapi-codegen/runtime v1.6.0 // indirect
github.com/package-url/packageurl-go v0.1.6 // indirect
github.com/pandatix/go-cvss v0.6.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
@ -239,25 +235,24 @@ require (
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.0 // indirect
github.com/raeperd/recvcheck v0.2.0 // 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/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect
github.com/rubyist/circuitbreaker v2.2.1+incompatible // 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/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
github.com/securego/gosec/v2 v2.28.0 // indirect
github.com/sirupsen/logrus v1.10.1 // indirect
github.com/securego/gosec/v2 v2.23.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.5.1 // indirect
github.com/sourcegraph/go-diff v0.8.0 // indirect
github.com/sonatard/noctx v0.4.0 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
@ -266,11 +261,11 @@ require (
github.com/spf13/viper v1.12.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/stretchr/testify v1.12.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tetafro/godot v1.5.6 // indirect
github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect
github.com/tetafro/godot v1.5.4 // indirect
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
github.com/timonwong/loggercheck v0.11.0 // indirect
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
@ -278,8 +273,8 @@ require (
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
github.com/uudashr/gocognit v1.2.1 // indirect
github.com/uudashr/iface v1.5.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/uudashr/iface v1.4.1 // indirect
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
@ -287,9 +282,9 @@ require (
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-simpler.org/sloglint v0.11.1 // indirect
go.augendre.info/arangolint v0.4.0 // indirect
go.augendre.info/fatcontext v0.10.0 // indirect
go.augendre.info/fatcontext v0.9.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
@ -299,28 +294,28 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.55.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/tools v0.49.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/tools v0.47.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
google.golang.org/api v0.288.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
google.golang.org/api v0.272.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.82.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.8.0 // indirect
honnef.co/go/tools v0.7.0 // indirect
modernc.org/libc v1.74.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
mvdan.cc/gofumpt v0.11.0 // indirect
mvdan.cc/unparam v0.0.0-20260818115549-3f964bcb5673 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

377
go.sum
View file

@ -1,15 +1,13 @@
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/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=
4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ=
charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.21.0 h1:g/QwYfYb2Ai6HH8oomAOyBaIHLbscZ4+T/F/f5JZHkE=
cloud.google.com/go/auth v0.21.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@ -26,25 +24,23 @@ codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh
codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8=
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y=
dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI=
dev.gaijin.team/go/exhaustruct/v5 v5.0.3 h1:yOeA7DNjlT8y4yfmN6nWWYYggA13N523YAj9/TXbuTM=
dev.gaijin.team/go/exhaustruct/v5 v5.0.3/go.mod h1:KwtBsX8nHHH1YxhxkpiBq6bfsmw5WnazWpNvJPHgY9Y=
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=
dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo=
dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8=
github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ=
github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4=
github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
github.com/AlwxSin/noinlineerr v1.0.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/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY=
github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q=
github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ=
github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ=
github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II=
github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
@ -66,8 +62,6 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI=
github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=
github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g=
@ -80,10 +74,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
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/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/MirrexOne/unqueryvet v1.5.3 h1:LpT3rsH+IY3cQddWF9bg4C7jsbASdGnrOSofY8IPEiw=
github.com/MirrexOne/unqueryvet v1.5.3/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/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@ -93,16 +87,16 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
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/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
github.com/alecthomas/repr v0.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/alexkohler/prealloc v1.0.2 h1:MPo8cIkGkZytq7WNH9UHv3DIX1mPz1RatPXnZb0zHWQ=
github.com/alexkohler/prealloc v1.0.2/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=
@ -113,10 +107,10 @@ github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9
github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
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/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo=
github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c=
github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE=
github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY=
github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4=
github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I=
@ -155,6 +149,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8=
github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s=
github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w=
@ -164,18 +160,18 @@ github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkAp
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
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/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8=
github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=
github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
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/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E=
github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70=
github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc=
github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI=
github.com/catenacyber/perfsprint v0.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=
@ -186,24 +182,18 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
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-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA=
github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
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/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs=
github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk=
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/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@ -219,15 +209,12 @@ github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEy
github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo=
github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
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/ecosyste-ms/ecosystems-go v0.4.0 h1:5A+zF+XWT8sYYYjlc2/tI1SmiDGzbHLyT9CapVc5dGA=
@ -241,40 +228,38 @@ 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/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA=
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/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
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/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E=
github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/ghostiam/protogetter v0.3.21 h1:EeWTGvL/Eyosp653hiWb6Byx4b69iJC4/E+za7vQHoI=
github.com/ghostiam/protogetter v0.3.21/go.mod h1:iAKSpyoHwYzay+OpjoWgwzRtPFthEfuUvmlomTThck0=
github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
github.com/git-pkgs/archives v0.5.1 h1:qwu/vsoerQZF1iysRtfcxpy1KIUSJJSpXJ5JNxzNoQw=
github.com/git-pkgs/archives v0.5.1/go.mod h1:AKpkxnts49R9uAt1mL2ULYcHrmYujCDVu24IsFvW9so=
github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w=
github.com/git-pkgs/cooldown v0.1.1/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E=
github.com/git-pkgs/enrichment v0.6.5 h1:U0SPzWVGoK4R8TwojCTASBRTEV+QSs0IitdLmzI/g/k=
github.com/git-pkgs/enrichment v0.6.5/go.mod h1:Vt2PLMvWPOio9DLyC8Gdhh1yxsHwcRcG+L2Kkc9+kak=
github.com/git-pkgs/integrity v0.1.1 h1:nHQ7SktOiGM1dOb5BFnkdtttG/6FCgE6r5ru6QnsGts=
github.com/git-pkgs/integrity v0.1.1/go.mod h1:hxu24lcd230377hCF28JQW7sGcCbuNLqo/0ULeb+F1Q=
github.com/git-pkgs/magic v0.2.0 h1:c7HqVxnP8c88EaVMH0/KraDFVTcmiXckRiSvNZEnvMQ=
github.com/git-pkgs/magic v0.2.0/go.mod h1:3ndidt+yvFaI1M0aEkkzkOlFnLPkeVQASIUojazcxCI=
github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M=
github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4=
github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY=
github.com/git-pkgs/pom v0.1.5/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk=
github.com/git-pkgs/purl v0.1.17 h1:oRSd8tqllTLl74Wa4WnuqU500hXd9OdUnImOEswQUVE=
github.com/git-pkgs/purl v0.1.17/go.mod h1:7u7ora8tQdrkS7Auclr5v8dCJdjN4ej6AbrvYZi2b7k=
github.com/git-pkgs/registries v0.8.1 h1:Yf2FFdARQ1HcdtZfWBYa5OZFwZHzhFYStiz7qbTDDUU=
github.com/git-pkgs/registries v0.8.1/go.mod h1:5dc3V7rOhAI5755L/bDtjtYV4D5XV4J/4ZtyIXSEs0U=
github.com/git-pkgs/purl v0.1.16 h1:VAX6tv0hhdTENbkrGMoPZbOAl1Y8U1/ZnzoCsYuNBYM=
github.com/git-pkgs/purl v0.1.16/go.mod h1:7u7ora8tQdrkS7Auclr5v8dCJdjN4ej6AbrvYZi2b7k=
github.com/git-pkgs/registries v0.7.0 h1:+LbOOMHbvjmXGfsi88hcGH+SfTXYsXA3UY5KYI5mB7s=
github.com/git-pkgs/registries v0.7.0/go.mod h1:VCD4q+ZW0fInopzseg9rAmBEL553R2JQe60UHXtv26w=
github.com/git-pkgs/spdx v0.3.1 h1:58JPY5X9pYpXvnzzZIgehItlBykeOOw52pNc4OBcS+c=
github.com/git-pkgs/spdx v0.3.1/go.mod h1:cqRoZcvl530s/W+oGNvwjt4ODN8T1W6D/20MUZEFdto=
github.com/git-pkgs/vers v0.3.1 h1:jy/ht2wIRJI5zQrccm6GTeYr+hGFwe2z8LV1HOr4Wco=
@ -285,8 +270,8 @@ github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XF
github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8=
github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-critic/go-critic v0.14.4 h1:dSX4C3pWSeuMVxvQh6yG8U0ReSf3YOmKi4nwX5q7n/8=
github.com/go-critic/go-critic v0.14.4/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@ -304,8 +289,8 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
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-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
@ -346,14 +331,14 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
github.com/golangci/go-printf-func-name v0.1.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.1 h1:RuM4OcluM4xFQcGuRE6R7jA33pqxK/W1EsBxpugdZjg=
github.com/golangci/golangci-lint/v2 v2.13.1/go.mod h1:HwX7mDzqHbcSxlhrTygjX1GJbAfQ3sJAqOx41qQlhDE=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE=
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY=
github.com/golangci/golangci-lint/v2 v2.10.1 h1:flhw5Px6ojbLyEFzXvJn5B2HEdkkRlkhE1SnmCbQBiE=
github.com/golangci/golangci-lint/v2 v2.10.1/go.mod h1:dBsrOk6zj0vDhlTv+IiJGqkDokR24IVTS7W3EVfPTQY=
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=
@ -362,8 +347,6 @@ github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3H
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-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg=
github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk=
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=
@ -387,10 +370,10 @@ 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/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/googleapis/enterprise-certificate-proxy v0.3.18 h1:hvVi34VucdrV1IIsiWuqYM8kutw/92MxNEFxCJZEh0k=
github.com/googleapis/enterprise-certificate-proxy v0.3.18/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs=
github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw=
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
@ -410,8 +393,8 @@ github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1T
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -420,8 +403,10 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
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/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4=
github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako=
github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs=
github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c=
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
@ -435,8 +420,8 @@ github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhE
github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
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/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
@ -458,8 +443,8 @@ github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
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.9.0 h1:2YV/EX7nVlWL4jySusYTzBKHuE3D2fgcRsQuMa3yIoo=
github.com/ldez/gomoddirectives v0.9.0/go.mod h1:DdZzfm9MdXCjn2/UGYXCFfo+tzrp2Ib4iD2Q0kIJkwE=
github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk=
github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q=
github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o=
github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas=
github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk=
@ -473,8 +458,8 @@ github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFB
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
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/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
@ -486,8 +471,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8=
github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA=
github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
@ -496,24 +481,24 @@ 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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/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.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q=
github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A=
github.com/mgechev/revive v1.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc=
github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
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=
@ -525,16 +510,16 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nunnatsa/ginkgolinter v0.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8=
github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c=
github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8=
github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=
github.com/oapi-codegen/nullable v1.2.0 h1:VflFkDW980KhBPiFF7nWSyjg+r4Obqj8lXipV0UkP5w=
github.com/oapi-codegen/nullable v1.2.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY=
github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU=
github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU=
github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
github.com/onsi/ginkgo/v2 v2.32.0/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/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
@ -548,17 +533,16 @@ github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITG
github.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q=
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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea h1:sKwxy1H95npauwu8vtF95vG/syrL0p8fSZo/XlDg5gk=
github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea/go.mod h1:1VcHEd3ro4QMoHfiNl/j7Jkln9+KQuorp0PItHMJYNg=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
@ -577,14 +561,15 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl
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.0 h1:PM+XYvyxIj3bo+kobJfFTdTuU3Lmfu96mKDbyHDbRt8=
github.com/raeperd/recvcheck v0.3.0/go.mod h1:PZNwG+HztFYMH2ZPq0Hu3QgkV2yiA6VrtNz9c1fXWJo=
github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI=
github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU=
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8=
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA=
github.com/rubyist/circuitbreaker v2.2.1+incompatible h1:KUKd/pV8Geg77+8LNDwdow6rVCAYOp8+kHUyFvL6Mhk=
@ -594,31 +579,31 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
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/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
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/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
github.com/securego/gosec/v2 v2.28.0 h1:ZsSdiDb0AtTpLFVol5z91gbMei9ZiLEPG/pZjZujp7c=
github.com/securego/gosec/v2 v2.28.0/go.mod h1:lb4/9AHe+lJy/kjWmWRWWsEipvbwGKuxf+tY1Pmjdnk=
github.com/securego/gosec/v2 v2.23.0 h1:h4TtF64qFzvnkqvsHC/knT7YC5fqyOCItlVR8+ptEBo=
github.com/securego/gosec/v2 v2.23.0/go.mod h1:qRHEgXLFuYUDkI2T7W7NJAmOkxVhkR0x9xyHOIcMNZ0=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
github.com/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.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQytHhvY=
github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E=
github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o=
github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=
github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@ -643,14 +628,14 @@ github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRk
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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
@ -661,10 +646,10 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=
github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=
github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA=
github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 h1:SiHe5XLTn9sFWJ5pBwJ5FN/4j34q9ZlOAD//kMoMYp0=
github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg=
github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU=
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk=
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
@ -679,10 +664,10 @@ github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSW
github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
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.0 h1:PgdMt4uAettGG8K/Kbamc4B9FABgUgnS3TLbl6fnjEk=
github.com/uudashr/iface v1.5.0/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA=
github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU=
github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU=
github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@ -702,6 +687,7 @@ github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2
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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
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=
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
@ -710,20 +696,20 @@ 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-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s=
go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ=
go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
go.augendre.info/fatcontext v0.10.0 h1:HhFopmivh8U1+AU7f0kuwUeg2eiIns7YsGQOMHwSJ90=
go.augendre.info/fatcontext v0.10.0/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE=
go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
@ -744,43 +730,51 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
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=
gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q=
gocloud.dev v0.46.0/go.mod h1:ACQe+2qO+hEO+pdcvvsM+RB63r8TyGD1W3ESCLFyzvM=
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.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
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-20260811152304-ee035b5b010f h1:+lI8cDJ4uceLipg2f1ODay7fEuLkk0BIHXd6PB8icxo=
golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk=
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
golang.org/x/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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
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.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -790,6 +784,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -798,8 +794,10 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
@ -807,19 +805,27 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
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.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -828,11 +834,14 @@ golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWc
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.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
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=
@ -845,14 +854,14 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.288.0 h1:glhO/J88obKP5I269W3hB73dvBKrjU56ZfmNlNXpgTU=
google.golang.org/api v0.288.0/go.mod h1:lM2kYRzYUCBY91P9h6VF1PYmvhxii3O5hji37qRvIcY=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU=
google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE=
google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
@ -872,8 +881,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.8.0 h1:UacpzPr7D6i5BAjTkA7sNVcx4kIbhAZcQ4zYtKiXx68=
honnef.co/go/tools v0.8.0/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
@ -902,9 +911,9 @@ 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.11.0 h1:0H01XB95PnN2QgCSR9ELdZyTlJqNZ7181B0BTMh5VZc=
mvdan.cc/gofumpt v0.11.0/go.mod h1:BeT5wCsOJt6J9zT2MZIOGszjUHzFkn1/l9g6xAzqsXo=
mvdan.cc/unparam v0.0.0-20260818115549-3f964bcb5673 h1:dEE6li4OPIE54oojY2qaayFS1fSp17G14si0gXRxl0U=
mvdan.cc/unparam v0.0.0-20260818115549-3f964bcb5673/go.mod h1:62roFV3D3nYOWIXv3PfGO4UYEKAotz2WgLywT87ONd8=
mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4=
mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s=
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI=
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

View file

@ -289,13 +289,49 @@ type AccessLogConfig struct {
Path string `json:"path" yaml:"path"`
}
// UpstreamConfig configures upstream registry URLs and authentication.
// UpstreamConfig configures upstream URLs for built-in routes and authentication.
// Leave empty to use defaults.
type UpstreamConfig struct {
// NPM is the upstream npm registry URL.
// Default: https://registry.npmjs.org
NPM string `json:"npm" yaml:"npm"`
// Cargo is the upstream cargo index URL.
// Default: https://index.crates.io
Cargo string `json:"cargo" yaml:"cargo"`
// CargoDownload is the upstream cargo download URL.
// Default: https://static.crates.io/crates
CargoDownload string `json:"cargo_download" yaml:"cargo_download"`
// Gem is the upstream RubyGems registry URL.
// Default: https://rubygems.org
Gem string `json:"gem" yaml:"gem"`
// Go is the upstream Go module proxy URL.
// Default: https://proxy.golang.org
Go string `json:"go" yaml:"go"`
// Hex is the upstream Hex repository URL.
// Default: https://repo.hex.pm
Hex string `json:"hex" yaml:"hex"`
// HexAPI is the upstream Hex API URL used for package timestamps.
// Default: https://hex.pm
HexAPI string `json:"hex_api" yaml:"hex_api"`
// Pub is the upstream pub registry URL.
// Default: https://pub.dev
Pub string `json:"pub" yaml:"pub"`
// PyPI is the upstream PyPI index and API URL.
// Default: https://pypi.org
PyPI string `json:"pypi" yaml:"pypi"`
// PyPIDownload is the upstream PyPI package download URL.
// Default: https://files.pythonhosted.org
PyPIDownload string `json:"pypi_download" yaml:"pypi_download"`
// Maven is the upstream Maven repository URL.
// Default: https://repo1.maven.org/maven2
Maven string `json:"maven" yaml:"maven"`
@ -305,19 +341,47 @@ type UpstreamConfig struct {
// Default: https://plugins.gradle.org/m2
GradlePluginPortal string `json:"gradle_plugin_portal" yaml:"gradle_plugin_portal"`
// Cargo is the upstream cargo index URL.
// Default: https://index.crates.io
Cargo string `json:"cargo" yaml:"cargo"`
// NuGet is the upstream NuGet API URL.
// Default: https://api.nuget.org
NuGet string `json:"nuget" yaml:"nuget"`
// CargoDownload is the upstream cargo download URL.
// Default: https://static.crates.io/crates
CargoDownload string `json:"cargo_download" yaml:"cargo_download"`
// NuGetSearch is the upstream NuGet search API URL.
// Default: https://azuresearch-usnc.nuget.org
NuGetSearch string `json:"nuget_search" yaml:"nuget_search"`
// Composer is the upstream Packagist API URL.
// Default: https://packagist.org
Composer string `json:"composer" yaml:"composer"`
// ComposerRepository is the upstream Packagist repository URL.
// Default: https://repo.packagist.org
ComposerRepository string `json:"composer_repository" yaml:"composer_repository"`
// Conan is the upstream Conan registry URL.
// Default: https://center.conan.io
Conan string `json:"conan" yaml:"conan"`
// Conda is the upstream Conda channel base URL.
// Default: https://conda.anaconda.org
Conda string `json:"conda" yaml:"conda"`
// CRAN is the upstream CRAN mirror URL.
// Default: https://cloud.r-project.org
CRAN string `json:"cran" yaml:"cran"`
// Julia is the upstream Julia package server URL.
// Default: https://pkg.julialang.org
Julia string `json:"julia" yaml:"julia"`
// Debian is the upstream APT repository base URL.
// Example: http://archive.ubuntu.com/ubuntu would get Ubuntu.
// Default: http://deb.debian.org/debian
Debian string `json:"debian" yaml:"debian"`
// RPM is the upstream RPM repository base URL.
// Default: https://dl.fedoraproject.org/pub/fedora/linux
RPM string `json:"rpm" yaml:"rpm"`
// Helm maps repository names to HTTP Helm chart repository URLs.
// Requests use /helm/{name}/index.yaml and chart URLs in the index are
// rewritten to the same named proxy endpoint.
@ -471,11 +535,27 @@ func Default() *Config {
},
Upstream: UpstreamConfig{
NPM: "https://registry.npmjs.org",
Maven: "https://repo1.maven.org/maven2",
GradlePluginPortal: "https://plugins.gradle.org/m2",
Cargo: "https://index.crates.io",
CargoDownload: "https://static.crates.io/crates",
Gem: "https://rubygems.org",
Go: "https://proxy.golang.org",
Hex: "https://repo.hex.pm",
HexAPI: "https://hex.pm",
Pub: "https://pub.dev",
PyPI: "https://pypi.org",
PyPIDownload: "https://files.pythonhosted.org",
Maven: "https://repo1.maven.org/maven2",
GradlePluginPortal: "https://plugins.gradle.org/m2",
NuGet: "https://api.nuget.org",
NuGetSearch: "https://azuresearch-usnc.nuget.org",
Composer: "https://packagist.org",
ComposerRepository: "https://repo.packagist.org",
Conan: "https://center.conan.io",
Conda: "https://conda.anaconda.org",
CRAN: "https://cloud.r-project.org",
Julia: "https://pkg.julialang.org",
Debian: "http://deb.debian.org/debian",
RPM: "https://dl.fedoraproject.org/pub/fedora/linux",
},
Gradle: GradleConfig{
BuildCache: GradleBuildCacheConfig{
@ -563,9 +643,28 @@ func (c *Config) LoadFromEnv() {
setEnvString(&c.Log.Level, "PROXY_LOG_LEVEL")
setEnvString(&c.Log.Format, "PROXY_LOG_FORMAT")
setEnvString(&c.AccessLog.Path, "PROXY_ACCESS_LOG_PATH")
setEnvString(&c.Upstream.NPM, "PROXY_UPSTREAM_NPM")
setEnvString(&c.Upstream.Cargo, "PROXY_UPSTREAM_CARGO")
setEnvString(&c.Upstream.CargoDownload, "PROXY_UPSTREAM_CARGO_DOWNLOAD")
setEnvString(&c.Upstream.Gem, "PROXY_UPSTREAM_GEM")
setEnvString(&c.Upstream.Go, "PROXY_UPSTREAM_GO")
setEnvString(&c.Upstream.Hex, "PROXY_UPSTREAM_HEX")
setEnvString(&c.Upstream.HexAPI, "PROXY_UPSTREAM_HEX_API")
setEnvString(&c.Upstream.Pub, "PROXY_UPSTREAM_PUB")
setEnvString(&c.Upstream.PyPI, "PROXY_UPSTREAM_PYPI")
setEnvString(&c.Upstream.PyPIDownload, "PROXY_UPSTREAM_PYPI_DOWNLOAD")
setEnvString(&c.Upstream.Maven, "PROXY_UPSTREAM_MAVEN")
setEnvString(&c.Upstream.GradlePluginPortal, "PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL")
setEnvString(&c.Upstream.NuGet, "PROXY_UPSTREAM_NUGET")
setEnvString(&c.Upstream.NuGetSearch, "PROXY_UPSTREAM_NUGET_SEARCH")
setEnvString(&c.Upstream.Composer, "PROXY_UPSTREAM_COMPOSER")
setEnvString(&c.Upstream.ComposerRepository, "PROXY_UPSTREAM_COMPOSER_REPOSITORY")
setEnvString(&c.Upstream.Conan, "PROXY_UPSTREAM_CONAN")
setEnvString(&c.Upstream.Conda, "PROXY_UPSTREAM_CONDA")
setEnvString(&c.Upstream.CRAN, "PROXY_UPSTREAM_CRAN")
setEnvString(&c.Upstream.Julia, "PROXY_UPSTREAM_JULIA")
setEnvString(&c.Upstream.Debian, "PROXY_UPSTREAM_DEBIAN")
setEnvString(&c.Upstream.RPM, "PROXY_UPSTREAM_RPM")
setEnvString(&c.Cooldown.Default, "PROXY_COOLDOWN_DEFAULT")
setEnvBool(&c.CacheMetadata, "PROXY_CACHE_METADATA")
setEnvBool(&c.MirrorAPI, "PROXY_MIRROR_API")
@ -928,7 +1027,8 @@ func ParseSize(s string) (int64, error) {
}
for _, s2 := range suffixes {
if numStr, ok := strings.CutSuffix(s, s2.suffix); ok {
if strings.HasSuffix(s, s2.suffix) {
numStr := strings.TrimSuffix(s, s2.suffix)
num, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, fmt.Errorf("invalid number %q", numStr)

View file

@ -14,6 +14,97 @@ const (
testLevelDebug = "debug"
)
func upstreamConfigValues(upstream UpstreamConfig) map[string]string {
return map[string]string{
"npm": upstream.NPM,
"cargo": upstream.Cargo,
"cargo_download": upstream.CargoDownload,
"gem": upstream.Gem,
"go": upstream.Go,
"hex": upstream.Hex,
"hex_api": upstream.HexAPI,
"pub": upstream.Pub,
"pypi": upstream.PyPI,
"pypi_download": upstream.PyPIDownload,
"maven": upstream.Maven,
"gradle_plugin_portal": upstream.GradlePluginPortal,
"nuget": upstream.NuGet,
"nuget_search": upstream.NuGetSearch,
"composer": upstream.Composer,
"composer_repository": upstream.ComposerRepository,
"conan": upstream.Conan,
"conda": upstream.Conda,
"cran": upstream.CRAN,
"julia": upstream.Julia,
"debian": upstream.Debian,
"rpm": upstream.RPM,
}
}
func defaultUpstreamValues() map[string]string {
return map[string]string{
"npm": "https://registry.npmjs.org",
"cargo": "https://index.crates.io",
"cargo_download": "https://static.crates.io/crates",
"gem": "https://rubygems.org",
"go": "https://proxy.golang.org",
"hex": "https://repo.hex.pm",
"hex_api": "https://hex.pm",
"pub": "https://pub.dev",
"pypi": "https://pypi.org",
"pypi_download": "https://files.pythonhosted.org",
"maven": "https://repo1.maven.org/maven2",
"gradle_plugin_portal": "https://plugins.gradle.org/m2",
"nuget": "https://api.nuget.org",
"nuget_search": "https://azuresearch-usnc.nuget.org",
"composer": "https://packagist.org",
"composer_repository": "https://repo.packagist.org",
"conan": "https://center.conan.io",
"conda": "https://conda.anaconda.org",
"cran": "https://cloud.r-project.org",
"julia": "https://pkg.julialang.org",
"debian": "http://deb.debian.org/debian",
"rpm": "https://dl.fedoraproject.org/pub/fedora/linux",
}
}
func upstreamEnvironmentVariables() map[string]string {
return map[string]string{
"npm": "PROXY_UPSTREAM_NPM",
"cargo": "PROXY_UPSTREAM_CARGO",
"cargo_download": "PROXY_UPSTREAM_CARGO_DOWNLOAD",
"gem": "PROXY_UPSTREAM_GEM",
"go": "PROXY_UPSTREAM_GO",
"hex": "PROXY_UPSTREAM_HEX",
"hex_api": "PROXY_UPSTREAM_HEX_API",
"pub": "PROXY_UPSTREAM_PUB",
"pypi": "PROXY_UPSTREAM_PYPI",
"pypi_download": "PROXY_UPSTREAM_PYPI_DOWNLOAD",
"maven": "PROXY_UPSTREAM_MAVEN",
"gradle_plugin_portal": "PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL",
"nuget": "PROXY_UPSTREAM_NUGET",
"nuget_search": "PROXY_UPSTREAM_NUGET_SEARCH",
"composer": "PROXY_UPSTREAM_COMPOSER",
"composer_repository": "PROXY_UPSTREAM_COMPOSER_REPOSITORY",
"conan": "PROXY_UPSTREAM_CONAN",
"conda": "PROXY_UPSTREAM_CONDA",
"cran": "PROXY_UPSTREAM_CRAN",
"julia": "PROXY_UPSTREAM_JULIA",
"debian": "PROXY_UPSTREAM_DEBIAN",
"rpm": "PROXY_UPSTREAM_RPM",
}
}
func assertUpstreamValues(t *testing.T, cfg *Config, want map[string]string) {
t.Helper()
got := upstreamConfigValues(cfg.Upstream)
for name, wantValue := range want {
if gotValue := got[name]; gotValue != wantValue {
t.Errorf("Upstream %s = %q, want %q", name, gotValue, wantValue)
}
}
}
func TestDefault(t *testing.T) {
cfg := Default()
@ -35,15 +126,7 @@ func TestDefault(t *testing.T) {
if cfg.Gradle.BuildCache.MaxAge != "168h" {
t.Errorf("Gradle.BuildCache.MaxAge = %q, want %q", cfg.Gradle.BuildCache.MaxAge, "168h")
}
if cfg.Upstream.Maven != "https://repo1.maven.org/maven2" {
t.Errorf("Upstream.Maven = %q, want %q", cfg.Upstream.Maven, "https://repo1.maven.org/maven2")
}
if cfg.Upstream.GradlePluginPortal != "https://plugins.gradle.org/m2" {
t.Errorf("Upstream.GradlePluginPortal = %q, want %q", cfg.Upstream.GradlePluginPortal, "https://plugins.gradle.org/m2")
}
if cfg.Upstream.Debian != "http://deb.debian.org/debian" {
t.Errorf("Upstream.Debian = %q, want %q", cfg.Upstream.Debian, "http://deb.debian.org/debian")
}
assertUpstreamValues(t, cfg, defaultUpstreamValues())
}
func TestValidate(t *testing.T) {
@ -250,13 +333,60 @@ access_log:
}
}
func TestLoadYAMLUpstreams(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.yaml")
content := `
upstream:
npm: "https://upstream.example.com/npm"
cargo: "https://upstream.example.com/cargo"
cargo_download: "https://upstream.example.com/cargo_download"
gem: "https://upstream.example.com/gem"
go: "https://upstream.example.com/go"
hex: "https://upstream.example.com/hex"
hex_api: "https://upstream.example.com/hex_api"
pub: "https://upstream.example.com/pub"
pypi: "https://upstream.example.com/pypi"
pypi_download: "https://upstream.example.com/pypi_download"
maven: "https://upstream.example.com/maven"
gradle_plugin_portal: "https://upstream.example.com/gradle_plugin_portal"
nuget: "https://upstream.example.com/nuget"
nuget_search: "https://upstream.example.com/nuget_search"
composer: "https://upstream.example.com/composer"
composer_repository: "https://upstream.example.com/composer_repository"
conan: "https://upstream.example.com/conan"
conda: "https://upstream.example.com/conda"
cran: "https://upstream.example.com/cran"
julia: "https://upstream.example.com/julia"
debian: "https://upstream.example.com/debian"
rpm: "https://upstream.example.com/rpm"
`
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing config file: %v", err)
}
cfg, err := Load(path)
if err != nil {
t.Fatalf("Load failed: %v", err)
}
want := make(map[string]string)
for name := range defaultUpstreamValues() {
want[name] = "https://upstream.example.com/" + name
}
assertUpstreamValues(t, cfg, want)
}
func TestLoadJSON(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.json")
content := `{
"listen": ":4000",
"base_url": "https://json.example.com"
"base_url": "https://json.example.com",
"upstream": {
"gem": "https://json.example.com/gem",
"rpm": "https://json.example.com/rpm"
}
}`
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("writing config file: %v", err)
@ -273,6 +403,12 @@ func TestLoadJSON(t *testing.T) {
if cfg.BaseURL != "https://json.example.com" {
t.Errorf("BaseURL = %q, want %q", cfg.BaseURL, "https://json.example.com")
}
if cfg.Upstream.Gem != "https://json.example.com/gem" {
t.Errorf("Upstream.Gem = %q, want %q", cfg.Upstream.Gem, "https://json.example.com/gem")
}
if cfg.Upstream.RPM != "https://json.example.com/rpm" {
t.Errorf("Upstream.RPM = %q, want %q", cfg.Upstream.RPM, "https://json.example.com/rpm")
}
}
func TestLoadFromEnv(t *testing.T) {
@ -284,9 +420,6 @@ func TestLoadFromEnv(t *testing.T) {
t.Setenv("PROXY_STORAGE_PATH", "/env/cache")
t.Setenv("PROXY_LOG_LEVEL", testLevelDebug)
t.Setenv("PROXY_ACCESS_LOG_PATH", "/tmp/proxy-access.jsonl")
t.Setenv("PROXY_UPSTREAM_MAVEN", "https://maven.example.com/repository/maven-public")
t.Setenv("PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL", "https://plugins.example.com/m2")
t.Setenv("PROXY_UPSTREAM_DEBIAN", "http://archive.ubuntu.com/ubuntu")
t.Setenv("PROXY_GRADLE_BUILD_CACHE_READ_ONLY", "true")
t.Setenv("PROXY_GRADLE_BUILD_CACHE_MAX_UPLOAD_SIZE", "32MB")
t.Setenv("PROXY_GRADLE_BUILD_CACHE_MAX_AGE", "12h")
@ -313,15 +446,6 @@ func TestLoadFromEnv(t *testing.T) {
if cfg.AccessLog.Path != "/tmp/proxy-access.jsonl" {
t.Errorf("AccessLog.Path = %q, want %q", cfg.AccessLog.Path, "/tmp/proxy-access.jsonl")
}
if cfg.Upstream.Maven != "https://maven.example.com/repository/maven-public" {
t.Errorf("Upstream.Maven = %q, want %q", cfg.Upstream.Maven, "https://maven.example.com/repository/maven-public")
}
if cfg.Upstream.GradlePluginPortal != "https://plugins.example.com/m2" {
t.Errorf("Upstream.GradlePluginPortal = %q, want %q", cfg.Upstream.GradlePluginPortal, "https://plugins.example.com/m2")
}
if cfg.Upstream.Debian != "http://archive.ubuntu.com/ubuntu" {
t.Errorf("Upstream.Debian = %q, want %q", cfg.Upstream.Debian, "http://archive.ubuntu.com/ubuntu")
}
if !cfg.Gradle.BuildCache.ReadOnly {
t.Error("Gradle.BuildCache.ReadOnly = false, want true")
}
@ -339,6 +463,19 @@ func TestLoadFromEnv(t *testing.T) {
}
}
func TestLoadFromEnvUpstreams(t *testing.T) {
cfg := Default()
want := make(map[string]string)
for name, envName := range upstreamEnvironmentVariables() {
value := "https://env.example.com/" + name
t.Setenv(envName, value)
want[name] = value
}
cfg.LoadFromEnv()
assertUpstreamValues(t, cfg, want)
}
func TestLoadCooldownConfig(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "config.yaml")

View file

@ -8,11 +8,6 @@ import (
"time"
)
const (
testContentHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
testIntegrity = "sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="
)
func TestCreateAndOpen(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "test.db")
@ -137,7 +132,7 @@ func TestVersionCRUD(t *testing.T) {
v := &Version{
PURL: "pkg:npm/lodash@4.17.21",
PackagePURL: "pkg:npm/lodash",
Integrity: sql.NullString{String: testIntegrity, Valid: true},
Integrity: sql.NullString{String: "sha512-abc123", Valid: true},
}
err = db.UpsertVersion(v)
@ -205,7 +200,7 @@ func TestArtifactCRUD(t *testing.T) {
t.Error("expected artifact to not be cached yet")
}
err = db.MarkArtifactCached(versionPURL, "lodash-4.17.21.tgz", "/cache/npm/lodash-4.17.21.tgz", testContentHash, 12345, "application/gzip")
err = db.MarkArtifactCached(versionPURL, "lodash-4.17.21.tgz", "/cache/npm/lodash-4.17.21.tgz", "sha256-abc", 12345, "application/gzip")
if err != nil {
t.Fatalf("MarkArtifactCached failed: %v", err)
}
@ -262,7 +257,7 @@ func TestGetCachedArtifact(t *testing.T) {
}
if err := db.MarkArtifactCached(versionPURL, filename, "/cache/npm/"+filename,
testContentHash, 12345, "application/gzip"); err != nil {
"sha256-abc", 12345, "application/gzip"); err != nil {
t.Fatalf("MarkArtifactCached failed: %v", err)
}
@ -279,7 +274,7 @@ func TestGetCachedArtifact(t *testing.T) {
if cached.StoragePath != "/cache/npm/"+filename {
t.Errorf("expected cached storage path, got %q", cached.StoragePath)
}
if cached.ContentHash.String != testContentHash {
if cached.ContentHash.String != "sha256-abc" {
t.Errorf("expected cached content hash, got %q", cached.ContentHash.String)
}
if cached.Size.Int64 != 12345 {
@ -288,7 +283,7 @@ func TestGetCachedArtifact(t *testing.T) {
if cached.ContentType.String != "application/gzip" {
t.Errorf("expected cached content type, got %q", cached.ContentType.String)
}
if cached.Integrity.String != testIntegrity {
if cached.Integrity.String != "sha512-abc123" {
t.Errorf("expected cached integrity, got %q", cached.Integrity.String)
}
@ -311,7 +306,7 @@ func seedCachedArtifactTestData(t *testing.T, db *DB, packagePURL, versionPURL,
if err := db.UpsertVersion(&Version{
PURL: versionPURL,
PackagePURL: packagePURL,
Integrity: sql.NullString{String: testIntegrity, Valid: true},
Integrity: sql.NullString{String: "sha512-abc123", Valid: true},
}); err != nil {
t.Fatalf("UpsertVersion failed: %v", err)
}

View file

@ -201,6 +201,43 @@ func (s *Service) CheckVulnerabilities(ctx context.Context, ecosystem, name, ver
return results, nil
}
// BulkCheckVulnerabilities queries vulnerabilities for multiple package versions.
func (s *Service) BulkCheckVulnerabilities(ctx context.Context, packages []struct{ Ecosystem, Name, Version string }) (map[string][]VulnInfo, error) {
purls := make([]*purl.PURL, len(packages))
for i, pkg := range packages {
purls[i] = purl.MakePURL(pkg.Ecosystem, pkg.Name, pkg.Version)
}
vulnResults, err := s.vulnSource.QueryBatch(ctx, purls)
if err != nil {
return nil, err
}
result := make(map[string][]VulnInfo, len(packages))
for i, vulnList := range vulnResults {
pkg := packages[i]
key := purl.MakePURLString(pkg.Ecosystem, pkg.Name, pkg.Version)
var infos []VulnInfo
for _, v := range vulnList {
info := VulnInfo{
ID: v.ID,
Summary: v.Summary,
Severity: v.SeverityLevel(),
CVSSScore: v.CVSSScore(),
FixedVersion: v.FixedVersion(pkg.Ecosystem, pkg.Name),
}
for _, ref := range v.References {
info.References = append(info.References, ref.URL)
}
infos = append(infos, info)
}
result[key] = infos
}
return result, nil
}
// IsOutdated checks if a version is older than the latest version.
func (s *Service) IsOutdated(currentVersion, latestVersion string) bool {
if latestVersion == "" || currentVersion == "" {
@ -251,6 +288,19 @@ func (s *Service) CategorizeLicense(license string) LicenseCategory {
return LicenseUnknown
}
// NormalizeLicense normalizes a license string to SPDX format.
func (s *Service) NormalizeLicense(license string) string {
if license == "" {
return ""
}
if normalized, err := spdx.NormalizeExpressionLax(license); err == nil {
return normalized
}
return license
}
// EnrichmentResult contains all enrichment data for a package version.
type EnrichmentResult struct {
Package *PackageInfo

View file

@ -74,3 +74,25 @@ func TestCategorizeLicense(t *testing.T) {
}
}
}
func TestNormalizeLicense(t *testing.T) {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
svc := New(logger)
tests := []struct {
input string
expected string
}{
{"MIT", "MIT"},
{"Apache 2", "Apache-2.0"},
{"Apache-2.0", "Apache-2.0"},
{"", ""},
}
for _, tc := range tests {
result := svc.NormalizeLicense(tc.input)
if result != tc.expected {
t.Errorf("NormalizeLicense(%q) = %q, want %q", tc.input, result, tc.expected)
}
}
}

View file

@ -37,6 +37,15 @@ func NewComposerHandler(proxy *Proxy, proxyURL string) *ComposerHandler {
}
}
// NewComposerHandlerWithUpstreams creates a Composer handler with custom API
// and repository upstreams.
func NewComposerHandlerWithUpstreams(proxy *Proxy, proxyURL, upstreamURL, repoURL string) *ComposerHandler {
h := NewComposerHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, composerUpstream)
h.repoURL = configuredUpstreamURL(repoURL, composerRepo)
return h
}
// Routes returns the HTTP handler for Composer requests.
func (h *ComposerHandler) Routes() http.Handler {
mux := http.NewServeMux()

View file

@ -27,6 +27,13 @@ func NewConanHandler(proxy *Proxy, proxyURL string) *ConanHandler {
}
}
// NewConanHandlerWithUpstream creates a Conan handler with a custom upstream.
func NewConanHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *ConanHandler {
h := NewConanHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, conanUpstream)
return h
}
// Routes returns the HTTP handler for Conan requests.
func (h *ConanHandler) Routes() http.Handler {
mux := http.NewServeMux()

View file

@ -29,6 +29,13 @@ func NewCondaHandler(proxy *Proxy, proxyURL string) *CondaHandler {
}
}
// NewCondaHandlerWithUpstream creates a Conda handler with a custom upstream.
func NewCondaHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *CondaHandler {
h := NewCondaHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, condaUpstream)
return h
}
// Routes returns the HTTP handler for Conda requests.
func (h *CondaHandler) Routes() http.Handler {
mux := http.NewServeMux()

View file

@ -482,9 +482,8 @@ func TestContainerHandler_BlobHead_DirectServeRedirects(t *testing.T) {
if got := w.Header().Get("Location"); got != store.signedURL {
t.Errorf("Location = %q, want %q", got, store.signedURL)
}
wantETag := `"` + sha256Hex("cached blob") + `"`
if got := w.Header().Get("ETag"); got != wantETag {
t.Errorf("ETag = %q, want %q", got, wantETag)
if got := w.Header().Get("ETag"); got != `"abc123"` {
t.Errorf("ETag = %q, want %q", got, `"abc123"`)
}
if w.Body.Len() != 0 {
t.Errorf("HEAD response body length = %d, want 0", w.Body.Len())

View file

@ -25,6 +25,13 @@ func NewCRANHandler(proxy *Proxy, proxyURL string) *CRANHandler {
}
}
// NewCRANHandlerWithUpstream creates a CRAN handler with a custom upstream.
func NewCRANHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *CRANHandler {
h := NewCRANHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, cranUpstream)
return h
}
// Routes returns the HTTP handler for CRAN requests.
func (h *CRANHandler) Routes() http.Handler {
mux := http.NewServeMux()

View file

@ -49,7 +49,7 @@ func seedPackageWithPURL(t *testing.T, db *database.DB, store *mockStorage, ecos
Filename: filename,
UpstreamURL: "https://example.com/" + filename,
StoragePath: sql.NullString{String: storagePath, Valid: true},
ContentHash: sql.NullString{String: sha256Hex(content), Valid: true},
ContentHash: sql.NullString{String: "abc123", Valid: true},
Size: sql.NullInt64{Int64: int64(len(content)), Valid: true},
ContentType: sql.NullString{String: "application/octet-stream", Valid: true},
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},

View file

@ -30,6 +30,13 @@ func NewGemHandler(proxy *Proxy, proxyURL string) *GemHandler {
}
}
// NewGemHandlerWithUpstream creates a RubyGems handler with a custom upstream.
func NewGemHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *GemHandler {
h := NewGemHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, gemUpstream)
return h
}
// Routes returns the HTTP handler for RubyGems requests.
func (h *GemHandler) Routes() http.Handler {
mux := http.NewServeMux()

View file

@ -30,6 +30,13 @@ func NewGoHandler(proxy *Proxy, proxyURL string) *GoHandler {
}
}
// NewGoHandlerWithUpstream creates a Go module handler with a custom upstream.
func NewGoHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *GoHandler {
h := NewGoHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, goUpstream)
return h
}
// Routes returns the HTTP handler for Go proxy requests.
func (h *GoHandler) Routes() http.Handler {
// Go module paths can contain slashes, so just use the handler directly

View file

@ -47,6 +47,13 @@ func hasDotDotSegment(path string) bool {
return false
}
func configuredUpstreamURL(value, defaultValue string) string {
if value == "" {
value = defaultValue
}
return strings.TrimRight(value, "/")
}
const defaultHTTPTimeout = 30 * time.Second
const artifactCopyBufferSize = 32 << 10
@ -61,7 +68,9 @@ var artifactCopyBufferPool = sync.Pool{ //nolint:gochecknoglobals // shared acro
// canonicalPackagePURL returns a versionless PURL in canonical form so cooldown
// lookups match keys produced by config.CooldownConfig.NormalizedPackages.
func canonicalPackagePURL(ecosystem, name string) string {
return purl.MakePURLString(ecosystem, name, "")
p := purl.MakePURL(ecosystem, name, "")
_ = p.Normalize()
return p.String()
}
const contentTypeJSON = "application/json"
@ -194,11 +203,6 @@ func (p *Proxy) checkCache(ctx context.Context, pkgPURL, versionPURL, filename s
if artifact == nil {
return nil, nil
}
checks, err := newIntegrityChecks(artifact.ContentHash.String, artifact.Integrity.String)
if err != nil {
p.rejectUnusableCacheRecord(artifact, versionPURL, filename, err)
return nil, nil
}
result := &CacheResult{
Size: artifact.Size.Int64,
@ -230,21 +234,16 @@ func (p *Proxy) checkCache(ctx context.Context, pkgPURL, versionPURL, filename s
return nil, nil
}
result.Reader, err = checks.wrap(reader,
result.Reader = newVerifyingReader(reader, artifact.ContentHash.String, artifact.Integrity.String,
func(reason string) {
p.Logger.Error("cached artifact failed integrity check",
"purl", versionPURL, "filename", filename,
"path", artifact.StoragePath, "reason", reason)
metrics.RecordIntegrityFailure(purl.NormalizeEcosystem(artifact.Ecosystem))
metrics.RecordIntegrityFailure(artifact.Ecosystem)
if err := p.DB.ClearArtifactCache(versionPURL, filename); err != nil {
p.Logger.Warn("failed to clear corrupt artifact from cache", "error", err)
}
})
if err != nil {
_ = reader.Close()
p.rejectUnusableCacheRecord(artifact, versionPURL, filename, err)
return nil, nil
}
p.recordCacheHit(artifact.Ecosystem, versionPURL, filename)
return result, nil
}
@ -274,16 +273,6 @@ func (p *Proxy) recordCacheHit(ecosystem, versionPURL, filename string) {
metrics.RecordCacheHit(ecosystem)
}
func (p *Proxy) rejectUnusableCacheRecord(artifact *database.CachedArtifact, versionPURL, filename string, cause error) {
p.Logger.Warn("cached artifact has unusable integrity metadata",
"purl", versionPURL, "filename", filename,
"path", artifact.StoragePath, "error", cause)
metrics.RecordIntegrityFailure(purl.NormalizeEcosystem(artifact.Ecosystem))
if err := p.DB.ClearArtifactCache(versionPURL, filename); err != nil {
p.Logger.Warn("failed to clear unusable artifact from cache", "error", err)
}
}
func (p *Proxy) fetchAndCache(ctx context.Context, ecosystem, name, version, filename, pkgPURL, versionPURL string) (*CacheResult, error) {
// Resolve download URL
info, err := p.Resolver.Resolve(ctx, ecosystem, name, version)

View file

@ -3,7 +3,9 @@ package handler
import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"io"
"log/slog"
@ -44,7 +46,8 @@ func (s *mockStorage) Store(_ context.Context, path string, r io.Reader) (int64,
return 0, "", err
}
s.files[path] = data
return int64(len(data)), sha256Hex(string(data)), nil
digest := sha256.Sum256(data)
return int64(len(data)), hex.EncodeToString(digest[:]), nil
}
func (s *mockStorage) Open(_ context.Context, path string) (io.ReadCloser, error) {
@ -179,7 +182,7 @@ func seedPackage(t testing.TB, db *database.DB, store *mockStorage, ecosystem, n
Filename: filename,
UpstreamURL: "https://example.com/" + filename,
StoragePath: sql.NullString{String: storagePath, Valid: true},
ContentHash: sql.NullString{String: sha256Hex(content), Valid: true},
ContentHash: sql.NullString{String: "abc123", Valid: true},
Size: sql.NullInt64{Int64: int64(len(content)), Valid: true},
ContentType: sql.NullString{String: "application/octet-stream", Valid: true},
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},
@ -268,74 +271,8 @@ func TestGetOrFetchArtifact_CacheHit(t *testing.T) {
if result.ContentType != "application/octet-stream" {
t.Errorf("got content type %q, want %q", result.ContentType, "application/octet-stream")
}
if result.Hash != sha256Hex("cached content") {
t.Errorf("got hash %q, want %q", result.Hash, sha256Hex("cached content"))
}
}
func TestGetCachedArtifactRejectsMalformedIntegrityMetadata(t *testing.T) {
tests := []struct {
name string
malformedHash string
malformedIntegrity string
}{
{name: "content hash", malformedHash: "abc123"},
{name: "native integrity", malformedIntegrity: "sha512-abc123"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assertMalformedCacheRejected(t, test.malformedHash, test.malformedIntegrity)
})
}
}
func assertMalformedCacheRejected(t *testing.T, malformedHash, malformedIntegrity string) {
t.Helper()
proxy, db, store, _ := setupTestProxy(t)
const (
packageName = "broken"
version = "1.0.0"
filename = "broken-1.0.0.tgz"
)
seedPackage(t, db, store, "npm", packageName, version, filename, "cached content")
versionPURL := purl.MakePURLString("npm", packageName, version)
if malformedHash != "" {
artifact, err := db.GetArtifact(versionPURL, filename)
if err != nil {
t.Fatal(err)
}
artifact.ContentHash = sql.NullString{String: malformedHash, Valid: true}
if err := db.UpsertArtifact(artifact); err != nil {
t.Fatal(err)
}
}
if malformedIntegrity != "" {
versionRecord := &database.Version{
PURL: versionPURL,
PackagePURL: purl.MakePURLString("npm", packageName, ""),
Integrity: sql.NullString{String: malformedIntegrity, Valid: true},
}
if err := db.UpsertVersion(versionRecord); err != nil {
t.Fatal(err)
}
}
proxy.DirectServe = true
store.signedURL = "https://cache.example/broken"
result, err := proxy.GetCachedArtifact(context.Background(), "npm", packageName, version, filename)
if err != nil {
t.Fatalf("GetCachedArtifact: %v", err)
}
if result != nil {
t.Errorf("GetCachedArtifact = %+v, want nil", result)
}
artifact, err := db.GetArtifact(versionPURL, filename)
if err != nil {
t.Fatal(err)
}
if artifact.StoragePath.Valid {
t.Error("unusable cache record retained its storage path")
if result.Hash != "abc123" {
t.Errorf("got hash %q, want %q", result.Hash, "abc123")
}
}
@ -370,7 +307,7 @@ func TestGetOrFetchArtifactFromURL_CacheMiss_StorageMissing(t *testing.T) {
Filename: "missing-1.0.0.tgz",
UpstreamURL: "https://example.com/missing.tgz",
StoragePath: sql.NullString{String: "nonexistent/path.tgz", Valid: true},
ContentHash: sql.NullString{String: sha256Hex("missing content"), Valid: true},
ContentHash: sql.NullString{String: "hash", Valid: true},
Size: sql.NullInt64{Int64: 100, Valid: true},
ContentType: sql.NullString{String: "application/octet-stream", Valid: true},
FetchedAt: sql.NullTime{Time: time.Now(), Valid: true},

View file

@ -21,6 +21,7 @@ const (
type HexHandler struct {
proxy *Proxy
upstreamURL string
apiURL string
proxyURL string
}
@ -29,10 +30,20 @@ func NewHexHandler(proxy *Proxy, proxyURL string) *HexHandler {
return &HexHandler{
proxy: proxy,
upstreamURL: hexUpstream,
apiURL: hexAPIURL,
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
}
// NewHexHandlerWithUpstreams creates a Hex handler with custom repository and
// API upstreams.
func NewHexHandlerWithUpstreams(proxy *Proxy, proxyURL, upstreamURL, apiURL string) *HexHandler {
h := NewHexHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, hexUpstream)
h.apiURL = configuredUpstreamURL(apiURL, hexAPIURL)
return h
}
// Routes returns the HTTP handler for Hex requests.
func (h *HexHandler) Routes() http.Handler {
mux := http.NewServeMux()
@ -197,7 +208,7 @@ type hexPackageAPI struct {
// fetchFilteredVersions fetches the Hex API and returns a set of version
// strings that should be filtered out by cooldown.
func (h *HexHandler) fetchFilteredVersions(r *http.Request, name string) (map[string]bool, error) {
apiURL := fmt.Sprintf("%s/api/packages/%s", hexAPIURL, name)
apiURL := fmt.Sprintf("%s/api/packages/%s", h.apiURL, name)
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, apiURL, nil)
if err != nil {
return nil, err

View file

@ -1,100 +1,140 @@
package handler
import (
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"fmt"
"hash"
"io"
"github.com/git-pkgs/integrity"
"strings"
)
type integrityChecks struct {
contentHash integrity.SRI
native integrity.SRI
algorithms []integrity.Algorithm
// parseSRI parses a Subresource Integrity string (e.g. "sha512-abc==") into
// an algorithm name and raw digest bytes. Returns ok=false for empty,
// malformed, or unsupported entries. Only the first hash in a multi-hash
// SRI string is considered.
func parseSRI(s string) (algo string, digest []byte, ok bool) {
s = strings.TrimSpace(s)
if s == "" {
return "", nil, false
}
func newIntegrityChecks(contentHash, native string) (integrityChecks, error) {
checks := integrityChecks{}
if contentHash != "" {
digest, err := integrity.ParseHex(integrity.SHA256, contentHash)
if i := strings.IndexByte(s, ' '); i >= 0 {
s = s[:i]
}
algo, b64, found := strings.Cut(s, "-")
if !found {
return "", nil, false
}
d, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return integrityChecks{}, fmt.Errorf("parse content_hash: %w", err)
return "", nil, false
}
checks.contentHash = integrity.SRI{digest}
checks.algorithms = append(checks.algorithms, integrity.SHA256)
}
if native != "" {
digests, err := integrity.ParseSRI(native)
if err != nil {
return integrityChecks{}, fmt.Errorf("parse integrity: %w", err)
}
checks.native = digests
for _, digest := range digests {
checks.algorithms = append(checks.algorithms, digest.Algorithm())
switch algo {
case "sha256", "sha384", "sha512":
return algo, d, true
default:
return "", nil, false
}
}
return checks, nil
func newSRIHash(algo string) hash.Hash {
switch algo {
case "sha256":
return sha256.New()
case "sha384":
return sha512.New384()
case "sha512":
return sha512.New()
}
return nil
}
func (c integrityChecks) wrap(source io.ReadCloser, onMismatch func(string)) (io.ReadCloser, error) {
if len(c.algorithms) == 0 {
return source, nil
}
reader, err := integrity.NewReader(source, c.algorithms...)
if err != nil {
return nil, fmt.Errorf("create integrity reader: %w", err)
}
return &verifyingReader{
source: source,
reader: reader,
checks: c,
onMismatch: onMismatch,
}, nil
}
// verifyingReader forwards Close to its source and reports completed digest
// mismatches after its shared integrity reader observes EOF.
// verifyingReader wraps an io.ReadCloser and computes SHA256 (and optionally
// a second SRI hash) as bytes are read. When the underlying reader reaches
// EOF it compares the digests against the expected values and calls
// onMismatch for each failure. Verification is skipped if the stream was
// not fully consumed (e.g. client disconnect) to avoid false positives.
type verifyingReader struct {
source io.ReadCloser
reader *integrity.Reader
checks integrityChecks
r io.ReadCloser
sha256 hash.Hash
wantSHA256 string
sri hash.Hash
sriAlgo string
wantSRI []byte
onMismatch func(reason string)
eof bool
verified bool
}
func (r *verifyingReader) Read(p []byte) (int, error) {
n, err := r.reader.Read(p)
func newVerifyingReader(r io.ReadCloser, contentHash, sri string, onMismatch func(string)) io.ReadCloser {
if contentHash == "" && sri == "" {
return r
}
v := &verifyingReader{
r: r,
onMismatch: onMismatch,
}
if contentHash != "" {
v.sha256 = sha256.New()
v.wantSHA256 = contentHash
}
if algo, digest, ok := parseSRI(sri); ok {
v.sri = newSRIHash(algo)
v.sriAlgo = algo
v.wantSRI = digest
}
if v.sha256 == nil && v.sri == nil {
return r
}
return v
}
func (v *verifyingReader) Read(p []byte) (int, error) {
n, err := v.r.Read(p)
if n > 0 {
if v.sha256 != nil {
v.sha256.Write(p[:n])
}
if v.sri != nil {
v.sri.Write(p[:n])
}
}
if err == io.EOF {
r.verify()
v.eof = true
v.verify()
}
return n, err
}
func (r *verifyingReader) Close() error {
return r.source.Close()
func (v *verifyingReader) Close() error {
if v.eof {
v.verify()
}
return v.r.Close()
}
func (r *verifyingReader) verify() {
if r.verified {
return
}
r.verified = true
result := r.reader.Result()
if !result.Complete {
func (v *verifyingReader) verify() {
if v.verified {
return
}
v.verified = true
if len(r.checks.contentHash) > 0 {
if err := result.Verify(r.checks.contentHash); err != nil {
r.onMismatch("content_hash: " + err.Error())
if v.sha256 != nil {
got := hex.EncodeToString(v.sha256.Sum(nil))
if subtle.ConstantTimeCompare([]byte(got), []byte(v.wantSHA256)) != 1 {
v.onMismatch(fmt.Sprintf("content_hash mismatch: stored=%s computed=%s", v.wantSHA256, got))
}
}
if len(r.checks.native) > 0 {
if err := result.Verify(r.checks.native); err != nil {
r.onMismatch("integrity: " + err.Error())
if v.sri != nil {
got := v.sri.Sum(nil)
if subtle.ConstantTimeCompare(got, v.wantSRI) != 1 {
v.onMismatch(fmt.Sprintf("integrity mismatch: %s expected=%s computed=%s",
v.sriAlgo,
base64.StdEncoding.EncodeToString(v.wantSRI),
base64.StdEncoding.EncodeToString(got)))
}
}
}

View file

@ -5,7 +5,6 @@ import (
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"errors"
"io"
"strings"
"testing"
@ -16,68 +15,42 @@ func sha256Hex(data string) string {
return hex.EncodeToString(sum[:])
}
func sha256SRI(data string) string {
sum := sha256.Sum256([]byte(data))
return "sha256-" + base64.StdEncoding.EncodeToString(sum[:])
}
func sha384SRI(data string) string {
sum := sha512.Sum384([]byte(data))
return "sha384-" + base64.StdEncoding.EncodeToString(sum[:])
}
func sha512SRI(data string) string {
sum := sha512.Sum512([]byte(data))
return "sha512-" + base64.StdEncoding.EncodeToString(sum[:])
}
func wrapIntegrityReader(t *testing.T, source io.ReadCloser, contentHash, native string, onMismatch func(string)) io.ReadCloser {
t.Helper()
checks, err := newIntegrityChecks(contentHash, native)
if err != nil {
t.Fatalf("newIntegrityChecks: %v", err)
}
reader, err := checks.wrap(source, onMismatch)
if err != nil {
t.Fatalf("wrap: %v", err)
}
return reader
}
func TestNewIntegrityChecksCollectsAlgorithms(t *testing.T) {
checks, err := newIntegrityChecks(
sha256Hex("hello"),
strings.Join([]string{sha256SRI("first"), sha512SRI("second"), sha384SRI("third"), sha512SRI("alternative")}, " "),
)
if err != nil {
t.Fatal(err)
}
if len(checks.algorithms) != 5 {
t.Fatalf("algorithms = %v, want 5 entries", checks.algorithms)
}
if len(checks.native) != 4 {
t.Errorf("native digests = %d, want 4", len(checks.native))
}
}
func TestNewIntegrityChecksRejectsMalformedMetadata(t *testing.T) {
func TestParseSRI(t *testing.T) {
tests := []struct {
name string
contentHash string
native string
input string
algo string
ok bool
}{
{name: "short content hash", contentHash: "abc123"},
{name: "non-hex content hash", contentHash: strings.Repeat("z", sha256.Size*2)},
{name: "missing SRI separator", native: "sha512"},
{name: "malformed SRI base64", native: "sha512-not!base64"},
{name: "wrong SRI length", native: "sha512-" + base64.StdEncoding.EncodeToString([]byte("short"))},
{name: "unsupported SRI algorithm", native: "md5-1B2M2Y8AsgTpgAmY7PhCfg=="},
{name: "invalid SRI alternative", native: sha512SRI("valid") + " sha384-nope"},
{"sha512", sha512SRI("hello"), "sha512", true},
{"sha256", "sha256-" + base64.StdEncoding.EncodeToString([]byte("0123456789012345678901234567890123456789")), "sha256", true},
{"empty", "", "", false},
{"no dash", "sha512abc", "", false},
{"bad base64", "sha512-not!base64", "", false},
{"unsupported algo", "md5-" + base64.StdEncoding.EncodeToString([]byte("x")), "", false},
{"multi hash takes first", sha512SRI("a") + " " + sha512SRI("b"), "sha512", true},
{"whitespace", " " + sha512SRI("x") + " ", "sha512", true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := newIntegrityChecks(test.contentHash, test.native); err == nil {
t.Fatal("newIntegrityChecks returned nil error")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
algo, digest, ok := parseSRI(tt.input)
if ok != tt.ok {
t.Fatalf("ok = %v, want %v", ok, tt.ok)
}
if !tt.ok {
return
}
if algo != tt.algo {
t.Errorf("algo = %q, want %q", algo, tt.algo)
}
if len(digest) == 0 {
t.Error("digest is empty")
}
})
}
@ -94,156 +67,69 @@ func TestVerifyingReader(t *testing.T) {
sri string
wantCalls int
}{
{name: "both match", hash: goodSHA, sri: goodSRI},
{name: "SHA-256 only match", hash: goodSHA},
{name: "SRI only match", sri: goodSRI},
{name: "SHA-256 mismatch", hash: sha256Hex("other"), wantCalls: 1},
{name: "SRI mismatch", sri: sha512SRI("other"), wantCalls: 1},
{name: "both mismatch", hash: sha256Hex("other"), sri: sha512SRI("other"), wantCalls: 2},
{name: "no checks"},
{"both match", goodSHA, goodSRI, 0},
{"sha256 only match", goodSHA, "", 0},
{"sri only match", "", goodSRI, 0},
{"sha256 mismatch", sha256Hex("other"), "", 1},
{"sri mismatch", "", sha512SRI("other"), 1},
{"both mismatch", sha256Hex("other"), sha512SRI("other"), 2},
{"no checks", "", "", 0},
{"unparseable sri ignored", goodSHA, "garbage", 0},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var calls []string
reader := wrapIntegrityReader(t, io.NopCloser(strings.NewReader(data)), test.hash, test.sri,
r := newVerifyingReader(io.NopCloser(strings.NewReader(data)), tt.hash, tt.sri,
func(reason string) { calls = append(calls, reason) })
got, err := io.ReadAll(reader)
got, err := io.ReadAll(r)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(got) != data {
t.Errorf("data corrupted: got %q", got)
}
if err := reader.Close(); err != nil {
if err := r.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
if len(calls) != test.wantCalls {
t.Errorf("onMismatch called %d times, want %d: %v", len(calls), test.wantCalls, calls)
if len(calls) != tt.wantCalls {
t.Errorf("onMismatch called %d times, want %d: %v", len(calls), tt.wantCalls, calls)
}
})
}
}
func TestVerifyingReaderUsesStrongestNativeAlgorithm(t *testing.T) {
const data = "artifact"
tests := []struct {
name string
native string
wantCalls int
}{
{
name: "weaker match does not override stronger mismatch",
native: sha256SRI(data) + " " + sha512SRI("other"),
wantCalls: 1,
},
{
name: "stronger match ignores weaker mismatch",
native: sha256SRI("other") + " " + sha512SRI(data),
},
{
name: "same algorithm alternative matches",
native: sha512SRI("other") + " " + sha512SRI(data),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var calls int
reader := wrapIntegrityReader(t, io.NopCloser(strings.NewReader(data)), "", test.native, func(string) { calls++ })
if _, err := io.Copy(io.Discard, reader); err != nil {
t.Fatal(err)
}
if calls != test.wantCalls {
t.Errorf("onMismatch called %d times, want %d", calls, test.wantCalls)
}
})
}
}
func TestVerifyingReaderMismatchMessages(t *testing.T) {
const data = "actual"
wantHash := sha256Hex("expected")
wantSRI := sha512SRI("expected")
var reasons []string
reader := wrapIntegrityReader(t, io.NopCloser(strings.NewReader(data)), wantHash, wantSRI,
func(reason string) { reasons = append(reasons, reason) })
if _, err := io.Copy(io.Discard, reader); err != nil {
t.Fatal(err)
}
if len(reasons) != 2 {
t.Fatalf("reasons = %v, want two", reasons)
}
wantContentReason := "content_hash: integrity mismatch: expected " + sha256SRI("expected") + ", calculated " + sha256SRI(data)
if reasons[0] != wantContentReason {
t.Errorf("content reason = %q, want %q", reasons[0], wantContentReason)
}
wantNativeReason := "integrity: integrity mismatch: expected " + wantSRI + ", calculated " + sha512SRI(data)
if reasons[1] != wantNativeReason {
t.Errorf("native reason = %q, want %q", reasons[1], wantNativeReason)
}
}
func TestVerifyingReaderPassthrough(t *testing.T) {
source := io.NopCloser(strings.NewReader("x"))
reader := wrapIntegrityReader(t, source, "", "", func(string) { t.Fatal("should not be called") })
if reader != source {
t.Error("expected passthrough when no hashes were provided")
src := io.NopCloser(strings.NewReader("x"))
r := newVerifyingReader(src, "", "", func(string) { t.Fatal("should not be called") })
if r != src {
t.Error("expected passthrough when no hashes provided")
}
}
type closeTrackingReader struct {
io.Reader
closed bool
}
func (r *closeTrackingReader) Close() error {
r.closed = true
return nil
}
func TestVerifyingReaderPartialRead(t *testing.T) {
source := &closeTrackingReader{Reader: strings.NewReader("hello world")}
var calls int
reader := wrapIntegrityReader(t, source, sha256Hex("other"), "", func(string) { calls++ })
r := newVerifyingReader(io.NopCloser(strings.NewReader("hello world")),
sha256Hex("hello world"), "", func(string) { calls++ })
buffer := make([]byte, 5)
_, _ = reader.Read(buffer)
_ = reader.Close()
buf := make([]byte, 5)
_, _ = r.Read(buf)
_ = r.Close()
if calls != 0 {
t.Errorf("onMismatch called %d times for partial read, want 0", calls)
}
if !source.closed {
t.Error("Close was not forwarded to the source")
}
}
func TestVerifyingReaderNonEOFError(t *testing.T) {
var calls int
reader := wrapIntegrityReader(t, io.NopCloser(errorFixtureReader{}), sha256Hex("data"), "", func(string) { calls++ })
if _, err := io.ReadAll(reader); !errors.Is(err, errIntegrityReadFixture) {
t.Fatalf("ReadAll error = %v", err)
}
if calls != 0 {
t.Errorf("onMismatch called %d times after non-EOF error", calls)
}
}
var errIntegrityReadFixture = errors.New("integrity read fixture")
type errorFixtureReader struct{}
func (errorFixtureReader) Read(p []byte) (int, error) {
return copy(p, "data"), errIntegrityReadFixture
}
func TestVerifyingReaderVerifyOnce(t *testing.T) {
var calls int
reader := wrapIntegrityReader(t, io.NopCloser(strings.NewReader("x")), sha256Hex("y"), "", func(string) { calls++ })
_, _ = io.ReadAll(reader)
_ = reader.Close()
_ = reader.Close()
r := newVerifyingReader(io.NopCloser(strings.NewReader("x")), sha256Hex("y"), "",
func(string) { calls++ })
_, _ = io.ReadAll(r)
_ = r.Close()
_ = r.Close()
if calls != 1 {
t.Errorf("onMismatch called %d times, want 1", calls)
}

View file

@ -53,6 +53,13 @@ func NewJuliaHandler(proxy *Proxy, _ string) *JuliaHandler {
}
}
// NewJuliaHandlerWithUpstream creates a Julia handler with a custom upstream.
func NewJuliaHandlerWithUpstream(proxy *Proxy, upstreamURL string) *JuliaHandler {
h := NewJuliaHandler(proxy, "")
h.upstreamURL = configuredUpstreamURL(upstreamURL, juliaUpstream)
return h
}
// Routes returns the HTTP handler for Julia requests.
func (h *JuliaHandler) Routes() http.Handler {
mux := http.NewServeMux()

View file

@ -12,12 +12,14 @@ import (
const (
nugetUpstream = "https://api.nuget.org"
nugetSearchUpstream = "https://azuresearch-usnc.nuget.org"
)
// NuGetHandler handles NuGet V3 API protocol requests.
type NuGetHandler struct {
proxy *Proxy
upstreamURL string
searchURL string
proxyURL string
}
@ -26,10 +28,20 @@ func NewNuGetHandler(proxy *Proxy, proxyURL string) *NuGetHandler {
return &NuGetHandler{
proxy: proxy,
upstreamURL: nugetUpstream,
searchURL: nugetSearchUpstream,
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
}
// NewNuGetHandlerWithUpstreams creates a NuGet handler with custom API and
// search upstreams.
func NewNuGetHandlerWithUpstreams(proxy *Proxy, proxyURL, upstreamURL, searchURL string) *NuGetHandler {
h := NewNuGetHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, nugetUpstream)
h.searchURL = configuredUpstreamURL(searchURL, nugetSearchUpstream)
return h
}
// Routes returns the HTTP handler for NuGet requests.
func (h *NuGetHandler) Routes() http.Handler {
mux := http.NewServeMux()
@ -103,56 +115,32 @@ func (h *NuGetHandler) rewriteServiceIndex(body []byte) ([]byte, error) {
id, _ := rmap["@id"].(string)
rtype, _ := rmap["@type"].(string)
// Rewrite URLs for services we proxy
if id != "" && h.shouldRewriteService(rtype) {
newURL := h.rewriteNuGetURL(id)
rmap["@id"] = newURL
// Rewrite URLs for services we proxy. The service type determines the
// local route because an upstream index may advertise a different host.
if id != "" {
rmap["@id"] = h.rewriteNuGetURL(id, rtype)
}
}
return json.Marshal(index)
}
// shouldRewriteService returns true if the service type should be rewritten.
func (h *NuGetHandler) shouldRewriteService(serviceType string) bool {
// Rewrite package content and registration services
rewriteTypes := []string{
"PackageBaseAddress/3.0.0",
"RegistrationsBaseUrl/3.6.0",
"RegistrationsBaseUrl/Versioned",
"SearchQueryService",
"SearchQueryService/3.0.0-rc",
"SearchQueryService/3.5.0",
"SearchAutocompleteService",
"SearchAutocompleteService/3.5.0",
}
for _, t := range rewriteTypes {
if serviceType == t {
return true
}
}
return false
}
// rewriteNuGetURL rewrites a NuGet API URL to point at this proxy.
func (h *NuGetHandler) rewriteNuGetURL(origURL string) string {
// Map known NuGet API endpoints to our proxy paths
replacements := map[string]string{
"https://api.nuget.org/v3-flatcontainer/": h.proxyURL + "/nuget/v3-flatcontainer/",
"https://api.nuget.org/v3/registration5-gz-semver2/": h.proxyURL + "/nuget/v3/registration5-gz-semver2/",
"https://azuresearch-usnc.nuget.org/query": h.proxyURL + "/nuget/query",
"https://azuresearch-usnc.nuget.org/autocomplete": h.proxyURL + "/nuget/autocomplete",
}
for old, new := range replacements {
if strings.HasPrefix(origURL, old) {
return strings.Replace(origURL, old, new, 1)
}
}
// rewriteNuGetURL rewrites a NuGet service URL based on its advertised type.
// Service types the proxy does not handle are returned unchanged.
func (h *NuGetHandler) rewriteNuGetURL(origURL, serviceType string) string {
switch serviceType {
case "PackageBaseAddress/3.0.0":
return h.proxyURL + "/nuget/v3-flatcontainer/"
case "RegistrationsBaseUrl/3.6.0", "RegistrationsBaseUrl/Versioned":
return h.proxyURL + "/nuget/v3/registration5-gz-semver2/"
case "SearchQueryService", "SearchQueryService/3.0.0-rc", "SearchQueryService/3.5.0":
return h.proxyURL + "/nuget/query"
case "SearchAutocompleteService", "SearchAutocompleteService/3.5.0":
return h.proxyURL + "/nuget/autocomplete"
default:
return origURL
}
}
// handleRegistration proxies NuGet registration pages, applying cooldown filtering.
func (h *NuGetHandler) handleRegistration(w http.ResponseWriter, r *http.Request) {
@ -363,7 +351,7 @@ func (h *NuGetHandler) buildUpstreamURL(r *http.Request) string {
// Handle query and autocomplete which go to azuresearch
if strings.HasPrefix(path, "/query") || strings.HasPrefix(path, "/autocomplete") {
return "https://azuresearch-usnc.nuget.org" + path + "?" + r.URL.RawQuery
return h.searchURL + path + "?" + r.URL.RawQuery
}
return h.upstreamURL + path

View file

@ -91,40 +91,6 @@ func TestNuGetRewriteServiceIndex(t *testing.T) {
}
}
func TestNuGetShouldRewriteService(t *testing.T) {
h := &NuGetHandler{}
rewriteTypes := []string{
"PackageBaseAddress/3.0.0",
"RegistrationsBaseUrl/3.6.0",
"RegistrationsBaseUrl/Versioned",
"SearchQueryService",
"SearchQueryService/3.0.0-rc",
"SearchQueryService/3.5.0",
"SearchAutocompleteService",
"SearchAutocompleteService/3.5.0",
}
for _, stype := range rewriteTypes {
if !h.shouldRewriteService(stype) {
t.Errorf("shouldRewriteService(%q) = false, want true", stype)
}
}
noRewriteTypes := []string{
"SomeOtherService/1.0.0",
"PackagePublish/2.0.0",
"",
"SearchQueryService/99.0.0",
}
for _, stype := range noRewriteTypes {
if h.shouldRewriteService(stype) {
t.Errorf("shouldRewriteService(%q) = true, want false", stype)
}
}
}
func TestNuGetRewriteURL(t *testing.T) {
h := &NuGetHandler{
proxyURL: "http://localhost:8080",
@ -132,34 +98,75 @@ func TestNuGetRewriteURL(t *testing.T) {
tests := []struct {
input string
serviceType string
want string
}{
{
"https://api.nuget.org/v3-flatcontainer/",
"PackageBaseAddress/3.0.0",
"http://localhost:8080/nuget/v3-flatcontainer/",
},
{
"https://api.nuget.org/v3/registration5-gz-semver2/",
"RegistrationsBaseUrl/3.6.0",
"http://localhost:8080/nuget/v3/registration5-gz-semver2/",
},
{
"https://api.nuget.org/v3/registration5-gz-semver2/",
"RegistrationsBaseUrl/Versioned",
"http://localhost:8080/nuget/v3/registration5-gz-semver2/",
},
{
"https://azuresearch-usnc.nuget.org/query",
"SearchQueryService",
"http://localhost:8080/nuget/query",
},
{
"https://azuresearch-usnc.nuget.org/query",
"SearchQueryService/3.0.0-rc",
"http://localhost:8080/nuget/query",
},
{
"https://azuresearch-usnc.nuget.org/query",
"SearchQueryService/3.5.0",
"http://localhost:8080/nuget/query",
},
{
"https://azuresearch-usnc.nuget.org/autocomplete",
"SearchAutocompleteService",
"http://localhost:8080/nuget/autocomplete",
},
{
"https://azuresearch-usnc.nuget.org/autocomplete",
"SearchAutocompleteService/3.5.0",
"http://localhost:8080/nuget/autocomplete",
},
{
"https://example.com/unknown",
"SomeOtherService/1.0.0",
"https://example.com/unknown",
},
{
"https://api.nuget.org/v2/package",
"PackagePublish/2.0.0",
"https://api.nuget.org/v2/package",
},
{
"https://azuresearch-usnc.nuget.org/query",
"SearchQueryService/99.0.0",
"https://azuresearch-usnc.nuget.org/query",
},
{
"https://example.com/resource",
"",
"https://example.com/resource",
},
}
for _, tt := range tests {
got := h.rewriteNuGetURL(tt.input)
got := h.rewriteNuGetURL(tt.input, tt.serviceType)
if got != tt.want {
t.Errorf("rewriteNuGetURL(%q) = %q, want %q", tt.input, got, tt.want)
t.Errorf("rewriteNuGetURL(%q, %q) = %q, want %q", tt.input, tt.serviceType, got, tt.want)
}
}
}
@ -458,6 +465,7 @@ func TestNuGetProxyUpstreamForwardsAcceptEncoding(t *testing.T) {
func TestNuGetBuildUpstreamURL(t *testing.T) {
h := &NuGetHandler{
upstreamURL: "https://api.nuget.org",
searchURL: "https://azuresearch-usnc.nuget.org",
}
tests := []struct {
@ -736,6 +744,7 @@ func TestNuGetHandleDownloadMissingFilename(t *testing.T) {
func TestNuGetBuildUpstreamURLQueryPath(t *testing.T) {
h := &NuGetHandler{
upstreamURL: "https://api.nuget.org",
searchURL: "https://azuresearch-usnc.nuget.org",
}
// Query endpoint should go to azuresearch
@ -750,6 +759,7 @@ func TestNuGetBuildUpstreamURLQueryPath(t *testing.T) {
func TestNuGetBuildUpstreamURLAutocompletePath(t *testing.T) {
h := &NuGetHandler{
upstreamURL: "https://api.nuget.org",
searchURL: "https://azuresearch-usnc.nuget.org",
}
req := httptest.NewRequest(http.MethodGet, "/autocomplete?q=new&take=10", nil)

View file

@ -30,6 +30,13 @@ func NewPubHandler(proxy *Proxy, proxyURL string) *PubHandler {
}
}
// NewPubHandlerWithUpstream creates a pub handler with a custom upstream.
func NewPubHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *PubHandler {
h := NewPubHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, pubUpstream)
return h
}
// Routes returns the HTTP handler for pub requests.
func (h *PubHandler) Routes() http.Handler {
mux := http.NewServeMux()

View file

@ -8,7 +8,6 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"
@ -16,6 +15,7 @@ import (
const (
pypiUpstream = "https://pypi.org"
pypiDownloadUpstream = "https://files.pythonhosted.org"
minWheelParts = 5 // name + version + python + abi + platform
minSubmatchParts = 2 // full match + first capture group
minPyPIPathParts = 3 // hash_prefix + hash + filename
@ -33,16 +33,27 @@ const (
type PyPIHandler struct {
proxy *Proxy
upstreamURL string
downloadURL string
downloadHrefRe *regexp.Regexp
proxyURL string
}
// NewPyPIHandler creates a new PyPI protocol handler.
func NewPyPIHandler(proxy *Proxy, proxyURL string) *PyPIHandler {
return &PyPIHandler{
return NewPyPIHandlerWithUpstreams(proxy, proxyURL, "", "")
}
// NewPyPIHandlerWithUpstreams creates a PyPI handler with custom API and
// package download upstreams.
func NewPyPIHandlerWithUpstreams(proxy *Proxy, proxyURL, upstreamURL, downloadURL string) *PyPIHandler {
h := &PyPIHandler{
proxy: proxy,
upstreamURL: pypiUpstream,
upstreamURL: configuredUpstreamURL(upstreamURL, pypiUpstream),
downloadURL: configuredUpstreamURL(downloadURL, pypiDownloadUpstream),
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
h.downloadHrefRe = regexp.MustCompile(`href="(` + regexp.QuoteMeta(h.downloadURL) + `/packages/[^"]+)"`)
return h
}
// Routes returns the HTTP handler for PyPI requests.
@ -169,24 +180,16 @@ func (h *PyPIHandler) rewriteSimpleHTML(body []byte, filteredVersions map[string
})
}
// Match href attributes pointing to packages
// PyPI URLs look like: https://files.pythonhosted.org/packages/...
re := regexp.MustCompile(`href="(https://files\.pythonhosted\.org/packages/[^"]+)"`)
return re.ReplaceAllFunc(body, func(match []byte) []byte {
submatch := re.FindSubmatch(match)
// Match href attributes pointing to packages on the configured download host.
return h.downloadHrefRe.ReplaceAllFunc(body, func(match []byte) []byte {
submatch := h.downloadHrefRe.FindSubmatch(match)
if len(submatch) < minSubmatchParts {
return match
}
origURL := string(submatch[1])
u, err := url.Parse(origURL)
if err != nil {
return match
}
newURL := fmt.Sprintf("%s/pypi/packages%s", h.proxyURL, u.Path)
newURL := h.proxyURL + "/pypi/packages" + strings.TrimPrefix(origURL, h.downloadURL)
return []byte(fmt.Sprintf(`href="%s"`, newURL))
})
}
@ -391,15 +394,8 @@ func (h *PyPIHandler) rewriteURLEntry(entry map[string]any) {
return
}
u, err := url.Parse(urlStr)
if err != nil {
return
}
// Only rewrite pythonhosted.org URLs
if u.Host == "files.pythonhosted.org" {
newURL := fmt.Sprintf("%s/pypi/packages%s", h.proxyURL, u.Path)
entry["url"] = newURL
if strings.HasPrefix(urlStr, h.downloadURL+"/packages/") {
entry["url"] = h.proxyURL + "/pypi/packages" + strings.TrimPrefix(urlStr, h.downloadURL)
}
}
@ -441,7 +437,7 @@ func (h *PyPIHandler) handleDownload(w http.ResponseWriter, r *http.Request) {
// Construct upstream URL; the incoming path starts with
// '/packages' so there is no need to include it in the format
// string
upstreamURL := fmt.Sprintf("https://files.pythonhosted.org/%s", path)
upstreamURL := fmt.Sprintf("%s/%s", h.downloadURL, path)
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "pypi", name, version, filename, upstreamURL)
if err != nil {

View file

@ -30,6 +30,13 @@ func NewRPMHandler(proxy *Proxy, proxyURL string) *RPMHandler {
}
}
// NewRPMHandlerWithUpstream creates an RPM handler with a custom upstream.
func NewRPMHandlerWithUpstream(proxy *Proxy, proxyURL, upstreamURL string) *RPMHandler {
h := NewRPMHandler(proxy, proxyURL)
h.upstreamURL = configuredUpstreamURL(upstreamURL, defaultRPMUpstream)
return h
}
// Routes returns the HTTP handler for RPM requests.
// Mount this at /rpm on your router.
func (h *RPMHandler) Routes() http.Handler {

View file

@ -0,0 +1,128 @@
package handler
import (
"io"
"net/http"
"net/http/httptest"
"testing"
)
func TestHandlerUpstreamConfiguration(t *testing.T) {
const (
proxyURL = "https://proxy.example.com/"
baseURL = "https://upstream.example.com"
)
hex := NewHexHandlerWithUpstreams(nil, proxyURL, baseURL+"/hex/", baseURL+"/hex-api/")
pypi := NewPyPIHandlerWithUpstreams(nil, proxyURL, baseURL+"/pypi/", baseURL+"/pypi-download/")
nuget := NewNuGetHandlerWithUpstreams(nil, proxyURL, baseURL+"/nuget/", baseURL+"/nuget-search/")
composer := NewComposerHandlerWithUpstreams(
nil, proxyURL, baseURL+"/composer/", baseURL+"/composer-repository/",
)
got := map[string]string{
"gem": NewGemHandlerWithUpstream(nil, proxyURL, baseURL+"/gem/").upstreamURL,
"go": NewGoHandlerWithUpstream(nil, proxyURL, baseURL+"/go/").upstreamURL,
"hex": hex.upstreamURL,
"hex_api": hex.apiURL,
"pub": NewPubHandlerWithUpstream(nil, proxyURL, baseURL+"/pub/").upstreamURL,
"pypi": pypi.upstreamURL,
"pypi_download": pypi.downloadURL,
"nuget": nuget.upstreamURL,
"nuget_search": nuget.searchURL,
"composer": composer.upstreamURL,
"composer_repository": composer.repoURL,
"conan": NewConanHandlerWithUpstream(nil, proxyURL, baseURL+"/conan/").upstreamURL,
"conda": NewCondaHandlerWithUpstream(nil, proxyURL, baseURL+"/conda/").upstreamURL,
"cran": NewCRANHandlerWithUpstream(nil, proxyURL, baseURL+"/cran/").upstreamURL,
"julia": NewJuliaHandlerWithUpstream(nil, baseURL+"/julia/").upstreamURL,
"rpm": NewRPMHandlerWithUpstream(nil, proxyURL, baseURL+"/rpm/").upstreamURL,
}
want := map[string]string{
"gem": baseURL + "/gem",
"go": baseURL + "/go",
"hex": baseURL + "/hex",
"hex_api": baseURL + "/hex-api",
"pub": baseURL + "/pub",
"pypi": baseURL + "/pypi",
"pypi_download": baseURL + "/pypi-download",
"nuget": baseURL + "/nuget",
"nuget_search": baseURL + "/nuget-search",
"composer": baseURL + "/composer",
"composer_repository": baseURL + "/composer-repository",
"conan": baseURL + "/conan",
"conda": baseURL + "/conda",
"cran": baseURL + "/cran",
"julia": baseURL + "/julia",
"rpm": baseURL + "/rpm",
}
for name, wantURL := range want {
if gotURL := got[name]; gotURL != wantURL {
t.Errorf("%s upstream = %q, want %q", name, gotURL, wantURL)
}
}
}
func TestConfiguredUpstreamURL(t *testing.T) {
if got := configuredUpstreamURL("", "https://default.example.com/"); got != "https://default.example.com" {
t.Errorf("empty configured URL = %q, want default", got)
}
if got := configuredUpstreamURL("https://custom.example.com/", "https://default.example.com"); got != "https://custom.example.com" {
t.Errorf("configured URL = %q, want trimmed custom URL", got)
}
if got := configuredUpstreamURL("https://custom.example.com///", "https://default.example.com"); got != "https://custom.example.com" {
t.Errorf("configured URL with trailing slashes = %q, want trimmed custom URL", got)
}
}
func TestHexHandlerUsesConfiguredAPIUpstream(t *testing.T) {
var requestedPath string
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestedPath = r.URL.Path
_, _ = io.WriteString(w, `{"releases":[]}`)
}))
defer upstream.Close()
h := NewHexHandlerWithUpstreams(
&Proxy{HTTPClient: upstream.Client()},
"https://proxy.example.com",
upstream.URL+"/hex",
upstream.URL+"/hex-api",
)
_, err := h.fetchFilteredVersions(httptest.NewRequest(http.MethodGet, "/", nil), "demo")
if err != nil {
t.Fatalf("fetchFilteredVersions failed: %v", err)
}
if requestedPath != "/hex-api/api/packages/demo" {
t.Errorf("API path = %q, want %q", requestedPath, "/hex-api/api/packages/demo")
}
}
func TestPyPIHandlerRewritesConfiguredDownloadUpstream(t *testing.T) {
h := NewPyPIHandlerWithUpstreams(
nil,
"https://proxy.example.com",
"https://upstream.example.com/pypi",
"https://upstream.example.com/pypi",
)
body := []byte(`<a href="https://upstream.example.com/pypi/packages/packages/ab/demo.whl#sha256=abc">demo</a>`)
want := `<a href="https://proxy.example.com/pypi/packages/packages/packages/ab/demo.whl#sha256=abc">demo</a>`
if got := string(h.rewriteSimpleHTML(body, nil)); got != want {
t.Errorf("rewritten HTML = %q, want %q", got, want)
}
}
func TestNuGetHandlerUsesConfiguredSearchUpstream(t *testing.T) {
h := NewNuGetHandlerWithUpstreams(
nil,
"https://proxy.example.com",
"https://upstream.example.com/nuget",
"https://upstream.example.com/nuget-search",
)
req := httptest.NewRequest(http.MethodGet, "/query?q=demo", nil)
want := "https://upstream.example.com/nuget-search/query?q=demo"
if got := h.buildUpstreamURL(req); got != want {
t.Errorf("search URL = %q, want %q", got, want)
}
}

View file

@ -0,0 +1,16 @@
package mirror
import (
"context"
"fmt"
)
// RegistrySource enumerates all packages in a registry for full mirroring.
// Registry enumeration is not yet implemented for any ecosystem.
type RegistrySource struct {
Ecosystem string
}
func (s *RegistrySource) Enumerate(_ context.Context, _ func(PackageVersion) error) error {
return fmt.Errorf("registry enumeration is not yet implemented for ecosystem %q", s.Ecosystem)
}

View file

@ -0,0 +1,46 @@
package mirror
import (
"context"
"testing"
)
func TestRegistrySourceUnsupported(t *testing.T) {
source := &RegistrySource{Ecosystem: "golang"}
err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
return nil
})
if err == nil {
t.Fatal("expected error for unsupported ecosystem")
}
}
func TestRegistrySourceNPMNotImplemented(t *testing.T) {
source := &RegistrySource{Ecosystem: "npm"}
err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
return nil
})
if err == nil {
t.Fatal("expected not-implemented error")
}
}
func TestRegistrySourcePyPINotImplemented(t *testing.T) {
source := &RegistrySource{Ecosystem: "pypi"}
err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
return nil
})
if err == nil {
t.Fatal("expected not-implemented error")
}
}
func TestRegistrySourceCargoNotImplemented(t *testing.T) {
source := &RegistrySource{Ecosystem: "cargo"}
err := source.Enumerate(context.Background(), func(pv PackageVersion) error {
return nil
})
if err == nil {
t.Fatal("expected not-implemented error")
}
}

View file

@ -430,10 +430,6 @@ func TestHandleBrowseSourcePage(t *testing.T) {
}
}
if !strings.Contains(body, "proxy test-version (test-commit)") {
t.Error("browse source footer should contain proxy build information, not the package version")
}
// Check that the escapeHTML function is present for XSS protection
if !strings.Contains(body, "function escapeHTML(str)") {
t.Error("browse source page missing escapeHTML function for XSS protection")

View file

@ -15,7 +15,7 @@ import (
"github.com/git-pkgs/proxy/internal/storage"
)
func setupEvictionTest(t *testing.T) (*database.DB, *storage.Blob) {
func setupEvictionTest(t *testing.T) (*database.DB, *storage.Filesystem) {
t.Helper()
tempDir := t.TempDir()
@ -27,7 +27,7 @@ func setupEvictionTest(t *testing.T) (*database.DB, *storage.Blob) {
t.Fatalf("failed to create database: %v", err)
}
store, err := storage.OpenBucket(context.Background(), "file://"+storagePath)
store, err := storage.NewFilesystem(storagePath)
if err != nil {
_ = db.Close()
t.Fatalf("failed to create storage: %v", err)
@ -243,7 +243,7 @@ func TestStartEvictionLoop_UnlimitedSkips(t *testing.T) {
}
defer func() { _ = db.Close() }()
store, err := storage.OpenBucket(context.Background(), "file://"+storagePath)
store, err := storage.NewFilesystem(storagePath)
if err != nil {
t.Fatalf("failed to create storage: %v", err)
}
@ -280,7 +280,7 @@ func defaultTestConfig(storagePath, dbPath string) *config.Config {
return &config.Config{
Listen: ":8080",
BaseURL: "http://localhost:8080",
Storage: config.StorageConfig{URL: "file://" + storagePath, MaxSize: ""},
Storage: config.StorageConfig{Path: storagePath, MaxSize: ""},
Database: config.DatabaseConfig{
Driver: "sqlite",
Path: dbPath,

View file

@ -2,24 +2,17 @@ package server
import "net/http"
// BuildInfo identifies the running proxy binary.
type BuildInfo struct {
Version string
Commit string
}
// Layout carries shared fields consumed by the base template. It is embedded
// in every page data struct so templates can access canonical URL and build
// information alongside the page's own fields.
// Layout carries per-request fields consumed by the shared base template
// (canonical URL, og:url). It is embedded in every page data struct so that
// templates can reference {{.UIBaseURL}} and {{.CanonicalPath}} alongside the
// page's own fields.
type Layout struct {
BuildInfo BuildInfo
UIBaseURL string
CanonicalPath string
}
func (s *Server) layoutFor(r *http.Request) Layout {
return Layout{
BuildInfo: s.buildInfo,
UIBaseURL: s.cfg.UIBaseURL,
CanonicalPath: r.URL.Path,
}

View file

@ -92,3 +92,17 @@ func requestEcosystem(path string) string {
return "other"
}
}
// ActiveRequestsMiddleware tracks the number of active requests using Prometheus metrics.
func ActiveRequestsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Don't track metrics endpoint itself
if r.URL.Path == "/metrics" {
next.ServeHTTP(w, r)
return
}
// Implemented in server.go where metrics package is imported
next.ServeHTTP(w, r)
})
}

View file

@ -73,6 +73,36 @@ func TestGetRequestID(t *testing.T) {
}
}
func TestActiveRequestsMiddleware(t *testing.T) {
handler := ActiveRequestsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
}
func TestActiveRequestsMiddleware_SkipsMetricsEndpoint(t *testing.T) {
handler := ActiveRequestsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
req := httptest.NewRequest(http.MethodGet, "/metrics", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
}
func TestLoggerMiddleware(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
s := &Server{logger: logger}

View file

@ -92,7 +92,6 @@ type Server struct {
db *database.DB
storage storage.Storage
logger *slog.Logger
buildInfo BuildInfo
http *http.Server
templates *Templates
cancel context.CancelFunc
@ -101,7 +100,7 @@ type Server struct {
}
// New creates a new Server with the given configuration.
func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, error) {
func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
var activityLog *accesslog.Logger
if cfg.AccessLog.Path != "" {
var err error
@ -170,7 +169,6 @@ func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server,
db: db,
storage: store,
logger: logger,
buildInfo: buildInfo,
templates: &Templates{},
healthCache: hc,
accessLog: activityLog,
@ -243,11 +241,21 @@ func (s *Server) Start() error {
s.cfg.Upstream.Cargo,
s.cfg.Upstream.CargoDownload,
)
gemHandler := handler.NewGemHandler(proxy, s.cfg.BaseURL)
goHandler := handler.NewGoHandler(proxy, s.cfg.BaseURL)
hexHandler := handler.NewHexHandler(proxy, s.cfg.BaseURL)
pubHandler := handler.NewPubHandler(proxy, s.cfg.BaseURL)
pypiHandler := handler.NewPyPIHandler(proxy, s.cfg.BaseURL)
gemHandler := handler.NewGemHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Gem)
goHandler := handler.NewGoHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Go)
hexHandler := handler.NewHexHandlerWithUpstreams(
proxy,
s.cfg.BaseURL,
s.cfg.Upstream.Hex,
s.cfg.Upstream.HexAPI,
)
pubHandler := handler.NewPubHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Pub)
pypiHandler := handler.NewPyPIHandlerWithUpstreams(
proxy,
s.cfg.BaseURL,
s.cfg.Upstream.PyPI,
s.cfg.Upstream.PyPIDownload,
)
mavenHandler := handler.NewMavenHandler(
proxy,
s.cfg.BaseURL,
@ -255,16 +263,26 @@ func (s *Server) Start() error {
s.cfg.Upstream.GradlePluginPortal,
)
gradleHandler := handler.NewGradleBuildCacheHandler(proxy)
nugetHandler := handler.NewNuGetHandler(proxy, s.cfg.BaseURL)
composerHandler := handler.NewComposerHandler(proxy, s.cfg.BaseURL)
conanHandler := handler.NewConanHandler(proxy, s.cfg.BaseURL)
condaHandler := handler.NewCondaHandler(proxy, s.cfg.BaseURL)
cranHandler := handler.NewCRANHandler(proxy, s.cfg.BaseURL)
juliaHandler := handler.NewJuliaHandler(proxy, s.cfg.BaseURL)
nugetHandler := handler.NewNuGetHandlerWithUpstreams(
proxy,
s.cfg.BaseURL,
s.cfg.Upstream.NuGet,
s.cfg.Upstream.NuGetSearch,
)
composerHandler := handler.NewComposerHandlerWithUpstreams(
proxy,
s.cfg.BaseURL,
s.cfg.Upstream.Composer,
s.cfg.Upstream.ComposerRepository,
)
conanHandler := handler.NewConanHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Conan)
condaHandler := handler.NewCondaHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.Conda)
cranHandler := handler.NewCRANHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.CRAN)
juliaHandler := handler.NewJuliaHandlerWithUpstream(proxy, s.cfg.Upstream.Julia)
containerHandler := handler.NewContainerHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.OCI)
helmHandler := handler.NewHelmHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Helm)
debianHandler := handler.NewDebianHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Debian)
rpmHandler := handler.NewRPMHandler(proxy, s.cfg.BaseURL)
rpmHandler := handler.NewRPMHandlerWithUpstream(proxy, s.cfg.BaseURL, s.cfg.Upstream.RPM)
r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes()))
r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes()))

View file

@ -1,7 +1,6 @@
package server
import (
"context"
"database/sql"
"encoding/json"
"fmt"
@ -52,7 +51,7 @@ func newTestServer(t *testing.T) *testServer {
t.Fatalf("failed to create database: %v", err)
}
store, err := storage.OpenBucket(context.Background(), "file://"+storagePath)
store, err := storage.NewFilesystem(storagePath)
if err != nil {
_ = db.Close()
_ = os.RemoveAll(tempDir)
@ -66,7 +65,7 @@ func newTestServer(t *testing.T) *testServer {
cfg := &config.Config{
BaseURL: "http://localhost:8080",
Storage: config.StorageConfig{URL: "file://" + storagePath},
Storage: config.StorageConfig{Path: storagePath},
Database: config.DatabaseConfig{Path: dbPath},
}
@ -105,7 +104,6 @@ func newTestServer(t *testing.T) *testServer {
db: db,
storage: store,
logger: logger,
buildInfo: BuildInfo{Version: "test-version", Commit: "test-commit"},
templates: &Templates{},
healthCache: hc,
}
@ -315,9 +313,6 @@ func TestDashboard(t *testing.T) {
if !strings.Contains(body, "Cached Artifacts") {
t.Error("dashboard should contain stats")
}
if !strings.Contains(body, "proxy test-version (test-commit)") {
t.Error("dashboard footer should contain build information")
}
if !strings.Contains(body, "Popular Packages") {
t.Error("dashboard should contain popular packages section")
}
@ -603,9 +598,6 @@ func TestVersionShowWithHitCount(t *testing.T) {
if !strings.Contains(body, "42 cache hits") {
t.Error("expected page to show hit count")
}
if !strings.Contains(body, "proxy test-version (test-commit)") {
t.Error("version show footer should contain proxy build information, not the package version")
}
}
func TestSearchWithNullValues(t *testing.T) {
@ -1335,14 +1327,10 @@ func TestNewServer_StorageConnectivityCheck(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
buildInfo := BuildInfo{Version: "test-version", Commit: "test-commit"}
srv, err := New(cfg, logger, buildInfo)
srv, err := New(cfg, logger)
if err != nil {
t.Fatalf("New() failed: %v", err)
}
if srv.buildInfo != buildInfo {
t.Errorf("build info = %#v, want %#v", srv.buildInfo, buildInfo)
}
// On Windows, OpenBucket normalises to file:///C:/path; on Unix the
// absolute path already starts with /, so file:// + /path == file:///path.
@ -1360,13 +1348,13 @@ func TestNewServer_InvalidAccessLogFailsBeforeDatabaseInit(t *testing.T) {
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "test.db")
cfg := &config.Config{
Storage: config.StorageConfig{URL: "file://" + filepath.Join(tempDir, "artifacts")},
Storage: config.StorageConfig{Path: filepath.Join(tempDir, "artifacts")},
Database: config.DatabaseConfig{Path: dbPath},
AccessLog: config.AccessLogConfig{Path: filepath.Join(tempDir, "missing", "access.jsonl")},
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
if _, err := New(cfg, logger, BuildInfo{}); err == nil {
if _, err := New(cfg, logger); err == nil {
t.Fatal("New() succeeded with invalid access log path")
} else if !strings.Contains(err.Error(), "initializing access log") {
t.Fatalf("New() error = %v, want access log initialization error", err)

View file

@ -12,11 +12,6 @@
<i data-lucide="github" class="w-4 h-4"></i><span>github.com/git-pkgs/proxy</span>
</a>
</p>
{{if .BuildInfo.Version}}
<p class="text-xs text-gray-500 dark:text-gray-500 mt-2">
proxy {{.BuildInfo.Version}}{{if .BuildInfo.Commit}} ({{.BuildInfo.Commit}}){{end}}
</p>
{{end}}
</div>
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-3">Resources</h3>

View file

@ -186,64 +186,6 @@ func TestRenderEmitsCanonicalAndOG(t *testing.T) {
}
}
func TestFooterUsesBuildInfoWhenPageDefinesVersion(t *testing.T) {
templates := &Templates{}
buildInfo := BuildInfo{Version: "proxy-build-1.2.3", Commit: "abc123def"}
wantFooter := "proxy proxy-build-1.2.3 (abc123def)"
tests := []struct {
name string
page string
data any
shadow string
}{
{
name: "version show page",
page: "version_show",
data: VersionShowData{
Layout: Layout{BuildInfo: buildInfo},
Package: &database.Package{
PURL: "pkg:npm/lodash",
Ecosystem: "npm",
Name: "lodash",
},
Version: &database.Version{
PURL: "pkg:npm/lodash@9.9.9",
PackagePURL: "pkg:npm/lodash",
},
},
shadow: "9.9.9",
},
{
name: "browse source page",
page: "browse_source",
data: BrowseSourceData{
Layout: Layout{BuildInfo: buildInfo},
Ecosystem: "npm",
PackageName: "lodash",
Version: "9.9.9",
},
shadow: "9.9.9",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
w := httptest.NewRecorder()
if err := templates.Render(w, tt.page, tt.data); err != nil {
t.Fatalf("Render(%q) failed: %v", tt.page, err)
}
body := w.Body.String()
if !strings.Contains(body, wantFooter) {
t.Errorf("footer missing build info %q", wantFooter)
}
if strings.Contains(body, "proxy "+tt.shadow) {
t.Errorf("footer used page Version %q instead of BuildInfo", tt.shadow)
}
})
}
}
func TestRenderOmitsCanonicalWhenUIBaseURLUnset(t *testing.T) {
templates := &Templates{}

View file

@ -0,0 +1,277 @@
package storage
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
fsys "io/fs"
"os"
"path/filepath"
"strings"
"time"
)
// Filesystem implements Storage using the local filesystem.
type Filesystem struct {
root string
}
// NewFilesystem creates a new filesystem storage rooted at the given directory.
// The directory will be created if it does not exist.
func NewFilesystem(root string) (*Filesystem, error) {
absRoot, err := filepath.Abs(root)
if err != nil {
return nil, fmt.Errorf("resolving root path: %w", err)
}
if err := os.MkdirAll(absRoot, dirPermissions); err != nil {
return nil, fmt.Errorf("creating root directory: %w", err)
}
return &Filesystem{root: absRoot}, nil
}
func (fs *Filesystem) fullPath(path string) (string, error) {
localPath, err := cleanStoragePath(path)
if err != nil {
return "", err
}
return filepath.Join(fs.root, localPath), nil
}
func (fs *Filesystem) prefixPath(prefix string) (string, error) {
if prefix == "" {
return fs.root, nil
}
return fs.fullPath(prefix)
}
func cleanStoragePath(path string) (string, error) {
if path == "." || strings.Contains(path, `\`) || !filepath.IsLocal(path) {
return "", fmt.Errorf("%w: invalid storage path", ErrNotFound)
}
localPath, err := filepath.Localize(path)
if err != nil || localPath == "." || !filepath.IsLocal(localPath) {
return "", fmt.Errorf("%w: invalid storage path", ErrNotFound)
}
return localPath, nil
}
func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return 0, "", err
}
dir := filepath.Dir(fullPath)
if err := os.MkdirAll(dir, dirPermissions); err != nil {
return 0, "", fmt.Errorf("creating directory: %w", err)
}
// Write to temp file first for atomic operation
tmpFile, err := os.CreateTemp(dir, ".tmp-*")
if err != nil {
return 0, "", fmt.Errorf("creating temp file: %w", err)
}
tmpPath := tmpFile.Name()
// Clean up temp file on error
success := false
defer func() {
if !success {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
}
}()
// Write content and compute hash
h := sha256.New()
w := io.MultiWriter(tmpFile, h)
size, err := io.Copy(w, r)
if err != nil {
return 0, "", fmt.Errorf("writing content: %w", err)
}
if err := tmpFile.Close(); err != nil {
return 0, "", fmt.Errorf("closing temp file: %w", err)
}
// Atomic rename
if err := os.Rename(tmpPath, fullPath); err != nil {
return 0, "", fmt.Errorf("renaming temp file: %w", err)
}
success = true
hash := hex.EncodeToString(h.Sum(nil))
return size, hash, nil
}
func (fs *Filesystem) Open(ctx context.Context, path string) (io.ReadCloser, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return nil, err
}
f, err := os.Open(fullPath)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("opening file: %w", err)
}
return f, nil
}
func (fs *Filesystem) Exists(ctx context.Context, path string) (bool, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return false, err
}
_, err = os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, fmt.Errorf("checking file: %w", err)
}
return true, nil
}
func (fs *Filesystem) Delete(ctx context.Context, path string) error {
fullPath, err := fs.fullPath(path)
if err != nil {
return err
}
err = os.Remove(fullPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("removing file: %w", err)
}
// Try to clean up empty parent directories
dir := filepath.Dir(fullPath)
for dir != fs.root {
if err := os.Remove(dir); err != nil {
break // Directory not empty or other error
}
dir = filepath.Dir(dir)
}
return nil
}
func (fs *Filesystem) SignedURL(_ context.Context, _ string, _ time.Duration) (string, error) {
return "", ErrSignedURLUnsupported
}
func (fs *Filesystem) Size(ctx context.Context, path string) (int64, error) {
fullPath, err := fs.fullPath(path)
if err != nil {
return 0, err
}
info, err := os.Stat(fullPath)
if err != nil {
if os.IsNotExist(err) {
return 0, ErrNotFound
}
return 0, fmt.Errorf("stat file: %w", err)
}
return info.Size(), nil
}
func (fs *Filesystem) UsedSpace(ctx context.Context) (int64, error) {
var total int64
err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
total += info.Size()
}
return nil
})
if err != nil {
return 0, fmt.Errorf("walking directory: %w", err)
}
return total, nil
}
// ListPrefix returns object metadata for paths under a prefix.
func (fs *Filesystem) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) {
searchRoot, err := fs.prefixPath(prefix)
if err != nil {
return nil, err
}
if _, err := os.Stat(searchRoot); err != nil {
if os.IsNotExist(err) {
return []ObjectInfo{}, nil
}
return nil, fmt.Errorf("stat prefix: %w", err)
}
objects := make([]ObjectInfo, 0)
err = filepath.WalkDir(searchRoot, func(path string, entry fsys.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
return nil
}
info, err := entry.Info()
if err != nil {
return err
}
relPath, err := filepath.Rel(fs.root, path)
if err != nil {
return err
}
objects = append(objects, ObjectInfo{
Path: filepath.ToSlash(relPath),
Size: info.Size(),
ModTime: info.ModTime(),
})
return nil
})
if err != nil {
return nil, fmt.Errorf("walking prefix: %w", err)
}
return objects, nil
}
// Root returns the root directory of the storage.
func (fs *Filesystem) Root() string {
return fs.root
}
// FullPath returns the full filesystem path for a storage path.
// Useful for serving files directly or debugging.
// Returns an error if the resulting path would escape the storage root.
func (fs *Filesystem) FullPath(path string) (string, error) {
return fs.fullPath(path)
}
func (fs *Filesystem) URL() string {
return "file://" + filepath.ToSlash(fs.root)
}
func (fs *Filesystem) Close() error {
return nil
}

View file

@ -0,0 +1,331 @@
package storage
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestNewFilesystem(t *testing.T) {
dir := t.TempDir()
root := filepath.Join(dir, "cache")
fs, err := NewFilesystem(root)
if err != nil {
t.Fatalf("NewFilesystem failed: %v", err)
}
if _, err := os.Stat(root); err != nil {
t.Errorf("root directory not created: %v", err)
}
if fs.Root() != root {
t.Errorf("Root() = %q, want %q", fs.Root(), root)
}
}
func TestFilesystemStore(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
content := "test content for storage"
size, hash, err := fs.Store(ctx, "npm/lodash/4.17.21/lodash.tgz", strings.NewReader(content))
if err != nil {
t.Fatalf("Store failed: %v", err)
}
if size != int64(len(content)) {
t.Errorf("size = %d, want %d", size, len(content))
}
h := sha256.Sum256([]byte(content))
wantHash := hex.EncodeToString(h[:])
if hash != wantHash {
t.Errorf("hash = %s, want %s", hash, wantHash)
}
// Verify file exists on disk
fullPath, err := fs.FullPath("npm/lodash/4.17.21/lodash.tgz")
if err != nil {
t.Fatalf("FullPath failed: %v", err)
}
data, err := os.ReadFile(fullPath)
if err != nil {
t.Fatalf("reading stored file: %v", err)
}
if string(data) != content {
t.Errorf("stored content = %q, want %q", string(data), content)
}
}
func TestFilesystemStoreAtomic(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
// Store initial content
_, _, err := fs.Store(ctx, "test/file.txt", strings.NewReader("initial"))
if err != nil {
t.Fatalf("initial Store failed: %v", err)
}
// Overwrite with new content
_, _, err = fs.Store(ctx, "test/file.txt", strings.NewReader("updated"))
if err != nil {
t.Fatalf("update Store failed: %v", err)
}
// Verify updated content
r, err := fs.Open(ctx, "test/file.txt")
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer func() { _ = r.Close() }()
data, _ := io.ReadAll(r)
if string(data) != "updated" {
t.Errorf("content = %q, want %q", string(data), "updated")
}
}
func TestFilesystemOpen(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
content := "readable content"
_, _, _ = fs.Store(ctx, "test/read.txt", strings.NewReader(content))
r, err := fs.Open(ctx, "test/read.txt")
if err != nil {
t.Fatalf("Open failed: %v", err)
}
defer func() { _ = r.Close() }()
data, err := io.ReadAll(r)
if err != nil {
t.Fatalf("ReadAll failed: %v", err)
}
if string(data) != content {
t.Errorf("content = %q, want %q", string(data), content)
}
}
func TestFilesystemOpenNotFound(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
_, err := fs.Open(ctx, "does/not/exist.txt")
if !errors.Is(err, ErrNotFound) {
t.Errorf("Open non-existent = %v, want ErrNotFound", err)
}
}
func TestFilesystemExists(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
exists, err := fs.Exists(ctx, "test/exists.txt")
if err != nil {
t.Fatalf("Exists failed: %v", err)
}
if exists {
t.Error("Exists returned true for non-existent file")
}
_, _, _ = fs.Store(ctx, "test/exists.txt", strings.NewReader("content"))
exists, err = fs.Exists(ctx, "test/exists.txt")
if err != nil {
t.Fatalf("Exists after store failed: %v", err)
}
if !exists {
t.Error("Exists returned false for existing file")
}
}
func TestFilesystemDelete(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
_, _, _ = fs.Store(ctx, "test/delete/nested/file.txt", strings.NewReader("content"))
err := fs.Delete(ctx, "test/delete/nested/file.txt")
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
exists, _ := fs.Exists(ctx, "test/delete/nested/file.txt")
if exists {
t.Error("file still exists after delete")
}
// Empty parent directories should be cleaned up
nestedDir, err := fs.FullPath("test/delete/nested")
if err != nil {
t.Fatalf("FullPath failed: %v", err)
}
if _, err := os.Stat(nestedDir); !os.IsNotExist(err) {
t.Error("empty nested directory not cleaned up")
}
}
func TestFilesystemDeleteNotFound(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
// Delete non-existent file should not error
err := fs.Delete(ctx, "does/not/exist.txt")
if err != nil {
t.Errorf("Delete non-existent = %v, want nil", err)
}
}
func TestFilesystemSize(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
content := "size test content"
_, _, _ = fs.Store(ctx, "test/size.txt", strings.NewReader(content))
size, err := fs.Size(ctx, "test/size.txt")
if err != nil {
t.Fatalf("Size failed: %v", err)
}
if size != int64(len(content)) {
t.Errorf("Size = %d, want %d", size, len(content))
}
}
func TestFilesystemSizeNotFound(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
_, err := fs.Size(ctx, "does/not/exist.txt")
if !errors.Is(err, ErrNotFound) {
t.Errorf("Size non-existent = %v, want ErrNotFound", err)
}
}
func TestFilesystemUsedSpace(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
// Empty storage
used, err := fs.UsedSpace(ctx)
if err != nil {
t.Fatalf("UsedSpace failed: %v", err)
}
if used != 0 {
t.Errorf("UsedSpace empty = %d, want 0", used)
}
// Add some files
_, _, _ = fs.Store(ctx, "a.txt", strings.NewReader("aaaa")) // 4 bytes
_, _, _ = fs.Store(ctx, "b.txt", strings.NewReader("bbbbbb")) // 6 bytes
_, _, _ = fs.Store(ctx, "c/d.txt", strings.NewReader("ccccc")) // 5 bytes
used, err = fs.UsedSpace(ctx)
if err != nil {
t.Fatalf("UsedSpace failed: %v", err)
}
if used != 15 {
t.Errorf("UsedSpace = %d, want 15", used)
}
}
func TestFilesystemLargeFile(t *testing.T) {
assertLargeFileRoundTrip(t, createTestFilesystem(t))
}
func TestFilesystemRejectsInvalidPaths(t *testing.T) {
tmp := t.TempDir()
fs, err := NewFilesystem(tmp)
if err != nil {
t.Fatal(err)
}
for _, p := range []string{
"",
".",
"../etc/passwd",
"../../etc/passwd",
"a/../../etc/passwd",
"/etc/passwd",
"test//file.txt",
"test/./file.txt",
"test/../file.txt",
`test\..\file.txt`,
} {
name := p
if name == "" {
name = "empty"
}
t.Run(name, func(t *testing.T) {
ctx := context.Background()
if _, err := fs.FullPath(p); !errors.Is(err, ErrNotFound) {
t.Errorf("FullPath(%q) = %v, want ErrNotFound", p, err)
}
if _, err := fs.Open(ctx, p); err == nil {
t.Errorf("Open(%q) should reject invalid path", p)
}
if _, _, err := fs.Store(ctx, p, strings.NewReader("x")); err == nil {
t.Errorf("Store(%q) should reject invalid path", p)
}
if _, err := fs.Exists(ctx, p); err == nil {
t.Errorf("Exists(%q) should reject invalid path", p)
}
if err := fs.Delete(ctx, p); err == nil {
t.Errorf("Delete(%q) should reject invalid path", p)
}
if _, err := fs.Size(ctx, p); err == nil {
t.Errorf("Size(%q) should reject invalid path", p)
}
if _, err := fs.ListPrefix(ctx, p); p != "" && err == nil {
t.Errorf("ListPrefix(%q) should reject invalid path", p)
}
})
}
}
func TestFilesystemListPrefixAllowsEmptyPrefix(t *testing.T) {
fs := createTestFilesystem(t)
ctx := context.Background()
_, _, _ = fs.Store(ctx, "a.txt", strings.NewReader("aaaa"))
_, _, _ = fs.Store(ctx, "c/d.txt", strings.NewReader("ccccc"))
objects, err := fs.ListPrefix(ctx, "")
if err != nil {
t.Fatalf("ListPrefix empty prefix failed: %v", err)
}
if len(objects) != 2 {
t.Fatalf("ListPrefix empty prefix returned %d objects, want 2", len(objects))
}
}
func TestFilesystemSignedURLUnsupported(t *testing.T) {
fs := createTestFilesystem(t)
_, err := fs.SignedURL(context.Background(), "test/file.txt", time.Minute)
if !errors.Is(err, ErrSignedURLUnsupported) {
t.Errorf("SignedURL = %v, want ErrSignedURLUnsupported", err)
}
}
func createTestFilesystem(t *testing.T) *Filesystem {
t.Helper()
dir := t.TempDir()
fs, err := NewFilesystem(dir)
if err != nil {
t.Fatalf("NewFilesystem failed: %v", err)
}
return fs
}

View file

@ -11,6 +11,8 @@ package storage
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"time"
@ -78,3 +80,42 @@ func ArtifactPath(ecosystem, namespace, name, version, filename string) string {
}
return ecosystem + "/" + name + "/" + version + "/" + filename
}
// HashingReader wraps a reader and computes SHA256 hash as content is read.
type HashingReader struct {
r io.Reader
hash []byte
h interface{ Sum([]byte) []byte }
size int64
done bool
}
func NewHashingReader(r io.Reader) *HashingReader {
h := sha256.New()
return &HashingReader{
r: io.TeeReader(r, h),
h: h,
}
}
func (hr *HashingReader) Read(p []byte) (n int, err error) {
n, err = hr.r.Read(p)
hr.size += int64(n)
if err == io.EOF {
hr.done = true
hr.hash = hr.h.Sum(nil)
}
return
}
func (hr *HashingReader) Sum() string {
if !hr.done {
hr.hash = hr.h.Sum(nil)
hr.done = true
}
return hex.EncodeToString(hr.hash)
}
func (hr *HashingReader) Size() int64 {
return hr.size
}

View file

@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"io"
"strings"
"testing"
)
@ -34,6 +35,30 @@ func TestArtifactPath(t *testing.T) {
}
}
func TestHashingReader(t *testing.T) {
content := "hello world"
r := NewHashingReader(strings.NewReader(content))
data, err := io.ReadAll(r)
if err != nil {
t.Fatalf("ReadAll failed: %v", err)
}
if string(data) != content {
t.Errorf("got content %q, want %q", string(data), content)
}
if r.Size() != int64(len(content)) {
t.Errorf("got size %d, want %d", r.Size(), len(content))
}
h := sha256.Sum256([]byte(content))
wantHash := hex.EncodeToString(h[:])
if r.Sum() != wantHash {
t.Errorf("got hash %s, want %s", r.Sum(), wantHash)
}
}
// assertLargeFileRoundTrip stores a 1MB file in the given storage, verifies size and
// hash, then reads it back and confirms the content matches.
func assertLargeFileRoundTrip(t *testing.T, s Storage) {

296
supporting-module-issues.md Normal file
View file

@ -0,0 +1,296 @@
# Package repository support issues
These drafts are grouped by the repository where the work belongs. The proxy
issues are already open. The remaining drafts have not been posted.
Helm, Chef, and Vagrant are listed as candidate PURL types. Helm has an open
upstream pull request, while Chef and Vagrant still need upstream type
definitions. Their registry clients should follow the accepted definitions
when exposing package identities through shared APIs.
## `git-pkgs/proxy`
Already open:
- [#261 Add Helm repository proxy support](https://github.com/git-pkgs/proxy/issues/261)
- [#262 Add Alpine APK repository proxy support](https://github.com/git-pkgs/proxy/issues/262)
- [#263 Add Arch Linux package repository proxy support](https://github.com/git-pkgs/proxy/issues/263)
- [#264 Add Chef Supermarket proxy support](https://github.com/git-pkgs/proxy/issues/264)
- [#265 Add Vagrant box repository proxy support](https://github.com/git-pkgs/proxy/issues/265)
## `package-url/purl-spec`
Helm type work is already open as
[#236](https://github.com/package-url/purl-spec/pull/236). The pull request
needs to be updated to the current JSON type-definition format. Do not open a
second Helm proposal.
### Define the Chef PURL type
Chef is listed as a candidate PURL type, but it has no type definition or
tracking issue.
Define how Chef cookbooks map to PURL components. Cover cookbook names,
versions, Supermarket hosts, private Supermarket instances, and source
repositories. Include type tests and examples for the public Supermarket and
a configured private server.
### Define the Vagrant PURL type
Vagrant is listed as a candidate PURL type, but it has no type definition or
tracking issue.
Define how Vagrant boxes map to PURL components. Cover namespaces, box names,
versions, providers, architectures, catalog URLs, and checksums. Include type
tests and examples for Vagrant Cloud and a configured private catalog.
## `git-pkgs/purl`
### Add Helm, Chef, and Vagrant PURL type support
The upstream specification lists Helm, Chef, and Vagrant as candidate types.
Helm has an open type proposal, while Chef and Vagrant still need definitions.
After each definition is accepted upstream, add it to the local type catalog
and implement any required normalization, validation, version handling, and
OSV mappings. Add construction, parsing, normalization, and round-trip tests
using the upstream examples.
## `git-pkgs/manifests`
### Add Helm chart manifest and lockfile support
Add parsers for `Chart.yaml` and `Chart.lock`.
`Chart.yaml` should report the chart name, version, declared dependencies,
repository URLs, and dependency version constraints. `Chart.lock` should
report the resolved dependency set and digest without treating the generated
timestamp as package identity.
Tests should cover aliased repositories, OCI repository URLs, conditions,
tags, and charts with no dependencies.
### Add Chef cookbook manifest support
Add static parsers for Chef `metadata.rb` and `Berksfile` files. Do not execute
Ruby while parsing them.
The metadata parser should report the cookbook name, version, license, and
`depends` declarations. The Berksfile parser should report cookbook sources,
version constraints, and git or path sources when they are static string
literals.
Tests should cover unconstrained dependencies, pessimistic constraints,
custom Supermarket sources, and dynamic Ruby expressions that must be skipped
without failing the whole file.
### Add Vagrant box declarations from Vagrantfile
Add a static `Vagrantfile` parser for box dependencies. Do not execute the
Ruby configuration file.
Recognize literal values assigned to `config.vm.box`, `box_version`,
`box_url`, `box_download_checksum`, and `box_download_checksum_type`. Return
the box as a direct dependency with its version, source URL, and integrity
metadata when available.
Tests should cover namespaced boxes, version constraints, custom catalog URLs,
checksums, and dynamic expressions that cannot be resolved safely.
Alpine `APKBUILD` and Arch `PKGBUILD` parsing already exist, so they do not
need new manifest issues.
## `git-pkgs/registries`
### Add Helm HTTP repository metadata support
Add a registry client for HTTP Helm repositories backed by `index.yaml`.
Map charts and chart releases to the shared package and version types. Include
creation time, digest, deprecation status, home and source links,
dependencies, maintainers, and absolute or relative artifact URLs when those
fields are present.
Require a configured repository URL because Helm has no single default HTTP
repository. Tests should cover multiple chart versions, relative URLs,
deprecated charts, missing optional fields, and custom authenticated
repositories.
This issue depends on an accepted Helm PURL type definition. Track
[purl-spec#236](https://github.com/package-url/purl-spec/pull/236).
### Add OCI registry metadata support
Add a registry client for `pkg:oci` packages. Support tag listing, manifest
retrieval by tag or digest, OCI indexes, annotations, and Bearer authentication
challenges.
Preserve the artifact type and layer media types so callers can distinguish
container images, Helm charts, and other OCI artifacts. Tests should include
the Helm config and chart layer media types as well as ordinary image
manifests.
### Add Alpine APK repository metadata support
Add an `apk` registry client for configured Alpine repositories. There is no
single default repository, so `repository_url` should identify a concrete
release, repository, and architecture location.
Read package names, versions, descriptions, licenses, dependencies, build
times, maintainers, architectures, and checksums from v2 `APKINDEX.tar.gz` and
v3 `Packages.adb` indexes. Keep namespace handling compatible with standard
`pkg:apk` PURLs.
Tests should cover v2 and v3 indexes, multiple architectures, versioned PURLs,
missing optional fields, and authenticated repositories.
### Add Arch Linux repository metadata support
Add an `alpm` registry client for configured pacman repositories. There is no
single default repository, so `repository_url` should identify the repository
and architecture location.
Parse repository database entries into package metadata, versions,
dependencies, licenses, build dates, packagers, architectures, and SHA-256
checksums. Keep namespace handling compatible with standard `pkg:alpm` PURLs.
Tests should cover compressed repository databases, epoch and package-release
versions, architecture qualifiers, missing optional fields, and authenticated
repositories.
### Add Chef Supermarket registry support
Add a Chef Supermarket registry client using `/universe` and the cookbook API.
Return cookbook metadata, available versions, publication times,
dependencies, maintainers, source links, and versioned download locations.
Support the public Supermarket and configured private Supermarket servers.
Tests should cover dependency graphs, version metadata, custom base URLs,
authentication, redirects, and missing cookbooks.
This issue depends on an accepted Chef PURL type definition.
### Add Vagrant box catalog registry support
Add a Vagrant registry client that reads box catalog metadata and supports
Vagrant Cloud catalog responses.
Return box descriptions, versions, providers, architectures, download URLs,
checksums, and release timestamps when available. Support custom catalog URLs
and configured credentials.
Tests should cover multiple providers and architectures, legacy catalog
entries without architecture fields, checksum types, redirects, private
catalogs, and missing boxes.
This issue depends on an accepted Vagrant PURL type definition.
## `git-pkgs/archives`
### Handle real Alpine APK v2 and v3 packages
The archive reader currently routes Alpine `.apk` files to the shared gzip
and tar reader. Signed APK v2 packages contain concatenated gzip streams for
the signature, control metadata, and payload. A standard tar reader stops after
the first tar stream, so it cannot reliably browse or extract the package
payload. APK v3 adds the ADB format.
Add format-aware APK readers that expose package metadata and payload files
without confusing Android APK ZIP files. Preserve the existing decompression
and path safety limits.
Tests should use signed v2 and v3 fixtures and cover listing, extraction,
malformed stream boundaries, decompression limits, and Android APK detection.
Arch `.pkg.tar.zst` support is already tracked by
[archives#23](https://github.com/git-pkgs/archives/issues/23), so do not open a
duplicate zstd issue. Helm charts, Chef cookbooks, and Vagrant boxes use
physical archive formats the module can already detect.
## `git-pkgs/magic`
### Detect Alpine APK v3 ADB data
Add byte-signature detection for Alpine APK v3 ADB indexes and packages. Keep
the result at the physical format level, consistent with the existing gzip,
zstd, tar, and ZIP classifications.
Tests should cover valid ADB prefixes, short input, malformed headers, and
nearby byte sequences that must remain unclassified.
## `git-pkgs/integrity`
### Add verification-only MD5 and SHA-1 checksums
Vagrant box catalogs may publish MD5 or SHA-1 checksums as well as SHA-256,
SHA-384, and SHA-512. Add MD5 and SHA-1 algorithms for checking bytes against
publisher-supplied metadata.
Keep the weak algorithms out of SRI parsing and formatting, and document that
they are provided only for compatibility verification. They must not be
selected for newly generated integrity metadata when a stronger algorithm is
available.
Add reader, hexadecimal parsing, length validation, verification, and failure
tests for both algorithms.
## `git-pkgs/managers`
These command definitions are useful for client integration and broader tool
support, but they do not block the proxy handlers.
### Add apk package manager commands
Add an `apk` manager definition for Alpine packages. Cover install, add,
remove, list, outdated, update, resolve, and path operations where apk exposes
a suitable command.
Tests should verify generated commands, repository arguments, versioned
packages, non-interactive flags, and operations that require elevated
permissions.
### Add pacman package manager commands
Add a `pacman` manager definition for Arch packages. Cover install, add,
remove, list, outdated, update, resolve, and path operations where pacman
exposes a suitable command.
Tests should verify generated commands, repository refresh behavior,
versioned packages, non-interactive flags, and operations that require
elevated permissions.
### Add Berkshelf commands
Add a `berks` manager definition for Chef cookbook dependencies. Cover
install, list, outdated, update, resolve, and vendor operations.
Tests should verify generated commands, frozen or locked installs, custom
Berksfile paths, and JSON or machine-readable output where Berkshelf provides
it.
### Add Vagrant box commands
Add a `vagrant` manager definition for box operations. Cover box add, remove,
list, outdated, update, and path operations where Vagrant exposes a suitable
command.
Tests should verify namespaced boxes, versions, providers, architectures,
custom catalog URLs, checksums, and non-interactive flags.
Helm manager support already exists.
## `git-pkgs/enrichment`
### Expose new package registry clients through enrichment
After the registry clients and PURL conventions land, update `enrichment` to
expose metadata for Helm, Alpine, Arch, Chef, and Vagrant packages through the
direct and hybrid clients.
Add dependency updates and conversion tests for package metadata, versions,
publication times, integrity values, status fields, and custom
`repository_url` values. Alpine and Arch vulnerability PURL translation
already exists and should remain covered.
This issue should wait until the corresponding `registries` work is available.