Files
gateway/build_app.go
zeekay 7f972575a8 feat(gateway): ZAP relay via forward.Relay (envelope auth + identity inject)
cmd/gateway is now a ZAP->ZAP relay: forward.Relay(node, gate, pickPeer) authenticates
each inbound Forward on the envelope (JWT validate, clear-then-reassert identity, never
reads body), forwards to cloud/base over ZAP. Billing deferred to cloud-api's balance
gate. Replaces the 503 stub + deletes the old inconsistent zap_wire.go. //go:build
!legacy separates the edge relay from main_legacy (krakend). luxfi/[email protected].
2026-06-18 13:41:22 -07:00

124 lines
4.3 KiB
Go

// Copyright © 2026 Hanzo AI. MIT 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"
"net/http"
"time"
luxlog "github.com/luxfi/log"
zaplib "github.com/luxfi/zap"
"github.com/luxfi/zap/forward"
"github.com/hanzoai/zip"
"github.com/hanzoai/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
// BasePeerID / CloudPeerID are the handshake-learned NodeIDs of the
// already-connected base and cloud peers (from ConnectDirectID at dial
// time). pickPeer returns these for node.Call to resolve.
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-api 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 package iamauth + gateway's permission 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 := pickPeer(deps.BasePeerID, deps.CloudPeerID)
forward.Relay(deps.Node, gate, pick)
forward.RegisterReversePushHandler(deps.Node)
deps.Logger.Info("gateway relay registered",
"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")
}
app := zip.New(zip.Config{
Logger: deps.Logger,
ServerHeader: "hanzoai",
DisableStartupMessage: true,
AppName: "gateway",
})
app.Use(
middleware.Recover(),
middleware.RequestID(),
middleware.Logger(deps.Logger),
)
app.Get("/healthz", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ok"})
})
app.Get("/readyz", func(c *zip.Ctx) error {
return c.JSON(http.StatusOK, map[string]string{"status": "ready"})
})
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
}