keri-ts 2.1.0 → 2.2.1

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 (300) hide show
  1. package/README.md +30 -2
  2. package/esm/keri/npm/src/acdc/authoring.js +195 -0
  3. package/esm/keri/npm/src/acdc/custody.js +187 -0
  4. package/esm/keri/npm/src/acdc/dhking.js +244 -32
  5. package/esm/keri/npm/src/acdc/regbasing.js +10 -0
  6. package/esm/keri/npm/src/acdc/salting.js +16 -1
  7. package/esm/keri/npm/src/acdc/scheming.js +93 -54
  8. package/esm/keri/npm/src/acdc/unblinding.js +118 -95
  9. package/esm/keri/npm/src/app/bootstrap/authenticating.js +4 -6
  10. package/esm/keri/npm/src/app/bootstrap/oobi-resolving.js +41 -0
  11. package/esm/keri/npm/src/app/bootstrap/oobiery.js +4 -6
  12. package/esm/keri/npm/src/app/credentials/credential-revoking.js +1 -0
  13. package/esm/keri/npm/src/app/credentials/credential-workflows.js +2 -0
  14. package/esm/keri/npm/src/app/credentials/registry-settling.js +44 -0
  15. package/esm/keri/npm/src/app/credentials/root-replicating.js +68 -60
  16. package/esm/keri/npm/src/app/credentials/root-replicator.js +198 -0
  17. package/esm/keri/npm/src/app/credentials/tel-completion.js +36 -0
  18. package/esm/keri/npm/src/app/delivery/forwarding.js +16 -3
  19. package/esm/keri/npm/src/app/exchanges/exchanging.js +14 -4
  20. package/esm/keri/npm/src/app/identity/challenging.js +50 -0
  21. package/esm/keri/npm/src/app/identity/delegating.js +77 -77
  22. package/esm/keri/npm/src/app/identity/delegation-approval.js +333 -0
  23. package/esm/keri/npm/src/app/identity/event-completion.js +154 -0
  24. package/esm/keri/npm/src/app/identity/habbing.js +129 -6
  25. package/esm/keri/npm/src/app/identity/keeping.js +77 -4
  26. package/esm/keri/npm/src/app/ipex/group-ipex.js +341 -0
  27. package/esm/keri/npm/src/app/ipex/group-issuance.js +905 -0
  28. package/esm/keri/npm/src/app/ipex/ipex-admitting.js +120 -0
  29. package/esm/keri/npm/src/app/ipex/ipex-conversation.js +197 -0
  30. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +591 -0
  31. package/esm/keri/npm/src/app/ipex/ipex-granting.js +93 -0
  32. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +50 -19
  33. package/esm/keri/npm/src/app/ipex/ipex-node-authentication.js +42 -18
  34. package/esm/keri/npm/src/app/ipex/issuance.js +896 -0
  35. package/esm/keri/npm/src/app/ipex/pending-presentations.js +46 -0
  36. package/esm/keri/npm/src/app/ipex/presentation-files.js +200 -0
  37. package/esm/keri/npm/src/app/ipex/presenting.js +408 -0
  38. package/esm/keri/npm/src/app/ipex/receive-issuance.js +86 -0
  39. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +667 -0
  40. package/esm/keri/npm/src/app/mailbox/ipex-arrival.js +103 -0
  41. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +322 -0
  42. package/esm/keri/npm/src/app/mailbox/kel-publishing.js +65 -0
  43. package/esm/keri/npm/src/app/mailbox/kel-submission.js +24 -0
  44. package/esm/keri/npm/src/app/mailbox/mailbox-admin.js +208 -0
  45. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +212 -47
  46. package/esm/keri/npm/src/app/mailbox/mailboxing.js +10 -5
  47. package/esm/keri/npm/src/app/multisig/counseling.js +488 -0
  48. package/esm/keri/npm/src/app/multisig/group-members.js +18 -0
  49. package/esm/keri/npm/src/app/multisig/multisig-messaging.js +33 -6
  50. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +98 -147
  51. package/esm/keri/npm/src/app/multisig/proposals.js +281 -0
  52. package/esm/keri/npm/src/app/multisig/proposing.js +122 -0
  53. package/esm/keri/npm/src/app/notifications/signaling.js +5 -5
  54. package/esm/keri/npm/src/app/protocol/parsering.js +26 -1
  55. package/esm/keri/npm/src/app/protocol/protocol-processing.js +9 -3
  56. package/esm/keri/npm/src/app/queries/query-workflows.js +98 -0
  57. package/esm/keri/npm/src/app/runtime/agent-runtime.js +106 -55
  58. package/esm/keri/npm/src/app/runtime/cue-runtime.js +3 -6
  59. package/esm/keri/npm/src/app/runtime/delivery-outbox.js +87 -0
  60. package/esm/keri/npm/src/app/runtime/doing.js +17 -0
  61. package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +207 -0
  62. package/esm/keri/npm/src/app/runtime/operation-owners.js +151 -0
  63. package/esm/keri/npm/src/app/runtime/retry-backoff.js +93 -0
  64. package/esm/keri/npm/src/app/runtime/workflow-contract.js +80 -0
  65. package/esm/keri/npm/src/app/transport/http-client.js +25 -6
  66. package/esm/keri/npm/src/app/witness/http-endpoints.js +4 -4
  67. package/esm/keri/npm/src/cli/common/event-completion.js +49 -0
  68. package/esm/keri/npm/src/cli/common/runtime-workers.js +23 -0
  69. package/esm/keri/npm/src/cli/credentials/ipex.js +164 -191
  70. package/esm/keri/npm/src/cli/credentials/vc.js +53 -72
  71. package/esm/keri/npm/src/cli/endpoints/did.js +12 -3
  72. package/esm/keri/npm/src/cli/endpoints/oobi.js +10 -13
  73. package/esm/keri/npm/src/cli/identity/challenge.js +8 -34
  74. package/esm/keri/npm/src/cli/identity/delegate.js +81 -258
  75. package/esm/keri/npm/src/cli/identity/incept.js +15 -32
  76. package/esm/keri/npm/src/cli/identity/interact.js +5 -17
  77. package/esm/keri/npm/src/cli/identity/rotate.js +8 -36
  78. package/esm/keri/npm/src/cli/index.js +3 -1
  79. package/esm/keri/npm/src/cli/messaging/group-join.js +83 -0
  80. package/esm/keri/npm/src/cli/messaging/group-wait.js +88 -0
  81. package/esm/keri/npm/src/cli/messaging/multisig.js +190 -285
  82. package/esm/keri/npm/src/cli/messaging/root-copy-lines.js +31 -0
  83. package/esm/keri/npm/src/core/kel/eventing.js +66 -10
  84. package/esm/keri/npm/src/core/mailbox/kram-policy.js +39 -6
  85. package/esm/keri/npm/src/core/mailbox/kraming.js +2 -0
  86. package/esm/keri/npm/src/core/records.js +23 -0
  87. package/esm/keri/npm/src/core/routing.js +13 -0
  88. package/esm/keri/npm/src/db/basing.js +34 -2
  89. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +221 -0
  90. package/esm/keri/npm/src/db/indexeddb.js +98 -0
  91. package/esm/keri/npm/src/db/keeping.js +11 -2
  92. package/esm/keri/npm/src/db/store-lock.js +150 -0
  93. package/esm/keri/npm/src/npm/db-indexeddb.browser.js +2 -0
  94. package/esm/keri/npm/src/npm/db-indexeddb.js +3 -0
  95. package/esm/keri/npm/src/npm/db.browser.js +2 -0
  96. package/esm/keri/npm/src/npm/index.browser.js +9 -0
  97. package/esm/keri/npm/src/npm/runtime-web.browser.js +2 -0
  98. package/esm/keri/npm/src/npm/runtime-web.js +3 -0
  99. package/esm/keri/npm/src/npm/runtime.browser.js +2 -0
  100. package/esm/keri/npm/src/runtime/index.js +51 -4
  101. package/esm/keri/npm/src/runtime/web/output-barrier.js +47 -0
  102. package/esm/keri/npm/src/runtime/web/persistent-storage.js +37 -0
  103. package/esm/keri/npm/src/runtime/web.js +10 -0
  104. package/esm/keri/npm/src/vdr/tel-publication.js +16 -0
  105. package/esm/keri/npm/src/version.js +2 -2
  106. package/package.json +15 -2
  107. package/types/acdc/authoring.d.ts +54 -0
  108. package/types/acdc/authoring.d.ts.map +1 -0
  109. package/types/acdc/custody.d.ts +84 -0
  110. package/types/acdc/custody.d.ts.map +1 -0
  111. package/types/acdc/dhking.d.ts +94 -3
  112. package/types/acdc/dhking.d.ts.map +1 -1
  113. package/types/acdc/regbasing.d.ts +4 -0
  114. package/types/acdc/regbasing.d.ts.map +1 -1
  115. package/types/acdc/salting.d.ts +6 -1
  116. package/types/acdc/salting.d.ts.map +1 -1
  117. package/types/acdc/scheming.d.ts +2 -0
  118. package/types/acdc/scheming.d.ts.map +1 -1
  119. package/types/acdc/unblinding.d.ts +58 -19
  120. package/types/acdc/unblinding.d.ts.map +1 -1
  121. package/types/app/bootstrap/authenticating.d.ts +3 -1
  122. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  123. package/types/app/bootstrap/oobi-resolving.d.ts +92 -0
  124. package/types/app/bootstrap/oobi-resolving.d.ts.map +1 -0
  125. package/types/app/bootstrap/oobiery.d.ts +3 -1
  126. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  127. package/types/app/credentials/credential-revoking.d.ts +3 -0
  128. package/types/app/credentials/credential-revoking.d.ts.map +1 -1
  129. package/types/app/credentials/credential-workflows.d.ts +5 -0
  130. package/types/app/credentials/credential-workflows.d.ts.map +1 -1
  131. package/types/app/credentials/registry-settling.d.ts +85 -0
  132. package/types/app/credentials/registry-settling.d.ts.map +1 -0
  133. package/types/app/credentials/root-replicating.d.ts +56 -44
  134. package/types/app/credentials/root-replicating.d.ts.map +1 -1
  135. package/types/app/credentials/root-replicator.d.ts +105 -0
  136. package/types/app/credentials/root-replicator.d.ts.map +1 -0
  137. package/types/app/credentials/tel-completion.d.ts +55 -0
  138. package/types/app/credentials/tel-completion.d.ts.map +1 -0
  139. package/types/app/delivery/forwarding.d.ts +6 -0
  140. package/types/app/delivery/forwarding.d.ts.map +1 -1
  141. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  142. package/types/app/identity/challenging.d.ts +54 -0
  143. package/types/app/identity/challenging.d.ts.map +1 -1
  144. package/types/app/identity/delegating.d.ts +21 -4
  145. package/types/app/identity/delegating.d.ts.map +1 -1
  146. package/types/app/identity/delegation-approval.d.ts +228 -0
  147. package/types/app/identity/delegation-approval.d.ts.map +1 -0
  148. package/types/app/identity/event-completion.d.ts +151 -0
  149. package/types/app/identity/event-completion.d.ts.map +1 -0
  150. package/types/app/identity/habbing.d.ts +67 -2
  151. package/types/app/identity/habbing.d.ts.map +1 -1
  152. package/types/app/identity/keeping.d.ts +49 -3
  153. package/types/app/identity/keeping.d.ts.map +1 -1
  154. package/types/app/ipex/group-ipex.d.ts +254 -0
  155. package/types/app/ipex/group-ipex.d.ts.map +1 -0
  156. package/types/app/ipex/group-issuance.d.ts +281 -0
  157. package/types/app/ipex/group-issuance.d.ts.map +1 -0
  158. package/types/app/ipex/ipex-admitting.d.ts +47 -0
  159. package/types/app/ipex/ipex-admitting.d.ts.map +1 -0
  160. package/types/app/ipex/ipex-conversation.d.ts +195 -0
  161. package/types/app/ipex/ipex-conversation.d.ts.map +1 -0
  162. package/types/app/ipex/ipex-discloser.d.ts +156 -0
  163. package/types/app/ipex/ipex-discloser.d.ts.map +1 -0
  164. package/types/app/ipex/ipex-granting.d.ts +145 -0
  165. package/types/app/ipex/ipex-granting.d.ts.map +1 -0
  166. package/types/app/ipex/ipex-grouping.d.ts +18 -0
  167. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  168. package/types/app/ipex/ipex-node-authentication.d.ts +9 -1
  169. package/types/app/ipex/ipex-node-authentication.d.ts.map +1 -1
  170. package/types/app/ipex/issuance.d.ts +445 -0
  171. package/types/app/ipex/issuance.d.ts.map +1 -0
  172. package/types/app/ipex/pending-presentations.d.ts +25 -0
  173. package/types/app/ipex/pending-presentations.d.ts.map +1 -0
  174. package/types/app/ipex/presentation-files.d.ts +47 -0
  175. package/types/app/ipex/presentation-files.d.ts.map +1 -0
  176. package/types/app/ipex/presenting.d.ts +300 -0
  177. package/types/app/ipex/presenting.d.ts.map +1 -0
  178. package/types/app/ipex/receive-issuance.d.ts +99 -0
  179. package/types/app/ipex/receive-issuance.d.ts.map +1 -0
  180. package/types/app/ipex/unblinding-workflow.d.ts +257 -0
  181. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -0
  182. package/types/app/mailbox/ipex-arrival.d.ts +104 -0
  183. package/types/app/mailbox/ipex-arrival.d.ts.map +1 -0
  184. package/types/app/mailbox/kel-publisher.d.ts +141 -0
  185. package/types/app/mailbox/kel-publisher.d.ts.map +1 -0
  186. package/types/app/mailbox/kel-publishing.d.ts +70 -0
  187. package/types/app/mailbox/kel-publishing.d.ts.map +1 -0
  188. package/types/app/mailbox/kel-submission.d.ts +18 -0
  189. package/types/app/mailbox/kel-submission.d.ts.map +1 -0
  190. package/types/app/mailbox/mailbox-admin.d.ts +113 -0
  191. package/types/app/mailbox/mailbox-admin.d.ts.map +1 -0
  192. package/types/app/mailbox/mailbox-poller.d.ts +92 -3
  193. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  194. package/types/app/mailbox/mailboxing.d.ts +3 -0
  195. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  196. package/types/app/multisig/counseling.d.ts +196 -0
  197. package/types/app/multisig/counseling.d.ts.map +1 -0
  198. package/types/app/multisig/group-members.d.ts +11 -0
  199. package/types/app/multisig/group-members.d.ts.map +1 -1
  200. package/types/app/multisig/grouping.d.ts +39 -20
  201. package/types/app/multisig/grouping.d.ts.map +1 -1
  202. package/types/app/multisig/multisig-messaging.d.ts +23 -6
  203. package/types/app/multisig/multisig-messaging.d.ts.map +1 -1
  204. package/types/app/multisig/multisig-workflows.d.ts +91 -22
  205. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  206. package/types/app/multisig/proposals.d.ts +129 -0
  207. package/types/app/multisig/proposals.d.ts.map +1 -0
  208. package/types/app/multisig/proposing.d.ts +125 -0
  209. package/types/app/multisig/proposing.d.ts.map +1 -0
  210. package/types/app/notifications/signaling.d.ts +3 -1
  211. package/types/app/notifications/signaling.d.ts.map +1 -1
  212. package/types/app/protocol/parsering.d.ts +13 -0
  213. package/types/app/protocol/parsering.d.ts.map +1 -1
  214. package/types/app/protocol/protocol-processing.d.ts +7 -1
  215. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  216. package/types/app/queries/query-workflows.d.ts +98 -0
  217. package/types/app/queries/query-workflows.d.ts.map +1 -0
  218. package/types/app/runtime/agent-runtime.d.ts +72 -25
  219. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  220. package/types/app/runtime/cue-runtime.d.ts +2 -1
  221. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  222. package/types/app/runtime/delivery-outbox.d.ts +68 -0
  223. package/types/app/runtime/delivery-outbox.d.ts.map +1 -0
  224. package/types/app/runtime/doing.d.ts +27 -0
  225. package/types/app/runtime/doing.d.ts.map +1 -0
  226. package/types/app/runtime/ipex-auxiliaries.d.ts +74 -0
  227. package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -0
  228. package/types/app/runtime/operation-owners.d.ts +137 -0
  229. package/types/app/runtime/operation-owners.d.ts.map +1 -0
  230. package/types/app/runtime/retry-backoff.d.ts +49 -0
  231. package/types/app/runtime/retry-backoff.d.ts.map +1 -0
  232. package/types/app/runtime/workflow-contract.d.ts +79 -0
  233. package/types/app/runtime/workflow-contract.d.ts.map +1 -0
  234. package/types/app/transport/http-client.d.ts.map +1 -1
  235. package/types/cli/common/event-completion.d.ts +26 -0
  236. package/types/cli/common/event-completion.d.ts.map +1 -0
  237. package/types/cli/common/runtime-workers.d.ts +23 -0
  238. package/types/cli/common/runtime-workers.d.ts.map +1 -0
  239. package/types/cli/credentials/ipex.d.ts.map +1 -1
  240. package/types/cli/credentials/vc.d.ts +1 -1
  241. package/types/cli/credentials/vc.d.ts.map +1 -1
  242. package/types/cli/endpoints/did.d.ts.map +1 -1
  243. package/types/cli/endpoints/oobi.d.ts +3 -1
  244. package/types/cli/endpoints/oobi.d.ts.map +1 -1
  245. package/types/cli/identity/challenge.d.ts +1 -1
  246. package/types/cli/identity/challenge.d.ts.map +1 -1
  247. package/types/cli/identity/delegate.d.ts +14 -1
  248. package/types/cli/identity/delegate.d.ts.map +1 -1
  249. package/types/cli/identity/incept.d.ts.map +1 -1
  250. package/types/cli/identity/interact.d.ts.map +1 -1
  251. package/types/cli/identity/rotate.d.ts.map +1 -1
  252. package/types/cli/index.d.ts +3 -1
  253. package/types/cli/index.d.ts.map +1 -1
  254. package/types/cli/messaging/group-join.d.ts +56 -0
  255. package/types/cli/messaging/group-join.d.ts.map +1 -0
  256. package/types/cli/messaging/group-wait.d.ts +34 -0
  257. package/types/cli/messaging/group-wait.d.ts.map +1 -0
  258. package/types/cli/messaging/multisig.d.ts +29 -5
  259. package/types/cli/messaging/multisig.d.ts.map +1 -1
  260. package/types/cli/messaging/root-copy-lines.d.ts +39 -0
  261. package/types/cli/messaging/root-copy-lines.d.ts.map +1 -0
  262. package/types/core/cues.d.ts +11 -0
  263. package/types/core/cues.d.ts.map +1 -1
  264. package/types/core/kel/eventing.d.ts.map +1 -1
  265. package/types/core/mailbox/kram-policy.d.ts +14 -0
  266. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  267. package/types/core/mailbox/kraming.d.ts +1 -1
  268. package/types/core/mailbox/kraming.d.ts.map +1 -1
  269. package/types/core/records.d.ts +61 -0
  270. package/types/core/records.d.ts.map +1 -1
  271. package/types/core/routing.d.ts +6 -0
  272. package/types/core/routing.d.ts.map +1 -1
  273. package/types/db/basing.d.ts +8 -2
  274. package/types/db/basing.d.ts.map +1 -1
  275. package/types/db/core/indexeddb-persistence.d.ts +59 -0
  276. package/types/db/core/indexeddb-persistence.d.ts.map +1 -0
  277. package/types/db/indexeddb.d.ts +37 -0
  278. package/types/db/indexeddb.d.ts.map +1 -0
  279. package/types/db/keeping.d.ts +5 -2
  280. package/types/db/keeping.d.ts.map +1 -1
  281. package/types/db/store-lock.d.ts +72 -0
  282. package/types/db/store-lock.d.ts.map +1 -0
  283. package/types/npm/db-indexeddb.d.ts +4 -0
  284. package/types/npm/db-indexeddb.d.ts.map +1 -0
  285. package/types/npm/runtime-web.d.ts +4 -0
  286. package/types/npm/runtime-web.d.ts.map +1 -0
  287. package/types/runtime/index.d.ts +42 -7
  288. package/types/runtime/index.d.ts.map +1 -1
  289. package/types/runtime/web/output-barrier.d.ts +30 -0
  290. package/types/runtime/web/output-barrier.d.ts.map +1 -0
  291. package/types/runtime/web/persistent-storage.d.ts +36 -0
  292. package/types/runtime/web/persistent-storage.d.ts.map +1 -0
  293. package/types/runtime/web.d.ts +11 -0
  294. package/types/runtime/web.d.ts.map +1 -0
  295. package/types/vdr/tel-publication.d.ts +6 -0
  296. package/types/vdr/tel-publication.d.ts.map +1 -1
  297. package/types/version.d.ts +2 -2
  298. package/esm/keri/npm/src/cli/common/tel-completion.js +0 -24
  299. package/types/cli/common/tel-completion.d.ts +0 -8
  300. package/types/cli/common/tel-completion.d.ts.map +0 -1
