@enbox/dwn-server 0.0.1

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 (247) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +353 -0
  3. package/dist/cjs/index.js +6811 -0
  4. package/dist/cjs/package.json +1 -0
  5. package/dist/esm/src/config.d.ts +55 -0
  6. package/dist/esm/src/config.d.ts.map +1 -0
  7. package/dist/esm/src/config.js +60 -0
  8. package/dist/esm/src/config.js.map +1 -0
  9. package/dist/esm/src/connection/connection-manager.d.ts +25 -0
  10. package/dist/esm/src/connection/connection-manager.d.ts.map +1 -0
  11. package/dist/esm/src/connection/connection-manager.js +26 -0
  12. package/dist/esm/src/connection/connection-manager.js.map +1 -0
  13. package/dist/esm/src/connection/socket-connection.d.ts +65 -0
  14. package/dist/esm/src/connection/socket-connection.d.ts.map +1 -0
  15. package/dist/esm/src/connection/socket-connection.js +180 -0
  16. package/dist/esm/src/connection/socket-connection.js.map +1 -0
  17. package/dist/esm/src/dwn-error.d.ts +29 -0
  18. package/dist/esm/src/dwn-error.d.ts.map +1 -0
  19. package/dist/esm/src/dwn-error.js +36 -0
  20. package/dist/esm/src/dwn-error.js.map +1 -0
  21. package/dist/esm/src/dwn-server.d.ts +60 -0
  22. package/dist/esm/src/dwn-server.d.ts.map +1 -0
  23. package/dist/esm/src/dwn-server.js +130 -0
  24. package/dist/esm/src/dwn-server.js.map +1 -0
  25. package/dist/esm/src/http-api.d.ts +26 -0
  26. package/dist/esm/src/http-api.d.ts.map +1 -0
  27. package/dist/esm/src/http-api.js +474 -0
  28. package/dist/esm/src/http-api.js.map +1 -0
  29. package/dist/esm/src/index.d.ts +7 -0
  30. package/dist/esm/src/index.d.ts.map +1 -0
  31. package/dist/esm/src/index.js +6 -0
  32. package/dist/esm/src/index.js.map +1 -0
  33. package/dist/esm/src/json-rpc-api.d.ts +3 -0
  34. package/dist/esm/src/json-rpc-api.d.ts.map +1 -0
  35. package/dist/esm/src/json-rpc-api.js +8 -0
  36. package/dist/esm/src/json-rpc-api.js.map +1 -0
  37. package/dist/esm/src/json-rpc-handlers/dwn/index.d.ts +2 -0
  38. package/dist/esm/src/json-rpc-handlers/dwn/index.d.ts.map +1 -0
  39. package/dist/esm/src/json-rpc-handlers/dwn/index.js +2 -0
  40. package/dist/esm/src/json-rpc-handlers/dwn/index.js.map +1 -0
  41. package/dist/esm/src/json-rpc-handlers/dwn/process-message.d.ts +3 -0
  42. package/dist/esm/src/json-rpc-handlers/dwn/process-message.d.ts.map +1 -0
  43. package/dist/esm/src/json-rpc-handlers/dwn/process-message.js +73 -0
  44. package/dist/esm/src/json-rpc-handlers/dwn/process-message.js.map +1 -0
  45. package/dist/esm/src/json-rpc-handlers/subscription/close.d.ts +10 -0
  46. package/dist/esm/src/json-rpc-handlers/subscription/close.d.ts.map +1 -0
  47. package/dist/esm/src/json-rpc-handlers/subscription/close.js +39 -0
  48. package/dist/esm/src/json-rpc-handlers/subscription/close.js.map +1 -0
  49. package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts +2 -0
  50. package/dist/esm/src/json-rpc-handlers/subscription/index.d.ts.map +1 -0
  51. package/dist/esm/src/json-rpc-handlers/subscription/index.js +2 -0
  52. package/dist/esm/src/json-rpc-handlers/subscription/index.js.map +1 -0
  53. package/dist/esm/src/json-rpc-socket.d.ts +39 -0
  54. package/dist/esm/src/json-rpc-socket.d.ts.map +1 -0
  55. package/dist/esm/src/json-rpc-socket.js +125 -0
  56. package/dist/esm/src/json-rpc-socket.js.map +1 -0
  57. package/dist/esm/src/lib/http-server-shutdown-handler.d.ts +10 -0
  58. package/dist/esm/src/lib/http-server-shutdown-handler.d.ts.map +1 -0
  59. package/dist/esm/src/lib/http-server-shutdown-handler.js +65 -0
  60. package/dist/esm/src/lib/http-server-shutdown-handler.js.map +1 -0
  61. package/dist/esm/src/lib/json-rpc-router.d.ts +30 -0
  62. package/dist/esm/src/lib/json-rpc-router.d.ts.map +1 -0
  63. package/dist/esm/src/lib/json-rpc-router.js +14 -0
  64. package/dist/esm/src/lib/json-rpc-router.js.map +1 -0
  65. package/dist/esm/src/lib/json-rpc.d.ts +54 -0
  66. package/dist/esm/src/lib/json-rpc.d.ts.map +1 -0
  67. package/dist/esm/src/lib/json-rpc.js +60 -0
  68. package/dist/esm/src/lib/json-rpc.js.map +1 -0
  69. package/dist/esm/src/main.d.ts +3 -0
  70. package/dist/esm/src/main.d.ts.map +1 -0
  71. package/dist/esm/src/main.js +11 -0
  72. package/dist/esm/src/main.js.map +1 -0
  73. package/dist/esm/src/metrics.d.ts +4 -0
  74. package/dist/esm/src/metrics.d.ts.map +1 -0
  75. package/dist/esm/src/metrics.js +13 -0
  76. package/dist/esm/src/metrics.js.map +1 -0
  77. package/dist/esm/src/plugin-loader.d.ts +10 -0
  78. package/dist/esm/src/plugin-loader.d.ts.map +1 -0
  79. package/dist/esm/src/plugin-loader.js +19 -0
  80. package/dist/esm/src/plugin-loader.js.map +1 -0
  81. package/dist/esm/src/process-handlers.d.ts +11 -0
  82. package/dist/esm/src/process-handlers.d.ts.map +1 -0
  83. package/dist/esm/src/process-handlers.js +40 -0
  84. package/dist/esm/src/process-handlers.js.map +1 -0
  85. package/dist/esm/src/registration/proof-of-work-manager.d.ts +93 -0
  86. package/dist/esm/src/registration/proof-of-work-manager.d.ts.map +1 -0
  87. package/dist/esm/src/registration/proof-of-work-manager.js +243 -0
  88. package/dist/esm/src/registration/proof-of-work-manager.js.map +1 -0
  89. package/dist/esm/src/registration/proof-of-work-types.d.ts +8 -0
  90. package/dist/esm/src/registration/proof-of-work-types.d.ts.map +1 -0
  91. package/dist/esm/src/registration/proof-of-work-types.js +2 -0
  92. package/dist/esm/src/registration/proof-of-work-types.js.map +1 -0
  93. package/dist/esm/src/registration/proof-of-work.d.ts +41 -0
  94. package/dist/esm/src/registration/proof-of-work.d.ts.map +1 -0
  95. package/dist/esm/src/registration/proof-of-work.js +69 -0
  96. package/dist/esm/src/registration/proof-of-work.js.map +1 -0
  97. package/dist/esm/src/registration/registration-manager.d.ts +55 -0
  98. package/dist/esm/src/registration/registration-manager.d.ts.map +1 -0
  99. package/dist/esm/src/registration/registration-manager.js +121 -0
  100. package/dist/esm/src/registration/registration-manager.js.map +1 -0
  101. package/dist/esm/src/registration/registration-store.d.ts +24 -0
  102. package/dist/esm/src/registration/registration-store.d.ts.map +1 -0
  103. package/dist/esm/src/registration/registration-store.js +56 -0
  104. package/dist/esm/src/registration/registration-store.js.map +1 -0
  105. package/dist/esm/src/registration/registration-types.d.ts +18 -0
  106. package/dist/esm/src/registration/registration-types.d.ts.map +1 -0
  107. package/dist/esm/src/registration/registration-types.js +2 -0
  108. package/dist/esm/src/registration/registration-types.js.map +1 -0
  109. package/dist/esm/src/storage.d.ts +24 -0
  110. package/dist/esm/src/storage.d.ts.map +1 -0
  111. package/dist/esm/src/storage.js +146 -0
  112. package/dist/esm/src/storage.js.map +1 -0
  113. package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts +39 -0
  114. package/dist/esm/src/web5-connect/sql-ttl-cache.d.ts.map +1 -0
  115. package/dist/esm/src/web5-connect/sql-ttl-cache.js +106 -0
  116. package/dist/esm/src/web5-connect/sql-ttl-cache.js.map +1 -0
  117. package/dist/esm/src/web5-connect/web5-connect-server.d.ts +58 -0
  118. package/dist/esm/src/web5-connect/web5-connect-server.d.ts.map +1 -0
  119. package/dist/esm/src/web5-connect/web5-connect-server.js +77 -0
  120. package/dist/esm/src/web5-connect/web5-connect-server.js.map +1 -0
  121. package/dist/esm/src/ws-api.d.ts +13 -0
  122. package/dist/esm/src/ws-api.d.ts.map +1 -0
  123. package/dist/esm/src/ws-api.js +31 -0
  124. package/dist/esm/src/ws-api.js.map +1 -0
  125. package/dist/esm/tests/common-scenario-validator.d.ts +11 -0
  126. package/dist/esm/tests/common-scenario-validator.d.ts.map +1 -0
  127. package/dist/esm/tests/common-scenario-validator.js +114 -0
  128. package/dist/esm/tests/common-scenario-validator.js.map +1 -0
  129. package/dist/esm/tests/connection/connection-manager.spec.d.ts +2 -0
  130. package/dist/esm/tests/connection/connection-manager.spec.d.ts.map +1 -0
  131. package/dist/esm/tests/connection/connection-manager.spec.js +47 -0
  132. package/dist/esm/tests/connection/connection-manager.spec.js.map +1 -0
  133. package/dist/esm/tests/connection/socket-connection.spec.d.ts +2 -0
  134. package/dist/esm/tests/connection/socket-connection.spec.d.ts.map +1 -0
  135. package/dist/esm/tests/connection/socket-connection.spec.js +125 -0
  136. package/dist/esm/tests/connection/socket-connection.spec.js.map +1 -0
  137. package/dist/esm/tests/cors/http-api.browser.d.ts +2 -0
  138. package/dist/esm/tests/cors/http-api.browser.d.ts.map +1 -0
  139. package/dist/esm/tests/cors/http-api.browser.js +60 -0
  140. package/dist/esm/tests/cors/http-api.browser.js.map +1 -0
  141. package/dist/esm/tests/cors/ping.browser.d.ts +2 -0
  142. package/dist/esm/tests/cors/ping.browser.d.ts.map +1 -0
  143. package/dist/esm/tests/cors/ping.browser.js +7 -0
  144. package/dist/esm/tests/cors/ping.browser.js.map +1 -0
  145. package/dist/esm/tests/dwn-process-message.spec.d.ts +2 -0
  146. package/dist/esm/tests/dwn-process-message.spec.d.ts.map +1 -0
  147. package/dist/esm/tests/dwn-process-message.spec.js +172 -0
  148. package/dist/esm/tests/dwn-process-message.spec.js.map +1 -0
  149. package/dist/esm/tests/dwn-server.spec.d.ts +2 -0
  150. package/dist/esm/tests/dwn-server.spec.d.ts.map +1 -0
  151. package/dist/esm/tests/dwn-server.spec.js +49 -0
  152. package/dist/esm/tests/dwn-server.spec.js.map +1 -0
  153. package/dist/esm/tests/http-api.spec.d.ts +2 -0
  154. package/dist/esm/tests/http-api.spec.d.ts.map +1 -0
  155. package/dist/esm/tests/http-api.spec.js +775 -0
  156. package/dist/esm/tests/http-api.spec.js.map +1 -0
  157. package/dist/esm/tests/json-rpc-socket.spec.d.ts +2 -0
  158. package/dist/esm/tests/json-rpc-socket.spec.d.ts.map +1 -0
  159. package/dist/esm/tests/json-rpc-socket.spec.js +225 -0
  160. package/dist/esm/tests/json-rpc-socket.spec.js.map +1 -0
  161. package/dist/esm/tests/plugins/data-store-sqlite.d.ts +17 -0
  162. package/dist/esm/tests/plugins/data-store-sqlite.d.ts.map +1 -0
  163. package/dist/esm/tests/plugins/data-store-sqlite.js +23 -0
  164. package/dist/esm/tests/plugins/data-store-sqlite.js.map +1 -0
  165. package/dist/esm/tests/plugins/event-log-sqlite.d.ts +17 -0
  166. package/dist/esm/tests/plugins/event-log-sqlite.d.ts.map +1 -0
  167. package/dist/esm/tests/plugins/event-log-sqlite.js +23 -0
  168. package/dist/esm/tests/plugins/event-log-sqlite.js.map +1 -0
  169. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts +17 -0
  170. package/dist/esm/tests/plugins/event-stream-in-memory.d.ts.map +1 -0
  171. package/dist/esm/tests/plugins/event-stream-in-memory.js +21 -0
  172. package/dist/esm/tests/plugins/event-stream-in-memory.js.map +1 -0
  173. package/dist/esm/tests/plugins/message-store-sqlite.d.ts +17 -0
  174. package/dist/esm/tests/plugins/message-store-sqlite.d.ts.map +1 -0
  175. package/dist/esm/tests/plugins/message-store-sqlite.js +23 -0
  176. package/dist/esm/tests/plugins/message-store-sqlite.js.map +1 -0
  177. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts +17 -0
  178. package/dist/esm/tests/plugins/resumable-task-store-sqlite.d.ts.map +1 -0
  179. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js +23 -0
  180. package/dist/esm/tests/plugins/resumable-task-store-sqlite.js.map +1 -0
  181. package/dist/esm/tests/process-handler.spec.d.ts +2 -0
  182. package/dist/esm/tests/process-handler.spec.d.ts.map +1 -0
  183. package/dist/esm/tests/process-handler.spec.js +60 -0
  184. package/dist/esm/tests/process-handler.spec.js.map +1 -0
  185. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts +2 -0
  186. package/dist/esm/tests/registration/proof-of-work-manager.spec.d.ts.map +1 -0
  187. package/dist/esm/tests/registration/proof-of-work-manager.spec.js +157 -0
  188. package/dist/esm/tests/registration/proof-of-work-manager.spec.js.map +1 -0
  189. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts +2 -0
  190. package/dist/esm/tests/rpc-subscribe-close.spec.d.ts.map +1 -0
  191. package/dist/esm/tests/rpc-subscribe-close.spec.js +81 -0
  192. package/dist/esm/tests/rpc-subscribe-close.spec.js.map +1 -0
  193. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts +2 -0
  194. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.d.ts.map +1 -0
  195. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js +73 -0
  196. package/dist/esm/tests/scenarios/dynamic-plugin-loading.spec.js.map +1 -0
  197. package/dist/esm/tests/scenarios/registration.spec.d.ts +2 -0
  198. package/dist/esm/tests/scenarios/registration.spec.d.ts.map +1 -0
  199. package/dist/esm/tests/scenarios/registration.spec.js +507 -0
  200. package/dist/esm/tests/scenarios/registration.spec.js.map +1 -0
  201. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts +2 -0
  202. package/dist/esm/tests/scenarios/web5-connect.spec.d.ts.map +1 -0
  203. package/dist/esm/tests/scenarios/web5-connect.spec.js +137 -0
  204. package/dist/esm/tests/scenarios/web5-connect.spec.js.map +1 -0
  205. package/dist/esm/tests/test-dwn.d.ts +7 -0
  206. package/dist/esm/tests/test-dwn.d.ts.map +1 -0
  207. package/dist/esm/tests/test-dwn.js +34 -0
  208. package/dist/esm/tests/test-dwn.js.map +1 -0
  209. package/dist/esm/tests/utils.d.ts +46 -0
  210. package/dist/esm/tests/utils.d.ts.map +1 -0
  211. package/dist/esm/tests/utils.js +116 -0
  212. package/dist/esm/tests/utils.js.map +1 -0
  213. package/dist/esm/tests/ws-api.spec.d.ts +2 -0
  214. package/dist/esm/tests/ws-api.spec.d.ts.map +1 -0
  215. package/dist/esm/tests/ws-api.spec.js +327 -0
  216. package/dist/esm/tests/ws-api.spec.js.map +1 -0
  217. package/package.json +119 -0
  218. package/src/config.ts +71 -0
  219. package/src/connection/connection-manager.ts +39 -0
  220. package/src/connection/socket-connection.ts +221 -0
  221. package/src/dwn-error.ts +38 -0
  222. package/src/dwn-server.ts +178 -0
  223. package/src/http-api.ts +541 -0
  224. package/src/index.ts +6 -0
  225. package/src/json-rpc-api.ts +11 -0
  226. package/src/json-rpc-handlers/dwn/index.ts +1 -0
  227. package/src/json-rpc-handlers/dwn/process-message.ts +123 -0
  228. package/src/json-rpc-handlers/subscription/close.ts +59 -0
  229. package/src/json-rpc-handlers/subscription/index.ts +1 -0
  230. package/src/json-rpc-socket.ts +155 -0
  231. package/src/lib/http-server-shutdown-handler.ts +79 -0
  232. package/src/lib/json-rpc-router.ts +52 -0
  233. package/src/lib/json-rpc.ts +126 -0
  234. package/src/main.ts +14 -0
  235. package/src/metrics.ts +14 -0
  236. package/src/plugin-loader.ts +17 -0
  237. package/src/process-handlers.ts +65 -0
  238. package/src/registration/proof-of-work-manager.ts +317 -0
  239. package/src/registration/proof-of-work-types.ts +7 -0
  240. package/src/registration/proof-of-work.ts +100 -0
  241. package/src/registration/registration-manager.ts +153 -0
  242. package/src/registration/registration-store.ts +79 -0
  243. package/src/registration/registration-types.ts +18 -0
  244. package/src/storage.ts +213 -0
  245. package/src/web5-connect/sql-ttl-cache.ts +137 -0
  246. package/src/web5-connect/web5-connect-server.ts +122 -0
  247. package/src/ws-api.ts +45 -0
