@real-life/trust-protocol 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/CHANGELOG.md +97 -0
  2. package/README.md +209 -78
  3. package/dist/ack.d.ts +84 -0
  4. package/dist/ack.d.ts.map +1 -0
  5. package/dist/ack.js +114 -0
  6. package/dist/ack.js.map +1 -0
  7. package/dist/adapters/vti-mediator.d.ts +169 -0
  8. package/dist/adapters/vti-mediator.d.ts.map +1 -0
  9. package/dist/adapters/vti-mediator.js +659 -0
  10. package/dist/adapters/vti-mediator.js.map +1 -0
  11. package/dist/anchor.d.ts +226 -0
  12. package/dist/anchor.d.ts.map +1 -0
  13. package/dist/anchor.js +347 -0
  14. package/dist/anchor.js.map +1 -0
  15. package/dist/{holder.d.ts → carrier-holder.d.ts} +1 -1
  16. package/dist/carrier-holder.d.ts.map +1 -0
  17. package/dist/{holder.js → carrier-holder.js} +5 -4
  18. package/dist/carrier-holder.js.map +1 -0
  19. package/dist/carrier-identity.js +4 -4
  20. package/dist/carrier-identity.js.map +1 -1
  21. package/dist/carrier.d.ts.map +1 -1
  22. package/dist/carrier.js +4 -3
  23. package/dist/carrier.js.map +1 -1
  24. package/dist/ceremony.d.ts +3 -2
  25. package/dist/ceremony.d.ts.map +1 -1
  26. package/dist/ceremony.js +16 -26
  27. package/dist/ceremony.js.map +1 -1
  28. package/dist/codec.d.ts +54 -0
  29. package/dist/codec.d.ts.map +1 -0
  30. package/dist/codec.js +494 -0
  31. package/dist/codec.js.map +1 -0
  32. package/dist/continuity.d.ts +67 -0
  33. package/dist/continuity.d.ts.map +1 -0
  34. package/dist/continuity.js +110 -0
  35. package/dist/continuity.js.map +1 -0
  36. package/dist/core.d.ts +4 -0
  37. package/dist/core.d.ts.map +1 -1
  38. package/dist/core.js.map +1 -1
  39. package/dist/crypto.d.ts +23 -7
  40. package/dist/crypto.d.ts.map +1 -1
  41. package/dist/crypto.js +54 -20
  42. package/dist/crypto.js.map +1 -1
  43. package/dist/delivery.d.ts +31 -3
  44. package/dist/delivery.d.ts.map +1 -1
  45. package/dist/delivery.js +75 -28
  46. package/dist/delivery.js.map +1 -1
  47. package/dist/encounter/ceremony.d.ts +37 -0
  48. package/dist/encounter/ceremony.d.ts.map +1 -0
  49. package/dist/encounter/ceremony.js +662 -0
  50. package/dist/encounter/ceremony.js.map +1 -0
  51. package/dist/encounter.d.ts +91 -1
  52. package/dist/encounter.d.ts.map +1 -1
  53. package/dist/encounter.js +191 -12
  54. package/dist/encounter.js.map +1 -1
  55. package/dist/engine.d.ts +303 -0
  56. package/dist/engine.d.ts.map +1 -0
  57. package/dist/engine.js +1509 -0
  58. package/dist/engine.js.map +1 -0
  59. package/dist/entropy.d.ts +28 -0
  60. package/dist/entropy.d.ts.map +1 -0
  61. package/dist/entropy.js +96 -0
  62. package/dist/entropy.js.map +1 -0
  63. package/dist/events.d.ts +198 -0
  64. package/dist/events.d.ts.map +1 -0
  65. package/dist/events.js +2 -0
  66. package/dist/events.js.map +1 -0
  67. package/dist/fixtures.d.ts +30 -0
  68. package/dist/fixtures.d.ts.map +1 -0
  69. package/dist/fixtures.js +155 -0
  70. package/dist/fixtures.js.map +1 -0
  71. package/dist/forge.d.ts +47 -0
  72. package/dist/forge.d.ts.map +1 -0
  73. package/dist/forge.js +20 -0
  74. package/dist/forge.js.map +1 -0
  75. package/dist/group-star.d.ts +251 -0
  76. package/dist/group-star.d.ts.map +1 -0
  77. package/dist/group-star.js +348 -0
  78. package/dist/group-star.js.map +1 -0
  79. package/dist/holder-api.d.ts +165 -0
  80. package/dist/holder-api.d.ts.map +1 -0
  81. package/dist/holder-api.js +142 -0
  82. package/dist/holder-api.js.map +1 -0
  83. package/dist/holder-state-v1.d.ts +801 -0
  84. package/dist/holder-state-v1.d.ts.map +1 -0
  85. package/dist/holder-state-v1.js +2 -0
  86. package/dist/holder-state-v1.js.map +1 -0
  87. package/dist/holder-state-v2.d.ts +858 -0
  88. package/dist/holder-state-v2.d.ts.map +1 -0
  89. package/dist/holder-state-v2.js +2 -0
  90. package/dist/holder-state-v2.js.map +1 -0
  91. package/dist/identity-state.d.ts +43 -0
  92. package/dist/identity-state.d.ts.map +1 -0
  93. package/dist/identity-state.js +345 -0
  94. package/dist/identity-state.js.map +1 -0
  95. package/dist/identity.d.ts +24 -2
  96. package/dist/identity.d.ts.map +1 -1
  97. package/dist/identity.js +70 -11
  98. package/dist/identity.js.map +1 -1
  99. package/dist/index.d.ts +27 -1
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +81 -29
  102. package/dist/index.js.map +1 -1
  103. package/dist/internal.d.ts +37 -0
  104. package/dist/internal.d.ts.map +1 -0
  105. package/dist/internal.js +22 -0
  106. package/dist/internal.js.map +1 -0
  107. package/dist/introduction/holder.d.ts +18 -0
  108. package/dist/introduction/holder.d.ts.map +1 -0
  109. package/dist/introduction/holder.js +764 -0
  110. package/dist/introduction/holder.js.map +1 -0
  111. package/dist/introduction.d.ts +101 -0
  112. package/dist/introduction.d.ts.map +1 -0
  113. package/dist/introduction.js +132 -0
  114. package/dist/introduction.js.map +1 -0
  115. package/dist/invariants.d.ts +43 -0
  116. package/dist/invariants.d.ts.map +1 -0
  117. package/dist/invariants.js +953 -0
  118. package/dist/invariants.js.map +1 -0
  119. package/dist/keys-seed.d.ts +7 -0
  120. package/dist/keys-seed.d.ts.map +1 -0
  121. package/dist/keys-seed.js +133 -0
  122. package/dist/keys-seed.js.map +1 -0
  123. package/dist/log-memory.d.ts +12 -0
  124. package/dist/log-memory.d.ts.map +1 -0
  125. package/dist/log-memory.js +101 -0
  126. package/dist/log-memory.js.map +1 -0
  127. package/dist/membership/holder.d.ts +33 -0
  128. package/dist/membership/holder.d.ts.map +1 -0
  129. package/dist/membership/holder.js +929 -0
  130. package/dist/membership/holder.js.map +1 -0
  131. package/dist/membership.d.ts +123 -0
  132. package/dist/membership.d.ts.map +1 -0
  133. package/dist/membership.js +180 -0
  134. package/dist/membership.js.map +1 -0
  135. package/dist/message/holder.d.ts +49 -0
  136. package/dist/message/holder.d.ts.map +1 -0
  137. package/dist/message/holder.js +290 -0
  138. package/dist/message/holder.js.map +1 -0
  139. package/dist/message.d.ts +43 -0
  140. package/dist/message.d.ts.map +1 -0
  141. package/dist/message.js +51 -0
  142. package/dist/message.js.map +1 -0
  143. package/dist/ports.d.ts +237 -0
  144. package/dist/ports.d.ts.map +1 -0
  145. package/dist/ports.js +2 -0
  146. package/dist/ports.js.map +1 -0
  147. package/dist/profile.d.ts +56 -0
  148. package/dist/profile.d.ts.map +1 -0
  149. package/dist/profile.js +193 -0
  150. package/dist/profile.js.map +1 -0
  151. package/dist/prompts.d.ts +14 -0
  152. package/dist/prompts.d.ts.map +1 -0
  153. package/dist/prompts.js +69 -0
  154. package/dist/prompts.js.map +1 -0
  155. package/dist/receive.d.ts +134 -0
  156. package/dist/receive.d.ts.map +1 -0
  157. package/dist/receive.js +317 -0
  158. package/dist/receive.js.map +1 -0
  159. package/dist/results.d.ts +173 -0
  160. package/dist/results.d.ts.map +1 -0
  161. package/dist/results.js +21 -0
  162. package/dist/results.js.map +1 -0
  163. package/dist/schemas.d.ts.map +1 -1
  164. package/dist/schemas.js +45 -38
  165. package/dist/schemas.js.map +1 -1
  166. package/dist/sha256.d.ts +3 -0
  167. package/dist/sha256.d.ts.map +1 -0
  168. package/dist/sha256.js +69 -0
  169. package/dist/sha256.js.map +1 -0
  170. package/dist/star.d.ts +31 -0
  171. package/dist/star.d.ts.map +1 -0
  172. package/dist/star.js +56 -0
  173. package/dist/star.js.map +1 -0
  174. package/dist/state-builder.d.ts +42 -0
  175. package/dist/state-builder.d.ts.map +1 -0
  176. package/dist/state-builder.js +100 -0
  177. package/dist/state-builder.js.map +1 -0
  178. package/dist/state.d.ts +914 -0
  179. package/dist/state.d.ts.map +1 -0
  180. package/dist/state.js +37 -0
  181. package/dist/state.js.map +1 -0
  182. package/dist/store-contract.d.ts +11 -0
  183. package/dist/store-contract.d.ts.map +1 -0
  184. package/dist/store-contract.js +189 -0
  185. package/dist/store-contract.js.map +1 -0
  186. package/dist/store-memory.d.ts +8 -0
  187. package/dist/store-memory.d.ts.map +1 -0
  188. package/dist/store-memory.js +17 -0
  189. package/dist/store-memory.js.map +1 -0
  190. package/dist/testing.d.ts +22 -0
  191. package/dist/testing.d.ts.map +1 -0
  192. package/dist/testing.js +21 -0
  193. package/dist/testing.js.map +1 -0
  194. package/dist/trust/continuity.d.ts +71 -0
  195. package/dist/trust/continuity.d.ts.map +1 -0
  196. package/dist/trust/continuity.js +1222 -0
  197. package/dist/trust/continuity.js.map +1 -0
  198. package/dist/trust/duties.d.ts +67 -0
  199. package/dist/trust/duties.d.ts.map +1 -0
  200. package/dist/trust/duties.js +179 -0
  201. package/dist/trust/duties.js.map +1 -0
  202. package/dist/trust/entries.d.ts +139 -0
  203. package/dist/trust/entries.d.ts.map +1 -0
  204. package/dist/trust/entries.js +795 -0
  205. package/dist/trust/entries.js.map +1 -0
  206. package/dist/trust/group-star.d.ts +43 -0
  207. package/dist/trust/group-star.d.ts.map +1 -0
  208. package/dist/trust/group-star.js +188 -0
  209. package/dist/trust/group-star.js.map +1 -0
  210. package/dist/trust/mapping.d.ts +56 -0
  211. package/dist/trust/mapping.d.ts.map +1 -0
  212. package/dist/trust/mapping.js +584 -0
  213. package/dist/trust/mapping.js.map +1 -0
  214. package/dist/trust/profile.d.ts +25 -0
  215. package/dist/trust/profile.d.ts.map +1 -0
  216. package/dist/trust/profile.js +249 -0
  217. package/dist/trust/profile.js.map +1 -0
  218. package/dist/trust/star.d.ts +208 -0
  219. package/dist/trust/star.d.ts.map +1 -0
  220. package/dist/trust/star.js +588 -0
  221. package/dist/trust/star.js.map +1 -0
  222. package/dist/trust/state.d.ts +83 -0
  223. package/dist/trust/state.d.ts.map +1 -0
  224. package/dist/trust/state.js +154 -0
  225. package/dist/trust/state.js.map +1 -0
  226. package/dist/util-freeze.d.ts +3 -0
  227. package/dist/util-freeze.d.ts.map +1 -0
  228. package/dist/util-freeze.js +11 -0
  229. package/dist/util-freeze.js.map +1 -0
  230. package/dist/views.d.ts +179 -0
  231. package/dist/views.d.ts.map +1 -0
  232. package/dist/views.js +286 -0
  233. package/dist/views.js.map +1 -0
  234. package/dist/visibility.d.ts +5 -5
  235. package/dist/visibility.d.ts.map +1 -1
  236. package/dist/visibility.js +22 -28
  237. package/dist/visibility.js.map +1 -1
  238. package/dist/wire.d.ts +218 -83
  239. package/dist/wire.d.ts.map +1 -1
  240. package/dist/wire.js.map +1 -1
  241. package/package.json +60 -10
  242. package/schemas/holder-state-v1.schema.json +3880 -0
  243. package/schemas/holder-state-v2.schema.json +4123 -0
  244. package/dist/holder.d.ts.map +0 -1
  245. package/dist/holder.js.map +0 -1
  246. package/dist/probe/acks.d.ts +0 -27
  247. package/dist/probe/acks.d.ts.map +0 -1
  248. package/dist/probe/acks.js +0 -152
  249. package/dist/probe/acks.js.map +0 -1
  250. package/dist/probe/continuity.d.ts +0 -20
  251. package/dist/probe/continuity.d.ts.map +0 -1
  252. package/dist/probe/continuity.js +0 -805
  253. package/dist/probe/continuity.js.map +0 -1
  254. package/dist/probe/deps.d.ts +0 -61
  255. package/dist/probe/deps.d.ts.map +0 -1
  256. package/dist/probe/deps.js +0 -54
  257. package/dist/probe/deps.js.map +0 -1
  258. package/dist/probe/encounter.d.ts +0 -26
  259. package/dist/probe/encounter.d.ts.map +0 -1
  260. package/dist/probe/encounter.js +0 -938
  261. package/dist/probe/encounter.js.map +0 -1
  262. package/dist/probe/introduce.d.ts +0 -62
  263. package/dist/probe/introduce.d.ts.map +0 -1
  264. package/dist/probe/introduce.js +0 -306
  265. package/dist/probe/introduce.js.map +0 -1
  266. package/dist/probe/membership.d.ts +0 -96
  267. package/dist/probe/membership.d.ts.map +0 -1
  268. package/dist/probe/membership.js +0 -467
  269. package/dist/probe/membership.js.map +0 -1
  270. package/dist/probe/trust.d.ts +0 -156
  271. package/dist/probe/trust.d.ts.map +0 -1
  272. package/dist/probe/trust.js +0 -1233
  273. package/dist/probe/trust.js.map +0 -1
  274. package/dist/probe.d.ts +0 -7
  275. package/dist/probe.d.ts.map +0 -1
  276. package/dist/probe.js +0 -42
  277. package/dist/probe.js.map +0 -1
