@enbox/dwn-server 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/LICENSE +3 -2
  2. package/README.md +112 -212
  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 +122 -3
  36. package/dist/esm/src/config.d.ts.map +1 -1
  37. package/dist/esm/src/config.js +151 -5
  38. package/dist/esm/src/config.js.map +1 -1
  39. package/dist/esm/src/connection/connection-manager.d.ts +24 -1
  40. package/dist/esm/src/connection/connection-manager.d.ts.map +1 -1
  41. package/dist/esm/src/connection/connection-manager.js +33 -2
  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 +39 -4
  48. package/dist/esm/src/connection/socket-connection.d.ts.map +1 -1
  49. package/dist/esm/src/connection/socket-connection.js +80 -9
  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 +8 -0
  60. package/dist/esm/src/dwn-server.d.ts.map +1 -1
  61. package/dist/esm/src/dwn-server.js +198 -12
  62. package/dist/esm/src/dwn-server.js.map +1 -1
  63. package/dist/esm/src/http-api.d.ts +19 -2
  64. package/dist/esm/src/http-api.d.ts.map +1 -1
  65. package/dist/esm/src/http-api.js +219 -19
  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 +106 -4
  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 +22 -4
  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 +13 -1
  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/provider-auth-plugin.d.ts +46 -0
  123. package/dist/esm/src/registration/provider-auth-plugin.d.ts.map +1 -0
  124. package/dist/esm/src/registration/provider-auth-plugin.js +29 -0
  125. package/dist/esm/src/registration/provider-auth-plugin.js.map +1 -0
  126. package/dist/esm/src/registration/registration-manager.d.ts +27 -4
  127. package/dist/esm/src/registration/registration-manager.d.ts.map +1 -1
  128. package/dist/esm/src/registration/registration-manager.js +77 -6
  129. package/dist/esm/src/registration/registration-manager.js.map +1 -1
  130. package/dist/esm/src/registration/registration-store.d.ts +83 -3
  131. package/dist/esm/src/registration/registration-store.d.ts.map +1 -1
  132. package/dist/esm/src/registration/registration-store.js +248 -11
  133. package/dist/esm/src/registration/registration-store.js.map +1 -1
  134. package/dist/esm/src/storage.d.ts +4 -4
  135. package/dist/esm/src/storage.d.ts.map +1 -1
  136. package/dist/esm/src/storage.js +100 -20
  137. package/dist/esm/src/storage.js.map +1 -1
  138. package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts.map +1 -1
  139. package/dist/esm/src/web5-connect/sql-ttl-cache.js +8 -1
  140. package/dist/esm/src/web5-connect/sql-ttl-cache.js.map +1 -1
  141. package/dist/esm/src/ws-api.d.ts +17 -1
  142. package/dist/esm/src/ws-api.d.ts.map +1 -1
  143. package/dist/esm/src/ws-api.js +9 -2
  144. package/dist/esm/src/ws-api.js.map +1 -1
  145. package/package.json +18 -16
  146. package/src/admin/activity-log.ts +100 -0
  147. package/src/admin/admin-api.ts +1308 -0
  148. package/src/admin/admin-auth.ts +56 -0
  149. package/src/admin/admin-store.ts +515 -0
  150. package/src/admin/audit-log.ts +327 -0
  151. package/src/admin/index.ts +34 -0
  152. package/src/admin/types.ts +352 -0
  153. package/src/admin/webhook-manager.ts +245 -0
  154. package/src/config.ts +177 -5
  155. package/src/connection/connection-manager.ts +50 -6
  156. package/src/connection/flow-controller.ts +117 -0
  157. package/src/connection/socket-connection.ts +103 -21
  158. package/src/delivery-service.ts +740 -0
  159. package/src/dwn-error.ts +9 -0
  160. package/src/dwn-server.ts +242 -14
  161. package/src/http-api.ts +271 -30
  162. package/src/index.ts +13 -2
  163. package/src/json-rpc-api.ts +2 -1
  164. package/src/json-rpc-handlers/dwn/process-message.ts +140 -5
  165. package/src/json-rpc-handlers/subscription/ack.ts +63 -0
  166. package/src/json-rpc-handlers/subscription/close.ts +2 -6
  167. package/src/json-rpc-handlers/subscription/index.ts +1 -0
  168. package/src/lib/json-rpc-router.ts +22 -6
  169. package/src/lib/sql-utils.ts +7 -0
  170. package/src/main.ts +0 -8
  171. package/src/message-processed-hook.ts +33 -0
  172. package/src/metrics.ts +50 -1
  173. package/src/plugins/event-log-nats.ts +466 -0
  174. package/src/rate-limiter.ts +143 -0
  175. package/src/registration/jwt-provider-auth-plugin.ts +119 -0
  176. package/src/registration/open-auth-handler.ts +263 -0
  177. package/src/registration/proof-of-work-manager.ts +1 -1
  178. package/src/registration/provider-auth-plugin.ts +84 -0
  179. package/src/registration/registration-manager.ts +108 -12
  180. package/src/registration/registration-store.ts +326 -17
  181. package/src/storage.ts +121 -27
  182. package/src/web5-connect/sql-ttl-cache.ts +7 -1
  183. package/src/ws-api.ts +30 -2
  184. package/dist/esm/src/json-rpc-socket.d.ts +0 -39
  185. package/dist/esm/src/json-rpc-socket.d.ts.map +0 -1
  186. package/dist/esm/src/json-rpc-socket.js +0 -125
  187. package/dist/esm/src/json-rpc-socket.js.map +0 -1
  188. package/dist/esm/src/lib/json-rpc.d.ts +0 -54
  189. package/dist/esm/src/lib/json-rpc.d.ts.map +0 -1
  190. package/dist/esm/src/lib/json-rpc.js +0 -60
  191. package/dist/esm/src/lib/json-rpc.js.map +0 -1
  192. package/dist/esm/src/registration/proof-of-work-types.d.ts +0 -8
  193. package/dist/esm/src/registration/proof-of-work-types.d.ts.map +0 -1
  194. package/dist/esm/src/registration/proof-of-work-types.js +0 -2
  195. package/dist/esm/src/registration/proof-of-work-types.js.map +0 -1
  196. package/dist/esm/src/registration/registration-types.d.ts +0 -18
  197. package/dist/esm/src/registration/registration-types.d.ts.map +0 -1
  198. package/dist/esm/src/registration/registration-types.js +0 -2
  199. package/dist/esm/src/registration/registration-types.js.map +0 -1
  200. package/dist/esm/tests/common-scenario-validator.d.ts +0 -11
  201. package/dist/esm/tests/common-scenario-validator.d.ts.map +0 -1
  202. package/dist/esm/tests/common-scenario-validator.js +0 -113
  203. package/dist/esm/tests/common-scenario-validator.js.map +0 -1
  204. package/dist/esm/tests/connection/connection-manager.spec.d.ts +0 -2
  205. package/dist/esm/tests/connection/connection-manager.spec.d.ts.map +0 -1
  206. package/dist/esm/tests/connection/connection-manager.spec.js +0 -49
  207. package/dist/esm/tests/connection/connection-manager.spec.js.map +0 -1
  208. package/dist/esm/tests/connection/socket-connection.spec.d.ts +0 -2
  209. package/dist/esm/tests/connection/socket-connection.spec.d.ts.map +0 -1
  210. package/dist/esm/tests/connection/socket-connection.spec.js +0 -147
  211. package/dist/esm/tests/connection/socket-connection.spec.js.map +0 -1
  212. package/dist/esm/tests/cors/http-api.browser.d.ts +0 -2
  213. package/dist/esm/tests/cors/http-api.browser.d.ts.map +0 -1
  214. package/dist/esm/tests/cors/http-api.browser.js +0 -60
  215. package/dist/esm/tests/cors/http-api.browser.js.map +0 -1
  216. package/dist/esm/tests/cors/ping.browser.d.ts +0 -2
  217. package/dist/esm/tests/cors/ping.browser.d.ts.map +0 -1
  218. package/dist/esm/tests/cors/ping.browser.js +0 -7
  219. package/dist/esm/tests/cors/ping.browser.js.map +0 -1
  220. package/dist/esm/tests/dwn-process-message.spec.d.ts +0 -2
  221. package/dist/esm/tests/dwn-process-message.spec.d.ts.map +0 -1
  222. package/dist/esm/tests/dwn-process-message.spec.js +0 -172
  223. package/dist/esm/tests/dwn-process-message.spec.js.map +0 -1
  224. package/dist/esm/tests/dwn-server.spec.d.ts +0 -2
  225. package/dist/esm/tests/dwn-server.spec.d.ts.map +0 -1
  226. package/dist/esm/tests/dwn-server.spec.js +0 -48
  227. package/dist/esm/tests/dwn-server.spec.js.map +0 -1
  228. package/dist/esm/tests/http-api.spec.d.ts +0 -2
  229. package/dist/esm/tests/http-api.spec.d.ts.map +0 -1
  230. package/dist/esm/tests/http-api.spec.js +0 -782
  231. package/dist/esm/tests/http-api.spec.js.map +0 -1
  232. package/dist/esm/tests/json-rpc-socket.spec.d.ts +0 -2
  233. package/dist/esm/tests/json-rpc-socket.spec.d.ts.map +0 -1
  234. package/dist/esm/tests/json-rpc-socket.spec.js +0 -227
  235. package/dist/esm/tests/json-rpc-socket.spec.js.map +0 -1
  236. package/dist/esm/tests/plugins/data-store-sqlite.d.ts +0 -17
  237. package/dist/esm/tests/plugins/data-store-sqlite.d.ts.map +0 -1
  238. package/dist/esm/tests/plugins/data-store-sqlite.js +0 -23
  239. package/dist/esm/tests/plugins/data-store-sqlite.js.map +0 -1
  240. package/dist/esm/tests/plugins/event-log-sqlite.d.ts +0 -17
  241. package/dist/esm/tests/plugins/event-log-sqlite.d.ts.map +0 -1
  242. package/dist/esm/tests/plugins/event-log-sqlite.js +0 -23
  243. package/dist/esm/tests/plugins/event-log-sqlite.js.map +0 -1
  244. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts +0 -17
  245. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts.map +0 -1
  246. package/dist/esm/tests/plugins/event-stream-in-memory.js +0 -21
  247. package/dist/esm/tests/plugins/event-stream-in-memory.js.map +0 -1
  248. package/dist/esm/tests/plugins/message-store-sqlite.d.ts +0 -17
  249. package/dist/esm/tests/plugins/message-store-sqlite.d.ts.map +0 -1
  250. package/dist/esm/tests/plugins/message-store-sqlite.js +0 -23
  251. package/dist/esm/tests/plugins/message-store-sqlite.js.map +0 -1
  252. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts +0 -17
  253. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts.map +0 -1
  254. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js +0 -23
  255. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js.map +0 -1
  256. package/dist/esm/tests/process-handler.spec.d.ts +0 -2
  257. package/dist/esm/tests/process-handler.spec.d.ts.map +0 -1
  258. package/dist/esm/tests/process-handler.spec.js +0 -60
  259. package/dist/esm/tests/process-handler.spec.js.map +0 -1
  260. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts +0 -2
  261. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts.map +0 -1
  262. package/dist/esm/tests/registration/proof-of-work-manager.spec.js +0 -156
  263. package/dist/esm/tests/registration/proof-of-work-manager.spec.js.map +0 -1
  264. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts +0 -2
  265. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts.map +0 -1
  266. package/dist/esm/tests/rpc-subscribe-close.spec.js +0 -81
  267. package/dist/esm/tests/rpc-subscribe-close.spec.js.map +0 -1
  268. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts +0 -2
  269. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts.map +0 -1
  270. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js +0 -74
  271. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js.map +0 -1
  272. package/dist/esm/tests/scenarios/registration.spec.d.ts +0 -2
  273. package/dist/esm/tests/scenarios/registration.spec.d.ts.map +0 -1
  274. package/dist/esm/tests/scenarios/registration.spec.js +0 -511
  275. package/dist/esm/tests/scenarios/registration.spec.js.map +0 -1
  276. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts +0 -2
  277. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts.map +0 -1
  278. package/dist/esm/tests/scenarios/web5-connect.spec.js +0 -141
  279. package/dist/esm/tests/scenarios/web5-connect.spec.js.map +0 -1
  280. package/dist/esm/tests/test-dwn.d.ts +0 -7
  281. package/dist/esm/tests/test-dwn.d.ts.map +0 -1
  282. package/dist/esm/tests/test-dwn.js +0 -28
  283. package/dist/esm/tests/test-dwn.js.map +0 -1
  284. package/dist/esm/tests/utils.d.ts +0 -43
  285. package/dist/esm/tests/utils.d.ts.map +0 -1
  286. package/dist/esm/tests/utils.js +0 -107
  287. package/dist/esm/tests/utils.js.map +0 -1
  288. package/dist/esm/tests/ws-api.spec.d.ts +0 -2
  289. package/dist/esm/tests/ws-api.spec.d.ts.map +0 -1
  290. package/dist/esm/tests/ws-api.spec.js +0 -332
  291. package/dist/esm/tests/ws-api.spec.js.map +0 -1
  292. package/src/json-rpc-socket.ts +0 -156
  293. package/src/lib/json-rpc.ts +0 -126
  294. package/src/registration/proof-of-work-types.ts +0 -7
  295. package/src/registration/registration-types.ts +0 -18
