@did-btcr2/aggregation 0.1.0 → 0.3.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 (342) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/browser.js +18107 -18096
  3. package/dist/browser.mjs +18107 -18096
  4. package/dist/cjs/core/index.js +2423 -0
  5. package/dist/cjs/index.js +3513 -3521
  6. package/dist/cjs/participant/index.js +2425 -0
  7. package/dist/cjs/service/index.js +3199 -0
  8. package/dist/esm/{runner/aggregation-runner.js → aggregation-runner.js} +4 -4
  9. package/dist/esm/aggregation-runner.js.map +1 -0
  10. package/dist/esm/core/beacon-strategy.js.map +1 -0
  11. package/dist/esm/core/cohort.js.map +1 -0
  12. package/dist/esm/core/conditions.js.map +1 -0
  13. package/dist/esm/core/errors.js.map +1 -0
  14. package/dist/esm/core/fallback-spend.js.map +1 -0
  15. package/dist/esm/core/index.js +23 -0
  16. package/dist/esm/core/index.js.map +1 -0
  17. package/dist/esm/core/logger.js.map +1 -0
  18. package/dist/esm/core/messages/base.js.map +1 -0
  19. package/dist/esm/core/messages/bodies.js.map +1 -0
  20. package/dist/esm/core/messages/constants.js.map +1 -0
  21. package/dist/esm/core/messages/factories.js.map +1 -0
  22. package/dist/esm/core/messages/guards.js.map +1 -0
  23. package/dist/esm/core/messages/index.js.map +1 -0
  24. package/dist/esm/core/phases.js.map +1 -0
  25. package/dist/esm/core/recovery-policy.js.map +1 -0
  26. package/dist/esm/core/recovery-spend.js.map +1 -0
  27. package/dist/esm/core/signer.js.map +1 -0
  28. package/dist/esm/core/signing-session.js.map +1 -0
  29. package/dist/esm/core/transport/didcomm.js.map +1 -0
  30. package/dist/esm/core/transport/error.js.map +1 -0
  31. package/dist/esm/core/transport/http/envelope.js.map +1 -0
  32. package/dist/esm/core/transport/http/errors.js.map +1 -0
  33. package/dist/esm/core/transport/http/index.js +9 -0
  34. package/dist/esm/core/transport/http/index.js.map +1 -0
  35. package/dist/esm/core/transport/http/protocol.js.map +1 -0
  36. package/dist/esm/core/transport/http/request-auth.js.map +1 -0
  37. package/dist/esm/core/transport/in-memory.js.map +1 -0
  38. package/dist/esm/core/transport/index.js +12 -0
  39. package/dist/esm/core/transport/index.js.map +1 -0
  40. package/dist/esm/core/transport/nostr.js.map +1 -0
  41. package/dist/esm/core/transport/transport.js.map +1 -0
  42. package/dist/esm/core/typed-emitter.js.map +1 -0
  43. package/dist/esm/index.js +13 -20
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/participant/events.js.map +1 -0
  46. package/dist/esm/{transport/http/client.js → participant/http-client.js} +6 -6
  47. package/dist/esm/participant/http-client.js.map +1 -0
  48. package/dist/esm/participant/index.js +13 -0
  49. package/dist/esm/participant/index.js.map +1 -0
  50. package/dist/esm/{runner → participant}/participant-runner.js +5 -5
  51. package/dist/esm/participant/participant-runner.js.map +1 -0
  52. package/dist/esm/{participant.js → participant/participant.js} +11 -11
  53. package/dist/esm/participant/participant.js.map +1 -0
  54. package/dist/esm/participant/sse-stream.js.map +1 -0
  55. package/dist/esm/service/events.js +2 -0
  56. package/dist/esm/{runner → service}/events.js.map +1 -1
  57. package/dist/esm/{transport/http/server.js → service/http-server.js} +6 -6
  58. package/dist/esm/service/http-server.js.map +1 -0
  59. package/dist/esm/service/inbox-buffer.js.map +1 -0
  60. package/dist/esm/service/index.js +16 -0
  61. package/dist/esm/service/index.js.map +1 -0
  62. package/dist/esm/service/nonce-cache.js.map +1 -0
  63. package/dist/esm/service/rate-limiter.js.map +1 -0
  64. package/dist/esm/{runner → service}/service-runner.js +5 -5
  65. package/dist/esm/service/service-runner.js.map +1 -0
  66. package/dist/esm/{service.js → service/service.js} +12 -12
  67. package/dist/esm/service/service.js.map +1 -0
  68. package/dist/esm/service/sse-writer.js.map +1 -0
  69. package/dist/esm/{transport/factory.js → transport-factory.js} +5 -5
  70. package/dist/esm/transport-factory.js.map +1 -0
  71. package/dist/types/{runner/aggregation-runner.d.ts → aggregation-runner.d.ts} +3 -3
  72. package/dist/types/aggregation-runner.d.ts.map +1 -0
  73. package/dist/types/{beacon-strategy.d.ts → core/beacon-strategy.d.ts} +2 -2
  74. package/dist/types/core/beacon-strategy.d.ts.map +1 -0
  75. package/dist/types/{cohort.d.ts → core/cohort.d.ts} +3 -3
  76. package/dist/types/core/cohort.d.ts.map +1 -0
  77. package/dist/types/core/conditions.d.ts.map +1 -0
  78. package/dist/types/core/errors.d.ts.map +1 -0
  79. package/dist/types/core/fallback-spend.d.ts.map +1 -0
  80. package/dist/types/core/index.d.ts +15 -0
  81. package/dist/types/core/index.d.ts.map +1 -0
  82. package/dist/types/core/logger.d.ts.map +1 -0
  83. package/dist/types/core/messages/base.d.ts.map +1 -0
  84. package/dist/types/core/messages/bodies.d.ts.map +1 -0
  85. package/dist/types/core/messages/constants.d.ts.map +1 -0
  86. package/dist/types/core/messages/factories.d.ts.map +1 -0
  87. package/dist/types/core/messages/guards.d.ts.map +1 -0
  88. package/dist/types/core/messages/index.d.ts.map +1 -0
  89. package/dist/types/core/phases.d.ts.map +1 -0
  90. package/dist/types/core/recovery-policy.d.ts.map +1 -0
  91. package/dist/types/core/recovery-spend.d.ts.map +1 -0
  92. package/dist/types/core/signer.d.ts.map +1 -0
  93. package/dist/types/core/signing-session.d.ts.map +1 -0
  94. package/dist/types/core/transport/didcomm.d.ts.map +1 -0
  95. package/dist/types/core/transport/error.d.ts.map +1 -0
  96. package/dist/types/core/transport/http/envelope.d.ts.map +1 -0
  97. package/dist/types/core/transport/http/errors.d.ts.map +1 -0
  98. package/dist/types/core/transport/http/index.d.ts +5 -0
  99. package/dist/types/core/transport/http/index.d.ts.map +1 -0
  100. package/dist/types/core/transport/http/protocol.d.ts.map +1 -0
  101. package/dist/types/core/transport/http/request-auth.d.ts.map +1 -0
  102. package/dist/types/core/transport/in-memory.d.ts.map +1 -0
  103. package/{src/transport/index.ts → dist/types/core/transport/index.d.ts} +1 -1
  104. package/dist/types/core/transport/index.d.ts.map +1 -0
  105. package/dist/types/core/transport/nostr.d.ts.map +1 -0
  106. package/dist/types/core/transport/transport.d.ts.map +1 -0
  107. package/dist/types/core/typed-emitter.d.ts.map +1 -0
  108. package/dist/types/index.d.ts +5 -16
  109. package/dist/types/index.d.ts.map +1 -1
  110. package/dist/types/{runner → participant}/events.d.ts +1 -76
  111. package/dist/types/participant/events.d.ts.map +1 -0
  112. package/dist/types/{transport/http/client.d.ts → participant/http-client.d.ts} +4 -4
  113. package/dist/types/participant/http-client.d.ts.map +1 -0
  114. package/dist/types/participant/index.d.ts +6 -0
  115. package/dist/types/participant/index.d.ts.map +1 -0
  116. package/dist/types/{runner → participant}/participant-runner.d.ts +6 -6
  117. package/dist/types/participant/participant-runner.d.ts.map +1 -0
  118. package/dist/types/{participant.d.ts → participant/participant.d.ts} +6 -6
  119. package/dist/types/participant/participant.d.ts.map +1 -0
  120. package/dist/types/participant/sse-stream.d.ts.map +1 -0
  121. package/dist/types/service/events.d.ts +76 -0
  122. package/dist/types/service/events.d.ts.map +1 -0
  123. package/dist/types/{transport/http/server.d.ts → service/http-server.d.ts} +4 -4
  124. package/dist/types/{transport/http/server.d.ts.map → service/http-server.d.ts.map} +1 -1
  125. package/dist/types/service/inbox-buffer.d.ts.map +1 -0
  126. package/dist/types/service/index.d.ts +9 -0
  127. package/dist/types/service/index.d.ts.map +1 -0
  128. package/dist/types/service/nonce-cache.d.ts.map +1 -0
  129. package/dist/types/service/rate-limiter.d.ts.map +1 -0
  130. package/dist/types/{runner → service}/service-runner.d.ts +4 -4
  131. package/dist/types/service/service-runner.d.ts.map +1 -0
  132. package/dist/types/{service.d.ts → service/service.d.ts} +6 -6
  133. package/dist/types/service/service.d.ts.map +1 -0
  134. package/dist/types/service/sse-writer.d.ts.map +1 -0
  135. package/dist/types/{transport/factory.d.ts → transport-factory.d.ts} +5 -5
  136. package/dist/types/transport-factory.d.ts.map +1 -0
  137. package/package.json +23 -6
  138. package/src/{runner/aggregation-runner.ts → aggregation-runner.ts} +7 -7
  139. package/src/{beacon-strategy.ts → core/beacon-strategy.ts} +2 -2
  140. package/src/{cohort.ts → core/cohort.ts} +3 -3
  141. package/src/core/index.ts +22 -0
  142. package/src/core/transport/http/index.ts +8 -0
  143. package/src/core/transport/index.ts +11 -0
  144. package/src/index.ts +13 -23
  145. package/src/{runner → participant}/events.ts +1 -59
  146. package/src/{transport/http/client.ts → participant/http-client.ts} +8 -8
  147. package/src/participant/index.ts +12 -0
  148. package/src/{runner → participant}/participant-runner.ts +10 -10
  149. package/src/{participant.ts → participant/participant.ts} +18 -18
  150. package/src/service/events.ts +58 -0
  151. package/src/{transport/http/server.ts → service/http-server.ts} +8 -8
  152. package/src/service/index.ts +15 -0
  153. package/src/{runner → service}/service-runner.ts +9 -9
  154. package/src/{service.ts → service/service.ts} +19 -19
  155. package/src/{transport/factory.ts → transport-factory.ts} +8 -8
  156. package/dist/esm/beacon-strategy.js.map +0 -1
  157. package/dist/esm/cohort.js.map +0 -1
  158. package/dist/esm/conditions.js.map +0 -1
  159. package/dist/esm/errors.js.map +0 -1
  160. package/dist/esm/fallback-spend.js.map +0 -1
  161. package/dist/esm/logger.js.map +0 -1
  162. package/dist/esm/messages/base.js.map +0 -1
  163. package/dist/esm/messages/bodies.js.map +0 -1
  164. package/dist/esm/messages/constants.js.map +0 -1
  165. package/dist/esm/messages/factories.js.map +0 -1
  166. package/dist/esm/messages/guards.js.map +0 -1
  167. package/dist/esm/messages/index.js.map +0 -1
  168. package/dist/esm/participant.js.map +0 -1
  169. package/dist/esm/phases.js.map +0 -1
  170. package/dist/esm/recovery-policy.js.map +0 -1
  171. package/dist/esm/recovery-spend.js.map +0 -1
  172. package/dist/esm/runner/aggregation-runner.js.map +0 -1
  173. package/dist/esm/runner/index.js +0 -6
  174. package/dist/esm/runner/index.js.map +0 -1
  175. package/dist/esm/runner/participant-runner.js.map +0 -1
  176. package/dist/esm/runner/service-runner.js.map +0 -1
  177. package/dist/esm/runner/typed-emitter.js.map +0 -1
  178. package/dist/esm/service.js.map +0 -1
  179. package/dist/esm/signer.js.map +0 -1
  180. package/dist/esm/signing-session.js.map +0 -1
  181. package/dist/esm/transport/didcomm.js.map +0 -1
  182. package/dist/esm/transport/error.js.map +0 -1
  183. package/dist/esm/transport/factory.js.map +0 -1
  184. package/dist/esm/transport/http/client.js.map +0 -1
  185. package/dist/esm/transport/http/envelope.js.map +0 -1
  186. package/dist/esm/transport/http/errors.js.map +0 -1
  187. package/dist/esm/transport/http/inbox-buffer.js.map +0 -1
  188. package/dist/esm/transport/http/index.js +0 -12
  189. package/dist/esm/transport/http/index.js.map +0 -1
  190. package/dist/esm/transport/http/nonce-cache.js.map +0 -1
  191. package/dist/esm/transport/http/protocol.js.map +0 -1
  192. package/dist/esm/transport/http/rate-limiter.js.map +0 -1
  193. package/dist/esm/transport/http/request-auth.js.map +0 -1
  194. package/dist/esm/transport/http/server.js.map +0 -1
  195. package/dist/esm/transport/http/sse-stream.js.map +0 -1
  196. package/dist/esm/transport/http/sse-writer.js.map +0 -1
  197. package/dist/esm/transport/in-memory.js.map +0 -1
  198. package/dist/esm/transport/index.js +0 -8
  199. package/dist/esm/transport/index.js.map +0 -1
  200. package/dist/esm/transport/nostr.js.map +0 -1
  201. package/dist/esm/transport/transport.js.map +0 -1
  202. package/dist/types/beacon-strategy.d.ts.map +0 -1
  203. package/dist/types/cohort.d.ts.map +0 -1
  204. package/dist/types/conditions.d.ts.map +0 -1
  205. package/dist/types/errors.d.ts.map +0 -1
  206. package/dist/types/fallback-spend.d.ts.map +0 -1
  207. package/dist/types/logger.d.ts.map +0 -1
  208. package/dist/types/messages/base.d.ts.map +0 -1
  209. package/dist/types/messages/bodies.d.ts.map +0 -1
  210. package/dist/types/messages/constants.d.ts.map +0 -1
  211. package/dist/types/messages/factories.d.ts.map +0 -1
  212. package/dist/types/messages/guards.d.ts.map +0 -1
  213. package/dist/types/messages/index.d.ts.map +0 -1
  214. package/dist/types/participant.d.ts.map +0 -1
  215. package/dist/types/phases.d.ts.map +0 -1
  216. package/dist/types/recovery-policy.d.ts.map +0 -1
  217. package/dist/types/recovery-spend.d.ts.map +0 -1
  218. package/dist/types/runner/aggregation-runner.d.ts.map +0 -1
  219. package/dist/types/runner/events.d.ts.map +0 -1
  220. package/dist/types/runner/index.d.ts +0 -6
  221. package/dist/types/runner/index.d.ts.map +0 -1
  222. package/dist/types/runner/participant-runner.d.ts.map +0 -1
  223. package/dist/types/runner/service-runner.d.ts.map +0 -1
  224. package/dist/types/runner/typed-emitter.d.ts.map +0 -1
  225. package/dist/types/service.d.ts.map +0 -1
  226. package/dist/types/signer.d.ts.map +0 -1
  227. package/dist/types/signing-session.d.ts.map +0 -1
  228. package/dist/types/transport/didcomm.d.ts.map +0 -1
  229. package/dist/types/transport/error.d.ts.map +0 -1
  230. package/dist/types/transport/factory.d.ts.map +0 -1
  231. package/dist/types/transport/http/client.d.ts.map +0 -1
  232. package/dist/types/transport/http/envelope.d.ts.map +0 -1
  233. package/dist/types/transport/http/errors.d.ts.map +0 -1
  234. package/dist/types/transport/http/inbox-buffer.d.ts.map +0 -1
  235. package/dist/types/transport/http/index.d.ts +0 -12
  236. package/dist/types/transport/http/index.d.ts.map +0 -1
  237. package/dist/types/transport/http/nonce-cache.d.ts.map +0 -1
  238. package/dist/types/transport/http/protocol.d.ts.map +0 -1
  239. package/dist/types/transport/http/rate-limiter.d.ts.map +0 -1
  240. package/dist/types/transport/http/request-auth.d.ts.map +0 -1
  241. package/dist/types/transport/http/sse-stream.d.ts.map +0 -1
  242. package/dist/types/transport/http/sse-writer.d.ts.map +0 -1
  243. package/dist/types/transport/in-memory.d.ts.map +0 -1
  244. package/dist/types/transport/index.d.ts +0 -8
  245. package/dist/types/transport/index.d.ts.map +0 -1
  246. package/dist/types/transport/nostr.d.ts.map +0 -1
  247. package/dist/types/transport/transport.d.ts.map +0 -1
  248. package/src/runner/index.ts +0 -5
  249. package/src/transport/http/index.ts +0 -11
  250. /package/dist/esm/{beacon-strategy.js → core/beacon-strategy.js} +0 -0
  251. /package/dist/esm/{cohort.js → core/cohort.js} +0 -0
  252. /package/dist/esm/{conditions.js → core/conditions.js} +0 -0
  253. /package/dist/esm/{errors.js → core/errors.js} +0 -0
  254. /package/dist/esm/{fallback-spend.js → core/fallback-spend.js} +0 -0
  255. /package/dist/esm/{logger.js → core/logger.js} +0 -0
  256. /package/dist/esm/{messages → core/messages}/base.js +0 -0
  257. /package/dist/esm/{messages → core/messages}/bodies.js +0 -0
  258. /package/dist/esm/{messages → core/messages}/constants.js +0 -0
  259. /package/dist/esm/{messages → core/messages}/factories.js +0 -0
  260. /package/dist/esm/{messages → core/messages}/guards.js +0 -0
  261. /package/dist/esm/{messages → core/messages}/index.js +0 -0
  262. /package/dist/esm/{phases.js → core/phases.js} +0 -0
  263. /package/dist/esm/{recovery-policy.js → core/recovery-policy.js} +0 -0
  264. /package/dist/esm/{recovery-spend.js → core/recovery-spend.js} +0 -0
  265. /package/dist/esm/{signer.js → core/signer.js} +0 -0
  266. /package/dist/esm/{signing-session.js → core/signing-session.js} +0 -0
  267. /package/dist/esm/{transport → core/transport}/didcomm.js +0 -0
  268. /package/dist/esm/{transport → core/transport}/error.js +0 -0
  269. /package/dist/esm/{transport → core/transport}/http/envelope.js +0 -0
  270. /package/dist/esm/{transport → core/transport}/http/errors.js +0 -0
  271. /package/dist/esm/{transport → core/transport}/http/protocol.js +0 -0
  272. /package/dist/esm/{transport → core/transport}/http/request-auth.js +0 -0
  273. /package/dist/esm/{transport → core/transport}/in-memory.js +0 -0
  274. /package/dist/esm/{transport → core/transport}/nostr.js +0 -0
  275. /package/dist/esm/{transport → core/transport}/transport.js +0 -0
  276. /package/dist/esm/{runner → core}/typed-emitter.js +0 -0
  277. /package/dist/esm/{runner → participant}/events.js +0 -0
  278. /package/dist/esm/{transport/http → participant}/sse-stream.js +0 -0
  279. /package/dist/esm/{transport/http → service}/inbox-buffer.js +0 -0
  280. /package/dist/esm/{transport/http → service}/nonce-cache.js +0 -0
  281. /package/dist/esm/{transport/http → service}/rate-limiter.js +0 -0
  282. /package/dist/esm/{transport/http → service}/sse-writer.js +0 -0
  283. /package/dist/types/{conditions.d.ts → core/conditions.d.ts} +0 -0
  284. /package/dist/types/{errors.d.ts → core/errors.d.ts} +0 -0
  285. /package/dist/types/{fallback-spend.d.ts → core/fallback-spend.d.ts} +0 -0
  286. /package/dist/types/{logger.d.ts → core/logger.d.ts} +0 -0
  287. /package/dist/types/{messages → core/messages}/base.d.ts +0 -0
  288. /package/dist/types/{messages → core/messages}/bodies.d.ts +0 -0
  289. /package/dist/types/{messages → core/messages}/constants.d.ts +0 -0
  290. /package/dist/types/{messages → core/messages}/factories.d.ts +0 -0
  291. /package/dist/types/{messages → core/messages}/guards.d.ts +0 -0
  292. /package/dist/types/{messages → core/messages}/index.d.ts +0 -0
  293. /package/dist/types/{phases.d.ts → core/phases.d.ts} +0 -0
  294. /package/dist/types/{recovery-policy.d.ts → core/recovery-policy.d.ts} +0 -0
  295. /package/dist/types/{recovery-spend.d.ts → core/recovery-spend.d.ts} +0 -0
  296. /package/dist/types/{signer.d.ts → core/signer.d.ts} +0 -0
  297. /package/dist/types/{signing-session.d.ts → core/signing-session.d.ts} +0 -0
  298. /package/dist/types/{transport → core/transport}/didcomm.d.ts +0 -0
  299. /package/dist/types/{transport → core/transport}/error.d.ts +0 -0
  300. /package/dist/types/{transport → core/transport}/http/envelope.d.ts +0 -0
  301. /package/dist/types/{transport → core/transport}/http/errors.d.ts +0 -0
  302. /package/dist/types/{transport → core/transport}/http/protocol.d.ts +0 -0
  303. /package/dist/types/{transport → core/transport}/http/request-auth.d.ts +0 -0
  304. /package/dist/types/{transport → core/transport}/in-memory.d.ts +0 -0
  305. /package/dist/types/{transport → core/transport}/nostr.d.ts +0 -0
  306. /package/dist/types/{transport → core/transport}/transport.d.ts +0 -0
  307. /package/dist/types/{runner → core}/typed-emitter.d.ts +0 -0
  308. /package/dist/types/{transport/http → participant}/sse-stream.d.ts +0 -0
  309. /package/dist/types/{transport/http → service}/inbox-buffer.d.ts +0 -0
  310. /package/dist/types/{transport/http → service}/nonce-cache.d.ts +0 -0
  311. /package/dist/types/{transport/http → service}/rate-limiter.d.ts +0 -0
  312. /package/dist/types/{transport/http → service}/sse-writer.d.ts +0 -0
  313. /package/src/{conditions.ts → core/conditions.ts} +0 -0
  314. /package/src/{errors.ts → core/errors.ts} +0 -0
  315. /package/src/{fallback-spend.ts → core/fallback-spend.ts} +0 -0
  316. /package/src/{logger.ts → core/logger.ts} +0 -0
  317. /package/src/{messages → core/messages}/base.ts +0 -0
  318. /package/src/{messages → core/messages}/bodies.ts +0 -0
  319. /package/src/{messages → core/messages}/constants.ts +0 -0
  320. /package/src/{messages → core/messages}/factories.ts +0 -0
  321. /package/src/{messages → core/messages}/guards.ts +0 -0
  322. /package/src/{messages → core/messages}/index.ts +0 -0
  323. /package/src/{phases.ts → core/phases.ts} +0 -0
  324. /package/src/{recovery-policy.ts → core/recovery-policy.ts} +0 -0
  325. /package/src/{recovery-spend.ts → core/recovery-spend.ts} +0 -0
  326. /package/src/{signer.ts → core/signer.ts} +0 -0
  327. /package/src/{signing-session.ts → core/signing-session.ts} +0 -0
  328. /package/src/{transport → core/transport}/didcomm.ts +0 -0
  329. /package/src/{transport → core/transport}/error.ts +0 -0
  330. /package/src/{transport → core/transport}/http/envelope.ts +0 -0
  331. /package/src/{transport → core/transport}/http/errors.ts +0 -0
  332. /package/src/{transport → core/transport}/http/protocol.ts +0 -0
  333. /package/src/{transport → core/transport}/http/request-auth.ts +0 -0
  334. /package/src/{transport → core/transport}/in-memory.ts +0 -0
  335. /package/src/{transport → core/transport}/nostr.ts +0 -0
  336. /package/src/{transport → core/transport}/transport.ts +0 -0
  337. /package/src/{runner → core}/typed-emitter.ts +0 -0
  338. /package/src/{transport/http → participant}/sse-stream.ts +0 -0
  339. /package/src/{transport/http → service}/inbox-buffer.ts +0 -0
  340. /package/src/{transport/http → service}/nonce-cache.ts +0 -0
  341. /package/src/{transport/http → service}/rate-limiter.ts +0 -0
  342. /package/src/{transport/http → service}/sse-writer.ts +0 -0
