@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
@@ -1,474 +1,749 @@
1
- import { DateSort, RecordsRead, RecordsQuery, ProtocolsQuery } from '@enbox/dwn-sdk-js';
2
- import cors from 'cors';
3
- import express from 'express';
4
- import { readFileSync } from 'fs';
5
- import http from 'http';
6
1
  import log from 'loglevel';
2
+ import { Convert } from '@enbox/common';
7
3
  import { register } from 'prom-client';
8
- import responseTime from 'response-time';
9
4
  import { v4 as uuidv4 } from 'uuid';
5
+ import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from '@enbox/dwn-clients';
6
+ import { DataStream, DateSort, ProtocolsQuery, RecordsQuery, RecordsRead } from '@enbox/dwn-sdk-js';
7
+ import { existsSync, readFileSync } from 'fs';
8
+ import { join, resolve } from 'path';
10
9
  import { config } from './config.js';
11
10
  import { jsonRpcRouter } from './json-rpc-api.js';
11
+ import { validateAdminAuth } from './admin/admin-auth.js';
12
12
  import { Web5ConnectServer } from './web5-connect/web5-connect-server.js';
13
- import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js';
14
13
  import { requestCounter, responseHistogram } from './metrics.js';
15
- import { Convert } from '@enbox/common';
14
+ /** Property names that must never be used as keys when building objects from user input. */
15
+ const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
16
+ // Resolve admin UI dist path at module load time. Gracefully handle the case
17
+ // where the admin UI package is not installed.
18
+ let resolvedAdminUiPath;
19
+ try {
20
+ const adminUiModule = require('@enbox/dwn-server-admin-ui');
21
+ resolvedAdminUiPath = adminUiModule.adminUiDistPath;
22
+ }
23
+ catch {
24
+ // Admin UI package not installed — static serving will be disabled.
25
+ }
16
26
  export class HttpApi {
17
27
  #config;
18
28
  #packageInfo;
19
- #api;
20
29
  #server;
30
+ #adminApi;
31
+ #activityLog;
32
+ #adminStore;
33
+ #registrationStore;
34
+ #ipRateLimiter;
35
+ #tenantRateLimiter;
36
+ #messageProcessedHooks;
37
+ #openAuthHandler;
38
+ #adminUiPath;
21
39
  web5ConnectServer;
22
40
  registrationManager;
23
41
  dwn;
42
+ /** Called by WsApi/ConnectionManager when a new WS connection is established. */
43
+ onWebSocketConnection;
24
44
  constructor() { }
25
- static async create(config, dwn, registrationManager) {
45
+ static async create(config, dwn, registrationManager, adminApi, activityLog, options) {
26
46
  const httpApi = new HttpApi();
27
- log.info(config);
47
+ log.info(HttpApi.#redactConfig(config));
28
48
  httpApi.#packageInfo = {
29
49
  server: config.serverName,
30
50
  };
31
51
  try {
32
- // We populate the `version` and `sdkVersion` properties from the `package.json` file.
33
52
  const packageJson = JSON.parse(readFileSync(config.packageJsonPath).toString());
34
53
  httpApi.#packageInfo.version = packageJson.version;
35
- httpApi.#packageInfo.sdkVersion = packageJson.dependencies ? packageJson.dependencies['@enbox/dwn-sdk-js'] : undefined;
54
+ httpApi.#packageInfo.sdkVersion = packageJson.dependencies
55
+ ? packageJson.dependencies['@enbox/dwn-sdk-js']
56
+ : undefined;
36
57
  }
37
58
  catch (error) {
38
59
  log.info('could not read `package.json` for version info', error);
39
60
  }
40
61
  httpApi.#config = config;
41
- httpApi.#api = express();
42
- httpApi.#server = http.createServer(httpApi.#api);
43
62
  httpApi.dwn = dwn;
63
+ httpApi.#adminApi = adminApi;
64
+ httpApi.#activityLog = activityLog;
65
+ httpApi.#adminStore = options?.adminStore;
66
+ httpApi.#registrationStore = options?.registrationStore;
67
+ httpApi.#ipRateLimiter = options?.ipRateLimiter;
68
+ httpApi.#tenantRateLimiter = options?.tenantRateLimiter;
69
+ httpApi.#messageProcessedHooks = options?.messageProcessedHooks ?? [];
70
+ httpApi.#openAuthHandler = options?.openAuthHandler;
71
+ httpApi.#adminUiPath = resolvedAdminUiPath;
44
72
  if (registrationManager !== undefined) {
45
73
  httpApi.registrationManager = registrationManager;
46
74
  }
47
- // create the Web5 Connect Server
48
75
  httpApi.web5ConnectServer = await Web5ConnectServer.create({
49
76
  baseUrl: config.baseUrl,
50
77
  sqlTtlCacheUrl: config.ttlCacheUrl,
51
78
  });
52
- httpApi.#setupMiddleware();
53
- httpApi.#setupRoutes();
54
79
  return httpApi;
55
80
  }
56
81
  get server() {
57
82
  return this.#server;
58
83
  }
59
- get api() {
60
- return this.#api;
84
+ get ipRateLimiter() {
85
+ return this.#ipRateLimiter;
61
86
  }
62
- #setupMiddleware() {
63
- this.#api.use(cors({ exposedHeaders: 'dwn-response' }));
64
- this.#api.use(express.json());
65
- // We enable the formData middleware to handle multipart/form-data requests.
66
- // This is necessary for the endpoints used by the Web5 Connect Server/OIDC flow.
67
- this.#api.use(express.urlencoded({ extended: true }));
68
- this.#api.use(responseTime((req, res, time) => {
69
- const url = req.url === '/' ? '/jsonrpc' : req.url;
70
- const route = (req.method + url)
71
- .toLowerCase()
72
- .replace(/[:.]/g, '')
73
- .replace(/\//g, '_');
74
- const statusCode = res.statusCode.toString();
75
- responseHistogram.labels(route, statusCode).observe(time);
76
- log.info(req.method, decodeURI(req.url), res.statusCode);
77
- }));
87
+ get tenantRateLimiter() {
88
+ return this.#tenantRateLimiter;
78
89
  }
90
+ get messageProcessedHooks() {
91
+ return this.#messageProcessedHooks;
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // HTTP request handler
95
+ // ---------------------------------------------------------------------------
96
+ async start(port) {
97
+ const self = this; // capture for closures
98
+ this.#server = Bun.serve({
99
+ port,
100
+ async fetch(req, server) {
101
+ const startTime = performance.now();
102
+ const url = new URL(req.url);
103
+ const path = url.pathname;
104
+ const method = req.method;
105
+ // --- WebSocket upgrade ---
106
+ if (method === 'GET' && req.headers.get('upgrade') === 'websocket') {
107
+ const upgraded = server.upgrade(req, { data: { connection: null } });
108
+ if (upgraded) {
109
+ return undefined;
110
+ }
111
+ return new Response('WebSocket upgrade failed', { status: 400 });
112
+ }
113
+ // --- Per-IP rate limiting ---
114
+ if (self.#ipRateLimiter) {
115
+ const ip = server.requestIP(req)?.address ?? 'unknown';
116
+ const result = self.#ipRateLimiter.consume(ip);
117
+ if (result.allowed === false) {
118
+ const retryAfterSec = Math.ceil(result.retryAfterMs / 1000);
119
+ return new Response(JSON.stringify({ error: 'Rate limit exceeded' }), {
120
+ status: 429,
121
+ headers: {
122
+ 'content-type': 'application/json',
123
+ 'retry-after': String(retryAfterSec),
124
+ },
125
+ });
126
+ }
127
+ }
128
+ // --- Route matching ---
129
+ let response;
130
+ try {
131
+ response = await self.#route(req, url, path, method);
132
+ }
133
+ catch (error) {
134
+ log.error(`Unhandled error on ${method} ${path}:`, error);
135
+ response = new Response('Internal Server Error', { status: 500 });
136
+ }
137
+ // --- CORS headers ---
138
+ // Admin API and metrics endpoints do not receive wildcard CORS headers
139
+ // to limit cross-origin access when the admin token is configured.
140
+ const isAdminRoute = path.startsWith('/admin') || path === '/metrics';
141
+ if (!isAdminRoute) {
142
+ response.headers.set('access-control-allow-origin', '*');
143
+ response.headers.set('access-control-allow-methods', 'GET, POST, OPTIONS');
144
+ response.headers.set('access-control-allow-headers', '*');
145
+ response.headers.set('access-control-expose-headers', 'dwn-response');
146
+ }
147
+ // --- Response-time metrics ---
148
+ const elapsed = performance.now() - startTime;
149
+ const routeLabel = (method + (path === '/' ? '/jsonrpc' : path))
150
+ .toLowerCase()
151
+ .replace(/[:.]/g, '')
152
+ .replace(/\//g, '_');
153
+ responseHistogram.labels(routeLabel, String(response.status)).observe(elapsed);
154
+ log.info(method, decodeURI(path), response.status);
155
+ return response;
156
+ },
157
+ websocket: {
158
+ maxPayloadLength: self.#config.maxRecordDataSize,
159
+ open(ws) {
160
+ if (self.onWebSocketConnection) {
161
+ self.onWebSocketConnection(ws);
162
+ }
163
+ },
164
+ message(ws, msg) {
165
+ const connection = ws.data?.connection;
166
+ if (connection) {
167
+ connection.message(typeof msg === 'string' ? Buffer.from(msg) : msg);
168
+ }
169
+ },
170
+ close(ws) {
171
+ const connection = ws.data?.connection;
172
+ if (connection) {
173
+ connection.close();
174
+ }
175
+ },
176
+ pong(ws) {
177
+ const connection = ws.data?.connection;
178
+ if (connection) {
179
+ connection.pong();
180
+ }
181
+ },
182
+ },
183
+ });
184
+ }
185
+ async close() {
186
+ if (this.#openAuthHandler) {
187
+ this.#openAuthHandler.destroy();
188
+ }
189
+ if (this.#server) {
190
+ this.#server.stop(true); // close all connections immediately
191
+ }
192
+ }
193
+ // ---------------------------------------------------------------------------
194
+ // Admin UI static file serving
195
+ // ---------------------------------------------------------------------------
79
196
  /**
80
- * Configures the HTTP server's request handlers.
197
+ * Serves static files from the admin UI dist directory. Returns `null` when
198
+ * the admin UI package is not installed or the requested file does not exist.
199
+ * All non-file paths under `/admin` fall back to `index.html` (SPA routing).
81
200
  */
82
- #setupRoutes() {
83
- const leadTailSlashRegex = /^\/|\/$/;
84
- function readReplyHandler(res, reply) {
85
- if (reply.status.code === 200) {
86
- if (reply?.entry?.data) {
87
- const stream = reply.entry.data;
88
- res.setHeader('content-type', reply.entry.recordsWrite.descriptor.dataFormat);
89
- res.setHeader('dwn-response', JSON.stringify(reply));
90
- return stream.pipe(res);
91
- }
92
- else {
93
- return res.sendStatus(400);
201
+ #serveAdminUi(path) {
202
+ if (!this.#adminUiPath) {
203
+ return null;
204
+ }
205
+ // Strip the `/admin` prefix to get the file path within the dist directory.
206
+ const relativePath = path.replace(/^\/admin\/?/, '');
207
+ // Map to a file on disk. Empty path or paths without an extension get
208
+ // the SPA index.html (client-side routing).
209
+ let filePath;
210
+ if (relativePath === '' || !relativePath.includes('.')) {
211
+ filePath = join(this.#adminUiPath, 'index.html');
212
+ }
213
+ else {
214
+ filePath = join(this.#adminUiPath, relativePath);
215
+ }
216
+ // Prevent path traversal: resolved path must stay within the admin UI directory.
217
+ const resolvedBase = resolve(this.#adminUiPath);
218
+ if (!resolve(filePath).startsWith(resolvedBase)) {
219
+ return null;
220
+ }
221
+ if (!existsSync(filePath)) {
222
+ return null;
223
+ }
224
+ const file = Bun.file(filePath);
225
+ return new Response(file);
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // Router
229
+ // ---------------------------------------------------------------------------
230
+ async #route(req, url, path, method) {
231
+ // --- CORS preflight ---
232
+ if (method === 'OPTIONS') {
233
+ return new Response(null, { status: 204 });
234
+ }
235
+ // --- Static routes ---
236
+ if (method === 'GET' && path === '/health') {
237
+ return Response.json({ ok: true });
238
+ }
239
+ if (method === 'GET' && path === '/metrics') {
240
+ // Metrics require admin authentication when an admin token is configured.
241
+ if (this.#config.adminToken) {
242
+ const authError = validateAdminAuth(req, this.#config);
243
+ if (authError) {
244
+ return authError;
94
245
  }
95
246
  }
96
- else if (reply.status.code === 401) {
97
- return res.sendStatus(404);
247
+ try {
248
+ const metricsBody = await register.metrics();
249
+ return new Response(metricsBody, {
250
+ headers: { 'content-type': register.contentType },
251
+ });
98
252
  }
99
- else {
100
- return res.status(reply.status.code).send(reply);
253
+ catch (e) {
254
+ return new Response(String(e), { status: 500 });
101
255
  }
102
256
  }
103
- this.#api.get('/health', (_req, res) => {
104
- // return 200 ok
105
- return res.json({ ok: true });
106
- });
107
- this.#api.get('/metrics', async (req, res) => {
108
- try {
109
- res.set('Content-Type', register.contentType);
110
- res.end(await register.metrics());
257
+ if (method === 'GET' && path === '/') {
258
+ return new Response('please use am enbox client, for example: https://github.com/enboxorg/enbox ', { headers: { 'content-type': 'text/plain' } });
259
+ }
260
+ if (method === 'GET' && path === '/info') {
261
+ return this.#handleInfo();
262
+ }
263
+ // --- JSON-RPC POST ---
264
+ if (method === 'POST' && path === '/') {
265
+ return this.#handleJsonRpcPost(req);
266
+ }
267
+ // --- Admin API routes ---
268
+ if (path.startsWith('/admin/api/') && this.#adminApi) {
269
+ return this.#adminApi.route(req, url, path, method);
270
+ }
271
+ // --- Admin UI static files (only when admin API is enabled) ---
272
+ if (method === 'GET' && path.startsWith('/admin') && this.#adminApi) {
273
+ const uiResponse = this.#serveAdminUi(path);
274
+ if (uiResponse) {
275
+ return uiResponse;
111
276
  }
112
- catch (e) {
113
- res.status(500).end(e);
277
+ }
278
+ // --- Provider auth (open-auth) routes ---
279
+ if (this.#openAuthHandler && path.startsWith('/provider-auth/')) {
280
+ if (method === 'GET' && path === '/provider-auth/authorize') {
281
+ return this.#openAuthHandler.handleAuthorize(url);
114
282
  }
115
- });
116
- // Returns the data for the most recently published record under a given protocol path collection, if one is present
117
- this.#api.get('/:did/read/protocols/:protocol/*', async (req, res) => {
118
- if (!req.params[0]) {
119
- return res.status(400).send('protocol path is required');
283
+ if (method === 'POST' && path === '/provider-auth/token') {
284
+ return this.#openAuthHandler.handleToken(req);
120
285
  }
121
- // wrap request in a try-catch block to handle any unexpected errors
122
- try {
123
- const queryOptions = { filter: {} };
124
- for (const param in req.query) {
125
- const keys = param.split('.');
126
- const lastKey = keys.pop();
127
- const lastLevelObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, queryOptions);
128
- lastLevelObject[lastKey] = req.query[param];
129
- }
130
- // the protocol path segment is base64url encoded, as the actual protocol is a URL
131
- // we decode it here in order to filter for the correct protocol
132
- const protocol = Convert.base64Url(req.params.protocol).toString();
133
- queryOptions.filter.protocol = protocol;
134
- queryOptions.filter.protocolPath = req.params[0].replace(leadTailSlashRegex, '');
135
- const query = await RecordsQuery.create({
136
- filter: queryOptions.filter,
137
- pagination: { limit: 1 },
138
- dateSort: DateSort.PublishedDescending
139
- });
140
- const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
141
- if (status.code === 200) {
142
- if (entries[0]) {
143
- const record = await RecordsRead.create({
144
- filter: { recordId: entries[0].recordId },
145
- });
146
- const reply = await this.dwn.processMessage(req.params.did, record.toJSON());
147
- return readReplyHandler(res, reply);
148
- }
149
- else {
150
- return res.sendStatus(404);
151
- }
152
- }
153
- else if (status.code === 401) {
154
- return res.sendStatus(404);
155
- }
156
- else {
157
- return res.sendStatus(status.code);
158
- }
286
+ if (method === 'POST' && path === '/provider-auth/refresh') {
287
+ return this.#openAuthHandler.handleRefresh(req);
159
288
  }
160
- catch (error) {
161
- log.error(`Error processing request: ${decodeURI(req.url)}`, error);
162
- return res.sendStatus(400);
289
+ }
290
+ // --- Registration routes ---
291
+ const registrationResponse = await this.#matchRegistrationRoutes(req, path, method);
292
+ if (registrationResponse) {
293
+ return registrationResponse;
294
+ }
295
+ // --- Web5 Connect routes ---
296
+ const connectResponse = await this.#matchWeb5ConnectRoutes(req, path, method);
297
+ if (connectResponse) {
298
+ return connectResponse;
299
+ }
300
+ // --- DID routes (parameterized) ---
301
+ return this.#matchDidRoutes(req, url, path);
302
+ }
303
+ // ---------------------------------------------------------------------------
304
+ // DID convenience routes
305
+ // ---------------------------------------------------------------------------
306
+ async #matchDidRoutes(req, url, path) {
307
+ const leadTailSlashRegex = /^\/|\/$/g;
308
+ // /:did/read/protocols/:protocol/* (also matches trailing slash with empty path)
309
+ {
310
+ const match = path.match(/^\/([^/]+)\/read\/protocols\/([^/]+)\/(.*)$/);
311
+ if (match && req.method === 'GET') {
312
+ const [, did, protocolParam, protocolPathRaw] = match;
313
+ return this.#handleReadProtocolRecord(did, protocolParam, protocolPathRaw, url, leadTailSlashRegex);
163
314
  }
315
+ }
316
+ // /:did/read/protocols/:protocol
317
+ {
318
+ const match = path.match(/^\/([^/]+)\/read\/protocols\/([^/]+)$/);
319
+ if (match && req.method === 'GET') {
320
+ const [, did, protocolParam] = match;
321
+ return this.#handleReadProtocol(did, protocolParam);
322
+ }
323
+ }
324
+ // /:did/read/records/:id OR /:did/records/:id
325
+ {
326
+ const match = path.match(/^\/([^/]+)\/(?:read\/)?records\/([^/]+)$/);
327
+ if (match && req.method === 'GET') {
328
+ const [, did, recordId] = match;
329
+ return this.#handleReadRecord(did, recordId);
330
+ }
331
+ }
332
+ // /:did/query/protocols
333
+ {
334
+ const match = path.match(/^\/([^/]+)\/query\/protocols$/);
335
+ if (match && req.method === 'GET') {
336
+ const [, did] = match;
337
+ return this.#handleQueryProtocols(did);
338
+ }
339
+ }
340
+ // /:did/query
341
+ {
342
+ const match = path.match(/^\/([^/]+)\/query$/);
343
+ if (match && req.method === 'GET') {
344
+ const [, did] = match;
345
+ return this.#handleQueryRecords(did, url);
346
+ }
347
+ }
348
+ return new Response('Not Found', { status: 404 });
349
+ }
350
+ // ---------------------------------------------------------------------------
351
+ // Security helpers
352
+ // ---------------------------------------------------------------------------
353
+ /** Returns `true` if the given key is a prototype-pollution-dangerous property name. */
354
+ static #isDangerousKey(key) {
355
+ return key !== undefined && DANGEROUS_KEYS.has(key);
356
+ }
357
+ /** Returns `true` if any element in `keys` is a dangerous property name. */
358
+ static #hasDangerousKey(keys) {
359
+ return keys.some(k => DANGEROUS_KEYS.has(k));
360
+ }
361
+ /** Returns a shallow copy of the config with sensitive values redacted for logging. */
362
+ static #redactConfig(cfg) {
363
+ const redacted = { ...cfg };
364
+ const sensitiveKeys = ['adminToken', 'providerAuthJwtSecret'];
365
+ for (const key of sensitiveKeys) {
366
+ if (redacted[key]) {
367
+ redacted[key] = '[REDACTED]';
368
+ }
369
+ }
370
+ // Redact passwords in connection-string-like values.
371
+ for (const [key, value] of Object.entries(redacted)) {
372
+ if (typeof value === 'string' && /^(?:postgres|mysql|sqlite):\/\//.test(value) && value.includes('@')) {
373
+ redacted[key] = value.replace(/:\/\/([^:]+):([^@]+)@/, '://$1:****@');
374
+ }
375
+ }
376
+ return redacted;
377
+ }
378
+ // ---------------------------------------------------------------------------
379
+ // Handlers
380
+ // ---------------------------------------------------------------------------
381
+ #handleInfo() {
382
+ const registrationRequirements = [];
383
+ if (config.registrationProofOfWorkEnabled) {
384
+ registrationRequirements.push('proof-of-work-sha256-v0');
385
+ }
386
+ if (config.termsOfServiceFilePath !== undefined) {
387
+ registrationRequirements.push('terms-of-service');
388
+ }
389
+ if (config.providerAuthEnabled && !registrationRequirements.includes('provider-auth-v0')) {
390
+ registrationRequirements.push('provider-auth-v0');
391
+ }
392
+ const serverInfo = {
393
+ maxFileSize: config.maxRecordDataSize,
394
+ maxInFlight: config.maxInFlight,
395
+ registrationRequirements: registrationRequirements,
396
+ server: this.#packageInfo.server,
397
+ sdkVersion: this.#packageInfo.sdkVersion,
398
+ url: config.baseUrl,
399
+ version: this.#packageInfo.version,
400
+ webSocketSupport: config.webSocketSupport,
401
+ };
402
+ if (config.providerAuthEnabled) {
403
+ serverInfo.providerAuth = {
404
+ authorizeUrl: config.providerAuthAuthorizeUrl,
405
+ tokenUrl: config.providerAuthTokenUrl,
406
+ refreshUrl: config.providerAuthRefreshUrl,
407
+ managementUrl: config.providerAuthManagementUrl,
408
+ };
409
+ }
410
+ return Response.json(serverInfo);
411
+ }
412
+ async #handleJsonRpcPost(req) {
413
+ const dwnRpcRequestString = req.headers.get('dwn-request');
414
+ if (!dwnRpcRequestString) {
415
+ const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, 'request payload required.');
416
+ return Response.json(reply, { status: 400 });
417
+ }
418
+ let dwnRpcRequest;
419
+ try {
420
+ dwnRpcRequest = JSON.parse(dwnRpcRequestString);
421
+ }
422
+ catch (e) {
423
+ const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, e.message);
424
+ return Response.json(reply, { status: 400 });
425
+ }
426
+ // Materialise the request body before passing to DWN.
427
+ // Bun's Bun.serve() returns a ReadableStream for req.body that is
428
+ // incompatible with the ReadableStream consumer code in dwn-sdk-js,
429
+ // causing DataStream.toBytes() to crash with "undefined is not a
430
+ // function" at reader.releaseLock(). Buffering via arrayBuffer()
431
+ // converts it into a well-behaved stream that dwn-sdk-js can consume.
432
+ // TODO: https://github.com/enboxorg/enbox/issues/90 — remove once Bun ships fix
433
+ const contentLength = req.headers.get('content-length');
434
+ const transferEncoding = req.headers.get('transfer-encoding');
435
+ let requestDataStream;
436
+ if (parseInt(contentLength ?? '0') > 0 || transferEncoding !== null) {
437
+ const bodyBytes = new Uint8Array(await req.arrayBuffer());
438
+ requestDataStream = DataStream.fromBytes(bodyBytes);
439
+ }
440
+ const requestContext = {
441
+ dwn: this.dwn,
442
+ transport: 'http',
443
+ dataStream: requestDataStream,
444
+ activityLog: this.#activityLog,
445
+ adminStore: this.#adminStore,
446
+ registrationStore: this.#registrationStore,
447
+ config: this.#config,
448
+ tenantRateLimiter: this.#tenantRateLimiter,
449
+ messageProcessedHooks: this.#messageProcessedHooks,
450
+ };
451
+ const { jsonRpcResponse, dataStream: responseDataStream } = await jsonRpcRouter.handle(dwnRpcRequest, requestContext);
452
+ if (jsonRpcResponse.error) {
453
+ requestCounter.inc({ method: dwnRpcRequest.method, error: 1 });
454
+ // Return HTTP 429 with Retry-After header for rate-limit rejections.
455
+ if (jsonRpcResponse.error.code === JsonRpcErrorCodes.TooManyRequests) {
456
+ const retryAfterSec = jsonRpcResponse.error.data?.retryAfterSec ?? 1;
457
+ return Response.json(jsonRpcResponse, {
458
+ status: 429,
459
+ headers: { 'retry-after': String(retryAfterSec) },
460
+ });
461
+ }
462
+ return Response.json(jsonRpcResponse, { status: 500 });
463
+ }
464
+ requestCounter.inc({
465
+ method: dwnRpcRequest.method,
466
+ status: jsonRpcResponse?.result?.reply?.status?.code || 0,
164
467
  });
165
- this.#api.get('/:did/read/protocols/:protocol', async (req, res) => {
166
- // wrap request in a try-catch block to handle any unexpected errors
167
- try {
168
- // the protocol segment is base64url encoded, as the actual protocol is a URL
169
- // we decode it here in order to filter for the correct protocol
170
- const protocol = Convert.base64Url(req.params.protocol).toString();
171
- const query = await ProtocolsQuery.create({
172
- filter: { protocol }
468
+ if (responseDataStream) {
469
+ return new Response(responseDataStream, {
470
+ headers: {
471
+ 'content-type': 'application/octet-stream',
472
+ 'dwn-response': JSON.stringify(jsonRpcResponse),
473
+ },
474
+ });
475
+ }
476
+ else {
477
+ return Response.json(jsonRpcResponse);
478
+ }
479
+ }
480
+ #readReplyToResponse(reply) {
481
+ if (reply.status.code === 200) {
482
+ if (reply?.entry?.data) {
483
+ return new Response(reply.entry.data, {
484
+ headers: {
485
+ 'content-type': reply.entry.recordsWrite.descriptor.dataFormat,
486
+ 'dwn-response': JSON.stringify(reply),
487
+ },
173
488
  });
174
- const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
175
- if (status.code === 200) {
176
- if (entries.length) {
177
- res.status(status.code);
178
- res.json(entries[0]);
179
- }
180
- else {
181
- return res.sendStatus(404);
182
- }
183
- }
184
- else if (status.code === 401) {
185
- return res.sendStatus(404);
186
- }
187
- else {
188
- return res.sendStatus(status.code);
189
- }
190
489
  }
191
- catch (error) {
192
- log.error(`Error processing request: ${decodeURI(req.url)}`, error);
193
- return res.sendStatus(400);
490
+ else {
491
+ return new Response(null, { status: 400 });
194
492
  }
493
+ }
494
+ else if (reply.status.code === 401) {
495
+ return new Response(null, { status: 404 });
496
+ }
497
+ else {
498
+ return Response.json(reply, { status: reply.status.code });
499
+ }
500
+ }
501
+ async #handleReadRecord(did, recordId) {
502
+ const record = await RecordsRead.create({
503
+ filter: { recordId },
195
504
  });
196
- const recordsReadHandler = async (req, res) => {
197
- const record = await RecordsRead.create({
198
- filter: { recordId: req.params.id },
505
+ const reply = await this.dwn.processMessage(did, record.message);
506
+ return this.#readReplyToResponse(reply);
507
+ }
508
+ async #handleReadProtocolRecord(did, protocolParam, protocolPathRaw, url, leadTailSlashRegex) {
509
+ if (!protocolPathRaw || protocolPathRaw.replace(leadTailSlashRegex, '') === '') {
510
+ return new Response('protocol path is required', { status: 400 });
511
+ }
512
+ try {
513
+ const queryOptions = { filter: {} };
514
+ for (const [param, value] of url.searchParams) {
515
+ const keys = param.split('.');
516
+ const lastKey = keys.pop();
517
+ if (HttpApi.#hasDangerousKey(keys) || HttpApi.#isDangerousKey(lastKey)) {
518
+ continue;
519
+ }
520
+ const nestObj = (obj, key) => obj[key] = obj[key] || {};
521
+ const lastLevelObject = keys.reduce(nestObj, queryOptions);
522
+ lastLevelObject[lastKey] = value;
523
+ }
524
+ const protocol = Convert.base64Url(protocolParam).toString();
525
+ queryOptions.filter.protocol = protocol;
526
+ queryOptions.filter.protocolPath = protocolPathRaw.replace(leadTailSlashRegex, '');
527
+ const query = await RecordsQuery.create({
528
+ filter: queryOptions.filter,
529
+ pagination: { limit: 1 },
530
+ dateSort: DateSort.PublishedDescending,
199
531
  });
200
- const reply = await this.dwn.processMessage(req.params.did, record.message);
201
- return readReplyHandler(res, reply);
202
- };
203
- this.#api.get('/:did/read/records/:id', recordsReadHandler);
204
- this.#api.get('/:did/records/:id', recordsReadHandler);
205
- this.#api.get('/:did/query/protocols', async (req, res) => {
206
- const query = await ProtocolsQuery.create({});
207
- const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
532
+ const { entries, status } = await this.dwn.processMessage(did, query.message);
208
533
  if (status.code === 200) {
209
- res.status(status.code);
210
- res.json(entries);
534
+ if (entries[0]) {
535
+ const record = await RecordsRead.create({
536
+ filter: { recordId: entries[0].recordId },
537
+ });
538
+ const reply = await this.dwn.processMessage(did, record.toJSON());
539
+ return this.#readReplyToResponse(reply);
540
+ }
541
+ else {
542
+ return new Response(null, { status: 404 });
543
+ }
211
544
  }
212
545
  else if (status.code === 401) {
213
- return res.sendStatus(404);
546
+ return new Response(null, { status: 404 });
214
547
  }
215
548
  else {
216
- return res.sendStatus(status.code);
549
+ return new Response(null, { status: status.code });
217
550
  }
218
- });
219
- this.#api.get('/:did/query', async (req, res) => {
220
- try {
221
- // builds a nested object from flat keys with dot notation which may share the same parent path
222
- // e.g. "did:dht:123/query?filter.protocol=foo&filter.protocolPath=bar" becomes
223
- // {
224
- // filter: {
225
- // protocol: 'foo',
226
- // protocolPath: 'bar'
227
- // }
228
- // }
229
- const recordsQueryOptions = {};
230
- for (const param in req.query) {
231
- const keys = param.split('.');
232
- const lastKey = keys.pop();
233
- const lastLevelObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, recordsQueryOptions);
234
- lastLevelObject[lastKey] = req.query[param];
551
+ }
552
+ catch (error) {
553
+ log.error(`Error processing request: ${decodeURI(url.pathname)}`, error);
554
+ return new Response('Bad Request', { status: 400 });
555
+ }
556
+ }
557
+ async #handleReadProtocol(did, protocolParam) {
558
+ try {
559
+ const protocol = Convert.base64Url(protocolParam).toString();
560
+ const query = await ProtocolsQuery.create({
561
+ filter: { protocol },
562
+ });
563
+ const { entries, status } = await this.dwn.processMessage(did, query.message);
564
+ if (status.code === 200) {
565
+ if (entries.length) {
566
+ return Response.json(entries[0], { status: status.code });
567
+ }
568
+ else {
569
+ return new Response(null, { status: 404 });
235
570
  }
236
- const recordsQuery = await RecordsQuery.create({
237
- filter: recordsQueryOptions.filter,
238
- pagination: recordsQueryOptions.pagination,
239
- dateSort: recordsQueryOptions.dateSort,
240
- });
241
- // should always return a 200 status code with a JSON response
242
- const reply = await this.dwn.processMessage(req.params.did, recordsQuery.message);
243
- res.setHeader('content-type', 'application/json');
244
- return res.json(reply);
245
571
  }
246
- catch (error) {
247
- // error should only occur when we are unable to create the RecordsQuery message internally, making it a client error
248
- return res.status(400).send(error);
572
+ else if (status.code === 401) {
573
+ return new Response(null, { status: 404 });
249
574
  }
250
- });
251
- this.#api.get('/', (_req, res) => {
252
- // return a plain text string
253
- res.setHeader('content-type', 'text/plain');
254
- return res.send('please use a web5 client, for example: https://github.com/TBD54566975/web5-js ');
255
- });
256
- this.#api.post('/', async (req, res) => {
257
- const dwnRpcRequestString = req.headers['dwn-request'];
258
- if (!dwnRpcRequestString) {
259
- const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, 'request payload required.');
260
- return res.status(400).json(reply);
575
+ else {
576
+ return new Response(null, { status: status.code });
261
577
  }
262
- let dwnRpcRequest;
263
- try {
264
- dwnRpcRequest = JSON.parse(dwnRpcRequestString);
578
+ }
579
+ catch (error) {
580
+ log.error(`Error processing request`, error);
581
+ return new Response('Bad Request', { status: 400 });
582
+ }
583
+ }
584
+ async #handleQueryProtocols(did) {
585
+ const query = await ProtocolsQuery.create({});
586
+ const { entries, status } = await this.dwn.processMessage(did, query.message);
587
+ if (status.code === 200) {
588
+ return Response.json(entries, { status: status.code });
589
+ }
590
+ else if (status.code === 401) {
591
+ return new Response(null, { status: 404 });
592
+ }
593
+ else {
594
+ return new Response(null, { status: status.code });
595
+ }
596
+ }
597
+ async #handleQueryRecords(did, url) {
598
+ try {
599
+ const recordsQueryOptions = {};
600
+ for (const [param, value] of url.searchParams) {
601
+ const keys = param.split('.');
602
+ const lastKey = keys.pop();
603
+ if (HttpApi.#hasDangerousKey(keys) || HttpApi.#isDangerousKey(lastKey)) {
604
+ continue;
605
+ }
606
+ const nestObj = (obj, key) => obj[key] = obj[key] || {};
607
+ const lastLevelObject = keys.reduce(nestObj, recordsQueryOptions);
608
+ lastLevelObject[lastKey] = value;
265
609
  }
266
- catch (e) {
267
- const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, e.message);
268
- return res.status(400).json(reply);
269
- }
270
- // Check whether data was provided in the request body
271
- const contentLength = req.headers['content-length'];
272
- const transferEncoding = req.headers['transfer-encoding'];
273
- const requestDataStream = parseInt(contentLength) > 0 || transferEncoding !== undefined ? req : undefined;
274
- const requestContext = {
275
- dwn: this.dwn,
276
- transport: 'http',
277
- dataStream: requestDataStream,
278
- };
279
- const { jsonRpcResponse, dataStream: responseDataStream } = await jsonRpcRouter.handle(dwnRpcRequest, requestContext);
280
- // If the handler catches a thrown exception and returns a JSON RPC InternalError, return the equivalent
281
- // HTTP 500 Internal Server Error with the response.
282
- if (jsonRpcResponse.error) {
283
- requestCounter.inc({ method: dwnRpcRequest.method, error: 1 });
284
- return res.status(500).json(jsonRpcResponse);
285
- }
286
- requestCounter.inc({
287
- method: dwnRpcRequest.method,
288
- status: jsonRpcResponse?.result?.reply?.status?.code || 0,
610
+ const recordsQuery = await RecordsQuery.create({
611
+ filter: recordsQueryOptions.filter,
612
+ pagination: recordsQueryOptions.pagination,
613
+ dateSort: recordsQueryOptions.dateSort,
289
614
  });
290
- if (responseDataStream) {
291
- res.setHeader('content-type', 'application/octet-stream');
292
- res.setHeader('dwn-response', JSON.stringify(jsonRpcResponse));
293
- return responseDataStream.pipe(res);
294
- }
295
- else {
296
- return res.json(jsonRpcResponse);
297
- }
298
- });
299
- this.#setupRegistrationRoutes();
300
- this.#api.get('/info', (req, res) => {
301
- res.setHeader('content-type', 'application/json');
302
- const registrationRequirements = [];
303
- if (config.registrationProofOfWorkEnabled) {
304
- registrationRequirements.push('proof-of-work-sha256-v0');
305
- }
306
- if (config.termsOfServiceFilePath !== undefined) {
307
- registrationRequirements.push('terms-of-service');
308
- }
309
- res.json({
310
- url: config.baseUrl,
311
- server: this.#packageInfo.server,
312
- maxFileSize: config.maxRecordDataSize,
313
- registrationRequirements: registrationRequirements,
314
- version: this.#packageInfo.version,
315
- sdkVersion: this.#packageInfo.sdkVersion,
316
- webSocketSupport: config.webSocketSupport,
615
+ const reply = await this.dwn.processMessage(did, recordsQuery.message);
616
+ return Response.json(reply, {
617
+ headers: { 'content-type': 'application/json' },
317
618
  });
318
- });
319
- this.#setupWeb5ConnectServerRoutes();
619
+ }
620
+ catch (error) {
621
+ log.error('Error processing query records request', error);
622
+ return Response.json({ error: 'Bad Request' }, { status: 400 });
623
+ }
320
624
  }
321
- #setupRegistrationRoutes() {
322
- if (this.#config.registrationProofOfWorkEnabled) {
323
- this.#api.get('/registration/proof-of-work', async (_req, res) => {
324
- const proofOfWorkChallenge = this.registrationManager.getProofOfWorkChallenge();
325
- res.json(proofOfWorkChallenge);
326
- });
625
+ // ---------------------------------------------------------------------------
626
+ // Registration routes
627
+ // ---------------------------------------------------------------------------
628
+ async #matchRegistrationRoutes(req, path, method) {
629
+ if (method === 'GET' && path === '/registration/proof-of-work'
630
+ && this.#config.registrationProofOfWorkEnabled) {
631
+ const proofOfWorkChallenge = this.registrationManager.getProofOfWorkChallenge();
632
+ return Response.json(proofOfWorkChallenge);
327
633
  }
328
- if (this.#config.termsOfServiceFilePath !== undefined) {
329
- this.#api.get('/registration/terms-of-service', (_req, res) => res.send(this.registrationManager.getTermsOfService()));
634
+ if (method === 'GET' && path === '/registration/terms-of-service'
635
+ && this.#config.termsOfServiceFilePath !== undefined) {
636
+ return new Response(this.registrationManager.getTermsOfService());
330
637
  }
331
- if (this.#config.registrationStoreUrl !== undefined) {
332
- this.#api.post('/registration', async (req, res) => {
333
- const requestBody = req.body;
334
- log.info('Registration request:', requestBody);
335
- try {
336
- await this.registrationManager.handleRegistrationRequest(requestBody);
337
- res.status(200).json({ success: true });
638
+ if (method === 'POST' && path === '/registration'
639
+ && this.#config.registrationStoreUrl !== undefined) {
640
+ const requestBody = await req.json();
641
+ log.info('Registration request received');
642
+ try {
643
+ await this.registrationManager.handleRegistrationRequest(requestBody);
644
+ return Response.json({ success: true }, { status: 200 });
645
+ }
646
+ catch (error) {
647
+ const dwnServerError = error;
648
+ if (dwnServerError.code !== undefined) {
649
+ return Response.json(dwnServerError, { status: 400 });
338
650
  }
339
- catch (error) {
340
- const dwnServerError = error;
341
- if (dwnServerError.code !== undefined) {
342
- res.status(400).json(dwnServerError);
343
- }
344
- else {
345
- log.info('Error handling registration request:', error);
346
- res.status(500).json({ success: false });
347
- }
651
+ else {
652
+ log.info('Error handling registration request:', error);
653
+ return Response.json({ success: false }, { status: 500 });
348
654
  }
349
- });
655
+ }
350
656
  }
657
+ return null;
351
658
  }
352
- #setupWeb5ConnectServerRoutes() {
353
- /**
354
- * Endpoint allows a Client app (RP) to submit an Authorization Request.
355
- * The Authorization Request is stored on the server, and a unique `request_uri` is returned to the Client app.
356
- * The Client app can then provide this `request_uri` to the Provider app (wallet).
357
- * The Provider app uses the `request_uri` to retrieve the stored Authorization Request.
358
- */
359
- this.#api.post('/connect/par', async (req, res) => {
659
+ // ---------------------------------------------------------------------------
660
+ // Web5 Connect routes
661
+ // ---------------------------------------------------------------------------
662
+ async #matchWeb5ConnectRoutes(req, path, method) {
663
+ // POST /connect/par
664
+ if (method === 'POST' && path === '/connect/par') {
360
665
  log.info('Storing Pushed Authorization Request (PAR) request...');
361
- // TODO: Add validation for request too large HTTP 413: https://github.com/TBD54566975/dwn-server/issues/146
362
- // TODO: Add validation for too many requests HTTP 429: https://github.com/TBD54566975/dwn-server/issues/147
363
- if (!req.body.request) {
364
- return res.status(400).json({
666
+ const body = await req.json();
667
+ if (!body.request) {
668
+ return Response.json({
365
669
  ok: false,
366
- status: {
367
- code: 400,
368
- message: "Bad Request: Missing 'request' parameter",
369
- },
370
- });
670
+ status: { code: 400, message: 'Bad Request: Missing \'request\' parameter' },
671
+ }, { status: 400 });
371
672
  }
372
- // Validate that `request_uri` was NOT provided
373
- if (req.body?.request?.request_uri) {
374
- return res.status(400).json({
673
+ if (body?.request?.request_uri) {
674
+ return Response.json({
375
675
  ok: false,
376
- status: {
377
- code: 400,
378
- message: "Bad Request: 'request_uri' parameter is not allowed in PAR",
379
- },
380
- });
676
+ status: { code: 400, message: 'Bad Request: \'request_uri\' parameter is not allowed in PAR' },
677
+ }, { status: 400 });
381
678
  }
382
- const result = await this.web5ConnectServer.setWeb5ConnectRequest(req.body.request);
383
- res.status(201).json(result);
384
- });
385
- /**
386
- * Endpoint for the Provider to retrieve the Authorization Request from the request_uri
387
- */
388
- this.#api.get('/connect/authorize/:requestId.jwt', async (req, res) => {
389
- log.info(`Retrieving Web5 Connect Request object of ID: ${req.params.requestId}...`);
390
- // Look up the request object based on the requestId.
391
- const requestObjectJwt = await this.web5ConnectServer.getWeb5ConnectRequest(req.params.requestId);
392
- if (!requestObjectJwt) {
393
- res.status(404).json({
394
- ok: false,
395
- status: { code: 404, message: 'Not Found' }
396
- });
397
- }
398
- else {
399
- res.set('Content-Type', 'application/jwt');
400
- res.send(requestObjectJwt);
679
+ const result = await this.web5ConnectServer.setWeb5ConnectRequest(body.request);
680
+ return Response.json(result, { status: 201 });
681
+ }
682
+ // GET /connect/authorize/:requestId.jwt
683
+ {
684
+ const match = path.match(/^\/connect\/authorize\/([^/]+)\.jwt$/);
685
+ if (match && method === 'GET') {
686
+ const requestId = match[1];
687
+ log.info(`Retrieving Web5 Connect Request object of ID: ${requestId}...`);
688
+ const requestObjectJwt = await this.web5ConnectServer.getWeb5ConnectRequest(requestId);
689
+ if (!requestObjectJwt) {
690
+ return Response.json({
691
+ ok: false,
692
+ status: { code: 404, message: 'Not Found' },
693
+ }, { status: 404 });
694
+ }
695
+ else {
696
+ const body = typeof requestObjectJwt === 'string'
697
+ ? requestObjectJwt
698
+ : JSON.stringify(requestObjectJwt);
699
+ return new Response(body, {
700
+ headers: { 'content-type': 'application/jwt' },
701
+ });
702
+ }
401
703
  }
402
- });
403
- /**
404
- * Endpoint that the Provider sends the Authorization Response to
405
- */
406
- this.#api.post('/connect/callback', async (req, res) => {
704
+ }
705
+ // POST /connect/callback
706
+ if (method === 'POST' && path === '/connect/callback') {
407
707
  log.info('Storing Identity Provider (wallet) pushed response with ID token...');
408
- // Store the ID token.
409
- const idToken = req.body.id_token;
410
- const state = req.body.state;
708
+ const body = await req.json();
709
+ const idToken = body.id_token;
710
+ const state = body.state;
411
711
  if (idToken !== undefined && state != undefined) {
412
712
  await this.web5ConnectServer.setWeb5ConnectResponse(state, idToken);
413
- res.status(201).json({
713
+ return Response.json({
414
714
  ok: true,
415
- status: { code: 201, message: 'Created' }
416
- });
715
+ status: { code: 201, message: 'Created' },
716
+ }, { status: 201 });
417
717
  }
418
718
  else {
419
- res.status(400).json({
719
+ return Response.json({
420
720
  ok: false,
421
- status: { code: 400, message: 'Bad Request' }
422
- });
721
+ status: { code: 400, message: 'Bad Request' },
722
+ }, { status: 400 });
423
723
  }
424
- });
425
- /**
426
- * Endpoint for the connecting Client to retrieve the Authorization Response
427
- */
428
- this.#api.get('/connect/token/:state.jwt', async (req, res) => {
429
- log.info(`Retrieving ID token for state: ${req.params.state}...`);
430
- // Look up the ID token.
431
- const idToken = await this.web5ConnectServer.getWeb5ConnectResponse(req.params.state);
432
- if (!idToken) {
433
- res.status(404).json({
434
- ok: false,
435
- status: { code: 404, message: 'Not Found' }
436
- });
437
- }
438
- else {
439
- res.set('Content-Type', 'application/jwt');
440
- res.send(idToken);
441
- }
442
- });
443
- }
444
- /**
445
- * Starts the HTTP API endpoint on the given port.
446
- * @returns The HTTP server instance.
447
- */
448
- async start(port) {
449
- // promisify http.Server.listen() and await on it
450
- await new Promise((resolve) => {
451
- this.#server.listen(port, () => {
452
- resolve();
453
- });
454
- });
455
- }
456
- /**
457
- * Stops the HTTP API endpoint.
458
- */
459
- async close() {
460
- // promisify http.Server.close() and await on it
461
- await new Promise((resolve, reject) => {
462
- this.#server.close((err) => {
463
- if (err) {
464
- reject(err);
724
+ }
725
+ // GET /connect/token/:state.jwt
726
+ {
727
+ const match = path.match(/^\/connect\/token\/([^/]+)\.jwt$/);
728
+ if (match && method === 'GET') {
729
+ const state = match[1];
730
+ log.info(`Retrieving ID token for state: ${state}...`);
731
+ const idToken = await this.web5ConnectServer.getWeb5ConnectResponse(state);
732
+ if (!idToken) {
733
+ return Response.json({
734
+ ok: false,
735
+ status: { code: 404, message: 'Not Found' },
736
+ }, { status: 404 });
465
737
  }
466
738
  else {
467
- resolve();
739
+ const body = typeof idToken === 'string' ? idToken : JSON.stringify(idToken);
740
+ return new Response(body, {
741
+ headers: { 'content-type': 'application/jwt' },
742
+ });
468
743
  }
469
- });
470
- });
471
- this.server.closeAllConnections();
744
+ }
745
+ }
746
+ return null;
472
747
  }
473
748
  }
474
749
  //# sourceMappingURL=http-api.js.map