@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
package/src/http-api.ts CHANGED
@@ -1,541 +1,885 @@
1
+ import type { JsonRpcRequest } from '@enbox/dwn-clients';
1
2
  import type { RecordsReadReply } from '@enbox/dwn-sdk-js';
2
- import { type Dwn, DateSort, RecordsRead, RecordsQuery, ProtocolsQuery } from '@enbox/dwn-sdk-js';
3
+ import type { ServerInfo } from '@enbox/dwn-clients';
4
+ import type { Server, ServerWebSocket } from 'bun';
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';
3
18
 
4
- import cors from 'cors';
5
- import type { Express, Request, Response } from 'express';
6
- import express from 'express';
7
- import { readFileSync } from 'fs';
8
- import http from 'http';
9
19
  import log from 'loglevel';
20
+
21
+ import { Convert } from '@enbox/common';
10
22
  import { register } from 'prom-client';
11
- import responseTime from 'response-time';
12
23
  import { v4 as uuidv4 } from 'uuid';
24
+ import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from '@enbox/dwn-clients';
25
+ import { DataStream, DateSort, type Dwn, ProtocolsQuery, RecordsQuery, RecordsRead } from '@enbox/dwn-sdk-js';
26
+ import { existsSync, readFileSync } from 'fs';
27
+ import { join, resolve } from 'path';
13
28
 
14
- import type { RequestContext } from './lib/json-rpc-router.js';
15
- import type { JsonRpcRequest } from './lib/json-rpc.js';
16
-
17
- import type { DwnServerConfig } from './config.js';
18
- import type { DwnServerError } from './dwn-error.js';
19
- import type { RegistrationManager } from './registration/registration-manager.js';
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
- import { Convert } from '@enbox/common';
26
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
+
48
+ /** Data attached to each Bun WebSocket via `ws.data`. */
49
+ export interface WsData {
50
+ connection: SocketConnection;
51
+ }
27
52
 
28
53
  export class HttpApi {
29
54
  #config: DwnServerConfig;
30
55
  #packageInfo: { version?: string, sdkVersion?: string, server: string };
31
- #api: Express;
32
- #server: http.Server;
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;
33
66
  web5ConnectServer: Web5ConnectServer;
34
67
  registrationManager: RegistrationManager;
35
68
  dwn: Dwn;
36
69
 
70
+ /** Called by WsApi/ConnectionManager when a new WS connection is established. */
71
+ onWebSocketConnection?: (ws: ServerWebSocket<WsData>) => void;
72
+
37
73
  private constructor() { }
38
74
 
39
- public static async create(config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager): Promise<HttpApi> {
75
+ public static async create(
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
+ },
86
+ ): Promise<HttpApi> {
40
87
  const httpApi = new HttpApi();
41
88
 
42
- log.info(config);
89
+ log.info(HttpApi.#redactConfig(config));
43
90
 
44
91
  httpApi.#packageInfo = {
45
92
  server: config.serverName,
46
93
  };
47
-
94
+
48
95
  try {
49
- // We populate the `version` and `sdkVersion` properties from the `package.json` file.
50
96
  const packageJson = JSON.parse(readFileSync(config.packageJsonPath).toString());
51
97
  httpApi.#packageInfo.version = packageJson.version;
52
- httpApi.#packageInfo.sdkVersion = packageJson.dependencies ? packageJson.dependencies['@enbox/dwn-sdk-js'] : undefined;
98
+ httpApi.#packageInfo.sdkVersion = packageJson.dependencies
99
+ ? packageJson.dependencies['@enbox/dwn-sdk-js']
100
+ : undefined;
53
101
  } catch (error: any) {
54
102
  log.info('could not read `package.json` for version info', error);
55
103
  }
56
104
 
57
105
  httpApi.#config = config;
58
- httpApi.#api = express();
59
- httpApi.#server = http.createServer(httpApi.#api);
60
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;
61
116
 
62
117
  if (registrationManager !== undefined) {
63
118
  httpApi.registrationManager = registrationManager;
64
119
  }
65
120
 
66
- // create the Web5 Connect Server
67
121
  httpApi.web5ConnectServer = await Web5ConnectServer.create({
68
- baseUrl: config.baseUrl,
69
- sqlTtlCacheUrl: config.ttlCacheUrl,
122
+ baseUrl : config.baseUrl,
123
+ sqlTtlCacheUrl : config.ttlCacheUrl,
70
124
  });
71
125
 
72
- httpApi.#setupMiddleware();
73
- httpApi.#setupRoutes();
74
-
75
126
  return httpApi;
76
127
  }
77
128
 
78
- get server(): http.Server {
129
+ get server(): Server<WsData> {
79
130
  return this.#server;
80
131
  }
81
132
 
82
- get api(): Express {
83
- return this.#api;
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;
84
143
  }
85
144
 
86
- #setupMiddleware(): void {
87
- this.#api.use(cors({ exposedHeaders: 'dwn-response' }));
88
- this.#api.use(express.json());
89
-
90
- // We enable the formData middleware to handle multipart/form-data requests.
91
- // This is necessary for the endpoints used by the Web5 Connect Server/OIDC flow.
92
- this.#api.use(express.urlencoded({ extended: true }));
93
- this.#api.use(
94
- responseTime((req: Request, res: Response, time) => {
95
- const url = req.url === '/' ? '/jsonrpc' : req.url;
96
- const route = (req.method + url)
145
+ // ---------------------------------------------------------------------------
146
+ // HTTP request handler
147
+ // ---------------------------------------------------------------------------
148
+
149
+ async start(port: number): Promise<void> {
150
+ const self = this; // capture for closures
151
+
152
+ this.#server = Bun.serve<WsData>({
153
+ port,
154
+
155
+ async fetch(req: Request, server): Promise<Response | undefined> {
156
+ const startTime = performance.now();
157
+ const url = new URL(req.url);
158
+ const path = url.pathname;
159
+ const method = req.method;
160
+
161
+ // --- WebSocket upgrade ---
162
+ if (method === 'GET' && req.headers.get('upgrade') === 'websocket') {
163
+ const upgraded = server.upgrade(req, { data: { connection: null } });
164
+ if (upgraded) {
165
+ return undefined;
166
+ }
167
+ return new Response('WebSocket upgrade failed', { status: 400 });
168
+ }
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
+
189
+ // --- Route matching ---
190
+ let response: Response;
191
+ try {
192
+ response = await self.#route(req, url, path, method);
193
+ } catch (error) {
194
+ log.error(`Unhandled error on ${method} ${path}:`, error);
195
+ response = new Response('Internal Server Error', { status: 500 });
196
+ }
197
+
198
+ // --- CORS headers ---
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
+ }
208
+
209
+ // --- Response-time metrics ---
210
+ const elapsed = performance.now() - startTime;
211
+ const routeLabel = (method + (path === '/' ? '/jsonrpc' : path))
97
212
  .toLowerCase()
98
213
  .replace(/[:.]/g, '')
99
214
  .replace(/\//g, '_');
215
+ responseHistogram.labels(routeLabel, String(response.status)).observe(elapsed);
216
+ log.info(method, decodeURI(path), response.status);
100
217
 
101
- const statusCode = res.statusCode.toString();
102
- responseHistogram.labels(route, statusCode).observe(time);
103
- log.info(req.method, decodeURI(req.url), res.statusCode);
104
- }),
105
- );
218
+ return response;
219
+ },
220
+
221
+ websocket: {
222
+ maxPayloadLength: self.#config.maxRecordDataSize,
223
+ open(ws: ServerWebSocket<WsData>): void {
224
+ if (self.onWebSocketConnection) {
225
+ self.onWebSocketConnection(ws);
226
+ }
227
+ },
228
+ message(ws: ServerWebSocket<WsData>, msg: string | Buffer): void {
229
+ const connection = ws.data?.connection;
230
+ if (connection) {
231
+ connection.message(typeof msg === 'string' ? Buffer.from(msg) : msg as Buffer);
232
+ }
233
+ },
234
+ close(ws: ServerWebSocket<WsData>): void {
235
+ const connection = ws.data?.connection;
236
+ if (connection) {
237
+ connection.close();
238
+ }
239
+ },
240
+ pong(ws: ServerWebSocket<WsData>): void {
241
+ const connection = ws.data?.connection;
242
+ if (connection) {
243
+ connection.pong();
244
+ }
245
+ },
246
+ },
247
+ });
106
248
  }
