mirror of
https://github.com/hanzoai/gateway.git
synced 2026-11-02 15:19:24 +00:00
The trust boundary's strip did not survive its own bridge. gateway.Mount — the HIP-0106 boundary the unified cloud binary runs, and the one piece of code whose entire job is to delete client-supplied identity — reached the gin middleware through zip.AdaptNetHTTPMiddleware. fiber's adaptor copies the middleware's headers back onto the fasthttp request with Set and never Del, so every header the strip DELETED was restored from the original request. A client-supplied X-User-IsAdmin: true reached the downstream subsystem intact. Reproduced against the old bridge before deleting it; TestZipAuth_StripSurvives is the guard. The cause was braiding: the ALLOW/DENY ladder was written out inside a gin closure, so the zip edge could not call it and bridged to it instead. The ladder is now a value — authGate.admit in authpolicy.go, which knows about no framework — and each transport is a dozen lines over it: gin for the legacy Lura edge, native zip for the mount. TestBothTransportsAdmitAlike drives one request through both and asserts they agree, across refuse / tokenless / public path / public host / ingest / API key / valid token. Every gate is unchanged; the 90KB security suite passes untouched, which is the proof. The probes are TYPED ops. Five anonymous closures across three apps each wrote their own map literal, so one answer had five spellings and none was in any document — an untyped route appends nothing to the op registry, so no OpenAPI operation, no MCP tool, no CLI, no SDK method. One declaration in probes.go now serves all three mount points, and zipdoc_gen.go carries the prose. Route table: BuildApp 2/2 typed, health app 3 routes/2 typed (/metrics is the one hatch — Prometheus exposition is text, and the reason is written next to it), Mount 1 route/0 typed → 2/2. Counts are asserted, not described: TestBuildApp_IsProbesAndNothingElse, TestMount_ProbesAreTypedOps and TestHealthApp_TableIsInTheBinary, which boots the app main boots rather than mounting the table directly. internal/lura and internal/plugin are untouched: they are vendored KrakenD/Lura, they are what the shipping `legacy` image runs, and rewriting a fork's router buys permanent merge pain. They go at Phase C with the rest of the legacy set. What left is the gin RUNTIME on the default build — no first-party path constructs a gin engine any more; the three files that still import it export transports consumed solely by legacy_engine.go. Co-authored-by: Hanzo Dev <[email protected]>
137 lines
5.1 KiB
Go
137 lines
5.1 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",
|
|
})
|
|
app.Use(
|
|
middleware.Recover(),
|
|
middleware.RequestID(),
|
|
middleware.Logger(deps.Logger),
|
|
)
|
|
// 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
|
|
}
|