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
@@ -12,12 +12,13 @@ import { validateRip, validateUpdate } from "../../acdc/regeventing.js";
12
12
  import { ValidationError } from "../../core/errors.js";
13
13
  import { exchange, isIpexVerb, isV2RegistryRoute, multisigChildField, nestedIpexRoute, nestedMultisigIlks, } from "../../core/exchange/protocol-exchanging.js";
14
14
  import { Kevery } from "../../core/kel/eventing.js";
15
+ import { Kever } from "../../core/kel/kever.js";
15
16
  import { BasicReplyRouteHandler, Revery, Router } from "../../core/routing.js";
16
17
  import { Exchanger } from "../exchanges/exchanging.js";
17
18
  import { IpexV2Handler } from "../ipex/ipex-v2.js";
18
19
  import { dispatchEnvelope, envelopeFromFrame } from "../protocol/parsering.js";
19
20
  import { abortable, rethrowCleanupFailure } from "../runtime/abortable.js";
20
- import { groupSigningMembers, localIpexContribution } from "./group-members.js";
21
+ import { groupSigningMembers, isLocalGroupHab, localIpexContribution } from "./group-members.js";
21
22
  import { multisigEmbeddedSection, multisigPayloadSaid } from "./multisig-messaging.js";
22
23
  /** KERIpy multisig route constants. */
23
24
  export const MULTISIG_ICP_ROUTE = "/multisig/icp";
@@ -579,6 +580,14 @@ export class Multiplexor {
579
580
  else
580
581
  yield* this.processApproved(nests);
581
582
  }
583
+ else if (!unreplayed && this.alreadyComplete(serder, nests, route)) {
584
+ // Consent only adds this member's signature; an event the other members' signatures already complete needs
585
+ // none, so a member that only receives it takes it on receipt instead of waiting for an approval it need not give.
586
+ if (serder.pvrsn.major === 1)
587
+ yield* this.processLegacyApproved(serder);
588
+ else
589
+ yield* this.processApproved(nests);
590
+ }
582
591
  else if (serder.pvrsn.major === 2 || firstSubmission) {
583
592
  // Legacy approval polling consumes its first notice; repeated v1 notices would re-propose it.
584
593
  this.notifier?.add(firstSubmission ? { r: route, d: said } : { r: route, d: said, e: embeddedSaid });
@@ -598,6 +607,71 @@ export class Multiplexor {
598
607
  sender,
599
608
  };
600
609
  }
