diff --git a/esignet-service/README.md b/esignet-service/README.md index 77ce72203..c85b0a91c 100644 --- a/esignet-service/README.md +++ b/esignet-service/README.md @@ -9,6 +9,7 @@ A production-grade Go HTTP service with PostgreSQL and Redis integration, struct - [Overview](#overview) - [Project Structure](#project-structure) - [Prerequisites](#prerequisites) +- [Go modules: ThunderID dependency](#go-modules-thunderid-dependency) - [Quickstart](#quickstart) - [Configuration](#configuration) - [API Reference](#api-reference) @@ -20,14 +21,16 @@ A production-grade Go HTTP service with PostgreSQL and Redis integration, struct ## Overview -`esignet-service` is a Go HTTP service running on port **8088**. It provides: +`esignet-service` is a Go HTTP service running on port **8088** (configurable via `PORT`). It provides: - Structured JSON logging via `log/slog` - PostgreSQL connection pool via `pgx/v5` - Redis client via `go-redis/v9` -- Chi router with request-ID injection, access logging, panic recovery, and gzip compression +- **Two HTTP modes** + - **Standalone** (default): Chi router with request-ID injection, access logging, panic recovery, gzip compression, and the routes below. + - **Thunder embed** (optional): If `THUNDER_HOME` is set, the process serves `/ping` and `/health` on a `net/http` mux and registers ThunderID OAuth and flow routes via [`embed.WireThunder`](https://github.com/anushasunkada/thunder/tree/public-package/backend/pkg/embed) from the pinned Thunder module (see [Go modules: ThunderID dependency](#go-modules-thunderid-dependency)). - A deep health endpoint that concurrently pings all backing services -- Graceful shutdown on `SIGINT`/`SIGTERM` +- Graceful shutdown on `SIGINT`/`SIGTERM` (cleanup runs on all exit paths; `main` delegates to `run()` which returns an exit code instead of calling `os.Exit` before defers) - Multi-stage Docker build with a minimal Alpine production image --- @@ -38,28 +41,33 @@ A production-grade Go HTTP service with PostgreSQL and Redis integration, struct . ├── cmd/ │ └── esignet/ -│ └── main.go # Entrypoint — wires config, logger, DB, Redis, server +│ └── main.go # Entrypoint — config, logger, DB, Redis, std server or Thunder mux ├── internal/ +│ ├── authn/ +│ │ └── provider.go # Authn provider passed to Thunder embed.WireThunder │ ├── config/ -│ │ └── config.go # Env-var configuration (envconfig) +│ │ └── config.go # Env-var configuration (envconfig), incl. optional Thunder │ ├── db/ │ │ └── postgres.go # pgx/v5 connection pool + Ping │ ├── cache/ │ │ └── redis.go # go-redis/v9 client + Ping │ ├── middleware/ -│ │ └── middleware.go # RequestID · Logger · Recoverer +│ │ └── middleware.go # RequestID · Logger · Recoverer (standalone stack) │ ├── handler/ │ │ └── health.go # GET /health — concurrent dependency checks -│ └── server/ -│ └── server.go # Chi router, route registration, graceful shutdown +│ ├── server/ +│ │ └── server.go # Chi router, route registration, graceful shutdown +│ └── thunderembed/ +│ └── server.go # Optional ThunderID mux + WireThunder when THUNDER_HOME is set ├── pkg/ │ └── logger/ │ └── logger.go # slog JSON/text handler factory ├── Dockerfile # Multi-stage: dev → builder → production ├── compose.yaml # Full local stack: app + postgres + redis ├── Makefile # Developer targets -├── go.mod -└── .env.example # All supported environment variables with defaults +├── go.mod # Pins ThunderID; see Thunder dependency section +├── config.yaml.example # Optional YAML template for local/docs (env vars are canonical) +└── .env.example # Supported environment variables with defaults ``` --- @@ -68,12 +76,55 @@ A production-grade Go HTTP service with PostgreSQL and Redis integration, struct | Tool | Version | Purpose | |---|---|---| -| Go | 1.23+ | Build and run the service | +| Go | 1.26+ | Build and run the service (matches `go.mod`) | | Docker + Compose | v2 | Local backing services and container builds | | Make | any | Convenience targets | --- +## Go modules: ThunderID dependency + +The service imports ThunderID as **`github.com/thunder-id/thunderid`** (for example `pkg/embed` in Thunder embed mode). Upstream development often tracks a **fork** that hosts the embed-friendly `backend/` tree on branch **`public-package`**: + +- Web UI (for context only — not pasted into `go.mod`): [github.com/anushasunkada/thunder/tree/public-package/backend/pkg](https://github.com/anushasunkada/thunder/tree/public-package/backend/pkg) + +### What goes in `go.mod` + +Go does **not** support raw `https://github.com/.../tree/...` URLs in `go.mod`. Use a **module path** and **version** (or `replace`): + +1. **`require`** — pin the logical module `github.com/thunder-id/thunderid` to a **pseudo-version** resolved from the fork (commit time + short hash). +2. **`replace`** — map that module to the fork’s **`backend`** subdirectory module, which Go fetches as `github.com/anushasunkada/thunder/backend` (repository `thunder`, subdir `backend`, same `module github.com/thunder-id/thunderid` line in `backend/go.mod`). + +Example shape (exact versions change when you refresh the pin): + +```go +require github.com/thunder-id/thunderid v0.0.0-20260514111244-7975af7f6646 + +replace github.com/thunder-id/thunderid => github.com/anushasunkada/thunder/backend v0.0.0-20260514111244-7975af7f6646 +``` + +### Move the pin to the latest `public-package` commit + +```bash +cd esignet-service +go get github.com/anushasunkada/thunder/backend@public-package +go mod tidy +``` + +Commit the updated `go.mod` and `go.sum` when you intentionally upgrade Thunder. + +### Work against a local Thunder checkout + +Temporarily point `replace` at your machine (path must reach the directory that contains Thunder’s `go.mod`, usually `backend/`): + +```go +replace github.com/thunder-id/thunderid => /absolute/or/relative/path/to/thunder/backend +``` + +Remove or swap the `replace` before pushing if CI should use the remote fork instead. + +--- + ## Quickstart ```bash @@ -84,13 +135,15 @@ cd esignet-service # 2. Copy the example env file cp .env.example .env -# 3. Pull Go dependencies +# 3. Pull Go dependencies (downloads the pinned Thunder fork; see [Go modules: ThunderID dependency](#go-modules-thunderid-dependency)) go mod tidy -# 4a. Start everything with Docker Compose (app + postgres + redis) +# 4. Optional: Thunder embed — set THUNDER_HOME in .env to a Thunder deployment directory (see Configuration) + +# 5a. Start everything with Docker Compose (app + postgres + redis) make up -# 4b. OR start only the backing services and run the server locally +# 5b. OR start only the backing services and run the server locally make db redis # starts postgres + redis in Docker make dev # go run ./cmd/esignet ``` @@ -144,10 +197,20 @@ All configuration is supplied through environment variables. Copy `.env.example` | `LOG_LEVEL` | `info` | `debug` · `info` · `warn` · `error` | | `LOG_FORMAT` | `json` | `json` · `text` | +### Thunder embed (optional) + +When **`THUNDER_HOME`** is non-empty after trimming whitespace, the binary uses **`internal/thunderembed`**: a `net/http.ServeMux` with the same `/ping` and `/health` handlers as standalone mode, plus Thunder routes from **`embed.WireThunder`**. Point `THUNDER_HOME` at a Thunder **deployment directory** on disk (the layout Thunder expects for config and assets — see ThunderID / fork docs under `pkg/embed`). + +| Variable | Default | Description | +|---|---|---| +| `THUNDER_HOME` | *(empty)* | If set, enables Thunder embed mode; if empty, the standalone Chi stack is used | + --- ## API Reference +In **standalone** mode, the routes below are the primary application surface. In **Thunder embed** mode, **`GET /ping`** and **`GET /health`** behave the same; additional OAuth, flow, and related paths are registered by Thunder (`embed.WireThunder`). Consult the ThunderID `pkg/` README on your pinned fork for those surfaces. + ### `GET /ping` Lightweight liveness probe. No database or cache calls are made. Use this for high-frequency load-balancer checks. @@ -187,9 +250,7 @@ Deep readiness probe. Pings PostgreSQL and Redis concurrently within a 5-second } ``` -Every response includes `X-Request-ID` in the response headers. - ---- +In **standalone** mode, responses from the Chi stack include `X-Request-ID` (middleware). In **Thunder embed** mode, `/ping` and `/health` do not go through that Chi middleware; Thunder-registered routes follow Thunder’s own HTTP behavior. ## Development @@ -200,7 +261,7 @@ make dev # go run ./cmd/esignet (fastest inner loop) make build # compile → bin/esignet make run # build + run the binary make test # go test -race -cover ./... -make lint # golangci-lint run +make lint # golangci-lint (via `go run …`; needs network on first run) make format # go fmt ./... make tidy # go mod tidy make clean # remove bin/ diff --git a/esignet-service/cmd/esignet/main.go b/esignet-service/cmd/esignet/main.go index a85d1f1d2..ee560a369 100644 --- a/esignet-service/cmd/esignet/main.go +++ b/esignet-service/cmd/esignet/main.go @@ -1,3 +1,4 @@ +// Command esignet is the esignet HTTP service entrypoint. package main import ( @@ -6,22 +7,29 @@ import ( "log/slog" "os" "os/signal" + "strings" "syscall" "github.com/mosip/esignet/internal/cache" "github.com/mosip/esignet/internal/config" "github.com/mosip/esignet/internal/db" + "github.com/mosip/esignet/internal/handler" "github.com/mosip/esignet/internal/server" + "github.com/mosip/esignet/internal/thunderembed" "github.com/mosip/esignet/pkg/logger" ) func main() { + os.Exit(run()) +} + +func run() int { // ── 1. Configuration ───────────────────────────────────────────────────── cfg, err := config.Load() if err != nil { // slog default is not yet initialised; fall back to stdlib. slog.Error("failed to load config", slog.String("error", err.Error())) - os.Exit(1) + return 1 } // ── 2. Logger ──────────────────────────────────────────────────────────── @@ -41,7 +49,7 @@ func main() { postgres, err := db.NewPostgres(ctx, cfg.Postgres, log) if err != nil { log.Error("postgres init failed", slog.String("error", err.Error())) - os.Exit(1) + return 1 } defer postgres.Close() @@ -49,7 +57,7 @@ func main() { redisClient, err := cache.NewRedis(ctx, cfg.Redis, log) if err != nil { log.Error("redis init failed", slog.String("error", err.Error())) - os.Exit(1) + return 1 } defer func() { if err := redisClient.Close(); err != nil { @@ -57,16 +65,37 @@ func main() { } }() + pingers := map[string]handler.Pinger{ + "postgres": postgres, + "redis": redisClient, + } + // ── 6. HTTP Server ─────────────────────────────────────────────────────── - srv := server.New(cfg.Server, server.Dependencies{ - DB: postgres, - Cache: redisClient, - }, log) + thunderHome := strings.TrimSpace(cfg.Thunder.Home) + var ( + srvStd *server.Server + srvEmb *thunderembed.Server + ) + if thunderHome != "" { + srvEmb, err = thunderembed.NewServer(cfg, log, pingers) + if err != nil { + log.Error("thunder embed init failed", slog.String("error", err.Error())) + return 1 + } + } else { + srvStd = server.New(cfg.Server, server.Dependencies{ + DB: postgres, + Cache: redisClient, + }, log) + } - // Start server in a goroutine so we can listen for shutdown signals below. srvErr := make(chan error, 1) go func() { - srvErr <- srv.Start() + if srvEmb != nil { + srvErr <- srvEmb.Start() + return + } + srvErr <- srvStd.Start() }() // ── 7. Block until signal or server error ──────────────────────────────── @@ -74,18 +103,26 @@ func main() { case err := <-srvErr: if err != nil && !errors.Is(err, context.Canceled) { log.Error("server exited with error", slog.String("error", err.Error())) - os.Exit(1) + return 1 } case <-ctx.Done(): log.Info("shutdown signal received", slog.String("signal", ctx.Err().Error())) } // ── 8. Graceful shutdown ───────────────────────────────────────────────── - // Use a fresh background context – the parent ctx is already cancelled. - if err := srv.Shutdown(context.Background()); err != nil { - log.Error("graceful shutdown failed", slog.String("error", err.Error())) - os.Exit(1) + shutCtx := context.Background() + if srvEmb != nil { + if err := srvEmb.Shutdown(shutCtx); err != nil { + log.Error("graceful shutdown failed", slog.String("error", err.Error())) + return 1 + } + } else { + if err := srvStd.Shutdown(shutCtx); err != nil { + log.Error("graceful shutdown failed", slog.String("error", err.Error())) + return 1 + } } log.Info("esignet service stopped cleanly") + return 0 } diff --git a/esignet-service/config.yaml.example b/esignet-service/config.yaml.example new file mode 100644 index 000000000..f99fee8e3 --- /dev/null +++ b/esignet-service/config.yaml.example @@ -0,0 +1,52 @@ +# ───────────────────────────────────────────────────────────────────────────── +# esignet-service YAML configuration +# +# Copy this file to config.yaml (or any path) and set CONFIG_FILE to point at +# it. Values here are defaults; any matching environment variable always wins. +# +# Load order (lowest → highest priority): +# 1. Struct defaults (hard-coded in the binary) +# 2. This YAML file +# 3. Environment variables +# +# Usage: +# CONFIG_FILE=/etc/esignet/config.yaml ./esignet +# CONFIG_FILE=config.yaml make dev +# +# Tip: commit a config.yaml for local development; never commit secrets. +# ───────────────────────────────────────────────────────────────────────────── + +# ── HTTP Server ─────────────────────────────────────────────────────────────── +server: + port: 8088 + read_timeout: 15s + write_timeout: 15s + idle_timeout: 60s + shutdown_timeout: 30s + +# ── PostgreSQL ──────────────────────────────────────────────────────────────── +postgres: + url: postgres://postgres:postgres@localhost:5432/app?sslmode=disable + max_conns: 10 + min_conns: 2 + max_conn_lifetime: 1h + max_conn_idle_time: 30m + health_timeout: 5s + +# ── Redis ───────────────────────────────────────────────────────────────────── +redis: + addr: localhost:6379 + password: "" + db: 0 + dial_timeout: 5s + read_timeout: 3s + write_timeout: 3s + pool_size: 10 + health_timeout: 5s + +# ── Logging ─────────────────────────────────────────────────────────────────── +log: + # debug | info | warn | error + level: info + # json | text + format: json diff --git a/esignet-service/go.mod b/esignet-service/go.mod index 0c1634947..cdbdd2c25 100644 --- a/esignet-service/go.mod +++ b/esignet-service/go.mod @@ -1,6 +1,6 @@ module github.com/mosip/esignet -go 1.23 +go 1.26 require ( github.com/alicebob/miniredis/v2 v2.33.0 @@ -8,18 +8,67 @@ require ( github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.1 github.com/kelseyhightower/envconfig v1.4.0 - github.com/redis/go-redis/v9 v9.7.0 + github.com/redis/go-redis/v9 v9.18.0 + github.com/thunder-id/thunderid v0.0.0-20260514111244-7975af7f6646 ) +// Fork: public-package branch (backend/go.mod → module github.com/thunder-id/thunderid). +// Use `go get github.com/anushasunkada/thunder/backend@public-package` to refresh the pseudo-version. +replace github.com/thunder-id/thunderid => github.com/anushasunkada/thunder/backend v0.0.0-20260514111244-7975af7f6646 + require ( github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-webauthn/webauthn v0.15.0 // indirect + github.com/go-webauthn/x v0.1.26 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/go-tpm v0.9.6 // indirect + github.com/google/jsonschema-go v0.4.2 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modelcontextprotocol/go-sdk v1.4.1 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/segmentio/asm v1.1.3 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/text v0.20.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.65.2 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.10.0 // indirect + modernc.org/sqlite v1.37.0 // indirect ) diff --git a/esignet-service/go.sum b/esignet-service/go.sum index 62684955d..c629c49d0 100644 --- a/esignet-service/go.sum +++ b/esignet-service/go.sum @@ -1,11 +1,17 @@ +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA= github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0= +github.com/anushasunkada/thunder/backend v0.0.0-20260514111244-7975af7f6646 h1:hP9l3B2C3CbbNX73S3dpqyFC6muRbIoihU+A67HatDg= +github.com/anushasunkada/thunder/backend v0.0.0-20260514111244-7975af7f6646/go.mod h1:bHFQmUiriPo26b8AYbAqc0yVjqdTYNuiDTpz10Q9OiU= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,10 +19,39 @@ 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY= +github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A= +github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs= +github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= +github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -27,24 +62,129 @@ github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= +github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc= +github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= 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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +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/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0 h1:8UPA4IbVZxpsD76ihGOQiFml99GPAEZLohDXvqHdi6U= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.39.0/go.mod h1:MZ1T/+51uIVKlRzGw1Fo46KEWThjlCBZKl2LzY5nv4g= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= +golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.27.1 h1:emhLB4uoOmkZUnTDFcMI3AbkmU/Evjuerit9Taqe6Ss= +modernc.org/ccgo/v4 v4.27.1/go.mod h1:543Q0qQhJWekKVS5P6yL5fO6liNhla9Lbm2/B3rEKDE= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.2 h1:drWL1QO9fKXr3kXDN8y+4lKyBr8bA3mtUBQpftq3IJw= +modernc.org/libc v1.65.2/go.mod h1:VI3V2S5mNka4deJErQ0jsMXe7jgxojE2fOB/mWoHlbc= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= +modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= +modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= +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= diff --git a/esignet-service/internal/authn/provider.go b/esignet-service/internal/authn/provider.go new file mode 100644 index 000000000..91edfd463 --- /dev/null +++ b/esignet-service/internal/authn/provider.go @@ -0,0 +1,45 @@ +// Package authn holds the eSignet-side [pkg/authnprovider.AuthnProvider] used when +// Thunder is embedded. Replace the bodies of Authenticate / GetAttributes with +// MOSIP-backed logic. +package authn + +import ( + "context" + + "github.com/thunder-id/thunderid/pkg/authnprovider" +) + +// Provider is the custom authentication provider passed into Thunder OAuth / flow. +type Provider struct{} + +// New returns a Provider suitable for [github.com/thunder-id/thunderid/pkg/embed.WireThunder]. +func New() *Provider { + return &Provider{} +} + +// Authenticate implements [authnprovider.AuthnProvider]. +func (p *Provider) Authenticate( + _ context.Context, + _, _ map[string]interface{}, + _ *authnprovider.AuthnMetadata, +) (*authnprovider.AuthnResult, *authnprovider.ServiceError) { + return nil, authnprovider.NewClientError( + authnprovider.ErrorCodeNotImplemented, + "authentication not implemented", + "replace internal/authn.Provider with MOSIP integration", + ) +} + +// GetAttributes implements [authnprovider.AuthnProvider]. +func (p *Provider) GetAttributes( + _ context.Context, + _ string, + _ *authnprovider.RequestedAttributes, + _ *authnprovider.GetAttributesMetadata, +) (*authnprovider.GetAttributesResult, *authnprovider.ServiceError) { + return nil, authnprovider.NewClientError( + authnprovider.ErrorCodeNotImplemented, + "get attributes not implemented", + "replace internal/authn.Provider with MOSIP integration", + ) +} diff --git a/esignet-service/internal/cache/redis.go b/esignet-service/internal/cache/redis.go index 69b806c38..1d3a15f03 100644 --- a/esignet-service/internal/cache/redis.go +++ b/esignet-service/internal/cache/redis.go @@ -7,8 +7,8 @@ import ( "fmt" "log/slog" - "github.com/redis/go-redis/v9" "github.com/mosip/esignet/internal/config" + "github.com/redis/go-redis/v9" ) // Redis wraps a go-redis Client. diff --git a/esignet-service/internal/config/config.go b/esignet-service/internal/config/config.go index d0a893e81..e0eec6b2d 100644 --- a/esignet-service/internal/config/config.go +++ b/esignet-service/internal/config/config.go @@ -1,3 +1,4 @@ +// Package config defines runtime configuration loaded from environment variables. package config import ( @@ -15,6 +16,14 @@ type Config struct { Postgres PostgresConfig Redis RedisConfig Log LogConfig + // Thunder is optional: when Home is non-empty, HTTP uses ThunderID OAuth + flow + // (see github.com/thunder-id/thunderid/pkg/embed) instead of the standalone chi stack. + Thunder ThunderConfig +} + +// ThunderConfig points at a Thunder server deployment directory (THUNDER_HOME). +type ThunderConfig struct { + Home string `envconfig:"THUNDER_HOME"` } // ServerConfig controls the HTTP server. diff --git a/esignet-service/internal/config/config_test.go b/esignet-service/internal/config/config_test.go index fd2b1beb1..eb0958576 100644 --- a/esignet-service/internal/config/config_test.go +++ b/esignet-service/internal/config/config_test.go @@ -1,10 +1,19 @@ package config import ( + "os" "testing" "time" ) +func TestMain(m *testing.M) { + // Load() reads the real process environment. Start from an empty slate so + // default and override tests stay deterministic under developer shells / CI + // (e.g. LOG_LEVEL=debug, PORT=..., SERVER_PORT=...). + os.Clearenv() + os.Exit(m.Run()) +} + func TestLoad_Defaults(t *testing.T) { t.Setenv("DATABASE_URL", "postgres://user:pass@localhost:5432/db?sslmode=disable") @@ -40,8 +49,8 @@ func TestLoad_Defaults(t *testing.T) { if cfg.Redis.PoolSize != 10 { t.Errorf("Redis.PoolSize = %d, want 10", cfg.Redis.PoolSize) } - if cfg.Log.Level != "debug" { - t.Errorf("Log.Level = %q, want debug", cfg.Log.Level) + if cfg.Log.Level != "info" { + t.Errorf("Log.Level = %q, want info", cfg.Log.Level) } if cfg.Log.Format != "json" { t.Errorf("Log.Format = %q, want json", cfg.Log.Format) diff --git a/esignet-service/internal/db/postgres_test.go b/esignet-service/internal/db/postgres_test.go index 9761e9956..8ec70c9a7 100644 --- a/esignet-service/internal/db/postgres_test.go +++ b/esignet-service/internal/db/postgres_test.go @@ -23,7 +23,7 @@ type mockPool struct { } func (m *mockPool) Ping(_ context.Context) error { return m.pingErr } -func (m *mockPool) Close() { m.closed = true } +func (m *mockPool) Close() { m.closed = true } // ── helpers ─────────────────────────────────────────────────────────────────── diff --git a/esignet-service/internal/middleware/middleware_test.go b/esignet-service/internal/middleware/middleware_test.go index d2e0e26f8..16969feef 100644 --- a/esignet-service/internal/middleware/middleware_test.go +++ b/esignet-service/internal/middleware/middleware_test.go @@ -22,7 +22,7 @@ func withRequestID(ctx context.Context, id string) context.Context { func TestRequestID_GeneratesID(t *testing.T) { var capturedID string - next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { capturedID, _ = r.Context().Value(RequestIDKey).(string) }) @@ -42,7 +42,7 @@ func TestRequestID_GeneratesID(t *testing.T) { func TestRequestID_HonoursInboundHeader(t *testing.T) { const existingID = "my-upstream-id" var capturedID string - next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { capturedID, _ = r.Context().Value(RequestIDKey).(string) }) @@ -62,7 +62,7 @@ func TestRequestID_HonoursInboundHeader(t *testing.T) { // ── Logger ──────────────────────────────────────────────────────────────────── func TestLogger_LogsRequest(t *testing.T) { - next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusCreated) _, _ = w.Write([]byte("hello")) }) @@ -96,7 +96,7 @@ func TestLogger_WithRequestIDInContext(t *testing.T) { // ── Recoverer ───────────────────────────────────────────────────────────────── func TestRecoverer_CatchesPanic(t *testing.T) { - panicking := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + panicking := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { panic("test panic") }) @@ -114,7 +114,7 @@ func TestRecoverer_CatchesPanic(t *testing.T) { } func TestRecoverer_CatchesPanic_WithRequestID(t *testing.T) { - panicking := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + panicking := http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { panic("panic with request id") }) diff --git a/esignet-service/internal/server/server.go b/esignet-service/internal/server/server.go index 00aa2cdc1..bb8e60ca9 100644 --- a/esignet-service/internal/server/server.go +++ b/esignet-service/internal/server/server.go @@ -36,10 +36,10 @@ func New(cfg config.ServerConfig, deps Dependencies, log *slog.Logger) *Server { r := chi.NewRouter() // ── Middleware stack (applied to every request) ───────────────────────── - r.Use(middleware.RequestID) // inject/echo X-Request-ID - r.Use(middleware.Logger(log)) // structured access log - r.Use(middleware.Recoverer(log)) // panic → 500 + stack log - r.Use(chimw.StripSlashes) // /health/ → /health + r.Use(middleware.RequestID) // inject/echo X-Request-ID + r.Use(middleware.Logger(log)) // structured access log + r.Use(middleware.Recoverer(log)) // panic → 500 + stack log + r.Use(chimw.StripSlashes) // /health/ → /health r.Use(chimw.Compress(5)) // gzip response bodies r.Use(chimw.Timeout(cfg.WriteTimeout)) // per-request deadline diff --git a/esignet-service/internal/server/server_test.go b/esignet-service/internal/server/server_test.go index a6e357876..34f7c6764 100644 --- a/esignet-service/internal/server/server_test.go +++ b/esignet-service/internal/server/server_test.go @@ -53,7 +53,7 @@ func TestServer_Ping(t *testing.T) { if err != nil { t.Fatalf("GET /ping: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want 200", resp.StatusCode) @@ -72,7 +72,7 @@ func TestServer_Health_AllUp(t *testing.T) { if err != nil { t.Fatalf("GET /health: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want 200", resp.StatusCode) @@ -91,7 +91,7 @@ func TestServer_Health_DBDown(t *testing.T) { if err != nil { t.Fatalf("GET /health: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusServiceUnavailable { t.Errorf("status = %d, want 503", resp.StatusCode) @@ -108,7 +108,7 @@ func TestServer_Health_NoDeps(t *testing.T) { if err != nil { t.Fatalf("GET /health: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want 200", resp.StatusCode) @@ -126,7 +126,7 @@ func TestServer_Health_DBOnly(t *testing.T) { if err != nil { t.Fatalf("GET /health: %v", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { t.Errorf("status = %d, want 200", resp.StatusCode) diff --git a/esignet-service/internal/thunderembed/server.go b/esignet-service/internal/thunderembed/server.go new file mode 100644 index 000000000..a406f0ee7 --- /dev/null +++ b/esignet-service/internal/thunderembed/server.go @@ -0,0 +1,77 @@ +// Package thunderembed wires ThunderID OAuth and flow execution into this process +// when THUNDER_HOME is set. +package thunderembed + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "strings" + + "github.com/mosip/esignet/internal/authn" + "github.com/mosip/esignet/internal/config" + "github.com/mosip/esignet/internal/handler" + "github.com/thunder-id/thunderid/pkg/embed" +) + +// Server is a thin wrapper around [http.Server] for the Thunder-combined mux. +type Server struct { + http *http.Server + log *slog.Logger + shutdownTimeout config.ServerConfig +} + +// NewServer builds a [http.ServeMux] with esignet probes, then registers Thunder +// routes via [github.com/thunder-id/thunderid/pkg/embed.WireThunder]. +func NewServer(cfg *config.Config, log *slog.Logger, pingers map[string]handler.Pinger) (*Server, error) { + mux := http.NewServeMux() + + mux.HandleFunc("GET /ping", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/plain") + _, _ = w.Write([]byte("pong")) + }) + + mux.HandleFunc("GET /health", handler.HealthHandler(log, pingers)) + + if err := embed.WireThunder(mux, strings.TrimSpace(cfg.Thunder.Home), authn.New()); err != nil { + return nil, fmt.Errorf("thunder embed: %w", err) + } + + addr := fmt.Sprintf(":%d", cfg.Server.Port) + httpSrv := &http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + IdleTimeout: cfg.Server.IdleTimeout, + } + + return &Server{ + http: httpSrv, + log: log, + shutdownTimeout: cfg.Server, + }, nil +} + +// Start begins accepting connections. +func (s *Server) Start() error { + s.log.Info("http server listening (thunder embed)", slog.String("addr", s.http.Addr)) + if err := s.http.ListenAndServe(); err != nil && err != http.ErrServerClosed { + return fmt.Errorf("http server: %w", err) + } + return nil +} + +// Shutdown drains in-flight requests. +func (s *Server) Shutdown(ctx context.Context) error { + s.log.Info("graceful shutdown initiated") + shutCtx, cancel := context.WithTimeout(ctx, s.shutdownTimeout.ShutdownTimeout) + defer cancel() + if err := s.http.Shutdown(shutCtx); err != nil { + return fmt.Errorf("shutdown: %w", err) + } + embed.ShutdownThunder() + s.log.Info("http server stopped") + return nil +} diff --git a/esignet-service/pkg/logger/logger_test.go b/esignet-service/pkg/logger/logger_test.go index f488cdbeb..525774a9f 100644 --- a/esignet-service/pkg/logger/logger_test.go +++ b/esignet-service/pkg/logger/logger_test.go @@ -1,6 +1,7 @@ package logger import ( + "context" "log/slog" "testing" ) @@ -34,11 +35,11 @@ func TestNew_LevelsAndFormats(t *testing.T) { if log == nil { t.Fatal("New() returned nil") } - if !log.Enabled(nil, tc.wantLevel) { + if !log.Enabled(context.TODO(), tc.wantLevel) { t.Errorf("New(%q,%q): level %v should be enabled", tc.level, tc.format, tc.wantLevel) } // One level above the minimum should also be enabled. - if tc.wantLevel < slog.LevelError && !log.Enabled(nil, tc.wantLevel+4) { + if tc.wantLevel < slog.LevelError && !log.Enabled(context.TODO(), tc.wantLevel+4) { t.Errorf("New(%q,%q): next level should be enabled too", tc.level, tc.format) } })