keri-ts 2.2.4 → 2.2.6

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 (256) hide show
  1. package/esm/keri/npm/src/_dnt.polyfills.js +14 -14
  2. package/esm/keri/npm/src/acdc/dhking.js +29 -2
  3. package/esm/keri/npm/src/acdc/observing.js +10 -3
  4. package/esm/keri/npm/src/acdc/query-resend.js +74 -0
  5. package/esm/keri/npm/src/acdc/regengine.js +39 -9
  6. package/esm/keri/npm/src/acdc/regeventing.js +5 -3
  7. package/esm/keri/npm/src/acdc/registering.js +19 -0
  8. package/esm/keri/npm/src/acdc/salting.js +4 -0
  9. package/esm/keri/npm/src/acdc/unblinding.js +38 -3
  10. package/esm/keri/npm/src/app/bootstrap/authenticating.js +32 -10
  11. package/esm/keri/npm/src/app/bootstrap/oobiery.js +363 -30
  12. package/esm/keri/npm/src/app/credentials/credential-importing.js +33 -4
  13. package/esm/keri/npm/src/app/credentials/root-replicator.js +27 -15
  14. package/esm/keri/npm/src/app/credentials/schema-resolving.js +5 -1
  15. package/esm/keri/npm/src/app/credentials/tel-publishing.js +16 -7
  16. package/esm/keri/npm/src/app/delivery/forwarding.js +205 -44
  17. package/esm/keri/npm/src/app/delivery/relaying.js +232 -0
  18. package/esm/keri/npm/src/app/delivery/respondant.js +8 -5
  19. package/esm/keri/npm/src/app/exchanges/exchange-messaging.js +1 -1
  20. package/esm/keri/npm/src/app/exchanges/exchanging.js +65 -9
  21. package/esm/keri/npm/src/app/identity/delegating.js +66 -9
  22. package/esm/keri/npm/src/app/identity/habbing.js +190 -71
  23. package/esm/keri/npm/src/app/identity/keeping.js +111 -39
  24. package/esm/keri/npm/src/app/identity/retire-identifier.js +98 -0
  25. package/esm/keri/npm/src/app/ipex/disclosure-replaying.js +116 -0
  26. package/esm/keri/npm/src/app/ipex/group-ipex.js +122 -25
  27. package/esm/keri/npm/src/app/ipex/group-issuance.js +137 -37
  28. package/esm/keri/npm/src/app/ipex/ipex-discloser.js +94 -31
  29. package/esm/keri/npm/src/app/ipex/ipex-grouping.js +53 -3
  30. package/esm/keri/npm/src/app/ipex/ipex-v2.js +7 -2
  31. package/esm/keri/npm/src/app/ipex/issuance.js +147 -44
  32. package/esm/keri/npm/src/app/ipex/pending-presentations.js +4 -2
  33. package/esm/keri/npm/src/app/ipex/presenting.js +3 -18
  34. package/esm/keri/npm/src/app/ipex/receive-issuance.js +2 -1
  35. package/esm/keri/npm/src/app/ipex/unblinding-workflow.js +39 -11
  36. package/esm/keri/npm/src/app/mailbox/agent-inbox.js +292 -0
  37. package/esm/keri/npm/src/app/mailbox/forward-handler.js +298 -52
  38. package/esm/keri/npm/src/app/mailbox/kel-publisher.js +26 -31
  39. package/esm/keri/npm/src/app/mailbox/mailbox-poller.js +70 -62
  40. package/esm/keri/npm/src/app/mailbox/mailboxing.js +20 -4
  41. package/esm/keri/npm/src/app/multisig/counseling.js +71 -27
  42. package/esm/keri/npm/src/app/multisig/group-members.js +41 -5
  43. package/esm/keri/npm/src/app/multisig/grouping.js +50 -2
  44. package/esm/keri/npm/src/app/multisig/multisig-workflows.js +3 -18
  45. package/esm/keri/npm/src/app/multisig/proposing.js +1 -16
  46. package/esm/keri/npm/src/app/multisig/rejoin-group.js +99 -0
  47. package/esm/keri/npm/src/app/multisig/replace-member.js +29 -8
  48. package/esm/keri/npm/src/app/protocol/parsering.js +45 -17
  49. package/esm/keri/npm/src/app/protocol/protocol-processing.js +47 -21
  50. package/esm/keri/npm/src/app/queries/query-replies.js +0 -22
  51. package/esm/keri/npm/src/app/queries/relay-refresh.js +81 -0
  52. package/esm/keri/npm/src/app/queries/witness-inquisitor.js +180 -34
  53. package/esm/keri/npm/src/app/runtime/agent-runtime.js +42 -73
  54. package/esm/keri/npm/src/app/runtime/cue-runtime.js +45 -5
  55. package/esm/keri/npm/src/app/runtime/ipex-auxiliaries.js +8 -1
  56. package/esm/keri/npm/src/app/runtime/operation-owners.js +26 -56
  57. package/esm/keri/npm/src/app/runtime/workflow-contract.js +7 -4
  58. package/esm/keri/npm/src/app/transport/http-client.js +20 -2
  59. package/esm/keri/npm/src/app/transport/message-source.js +2 -1
  60. package/esm/keri/npm/src/app/transport/messengering.js +39 -8
  61. package/esm/keri/npm/src/app/witness/ingress.js +7 -0
  62. package/esm/keri/npm/src/app/witness/receipt-client.js +43 -16
  63. package/esm/keri/npm/src/app/witness/receipting.js +62 -20
  64. package/esm/keri/npm/src/cli/messaging/multisig.js +4 -3
  65. package/esm/keri/npm/src/core/errors.js +73 -0
  66. package/esm/keri/npm/src/core/kel/delegation-ancestry.js +11 -2
  67. package/esm/keri/npm/src/core/kel/eventing.js +25 -28
  68. package/esm/keri/npm/src/core/kel/kever.js +15 -9
  69. package/esm/keri/npm/src/core/mailbox/kram-policy.js +16 -0
  70. package/esm/keri/npm/src/core/mailbox/kram-pruning.js +25 -6
  71. package/esm/keri/npm/src/core/mailbox/kram-time.js +34 -6
  72. package/esm/keri/npm/src/core/mailbox/kraming.js +58 -2
  73. package/esm/keri/npm/src/core/missing-anchor-query.js +20 -0
  74. package/esm/keri/npm/src/core/routing.js +18 -25
  75. package/esm/keri/npm/src/core/scheming.js +12 -5
  76. package/esm/keri/npm/src/core/wire-fields.js +126 -0
  77. package/esm/keri/npm/src/db/basing.js +4 -13
  78. package/esm/keri/npm/src/db/core/indexeddb-persistence.js +3 -2
  79. package/esm/keri/npm/src/db/core/webdber.js +85 -27
  80. package/esm/keri/npm/src/db/mailbox/mailboxing.js +13 -0
  81. package/esm/keri/npm/src/db/subing.js +4 -0
  82. package/esm/keri/npm/src/runtime/index.js +11 -5
  83. package/esm/keri/npm/src/time/mod.js +5 -0
  84. package/esm/keri/npm/src/vdr/credential-tsn-record.js +2 -7
  85. package/esm/keri/npm/src/vdr/eventing.js +71 -28
  86. package/esm/keri/npm/src/vdr/registry-tsn-record.js +3 -13
  87. package/esm/keri/npm/src/version.js +2 -2
  88. package/package.json +1 -1
  89. package/types/_dnt.polyfills.d.ts +9 -9
  90. package/types/_dnt.polyfills.d.ts.map +1 -1
  91. package/types/acdc/dhking.d.ts +8 -0
  92. package/types/acdc/dhking.d.ts.map +1 -1
  93. package/types/acdc/observing.d.ts +5 -1
  94. package/types/acdc/observing.d.ts.map +1 -1
  95. package/types/acdc/query-resend.d.ts +24 -0
  96. package/types/acdc/query-resend.d.ts.map +1 -0
  97. package/types/acdc/regengine.d.ts +19 -1
  98. package/types/acdc/regengine.d.ts.map +1 -1
  99. package/types/acdc/regeventing.d.ts.map +1 -1
  100. package/types/acdc/registering.d.ts +6 -0
  101. package/types/acdc/registering.d.ts.map +1 -1
  102. package/types/acdc/salting.d.ts.map +1 -1
  103. package/types/acdc/unblinding.d.ts +13 -1
  104. package/types/acdc/unblinding.d.ts.map +1 -1
  105. package/types/app/bootstrap/authenticating.d.ts +12 -2
  106. package/types/app/bootstrap/authenticating.d.ts.map +1 -1
  107. package/types/app/bootstrap/oobiery.d.ts +144 -7
  108. package/types/app/bootstrap/oobiery.d.ts.map +1 -1
  109. package/types/app/credentials/credential-importing.d.ts +8 -0
  110. package/types/app/credentials/credential-importing.d.ts.map +1 -1
  111. package/types/app/credentials/root-replicator.d.ts +19 -8
  112. package/types/app/credentials/root-replicator.d.ts.map +1 -1
  113. package/types/app/credentials/schema-resolving.d.ts.map +1 -1
  114. package/types/app/credentials/tel-publishing.d.ts +4 -2
  115. package/types/app/credentials/tel-publishing.d.ts.map +1 -1
  116. package/types/app/delivery/forwarding.d.ts +90 -5
  117. package/types/app/delivery/forwarding.d.ts.map +1 -1
  118. package/types/app/delivery/relaying.d.ts +144 -0
  119. package/types/app/delivery/relaying.d.ts.map +1 -0
  120. package/types/app/delivery/respondant.d.ts +9 -2
  121. package/types/app/delivery/respondant.d.ts.map +1 -1
  122. package/types/app/exchanges/exchanging.d.ts +36 -0
  123. package/types/app/exchanges/exchanging.d.ts.map +1 -1
  124. package/types/app/identity/delegating.d.ts +6 -0
  125. package/types/app/identity/delegating.d.ts.map +1 -1
  126. package/types/app/identity/habbing.d.ts +106 -19
  127. package/types/app/identity/habbing.d.ts.map +1 -1
  128. package/types/app/identity/keeping.d.ts +54 -19
  129. package/types/app/identity/keeping.d.ts.map +1 -1
  130. package/types/app/identity/retire-identifier.d.ts +24 -0
  131. package/types/app/identity/retire-identifier.d.ts.map +1 -0
  132. package/types/app/ipex/disclosure-replaying.d.ts +61 -0
  133. package/types/app/ipex/disclosure-replaying.d.ts.map +1 -0
  134. package/types/app/ipex/group-ipex.d.ts +60 -14
  135. package/types/app/ipex/group-ipex.d.ts.map +1 -1
  136. package/types/app/ipex/group-issuance.d.ts +54 -8
  137. package/types/app/ipex/group-issuance.d.ts.map +1 -1
  138. package/types/app/ipex/ipex-discloser.d.ts +39 -2
  139. package/types/app/ipex/ipex-discloser.d.ts.map +1 -1
  140. package/types/app/ipex/ipex-grouping.d.ts +23 -2
  141. package/types/app/ipex/ipex-grouping.d.ts.map +1 -1
  142. package/types/app/ipex/ipex-v2.d.ts +5 -2
  143. package/types/app/ipex/ipex-v2.d.ts.map +1 -1
  144. package/types/app/ipex/issuance.d.ts +46 -16
  145. package/types/app/ipex/issuance.d.ts.map +1 -1
  146. package/types/app/ipex/pending-presentations.d.ts +4 -2
  147. package/types/app/ipex/pending-presentations.d.ts.map +1 -1
  148. package/types/app/ipex/presenting.d.ts.map +1 -1
  149. package/types/app/ipex/receive-issuance.d.ts +2 -1
  150. package/types/app/ipex/receive-issuance.d.ts.map +1 -1
  151. package/types/app/ipex/unblinding-workflow.d.ts +13 -2
  152. package/types/app/ipex/unblinding-workflow.d.ts.map +1 -1
  153. package/types/app/mailbox/agent-inbox.d.ts +126 -0
  154. package/types/app/mailbox/agent-inbox.d.ts.map +1 -0
  155. package/types/app/mailbox/forward-handler.d.ts +114 -14
  156. package/types/app/mailbox/forward-handler.d.ts.map +1 -1
  157. package/types/app/mailbox/kel-publisher.d.ts +12 -22
  158. package/types/app/mailbox/kel-publisher.d.ts.map +1 -1
  159. package/types/app/mailbox/mailbox-poller.d.ts +40 -24
  160. package/types/app/mailbox/mailbox-poller.d.ts.map +1 -1
  161. package/types/app/mailbox/mailboxing.d.ts +19 -5
  162. package/types/app/mailbox/mailboxing.d.ts.map +1 -1
  163. package/types/app/multisig/counseling.d.ts +32 -11
  164. package/types/app/multisig/counseling.d.ts.map +1 -1
  165. package/types/app/multisig/group-members.d.ts +17 -1
  166. package/types/app/multisig/group-members.d.ts.map +1 -1
  167. package/types/app/multisig/grouping.d.ts +17 -1
  168. package/types/app/multisig/grouping.d.ts.map +1 -1
  169. package/types/app/multisig/multisig-workflows.d.ts +0 -8
  170. package/types/app/multisig/multisig-workflows.d.ts.map +1 -1
  171. package/types/app/multisig/proposing.d.ts +4 -19
  172. package/types/app/multisig/proposing.d.ts.map +1 -1
  173. package/types/app/multisig/rejoin-group.d.ts +119 -0
  174. package/types/app/multisig/rejoin-group.d.ts.map +1 -0
  175. package/types/app/multisig/replace-member.d.ts +15 -1
  176. package/types/app/multisig/replace-member.d.ts.map +1 -1
  177. package/types/app/protocol/parsering.d.ts +21 -2
  178. package/types/app/protocol/parsering.d.ts.map +1 -1
  179. package/types/app/protocol/protocol-processing.d.ts +25 -5
  180. package/types/app/protocol/protocol-processing.d.ts.map +1 -1
  181. package/types/app/queries/query-replies.d.ts +4 -8
  182. package/types/app/queries/query-replies.d.ts.map +1 -1
  183. package/types/app/queries/relay-refresh.d.ts +26 -0
  184. package/types/app/queries/relay-refresh.d.ts.map +1 -0
  185. package/types/app/queries/witness-inquisitor.d.ts +56 -5
  186. package/types/app/queries/witness-inquisitor.d.ts.map +1 -1
  187. package/types/app/runtime/agent-runtime.d.ts +34 -3
  188. package/types/app/runtime/agent-runtime.d.ts.map +1 -1
  189. package/types/app/runtime/cue-runtime.d.ts +15 -3
  190. package/types/app/runtime/cue-runtime.d.ts.map +1 -1
  191. package/types/app/runtime/ipex-auxiliaries.d.ts.map +1 -1
  192. package/types/app/runtime/operation-owners.d.ts +16 -26
  193. package/types/app/runtime/operation-owners.d.ts.map +1 -1
  194. package/types/app/runtime/workflow-contract.d.ts +21 -6
  195. package/types/app/runtime/workflow-contract.d.ts.map +1 -1
  196. package/types/app/transport/http-client.d.ts +9 -0
  197. package/types/app/transport/http-client.d.ts.map +1 -1
  198. package/types/app/transport/message-source.d.ts +6 -3
  199. package/types/app/transport/message-source.d.ts.map +1 -1
  200. package/types/app/transport/messengering.d.ts +10 -1
  201. package/types/app/transport/messengering.d.ts.map +1 -1
  202. package/types/app/witness/ingress.d.ts +1 -0
  203. package/types/app/witness/ingress.d.ts.map +1 -1
  204. package/types/app/witness/receipt-client.d.ts +19 -1
  205. package/types/app/witness/receipt-client.d.ts.map +1 -1
  206. package/types/app/witness/receipting.d.ts +24 -1
  207. package/types/app/witness/receipting.d.ts.map +1 -1
  208. package/types/cli/messaging/multisig.d.ts.map +1 -1
  209. package/types/core/cues.d.ts +53 -1
  210. package/types/core/cues.d.ts.map +1 -1
  211. package/types/core/errors.d.ts +32 -0
  212. package/types/core/errors.d.ts.map +1 -1
  213. package/types/core/kel/delegation-ancestry.d.ts.map +1 -1
  214. package/types/core/kel/eventing.d.ts.map +1 -1
  215. package/types/core/kel/kever-decisions.d.ts +3 -1
  216. package/types/core/kel/kever-decisions.d.ts.map +1 -1
  217. package/types/core/kel/kever.d.ts +7 -1
  218. package/types/core/kel/kever.d.ts.map +1 -1
  219. package/types/core/mailbox/kram-policy.d.ts +12 -0
  220. package/types/core/mailbox/kram-policy.d.ts.map +1 -1
  221. package/types/core/mailbox/kram-pruning.d.ts +5 -1
  222. package/types/core/mailbox/kram-pruning.d.ts.map +1 -1
  223. package/types/core/mailbox/kram-time.d.ts +26 -2
  224. package/types/core/mailbox/kram-time.d.ts.map +1 -1
  225. package/types/core/mailbox/kraming.d.ts +14 -1
  226. package/types/core/mailbox/kraming.d.ts.map +1 -1
  227. package/types/core/missing-anchor-query.d.ts +34 -0
  228. package/types/core/missing-anchor-query.d.ts.map +1 -0
  229. package/types/core/routing.d.ts +3 -7
  230. package/types/core/routing.d.ts.map +1 -1
  231. package/types/core/scheming.d.ts.map +1 -1
  232. package/types/core/wire-fields.d.ts +56 -0
  233. package/types/core/wire-fields.d.ts.map +1 -0
  234. package/types/db/basing.d.ts.map +1 -1
  235. package/types/db/core/indexeddb-persistence.d.ts +3 -2
  236. package/types/db/core/indexeddb-persistence.d.ts.map +1 -1
  237. package/types/db/core/webdber.d.ts +32 -6
  238. package/types/db/core/webdber.d.ts.map +1 -1
  239. package/types/db/exchange/exchange-records.d.ts +4 -0
  240. package/types/db/exchange/exchange-records.d.ts.map +1 -1
  241. package/types/db/mailbox/mailboxing.d.ts +10 -1
  242. package/types/db/mailbox/mailboxing.d.ts.map +1 -1
  243. package/types/db/subing.d.ts +2 -0
  244. package/types/db/subing.d.ts.map +1 -1
  245. package/types/runtime/index.d.ts +20 -14
  246. package/types/runtime/index.d.ts.map +1 -1
  247. package/types/time/mod.d.ts +2 -0
  248. package/types/time/mod.d.ts.map +1 -1
  249. package/types/vdr/credential-tsn-record.d.ts.map +1 -1
  250. package/types/vdr/eventing.d.ts +32 -1
  251. package/types/vdr/eventing.d.ts.map +1 -1
  252. package/types/vdr/registry-tsn-record.d.ts.map +1 -1
  253. package/types/version.d.ts +2 -2
  254. package/esm/keri/npm/src/app/runtime/signer-conditions.js +0 -51
  255. package/types/app/runtime/signer-conditions.d.ts +0 -32
  256. 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,11 +1,12 @@
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 { holdsHeads, kelHeads } from "../mailbox/kel-publishing.js";
9
+ import { mailboxTopicKey } from "../mailbox/mailboxing.js";
9
10
  import { transportMailboxTopic } from "../mailbox/topic-routing.js";