610
+ /**
611
+ * Whether the proposed event is already signed by enough keys to be the group's next event or reply, so that a
612
+ * member that only receives it need not approve it. An interaction (or the anchor of a registry proposal) is judged
613
+ * against the group's current keys and threshold; a rotation against its own keys and threshold, when it names the
614
+ * members the group already has (the prior next-key commitments are checked by ordinary processing, and a
615
+ * membership change waits for approval); a reply against the group's current establishment.
616
+ */
617
+ alreadyComplete(serder, nests, route) {
618
+ let event, frame;
619
+ if (serder.pvrsn.major === 1) {
620
+ const label = route === MULTISIG_IXN_ROUTE
621
+ ? "ixn"
622
+ : route === MULTISIG_ROT_ROUTE
623
+ ? "rot"
624
+ : route === MULTISIG_RPY_ROUTE
625
+ ? "rpy"
626
+ : null;
627
+ if (!label)
628
+ return false;
629
+ const child = multisigChild(this.hby, serder, label);
630
+ const attrs = isRecord(serder.ked?.a) ? serder.ked.a : {};
631
+ if (!child?.attachment.length || attrs.gid !== child.event.pre)
632
+ return false;
633
+ if (route === MULTISIG_ROT_ROUTE && !this.namesCurrentMembers(attrs))
634
+ return false;
635
+ frame = parseBytes(concatBytes(Counter.makeGVC(child.genus), child.event.raw, child.attachment))[0];
636
+ event = child.event;
637
+ }
638
+ else if (isV2RegistryRoute(route)) {
639
+ const anchor = acdcV2RegistryProposal(serder, nests, this.registry?.store ?? null)?.anchor;
640
+ frame = nests.find((nest) => nest.body.said === anchor?.said);
641
+ event = anchor;
642
+ }
643
+ if (!event || !frame || !event.pre || !this.isLocalGroup(event.pre))
644
+ return false;
645
+ const kever = this.hby.db.getKever(event.pre);
646
+ if (!kever?.tholder)
647
+ return false;
648
+ const envelope = envelopeFromFrame(frame);
649
+ if (route === MULTISIG_RPY_ROUTE) {
650
+ const proof = envelope.tsgs.find((group) => group.pre === event.pre && group.sn === BigInt(`0x${kever.lastEst.s}`) && group.said === kever.lastEst.d);
651
+ return !!proof
652
+ && kever.tholder.satisfy(Kever.verifyIndexedSignatures(event.raw, proof.sigers, kever.verfers).indices);
653
+ }
654
+ if (event.sn !== kever.sn + 1 || event.prior !== kever.said)
655
+ return false;
656
+ if (event.ilk === "rot") {
657
+ return !!event.tholder
658
+ && event.tholder.satisfy(Kever.verifyIndexedSignatures(event.raw, envelope.sigers, event.verfers).indices);
659
+ }
660
+ return event.ilk === "ixn"
661
+ && kever.tholder.satisfy(Kever.verifyIndexedSignatures(event.raw, envelope.sigers, kever.verfers).indices);
662
+ }
663
+ /** Whether a rotation proposal names exactly the group's current signing members and rotation members. */
664
+ namesCurrentMembers(attrs) {
665
+ const group = typeof attrs.gid === "string" ? attrs.gid : "";
666
+ const same = (named, current) => {
667
+ const set = new Set(named);
668
+ return set.size === current.length && current.every((member) => set.has(member));
669
+ };
670
+ const signing = memberPrefixes(attrs.smids, []);
671
+ const rotation = attrs.rmids === undefined ? signing : memberPrefixes(attrs.rmids, []);
672
+ return same(signing, groupSigningMembers(this.hby, group))
673
+ && same(rotation, this.hby.ks.getRmids(group).map((tuple) => tuple[0].qb64));
674
+ }
601
675
  /** Return true when this wrapper can be accepted for local group work. */
