keri-ts 2.2.3 → 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +14 -14
  2. package/esm/keri/npm/src/acdc/registering.js +19 -0
  3. package/esm/keri/npm/src/acdc/salting.js +4 -0
  4. package/esm/keri/npm/src/app/bootstrap/authenticating.js +34 -12
  5. package/esm/keri/npm/src/app/bootstrap/oobiery.js +365 -32
  6. package/esm/keri/npm/src/app/credentials/credential-importing.js +33 -4
  7. package/esm/keri/npm/src/app/credentials/root-replicator.js +27 -15
  8. package/esm/keri/npm/src/app/credentials/schema-resolving.js +5 -1
  9. package/esm/keri/npm/src/app/credentials/tel-publishing.js +16 -7
  10. package/esm/keri/npm/src/app/delivery/forwarding.js +188 -47
  11. package/esm/keri/npm/src/app/delivery/relaying.js +232 -0
  12. package/esm/keri/npm/src/app/delivery/respondant.js +8 -5
  13. package/esm/keri/npm/src/app/exchanges/exchange-messaging.js +1 -1
  14. package/esm/keri/npm/src/app/exchanges/exchanging.js +65 -9
  15. package/esm/keri/npm/src/app/identity/delegating.js +66 -9
  16. package/esm/keri/npm/src/app/identity/habbing.js +190 -71
  17. package/esm/keri/npm/src/app/identity/keeping.js +111 -39
  18. package/esm/keri/npm/src/app/identity/retire-identifier.js +98 -0
  19. package/esm/keri/npm/src/app/ipex/disclosure-replaying.js +116 -0
  20. package/esm/keri/npm/src/app/ipex/group-ipex.js +122 -25
  21. package/esm/keri/npm/src/app/ipex/group-issuance.js +109 -23
  22. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +36 -22
  23. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +53 -3
  24. package/esm/keri/npm/src/app/ipex/ipex-v2.js +7 -2
  25. package/esm/keri/npm/src/app/ipex/issuance.js +131 -35
  26. package/esm/keri/npm/src/app/ipex/pending-presentations.js +4 -2
  27. package/esm/keri/npm/src/app/ipex/presenting.js +3 -18
  28. package/esm/keri/npm/src/app/ipex/receive-issuance.js +2 -1
  29. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +16 -3
  30. package/esm/keri/npm/src/app/mailbox/agent-inbox.js +292 -0
  31. package/esm/keri/npm/src/app/mailbox/forward-handler.js +298 -52
  32. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +26 -31
  33. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +70 -62
  34. package/esm/keri/npm/src/app/mailbox/mailboxing.js +20 -4
  35. package/esm/keri/npm/src/app/multisig/counseling.js +75 -27
  36. package/esm/keri/npm/src/app/multisig/group-members.js +41 -5
  37. package/esm/keri/npm/src/app/multisig/grouping.js +50 -2
  38. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +3 -18
  39. package/esm/keri/npm/src/app/multisig/proposing.js +1 -16
  40. package/esm/keri/npm/src/app/multisig/rejoin-group.js +99 -0
  41. package/esm/keri/npm/src/app/multisig/replace-member.js +29 -8
  42. package/esm/keri/npm/src/app/protocol/parsering.js +45 -17
  43. package/esm/keri/npm/src/app/protocol/protocol-processing.js +62 -27
  44. package/esm/keri/npm/src/app/queries/query-replies.js +0 -22
  45. package/esm/keri/npm/src/app/queries/relay-refresh.js +81 -0
  46. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +190 -40
  47. package/esm/keri/npm/src/app/runtime/agent-runtime.js +46 -73
  48. package/esm/keri/npm/src/app/runtime/cue-runtime.js +45 -5
  49. package/esm/keri/npm/src/app/runtime/doing.js +25 -0
  50. package/esm/keri/npm/src/app/runtime/operation-owners.js +26 -56
  51. package/esm/keri/npm/src/app/runtime/workflow-contract.js +7 -4
  52. package/esm/keri/npm/src/app/transport/http-client.js +20 -2
  53. package/esm/keri/npm/src/app/transport/message-source.js +2 -1
  54. package/esm/keri/npm/src/app/transport/messengering.js +39 -8
  55. package/esm/keri/npm/src/app/witness/ingress.js +7 -0
  56. package/esm/keri/npm/src/app/witness/receipt-client.js +43 -16
  57. package/esm/keri/npm/src/app/witness/receipting.js +62 -20
  58. package/esm/keri/npm/src/cli/messaging/multisig.js +4 -3
  59. package/esm/keri/npm/src/core/errors.js +73 -0
  60. package/esm/keri/npm/src/core/kel/delegation-ancestry.js +11 -2
  61. package/esm/keri/npm/src/core/kel/eventing.js +25 -28
  62. package/esm/keri/npm/src/core/kel/kever.js +15 -9
  63. package/esm/keri/npm/src/core/mailbox/kram-policy.js +8 -0
  64. package/esm/keri/npm/src/core/mailbox/kram-pruning.js +25 -6
  65. package/esm/keri/npm/src/core/mailbox/kram-time.js +34 -6
  66. package/esm/keri/npm/src/core/mailbox/kraming.js +58 -2
  67. package/esm/keri/npm/src/core/routing.js +18 -25
  68. package/esm/keri/npm/src/core/scheming.js +12 -5
  69. package/esm/keri/npm/src/core/wire-fields.js +126 -0
  70. package/esm/keri/npm/src/db/basing.js +4 -13
  71. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +3 -2
  72. package/esm/keri/npm/src/db/core/lmdber.js +25 -6
  73. package/esm/keri/npm/src/db/core/memstore.js +4 -0
  74. package/esm/keri/npm/src/db/core/store-owner.js +4 -0
  75. package/esm/keri/npm/src/db/core/webdber.js +85 -27
  76. package/esm/keri/npm/src/db/mailbox/mailboxing.js +13 -0
  77. package/esm/keri/npm/src/db/subing.js +4 -0
  78. package/esm/keri/npm/src/runtime/index.js +11 -5
  79. package/esm/keri/npm/src/time/mod.js +5 -0
  80. package/esm/keri/npm/src/vdr/credential-tsn-record.js +2 -7
  81. package/esm/keri/npm/src/vdr/eventing.js +28 -19
  82. package/esm/keri/npm/src/vdr/registry-tsn-record.js +3 -13
  83. package/esm/keri/npm/src/version.js +2 -2
  84. package/package.json +1 -1
  85. package/types/_dnt.polyfills.d.ts +9 -9
  86. package/types/_dnt.polyfills.d.ts.map +1 -1
  87. package/types/acdc/registering.d.ts +6 -0
  88. package/types/acdc/registering.d.ts.map +1 -1
  89. package/types/acdc/salting.d.ts.map +1 -1
  90. package/types/app/bootstrap/authenticating.d.ts +12 -2
  91. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  92. package/types/app/bootstrap/oobiery.d.ts +144 -7
  93. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  94. package/types/app/credentials/credential-importing.d.ts +8 -0
  95. package/types/app/credentials/credential-importing.d.ts.map +1 -1
  96. package/types/app/credentials/root-replicator.d.ts +19 -8
  97. package/types/app/credentials/root-replicator.d.ts.map +1 -1
  98. package/types/app/credentials/schema-resolving.d.ts.map +1 -1
  99. package/types/app/credentials/tel-publishing.d.ts +4 -2
  100. package/types/app/credentials/tel-publishing.d.ts.map +1 -1
  101. package/types/app/delivery/forwarding.d.ts +75 -7
  102. package/types/app/delivery/forwarding.d.ts.map +1 -1
  103. package/types/app/delivery/relaying.d.ts +144 -0
  104. package/types/app/delivery/relaying.d.ts.map +1 -0
  105. package/types/app/delivery/respondant.d.ts +9 -2
  106. package/types/app/delivery/respondant.d.ts.map +1 -1
  107. package/types/app/exchanges/exchanging.d.ts +36 -0
  108. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  109. package/types/app/identity/delegating.d.ts +6 -0
  110. package/types/app/identity/delegating.d.ts.map +1 -1
  111. package/types/app/identity/habbing.d.ts +106 -19
  112. package/types/app/identity/habbing.d.ts.map +1 -1
  113. package/types/app/identity/keeping.d.ts +54 -19
  114. package/types/app/identity/keeping.d.ts.map +1 -1
  115. package/types/app/identity/retire-identifier.d.ts +24 -0
  116. package/types/app/identity/retire-identifier.d.ts.map +1 -0
  117. package/types/app/ipex/disclosure-replaying.d.ts +61 -0
  118. package/types/app/ipex/disclosure-replaying.d.ts.map +1 -0
  119. package/types/app/ipex/group-ipex.d.ts +60 -14
  120. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  121. package/types/app/ipex/group-issuance.d.ts +48 -6
  122. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  123. package/types/app/ipex/ipex-discloser.d.ts +14 -2
  124. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  125. package/types/app/ipex/ipex-grouping.d.ts +23 -2
  126. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  127. package/types/app/ipex/ipex-v2.d.ts +5 -2
  128. package/types/app/ipex/ipex-v2.d.ts.map +1 -1
  129. package/types/app/ipex/issuance.d.ts +35 -11
  130. package/types/app/ipex/issuance.d.ts.map +1 -1
  131. package/types/app/ipex/pending-presentations.d.ts +4 -2
  132. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  133. package/types/app/ipex/presenting.d.ts.map +1 -1
  134. package/types/app/ipex/receive-issuance.d.ts +2 -1
  135. package/types/app/ipex/receive-issuance.d.ts.map +1 -1
  136. package/types/app/ipex/unblinding-workflow.d.ts +4 -1
  137. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  138. package/types/app/mailbox/agent-inbox.d.ts +126 -0
  139. package/types/app/mailbox/agent-inbox.d.ts.map +1 -0
  140. package/types/app/mailbox/forward-handler.d.ts +114 -14
  141. package/types/app/mailbox/forward-handler.d.ts.map +1 -1
  142. package/types/app/mailbox/kel-publisher.d.ts +12 -22
  143. package/types/app/mailbox/kel-publisher.d.ts.map +1 -1
  144. package/types/app/mailbox/mailbox-poller.d.ts +40 -24
  145. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  146. package/types/app/mailbox/mailboxing.d.ts +19 -5
  147. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  148. package/types/app/multisig/counseling.d.ts +34 -11
  149. package/types/app/multisig/counseling.d.ts.map +1 -1
  150. package/types/app/multisig/group-members.d.ts +17 -1
  151. package/types/app/multisig/group-members.d.ts.map +1 -1
  152. package/types/app/multisig/grouping.d.ts +17 -1
  153. package/types/app/multisig/grouping.d.ts.map +1 -1
  154. package/types/app/multisig/multisig-workflows.d.ts +0 -8
  155. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  156. package/types/app/multisig/proposing.d.ts +4 -19
  157. package/types/app/multisig/proposing.d.ts.map +1 -1
  158. package/types/app/multisig/rejoin-group.d.ts +119 -0
  159. package/types/app/multisig/rejoin-group.d.ts.map +1 -0
  160. package/types/app/multisig/replace-member.d.ts +15 -1
  161. package/types/app/multisig/replace-member.d.ts.map +1 -1
  162. package/types/app/protocol/parsering.d.ts +21 -2
  163. package/types/app/protocol/parsering.d.ts.map +1 -1
  164. package/types/app/protocol/protocol-processing.d.ts +31 -6
  165. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  166. package/types/app/queries/query-replies.d.ts +4 -8
  167. package/types/app/queries/query-replies.d.ts.map +1 -1
  168. package/types/app/queries/relay-refresh.d.ts +26 -0
  169. package/types/app/queries/relay-refresh.d.ts.map +1 -0
  170. package/types/app/queries/witness-inquisitor.d.ts +57 -6
  171. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  172. package/types/app/runtime/agent-runtime.d.ts +34 -3
  173. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  174. package/types/app/runtime/cue-runtime.d.ts +15 -3
  175. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  176. package/types/app/runtime/doing.d.ts +17 -0
  177. package/types/app/runtime/doing.d.ts.map +1 -1
  178. package/types/app/runtime/operation-owners.d.ts +16 -26
  179. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  180. package/types/app/runtime/workflow-contract.d.ts +21 -6
  181. package/types/app/runtime/workflow-contract.d.ts.map +1 -1
  182. package/types/app/transport/http-client.d.ts +9 -0
  183. package/types/app/transport/http-client.d.ts.map +1 -1
  184. package/types/app/transport/message-source.d.ts +6 -3
  185. package/types/app/transport/message-source.d.ts.map +1 -1
  186. package/types/app/transport/messengering.d.ts +10 -1
  187. package/types/app/transport/messengering.d.ts.map +1 -1
  188. package/types/app/witness/ingress.d.ts +1 -0
  189. package/types/app/witness/ingress.d.ts.map +1 -1
  190. package/types/app/witness/receipt-client.d.ts +19 -1
  191. package/types/app/witness/receipt-client.d.ts.map +1 -1
  192. package/types/app/witness/receipting.d.ts +24 -1
  193. package/types/app/witness/receipting.d.ts.map +1 -1
  194. package/types/cli/messaging/multisig.d.ts.map +1 -1
  195. package/types/core/cues.d.ts +53 -1
  196. package/types/core/cues.d.ts.map +1 -1
  197. package/types/core/errors.d.ts +32 -0
  198. package/types/core/errors.d.ts.map +1 -1
  199. package/types/core/kel/delegation-ancestry.d.ts.map +1 -1
  200. package/types/core/kel/eventing.d.ts.map +1 -1
  201. package/types/core/kel/kever-decisions.d.ts +3 -1
  202. package/types/core/kel/kever-decisions.d.ts.map +1 -1
  203. package/types/core/kel/kever.d.ts +7 -1
  204. package/types/core/kel/kever.d.ts.map +1 -1
  205. package/types/core/mailbox/kram-policy.d.ts +6 -0
  206. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  207. package/types/core/mailbox/kram-pruning.d.ts +5 -1
  208. package/types/core/mailbox/kram-pruning.d.ts.map +1 -1
  209. package/types/core/mailbox/kram-time.d.ts +26 -2
  210. package/types/core/mailbox/kram-time.d.ts.map +1 -1
  211. package/types/core/mailbox/kraming.d.ts +14 -1
  212. package/types/core/mailbox/kraming.d.ts.map +1 -1
  213. package/types/core/routing.d.ts +3 -7
  214. package/types/core/routing.d.ts.map +1 -1
  215. package/types/core/scheming.d.ts.map +1 -1
  216. package/types/core/wire-fields.d.ts +56 -0
  217. package/types/core/wire-fields.d.ts.map +1 -0
  218. package/types/db/basing.d.ts.map +1 -1
  219. package/types/db/core/dber.d.ts +5 -0
  220. package/types/db/core/dber.d.ts.map +1 -1
  221. package/types/db/core/indexeddb-persistence.d.ts +3 -2
  222. package/types/db/core/indexeddb-persistence.d.ts.map +1 -1
  223. package/types/db/core/lmdber.d.ts +6 -0
  224. package/types/db/core/lmdber.d.ts.map +1 -1
  225. package/types/db/core/memstore.d.ts +2 -0
  226. package/types/db/core/memstore.d.ts.map +1 -1
  227. package/types/db/core/store-owner.d.ts +2 -0
  228. package/types/db/core/store-owner.d.ts.map +1 -1
  229. package/types/db/core/webdber.d.ts +32 -6
  230. package/types/db/core/webdber.d.ts.map +1 -1
  231. package/types/db/exchange/exchange-records.d.ts +4 -0
  232. package/types/db/exchange/exchange-records.d.ts.map +1 -1
  233. package/types/db/mailbox/mailboxing.d.ts +10 -1
  234. package/types/db/mailbox/mailboxing.d.ts.map +1 -1
  235. package/types/db/subing.d.ts +2 -0
  236. package/types/db/subing.d.ts.map +1 -1
  237. package/types/runtime/index.d.ts +20 -14
  238. package/types/runtime/index.d.ts.map +1 -1
  239. package/types/time/mod.d.ts +2 -0
  240. package/types/time/mod.d.ts.map +1 -1
  241. package/types/vdr/credential-tsn-record.d.ts.map +1 -1
  242. package/types/vdr/eventing.d.ts.map +1 -1
  243. package/types/vdr/registry-tsn-record.d.ts.map +1 -1
  244. package/types/version.d.ts +2 -2
  245. package/esm/keri/npm/src/app/runtime/signer-conditions.js +0 -51
  246. package/types/app/runtime/signer-conditions.d.ts +0 -32
  247. package/types/app/runtime/signer-conditions.d.ts.map +0 -1
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
12
  var _RootReplicator_instances, _RootReplicator_deps, _RootReplicator_pending, _RootReplicator_inflight, _RootReplicator_seen, _RootReplicator_dirty, _RootReplicator_pruned, _RootReplicator_attempt, _RootReplicator_refreshDirty, _RootReplicator_derive;