107
249
 
250
+ async close(): Promise<void> {
251
+ if (this.#openAuthHandler) {
252
+ this.#openAuthHandler.destroy();
253
+ }
254
+ if (this.#server) {
255
+ this.#server.stop(true); // close all connections immediately
256
+ }
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // Admin UI static file serving
261
+ // ---------------------------------------------------------------------------
262
+
108
263
  /**
109
- * Configures the HTTP server's request handlers.
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).
110
267
  */
111
- #setupRoutes(): void {
268
+ #serveAdminUi(path: string): Response | null {
269
+ if (!this.#adminUiPath) {
270
+ return null;
271
+ }
112
272
 
113
- const leadTailSlashRegex = /^\/|\/$/;
273
+ // Strip the `/admin` prefix to get the file path within the dist directory.
274
+ const relativePath = path.replace(/^\/admin\/?/, '');
114
275
 
115
- function readReplyHandler(res, reply: RecordsReadReply): any {
116
- if (reply.status.code === 200) {
117
- if (reply?.entry?.data) {
118
- const stream = reply.entry.data;
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
+ }
119
284
 
120
- res.setHeader('content-type', reply.entry.recordsWrite.descriptor.dataFormat);
121
- res.setHeader('dwn-response', JSON.stringify(reply));
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
+ }
122
290
 
123
- return stream.pipe(res);
124
- } else {
125
- return res.sendStatus(400);
126
- }
127
- } else if (reply.status.code === 401) {
128
- return res.sendStatus(404);
129
- } else {
130
- return res.status(reply.status.code).send(reply);
131
- }
291
+ if (!existsSync(filePath)) {
292
+ return null;
132
293
  }
133
294
 
134
- this.#api.get('/health', (_req, res) => {
135
- // return 200 ok
136
- return res.json({ ok: true });
137
- });
295
+ const file = Bun.file(filePath);
296
+ return new Response(file);
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Router
301
+ // ---------------------------------------------------------------------------
138
302
 
139
- this.#api.get('/metrics', async (req, res) => {
303
+ async #route(req: Request, url: URL, path: string, method: string): Promise<Response> {
304
+ // --- CORS preflight ---
305
+ if (method === 'OPTIONS') {
306
+ return new Response(null, { status: 204 });
307
+ }
308
+
309
+ // --- Static routes ---
310
+ if (method === 'GET' && path === '/health') {
311
+ return Response.json({ ok: true });
312
+ }
313
+
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
+ }
140
322
  try {
141
- res.set('Content-Type', register.contentType);
142
- res.end(await register.metrics());
323
+ const metricsBody = await register.metrics();
324
+ return new Response(metricsBody, {
325
+ headers: { 'content-type': register.contentType },
326
+ });
143
327
  } catch (e) {
144
- res.status(500).end(e);
328
+ return new Response(String(e), { status: 500 });
145
329
  }
