keri-ts 2.2.4 → 2.2.6

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 (256) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +14 -14
  2. package/esm/keri/npm/src/acdc/dhking.js +29 -2
  3. package/esm/keri/npm/src/acdc/observing.js +10 -3
  4. package/esm/keri/npm/src/acdc/query-resend.js +74 -0
  5. package/esm/keri/npm/src/acdc/regengine.js +39 -9
  6. package/esm/keri/npm/src/acdc/regeventing.js +5 -3
  7. package/esm/keri/npm/src/acdc/registering.js +19 -0
  8. package/esm/keri/npm/src/acdc/salting.js +4 -0
  9. package/esm/keri/npm/src/acdc/unblinding.js +38 -3
  10. package/esm/keri/npm/src/app/bootstrap/authenticating.js +32 -10
  11. package/esm/keri/npm/src/app/bootstrap/oobiery.js +363 -30
  12. package/esm/keri/npm/src/app/credentials/credential-importing.js +33 -4
  13. package/esm/keri/npm/src/app/credentials/root-replicator.js +27 -15
  14. package/esm/keri/npm/src/app/credentials/schema-resolving.js +5 -1
  15. package/esm/keri/npm/src/app/credentials/tel-publishing.js +16 -7
  16. package/esm/keri/npm/src/app/delivery/forwarding.js +205 -44
  17. package/esm/keri/npm/src/app/delivery/relaying.js +232 -0
  18. package/esm/keri/npm/src/app/delivery/respondant.js +8 -5
  19. package/esm/keri/npm/src/app/exchanges/exchange-messaging.js +1 -1
  20. package/esm/keri/npm/src/app/exchanges/exchanging.js +65 -9
  21. package/esm/keri/npm/src/app/identity/delegating.js +66 -9
  22. package/esm/keri/npm/src/app/identity/habbing.js +190 -71
  23. package/esm/keri/npm/src/app/identity/keeping.js +111 -39
  24. package/esm/keri/npm/src/app/identity/retire-identifier.js +98 -0
  25. package/esm/keri/npm/src/app/ipex/disclosure-replaying.js +116 -0
  26. package/esm/keri/npm/src/app/ipex/group-ipex.js +122 -25
  27. package/esm/keri/npm/src/app/ipex/group-issuance.js +137 -37
  28. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +94 -31
  29. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +53 -3
  30. package/esm/keri/npm/src/app/ipex/ipex-v2.js +7 -2
  31. package/esm/keri/npm/src/app/ipex/issuance.js +147 -44
  32. package/esm/keri/npm/src/app/ipex/pending-presentations.js +4 -2
  33. package/esm/keri/npm/src/app/ipex/presenting.js +3 -18
  34. package/esm/keri/npm/src/app/ipex/receive-issuance.js +2 -1
  35. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +39 -11
  36. package/esm/keri/npm/src/app/mailbox/agent-inbox.js +292 -0
  37. package/esm/keri/npm/src/app/mailbox/forward-handler.js +298 -52
  38. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +26 -31
  39. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +70 -62
  40. package/esm/keri/npm/src/app/mailbox/mailboxing.js +20 -4
  41. package/esm/keri/npm/src/app/multisig/counseling.js +71 -27
  42. package/esm/keri/npm/src/app/multisig/group-members.js +41 -5
  43. package/esm/keri/npm/src/app/multisig/grouping.js +50 -2
  44. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +3 -18
  45. package/esm/keri/npm/src/app/multisig/proposing.js +1 -16
  46. package/esm/keri/npm/src/app/multisig/rejoin-group.js +99 -0
  47. package/esm/keri/npm/src/app/multisig/replace-member.js +29 -8
  48. package/esm/keri/npm/src/app/protocol/parsering.js +45 -17
  49. package/esm/keri/npm/src/app/protocol/protocol-processing.js +47 -21
  50. package/esm/keri/npm/src/app/queries/query-replies.js +0 -22
  51. package/esm/keri/npm/src/app/queries/relay-refresh.js +81 -0
  52. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +180 -34
  53. package/esm/keri/npm/src/app/runtime/agent-runtime.js +42 -73
  54. package/esm/keri/npm/src/app/runtime/cue-runtime.js +45 -5
  55. package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +8 -1
  56. package/esm/keri/npm/src/app/runtime/operation-owners.js +26 -56
  57. package/esm/keri/npm/src/app/runtime/workflow-contract.js +7 -4
  58. package/esm/keri/npm/src/app/transport/http-client.js +20 -2
  59. package/esm/keri/npm/src/app/transport/message-source.js +2 -1
  60. package/esm/keri/npm/src/app/transport/messengering.js +39 -8
  61. package/esm/keri/npm/src/app/witness/ingress.js +7 -0
  62. package/esm/keri/npm/src/app/witness/receipt-client.js +43 -16
  63. package/esm/keri/npm/src/app/witness/receipting.js +62 -20
  64. package/esm/keri/npm/src/cli/messaging/multisig.js +4 -3
  65. package/esm/keri/npm/src/core/errors.js +73 -0
  66. package/esm/keri/npm/src/core/kel/delegation-ancestry.js +11 -2
  67. package/esm/keri/npm/src/core/kel/eventing.js +25 -28
  68. package/esm/keri/npm/src/core/kel/kever.js +15 -9
  69. package/esm/keri/npm/src/core/mailbox/kram-policy.js +16 -0
  70. package/esm/keri/npm/src/core/mailbox/kram-pruning.js +25 -6
  71. package/esm/keri/npm/src/core/mailbox/kram-time.js +34 -6
  72. package/esm/keri/npm/src/core/mailbox/kraming.js +58 -2
  73. package/esm/keri/npm/src/core/missing-anchor-query.js +20 -0
  74. package/esm/keri/npm/src/core/routing.js +18 -25
  75. package/esm/keri/npm/src/core/scheming.js +12 -5
  76. package/esm/keri/npm/src/core/wire-fields.js +126 -0
  77. package/esm/keri/npm/src/db/basing.js +4 -13
  78. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +3 -2
  79. package/esm/keri/npm/src/db/core/webdber.js +85 -27
  80. package/esm/keri/npm/src/db/mailbox/mailboxing.js +13 -0
  81. package/esm/keri/npm/src/db/subing.js +4 -0
  82. package/esm/keri/npm/src/runtime/index.js +11 -5
  83. package/esm/keri/npm/src/time/mod.js +5 -0
  84. package/esm/keri/npm/src/vdr/credential-tsn-record.js +2 -7
  85. package/esm/keri/npm/src/vdr/eventing.js +71 -28
  86. package/esm/keri/npm/src/vdr/registry-tsn-record.js +3 -13
  87. package/esm/keri/npm/src/version.js +2 -2
  88. package/package.json +1 -1
  89. package/types/_dnt.polyfills.d.ts +9 -9
  90. package/types/_dnt.polyfills.d.ts.map +1 -1
  91. package/types/acdc/dhking.d.ts +8 -0
  92. package/types/acdc/dhking.d.ts.map +1 -1
  93. package/types/acdc/observing.d.ts +5 -1
  94. package/types/acdc/observing.d.ts.map +1 -1
  95. package/types/acdc/query-resend.d.ts +24 -0
  96. package/types/acdc/query-resend.d.ts.map +1 -0
  97. package/types/acdc/regengine.d.ts +19 -1
  98. package/types/acdc/regengine.d.ts.map +1 -1
  99. package/types/acdc/regeventing.d.ts.map +1 -1
  100. package/types/acdc/registering.d.ts +6 -0
  101. package/types/acdc/registering.d.ts.map +1 -1
  102. package/types/acdc/salting.d.ts.map +1 -1
  103. package/types/acdc/unblinding.d.ts +13 -1
  104. package/types/acdc/unblinding.d.ts.map +1 -1
  105. package/types/app/bootstrap/authenticating.d.ts +12 -2
  106. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  107. package/types/app/bootstrap/oobiery.d.ts +144 -7
  108. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  109. package/types/app/credentials/credential-importing.d.ts +8 -0
  110. package/types/app/credentials/credential-importing.d.ts.map +1 -1
  111. package/types/app/credentials/root-replicator.d.ts +19 -8
  112. package/types/app/credentials/root-replicator.d.ts.map +1 -1
  113. package/types/app/credentials/schema-resolving.d.ts.map +1 -1
  114. package/types/app/credentials/tel-publishing.d.ts +4 -2
  115. package/types/app/credentials/tel-publishing.d.ts.map +1 -1
  116. package/types/app/delivery/forwarding.d.ts +90 -5
  117. package/types/app/delivery/forwarding.d.ts.map +1 -1
  118. package/types/app/delivery/relaying.d.ts +144 -0
  119. package/types/app/delivery/relaying.d.ts.map +1 -0
  120. package/types/app/delivery/respondant.d.ts +9 -2
  121. package/types/app/delivery/respondant.d.ts.map +1 -1
  122. package/types/app/exchanges/exchanging.d.ts +36 -0
  123. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  124. package/types/app/identity/delegating.d.ts +6 -0
  125. package/types/app/identity/delegating.d.ts.map +1 -1
  126. package/types/app/identity/habbing.d.ts +106 -19
  127. package/types/app/identity/habbing.d.ts.map +1 -1
  128. package/types/app/identity/keeping.d.ts +54 -19
  129. package/types/app/identity/keeping.d.ts.map +1 -1
  130. package/types/app/identity/retire-identifier.d.ts +24 -0
  131. package/types/app/identity/retire-identifier.d.ts.map +1 -0
  132. package/types/app/ipex/disclosure-replaying.d.ts +61 -0
  133. package/types/app/ipex/disclosure-replaying.d.ts.map +1 -0
  134. package/types/app/ipex/group-ipex.d.ts +60 -14
  135. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  136. package/types/app/ipex/group-issuance.d.ts +54 -8
  137. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  138. package/types/app/ipex/ipex-discloser.d.ts +39 -2
  139. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  140. package/types/app/ipex/ipex-grouping.d.ts +23 -2
  141. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  142. package/types/app/ipex/ipex-v2.d.ts +5 -2
  143. package/types/app/ipex/ipex-v2.d.ts.map +1 -1
  144. package/types/app/ipex/issuance.d.ts +46 -16
  145. package/types/app/ipex/issuance.d.ts.map +1 -1
  146. package/types/app/ipex/pending-presentations.d.ts +4 -2
  147. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  148. package/types/app/ipex/presenting.d.ts.map +1 -1
  149. package/types/app/ipex/receive-issuance.d.ts +2 -1
  150. package/types/app/ipex/receive-issuance.d.ts.map +1 -1
  151. package/types/app/ipex/unblinding-workflow.d.ts +13 -2
  152. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  153. package/types/app/mailbox/agent-inbox.d.ts +126 -0
  154. package/types/app/mailbox/agent-inbox.d.ts.map +1 -0
  155. package/types/app/mailbox/forward-handler.d.ts +114 -14
  156. package/types/app/mailbox/forward-handler.d.ts.map +1 -1
  157. package/types/app/mailbox/kel-publisher.d.ts +12 -22
  158. package/types/app/mailbox/kel-publisher.d.ts.map +1 -1
  159. package/types/app/mailbox/mailbox-poller.d.ts +40 -24
  160. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  161. package/types/app/mailbox/mailboxing.d.ts +19 -5
  162. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  163. package/types/app/multisig/counseling.d.ts +32 -11
  164. package/types/app/multisig/counseling.d.ts.map +1 -1
  165. package/types/app/multisig/group-members.d.ts +17 -1
  166. package/types/app/multisig/group-members.d.ts.map +1 -1
  167. package/types/app/multisig/grouping.d.ts +17 -1
  168. package/types/app/multisig/grouping.d.ts.map +1 -1
  169. package/types/app/multisig/multisig-workflows.d.ts +0 -8
  170. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  171. package/types/app/multisig/proposing.d.ts +4 -19
  172. package/types/app/multisig/proposing.d.ts.map +1 -1
  173. package/types/app/multisig/rejoin-group.d.ts +119 -0
  174. package/types/app/multisig/rejoin-group.d.ts.map +1 -0
  175. package/types/app/multisig/replace-member.d.ts +15 -1
  176. package/types/app/multisig/replace-member.d.ts.map +1 -1
  177. package/types/app/protocol/parsering.d.ts +21 -2
  178. package/types/app/protocol/parsering.d.ts.map +1 -1
  179. package/types/app/protocol/protocol-processing.d.ts +25 -5
  180. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  181. package/types/app/queries/query-replies.d.ts +4 -8
  182. package/types/app/queries/query-replies.d.ts.map +1 -1
  183. package/types/app/queries/relay-refresh.d.ts +26 -0
  184. package/types/app/queries/relay-refresh.d.ts.map +1 -0
  185. package/types/app/queries/witness-inquisitor.d.ts +56 -5
  186. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  187. package/types/app/runtime/agent-runtime.d.ts +34 -3
  188. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  189. package/types/app/runtime/cue-runtime.d.ts +15 -3
  190. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  191. package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -1
  192. package/types/app/runtime/operation-owners.d.ts +16 -26
  193. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  194. package/types/app/runtime/workflow-contract.d.ts +21 -6
  195. package/types/app/runtime/workflow-contract.d.ts.map +1 -1
  196. package/types/app/transport/http-client.d.ts +9 -0
  197. package/types/app/transport/http-client.d.ts.map +1 -1
  198. package/types/app/transport/message-source.d.ts +6 -3
  199. package/types/app/transport/message-source.d.ts.map +1 -1
  200. package/types/app/transport/messengering.d.ts +10 -1
  201. package/types/app/transport/messengering.d.ts.map +1 -1
  202. package/types/app/witness/ingress.d.ts +1 -0
  203. package/types/app/witness/ingress.d.ts.map +1 -1
  204. package/types/app/witness/receipt-client.d.ts +19 -1
  205. package/types/app/witness/receipt-client.d.ts.map +1 -1
  206. package/types/app/witness/receipting.d.ts +24 -1
  207. package/types/app/witness/receipting.d.ts.map +1 -1
  208. package/types/cli/messaging/multisig.d.ts.map +1 -1
  209. package/types/core/cues.d.ts +53 -1
  210. package/types/core/cues.d.ts.map +1 -1
  211. package/types/core/errors.d.ts +32 -0
  212. package/types/core/errors.d.ts.map +1 -1
  213. package/types/core/kel/delegation-ancestry.d.ts.map +1 -1
  214. package/types/core/kel/eventing.d.ts.map +1 -1
  215. package/types/core/kel/kever-decisions.d.ts +3 -1
  216. package/types/core/kel/kever-decisions.d.ts.map +1 -1
  217. package/types/core/kel/kever.d.ts +7 -1
  218. package/types/core/kel/kever.d.ts.map +1 -1
  219. package/types/core/mailbox/kram-policy.d.ts +12 -0
  220. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  221. package/types/core/mailbox/kram-pruning.d.ts +5 -1
  222. package/types/core/mailbox/kram-pruning.d.ts.map +1 -1
  223. package/types/core/mailbox/kram-time.d.ts +26 -2
  224. package/types/core/mailbox/kram-time.d.ts.map +1 -1
  225. package/types/core/mailbox/kraming.d.ts +14 -1
  226. package/types/core/mailbox/kraming.d.ts.map +1 -1
  227. package/types/core/missing-anchor-query.d.ts +34 -0
  228. package/types/core/missing-anchor-query.d.ts.map +1 -0
  229. package/types/core/routing.d.ts +3 -7
  230. package/types/core/routing.d.ts.map +1 -1
  231. package/types/core/scheming.d.ts.map +1 -1
  232. package/types/core/wire-fields.d.ts +56 -0
  233. package/types/core/wire-fields.d.ts.map +1 -0
  234. package/types/db/basing.d.ts.map +1 -1
  235. package/types/db/core/indexeddb-persistence.d.ts +3 -2
  236. package/types/db/core/indexeddb-persistence.d.ts.map +1 -1
  237. package/types/db/core/webdber.d.ts +32 -6
  238. package/types/db/core/webdber.d.ts.map +1 -1
  239. package/types/db/exchange/exchange-records.d.ts +4 -0
  240. package/types/db/exchange/exchange-records.d.ts.map +1 -1
  241. package/types/db/mailbox/mailboxing.d.ts +10 -1
  242. package/types/db/mailbox/mailboxing.d.ts.map +1 -1
  243. package/types/db/subing.d.ts +2 -0
  244. package/types/db/subing.d.ts.map +1 -1
  245. package/types/runtime/index.d.ts +20 -14
  246. package/types/runtime/index.d.ts.map +1 -1
  247. package/types/time/mod.d.ts +2 -0
  248. package/types/time/mod.d.ts.map +1 -1
  249. package/types/vdr/credential-tsn-record.d.ts.map +1 -1
  250. package/types/vdr/eventing.d.ts +32 -1
  251. package/types/vdr/eventing.d.ts.map +1 -1
  252. package/types/vdr/registry-tsn-record.d.ts.map +1 -1
  253. package/types/version.d.ts +2 -2
  254. package/esm/keri/npm/src/app/runtime/signer-conditions.js +0 -51
  255. package/types/app/runtime/signer-conditions.d.ts +0 -32
  256. package/types/app/runtime/signer-conditions.d.ts.map +0 -1
