# Core

Source: https://gortexa.sheng.page/en/components/core/

The five packages that assemble a Gortexa service: `kernel` owns the single h2c port and the lifecycle, `config` supplies layered configuration, `interceptor` is the fixed eight-stage governance chain, `auth` is the pluggable verification stage, and `apperr` is the error model all three transports share.

Fetch the module first: `go get github.com/yshengliao/gortexa@v0.28.3`. Every snippet below is compile-verified against v0.28.3.

## kernel

The composition root: `App` owns the single h2c listener (multiplexing gRPC, HTTP/JSON and MCP), the gRPC server, the health registry, an in-process loopback, and graceful shutdown. Build it with `kernel.New(opts...)`; options include `WithConfig`, `WithLogger`, `WithInterceptors` (takes an `interceptor.Set` by value, not a pointer), `WithoutInterceptors` (an explicit opt-out — exactly one of the two is required, omitting both fails at startup and passing both is a mutual-exclusion error), `WithStatsHandler`, `WithHTTPWrap`, `WithGateway`, `WithMCPHandler`, `WithShutdownHook`, `WithServerOptions` (append gRPC ServerOptions to add your own interceptors/stats, chained after the framework's), `WithAdminListener` (opt-in separate ops port serving only /healthz + /readyz), `WithExtraListener` (serve any handler on a caller-owned listener). The main h2c port stays single-port-multiplexed; `app.AdminAddr()` reports the bound admin address.

```go
import (
	"context"
	"github.com/yshengliao/gortexa/config"
	"github.com/yshengliao/gortexa/health"
	"github.com/yshengliao/gortexa/kernel"
)

cfg := config.MustBuild()
// New requires exactly one of WithInterceptors / WithoutInterceptors;
// `set` comes from the interceptor section below.
app, err := kernel.New(kernel.WithConfig(cfg), kernel.WithInterceptors(set))
if err != nil {
	return err
}
// register your gRPC services on app.GRPCServer() here
app.Health().Register("self", func(context.Context) health.State { return health.Healthy })
return app.Run(ctx) // /healthz and /readyz are built in
```

`App` methods: `GRPCServer()`, `Health()`, `Loopback()`, `Logger()`, `SetGateway()`, `SetMCPHandler()`, `Run(ctx)`, `Shutdown(ctx)`. `SetGateway` and `SetMCPHandler` are assembly-time setters: calling either once the server is serving panics. `WithHTTPWrap` composes rather than overwrites (the first one supplied ends up innermost), so a second wrap cannot silently discard an earlier security layer; `WithAdminListener` may be used at most once, since two admin listeners would race over what `AdminAddr` reports.

## config

Fixed precedence: built-in defaults < YAML file < .env file < environment (`GORTEXA_` prefix, `__` for nesting). The entry points are `MustBuild` (fail-loud: it panics when jwt_secret is empty, the dev placeholder, or < 32 bytes, and when issuer is blank) and `BuildUnvalidated`. There is no `Load`. `WithConfigFile` makes the file mandatory: a missing file fails the build (`MustBuild` panics).

```go
import "github.com/yshengliao/gortexa/config"

cfg := config.MustBuild(config.WithConfigFile("etc/config.yaml"))
dsn := cfg.DB.DSN.Reveal() // a Secret is revealed only at the point of use
```

The `config.Secret` type masks itself to `****` in `String()`, logs and JSON; call `Reveal()` for the real value. `Config`'s sub-structs: `Server`, `Auth`, `DB`, `Cache`, `MQ`, `Log`, `Observ` (all fields documented in [Configuration](https://gortexa.sheng.page/en/configuration/)).

## interceptor

`NewSet(Config)` builds the eight-stage fixed-order chain (recover → request-id → logger → load-shed → rate-limit → circuit-breaker → auth → validation), which you pass to `kernel.WithInterceptors`.

```go
import (
	"time"
	"github.com/yshengliao/gortexa/auth"
	"github.com/yshengliao/gortexa/interceptor"
)

set, err := interceptor.NewSet(interceptor.Config{
	Verifier:  auth.MustNewVerifier([]byte(secret), "gortexa"),
	RateLimit: interceptor.RateLimitConfig{RPS: 200, Burst: 100, TTL: 10 * time.Minute},
})
// pass set to kernel.WithInterceptors(set)
```

`Config` also carries `Logger`, `AuthSkip`, `CircuitBreaker`, `LoadShedding`, and `Metrics`. `Set` exposes `UnaryChain()` / `StreamChain()`. `CBConfig.Logger` gets one line per state change; `NewSet` back-fills it from `Config.Logger`, but a breaker you build directly stays silent without it — the metric and span equivalents are dropped unless an OTLP endpoint is configured. `CBConfig` uses a three-valued outcome: only server-side failures (`Unavailable`, `Internal`, a `DeadlineExceeded` while the request context is still live) count; client-caused results (a cancelled/expired context, `Canceled`, `InvalidArgument`, `Unauthenticated`, `PermissionDenied`) are neutral — they neither trip nor reset it — and a half-open episode with no probe reporting back within `OpenInterval` is abandoned, so a long-lived stream cannot wedge a method shut.

The auth stage is pluggable: `Verifier` uses the built-in HS256 JWT; a non-JWT scheme sets `Authenticator` instead (an `auth.Authenticator`, `Authenticate(ctx) (ctx, error)`) — static bearer, mTLS, API key. `Authenticator` wins when both are set. JWT is also available explicitly via `auth.NewJWTAuthenticator(v)`.

## auth

HS256 JWT verification/signing plus context helpers. The secret must be ≥ 32 bytes.

```go
import "github.com/yshengliao/gortexa/auth"

v := auth.MustNewVerifier([]byte(secret), "gortexa")
tok, err := v.Sign("user-123", []string{"admin"}, time.Hour)
// later, inside a handler, retrieve the verified claims:
if claims, ok := auth.ClaimsFrom(ctx); ok {
	_ = claims.Subject // "user-123"
}
```

`Verify(token)` returns `*Claims` (with `Roles` plus embedded `jwt.RegisteredClaims`); `BearerToken(header)` extracts a token from the Authorization header.

## apperr

A single mapping table drives the gRPC status, HTTP status and MCP error envelope. `*apperr.Error` implements `GRPCStatus()`, so a handler just returns it.

```go
import "github.com/yshengliao/gortexa/apperr"

func (s *svc) GetThing(ctx context.Context, req *pb.GetThingRequest) (*pb.Thing, error) {
	t, err := s.repo.Find(ctx, req.GetId())
	if err != nil {
		return nil, apperr.Wrap(apperr.CatNotFound, "thing not found", err)
	}
	return t, nil
}
```

Only `CatInvalidArgument` and `CatUnauthenticated` forward the developer message to the caller; every other category exposes only the registry SafeMessage, so internal causes never leak. A validation failure's forwarded message is bounded — at most 10 violations and 512 bytes, with a `(+N more)` suffix — so a request carrying a large repeated field cannot turn one `InvalidArgument` into a multi-megabyte response and log record. When a status is wrapped with `%w`, only the innermost status's own message is forwarded; the wrapper's context is replaced by the SafeMessage. The category constants cover the full gRPC set (`CatNotFound`, `CatPermissionDenied`, `CatFailedPrecondition`, `CatInternal`, and so on).