146
- });
330
+ }
147
331
 
148
- // Returns the data for the most recently published record under a given protocol path collection, if one is present
149
- this.#api.get('/:did/read/protocols/:protocol/*', async (req, res) => {
150
- if (!req.params[0]) {
151
- return res.status(400).send('protocol path is required');
152
- }
332
+ if (method === 'GET' && path === '/') {
333
+ return new Response(
334
+ 'please use am enbox client, for example: https://github.com/enboxorg/enbox ',
335
+ { headers: { 'content-type': 'text/plain' } },
336
+ );
337
+ }
153
338
 
154
- // wrap request in a try-catch block to handle any unexpected errors
155
- try {
156
- const queryOptions = { filter: {} } as any;
157
- for (const param in req.query) {
158
- const keys = param.split('.');
159
- const lastKey = keys.pop();
160
- const lastLevelObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, queryOptions)
161
- lastLevelObject[lastKey] = req.query[param];
162
- }
339
+ if (method === 'GET' && path === '/info') {
340
+ return this.#handleInfo();
341
+ }
163
342
 
164
- // the protocol path segment is base64url encoded, as the actual protocol is a URL
165
- // we decode it here in order to filter for the correct protocol
166
- const protocol = Convert.base64Url(req.params.protocol).toString()
167
- queryOptions.filter.protocol = protocol;
168
- queryOptions.filter.protocolPath = req.params[0].replace(leadTailSlashRegex, '');
343
+ // --- JSON-RPC POST ---
344
+ if (method === 'POST' && path === '/') {
345
+ return this.#handleJsonRpcPost(req);
346
+ }
169
347
 
170
- const query = await RecordsQuery.create({
171
- filter: queryOptions.filter,
172
- pagination: { limit: 1 },
173
- dateSort: DateSort.PublishedDescending
174
- });
348
+ // --- Admin API routes ---
349
+ if (path.startsWith('/admin/api/') && this.#adminApi) {
350
+ return this.#adminApi.route(req, url, path, method);
351
+ }
175
352
 
176
- const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
177
-
178
- if (status.code === 200) {
179
- if (entries[0]) {
180
- const record = await RecordsRead.create({
181
- filter: { recordId: entries[0].recordId },
182
- });
183
- const reply = await this.dwn.processMessage(req.params.did, record.toJSON());
184
- return readReplyHandler(res, reply);
185
- } else {
186
- return res.sendStatus(404);
187
- }
188
- } else if (status.code === 401) {
189
- return res.sendStatus(404);
190
- } else {
191
- return res.sendStatus(status.code);
192
- }
193
- } catch(error) {
194
- log.error(`Error processing request: ${decodeURI(req.url)}`, error);
195
- return res.sendStatus(400);
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;
196
358
  }
197
- })
198
-
199
- this.#api.get('/:did/read/protocols/:protocol', async (req, res) => {
200
- // wrap request in a try-catch block to handle any unexpected errors
201
- try {
359
+ }
202
360
 
203
- // the protocol segment is base64url encoded, as the actual protocol is a URL
204
- // we decode it here in order to filter for the correct protocol
205
- const protocol = Convert.base64Url(req.params.protocol).toString()
206
- const query = await ProtocolsQuery.create({
207
- filter: { protocol }
208
- });
209
- const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
210
- if (status.code === 200) {
211
- if (entries.length) {
212
- res.status(status.code);
213
- res.json(entries[0]);
214
- } else {
215
- return res.sendStatus(404);
216
- }
217
- } else if (status.code === 401) {
218
- return res.sendStatus(404);
219
- } else {
220
- return res.sendStatus(status.code);
221
- }
222
- } catch(error) {
223
- log.error(`Error processing request: ${decodeURI(req.url)}`, error);
224
- return res.sendStatus(400);
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);
225
365
  }
226
- })
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
+ }
227
373
 
228
- const recordsReadHandler = async (req, res): Promise<any> => {
229
- const record = await RecordsRead.create({
230
- filter: { recordId: req.params.id },
231
- });
232
- const reply = await this.dwn.processMessage(req.params.did, record.message);
233
- return readReplyHandler(res, reply);
374
+ // --- Registration routes ---
375
+ const registrationResponse = await this.#matchRegistrationRoutes(req, path, method);
376
+ if (registrationResponse) {
377
+ return registrationResponse;
234
378
  }
235
379
 
236
- this.#api.get('/:did/read/records/:id', recordsReadHandler);
237
- this.#api.get('/:did/records/:id', recordsReadHandler);
380
+ // --- Web5 Connect routes ---
381
+ const connectResponse = await this.#matchWeb5ConnectRoutes(req, path, method);
382
+ if (connectResponse) {
383
+ return connectResponse;
384
+ }
238
385
 
239
- this.#api.get('/:did/query/protocols', async (req, res) => {
240
- const query = await ProtocolsQuery.create({});
241
- const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
242
- if (status.code === 200) {
243
- res.status(status.code);
244
- res.json(entries);
245
- } else if (status.code === 401) {
246
- return res.sendStatus(404);
247
- } else {
248
- return res.sendStatus(status.code);
249
- }
250
- });
386
+ // --- DID routes (parameterized) ---
387
+ return this.#matchDidRoutes(req, url, path);
388
+ }
251
389
 
