keri-ts 2.2.4 → 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +14 -14
  2. package/esm/keri/npm/src/acdc/registering.js +19 -0
  3. package/esm/keri/npm/src/acdc/salting.js +4 -0
  4. package/esm/keri/npm/src/app/bootstrap/authenticating.js +32 -10
  5. package/esm/keri/npm/src/app/bootstrap/oobiery.js +363 -30
  6. package/esm/keri/npm/src/app/credentials/credential-importing.js +33 -4
  7. package/esm/keri/npm/src/app/credentials/root-replicator.js +27 -15
  8. package/esm/keri/npm/src/app/credentials/schema-resolving.js +5 -1
  9. package/esm/keri/npm/src/app/credentials/tel-publishing.js +16 -7
  10. package/esm/keri/npm/src/app/delivery/forwarding.js +177 -43
  11. package/esm/keri/npm/src/app/delivery/relaying.js +232 -0
  12. package/esm/keri/npm/src/app/delivery/respondant.js +8 -5
  13. package/esm/keri/npm/src/app/exchanges/exchange-messaging.js +1 -1
  14. package/esm/keri/npm/src/app/exchanges/exchanging.js +65 -9
  15. package/esm/keri/npm/src/app/identity/delegating.js +66 -9
  16. package/esm/keri/npm/src/app/identity/habbing.js +190 -71
  17. package/esm/keri/npm/src/app/identity/keeping.js +111 -39
  18. package/esm/keri/npm/src/app/identity/retire-identifier.js +98 -0
  19. package/esm/keri/npm/src/app/ipex/disclosure-replaying.js +116 -0
  20. package/esm/keri/npm/src/app/ipex/group-ipex.js +122 -25
  21. package/esm/keri/npm/src/app/ipex/group-issuance.js +109 -23
  22. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +36 -22
  23. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +53 -3
  24. package/esm/keri/npm/src/app/ipex/ipex-v2.js +7 -2
  25. package/esm/keri/npm/src/app/ipex/issuance.js +131 -35
  26. package/esm/keri/npm/src/app/ipex/pending-presentations.js +4 -2
  27. package/esm/keri/npm/src/app/ipex/presenting.js +3 -18
  28. package/esm/keri/npm/src/app/ipex/receive-issuance.js +2 -1
  29. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +16 -3
  30. package/esm/keri/npm/src/app/mailbox/agent-inbox.js +292 -0
  31. package/esm/keri/npm/src/app/mailbox/forward-handler.js +298 -52
  32. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +26 -31
  33. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +70 -62
  34. package/esm/keri/npm/src/app/mailbox/mailboxing.js +20 -4
  35. package/esm/keri/npm/src/app/multisig/counseling.js +71 -27
  36. package/esm/keri/npm/src/app/multisig/group-members.js +41 -5
  37. package/esm/keri/npm/src/app/multisig/grouping.js +50 -2
  38. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +3 -18
  39. package/esm/keri/npm/src/app/multisig/proposing.js +1 -16
  40. package/esm/keri/npm/src/app/multisig/rejoin-group.js +99 -0
  41. package/esm/keri/npm/src/app/multisig/replace-member.js +29 -8
  42. package/esm/keri/npm/src/app/protocol/parsering.js +45 -17
  43. package/esm/keri/npm/src/app/protocol/protocol-processing.js +47 -21
  44. package/esm/keri/npm/src/app/queries/query-replies.js +0 -22
  45. package/esm/keri/npm/src/app/queries/relay-refresh.js +81 -0
  46. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +180 -34
  47. package/esm/keri/npm/src/app/runtime/agent-runtime.js +41 -72
  48. package/esm/keri/npm/src/app/runtime/cue-runtime.js +45 -5
  49. package/esm/keri/npm/src/app/runtime/operation-owners.js +26 -56
  50. package/esm/keri/npm/src/app/runtime/workflow-contract.js +7 -4
  51. package/esm/keri/npm/src/app/transport/http-client.js +20 -2
  52. package/esm/keri/npm/src/app/transport/message-source.js +2 -1
  53. package/esm/keri/npm/src/app/transport/messengering.js +39 -8
  54. package/esm/keri/npm/src/app/witness/ingress.js +7 -0
  55. package/esm/keri/npm/src/app/witness/receipt-client.js +43 -16
  56. package/esm/keri/npm/src/app/witness/receipting.js +62 -20
  57. package/esm/keri/npm/src/cli/messaging/multisig.js +4 -3
  58. package/esm/keri/npm/src/core/errors.js +73 -0
  59. package/esm/keri/npm/src/core/kel/delegation-ancestry.js +11 -2
  60. package/esm/keri/npm/src/core/kel/eventing.js +25 -28
  61. package/esm/keri/npm/src/core/kel/kever.js +15 -9
  62. package/esm/keri/npm/src/core/mailbox/kram-policy.js +8 -0
  63. package/esm/keri/npm/src/core/mailbox/kram-pruning.js +25 -6
  64. package/esm/keri/npm/src/core/mailbox/kram-time.js +34 -6
  65. package/esm/keri/npm/src/core/mailbox/kraming.js +58 -2
  66. package/esm/keri/npm/src/core/routing.js +18 -25
  67. package/esm/keri/npm/src/core/scheming.js +12 -5
  68. package/esm/keri/npm/src/core/wire-fields.js +126 -0
  69. package/esm/keri/npm/src/db/basing.js +4 -13
  70. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +3 -2
  71. package/esm/keri/npm/src/db/core/webdber.js +85 -27
  72. package/esm/keri/npm/src/db/mailbox/mailboxing.js +13 -0
  73. package/esm/keri/npm/src/db/subing.js +4 -0
  74. package/esm/keri/npm/src/runtime/index.js +11 -5
  75. package/esm/keri/npm/src/time/mod.js +5 -0
  76. package/esm/keri/npm/src/vdr/credential-tsn-record.js +2 -7
  77. package/esm/keri/npm/src/vdr/eventing.js +28 -19
  78. package/esm/keri/npm/src/vdr/registry-tsn-record.js +3 -13
  79. package/esm/keri/npm/src/version.js +2 -2
  80. package/package.json +1 -1
  81. package/types/_dnt.polyfills.d.ts +9 -9
  82. package/types/_dnt.polyfills.d.ts.map +1 -1
  83. package/types/acdc/registering.d.ts +6 -0
  84. package/types/acdc/registering.d.ts.map +1 -1
  85. package/types/acdc/salting.d.ts.map +1 -1
  86. package/types/app/bootstrap/authenticating.d.ts +12 -2
  87. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  88. package/types/app/bootstrap/oobiery.d.ts +144 -7
  89. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  90. package/types/app/credentials/credential-importing.d.ts +8 -0
  91. package/types/app/credentials/credential-importing.d.ts.map +1 -1
  92. package/types/app/credentials/root-replicator.d.ts +19 -8
  93. package/types/app/credentials/root-replicator.d.ts.map +1 -1
  94. package/types/app/credentials/schema-resolving.d.ts.map +1 -1
  95. package/types/app/credentials/tel-publishing.d.ts +4 -2
  96. package/types/app/credentials/tel-publishing.d.ts.map +1 -1
  97. package/types/app/delivery/forwarding.d.ts +69 -5
  98. package/types/app/delivery/forwarding.d.ts.map +1 -1
  99. package/types/app/delivery/relaying.d.ts +144 -0
  100. package/types/app/delivery/relaying.d.ts.map +1 -0
  101. package/types/app/delivery/respondant.d.ts +9 -2
  102. package/types/app/delivery/respondant.d.ts.map +1 -1
  103. package/types/app/exchanges/exchanging.d.ts +36 -0
  104. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  105. package/types/app/identity/delegating.d.ts +6 -0
  106. package/types/app/identity/delegating.d.ts.map +1 -1
  107. package/types/app/identity/habbing.d.ts +106 -19
  108. package/types/app/identity/habbing.d.ts.map +1 -1
  109. package/types/app/identity/keeping.d.ts +54 -19
  110. package/types/app/identity/keeping.d.ts.map +1 -1
  111. package/types/app/identity/retire-identifier.d.ts +24 -0
  112. package/types/app/identity/retire-identifier.d.ts.map +1 -0
  113. package/types/app/ipex/disclosure-replaying.d.ts +61 -0
  114. package/types/app/ipex/disclosure-replaying.d.ts.map +1 -0
  115. package/types/app/ipex/group-ipex.d.ts +60 -14
  116. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  117. package/types/app/ipex/group-issuance.d.ts +48 -6
  118. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  119. package/types/app/ipex/ipex-discloser.d.ts +14 -2
  120. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  121. package/types/app/ipex/ipex-grouping.d.ts +23 -2
  122. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  123. package/types/app/ipex/ipex-v2.d.ts +5 -2
  124. package/types/app/ipex/ipex-v2.d.ts.map +1 -1
  125. package/types/app/ipex/issuance.d.ts +35 -11
  126. package/types/app/ipex/issuance.d.ts.map +1 -1
  127. package/types/app/ipex/pending-presentations.d.ts +4 -2
  128. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  129. package/types/app/ipex/presenting.d.ts.map +1 -1
  130. package/types/app/ipex/receive-issuance.d.ts +2 -1
  131. package/types/app/ipex/receive-issuance.d.ts.map +1 -1
  132. package/types/app/ipex/unblinding-workflow.d.ts +4 -1
  133. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  134. package/types/app/mailbox/agent-inbox.d.ts +126 -0
  135. package/types/app/mailbox/agent-inbox.d.ts.map +1 -0
  136. package/types/app/mailbox/forward-handler.d.ts +114 -14
  137. package/types/app/mailbox/forward-handler.d.ts.map +1 -1
  138. package/types/app/mailbox/kel-publisher.d.ts +12 -22
  139. package/types/app/mailbox/kel-publisher.d.ts.map +1 -1
  140. package/types/app/mailbox/mailbox-poller.d.ts +40 -24
  141. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  142. package/types/app/mailbox/mailboxing.d.ts +19 -5
  143. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  144. package/types/app/multisig/counseling.d.ts +32 -11
  145. package/types/app/multisig/counseling.d.ts.map +1 -1
  146. package/types/app/multisig/group-members.d.ts +17 -1
  147. package/types/app/multisig/group-members.d.ts.map +1 -1
  148. package/types/app/multisig/grouping.d.ts +17 -1
  149. package/types/app/multisig/grouping.d.ts.map +1 -1
  150. package/types/app/multisig/multisig-workflows.d.ts +0 -8
  151. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  152. package/types/app/multisig/proposing.d.ts +4 -19
  153. package/types/app/multisig/proposing.d.ts.map +1 -1
  154. package/types/app/multisig/rejoin-group.d.ts +119 -0
  155. package/types/app/multisig/rejoin-group.d.ts.map +1 -0
  156. package/types/app/multisig/replace-member.d.ts +15 -1
  157. package/types/app/multisig/replace-member.d.ts.map +1 -1
  158. package/types/app/protocol/parsering.d.ts +21 -2
  159. package/types/app/protocol/parsering.d.ts.map +1 -1
  160. package/types/app/protocol/protocol-processing.d.ts +25 -5
  161. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  162. package/types/app/queries/query-replies.d.ts +4 -8
  163. package/types/app/queries/query-replies.d.ts.map +1 -1
  164. package/types/app/queries/relay-refresh.d.ts +26 -0
  165. package/types/app/queries/relay-refresh.d.ts.map +1 -0
  166. package/types/app/queries/witness-inquisitor.d.ts +56 -5
  167. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  168. package/types/app/runtime/agent-runtime.d.ts +34 -3
  169. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  170. package/types/app/runtime/cue-runtime.d.ts +15 -3
  171. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  172. package/types/app/runtime/operation-owners.d.ts +16 -26
  173. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  174. package/types/app/runtime/workflow-contract.d.ts +21 -6
  175. package/types/app/runtime/workflow-contract.d.ts.map +1 -1
  176. package/types/app/transport/http-client.d.ts +9 -0
  177. package/types/app/transport/http-client.d.ts.map +1 -1
  178. package/types/app/transport/message-source.d.ts +6 -3
  179. package/types/app/transport/message-source.d.ts.map +1 -1
  180. package/types/app/transport/messengering.d.ts +10 -1
  181. package/types/app/transport/messengering.d.ts.map +1 -1
  182. package/types/app/witness/ingress.d.ts +1 -0
  183. package/types/app/witness/ingress.d.ts.map +1 -1
  184. package/types/app/witness/receipt-client.d.ts +19 -1
  185. package/types/app/witness/receipt-client.d.ts.map +1 -1
  186. package/types/app/witness/receipting.d.ts +24 -1
  187. package/types/app/witness/receipting.d.ts.map +1 -1
  188. package/types/cli/messaging/multisig.d.ts.map +1 -1
  189. package/types/core/cues.d.ts +53 -1
  190. package/types/core/cues.d.ts.map +1 -1
  191. package/types/core/errors.d.ts +32 -0
  192. package/types/core/errors.d.ts.map +1 -1
  193. package/types/core/kel/delegation-ancestry.d.ts.map +1 -1
  194. package/types/core/kel/eventing.d.ts.map +1 -1
  195. package/types/core/kel/kever-decisions.d.ts +3 -1
  196. package/types/core/kel/kever-decisions.d.ts.map +1 -1
  197. package/types/core/kel/kever.d.ts +7 -1
  198. package/types/core/kel/kever.d.ts.map +1 -1
  199. package/types/core/mailbox/kram-policy.d.ts +6 -0
  200. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  201. package/types/core/mailbox/kram-pruning.d.ts +5 -1
  202. package/types/core/mailbox/kram-pruning.d.ts.map +1 -1
  203. package/types/core/mailbox/kram-time.d.ts +26 -2
  204. package/types/core/mailbox/kram-time.d.ts.map +1 -1
  205. package/types/core/mailbox/kraming.d.ts +14 -1
  206. package/types/core/mailbox/kraming.d.ts.map +1 -1
  207. package/types/core/routing.d.ts +3 -7
  208. package/types/core/routing.d.ts.map +1 -1
  209. package/types/core/scheming.d.ts.map +1 -1
  210. package/types/core/wire-fields.d.ts +56 -0
  211. package/types/core/wire-fields.d.ts.map +1 -0
  212. package/types/db/basing.d.ts.map +1 -1
  213. package/types/db/core/indexeddb-persistence.d.ts +3 -2
  214. package/types/db/core/indexeddb-persistence.d.ts.map +1 -1
  215. package/types/db/core/webdber.d.ts +32 -6
  216. package/types/db/core/webdber.d.ts.map +1 -1
  217. package/types/db/exchange/exchange-records.d.ts +4 -0
  218. package/types/db/exchange/exchange-records.d.ts.map +1 -1
  219. package/types/db/mailbox/mailboxing.d.ts +10 -1
  220. package/types/db/mailbox/mailboxing.d.ts.map +1 -1
  221. package/types/db/subing.d.ts +2 -0
  222. package/types/db/subing.d.ts.map +1 -1
  223. package/types/runtime/index.d.ts +20 -14
  224. package/types/runtime/index.d.ts.map +1 -1
  225. package/types/time/mod.d.ts +2 -0
  226. package/types/time/mod.d.ts.map +1 -1
  227. package/types/vdr/credential-tsn-record.d.ts.map +1 -1
  228. package/types/vdr/eventing.d.ts.map +1 -1
  229. package/types/vdr/registry-tsn-record.d.ts.map +1 -1
  230. package/types/version.d.ts +2 -2
  231. package/esm/keri/npm/src/app/runtime/signer-conditions.js +0 -51
  232. package/types/app/runtime/signer-conditions.d.ts +0 -32
  233. package/types/app/runtime/signer-conditions.d.ts.map +0 -1
