diff --git a/go/buscompose/accounts.go b/go/buscompose/accounts.go index 2f64e70..3a40eb5 100644 --- a/go/buscompose/accounts.go +++ b/go/buscompose/accounts.go @@ -219,6 +219,10 @@ type LeaseHolder struct { // HandedBy is the credential that wrote it (the operator's), and Why its reason. HandedBy string `json:"handed-by,omitempty"` Why string `json:"why,omitempty"` + // Ask is the operator's ask whose warrant handed it (novox/hq ADR 0582 clause 3: "with the why, the ask and the + // time"), and At when it was written. Neither turns the lease: LeaseHandedAway reads HandedTo alone. + Ask string `json:"ask,omitempty"` + At time.Time `json:"at,omitzero"` } // LeaseHandedAway reads the lease key as a getter answered it (its value, whether it was there, the read's error) and diff --git a/go/buscompose/buscompose_test.go b/go/buscompose/buscompose_test.go index 6c2f676..112ee09 100644 --- a/go/buscompose/buscompose_test.go +++ b/go/buscompose/buscompose_test.go @@ -561,6 +561,9 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) { }{ {"a controller holds it", `{"instance":"novox/1234/2026-10-27T20:00:00Z","epoch":57}`, true, nil, false}, {"handed away", `{"handed-to":"the operator's hand-over","handed-by":"person.jochen"}`, true, nil, true}, + // ADR 0582 clause 3 (774/S3): the write names the ask and the time, which turn nothing. + {"handed to mesh-grants with its ask and time", `{"handed-to":"mesh-grants","handed-by":"person.jochen","why":"w",` + + `"ask":"lease-write-1","at":"2026-11-02T10:00:00Z"}`, true, nil, true}, {"expired or deleted", ``, false, nil, false}, {"unreadable", ``, false, errors.New("refused"), false}, {"not a holder", `[1]`, true, nil, false}, diff --git a/go/delivery/delivery_test.go b/go/delivery/delivery_test.go index 0ddd2a3..8bdc8bd 100644 --- a/go/delivery/delivery_test.go +++ b/go/delivery/delivery_test.go @@ -242,9 +242,22 @@ func TestRoundTrip(t *testing.T) { t.Fatal(err) } back, err := DecodeGrantsUserList(raw) - if err != nil || back.Digest != digest || back.LeftOut == nil || back.Follow != FollowGrants { + if err != nil || back.Digest != digest || back.LeftOut == nil || back.Follow != FollowGrants || back.LeaseHanded { t.Fatalf("decoded %+v, %v", back, err) } + // ADR 0582 clause 3 (issue 774, 774/S3): a key written before the flag existed reads not handed; one written with + // it reads handed. + if old, err := DecodeGrantsUserList([]byte(`{"v":1,"follow":"grants","digest":"","object":"","sealed_to":"",` + + `"node":"novox","revision":3,"users":40,"left_out":[],"at":"2026-10-30T10:00:00Z"}`)); err != nil || old.LeaseHanded { + t.Errorf("a key from before the lease write decodes %+v, %v", old, err) + } + g.LeaseHanded = true + if raw, err := EncodeGrantsUserList(g); err != nil || !bytes.Contains(raw, []byte(`"lease-handed":true`)) { + t.Errorf("the handed flag is written as %s, %v", raw, err) + } else if back, err := DecodeGrantsUserList(raw); err != nil || !back.LeaseHanded { + t.Errorf("the handed flag reads back %+v, %v", back, err) + } + g.LeaseHanded = false for _, bad := range []GrantsUserList{{Follow: "both", Digest: digest, Object: g.Object}, {Follow: FollowGrants, Digest: digest, Object: "other"}, {Follow: FollowGrants, Digest: "md5:x", Object: "md5:x"}} { if _, err := EncodeGrantsUserList(bad); err == nil { diff --git a/go/delivery/userlist.go b/go/delivery/userlist.go index 0860350..ea27e77 100644 --- a/go/delivery/userlist.go +++ b/go/delivery/userlist.go @@ -65,6 +65,10 @@ type GrantsUserList struct { // LeftOut are the users the list leaves out for want of a hash in the vault. LeftOut []string `json:"left_out"` At time.Time `json:"at"` + // LeaseHanded says the controller's lease is handed to the grants part (novox/hq ADR 0582 clause 3, ADR 0571 clause + // 8): from then on the bus node follows this list alone and never falls back to the controller's. A key written + // before it reads false. + LeaseHanded bool `json:"lease-handed,omitempty"` } // EncodeGrantsUserList is g as JSON at Version; it refuses a value the bus node could not follow.