13 // Schema is the supported envelope and payload schema version.
16 // MeasurementRepeatInterval bounds equal retained measurement repeats.
17 MeasurementRepeatInterval = 5 * time.Minute
19 // OnlineRepeatInterval bounds equal online availability repeats.
20 OnlineRepeatInterval = 5 * time.Minute
22 // DeviceContactTimeout bounds the age of device contact evidence.
23 DeviceContactTimeout = 15 * time.Minute
26 // Metadata holds message identity and optional UTC times. Retries keep the same
27 // ID. A receiver checks ExpiresAt against its clock before processing.
28 type Metadata struct {
29 MessageID string `json:"message_id"`
30 SentAt *time.Time `json:"sent_at,omitempty"`
31 ExpiresAt *time.Time `json:"expires_at,omitempty"`
32 CorrelationID string `json:"correlation_id,omitempty"`
33 CausationID string `json:"causation_id,omitempty"`
36 // Envelope carries one JSON object and its schema and metadata.
37 type Envelope struct {
38 Schema int `json:"schema"`
40 Payload json.RawMessage `json:"payload"`
43 // EncodeEnvelope validates metadata and encodes a schema-1 object payload.
44 func EncodeEnvelope(metadata Metadata, payload any) ([]byte, error) {
45 encoded, err := json.Marshal(payload)
47 return nil, fmt.Errorf("encode payload: %w", err)
55 err = validateEnvelope(envelope)
57 return nil, fmt.Errorf("validate envelope: %w", err)
60 encoded, err = json.Marshal(envelope)
62 return nil, fmt.Errorf("encode envelope: %w", err)
68 // ParseEnvelope rejects unknown fields, trailing JSON, unsupported schemas,
69 // invalid IDs, non-UTC times, and missing or non-object payloads.
70 func ParseEnvelope(data []byte) (Envelope, error) {
73 err := decodeObject(data, &envelope)
75 return Envelope{}, fmt.Errorf("decode envelope: %w", err)
78 err = validateEnvelope(envelope)
80 return Envelope{}, fmt.Errorf("validate envelope: %w", err)
86 func validateEnvelope(envelope Envelope) error {
87 if envelope.Schema != Schema {
88 return fmt.Errorf("unsupported schema %d", envelope.Schema)
91 if !ValidID(envelope.MessageID) {
92 return fmt.Errorf("message ID must be a canonical ULID")
95 if envelope.CorrelationID != "" && !ValidID(envelope.CorrelationID) {
96 return fmt.Errorf("correlation ID must be a canonical ULID")
99 if envelope.CausationID != "" && !ValidID(envelope.CausationID) {
100 return fmt.Errorf("causation ID must be a canonical ULID")
103 for _, timestamp := range []*time.Time{envelope.SentAt, envelope.ExpiresAt} {
104 if timestamp != nil {
105 _, offset := timestamp.Zone()
107 return fmt.Errorf("message times must use UTC")
112 if !isObject(envelope.Payload) {
113 return fmt.Errorf("payload must be a JSON object")
119 func decodeObject(data []byte, target any) error {
121 return fmt.Errorf("expected one JSON object")
124 decoder := json.NewDecoder(bytes.NewReader(data))
125 decoder.DisallowUnknownFields()
127 err := decoder.Decode(target)
129 return fmt.Errorf("decode object: %w", err)
134 err = decoder.Decode(&extra)
135 if !errors.Is(err, io.EOF) {
136 return fmt.Errorf("expected end of JSON object")
142 func isObject(data []byte) bool {
143 data = bytes.TrimSpace(data)
144 return len(data) > 0 && data[0] == '{' && json.Valid(data)