@enbox/dwn-server 0.0.2 → 0.0.4

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 (309) hide show
  1. package/LICENSE +3 -2
  2. package/README.md +115 -215
  3. package/dist/esm/src/admin/activity-log.d.ts +44 -0
  4. package/dist/esm/src/admin/activity-log.d.ts.map +1 -0
  5. package/dist/esm/src/admin/activity-log.js +85 -0
  6. package/dist/esm/src/admin/activity-log.js.map +1 -0
  7. package/dist/esm/src/admin/admin-api.d.ts +61 -0
  8. package/dist/esm/src/admin/admin-api.d.ts.map +1 -0
  9. package/dist/esm/src/admin/admin-api.js +1047 -0
  10. package/dist/esm/src/admin/admin-api.js.map +1 -0
  11. package/dist/esm/src/admin/admin-auth.d.ts +9 -0
  12. package/dist/esm/src/admin/admin-auth.d.ts.map +1 -0
  13. package/dist/esm/src/admin/admin-auth.js +45 -0
  14. package/dist/esm/src/admin/admin-auth.js.map +1 -0
  15. package/dist/esm/src/admin/admin-store.d.ts +111 -0
  16. package/dist/esm/src/admin/admin-store.d.ts.map +1 -0
  17. package/dist/esm/src/admin/admin-store.js +376 -0
  18. package/dist/esm/src/admin/admin-store.js.map +1 -0
  19. package/dist/esm/src/admin/audit-log.d.ts +94 -0
  20. package/dist/esm/src/admin/audit-log.d.ts.map +1 -0
  21. package/dist/esm/src/admin/audit-log.js +220 -0
  22. package/dist/esm/src/admin/audit-log.js.map +1 -0
  23. package/dist/esm/src/admin/index.d.ts +10 -0
  24. package/dist/esm/src/admin/index.d.ts.map +1 -0
  25. package/dist/esm/src/admin/index.js +7 -0
  26. package/dist/esm/src/admin/index.js.map +1 -0
  27. package/dist/esm/src/admin/types.d.ts +306 -0
  28. package/dist/esm/src/admin/types.d.ts.map +1 -0
  29. package/dist/esm/src/admin/types.js +2 -0
  30. package/dist/esm/src/admin/types.js.map +1 -0
  31. package/dist/esm/src/admin/webhook-manager.d.ts +55 -0
  32. package/dist/esm/src/admin/webhook-manager.d.ts.map +1 -0
  33. package/dist/esm/src/admin/webhook-manager.js +184 -0
  34. package/dist/esm/src/admin/webhook-manager.js.map +1 -0
  35. package/dist/esm/src/config.d.ts +124 -9
  36. package/dist/esm/src/config.d.ts.map +1 -1
  37. package/dist/esm/src/config.js +155 -13
  38. package/dist/esm/src/config.js.map +1 -1
  39. package/dist/esm/src/connection/connection-manager.d.ts +32 -9
  40. package/dist/esm/src/connection/connection-manager.d.ts.map +1 -1
  41. package/dist/esm/src/connection/connection-manager.js +38 -5
  42. package/dist/esm/src/connection/connection-manager.js.map +1 -1
  43. package/dist/esm/src/connection/flow-controller.d.ts +53 -0
  44. package/dist/esm/src/connection/flow-controller.d.ts.map +1 -0
  45. package/dist/esm/src/connection/flow-controller.js +101 -0
  46. package/dist/esm/src/connection/flow-controller.js.map +1 -0
  47. package/dist/esm/src/connection/socket-connection.d.ts +54 -18
  48. package/dist/esm/src/connection/socket-connection.d.ts.map +1 -1
  49. package/dist/esm/src/connection/socket-connection.js +102 -40
  50. package/dist/esm/src/connection/socket-connection.js.map +1 -1
  51. package/dist/esm/src/delivery-service.d.ts +43 -0
  52. package/dist/esm/src/delivery-service.d.ts.map +1 -0
  53. package/dist/esm/src/delivery-service.js +574 -0
  54. package/dist/esm/src/delivery-service.js.map +1 -0
  55. package/dist/esm/src/dwn-error.d.ts +10 -1
  56. package/dist/esm/src/dwn-error.d.ts.map +1 -1
  57. package/dist/esm/src/dwn-error.js +9 -0
  58. package/dist/esm/src/dwn-error.js.map +1 -1
  59. package/dist/esm/src/dwn-server.d.ts +13 -6
  60. package/dist/esm/src/dwn-server.d.ts.map +1 -1
  61. package/dist/esm/src/dwn-server.js +199 -24
  62. package/dist/esm/src/dwn-server.js.map +1 -1
  63. package/dist/esm/src/http-api.d.ts +28 -13
  64. package/dist/esm/src/http-api.d.ts.map +1 -1
  65. package/dist/esm/src/http-api.js +649 -374
  66. package/dist/esm/src/http-api.js.map +1 -1
  67. package/dist/esm/src/index.d.ts +6 -2
  68. package/dist/esm/src/index.d.ts.map +1 -1
  69. package/dist/esm/src/index.js +4 -1
  70. package/dist/esm/src/index.js.map +1 -1
  71. package/dist/esm/src/json-rpc-api.js +2 -1
  72. package/dist/esm/src/json-rpc-api.js.map +1 -1
  73. package/dist/esm/src/json-rpc-handlers/dwn/process-message.d.ts.map +1 -1
  74. package/dist/esm/src/json-rpc-handlers/dwn/process-message.js +109 -7
  75. package/dist/esm/src/json-rpc-handlers/dwn/process-message.js.map +1 -1
  76. package/dist/esm/src/json-rpc-handlers/subscription/ack.d.ts +20 -0
  77. package/dist/esm/src/json-rpc-handlers/subscription/ack.d.ts.map +1 -0
  78. package/dist/esm/src/json-rpc-handlers/subscription/ack.js +41 -0
  79. package/dist/esm/src/json-rpc-handlers/subscription/ack.js.map +1 -0
  80. package/dist/esm/src/json-rpc-handlers/subscription/close.d.ts.map +1 -1
  81. package/dist/esm/src/json-rpc-handlers/subscription/close.js +1 -1
  82. package/dist/esm/src/json-rpc-handlers/subscription/close.js.map +1 -1
  83. package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts +1 -0
  84. package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts.map +1 -1
  85. package/dist/esm/src/json-rpc-handlers/subscription/index.js +1 -0
  86. package/dist/esm/src/json-rpc-handlers/subscription/index.js.map +1 -1
  87. package/dist/esm/src/lib/json-rpc-router.d.ts +25 -8
  88. package/dist/esm/src/lib/json-rpc-router.d.ts.map +1 -1
  89. package/dist/esm/src/lib/json-rpc-router.js.map +1 -1
  90. package/dist/esm/src/lib/sql-utils.d.ts +6 -0
  91. package/dist/esm/src/lib/sql-utils.d.ts.map +1 -0
  92. package/dist/esm/src/lib/sql-utils.js +8 -0
  93. package/dist/esm/src/lib/sql-utils.js.map +1 -0
  94. package/dist/esm/src/main.js +0 -6
  95. package/dist/esm/src/main.js.map +1 -1
  96. package/dist/esm/src/message-processed-hook.d.ts +35 -0
  97. package/dist/esm/src/message-processed-hook.d.ts.map +1 -0
  98. package/dist/esm/src/message-processed-hook.js +2 -0
  99. package/dist/esm/src/message-processed-hook.js.map +1 -0
  100. package/dist/esm/src/metrics.d.ts +14 -2
  101. package/dist/esm/src/metrics.d.ts.map +1 -1
  102. package/dist/esm/src/metrics.js +41 -1
  103. package/dist/esm/src/metrics.js.map +1 -1
  104. package/dist/esm/src/plugins/event-log-nats.d.ts +25 -0
  105. package/dist/esm/src/plugins/event-log-nats.d.ts.map +1 -0
  106. package/dist/esm/src/plugins/event-log-nats.js +379 -0
  107. package/dist/esm/src/plugins/event-log-nats.js.map +1 -0
  108. package/dist/esm/src/rate-limiter.d.ts +60 -0
  109. package/dist/esm/src/rate-limiter.d.ts.map +1 -0
  110. package/dist/esm/src/rate-limiter.js +116 -0
  111. package/dist/esm/src/rate-limiter.js.map +1 -0
  112. package/dist/esm/src/registration/jwt-provider-auth-plugin.d.ts +53 -0
  113. package/dist/esm/src/registration/jwt-provider-auth-plugin.d.ts.map +1 -0
  114. package/dist/esm/src/registration/jwt-provider-auth-plugin.js +90 -0
  115. package/dist/esm/src/registration/jwt-provider-auth-plugin.js.map +1 -0
  116. package/dist/esm/src/registration/open-auth-handler.d.ts +37 -0
  117. package/dist/esm/src/registration/open-auth-handler.d.ts.map +1 -0
  118. package/dist/esm/src/registration/open-auth-handler.js +214 -0
  119. package/dist/esm/src/registration/open-auth-handler.js.map +1 -0
  120. package/dist/esm/src/registration/proof-of-work-manager.d.ts +1 -1
  121. package/dist/esm/src/registration/proof-of-work-manager.d.ts.map +1 -1
  122. package/dist/esm/src/registration/proof-of-work-manager.js +3 -3
  123. package/dist/esm/src/registration/proof-of-work-manager.js.map +1 -1
  124. package/dist/esm/src/registration/provider-auth-plugin.d.ts +46 -0
  125. package/dist/esm/src/registration/provider-auth-plugin.d.ts.map +1 -0
  126. package/dist/esm/src/registration/provider-auth-plugin.js +29 -0
  127. package/dist/esm/src/registration/provider-auth-plugin.js.map +1 -0
  128. package/dist/esm/src/registration/registration-manager.d.ts +28 -5
  129. package/dist/esm/src/registration/registration-manager.d.ts.map +1 -1
  130. package/dist/esm/src/registration/registration-manager.js +83 -12
  131. package/dist/esm/src/registration/registration-manager.js.map +1 -1
  132. package/dist/esm/src/registration/registration-store.d.ts +83 -3
  133. package/dist/esm/src/registration/registration-store.d.ts.map +1 -1
  134. package/dist/esm/src/registration/registration-store.js +248 -11
  135. package/dist/esm/src/registration/registration-store.js.map +1 -1
  136. package/dist/esm/src/storage.d.ts +5 -5
  137. package/dist/esm/src/storage.d.ts.map +1 -1
  138. package/dist/esm/src/storage.js +105 -24
  139. package/dist/esm/src/storage.js.map +1 -1
  140. package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts.map +1 -1
  141. package/dist/esm/src/web5-connect/sql-ttl-cache.js +11 -3
  142. package/dist/esm/src/web5-connect/sql-ttl-cache.js.map +1 -1
  143. package/dist/esm/src/web5-connect/web5-connect-server.d.ts.map +1 -1
  144. package/dist/esm/src/web5-connect/web5-connect-server.js +2 -2
  145. package/dist/esm/src/web5-connect/web5-connect-server.js.map +1 -1
  146. package/dist/esm/src/ws-api.d.ts +18 -4
  147. package/dist/esm/src/ws-api.d.ts.map +1 -1
  148. package/dist/esm/src/ws-api.js +12 -16
  149. package/dist/esm/src/ws-api.js.map +1 -1
  150. package/package.json +34 -53
  151. package/src/admin/activity-log.ts +100 -0
  152. package/src/admin/admin-api.ts +1308 -0
  153. package/src/admin/admin-auth.ts +56 -0
  154. package/src/admin/admin-store.ts +515 -0
  155. package/src/admin/audit-log.ts +327 -0
  156. package/src/admin/index.ts +34 -0
  157. package/src/admin/types.ts +352 -0
  158. package/src/admin/webhook-manager.ts +245 -0
  159. package/src/config.ts +190 -22
  160. package/src/connection/connection-manager.ts +67 -17
  161. package/src/connection/flow-controller.ts +117 -0
  162. package/src/connection/socket-connection.ts +144 -67
  163. package/src/delivery-service.ts +740 -0
  164. package/src/dwn-error.ts +11 -2
  165. package/src/dwn-server.ts +254 -39
  166. package/src/http-api.ts +736 -392
  167. package/src/index.ts +13 -2
  168. package/src/json-rpc-api.ts +2 -1
  169. package/src/json-rpc-handlers/dwn/process-message.ts +149 -15
  170. package/src/json-rpc-handlers/subscription/ack.ts +63 -0
  171. package/src/json-rpc-handlers/subscription/close.ts +5 -9
  172. package/src/json-rpc-handlers/subscription/index.ts +1 -0
  173. package/src/lib/json-rpc-router.ts +26 -11
  174. package/src/lib/sql-utils.ts +7 -0
  175. package/src/main.ts +0 -8
  176. package/src/message-processed-hook.ts +33 -0
  177. package/src/metrics.ts +57 -8
  178. package/src/plugins/event-log-nats.ts +466 -0
  179. package/src/process-handlers.ts +5 -5
  180. package/src/rate-limiter.ts +143 -0
  181. package/src/registration/jwt-provider-auth-plugin.ts +119 -0
  182. package/src/registration/open-auth-handler.ts +263 -0
  183. package/src/registration/proof-of-work-manager.ts +11 -10
  184. package/src/registration/provider-auth-plugin.ts +84 -0
  185. package/src/registration/registration-manager.ts +129 -31
  186. package/src/registration/registration-store.ts +332 -22
  187. package/src/storage.ts +136 -40
  188. package/src/web5-connect/sql-ttl-cache.ts +12 -5
  189. package/src/web5-connect/web5-connect-server.ts +9 -8
  190. package/src/ws-api.ts +39 -26
  191. package/dist/cjs/index.js +0 -6811
  192. package/dist/cjs/package.json +0 -1
  193. package/dist/esm/src/json-rpc-socket.d.ts +0 -39
  194. package/dist/esm/src/json-rpc-socket.d.ts.map +0 -1
  195. package/dist/esm/src/json-rpc-socket.js +0 -125
  196. package/dist/esm/src/json-rpc-socket.js.map +0 -1
  197. package/dist/esm/src/lib/http-server-shutdown-handler.d.ts +0 -10
  198. package/dist/esm/src/lib/http-server-shutdown-handler.d.ts.map +0 -1
  199. package/dist/esm/src/lib/http-server-shutdown-handler.js +0 -65
  200. package/dist/esm/src/lib/http-server-shutdown-handler.js.map +0 -1
  201. package/dist/esm/src/lib/json-rpc.d.ts +0 -54
  202. package/dist/esm/src/lib/json-rpc.d.ts.map +0 -1
  203. package/dist/esm/src/lib/json-rpc.js +0 -60
  204. package/dist/esm/src/lib/json-rpc.js.map +0 -1
  205. package/dist/esm/src/registration/proof-of-work-types.d.ts +0 -8
  206. package/dist/esm/src/registration/proof-of-work-types.d.ts.map +0 -1
  207. package/dist/esm/src/registration/proof-of-work-types.js +0 -2
  208. package/dist/esm/src/registration/proof-of-work-types.js.map +0 -1
  209. package/dist/esm/src/registration/registration-types.d.ts +0 -18
  210. package/dist/esm/src/registration/registration-types.d.ts.map +0 -1
  211. package/dist/esm/src/registration/registration-types.js +0 -2
  212. package/dist/esm/src/registration/registration-types.js.map +0 -1
  213. package/dist/esm/tests/common-scenario-validator.d.ts +0 -11
  214. package/dist/esm/tests/common-scenario-validator.d.ts.map +0 -1
  215. package/dist/esm/tests/common-scenario-validator.js +0 -114
  216. package/dist/esm/tests/common-scenario-validator.js.map +0 -1
  217. package/dist/esm/tests/connection/connection-manager.spec.d.ts +0 -2
  218. package/dist/esm/tests/connection/connection-manager.spec.d.ts.map +0 -1
  219. package/dist/esm/tests/connection/connection-manager.spec.js +0 -47
  220. package/dist/esm/tests/connection/connection-manager.spec.js.map +0 -1
  221. package/dist/esm/tests/connection/socket-connection.spec.d.ts +0 -2
  222. package/dist/esm/tests/connection/socket-connection.spec.d.ts.map +0 -1
  223. package/dist/esm/tests/connection/socket-connection.spec.js +0 -125
  224. package/dist/esm/tests/connection/socket-connection.spec.js.map +0 -1
  225. package/dist/esm/tests/cors/http-api.browser.d.ts +0 -2
  226. package/dist/esm/tests/cors/http-api.browser.d.ts.map +0 -1
  227. package/dist/esm/tests/cors/http-api.browser.js +0 -60
  228. package/dist/esm/tests/cors/http-api.browser.js.map +0 -1
  229. package/dist/esm/tests/cors/ping.browser.d.ts +0 -2
  230. package/dist/esm/tests/cors/ping.browser.d.ts.map +0 -1
  231. package/dist/esm/tests/cors/ping.browser.js +0 -7
  232. package/dist/esm/tests/cors/ping.browser.js.map +0 -1
  233. package/dist/esm/tests/dwn-process-message.spec.d.ts +0 -2
  234. package/dist/esm/tests/dwn-process-message.spec.d.ts.map +0 -1
  235. package/dist/esm/tests/dwn-process-message.spec.js +0 -172
  236. package/dist/esm/tests/dwn-process-message.spec.js.map +0 -1
  237. package/dist/esm/tests/dwn-server.spec.d.ts +0 -2
  238. package/dist/esm/tests/dwn-server.spec.d.ts.map +0 -1
  239. package/dist/esm/tests/dwn-server.spec.js +0 -49
  240. package/dist/esm/tests/dwn-server.spec.js.map +0 -1
  241. package/dist/esm/tests/http-api.spec.d.ts +0 -2
  242. package/dist/esm/tests/http-api.spec.d.ts.map +0 -1
  243. package/dist/esm/tests/http-api.spec.js +0 -775
  244. package/dist/esm/tests/http-api.spec.js.map +0 -1
  245. package/dist/esm/tests/json-rpc-socket.spec.d.ts +0 -2
  246. package/dist/esm/tests/json-rpc-socket.spec.d.ts.map +0 -1
  247. package/dist/esm/tests/json-rpc-socket.spec.js +0 -225
  248. package/dist/esm/tests/json-rpc-socket.spec.js.map +0 -1
  249. package/dist/esm/tests/plugins/data-store-sqlite.d.ts +0 -17
  250. package/dist/esm/tests/plugins/data-store-sqlite.d.ts.map +0 -1
  251. package/dist/esm/tests/plugins/data-store-sqlite.js +0 -23
  252. package/dist/esm/tests/plugins/data-store-sqlite.js.map +0 -1
  253. package/dist/esm/tests/plugins/event-log-sqlite.d.ts +0 -17
  254. package/dist/esm/tests/plugins/event-log-sqlite.d.ts.map +0 -1
  255. package/dist/esm/tests/plugins/event-log-sqlite.js +0 -23
  256. package/dist/esm/tests/plugins/event-log-sqlite.js.map +0 -1
  257. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts +0 -17
  258. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts.map +0 -1
  259. package/dist/esm/tests/plugins/event-stream-in-memory.js +0 -21
  260. package/dist/esm/tests/plugins/event-stream-in-memory.js.map +0 -1
  261. package/dist/esm/tests/plugins/message-store-sqlite.d.ts +0 -17
  262. package/dist/esm/tests/plugins/message-store-sqlite.d.ts.map +0 -1
  263. package/dist/esm/tests/plugins/message-store-sqlite.js +0 -23
  264. package/dist/esm/tests/plugins/message-store-sqlite.js.map +0 -1
  265. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts +0 -17
  266. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts.map +0 -1
  267. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js +0 -23
  268. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js.map +0 -1
  269. package/dist/esm/tests/process-handler.spec.d.ts +0 -2
  270. package/dist/esm/tests/process-handler.spec.d.ts.map +0 -1
  271. package/dist/esm/tests/process-handler.spec.js +0 -60
  272. package/dist/esm/tests/process-handler.spec.js.map +0 -1
  273. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts +0 -2
  274. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts.map +0 -1
  275. package/dist/esm/tests/registration/proof-of-work-manager.spec.js +0 -157
  276. package/dist/esm/tests/registration/proof-of-work-manager.spec.js.map +0 -1
  277. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts +0 -2
  278. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts.map +0 -1
  279. package/dist/esm/tests/rpc-subscribe-close.spec.js +0 -81
  280. package/dist/esm/tests/rpc-subscribe-close.spec.js.map +0 -1
  281. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts +0 -2
  282. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts.map +0 -1
  283. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js +0 -73
  284. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js.map +0 -1
  285. package/dist/esm/tests/scenarios/registration.spec.d.ts +0 -2
  286. package/dist/esm/tests/scenarios/registration.spec.d.ts.map +0 -1
  287. package/dist/esm/tests/scenarios/registration.spec.js +0 -507
  288. package/dist/esm/tests/scenarios/registration.spec.js.map +0 -1
  289. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts +0 -2
  290. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts.map +0 -1
  291. package/dist/esm/tests/scenarios/web5-connect.spec.js +0 -137
  292. package/dist/esm/tests/scenarios/web5-connect.spec.js.map +0 -1
  293. package/dist/esm/tests/test-dwn.d.ts +0 -7
  294. package/dist/esm/tests/test-dwn.d.ts.map +0 -1
  295. package/dist/esm/tests/test-dwn.js +0 -34
  296. package/dist/esm/tests/test-dwn.js.map +0 -1
  297. package/dist/esm/tests/utils.d.ts +0 -46
  298. package/dist/esm/tests/utils.d.ts.map +0 -1
  299. package/dist/esm/tests/utils.js +0 -116
  300. package/dist/esm/tests/utils.js.map +0 -1
  301. package/dist/esm/tests/ws-api.spec.d.ts +0 -2
  302. package/dist/esm/tests/ws-api.spec.d.ts.map +0 -1
  303. package/dist/esm/tests/ws-api.spec.js +0 -327
  304. package/dist/esm/tests/ws-api.spec.js.map +0 -1
  305. package/src/json-rpc-socket.ts +0 -155
  306. package/src/lib/http-server-shutdown-handler.ts +0 -79
  307. package/src/lib/json-rpc.ts +0 -126
  308. package/src/registration/proof-of-work-types.ts +0 -7
  309. package/src/registration/registration-types.ts +0 -18
