keri-ts 2.2.0 → 2.2.2

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 (132) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +1 -127
  2. package/esm/keri/npm/src/acdc/custody.js +23 -0
  3. package/esm/keri/npm/src/acdc/dhking.js +6 -1
  4. package/esm/keri/npm/src/acdc/salting.js +18 -0
  5. package/esm/keri/npm/src/acdc/scheming.js +31 -18
  6. package/esm/keri/npm/src/acdc/unblinding.js +112 -6
  7. package/esm/keri/npm/src/app/delivery/forwarding.js +9 -0
  8. package/esm/keri/npm/src/app/delivery/respondant.js +21 -0
  9. package/esm/keri/npm/src/app/identity/habbing.js +4 -1
  10. package/esm/keri/npm/src/app/identity/keeping.js +38 -1
  11. package/esm/keri/npm/src/app/ipex/group-ipex.js +14 -0
  12. package/esm/keri/npm/src/app/ipex/group-issuance.js +15 -9
  13. package/esm/keri/npm/src/app/ipex/ipex-building.js +2 -1
  14. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +32 -8
  15. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +16 -8
  16. package/esm/keri/npm/src/app/ipex/ipex-node-authentication.js +42 -18
  17. package/esm/keri/npm/src/app/ipex/issuance.js +136 -40
  18. package/esm/keri/npm/src/app/ipex/pending-presentations.js +50 -0
  19. package/esm/keri/npm/src/app/ipex/presenting.js +44 -2
  20. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +141 -30
  21. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +37 -9
  22. package/esm/keri/npm/src/app/mailbox/mailboxing.js +9 -3
  23. package/esm/keri/npm/src/app/multisig/counseling.js +83 -4
  24. package/esm/keri/npm/src/app/multisig/event-forwarding.js +27 -0
  25. package/esm/keri/npm/src/app/multisig/grouping.js +120 -1
  26. package/esm/keri/npm/src/app/multisig/multisig-messaging.js +33 -6
  27. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +5 -9
  28. package/esm/keri/npm/src/app/multisig/proposals.js +1 -1
  29. package/esm/keri/npm/src/app/runtime/agent-runtime.js +43 -23
  30. package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +59 -25
  31. package/esm/keri/npm/src/app/runtime/operation-owners.js +1 -0
  32. package/esm/keri/npm/src/app/runtime/signer-conditions.js +51 -0
  33. package/esm/keri/npm/src/app/transport/cesr-egress.js +6 -0
  34. package/esm/keri/npm/src/cli/credentials/ipex.js +7 -1
  35. package/esm/keri/npm/src/cli/messaging/group-join.js +7 -3
  36. package/esm/keri/npm/src/cli/messaging/multisig.js +37 -11
  37. package/esm/keri/npm/src/core/errors.js +91 -3
  38. package/esm/keri/npm/src/core/json-schema-meta.js +688 -0
  39. package/esm/keri/npm/src/core/json-schema.js +147 -0
  40. package/esm/keri/npm/src/core/kel/eventing.js +20 -2
  41. package/esm/keri/npm/src/core/scheming.js +27 -26
  42. package/esm/keri/npm/src/db/replay/tel-replay.js +6 -0
  43. package/esm/keri/npm/src/db/store-lock.js +12 -0
  44. package/esm/keri/npm/src/runtime/index.js +9 -8
  45. package/esm/keri/npm/src/runtime/web/output-barrier.js +9 -0
  46. package/esm/keri/npm/src/version.js +2 -2
  47. package/package.json +2 -2
  48. package/types/_dnt.polyfills.d.ts +0 -99
  49. package/types/_dnt.polyfills.d.ts.map +1 -1
  50. package/types/acdc/custody.d.ts +7 -0
  51. package/types/acdc/custody.d.ts.map +1 -1
  52. package/types/acdc/dhking.d.ts.map +1 -1
  53. package/types/acdc/salting.d.ts +2 -0
  54. package/types/acdc/salting.d.ts.map +1 -1
  55. package/types/acdc/scheming.d.ts +2 -0
  56. package/types/acdc/scheming.d.ts.map +1 -1
  57. package/types/acdc/unblinding.d.ts +16 -5
  58. package/types/acdc/unblinding.d.ts.map +1 -1
  59. package/types/app/delivery/forwarding.d.ts +1 -0
  60. package/types/app/delivery/forwarding.d.ts.map +1 -1
  61. package/types/app/delivery/respondant.d.ts +7 -0
  62. package/types/app/delivery/respondant.d.ts.map +1 -1
  63. package/types/app/identity/habbing.d.ts +10 -1
  64. package/types/app/identity/habbing.d.ts.map +1 -1
  65. package/types/app/identity/keeping.d.ts +18 -0
  66. package/types/app/identity/keeping.d.ts.map +1 -1
  67. package/types/app/ipex/group-ipex.d.ts +7 -0
  68. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  69. package/types/app/ipex/group-issuance.d.ts +1 -1
  70. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  71. package/types/app/ipex/ipex-building.d.ts.map +1 -1
  72. package/types/app/ipex/ipex-discloser.d.ts +31 -19
  73. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  74. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  75. package/types/app/ipex/ipex-node-authentication.d.ts +9 -1
  76. package/types/app/ipex/ipex-node-authentication.d.ts.map +1 -1
  77. package/types/app/ipex/issuance.d.ts +41 -8
  78. package/types/app/ipex/issuance.d.ts.map +1 -1
  79. package/types/app/ipex/pending-presentations.d.ts +62 -1
  80. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  81. package/types/app/ipex/presenting.d.ts +41 -4
  82. package/types/app/ipex/presenting.d.ts.map +1 -1
  83. package/types/app/ipex/unblinding-workflow.d.ts +46 -10
  84. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  85. package/types/app/mailbox/mailbox-poller.d.ts +11 -2
  86. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  87. package/types/app/mailbox/mailboxing.d.ts +4 -1
  88. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  89. package/types/app/multisig/counseling.d.ts +22 -1
  90. package/types/app/multisig/counseling.d.ts.map +1 -1
  91. package/types/app/multisig/event-forwarding.d.ts +6 -0
  92. package/types/app/multisig/event-forwarding.d.ts.map +1 -0
  93. package/types/app/multisig/grouping.d.ts +26 -0
  94. package/types/app/multisig/grouping.d.ts.map +1 -1
  95. package/types/app/multisig/multisig-messaging.d.ts +23 -6
  96. package/types/app/multisig/multisig-messaging.d.ts.map +1 -1
  97. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  98. package/types/app/multisig/proposals.d.ts +3 -2
  99. package/types/app/multisig/proposals.d.ts.map +1 -1
  100. package/types/app/multisig/proposing.d.ts +8 -1
  101. package/types/app/multisig/proposing.d.ts.map +1 -1
  102. package/types/app/runtime/agent-runtime.d.ts +3 -0
  103. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  104. package/types/app/runtime/ipex-auxiliaries.d.ts +8 -3
  105. package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -1
  106. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  107. package/types/app/runtime/signer-conditions.d.ts +32 -0
  108. package/types/app/runtime/signer-conditions.d.ts.map +1 -0
  109. package/types/app/transport/cesr-egress.d.ts +1 -0
  110. package/types/app/transport/cesr-egress.d.ts.map +1 -1
  111. package/types/cli/credentials/ipex.d.ts.map +1 -1
  112. package/types/cli/messaging/group-join.d.ts +4 -1
  113. package/types/cli/messaging/group-join.d.ts.map +1 -1
  114. package/types/cli/messaging/multisig.d.ts +1 -0
  115. package/types/cli/messaging/multisig.d.ts.map +1 -1
  116. package/types/core/errors.d.ts +20 -1
  117. package/types/core/errors.d.ts.map +1 -1
  118. package/types/core/json-schema-meta.d.ts +19 -0
  119. package/types/core/json-schema-meta.d.ts.map +1 -0
  120. package/types/core/json-schema.d.ts +29 -0
  121. package/types/core/json-schema.d.ts.map +1 -0
  122. package/types/core/scheming.d.ts +1 -0
  123. package/types/core/scheming.d.ts.map +1 -1
  124. package/types/db/replay/tel-replay.d.ts +1 -0
  125. package/types/db/replay/tel-replay.d.ts.map +1 -1
  126. package/types/db/store-lock.d.ts +2 -0
  127. package/types/db/store-lock.d.ts.map +1 -1
  128. package/types/runtime/index.d.ts +12 -11
  129. package/types/runtime/index.d.ts.map +1 -1
  130. package/types/runtime/web/output-barrier.d.ts +1 -0
  131. package/types/runtime/web/output-barrier.d.ts.map +1 -1
  132. package/types/version.d.ts +2 -2
