keri-ts 2.2.1 → 2.2.3

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 (115) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +1 -127
  2. package/esm/keri/npm/src/acdc/dhking.js +6 -1
  3. package/esm/keri/npm/src/acdc/salting.js +18 -0
  4. package/esm/keri/npm/src/acdc/scheming.js +31 -18
  5. package/esm/keri/npm/src/acdc/unblinding.js +112 -6
  6. package/esm/keri/npm/src/app/delivery/forwarding.js +9 -0
  7. package/esm/keri/npm/src/app/delivery/respondant.js +21 -0
  8. package/esm/keri/npm/src/app/identity/delegating.js +2 -5
  9. package/esm/keri/npm/src/app/identity/habbing.js +52 -6
  10. package/esm/keri/npm/src/app/identity/keeping.js +38 -1
  11. package/esm/keri/npm/src/app/ipex/group-issuance.js +9 -6
  12. package/esm/keri/npm/src/app/ipex/ipex-building.js +2 -1
  13. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +32 -8
  14. package/esm/keri/npm/src/app/ipex/issuance.js +142 -43
  15. package/esm/keri/npm/src/app/ipex/pending-presentations.js +50 -0
  16. package/esm/keri/npm/src/app/ipex/presenting.js +36 -1
  17. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +150 -36
  18. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +37 -9
  19. package/esm/keri/npm/src/app/mailbox/mailboxing.js +9 -3
  20. package/esm/keri/npm/src/app/multisig/counseling.js +76 -3
  21. package/esm/keri/npm/src/app/multisig/event-forwarding.js +27 -0
  22. package/esm/keri/npm/src/app/multisig/group-members.js +22 -0
  23. package/esm/keri/npm/src/app/multisig/grouping.js +120 -1
  24. package/esm/keri/npm/src/app/multisig/proposals.js +1 -1
  25. package/esm/keri/npm/src/app/multisig/replace-member.js +34 -0
  26. package/esm/keri/npm/src/app/runtime/agent-runtime.js +43 -23
  27. package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +59 -25
  28. package/esm/keri/npm/src/app/runtime/operation-owners.js +1 -0
  29. package/esm/keri/npm/src/app/runtime/signer-conditions.js +51 -0
  30. package/esm/keri/npm/src/app/transport/cesr-egress.js +6 -0
  31. package/esm/keri/npm/src/core/errors.js +91 -3
  32. package/esm/keri/npm/src/core/json-schema-meta.js +688 -0
  33. package/esm/keri/npm/src/core/json-schema.js +147 -0
  34. package/esm/keri/npm/src/core/kel/eventing.js +20 -2
  35. package/esm/keri/npm/src/core/scheming.js +27 -26
  36. package/esm/keri/npm/src/db/replay/tel-replay.js +6 -0
  37. package/esm/keri/npm/src/db/store-lock.js +12 -0
  38. package/esm/keri/npm/src/runtime/index.js +9 -7
  39. package/esm/keri/npm/src/runtime/web/output-barrier.js +9 -0
  40. package/esm/keri/npm/src/version.js +2 -2
  41. package/package.json +2 -2
  42. package/types/_dnt.polyfills.d.ts +0 -99
  43. package/types/_dnt.polyfills.d.ts.map +1 -1
  44. package/types/acdc/dhking.d.ts.map +1 -1
  45. package/types/acdc/salting.d.ts +2 -0
  46. package/types/acdc/salting.d.ts.map +1 -1
  47. package/types/acdc/scheming.d.ts +2 -0
  48. package/types/acdc/scheming.d.ts.map +1 -1
  49. package/types/acdc/unblinding.d.ts +16 -5
  50. package/types/acdc/unblinding.d.ts.map +1 -1
  51. package/types/app/delivery/forwarding.d.ts +1 -0
  52. package/types/app/delivery/forwarding.d.ts.map +1 -1
  53. package/types/app/delivery/respondant.d.ts +7 -0
  54. package/types/app/delivery/respondant.d.ts.map +1 -1
  55. package/types/app/identity/delegating.d.ts.map +1 -1
  56. package/types/app/identity/habbing.d.ts +21 -1
  57. package/types/app/identity/habbing.d.ts.map +1 -1
  58. package/types/app/identity/keeping.d.ts +18 -0
  59. package/types/app/identity/keeping.d.ts.map +1 -1
  60. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  61. package/types/app/ipex/ipex-building.d.ts.map +1 -1
  62. package/types/app/ipex/ipex-discloser.d.ts +31 -19
  63. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  64. package/types/app/ipex/issuance.d.ts +46 -10
  65. package/types/app/ipex/issuance.d.ts.map +1 -1
  66. package/types/app/ipex/pending-presentations.d.ts +62 -1
  67. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  68. package/types/app/ipex/presenting.d.ts +39 -4
  69. package/types/app/ipex/presenting.d.ts.map +1 -1
  70. package/types/app/ipex/unblinding-workflow.d.ts +51 -12
  71. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  72. package/types/app/mailbox/mailbox-poller.d.ts +11 -2
  73. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  74. package/types/app/mailbox/mailboxing.d.ts +4 -1
  75. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  76. package/types/app/multisig/counseling.d.ts +22 -1
  77. package/types/app/multisig/counseling.d.ts.map +1 -1
  78. package/types/app/multisig/event-forwarding.d.ts +6 -0
  79. package/types/app/multisig/event-forwarding.d.ts.map +1 -0
  80. package/types/app/multisig/group-members.d.ts +6 -0
  81. package/types/app/multisig/group-members.d.ts.map +1 -1
  82. package/types/app/multisig/grouping.d.ts +26 -0
  83. package/types/app/multisig/grouping.d.ts.map +1 -1
  84. package/types/app/multisig/proposals.d.ts +3 -2
  85. package/types/app/multisig/proposals.d.ts.map +1 -1
  86. package/types/app/multisig/proposing.d.ts +8 -1
  87. package/types/app/multisig/proposing.d.ts.map +1 -1
  88. package/types/app/multisig/replace-member.d.ts +30 -0
  89. package/types/app/multisig/replace-member.d.ts.map +1 -0
  90. package/types/app/runtime/agent-runtime.d.ts +3 -0
  91. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  92. package/types/app/runtime/ipex-auxiliaries.d.ts +8 -3
  93. package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -1
  94. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  95. package/types/app/runtime/signer-conditions.d.ts +32 -0
  96. package/types/app/runtime/signer-conditions.d.ts.map +1 -0
  97. package/types/app/transport/cesr-egress.d.ts +1 -0
  98. package/types/app/transport/cesr-egress.d.ts.map +1 -1
  99. package/types/core/errors.d.ts +20 -1
  100. package/types/core/errors.d.ts.map +1 -1
  101. package/types/core/json-schema-meta.d.ts +19 -0
  102. package/types/core/json-schema-meta.d.ts.map +1 -0
  103. package/types/core/json-schema.d.ts +29 -0
  104. package/types/core/json-schema.d.ts.map +1 -0
  105. package/types/core/scheming.d.ts +1 -0
  106. package/types/core/scheming.d.ts.map +1 -1
  107. package/types/db/replay/tel-replay.d.ts +1 -0
  108. package/types/db/replay/tel-replay.d.ts.map +1 -1
  109. package/types/db/store-lock.d.ts +2 -0
  110. package/types/db/store-lock.d.ts.map +1 -1
  111. package/types/runtime/index.d.ts +11 -9
  112. package/types/runtime/index.d.ts.map +1 -1
  113. package/types/runtime/web/output-barrier.d.ts +1 -0
  114. package/types/runtime/web/output-barrier.d.ts.map +1 -1
  115. 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,10 +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"));