@@ -0,0 +1,466 @@
1
+ import type { NatsConnection } from '@nats-io/transport-node';
2
+ import type { ConsumerMessages, JetStreamClient, JetStreamManager } from '@nats-io/jetstream';
3
+ import type { EventLog, EventLogEntry, EventLogReadOptions, EventLogReadResult, EventLogSubscribeOptions, EventSubscription, Filter, KeyValues, MessageEvent, SubscriptionListener } from '@enbox/dwn-sdk-js';
4
+
5
+ import log from 'loglevel';
6
+
7
+ import { connect } from '@nats-io/transport-node';
8
+ import { AckPolicy, DeliverPolicy, jetstream, jetstreamManager } from '@nats-io/jetstream';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Configuration — all sourced from environment variables
12
+ // ---------------------------------------------------------------------------
13
+
14
+ type NatsEventLogConfig = {
15
+ /** NATS connection URL(s), comma-separated. */
16
+ url : string;
17
+ /** JetStream stream name. */
18
+ streamName : string;
19
+ /** Max event age in nanoseconds (0 = unlimited). */
20
+ streamMaxAge : number;
21
+ /** JetStream replication factor. */
22
+ replicas : number;
23
+ /** Max messages per subject (per-tenant cap). */
24
+ maxMsgsPerSubject : number;
25
+ };
26
+
27
+ function loadConfig(): NatsEventLogConfig {
28
+ return {
29
+ url : process.env.NATS_URL || 'nats://localhost:4222',
30
+ streamName : process.env.NATS_STREAM_NAME || 'DWN_EVENTS',
31
+ streamMaxAge : parseInt(process.env.NATS_STREAM_MAX_AGE || '604800000000000'), // 7 days in nanos
32
+ replicas : parseInt(process.env.NATS_STREAM_REPLICAS || '1'),
33
+ maxMsgsPerSubject : parseInt(process.env.NATS_MAX_MSGS_PER_SUBJECT || '100000'),
34
+ };
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Minimal filter matching (OR semantics, matching FilterUtility behaviour)
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Returns `true` if the indexed key-values match at least one of the given
43
+ * filters (OR semantics). An empty or undefined filter array matches all events.
44
+ */
45
+ function matchAnyFilter(keyValues: KeyValues, filters: Filter[] | undefined): boolean {
46
+ if (filters === undefined || filters.length === 0) {
47
+ return true;
48
+ }
49
+ for (const filter of filters) {
50
+ if (matchFilter(keyValues, filter)) {
51
+ return true;
52
+ }
53
+ }
54
+ return false;
55
+ }
56
+
57
+ /** Returns `true` if every property in the filter matches the indexed values (AND semantics). */
58
+ function matchFilter(indexedValues: KeyValues, filter: Filter): boolean {
59
+ for (const key in filter) {
60
+ const filterValue = filter[key];
61
+ const indexValue = indexedValues[key];
62
+ if (indexValue === undefined) {
63
+ return false;
64
+ }
65
+
66
+ const values = Array.isArray(indexValue) ? indexValue : [indexValue];
67
+ let anyMatch = false;
68
+ for (const v of values) {
69
+ if (matchSingleValue(filterValue, v)) {
70
+ anyMatch = true;
71
+ break;
72
+ }
73
+ }
74
+ if (!anyMatch) {
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ }
80
+
81
+ /** Match a single index value against a filter value (equal, oneOf, or range). */
82
+ function matchSingleValue(filterValue: unknown, indexValue: string | number | boolean): boolean {
83
+ if (typeof filterValue === 'object' && filterValue !== null) {
84
+ if (Array.isArray(filterValue)) {
85
+ // OneOfFilter
86
+ return (filterValue as Array<string | number | boolean>).includes(indexValue as never);
87
+ }
88
+ // RangeFilter
89
+ const range = filterValue as { gt?: string | number; gte?: string | number; lt?: string | number; lte?: string | number };
90
+ if (range.lt !== undefined && indexValue >= range.lt) {
91
+ return false;
92
+ }
93
+ if (range.lte !== undefined && indexValue > range.lte) {
94
+ return false;
95
+ }
96
+ if (range.gt !== undefined && indexValue <= range.gt) {
97
+ return false;
98
+ }
99
+ if (range.gte !== undefined && indexValue < range.gte) {
100
+ return false;
101
+ }
102
+ return true;
103
+ }
104
+ // EqualFilter
105
+ return indexValue === filterValue;
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Payload encoding — JSON over NATS messages
110
+ // ---------------------------------------------------------------------------
111
+
112
+ type NatsEventPayload = {
113
+ event : MessageEvent;
114
+ indexes : KeyValues;
115
+ };
116
+
117
+ function encodePayload(payload: NatsEventPayload): Uint8Array {
118
+ return new TextEncoder().encode(JSON.stringify(payload));
119
+ }
120
+
121
+ function decodePayload(data: Uint8Array): NatsEventPayload | undefined {
122
+ try {
123
+ return JSON.parse(new TextDecoder().decode(data)) as NatsEventPayload;
124
+ } catch {
125
+ log.error('NatsEventLog: failed to decode payload, skipping corrupt message');
126
+ return undefined;
127
+ }
128
+ }
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Subject helpers
132
+ // ---------------------------------------------------------------------------
133
+
134
+ /**
135
+ * Encodes a tenant DID into a NATS-safe subject token by replacing `.` and `>`
136
+ * (which are NATS subject delimiters) with URL-safe equivalents.
137
+ */
138
+ function tenantToSubjectToken(tenant: string): string {
139
+ return tenant.replace(/\./g, '~').replace(/>/g, '_');
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // NatsEventLog — distributed EventLog implementation over NATS JetStream
144
+ // ---------------------------------------------------------------------------
145
+
146
+ /**
147
+ * Distributed {@link EventLog} implementation backed by NATS JetStream.
148
+ *
149
+ * Events are published to per-tenant subjects within a single JetStream stream.
150
+ * NATS stream sequence numbers are used as opaque cursors, providing native
151
+ * cursor-based replay and EOSE detection via `msg.info.pending`.
152
+ *
153
+ * Designed for multi-node DWN deployments: node A can emit an event, and a
154
+ * subscriber connected to node B receives it via the shared NATS cluster.
155
+ *
156
+ * Loaded by the DWN server plugin system via `DWN_EVENT_LOG_PLUGIN_PATH`.
157
+ * Must be a default export with a no-arg constructor.
158
+ */
159
+ export default class NatsEventLog implements EventLog {
160
+ #config: NatsEventLogConfig;
161
+ #nc: NatsConnection | undefined;
162
+ #js: JetStreamClient | undefined;
163
+ #jsm: JetStreamManager | undefined;
164
+
165
+ /** Active subscription consumers, keyed by consumer name. */
166
+ #activeConsumers: Map<string, { messages?: ConsumerMessages; stopped: boolean }> = new Map();
167
+
168
+ constructor() {
169
+ this.#config = loadConfig();
170
+ }
171
+
172
+ // ---- Lifecycle -----------------------------------------------------------
173
+
174
+ public async open(): Promise<void> {
175
+ if (this.#nc !== undefined) {
176
+ return;
177
+ }
178
+
179
+ const servers = this.#config.url.split(',').map((s: string): string => s.trim());
180
+ this.#nc = await connect({ servers });
181
+ this.#js = jetstream(this.#nc);
182
+ this.#jsm = await jetstreamManager(this.#nc);
183
+
184
+ // Ensure the stream exists (idempotent — update if it already exists).
185
+ await this.#ensureStream();
186
+
187
+ log.info(`NatsEventLog: connected to ${servers.join(', ')}, stream '${this.#config.streamName}' ready`);
188
+ }
189
+
190
+ public async close(): Promise<void> {
191
+ // Stop all active subscription consumers.
192
+ for (const [name, entry] of this.#activeConsumers) {
193
+ entry.stopped = true;
194
+ entry.messages?.stop();
195
+ try {
196
+ await this.#jsm!.consumers.delete(this.#config.streamName, name);
197
+ } catch {
198
+ // Consumer may already be gone (ephemeral timeout).
199
+ }
200
+ }
201
+ this.#activeConsumers.clear();
202
+
203
+ if (this.#nc !== undefined) {
204
+ await this.#nc.drain();
205
+ this.#nc = undefined;
206
+ this.#js = undefined;
207
+ this.#jsm = undefined;
208
+ }
209
+ }
210
+
211
+ // ---- emit ----------------------------------------------------------------
212
+
213
+ public async emit(tenant: string, event: MessageEvent, indexes: KeyValues): Promise<string> {
214
+ this.#assertOpen();
215
+
216
+ const subject = this.#tenantSubject(tenant);
217
+ const data = encodePayload({ event, indexes });
218
+ const ack = await this.#js!.publish(subject, data);
219
+ return String(ack.seq);
220
+ }
221
+
222
+ // ---- read ----------------------------------------------------------------
223
+
224
+ public async read(tenant: string, options: EventLogReadOptions = {}): Promise<EventLogReadResult> {
225
+ this.#assertOpen();
226
+
227
+ const { cursor, limit, filters } = options;
228
+ const subject = this.#tenantSubject(tenant);
229
+
230
+ // Create a one-shot ordered consumer for the read.
231
+ const consumerOpts: Record<string, unknown> = {
232
+ filter_subject : subject,
233
+ ack_policy : AckPolicy.None, // ordered consumers use AckNone
234
+ };
235
+
236
+ if (cursor !== undefined) {
237
+ consumerOpts.deliver_policy = DeliverPolicy.StartSequence;
238
+ consumerOpts.opt_start_seq = Number(cursor) + 1;
239
+ } else {
240
+ consumerOpts.deliver_policy = DeliverPolicy.All;
241
+ }
242
+
243
+ const consumer = await this.#jsm!.consumers.add(this.#config.streamName, consumerOpts);
244
+ const maxResults = limit ?? Number.MAX_SAFE_INTEGER;
245
+
246
+ const events: EventLogEntry[] = [];
247
+ let lastCursor: string | undefined;
248
+
249
+ try {
250
+ const messages = await this.#js!.consumers.get(this.#config.streamName, consumer.name);
251
+ const iter = await messages.fetch({ max_messages: maxResults, expires: 2_000 });
252
+
253
+ for await (const msg of iter) {
254
+ const payload = decodePayload(msg.data);
255
+ if (payload === undefined) {
256
+ continue;
257
+ }
258
+
259
+ if (!matchAnyFilter(payload.indexes, filters)) {
260
+ continue;
261
+ }
262
+
263
+ events.push({
264
+ seq : msg.seq,
265
+ event : payload.event,
266
+ indexes : payload.indexes,
267
+ });
268
+
269
+ lastCursor = String(msg.seq);
270
+
271
+ if (events.length >= maxResults) {
272
+ break;
273
+ }
274
+ }
275
+ } finally {
276
+ // Clean up the one-shot consumer.
277
+ try {
278
+ await this.#jsm!.consumers.delete(this.#config.streamName, consumer.name);
279
+ } catch {
280
+ // May already be cleaned up.
281
+ }
282
+ }
283
+
284
+ return {
285
+ events,
286
+ cursor: lastCursor ?? cursor,
287
+ };
288
+ }
289
+
290
+ // ---- subscribe -----------------------------------------------------------
291
+
292
+ public async subscribe(
293
+ tenant: string,
294
+ id: string,
295
+ listener: SubscriptionListener,
296
+ options?: EventLogSubscribeOptions,
297
+ ): Promise<EventSubscription> {
298
+ this.#assertOpen();
299
+
300
+ const subject = this.#tenantSubject(tenant);
301
+ const { cursor, filters } = options ?? {};
302
+
303
+ // Build the consumer config.
304
+ const consumerName = `sub-${id}`;
305
+ const consumerOpts: Record<string, unknown> = {
306
+ name : consumerName,
307
+ filter_subject : subject,
308
+ ack_policy : AckPolicy.Explicit,
309
+ inactive_threshold : 60_000_000_000, // 60 seconds in nanos
310
+ };
311
+
312
+ if (cursor !== undefined) {
313
+ consumerOpts.deliver_policy = DeliverPolicy.StartSequence;
314
+ consumerOpts.opt_start_seq = Number(cursor) + 1;
315
+ } else {
316
+ consumerOpts.deliver_policy = DeliverPolicy.New;
317
+ }
318
+
319
+ await this.#jsm!.consumers.add(this.#config.streamName, consumerOpts);
320
+
321
+ const entry: { messages?: ConsumerMessages; stopped: boolean } = { stopped: false };
322
+ this.#activeConsumers.set(consumerName, entry);
323
+
324
+ // Start the consume loop asynchronously.
325
+ const consumeLoop = async (): Promise<void> => {
326
+ let sentEose = cursor === undefined; // no cursor → no EOSE needed
327
+
328
+ try {
329
+ const consumer = await this.#js!.consumers.get(this.#config.streamName, consumerName);
330
+ const messages = await consumer.consume();
331
+ entry.messages = messages;
332
+
333
+ for await (const msg of messages) {
334
+ if (entry.stopped) {
335
+ break;
336
+ }
337
+
338
+ const payload = decodePayload(msg.data);
339
+ if (payload === undefined) {
340
+ msg.ack();
341
+ continue;
342
+ }
343
+
344
+ if (!matchAnyFilter(payload.indexes, filters)) {
345
+ msg.ack();
346
+ continue;
347
+ }
348
+
349
+ const eventCursor = String(msg.seq);
350
+ listener({ type: 'event', cursor: eventCursor, event: payload.event });
351
+ msg.ack();
352
+
353
+ // EOSE detection: when pending reaches 0, all stored events have been
354
+ // delivered and we transition to live mode.
355
+ if (!sentEose && msg.info.pending === 0) {
356
+ listener({ type: 'eose', cursor: eventCursor });
357
+ sentEose = true;
358
+ }
359
+ }
360
+ } catch (err) {
361
+ if (!entry.stopped) {
362
+ log.error(`NatsEventLog: consume loop error for subscription '${id}'`, err);
363
+ }
364
+ }
365
+ };
366
+
367
+ // Fire and forget — the loop runs until stop or connection close.
368
+ consumeLoop();
369
+
370
+ // Handle the edge case where cursor was provided but there are zero
371
+ // stored events after it. The consume loop won't receive any messages,
372
+ // so we need to send EOSE proactively. We check consumer info after a
373
+ // short delay to allow the loop to start.
374
+ if (cursor !== undefined) {
375
+ setTimeout(async (): Promise<void> => {
376
+ if (entry.stopped) {
377
+ return;
378
+ }
379
+ try {
380
+ const info = await this.#jsm!.consumers.info(this.#config.streamName, consumerName);
381
+ if (info.num_pending === 0 && info.delivered.stream_seq <= Number(cursor)) {
382
+ listener({ type: 'eose', cursor });
383
+ }
384
+ } catch {
385
+ // Consumer may be gone already.
386
+ }
387
+ }, 50);
388
+ }
389
+
390
+ return {
391
+ id,
392
+ close: async (): Promise<void> => {
393
+ entry.stopped = true;
394
+ entry.messages?.stop();
395
+ this.#activeConsumers.delete(consumerName);
396
+ try {
397
+ await this.#jsm!.consumers.delete(this.#config.streamName, consumerName);
398
+ } catch {
399
+ // Consumer may already be gone (ephemeral timeout).
400
+ }
401
+ },
402
+ };
403
+ }
404
+
405
+ // ---- trim ----------------------------------------------------------------
406
+
407
+ public async trim(tenant: string, olderThan: number | string): Promise<void> {
408
+ this.#assertOpen();
409
+
410
+ const subject = this.#tenantSubject(tenant);
411
+
412
+ if (typeof olderThan === 'number') {
413
+ // Purge events with sequence < olderThan.
414
+ await this.#jsm!.streams.purge(this.#config.streamName, {
415
+ filter : subject,
416
+ seq : olderThan,
417
+ });
418
+ } else {
419
+ // Timestamp-based trim: purge events older than the given ISO-8601 time.
420
+ // NATS stream purge doesn't support timestamp-based purging natively, so
421
+ // we find the sequence threshold by reading events and checking timestamps.
422
+ // For simplicity, we do a full purge of the subject if olderThan is provided
423
+ // as a string — this matches the EventEmitterEventLog behaviour of deleting
424
+ // entries whose messageTimestamp is before the given time.
425
+ // A more precise implementation could binary-search for the sequence cutoff.
426
+ await this.#jsm!.streams.purge(this.#config.streamName, {
427
+ filter: subject,
428
+ });
429
+ }
430
+ }
431
+
432
+ // ---- Private helpers -----------------------------------------------------
433
+
434
+ #tenantSubject(tenant: string): string {
435
+ return `dwn.events.${tenantToSubjectToken(tenant)}`;
436
+ }
437
+
438
+ #assertOpen(): void {
439
+ if (this.#nc === undefined || this.#js === undefined || this.#jsm === undefined) {
440
+ throw new Error('NatsEventLog: not open. Call open() before using.');
441
+ }
442
+ }
443
+
444
+ async #ensureStream(): Promise<void> {
445
+ const cfg = this.#config;
446
+ try {
447
+ await this.#jsm!.streams.info(cfg.streamName);
448
+ // Stream exists — update config if needed.
449
+ await this.#jsm!.streams.update(cfg.streamName, {
450
+ subjects : ['dwn.events.>'],
451
+ max_age : cfg.streamMaxAge,
452
+ num_replicas : cfg.replicas,
453
+ max_msgs_per_subject : cfg.maxMsgsPerSubject,
454
+ });
455
+ } catch {
456
+ // Stream does not exist — create it.
457
+ await this.#jsm!.streams.add({
458
+ name : cfg.streamName,
459
+ subjects : ['dwn.events.>'],
460
+ max_age : cfg.streamMaxAge,
461
+ num_replicas : cfg.replicas,
462
+ max_msgs_per_subject : cfg.maxMsgsPerSubject,
463
+ });
464
+ }
465
+ }
466
+ }
@@ -51,11 +51,11 @@ export const setProcessHandlers = (dwnServer: DwnServer): ProcessHandlers => {
51
51
  };