252
- this.#api.get('/:did/query', async (req, res) => {
253
-
254
- try {
255
- // builds a nested object from flat keys with dot notation which may share the same parent path
256
- // e.g. "did:dht:123/query?filter.protocol=foo&filter.protocolPath=bar" becomes
257
- // {
258
- // filter: {
259
- // protocol: 'foo',
260
- // protocolPath: 'bar'
261
- // }
262
- // }
263
- const recordsQueryOptions = {} as any;
264
- for (const param in req.query) {
265
- const keys = param.split('.');
266
- const lastKey = keys.pop();
267
- const lastLevelObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, recordsQueryOptions)
268
- lastLevelObject[lastKey] = req.query[param];
269
- }
270
-
271
- const recordsQuery = await RecordsQuery.create({
272
- filter: recordsQueryOptions.filter,
273
- pagination: recordsQueryOptions.pagination,
274
- dateSort: recordsQueryOptions.dateSort,
275
- });
390
+ // ---------------------------------------------------------------------------
391
+ // DID convenience routes
392
+ // ---------------------------------------------------------------------------
276
393
 
277
- // should always return a 200 status code with a JSON response
278
- const reply = await this.dwn.processMessage(req.params.did, recordsQuery.message);
394
+ async #matchDidRoutes(req: Request, url: URL, path: string): Promise<Response> {
395
+ const leadTailSlashRegex = /^\/|\/$/g;
279
396
 
280
- res.setHeader('content-type', 'application/json');
281
- return res.json(reply);
282
- } catch (error) {
283
- // error should only occur when we are unable to create the RecordsQuery message internally, making it a client error
284
- return res.status(400).send(error);
397
+ // /:did/read/protocols/:protocol/* (also matches trailing slash with empty path)
398
+ {
399
+ const match = path.match(/^\/([^/]+)\/read\/protocols\/([^/]+)\/(.*)$/);
400
+ if (match && req.method === 'GET') {
401
+ const [, did, protocolParam, protocolPathRaw] = match;
402
+ return this.#handleReadProtocolRecord(did, protocolParam, protocolPathRaw, url, leadTailSlashRegex);
285
403
  }
286
- });
404
+ }
287
405
 
288
- this.#api.get('/', (_req, res) => {
289
- // return a plain text string
290
- res.setHeader('content-type', 'text/plain');
291
- return res.send('please use a web5 client, for example: https://github.com/TBD54566975/web5-js ');
292
- });
406
+ // /:did/read/protocols/:protocol
407
+ {
408
+ const match = path.match(/^\/([^/]+)\/read\/protocols\/([^/]+)$/);
409
+ if (match && req.method === 'GET') {
410
+ const [, did, protocolParam] = match;
411
+ return this.#handleReadProtocol(did, protocolParam);
412
+ }
413
+ }
293
414
 
294
- this.#api.post('/', async (req: Request, res) => {
295
- const dwnRpcRequestString = req.headers['dwn-request'] as string;
415
+ // /:did/read/records/:id OR /:did/records/:id
416
+ {
417
+ const match = path.match(/^\/([^/]+)\/(?:read\/)?records\/([^/]+)$/);
418
+ if (match && req.method === 'GET') {
419
+ const [, did, recordId] = match;
420
+ return this.#handleReadRecord(did, recordId);
421
+ }
422
+ }
296
423
 
297
- if (!dwnRpcRequestString) {
298
- const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, 'request payload required.');
424
+ // /:did/query/protocols
425
+ {
426
+ const match = path.match(/^\/([^/]+)\/query\/protocols$/);
427
+ if (match && req.method === 'GET') {
428
+ const [, did] = match;
429
+ return this.#handleQueryProtocols(did);
430
+ }
431
+ }
299
432
 
300
- return res.status(400).json(reply);
433
+ // /:did/query
434
+ {
435
+ const match = path.match(/^\/([^/]+)\/query$/);
436
+ if (match && req.method === 'GET') {
437
+ const [, did] = match;
438
+ return this.#handleQueryRecords(did, url);
301
439
  }
440
+ }
302
441
 
303
- let dwnRpcRequest: JsonRpcRequest;
304
- try {
305
- dwnRpcRequest = JSON.parse(dwnRpcRequestString);
306
- } catch (e) {
307
- const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, e.message);
442
+ return new Response('Not Found', { status: 404 });
443
+ }
308
444
 
309
- return res.status(400).json(reply);
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]';
310
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
+ }
311
476
 
312
- // Check whether data was provided in the request body
313
- const contentLength = req.headers['content-length'];
314
- const transferEncoding = req.headers['transfer-encoding'];
315
- const requestDataStream = parseInt(contentLength) > 0 || transferEncoding !== undefined ? req : undefined;
477
+ // ---------------------------------------------------------------------------
478
+ // Handlers
479
+ // ---------------------------------------------------------------------------
316
480
 
