// Package frame defines the wire format of the unibus message bus. // // A Frame is the unit transported over NATS. It carries an envelope (type, // subject, sender, message id, epoch) plus an optional payload that, in // encrypted rooms, is an AEAD ciphertext. Frames may be signed end-to-end with // Ed25519 so that any receiver can authenticate the sender without trusting the // transport. // // v1 serializes frames as JSON for legibility and forward-compatibility. The // canonical signing bytes are the JSON of the frame with Sig cleared, so that // signature verification is independent of field ordering as long as the // marshaler is deterministic (encoding/json emits struct fields in declaration // order). package frame import ( "crypto/sha256" "encoding/base64" "encoding/json" ) // FrameType enumerates the kinds of frames that travel over the bus. type FrameType uint8 const ( // PUB is a published message to a room/subject. PUB FrameType = iota // INVITE announces a new member was invited (informational; the key // distribution itself happens over the control plane). INVITE // JOIN announces a member joined a room. JOIN // LEAVE announces a member voluntarily left a room. LEAVE // KICK announces a member was removed from a room. KICK // ACK acknowledges receipt of a previous frame. ACK ) // BlobRef references an out-of-band encrypted blob stored in the object store. // The bus never carries blob bytes; only this reference travels over NATS. type BlobRef struct { Hash string `json:"h"` // sha256 hex of the ciphertext in the blob store Nonce []byte `json:"n"` // AEAD nonce used to encrypt the blob Size int64 `json:"sz"` // size in bytes of the ciphertext } // Frame is the unit of transport on the unibus message bus. type Frame struct { Type FrameType `json:"t"` Subject string `json:"s"` Sender string `json:"from"` // endpoint id = EndpointID(signPub) MsgID string `json:"id"` // ULID Epoch int `json:"e"` // epoch of the room key K used to encrypt Nonce []byte `json:"n,omitempty"` // AEAD nonce (encrypted rooms only) Payload []byte `json:"p,omitempty"` // AEAD ciphertext (or cleartext if the room does not encrypt) Blob *BlobRef `json:"b,omitempty"` Sig []byte `json:"sig,omitempty"` // Ed25519 signature over SigningBytes() } // Marshal serializes the frame to its wire representation (JSON in v1). func (f Frame) Marshal() ([]byte, error) { return json.Marshal(f) } // Unmarshal parses a wire representation back into a Frame. func Unmarshal(b []byte) (Frame, error) { var f Frame err := json.Unmarshal(b, &f) return f, err } // EndpointID derives a stable, transport-agnostic endpoint identifier from an // Ed25519 signing public key: base64url(sha256(signPub)), unpadded. func EndpointID(signPub []byte) string { sum := sha256.Sum256(signPub) return base64.RawURLEncoding.EncodeToString(sum[:]) } // SigningBytes returns the canonical bytes that are signed and verified. The // signature covers the entire frame except the Sig field itself, so we clear // Sig before marshaling. Errors are intentionally swallowed: the frame is a // plain struct of JSON-serializable fields, so json.Marshal cannot fail here. func (f Frame) SigningBytes() []byte { f.Sig = nil b, _ := json.Marshal(f) return b }