@@ -1,19 +1,18 @@
1
1
  /**
2
2
  * Finite delivery of prepared `/registry/root` copies; transmission is never receipt.
3
3
  *
4
- * `replicateRegistryRoots` is the KERIpy `rootreplicating.replicate` happy path:
5
- * group commands call it once a send is possible and report each outcome. There
6
- * is no acknowledgement, sent-copies record or retry; receiving is idempotent,
7
- * so duplicate copies are harmless, and `tufa vc registry root send` is the
8
- * recovery path for a copy that was not sent.
4
+ * `replicateRootCopies` is the KERIpy `rootreplicating.replicate` happy path: a
5
+ * command calls it once a send is possible and reports each typed outcome. There
6
+ * is no acknowledgement or retry here (the runtime's `RootReplicator` derives
7
+ * what is owed and retries); receiving is idempotent, so duplicate copies are
8
+ * harmless.
9
9
  */
10
10
  import { parseBytes } from "cesr-ts";
11
11
  import { action, race } from "effection";
12
+ import { RootUnverified } from "../../acdc/salting.js";
12
13
  import { AppError, ValidationError } from "../../core/errors.js";
13
14
  import { CREDENTIAL_MAILBOX_TOPIC } from "../../core/mailbox/mailbox-topics.js";
14
15
  import { localGroupMember } from "../multisig/group-members.js";
