feat: initial mcp-chassis with auth primitives
Shared Go library for Mathias-owned MCP servers, born from spike S3 of the 2026-05 homelab architecture review (see gitea.d-ma.be/mathias/infra/docs/superpowers/handoffs/2026-05-22-mcp-chassis-spike.md for the viability assessment and abort-criterion check). Provides three primitives every MCP server today re-implements: - auth.JWTValidator — Dex OIDC JWT validation. nil-safe (nil = "JWT disabled"), audience-optional. Lifted from the identical ~80-LOC implementations in gitea-mcp and hyperguild/ingestion. - auth.BearerMiddleware — dual-mode static-Bearer-or-Dex-JWT gate. Static wins first to avoid emitting a WWW-Authenticate challenge that would flip claude.ai's MCP client into OAuth discovery for static-only deployments. The fall-through 401 emits the RFC 9728 resource_metadata header only when explicitly configured. - auth.ProtectedResourceHandler — RFC 9728 /.well-known/oauth-protected-resource metadata document handler. Test coverage exercises every branch (static OK, JWT-disabled, empty bearer, wrong static, with-challenge vs without-challenge, nil-validator-Validate). go test -race clean. Deps: github.com/lestrrat-go/jwx/v2 (already a dep of every consumer) and testify (test-only). No new transitive deps. First migration target: gitea-mcp. If that port lands in one PR (abort criterion from spec), brain-mcp (ingestion) follows. Otherwise chassis reverts per the spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
README.md
Normal file
96
README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# mcp-chassis
|
||||
|
||||
Shared Go library for Mathias-owned MCP servers. Provides the
|
||||
auth + middleware primitives that every MCP server needs.
|
||||
|
||||
## Why
|
||||
|
||||
By 2026-05-22 there were three+ MCP servers (`gitea-mcp`, `brain-mcp` /
|
||||
`ingestion`, future ones from `template-go-agent`) each carrying their
|
||||
own near-identical:
|
||||
|
||||
- Dex JWT validator (~80 LOC, identical `jwx/v2` plumbing)
|
||||
- Bearer middleware (~50 LOC, dual-mode static + JWT)
|
||||
- RFC 9728 protected-resource metadata handler (~25 LOC)
|
||||
|
||||
The homelab architecture review's spike S3 (see
|
||||
`gitea.d-ma.be/mathias/infra/docs/superpowers/handoffs/2026-05-22-mcp-chassis-spike.md`)
|
||||
concluded a thin shared lib pays for itself within the first migration.
|
||||
This is that lib.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Replacing each MCP's tool registration / handler logic — that is per-domain.
|
||||
- Solving HTTP routing — consumers keep their own `http.ServeMux`.
|
||||
- Solving observability — see `gitea.d-ma.be/mathias/hyperguild/ingestion/internal/metrics`
|
||||
for the hand-rolled Prometheus pattern. May absorb a `metrics` subpackage here
|
||||
later, once a second consumer needs it.
|
||||
|
||||
## Packages
|
||||
|
||||
### `auth`
|
||||
|
||||
- `JWTValidator` — Dex OIDC JWT validation. `nil` is a valid value meaning
|
||||
"JWT auth disabled".
|
||||
- `BearerMiddleware` — static-Bearer-or-Dex-JWT gate. Static wins first; only
|
||||
emits `WWW-Authenticate: Bearer ... resource_metadata=...` on 401 when
|
||||
`resourceMetadataURL` is non-empty (claude.ai OAuth discovery).
|
||||
- `ProtectedResourceHandler` — RFC 9728 metadata document for
|
||||
`GET /.well-known/oauth-protected-resource`.
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"gitea.d-ma.be/mathias/mcp-chassis/auth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
staticToken := os.Getenv("BRAIN_MCP_TOKEN")
|
||||
dexIssuer := os.Getenv("DEX_ISSUER_URL")
|
||||
audience := os.Getenv("MCP_AUDIENCE")
|
||||
resourceURL := os.Getenv("MCP_RESOURCE_URL")
|
||||
|
||||
validator, err := auth.NewJWTValidator(context.Background(), dexIssuer, audience)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /.well-known/oauth-protected-resource",
|
||||
auth.ProtectedResourceHandler(resourceURL, dexIssuer))
|
||||
mux.Handle("/mcp", auth.BearerMiddleware(
|
||||
staticToken,
|
||||
validator,
|
||||
"brain",
|
||||
resourceURL+"/.well-known/oauth-protected-resource",
|
||||
mcpHandler(),
|
||||
))
|
||||
|
||||
_ = http.ListenAndServe(":3300", mux)
|
||||
}
|
||||
|
||||
func mcpHandler() http.Handler { /* per-domain */ return nil }
|
||||
```
|
||||
|
||||
## Versioning
|
||||
|
||||
Trunk-based development on `main`. Tagged with semver. Consumers pin
|
||||
specific tags (`go.mod` `require gitea.d-ma.be/mathias/mcp-chassis v0.x.y`)
|
||||
and bump deliberately.
|
||||
|
||||
Migrations are documented per-consumer in the consumer's CHANGELOG / commits.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `github.com/lestrrat-go/jwx/v2` — JWKS cache + JWT parsing. Same dep
|
||||
every MCP already had; no new transitive cost when adopting the chassis.
|
||||
- `github.com/stretchr/testify` — tests only.
|
||||
|
||||
stdlib otherwise.
|
||||
77
auth/bearer.go
Normal file
77
auth/bearer.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BearerMiddleware gates next behind dual-mode authentication. It is the
|
||||
// canonical pattern across every Mathias-owned MCP server.
|
||||
//
|
||||
// Auth precedence:
|
||||
//
|
||||
// 1. Static Bearer match (constant-time compare against staticToken).
|
||||
// Wins immediately and never emits a WWW-Authenticate header. This is
|
||||
// the path used by internal CLI callers that supply
|
||||
// `Authorization: Bearer $XXX_MCP_TOKEN` via `.mcp.json`. Returning
|
||||
// 401 without a WWW-Authenticate prevents the MCP client from
|
||||
// speculatively flipping into OAuth-discovery mode and discarding
|
||||
// the static token.
|
||||
// 2. Dex JWT validation (when validator is non-nil). Used by claude.ai
|
||||
// custom MCP connectors that finished the OAuth handshake.
|
||||
// 3. Otherwise 401. When resourceMetadataURL is non-empty, a
|
||||
// `WWW-Authenticate: Bearer realm="<realm>", resource_metadata="…"`
|
||||
// header is emitted per RFC 9728 §6.2 so claude.ai's OAuth discovery
|
||||
// flow can find the server's protected-resource metadata document.
|
||||
//
|
||||
// The order matters: a valid static Bearer must short-circuit BEFORE the
|
||||
// JWT path runs, because the WWW-Authenticate emitted on the fall-through
|
||||
// 401 confuses static-Bearer-only clients into discarding their header
|
||||
// and starting an OAuth handshake instead.
|
||||
//
|
||||
// staticToken may be empty (static auth disabled — only JWT accepted).
|
||||
// validator may be nil (JWT auth disabled — only static accepted).
|
||||
// realm is a free-text identifier used in the WWW-Authenticate challenge;
|
||||
// MCP servers conventionally use their service name (e.g. "brain", "gitea").
|
||||
func BearerMiddleware(
|
||||
staticToken string,
|
||||
validator *JWTValidator,
|
||||
realm string,
|
||||
resourceMetadataURL string,
|
||||
next http.Handler,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rawToken, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
if !ok || rawToken == "" {
|
||||
unauthorized(w, realm, resourceMetadataURL)
|
||||
return
|
||||
}
|
||||
|
||||
// 1. Static Bearer wins first — never emits a challenge.
|
||||
if staticToken != "" &&
|
||||
subtle.ConstantTimeCompare([]byte(rawToken), []byte(staticToken)) == 1 {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Then Dex JWT, if configured.
|
||||
if validator != nil {
|
||||
if _, err := validator.Validate(r.Context(), rawToken); err == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Reject with an OAuth resource-metadata challenge if configured.
|
||||
unauthorized(w, realm, resourceMetadataURL)
|
||||
})
|
||||
}
|
||||
|
||||
func unauthorized(w http.ResponseWriter, realm, resourceMetadataURL string) {
|
||||
if resourceMetadataURL != "" {
|
||||
w.Header().Set("WWW-Authenticate",
|
||||
`Bearer realm="`+realm+`", resource_metadata="`+resourceMetadataURL+`"`)
|
||||
}
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
}
|
||||
114
auth/bearer_test.go
Normal file
114
auth/bearer_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBearerMiddleware_StaticTokenWins(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
called := false
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
called = true
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
h := BearerMiddleware("supersecret", nil, "brain", "", next)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer supersecret")
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
require.True(t, called, "next must be called on valid static token")
|
||||
require.Equal(t, http.StatusNoContent, rec.Code)
|
||||
}
|
||||
|
||||
func TestBearerMiddleware_NoHeader_401NoChallengeWhenMetadataEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := BearerMiddleware("any", nil, "brain", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
require.Empty(t, rec.Header().Get("WWW-Authenticate"))
|
||||
}
|
||||
|
||||
func TestBearerMiddleware_NoHeader_EmitsChallengeWhenMetadataSet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := BearerMiddleware("any", nil, "brain",
|
||||
"https://brain-mcp.d-ma.be/.well-known/oauth-protected-resource",
|
||||
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
require.Equal(t,
|
||||
`Bearer realm="brain", resource_metadata="https://brain-mcp.d-ma.be/.well-known/oauth-protected-resource"`,
|
||||
rec.Header().Get("WWW-Authenticate"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestBearerMiddleware_WrongStaticToken_401(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := BearerMiddleware("expected", nil, "brain", "", http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("next must NOT be called on wrong token")
|
||||
}))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer wrong")
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
}
|
||||
|
||||
func TestBearerMiddleware_EmptyBearer_401(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := BearerMiddleware("expected", nil, "brain", "",
|
||||
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("next must NOT be called on empty bearer")
|
||||
}))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer ")
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusUnauthorized, rec.Code)
|
||||
}
|
||||
|
||||
func TestBearerMiddleware_StaticOnly_NilValidator_OK(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Verifies that JWT-disabled deployments (validator == nil) work end-to-end.
|
||||
called := false
|
||||
h := BearerMiddleware("tok", nil, "brain", "",
|
||||
http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer tok")
|
||||
h.ServeHTTP(rec, req)
|
||||
|
||||
require.True(t, called)
|
||||
}
|
||||
|
||||
func TestJWTValidator_NilReturnsError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var v *JWTValidator
|
||||
subj, err := v.Validate(t.Context(), "anything")
|
||||
require.Empty(t, subj)
|
||||
require.Error(t, err)
|
||||
}
|
||||
120
auth/jwt.go
Normal file
120
auth/jwt.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Package auth provides the Dex-JWT + static-Bearer authentication primitives
|
||||
// shared by every Mathias-owned MCP server (gitea-mcp, brain-mcp / ingestion,
|
||||
// future MCPs spawned from template-go-agent).
|
||||
//
|
||||
// Replaces ~80 LOC of near-identical jwt.go in each consumer, ~50 LOC of
|
||||
// Bearer middleware, and ~25 LOC of RFC 9728 protected-resource metadata
|
||||
// handler. See `gitea.d-ma.be/mathias/infra` docs/superpowers/handoffs/
|
||||
// 2026-05-22-mcp-chassis-spike.md for the design rationale and the
|
||||
// abort-criterion check.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
"github.com/lestrrat-go/jwx/v2/jwt"
|
||||
)
|
||||
|
||||
// JWTValidator validates Bearer JWTs issued by a Dex (OIDC) authorization server.
|
||||
// Audience is optional; leave empty to skip audience validation.
|
||||
//
|
||||
// A nil *JWTValidator behaves as "JWT auth disabled" — Validate returns an
|
||||
// error without panicking. Callers can construct one validator at startup
|
||||
// keyed on whether DEX_ISSUER_URL is set, and pass nil through the rest of
|
||||
// the codebase without further branching.
|
||||
type JWTValidator struct {
|
||||
issuer string
|
||||
audience string
|
||||
jwksURI string
|
||||
cache *jwk.Cache
|
||||
}
|
||||
|
||||
// NewJWTValidator fetches the OIDC discovery document from issuerURL,
|
||||
// extracts jwks_uri, warms the JWKS cache, and returns a ready validator.
|
||||
// Empty issuerURL returns (nil, nil) so callers can use a single
|
||||
// constructor regardless of whether Dex is configured.
|
||||
func NewJWTValidator(ctx context.Context, issuerURL, audience string) (*JWTValidator, error) {
|
||||
if issuerURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||
issuerURL+"/.well-known/openid-configuration", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build oidc discovery request: %w", err)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch oidc discovery: %w", err)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("oidc discovery: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var doc struct {
|
||||
JWKSURI string `json:"jwks_uri"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil {
|
||||
return nil, fmt.Errorf("decode oidc discovery: %w", err)
|
||||
}
|
||||
if doc.JWKSURI == "" {
|
||||
return nil, fmt.Errorf("oidc discovery: empty jwks_uri")
|
||||
}
|
||||
|
||||
cache := jwk.NewCache(ctx)
|
||||
if err := cache.Register(doc.JWKSURI, jwk.WithMinRefreshInterval(time.Hour)); err != nil {
|
||||
return nil, fmt.Errorf("register jwks cache: %w", err)
|
||||
}
|
||||
if _, err := cache.Refresh(ctx, doc.JWKSURI); err != nil {
|
||||
return nil, fmt.Errorf("initial jwks fetch: %w", err)
|
||||
}
|
||||
|
||||
return &JWTValidator{
|
||||
issuer: issuerURL,
|
||||
audience: audience,
|
||||
jwksURI: doc.JWKSURI,
|
||||
cache: cache,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validate parses and validates rawToken against the OIDC issuer. Returns
|
||||
// the subject claim on success. A nil receiver returns
|
||||
// (`""`, errDisabled) so callers can dispatch on `err != nil` without
|
||||
// nil-checks at every call site.
|
||||
func (v *JWTValidator) Validate(ctx context.Context, rawToken string) (string, error) {
|
||||
if v == nil {
|
||||
return "", errDisabled
|
||||
}
|
||||
|
||||
keySet, err := v.cache.Get(ctx, v.jwksURI)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get jwks: %w", err)
|
||||
}
|
||||
|
||||
opts := []jwt.ParseOption{
|
||||
jwt.WithKeySet(keySet),
|
||||
jwt.WithValidate(true),
|
||||
jwt.WithIssuer(v.issuer),
|
||||
}
|
||||
if v.audience != "" {
|
||||
opts = append(opts, jwt.WithAudience(v.audience))
|
||||
}
|
||||
|
||||
tok, err := jwt.ParseString(rawToken, opts...)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("validate jwt: %w", err)
|
||||
}
|
||||
return tok.Subject(), nil
|
||||
}
|
||||
|
||||
// errDisabled is the sentinel returned by Validate on a nil receiver.
|
||||
// Not exported because callers care about "not authorized" not "why";
|
||||
// this lets consumers treat any err the same way without depending on
|
||||
// the specific value.
|
||||
var errDisabled = fmt.Errorf("jwt auth disabled")
|
||||
29
auth/protected_resource.go
Normal file
29
auth/protected_resource.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ProtectedResourceHandler returns an RFC 9728 oauth-protected-resource
|
||||
// metadata handler. Mount at GET /.well-known/oauth-protected-resource
|
||||
// (no auth required).
|
||||
//
|
||||
// claude.ai's OAuth discovery flow fetches this endpoint when an MCP
|
||||
// server's WWW-Authenticate challenge points at it via the
|
||||
// `resource_metadata` parameter; the document points back at the Dex
|
||||
// authorization server, completing the discovery loop.
|
||||
func ProtectedResourceHandler(resourceURL, issuerURL string) http.HandlerFunc {
|
||||
type metadata struct {
|
||||
Resource string `json:"resource"`
|
||||
AuthorizationServers []string `json:"authorization_servers"`
|
||||
}
|
||||
body, _ := json.Marshal(metadata{
|
||||
Resource: resourceURL,
|
||||
AuthorizationServers: []string{issuerURL},
|
||||
})
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
}
|
||||
33
auth/protected_resource_test.go
Normal file
33
auth/protected_resource_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProtectedResourceHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := ProtectedResourceHandler(
|
||||
"https://brain-mcp.d-ma.be",
|
||||
"https://auth.d-ma.be",
|
||||
)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/.well-known/oauth-protected-resource", nil))
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
require.Equal(t, "application/json", rec.Header().Get("Content-Type"))
|
||||
|
||||
var got struct {
|
||||
Resource string `json:"resource"`
|
||||
AuthorizationServers []string `json:"authorization_servers"`
|
||||
}
|
||||
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &got))
|
||||
require.Equal(t, "https://brain-mcp.d-ma.be", got.Resource)
|
||||
require.Equal(t, []string{"https://auth.d-ma.be"}, got.AuthorizationServers)
|
||||
}
|
||||
24
go.mod
Normal file
24
go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module gitea.d-ma.be/mathias/mcp-chassis
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.6
|
||||
github.com/stretchr/testify v1.11.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc v1.0.6 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
37
go.sum
Normal file
37
go.sum
Normal file
@@ -0,0 +1,37 @@
|
||||
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/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/lestrrat-go/blackmagic v1.0.3 h1:94HXkVLxkZO9vJI/w2u1T0DAoprShFd13xtnSINtDWs=
|
||||
github.com/lestrrat-go/blackmagic v1.0.3/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
|
||||
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
|
||||
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
|
||||
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA=
|
||||
github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
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/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
|
||||
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.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=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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=
|
||||
Reference in New Issue
Block a user