@enbox/dwn-server 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/LICENSE +3 -2
  2. package/README.md +115 -215
  3. package/dist/esm/src/admin/activity-log.d.ts +44 -0
  4. package/dist/esm/src/admin/activity-log.d.ts.map +1 -0
  5. package/dist/esm/src/admin/activity-log.js +85 -0
  6. package/dist/esm/src/admin/activity-log.js.map +1 -0
  7. package/dist/esm/src/admin/admin-api.d.ts +61 -0
  8. package/dist/esm/src/admin/admin-api.d.ts.map +1 -0
  9. package/dist/esm/src/admin/admin-api.js +1047 -0
  10. package/dist/esm/src/admin/admin-api.js.map +1 -0
  11. package/dist/esm/src/admin/admin-auth.d.ts +9 -0
  12. package/dist/esm/src/admin/admin-auth.d.ts.map +1 -0
  13. package/dist/esm/src/admin/admin-auth.js +45 -0
  14. package/dist/esm/src/admin/admin-auth.js.map +1 -0
  15. package/dist/esm/src/admin/admin-store.d.ts +111 -0
  16. package/dist/esm/src/admin/admin-store.d.ts.map +1 -0
  17. package/dist/esm/src/admin/admin-store.js +376 -0
  18. package/dist/esm/src/admin/admin-store.js.map +1 -0
  19. package/dist/esm/src/admin/audit-log.d.ts +94 -0
  20. package/dist/esm/src/admin/audit-log.d.ts.map +1 -0
  21. package/dist/esm/src/admin/audit-log.js +220 -0
  22. package/dist/esm/src/admin/audit-log.js.map +1 -0
  23. package/dist/esm/src/admin/index.d.ts +10 -0
  24. package/dist/esm/src/admin/index.d.ts.map +1 -0
  25. package/dist/esm/src/admin/index.js +7 -0
  26. package/dist/esm/src/admin/index.js.map +1 -0
  27. package/dist/esm/src/admin/types.d.ts +306 -0
  28. package/dist/esm/src/admin/types.d.ts.map +1 -0
  29. package/dist/esm/src/admin/types.js +2 -0
  30. package/dist/esm/src/admin/types.js.map +1 -0
  31. package/dist/esm/src/admin/webhook-manager.d.ts +55 -0
  32. package/dist/esm/src/admin/webhook-manager.d.ts.map +1 -0
  33. package/dist/esm/src/admin/webhook-manager.js +184 -0
  34. package/dist/esm/src/admin/webhook-manager.js.map +1 -0
  35. package/dist/esm/src/config.d.ts +124 -9
  36. package/dist/esm/src/config.d.ts.map +1 -1
  37. package/dist/esm/src/config.js +155 -13
  38. package/dist/esm/src/config.js.map +1 -1
  39. package/dist/esm/src/connection/connection-manager.d.ts +32 -9
  40. package/dist/esm/src/connection/connection-manager.d.ts.map +1 -1
  41. package/dist/esm/src/connection/connection-manager.js +38 -5
  42. package/dist/esm/src/connection/connection-manager.js.map +1 -1
  43. package/dist/esm/src/connection/flow-controller.d.ts +53 -0
  44. package/dist/esm/src/connection/flow-controller.d.ts.map +1 -0
  45. package/dist/esm/src/connection/flow-controller.js +101 -0
  46. package/dist/esm/src/connection/flow-controller.js.map +1 -0
  47. package/dist/esm/src/connection/socket-connection.d.ts +54 -18
  48. package/dist/esm/src/connection/socket-connection.d.ts.map +1 -1
  49. package/dist/esm/src/connection/socket-connection.js +102 -40
  50. package/dist/esm/src/connection/socket-connection.js.map +1 -1
  51. package/dist/esm/src/delivery-service.d.ts +43 -0
  52. package/dist/esm/src/delivery-service.d.ts.map +1 -0
  53. package/dist/esm/src/delivery-service.js +574 -0
  54. package/dist/esm/src/delivery-service.js.map +1 -0
  55. package/dist/esm/src/dwn-error.d.ts +10 -1
  56. package/dist/esm/src/dwn-error.d.ts.map +1 -1
  57. package/dist/esm/src/dwn-error.js +9 -0
  58. package/dist/esm/src/dwn-error.js.map +1 -1
  59. package/dist/esm/src/dwn-server.d.ts +13 -6
  60. package/dist/esm/src/dwn-server.d.ts.map +1 -1
  61. package/dist/esm/src/dwn-server.js +199 -24
  62. package/dist/esm/src/dwn-server.js.map +1 -1
  63. package/dist/esm/src/http-api.d.ts +28 -13
  64. package/dist/esm/src/http-api.d.ts.map +1 -1
  65. package/dist/esm/src/http-api.js +649 -374
  66. package/dist/esm/src/http-api.js.map +1 -1
  67. package/dist/esm/src/index.d.ts +6 -2
  68. package/dist/esm/src/index.d.ts.map +1 -1
  69. package/dist/esm/src/index.js +4 -1
  70. package/dist/esm/src/index.js.map +1 -1
  71. package/dist/esm/src/json-rpc-api.js +2 -1
  72. package/dist/esm/src/json-rpc-api.js.map +1 -1
  73. package/dist/esm/src/json-rpc-handlers/dwn/process-message.d.ts.map +1 -1
  74. package/dist/esm/src/json-rpc-handlers/dwn/process-message.js +109 -7
  75. package/dist/esm/src/json-rpc-handlers/dwn/process-message.js.map +1 -1
  76. package/dist/esm/src/json-rpc-handlers/subscription/ack.d.ts +20 -0
  77. package/dist/esm/src/json-rpc-handlers/subscription/ack.d.ts.map +1 -0
  78. package/dist/esm/src/json-rpc-handlers/subscription/ack.js +41 -0
  79. package/dist/esm/src/json-rpc-handlers/subscription/ack.js.map +1 -0
  80. package/dist/esm/src/json-rpc-handlers/subscription/close.d.ts.map +1 -1
  81. package/dist/esm/src/json-rpc-handlers/subscription/close.js +1 -1
  82. package/dist/esm/src/json-rpc-handlers/subscription/close.js.map +1 -1
  83. package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts +1 -0
  84. package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts.map +1 -1
  85. package/dist/esm/src/json-rpc-handlers/subscription/index.js +1 -0
  86. package/dist/esm/src/json-rpc-handlers/subscription/index.js.map +1 -1
  87. package/dist/esm/src/lib/json-rpc-router.d.ts +25 -8
  88. package/dist/esm/src/lib/json-rpc-router.d.ts.map +1 -1
  89. package/dist/esm/src/lib/json-rpc-router.js.map +1 -1
  90. package/dist/esm/src/lib/sql-utils.d.ts +6 -0
  91. package/dist/esm/src/lib/sql-utils.d.ts.map +1 -0
  92. package/dist/esm/src/lib/sql-utils.js +8 -0
  93. package/dist/esm/src/lib/sql-utils.js.map +1 -0
  94. package/dist/esm/src/main.js +0 -6
  95. package/dist/esm/src/main.js.map +1 -1
  96. package/dist/esm/src/message-processed-hook.d.ts +35 -0
  97. package/dist/esm/src/message-processed-hook.d.ts.map +1 -0
  98. package/dist/esm/src/message-processed-hook.js +2 -0
  99. package/dist/esm/src/message-processed-hook.js.map +1 -0
  100. package/dist/esm/src/metrics.d.ts +14 -2
  101. package/dist/esm/src/metrics.d.ts.map +1 -1
  102. package/dist/esm/src/metrics.js +41 -1
  103. package/dist/esm/src/metrics.js.map +1 -1
  104. package/dist/esm/src/plugins/event-log-nats.d.ts +25 -0
  105. package/dist/esm/src/plugins/event-log-nats.d.ts.map +1 -0
  106. package/dist/esm/src/plugins/event-log-nats.js +379 -0
  107. package/dist/esm/src/plugins/event-log-nats.js.map +1 -0
  108. package/dist/esm/src/rate-limiter.d.ts +60 -0
  109. package/dist/esm/src/rate-limiter.d.ts.map +1 -0
  110. package/dist/esm/src/rate-limiter.js +116 -0
  111. package/dist/esm/src/rate-limiter.js.map +1 -0
  112. package/dist/esm/src/registration/jwt-provider-auth-plugin.d.ts +53 -0
  113. package/dist/esm/src/registration/jwt-provider-auth-plugin.d.ts.map +1 -0
  114. package/dist/esm/src/registration/jwt-provider-auth-plugin.js +90 -0
  115. package/dist/esm/src/registration/jwt-provider-auth-plugin.js.map +1 -0
  116. package/dist/esm/src/registration/open-auth-handler.d.ts +37 -0
  117. package/dist/esm/src/registration/open-auth-handler.d.ts.map +1 -0
  118. package/dist/esm/src/registration/open-auth-handler.js +214 -0
  119. package/dist/esm/src/registration/open-auth-handler.js.map +1 -0
  120. package/dist/esm/src/registration/proof-of-work-manager.d.ts +1 -1
  121. package/dist/esm/src/registration/proof-of-work-manager.d.ts.map +1 -1
  122. package/dist/esm/src/registration/proof-of-work-manager.js +3 -3
  123. package/dist/esm/src/registration/proof-of-work-manager.js.map +1 -1
  124. package/dist/esm/src/registration/provider-auth-plugin.d.ts +46 -0
  125. package/dist/esm/src/registration/provider-auth-plugin.d.ts.map +1 -0
  126. package/dist/esm/src/registration/provider-auth-plugin.js +29 -0
  127. package/dist/esm/src/registration/provider-auth-plugin.js.map +1 -0
  128. package/dist/esm/src/registration/registration-manager.d.ts +28 -5
  129. package/dist/esm/src/registration/registration-manager.d.ts.map +1 -1
  130. package/dist/esm/src/registration/registration-manager.js +83 -12
  131. package/dist/esm/src/registration/registration-manager.js.map +1 -1
  132. package/dist/esm/src/registration/registration-store.d.ts +83 -3
  133. package/dist/esm/src/registration/registration-store.d.ts.map +1 -1
  134. package/dist/esm/src/registration/registration-store.js +248 -11
  135. package/dist/esm/src/registration/registration-store.js.map +1 -1
  136. package/dist/esm/src/storage.d.ts +5 -5
  137. package/dist/esm/src/storage.d.ts.map +1 -1
  138. package/dist/esm/src/storage.js +105 -24
  139. package/dist/esm/src/storage.js.map +1 -1
  140. package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts.map +1 -1
  141. package/dist/esm/src/web5-connect/sql-ttl-cache.js +11 -3
  142. package/dist/esm/src/web5-connect/sql-ttl-cache.js.map +1 -1
  143. package/dist/esm/src/web5-connect/web5-connect-server.d.ts.map +1 -1
  144. package/dist/esm/src/web5-connect/web5-connect-server.js +2 -2
  145. package/dist/esm/src/web5-connect/web5-connect-server.js.map +1 -1
  146. package/dist/esm/src/ws-api.d.ts +18 -4
  147. package/dist/esm/src/ws-api.d.ts.map +1 -1
  148. package/dist/esm/src/ws-api.js +12 -16
  149. package/dist/esm/src/ws-api.js.map +1 -1
  150. package/package.json +34 -53
  151. package/src/admin/activity-log.ts +100 -0
  152. package/src/admin/admin-api.ts +1308 -0
  153. package/src/admin/admin-auth.ts +56 -0
  154. package/src/admin/admin-store.ts +515 -0
  155. package/src/admin/audit-log.ts +327 -0
  156. package/src/admin/index.ts +34 -0
  157. package/src/admin/types.ts +352 -0
  158. package/src/admin/webhook-manager.ts +245 -0
  159. package/src/config.ts +190 -22
  160. package/src/connection/connection-manager.ts +67 -17
  161. package/src/connection/flow-controller.ts +117 -0
  162. package/src/connection/socket-connection.ts +144 -67
  163. package/src/delivery-service.ts +740 -0
  164. package/src/dwn-error.ts +11 -2
  165. package/src/dwn-server.ts +254 -39
  166. package/src/http-api.ts +736 -392
  167. package/src/index.ts +13 -2
  168. package/src/json-rpc-api.ts +2 -1
  169. package/src/json-rpc-handlers/dwn/process-message.ts +149 -15
  170. package/src/json-rpc-handlers/subscription/ack.ts +63 -0
  171. package/src/json-rpc-handlers/subscription/close.ts +5 -9
  172. package/src/json-rpc-handlers/subscription/index.ts +1 -0
  173. package/src/lib/json-rpc-router.ts +26 -11
  174. package/src/lib/sql-utils.ts +7 -0
  175. package/src/main.ts +0 -8
  176. package/src/message-processed-hook.ts +33 -0
  177. package/src/metrics.ts +57 -8
  178. package/src/plugins/event-log-nats.ts +466 -0
  179. package/src/process-handlers.ts +5 -5
  180. package/src/rate-limiter.ts +143 -0
  181. package/src/registration/jwt-provider-auth-plugin.ts +119 -0
  182. package/src/registration/open-auth-handler.ts +263 -0
  183. package/src/registration/proof-of-work-manager.ts +11 -10
  184. package/src/registration/provider-auth-plugin.ts +84 -0
  185. package/src/registration/registration-manager.ts +129 -31
  186. package/src/registration/registration-store.ts +332 -22
  187. package/src/storage.ts +136 -40
  188. package/src/web5-connect/sql-ttl-cache.ts +12 -5
  189. package/src/web5-connect/web5-connect-server.ts +9 -8
  190. package/src/ws-api.ts +39 -26
  191. package/dist/cjs/index.js +0 -6811
  192. package/dist/cjs/package.json +0 -1
  193. package/dist/esm/src/json-rpc-socket.d.ts +0 -39
  194. package/dist/esm/src/json-rpc-socket.d.ts.map +0 -1
  195. package/dist/esm/src/json-rpc-socket.js +0 -125
  196. package/dist/esm/src/json-rpc-socket.js.map +0 -1
  197. package/dist/esm/src/lib/http-server-shutdown-handler.d.ts +0 -10
  198. package/dist/esm/src/lib/http-server-shutdown-handler.d.ts.map +0 -1
  199. package/dist/esm/src/lib/http-server-shutdown-handler.js +0 -65
  200. package/dist/esm/src/lib/http-server-shutdown-handler.js.map +0 -1
  201. package/dist/esm/src/lib/json-rpc.d.ts +0 -54
  202. package/dist/esm/src/lib/json-rpc.d.ts.map +0 -1
  203. package/dist/esm/src/lib/json-rpc.js +0 -60
  204. package/dist/esm/src/lib/json-rpc.js.map +0 -1
  205. package/dist/esm/src/registration/proof-of-work-types.d.ts +0 -8
  206. package/dist/esm/src/registration/proof-of-work-types.d.ts.map +0 -1
  207. package/dist/esm/src/registration/proof-of-work-types.js +0 -2
  208. package/dist/esm/src/registration/proof-of-work-types.js.map +0 -1
  209. package/dist/esm/src/registration/registration-types.d.ts +0 -18
  210. package/dist/esm/src/registration/registration-types.d.ts.map +0 -1
  211. package/dist/esm/src/registration/registration-types.js +0 -2
  212. package/dist/esm/src/registration/registration-types.js.map +0 -1
  213. package/dist/esm/tests/common-scenario-validator.d.ts +0 -11
  214. package/dist/esm/tests/common-scenario-validator.d.ts.map +0 -1
  215. package/dist/esm/tests/common-scenario-validator.js +0 -114
  216. package/dist/esm/tests/common-scenario-validator.js.map +0 -1
  217. package/dist/esm/tests/connection/connection-manager.spec.d.ts +0 -2
  218. package/dist/esm/tests/connection/connection-manager.spec.d.ts.map +0 -1
  219. package/dist/esm/tests/connection/connection-manager.spec.js +0 -47
  220. package/dist/esm/tests/connection/connection-manager.spec.js.map +0 -1
  221. package/dist/esm/tests/connection/socket-connection.spec.d.ts +0 -2
  222. package/dist/esm/tests/connection/socket-connection.spec.d.ts.map +0 -1
  223. package/dist/esm/tests/connection/socket-connection.spec.js +0 -125
  224. package/dist/esm/tests/connection/socket-connection.spec.js.map +0 -1
  225. package/dist/esm/tests/cors/http-api.browser.d.ts +0 -2
  226. package/dist/esm/tests/cors/http-api.browser.d.ts.map +0 -1
  227. package/dist/esm/tests/cors/http-api.browser.js +0 -60
  228. package/dist/esm/tests/cors/http-api.browser.js.map +0 -1
  229. package/dist/esm/tests/cors/ping.browser.d.ts +0 -2
  230. package/dist/esm/tests/cors/ping.browser.d.ts.map +0 -1
  231. package/dist/esm/tests/cors/ping.browser.js +0 -7
  232. package/dist/esm/tests/cors/ping.browser.js.map +0 -1
  233. package/dist/esm/tests/dwn-process-message.spec.d.ts +0 -2
  234. package/dist/esm/tests/dwn-process-message.spec.d.ts.map +0 -1
  235. package/dist/esm/tests/dwn-process-message.spec.js +0 -172
  236. package/dist/esm/tests/dwn-process-message.spec.js.map +0 -1
  237. package/dist/esm/tests/dwn-server.spec.d.ts +0 -2
  238. package/dist/esm/tests/dwn-server.spec.d.ts.map +0 -1
  239. package/dist/esm/tests/dwn-server.spec.js +0 -49
  240. package/dist/esm/tests/dwn-server.spec.js.map +0 -1
  241. package/dist/esm/tests/http-api.spec.d.ts +0 -2
  242. package/dist/esm/tests/http-api.spec.d.ts.map +0 -1
  243. package/dist/esm/tests/http-api.spec.js +0 -775
  244. package/dist/esm/tests/http-api.spec.js.map +0 -1
  245. package/dist/esm/tests/json-rpc-socket.spec.d.ts +0 -2
  246. package/dist/esm/tests/json-rpc-socket.spec.d.ts.map +0 -1
  247. package/dist/esm/tests/json-rpc-socket.spec.js +0 -225
  248. package/dist/esm/tests/json-rpc-socket.spec.js.map +0 -1
  249. package/dist/esm/tests/plugins/data-store-sqlite.d.ts +0 -17
  250. package/dist/esm/tests/plugins/data-store-sqlite.d.ts.map +0 -1
  251. package/dist/esm/tests/plugins/data-store-sqlite.js +0 -23
  252. package/dist/esm/tests/plugins/data-store-sqlite.js.map +0 -1
  253. package/dist/esm/tests/plugins/event-log-sqlite.d.ts +0 -17
  254. package/dist/esm/tests/plugins/event-log-sqlite.d.ts.map +0 -1
  255. package/dist/esm/tests/plugins/event-log-sqlite.js +0 -23
  256. package/dist/esm/tests/plugins/event-log-sqlite.js.map +0 -1
  257. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts +0 -17
  258. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts.map +0 -1
  259. package/dist/esm/tests/plugins/event-stream-in-memory.js +0 -21
  260. package/dist/esm/tests/plugins/event-stream-in-memory.js.map +0 -1
  261. package/dist/esm/tests/plugins/message-store-sqlite.d.ts +0 -17
  262. package/dist/esm/tests/plugins/message-store-sqlite.d.ts.map +0 -1
  263. package/dist/esm/tests/plugins/message-store-sqlite.js +0 -23
  264. package/dist/esm/tests/plugins/message-store-sqlite.js.map +0 -1
  265. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts +0 -17
  266. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts.map +0 -1
  267. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js +0 -23
  268. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js.map +0 -1
  269. package/dist/esm/tests/process-handler.spec.d.ts +0 -2
  270. package/dist/esm/tests/process-handler.spec.d.ts.map +0 -1
  271. package/dist/esm/tests/process-handler.spec.js +0 -60
  272. package/dist/esm/tests/process-handler.spec.js.map +0 -1
  273. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts +0 -2
  274. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts.map +0 -1
  275. package/dist/esm/tests/registration/proof-of-work-manager.spec.js +0 -157
  276. package/dist/esm/tests/registration/proof-of-work-manager.spec.js.map +0 -1
  277. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts +0 -2
  278. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts.map +0 -1
  279. package/dist/esm/tests/rpc-subscribe-close.spec.js +0 -81
  280. package/dist/esm/tests/rpc-subscribe-close.spec.js.map +0 -1
  281. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts +0 -2
  282. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts.map +0 -1
  283. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js +0 -73
  284. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js.map +0 -1
  285. package/dist/esm/tests/scenarios/registration.spec.d.ts +0 -2
  286. package/dist/esm/tests/scenarios/registration.spec.d.ts.map +0 -1
  287. package/dist/esm/tests/scenarios/registration.spec.js +0 -507
  288. package/dist/esm/tests/scenarios/registration.spec.js.map +0 -1
  289. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts +0 -2
  290. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts.map +0 -1
  291. package/dist/esm/tests/scenarios/web5-connect.spec.js +0 -137
  292. package/dist/esm/tests/scenarios/web5-connect.spec.js.map +0 -1
  293. package/dist/esm/tests/test-dwn.d.ts +0 -7
  294. package/dist/esm/tests/test-dwn.d.ts.map +0 -1
  295. package/dist/esm/tests/test-dwn.js +0 -34
  296. package/dist/esm/tests/test-dwn.js.map +0 -1
  297. package/dist/esm/tests/utils.d.ts +0 -46
  298. package/dist/esm/tests/utils.d.ts.map +0 -1
  299. package/dist/esm/tests/utils.js +0 -116
  300. package/dist/esm/tests/utils.js.map +0 -1
  301. package/dist/esm/tests/ws-api.spec.d.ts +0 -2
  302. package/dist/esm/tests/ws-api.spec.d.ts.map +0 -1
  303. package/dist/esm/tests/ws-api.spec.js +0 -327
  304. package/dist/esm/tests/ws-api.spec.js.map +0 -1
  305. package/src/json-rpc-socket.ts +0 -155
  306. package/src/lib/http-server-shutdown-handler.ts +0 -79
  307. package/src/lib/json-rpc.ts +0 -126
  308. package/src/registration/proof-of-work-types.ts +0 -7
  309. package/src/registration/registration-types.ts +0 -18