137
167
  }
138
- /** Whether the live operation for `grant` still holds a message the transport has not taken. */
139
- owes(grant) {
140
- return (this.live.get(grant)?.outbox.length ?? 0) > 0;
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");
141
186
  }
142
187
  /**
143
188
  * Whether a pass has work: a live operation, or an owner holds an authenticated peer query whose reply is
@@ -156,19 +201,23 @@ export class UnblindingOperations {
156
201
  const { deps } = this;
157
202
  this.adopt();
158
203
  for (const op of [...this.live.values()]) {
159
- if (op.busy || !this.live.has(op.grant))
204
+ if (op.busy || this.live.get(unblindingKey(op.grant, op.role)) !== op)
160
205
  continue;
161
206
  op.busy = true;
162
207
  try {
163
208
  const left = op.expires - BigInt(deps.clock.now()) * 1000n;
164
209
  if (left <= 0n)
165
- this.end(op, "expired", EXPIRED);
210
+ this.end(op, "expired", this.expiry(op));
166
211
  else if ((yield* race([this.advance(op), this.timer(Number(left) / 1000)])) === "expired") {
167
- this.end(op, "expired", EXPIRED);
212
+ this.end(op, "expired", this.expiry(op));
168
213
  }
169
214
  }
170
215
  catch (error) {
171
- 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));
172
221
  }
173
222
  finally {
174
223
  op.busy = false;
@@ -185,7 +234,7 @@ export class UnblindingOperations {
185
234
  const { deps } = this;
186
235
  for (const owner of deps.unblinds.unblindOwners()) {
187
236
  for (const { xid, grant } of owner.awaitingReply()) {
188
- if (this.live.has(grant))
237
+ if (this.live.has(unblindingKey(grant, "responder")))
189
238
  continue;
190
239
  let input, deadline, expires;
191
240
  try {
@@ -196,14 +245,18 @@ export class UnblindingOperations {
196
245
  catch (error) {
197
246
  // Unreadable stored state fails this reply alone; release the owner's approval so it is not retried.
198
247
  owner.abandon(xid);
199
- 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
+ });
200
253
  deps.changes?.push({ kind: "unblinding", id: grant });
201
254
  continue;
202
255
  }
203
256
  if (!input || !deadline || expires === null || BigInt(deps.clock.now()) * 1000n >= expires)
204
257
  continue;
205
- this.settled.delete(grant);
206
- this.live.set(grant, {
258
+ this.settled.delete(unblindingKey(grant, "responder"));
259
+ this.live.set(unblindingKey(grant, "responder"), {
207
260
  grant,
208
261
  xid,
209
262
  role: "responder",
@@ -223,15 +276,30 @@ export class UnblindingOperations {
223
276
  }
224
277
  }
225
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
+ }
226
284
  /** End a live operation: release the approval, remember why until the process ends, tell the sink. */