15
- /** The operator command that resends a copy automatic replication did not send. */
16
- export const RootCopyRecovery = "tufa vc registry root send";
17
16
  /**
18
17
  * Send each copy from `sender` to its recipient's `credential` mailbox, in
19
18
  * order, and return one outcome per copy. A failed copy does not stop later
@@ -42,71 +41,79 @@ function* within(clock, milliseconds, operation) {
42
41
  ]);
43
42
  }
44
43
  /**
45
- * Fire and forget one copy of each of `registries`' held roots for group
46
- * `ghab` to each recipient, signed by `ghab`'s local member.
44
+ * Prepare and deliver one `/registry/root` copy for `gid` from its local member to
45
+ * `recipient`, before `deadline` (epoch ms of `clock`).
47
46
  *
48
- * `recipients` defaults to every other current signing member. Each copy is
49
- * prepared with `RegistryRoot.prepare` and a per-registry derived hint
50
- * (`hinted`, so a copy that could never verify is reported, not sent) and
51
- * delivered through `deliverRegistryRoots`, as `tufa vc registry root send`
52
- * does. `timeoutMs` is this replication's own budget: a copy reached after it
53
- * ends, or still sending when it ends, is reported unsent. Nothing is retried
54
- * or recorded and an `AppError` never escapes: every outcome is returned for
55
- * display. The caller runs `postman.deliverDo` for the duration.
47
+ * The copy is sealed with `RegistryRoot.prepare` and a per-registry derived hint
48
+ * (`hinted`, so a copy that could never verify is reported, not sent). Any
49
+ * `AppError` ends in the result; any other exception is an invariant failure and
50
+ * escapes. The caller runs `postman.deliverDo` for the duration.
56
51
  */
