keri-ts 2.2.4 → 2.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +14 -14
  2. package/esm/keri/npm/src/acdc/dhking.js +29 -2
  3. package/esm/keri/npm/src/acdc/observing.js +10 -3
  4. package/esm/keri/npm/src/acdc/query-resend.js +74 -0
  5. package/esm/keri/npm/src/acdc/regengine.js +39 -9
  6. package/esm/keri/npm/src/acdc/regeventing.js +5 -3
  7. package/esm/keri/npm/src/acdc/registering.js +19 -0
  8. package/esm/keri/npm/src/acdc/salting.js +4 -0
  9. package/esm/keri/npm/src/acdc/unblinding.js +38 -3
  10. package/esm/keri/npm/src/app/bootstrap/authenticating.js +32 -10
  11. package/esm/keri/npm/src/app/bootstrap/oobiery.js +363 -30
  12. package/esm/keri/npm/src/app/credentials/credential-importing.js +33 -4
  13. package/esm/keri/npm/src/app/credentials/root-replicator.js +27 -15
  14. package/esm/keri/npm/src/app/credentials/schema-resolving.js +5 -1
  15. package/esm/keri/npm/src/app/credentials/tel-publishing.js +16 -7
  16. package/esm/keri/npm/src/app/delivery/forwarding.js +205 -44
  17. package/esm/keri/npm/src/app/delivery/relaying.js +232 -0
  18. package/esm/keri/npm/src/app/delivery/respondant.js +8 -5
  19. package/esm/keri/npm/src/app/exchanges/exchange-messaging.js +1 -1
  20. package/esm/keri/npm/src/app/exchanges/exchanging.js +65 -9
  21. package/esm/keri/npm/src/app/identity/delegating.js +66 -9
  22. package/esm/keri/npm/src/app/identity/habbing.js +190 -71
  23. package/esm/keri/npm/src/app/identity/keeping.js +111 -39
  24. package/esm/keri/npm/src/app/identity/retire-identifier.js +98 -0
  25. package/esm/keri/npm/src/app/ipex/disclosure-replaying.js +116 -0
  26. package/esm/keri/npm/src/app/ipex/group-ipex.js +122 -25
  27. package/esm/keri/npm/src/app/ipex/group-issuance.js +137 -37
  28. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +94 -31
  29. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +53 -3
  30. package/esm/keri/npm/src/app/ipex/ipex-v2.js +7 -2
  31. package/esm/keri/npm/src/app/ipex/issuance.js +147 -44
  32. package/esm/keri/npm/src/app/ipex/pending-presentations.js +4 -2
  33. package/esm/keri/npm/src/app/ipex/presenting.js +3 -18
  34. package/esm/keri/npm/src/app/ipex/receive-issuance.js +2 -1
  35. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +39 -11
  36. package/esm/keri/npm/src/app/mailbox/agent-inbox.js +292 -0
  37. package/esm/keri/npm/src/app/mailbox/forward-handler.js +298 -52
  38. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +26 -31
  39. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +70 -62
  40. package/esm/keri/npm/src/app/mailbox/mailboxing.js +20 -4
  41. package/esm/keri/npm/src/app/multisig/counseling.js +71 -27
  42. package/esm/keri/npm/src/app/multisig/group-members.js +41 -5
  43. package/esm/keri/npm/src/app/multisig/grouping.js +50 -2
  44. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +3 -18
  45. package/esm/keri/npm/src/app/multisig/proposing.js +1 -16
  46. package/esm/keri/npm/src/app/multisig/rejoin-group.js +99 -0
  47. package/esm/keri/npm/src/app/multisig/replace-member.js +29 -8
  48. package/esm/keri/npm/src/app/protocol/parsering.js +45 -17
  49. package/esm/keri/npm/src/app/protocol/protocol-processing.js +47 -21
  50. package/esm/keri/npm/src/app/queries/query-replies.js +0 -22
  51. package/esm/keri/npm/src/app/queries/relay-refresh.js +81 -0
  52. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +180 -34
  53. package/esm/keri/npm/src/app/runtime/agent-runtime.js +42 -73
  54. package/esm/keri/npm/src/app/runtime/cue-runtime.js +45 -5
  55. package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +8 -1
  56. package/esm/keri/npm/src/app/runtime/operation-owners.js +26 -56
  57. package/esm/keri/npm/src/app/runtime/workflow-contract.js +7 -4
  58. package/esm/keri/npm/src/app/transport/http-client.js +20 -2
  59. package/esm/keri/npm/src/app/transport/message-source.js +2 -1
  60. package/esm/keri/npm/src/app/transport/messengering.js +39 -8
  61. package/esm/keri/npm/src/app/witness/ingress.js +7 -0
  62. package/esm/keri/npm/src/app/witness/receipt-client.js +43 -16
  63. package/esm/keri/npm/src/app/witness/receipting.js +62 -20
  64. package/esm/keri/npm/src/cli/messaging/multisig.js +4 -3
  65. package/esm/keri/npm/src/core/errors.js +73 -0
  66. package/esm/keri/npm/src/core/kel/delegation-ancestry.js +11 -2
  67. package/esm/keri/npm/src/core/kel/eventing.js +25 -28
  68. package/esm/keri/npm/src/core/kel/kever.js +15 -9
  69. package/esm/keri/npm/src/core/mailbox/kram-policy.js +16 -0
  70. package/esm/keri/npm/src/core/mailbox/kram-pruning.js +25 -6
  71. package/esm/keri/npm/src/core/mailbox/kram-time.js +34 -6
  72. package/esm/keri/npm/src/core/mailbox/kraming.js +58 -2
  73. package/esm/keri/npm/src/core/missing-anchor-query.js +20 -0
  74. package/esm/keri/npm/src/core/routing.js +18 -25
  75. package/esm/keri/npm/src/core/scheming.js +12 -5
  76. package/esm/keri/npm/src/core/wire-fields.js +126 -0
  77. package/esm/keri/npm/src/db/basing.js +4 -13
  78. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +3 -2
  79. package/esm/keri/npm/src/db/core/webdber.js +85 -27
  80. package/esm/keri/npm/src/db/mailbox/mailboxing.js +13 -0
  81. package/esm/keri/npm/src/db/subing.js +4 -0
  82. package/esm/keri/npm/src/runtime/index.js +11 -5
  83. package/esm/keri/npm/src/time/mod.js +5 -0
  84. package/esm/keri/npm/src/vdr/credential-tsn-record.js +2 -7
  85. package/esm/keri/npm/src/vdr/eventing.js +71 -28
  86. package/esm/keri/npm/src/vdr/registry-tsn-record.js +3 -13
  87. package/esm/keri/npm/src/version.js +2 -2
  88. package/package.json +1 -1
  89. package/types/_dnt.polyfills.d.ts +9 -9
  90. package/types/_dnt.polyfills.d.ts.map +1 -1
  91. package/types/acdc/dhking.d.ts +8 -0
  92. package/types/acdc/dhking.d.ts.map +1 -1
  93. package/types/acdc/observing.d.ts +5 -1
  94. package/types/acdc/observing.d.ts.map +1 -1
  95. package/types/acdc/query-resend.d.ts +24 -0
  96. package/types/acdc/query-resend.d.ts.map +1 -0
  97. package/types/acdc/regengine.d.ts +19 -1
  98. package/types/acdc/regengine.d.ts.map +1 -1
  99. package/types/acdc/regeventing.d.ts.map +1 -1
  100. package/types/acdc/registering.d.ts +6 -0
  101. package/types/acdc/registering.d.ts.map +1 -1
  102. package/types/acdc/salting.d.ts.map +1 -1
  103. package/types/acdc/unblinding.d.ts +13 -1
  104. package/types/acdc/unblinding.d.ts.map +1 -1
  105. package/types/app/bootstrap/authenticating.d.ts +12 -2
  106. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  107. package/types/app/bootstrap/oobiery.d.ts +144 -7
  108. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  109. package/types/app/credentials/credential-importing.d.ts +8 -0
  110. package/types/app/credentials/credential-importing.d.ts.map +1 -1
  111. package/types/app/credentials/root-replicator.d.ts +19 -8
  112. package/types/app/credentials/root-replicator.d.ts.map +1 -1
  113. package/types/app/credentials/schema-resolving.d.ts.map +1 -1
  114. package/types/app/credentials/tel-publishing.d.ts +4 -2
  115. package/types/app/credentials/tel-publishing.d.ts.map +1 -1
  116. package/types/app/delivery/forwarding.d.ts +90 -5
  117. package/types/app/delivery/forwarding.d.ts.map +1 -1
  118. package/types/app/delivery/relaying.d.ts +144 -0
  119. package/types/app/delivery/relaying.d.ts.map +1 -0
  120. package/types/app/delivery/respondant.d.ts +9 -2
  121. package/types/app/delivery/respondant.d.ts.map +1 -1
  122. package/types/app/exchanges/exchanging.d.ts +36 -0
  123. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  124. package/types/app/identity/delegating.d.ts +6 -0
  125. package/types/app/identity/delegating.d.ts.map +1 -1
  126. package/types/app/identity/habbing.d.ts +106 -19
  127. package/types/app/identity/habbing.d.ts.map +1 -1
  128. package/types/app/identity/keeping.d.ts +54 -19
  129. package/types/app/identity/keeping.d.ts.map +1 -1
  130. package/types/app/identity/retire-identifier.d.ts +24 -0
  131. package/types/app/identity/retire-identifier.d.ts.map +1 -0
  132. package/types/app/ipex/disclosure-replaying.d.ts +61 -0
  133. package/types/app/ipex/disclosure-replaying.d.ts.map +1 -0
  134. package/types/app/ipex/group-ipex.d.ts +60 -14
  135. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  136. package/types/app/ipex/group-issuance.d.ts +54 -8
  137. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  138. package/types/app/ipex/ipex-discloser.d.ts +39 -2
  139. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  140. package/types/app/ipex/ipex-grouping.d.ts +23 -2
  141. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  142. package/types/app/ipex/ipex-v2.d.ts +5 -2
  143. package/types/app/ipex/ipex-v2.d.ts.map +1 -1
  144. package/types/app/ipex/issuance.d.ts +46 -16
  145. package/types/app/ipex/issuance.d.ts.map +1 -1
  146. package/types/app/ipex/pending-presentations.d.ts +4 -2
  147. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  148. package/types/app/ipex/presenting.d.ts.map +1 -1
  149. package/types/app/ipex/receive-issuance.d.ts +2 -1
  150. package/types/app/ipex/receive-issuance.d.ts.map +1 -1
  151. package/types/app/ipex/unblinding-workflow.d.ts +13 -2
  152. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  153. package/types/app/mailbox/agent-inbox.d.ts +126 -0
  154. package/types/app/mailbox/agent-inbox.d.ts.map +1 -0
  155. package/types/app/mailbox/forward-handler.d.ts +114 -14
  156. package/types/app/mailbox/forward-handler.d.ts.map +1 -1
  157. package/types/app/mailbox/kel-publisher.d.ts +12 -22
  158. package/types/app/mailbox/kel-publisher.d.ts.map +1 -1
  159. package/types/app/mailbox/mailbox-poller.d.ts +40 -24
  160. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  161. package/types/app/mailbox/mailboxing.d.ts +19 -5
  162. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  163. package/types/app/multisig/counseling.d.ts +32 -11
  164. package/types/app/multisig/counseling.d.ts.map +1 -1
  165. package/types/app/multisig/group-members.d.ts +17 -1
  166. package/types/app/multisig/group-members.d.ts.map +1 -1
  167. package/types/app/multisig/grouping.d.ts +17 -1
  168. package/types/app/multisig/grouping.d.ts.map +1 -1
  169. package/types/app/multisig/multisig-workflows.d.ts +0 -8
  170. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  171. package/types/app/multisig/proposing.d.ts +4 -19
  172. package/types/app/multisig/proposing.d.ts.map +1 -1
  173. package/types/app/multisig/rejoin-group.d.ts +119 -0
  174. package/types/app/multisig/rejoin-group.d.ts.map +1 -0
  175. package/types/app/multisig/replace-member.d.ts +15 -1
  176. package/types/app/multisig/replace-member.d.ts.map +1 -1
  177. package/types/app/protocol/parsering.d.ts +21 -2
  178. package/types/app/protocol/parsering.d.ts.map +1 -1
  179. package/types/app/protocol/protocol-processing.d.ts +25 -5
  180. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  181. package/types/app/queries/query-replies.d.ts +4 -8
  182. package/types/app/queries/query-replies.d.ts.map +1 -1
  183. package/types/app/queries/relay-refresh.d.ts +26 -0
  184. package/types/app/queries/relay-refresh.d.ts.map +1 -0
  185. package/types/app/queries/witness-inquisitor.d.ts +56 -5
  186. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  187. package/types/app/runtime/agent-runtime.d.ts +34 -3
  188. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  189. package/types/app/runtime/cue-runtime.d.ts +15 -3
  190. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  191. package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -1
  192. package/types/app/runtime/operation-owners.d.ts +16 -26
  193. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  194. package/types/app/runtime/workflow-contract.d.ts +21 -6
  195. package/types/app/runtime/workflow-contract.d.ts.map +1 -1
  196. package/types/app/transport/http-client.d.ts +9 -0
  197. package/types/app/transport/http-client.d.ts.map +1 -1
  198. package/types/app/transport/message-source.d.ts +6 -3
  199. package/types/app/transport/message-source.d.ts.map +1 -1
  200. package/types/app/transport/messengering.d.ts +10 -1
  201. package/types/app/transport/messengering.d.ts.map +1 -1
  202. package/types/app/witness/ingress.d.ts +1 -0
  203. package/types/app/witness/ingress.d.ts.map +1 -1
  204. package/types/app/witness/receipt-client.d.ts +19 -1
  205. package/types/app/witness/receipt-client.d.ts.map +1 -1
  206. package/types/app/witness/receipting.d.ts +24 -1
  207. package/types/app/witness/receipting.d.ts.map +1 -1
  208. package/types/cli/messaging/multisig.d.ts.map +1 -1
  209. package/types/core/cues.d.ts +53 -1
  210. package/types/core/cues.d.ts.map +1 -1
  211. package/types/core/errors.d.ts +32 -0
  212. package/types/core/errors.d.ts.map +1 -1
  213. package/types/core/kel/delegation-ancestry.d.ts.map +1 -1
  214. package/types/core/kel/eventing.d.ts.map +1 -1
  215. package/types/core/kel/kever-decisions.d.ts +3 -1
  216. package/types/core/kel/kever-decisions.d.ts.map +1 -1
  217. package/types/core/kel/kever.d.ts +7 -1
  218. package/types/core/kel/kever.d.ts.map +1 -1
  219. package/types/core/mailbox/kram-policy.d.ts +12 -0
  220. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  221. package/types/core/mailbox/kram-pruning.d.ts +5 -1
  222. package/types/core/mailbox/kram-pruning.d.ts.map +1 -1
  223. package/types/core/mailbox/kram-time.d.ts +26 -2
  224. package/types/core/mailbox/kram-time.d.ts.map +1 -1
  225. package/types/core/mailbox/kraming.d.ts +14 -1
  226. package/types/core/mailbox/kraming.d.ts.map +1 -1
  227. package/types/core/missing-anchor-query.d.ts +34 -0
  228. package/types/core/missing-anchor-query.d.ts.map +1 -0
  229. package/types/core/routing.d.ts +3 -7
  230. package/types/core/routing.d.ts.map +1 -1
  231. package/types/core/scheming.d.ts.map +1 -1
  232. package/types/core/wire-fields.d.ts +56 -0
  233. package/types/core/wire-fields.d.ts.map +1 -0
  234. package/types/db/basing.d.ts.map +1 -1
  235. package/types/db/core/indexeddb-persistence.d.ts +3 -2
  236. package/types/db/core/indexeddb-persistence.d.ts.map +1 -1
  237. package/types/db/core/webdber.d.ts +32 -6
  238. package/types/db/core/webdber.d.ts.map +1 -1
  239. package/types/db/exchange/exchange-records.d.ts +4 -0
  240. package/types/db/exchange/exchange-records.d.ts.map +1 -1
  241. package/types/db/mailbox/mailboxing.d.ts +10 -1
  242. package/types/db/mailbox/mailboxing.d.ts.map +1 -1
  243. package/types/db/subing.d.ts +2 -0
  244. package/types/db/subing.d.ts.map +1 -1
  245. package/types/runtime/index.d.ts +20 -14
  246. package/types/runtime/index.d.ts.map +1 -1
  247. package/types/time/mod.d.ts +2 -0
  248. package/types/time/mod.d.ts.map +1 -1
  249. package/types/vdr/credential-tsn-record.d.ts.map +1 -1
  250. package/types/vdr/eventing.d.ts +32 -1
  251. package/types/vdr/eventing.d.ts.map +1 -1
  252. package/types/vdr/registry-tsn-record.d.ts.map +1 -1
  253. package/types/version.d.ts +2 -2
  254. package/esm/keri/npm/src/app/runtime/signer-conditions.js +0 -51
  255. package/types/app/runtime/signer-conditions.d.ts +0 -32
  256. package/types/app/runtime/signer-conditions.d.ts.map +0 -1