227
285
  end(op, state, reason) {
228
286
  this.release(op);
229
- this.settled.set(op.grant, { role: op.role, state, reason });
287
+ this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state, reason });
230
288
  this.deps.changes?.push({ kind: "unblinding", id: op.grant });
231
289
  }
290
+ /**
291
+ * End, as abandoned with `reason`, every live Unblind of local group `group` whose party member is `member`.
292
+ * Releasing the approval drops the owner's held query. Returns the Grants ended.
293
+ */
294
+ endBoundToMember(group, member, reason) {
295
+ const bound = [...this.live.values()].filter(op => op.hab.pre === group && op.group?.member.pre === member);
296
+ for (const op of bound)
297
+ this.end(op, "abandoned", reason);
298
+ return bound.map(op => op.grant);
299
+ }
232
300
  /** Drop the live entry and the owner's approval and frozen messages; delivered bytes stay the peer's. */
233
301
  release(op) {
234
- this.live.delete(op.grant);
302
+ this.live.delete(unblindingKey(op.grant, op.role));
235
303
  try {
236
304
  op.owner.abandon(op.xid);
237
305
  }
@@ -242,7 +310,7 @@ export class UnblindingOperations {
242
310
  }
243
311
  finished(op) {
244
312
  this.release(op);
245
- this.settled.set(op.grant, { role: op.role, state: "done" });
313
+ this.settled.set(unblindingKey(op.grant, op.role), { role: op.role, state: "done" });
246
314
  this.deps.changes?.push({ kind: "unblinding", id: op.grant });
247
315
  }
248
316
  *timer(milliseconds) {
@@ -413,9 +481,23 @@ export class UnblindingOperations {
413
481
  op.finished = true;
414
482
  return "stepped";
415
483
  }
484
+ *deliverToParty(message) {
485
+ const { hby, reactor, delivery } = this.deps, recipient = hby.habs.get(message.dest);
486
+ if (!recipient || !reactor)
487
+ return yield* delivery.deliver(message);
488
+ yield* reactor.processBytes(message.msg);
489
+ const reached = [`local:${recipient.pre}`];
490
+ if (!isLocalGroupHab(hby, recipient))
491
+ return reached;
492
+ const member = localGroupMember(hby, recipient.pre);
493
+ for (const dest of remoteGroupMembers(hby, member, groupSigningMembers(hby, recipient.pre))) {
494
+ reached.push(...yield* delivery.deliver({ ...message, dest }));
495
+ }
496
+ return reached;
497
+ }
416
498
  /** One pass: send what is queued, step only once the queue is empty, send what the step queued. */
417
499
  *advance(op) {
418
- const drain = () => op.outbox.drain(this.deps.delivery, this.deps.clock);
500
+ const drain = () => op.outbox.drain(this.transport, this.deps.clock);
419
501
  if (!(yield* drain()))
420
502
  return "stepped";
421
503
  if (!op.finished)
@@ -428,7 +510,7 @@ export class UnblindingOperations {
428
510
  /** The exact live Grant the local party may consent to, with its deadline and role. */
429
511
  function selectGrant(ops, grant, role, proposer) {
430
512
  const { deps } = ops;
431
- if (ops.live.has(grant))
513
+ if (ops.liveOf(grant, role))
432
514
  throw new ValidationError("Unblind was already begun for this Grant");
433
515
  const input = deps.exc.retainedInput(grant);
434
516
  if (!input)
@@ -460,6 +542,14 @@ function consent(ops, grant, role, options) {
460
542
  if (role === "responder" && !isLocalGroupHab(deps.hby, hab)) {
461
543
  throw new ValidationError("An individual discloser answers an authenticated query without consent");
462
544
  }
545
+ if (role === "responder" && deps.hby.db.erpy.get([grant])) {
546
+ // The verifier already answered the presentation, so no query will come: nothing to approve or answer.
547
+ const closed = selected.input.serder;
548
+ const xid = closed.ilk === "xip" ? closed.said : String(closed.sad.x);
549
+ ops.remember(grant, { role: "responder", state: "unneeded", reason: UNNEEDED });
550
+ deps.changes?.push({ kind: "unblinding", id: grant });
551
+ return { op: null, begun: { grant, xid, role } };
552
+ }
463
553
  const owner = deps.unblinds.unblind(hab), xid = owner.approve(grant, { states: options.states });
464
554
  const begun = { grant, xid, role };
465
555
  // A current authenticated view needs no empty auxiliary query or new proof writes.
@@ -467,7 +557,7 @@ function consent(ops, grant, role, options) {
467
557
  const current = deps.exc.verifyGrantProofs(grant, selected.input.nests ?? []);
468
558
  if (current === true || typeof current === "object" && current.kind === "accept") {
469
559
  owner.abandon(xid);
470
- ops.settled.set(grant, { role: "requester", state: "done" });
560
+ ops.remember(grant, { role: "requester", state: "done" });
471
561
  deps.changes?.push({ kind: "unblinding", id: grant });
472
562
  return { op: null, begun };
473
563
  }
@@ -512,8 +602,7 @@ export function requestUnblind(ops, grant, options = {}) {
512
602
  }
513
603
  else
514
604
  op.query = op.owner.start(grant);
515
- ops.settled.delete(grant);
516
- ops.live.set(grant, op);
605
+ ops.begin(op);
517
606
  ops.deps.changes?.push({ kind: "unblinding", id: grant });
518
607
  }
519
608
  return begun;
@@ -530,9 +619,10 @@ export function requestUnblind(ops, grant, options = {}) {
530
619
  export function answerUnblind(ops, grant, options = {}) {
531
620
  const { deps } = ops;
532
621
  const { op, begun } = consent(ops, grant, "responder", options);
622
+ if (!op)
623
+ return begun;
533
624
  registerUnblindRoutes(ops);
534
- ops.settled.delete(grant);
535
- ops.live.set(grant, op);
625
+ ops.begin(op);
536
626
  deps.changes?.push({ kind: "unblinding", id: grant });
537
627
  return begun;
538
628
  }
@@ -582,7 +672,7 @@ function registerUnblindRoutes(ops) {
582
672
  acceptsNests: true,
583
673
  verify: (args) => structural(deps.hby, args.serder, args.nests ?? []) && !args.attachments.length && !args.essrs?.length,
584
674
  handle: (args) => {
585
- for (const op of ops.live.values()) {
675
+ for (const op of ops.liveOps()) {
586
676
  if (op.group && groupAccepts(deps.hby, op, args.serder, args.nests)) {
587
677
  op.group.pending.set(args.serder.said, { parent: args.serder, child: args.nests[0] });
588
678
  }
@@ -601,8 +691,9 @@ function registerUnblindRoutes(ops) {
601
691
  * @throws ValidationError when `grant` is not a Grant a local identifier is the disclosee of, and no
602
692
  * operation for it is live or remembered.
603
693
  */
604
- export function unblindingStatus(ops, grant) {
605
- const { deps } = ops, settled = ops.settled.get(grant), live = ops.live.get(grant);
694
+ export function unblindingStatus(ops, grant, party = {}) {
695
+ const { deps } = ops, role = chooseParty(deps, grant, party);
696
+ const settled = ops.settledOf(grant, role), live = ops.liveOf(grant, role);
606
697
  if (settled) {
607
698
  return settled.state === "done"
608
699
  ? { state: "done", role: settled.role }
@@ -618,6 +709,13 @@ export function unblindingStatus(ops, grant) {
618
709
  };
619
710
  }
620
711
  const input = deps.exc.retainedInput(grant), recipient = input?.serder.sad?.ri;
712
+ if (role === "responder" && input && input.serder.pre && deps.hby.habs.has(input.serder.pre)) {
713
+ // A discloser that has not begun (a group member before its answer) reads as not begun, not as an error.
714
+ const lifetime = deps.exc.exchangeDeadline(input.serder);
715
+ return lifetime && lapsed(deps.clock, lifetime)
716
+ ? { state: "expired", role: "responder", reason: EXPIRED }
717
+ : { state: "interrupted", role: "responder", reason: NOT_ANSWERED };
718
+ }
621
719
  if (!input || typeof recipient !== "string" || !deps.hby.habs.has(recipient)) {
622
720
  throw new ValidationError("No Unblind was begun for this Grant");
623
721
  }
@@ -634,16 +732,32 @@ export function unblindingStatus(ops, grant) {
634
732
  return { state: "expired", role: "requester", reason: EXPIRED };
635
733
  return { state: "interrupted", role: "requester", reason: INTERRUPTED };
636
734
  }
735
+ /**
736
+ * The party a read is for: the one asked for, else the Habery's only party, else (it holds both) the requester.
737
+ * Undefined when the Grant is not stored here, so only what is live or remembered answers.
738
+ *
739
+ * @throws ValidationError when `choice` names a party this Habery does not hold.
740
+ */
741
+ function chooseParty(deps, grant, choice) {
742
+ const input = deps.exc.retainedInput(grant);
743
+ if (!input)
744
+ return choice.role;
745
+ const held = [
746
+ ...(deps.hby.habs.has(String(input.serder.sad?.ri)) ? ["requester"] : []),
747
+ ...(input.serder.pre && deps.hby.habs.has(input.serder.pre) ? ["responder"] : []),
748
+ ];
749
+ if (choice.role && !held.includes(choice.role)) {
750
+ throw new ValidationError(`This Habery holds no ${choice.role} of this presentation`);
751
+ }
752
+ return choice.role ?? held[0];
753
+ }
637
754
  /**
638
755
  * Observe until the Unblind is done or ended, or the budget expires. Starts no worker and drives nothing:
639
756
  * someone else must call `processOnce`.
640
757
  */
641
- export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock }) {
642
- const settled = yield* observeUntil(() => unblindingStatus(ops, grant).state !== "active" && !ops.owes(grant), {
643
- timeoutMs,
644
- clock,
645
- });
646
- const status = unblindingStatus(ops, grant);
758
+ export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clock, role }) {
759
+ const settled = yield* observeUntil(() => unblindingStatus(ops, grant, { role }).state !== "active" && !ops.owes(grant, role), { timeoutMs, clock });
760
+ const status = unblindingStatus(ops, grant, { role });
647
761
  return settled ? { outcome: "settled", status } : { outcome: "timed-out", status, timeoutMs };
648
762
  }
649
763
  /**
@@ -653,15 +767,15 @@ export function* waitForUnblinding(ops, grant, { timeoutMs, clock = ops.deps.clo
653
767
  *
654
768
  * @throws ValidationError when no Unblind was begun for `grant`.
655
769
  */
656
- export function abandonUnblinding(ops, grant, reason = "Abandoned by the user") {
657
- const status = unblindingStatus(ops, grant);
770
+ export function abandonUnblinding(ops, grant, reason = "Abandoned by the user", party = {}) {
771
+ const status = unblindingStatus(ops, grant, party);
658
772
  if (status.state !== "active" && status.state !== "interrupted")
659
773
  return;
660
- const live = ops.live.get(grant);
774
+ const live = ops.liveOf(grant, status.role);
661
775
  if (live)
662
776
  ops.end(live, "abandoned", reason);
663
777
  else {
664
- ops.settled.set(grant, { role: status.role, state: "abandoned", reason });
778
+ ops.remember(grant, { role: status.role, state: "abandoned", reason });
665
779
  ops.deps.changes?.push({ kind: "unblinding", id: grant });
666
780
  }
667
781
  }
@@ -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];
@@ -41,7 +41,10 @@ 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 });
@@ -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
+ }