Files
zeekay 6dcfe7e90b Depend on zip v1.34.1; the framework reports, so this list does not
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.
2026-08-22 15:24:14 -07:00

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
}