@@ -22,6 +22,7 @@
22
22
  * | holder: pending Grant, no root held | `awaitingExchangeConsent` |
23
23
  * | pending Grant past its deadline | `expired` |
24
24
  * | holder: root held, not committed otherwise; issuer: update accepted | `active`, `rootRetained` (adopted) |
25
+ * | group member: root copy received or the custodian's final proof stored | `active`, `awaitingAnchor` (followed) |
25
26
  * | issuer: pending Grant, update not accepted | `interrupted` |
26
27
  * | root held and the registry committed to another exchange | `interrupted` |
27
28
  * | holder: Grant accepted, no accepted Admit | `active`, `awaitingAdmitConsent` |
@@ -54,9 +55,12 @@
54
55
  * `{kind: "issuance", id: <grant>}` and the embedder records it.
55
56
  *
56
57
  * A local group issues and receives through the same table, status and wait: its consent calls and per-operation
57
- * steps are in `group-issuance.ts` and run inside the same `processOnce`. A group exchange is not resumed after a
58
- * restart (its frozen coordinator and custodian are not stored): a group pending Grant with no root reads
59
- * `awaitingExchangeConsent`, and a member consents again with the same call.
58
+ * steps are in `group-issuance.ts` and run inside the same `processOnce`. A group exchange in progress is not
59
+ * resumed after a restart (its frozen coordinator and custodian are not stored): a group pending Grant with no root
60
+ * reads `awaitingExchangeConsent`, and a member consents again with the same call. The custodian that retained the
61
+ * root is adopted once the stores allow it, and a member the group completed without is followed: the same sweep
62
+ * that finds resumable Grants begins the proof-only follow of each group Grant whose custodian's final proof is
63
+ * stored, signing nothing, so a restart begins it again.
60
64
  *