package/src/http-api.ts CHANGED
@@ -1,28 +1,50 @@
1
+ import type { JsonRpcRequest } from '@enbox/dwn-clients';
1
2
  import type { RecordsReadReply } from '@enbox/dwn-sdk-js';
3
+ import type { ServerInfo } from '@enbox/dwn-clients';
2
4
  import type { Server, ServerWebSocket } from 'bun';
3
5
 
6
+ import type { ActivityLog } from './admin/activity-log.js';
7
+ import type { AdminApi } from './admin/admin-api.js';
8
+ import type { AdminStore } from './admin/admin-store.js';
9
+ import type { DwnServerConfig } from './config.js';
10
+ import type { DwnServerError } from './dwn-error.js';
11
+ import type { MessageProcessedHook } from './message-processed-hook.js';
12
+ import type { OpenAuthHandler } from './registration/open-auth-handler.js';
13
+ import type { RateLimiter } from './rate-limiter.js';
14
+ import type { RegistrationManager } from './registration/registration-manager.js';
15
+ import type { RegistrationStore } from './registration/registration-store.js';
16
+ import type { RequestContext } from './lib/json-rpc-router.js';
17
+ import type { SocketConnection } from './connection/socket-connection.js';
18
+
4
19
  import log from 'loglevel';
5
20
 