317
- const requestContext: RequestContext = {
318
- dwn : this.dwn,
319
- transport : 'http',
320
- dataStream : requestDataStream,
481
+ #handleInfo(): Response {
482
+ const registrationRequirements: string[] = [];
483
+ if (config.registrationProofOfWorkEnabled) {
484
+ registrationRequirements.push('proof-of-work-sha256-v0');
485
+ }
486
+ if (config.termsOfServiceFilePath !== undefined) {
487
+ registrationRequirements.push('terms-of-service');
488
+ }
489
+ if (config.providerAuthEnabled && !registrationRequirements.includes('provider-auth-v0')) {
490
+ registrationRequirements.push('provider-auth-v0');
491
+ }
492
+
493
+ const serverInfo: ServerInfo = {
494
+ maxFileSize : config.maxRecordDataSize,
495
+ maxInFlight : config.maxInFlight,
496
+ registrationRequirements : registrationRequirements,
497
+ server : this.#packageInfo.server,
498
+ sdkVersion : this.#packageInfo.sdkVersion,
499
+ url : config.baseUrl,
500
+ version : this.#packageInfo.version,
501
+ webSocketSupport : config.webSocketSupport,
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,
321
510
  };
322
- const { jsonRpcResponse, dataStream: responseDataStream } = await jsonRpcRouter.handle(dwnRpcRequest, requestContext as RequestContext);
511
+ }
512
+
513
+ return Response.json(serverInfo);
514
+ }
515
+
516
+ async #handleJsonRpcPost(req: Request): Promise<Response> {
517
+ const dwnRpcRequestString = req.headers.get('dwn-request');
518
+
519
+ if (!dwnRpcRequestString) {
520
+ const reply = createJsonRpcErrorResponse(
521
+ uuidv4(), JsonRpcErrorCodes.BadRequest, 'request payload required.'
522
+ );
523
+ return Response.json(reply, { status: 400 });
524
+ }
323
525
 
324
- // If the handler catches a thrown exception and returns a JSON RPC InternalError, return the equivalent
325
- // HTTP 500 Internal Server Error with the response.
326
- if (jsonRpcResponse.error) {
327
- requestCounter.inc({ method: dwnRpcRequest.method, error: 1 });
328
- return res.status(500).json(jsonRpcResponse);
526
+ let dwnRpcRequest: JsonRpcRequest;
527
+ try {
528
+ dwnRpcRequest = JSON.parse(dwnRpcRequestString);
529
+ } catch (e) {
530
+ const reply = createJsonRpcErrorResponse(
531
+ uuidv4(), JsonRpcErrorCodes.BadRequest, (e as Error).message
532
+ );
533
+ return Response.json(reply, { status: 400 });
534
+ }
535
+
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
543
+ const contentLength = req.headers.get('content-length');
544
+ const transferEncoding = req.headers.get('transfer-encoding');
545
+ let requestDataStream: ReadableStream<Uint8Array> | undefined;
546
+ if (parseInt(contentLength ?? '0') > 0 || transferEncoding !== null) {
547
+ const bodyBytes = new Uint8Array(await req.arrayBuffer());
548
+ requestDataStream = DataStream.fromBytes(bodyBytes);
549
+ }
550
+
551
+ const requestContext: RequestContext = {
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,
561
+ };
562
+ const { jsonRpcResponse, dataStream: responseDataStream } =
563
+ await jsonRpcRouter.handle(dwnRpcRequest, requestContext);
564
+
565
+ if (jsonRpcResponse.error) {
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
+ });
329
575
  }
330
576
 
331
- requestCounter.inc({
332
- method : dwnRpcRequest.method,
333
- status : jsonRpcResponse?.result?.reply?.status?.code || 0,
577
+ return Response.json(jsonRpcResponse, { status: 500 });
578
+ }
579
+
580
+ requestCounter.inc({
581
+ method : dwnRpcRequest.method,
582
+ status : jsonRpcResponse?.result?.reply?.status?.code || 0,
583
+ });
584
+
585
+ if (responseDataStream) {
586
+ return new Response(responseDataStream, {
587
+ headers: {
588
+ 'content-type' : 'application/octet-stream',
589
+ 'dwn-response' : JSON.stringify(jsonRpcResponse),
590
+ },
334
591
  });
335
- if (responseDataStream) {
336
- res.setHeader('content-type', 'application/octet-stream');
337
- res.setHeader('dwn-response', JSON.stringify(jsonRpcResponse));
592
+ } else {
593
+ return Response.json(jsonRpcResponse);
594
+ }
595
+ }
338
596
 
339
- return responseDataStream.pipe(res);
597
+ #readReplyToResponse(reply: RecordsReadReply): Response {
598
+ if (reply.status.code === 200) {
599
+ if (reply?.entry?.data) {
600
+ return new Response(reply.entry.data, {
601
+ headers: {
602
+ 'content-type' : reply.entry.recordsWrite.descriptor.dataFormat,
603
+ 'dwn-response' : JSON.stringify(reply),
604
+ },
605
+ });
340
606
  } else {
341
- return res.json(jsonRpcResponse);
607
+ return new Response(null, { status: 400 });
342
608
  }
609
+ } else if (reply.status.code === 401) {
610
+ return new Response(null, { status: 404 });
611
+ } else {
612
+ return Response.json(reply, { status: reply.status.code });
613
+ }
614
+ }
615
+
616
+ async #handleReadRecord(did: string, recordId: string): Promise<Response> {
617
+ const record = await RecordsRead.create({
618
+ filter: { recordId },
343
619
  });
620
+ const reply = await this.dwn.processMessage(did, record.message);
621
+ return this.#readReplyToResponse(reply);
622
+ }
344
623
 