57
- export function* replicateRegistryRoots(root, postman, clock, ghab, registries, { recipients, timeoutMs }) {
52
+ export function* replicateRootCopy(root, postman, clock, gid, registry, recipient, deadline) {
53
+ const sender = localGroupMember(root.hby, gid);
54
+ const result = { kin: "failed", group: gid, registry, sender: sender.pre, recipient };
55
+ try {
56
+ if (clock.now() >= deadline) {
57
+ throw new ValidationError("registry root copies did not complete within their budget");
58
+ }
59
+ let wire;
60
+ try {
61
+ wire = root.prepare(sender, recipient, gid, registry, { hinted: true });
62
+ }
63
+ catch (error) {
64
+ result.kin = error instanceof RootUnverified ? "deferred" : "refused";
65
+ throw error;
66
+ }
67
+ result.exchange = parseBytes(wire)[0].body.said;
68
+ const outcome = yield* within(clock, deadline - clock.now(), deliverRegistryRoots(postman, sender.pre, [{ recipient, wire }]));
69
+ if (!outcome)
70
+ throw new ValidationError("registry root copies did not complete within their budget");
71
+ if (outcome[0].kin === "failed") {
72
+ result.error = outcome[0].error ?? "delivery failed";
73
+ }
74
+ else {
75
+ result.kin = "sent";
76
+ result.deliveries = outcome[0].deliveries;
77
+ }
78
+ }
79
+ catch (error) {
80
+ if (!(error instanceof AppError))
81
+ throw error;
82
+ result.error = error;
83
+ }
84
+ return result;
85
+ }
86
+ /**
87
+ * Send one copy of each of `registries`' held roots for group `ghab` to each
88
+ * recipient, signed by `ghab`'s local member, and return one result per copy.
89
+ *
90
+ * `recipients` defaults to every other current signing member. `timeoutMs` is this
91
+ * replication's own budget: a copy reached after it ends, or still sending when it
92
+ * ends, is `failed`. Nothing is retried or recorded and an `AppError` never
93
+ * escapes. The caller runs `postman.deliverDo` for the duration.
94
+ */
95
+ export function* replicateRootCopies(root, postman, clock, ghab, registries, { recipients, timeoutMs }) {
58
96
  const gid = ghab.pre, sender = localGroupMember(root.hby, gid), deadline = clock.now() + Math.max(timeoutMs, 0);
59
97
  const targets = recipients ?? root.members(gid).filter(aid => aid !== sender.pre);
60
- const reports = [];
98
+ const results = [];
61
99
  for (const registry of registries) {
62
100
  for (const recipient of targets) {
63
- const result = {
64
- phase: "root_copy",
65
- gid,
66
- registry,
67
- sender: sender.pre,
68
- recipient,
69
- transmitted: false,
70
- };
71
- let failure = null;
72
- try {
73
- if (clock.now() >= deadline) {
74
- throw new ValidationError("registry root copies did not complete within their budget");
75
- }
76
- const wire = root.prepare(sender, recipient, gid, registry, { hinted: true });
77
- result.exchange = parseBytes(wire)[0].body.said;
78
- const outcome = yield* within(clock, deadline - clock.now(), deliverRegistryRoots(postman, sender.pre, [{ recipient, wire }]));
79
- if (!outcome)
80
- failure = new ValidationError("registry root copies did not complete within their budget");
81
- else if (outcome[0].kin === "failed")
82
- failure = outcome[0].error ?? "delivery failed";
83
- }
84
- catch (error) {
85
- if (!(error instanceof AppError))
86
- throw error;
87
- failure = error;
88
- }
89
- reports.push(failure === null ? { ...result, transmitted: true } : {
90
- ...result,
91
- error: failure instanceof Error ? failure.message : String(failure),
92
- recovery: RootCopyRecovery,
93
- });
101
+ results.push(yield* replicateRootCopy(root, postman, clock, gid, registry, recipient, deadline));
94
102
  }
95
103
  }
96
- return reports;
104
+ return results;
97
105
  }
98
106
  /** Budget for re-seal copies from a command without its own `--timeout`, as KERIpy `rootreplicating.Budget`. */
99
107
  export const RootResealBudgetMs = 30_000;
100
108
  /**
101
109
  * After an accepted group rotation, send held roots to new or rekeyed members.
102
110
  *
103
- * Fire and forget like `replicateRegistryRoots`: only a signer of the rotation
104
- * that held a prior key sends (`RegistryRoot.resealed`), every such signer
105
- * sends with no election, a member holding no root for the group sends
106
- * nothing, and no failure ever fails the rotation. The caller runs
107
- * `postman.deliverDo` for the duration.
111
+ * Only a signer of the rotation that held a prior key sends (`RegistryRoot.resealed`),
112
+ * every such signer sends with no election, and a member holding no root for the
113
+ * group sends nothing. `examination` is set when the recipients could not be
114
+ * selected, and then nothing is sent. Fire and forget like `replicateRootCopies`.
108
115
  */
109
- export function* resealRegistryRoots(root, postman, clock, ghab, rotation, { timeoutMs = RootResealBudgetMs } = {}) {
116
+ export function* resealRootCopies(root, postman, clock, ghab, rotation, { timeoutMs = RootResealBudgetMs } = {}) {
110
117
  let recipients;
111
118
  try {
112
119
  recipients = root.resealed(ghab, rotation);
@@ -114,10 +121,11 @@ export function* resealRegistryRoots(root, postman, clock, ghab, rotation, { tim
114
121
  catch (error) {
115
122
  if (!(error instanceof AppError))
116
123
  throw error;
117
- return [{ phase: "root_copy", gid: ghab.pre, rotation, transmitted: false, error: error.message }];
124
+ return { examination: { group: ghab.pre, rotation, error }, copies: [] };
118
125
  }
119
126
  const registries = root.held(ghab.pre);
120
127
  if (!recipients.length || !registries.length)
121
- return [];
122
- return yield* replicateRegistryRoots(root, postman, clock, ghab, registries, { recipients, timeoutMs });
128
+ return { examination: null, copies: [] };
129
+ const copies = yield* replicateRootCopies(root, postman, clock, ghab, registries, { recipients, timeoutMs });
130
+ return { examination: null, copies };
123
131
  }
@@ -0,0 +1,198 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _RootReplicator_instances, _RootReplicator_deps, _RootReplicator_pending, _RootReplicator_inflight, _RootReplicator_seen, _RootReplicator_dirty, _RootReplicator_pruned, _RootReplicator_attempt, _RootReplicator_refreshDirty, _RootReplicator_derive;
13
+ import { isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
14
+ import { replicateRootCopy, RootResealBudgetMs } from "./root-replicating.js";
15
+ /** Most copies one `processOnce` attempts, so a first pass over a large store cannot stall the runtime. */
16
+ export const RootSendsPerPass = 4;
17
+ /** Process-local retry delay after a copy's first failed attempt; doubles per attempt up to {@link RootRetryMaxMs}. */
18
+ export const RootRetryBaseMs = 1_000;
19
+ export const RootRetryMaxMs = 300_000;
20
+ const idOf = (group, registry, recipient) => [group, registry, recipient].join("/");
21
+ /**
22
+ * Derives, sends and records registry root copies for a wallet's local groups.
23
+ *
24
+ * Holds no durable state of its own beyond `Baser.rrsn`; what is pending and the
25
+ * backoff are process-local and rebuilt from state. A host calls `prompt` (or
26
+ * `replicate`) for each Dhke cue and `processOnce` on its loop; both are safe to repeat,
27
+ * and overlapping `processOnce` calls never send one copy twice.
28
+ */
29
+ export class RootReplicator {
30
+ constructor(deps) {
31
+ _RootReplicator_instances.add(this);
32
+ _RootReplicator_deps.set(this, void 0);
33
+ _RootReplicator_pending.set(this, new Map());
34
+ _RootReplicator_inflight.set(this, new Set());
35
+ _RootReplicator_seen.set(this, new Map());
36
+ _RootReplicator_dirty.set(this, new Set());
37
+ _RootReplicator_pruned.set(this, false);
38
+ __classPrivateFieldSet(this, _RootReplicator_deps, deps, "f");
39
+ }
40
+ /** Re-derive `group`'s owed copies on the next pass and retry its failed copies at once. */
41
+ prompt(group) {
42
+ __classPrivateFieldGet(this, _RootReplicator_dirty, "f").add(group);
43
+ for (const entry of __classPrivateFieldGet(this, _RootReplicator_pending, "f").values())
44
+ if (entry.group === group)
45
+ entry.retryAt = 0;
46
+ }
47
+ /** The Dhke completion cue: a prompt to run the pass now, not the source of truth. */
48
+ replicate(cue) {
49
+ this.prompt(cue.gid);
50
+ }
51
+ /**
52
+ * Derive what is owed, then send up to {@link RootSendsPerPass} due copies within
53
+ * `timeoutMs`. A copy is recorded when sent and kept for retry otherwise. Never throws
54
+ * on a remote failure; every attempt is returned. The host runs `postman.deliverDo`.
55
+ */
56
+ *processOnce({ timeoutMs = RootResealBudgetMs } = {}) {
57
+ this.sweep();
58
+ return yield* this.processDue({ timeoutMs });
59
+ }
60
+ /**
61
+ * The fixed-tock pass: re-derive only the groups that were prompted, then send up to {@link RootSendsPerPass}
62
+ * due copies. A rotation nobody prompted for waits for {@link RootReplicator.sweep}.
63
+ */
64
+ *processDue({ timeoutMs = RootResealBudgetMs } = {}) {
65
+ const { clock } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), deadline = clock.now() + Math.max(timeoutMs, 0);
66
+ __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_refreshDirty).call(this);
67
+ const results = [];
68
+ for (const entry of [...__classPrivateFieldGet(this, _RootReplicator_pending, "f").values()]) {
69
+ if (results.length >= RootSendsPerPass)
70
+ break;
71
+ if (entry.retryAt > clock.now())
72
+ continue;
73
+ const result = yield* __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_attempt).call(this, entry.group, entry.registry, entry.recipient, deadline);
74
+ if (!result)
75
+ continue;
76
+ results.push(result);
77
+ if (result.kin === "sent") {
78
+ __classPrivateFieldGet(this, _RootReplicator_pending, "f").delete(idOf(entry.group, entry.registry, entry.recipient));
79
+ }
80
+ else {
81
+ entry.attempts += 1;
82
+ entry.lastFailure = result.error;
83
+ entry.retryAt = clock.now() + Math.min(RootRetryBaseMs * 2 ** (entry.attempts - 1), RootRetryMaxMs);
84
+ }
85
+ }
86
+ return results;
87
+ }
88
+ /**
89
+ * Send each registry's root to each recipient now, whatever was sent before, as an
90
+ * explicit command does; a sent copy is recorded like any other. `recipients` defaults
91
+ * to every other current signing member.
92
+ */
93
+ *send(ghab, registries, { recipients, timeoutMs }) {
94
+ const { root, clock } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), gid = ghab.pre, deadline = clock.now() + Math.max(timeoutMs, 0);
95
+ const sender = localGroupMember(root.hby, gid).pre;
96
+ const targets = recipients ?? root.members(gid).filter(aid => aid !== sender);
97
+ const results = [];
98
+ for (const registry of registries) {
99
+ for (const recipient of targets) {
100
+ const result = yield* __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_attempt).call(this, gid, registry, recipient, deadline);
101
+ if (result)
102
+ results.push(result);
103
+ }
104
+ }
105
+ return results;
106
+ }
107
+ /** Whether a copy is owed or a group is waiting to be re-derived; in memory, reads no store. */
108
+ pending() {
109
+ return __classPrivateFieldGet(this, _RootReplicator_pending, "f").size > 0 || __classPrivateFieldGet(this, _RootReplicator_dirty, "f").size > 0;
110
+ }
111
+ /** Pure read: what is owed and why it has not gone yet. */
112
+ status() {
113
+ return {
114
+ owed: [...__classPrivateFieldGet(this, _RootReplicator_pending, "f").values()].map(({ retryAt: _retryAt, ...entry }) => ({ ...entry })),
115
+ };
116
+ }
117
+ /**
118
+ * The full check: drop markers of groups that are gone (once) and re-derive each local group that is new or
119
+ * whose establishment event changed. One keyed read per local identifier, so a host runs it every
120
+ * many tocks, not every tock.
121
+ */
122
+ sweep() {
123
+ const { root } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), hby = root.hby;
124
+ if (!__classPrivateFieldGet(this, _RootReplicator_pruned, "f")) {
125
+ __classPrivateFieldSet(this, _RootReplicator_pruned, true, "f");
126
+ for (const [[group]] of [...hby.db.rrsn.getTopItemIter()]) {
127
+ if (!hby.db.getHab(group)?.mid)
128
+ hby.db.rrsn.trim(group, { topive: true });
129
+ }
130
+ }
131
+ for (const hab of hby.habs.values()) {
132
+ const established = hab.pre && isLocalGroupHab(hby, hab) ? hab.kever?.lastEst.d : null;
133
+ if (!established || (__classPrivateFieldGet(this, _RootReplicator_seen, "f").get(hab.pre) === established && !__classPrivateFieldGet(this, _RootReplicator_dirty, "f").has(hab.pre)))
134
+ continue;
135
+ __classPrivateFieldGet(this, _RootReplicator_seen, "f").set(hab.pre, established);
136
+ __classPrivateFieldGet(this, _RootReplicator_dirty, "f").delete(hab.pre);
137
+ __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_derive).call(this, hab.pre);
138
+ }
139
+ }
140
+ }
141
+ _RootReplicator_deps = new WeakMap(), _RootReplicator_pending = new WeakMap(), _RootReplicator_inflight = new WeakMap(), _RootReplicator_seen = new WeakMap(), _RootReplicator_dirty = new WeakMap(), _RootReplicator_pruned = new WeakMap(), _RootReplicator_instances = new WeakSet(), _RootReplicator_attempt = function
142
+ /** Deliver one copy unless an overlapping call already is; record it when sent. */
143
+ * _RootReplicator_attempt(group, registry, recipient, deadline) {
144
+ const { root, postman, clock, changes } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), id = idOf(group, registry, recipient);
145
+ if (__classPrivateFieldGet(this, _RootReplicator_inflight, "f").has(id))
146
+ return null;
147
+ __classPrivateFieldGet(this, _RootReplicator_inflight, "f").add(id);
148
+ try {
149
+ const key = root.contributor(group, recipient);
150
+ const result = yield* replicateRootCopy(root, postman, clock, group, registry, recipient, deadline);
151
+ if (result.kin === "sent" && key) {
152
+ root.hby.db.rrsn.pin([group, registry, recipient], { key, dt: new Date(clock.now()).toISOString() });
153
+ }
154
+ changes?.push({ kind: "group", id: group });
155
+ return result;
156
+ }
157
+ finally {
158
+ __classPrivateFieldGet(this, _RootReplicator_inflight, "f").delete(id);
159
+ }
160
+ }, _RootReplicator_refreshDirty = function _RootReplicator_refreshDirty() {
161
+ const hby = __classPrivateFieldGet(this, _RootReplicator_deps, "f").root.hby;
162
+ for (const group of [...__classPrivateFieldGet(this, _RootReplicator_dirty, "f")]) {
163
+ const hab = hby.habs.get(group);
164
+ const established = hab && isLocalGroupHab(hby, hab) ? hab.kever?.lastEst.d : null;
165
+ __classPrivateFieldGet(this, _RootReplicator_dirty, "f").delete(group);
166
+ if (!established)
167
+ continue;
168
+ __classPrivateFieldGet(this, _RootReplicator_seen, "f").set(group, established);
169
+ __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_derive).call(this, group);
170
+ }
171
+ }, _RootReplicator_derive = function _RootReplicator_derive(group) {
172
+ const { root, changes } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), db = root.hby.db;
173
+ const sender = localGroupMember(root.hby, group).pre, registries = root.held(group);
174
+ const members = root.members(group).filter(aid => aid !== sender);
175
+ const wanted = new Set();
176
+ for (const registry of registries) {
177
+ for (const recipient of members) {
178
+ const id = idOf(group, registry, recipient);
179
+ wanted.add(id);
180
+ if (db.rrsn.get([group, registry, recipient])?.key === root.contributor(group, recipient)) {
181
+ __classPrivateFieldGet(this, _RootReplicator_pending, "f").delete(id);
182
+ }
183
+ else if (!__classPrivateFieldGet(this, _RootReplicator_pending, "f").has(id)) {
184
+ __classPrivateFieldGet(this, _RootReplicator_pending, "f").set(id, { group, registry, recipient, attempts: 0, lastFailure: null, retryAt: 0 });
185
+ }
186
+ }
187
+ }
188
+ for (const [id, entry] of [...__classPrivateFieldGet(this, _RootReplicator_pending, "f")]) {
189
+ if (entry.group === group && !wanted.has(id))
190
+ __classPrivateFieldGet(this, _RootReplicator_pending, "f").delete(id);
191
+ }
192
+ for (const [[, registry, recipient]] of [...db.rrsn.getTopItemIter(group, { topive: true })]) {
193
+ if (!wanted.has(idOf(group, registry, recipient)))
194
+ db.rrsn.rem([group, registry, recipient]);
195
+ }
196
+ if (wanted.size)
197
+ changes?.push({ kind: "group", id: group });
198
+ };
@@ -0,0 +1,36 @@
1
+ import { systemClock } from "../runtime/runtime-clock.js";
2
+ import { observeUntil } from "../runtime/workflow-contract.js";
3
+ /** Elapsed milliseconds between reads of a TEL wait. */
4
+ export const TEL_WAIT_POLL_MS = 50;
5
+ /** Read the status of the TEL event `pre` at `sn`; mutates nothing. */
6
+ export function telEventStatus(deps, pre, sn = 0) {
7
+ const progress = deps.publication.progress(pre, sn);
8
+ if (progress === null)
9
+ return { state: "unknown", reason: "not-tracked" };
10
+ return progress === "complete" ? { state: "complete" } : { state: "active", phase: progress };
11
+ }
12
+ /** Status of an issued credential: its TEL issuance event plus saved credential evidence. */
13
+ export function issuedCredentialStatus(deps, said) {
14
+ const status = telEventStatus(deps, said, 0);
15
+ if (status.state === "complete" && deps.publication.reger.saved.get(said) === null) {
16
+ return { state: "active", phase: "awaitingCredentialSave" };
17
+ }
18
+ return status;
19
+ }
20
+ function* observe(read, { deadline, clock = systemClock }) {
21
+ const done = yield* observeUntil(() => read().state === "complete", {
22
+ timeoutMs: Math.max(0, deadline - clock.now()),
23
+ clock,
24
+ pollMs: TEL_WAIT_POLL_MS,
25
+ });
26
+ const status = read();
27
+ return done ? { outcome: "settled", status } : { outcome: "timed-out", status, deadline };
28
+ }
29
+ /** Observe one exact TEL coordinate until complete or the deadline passes. Starts no worker. */
30
+ export function* waitForTelEvent(deps, pre, sn, options) {
31
+ return yield* observe(() => telEventStatus(deps, pre, sn), options);
32
+ }
33
+ /** Observe an issuance until its TEL event is complete and the credential is saved. Starts no worker. */
34
+ export function* waitForIssuedCredential(deps, said, options) {
35
+ return yield* observe(() => issuedCredentialStatus(deps, said), options);
36
+ }
@@ -7,6 +7,7 @@ import { exchange } from "../../core/exchange/protocol-exchanging.js";
7
7
  import { Roles } from "../../core/roles.js";