@@ -68,7 +68,8 @@ import { grantTransactionId, registryUncommitted, resumableExchange } from "../.
68
68
  import { ValidationError } from "../../core/errors.js";
69
69
  import { CREDENTIAL_MAILBOX_TOPIC } from "../../core/mailbox/mailbox-topics.js";
70
70
  import { timestampMicros } from "../../time/mod.js";
71
- import { isLocalGroupHab } from "../multisig/group-members.js";
71
+ import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
72
+ import { remoteGroupMembers } from "../multisig/multisig-messaging.js";
72
73
  import { DeliveryOutbox, } from "../runtime/delivery-outbox.js";
73
74
  import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
74
75
  import { deriveGroupCustody, GroupIssuance, GroupParty, registerGroupRoutes, stepGroup, } from "./group-issuance.js";
@@ -87,6 +88,10 @@ export const ISSUANCE_QUERY_RESEND_MS = 1000;
87
88
  * other members approve it).
88
89
  */
89
90
  export const ISSUANCE_SWEEP_MS = 5_000;
91
+ /** The key of one party's operation on a Grant. */
92
+ export function partyKey(grant, role) {
93
+ return `${role}:${grant}`;
94
+ }
90
95
  /**
91
96
  * Adapt a `Poster`; its delivery worker must be running (a one-shot CLI runs it under `withRuntimeWorkers`).
92
97
  * A wire of several complete messages (proof support plus Grant) goes as one ordered stream, which mailbox
@@ -128,14 +133,14 @@ export class IssuanceOperations {
128
133
  writable: true,
129
134
  value: deps
130
135
  });
131
- /** Operations live in this process. Internal to this module's functions. */
136
+ /** Operations live in this process, by {@link partyKey}. Read them through {@link liveOf}. */
132
137
  Object.defineProperty(this, "live", {
133
138
  enumerable: true,
134
139
  configurable: true,
135
140
  writable: true,
136
141
  value: new Map()
137
142
  });