61
65
  * Collaborators are narrow concrete owners (ADR-0013); no `AgentRuntime`.
62
66
  */
@@ -65,27 +69,29 @@ import { call, race } from "effection";
65
69
  import { RegistryAuthor } from "../../acdc/authoring.js";
66
70
  import { RegistryCustody } from "../../acdc/custody.js";
67
71
  import { grantTransactionId, registryUncommitted, resumableExchange } from "../../acdc/dhking.js";
68
- import { ValidationError } from "../../core/errors.js";
72
+ import { QUERY_RESEND_MS } from "../../acdc/query-resend.js";
73
+ import { KramRefusedError, ValidationError } from "../../core/errors.js";
69
74
  import { CREDENTIAL_MAILBOX_TOPIC } from "../../core/mailbox/mailbox-topics.js";
70
75
  import { timestampMicros } from "../../time/mod.js";
71
76
  import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
72
77
  import { remoteGroupMembers } from "../multisig/multisig-messaging.js";
73
78
  import { DeliveryOutbox, } from "../runtime/delivery-outbox.js";
74
79
  import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
75
- import { deriveGroupCustody, GroupIssuance, GroupParty, registerGroupRoutes, stepGroup, } from "./group-issuance.js";
80
+ import { awaitsGroupProof, beginGroupProofFollow, deriveGroupCustody, GroupIssuance, GroupParty, registerGroupRoutes, stepGroup, storedGroupProof, } from "./group-issuance.js";
76
81
  import { IpexAdmitter } from "./ipex-admitting.js";