602
676
  verify(serder, nests = []) {
603
677
  return this.inspect(serder, nests).ok;
@@ -765,6 +839,9 @@ export class Multiplexor {
765
839
  if (membership) {
766
840
  return { ok: false, said, reason: membership };
767
841
  }
842
+ const stray = this.strayReason(serder, request, embeddedSaid);
843
+ if (stray)
844
+ return { ok: false, said, reason: stray };
768
845
  return {
769
846
  ok: true,
770
847
  said,
@@ -788,6 +865,44 @@ export class Multiplexor {
788
865
  }
789
866
  return exns;
790
867
  }
868
+ /**
869
+ * Why a remote wrapper is not for this runtime's members, or null. It must be addressed to a member held here and,
870
+ * for a proposal about a group, come from one of that group's signing members: a wrapper for a member the receiver
871
+ * does not hold, or from an identifier that is not in the group, is not stored, noticed or listed. The one exception
872
+ * is a rotation's proposed new member, accepted only once a current member's stored wrapper for the same event names
873
+ * it ({@link proposedByMember}).
874
+ */
875
+ strayReason(serder, request, embeddedSaid) {
876
+ const sender = serder.pre ?? "";
877
+ if (this.isLocalHab(sender))
878
+ return null;
879
+ const recipient = serder.sad?.rp;
880
+ if (typeof recipient === "string" && recipient.length > 0 && !this.isLocalHab(recipient)) {
881
+ return `Multisig wrapper is addressed to ${recipient}, which is not a local identifier.`;
882
+ }
883
+ const proposed = request.kind === "rotate" && request.members.includes(sender)
884
+ && this.proposedByMember(embeddedSaid, sender);
885
+ if (request.kind !== "reject" && request.kind !== "join" && this.isLocalGroup(request.group)
886
+ && !proposed && !groupSigningMembers(this.hby, request.group).includes(sender)) {
887
+ return `Multisig wrapper sender ${sender} is not a signing member of group ${request.group}.`;
888
+ }
889
+ return null;
890
+ }
891
+ /**
892
+ * Whether a current member of the group (or a local identifier) already proposed or approved this exact rotation
893
+ * in a stored wrapper that names `sender` among its members. A rotation's proposed members answer it before they
894
+ * are signing members of the group, but a sender cannot certify itself: the wrapper it names itself in is not
895
+ * enough, a wrapper for the same event from a member the group already trusts must name it too.
896
+ */
897
+ proposedByMember(embeddedSaid, sender) {
898
+ return this.get(embeddedSaid).some(({ exn }) => {
899
+ const attrs = isRecord(exn.a) ? exn.a : {};
900
+ const from = typeof exn.i === "string" ? exn.i : "";
901
+ const gid = typeof attrs.gid === "string" ? attrs.gid : "";
902
+ const named = memberPrefixes(attrs.smids, attrs.rmids).includes(sender);
903
+ return named && (this.isLocalHab(from) || groupSigningMembers(this.hby, gid).includes(from));
904
+ });
905
+ }
791
906
  /** Apply local habitat membership to an already decoded request. */
792
907
  verifyMembership(request) {
793
908
  if (request.kind === "participate") {
@@ -802,6 +917,10 @@ export class Multiplexor {
802
917
  ? null
803
918
  : `Invalid request to join group; no local member in mids=${request.members.join(",")}.`;
804
919
  }
920
+ isLocalGroup(pre) {
921
+ const hab = this.hby.habs.get(pre);
922
+ return !!hab && isLocalGroupHab(this.hby, hab);
923
+ }
805
924
  isLocalHab(pre) {
806
925
  return pre.length > 0 && this.hby.habs.has(pre);
807
926
  }
@@ -142,23 +142,50 @@ export function* sendGroupMessage(postman, prepared) {
142
142
  recipient: prepared.recipient,
143
143
  exnSaid: prepared.exnSaid,
144
144
  payloadSaid: prepared.payloadSaid,
145
+ outcome: "delivered",
145
146
  deliveries: [...outcome.deliveries],
146
147
  };
147
148
  }
148
- /** Collect successful recipient outcomes into message identities and completed target URLs. */
149
+ /** The outcome of sending `prepared` as a failed delivery with the transport's reason. */
150
+ export function failedGroupDelivery(prepared, error) {
151
+ return {
152
+ recipient: prepared.recipient,
153
+ exnSaid: prepared.exnSaid,
154
+ payloadSaid: prepared.payloadSaid,
155
+ outcome: "failed",
156
+ deliveries: [],
157
+ reason: error instanceof Error ? error.message : String(error),
158
+ };
159
+ }
160
+ /**
161
+ * Like {@link sendGroupMessage}, but a transport failure is this member's outcome and not an error: the wrapper
162
+ * is already accepted locally, so one unreachable member must not stop the others or the caller's next step.
163
+ */
164
+ export function* deliverGroupMessage(postman, prepared) {
165
+ try {
166
+ return yield* sendGroupMessage(postman, prepared);
167
+ }
168
+ catch (error) {
169
+ return failedGroupDelivery(prepared, error);
170
+ }
171
+ }
172
+ /** Collect recipient outcomes: delivered message identities and completed target URLs, and every member's outcome. */
149
173
  export function groupSendResult(outcomes) {
174
+ const delivered = outcomes.filter(outcome => outcome.outcome === "delivered");
150
175
  return {
151
- messages: outcomes.map(({ recipient, exnSaid, payloadSaid }) => ({ recipient, exnSaid, payloadSaid })),
152
- deliveries: outcomes.flatMap(outcome => outcome.deliveries),
176
+ messages: delivered.map(({ recipient, exnSaid, payloadSaid }) => ({ recipient, exnSaid, payloadSaid })),
177
+ deliveries: delivered.flatMap(outcome => outcome.deliveries),
178
+ members: [...outcomes],
153
179
  };
154
180
  }
155
181
  /**
156
182
  * Process the exact signed local contribution before admitting remote delivery,
157
183
  * so an immediate peer reply cannot outrun local Multiplexor evidence. Await
158
- * normal intake, then send the same bytes without signing again. Intake or send
159
- * failures propagate; no cross-store/network transaction is implied.
184
+ * normal intake, then send the same bytes without signing again. Intake failures
185
+ * propagate; a send failure is the member's `failed` outcome. No cross-store/network
186
+ * transaction is implied.
160
187
  */
161
188
  export function* admitAndSendGroupMessage(input, postman, prepared) {
162
189
  yield* input.processBytes(prepared.message, { local: true });
163
- return yield* sendGroupMessage(postman, prepared);
190
+ return yield* deliverGroupMessage(postman, prepared);
164
191
  }
@@ -25,7 +25,7 @@ import { abortable } from "../runtime/abortable.js";
25
25
  import { collectBytes } from "../transport/byte-source.js";
26
26
  import { findLocalGroupMember as findLocalMember, groupMemberHasKey, uniqueMembers } from "./group-members.js";
27
27
  import { acdcV2RegistryProposal, MULTISIG_ICP_ROUTE, MULTISIG_ISS_ROUTE, MULTISIG_IXN_ROUTE, MULTISIG_REV_ROUTE, MULTISIG_ROT_ROUTE, MULTISIG_RPY_ROUTE, MULTISIG_VCP_ROUTE, multisigChild, multisigChildEvidence, multisigChildGenusAttrs, prepareAcdcV2RegistryExn, prepareMultisigEventExn, prepareMultisigVdrExn, registryProposalEvent, } from "./grouping.js";
28
- import { admitAndSendGroupMessage, groupSendResult, prepareGroupMessage, remoteGroupMembers, signGroupMessage, storedGroupMessage, } from "./multisig-messaging.js";
28
+ import { admitAndSendGroupMessage, failedGroupDelivery, groupSendResult, prepareGroupMessage, remoteGroupMembers, signGroupMessage, storedGroupMessage, } from "./multisig-messaging.js";
29
29
  /** Approve one exact ACDC v2 TEL/anchor through existing member signing and ordinary processors.
30
30
  * A returned wrapper records consent, not quorum/witness/TEL completion; retry the accepted-state owners.
31
31
  * `resume` reuses this member's recorded anchor signatures after a crash instead of signing again.
@@ -135,14 +135,10 @@ export function* publishAcdcV2Registry(hby, postman, group, approval) {
135
135
  notify: true,
136
136
  });
137
137
  const outcome = yield* postman.wait(evt);
138
- if (outcome.kin === "failed")
139
- throw outcome.error;
140
- outcomes.push({
141
- recipient,
142
- exnSaid: approval[0].said,
143
- payloadSaid: approval[0].sad.a.anc,
144
- deliveries: outcome.deliveries,
145
- });
138
+ const ref = { recipient, exnSaid: approval[0].said, payloadSaid: approval[0].sad.a.anc };
139
+ outcomes.push(outcome.kin === "failed"
140
+ ? failedGroupDelivery(ref, outcome.error)
141
+ : { ...ref, outcome: "delivered", deliveries: outcome.deliveries });
146
142
  }
147
143
  return groupSendResult(outcomes);
148
144
  }
@@ -93,7 +93,7 @@ function decode(deps, exn, route, kind) {
93
93
  smids,
94
94
  rmids: rmids ?? [],
95
95
  event: eventRef(approval.event, approval.group),
96
- inapplicable: eventAccepted(hby, approval.event) && !smids.some(mid => hby.habs.has(mid)),
96
+ inapplicable: eventAccepted(hby, approval.event) && (kind !== "icp" || !smids.some(mid => hby.habs.has(mid))),
97
97
  };
98
98
  }
99
99
  if (isV2RegistryRoute(route)) {
@@ -25,6 +25,7 @@ import { Respondant } from "../delivery/respondant.js";
25
25
  import { Exchanger, RouteRefuser } from "../exchanges/exchanging.js";
26
26
  import { loadChallengeHandlers } from "../identity/challenging.js";
27
27
  import { Anchorer, loadDelegationHandlers } from "../identity/delegating.js";
28
+ import { SignerNotHeldError } from "../identity/keeping.js";
28
29
  import { loadIpexHandlers } from "../ipex/ipexing.js";
29
30
  import { ForwardHandler } from "../mailbox/forward-handler.js";
30
31
  import { KEL_PUBLISHER_PACING, KelPublisher, kelPublisherDo } from "../mailbox/kel-publisher.js";
@@ -33,6 +34,7 @@ import { MAILBOX_POLLER_PACING, MailboxPoller, } from "../mailbox/mailbox-poller
33
34
  import { mailboxerOptionsForHabery } from "../mailbox/mailboxing.js";
34
35
  import { mailboxTopicForRoute } from "../mailbox/topic-routing.js";
35
36
  import { Counselor } from "../multisig/counseling.js";
37
+ import { groupEventForwarding } from "../multisig/event-forwarding.js";
36
38
  import { loadMultisigHandlers } from "../multisig/grouping.js";
37
39
  import { noterOptionsForHabery, Notifier } from "../notifications/notifying.js";
38
40
  import { Signaler } from "../notifications/signaling.js";
@@ -45,6 +47,7 @@ import { cueDo } from "./cue-runtime.js";
45
47
  import { doEvery, PROTOCOL_PACING } from "./doing.js";
46
48
  import { IpexAuxiliaries } from "./ipex-auxiliaries.js";
47
49
  import { composeOwners, operationOwnersDo, OWNER_PACING, } from "./operation-owners.js";
50
+ import { SignerConditions } from "./signer-conditions.js";
48
51
  import { signalerChangeSink } from "./workflow-contract.js";
49
52
  import { observeUntil } from "./workflow-contract.js";
50
53
  /**
@@ -203,7 +206,9 @@ export function* createAgentRuntime(hby, http, clock, options = {}) {
203
206
  changes: signaler ? signalerChangeSink(signaler) : undefined,
204
207
  retry: options.kelPublishRetry,
205
208
  });
209
+ const signerConditions = new SignerConditions();
206
210
  const mailboxPoller = new MailboxPoller(hby, mailboxDirector, http, clock, {
211
+ signerConditions,
207
212
  onPolled: (pre, mailbox) => kelPublisher.prompt(pre, mailbox),
208
213
  onUnauthorized: (pre, mailbox) => kelPublisher.repair(pre, mailbox),
209
214
  timeouts: options.mailboxPolling,
@@ -244,7 +249,7 @@ export function* createAgentRuntime(hby, http, clock, options = {}) {
244
249
  kelPublisher.rotated.push(cue);
245
250
  owned?.rootReplicator?.prompt(cue.pre);
246
251
  },
247
- }, options.counselorReceiptRetry);
252
+ }, options.counselorReceiptRetry, groupEventForwarding(hby, postman));
248
253
  const owned = acdc && ipex
249
254
  ? composeOwners({
250
255
  hby,
@@ -291,6 +296,7 @@ export function* createAgentRuntime(hby, http, clock, options = {}) {
291
296
  delegating,
292
297
  counselor,
293
298
  kelPublisher,
299
+ signerConditions,
294
300
  ownerPacing: options.ownerPacing ?? OWNER_PACING,
295
301
  publisherPacing: options.publisherPacing ?? KEL_PUBLISHER_PACING,
296
302
  protocolPacing: options.protocolPacing ?? PROTOCOL_PACING,
@@ -622,31 +628,24 @@ export function* runAgentRuntime(runtime, options = {}) {
622
628
  runtime.querying.configure({ hab: options.hab });
623
629
  const pacing = runtime.protocolPacing;
624
630
  const telReceiptor = runtime.vdr ? new Receiptor(runtime.hby, runtime.protocol, runtime.http, runtime.clock) : null;
631
+ // A loop that needs a signer the host released waits for it; it never ends the runtime (see `surviving`).
632
+ const alive = (loop, make) => spawn(() => surviving(runtime, loop, make));
633
+ const stopHoldWatch = runtime.hby.mgr.onHold(() => runtime.signerConditions.held());
625
634
  const tasks = [
626
- yield* spawn(() => runtime.querying.deliverDo()),
627
- yield* spawn(() => runtime.postman.deliverDo()),
635
+ yield* alive("querying", () => runtime.querying.deliverDo()),
636
+ yield* alive("postman", () => runtime.postman.deliverDo()),
628
637
  ...(runtime.vdr && telReceiptor
629
638
  ? [
630
- yield* spawn(() => doEvery(runtime.clock, pacing.tockMs, () => publishPendingTel(runtime.hby, runtime.vdr.publication, telReceiptor, runtime.http, runtime.clock))),
639
+ yield* alive("tel-publication", () => doEvery(runtime.clock, pacing.tockMs, () => publishPendingTel(runtime.hby, runtime.vdr.publication, telReceiptor, runtime.http, runtime.clock))),
631
640
  ]
632
641
  : []),
633
- yield* spawn(function* () {
634
- yield* cueDo(runtime, options, pacing);
635
- }),
636
- yield* spawn(function* () {
637
- yield* runtime.protocol.escrowDo(runtime.clock);
638
- }),
639
- yield* spawn(function* () {
640
- yield* runtime.oobiery.oobiDo(runtime.clock, pacing);
641
- }),
642
- yield* spawn(function* () {
643
- yield* runtime.authenticator.authDo(runtime.clock, pacing);
644
- }),
642
+ yield* alive("cues", () => cueDo(runtime, options, pacing)),
643
+ yield* alive("escrows", () => runtime.protocol.escrowDo(runtime.clock)),
644
+ yield* alive("oobi", () => runtime.oobiery.oobiDo(runtime.clock, pacing)),
645
+ yield* alive("authentication", () => runtime.authenticator.authDo(runtime.clock, pacing)),
645
646
  ...(runtime.signaler
646
647
  ? [
647
- yield* spawn(function* () {
648
- yield* runtime.signaler.signalDo(runtime.clock, pacing);
649
- }),
648
+ yield* alive("signals", () => runtime.signaler.signalDo(runtime.clock, pacing)),
650
649
  ]
651
650
  : []),
652
651
  ...(options.pollMailbox ?? true
@@ -656,12 +655,12 @@ export function* runAgentRuntime(runtime, options = {}) {
656
655
  }),
657
656
  ]
658
657
  : []),
659
- yield* spawn(() => doEvery(runtime.clock, pacing.tockMs, () => runtime.delegating.processAllOnce())),
660
- yield* spawn(() => doEvery(runtime.clock, pacing.tockMs, () => runtime.counselor.processOnce())),
661
- yield* spawn(() => kelPublisherDo(runtime.kelPublisher, runtime.clock, runtime.publisherPacing)),
658
+ yield* alive("delegation", () => doEvery(runtime.clock, pacing.tockMs, () => runtime.delegating.processAllOnce())),
659
+ yield* alive("counselor", () => doEvery(runtime.clock, pacing.tockMs, () => runtime.counselor.processOnce())),
660
+ yield* alive("kel-publisher", () => kelPublisherDo(runtime.kelPublisher, runtime.clock, runtime.publisherPacing)),
662
661
  ...(runtime.operations && runtime.ipex
663
662
  ? [
664
- yield* spawn(() => operationOwnersDo({
663
+ yield* alive("operation-owners", () => operationOwnersDo({
665
664
  operations: runtime.operations,
666
665
  rootReplicator: runtime.rootReplicator,
667
666
  ipex: runtime.ipex,
@@ -674,8 +673,29 @@ export function* runAgentRuntime(runtime, options = {}) {
674
673
  return yield* suspend();
675
674
  }
676
675
  finally {
676
+ stopHoldWatch();
677
677
  for (const task of tasks.reverse()) {
678
678
  yield* task.halt();
679
679
  }
680
680
  }
681
681
  }
682
+ /** How long a loop waits for a held signer before trying again, in milliseconds. */
683
+ const SIGNER_RETRY_MS = 1_000;
684
+ /**
685
+ * Run a background loop so that a signer the host released does not end the runtime: the loop's
686
+ * `SignerNotHeldError` is recorded in `runtime.signerConditions`, the loop waits for a signer to be held (or a
687
+ * second) and starts again. Every other failure still ends it.
688
+ */
689
+ function* surviving(runtime, loop, make) {
690
+ while (true) {
691
+ try {
692
+ return yield* make();
693
+ }
694
+ catch (error) {
695
+ if (!(error instanceof SignerNotHeldError))
696
+ throw error;
697
+ runtime.signerConditions.report(`runtime:${loop}`, error.pub, runtime.clock.now());
698
+ yield* runtime.signerConditions.waitForHold(runtime.clock, SIGNER_RETRY_MS);
699
+ }
700
+ }
701
+ }
@@ -143,28 +143,58 @@ export class IpexAuxiliaries {
143
143
  this.unblinds.clear();
144
144
  }
145
145
  claims(serder) {
146
- return this.select(serder) !== null;
146
+ return this.select(serder).length > 0;
147
147
  }
148
+ /**
149
+ * Every local party to the transaction takes the query: a Habery that holds both sides of a Grant has the sender's
150
+ * owner (its own threshold query) and the recipient's (the query received), and each decides by its own role.
151
+ */
148
152
  processQuery(envelope) {
149
- return this.select(envelope.serder)?.processQuery(envelope) ?? { kind: "drop", reason: "missingProcessor" };
153
+ const owners = this.select(envelope.serder);
154
+ const decisions = owners.map((owner) => {
155
+ try {
156
+ return owner.processQuery(envelope);
157
+ }
158
+ catch (error) {
159
+ // With two parties, the one that is not the query's audience may refuse it; the other still decides.
160
+ if (owners.length > 1 && error instanceof ValidationError)
161
+ return { kind: "drop", reason: "missingProcessor" };
162
+ throw error;
163
+ }
164
+ });
165
+ return decisions.find((decision) => decision && decision.kind === "accept") ?? decisions[0]
166
+ ?? { kind: "drop", reason: "missingProcessor" };
150
167
  }
151
168
  reply(kind, args) {
152
- const owner = this.claimant(kind, args.serder.pre, args.serder.sad?.a?.x);
153
- if (!owner)
169
+ const owners = this.claimants(kind, args.serder.pre, args.serder.sad?.a?.x);
170
+ if (!owners.length)
154
171
  throw new ValidationError(`No resource is registered to handle route ${args.route}`);
155
- return owner.processReply(args);
172
+ let accepted, failure;
173
+ for (const owner of owners) {
174
+ try {
175
+ const decision = owner.processReply(args);
176
+ accepted ??= decision;
177
+ }
178
+ catch (error) {
179
+ failure ??= error;
180
+ }
181
+ }
182
+ if (accepted === undefined)
183
+ throw failure;
184
+ return accepted;
156
185
  }
157
186
  select(serder) {
158
187
  const kind = serder.route === Dhke.Route ? "dhke" : serder.route === Unblind.Route ? "unblind" : null;
159
- return kind && this.claimant(kind, serder.pre, serder.sad?.q?.x);
188
+ return kind ? this.claimants(kind, serder.pre, serder.sad?.q?.x) : [];
160
189
  }
161
190
  /**
162
- * The owner of the one local identifier party to the transaction, created now if needed; absent or
163
- * ambiguous means unowned. Parties come from live owners (approved operations) and from the store.
191
+ * The owners of the local identifiers party to the transaction, created now if needed; none means unowned.
192
+ * Parties come from live owners (approved operations) and from the store. Usually one; two when this Habery
193
+ * holds both the sender and the recipient.
164
194
  */
165
- claimant(kind, sender, xid) {
195
+ claimants(kind, sender, xid) {
166
196
  if (!sender || typeof xid !== "string")
167
- return null;
197
+ return [];
168
198
  const owners = kind === "dhke" ? this.dhkes : this.unblinds;
169
199
  const habs = this.exc.hby.habs;
170
200
  const parties = new Set();
@@ -186,22 +216,26 @@ export class IpexAuxiliaries {
186
216
  const group = habs.get(sender);
187
217
  if (group && isLocalGroupHab(this.exc.hby, group))
188
218
  parties.add(sender);
189
- if (parties.size !== 1)
190
- return null;
191
- const [pre] = parties;
192
- const hab = habs.get(pre);
193
- if (!hab) {
194
- this.release(pre);
195
- return null;
196
- }
197
- try {
198
- return kind === "dhke" ? this.dhke(hab) : this.unblind(hab);
219
+ // Two parties are one transaction only when the sender is itself local: its owner and the recipient's.
220
+ if (parties.size < 1 || (parties.size > 1 && !(parties.size === 2 && parties.has(sender)))) {
221
+ return [];
199
222
  }
200
- catch (error) {
201
- // A group without a local signing member cannot own the route.
202
- if (error instanceof ValidationError)
203
- return null;
204
- throw error;
223
+ const owned = [];
224
+ for (const pre of parties) {
225
+ const hab = habs.get(pre);
226
+ if (!hab) {
227
+ this.release(pre);
228
+ continue;
229
+ }
230
+ try {
231
+ owned.push(kind === "dhke" ? this.dhke(hab) : this.unblind(hab));
232
+ }
233
+ catch (error) {
234
+ // A group without a local signing member cannot own the route.
235
+ if (!(error instanceof ValidationError))
236
+ throw error;
237
+ }
205
238
  }
239
+ return owned;
206
240
  }
207
241
  }
@@ -89,6 +89,7 @@ export function composeOwners(parts, stores = {}) {
89
89
  unblinds: ipex,
90
90
  group: { mux, protocol },
91
91
  delivery,
92
+ reactor,
92
93
  clock,
93
94
  retry: parts.retry,
94
95
  changes,
@@ -0,0 +1,51 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
+ };
6
+ var _SignerConditions_conditions, _SignerConditions_waiters;
7
+ /**
8
+ * Background loops that sign with a local key meet a missing key when the host has released it
9
+ * (`Manager.release`), which a custodial host does on purpose. That is a condition to wait out, not a failure
10
+ * of the runtime: the loop reports it here, backs off and resumes when a signer is held again
11
+ * (`Manager.hold`), and an application reads the list to tell its user which identifier cannot act.
12
+ */
13
+ import { action } from "effection";
14
+ /** The runtime's record of loops waiting for a signer; process-local, rebuilt by the loops themselves. */
15
+ export class SignerConditions {
16
+ constructor() {
17
+ _SignerConditions_conditions.set(this, new Map());
18
+ _SignerConditions_waiters.set(this, new Set());
19
+ }
20
+ /** `source` cannot sign because `pub` is not held; keeps the first time it was reported. */
21
+ report(source, pub, now) {
22
+ const known = __classPrivateFieldGet(this, _SignerConditions_conditions, "f").get(source);
23
+ __classPrivateFieldGet(this, _SignerConditions_conditions, "f").set(source, known?.pub === pub ? known : { source, pub, since: now });
24
+ }
25
+ /** `source` no longer waits for a signer. */
26
+ clear(source) {
27
+ __classPrivateFieldGet(this, _SignerConditions_conditions, "f").delete(source);
28
+ }
29
+ /** The loops waiting for a signer now. */
30
+ list() {
31
+ return [...__classPrivateFieldGet(this, _SignerConditions_conditions, "f").values()];
32
+ }
33
+ /** A signer was held: forget every condition (a loop that still lacks its key reports again) and wake the waiters. */
34
+ held() {
35
+ __classPrivateFieldGet(this, _SignerConditions_conditions, "f").clear();
36
+ for (const wake of [...__classPrivateFieldGet(this, _SignerConditions_waiters, "f")])
37
+ wake();
38
+ }
39
+ /** Wait up to `ms` on the host clock, or until a signer is held, whichever comes first. */
40
+ *waitForHold(clock, ms) {
41
+ yield* action((resolve) => {
42
+ const timer = clock.setTimeout(resolve, ms);
43
+ __classPrivateFieldGet(this, _SignerConditions_waiters, "f").add(resolve);
44
+ return () => {
45
+ clock.clearTimeout(timer);
46
+ __classPrivateFieldGet(this, _SignerConditions_waiters, "f").delete(resolve);
47
+ };
48
+ });
49
+ }
50
+ }
51
+ _SignerConditions_conditions = new WeakMap(), _SignerConditions_waiters = new WeakMap();
@@ -34,6 +34,12 @@ export class CesrHttpRefusal extends ValidationError {
34
34
  writable: true,
35
35
  value: status
36
36
  });
37
+ Object.defineProperty(this, "name", {
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true,
41
+ value: "CesrHttpRefusal"
42
+ });
37
43
  }
38
44
  }
39
45
  /**
@@ -372,7 +372,10 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
372
372
  throw new ValidationError(`Multisig IPEX sender ${group ?? "<missing>"} is not a local group AID.`);
373
373
  }
374
374
  if (!ipexArgs.auto) {
375
- return { exn, group: { embedded, accepted: null, approved: null, lead: null, messages: [], deliveries: [] } };
375
+ return {
376
+ exn,
377
+ group: { embedded, accepted: null, approved: null, lead: null, messages: [], deliveries: [], members: [] },
378
+ };
376
379
  }
377
380
  if (embedded.pvrsn.major === 2) {
378
381
  // ACDC v2: the runtime's group IPEX owner approves, publishes, collects and lets the lead deliver.
@@ -393,6 +396,7 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
393
396
  lead: runtime.exc.lead(hby.habs.get(group), said),
394
397
  messages: outcome?.messages ?? [],
395
398
  deliveries: [...(outcome?.deliveries ?? []), ...(outcome?.sent ?? [])],
399
+ members: outcome?.members ?? [],
396
400
  },
397
401
  };
398
402
  }
@@ -416,6 +420,7 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
416
420
  credentialSaved,
417
421
  messages: [],
418
422
  deliveries: [],
423
+ members: [],
419
424
  },
420
425
  };
421
426
  }
@@ -437,6 +442,7 @@ export function* joinIpexProposal(hby, runtime, reger, ipexArgs) {
437
442
  credentialSaved,
438
443
  messages: approval.messages,
439
444
  deliveries: approval.deliveries,
445
+ members: approval.members,
440
446
  },
441
447
  };
442
448
  }