@@ -16,6 +16,9 @@
16
16
  * | holder custodian or member | {@link approveGroupIssuanceExchange} | consent to DH (contributions follow as they arrive) |
17
17
  * | a member that only follows | {@link followGroupIssuanceProof} | nothing; verifies the custodian's final proof |
18
18
  *
19
+ * The follow is not a consent and needs no call: the owner begins it itself (below). The explicit call remains for a
20
+ * proof supplied as bytes and is idempotent with the automatic follow.
21
+ *
19
22
  * The registry update is a separate consent: the custodian proposes the BUP through `proposeRegistryEvent`
20
23
  * (its own contribution is its approval) and every other issuer member approves that proposal with
21
24
  * `approveGroupProposal`. Admit is a separate consent as well.
@@ -25,20 +28,30 @@
25
28
  * Grant and the proof broadcast to the other members. Root copies are not sent here: the DH completion cue
26
29
  * feeds the runtime's `RootReplicator`, which sends exactly one copy per member.
27
30
  *
31
+ * Automatic follow: on its slow sweep of the pending exchange escrow the owner begins the proof-only follow, in either
32
+ * role, of each pending group Grant inside its deadline that this member has no live, ended or complete operation for
33
+ * and whose custodian's final proof is stored ({@link storedGroupProof}). The proof exists only after the key exchange
34
+ * finished, so the follow never takes the place of a member whose signature is still wanted; until the proof or a root
35
+ * copy is stored the member reads `awaitingExchangeConsent` and consents as usual. A member holding a root copy or the
36
+ * proof reads `awaitingAnchor` ({@link awaitsGroupProof}) until the follow accepts the Grant.
37
+ *
28
38
  * Reload: nothing about a group operation is durable but the protocol stores. A member that reloads reads