8
8
  import { mailboxTopicKey, preferredUrl } from "../mailbox/mailboxing.js";
9
9
  import { transportMailboxTopic } from "../mailbox/topic-routing.js";
10
+ import { localGroupMember } from "../multisig/group-members.js";
10
11
  import { abortable } from "../runtime/abortable.js";
11
12
  import { runtimeTurn } from "../runtime/runtime-turn.js";
12
13
  import { CesrHttpRefusal } from "../transport/cesr-egress.js";
@@ -505,6 +506,15 @@ export class Poster {
505
506
  };
506
507
  });
507
508
  }
509
+ /**
510
+ * Whether `src` has any eligible role (controller, agent, mailbox or witness) from which to reach `dest`, as
511
+ * delivery would find it now. A pure read of accepted endpoint state: it says nothing about whether the
512
+ * endpoint answers. A proposer uses it to refuse before it creates anything it could not publish.
513
+ */
514
+ canReach(src, dest, topic = "multisig") {
515
+ const hab = this.hby.habs.get(src);
516
+ return !!hab && this.findBytesRoutes(hab, dest, topic).length > 0;
517
+ }
508
518
  /**
509
519
  * Prepare a target using the supplied wire and selected endpoint.
510
520
  *
@@ -544,6 +554,9 @@ export class Poster {
544
554
  throw new Error("Mailbox publication did not complete");
545
555
  return { kind: "stored" };
546
556
  }
557
+ // A group cannot sign the wrapper alone: one member's signature does not meet a threshold above one, and the
558
+ // host would escrow the wrapper. The local member wraps; the payload keeps whatever signatures it carries.
559
+ const signer = this.hby.db.groups.has(hab.pre) ? localGroupMember(this.hby, hab.pre) : hab;
547
560
  const msgs = [];
548
561
  if (evt.kin === "stream") {
549
562
  // Prepare every frame before opening the one selected relay attempt.
@@ -555,13 +568,13 @@ export class Poster {
555
568
  msgs.push(evt.msg);
556
569
  }
557
570
  // The provider reads the envelope, so it must read v2 whenever the envelope or child is v2.
558
- if ((hab.kelVersion.major >= 2 || msgs.some(wireNeedsV2)) && knownV1Peer(this.hby, endpoint.eid)) {
571
+ if ((signer.kelVersion.major >= 2 || msgs.some(wireNeedsV2)) && knownV1Peer(this.hby, endpoint.eid)) {
559
572
  throw new PeerVersionError(`Refusing a v2 envelope for provider ${endpoint.eid}, whose latest accepted event is v1.`);
560
573
  }
561
574
  const forwarded = [];
562
575
  for (const message of msgs)
563
- forwarded.push(yield* buildForwardedDelivery(hab, { recipient, topic, message }));
564
- const intro = introduce(hab, endpoint.eid);
576
+ forwarded.push(yield* buildForwardedDelivery(signer, { recipient, topic, message }));
577
+ const intro = introduce(signer, endpoint.eid);
565
578
  let message = concatBytes(intro, ...forwarded);
566
579
  if (evt.kin === "stream") {
567
580
  const frames = parseBytes(message);
@@ -13,6 +13,7 @@ import { hasNests, loadNests, prepareNests, publishNests } from "../../db/exchan
13
13
  import { encodeDateTimeToDater, makeNowIso8601, timestampMicros } from "../../time/mod.js";
14
14
  import { groupMemberHasKey } from "../multisig/group-members.js";
15
15
  import { abortable } from "../runtime/abortable.js";
16
+ import { runtimeTurn } from "../runtime/runtime-turn.js";
16
17
  const textDecoder = new TextDecoder();
17
18
  const EXCHANGE_ESCROW_TIMEOUT_MS = 10_000;
18
19
  /** CESR syntax failures identify malformed evidence; I/O, limits, and hashing failures do not. */