77
- import { exchangeWire, IpexDiscloser, storedExchangeWire } from "./ipex-discloser.js";
82
+ import { exchangeWire, IpexDiscloser, storedExchangeWire, } from "./ipex-discloser.js";
78
83
  /**
79
- * Milliseconds between resends of the issuer query. Each resend is a new query (new date, SAID and signatures):
80
- * the peer's KRAM caches every query it accepts, so an identical resend is a replay it drops, which would
81
- * strand a peer that took the first query and then lost it before consenting (a process that ended, or a
82
- * retained query that outlived its window).
84
+ * Milliseconds between the first resends of the issuer query, which the holder may have lost before consenting (a
85
+ * process that ended, or a retained query that outlived its window). A single-signer issuer re-dates every resend
86
+ * (new date, SAID and signatures): the peer's KRAM caches every query it accepts, so an identical resend would be
87
+ * a replay it drops. A group cannot re-date (re-signing needs the members), so it resends the same bytes, doubling
88
+ * this interval up to a cap and stopping once the query is older than the multi-key acceptance window.
83
89
  */
84
- export const ISSUANCE_QUERY_RESEND_MS = 1000;
90
+ export const ISSUANCE_QUERY_RESEND_MS = QUERY_RESEND_MS;
85
91
  /**
86
- * Milliseconds between scans of the pending exchange escrow for Grants a restarted process can finish. The scan
87
- * repeats because what makes a Grant resumable can arrive after the restart (an issuer's update accepted once the
88
- * other members approve it).
92
+ * Milliseconds between scans of the pending exchange escrow for Grants a restarted process can finish and group
93
+ * Grants whose final proof a member follows. The scan repeats because what makes a Grant resumable can arrive after
94
+ * the restart (an issuer's update accepted once the other members approve it), and a custodian's proof arrives late.
89
95
  */
90
96
  export const ISSUANCE_SWEEP_MS = 5_000;
91
97
  /** The key of one party's operation on a Grant. */
