gateway

package module
v2.16.26 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

README

gateway

Hanzo Gateway

Gateway is the trust boundary in front of the Hanzo API. Every request to api.hanzo.ai passes through it, and what it does is decide who the caller is and whether the request may proceed — validate the JWT against Hanzo IAM's JWKS, throw away every identity header the client sent, write the canonical identity headers from the verified token, apply rate limits and circuit breakers, emit telemetry, and hand the request on.

It is not a router, and it deliberately does not have a route map. For api.hanzo.ai it forwards /* straight to hanzoai/cloud with no per-route allow-list. Cloud's mount table is the one routing source of truth; a second table here would be a second thing to get out of sync. The comment above apiCloudHosts in routes.go says it plainly, and that is the rule to preserve when changing this repo.

The value of the boundary is exactly that downstream services never have to wonder whether X-Org-Id came from a token or from a curl flag. It came from a token, because gateway strips it unconditionally on the way in and rewrites it on the way out.

Go License

Where it sits

Internet
  -> Cloudflare
  -> hanzoai/ingress        TLS termination, L7, the front door
  -> hanzoai/gateway        identity, rate limit, circuit break, telemetry
  -> hanzoai/cloud          owns /v1/* routing (HIP-0106 mount table)

Ingress and gateway are different jobs and both are needed. Ingress terminates TLS and decides which cluster service a hostname belongs to. Gateway decides who the caller is. Neither does the other's work.

Implements HIP-0026 (the identity-header contract) and HIP-0106 (unified cloud binary; gateway exposes Mount() so it can run in-process inside the cloud binary instead of as a separate hop).

Identity headers

Stripped from every inbound request, unconditionally, before anything else reads them:

X-User-Id, X-Org-Id, X-Roles, X-User-Email, X-Phone-Number, X-User-IsAdmin, X-User-Role, X-User-Roles, X-User-Name, X-Tenant-Id, X-Org, and every X-IAM-* and X-HANZO-* variant.

Written from the validated JWT. These three are canonical:

Header Source claim
X-User-Id sub
X-Org-Id owner
X-Roles roles, comma-joined

Plus X-User-Email (email), X-Phone-Number (phone_number / phone) and X-User-IsAdmin (isAdmin) when the token carries them.

Authorization, Content-Type, Accept and X-Request-ID pass through untouched. Opaque API keys (hk-, sk-, fw_, hz_, pk-) are not JWTs and are forwarded to the backend that owns them for validation.

Calling the API

Get a key from console.hanzo.ai and send it as a bearer token:

curl https://api.hanzo.ai/v1/chat/completions \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"enso","messages":[{"role":"user","content":"Hello"}]}'

Model ids come from the catalog, not from this repo — gateway does not know what a model is. curl https://catalog.hanzo.ai/v1/models lists them.

The API surface itself is documented at docs.hanzo.ai/docs; the route table lives in hanzoai/cloud. This README deliberately does not reproduce either, because a copy here would be the second source of truth the design exists to avoid.

Rate limiting

Configured in configs/<brand>/gateway.json under qos/ratelimit/router. Current Hanzo values:

Scope Limit
Global 5,000 req/s
Per client IP 100 req/s

Individual endpoints can override this in their own extra_config. Rejected requests get 429 with Retry-After.

Health

Path What it answers
/healthz The process is up
/health Backends are reachable

Build and run

make build          # gateway binary (legacy HTTP edge, BUILD_TAGS=legacy)
make build-ingress  # ingress sidecar binary
make test           # both builds
make validate       # validate every config

Images are published per brand, not as a bare latest:

docker run -p 8080:8080 ghcr.io/hanzoai/gateway:hanzo-latest

hanzo-latest and lux-latest are the two brand tags, alongside <brand>-<sha> for pinning. make docker-hanzo / make docker-lux build them; make deploy-hanzo / make deploy-lux apply to the clusters.

GOEXPERIMENT=jsonv2

The HIP-0106 in-process mount runs on hanzoai/zip, which routes JSON through stdlib encoding/json/v2 when the binary is built with the experiment on. The Dockerfile and CI set it; manual builds should too:

GOEXPERIMENT=jsonv2 make build

Without it the binary still builds and runs — zip falls back to encoding/json v1. v2 is preferred in production: roughly 10% faster and 25% fewer allocations per request. The startup log line json_variant=encoding/json/v2 confirms it is active. No third-party JSON library is permitted in the Hanzo Go stack; stdlib only.

Configuration

configs/
  hanzo/gateway.json    Hanzo config
  hanzo/ingress.json    ingress sidecar config
  lux/                  same, Lux brand
k8s/                    cluster manifests
cmd/gateway/            gateway entry point
cmd/ingress/            ingress sidecar entry point
tests/                  integration tests

An endpoint entry declares the path, the input headers it is allowed to carry, and the backend host — no path rewriting, since cloud speaks /v1/* natively:

{
  "endpoint": "/v1/chat/completions",
  "method": "POST",
  "input_headers": ["Authorization", "Content-Type", "Accept", "X-Request-ID",
                    "X-Org-Id", "X-Project-Id", "X-Billing-Account-Id"],
  "output_encoding": "no-op",
  "backend": [{
    "url_pattern": "/v1/chat/completions",
    "host": ["http://cloud.hanzo.svc.cluster.local:8000"],
    "encoding": "no-op"
  }]
}

These per-endpoint entries drive the legacy HTTP edge build. The default build is the passthrough relay described at the top: it does not consult them for api.hanzo.ai.

Run make validate after any config edit.

Repository Role
hanzoai/ingress The L7 front door — TLS, Kubernetes Ingress, load balancing
hanzoai/cloud The API itself; owns the /v1/* mount table
hanzoai/iam Issues the tokens gateway validates
hanzoai/engine Serves models on cloud GPUs
hanzoai/edge On-device inference runtime — runs models locally, not part of this path

License

Apache-2.0 — see LICENSE and NOTICE.

The service-assembly layer derives from upstream Apache-2.0 projects and is built on the Lura framework. The authoritative attributions are preserved verbatim in NOTICE.

Documentation

Overview

authpolicy.go is the gateway's HTTP authn policy, stated ONCE.

The policy is a decision about a request — its method, its host, its path and its headers — and it is the same decision whichever HTTP framework carries the request. It used to be written inside a gin closure, which braided the decision together with the framework that happened to be running it, and the cost of that braiding was a second copy: the HIP-0106 mount could not run the gin middleware natively, so it bridged through a whole gin engine per request to reach a rule that never needed gin at all.

Now the rule is a value — [authGate] — and each transport is a dozen lines of adapter over it:

auth_middleware.go   gin, for the legacy Lura edge (the shipping image)
mount.go             native zip, for the HIP-0106 unified cloud binary

(The ZAP relay's envelope gate in gate.go states the same ALLOW/DENY ladder against forward.Forward, which carries identity as fields rather than as headers and is billed by cloud rather than here. It is listed as the third transport in that file's header; folding it in here would require a header view over the envelope and would give the relay a per-request Commerce round-trip it deliberately does not have — see HIP-0110.)

What "one place" buys, concretely: the identity strip is now the FIRST thing every HTTP transport does, and it now actually survives. Through the old gin bridge it did not — the net/http→fasthttp adapter copies the middleware's headers back onto the request with Set and never Del, so a header the strip DELETED stayed on the request the downstream read. A client-supplied X-User-IsAdmin survived the trust boundary. See TestMountAuth_StripSurvives.

build_app.go wires the gateway edge per HIP-0110.

The gateway is a pure ZAP→ZAP relay: ingress terminates TLS+HTTP and speaks ZAP to the gateway; the gateway authenticates on the Forward ENVELOPE, injects identity, and forwards to cloud/base over ZAP. The relay therefore lives on the ZAP node (RegisterRelay), not on an HTTP router — there is no per-request HTTP handling at the gateway anymore.

BuildApp still returns a tiny *zip.App so cmd/gateway/main can keep its :8080 listener as a process/liveness surface (ingress owns real client HTTP). It carries no forwarder and no auth middleware — those moved onto the node. RegisterRelay is the real entrypoint.

clientip.go answers "which client is this" — ONCE, for both edges.

A rate limit is only as good as the key it buckets on, and until now each transport supplied that key from its own framework:

gin's ClientIP()  trusts X-Forwarded-For from EVERY peer — gin's
                  defaultTrustedCIDRs is 0.0.0.0/0 + ::/0 and this repo never
                  calls SetTrustedProxies — and then returns the LEFTMOST
                  entry, which is the one the client wrote.

fiber's IP()      ignores X-Forwarded-For entirely (zip sets no ProxyHeader,
                  so it returns fasthttp's RemoteIP), so behind hanzoai/ingress
                  every request in the cluster carries the SAME peer — the
                  ingress pod.

One is forgeable and the other is a self-DoS: a 10/min per-IP cap that either never trips or trips once for the whole internet. Both were measured on the live lux-ns edge — 20 requests with a rotating X-Forwarded-For never hit the cap, and the gin access log printed the forged address as the client.

transport_parity_test.go could not see either, because its two harnesses never set a forwarded header and never vary the peer: both edges answered about the same nothing, and agreed. So the peer becomes a value, like every other decision in this package — a trusted set and a walk over it, stated here and asked by both transports.

cors.go is the gateway's credentialed-CORS policy, stated ONCE.

Which Origin may be reflected into Access-Control-Allow-Origin next to Access-Control-Allow-Credentials is a property of the REQUEST, not of the framework carrying it — the same decomplection [authGate] already made for authn. It lived inside a gin closure, and the cost of that braiding was the familiar one: the zip edge could not run the rule, so the HIP-0106 mount answered no preflight at all. A browser SPA talking to the unified binary got whatever the router does with an unrouted OPTIONS where the legacy edge answers 204 with the allowlist applied.

Now the rule is a value — [corsPolicy] — and each transport is a few lines of adapter over it:

legacy_transports.go   gin, for the Lura edge (the shipping image)
mount.go               native zip, for the HIP-0106 unified cloud binary

The header NAMES and VALUES live here too, in [corsAnswer], so the two edges cannot answer one preflight two ways.

Package gateway is the Hanzo Gateway edge: the JWT trust boundary (identity strip + write), the host/path routing table, and the HIP-0106 in-process mount surface consumed by the unified cloud binary.

Mount

The canonical entrypoint is Mount, in mount.go at the module root:

func Mount(app *zip.App, deps MountDeps) error

It carries NO build tag — it compiles in the default build. Mount installs the native zip gate chain (zipCORS, zipAuth, zipWidget), serves the typed probe pair under /_/gateway, and best-effort loads the routes table. A host calls it explicitly, not an init() and not a global registry.

MountDeps is declared HERE, in three fields, and names no host. This package imports zip and nothing of whoever composes it: the dependency points from the host into the gateway, so any host can install this boundary and none of them can be the only one that compiles.

One policy per gate, two HTTP transports

Every gate this edge applies is a framework-free VALUE, and each of the gateway's two HTTP edges is a few lines of adapter over it:

authGate.admit    authpolicy.go       strip, route class, public
                                      allowlists, token extraction, JWT
                                      validation, identity write, balance
widgetGate.admit  widget_security.go  hz_ origin allowlist + rate limits
corsPolicy.admit  cors.go             credentialed-origin allowlist

native zip   mount.go              zipAuth, zipWidget, zipCORS
gin          legacy_transports.go  the same three, for the Lura edge

Mount installs all three in the order the legacy engine runs them, so the two edges cannot admit different requests. transport_parity_test.go drives one request through both and asserts they agree.

The routing TABLE (routes.go) is the one thing with no zip twin: its compiled proxies are net/http, and reaching them from fasthttp would drop WebSocket upgrade and streaming. That file's header states it.

The HTTP surface

The gateway owns two doors — liveness and readiness — and they are TYPED ops declared once in probes.go, mounted at the root by the standalone binary's two listeners and under /_/gateway by the cloud mount. /metrics is the one deliberate escape hatch (Prometheus exposition is text, not JSON); MountMetrics carries the reason. Everything else on the wire is a relay (gate.go) or a proxy (routes.go) and is somebody else's contract.

Build tags

Two builds exist, and the one that SHIPS is `legacy`:

go build ./...               // default: no Lura; ZAP relay edge
go build -tags legacy ./...  // legacy: full Lura gin engine

Makefile sets BUILD_TAGS ?= legacy and the Dockerfile runs `make build`, so ghcr.io/hanzoai/gateway is the legacy engine. The default build serves the probes until the HIP-0110 ZAP relay backends are live; see the rationale comment above BUILD_TAGS in the Makefile.

The typed op table therefore reaches the DEFAULT binary (and the cloud binary through Mount) and NOT the legacy image, whose routes are the vendored Lura engine's. That is deliberate: internal/lura and internal/plugin are upstream KrakenD/Lura, and rewriting a fork's router buys permanent merge pain. They go at Phase C, with the rest of the legacy set, rather than being converted.

Forwards-only: never add a lura import to a non-`legacy` file, and never add gin to one. The default build imports gin NOWHERE — assert it, do not assume it:

go list -deps ./cmd/gateway | grep gin-gonic    # empty

Every gin transport lives in legacy_transports.go behind the tag, and gin remains in go.mod solely for the vendored KrakenD/Lura tree (internal/lura, internal/plugin) that the shipping image runs. It leaves go.mod when that tree is deleted at Phase C, and not before: `go list -deps -tags legacy` still reports it, because the shipped binary IS the legacy build.

gate.go is the gateway relay's authn policy, run on every inbound Forward ENVELOPE per HIP-0110. It is the edge trust boundary: the gate validates the IAM JWT carried in the Forward's headers and stamps the resolved identity (TenantID/UserID/IsAdmin/Permissions) onto the envelope so the backend trusts the edge. forward.Relay then re-emits the Forward with that identity and ships it to the chosen backend.

One auth implementation: the gate reuses hanzoai/authz/edge (the same JWKS cache + JWT validation + token extraction shared with cmd/ingress and the gin/Lura middleware) and gateway's own permission bit-field math (computePermissionsBitField / permissionBits). No second copy.

Billing is NOT enforced here. The gateway is authn + identity injection only; cloud's own prepaid balance gate bills the bridged request. This split is deliberate (HIP-0110): the relay must not read f.Body and must not block on a per-request Commerce round-trip — it stamps identity and forwards. The gate therefore touches f.Path and f.Headers only.

Package gateway exposes the HIP-0106 unified-binary mount surface for the Hanzo Gateway. In the unified cloud binary the gateway acts as the trust boundary: it validates JWTs, strips client-supplied identity headers, and writes the gateway-authorized X-Org-Id / X-User-Id / X-User-Email / X-Roles / X-User-Permissions / X-User-IsAdmin / X-Phone-Number set documented in HIP-0026.

In split-deploy mode (legacy ghcr.io/hanzoai/gateway image) the gateway runs as its own legacy-engine process — the standalone cmd/gateway binary keeps that path. The Mount path below is the SAME logic, reused inside the cloud binary so we do not maintain two trust boundaries.

peer_resolver.go decouples "where a backend lives" (its ZAP address) from "which connected NodeID forward.Relay must Call" (the handshake- learned peerID). HIP-0110's relay needs a peerID per request path, but a peerID is only knowable AFTER a successful ConnectDirectID handshake.

The original boot path braided three concerns into dialBackends: start the node, eagerly dial both backends, and treat any dial failure as a fatal os.Exit. That made the gateway un-bootable whenever cloud:9090 / base:9091 were absent — which is the steady state in environments where the ZAP backends aren't deployed yet, while HTTP serving (the part prod actually needs) would work fine.

peerResolver separates those concerns. The node is started once and unconditionally; dialing a backend is deferred to first use and retried on every Call until it succeeds. A backend being down degrades only the requests routed to it (they get a "peer not found" error that forward.Relay returns to the caller as a normal per-request failure), never the process. ConnectDirectID is idempotent — once a backend is reachable the first resolve caches its peerID and every subsequent resolve is a lock-free map read — so a healthy backend is dialed exactly once and ZAP behaves identically to the old eager path.

probes.go is the gateway's OWN HTTP surface — and it is the whole of it.

The gateway is an edge. Every byte a customer sends it belongs to some other service, so the only doors this repo owns are the ones that answer a question about the gateway itself: is the process alive, is it ready to carry traffic. Everything else on the wire is a relay (HIP-0110, gate.go) or a proxy (routes.go) and is somebody else's contract.

Those two doors are TYPED ops. They used to be three separate anonymous closures — one pair in BuildApp, one pair plus /metrics in cmd/gateway's health app, one lone healthz in Mount — each writing its own map literal, and each therefore free to answer a different shape. An untyped route appends nothing to the op registry, so all five were in no document: no OpenAPI operation, no MCP tool, no CLI command, no generated SDK method. A probe that is in no document is a probe an operator cannot discover, and five spellings of one answer is a shape that eventually disagrees with itself.

One declaration, mounted at every place the gateway becomes an HTTP server:

BuildApp                 (HIP-0110 relay's :8080 liveness surface)
buildHealthApp           (cmd/gateway's :8081 k8s listener)
Mount + /_/gateway       (HIP-0106 unified-binary trust boundary)

/metrics is deliberately NOT here — see MountMetrics.

routes.go is the gateway's host/path reverse-proxy routing TABLE — stdlib + yaml, with ZERO dependency on the upstream Lura SDK and, now, on any HTTP framework. It parses the config, compiles one httputil.ReverseProxy per prefix, and holds them behind a hot-reloadable lock. Nothing here answers a request.

The table's reader is [hostProxyMiddleware] in legacy_transports.go, the standalone edge's transport. It stays gin-side deliberately, and this is the one gate in this repo that does NOT have a zip twin:

  • a compiled httputil.ReverseProxy is net/http, and reaching it from a zip handler means the net/http↔fasthttp adaptor, which cannot hijack a connection. WebSocket upgrade and streamed responses — which this proxy serves today — would silently stop working. Carrying it means a NATIVE fasthttp proxy, which is its own reviewable change.
  • the api-host passthrough below rests on a premise that is FALSE co-resident: it forwards a whole host to the cloud service because cloud is another process. Inside the unified binary that address is the binary itself, and the same rule proxies the process to itself. In cloud-mode the routing source of truth is cloud's own mount table — stated below, in capitals, and true.

The CORS policy that used to live here moved to cors.go, which is what it is; this file is the routing table and nothing else.

widget_security.go is the widget-key gate: what a PUBLIC credential embedded in client-side JavaScript is allowed to do.

The gate is a value ([widgetGate]) for the same reason [authGate] is: the decision is about the request — its bearer token, its Origin, its client IP — and the gateway has two HTTP edges. While it was written inside a gin closure the zip edge could not run it, so the HIP-0106 mount enforced neither the origin allowlist nor the rate limit: an hz_ key stolen out of any page's source worked from anywhere, at any rate, against the model budget.

legacy_transports.go   gin, for the Lura edge (the shipping image)
mount.go               native zip, for the HIP-0106 unified cloud binary

Index

Constants

View Source
const NeutralServerBrand = "gateway"

NeutralServerBrand is the Server value when a request Host matches no brand (internal k8s probes, direct-IP hits). Brand-neutral and honest about the role without ever naming the framework (fasthttp / fiber / zip / the legacy engine).

View Source
const ServiceName = "gateway"

ServiceName is what a probe calls this service. One constant, so the answer is the same at all three mount points and an operator reading a probe body never has to work out which listener replied.

Variables

This section is empty.

Functions

func AssertGatewayWritten added in v2.16.20

func AssertGatewayWritten(c *zip.Ctx) bool

AssertGatewayWritten returns true iff the request flowed through gateway's auth middleware (i.e. X-Org-Id was written by gateway, not supplied by the client).

Implementation: gateway middleware sets a per-request Locals key after JWT validation. AssertGatewayWritten reads that key.

In production, every cloud-mounted subsystem should reject any request where AssertGatewayWritten returns false (HTTP 502 with a clear message — it indicates a deployment misconfiguration, not a client problem).

func BuildApp

func BuildApp(deps RouterDeps) (*zip.App, error)

BuildApp returns the gateway's tiny HTTP surface for cmd/gateway/main's :8080 listener. Real client traffic arrives over ZAP via RegisterRelay; this app exists only so the process exposes a liveness/readiness HTTP endpoint on the public port. It carries no forwarder.

func InitZapListenerFromEnv

func InitZapListenerFromEnv()

InitZapListenerFromEnv initializes the ZAP listener from environment variables. Set ZAP_LISTENER_ENABLED=true to enable.

func LoadRoutes

func LoadRoutes(cfg *RoutesConfig) error

LoadRoutes loads routing config from YAML. Called at startup and on hot-reload.

func LoadRoutesFromFile

func LoadRoutesFromFile(path string) error

LoadRoutesFromFile loads routes from a YAML file.

func Mount

func Mount(app *zip.App, deps MountDeps) error

Mount registers the gateway subsystem on app per HIP-0106. The mount does three things:

  1. Installs the canonical zip gate chain — CORS, then the trust boundary (strip client-supplied identity headers, validate JWT, write X-* headers), then the widget-key gate — so every downstream subsystem on the same zip.App sees one clean boundary, and the same one the legacy edge applies.

  2. Parses the gateway routes table (KMS or local file) when one is configured. Its reader is the standalone edge's transport, NOT this mount: co-resident, the routing source of truth is cloud's own mount table (routes.go states why in full). Loading it here is a Phase-C hook and a config validation, not a routing decision.

  3. Exposes /_/gateway/healthz on the native zip surface so liveness probes work even with auth fully enabled.

Mount installs onto the HOST's App and returns nothing, which is the whole difference between this subsystem and one that contributes routes. A child App's Use-chain is scoped to that child's own subtree (zip's snapshot semantics), so a gateway that built and returned its own App would guard its two probes and leave every sibling subsystem on the host unguarded — the boundary would still be there, still tested, and no longer on the path. Composition by return value is right for a subsystem that OWNS a prefix; this one owns the whole edge.

Mount is not idempotent — calling it twice on the same App installs the middleware twice. A host lists the gateway once.

func MountMetrics added in v2.16.21

func MountMetrics(app *zip.App)

MountMetrics registers /metrics, the ONE route in this repo that is deliberately not a typed op.

Prometheus' exposition format is line-oriented text, and a typed op answers JSON — the In/Out types ARE the contract, so declaring one here would put a JSON schema in the document for a route that has never sent JSON. That is worse than being absent from the document: an SDK generated from it would not work. So it stays an untyped route with the reason written next to it, which is the whole cost of the escape hatch and the reason it is countable.

The body is a stub. The real collector is installed by the telemetry middleware when GATEWAY_METRICS_ENABLED=true; this keeps the path answering for scrapers when telemetry is off (dev/local) rather than 404ing at them.

func MountProbes added in v2.16.21

func MountProbes(app *zip.App)

MountProbes registers the SAME two ops under the gateway's reserved prefix, which is where they belong inside the unified cloud binary: there the root /healthz is the BINARY's, answered by whichever subsystem the composition root gives it to, and a co-resident subsystem that claimed it would be answering for the whole process.

`/_/` rather than `/v1/` because these are the gateway's own doors, not product surface — the gateway is plumbing, and plumbing earns no version prefix.

The two registrations are spelled out rather than sharing one loop over a prefix, because the PATH is the op's identity: every projection — the OpenAPI document, the MCP tool list, the CLI, a generated SDK — keys on it, and a prefix arriving as a parameter is a path no generator can resolve at the point of declaration.

func NewLazyPicker

func NewLazyPicker(node *zaplib.Node, logger luxlog.Logger, baseAddr, cloudAddr string) forward.PeerPicker

NewLazyPicker is the production seam: it builds a lazy resolver over an already-started node, best-effort warms the base and cloud backends (a failed warm is logged at WARN and ignored — the picker will retry on first use), and returns the forward.PeerPicker for RegisterRelay. The gateway boots regardless of backend reachability; healthy backends are dialed once here and served from cache thereafter.

func Probes added in v2.16.21

func Probes(app *zip.App)

Probes registers the liveness and readiness ops at the ROOT of app — where a standalone process serves them, and where every orchestrator looks.

Both of the standalone binary's listeners call it (the public :8080 relay surface and the :8081 k8s listener), so the two cannot answer differently about one process.

func RegisterRelay

func RegisterRelay(deps RelayDeps) error

RegisterRelay installs the canonical HIP-0110 relay on deps.Node:

  • forward.Relay(node, gate, pick): decodes each inbound Forward envelope, runs the auth gate on it (JWT validate + identity inject, NO body read, NO billing — cloud bills the bridged request), re-emits the Forward with identity, and forwards to the picked peer, streaming the backend's Response back verbatim.
  • forward.RegisterReversePushHandler(node): routes backend→gateway Push frames (SSE / WebSocket) back to the originating client conn.

The gate reuses hanzoai/authz/edge + this gateway's money bit-field math; there is exactly one auth implementation. pick routes /v1/base/* to the base peer, everything else to cloud.

func SetGatewayWritten added in v2.16.20

func SetGatewayWritten(c *zip.Ctx)

SetGatewayWritten marks the current request as gateway-written. Called by gateway's auth middleware after successful JWT validation (or trusted-headers pass-through). Subsystems must NEVER call this — it is the trust boundary itself.

func StartZapListener

func StartZapListener(cfg ZapListenerConfig) error

StartZapListener starts a TLS 1.3+PQ listener on the given port. External clients (e.g. dev CLI) connect here with TLS-wrapped ZAP binary. Each accepted TLS connection is transparently proxied to the internal ZAP node (started by the ZapBackendFactory pool on the internal port), which handles the ZAP handshake, message dispatch, and forwarding to cloud.

func StopZapListener

func StopZapListener()

StopZapListener gracefully shuts down the ZAP TLS listener.

Types

type AuthConfig

type AuthConfig struct {
	// Enabled controls whether the auth middleware is active.
	// Default: true. Set to false via AUTH_ENABLED=false to disable
	// all auth checks (useful for integration tests and development).
	Enabled bool

	// JWKS URL to fetch signing keys (default: https://hanzo.id/v1/iam/.well-known/jwks)
	JWKSURL string

	// Expected JWT issuer (default: https://hanzo.id)
	Issuer string

	// Audiences is the allowlist of acceptable JWT `aud` values. A token
	// passes when its audience matches ANY entry. IAM stamps user tokens with
	// aud=<client_id>, so a single fixed audience rejects every user JWT; the
	// allowlist (token.AudiencesFromEnv) is the fix. Override entirely with
	// GATEWAY_ALLOWED_AUDIENCES.
	Audiences []string

	// Billing check endpoint (default: http://commerce.hanzo.svc.cluster.local:8001)
	BillingURL string

	// BillingToken is the COMMERCE_SERVICE_TOKEN for authenticating with Commerce.
	BillingToken string

	// BillingEnabled controls whether billing checks are performed.
	// Default: true (checks enabled). Set to false to disable.
	BillingEnabled bool

	// BillingPaths scopes balance enforcement to request paths matching one
	// of these prefixes. This is the one knob that keeps billing OFF for the
	// AI/validated (per-token billed downstream) and public routes while
	// ON for the metered must-gate platform surface (cloud/tasks/insights/
	// o11y/mpc/evals/licensing/product/provisioning/…). The /v1/commerce
	// funding surface is hard-excluded in code (billingPathMatch) regardless
	// of this list. Empty list enforces on every non-funding, non-public
	// route. Set BILLING_PATHS to the must-gate prefixes. One scope.
	BillingPaths []string

	// Paths that bypass auth entirely (exact prefix match)
	PublicPaths []string

	// Hosts that bypass auth entirely (e.g. hanzo.id for login)
	PublicHosts []string

	// If true, requests without a token are rejected (402/401).
	// If false (default), requests without a token pass through without headers.
	RequireAuth bool
}

AuthConfig holds configuration for the auth middleware.

func DefaultAuthConfig

func DefaultAuthConfig() AuthConfig

DefaultAuthConfig returns the default auth configuration from environment variables.

func (AuthConfig) Validate added in v2.15.0

func (c AuthConfig) Validate() error

Validate reports a fatal misconfiguration: billing enabled without the Commerce endpoint and service token it depends on. Enforced so enabling billing can never silently fail open (empty URL → checkBalance allows) nor 503-storm the whole metered surface (empty token → Commerce 401). The one caller that can return an error — gateway.Mount — refuses to start in this state; NewAuthMiddleware additionally fails the balance gate closed.

type Headers added in v2.16.21

type Headers interface {
	edge.Headers
	// Names calls fn once per header name on the request. Implementations must
	// tolerate fn deleting the name it was handed.
	Names(fn func(name string))
}

Headers is a header set this edge can strip, write and ENUMERATE.

edge.Headers is the estate's minimum — Get/Set/Del — and it is deliberately that small, so a transport qualifies by shape rather than by being named. The vendor-prefix backstop below needs one thing more: the NAMES present, which cannot be expressed in Get/Set/Del and is spelled differently by every transport (a Go map ranges, fasthttp visits). So the enumeration is added here, in the one package that needs it, and each transport supplies it in a line or two.

type KMSResolver

type KMSResolver interface {
	// FetchRoutes returns the raw YAML payload at the given KMS path.
	// An empty path is a programmer error; resolvers should return an error.
	FetchRoutes(path string) ([]byte, error)
}

KMSResolver fetches a routing-config secret payload from a Hanzo KMS path. The default implementation is a noop; activated implementations are wired in by setting GATEWAY_ROUTES_KMS_PATH (and the resolver-specific env vars documented on each implementation).

Implementations MUST return the raw YAML bytes for the routes config; the caller (loadRoutesFromEnv) is responsible for parsing them via yaml.Unmarshal.

func SetKMSResolver

func SetKMSResolver(r KMSResolver) KMSResolver

SetKMSResolver swaps the package-level KMS resolver. Intended for tests and for embedders that want to plug in a non-HTTP secret backend. Returns the previous resolver so callers can restore it.

type MountDeps added in v2.16.26

type MountDeps struct {
	// Logger receives the mount's own lifecycle lines. Required — Mount refuses
	// a nil one rather than dropping the record of what boundary it installed.
	Logger luxlog.Logger
	// Brand is the white-label deployment id ("hanzo", "lux", "zoo", ...). The
	// boundary derives NO behaviour from it; it is logged so an operator can see
	// which deployment's edge came up.
	Brand string
	// Domain is the deployment's own public API host (api.hanzo.ai,
	// api.lux.network). It supplies the default JWKS URL when JWKS_URL is unset,
	// which is the one decision this value drives.
	Domain string
}

MountDeps is the whole of what the boundary needs from whoever installs it — alongside RouterDeps (BuildApp) and RelayDeps (RegisterRelay), one narrow shape per entrypoint, all three declared in this module.

It was the HOST's own cloud.Deps: 24 fields carried across the boundary to reach three. The other 21 were not unused so much as untyped coupling, and coupling to a NAME rather than to a value — hanzoai/cloud ships two editions under one module path, so cloud.Deps denotes a different type in each, and a package that takes it can be composed by exactly one of them. Which one is decided by whichever edition happens to be in the build, not by anything this package says.

Naming the three values instead turns the arrow around: the host knows the gateway, and the gateway knows zip and nothing of whoever runs it.

type ProbeIn added in v2.16.21

type ProbeIn struct{}

ProbeIn is the input of a probe: none. A liveness answer that depended on something the caller sent would not be a liveness answer.

type ProbeOut added in v2.16.21

type ProbeOut struct {
	// Status is "ok" from the liveness probe and "ready" from the readiness
	// probe. It is the field a scraper reads when it reads the body at all;
	// orchestrators read the status code.
	Status string `json:"status"`
	// Service names the service that answered — always "gateway", including on
	// the separate k8s health listener, because the listener is a deployment
	// detail and the service is what the operator asked about.
	Service string `json:"service"`
}

ProbeOut is what every gateway probe answers.

type RelayDeps

type RelayDeps struct {
	Logger luxlog.Logger
	Node   *zaplib.Node
	// Pick chooses the backend peer for a request path. Production passes a
	// lazy resolver-backed picker (peerResolver.picker) that dials backends
	// on first use, so the gateway boots even when the backends are absent.
	// When nil, RegisterRelay falls back to the static pickPeer over the
	// pre-resolved BasePeerID / CloudPeerID below.
	Pick forward.PeerPicker
	// BasePeerID / CloudPeerID are the handshake-learned NodeIDs of the
	// already-connected base and cloud peers (from ConnectDirectID at dial
	// time). Used only when Pick is nil (e.g. tests that dial up front).
	BasePeerID  string
	CloudPeerID string
	// Auth is the IAM JWT validation config for the relay gate. Use
	// DefaultAuthConfig() to read it from the environment.
	Auth AuthConfig
}

RelayDeps is what RegisterRelay needs to stand up the ZAP relay on a node.

type RouteEntry

type RouteEntry struct {
	Prefix  string `yaml:"prefix" json:"prefix"`
	Backend string `yaml:"backend" json:"backend"`
	Rewrite string `yaml:"rewrite,omitempty" json:"rewrite,omitempty"` // optional: rewrite prefix
}

RouteEntry maps a path prefix to a backend URL.

type RouterDeps

type RouterDeps struct {
	Logger    luxlog.Logger
	ZAPNode   *zaplib.Node
	CloudAddr string
	BaseAddr  string
}

RouterDeps is the set of dependencies BuildApp needs.

type RoutesConfig

type RoutesConfig struct {
	Redirects  map[string]string       `yaml:"redirects" json:"redirects"`
	Routes     map[string][]RouteEntry `yaml:"routes" json:"routes"`
	Subdomains map[string]string       `yaml:"subdomains" json:"subdomains"`
}

RoutesConfig is the YAML structure for gateway routing. Loaded from KMS (GATEWAY_ROUTES_KMS_PATH) or local file (GATEWAY_ROUTES_FILE).

type WidgetSecurityConfig

type WidgetSecurityConfig struct {
	// MaxRequestsPerIP is the maximum number of widget requests per IP
	// within the rate limit window. Default: 10.
	MaxRequestsPerIP int

	// Window is the sliding window duration for per-IP rate limiting.
	// Default: 1 minute.
	Window time.Duration

	// GlobalMaxRequests is the maximum total widget requests across all
	// IPs within the window. Protects against distributed abuse.
	// Default: 600.
	GlobalMaxRequests int

	// AllowedOrigins is the set of origin domains allowed for widget
	// requests. If empty, origin checking is disabled.
	AllowedOrigins []string

	// CleanupInterval controls how often stale entries are evicted
	// from the per-IP rate limit map. Default: 5 minutes.
	CleanupInterval time.Duration
}

WidgetSecurityConfig holds configuration for widget key rate limiting and origin validation.

func DefaultWidgetSecurityConfig

func DefaultWidgetSecurityConfig() WidgetSecurityConfig

DefaultWidgetSecurityConfig returns safe defaults.

AllowedOrigins can be overridden via WIDGET_ALLOWED_ORIGINS env var (comma-separated list of bare hostnames, no scheme/port). Subdomain matches are automatic: "hanzo.ai" also allows "*.hanzo.ai".

type ZapListenerConfig

type ZapListenerConfig struct {
	Port     int
	CertFile string
	KeyFile  string
	// InternalAddr is the local ZAP node's address to proxy to (e.g. "127.0.0.1:9652").
	InternalAddr string
}

ZapListenerConfig configures the inbound ZAP listener for external clients.

Directories

Path Synopsis
cmd
admin-api command
admin-api is the god-mode backend for the Hanzo Operator console (admin.hanzo.ai).
admin-api is the god-mode backend for the Hanzo Operator console (admin.hanzo.ai).
admin-guard command
admin-guard is the single forward-auth gate for Hanzo's admin surfaces.
admin-guard is the single forward-auth gate for Hanzo's admin surfaces.
gateway command
cmd/gateway is the standalone Hanzo Gateway edge process per HIP-0110.
cmd/gateway is the standalone Hanzo Gateway edge process per HIP-0110.
ingress command
Hanzo Ingress — lightweight host-based reverse proxy Replaces nginx-ingress with a minimal, config-driven proxy.
Hanzo Ingress — lightweight host-based reverse proxy Replaces nginx-ingress with a minimal, config-driven proxy.
waitlist-guard command
admin-guard is the single forward-auth gate that restricts Hanzo's RAW global-admin surfaces (platform.hanzo.ai, studio, commerce-admin, the raw KMS admin UI, the IAM management UI) to GLOBAL ADMINS ONLY — an IAM user whose org (`owner`) is the admin org (IAM `IsGlobalAdmin`: owner == AdminOrg).
admin-guard is the single forward-auth gate that restricts Hanzo's RAW global-admin surfaces (platform.hanzo.ai, studio, commerce-admin, the raw KMS admin UI, the IAM management UI) to GLOBAL ADMINS ONLY — an IAM user whose org (`owner`) is the admin org (IAM `IsGlobalAdmin`: owner == AdminOrg).
internal
hanzolog
Package hanzolog backs the engine's logging.Logger with hanzoai/log, the one logging library every Hanzo Go service uses.
Package hanzolog backs the engine's logging.Logger with hanzoai/log, the one logging library every Hanzo Go service uses.
lura/backoff
Package backoff contains some basic implementations and a selector by strategy name
Package backoff contains some basic implementations and a selector by strategy name
lura/config
Package config defines the config structs and some config parser interfaces and implementations
Package config defines the config structs and some config parser interfaces and implementations
lura/core
Package core contains some basic constants and variables
Package core contains some basic constants and variables
lura/encoding
Package encoding provides basic decoding implementations.
Package encoding provides basic decoding implementations.
lura/logging
Package logging provides a simple logger interface and implementations
Package logging provides a simple logger interface and implementations
lura/plugin
Package plugin provides tools for loading and registering plugins
Package plugin provides tools for loading and registering plugins
lura/proxy
Package proxy provides proxy and proxy middleware interfaces and implementations.
Package proxy provides proxy and proxy middleware interfaces and implementations.
lura/proxy/plugin
Package plugin provides tools for loading and registering proxy plugins
Package plugin provides tools for loading and registering proxy plugins
lura/register
Package register offers tools for creating and managing registers.
Package register offers tools for creating and managing registers.
lura/router
Package router defines some interfaces and common helpers for router adapters
Package router defines some interfaces and common helpers for router adapters
lura/router/gin
Package gin provides some basic implementations for building routers based on gin-gonic/gin
Package gin provides some basic implementations for building routers based on gin-gonic/gin
lura/router/mux
Package mux provides some basic implementations for building routers based on net/http mux
Package mux provides some basic implementations for building routers based on net/http mux
lura/sd
Package sd defines some interfaces and implementations for service discovery
Package sd defines some interfaces and implementations for service discovery
lura/sd/dnssrv
Package dnssrv defines some implementations for a dns based service discovery
Package dnssrv defines some implementations for a dns based service discovery
lura/transport/http/client
Package client provides some http helpers to create http clients and executors
Package client provides some http helpers to create http clients and executors
lura/transport/http/client/graphql
Package graphql offers a param extractor and basic types for building GraphQL requests
Package graphql offers a param extractor and basic types for building GraphQL requests
lura/transport/http/client/plugin
Package plugin provides plugin register interfaces for building http client plugins.
Package plugin provides plugin register interfaces for building http client plugins.
lura/transport/http/server
Package server provides tools to create http servers and handlers wrapping the lura router
Package server provides tools to create http servers and handlers wrapping the lura router
lura/transport/http/server/plugin
Package plugin provides plugin register interfaces for building http handler plugins.
Package plugin provides plugin register interfaces for building http handler plugins.
pkg/binder
Package binder allows to easily bind to Lua.
Package binder allows to easily bind to Lua.
pkg/bloomfilter
Package bloomfilter contains common data and interfaces needed to implement bloomfilters.
Package bloomfilter contains common data and interfaces needed to implement bloomfilters.
pkg/bloomfilter/bloomfilter
Package bbloomfilter implements a bloomfilter based on an m-bit bit array, k hashfilters and configuration.
Package bbloomfilter implements a bloomfilter based on an m-bit bit array, k hashfilters and configuration.
pkg/bloomfilter/register
Package register wires a rotating bloomfilter into the gateway from its extra_config block, exposing it over the internal RPC service.
Package register wires a rotating bloomfilter into the gateway from its extra_config block, exposing it over the internal RPC service.
pkg/bloomfilter/rotate
Package rotate implemennts a sliding set of three bloomfilters: `previous`, `current` and `next` and the bloomfilter interface.
Package rotate implemennts a sliding set of three bloomfilters: `previous`, `current` and `next` and the bloomfilter interface.
pkg/bloomfilter/rpc
Package rpc implements the rpc layer for the bloomfilter, following the principles from https://golang.org/pkg/net/rpc
Package rpc implements the rpc layer for the bloomfilter, following the principles from https://golang.org/pkg/net/rpc
pkg/bloomfilter/rpc/server
Package server implements an rpc server for the bloomfilter, registering a bloomfilter and accepting a tcp listener.
Package server implements an rpc server for the bloomfilter, registering a bloomfilter and accepting a tcp listener.
pkg/httpcache
Package httpcache provides a http.RoundTripper implementation that works as a mostly RFC-compliant cache for http responses.
Package httpcache provides a http.RoundTripper implementation that works as a mostly RFC-compliant cache for http responses.
plugin/audit
Package audit contains types and functions to summarize the features used in a configuration and to emit recommendations and comments when executing a check
Package audit contains types and functions to summarize the features used in a configuration and to emit recommendations and comments when executing a check
plugin/circuitbreaker/gobreaker
Package gobreaker provides a circuit breaker adapter using the sony/gobreaker lib.
Package gobreaker provides a circuit breaker adapter using the sony/gobreaker lib.
plugin/circuitbreaker/gobreaker/proxy
Package gobreaker provides a circuit breaker proxy middleware using the sony/gobreaker lib.
Package gobreaker provides a circuit breaker proxy middleware using the sony/gobreaker lib.
plugin/cobra
Package cmd defines the cobra command structs and an execution method for adding an improved CLI to KrakenD based api gateways
Package cmd defines the cobra command structs and an execution method for adding an improved CLI to KrakenD based api gateways
plugin/httpcache
Package httpcache introduces an in-memory-cached http client into the KrakenD stack
Package httpcache introduces an in-memory-cached http client into the KrakenD stack
plugin/koanf
Package koanf defines a config parser implementation based on the koanf pkg
Package koanf defines a config parser implementation based on the koanf pkg
plugin/metrics
Package metrics defines a set of basic building blocks for instrumenting the gateway.
Package metrics defines a set of basic building blocks for instrumenting the gateway.
plugin/metrics/gin
Package gin defines a set of basic building blocks for instrumenting KrakenD gateways built using the gin router
Package gin defines a set of basic building blocks for instrumenting KrakenD gateways built using the gin router
plugin/metrics/mux
Package mux defines a set of basic building blocks for instrumenting KrakenD gateways built using the mux router
Package mux defines a set of basic building blocks for instrumenting KrakenD gateways built using the mux router
plugin/ratelimit
krakendrate contains a collection of curated rate limit adaptors for the KrakenD framework
krakendrate contains a collection of curated rate limit adaptors for the KrakenD framework
plugin/ratelimit/proxy
Package proxy provides a rate-limit proxy middleware.
Package proxy provides a rate-limit proxy middleware.
plugin/ratelimit/router
Package router provides several rate-limit routers.
Package router provides several rate-limit routers.
loadgen
conn_holder command
Package main holds N idle keep-alive HTTP/1.1 connections against a target URL.
Package main holds N idle keep-alive HTTP/1.1 connections against a target URL.
Package middleware ships gateway-owned middleware for the zip web framework.
Package middleware ships gateway-owned middleware for the zip web framework.
Package token is the gateway's credential check: which issuer it trusts, which audiences it accepts, and the keys it verifies against.
Package token is the gateway's credential check: which issuer it trusts, which audiences it accepts, and the keys it verifies against.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL