@did-btcr2/aggregation 0.1.0

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 (233) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +19 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/browser.js +24017 -0
  5. package/dist/browser.mjs +24011 -0
  6. package/dist/cjs/index.js +5690 -0
  7. package/dist/cjs/package.json +1 -0
  8. package/dist/esm/beacon-strategy.js +76 -0
  9. package/dist/esm/beacon-strategy.js.map +1 -0
  10. package/dist/esm/cohort.js +316 -0
  11. package/dist/esm/cohort.js.map +1 -0
  12. package/dist/esm/conditions.js +102 -0
  13. package/dist/esm/conditions.js.map +1 -0
  14. package/dist/esm/errors.js +22 -0
  15. package/dist/esm/errors.js.map +1 -0
  16. package/dist/esm/fallback-spend.js +132 -0
  17. package/dist/esm/fallback-spend.js.map +1 -0
  18. package/dist/esm/index.js +21 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/logger.js +15 -0
  21. package/dist/esm/logger.js.map +1 -0
  22. package/dist/esm/messages/base.js +36 -0
  23. package/dist/esm/messages/base.js.map +1 -0
  24. package/dist/esm/messages/bodies.js +149 -0
  25. package/dist/esm/messages/bodies.js.map +1 -0
  26. package/dist/esm/messages/constants.js +37 -0
  27. package/dist/esm/messages/constants.js.map +1 -0
  28. package/dist/esm/messages/factories.js +139 -0
  29. package/dist/esm/messages/factories.js.map +1 -0
  30. package/dist/esm/messages/guards.js +40 -0
  31. package/dist/esm/messages/guards.js.map +1 -0
  32. package/dist/esm/messages/index.js +6 -0
  33. package/dist/esm/messages/index.js.map +1 -0
  34. package/dist/esm/participant.js +588 -0
  35. package/dist/esm/participant.js.map +1 -0
  36. package/dist/esm/phases.js +45 -0
  37. package/dist/esm/phases.js.map +1 -0
  38. package/dist/esm/recovery-policy.js +156 -0
  39. package/dist/esm/recovery-policy.js.map +1 -0
  40. package/dist/esm/recovery-spend.js +106 -0
  41. package/dist/esm/recovery-spend.js.map +1 -0
  42. package/dist/esm/runner/aggregation-runner.js +75 -0
  43. package/dist/esm/runner/aggregation-runner.js.map +1 -0
  44. package/dist/esm/runner/events.js +2 -0
  45. package/dist/esm/runner/events.js.map +1 -0
  46. package/dist/esm/runner/index.js +6 -0
  47. package/dist/esm/runner/index.js.map +1 -0
  48. package/dist/esm/runner/participant-runner.js +407 -0
  49. package/dist/esm/runner/participant-runner.js.map +1 -0
  50. package/dist/esm/runner/service-runner.js +701 -0
  51. package/dist/esm/runner/service-runner.js.map +1 -0
  52. package/dist/esm/runner/typed-emitter.js +80 -0
  53. package/dist/esm/runner/typed-emitter.js.map +1 -0
  54. package/dist/esm/service.js +774 -0
  55. package/dist/esm/service.js.map +1 -0
  56. package/dist/esm/signer.js +33 -0
  57. package/dist/esm/signer.js.map +1 -0
  58. package/dist/esm/signing-session.js +195 -0
  59. package/dist/esm/signing-session.js.map +1 -0
  60. package/dist/esm/transport/didcomm.js +41 -0
  61. package/dist/esm/transport/didcomm.js.map +1 -0
  62. package/dist/esm/transport/error.js +12 -0
  63. package/dist/esm/transport/error.js.map +1 -0
  64. package/dist/esm/transport/factory.js +29 -0
  65. package/dist/esm/transport/factory.js.map +1 -0
  66. package/dist/esm/transport/http/client.js +342 -0
  67. package/dist/esm/transport/http/client.js.map +1 -0
  68. package/dist/esm/transport/http/envelope.js +126 -0
  69. package/dist/esm/transport/http/envelope.js.map +1 -0
  70. package/dist/esm/transport/http/errors.js +11 -0
  71. package/dist/esm/transport/http/errors.js.map +1 -0
  72. package/dist/esm/transport/http/inbox-buffer.js +45 -0
  73. package/dist/esm/transport/http/inbox-buffer.js.map +1 -0
  74. package/dist/esm/transport/http/index.js +12 -0
  75. package/dist/esm/transport/http/index.js.map +1 -0
  76. package/dist/esm/transport/http/nonce-cache.js +38 -0
  77. package/dist/esm/transport/http/nonce-cache.js.map +1 -0
  78. package/dist/esm/transport/http/protocol.js +28 -0
  79. package/dist/esm/transport/http/protocol.js.map +1 -0
  80. package/dist/esm/transport/http/rate-limiter.js +45 -0
  81. package/dist/esm/transport/http/rate-limiter.js.map +1 -0
  82. package/dist/esm/transport/http/request-auth.js +100 -0
  83. package/dist/esm/transport/http/request-auth.js.map +1 -0
  84. package/dist/esm/transport/http/server.js +475 -0
  85. package/dist/esm/transport/http/server.js.map +1 -0
  86. package/dist/esm/transport/http/sse-stream.js +110 -0
  87. package/dist/esm/transport/http/sse-stream.js.map +1 -0
  88. package/dist/esm/transport/http/sse-writer.js +25 -0
  89. package/dist/esm/transport/http/sse-writer.js.map +1 -0
  90. package/dist/esm/transport/in-memory.js +149 -0
  91. package/dist/esm/transport/in-memory.js.map +1 -0
  92. package/dist/esm/transport/index.js +8 -0
  93. package/dist/esm/transport/index.js.map +1 -0
  94. package/dist/esm/transport/nostr.js +491 -0
  95. package/dist/esm/transport/nostr.js.map +1 -0
  96. package/dist/esm/transport/transport.js +2 -0
  97. package/dist/esm/transport/transport.js.map +1 -0
  98. package/dist/types/beacon-strategy.d.ts +56 -0
  99. package/dist/types/beacon-strategy.d.ts.map +1 -0
  100. package/dist/types/cohort.d.ts +188 -0
  101. package/dist/types/cohort.d.ts.map +1 -0
  102. package/dist/types/conditions.d.ts +86 -0
  103. package/dist/types/conditions.d.ts.map +1 -0
  104. package/dist/types/errors.d.ts +14 -0
  105. package/dist/types/errors.d.ts.map +1 -0
  106. package/dist/types/fallback-spend.d.ts +94 -0
  107. package/dist/types/fallback-spend.d.ts.map +1 -0
  108. package/dist/types/index.d.ts +17 -0
  109. package/dist/types/index.d.ts.map +1 -0
  110. package/dist/types/logger.d.ts +22 -0
  111. package/dist/types/logger.d.ts.map +1 -0
  112. package/dist/types/messages/base.d.ts +58 -0
  113. package/dist/types/messages/base.d.ts.map +1 -0
  114. package/dist/types/messages/bodies.d.ts +177 -0
  115. package/dist/types/messages/bodies.d.ts.map +1 -0
  116. package/dist/types/messages/constants.d.ts +31 -0
  117. package/dist/types/messages/constants.d.ts.map +1 -0
  118. package/dist/types/messages/factories.d.ts +217 -0
  119. package/dist/types/messages/factories.d.ts.map +1 -0
  120. package/dist/types/messages/guards.d.ts +11 -0
  121. package/dist/types/messages/guards.d.ts.map +1 -0
  122. package/dist/types/messages/index.d.ts +6 -0
  123. package/dist/types/messages/index.d.ts.map +1 -0
  124. package/dist/types/participant.d.ts +162 -0
  125. package/dist/types/participant.d.ts.map +1 -0
  126. package/dist/types/phases.d.ts +55 -0
  127. package/dist/types/phases.d.ts.map +1 -0
  128. package/dist/types/recovery-policy.d.ts +132 -0
  129. package/dist/types/recovery-policy.d.ts.map +1 -0
  130. package/dist/types/recovery-spend.d.ts +94 -0
  131. package/dist/types/recovery-spend.d.ts.map +1 -0
  132. package/dist/types/runner/aggregation-runner.d.ts +72 -0
  133. package/dist/types/runner/aggregation-runner.d.ts.map +1 -0
  134. package/dist/types/runner/events.d.ts +147 -0
  135. package/dist/types/runner/events.d.ts.map +1 -0
  136. package/dist/types/runner/index.d.ts +6 -0
  137. package/dist/types/runner/index.d.ts.map +1 -0
  138. package/dist/types/runner/participant-runner.d.ts +122 -0
  139. package/dist/types/runner/participant-runner.d.ts.map +1 -0
  140. package/dist/types/runner/service-runner.d.ts +228 -0
  141. package/dist/types/runner/service-runner.d.ts.map +1 -0
  142. package/dist/types/runner/typed-emitter.d.ts +41 -0
  143. package/dist/types/runner/typed-emitter.d.ts.map +1 -0
  144. package/dist/types/service.d.ts +177 -0
  145. package/dist/types/service.d.ts.map +1 -0
  146. package/dist/types/signer.d.ts +50 -0
  147. package/dist/types/signer.d.ts.map +1 -0
  148. package/dist/types/signing-session.d.ts +81 -0
  149. package/dist/types/signing-session.d.ts.map +1 -0
  150. package/dist/types/transport/didcomm.d.ts +23 -0
  151. package/dist/types/transport/didcomm.d.ts.map +1 -0
  152. package/dist/types/transport/error.d.ts +8 -0
  153. package/dist/types/transport/error.d.ts.map +1 -0
  154. package/dist/types/transport/factory.d.ts +28 -0
  155. package/dist/types/transport/factory.d.ts.map +1 -0
  156. package/dist/types/transport/http/client.d.ts +56 -0
  157. package/dist/types/transport/http/client.d.ts.map +1 -0
  158. package/dist/types/transport/http/envelope.d.ts +64 -0
  159. package/dist/types/transport/http/envelope.d.ts.map +1 -0
  160. package/dist/types/transport/http/errors.d.ts +9 -0
  161. package/dist/types/transport/http/errors.d.ts.map +1 -0
  162. package/dist/types/transport/http/inbox-buffer.d.ts +32 -0
  163. package/dist/types/transport/http/inbox-buffer.d.ts.map +1 -0
  164. package/dist/types/transport/http/index.d.ts +12 -0
  165. package/dist/types/transport/http/index.d.ts.map +1 -0
  166. package/dist/types/transport/http/nonce-cache.d.ts +26 -0
  167. package/dist/types/transport/http/nonce-cache.d.ts.map +1 -0
  168. package/dist/types/transport/http/protocol.d.ts +53 -0
  169. package/dist/types/transport/http/protocol.d.ts.map +1 -0
  170. package/dist/types/transport/http/rate-limiter.d.ts +41 -0
  171. package/dist/types/transport/http/rate-limiter.d.ts.map +1 -0
  172. package/dist/types/transport/http/request-auth.d.ts +50 -0
  173. package/dist/types/transport/http/request-auth.d.ts.map +1 -0
  174. package/dist/types/transport/http/server.d.ts +118 -0
  175. package/dist/types/transport/http/server.d.ts.map +1 -0
  176. package/dist/types/transport/http/sse-stream.d.ts +34 -0
  177. package/dist/types/transport/http/sse-stream.d.ts.map +1 -0
  178. package/dist/types/transport/http/sse-writer.d.ts +12 -0
  179. package/dist/types/transport/http/sse-writer.d.ts.map +1 -0
  180. package/dist/types/transport/in-memory.d.ts +64 -0
  181. package/dist/types/transport/in-memory.d.ts.map +1 -0
  182. package/dist/types/transport/index.d.ts +8 -0
  183. package/dist/types/transport/index.d.ts.map +1 -0
  184. package/dist/types/transport/nostr.d.ts +153 -0
  185. package/dist/types/transport/nostr.d.ts.map +1 -0
  186. package/dist/types/transport/transport.d.ts +62 -0
  187. package/dist/types/transport/transport.d.ts.map +1 -0
  188. package/package.json +128 -0
  189. package/src/beacon-strategy.ts +142 -0
  190. package/src/cohort.ts +422 -0
  191. package/src/conditions.ts +172 -0
  192. package/src/errors.ts +25 -0
  193. package/src/fallback-spend.ts +223 -0
  194. package/src/index.ts +23 -0
  195. package/src/logger.ts +33 -0
  196. package/src/messages/base.ts +80 -0
  197. package/src/messages/bodies.ts +328 -0
  198. package/src/messages/constants.ts +40 -0
  199. package/src/messages/factories.ts +297 -0
  200. package/src/messages/guards.ts +61 -0
  201. package/src/messages/index.ts +5 -0
  202. package/src/participant.ts +779 -0
  203. package/src/phases.ts +91 -0
  204. package/src/recovery-policy.ts +237 -0
  205. package/src/recovery-spend.ts +192 -0
  206. package/src/runner/aggregation-runner.ts +125 -0
  207. package/src/runner/events.ts +129 -0
  208. package/src/runner/index.ts +5 -0
  209. package/src/runner/participant-runner.ts +488 -0
  210. package/src/runner/service-runner.ts +891 -0
  211. package/src/runner/typed-emitter.ts +87 -0
  212. package/src/service.ts +1000 -0
  213. package/src/signer.ts +67 -0
  214. package/src/signing-session.ts +290 -0
  215. package/src/transport/didcomm.ts +59 -0
  216. package/src/transport/error.ts +13 -0
  217. package/src/transport/factory.ts +65 -0
  218. package/src/transport/http/client.ts +409 -0
  219. package/src/transport/http/envelope.ts +204 -0
  220. package/src/transport/http/errors.ts +11 -0
  221. package/src/transport/http/inbox-buffer.ts +53 -0
  222. package/src/transport/http/index.ts +11 -0
  223. package/src/transport/http/nonce-cache.ts +43 -0
  224. package/src/transport/http/protocol.ts +57 -0
  225. package/src/transport/http/rate-limiter.ts +75 -0
  226. package/src/transport/http/request-auth.ts +164 -0
  227. package/src/transport/http/server.ts +617 -0
  228. package/src/transport/http/sse-stream.ts +121 -0
  229. package/src/transport/http/sse-writer.ts +23 -0
  230. package/src/transport/in-memory.ts +178 -0
  231. package/src/transport/index.ts +7 -0
  232. package/src/transport/nostr.ts +576 -0
  233. package/src/transport/transport.ts +79 -0
