package types import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" ) // ExportRecord mirrors a line/event from plc.directory export stream. type ExportRecord struct { Seq uint64 `json:"seq"` DID string `json:"did"` CreatedAt string `json:"createdAt,omitempty"` CID string `json:"cid,omitempty"` Nullified bool `json:"nullified,omitempty"` Operation json.RawMessage `json:"operation"` } type ParsedOperation struct { DID string CanonicalBytes []byte Payload map[string]any Sequence uint64 CID string Prev string RawRecord ExportRecord } func CanonicalizeJSON(raw []byte) ([]byte, error) { trimmed := bytes.TrimSpace(raw) if len(trimmed) == 0 { return nil, fmt.Errorf("empty json payload") } if !json.Valid(trimmed) { return nil, fmt.Errorf("invalid json payload") } var out bytes.Buffer if err := json.Compact(&out, trimmed); err != nil { return nil, fmt.Errorf("compact json: %w", err) } return out.Bytes(), nil } func ParseOperation(rec ExportRecord) (ParsedOperation, error) { if rec.DID == "" { return ParsedOperation{}, fmt.Errorf("missing DID") } canonical, err := CanonicalizeJSON(rec.Operation) if err != nil { return ParsedOperation{}, err } payload := map[string]any{} if err := json.Unmarshal(canonical, &payload); err != nil { return ParsedOperation{}, fmt.Errorf("decode operation: %w", err) } prev, _ := payload["prev"].(string) cid := rec.CID if cid == "" { cid = ComputeDigestCID(canonical) } return ParsedOperation{ DID: rec.DID, CanonicalBytes: canonical, Payload: payload, Sequence: rec.Seq, CID: cid, Prev: prev, RawRecord: rec, }, nil } func ComputeDigestCID(payload []byte) string { sum := sha256.Sum256(payload) return "sha256:" + hex.EncodeToString(sum[:]) }