keri-ts 2.2.4 → 2.2.5

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 (233) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +14 -14
  2. package/esm/keri/npm/src/acdc/registering.js +19 -0
  3. package/esm/keri/npm/src/acdc/salting.js +4 -0
  4. package/esm/keri/npm/src/app/bootstrap/authenticating.js +32 -10
  5. package/esm/keri/npm/src/app/bootstrap/oobiery.js +363 -30
  6. package/esm/keri/npm/src/app/credentials/credential-importing.js +33 -4
  7. package/esm/keri/npm/src/app/credentials/root-replicator.js +27 -15
  8. package/esm/keri/npm/src/app/credentials/schema-resolving.js +5 -1
  9. package/esm/keri/npm/src/app/credentials/tel-publishing.js +16 -7
  10. package/esm/keri/npm/src/app/delivery/forwarding.js +177 -43
  11. package/esm/keri/npm/src/app/delivery/relaying.js +232 -0
  12. package/esm/keri/npm/src/app/delivery/respondant.js +8 -5
  13. package/esm/keri/npm/src/app/exchanges/exchange-messaging.js +1 -1
  14. package/esm/keri/npm/src/app/exchanges/exchanging.js +65 -9
  15. package/esm/keri/npm/src/app/identity/delegating.js +66 -9
  16. package/esm/keri/npm/src/app/identity/habbing.js +190 -71
  17. package/esm/keri/npm/src/app/identity/keeping.js +111 -39
  18. package/esm/keri/npm/src/app/identity/retire-identifier.js +98 -0
  19. package/esm/keri/npm/src/app/ipex/disclosure-replaying.js +116 -0
  20. package/esm/keri/npm/src/app/ipex/group-ipex.js +122 -25
  21. package/esm/keri/npm/src/app/ipex/group-issuance.js +109 -23
  22. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +36 -22
  23. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +53 -3
  24. package/esm/keri/npm/src/app/ipex/ipex-v2.js +7 -2
  25. package/esm/keri/npm/src/app/ipex/issuance.js +131 -35
  26. package/esm/keri/npm/src/app/ipex/pending-presentations.js +4 -2
  27. package/esm/keri/npm/src/app/ipex/presenting.js +3 -18
  28. package/esm/keri/npm/src/app/ipex/receive-issuance.js +2 -1
  29. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +16 -3
  30. package/esm/keri/npm/src/app/mailbox/agent-inbox.js +292 -0
  31. package/esm/keri/npm/src/app/mailbox/forward-handler.js +298 -52
  32. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +26 -31
  33. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +70 -62
  34. package/esm/keri/npm/src/app/mailbox/mailboxing.js +20 -4
  35. package/esm/keri/npm/src/app/multisig/counseling.js +71 -27
  36. package/esm/keri/npm/src/app/multisig/group-members.js +41 -5
  37. package/esm/keri/npm/src/app/multisig/grouping.js +50 -2
  38. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +3 -18
  39. package/esm/keri/npm/src/app/multisig/proposing.js +1 -16
  40. package/esm/keri/npm/src/app/multisig/rejoin-group.js +99 -0
  41. package/esm/keri/npm/src/app/multisig/replace-member.js +29 -8
  42. package/esm/keri/npm/src/app/protocol/parsering.js +45 -17
  43. package/esm/keri/npm/src/app/protocol/protocol-processing.js +47 -21
  44. package/esm/keri/npm/src/app/queries/query-replies.js +0 -22
  45. package/esm/keri/npm/src/app/queries/relay-refresh.js +81 -0
  46. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +180 -34
  47. package/esm/keri/npm/src/app/runtime/agent-runtime.js +41 -72
  48. package/esm/keri/npm/src/app/runtime/cue-runtime.js +45 -5
  49. package/esm/keri/npm/src/app/runtime/operation-owners.js +26 -56
  50. package/esm/keri/npm/src/app/runtime/workflow-contract.js +7 -4
  51. package/esm/keri/npm/src/app/transport/http-client.js +20 -2
  52. package/esm/keri/npm/src/app/transport/message-source.js +2 -1
  53. package/esm/keri/npm/src/app/transport/messengering.js +39 -8
  54. package/esm/keri/npm/src/app/witness/ingress.js +7 -0
  55. package/esm/keri/npm/src/app/witness/receipt-client.js +43 -16
  56. package/esm/keri/npm/src/app/witness/receipting.js +62 -20
  57. package/esm/keri/npm/src/cli/messaging/multisig.js +4 -3
  58. package/esm/keri/npm/src/core/errors.js +73 -0
  59. package/esm/keri/npm/src/core/kel/delegation-ancestry.js +11 -2
  60. package/esm/keri/npm/src/core/kel/eventing.js +25 -28
  61. package/esm/keri/npm/src/core/kel/kever.js +15 -9
  62. package/esm/keri/npm/src/core/mailbox/kram-policy.js +8 -0
  63. package/esm/keri/npm/src/core/mailbox/kram-pruning.js +25 -6
  64. package/esm/keri/npm/src/core/mailbox/kram-time.js +34 -6
  65. package/esm/keri/npm/src/core/mailbox/kraming.js +58 -2
  66. package/esm/keri/npm/src/core/routing.js +18 -25
  67. package/esm/keri/npm/src/core/scheming.js +12 -5
  68. package/esm/keri/npm/src/core/wire-fields.js +126 -0
  69. package/esm/keri/npm/src/db/basing.js +4 -13
  70. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +3 -2
  71. package/esm/keri/npm/src/db/core/webdber.js +85 -27
  72. package/esm/keri/npm/src/db/mailbox/mailboxing.js +13 -0
  73. package/esm/keri/npm/src/db/subing.js +4 -0
  74. package/esm/keri/npm/src/runtime/index.js +11 -5
  75. package/esm/keri/npm/src/time/mod.js +5 -0
  76. package/esm/keri/npm/src/vdr/credential-tsn-record.js +2 -7
  77. package/esm/keri/npm/src/vdr/eventing.js +28 -19
  78. package/esm/keri/npm/src/vdr/registry-tsn-record.js +3 -13
  79. package/esm/keri/npm/src/version.js +2 -2
  80. package/package.json +1 -1
  81. package/types/_dnt.polyfills.d.ts +9 -9
  82. package/types/_dnt.polyfills.d.ts.map +1 -1
  83. package/types/acdc/registering.d.ts +6 -0
  84. package/types/acdc/registering.d.ts.map +1 -1
  85. package/types/acdc/salting.d.ts.map +1 -1
  86. package/types/app/bootstrap/authenticating.d.ts +12 -2
  87. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  88. package/types/app/bootstrap/oobiery.d.ts +144 -7
  89. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  90. package/types/app/credentials/credential-importing.d.ts +8 -0
  91. package/types/app/credentials/credential-importing.d.ts.map +1 -1
  92. package/types/app/credentials/root-replicator.d.ts +19 -8
  93. package/types/app/credentials/root-replicator.d.ts.map +1 -1
  94. package/types/app/credentials/schema-resolving.d.ts.map +1 -1
  95. package/types/app/credentials/tel-publishing.d.ts +4 -2
  96. package/types/app/credentials/tel-publishing.d.ts.map +1 -1
  97. package/types/app/delivery/forwarding.d.ts +69 -5
  98. package/types/app/delivery/forwarding.d.ts.map +1 -1
  99. package/types/app/delivery/relaying.d.ts +144 -0
  100. package/types/app/delivery/relaying.d.ts.map +1 -0
  101. package/types/app/delivery/respondant.d.ts +9 -2
  102. package/types/app/delivery/respondant.d.ts.map +1 -1
  103. package/types/app/exchanges/exchanging.d.ts +36 -0
  104. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  105. package/types/app/identity/delegating.d.ts +6 -0
  106. package/types/app/identity/delegating.d.ts.map +1 -1
  107. package/types/app/identity/habbing.d.ts +106 -19
  108. package/types/app/identity/habbing.d.ts.map +1 -1
  109. package/types/app/identity/keeping.d.ts +54 -19
  110. package/types/app/identity/keeping.d.ts.map +1 -1
  111. package/types/app/identity/retire-identifier.d.ts +24 -0
  112. package/types/app/identity/retire-identifier.d.ts.map +1 -0
  113. package/types/app/ipex/disclosure-replaying.d.ts +61 -0
  114. package/types/app/ipex/disclosure-replaying.d.ts.map +1 -0
  115. package/types/app/ipex/group-ipex.d.ts +60 -14
  116. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  117. package/types/app/ipex/group-issuance.d.ts +48 -6
  118. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  119. package/types/app/ipex/ipex-discloser.d.ts +14 -2
  120. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  121. package/types/app/ipex/ipex-grouping.d.ts +23 -2
  122. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  123. package/types/app/ipex/ipex-v2.d.ts +5 -2
  124. package/types/app/ipex/ipex-v2.d.ts.map +1 -1
  125. package/types/app/ipex/issuance.d.ts +35 -11
  126. package/types/app/ipex/issuance.d.ts.map +1 -1
  127. package/types/app/ipex/pending-presentations.d.ts +4 -2
  128. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  129. package/types/app/ipex/presenting.d.ts.map +1 -1
  130. package/types/app/ipex/receive-issuance.d.ts +2 -1
  131. package/types/app/ipex/receive-issuance.d.ts.map +1 -1
  132. package/types/app/ipex/unblinding-workflow.d.ts +4 -1
  133. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  134. package/types/app/mailbox/agent-inbox.d.ts +126 -0
  135. package/types/app/mailbox/agent-inbox.d.ts.map +1 -0
  136. package/types/app/mailbox/forward-handler.d.ts +114 -14
  137. package/types/app/mailbox/forward-handler.d.ts.map +1 -1
  138. package/types/app/mailbox/kel-publisher.d.ts +12 -22
  139. package/types/app/mailbox/kel-publisher.d.ts.map +1 -1
  140. package/types/app/mailbox/mailbox-poller.d.ts +40 -24
  141. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  142. package/types/app/mailbox/mailboxing.d.ts +19 -5
  143. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  144. package/types/app/multisig/counseling.d.ts +32 -11
  145. package/types/app/multisig/counseling.d.ts.map +1 -1
  146. package/types/app/multisig/group-members.d.ts +17 -1
  147. package/types/app/multisig/group-members.d.ts.map +1 -1
  148. package/types/app/multisig/grouping.d.ts +17 -1
  149. package/types/app/multisig/grouping.d.ts.map +1 -1
  150. package/types/app/multisig/multisig-workflows.d.ts +0 -8
  151. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  152. package/types/app/multisig/proposing.d.ts +4 -19
  153. package/types/app/multisig/proposing.d.ts.map +1 -1
  154. package/types/app/multisig/rejoin-group.d.ts +119 -0
  155. package/types/app/multisig/rejoin-group.d.ts.map +1 -0
  156. package/types/app/multisig/replace-member.d.ts +15 -1
  157. package/types/app/multisig/replace-member.d.ts.map +1 -1
  158. package/types/app/protocol/parsering.d.ts +21 -2
  159. package/types/app/protocol/parsering.d.ts.map +1 -1
  160. package/types/app/protocol/protocol-processing.d.ts +25 -5
  161. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  162. package/types/app/queries/query-replies.d.ts +4 -8
  163. package/types/app/queries/query-replies.d.ts.map +1 -1
  164. package/types/app/queries/relay-refresh.d.ts +26 -0
  165. package/types/app/queries/relay-refresh.d.ts.map +1 -0
  166. package/types/app/queries/witness-inquisitor.d.ts +56 -5
  167. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  168. package/types/app/runtime/agent-runtime.d.ts +34 -3
  169. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  170. package/types/app/runtime/cue-runtime.d.ts +15 -3
  171. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  172. package/types/app/runtime/operation-owners.d.ts +16 -26
  173. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  174. package/types/app/runtime/workflow-contract.d.ts +21 -6
  175. package/types/app/runtime/workflow-contract.d.ts.map +1 -1
  176. package/types/app/transport/http-client.d.ts +9 -0
  177. package/types/app/transport/http-client.d.ts.map +1 -1
  178. package/types/app/transport/message-source.d.ts +6 -3
  179. package/types/app/transport/message-source.d.ts.map +1 -1
  180. package/types/app/transport/messengering.d.ts +10 -1
  181. package/types/app/transport/messengering.d.ts.map +1 -1
  182. package/types/app/witness/ingress.d.ts +1 -0
  183. package/types/app/witness/ingress.d.ts.map +1 -1
  184. package/types/app/witness/receipt-client.d.ts +19 -1
  185. package/types/app/witness/receipt-client.d.ts.map +1 -1
  186. package/types/app/witness/receipting.d.ts +24 -1
  187. package/types/app/witness/receipting.d.ts.map +1 -1
  188. package/types/cli/messaging/multisig.d.ts.map +1 -1
  189. package/types/core/cues.d.ts +53 -1
  190. package/types/core/cues.d.ts.map +1 -1
  191. package/types/core/errors.d.ts +32 -0
  192. package/types/core/errors.d.ts.map +1 -1
  193. package/types/core/kel/delegation-ancestry.d.ts.map +1 -1
  194. package/types/core/kel/eventing.d.ts.map +1 -1
  195. package/types/core/kel/kever-decisions.d.ts +3 -1
  196. package/types/core/kel/kever-decisions.d.ts.map +1 -1
  197. package/types/core/kel/kever.d.ts +7 -1
  198. package/types/core/kel/kever.d.ts.map +1 -1
  199. package/types/core/mailbox/kram-policy.d.ts +6 -0
  200. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  201. package/types/core/mailbox/kram-pruning.d.ts +5 -1
  202. package/types/core/mailbox/kram-pruning.d.ts.map +1 -1
  203. package/types/core/mailbox/kram-time.d.ts +26 -2
  204. package/types/core/mailbox/kram-time.d.ts.map +1 -1
  205. package/types/core/mailbox/kraming.d.ts +14 -1
  206. package/types/core/mailbox/kraming.d.ts.map +1 -1
  207. package/types/core/routing.d.ts +3 -7
  208. package/types/core/routing.d.ts.map +1 -1
  209. package/types/core/scheming.d.ts.map +1 -1
  210. package/types/core/wire-fields.d.ts +56 -0
  211. package/types/core/wire-fields.d.ts.map +1 -0
  212. package/types/db/basing.d.ts.map +1 -1
  213. package/types/db/core/indexeddb-persistence.d.ts +3 -2
  214. package/types/db/core/indexeddb-persistence.d.ts.map +1 -1
  215. package/types/db/core/webdber.d.ts +32 -6
  216. package/types/db/core/webdber.d.ts.map +1 -1
  217. package/types/db/exchange/exchange-records.d.ts +4 -0
  218. package/types/db/exchange/exchange-records.d.ts.map +1 -1
  219. package/types/db/mailbox/mailboxing.d.ts +10 -1
  220. package/types/db/mailbox/mailboxing.d.ts.map +1 -1
  221. package/types/db/subing.d.ts +2 -0
  222. package/types/db/subing.d.ts.map +1 -1
  223. package/types/runtime/index.d.ts +20 -14
  224. package/types/runtime/index.d.ts.map +1 -1
  225. package/types/time/mod.d.ts +2 -0
  226. package/types/time/mod.d.ts.map +1 -1
  227. package/types/vdr/credential-tsn-record.d.ts.map +1 -1
  228. package/types/vdr/eventing.d.ts.map +1 -1
  229. package/types/vdr/registry-tsn-record.d.ts.map +1 -1
  230. package/types/version.d.ts +2 -2
  231. package/esm/keri/npm/src/app/runtime/signer-conditions.js +0 -51
  232. package/types/app/runtime/signer-conditions.d.ts +0 -32
  233. package/types/app/runtime/signer-conditions.d.ts.map +0 -1
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Retire a local, non-group identifier from a running agent: `Habery.retireHab` at the store level, then the
3
+ * runtime's own state for it.
4
+ *
5
+ * {@link retireIdentifierRefusal} decides first, as a pure read, and {@link retireIdentifier} refuses with its
6
+ * reason before any write. Beyond `retireHab`'s own refusals (not local, a group, a local group's member or
7
+ * accepted member, a local habitat's delegator) the runtime refuses an identifier it still needs: the issuer of a
8
+ * legacy or v2 registry (a v2 registry's alias cannot be reloaded without its local issuer), one with a delegated
9
+ * event pending in `dpwe.`/`dune.`/`dpub.` or pinned as a delegation proxy, the habitat of a live issuance or
10
+ * Unblind operation, the source of a queued Poster event, the explicit signer of a queued query, and the
11
+ * querier's configured default signer.
12
+ *
13
+ * `retireHab` drops the held signers of the identifier's current keys with its stored keys. After it commits,
14
+ * this deletes the identifier's mailbox poll cursors (`Baser.tops`, keyed by subject and endpoint) and its KEL
15
+ * publisher bookkeeping (`Baser.kpub`), and prompts the mailbox poller and the KEL publisher. The poller halts
16
+ * the identifier's workers on its next tock; the publisher drops the identifier's budgets and derives nothing
17
+ * for it, since it is no longer local.
18
+ *
19
+ * Deleted, and why none of it is acceptance state: `tops.` is this consumer's own poll progress against a
20
+ * remote mailbox, not a record of anything accepted; `kpub.` records which mailboxes were told the
21
+ * identifier's KEL, the publisher's own bookkeeping (its sweep prunes rows of non-local identifiers anyway).
22
+ * Neither takes part in reply acceptance (BADA/RUN) or replay protection.
23
+ *
24
+ * Kept, deliberately (BADA/RUN): the KEL and its attachments, `states.` and key-state notices, every accepted
25
+ * reply the identifier authored or that names it (end-role and location records, reply SADs, their date-times
26
+ * and signatures or seal couples), transaction-state notices, KRAM cache rows, stored exchanges and
27
+ * credentials. Keeper registry-root rows (`rroots.`) keyed by this identifier are left too: a registry issuer
28
+ * is refused, so any such row belongs to a registry the identifier does not own.
29
+ *
30
+ * Retirement is one-way, as for `retireHab`: the retired name cannot be reused with the same salt and
31
+ * namespace, and the seeds of salt-derived keys stay derivable from the Keeper's root salt.
32
+ */
33
+ import { ValidationError } from "../../core/errors.js";
34
+ import { mailboxPollEndpoints } from "../mailbox/mailboxing.js";
35
+ /**
36
+ * Why `runtime` would refuse to retire `pre`, or null when {@link retireIdentifier} would retire it. A pure read;
37
+ * the first refusal found is returned, `retireHab`'s own first.
38
+ */
39
+ export function retireIdentifierRefusal(runtime, pre) {
40
+ const { hby } = runtime;
41
+ const store = hby.retireRefusal(pre);
42
+ if (store)
43
+ return store;
44
+ for (const [keys, record] of runtime.vdr?.reger.regs.getTopItemIter() ?? []) {
45
+ if (record.prefix === pre)
46
+ return `${pre} is the issuer of registry ${keys.join(".")}.`;
47
+ }
48
+ const acdc = runtime.acdc?.store;
49
+ for (const [keys, record] of acdc?.records?.getTopItemIter() ?? []) {
50
+ if (acdc.event(record.registryKey)?.issuer === pre)
51
+ return `${pre} is the issuer of registry ${keys.join(".")}.`;
52
+ }
53
+ const pending = runtime.delegating.pendingOf(pre);
54
+ if (pending.length)
55
+ return `${pre} has a pending delegated event: ${pending.join(", ")}.`;
56
+ const proxied = runtime.delegating.proxiedBy(pre);
57
+ if (proxied.length)
58
+ return `${pre} is the delegation proxy of ${proxied.join(", ")}.`;
59
+ const issuance = runtime.operations?.issuance.grantsOf(pre) ?? [];
60
+ if (issuance.length)
61
+ return `${pre} has live issuance operations: ${issuance.join(", ")}.`;
62
+ const unblinding = runtime.operations?.unblinding.grantsOf(pre) ?? [];
63
+ if (unblinding.length)
64
+ return `${pre} has live Unblind operations: ${unblinding.join(", ")}.`;
65
+ for (const evt of runtime.postman.evts) {
66
+ if (evt.src === pre)
67
+ return `${pre} has queued outgoing messages.`;
68
+ }
69
+ if (runtime.querying.hasRequestsSignedBy(pre))
70
+ return `${pre} signs queued queries.`;
71
+ if (runtime.querying.defaultSigner === pre)
72
+ return `${pre} is the runtime's configured query signer.`;
73
+ return null;
74
+ }
75
+ /**
76
+ * Retire `pre`: `Habery.retireHab` (which drops its held signers), then delete its mailbox cursors and KEL
77
+ * publisher rows, and prompt the mailbox poller and KEL publisher. Makes no event and signs nothing.
78
+ *
79
+ * @throws ValidationError with {@link retireIdentifierRefusal}'s reason, before any write.
80
+ */
81
+ export function retireIdentifier(runtime, pre) {
82
+ const refusal = retireIdentifierRefusal(runtime, pre);
83
+ if (refusal)
84
+ throw new ValidationError(refusal);
85
+ const { hby } = runtime;
86
+ const hab = hby.habs.get(pre);
87
+ const mailboxes = hab
88
+ ? [...new Set(mailboxPollEndpoints(hab, runtime.mailboxPoller.relay).map((endpoint) => endpoint.eid))]
89
+ : [];
90
+ const { name, released } = hby.retireHab(pre);
91
+ hby.db.transaction(() => {
92
+ hby.db.tops.trim([pre], { topive: true });
93
+ hby.db.kpub.trim([pre], { topive: true });
94
+ });
95
+ runtime.mailboxPoller.prompt(pre);
96
+ runtime.kelPublisher.prompt(pre);
97
+ return { pre, name, released, mailboxes };
98
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Read a group's completed credential disclosures back out of the runtime's stores, as CESR a fresh store can take.
3
+ *
4
+ * A host that serves a group member (for example a custodian holding a weight-0 co-member) answers a wiped
5
+ * co-member's request for the group's credentials with these two reads, inside its own request handler, the way
6
+ * KERIA serves its REST API from the existing databases. Both are plain reads over the Habery's exchange tables,
7
+ * its KEL tables and the registry store: no network I/O, no writes, no new store or index, no signing, and no
8
+ * dependence on the wallet package.
9
+ *
10
+ * KERIpy's late-joiner refill sends KELs, schemas, the registry TEL and then the credential. Here the credential
11
+ * travels inside its Grant, so a disclosure is replayed as KELs, TELs, the Grant and its Admit; workflow wrappers
12
+ * (`/multisig/*`) stay out, as in KERIpy.
13
+ */
14
+ import { collectPayload, concatBytes, Counter, framePayload, SerderACDC, Vrsn_2_0 } from "cesr-ts";
15
+ import { call } from "effection";
16
+ import { ValidationError } from "../../core/errors.js";
17
+ import { loadNests } from "../../db/exchange/exchange-nests.js";
18
+ /**
19
+ * Resolve `grant` to a completed disclosure, or null. Complete means: the Grant is an accepted v2 `/ipex/grant`
20
+ * disclosing exactly one origin that its stored nests carry; `erpy.` names its response; that response is an
21
+ * accepted v2 `/ipex/admit` whose prior is the Grant, sent by the Grant's recipient back to the Grant's sender.
22
+ * "Accepted" is the Exchanger's own record (`exns.`): it is written only once the sender's signing threshold is met,
23
+ * so a group's Admit counts when it holds enough weight overall, whatever weight the local member carries, and a
24
+ * proposal or partially signed Admit still in escrow does not count. A spurn, or no response, is not complete.
25
+ */
26
+ function completed(hby, exc, grant) {
27
+ const db = hby.db, body = db.exns.get([grant]);
28
+ if (!body || body.said !== grant || body.route !== "/ipex/grant" || body.pvrsn.major !== 2 || !exc.complete(grant)) {
29
+ return null;
30
+ }
31
+ const said = db.erpy.get([grant])?.qb64, admit = said ? db.exns.get([said]) : null;
32
+ if (!admit || admit.said !== said || admit.route !== "/ipex/admit" || admit.pvrsn.major !== 2 || !exc.complete(said)
33
+ || admit.sad.p !== grant || admit.pre !== body.sad.ri || admit.sad.ri !== body.pre)
34
+ return null;
35
+ const origins = body.sad.a?.o;
36
+ if (!Array.isArray(origins) || origins.length !== 1 || typeof origins[0] !== "string")
37
+ return null;
38
+ const origin = loadNests(db, grant).find((node) => node.body instanceof SerderACDC && node.body.said === origins[0])
39
+ ?.body;
40
+ return origin ? { grant: body, admit, origin } : null;
41
+ }
42
+ /**
43
+ * The group's completed credential disclosures, in `erpy.` key order (by Grant SAID). Scans `erpy.` (one row per
44
+ * accepted exchange that answered a prior) and reads the named Grant, Admit and Grant nests; nothing else.
45
+ *
46
+ * - `holder`: the group is the Grant's recipient and its Admit is accepted, whoever sent the Grant. This is what a
47
+ * wallet records as admitted.
48
+ * - `issuer`: the group sent the Grant and is the origin credential's issuer, and the recipient's Admit is accepted.
49
+ * A Grant in which the group re-presents a credential it holds is not listed; that credential is listed as held.
50
+ *
51
+ * Neither role needs the local member to sign or to carry weight, so a weight-0 member lists the same rows as its
52
+ * co-members once it has accepted the Admit. Listing does not reauthenticate stored proofs;
53
+ * {@link exportDisclosureStream} does. Each Grant carries its whole credential graph, so the rows are independent
54
+ * and need no order between them.
55
+ */
56
+ export function listGroupDisclosures(hby, exc, group) {
57
+ const found = [];
58
+ for (const [keys] of hby.db.erpy.getTopItemIter()) {
59
+ const disclosure = completed(hby, exc, String(keys[0]));
60
+ if (!disclosure)
61
+ continue;
62
+ const { grant, admit, origin } = disclosure;
63
+ const role = grant.sad.ri === group
64
+ ? "holder"
65
+ : grant.pre === group && origin.issuer === group
66
+ ? "issuer"
67
+ : null;
68
+ if (!role)
69
+ continue;
70
+ found.push({ grant: grant.said, admit: admit.said, credential: origin.said, registry: origin.regid, role });
71
+ }
72
+ return found;
73
+ }
74
+ /**
75
+ * One dependency-ordered CESR stream for a completed disclosure, which a receiver with an empty store can process
76
+ * front to back:
77
+ *
78
+ * 1. KELs, each after its delegation ancestry, with the stored signatures, witness receipts and first-seen replay
79
+ * attachments, of `hab`, the Grant's sender, the Admit's sender and every identifier the carried ACDC nodes name
80
+ * (issuers, issuees, source-seal prefixes);
81
+ * 2. the accepted TEL (`rip`, `bup`, with any retained public opening) of every registry those nodes name;
82
+ * 3. the Grant, as accepted, with its nests: the credential and every disclosed far node of its graph (a chained
83
+ * credential's dependencies travel inside it, before nothing that cites them);
84
+ * 4. the Admit, as accepted.
85
+ *
86
+ * Not in the stream: schemas (JSON Schema documents are not CESR; the receiver must hold or fetch them), the
87
+ * registry root and any other private registry state (a group root travels only as a `/registry/root` copy), the
88
+ * group's habitat record and its members' KELs beyond those named above, OOBI and end-role replies, KRAM cache
89
+ * rows, `/multisig/*` workflow wrappers and wallet inventory rows.
90
+ *
91
+ * Everything is re-encoded from stored, accepted evidence: `Exchanger.replay` revalidates each exchange's retained
92
+ * proofs, and nothing is signed, so `hab` needs no usable signing key; a weight-0 member, or one whose signer the
93
+ * host does not hold, exports the same stream. `hab` only adds its own KEL. A Grant that is not a completed
94
+ * disclosure (see {@link listGroupDisclosures}) is refused with a `ValidationError`. A host passes the runtime's
95
+ * own discloser, `runtime.operations.issuance.discloser`.
96
+ *
97
+ * The exchanges keep their original timestamps, so once their KRAM windows have passed, ordinary network intake
98
+ * with KRAM enabled refuses them as replays; accepting them on import is the importer's concern, not this read's.
99
+ */
100
+ export function* exportDisclosureStream(discloser, hab, grant) {
101
+ const disclosure = completed(discloser.hby, discloser.exc, grant);
102
+ if (!disclosure)
103
+ throw new ValidationError(`Grant ${grant} is not an accepted Grant with an accepted Admit.`);
104
+ const { admit } = disclosure, db = discloser.hby.db, exc = discloser.exc;
105
+ const nests = loadNests(db, grant);
106
+ const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), ...yield* call(() => Promise.all(nests.map((nest) => collectPayload(framePayload(nest))))));
107
+ const support = discloser.proofSupportFor(raw, [hab.pre, disclosure.grant.pre, admit.pre]);
108
+ const exchanges = yield* call(async () => {
109
+ const chunks = [];
110
+ for (const said of [grant, admit.said])
111
+ for await (const chunk of exc.replay(said))
112
+ chunks.push(chunk);
113
+ return chunks;
114
+ });
115
+ return concatBytes(support, ...exchanges);
116
+ }
@@ -13,33 +13,54 @@
13
13
  * a member that is briefly offline), collects the other members' stored contributions, and, once the message is
