Documentation
¶
Overview ¶
Package gateway is the entry point that wraps a sov rpc.Engine and exposes it over an HTTP-shaped server. The package owns the HTTP boundary; the rpc package itself is transport-free.
The HTTP server is pluggable via the Server interface. The default implementation uses net/http and is suitable for production. Consumers who want fiber / fasthttp / echo / a custom server implement Server themselves and pass it via WithServer(...). Users do NOT depend on a fiber adapter shipped by sov.
A Gateway wraps:
- rpc.Engine — services hosted IN-PROCESS (the modular-monolith case)
- Resolver chain — services resolved to remote endpoints (the microservice case). LocalResolver consults the in-process engine; RegisterResolver consults the TTL-backed map populated by POST /rpc/_register.
Both can coexist per request: the gateway tries each resolver in order. In-process services dispatch via direct Engine.Dispatch; remote services HTTP-proxy with strip+inject of X-Sov-* claim headers. The wire shape is identical in either path — same encoder, same auth, same observability, same error model. That symmetry is the PEMM thesis.
Framework endpoints — always present, gateway-owned:
- GET /rpc/_health aggregated health rollup
- GET /rpc/_introspect aggregated rpc.Engine.Describe() across services
Builtin-plugin endpoints — present when the plugin is registered via gw.Use(...), not part of the bare gateway:
- POST /rpc/_register service self-registration + heartbeat TTL (builtin/registry)
- POST /rpc/_batch object-keyed fan-in across multiple methods (builtin/batch)
Service-level `_X` paths (e.g. /rpc/WidgetService/_debug) are refused at the gateway with 404; those are reachable only via direct intra-cluster addressing.
Index ¶
- Constants
- func AuthorizationFromContext(ctx *rpc.Context) string
- func BuildTypeCatalog(report *IntrospectReport)
- func ComputeOverallHealthStatus(svc map[string]HealthService) string
- func GenerateIntrospectTraceID() string
- func GetCapability[T any](g *Gateway, typeName string) (T, bool)
- func HaltErr(err error) error
- func InheritRequestSnapshot(sub, parent *Request)
- func IsHalt(err error) bool
- func LocalRouters(gw *Gateway) []string
- func NormalizeUpstreamURL(raw string) (string, error)
- func PipeStream(fn func(w io.Writer) error) io.ReadCloser
- func PluginsImplementing[T any](g *Gateway) []T
- func RespondErr(resp *Response, err error) error
- type AuthBinding
- type AuthService
- type AuthTranslator
- type AuthzBinding
- type AuthzDecision
- type AuthzService
- type BatchCall
- type BatchRequest
- type BatchResponse
- type BootValidator
- type BreakerConfig
- type BreakerReport
- type Capability
- type CapabilityProvider
- type CheckParams
- type Claims
- type ClaimsCache
- type Client
- type ClientOption
- type ConfigApplier
- type ConfigReport
- type Conflict
- type ContextContributor
- type DispatchEvent
- type DispatchHook
- type Endpoint
- type EndpointPicker
- type EntryOptions
- type FieldDivergence
- type Gateway
- func (g *Gateway) AllCapabilities() map[string][]Capability
- func (g *Gateway) AuthBinding() *AuthBinding
- func (g *Gateway) Authorize(ctx context.Context, claims *Claims, router, method string, headers Header) error
- func (g *Gateway) AuthzBinding() *AuthzBinding
- func (g *Gateway) BreakerSnapshot() map[string]int
- func (g *Gateway) BuildProxyRequest(ctx context.Context, method, addr, path string, body []byte, parent *Request) (*http.Request, error)
- func (g *Gateway) CodecFor(req *Request) rpc.Codec
- func (g *Gateway) ConfigExposed() bool
- func (g *Gateway) ConfigReportBody() ConfigReport
- func (g *Gateway) ConsumeFederationPreemption(svc string)
- func (g *Gateway) Dispatch(ctx context.Context, req *Request) *Response
- func (g *Gateway) DispatchResolved(ctx context.Context, req *Request, router, method string) *Response
- func (g *Gateway) Engine() *rpc.Engine
- func (g *Gateway) ExposeConfig()
- func (g *Gateway) ExposeIntrospect()
- func (g *Gateway) FederatedCatalog(ctx context.Context) *IntrospectReport
- func (g *Gateway) Handle(ctx context.Context, req *Request) *Response
- func (g *Gateway) InFlight() int64
- func (g *Gateway) IntrospectBody(ctx context.Context, req *Request) *Response
- func (g *Gateway) IntrospectExposed() bool
- func (g *Gateway) InvalidateCatalog()
- func (g *Gateway) JoinMesh(ctx context.Context, opts MeshOptions) error
- func (g *Gateway) LinkPeer(peer *Gateway, services ...string)
- func (g *Gateway) ListenAndServe(ctx context.Context, addr string) error
- func (g *Gateway) LocalClient() Client
- func (g *Gateway) Log() Logger
- func (g *Gateway) LookupCapability(typeName string) (any, bool)
- func (g *Gateway) MustUse(p any)
- func (g *Gateway) MustUseAll(plugins ...any)
- func (g *Gateway) OverrideAuthBinding(service, method string)
- func (g *Gateway) OverrideAuthzBinding(service, method string)
- func (g *Gateway) PluginByName(name string) any
- func (g *Gateway) PluginInfos() []PluginInfo
- func (g *Gateway) PluginNames() []string
- func (g *Gateway) PolicyAllowsRoleTakeover(current, candidate string, role RoleFlag) bool
- func (g *Gateway) PreParserHeader(req *Request) Header
- func (g *Gateway) PreemptFederation(svc, oldAddr, newAddr string) bool
- func (g *Gateway) ProxyClient() *http.Client
- func (g *Gateway) RecordDispatch(req *Request, router, method, path string, resp *Response, started time.Time)
- func (g *Gateway) Register(router any)
- func (g *Gateway) RegisterAuth(svc AuthService)
- func (g *Gateway) RegisterAuthz(svc AuthzService)
- func (g *Gateway) RegisterRemote(name, address string, ttl time.Duration, opts ...RemoteOptions)
- func (g *Gateway) RegisterResolver() *RegisterResolver
- func (g *Gateway) Resolver() Resolver
- func (g *Gateway) Run(ctx context.Context, addr string) error
- func (g *Gateway) RunMesh(ctx context.Context, opts MeshOptions) error
- func (g *Gateway) TagSurface(report *IntrospectReport, surfaceName string, pred func(rpc.RouterInfo) bool)
- func (g *Gateway) Use(p any) error
- func (g *Gateway) UseAll(plugins ...any) error
- func (g *Gateway) UseMiddleware(mw Middleware)
- type Handler
- type Header
- type HeaderClaimer
- type HeaderInjector
- type HeaderParser
- type HealthAggregator
- type HealthGateway
- type HealthReport
- type HealthService
- type HookFailure
- type IntrospectContributor
- type IntrospectReport
- type LifecycleHook
- type LocalResolver
- type LogLevel
- type Logger
- type MeshConflictPolicy
- type MeshOptions
- type Middleware
- type Middlewarer
- type NetHTTPOptions
- type NetHTTPServer
- type Option
- func WithAdvertiseURL(url string) Option
- func WithAuthCacheTTL(d time.Duration) Option
- func WithClaimsCache(c ClaimsCache) Option
- func WithMaxInFlight(n int) Option
- func WithMiddleware(mw ...Middleware) Option
- func WithProxyClient(c *http.Client) Option
- func WithRegisterResolver(r *RegisterResolver) Option
- func WithRemoteBreaker(cfg BreakerConfig) Option
- func WithResolver(r Resolver) Option
- func WithRetries(cfg RetryConfig) Option
- func WithServer(s Server) Option
- func WithTrustUpstreamClaims(b bool) Option
- type Options
- type Plugin
- type PluginDependency
- type PluginDoc
- type PluginInfo
- type ReadinessContributor
- type RecoveryHandler
- type RegisterEntry
- type RegisterRequest
- type RegisterResolver
- func (r *RegisterResolver) AddressGroup() map[string][]string
- func (r *RegisterResolver) Close()
- func (r *RegisterResolver) Delete(service, address string)
- func (r *RegisterResolver) Introspectables() []string
- func (r *RegisterResolver) Put(service, address string, ttl time.Duration)
- func (r *RegisterResolver) PutEntry(service, address string, ttl time.Duration, opts EntryOptions)
- func (r *RegisterResolver) Resolve(_ context.Context, service string) (*Endpoint, bool)
- func (r *RegisterResolver) Services() []string
- type RegisterResolverOption
- type RegisterResponse
- type RegisterStore
- type RemoteOptions
- type Request
- type RequestHandler
- type Resolver
- type Response
- type ResponseInterceptor
- type RetryConfig
- type RoleFlag
- type RouteHandler
- type RoutePrioritizer
- type SealVerifier
- type Server
- type TypeDescriptor
- type TypeUse
- type TypeVariant
- type TypeVariants
- type UpstreamTrustPolicy
- type VerifyParams
Constants ¶
const ( ContextKeyRemoteIP = "http.remoteIP" ContextKeyPath = "gateway.path" )
ContextKeyRemoteIP and ContextKeyPath are where dispatchLocal stashes the caller IP and the inbound request path for handlers that need them via ctx.Get(...). Constants so callers don't repeat the literals.
const ( HeaderSubject = "X-Sov-Subject" HeaderIssuer = "X-Sov-Issuer" HeaderScopes = "X-Sov-Scopes" // comma-joined OAuth scopes HeaderExpires = "X-Sov-Expires" // unix seconds )
Header name constants for the injected claim bundle. Identity-only — no role/permission header. Role lookup happens at the AuthzService at decision time; per-request claim bundles never carry authorization state. These four are framework-owned because every sov gateway speaks them on the wire; the seal (X-Sov-Seal) is the hmacseal plugin's contract — see gateway/builtin/hmacseal/proto.
const ( IntrospectTraceHeader = "X-Sov-Introspect-Trace" IntrospectVisitedHeader = "X-Sov-Introspect-Visited" // IntrospectInternalHeader, when set to "1" on a root /rpc/_introspect // request, returns the FULL catalog including SOFT-hidden methods // (flagged internal:true). Hard-hidden methods are never returned. // The explorer's "show internal" toggle sets it. IntrospectInternalHeader = "X-Sov-Introspect-Internal" )
Introspect cascade headers — propagate a request id + the set of visited gateway addresses so federated cycles short-circuit and duplicate probes are deduped.
const ( ModeLocal = "local" // in-process router method handled it ModeRemote = "remote" // proxied to a single remote address ModeFederated = "federated" // proxied via an intermediate gateway ModePeer = "peer" // dispatched to a linked in-process peer ModeFramework = "framework" // framework endpoint (_health, _introspect, …) ModePlugin = "plugin" // handled by a RouteHandler plugin )
Dispatch mode labels for DispatchEvent.Mode / Response.Mode. Use these constants instead of the bare string literals at the dispatch call sites.
const ContextKeyAuthorization = "sov.authorization"
ContextKeyAuthorization is where dispatchLocal stashes the verbatim inbound Authorization header. Cross-service callers (e.g. a mesh pod's HTTP-backed Client) forward this back to the gateway for the downstream call so the impersonation chain is intact.
const ContextKeyClaims = rpc.ContextKeyClaims
ContextKeyClaims is re-exported from rpc so existing gateway callers keep working. Prefer ctx.Claims() in handler code.
Variables ¶
This section is empty.
Functions ¶
func AuthorizationFromContext ¶
AuthorizationFromContext returns the verbatim inbound bearer header the gateway stashed during dispatch. Cross-service callers forward this on subsequent RPCs so identity propagates through call chains. Typed accessor — consumers never reach for ctx.Get(ContextKeyAuthorization) directly.
func BuildTypeCatalog ¶
func BuildTypeCatalog(report *IntrospectReport)
BuildTypeCatalog walks every service's descriptors, extracts each param + response type, builds the flat Types map, and detects drift across same-named types into CrossRefs. Exported so plugin aggregators can rebuild the catalog after merging remote descriptors into report.Services.
func ComputeOverallHealthStatus ¶
func ComputeOverallHealthStatus(svc map[string]HealthService) string
ComputeOverallHealthStatus rolls up per-service health into a single status. Exported so plugin health aggregators can reuse the same rule the framework applies to its local view.
func GenerateIntrospectTraceID ¶
func GenerateIntrospectTraceID() string
generateTraceID returns a short id used to dedupe diamond fan-outs across the introspect cascade. Time-based (cheap, no crypto/rand dep here) — collisions are harmless because the visited list is the primary loop guard. GenerateIntrospectTraceID returns a short id used to dedupe diamond fan-outs across the introspect cascade. Exported for plugin aggregators that originate a cascade from a non-root call.
func GetCapability ¶
GetCapability is the generic helper consumers reach for. Returns the typed value when the capability exists AND the stored Impl matches the requested type; (zero, false) otherwise.
gen, ok := gateway.GetCapability[func() string](gw, "requestid.IDGenerator")
if ok {
id := gen()
...
}
Defined as a package-level generic (not a method) because Go methods don't take type parameters. The pattern matches the stdlib's slog.Any/AttrFunc style.
func HaltErr ¶
HaltErr wraps err so the gateway refuses startup. Boot-time hooks (BootValidator, ConfigApplier, LifecycleHook.OnStart) recognize this sentinel and bubble it up from ListenAndServe.
if !cfg.Valid() {
return gateway.HaltErr(fmt.Errorf("config invalid: %s", reason))
}
func InheritRequestSnapshot ¶
func InheritRequestSnapshot(sub, parent *Request)
InheritRequestSnapshot copies the pre-parser header snapshot from parent onto a synthetic sub-request, so a surface that builds its own *Request and routes it via Dispatch (bypassing Handle + HeaderParsers) still binds header= params from the pre-parser state — matching the /rpc surface and the documented invariant. Call it on any sub-request built from an inbound one before Dispatch.
func LocalRouters ¶
LocalRouters returns every wire-named router registered on gw — a convenience for team gateways that want to federate "everything I host":
gw.JoinMesh(ctx, sov.MeshOptions{
UpstreamURL: prime,
Federate: sov.LocalRouters(gw),
})
func NormalizeUpstreamURL ¶
NormalizeUpstreamURL returns the canonical scheme://host:port form every federation layer agrees on. Used by:
- WithFederationPreemption map keys and values
- WithUpstreamGateways allowlist
- X-Sov-Introspect-Visited cycle detection
Rules:
- Scheme is lowercased; only http and https are accepted.
- Host is lowercased; IPv6 keeps brackets.
- Port is always explicit (defaults are NOT stripped — http://x:80 stays http://x:80 so "is x the same as x:80?" never arises).
- Path, query, fragment, user-info, and trailing slash are stripped. Federation identity is host:port only.
Two DNS aliases that resolve to the same IP normalize to distinct canonical strings — sov does not resolve DNS here. Operators using multiple aliases for the same pod must list every alias explicitly in their preemption/allowlist sets.
func PipeStream ¶
func PipeStream(fn func(w io.Writer) error) io.ReadCloser
PipeStream adapts a writer-callback into an io.ReadCloser for Response.Stream — the ergonomic form when you have code that writes to an io.Writer (a zip.Writer, csv.Writer, a template, an SSE loop) rather than a ready-made reader.
fn runs in its own goroutine writing to the pipe; whatever it writes is streamed to the client as it is produced, in constant memory. fn's return value closes the read end: nil → clean EOF; an error → the adapter's io.Copy stops and the transfer truncates (the client sees a short/aborted body — there is no way to send a 500 after the status line is already on the wire, so validate before you start streaming).
The producer never blocks forever: if the client disconnects, the adapter closes the reader, the next pw.Write fails with io.ErrClosedPipe, and fn returns.
A panic inside fn is recovered and surfaced to the reader as an error (the transfer truncates) instead of crashing the process — fn runs in its own goroutine, so the gateway's request-scoped recovery middleware cannot see it. The panic value is not swallowed silently: it becomes the pipe's close error.
return &gateway.Response{
Status: 200,
Header: gateway.Header{
"Content-Type": "application/zip",
"Content-Disposition": `attachment; filename="export.zip"`,
},
Stream: gateway.PipeStream(func(w io.Writer) error {
zw := zip.NewWriter(w)
for _, f := range files {
fw, err := zw.Create(f.Name)
if err != nil {
return err
}
if _, err := fw.Write(f.Data); err != nil {
return err
}
}
return zw.Close()
}),
}
func PluginsImplementing ¶
PluginsImplementing returns every registered plugin that satisfies interface T, in registration order (after dependency reordering). This is how a subsystem — the framework's own dispatch OR a builtin like the explorer — discovers the plugins that implement ITS interface, WITHOUT the core cataloguing that interface. The plugin owns its interfaces; consumers ask for what they need:
for _, h := range gateway.PluginsImplementing[HeaderInjector](g) { h.InjectHeaders(...) }
for _, x := range gateway.PluginsImplementing[explorer.Extender](gw) { ... }
Results are memoized per T and rebuilt when a plugin is added, so the hot dispatch paths pay one type-assert per plugin per T only once. The returned slice is shared — treat it read-only.
func RespondErr ¶
RespondErr wraps err with a *Response the gateway returns to the caller instead of a default 500 envelope. Request-path hooks (HeaderParser, Middlewarer, RouteHandler, ResponseInterceptor) recognize this and short-circuit.
if origin == "" {
return gateway.RespondErr(&gateway.Response{Status: 400, Body: ...},
fmt.Errorf("missing Origin header"))
}
Types ¶
type AuthBinding ¶
AuthBinding records which registered service is the auth verifier. A gateway has at most one.
type AuthService ¶
type AuthService interface {
Verify(ctx *rpc.Context, p *VerifyParams) (*Claims, error)
}
AuthService is the contract gw.RegisterAuth requires. The wire name is derived from the implementing struct's type name (strip "Router" suffix); the method is always "verify". Verify resolves a bearer token to identity-only Claims — never to a profile row.
type AuthTranslator ¶
AuthTranslator fires AFTER the auth middleware has resolved Claims (when it has). Translate the verified identity into any format your brownfield downstreams expect. Mutate req.Header to add legacy headers; the gateway forwards them on the proxy hop.
Claims may be nil for anonymous requests — translators that need an identity should early-return in that case.
type AuthzBinding ¶
AuthzBinding records the policy-as-service binding, if any. The gateway calls Service/Method with the resolved Claims (or nil for anonymous) plus the downstream {service, method} the caller wants to invoke; the response is `{allow, authenticate, reason}`.
type AuthzDecision ¶
type AuthzDecision struct {
Allow bool `json:"allow"`
Reason string `json:"reason,omitempty"`
Authenticate bool `json:"authenticate,omitempty"`
}
AuthzDecision is the shape the registered authz service must return.
- Allow=true → request proceeds.
- Allow=false → request denied. Gateway returns 403 FORBIDDEN with Reason as the message — unless Authenticate=true.
- Allow=false, Authenticate=true → gateway returns 401 UNAUTHORIZED with Reason as the message. Use this when the method requires a logged-in caller and none is present (anonymous claims).
type AuthzService ¶
type AuthzService interface {
Check(ctx *rpc.Context, p *CheckParams) (*AuthzDecision, error)
}
AuthzService is the contract gw.RegisterAuthz requires. The method is always "check"; the wire name comes from the struct type.
The gateway calls Check on EVERY request when an AuthzService is bound — including anonymous requests (claims == nil). This makes the authz service the single source of truth for "this method requires auth" vs "this method requires admin scope" vs "this method is public". Returning {Allow:false, Authenticate:true} tells the gateway to surface 401 not 403, so the same primitive expresses both authentication-required and authorization-denied without a second hop.
type BatchCall ¶
type BatchCall struct {
Service string `json:"service"`
Method string `json:"method"`
Args json.RawMessage `json:"args,omitempty"` // forwarded verbatim as {"args": <args>}
}
BatchCall is one entry in a batch — caller-supplied alias keys the result map symmetrically.
type BatchRequest ¶
BatchRequest is the JSON body of POST /rpc/_batch.
type BatchResponse ¶
type BatchResponse struct {
Results map[string]json.RawMessage `json:"results"`
}
BatchResponse is the success body.
type BootValidator ¶
BootValidator fires once at gw.ListenAndServe entry. Return an error to refuse startup with a clear message — replaces the boot-panic pattern (e.g. TrustUpstreamClaims-without-seal panic becomes a validator on the trust-guard plugin).
type BreakerConfig ¶
type BreakerConfig struct {
// FailureThreshold is the number of consecutive transport failures to
// one upstream that trips its breaker open. Default 5.
FailureThreshold int
// Cooldown is how long a tripped breaker fails fast before allowing a
// single probe (half-open). Default 10s.
Cooldown time.Duration
// Disabled turns the breaker off entirely — every remote dispatch is
// attempted, as it was before the breaker existed.
Disabled bool
// FailureRateThreshold ejects a replica whose failure RATE over the recent
// window reaches this fraction (0..1), even without consecutive failures —
// catching a pod that errors intermittently (e.g. 50% 5xx), which the
// consecutive-count trip alone never notices (W1.6 outlier ejection). 0
// (default) disables rate-based ejection.
FailureRateThreshold float64
// RollingWindow is how many recent outcomes the failure rate is computed
// over. Default 20 when FailureRateThreshold > 0.
RollingWindow int
// MinRequests is the minimum window fill before rate-ejection can trip, so a
// cold breaker doesn't eject on one or two early errors. Default = window.
MinRequests int
}
BreakerConfig tunes the per-upstream circuit breaker that guards remote dispatch. Zero values take the defaults below.
type BreakerReport ¶
type BreakerReport struct {
Disabled bool `json:"disabled"`
FailureThreshold int `json:"failure_threshold"`
Cooldown string `json:"cooldown"` // human-readable duration
}
BreakerReport is the effective per-upstream circuit-breaker config.
type Capability ¶
Capability is a plugin-published, type-safe contract another plugin (or business handler) consumes. Plugin advertises capabilities via CapabilityProvider; consumers look them up with GetCapability[T] (see capability.go). Convention: Type uses "<plugin>.<contract>" namespace (e.g. "requestid.IDGenerator", "audit.Recent") so the origin is visible at a glance and collisions are rare.
type IDGenerator func() string
func (p *Plugin) Capabilities() []gateway.Capability {
return []gateway.Capability{
{Type: "requestid.IDGenerator", Impl: IDGenerator(p.generate)},
}
}
type CapabilityProvider ¶
type CapabilityProvider interface {
Capabilities() []Capability
}
CapabilityProvider opts a plugin into capability publication. gw.Use() harvests Capabilities() once at registration. Multiple plugins MAY publish the same Type — first wins on lookup, but the framework records all and surfaces them in introspect for drift detection.
type CheckParams ¶
type CheckParams struct {
Claims *Claims `json:"claims,omitempty"`
Service string `json:"service"`
Method string `json:"method"`
Perm string `json:"perm,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
}
CheckParams is the request payload the gateway sends to AuthzService.Check. Claims is nil for anonymous requests; the authz service is expected to handle that case (typically by returning {Allow:false, Authenticate:true} for non-public methods).
Check is request-aware, not claims-only (HELL-278). Two inputs beyond identity let the AuthzService decide declaratively with no side map:
- Perm: the OPAQUE authz requirement the target method DECLARED (HELL-280) — reflected from a sov:"perm=…" sentinel or the router's AuthzRequirements() marker. The gateway carries it verbatim; only the AuthzService knows what "pages:write" or "public" means. Empty when the method declared none — the AuthzService picks the default (sov bakes in none). This is the static "what the method needs".
- Headers: the caller's inbound request headers — the tenant/workspace selector (e.g. X-Workspace) the AuthzService needs to resolve a PER-SPACE grant. This is the dynamic "what the caller has, here".
Together: perm (what's needed) + claims/headers (what's granted) = a complete decision in one seam. Target-id-scoped checks that need the store (a node must live inside a subtree) still belong in the handler — Check gets headers, deliberately NOT decoded args.
type Claims ¶
Claims is the verified caller identity. Aliased to rpc.Claims so handlers can read identity via ctx.Claims() without importing the gateway package. The wire shape (X-Sov-* headers, seal) is owned by the gateway; the Go type lives in rpc/.
func ClaimsFromContext ¶
ClaimsFromContext returns the gateway-stamped Claims, or nil. Prefer ctx.Claims() in handlers; this helper exists for callers that already have a *rpc.Context and don't want the method syntax.
func ClaimsFromHeaders ¶
ClaimsFromHeaders parses X-Sov-* headers into a *Claims. Returns nil if no X-Sov-Subject is present. Typed accessor — downstream services reach for this instead of `h.Get(gateway.HeaderSubject)` + string gymnastics. Pair with the hmacseal/proto.Verify call when an HMAC secret is configured so forged claim headers can be rejected.
type ClaimsCache ¶
type ClaimsCache interface {
// Get returns cached Claims for token, or ok=false on miss.
Get(token string) (claims *Claims, ok bool)
// Put stores Claims for token. Honor claims.ExpiresAt for eviction.
Put(token string, claims *Claims)
}
ClaimsCache caches verified Claims keyed by the raw bearer token so the gateway skips AuthService.verify on repeat requests. The default impl is in-memory (per-replica). Implement this and pass WithClaimsCache to back it with Redis/memcached so a fleet of gateway replicas shares verified results instead of each re-verifying.
SECURITY: the token is a secret. Never log it. In a shared/remote store, hash the key (e.g. SHA-256) rather than storing raw tokens, and set the entry TTL from claims.ExpiresAt so a cached identity can't outlive the token. The gateway independently re-checks ExpiresAt after Get, so a stale entry is never honored even if an impl forgets to expire it.
type Client ¶
Client is the cross-service caller a router uses to invoke another service's method. Two implementations satisfy it:
gw.LocalClient() — calls back into the same gateway's dispatch in-process. No HTTP loopback, no JSON round-trip; the engine handles the call directly. Right answer for monolith mode.
gateway.NewClient(baseURL) — HTTP POST to a remote gateway. Right answer for mesh pods making cross-service calls.
Both auto-forward the verbatim inbound Authorization header so identity propagates across hops without manual header plumbing. Consumer code calls the same Call signature in either topology — that is the PEMM "wire IS the in-process API" contract made literal.
func NewClient ¶
func NewClient(baseURL string, opts ...ClientOption) Client
NewClient returns a Client that POSTs /rpc/{router}/{method} against baseURL. The base URL has no trailing slash and no /rpc/ suffix.
type ClientOption ¶
type ClientOption func(*httpClient)
ClientOption mutates a *httpClient.
func WithHTTPClient ¶
func WithHTTPClient(c *http.Client) ClientOption
WithHTTPClient overrides the underlying *http.Client.
type ConfigApplier ¶
ConfigApplier lets a plugin mutate the gateway's framework-owned configuration at registration time. Fires synchronously inside gw.Use BEFORE any other hook is registered, so a config-applying plugin (mesh secret, allowlist, etc.) takes effect for every subsequent request — including ones served while later plugins are still being registered.
type ConfigReport ¶
type ConfigReport struct {
MaxInFlight int64 `json:"max_in_flight"` // load-shed cap; 0 = unlimited
InFlight int64 `json:"in_flight"` // requests currently in the dispatch chain
AdvertiseURL string `json:"advertise_url,omitempty"`
TrustUpstream bool `json:"trust_upstream_claims"`
IntrospectExposed bool `json:"introspect_exposed"`
AuthConfigured bool `json:"auth_configured"`
AuthzConfigured bool `json:"authz_configured"`
CustomClaimsCache bool `json:"custom_claims_cache"` // a non-default ClaimsCache is installed
RemoteBreaker BreakerReport `json:"remote_breaker"`
Plugins []string `json:"plugins"`
Routers []string `json:"routers"`
}
ConfigReport is the sanitized /rpc/_config body: the gateway's effective operational knobs plus the set of wired plugins and locally-served routers.
SANITIZED BY CONSTRUCTION. It names only scalar tuning knobs and plugin/ router NAMES — never a plugin's Config. Mesh secrets, register tokens, TLS keys and bearer material live inside individual plugins and are never read here, so no secret, key, or token can appear in the report. Adding a field that reaches into a plugin's config would break that invariant.
type Conflict ¶
type Conflict struct {
Role RoleFlag // RoleAuth | RoleAuthz, zero if federation
FederatedAddrs [2]string // [old, new] for federation case; zero strings if role case
}
Conflict carries the case discriminator for MeshConflictPolicy. Exactly one of Role / FederatedAddrs is populated at any call site:
- Role != 0 means role-takeover (FederatedAddrs is zero strings).
- FederatedAddrs[0] != "" means federation preemption (Role is zero); FederatedAddrs is [oldAddr, newAddr].
type ContextContributor ¶
ContextContributor lets a plugin stash per-request metadata onto the *rpc.Context that the in-process engine hands to local handlers. Fires in dispatchLocal AFTER the framework's own stashes (Authorization, Claims, path) but BEFORE engine.Dispatch.
The symmetric counterpart of HeaderInjector for the local path — HeaderInjector adds bytes to outbound HTTP requests; this adds values to in-process ctx. A plugin that wants its metadata available in BOTH paths (request-id, trace-id, tenant) implements both. Plugin owns the ctx-key namespace; conventionally "sov.<plugin>.<field>".
func (p *Plugin) ContributeContext(ctx *rpc.Context, req *gateway.Request) {
ctx.Set("sov.requestid", req.Header.Get("X-Sov-Request-Id"))
}
type DispatchEvent ¶
type DispatchEvent struct {
Router string `json:"router"`
Method string `json:"method"`
Path string `json:"path"`
Status int `json:"status"`
Duration time.Duration `json:"duration_ns"`
Subject string `json:"subject,omitempty"`
RemoteIP string `json:"remote_ip,omitempty"`
RequestID string `json:"request_id,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
BatchID string `json:"batch_id,omitempty"`
At time.Time `json:"at"`
// Mode records where the call actually ran. PEMM observability —
// audit + metrics + tracing can split rates by where the work
// landed without instrumenting the dispatcher itself.
//
// Values:
// "local" — in-process router method handled it
// "remote" — proxied to a single remote address
// "federated" — proxied via an intermediate gateway
// "framework" — framework endpoint (/rpc/_health, _introspect, …)
// "plugin" — handled by a RouteHandler plugin
// "" — pre-dispatch reject (404, path validation, etc.)
Mode string `json:"mode,omitempty"`
}
DispatchEvent is the post-handler observation the gateway emits to every DispatchHook. JSON-tagged so plugins can marshal events directly without further translation.
type DispatchHook ¶
type DispatchHook interface {
OnDispatch(ev DispatchEvent) error
}
DispatchHook fires AFTER a handler returns (success or error). Sees the resolved router/method, status, duration, and identity. Runs on the dispatch goroutine; long-running work belongs on a buffered chan the plugin owns (otherwise you slow down every request).
type Endpoint ¶
type Endpoint struct {
Local bool
RemoteAddr string // e.g. "http://widgets-pod-7:8080" — no trailing slash
Peer Handler // when set, dispatch via this in-process Handler (nested PEMM)
}
Endpoint is what a resolver returns. Three shapes:
- Local=true: dispatch against the in-process engine
- Peer non-nil: dispatch against another *Gateway in the same binary (nested PEMM, no HTTP hop)
- RemoteAddr set: HTTP-proxy to that base URL
Peer takes precedence over Local; RemoteAddr is the fallback.
type EndpointPicker ¶
type EndpointPicker interface {
Pick(service string, live []RegisterEntry) (RegisterEntry, bool)
}
EndpointPicker chooses ONE replica from a service's live set on the dispatch hot path. The default is round-robin; supply your own (least-connections, zone-aware, sticky) via WithEndpointPicker. live is sorted by Address, so a stateless picker can index it deterministically. Must be safe for concurrent use and cheap — it runs per request.
type EntryOptions ¶
type EntryOptions struct {
// Introspectable, when true, lets the gateway probe this remote
// pod's /rpc/_introspect on aggregation. When false (default), the
// remote stays out of the catalog — useful for services that
// haven't enabled introspection or want to opt out of the
// org-wide type browser.
Introspectable bool
}
EntryOptions configure a RegisterResolver entry.
type FieldDivergence ¶
type FieldDivergence struct {
Field string `json:"field"` // JSON field name
Types []string `json:"types"` // schema type per variant, aligned to Variants; "" = absent
Diverges bool `json:"diverges"` // true when not identical across all variants
}
FieldDivergence is one row of a drift diff: a field's type across the variants.
type Gateway ¶
type Gateway struct {
// contains filtered or unexported fields
}
Gateway is the entry point. It wraps a rpc.Engine (services in-process) and a Resolver chain (services elsewhere) behind a pluggable Server. The same Gateway can do both — that is the PEMM thesis the framework promises.
func New ¶
New constructs a Gateway. All Options are optional — the bare gateway.New() returns a usable standalone gateway with sensible defaults. The gateway always owns its rpc.Engine internally; reach it via g.Engine() only for power-user escape hatches.
New panics on an Option carrying config it cannot honor — e.g. WithAdvertiseURL with an unparseable URL — so a misconfigured gateway fails loudly at construction rather than silently mis-routing later.
func (*Gateway) AllCapabilities ¶
func (g *Gateway) AllCapabilities() map[string][]Capability
AllCapabilities returns every registered capability grouped by Type — useful for diagnostics that surface collisions (more than one plugin publishing the same Type). Order of entries within a slice matches registration order.
func (*Gateway) AuthBinding ¶
func (g *Gateway) AuthBinding() *AuthBinding
AuthBinding returns the current auth binding (nil if unbound). Registry plugin reads this to detect cross-name role conflicts on /rpc/_register.
func (*Gateway) Authorize ¶
func (g *Gateway) Authorize(ctx context.Context, claims *Claims, router, method string, headers Header) error
Authorize runs the bound AuthzService against (claims, router, method) with the method's DECLARED perm (engine.Perm) and the caller's headers — the same gate authzMiddleware applies on /rpc. Returns nil when allowed, or when no authz service is bound. A surface that dispatches a method OUTSIDE the /rpc middleware chain — MCP tools routing through Dispatch — calls this so authz applies exactly as it would over /rpc.
func (*Gateway) AuthzBinding ¶
func (g *Gateway) AuthzBinding() *AuthzBinding
AuthzBinding returns the current authz binding (nil if unbound).
func (*Gateway) BreakerSnapshot ¶
BreakerSnapshot returns each known upstream's circuit-breaker state as an int (0=closed, 1=open, 2=half-open) for observability — e.g. a metrics gauge or a health rollup. An upstream that has never failed is absent (implicitly closed).
func (*Gateway) BuildProxyRequest ¶
func (g *Gateway) BuildProxyRequest(ctx context.Context, method, addr, path string, body []byte, parent *Request) (*http.Request, error)
BuildProxyRequest constructs an outbound HTTP request to addr+path pre-populated with the parent request's forwarded headers, the injected X-Sov-* claim bundle, the forwarded-for IP, and every registered HeaderInjector. Plugin authors (batch, custom proxy) use this so their outbound calls participate in the same header-injection chain as the framework's own dispatchRemote.
func (*Gateway) CodecFor ¶
CodecFor returns the business codec negotiated for req by its Content-Type, or the JSON default when none/unknown. Exposed (HELL-286) so a codec-aware plugin can encode per-entry values with the same codec the caller used, instead of hardcoding JSON.
func (*Gateway) ConfigExposed ¶
ConfigExposed reports whether the public /rpc/_config endpoint is open.
func (*Gateway) ConfigReportBody ¶
func (g *Gateway) ConfigReportBody() ConfigReport
ConfigReportBody builds the sanitized runtime-config report. Exported so an operator UI (e.g. the explorer) can render it in-process without opening the public endpoint — mirroring IntrospectBody.
func (*Gateway) ConsumeFederationPreemption ¶
ConsumeFederationPreemption notifies registered MeshConflictPolicy plugins that a preempted federation rule has been consumed (the takeover succeeded). Plugins drop the rule they own.
func (*Gateway) Dispatch ¶
Dispatch is the mesh fabric. It resolves req's service and routes the call to wherever that service lives — the local engine, an in-process peer gateway, or a remote pod over HTTP — and returns the response. It is protocol-agnostic: ANY surface (the /rpc adapter, MCP tools, batch) builds a Request whose Path is /rpc/{service}/{method} and calls Dispatch; the surface never knows or cares whether the service is local or across the mesh. That single seam is what lets any surface mesh with no surface-specific routing code — a tool whose service is federated to another node just resolves remote here.
Dispatch does NOT run the auth/authz middleware: a caller that reaches it OUTSIDE the HTTP chain (an MCP tool call, an internal fan-out) owns identity (set req.User) and authorization (call Authorize first). The /rpc HTTP path still runs the full middleware chain before landing here via the rpc surface.
func (*Gateway) DispatchResolved ¶
func (g *Gateway) DispatchResolved(ctx context.Context, req *Request, router, method string) *Response
DispatchResolved is Dispatch for a caller that has ALREADY parsed req.Path into router + method — the rpc surface, for one, parses to apply its POST/reserved policy, so it hands the parts straight here instead of re-splitting.
INVARIANT: router and method MUST equal SplitRPCPath(req.Path). Dispatch resolves and dispatches by the router/method ARGUMENTS, but dispatchLocal stashes req.Path verbatim as ContextKeyPath — pass mismatched values and a handler (or an audit/tenant plugin) reading ctx path sees a lie. Callers that haven't parsed the path should use Dispatch. See Dispatch for routing.
func (*Gateway) Engine ¶
Engine returns the underlying rpc.Engine — an escape hatch for power users. Normal consumers register via g.Register / g.RegisterRemote and never need this.
func (*Gateway) ExposeConfig ¶
func (g *Gateway) ExposeConfig()
ExposeConfig opens the opt-in /rpc/_config endpoint. Called by the builtin configdump plugin's Apply; off by default because the report, while sanitized, still discloses operational topology. Idempotent.
func (*Gateway) ExposeIntrospect ¶
func (g *Gateway) ExposeIntrospect()
ExposeIntrospect opens the PUBLIC /rpc/_introspect endpoint. Called by the builtin introspect plugin's Apply; the endpoint is off by default because the catalog discloses the full API surface. Idempotent.
func (*Gateway) FederatedCatalog ¶
func (g *Gateway) FederatedCatalog(ctx context.Context) *IntrospectReport
FederatedCatalog returns the aggregated introspect report — this gateway's local engine plus every remote service the registry aggregator merged in — or nil on failure. A surface builtin uses it to enumerate the routers it serves across the whole mesh, then filters by RouterDescriptor.HasSurface.
func (*Gateway) Handle ¶
Handle dispatches one Request through the full middleware chain and returns the resulting Response. Useful for tests and for embedding the gateway inside a custom Server implementation that wants to call dispatch directly rather than going through Server.Handle.
func (*Gateway) InFlight ¶
InFlight is the number of requests currently being handled. Counted at the outermost middleware, so it spans the whole dispatch chain.
func (*Gateway) IntrospectBody ¶
IntrospectBody builds (and caches) the introspect catalog body for req, independent of whether the PUBLIC endpoint is exposed. The explorer and federation aggregators call this directly so they get the catalog in-process without requiring /rpc/_introspect to be open. req may carry the X-Sov-Introspect-Internal / -Trace / -Visited headers.
func (*Gateway) IntrospectExposed ¶
IntrospectExposed reports whether the public /rpc/_introspect endpoint is open (i.e. the introspect plugin was used).
func (*Gateway) InvalidateCatalog ¶
func (g *Gateway) InvalidateCatalog()
InvalidateCatalog drops the cached aggregated _introspect body so the next probe rebuilds. Registry plugin calls this after a /rpc/_register write.
func (*Gateway) JoinMesh ¶
func (g *Gateway) JoinMesh(ctx context.Context, opts MeshOptions) error
JoinMesh starts the pod: registers with the upstream gateway, heartbeats on the configured interval, and serves until ctx is cancelled. Combines what the previous demo's hand-coded heartbeat() + ListenAndServe() did into one call.
JoinMesh blocks until ctx is cancelled or the server returns an error. The heartbeat goroutine stops when ctx is cancelled.
func (*Gateway) LinkPeer ¶
LinkPeer wires another in-process *Gateway as the dispatch target for the listed service names. Calls bypass HTTP entirely — peer's Handle is invoked directly on the dispatch path. Nested PEMM in one binary: same handler code, different gateway. Folds what the retired localpeer plugin did into the core API.
pub := sov.New()
pub.Register(&PublicRouter{})
admin := sov.New()
admin.Register(&AdminRouter{})
pub.LinkPeer(admin, "Admin")
Service names should NOT collide with the host gateway's own routers — the LocalResolver wins the chain.
SCOPE — dispatch only, not introspection. A LinkPeer'd peer is a dispatch target: /rpc calls (and anything that routes through Dispatch) resolve to it. It is NOT merged into this gateway's /rpc/_introspect catalog (peerResolver.Introspectables returns nil), so introspection-driven surfaces — MCP tools/list, the explorer, upward federation — do NOT see a peer's routers. To expose a node's routers as MCP tools ACROSS a boundary, use registry/remote federation (JoinMesh): each node runs its own mcp builtin and tags its own tool routers, and the tag federates on the descriptor. In-process peer introspection merge is a known gap (HELL-295).
func (*Gateway) ListenAndServe ¶
ListenAndServe starts the underlying server. Returns when ctx is cancelled or the server returns an error.
Plugin lifecycle: every registered BootValidator runs first; the first error refuses startup. Then every LifecycleHook.OnStart runs. On context cancel or server return, LifecycleHook.OnStop runs in reverse registration order.
func (*Gateway) LocalClient ¶
LocalClient returns a Client that dispatches against this gateway directly — no HTTP, no JSON encoding overhead. The resolver chain still decides whether a given service call is local or proxied to a remote pod, so a "local" client can transparently reach remote services too. This is the PEMM "wire IS the in-process API" point.
func (*Gateway) Log ¶
Log returns the structured logger framework + builtins should use. Returns the first-registered Logger plugin, or a slog.Default() adapter when none registered. Always non-nil — safe to call before any plugin is wired.
gw.Log().Info("plugin loaded", "name", p.PluginName())
func (*Gateway) LookupCapability ¶
LookupCapability returns the first-registered capability with the given Type, or (nil, false) when none. Most callers should use the generic GetCapability[T] for type-safe access; LookupCapability is here for introspection-style code that needs the raw any.
func (*Gateway) MustUseAll ¶
MustUseAll is the panicking variant of UseAll for main() callers that would otherwise unwrap with log.Fatal. Panics with the wrapped error so the program exits with a stack trace.
func (*Gateway) OverrideAuthBinding ¶
OverrideAuthBinding records the auth role binding, SILENTLY overwriting any existing one — the override semantics are the whole point, so the name says so. The registry plugin calls this from its /rpc/_register handler AFTER its RoleConflictPolicy check has already authorized the takeover; RegisterRemote uses it because remote registration is inherently last-writer-wins. The in-process RegisterAuth path instead uses the strict, unexported bindAuth, which panics on a conflicting binding (fail-fast for two AuthService implementers compiled into one binary).
func (*Gateway) OverrideAuthzBinding ¶
OverrideAuthzBinding is the authz equivalent of OverrideAuthBinding.
func (*Gateway) PluginByName ¶
PluginByName returns the registered plugin whose PluginName equals name, or nil if no plugin matches. Type-assert at the call site for typed access — sov stores plugins as `any` because the sub-interface they satisfy is what actually matters for dispatch.
if rid, ok := gw.PluginByName("request-id").(*requestid.Plugin); ok {
fmt.Println(rid.LastIssued())
}
PluginByName is the foundation of cross-plugin coordination — paired with PluginDependency (Requires + After) it lets one plugin opt in to behavior another plugin owns, without hard-coding import paths.
func (*Gateway) PluginInfos ¶
func (g *Gateway) PluginInfos() []PluginInfo
PluginInfos returns the PluginInfo list for this gateway's registered plugins. Order matches registration order so operators can read the wiring sequence directly. Exported so plugins (e.g. manifest) can read it without a /rpc/_introspect HTTP round trip.
func (*Gateway) PluginNames ¶
PluginNames returns the registered plugin names in registration order. Useful for diagnostics tools that don't want to walk IntrospectReport.Plugins.
func (*Gateway) PolicyAllowsRoleTakeover ¶
PolicyAllowsRoleTakeover exposes the mesh-conflict iterator for the role-takeover path. Registry plugin calls this when a different service name tries to claim an auth/authz role another service already holds. Defers to registered MeshConflictPolicy plugins.
func (*Gateway) PreParserHeader ¶
PreParserHeader returns the header state captured at gateway ingress — before any HeaderParser plugin mutated req.Header — falling back to req.Header when no snapshot was taken (no registered method uses header= params). A surface that AUTHORIZES or BINDS header= params off an inbound request should use this so its view matches the /rpc surface, which authorizes and binds pre-parser. See docs/HEADER_PARAMS.md.
func (*Gateway) PreemptFederation ¶
PreemptFederation exposes the mesh-conflict iterator for the federation-preemption path. Registry plugin calls this when a federated _register tries to claim a wire name already federated by a different address. Defers to registered MeshConflictPolicy plugins.
func (*Gateway) ProxyClient ¶
ProxyClient returns the http client used for outbound proxy hops. Registry plugin uses this for the introspect/health fan-out so the timeouts and TLS config stay consistent with business calls.
func (*Gateway) RecordDispatch ¶
func (g *Gateway) RecordDispatch(req *Request, router, method, path string, resp *Response, started time.Time)
RecordDispatch emits a DispatchHook event (audit, metrics) for a call a surface dispatched OUTSIDE the /rpc HTTP path — an MCP tools/call, say — so observability plugins see per-call router/method/status/subject just as they do for /rpc, instead of only the opaque outer request. Status, error code, and Mode are read from resp; subject from req. No-op when resp is nil.
func (*Gateway) Register ¶
Register adds an in-process router to the gateway's engine. If the router happens to satisfy AuthService or AuthzService, the gateway auto-binds the corresponding role — same effect as RegisterAuth / RegisterAuthz but without the consumer having to know which routers hold which roles. Mesh pods self-declare via Roles on JoinMesh; monolith mode discovers the role by interface implementation. Both paths land in bindAuth / bindAuthz.
Boot-panics if two distinct routers implement the same role interface — see bindAuth / bindAuthz for the message.
func (*Gateway) RegisterAuth ¶
func (g *Gateway) RegisterAuth(svc AuthService)
RegisterAuth registers a typed AuthService as the gateway's auth verifier AND adds it to the engine. Compile-time check via the AuthService interface replaces the prior boot-time panic on magic-method-name + signature mismatches.
Equivalent to gw.Register(svc) — the plain Register path auto-detects AuthService implementers and binds them. Keep this form when you want the intent explicit in main(); use Register when the wiring is generic.
func (*Gateway) RegisterAuthz ¶
func (g *Gateway) RegisterAuthz(svc AuthzService)
RegisterAuthz is the authz equivalent of RegisterAuth.
func (*Gateway) RegisterRemote ¶
func (g *Gateway) RegisterRemote(name, address string, ttl time.Duration, opts ...RemoteOptions)
RegisterRemote hand-registers a remote service in the resolver chain. In-process equivalent of a remote pod POSTing /rpc/_register. ttl is how long the entry stays alive without a refresh.
Pass RemoteOptions to bind auth/authz roles AND/OR opt into /rpc/_introspect aggregation in the same call — registering an auth service remotely now matches the in-process RegisterAuth shape so the call site can switch deployment modes without touching the registration code:
// In-process:
gw.RegisterAuth(&auth.AuthRouter{...})
// Remote (mesh):
gw.RegisterRemote("Auth", "http://auth-pod:9001", time.Minute,
RemoteOptions{Auth: true, Verify: "verify"})
func (*Gateway) RegisterResolver ¶
func (g *Gateway) RegisterResolver() *RegisterResolver
RegisterResolver returns the gateway's register-based remote resolver so callers can pre-populate entries or Close on shutdown.
func (*Gateway) Resolver ¶
Resolver returns the resolver chain so the registry plugin can check whether a wire name is already federated by another address.
func (*Gateway) Run ¶
Run is the ergonomic main() helper: installs SIGINT/SIGTERM handling that cancels the supplied ctx, then calls ListenAndServe. On signal, the gateway's LifecycleHook.OnStop fires and the server gracefully drains in-flight requests. Returns nil on clean shutdown, error on boot/serve failure.
func main() {
log.Fatal(sov.NewMonolith(cfg).Run(context.Background(), ":8080"))
}
func (*Gateway) RunMesh ¶
func (g *Gateway) RunMesh(ctx context.Context, opts MeshOptions) error
RunMesh is the pod equivalent of Run — JoinMesh + SIGINT/SIGTERM graceful shutdown. Returns nil on signal-driven shutdown so callers can `log.Fatal(gw.RunMesh(ctx, opts))` without printing a spurious "<nil>" on Ctrl-C.
func main() {
log.Fatal(sov.NewPod(cfg).RunMesh(ctx, sov.MeshOptions{...}))
}
func (*Gateway) TagSurface ¶
func (g *Gateway) TagSurface(report *IntrospectReport, surfaceName string, pred func(rpc.RouterInfo) bool)
TagSurface stamps surfaceName onto every LOCAL router (in this gateway's engine) that satisfies pred, within report.Services. The tag lives on the rpc.RouterDescriptor (its Surfaces field), so it FEDERATES: a parent gateway that aggregates this node's /rpc/_introspect inherits the tag. A surface builtin calls this from ContributeIntrospect so the catalog — local and aggregated — records which services speak the surface, and a remote surface can discover them (see FederatedCatalog).
// in the mcp builtin's ContributeIntrospect: g.TagSurface(report, "mcp", rpc.Implements[ToolRouter]())
func (*Gateway) Use ¶
Use registers a plugin on the gateway. The argument is `any` because plugins are duck-typed: the gateway checks each sub-interface (HeaderInjector, AuthTranslator, …) via Go interface assertion and stashes pointers in the appropriate slot list.
If the plugin ALSO has RPC-shaped methods (matching the same signature contract gw.Register requires), they are registered on the engine — one Use call yields both extension hooks and wire-callable methods.
Use is safe to call before or after ListenAndServe. Plugins added post-start participate in subsequent requests; existing in-flight requests are not retroactively wrapped.
Returns an error when the plugin satisfies NO sub-interface AND has no RPC methods — that's almost certainly a bug (the caller probably forgot to make the methods exported, or passed the wrong type).
func (*Gateway) UseAll ¶
UseAll calls Use on each plugin in order. First error stops the chain so config-applying plugins that need to succeed before later plugins fail fast. Preset packages return []any slices that pair with this helper:
gw := sov.New()
if err := gw.UseAll(preset.Monolith()...); err != nil {
log.Fatal(err)
}
func (*Gateway) UseMiddleware ¶
func (g *Gateway) UseMiddleware(mw Middleware)
UseMiddleware appends a raw Middleware closure to the dispatch chain. The plugin-shaped `Use(any)` is preferred for new code; this is the back-compat path for the closure form. May be called before or after ListenAndServe.
type Header ¶
Header is a transport-neutral header map. Multi-value headers are represented as comma-joined strings — the gateway never needs more.
func (Header) Clone ¶
Clone returns a shallow copy of h. Use it when a request's header map is handed to concurrent sub-dispatches (e.g. batch fan-out): plugins like requestid MUTATE req.Header, so parallel handlers must each get their own map or they race on the shared one (fatal concurrent map access). Returns nil for a nil receiver.
func (Header) Get ¶
Get returns the value for key, case-insensitively. It looks up the canonical form (http.CanonicalHeaderKey) first — which is how inbound headers and Set store keys — then falls back to an exact match so a literal-constructed Header still resolves. Callers no longer need to try both "Authorization" and "authorization".
type HeaderClaimer ¶
type HeaderClaimer interface {
ClaimedHeaders() []string
}
HeaderClaimer lets a plugin declare which inbound HTTP header names it OWNS — those headers bypass the framework's X-Sov-* identity- strip and reach req.Header intact. Without this, a plugin that reads e.g. X-Sov-Register-Sig (mesh-secret) would never see it because the edge strip nukes every X-Sov-*.
Names are http.CanonicalHeaderKey-compared. Identity headers (X-Sov-Subject/Issuer/Scopes/Expires/Seal) cannot be claimed — the strip honors anti-smuggling regardless. Other X-Sov-* and arbitrary-named headers are passable.
func (p *Plugin) ClaimedHeaders() []string {
return []string{"X-Sov-Register-Sig", "X-Sov-Register-Ts"}
}
type HeaderInjector ¶
type HeaderInjector interface {
InjectHeaders(ctx context.Context, req *Request, hreq *http.Request) error
}
HeaderInjector fires before every OUTBOUND proxy hop (dispatchRemote, dispatchRemoteBatch, federated introspect/health probes). Add headers to hreq; do NOT mutate ctx or body.
type HeaderParser ¶
HeaderParser fires on every INBOUND request, after the trust guard has decided whether to keep X-Sov-* headers. Read non-standard headers off req.Header and stash interpreted values on req.User / req.Header via mutation. Returning a non-nil *rpc.Error short-circuits dispatch with that error — use sparingly; most parsers should be lossy.
type HealthAggregator ¶
type HealthAggregator interface {
AggregateHealth(ctx context.Context, report *HealthReport) error
}
HealthAggregator lets a plugin merge remote-pod health probes into the framework's local /rpc/_health report. Framework calls every registered aggregator AFTER building the local report (gateway + in-process services) and BEFORE marshalling. Plugin mutates *HealthReport directly — services map may be extended; the top-level Status field should be downgraded ONLY if a probed remote returned degraded/unhealthy.
type HealthGateway ¶
type HealthGateway struct {
Status string `json:"status"`
}
HealthGateway is the gateway's own health stanza.
type HealthReport ¶
type HealthReport struct {
Status string `json:"status"`
CheckedAt time.Time `json:"checked_at"`
Gateway HealthGateway `json:"gateway"`
Services map[string]HealthService `json:"services"`
}
HealthReport is the aggregated /_health JSON body.
type HealthService ¶
type HealthService struct {
Status string `json:"status"`
Local bool `json:"local"`
Source string `json:"source,omitempty"` // RemoteAddr when not local
Detail string `json:"detail,omitempty"` // populated on failure
Children map[string]HealthService `json:"children,omitempty"` // populated for federated tier rollup
}
HealthService is one service's reported health.
Status taxonomy:
- healthy — service responded green.
- degraded — federated team gateway responded 207 (some-but-not-all of its children are healthy); see Children for the per-tier rollup.
- unhealthy — gateway/pod reachable but reported failure (5xx).
- unknown — registered but no liveness signal (4xx, no _health).
- missing — network unreachable / connection refused.
type HookFailure ¶
type HookFailure struct {
HookName string
PluginName string
// Err is the returned error OR a wrapped panic. Use HaltErr /
// RespondErr sentinel checks (errors.As) to read the plugin's
// intent.
Err error
// Panic is the recovered value when this failure came from a
// panic; nil when the hook returned an error normally.
Panic any
// Stack is the goroutine stack trace at the panic site, only
// populated when Panic != nil.
Stack []byte
}
HookFailure carries everything a RecoveryHandler needs to react to a hook failure — panic recovered or error returned. The reaction is derived from the returned error: HaltErr → halt boot, RespondErr → override the response, anything else → log + continue. Panics are treated as continue by default; the RecoveryHandler can re-classify.
type IntrospectContributor ¶
type IntrospectContributor interface {
ContributeIntrospect(ctx context.Context, report *IntrospectReport, trace string, visited []string) error
}
IntrospectContributor lets a plugin contribute to /rpc/_introspect. Fires after the framework builds the local report. Plugins may:
- decorate the report with their own block (cache stats, ring state)
- fan out to remote pods and merge their descriptors
Use ctx and the cascade headers (trace, visited) for loop guarding when probing remotes. visited carries the normalized address list of gateways already visited on this introspect cascade; plugins MUST append themselves before fanning out and MUST skip any address already in visited.
Replaces the prior IntrospectAugmenter (no-arg, local decoration) and IntrospectAggregator (ctx + cascade headers, remote fan-out) interfaces — both folded under one name. Plugins that don't need ctx/trace/visited just ignore those args.
type IntrospectReport ¶
type IntrospectReport struct {
Services map[string][]rpc.RouterDescriptor `json:"services"`
Types map[string]TypeDescriptor `json:"types"`
CrossRefs map[string]TypeVariants `json:"cross_refs"`
// Plugins lists every plugin registered on this gateway via
// gw.Use(). Populated for the ROOT introspect call only;
// federated probes skip it so per-tier visibility stays clean.
Plugins []PluginInfo `json:"plugins,omitempty"`
// BoundaryWarnings flags data-ownership smells found while inferring
// type ownership — currently, any type returned by more than one
// service (ambiguous producer). Empty when boundaries are clean.
BoundaryWarnings []string `json:"boundary_warnings,omitempty"`
}
IntrospectReport is the aggregated /_introspect JSON body.
Services keys each declared router → its descriptors (one or more when the same name appears in multiple pods — drift!).
Types flattens every Go type appearing in any service's request or response into one map. UsedBy on each TypeDescriptor records every (service, method, role) the type appears in.
CrossRefs is the drift radar: any type name that surfaced in multiple services with differing ShapeHashes lands here, grouped by hash. Empty when every consumer agrees on a single shape.
type LifecycleHook ¶
type LifecycleHook interface {
OnStart(ctx context.Context) error
OnStop(ctx context.Context) error
}
LifecycleHook fires on gw.ListenAndServe start/stop. Use for background goroutines, connection pools, shutdown drains. OnStart runs after BootValidator passes; OnStop runs on context cancel.
type LocalResolver ¶
type LocalResolver struct {
// contains filtered or unexported fields
}
LocalResolver wraps an in-process Engine. Hit when the requested router is registered locally. Every locally-registered router is always introspectable — the Describe path is in-process and cheap.
func NewLocalResolverFunc ¶
func NewLocalResolverFunc(hasRouter func(name string) bool, routers func() []string) *LocalResolver
NewLocalResolverFunc lets the gateway pass plain lookup closures so we don't have to import rpc inside this file (resolver.go stays dependency-free; the engine-aware glue lives in gateway.go).
func (*LocalResolver) Introspectables ¶
func (l *LocalResolver) Introspectables() []string
Introspectables implements Resolver. Local routers are always opt-in.
func (*LocalResolver) Services ¶
func (l *LocalResolver) Services() []string
Services implements Resolver.
type LogLevel ¶
type LogLevel string
LogLevel is the level passed to Logger.Log. Standard four — Debug, Info, Warn, Error — covering 95% of consumer needs without forcing a specific log lib's enum. Plugins implementing Logger map these to whatever level convention they use.
type Logger ¶
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
}
Logger is the structured logging contract. Plugins that satisfy this become the gateway-wide logger sink — framework + every builtin route Log() calls through it.
Signature is slog-compatible (Debug/Info/Warn/Error all take `(msg, args...)` — args are key-value pairs). A *slog.Logger satisfies Logger directly. Other libs (zap, logrus, custom) need a tiny adapter (4 methods).
First registered Logger wins. When none, gw.Log() returns a slog adapter backed by slog.Default() so log lines still land somewhere.
type MeshConflictPolicy ¶
type MeshConflictPolicy interface {
AllowMeshConflict(current, candidate string, c Conflict) bool
ConsumeConflict(name string, c Conflict)
}
MeshConflictPolicy decides whether an inbound /rpc/_register may take over an existing claim. Two paths share the interface:
- Role takeover: name already bound to an auth/authz role; the framework calls AllowMeshConflict with (current, candidate, Conflict{Role: <RoleAuth|RoleAuthz>}) and uses the result verbatim.
- Federation preemption: name already federated by a different address; the framework calls AllowMeshConflict with (current, candidate, Conflict{FederatedAddrs: [old, new]}) — "current" is the wire name, "candidate" is the same wire name, and the address pair is in FederatedAddrs.
Framework iterates registered policies; first true wins. Default (no policy registered, or all return false) is deny — handleRegister returns 409 ROLE_CONFLICT / SERVICE_CONFLICT.
ConsumeConflict fires AFTER the takeover succeeds so the plugin can drop the rule it owns (one-shot preemption map cleanup, audit log). Plugins that have nothing to clean up should leave it a no-op.
Replaces the prior RoleConflictPolicy + FederationPreemptionPolicy pair — both folded under one name with Conflict carrying the case discriminator.
type MeshOptions ¶
type MeshOptions struct {
// UpstreamURL is the base URL of the central registry, e.g.
// "http://gateway:8080". Required.
UpstreamURL string
// Address is the local listen address (":9001"). Required.
Address string
// Advertise is the URL the upstream gateway will reach this pod on
// (e.g. "http://auth-pod:9001"). Required.
Advertise string
// ServiceName, if set, overrides the pod's advertised wire name.
// Defaults to the wire name of the single router registered on the
// gateway — i.e. for a pod hosting just AuthRouter, defaults to
// "Auth". When the pod hosts multiple routers, ServiceName MUST be
// set explicitly.
ServiceName string
// Heartbeat is the registration refresh interval. Default 5s.
// Upstream TTL is heartbeat × 3.
Heartbeat time.Duration
// Roles is the bit set of gateway-level roles to self-declare.
// Use RoleAuth | RoleAuthz combos.
Roles RoleFlag
// HTTPClient overrides the http.Client used for /_register POSTs.
HTTPClient *http.Client
// Introspectable, when true, opts this pod into the central
// gateway's /rpc/_introspect aggregation. Default false (zero
// value) keeps surface minimal; chirp mesh sets it true so the
// type catalog spans every pod.
Introspectable bool
// MeshSecret, when non-nil, signs every _register POST with
// HMAC-SHA256 + a current timestamp. Required when the upstream
// gateway was constructed WithMeshSecret(...); the bytes must
// match. Without this, _register on a hardened gateway returns
// 401 and the pod never joins the mesh.
MeshSecret []byte
// RegisterToken, when non-empty, is stamped as the X-Sov-Register-Token
// header on every _register POST — the simple shared-bearer join gate
// (see builtin/registertoken). Required when the upstream gateway runs
// the registertoken plugin; the bytes must match. Independent of
// MeshSecret (you may set either, both, or neither).
RegisterToken []byte
// Federate, when non-empty, advertises this gateway as a tiered
// router that fronts every name in the slice. Upstream registers
// all of them at Advertise via one POST + one heartbeat. The pod
// path (single ServiceName) is bypassed when Federate is set.
//
// Use this for an EXPLICIT, fixed federation list. For a team gateway
// whose service set changes at runtime (sub-pods come and go), prefer
// FederateAll so the list can't drift from reality.
Federate []string
// FederateAll, when true, advertises this gateway's LIVE service set
// (everything its resolver currently knows — local routers + sub-pods
// registered to it), recomputed on every heartbeat. A service that
// appears is federated upstream on the next beat; one that disappears
// drops out of the heartbeat and TTL-expires upstream (~3 beats). This
// keeps the upstream's introspect + health in sync with the team
// gateway with zero hand-maintained lists. Takes precedence over
// Federate when both are set.
FederateAll bool
}
MeshOptions configures gw.JoinMesh.
type Middleware ¶
Middleware wraps the gateway's request handler. Return a non-nil Response to short-circuit dispatch; return nil to fall through to the next middleware (or the gateway's own handler).
type Middlewarer ¶
Middlewarer is the new home for plugins that wrap the dispatch chain. Equivalent to today's Middleware closure but reachable through gw.Use so it shows up in the plugin list. Existing Middleware closures continue to work via the legacy WithMiddleware option.
type NetHTTPOptions ¶
type NetHTTPOptions struct {
// MaxBodyBytes caps the request body size. 0 → default 4 MiB.
MaxBodyBytes int64
// ReadTimeout bounds the TOTAL time to read a request (headers + body).
// 0 → default 30s. ReadHeaderTimeout alone only bounds the header phase, so
// without this a client that trickles the body arbitrarily slowly (a
// slowloris body variant) holds a goroutine + connection open indefinitely.
// Raise it if you accept large bodies over slow links.
ReadTimeout time.Duration
// HTTPServer, if set, is used verbatim and Addr/timeouts are
// honored. Otherwise the constructor builds a server with sensible
// defaults using ListenAndServe's addr.
HTTPServer *http.Server
// TrustUpstreamClaims controls inbound X-Sov-* handling.
//
// false (default): strip every inbound X-Sov-* header. The right
// answer for any gateway facing the public internet — clients
// could otherwise smuggle X-Sov-Subject: admin.
//
// true: pass X-Sov-* through. The right answer for downstream
// service pods that sit behind a trusted upstream gateway which
// itself injects the verified claims (and ideally HMAC-seals
// them — pair with hmacseal/proto.Verify middleware to cryptographically
// verify rather than trust the network alone).
TrustUpstreamClaims bool
// TrustProxyHeaders controls whether the client IP (Request.RemoteIP,
// stamped into audit events and re-forwarded as X-Forwarded-For to
// remotes) is read from the inbound X-Forwarded-For header.
//
// false (default): use the transport-level peer address (r.RemoteAddr).
// Correct for any gateway a client can reach directly — X-Forwarded-For
// is attacker-controlled there, so trusting it lets a caller forge the
// source IP in your audit trail and any IP-derived policy.
//
// true: trust the left-most X-Forwarded-For hop. Set ONLY when the
// gateway sits behind a trusted L7 proxy / load balancer that
// overwrites (not appends to) X-Forwarded-For with the real client IP.
TrustProxyHeaders bool
// TLSConfig, when set, makes the server serve HTTPS (ListenAndServeTLS)
// using its Certificates — the in-memory way to terminate TLS in-process
// (self-signed dev, or a cert loaded from a secret manager). Alternatively
// set CertFile/KeyFile to load from disk. Either one switches the server
// from plaintext to TLS.
TLSConfig *tls.Config
// CertFile / KeyFile are PEM paths; when CertFile is set the server serves
// HTTPS from them. Ignored when empty (unless TLSConfig is set).
CertFile string
KeyFile string
}
NetHTTPOptions configures NetHTTPServer.
type NetHTTPServer ¶
type NetHTTPServer struct {
// contains filtered or unexported fields
}
NetHTTPServer is the default Server implementation. Production-grade defaults: 10s header timeout, 60s write timeout, configurable max body (default 4 MiB). Consumers who need different limits or TLS pass a pre-configured *http.Server via NetHTTPOptions.HTTPServer.
func NewNetHTTPServer ¶
func NewNetHTTPServer(opts NetHTTPOptions) *NetHTTPServer
NewNetHTTPServer returns a Server backed by net/http.
func (*NetHTTPServer) Handle ¶
func (s *NetHTTPServer) Handle(h RequestHandler)
Handle implements Server.
func (*NetHTTPServer) ListenAndServe ¶
func (s *NetHTTPServer) ListenAndServe(ctx context.Context, addr string) error
ListenAndServe implements Server.
func (*NetHTTPServer) SetHeaderClaim ¶
func (s *NetHTTPServer) SetHeaderClaim(fn func(canonicalName string) bool)
SetHeaderClaim wires the predicate consulted before stripping an identity-shaped header. Returning true preserves the header so a plugin that claimed it can read it from req.Header.
func (*NetHTTPServer) SetTrustGuard ¶
func (s *NetHTTPServer) SetTrustGuard(fn func(req *http.Request, body []byte) bool)
SetTrustGuard wires a predicate that decides whether to KEEP an inbound X-Sov-* bundle (only meaningful when TrustUpstreamClaims is true). False means strip the claims; the request still flows through — auth gating is the authz service's job, not the trust layer.
type Option ¶
type Option func(*Options)
Option mutates Options.
func WithAdvertiseURL ¶
WithAdvertiseURL sets the public address this gateway stamps as X-Sov-Upstream on every outbound proxy hop. Replaces the builtin/advertise plugin (folded into core).
func WithAuthCacheTTL ¶
WithAuthCacheTTL caps how long a verified bearer is trusted from the default claims cache before re-verifying — the revocation-lag bound. See Options.AuthCacheTTL.
func WithClaimsCache ¶
func WithClaimsCache(c ClaimsCache) Option
WithClaimsCache overrides the verified-claims cache (default: in-memory, per-replica). Pass a shared (e.g. Redis) implementation so gateway replicas reuse each other's AuthService.verify results. See ClaimsCache.
func WithMaxInFlight ¶
WithMaxInFlight load-sheds: concurrent requests above n get a retryable 503 OVERLOADED. 0 = unlimited. See Options.MaxInFlight.
func WithMiddleware ¶
func WithMiddleware(mw ...Middleware) Option
WithMiddleware appends middleware that wrap the dispatch. Consumer middleware runs AFTER the gateway's own auth + authz middleware (when configured) so it can rely on Claims being resolved.
func WithProxyClient ¶
WithProxyClient overrides the http.Client used to proxy remote calls.
func WithRegisterResolver ¶
func WithRegisterResolver(r *RegisterResolver) Option
WithRegisterResolver sets the resolver backing /_register. Defaults to NewRegisterResolver(5s) — pass one explicitly if you need Close() or custom eviction.
func WithRemoteBreaker ¶
func WithRemoteBreaker(cfg BreakerConfig) Option
WithRemoteBreaker tunes the per-upstream circuit breaker on remote dispatch. See Options.RemoteBreaker.
func WithResolver ¶
WithResolver sets the resolver chain. Default: a chain of LocalResolver + RegisterResolver.
func WithRetries ¶
func WithRetries(cfg RetryConfig) Option
WithRetries enables bounded automatic retry of failed remote dispatches (W1.4). See RetryConfig. cfg.MaxAttempts <= 1 leaves retry off.
func WithServer ¶
WithServer overrides the HTTP server (default: NewNetHTTPServer).
func WithTrustUpstreamClaims ¶
WithTrustUpstreamClaims is a top-level lift of NetHTTPOptions. TrustUpstreamClaims so consumers don't have to construct a server by hand. Pods set this true (the upstream gateway is trusted to inject X-Sov-*); edge gateways leave it false (strip inbound X-Sov-*).
type Options ¶
type Options struct {
Server Server // default NewNetHTTPServer
Resolver Resolver // default chain: Local + RegisterResolver
RegisterResolver *RegisterResolver // default NewRegisterResolver(5s)
ProxyClient *http.Client // default http.Client{Timeout: 30s}
Middleware []Middleware // appended after the gateway's own auth/authz middleware
TrustUpstreamClaims bool // when true, the default NetHTTPServer trusts inbound X-Sov-*
// AdvertiseURL, when set, makes the framework stamp X-Sov-Upstream
// on every outbound proxy hop so downstream pods running the
// upstreams plugin can verify the claim bundle came from a trusted
// peer. Empty disables the stamp. URL is normalized at New().
AdvertiseURL string
// ClaimsCache overrides the in-memory verified-claims cache. Supply a
// Redis/memcached-backed impl to share auth-verify results across a
// fleet of gateway replicas. Default: per-replica in-memory.
ClaimsCache ClaimsCache
// AuthCacheTTL caps how long a verified bearer is trusted from the DEFAULT
// cache before re-verifying against the auth service — the bound on
// revocation lag (and the only bound for a token with no expiry). Default
// 5m when 0. Ignored when a custom ClaimsCache is supplied (that impl owns
// its own TTL). Set very large to effectively disable the cap.
AuthCacheTTL time.Duration
// RemoteBreaker tunes the per-upstream circuit breaker on remote
// dispatch (default: 5 consecutive failures → open, 10s cooldown). Set
// RemoteBreaker.Disabled to turn it off.
RemoteBreaker BreakerConfig
// MaxInFlight load-sheds: when more than this many requests are being
// handled concurrently, further requests get an immediate retryable 503
// OVERLOADED instead of being accepted and exhausting goroutines/FDs.
// 0 (default) = unlimited.
MaxInFlight int
// Retry bounds automatic retry of a failed REMOTE dispatch (W1.4). Off by
// default; see RetryConfig + WithRetries.
Retry RetryConfig
}
Options configures a Gateway. All fields optional — defaults are sensible for the standalone-single-binary case.
type Plugin ¶
type Plugin interface {
PluginName() string
}
Plugin is the optional diagnostic marker. Types that implement at least one sub-interface (HeaderInjector, AuthTranslator, etc.) are treated as plugins even if they don't satisfy Plugin — but implementing it adds a human-readable name to the introspect "plugins" array and the explorer's Plugins tab.
type PluginDependency ¶
PluginDependency declares cross-plugin ordering constraints. gw.Use errors out when any Requires() name isn't yet registered — fail-fast on misconfiguration. After() is a soft, diagnostic hint that the plugin documents its preferred ordering; the framework records it for introspect surfacing + tooling, but does NOT reorder calls automatically (operator-controlled Use order is load-bearing for predictability).
func (p *CorsPlugin) Requires() []string { return []string{"request-id"} }
func (p *CorsPlugin) After() []string { return []string{"audit"} }
Wire cors after request-id or gw.Use returns:
gw.Use: cors requires plugin "request-id" which is not registered
type PluginDoc ¶
type PluginDoc interface {
Doc() string
}
PluginDoc is an optional companion to Plugin — implement when you want a one-paragraph description to surface in /rpc/_introspect's plugin entry (Extra["doc"]) and in `sov inspect` output. Keep it short; multi-line text is fine but the explorer renders inline.
func (p *Plugin) Doc() string {
return "Stamps X-Sov-Request-Id on every request and propagates it across hops."
}
type PluginInfo ¶
type PluginInfo struct {
Name string `json:"name"`
Hooks []string `json:"hooks"` // ["HeaderInjector", "DispatchHook"]
HasRouter bool `json:"has_router"` // true when the plugin also registered RPC methods
Requires []string `json:"requires,omitempty"` // hard deps from PluginDependency
After []string `json:"after,omitempty"` // soft ordering hint from PluginDependency
Capabilities []string `json:"capabilities,omitempty"` // Type names published via CapabilityProvider
Extra map[string]any `json:"extra,omitempty"` // per-plugin metadata (version, source URL, ...)
}
PluginInfo is the introspect-time descriptor of a registered plugin. Surfaces in IntrospectReport.Plugins; the explorer's Plugins tab renders it; drift radar uses it to flag same-name-different-version across team gateways.
type ReadinessContributor ¶
ReadinessContributor lets a plugin gate the gateway's readiness. Ready returns a non-nil error to report NOT ready (a cache still warming, mesh registration not yet confirmed, a downstream dependency unreachable). It is consulted on every /rpc/_ready probe and never by /rpc/_health — liveness and readiness are deliberately separate signals.
type RecoveryHandler ¶
type RecoveryHandler interface {
HandleHookFailure(failure HookFailure) (override *Response)
}
RecoveryHandler observes every hook failure (returned error OR recovered panic). Plugins implement it to log, sample, alert, or shape the 500-response. Framework installs a default stderr-logging handler when no plugin registers one.
The handler may return a non-nil response override; the framework uses it instead of the default 500 envelope. Return nil to accept the default. A plugin-supplied RespondErr (see plugin_errors.go) always wins over a handler override.
type RegisterEntry ¶
RegisterEntry is one service registration: where the service lives, when the registration expires, and whether it opts into introspect aggregation. The unit a RegisterStore persists. Multiple entries with the same service name but different Address are REPLICAS of that service — the resolver load-balances across them (W1.1).
type RegisterRequest ¶
type RegisterRequest struct {
Name string `json:"name"`
Address string `json:"address"`
HeartbeatInterval int `json:"heartbeat_interval_seconds"`
Auth bool `json:"auth,omitempty"`
Verify string `json:"verify,omitempty"`
Authz bool `json:"authz,omitempty"`
Check string `json:"check,omitempty"`
Introspect bool `json:"introspect,omitempty"`
// Federate, when true, advertises this gateway as a tiered router
// that fronts every name in Services. Master registers each name
// at Address. Name in this mode is a label, not a wire name.
Federate bool `json:"federate,omitempty"`
Services []string `json:"services,omitempty"`
// Deregister, when true, withdraws this pod's services from the registry
// (route removal) instead of registering. Sent best-effort on graceful
// shutdown so a dead pod's routes drop immediately instead of lingering
// until the heartbeat TTL expires (~3 missed beats). Gated exactly like a
// register (mesh secret / register token), and only removes entries this
// Address still owns — it can never yank a service a newer pod took over.
Deregister bool `json:"deregister,omitempty"`
}
RegisterRequest is the JSON body of POST /rpc/_register.
Auth+Verify, when both set, tell the gateway "this service is the auth verifier — route bearer tokens here." Authz+Check tell the gateway "this service is the policy-as-service hook." A service may declare zero, one, or both roles; non-role services just expose their own business methods.
Introspect, when true, opts the pod into the gateway's /_introspect aggregation: the gateway will probe this pod's /rpc/_introspect on catalog rebuild and merge its descriptors into the org-wide type catalog. Defaults to false on the wire so that services opt in explicitly; the chirp mesh demo sets it true.
type RegisterResolver ¶
type RegisterResolver struct {
// contains filtered or unexported fields
}
RegisterResolver holds a TTL-backed map of service → remote replicas, populated by services that POST /rpc/_register on startup and refresh via heartbeat. Reads are served from a lock-free local cache; the backing store (RegisterStore) is pluggable for multi-replica meshes.
func NewRegisterResolver ¶
func NewRegisterResolver(evictInterval time.Duration, opts ...RegisterResolverOption) *RegisterResolver
NewRegisterResolver returns a resolver with a background reaper that runs at evictInterval (defaults to 5s). Pass options to swap the store or enable cross-replica refresh.
func (*RegisterResolver) AddressGroup ¶
func (r *RegisterResolver) AddressGroup() map[string][]string
AddressGroup returns unique-address → service-names-served-at-that- address. Live replicas only (TTL respected). Used by the gateway's introspect + health cascades to probe each federated team gateway exactly once rather than once per service it fronts.
func (*RegisterResolver) Close ¶
func (r *RegisterResolver) Close()
Close stops the background reaper. Safe to call multiple times.
func (*RegisterResolver) Delete ¶
func (r *RegisterResolver) Delete(service, address string)
Delete removes ONE replica (service, address). No-op if absent.
func (*RegisterResolver) Introspectables ¶
func (r *RegisterResolver) Introspectables() []string
Introspectables implements Resolver — a service is introspectable if any of its live replicas registered with Introspectable=true.
func (*RegisterResolver) Put ¶
func (r *RegisterResolver) Put(service, address string, ttl time.Duration)
Put inserts or refreshes a service replica. Defaults to Introspectable=true for the common case (every pod opts in unless it declared otherwise on _register). Use PutEntry to set flags explicitly.
func (*RegisterResolver) PutEntry ¶
func (r *RegisterResolver) PutEntry(service, address string, ttl time.Duration, opts EntryOptions)
PutEntry inserts or refreshes a service replica with explicit options. A new address for an existing service is added as a replica; the same address refreshes in place.
func (*RegisterResolver) Resolve ¶
Resolve implements Resolver. Reads the lock-free local cache, filters out replicas whose breaker is open (unless all are open), and picks one via the EndpointPicker.
func (*RegisterResolver) Services ¶
func (r *RegisterResolver) Services() []string
Services implements Resolver — every service with at least one live replica.
type RegisterResolverOption ¶
type RegisterResolverOption func(*RegisterResolver)
RegisterResolverOption configures a RegisterResolver.
func WithEndpointPicker ¶
func WithEndpointPicker(p EndpointPicker) RegisterResolverOption
WithEndpointPicker overrides the replica-selection strategy (default round-robin). See EndpointPicker.
func WithRegisterRefresh ¶
func WithRegisterRefresh(d time.Duration) RegisterResolverOption
WithRegisterRefresh makes the resolver pull store.Snapshot() every d to pick up registrations made on OTHER replicas against a shared store. Only one replica receives a given pod's heartbeat, so the rest must poll to converge. 0 (default) = local-only (single replica / in-memory).
func WithRegisterStore ¶
func WithRegisterStore(s RegisterStore) RegisterResolverOption
WithRegisterStore overrides the backing store (default in-memory). Pass a shared (e.g. Redis) store so gateway replicas share one mesh view.
type RegisterResponse ¶
type RegisterResponse struct {
OK bool `json:"ok"`
TTL int `json:"ttl_seconds"`
ForceIntrospect bool `json:"force_introspect,omitempty"`
}
RegisterResponse is the success body. ForceIntrospect, when true, signals the pod that the gateway has the explorer plugin (or other catalog consumer) installed and the pod's entry was force-flipped to Introspectable=true server-side regardless of what the pod requested. The pod SHOULD update its in-memory mesh options so future heartbeats / re-registers stay consistent.
type RegisterStore ¶
type RegisterStore interface {
Put(service string, e RegisterEntry)
Delete(service, address string)
Snapshot() map[string][]RegisterEntry
ReapExpired(now time.Time) (changed bool)
}
RegisterStore is the pluggable backing store for the registry's service→replicas map. Default is in-memory, per replica. Supply a shared impl (e.g. Redis) via WithRegisterStore so a FLEET of gateway replicas shares one mesh view — otherwise each replica only knows the pods whose heartbeat happened to land on it (partial-view routing/health drift).
A service may have MORE THAN ONE live entry: each distinct Address is a replica. Put upserts by (service, entry.Address) — a second address adds a replica; the same address refreshes in place. Delete removes ONE replica by (service, address).
Implementations must be safe for concurrent use. Put/Delete are writes at heartbeat rate (from /rpc/_register). Snapshot is read by the resolver to refill its local read cache — NOT per request — so a remote store is hit at most once per refresh tick, never on the dispatch hot path. ReapExpired drops entries past ExpiresAt and reports whether anything changed; a store with native TTL (Redis) may no-op + return false.
type RemoteOptions ¶
type RemoteOptions struct {
// Auth, when true, binds (name, Verify) as the gateway's auth
// verifier. Verify defaults to "verify" if empty.
Auth bool
Verify string
// Authz, when true, binds (name, Check) as the gateway's authz
// policy hook. Check defaults to "check" if empty.
Authz bool
Check string
// Introspect, when true, opts this entry into the gateway's
// /rpc/_introspect aggregator fan-out. Default false (no probe).
Introspect bool
}
RemoteOptions configures RegisterRemote — role flags + introspect opt-in, parallel to what an inbound /rpc/_register POST carries. Empty zero value preserves the legacy two-arg behavior.
type Request ¶
type Request struct {
// Method is the HTTP method, e.g. "POST" or "GET". Most business
// calls are POST; framework endpoints like /rpc/_health are GET.
Method string
// Path is the full URL path, e.g. "/rpc/WidgetService/create".
Path string
// Header is the inbound HTTP headers, with sov-namespaced inbound
// headers already stripped by Server.
Header Header
// Body is the request body, fully buffered. Empty body is allowed.
Body []byte
// RemoteIP is the caller's source IP. Server picks it from
// X-Forwarded-For or the transport-level remote address.
RemoteIP string
// Host is the inbound request's Host (net/http lifts Host OUT of the header
// map into http.Request.Host, so it is otherwise invisible to a plugin
// reading req.Header). First-class so vhost/tenant routing, host-based
// policy, and audit can read it. Empty for in-process dispatched
// sub-requests (MCP, /rpc surfaces, batch), which carry no transport Host.
Host string
// RawQuery is the URL query string without the leading '?', e.g.
// "seconds=10". Empty for most business calls — the /rpc codec reads args
// from the body, not the query. Surfaced so a RouteHandler plugin (pprof,
// a custom GET endpoint) can read query params, which were otherwise
// inaccessible through the gateway's Request.
RawQuery string
// User is the authenticated subject, set by Server-side middleware
// before dispatch. nil for anonymous calls. Gateway copies this onto
// the rpc.Context handed to handlers.
User any
// contains filtered or unexported fields
}
Request is the transport-neutral inbound RPC. Server adapters populate this from whatever HTTP types they use.
type RequestHandler ¶
RequestHandler is what the gateway hands to Server.Handle. Server constructs a Request from the inbound HTTP request and passes it; the returned Response is written back to the client.
type Resolver ¶
type Resolver interface {
Resolve(ctx context.Context, service string) (*Endpoint, bool)
// Services returns a snapshot of every service this resolver knows
// about, used by the gateway's /_health aggregator.
Services() []string
// Introspectables returns the subset of Services() that opt in to
// /_introspect probing. Returning a nil/empty slice means "no
// introspection allowed" — the gateway will skip the HTTP probe and
// the service stays out of the aggregated type catalog.
Introspectables() []string
}
Resolver locates a service by wire name and returns either a local dispatch (handlers in-process) or a remote endpoint (HTTP-proxy). A gateway holds a chain of resolvers and tries them in order — the first hit wins. Implementations must be safe for concurrent use.
type Response ¶
type Response struct {
// Status is the HTTP status code.
Status int
// Header carries response headers (Content-Type etc.). Server merges
// these onto its response.
Header Header
// Body is the response payload. Ignored when Stream is set.
Body []byte
// Stream, when non-nil, is written to the client incrementally
// (constant memory) INSTEAD of Body — for large or open-ended payloads
// a RouteHandler plugin produces without buffering: a zip export, a
// file download, a server-sent-event feed. The adapter io.Copy's it to
// the wire (chunked transfer; no Content-Length) and, if it also
// implements io.Closer, closes it when done.
//
// Streaming is a RouteHandler-plugin concern. RPC methods return
// (T, error) and are enveloped as buffered JSON — they cannot stream.
// Set Content-Type in Header (e.g. "application/zip"); the adapter does
// not guess it for a stream. See PipeStream for the writer-callback form.
Stream io.Reader
// Mode is the dispatch-mode hint set by the gateway's internal
// dispatch path (local/remote/federated/framework/plugin). Surfaces
// in DispatchEvent.Mode for observability. Plugins that build a
// Response from scratch should leave this empty; the framework
// fills it in at known dispatch sites.
Mode string
}
Response is what the RequestHandler returns. Server writes it back.
func ErrorResponse ¶
ErrorResponse builds a *Response from an *rpc.Error: the error's HTTP status plus its JSON-marshaled envelope body. Plugins returning an error from a route handler should use this so the wire shape matches the framework's own error responses.
func ErrorResponseFromAny ¶
func ResponseFrom ¶
ResponseFrom returns the embedded *Response when err was wrapped with RespondErr, or nil otherwise.
type ResponseInterceptor ¶
ResponseInterceptor fires AFTER dispatch with the resolved *Response. Plugin may mutate Status, Header, Body — or replace them entirely. Runs in registration order so each interceptor sees the output of the previous one.
Use cases: CORS header injection on every response, response compression, body redaction, status remapping, structured logging of the final envelope.
Fires for ALL responses: framework endpoints, plugin routes, business dispatch. The runtime tags the Response.Mode field before this hook fires so interceptors can branch by dispatch source.
type RetryConfig ¶
type RetryConfig struct {
MaxAttempts int // total attempts including the first; <=1 disables retry
BaseBackoff time.Duration // first-retry backoff; default 20ms
MaxBackoff time.Duration // backoff cap; default 1s
}
RetryConfig bounds automatic retry of a failed REMOTE dispatch (W1.4). Retry is OFF by default (MaxAttempts <= 1); enable it with WithRetries. A retry re-resolves onto a DIFFERENT replica (round-robin; open breakers skipped). Only a provably-never-executed failure (breaker open / dial refused) is retried unconditionally; an ambiguous failure (timeout) or an upstream 5xx is retried ONLY when the request carries an Idempotency-Key, so a non-idempotent op is never silently re-sent. Backoff is exponential with full jitter and never sleeps past the request deadline (W1.2).
type RoleFlag ¶
type RoleFlag uint32
RoleFlag is a bit-flag a pod uses to declare gateway-level roles to the upstream registry on /_register. The upstream binds those roles (auth verifier, authz checker) to the pod's wire name.
type RouteHandler ¶
type RouteHandler interface {
RoutePatterns() []string
ServeRoute(ctx context.Context, req *Request) *Response
}
RouteHandler lets a plugin own a path on the gateway, instead of wrapping the dispatch chain in a Middlewarer just to peek for a prefix. Patterns follow net/http ServeMux convention: a trailing "/" matches the subtree (prefix match); no trailing "/" matches the exact path only. Routes are checked AFTER the framework's own endpoints (_health/_introspect/_register/_batch) so plugins extend the framework surface but cannot shadow built-ins.
Match order is by SPECIFICITY, not registration order: the longest matching pattern wins, so a broad surface ("/rpc/") never shadows a more-specific route ("/rpc/_explorer/") or a catch-all SPA ("/") no matter when each was registered. Among fully-equal routes (same priority AND pattern length) the earliest-registered wins. A handler may DECLINE by returning a nil *Response, and routing falls through to the next-most-specific match — this is how a "/" SPA coexists with /rpc dispatch. Return a non-nil Response (including a 404) to CLAIM the path and stop fall-through. To override specificity, also implement RoutePrioritizer.
type RoutePrioritizer ¶
type RoutePrioritizer interface {
RoutePriority() int
}
RoutePrioritizer is an optional interface a RouteHandler may implement to override specificity-based match order. A higher RoutePriority() is tried before any longer pattern; the default (unimplemented) is 0, and negatives are allowed. Use it when a plugin must win — or deliberately lose — regardless of pattern length: e.g. a maintenance/lockdown handler that should intercept every path (high priority on "/"), or a fallback that should yield to everything (negative priority). Ties (equal priority AND length) still fall back to registration order.
type SealVerifier ¶
SealVerifier decides whether inbound X-Sov-* claim headers carry a valid HMAC seal under any registered key. Trust guard iterates registered verifiers; first true wins. If NO SealVerifier is registered AND TrustUpstreamClaims is true, the seal check is skipped — pair with an UpstreamTrustPolicy in that case.
type Server ¶
type Server interface {
// Handle registers the single catch-all handler for /rpc/* requests.
// Server must invoke it for every POST under /rpc/.
Handle(h RequestHandler)
// ListenAndServe binds and serves until ctx is cancelled.
ListenAndServe(ctx context.Context, addr string) error
}
Server abstracts the HTTP runtime so the gateway is not coupled to any one router/framework. The default implementation (NewNetHTTPServer) is built into this package and uses net/http; consumers who want fiber, fasthttp, echo, or anything else implement Server themselves and pass it via WithServer.
The contract: Server must route every incoming POST request whose path starts with /rpc/ to the gateway. The gateway tells Server how to do that via Handle. Server then calls the registered RequestHandler with the parsed Request and writes whatever ResponseWriter the gateway returns back to the client. Server is responsible for the network (listening, TLS, keep-alives, etc.); the gateway is responsible for the protocol.
type TypeDescriptor ¶
type TypeDescriptor struct {
Name string `json:"name"`
Fields []rpc.ParamField `json:"fields"`
UsedBy []TypeUse `json:"used_by"`
ShapeHash string `json:"shape_hash"`
// Owner is the service that PRODUCES this type — the one that returns
// it (a "response"-role usage). Empty for request/nested-only types
// (no producer) or when ownership is ambiguous, i.e. >1 producer (see
// Owners + IntrospectReport.BoundaryWarnings). Data-boundary metadata,
// inferred by convention; see inferOwnership.
Owner string `json:"owner,omitempty"`
// Owners is the full set of producing services (every service that
// returns this type). One entry is the normal case (Owner mirrors it);
// two or more is a data-boundary smell — overlapping producers — which
// also raises a BoundaryWarning. Sorted, deduped. Surfaced so the
// ambiguous case shows WHO the producers are, not just that it's
// ambiguous.
Owners []string `json:"owners,omitempty"`
// Consumers are services that use this type without owning it (as a
// request param or a nested reference). Sorted, deduped.
Consumers []string `json:"consumers,omitempty"`
// agree on its shape (one ShapeHash). That is a legitimately shared domain
// model, not a boundary smell — Owners lists the co-owners. Contrast DRIFT:
// same name, DIFFERENT shapes, which lands in IntrospectReport.CrossRefs.
Shared bool `json:"shared,omitempty"`
}
TypeDescriptor is the catalog entry for one Go type. Fields is the full ParamField list (including the new Position/Omitempty/Deprecated markers). UsedBy lists every appearance in the org's RPC surface.
type TypeUse ¶
type TypeUse struct {
Service string `json:"service"`
Method string `json:"method"`
Role string `json:"role"` // "request" | "response"
}
TypeUse records one appearance of a TypeDescriptor on a method.
type TypeVariant ¶
type TypeVariant struct {
ShapeHash string `json:"shape_hash"`
Fields []rpc.ParamField `json:"fields"`
Services []string `json:"services"`
}
TypeVariant is one shape of a same-named type.
type TypeVariants ¶
type TypeVariants struct {
Name string `json:"name"`
Variants []TypeVariant `json:"variants"`
// Diff is the per-field breakdown across the variants: for every field name
// that appears in ANY variant, its schema type in each variant (aligned to
// Variants order; "" = the field is absent from that variant). Field.Diverges
// marks the rows that actually differ — that IS the drift, spelled out, so a
// reader sees exactly which fields moved instead of just "these shapes differ".
Diff []FieldDivergence `json:"diff,omitempty"`
}
TypeVariants groups same-named types that diverged in shape across services — drift detection. variants[0] is the canonical (sorted by ShapeHash), the rest are the divergences.
type UpstreamTrustPolicy ¶
UpstreamTrustPolicy decides whether inbound X-Sov-* claim headers from the given request should be trusted. Trust guard iterates registered policies; ALL must return true (logical AND) for the bundle to pass. If NO UpstreamTrustPolicy is registered, the allowlist check is skipped (a request passes the upstream gate trivially) — pair with a SealVerifier to keep the trust guard honest.
type VerifyParams ¶
type VerifyParams struct {
Token string `json:"token"`
}
VerifyParams is the request payload the gateway sends to AuthService.Verify.
Source Files
¶
- auth.go
- auth_binding.go
- auth_seal.go
- breaker.go
- capability.go
- client.go
- config_dump.go
- dispatch.go
- doc.go
- endpoints.go
- gateway.go
- health.go
- introspect.go
- mesh.go
- mesh_peer.go
- mesh_resolver.go
- mesh_url.go
- nethttp.go
- plugin.go
- plugin_discover.go
- plugin_entry.go
- plugin_errors.go
- plugin_hooks.go
- plugin_interfaces.go
- plugin_lookup.go
- plugin_order.go
- plugin_use.go
- readiness.go
- recovery.go
- server.go
- stream.go
- streaming.go
- surface.go
- typecatalog.go
- wire_types.go
Directories
¶
| Path | Synopsis |
|---|---|
|
builtin
|
|
|
accesslog
Package accesslog emits one structured log line per dispatched call via the gateway's Logger.
|
Package accesslog emits one structured log line per dispatched call via the gateway's Logger. |
|
audit
Package audit ships a sov plugin that records every dispatch event into a sliding-window in-memory ring AND emits structured JSON to a writer (typically os.Stdout or a log file).
|
Package audit ships a sov plugin that records every dispatch event into a sliding-window in-memory ring AND emits structured JSON to a writer (typically os.Stdout or a log file). |
|
auth
Package auth ships a first-party sov plugin that translates verified Claims into legacy-shape headers ("X-Forwarded-User" / "X-Forwarded-Scopes" / "X-Tenant-Id") on every outbound proxy hop.
|
Package auth ships a first-party sov plugin that translates verified Claims into legacy-shape headers ("X-Forwarded-User" / "X-Forwarded-Scopes" / "X-Tenant-Id") on every outbound proxy hop. |
|
batch
Package batch ships the cascading-batch endpoint at /rpc/_batch.
|
Package batch ships the cascading-batch endpoint at /rpc/_batch. |
|
batchstream
Package batchstream ships the STREAMING batch endpoint at /rpc/_batchstream.
|
Package batchstream ships the STREAMING batch endpoint at /rpc/_batchstream. |
|
configdump
Package configdump is the opt-in plugin that exposes /rpc/_config — a sanitized dump of the gateway's effective runtime configuration (load-shed cap, circuit-breaker settings, auth/authz wiring, advertise URL, the set of wired plugins, and the locally-served routers).
|
Package configdump is the opt-in plugin that exposes /rpc/_config — a sanitized dump of the gateway's effective runtime configuration (load-shed cap, circuit-breaker settings, auth/authz wiring, advertise URL, the set of wired plugins, and the locally-served routers). |
|
cors
Package cors adds CORS headers to every response and handles the browser's OPTIONS preflight short-circuit.
|
Package cors adds CORS headers to every response and handles the browser's OPTIONS preflight short-circuit. |
|
deadline
Package deadline bounds each request to a deadline budget so a mesh chain A->B->C shares ONE deadline instead of each hop independently waiting out a full timeout (worst case: the sum).
|
Package deadline bounds each request to a deadline budget so a mesh chain A->B->C shares ONE deadline instead of each hop independently waiting out a full timeout (worst case: the sum). |
|
explorer
Package explorer mounts the embedded HTML browser at /rpc/_explorer/.
|
Package explorer mounts the embedded HTML browser at /rpc/_explorer/. |
|
hmacseal
Package hmacseal seals injected X-Sov-* claim headers with HMAC-SHA256 so downstream services can detect forged claim bundles.
|
Package hmacseal seals injected X-Sov-* claim headers with HMAC-SHA256 so downstream services can detect forged claim bundles. |
|
hmacseal/proto
Package proto is the wire format for X-Sov-Seal.
|
Package proto is the wire format for X-Sov-Seal. |
|
idempotency
Package idempotency lets a client make a mutating call safe to retry: it sends an Idempotency-Key header, and a replay of the same key returns the original SUCCESSFUL response instead of executing the handler again.
|
Package idempotency lets a client make a mutating call safe to retry: it sends an Idempotency-Key header, and a replay of the same key returns the original SUCCESSFUL response instead of executing the handler again. |
|
introspect
Package introspect is the opt-in plugin that exposes the public /rpc/_introspect endpoint — the aggregated API catalog (every service, method, and type the gateway can route).
|
Package introspect is the opt-in plugin that exposes the public /rpc/_introspect endpoint — the aggregated API catalog (every service, method, and type the gateway can route). |
|
manifest
Package manifest emits the PEMM manifest of a running gateway — a single JSON document describing services, plugins, role bindings, federation map, and registered remotes.
|
Package manifest emits the PEMM manifest of a running gateway — a single JSON document describing services, plugins, role bindings, federation map, and registered remotes. |
|
mcp
Package mcp serves a Model Context Protocol endpoint (default /mcp) that exposes registered routers as MCP tools — so any MCP client (Claude, Cursor, Copilot) can call your sov methods as tools.
|
Package mcp serves a Model Context Protocol endpoint (default /mcp) that exposes registered routers as MCP tools — so any MCP client (Claude, Cursor, Copilot) can call your sov methods as tools. |
|
meshsecret
Package meshsecret ships the HMAC gate for /rpc/_register.
|
Package meshsecret ships the HMAC gate for /rpc/_register. |
|
meshsecret/proto
Package proto is the wire format for the mesh-secret-gated /rpc/_register sig.
|
Package proto is the wire format for the mesh-secret-gated /rpc/_register sig. |
|
metrics
Package metrics ships a Prometheus-text-format metrics plugin for sov.
|
Package metrics ships a Prometheus-text-format metrics plugin for sov. |
|
preempt
Package preempt permits a federated _register to take over a service name already claimed by a different address.
|
Package preempt permits a federated _register to take over a service name already claimed by a different address. |
|
profiler
Package profiler serves the stdlib net/http/pprof endpoints at /debug/pprof/ as a gateway RouteHandler.
|
Package profiler serves the stdlib net/http/pprof endpoints at /debug/pprof/ as a gateway RouteHandler. |
|
ratelimit
Package ratelimit is an optional rate limiter on the HeaderParser seam.
|
Package ratelimit is an optional rate limiter on the HeaderParser seam. |
|
registertoken
Package registertoken ships the simple shared-token gate for /rpc/_register.
|
Package registertoken ships the simple shared-token gate for /rpc/_register. |
|
registertoken/proto
Package proto is the wire format for the static register-token join gate.
|
Package proto is the wire format for the static register-token join gate. |
|
registry
Package registry promotes the gateway to central-registry shape — owns POST /rpc/_register and the public top-level /health URL.
|
Package registry promotes the gateway to central-registry shape — owns POST /rpc/_register and the public top-level /health URL. |
|
requestid
Package requestid stamps a stable X-Sov-Request-Id on every request as it enters the gateway and propagates it through every hop the request takes: outbound proxy calls, in-process local dispatch, and downstream gateways (via the same header).
|
Package requestid stamps a stable X-Sov-Request-Id on every request as it enters the gateway and propagates it through every hop the request takes: outbound proxy calls, in-process local dispatch, and downstream gateways (via the same header). |
|
roletakeover
Package roletakeover relaxes the default cross-name role-binding guard.
|
Package roletakeover relaxes the default cross-name role-binding guard. |
|
rpc
Package rpc is the RPC surface builtin — the `/rpc/{router}/{method}` wire, exactly as gateway/builtin/mcp is the MCP surface.
|
Package rpc is the RPC surface builtin — the `/rpc/{router}/{method}` wire, exactly as gateway/builtin/mcp is the MCP surface. |
|
static
Package static mounts a static-file tree (a built SPA, a docs site, a bundle of assets) on the gateway via a RouteHandler.
|
Package static mounts a static-file tree (a built SPA, a docs site, a bundle of assets) on the gateway via a RouteHandler. |
|
tracing
Package tracing propagates a W3C traceparent across every hop so a mesh request forms a real span tree in an OpenTelemetry / Jaeger / Zipkin collector — not just one flat correlation id (that is requestid's job).
|
Package tracing propagates a W3C traceparent across every hop so a mesh request forms a real span tree in an OpenTelemetry / Jaeger / Zipkin collector — not just one flat correlation id (that is requestid's job). |
|
upstreams
Package upstreams limits which upstream gateway URLs a pod trusts X-Sov-* claim headers from.
|
Package upstreams limits which upstream gateway URLs a pod trusts X-Sov-* claim headers from. |
|
Package gatewaytest is the integration-test harness for a sov Gateway — the gateway-level analog of rpctest (which dispatches straight into the engine).
|
Package gatewaytest is the integration-test harness for a sov Gateway — the gateway-level analog of rpctest (which dispatches straight into the engine). |
|
internal
|
|
|
gwtest
Package gwtest holds gateway test helpers shared across the sov test suite — the counterpart of rpctest for handler tests.
|
Package gwtest holds gateway test helpers shared across the sov test suite — the counterpart of rpctest for handler tests. |
|
Package preset curates default plugin bundles for the common sov deployment modes.
|
Package preset curates default plugin bundles for the common sov deployment modes. |