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
@@ -34,11 +34,16 @@ import { singleKeyQueryWindow } from "../../core/mailbox/kram-policy.js";
34
34
  import { timestampMicros } from "../../time/mod.js";
35
35
  import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
36
36
  import { MULTISIG_QRY_ROUTE, MULTISIG_RPY_ROUTE, multisigExnBody } from "../multisig/grouping.js";
37
+ import { remoteGroupMembers } from "../multisig/multisig-messaging.js";
37
38
  import { envelopeFromFrame } from "../protocol/parsering.js";
38
39
  import { DeliveryOutbox } from "../runtime/delivery-outbox.js";
39
40
  import { observeUntil, waitOnClock } from "../runtime/workflow-contract.js";
40
41
  /** Milliseconds between resends of the unchanged disclosee query, as `tufa ipex unblind` does. */
41
42
  export const UNBLIND_QUERY_RESEND_MS = 1000;
43
+ /** The key of one party's operation on a Grant. */
44
+ export function unblindingKey(grant, role) {
45
+ return `${role}:${grant}`;
46
+ }
42
47
  /** Per-operation state of a group party; memory only. */
43
48
  class GroupUnblind {
44
49
  constructor(member, proposer) {
@@ -94,6 +99,8 @@ class GroupUnblind {
94
99
  }
95
100
  }
96
101
  const EXPIRED = "The presentation reached its original exchange deadline";
102
+ const NOT_ANSWERED = "No answer is begun for this presentation here; a group discloser's members begin it with answerUnblind";
103
+ const UNNEEDED = "The presentation was already answered with its opening delivered, so this answer was not needed";
97
104
  const CHANGED = "Original presentation deadline changed";
98
105
  function message(error) {
99
106
  return error instanceof Error ? error.message : String(error);
@@ -113,14 +120,14 @@ export class UnblindingOperations {
113
120
  writable: true,
114
121
  value: deps
115
122
  });
116
- /** Operations whose approval is live in this process. Internal to this module's functions. */
123
+ /** Operations whose approval is live in this process, by {@link unblindingKey}. Read them through {@link liveOf}. */
117
124
  Object.defineProperty(this, "live", {
118
125
  enumerable: true,
119
126
  configurable: true,
120
127
  writable: true,
121
128
  value: new Map()
122
129
  });
123
- /** Operations that finished or ended in this process; not durable, see the module doc. */
130
+ /** Operations that finished or ended in this process, by {@link unblindingKey}; not durable, see the module doc. */
124
131
  Object.defineProperty(this, "settled", {
125
132
  enumerable: true,
126
133
  configurable: true,
@@ -134,6 +141,48 @@ export class UnblindingOperations {
134
141
  writable: true,
135
142
  value: null
136
143
  });
144
+ /**
145
+ * The delivery the outboxes use. A recipient that is an identifier of this Habery takes the bytes through the
146
+ * ordinary reactor path in process, so the checks a remote delivery meets run too; a local group's members that
147
+ * are not held here are still delivered to.
148
+ */
149
+ Object.defineProperty(this, "transport", {
150
+ enumerable: true,
151
+ configurable: true,
152
+ writable: true,
153
+ value: { deliver: (message) => this.deliverToParty(message) }
154
+ });
155
+ }
156
+ /** The live operation of one party of `grant`; without `role`, the requester's when this Habery holds both. */
157
+ liveOf(grant, role) {
158
+ return role
159
+ ? this.live.get(unblindingKey(grant, role))
160
+ : this.live.get(unblindingKey(grant, "requester")) ?? this.live.get(unblindingKey(grant, "responder"));
161
+ }
162
+ /** The operation of one party of `grant` that finished or ended in this process; without `role`, as {@link liveOf}. */
163
+ settledOf(grant, role) {
164
+ return role
165
+ ? this.settled.get(unblindingKey(grant, role))
166
+ : this.settled.get(unblindingKey(grant, "requester")) ?? this.settled.get(unblindingKey(grant, "responder"));
167
+ }
168
+ /** Every live operation, for the module functions that look across parties. */
169
+ liveOps() {
170
+ return [...this.live.values()];
171
+ }
172
+ /** Record a consented operation as live, replacing what an earlier one of the same party left behind. */
173
+ begin(op) {
174
+ this.settled.delete(unblindingKey(op.grant, op.role));
175
+ this.live.set(unblindingKey(op.grant, op.role), op);
176
+ }
177
+ /** Remember how a party's operation on `grant` finished or ended without a live operation to end. */
178
+ remember(grant, settled) {
179
+ this.settled.set(unblindingKey(grant, settled.role), settled);
180
+ }
181
+ /** Whether a live operation for `grant` (or only its party `role`) still holds a message the transport has not taken. */
182
+ owes(grant, role) {
183
+ return role
184
+ ? (this.liveOf(grant, role)?.outbox.length ?? 0) > 0
185
+ : this.owes(grant, "requester") || this.owes(grant, "responder");
137
186
  }
138
187
  /**
139
188
  * Whether a pass has work: a live operation, or an owner holds an authenticated peer query whose reply is
@@ -152,19 +201,23 @@ export class UnblindingOperations {
152
201
  const { deps } = this;
153
202
  this.adopt();
154
203
  for (const op of [...this.live.values()]) {
155
- if (op.busy || !this.live.has(op.grant))
204
+ if (op.busy || this.live.get(unblindingKey(op.grant, op.role)) !== op)
156
205
  continue;
157
206
  op.busy = true;
158
207
  try {
159
208
  const left = op.expires - BigInt(deps.clock.now()) * 1000n;
160
209
  if (left <= 0n)
161
- this.end(op, "expired", EXPIRED);
210
+ this.end(op, "expired", this.expiry(op));
162
211
  else if ((yield* race([this.advance(op), this.timer(Number(left) / 1000)])) === "expired") {
163
- this.end(op, "expired", EXPIRED);
212
+ this.end(op, "expired", this.expiry(op));
164
213
  }
165
214
  }
166
215
  catch (error) {
167
- this.end(op, "failed", message(error));
216
+ // A discloser whose presentation closed before it was asked had nothing left to answer; that is not a failure.
217
+ if (op.role === "responder" && deps.hby.db.erpy.get([op.grant]))
218
+ this.end(op, "unneeded", UNNEEDED);
219
+ else
220
+ this.end(op, "failed", message(error));
168
221
  }
169
222
  finally {
170
223
  op.busy = false;
@@ -181,7 +234,7 @@ export class UnblindingOperations {
181
234
  const { deps } = this;
182
235
  for (const owner of deps.unblinds.unblindOwners()) {
183
236
  for (const { xid, grant } of owner.awaitingReply()) {
184
- if (this.live.has(grant))
237
+ if (this.live.has(unblindingKey(grant, "responder")))
185
238
  continue;
186
239
  let input, deadline, expires;
187
240
  try {
@@ -192,14 +245,18 @@ export class UnblindingOperations {
192
245
  catch (error) {
193
246
  // Unreadable stored state fails this reply alone; release the owner's approval so it is not retried.
194
247
  owner.abandon(xid);
195
- this.settled.set(grant, { role: "responder", state: "failed", reason: message(error) });
248
+ this.settled.set(unblindingKey(grant, "responder"), {
249
+ role: "responder",
250
+ state: "failed",
251
+ reason: message(error),
252
+ });
196
253
  deps.changes?.push({ kind: "unblinding", id: grant });
197
254
  continue;
198
255
  }
199
256
  if (!input || !deadline || expires === null || BigInt(deps.clock.now()) * 1000n >= expires)
200
257
  continue;
201
- this.settled.delete(grant);
202
- this.live.set(grant, {
258
+ this.settled.delete(unblindingKey(grant, "responder"));
259
+ this.live.set(unblindingKey(grant, "responder"), {
203
260
  grant,
204
261
  xid,
205
262
  role: "responder",
@@ -219,15 +276,20 @@ export class UnblindingOperations {
219
276
  }
220
277
  }
221
278
  }
279
+ /** The reason an operation reached its deadline; a query still waiting for the issuer's anchoring event says so. */
280
+ expiry(op) {
281
+ const missing = op.role === "responder" ? op.owner.missingSource(op.xid) : null;
282
+ return missing ? `${EXPIRED}; ${missing}` : EXPIRED;
283
+ }
222
284
  /** End a live operation: release the approval, remember why until the process ends, tell the sink. */
223
285
  end(op, state, reason) {
224
286
  this.release(op);
225
- this.settled.set(op.grant, { role: op.role, state, reason });
287
+ this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state, reason });
226
288
  this.deps.changes?.push({ kind: "unblinding", id: op.grant });
