mirror of
https://github.com/hanzoai/gateway.git
synced 2026-10-30 15:04:44 +00:00
zip removed the standalone request logger when it made reporting automatic at build. The report goes on outermost, so it observes the requests that match no route and the ones a later middleware refuses — neither of which a Use() call in this list could see. Ten minors of zip arrive with it, including an answer naming its own address.
140 lines
5.4 KiB
Go
140 lines
5.4 KiB
Go
// Copyright © 2026 Hanzo AI. Apache-2.0 License.
|
|
|
|
// 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.
|
|
package gateway
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
|
|
luxlog "github.com/luxfi/log"
|
|
zaplib "github.com/luxfi/zap"
|
|
"github.com/luxfi/zap/forward"
|
|
|
|
"github.com/zap-proto/zip"
|
|
"github.com/zap-proto/zip/middleware"
|
|
)
|
|
|
|
// jwksTTL is the signing-key cache lifetime for the relay gate. Matches the
|
|
// edge cache used elsewhere; keys refresh past this, stale-on-error.
|
|
const jwksTTL = 5 * time.Minute
|
|
|
|
// RouterDeps is the set of dependencies BuildApp needs.
|
|
type RouterDeps struct {
|
|
Logger luxlog.Logger
|
|
ZAPNode *zaplib.Node
|
|
CloudAddr string
|
|
BaseAddr string
|
|
}
|
|
|
|
// RelayDeps is what RegisterRelay needs to stand up the ZAP relay on a node.
|
|
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
|
|
}
|
|
|
|
// 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 RegisterRelay(deps RelayDeps) error {
|
|
if deps.Logger == nil {
|
|
return errors.New("gateway.RegisterRelay: nil Logger")
|
|
}
|
|
if deps.Node == nil {
|
|
return errors.New("gateway.RegisterRelay: nil Node")
|
|
}
|
|
|
|
gate := newGate(deps.Auth)
|
|
pick := deps.Pick
|
|
if pick == nil {
|
|
pick = pickPeer(deps.BasePeerID, deps.CloudPeerID)
|
|
}
|
|
|
|
forward.Relay(deps.Node, gate, pick)
|
|
forward.RegisterReversePushHandler(deps.Node)
|
|
|
|
deps.Logger.Info("gateway relay registered",
|
|
"lazy_picker", deps.Pick != nil,
|
|
"base_peer", deps.BasePeerID,
|
|
"cloud_peer", deps.CloudPeerID,
|
|
"auth_enabled", deps.Auth.Enabled,
|
|
"require_auth", deps.Auth.RequireAuth,
|
|
)
|
|
return nil
|
|
}
|
|
|
|
// 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 BuildApp(deps RouterDeps) (*zip.App, error) {
|
|
if deps.Logger == nil {
|
|
return nil, errors.New("gateway.BuildApp: nil Logger")
|
|
}
|
|
if deps.ZAPNode == nil {
|
|
return nil, errors.New("gateway.BuildApp: nil ZAPNode")
|
|
}
|
|
// This is the liveness/readiness HTTP surface only (real client traffic is
|
|
// the ZAP relay, whose responses carry the backend's own posture). A neutral,
|
|
// brand-agnostic Server header — never "hanzoai" (which white-label-leaks on
|
|
// lux/zoo) and never the framework name. The full brand-by-host posture rides
|
|
// the legacy edge today (ProductionHeadersMiddleware) and, once the zip pin
|
|
// clears the cloud/zip fiber-fork coupling, middleware.ProductionHeaders here.
|
|
app := zip.New(zip.Config{
|
|
Logger: deps.Logger,
|
|
ServerHeader: NeutralServerBrand,
|
|
DisableStartupMessage: true,
|
|
AppName: "gateway",
|
|
})
|
|
// No request logger here: an app reports because it serves. zip installs the
|
|
// report at build, outermost, so it observes the requests that match no route
|
|
// and the ones a later middleware refuses — which a Use() call in this list
|
|
// could not, and which is why the standalone middleware went away.
|
|
app.Use(
|
|
middleware.Recover(),
|
|
middleware.RequestID(),
|
|
)
|
|
// The one probe declaration (probes.go), shared with the k8s health
|
|
// listener and with the HIP-0106 mount. This app carries no other route:
|
|
// the relay is on the ZAP node, not here.
|
|
Probes(app)
|
|
deps.Logger.Info("gateway.BuildApp ready (relay on ZAP node; HTTP is liveness-only)",
|
|
"cloud_zap", deps.CloudAddr, "base_zap", deps.BaseAddr)
|
|
return app, nil
|
|
}
|