29
39
  * `awaitingExchangeConsent` for a pending Grant it holds no root for and consents again with the same call;
30
40
  * its signatures are deterministic, so consenting twice produces the same bytes. A custodian that reloaded
31
- * after retaining its root resumes when the stores allow it ({@link deriveGroupCustody}).
41
+ * after retaining its root resumes when the stores allow it ({@link deriveGroupCustody}). A follow signs nothing, so
42
+ * the first sweep after a reload begins it again from the stored proof.
32
43
  */
33
44
  import { bytesEqual, collectPayload, concatBytes, Counter, framePayload, parseBytes, SerderACDC, SerderKERI, Vrsn_2_0, } from "cesr-ts";
34
45
  import { call } from "effection";
35
46
  import { RegistryAuthor } from "../../acdc/authoring.js";
36
47
  import { RegistryCustody } from "../../acdc/custody.js";
37
48
  import { Dhke, grantTransactionId } from "../../acdc/dhking.js";
49
+ import { ThresholdQueryResend } from "../../acdc/query-resend.js";
38
50
  import { vet } from "../../acdc/regeventing.js";
39
51
  import { RegistryRoot } from "../../acdc/salting.js";
40
52
  import { ValidationError } from "../../core/errors.js";
41
53
  import { Kever } from "../../core/kel/kever.js";