345
- this.#setupRegistrationRoutes();
624
+ async #handleReadProtocolRecord(
625
+ did: string, protocolParam: string, protocolPathRaw: string,
626
+ url: URL, leadTailSlashRegex: RegExp
627
+ ): Promise<Response> {
628
+ if (!protocolPathRaw || protocolPathRaw.replace(leadTailSlashRegex, '') === '') {
629
+ return new Response('protocol path is required', { status: 400 });
630
+ }
346
631
 
347
- this.#api.get('/info', (req, res) => {
348
- res.setHeader('content-type', 'application/json');
349
- const registrationRequirements: string[] = [];
350
- if (config.registrationProofOfWorkEnabled) {
351
- registrationRequirements.push('proof-of-work-sha256-v0');
352
- }
353
- if (config.termsOfServiceFilePath !== undefined) {
354
- registrationRequirements.push('terms-of-service');
632
+ try {
633
+ const queryOptions: Record<string, any> = { filter: {} };
634
+ for (const [param, value] of url.searchParams) {
635
+ const keys = param.split('.');
636
+ const lastKey = keys.pop();
637
+ if (HttpApi.#hasDangerousKey(keys) || HttpApi.#isDangerousKey(lastKey)) {
638
+ continue;
639
+ }
640
+ const nestObj = (obj: Record<string, any>, key: string): Record<string, any> =>
641
+ obj[key] = obj[key] || {};
642
+ const lastLevelObject = keys.reduce(nestObj, queryOptions);
643
+ lastLevelObject[lastKey!] = value;
355
644
  }
356
645
 
357
- res.json({
358
- url : config.baseUrl,
359
- server : this.#packageInfo.server,
360
- maxFileSize : config.maxRecordDataSize,
361
- registrationRequirements : registrationRequirements,
362
- version : this.#packageInfo.version,
363
- sdkVersion : this.#packageInfo.sdkVersion,
364
- webSocketSupport : config.webSocketSupport,
646
+ const protocol = Convert.base64Url(protocolParam).toString();
647
+ queryOptions.filter.protocol = protocol;
648
+ queryOptions.filter.protocolPath = protocolPathRaw.replace(leadTailSlashRegex, '');
649
+
650
+ const query = await RecordsQuery.create({
651
+ filter : queryOptions.filter,
652
+ pagination : { limit: 1 },
653
+ dateSort : DateSort.PublishedDescending,
365
654
  });
366
- });
367
655
 
368
- this.#setupWeb5ConnectServerRoutes();
656
+ const { entries, status } = await this.dwn.processMessage(did, query.message);
657
+
658
+ if (status.code === 200) {
659
+ if (entries[0]) {
660
+ const record = await RecordsRead.create({
661
+ filter: { recordId: entries[0].recordId },
662
+ });
663
+ const reply = await this.dwn.processMessage(did, record.toJSON());
664
+ return this.#readReplyToResponse(reply);
665
+ } else {
666
+ return new Response(null, { status: 404 });
667
+ }
668
+ } else if (status.code === 401) {
669
+ return new Response(null, { status: 404 });
670
+ } else {
671
+ return new Response(null, { status: status.code });
672
+ }
673
+ } catch (error) {
674
+ log.error(`Error processing request: ${decodeURI(url.pathname)}`, error);
675
+ return new Response('Bad Request', { status: 400 });
676
+ }
369
677
  }
370
678
 
371
- #setupRegistrationRoutes(): void {
372
- if (this.#config.registrationProofOfWorkEnabled) {
373
- this.#api.get('/registration/proof-of-work', async (_req: Request, res: Response) => {
374
- const proofOfWorkChallenge = this.registrationManager.getProofOfWorkChallenge();
375
- res.json(proofOfWorkChallenge);
679
+ async #handleReadProtocol(did: string, protocolParam: string): Promise<Response> {
680
+ try {
681
+ const protocol = Convert.base64Url(protocolParam).toString();
682
+ const query = await ProtocolsQuery.create({
683
+ filter: { protocol },
376
684
  });
685
+ const { entries, status } = await this.dwn.processMessage(did, query.message);
686
+ if (status.code === 200) {
687
+ if (entries.length) {
688
+ return Response.json(entries[0], { status: status.code });
689
+ } else {
690
+ return new Response(null, { status: 404 });
691
+ }
692
+ } else if (status.code === 401) {
693
+ return new Response(null, { status: 404 });
694
+ } else {
695
+ return new Response(null, { status: status.code });
696
+ }
697
+ } catch (error) {
698
+ log.error(`Error processing request`, error);
699
+ return new Response('Bad Request', { status: 400 });
377
700
  }
701
+ }
378
702
 
379
- if (this.#config.termsOfServiceFilePath !== undefined) {
380
- this.#api.get('/registration/terms-of-service', (_req: Request, res: Response) => res.send(this.registrationManager.getTermsOfService()));
703
+ async #handleQueryProtocols(did: string): Promise<Response> {
704
+ const query = await ProtocolsQuery.create({});
705
+ const { entries, status } = await this.dwn.processMessage(did, query.message);
706
+ if (status.code === 200) {
707
+ return Response.json(entries, { status: status.code });
708
+ } else if (status.code === 401) {
709
+ return new Response(null, { status: 404 });
710
+ } else {
711
+ return new Response(null, { status: status.code });
381
712
  }
713
+ }
382
714
 
383
- if (this.#config.registrationStoreUrl !== undefined) {
384
- this.#api.post('/registration', async (req: Request, res: Response) => {
385
- const requestBody = req.body;
386
- log.info('Registration request:', requestBody);
715
+ async #handleQueryRecords(did: string, url: URL): Promise<Response> {
716
+ try {
717
+ const recordsQueryOptions: Record<string, any> = {};
718
+ for (const [param, value] of url.searchParams) {
719
+ const keys = param.split('.');
720
+ const lastKey = keys.pop();
721
+ if (HttpApi.#hasDangerousKey(keys) || HttpApi.#isDangerousKey(lastKey)) {
722
+ continue;
723
+ }
724
+ const nestObj = (obj: Record<string, any>, key: string): Record<string, any> =>
725
+ obj[key] = obj[key] || {};
726
+ const lastLevelObject = keys.reduce(nestObj, recordsQueryOptions);
727
+ lastLevelObject[lastKey!] = value;
728
+ }
387
729
 
388
- try {
389
- await this.registrationManager.handleRegistrationRequest(requestBody);
390
- res.status(200).json({ success: true });
391
- } catch (error) {
392
- const dwnServerError = error as DwnServerError;
730
+ const recordsQuery = await RecordsQuery.create({
731
+ filter : recordsQueryOptions.filter,
732
+ pagination : recordsQueryOptions.pagination,
733
+ dateSort : recordsQueryOptions.dateSort,
734
+ });
393
735
 
394
- if (dwnServerError.code !== undefined) {
395
- res.status(400).json(dwnServerError);
396
- } else {
397
- log.info('Error handling registration request:', error);
398
- res.status(500).json({ success: false });
399
- }
400
- }
736
+ const reply = await this.dwn.processMessage(did, recordsQuery.message);
737
+ return Response.json(reply, {
738
+ headers: { 'content-type': 'application/json' },
401
739
  });
740
+ } catch (error) {
741
+ log.error('Error processing query records request', error);
742
+ return Response.json({ error: 'Bad Request' }, { status: 400 });
402
743
  }
403
744
  }
404
745
 
405
- #setupWeb5ConnectServerRoutes(): void {
406
- /**
407
- * Endpoint allows a Client app (RP) to submit an Authorization Request.
408
- * The Authorization Request is stored on the server, and a unique `request_uri` is returned to the Client app.
409
- * The Client app can then provide this `request_uri` to the Provider app (wallet).
410
- * The Provider app uses the `request_uri` to retrieve the stored Authorization Request.
411
- */
412
- this.#api.post('/connect/par', async (req, res) => {
413
- log.info('Storing Pushed Authorization Request (PAR) request...');
746
+ // ---------------------------------------------------------------------------
747
+ // Registration routes
748
+ // ---------------------------------------------------------------------------
749
+
750
+ async #matchRegistrationRoutes(
751
+ req: Request, path: string, method: string
752
+ ): Promise<Response | null> {
753
+ if (method === 'GET' && path === '/registration/proof-of-work'
754
+ && this.#config.registrationProofOfWorkEnabled) {
755
+ const proofOfWorkChallenge = this.registrationManager.getProofOfWorkChallenge();
756
+ return Response.json(proofOfWorkChallenge);
757
+ }
414
758
 
415
- // TODO: Add validation for request too large HTTP 413: https://github.com/TBD54566975/dwn-server/issues/146
416
- // TODO: Add validation for too many requests HTTP 429: https://github.com/TBD54566975/dwn-server/issues/147
759
+ if (method === 'GET' && path === '/registration/terms-of-service'
760
+ && this.#config.termsOfServiceFilePath !== undefined) {
761
+ return new Response(this.registrationManager.getTermsOfService());
762
+ }
417
763
 
418
- if (!req.body.request) {
419
- return res.status(400).json({
420
- ok: false,
421
- status: {
422
- code: 400,
423
- message: "Bad Request: Missing 'request' parameter",
424
- },
425
- });
426
- }
764
+ if (method === 'POST' && path === '/registration'
765
+ && this.#config.registrationStoreUrl !== undefined) {
766
+ const requestBody = await req.json();
767
+ log.info('Registration request received');
427
768
 
428
- // Validate that `request_uri` was NOT provided
429
- if (req.body?.request?.request_uri) {
430
- return res.status(400).json({
431
- ok: false,
432
- status: {
433
- code: 400,
434
- message: "Bad Request: 'request_uri' parameter is not allowed in PAR",
435
- },
436
- });
769
+ try {
770
+ await this.registrationManager.handleRegistrationRequest(requestBody);
771
+ return Response.json({ success: true }, { status: 200 });
772
+ } catch (error) {
773
+ const dwnServerError = error as DwnServerError;
774
+ if (dwnServerError.code !== undefined) {
775
+ return Response.json(dwnServerError, { status: 400 });
776
+ } else {
777
+ log.info('Error handling registration request:', error);
778
+ return Response.json({ success: false }, { status: 500 });
779
+ }
437
780
  }
781
+ }
438
782
 
439
- const result = await this.web5ConnectServer.setWeb5ConnectRequest(req.body.request);
440
- res.status(201).json(result);
441
- });
783
+ return null;
784
+ }
442
785
 