10
11
  import { localGroupMember } from "../multisig/group-members.js";
11
12
  import { abortable } from "../runtime/abortable.js";
@@ -13,10 +14,11 @@ import { PROTOCOL_TOCK_MS } from "../runtime/doing.js";
13
14
  import { runtimeTurn } from "../runtime/runtime-turn.js";
14
15
  import { waitOnClock } from "../runtime/workflow-contract.js";
15
16
  import { CesrHttpRefusal } from "../transport/cesr-egress.js";
16
- import { messengerFrom, streamMessengerFrom } from "../transport/messengering.js";
17
+ import { messengerFrom, messengerUrl, streamMessengerFrom } from "../transport/messengering.js";
17
18
  import { requireTcpDialer } from "../transport/tcp-dialer.js";
18
19
  import { TransportError } from "../transport/transport-error.js";
19
20
  import { introduce } from "./introducing.js";
21
+ import { relayEndpoint, relayRefusal } from "./relaying.js";
20
22
  /**
21
23
  * A v2 message refused for a peer this node must treat as v1.
22
24
  *
@@ -44,9 +46,47 @@ function knownV1Peer(hby, aid) {
44
46
  const kever = hby.db.getKever(aid);
45
47
  return kever !== null && kever !== undefined && kever.serder.pvrsn.major < 2;
46
48
  }
47
- /** Whether any frame of this complete signed wire needs a v2 reader. */
48
- function wireNeedsV2(message) {
49
- return parseBytes(message).some(frame => frame.body.pvrsn.major >= 2 || (frame.gvrsn?.major ?? 1) >= 2);
49
+ /**
50
+ * A queued message whose own bytes cannot be prepared for sending: they are not complete CESR, or a forwarded
51
+ * delivery's source is not exactly one complete message. It describes the event, never this node, so it fails that
52
+ * delivery and the Poster continues with the next.
53
+ */
54
+ export class UnsendableMessageError extends ValidationError {
55
+ constructor() {
56
+ super(...arguments);
57
+ Object.defineProperty(this, "name", {
58
+ enumerable: true,
59
+ configurable: true,
60
+ writable: true,
61
+ value: "UnsendableMessageError"
62
+ });
63
+ }
64
+ }
65
+ /**
66
+ * Decode a queued message once, before any target is prepared.
67
+ *
68
+ * `ParserError` is the decoder's refusal of the bytes. This is the Poster's only decode of a producer's bytes; the
69
+ * wrappers it later builds and re-parses are its own output, so a failure there stays a fault.
70
+ *
71
+ * @param evt Admitted event whose copied bytes are decoded.
72
+ * @returns The message's complete frames in wire order.
73
+ * @throws UnsendableMessageError when the bytes are not complete CESR.
74
+ */
75
+ function messageFrames(evt) {
76
+ try {
77
+ return parseBytes(evt.msg);
78
+ }
79
+ catch (error) {
80
+ if (!(error instanceof ParserError))
81
+ throw error;
82
+ throw new UnsendableMessageError(`Queued message for ${evt.dest} is not complete CESR: ${error.message}`, {
83
+ cause: error,
84
+ });
85
+ }
86
+ }
87
+ /** Whether any of these frames needs a v2 reader. */
88
+ function framesNeedV2(frames) {
89
+ return frames.some(frame => frame.body.pvrsn.major >= 2 || (frame.gvrsn?.major ?? 1) >= 2);
50
90
  }
51
91
  /**
52
92
  * Preserve resolved endpoint schemes and signed bytes for concrete transport construction.
@@ -66,6 +106,13 @@ function remoteCesrTarget(endpoint, message) {
66
106
  * storage). Only recipients without those roles use one witness. Transport
67
107
  * completion and opt-in outcomes do not prove remote protocol acceptance. Finite
68
108
  * stream admissions use the first present role through the same execution owner.
109
+ *
110
+ * With a relay (an edge runtime) there is no role traversal: every recipient other
111
+ * than the relay gets one `/fwd` delivery to the relay, which does the forwarding,
112
+ * and the relay itself gets the message directly. `sent` then means the relay
113
+ * accepted the wrapper, not that the final recipient received it. A forwarded
114
+ * delivery fails with `RelayUnauthorizedError`, sending nothing, until its wrapper
115
+ * signer (a group's local member) has authorized the relay as its mailbox.
69
116
  */
70
117
  export class Poster {
71
118
  /**
@@ -76,9 +123,10 @@ export class Poster {
76
123
  * @param protocol Borrowed same-database processor for TCP replies; null permits send-only work.
77
124
  * @param http Borrowed HTTP implementation selected by composition.
78
125
  * @param clock Borrowed clock for transport deadlines; times are milliseconds.
126
+ * @param relay The agent every message goes through; null selects ordinary direct role traversal.
79
127
  * @throws RangeError if protocol state belongs to a different database.
80
128
  */
81
- constructor(hby, mbx, protocol, http, clock) {
129
+ constructor(hby, mbx, protocol, http, clock, relay) {
82
130
  /** Borrowed habitat collection and backing accepted protocol state. */
83
131
  Object.defineProperty(this, "hby", {
84
132
  enumerable: true,
@@ -170,6 +218,13 @@ export class Poster {
170
218
  writable: true,
171
219
  value: void 0
172
220
  });
221
+ /** The agent every message goes through (an edge); null delivers directly (a forwarder). */
222
+ Object.defineProperty(this, "relay", {
223
+ enumerable: true,
224
+ configurable: true,
225
+ writable: true,
226
+ value: void 0
227
+ });
173
228
  if (protocol && protocol.kvy.db !== hby.db)
174
229
  throw new RangeError("Poster protocol must use the same database");
175
230
  this.hby = hby;
@@ -177,6 +232,7 @@ export class Poster {
177
232
  this.http = http;
178
233
  this.clock = clock;
179
234
  this.protocol = protocol;
235
+ this.relay = relay;
180
236
  }
181
237
  /**
182
238
  * Admit signed source bytes; callers must observe an outcome before reporting transmission.
@@ -383,7 +439,11 @@ export class Poster {
383
439
  /**
384
440
  * Resolve and execute one event using sequential current endpoints.
385
441
  * Observed HTTP refusal and TransportError permit remaining targets to run.
386
- * Unexpected construction/parser/storage/cleanup errors propagate so process can
442
+ * The event's bytes are decoded once; bytes that are not complete CESR fail the
443
+ * event. `PeerVersionError` and `UnsendableMessageError` from preparation fail
444
+ * only that target: both are decided from the event's own bytes and the peer's
445
+ * accepted version before any store write or I/O for the target. Other
446
+ * construction/storage/cleanup errors are faults and propagate so process can
387
447
  * publish or report the failed outcome before exposing the error.
388
448
  *
389
449
  * @param evt Concrete source-message record; completion/failure lookup uses object identity, not message SAID.
@@ -395,6 +455,11 @@ export class Poster {
395
455
  const hab = this.hby.habs.get(evt.src);
396
456
  if (!hab)
397
457
  throw new Error(`Unknown local sender ${evt.src}`);
458
+ if (this.relay) {
459
+ const refusal = relayRefusal(this.hby, this.relay, this.relaySigner(hab, this.relay, evt.dest));
460
+ if (refusal)
461
+ return { kin: "failed", evt, deliveries, error: refusal };
462
+ }
398
463
  const routes = this.findBytesRoutes(hab, evt.dest, evt.topic, evt.kin);
399
464
  if (!routes.length) {
400
465
  return {
@@ -404,6 +469,15 @@ export class Poster {
404
469
  error: new ValidationError(`No end roles for ${evt.dest} to send raw CESR payload.`),
405
470
  };
406
471
  }
472
+ let frames;
473
+ try {
474
+ frames = messageFrames(evt);
475
+ }
476
+ catch (error) {
477
+ if (!(error instanceof UnsendableMessageError))
478
+ throw error;
479
+ return { kin: "failed", evt, deliveries, error };
480
+ }
407
481
  // A refused target does not prevent later targets from receiving this event.
408
482
  const failed = [];
409
483
  try {
@@ -419,14 +493,19 @@ export class Poster {
419
493
  eid: endpoint.eid,
420
494
  url: endpoint.url,
421
495
  urls: endpoint.urls,
422
- }, evt);
496
+ }, evt, frames);
423
497
  }
424
498
  catch (error) {
425
- if (!(error instanceof PeerVersionError))
499
+ // Facts about this event and its peer, never this node: that target fails and the rest still run.
500
+ if (!(error instanceof PeerVersionError || error instanceof UnsendableMessageError))
426
501
  throw error;
427
502
  failed.push(error);
428
503
  continue;
429
504
  }
505
+ if (target.kind === "unsignable") {
506
+ failed.push(new ValidationError(`Local signer ${target.signer} cannot sign now; its signer is not held.`));
507
+ continue;
508
+ }
430
509
  try {
431
510
  if (target.kind !== "stored") {
432
511
  const refusal = yield* this.sendPreparedTarget(target, evt, signal);
@@ -486,7 +565,10 @@ export class Poster {
486
565
  *
487
566
  * Controller and agent roles each retain all endpoints. An indirect role picks
488
567
  * one random provider, unless an explicitly composed store owns an eligible local
489
- * AID. Empty location maps retain role selection and become execution failures.
568
+ * AID. An agent role whose endpoint is a local AID with a composed store is that
569
+ * same local store target (given a topic), as the hosted agent keeps the recipient's
570
+ * mail for pickup. Empty location maps retain role selection and become execution failures.
571
+ * With a relay, the relay alone is the route (see {@link relayRoutes}).
490
572
  *
491
573
  * @param hab Local signing habitat borrowing accepted role/location state.
492
574
  * @param recipient Resolved final recipient AID.
@@ -495,6 +577,8 @@ export class Poster {
495
577
  * @returns Ordered role groups; no eligible role returns an empty array.
496
578
  */
497
579
  findBytesRoutes(hab, recipient, topic, kin = "msg") {
580
+ if (this.relay)
581
+ return this.relayRoutes(hab, this.relay, recipient, topic);
498
582
  const ends = hab.endsFor(recipient);
499
583
  const roles = [Roles.controller, Roles.agent, Roles.mailbox].filter((role) => Object.keys(ends[role] ?? {}).length);
500
584
  if (!roles.length && Object.keys(ends[Roles.witness] ?? {}).length)
@@ -503,10 +587,16 @@ export class Poster {
503
587
  roles.splice(1);
504
588
  return roles.map((role) => {
505
589
  const candidates = Object.entries(ends[role]);
590
+ // An agent hosted by this Habery keeps the recipient's mail itself: store it, never post to our own ingress.
591
+ const localAgent = role === Roles.agent && topic && this.mbx
592
+ && candidates.find(([eid]) => this.hby.db.prefixes.has(eid));
593
+ if (localAgent) {
594
+ return { kind: "forward", targets: [{ eid: localAgent[0], url: `local-mailbox:${localAgent[0]}`, urls: {} }] };
595
+ }
506
596
  if (role === Roles.controller || role === Roles.agent) {
507
597
  return {
508
598
  kind: "direct",
509
- targets: candidates.map(([eid, urls]) => ({ eid, url: preferredUrl(urls), urls })),
599
+ targets: candidates.map(([eid, urls]) => ({ eid, url: messengerUrl(urls), urls })),
510
600
  };
511
601
  }
512
602
  if (!topic)
@@ -518,14 +608,60 @@ export class Poster {
518
608
  const [eid, urls] = candidates[Math.floor(Math.random() * candidates.length)];
519
609
  return {
520
610
  kind: "forward",
521
- targets: [{ eid, url: preferredUrl(urls), urls }],
611
+ targets: [{ eid, url: messengerUrl(urls), urls }],
522
612
  };
523
613
  });
524
614
  }
615
+ /**
616
+ * Route through the relay: one `forward` route to the relay for every other recipient, never a direct fallback,
617
+ * and one `direct` route when the recipient is the relay. Recipient end roles are not read.
618
+ *
619
+ * @param hab Local sending habitat; its wrapper signer must have authorized the relay as mailbox.
620
+ * @param relay The configured relay agent.
621
+ * @param recipient Resolved final recipient AID.
622
+ * @param topic Recipient mailbox topic; the forwarded route requires a nonempty topic.
623
+ * @returns One route to the relay, or none while {@link relayRefusal} refuses the relay for this sender.
624
+ */
625
+ relayRoutes(hab, relay, recipient, topic) {
626
+ if (relayRefusal(this.hby, relay, this.relaySigner(hab, relay, recipient)))
627
+ return [];
628
+ const endpoint = relayEndpoint(this.hby, relay);
629
+ if (recipient === relay.aid)
630
+ return [{ kind: "direct", targets: [endpoint] }];
631
+ if (!topic)
632
+ throw new ValidationError(`Forwarded delivery requires an explicit topic for ${recipient}.`);
633
+ return [{ kind: "forward", targets: [endpoint] }];
634
+ }
635
+ /**
636
+ * The identifier whose mailbox authorization of the relay a delivery needs: the `/fwd` wrapper's signer (see
637
+ * {@link Poster.wrapperSigner}), the same `i` the agent reads to decide it serves the sender. For a group source
638
+ * that is the local member, not the group. A message addressed to the relay is not wrapped and needs none (null).
639
+ *
640
+ * @param hab Local sending habitat.
641
+ * @param relay The configured relay agent.
642
+ * @param recipient Resolved final recipient AID.
643
+ * @returns The wrapper signer's AID, or null when the recipient is the relay.
644
+ */
645
+ relaySigner(hab, relay, recipient) {
646
+ return recipient === relay.aid ? null : this.wrapperSigner(hab).pre;
647
+ }
648
+ /**
649
+ * The habitat that signs a `/fwd` wrapper for `hab`. A group cannot sign the wrapper alone: one member's signature
650
+ * does not meet a threshold above one, and the host would escrow the wrapper. The local member wraps; the payload
651
+ * keeps whatever signatures it carries.
652
+ *
653
+ * @param hab Local sending habitat, possibly a group.
654
+ * @returns `hab` itself, or the group's local member.
655
+ * @throws ValidationError when a group has no local member metadata.
656
+ */
657
+ wrapperSigner(hab) {
658
+ return this.hby.db.groups.has(hab.pre) ? localGroupMember(this.hby, hab.pre) : hab;
659
+ }
525
660
  /**
526
661
  * Whether `src` has any eligible role (controller, agent, mailbox or witness) from which to reach `dest`, as
527
662
  * delivery would find it now. A pure read of accepted endpoint state: it says nothing about whether the
528
- * endpoint answers. A proposer uses it to refuse before it creates anything it could not publish.
663
+ * endpoint answers. A proposer uses it to refuse before it creates anything it could not publish. With a relay,
664
+ * it is whether {@link relayRefusal} accepts the relay for this sender.
529
665
  */
530
666
  canReach(src, dest, topic = "multisig") {
531
667
  const hab = this.hby.habs.get(src);
@@ -538,21 +674,49 @@ export class Poster {
538
674
  * @param route Selected direct or forwarded endpoint group.
539
675
  * @param endpoint One selected endpoint AID and URL from the route.
540
676
  * @param evt Original signed source and concrete ordinary/stream operation.
677
+ * @param frames The event's bytes, decoded once by {@link messageFrames}.
541
678
  * @returns A prepared remote target, or stored after authorized local mailbox publication.
542
679
  * @throws PeerVersionError when the wire needs a v2 reader that this peer is known not to be.
680
+ * @throws UnsendableMessageError when a forwarded ordinary message is not exactly one message.
543
681
  */
544
- *prepareBytesTarget(hab, route, endpoint, evt) {
545
- if (wireNeedsV2(evt.msg) && knownV1Peer(this.hby, evt.dest)) {
682
+ *prepareBytesTarget(hab, route, endpoint, evt, frames) {
683
+ if (framesNeedV2(frames) && knownV1Peer(this.hby, evt.dest)) {
546
684
  throw new PeerVersionError(`Refusing a v2 message for ${evt.dest}, whose latest accepted event is v1.`);
547
685
  }
548
686
  if (route.kind !== "direct") {
549
- return yield* this.prepareForwardedTarget(hab, evt.dest, evt.topic, evt, endpoint);
687
+ return yield* this.prepareForwardedTarget(hab, evt.dest, evt.topic, evt, frames, endpoint);
550
688
  }
551
- if (wireNeedsV2(evt.msg) && knownV1Peer(this.hby, endpoint.eid)) {
689
+ if (framesNeedV2(frames) && knownV1Peer(this.hby, endpoint.eid)) {
552
690
  throw new PeerVersionError(`Refusing a v2 message for endpoint ${endpoint.eid}, whose latest accepted event is v1.`);
553
691
  }
554
692
  return remoteCesrTarget(endpoint, evt.msg);
555
693
  }
694
+ /**
695
+ * Whether the configured relay is already known to hold what it needs to verify `signer`'s `/fwd` wrappers, so
696
+ * the whole-KEL introduction can be left out.
697
+ *
698
+ * The relay verifies a wrapper against the signer's current key state, which the signer's latest establishment
699
+ * event fixes; interaction events do not change it. The evidence is `Baser.kpub`, the record the KEL publisher
700
+ * keeps per (identifier, mailbox) of the establishment event whose KEL (delegation chain included, and only once a
701
+ * delegated establishment is anchored) the mailbox was last given. The relay is the signer's mailbox, so the
702
+ * publisher and the mailbox admin request keep that record. Any other endpoint, and any mismatch or missing
703
+ * record, keeps today's full introduction.
704
+ *
705
+ * Known limit: a 2xx is not acceptance, and a relay that lost its store answers a `/fwd` from a sender it no
706
+ * longer knows with the same 204 acknowledgement and drops it. Nothing in the `/fwd` path signals that today; the
707
+ * sender's next mailbox poll gets the relay's 403, which makes `KelPublisher` clear the record and repost the KEL.
708
+ * Wrappers sent in between are lost until then.
709
+ *
710
+ * @param signer Local habitat that signs the wrapper (a group's local member).
711
+ * @param eid Endpoint AID the prepared wire goes to.
712
+ * @returns True only when `eid` is the relay and the record holds `signer`'s current establishment heads.
713
+ */
714
+ relayHoldsKel(signer, eid) {
715
+ if (!this.relay || this.relay.aid !== eid || !signer.pre)
716
+ return false;
717
+ const heads = kelHeads(this.hby, signer.pre);
718
+ return !!heads && holdsHeads(this.hby.db.kpub.get([signer.pre, eid]), heads);
719
+ }
556
720
  /**
557
721
  * Prepare complete signed wire and its introduction before opening transport.
558
722
  *
@@ -560,46 +724,46 @@ export class Poster {
560
724
  * @param recipient Final recipient AID, distinct from the intermediary endpoint.
561
725
  * @param topic Recipient mailbox topic used by the forwarding envelope.
562
726
  * @param evt Original signed source and operation; outer envelopes may be rebuilt.
727
+ * @param frames The event's decoded messages: exactly one for an ordinary event, each forwarded alone in a stream.
563
728
  * @param endpoint Mailbox/witness endpoint selected for this attempt.
564
- * @returns Stored for a local endpoint, or signed forwarding wire with its introduction.
729
+ * @returns Stored for a local endpoint, unsignable when the wrapper signer cannot sign now, or signed forwarding
730
+ * wire with its introduction.
731
+ * @throws UnsendableMessageError when an ordinary event is not exactly one message.
565
732
  */
566
- *prepareForwardedTarget(hab, recipient, topic, evt, endpoint) {
733
+ *prepareForwardedTarget(hab, recipient, topic, evt, frames, endpoint) {
567
734
  if (this.mbx && this.hby.db.prefixes.has(endpoint.eid)) {
568
735
  const stored = this.mbx.storeMsg(mailboxTopicKey(recipient, topic), mailboxReplyPayload(hab, recipient, topic, evt.msg));
569
736
  if (!stored)
570
737
  throw new Error("Mailbox publication did not complete");
571
738
  return { kind: "stored" };
572
739
  }
573
- // A group cannot sign the wrapper alone: one member's signature does not meet a threshold above one, and the
574
- // host would escrow the wrapper. The local member wraps; the payload keeps whatever signatures it carries.
575
- const signer = this.hby.db.groups.has(hab.pre) ? localGroupMember(this.hby, hab.pre) : hab;
576
- const msgs = [];
577
- if (evt.kin === "stream") {
578
- // Prepare every frame before opening the one selected relay attempt.
579
- for (const frame of parseBytes(evt.msg)) {
580
- msgs.push(yield* abortable((signal) => collectPayload(framePayload(frame), signal)));
581
- }
582
- }
583
- else {
584
- msgs.push(evt.msg);
740
+ const signer = this.wrapperSigner(hab);
741
+ // Poster does not retry: an event whose wrapper cannot be signed now fails like any other undeliverable one.
742
+ if (!signer.canSign())
743
+ return { kind: "unsignable", signer: signer.pre };
744
+ // A stream forwards each of its messages alone; an ordinary event is forwarded whole, so it must be one message.
745
+ if (evt.kin !== "stream" && frames.length !== 1) {
746
+ throw new UnsendableMessageError(`Forwarding to ${recipient} requires one complete CESR message.`);
585
747
  }
586
748
  // The provider reads the envelope, so it must read v2 whenever the envelope or child is v2.
587
- if ((signer.kelVersion.major >= 2 || msgs.some(wireNeedsV2)) && knownV1Peer(this.hby, endpoint.eid)) {
749
+ if ((signer.kelVersion.major >= 2 || framesNeedV2(frames)) && knownV1Peer(this.hby, endpoint.eid)) {
588
750
  throw new PeerVersionError(`Refusing a v2 envelope for provider ${endpoint.eid}, whose latest accepted event is v1.`);
589
751
  }
752
+ // Prepare every frame before opening the one selected relay attempt.
590
753
  const forwarded = [];
591
- for (const message of msgs)
592
- forwarded.push(yield* buildForwardedDelivery(signer, { recipient, topic, message }));
593
- const intro = introduce(signer, endpoint.eid);
754
+ for (const child of frames)
755
+ forwarded.push(yield* buildForwardedDelivery(signer, { recipient, topic, child }));
756
+ const intro = this.relayHoldsKel(signer, endpoint.eid) ? new Uint8Array() : introduce(signer, endpoint.eid);
594
757
  let message = concatBytes(intro, ...forwarded);
595
758
  if (evt.kin === "stream") {
596
- const frames = parseBytes(message);
597
- if (frames.some(frame => (frame.gvrsn?.major ?? 1) >= 2)) {
759
+ // This node's own wrappers around already-decoded children: a parse failure here is a fault.
760
+ const prepared = parseBytes(message);
761
+ if (prepared.some(frame => (frame.gvrsn?.major ?? 1) >= 2)) {
598
762
  // A v2 peer's default cannot interpret unqualified v1 introduction counters.
599
763
  // Declare each prepared frame's actual genus without changing signed bodies.
600
764
  // Entirely legacy streams retain their v1-peer-compatible unqualified wire.
601
765
  const qualified = [];
602
- for (const frame of frames) {
766
+ for (const frame of prepared) {
603
767
  qualified.push(yield* abortable(signal => collectPayload(framePayload(frame), signal)));
604
768
  }
605
769
  message = concatBytes(...qualified);
@@ -616,14 +780,11 @@ export class Poster {
616
780
  * material that should travel with the forwarded request.
617
781
  *
618
782
  * @param hab Local signing habitat; its state/store lifetime remains with the caller.
619
- * @param args Final recipient, mailbox topic and exactly one complete signed source message.
783
+ * @param args Final recipient, mailbox topic and one decoded signed source message.
620
784
  * @returns Signed /fwd wire; v2 binds one nested child body, legacy uses pathed embeds.
621
785
  */
622
786
  function* buildForwardedDelivery(hab, args) {
623
- const frames = parseBytes(args.message);
624
- if (frames.length !== 1)
625
- throw new ValidationError("Forwarding requires one complete CESR message.");
626
- const child = frames[0];
787
+ const child = args.child;
627
788
  // A v2 member's stream is v2 after its introduction, so even a v1 child (such as a
628
789
  // KERIpy-form `e`-embed group proposal) travels nested with its own genus selector,
629
790
  // like KERIpy `StreamPoster.createForward`. Only v1 members keep the legacy embed.