54
+ import { multiKeyQueryWindow } from "../../core/mailbox/kram-policy.js";
42
55
  import { messagize } from "../../core/protocol-serialization.js";
43
56
  import { loadNests } from "../../db/exchange/exchange-nests.js";
44
57
  import { timestampMicros } from "../../time/mod.js";
@@ -197,29 +210,76 @@ export function deriveGroupCustody(deps, hab, grant) {
197
210
  if (!isLocalGroupHab(hby, hab) || !hab.kever)
198
211
  return null;
199
212
  const member = localGroupMember(hby, hab.pre);
200
- const registries = new Set();
201
- for (const nest of deps.exc.retainedInput(grant)?.nests ?? []) {
202
- const rd = nest.body.ked?.rd;
203
- if (typeof rd === "string")
204
- registries.add(rd);
205
- }
213
+ const registries = disclosedRegistries(deps.exc, grant);
206
214
  if (!registries.size || [...registries].some((registry) => !hab.mgr.hasRegistryRoot(hab.pre, registry))) {
207
215
  return null;
208
216
  }
209
217
  if (receivedRootCopy(hby, hab.pre, member.pre, registries))
210
218
  return null;
211
- const events = [...hby.db.getKelItemIter(hab.pre)].slice(-GROUP_COORDINATOR_SEARCH);
219
+ const coordinator = electingCoordinator(hby, hab.pre, member.pre);
220
+ return coordinator ? { coordinator, custodian: member.pre, lead: true } : null;
221
+ }
222
+ /** The registries a retained Grant discloses credentials of. */
223
+ function disclosedRegistries(exc, grant) {
224
+ const registries = new Set();
225
+ for (const nest of exc.retainedInput(grant)?.nests ?? []) {
226
+ const rd = nest.body.ked?.rd;
227
+ if (typeof rd === "string")
228
+ registries.add(rd);
229
+ }
230
+ return registries;
231
+ }
232
+ /** The latest of the group's recent accepted events whose threshold evidence elects `member`, or null. */
233
+ function electingCoordinator(hby, group, member) {
234
+ const events = [...hby.db.getKelItemIter(group)].slice(-GROUP_COORDINATOR_SEARCH);
212
235
  for (const [, said] of events.reverse()) {
213
- const event = hby.db.getEvtSerder(hab.pre, said);
236
+ const event = hby.db.getEvtSerder(group, said);
214
237
  if (!event || event.sn === null)
215
238
  continue;
216
239
  const coordinator = { sn: event.sn, said: event.said };
217
- if (groupElectedMember(hby, hab.pre, coordinator) === member.pre) {
218
- return { coordinator, custodian: member.pre, lead: true };
219
- }
240
+ if (groupElectedMember(hby, group, coordinator) === member)
241
+ return coordinator;
220
242
  }
221
243
  return null;
222
244
  }
245
+ /**
246
+ * The election of a final proof the local member of `hab` can follow for `grant` without taking part: the
247
+ * `/multisig/ipex-proof` wrapper of this exact Grant that another current signing member published, with a recent
248
+ * accepted group event electing that member as custodian. The custodian publishes the proof only after the key
249
+ * exchange finished and the registry update was accepted, so once it is stored no member's signature is still wanted
250
+ * for this issuance. The sender names the custodian exactly, which a root copy cannot (any member holding a root
251
+ * sends copies). Null when no such proof is stored. A pure read of the stored exchanges.
252
+ */
253
+ export function storedGroupProof(deps, hab, grant) {
254
+ const { hby } = deps;
255
+ if (!isLocalGroupHab(hby, hab) || !hab.kever)
256
+ return null;
257
+ const member = localGroupMember(hby, hab.pre).pre, signing = groupSigningMembers(hby, hab.pre);
258
+ for (const [, parent] of hby.db.exns.getTopItemIter()) {
259
+ const attrs = parent.sad?.a;
260
+ if (parent.route !== MULTISIG_IPEX_PROOF_ROUTE || attrs?.gid !== hab.pre || attrs?.grant !== grant
261
+ || !parent.pre || parent.pre === member || !signing.includes(parent.pre))
262
+ continue;
263
+ const coordinator = electingCoordinator(hby, hab.pre, parent.pre);
264
+ if (coordinator)
265
+ return { coordinator, custodian: parent.pre, lead: false };
266
+ }
267
+ return null;
268
+ }
269
+ /**
270
+ * Whether the group completed `grant` without the local member of `hab`, so that its runtime follows the final proof
271
+ * instead of asking it to consent: a `/registry/root` copy of a disclosed registry was sent to it (copies are
272
+ * authorized only once the registry update is accepted, after the key exchange), or the custodian's final proof is
273
+ * stored ({@link storedGroupProof}). A pure read of the stores.
274
+ */
275
+ export function awaitsGroupProof(deps, hab, grant) {
276
+ const { hby } = deps;
277
+ if (!isLocalGroupHab(hby, hab) || !hab.kever)
278
+ return false;
279
+ const member = localGroupMember(hby, hab.pre).pre, registries = disclosedRegistries(deps.exc, grant);
280
+ return (registries.size > 0 && receivedRootCopy(hby, hab.pre, member, registries))
281
+ || !!storedGroupProof(deps, hab, grant);
282
+ }
223
283
  /** Whether `member` was sent a `/registry/root` copy of the group's root for any of `registries`. */