138
- /** Operations that ended in this process with their reasons; not durable, see the module doc. */
143
+ /** Operations that ended in this process with their reasons, by {@link partyKey}; not durable, see the module doc. */
139
144
  Object.defineProperty(this, "ended", {
140
145
  enumerable: true,
141
146
  configurable: true,
@@ -176,8 +181,44 @@ export class IssuanceOperations {
176
181
  writable: true,
177
182
  value: []
178
183
  });
184
+ /**
185
+ * The delivery the outboxes use. A recipient that is an identifier of this Habery takes the bytes through the
186
+ * ordinary reactor path, so the checks a remote delivery meets run too; for a local group its members that are
187
+ * not held here are still delivered to, each through the host's delivery.
188
+ */
189
+ Object.defineProperty(this, "transport", {
190
+ enumerable: true,
191
+ configurable: true,
192
+ writable: true,
193
+ value: { deliver: (message) => this.deliverToParty(message) }
194
+ });
179
195
  this.discloser = new IpexDiscloser(deps.hby, deps.rgy.store, deps.exc, deps.receiptor);
180
196
  }
197
+ /** The live operation of one party of `grant`; without `role`, the issuer's when this Habery holds both. */
198
+ liveOf(grant, role) {
199
+ return role
200
+ ? this.live.get(partyKey(grant, role))
201
+ : this.live.get(partyKey(grant, "issuer")) ?? this.live.get(partyKey(grant, "holder"));
202
+ }
203
+ /** The operation of one party of `grant` that ended in this process; without `role`, as {@link liveOf}. */
204
+ endedOf(grant, role) {
205
+ return role
206
+ ? this.ended.get(partyKey(grant, role))
207
+ : this.ended.get(partyKey(grant, "issuer")) ?? this.ended.get(partyKey(grant, "holder"));
208
+ }
209
+ /** Every live operation, for the module functions that look across parties. */
210
+ liveOps() {
211
+ return [...this.live.values()];
212
+ }
213
+ /** Record a consented operation as live, replacing what an earlier one of the same party left behind. */
214
+ begin(op) {
215
+ this.ended.delete(partyKey(op.grant, op.role));
216
+ this.live.set(partyKey(op.grant, op.role), op);
217
+ }
218
+ /** Remember how a party's operation on `grant` ended without a live operation to end. */
219
+ remember(grant, ended) {
220
+ this.ended.set(partyKey(grant, ended.role), ended);
221
+ }
181
222
  /** Whether a pass has work: a live operation, or the one-time scan for resumable Grants has not run. */