443
- /**
444
- * Endpoint for the Provider to retrieve the Authorization Request from the request_uri
445
- */
446
- this.#api.get('/connect/authorize/:requestId.jwt', async (req, res) => {
447
- log.info(`Retrieving Web5 Connect Request object of ID: ${req.params.requestId}...`);
786
+ // ---------------------------------------------------------------------------
787
+ // Web5 Connect routes
788
+ // ---------------------------------------------------------------------------
448
789
 
449
- // Look up the request object based on the requestId.
450
- const requestObjectJwt = await this.web5ConnectServer.getWeb5ConnectRequest(req.params.requestId);
790
+ async #matchWeb5ConnectRoutes(
791
+ req: Request, path: string, method: string
792
+ ): Promise<Response | null> {
793
+ // POST /connect/par
794
+ if (method === 'POST' && path === '/connect/par') {
795
+ log.info('Storing Pushed Authorization Request (PAR) request...');
796
+ const body = await req.json();
451
797
 
452
- if (!requestObjectJwt) {
453
- res.status(404).json({
798
+ if (!body.request) {
799
+ return Response.json({
454
800
  ok : false,
455
- status : { code: 404, message: 'Not Found' }
456
- });
457
- } else {
458
- res.set('Content-Type', 'application/jwt');
459
- res.send(requestObjectJwt);
801
+ status : { code: 400, message: 'Bad Request: Missing \'request\' parameter' },
802
+ }, { status: 400 });
460
803
  }
461
- });
462
804
 
463
- /**
464
- * Endpoint that the Provider sends the Authorization Response to
465
- */
466
- this.#api.post('/connect/callback', async (req, res) => {
467
- log.info('Storing Identity Provider (wallet) pushed response with ID token...');
805
+ if (body?.request?.request_uri) {
806
+ return Response.json({
807
+ ok : false,
808
+ status : { code: 400, message: 'Bad Request: \'request_uri\' parameter is not allowed in PAR' },
809
+ }, { status: 400 });
810
+ }
468
811
 
469
- // Store the ID token.
470
- const idToken = req.body.id_token;
471
- const state = req.body.state;
812
+ const result = await this.web5ConnectServer.setWeb5ConnectRequest(body.request);
813
+ return Response.json(result, { status: 201 });
814
+ }
472
815
 