@@ -212,6 +213,7 @@ export class Exchanger {
212
213
  const nests = args.nests?.length
213
214
  ? yield* abortable((signal) => prepareNests(args.nests, signal))
214
215
  : undefined;
216
+ const written = this.hby.db.writeVersion;
215
217
  const result = this.hby.db.transaction(() => {
216
218
  if (selectedGrant) {
217
219
  const db = this.hby.db;
@@ -270,8 +272,9 @@ export class Exchanger {
270
272
  }
271
273
  return admission;
272
274
  });
273
- // Durable completion joins the actual LMDB flush; no external file lifetime remains.
274
- yield* this.hby.db.flushed();
275
+ // Durable completion joins the actual LMDB flush; a transaction that wrote nothing has nothing to wait for.
276
+ if (this.hby.db.writeVersion !== written)
277
+ yield* this.hby.db.flushed();
275
278
  if (!result)
276
279
  return undefined;
277
280
  for (const cue of result.cues)
@@ -406,11 +409,16 @@ export class Exchanger {
406
409
  // False means an existing qualified member, not a failed write. Exceptions abort the writer.
407
410
  for (const texter of args.essrs ?? [])
408
411
  this.hby.db.essrs.add([said], texter);
409
- this.hby.db.exchangeMeta.pin([said], {
412
+ const meta = {
410
413
  mailboxAid,
411
414
  ...(receiverTime !== undefined ? { receiverTime } : {}),
412
415
  gvrsn: previous?.gvrsn ?? args.gvrsn ?? args.serder.gvrsn ?? args.serder.pvrsn,
413
- });
416
+ };
417
+ // A retry that learns nothing new leaves the stored record as it is rather than committing it again.
418
+ if (!previous || previous.mailboxAid !== meta.mailboxAid || previous.receiverTime !== meta.receiverTime
419
+ || JSON.stringify(previous.gvrsn) !== JSON.stringify(meta.gvrsn)) {
420
+ this.hby.db.exchangeMeta.pin([said], meta);
421
+ }
414
422
  if (pending) {
415
423
  if (newEscrow) {
416
424
  if (senderQuery)
@@ -492,6 +500,8 @@ export class Exchanger {
492
500
  ...attempt.ejection,
493
501
  });
494
502
  }
503
+ // Deliveries and I/O take a slot between escrowed messages; none is skipped.
504
+ yield* runtimeTurn();
495
505
  }
496
506
  }
497
507
  /**
@@ -1,4 +1,9 @@
1
1
  import { Diger } from "cesr-ts";
2
+ import { ValidationError } from "../../core/errors.js";
3
+ import { sendExchangeMessage } from "../exchanges/exchange-messaging.js";
4
+ import { systemClock } from "../runtime/runtime-clock.js";
5
+ import { observeUntil } from "../runtime/workflow-contract.js";
6
+ import { isLocalGroupHab } from "./endpoint-roleing.js";
2
7
  const CHALLENGE_CONSONANTS = [
3
8
  "b",
4
9
  "c",
@@ -91,6 +96,51 @@ export function findVerifiedChallengeResponse(db, signer, expected) {
91
96
  }
92
97
  return null;
93
98
  }
99
+ /**
100
+ * Respond to a challenge: sign and send a `/challenge/response` exchange message
101
+ * carrying `words` to `recipient`. Each call authors a new message (the exchange
102
+ * carries a timestamp); the verifier's matching is idempotent over repeats.
103
+ *
104
+ * @returns The signed exchange message that was sent.
105
+ * @throws ValidationError for a group habitat or empty words; send failures propagate.
106
+ */
107
+ export function* respondToChallenge(deps, hab, recipient, words) {
108
+ if (isLocalGroupHab(deps.hby, hab)) {
109
+ throw new ValidationError("Challenge respond does not coordinate group approval. Select the responding member alias.");
110
+ }
111
+ if (words.length === 0)
112
+ throw new ValidationError("Challenge words must not be empty.");
113
+ const payload = { words: [...words] };
114
+ const { serder } = yield* sendExchangeMessage(deps.hby, hab, deps.http, deps.clock, {
115
+ dest: recipient,
116
+ route: CHALLENGE_RESPONSE_ROUTE,
117
+ topic: "challenge",
118
+ payload: { i: hab.pre, ...payload },
119
+ });
120
+ return serder;
121
+ }
122
+ /** Read the status of `words` from `signer`; mutates nothing. */
123
+ export function challengeStatus(deps, signer, words) {
124
+ const response = findVerifiedChallengeResponse(deps.db, signer, words);
125
+ if (!response)
126
+ return { state: "waiting" };
127
+ const recorded = deps.db.chas.get([signer]).some((diger) => diger.qb64 === response.said);
128
+ return { state: recorded ? "verified" : "received", response };
129
+ }
130
+ /**
131
+ * Observe stored exchange messages until a response with the expected words from
132
+ * `signer` arrives or the budget expires. Starts no worker: the host's runtime
133
+ * (mailbox polling, exchange intake) is what stores the response.
134
+ */
135
+ export function* waitForChallengeResponse(deps, signer, words, { timeoutMs, pollMs = 250, clock = systemClock }) {
136
+ const done = yield* observeUntil(() => challengeStatus(deps, signer, words).state !== "waiting", {
137
+ timeoutMs,
138
+ pollMs,
139
+ clock,
140
+ });
141
+ const status = challengeStatus(deps, signer, words);
142
+ return done ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
143
+ }
94
144
  /** Persist one verifier-confirmed challenge response in `chas.`. */
95
145
  export function markChallengeVerified(db, signer, serder) {
96
146
  if (!serder.said) {