package membership import ( "bytes" "encoding/json" "errors" "fmt" ) // The grants part's state memberships (buscompose.GrantsMembershipsState) holds, at `.`, the bare // membership as the assignments stream carries it. The writer and every reader use this one codec, so the two sides // cannot drift: until 2026-10-30 the grants part wrote its own wrapper ({node, module, digest, membership, published, // retired}) there while the tool runner read the value as a bare membership, and served nothing (issue 774). // GrantsBucket is the grants part's state memberships (buscompose.GrantsMembershipsBucket, which a test keeps agreeing), // and GrantsKey an assignment's key in it, `.`: where a runtime reads its membership (FollowMembership). const GrantsBucket = "mesh-grants_memberships" // GrantsKey is an assignment's key in GrantsBucket. func GrantsKey(node, module string) string { return node + "." + module } // EncodeGrantsMembership is the value of a membership's key in the grants part's memberships: the bare membership. func EncodeGrantsMembership(m Membership) []byte { raw, _ := json.Marshal(m) // a Membership holds nothing json cannot write return raw } // DecodeGrantsMembership reads a value of the grants part's memberships (BareGrantsMembership) as a Membership. func DecodeGrantsMembership(raw []byte) (Membership, error) { var m Membership body, err := BareGrantsMembership(raw) if err != nil { return m, err } err = json.Unmarshal(body, &m) return m, err } // BareGrantsMembership is the bare membership a value of the grants part's memberships holds, for a reader that // decodes it into a type of its own. It accepts the bare membership and, for the transition, the wrapper the grants // part wrote before (a top-level object whose "membership" is an object), answering the membership inside it only when // its "published" says "published" or "unchanged". A value that is not a JSON object, a wrapper of a membership held // back or saying nothing of its publishing, or a membership naming no node or no module, is an error, never an empty // or unpublished membership. func BareGrantsMembership(raw []byte) (json.RawMessage, error) { var probe struct { Membership json.RawMessage `json:"membership"` Published *string `json:"published"` } if err := json.Unmarshal(raw, &probe); err != nil { return nil, fmt.Errorf("not a membership: %w", err) } body := raw if inner := bytes.TrimSpace(probe.Membership); len(inner) > 0 { if inner[0] != '{' { return nil, errors.New("not a membership: its membership is no object") } // The old wrapper was written for a membership held back too, its composed and unpublished membership inside: // only one published (or unchanged) is the stream's, so any other is refused and the reader keeps the stream's. switch { case probe.Published == nil: return nil, errors.New("not a published membership: the wrapper says nothing of its publishing") case *probe.Published != "published" && *probe.Published != "unchanged": return nil, fmt.Errorf("not a published membership: the wrapper says %q", *probe.Published) } body = inner } var m Membership if err := json.Unmarshal(body, &m); err != nil { return nil, fmt.Errorf("not a membership: %w", err) } if m.Node == "" || m.Module == "" { return nil, errors.New("not a membership: it names no node or no module") } return json.RawMessage(body), nil }