@@ -219,23 +225,27 @@ export class IssuanceOperations {
219
225
  remember(grant, ended) {
220
226
  this.ended.set(partyKey(grant, ended.role), ended);
221
227
  }
222
- /** Whether a pass has work: a live operation, or the one-time scan for resumable Grants has not run. */
228
+ /** Whether a pass has work: a live operation, an adoption still owed, or a sweep of the pending Grants is due. */
223
229
  active() {
224
230
  return this.live.size > 0 || this.stale.length > 0 || this.deps.clock.now() >= this.nextSweep;
225
231
  }
226
232
  /**
227
233
  * Advance every live operation by one pass. Each pass is bounded by the operation's Grant deadline.
228
- * The first pass also adopts the pending Grants the stores prove resumable ({@link resumableGrants});
229
- * that signs and sends nothing.
234
+ * Every {@link ISSUANCE_SWEEP_MS} a pass also scans the pending exchange escrow once ({@link sweptGrants}): it
235
+ * adopts the Grants the stores prove resumable and begins the proof-only follow of the group Grants whose
236
+ * custodian's proof is stored. Neither signs nor sends anything.
230
237
  */
231
238
  *processOnce() {
232
239
  if (this.deps.clock.now() >= this.nextSweep) {
233
240
  this.nextSweep = this.deps.clock.now() + ISSUANCE_SWEEP_MS;
234
241
  const known = new Set(this.stale.map(({ grant, role }) => partyKey(grant, role)));
235
- this.stale.push(...resumableGrants(this.deps).filter(({ grant, role }) => {
242
+ const swept = sweptGrants(this.deps, (grant, role) => {
236
243
  const key = partyKey(grant, role);
237
- return !this.live.has(key) && !known.has(key) && !this.refused.has(key) && !this.ended.has(key);
238
- }));
244
+ return this.live.has(key) || known.has(key) || this.refused.has(key) || this.ended.has(key);
245
+ });
246
+ this.stale.push(...swept.resumable);
247
+ for (const found of swept.follows)
248
+ this.follow(found);
239
249
  }
240
250
  for (const found of [...this.stale]) {
241
251
  if (yield* this.settle(found))
@@ -249,6 +259,10 @@ export class IssuanceOperations {
249
259
  const left = op.expires === null ? null : op.expires - BigInt(this.deps.clock.now()) * 1000n;
250
260
  if (left !== null && left <= 0n)
251
261
  this.end(op, "expired", EXPIRED);
262
+ // A habitat that cannot sign yet waits for its signer; only its Grant deadline ends the operation meanwhile.
263
+ // A proof-only follow signs nothing, so it proceeds without one.
264
+ else if (!op.hab.canSign() && op.group?.mode !== "proofOnly")
265
+ continue;
252
266
  else if ((yield* race([this.advance(op), ...(left === null ? [] : [this.timer(Number(left) / 1000)])])) === "expired")
253
267
  this.end(op, "expired", EXPIRED);
254
268
  }
@@ -263,7 +277,8 @@ export class IssuanceOperations {
263
277
  /** End a live operation: release its DH approval, remember why until the process ends, tell the sink. */
264
278
  end(op, state, reason) {
265
279
  this.release(op);
266
- this.ended.set(partyKey(op.grant, op.role), { role: op.role, state, reason });
280
+ const deadline = state === "expired" ? op.deadline : null;
281
+ this.ended.set(partyKey(op.grant, op.role), { role: op.role, state, reason, ...(deadline && { deadline }) });
267
282
  this.deps.changes?.push({ kind: "issuance", id: op.grant });
268
283
  }
269
284
  /**
@@ -277,6 +292,14 @@ export class IssuanceOperations {
277
292
  this.end(op, "abandoned", reason);
278
293
  return bound.map(op => op.grant);
279
294
  }
295
+ /** Grants of the live operations, and of those found resumable and not yet adopted, whose habitat is `pre`. */
296
+ grantsOf(pre) {
297
+ const grants = [...this.live.values()].filter(op => op.hab.pre === pre).map(op => op.grant);
298
+ for (const found of this.stale)
299
+ if (found.hab.pre === pre)
300
+ grants.push(found.grant);
301
+ return [...new Set(grants)];
302
+ }
280
303
  /** Drop the live entry and the DH approval; a missing approval means the owner already closed. */
281
304
  release(op) {
282
305
  this.live.delete(partyKey(op.grant, op.role));
@@ -324,6 +347,24 @@ export class IssuanceOperations {
324
347
  }
325
348
  return true;
326
349
  }
350
+ /**
351
+ * Begin the proof-only follow a sweep found, under the stored proof's election; it signs nothing, so it needs no
352
+ * unlocked Keeper. A refusal means the stores no longer support it: the Grant then reads from them and is not tried
353
+ * again in this process. Any other failure is this Grant's alone and ends it `failed`, as for an adoption.
354
+ */
355
+ follow(found) {
356
+ const key = partyKey(found.grant, found.role);
357
+ try {
358
+ beginGroupProofFollow(this, found.grant, found.role, found.election.coordinator.said);
359
+ }
360
+ catch (error) {
361
+ this.refused.add(key);
362
+ if (!(error instanceof ValidationError)) {
363
+ this.ended.set(key, { role: found.role, state: "failed", reason: message(error) });
364
+ this.deps.changes?.push({ kind: "issuance", id: found.grant });
365
+ }
366
+ }
367
+ }
327
368
  /** Rebuild the live entry of a resumable Grant from the stored Grant and the held roots. */
328
369
  *adopt({ hab, grant, role }) {
329
370
  const group = isLocalGroupHab(this.deps.hby, hab);
@@ -345,7 +386,7 @@ export class IssuanceOperations {
345
386
  peer: wires ? retained.serder.sad.ri : retained.serder.pre,
346
387
  registry: credential.regid,
347
388
  expires,
348
- issuer: wires ? { credential, ...wires, query: null, sent: true, nextQuery: 0 } : undefined,
389
+ issuer: wires ? { credential, ...wires, query: null, sent: true, supportMark: null, nextQuery: 0 } : undefined,
349
390
  phase: "rootRetained",
350
391
  outbox: new DeliveryOutbox(this.deps.retry),
351
392
  finished: false,
@@ -442,7 +483,8 @@ export class IssuanceOperations {
442
483
  }
443
484
  /**
444
485
  * Record one group operation the user consented to ({@link approveGroupIssuance},
445
- * {@link approveGroupIssuanceExchange}, {@link followGroupIssuanceProof}). A holder approves DH here, with its
486
+ * {@link approveGroupIssuanceExchange}), or a proof-only follow, which needs no consent ({@link beginGroupProofFollow},
487
+ * begun by the sweep or by {@link followGroupIssuanceProof}). A holder approves DH here, with its
446
488
  * consent; an issuer approves once the group has authenticated the Grant.
447
489
  */
448
490
  beginGroup(group) {
@@ -482,6 +524,7 @@ export class IssuanceOperations {
482
524
  dest: op.peer,
483
525
  msg: concatBytes(this.discloser.proofSupport(issuer.raw, op.hab), issuer.grantWire),
484
526
  });
527
+ issuer.supportMark = this.discloser.proofSupportMark(issuer.raw, op.hab);
485
528
  issuer.sent = true;
486
529
  }
487
530
  while (op.dh.outgoing.length) {
@@ -551,7 +594,11 @@ export class IssuanceOperations {
551
594
  yield* op.dh.finish(op.xid);
552
595
  if (!this.deps.exc.complete(op.grant))
553
596
  throw new ValidationError("Issued Grant remains unaccepted");
554
- this.enqueue(op, { src: op.hab.pre, dest: op.peer, msg: this.discloser.proofSupport(issuer.raw, op.hab) });
597
+ // The Grant send already carried the support; proving added the anchoring event and the registry update.
598
+ const support = issuer.supportMark
599
+ ? this.discloser.proofSupportSince(issuer.raw, op.hab, issuer.supportMark)
600
+ : this.discloser.proofSupport(issuer.raw, op.hab);
601
+ this.enqueue(op, { src: op.hab.pre, dest: op.peer, msg: support });
555
602
  op.finished = true;
556
603
  }
557
604
  }
@@ -559,6 +606,20 @@ export class IssuanceOperations {
559
606
  function reached(clock, expires) {
560
607
  return expires !== null && BigInt(clock.now()) * 1000n >= expires;
561
608
  }
609
+ /**
610
+ * Refuse a Grant whose exchange deadline has passed with the KRAM lifetime refusal that names that deadline,
611
+ * so a caller learns why the Grant can no longer be acted on rather than that it is unknown. The deadline
612
+ * outlives the Grant's KRAM cache row and its ejection from escrow ({@link Exchanger.exchangeExpiry}). Returns
613
+ * when the Grant has no known deadline or is still inside it; the caller then raises its own refusal.
614
+ *
615
+ * @throws KramRefusedError (reason `lifetime`) once the deadline has passed.
616
+ */
617
+ function refuseLapsedGrant(deps, grant) {
618
+ const deadline = deps.exc.exchangeExpiry(grant);
619
+ if (deadline && reached(deps.clock, timestampMicros(deadline))) {
620
+ throw new KramRefusedError(grant, { kind: "drop", reason: "lifetime", deadline });
621
+ }
622
+ }
562
623
  /**
563
624
  * Whether a restarted process can complete this Grant without a new key exchange: it is pending inside its
564
625
  * deadline, `hab` is the party `role` names, and {@link resumableExchange} finds the roots. An issuer
@@ -585,23 +646,43 @@ function grantParties(hby, grant) {
585
646
  return issuer || holder ? { serder, issuer, holder } : null;
586
647
  }
587
648
  /**
588
- * Every pending Grant the stores prove a restarted process can finish: one scan of the pending exchange
589
- * escrow, made once when the owner first runs. A pure read.
649
+ * One scan of the pending exchange escrow, made every {@link ISSUANCE_SWEEP_MS}: each local party of a pending Grant
650
+ * that `skip` does not exclude (the owner skips what it holds live, ended, refused or already found) is either
651
+ * resumable, or a group member that follows the custodian's stored proof ({@link followable}). A pure read.
590
652
  */
591
- function resumableGrants(deps) {
592
- const found = [];
653
+ function sweptGrants(deps, skip) {
654
+ const swept = { resumable: [], follows: [] };
593
655
  for (const [[grant]] of [...deps.hby.db.epse.getTopItemIter()]) {
594
656
  const parties = grantParties(deps.hby, grant);
595
657
  if (!parties)
596
658
  continue;
597
- if (parties.issuer && resumable(deps, parties.issuer, grant, "issuer")) {
598
- found.push({ hab: parties.issuer, grant, role: "issuer" });
599
- }
600
- if (parties.holder && resumable(deps, parties.holder, grant, "holder")) {
601
- found.push({ hab: parties.holder, grant, role: "holder" });
659
+ for (const [hab, role] of [[parties.issuer, "issuer"], [parties.holder, "holder"]]) {
660
+ if (!hab || skip(grant, role))
661
+ continue;
662
+ if (resumable(deps, hab, grant, role)) {
663
+ swept.resumable.push({ hab, grant, role });
664
+ continue;
665
+ }
666
+ const election = followable(deps, hab, grant, role, parties.serder);
667
+ if (election)
668
+ swept.follows.push({ hab, grant, role, election });
602
669
  }
603
670
  }
604
- return found;
671
+ return swept;
672
+ }
673
+ /**
674
+ * The election under which the local member of group `hab` follows `grant` to its final proof, or null: the party's
675
+ * side is not complete, the Grant is inside its exchange deadline, and the custodian's proof is stored
676
+ * ({@link storedGroupProof}). Only the proof decides it, never the member's weight: a member that should sign is not
677
+ * followed, because the proof exists only once the key exchange no longer wants any signature.
678
+ */
679
+ function followable(deps, hab, grant, role, serder) {
680
+ if (!isLocalGroupHab(deps.hby, hab) || partyComplete(deps, grant, role))
681
+ return null;
682
+ const deadline = deps.exc.exchangeDeadline(serder), expires = deadline ? timestampMicros(deadline) : null;
683
+ if (expires === null || reached(deps.clock, expires))
684
+ return null;
685
+ return storedGroupProof(deps, hab, grant);
605
686
  }
606
687
  /** The terminal reading of a pending Grant no process can complete: ended by a restart, or by another exchange. */
607
688
  function orphaned(deps, hab, grant) {
@@ -641,7 +722,8 @@ function requireIndividual(deps, hab) {
641
722
  }
642
723
  /**
643
724
  * The exact unanchored pending Grant a local party may consent to, with its deadline. Refuses a group
644
- * identifier, an issuance already begun, a Grant that is not this party's, and an issuer without its registry.
725
+ * identifier, an issuance already begun, a Grant that is not this party's, and an issuer without its registry;
726
+ * a Grant past its deadline is refused with `KramRefusedError`.
645
727
  */
646
728
  function selectGrant(ops, hab, grant, role, group = false) {
647
729
  const { deps } = ops;
@@ -655,6 +737,7 @@ function selectGrant(ops, hab, grant, role, group = false) {
655
737
  if (!retained || !(credential instanceof SerderACDC) || !credential.regid
656
738
  || (role === "issuer" ? retained.serder.pre : retained.serder.sad.ri) !== hab.pre
657
739
  || retained.sourceSeals?.length || retained.ptds?.length || retained.essrs?.length) {
740
+ refuseLapsedGrant(deps, grant);
658
741
  throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
659
742
  }
660
743
  const reg = deps.rgy.regs.get(credential.regid);
@@ -689,12 +772,15 @@ function* grantWires(retained) {
689
772
  *
690
773
  * @throws ValidationError for a Grant that is not this identifier's exact
691
774
  * unanchored pending issuance, an unmanaged registry, a group identifier, or
692
- * an issuance already begun or completed.
775
+ * an issuance already begun or completed; its subclass `KramRefusedError`
776
+ * (reason `lifetime`, with the deadline) for a Grant past its exchange deadline.
693
777
  */
694
778
  export function* beginIssuance(ops, grant, auths) {
695
779
  const { deps } = ops, hab = grantParties(deps.hby, grant)?.issuer;
696
- if (!hab)
780
+ if (!hab) {
781
+ refuseLapsedGrant(deps, grant);
697
782
  throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
783
+ }
698
784
  const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "issuer");
699
785
  const { raw, grantWire } = yield* grantWires(retained);
700
786
  const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant), query = dh.start(grant);
@@ -708,7 +794,7 @@ export function* beginIssuance(ops, grant, auths) {
708
794
  peer: retained.serder.sad.ri,
709
795
  registry: credential.regid,
710
796
  expires,
711
- issuer: { credential, raw, grantWire, query, auths, sent: false, nextQuery: 0 },
797
+ issuer: { credential, raw, grantWire, query, auths, sent: false, supportMark: null, nextQuery: 0 },
712
798
  phase: "exchangingKeys",
713
799
  outbox: new DeliveryOutbox(ops.deps.retry),
714
800
  finished: false,
@@ -730,14 +816,18 @@ export function* proposeIssuance(ops, draft, auths) {
730
816
  * operation holds reads `interrupted` (or `expired` once the Grant deadline has passed); that is a
731
817
  * statement, not a write.
732
818
  *
819
+ * @throws KramRefusedError (reason `lifetime`, with the deadline) when the Grant's deadline has passed and the
820
+ * stores no longer hold what a status needs: KRAM pruned its cache row, or the escrow sweep ejected the Grant.
733
821
  * @throws ValidationError when `grant` is not a v2 Grant this runtime's identifiers are party to.
734
822
  */
735
823
  export function issuanceStatus(ops, grant, party = {}) {
736
824
  const { deps } = ops, parties = grantParties(deps.hby, grant);
737
825
  const chosen = chooseParty(parties, party);
738
826
  const ended = ops.endedOf(grant, chosen), live = ops.liveOf(grant, chosen);
739
- if (ended)
740
- return { state: ended.state, role: ended.role, reason: ended.reason };
827
+ if (ended) {
828
+ const { state, role, reason, deadline } = ended;
829
+ return { state, role, reason, ...(deadline && { deadline }) };
830
+ }
741
831
  if (live && !reached(deps.clock, live.expires)) {
742
832
  return {
743
833
  state: "active",
@@ -747,8 +837,10 @@ export function issuanceStatus(ops, grant, party = {}) {
747
837
  ...(live.outbox.failure && { failure: live.outbox.failure }),
748
838
  };
749
839
  }
750
- if (!parties)
840
+ if (!parties) {
841
+ refuseLapsedGrant(deps, grant);
751
842
  throw new ValidationError("No issuance was begun for this Grant");
843
+ }
752
844
  const { serder, issuer, holder } = parties, db = deps.hby.db;
753
845
  if (partyComplete(deps, grant, chosen)) {
754
846
  const admitted = !!db.erpy.get([grant]);
@@ -763,16 +855,22 @@ export function issuanceStatus(ops, grant, party = {}) {
763
855
  // A group issuer's partial Grant has no accepted lifetime until its members' contributions authenticate it.
764
856
  const lifetime = deps.exc.exchangeDeadline(serder) ?? undefined;
765
857
  const deadline = lifetime, expires = lifetime ? timestampMicros(lifetime) : null;
766
- if (!group && (!deadline || expires === null))
858
+ if (!group && (!deadline || expires === null)) {
859
+ // KRAM pruned the Grant's cache row at its deadline, before the escrow sweep ejected the Grant.
860
+ refuseLapsedGrant(deps, grant);
767
861
  throw new ValidationError("No issuance was begun for this Grant");
862
+ }
768
863
  if (reached(deps.clock, expires)) {
769
864
  const credential = deps.exc.retainedInput(grant)?.nests?.[0]?.body;
770
865
  const registry = credential instanceof SerderACDC ? credential.regid : undefined;
771
866
  const late = role === "holder" && registry && deps.rgy.store.headEvent(registry)?.ilk === "bup";
772
- return { state: "expired", role, reason: late ? EXPIRED_LATE : EXPIRED };
867
+ return { state: "expired", role, reason: late ? EXPIRED_LATE : EXPIRED, deadline };
773
868
  }
774
869
  if (resumable(deps, hab, grant, role))
775
870
  return { state: "active", role, phase: "rootRetained", deadline };
871
+ // The group completed without this member: the owner follows the custodian's proof once it is stored.
872
+ if (group && awaitsGroupProof(deps, hab, grant))
873
+ return { state: "active", role, phase: "awaitingAnchor", deadline };
776
874
  if ((role === "holder" || group) && !heldRoot(deps, hab, grant)) {
777
875
  // A group issuer's Grant may still be a partial one that only its members' contributions authenticate.
778
876
  if (!group && !deps.exc.retainedInput(grant, { pending: true })) {
@@ -892,11 +990,16 @@ export function* waitForIssuance(ops, grant, { timeoutMs, clock = ops.deps.clock
892
990
  * The holder's consent to the DH exchange for one pending Grant addressed to a local identifier. Approves DH and records the
893
991
  * consent; `processOnce` then answers the issuer's query, waits for its registry update and finishes the Grant.
894
992
  * Admitting the credential is a separate consent ({@link admitIssuance}).
993
+ *
994
+ * @throws KramRefusedError (reason `lifetime`, with the deadline) for a Grant past its exchange deadline, and
995
+ * ValidationError for any other Grant this holder cannot consent to.
895
996
  */
896
997
  export function approveIssuanceExchange(ops, grant) {
897
998
  const { deps } = ops, hab = grantParties(deps.hby, grant)?.holder;
898
- if (!hab)
999
+ if (!hab) {
1000
+ refuseLapsedGrant(deps, grant);
899
1001
  throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
1002
+ }
900
1003
  const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "holder");
901
1004
  const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant);
902
1005
  ops.begin({
@@ -49,7 +49,9 @@ export function pendingPresentations(ops, { hab } = {}) {
49
49
  /**
50
50
  * The accepted v2 Applies addressed to a local identifier (to `hab` when given) that no exchange has answered
51
51
  * (no `erpy` pointer) and whose deadline has not passed; with `includeAnswered`, the answered ones too. Mutates
52
- * nothing. A group member lists the Applies of its group once the group has accepted them.
52
+ * nothing. A group member lists the Applies of its group once the group has accepted them. The deadline is the
53
+ * Apply's own stamp plus the exchange window once KRAM has pruned its cache row, so an expired Apply stays
54
+ * omitted after the prune.
53
55
  */
54
56
  export function pendingApplies(deps, { hab, includeAnswered = false } = {}) {
55
57
  const { hby, exc, clock } = deps, found = [];
@@ -65,7 +67,7 @@ export function pendingApplies(deps, { hab, includeAnswered = false } = {}) {
65
67
  const answer = hby.db.erpy.get([apply])?.qb64 ?? null;
66
68
  if (answer && !includeAnswered)
67
69
  continue;
68
- const deadline = exc.exchangeDeadline(serder), expiry = deadline ? timestampMicros(deadline) : null;
70
+ const deadline = exc.exchangeExpiry(apply), expiry = deadline ? timestampMicros(deadline) : null;
69
71
  if (!answer && expiry !== null && now >= expiry)
70
72
  continue;
71
73
  const q = serder.sad?.q;
@@ -47,7 +47,6 @@ import { DeliveryOutbox } from "../runtime/delivery-outbox.js";
47
47
  import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
48
48
  import { proposeGroupIpex, proposeGroupIpexAfter as groupIpexAfter, } from "./group-ipex.js";
49
49
  import { IpexDiscloser, storedExchangeWire, } from "./ipex-discloser.js";
50
- import { prepareAcdcV2GroupIpex } from "./ipex-grouping.js";
51
50
  /** How long delivery failures may last on an exchange with no deadline (an Apply, an Agree) before it reads `failed`. */
52
51
  export const PRESENTATION_RETRY_BOUND_MS = 600_000;
53
52
  const EXPIRED = "The presentation reached its original exchange deadline";
@@ -220,7 +219,7 @@ export function* proposeGroupPresentation(deps, groupIpex, { hab, selection, opt
220
219
  const { presentationRegistry, ...body } = options;
221
220
  const gvrsn = body.gvrsn ?? undefined;
222
221
  const discloser = discloserFor(deps, hab, selection.derive);
223
- let prepared = yield* discloser.prepareGrantBody(hab.pre, selection, body);
222
+ const prepared = yield* discloser.prepareGrantBody(hab.pre, selection, body);
224
223
  if (!presentationRegistry) {
225
224
  return yield* proposeGroupIpex(groupIpex, { hab, exn: prepared.exn, nests: prepared.nests, gvrsn, deliver });
226
225
  }
@@ -237,26 +236,12 @@ export function* proposeGroupPresentation(deps, groupIpex, { hab, selection, opt
237
236
  // The Grant SAID is frozen before the update is anchored; only its nested proof is extended afterwards.
238
237
  const [proof, update] = reg.present(prepared.exn, "presented", { stamp: body.stamp });
239
238
  const proposed = yield* proposeRegistryEvent(proposals, hab, update, proof, counsel);
240
- const exn = prepared.exn;
241
239
  return groupIpexAfter(groupIpex, {
242
240
  hab,
243
- exn,
241
+ exn: prepared.exn,
244
242
  gvrsn,
245
243
  deliver,
246
- prerequisite: {
247
- anchor: registryEventRef(update, proposed.event),
248
- *proceed() {
249
- const seal = deps.rgy.store.anchor(update.said);
250
- if (!seal)
251
- throw new ValidationError("The presentation update is accepted but its anchor is unavailable");
252
- prepared = yield* discloser.attachPresentationProof(prepared, proof, { s: seal[0], d: seal[1] });
253
- const candidate = concatBytes(Counter.makeGVC(gvrsn ?? Vrsn_2_0), prepared.exn.raw, Counter.enclose({ qb64: prepared.nests, version: gvrsn ?? Vrsn_2_0 }));
254
- const own = yield* prepareAcdcV2GroupIpex(deps.hby, groupIpex.deps.protocol, hab, candidate);
255
- if (!own)
256
- throw new ValidationError("ACDC v2 group disclosure awaits authenticating dependencies.");
257
- return own;
258
- },
259
- },
244
+ prerequisite: { anchor: registryEventRef(update, proposed.event), grant: prepared, opening: proof },
260
245
  });
261
246
  }
262
247
  /**
@@ -48,7 +48,8 @@ export function* admitAndDeliver(deps, input) {
48
48
  * wait for the issuer to finish it, then admit and deliver ({@link admitAndDeliver}).
49
49
  *
50
50
  * @throws ValidationError for a Grant that is not addressed to a local identifier, one this runtime issued, or a
51
- * group identifier.
51
+ * group identifier; its subclass `KramRefusedError` (reason `lifetime`, with the deadline) for a Grant whose
52
+ * deadline passed before the stores could say more (its cache row pruned or the Grant ejected from escrow).
52
53
  */
53
54
  export function* receiveIssuance(ops, input) {
54
55
  const { grant, timeoutMs } = input, { deps } = ops;
@@ -28,9 +28,10 @@
28
28
  */
29
29
  import { bytesEqual, collectPayload, framePayload, parseBytes, SerderKERI, Vrsn_2_0 } from "cesr-ts";
30
30
  import { call, race } from "effection";
31
+ import { QUERY_RESEND_MS, ThresholdQueryResend } from "../../acdc/query-resend.js";
31
32
  import { Unblind } from "../../acdc/unblinding.js";
32
33
  import { ValidationError } from "../../core/errors.js";
33
- import { singleKeyQueryWindow } from "../../core/mailbox/kram-policy.js";
34
+ import { multiKeyQueryWindow, singleKeyQueryWindow } from "../../core/mailbox/kram-policy.js";
34
35
  import { timestampMicros } from "../../time/mod.js";
35
36
  import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
36
37
  import { MULTISIG_QRY_ROUTE, MULTISIG_RPY_ROUTE, multisigExnBody } from "../multisig/grouping.js";
@@ -38,8 +39,13 @@ import { remoteGroupMembers } from "../multisig/multisig-messaging.js";
38
39
  import { envelopeFromFrame } from "../protocol/parsering.js";
39
40
  import { DeliveryOutbox } from "../runtime/delivery-outbox.js";
40
41
  import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
41
- /** Milliseconds between resends of the unchanged disclosee query, as `tufa ipex unblind` does. */
42
- export const UNBLIND_QUERY_RESEND_MS = 1000;
42
+ /**
43
+ * Milliseconds between the first resends of the disclosee query, as `tufa ipex unblind` does. A single-signer
44
+ * requester sends the signed bytes unchanged while they are timely and re-dates them once older than the
45
+ * single-key window. A group requester cannot re-date (re-signing needs the members), so it resends the same
46
+ * bytes, doubling this interval up to a cap and stopping once the query is older than the multi-key window.
47
+ */
48
+ export const UNBLIND_QUERY_RESEND_MS = QUERY_RESEND_MS;
43
49
  /** The key of one party's operation on a Grant. */
44
50
  export function unblindingKey(grant, role) {
45
51
  return `${role}:${grant}`;
@@ -96,6 +102,13 @@ class GroupUnblind {
96
102
  writable: true,
97
103
  value: false
98
104
  });
105
+ /** The frozen threshold query a requester resends unchanged, with its backoff schedule. */
106
+ Object.defineProperty(this, "resend", {
107
+ enumerable: true,
108
+ configurable: true,
109
+ writable: true,
110
+ value: new ThresholdQueryResend()
111
+ });
99
112
  }
100
113
  }
101
114
  const EXPIRED = "The presentation reached its original exchange deadline";
@@ -186,14 +199,16 @@ export class UnblindingOperations {
186
199
  }
187
200
  /**
188
201
  * Whether a pass has work: a live operation, or an owner holds an authenticated peer query whose reply is
189
- * owed (an in-memory walk of the owners, no store read).
202
+ * owed (an in-memory walk of the owners, no store read). Finding what is owed reprocesses held queries, which
203
+ * answers them (signs), so an owner whose habitat cannot sign yet is skipped here as it is in `adopt`.
190
204
  */
191
205
  active() {
192
206
  if (this.live.size > 0)
193
207
  return true;
194
- for (const owner of this.deps.unblinds.unblindOwners())
195
- if (owner.awaitingReply().length)
208
+ for (const owner of this.deps.unblinds.unblindOwners()) {
209
+ if (owner.hab.canSign() && owner.awaitingReply().length)
196
210
  return true;
211
+ }
197
212
  return false;
198
213
  }
199
214
  /** Advance every live operation by one pass, each bounded by its Grant deadline. */
@@ -208,6 +223,9 @@ export class UnblindingOperations {
208
223
  const left = op.expires - BigInt(deps.clock.now()) * 1000n;
209
224
  if (left <= 0n)
210
225
  this.end(op, "expired", this.expiry(op));
226
+ // A habitat that cannot sign yet waits for its signer; only its Grant deadline ends the operation meanwhile.
227
+ else if (!op.hab.canSign())
228
+ continue;
211
229
  else if ((yield* race([this.advance(op), this.timer(Number(left) / 1000)])) === "expired") {
212
230
  this.end(op, "expired", this.expiry(op));
213
231
  }
@@ -233,6 +251,10 @@ export class UnblindingOperations {
233
251
  adopt() {
234
252
  const { deps } = this;
235
253
  for (const owner of deps.unblinds.unblindOwners()) {
254
+ // Finding what is owed reprocesses held queries, which answers them (signs); an owner whose habitat cannot
255
+ // sign yet is asked on a later pass.
256
+ if (!owner.hab.canSign())
257
+ continue;
236
258
  for (const { xid, grant } of owner.awaitingReply()) {
237
259
  if (this.live.has(unblindingKey(grant, "responder")))
238
260
  continue;
@@ -297,6 +319,10 @@ export class UnblindingOperations {
297
319
  this.end(op, "abandoned", reason);
298
320
  return bound.map(op => op.grant);
299
321
  }
322
+ /** Grants of the live Unblind operations whose habitat is `pre`. */
323
+ grantsOf(pre) {
324
+ return [...new Set([...this.live.values()].filter(op => op.hab.pre === pre).map(op => op.grant))];
325
+ }
300
326
  /** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
301
327
  release(op) {
302
328
  this.live.delete(unblindingKey(op.grant, op.role));
@@ -396,7 +422,7 @@ export class UnblindingOperations {
396
422
  yield* broadcast(message.msg);
397
423
  }
398
424
  else if (message.kin === "query")
399
- op.query = message.msg;
425
+ g.resend.set(message.msg);
400
426
  else if (!g.finished) {
401
427
  yield* owner.finish(xid); // Validate exact evidence; no issuer-state admission policy here.
402
428
  g.finished = true;
@@ -415,10 +441,12 @@ export class UnblindingOperations {
415
441
  yield* owner.finish(xid);
416
442
  return this.finishedStep(op);
417
443
  }
418
- // A threshold query cannot be re-dated by one member: it is resent unchanged.
419
- if (op.query && deps.clock.now() >= op.nextQuery) {
420
- yield* send(op.peer, op.query);
421
- op.nextQuery = deps.clock.now() + UNBLIND_QUERY_RESEND_MS;
444
+ // A threshold query cannot be re-dated by one member: it is resent unchanged, with backoff, while the
445
+ // discloser can still accept it. The operation keeps waiting past that, to its own deadline.
446
+ const now = deps.clock.now();
447
+ if (g.resend.ready(now) && g.resend.fresh(now, multiKeyQueryWindow(deps.hby.db, Unblind.Route))) {
448
+ yield* send(op.peer, g.resend.query);
449
+ g.resend.sent(now, UNBLIND_QUERY_RESEND_MS);
422
450
  }
423
451
  return "stepped";
424
452
  }