13
- import { isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
13
+ import { groupSigningMembers, isLocalGroupHab, localGroupMember } from "../multisig/group-members.js";
14
14
  import { replicateRootCopy, RootResealBudgetMs } from "./root-replicating.js";
15
15
  /** Most copies one `processOnce` attempts, so a first pass over a large store cannot stall the runtime. */
16
16
  export const RootSendsPerPass = 4;
@@ -18,13 +18,22 @@ export const RootSendsPerPass = 4;
18
18
  export const RootRetryBaseMs = 1_000;
19
19
  export const RootRetryMaxMs = 300_000;
20
20
  const idOf = (group, registry, recipient) => [group, registry, recipient].join("/");
21
+ /**
22
+ * What a group's owed copies were last derived from: its latest establishment SAID and its stored signing members.
23
+ * The members change without a new establishment event when a member's KEL arrives after the event was accepted.
24
+ */
25
+ function derivedFrom(hby, hab) {
26
+ const established = hab.pre && isLocalGroupHab(hby, hab) ? hab.kever?.lastEst.d : null;
27
+ return established ? [established, ...groupSigningMembers(hby, hab.pre)].join(" ") : null;
28
+ }
21
29
  /**
22
30
  * Derives, sends and records registry root copies for a wallet's local groups.
23
31
  *
24
32
  * Holds no durable state of its own beyond `Baser.rrsn`; what is pending and the
25
33
  * backoff are process-local and rebuilt from state. A host calls `prompt` (or
26
34
  * `replicate`) for each Dhke cue and `processOnce` on its loop; both are safe to repeat,
27
- * and overlapping `processOnce` calls never send one copy twice.
35
+ * and overlapping `processOnce` calls never send one copy twice. Groups on the `changed`
36
+ * deck are prompted by the next pass.
28
37
  */
29
38
  export class RootReplicator {
30
39
  constructor(deps) {
@@ -58,8 +67,8 @@ export class RootReplicator {
58
67
  return yield* this.processDue({ timeoutMs });
59
68
  }
60
69
  /**
61
- * The fixed-tock pass: re-derive only the groups that were prompted, then send up to {@link RootSendsPerPass}
62
- * due copies. A rotation nobody prompted for waits for {@link RootReplicator.sweep}.
70
+ * The fixed-tock pass: re-derive only the groups that were prompted or are on the `changed` deck, then send up
71
+ * to {@link RootSendsPerPass} due copies. A rotation nobody prompted for waits for {@link RootReplicator.sweep}.
63
72
  */
64
73
  *processDue({ timeoutMs = RootResealBudgetMs } = {}) {
65
74
  const { clock } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), deadline = clock.now() + Math.max(timeoutMs, 0);
@@ -68,7 +77,8 @@ export class RootReplicator {
68
77
  for (const entry of [...__classPrivateFieldGet(this, _RootReplicator_pending, "f").values()]) {
69
78
  if (results.length >= RootSendsPerPass)
70
79
  break;
71
- if (entry.retryAt > clock.now())
80
+ // The group's local member signs the copy; one that cannot sign yet keeps it without spending an attempt.
81
+ if (entry.retryAt > clock.now() || !__classPrivateFieldGet(this, _RootReplicator_deps, "f").root.hby.habs.get(entry.group)?.canSign())
72
82
  continue;
73
83
  const result = yield* __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_attempt).call(this, entry.group, entry.registry, entry.recipient, deadline);
74
84
  if (!result)
@@ -106,7 +116,7 @@ export class RootReplicator {
106
116
  }
107
117
  /** Whether a copy is owed or a group is waiting to be re-derived; in memory, reads no store. */
108
118
  pending() {
109
- return __classPrivateFieldGet(this, _RootReplicator_pending, "f").size > 0 || __classPrivateFieldGet(this, _RootReplicator_dirty, "f").size > 0;
119
+ return __classPrivateFieldGet(this, _RootReplicator_pending, "f").size > 0 || __classPrivateFieldGet(this, _RootReplicator_dirty, "f").size > 0 || !(__classPrivateFieldGet(this, _RootReplicator_deps, "f").changed?.empty ?? true);
110
120
  }
111
121
  /** Pure read: what is owed and why it has not gone yet. */
112
122
  status() {
@@ -116,8 +126,8 @@ export class RootReplicator {
116
126
  }
117
127
  /**
118
128
  * The full check: drop markers of groups that are gone (once) and re-derive each local group that is new or
119
- * whose establishment event changed. One keyed read per local identifier, so a host runs it every
120
- * many tocks, not every tock.
129
+ * whose establishment event or stored members changed. One keyed read per local identifier, so a host runs it
130
+ * every many tocks, not every tock.
121
131
  */
122
132
  sweep() {
123
133
  const { root } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), hby = root.hby;
@@ -129,10 +139,10 @@ export class RootReplicator {
129
139
  }
130
140
  }
131
141
  for (const hab of hby.habs.values()) {
132
- const established = hab.pre && isLocalGroupHab(hby, hab) ? hab.kever?.lastEst.d : null;
133
- if (!established || (__classPrivateFieldGet(this, _RootReplicator_seen, "f").get(hab.pre) === established && !__classPrivateFieldGet(this, _RootReplicator_dirty, "f").has(hab.pre)))
142
+ const derived = derivedFrom(hby, hab);
143
+ if (!derived || (__classPrivateFieldGet(this, _RootReplicator_seen, "f").get(hab.pre) === derived && !__classPrivateFieldGet(this, _RootReplicator_dirty, "f").has(hab.pre)))
134
144
  continue;
135
- __classPrivateFieldGet(this, _RootReplicator_seen, "f").set(hab.pre, established);
145
+ __classPrivateFieldGet(this, _RootReplicator_seen, "f").set(hab.pre, derived);
136
146
  __classPrivateFieldGet(this, _RootReplicator_dirty, "f").delete(hab.pre);
137
147
  __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_derive).call(this, hab.pre);
138
148
  }
@@ -158,14 +168,16 @@ _RootReplicator_deps = new WeakMap(), _RootReplicator_pending = new WeakMap(), _
158
168
  __classPrivateFieldGet(this, _RootReplicator_inflight, "f").delete(id);
159
169
  }
160
170
  }, _RootReplicator_refreshDirty = function _RootReplicator_refreshDirty() {
161
- const hby = __classPrivateFieldGet(this, _RootReplicator_deps, "f").root.hby;
171
+ const { root, changed } = __classPrivateFieldGet(this, _RootReplicator_deps, "f"), hby = root.hby;
172
+ for (let group = changed?.pull(); group !== undefined; group = changed?.pull())
173
+ this.prompt(group);
162
174
  for (const group of [...__classPrivateFieldGet(this, _RootReplicator_dirty, "f")]) {
163
175
  const hab = hby.habs.get(group);
164
- const established = hab && isLocalGroupHab(hby, hab) ? hab.kever?.lastEst.d : null;
176
+ const derived = hab ? derivedFrom(hby, hab) : null;
165
177
  __classPrivateFieldGet(this, _RootReplicator_dirty, "f").delete(group);
166
- if (!established)
178
+ if (!derived)
167
179
  continue;
168
- __classPrivateFieldGet(this, _RootReplicator_seen, "f").set(group, established);
180
+ __classPrivateFieldGet(this, _RootReplicator_seen, "f").set(group, derived);
169
181
  __classPrivateFieldGet(this, _RootReplicator_instances, "m", _RootReplicator_derive).call(this, group);
170
182
  }
171
183
  }, _RootReplicator_derive = function _RootReplicator_derive(group) {
@@ -1,3 +1,4 @@
1
+ import { readUrl } from "../../core/wire-fields.js";
1
2
  /** Parse one schema reference into the supported resolver contract. */
2
3
  export function parseSchemaReference(reference) {
3
4
  if (reference.startsWith("sad:")) {
@@ -7,7 +8,10 @@ export function parseSchemaReference(reference) {
7
8
  return { kind: "did", url: reference };
8
9
  }
9
10
  if (reference.startsWith("http://") || reference.startsWith("https://")) {
10
- const parsed = new URL(reference);
11
+ // The reference is a remote credential's `s`; one that does not parse names no cached schema.
12
+ const parsed = readUrl(reference);
13
+ if (!parsed)
14
+ return { kind: "oobi", url: reference };
11
15
  const parts = parsed.pathname.split("/").filter((part) => part.length > 0);
12
16
  const oobiIndex = parts.lastIndexOf("oobi");
13
17
  const said = oobiIndex >= 0 && oobiIndex + 2 === parts.length ? parts[oobiIndex + 1] : undefined;
@@ -1,17 +1,20 @@
1
1
  import { ValidationError } from "../../core/errors.js";
2
- import { sendWitnessMessage } from "../witness/receipt-client.js";
2
+ import { deliverWitnessMessage } from "../witness/receipt-client.js";
3
3
  /**
4
4
  * Advance one captured set of protocol escrows. Receipt collection includes
5
5
  * fanout; a crash before its commit repeats that ordinary finite operation.
6
6
  * Dissemination replays accepted TEL evidence to the anchor's historical witness
7
- * cohort and commits ctel only after every local transmission finished. Failure
8
- * retains the corresponding escrow for explicit resume, without an outbox.
7
+ * cohort and commits ctel only after every local transmission finished. A witness
8
+ * that is unreachable, refuses or does not receipt ends only that target: its
9
+ * escrow is retained and the next pass retries it, without an outbox. Local faults
10
+ * (missing issuer Hab, storage) still throw.
9
11
  */
10
12
  export function* publishPendingTel(hby, publication, receiptor, http, clock) {
11
13
  publication.advance();
12
14
  for (const target of publication.pendingReceipts()) {
13
- yield* receiptor.receipt(target.issuer, { sn: target.anchorSn, allowTcp: true });
14
- publication.receipted(target);
15
+ const outcome = yield* receiptor.attempt(target.issuer, { sn: target.anchorSn, allowTcp: true });
16
+ if (outcome.kind === "receipted")
17
+ publication.receipted(target);
15
18
  }
16
19
  for (const target of publication.pendingPublications()) {
17
20
  const hab = hby.habs.get(target.issuer);
@@ -19,9 +22,15 @@ export function* publishPendingTel(hby, publication, receiptor, http, clock) {
19
22
  throw new ValidationError(`Missing local TEL issuer ${target.issuer}.`);
20
23
  const diger = publication.reger.tels.getOn(target.pre, target.sn);
21
24
  const message = publication.reger.cloneTvt(target.pre, diger);
25
+ let delivered = true;
22
26
  for (const witness of target.witnesses) {
23
- yield* sendWitnessMessage(hab, witness, message, http, clock, null);
27
+ const delivery = yield* deliverWitnessMessage(hab, witness, message, http, clock, null);
28
+ if (delivery.kind === "unreachable") {
29
+ delivered = false;
30
+ break;
31
+ }
24
32
  }
25
- publication.published(target);
33
+ if (delivered)
34
+ publication.published(target);
26
35
  }
27
36
  }
@@ -1,20 +1,23 @@
1
1
  /** Queued signed-message publication. Poster owns transport attempts and their concrete outcomes. */
2
- import { attachmentPayload, collectPayload, concatBytes, Diger, framePayload, MtrDex, parseBytes, Vrsn_2_0, } from "cesr-ts";
2
+ import { attachmentPayload, collectPayload, concatBytes, Diger, framePayload, MtrDex, parseBytes, ParserError, Vrsn_2_0, } from "cesr-ts";
3
3
  import { call, resource } from "effection";
4
4
  import { Deck } from "../../core/deck.js";
5
5
  import { ValidationError } from "../../core/errors.js";
6
6
  import { exchange } from "../../core/exchange/protocol-exchanging.js";
7
7
  import { Roles } from "../../core/roles.js";
8
- import { mailboxTopicKey, preferredUrl } from "../mailbox/mailboxing.js";
8
+ import { mailboxTopicKey } from "../mailbox/mailboxing.js";
9
9
  import { transportMailboxTopic } from "../mailbox/topic-routing.js";
10
10
  import { localGroupMember } from "../multisig/group-members.js";
11
11
  import { abortable } from "../runtime/abortable.js";
12
+ import { PROTOCOL_TOCK_MS } from "../runtime/doing.js";
12
13
  import { runtimeTurn } from "../runtime/runtime-turn.js";
14
+ import { waitOnClock } from "../runtime/workflow-contract.js";
13
15
  import { CesrHttpRefusal } from "../transport/cesr-egress.js";
14
- import { messengerFrom, streamMessengerFrom } from "../transport/messengering.js";
16
+ import { messengerFrom, messengerUrl, streamMessengerFrom } from "../transport/messengering.js";
15
17
  import { requireTcpDialer } from "../transport/tcp-dialer.js";
16
18
  import { TransportError } from "../transport/transport-error.js";
17
19
  import { introduce } from "./introducing.js";
20
+ import { relayEndpoint, relayRefusal } from "./relaying.js";
18
21
  /**
19
22
  * A v2 message refused for a peer this node must treat as v1.
20
23
  *
@@ -42,9 +45,47 @@ function knownV1Peer(hby, aid) {
42
45
  const kever = hby.db.getKever(aid);
43
46
  return kever !== null && kever !== undefined && kever.serder.pvrsn.major < 2;
44
47
  }
45
- /** Whether any frame of this complete signed wire needs a v2 reader. */
46
- function wireNeedsV2(message) {
47
- return parseBytes(message).some(frame => frame.body.pvrsn.major >= 2 || (frame.gvrsn?.major ?? 1) >= 2);
48
+ /**
49
+ * A queued message whose own bytes cannot be prepared for sending: they are not complete CESR, or a forwarded
50
+ * delivery's source is not exactly one complete message. It describes the event, never this node, so it fails that
51
+ * delivery and the Poster continues with the next.
52
+ */
53
+ export class UnsendableMessageError extends ValidationError {
54
+ constructor() {
55
+ super(...arguments);
56
+ Object.defineProperty(this, "name", {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: "UnsendableMessageError"
61
+ });
62
+ }
63
+ }
64
+ /**
65
+ * Decode a queued message once, before any target is prepared.
66
+ *
67
+ * `ParserError` is the decoder's refusal of the bytes. This is the Poster's only decode of a producer's bytes; the
68
+ * wrappers it later builds and re-parses are its own output, so a failure there stays a fault.
69
+ *
70
+ * @param evt Admitted event whose copied bytes are decoded.
71
+ * @returns The message's complete frames in wire order.
72
+ * @throws UnsendableMessageError when the bytes are not complete CESR.
73
+ */
74
+ function messageFrames(evt) {
75
+ try {
76
+ return parseBytes(evt.msg);
77
+ }
78
+ catch (error) {
79
+ if (!(error instanceof ParserError))
80
+ throw error;
81
+ throw new UnsendableMessageError(`Queued message for ${evt.dest} is not complete CESR: ${error.message}`, {
82
+ cause: error,
83
+ });
84
+ }
85
+ }
86
+ /** Whether any of these frames needs a v2 reader. */
87
+ function framesNeedV2(frames) {
88
+ return frames.some(frame => frame.body.pvrsn.major >= 2 || (frame.gvrsn?.major ?? 1) >= 2);
48
89
  }
49
90
  /**
50
91
  * Preserve resolved endpoint schemes and signed bytes for concrete transport construction.
@@ -64,6 +105,13 @@ function remoteCesrTarget(endpoint, message) {
64
105
  * storage). Only recipients without those roles use one witness. Transport
65
106
  * completion and opt-in outcomes do not prove remote protocol acceptance. Finite
66
107
  * stream admissions use the first present role through the same execution owner.
108
+ *
109
+ * With a relay (an edge runtime) there is no role traversal: every recipient other
110
+ * than the relay gets one `/fwd` delivery to the relay, which does the forwarding,
111
+ * and the relay itself gets the message directly. `sent` then means the relay
112
+ * accepted the wrapper, not that the final recipient received it. A forwarded
113
+ * delivery fails with `RelayUnauthorizedError`, sending nothing, until its wrapper
114
+ * signer (a group's local member) has authorized the relay as its mailbox.
67
115
  */
68
116
  export class Poster {
69
117
  /**
@@ -74,9 +122,10 @@ export class Poster {
74
122
  * @param protocol Borrowed same-database processor for TCP replies; null permits send-only work.
75
123
  * @param http Borrowed HTTP implementation selected by composition.
76
124
  * @param clock Borrowed clock for transport deadlines; times are milliseconds.
125
+ * @param relay The agent every message goes through; null selects ordinary direct role traversal.
77
126
  * @throws RangeError if protocol state belongs to a different database.
78
127
  */
79
- constructor(hby, mbx, protocol, http, clock) {
128
+ constructor(hby, mbx, protocol, http, clock, relay) {
80
129
  /** Borrowed habitat collection and backing accepted protocol state. */
81
130
  Object.defineProperty(this, "hby", {
82
131
  enumerable: true,
@@ -168,6 +217,13 @@ export class Poster {
168
217
  writable: true,
169
218
  value: void 0
170
219
  });
220
+ /** The agent every message goes through (an edge); null delivers directly (a forwarder). */
221
+ Object.defineProperty(this, "relay", {
222
+ enumerable: true,
223
+ configurable: true,
224
+ writable: true,
225
+ value: void 0
226
+ });
171
227
  if (protocol && protocol.kvy.db !== hby.db)
172
228
  throw new RangeError("Poster protocol must use the same database");
173
229
  this.hby = hby;
@@ -175,6 +231,7 @@ export class Poster {
175
231
  this.http = http;
176
232
  this.clock = clock;
177
233
  this.protocol = protocol;
234
+ this.relay = relay;
178
235
  }
179
236
  /**
180
237
  * Admit signed source bytes; callers must observe an outcome before reporting transmission.
@@ -283,11 +340,16 @@ export class Poster {
283
340
  }
284
341
  return found;
285
342
  }
286
- /** Run queued delivery independently of query response intake and producer lifetimes. */
287
- *deliverDo() {
343
+ /**
344
+ * Run queued delivery independently of query response intake and producer lifetimes, as a fixed-tock doer:
345
+ * a pass only when something is queued (an emptiness check, no store read), then one tock. A message queued
346
+ * between tocks waits at most `tockMs` before its delivery starts.
347
+ */
348
+ *deliverDo(tockMs = PROTOCOL_TOCK_MS) {
288
349
  while (!this.closed) {
289
- yield* this.process();
290
- yield* runtimeTurn();
350
+ if (!this.evts.empty)
351
+ yield* this.process();
352
+ yield* waitOnClock(this.clock, tockMs);
291
353
  }
292
354
  }
293
355
  /** Runnable and active work determine convergence; consumable observations do not. */
@@ -376,7 +438,11 @@ export class Poster {
376
438
  /**
377
439
  * Resolve and execute one event using sequential current endpoints.
378
440
  * Observed HTTP refusal and TransportError permit remaining targets to run.
379
- * Unexpected construction/parser/storage/cleanup errors propagate so process can
441
+ * The event's bytes are decoded once; bytes that are not complete CESR fail the
442
+ * event. `PeerVersionError` and `UnsendableMessageError` from preparation fail
443
+ * only that target: both are decided from the event's own bytes and the peer's
444
+ * accepted version before any store write or I/O for the target. Other
445
+ * construction/storage/cleanup errors are faults and propagate so process can
380
446
  * publish or report the failed outcome before exposing the error.
381
447
  *
382
448
  * @param evt Concrete source-message record; completion/failure lookup uses object identity, not message SAID.
@@ -388,6 +454,11 @@ export class Poster {
388
454
  const hab = this.hby.habs.get(evt.src);
389
455
  if (!hab)
390
456
  throw new Error(`Unknown local sender ${evt.src}`);
457
+ if (this.relay) {
458
+ const refusal = relayRefusal(this.hby, this.relay, this.relaySigner(hab, this.relay, evt.dest));
459
+ if (refusal)
460
+ return { kin: "failed", evt, deliveries, error: refusal };
461
+ }
391
462
  const routes = this.findBytesRoutes(hab, evt.dest, evt.topic, evt.kin);
392
463
  if (!routes.length) {
393
464
  return {
@@ -397,6 +468,15 @@ export class Poster {
397
468
  error: new ValidationError(`No end roles for ${evt.dest} to send raw CESR payload.`),
398
469
  };
399
470
  }
471
+ let frames;
472
+ try {
473
+ frames = messageFrames(evt);
474
+ }
475
+ catch (error) {
476
+ if (!(error instanceof UnsendableMessageError))
477
+ throw error;
478
+ return { kin: "failed", evt, deliveries, error };
479
+ }
400
480
  // A refused target does not prevent later targets from receiving this event.
401
481
  const failed = [];
402
482
  try {
@@ -412,14 +492,19 @@ export class Poster {
412
492
  eid: endpoint.eid,
413
493
  url: endpoint.url,
414
494
  urls: endpoint.urls,
415
- }, evt);
495
+ }, evt, frames);
416
496
  }
417
497
  catch (error) {
418
- if (!(error instanceof PeerVersionError))
498
+ // Facts about this event and its peer, never this node: that target fails and the rest still run.
499
+ if (!(error instanceof PeerVersionError || error instanceof UnsendableMessageError))
419
500
  throw error;
420
501
  failed.push(error);
421
502
  continue;
422
503
  }
504
+ if (target.kind === "unsignable") {
505
+ failed.push(new ValidationError(`Local signer ${target.signer} cannot sign now; its signer is not held.`));
506
+ continue;
507
+ }
423
508
  try {
424
509
  if (target.kind !== "stored") {
425
510
  const refusal = yield* this.sendPreparedTarget(target, evt, signal);
@@ -479,7 +564,10 @@ export class Poster {
479
564
  *
480
565
  * Controller and agent roles each retain all endpoints. An indirect role picks
481
566
  * one random provider, unless an explicitly composed store owns an eligible local
482
- * AID. Empty location maps retain role selection and become execution failures.
567
+ * AID. An agent role whose endpoint is a local AID with a composed store is that
568
+ * same local store target (given a topic), as the hosted agent keeps the recipient's
569
+ * mail for pickup. Empty location maps retain role selection and become execution failures.
570
+ * With a relay, the relay alone is the route (see {@link relayRoutes}).
483
571
  *
484
572
  * @param hab Local signing habitat borrowing accepted role/location state.
485
573
  * @param recipient Resolved final recipient AID.
@@ -488,6 +576,8 @@ export class Poster {
488
576
  * @returns Ordered role groups; no eligible role returns an empty array.
489
577
  */
490
578
  findBytesRoutes(hab, recipient, topic, kin = "msg") {
579
+ if (this.relay)
580
+ return this.relayRoutes(hab, this.relay, recipient, topic);
491
581
  const ends = hab.endsFor(recipient);
492
582
  const roles = [Roles.controller, Roles.agent, Roles.mailbox].filter((role) => Object.keys(ends[role] ?? {}).length);
493
583
  if (!roles.length && Object.keys(ends[Roles.witness] ?? {}).length)
@@ -496,10 +586,16 @@ export class Poster {
496
586
  roles.splice(1);
497
587
  return roles.map((role) => {
498
588
  const candidates = Object.entries(ends[role]);
589
+ // An agent hosted by this Habery keeps the recipient's mail itself: store it, never post to our own ingress.
590
+ const localAgent = role === Roles.agent && topic && this.mbx
591
+ && candidates.find(([eid]) => this.hby.db.prefixes.has(eid));
592
+ if (localAgent) {
593
+ return { kind: "forward", targets: [{ eid: localAgent[0], url: `local-mailbox:${localAgent[0]}`, urls: {} }] };
594
+ }
499
595
  if (role === Roles.controller || role === Roles.agent) {
500
596
  return {
501
597
  kind: "direct",
502
- targets: candidates.map(([eid, urls]) => ({ eid, url: preferredUrl(urls), urls })),
598
+ targets: candidates.map(([eid, urls]) => ({ eid, url: messengerUrl(urls), urls })),
503
599
  };
504
600
  }
505
601
  if (!topic)
@@ -511,14 +607,60 @@ export class Poster {
511
607
  const [eid, urls] = candidates[Math.floor(Math.random() * candidates.length)];
512
608
  return {
513
609
  kind: "forward",
514
- targets: [{ eid, url: preferredUrl(urls), urls }],
610
+ targets: [{ eid, url: messengerUrl(urls), urls }],
515
611
  };
516
612
  });
517
613
  }
614
+ /**
615
+ * Route through the relay: one `forward` route to the relay for every other recipient, never a direct fallback,
616
+ * and one `direct` route when the recipient is the relay. Recipient end roles are not read.
617
+ *
618
+ * @param hab Local sending habitat; its wrapper signer must have authorized the relay as mailbox.
619
+ * @param relay The configured relay agent.
620
+ * @param recipient Resolved final recipient AID.
621
+ * @param topic Recipient mailbox topic; the forwarded route requires a nonempty topic.
622
+ * @returns One route to the relay, or none while {@link relayRefusal} refuses the relay for this sender.
623
+ */
624
+ relayRoutes(hab, relay, recipient, topic) {
625
+ if (relayRefusal(this.hby, relay, this.relaySigner(hab, relay, recipient)))
626
+ return [];
627
+ const endpoint = relayEndpoint(this.hby, relay);
628
+ if (recipient === relay.aid)
629
+ return [{ kind: "direct", targets: [endpoint] }];
630
+ if (!topic)
631
+ throw new ValidationError(`Forwarded delivery requires an explicit topic for ${recipient}.`);
632
+ return [{ kind: "forward", targets: [endpoint] }];
633
+ }
634
+ /**
635
+ * The identifier whose mailbox authorization of the relay a delivery needs: the `/fwd` wrapper's signer (see
636
+ * {@link Poster.wrapperSigner}), the same `i` the agent reads to decide it serves the sender. For a group source
637
+ * that is the local member, not the group. A message addressed to the relay is not wrapped and needs none (null).
638
+ *
639
+ * @param hab Local sending habitat.
640
+ * @param relay The configured relay agent.
641
+ * @param recipient Resolved final recipient AID.
642
+ * @returns The wrapper signer's AID, or null when the recipient is the relay.
643
+ */
644
+ relaySigner(hab, relay, recipient) {
645
+ return recipient === relay.aid ? null : this.wrapperSigner(hab).pre;
646
+ }
647
+ /**
648
+ * The habitat that signs a `/fwd` wrapper for `hab`. A group cannot sign the wrapper alone: one member's signature
649
+ * does not meet a threshold above one, and the host would escrow the wrapper. The local member wraps; the payload
650
+ * keeps whatever signatures it carries.
651
+ *
652
+ * @param hab Local sending habitat, possibly a group.
653
+ * @returns `hab` itself, or the group's local member.
654
+ * @throws ValidationError when a group has no local member metadata.
655
+ */
656
+ wrapperSigner(hab) {
657
+ return this.hby.db.groups.has(hab.pre) ? localGroupMember(this.hby, hab.pre) : hab;
658
+ }
518
659
  /**
519
660
  * Whether `src` has any eligible role (controller, agent, mailbox or witness) from which to reach `dest`, as
520
661
  * delivery would find it now. A pure read of accepted endpoint state: it says nothing about whether the
521
- * endpoint answers. A proposer uses it to refuse before it creates anything it could not publish.
662
+ * endpoint answers. A proposer uses it to refuse before it creates anything it could not publish. With a relay,
663
+ * it is whether {@link relayRefusal} accepts the relay for this sender.
522
664
  */
523
665
  canReach(src, dest, topic = "multisig") {
524
666
  const hab = this.hby.habs.get(src);
@@ -531,17 +673,19 @@ export class Poster {
531
673
  * @param route Selected direct or forwarded endpoint group.
532
674
  * @param endpoint One selected endpoint AID and URL from the route.
533
675
  * @param evt Original signed source and concrete ordinary/stream operation.
676
+ * @param frames The event's bytes, decoded once by {@link messageFrames}.
534
677
  * @returns A prepared remote target, or stored after authorized local mailbox publication.
535
678
  * @throws PeerVersionError when the wire needs a v2 reader that this peer is known not to be.
679
+ * @throws UnsendableMessageError when a forwarded ordinary message is not exactly one message.
536
680
  */
537
- *prepareBytesTarget(hab, route, endpoint, evt) {
538
- if (wireNeedsV2(evt.msg) && knownV1Peer(this.hby, evt.dest)) {
681
+ *prepareBytesTarget(hab, route, endpoint, evt, frames) {
682
+ if (framesNeedV2(frames) && knownV1Peer(this.hby, evt.dest)) {
539
683
  throw new PeerVersionError(`Refusing a v2 message for ${evt.dest}, whose latest accepted event is v1.`);
540
684
  }
541
685
  if (route.kind !== "direct") {
542
- return yield* this.prepareForwardedTarget(hab, evt.dest, evt.topic, evt, endpoint);
686
+ return yield* this.prepareForwardedTarget(hab, evt.dest, evt.topic, evt, frames, endpoint);
543
687
  }
544
- if (wireNeedsV2(evt.msg) && knownV1Peer(this.hby, endpoint.eid)) {
688
+ if (framesNeedV2(frames) && knownV1Peer(this.hby, endpoint.eid)) {
545
689
  throw new PeerVersionError(`Refusing a v2 message for endpoint ${endpoint.eid}, whose latest accepted event is v1.`);
546
690
  }
547
691
  return remoteCesrTarget(endpoint, evt.msg);
@@ -553,46 +697,46 @@ export class Poster {
553
697
  * @param recipient Final recipient AID, distinct from the intermediary endpoint.
554
698
  * @param topic Recipient mailbox topic used by the forwarding envelope.
555
699
  * @param evt Original signed source and operation; outer envelopes may be rebuilt.
700
+ * @param frames The event's decoded messages: exactly one for an ordinary event, each forwarded alone in a stream.
556
701
  * @param endpoint Mailbox/witness endpoint selected for this attempt.
557
- * @returns Stored for a local endpoint, or signed forwarding wire with its introduction.
702
+ * @returns Stored for a local endpoint, unsignable when the wrapper signer cannot sign now, or signed forwarding
703
+ * wire with its introduction.
704
+ * @throws UnsendableMessageError when an ordinary event is not exactly one message.
558
705
  */
559
- *prepareForwardedTarget(hab, recipient, topic, evt, endpoint) {
706
+ *prepareForwardedTarget(hab, recipient, topic, evt, frames, endpoint) {
560
707
  if (this.mbx && this.hby.db.prefixes.has(endpoint.eid)) {
561
708
  const stored = this.mbx.storeMsg(mailboxTopicKey(recipient, topic), mailboxReplyPayload(hab, recipient, topic, evt.msg));
562
709
  if (!stored)
563
710
  throw new Error("Mailbox publication did not complete");
564
711
  return { kind: "stored" };
565
712
  }
566
- // A group cannot sign the wrapper alone: one member's signature does not meet a threshold above one, and the
567
- // host would escrow the wrapper. The local member wraps; the payload keeps whatever signatures it carries.
568
- const signer = this.hby.db.groups.has(hab.pre) ? localGroupMember(this.hby, hab.pre) : hab;
569
- const msgs = [];
570
- if (evt.kin === "stream") {
571
- // Prepare every frame before opening the one selected relay attempt.
572
- for (const frame of parseBytes(evt.msg)) {
573
- msgs.push(yield* abortable((signal) => collectPayload(framePayload(frame), signal)));
574
- }
575
- }
576
- else {
577
- msgs.push(evt.msg);
713
+ const signer = this.wrapperSigner(hab);
714
+ // Poster does not retry: an event whose wrapper cannot be signed now fails like any other undeliverable one.
715
+ if (!signer.canSign())
716
+ return { kind: "unsignable", signer: signer.pre };
717
+ // A stream forwards each of its messages alone; an ordinary event is forwarded whole, so it must be one message.
718
+ if (evt.kin !== "stream" && frames.length !== 1) {
719
+ throw new UnsendableMessageError(`Forwarding to ${recipient} requires one complete CESR message.`);
578
720
  }
579
721
  // The provider reads the envelope, so it must read v2 whenever the envelope or child is v2.
580
- if ((signer.kelVersion.major >= 2 || msgs.some(wireNeedsV2)) && knownV1Peer(this.hby, endpoint.eid)) {
722
+ if ((signer.kelVersion.major >= 2 || framesNeedV2(frames)) && knownV1Peer(this.hby, endpoint.eid)) {
581
723
  throw new PeerVersionError(`Refusing a v2 envelope for provider ${endpoint.eid}, whose latest accepted event is v1.`);
582
724
  }
725
+ // Prepare every frame before opening the one selected relay attempt.
583
726
  const forwarded = [];
584
- for (const message of msgs)
585
- forwarded.push(yield* buildForwardedDelivery(signer, { recipient, topic, message }));
727
+ for (const child of frames)
728
+ forwarded.push(yield* buildForwardedDelivery(signer, { recipient, topic, child }));
586
729
  const intro = introduce(signer, endpoint.eid);
587
730
  let message = concatBytes(intro, ...forwarded);
588
731
  if (evt.kin === "stream") {
589
- const frames = parseBytes(message);
590
- if (frames.some(frame => (frame.gvrsn?.major ?? 1) >= 2)) {
732
+ // This node's own wrappers around already-decoded children: a parse failure here is a fault.
733
+ const prepared = parseBytes(message);
734
+ if (prepared.some(frame => (frame.gvrsn?.major ?? 1) >= 2)) {
591
735
  // A v2 peer's default cannot interpret unqualified v1 introduction counters.
592
736
  // Declare each prepared frame's actual genus without changing signed bodies.
593
737
  // Entirely legacy streams retain their v1-peer-compatible unqualified wire.
594
738
  const qualified = [];
595
- for (const frame of frames) {
739
+ for (const frame of prepared) {
596
740
  qualified.push(yield* abortable(signal => collectPayload(framePayload(frame), signal)));
597
741
  }
598
742
  message = concatBytes(...qualified);
@@ -609,14 +753,11 @@ export class Poster {
609
753
  * material that should travel with the forwarded request.
610
754
  *
611
755
  * @param hab Local signing habitat; its state/store lifetime remains with the caller.
612
- * @param args Final recipient, mailbox topic and exactly one complete signed source message.
756
+ * @param args Final recipient, mailbox topic and one decoded signed source message.
613
757
  * @returns Signed /fwd wire; v2 binds one nested child body, legacy uses pathed embeds.
614
758
  */
615
759
  function* buildForwardedDelivery(hab, args) {
616
- const frames = parseBytes(args.message);
617
- if (frames.length !== 1)
618
- throw new ValidationError("Forwarding requires one complete CESR message.");
619
- const child = frames[0];
760
+ const child = args.child;
620
761
  // A v2 member's stream is v2 after its introduction, so even a v1 child (such as a
621
762
  // KERIpy-form `e`-embed group proposal) travels nested with its own genus selector,
622
763
  // like KERIpy `StreamPoster.createForward`. Only v1 members keep the legacy embed.