14
14
  * accepted, lets the elected lead send the completed message to its recipient. It signs nothing beyond the
15
15
  * contribution that the consent recorded.
16
+ * - A member that did not sign a message the others' signatures complete (a weight-0 member, say) takes it with no
17
+ * call: intake on receipt of a wrapper whose copies carry the group's threshold, or else `processOnce`'s periodic
18
+ * sweep once the exchange the message answers is accepted. That adds no signature, so it is not consent; the
19
+ * member then holds what the group completed and can accept the counterparty's answer in its thread.
16
20
  * - {@link groupIpexStatus} is a pure read and {@link waitForGroupIpex} only observes.
17
21
  *
18
22
  * Status is derived from the protocol stores: an accepted message reads `done`; a stored proposal without a live
19
23
  * operation reads `awaitingConsent` (a member that reloaded consents again with the same call and reuses its
20
24
  * recorded signature). Lost on reload, by design: whether the lead had already delivered the completed message,
21
- * the `failed` and `abandoned` reasons, and the publication results. The runtime never writes wallet inventory; the
25
+ * the `failed`, `expired` and `abandoned` reasons, and the publication results. The runtime never writes wallet inventory; the
22
26
  * embedder reacts to the pushed `{kind: "ipex", id: <message>}` change.
23
27
  */
24
28
  import { concatBytes, Counter, Vrsn_2_0 } from "cesr-ts";
25
- import { ValidationError } from "../../core/errors.js";
29
+ import { KramRefusedError, ValidationError } from "../../core/errors.js";
26
30
  import { loadNests } from "../../db/exchange/exchange-nests.js";
27
31
  import { registryEventStatus, settleRegistryEvent } from "../credentials/registry-settling.js";
28
32
  import { isLocalGroupHab } from "../multisig/group-members.js";
29
- import { MULTISIG_EXN_ROUTE } from "../multisig/grouping.js";
33
+ import { groupIpexFollowable, MULTISIG_EXN_ROUTE } from "../multisig/grouping.js";
30
34
  import { RetryBackoff } from "../runtime/retry-backoff.js";
31
35
  import { observeUntil } from "../runtime/workflow-contract.js";
32
36
  import { IpexAdmitter } from "./ipex-admitting.js";
33
- import { approveMultisigIpexLocally, approveStoredMultisigIpexWrappers, findStoredMultisigIpex, groupIpexApproved, prepareAcdcV2GroupIpex, publishGroupIpexProposal, publishInitialGroupIpex, requireAcdcV2GroupUse, sendCompletedGroupIpex, storedMultisigIpexBody, } from "./ipex-grouping.js";
37
+ import { attachPresentationProof } from "./ipex-discloser.js";
38
+ import { acceptGroupIpexCopies, approveMultisigIpexLocally, approveStoredMultisigIpexWrappers, findStoredMultisigIpex, groupIpexApproved, prepareAcdcV2GroupIpex, publishGroupIpexProposal, publishInitialGroupIpex, requireAcdcV2GroupUse, sendCompletedGroupIpex, storedMultisigIpexBody, unacceptedGroupIpexCopies, } from "./ipex-grouping.js";
34
39
  import { isIssuanceOrigin } from "./ipex-node-authentication.js";
35
40
  import { IPEX_GRANT_ROUTE } from "./ipexing.js";
36
41
  /** Milliseconds between collections of the other members' stored contributions. */
37
42
  export const GROUP_IPEX_COLLECT_MS = 250;
43
+ /** Milliseconds between sweeps for group messages this member holds completed but has not accepted. */
44
+ export const GROUP_IPEX_FOLLOW_MS = 5_000;
38
45
  /** How long delivery failures may last before an operation ends `failed`; a group IPEX message has no deadline. */
39
46
  export const GROUP_IPEX_RETRY_BOUND_MS = 600_000;
40
47
  function message(error) {
41
48
  return error instanceof Error ? error.message : String(error);
42
49
  }
50
+ /**
51
+ * Frame a group-authored body with its nests and sign this member's contribution, accepting it locally; returns the
52
+ * contribution's wire. Both the proposer's consent and the owner's signing after a registry update use it.
53
+ *
54
+ * @throws ValidationError while the message awaits authenticating dependencies; KramRefusedError when this member's
55
+ * own KRAM refuses it.
56
+ */
57
+ function* contribute(deps, hab, exn, nests, gvrsn) {
58
+ const candidate = concatBytes(Counter.makeGVC(gvrsn), exn.raw, Counter.enclose({ qb64: nests, version: gvrsn }));
59
+ const own = yield* prepareAcdcV2GroupIpex(deps.hby, deps.protocol, hab, candidate);
60
+ if (!own)
61
+ throw new ValidationError("ACDC v2 group disclosure awaits authenticating dependencies.");
62
+ return own;
63
+ }
43
64
  /** The live table of group IPEX operations and its driver; one per host runtime. */
44
65
  export class GroupIpexOperations {
45
66
  constructor(deps) {
@@ -68,28 +89,75 @@ export class GroupIpexOperations {
68
89
  writable: true,
69
90
  value: new Map()
70
91
  });
92
+ /** Messages this member's KRAM refused to take from their copies in this process; a sweep tries them no more. */
93
+ Object.defineProperty(this, "unfollowable", {
94
+ enumerable: true,
95
+ configurable: true,
96
+ writable: true,
97
+ value: new Set()
98
+ });
99
+ /** When the next sweep for completed group messages is due. */
100
+ Object.defineProperty(this, "nextFollow", {
101
+ enumerable: true,
102
+ configurable: true,
103
+ writable: true,
104
+ value: 0
105
+ });
71
106
  }
72
- /** Whether a pass has work. */
107
+ /** Whether a pass has work: a live operation, or a sweep for completed group messages is due. */
73
108
  active() {
74
- return this.live.size > 0;
109
+ return this.live.size > 0 || this.deps.clock.now() >= this.nextFollow;
75
110
  }
76
- /** Advance every live operation by one pass. */
111
+ /**
112
+ * Advance every live operation by one pass. Every {@link GROUP_IPEX_FOLLOW_MS} a pass also takes the group messages
113
+ * this member did not sign but holds completed ({@link follow}); that signs and sends nothing.
114
+ */
77
115
  *processOnce() {
116
+ if (this.deps.clock.now() >= this.nextFollow) {
117
+ this.nextFollow = this.deps.clock.now() + GROUP_IPEX_FOLLOW_MS;
118
+ yield* this.follow();
119
+ }
78
120
  for (const op of [...this.live.values()]) {
79
- if (op.busy || !this.live.has(op.said))
121
+ // A group whose local member cannot sign yet waits for its signer.
122
+ if (op.busy || !this.live.has(op.said) || !op.group.canSign())
80
123
  continue;
81
124
  op.busy = true;
82
125
  try {
83
126
  yield* this.step(op);
84
127
  }
85
128
  catch (error) {
86
- this.end(op, "failed", message(error));
129
+ this.end(op, error instanceof KramRefusedError ? "expired" : "failed", message(error));
87
130
  }
88
131
  finally {
89
132
  op.busy = false;
90
133
  }
91
134
  }
92
135
  }
136
+ /**
137
+ * Accept each stored group IPEX message whose received copies already carry the group's threshold
138
+ * ({@link groupIpexFollowable}), adding no signature, so this member holds what the group completed without it.
139
+ * Intake takes such a message on receipt; this sweep takes what intake could not yet, such as an answer that arrived
140
+ * before the exchange it answers. A message with a live operation is left to that operation's own collection.
141
+ */
142
+ *follow() {
143
+ const { hby, exc, protocol } = this.deps;
144
+ for (const copies of unacceptedGroupIpexCopies(hby, exc)) {
145
+ const said = copies[0].body.said;
146
+ if (this.live.has(said) || this.unfollowable.has(said) || !groupIpexFollowable(hby, copies))
147
+ continue;
148
+ try {
149
+ yield* acceptGroupIpexCopies(protocol, copies);
150
+ }
151
+ catch (error) {
152
+ if (!(error instanceof KramRefusedError))
153
+ throw error;
154
+ this.unfollowable.add(said);
155
+ continue;
156
+ }
157
+ if (exc.complete(said))
158
+ this.deps.changes?.push({ kind: "ipex", id: said });
159
+ }
160
+ }
93
161
  /** End a live operation and tell the sink. */
94
162
  end(op, state, reason) {
95
163
  this.live.delete(op.said);
@@ -140,16 +208,46 @@ export class GroupIpexOperations {
140
208
  return false;
141
209
  }
142
210
  }