@@ -88,6 +88,32 @@ export function* storedExchangeWire(retained, gvrsn = Vrsn_2_0) {
88
88
  frame.nests = nests;
89
89
  return yield* call(() => collectPayload(framePayload(frame)));
90
90
  }
91
+ /**
92
+ * Extend a frozen Grant's exact origin proof with its presented-state opening and the KEL seal of the registry update
93
+ * that commits to it; the Grant body and its SAID are unchanged. Ordinary intake still authenticates the supplied
94
+ * presentation evidence. Reads no store and signs nothing.
95
+ *
96
+ * @throws ValidationError when the opening does not target this exact Grant's disclosed Issuee in the `presented` state.
97
+ */
98
+ export function* attachPresentationProof(prepared, opening, source) {
99
+ const frames = parseBytes(prepared.raw), node = frames[0].body;
100
+ if (!node.issuee || opening.td.nonce !== prepared.exn.said || opening.ts.label !== "presented") {
101
+ throw new ValidationError("Presentation proof must target this exact Grant and disclosed Issuee.");
102
+ }
103
+ const seal = Counter.enclose({
104
+ code: "SealSourceTriples",
105
+ qb64: concatBytes(new Prefixer({ qb64: node.issuee }).qb64b, source.s.qb64b, source.d.qb64b),
106
+ version: Vrsn_2_0,
107
+ });
108
+ const first = concatBytes(node.raw, Counter.enclose({
109
+ qb64: concatBytes(...frames[0].attachments.map(group => group.raw), publicOpeningAttachment(opening), seal),
110
+ version: Vrsn_2_0,
111
+ }));
112
+ const rest = yield* call(() => Promise.all(frames.slice(1).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
113
+ const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), first, ...rest);
114
+ const payloads = yield* call(() => Promise.all(parseBytes(raw).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
115
+ return { exn: prepared.exn, raw, nests: concatBytes(...payloads) };
116
+ }
91
117
  /** Borrow concrete owners only. No constructor opens a store, starts a worker or loads registry secrets. */
92
118
  export class IpexDiscloser {
93
119
  constructor(hby, registry, exc, receiptor, custody, views) {
@@ -201,7 +227,15 @@ export class IpexDiscloser {
201
227
  }
202
228
  /** Carry selected public evidence and root-first delegation ancestry; receiver intake still verifies every approval. */
203
229
  proofSupport(raw, hab) {
204
- const aids = new Set([hab.pre]), registries = new Set();
230
+ return this.proofSupportFor(raw, [hab.pre]);
231
+ }
232
+ /**
233
+ * `proofSupport` for explicitly named parties instead of one habitat: the KELs of `parties` and of every identifier
234
+ * the ACDC frames in `raw` name, each after its delegation ancestry, then the accepted TEL of every registry they
235
+ * name. Reads accepted state only and signs nothing, so it needs no keys.
236
+ */
237
+ proofSupportFor(raw, parties) {
238
+ const aids = new Set(parties), registries = new Set();
205
239
  for (const frame of parseBytes(raw)) {
206
240
  if (!(frame.body instanceof SerderACDC))
207
241
  continue;
@@ -399,26 +433,6 @@ export class IpexDiscloser {
399
433
  const prior = yield* this.prior(priorSaid);
400
434
  return ipexExnBody(sender, `/ipex/${verb}`, [], { message, prior, recipient });
401
435
  }
402
- /** Extend the frozen Grant's exact origin proof; ordinary intake still authenticates the supplied presentation evidence. */
403
- *attachPresentationProof(prepared, opening, source) {
404
- const frames = parseBytes(prepared.raw), node = frames[0].body;
405
- if (!node.issuee || opening.td.nonce !== prepared.exn.said || opening.ts.label !== "presented") {
406
- throw new ValidationError("Presentation proof must target this exact Grant and disclosed Issuee.");
407
- }
408
- const seal = Counter.enclose({
409
- code: "SealSourceTriples",
410
- qb64: concatBytes(new Prefixer({ qb64: node.issuee }).qb64b, source.s.qb64b, source.d.qb64b),
411
- version: Vrsn_2_0,
412
- });
413
- const first = concatBytes(node.raw, Counter.enclose({
414
- qb64: concatBytes(...frames[0].attachments.map(group => group.raw), publicOpeningAttachment(opening), seal),
415
- version: Vrsn_2_0,
416
- }));
417
- const rest = yield* call(() => Promise.all(frames.slice(1).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
418
- const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), first, ...rest);
419
- const payloads = yield* call(() => Promise.all(parseBytes(raw).map(frame => collectPayload(nestedPayload(frame, Vrsn_2_0)))));
420
- return { exn: prepared.exn, raw, nests: concatBytes(...payloads) };
421
- }
422
436
  /** Select immutable admitted/transferred nests; proof refresh changes attachments only when explicitly requested. */
423
437
  *selected(selection, offer = false) {
424
438
  if ([selection.said, selection.fromGrant, selection.transferred].filter(value => value !== undefined).length !== 1) {
@@ -540,7 +554,7 @@ export class IpexDiscloser {
540
554
  if (reg.store.seqEvent(reg.regk, Number(BigInt(`0x${update.sad.n}`)))?.said !== update.said) {
541
555
  throw new ValidationError("Presentation BUP remains pending authenticated acceptance.");
542
556
  }
543
- ({ raw, nests } = yield* this.attachPresentationProof({ exn, raw, nests }, proof, seal));
557
+ ({ raw, nests } = yield* attachPresentationProof({ exn, raw, nests }, proof, seal));
544
558
  }
545
559
  const source = exn.sad.a.ax?.[0]
546
560
  ? yield* anchorExchange(this.hby, this.receiptor, hab, exn, options.auths)
@@ -12,7 +12,7 @@ import { call } from "effection";
12
12
  import { evaluateChain } from "../../acdc/chaining.js";
13
13
  import { resolveAttachmentGvrsn } from "../../core/attachment-countering.js";
14
14
  import { TransIdxSigGroup } from "../../core/dispatch.js";
15
- import { ValidationError } from "../../core/errors.js";
15
+ import { KramRefusedError, ValidationError } from "../../core/errors.js";
16
16
  import { verifyExchangeProofs } from "../../core/exchange/exchange-proofing.js";
17
17
  import { exchange, serializeMessage } from "../../core/exchange/protocol-exchanging.js";
18
18
  import { Kever } from "../../core/kel/kever.js";
@@ -74,7 +74,19 @@ export function requireAcdcV2GroupUse(hby, protocol, embedded) {
74
74
  throw new ValidationError("ACDC v2 group graph is unavailable for completed use.");
75
75
  }
76
76
  }
77
- /** Approve exactly one ACDC v2 IPEX stream before adding this member's signature; no new acceptance rows are manufactured. */
77
+ /** KRAM refusals after which the message can no longer be accepted; a `replay` is routine, not one of them. */
78
+ const KRAM_EXPIRY_REASONS = new Set(["timestamp", "timeliness", "unknownThread", "lifetime"]);
79
+ /** Whether `decision` is this receiver's KRAM refusing a message for its time, its thread or its lifetime. */
80
+ function isKramExpiry(decision) {
81
+ return !!decision && decision.kind === "drop" && "reason" in decision && KRAM_EXPIRY_REASONS.has(decision.reason);
82
+ }
83
+ /**
84
+ * Approve exactly one ACDC v2 IPEX stream before adding this member's signature; no new acceptance rows are manufactured.
85
+ *
86
+ * @returns This member's accepted wire, or null while its evidence or dependencies are missing.
87
+ * @throws KramRefusedError when this member's own KRAM refuses the contribution for its time, thread or exchange
88
+ * lifetime: the message can no longer be accepted here. A `replay` refusal is routine and is not thrown.
89
+ */
78
90
  export function* prepareAcdcV2GroupIpex(hby, protocol, group, raw) {
79
91
  const frames = parseBytes(concatBytes(Counter.makeGVC(Vrsn_2_0), raw));
80
92
  const frame = frames[0], embedded = frame?.body;
@@ -136,7 +148,11 @@ export function* prepareAcdcV2GroupIpex(hby, protocol, group, raw) {
136
148
  };
137
149
  const attachments = yield* call(() => collectPayload(attachmentPayload(approved, { gvrsn: Vrsn_2_0 })));
138
150
  const wire = concatBytes(embedded.raw, attachments);
139
- yield* new Reactor(protocol).processBytes(wire, { local: true });
151
+ // Dispatch the frame itself, not its bytes, so this member's own KRAM decision is visible. A refusal here is
152
+ // not a remote outcome to skip: the contribution was never accepted and nothing else will complete the message.
153
+ const { decision } = yield* protocol.dispatch(parseBytes(wire)[0], { local: true }, new Reactor(protocol).kvy);
154
+ if (isKramExpiry(decision))
155
+ throw new KramRefusedError(embedded.said, decision);
140
156
  yield* protocol.processEscrowsOnce();
141
157
  return wire;
142
158
  }
@@ -457,6 +473,40 @@ export function* approveStoredMultisigIpexWrappers(hby, protocol, embeddedSaid,
457
473
  yield* approveIpexChild(hby, protocol, groupHab, child, wrapper, reger);
458
474
  }
459
475
  }
476
+ /**
477
+ * The copies this member received of each v2 group IPEX message it has not accepted: per message, the child of each
478
+ * stored, authenticated `/multisig/exn` wrapper that carries it, with its signatures and nests. A pure read; whether a
479
+ * message may be taken from its copies is {@link groupIpexFollowable}'s rule.
480
+ */
481
+ export function unacceptedGroupIpexCopies(hby, exchanger) {
482
+ const copies = new Map();
483
+ for (const [, wrapper] of hby.db.exns.getTopItemIter()) {
484
+ if (wrapper.route !== MULTISIG_EXN_ROUTE || wrapper.pvrsn.major !== 2)
485
+ continue;
486
+ const said = storedMultisigIpexBody(hby.db, wrapper)?.said;
487
+ if (!said || exchanger.complete(said) || !exchanger.verifyAccepted(wrapper.said, MULTISIG_EXN_ROUTE))
488
+ continue;
489
+ copies.set(said, [...copies.get(said) ?? [], loadNests(hby.db, wrapper.said)[0]]);
490
+ }
491
+ return [...copies.values()];
492
+ }
493
+ /**
494
+ * Accept a group IPEX message from the copies this member received, adding no signature: dispatch each copy with its
495
+ * own proofs, then advance escrows so that copies whose signatures only together meet the threshold complete. The
496
+ * caller has checked {@link groupIpexFollowable}.
497
+ *
498
+ * @throws KramRefusedError when this member's own KRAM refuses a copy for its time, thread or exchange lifetime: the
499
+ * message can no longer be accepted here.
500
+ */
501
+ export function* acceptGroupIpexCopies(protocol, copies) {
502
+ const kvy = new Reactor(protocol).kvy;
503
+ for (const copy of copies) {
504
+ const { decision } = yield* protocol.dispatch(copy, { local: true }, kvy);
505
+ if (isKramExpiry(decision))
506
+ throw new KramRefusedError(copy.body.said, decision);
507
+ }
508
+ yield* protocol.processEscrowsOnce();
509
+ }
460
510
  /**
461
511
  * Return the first incomplete stored `/multisig/exn` wrapping `/ipex/*`.
462
512
  *
@@ -464,8 +464,13 @@ export class IpexV2Handler {
464
464
  }
465
465
  return { pending, registries };
466
466
  }
467
- /** Notify only after Exchanger has durably accepted the parent and its evidence. */
468
- handle({ serder }) {
467
+ /**
468
+ * Notify only after Exchanger has durably accepted the parent and its evidence. A retained exchange (history a
469
+ * local import replayed, such as an already admitted Grant and its Admit) is not news: no notice asks anyone to act.
470
+ */
471
+ handle({ serder, retained }) {
472
+ if (retained)
473
+ return;
469
474
  this.notifier?.add({ r: `/exn${serder.route}`, d: serder.said, m: serder.sad.a.m });
470
475
  }
471
476
  /** Every disclosed own schema vets before presentation and issuer graph reduction. */
@@ -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,14 +69,14 @@ 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 { KramRefusedError, ValidationError } from "../../core/errors.js";
69
73
  import { CREDENTIAL_MAILBOX_TOPIC } from "../../core/mailbox/mailbox-topics.js";
70
74
  import { timestampMicros } from "../../time/mod.js";
71
75
  import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
72
76
  import { remoteGroupMembers } from "../multisig/multisig-messaging.js";
73
77
  import { DeliveryOutbox, } from "../runtime/delivery-outbox.js";
74
78
  import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
75
- import { deriveGroupCustody, GroupIssuance, GroupParty, registerGroupRoutes, stepGroup, } from "./group-issuance.js";
79
+ import { awaitsGroupProof, beginGroupProofFollow, deriveGroupCustody, GroupIssuance, GroupParty, registerGroupRoutes, stepGroup, storedGroupProof, } from "./group-issuance.js";
76
80
  import { IpexAdmitter } from "./ipex-admitting.js";
77
81
  import { exchangeWire, IpexDiscloser, storedExchangeWire } from "./ipex-discloser.js";
78
82
  /**
@@ -83,9 +87,9 @@ import { exchangeWire, IpexDiscloser, storedExchangeWire } from "./ipex-disclose
83
87
  */
84
88
  export const ISSUANCE_QUERY_RESEND_MS = 1000;
85
89
  /**
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).
90
+ * Milliseconds between scans of the pending exchange escrow for Grants a restarted process can finish and group
91
+ * Grants whose final proof a member follows. The scan repeats because what makes a Grant resumable can arrive after
92
+ * the restart (an issuer's update accepted once the other members approve it), and a custodian's proof arrives late.
89
93
  */
90
94
  export const ISSUANCE_SWEEP_MS = 5_000;
91
95
  /** The key of one party's operation on a Grant. */
@@ -219,23 +223,27 @@ export class IssuanceOperations {
219
223
  remember(grant, ended) {
220
224
  this.ended.set(partyKey(grant, ended.role), ended);
221
225
  }
222
- /** Whether a pass has work: a live operation, or the one-time scan for resumable Grants has not run. */
226
+ /** Whether a pass has work: a live operation, an adoption still owed, or a sweep of the pending Grants is due. */
223
227
  active() {
224
228
  return this.live.size > 0 || this.stale.length > 0 || this.deps.clock.now() >= this.nextSweep;
225
229
  }
226
230
  /**
227
231
  * 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.
232
+ * Every {@link ISSUANCE_SWEEP_MS} a pass also scans the pending exchange escrow once ({@link sweptGrants}): it
233
+ * adopts the Grants the stores prove resumable and begins the proof-only follow of the group Grants whose
234
+ * custodian's proof is stored. Neither signs nor sends anything.
230
235
  */
231
236
  *processOnce() {
232
237
  if (this.deps.clock.now() >= this.nextSweep) {
233
238
  this.nextSweep = this.deps.clock.now() + ISSUANCE_SWEEP_MS;
234
239
  const known = new Set(this.stale.map(({ grant, role }) => partyKey(grant, role)));
235
- this.stale.push(...resumableGrants(this.deps).filter(({ grant, role }) => {
240
+ const swept = sweptGrants(this.deps, (grant, role) => {
236
241
  const key = partyKey(grant, role);
237
- return !this.live.has(key) && !known.has(key) && !this.refused.has(key) && !this.ended.has(key);
238
- }));
242
+ return this.live.has(key) || known.has(key) || this.refused.has(key) || this.ended.has(key);
243
+ });
244
+ this.stale.push(...swept.resumable);
245
+ for (const found of swept.follows)
246
+ this.follow(found);
239
247
  }
240
248
  for (const found of [...this.stale]) {
241
249
  if (yield* this.settle(found))
@@ -249,6 +257,10 @@ export class IssuanceOperations {
249
257
  const left = op.expires === null ? null : op.expires - BigInt(this.deps.clock.now()) * 1000n;
250
258
  if (left !== null && left <= 0n)
251
259
  this.end(op, "expired", EXPIRED);
260
+ // A habitat that cannot sign yet waits for its signer; only its Grant deadline ends the operation meanwhile.
261
+ // A proof-only follow signs nothing, so it proceeds without one.
262
+ else if (!op.hab.canSign() && op.group?.mode !== "proofOnly")
263
+ continue;
252
264
  else if ((yield* race([this.advance(op), ...(left === null ? [] : [this.timer(Number(left) / 1000)])])) === "expired")
253
265
  this.end(op, "expired", EXPIRED);
254
266
  }
@@ -263,7 +275,8 @@ export class IssuanceOperations {
263
275
  /** End a live operation: release its DH approval, remember why until the process ends, tell the sink. */
264
276
  end(op, state, reason) {
265
277
  this.release(op);
266
- this.ended.set(partyKey(op.grant, op.role), { role: op.role, state, reason });
278
+ const deadline = state === "expired" ? op.deadline : null;
279
+ this.ended.set(partyKey(op.grant, op.role), { role: op.role, state, reason, ...(deadline && { deadline }) });
267
280
  this.deps.changes?.push({ kind: "issuance", id: op.grant });
268
281
  }
269
282
  /**
@@ -277,6 +290,14 @@ export class IssuanceOperations {
277
290
  this.end(op, "abandoned", reason);
278
291
  return bound.map(op => op.grant);
279
292
  }
293
+ /** Grants of the live operations, and of those found resumable and not yet adopted, whose habitat is `pre`. */
294
+ grantsOf(pre) {
295
+ const grants = [...this.live.values()].filter(op => op.hab.pre === pre).map(op => op.grant);
296
+ for (const found of this.stale)
297
+ if (found.hab.pre === pre)
298
+ grants.push(found.grant);
299
+ return [...new Set(grants)];
300
+ }
280
301
  /** Drop the live entry and the DH approval; a missing approval means the owner already closed. */
281
302
  release(op) {
282
303
  this.live.delete(partyKey(op.grant, op.role));
@@ -324,6 +345,24 @@ export class IssuanceOperations {
324
345
  }
325
346
  return true;
326
347
  }
348
+ /**
349
+ * Begin the proof-only follow a sweep found, under the stored proof's election; it signs nothing, so it needs no
350
+ * unlocked Keeper. A refusal means the stores no longer support it: the Grant then reads from them and is not tried
351
+ * again in this process. Any other failure is this Grant's alone and ends it `failed`, as for an adoption.
352
+ */
353
+ follow(found) {
354
+ const key = partyKey(found.grant, found.role);
355
+ try {
356
+ beginGroupProofFollow(this, found.grant, found.role, found.election.coordinator.said);
357
+ }
358
+ catch (error) {
359
+ this.refused.add(key);
360
+ if (!(error instanceof ValidationError)) {
361
+ this.ended.set(key, { role: found.role, state: "failed", reason: message(error) });
362
+ this.deps.changes?.push({ kind: "issuance", id: found.grant });
363
+ }
364
+ }
365
+ }
327
366
  /** Rebuild the live entry of a resumable Grant from the stored Grant and the held roots. */
328
367
  *adopt({ hab, grant, role }) {
329
368
  const group = isLocalGroupHab(this.deps.hby, hab);
@@ -442,7 +481,8 @@ export class IssuanceOperations {
442
481
  }
443
482
  /**
444
483
  * Record one group operation the user consented to ({@link approveGroupIssuance},
445
- * {@link approveGroupIssuanceExchange}, {@link followGroupIssuanceProof}). A holder approves DH here, with its
484
+ * {@link approveGroupIssuanceExchange}), or a proof-only follow, which needs no consent ({@link beginGroupProofFollow},
485
+ * begun by the sweep or by {@link followGroupIssuanceProof}). A holder approves DH here, with its
446
486
  * consent; an issuer approves once the group has authenticated the Grant.
447
487
  */
448
488
  beginGroup(group) {
@@ -559,6 +599,20 @@ export class IssuanceOperations {
559
599
  function reached(clock, expires) {
560
600
  return expires !== null && BigInt(clock.now()) * 1000n >= expires;
561
601
  }
602
+ /**
603
+ * Refuse a Grant whose exchange deadline has passed with the KRAM lifetime refusal that names that deadline,
604
+ * so a caller learns why the Grant can no longer be acted on rather than that it is unknown. The deadline
605
+ * outlives the Grant's KRAM cache row and its ejection from escrow ({@link Exchanger.exchangeExpiry}). Returns
606
+ * when the Grant has no known deadline or is still inside it; the caller then raises its own refusal.
607
+ *
608
+ * @throws KramRefusedError (reason `lifetime`) once the deadline has passed.
609
+ */
610
+ function refuseLapsedGrant(deps, grant) {
611
+ const deadline = deps.exc.exchangeExpiry(grant);
612
+ if (deadline && reached(deps.clock, timestampMicros(deadline))) {
613
+ throw new KramRefusedError(grant, { kind: "drop", reason: "lifetime", deadline });
614
+ }
615
+ }
562
616
  /**
563
617
  * Whether a restarted process can complete this Grant without a new key exchange: it is pending inside its
564
618
  * deadline, `hab` is the party `role` names, and {@link resumableExchange} finds the roots. An issuer
@@ -585,23 +639,43 @@ function grantParties(hby, grant) {
585
639
  return issuer || holder ? { serder, issuer, holder } : null;
586
640
  }
587
641
  /**
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.
642
+ * One scan of the pending exchange escrow, made every {@link ISSUANCE_SWEEP_MS}: each local party of a pending Grant
643
+ * that `skip` does not exclude (the owner skips what it holds live, ended, refused or already found) is either
644
+ * resumable, or a group member that follows the custodian's stored proof ({@link followable}). A pure read.
590
645
  */
591
- function resumableGrants(deps) {
592
- const found = [];
646
+ function sweptGrants(deps, skip) {
647
+ const swept = { resumable: [], follows: [] };
593
648
  for (const [[grant]] of [...deps.hby.db.epse.getTopItemIter()]) {
594
649
  const parties = grantParties(deps.hby, grant);
595
650
  if (!parties)
596
651
  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" });
652
+ for (const [hab, role] of [[parties.issuer, "issuer"], [parties.holder, "holder"]]) {
653
+ if (!hab || skip(grant, role))
654
+ continue;
655
+ if (resumable(deps, hab, grant, role)) {
656
+ swept.resumable.push({ hab, grant, role });
657
+ continue;
658
+ }
659
+ const election = followable(deps, hab, grant, role, parties.serder);
660
+ if (election)
661
+ swept.follows.push({ hab, grant, role, election });
602
662
  }
603
663
  }
604
- return found;
664
+ return swept;
665
+ }
666
+ /**
667
+ * The election under which the local member of group `hab` follows `grant` to its final proof, or null: the party's
668
+ * side is not complete, the Grant is inside its exchange deadline, and the custodian's proof is stored
669
+ * ({@link storedGroupProof}). Only the proof decides it, never the member's weight: a member that should sign is not
670
+ * followed, because the proof exists only once the key exchange no longer wants any signature.
671
+ */
672
+ function followable(deps, hab, grant, role, serder) {
673
+ if (!isLocalGroupHab(deps.hby, hab) || partyComplete(deps, grant, role))
674
+ return null;
675
+ const deadline = deps.exc.exchangeDeadline(serder), expires = deadline ? timestampMicros(deadline) : null;
676
+ if (expires === null || reached(deps.clock, expires))
677
+ return null;
678
+ return storedGroupProof(deps, hab, grant);
605
679
  }
606
680
  /** The terminal reading of a pending Grant no process can complete: ended by a restart, or by another exchange. */
607
681
  function orphaned(deps, hab, grant) {
@@ -641,7 +715,8 @@ function requireIndividual(deps, hab) {
641
715
  }
642
716
  /**
643
717
  * 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.
718
+ * identifier, an issuance already begun, a Grant that is not this party's, and an issuer without its registry;
719
+ * a Grant past its deadline is refused with `KramRefusedError`.
645
720
  */
646
721
  function selectGrant(ops, hab, grant, role, group = false) {
647
722
  const { deps } = ops;
@@ -655,6 +730,7 @@ function selectGrant(ops, hab, grant, role, group = false) {
655
730
  if (!retained || !(credential instanceof SerderACDC) || !credential.regid
656
731
  || (role === "issuer" ? retained.serder.pre : retained.serder.sad.ri) !== hab.pre
657
732
  || retained.sourceSeals?.length || retained.ptds?.length || retained.essrs?.length) {
733
+ refuseLapsedGrant(deps, grant);
658
734
  throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
659
735
  }
660
736
  const reg = deps.rgy.regs.get(credential.regid);
@@ -689,12 +765,15 @@ function* grantWires(retained) {
689
765
  *
690
766
  * @throws ValidationError for a Grant that is not this identifier's exact
691
767
  * unanchored pending issuance, an unmanaged registry, a group identifier, or
692
- * an issuance already begun or completed.
768
+ * an issuance already begun or completed; its subclass `KramRefusedError`
769
+ * (reason `lifetime`, with the deadline) for a Grant past its exchange deadline.
693
770
  */
694
771
  export function* beginIssuance(ops, grant, auths) {
695
772
  const { deps } = ops, hab = grantParties(deps.hby, grant)?.issuer;
696
- if (!hab)
773
+ if (!hab) {
774
+ refuseLapsedGrant(deps, grant);
697
775
  throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
776
+ }
698
777
  const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "issuer");
699
778
  const { raw, grantWire } = yield* grantWires(retained);
700
779
  const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant), query = dh.start(grant);
@@ -730,14 +809,18 @@ export function* proposeIssuance(ops, draft, auths) {
730
809
  * operation holds reads `interrupted` (or `expired` once the Grant deadline has passed); that is a
731
810
  * statement, not a write.
732
811
  *
812
+ * @throws KramRefusedError (reason `lifetime`, with the deadline) when the Grant's deadline has passed and the
813
+ * stores no longer hold what a status needs: KRAM pruned its cache row, or the escrow sweep ejected the Grant.
733
814
  * @throws ValidationError when `grant` is not a v2 Grant this runtime's identifiers are party to.
734
815
  */
735
816
  export function issuanceStatus(ops, grant, party = {}) {
736
817
  const { deps } = ops, parties = grantParties(deps.hby, grant);
737
818
  const chosen = chooseParty(parties, party);
738
819
  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 };
820
+ if (ended) {
821
+ const { state, role, reason, deadline } = ended;
822
+ return { state, role, reason, ...(deadline && { deadline }) };
823
+ }
741
824
  if (live && !reached(deps.clock, live.expires)) {
742
825
  return {
743
826
  state: "active",
@@ -747,8 +830,10 @@ export function issuanceStatus(ops, grant, party = {}) {
747
830
  ...(live.outbox.failure && { failure: live.outbox.failure }),
748
831
  };
749
832
  }
750
- if (!parties)
833
+ if (!parties) {
834
+ refuseLapsedGrant(deps, grant);
751
835
  throw new ValidationError("No issuance was begun for this Grant");
836
+ }
752
837
  const { serder, issuer, holder } = parties, db = deps.hby.db;
753
838
  if (partyComplete(deps, grant, chosen)) {
754
839
  const admitted = !!db.erpy.get([grant]);
@@ -763,16 +848,22 @@ export function issuanceStatus(ops, grant, party = {}) {
763
848
  // A group issuer's partial Grant has no accepted lifetime until its members' contributions authenticate it.
764
849
  const lifetime = deps.exc.exchangeDeadline(serder) ?? undefined;
765
850
  const deadline = lifetime, expires = lifetime ? timestampMicros(lifetime) : null;
766
- if (!group && (!deadline || expires === null))
851
+ if (!group && (!deadline || expires === null)) {
852
+ // KRAM pruned the Grant's cache row at its deadline, before the escrow sweep ejected the Grant.
853
+ refuseLapsedGrant(deps, grant);
767
854
  throw new ValidationError("No issuance was begun for this Grant");
855
+ }
768
856
  if (reached(deps.clock, expires)) {
769
857
  const credential = deps.exc.retainedInput(grant)?.nests?.[0]?.body;
770
858
  const registry = credential instanceof SerderACDC ? credential.regid : undefined;
771
859
  const late = role === "holder" && registry && deps.rgy.store.headEvent(registry)?.ilk === "bup";
772
- return { state: "expired", role, reason: late ? EXPIRED_LATE : EXPIRED };
860
+ return { state: "expired", role, reason: late ? EXPIRED_LATE : EXPIRED, deadline };
773
861
  }
774
862
  if (resumable(deps, hab, grant, role))
775
863
  return { state: "active", role, phase: "rootRetained", deadline };
864
+ // The group completed without this member: the owner follows the custodian's proof once it is stored.
865
+ if (group && awaitsGroupProof(deps, hab, grant))
866
+ return { state: "active", role, phase: "awaitingAnchor", deadline };
776
867
  if ((role === "holder" || group) && !heldRoot(deps, hab, grant)) {
777
868
  // A group issuer's Grant may still be a partial one that only its members' contributions authenticate.
778
869
  if (!group && !deps.exc.retainedInput(grant, { pending: true })) {
@@ -892,11 +983,16 @@ export function* waitForIssuance(ops, grant, { timeoutMs, clock = ops.deps.clock
892
983
  * The holder's consent to the DH exchange for one pending Grant addressed to a local identifier. Approves DH and records the
893
984
  * consent; `processOnce` then answers the issuer's query, waits for its registry update and finishes the Grant.
894
985
  * Admitting the credential is a separate consent ({@link admitIssuance}).
986
+ *
987
+ * @throws KramRefusedError (reason `lifetime`, with the deadline) for a Grant past its exchange deadline, and
988
+ * ValidationError for any other Grant this holder cannot consent to.
895
989
  */
896
990
  export function approveIssuanceExchange(ops, grant) {
897
991
  const { deps } = ops, hab = grantParties(deps.hby, grant)?.holder;
898
- if (!hab)
992
+ if (!hab) {
993
+ refuseLapsedGrant(deps, grant);
899
994
  throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
995
+ }
900
996
  const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "holder");
901
997
  const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant);
902
998
  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;