@@ -0,0 +1,129 @@
1
+ import type { SerializedSMTProof } from '@did-btcr2/smt';
2
+ import type { CohortAdvert, PendingFallbackRequest, PendingSigningRequest, PendingValidation } from '../participant.js';
3
+ import type { AggregationResult, PendingOptIn, Rejection } from '../service.js';
4
+
5
+ /**
6
+ * Sidecar data a participant keeps when a cohort completes from its
7
+ * perspective: the cohort's beacon coordinates plus the off-chain artifact it
8
+ * needs for future DID resolution (the CAS Announcement map for CAS beacons, or
9
+ * the SMT inclusion proof for SMT beacons). Emitted via `cohort-complete` and
10
+ * returned by the participant runner's join helpers.
11
+ */
12
+ export interface CohortCompleteInfo {
13
+ cohortId: string;
14
+ beaconAddress: string;
15
+ beaconType: string;
16
+ /** True if this participant submitted an update; false if it declined (non-inclusion). */
17
+ included: boolean;
18
+ /** DID to base64url update hash. Populated only for CAS beacons. */
19
+ casAnnouncement?: Record<string, string>;
20
+ /** Inclusion proof (submitter) or non-inclusion proof (decliner) for this slot. Populated only for SMT beacons. */
21
+ smtProof?: SerializedSMTProof;
22
+ }
23
+
24
+ /**
25
+ * AggregationServiceRunner events are emitted by the AggregationServiceRunner to signal important
26
+ * milestones and actions during the aggregation process. They can be listened to by external code
27
+ * to react to these events, such as logging, updating a UI, or triggering additional actions.
28
+ */
29
+ export type AggregationServiceEvents = {
30
+ /** A cohort has been created and the advert message broadcast. */
31
+ 'cohort-advertised': [{ cohortId: string }];
32
+
33
+ /** A participant has opted in. Fires before the accept decision callback. */
34
+ 'opt-in-received': [PendingOptIn];
35
+
36
+ /** A participant has been accepted into the cohort. */
37
+ 'participant-accepted': [{ cohortId: string; participantDid: string }];
38
+
39
+ /** Keygen has been finalized: beacon address is now available. */
40
+ 'keygen-complete': [{ cohortId: string; beaconAddress: string }];
41
+
42
+ /** A participant has submitted a signed update. */
43
+ 'update-received': [{ cohortId: string; participantDid: string }];
44
+
45
+ /**
46
+ * An inbound message was silently dropped by the state machine (bad proof,
47
+ * oversized payload, wrong wire version, etc.). Fires for *any* rejection,
48
+ * not just SUBMIT_UPDATE.
49
+ */
50
+ 'message-rejected': [Rejection & { cohortId: string }];
51
+
52
+ /** Aggregated data has been distributed to all participants for validation. */
53
+ 'data-distributed': [{ cohortId: string }];
54
+
55
+ /** A participant has acknowledged validation (approved or rejected). */
56
+ 'validation-received': [{ cohortId: string; participantDid: string; approved: boolean }];
57
+
58
+ /** Signing has started: auth requests sent to participants. */
59
+ 'signing-started': [{ cohortId: string; sessionId: string }];
60
+
61
+ /**
62
+ * The optimistic n-of-n key path was abandoned and the k-of-n fallback
63
+ * (script-path) signing round started (ADR 042). After this, the cohort
64
+ * completes via the fallback once k members sign.
65
+ */
66
+ 'fallback-started': [{ cohortId: string; sessionId: string }];
67
+
68
+ /** A participant has contributed their MuSig2 nonce. */
69
+ 'nonce-received': [{ cohortId: string; participantDid: string }];
70
+
71
+ /** Signing complete: final aggregated signature is ready to broadcast. */
72
+ 'signing-complete': [AggregationResult];
73
+
74
+ /** Cohort transitioned to Failed phase (e.g. a participant rejected validation). */
75
+ 'cohort-failed': [{ cohortId: string; reason: string }];
76
+
77
+ /** A non-fatal error occurred. Fatal errors reject the run() promise. */
78
+ 'error': [Error];
79
+ };
80
+
81
+ /**
82
+ * AggregationParticipantRunner events are emitted by the AggregationParticipantRunner to signal
83
+ * important milestones and actions during the participant's involvement in the aggregation process.
84
+ * They can be listened to by external code to react to these events, such as logging, updating a
85
+ * UI, or triggering additional actions.
86
+ */
87
+ export type AggregationParticipantEvents = {
88
+ /** A new cohort advert was discovered. Fires before the shouldJoin filter. */
89
+ 'cohort-discovered': [CohortAdvert];
90
+
91
+ /** Participant has opted in to a cohort. */
92
+ 'cohort-joined': [{ cohortId: string }];
93
+
94
+ /** Cohort keygen is complete: beacon address is now available. */
95
+ 'cohort-ready': [{ cohortId: string; beaconAddress: string }];
96
+
97
+ /** Participant has submitted their signed update. */
98
+ 'update-submitted': [{ cohortId: string }];
99
+
100
+ /** Participant declined to submit an update this round (cooperative non-inclusion). */
101
+ 'update-declined': [{ cohortId: string }];
102
+
103
+ /** Aggregated data has arrived for validation. Fires before the validate callback. */
104
+ 'validation-requested': [PendingValidation];
105
+
106
+ /** Signing request has arrived. Fires before the sign approval callback. */
107
+ 'signing-requested': [PendingSigningRequest];
108
+
109
+ /**
110
+ * A fallback (k-of-n script-path) signing request has arrived because the
111
+ * service abandoned the optimistic key path (ADR 042). Fires before the sign
112
+ * approval callback; approving signs the fallback spend.
113
+ */
114
+ 'fallback-requested': [PendingFallbackRequest];
115
+
116
+ /**
117
+ * Cohort signing is complete from this participant's perspective.
118
+ * Includes the aggregated sidecar data the participant needs to keep for
119
+ * future DID resolution: the CAS Announcement map (for CAS beacons) or the
120
+ * SMT inclusion proof (for SMT beacons).
121
+ */
122
+ 'cohort-complete': [CohortCompleteInfo];
123
+
124
+ /** Cohort failed (rejected validation, signing error, etc.). */
125
+ 'cohort-failed': [{ cohortId: string; reason: string }];
126
+
127
+ /** A non-fatal error occurred. */
128
+ 'error': [Error];
129
+ };
@@ -0,0 +1,5 @@
1
+ export * from './typed-emitter.js';
2
+ export * from './events.js';
3
+ export * from './service-runner.js';
4
+ export * from './participant-runner.js';
5
+ export * from './aggregation-runner.js';
@@ -0,0 +1,488 @@
1
+ import type { SignedBTCR2Update } from '@did-btcr2/cryptosuite';
2
+ import type { SchnorrKeyPair } from '@did-btcr2/keypair';
3
+ import type { BaseMessage } from '../messages/base.js';
4
+ import {
5
+ AGGREGATED_NONCE,
6
+ AUTHORIZATION_REQUEST,
7
+ COHORT_ADVERT,
8
+ COHORT_OPT_IN_ACCEPT,
9
+ COHORT_READY,
10
+ DISTRIBUTE_AGGREGATED_DATA,
11
+ FALLBACK_AUTHORIZATION_REQUEST,
12
+ } from '../messages/constants.js';
13
+ import type {
14
+ CohortAdvert,
15
+ PendingSigningRequest,
16
+ PendingValidation} from '../participant.js';
17
+ import {
18
+ AggregationParticipant
19
+ } from '../participant.js';
20
+ import { ParticipantCohortPhase } from '../phases.js';
21
+ import { KeyPairAggregationSigner } from '../signer.js';
22
+ import type { Transport } from '../transport/transport.js';
23
+ import type { AggregationParticipantEvents, CohortCompleteInfo } from './events.js';
24
+ import { TypedEventEmitter } from './typed-emitter.js';
25
+
26
+ /** Decision callback: filter discovered cohorts. Default rejects all. */
27
+ export type ShouldJoin = (advert: CohortAdvert) => Promise<boolean>;
28
+
29
+ /** Data callback: provide a signed BTCR2 update for a joined cohort. */
30
+ export type OnProvideUpdate = (info: {
31
+ cohortId: string;
32
+ beaconAddress: string;
33
+ }) => Promise<SignedBTCR2Update | null>;
34
+
35
+ /** Decision callback: approve or reject aggregated data. */
36
+ export type OnValidateData = (info: PendingValidation) => Promise<{ approved: boolean }>;
37
+
38
+ /** Decision callback: approve or reject signing. */
39
+ export type OnApproveSigning = (info: PendingSigningRequest) => Promise<{ approved: boolean }>;
40
+
41
+ export interface AggregationParticipantRunnerOptions {
42
+ /** Underlying transport. */
43
+ transport: Transport;
44
+
45
+ /** Participant identity. */
46
+ did: string;
47
+ keys: SchnorrKeyPair;
48
+
49
+ /**
50
+ * Filter discovered cohorts. Returns true to join, false to skip.
51
+ * Default: rejects all cohorts (caller MUST override or no cohorts will be joined).
52
+ */
53
+ shouldJoin?: ShouldJoin;
54
+
55
+ /**
56
+ * Provide a signed BTCR2 update for the cohort.
57
+ * REQUIRED - no sensible default.
58
+ */
59
+ onProvideUpdate: OnProvideUpdate;
60
+
61
+ /**
62
+ * Approve or reject aggregated data.
63
+ * Default: approve if validation matches the submitted update's hash.
64
+ */
65
+ onValidateData?: OnValidateData;
66
+
67
+ /**
68
+ * Approve or reject the signing request.
69
+ * Default: approve.
70
+ */
71
+ onApproveSigning?: OnApproveSigning;
72
+ }
73
+
74
+ /**
75
+ * High-level facade for an Aggregation Participant.
76
+ *
77
+ * Long-running listener: waits for cohort adverts, applies the `shouldJoin`
78
+ * filter, and drives each accepted cohort through the full protocol to
79
+ * completion in parallel.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const transport = new NostrTransport({ relays: [RELAY] });
84
+ * transport.registerActor(myDid, myKeys);
85
+ *
86
+ * const runner = new AggregationParticipantRunner({
87
+ * transport,
88
+ * did: myDid,
89
+ * keys: myKeys,
90
+ * shouldJoin: async (advert) => advert.beaconType === 'CASBeacon',
91
+ * onProvideUpdate: async ({ beaconAddress }) => {
92
+ * return Updater.sign(myDid, unsigned, vm, secretKey);
93
+ * },
94
+ * });
95
+ *
96
+ * runner.on('cohort-complete', ({ beaconAddress }) => {
97
+ * console.log(`Add to DID document: bitcoin:${beaconAddress}`);
98
+ * });
99
+ *
100
+ * await runner.start();
101
+ * ```
102
+ *
103
+ * For full manual control, drop down to the underlying state machine via
104
+ * `runner.session`. The state machine has no transport coupling and exposes
105
+ * every protocol decision as an explicit method.
106
+ *
107
+ * @class AggregationParticipantRunner
108
+ * @extends TypedEventEmitter<AggregationParticipantEvents>
109
+ */
110
+ export class AggregationParticipantRunner extends TypedEventEmitter<AggregationParticipantEvents> {
111
+ /** Direct access to the underlying state machine for advanced use. */
112
+ readonly session: AggregationParticipant;
113
+
114
+ readonly #transport: Transport;
115
+ readonly #did: string;
116
+ readonly #shouldJoin: ShouldJoin;
117
+ readonly #onProvideUpdate: OnProvideUpdate;
118
+ readonly #onValidateData: OnValidateData;
119
+ readonly #onApproveSigning: OnApproveSigning;
120
+
121
+ #handlersRegistered = false;
122
+ #stopped = false;
123
+
124
+ constructor(options: AggregationParticipantRunnerOptions) {
125
+ super();
126
+ this.#transport = options.transport;
127
+ this.#did = options.did;
128
+ this.#shouldJoin = options.shouldJoin ?? (async () => false);
129
+ this.#onProvideUpdate = options.onProvideUpdate;
130
+ this.#onValidateData = options.onValidateData ?? (async (info) => ({ approved: info.matches }));
131
+ this.#onApproveSigning = options.onApproveSigning ?? (async () => ({ approved: true }));
132
+
133
+ this.session = new AggregationParticipant({
134
+ did : options.did,
135
+ signer : new KeyPairAggregationSigner(options.keys),
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Start listening for cohorts. The runner stays active until {@link stop}
141
+ * is called or the underlying transport disconnects.
142
+ */
143
+ async start(): Promise<void> {
144
+ this.#registerHandlers();
145
+ }
146
+
147
+ /** Stop the runner and detach transport handlers. Safe to call repeatedly. */
148
+ stop(): void {
149
+ this.#stopped = true;
150
+ this.#unregisterHandlers();
151
+ // Deterministically wipe any secret nonce left on an abandoned signing
152
+ // session. The signing key itself lives behind the AggregationSigner and is
153
+ // never retained here.
154
+ this.session.clearSecrets();
155
+ }
156
+
157
+ /** Message types this runner listens for on the transport. */
158
+ static readonly #HANDLED_MESSAGE_TYPES: readonly string[] = [
159
+ COHORT_ADVERT,
160
+ COHORT_OPT_IN_ACCEPT,
161
+ COHORT_READY,
162
+ DISTRIBUTE_AGGREGATED_DATA,
163
+ AUTHORIZATION_REQUEST,
164
+ AGGREGATED_NONCE,
165
+ FALLBACK_AUTHORIZATION_REQUEST,
166
+ ];
167
+
168
+ /** Internal: detach from the transport. Safe to call repeatedly. */
169
+ #unregisterHandlers(): void {
170
+ if(!this.#handlersRegistered) return;
171
+ this.#handlersRegistered = false;
172
+ for(const type of AggregationParticipantRunner.#HANDLED_MESSAGE_TYPES) {
173
+ this.#transport.unregisterMessageHandler(this.#did, type);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Single-shot helper: start, join the first cohort that passes `shouldJoin`,
179
+ * drive it to completion, and resolve. Convenient for tests and demos. The
180
+ * single-cohort special case of {@link joinMatching} (count = 1).
181
+ */
182
+ static async joinFirst(
183
+ options: AggregationParticipantRunnerOptions
184
+ ): Promise<CohortCompleteInfo> {
185
+ return new Promise((resolve, reject) => {
186
+ const runner = new AggregationParticipantRunner(options);
187
+ runner.once('cohort-complete', (info) => {
188
+ runner.stop();
189
+ resolve(info);
190
+ });
191
+ runner.on('error', reject);
192
+ runner.start().catch(reject);
193
+ });
194
+ }
195
+
196
+ /**
197
+ * Multi-cohort helper: start, join EVERY cohort whose advert passes
198
+ * `shouldJoin`, drive each to completion in parallel, and resolve once
199
+ * `count` cohorts have completed (the runner stops at that point). The
200
+ * N-cohort generalization of {@link joinFirst}, for a participant that joins
201
+ * several cohorts advertised by one service.
202
+ *
203
+ * For an open-ended, long-lived subscriber (no fixed count), construct an
204
+ * {@link AggregationParticipantRunner} directly, set `shouldJoin`, call
205
+ * `start()`, and listen for `cohort-complete` - the runner already drives
206
+ * any number of cohorts concurrently.
207
+ *
208
+ * @param options Participant runner options (set `shouldJoin` to select cohorts).
209
+ * @param count Number of completed cohorts to collect before resolving.
210
+ * @returns The {@link CohortCompleteInfo} for each completed cohort, in completion order.
211
+ */
212
+ static async joinMatching(
213
+ options: AggregationParticipantRunnerOptions,
214
+ count: number,
215
+ ): Promise<CohortCompleteInfo[]> {
216
+ return new Promise((resolve, reject) => {
217
+ const runner = new AggregationParticipantRunner(options);
218
+ const completed: CohortCompleteInfo[] = [];
219
+ runner.on('cohort-complete', (info) => {
220
+ completed.push(info);
221
+ if(completed.length >= count) {
222
+ runner.stop();
223
+ resolve(completed);
224
+ }
225
+ });
226
+ runner.on('error', reject);
227
+ runner.start().catch(reject);
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Internal: handler registration with the transport. Idempotent and safe to call multiple times,
233
+ * but only registers handlers once.
234
+ */
235
+ #registerHandlers(): void {
236
+ if (this.#handlersRegistered) return;
237
+ this.#handlersRegistered = true;
238
+
239
+ this.#transport.registerMessageHandler(this.#did, COHORT_ADVERT, this.#handleAdvert.bind(this));
240
+ this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN_ACCEPT, this.#handleOptInAccept.bind(this));
241
+ this.#transport.registerMessageHandler(this.#did, COHORT_READY, this.#handleCohortReady.bind(this));
242
+ this.#transport.registerMessageHandler(this.#did, DISTRIBUTE_AGGREGATED_DATA, this.#handleDistributeData.bind(this));
243
+ this.#transport.registerMessageHandler(this.#did, AUTHORIZATION_REQUEST, this.#handleAuthorizationRequest.bind(this));
244
+ this.#transport.registerMessageHandler(this.#did, AGGREGATED_NONCE, this.#handleAggregatedNonce.bind(this));
245
+ this.#transport.registerMessageHandler(this.#did, FALLBACK_AUTHORIZATION_REQUEST, this.#handleFallbackAuthorizationRequest.bind(this));
246
+ }
247
+
248
+ /**
249
+ * Internal: handler for cohort adverts. Applies the `shouldJoin` filter and joins if approved.
250
+ * @param {BaseMessage} msg - The received cohort advert message.
251
+ * @returns {Promise<void>} Resolves when processing is complete.
252
+ */
253
+ async #handleAdvert(msg: BaseMessage): Promise<void> {
254
+ if (this.#stopped) return;
255
+ try {
256
+ this.session.receive(msg);
257
+
258
+ const advert = this.session.discoveredCohorts.get(msg.body?.cohortId ?? '');
259
+ if (!advert) return;
260
+ this.emit('cohort-discovered', advert);
261
+
262
+ // Register the service's communication key for encrypted message routing
263
+ if (advert.serviceCommunicationPk) {
264
+ this.#transport.registerPeer(advert.serviceDid, advert.serviceCommunicationPk);
265
+ }
266
+
267
+ const join = await this.#shouldJoin(advert);
268
+ if (!join) return;
269
+
270
+ await this.#sendAll(this.session.joinCohort(advert.cohortId));
271
+ this.emit('cohort-joined', { cohortId: advert.cohortId });
272
+ } catch (err) {
273
+ this.emit('error', err as Error);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Internal: handler for opt-in accept messages. Updates the session state accordingly.
279
+ * @param {BaseMessage} msg - The received opt-in accept message.
280
+ */
281
+ #handleOptInAccept(msg: BaseMessage): void {
282
+ if (this.#stopped) return;
283
+ try {
284
+ this.session.receive(msg);
285
+ } catch (err) {
286
+ this.emit('error', err as Error);
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Internal: handler for cohort ready messages. Updates the session state, emits a 'cohort-ready'
292
+ * event, and triggers the update submission flow via the `onProvideUpdate` callback.
293
+ * @param {BaseMessage} msg - The received cohort ready message.
294
+ * @returns {Promise<void>} Resolves when processing is complete.
295
+ */
296
+ async #handleCohortReady(msg: BaseMessage): Promise<void> {
297
+ if (this.#stopped) return;
298
+ try {
299
+ this.session.receive(msg);
300
+
301
+ const cohortId = msg.body?.cohortId;
302
+ if (!cohortId) return;
303
+
304
+ const info = this.session.joinedCohorts.get(cohortId);
305
+ if (!info) return;
306
+ this.emit('cohort-ready', { cohortId, beaconAddress: info.beaconAddress });
307
+
308
+ // Construct the signed update via caller callback and submit. A null return
309
+ // means the member has no update this round: it declines (cooperative
310
+ // non-inclusion) but stays in the cohort and still signs.
311
+ const signedUpdate = await this.#onProvideUpdate({
312
+ cohortId,
313
+ beaconAddress : info.beaconAddress,
314
+ });
315
+ if(signedUpdate === null) {
316
+ await this.#sendAll(this.session.declineUpdate(cohortId));
317
+ this.emit('update-declined', { cohortId });
318
+ } else {
319
+ await this.#sendAll(this.session.submitUpdate(cohortId, signedUpdate));
320
+ this.emit('update-submitted', { cohortId });
321
+ }
322
+ } catch (err) {
323
+ this.emit('error', err as Error);
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Internal: handler for distribute aggregated data messages. Updates the session state, emits a
329
+ * 'validation-requested' event, and triggers the validation decision flow via the `onValidateData`
330
+ * callback. Depending on the decision, sends an approval or rejection message.
331
+ * @param {BaseMessage} msg - The received distribute aggregated data message.
332
+ * @returns {Promise<void>} Resolves when processing is complete.
333
+ * @throws {Error} If an error occurs during message processing or callback execution.
334
+ */
335
+ async #handleDistributeData(msg: BaseMessage): Promise<void> {
336
+ if (this.#stopped) return;
337
+ try {
338
+ this.session.receive(msg);
339
+
340
+ const cohortId = msg.body?.cohortId;
341
+ if (!cohortId) return;
342
+
343
+ const validation = this.session.pendingValidations.get(cohortId);
344
+ if (!validation) return;
345
+ this.emit('validation-requested', validation);
346
+
347
+ const decision = await this.#onValidateData(validation);
348
+ if (decision.approved) {
349
+ await this.#sendAll(this.session.approveValidation(cohortId));
350
+ } else {
351
+ await this.#sendAll(this.session.rejectValidation(cohortId));
352
+ this.emit('cohort-failed', { cohortId, reason: 'Validation rejected by participant' });
353
+ }
354
+ } catch (err) {
355
+ this.emit('error', err as Error);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Internal: handler for authorization request messages. Updates the session state, emits a
361
+ * 'signing-requested' event, and triggers the signing approval flow via the `onApproveSigning`
362
+ * callback. Depending on the decision, sends a nonce approval message or emits a 'cohort-failed'
363
+ * event.
364
+ * @param {BaseMessage} msg - The received authorization request message.
365
+ * @returns {Promise<void>} Resolves when processing is complete.
366
+ * @throws {Error} If an error occurs during message processing or callback execution.
367
+ */
368
+ async #handleAuthorizationRequest(msg: BaseMessage): Promise<void> {
369
+ if (this.#stopped) return;
370
+ try {
371
+ this.session.receive(msg);
372
+
373
+ const cohortId = msg.body?.cohortId;
374
+ if (!cohortId) return;
375
+
376
+ const req = this.session.pendingSigningRequests.get(cohortId);
377
+ if (!req) return;
378
+ this.emit('signing-requested', req);
379
+
380
+ const decision = await this.#onApproveSigning(req);
381
+ if (!decision.approved) {
382
+ this.emit('cohort-failed', { cohortId, reason: 'Signing rejected by participant' });
383
+ return;
384
+ }
385
+
386
+ await this.#sendAll(this.session.approveNonce(cohortId));
387
+ } catch (err) {
388
+ this.emit('error', err as Error);
389
+ }
390
+ }
391
+
392
+ /**
393
+ * Internal: handler for aggregated nonce messages. Updates the session state, triggers partial
394
+ * signature generation, and sends the partial signature to the aggregator. If the cohort reaches
395
+ * completion after processing the nonce, emits a 'cohort-complete' event.
396
+ * @param {BaseMessage} msg - The received aggregated nonce message.
397
+ * @returns {Promise<void>} Resolves when processing is complete.
398
+ * @throws {Error} If an error occurs during message processing or partial signature generation.
399
+ */
400
+ async #handleAggregatedNonce(msg: BaseMessage): Promise<void> {
401
+ if (this.#stopped) return;
402
+ try {
403
+ this.session.receive(msg);
404
+
405
+ const cohortId = msg.body?.cohortId;
406
+ if (!cohortId) return;
407
+
408
+ await this.#sendAll(this.session.generatePartialSignature(cohortId));
409
+
410
+ // Check if we've reached completion
411
+ this.#emitCohortCompleteIfDone(cohortId);
412
+ } catch (err) {
413
+ this.emit('error', err as Error);
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Internal: handler for fallback authorization requests. The service abandoned
419
+ * the optimistic key path; the member authorizes the k-of-n script-path spend
420
+ * of the same beacon transaction (ADR 042). Reuses `onApproveSigning` to gate
421
+ * the decision, signs the fallback, and completes once it has contributed (the
422
+ * service needs only k members).
423
+ * @param {BaseMessage} msg - The received fallback authorization request message.
424
+ * @returns {Promise<void>} Resolves when processing is complete.
425
+ */
426
+ async #handleFallbackAuthorizationRequest(msg: BaseMessage): Promise<void> {
427
+ if (this.#stopped) return;
428
+ try {
429
+ this.session.receive(msg);
430
+
431
+ const cohortId = msg.body?.cohortId;
432
+ if (!cohortId) return;
433
+
434
+ const req = this.session.pendingFallbackRequests.get(cohortId);
435
+ if (!req) return;
436
+ this.emit('fallback-requested', req);
437
+
438
+ const decision = await this.#onApproveSigning(req);
439
+ if (!decision.approved) {
440
+ this.emit('cohort-failed', { cohortId, reason: 'Fallback signing rejected by participant' });
441
+ return;
442
+ }
443
+
444
+ await this.#sendAll(this.session.approveFallback(cohortId));
445
+ // The member has contributed its fallback signature and is done from its
446
+ // own perspective, regardless of whether the service has yet collected k.
447
+ this.#emitCohortCompleteIfDone(cohortId);
448
+ } catch (err) {
449
+ this.emit('error', err as Error);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Internal: emit `cohort-complete` with the participant's retained sidecar once
455
+ * the cohort has reached the Complete phase. Surfaces the CAS Announcement map
456
+ * (CAS beacons) or the SMT proof (SMT beacons) the participant keeps for future
457
+ * DID resolution. Read via getValidation (not pendingValidations, which lists
458
+ * only the AwaitingValidation phase) so the sidecar is still available at
459
+ * Complete. Shared by the optimistic and fallback completion paths.
460
+ */
461
+ #emitCohortCompleteIfDone(cohortId: string): void {
462
+ if (this.session.getCohortPhase(cohortId) !== ParticipantCohortPhase.Complete) return;
463
+ const info = this.session.joinedCohorts.get(cohortId);
464
+ if (!info) return;
465
+ const validation = this.session.getValidation(cohortId);
466
+ this.emit('cohort-complete', {
467
+ cohortId,
468
+ beaconAddress : info.beaconAddress,
469
+ beaconType : validation?.beaconType ?? '',
470
+ included : validation?.included ?? true,
471
+ casAnnouncement : validation?.casAnnouncement,
472
+ smtProof : validation?.smtProof,
473
+ });
474
+ }
475
+
476
+ /**
477
+ * Internal: send helper to ensure messages are sent sequentially. This is important for protocol
478
+ * correctness, as some transports may not guarantee message order if sent in parallel.
479
+ * @param {BaseMessage[]} msgs - The messages to send.
480
+ * @returns {Promise<void>} Resolves when all messages have been sent.
481
+ * @throws {Error} If an error occurs during message sending.
482
+ */
483
+ async #sendAll(msgs: BaseMessage[]): Promise<void> {
484
+ for (const m of msgs) {
485
+ await this.#transport.sendMessage(m, this.#did, m.to);
486
+ }
487
+ }
488
+ }