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