227
289
  }
228
290
  /** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
229
291
  release(op) {
230
- this.live.delete(op.grant);
292
+ this.live.delete(unblindingKey(op.grant, op.role));
231
293
  try {
232
294
  op.owner.abandon(op.xid);
233
295
  }
@@ -238,7 +300,7 @@ export class UnblindingOperations {
238
300
  }
239
301
  finished(op) {
240
302
  this.release(op);
241
- this.settled.set(op.grant, { role: op.role, state: "done" });
303
+ this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state: "done" });
242
304
  this.deps.changes?.push({ kind: "unblinding", id: op.grant });
243
305
  }
244
306
  *timer(milliseconds) {
@@ -409,9 +471,23 @@ export class UnblindingOperations {
409
471
  op.finished = true;
410
472
  return "stepped";
411
473
  }
474
+ *deliverToParty(message) {
475
+ const { hby, reactor, delivery } = this.deps, recipient = hby.habs.get(message.dest);
476
+ if (!recipient || !reactor)
477
+ return yield* delivery.deliver(message);
478
+ yield* reactor.processBytes(message.msg);
479
+ const reached = [`local:${recipient.pre}`];
480
+ if (!isLocalGroupHab(hby, recipient))
481
+ return reached;
482
+ const member = localGroupMember(hby, recipient.pre);
483
+ for (const dest of remoteGroupMembers(hby, member, groupSigningMembers(hby, recipient.pre))) {
484
+ reached.push(...yield* delivery.deliver({ ...message, dest }));
485
+ }
486
+ return reached;
487
+ }
412
488
  /** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
413
489
  *advance(op) {
414
- const drain = () => op.outbox.drain(this.deps.delivery, this.deps.clock);
490
+ const drain = () => op.outbox.drain(this.transport, this.deps.clock);
415
491
  if (!(yield* drain()))
416
492
  return "stepped";
417
493
  if (!op.finished)
@@ -424,7 +500,7 @@ export class UnblindingOperations {
424
500
  /** The exact live Grant the local party may consent to, with its deadline and role. */
