From d86bc633acd2e184d15c58007d10a4850501e6e5 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Fri, 14 Aug 2026 14:29:02 +0100
Subject: [PATCH 1/9] Make built-in upstream URLs configurable
---
README.md | 2 +
config.example.yaml | 62 ++++++++--
docs/configuration.md | 47 +++++++-
internal/config/config.go | 117 +++++++++++++++++--
internal/config/config_test.go | 181 ++++++++++++++++++++++++++----
internal/handler/composer.go | 9 ++
internal/handler/conan.go | 7 ++
internal/handler/conda.go | 7 ++
internal/handler/cran.go | 7 ++
internal/handler/gem.go | 7 ++
internal/handler/go.go | 7 ++
internal/handler/handler.go | 7 ++
internal/handler/hex.go | 13 ++-
internal/handler/julia.go | 7 ++
internal/handler/nuget.go | 51 +++++----
internal/handler/nuget_test.go | 14 ++-
internal/handler/pub.go | 7 ++
internal/handler/pypi.go | 50 ++++-----
internal/handler/rpm.go | 7 ++
internal/handler/upstream_test.go | 125 +++++++++++++++++++++
internal/server/server.go | 44 ++++++--
21 files changed, 671 insertions(+), 107 deletions(-)
create mode 100644 internal/handler/upstream_test.go
diff --git a/README.md b/README.md
index 320a737..253aff0 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/config.example.yaml b/config.example.yaml
index 1df95b3..f0a08f2 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -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"
diff --git a/docs/configuration.md b/docs/configuration.md
index 3b8b935..e3fcdd7 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -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"
diff --git a/internal/config/config.go b/internal/config/config.go
index fdf890d..ca6fdb6 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -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")
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 0ccc308..52f381d 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -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")
diff --git a/internal/handler/composer.go b/internal/handler/composer.go
index 45935b7..9b2de80 100644
--- a/internal/handler/composer.go
+++ b/internal/handler/composer.go
@@ -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()
diff --git a/internal/handler/conan.go b/internal/handler/conan.go
index c0476f9..7142f0d 100644
--- a/internal/handler/conan.go
+++ b/internal/handler/conan.go
@@ -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()
diff --git a/internal/handler/conda.go b/internal/handler/conda.go
index 224c25f..bee5694 100644
--- a/internal/handler/conda.go
+++ b/internal/handler/conda.go
@@ -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()
diff --git a/internal/handler/cran.go b/internal/handler/cran.go
index 2fc4fab..4a6ded8 100644
--- a/internal/handler/cran.go
+++ b/internal/handler/cran.go
@@ -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()
diff --git a/internal/handler/gem.go b/internal/handler/gem.go
index 260568a..080bcdd 100644
--- a/internal/handler/gem.go
+++ b/internal/handler/gem.go
@@ -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()
diff --git a/internal/handler/go.go b/internal/handler/go.go
index cf40aa1..c2690f9 100644
--- a/internal/handler/go.go
+++ b/internal/handler/go.go
@@ -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
diff --git a/internal/handler/handler.go b/internal/handler/handler.go
index 8dcc9c9..0ea5fbd 100644
--- a/internal/handler/handler.go
+++ b/internal/handler/handler.go
@@ -47,6 +47,13 @@ func hasDotDotSegment(path string) bool {
return false
}
+func configuredUpstreamURL(value, defaultValue string) string {
+ if value == "" {
+ value = defaultValue
+ }
+ return strings.TrimSuffix(value, "/")
+}
+
const defaultHTTPTimeout = 30 * time.Second
const artifactCopyBufferSize = 32 << 10
diff --git a/internal/handler/hex.go b/internal/handler/hex.go
index 2ff4f0f..3474181 100644
--- a/internal/handler/hex.go
+++ b/internal/handler/hex.go
@@ -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", configuredUpstreamURL(h.apiURL, hexAPIURL), name)
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, apiURL, nil)
if err != nil {
return nil, err
diff --git a/internal/handler/julia.go b/internal/handler/julia.go
index 0fed8c9..211d092 100644
--- a/internal/handler/julia.go
+++ b/internal/handler/julia.go
@@ -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()
diff --git a/internal/handler/nuget.go b/internal/handler/nuget.go
index 4785e40..e931d42 100644
--- a/internal/handler/nuget.go
+++ b/internal/handler/nuget.go
@@ -11,13 +11,15 @@ import (
)
const (
- nugetUpstream = "https://api.nuget.org"
+ 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,10 +115,10 @@ func (h *NuGetHandler) rewriteServiceIndex(body []byte) ([]byte, error) {
id, _ := rmap["@id"].(string)
rtype, _ := rmap["@type"].(string)
- // Rewrite URLs for services we proxy
+ // Rewrite URLs for services we proxy. The service type determines the
+ // local route because an upstream index may advertise a different host.
if id != "" && h.shouldRewriteService(rtype) {
- newURL := h.rewriteNuGetURL(id)
- rmap["@id"] = newURL
+ rmap["@id"] = h.rewriteNuGetURL(id, rtype)
}
}
@@ -135,23 +147,20 @@ func (h *NuGetHandler) shouldRewriteService(serviceType string) bool {
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",
+// rewriteNuGetURL rewrites a NuGet service URL based on its advertised type.
+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
}
-
- for old, new := range replacements {
- if strings.HasPrefix(origURL, old) {
- return strings.Replace(origURL, old, new, 1)
- }
- }
-
- return origURL
}
// handleRegistration proxies NuGet registration pages, applying cooldown filtering.
@@ -363,7 +372,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 configuredUpstreamURL(h.searchURL, nugetSearchUpstream) + path + "?" + r.URL.RawQuery
}
return h.upstreamURL + path
diff --git a/internal/handler/nuget_test.go b/internal/handler/nuget_test.go
index b2164e5..cf638b1 100644
--- a/internal/handler/nuget_test.go
+++ b/internal/handler/nuget_test.go
@@ -131,35 +131,41 @@ func TestNuGetRewriteURL(t *testing.T) {
}
tests := []struct {
- input string
- want string
+ 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://azuresearch-usnc.nuget.org/query",
+ "SearchQueryService",
"http://localhost:8080/nuget/query",
},
{
"https://azuresearch-usnc.nuget.org/autocomplete",
+ "SearchAutocompleteService",
"http://localhost:8080/nuget/autocomplete",
},
{
"https://example.com/unknown",
+ "SomeOtherService/1.0.0",
"https://example.com/unknown",
},
}
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)
}
}
}
diff --git a/internal/handler/pub.go b/internal/handler/pub.go
index e5ca199..fda1b1e 100644
--- a/internal/handler/pub.go
+++ b/internal/handler/pub.go
@@ -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()
diff --git a/internal/handler/pypi.go b/internal/handler/pypi.go
index 10a3a13..fbff5b0 100644
--- a/internal/handler/pypi.go
+++ b/internal/handler/pypi.go
@@ -8,18 +8,18 @@ import (
"fmt"
"io"
"net/http"
- "net/url"
"regexp"
"strings"
"time"
)
const (
- pypiUpstream = "https://pypi.org"
- minWheelParts = 5 // name + version + python + abi + platform
- minSubmatchParts = 2 // full match + first capture group
- minPyPIPathParts = 3 // hash_prefix + hash + filename
- minEggParts = 3 // name + version + python tag
+ 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
+ minEggParts = 3 // name + version + python tag
// PyPIMetadataSuffix is the PEP 658 core-metadata sidecar suffix that pip
// appends to a distribution URL when the index advertises core metadata.
@@ -33,6 +33,7 @@ const (
type PyPIHandler struct {
proxy *Proxy
upstreamURL string
+ downloadURL string
proxyURL string
}
@@ -41,10 +42,20 @@ func NewPyPIHandler(proxy *Proxy, proxyURL string) *PyPIHandler {
return &PyPIHandler{
proxy: proxy,
upstreamURL: pypiUpstream,
+ downloadURL: pypiDownloadUpstream,
proxyURL: strings.TrimSuffix(proxyURL, "/"),
}
}
+// NewPyPIHandlerWithUpstreams creates a PyPI handler with custom API and
+// package download upstreams.
+func NewPyPIHandlerWithUpstreams(proxy *Proxy, proxyURL, upstreamURL, downloadURL string) *PyPIHandler {
+ h := NewPyPIHandler(proxy, proxyURL)
+ h.upstreamURL = configuredUpstreamURL(upstreamURL, pypiUpstream)
+ h.downloadURL = configuredUpstreamURL(downloadURL, pypiDownloadUpstream)
+ return h
+}
+
// Routes returns the HTTP handler for PyPI requests.
func (h *PyPIHandler) Routes() http.Handler {
mux := http.NewServeMux()
@@ -169,9 +180,9 @@ 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/[^"]+)"`)
+ // Match href attributes pointing to packages on the configured download host.
+ downloadURL := configuredUpstreamURL(h.downloadURL, pypiDownloadUpstream)
+ re := regexp.MustCompile(`href="(` + regexp.QuoteMeta(downloadURL) + `/packages/[^"]+)"`)
return re.ReplaceAllFunc(body, func(match []byte) []byte {
submatch := re.FindSubmatch(match)
@@ -181,12 +192,7 @@ func (h *PyPIHandler) rewriteSimpleHTML(body []byte, filteredVersions map[string
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, downloadURL)
return []byte(fmt.Sprintf(`href="%s"`, newURL))
})
}
@@ -391,15 +397,9 @@ 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
+ downloadURL := configuredUpstreamURL(h.downloadURL, pypiDownloadUpstream)
+ if strings.HasPrefix(urlStr, downloadURL+"/packages/") {
+ entry["url"] = h.proxyURL + "/pypi/packages" + strings.TrimPrefix(urlStr, downloadURL)
}
}
@@ -441,7 +441,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", configuredUpstreamURL(h.downloadURL, pypiDownloadUpstream), path)
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "pypi", name, version, filename, upstreamURL)
if err != nil {
diff --git a/internal/handler/rpm.go b/internal/handler/rpm.go
index a5752ec..f4cf9e7 100644
--- a/internal/handler/rpm.go
+++ b/internal/handler/rpm.go
@@ -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 {
diff --git a/internal/handler/upstream_test.go b/internal/handler/upstream_test.go
new file mode 100644
index 0000000..de61c5a
--- /dev/null
+++ b/internal/handler/upstream_test.go
@@ -0,0 +1,125 @@
+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)
+ }
+}
+
+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(`demo`)
+ want := `demo`
+ 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)
+ }
+}
diff --git a/internal/server/server.go b/internal/server/server.go
index 71af1af..fac5c9f 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -241,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,
@@ -253,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()))
From 05ff7e99ec61a588edf332717b22245510e9eb69 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Sun, 16 Aug 2026 22:21:01 +0100
Subject: [PATCH 2/9] Simplify NuGet service rewrite and trim upstream URLs
---
internal/handler/handler.go | 2 +-
internal/handler/nuget.go | 25 +----------
internal/handler/nuget_test.go | 69 ++++++++++++++++---------------
internal/handler/upstream_test.go | 3 ++
4 files changed, 41 insertions(+), 58 deletions(-)
diff --git a/internal/handler/handler.go b/internal/handler/handler.go
index 0ea5fbd..2b88f98 100644
--- a/internal/handler/handler.go
+++ b/internal/handler/handler.go
@@ -51,7 +51,7 @@ func configuredUpstreamURL(value, defaultValue string) string {
if value == "" {
value = defaultValue
}
- return strings.TrimSuffix(value, "/")
+ return strings.TrimRight(value, "/")
}
const defaultHTTPTimeout = 30 * time.Second
diff --git a/internal/handler/nuget.go b/internal/handler/nuget.go
index e931d42..2371b79 100644
--- a/internal/handler/nuget.go
+++ b/internal/handler/nuget.go
@@ -117,7 +117,7 @@ func (h *NuGetHandler) rewriteServiceIndex(body []byte) ([]byte, error) {
// Rewrite URLs for services we proxy. The service type determines the
// local route because an upstream index may advertise a different host.
- if id != "" && h.shouldRewriteService(rtype) {
+ if id != "" {
rmap["@id"] = h.rewriteNuGetURL(id, rtype)
}
}
@@ -125,29 +125,8 @@ func (h *NuGetHandler) rewriteServiceIndex(body []byte) ([]byte, error) {
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 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":
diff --git a/internal/handler/nuget_test.go b/internal/handler/nuget_test.go
index cf638b1..4b13a63 100644
--- a/internal/handler/nuget_test.go
+++ b/internal/handler/nuget_test.go
@@ -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",
@@ -145,21 +111,56 @@ func TestNuGetRewriteURL(t *testing.T) {
"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 {
diff --git a/internal/handler/upstream_test.go b/internal/handler/upstream_test.go
index de61c5a..86ffb0a 100644
--- a/internal/handler/upstream_test.go
+++ b/internal/handler/upstream_test.go
@@ -71,6 +71,9 @@ func TestConfiguredUpstreamURL(t *testing.T) {
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) {
From 12ad4ecefc31ff9087a1528b938679e37847a721 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Mon, 17 Aug 2026 08:36:20 +0100
Subject: [PATCH 3/9] Bump github.com/git-pkgs/purl to v0.1.17 (#273)
MakePURL/MakePURLString/New now apply the same per-type normalization as
Parse (git-pkgs/purl#30), so canonicalPackagePURL no longer needs its own
Normalize call and DB writes/lookups produce canonical keys.
Existing rows written under a non-canonical purl (mixed-case pypi,
composer, etc) become cache misses on lookup and re-populate under the
canonical key on the next fetch; the old rows are left in place.
Closes #207
---
go.mod | 2 +-
go.sum | 4 ++--
internal/handler/handler.go | 4 +---
3 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/go.mod b/go.mod
index 0bb2e1b..c8bb4f6 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
github.com/git-pkgs/cooldown v0.1.1
github.com/git-pkgs/enrichment v0.6.5
github.com/git-pkgs/magic v0.2.0
- github.com/git-pkgs/purl v0.1.16
+ github.com/git-pkgs/purl v0.1.17
github.com/git-pkgs/registries v0.7.0
github.com/git-pkgs/spdx v0.3.1
github.com/git-pkgs/vers v0.3.1
diff --git a/go.sum b/go.sum
index 1ff787f..699e1cc 100644
--- a/go.sum
+++ b/go.sum
@@ -256,8 +256,8 @@ github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6
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.16 h1:VAX6tv0hhdTENbkrGMoPZbOAl1Y8U1/ZnzoCsYuNBYM=
-github.com/git-pkgs/purl v0.1.16/go.mod h1:7u7ora8tQdrkS7Auclr5v8dCJdjN4ej6AbrvYZi2b7k=
+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.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=
diff --git a/internal/handler/handler.go b/internal/handler/handler.go
index 8dcc9c9..cfbe5e0 100644
--- a/internal/handler/handler.go
+++ b/internal/handler/handler.go
@@ -61,9 +61,7 @@ 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 {
- p := purl.MakePURL(ecosystem, name, "")
- _ = p.Normalize()
- return p.String()
+ return purl.MakePURLString(ecosystem, name, "")
}
const contentTypeJSON = "application/json"
From 1a814c7e1f110d84737b9c84485f0895972dbec7 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Mon, 17 Aug 2026 09:20:11 +0100
Subject: [PATCH 4/9] Use shared integrity verification (#260)
* Use shared integrity verification
* Finish integrity migration
---
go.mod | 1 +
go.sum | 2 +
internal/database/database_test.go | 17 ++-
internal/handler/container_test.go | 5 +-
internal/handler/download_test.go | 2 +-
internal/handler/handler.go | 24 ++-
internal/handler/handler_test.go | 79 +++++++++-
internal/handler/integrity.go | 172 ++++++++-------------
internal/handler/integrity_test.go | 238 +++++++++++++++++++++--------
9 files changed, 353 insertions(+), 187 deletions(-)
diff --git a/go.mod b/go.mod
index c8bb4f6..beb444e 100644
--- a/go.mod
+++ b/go.mod
@@ -10,6 +10,7 @@ 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.7.0
diff --git a/go.sum b/go.sum
index 699e1cc..e8efd91 100644
--- a/go.sum
+++ b/go.sum
@@ -250,6 +250,8 @@ github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg
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=
diff --git a/internal/database/database_test.go b/internal/database/database_test.go
index 3e92b91..bb2b195 100644
--- a/internal/database/database_test.go
+++ b/internal/database/database_test.go
@@ -8,6 +8,11 @@ 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")
@@ -132,7 +137,7 @@ func TestVersionCRUD(t *testing.T) {
v := &Version{
PURL: "pkg:npm/lodash@4.17.21",
PackagePURL: "pkg:npm/lodash",
- Integrity: sql.NullString{String: "sha512-abc123", Valid: true},
+ Integrity: sql.NullString{String: testIntegrity, Valid: true},
}
err = db.UpsertVersion(v)
@@ -200,7 +205,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", "sha256-abc", 12345, "application/gzip")
+ err = db.MarkArtifactCached(versionPURL, "lodash-4.17.21.tgz", "/cache/npm/lodash-4.17.21.tgz", testContentHash, 12345, "application/gzip")
if err != nil {
t.Fatalf("MarkArtifactCached failed: %v", err)
}
@@ -257,7 +262,7 @@ func TestGetCachedArtifact(t *testing.T) {
}
if err := db.MarkArtifactCached(versionPURL, filename, "/cache/npm/"+filename,
- "sha256-abc", 12345, "application/gzip"); err != nil {
+ testContentHash, 12345, "application/gzip"); err != nil {
t.Fatalf("MarkArtifactCached failed: %v", err)
}
@@ -274,7 +279,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 != "sha256-abc" {
+ if cached.ContentHash.String != testContentHash {
t.Errorf("expected cached content hash, got %q", cached.ContentHash.String)
}
if cached.Size.Int64 != 12345 {
@@ -283,7 +288,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 != "sha512-abc123" {
+ if cached.Integrity.String != testIntegrity {
t.Errorf("expected cached integrity, got %q", cached.Integrity.String)
}
@@ -306,7 +311,7 @@ func seedCachedArtifactTestData(t *testing.T, db *DB, packagePURL, versionPURL,
if err := db.UpsertVersion(&Version{
PURL: versionPURL,
PackagePURL: packagePURL,
- Integrity: sql.NullString{String: "sha512-abc123", Valid: true},
+ Integrity: sql.NullString{String: testIntegrity, Valid: true},
}); err != nil {
t.Fatalf("UpsertVersion failed: %v", err)
}
diff --git a/internal/handler/container_test.go b/internal/handler/container_test.go
index 81c6807..04f00a7 100644
--- a/internal/handler/container_test.go
+++ b/internal/handler/container_test.go
@@ -482,8 +482,9 @@ 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)
}
- if got := w.Header().Get("ETag"); got != `"abc123"` {
- t.Errorf("ETag = %q, want %q", got, `"abc123"`)
+ wantETag := `"` + sha256Hex("cached blob") + `"`
+ if got := w.Header().Get("ETag"); got != wantETag {
+ t.Errorf("ETag = %q, want %q", got, wantETag)
}
if w.Body.Len() != 0 {
t.Errorf("HEAD response body length = %d, want 0", w.Body.Len())
diff --git a/internal/handler/download_test.go b/internal/handler/download_test.go
index 8192eeb..dda3e84 100644
--- a/internal/handler/download_test.go
+++ b/internal/handler/download_test.go
@@ -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: "abc123", Valid: true},
+ ContentHash: sql.NullString{String: sha256Hex(content), 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},
diff --git a/internal/handler/handler.go b/internal/handler/handler.go
index cfbe5e0..6c65682 100644
--- a/internal/handler/handler.go
+++ b/internal/handler/handler.go
@@ -194,6 +194,11 @@ 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,
@@ -225,16 +230,21 @@ func (p *Proxy) checkCache(ctx context.Context, pkgPURL, versionPURL, filename s
return nil, nil
}
- result.Reader = newVerifyingReader(reader, artifact.ContentHash.String, artifact.Integrity.String,
+ result.Reader, err = checks.wrap(reader,
func(reason string) {
p.Logger.Error("cached artifact failed integrity check",
"purl", versionPURL, "filename", filename,
"path", artifact.StoragePath, "reason", reason)
- metrics.RecordIntegrityFailure(artifact.Ecosystem)
+ metrics.RecordIntegrityFailure(purl.NormalizeEcosystem(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
}
@@ -264,6 +274,16 @@ 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)
diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go
index d8660d7..ec0e300 100644
--- a/internal/handler/handler_test.go
+++ b/internal/handler/handler_test.go
@@ -3,9 +3,7 @@ package handler
import (
"bytes"
"context"
- "crypto/sha256"
"database/sql"
- "encoding/hex"
"errors"
"io"
"log/slog"
@@ -46,8 +44,7 @@ func (s *mockStorage) Store(_ context.Context, path string, r io.Reader) (int64,
return 0, "", err
}
s.files[path] = data
- digest := sha256.Sum256(data)
- return int64(len(data)), hex.EncodeToString(digest[:]), nil
+ return int64(len(data)), sha256Hex(string(data)), nil
}
func (s *mockStorage) Open(_ context.Context, path string) (io.ReadCloser, error) {
@@ -182,7 +179,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: "abc123", Valid: true},
+ ContentHash: sql.NullString{String: sha256Hex(content), 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},
@@ -271,8 +268,74 @@ 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 != "abc123" {
- t.Errorf("got hash %q, want %q", result.Hash, "abc123")
+ 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")
}
}
@@ -307,7 +370,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: "hash", Valid: true},
+ ContentHash: sql.NullString{String: sha256Hex("missing content"), 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},
diff --git a/internal/handler/integrity.go b/internal/handler/integrity.go
index bb29a21..07963b9 100644
--- a/internal/handler/integrity.go
+++ b/internal/handler/integrity.go
@@ -1,140 +1,100 @@
package handler
import (
- "crypto/sha256"
- "crypto/sha512"
- "crypto/subtle"
- "encoding/base64"
- "encoding/hex"
"fmt"
- "hash"
"io"
- "strings"
+
+ "github.com/git-pkgs/integrity"
)
-// 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
+type integrityChecks struct {
+ contentHash integrity.SRI
+ native integrity.SRI
+ algorithms []integrity.Algorithm
+}
+
+func newIntegrityChecks(contentHash, native string) (integrityChecks, error) {
+ checks := integrityChecks{}
+
+ if contentHash != "" {
+ digest, err := integrity.ParseHex(integrity.SHA256, contentHash)
+ if err != nil {
+ return integrityChecks{}, fmt.Errorf("parse content_hash: %w", err)
+ }
+ checks.contentHash = integrity.SRI{digest}
+ checks.algorithms = append(checks.algorithms, integrity.SHA256)
}
- if i := strings.IndexByte(s, ' '); i >= 0 {
- s = s[:i]
+
+ 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())
+ }
}
- algo, b64, found := strings.Cut(s, "-")
- if !found {
- return "", nil, false
+
+ return checks, nil
+}
+
+func (c integrityChecks) wrap(source io.ReadCloser, onMismatch func(string)) (io.ReadCloser, error) {
+ if len(c.algorithms) == 0 {
+ return source, nil
}
- d, err := base64.StdEncoding.DecodeString(b64)
+ reader, err := integrity.NewReader(source, c.algorithms...)
if err != nil {
- return "", nil, false
- }
- switch algo {
- case "sha256", "sha384", "sha512":
- return algo, d, true
- default:
- return "", nil, false
+ return nil, fmt.Errorf("create integrity reader: %w", err)
}
+ return &verifyingReader{
+ source: source,
+ reader: reader,
+ checks: c,
+ onMismatch: onMismatch,
+ }, 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
-}
-
-// 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.
+// verifyingReader forwards Close to its source and reports completed digest
+// mismatches after its shared integrity reader observes EOF.
type verifyingReader struct {
- r io.ReadCloser
- sha256 hash.Hash
- wantSHA256 string
- sri hash.Hash
- sriAlgo string
- wantSRI []byte
+ source io.ReadCloser
+ reader *integrity.Reader
+ checks integrityChecks
onMismatch func(reason string)
- eof bool
verified bool
}
-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])
- }
- }
+func (r *verifyingReader) Read(p []byte) (int, error) {
+ n, err := r.reader.Read(p)
if err == io.EOF {
- v.eof = true
- v.verify()
+ r.verify()
}
return n, err
}
-func (v *verifyingReader) Close() error {
- if v.eof {
- v.verify()
- }
- return v.r.Close()
+func (r *verifyingReader) Close() error {
+ return r.source.Close()
}
-func (v *verifyingReader) verify() {
- if v.verified {
+func (r *verifyingReader) verify() {
+ if r.verified {
+ return
+ }
+ r.verified = true
+ result := r.reader.Result()
+ if !result.Complete {
return
}
- v.verified = true
- 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.contentHash) > 0 {
+ if err := result.Verify(r.checks.contentHash); err != nil {
+ r.onMismatch("content_hash: " + 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)))
+ if len(r.checks.native) > 0 {
+ if err := result.Verify(r.checks.native); err != nil {
+ r.onMismatch("integrity: " + err.Error())
}
}
}
diff --git a/internal/handler/integrity_test.go b/internal/handler/integrity_test.go
index 93c448c..95992c0 100644
--- a/internal/handler/integrity_test.go
+++ b/internal/handler/integrity_test.go
@@ -5,6 +5,7 @@ import (
"crypto/sha512"
"encoding/base64"
"encoding/hex"
+ "errors"
"io"
"strings"
"testing"
@@ -15,42 +16,68 @@ 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 TestParseSRI(t *testing.T) {
- tests := []struct {
- name string
- input string
- algo string
- ok bool
- }{
- {"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},
+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
+}
- 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")
+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) {
+ tests := []struct {
+ name string
+ contentHash string
+ native string
+ }{
+ {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"},
+ }
+ 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")
}
})
}
@@ -67,69 +94,156 @@ func TestVerifyingReader(t *testing.T) {
sri string
wantCalls int
}{
- {"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},
+ {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"},
}
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
var calls []string
- r := newVerifyingReader(io.NopCloser(strings.NewReader(data)), tt.hash, tt.sri,
+ reader := wrapIntegrityReader(t, io.NopCloser(strings.NewReader(data)), test.hash, test.sri,
func(reason string) { calls = append(calls, reason) })
- got, err := io.ReadAll(r)
+ got, err := io.ReadAll(reader)
if err != nil {
t.Fatalf("ReadAll: %v", err)
}
if string(got) != data {
t.Errorf("data corrupted: got %q", got)
}
- if err := r.Close(); err != nil {
+ if err := reader.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
-
- if len(calls) != tt.wantCalls {
- t.Errorf("onMismatch called %d times, want %d: %v", len(calls), tt.wantCalls, calls)
+ if len(calls) != test.wantCalls {
+ t.Errorf("onMismatch called %d times, want %d: %v", len(calls), test.wantCalls, calls)
}
})
}
}
-func TestVerifyingReaderPassthrough(t *testing.T) {
- 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")
+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 TestVerifyingReaderPartialRead(t *testing.T) {
- var calls int
- r := newVerifyingReader(io.NopCloser(strings.NewReader("hello world")),
- sha256Hex("hello world"), "", func(string) { calls++ })
+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)
+ }
+}
- buf := make([]byte, 5)
- _, _ = r.Read(buf)
- _ = r.Close()
+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")
+ }
+}
+
+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++ })
+
+ buffer := make([]byte, 5)
+ _, _ = reader.Read(buffer)
+ _ = reader.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
- r := newVerifyingReader(io.NopCloser(strings.NewReader("x")), sha256Hex("y"), "",
- func(string) { calls++ })
- _, _ = io.ReadAll(r)
- _ = r.Close()
- _ = r.Close()
+ reader := wrapIntegrityReader(t, io.NopCloser(strings.NewReader("x")), sha256Hex("y"), "", func(string) { calls++ })
+ _, _ = io.ReadAll(reader)
+ _ = reader.Close()
+ _ = reader.Close()
if calls != 1 {
t.Errorf("onMismatch called %d times, want 1", calls)
}
From 6ed0c75ccd41adbc58a361e8da836c23c5b13275 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Mon, 17 Aug 2026 09:27:22 +0100
Subject: [PATCH 5/9] Drop redundant upstream defaulting and cache PyPI href
regex
---
internal/handler/hex.go | 2 +-
internal/handler/nuget.go | 2 +-
internal/handler/nuget_test.go | 3 +++
internal/handler/pypi.go | 42 +++++++++++++++-------------------
4 files changed, 24 insertions(+), 25 deletions(-)
diff --git a/internal/handler/hex.go b/internal/handler/hex.go
index 3474181..05416c6 100644
--- a/internal/handler/hex.go
+++ b/internal/handler/hex.go
@@ -208,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", configuredUpstreamURL(h.apiURL, 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
diff --git a/internal/handler/nuget.go b/internal/handler/nuget.go
index 2371b79..0fb139b 100644
--- a/internal/handler/nuget.go
+++ b/internal/handler/nuget.go
@@ -351,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 configuredUpstreamURL(h.searchURL, nugetSearchUpstream) + path + "?" + r.URL.RawQuery
+ return h.searchURL + path + "?" + r.URL.RawQuery
}
return h.upstreamURL + path
diff --git a/internal/handler/nuget_test.go b/internal/handler/nuget_test.go
index 4b13a63..09efe95 100644
--- a/internal/handler/nuget_test.go
+++ b/internal/handler/nuget_test.go
@@ -465,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 {
@@ -743,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
@@ -757,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)
diff --git a/internal/handler/pypi.go b/internal/handler/pypi.go
index fbff5b0..08ab852 100644
--- a/internal/handler/pypi.go
+++ b/internal/handler/pypi.go
@@ -31,28 +31,28 @@ const (
// PyPIHandler handles PyPI registry protocol requests.
type PyPIHandler struct {
- proxy *Proxy
- upstreamURL string
- downloadURL string
- proxyURL string
+ 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{
- proxy: proxy,
- upstreamURL: pypiUpstream,
- downloadURL: pypiDownloadUpstream,
- proxyURL: strings.TrimSuffix(proxyURL, "/"),
- }
+ 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 := NewPyPIHandler(proxy, proxyURL)
- h.upstreamURL = configuredUpstreamURL(upstreamURL, pypiUpstream)
- h.downloadURL = configuredUpstreamURL(downloadURL, pypiDownloadUpstream)
+ h := &PyPIHandler{
+ proxy: proxy,
+ upstreamURL: configuredUpstreamURL(upstreamURL, pypiUpstream),
+ downloadURL: configuredUpstreamURL(downloadURL, pypiDownloadUpstream),
+ proxyURL: strings.TrimSuffix(proxyURL, "/"),
+ }
+ h.downloadHrefRe = regexp.MustCompile(`href="(` + regexp.QuoteMeta(h.downloadURL) + `/packages/[^"]+)"`)
return h
}
@@ -181,18 +181,15 @@ func (h *PyPIHandler) rewriteSimpleHTML(body []byte, filteredVersions map[string
}
// Match href attributes pointing to packages on the configured download host.
- downloadURL := configuredUpstreamURL(h.downloadURL, pypiDownloadUpstream)
- re := regexp.MustCompile(`href="(` + regexp.QuoteMeta(downloadURL) + `/packages/[^"]+)"`)
-
- return re.ReplaceAllFunc(body, func(match []byte) []byte {
- submatch := re.FindSubmatch(match)
+ return h.downloadHrefRe.ReplaceAllFunc(body, func(match []byte) []byte {
+ submatch := h.downloadHrefRe.FindSubmatch(match)
if len(submatch) < minSubmatchParts {
return match
}
origURL := string(submatch[1])
- newURL := h.proxyURL + "/pypi/packages" + strings.TrimPrefix(origURL, downloadURL)
+ newURL := h.proxyURL + "/pypi/packages" + strings.TrimPrefix(origURL, h.downloadURL)
return []byte(fmt.Sprintf(`href="%s"`, newURL))
})
}
@@ -397,9 +394,8 @@ func (h *PyPIHandler) rewriteURLEntry(entry map[string]any) {
return
}
- downloadURL := configuredUpstreamURL(h.downloadURL, pypiDownloadUpstream)
- if strings.HasPrefix(urlStr, downloadURL+"/packages/") {
- entry["url"] = h.proxyURL + "/pypi/packages" + strings.TrimPrefix(urlStr, downloadURL)
+ 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("%s/%s", configuredUpstreamURL(h.downloadURL, pypiDownloadUpstream), path)
+ upstreamURL := fmt.Sprintf("%s/%s", h.downloadURL, path)
result, err := h.proxy.GetOrFetchArtifactFromURL(r.Context(), "pypi", name, version, filename, upstreamURL)
if err != nil {
From 5cdbc89ed490826531c03ab6ee26b76abe554962 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Thu, 20 Aug 2026 08:29:51 +0100
Subject: [PATCH 6/9] Remove supporting-module-issues.md
---
supporting-module-issues.md | 296 ------------------------------------
1 file changed, 296 deletions(-)
delete mode 100644 supporting-module-issues.md
diff --git a/supporting-module-issues.md b/supporting-module-issues.md
deleted file mode 100644
index 299d9af..0000000
--- a/supporting-module-issues.md
+++ /dev/null
@@ -1,296 +0,0 @@
-# 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.
From 17446b419fa716e5e8b226421adcd3b7f0cf9944 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Thu, 20 Aug 2026 15:38:38 +0100
Subject: [PATCH 7/9] Bump registries to v0.8.1 (#277)
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index beb444e..066399f 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
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.7.0
+ github.com/git-pkgs/registries v0.8.1
github.com/git-pkgs/spdx v0.3.1
github.com/git-pkgs/vers v0.3.1
github.com/git-pkgs/vulns v0.2.2
diff --git a/go.sum b/go.sum
index e8efd91..33dff42 100644
--- a/go.sum
+++ b/go.sum
@@ -260,8 +260,8 @@ 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.7.0 h1:+LbOOMHbvjmXGfsi88hcGH+SfTXYsXA3UY5KYI5mB7s=
-github.com/git-pkgs/registries v0.7.0/go.mod h1:VCD4q+ZW0fInopzseg9rAmBEL553R2JQe60UHXtv26w=
+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/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=
From c1f09e7921e8438fb7a11e5fa09ae4672f180713 Mon Sep 17 00:00:00 2001
From: Ching Wei Kang <164879897+WilliamK112@users.noreply.github.com>
Date: Fri, 21 Aug 2026 04:25:02 -0400
Subject: [PATCH 8/9] Show build information in web UI (#257)
* Show build information in web UI
Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com>
* Fix footer build info shadowed by page Version fields
Shared footer templates were reading .Version and .Commit, which resolve
to package data on VersionShowData and BrowseSourceData. Point the footer
at Layout.BuildInfo and cover both pages so the proxy version stays visible.
Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com>
Co-authored-by: Cursor
* Fix Layout build info field promotion
---------
Signed-off-by: WilliamK112 <164879897+WilliamK112@users.noreply.github.com>
Co-authored-by: Cursor
---
cmd/proxy/main.go | 5 +-
internal/server/browse_test.go | 4 ++
internal/server/layout.go | 15 +++--
internal/server/server.go | 4 +-
internal/server/server_test.go | 15 ++++-
internal/server/templates/layout/footer.html | 5 ++
internal/server/templates_test.go | 58 ++++++++++++++++++++
7 files changed, 98 insertions(+), 8 deletions(-)
diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go
index 9054eee..c5549ad 100644
--- a/cmd/proxy/main.go
+++ b/cmd/proxy/main.go
@@ -275,7 +275,10 @@ func runServe() {
logger := setupLogger(cfg.Log.Level, cfg.Log.Format)
// Create and start server
- srv, err := server.New(cfg, logger)
+ srv, err := server.New(cfg, logger, server.BuildInfo{
+ Version: Version,
+ Commit: Commit,
+ })
if err != nil {
logger.Error("failed to create server", "error", err)
os.Exit(1)
diff --git a/internal/server/browse_test.go b/internal/server/browse_test.go
index 3cc37c8..f4f2f9a 100644
--- a/internal/server/browse_test.go
+++ b/internal/server/browse_test.go
@@ -430,6 +430,10 @@ 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")
diff --git a/internal/server/layout.go b/internal/server/layout.go
index ef39858..2da9469 100644
--- a/internal/server/layout.go
+++ b/internal/server/layout.go
@@ -2,17 +2,24 @@ package server
import "net/http"
-// 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.
+// 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.
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,
}
diff --git a/internal/server/server.go b/internal/server/server.go
index 71af1af..bb964e8 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -92,6 +92,7 @@ type Server struct {
db *database.DB
storage storage.Storage
logger *slog.Logger
+ buildInfo BuildInfo
http *http.Server
templates *Templates
cancel context.CancelFunc
@@ -100,7 +101,7 @@ type Server struct {
}
// New creates a new Server with the given configuration.
-func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
+func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, error) {
var activityLog *accesslog.Logger
if cfg.AccessLog.Path != "" {
var err error
@@ -169,6 +170,7 @@ func New(cfg *config.Config, logger *slog.Logger) (*Server, error) {
db: db,
storage: store,
logger: logger,
+ buildInfo: buildInfo,
templates: &Templates{},
healthCache: hc,
accessLog: activityLog,
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index a870106..03b253f 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -104,6 +104,7 @@ func newTestServer(t *testing.T) *testServer {
db: db,
storage: store,
logger: logger,
+ buildInfo: BuildInfo{Version: "test-version", Commit: "test-commit"},
templates: &Templates{},
healthCache: hc,
}
@@ -313,6 +314,9 @@ 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")
}
@@ -598,6 +602,9 @@ 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) {
@@ -1327,10 +1334,14 @@ func TestNewServer_StorageConnectivityCheck(t *testing.T) {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- srv, err := New(cfg, logger)
+ buildInfo := BuildInfo{Version: "test-version", Commit: "test-commit"}
+ srv, err := New(cfg, logger, buildInfo)
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.
@@ -1354,7 +1365,7 @@ func TestNewServer_InvalidAccessLogFailsBeforeDatabaseInit(t *testing.T) {
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
- if _, err := New(cfg, logger); err == nil {
+ if _, err := New(cfg, logger, BuildInfo{}); 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)
diff --git a/internal/server/templates/layout/footer.html b/internal/server/templates/layout/footer.html
index 5aa970d..33daddf 100644
--- a/internal/server/templates/layout/footer.html
+++ b/internal/server/templates/layout/footer.html
@@ -12,6 +12,11 @@
github.com/git-pkgs/proxy
+ {{if .BuildInfo.Version}}
+
+ proxy {{.BuildInfo.Version}}{{if .BuildInfo.Commit}} ({{.BuildInfo.Commit}}){{end}}
+
+ {{end}}
Resources
diff --git a/internal/server/templates_test.go b/internal/server/templates_test.go
index e9a4967..158278b 100644
--- a/internal/server/templates_test.go
+++ b/internal/server/templates_test.go
@@ -186,6 +186,64 @@ 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{}
From 272e6d904095d319f481ed0247c9e75238074fd0 Mon Sep 17 00:00:00 2001
From: Andrew Nesbitt
Date: Fri, 21 Aug 2026 09:26:27 +0100
Subject: [PATCH 9/9] Lint and dead-code cleanup (#279)
* Bump go tool golangci-lint to v2.13.1
The .golangci.yml goconst.ignore-tests setting was added in v2.12.0
(golangci/golangci-lint#6480). On the previously pinned v2.10.1,
config verify fails with "additional properties 'ignore-tests' not
allowed" and the setting is silently ignored at run time, so goconst
counts test-file literals toward min-occurrences.
* Apply gofmt and CutSuffix simplification
- gofmt -w internal/server/health_test.go
- Replace HasSuffix+TrimSuffix with CutSuffix in ParseSize
* Remove dead code and migrate tests off legacy Filesystem storage
Migrate the three test call sites of storage.NewFilesystem to
storage.OpenBucket("file://...") and drop the deprecated
StorageConfig.Path field from test configs, then delete code that
deadcode reports as unreachable from cmd/proxy:
- internal/storage/filesystem.go and its tests
- storage.HashingReader
- enrichment.Service.BulkCheckVulnerabilities and NormalizeLicense
- server.ActiveRequestsMiddleware (no-op body; the real tracking
is the inline r.Use at server.go:226)
- mirror.RegistrySource (unimplemented stub)
metrics.UpdateCircuitBreakerState and RecordCircuitBreakerTrip are
kept because #275 wires them.
Update the CONTRIBUTING.md storage section to reflect blob.go.
---
CONTRIBUTING.md | 4 +-
go.mod | 156 ++++++-----
go.sum | 367 ++++++++++++-------------
internal/config/config.go | 3 +-
internal/enrichment/enrichment.go | 50 ----
internal/enrichment/enrichment_test.go | 22 --
internal/mirror/registry.go | 16 --
internal/mirror/registry_test.go | 46 ----
internal/server/eviction_test.go | 8 +-
internal/server/health_test.go | 6 +-
internal/server/middleware.go | 14 -
internal/server/middleware_test.go | 30 --
internal/server/server_test.go | 7 +-
internal/storage/filesystem.go | 277 -------------------
internal/storage/filesystem_test.go | 331 ----------------------
internal/storage/storage.go | 41 ---
internal/storage/storage_test.go | 25 --
17 files changed, 272 insertions(+), 1131 deletions(-)
delete mode 100644 internal/mirror/registry.go
delete mode 100644 internal/mirror/registry_test.go
delete mode 100644 internal/storage/filesystem.go
delete mode 100644 internal/storage/filesystem_test.go
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 88ad1cb..68a6acf 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -39,7 +39,7 @@ proxy/
│ │ └── queries.go # CRUD operations
│ ├── storage/ # Artifact file storage
│ │ ├── storage.go # Storage interface
-│ │ └── filesystem.go # Local filesystem impl
+│ │ └── blob.go # gocloud.dev/blob backends (file, S3, Azure)
│ ├── 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. Currently implements local filesystem storage. Designed to allow future backends (S3, GCS).
+Artifact file storage abstraction backed by `gocloud.dev/blob`. Supports local filesystem (`file://`), S3 (`s3://`), and Azure (`azblob://`) URLs.
Interface:
```go
diff --git a/go.mod b/go.mod
index 066399f..d95ee13 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/git-pkgs/proxy
-go 1.25.6
+go 1.26.0
toolchain go1.26.6
@@ -32,45 +32,48 @@ require (
)
require (
- 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
+ 4d63.com/gocheckcompilerdirectives v1.4.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
- cloud.google.com/go/auth v0.18.2 // indirect
+ charm.land/lipgloss/v2 v2.0.6 // indirect
+ cloud.google.com/go/auth v0.21.0 // 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/golib v0.6.0 // indirect
+ dev.gaijin.team/go/exhaustruct/v5 v5.0.3 // indirect
+ dev.gaijin.team/go/golib v0.8.1 // indirect
github.com/4meepo/tagalign v1.4.3 // indirect
- github.com/Abirdcfly/dupword v0.1.7 // indirect
+ github.com/Abirdcfly/dupword v0.1.8 // indirect
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
- github.com/AlwxSin/noinlineerr v1.0.5 // indirect
- github.com/Antonboom/errname v1.1.1 // indirect
- github.com/Antonboom/nilnil v1.1.1 // indirect
+ github.com/AlwxSin/noinlineerr v1.0.6 // indirect
+ github.com/Antonboom/errname v1.1.2 // indirect
+ github.com/Antonboom/nilnil v1.1.2 // indirect
github.com/Antonboom/testifylint v1.6.4 // indirect
github.com/Azure/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.4.0 // indirect
- github.com/MirrexOne/unqueryvet v1.5.3 // indirect
+ github.com/Masterminds/semver/v3 v3.5.0 // indirect
+ github.com/MirrexOne/unqueryvet v1.5.4 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
- github.com/alecthomas/chroma/v2 v2.23.1 // indirect
+ github.com/alecthomas/chroma/v2 v2.27.0 // 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.0.2 // indirect
+ github.com/alexkohler/prealloc v1.1.0 // indirect
github.com/alfatraining/structtag v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.2.0 // indirect
github.com/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.0 // indirect
- github.com/ashanbrown/makezero/v2 v2.1.0 // indirect
+ github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
+ github.com/ashanbrown/makezero/v2 v2.2.1 // 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
@@ -90,48 +93,49 @@ 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.6.0 // indirect
+ github.com/bombsimon/wsl/v5 v5.9.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
- github.com/butuzov/ireturn v0.4.0 // indirect
- github.com/butuzov/mirror v1.3.0 // indirect
+ github.com/butuzov/ireturn v0.4.1 // indirect
+ github.com/butuzov/mirror v1.3.3 // indirect
github.com/catenacyber/perfsprint v0.10.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/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.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/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/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 v1.11.5 // indirect
+ github.com/dlclark/regexp2/v2 v2.2.1 // 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.18.0 // indirect
+ github.com/fatih/color v1.19.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
- github.com/firefart/nonamedreturns v1.0.6 // indirect
+ github.com/firefart/nonamedreturns v1.0.8 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
- github.com/ghostiam/protogetter v0.3.20 // indirect
+ github.com/ghostiam/protogetter v0.3.21 // 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.3 // indirect
+ github.com/go-critic/go-critic v0.14.4 // 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
@@ -152,83 +156,82 @@ 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-20250308024227-f665c8d69b32 // indirect
+ github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
github.com/golangci/go-printf-func-name v0.1.1 // indirect
- github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
- github.com/golangci/golangci-lint/v2 v2.10.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/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.14 // indirect
- github.com/googleapis/gax-go/v2 v2.19.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.18 // indirect
+ github.com/googleapis/gax-go/v2 v2.23.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.8.0 // indirect
+ github.com/hashicorp/go-version v1.9.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.8.2 // indirect
- github.com/jingyugao/rowserrcheck v1.1.1 // indirect
+ github.com/jgautheron/goconst v1.11.0 // 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.9.0 // indirect
+ github.com/kisielk/errcheck v1.20.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.15 // indirect
github.com/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.8.0 // indirect
+ github.com/ldez/gomoddirectives v0.9.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.2.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.4.1 // 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.5.0 // indirect
+ github.com/manuelarte/funcorder v0.6.0 // indirect
github.com/maratori/testableexamples v1.0.1 // indirect
github.com/maratori/testpackage v1.1.2 // indirect
github.com/matoous/godox v1.1.0 // indirect
- github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
- github.com/mattn/go-runewidth v0.0.16 // indirect
- github.com/mgechev/revive v1.14.0 // indirect
+ github.com/mattn/go-runewidth v0.0.24 // indirect
+ github.com/mgechev/revive v1.15.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
- github.com/muesli/termenv v0.16.0 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
- github.com/nunnatsa/ginkgolinter v0.23.0 // indirect
+ github.com/nunnatsa/ginkgolinter v0.24.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.2.4 // indirect
+ github.com/pelletier/go-toml/v2 v2.4.3 // 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
@@ -236,24 +239,25 @@ 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.2.0 // indirect
+ github.com/raeperd/recvcheck v0.3.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.14.1 // indirect
+ github.com/rogpeppe/go-internal v1.16.0 // 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/ryanrolds/sqlclosecheck v0.5.1 // indirect
+ github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect
+ github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
- github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
- github.com/securego/gosec/v2 v2.23.0 // indirect
- github.com/sirupsen/logrus v1.9.4 // indirect
+ github.com/securego/gosec/v2 v2.28.0 // indirect
+ github.com/sirupsen/logrus v1.10.1 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
- github.com/sonatard/noctx v0.4.0 // indirect
- github.com/sourcegraph/go-diff v0.7.0 // indirect
+ github.com/sonatard/noctx v0.5.1 // indirect
+ github.com/sourcegraph/go-diff v0.8.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
@@ -262,11 +266,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.2 // indirect
- github.com/stretchr/testify v1.11.1 // indirect
+ github.com/stretchr/objx v0.5.3 // indirect
+ github.com/stretchr/testify v1.12.1 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
- github.com/tetafro/godot v1.5.4 // indirect
- github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
+ github.com/tetafro/godot v1.5.6 // indirect
+ github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // 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
@@ -274,8 +278,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.0 // indirect
- github.com/uudashr/iface v1.4.1 // indirect
+ github.com/uudashr/gocognit v1.2.1 // indirect
+ github.com/uudashr/iface v1.5.0 // 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
@@ -283,9 +287,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.11.1 // indirect
+ go-simpler.org/sloglint v0.12.0 // indirect
go.augendre.info/arangolint v0.4.0 // indirect
- go.augendre.info/fatcontext v0.9.0 // indirect
+ go.augendre.info/fatcontext v0.10.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
@@ -295,28 +299,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.4 // indirect
- golang.org/x/crypto v0.54.0 // indirect
+ go.yaml.in/yaml/v3 v3.0.5 // indirect
+ golang.org/x/crypto v0.55.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // 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/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/oauth2 v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
- golang.org/x/text v0.40.0 // indirect
- golang.org/x/tools v0.47.0 // indirect
+ golang.org/x/text v0.41.0 // indirect
+ golang.org/x/tools v0.49.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
- google.golang.org/api v0.272.0 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
+ google.golang.org/api v0.288.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // 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.7.0 // indirect
+ honnef.co/go/tools v0.8.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.9.2 // indirect
- mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
+ mvdan.cc/gofumpt v0.11.0 // indirect
+ mvdan.cc/unparam v0.0.0-20260818115549-3f964bcb5673 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
diff --git a/go.sum b/go.sum
index 33dff42..e2fedc7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,15 @@
-4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A=
-4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
+4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY=
+4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ=
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
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.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
-cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
+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/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=
@@ -24,23 +26,25 @@ 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/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo=
-dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE=
+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=
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.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ=
-github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4=
+github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
+github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY=
-github.com/AlwxSin/noinlineerr v1.0.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/AlwxSin/noinlineerr v1.0.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8=
+github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc=
+github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ=
+github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI=
+github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY=
+github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA=
github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ=
github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
@@ -62,6 +66,8 @@ 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=
@@ -74,10 +80,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.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/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
+github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ=
+github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4=
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@@ -87,16 +93,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.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
-github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
+github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
+github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/go-check-sumtype v0.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.0.2 h1:MPo8cIkGkZytq7WNH9UHv3DIX1mPz1RatPXnZb0zHWQ=
-github.com/alexkohler/prealloc v1.0.2/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
+github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M=
+github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig=
github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc=
github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus=
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
@@ -107,10 +113,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.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/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/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=
@@ -149,8 +155,6 @@ 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=
@@ -160,18 +164,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.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8=
-github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU=
+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/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.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/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I=
+github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4=
+github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA=
+github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w=
github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ=
github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc=
github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc=
@@ -182,18 +186,24 @@ 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.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/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/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=
@@ -209,12 +219,15 @@ 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.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
-github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+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/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=
@@ -228,22 +241,22 @@ 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.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
-github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
+github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
-github.com/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/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/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.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
-github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
+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/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=
@@ -272,8 +285,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.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
-github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
+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-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=
@@ -291,8 +304,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.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
-github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
+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-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=
@@ -333,14 +346,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-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw=
-github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
+github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
-github.com/golangci/gofmt v0.0.0-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/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/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=
@@ -349,6 +362,8 @@ 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=
@@ -372,10 +387,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.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/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/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=
@@ -395,8 +410,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.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
-github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
+github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -405,10 +420,8 @@ 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.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/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk=
+github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc=
github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8=
github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
@@ -422,8 +435,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.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M=
-github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
+github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI=
+github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
@@ -445,8 +458,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.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk=
-github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q=
+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/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=
@@ -460,8 +473,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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
+github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE=
github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
@@ -473,8 +486,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.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8=
-github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA=
+github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0=
+github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g=
github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8=
github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ=
github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs=
@@ -483,24 +496,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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
+github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
-github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
-github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+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-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.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc=
-github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo=
+github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q=
+github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
-github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
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=
@@ -512,16 +525,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.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8=
-github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4=
+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/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.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/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/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=
@@ -535,16 +548,17 @@ 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.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
-github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+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/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=
@@ -563,15 +577,14 @@ 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.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI=
-github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU=
+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/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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
-github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+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/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=
@@ -581,31 +594,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/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
-github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ=
+github.com/ryancurrah/gomodguard/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/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.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
-github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28=
+github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw=
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
-github.com/securego/gosec/v2 v2.23.0 h1:h4TtF64qFzvnkqvsHC/knT7YC5fqyOCItlVR8+ptEBo=
-github.com/securego/gosec/v2 v2.23.0/go.mod h1:qRHEgXLFuYUDkI2T7W7NJAmOkxVhkR0x9xyHOIcMNZ0=
+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/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.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
-github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
+github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
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.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/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/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=
@@ -630,14 +643,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.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
-github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
+github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-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/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
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=
@@ -648,10 +661,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.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/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/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=
@@ -666,10 +679,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.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/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/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=
@@ -689,7 +702,6 @@ 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=
@@ -698,20 +710,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.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s=
-go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ=
+go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4=
+go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I=
go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50=
go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA=
-go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE=
-go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw=
+go.augendre.info/fatcontext v0.10.0 h1:HhFopmivh8U1+AU7f0kuwUeg2eiIns7YsGQOMHwSJ90=
+go.augendre.info/fatcontext v0.10.0/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w=
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.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/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/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=
@@ -732,51 +744,43 @@ 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.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/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
+golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
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-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk=
-golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
+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/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.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/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
+golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
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.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/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
+golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
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=
@@ -786,8 +790,6 @@ 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=
@@ -796,10 +798,8 @@ 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,27 +807,19 @@ 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.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/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
+golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
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=
@@ -836,14 +828,11 @@ 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.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 v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
+golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
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=
@@ -856,14 +845,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.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/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/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=
@@ -883,8 +872,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.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
-honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
+honnef.co/go/tools v0.8.0 h1:UacpzPr7D6i5BAjTkA7sNVcx4kIbhAZcQ4zYtKiXx68=
+honnef.co/go/tools v0.8.0/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks=
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=
@@ -913,9 +902,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.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=
+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=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
diff --git a/internal/config/config.go b/internal/config/config.go
index fdf890d..a3dfbc6 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -928,8 +928,7 @@ func ParseSize(s string) (int64, error) {
}
for _, s2 := range suffixes {
- if strings.HasSuffix(s, s2.suffix) {
- numStr := strings.TrimSuffix(s, s2.suffix)
+ if numStr, ok := strings.CutSuffix(s, s2.suffix); ok {
num, err := strconv.ParseFloat(numStr, 64)
if err != nil {
return 0, fmt.Errorf("invalid number %q", numStr)
diff --git a/internal/enrichment/enrichment.go b/internal/enrichment/enrichment.go
index 247dd2b..6b09db9 100644
--- a/internal/enrichment/enrichment.go
+++ b/internal/enrichment/enrichment.go
@@ -201,43 +201,6 @@ 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 == "" {
@@ -288,19 +251,6 @@ 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
diff --git a/internal/enrichment/enrichment_test.go b/internal/enrichment/enrichment_test.go
index aa9a16e..e6a6dde 100644
--- a/internal/enrichment/enrichment_test.go
+++ b/internal/enrichment/enrichment_test.go
@@ -74,25 +74,3 @@ 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)
- }
- }
-}
diff --git a/internal/mirror/registry.go b/internal/mirror/registry.go
deleted file mode 100644
index 6b2c449..0000000
--- a/internal/mirror/registry.go
+++ /dev/null
@@ -1,16 +0,0 @@
-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)
-}
diff --git a/internal/mirror/registry_test.go b/internal/mirror/registry_test.go
deleted file mode 100644
index 363bfea..0000000
--- a/internal/mirror/registry_test.go
+++ /dev/null
@@ -1,46 +0,0 @@
-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")
- }
-}
diff --git a/internal/server/eviction_test.go b/internal/server/eviction_test.go
index 9fa9e6b..80badbe 100644
--- a/internal/server/eviction_test.go
+++ b/internal/server/eviction_test.go
@@ -15,7 +15,7 @@ import (
"github.com/git-pkgs/proxy/internal/storage"
)
-func setupEvictionTest(t *testing.T) (*database.DB, *storage.Filesystem) {
+func setupEvictionTest(t *testing.T) (*database.DB, *storage.Blob) {
t.Helper()
tempDir := t.TempDir()
@@ -27,7 +27,7 @@ func setupEvictionTest(t *testing.T) (*database.DB, *storage.Filesystem) {
t.Fatalf("failed to create database: %v", err)
}
- store, err := storage.NewFilesystem(storagePath)
+ store, err := storage.OpenBucket(context.Background(), "file://"+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.NewFilesystem(storagePath)
+ store, err := storage.OpenBucket(context.Background(), "file://"+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{Path: storagePath, MaxSize: ""},
+ Storage: config.StorageConfig{URL: "file://" + storagePath, MaxSize: ""},
Database: config.DatabaseConfig{
Driver: "sqlite",
Path: dbPath,
diff --git a/internal/server/health_test.go b/internal/server/health_test.go
index c0f70c9..3b7eae8 100644
--- a/internal/server/health_test.go
+++ b/internal/server/health_test.go
@@ -32,7 +32,7 @@ type fakeStorage struct {
// Failure injection.
storeErr error
openErr error
- readErr error // returned by the io.ReadCloser.Read after partial bytes
+ readErr error // returned by the io.ReadCloser.Read after partial bytes
deleteErr error
// Misbehavior knobs.
@@ -132,8 +132,8 @@ func (f *fakeStorage) SignedURL(ctx context.Context, path string, expiry time.Du
return "", storage.ErrSignedURLUnsupported
}
func (f *fakeStorage) UsedSpace(ctx context.Context) (int64, error) { return 0, nil }
-func (f *fakeStorage) URL() string { return "fake://" }
-func (f *fakeStorage) Close() error { return nil }
+func (f *fakeStorage) URL() string { return "fake://" }
+func (f *fakeStorage) Close() error { return nil }
// --- Tests follow. First test: happy path ---
diff --git a/internal/server/middleware.go b/internal/server/middleware.go
index 52f9b91..b6d483f 100644
--- a/internal/server/middleware.go
+++ b/internal/server/middleware.go
@@ -92,17 +92,3 @@ 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)
- })
-}
diff --git a/internal/server/middleware_test.go b/internal/server/middleware_test.go
index eb81881..38905b2 100644
--- a/internal/server/middleware_test.go
+++ b/internal/server/middleware_test.go
@@ -73,36 +73,6 @@ 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}
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index 03b253f..77c32ae 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -1,6 +1,7 @@
package server
import (
+ "context"
"database/sql"
"encoding/json"
"fmt"
@@ -51,7 +52,7 @@ func newTestServer(t *testing.T) *testServer {
t.Fatalf("failed to create database: %v", err)
}
- store, err := storage.NewFilesystem(storagePath)
+ store, err := storage.OpenBucket(context.Background(), "file://"+storagePath)
if err != nil {
_ = db.Close()
_ = os.RemoveAll(tempDir)
@@ -65,7 +66,7 @@ func newTestServer(t *testing.T) *testServer {
cfg := &config.Config{
BaseURL: "http://localhost:8080",
- Storage: config.StorageConfig{Path: storagePath},
+ Storage: config.StorageConfig{URL: "file://" + storagePath},
Database: config.DatabaseConfig{Path: dbPath},
}
@@ -1359,7 +1360,7 @@ func TestNewServer_InvalidAccessLogFailsBeforeDatabaseInit(t *testing.T) {
tempDir := t.TempDir()
dbPath := filepath.Join(tempDir, "test.db")
cfg := &config.Config{
- Storage: config.StorageConfig{Path: filepath.Join(tempDir, "artifacts")},
+ Storage: config.StorageConfig{URL: "file://" + filepath.Join(tempDir, "artifacts")},
Database: config.DatabaseConfig{Path: dbPath},
AccessLog: config.AccessLogConfig{Path: filepath.Join(tempDir, "missing", "access.jsonl")},
}
diff --git a/internal/storage/filesystem.go b/internal/storage/filesystem.go
deleted file mode 100644
index d509e9d..0000000
--- a/internal/storage/filesystem.go
+++ /dev/null
@@ -1,277 +0,0 @@
-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
-}
diff --git a/internal/storage/filesystem_test.go b/internal/storage/filesystem_test.go
deleted file mode 100644
index de0e418..0000000
--- a/internal/storage/filesystem_test.go
+++ /dev/null
@@ -1,331 +0,0 @@
-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
-}
diff --git a/internal/storage/storage.go b/internal/storage/storage.go
index e11db53..3d0be1c 100644
--- a/internal/storage/storage.go
+++ b/internal/storage/storage.go
@@ -11,8 +11,6 @@ package storage
import (
"context"
- "crypto/sha256"
- "encoding/hex"
"errors"
"io"
"time"
@@ -80,42 +78,3 @@ 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
-}
diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go
index 97800f0..65f5a23 100644
--- a/internal/storage/storage_test.go
+++ b/internal/storage/storage_test.go
@@ -6,7 +6,6 @@ import (
"crypto/sha256"
"encoding/hex"
"io"
- "strings"
"testing"
)
@@ -35,30 +34,6 @@ 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) {