@@ -0,0 +1,929 @@
1
+ // membership/holder — groups on the holder (lib Stage 2, PR 7a; Membership
2
+ // Tasks 0.20 on Access 0.57, Delivery 0.82): founding, the prelude and
3
+ // the invite, the accept, admission and welcome, evidence and vouches, on
4
+ // HolderState and the engine. The pure forms and checks are the root
5
+ // module's (membership.ts).
6
+ //
7
+ // What it registers (membershipModule):
8
+ // human acts membership.found a group founded under a fresh pair
9
+ // label, its genesis signed (Access 3.4.1:
10
+ // that context is the founder's member
11
+ // anchor, its memberOp the genesis)
12
+ // membership.invite the prelude toward a contact (Membership
13
+ // 3.5); the invite follows its answer
14
+ // membership.vouch a vouch@2 for a held consent pair
15
+ // (Access 5.3), relayed as evidence
16
+ // decision group-invite: accept signs and sends the accept (3.2),
17
+ // decline closes
18
+ // tasks membership-invite/0.2 (proof absent), membership-accept/0.2
19
+ // (proof required), membership-evidence/0.2 and /0.1 (proof
20
+ // absent): channel bindings on member keys (receive.ts);
21
+ // membership-prelude-request/0.1 and membership-prelude-
22
+ // reply/0.1 (proof required, tuple binding; Membership 3.5),
23
+ // and x-membership-welcome/0.1 (channel binding), this
24
+ // library's experimental form; all signature-class: signed acks
25
+ // keepKeys the member keys of held groups and of groups on the way in,
26
+ // and the tuple rkid of every prelude sent and held, and of an
27
+ // answered one while its answer may still travel
28
+ // retain declined and expired invitations leave `joining`; a
29
+ // binding once its relationship ended and no open or accepted
30
+ // invitation rests on it (RLTP-MT-3945)
31
+ //
32
+ // Choices this file makes where spec and note leave them open:
33
+ // - the prelude (Membership 3.5) travels over the relationship tuple as
34
+ // its two registered types; the invitee's holder answers on its own
35
+ // (3960), registering its member label and key first (Identity 6.1),
36
+ // with a displayed member card (2.6) whose key the invite is sealed
37
+ // to. An answer is taken only on a thread whose request this holder
38
+ // sent and still holds, on its tuple (3965)
39
+ // - the welcome: Membership 3.3 carries it in access-operation/0.1 with
40
+ // a member.add envelope and a sealed rltp-welcome/0.1 (Section 4),
41
+ // Access's materialization deciding; until the Access envelope is
42
+ // built here, the admitting member sends x-membership-welcome/0.1,
43
+ // signed under its member anchor: the group's name, DID, genesis, rules
44
+ // and the roster it holds, the new member's entry naming its admission
45
+ // by the accept's document digest (its memberOp). Replication of the
46
+ // roster beyond that is the Access port's. Operation ids: the founder's
47
+ // memberOp is the genesis's oid (the genesis digest's SHA-256, Access
48
+ // 3.3), a member's the oid of its accept document's digest (a member admitted later is
49
+ // unknown to earlier members until their port materializes it)
50
+ // - the admission without a human act at the admitting member: the
51
+ // invitation stands as the inviter's act of admission (RLTP-MT-3282),
52
+ // so the accept (n = 1) or the evidence that completes the vouches
53
+ // admits in the same commit — before invite.validUntil +
54
+ // membership-skew + the longest give-up horizon by the inviter's
55
+ // clock; later the consent stays as evidence (an admission prompt is
56
+ // not built)
57
+ // - founding: the genesis admits under the rule of one (vouchThreshold
58
+ // "1"); a stricter rule needs policy.change, which is not built, so
59
+ // found refuses another (threshold-unsupported). A member's own vouch
60
+ // for its own invitee never counts beside its admission and is refused
61
+ // (own-admission)
62
+ // - the prelude is bound both ways: the request names the inviter's
63
+ // member anchor and the answering holder keeps the binding under
64
+ // (genesis digest, inviter) with the arrival tuple (3925), so an invite
65
+ // surfaces as the contact that asked as that inviter; the inviter files its request by thread
66
+ // (group, contact, own rkid) and takes an answer only for exactly that
67
+ // group on exactly that thread and tuple
68
+ // - invitations are kept per thread: a new invite opens its own thread
69
+ // beside earlier ones, so an accept signed in time on an earlier thread
70
+ // still answers it; an accept answers exactly the invitation of its
71
+ // thread, and an admission answers every invitation to its subject
72
+ // - the lock keys of 3950–3957 and 3980: the engine serializes every
73
+ // operation of a holder and commits by CAS over the whole state
74
+ // (receive.ts), a superset of every key the rules name
75
+ // - bounded state (tick): an issued invitation leaves past validUntil +
76
+ // membership-skew + the longest give-up horizon (P90D, RLTP-MT-3090),
77
+ // as does evidence (an accept signed in time may arrive that late), or
78
+ // once its subject is on the roster; an outgoing prelude past an
79
+ // invite's life and the horizon (its answer may travel that long); an
80
+ // answered prelude's binding once its relationship ended and no open
81
+ // or accepted invitation rests on it (3945); an accepted or joined invitation past
82
+ // validUntil + skew + two horizons — the accept's delivery and the
83
+ // welcome's — so the welcome finds the accepted state and the member
84
+ // key (kept while an invitation is open or accepted); an explicit abort
85
+ // is not built. A member key leaving keepKeys leaves `addresses`
86
+ // without a tombstone (an identity context, engine.ts tick), so a later
87
+ // invitation to the same group derives and uses the same key again
88
+ // - the prelude request proves its claimed inviter: a binding signed
89
+ // under the inviter's member key over (anchor, group, thread, and both
90
+ // pair anchors of the channel it travels on, checked against the
91
+ // document's issuer and recipient as stage 8 authenticated them), so
92
+ // no other contact can replay it on its own channel (3905, 3910); the
93
+ // answering holder keeps one binding per relationship (3930, 3935):
94
+ // a request from another relationship is refused while the bound one
95
+ // is current, whichever entries they feed. Residue: the binding links the inviter's member
96
+ // anchor to its pair channel transferably (the invite names that
97
+ // anchor before consent anyway)
98
+ // - the admission policy: the group rule vouchThreshold n reads as the
99
+ // `vouch` rule of member.add (Access 4.2) counting the admitting
100
+ // member: n distinct members stand behind an admission, the admitting
101
+ // member one of them, the others by vouch@2 credentials whose issuer
102
+ // is on its roster. n = 1 admits on the accept. Only the inviter (the
103
+ // holder that recorded the accept) admits; with n > 1 it relays the
104
+ // consent pair as membership-evidence/0.2 to the other members on its
105
+ // roster (candidacy false still permits this silent relay, 3250), and
106
+ // a member's vouch travels back to it as evidence with its vouch
107
+ // - membership-evidence: Delivery §4.4 still lists membership-evidence/
108
+ // 0.1; Membership 0.19 registers /0.2 and keeps /0.1 receivable
109
+ // (3815): both are registered here, /0.2 is the one issued (a gap to
110
+ // close in Delivery 0.82). A relayer this holder does not have on its
111
+ // roster cannot be acknowledged (its key is unknown): validation-failed
112
+ // where 3750 has the pending disposition incomplete(missing:
113
+ // group-state), which this engine does not have
114
+ // - the accept's candidacy is false (the decision carries only accept
115
+ // or decline); surfacing a candidacy (3765) is not built
116
+ // - the in-library Access (localAccess): where createHolder gets no
117
+ // Access port, every preparation sees one over its own snapshot's
118
+ // `groups`, so a group star opens between members whose rosters the
119
+ // holder holds: isMember by the roster, resolve(oid) by the roster
120
+ // entry whose op it is (its member anchor and card key)
121
+ // - a member group is listed in the group star (5420: listed unless
122
+ // switched off)
123
+ import { b64uOf, fromB64u, iso, sameDigest, toU } from '../core.js';
124
+ import { anchorOfEd, diSignWith, diVerify, digestBytes, digestDoc } from '../crypto.js';
125
+ import { seal } from '../delivery.js';
126
+ import { cardBody } from '../encounter.js';
127
+ import { ACCEPT_TYPE, EVIDENCE_TYPE, EVIDENCE_TYPE_01, INVITE_TYPE, MEMBERSHIP_SKEW, PRELUDE_REPLY_TYPE, PRELUDE_REQUEST_TYPE, acceptProblem, enclosedCardOk, evidenceProblem, inviteCredentialBody, inviteProblem, inviterBindingBody, memberLabelOf, membershipSchemaOk, oidOf, preludeRequestProblem, vouchCredentialBody, vouchProblem, withinBudget, } from '../membership.js';
128
+ import { withLabel, withPairNonce } from '../identity-state.js';
129
+ import { ED_PKCS8 } from '../keys-seed.js';
130
+ import { tupleByRkid } from '../receive.js';
131
+ import { INVITE_RATE, blockedRel, rateCount, relOfRkid, withRate } from '../message/holder.js';
132
+ import { entryOf, findContact, liveRel, activeTuple } from '../trust/state.js';
133
+ import { groupStarRematch, withGroupHits } from '../trust/group-star.js';
134
+ const TT = 'https://real-life.org/trust-tasks/';
135
+ /** This library's experimental form of the welcome (receive.ts SIGNATURE_CLASS). */
136
+ export const WELCOME_TYPE = TT + 'x-membership-welcome/0.1';
137
+ export { INVITE_TYPE, ACCEPT_TYPE, EVIDENCE_TYPE, PRELUDE_REQUEST_TYPE, PRELUDE_REPLY_TYPE };
138
+ /** How long an invite is answerable (validUntil − validFrom), as the probe had it. */
139
+ export const INVITE_LIFE = 24 * 3_600_000;
140
+ /** The longest give-up horizon (Delivery [P1D, P90D]): what late answers may still need (RLTP-MT-3090). */
141
+ const HORIZON_MAX = 90 * 86_400_000;
142
+ export const MEMBERSHIP_COMMANDS = Object.freeze({
143
+ found: 'membership.found',
144
+ invite: 'membership.invite',
145
+ vouch: 'membership.vouch',
146
+ });
147
+ const refuse = (reason, ref) => ({ ok: false, reason, ...(ref ? { ref } : {}) });
148
+ const no = (code, ref) => ({ ok: false, code, ...(ref ? { ref } : {}) });
149
+ const isRefusal = (x) => x?.ok === false;
150
+ const sealWith = (ctx, doc, keyAgreement) => seal(doc, keyAgreement, { ephSeed: ctx.entropy.bytes('seal.eph', 32), nonce: ctx.entropy.bytes('seal.nonce', 12) }, ctx.crypto.subtle);
151
+ const signer = (ctx, label) => (b) => ctx.keys.sign(label, b);
152
+ const signDoc = async (ctx, label, anchor, doc, at) => diSignWith(anchor, signer(ctx, label), doc, at, ctx.crypto.subtle);
153
+ const draft = (s) => ({ digest: s.digest, envelope: s.envelope, type: s.type, threadId: s.threadId, recipient: s.recipient });
154
+ async function sealed(ctx, doc, recipient) {
155
+ return { digest: await digestDoc(doc, ctx.crypto.subtle), envelope: await sealWith(ctx, doc, recipient.keyAgreement), threadId: doc.threadId, type: doc.type, recipient };
156
+ }
157
+ /** The own key-agreement id a label derives under, from `addresses`. */
158
+ const rkidOf = (state, label) => Object.entries(state.addresses.keys).find(([, k]) => k.label === label)?.[0];
159
+ /** A displayed member card (Membership 2.6): no challenge, no sentTo/boundTo/hints. */
160
+ async function memberCard(ctx, label, name, at) {
161
+ const me = await ctx.keys.anchorOf(label);
162
+ const body = cardBody(me, name !== undefined ? { name } : {});
163
+ return await signDoc(ctx, label, me.anchor, body, at);
164
+ }
165
+ const ownName = (state) => {
166
+ const n = state.profile.body?.name;
167
+ return typeof n === 'string' && n.length ? [...n].slice(0, 200).join('') : undefined;
168
+ };
169
+ const groupOfLabel = (state, label) => label === undefined ? undefined : Object.values(state.groups).find((g) => g.memberLabel === label);
170
+ /** RLTP-MT-3282: the inviter admits on its own invitation without a
171
+ * further human act only before invite.validUntil + membership-skew +
172
+ * the longest give-up horizon, by its own clock; later, the admission
173
+ * would wait for a human act, which this library does not offer — the
174
+ * consent stays as evidence until its retention lets it go. */
175
+ const admissionOpen = (invite, when) => when < untilOf(invite) + MEMBERSHIP_SKEW + HORIZON_MAX;
176
+ /** The policy (see the header): admitted when vouchThreshold distinct
177
+ * members stand behind it, the admitting member one of them. */
178
+ function policyMet(g, ev) {
179
+ const backers = new Set([g.memberAnchor ?? '']);
180
+ for (const v of ev.vouches)
181
+ if (Object.hasOwn(g.roster, v.issuer))
182
+ backers.add(v.issuer);
183
+ return BigInt(backers.size) >= BigInt(g.rules.vouchThreshold);
184
+ }
185
+ // ── the in-library Access over the holder's own groups ─────────────────
186
+ const groupAccess = (g) => Object.freeze({
187
+ group: g.id,
188
+ isMember: (a) => Object.hasOwn(g.roster, a),
189
+ resolve: (oid) => {
190
+ const e = Object.values(g.roster).find((x) => x.op === oid);
191
+ return e ? { subject: e.anchor, keyAgreement: e.keyAgreement } : null;
192
+ },
193
+ });
194
+ /** An Access over one snapshot's `groups` (note §3 Access; the header). */
195
+ export function localAccess(state) {
196
+ return Object.freeze({
197
+ memberships: async () => Object.values(state.groups).map(groupAccess),
198
+ membership: async (id) => (Object.hasOwn(state.groups, id) ? groupAccess(state.groups[id]) : null),
199
+ });
200
+ }
201
+ const foundDef = {
202
+ human: true,
203
+ async prepare(ctx, input) {
204
+ if (typeof input?.name !== 'string' || !input.name.length)
205
+ throw new TypeError('membership.found needs {name}');
206
+ const S = ctx.crypto.subtle;
207
+ const n = ctx.entropy.bytes('founding.nonce', 32);
208
+ const label = 'pair/' + await digestBytes(n, S);
209
+ const me = await ctx.keys.anchorOf(label);
210
+ // the group address: a probe mint until Identity names the group key's origin
211
+ const k = await S.importKey('pkcs8', Uint8Array.from([...ED_PKCS8, ...ctx.entropy.bytes('group.seed', 32)]), { name: 'Ed25519' }, true, ['sign']);
212
+ const did = anchorOfEd(fromB64u((await S.exportKey('jwk', k)).x));
213
+ const at = iso(ctx.when);
214
+ const body = { type: 'group-genesis@probe', group: did, label: input.name, founder: me.anchor, issuedAt: at };
215
+ const genesis = await signDoc(ctx, label, me.anchor, body, at);
216
+ return { label, nonce: b64uOf(n), rkid: me.keyAgreement, anchor: me.anchor, did, genesis, genesisDigest: await digestDoc(body, S), keyAgreement: me.keyAgreement, name: ownName(ctx.snapshot) };
217
+ },
218
+ apply(state, prepared, input, when) {
219
+ const p = prepared.value;
220
+ if (state.register.labels.includes(p.label))
221
+ throw new Error('membership.found: the founding label is not fresh');
222
+ if (Object.hasOwn(state.groups, p.genesisDigest))
223
+ throw new Error('membership.found: the genesis exists');
224
+ const threshold = input.vouchThreshold ?? '1';
225
+ if (!/^[1-9][0-9]?$/.test(threshold))
226
+ throw new TypeError('membership.found: vouchThreshold is a positive decimal');
227
+ if (threshold !== '1')
228
+ return no('threshold-unsupported');
229
+ const group = {
230
+ id: p.genesisDigest, name: input.name, did: p.did, genesis: p.genesis, memberLabel: p.label, rules: { vouchThreshold: threshold },
231
+ listed: input.listed ?? true, memberAnchor: p.anchor, memberOp: oidOf(p.genesisDigest),
232
+ roster: { [p.anchor]: { anchor: p.anchor, op: oidOf(p.genesisDigest), keyAgreement: p.keyAgreement, ...(p.name !== undefined ? { name: p.name } : {}), addedAt: when } },
233
+ invitations: {}, evidence: {}, preludes: {},
234
+ };
235
+ return {
236
+ state: {
237
+ ...state,
238
+ register: withPairNonce(state.register, p.label, p.nonce),
239
+ addresses: { ...state.addresses, keys: { ...state.addresses.keys, [p.rkid]: { label: p.label, lastShown: when } } },
240
+ groups: { ...state.groups, [group.id]: group },
241
+ },
242
+ events: [{ type: 'group.founded', groupId: group.id }],
243
+ };
244
+ },
245
+ };
246
+ // ── the prelude (Membership 1.2) ───────────────────────────────────────
247
+ /** The live active tuple toward a contact, with its relationship. */
248
+ function channelTo(state, contactId) {
249
+ const c = findContact(state, contactId);
250
+ if (!c)
251
+ return null;
252
+ const ids = new Set([c.id, ...c.aliases]);
253
+ for (const id of c.relIds) {
254
+ const r = state.relationships[id];
255
+ const t = r && liveRel(r) ? activeTuple(r) : undefined;
256
+ if (t && !t.deactivated && ids.has(t.entryId))
257
+ return { contact: c, rel: r, tuple: t };
258
+ }
259
+ return null;
260
+ }
261
+ const inviteDef = {
262
+ human: true,
263
+ async prepare(ctx, input) {
264
+ const g = ctx.snapshot.groups[input?.groupId];
265
+ if (!g || g.memberAnchor === undefined)
266
+ return no('unknown-group');
267
+ const ch = channelTo(ctx.snapshot, input.contactId);
268
+ if (!ch)
269
+ return no('no-own-channel');
270
+ // no invitation toward a blocked relationship (Message Tasks 6410)
271
+ if (blockedRel(ctx.snapshot, ch.rel.id))
272
+ return no('blocked', 'RLTP-MSG-6410');
273
+ const own = await ctx.keys.anchorOf(ch.tuple.ownPairLabel);
274
+ const at = iso(ctx.when);
275
+ const body = {
276
+ id: ctx.entropy.id('doc.id'), type: PRELUDE_REQUEST_TYPE, issuer: own.anchor, recipient: ch.tuple.counterpartAnchor,
277
+ threadId: ctx.entropy.id('thread.id'), issuedAt: at, payload: { genesisDigest: g.id, group: g.did ?? '', name: g.name, inviter: g.memberAnchor, binding: {} },
278
+ };
279
+ // the inviter binding (RLTP-MT-3905): signed under the member anchor
280
+ // over (inviter, genesis digest, thread, from, to)
281
+ body.payload.binding = await signDoc(ctx, g.memberLabel, g.memberAnchor, inviterBindingBody({ inviter: g.memberAnchor, genesisDigest: g.id, threadId: body.threadId, from: own.anchor, to: ch.tuple.counterpartAnchor }), at);
282
+ const doc = await signDoc(ctx, ch.tuple.ownPairLabel, own.anchor, body, at);
283
+ return sealed(ctx, doc, { contactId: ch.contact.id, rkid: ch.tuple.rkid, keyAgreement: ch.tuple.counterpartKeyAgreement });
284
+ },
285
+ apply(state, prepared, input, when) {
286
+ const p = prepared.value;
287
+ if (isRefusal(p))
288
+ return p;
289
+ const g = state.groups[input.groupId];
290
+ const preludes = { ...g.preludes, [p.threadId]: { contactId: p.recipient.contactId, rkid: p.recipient.rkid, at: when } };
291
+ return { state: { ...state, groups: { ...state.groups, [g.id]: { ...g, preludes } } }, outbound: [draft(p)] };
292
+ },
293
+ };
294
+ /** The arrival tuple's own label and the entry it feeds. */
295
+ function arrival(state, rkid) {
296
+ const at = tupleByRkid(state, rkid);
297
+ return at ? { label: at.tuple.ownPairLabel, contactId: entryOf(state, at.rel)?.id ?? findContact(state, at.rel.contactId)?.id ?? at.rel.contactId, tuple: at.tuple } : null;
298
+ }
299
+ /** An answered prelude's key: the group and the inviter that asked. */
300
+ const preludeKey = (group, inviter) => `${group} ${inviter}`;
301
+ const preludeRequestTask = {
302
+ proof: 'required',
303
+ // stage 7: the payload schema (RLTP-MT-3905)
304
+ decode(p) {
305
+ if (!membershipSchemaOk('payload-membership-prelude-request.schema.json', p))
306
+ return refuse('malformed', 'RLTP-MT-3905');
307
+ return memberLabelOf(p.genesisDigest) === null ? refuse('validation-failed') : p;
308
+ },
309
+ async prepare(ctx, doc, arr) {
310
+ // the document proof under its issuer, the binding's fields (genesis
311
+ // digests by their decoded bytes) and its proof under the inviter
312
+ // (RLTP-MT-3905, 3910); the issuer and recipient are the arrival
313
+ // tuple's pair anchors by the core's tuple binding (receive.ts)
314
+ const bad = await preludeRequestProblem(doc, ctx.crypto.subtle);
315
+ if (bad)
316
+ return refuse('validation-failed', bad === 'documentProof' ? 'RLTP-MT-3900' : 'RLTP-MT-3905');
317
+ const a = arrival(ctx.snapshot, arr.rkid);
318
+ const label = memberLabelOf(doc.payload.genesisDigest);
319
+ const member = await ctx.keys.anchorOf(label);
320
+ const at = iso(ctx.when);
321
+ const card = await memberCard(ctx, label, ownName(ctx.snapshot), at);
322
+ const own = await ctx.keys.anchorOf(a.label);
323
+ const body = {
324
+ id: ctx.entropy.id('doc.id'), type: PRELUDE_REPLY_TYPE, issuer: own.anchor, recipient: doc.issuer, threadId: doc.threadId, issuedAt: at,
325
+ payload: { genesisDigest: doc.payload.genesisDigest, card },
326
+ };
327
+ const reply = await sealed(ctx, await signDoc(ctx, a.label, own.anchor, body, at), { contactId: a.contactId, rkid: arr.rkid, keyAgreement: a.tuple.counterpartKeyAgreement });
328
+ return { reply, label, rkid: member.keyAgreement };
329
+ },
330
+ apply(state, prepared, doc, arr) {
331
+ const p = prepared.value;
332
+ const a = arrival(state, arr.rkid);
333
+ // the binding per relationship (RLTP-MT-3925–3935): a request under a
334
+ // held key from a tuple of the bound tuple's relationship replaces the
335
+ // tuple (3930); from another relationship it is refused while the bound
336
+ // one is current, whatever entry either feeds (3935), and replaces the
337
+ // binding once that one has ended. The relationships are read through
338
+ // the tuples now (3940): a merge, a split or a chaining moves them
339
+ const bound = state.joining.preludes[preludeKey(toU(doc.payload.genesisDigest), doc.payload.inviter)];
340
+ const was = bound ? tupleByRkid(state, bound.rkid) : null;
341
+ const here = tupleByRkid(state, arr.rkid);
342
+ if (was && liveRel(was.rel) && was.rel.id !== here.rel.id)
343
+ return refuse('validation-failed', 'RLTP-MT-3935');
344
+ const keys = Object.hasOwn(state.addresses.keys, p.rkid) ? state.addresses.keys : { ...state.addresses.keys, [p.rkid]: { label: p.label, lastShown: arr.when } };
345
+ return {
346
+ state: {
347
+ ...state,
348
+ // the member label in the register before its anchor is shown (Identity 6.1)
349
+ register: withLabel(state.register, p.label),
350
+ addresses: { ...state.addresses, keys },
351
+ joining: { ...state.joining, preludes: { ...state.joining.preludes, [preludeKey(toU(doc.payload.genesisDigest), doc.payload.inviter)]: { group: toU(doc.payload.genesisDigest), inviter: doc.payload.inviter, contactId: a.contactId, rkid: arr.rkid, memberLabel: p.label, at: arr.when } } },
352
+ },
353
+ outbound: [draft(p.reply)],
354
+ };
355
+ },
356
+ };
357
+ const preludeReplyTask = {
358
+ proof: 'required',
359
+ // stage 7: the payload schema (RLTP-MT-3960)
360
+ decode(p) {
361
+ return membershipSchemaOk('payload-membership-prelude-reply.schema.json', p) ? p : refuse('malformed', 'RLTP-MT-3960');
362
+ },
363
+ async prepare(ctx, doc, arr) {
364
+ const s = ctx.snapshot;
365
+ if (!(await diVerify(doc, doc.issuer, ctx.crypto.subtle)))
366
+ return refuse('validation-failed', 'RLTP-MT-3960');
367
+ // the answer to exactly the prelude this holder sent and still holds:
368
+ // its group (genesis digests by their decoded bytes), thread and tuple (3965)
369
+ const g = s.groups[toU(doc.payload.genesisDigest) ?? ''];
370
+ const asked = g?.preludes[doc.threadId];
371
+ if (!g || g.memberAnchor === undefined || !asked || asked.rkid !== arr.rkid)
372
+ return refuse('validation-failed', 'RLTP-MT-3965');
373
+ const card = doc.payload.card;
374
+ if (!(await enclosedCardOk(card, card.anchor, ctx.crypto.subtle)))
375
+ return refuse('validation-failed', 'RLTP-MT-3960');
376
+ const a = arrival(s, arr.rkid);
377
+ const S = ctx.crypto.subtle;
378
+ const at = iso(ctx.when);
379
+ const until = ctx.when + INVITE_LIFE;
380
+ const threadId = ctx.entropy.id('thread.id');
381
+ const inviterCard = await memberCard(ctx, g.memberLabel, ownName(s), at);
382
+ const cred = inviteCredentialBody({
383
+ issuer: g.memberAnchor, invitee: card.anchor, group: g.did ?? '', genesisDigest: g.id, card: inviterCard, name: g.name,
384
+ validFrom: at, validUntil: iso(until), taskContext: threadId,
385
+ });
386
+ const invite = await signDoc(ctx, g.memberLabel, g.memberAnchor, cred, at);
387
+ const inviteDoc = { id: ctx.entropy.id('doc.id'), type: INVITE_TYPE, issuer: g.memberAnchor, recipient: card.anchor, threadId, issuedAt: at, payload: { invite } };
388
+ if (!membershipSchemaOk('payload-membership-invite.schema.json', inviteDoc.payload) || !withinBudget(inviteDoc))
389
+ return refuse('validation-failed', 'RLTP-MT-2220');
390
+ const out = await sealed(ctx, inviteDoc, { contactId: a.contactId, rkid: rkidOf(s, g.memberLabel) ?? '', keyAgreement: card.keyAgreement, anchor: card.anchor, via: arr.rkid });
391
+ return { ...out, invitee: card.anchor, credDigest: await digestDoc(invite, S), invite: inviteDoc, validUntil: until };
392
+ },
393
+ apply(state, prepared, doc, arr) {
394
+ const p = prepared.value;
395
+ const g = state.groups[toU(doc.payload.genesisDigest)];
396
+ const preludes = { ...g.preludes };
397
+ delete preludes[doc.threadId];
398
+ // under a block of the relationship the reply arrives on: the waiting
399
+ // prelude closes, acknowledged, and no invitation is issued or sent —
400
+ // one transaction with the block check (RLTP-MT-3975, 3980)
401
+ const rel = relOfRkid(state, arr.rkid);
402
+ if (rel !== null && blockedRel(state, rel))
403
+ return { state: { ...state, groups: { ...state.groups, [g.id]: { ...g, preludes } } } };
404
+ // a member is not invited again; every invitation is its own thread,
405
+ // kept beside earlier ones (an accept signed in time may still travel)
406
+ if (Object.hasOwn(g.roster, p.invitee))
407
+ return { state: { ...state, groups: { ...state.groups, [g.id]: { ...g, preludes } } } };
408
+ if (!p.recipient.rkid)
409
+ throw new Error(`membership: no address key for ${g.memberLabel}`);
410
+ const inv = { invitee: p.invitee, keyAgreement: p.recipient.keyAgreement, contactId: p.recipient.contactId, rkid: arr.rkid, threadId: p.threadId, credDigest: p.credDigest, invite: p.invite, validUntil: p.validUntil };
411
+ return {
412
+ state: { ...state, groups: { ...state.groups, [g.id]: { ...g, preludes, invitations: { ...g.invitations, [p.threadId]: inv } } } },
413
+ outbound: [draft(p)],
414
+ };
415
+ },
416
+ };
417
+ // ── the invite at the invitee (Membership 3.1) ─────────────────────────
418
+ const invitePromptId = (credDigest) => 'invite:' + credDigest;
419
+ /** The invitation's attribution (RLTP-MT-3100): the relationship of the
420
+ * prelude binding held for its genesis digest and issuer, read through
421
+ * the binding's tuple now (3940), or none. */
422
+ function attribution(state, inv) {
423
+ const pre = state.joining.preludes[preludeKey(toU(inv.credentialSubject.genesisDigest) ?? '', inv.issuer)];
424
+ const relId = pre ? relOfRkid(state, pre.rkid) : null;
425
+ return pre && relId !== null ? { pre, relId } : null;
426
+ }
427
+ /** Whether the holder holds the credential digest in any record (3105):
428
+ * surfaced, declined, or a received invitation. */
429
+ const knownInvite = (state, credDigest) => state.joining.surfaced.includes(credDigest) || state.joining.declined.includes(credDigest) || Object.hasOwn(state.joining.invites, credDigest);
430
+ const sortedWith = (xs, x) => (xs.includes(x) ? [...xs] : [...xs, x].sort());
431
+ const inviteTask = {
432
+ proof: 'absent',
433
+ decode: (p) => (membershipSchemaOk('payload-membership-invite.schema.json', p) ? p : refuse('malformed')),
434
+ // the prelude check (3030): the invite reaches the key of the own
435
+ // derivation from its genesis digest. The ack is sealed to the inviter's
436
+ // card and filed under the contact of the binding's relationship, or
437
+ // under the member where no binding attributes it (3100)
438
+ channel(state, doc, rkid) {
439
+ const inv = doc.payload.invite;
440
+ const label = memberLabelOf(inv.credentialSubject.genesisDigest);
441
+ if (label === null || state.addresses.keys[rkid]?.label !== label)
442
+ return refuse('validation-failed', 'RLTP-MT-3030');
443
+ const at = attribution(state, inv);
444
+ return at
445
+ ? { keyAgreement: inv.credentialSubject.card.keyAgreement, contactId: at.pre.contactId, via: at.pre.rkid }
446
+ : { keyAgreement: inv.credentialSubject.card.keyAgreement, contactId: 'member:' + inv.issuer };
447
+ },
448
+ async prepare(ctx, doc) {
449
+ const bad = await inviteProblem(doc, ctx.crypto.subtle);
450
+ if (bad)
451
+ return refuse('validation-failed', bad);
452
+ const credDigest = await digestDoc(doc.payload.invite, ctx.crypto.subtle);
453
+ // the rate (3105, Message Tasks 4115): a digest new to the holder, on a
454
+ // relationship that accepted 20 new ones in the window, counted before
455
+ // the block, waits at its carrier: failed(rate-limited) at stage 8,
456
+ // nothing committed (Delivery RLTP-DEL-6270); a known digest never
457
+ // counts and is never refused by it
458
+ const s = ctx.snapshot;
459
+ const at = attribution(s, doc.payload.invite);
460
+ if (at && !knownInvite(s, credDigest) && rateCount(s, at.relId, 'invites', ctx.when) >= INVITE_RATE)
461
+ return refuse('rate-limited', 'RLTP-MT-3105');
462
+ return { credDigest };
463
+ },
464
+ // the defined effect (3095): durable buffering and the surfacing
465
+ // decision, in one transaction under the credential digest's key (3110;
466
+ // the engine serializes the whole state, receive.ts): exactly one of
467
+ // decline, surface, or no new effect
468
+ apply(state, prepared, doc, arr) {
469
+ const inv = doc.payload.invite;
470
+ const gd = toU(inv.credentialSubject.genesisDigest);
471
+ const { credDigest } = prepared.value;
472
+ const at = attribution(state, inv);
473
+ // the rate counts the new digest before the block is applied (3105, Message Tasks 4130)
474
+ let next = at && !knownInvite(state, credDigest) ? withRate(state, at.relId, 'invites', arr.when) : state;
475
+ // decline under a block (3115, Message Tasks 6.4): the register holds
476
+ // the digest, an open prompt of it closes as declined, silently
477
+ if (at && blockedRel(state, at.relId)) {
478
+ const promptId = invitePromptId(credDigest);
479
+ const prompts = { ...next.prompts };
480
+ delete prompts[promptId];
481
+ const held = next.joining.invites[credDigest];
482
+ const invites = held && held.status === 'open' ? { ...next.joining.invites, [credDigest]: { ...held, status: 'declined' } } : next.joining.invites;
483
+ return { state: { ...next, prompts, joining: { ...next.joining, invites, declined: sortedWith(next.joining.declined, credDigest) } } };
484
+ }
485
+ // no new effect: the register holds it (3120), or it surfaced before
486
+ // (3125). An invitation into a group the holder holds surfaces like any
487
+ // other: 3125 names no exception, and the person decides
488
+ if (next.joining.declined.includes(credDigest) || next.joining.surfaced.includes(credDigest) || Object.hasOwn(next.joining.invites, credDigest))
489
+ return { state: next };
490
+ // surface (3125): a prompt opened, the digest kept for the holder's life (3130)
491
+ const validUntil = Date.parse(inv.validUntil);
492
+ const r = {
493
+ credDigest, group: gd, name: typeof inv.credentialSubject.name === 'string' ? inv.credentialSubject.name : '',
494
+ ...(at ? { contactId: at.pre.contactId, rkid: at.pre.rkid } : {}),
495
+ memberLabel: memberLabelOf(gd), invite: doc, promptId: invitePromptId(credDigest), validUntil, status: 'open', at: arr.when,
496
+ };
497
+ const prompt = { id: r.promptId, kind: 'group-invite', ...(r.contactId !== undefined ? { contactId: r.contactId } : {}), groupId: gd, openedAt: arr.when, expiresAt: validUntil + MEMBERSHIP_SKEW };
498
+ return {
499
+ state: {
500
+ ...next,
501
+ joining: { ...next.joining, invites: { ...next.joining.invites, [credDigest]: r }, surfaced: sortedWith(next.joining.surfaced, credDigest) },
502
+ prompts: { ...next.prompts, [prompt.id]: prompt },
503
+ },
504
+ events: [{ type: 'prompt.opened', promptId: prompt.id, kind: 'group-invite' }],
505
+ };
506
+ },
507
+ };
508
+ // ── the accept (Membership 3.2): the group-invite decision ─────────────
509
+ const inviteOfPrompt = (state, promptId) => Object.values(state.joining.invites).find((i) => i.promptId === promptId);
510
+ const acceptDecision = {
511
+ async prepare(ctx, prompt, choice) {
512
+ if (choice !== 'accept')
513
+ return null;
514
+ const r = inviteOfPrompt(ctx.snapshot, prompt.id);
515
+ if (!r || r.status !== 'open')
516
+ return no('unknown-group');
517
+ const inv = r.invite.payload.invite;
518
+ const me = await ctx.keys.anchorOf(r.memberLabel);
519
+ const at = iso(ctx.when);
520
+ const card = await memberCard(ctx, r.memberLabel, ownName(ctx.snapshot), at);
521
+ const body = {
522
+ id: ctx.entropy.id('doc.id'), type: ACCEPT_TYPE, issuer: me.anchor, recipient: inv.issuer, threadId: r.invite.threadId, issuedAt: at,
523
+ payload: { accept: { group: inv.credentialSubject.group, subject: me.anchor, ref: r.credDigest, card, candidacy: false } },
524
+ };
525
+ const doc = await signDoc(ctx, r.memberLabel, me.anchor, body, at);
526
+ if (!withinBudget(doc))
527
+ return no('unknown-group', 'RLTP-MT-2220');
528
+ const out = await sealed(ctx, doc, { contactId: r.contactId ?? 'member:' + inv.issuer, rkid: rkidOf(ctx.snapshot, r.memberLabel) ?? '', keyAgreement: inv.credentialSubject.card.keyAgreement, anchor: inv.issuer, ...(r.rkid !== undefined ? { via: r.rkid } : {}) });
529
+ return { ...out, acceptDigest: await digestDoc(doc, ctx.crypto.subtle) };
530
+ },
531
+ apply(state, prompt, choice, when, prepared) {
532
+ const r = inviteOfPrompt(state, prompt.id);
533
+ if (!r)
534
+ return { state: state };
535
+ if (choice === 'decline')
536
+ return { state: { ...state, joining: { ...state.joining, invites: { ...state.joining.invites, [r.credDigest]: { ...r, status: 'declined' } } } } };
537
+ const p = prepared.value;
538
+ if (p === null || isRefusal(p))
539
+ return p ?? no('unknown-group');
540
+ if (when > r.validUntil + MEMBERSHIP_SKEW)
541
+ return no('prompt-expired', 'RLTP-MT-3270');
542
+ if (!p.recipient.rkid)
543
+ throw new Error(`membership: no address key for ${r.memberLabel}`);
544
+ return {
545
+ state: { ...state, joining: { ...state.joining, invites: { ...state.joining.invites, [r.credDigest]: { ...r, status: 'accepted', acceptDigest: p.acceptDigest } } } },
546
+ outbound: [draft(p)],
547
+ };
548
+ },
549
+ };
550
+ /** The welcome (this library's form, see the header) and, for n > 1, the
551
+ * evidence relays to the other members. */
552
+ async function admissionDocs(ctx, g, ev, acceptDigest, admit) {
553
+ const acc = ev.accept.payload.accept;
554
+ const at = iso(ctx.when);
555
+ const entry = rosterEntryOf(acc, acceptDigest, ctx.when, undefined);
556
+ if (admit) {
557
+ const roster = [...Object.values(g.roster), entry].map((m) => ({ anchor: m.anchor, op: m.op, keyAgreement: m.keyAgreement, ...(m.name !== undefined ? { name: m.name } : {}) }));
558
+ const body = {
559
+ id: ctx.entropy.id('doc.id'), type: WELCOME_TYPE, issuer: g.memberAnchor, recipient: acc.subject, threadId: ev.accept.threadId, issuedAt: at,
560
+ payload: { genesisDigest: g.id, group: g.did ?? '', name: g.name, genesis: g.genesis ?? {}, vouchThreshold: g.rules.vouchThreshold, accept: acceptDigest, roster },
561
+ };
562
+ const doc = await signDoc(ctx, g.memberLabel, g.memberAnchor, body, at);
563
+ return { welcome: await sealed(ctx, doc, { ...contactOfInvitation(g, acc.subject), rkid: rkidOf(ctx.snapshot, g.memberLabel) ?? '', keyAgreement: acc.card.keyAgreement, anchor: acc.subject }), relays: [] };
564
+ }
565
+ const relays = [];
566
+ for (const m of Object.values(g.roster)) {
567
+ if (m.anchor === g.memberAnchor || m.anchor === acc.subject)
568
+ continue;
569
+ const body = {
570
+ id: ctx.entropy.id('doc.id'), type: EVIDENCE_TYPE, issuer: g.memberAnchor, recipient: m.anchor, threadId: ev.accept.threadId, issuedAt: at,
571
+ payload: { evidence: { invite: ev.invite, accept: ev.accept } },
572
+ };
573
+ relays.push(await sealed(ctx, body, { ...contactOfMember(m), rkid: rkidOf(ctx.snapshot, g.memberLabel) ?? '', keyAgreement: m.keyAgreement, anchor: m.anchor }));
574
+ }
575
+ return { welcome: null, relays };
576
+ }
577
+ /** The invitation of a thread to a subject (Membership 3.2: the accept answers the invite of its thread). */
578
+ const invitationOf = (g, threadId, subject) => {
579
+ const i = Object.hasOwn(g.invitations, threadId) ? g.invitations[threadId] : undefined;
580
+ return i && i.invitee === subject ? i : undefined;
581
+ };
582
+ /** The contact an invitee was invited as, with the tuple its prelude went on (Recipient.via). */
583
+ const contactOfInvitation = (g, subject) => {
584
+ const i = Object.values(g.invitations).find((x) => x.invitee === subject);
585
+ return i ? { contactId: i.contactId, via: i.rkid } : { contactId: 'member:' + subject };
586
+ };
587
+ /** A member as a recipient's contact: the contact it is known as, with its tuple (Recipient.via). */
588
+ const contactOfMember = (m) => (m.contactId !== undefined ? { contactId: m.contactId, ...(m.rkid !== undefined ? { via: m.rkid } : {}) } : { contactId: 'member:' + m.anchor });
589
+ const rosterEntryOf = (acc, acceptDigest, when, by) => ({
590
+ anchor: acc.subject, op: oidOf(acceptDigest), keyAgreement: acc.card.keyAgreement,
591
+ ...(typeof acc.card.name === 'string' ? { name: acc.card.name } : {}), addedAt: when, ...(by ? { contactId: by.contactId, rkid: by.rkid } : {}),
592
+ });
593
+ /** The admission's state: the roster grows, the evidence and the invitation go. */
594
+ function admitted(state, g, acceptDigest, ev, when) {
595
+ const acc = ev.accept.payload.accept;
596
+ // the contact the member was invited as, on the tuple its prelude went on
597
+ const by = Object.values(g.invitations).find((i) => i.invitee === acc.subject);
598
+ // every invitation to the now member is answered
599
+ const invitations = Object.fromEntries(Object.entries(g.invitations).filter(([, i]) => i.invitee !== acc.subject));
600
+ const evidence = { ...g.evidence };
601
+ delete evidence[acceptDigest];
602
+ const next = { ...g, roster: { ...g.roster, [acc.subject]: rosterEntryOf(acc, acceptDigest, when, by) }, invitations, evidence };
603
+ return { state: { ...state, groups: { ...state.groups, [g.id]: next } }, events: [{ type: 'group.admitted', groupId: g.id, member: acc.subject }] };
604
+ }
605
+ const acceptTask = {
606
+ proof: 'required',
607
+ decode: (p) => (membershipSchemaOk('payload-membership-accept.schema.json', p) ? p : refuse('malformed')),
608
+ // to the member key that issued the invitation this accept answers
609
+ channel(state, doc, rkid) {
610
+ const g = groupOfLabel(state, state.addresses.keys[rkid]?.label);
611
+ const inv = g && invitationOf(g, doc.threadId, doc.issuer);
612
+ if (!g || !inv)
613
+ return refuse('validation-failed', 'RLTP-MT-3270');
614
+ return { keyAgreement: doc.payload.accept.card.keyAgreement, contactId: inv.contactId, via: inv.rkid };
615
+ },
616
+ async prepare(ctx, doc, arr) {
617
+ const g = groupOfLabel(ctx.snapshot, ctx.snapshot.addresses.keys[arr.rkid].label);
618
+ const inv = invitationOf(g, doc.threadId, doc.issuer);
619
+ const bad = await acceptProblem(doc, inv.invite, inv.credDigest, ctx.crypto.subtle);
620
+ if (bad)
621
+ return refuse('validation-failed', bad);
622
+ const acceptDigest = await digestDoc(doc, ctx.crypto.subtle);
623
+ const ev = { invite: inv.invite, accept: doc, vouches: [], at: arr.when };
624
+ const docs = await admissionDocs(ctx, g, ev, acceptDigest, policyMet(g, ev) && admissionOpen(inv.invite, arr.when));
625
+ return { group: g.id, acceptDigest, ...docs };
626
+ },
627
+ // the defined effect: the consent recorded (3280); the admission where
628
+ // the policy holds, else the pair relayed for vouches (the header)
629
+ apply(state, prepared, doc, arr) {
630
+ const p = prepared.value;
631
+ const g = state.groups[p.group];
632
+ const ev = { invite: invitationOf(g, doc.threadId, doc.issuer).invite, accept: doc, vouches: [], at: arr.when };
633
+ if (p.welcome) {
634
+ const a = admitted(state, g, p.acceptDigest, ev, arr.when);
635
+ return { state: a.state, events: a.events, outbound: [draft(p.welcome)] };
636
+ }
637
+ // the consent waits as evidence; the invitation stays until the
638
+ // admission (its contact goes onto the roster then)
639
+ return {
640
+ state: { ...state, groups: { ...state.groups, [g.id]: { ...g, evidence: { ...g.evidence, [p.acceptDigest]: ev } } } },
641
+ outbound: p.relays.map(draft),
642
+ };
643
+ },
644
+ };
645
+ const evidenceTask = (schema) => ({
646
+ proof: 'absent',
647
+ decode: (p) => (membershipSchemaOk(schema, p) ? p : refuse('malformed')),
648
+ // 3745: this holder is a member of the pair's group; the relayer is on its roster
649
+ channel(state, doc, rkid) {
650
+ const inv = doc.payload.evidence.invite.payload.invite;
651
+ const g = groupOfLabel(state, state.addresses.keys[rkid]?.label);
652
+ if (!g || !sameDigest(g.id, inv.credentialSubject.genesisDigest))
653
+ return refuse('validation-failed', 'RLTP-MT-3755');
654
+ const m = g.roster[doc.issuer];
655
+ if (!m)
656
+ return refuse('validation-failed', 'RLTP-MT-3750');
657
+ return { keyAgreement: m.keyAgreement, ...contactOfMember(m) };
658
+ },
659
+ async prepare(ctx, doc, arr) {
660
+ const { invite, accept, vouches = [] } = doc.payload.evidence;
661
+ const S = ctx.crypto.subtle;
662
+ const credDigest = await digestDoc(invite.payload.invite, S);
663
+ const bad = await evidenceProblem(invite, accept, credDigest, S);
664
+ if (bad)
665
+ return refuse('validation-failed', bad);
666
+ const acceptDigest = await digestDoc(accept, S);
667
+ for (const v of vouches) {
668
+ const vb = await vouchProblem(v, accept.payload.accept.subject, acceptDigest, invite.payload.invite.credentialSubject.genesisDigest, S);
669
+ if (vb)
670
+ return refuse('validation-failed', vb);
671
+ }
672
+ const g = groupOfLabel(ctx.snapshot, ctx.snapshot.addresses.keys[arr.rkid].label);
673
+ const held = g.evidence[acceptDigest];
674
+ const merged = mergeVouches(held?.vouches ?? [], vouches);
675
+ const ev = { invite, accept, vouches: merged, at: arr.when };
676
+ // the inviter admits once the policy holds (the header)
677
+ const admit = invite.payload.invite.issuer === g.memberAnchor && !Object.hasOwn(g.roster, accept.payload.accept.subject) && policyMet(g, ev) && admissionOpen(invite, arr.when);
678
+ const welcome = admit ? (await admissionDocs(ctx, g, ev, acceptDigest, true)).welcome : null;
679
+ return { group: g.id, acceptDigest, vouches, welcome };
680
+ },
681
+ apply(state, prepared, doc, arr) {
682
+ const p = prepared.value;
683
+ const g = state.groups[p.group];
684
+ const { invite, accept } = doc.payload.evidence;
685
+ if (Object.hasOwn(g.roster, accept.payload.accept.subject))
686
+ return { state: state };
687
+ const held = g.evidence[p.acceptDigest];
688
+ const ev = { invite, accept, vouches: mergeVouches(held?.vouches ?? [], p.vouches), at: held?.at ?? arr.when };
689
+ if (p.welcome) {
690
+ const a = admitted(state, g, p.acceptDigest, ev, arr.when);
691
+ return { state: a.state, events: a.events, outbound: [draft(p.welcome)] };
692
+ }
693
+ // keyed by the accept: one decision per consent, vouches added up to 16 (3795)
694
+ return { state: { ...state, groups: { ...state.groups, [g.id]: { ...g, evidence: { ...g.evidence, [p.acceptDigest]: ev } } } } };
695
+ },
696
+ });
697
+ /** Vouches by distinct issuers, the held ones first, at most 16 (3805, 3795). */
698
+ function mergeVouches(held, more) {
699
+ const out = [...held];
700
+ for (const v of more)
701
+ if (out.length < 16 && !out.some((x) => x.issuer === v.issuer))
702
+ out.push(v);
703
+ return out;
704
+ }
705
+ const vouchDef = {
706
+ human: true,
707
+ async prepare(ctx, input) {
708
+ const g = ctx.snapshot.groups[input?.groupId];
709
+ if (!g || g.memberAnchor === undefined)
710
+ return no('unknown-group');
711
+ const held = Object.entries(g.evidence).find(([, e]) => e.accept.payload.accept.subject === input.member);
712
+ if (!held)
713
+ return no('no-evidence');
714
+ const [acceptDigest, ev] = held;
715
+ // the admitting member stands behind its admission already (the header)
716
+ if (ev.invite.payload.invite.issuer === g.memberAnchor)
717
+ return no('own-admission');
718
+ if (ev.vouches.some((v) => v.issuer === g.memberAnchor))
719
+ return no('already-given');
720
+ const at = iso(ctx.when);
721
+ const body = vouchCredentialBody({ issuer: g.memberAnchor, subject: input.member, genesisDigest: g.id, accept: acceptDigest, provenance: input.provenance ?? 'met', validFrom: at });
722
+ const vouch = await signDoc(ctx, g.memberLabel, g.memberAnchor, body, at);
723
+ const inv = ev.invite.payload.invite;
724
+ const to = g.roster[inv.issuer];
725
+ if (!to)
726
+ return no('no-evidence');
727
+ const doc = {
728
+ id: ctx.entropy.id('doc.id'), type: EVIDENCE_TYPE, issuer: g.memberAnchor, recipient: inv.issuer, threadId: ev.accept.threadId, issuedAt: at,
729
+ payload: { evidence: { invite: ev.invite, accept: ev.accept, vouches: [vouch] } },
730
+ };
731
+ const relay = await sealed(ctx, doc, { ...contactOfMember(to), rkid: rkidOf(ctx.snapshot, g.memberLabel) ?? '', keyAgreement: to.keyAgreement, anchor: to.anchor });
732
+ return { group: g.id, acceptDigest, vouch, relay, welcome: null };
733
+ },
734
+ apply(state, prepared, input, when) {
735
+ const p = prepared.value;
736
+ if (isRefusal(p))
737
+ return p;
738
+ const g = state.groups[p.group];
739
+ const ev = g.evidence[p.acceptDigest];
740
+ const next = { ...ev, vouches: mergeVouches(ev.vouches, [p.vouch]) };
741
+ const events = [{ type: 'group.vouched', groupId: g.id, member: input.member }];
742
+ if (p.welcome) {
743
+ const a = admitted(state, g, p.acceptDigest, next, when);
744
+ return { state: a.state, events: [...events, ...a.events], outbound: [draft(p.welcome)] };
745
+ }
746
+ return {
747
+ state: { ...state, groups: { ...state.groups, [g.id]: { ...g, evidence: { ...g.evidence, [p.acceptDigest]: next } } } },
748
+ events,
749
+ outbound: p.relay ? [draft(p.relay)] : [],
750
+ };
751
+ },
752
+ };
753
+ const isStr = (x) => typeof x === 'string';
754
+ const welcomeShape = (p) => {
755
+ const x = p;
756
+ if (x === null || typeof x !== 'object')
757
+ return false;
758
+ if (![x.genesisDigest, x.group, x.name, x.vouchThreshold, x.accept].every(isStr) || x.genesis === null || typeof x.genesis !== 'object' || !Array.isArray(x.roster))
759
+ return false;
760
+ if (!/^[1-9][0-9]?$/.test(x.vouchThreshold) || toU(x.genesisDigest) !== x.genesisDigest)
761
+ return false;
762
+ return x.roster.every((m) => {
763
+ const e = m;
764
+ return e !== null && typeof e === 'object' && isStr(e.anchor) && isStr(e.op) && isStr(e.keyAgreement) && (e.name === undefined || isStr(e.name));
765
+ });
766
+ };
767
+ /** A roster carries each member once: its anchors and its ops distinct
768
+ * (one admission per member, one member per admission). No rule of
769
+ * Membership 0.19 names it for this library's welcome form, so a repeat
770
+ * is validation-failed without a ref. */
771
+ const rosterDistinct = (roster) => new Set(roster.map((m) => m.anchor)).size === roster.length && new Set(roster.map((m) => m.op)).size === roster.length;
772
+ /** The welcome's roster as the holder materialises it, by member anchor:
773
+ * the one map the re-match reads and the group commits. */
774
+ const welcomeRoster = (roster) => new Map(roster.map((m) => [m.anchor, m]));
775
+ /** The invite this holder accepted whose welcome `doc` would be. */
776
+ const acceptedInvite = (state, doc) => Object.values(state.joining.invites).find((i) => i.status !== 'open' && i.status !== 'declined' &&
777
+ i.invite.payload.invite.issuer === doc.issuer && i.invite.threadId === doc.threadId && i.group === doc.payload.genesisDigest);
778
+ const welcomeTask = {
779
+ proof: 'required',
780
+ decode: (p) => (!welcomeShape(p) ? refuse('malformed') : !rosterDistinct(p.roster) ? refuse('validation-failed') : p),
781
+ // the welcome answers an invitation this holder accepted: in its thread,
782
+ // from its inviter, to the member key of its group
783
+ channel(state, doc, rkid) {
784
+ const i = acceptedInvite(state, doc);
785
+ if (!i || state.addresses.keys[rkid]?.label !== i.memberLabel)
786
+ return refuse('validation-failed');
787
+ return { keyAgreement: i.invite.payload.invite.credentialSubject.card.keyAgreement, contactId: i.contactId ?? 'member:' + doc.issuer, ...(i.rkid !== undefined ? { via: i.rkid } : {}) };
788
+ },
789
+ // the group stars already accepted are matched against the joined group (5520)
790
+ async prepare(ctx, doc) {
791
+ if (!(await diVerify(doc, doc.issuer, ctx.crypto.subtle)))
792
+ return refuse('validation-failed', 'RLTP-MT-2130');
793
+ // the joined group as the welcome carries it: its roster, the oids naming the admissions
794
+ const roster = welcomeRoster(doc.payload.roster);
795
+ const membership = {
796
+ group: doc.payload.genesisDigest,
797
+ isMember: (a) => roster.has(a),
798
+ resolve: (oid) => { const e = [...roster.values()].find((m) => m.op === oid); return e ? { subject: e.anchor, keyAgreement: e.keyAgreement } : null; },
799
+ };
800
+ return { hits: await groupStarRematch(ctx, membership) };
801
+ },
802
+ apply(state, p, doc, arr) {
803
+ const i = acceptedInvite(state, doc);
804
+ const w = doc.payload;
805
+ if (Object.hasOwn(state.groups, w.genesisDigest))
806
+ return { state: state };
807
+ const own = w.roster.find((m) => m.anchor === doc.recipient);
808
+ if (!own || !w.roster.some((m) => m.anchor === doc.issuer) || i.acceptDigest === undefined || own.op !== oidOf(i.acceptDigest) || !sameDigest(w.accept, i.acceptDigest)) {
809
+ return refuse('validation-failed');
810
+ }
811
+ const roster = {};
812
+ for (const m of welcomeRoster(w.roster).values()) {
813
+ roster[m.anchor] = { anchor: m.anchor, op: m.op, keyAgreement: m.keyAgreement, ...(m.name !== undefined ? { name: m.name } : {}), addedAt: arr.when, ...(m.anchor === doc.issuer && i.contactId !== undefined && i.rkid !== undefined ? { contactId: i.contactId, rkid: i.rkid } : {}) };
814
+ }
815
+ const g = {
816
+ id: w.genesisDigest, name: w.name, did: w.group, genesis: w.genesis, memberLabel: i.memberLabel, rules: { vouchThreshold: w.vouchThreshold },
817
+ listed: true, memberAnchor: doc.recipient, memberOp: own.op, roster, invitations: {}, evidence: {}, preludes: {}, acceptDigest: i.acceptDigest,
818
+ };
819
+ return {
820
+ state: withGroupHits({
821
+ ...state, groups: { ...state.groups, [g.id]: g },
822
+ joining: { ...state.joining, invites: { ...state.joining.invites, [i.credDigest]: { ...i, status: 'joined' } } },
823
+ }, p.value.hits, g.id),
824
+ events: [{ type: 'group.joined', groupId: g.id }],
825
+ };
826
+ },
827
+ };
828
+ // ── keys and retention ─────────────────────────────────────────────────
829
+ /** The member keys still needed: held groups' and those on the way in
830
+ * (an answered prelude, an invitation open or accepted). */
831
+ function memberKeys(state, when) {
832
+ const labels = new Set(Object.values(state.groups).map((g) => g.memberLabel));
833
+ for (const p of Object.values(state.joining.preludes))
834
+ labels.add(p.memberLabel);
835
+ for (const i of Object.values(state.joining.invites))
836
+ if (i.status === 'open' || i.status === 'accepted')
837
+ labels.add(i.memberLabel);
838
+ const out = Object.entries(state.addresses.keys).filter(([, k]) => labels.has(k.label)).map(([rkid]) => rkid);
839
+ // the tuple a held prelude travelled on: a chaining may deactivate it
840
+ // while the prelude is open, and its answer, its acks and the invitation
841
+ // bound to it still arrive under that key
842
+ for (const g of Object.values(state.groups))
843
+ for (const p of Object.values(g.preludes))
844
+ out.push(p.rkid);
845
+ // an answered prelude's tuple, while its answer and the acks may still
846
+ // travel; the binding itself lives longer (3945) and needs no key
847
+ for (const p of Object.values(state.joining.preludes))
848
+ if (p.at + INVITE_LIFE + MEMBERSHIP_SKEW + HORIZON_MAX >= when)
849
+ out.push(p.rkid);
850
+ return out;
851
+ }
852
+ const untilOf = (invite) => Date.parse(invite.payload.invite.validUntil);
853
+ /** Bounded membership state (the header): what tick lets go. */
854
+ function retainMembership(state, when) {
855
+ let changed = false;
856
+ const groups = {};
857
+ for (const [id, g] of Object.entries(state.groups)) {
858
+ const invitations = Object.fromEntries(Object.entries(g.invitations).filter(([, i]) => i.validUntil + MEMBERSHIP_SKEW + HORIZON_MAX >= when));
859
+ const evidence = Object.fromEntries(Object.entries(g.evidence).filter(([, e]) => untilOf(e.invite) + MEMBERSHIP_SKEW + HORIZON_MAX >= when && !Object.hasOwn(g.roster, e.accept.payload.accept.subject)));
860
+ const preludes = Object.fromEntries(Object.entries(g.preludes).filter(([, p]) => p.at + INVITE_LIFE + MEMBERSHIP_SKEW + HORIZON_MAX >= when));
861
+ const same = Object.keys(invitations).length === Object.keys(g.invitations).length && Object.keys(evidence).length === Object.keys(g.evidence).length &&
862
+ Object.keys(preludes).length === Object.keys(g.preludes).length;
863
+ groups[id] = same ? g : { ...g, invitations, evidence, preludes };
864
+ if (!same)
865
+ changed = true;
866
+ }
867
+ // the accept → admission → welcome chain: two deliveries, each within a give-up horizon
868
+ const late = (i) => i.validUntil + MEMBERSHIP_SKEW + 2 * HORIZON_MAX < when;
869
+ const invites = Object.fromEntries(Object.entries(state.joining.invites).filter(([, i]) => !(i.status === 'declined' || (i.status === 'open' && i.validUntil + MEMBERSHIP_SKEW < when) || ((i.status === 'accepted' || i.status === 'joined') && late(i)))));
870
+ // a binding lives while the relationship of its tuple is current or an
871
+ // invitation attributed to it is open or accepted (RLTP-MT-3945); tick
872
+ // deletes it in its own serialized commit, deciding on the state it reads
873
+ // there (3947)
874
+ // an invitation rests on the binding of its key (genesis digest, inviter):
875
+ // a renewal may have moved the binding to a newer tuple since (3930)
876
+ const rests = (p) => Object.values(invites).some((i) => i.group === p.group && i.invite.payload.invite.issuer === p.inviter && (i.status === 'open' || i.status === 'accepted'));
877
+ const current = (p) => { const t = tupleByRkid(state, p.rkid); return t !== null && liveRel(t.rel); };
878
+ const preludes = Object.fromEntries(Object.entries(state.joining.preludes).filter(([, p]) => current(p) || rests(p)));
879
+ if (Object.keys(invites).length !== Object.keys(state.joining.invites).length || Object.keys(preludes).length !== Object.keys(state.joining.preludes).length)
880
+ changed = true;
881
+ return changed ? { ...state, groups, joining: { ...state.joining, preludes, invites } } : state;
882
+ }
883
+ /** When retainMembership lets something go (the timer contract, engine.ts). */
884
+ function membershipDeadlines(state) {
885
+ const out = [];
886
+ for (const g of Object.values(state.groups)) {
887
+ for (const i of Object.values(g.invitations))
888
+ out.push(i.validUntil + MEMBERSHIP_SKEW + HORIZON_MAX + 1);
889
+ for (const e of Object.values(g.evidence))
890
+ out.push(untilOf(e.invite) + MEMBERSHIP_SKEW + HORIZON_MAX + 1);
891
+ for (const p of Object.values(g.preludes))
892
+ out.push(p.at + INVITE_LIFE + MEMBERSHIP_SKEW + HORIZON_MAX + 1);
893
+ }
894
+ for (const i of Object.values(state.joining.invites)) {
895
+ if (i.status === 'open')
896
+ out.push(i.validUntil + MEMBERSHIP_SKEW + 1);
897
+ if (i.status === 'accepted' || i.status === 'joined')
898
+ out.push(i.validUntil + MEMBERSHIP_SKEW + 2 * HORIZON_MAX + 1);
899
+ }
900
+ // an answered prelude's tuple key leaves keepKeys then (memberKeys)
901
+ for (const p of Object.values(state.joining.preludes))
902
+ out.push(p.at + INVITE_LIFE + MEMBERSHIP_SKEW + HORIZON_MAX + 1);
903
+ return out;
904
+ }
905
+ /** The membership module createHolder registers. */
906
+ export function membershipModule() {
907
+ return Object.freeze({
908
+ name: 'membership',
909
+ commands: Object.freeze({
910
+ [MEMBERSHIP_COMMANDS.found]: foundDef,
911
+ [MEMBERSHIP_COMMANDS.invite]: inviteDef,
912
+ [MEMBERSHIP_COMMANDS.vouch]: vouchDef,
913
+ }),
914
+ tasks: Object.freeze({
915
+ [PRELUDE_REQUEST_TYPE]: preludeRequestTask,
916
+ [PRELUDE_REPLY_TYPE]: preludeReplyTask,
917
+ [INVITE_TYPE]: inviteTask,
918
+ [ACCEPT_TYPE]: acceptTask,
919
+ [EVIDENCE_TYPE]: evidenceTask('payload-membership-evidence.schema.json'),
920
+ [EVIDENCE_TYPE_01]: evidenceTask('payload-membership-evidence-0.1.schema.json'),
921
+ [WELCOME_TYPE]: welcomeTask,
922
+ }),
923
+ decisions: Object.freeze({ 'group-invite': acceptDecision }),
924
+ keepKeys: memberKeys,
925
+ retain: retainMembership,
926
+ deadlines: membershipDeadlines,
927
+ });
928
+ }
929
+ //# sourceMappingURL=holder.js.map