6
21
  import { Convert } from '@enbox/common';
7
- import { readFileSync } from 'fs';
8
22
  import { register } from 'prom-client';
9
23
  import { v4 as uuidv4 } from 'uuid';
24
+ import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from '@enbox/dwn-clients';
10
25
  import { DataStream, DateSort, type Dwn, ProtocolsQuery, RecordsQuery, RecordsRead } from '@enbox/dwn-sdk-js';
11
-
12
-
13
- import type { DwnServerConfig } from './config.js';
14
- import type { DwnServerError } from './dwn-error.js';
15
- import type { JsonRpcRequest } from './lib/json-rpc.js';
16
- import type { RegistrationManager } from './registration/registration-manager.js';
17
- import type { RequestContext } from './lib/json-rpc-router.js';
18
- import type { SocketConnection } from './connection/socket-connection.js';
26
+ import { existsSync, readFileSync } from 'fs';
27
+ import { join, resolve } from 'path';
19
28
 
20
29
  import { config } from './config.js';
21
30
  import { jsonRpcRouter } from './json-rpc-api.js';
31
+ import { validateAdminAuth } from './admin/admin-auth.js';
22
32
  import { Web5ConnectServer } from './web5-connect/web5-connect-server.js';
23
- import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js';
24
33
  import { requestCounter, responseHistogram } from './metrics.js';
25
34
 
35
+ /** Property names that must never be used as keys when building objects from user input. */
36
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
37
+
38
+ // Resolve admin UI dist path at module load time. Gracefully handle the case
39
+ // where the admin UI package is not installed.
40
+ let resolvedAdminUiPath: string | undefined;
41
+ try {
42
+ const adminUiModule = require('@enbox/dwn-server-admin-ui');
43
+ resolvedAdminUiPath = adminUiModule.adminUiDistPath;
44
+ } catch {
45
+ // Admin UI package not installed — static serving will be disabled.
46
+ }
47
+
26
48
  /** Data attached to each Bun WebSocket via `ws.data`. */