@@ -0,0 +1,3199 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/service/index.ts
31
+ var service_exports = {};
32
+ __export(service_exports, {
33
+ AggregationService: () => AggregationService,
34
+ AggregationServiceRunner: () => AggregationServiceRunner,
35
+ DEFAULT_ADVERT_REPEAT_INTERVAL_MS: () => DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
36
+ DEFAULT_MAX_UPDATE_SIZE_BYTES: () => DEFAULT_MAX_UPDATE_SIZE_BYTES,
37
+ HttpServerTransport: () => HttpServerTransport,
38
+ InMemoryRateLimitStore: () => InMemoryRateLimitStore,
39
+ InboxBuffer: () => InboxBuffer,
40
+ NonceCache: () => NonceCache,
41
+ RateLimiter: () => RateLimiter,
42
+ formatSseComment: () => formatSseComment,
43
+ formatSseEvent: () => formatSseEvent
44
+ });
45
+ module.exports = __toCommonJS(service_exports);
46
+
47
+ // src/service/service.ts
48
+ var import_common4 = require("@did-btcr2/common");
49
+ var import_cryptosuite = require("@did-btcr2/cryptosuite");
50
+ var import_secp256k14 = require("@noble/curves/secp256k1.js");
51
+ var import_utils3 = require("@noble/hashes/utils");
52
+
53
+ // src/core/beacon-strategy.ts
54
+ var import_common = require("@did-btcr2/common");
55
+ var import_smt = require("@did-btcr2/smt");
56
+ var CAS_STRATEGY = {
57
+ type: "CASBeacon",
58
+ buildAggregatedData(cohort) {
59
+ cohort.buildCASAnnouncement();
60
+ },
61
+ getDistributePayload(cohort) {
62
+ return { casAnnouncement: cohort.casAnnouncement };
63
+ },
64
+ validateParticipantView({ participantDid, included, expectedHash, body }) {
65
+ const casAnnouncement = body.casAnnouncement;
66
+ if (!casAnnouncement) return { matches: false };
67
+ if (!included) {
68
+ return { matches: !(participantDid in casAnnouncement), casAnnouncement };
69
+ }
70
+ return {
71
+ matches: casAnnouncement[participantDid] === expectedHash,
72
+ casAnnouncement
73
+ };
74
+ }
75
+ };
76
+ var SMT_STRATEGY = {
77
+ type: "SMTBeacon",
78
+ buildAggregatedData(cohort) {
79
+ cohort.buildSMTTree();
80
+ },
81
+ getDistributePayload(cohort, participantDid) {
82
+ const proof = cohort.smtProofs?.get(participantDid);
83
+ return { smtProof: proof };
84
+ },
85
+ validateParticipantView({ participantDid, included, submittedUpdate, body }) {
86
+ const smtProof = body.smtProof;
87
+ const index = (0, import_smt.didToIndex)(participantDid);
88
+ if (!included) {
89
+ if (!smtProof?.nonce || smtProof?.updateId) return { matches: false, smtProof };
90
+ const candidateHash2 = (0, import_smt.blockHash)((0, import_smt.blockHash)((0, import_smt.base64UrlToHash)(smtProof.nonce)));
91
+ return { matches: (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash2), smtProof };
92
+ }
93
+ if (!smtProof?.updateId || !smtProof?.nonce || !submittedUpdate) return { matches: false, smtProof };
94
+ const canonicalBytes = new TextEncoder().encode((0, import_common.canonicalize)(submittedUpdate));
95
+ const expectedUpdateId = (0, import_smt.hashToBase64Url)((0, import_smt.blockHash)(canonicalBytes));
96
+ if (smtProof.updateId !== expectedUpdateId) {
97
+ return { matches: false, smtProof };
98
+ }
99
+ const candidateHash = (0, import_smt.blockHash)((0, import_smt.blockHash)((0, import_smt.base64UrlToHash)(smtProof.nonce)), (0, import_smt.base64UrlToHash)(smtProof.updateId));
100
+ return {
101
+ matches: (0, import_smt.verifySerializedProof)(smtProof, index, candidateHash),
102
+ smtProof
103
+ };
104
+ }
105
+ };
106
+ var STRATEGIES = /* @__PURE__ */ new Map([
107
+ [CAS_STRATEGY.type, CAS_STRATEGY],
108
+ [SMT_STRATEGY.type, SMT_STRATEGY]
109
+ ]);
110
+ function getBeaconStrategy(type) {
111
+ return STRATEGIES.get(type);
112
+ }
113
+
114
+ // src/core/cohort.ts
115
+ var import_bitcoin = require("@did-btcr2/bitcoin");
116
+ var import_common3 = require("@did-btcr2/common");
117
+ var import_smt2 = require("@did-btcr2/smt");
118
+ var import_secp256k12 = require("@noble/curves/secp256k1.js");
119
+ var import_utils2 = require("@noble/hashes/utils");
120
+ var import_btc_signer2 = require("@scure/btc-signer");
121
+ var import_musig22 = require("@scure/btc-signer/musig2");
122
+
123
+ // src/core/errors.ts
124
+ var import_common2 = require("@did-btcr2/common");
125
+ var AggregationServiceError = class extends import_common2.MethodError {
126
+ constructor(message, type = "AggregationServiceError", data) {
127
+ super(message, type, data);
128
+ }
129
+ };
130
+ var AggregationCohortError = class extends import_common2.MethodError {
131
+ constructor(message, type = "AggregationCohortError", data) {
132
+ super(message, type, data);
133
+ }
134
+ };
135
+ var SigningSessionError = class extends import_common2.MethodError {
136
+ constructor(message, type = "SigningSessionError", data) {
137
+ super(message, type, data);
138
+ }
139
+ };
140
+
141
+ // src/core/recovery-policy.ts
142
+ var import_secp256k1 = require("@noble/curves/secp256k1.js");
143
+ var import_utils = require("@noble/hashes/utils");
144
+ var import_btc_signer = require("@scure/btc-signer");
145
+ var import_musig2 = require("@scure/btc-signer/musig2");
146
+ var DEFAULT_FUNDING_MODEL = "operator-funded";
147
+ var MAX_RECOVERY_SEQUENCE = 65535;
148
+ var TAPROOT_LEAF_VERSION = 192;
149
+ function compactSize(n) {
150
+ if (n < 253) return new Uint8Array([n]);
151
+ if (n <= 65535) return new Uint8Array([253, n & 255, n >> 8 & 255]);
152
+ return new Uint8Array([254, n & 255, n >> 8 & 255, n >> 16 & 255, n >> 24 & 255]);
153
+ }
154
+ function tapLeafHash(script, leafVersion = TAPROOT_LEAF_VERSION) {
155
+ return import_secp256k1.schnorr.utils.taggedHash(
156
+ "TapLeaf",
157
+ (0, import_utils.concatBytes)(new Uint8Array([leafVersion]), compactSize(script.length), script)
158
+ );
159
+ }
160
+ function resolveFallbackThreshold(advertised, n) {
161
+ return advertised ?? Math.max(1, n - 1);
162
+ }
163
+ function assertFallbackParams({ cohortKeys, fallbackThreshold }) {
164
+ const n = cohortKeys.length;
165
+ if (n === 0) {
166
+ throw new AggregationCohortError(
167
+ "Cannot build fallback leaf: no cohort keys.",
168
+ "NO_COHORT_KEYS"
169
+ );
170
+ }
171
+ for (const key of cohortKeys) {
172
+ if (key.length !== 33) {
173
+ throw new AggregationCohortError(
174
+ `Cohort key must be a 33-byte compressed public key, got ${key.length} bytes.`,
175
+ "INVALID_COHORT_KEY",
176
+ { length: key.length }
177
+ );
178
+ }
179
+ }
180
+ if (!Number.isInteger(fallbackThreshold) || fallbackThreshold < 1 || fallbackThreshold > n) {
181
+ throw new AggregationCohortError(
182
+ `Fallback threshold must be an integer in [1, ${n}] (k-of-n), got ${fallbackThreshold}.`,
183
+ "INVALID_FALLBACK_THRESHOLD",
184
+ { fallbackThreshold, n }
185
+ );
186
+ }
187
+ }
188
+ function buildFallbackLeaf(params) {
189
+ assertFallbackParams(params);
190
+ const xOnlyKeys = (0, import_musig2.sortKeys)(params.cohortKeys).map((k) => k.slice(1));
191
+ return (0, import_btc_signer.p2tr_ms)(params.fallbackThreshold, xOnlyKeys).script;
192
+ }
193
+ function assertRecoveryParams({ recoveryKey, recoverySequence }) {
194
+ if (recoveryKey.length !== 32) {
195
+ throw new AggregationCohortError(
196
+ `Recovery key must be a 32-byte x-only public key, got ${recoveryKey.length} bytes.`,
197
+ "INVALID_RECOVERY_KEY"
198
+ );
199
+ }
200
+ if (!Number.isInteger(recoverySequence) || recoverySequence < 1 || recoverySequence > MAX_RECOVERY_SEQUENCE) {
201
+ throw new AggregationCohortError(
202
+ `Recovery sequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}], got ${recoverySequence}.`,
203
+ "INVALID_RECOVERY_SEQUENCE"
204
+ );
205
+ }
206
+ }
207
+ function buildRecoveryScript(params) {
208
+ assertRecoveryParams(params);
209
+ return import_btc_signer.Script.encode([
210
+ params.recoverySequence,
211
+ "CHECKSEQUENCEVERIFY",
212
+ "DROP",
213
+ params.recoveryKey,
214
+ "CHECKSIG"
215
+ ]);
216
+ }
217
+ function buildRecoveryLeaves(fundingModel, params) {
218
+ switch (fundingModel) {
219
+ case "operator-funded":
220
+ return [
221
+ { script: buildFallbackLeaf(params) },
222
+ { script: buildRecoveryScript(params) }
223
+ ];
224
+ case "participant-funded":
225
+ throw new AggregationCohortError(
226
+ "Funding model 'participant-funded' is reserved and not yet implemented.",
227
+ "UNSUPPORTED_FUNDING_MODEL",
228
+ { fundingModel }
229
+ );
230
+ default:
231
+ throw new AggregationCohortError(
232
+ `Unknown funding model: ${fundingModel}.`,
233
+ "UNKNOWN_FUNDING_MODEL",
234
+ { fundingModel }
235
+ );
236
+ }
237
+ }
238
+
239
+ // src/core/cohort.ts
240
+ var AggregationCohort = class {
241
+ /** Unique identifier for the cohort. */
242
+ id;
243
+ /** DID of the Aggregation Service managing this cohort. */
244
+ serviceDid;
245
+ /** Minimum number of participants required to finalize the cohort. */
246
+ minParticipants;
247
+ /** Network on which the cohort operates (mainnet, mutinynet, etc.). */
248
+ network;
249
+ /** Type of beacon used in the cohort: 'CASBeacon' or 'SMTBeacon'. */
250
+ beaconType;
251
+ /** List of participant DIDs that have been accepted into the cohort. */
252
+ participants = [];
253
+ /**
254
+ * Mapping from participant DID to their compressed secp256k1 public key.
255
+ * Distinct from {@link cohortKeys} (which is sorted per BIP-327): this lets
256
+ * callers look up a participant's key without knowing their position in the
257
+ * sorted array. Populated by the service at `acceptParticipant` time.
258
+ */
259
+ participantKeys = /* @__PURE__ */ new Map();
260
+ /** Sorted list of cohort participants' compressed public keys. */
261
+ #cohortKeys = [];
262
+ /**
263
+ * BIP-341 TapTweak scalar: `taggedHash("TapTweak", internalPubkey || tapMerkleRoot)`.
264
+ * The beacon output is an internal key (the MuSig2 aggregate) plus a script
265
+ * tree (the recovery leaves), so the tweak commits to the tree's Merkle root.
266
+ * The MuSig2 signing session applies this as an x-only tweak; a value that does
267
+ * not match the root the funded address was derived from silently yields an
268
+ * invalid key-path signature.
269
+ */
270
+ tapTweak = new Uint8Array();
271
+ /** The n-of-n MuSig2 aggregate internal key, x-only (32 bytes), set by computeBeaconAddress(). */
272
+ internalKey = new Uint8Array();
273
+ /** BIP-341 Taproot Merkle root of the recovery script tree, set by computeBeaconAddress(). */
274
+ tapMerkleRoot = new Uint8Array();
275
+ /** Operator recovery key, x-only (32 bytes). Used to build the CSV recovery leaf. */
276
+ recoveryKey;
277
+ /** Relative-timelock (BIP-68 nSequence) before the recovery leaf is spendable. */
278
+ recoverySequence;
279
+ /** Funding model governing the recovery leaves (default 'operator-funded'). */
280
+ fundingModel;
281
+ /**
282
+ * Advertised k of the k-of-n fallback leaf, or undefined to default to n-1 at
283
+ * address computation. Read the resolved value via {@link effectiveFallbackThreshold}.
284
+ */
285
+ fallbackThreshold;
286
+ /** The Taproot beacon address: key path is the MuSig2 aggregate, script path is fallback + recovery. */
287
+ beaconAddress = "";
288
+ /** Pending DID updates submitted by participants, keyed by DID. */
289
+ pendingUpdates = /* @__PURE__ */ new Map();
290
+ /**
291
+ * Participant DIDs that explicitly declined to submit an update this round
292
+ * (cooperative non-inclusion). A decliner is absent from the CAS Announcement
293
+ * Map and carries a non-inclusion leaf in the SMT, yet still signs. Kept
294
+ * disjoint from {@link pendingUpdates} so CAS correctness holds by construction.
295
+ */
296
+ nonIncluded = /* @__PURE__ */ new Set();
297
+ /** CAS Beacon Announcement Map (DID to updateHash), set by buildCASAnnouncement(). */
298
+ casAnnouncement;
299
+ /** Per-participant SMT proofs, set by buildSMTTree(). */
300
+ smtProofs;
301
+ /** Signal bytes (32 bytes) for OP_RETURN: SHA-256 of CAS announcement OR SMT root. */
302
+ signalBytes;
303
+ /** Set of participant DIDs that have approved the aggregated data. */
304
+ validationAcks = /* @__PURE__ */ new Set();
305
+ /** Set of participant DIDs that have rejected the aggregated data. */
306
+ validationRejections = /* @__PURE__ */ new Set();
307
+ constructor({ id, minParticipants, serviceDid, network, beaconType, recoveryKey, recoverySequence, fundingModel, fallbackThreshold }) {
308
+ this.id = id || crypto.randomUUID();
309
+ this.minParticipants = minParticipants ?? 2;
310
+ this.serviceDid = serviceDid || "";
311
+ this.network = network;
312
+ this.beaconType = beaconType || "CASBeacon";
313
+ this.recoveryKey = recoveryKey;
314
+ this.recoverySequence = recoverySequence;
315
+ this.fundingModel = fundingModel ?? DEFAULT_FUNDING_MODEL;
316
+ this.fallbackThreshold = fallbackThreshold;
317
+ }
318
+ /** Sorted cohort keys (sorted on assignment per BIP-327). */
319
+ get cohortKeys() {
320
+ return this.#cohortKeys;
321
+ }
322
+ set cohortKeys(keys) {
323
+ this.#cohortKeys = (0, import_musig22.sortKeys)(keys);
324
+ }
325
+ /**
326
+ * The resolved k of the k-of-n fallback leaf: the advertised
327
+ * {@link fallbackThreshold}, or n-1 (floored at 1) when unadvertised, where n is
328
+ * the current cohort size. This is the value the beacon address commits to and
329
+ * the spend builders must reproduce. Returns 0 before any cohort keys are set.
330
+ */
331
+ get effectiveFallbackThreshold() {
332
+ if (this.#cohortKeys.length === 0) return 0;
333
+ return resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length);
334
+ }
335
+ /**
336
+ * Computes the Taproot beacon address from the cohort keys and recovery params.
337
+ *
338
+ * The output's key path is the n-of-n MuSig2 aggregate of the cohort keys; its
339
+ * script path is the recovery tree (a CSV recovery leaf so a missing signer
340
+ * cannot permanently lock the UTXO). Sets `internalKey`, `tapMerkleRoot`, and
341
+ * the `tapTweak` the MuSig2 session must apply for the key-path spend.
342
+ *
343
+ * The tweak is derived from the Merkle root the address was built with (read
344
+ * back from the payment), never recomputed by hand, so the MuSig2 key-path
345
+ * signature is guaranteed to validate against the funded address.
346
+ */
347
+ computeBeaconAddress() {
348
+ if (this.#cohortKeys.length === 0) {
349
+ throw new AggregationCohortError(
350
+ "Cannot compute beacon address: no cohort keys.",
351
+ "NO_COHORT_KEYS",
352
+ { cohortId: this.id }
353
+ );
354
+ }
355
+ if (!this.recoveryKey || this.recoveryKey.length === 0 || this.recoverySequence === void 0) {
356
+ throw new AggregationCohortError(
357
+ "Cannot compute beacon address: missing recovery key or sequence.",
358
+ "NO_RECOVERY_PARAMS",
359
+ { cohortId: this.id }
360
+ );
361
+ }
362
+ const keyAggContext = (0, import_musig22.keyAggregate)(this.#cohortKeys);
363
+ const aggPubkey = (0, import_musig22.keyAggExport)(keyAggContext);
364
+ const leaves = buildRecoveryLeaves(this.fundingModel, {
365
+ recoveryKey: this.recoveryKey,
366
+ recoverySequence: this.recoverySequence,
367
+ cohortKeys: this.#cohortKeys,
368
+ fallbackThreshold: resolveFallbackThreshold(this.fallbackThreshold, this.#cohortKeys.length)
369
+ });
370
+ const payment = (0, import_btc_signer2.p2tr)(aggPubkey, leaves, (0, import_bitcoin.getNetwork)(this.network), true);
371
+ this.internalKey = aggPubkey;
372
+ this.tapMerkleRoot = payment.tapMerkleRoot;
373
+ this.tapTweak = import_secp256k12.schnorr.utils.taggedHash("TapTweak", (0, import_utils2.concatBytes)(aggPubkey, payment.tapMerkleRoot));
374
+ if (!payment.address) {
375
+ throw new AggregationCohortError(
376
+ "Failed to compute Taproot address",
377
+ "BEACON_ADDRESS_ERROR",
378
+ { cohortId: this.id }
379
+ );
380
+ }
381
+ this.beaconAddress = payment.address;
382
+ return payment.address;
383
+ }
384
+ /**
385
+ * Validates that the participant's key is in the cohort and the beacon address
386
+ * matches the locally-computed one. Used by participants to verify cohort ready
387
+ * messages from the service.
388
+ */
389
+ validateMembership(participantPkHex, cohortKeysHex, expectedBeaconAddress) {
390
+ if (!cohortKeysHex.includes(participantPkHex)) {
391
+ throw new AggregationCohortError(
392
+ `Participant key not found in cohort ${this.id}.`,
393
+ "COHORT_VALIDATION_ERROR",
394
+ { cohortId: this.id, participantPkHex }
395
+ );
396
+ }
397
+ this.cohortKeys = cohortKeysHex.map((k) => (0, import_utils2.hexToBytes)(k));
398
+ const computed = this.computeBeaconAddress();
399
+ if (computed !== expectedBeaconAddress) {
400
+ throw new AggregationCohortError(
401
+ `Computed beacon address ${computed} does not match expected ${expectedBeaconAddress}.`,
402
+ "BEACON_ADDRESS_MISMATCH",
403
+ { cohortId: this.id, computed, expected: expectedBeaconAddress }
404
+ );
405
+ }
406
+ }
407
+ /**
408
+ * Returns the position of a participant's public key in the sorted
409
+ * {@link cohortKeys} array, or -1 if the participant is not in the cohort.
410
+ * Required by MuSig2 partial-sig verification which indexes by signer position.
411
+ */
412
+ indexOfParticipant(did) {
413
+ const pk = this.participantKeys.get(did);
414
+ if (!pk) return -1;
415
+ return this.#cohortKeys.findIndex(
416
+ (k) => k.length === pk.length && k.every((b, i) => b === pk[i])
417
+ );
418
+ }
419
+ addUpdate(participantDid, signedUpdate) {
420
+ if (!this.participants.includes(participantDid)) {
421
+ throw new AggregationCohortError(
422
+ `Participant ${participantDid} is not in cohort ${this.id}.`,
423
+ "UNKNOWN_PARTICIPANT",
424
+ { cohortId: this.id, participantDid }
425
+ );
426
+ }
427
+ this.pendingUpdates.set(participantDid, signedUpdate);
428
+ }
429
+ /**
430
+ * Record that a participant declined to submit an update this round
431
+ * (cooperative non-inclusion). The member stays in the cohort and still signs.
432
+ */
433
+ addNonInclusion(participantDid) {
434
+ if (!this.participants.includes(participantDid)) {
435
+ throw new AggregationCohortError(
436
+ `Participant ${participantDid} is not in cohort ${this.id}.`,
437
+ "UNKNOWN_PARTICIPANT",
438
+ { cohortId: this.id, participantDid }
439
+ );
440
+ }
441
+ if (this.pendingUpdates.has(participantDid)) {
442
+ throw new AggregationCohortError(
443
+ `Participant ${participantDid} already submitted an update; cannot also decline.`,
444
+ "CONFLICTING_RESPONSE",
445
+ { cohortId: this.id, participantDid }
446
+ );
447
+ }
448
+ this.nonIncluded.add(participantDid);
449
+ }
450
+ hasAllUpdates() {
451
+ return this.pendingUpdates.size === this.participants.length;
452
+ }
453
+ /**
454
+ * True when every participant has responded for this round, either with an
455
+ * update or with an explicit non-inclusion. This is the aggregation gate when
456
+ * non-inclusion is in play; it generalizes {@link hasAllUpdates} the same way
457
+ * {@link hasAllValidationResponses} generalizes a unanimous ack.
458
+ */
459
+ hasAllResponses() {
460
+ return this.pendingUpdates.size + this.nonIncluded.size === this.participants.length;
461
+ }
462
+ /**
463
+ * Builds a CAS Announcement Map from collected updates.
464
+ * Maps each participant DID to base64url canonical hash of their signed update.
465
+ * Computes signal bytes as SHA-256 of canonicalized announcement.
466
+ *
467
+ * Members who declined (cooperative non-inclusion) are naturally absent from
468
+ * the map: the body iterates {@link pendingUpdates}, which never holds a
469
+ * decliner. Absence from the map is exactly the CAS non-inclusion signal.
470
+ */
471
+ buildCASAnnouncement() {
472
+ if (!this.hasAllResponses()) {
473
+ throw new AggregationCohortError(
474
+ "Cannot build CAS Announcement: not all participants have responded.",
475
+ "INCOMPLETE_RESPONSES",
476
+ { cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
477
+ );
478
+ }
479
+ const announcement = {};
480
+ for (const [did, signedUpdate] of this.pendingUpdates) {
481
+ announcement[did] = (0, import_common3.canonicalHash)(signedUpdate);
482
+ }
483
+ this.casAnnouncement = announcement;
484
+ this.signalBytes = (0, import_common3.hash)((0, import_common3.canonicalize)(announcement));
485
+ return announcement;
486
+ }
487
+ /**
488
+ * Builds an SMT tree with one leaf per participant.
489
+ *
490
+ * A member who submitted an update gets an inclusion leaf
491
+ * (SHA-256(SHA-256(nonce) || SHA-256(update))); a member who declined gets a
492
+ * non-inclusion leaf (SHA-256(SHA-256(nonce)), the `signedUpdate` entry field
493
+ * omitted). The cohort mints each member's nonce and returns it inside that
494
+ * member's serialized proof, so a decliner can self-validate its own
495
+ * non-inclusion slot and the resolver can recompute the leaf. Stores
496
+ * per-participant proofs and the SMT root as signalBytes.
497
+ */
498
+ buildSMTTree() {
499
+ if (!this.hasAllResponses()) {
500
+ throw new AggregationCohortError(
501
+ "Cannot build SMT tree: not all participants have responded.",
502
+ "INCOMPLETE_RESPONSES",
503
+ { cohortId: this.id, updates: this.pendingUpdates.size, declined: this.nonIncluded.size, total: this.participants.length }
504
+ );
505
+ }
506
+ const tree = new import_smt2.BTCR2MerkleTree();
507
+ const entries = [];
508
+ const encoder = new TextEncoder();
509
+ for (const did of this.participants) {
510
+ const nonce = (0, import_utils2.randomBytes)(32);
511
+ const signedUpdate = this.pendingUpdates.get(did);
512
+ if (signedUpdate) {
513
+ entries.push({ did, nonce, signedUpdate: encoder.encode((0, import_common3.canonicalize)(signedUpdate)) });
514
+ } else {
515
+ entries.push({ did, nonce });
516
+ }
517
+ }
518
+ tree.addEntries(entries);
519
+ tree.finalize();
520
+ this.signalBytes = tree.rootHash;
521
+ this.smtProofs = /* @__PURE__ */ new Map();
522
+ for (const did of this.participants) {
523
+ this.smtProofs.set(did, tree.proof(did));
524
+ }
525
+ return this.smtProofs;
526
+ }
527
+ addValidation(participantDid, approved) {
528
+ if (!this.participants.includes(participantDid)) {
529
+ throw new AggregationCohortError(
530
+ `Unknown participant ${participantDid} in cohort ${this.id}.`,
531
+ "UNKNOWN_PARTICIPANT",
532
+ { cohortId: this.id, participantDid }
533
+ );
534
+ }
535
+ if (approved) {
536
+ this.validationAcks.add(participantDid);
537
+ } else {
538
+ this.validationRejections.add(participantDid);
539
+ }
540
+ }
541
+ /**
542
+ * True when every participant has either approved or rejected the aggregated data.
543
+ */
544
+ hasAllValidationResponses() {
545
+ return this.validationAcks.size + this.validationRejections.size === this.participants.length;
546
+ }
547
+ /**
548
+ * True when all participants approved. Note: differs from {@link hasAllValidationResponses},
549
+ * this returns false if any participant rejected, even if all responses are in.
550
+ */
551
+ isFullyValidated() {
552
+ return this.validationRejections.size === 0 && this.validationAcks.size === this.participants.length;
553
+ }
554
+ };
555
+
556
+ // src/core/conditions.ts
557
+ var KNOWN_BEACON_TYPES = ["CASBeacon", "SMTBeacon"];
558
+ var KNOWN_FUNDING_MODELS = ["operator-funded", "participant-funded"];
559
+ function checkPair(problems, label, min, max) {
560
+ if (min !== void 0 && (!Number.isInteger(min) || min < 0)) {
561
+ problems.push(`min${label} must be an integer >= 0`);
562
+ }
563
+ if (max !== void 0 && (!Number.isInteger(max) || max < 0)) {
564
+ problems.push(`max${label} must be an integer >= 0`);
565
+ }
566
+ if (min !== void 0 && max !== void 0 && Number.isInteger(min) && Number.isInteger(max) && max < min) {
567
+ problems.push(`max${label} must be >= min${label}`);
568
+ }
569
+ }
570
+ function checkCost(problems, label, cost) {
571
+ if (cost === void 0) return;
572
+ if (typeof cost.amount !== "number" || !Number.isFinite(cost.amount) || cost.amount < 0) {
573
+ problems.push(`${label}.amount must be a finite number >= 0`);
574
+ }
575
+ if (typeof cost.unit !== "string" || cost.unit.length === 0) {
576
+ problems.push(`${label}.unit must be a non-empty string`);
577
+ }
578
+ if (cost.basis !== void 0 && cost.basis !== "per-did" && cost.basis !== "per-participant") {
579
+ problems.push(`${label}.basis must be 'per-did' or 'per-participant'`);
580
+ }
581
+ }
582
+ function validateCohortConditions(c) {
583
+ const problems = [];
584
+ if (!KNOWN_BEACON_TYPES.includes(c.beaconType)) {
585
+ problems.push(`beaconType must be one of ${KNOWN_BEACON_TYPES.join(", ")}`);
586
+ }
587
+ if (!Number.isInteger(c.minParticipants) || c.minParticipants < 1) {
588
+ problems.push("minParticipants must be an integer >= 1");
589
+ }
590
+ if (c.maxParticipants !== void 0) {
591
+ if (!Number.isInteger(c.maxParticipants) || c.maxParticipants < 1) {
592
+ problems.push("maxParticipants must be an integer >= 1");
593
+ } else if (Number.isInteger(c.minParticipants) && c.maxParticipants < c.minParticipants) {
594
+ problems.push("maxParticipants must be >= minParticipants");
595
+ }
596
+ }
597
+ checkPair(problems, "DidsPerParticipant", c.minDidsPerParticipant, c.maxDidsPerParticipant);
598
+ checkPair(problems, "SecondsBetweenAnnouncements", c.minSecondsBetweenAnnouncements, c.maxSecondsBetweenAnnouncements);
599
+ if (c.pendingUpdateTrigger !== void 0 && (!Number.isInteger(c.pendingUpdateTrigger) || c.pendingUpdateTrigger < 1)) {
600
+ problems.push("pendingUpdateTrigger must be an integer >= 1");
601
+ }
602
+ if (c.fallbackThreshold !== void 0) {
603
+ if (!Number.isInteger(c.fallbackThreshold) || c.fallbackThreshold < 1) {
604
+ problems.push("fallbackThreshold must be an integer >= 1");
605
+ } else if (c.maxParticipants !== void 0 && Number.isInteger(c.maxParticipants) && c.fallbackThreshold > c.maxParticipants) {
606
+ problems.push("fallbackThreshold must be <= maxParticipants");
607
+ }
608
+ }
609
+ checkCost(problems, "costOfEnrollment", c.costOfEnrollment);
610
+ checkCost(problems, "costPerAnnouncement", c.costPerAnnouncement);
611
+ if (typeof c.recoveryKey !== "string" || !/^[0-9a-fA-F]{64}$/.test(c.recoveryKey)) {
612
+ problems.push("recoveryKey must be a 64-character hex string (x-only public key)");
613
+ }
614
+ if (!Number.isInteger(c.recoverySequence) || c.recoverySequence < 1 || c.recoverySequence > MAX_RECOVERY_SEQUENCE) {
615
+ problems.push(`recoverySequence must be a block-based BIP-68 relative timelock in [1, ${MAX_RECOVERY_SEQUENCE}]`);
616
+ }
617
+ if (c.fundingModel !== void 0 && !KNOWN_FUNDING_MODELS.includes(c.fundingModel)) {
618
+ problems.push(`fundingModel must be one of ${KNOWN_FUNDING_MODELS.join(", ")}`);
619
+ }
620
+ return problems;
621
+ }
622
+
623
+ // src/core/fallback-spend.ts
624
+ var import_bitcoin2 = require("@did-btcr2/bitcoin");
625
+ var import_secp256k13 = require("@noble/curves/secp256k1.js");
626
+ var import_btc_signer3 = require("@scure/btc-signer");
627
+ var import_musig23 = require("@scure/btc-signer/musig2");
628
+ function bytesEqual(a, b) {
629
+ return a.length === b.length && a.every((x, i) => x === b[i]);
630
+ }
631
+ function fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeafScript) {
632
+ return tx.preimageWitnessV1(
633
+ inputIndex,
634
+ [prevOutScript],
635
+ import_btc_signer3.SigHash.DEFAULT,
636
+ [prevOutValue],
637
+ void 0,
638
+ fallbackLeafScript,
639
+ TAPROOT_LEAF_VERSION
640
+ );
641
+ }
642
+ function buildFallbackSpend(params) {
643
+ const {
644
+ pendingTx,
645
+ cohortKeys,
646
+ recoveryKey,
647
+ recoverySequence,
648
+ fundingModel,
649
+ network,
650
+ prevOutScript,
651
+ prevOutValue,
652
+ signatures
653
+ } = params;
654
+ const inputIndex = params.inputIndex ?? 0;
655
+ if (cohortKeys.length === 0) {
656
+ throw new AggregationCohortError(
657
+ "Cannot build fallback spend: no cohort keys.",
658
+ "NO_COHORT_KEYS"
659
+ );
660
+ }
661
+ const fallbackThreshold = resolveFallbackThreshold(params.fallbackThreshold, cohortKeys.length);
662
+ const internalKey = (0, import_musig23.keyAggExport)((0, import_musig23.keyAggregate)((0, import_musig23.sortKeys)(cohortKeys)));
663
+ const leaves = buildRecoveryLeaves(fundingModel ?? DEFAULT_FUNDING_MODEL, {
664
+ recoveryKey,
665
+ recoverySequence,
666
+ cohortKeys,
667
+ fallbackThreshold
668
+ });
669
+ const net = (0, import_bitcoin2.getNetwork)(network);
670
+ const payment = (0, import_btc_signer3.p2tr)(internalKey, leaves, net, true);
671
+ if (!bytesEqual(payment.script, prevOutScript)) {
672
+ throw new AggregationCohortError(
673
+ "Reconstructed beacon output script does not match the spent prevout script.",
674
+ "PREVOUT_SCRIPT_MISMATCH"
675
+ );
676
+ }
677
+ const fallbackLeaf = buildFallbackLeaf({ cohortKeys, fallbackThreshold });
678
+ const leafEntry = payment.tapLeafScript?.find(([, scriptVer]) => {
679
+ const script = scriptVer.slice(0, scriptVer.length - 1);
680
+ return bytesEqual(script, fallbackLeaf);
681
+ });
682
+ if (!leafEntry) {
683
+ throw new AggregationCohortError(
684
+ "Could not locate the fallback leaf in the reconstructed beacon output.",
685
+ "FALLBACK_LEAF_NOT_FOUND"
686
+ );
687
+ }
688
+ const cohortXOnly = (0, import_musig23.sortKeys)(cohortKeys).map((k) => k.slice(1));
689
+ const tx = pendingTx;
690
+ const sighash = fallbackSighash(tx, inputIndex, prevOutScript, prevOutValue, fallbackLeaf);
691
+ const leafHash = tapLeafHash(fallbackLeaf);
692
+ const accepted = /* @__PURE__ */ new Map();
693
+ for (const { pubKey, signature } of signatures) {
694
+ if (pubKey.length !== 32 || signature.length !== 64) continue;
695
+ const isCohortKey = cohortXOnly.some((k) => bytesEqual(k, pubKey));
696
+ if (!isCohortKey) continue;
697
+ const hex = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
698
+ if (accepted.has(hex)) continue;
699
+ let ok = false;
700
+ try {
701
+ ok = import_secp256k13.schnorr.verify(signature, sighash, pubKey);
702
+ } catch {
703
+ ok = false;
704
+ }
705
+ if (ok) accepted.set(hex, signature);
706
+ }
707
+ if (accepted.size < fallbackThreshold) {
708
+ throw new AggregationCohortError(
709
+ `Not enough valid fallback signatures: have ${accepted.size}, need ${fallbackThreshold}.`,
710
+ "NOT_ENOUGH_FALLBACK_SIGNATURES",
711
+ { have: accepted.size, need: fallbackThreshold }
712
+ );
713
+ }
714
+ const chosen = cohortXOnly.filter((k) => accepted.has(Array.from(k, (b) => b.toString(16).padStart(2, "0")).join(""))).slice(0, fallbackThreshold);
715
+ tx.updateInput(inputIndex, {
716
+ tapLeafScript: [leafEntry],
717
+ tapScriptSig: chosen.map((pubKey) => {
718
+ const hex = Array.from(pubKey, (b) => b.toString(16).padStart(2, "0")).join("");
719
+ return [{ pubKey, leafHash }, accepted.get(hex)];
720
+ })
721
+ });
722
+ tx.finalize();
723
+ return tx;
724
+ }
725
+
726
+ // src/core/messages/base.ts
727
+ var AGGREGATION_WIRE_VERSION = 1;
728
+ var BaseMessage = class {
729
+ type;
730
+ version;
731
+ to;
732
+ from;
733
+ body;
734
+ constructor({ type, version, to, from, body }) {
735
+ this.type = type;
736
+ this.version = version ?? AGGREGATION_WIRE_VERSION;
737
+ this.to = to;
738
+ this.from = from;
739
+ this.body = body;
740
+ }
741
+ /**
742
+ * Converts a BaseMessage to a JSON object.
743
+ * @returns {Base} The JSON representation of the BaseMessage.
744
+ */
745
+ toJSON() {
746
+ return {
747
+ type: this.type,
748
+ version: this.version,
749
+ to: this.to,
750
+ from: this.from,
751
+ body: this.body
752
+ };
753
+ }
754
+ };
755
+
756
+ // src/core/messages/constants.ts
757
+ var AGGREGATION_MESSAGE_PREFIX = "https://btcr2.dev/aggregation";
758
+ var COHORT_ADVERT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_advert`;
759
+ var COHORT_OPT_IN = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in`;
760
+ var COHORT_OPT_IN_ACCEPT = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_opt_in_accept`;
761
+ var COHORT_READY = `${AGGREGATION_MESSAGE_PREFIX}/keygen/cohort_ready`;
762
+ var SUBMIT_UPDATE = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_update`;
763
+ var SUBMIT_NONINCLUDED = `${AGGREGATION_MESSAGE_PREFIX}/update/submit_nonincluded`;
764
+ var DISTRIBUTE_AGGREGATED_DATA = `${AGGREGATION_MESSAGE_PREFIX}/update/distribute_aggregated_data`;
765
+ var VALIDATION_ACK = `${AGGREGATION_MESSAGE_PREFIX}/update/validation_ack`;
766
+ var AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/authorization_request`;
767
+ var NONCE_CONTRIBUTION = `${AGGREGATION_MESSAGE_PREFIX}/sign/nonce_contribution`;
768
+ var AGGREGATED_NONCE = `${AGGREGATION_MESSAGE_PREFIX}/sign/aggregated_nonce`;
769
+ var SIGNATURE_AUTHORIZATION = `${AGGREGATION_MESSAGE_PREFIX}/sign/signature_authorization`;
770
+ var FALLBACK_AUTHORIZATION_REQUEST = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_authorization_request`;
771
+ var FALLBACK_SIGNATURE = `${AGGREGATION_MESSAGE_PREFIX}/sign/fallback_signature`;
772
+
773
+ // src/core/messages/factories.ts
774
+ function createCohortAdvertMessage(fields) {
775
+ const { from, ...body } = fields;
776
+ return new BaseMessage({ type: COHORT_ADVERT, from, body });
777
+ }
778
+ function createCohortOptInAcceptMessage(fields) {
779
+ const { from, to, ...body } = fields;
780
+ return new BaseMessage({ type: COHORT_OPT_IN_ACCEPT, from, to, body });
781
+ }
782
+ function createCohortReadyMessage(fields) {
783
+ const { from, to, ...body } = fields;
784
+ return new BaseMessage({ type: COHORT_READY, from, to, body });
785
+ }
786
+ function createDistributeAggregatedDataMessage(fields) {
787
+ const { from, to, ...body } = fields;
788
+ return new BaseMessage({ type: DISTRIBUTE_AGGREGATED_DATA, from, to, body });
789
+ }
790
+ function createAuthorizationRequestMessage(fields) {
791
+ const { from, to, ...body } = fields;
792
+ return new BaseMessage({ type: AUTHORIZATION_REQUEST, from, to, body });
793
+ }
794
+ function createAggregatedNonceMessage(fields) {
795
+ const { from, to, ...body } = fields;
796
+ return new BaseMessage({ type: AGGREGATED_NONCE, from, to, body });
797
+ }
798
+ function createFallbackAuthorizationRequestMessage(fields) {
799
+ const { from, to, ...body } = fields;
800
+ return new BaseMessage({ type: FALLBACK_AUTHORIZATION_REQUEST, from, to, body });
801
+ }
802
+
803
+ // src/core/signing-session.ts
804
+ var import_keypair = require("@did-btcr2/keypair");
805
+ var import_btc_signer4 = require("@scure/btc-signer");
806
+ var musig2 = __toESM(require("@scure/btc-signer/musig2"), 1);
807
+ var BeaconSigningSession = class {
808
+ /** Unique identifier for this signing session. */
809
+ id;
810
+ /** The cohort this session signs for. */
811
+ cohort;
812
+ /** The Bitcoin transaction being signed. */
813
+ pendingTx;
814
+ /** Previous output scripts for Taproot sighash computation (BIP-341). */
815
+ prevOutScripts;
816
+ /** Previous output values for Taproot sighash computation. */
817
+ prevOutValues;
818
+ /** Map of participant publicKey-hex to public nonce contribution. */
819
+ nonceContributions = /* @__PURE__ */ new Map();
820
+ /** Aggregated MuSig2 nonce (66 bytes). */
821
+ aggregatedNonce;
822
+ /** Map of participant DID to partial signature. */
823
+ partialSignatures = /* @__PURE__ */ new Map();
824
+ /** Final 64-byte Schnorr signature. */
825
+ signature;
826
+ /** Current signing session phase. */
827
+ phase;
828
+ /**
829
+ * Participant's MuSig2 secret nonce, held only on the participant side
830
+ * between {@link generateNonceContribution} and {@link generatePartialSignature}.
831
+ * Private and cleared on every terminal path (success, failure, or teardown
832
+ * via {@link clearSecrets}) so a spent or abandoned nonce cannot be reused or
833
+ * serialized.
834
+ */
835
+ #secretNonce;
836
+ constructor({ id, cohort, pendingTx, prevOutScripts, prevOutValues }) {
837
+ this.id = id || crypto.randomUUID();
838
+ this.cohort = cohort;
839
+ this.pendingTx = pendingTx;
840
+ this.prevOutScripts = prevOutScripts || [];
841
+ this.prevOutValues = prevOutValues || [];
842
+ this.phase = "AwaitingNonceContributions" /* AwaitingNonceContributions */;
843
+ }
844
+ /**
845
+ * Computes the Taproot sighash (BIP-341) for the first input.
846
+ */
847
+ get sigHash() {
848
+ if (!this.prevOutScripts.length || !this.prevOutValues.length) {
849
+ throw new SigningSessionError(
850
+ "Cannot compute sighash: missing prevOutScripts or prevOutValues.",
851
+ "SIGHASH_ERROR"
852
+ );
853
+ }
854
+ return this.pendingTx.preimageWitnessV1(
855
+ 0,
856
+ this.prevOutScripts,
857
+ import_btc_signer4.SigHash.DEFAULT,
858
+ this.prevOutValues
859
+ );
860
+ }
861
+ addNonceContribution(participantDid, nonceContribution) {
862
+ if (this.phase !== "AwaitingNonceContributions" /* AwaitingNonceContributions */) {
863
+ throw new SigningSessionError(
864
+ `Nonce contributions not expected. Current phase: ${this.phase}`,
865
+ "INVALID_PHASE",
866
+ { phase: this.phase }
867
+ );
868
+ }
869
+ if (!this.cohort.participants.includes(participantDid)) {
870
+ throw new SigningSessionError(
871
+ `Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
872
+ "UNKNOWN_PARTICIPANT",
873
+ { cohortId: this.cohort.id, participantDid }
874
+ );
875
+ }
876
+ if (nonceContribution.length !== 66) {
877
+ throw new SigningSessionError(
878
+ `Invalid nonce contribution: expected 66 bytes, got ${nonceContribution.length}.`,
879
+ "INVALID_NONCE_LENGTH"
880
+ );
881
+ }
882
+ if (this.nonceContributions.has(participantDid)) {
883
+ throw new SigningSessionError(
884
+ `Duplicate nonce contribution from ${participantDid}.`,
885
+ "DUPLICATE_NONCE"
886
+ );
887
+ }
888
+ this.nonceContributions.set(participantDid, nonceContribution);
889
+ if (this.nonceContributions.size === this.cohort.participants.length) {
890
+ this.phase = "NonceContributionsReceived" /* NonceContributionsReceived */;
891
+ }
892
+ }
893
+ generateAggregatedNonce() {
894
+ if (this.phase !== "NonceContributionsReceived" /* NonceContributionsReceived */) {
895
+ throw new SigningSessionError(
896
+ `Cannot aggregate nonces: phase is ${this.phase}, expected NonceContributionsReceived.`,
897
+ "INVALID_PHASE"
898
+ );
899
+ }
900
+ this.aggregatedNonce = musig2.nonceAggregate([...this.nonceContributions.values()]);
901
+ this.phase = "AwaitingPartialSignatures" /* AwaitingPartialSignatures */;
902
+ return this.aggregatedNonce;
903
+ }
904
+ addPartialSignature(participantDid, partialSig) {
905
+ if (this.phase !== "AwaitingPartialSignatures" /* AwaitingPartialSignatures */) {
906
+ throw new SigningSessionError(
907
+ `Partial signatures not expected. Current phase: ${this.phase}`,
908
+ "INVALID_PHASE"
909
+ );
910
+ }
911
+ if (!this.cohort.participants.includes(participantDid)) {
912
+ throw new SigningSessionError(
913
+ `Participant ${participantDid} is not a member of cohort ${this.cohort.id}.`,
914
+ "UNKNOWN_PARTICIPANT",
915
+ { cohortId: this.cohort.id, participantDid }
916
+ );
917
+ }
918
+ if (this.partialSignatures.has(participantDid)) {
919
+ throw new SigningSessionError(
920
+ `Duplicate partial signature from ${participantDid}.`,
921
+ "DUPLICATE_PARTIAL_SIG"
922
+ );
923
+ }
924
+ this.partialSignatures.set(participantDid, partialSig);
925
+ if (this.partialSignatures.size === this.cohort.participants.length) {
926
+ this.phase = "PartialSignaturesReceived" /* PartialSignaturesReceived */;
927
+ }
928
+ }
929
+ generateFinalSignature() {
930
+ if (this.phase !== "PartialSignaturesReceived" /* PartialSignaturesReceived */) {
931
+ throw new SigningSessionError(
932
+ `Cannot generate final signature: phase is ${this.phase}.`,
933
+ "INVALID_PHASE"
934
+ );
935
+ }
936
+ if (!this.aggregatedNonce) {
937
+ throw new SigningSessionError("Aggregated nonce missing.", "MISSING_AGGREGATED_NONCE");
938
+ }
939
+ const session = new musig2.Session(
940
+ this.aggregatedNonce,
941
+ this.cohort.cohortKeys,
942
+ this.sigHash,
943
+ [this.cohort.tapTweak],
944
+ [true]
945
+ );
946
+ const pubNoncesByIndex = new Array(this.cohort.cohortKeys.length);
947
+ for (const [did, nonce] of this.nonceContributions) {
948
+ const idx = this.cohort.indexOfParticipant(did);
949
+ if (idx < 0) {
950
+ throw new SigningSessionError(
951
+ `Cannot verify nonce from ${did}: participant key missing from cohort.`,
952
+ "UNKNOWN_PARTICIPANT_KEY",
953
+ { participantDid: did }
954
+ );
955
+ }
956
+ pubNoncesByIndex[idx] = nonce;
957
+ }
958
+ for (const [did, partialSig] of this.partialSignatures) {
959
+ const idx = this.cohort.indexOfParticipant(did);
960
+ if (idx < 0) {
961
+ throw new SigningSessionError(
962
+ `Cannot verify partial signature from ${did}: participant key missing from cohort.`,
963
+ "UNKNOWN_PARTICIPANT_KEY",
964
+ { participantDid: did }
965
+ );
966
+ }
967
+ const ok = session.partialSigVerify(partialSig, pubNoncesByIndex, idx);
968
+ if (!ok) {
969
+ throw new SigningSessionError(
970
+ `Bad partial signature from ${did}.`,
971
+ "BAD_PARTIAL_SIG",
972
+ { participantDid: did, index: idx }
973
+ );
974
+ }
975
+ }
976
+ this.signature = session.partialSigAgg([...this.partialSignatures.values()]);
977
+ this.phase = "Complete" /* Complete */;
978
+ return this.signature;
979
+ }
980
+ /**
981
+ * Generates a fresh MuSig2 nonce contribution for the participant.
982
+ * Stores the secret nonce internally for use in `generatePartialSignature()`.
983
+ */
984
+ generateNonceContribution(participantPublicKey, participantSecretKey) {
985
+ const aggPublicKey = musig2.keyAggExport(musig2.keyAggregate(this.cohort.cohortKeys));
986
+ const nonces = musig2.nonceGen(participantPublicKey, participantSecretKey, aggPublicKey);
987
+ this.#secretNonce = nonces.secret;
988
+ return nonces.public;
989
+ }
990
+ /**
991
+ * Generates a partial signature using the participant's secret key + secret nonce.
992
+ * Requires the aggregated nonce to have been set first (via the service).
993
+ *
994
+ * Clears the stored secret nonce after use on every path (success or throw)
995
+ * via {@link clearSecrets}. JS cannot truly erase memory (GC may relocate
996
+ * buffers), but overwriting the bytes shortens the exposure window and
997
+ * prevents accidental reuse or serialization of a spent nonce - reuse of a
998
+ * MuSig2 nonce leaks the secret key.
999
+ */
1000
+ generatePartialSignature(participantSecretKey) {
1001
+ if (!this.aggregatedNonce) {
1002
+ throw new SigningSessionError("Aggregated nonce not available.", "MISSING_AGGREGATED_NONCE");
1003
+ }
1004
+ if (!this.#secretNonce) {
1005
+ throw new SigningSessionError("Secret nonce not available - generateNonceContribution() must be called first.", "MISSING_SECRET_NONCE");
1006
+ }
1007
+ const session = new musig2.Session(
1008
+ this.aggregatedNonce,
1009
+ this.cohort.cohortKeys,
1010
+ this.sigHash,
1011
+ [this.cohort.tapTweak],
1012
+ [true]
1013
+ );
1014
+ try {
1015
+ return session.sign(this.#secretNonce, participantSecretKey);
1016
+ } finally {
1017
+ this.clearSecrets();
1018
+ }
1019
+ }
1020
+ /**
1021
+ * Zeroize any retained secret nonce. Safe to call repeatedly and on any path
1022
+ * (completion, failure, or teardown of an abandoned session). Callers that
1023
+ * drop a session before it reaches a partial signature should invoke this so
1024
+ * the secret nonce does not linger on the live object.
1025
+ */
1026
+ clearSecrets() {
1027
+ if (this.#secretNonce) {
1028
+ (0, import_keypair.wipe)(this.#secretNonce);
1029
+ this.#secretNonce = void 0;
1030
+ }
1031
+ }
1032
+ isComplete() {
1033
+ return this.phase === "Complete" /* Complete */;
1034
+ }
1035
+ isFailed() {
1036
+ return this.phase === "Failed" /* Failed */;
1037
+ }
1038
+ };
1039
+
1040
+ // src/service/service.ts
1041
+ var DEFAULT_MAX_UPDATE_SIZE_BYTES = 256 * 1024;
1042
+ var AggregationService = class {
1043
+ did;
1044
+ publicKey;
1045
+ maxUpdateSizeBytes;
1046
+ /** Per-cohort state, keyed by cohortId. */
1047
+ #cohortStates = /* @__PURE__ */ new Map();
1048
+ constructor({ did, publicKey, maxUpdateSizeBytes }) {
1049
+ this.did = did;
1050
+ this.publicKey = publicKey;
1051
+ this.maxUpdateSizeBytes = maxUpdateSizeBytes ?? DEFAULT_MAX_UPDATE_SIZE_BYTES;
1052
+ }
1053
+ receive(message) {
1054
+ const version = message.version;
1055
+ if (version === void 0 || version !== AGGREGATION_WIRE_VERSION) {
1056
+ const cohortId = message.body?.cohortId;
1057
+ const state = cohortId ? this.#cohortStates.get(cohortId) : void 0;
1058
+ if (state) {
1059
+ state.rejections.push({
1060
+ from: message.from,
1061
+ code: "WRONG_VERSION",
1062
+ reason: `Expected wire version ${AGGREGATION_WIRE_VERSION}, got ${String(version)}`
1063
+ });
1064
+ }
1065
+ return;
1066
+ }
1067
+ const type = message.type;
1068
+ switch (type) {
1069
+ case COHORT_OPT_IN:
1070
+ this.#handleOptIn(message);
1071
+ break;
1072
+ case SUBMIT_UPDATE:
1073
+ this.#handleSubmitUpdate(message);
1074
+ break;
1075
+ case SUBMIT_NONINCLUDED:
1076
+ this.#handleSubmitNonInclusion(message);
1077
+ break;
1078
+ case VALIDATION_ACK:
1079
+ this.#handleValidationAck(message);
1080
+ break;
1081
+ case NONCE_CONTRIBUTION:
1082
+ this.#handleNonceContribution(message);
1083
+ break;
1084
+ case SIGNATURE_AUTHORIZATION:
1085
+ this.#handleSignatureAuthorization(message);
1086
+ break;
1087
+ case FALLBACK_SIGNATURE:
1088
+ this.#handleFallbackSignature(message);
1089
+ break;
1090
+ default:
1091
+ break;
1092
+ }
1093
+ }
1094
+ /**
1095
+ * Drain the rejection log for a cohort. Used by runners to surface silent
1096
+ * drops (bad proof, oversized update, wrong version, etc.) as structured
1097
+ * events without breaking the sans-I/O state machine contract.
1098
+ */
1099
+ drainRejections(cohortId) {
1100
+ const state = this.#cohortStates.get(cohortId);
1101
+ if (!state) return [];
1102
+ const out = state.rejections;
1103
+ state.rejections = [];
1104
+ return out;
1105
+ }
1106
+ /**
1107
+ * Create a new cohort with the given config. Returns the cohort ID.
1108
+ * Cohort starts in `Created` phase - call `advertise()` to broadcast.
1109
+ */
1110
+ createCohort(config) {
1111
+ const problems = validateCohortConditions(config);
1112
+ if (problems.length > 0) {
1113
+ throw new AggregationServiceError(
1114
+ `Invalid cohort conditions: ${problems.join("; ")}`,
1115
+ "INVALID_COHORT_CONDITIONS",
1116
+ { problems }
1117
+ );
1118
+ }
1119
+ const cohort = new AggregationCohort({
1120
+ serviceDid: this.did,
1121
+ minParticipants: config.minParticipants,
1122
+ network: config.network,
1123
+ beaconType: config.beaconType,
1124
+ recoveryKey: (0, import_utils3.hexToBytes)(config.recoveryKey),
1125
+ recoverySequence: config.recoverySequence,
1126
+ fundingModel: config.fundingModel,
1127
+ fallbackThreshold: config.fallbackThreshold
1128
+ });
1129
+ this.#cohortStates.set(cohort.id, {
1130
+ phase: "Created" /* Created */,
1131
+ cohort,
1132
+ config,
1133
+ pendingOptIns: /* @__PURE__ */ new Map(),
1134
+ acceptedParticipants: /* @__PURE__ */ new Set(),
1135
+ rejections: []
1136
+ });
1137
+ return cohort.id;
1138
+ }
1139
+ /**
1140
+ * Advertise a cohort to discover participants.
1141
+ * Returns the advert message to broadcast.
1142
+ */
1143
+ advertise(cohortId) {
1144
+ const state = this.#cohortStates.get(cohortId);
1145
+ if (!state) {
1146
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
1147
+ }
1148
+ if (state.phase !== "Created" /* Created */) {
1149
+ throw new AggregationServiceError(
1150
+ `Cannot advertise cohort ${cohortId}: phase is ${state.phase}.`,
1151
+ "INVALID_PHASE",
1152
+ { cohortId, phase: state.phase }
1153
+ );
1154
+ }
1155
+ const { network, ...conditions } = state.config;
1156
+ const message = createCohortAdvertMessage({
1157
+ from: this.did,
1158
+ cohortId,
1159
+ network,
1160
+ communicationPk: this.publicKey.compressed,
1161
+ ...conditions
1162
+ });
1163
+ state.phase = "Advertised" /* Advertised */;
1164
+ return [message];
1165
+ }
1166
+ /** Pending opt-ins awaiting operator approval. */
1167
+ pendingOptIns(cohortId) {
1168
+ const state = this.#cohortStates.get(cohortId);
1169
+ if (!state) return /* @__PURE__ */ new Map();
1170
+ const map = /* @__PURE__ */ new Map();
1171
+ for (const [did, optIn] of state.pendingOptIns) {
1172
+ if (!state.acceptedParticipants.has(did)) {
1173
+ map.set(did, optIn);
1174
+ }
1175
+ }
1176
+ return map;
1177
+ }
1178
+ #handleOptIn(message) {
1179
+ const cohortId = message.body?.cohortId;
1180
+ if (!cohortId) return;
1181
+ const state = this.#cohortStates.get(cohortId);
1182
+ if (!state) return;
1183
+ if (state.phase !== "Advertised" /* Advertised */) return;
1184
+ const participantDid = message.from;
1185
+ const participantPk = message.body?.participantPk;
1186
+ const communicationPk = message.body?.communicationPk;
1187
+ if (!participantPk || !communicationPk) return;
1188
+ if (state.acceptedParticipants.has(participantDid)) return;
1189
+ state.pendingOptIns.set(participantDid, {
1190
+ cohortId,
1191
+ participantDid,
1192
+ participantPk,
1193
+ communicationPk
1194
+ });
1195
+ }
1196
+ /**
1197
+ * Service operator accepts a participant's opt-in.
1198
+ * Returns the accept message to send.
1199
+ */
1200
+ acceptParticipant(cohortId, participantDid) {
1201
+ const state = this.#cohortStates.get(cohortId);
1202
+ if (!state) {
1203
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
1204
+ }
1205
+ const optIn = state.pendingOptIns.get(participantDid);
1206
+ if (!optIn) {
1207
+ throw new AggregationServiceError(
1208
+ `No pending opt-in from ${participantDid} for cohort ${cohortId}.`,
1209
+ "NO_OPT_IN",
1210
+ { cohortId, participantDid }
1211
+ );
1212
+ }
1213
+ if (state.acceptedParticipants.has(participantDid)) {
1214
+ throw new AggregationServiceError(
1215
+ `Participant ${participantDid} already accepted into cohort ${cohortId}.`,
1216
+ "ALREADY_ACCEPTED",
1217
+ { cohortId, participantDid }
1218
+ );
1219
+ }
1220
+ const maxParticipants = state.config.maxParticipants;
1221
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size >= maxParticipants) {
1222
+ throw new AggregationServiceError(
1223
+ `Cohort ${cohortId} is full: ${maxParticipants} participants already accepted.`,
1224
+ "COHORT_FULL",
1225
+ { cohortId, maxParticipants }
1226
+ );
1227
+ }
1228
+ state.acceptedParticipants.add(participantDid);
1229
+ state.cohort.participants.push(participantDid);
1230
+ state.cohort.participantKeys.set(participantDid, optIn.participantPk);
1231
+ state.cohort.cohortKeys = [...state.cohort.cohortKeys, optIn.participantPk];
1232
+ return [createCohortOptInAcceptMessage({
1233
+ from: this.did,
1234
+ to: participantDid,
1235
+ cohortId
1236
+ })];
1237
+ }
1238
+ /**
1239
+ * Finalize cohort keygen: compute MuSig2 Taproot beacon address and send
1240
+ * COHORT_READY messages to all accepted participants.
1241
+ */
1242
+ finalizeKeygen(cohortId) {
1243
+ const state = this.#cohortStates.get(cohortId);
1244
+ if (!state) {
1245
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
1246
+ }
1247
+ if (state.phase !== "Advertised" /* Advertised */) {
1248
+ throw new AggregationServiceError(
1249
+ `Cannot finalize keygen for cohort ${cohortId}: phase is ${state.phase}.`,
1250
+ "INVALID_PHASE",
1251
+ { cohortId, phase: state.phase }
1252
+ );
1253
+ }
1254
+ if (state.acceptedParticipants.size < state.config.minParticipants) {
1255
+ throw new AggregationServiceError(
1256
+ `Cohort ${cohortId} has only ${state.acceptedParticipants.size} accepted participants, need ${state.config.minParticipants}.`,
1257
+ "NOT_ENOUGH_PARTICIPANTS",
1258
+ { cohortId }
1259
+ );
1260
+ }
1261
+ const maxParticipants = state.config.maxParticipants;
1262
+ if (maxParticipants !== void 0 && state.acceptedParticipants.size > maxParticipants) {
1263
+ throw new AggregationServiceError(
1264
+ `Cohort ${cohortId} has ${state.acceptedParticipants.size} accepted participants, exceeds max ${maxParticipants}.`,
1265
+ "TOO_MANY_PARTICIPANTS",
1266
+ { cohortId, maxParticipants }
1267
+ );
1268
+ }
1269
+ const beaconAddress = state.cohort.computeBeaconAddress();
1270
+ state.phase = "CohortSet" /* CohortSet */;
1271
+ const messages = [];
1272
+ for (const participantDid of state.cohort.participants) {
1273
+ messages.push(createCohortReadyMessage({
1274
+ from: this.did,
1275
+ to: participantDid,
1276
+ cohortId,
1277
+ beaconAddress,
1278
+ cohortKeys: state.cohort.cohortKeys
1279
+ }));
1280
+ }
1281
+ return messages;
1282
+ }
1283
+ /** Updates collected so far for a cohort. */
1284
+ collectedUpdates(cohortId) {
1285
+ const state = this.#cohortStates.get(cohortId);
1286
+ if (!state) return /* @__PURE__ */ new Map();
1287
+ return state.cohort.pendingUpdates;
1288
+ }
1289
+ /**
1290
+ * Handle an incoming SUBMIT_UPDATE message from a participant containing their signed update to
1291
+ * submit for aggregation.
1292
+ * @param {BaseMessage} message - incoming SUBMIT_UPDATE message containing a participant's signed
1293
+ * update to submit for aggregation
1294
+ * @returns {void} - no return value; updates the service state with the submitted update if valid
1295
+ */
1296
+ #handleSubmitUpdate(message) {
1297
+ const cohortId = message.body?.cohortId;
1298
+ if (!cohortId) return;
1299
+ const state = this.#cohortStates.get(cohortId);
1300
+ if (!state) return;
1301
+ if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
1302
+ const signedUpdate = message.body?.signedUpdate;
1303
+ if (!signedUpdate) {
1304
+ state.rejections.push({
1305
+ from: message.from,
1306
+ code: "UPDATE_MALFORMED",
1307
+ reason: "SUBMIT_UPDATE missing signedUpdate body"
1308
+ });
1309
+ return;
1310
+ }
1311
+ const canonicalSize = (0, import_common4.canonicalize)(signedUpdate).length;
1312
+ if (canonicalSize > this.maxUpdateSizeBytes) {
1313
+ state.rejections.push({
1314
+ from: message.from,
1315
+ code: "UPDATE_TOO_LARGE",
1316
+ reason: `Canonicalized update is ${canonicalSize} bytes; max allowed is ${this.maxUpdateSizeBytes}`
1317
+ });
1318
+ return;
1319
+ }
1320
+ if (!this.#verifySubmittedUpdate(state, message.from, signedUpdate)) {
1321
+ state.rejections.push({
1322
+ from: message.from,
1323
+ code: "UPDATE_VERIFICATION_FAILED",
1324
+ reason: "BIP-340 Data Integrity proof verification failed"
1325
+ });
1326
+ return;
1327
+ }
1328
+ if (state.cohort.nonIncluded.has(message.from)) {
1329
+ state.rejections.push({
1330
+ from: message.from,
1331
+ code: "UPDATE_MALFORMED",
1332
+ reason: "Participant already declined this round; cannot also submit an update"
1333
+ });
1334
+ return;
1335
+ }
1336
+ if (state.cohort.pendingUpdates.has(message.from)) {
1337
+ state.rejections.push({
1338
+ from: message.from,
1339
+ code: "UPDATE_MALFORMED",
1340
+ reason: "Participant already submitted an update this round; cannot resubmit"
1341
+ });
1342
+ return;
1343
+ }
1344
+ state.cohort.addUpdate(message.from, signedUpdate);
1345
+ if (state.phase === "CohortSet" /* CohortSet */) {
1346
+ state.phase = "CollectingUpdates" /* CollectingUpdates */;
1347
+ }
1348
+ if (state.cohort.hasAllResponses()) {
1349
+ state.phase = "UpdatesCollected" /* UpdatesCollected */;
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Handle an incoming SUBMIT_NONINCLUDED message: a member declares it has no
1354
+ * update this round (cooperative non-inclusion). Membership is proven by the
1355
+ * signed transport envelope, so the body carries only the cohortId. The member
1356
+ * stays in the cohort and still signs; it is absent from the CAS map and
1357
+ * carries a non-inclusion leaf in the SMT.
1358
+ */
1359
+ #handleSubmitNonInclusion(message) {
1360
+ const cohortId = message.body?.cohortId;
1361
+ if (!cohortId) return;
1362
+ const state = this.#cohortStates.get(cohortId);
1363
+ if (!state) return;
1364
+ if (state.phase !== "CohortSet" /* CohortSet */ && state.phase !== "CollectingUpdates" /* CollectingUpdates */) return;
1365
+ if (!state.cohort.participants.includes(message.from)) {
1366
+ state.rejections.push({
1367
+ from: message.from,
1368
+ code: "UPDATE_MALFORMED",
1369
+ reason: "Sender is not a member of this cohort"
1370
+ });
1371
+ return;
1372
+ }
1373
+ if (state.cohort.pendingUpdates.has(message.from) || state.cohort.nonIncluded.has(message.from)) {
1374
+ state.rejections.push({
1375
+ from: message.from,
1376
+ code: "UPDATE_MALFORMED",
1377
+ reason: "Participant already responded this round"
1378
+ });
1379
+ return;
1380
+ }
1381
+ state.cohort.addNonInclusion(message.from);
1382
+ if (state.phase === "CohortSet" /* CohortSet */) {
1383
+ state.phase = "CollectingUpdates" /* CollectingUpdates */;
1384
+ }
1385
+ if (state.cohort.hasAllResponses()) {
1386
+ state.phase = "UpdatesCollected" /* UpdatesCollected */;
1387
+ }
1388
+ }
1389
+ /**
1390
+ * Verify the BIP-340 Schnorr Data Integrity proof on a submitted update using the
1391
+ * participant's public key from their cohort opt-in. Returns `false` (and the
1392
+ * update is silently dropped) if the proof is missing, the verificationMethod does
1393
+ * not name the sender's DID, the participant has no opt-in on record, or the
1394
+ * signature fails verification.
1395
+ * @param {ServiceCohortState} state - the current state of the cohort to which the update was submitted
1396
+ * @param {string} sender - the DID of the participant who submitted the update
1397
+ * @param {SecuredDocument} signedUpdate - the signed update containing the proof to verify
1398
+ * @returns {boolean} - `true` if the proof is valid and the update can be accepted; `false` otherwise
1399
+ */
1400
+ #verifySubmittedUpdate(state, sender, signedUpdate) {
1401
+ const proof = signedUpdate.proof;
1402
+ if (!proof?.verificationMethod || !proof.proofValue) return false;
1403
+ const vmDid = proof.verificationMethod.split("#")[0];
1404
+ if (vmDid !== sender) return false;
1405
+ const optIn = state.pendingOptIns.get(sender);
1406
+ if (!optIn) return false;
1407
+ try {
1408
+ const multikey = import_cryptosuite.SchnorrMultikey.fromPublicKey({
1409
+ id: proof.verificationMethod,
1410
+ controller: sender,
1411
+ publicKeyBytes: optIn.participantPk
1412
+ });
1413
+ const suite = new import_cryptosuite.BIP340Cryptosuite(multikey);
1414
+ return suite.verifyProof(signedUpdate).verified === true;
1415
+ } catch {
1416
+ return false;
1417
+ }
1418
+ }
1419
+ /**
1420
+ * Build the aggregated data structure (CAS Announcement or SMT tree) and
1421
+ * return distribute messages to send to all participants for validation.
1422
+ */
1423
+ buildAndDistribute(cohortId) {
1424
+ const state = this.#cohortStates.get(cohortId);
1425
+ if (!state) {
1426
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
1427
+ }
1428
+ if (state.phase !== "UpdatesCollected" /* UpdatesCollected */) {
1429
+ throw new AggregationServiceError(
1430
+ `Cannot build aggregated data for cohort ${cohortId}: phase is ${state.phase}.`,
1431
+ "INVALID_PHASE",
1432
+ { cohortId, phase: state.phase }
1433
+ );
1434
+ }
1435
+ const strategy = getBeaconStrategy(state.config.beaconType);
1436
+ if (!strategy) {
1437
+ throw new AggregationServiceError(
1438
+ `Unsupported beacon type: ${state.config.beaconType}`,
1439
+ "UNSUPPORTED_BEACON_TYPE",
1440
+ { cohortId, beaconType: state.config.beaconType }
1441
+ );
1442
+ }
1443
+ strategy.buildAggregatedData(state.cohort);
1444
+ const signalBytesHex = (0, import_utils3.bytesToHex)(state.cohort.signalBytes);
1445
+ state.phase = "DataDistributed" /* DataDistributed */;
1446
+ const messages = [];
1447
+ for (const participantDid of state.cohort.participants) {
1448
+ const payload = strategy.getDistributePayload(state.cohort, participantDid);
1449
+ messages.push(createDistributeAggregatedDataMessage({
1450
+ from: this.did,
1451
+ to: participantDid,
1452
+ cohortId,
1453
+ beaconType: state.config.beaconType,
1454
+ signalBytesHex,
1455
+ casAnnouncement: payload.casAnnouncement,
1456
+ smtProof: payload.smtProof
1457
+ }));
1458
+ }
1459
+ return messages;
1460
+ }
1461
+ validationProgress(cohortId) {
1462
+ const state = this.#cohortStates.get(cohortId);
1463
+ if (!state) {
1464
+ return { approved: /* @__PURE__ */ new Set(), rejected: /* @__PURE__ */ new Set(), pending: /* @__PURE__ */ new Set(), total: 0 };
1465
+ }
1466
+ const approved = state.cohort.validationAcks;
1467
+ const rejected = state.cohort.validationRejections;
1468
+ const allParticipants = new Set(state.cohort.participants);
1469
+ const responded = /* @__PURE__ */ new Set([...approved, ...rejected]);
1470
+ const pending = new Set([...allParticipants].filter((p) => !responded.has(p)));
1471
+ return {
1472
+ approved,
1473
+ rejected,
1474
+ pending,
1475
+ total: allParticipants.size
1476
+ };
1477
+ }
1478
+ #handleValidationAck(message) {
1479
+ const cohortId = message.body?.cohortId;
1480
+ if (!cohortId) return;
1481
+ const state = this.#cohortStates.get(cohortId);
1482
+ if (!state) return;
1483
+ if (state.phase !== "DataDistributed" /* DataDistributed */) return;
1484
+ const approved = message.body?.approved;
1485
+ if (approved === void 0) return;
1486
+ state.cohort.addValidation(message.from, approved);
1487
+ if (state.cohort.isFullyValidated()) {
1488
+ state.phase = "Validated" /* Validated */;
1489
+ } else if (state.cohort.hasAllValidationResponses()) {
1490
+ state.phase = "Failed" /* Failed */;
1491
+ }
1492
+ }
1493
+ /**
1494
+ * Start a signing session by creating auth requests for all participants.
1495
+ * The caller provides the transaction data - typically built via
1496
+ * `buildBeaconTransaction()` against a Bitcoin connection.
1497
+ */
1498
+ startSigning(cohortId, txData) {
1499
+ const state = this.#cohortStates.get(cohortId);
1500
+ if (!state) {
1501
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
1502
+ }
1503
+ if (state.phase !== "Validated" /* Validated */) {
1504
+ throw new AggregationServiceError(
1505
+ `Cannot start signing for cohort ${cohortId}: phase is ${state.phase}.`,
1506
+ "INVALID_PHASE",
1507
+ { cohortId, phase: state.phase }
1508
+ );
1509
+ }
1510
+ const session = new BeaconSigningSession({
1511
+ cohort: state.cohort,
1512
+ pendingTx: txData.tx,
1513
+ prevOutScripts: txData.prevOutScripts,
1514
+ prevOutValues: txData.prevOutValues
1515
+ });
1516
+ state.signingSession = session;
1517
+ state.phase = "SigningStarted" /* SigningStarted */;
1518
+ const prevOutScript = txData.prevOutScripts[0];
1519
+ if (!prevOutScript) {
1520
+ throw new AggregationServiceError(
1521
+ `Cannot start signing for cohort ${cohortId}: txData.prevOutScripts[0] is missing.`,
1522
+ "MISSING_PREV_OUT_SCRIPT",
1523
+ { cohortId }
1524
+ );
1525
+ }
1526
+ const messages = [];
1527
+ for (const participantDid of state.cohort.participants) {
1528
+ messages.push(createAuthorizationRequestMessage({
1529
+ from: this.did,
1530
+ to: participantDid,
1531
+ cohortId,
1532
+ sessionId: session.id,
1533
+ pendingTx: txData.tx.hex,
1534
+ prevOutScriptHex: (0, import_utils3.bytesToHex)(prevOutScript),
1535
+ prevOutValue: txData.prevOutValues[0]?.toString() ?? "0"
1536
+ }));
1537
+ }
1538
+ return messages;
1539
+ }
1540
+ #handleNonceContribution(message) {
1541
+ const cohortId = message.body?.cohortId;
1542
+ if (!cohortId) return;
1543
+ const state = this.#cohortStates.get(cohortId);
1544
+ if (!state || !state.signingSession) return;
1545
+ if (state.phase !== "SigningStarted" /* SigningStarted */) return;
1546
+ const sessionId = message.body?.sessionId;
1547
+ if (sessionId !== state.signingSession.id) return;
1548
+ const nonceContribution = message.body?.nonceContribution;
1549
+ if (!nonceContribution) return;
1550
+ state.signingSession.addNonceContribution(message.from, nonceContribution);
1551
+ if (state.signingSession.nonceContributions.size === state.cohort.participants.length) {
1552
+ state.phase = "NoncesCollected" /* NoncesCollected */;
1553
+ }
1554
+ }
1555
+ /**
1556
+ * Generate the aggregated nonce and return messages to send to participants.
1557
+ * Call after `validationProgress(cohortId).approved.size === total`.
1558
+ */
1559
+ sendAggregatedNonce(cohortId) {
1560
+ const state = this.#cohortStates.get(cohortId);
1561
+ if (!state) {
1562
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
1563
+ }
1564
+ if (state.phase !== "NoncesCollected" /* NoncesCollected */ || !state.signingSession) {
1565
+ throw new AggregationServiceError(
1566
+ `Cannot send aggregated nonce for cohort ${cohortId}: phase is ${state.phase}.`,
1567
+ "INVALID_PHASE",
1568
+ { cohortId, phase: state.phase }
1569
+ );
1570
+ }
1571
+ const aggregatedNonce = state.signingSession.generateAggregatedNonce();
1572
+ state.phase = "AwaitingPartialSigs" /* AwaitingPartialSigs */;
1573
+ const messages = [];
1574
+ for (const participantDid of state.cohort.participants) {
1575
+ messages.push(createAggregatedNonceMessage({
1576
+ from: this.did,
1577
+ to: participantDid,
1578
+ cohortId,
1579
+ sessionId: state.signingSession.id,
1580
+ aggregatedNonce
1581
+ }));
1582
+ }
1583
+ return messages;
1584
+ }
1585
+ #handleSignatureAuthorization(message) {
1586
+ const cohortId = message.body?.cohortId;
1587
+ if (!cohortId) return;
1588
+ const state = this.#cohortStates.get(cohortId);
1589
+ if (!state || !state.signingSession) return;
1590
+ if (state.phase !== "AwaitingPartialSigs" /* AwaitingPartialSigs */) return;
1591
+ const sessionId = message.body?.sessionId;
1592
+ if (sessionId !== state.signingSession.id) return;
1593
+ const partialSignature = message.body?.partialSignature;
1594
+ if (!partialSignature) return;
1595
+ state.signingSession.addPartialSignature(message.from, partialSignature);
1596
+ if (state.signingSession.partialSignatures.size === state.cohort.participants.length) {
1597
+ const signature = state.signingSession.generateFinalSignature();
1598
+ state.signingSession.pendingTx.updateInput(0, { finalScriptWitness: [signature] });
1599
+ state.result = {
1600
+ cohortId,
1601
+ signature,
1602
+ signedTx: state.signingSession.pendingTx,
1603
+ path: "key-path"
1604
+ };
1605
+ state.phase = "Complete" /* Complete */;
1606
+ }
1607
+ }
1608
+ /**
1609
+ * Abandon the optimistic n-of-n key path and ask members to authorize the
1610
+ * k-of-n fallback (script-path) spend of the SAME beacon transaction (graceful
1611
+ * liveness, ADR 042). Reuses the in-flight signing session's transaction and
1612
+ * spent output, so the announcement and its outputs are unchanged: only the
1613
+ * witness path differs. Returns one FALLBACK_AUTHORIZATION_REQUEST per
1614
+ * participant.
1615
+ *
1616
+ * Callable once optimistic signing has started (the session and its tx exist)
1617
+ * and before it completes. A cohort can take exactly one of the two paths: the
1618
+ * caller (runner) must commit to fallback and stop driving the optimistic path.
1619
+ */
1620
+ startFallbackSigning(cohortId) {
1621
+ const state = this.#cohortStates.get(cohortId);
1622
+ if (!state) {
1623
+ throw new AggregationServiceError(`Cohort ${cohortId} not found.`, "COHORT_NOT_FOUND", { cohortId });
1624
+ }
1625
+ if (!state.signingSession) {
1626
+ throw new AggregationServiceError(
1627
+ `Cannot start fallback for cohort ${cohortId}: no signing session.`,
1628
+ "NO_SIGNING_SESSION",
1629
+ { cohortId }
1630
+ );
1631
+ }
1632
+ const signingPhases = [
1633
+ "SigningStarted" /* SigningStarted */,
1634
+ "NoncesCollected" /* NoncesCollected */,
1635
+ "AwaitingPartialSigs" /* AwaitingPartialSigs */
1636
+ ];
1637
+ if (!signingPhases.includes(state.phase)) {
1638
+ throw new AggregationServiceError(
1639
+ `Cannot start fallback for cohort ${cohortId}: phase is ${state.phase}.`,
1640
+ "INVALID_PHASE",
1641
+ { cohortId, phase: state.phase }
1642
+ );
1643
+ }
1644
+ const session = state.signingSession;
1645
+ const prevOutScript = session.prevOutScripts[0];
1646
+ const prevOutValue = session.prevOutValues[0];
1647
+ if (!prevOutScript || prevOutValue === void 0) {
1648
+ throw new AggregationServiceError(
1649
+ `Cannot start fallback for cohort ${cohortId}: signing session missing prevout data.`,
1650
+ "MISSING_PREV_OUT",
1651
+ { cohortId }
1652
+ );
1653
+ }
1654
+ const fallbackLeaf = buildFallbackLeaf({
1655
+ cohortKeys: state.cohort.cohortKeys,
1656
+ fallbackThreshold: state.cohort.effectiveFallbackThreshold
1657
+ });
1658
+ state.fallbackSignatures = /* @__PURE__ */ new Map();
1659
+ state.phase = "FallbackRequested" /* FallbackRequested */;
1660
+ const messages = [];
1661
+ for (const participantDid of state.cohort.participants) {
1662
+ messages.push(createFallbackAuthorizationRequestMessage({
1663
+ from: this.did,
1664
+ to: participantDid,
1665
+ cohortId,
1666
+ sessionId: session.id,
1667
+ pendingTx: session.pendingTx.hex,
1668
+ prevOutScriptHex: (0, import_utils3.bytesToHex)(prevOutScript),
1669
+ prevOutValue: prevOutValue.toString(),
1670
+ fallbackLeafScriptHex: (0, import_utils3.bytesToHex)(fallbackLeaf)
1671
+ }));
1672
+ }
1673
+ return messages;
1674
+ }
1675
+ /**
1676
+ * Handle an incoming FALLBACK_SIGNATURE: a member's standalone BIP-340
1677
+ * signature over the fallback script-path sighash. The signature is
1678
+ * authenticated to the sender (its `signerPk` must be the sender's own cohort
1679
+ * key and the signature must verify against the sighash) and collected. Once k
1680
+ * valid signatures are in, the k-of-n fallback spend is assembled and the
1681
+ * cohort completes via the script path.
1682
+ */
1683
+ #handleFallbackSignature(message) {
1684
+ const cohortId = message.body?.cohortId;
1685
+ if (!cohortId) return;
1686
+ const state = this.#cohortStates.get(cohortId);
1687
+ if (!state || !state.signingSession || !state.fallbackSignatures) return;
1688
+ if (state.phase !== "FallbackRequested" /* FallbackRequested */) return;
1689
+ const sessionId = message.body?.sessionId;
1690
+ if (sessionId !== state.signingSession.id) return;
1691
+ const signerPk = message.body?.signerPk;
1692
+ const fallbackSignature = message.body?.fallbackSignature;
1693
+ if (!signerPk || !fallbackSignature) return;
1694
+ const prevOutScript = state.signingSession.prevOutScripts[0];
1695
+ const prevOutValue = state.signingSession.prevOutValues[0];
1696
+ if (!prevOutScript || prevOutValue === void 0) return;
1697
+ const memberKey = state.cohort.participantKeys.get(message.from);
1698
+ if (!memberKey) {
1699
+ state.rejections.push({ from: message.from, code: "UPDATE_MALFORMED", reason: "Fallback signature from a non-member" });
1700
+ return;
1701
+ }
1702
+ const memberXOnly = memberKey.slice(1);
1703
+ if (signerPk.length !== 32 || !memberXOnly.every((b, i) => b === signerPk[i])) {
1704
+ state.rejections.push({ from: message.from, code: "UPDATE_MALFORMED", reason: "Fallback signerPk does not match the sender cohort key" });
1705
+ return;
1706
+ }
1707
+ const fallbackLeaf = buildFallbackLeaf({
1708
+ cohortKeys: state.cohort.cohortKeys,
1709
+ fallbackThreshold: state.cohort.effectiveFallbackThreshold
1710
+ });
1711
+ const sighash = fallbackSighash(state.signingSession.pendingTx, 0, prevOutScript, prevOutValue, fallbackLeaf);
1712
+ let valid = false;
1713
+ try {
1714
+ valid = fallbackSignature.length === 64 && import_secp256k14.schnorr.verify(fallbackSignature, sighash, signerPk);
1715
+ } catch {
1716
+ valid = false;
1717
+ }
1718
+ if (!valid) {
1719
+ state.rejections.push({ from: message.from, code: "UPDATE_VERIFICATION_FAILED", reason: "Fallback signature failed verification" });
1720
+ return;
1721
+ }
1722
+ state.fallbackSignatures.set(message.from, { pubKey: signerPk, signature: fallbackSignature });
1723
+ if (state.fallbackSignatures.size >= state.cohort.effectiveFallbackThreshold) {
1724
+ const signedTx = buildFallbackSpend({
1725
+ pendingTx: state.signingSession.pendingTx,
1726
+ cohortKeys: state.cohort.cohortKeys,
1727
+ fallbackThreshold: state.cohort.effectiveFallbackThreshold,
1728
+ recoveryKey: state.cohort.recoveryKey,
1729
+ recoverySequence: state.cohort.recoverySequence,
1730
+ fundingModel: state.cohort.fundingModel,
1731
+ network: state.cohort.network,
1732
+ prevOutScript,
1733
+ prevOutValue,
1734
+ signatures: [...state.fallbackSignatures.values()]
1735
+ });
1736
+ state.result = { cohortId, signature: new Uint8Array(), signedTx, path: "script-path" };
1737
+ state.phase = "Complete" /* Complete */;
1738
+ }
1739
+ }
1740
+ getResult(cohortId) {
1741
+ return this.#cohortStates.get(cohortId)?.result;
1742
+ }
1743
+ getCohortPhase(cohortId) {
1744
+ return this.#cohortStates.get(cohortId)?.phase;
1745
+ }
1746
+ getCohort(cohortId) {
1747
+ return this.#cohortStates.get(cohortId)?.cohort;
1748
+ }
1749
+ /**
1750
+ * Get the signing session ID for a cohort, if a signing session has been started.
1751
+ * @param {string} cohortId - The cohort ID.
1752
+ * @returns {string | undefined} The session ID, or undefined if no session is active.
1753
+ */
1754
+ getSigningSessionId(cohortId) {
1755
+ return this.#cohortStates.get(cohortId)?.signingSession?.id;
1756
+ }
1757
+ get cohorts() {
1758
+ return [...this.#cohortStates.values()].map((s) => s.cohort);
1759
+ }
1760
+ /**
1761
+ * Remove a cohort from the state map. Used by runners to GC state on cohort
1762
+ * completion, failure, or expiry. No-op if the cohort doesn't exist.
1763
+ */
1764
+ removeCohort(cohortId) {
1765
+ this.#cohortStates.delete(cohortId);
1766
+ }
1767
+ };
1768
+
1769
+ // src/service/service-runner.ts
1770
+ var import_bitcoin3 = require("@did-btcr2/bitcoin");
1771
+
1772
+ // src/core/typed-emitter.ts
1773
+ var TypedEventEmitter = class {
1774
+ #listeners = /* @__PURE__ */ new Map();
1775
+ /** Subscribe to an event. */
1776
+ on(event, listener) {
1777
+ let set = this.#listeners.get(event);
1778
+ if (!set) {
1779
+ set = /* @__PURE__ */ new Set();
1780
+ this.#listeners.set(event, set);
1781
+ }
1782
+ set.add(listener);
1783
+ return this;
1784
+ }
1785
+ /** Subscribe to an event, automatically unsubscribing after the first call. */
1786
+ once(event, listener) {
1787
+ const wrapped = ((...args) => {
1788
+ this.off(event, wrapped);
1789
+ listener(...args);
1790
+ });
1791
+ return this.on(event, wrapped);
1792
+ }
1793
+ /** Unsubscribe a specific listener. */
1794
+ off(event, listener) {
1795
+ this.#listeners.get(event)?.delete(listener);
1796
+ return this;
1797
+ }
1798
+ /** Emit an event to all subscribed listeners. */
1799
+ emit(event, ...args) {
1800
+ const set = this.#listeners.get(event);
1801
+ if (!set || set.size === 0) return false;
1802
+ for (const listener of set) {
1803
+ try {
1804
+ listener(...args);
1805
+ } catch (err) {
1806
+ console.error(`Listener for event "${String(event)}" threw:`, err);
1807
+ }
1808
+ }
1809
+ return true;
1810
+ }
1811
+ /** Remove all listeners (optionally for a specific event). */
1812
+ removeAllListeners(event) {
1813
+ if (event === void 0) {
1814
+ this.#listeners.clear();
1815
+ } else {
1816
+ this.#listeners.delete(event);
1817
+ }
1818
+ return this;
1819
+ }
1820
+ /** Number of listeners for an event. */
1821
+ listenerCount(event) {
1822
+ return this.#listeners.get(event)?.size ?? 0;
1823
+ }
1824
+ };
1825
+
1826
+ // src/service/service-runner.ts
1827
+ var DEFAULT_FEE_ESTIMATOR = new import_bitcoin3.StaticFeeEstimator(5);
1828
+ var DEFAULT_ADVERT_REPEAT_INTERVAL_MS = 6e4;
1829
+ var AggregationServiceRunner = class _AggregationServiceRunner extends TypedEventEmitter {
1830
+ /** Direct access to the underlying state machine for advanced use. */
1831
+ session;
1832
+ #transport;
1833
+ #did;
1834
+ #defaultConfig;
1835
+ #onOptInReceived;
1836
+ #onReadyToFinalize;
1837
+ #onProvideTxData;
1838
+ #feeEstimator;
1839
+ #cohortTtlMs;
1840
+ #phaseTimeoutMs;
1841
+ #advertRepeatIntervalMs;
1842
+ #autoFallbackOnStall;
1843
+ /** Phases during which a stall can be salvaged by the k-of-n fallback (ADR 042). */
1844
+ static #SIGNING_PHASES = [
1845
+ "SigningStarted" /* SigningStarted */,
1846
+ "NoncesCollected" /* NoncesCollected */,
1847
+ "AwaitingPartialSigs" /* AwaitingPartialSigs */
1848
+ ];
1849
+ /** Per-cohort run state, keyed by cohortId. */
1850
+ #contexts = /* @__PURE__ */ new Map();
1851
+ #handlersRegistered = false;
1852
+ #stopped = false;
1853
+ constructor(options) {
1854
+ super();
1855
+ this.#transport = options.transport;
1856
+ this.#did = options.did;
1857
+ this.#defaultConfig = options.config;
1858
+ this.#onOptInReceived = options.onOptInReceived ?? (async () => ({ accepted: true }));
1859
+ this.#onReadyToFinalize = options.onReadyToFinalize ?? (async ({ acceptedCount, minRequired }) => ({
1860
+ finalize: acceptedCount >= minRequired
1861
+ }));
1862
+ this.#onProvideTxData = options.onProvideTxData;
1863
+ this.#feeEstimator = options.feeEstimator ?? DEFAULT_FEE_ESTIMATOR;
1864
+ this.#cohortTtlMs = options.cohortTtlMs;
1865
+ this.#phaseTimeoutMs = options.phaseTimeoutMs;
1866
+ this.#advertRepeatIntervalMs = options.advertRepeatIntervalMs ?? DEFAULT_ADVERT_REPEAT_INTERVAL_MS;
1867
+ this.#autoFallbackOnStall = options.autoFallbackOnStall ?? false;
1868
+ this.session = new AggregationService({
1869
+ // The coordinator never signs, so the state machine receives only the
1870
+ // public half of the operator's keypair (see ADR 038). The full keypair
1871
+ // remains the operator's transport/communication identity.
1872
+ did: options.did,
1873
+ publicKey: options.keys.publicKey,
1874
+ maxUpdateSizeBytes: options.maxUpdateSizeBytes
1875
+ });
1876
+ }
1877
+ /** Resolve the {@link RunContext} an inbound message belongs to, by cohortId. */
1878
+ #contextFor(msg) {
1879
+ const cohortId = msg.body?.cohortId;
1880
+ if (!cohortId) return void 0;
1881
+ return this.#contexts.get(cohortId);
1882
+ }
1883
+ /**
1884
+ * Drain any silent rejections the state machine recorded for a cohort during
1885
+ * the most recent receive() and surface them as `message-rejected` events.
1886
+ */
1887
+ #drainRejections(ctx) {
1888
+ for (const r of this.session.drainRejections(ctx.cohortId)) {
1889
+ this.emit("message-rejected", { cohortId: ctx.cohortId, ...r });
1890
+ }
1891
+ }
1892
+ /**
1893
+ * Advertise a new cohort and begin driving it to completion. Callable many
1894
+ * times on one runner; each cohort runs concurrently and independently.
1895
+ *
1896
+ * @param config Per-cohort conditions + network (see {@link CohortConfig}).
1897
+ * @returns The new cohort's id and a `completion` promise that resolves with
1898
+ * that cohort's {@link AggregationResult} (or rejects if it fails/stalls).
1899
+ * @throws If the runner has been stopped, or the config is invalid
1900
+ * (fail-fast via `createCohort`).
1901
+ */
1902
+ advertiseCohort(config) {
1903
+ if (this.#stopped) {
1904
+ throw new AggregationServiceError("Cannot advertise on a stopped runner.", "RUNNER_STOPPED", {});
1905
+ }
1906
+ this.#registerHandlers();
1907
+ const cohortId = this.session.createCohort(config);
1908
+ let resolve;
1909
+ let reject;
1910
+ const completion = new Promise((res, rej) => {
1911
+ resolve = res;
1912
+ reject = rej;
1913
+ });
1914
+ const ctx = {
1915
+ cohortId,
1916
+ config,
1917
+ resolve,
1918
+ reject,
1919
+ completion,
1920
+ finalizing: false,
1921
+ settled: false
1922
+ };
1923
+ this.#contexts.set(cohortId, ctx);
1924
+ try {
1925
+ this.#startTimers(ctx);
1926
+ const advertMsgs = this.session.advertise(cohortId);
1927
+ this.#onPhaseMaybeChanged(ctx);
1928
+ this.emit("cohort-advertised", { cohortId });
1929
+ if (this.#advertRepeatIntervalMs > 0) {
1930
+ this.#startAdvertRepeat(ctx, advertMsgs);
1931
+ } else {
1932
+ this.#sendAll(advertMsgs).catch((err) => this.#failCohort(ctx, err));
1933
+ }
1934
+ } catch (err) {
1935
+ this.#failCohort(ctx, err);
1936
+ }
1937
+ return { cohortId, completion };
1938
+ }
1939
+ /**
1940
+ * Run a single cohort to completion using the `config` supplied in the
1941
+ * runner options. Thin convenience over {@link advertiseCohort} for the
1942
+ * single-cohort case (and the path {@link AggregationRunner.solo} rides).
1943
+ *
1944
+ * @returns {Promise<AggregationResult>} The final result with signature and signed tx.
1945
+ */
1946
+ run() {
1947
+ if (!this.#defaultConfig) {
1948
+ return Promise.reject(new AggregationServiceError(
1949
+ "run() requires `config` in the runner options; use advertiseCohort(config) to drive cohorts explicitly.",
1950
+ "MISSING_COHORT_CONFIG",
1951
+ {}
1952
+ ));
1953
+ }
1954
+ try {
1955
+ return this.advertiseCohort(this.#defaultConfig).completion;
1956
+ } catch (err) {
1957
+ return Promise.reject(err);
1958
+ }
1959
+ }
1960
+ /**
1961
+ * Wait for every currently-outstanding cohort to settle and return the
1962
+ * successful results. Dynamic drain: cohorts advertised while this is pending
1963
+ * are included, and it resolves only once no cohorts remain. Failed cohorts
1964
+ * are surfaced via `error` / `cohort-failed` events and their rejected
1965
+ * `completion` promises; they are omitted from the returned array (this
1966
+ * method does not throw). Bound long-running cohorts with `cohortTtlMs` /
1967
+ * `phaseTimeoutMs` or this may never resolve.
1968
+ *
1969
+ * @returns {Promise<AggregationResult[]>} Results of the cohorts that completed.
1970
+ */
1971
+ async runAll() {
1972
+ const collected = /* @__PURE__ */ new Map();
1973
+ const onComplete = (result) => {
1974
+ collected.set(result.cohortId, result);
1975
+ };
1976
+ this.on("signing-complete", onComplete);
1977
+ try {
1978
+ while (this.#contexts.size > 0) {
1979
+ await Promise.allSettled([...this.#contexts.values()].map((c) => c.completion));
1980
+ }
1981
+ } finally {
1982
+ this.off("signing-complete", onComplete);
1983
+ }
1984
+ return [...collected.values()];
1985
+ }
1986
+ /**
1987
+ * Begin publishing a cohort's advert immediately and on a repeating interval
1988
+ * until the cohort's advert loop is stopped. Each advert is broadcast (no
1989
+ * recipient) via the transport's `publishRepeating` primitive.
1990
+ */
1991
+ #startAdvertRepeat(ctx, advertMsgs) {
1992
+ const stops = [];
1993
+ for (const msg of advertMsgs) {
1994
+ stops.push(this.#transport.publishRepeating(msg, this.#did, this.#advertRepeatIntervalMs));
1995
+ }
1996
+ ctx.stopAdvertRepeat = () => {
1997
+ for (const stop of stops) {
1998
+ try {
1999
+ stop();
2000
+ } catch {
2001
+ }
2002
+ }
2003
+ };
2004
+ }
2005
+ /** Stop a cohort's advert republish loop. Idempotent. */
2006
+ #stopAdvertRepeating(ctx) {
2007
+ if (!ctx.stopAdvertRepeat) return;
2008
+ const stop = ctx.stopAdvertRepeat;
2009
+ ctx.stopAdvertRepeat = void 0;
2010
+ stop();
2011
+ }
2012
+ /** Schedule a cohort's TTL + phase timeout when it is advertised. */
2013
+ #startTimers(ctx) {
2014
+ if (this.#cohortTtlMs !== void 0) {
2015
+ ctx.cohortTtlTimer = setTimeout(() => {
2016
+ const reason = `Cohort ${ctx.cohortId} exceeded TTL of ${this.#cohortTtlMs}ms`;
2017
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
2018
+ this.#failCohort(ctx, new Error(reason));
2019
+ }, this.#cohortTtlMs);
2020
+ }
2021
+ this.#resetPhaseTimer(ctx);
2022
+ }
2023
+ /** Reset a cohort's per-phase stall timer. Called when a phase transition is observed. */
2024
+ #resetPhaseTimer(ctx) {
2025
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
2026
+ ctx.phaseTimer = void 0;
2027
+ if (this.#phaseTimeoutMs === void 0) return;
2028
+ ctx.phaseTimer = setTimeout(() => {
2029
+ const phase = this.session.getCohortPhase(ctx.cohortId);
2030
+ const inSigning = phase !== void 0 && _AggregationServiceRunner.#SIGNING_PHASES.includes(phase);
2031
+ if (this.#autoFallbackOnStall && inSigning && !ctx.committedPath && !ctx.settled) {
2032
+ this.triggerFallback(ctx.cohortId).catch((err) => this.#failCohort(ctx, err));
2033
+ return;
2034
+ }
2035
+ const reason = `Cohort ${ctx.cohortId} stalled in phase ${ctx.lastObservedPhase ?? "?"} for ${this.#phaseTimeoutMs}ms`;
2036
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
2037
+ this.#failCohort(ctx, new Error(reason));
2038
+ }, this.#phaseTimeoutMs);
2039
+ }
2040
+ /**
2041
+ * Abandon the optimistic n-of-n key path for a cohort and collect k-of-n
2042
+ * fallback (script-path) signatures instead (ADR 042). Idempotent and safe
2043
+ * against the optimistic completion: it commits the cohort to the fallback
2044
+ * path synchronously (the `committedPath` latch) before sending anything, so a
2045
+ * late optimistic signature can no longer complete-and-broadcast a competing
2046
+ * spend of the same UTXO. No-op if the cohort is unknown, already settled, or
2047
+ * already committed to a path.
2048
+ *
2049
+ * Wired automatically to the phase-stall timer when `autoFallbackOnStall` is
2050
+ * set; otherwise call it from an operator decision (a UI "fall back now"
2051
+ * action). Throws only if the underlying state machine rejects the transition
2052
+ * (e.g. signing has not started).
2053
+ */
2054
+ async triggerFallback(cohortId) {
2055
+ const ctx = this.#contexts.get(cohortId);
2056
+ if (!ctx || ctx.settled || ctx.committedPath) return;
2057
+ const messages = this.session.startFallbackSigning(cohortId);
2058
+ ctx.committedPath = "fallback";
2059
+ this.#stopAdvertRepeating(ctx);
2060
+ this.#onPhaseMaybeChanged(ctx);
2061
+ const sessionId = this.session.getSigningSessionId(cohortId) ?? "";
2062
+ this.emit("fallback-started", { cohortId, sessionId });
2063
+ await this.#sendAll(messages);
2064
+ }
2065
+ /** Detect a phase change for a cohort since the last observation and reset its phase timer. */
2066
+ #onPhaseMaybeChanged(ctx) {
2067
+ const phase = this.session.getCohortPhase(ctx.cohortId);
2068
+ if (phase !== ctx.lastObservedPhase) {
2069
+ ctx.lastObservedPhase = phase;
2070
+ this.#resetPhaseTimer(ctx);
2071
+ }
2072
+ }
2073
+ /** Clear a cohort's timers. Called on completion, stop, and failure. */
2074
+ #clearTimers(ctx) {
2075
+ if (ctx.cohortTtlTimer) clearTimeout(ctx.cohortTtlTimer);
2076
+ if (ctx.phaseTimer) clearTimeout(ctx.phaseTimer);
2077
+ ctx.cohortTtlTimer = void 0;
2078
+ ctx.phaseTimer = void 0;
2079
+ }
2080
+ /**
2081
+ * Reclaim one cohort's runner-layer bookkeeping: stop its advert loop, clear
2082
+ * its timers, and drop its {@link RunContext}. Does NOT touch sibling cohorts
2083
+ * and does NOT detach the shared transport handlers. Leaves the cohort in the
2084
+ * state machine; whether that cohort's `session` state is also removed is the
2085
+ * caller's choice (see {@link #completeCohort} vs {@link #failCohort}).
2086
+ */
2087
+ #disposeCohort(ctx) {
2088
+ this.#stopAdvertRepeating(ctx);
2089
+ this.#clearTimers(ctx);
2090
+ this.#contexts.delete(ctx.cohortId);
2091
+ }
2092
+ /**
2093
+ * Settle one cohort successfully. Reclaims the runner context but leaves the
2094
+ * completed cohort in `session` so callers can read its beaconAddress / cohort
2095
+ * via `session.getCohort(result.cohortId)`; reclaim it with
2096
+ * `session.removeCohort(cohortId)` when done. Idempotent via `ctx.settled`.
2097
+ */
2098
+ #completeCohort(ctx, result) {
2099
+ if (ctx.settled) return;
2100
+ ctx.settled = true;
2101
+ this.#disposeCohort(ctx);
2102
+ this.emit("signing-complete", result);
2103
+ ctx.resolve(result);
2104
+ }
2105
+ /**
2106
+ * Fail one cohort. Reclaims its runner context, drops its now-dead state from
2107
+ * the state machine, and rejects only its completion; siblings keep running
2108
+ * and the shared transport handlers stay registered. Idempotent via
2109
+ * `ctx.settled`.
2110
+ */
2111
+ #failCohort(ctx, err) {
2112
+ if (ctx.settled) return;
2113
+ ctx.settled = true;
2114
+ this.#disposeCohort(ctx);
2115
+ this.session.removeCohort(ctx.cohortId);
2116
+ this.emit("error", err);
2117
+ ctx.reject(err);
2118
+ }
2119
+ /**
2120
+ * Stop a single cohort early without affecting the rest of the runner. Drops
2121
+ * the cohort's state machine state; its `completion` promise rejects with a
2122
+ * stopped error.
2123
+ */
2124
+ stopCohort(cohortId) {
2125
+ const ctx = this.#contexts.get(cohortId);
2126
+ if (!ctx || ctx.settled) return;
2127
+ ctx.settled = true;
2128
+ this.#disposeCohort(ctx);
2129
+ this.session.removeCohort(cohortId);
2130
+ ctx.reject(new AggregationServiceError(`Cohort ${cohortId} stopped.`, "COHORT_STOPPED", { cohortId }));
2131
+ }
2132
+ /**
2133
+ * Stop the whole runner. Fails every outstanding cohort, then detaches the
2134
+ * shared transport handlers so a restart or a new runner doesn't inherit
2135
+ * stale dispatch. Safe to call repeatedly.
2136
+ */
2137
+ stop() {
2138
+ this.#stopped = true;
2139
+ for (const ctx of [...this.#contexts.values()]) {
2140
+ if (ctx.settled) continue;
2141
+ ctx.settled = true;
2142
+ this.#disposeCohort(ctx);
2143
+ this.session.removeCohort(ctx.cohortId);
2144
+ ctx.reject(new AggregationServiceError("Service runner stopped.", "RUNNER_STOPPED", { cohortId: ctx.cohortId }));
2145
+ }
2146
+ this.#contexts.clear();
2147
+ this.#unregisterHandlers();
2148
+ }
2149
+ /** Message types this runner listens for on the transport. */
2150
+ static #HANDLED_MESSAGE_TYPES = [
2151
+ COHORT_OPT_IN,
2152
+ SUBMIT_UPDATE,
2153
+ SUBMIT_NONINCLUDED,
2154
+ VALIDATION_ACK,
2155
+ NONCE_CONTRIBUTION,
2156
+ SIGNATURE_AUTHORIZATION,
2157
+ FALLBACK_SIGNATURE
2158
+ ];
2159
+ /**
2160
+ * Internal: handler registration with the transport. Idempotent. Handlers
2161
+ * are DID-scoped and cohort-agnostic - one registration serves every cohort
2162
+ * this runner drives; demux to the right {@link RunContext} happens in each
2163
+ * handler via the inbound message's cohortId.
2164
+ */
2165
+ #registerHandlers() {
2166
+ if (this.#handlersRegistered) return;
2167
+ this.#handlersRegistered = true;
2168
+ this.#transport.registerMessageHandler(this.#did, COHORT_OPT_IN, this.#handleOptIn.bind(this));
2169
+ this.#transport.registerMessageHandler(this.#did, SUBMIT_UPDATE, this.#handleSubmitUpdate.bind(this));
2170
+ this.#transport.registerMessageHandler(this.#did, SUBMIT_NONINCLUDED, this.#handleSubmitUpdate.bind(this));
2171
+ this.#transport.registerMessageHandler(this.#did, VALIDATION_ACK, this.#handleValidationAck.bind(this));
2172
+ this.#transport.registerMessageHandler(this.#did, NONCE_CONTRIBUTION, this.#handleNonceContribution.bind(this));
2173
+ this.#transport.registerMessageHandler(this.#did, SIGNATURE_AUTHORIZATION, this.#handleSignatureAuthorization.bind(this));
2174
+ this.#transport.registerMessageHandler(this.#did, FALLBACK_SIGNATURE, this.#handleFallbackSignature.bind(this));
2175
+ }
2176
+ /** Internal: detach from the transport. Safe to call repeatedly. */
2177
+ #unregisterHandlers() {
2178
+ if (!this.#handlersRegistered) return;
2179
+ this.#handlersRegistered = false;
2180
+ for (const type of _AggregationServiceRunner.#HANDLED_MESSAGE_TYPES) {
2181
+ this.#transport.unregisterMessageHandler(this.#did, type);
2182
+ }
2183
+ }
2184
+ /**
2185
+ * Internal: message handlers for each protocol step. Each handler:
2186
+ * 1) resolves the cohort the message belongs to (by cohortId); ignores it if unknown
2187
+ * 2) feeds the message into the state machine via session.receive()
2188
+ * 3) emits a high-level event (carrying cohortId) for external observers
2189
+ * 4) checks if the new state triggers any automatic next steps, and if so:
2190
+ * a) calls the appropriate decision callback(s)
2191
+ * b) sends any resulting messages from the state machine
2192
+ * Errors fail only the owning cohort. A stopped runner ignores messages.
2193
+ * @param {BaseMessage} msg - The incoming message to handle.
2194
+ * @returns {Promise<void>} Resolves when handling is complete.
2195
+ */
2196
+ async #handleOptIn(msg) {
2197
+ if (this.#stopped) return;
2198
+ const ctx = this.#contextFor(msg);
2199
+ if (!ctx) return;
2200
+ try {
2201
+ this.session.receive(msg);
2202
+ this.#drainRejections(ctx);
2203
+ this.#onPhaseMaybeChanged(ctx);
2204
+ const optIn = this.session.pendingOptIns(ctx.cohortId).get(msg.from);
2205
+ if (!optIn) return;
2206
+ this.emit("opt-in-received", optIn);
2207
+ if (optIn.communicationPk) {
2208
+ this.#transport.registerPeer(msg.from, optIn.communicationPk);
2209
+ }
2210
+ const decision = await this.#onOptInReceived(optIn);
2211
+ if (!decision.accepted) return;
2212
+ const maxParticipants = ctx.config.maxParticipants;
2213
+ const cohortNow = this.session.getCohort(ctx.cohortId);
2214
+ if (maxParticipants !== void 0 && cohortNow && cohortNow.participants.length >= maxParticipants) {
2215
+ return;
2216
+ }
2217
+ await this.#sendAll(this.session.acceptParticipant(ctx.cohortId, msg.from));
2218
+ this.emit("participant-accepted", { cohortId: ctx.cohortId, participantDid: msg.from });
2219
+ const cohort = this.session.getCohort(ctx.cohortId);
2220
+ if (cohort.participants.length >= ctx.config.minParticipants && !ctx.finalizing) {
2221
+ ctx.finalizing = true;
2222
+ const finalizeDecision = await this.#onReadyToFinalize({
2223
+ acceptedCount: cohort.participants.length,
2224
+ minRequired: ctx.config.minParticipants
2225
+ });
2226
+ if (!finalizeDecision.finalize) {
2227
+ ctx.finalizing = false;
2228
+ return;
2229
+ }
2230
+ const readyMsgs = this.session.finalizeKeygen(ctx.cohortId);
2231
+ this.#stopAdvertRepeating(ctx);
2232
+ this.emit("keygen-complete", {
2233
+ cohortId: ctx.cohortId,
2234
+ beaconAddress: cohort.beaconAddress
2235
+ });
2236
+ await this.#sendAll(readyMsgs);
2237
+ }
2238
+ } catch (err) {
2239
+ this.#failCohort(ctx, err);
2240
+ }
2241
+ }
2242
+ /**
2243
+ * Handler for receiving participant updates. When all updates are received, automatically builds
2244
+ * and distributes the data for validation.
2245
+ * @param {BaseMessage} msg - The incoming message to handle.
2246
+ * @returns {Promise<void>} Resolves when handling is complete.
2247
+ */
2248
+ async #handleSubmitUpdate(msg) {
2249
+ if (this.#stopped) return;
2250
+ const ctx = this.#contextFor(msg);
2251
+ if (!ctx) return;
2252
+ try {
2253
+ this.session.receive(msg);
2254
+ this.#drainRejections(ctx);
2255
+ this.#onPhaseMaybeChanged(ctx);
2256
+ this.emit("update-received", { cohortId: ctx.cohortId, participantDid: msg.from });
2257
+ if (this.session.getCohortPhase(ctx.cohortId) === "UpdatesCollected" /* UpdatesCollected */) {
2258
+ const distributeMsgs = this.session.buildAndDistribute(ctx.cohortId);
2259
+ this.emit("data-distributed", { cohortId: ctx.cohortId });
2260
+ await this.#sendAll(distributeMsgs);
2261
+ }
2262
+ } catch (err) {
2263
+ this.#failCohort(ctx, err);
2264
+ }
2265
+ }
2266
+ /**
2267
+ * Handler for receiving validation acknowledgments. When all validations are received,
2268
+ * automatically requests tx data and starts signing.
2269
+ * @param {BaseMessage} msg - The incoming message to handle.
2270
+ * @returns {Promise<void>} Resolves when handling is complete.
2271
+ */
2272
+ async #handleValidationAck(msg) {
2273
+ if (this.#stopped) return;
2274
+ const ctx = this.#contextFor(msg);
2275
+ if (!ctx) return;
2276
+ try {
2277
+ this.session.receive(msg);
2278
+ this.#drainRejections(ctx);
2279
+ this.#onPhaseMaybeChanged(ctx);
2280
+ const approved = !!msg.body?.approved;
2281
+ this.emit("validation-received", { cohortId: ctx.cohortId, participantDid: msg.from, approved });
2282
+ const phase = this.session.getCohortPhase(ctx.cohortId);
2283
+ if (phase === "Failed" /* Failed */) {
2284
+ const reason = `Validation rejected by participant ${msg.from}`;
2285
+ this.emit("cohort-failed", { cohortId: ctx.cohortId, reason });
2286
+ this.#failCohort(ctx, new Error(reason));
2287
+ return;
2288
+ }
2289
+ if (phase === "Validated" /* Validated */) {
2290
+ const cohort = this.session.getCohort(ctx.cohortId);
2291
+ const txData = await this.#onProvideTxData({
2292
+ cohortId: ctx.cohortId,
2293
+ beaconAddress: cohort.beaconAddress,
2294
+ signalBytes: cohort.signalBytes,
2295
+ feeEstimator: this.#feeEstimator
2296
+ });
2297
+ const authMsgs = this.session.startSigning(ctx.cohortId, txData);
2298
+ const sessionId = this.session.getSigningSessionId(ctx.cohortId) ?? "";
2299
+ this.emit("signing-started", { cohortId: ctx.cohortId, sessionId });
2300
+ await this.#sendAll(authMsgs);
2301
+ }
2302
+ } catch (err) {
2303
+ this.#failCohort(ctx, err);
2304
+ }
2305
+ }
2306
+ /**
2307
+ * Handler for receiving nonce contributions. When all nonces are received, sends the aggregated
2308
+ * nonce back to the cohort.
2309
+ * @param {BaseMessage} msg - The incoming message to handle.
2310
+ * @returns {Promise<void>} Resolves when handling is complete.
2311
+ */
2312
+ async #handleNonceContribution(msg) {
2313
+ if (this.#stopped) return;
2314
+ const ctx = this.#contextFor(msg);
2315
+ if (!ctx) return;
2316
+ try {
2317
+ this.session.receive(msg);
2318
+ this.#drainRejections(ctx);
2319
+ this.#onPhaseMaybeChanged(ctx);
2320
+ this.emit("nonce-received", { cohortId: ctx.cohortId, participantDid: msg.from });
2321
+ if (this.session.getCohortPhase(ctx.cohortId) === "NoncesCollected" /* NoncesCollected */) {
2322
+ await this.#sendAll(this.session.sendAggregatedNonce(ctx.cohortId));
2323
+ }
2324
+ } catch (err) {
2325
+ this.#failCohort(ctx, err);
2326
+ }
2327
+ }
2328
+ /**
2329
+ * Handler for receiving signature authorizations. When all partial signatures are received, the
2330
+ * session automatically completes; the final result is emitted and the cohort's completion
2331
+ * promise resolves.
2332
+ * @param {BaseMessage} msg - The incoming message to handle.
2333
+ * @returns {Promise<void>} Resolves when handling is complete.
2334
+ */
2335
+ async #handleSignatureAuthorization(msg) {
2336
+ if (this.#stopped) return;
2337
+ const ctx = this.#contextFor(msg);
2338
+ if (!ctx) return;
2339
+ try {
2340
+ this.session.receive(msg);
2341
+ this.#drainRejections(ctx);
2342
+ this.#onPhaseMaybeChanged(ctx);
2343
+ if (ctx.committedPath === "fallback") return;
2344
+ const result = this.session.getResult(ctx.cohortId);
2345
+ if (result) {
2346
+ ctx.committedPath = "optimistic";
2347
+ this.#completeCohort(ctx, result);
2348
+ }
2349
+ } catch (err) {
2350
+ this.#failCohort(ctx, err);
2351
+ }
2352
+ }
2353
+ /**
2354
+ * Handler for receiving fallback (k-of-n script-path) signatures. The state
2355
+ * machine assembles and finalizes the fallback spend once k valid signatures
2356
+ * are in; the result is then emitted and the cohort's completion resolves. The
2357
+ * cohort is already committed to the fallback path (via {@link triggerFallback}).
2358
+ * @param {BaseMessage} msg - The incoming FALLBACK_SIGNATURE message.
2359
+ * @returns {Promise<void>} Resolves when handling is complete.
2360
+ */
2361
+ async #handleFallbackSignature(msg) {
2362
+ if (this.#stopped) return;
2363
+ const ctx = this.#contextFor(msg);
2364
+ if (!ctx) return;
2365
+ try {
2366
+ this.session.receive(msg);
2367
+ this.#drainRejections(ctx);
2368
+ this.#onPhaseMaybeChanged(ctx);
2369
+ const result = this.session.getResult(ctx.cohortId);
2370
+ if (result) {
2371
+ this.#completeCohort(ctx, result);
2372
+ }
2373
+ } catch (err) {
2374
+ this.#failCohort(ctx, err);
2375
+ }
2376
+ }
2377
+ /**
2378
+ * Internal: helper to send all messages sequentially. Catches and propagates errors.
2379
+ * @param {BaseMessage[]} msgs - The messages to send.
2380
+ * @returns {Promise<void>} Resolves when all messages have been sent.
2381
+ */
2382
+ async #sendAll(msgs) {
2383
+ for (const m of msgs) {
2384
+ await this.#transport.sendMessage(m, this.#did, m.to);
2385
+ }
2386
+ }
2387
+ };
2388
+
2389
+ // src/service/http-server.ts
2390
+ var import_keypair2 = require("@did-btcr2/keypair");
2391
+
2392
+ // src/core/logger.ts
2393
+ var CONSOLE_LOGGER = {
2394
+ debug: (msg, ...args) => console.debug(msg, ...args),
2395
+ info: (msg, ...args) => console.info(msg, ...args),
2396
+ warn: (msg, ...args) => console.warn(msg, ...args),
2397
+ error: (msg, ...args) => console.error(msg, ...args)
2398
+ };
2399
+
2400
+ // src/core/transport/http/envelope.ts
2401
+ var import_common6 = require("@did-btcr2/common");
2402
+ var import_utils4 = require("@noble/hashes/utils");
2403
+
2404
+ // src/core/transport/error.ts
2405
+ var import_common5 = require("@did-btcr2/common");
2406
+ var TransportAdapterError = class extends import_common5.MethodError {
2407
+ constructor(message, type = "TransportAdapterError", data) {
2408
+ super(message, type, data);
2409
+ }
2410
+ };
2411
+
2412
+ // src/core/transport/http/errors.ts
2413
+ var HttpTransportError = class extends TransportAdapterError {
2414
+ constructor(message, type = "HttpTransportError", data) {
2415
+ super(message, type, { adapter: "http", ...data ?? {} });
2416
+ }
2417
+ };
2418
+
2419
+ // src/core/transport/http/protocol.ts
2420
+ var HTTP_ENVELOPE_VERSION = 1;
2421
+ var HTTP_ROUTE = {
2422
+ ADVERTS: "/v1/adverts",
2423
+ MESSAGES: "/v1/messages",
2424
+ ACTOR_INBOX: "/v1/actors/{did}/inbox",
2425
+ WELL_KNOWN: "/v1/.well-known/aggregation"
2426
+ };
2427
+ var SSE_EVENT = {
2428
+ ADVERT: "advert",
2429
+ MESSAGE: "message",
2430
+ HEARTBEAT: "heartbeat"
2431
+ };
2432
+ var DEFAULT_CLOCK_SKEW_SEC = 60;
2433
+ var DEFAULT_NONCE_LEN_BYTES = 16;
2434
+
2435
+ // src/core/transport/http/envelope.ts
2436
+ function signEnvelope(message, sender, opts = {}) {
2437
+ const timestamp = opts.timestamp ?? Math.floor(Date.now() / 1e3);
2438
+ const nonce = opts.nonce ?? (0, import_utils4.bytesToHex)((0, import_utils4.randomBytes)(DEFAULT_NONCE_LEN_BYTES));
2439
+ const messageJson = normalizeForWire(normalizeMessage(message));
2440
+ const unsigned = {
2441
+ v: HTTP_ENVELOPE_VERSION,
2442
+ from: sender.did,
2443
+ ...opts.to !== void 0 ? { to: opts.to } : {},
2444
+ timestamp,
2445
+ nonce,
2446
+ message: messageJson
2447
+ };
2448
+ const digest = (0, import_common6.canonicalHashBytes)(unsigned);
2449
+ const sig = sender.keys.secretKey.sign(digest, { scheme: "schnorr" });
2450
+ return { ...unsigned, sig: (0, import_utils4.bytesToHex)(sig) };
2451
+ }
2452
+ function verifyEnvelope(envelope, senderPk, opts = {}) {
2453
+ if (envelope.v !== HTTP_ENVELOPE_VERSION) {
2454
+ throw new HttpTransportError(
2455
+ `Unsupported envelope version: ${envelope.v}`,
2456
+ "ENVELOPE_VERSION_MISMATCH",
2457
+ { version: envelope.v, expected: HTTP_ENVELOPE_VERSION }
2458
+ );
2459
+ }
2460
+ if (opts.expectedFrom !== void 0 && envelope.from !== opts.expectedFrom) {
2461
+ throw new HttpTransportError(
2462
+ `Envelope from mismatch: expected ${opts.expectedFrom}, got ${envelope.from}`,
2463
+ "ENVELOPE_FROM_MISMATCH",
2464
+ { expected: opts.expectedFrom, got: envelope.from }
2465
+ );
2466
+ }
2467
+ if ("expectedTo" in opts && envelope.to !== opts.expectedTo) {
2468
+ throw new HttpTransportError(
2469
+ `Envelope to mismatch: expected ${opts.expectedTo ?? "<broadcast>"}, got ${envelope.to ?? "<broadcast>"}`,
2470
+ "ENVELOPE_TO_MISMATCH",
2471
+ { expected: opts.expectedTo, got: envelope.to }
2472
+ );
2473
+ }
2474
+ const skewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
2475
+ const nowMs = opts.now ? opts.now() : Date.now();
2476
+ const nowSec = Math.floor(nowMs / 1e3);
2477
+ const diff = Math.abs(nowSec - envelope.timestamp);
2478
+ if (diff > skewSec) {
2479
+ throw new HttpTransportError(
2480
+ `Envelope timestamp out of skew: ${diff}s > ${skewSec}s`,
2481
+ "ENVELOPE_TIMESTAMP_SKEW",
2482
+ { diff, skewSec, timestamp: envelope.timestamp, now: nowSec }
2483
+ );
2484
+ }
2485
+ let sigBytes;
2486
+ try {
2487
+ sigBytes = (0, import_utils4.hexToBytes)(envelope.sig);
2488
+ } catch {
2489
+ throw new HttpTransportError(
2490
+ "Envelope signature is not valid hex",
2491
+ "ENVELOPE_SIG_HEX"
2492
+ );
2493
+ }
2494
+ if (sigBytes.length !== 64) {
2495
+ throw new HttpTransportError(
2496
+ `Invalid signature length: ${sigBytes.length} (expected 64)`,
2497
+ "ENVELOPE_SIG_LENGTH",
2498
+ { length: sigBytes.length }
2499
+ );
2500
+ }
2501
+ const { sig: _sig, ...unsigned } = envelope;
2502
+ const digest = (0, import_common6.canonicalHashBytes)(unsigned);
2503
+ const ok = senderPk.verify(sigBytes, digest, { scheme: "schnorr" });
2504
+ if (!ok) {
2505
+ throw new HttpTransportError(
2506
+ "Envelope signature verification failed",
2507
+ "ENVELOPE_SIG_INVALID"
2508
+ );
2509
+ }
2510
+ }
2511
+ function normalizeMessage(message) {
2512
+ const maybeToJSON = message.toJSON;
2513
+ if (typeof maybeToJSON === "function") {
2514
+ return maybeToJSON.call(message);
2515
+ }
2516
+ return message;
2517
+ }
2518
+ function normalizeForWire(value) {
2519
+ if (value instanceof Uint8Array) {
2520
+ return { __bytes: (0, import_utils4.bytesToHex)(value) };
2521
+ }
2522
+ if (Array.isArray(value)) {
2523
+ return value.map((v) => normalizeForWire(v));
2524
+ }
2525
+ if (value && typeof value === "object") {
2526
+ const out = {};
2527
+ for (const [k, v] of Object.entries(value)) {
2528
+ out[k] = normalizeForWire(v);
2529
+ }
2530
+ return out;
2531
+ }
2532
+ return value;
2533
+ }
2534
+ function reviveFromWire(value) {
2535
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2536
+ const rec = value;
2537
+ const keys = Object.keys(rec);
2538
+ if (keys.length === 1 && keys[0] === "__bytes" && typeof rec.__bytes === "string") {
2539
+ return (0, import_utils4.hexToBytes)(rec.__bytes);
2540
+ }
2541
+ const out = {};
2542
+ for (const [k, v] of Object.entries(rec)) out[k] = reviveFromWire(v);
2543
+ return out;
2544
+ }
2545
+ if (Array.isArray(value)) {
2546
+ return value.map((v) => reviveFromWire(v));
2547
+ }
2548
+ return value;
2549
+ }
2550
+
2551
+ // src/service/inbox-buffer.ts
2552
+ var InboxBuffer = class {
2553
+ #capacity;
2554
+ #entries = [];
2555
+ #nextId = 1;
2556
+ constructor(capacity = 100) {
2557
+ if (capacity < 1) throw new Error(`InboxBuffer capacity must be >= 1; got ${capacity}`);
2558
+ this.#capacity = capacity;
2559
+ }
2560
+ /** Append an event. Returns the stored record (including its assigned id). */
2561
+ append(event, data) {
2562
+ const stored = { id: String(this.#nextId++), event, data };
2563
+ this.#entries.push(stored);
2564
+ if (this.#entries.length > this.#capacity) this.#entries.shift();
2565
+ return stored;
2566
+ }
2567
+ /**
2568
+ * Return stored events with id strictly greater than `lastEventId`. If
2569
+ * `lastEventId` is unset or unparseable, returns everything currently
2570
+ * retained.
2571
+ */
2572
+ since(lastEventId) {
2573
+ if (!lastEventId) return this.#entries.slice();
2574
+ const boundary = Number(lastEventId);
2575
+ if (!Number.isFinite(boundary)) return this.#entries.slice();
2576
+ return this.#entries.filter((e) => Number(e.id) > boundary);
2577
+ }
2578
+ /** Currently retained event count. */
2579
+ size() {
2580
+ return this.#entries.length;
2581
+ }
2582
+ };
2583
+
2584
+ // src/service/nonce-cache.ts
2585
+ var NonceCache = class {
2586
+ #maxEntries;
2587
+ #entries = /* @__PURE__ */ new Map();
2588
+ constructor(config = {}) {
2589
+ this.#maxEntries = config.maxEntries ?? 1e4;
2590
+ }
2591
+ /**
2592
+ * Record a nonce. Returns `true` if it was novel (caller should accept the
2593
+ * request) or `false` if it was a replay (caller should reject).
2594
+ */
2595
+ store(did, nonce, timestampSec) {
2596
+ const key = `${did}:${nonce}`;
2597
+ if (this.#entries.has(key)) return false;
2598
+ this.#entries.set(key, timestampSec);
2599
+ if (this.#entries.size > this.#maxEntries) {
2600
+ const oldest = this.#entries.keys().next();
2601
+ if (!oldest.done) this.#entries.delete(oldest.value);
2602
+ }
2603
+ return true;
2604
+ }
2605
+ /** Current cache size. Exposed for observability and tests. */
2606
+ size() {
2607
+ return this.#entries.size;
2608
+ }
2609
+ };
2610
+
2611
+ // src/service/rate-limiter.ts
2612
+ var InMemoryRateLimitStore = class {
2613
+ #buckets = /* @__PURE__ */ new Map();
2614
+ get(key) {
2615
+ return this.#buckets.get(key);
2616
+ }
2617
+ set(key, state) {
2618
+ this.#buckets.set(key, state);
2619
+ }
2620
+ };
2621
+ var RateLimiter = class {
2622
+ #rps;
2623
+ #burst;
2624
+ #store;
2625
+ constructor(config = {}) {
2626
+ this.#rps = config.rps ?? 10;
2627
+ this.#burst = config.burst ?? 30;
2628
+ this.#store = config.store ?? new InMemoryRateLimitStore();
2629
+ }
2630
+ /** Consume one token for `key`. Returns `true` if accepted, `false` if throttled. */
2631
+ consume(key, nowMs) {
2632
+ const existing = this.#store.get(key);
2633
+ const state = existing ?? { tokens: this.#burst, lastRefillMs: nowMs };
2634
+ if (existing) {
2635
+ const elapsedSec = Math.max(0, (nowMs - existing.lastRefillMs) / 1e3);
2636
+ state.tokens = Math.min(this.#burst, existing.tokens + elapsedSec * this.#rps);
2637
+ state.lastRefillMs = nowMs;
2638
+ }
2639
+ if (state.tokens < 1) {
2640
+ this.#store.set(key, state);
2641
+ return false;
2642
+ }
2643
+ state.tokens -= 1;
2644
+ this.#store.set(key, state);
2645
+ return true;
2646
+ }
2647
+ };
2648
+
2649
+ // src/core/transport/http/request-auth.ts
2650
+ var import_common7 = require("@did-btcr2/common");
2651
+ var import_utils5 = require("@noble/hashes/utils");
2652
+ var REQUEST_AUTH_SCHEME = "BTCR2-Sig";
2653
+ function parseRequestAuth(headerValue) {
2654
+ const prefix = `${REQUEST_AUTH_SCHEME} `;
2655
+ if (!headerValue.startsWith(prefix)) {
2656
+ throw new HttpTransportError(
2657
+ `Unexpected auth scheme (want ${REQUEST_AUTH_SCHEME})`,
2658
+ "REQUEST_AUTH_SCHEME"
2659
+ );
2660
+ }
2661
+ const params = {};
2662
+ for (const piece of headerValue.slice(prefix.length).split(",")) {
2663
+ const eq = piece.indexOf("=");
2664
+ if (eq === -1) continue;
2665
+ const key = piece.slice(0, eq).trim();
2666
+ const val = piece.slice(eq + 1).trim();
2667
+ if (key.length > 0) params[key] = val;
2668
+ }
2669
+ const v = Number(params.v);
2670
+ const ts = Number(params.ts);
2671
+ if (!Number.isInteger(v) || !Number.isInteger(ts) || !params.did || !params.nonce || !params.sig) {
2672
+ throw new HttpTransportError(
2673
+ "Malformed auth header (missing or invalid field)",
2674
+ "REQUEST_AUTH_MALFORMED",
2675
+ { received: Object.keys(params) }
2676
+ );
2677
+ }
2678
+ return { v, did: params.did, ts, nonce: params.nonce, sig: params.sig };
2679
+ }
2680
+ function verifyRequestAuth(headerValue, expectedPath, senderPk, opts = {}) {
2681
+ const parsed = parseRequestAuth(headerValue);
2682
+ if (parsed.v !== HTTP_ENVELOPE_VERSION) {
2683
+ throw new HttpTransportError(
2684
+ `Unsupported auth version: ${parsed.v}`,
2685
+ "REQUEST_AUTH_VERSION_MISMATCH",
2686
+ { version: parsed.v, expected: HTTP_ENVELOPE_VERSION }
2687
+ );
2688
+ }
2689
+ const skewSec = opts.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
2690
+ const nowMs = opts.now ? opts.now() : Date.now();
2691
+ const nowSec = Math.floor(nowMs / 1e3);
2692
+ const diff = Math.abs(nowSec - parsed.ts);
2693
+ if (diff > skewSec) {
2694
+ throw new HttpTransportError(
2695
+ `Auth timestamp out of skew: ${diff}s > ${skewSec}s`,
2696
+ "REQUEST_AUTH_TIMESTAMP_SKEW",
2697
+ { diff, skewSec }
2698
+ );
2699
+ }
2700
+ let sigBytes;
2701
+ try {
2702
+ sigBytes = (0, import_utils5.hexToBytes)(parsed.sig);
2703
+ } catch {
2704
+ throw new HttpTransportError("Auth signature is not valid hex", "REQUEST_AUTH_SIG_HEX");
2705
+ }
2706
+ if (sigBytes.length !== 64) {
2707
+ throw new HttpTransportError(
2708
+ `Invalid auth signature length: ${sigBytes.length}`,
2709
+ "REQUEST_AUTH_SIG_LENGTH",
2710
+ { length: sigBytes.length }
2711
+ );
2712
+ }
2713
+ const digest = (0, import_common7.canonicalHashBytes)({
2714
+ v: parsed.v,
2715
+ did: parsed.did,
2716
+ ts: parsed.ts,
2717
+ nonce: parsed.nonce,
2718
+ path: expectedPath
2719
+ });
2720
+ const ok = senderPk.verify(sigBytes, digest, { scheme: "schnorr" });
2721
+ if (!ok) {
2722
+ throw new HttpTransportError("Auth signature verification failed", "REQUEST_AUTH_SIG_INVALID");
2723
+ }
2724
+ return parsed;
2725
+ }
2726
+
2727
+ // src/service/http-server.ts
2728
+ var INBOX_PATH_PREFIX = "/v1/actors/";
2729
+ var INBOX_PATH_SUFFIX = "/inbox";
2730
+ var DEFAULT_ADVERT_TTL_MS = 5 * 60 * 1e3;
2731
+ var DEFAULT_HEARTBEAT_MS = 2e4;
2732
+ var HttpServerTransport = class {
2733
+ name = "http";
2734
+ #logger;
2735
+ #cors;
2736
+ #clockSkewSec;
2737
+ #inboxBufferSize;
2738
+ #advertTtlMs;
2739
+ #heartbeatMs;
2740
+ #rateLimiter;
2741
+ #nonceCache;
2742
+ #now;
2743
+ #resolveSenderPkFn;
2744
+ #actors = /* @__PURE__ */ new Map();
2745
+ #peers = /* @__PURE__ */ new Map();
2746
+ #inboxes = /* @__PURE__ */ new Map();
2747
+ #broadcastSubscribers = /* @__PURE__ */ new Set();
2748
+ #currentAdvert;
2749
+ #advertSeq = 0;
2750
+ constructor(config = {}) {
2751
+ this.#logger = config.logger ?? CONSOLE_LOGGER;
2752
+ this.#cors = config.cors ?? { mode: "permissive" };
2753
+ this.#clockSkewSec = config.clockSkewSec ?? DEFAULT_CLOCK_SKEW_SEC;
2754
+ this.#inboxBufferSize = config.inboxBufferSize ?? 100;
2755
+ this.#advertTtlMs = config.advertTtlMs ?? DEFAULT_ADVERT_TTL_MS;
2756
+ this.#heartbeatMs = config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_MS;
2757
+ this.#rateLimiter = config.rateLimiter ?? new RateLimiter();
2758
+ this.#nonceCache = config.nonceCache ?? new NonceCache();
2759
+ this.#now = config.now ?? (() => Date.now());
2760
+ this.#resolveSenderPkFn = config.resolveSenderPk;
2761
+ }
2762
+ // ----------------------------------------------------------------
2763
+ // Transport interface
2764
+ // ----------------------------------------------------------------
2765
+ start() {
2766
+ }
2767
+ /**
2768
+ * Detach the transport: close every open SSE subscription, clear the advert
2769
+ * cache, and drop all actor / peer / inbox state. Intended for shutdown and
2770
+ * for test teardown.
2771
+ */
2772
+ stop() {
2773
+ for (const sub of this.#broadcastSubscribers) this.#closeBroadcastSubscriber(sub);
2774
+ this.#broadcastSubscribers.clear();
2775
+ for (const inbox of this.#inboxes.values()) {
2776
+ for (const sub of inbox.subscribers) this.#closeInboxSubscriber(sub);
2777
+ inbox.subscribers.clear();
2778
+ }
2779
+ this.#inboxes.clear();
2780
+ this.#currentAdvert = void 0;
2781
+ }
2782
+ registerActor(did, keys) {
2783
+ this.#actors.set(did, { keys, handlers: /* @__PURE__ */ new Map() });
2784
+ }
2785
+ unregisterActor(did) {
2786
+ this.#actors.delete(did);
2787
+ this.#peers.delete(did);
2788
+ }
2789
+ getActorPk(did) {
2790
+ return this.#actors.get(did)?.keys.publicKey.compressed;
2791
+ }
2792
+ registerPeer(did, communicationPk) {
2793
+ try {
2794
+ new import_keypair2.CompressedSecp256k1PublicKey(communicationPk);
2795
+ } catch {
2796
+ throw new HttpTransportError(
2797
+ `Invalid peer public key for ${did}`,
2798
+ "INVALID_PEER_KEY",
2799
+ { did, keyLength: communicationPk.length }
2800
+ );
2801
+ }
2802
+ this.#peers.set(did, communicationPk);
2803
+ }
2804
+ getPeerPk(did) {
2805
+ return this.#peers.get(did);
2806
+ }
2807
+ registerMessageHandler(actorDid, messageType, handler) {
2808
+ const actor = this.#actors.get(actorDid);
2809
+ if (!actor) {
2810
+ throw new HttpTransportError(
2811
+ `Cannot register handler: actor ${actorDid} not registered`,
2812
+ "UNKNOWN_ACTOR",
2813
+ { did: actorDid }
2814
+ );
2815
+ }
2816
+ actor.handlers.set(messageType, handler);
2817
+ }
2818
+ unregisterMessageHandler(actorDid, messageType) {
2819
+ this.#actors.get(actorDid)?.handlers.delete(messageType);
2820
+ }
2821
+ async sendMessage(message, sender, recipient) {
2822
+ if (!recipient) {
2823
+ throw new HttpTransportError(
2824
+ "HttpServerTransport.sendMessage requires a recipient. Use publishRepeating for broadcasts.",
2825
+ "MISSING_RECIPIENT",
2826
+ { messageType: message.type }
2827
+ );
2828
+ }
2829
+ const actor = this.#actors.get(sender);
2830
+ if (!actor) {
2831
+ throw new HttpTransportError(
2832
+ `Unknown sender: ${sender}`,
2833
+ "UNKNOWN_SENDER",
2834
+ { did: sender }
2835
+ );
2836
+ }
2837
+ const envelope = signEnvelope(message, { did: sender, keys: actor.keys }, { to: recipient });
2838
+ const dataJson = JSON.stringify(envelope);
2839
+ const inbox = this.#getOrCreateInbox(recipient);
2840
+ const stored = inbox.buffer.append(SSE_EVENT.MESSAGE, dataJson);
2841
+ for (const sub of inbox.subscribers) {
2842
+ this.#safeWrite(sub.stream, stored.event, stored.data, stored.id);
2843
+ }
2844
+ }
2845
+ publishRepeating(message, sender, _intervalMs, _recipient) {
2846
+ const actor = this.#actors.get(sender);
2847
+ if (!actor) {
2848
+ throw new HttpTransportError(`Unknown sender: ${sender}`, "UNKNOWN_SENDER", { did: sender });
2849
+ }
2850
+ const envelope = signEnvelope(message, { did: sender, keys: actor.keys });
2851
+ const dataJson = JSON.stringify(envelope);
2852
+ const id = String(++this.#advertSeq);
2853
+ const expiresAtMs = this.#now() + this.#advertTtlMs;
2854
+ this.#currentAdvert = { dataJson, id, expiresAtMs };
2855
+ for (const sub of this.#broadcastSubscribers) {
2856
+ this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, dataJson, id);
2857
+ }
2858
+ return () => {
2859
+ if (this.#currentAdvert?.id === id) this.#currentAdvert = void 0;
2860
+ };
2861
+ }
2862
+ // ----------------------------------------------------------------
2863
+ // Sans-I/O HTTP surface
2864
+ // ----------------------------------------------------------------
2865
+ /**
2866
+ * Handle a POST / GET request (non-SSE). The caller dispatches SSE paths to
2867
+ * {@link handleSse} instead. Returns a fully formed response; the caller's
2868
+ * adapter turns it into an HTTP write.
2869
+ */
2870
+ async handleRequest(req) {
2871
+ const method = req.method.toUpperCase();
2872
+ if (method === "OPTIONS") return this.#respond(204, "", req);
2873
+ const path = extractPath(req.url);
2874
+ if (method === "GET" && path === HTTP_ROUTE.WELL_KNOWN) {
2875
+ return this.#respondJson(200, this.#wellKnownMetadata(), req);
2876
+ }
2877
+ if (method === "POST" && path === HTTP_ROUTE.MESSAGES) {
2878
+ return await this.#handleMessagesPost(req);
2879
+ }
2880
+ if (method === "POST" && path === HTTP_ROUTE.ADVERTS) {
2881
+ return await this.#handleAdvertsPost(req);
2882
+ }
2883
+ return this.#respondJson(404, { error: "not_found" }, req);
2884
+ }
2885
+ /**
2886
+ * Open an SSE stream for a GET request. The caller is responsible for
2887
+ * flushing writes and propagating the `onClose` callback when the HTTP
2888
+ * connection ends.
2889
+ */
2890
+ handleSse(req, stream) {
2891
+ if (req.method.toUpperCase() !== "GET") {
2892
+ stream.close();
2893
+ return;
2894
+ }
2895
+ const path = extractPath(req.url);
2896
+ if (path === HTTP_ROUTE.ADVERTS) {
2897
+ this.#openBroadcastSubscription(stream);
2898
+ return;
2899
+ }
2900
+ const inboxMatch = matchInboxPath(path);
2901
+ if (inboxMatch) {
2902
+ this.#openInboxSubscription(req, stream, inboxMatch.did, path);
2903
+ return;
2904
+ }
2905
+ stream.close();
2906
+ }
2907
+ // ----------------------------------------------------------------
2908
+ // Request handlers
2909
+ // ----------------------------------------------------------------
2910
+ async #handleMessagesPost(req) {
2911
+ const envelope = parseJsonBody(req.body);
2912
+ if (!envelope) return this.#respondJson(400, { error: "invalid_json" }, req);
2913
+ const senderPk = this.#resolveSenderPk(envelope.from);
2914
+ if (!senderPk) {
2915
+ return this.#respondJson(401, { error: "unknown_sender" }, req);
2916
+ }
2917
+ try {
2918
+ verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
2919
+ } catch (err) {
2920
+ this.#logger.debug("POST /v1/messages: envelope verification failed:", err);
2921
+ return this.#respondJson(401, { error: "invalid_envelope" }, req);
2922
+ }
2923
+ if (!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
2924
+ return this.#respondJson(409, { error: "replay" }, req);
2925
+ }
2926
+ if (!this.#rateLimiter.consume(envelope.from, this.#now())) {
2927
+ return this.#respondJson(429, { error: "rate_limited" }, req);
2928
+ }
2929
+ if (!envelope.to) {
2930
+ return this.#respondJson(400, { error: "missing_recipient" }, req);
2931
+ }
2932
+ const actor = this.#actors.get(envelope.to);
2933
+ if (!actor) {
2934
+ return this.#respondJson(404, { error: "unknown_recipient" }, req);
2935
+ }
2936
+ const revived = reviveFromWire(envelope.message);
2937
+ const flat = flattenMessage(revived);
2938
+ const messageType = typeof flat.type === "string" ? flat.type : void 0;
2939
+ if (!messageType) return this.#respondJson(400, { error: "missing_message_type" }, req);
2940
+ const handler = actor.handlers.get(messageType);
2941
+ if (handler) {
2942
+ try {
2943
+ await handler(flat);
2944
+ } catch (err) {
2945
+ this.#logger.debug(`Handler threw for ${messageType}:`, err);
2946
+ }
2947
+ }
2948
+ return this.#respondJson(202, { ok: true }, req);
2949
+ }
2950
+ async #handleAdvertsPost(req) {
2951
+ const envelope = parseJsonBody(req.body);
2952
+ if (!envelope) return this.#respondJson(400, { error: "invalid_json" }, req);
2953
+ const senderPk = this.#resolveSenderPk(envelope.from);
2954
+ if (!senderPk) return this.#respondJson(401, { error: "unknown_sender" }, req);
2955
+ try {
2956
+ verifyEnvelope(envelope, senderPk, { clockSkewSec: this.#clockSkewSec });
2957
+ } catch {
2958
+ return this.#respondJson(401, { error: "invalid_envelope" }, req);
2959
+ }
2960
+ if (!this.#nonceCache.store(envelope.from, envelope.nonce, envelope.timestamp)) {
2961
+ return this.#respondJson(409, { error: "replay" }, req);
2962
+ }
2963
+ if (!this.#rateLimiter.consume(envelope.from, this.#now())) {
2964
+ return this.#respondJson(429, { error: "rate_limited" }, req);
2965
+ }
2966
+ if (!this.#actors.has(envelope.from)) {
2967
+ return this.#respondJson(403, { error: "not_an_actor" }, req);
2968
+ }
2969
+ const id = String(++this.#advertSeq);
2970
+ this.#currentAdvert = {
2971
+ dataJson: JSON.stringify(envelope),
2972
+ id,
2973
+ expiresAtMs: this.#now() + this.#advertTtlMs
2974
+ };
2975
+ for (const sub of this.#broadcastSubscribers) {
2976
+ this.#safeWrite(sub.stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, id);
2977
+ }
2978
+ return this.#respondJson(202, { ok: true }, req);
2979
+ }
2980
+ #openBroadcastSubscription(stream) {
2981
+ const sub = { stream };
2982
+ this.#broadcastSubscribers.add(sub);
2983
+ stream.onClose(() => {
2984
+ this.#closeBroadcastSubscriber(sub);
2985
+ this.#broadcastSubscribers.delete(sub);
2986
+ });
2987
+ if (this.#currentAdvert && this.#currentAdvert.expiresAtMs > this.#now()) {
2988
+ this.#safeWrite(stream, SSE_EVENT.ADVERT, this.#currentAdvert.dataJson, this.#currentAdvert.id);
2989
+ }
2990
+ if (this.#heartbeatMs > 0) {
2991
+ sub.heartbeatTimer = setInterval(() => {
2992
+ try {
2993
+ stream.writeComment("hb");
2994
+ } catch {
2995
+ }
2996
+ }, this.#heartbeatMs);
2997
+ }
2998
+ }
2999
+ #openInboxSubscription(req, stream, did, path) {
3000
+ const auth = req.headers.authorization;
3001
+ if (!auth) {
3002
+ this.#logger.debug(`Inbox subscribe: missing authorization header for ${did}`);
3003
+ stream.close();
3004
+ return;
3005
+ }
3006
+ const senderPk = this.#resolveSenderPk(did);
3007
+ if (!senderPk) {
3008
+ stream.close();
3009
+ return;
3010
+ }
3011
+ let parsedTs = 0;
3012
+ let parsedNonce = "";
3013
+ try {
3014
+ const parsed = verifyRequestAuth(auth, path, senderPk, {
3015
+ clockSkewSec: this.#clockSkewSec,
3016
+ now: () => this.#now()
3017
+ });
3018
+ if (parsed.did !== did) {
3019
+ stream.close();
3020
+ return;
3021
+ }
3022
+ parsedTs = parsed.ts;
3023
+ parsedNonce = parsed.nonce;
3024
+ } catch (err) {
3025
+ this.#logger.debug(`Inbox subscribe: auth verification failed for ${did}:`, err);
3026
+ stream.close();
3027
+ return;
3028
+ }
3029
+ if (!this.#nonceCache.store(did, parsedNonce, parsedTs)) {
3030
+ stream.close();
3031
+ return;
3032
+ }
3033
+ if (!this.#rateLimiter.consume(did, this.#now())) {
3034
+ stream.close();
3035
+ return;
3036
+ }
3037
+ const inbox = this.#getOrCreateInbox(did);
3038
+ const sub = { stream };
3039
+ inbox.subscribers.add(sub);
3040
+ stream.onClose(() => {
3041
+ this.#closeInboxSubscriber(sub);
3042
+ inbox.subscribers.delete(sub);
3043
+ });
3044
+ const lastEventId = req.headers["last-event-id"];
3045
+ for (const stored of inbox.buffer.since(lastEventId)) {
3046
+ this.#safeWrite(stream, stored.event, stored.data, stored.id);
3047
+ }
3048
+ if (this.#heartbeatMs > 0) {
3049
+ sub.heartbeatTimer = setInterval(() => {
3050
+ try {
3051
+ stream.writeComment("hb");
3052
+ } catch {
3053
+ }
3054
+ }, this.#heartbeatMs);
3055
+ }
3056
+ }
3057
+ // ----------------------------------------------------------------
3058
+ // Internal helpers
3059
+ // ----------------------------------------------------------------
3060
+ #getOrCreateInbox(did) {
3061
+ let inbox = this.#inboxes.get(did);
3062
+ if (!inbox) {
3063
+ inbox = { buffer: new InboxBuffer(this.#inboxBufferSize), subscribers: /* @__PURE__ */ new Set() };
3064
+ this.#inboxes.set(did, inbox);
3065
+ }
3066
+ return inbox;
3067
+ }
3068
+ #resolveSenderPk(did) {
3069
+ const peerBytes = this.#peers.get(did);
3070
+ if (peerBytes) {
3071
+ try {
3072
+ return new import_keypair2.CompressedSecp256k1PublicKey(peerBytes);
3073
+ } catch {
3074
+ }
3075
+ }
3076
+ return this.#resolveSenderPkFn?.(did);
3077
+ }
3078
+ #safeWrite(stream, event, data, id) {
3079
+ try {
3080
+ stream.writeEvent(event, data, id);
3081
+ } catch (err) {
3082
+ this.#logger.debug("SSE writeEvent failed:", err);
3083
+ }
3084
+ }
3085
+ #closeBroadcastSubscriber(sub) {
3086
+ if (sub.heartbeatTimer) clearInterval(sub.heartbeatTimer);
3087
+ try {
3088
+ sub.stream.close();
3089
+ } catch {
3090
+ }
3091
+ }
3092
+ #closeInboxSubscriber(sub) {
3093
+ if (sub.heartbeatTimer) clearInterval(sub.heartbeatTimer);
3094
+ try {
3095
+ sub.stream.close();
3096
+ } catch {
3097
+ }
3098
+ }
3099
+ #respondJson(status, body, req) {
3100
+ return {
3101
+ status,
3102
+ headers: { "content-type": "application/json", ...this.#corsHeaders(req) },
3103
+ body: JSON.stringify(body)
3104
+ };
3105
+ }
3106
+ #respond(status, body, req) {
3107
+ return { status, headers: this.#corsHeaders(req), body };
3108
+ }
3109
+ #corsHeaders(req) {
3110
+ const origin = req.headers.origin;
3111
+ if (!origin) return {};
3112
+ const common = {
3113
+ "access-control-allow-methods": "GET, POST, OPTIONS",
3114
+ "access-control-allow-headers": "authorization, content-type, last-event-id",
3115
+ "access-control-max-age": "86400"
3116
+ };
3117
+ switch (this.#cors.mode) {
3118
+ case "permissive":
3119
+ return { "access-control-allow-origin": "*", ...common };
3120
+ case "allowlist":
3121
+ if (this.#cors.origins.includes(origin)) {
3122
+ return { "access-control-allow-origin": origin, vary: "origin", ...common };
3123
+ }
3124
+ return {};
3125
+ case "same-origin":
3126
+ return {};
3127
+ }
3128
+ }
3129
+ #wellKnownMetadata() {
3130
+ return {
3131
+ envelopeVersion: HTTP_ENVELOPE_VERSION,
3132
+ heartbeatIntervalMs: this.#heartbeatMs,
3133
+ inboxBufferSize: this.#inboxBufferSize,
3134
+ advertTtlMs: this.#advertTtlMs
3135
+ };
3136
+ }
3137
+ };
3138
+ function extractPath(reqUrl) {
3139
+ if (reqUrl.startsWith("http://") || reqUrl.startsWith("https://")) {
3140
+ return new URL(reqUrl).pathname;
3141
+ }
3142
+ const q = reqUrl.indexOf("?");
3143
+ return q === -1 ? reqUrl : reqUrl.slice(0, q);
3144
+ }
3145
+ function matchInboxPath(path) {
3146
+ if (!path.startsWith(INBOX_PATH_PREFIX) || !path.endsWith(INBOX_PATH_SUFFIX)) return void 0;
3147
+ const encodedDid = path.slice(INBOX_PATH_PREFIX.length, path.length - INBOX_PATH_SUFFIX.length);
3148
+ if (!encodedDid) return void 0;
3149
+ try {
3150
+ return { did: decodeURIComponent(encodedDid) };
3151
+ } catch {
3152
+ return void 0;
3153
+ }
3154
+ }
3155
+ function parseJsonBody(body) {
3156
+ if (body === void 0 || body === "") return void 0;
3157
+ try {
3158
+ return JSON.parse(body);
3159
+ } catch {
3160
+ return void 0;
3161
+ }
3162
+ }
3163
+ function flattenMessage(msg) {
3164
+ if (msg.body && typeof msg.body === "object") {
3165
+ return { ...msg, ...msg.body };
3166
+ }
3167
+ return msg;
3168
+ }
3169
+
3170
+ // src/service/sse-writer.ts
3171
+ function formatSseEvent(event, data, id) {
3172
+ const lines = [];
3173
+ if (id !== void 0) lines.push(`id: ${id}`);
3174
+ lines.push(`event: ${event}`);
3175
+ for (const part of data.split("\n")) lines.push(`data: ${part}`);
3176
+ lines.push("");
3177
+ lines.push("");
3178
+ return lines.join("\n");
3179
+ }
3180
+ function formatSseComment(comment) {
3181
+ const safe = comment.replace(/\n/g, " ");
3182
+ return `: ${safe}
3183
+
3184
+ `;
3185
+ }
3186
+ // Annotate the CommonJS export names for ESM import in node:
3187
+ 0 && (module.exports = {
3188
+ AggregationService,
3189
+ AggregationServiceRunner,
3190
+ DEFAULT_ADVERT_REPEAT_INTERVAL_MS,
3191
+ DEFAULT_MAX_UPDATE_SIZE_BYTES,
3192
+ HttpServerTransport,
3193
+ InMemoryRateLimitStore,
3194
+ InboxBuffer,
3195
+ NonceCache,
3196
+ RateLimiter,
3197
+ formatSseComment,
3198
+ formatSseEvent
3199
+ });