Documentation
¶
Overview ¶
Package forward is the canonical HIP-0110 "HTTP-over-ZAP" contract.
It is the single source of truth for the gateway↔backend boundary, replacing the two inconsistent ad-hoc encodings that previously lived in the gateway repo (zap_wire.go and zap_backend.go). Three envelopes cover the entire boundary:
Forward — gateway → backend, one per inbound HTTP request, carrying
the edge-validated identity (so the backend trusts the edge).
Response — backend → gateway, the buffered HTTP response for a Forward.
Push — backend → gateway, one per server-initiated stream frame
(SSE / WebSocket), correlated to a Forward by ConnID.
On-wire bytes are github.com/luxfi/zap Objects. Every field is packed at an explicit byte offset within the object's fixed payload.
Offset discipline ¶
luxfi/zap's ObjectBuilder.SetBytes / SetText write an 8-byte slot at the field offset: {relOffset uint32, length uint32}. The variable- length data tail is appended after the fixed section in Finish(), and the relOffset is patched to point at it. Consequently EVERY text or bytes field consumes 8 bytes of fixed payload — adjacent text fields MUST be spaced 8 bytes apart or their slots overlap and corrupt each other (proven empirically: a 4-byte-spaced layout drops the body).
Fixed scalars use their natural width (bool=1, uint32=4, int64=8) and are placed on a natural boundary. These offsets are the contract; changing one is a wire break.
Index ¶
- Constants
- func BuildForward(f Forward) (*zaplib.Message, error)
- func BuildPush(p Push) (*zaplib.Message, error)
- func BuildResponse(r Response) (*zaplib.Message, error)
- func HandleReversePush(ctx context.Context, from string, msg *zaplib.Message) (*zaplib.Message, error)
- func RegisterReversePush(connID string, sink PushSink)
- func RegisterReversePushHandler(node *zaplib.Node)
- func Relay(node *zaplib.Node, gate Gate, pick PeerPicker)
- func Serve(node *zaplib.Node, h http.Handler)
- func UnregisterReversePush(connID string)
- type Forward
- type Forwarder
- type Gate
- type PeerPicker
- type Push
- type PushSink
- type Response
Constants ¶
const ( MsgTypeForward uint16 = 0x80 MsgTypePush uint16 = 0x81 )
HIP-0110 message-type IDs. The ZAP dispatcher routes on msg.Flags()>>8, and FinishWithFlags(t<<8) tags the message, so a type must fit in the upper byte of a uint16 (i.e. uint8). The 0x80+ range avoids collision with base/plugins/zap's lower-byte IDs (Collections=100, Records=101, Auth=102, Realtime=103).
const ( HeaderOrgID = "X-Org-Id" HeaderUserID = "X-User-Id" HeaderUserIsAdmin = "X-User-IsAdmin" HeaderUserPerms = "X-User-Permissions" )
Identity headers injected by the service side so the backend trusts the edge's pre-validated identity. These follow the vendor-free X-* header convention (no X-Hanzo-*, no X-IAM-*).
const ( EncSSE = "sse" EncWSText = "ws-text" EncWSBinary = "ws-binary" )
Canonical Encoding values on a Push envelope.
Variables ¶
This section is empty.
Functions ¶
func BuildForward ¶
BuildForward serializes f into a ZAP message tagged MsgTypeForward.
func BuildResponse ¶
BuildResponse serializes r into a ZAP message. Status rides as a uint32; body and headers-JSON follow, matching the Response read order.
func HandleReversePush ¶
func HandleReversePush(ctx context.Context, from string, msg *zaplib.Message) (*zaplib.Message, error)
HandleReversePush is the zaplib.Handler for MsgTypePush. It decodes the Push, looks up the sink for its ConnID, and delivers. An unknown ConnID (client already gone) is a silent no-op. A delivery error drops the sink.
func RegisterReversePush ¶
RegisterReversePush records the sink for an active SSE / WS subscription.
func RegisterReversePushHandler ¶
RegisterReversePushHandler installs HandleReversePush on node so a Forwarder's node receives backend-initiated Push frames.
func Relay ¶
func Relay(node *zaplib.Node, gate Gate, pick PeerPicker)
Relay registers the gateway relay handler on node for MsgTypeForward.
On each inbound Forward the relay:
- ReadForward(msg) — decodes the envelope (Body stays a sub-slice; it is not decoded).
- gate(ctx, &f) — runs auth + the balance gate on the envelope. A non-nil deny Response short-circuits: the relay returns it immediately via BuildResponse and NEVER touches a backend. A non-nil error is returned to the ZAP layer. Otherwise the gate has mutated f's identity fields in place and the request is allowed.
- BuildForward(f) — re-emits the Forward carrying the injected identity (the single per-request envelope re-encode; see the budget note above), then node.Call(ctx, pick(f.Path), augmented) forwards it to the chosen backend.
- returns the backend's reply *zaplib.Message VERBATIM — no ReadResponse/BuildResponse round-trip, the response bytes pass through unchanged.
func Serve ¶
Serve registers the canonical MsgTypeForward handler on node, dispatching each Forward envelope to h. The handler reconstructs the *http.Request, injects the edge-validated identity as X-* headers (so the backend trusts the edge), serves it through h, and returns a Response envelope carrying the captured status, headers, and body.
Streaming (SSE / chunked) is not yet emitted as Push frames: the full response is buffered and returned as one Response. The extension point is marked below.
func UnregisterReversePush ¶
func UnregisterReversePush(connID string)
UnregisterReversePush drops the sink when the client connection closes.
Types ¶
type Forward ¶
type Forward struct {
TenantID string // X-Org-Id (JWT 'owner')
UserID string // X-User-Id (JWT 'sub')
IsAdmin bool // X-User-IsAdmin (JWT 'isAdmin')
Permissions int64 // X-User-Permissions (bit field)
Method string // GET, POST, ...
Path string // /v1/iam/users/123?expand=roles (query included)
Headers []byte // JSON map[string][]string, canonicalized client headers
Body []byte // raw client body, verbatim
ConnID string // gateway-assigned conn id for reverse push
}
Forward is the typed view of the gateway→backend envelope. Path includes the raw query string (there is no separate query field).
func ReadForward ¶
ReadForward decodes a Forward envelope from a parsed message.
type Forwarder ¶
type Forwarder struct {
Node *zaplib.Node // started ZAP node
Peer string // backend NodeID to Call
}
Forwarder is the gateway-side client: it tunnels an *http.Request to a backend Peer over ZAP and returns the *http.Response. It is an http.RoundTripper, so it drops into any http.Client/transport chain.
type Gate ¶
Gate is the gateway-supplied policy run on each inbound Forward ENVELOPE. It reads f.Headers (to find and validate the JWT) and f.Path, runs the prepaid balance gate, and EITHER:
- mutates f's identity fields in place — f.TenantID, f.UserID, f.IsAdmin, f.Permissions — and returns (nil, nil) to ALLOW, or
- returns a non-nil *Response to short-circuit (deny), e.g. {Status: 401} for a bad token or {Status: 402, Body: insufficient_balance JSON} when the balance gate trips.
A Gate MUST NOT read or copy f.Body. It operates on the envelope only. If it returns a non-nil error the relay surfaces it to the ZAP layer (the caller's Call fails); use a deny *Response for ordinary policy rejections so the client sees a clean HTTP status.
type PeerPicker ¶
PeerPicker chooses the backend node for a request path. It mirrors pickBackend from the gateway: /v1/base/* routes to the base peer, everything else routes to the cloud peer (which fans out to the per-subsystem services). It is given the Forward's Path (raw query included) and returns the backend NodeID to Call.
type Push ¶
type Push struct {
ConnID string // correlates to the Forward's ConnID
Frame []byte // pre-encoded SSE / WebSocket payload
Encoding string // "sse" | "ws-text" | "ws-binary"
}
Push is the typed view of the backend→gateway reverse-push envelope.
type Response ¶
type Response struct {
Status int // HTTP status code
Headers []byte // JSON map[string][]string
Body []byte // raw response body, verbatim
}
Response is the typed view of the backend→gateway response envelope.
func ReadResponse ¶
ReadResponse decodes a Response envelope from a parsed message.