473
- if (idToken !== undefined && state != undefined) {
816
+ // GET /connect/authorize/:requestId.jwt
817
+ {
818
+ const match = path.match(/^\/connect\/authorize\/([^/]+)\.jwt$/);
819
+ if (match && method === 'GET') {
820
+ const requestId = match[1];
821
+ log.info(`Retrieving Web5 Connect Request object of ID: ${requestId}...`);
822
+
823
+ const requestObjectJwt = await this.web5ConnectServer.getWeb5ConnectRequest(requestId);
824
+ if (!requestObjectJwt) {
825
+ return Response.json({
826
+ ok : false,
827
+ status : { code: 404, message: 'Not Found' },
828
+ }, { status: 404 });
829
+ } else {
830
+ const body = typeof requestObjectJwt === 'string'
831
+ ? requestObjectJwt
832
+ : JSON.stringify(requestObjectJwt);
833
+ return new Response(body, {
834
+ headers: { 'content-type': 'application/jwt' },
835
+ });
836
+ }
837
+ }
838
+ }
474
839
 
475
- await this.web5ConnectServer.setWeb5ConnectResponse(state, idToken);
840
+ // POST /connect/callback
841
+ if (method === 'POST' && path === '/connect/callback') {
842
+ log.info('Storing Identity Provider (wallet) pushed response with ID token...');
843
+ const body = await req.json();
844
+ const idToken = body.id_token;
845
+ const state = body.state;
476
846
 
477
- res.status(201).json({
847
+ if (idToken !== undefined && state != undefined) {
848
+ await this.web5ConnectServer.setWeb5ConnectResponse(state, idToken);
849
+ return Response.json({
478
850
  ok : true,
479
- status : { code: 201, message: 'Created' }
480
- });
481
-
851
+ status : { code: 201, message: 'Created' },
852
+ }, { status: 201 });
482
853
  } else {
483
- res.status(400).json({
854
+ return Response.json({
484
855
  ok : false,
485
- status : { code: 400, message: 'Bad Request' }
486
- });
856
+ status : { code: 400, message: 'Bad Request' },
857
+ }, { status: 400 });
487
858
  }
488
- });
489
-
490
- /**
491
- * Endpoint for the connecting Client to retrieve the Authorization Response
492
- */
493
- this.#api.get('/connect/token/:state.jwt', async (req, res) => {
494
- log.info(`Retrieving ID token for state: ${req.params.state}...`);
495
-
496
- // Look up the ID token.
497
- const idToken = await this.web5ConnectServer.getWeb5ConnectResponse(req.params.state);
498
-
499
- if (!idToken) {
500
- res.status(404).json({
501
- ok : false,
502
- status : { code: 404, message: 'Not Found' }
503
- });
504
- } else {
505
- res.set('Content-Type', 'application/jwt');
506
- res.send(idToken);
507
- }
508
- });
509
- }
510
-
511
- /**
512
- * Starts the HTTP API endpoint on the given port.
513
- * @returns The HTTP server instance.
514
- */
515
- async start(port: number): Promise<void> {
516
- // promisify http.Server.listen() and await on it
517
- await new Promise<void>((resolve) => {
518
- this.#server.listen(port, () => {
519
- resolve();
520
- });
521
- });
522
- }
859
+ }
523
860
 
524
- /**
525
- * Stops the HTTP API endpoint.
526
- */
527
- async close(): Promise<void> {
528
- // promisify http.Server.close() and await on it
529
- await new Promise<void>((resolve, reject) => {
530
- this.#server.close((err?: Error) => {
531
- if (err) {
532
- reject(err);
861
+ // GET /connect/token/:state.jwt
862
+ {
863
+ const match = path.match(/^\/connect\/token\/([^/]+)\.jwt$/);
864
+ if (match && method === 'GET') {
865
+ const state = match[1];
866
+ log.info(`Retrieving ID token for state: ${state}...`);
867
+
868
+ const idToken = await this.web5ConnectServer.getWeb5ConnectResponse(state);
869
+ if (!idToken) {
870
+ return Response.json({
871
+ ok : false,
872
+ status : { code: 404, message: 'Not Found' },
873
+ }, { status: 404 });
533
874
  } else {
534
- resolve();
875
+ const body = typeof idToken === 'string' ? idToken : JSON.stringify(idToken);
876
+ return new Response(body, {
877
+ headers: { 'content-type': 'application/jwt' },
878
+ });
535
879
  }
536
- });
537
- });
880
+ }
881
+ }
538
882
 
539
- this.server.closeAllConnections();
883
+ return null;
540
884
  }
541
885
  }