forward

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: BSD-3-Clause Imports: 11 Imported by: 0

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

View Source
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).

View Source
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-*).

View Source
const (
	EncSSE      = "sse"
	EncWSText   = "ws-text"
	EncWSBinary = "ws-binary"
)

Canonical Encoding values on a Push envelope.

Variables

This section is empty.

Functions

func BuildForward

func BuildForward(f Forward) (*zaplib.Message, error)

BuildForward serializes f into a ZAP message tagged MsgTypeForward.

func BuildPush

func BuildPush(p Push) (*zaplib.Message, error)

BuildPush serializes p into a ZAP message tagged MsgTypePush.

func BuildResponse

func BuildResponse(r Response) (*zaplib.Message, error)

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

func RegisterReversePush(connID string, sink PushSink)

RegisterReversePush records the sink for an active SSE / WS subscription.

func RegisterReversePushHandler

func RegisterReversePushHandler(node *zaplib.Node)

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:

  1. ReadForward(msg) — decodes the envelope (Body stays a sub-slice; it is not decoded).
  2. 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.
  3. 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.
  4. returns the backend's reply *zaplib.Message VERBATIM — no ReadResponse/BuildResponse round-trip, the response bytes pass through unchanged.

func Serve

func Serve(node *zaplib.Node, h http.Handler)

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

func ReadForward(msg *zaplib.Message) Forward

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.

func (*Forwarder) RoundTrip

func (f *Forwarder) RoundTrip(req *http.Request) (*http.Response, error)

RoundTrip serializes req into a Forward (lifting the edge-validated identity out of the X-* headers if present), Calls the peer, and rebuilds the *http.Response from the Response envelope.

type Gate

type Gate func(ctx context.Context, f *Forward) (deny *Response, err error)

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

type PeerPicker func(path string) (peerID string)

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.

func ReadPush

func ReadPush(msg *zaplib.Message) Push

ReadPush decodes a Push envelope from a parsed message.

type PushSink

type PushSink interface {
	Deliver(ctx context.Context, p Push) error
}

PushSink delivers a Push frame to a live client connection.

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

func ReadResponse(msg *zaplib.Message) Response

ReadResponse decodes a Response envelope from a parsed message.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL