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
@@ -6,7 +6,6 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
6
6
  var _MailboxPoller_dirty;
7
7
  /** Recipient mailbox polling: signed queries, awaited SSE intake and persisted cursor progression. */
8
8
  import { call, scoped, spawn } from "effection";
9
- import { SignerNotHeldError } from "../identity/keeping.js";
10
9
  import { abortable } from "../runtime/abortable.js";
11
10
  import { doEvery, PROTOCOL_TOCK_MS } from "../runtime/doing.js";
12
11
  import { runtimeTurn } from "../runtime/runtime-turn.js";
@@ -15,6 +14,7 @@ import { isLegacyCesrMediaRejection, readResponseDetail } from "../transport/ces
15
14
  import { buildCesrRequest, CESR_CONTENT_TYPE, KERIPY_CESR_JSON_CONTENT_TYPE, } from "../transport/cesr-http.js";
16
15
  import { requestWithHeaderTimeout } from "../transport/httping.js";
17
16
  import { TransportError } from "../transport/transport-error.js";
17
+ import { keepsForPickup } from "./agent-inbox.js";
18
18
  import { consumeMailboxSse } from "./mailbox-sse.js";
19
19
  import { mailboxPollEndpoints } from "./mailboxing.js";
20
20
  /** Tocks between the poller's full re-selection of workers: 160 tocks of 31.25 ms is five seconds. */
@@ -35,7 +35,8 @@ export const MAILBOX_POLLER_PACING = Object.freeze({
35
35
  *
36
36
  * Each endpoint has an independent worker and durable progress in the director.
37
37
  * Retrieved records are lent to the awaited consumer; protocol intake stays outside.
38
- * A provider store in the same runtime is never scanned for local recipient input.
38
+ * A provider store in the same runtime is never scanned for local recipient input, and this runtime never polls its
39
+ * own endpoint for a local group whose mail it processes on arrival (see {@link MailboxPoller.deliveredOnArrival}).
39
40
  */
40
41
  export class MailboxPoller {
41
42
  /**
@@ -45,9 +46,10 @@ export class MailboxPoller {
45
46
  * @param mailboxDirector Borrowed provider/topic/cursor owner.
46
47
  * @param http Borrowed HTTP implementation selected by composition.
47
48
  * @param clock Borrowed clock with epoch-millisecond now and millisecond timer delays.
48
- * @param options Optional timeout overrides; finite positive values are floored with a 1 ms minimum. Missing/nonpositive/nonfinite values use 5-second headers and 30-second poll windows.
49
+ * @param relay The agent every subject is polled through; null selects authorized mailboxes and witnesses.
50
+ * @param options Optional timeout overrides and poll-report deck; finite positive timeouts are floored with a 1 ms minimum. Missing/nonpositive/nonfinite values use 5-second headers and 30-second poll windows.
49
51
  */
50
- constructor(hby, mailboxDirector, http, clock, options = {}) {
52
+ constructor(hby, mailboxDirector, http, clock, relay, options = {}) {
51
53
  /** Borrowed habitat collection used to select mailbox signers. */
52
54
  Object.defineProperty(this, "hby", {
53
55
  enumerable: true,
@@ -83,22 +85,15 @@ export class MailboxPoller {
83
85
  writable: true,
84
86
  value: void 0
85
87
  });
86
- /** Told after each poll a mailbox served. */
87
- Object.defineProperty(this, "onPolled", {
88
+ /** Where served polls and 403s are pushed, for the KEL publisher; null when nothing consumes them. */
89
+ Object.defineProperty(this, "polls", {
88
90
  enumerable: true,
89
91
  configurable: true,
90
92
  writable: true,
91
93
  value: void 0
92
94
  });
93
- /** Where unsignable polls are reported, when the host keeps such a record. */
94
- Object.defineProperty(this, "signerConditions", {
95
- enumerable: true,
96
- configurable: true,
97
- writable: true,
98
- value: void 0
99
- });
100
- /** Told when a mailbox answers 403. */
101
- Object.defineProperty(this, "onUnauthorized", {
95
+ /** The agent this runtime polls through (an edge), its only provider; null polls mailboxes and witnesses. */
96
+ Object.defineProperty(this, "relay", {
102
97
  enumerable: true,
103
98
  configurable: true,
104
99
  writable: true,
@@ -106,15 +101,14 @@ export class MailboxPoller {
106
101
  });
107
102
  /** Identifiers whose mailbox endpoints changed; the poller re-selects their workers on the next tock. */
108
103
  _MailboxPoller_dirty.set(this, new Set());
109
- const { timeouts, onPolled, onUnauthorized, signerConditions } = options;
110
- this.signerConditions = signerConditions;
111
- this.onPolled = onPolled;
112
- this.onUnauthorized = onUnauthorized;
104
+ const { timeouts, polls } = options;
105
+ this.polls = polls ?? null;
113
106
  this.hby = hby;
114
107
  this.mailboxDirector = mailboxDirector;
115
108
  this.timeoutPolicy = normalizeMailboxPollingTimeoutPolicy(timeouts);
116
109
  this.http = http;
117
110
  this.clock = clock;
111
+ this.relay = relay;
118
112
  }
119
113
  /**
120
114
  * Register one mailbox topic that this poller should retrieve.
@@ -148,12 +142,12 @@ export class MailboxPoller {
148
142
  shape = topics.join("|");
149
143
  habs = self.hby.habs.size;
150
144
  __classPrivateFieldGet(self, _MailboxPoller_dirty, "f").clear();
151
- yield* self.syncRemoteWorkers(remoteWorkers, onBatch, topics);
145
+ yield* self.syncRemoteWorkers(remoteWorkers, onBatch, topics, pacing.tockMs);
152
146
  }
153
147
  else if (__classPrivateFieldGet(self, _MailboxPoller_dirty, "f").size) {
154
148
  const only = new Set(__classPrivateFieldGet(self, _MailboxPoller_dirty, "f"));
155
149
  __classPrivateFieldGet(self, _MailboxPoller_dirty, "f").clear();
156
- yield* self.syncRemoteWorkers(remoteWorkers, onBatch, topics, only);
150
+ yield* self.syncRemoteWorkers(remoteWorkers, onBatch, topics, pacing.tockMs, only);
157
151
  }
158
152
  });
159
153
  }
@@ -176,26 +170,20 @@ export class MailboxPoller {
176
170
  * @param target Mailbox subject habitat plus the actual local signer (a member for groups).
177
171
  * @param endpoint Selected remote endpoint AID and HTTP(S) URL.
178
172
  * @param onBatch Awaited consumer; successful completion precedes durable cursor advancement.
179
- * @returns How the poll ended; see {@link PollOutcome}. An `unsignable` poll carries the missing public key.
173
+ * @returns How the poll ended; see {@link PollOutcome}.
180
174
  */
181
175
  *pollRemoteEndpointOnce(target, endpoint, onBatch) {
182
176
  const cursor = this.mailboxDirector.remoteQueryCursor(target.targetPre, endpoint.eid);
183
177
  // deno-lint-ignore no-this-alias -- Generator callbacks have no lexical this.
184
178
  const self = this;
185
- // The concrete HTTP/SSE operation owns and joins this whole poll attempt. A signer the host released
186
- // (while signing the query, or in the consumer's protocol work) ends the poll as `unsignable`.
187
- try {
188
- const outcome = yield* call(() => fetchMailboxMessages(target.signerHab, target.targetPre, endpoint.eid, endpoint.url, cursor, mailboxFetchTimeoutPolicy(this.timeoutPolicy), this.http, this.clock, function* deliverThenAdvanceCursor(message) {
189
- yield* call(() => onBatch({ pre: target.targetPre, eid: endpoint.eid, messages: [message.msg] }));
190
- self.mailboxDirector.updateRemoteCursor(target.targetPre, endpoint.eid, message.topic, message.idx);
191
- }));
192
- return { outcome };
193
- }
194
- catch (error) {
195
- if (error instanceof SignerNotHeldError)
196
- return { outcome: "unsignable", missing: error.pub };
197
- throw error;
198
- }
179
+ // The concrete HTTP/SSE operation owns and joins this whole poll attempt.
180
+ return yield* call(() => fetchMailboxMessages(target.signerHab, target.targetPre, endpoint.eid, endpoint.url, cursor, mailboxFetchTimeoutPolicy(this.timeoutPolicy), this.http, this.clock, function* deliverThenAdvanceCursor(message) {
181
+ yield* call(() => onBatch({ pre: target.targetPre, eid: endpoint.eid, messages: [message.msg] }));
182
+ // A subject retired during this poll keeps no cursor: its worker halts on the next tock.
183
+ if (!self.hby.habs.has(target.targetPre))
184
+ return;
185
+ self.mailboxDirector.updateRemoteCursor(target.targetPre, endpoint.eid, message.topic, message.idx);
186
+ }));
199
187
  }
200
188
  /**
201
189
  * Keep one long-lived worker running for each currently configured remote endpoint.
@@ -203,29 +191,32 @@ export class MailboxPoller {
203
191
  * @param remoteWorkers Owned worker map, mutated to match current endpoint selection.
204
192
  * @param onBatch Shared awaited consumer used independently by each endpoint worker.
205
193
  * @param topics Current registered topics; no topics means no remote workers.
194
+ * @param tockMs How long a worker whose signer cannot sign yet waits before it looks again.
206
195
  */
207
- *syncRemoteWorkers(remoteWorkers, onBatch, topics, only) {
208
- const active = new Set();
196
+ *syncRemoteWorkers(remoteWorkers, onBatch, topics, tockMs, only) {
197
+ const wanted = new Map();
209
198
  if (topics.length > 0) {
210
199
  for (const target of this.remotePollTargets(only)) {
211
- for (const endpoint of mailboxPollEndpoints(target.hab)) {
212
- const workerKey = `${target.targetPre}:${endpoint.key}`;
213
- active.add(workerKey);
214
- if (remoteWorkers.has(workerKey)) {
200
+ for (const endpoint of mailboxPollEndpoints(target.hab, this.relay)) {
201
+ if (this.deliveredOnArrival(target.targetPre, endpoint.eid))
215
202
  continue;
216
- }
217
- const task = yield* spawn(() => this.remoteEndpointWorker(target, endpoint, onBatch));
218
- remoteWorkers.set(workerKey, task);
203
+ // The signer is part of the identity: a group whose local member was replaced gets a new worker.
204
+ wanted.set(`${target.targetPre}:${target.signerHab.pre}:${endpoint.key}`, { target, endpoint });
219
205
  }
220
206
  }
221
207
  }
222
208
  for (const [workerKey, task] of [...remoteWorkers.entries()]) {
223
- if (active.has(workerKey) || (only && !only.has(workerKey.slice(0, workerKey.indexOf(":"))))) {
209
+ if (wanted.has(workerKey) || (only && !only.has(workerKey.slice(0, workerKey.indexOf(":"))))) {
224
210
  continue;
225
211
  }
226
212
  yield* task.halt();
227
213
  remoteWorkers.delete(workerKey);
228
214
  }
215
+ for (const [workerKey, { target, endpoint }] of wanted) {
216
+ if (remoteWorkers.has(workerKey))
217
+ continue;
218
+ remoteWorkers.set(workerKey, yield* spawn(() => this.remoteEndpointWorker(target, endpoint, onBatch, tockMs)));
219
+ }
229
220
  }
230
221
  /**
231
222
  * Poll one remote endpoint continuously in the long-lived runtime shape.
@@ -234,21 +225,26 @@ export class MailboxPoller {
234
225
  * `keri-ts`'s split between mailbox transport and runtime parser ownership. Each poll ends in a
235
226
  * {@link PollOutcome}; {@link nextPoll} maps it and the backoff state to the next action. A mailbox that is
236
227
  * unreachable or refuses is retried on {@link MailboxPoller.RefusalBackoff}, never in a tight loop and never
237
- * by failing the runtime. A 403 also prompts the KEL publisher, which owns posting the KEL.
228
+ * by failing the runtime. A served poll and a 403 are pushed onto `polls` for the KEL publisher, which owns posting
229
+ * the KEL.
230
+ *
231
+ * A signer that cannot sign yet (a store opened without it) is not a remote failure: the worker neither polls
232
+ * nor backs off, and looks again each tock, so it polls within one tock of `Manager.hold`. Because it never polls
233
+ * for such a subject, the consumer's protocol work never runs for it either.
238
234
  *
239
235
  * @param target Mailbox subject habitat plus the actual local signer (a member for groups).
240
236
  * @param endpoint Selected remote endpoint AID and HTTP(S) URL.
241
237
  * @param onBatch Awaited consumer; successful completion precedes durable cursor advancement.
238
+ * @param tockMs How long the worker waits before it looks again at a signer that cannot sign.
242
239
  */
243
- *remoteEndpointWorker(target, endpoint, onBatch) {
240
+ *remoteEndpointWorker(target, endpoint, onBatch, tockMs) {
244
241
  let state = { backoffMs: 0 };
245
242
  while (true) {
246
- const { outcome, missing } = yield* this.pollRemoteEndpointOnce(target, endpoint, onBatch);
247
- const source = `mailbox-poller:${target.targetPre}/${endpoint.eid}`;
248
- if (missing)
249
- this.signerConditions?.report(source, missing, this.clock.now());
250
- else
251
- this.signerConditions?.clear(source);
243
+ if (!target.signerHab.canSign()) {
244
+ yield* waitOnClock(this.clock, tockMs);
245
+ continue;
246
+ }
247
+ const outcome = yield* this.pollRemoteEndpointOnce(target, endpoint, onBatch);
252
248
  const step = nextPoll(state, outcome, {
253
249
  initialMs: this.timeoutPolicy.refusalInitialMs,
254
250
  maxMs: this.timeoutPolicy.refusalMaxMs,
@@ -256,23 +252,36 @@ export class MailboxPoller {
256
252
  state = step.state;
257
253
  switch (step.action.kind) {
258
254
  case "poll":
259
- this.onPolled?.(target.targetPre, endpoint.eid);
255
+ this.polls?.push({ kin: "polled", pre: target.targetPre, mailbox: endpoint.eid });
260
256
  yield* runtimeTurn();
261
257
  break;
262
258
  case "prompt-publisher":
263
- this.onUnauthorized?.(target.targetPre, endpoint.eid);
259
+ this.polls?.push({ kin: "unauthorized", pre: target.targetPre, mailbox: endpoint.eid });
264
260
  yield* waitOnClock(this.clock, step.action.ms);
265
261
  break;
266
262
  case "wait":
267
- // A released signer is waited out until one is held again, not for the full backoff.
268
- if (missing && this.signerConditions)
269
- yield* this.signerConditions.waitForHold(this.clock, step.action.ms);
270
- else
271
- yield* waitOnClock(this.clock, step.action.ms);
263
+ yield* waitOnClock(this.clock, step.action.ms);
272
264
  break;
273
265
  }
274
266
  }
275
267
  }
268
+ /**
269
+ * Whether `subject`'s mail at `eid` already reached local processing when it arrived, so polling `eid` for it would
270
+ * process every stored message a second time. That holds when this runtime is the provider (it owns the mailbox
271
+ * store), `eid` is one of its own habitats and it keeps `subject`'s mail for pickup ({@link keepsForPickup}: for a
272
+ * local subject, a group that authorized `eid` as mailbox or agent). Such a host ingests every `/fwd` for the group
273
+ * on arrival (`ForwardHandler`) and dispatches every raw arrival before keeping it; the copy it stores is for the
274
+ * group's remote members, who poll it from here. Any other local subject at a local endpoint (a single-sig habitat
275
+ * whose mailbox is this host) is still polled over loopback HTTP, which is its only local delivery of stored mail.
276
+ * Without a mailbox store this runtime is not the provider, so it polls as any remote client would.
277
+ *
278
+ * @param subject Local mailbox subject AID.
279
+ * @param eid Selected provider AID.
280
+ * @returns True when the endpoint must not be polled for this subject.
281
+ */
282
+ deliveredOnArrival(subject, eid) {
283
+ return !!this.mailboxDirector.mailboxer && this.hby.habs.has(eid) && keepsForPickup(this.hby, subject, eid);
284
+ }
276
285
  /**
277
286
  * Select locally pollable subjects without substituting the group for its member signer.
278
287
  *
@@ -374,7 +383,6 @@ export function nextPoll(state, outcome, backoff = MailboxPoller.RefusalBackoff)
374
383
  }
375
384
  case "no-response":
376
385
  case "unreachable":
377
- case "unsignable":
378
386
  case "refused":
379
387
  return backOff(state, backoff);
380
388
  }
@@ -103,15 +103,31 @@ export function authorizedMailboxes(db, pre) {
103
103
  * Both sets are polled, as in KERIpy MailboxDirector. A provider in both roles
104
104
  * shares one history and cursor, so it gets one worker. Locally controlled AIDs
105
105
  * remain eligible for HTTP polling; provider storage is never scanned directly.
106
+ * This lists every provider; the poller itself drops a local provider that
107
+ * processes the subject's mail on arrival (`MailboxPoller.deliveredOnArrival`).
106
108
  * Missing HTTP(S) locations produce no poll target until a location is available.
107
109
  *
110
+ * With a relay (an edge runtime) the relay is the only provider: witnesses and other mailboxes are not polled, and
111
+ * the relay is polled once the subject has authorized it as mailbox (for a group, the group's own row, since the
112
+ * relay stores the group's messages under that authorization). Until then the subject has no poll target; the
113
+ * runtime prompts the poller when the role is accepted.
114
+ *
108
115
  * @param hab Mailbox subject habitat supplying accepted roles and current witnesses.
116
+ * @param relay The agent this runtime polls through; null polls authorized mailboxes and witnesses.
109
117
  * @returns One HTTPS-preferred HTTP target per provider AID, with stable worker keys.
110
118
  */
111
- export function mailboxPollEndpoints(hab) {
112
- const eids = new Set(authorizedMailboxes(hab.db, hab.pre));
113
- for (const eid of hab.kever?.wits ?? [])
114
- eids.add(eid);
119
+ export function mailboxPollEndpoints(hab, relay) {
120
+ const eids = new Set();
121
+ if (relay) {
122
+ if (authorizedMailboxes(hab.db, hab.pre).includes(relay.aid))
123
+ eids.add(relay.aid);
124
+ }
125
+ else {
126
+ for (const eid of authorizedMailboxes(hab.db, hab.pre))
127
+ eids.add(eid);
128
+ for (const eid of hab.kever?.wits ?? [])
129
+ eids.add(eid);
130
+ }
115
131
  const endpoints = [];
116
132
  for (const eid of eids) {
117
133
  const urls = hab.fetchUrls(eid);
@@ -80,10 +80,14 @@ export class Counselor {
80
80
  * @param clock Time source for fallback and retry; only `now` is used.
81
81
  * @param changes Optional sink told `{kind:"group", id:"<pre>:<sn>"}` after commits.
82
82
  * @param delegation Anchorer and query port for delegated events; omit for non-delegated hosts.
83
- * @param mailboxes Told when one of the group's rotations completes; the member runtime's publisher then tells
84
- * the group's mailboxes the new key state, and several members doing so is harmless.
83
+ * @param rotated Deck that gets a record of each completed group rotation (`rot` or `drt`); the member runtime
84
+ * passes its KEL publisher's, which then tells the group's mailboxes the new key state. Several members doing
85
+ * so is harmless.
86
+ * @param groupChanges Deck that gets the prefix of a group whose establishment event completed or whose stored
87
+ * members were aligned late; the member runtime shares it with its root replicator, which re-derives the
88
+ * copies the group owes on its next pass instead of at its next sweep.
85
89
  */
86
- constructor(hby, receipting, clock, changes, delegation, mailboxes, receiptRetry = DEFAULT_RECEIPT_RETRY, forwarding) {
90
+ constructor(hby, receipting, clock, changes, delegation, rotated, groupChanges, receiptRetry = DEFAULT_RECEIPT_RETRY, forwarding) {
87
91
  Object.defineProperty(this, "hby", {
88
92
  enumerable: true,
89
93
  configurable: true,
@@ -114,11 +118,17 @@ export class Counselor {
114
118
  writable: true,
115
119
  value: delegation
116
120
  });
117
- Object.defineProperty(this, "mailboxes", {
121
+ Object.defineProperty(this, "rotated", {
118
122
  enumerable: true,
119
123
  configurable: true,
120
124
  writable: true,
121
- value: mailboxes
125
+ value: rotated
126
+ });
127
+ Object.defineProperty(this, "groupChanges", {
128
+ enumerable: true,
129
+ configurable: true,
130
+ writable: true,
131
+ value: groupChanges
122
132
  });
123
133
  Object.defineProperty(this, "receiptRetry", {
124
134
  enumerable: true,
@@ -151,6 +161,13 @@ export class Counselor {
151
161
  writable: true,
152
162
  value: new Map()
153
163
  });
164
+ /** Per local group, the latest establishment SAID its stored members were found to follow; memory only. */
165
+ Object.defineProperty(this, "aligned", {
166
+ enumerable: true,
167
+ configurable: true,
168
+ writable: true,
169
+ value: new Map()
170
+ });
154
171
  Object.defineProperty(this, "anchorQuery", {
155
172
  enumerable: true,
156
173
  configurable: true,
@@ -286,8 +303,12 @@ export class Counselor {
286
303
  get holdsForwards() {
287
304
  return this.forwards.size > 0;
288
305
  }
289
- /** One pass over all stages, as KERIpy `processEscrows`. */
306
+ /**
307
+ * One pass over all stages, as KERIpy `processEscrows`, after aligning stored group members with accepted state
308
+ * so that a completion in this pass forwards to the members of the event that completed.
309
+ */
290
310
  *processOnce() {
311
+ this.alignMembers();
291
312
  // A runtime calls this every turn; with nothing counseled, three row counts are the whole cost.
292
313
  const db = this.hby.db;
293
314
  if (db.gpse.cnt() === 0 && db.gdee.cnt() === 0 && db.gpwe.cnt() === 0 && this.forwards.size === 0)
@@ -307,23 +328,12 @@ export class Counselor {
307
328
  return;
308
329
  for (const [key, owed] of [...this.forwards]) {
309
330
  const now = this.clock.now();
310
- if (now < owed.nextAt)
331
+ // The local member signs the forward; a group whose member cannot sign yet keeps it without spending an attempt.
332
+ if (now < owed.nextAt || !this.hby.habs.get(owed.event.pre)?.canSign())
311
333
  continue;
312
- let outcomes;
313
- try {
314
- outcomes = yield* this.forwarding.forward(owed.event, owed.members);
315
- }
316
- catch (error) {
317
- const reason = error instanceof Error ? error.message : String(error);
318
- outcomes = owed.members.map((recipient) => ({
319
- recipient,
320
- exnSaid: owed.event.said,
321
- payloadSaid: owed.event.said,
322
- outcome: "failed",
323
- deliveries: [],
324
- reason,
325
- }));
326
- }
334
+ // A member that could not be reached is its `failed` outcome; what `forward` throws is a fault (the accepted
335
+ // event or the local member missing from the store) and ends the loop instead of spending an attempt.
336
+ const outcomes = yield* this.forwarding.forward(owed.event, owed.members);
327
337
  const failed = outcomes.filter((outcome) => outcome.outcome === "failed");
328
338
  this.forwarded.set(key, [
329
339
  ...(this.forwarded.get(key)?.filter((prior) => !outcomes.some((o) => o.recipient === prior.recipient)) ?? []),
@@ -410,6 +420,34 @@ export class Counselor {
410
420
  });
411
421
  this.changes?.push({ kind: "group", id: `${event.pre}:${event.sn}` });
412
422
  }
423
+ /**
424
+ * Bring each local group's stored members in line with its latest accepted establishment event, whoever proposed
425
+ * it and however late its last signature arrived (`Habery.alignGroupMembers`). A group is re-read only when that
426
+ * event differs from the one its members were last found to follow; one that cannot be attributed yet (a member's
427
+ * KEL is missing) is left as it is and read again on a later pass, which the arriving KEL's write brings about.
428
+ *
429
+ * After a change, an elected member whose latest establishment event already completed owes it again to the
430
+ * members it now has, and the group goes on `groupChanges` so the root replicator re-derives its copies.
431
+ */
432
+ alignMembers() {
433
+ const db = this.hby.db;
434
+ for (const pre of [...db.groups]) {
435
+ const est = db.getKever(pre)?.lastEst;
436
+ if (!est || this.aligned.get(pre) === est.d)
437
+ continue;
438
+ const alignment = this.hby.alignGroupMembers(pre);
439
+ if (alignment === "unattributed")
440
+ continue;
441
+ this.aligned.set(pre, est.d);
442
+ if (alignment === "unchanged")
443
+ continue;
444
+ this.changes?.push({ kind: "group", id: `${pre}:${est.s}` });
445
+ this.groupChanges?.push(pre);
446
+ const event = { pre, sn: est.s, said: est.d };
447
+ if (this.completed(pre, est.s) === est.d)
448
+ this.owe(event);
449
+ }
450
+ }
413
451
  /** The elected member owes the completed event to the members that did not sign it into existence. */
414
452
  owe(event) {
415
453
  if (!this.forwarding || !groupWitnesser(this.hby, event.pre, event))
@@ -418,7 +456,10 @@ export class Counselor {
418
456
  if (members.length)
419
457
  this.forwards.set(this.runKey(event), { event, members, attempts: 0, nextAt: 0 });
420
458
  }
421
- /** Record completion of a counseled event and drop its row from `table`. */
459
+ /**
460
+ * Record completion of a counseled event and drop its row from `table`. A completed establishment event goes on
461
+ * `groupChanges`, and a completed rotation on `rotated` as well.
462
+ */
422
463
  finish(table, event, row) {
423
464
  const db = this.hby.db;
424
465
  db.transaction(() => {
@@ -428,9 +469,11 @@ export class Counselor {
428
469
  this.runs.delete(this.runKey(event));
429
470
  this.changes?.push({ kind: "group", id: `${event.pre}:${event.sn}` });
430
471
  this.owe(event);
431
- const ilk = db.getEvtSerder(event.pre, event.said)?.ilk;
432
- if (ilk === Ilks.rot || ilk === Ilks.drt) {
433
- this.mailboxes?.rotated({ kin: "rotated", pre: event.pre, sn: event.sn, said: event.said });
472
+ const serder = db.getEvtSerder(event.pre, event.said);
473
+ if (serder?.estive)
474
+ this.groupChanges?.push(event.pre);
475
+ if (serder?.ilk === Ilks.rot || serder?.ilk === Ilks.drt) {
476
+ this.rotated?.push({ kin: "rotated", pre: event.pre, sn: event.sn, said: event.said });
434
477
  }
435
478
  }
436
479
  /** Complete events whose own witness threshold is met; otherwise submit or fetch receipts. */
@@ -460,8 +503,9 @@ export class Counselor {
460
503
  return;
461
504
  const submit = forceSubmit || groupWitnesser(this.hby, event.pre, event) || now - run.since >= run.fallbackAfterMs;
462
505
  try {
506
+ // Group receipting reaches TCP-only witnesses, as `WitnessReceiptor` and KERIpy's Counselor do.
463
507
  if (submit)
464
- yield* this.receipting.receipt(event.pre, { sn: event.sn, auths: run.auths });
508
+ yield* this.receipting.receipt(event.pre, { sn: event.sn, auths: run.auths, allowTcp: true });
465
509
  else
466
510
  yield* this.receipting.get(event.pre, { sn: event.sn, said: event.said });
467
511
  run.failure = null;
@@ -1,17 +1,25 @@
1
1
  import { TransIdxSigGroup } from "../../core/dispatch.js";
2
- import { ValidationError } from "../../core/errors.js";
2
+ import { MissingDurableStateError, ValidationError } from "../../core/errors.js";
3
3
  import { Kever } from "../../core/kel/kever.js";
4
4
  import { dgKey } from "../../db/core/keys.js";
5
5
  /** True when the selected habitat is a local group habitat. */
6
6
  export function isLocalGroupHab(hby, hab) {
7
7
  return !!hab.pre && !!hby.db.getHab(hab.pre)?.mid;
8
8
  }
9
- /** Return the local member habitat for a persisted group identifier. */
9
+ /**
10
+ * Return the local member habitat for a persisted group identifier.
11
+ *
12
+ * @throws ValidationError when `groupPre` is not a local group (no habitat record names a local member).
13
+ * @throws MissingDurableStateError when the group's record names a local member the store does not hold: the
14
+ * store is damaged, which no remote message can cause (retirement refuses a local group's member).
15
+ */
10
16
  export function localGroupMember(hby, groupPre) {
11
- const record = hby.db.getHab(groupPre);
12
- const member = record?.mid ? hby.habs.get(record.mid) : null;
17
+ const mid = hby.db.getHab(groupPre)?.mid;
18
+ if (!mid)
19
+ throw new ValidationError(`${groupPre} is not a local group.`);
20
+ const member = hby.habs.get(mid);
13
21
  if (!member) {
14
- throw new ValidationError(`Group ${groupPre} is missing local member metadata.`);
22
+ throw new MissingDurableStateError(`Group ${groupPre} names local member ${mid}, which this store does not hold.`);
15
23
  }
16
24
  return member;
17
25
  }
@@ -172,6 +180,34 @@ export function localIpexContribution(db, member, embedded, proofs) {
172
180
  }
173
181
  return null;
174
182
  }
183
+ /**
184
+ * Whether the group signatures a member received on the group-authored message `embedded`, from every copy it holds,
185
+ * together satisfy the threshold of the accepted group establishment event they name. Only signatures that verify
186
+ * against that event count, and copies naming different events are not pooled, so a message below the threshold, or
187
+ * one signed under keys this store has not accepted, reads false.
188
+ *
189
+ * This is the rule under which a member takes a group message it did not sign: accepting what already carries the
190
+ * group's threshold adds no signature, so it is not consent.
191
+ */
192
+ export function groupThresholdSigned(db, embedded, proofs) {
193
+ const signed = new Map();
194
+ for (const proof of proofs) {
195
+ if (!embedded.pre || proof.pre !== embedded.pre || proof.sn > BigInt(Number.MAX_SAFE_INTEGER))
196
+ continue;
197
+ const said = db.kels.getLast(proof.pre, Number(proof.sn));
198
+ const event = said && said === proof.said ? db.getEvtSerder(proof.pre, said) : null;
199
+ if (!said || !event?.estive || !event.tholder)
200
+ continue;
201
+ const indices = signed.get(said) ?? new Set();
202
+ for (const index of Kever.verifyIndexedSignatures(embedded.raw, proof.sigers, event.verfers).indices) {
203
+ indices.add(index);
204
+ }
205
+ signed.set(said, indices);
206
+ if (event.tholder.satisfy([...indices]))
207
+ return true;
208
+ }
209
+ return false;
210
+ }
175
211
  /**
176
212
  * Derive the members of `group`'s latest accepted establishment event from
177
213
  * local KELs, as KERIpy `GroupHab.acceptedMembers`.
@@ -14,11 +14,14 @@ import { exchange, isIpexVerb, isV2RegistryRoute, multisigChildField, nestedIpex
14
14
  import { Kevery } from "../../core/kel/eventing.js";
15
15
  import { Kever } from "../../core/kel/kever.js";
16
16
  import { BasicReplyRouteHandler, Revery, Router } from "../../core/routing.js";
17
+ import { loadNests } from "../../db/exchange/exchange-nests.js";
17
18
  import { Exchanger } from "../exchanges/exchanging.js";
19
+ import { isIssuanceOrigin } from "../ipex/ipex-node-authentication.js";
18
20
  import { IpexV2Handler } from "../ipex/ipex-v2.js";
21
+ import { IPEX_GRANT_ROUTE } from "../ipex/ipexing.js";
19
22
  import { dispatchEnvelope, envelopeFromFrame } from "../protocol/parsering.js";
20
23
  import { abortable, rethrowCleanupFailure } from "../runtime/abortable.js";
21
- import { groupSigningMembers, isLocalGroupHab, localIpexContribution } from "./group-members.js";
24
+ import { groupSigningMembers, groupThresholdSigned, isLocalGroupHab, localIpexContribution } from "./group-members.js";
22
25
  import { multisigEmbeddedSection, multisigPayloadSaid } from "./multisig-messaging.js";
23
26
  /** KERIpy multisig route constants. */
24
27
  export const MULTISIG_ICP_ROUTE = "/multisig/icp";
@@ -529,7 +532,10 @@ export class Multiplexor {
529
532
  && handler instanceof IpexV2Handler && handler.hby === this.hby
530
533
  && handler.registry === this.registry.store;
531
534
  }
532
- /** Index an authenticated proposal; child acceptance still requires local approval and its own proofs. */
535
+ /**
536
+ * Index an authenticated proposal. Its child is accepted, with its own proofs, after local approval, or without one
537
+ * when the signatures received for it already complete it (see {@link alreadyComplete} and {@link groupIpexFollowable}).
538
+ */
533
539
  *add(serder, nests = []) {
534
540
  const inspection = this.inspect(serder, nests);
535
541
  if (!inspection.ok) {
@@ -588,6 +594,14 @@ export class Multiplexor {
588
594
  else
589
595
  yield* this.processApproved(nests);
590
596
  }
597
+ else if (v2Ipex && groupIpexFollowable(this.hby, this.receivedCopies(nests, existing))) {
598
+ // The same holds for a group IPEX message: once the copies received for it sign it to the group's threshold,
599
+ // this member takes it on receipt without signing, so every member holds what the group completed before the
600
+ // counterparty can answer in its thread. Earlier copies go first so that their signatures join this one's; a
601
+ // message already accepted here only gains this copy's. What cannot be taken yet (an answer whose prior has
602
+ // not arrived) the group IPEX owner takes on a later pass.
603
+ yield* this.processApproved(this.exchanger.complete(embeddedSaid) ? nests : this.receivedCopies(nests, existing).reverse());
604
+ }
591
605
  else if (serder.pvrsn.major === 2 || firstSubmission) {
592
606
  // Legacy approval polling consumes its first notice; repeated v1 notices would re-propose it.
593
607
  this.notifier?.add(firstSubmission ? { r: route, d: said } : { r: route, d: said, e: embeddedSaid });
@@ -660,6 +674,14 @@ export class Multiplexor {
660
674
  return event.ilk === "ixn"
661
675
  && kever.tholder.satisfy(Kever.verifyIndexedSignatures(event.raw, envelope.sigers, kever.verfers).indices);
662
676
  }
677
+ /** This wrapper's child first, then the children of the accepted wrappers already indexed for the same message. */
678
+ receivedCopies(nests, existing) {
679
+ const said = nests[0]?.body.said;
680
+ return [
681
+ ...nests.slice(0, 1),
682
+ ...existing.map((prior) => loadNests(this.hby.db, prior.qb64)[0]).filter((copy) => !!said && copy?.body.said === said),
683
+ ];
684
+ }
663
685
  /** Whether a rotation proposal names exactly the group's current signing members and rotation members. */
664
686
  namesCurrentMembers(attrs) {
665
687
  const group = typeof attrs.gid === "string" ? attrs.gid : "";
@@ -925,6 +947,32 @@ export class Multiplexor {
925
947
  return pre.length > 0 && this.hby.habs.has(pre);
926
948
  }
927
949
  }
950
+ /**
951
+ * Whether a member takes, without consent, the v2 group IPEX message that `copies` carry: the children of the stored
952
+ * `/multisig/exn` wrappers it received for that message, the first naming it. It does when the message belongs to a
953
+ * local group, the copies sign it to the group's threshold ({@link groupThresholdSigned}), the exchange it answers
954
+ * (if any) is accepted here, and it is not a group issuer's DH Grant, which the issuance owner follows instead.
955
+ * Accepting such a message adds no signature, so it is not consent; one below the threshold waits for approval.
956
+ *
957
+ * KERIpy's `Multiplexor.add` replays a child only after local approval, so a KERIpy member that did not sign holds
958
+ * only the proposal. keri-ts delivers to, and completes at, every member regardless of weight.
959
+ */
960
+ export function groupIpexFollowable(hby, copies) {
961
+ const first = copies[0], embedded = first?.body;
962
+ if (!(embedded instanceof SerderKERI) || embedded.pvrsn.major !== 2 || !embedded.pre || !embedded.said
963
+ || !embedded.route?.startsWith("/ipex/"))
964
+ return false;
965
+ const hab = hby.habs.get(embedded.pre);
966
+ if (!hab || !isLocalGroupHab(hby, hab))
967
+ return false;
968
+ if (embedded.route === IPEX_GRANT_ROUTE && isIssuanceOrigin(embedded.pre, first.nests?.[0]))
969
+ return false;
970
+ const prior = embedded.sad?.p;
971
+ if (typeof prior === "string" && prior.length > 0 && !hby.db.exns.get([prior]))
972
+ return false;
973
+ const proofs = copies.filter((copy) => copy.body.said === embedded.said).flatMap((copy) => envelopeFromFrame(copy).tsgs);
974
+ return groupThresholdSigned(hby.db, embedded, proofs);
975
+ }
928
976
  /** Extract the embedded business EXN SAD from a `/multisig/exn` wrapper. */
929
977
  export function embeddedBusinessExnSAD(serder) {
930
978
  const embed = serder.ked ? multisigEmbeddedSection(serder.ked) : null;