425
501
  function selectGrant(ops, grant, role, proposer) {
426
502
  const { deps } = ops;
427
- if (ops.live.has(grant))
503
+ if (ops.liveOf(grant, role))
428
504
  throw new ValidationError("Unblind was already begun for this Grant");
429
505
  const input = deps.exc.retainedInput(grant);
430
506
  if (!input)
@@ -456,6 +532,14 @@ function consent(ops, grant, role, options) {
456
532
  if (role === "responder" && !isLocalGroupHab(deps.hby, hab)) {
457
533
  throw new ValidationError("An individual discloser answers an authenticated query without consent");
458
534
  }
535
+ if (role === "responder" && deps.hby.db.erpy.get([grant])) {
536
+ // The verifier already answered the presentation, so no query will come: nothing to approve or answer.
537
+ const closed = selected.input.serder;
538
+ const xid = closed.ilk === "xip" ? closed.said : String(closed.sad.x);
539
+ ops.remember(grant, { role: "responder", state: "unneeded", reason: UNNEEDED });
540
+ deps.changes?.push({ kind: "unblinding", id: grant });
541
+ return { op: null, begun: { grant, xid, role } };
542
+ }
459
543
  const owner = deps.unblinds.unblind(hab), xid = owner.approve(grant, { states: options.states });
460
544
  const begun = { grant, xid, role };
461
545
  // A current authenticated view needs no empty auxiliary query or new proof writes.
@@ -463,7 +547,7 @@ function consent(ops, grant, role, options) {
463
547
  const current = deps.exc.verifyGrantProofs(grant, selected.input.nests ?? []);
464
548
  if (current === true || typeof current === "object" && current.kind === "accept") {
465
549
  owner.abandon(xid);
466
- ops.settled.set(grant, { role: "requester", state: "done" });
550
+ ops.remember(grant, { role: "requester", state: "done" });
467
551
  deps.changes?.push({ kind: "unblinding", id: grant });
468
552
  return { op: null, begun };
469
553
  }
@@ -508,8 +592,7 @@ export function requestUnblind(ops, grant, options = {}) {
508
592
  }
509
593
  else
510
594
  op.query = op.owner.start(grant);
511
- ops.settled.delete(grant);
512
- ops.live.set(grant, op);
595
+ ops.begin(op);
513
596
  ops.deps.changes?.push({ kind: "unblinding", id: grant });
514
597
  }
515
598
  return begun;
@@ -526,9 +609,10 @@ export function requestUnblind(ops, grant, options = {}) {
526
609
  export function answerUnblind(ops, grant, options = {}) {
527
610
  const { deps } = ops;
528
611
  const { op, begun } = consent(ops, grant, "responder", options);
612
+ if (!op)
613
+ return begun;
529
614
  registerUnblindRoutes(ops);
530
- ops.settled.delete(grant);
531
- ops.live.set(grant, op);
615
+ ops.begin(op);
532
616
  deps.changes?.push({ kind: "unblinding", id: grant });
533
617
  return begun;
534
618
  }
@@ -578,7 +662,7 @@ function registerUnblindRoutes(ops) {
578
662
  acceptsNests: true,
579
663
  verify: (args) => structural(deps.hby, args.serder, args.nests ?? []) && !args.attachments.length && !args.essrs?.length,
580
664
  handle: (args) => {
581
- for (const op of ops.live.values()) {
665
+ for (const op of ops.liveOps()) {
582
666
  if (op.group && groupAccepts(deps.hby, op, args.serder, args.nests)) {
583
667
  op.group.pending.set(args.serder.said, { parent: args.serder, child: args.nests[0] });
584
668
  }
@@ -597,8 +681,9 @@ function registerUnblindRoutes(ops) {
597
681
  * @throws ValidationError when `grant` is not a Grant a local identifier is the disclosee of, and no
598
682
  * operation for it is live or remembered.
599
683
  */
600
- export function unblindingStatus(ops, grant) {
601
- const { deps } = ops, settled = ops.settled.get(grant), live = ops.live.get(grant);
684
+ export function unblindingStatus(ops, grant, party = {}) {
685
+ const { deps } = ops, role = chooseParty(deps, grant, party);
686
+ const settled = ops.settledOf(grant, role), live = ops.liveOf(grant, role);
602
687
  if (settled) {
603
688
  return settled.state === "done"
604
689
  ? { state: "done", role: settled.role }
@@ -614,6 +699,13 @@ export function unblindingStatus(ops, grant) {
614
699
  };
615
700
  }
616
701
  const input = deps.exc.retainedInput(grant), recipient = input?.serder.sad?.ri;
702
+ if (role === "responder" && input && input.serder.pre && deps.hby.habs.has(input.serder.pre)) {
703
+ // A discloser that has not begun (a group member before its answer) reads as not begun, not as an error.
704
+ const lifetime = deps.exc.exchangeDeadline(input.serder);
705
+ return lifetime && lapsed(deps.clock, lifetime)
706
+ ? { state: "expired", role: "responder", reason: EXPIRED }
707
+ : { state: "interrupted", role: "responder", reason: NOT_ANSWERED };
708
+ }
617
709
  if (!input || typeof recipient !== "string" || !deps.hby.habs.has(recipient)) {
618
710
  throw new ValidationError("No Unblind was begun for this Grant");
619
711
  }
@@ -630,13 +722,32 @@ export function unblindingStatus(ops, grant) {
630
722
  return { state: "expired", role: "requester", reason: EXPIRED };
631
723
  return { state: "interrupted", role: "requester", reason: INTERRUPTED };
632
724
  }
725
+ /**
726
+ * The party a read is for: the one asked for, else the Habery's only party, else (it holds both) the requester.
727
+ * Undefined when the Grant is not stored here, so only what is live or remembered answers.
728
+ *
729
+ * @throws ValidationError when `choice` names a party this Habery does not hold.
730
+ */
731
+ function chooseParty(deps, grant, choice) {
732
+ const input = deps.exc.retainedInput(grant);
733
+ if (!input)
734
+ return choice.role;
735
+ const held = [
736
+ ...(deps.hby.habs.has(String(input.serder.sad?.ri)) ? ["requester"] : []),
737
+ ...(input.serder.pre && deps.hby.habs.has(input.serder.pre) ? ["responder"] : []),
738
+ ];
739
+ if (choice.role && !held.includes(choice.role)) {
740
+ throw new ValidationError(`This Habery holds no ${choice.role} of this presentation`);
741
+ }
742
+ return choice.role ?? held[0];
743
+ }
633
744
  /**
634
745
  * Observe until the Unblind is done or ended, or the budget expires. Starts no worker and drives nothing:
635
746
  * someone else must call `processOnce`.
636
747
  */
637
- export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock }) {
638
- const settled = yield* observeUntil(() => unblindingStatus(ops, grant).state !== "active", { timeoutMs, clock });
639
- const status = unblindingStatus(ops, grant);
748
+ export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock, role }) {
749
+ const settled = yield* observeUntil(() => unblindingStatus(ops, grant, { role }).state !== "active" && !ops.owes(grant, role), { timeoutMs, clock });
750
+ const status = unblindingStatus(ops, grant, { role });
640
751
  return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
641
752
  }
642
753
  /**
@@ -646,15 +757,15 @@ export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clo
646
757
  *
647
758
  * @throws ValidationError when no Unblind was begun for `grant`.
648
759
  */
649
- export function abandonUnblinding(ops, grant, reason = "Abandoned by the user") {
650
- const status = unblindingStatus(ops, grant);
760
+ export function abandonUnblinding(ops, grant, reason = "Abandoned by the user", party = {}) {
761
+ const status = unblindingStatus(ops, grant, party);
651
762
  if (status.state !== "active" && status.state !== "interrupted")
652
763
  return;
653
- const live = ops.live.get(grant);
764
+ const live = ops.liveOf(grant, status.role);
654
765
  if (live)
655
766
  ops.end(live, "abandoned", reason);
656
767
  else {
657
- ops.settled.set(grant, { role: status.role, state: "abandoned", reason });
768
+ ops.remember(grant, { role: status.role, state: "abandoned", reason });
658
769
  ops.deps.changes?.push({ kind: "unblinding", id: grant });
659
770
  }
660
771
  }
@@ -6,6 +6,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
6
6
  var _MailboxPoller_dirty;
7
7
  /** Recipient mailbox polling: signed queries, awaited SSE intake and persisted cursor progression. */
8
8
  import { call, scoped, spawn } from "effection";
9
+ import { SignerNotHeldError } from "../identity/keeping.js";
9
10
  import { abortable } from "../runtime/abortable.js";
10
11
  import { doEvery, PROTOCOL_TOCK_MS } from "../runtime/doing.js";
11
12
  import { runtimeTurn } from "../runtime/runtime-turn.js";
@@ -89,6 +90,13 @@ export class MailboxPoller {
89
90
  writable: true,
90
91
  value: void 0
91
92
  });
93
+ /** Where unsignable polls are reported, when the host keeps such a record. */
94
+ Object.defineProperty(this, "signerConditions", {
95
+ enumerable: true,
96
+ configurable: true,
97
+ writable: true,
98
+ value: void 0
99
+ });
92
100
  /** Told when a mailbox answers 403. */
93
101
  Object.defineProperty(this, "onUnauthorized", {
94
102
  enumerable: true,
@@ -98,7 +106,8 @@ export class MailboxPoller {
98
106
  });
99
107
  /** Identifiers whose mailbox endpoints changed; the poller re-selects their workers on the next tock. */
100
108
  _MailboxPoller_dirty.set(this, new Set());
101
- const { timeouts, onPolled, onUnauthorized } = options;
109
+ const { timeouts, onPolled, onUnauthorized, signerConditions } = options;
110
+ this.signerConditions = signerConditions;
102
111
  this.onPolled = onPolled;
103
112
  this.onUnauthorized = onUnauthorized;
104
113
  this.hby = hby;
@@ -167,17 +176,26 @@ export class MailboxPoller {
167
176
  * @param target Mailbox subject habitat plus the actual local signer (a member for groups).
168
177
  * @param endpoint Selected remote endpoint AID and HTTP(S) URL.
169
178
  * @param onBatch Awaited consumer; successful completion precedes durable cursor advancement.
170
- * @returns How the poll ended; see {@link PollOutcome}.
179
+ * @returns How the poll ended; see {@link PollOutcome}. An `unsignable` poll carries the missing public key.
171
180
  */
172
181
  *pollRemoteEndpointOnce(target, endpoint, onBatch) {
173
182
  const cursor = this.mailboxDirector.remoteQueryCursor(target.targetPre, endpoint.eid);
174
183
  // deno-lint-ignore no-this-alias -- Generator callbacks have no lexical this.
175
184
  const self = this;
176
- // The concrete HTTP/SSE operation owns and joins this whole poll attempt.
177
- return yield* call(() => fetchMailboxMessages(target.signerHab, target.targetPre, endpoint.eid, endpoint.url, cursor, mailboxFetchTimeoutPolicy(this.timeoutPolicy), this.http, this.clock, function* deliverThenAdvanceCursor(message) {
178
- yield* call(() => onBatch({ pre: target.targetPre, eid: endpoint.eid, messages: [message.msg] }));
179
- self.mailboxDirector.updateRemoteCursor(target.targetPre, endpoint.eid, message.topic, message.idx);
180
- }));
185
+ // The concrete HTTP/SSE operation owns and joins this whole poll attempt. A signer the host released
186
+ // (while signing the query, or in the consumer's protocol work) ends the poll as `unsignable`.
187
+ try {
188
+ const outcome = yield* call(() => fetchMailboxMessages(target.signerHab, target.targetPre, endpoint.eid, endpoint.url, cursor, mailboxFetchTimeoutPolicy(this.timeoutPolicy), this.http, this.clock, function* deliverThenAdvanceCursor(message) {
189
+ yield* call(() => onBatch({ pre: target.targetPre, eid: endpoint.eid, messages: [message.msg] }));
190
+ self.mailboxDirector.updateRemoteCursor(target.targetPre, endpoint.eid, message.topic, message.idx);
191
+ }));
192
+ return { outcome };
193
+ }
194
+ catch (error) {
195
+ if (error instanceof SignerNotHeldError)
196
+ return { outcome: "unsignable", missing: error.pub };
197
+ throw error;
198
+ }
181
199
  }
182
200
  /**
183
201
  * Keep one long-lived worker running for each currently configured remote endpoint.
@@ -225,7 +243,12 @@ export class MailboxPoller {
225
243
  *remoteEndpointWorker(target, endpoint, onBatch) {
226
244
  let state = { backoffMs: 0 };
227
245
  while (true) {
228
- const outcome = yield* this.pollRemoteEndpointOnce(target, endpoint, onBatch);
246
+ const { outcome, missing } = yield* this.pollRemoteEndpointOnce(target, endpoint, onBatch);
247
+ const source = `mailbox-poller:${target.targetPre}/${endpoint.eid}`;
248
+ if (missing)
249
+ this.signerConditions?.report(source, missing, this.clock.now());
250
+ else
251
+ this.signerConditions?.clear(source);
229
252
  const step = nextPoll(state, outcome, {
230
253
  initialMs: this.timeoutPolicy.refusalInitialMs,
231
254
  maxMs: this.timeoutPolicy.refusalMaxMs,
@@ -241,7 +264,11 @@ export class MailboxPoller {
241
264
  yield* waitOnClock(this.clock, step.action.ms);
242
265
  break;
243
266
  case "wait":
244
- yield* waitOnClock(this.clock, step.action.ms);
267
+ // A released signer is waited out until one is held again, not for the full backoff.
268
+ if (missing && this.signerConditions)
269
+ yield* this.signerConditions.waitForHold(this.clock, step.action.ms);
270
+ else
271
+ yield* waitOnClock(this.clock, step.action.ms);
245
272
  break;
246
273
  }
247
274
  }
@@ -347,6 +374,7 @@ export function nextPoll(state, outcome, backoff = MailboxPoller.RefusalBackoff)
347
374
  }
348
375
  case "no-response":
349
376
  case "unreachable":
377
+ case "unsignable":
350
378
  case "refused":
351
379
  return backOff(state, backoff);
352
380
  }
@@ -264,9 +264,12 @@ export function hostedEndpointPathMatches(hby, pathname, eids) {
264
264
  * @param pathname Incoming URL pathname to compare against accepted endpoint base paths.
265
265
  * @param resourceSuffix Protocol resource suffix sought beneath the hosted base path.
266
266
  * @param eids Optional explicit hosted-AID filter; do not infer host eligibility from all local identities.
267
+ * @param destination The request's `CESR-DESTINATION`: when several local identifiers share the longest base
268
+ * path (one ingress URL for many identifiers), the one it names is the match. Without it, or when it names
269
+ * none of them, the tie stays ambiguous.
267
270
  * @returns Matching hosted endpoint projection; ambiguity follows this helper's documented selection policy.
268
271
  */
269
- export function resolveHostedEndpointPath(hby, pathname, resourceSuffix = "", eids) {
272
+ export function resolveHostedEndpointPath(hby, pathname, resourceSuffix = "", eids, destination) {
270
273
  const matches = hostedEndpointPathMatches(hby, pathname, eids)
271
274
  .filter((match) => resourceSuffix.length === 0
272
275
  ? true
@@ -275,8 +278,11 @@ export function resolveHostedEndpointPath(hby, pathname, resourceSuffix = "", ei
275
278
  return { kind: "none", endpoint: null, relativePath: null };
276
279
  }
277
280
  const longest = matches[0].basePath.length;
278
- const narrowed = matches.filter((match) => match.basePath.length === longest);
279
- if (narrowed.length > 1) {
281
+ let narrowed = matches.filter((match) => match.basePath.length === longest);
282
+ if (narrowed.length > 1 && destination) {
283
+ narrowed = narrowed.filter((match) => match.eid === destination);
284
+ }
285
+ if (narrowed.length !== 1) {
280
286
  return { kind: "ambiguous", endpoint: null, relativePath: null };
281
287
  }
282
288
  const endpoint = narrowed[0];
@@ -35,13 +35,16 @@
35
35
  * restart never leaves a delegated event waiting for one. Collaborators are
36
36
  * narrow (ADR-0013); nothing here reaches a runtime.
37
37
  */
38
- import { Diger, Ilks, NumberPrimitive, NumDex, Verfer } from "cesr-ts";
38
+ import { Diger, Ilks, NumberPrimitive, NumDex, ParserError, Verfer } from "cesr-ts";
39
39
  import { ValidationError } from "../../core/errors.js";
40
40
  import { Kever } from "../../core/kel/kever.js";
41
41
  import { dgKey } from "../../db/core/keys.js";
42
42
  import { pinAuthorizingSeal, retryDelegatorWitnessQuery, } from "../identity/delegating.js";
43
43
  import { observeUntil } from "../runtime/workflow-contract.js";
44
- import { groupWitnesser, isLocalGroupHab, localGroupMember } from "./group-members.js";
44
+ import { groupSigningMembers, groupWitnesser, isLocalGroupHab, localGroupMember } from "./group-members.js";
45
+ import { remoteGroupMembers } from "./multisig-messaging.js";
46
+ /** Forwarding attempts of one completed event before the Counselor stops retrying its failed members. */
47
+ export const FORWARD_ATTEMPTS = 8;
45
48
  /** Grace before a non-elected member submits receipts itself. */
46
49
  export const DEFAULT_FALLBACK_AFTER_MS = 60_000;
47
50
  /** First and longest wait between receipt attempts for one event. */
@@ -80,7 +83,7 @@ export class Counselor {
80
83
  * @param mailboxes Told when one of the group's rotations completes; the member runtime's publisher then tells
81
84
  * the group's mailboxes the new key state, and several members doing so is harmless.
82
85
  */
83
- constructor(hby, receipting, clock, changes, delegation, mailboxes, receiptRetry = DEFAULT_RECEIPT_RETRY) {
86
+ constructor(hby, receipting, clock, changes, delegation, mailboxes, receiptRetry = DEFAULT_RECEIPT_RETRY, forwarding) {
84
87
  Object.defineProperty(this, "hby", {
85
88
  enumerable: true,
86
89
  configurable: true,
@@ -123,12 +126,31 @@ export class Counselor {
123
126
  writable: true,
124
127
  value: receiptRetry
125
128
  });
129
+ Object.defineProperty(this, "forwarding", {
130
+ enumerable: true,
131
+ configurable: true,
132
+ writable: true,
133
+ value: forwarding
134
+ });
126
135
  Object.defineProperty(this, "runs", {
127
136
  enumerable: true,
128
137
  configurable: true,
129
138
  writable: true,
130
139
  value: new Map()
131
140
  });
141
+ /** Completed events this member, as the elected one, still owes to other members; memory only. */
142
+ Object.defineProperty(this, "forwards", {
143
+ enumerable: true,
144
+ configurable: true,
145
+ writable: true,
146
+ value: new Map()
147
+ });
148
+ Object.defineProperty(this, "forwarded", {
149
+ enumerable: true,
150
+ configurable: true,
151
+ writable: true,
152
+ value: new Map()
153
+ });
132
154
  Object.defineProperty(this, "anchorQuery", {
133
155
  enumerable: true,
134
156
  configurable: true,
@@ -264,11 +286,53 @@ export class Counselor {
264
286
  *processOnce() {
265
287
  // A runtime calls this every turn; with nothing counseled, three row counts are the whole cost.
266
288
  const db = this.hby.db;
267
- if (db.gpse.cnt() === 0 && db.gdee.cnt() === 0 && db.gpwe.cnt() === 0)
289
+ if (db.gpse.cnt() === 0 && db.gdee.cnt() === 0 && db.gpwe.cnt() === 0 && this.forwards.size === 0)
268
290
  return;
269
291
  this.processSignatures();
270
292
  yield* this.processDelegation();
271
293
  yield* this.processWitnesses();
294
+ yield* this.forwardCompleted();
295
+ }
296
+ /** What became of the completed `event` this member forwarded to the others, per member; empty before it did. */
297
+ forwardOutcomes(event) {
298
+ return this.forwarded.get(this.runKey(event)) ?? [];
299
+ }
300
+ /** Send each completed event to the members that may not have it; failed members are retried with a doubling delay. */
301
+ *forwardCompleted() {
302
+ if (!this.forwarding)
303
+ return;
304
+ for (const [key, owed] of [...this.forwards]) {
305
+ const now = this.clock.now();
306
+ if (now < owed.nextAt)
307
+ continue;
308
+ let outcomes;
309
+ try {
310
+ outcomes = yield* this.forwarding.forward(owed.event, owed.members);
311
+ }
312
+ catch (error) {
313
+ const reason = error instanceof Error ? error.message : String(error);
314
+ outcomes = owed.members.map((recipient) => ({
315
+ recipient,
316
+ exnSaid: owed.event.said,
317
+ payloadSaid: owed.event.said,
318
+ outcome: "failed",
319
+ deliveries: [],
320
+ reason,
321
+ }));
322
+ }
323
+ const failed = outcomes.filter((outcome) => outcome.outcome === "failed");
324
+ this.forwarded.set(key, [
325
+ ...(this.forwarded.get(key)?.filter((prior) => !outcomes.some((o) => o.recipient === prior.recipient)) ?? []),
326
+ ...outcomes,
327
+ ]);
328
+ owed.attempts++;
329
+ if (!failed.length || owed.attempts >= FORWARD_ATTEMPTS) {
330
+ this.forwards.delete(key);
331
+ continue;
332
+ }
333
+ owed.members = failed.map((outcome) => outcome.recipient);
334
+ owed.nextAt = now + Math.min(this.receiptRetry.baseMs * 2 ** owed.attempts, this.receiptRetry.maxMs);
335
+ }
272
336
  }
273
337
  /** Move events accepted with their escrowed SAID on to delegation or witnesses. */
274
338
  processSignatures() {
@@ -342,6 +406,14 @@ export class Counselor {
342
406
  });
343
407
  this.changes?.push({ kind: "group", id: `${event.pre}:${event.sn}` });
344
408
  }
409
+ /** The elected member owes the completed event to the members that did not sign it into existence. */
410
+ owe(event) {
411
+ if (!this.forwarding || !groupWitnesser(this.hby, event.pre, event))
412
+ return;
413
+ const members = remoteGroupMembers(this.hby, localGroupMember(this.hby, event.pre), groupSigningMembers(this.hby, event.pre));
414
+ if (members.length)
415
+ this.forwards.set(this.runKey(event), { event, members, attempts: 0, nextAt: 0 });
416
+ }
345
417
  /** Record completion of a counseled event and drop its row from `table`. */
346
418
  finish(table, event, row) {
347
419
  const db = this.hby.db;
@@ -351,6 +423,7 @@ export class Counselor {
351
423
  });
352
424
  this.runs.delete(this.runKey(event));
353
425
  this.changes?.push({ kind: "group", id: `${event.pre}:${event.sn}` });
426
+ this.owe(event);
354
427
  const ilk = db.getEvtSerder(event.pre, event.said)?.ilk;
355
428
  if (ilk === Ilks.rot || ilk === Ilks.drt) {
356
429
  this.mailboxes?.rotated({ kin: "rotated", pre: event.pre, sn: event.sn, said: event.said });
@@ -394,6 +467,12 @@ export class Counselor {
394
467
  // A witness refusing or being unreachable is a remote outcome: report it in status and retry later.
395
468
  run.failure = error instanceof Error ? error.message : String(error);
396
469
  run.delay = Math.min(run.delay * 2, this.receiptRetry.maxMs);
470
+ // A witness that answered with a reply this member cannot read (a receipt the elected member already
471
+ // obtained) will not answer better later: submit on the next attempt instead of waiting out the grace.
472
+ if (!submit && error instanceof ParserError) {
473
+ run.fallbackAfterMs = 0;
474
+ run.delay = this.receiptRetry.baseMs;
475
+ }
397
476
  }
398
477
  run.nextAt = now + run.delay;
399
478
  if (!this.hby.db.fullyWitnessed(serder))
@@ -0,0 +1,27 @@
1
+ import { acceptedEventReplayMessage } from "../identity/habbing.js";
2
+ import { localGroupMember } from "./group-members.js";
3
+ import { failedGroupDelivery } from "./multisig-messaging.js";
4
+ /** The Counselor's forwarding port over a Habery and its Poster; the Poster's delivery worker must run. */
5
+ export function groupEventForwarding(hby, postman) {
6
+ return {
7
+ *forward(event, members) {
8
+ const { serder } = acceptedEventReplayMessage(hby, event.pre, event.sn);
9
+ const { message } = acceptedEventReplayMessage(hby, event.pre, event.sn, serder.pvrsn);
10
+ const sender = localGroupMember(hby, event.pre);
11
+ const outcomes = [];
12
+ for (const recipient of members) {
13
+ const ref = { recipient, exnSaid: event.said, payloadSaid: event.said };
14
+ try {
15
+ const outcome = yield* postman.wait(postman.send({ src: sender.pre, dest: recipient, topic: "/replay", msg: message }, { notify: true }));
16
+ outcomes.push(outcome.kin === "failed"
17
+ ? failedGroupDelivery(ref, outcome.error)
18
+ : { ...ref, outcome: "delivered", deliveries: [...outcome.deliveries] });
19
+ }
20
+ catch (error) {
21
+ outcomes.push(failedGroupDelivery(ref, error));
22
+ }
23
+ }
24
+ return outcomes;
25
+ },
26
+ };
27
+ }