182
223
  active() {
183
224
  return this.live.size > 0 || this.stale.length > 0 || this.deps.clock.now() >= this.nextSweep;
@@ -190,15 +231,18 @@ export class IssuanceOperations {
190
231
  *processOnce() {
191
232
  if (this.deps.clock.now() >= this.nextSweep) {
192
233
  this.nextSweep = this.deps.clock.now() + ISSUANCE_SWEEP_MS;
193
- const known = new Set(this.stale.map(({ grant }) => grant));
194
- this.stale.push(...resumableGrants(this.deps).filter(({ grant }) => !this.live.has(grant) && !known.has(grant) && !this.refused.has(grant) && !this.ended.has(grant)));
234
+ const known = new Set(this.stale.map(({ grant, role }) => partyKey(grant, role)));
235
+ this.stale.push(...resumableGrants(this.deps).filter(({ grant, role }) => {
236
+ const key = partyKey(grant, role);
237
+ return !this.live.has(key) && !known.has(key) && !this.refused.has(key) && !this.ended.has(key);
238
+ }));
195
239
  }
196
240
  for (const found of [...this.stale]) {
197
241
  if (yield* this.settle(found))
198
242
  this.stale.splice(this.stale.indexOf(found), 1);
199
243
  }
200
244
  for (const op of [...this.live.values()]) {
201
- if (op.busy || !this.live.has(op.grant))
245
+ if (op.busy || this.live.get(partyKey(op.grant, op.role)) !== op)
202
246
  continue;
203
247
  op.busy = true;
204
248
  try {
@@ -219,12 +263,12 @@ export class IssuanceOperations {
219
263
  /** End a live operation: release its DH approval, remember why until the process ends, tell the sink. */
220
264
  end(op, state, reason) {
221
265
  this.release(op);
222
- this.ended.set(op.grant, { role: op.role, state, reason });
266
+ this.ended.set(partyKey(op.grant, op.role), { role: op.role, state, reason });
223
267
  this.deps.changes?.push({ kind: "issuance", id: op.grant });
224
268
  }
225
269
  /** Drop the live entry and the DH approval; a missing approval means the owner already closed. */
226
270
  release(op) {
227
- this.live.delete(op.grant);
271
+ this.live.delete(partyKey(op.grant, op.role));
228
272
  try {
229
273
  op.dh.abandon(op.xid);
230
274
  }
@@ -239,7 +283,8 @@ export class IssuanceOperations {
239
283
  * dropped and reads from the stores. False means the Keeper is locked and the Grant is retried.
240
284
  */
241
285
  *settle(found) {
242
- if (this.live.has(found.grant))
286
+ const key = partyKey(found.grant, found.role);
287
+ if (this.live.has(key))
243
288
  return true;
244
289
  try {
245
290
  found.hab.mgr.requireRegistryRootAccess();
@@ -251,18 +296,18 @@ export class IssuanceOperations {
251
296
  }
252
297
  // Another operations table over this runtime reached the Grant first (a command's own, beside the host's).
253
298
  if (this.deps.dhkes.dhke(found.hab).holds(found.grant)) {
254
- this.refused.add(found.grant);
299
+ this.refused.add(key);
255
300
  return true;
256
301
  }
257
302
  try {
258
303
  yield* this.adopt(found);
259
304
  }
260
305
  catch (error) {
261
- this.refused.add(found.grant);
306
+ this.refused.add(key);
262
307
  // A refusal means the stores no longer prove it resumable and status reads from them; anything else is
263
308
  // this Grant's failure alone and must not stop the other operations.
264
309
  if (!(error instanceof ValidationError)) {
265
- this.ended.set(found.grant, { role: found.role, state: "failed", reason: message(error) });
310
+ this.ended.set(key, { role: found.role, state: "failed", reason: message(error) });
266
311
  this.deps.changes?.push({ kind: "issuance", id: found.grant });
267
312
  }
268
313
  }
@@ -278,8 +323,8 @@ export class IssuanceOperations {
278
323
  }
279
324
  const wires = role === "issuer" ? yield* grantWires(retained) : null;
280
325
  const dh = this.deps.dhkes.dhke(hab), xid = dh.resume(grant);
281
- this.ended.delete(grant);
282
- this.live.set(grant, {
326
+ this.ended.delete(partyKey(grant, role));
327
+ this.live.set(partyKey(grant, role), {
283
328
  grant,
284
329
  xid,
285
330
  role,
@@ -308,8 +353,8 @@ export class IssuanceOperations {
308
353
  const party = GroupParty.resolve(this.deps, hab, election.coordinator.said);
309
354
  const group = new GroupIssuance(party, role, retained.serder, retained.nests ?? [], { mode: "resumed" });
310
355
  const dh = this.deps.dhkes.dhke(hab), xid = dh.resume(grant, party);
311
- this.ended.delete(grant);
312
- this.live.set(grant, this.liveGroup(group, dh, xid, "rootRetained"));
356
+ this.ended.delete(partyKey(grant, role));
357
+ this.live.set(partyKey(grant, role), this.liveGroup(group, dh, xid, "rootRetained"));
313
358
  this.deps.changes?.push({ kind: "issuance", id: grant });
314
359
  }
315
360
  /** The live entry of a group operation; its lifetime is the Grant's once KRAM has accepted it, else unknown. */
@@ -336,6 +381,20 @@ export class IssuanceOperations {
336
381
  yield* waitOnClock(this.deps.clock, milliseconds);
337
382
  return "expired";
338
383
  }
384
+ *deliverToParty(message) {
385
+ const { hby, reactor, delivery } = this.deps, recipient = hby.habs.get(message.dest);
386
+ if (!recipient)
387
+ return yield* delivery.deliver(message);
388
+ yield* reactor.processBytes(message.msg);
389
+ const reached = [`local:${recipient.pre}`];
390
+ if (!isLocalGroupHab(hby, recipient))
391
+ return reached;
392
+ const member = localGroupMember(hby, recipient.pre);
393
+ for (const dest of remoteGroupMembers(hby, member, groupSigningMembers(hby, recipient.pre))) {
394
+ reached.push(...yield* delivery.deliver({ ...message, dest }));
395
+ }
396
+ return reached;
397
+ }
339
398
  /**
340
399
  * Queue signed bytes for delivery from the identifier that signed them. Steps decide what to send; {@link drain}
341
400
  * sends it, so a member that is briefly offline delays the operation instead of ending it.
@@ -349,7 +408,7 @@ export class IssuanceOperations {
349
408
  * lasting {@link ISSUANCE_RETRY_BOUND_MS} end one that has none yet. True when the outbox is empty.
350
409
  */
351
410
  *drain(op) {
352
- return yield* op.outbox.drain(this.deps.delivery, this.deps.clock, op.expires === null ? this.deps.retry?.boundMs ?? ISSUANCE_RETRY_BOUND_MS : undefined);
411
+ return yield* op.outbox.drain(this.transport, this.deps.clock, op.expires === null ? this.deps.retry?.boundMs ?? ISSUANCE_RETRY_BOUND_MS : undefined);
353
412
  }
354
413
  /** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
355
414
  *advance(op) {
@@ -388,11 +447,17 @@ export class IssuanceOperations {
388
447
  : group.role === "issuer"
389
448
  ? "awaitingGrantSignatures"
390
449
  : "exchangingKeys";
391
- this.ended.delete(group.said);
392
- this.live.set(group.said, this.liveGroup(group, dh, xid, phase));
450
+ this.ended.delete(partyKey(group.said, group.role));
451
+ this.live.set(partyKey(group.said, group.role), this.liveGroup(group, dh, xid, phase));
393
452
  deps.changes?.push({ kind: "issuance", id: group.said });
394
453
  return { grant: group.said, xid };
395
454
  }
455
+ /** Whether a live operation for `grant` (or only its party `role`) still holds a message the transport has not taken. */
456
+ owes(grant, role) {
457
+ return role
458
+ ? (this.liveOf(grant, role)?.outbox.length ?? 0) > 0
459
+ : this.owes(grant, "issuer") || this.owes(grant, "holder");
460
+ }
396
461
  /** One pass of the issuer loop, in the order of the CLI loop it replaces. */
397
462
  *step(op) {
398
463
  if (op.group) {
@@ -521,7 +586,7 @@ function resumableGrants(deps) {
521
586
  if (parties.issuer && resumable(deps, parties.issuer, grant, "issuer")) {
522
587
  found.push({ hab: parties.issuer, grant, role: "issuer" });
523
588
  }
524
- else if (parties.holder && resumable(deps, parties.holder, grant, "holder")) {
589
+ if (parties.holder && resumable(deps, parties.holder, grant, "holder")) {
525
590
  found.push({ hab: parties.holder, grant, role: "holder" });
526
591
  }
527
592
  }
@@ -571,7 +636,7 @@ function selectGrant(ops, hab, grant, role, group = false) {
571
636
  const { deps } = ops;
572
637
  if (!group)
573
638
  requireIndividual(deps, hab);
574
- if (ops.live.has(grant) || deps.exc.complete(grant)) {
639
+ if (ops.liveOf(grant, role) || partyComplete(deps, grant, role)) {
575
640
  throw new ValidationError("Issuance was already begun for this Grant");
576
641
  }
577
642
  const retained = deps.exc.retainedInput(grant, { pending: true });
@@ -590,6 +655,16 @@ function selectGrant(ops, hab, grant, role, group = false) {
590
655
  throw new Error("Authenticated pending Grant has a missing or corrupt deadline");
591
656
  return { retained, credential, deadline, expires };
592
657
  }
658
+ /**
659
+ * Whether one party's side of a Grant is finished. The accepted Grant is one fact of the Habery, but a Habery that
660
+ * holds both parties finishes the holder's side only when the holder's own exchange has retained its root.
661
+ */
662
+ export function partyComplete(deps, grant, role) {
663
+ if (!deps.exc.complete(grant))
664
+ return false;
665
+ const parties = grantParties(deps.hby, grant);
666
+ return role === "issuer" || !parties?.issuer || !parties.holder || heldRoot(deps, parties.holder, grant);
667
+ }
593
668
  /** The Grant body with its nested disclosure (the input of proof support) and the Grant as sent to the holder. */
594
669
  function* grantWires(retained) {
595
670
  const raw = concatBytes(Counter.makeGVC(Vrsn_2_0), ...yield* call(() => Promise.all((retained.nests ?? []).map((nest) => collectPayload(framePayload(nest))))));
@@ -612,8 +687,7 @@ export function* beginIssuance(ops, grant, auths) {
612
687
  const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "issuer");
613
688
  const { raw, grantWire } = yield* grantWires(retained);
614
689
  const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant), query = dh.start(grant);
615
- ops.ended.delete(grant);
616
- ops.live.set(grant, {
690
+ ops.begin({
617
691
  grant,
618
692
  xid,
619
693
  role: "issuer",
@@ -647,8 +721,10 @@ export function* proposeIssuance(ops, draft, auths) {
647
721
  *
648
722
  * @throws ValidationError when `grant` is not a v2 Grant this runtime's identifiers are party to.
649
723
  */
650
- export function issuanceStatus(ops, grant) {
651
- const { deps } = ops, ended = ops.ended.get(grant), live = ops.live.get(grant);
724
+ export function issuanceStatus(ops, grant, party = {}) {
725
+ const { deps } = ops, parties = grantParties(deps.hby, grant);
726
+ const chosen = chooseParty(parties, party);
727
+ const ended = ops.endedOf(grant, chosen), live = ops.liveOf(grant, chosen);
652
728
  if (ended)
653
729
  return { state: ended.state, role: ended.role, reason: ended.reason };
654
730
  if (live && !reached(deps.clock, live.expires)) {
@@ -660,19 +736,18 @@ export function issuanceStatus(ops, grant) {
660
736
  ...(live.outbox.failure && { failure: live.outbox.failure }),
661
737
  };
662
738
  }
663
- const parties = grantParties(deps.hby, grant);
664
739
  if (!parties)
665
740
  throw new ValidationError("No issuance was begun for this Grant");
666
741
  const { serder, issuer, holder } = parties, db = deps.hby.db;
667
- if (deps.exc.complete(grant)) {
742
+ if (partyComplete(deps, grant, chosen)) {
668
743
  const admitted = !!db.erpy.get([grant]);
669
- if (!holder || (issuer && !admitted))
744
+ if (chosen === "issuer")
670
745
  return { state: "done", role: "issuer", phase: "issued" };
671
746
  if (admitted)
672
747
  return { state: "done", role: "holder", phase: "admitted" };
673
748
  return { state: "active", role: "holder", phase: "awaitingAdmitConsent", needs: "admit" };
674
749
  }
675
- const role = issuer ? "issuer" : "holder", hab = (issuer ?? holder);
750
+ const role = chosen, hab = (role === "issuer" ? issuer : holder);
676
751
  const group = isLocalGroupHab(deps.hby, hab);
677
752
  // A group issuer's partial Grant has no accepted lifetime until its members' contributions authenticate it.
678
753
  const lifetime = deps.exc.exchangeDeadline(serder) ?? undefined;
@@ -697,6 +772,24 @@ export function issuanceStatus(ops, grant) {
697
772
  const [state, reason] = orphaned(deps, hab, grant);
698
773
  return { state, role, reason };
699
774
  }
775
+ /**
776
+ * The party a read is for: the one asked for, else the Habery's only party, else (it holds both) the issuer. Null
777
+ * when the Grant has no local party.
778
+ *
779
+ * @throws ValidationError when `choice` names a party this Habery does not hold.
780
+ */
781
+ function chooseParty(parties, choice) {
782
+ if (!parties)
783
+ return choice.role;
784
+ const held = [
785
+ ...(parties.issuer ? ["issuer"] : []),
786
+ ...(parties.holder ? ["holder"] : []),
787
+ ];
788
+ if (choice.role && !held.includes(choice.role)) {
789
+ throw new ValidationError(`This Habery holds no ${choice.role} of this Grant`);
790
+ }
791
+ return choice.role ?? held[0];
792
+ }
700
793
  /**
701
794
  * Pure read of the Grants waiting for the holder's exchange consent: authenticated pending v2 Grants addressed
702
795
  * to a local identifier (to `hab` when given) for which {@link approveIssuanceExchange} would be accepted.
@@ -709,7 +802,7 @@ export function pendingIssuanceGrants(ops, { hab } = {}) {
709
802
  if (!holder || (hab && holder.pre !== hab.pre))
710
803
  continue;
711
804
  try {
712
- const status = issuanceStatus(ops, grant);
805
+ const status = issuanceStatus(ops, grant, { role: "holder" });
713
806
  if (status.state !== "active" || status.needs !== "exchange")
714
807
  continue;
715
808
  const { retained, credential, deadline } = selectGrant(ops, holder, grant, "holder");
@@ -742,7 +835,7 @@ export function pendingGroupGrants(ops, { group } = {}) {
742
835
  if (!parties || !holder || !isLocalGroupHab(deps.hby, holder) || (group && holder.pre !== group.pre))
743
836
  continue;
744
837
  try {
745
- const status = issuanceStatus(ops, grant);
838
+ const status = issuanceStatus(ops, grant, { role: "holder" });
746
839
  if (status.state !== "active" || status.needs !== "exchange")
747
840
  continue;
748
841
  const credential = deps.exc.retainedInput(grant, { pending: true })?.nests?.[0]?.body;
@@ -775,9 +868,13 @@ function heldRoot(deps, hab, grant) {
775
868
  * Observe until the issuance is done or ended, or the budget expires. Starts no
776
869
  * worker and drives nothing: someone else must call `processOnce`.
777
870
  */
778
- export function* waitForIssuance(ops, grant, { timeoutMs, clock = ops.deps.clock, until = (status) => status.state !== "active" }) {
779
- const settled = yield* observeUntil(() => until(issuanceStatus(ops, grant)), { timeoutMs, clock });
780
- const status = issuanceStatus(ops, grant);
871
+ export function* waitForIssuance(ops, grant, { timeoutMs, clock = ops.deps.clock, until = (status) => status.state !== "active", role }) {
872
+ // A phase is not reached while the step that produced it still owes a delivery: the host would stop its workers first.
873
+ const settled = yield* observeUntil(() => until(issuanceStatus(ops, grant, { role })) && !ops.owes(grant, role), {
874
+ timeoutMs,
875
+ clock,
876
+ });
877
+ const status = issuanceStatus(ops, grant, { role });
781
878
  return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
782
879
  }
783
880
  /**
@@ -791,8 +888,7 @@ export function approveIssuanceExchange(ops, grant) {
791
888
  throw new ValidationError("Select the exact unanchored pending issuance Grant for this local party");
792
889
  const { retained, credential, deadline, expires } = selectGrant(ops, hab, grant, "holder");
793
890
  const dh = deps.dhkes.dhke(hab), xid = dh.approve(grant);
794
- ops.ended.delete(grant);
795
- ops.live.set(grant, {
891
+ ops.begin({
796
892
  grant,
797
893
  xid,
798
894
  role: "holder",
@@ -874,15 +970,15 @@ export function* rebuildIssuanceAdmit(deps, admit) {
874
970
  *
875
971
  * @throws ValidationError when no issuance was begun for `grant`.
876
972
  */
877
- export function abandonIssuance(ops, grant, reason = "Abandoned by the user") {
878
- const status = issuanceStatus(ops, grant);
973
+ export function abandonIssuance(ops, grant, reason = "Abandoned by the user", party = {}) {
974
+ const status = issuanceStatus(ops, grant, party);
879
975
  if (status.state !== "active")
880
976
  return;
881
- const live = ops.live.get(grant);
977
+ const live = ops.liveOf(grant, status.role);
882
978
  if (live)
883
979
  ops.end(live, "abandoned", reason);
884
980
  else {
885
- ops.ended.set(grant, { role: status.role, state: "abandoned", reason });
981
+ ops.remember(grant, { role: status.role, state: "abandoned", reason });
886
982
  ops.deps.changes?.push({ kind: "issuance", id: grant });
887
983
  }
888
984
  }
@@ -9,6 +9,8 @@
9
9
  */
10
10
  import { SerderACDC } from "cesr-ts";
11
11
  import { ValidationError } from "../../core/errors.js";
12
+ import { timestampMicros } from "../../time/mod.js";
13
+ import { singleDagDisclosurePlan } from "./ipex-graph.js";
12
14
  import { unblindingStatus } from "./unblinding-workflow.js";
13
15
  /**
14
16
  * The pending v2 Grants addressed to a local identifier (to `hab` when given) that are inside their deadline.
@@ -44,3 +46,51 @@ export function pendingPresentations(ops, { hab } = {}) {
44
46
  }
45
47
  return found;
46
48
  }
49
+ /**
50
+ * The accepted v2 Applies addressed to a local identifier (to `hab` when given) that no exchange has answered
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.
53
+ */
54
+ export function pendingApplies(deps, { hab, includeAnswered = false } = {}) {
55
+ const { hby, exc, clock } = deps, found = [];
56
+ const now = BigInt(clock.now()) * 1000n;
57
+ for (const [[apply], serder] of [...hby.db.exns.getTopItemIter()]) {
58
+ const holder = serder.sad?.ri;
59
+ if (serder.route !== "/ipex/apply" || serder.pvrsn.major < 2 || typeof holder !== "string")
60
+ continue;
61
+ if (!hby.habs.has(holder) || (hab && hab.pre !== holder))
62
+ continue;
63
+ if (!exc.complete(apply))
64
+ continue;
65
+ const answer = hby.db.erpy.get([apply])?.qb64 ?? null;
66
+ if (answer && !includeAnswered)
67
+ continue;
68
+ const deadline = exc.exchangeDeadline(serder), expiry = deadline ? timestampMicros(deadline) : null;
69
+ if (!answer && expiry !== null && now >= expiry)
70
+ continue;
71
+ const q = serder.sad?.q;
72
+ found.push({
73
+ apply,
74
+ holder,
75
+ applicant: serder.pre,
76
+ message: String(serder.sad?.a?.m ?? ""),
77
+ plan: singleDagDisclosurePlan(q?.dp) ?? [],
78
+ deadline,
79
+ state: answer ? "answered" : "pending",
80
+ answer,
81
+ });
82
+ }
83
+ return found;
84
+ }
85
+ /** What answered the exchange `said`, from the answer pointer the stores keep; null when nothing has. Mutates nothing. */
86
+ export function exchangeAnswer(deps, said) {
87
+ const answer = deps.hby.db.erpy.get([said])?.qb64, serder = answer ? deps.hby.db.exns.get([answer]) : null;
88
+ if (!answer || !serder?.route)
89
+ return null;
90
+ return { answer, route: serder.route, sender: serder.pre, accepted: deps.exc.complete(answer) };
91
+ }
92
+ /** The Agree that answers the Offer `offer`; null while nothing, or something other than an Agree (a Spurn), answered it. */
93
+ export function agreeFor(deps, offer) {
94
+ const answer = exchangeAnswer(deps, offer);
95
+ return answer?.route === "/ipex/agree" ? answer : null;
96
+ }
@@ -87,6 +87,10 @@ export class PresentationOperations {
87
87
  value: new Map()
88
88
  });
89
89
  }
90
+ /** Whether the live presentation `said` still holds a message the transport has not taken. */
91
+ owes(said) {
92
+ return (this.live.get(said)?.outbox.length ?? 0) > 0;
93
+ }
90
94
  /** Whether a delivery is live. */
91
95
  active() {
92
96
  return this.live.size > 0;
@@ -180,7 +184,7 @@ export function* preparePresentation(deps, input) {
180
184
  break;
181
185
  case "agree":
182
186
  case "spurn":
183
- prepared = yield* discloser.prepareReply(sender, input.verb, input.prior, input.message ?? "", input.recipient, input.auths);
187
+ prepared = yield* discloser.prepareReply(sender, input.verb, input.priorSaid, input.message ?? "", input.recipient, input.auths);
184
188
  break;
185
189
  case "offer":
186
190
  prepared = yield* discloser.prepareOffer(sender, input.selection, input.options);
@@ -255,6 +259,41 @@ export function* proposeGroupPresentation(deps, groupIpex, { hab, selection, opt
255
259
  },
256
260
  });
257
261
  }
262
+ /**
263
+ * A group's consent to send an Apply, Offer, Agree or Spurn: freezes the unsigned body (the same body a single
264
+ * identifier would sign) and proposes it as a group-authored message with {@link proposeGroupIpex}. The other members
265
+ * list and approve it with `approveGroupIpex` and the elected lead delivers the completed message. A Grant has its own
266
+ * call, {@link proposeGroupPresentation}; an Admit has `proposeGroupAdmit`.
267
+ *
268
+ * @throws ValidationError for an identifier that is not a local group, a selection or predecessor the discloser
269
+ * refuses, an exchange that requires anchoring (a group message carries member contributions, not one source seal),
270
+ * or a message awaiting authenticating dependencies.
271
+ */
272
+ export function* proposeGroupNegotiation(deps, groupIpex, input) {
273
+ const { hab, deliver } = input;
274
+ const sender = hab.pre;
275
+ let exn, nests, gvrsn;
276
+ switch (input.verb) {
277
+ case "apply":
278
+ gvrsn = input.options.gvrsn;
279
+ exn = discloserFor(deps, hab).applyBody(sender, input.options);
280
+ break;
281
+ case "offer": {
282
+ gvrsn = input.options.gvrsn;
283
+ const offer = yield* discloserFor(deps, hab, input.selection.derive).prepareOfferBody(sender, input.selection, input.options);
284
+ ({ exn, nests } = offer);
285
+ break;
286
+ }
287
+ case "agree":
288
+ case "spurn":
289
+ exn = yield* discloserFor(deps, hab).replyBody(sender, input.verb, input.priorSaid, input.message ?? "", input.recipient);
290
+ break;
291
+ }
292
+ if (exn.sad.a.ax?.[0] === true) {
293
+ throw new ValidationError("A group cannot send an exchange that requires anchoring.");
294
+ }
295
+ return yield* proposeGroupIpex(groupIpex, { hab, exn, nests, gvrsn: gvrsn ?? undefined, deliver });
296
+ }
258
297
  /**
259
298
  * Deliver a group's completed prepared Grant ({@link resumePresentationFile}): queues the delivery bytes for
260
299
  * `processOnce` when the local member is the elected lead, with the retry and deadline of any presentation. Signs
@@ -340,7 +379,10 @@ export function presentationStatus(ops, said) {
340
379
  * nothing: someone else must call `processOnce`.
341
380
  */
342
381
  export function* waitForPresentation(ops, said, { timeoutMs, clock = ops.deps.clock, until = (status) => status.state !== "active" }) {
343
- const settled = yield* observeUntil(() => until(presentationStatus(ops, said)), { timeoutMs, clock });
382
+ const settled = yield* observeUntil(() => until(presentationStatus(ops, said)) && !ops.owes(said), {
383
+ timeoutMs,
384
+ clock,
385
+ });
344
386
  const status = presentationStatus(ops, said);
345
387
  return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
346
388
  }