@@ -0,0 +1,541 @@
1
+ import type { RecordsReadReply } from '@enbox/dwn-sdk-js';
2
+ import { type Dwn, DateSort, RecordsRead, RecordsQuery, ProtocolsQuery } from '@enbox/dwn-sdk-js';
3
+
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
+ import log from 'loglevel';
10
+ import { register } from 'prom-client';
11
+ import responseTime from 'response-time';
12
+ import { v4 as uuidv4 } from 'uuid';
13
+
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
+ import { config } from './config.js';
21
+ import { jsonRpcRouter } from './json-rpc-api.js';
22
+ import { Web5ConnectServer } from './web5-connect/web5-connect-server.js';
23
+ import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js';
24
+ import { requestCounter, responseHistogram } from './metrics.js';
25
+ import { Convert } from '@enbox/common';
26
+
27
+
28
+ export class HttpApi {
29
+ #config: DwnServerConfig;
30
+ #packageInfo: { version?: string, sdkVersion?: string, server: string };
31
+ #api: Express;
32
+ #server: http.Server;
33
+ web5ConnectServer: Web5ConnectServer;
34
+ registrationManager: RegistrationManager;
35
+ dwn: Dwn;
36
+
37
+ private constructor() { }
38
+
39
+ public static async create(config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager): Promise<HttpApi> {
40
+ const httpApi = new HttpApi();
41
+
42
+ log.info(config);
43
+
44
+ httpApi.#packageInfo = {
45
+ server: config.serverName,
46
+ };
47
+
48
+ try {
49
+ // We populate the `version` and `sdkVersion` properties from the `package.json` file.
50
+ const packageJson = JSON.parse(readFileSync(config.packageJsonPath).toString());
51
+ httpApi.#packageInfo.version = packageJson.version;
52
+ httpApi.#packageInfo.sdkVersion = packageJson.dependencies ? packageJson.dependencies['@enbox/dwn-sdk-js'] : undefined;
53
+ } catch (error: any) {
54
+ log.info('could not read `package.json` for version info', error);
55
+ }
56
+
57
+ httpApi.#config = config;
58
+ httpApi.#api = express();
59
+ httpApi.#server = http.createServer(httpApi.#api);
60
+ httpApi.dwn = dwn;
61
+
62
+ if (registrationManager !== undefined) {
63
+ httpApi.registrationManager = registrationManager;
64
+ }
65
+
66
+ // create the Web5 Connect Server
67
+ httpApi.web5ConnectServer = await Web5ConnectServer.create({
68
+ baseUrl: config.baseUrl,
69
+ sqlTtlCacheUrl: config.ttlCacheUrl,
70
+ });
71
+
72
+ httpApi.#setupMiddleware();
73
+ httpApi.#setupRoutes();
74
+
75
+ return httpApi;
76
+ }
77
+
78
+ get server(): http.Server {
79
+ return this.#server;
80
+ }
81
+
82
+ get api(): Express {
83
+ return this.#api;
84
+ }
85
+
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)
97
+ .toLowerCase()
98
+ .replace(/[:.]/g, '')
99
+ .replace(/\//g, '_');
100
+
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
+ );
106
+ }
107
+
108
+ /**
109
+ * Configures the HTTP server's request handlers.
110
+ */
111
+ #setupRoutes(): void {
112
+
113
+ const leadTailSlashRegex = /^\/|\/$/;
114
+
115
+ function readReplyHandler(res, reply: RecordsReadReply): any {
116
+ if (reply.status.code === 200) {
117
+ if (reply?.entry?.data) {
118
+ const stream = reply.entry.data;
119
+
120
+ res.setHeader('content-type', reply.entry.recordsWrite.descriptor.dataFormat);
121
+ res.setHeader('dwn-response', JSON.stringify(reply));
122
+
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
+ }
132
+ }
133
+
134
+ this.#api.get('/health', (_req, res) => {
135
+ // return 200 ok
136
+ return res.json({ ok: true });
137
+ });
138
+
139
+ this.#api.get('/metrics', async (req, res) => {
140
+ try {
141
+ res.set('Content-Type', register.contentType);
142
+ res.end(await register.metrics());
143
+ } catch (e) {
144
+ res.status(500).end(e);
145
+ }
146
+ });
147
+
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
+ }
153
+
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
+ }
163
+
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, '');
169
+
170
+ const query = await RecordsQuery.create({
171
+ filter: queryOptions.filter,
172
+ pagination: { limit: 1 },
173
+ dateSort: DateSort.PublishedDescending
174
+ });
175
+
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);
196
+ }
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 {
202
+
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);
225
+ }
226
+ })
227
+
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);
234
+ }
235
+
236
+ this.#api.get('/:did/read/records/:id', recordsReadHandler);
237
+ this.#api.get('/:did/records/:id', recordsReadHandler);
238
+
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
+ });
251
+
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
+ });
276
+
277
+ // should always return a 200 status code with a JSON response
278
+ const reply = await this.dwn.processMessage(req.params.did, recordsQuery.message);
279
+
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);
285
+ }
286
+ });
287
+
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
+ });
293
+
294
+ this.#api.post('/', async (req: Request, res) => {
295
+ const dwnRpcRequestString = req.headers['dwn-request'] as string;
296
+
297
+ if (!dwnRpcRequestString) {
298
+ const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, 'request payload required.');
299
+
300
+ return res.status(400).json(reply);
301
+ }
302
+
303
+ let dwnRpcRequest: JsonRpcRequest;
304
+ try {
305
+ dwnRpcRequest = JSON.parse(dwnRpcRequestString);
306
+ } catch (e) {
307
+ const reply = createJsonRpcErrorResponse(uuidv4(), JsonRpcErrorCodes.BadRequest, e.message);
308
+
309
+ return res.status(400).json(reply);
310
+ }
311
+
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;
316
+
317
+ const requestContext: RequestContext = {
318
+ dwn : this.dwn,
319
+ transport : 'http',
320
+ dataStream : requestDataStream,
321
+ };
322
+ const { jsonRpcResponse, dataStream: responseDataStream } = await jsonRpcRouter.handle(dwnRpcRequest, requestContext as RequestContext);
323
+
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);
329
+ }
330
+
331
+ requestCounter.inc({
332
+ method : dwnRpcRequest.method,
333
+ status : jsonRpcResponse?.result?.reply?.status?.code || 0,
334
+ });
335
+ if (responseDataStream) {
336
+ res.setHeader('content-type', 'application/octet-stream');
337
+ res.setHeader('dwn-response', JSON.stringify(jsonRpcResponse));
338
+
339
+ return responseDataStream.pipe(res);
340
+ } else {
341
+ return res.json(jsonRpcResponse);
342
+ }
343
+ });
344
+
345
+ this.#setupRegistrationRoutes();
346
+
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');
355
+ }
356
+
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,
365
+ });
366
+ });
367
+
368
+ this.#setupWeb5ConnectServerRoutes();
369
+ }
370
+
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);
376
+ });
377
+ }
378
+
379
+ if (this.#config.termsOfServiceFilePath !== undefined) {
380
+ this.#api.get('/registration/terms-of-service', (_req: Request, res: Response) => res.send(this.registrationManager.getTermsOfService()));
381
+ }
382
+
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);
387
+
388
+ try {
389
+ await this.registrationManager.handleRegistrationRequest(requestBody);
390
+ res.status(200).json({ success: true });
391
+ } catch (error) {
392
+ const dwnServerError = error as DwnServerError;
393
+
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
+ }
401
+ });
402
+ }
403
+ }
404
+
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...');
414
+
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
417
+
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
+ }
427
+
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
+ });
437
+ }
438
+
439
+ const result = await this.web5ConnectServer.setWeb5ConnectRequest(req.body.request);
440
+ res.status(201).json(result);
441
+ });
442
+
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}...`);
448
+
449
+ // Look up the request object based on the requestId.
450
+ const requestObjectJwt = await this.web5ConnectServer.getWeb5ConnectRequest(req.params.requestId);
451
+
452
+ if (!requestObjectJwt) {
453
+ res.status(404).json({
454
+ ok : false,
455
+ status : { code: 404, message: 'Not Found' }
456
+ });
457
+ } else {
458
+ res.set('Content-Type', 'application/jwt');
459
+ res.send(requestObjectJwt);
460
+ }
461
+ });
462
+
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...');
468
+
469
+ // Store the ID token.
470
+ const idToken = req.body.id_token;
471
+ const state = req.body.state;
472
+
473
+ if (idToken !== undefined && state != undefined) {
474
+
475
+ await this.web5ConnectServer.setWeb5ConnectResponse(state, idToken);
476
+
477
+ res.status(201).json({
478
+ ok : true,
479
+ status : { code: 201, message: 'Created' }
480
+ });
481
+
482
+ } else {
483
+ res.status(400).json({
484
+ ok : false,
485
+ status : { code: 400, message: 'Bad Request' }
486
+ });
487
+ }
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
+ }
523
+
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);
533
+ } else {
534
+ resolve();
535
+ }
536
+ });
537
+ });
538
+
539
+ this.server.closeAllConnections();
540
+ }
541
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { DwnServerConfig } from './config.js';
2
+ export { DwnServer, DwnServerOptions } from './dwn-server.js';
3
+ export { HttpApi } from './http-api.js';
4
+ export { jsonRpcRouter } from './json-rpc-api.js';
5
+ export { StoreType, BackendTypes, DwnStore } from './storage.js';
6
+ export { WsApi } from './ws-api.js';
@@ -0,0 +1,11 @@
1
+ import { JsonRpcRouter } from './lib/json-rpc-router.js';
2
+
3
+ import { handleDwnProcessMessage } from './json-rpc-handlers/dwn/index.js';
4
+ import { handleSubscriptionsClose } from './json-rpc-handlers/subscription/index.js';
5
+
6
+ export const jsonRpcRouter = new JsonRpcRouter();
7
+
8
+ jsonRpcRouter.on('dwn.processMessage', handleDwnProcessMessage);
9
+ jsonRpcRouter.on('rpc.subscribe.dwn.processMessage', handleDwnProcessMessage);
10
+
11
+ jsonRpcRouter.on('rpc.subscribe.close', handleSubscriptionsClose);
@@ -0,0 +1 @@
1
+ export * from './process-message.js';
@@ -0,0 +1,123 @@
1
+ import type { GenericMessage } from '@enbox/dwn-sdk-js';
2
+ import { DwnInterfaceName, DwnMethodName } from '@enbox/dwn-sdk-js';
3
+
4
+ import type { Readable as IsomorphicReadable } from 'readable-stream';
5
+ import log from 'loglevel';
6
+ import { v4 as uuidv4 } from 'uuid';
7
+
8
+ import type { JsonRpcSubscription } from '../../lib/json-rpc.js';
9
+ import type {
10
+ HandlerResponse,
11
+ JsonRpcHandler,
12
+ } from '../../lib/json-rpc-router.js';
13
+
14
+ import {
15
+ createJsonRpcErrorResponse,
16
+ createJsonRpcSuccessResponse,
17
+ JsonRpcErrorCodes,
18
+ } from '../../lib/json-rpc.js';
19
+
20
+
21
+ export const handleDwnProcessMessage: JsonRpcHandler = async (
22
+ dwnRequest,
23
+ context,
24
+ ) => {
25
+ const { dwn, dataStream, subscriptionRequest, socketConnection, transport } = context;
26
+ const { target, message } = dwnRequest.params as { target: string, message: GenericMessage };
27
+ const requestId = dwnRequest.id ?? uuidv4();
28
+
29
+ try {
30
+ // RecordsWrite is only supported on 'http' to support data stream for large data
31
+ // TODO: https://github.com/TBD54566975/dwn-server/issues/108
32
+ if (
33
+ transport !== 'http' &&
34
+ message.descriptor.interface === DwnInterfaceName.Records &&
35
+ message.descriptor.method === DwnMethodName.Write
36
+ ) {
37
+ const jsonRpcResponse = createJsonRpcErrorResponse(
38
+ requestId,
39
+ JsonRpcErrorCodes.InvalidParams,
40
+ `RecordsWrite is not supported via ${context.transport}`
41
+ )
42
+ return { jsonRpcResponse };
43
+ }
44
+
45
+ // subscribe methods must come with a subscriptionRequest context
46
+ if (message.descriptor.method === DwnMethodName.Subscribe && subscriptionRequest === undefined) {
47
+ const jsonRpcResponse = createJsonRpcErrorResponse(
48
+ requestId,
49
+ JsonRpcErrorCodes.InvalidRequest,
50
+ `subscribe methods must contain a subscriptionRequest context`
51
+ );
52
+ return { jsonRpcResponse };
53
+ }
54
+
55
+ // Subscribe methods are only supported on 'ws' (WebSockets)
56
+ if (transport !== 'ws' && subscriptionRequest !== undefined) {
57
+ const jsonRpcResponse = createJsonRpcErrorResponse(
58
+ requestId,
59
+ JsonRpcErrorCodes.InvalidParams,
60
+ `subscriptions are not supported via ${context.transport}`
61
+ )
62
+ return { jsonRpcResponse };
63
+ }
64
+
65
+ // if this is a subscription request, we first check if the connection has a subscription with this Id
66
+ // we do this ahead of time to prevent opening a subscription on the dwn only to close it after attempting to add it to the subscription manager
67
+ // otherwise the subscription manager would throw an error that the Id is already in use and we would close the open subscription on the DWN.
68
+ if (subscriptionRequest !== undefined && socketConnection?.hasSubscription(subscriptionRequest.id)) {
69
+ const jsonRpcResponse = createJsonRpcErrorResponse(
70
+ requestId,
71
+ JsonRpcErrorCodes.InvalidParams,
72
+ `the subscribe id: ${subscriptionRequest.id} is in use by an active subscription`
73
+ )
74
+ return { jsonRpcResponse };
75
+ }
76
+
77
+ const reply = await dwn.processMessage(target, message, {
78
+ dataStream: dataStream as IsomorphicReadable,
79
+ subscriptionHandler: subscriptionRequest?.subscriptionHandler,
80
+ });
81
+
82
+
83
+ const { entry } = reply;
84
+ // RecordsRead or MessagesRead messages optionally return data as a stream to accommodate large amounts of data
85
+ // we remove the data stream from the reply that will be serialized and return it as a separate property in the response payload.
86
+ let recordDataStream: IsomorphicReadable;
87
+ if (entry !== undefined && entry.data !== undefined) {
88
+ recordDataStream = entry.data;
89
+ delete reply.entry.data; // not serializable via JSON
90
+ }
91
+
92
+ if (subscriptionRequest && reply.subscription) {
93
+ const { close } = reply.subscription;
94
+ // Subscribe messages return a close function to facilitate closing the subscription
95
+ // we add a reference to the close function for this subscription request to the socket connection.
96
+ // this will facilitate closing the subscription later.
97
+ const subscriptionReply: JsonRpcSubscription = {
98
+ id: subscriptionRequest.id,
99
+ close,
100
+ }
101
+ await socketConnection.addSubscription(subscriptionReply);
102
+ delete reply.subscription.close // delete the close method from the reply as it's not JSON serializable and has a held reference.
103
+ }
104
+
105
+ const jsonRpcResponse = createJsonRpcSuccessResponse(requestId, { reply });
106
+ const responsePayload: HandlerResponse = { jsonRpcResponse };
107
+ if (recordDataStream) {
108
+ responsePayload.dataStream = recordDataStream;
109
+ }
110
+
111
+ return responsePayload;
112
+ } catch (error) {
113
+ const jsonRpcResponse = createJsonRpcErrorResponse(
114
+ requestId,
115
+ JsonRpcErrorCodes.InternalError,
116
+ error.message,
117
+ );
118
+
119
+ // log the unhandled error response
120
+ log.error('handleDwnProcessMessage error', jsonRpcResponse, dwnRequest, error);
121
+ return { jsonRpcResponse } as HandlerResponse;
122
+ }
123
+ };