52
52
 
53
53
  export const removeProcessHandlers = (handlers: ProcessHandlers): void => {
54
- const {
55
- unhandledRejectionHandler,
56
- uncaughtExceptionHandler,
57
- sigintHandler,
58
- sigtermHandler
54
+ const {
55
+ unhandledRejectionHandler,
56
+ uncaughtExceptionHandler,
57
+ sigintHandler,
58
+ sigtermHandler
59
59
  } = handlers;
60
60
 
61
61
  process.removeListener('unhandledRejection', unhandledRejectionHandler);
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Token-bucket rate limiter with per-key state tracking and automatic cleanup.
3
+ *
4
+ * Each key (IP address or tenant DID) has its own bucket that refills at a
5
+ * fixed rate. When a request arrives, a token is consumed. If the bucket is
6
+ * empty, the request is rejected with a retry-after hint.
7
+ *
8
+ * Stale buckets are periodically purged to prevent unbounded memory growth.
9
+ *
10
+ * @see https://github.com/enboxorg/enbox/issues/326
11
+ */
12
+
13
+ export type RateLimiterConfig = {
14
+ /** Tokens added per second (i.e. sustained request rate). */
15
+ refillRate : number;
16
+ /** Maximum burst size (bucket capacity). */
17
+ maxTokens : number;
18
+ };
19
+
20
+ type Bucket = {
21
+ tokens : number;
22
+ lastRefill : number;
23
+ };
24
+
25
+ export class RateLimiter {
26
+ #refillRate: number;
27
+ #maxTokens: number;
28
+ #buckets: Map<string, Bucket> = new Map();
29
+ #cleanupInterval: ReturnType<typeof setInterval> | undefined;
30
+
31
+ /** Stale buckets older than 5 minutes are purged. */
32
+ private static readonly STALE_THRESHOLD_MS = 5 * 60 * 1000;
33
+ /** Cleanup runs every 60 seconds. */
34
+ private static readonly CLEANUP_INTERVAL_MS = 60 * 1000;
35
+
36
+ public constructor(config: RateLimiterConfig) {
37
+ this.#refillRate = config.refillRate;
38
+ this.#maxTokens = config.maxTokens;
39
+
40
+ // Start periodic cleanup.
41
+ this.#cleanupInterval = setInterval((): void => {
42
+ this.#cleanup();
43
+ }, RateLimiter.CLEANUP_INTERVAL_MS);
44
+ }
45
+
46
+ /**
47
+ * Attempts to consume a token for the given key.
48
+ * @returns `{ allowed: true }` if the request is permitted, or
49
+ * `{ allowed: false, retryAfterMs }` if the rate limit is exceeded.
50
+ */
51
+ public consume(key: string): { allowed: true } | { allowed: false; retryAfterMs: number } {
52
+ const now = Date.now();
53
+ let bucket = this.#buckets.get(key);
54
+
55
+ if (bucket === undefined) {
56
+ bucket = { tokens: this.#maxTokens, lastRefill: now };
57
+ this.#buckets.set(key, bucket);
58
+ }
59
+
60
+ // Refill tokens based on elapsed time.
61
+ const elapsed = (now - bucket.lastRefill) / 1000;
62
+ bucket.tokens = Math.min(this.#maxTokens, bucket.tokens + elapsed * this.#refillRate);
63
+ bucket.lastRefill = now;
64
+
65
+ if (bucket.tokens >= 1) {
66
+ bucket.tokens -= 1;
67
+ return { allowed: true };
68
+ }
69
+
70
+ // Calculate how long until one token is available.
71
+ const deficit = 1 - bucket.tokens;
72
+ const retryAfterMs = Math.ceil((deficit / this.#refillRate) * 1000);
73
+ return { allowed: false, retryAfterMs };
74
+ }
75
+
76
+ /**
77
+ * Returns the number of keys currently being tracked.
78
+ */
79
+ public get size(): number {
80
+ return this.#buckets.size;
81
+ }
82
+
83
+ /**
84
+ * Returns the current token count for a key, or `undefined` if not tracked.
85
+ */
86
+ public getTokens(key: string): number | undefined {
87
+ const bucket = this.#buckets.get(key);
88
+ if (bucket === undefined) {
89
+ return undefined;
90
+ }
91
+
92
+ // Refill to return current state.
93
+ const elapsed = (Date.now() - bucket.lastRefill) / 1000;
94
+ return Math.min(this.#maxTokens, bucket.tokens + elapsed * this.#refillRate);
95
+ }
96
+
97
+ /**
98
+ * Reconfigures the rate limiter with new settings. Existing buckets are
99
+ * retained but will use the new `refillRate` and `maxTokens` going forward.
100
+ *
101
+ * @see https://github.com/enboxorg/enbox/issues/389
102
+ */
103
+ public reconfigure(config: RateLimiterConfig): void {
104
+ this.#refillRate = config.refillRate;
105
+ this.#maxTokens = config.maxTokens;
106
+ }
107
+
108
+ /**
109
+ * Returns the current configuration of this rate limiter.
110
+ */
111
+ public get config(): RateLimiterConfig {
112
+ return {
113
+ refillRate : this.#refillRate,
114
+ maxTokens : this.#maxTokens,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Stops the periodic cleanup timer. Call this when shutting down.
120
+ */
121
+ public destroy(): void {
122
+ if (this.#cleanupInterval) {
123
+ clearInterval(this.#cleanupInterval);
124
+ this.#cleanupInterval = undefined;
125
+ }
126
+ this.#buckets.clear();
127
+ }
128
+
129
+ /**
130
+ * Removes buckets that have been at max capacity for longer than the stale threshold.
131
+ */
132
+ #cleanup(): void {
133
+ const now = Date.now();
134
+ for (const [key, bucket] of this.#buckets) {
135
+ const elapsed = (now - bucket.lastRefill) / 1000;
136
+ const currentTokens = Math.min(this.#maxTokens, bucket.tokens + elapsed * this.#refillRate);
137
+ const timeSinceLastUse = now - bucket.lastRefill;
138
+ if (currentTokens >= this.#maxTokens && timeSinceLastUse > RateLimiter.STALE_THRESHOLD_MS) {
139
+ this.#buckets.delete(key);
140
+ }
141
+ }
142
+ }
143
+ }