224
284
  function receivedRootCopy(hby, group, member, registries) {
225
285
  for (const [, copy] of hby.db.exns.getTopItemIter()) {
@@ -421,23 +481,51 @@ export function approveGroupIssuanceExchange(ops, said, coordinator) {
421
481
  return ops.beginGroup(new GroupIssuance(party, "holder", grant, nodes));
422
482
  }
423
483
  /**
424
- * Follow a group issuance that has already been contributed to, only to verify and accept the custodian's final
425
- * proof: the file-free path of an issuer member that left after contributing and returns for the proof. The group
426
- * is the Grant's sender. Signs nothing. The proof may already be stored (it arrives by mailbox), or be supplied as
427
- * bytes.
484
+ * Follow a group issuance only to verify and accept the custodian's final proof, signing nothing. It serves any
485
+ * member that does not contribute to the key exchange: an issuer member that left after contributing and returns
486
+ * for the proof, or a holder member the group completed without, such as one whose signing weight is 0. The group
487
+ * is the Grant's sender when that is a local group, otherwise its recipient. The proof may already be stored (it
488
+ * arrives by mailbox), or be supplied as bytes.
489
+ *
490
+ * The owner begins the same follow on its own once the custodian's proof is stored ({@link storedGroupProof}), so a
491
+ * host needs this call only to supply the proof as bytes or to pin the coordinator. It is idempotent with that
492
+ * follow; see {@link beginGroupProofFollow}.
428
493
  *
429
- * @throws ValidationError for a Grant this runtime's groups did not send, or one already begun.
494
+ * @throws ValidationError for a Grant that is not pending for a local group, or one begun under a consent.
430
495
  */
431
496
  export function* followGroupIssuanceProof(ops, said, options = {}) {
432
- const { deps } = ops;
433
- if (ops.liveOf(said, "issuer") || partyComplete(deps, said, "issuer")) {
497
+ const { deps } = ops, sent = deps.exc.retainedInput(said)?.serder.pre;
498
+ const sender = sent ? deps.hby.habs.get(sent) : undefined;
499
+ const role = sender && isLocalGroupHab(deps.hby, sender) ? "issuer" : "holder";
500
+ const begun = beginGroupProofFollow(ops, said, role, options.coordinator);
501
+ // Stored for the follow's next scan, whether it began now or earlier.
502
+ if (options.proof)
503
+ yield* deps.reactor.processBytes(options.proof);
504
+ return begun;
505
+ }
506
+ /**
507
+ * Begin `role`'s proof-only follow of the pending group Grant `said` under `coordinator` (default the group's latest
508
+ * establishment event). Signs nothing. Idempotent: when that party's follow is already live, or its side of the Grant
509
+ * is already complete, returns the Grant and its transaction and begins nothing. The owner's sweep and
510
+ * {@link followGroupIssuanceProof} both begin follows here.
511
+ *
512
+ * @throws ValidationError for a Grant that is not pending for a local group, one whose live operation is a consent,
513
+ * or a coordinator {@link GroupParty.resolve} refuses.
514
+ */
515
+ export function beginGroupProofFollow(ops, said, role, coordinator) {
516
+ const { deps } = ops, live = ops.liveOf(said, role);
517
+ if (live?.group?.mode === "proofOnly")
518
+ return { grant: said, xid: live.xid };
519
+ if (live)
434
520
  throw new ValidationError("Issuance was already begun for this Grant");
521
+ if (partyComplete(deps, said, role)) {
522
+ const serder = deps.exc.retainedInput(said)?.serder;
523
+ return { grant: said, xid: serder ? grantTransactionId(serder) : said };
435
524
  }
436
525
  const { grant, nodes } = pendingGrant(deps, said);
437
- const party = GroupParty.resolve(deps, localGroup(deps, grant.pre), options.coordinator);
438
- if (options.proof)
439
- yield* deps.reactor.processBytes(options.proof);
440
- return ops.beginGroup(new GroupIssuance(party, "issuer", grant, nodes, { mode: "proofOnly" }));
526
+ const group = localGroup(deps, role === "issuer" ? grant.pre : grant.sad.ri);
527
+ const party = GroupParty.resolve(deps, group, coordinator);
528
+ return ops.beginGroup(new GroupIssuance(party, role, grant, nodes, { mode: "proofOnly" }));
441
529
  }
442
530
  /**
443
531
  * The custodian's exact public proof of a completed group issuance: the proof support and the
@@ -527,17 +615,12 @@ export class GroupIssuance {
527
615
  writable: true,
528
616
  value: void 0
529
617
  });
530
- Object.defineProperty(this, "query", {
618
+ /** The frozen threshold query and its resend schedule; a group cannot re-date it (`Dhke.requery` refuses groups). */
619
+ Object.defineProperty(this, "resend", {
531
620
  enumerable: true,
532
621
  configurable: true,
533
622
  writable: true,
534
- value: void 0
535
- });
536
- Object.defineProperty(this, "nextQuery", {
537
- enumerable: true,
538
- configurable: true,
539
- writable: true,
540
- value: 0
623
+ value: new ThresholdQueryResend()
541
624
  });
542
625
  Object.defineProperty(this, "nextProofScan", {
543
626
  enumerable: true,
@@ -565,6 +648,13 @@ export class GroupIssuance {
565
648
  writable: true,
566
649
  value: void 0
567
650
  });
651
+ /** What the Grant send's proof support carried; absent when this operation did not send it (rebuilt after a restart). */
652
+ Object.defineProperty(this, "supportMark", {
653
+ enumerable: true,
654
+ configurable: true,
655
+ writable: true,
656
+ value: void 0
657
+ });
568
658
  Object.defineProperty(this, "mode", {
569
659
  enumerable: true,
570
660
  configurable: true,
@@ -800,7 +890,9 @@ export function* stepGroup(ops, op) {
800
890
  const input = deps.exc.retainedInput(grant, { pending: true });
801
891
  const frame = parseBytes(messagize(input.serder, { tsgs: input.tsgs, cigars: input.cigars, gvrsn: Vrsn_2_0 }))[0];
802
892
  frame.nests = [...input.nests ?? []];
803
- const full = concatBytes(ops.discloser.proofSupport(g.nodeBytes, hab), yield* wire(frame));
893
+ const support = ops.discloser.proofSupport(g.nodeBytes, hab);
894
+ g.supportMark = ops.discloser.proofSupportMark(g.nodeBytes, hab);
895
+ const full = concatBytes(support, yield* wire(frame));
804
896
  for (const peer of party.others())
805
897
  yield* send(peer, full);
806
898
  if (issuer) {
@@ -854,13 +946,17 @@ export function* stepGroup(ops, op) {
854
946
  yield* broadcast(yield* party.wrap(child, MULTISIG_RPY_ROUTE));
855
947
  }
856
948
  else if (message.kin === "query")
857
- g.query = message.msg;
949
+ g.resend.set(message.msg);
858
950
  else
859
951
  yield* send(message.dest, message.msg, "credential");
860
952
  }
861
- if (g.query && deps.clock.now() >= g.nextQuery && !dh.complete(xid)) {
862
- yield* send(g.grant.sad.ri, g.query, "credential");
863
- g.nextQuery = deps.clock.now() + (deps.queryResendMs ?? ISSUANCE_QUERY_RESEND_MS);
953
+ // The unchanged query is resent with backoff while the holder can still accept it; the operation keeps waiting
954
+ // for the holder past that, to its own deadline.
955
+ const now = deps.clock.now(), base = deps.queryResendMs ?? ISSUANCE_QUERY_RESEND_MS;
956
+ if (g.resend.ready(now) && g.resend.fresh(now, multiKeyQueryWindow(deps.hby.db, Dhke.Route))
957
+ && !dh.complete(xid)) {
958
+ yield* send(g.grant.sad.ri, g.resend.query, "credential");
959
+ g.resend.sent(now, base);
864
960
  }
865
961
  if (!lead || !dh.complete(xid))
866
962
  return false;
@@ -894,7 +990,11 @@ export function* stepGroup(ops, op) {
894
990
  if (!deps.exc.complete(grant))
895
991
  throw new ValidationError("Issued group Grant remains unaccepted");
896
992
  const completed = parseBytes(yield* storedBytes(deps.exc, grant))[0];
897
- const support = ops.discloser.proofSupport(g.nodeBytes, hab);
993
+ // The Grant send already carried the support; proving added the anchoring event and the registry update. An
994
+ // operation without a mark did not send it, so it sends in full.
995
+ const support = g.supportMark
996
+ ? ops.discloser.proofSupportSince(g.nodeBytes, hab, g.supportMark)
997
+ : ops.discloser.proofSupport(g.nodeBytes, hab);
898
998
  // KERIpy form: the elected member shares the proof with the other members by mailbox.
899
999
  const wrapped = yield* party.wrap(completed, MULTISIG_IPEX_PROOF_ROUTE);
900
1000
  // Kept locally, so the proof the members received is the one {@link groupIssuanceProof} exports.
@@ -88,6 +88,32 @@ export function* storedExchangeWire(retained, gvrsn = Vrsn_2_0) {
88
88
  frame.nests = nests;
89
89
  return yield* call(() => collectPayload(framePayload(frame)));
90
90
  }
91
+ /**
92
+ * Extend a frozen Grant's exact origin proof with its presented-state opening and the KEL seal of the registry update
93
+ * that commits to it; the Grant body and its SAID are unchanged. Ordinary intake still authenticates the supplied
94
+ * presentation evidence. Reads no store and signs nothing.
95
+ *
96
+ * @throws ValidationError when the opening does not target this exact Grant's disclosed Issuee in the `presented` state.
97
+ */
98
+ export function* attachPresentationProof(prepared, opening, source) {
99
+ const frames = parseBytes(prepared.raw), node = frames[0].body;
100
+ if (!node.issuee || opening.td.nonce !== prepared.exn.said || opening.ts.label !== "presented") {
101
+ throw new ValidationError("Presentation proof must target this exact Grant and disclosed Issuee.");
102
+ }
103
+ const seal = Counter.enclose({
104
+ code: "SealSourceTriples",
105
+ qb64: concatBytes(new Prefixer({ qb64: node.issuee }).qb64b, source.s.qb64b, source.d.qb64b),
106
+ version: Vrsn_2_0,
107
+ });
108
+ const first = concatBytes(node.raw, Counter.enclose({
109
+ qb64: concatBytes(...frames[0].attachments.map(group => group.raw), publicOpeningAttachment(opening), seal),
110
+ version: Vrsn_2_0,
111
+ }));
112
+ const rest = yield* call(() => Promise.all(frames.slice(1).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
113
+ const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), first, ...rest);
114
+ const payloads = yield* call(() => Promise.all(parseBytes(raw).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
115
+ return { exn: prepared.exn, raw, nests: concatBytes(...payloads) };
116
+ }
91
117
  /** Borrow concrete owners only. No constructor opens a store, starts a worker or loads registry secrets. */
92
118
  export class IpexDiscloser {
93
119
  constructor(hby, registry, exc, receiptor, custody, views) {
@@ -201,7 +227,65 @@ export class IpexDiscloser {
201
227
  }
202
228
  /** Carry selected public evidence and root-first delegation ancestry; receiver intake still verifies every approval. */
203
229
  proofSupport(raw, hab) {
204
- const aids = new Set([hab.pre]), registries = new Set();
230
+ return this.proofSupportFor(raw, [hab.pre]);
231
+ }
232
+ /**
233
+ * `proofSupport` for explicitly named parties instead of one habitat: the KELs of `parties` and of every identifier
234
+ * the ACDC frames in `raw` name, each after its delegation ancestry, then the accepted TEL of every registry they
235
+ * name. Reads accepted state only and signs nothing, so it needs no keys.
236
+ */
237
+ proofSupportFor(raw, parties) {
238
+ const { ordered, registries } = this.supportSelection(raw, parties);
239
+ const messages = [Counter.makeGVC(Vrsn_2_0)];
240
+ // Keep first-seen authorizers and exact stored attachments, including superseded history.
241
+ // clonePreIter skips broken rows; finite disclosure must instead fail if an accepted row cannot be replayed.
242
+ for (const aid of ordered)
243
+ messages.push(...this.supportKel(aid, 0));
244
+ for (const regid of registries)
245
+ messages.push(this.registry.cloneTel(regid));
246
+ return concatBytes(...messages);
247
+ }
248
+ /**
249
+ * Record how far `proofSupport(raw, hab)` reaches right now: each KEL's last first-seen ordinal and each
250
+ * registry's last TEL ordinal. Call it in the same synchronous turn as the send it describes, so the mark is
251
+ * exactly what that send carried; {@link proofSupportSince} then sends what accepted state has gained.
252
+ */
253
+ proofSupportMark(raw, hab) {
254
+ const { ordered, registries } = this.supportSelection(raw, [hab.pre]);
255
+ const kels = new Map(), tels = new Map();
256
+ for (const aid of ordered) {
257
+ let last = -1;
258
+ for (const [, fn] of this.hby.db.fels.getAllItemIter(aid))
259
+ last = fn;
260
+ kels.set(aid, last);
261
+ }
262
+ for (const regid of registries) {
263
+ let last = -1;
264
+ for (const [, sn] of this.registry.baser.tels.getAllItemIter(regid))
265
+ last = sn;
266
+ tels.set(regid, last);
267
+ }
268
+ return { kels, tels };
269
+ }
270
+ /**
271
+ * The part of `proofSupport(raw, hab)` that accepted state gained after `mark` was taken, in the same order and
272
+ * with the same stored attachments: later first-seen KEL events of each identifier (the anchoring interaction
273
+ * event of a registry update, say) and later TEL events of each registry. An identifier or registry the mark does
274
+ * not name (support that grew a new party) is carried whole. A recipient that holds what `mark` describes holds
275
+ * the same state after this as after a second full `proofSupport`.
276
+ */
277
+ proofSupportSince(raw, hab, mark) {
278
+ const { ordered, registries } = this.supportSelection(raw, [hab.pre]);
279
+ const messages = [Counter.makeGVC(Vrsn_2_0)];
280
+ for (const aid of ordered)
281
+ messages.push(...this.supportKel(aid, (mark.kels.get(aid) ?? -1) + 1));
282
+ for (const regid of registries)
283
+ messages.push(this.registry.cloneTel(regid, (mark.tels.get(regid) ?? -1) + 1));
284
+ return concatBytes(...messages);
285
+ }
286
+ /** The identifiers (each after its delegation ancestry) and registries that support for `raw` and `parties` names. */
287
+ supportSelection(raw, parties) {
288
+ const aids = new Set(parties), registries = new Set();
205
289
  for (const frame of parseBytes(raw)) {
206
290
  if (!(frame.body instanceof SerderACDC))
207
291
  continue;
@@ -222,7 +306,6 @@ export class IpexDiscloser {
222
306
  aids.add(seal.i.qb64);
223
307
  }
224
308
  }
225
- const messages = [Counter.makeGVC(Vrsn_2_0)];
226
309
  const db = this.hby.db, ordered = new Set();
227
310
  // An ancestry claim selects supporting KELs; it does not establish delegation authority.
228
311
  for (const aid of aids) {
@@ -241,15 +324,15 @@ export class IpexDiscloser {
241
324
  for (const ancestor of ancestors.reverse())
242
325
  ordered.add(ancestor);
243
326
  }
244
- // Keep first-seen authorizers and exact stored attachments, including superseded history.
245
- // clonePreIter skips broken rows; finite disclosure must instead fail if an accepted row cannot be replayed.
246
- for (const aid of ordered) {
247
- for (const [, fn, said] of db.fels.getAllItemIter(aid))
248
- messages.push(db.cloneEvtMsg(aid, fn, said, Vrsn_2_0));
327
+ return { ordered, registries };
328
+ }
329
+ /** Stored first-seen events of `aid` from ordinal `from`, each with its exact stored attachments. */
330
+ supportKel(aid, from) {
331
+ const messages = [];
332
+ for (const [, fn, said] of this.hby.db.fels.getAllItemIter(aid, from)) {
333
+ messages.push(this.hby.db.cloneEvtMsg(aid, fn, said, Vrsn_2_0));
249
334
  }
250
- for (const regid of registries)
251
- messages.push(this.registry.cloneTel(regid));
252
- return concatBytes(...messages);
335
+ return messages;
253
336
  }
254
337
  /** Public replay includes actual stored witness receipts; it grants no new consent. */
255
338
  kel(hab) {
@@ -399,26 +482,6 @@ export class IpexDiscloser {
399
482
  const prior = yield* this.prior(priorSaid);
400
483
  return ipexExnBody(sender, `/ipex/${verb}`, [], { message, prior, recipient });
401
484
  }
402
- /** Extend the frozen Grant's exact origin proof; ordinary intake still authenticates the supplied presentation evidence. */
403
- *attachPresentationProof(prepared, opening, source) {
404
- const frames = parseBytes(prepared.raw), node = frames[0].body;
405
- if (!node.issuee || opening.td.nonce !== prepared.exn.said || opening.ts.label !== "presented") {
406
- throw new ValidationError("Presentation proof must target this exact Grant and disclosed Issuee.");
407
- }
408
- const seal = Counter.enclose({
409
- code: "SealSourceTriples",
410
- qb64: concatBytes(new Prefixer({ qb64: node.issuee }).qb64b, source.s.qb64b, source.d.qb64b),
411
- version: Vrsn_2_0,
412
- });
413
- const first = concatBytes(node.raw, Counter.enclose({
414
- qb64: concatBytes(...frames[0].attachments.map(group => group.raw), publicOpeningAttachment(opening), seal),
415
- version: Vrsn_2_0,
416
- }));
417
- const rest = yield* call(() => Promise.all(frames.slice(1).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
418
- const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), first, ...rest);
419
- const payloads = yield* call(() => Promise.all(parseBytes(raw).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
420
- return { exn: prepared.exn, raw, nests: concatBytes(...payloads) };
421
- }
422
485
  /** Select immutable admitted/transferred nests; proof refresh changes attachments only when explicitly requested. */
423
486
  *selected(selection, offer = false) {
424
487
  if ([selection.said, selection.fromGrant, selection.transferred].filter(value => value !== undefined).length !== 1) {
@@ -540,7 +603,7 @@ export class IpexDiscloser {
540
603
  if (reg.store.seqEvent(reg.regk, Number(BigInt(`0x${update.sad.n}`)))?.said !== update.said) {
541
604
  throw new ValidationError("Presentation BUP remains pending authenticated acceptance.");
542
605
  }
543
- ({ raw, nests } = yield* this.attachPresentationProof({ exn, raw, nests }, proof, seal));
606
+ ({ raw, nests } = yield* attachPresentationProof({ exn, raw, nests }, proof, seal));
544
607
  }
545
608
  const source = exn.sad.a.ax?.[0]
546
609
  ? yield* anchorExchange(this.hby, this.receiptor, hab, exn, options.auths)
@@ -12,7 +12,7 @@ import { call } from "effection";
12
12
  import { evaluateChain } from "../../acdc/chaining.js";
13
13
  import { resolveAttachmentGvrsn } from "../../core/attachment-countering.js";
14
14
  import { TransIdxSigGroup } from "../../core/dispatch.js";
15
- import { ValidationError } from "../../core/errors.js";
15
+ import { KramRefusedError, ValidationError } from "../../core/errors.js";
16
16
  import { verifyExchangeProofs } from "../../core/exchange/exchange-proofing.js";
17
17
  import { exchange, serializeMessage } from "../../core/exchange/protocol-exchanging.js";
18
18
  import { Kever } from "../../core/kel/kever.js";
@@ -74,7 +74,19 @@ export function requireAcdcV2GroupUse(hby, protocol, embedded) {
74
74
  throw new ValidationError("ACDC v2 group graph is unavailable for completed use.");
75
75
  }
76
76
  }
77
- /** Approve exactly one ACDC v2 IPEX stream before adding this member's signature; no new acceptance rows are manufactured. */
77
+ /** KRAM refusals after which the message can no longer be accepted; a `replay` is routine, not one of them. */
78
+ const KRAM_EXPIRY_REASONS = new Set(["timestamp", "timeliness", "unknownThread", "lifetime"]);
79
+ /** Whether `decision` is this receiver's KRAM refusing a message for its time, its thread or its lifetime. */
80
+ function isKramExpiry(decision) {
81
+ return !!decision && decision.kind === "drop" && "reason" in decision && KRAM_EXPIRY_REASONS.has(decision.reason);
82
+ }
83
+ /**
84
+ * Approve exactly one ACDC v2 IPEX stream before adding this member's signature; no new acceptance rows are manufactured.
85
+ *
86
+ * @returns This member's accepted wire, or null while its evidence or dependencies are missing.
87
+ * @throws KramRefusedError when this member's own KRAM refuses the contribution for its time, thread or exchange
88
+ * lifetime: the message can no longer be accepted here. A `replay` refusal is routine and is not thrown.
89
+ */
78
90
  export function* prepareAcdcV2GroupIpex(hby, protocol, group, raw) {
79
91
  const frames = parseBytes(concatBytes(Counter.makeGVC(Vrsn_2_0), raw));
80
92
  const frame = frames[0], embedded = frame?.body;
@@ -136,7 +148,11 @@ export function* prepareAcdcV2GroupIpex(hby, protocol, group, raw) {
136
148
  };
137
149
  const attachments = yield* call(() => collectPayload(attachmentPayload(approved, { gvrsn: Vrsn_2_0 })));
138
150
  const wire = concatBytes(embedded.raw, attachments);
139
- yield* new Reactor(protocol).processBytes(wire, { local: true });
151
+ // Dispatch the frame itself, not its bytes, so this member's own KRAM decision is visible. A refusal here is
152
+ // not a remote outcome to skip: the contribution was never accepted and nothing else will complete the message.
153
+ const { decision } = yield* protocol.dispatch(parseBytes(wire)[0], { local: true }, new Reactor(protocol).kvy);
154
+ if (isKramExpiry(decision))
155
+ throw new KramRefusedError(embedded.said, decision);
140
156
  yield* protocol.processEscrowsOnce();
141
157
  return wire;
142
158
  }
@@ -457,6 +473,40 @@ export function* approveStoredMultisigIpexWrappers(hby, protocol, embeddedSaid,
457
473
  yield* approveIpexChild(hby, protocol, groupHab, child, wrapper, reger);
458
474
  }
459
475
  }
476
+ /**
477
+ * The copies this member received of each v2 group IPEX message it has not accepted: per message, the child of each
478
+ * stored, authenticated `/multisig/exn` wrapper that carries it, with its signatures and nests. A pure read; whether a
479
+ * message may be taken from its copies is {@link groupIpexFollowable}'s rule.
480
+ */
481
+ export function unacceptedGroupIpexCopies(hby, exchanger) {
482
+ const copies = new Map();
483
+ for (const [, wrapper] of hby.db.exns.getTopItemIter()) {
484
+ if (wrapper.route !== MULTISIG_EXN_ROUTE || wrapper.pvrsn.major !== 2)
485
+ continue;
486
+ const said = storedMultisigIpexBody(hby.db, wrapper)?.said;
487
+ if (!said || exchanger.complete(said) || !exchanger.verifyAccepted(wrapper.said, MULTISIG_EXN_ROUTE))
488
+ continue;
489
+ copies.set(said, [...copies.get(said) ?? [], loadNests(hby.db, wrapper.said)[0]]);
490
+ }
491
+ return [...copies.values()];
492
+ }
493
+ /**
494
+ * Accept a group IPEX message from the copies this member received, adding no signature: dispatch each copy with its
495
+ * own proofs, then advance escrows so that copies whose signatures only together meet the threshold complete. The
496
+ * caller has checked {@link groupIpexFollowable}.
497
+ *
498
+ * @throws KramRefusedError when this member's own KRAM refuses a copy for its time, thread or exchange lifetime: the
499
+ * message can no longer be accepted here.
500
+ */
501
+ export function* acceptGroupIpexCopies(protocol, copies) {
502
+ const kvy = new Reactor(protocol).kvy;
503
+ for (const copy of copies) {
504
+ const { decision } = yield* protocol.dispatch(copy, { local: true }, kvy);
505
+ if (isKramExpiry(decision))
506
+ throw new KramRefusedError(copy.body.said, decision);
507
+ }
508
+ yield* protocol.processEscrowsOnce();
509
+ }
460
510
  /**
461
511
  * Return the first incomplete stored `/multisig/exn` wrapping `/ipex/*`.
462
512
  *
@@ -464,8 +464,13 @@ export class IpexV2Handler {
464
464
  }
465
465
  return { pending, registries };
466
466
  }
467
- /** Notify only after Exchanger has durably accepted the parent and its evidence. */
468
- handle({ serder }) {
467
+ /**
468
+ * Notify only after Exchanger has durably accepted the parent and its evidence. A retained exchange (history a
469
+ * local import replayed, such as an already admitted Grant and its Admit) is not news: no notice asks anyone to act.
470
+ */
471
+ handle({ serder, retained }) {
472
+ if (retained)
473
+ return;
469
474
  this.notifier?.add({ r: `/exn${serder.route}`, d: serder.said, m: serder.sad.a.m });
470
475
  }
471
476
  /** Every disclosed own schema vets before presentation and issuer graph reduction. */