211
+ /**
212
+ * The event that took the awaited update's place, read from state: a different accepted KEL event at the anchor's
213
+ * sequence number, or a different accepted TEL event at the update's. Null while the update can still be accepted;
214
+ * an update the members never approve stays awaited until the user abandons it.
215
+ */
216
+ supersededBy(anchor) {
217
+ const kel = this.deps.hby.db.kels.getLast(anchor.anchor.pre, anchor.anchor.sn);
218
+ if (kel && kel !== anchor.anchor.said)
219
+ return kel;
220
+ const tel = this.deps.rgy.store.seqEvent(anchor.regid, anchor.sn)?.said;
221
+ if (tel && tel !== anchor.said)
222
+ return tel;
223
+ return null;
224
+ }
225
+ /**
226
+ * Sign this member's contribution to a presentation Grant whose registry update the group has accepted: attach the
227
+ * proof sealed by the accepted anchor, then contribute. The consent that began the operation covers this signature.
228
+ */
229
+ *signAfterUpdate(op, awaited) {
230
+ const seal = this.deps.rgy.store.anchor(awaited.anchor.said);
231
+ if (!seal)
232
+ throw new ValidationError("The presentation update is accepted but its anchor is unavailable");
233
+ const grant = yield* attachPresentationProof(awaited.grant, awaited.opening, { s: seal[0], d: seal[1] });
234
+ return yield* contribute(this.deps, op.group, grant.exn, grant.nests, op.genus);
235
+ }
143
236
  *step(op) {
144
237
  const { hby, protocol, postman, exc, rgy, clock } = this.deps;
145
238
  if (op.prerequisite) {
239
+ const awaited = op.prerequisite;
240
+ const superseding = this.supersededBy(awaited.anchor);
241
+ if (superseding) {
242
+ this.end(op, "failed", `The registry update was superseded by ${superseding}`);
243
+ return;
244
+ }
146
245
  const read = { db: hby.db, store: rgy.store, protocol };
147
- const { anchor, proceed } = op.prerequisite;
148
- if (registryEventStatus(read, anchor).state !== "accepted") {
149
- if ((yield* settleRegistryEvent(read, anchor)).state !== "accepted")
246
+ if (registryEventStatus(read, awaited.anchor).state !== "accepted") {
247
+ if ((yield* settleRegistryEvent(read, awaited.anchor)).state !== "accepted")
150
248
  return;
151
249
  }
152
- op.outbound = { kind: "initial", wire: yield* proceed() };
250
+ op.outbound = { kind: "initial", wire: yield* this.signAfterUpdate(op, awaited) };
153
251
  op.prerequisite = null;
154
252
  this.setPhase(op, "publishing");
155
253
  }
@@ -201,27 +299,25 @@ export class GroupIpexOperations {
201
299
  * `processOnce`.
202
300
  *
203
301
  * @throws ValidationError for a body the group did not author, a non-group identifier, a message awaiting
204
- * authenticating dependencies, or one already begun.
302
+ * authenticating dependencies, or one already begun; KramRefusedError (a ValidationError) when this member's own
303
+ * KRAM refuses the message, as an Offer past its exchange lifetime.
205
304
  */
206
305
  export function* proposeGroupIpex(ops, { hab, exn, nests = new Uint8Array(), gvrsn = Vrsn_2_0, deliver = true }) {
207
- const { hby, protocol } = ops.deps;
208
- if (!isLocalGroupHab(hby, hab))
306
+ if (!isLocalGroupHab(ops.deps.hby, hab))
209
307
  throw new ValidationError("Group IPEX requires a local group identifier");
210
308
  if (exn.pre !== hab.pre || !exn.said)
211
309
  throw new ValidationError("The IPEX message must be authored by the group");
212
310
  if (ops.live.has(exn.said))
213
311
  throw new ValidationError("A group IPEX message was already begun for this SAID");
214
- const candidate = concatBytes(Counter.makeGVC(gvrsn), exn.raw, Counter.enclose({ qb64: nests, version: gvrsn }));
215
- const own = yield* prepareAcdcV2GroupIpex(hby, protocol, hab, candidate);
216
- if (!own)
217
- throw new ValidationError("ACDC v2 group disclosure awaits authenticating dependencies.");
312
+ const own = yield* contribute(ops.deps, hab, exn, nests, gvrsn);
218
313
  return ops.begin(exn.said, hab, gvrsn, { kind: "initial", wire: own }, deliver);
219
314
  }
220
315
  /**
221
- * The proposer's consent to a group-authored message that waits for a registry update, as a Grant carrying a
222
- * presentation proof does: the caller has proposed the update (this member's contribution is its approval) and
223
- * passes the anchor to wait for. The owner signs the message's contribution, through `prerequisite.proceed`, once
224
- * the group has accepted the update, under this same consent. Returns after local commit.
316
+ * The proposer's consent to a presentation Grant that waits for a registry update: the caller has proposed the
317
+ * update (this member's contribution is its approval) and passes the frozen Grant, its opening and the anchor to wait
318
+ * for. On each pass the owner reads whether the group accepted the update; once it has, the owner attaches the proof
319
+ * and signs this member's contribution under this same consent. A superseded update ends the operation `failed`; one
320
+ * the members never approve stays active until {@link abandonGroupIpex}. Returns after local commit.
225
321
  */
226
322
  export function proposeGroupIpexAfter(ops, { hab, exn, gvrsn = Vrsn_2_0, deliver = true, prerequisite }) {
227
323
  if (!isLocalGroupHab(ops.deps.hby, hab))
@@ -286,7 +382,8 @@ export function groupIpexProposals(ops, hab) {
286
382
  * completed message, which a lead that learned of the acceptance from another member's wrapper has not yet done.
287
383
  *
288
384
  * @throws ValidationError when no v2 proposal is stored for `said`, an operation is already live for it, or this
289
- * member still lacks the evidence to approve it.
385
+ * member still lacks the evidence to approve it; KramRefusedError (a ValidationError) when this member's own KRAM
386
+ * refuses the contribution.
290
387
  */
291
388
  export function* approveGroupIpex(ops, said, { deliver = true } = {}) {
292
389
  const { hby, protocol } = ops.deps;
@@ -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,10 +28,18 @@
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";
@@ -197,29 +208,76 @@ export function deriveGroupCustody(deps, hab, grant) {
197
208
  if (!isLocalGroupHab(hby, hab) || !hab.kever)
198
209
  return null;
199
210
  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
- }
211
+ const registries = disclosedRegistries(deps.exc, grant);
206
212
  if (!registries.size || [...registries].some((registry) => !hab.mgr.hasRegistryRoot(hab.pre, registry))) {
207
213
  return null;
208
214
  }
209
215
  if (receivedRootCopy(hby, hab.pre, member.pre, registries))
210
216
  return null;
211
- const events = [...hby.db.getKelItemIter(hab.pre)].slice(-GROUP_COORDINATOR_SEARCH);
217
+ const coordinator = electingCoordinator(hby, hab.pre, member.pre);
218
+ return coordinator ? { coordinator, custodian: member.pre, lead: true } : null;
219
+ }
220
+ /** The registries a retained Grant discloses credentials of. */
221
+ function disclosedRegistries(exc, grant) {
222
+ const registries = new Set();
223
+ for (const nest of exc.retainedInput(grant)?.nests ?? []) {
224
+ const rd = nest.body.ked?.rd;
225
+ if (typeof rd === "string")
226
+ registries.add(rd);
227
+ }
228
+ return registries;
229
+ }
230
+ /** The latest of the group's recent accepted events whose threshold evidence elects `member`, or null. */
231
+ function electingCoordinator(hby, group, member) {
232
+ const events = [...hby.db.getKelItemIter(group)].slice(-GROUP_COORDINATOR_SEARCH);
212
233
  for (const [, said] of events.reverse()) {
213
- const event = hby.db.getEvtSerder(hab.pre, said);
234
+ const event = hby.db.getEvtSerder(group, said);
214
235
  if (!event || event.sn === null)
215
236
  continue;
216
237
  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
- }
238
+ if (groupElectedMember(hby, group, coordinator) === member)
239
+ return coordinator;
220
240
  }
221
241
  return null;
222
242
  }
243
+ /**
244
+ * The election of a final proof the local member of `hab` can follow for `grant` without taking part: the
245
+ * `/multisig/ipex-proof` wrapper of this exact Grant that another current signing member published, with a recent
246
+ * accepted group event electing that member as custodian. The custodian publishes the proof only after the key
247
+ * exchange finished and the registry update was accepted, so once it is stored no member's signature is still wanted
248
+ * for this issuance. The sender names the custodian exactly, which a root copy cannot (any member holding a root
249
+ * sends copies). Null when no such proof is stored. A pure read of the stored exchanges.
250
+ */
251
+ export function storedGroupProof(deps, hab, grant) {
252
+ const { hby } = deps;
253
+ if (!isLocalGroupHab(hby, hab) || !hab.kever)
254
+ return null;
255
+ const member = localGroupMember(hby, hab.pre).pre, signing = groupSigningMembers(hby, hab.pre);
256
+ for (const [, parent] of hby.db.exns.getTopItemIter()) {
257
+ const attrs = parent.sad?.a;
258
+ if (parent.route !== MULTISIG_IPEX_PROOF_ROUTE || attrs?.gid !== hab.pre || attrs?.grant !== grant
259
+ || !parent.pre || parent.pre === member || !signing.includes(parent.pre))
260
+ continue;
261
+ const coordinator = electingCoordinator(hby, hab.pre, parent.pre);
262
+ if (coordinator)
263
+ return { coordinator, custodian: parent.pre, lead: false };
264
+ }
265
+ return null;
266
+ }
267
+ /**
268
+ * Whether the group completed `grant` without the local member of `hab`, so that its runtime follows the final proof
269
+ * instead of asking it to consent: a `/registry/root` copy of a disclosed registry was sent to it (copies are
270
+ * authorized only once the registry update is accepted, after the key exchange), or the custodian's final proof is
271
+ * stored ({@link storedGroupProof}). A pure read of the stores.
272
+ */
273
+ export function awaitsGroupProof(deps, hab, grant) {
274
+ const { hby } = deps;
275
+ if (!isLocalGroupHab(hby, hab) || !hab.kever)
276
+ return false;
277
+ const member = localGroupMember(hby, hab.pre).pre, registries = disclosedRegistries(deps.exc, grant);
278
+ return (registries.size > 0 && receivedRootCopy(hby, hab.pre, member, registries))
279
+ || !!storedGroupProof(deps, hab, grant);
280
+ }
223
281
  /** Whether `member` was sent a `/registry/root` copy of the group's root for any of `registries`. */
224
282
  function receivedRootCopy(hby, group, member, registries) {
225
283
  for (const [, copy] of hby.db.exns.getTopItemIter()) {
@@ -421,23 +479,51 @@ export function approveGroupIssuanceExchange(ops, said, coordinator) {
421
479
  return ops.beginGroup(new GroupIssuance(party, "holder", grant, nodes));
422
480
  }
423
481
  /**
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.
482
+ * Follow a group issuance only to verify and accept the custodian's final proof, signing nothing. It serves any
483
+ * member that does not contribute to the key exchange: an issuer member that left after contributing and returns
484
+ * for the proof, or a holder member the group completed without, such as one whose signing weight is 0. The group
485
+ * is the Grant's sender when that is a local group, otherwise its recipient. The proof may already be stored (it
486
+ * arrives by mailbox), or be supplied as bytes.
487
+ *
488
+ * The owner begins the same follow on its own once the custodian's proof is stored ({@link storedGroupProof}), so a
489
+ * host needs this call only to supply the proof as bytes or to pin the coordinator. It is idempotent with that
490
+ * follow; see {@link beginGroupProofFollow}.
428
491
  *
429
- * @throws ValidationError for a Grant this runtime's groups did not send, or one already begun.
492
+ * @throws ValidationError for a Grant that is not pending for a local group, or one begun under a consent.
430
493
  */
431
494
  export function* followGroupIssuanceProof(ops, said, options = {}) {
432
- const { deps } = ops;
433
- if (ops.liveOf(said, "issuer") || partyComplete(deps, said, "issuer")) {
495
+ const { deps } = ops, sent = deps.exc.retainedInput(said)?.serder.pre;
496
+ const sender = sent ? deps.hby.habs.get(sent) : undefined;
497
+ const role = sender && isLocalGroupHab(deps.hby, sender) ? "issuer" : "holder";
498
+ const begun = beginGroupProofFollow(ops, said, role, options.coordinator);
499
+ // Stored for the follow's next scan, whether it began now or earlier.
500
+ if (options.proof)
501
+ yield* deps.reactor.processBytes(options.proof);
502
+ return begun;
503
+ }
504
+ /**
505
+ * Begin `role`'s proof-only follow of the pending group Grant `said` under `coordinator` (default the group's latest
506
+ * establishment event). Signs nothing. Idempotent: when that party's follow is already live, or its side of the Grant
507
+ * is already complete, returns the Grant and its transaction and begins nothing. The owner's sweep and
508
+ * {@link followGroupIssuanceProof} both begin follows here.
509
+ *
510
+ * @throws ValidationError for a Grant that is not pending for a local group, one whose live operation is a consent,
511
+ * or a coordinator {@link GroupParty.resolve} refuses.
512
+ */
513
+ export function beginGroupProofFollow(ops, said, role, coordinator) {
514
+ const { deps } = ops, live = ops.liveOf(said, role);
515
+ if (live?.group?.mode === "proofOnly")
516
+ return { grant: said, xid: live.xid };
517
+ if (live)
434
518
  throw new ValidationError("Issuance was already begun for this Grant");
519
+ if (partyComplete(deps, said, role)) {
520
+ const serder = deps.exc.retainedInput(said)?.serder;
521
+ return { grant: said, xid: serder ? grantTransactionId(serder) : said };
435
522
  }
436
523
  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" }));
524
+ const group = localGroup(deps, role === "issuer" ? grant.pre : grant.sad.ri);
525
+ const party = GroupParty.resolve(deps, group, coordinator);
526
+ return ops.beginGroup(new GroupIssuance(party, role, grant, nodes, { mode: "proofOnly" }));
441
527
  }
442
528
  /**
443
529
  * The custodian's exact public proof of a completed group issuance: the proof support and the