@@ -0,0 +1,56 @@
1
+ import type { DwnServerConfig } from '../config.js';
2
+
3
+ import { timingSafeEqual } from 'crypto';
4
+
5
+ /**
6
+ * Validates the admin bearer token from the `Authorization` header.
7
+ *
8
+ * @returns `null` if authentication succeeds, or a `Response` with the appropriate
9
+ * error status (404 if admin is disabled, 401 if credentials are missing/invalid).
10
+ */
11
+ export function validateAdminAuth(req: Request, config: DwnServerConfig): Response | null {
12
+ const expectedToken = config.adminToken;
13
+
14
+ // If no admin token is configured, the admin API is disabled.
15
+ // Return 404 to avoid revealing the endpoint exists.
16
+ if (!expectedToken) {
17
+ return new Response('Not Found', { status: 404 });
18
+ }
19
+
20
+ const authHeader = req.headers.get('authorization');
21
+ if (!authHeader) {
22
+ return new Response('Unauthorized', { status: 401 });
23
+ }
24
+
25
+ // Expect "Bearer <token>" format.
26
+ if (!authHeader.startsWith('Bearer ')) {
27
+ return new Response('Unauthorized', { status: 401 });
28
+ }
29
+
30
+ const suppliedToken = authHeader.slice('Bearer '.length);
31
+
32
+ if (!constantTimeEquals(expectedToken, suppliedToken)) {
33
+ return new Response('Unauthorized', { status: 401 });
34
+ }
35
+
36
+ return null; // auth passed
37
+ }
38
+
39
+ /**
40
+ * Compares two strings in constant time to prevent timing attacks.
41
+ * If the strings differ in length, the comparison still takes constant time
42
+ * relative to the expected token length.
43
+ */
44
+ function constantTimeEquals(expected: string, supplied: string): boolean {
45
+ const expectedBuf = Buffer.from(expected, 'utf-8');
46
+ const suppliedBuf = Buffer.from(supplied, 'utf-8');
47
+
48
+ // If lengths differ, we still perform a comparison against the expected buffer
49
+ // to avoid leaking length information through timing.
50
+ if (expectedBuf.length !== suppliedBuf.length) {
51
+ timingSafeEqual(expectedBuf, expectedBuf);
52
+ return false;
53
+ }
54
+
55
+ return timingSafeEqual(expectedBuf, suppliedBuf);
56
+ }
@@ -0,0 +1,515 @@
1
+ import type { Dialect } from '@enbox/dwn-sql-store';
2
+ import type { AdminMessageSummary, AdminProtocolSummary, GlobalStats, TenantExport, TenantStats } from './types.js';
3
+
4
+ import { Kysely } from 'kysely';
5
+
6
+ import { escapeLikeWildcards } from '../lib/sql-utils.js';
7
+ import { getDialectFromUrl } from '../storage.js';
8
+
9
+ /**
10
+ * Provides cross-tenant SQL queries for admin operations.
11
+ * The DWN SDK's store interfaces are always tenant-scoped — this store
12
+ * runs direct SQL for operations like listing tenants and computing aggregates.
13
+ */
14
+ export class AdminStore {
15
+ private db: Kysely<AdminDatabase>;
16
+ private cachedGlobalStats: GlobalStats | undefined;
17
+ private cachedGlobalStatsTimestamp = 0;
18
+ private cacheTtlMs: number;
19
+
20
+ private constructor(dialect: Dialect, cacheTtlMs: number) {
21
+ this.db = new Kysely<AdminDatabase>({ dialect });
22
+ this.cacheTtlMs = cacheTtlMs;
23
+ }
24
+
25
+ /**
26
+ * Creates an `AdminStore` from a storage URL string.
27
+ * @param storageUrl - A SQL connection URL (sqlite://, postgres://, mysql://).
28
+ * @param cacheTtlMs - TTL for cached global stats in milliseconds. Default: 60_000.
29
+ * @returns An `AdminStore` instance, or `undefined` if the URL uses a non-SQL backend.
30
+ */
31
+ public static create(storageUrl: string, cacheTtlMs = 60_000): AdminStore | undefined {
32
+ // LevelDB and file-path plugins cannot support cross-tenant queries.
33
+ const isFilePath = storageUrl.startsWith('/') || storageUrl.startsWith('./') || storageUrl.startsWith('../');
34
+ if (isFilePath || storageUrl.startsWith('level://')) {
35
+ return undefined;
36
+ }
37
+
38
+ try {
39
+ const dialect = getDialectFromUrl(new URL(storageUrl));
40
+ return new AdminStore(dialect, cacheTtlMs);
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Creates an `AdminStore` from an already-initialized Kysely dialect.
48
+ * Used when the admin store should share the same database connection as the DWN stores
49
+ * (e.g., in-memory SQLite for tests).
50
+ */
51
+ public static createFromDialect(dialect: Dialect, cacheTtlMs = 60_000): AdminStore {
52
+ return new AdminStore(dialect, cacheTtlMs);
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Tenant discovery
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Returns `true` if the DWN SQL tables exist (i.e. the admin store shares the same DB as the DWN).
61
+ */
62
+ public async isAvailable(): Promise<boolean> {
63
+ try {
64
+ await this.db
65
+ .selectFrom('messageStoreMessages')
66
+ .select(this.db.fn.countAll<number>().as('count'))
67
+ .limit(1)
68
+ .execute();
69
+ return true;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Returns a paginated list of distinct tenant DIDs from the message store.
77
+ *
78
+ * @see https://github.com/enboxorg/enbox/issues/390
79
+ */
80
+ public async getDistinctTenants(options?: {
81
+ cursor? : string;
82
+ limit? : number;
83
+ search? : string;
84
+ }): Promise<{ tenants: string[]; cursor?: string }> {
85
+ const limit = options?.limit ?? 20;
86
+
87
+ let query = this.db
88
+ .selectFrom('messageStoreMessages')
89
+ .select(this.db.fn.agg<string>('distinct', ['tenant']).as('tenant'))
90
+ .orderBy('tenant', 'asc')
91
+ .limit(limit + 1); // fetch one extra to detect next page
92
+
93
+ if (options?.cursor) {
94
+ query = query.where('tenant', '>', options.cursor);
95
+ }
96
+
97
+ if (options?.search) {
98
+ query = query.where('tenant', 'like', `%${escapeLikeWildcards(options.search)}%`);
99
+ }
100
+
101
+ const results = await query.execute();
102
+ const tenants = results.map((r: { tenant: string }): string => r.tenant);
103
+
104
+ let cursor: string | undefined;
105
+ if (tenants.length > limit) {
106
+ tenants.pop();
107
+ cursor = tenants[tenants.length - 1];
108
+ }
109
+
110
+ return { tenants, cursor };
111
+ }
112
+
113
+ /**
114
+ * Returns the total count of distinct tenants.
115
+ */
116
+ public async getTenantCount(): Promise<number> {
117
+ const result = await this.db
118
+ .selectFrom('messageStoreMessages')
119
+ .select(this.db.fn.countAll<number>().as('count'))
120
+ .select('tenant')
121
+ .groupBy('tenant')
122
+ .execute();
123
+
124
+ return result.length;
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Per-tenant aggregates
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /**
132
+ * Returns aggregate statistics for a single tenant.
133
+ */
134
+ public async getTenantStats(did: string): Promise<TenantStats> {
135
+ const [messageCount, dataStorageBytes, protocols] = await Promise.all([
136
+ this.getTenantMessageCount(did),
137
+ this.getTenantStorageSize(did),
138
+ this.getTenantProtocols(did),
139
+ ]);
140
+
141
+ return {
142
+ messageCount,
143
+ dataStorageBytes,
144
+ protocolCount: protocols.length,
145
+ protocols,
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Returns the total number of messages for a tenant.
151
+ */
152
+ public async getTenantMessageCount(did: string): Promise<number> {
153
+ const result = await this.db
154
+ .selectFrom('messageStoreMessages')
155
+ .select(this.db.fn.countAll<number>().as('count'))
156
+ .where('tenant', '=', did)
157
+ .executeTakeFirstOrThrow();
158
+
159
+ return Number(result.count);
160
+ }
161
+
162
+ /**
163
+ * Returns the total data storage in bytes for a tenant.
164
+ */
165
+ public async getTenantStorageSize(did: string): Promise<number> {
166
+ const result = await this.db
167
+ .selectFrom('dataRefs')
168
+ .select(this.db.fn.sum<number>('dataSize').as('totalBytes'))
169
+ .where('tenant', '=', did)
170
+ .executeTakeFirstOrThrow();
171
+
172
+ return Number(result.totalBytes) || 0;
173
+ }
174
+
175
+ /**
176
+ * Returns the list of distinct protocols used by a tenant.
177
+ */
178
+ public async getTenantProtocols(did: string): Promise<string[]> {
179
+ const results = await this.db
180
+ .selectFrom('messageStoreMessages')
181
+ .select('protocol')
182
+ .distinct()
183
+ .where('tenant', '=', did)
184
+ .where('protocol', 'is not', null)
185
+ .execute();
186
+
187
+ return results.map((r: { protocol: string | null }): string => r.protocol!);
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Global aggregates (cached)
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Returns global server statistics. Results are cached with the configured TTL.
196
+ * Pass `refresh: true` to force recalculation.
197
+ */
198
+ public async getGlobalStats(options?: { refresh?: boolean }): Promise<GlobalStats> {
199
+ const now = Date.now();
200
+ if (
201
+ !options?.refresh &&
202
+ this.cachedGlobalStats !== undefined &&
203
+ (now - this.cachedGlobalStatsTimestamp) < this.cacheTtlMs
204
+ ) {
205
+ return this.cachedGlobalStats;
206
+ }
207
+
208
+ const [tenantCount, messageStats, dataStats, protocolStats] = await Promise.all([
209
+ this.getTenantCount(),
210
+ this.db
211
+ .selectFrom('messageStoreMessages')
212
+ .select(this.db.fn.countAll<number>().as('count'))
213
+ .executeTakeFirstOrThrow(),
214
+ this.db
215
+ .selectFrom('dataRefs')
216
+ .select(this.db.fn.sum<number>('dataSize').as('totalBytes'))
217
+ .executeTakeFirstOrThrow(),
218
+ this.db
219
+ .selectFrom('messageStoreMessages')
220
+ .select(
221
+ this.db.fn.count<number>('protocol').distinct().as('count')
222
+ )
223
+ .where('protocol', 'is not', null)
224
+ .executeTakeFirstOrThrow(),
225
+ ]);
226
+
227
+ this.cachedGlobalStats = {
228
+ tenantCount,
229
+ totalMessages : Number(messageStats.count),
230
+ totalDataBytes : Number(dataStats.totalBytes) || 0,
231
+ totalProtocols : Number(protocolStats.count),
232
+ };
233
+ this.cachedGlobalStatsTimestamp = now;
234
+
235
+ return this.cachedGlobalStats;
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Tenant data purge
240
+ // ---------------------------------------------------------------------------
241
+
242
+ /**
243
+ * Deletes all DWN data for a tenant from every SQL table.
244
+ * @returns The number of messages deleted.
245
+ */
246
+ public async purgeTenantData(did: string): Promise<number> {
247
+ // Delete messages (cascades to tags via FK).
248
+ const messageResult = await this.db
249
+ .deleteFrom('messageStoreMessages')
250
+ .where('tenant', '=', did)
251
+ .executeTakeFirstOrThrow();
252
+
253
+ // Delete data refs for this tenant and garbage-collect orphaned blocks.
254
+ const tenantRefs = await this.db
255
+ .selectFrom('dataRefs')
256
+ .select('dataCid')
257
+ .where('tenant', '=', did)
258
+ .execute();
259
+
260
+ await this.db
261
+ .deleteFrom('dataRefs')
262
+ .where('tenant', '=', did)
263
+ .execute();
264
+
265
+ // GC: delete blocks for dataCids that no longer have any refs.
266
+ for (const ref of tenantRefs) {
267
+ const remaining = await this.db
268
+ .selectFrom('dataRefs')
269
+ .select('dataCid')
270
+ .where('dataCid', '=', ref.dataCid)
271
+ .executeTakeFirst();
272
+
273
+ if (!remaining) {
274
+ await this.db
275
+ .deleteFrom('dataBlocks')
276
+ .where('rootDataCid', '=', ref.dataCid)
277
+ .execute();
278
+ }
279
+ }
280
+
281
+ // Delete state index entries.
282
+ await this.db.deleteFrom('stateIndexNodes').where('tenant', '=', did).execute();
283
+ await this.db.deleteFrom('stateIndexRoots').where('tenant', '=', did).execute();
284
+ await this.db.deleteFrom('stateIndexMeta').where('tenant', '=', did).execute();
285
+
286
+ // Invalidate cache.
287
+ this.cachedGlobalStats = undefined;
288
+
289
+ return Number(messageResult.numDeletedRows);
290
+ }
291
+
292
+ /**
293
+ * Returns the number of suspended tenants from the registration table.
294
+ * Returns 0 if the registration table doesn't exist.
295
+ */
296
+ public async getSuspendedTenantCount(): Promise<number> {
297
+ try {
298
+ const result = await this.db
299
+ .selectFrom('registeredTenants' as any)
300
+ .select(this.db.fn.countAll<number>().as('count'))
301
+ .where('suspended' as any, '=', true)
302
+ .executeTakeFirstOrThrow();
303
+
304
+ return Number(result.count);
305
+ } catch {
306
+ return 0;
307
+ }
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // Tenant data browser
312
+ // ---------------------------------------------------------------------------
313
+
314
+ /**
315
+ * Returns paginated message metadata for a single tenant.
316
+ * Only returns metadata columns — never returns `encodedMessageBytes` (raw message content).
317
+ */
318
+ public async getTenantMessages(did: string, options?: {
319
+ interface? : string;
320
+ method? : string;
321
+ protocol? : string;
322
+ limit? : number;
323
+ cursor? : number;
324
+ }): Promise<{ messages: AdminMessageSummary[]; cursor?: number }> {
325
+ const limit = Math.min(options?.limit ?? 20, 100);
326
+
327
+ let query = this.db
328
+ .selectFrom('messageStoreMessages')
329
+ .select([
330
+ 'messageCid', 'interface', 'method', 'protocol', 'protocolPath',
331
+ 'schema', 'dataFormat', 'dataSize', 'dateCreated', 'messageTimestamp', 'id',
332
+ ])
333
+ .where('tenant', '=', did)
334
+ .orderBy('id', 'desc')
335
+ .limit(limit + 1);
336
+
337
+ if (options?.cursor !== undefined) {
338
+ query = query.where('id', '<', options.cursor);
339
+ }
340
+
341
+ if (options?.interface !== undefined) {
342
+ query = query.where('interface', '=', options.interface);
343
+ }
344
+
345
+ if (options?.method !== undefined) {
346
+ query = query.where('method', '=', options.method);
347
+ }
348
+
349
+ if (options?.protocol !== undefined) {
350
+ query = query.where('protocol', '=', options.protocol);
351
+ }
352
+
353
+ const results = await query.execute();
354
+
355
+ const messages: (AdminMessageSummary & { _id: number })[] = results.map((row): AdminMessageSummary & { _id: number } => ({
356
+ _id : Number(row.id),
357
+ messageCid : row.messageCid,
358
+ interface : row.interface,
359
+ method : row.method,
360
+ protocol : row.protocol,
361
+ protocolPath : row.protocolPath,
362
+ schema : row.schema,
363
+ dataFormat : row.dataFormat,
364
+ dataSize : row.dataSize !== null ? Number(row.dataSize) : null,
365
+ dateCreated : row.dateCreated,
366
+ messageTimestamp : row.messageTimestamp,
367
+ }));
368
+
369
+ let cursor: number | undefined;
370
+ if (messages.length > limit) {
371
+ messages.pop();
372
+ cursor = messages[messages.length - 1]._id;
373
+ }
374
+
375
+ // Strip internal _id before returning.
376
+ const cleaned: AdminMessageSummary[] = messages.map(({ _id, ...rest }): AdminMessageSummary => rest);
377
+
378
+ return { messages: cleaned, cursor };
379
+ }
380
+
381
+ /**
382
+ * Returns per-protocol message counts for a tenant.
383
+ */
384
+ public async getTenantProtocolCounts(did: string): Promise<AdminProtocolSummary[]> {
385
+ const results = await this.db
386
+ .selectFrom('messageStoreMessages')
387
+ .select([
388
+ 'protocol',
389
+ this.db.fn.countAll<number>().as('messageCount'),
390
+ ])
391
+ .where('tenant', '=', did)
392
+ .where('protocol', 'is not', null)
393
+ .groupBy('protocol')
394
+ .orderBy('messageCount', 'desc')
395
+ .execute();
396
+
397
+ return results.map((row): AdminProtocolSummary => ({
398
+ protocol : row.protocol!,
399
+ messageCount : Number(row.messageCount),
400
+ }));
401
+ }
402
+
403
+ /**
404
+ * Exports all message metadata and data records for a tenant as an array.
405
+ * Returns `{ messages, data, metadata }` for streaming/serialization.
406
+ *
407
+ * @see https://github.com/enboxorg/enbox/issues/391
408
+ */
409
+ public async exportTenantData(did: string): Promise<TenantExport> {
410
+ const messages = await this.db
411
+ .selectFrom('messageStoreMessages')
412
+ .select([
413
+ 'messageCid', 'interface', 'method', 'recordId', 'protocol',
414
+ 'protocolPath', 'schema', 'author', 'recipient', 'messageTimestamp',
415
+ 'dateCreated', 'datePublished', 'published', 'dataFormat', 'dataCid', 'dataSize',
416
+ ])
417
+ .where('tenant', '=', did)
418
+ .orderBy('id', 'asc')
419
+ .execute();
420
+
421
+ const dataRecords = await this.db
422
+ .selectFrom('dataRefs')
423
+ .select(['recordId', 'dataCid', 'dataSize'])
424
+ .where('tenant', '=', did)
425
+ .execute();
426
+
427
+ return {
428
+ metadata: {
429
+ tenant : did,
430
+ exportedAt : new Date().toISOString(),
431
+ messageCount : messages.length,
432
+ dataRecordCount : dataRecords.length,
433
+ },
434
+ messages : messages.map((m): Record<string, unknown> => ({ ...m })),
435
+ dataRecords : dataRecords.map((d): Record<string, unknown> => ({
436
+ recordId : d.recordId,
437
+ dataCid : d.dataCid,
438
+ dataSize : Number(d.dataSize),
439
+ })),
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Closes the underlying database connection.
445
+ */
446
+ public async close(): Promise<void> {
447
+ await this.db.destroy();
448
+ }
449
+ }
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // Kysely type definitions for the DWN SQL tables
453
+ // ---------------------------------------------------------------------------
454
+
455
+ interface MessageStoreMessages {
456
+ id : number;
457
+ tenant : string;
458
+ messageCid : string;
459
+ interface : string | null;
460
+ method : string | null;
461
+ recordId : string | null;
462
+ protocol : string | null;
463
+ protocolPath : string | null;
464
+ schema : string | null;
465
+ author : string | null;
466
+ recipient : string | null;
467
+ messageTimestamp : string | null;
468
+ dateCreated : string | null;
469
+ datePublished : string | null;
470
+ published : boolean | null;
471
+ dataFormat : string | null;
472
+ dataCid : string | null;
473
+ dataSize : number | null;
474
+ encodedMessageBytes : Uint8Array;
475
+ }
476
+
477
+ interface DataRefsRow {
478
+ tenant : string;
479
+ recordId : string;
480
+ dataCid : string;
481
+ dataSize : number;
482
+ }
483
+
484
+ interface DataBlocksRow {
485
+ rootDataCid : string;
486
+ blockCid : string;
487
+ data : Uint8Array;
488
+ }
489
+
490
+ interface StateIndexNodes {
491
+ tenant : string;
492
+ scope : string;
493
+ nodeHash : string;
494
+ }
495
+
496
+ interface StateIndexRoots {
497
+ tenant : string;
498
+ scope : string;
499
+ rootHash : string;
500
+ }
501
+
502
+ interface StateIndexMeta {
503
+ tenant : string;
504
+ messageCid : string;
505
+ protocol : string | null;
506
+ }
507
+
508
+ interface AdminDatabase {
509
+ messageStoreMessages : MessageStoreMessages;
510
+ dataRefs : DataRefsRow;
511
+ dataBlocks : DataBlocksRow;
512
+ stateIndexNodes : StateIndexNodes;
513
+ stateIndexRoots : StateIndexRoots;
514
+ stateIndexMeta : StateIndexMeta;
515
+ }