27
49
  export interface WsData {
28
50
  connection: SocketConnection;
@@ -32,6 +54,15 @@ export class HttpApi {
32
54
  #config: DwnServerConfig;
33
55
  #packageInfo: { version?: string, sdkVersion?: string, server: string };
34
56
  #server!: Server<WsData>;
57
+ #adminApi: AdminApi | undefined;
58
+ #activityLog: ActivityLog | undefined;
59
+ #adminStore: AdminStore | undefined;
60
+ #registrationStore: RegistrationStore | undefined;
61
+ #ipRateLimiter: RateLimiter | undefined;
62
+ #tenantRateLimiter: RateLimiter | undefined;
63
+ #messageProcessedHooks: MessageProcessedHook[];
64
+ #openAuthHandler: OpenAuthHandler | undefined;
65
+ #adminUiPath: string | undefined;
35
66
  web5ConnectServer: Web5ConnectServer;
36
67
  registrationManager: RegistrationManager;
37
68
  dwn: Dwn;
@@ -42,11 +73,20 @@ export class HttpApi {
42
73
  private constructor() { }
43
74
 
44
75
  public static async create(
45
- config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager
76
+ config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager,
77
+ adminApi?: AdminApi, activityLog?: ActivityLog,
78
+ options?: {
79
+ adminStore? : AdminStore;
80
+ registrationStore? : RegistrationStore;
81
+ ipRateLimiter? : RateLimiter;
82
+ tenantRateLimiter? : RateLimiter;
83
+ messageProcessedHooks? : MessageProcessedHook[];
84
+ openAuthHandler? : OpenAuthHandler;
85
+ },
46
86
  ): Promise<HttpApi> {
47
87
  const httpApi = new HttpApi();
48
88
 
49
- log.info(config);
89
+ log.info(HttpApi.#redactConfig(config));
50
90
 
51
91
  httpApi.#packageInfo = {
52
92
  server: config.serverName,
@@ -64,6 +104,15 @@ export class HttpApi {
64
104
 
65
105
  httpApi.#config = config;
66
106
  httpApi.dwn = dwn;
107
+ httpApi.#adminApi = adminApi;
108
+ httpApi.#activityLog = activityLog;
109
+ httpApi.#adminStore = options?.adminStore;
110
+ httpApi.#registrationStore = options?.registrationStore;
111
+ httpApi.#ipRateLimiter = options?.ipRateLimiter;
112
+ httpApi.#tenantRateLimiter = options?.tenantRateLimiter;
113
+ httpApi.#messageProcessedHooks = options?.messageProcessedHooks ?? [];
114
+ httpApi.#openAuthHandler = options?.openAuthHandler;
115
+ httpApi.#adminUiPath = resolvedAdminUiPath;
67
116
 
68
117
  if (registrationManager !== undefined) {
69
118
  httpApi.registrationManager = registrationManager;
@@ -81,6 +130,18 @@ export class HttpApi {
81
130
  return this.#server;
82
131
  }
83
132
 
133
+ get ipRateLimiter(): RateLimiter | undefined {
134
+ return this.#ipRateLimiter;
135
+ }
136
+
137
+ get tenantRateLimiter(): RateLimiter | undefined {
138
+ return this.#tenantRateLimiter;
139
+ }
140
+
141
+ get messageProcessedHooks(): MessageProcessedHook[] {
142
+ return this.#messageProcessedHooks;
143
+ }
144
+
84
145
  // ---------------------------------------------------------------------------
85
146
  // HTTP request handler
86
147
  // ---------------------------------------------------------------------------
@@ -106,6 +167,25 @@ export class HttpApi {
106
167
  return new Response('WebSocket upgrade failed', { status: 400 });
107
168
  }
108
169
 
170
+ // --- Per-IP rate limiting ---
171
+ if (self.#ipRateLimiter) {
172
+ const ip = server.requestIP(req)?.address ?? 'unknown';
173
+ const result = self.#ipRateLimiter.consume(ip);
174
+ if (result.allowed === false) {
175
+ const retryAfterSec = Math.ceil(result.retryAfterMs / 1000);
176
+ return new Response(
177
+ JSON.stringify({ error: 'Rate limit exceeded' }),
178
+ {
179
+ status : 429,
180
+ headers : {
181
+ 'content-type' : 'application/json',
182
+ 'retry-after' : String(retryAfterSec),
183
+ },
184
+ },
185
+ );
186
+ }
187
+ }
188
+
109
189
  // --- Route matching ---
110
190
  let response: Response;
111
191
  try {
@@ -116,10 +196,15 @@ export class HttpApi {
116
196
  }
117
197
 
118
198
  // --- CORS headers ---
119
- response.headers.set('access-control-allow-origin', '*');
120
- response.headers.set('access-control-allow-methods', 'GET, POST, OPTIONS');
121
- response.headers.set('access-control-allow-headers', '*');
122
- response.headers.set('access-control-expose-headers', 'dwn-response');
199
+ // Admin API and metrics endpoints do not receive wildcard CORS headers
200
+ // to limit cross-origin access when the admin token is configured.
201
+ const isAdminRoute = path.startsWith('/admin') || path === '/metrics';
202
+ if (!isAdminRoute) {
203
+ response.headers.set('access-control-allow-origin', '*');
204
+ response.headers.set('access-control-allow-methods', 'GET, POST, OPTIONS');
205
+ response.headers.set('access-control-allow-headers', '*');
206
+ response.headers.set('access-control-expose-headers', 'dwn-response');
207
+ }
123
208
 
124
209
  // --- Response-time metrics ---
125
210
  const elapsed = performance.now() - startTime;
@@ -134,6 +219,7 @@ export class HttpApi {
134
219
  },
135
220
 
136
221
  websocket: {
222
+ maxPayloadLength: self.#config.maxRecordDataSize,
137
223
  open(ws: ServerWebSocket<WsData>): void {
138
224
  if (self.onWebSocketConnection) {
139
225
  self.onWebSocketConnection(ws);
@@ -151,17 +237,65 @@ export class HttpApi {
151
237
  connection.close();
152
238
  }
153
239
  },
154
- // Bun automatically responds to pings with pongs
240
+ pong(ws: ServerWebSocket<WsData>): void {
241
+ const connection = ws.data?.connection;
242
+ if (connection) {
243
+ connection.pong();
244
+ }
245
+ },
155
246
  },
156
247
  });
157
248
  }
158
249
 
159
250
  async close(): Promise<void> {
251
+ if (this.#openAuthHandler) {
252
+ this.#openAuthHandler.destroy();
253
+ }
160
254
  if (this.#server) {
161
255
  this.#server.stop(true); // close all connections immediately
162
256
  }
163
257
  }
164
258
 
259
+ // ---------------------------------------------------------------------------
260
+ // Admin UI static file serving
261
+ // ---------------------------------------------------------------------------
262
+
263
+ /**
264
+ * Serves static files from the admin UI dist directory. Returns `null` when
265
+ * the admin UI package is not installed or the requested file does not exist.
266
+ * All non-file paths under `/admin` fall back to `index.html` (SPA routing).
267
+ */
268
+ #serveAdminUi(path: string): Response | null {
269
+ if (!this.#adminUiPath) {
270
+ return null;
271
+ }
272
+
273
+ // Strip the `/admin` prefix to get the file path within the dist directory.
274
+ const relativePath = path.replace(/^\/admin\/?/, '');
275
+
276
+ // Map to a file on disk. Empty path or paths without an extension get
277
+ // the SPA index.html (client-side routing).
278
+ let filePath: string;
279
+ if (relativePath === '' || !relativePath.includes('.')) {
280
+ filePath = join(this.#adminUiPath, 'index.html');
281
+ } else {
282
+ filePath = join(this.#adminUiPath, relativePath);
283
+ }
284
+
285
+ // Prevent path traversal: resolved path must stay within the admin UI directory.
286
+ const resolvedBase = resolve(this.#adminUiPath);
287
+ if (!resolve(filePath).startsWith(resolvedBase)) {
288
+ return null;
289
+ }
290
+
291
+ if (!existsSync(filePath)) {
292
+ return null;
293
+ }
294
+
295
+ const file = Bun.file(filePath);
296
+ return new Response(file);
297
+ }
298
+
165
299
  // ---------------------------------------------------------------------------
166
300
  // Router
167
301
  // ---------------------------------------------------------------------------
@@ -178,6 +312,13 @@ export class HttpApi {
178
312
  }
179
313
 
180
314
  if (method === 'GET' && path === '/metrics') {
315
+ // Metrics require admin authentication when an admin token is configured.
316
+ if (this.#config.adminToken) {
317
+ const authError = validateAdminAuth(req, this.#config);
318
+ if (authError) {
319
+ return authError;
320
+ }
321
+ }
181
322
  try {
182
323
  const metricsBody = await register.metrics();
183
324
  return new Response(metricsBody, {
@@ -204,6 +345,32 @@ export class HttpApi {
204
345
  return this.#handleJsonRpcPost(req);
205
346
  }
206
347
 
348
+ // --- Admin API routes ---
349
+ if (path.startsWith('/admin/api/') && this.#adminApi) {
350
+ return this.#adminApi.route(req, url, path, method);
351
+ }
352
+
353
+ // --- Admin UI static files (only when admin API is enabled) ---
354
+ if (method === 'GET' && path.startsWith('/admin') && this.#adminApi) {
355
+ const uiResponse = this.#serveAdminUi(path);
356
+ if (uiResponse) {
357
+ return uiResponse;
358
+ }
359
+ }
360
+
361
+ // --- Provider auth (open-auth) routes ---
362
+ if (this.#openAuthHandler && path.startsWith('/provider-auth/')) {
363
+ if (method === 'GET' && path === '/provider-auth/authorize') {
364
+ return this.#openAuthHandler.handleAuthorize(url);
365
+ }
366
+ if (method === 'POST' && path === '/provider-auth/token') {
367
+ return this.#openAuthHandler.handleToken(req);
368
+ }
369
+ if (method === 'POST' && path === '/provider-auth/refresh') {
370
+ return this.#openAuthHandler.handleRefresh(req);
371
+ }
372
+ }
373
+
207
374
  // --- Registration routes ---
208
375
  const registrationResponse = await this.#matchRegistrationRoutes(req, path, method);
209
376
  if (registrationResponse) {
@@ -275,6 +442,38 @@ export class HttpApi {
275
442
  return new Response('Not Found', { status: 404 });
276
443
  }
277
444
 
445
+ // ---------------------------------------------------------------------------
446
+ // Security helpers
447
+ // ---------------------------------------------------------------------------
448
+
449
+ /** Returns `true` if the given key is a prototype-pollution-dangerous property name. */
450
+ static #isDangerousKey(key: string | undefined): boolean {
451
+ return key !== undefined && DANGEROUS_KEYS.has(key);
452
+ }
453
+
454
+ /** Returns `true` if any element in `keys` is a dangerous property name. */
455
+ static #hasDangerousKey(keys: string[]): boolean {
456
+ return keys.some(k => DANGEROUS_KEYS.has(k));
457
+ }
458
+
459
+ /** Returns a shallow copy of the config with sensitive values redacted for logging. */
460
+ static #redactConfig(cfg: DwnServerConfig): Record<string, unknown> {
461
+ const redacted: Record<string, unknown> = { ...cfg };
462
+ const sensitiveKeys = ['adminToken', 'providerAuthJwtSecret'];
463
+ for (const key of sensitiveKeys) {
464
+ if (redacted[key]) {
465
+ redacted[key] = '[REDACTED]';
466
+ }
467
+ }
468
+ // Redact passwords in connection-string-like values.
469
+ for (const [key, value] of Object.entries(redacted)) {
470
+ if (typeof value === 'string' && /^(?:postgres|mysql|sqlite):\/\//.test(value) && value.includes('@')) {
471
+ redacted[key] = value.replace(/:\/\/([^:]+):([^@]+)@/, '://$1:****@');
472
+ }
473
+ }
474
+ return redacted;
475
+ }
476
+
278
477
  // ---------------------------------------------------------------------------
279
478
  // Handlers
280
479
  // ---------------------------------------------------------------------------
@@ -287,16 +486,31 @@ export class HttpApi {
287
486
  if (config.termsOfServiceFilePath !== undefined) {
288
487
  registrationRequirements.push('terms-of-service');
289
488
  }
489
+ if (config.providerAuthEnabled && !registrationRequirements.includes('provider-auth-v0')) {
490
+ registrationRequirements.push('provider-auth-v0');
491
+ }
290
492
 
291
- return Response.json({
292
- url : config.baseUrl,
293
- server : this.#packageInfo.server,
493
+ const serverInfo: ServerInfo = {
294
494
  maxFileSize : config.maxRecordDataSize,
495
+ maxInFlight : config.maxInFlight,
295
496
  registrationRequirements : registrationRequirements,
296
- version : this.#packageInfo.version,
497
+ server : this.#packageInfo.server,
297
498
  sdkVersion : this.#packageInfo.sdkVersion,
499
+ url : config.baseUrl,
500
+ version : this.#packageInfo.version,
298
501
  webSocketSupport : config.webSocketSupport,
299
- });
502
+ };
503
+
504
+ if (config.providerAuthEnabled) {
505
+ serverInfo.providerAuth = {
506
+ authorizeUrl : config.providerAuthAuthorizeUrl,
507
+ tokenUrl : config.providerAuthTokenUrl,
508
+ refreshUrl : config.providerAuthRefreshUrl,
509
+ managementUrl : config.providerAuthManagementUrl,
510
+ };
511
+ }
512
+
513
+ return Response.json(serverInfo);
300
514
  }
301
515
 
302
516
  async #handleJsonRpcPost(req: Request): Promise<Response> {
@@ -319,9 +533,13 @@ export class HttpApi {
319
533
  return Response.json(reply, { status: 400 });
320
534
  }
321
535
 
322
- // Read the request body into bytes first, then wrap in a fresh ReadableStream.
323
- // Bun's native Request.body stream has an incompatible reader.releaseLock()
324
- // that breaks DWN SDK's DataStream.toBytes(), so we materialise the body here.
536
+ // Materialise the request body before passing to DWN.
537
+ // Bun's Bun.serve() returns a ReadableStream for req.body that is
538
+ // incompatible with the ReadableStream consumer code in dwn-sdk-js,
539
+ // causing DataStream.toBytes() to crash with "undefined is not a
540
+ // function" at reader.releaseLock(). Buffering via arrayBuffer()
541
+ // converts it into a well-behaved stream that dwn-sdk-js can consume.
542
+ // TODO: https://github.com/enboxorg/enbox/issues/90 — remove once Bun ships fix
325
543
  const contentLength = req.headers.get('content-length');
326
544
  const transferEncoding = req.headers.get('transfer-encoding');
327
545
  let requestDataStream: ReadableStream<Uint8Array> | undefined;
@@ -331,15 +549,31 @@ export class HttpApi {
331
549
  }
332
550
 
333
551
  const requestContext: RequestContext = {
334
- dwn : this.dwn,
335
- transport : 'http',
336
- dataStream : requestDataStream,
552
+ dwn : this.dwn,
553
+ transport : 'http',
554
+ dataStream : requestDataStream,
555
+ activityLog : this.#activityLog,
556
+ adminStore : this.#adminStore,
557
+ registrationStore : this.#registrationStore,
558
+ config : this.#config,
559
+ tenantRateLimiter : this.#tenantRateLimiter,
560
+ messageProcessedHooks : this.#messageProcessedHooks,
337
561
  };
338
562
  const { jsonRpcResponse, dataStream: responseDataStream } =
339
563
  await jsonRpcRouter.handle(dwnRpcRequest, requestContext);
340
564
 
341
565
  if (jsonRpcResponse.error) {
342
566
  requestCounter.inc({ method: dwnRpcRequest.method, error: 1 });
567
+
568
+ // Return HTTP 429 with Retry-After header for rate-limit rejections.
569
+ if (jsonRpcResponse.error.code === JsonRpcErrorCodes.TooManyRequests) {
570
+ const retryAfterSec = jsonRpcResponse.error.data?.retryAfterSec ?? 1;
571
+ return Response.json(jsonRpcResponse, {
572
+ status : 429,
573
+ headers : { 'retry-after': String(retryAfterSec) },
574
+ });
575
+ }
576
+
343
577
  return Response.json(jsonRpcResponse, { status: 500 });
344
578
  }
345
579
 
@@ -400,6 +634,9 @@ export class HttpApi {
400
634
  for (const [param, value] of url.searchParams) {
401
635
  const keys = param.split('.');
402
636
  const lastKey = keys.pop();
637
+ if (HttpApi.#hasDangerousKey(keys) || HttpApi.#isDangerousKey(lastKey)) {
638
+ continue;
639
+ }
403
640
  const nestObj = (obj: Record<string, any>, key: string): Record<string, any> =>
404
641
  obj[key] = obj[key] || {};
405
642
  const lastLevelObject = keys.reduce(nestObj, queryOptions);
@@ -481,6 +718,9 @@ export class HttpApi {
481
718
  for (const [param, value] of url.searchParams) {
482
719
  const keys = param.split('.');
483
720
  const lastKey = keys.pop();
721
+ if (HttpApi.#hasDangerousKey(keys) || HttpApi.#isDangerousKey(lastKey)) {
722
+ continue;
723
+ }
484
724
  const nestObj = (obj: Record<string, any>, key: string): Record<string, any> =>
485
725
  obj[key] = obj[key] || {};
486
726
  const lastLevelObject = keys.reduce(nestObj, recordsQueryOptions);
@@ -498,7 +738,8 @@ export class HttpApi {
498
738
  headers: { 'content-type': 'application/json' },
499
739
  });
500
740
  } catch (error) {
501
- return Response.json(error, { status: 400 });
741
+ log.error('Error processing query records request', error);
742
+ return Response.json({ error: 'Bad Request' }, { status: 400 });
502
743
  }
503
744
  }
504
745
 
@@ -523,7 +764,7 @@ export class HttpApi {
523
764
  if (method === 'POST' && path === '/registration'
524
765
  && this.#config.registrationStoreUrl !== undefined) {
525
766
  const requestBody = await req.json();
526
- log.info('Registration request:', requestBody);
767
+ log.info('Registration request received');
527
768
 
528
769
  try {
529
770
  await this.registrationManager.handleRegistrationRequest(requestBody);
package/src/index.ts CHANGED
@@ -1,6 +1,17 @@
1
- export { DwnServerConfig } from './config.js';
1
+ export { ActivityLog, AdminApi, AdminStore } from './admin/index.js';
2
+ export type {
3
+ AdminActivityEvent,
4
+ AdminConnectionSnapshot,
5
+ AdminHealthCheck,
6
+ AdminServerStats,
7
+ AdminTenantDetail,
8
+ AdminTenantSummary,
9
+ } from './admin/index.js';
10
+ export { config as defaultDwnServerConfig, DwnServerConfig } from './config.js';
11
+ export { DeliveryService } from './delivery-service.js';
2
12
  export { DwnServer, DwnServerOptions } from './dwn-server.js';
3
13
  export { HttpApi } from './http-api.js';
4
14
  export { jsonRpcRouter } from './json-rpc-api.js';
5
- export { StoreType, BackendTypes, DwnStore } from './storage.js';
15
+ export type { MessageProcessedContext, MessageProcessedHook } from './message-processed-hook.js';
16
+ export { getDwnConfig, StoreType, BackendTypes, DwnStore } from './storage.js';
6
17
  export { WsApi } from './ws-api.js';
@@ -1,11 +1,12 @@
1
1
  import { JsonRpcRouter } from './lib/json-rpc-router.js';
2
2
 
3
3
  import { handleDwnProcessMessage } from './json-rpc-handlers/dwn/index.js';
4
- import { handleSubscriptionsClose } from './json-rpc-handlers/subscription/index.js';
4
+ import { handleSubscriptionAck, handleSubscriptionsClose } from './json-rpc-handlers/subscription/index.js';
5
5
 
6
6
  export const jsonRpcRouter = new JsonRpcRouter();
7
7
 
8
8
  jsonRpcRouter.on('dwn.processMessage', handleDwnProcessMessage);
9
9
  jsonRpcRouter.on('rpc.subscribe.dwn.processMessage', handleDwnProcessMessage);
10
10
 
11
+ jsonRpcRouter.on('rpc.ack', handleSubscriptionAck);
11
12
  jsonRpcRouter.on('rpc.subscribe.close', handleSubscriptionsClose);
@@ -4,17 +4,19 @@ import { DwnInterfaceName, DwnMethodName } from '@enbox/dwn-sdk-js';
4
4
  import log from 'loglevel';
5
5
  import { v4 as uuidv4 } from 'uuid';
6
6
 
7
- import type { JsonRpcSubscription } from '../../lib/json-rpc.js';
7
+ import type { JsonRpcSubscription } from '@enbox/dwn-clients';
8
8
  import type {
9
9
  HandlerResponse,
10
10
  JsonRpcHandler,
11
11
  } from '../../lib/json-rpc-router.js';
12
12
 
13
+ import { DwnServerErrorCode } from '../../dwn-error.js';
14
+ import { requestDataBytesTotal } from '../../metrics.js';
13
15
  import {
14
16
  createJsonRpcErrorResponse,
15
17
  createJsonRpcSuccessResponse,
16
18
  JsonRpcErrorCodes,
17
- } from '../../lib/json-rpc.js';
19
+ } from '@enbox/dwn-clients';
18
20
 
19
21
 
20
22
  export const handleDwnProcessMessage: JsonRpcHandler = async (
@@ -73,6 +75,34 @@ export const handleDwnProcessMessage: JsonRpcHandler = async (
73
75
  return { jsonRpcResponse };
74
76
  }
75
77
 
78
+ // --- Per-tenant rate limiting (before any DWN processing) ---
79
+ if (context.tenantRateLimiter) {
80
+ const result = context.tenantRateLimiter.consume(target);
81
+ if (result.allowed === false) {
82
+ const retryAfterSec = Math.ceil(result.retryAfterMs / 1000);
83
+ const jsonRpcResponse = createJsonRpcErrorResponse(
84
+ requestId,
85
+ JsonRpcErrorCodes.TooManyRequests,
86
+ `${DwnServerErrorCode.RateLimitExceeded}: tenant rate limit exceeded, retry after ${retryAfterSec}s`,
87
+ { retryAfterSec },
88
+ );
89
+ return { jsonRpcResponse };
90
+ }
91
+ }
92
+
93
+ // --- Per-tenant storage quota enforcement (RecordsWrite only) ---
94
+ if (
95
+ context.config &&
96
+ context.adminStore &&
97
+ message.descriptor.interface === DwnInterfaceName.Records &&
98
+ message.descriptor.method === DwnMethodName.Write
99
+ ) {
100
+ const quotaResult = await enforceQuota(target, message, context);
101
+ if (quotaResult !== undefined) {
102
+ return quotaResult;
103
+ }
104
+ }
105
+
76
106
  const reply = await dwn.processMessage(target, message, {
77
107
  dataStream,
78
108
  subscriptionHandler: subscriptionRequest?.subscriptionHandler,
@@ -107,16 +137,121 @@ export const handleDwnProcessMessage: JsonRpcHandler = async (
107
137
  responsePayload.dataStream = recordDataStream;
108
138
  }
109
139
 
140
+ // --- Fire-and-forget: post-processing hooks ---
141
+ const statusCode = reply.status?.code ?? 0;
142
+ if (context.messageProcessedHooks) {
143
+ const hookContext = { tenant: target, message, status: reply.status, transport };
144
+ for (const hook of context.messageProcessedHooks) {
145
+ try {
146
+ const result = hook.onMessageProcessed(hookContext);
147
+ if (result instanceof Promise) {
148
+ result.catch((err: unknown): void => {
149
+ log.error('MessageProcessedHook error', err);
150
+ });
151
+ }
152
+ } catch (err) {
153
+ log.error('MessageProcessedHook error', err);
154
+ }
155
+ }
156
+ }
157
+
158
+ // Capture activity event and per-request metrics.
159
+ const dwnInterface = message.descriptor.interface as string;
160
+ const dwnMethod = message.descriptor.method as string;
161
+ const dataSizeBytes = (message.descriptor as { dataSize?: number }).dataSize;
162
+
163
+ if (dataSizeBytes !== undefined && dataSizeBytes > 0) {
164
+ requestDataBytesTotal.inc({ interface: dwnInterface, method: dwnMethod }, dataSizeBytes);
165
+ }
166
+
167
+ if (context.activityLog) {
168
+ context.activityLog.record({
169
+ tenant : target,
170
+ interface : dwnInterface,
171
+ method : dwnMethod,
172
+ statusCode,
173
+ transport,
174
+ dataSizeBytes,
175
+ });
176
+ }
177
+
110
178
  return responsePayload;
111
179
  } catch (error) {
180
+ // Log the full error internally but return a generic message to the client
181
+ // to avoid leaking implementation details (SQL errors, file paths, etc.).
182
+ log.error('handleDwnProcessMessage error', error);
183
+
112
184
  const jsonRpcResponse = createJsonRpcErrorResponse(
113
185
  requestId,
114
186
  JsonRpcErrorCodes.InternalError,
115
- error.message,
187
+ 'an unexpected error occurred while processing the message',
116
188
  );
117
189
 
118
- // log the unhandled error response
119
- log.error('handleDwnProcessMessage error', jsonRpcResponse, dwnRequest, error);
120
190
  return { jsonRpcResponse } as HandlerResponse;
121
191
  }
122
192
  };
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Quota enforcement helper
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /**
199
+ * Checks whether the tenant has exceeded their message count or storage quota.
200
+ * Returns a JSON-RPC error response if the quota is exceeded, or `undefined` to proceed.
201
+ */
202
+ async function enforceQuota(
203
+ target: string,
204
+ message: GenericMessage,
205
+ context: Parameters<JsonRpcHandler>[1],
206
+ ): Promise<HandlerResponse | undefined> {
207
+ const { config, adminStore, registrationStore } = context;
208
+ const requestId = (message as any).recordId ?? uuidv4();
209
+
210
+ // Resolve effective quota: per-tenant override > global config > unlimited.
211
+ let maxMessages = config!.quotaMaxMessages ?? 0;
212
+ let maxStorageBytes = config!.quotaMaxStorageBytes ?? 0;
213
+
214
+ if (registrationStore) {
215
+ const tenantQuota = await registrationStore.getQuota(target);
216
+ if (tenantQuota !== undefined) {
217
+ maxMessages = tenantQuota.maxMessages ?? maxMessages;
218
+ maxStorageBytes = tenantQuota.maxStorageBytes ?? maxStorageBytes;
219
+ }
220
+ }
221
+
222
+ // 0 means unlimited — skip enforcement.
223
+ if (maxMessages === 0 && maxStorageBytes === 0) {
224
+ return undefined;
225
+ }
226
+
227
+ // Check message count quota.
228
+ if (maxMessages > 0) {
229
+ const currentMessages = await adminStore!.getTenantMessageCount(target);
230
+ if (currentMessages >= maxMessages) {
231
+ return {
232
+ jsonRpcResponse: createJsonRpcErrorResponse(
233
+ requestId,
234
+ JsonRpcErrorCodes.InvalidRequest,
235
+ `${DwnServerErrorCode.TenantMessageQuotaExceeded}: tenant has reached the message limit of ${maxMessages}`,
236
+ ),
237
+ };
238
+ }
239
+ }
240
+
241
+ // Check storage size quota.
242
+ if (maxStorageBytes > 0) {
243
+ const dataSize = (message.descriptor as { dataSize?: number }).dataSize ?? 0;
244
+ const currentStorage = await adminStore!.getTenantStorageSize(target);
245
+ if (currentStorage + dataSize > maxStorageBytes) {
246
+ return {
247
+ jsonRpcResponse: createJsonRpcErrorResponse(
248
+ requestId,
249
+ JsonRpcErrorCodes.InvalidRequest,
250
+ `${DwnServerErrorCode.TenantStorageQuotaExceeded}: tenant would exceed storage limit of ${maxStorageBytes} bytes`,
251
+ ),
252
+ };
253
+ }
254
+ }
255
+
256
+ return undefined;
257
+ }