@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.
- package/LICENSE +3 -2
- package/README.md +115 -215
- 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 +124 -9
- package/dist/esm/src/config.d.ts.map +1 -1
- package/dist/esm/src/config.js +155 -13
- package/dist/esm/src/config.js.map +1 -1
- package/dist/esm/src/connection/connection-manager.d.ts +32 -9
- package/dist/esm/src/connection/connection-manager.d.ts.map +1 -1
- package/dist/esm/src/connection/connection-manager.js +38 -5
- 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 +54 -18
- package/dist/esm/src/connection/socket-connection.d.ts.map +1 -1
- package/dist/esm/src/connection/socket-connection.js +102 -40
- 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 +13 -6
- package/dist/esm/src/dwn-server.d.ts.map +1 -1
- package/dist/esm/src/dwn-server.js +199 -24
- package/dist/esm/src/dwn-server.js.map +1 -1
- package/dist/esm/src/http-api.d.ts +28 -13
- package/dist/esm/src/http-api.d.ts.map +1 -1
- package/dist/esm/src/http-api.js +649 -374
- 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 +109 -7
- 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 +25 -8
- 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 +14 -2
- 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/proof-of-work-manager.js +3 -3
- package/dist/esm/src/registration/proof-of-work-manager.js.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 +28 -5
- package/dist/esm/src/registration/registration-manager.d.ts.map +1 -1
- package/dist/esm/src/registration/registration-manager.js +83 -12
- 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 +5 -5
- package/dist/esm/src/storage.d.ts.map +1 -1
- package/dist/esm/src/storage.js +105 -24
- 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 +11 -3
- package/dist/esm/src/web5-connect/sql-ttl-cache.js.map +1 -1
- package/dist/esm/src/web5-connect/web5-connect-server.d.ts.map +1 -1
- package/dist/esm/src/web5-connect/web5-connect-server.js +2 -2
- package/dist/esm/src/web5-connect/web5-connect-server.js.map +1 -1
- package/dist/esm/src/ws-api.d.ts +18 -4
- package/dist/esm/src/ws-api.d.ts.map +1 -1
- package/dist/esm/src/ws-api.js +12 -16
- package/dist/esm/src/ws-api.js.map +1 -1
- package/package.json +34 -53
- 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 +190 -22
- package/src/connection/connection-manager.ts +67 -17
- package/src/connection/flow-controller.ts +117 -0
- package/src/connection/socket-connection.ts +144 -67
- package/src/delivery-service.ts +740 -0
- package/src/dwn-error.ts +11 -2
- package/src/dwn-server.ts +254 -39
- package/src/http-api.ts +736 -392
- package/src/index.ts +13 -2
- package/src/json-rpc-api.ts +2 -1
- package/src/json-rpc-handlers/dwn/process-message.ts +149 -15
- package/src/json-rpc-handlers/subscription/ack.ts +63 -0
- package/src/json-rpc-handlers/subscription/close.ts +5 -9
- package/src/json-rpc-handlers/subscription/index.ts +1 -0
- package/src/lib/json-rpc-router.ts +26 -11
- 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 +57 -8
- package/src/plugins/event-log-nats.ts +466 -0
- package/src/process-handlers.ts +5 -5
- 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 +11 -10
- package/src/registration/provider-auth-plugin.ts +84 -0
- package/src/registration/registration-manager.ts +129 -31
- package/src/registration/registration-store.ts +332 -22
- package/src/storage.ts +136 -40
- package/src/web5-connect/sql-ttl-cache.ts +12 -5
- package/src/web5-connect/web5-connect-server.ts +9 -8
- package/src/ws-api.ts +39 -26
- package/dist/cjs/index.js +0 -6811
- package/dist/cjs/package.json +0 -1
- 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/http-server-shutdown-handler.d.ts +0 -10
- package/dist/esm/src/lib/http-server-shutdown-handler.d.ts.map +0 -1
- package/dist/esm/src/lib/http-server-shutdown-handler.js +0 -65
- package/dist/esm/src/lib/http-server-shutdown-handler.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 -114
- 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 -47
- 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 -125
- 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 -49
- 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 -775
- 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 -225
- 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 -157
- 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 -73
- 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 -507
- 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 -137
- 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 -34
- package/dist/esm/tests/test-dwn.js.map +0 -1
- package/dist/esm/tests/utils.d.ts +0 -46
- package/dist/esm/tests/utils.d.ts.map +0 -1
- package/dist/esm/tests/utils.js +0 -116
- 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 -327
- package/dist/esm/tests/ws-api.spec.js.map +0 -1
- package/src/json-rpc-socket.ts +0 -155
- package/src/lib/http-server-shutdown-handler.ts +0 -79
- 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
|
@@ -0,0 +1,1047 @@
|
|
|
1
|
+
import log from 'loglevel';
|
|
2
|
+
import { validateAdminAuth } from './admin-auth.js';
|
|
3
|
+
import { activeTenants, totalDataBytes, totalMessages, websocketConnections, websocketSubscriptions, } from '../metrics.js';
|
|
4
|
+
/** Parses a string to an integer, returning `defaultValue` when the input is null or non-numeric. */
|
|
5
|
+
function parseIntOrDefault(value, defaultValue) {
|
|
6
|
+
if (value === null) {
|
|
7
|
+
return defaultValue;
|
|
8
|
+
}
|
|
9
|
+
const parsed = parseInt(value, 10);
|
|
10
|
+
return Number.isNaN(parsed) ? defaultValue : parsed;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Handles all `/admin/api/*` routes.
|
|
14
|
+
* Requires bearer token authentication on every request.
|
|
15
|
+
*/
|
|
16
|
+
export class AdminApi {
|
|
17
|
+
#config;
|
|
18
|
+
#dwn;
|
|
19
|
+
#adminStore;
|
|
20
|
+
#registrationManager;
|
|
21
|
+
#registrationStore;
|
|
22
|
+
#connectionManager;
|
|
23
|
+
#activityLog;
|
|
24
|
+
#auditLog;
|
|
25
|
+
#ipRateLimiter;
|
|
26
|
+
#tenantRateLimiter;
|
|
27
|
+
#webhookManager;
|
|
28
|
+
#startTime;
|
|
29
|
+
#packageInfo;
|
|
30
|
+
#metricsInterval;
|
|
31
|
+
/** Tracks last failed-auth audit timestamp per IP to rate-limit logging. */
|
|
32
|
+
#failedAuthLog = new Map();
|
|
33
|
+
/** Minimum interval between audit log entries for the same IP (60 seconds). */
|
|
34
|
+
static #AUTH_AUDIT_INTERVAL_MS = 60_000;
|
|
35
|
+
constructor() {
|
|
36
|
+
this.#startTime = Date.now();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Creates a new `AdminApi` instance.
|
|
40
|
+
*/
|
|
41
|
+
static create(options) {
|
|
42
|
+
const api = new AdminApi();
|
|
43
|
+
api.#config = options.config;
|
|
44
|
+
api.#dwn = options.dwn;
|
|
45
|
+
api.#adminStore = options.adminStore;
|
|
46
|
+
api.#registrationManager = options.registrationManager;
|
|
47
|
+
api.#registrationStore = options.registrationStore;
|
|
48
|
+
api.#connectionManager = options.connectionManager;
|
|
49
|
+
api.#activityLog = options.activityLog;
|
|
50
|
+
api.#auditLog = options.auditLog;
|
|
51
|
+
api.#ipRateLimiter = options.ipRateLimiter;
|
|
52
|
+
api.#tenantRateLimiter = options.tenantRateLimiter;
|
|
53
|
+
api.#webhookManager = options.webhookManager;
|
|
54
|
+
api.#packageInfo = options.packageInfo ?? {};
|
|
55
|
+
return api;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sets the connection manager (wired after WsApi is created).
|
|
59
|
+
*/
|
|
60
|
+
setConnectionManager(connectionManager) {
|
|
61
|
+
this.#connectionManager = connectionManager;
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Route dispatcher
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/**
|
|
67
|
+
* Routes an admin API request. Called by `HttpApi` for paths starting with `/admin/api/`.
|
|
68
|
+
* @returns A `Response` to send to the client.
|
|
69
|
+
*/
|
|
70
|
+
async route(req, url, path, method) {
|
|
71
|
+
// Authenticate every request.
|
|
72
|
+
const authError = validateAdminAuth(req, this.#config);
|
|
73
|
+
if (authError) {
|
|
74
|
+
// Log failed authentication attempts (401 only, not 404 for disabled admin).
|
|
75
|
+
// Rate-limited to one audit entry per IP per 60 seconds.
|
|
76
|
+
// @see https://github.com/enboxorg/enbox/issues/392
|
|
77
|
+
if (authError.status === 401) {
|
|
78
|
+
this.#auditFailedAuth(req, path);
|
|
79
|
+
}
|
|
80
|
+
return authError;
|
|
81
|
+
}
|
|
82
|
+
// Strip the `/admin/api` prefix for cleaner matching.
|
|
83
|
+
const subPath = path.slice('/admin/api'.length);
|
|
84
|
+
try {
|
|
85
|
+
// --- Health ---
|
|
86
|
+
if (method === 'GET' && subPath === '/health') {
|
|
87
|
+
return this.#handleHealth();
|
|
88
|
+
}
|
|
89
|
+
// --- Stats ---
|
|
90
|
+
if (method === 'GET' && subPath === '/stats') {
|
|
91
|
+
return this.#handleStats(url);
|
|
92
|
+
}
|
|
93
|
+
// --- Tenant list ---
|
|
94
|
+
if (method === 'GET' && subPath === '/tenants') {
|
|
95
|
+
return this.#handleTenantList(url);
|
|
96
|
+
}
|
|
97
|
+
// --- Tenant creation ---
|
|
98
|
+
if (method === 'POST' && subPath === '/tenants') {
|
|
99
|
+
return this.#handleTenantCreate(req);
|
|
100
|
+
}
|
|
101
|
+
// --- Tenant detail / suspend / unsuspend / delete ---
|
|
102
|
+
{
|
|
103
|
+
const match = subPath.match(/^\/tenants\/([^/]+)$/);
|
|
104
|
+
if (match) {
|
|
105
|
+
const did = decodeURIComponent(match[1]);
|
|
106
|
+
if (method === 'GET') {
|
|
107
|
+
return this.#handleTenantDetail(did);
|
|
108
|
+
}
|
|
109
|
+
if (method === 'DELETE') {
|
|
110
|
+
const purge = url.searchParams.get('purge') === 'true';
|
|
111
|
+
return this.#handleTenantDelete(did, purge);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// --- Tenant suspend ---
|
|
116
|
+
{
|
|
117
|
+
const match = subPath.match(/^\/tenants\/([^/]+)\/suspend$/);
|
|
118
|
+
if (match && method === 'POST') {
|
|
119
|
+
const did = decodeURIComponent(match[1]);
|
|
120
|
+
return this.#handleTenantSuspend(did);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// --- Tenant unsuspend ---
|
|
124
|
+
{
|
|
125
|
+
const match = subPath.match(/^\/tenants\/([^/]+)\/unsuspend$/);
|
|
126
|
+
if (match && method === 'POST') {
|
|
127
|
+
const did = decodeURIComponent(match[1]);
|
|
128
|
+
return this.#handleTenantUnsuspend(did);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// --- Tenant quota ---
|
|
132
|
+
{
|
|
133
|
+
const match = subPath.match(/^\/tenants\/([^/]+)\/quota$/);
|
|
134
|
+
if (match) {
|
|
135
|
+
const did = decodeURIComponent(match[1]);
|
|
136
|
+
if (method === 'GET') {
|
|
137
|
+
return this.#handleQuotaGet(did);
|
|
138
|
+
}
|
|
139
|
+
if (method === 'PUT') {
|
|
140
|
+
return this.#handleQuotaSet(did, req);
|
|
141
|
+
}
|
|
142
|
+
if (method === 'DELETE') {
|
|
143
|
+
return this.#handleQuotaDelete(did);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// --- Tenant messages browser ---
|
|
148
|
+
{
|
|
149
|
+
const match = subPath.match(/^\/tenants\/([^/]+)\/messages$/);
|
|
150
|
+
if (match && method === 'GET') {
|
|
151
|
+
const did = decodeURIComponent(match[1]);
|
|
152
|
+
return this.#handleTenantMessages(did, url);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// --- Tenant protocols ---
|
|
156
|
+
{
|
|
157
|
+
const match = subPath.match(/^\/tenants\/([^/]+)\/protocols$/);
|
|
158
|
+
if (match && method === 'GET') {
|
|
159
|
+
const did = decodeURIComponent(match[1]);
|
|
160
|
+
return this.#handleTenantProtocols(did);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// --- Tenant data export ---
|
|
164
|
+
{
|
|
165
|
+
const match = subPath.match(/^\/tenants\/([^/]+)\/export$/);
|
|
166
|
+
if (match && method === 'POST') {
|
|
167
|
+
const did = decodeURIComponent(match[1]);
|
|
168
|
+
return this.#handleTenantExport(did);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// --- Rate limits ---
|
|
172
|
+
if (method === 'GET' && subPath === '/rate-limits') {
|
|
173
|
+
return this.#handleRateLimits();
|
|
174
|
+
}
|
|
175
|
+
// --- Audit log ---
|
|
176
|
+
if (method === 'GET' && subPath === '/audit') {
|
|
177
|
+
return this.#handleAudit(url);
|
|
178
|
+
}
|
|
179
|
+
// --- Runtime config ---
|
|
180
|
+
if (subPath === '/config') {
|
|
181
|
+
if (method === 'GET') {
|
|
182
|
+
return this.#handleConfigGet();
|
|
183
|
+
}
|
|
184
|
+
if (method === 'PATCH') {
|
|
185
|
+
return this.#handleConfigPatch(req);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// --- Activity events ---
|
|
189
|
+
if (method === 'GET' && subPath === '/events') {
|
|
190
|
+
return this.#handleEvents(url);
|
|
191
|
+
}
|
|
192
|
+
// --- WebSocket connections ---
|
|
193
|
+
if (method === 'GET' && subPath === '/connections') {
|
|
194
|
+
return this.#handleConnections();
|
|
195
|
+
}
|
|
196
|
+
// --- Webhooks ---
|
|
197
|
+
if (subPath === '/webhooks') {
|
|
198
|
+
if (method === 'GET') {
|
|
199
|
+
return this.#handleWebhookList();
|
|
200
|
+
}
|
|
201
|
+
if (method === 'POST') {
|
|
202
|
+
return this.#handleWebhookCreate(req);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// --- Webhook delete ---
|
|
206
|
+
{
|
|
207
|
+
const match = subPath.match(/^\/webhooks\/([^/]+)$/);
|
|
208
|
+
if (match && method === 'DELETE') {
|
|
209
|
+
return this.#handleWebhookDelete(match[1]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// --- Info (smoke test) ---
|
|
213
|
+
if (method === 'GET' && subPath === '/info') {
|
|
214
|
+
return Response.json({
|
|
215
|
+
adminApi: true,
|
|
216
|
+
uptime: Math.floor((Date.now() - this.#startTime) / 1000),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return new Response('Not Found', { status: 404 });
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
log.error('Admin API error:', error);
|
|
223
|
+
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
// Handlers
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
/**
|
|
230
|
+
* Deep health check. Probes the underlying stores.
|
|
231
|
+
*/
|
|
232
|
+
async #handleHealth() {
|
|
233
|
+
const checks = {};
|
|
234
|
+
let allHealthy = true;
|
|
235
|
+
// Probe message store by attempting a count for a non-existent tenant.
|
|
236
|
+
const messageStoreCheck = await this.#probeStore('messageStore', async () => {
|
|
237
|
+
await this.#dwn.processMessage('did:admin:healthcheck', {
|
|
238
|
+
descriptor: {
|
|
239
|
+
interface: 'Records',
|
|
240
|
+
method: 'Query',
|
|
241
|
+
messageTimestamp: new Date().toISOString(),
|
|
242
|
+
filter: {},
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
checks.messageStore = messageStoreCheck;
|
|
247
|
+
if (messageStoreCheck.status === 'unhealthy') {
|
|
248
|
+
allHealthy = false;
|
|
249
|
+
}
|
|
250
|
+
// Probe admin store if available.
|
|
251
|
+
if (this.#adminStore) {
|
|
252
|
+
const adminStoreCheck = await this.#probeStore('adminStore', async () => {
|
|
253
|
+
await this.#adminStore.getTenantCount();
|
|
254
|
+
});
|
|
255
|
+
checks.adminStore = adminStoreCheck;
|
|
256
|
+
if (adminStoreCheck.status === 'unhealthy') {
|
|
257
|
+
allHealthy = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const uptime = Math.floor((Date.now() - this.#startTime) / 1000);
|
|
261
|
+
return Response.json({
|
|
262
|
+
status: allHealthy ? 'healthy' : 'unhealthy',
|
|
263
|
+
uptime,
|
|
264
|
+
version: this.#packageInfo.version,
|
|
265
|
+
checks,
|
|
266
|
+
}, { status: allHealthy ? 200 : 503 });
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Measures the latency of a store probe function.
|
|
270
|
+
*/
|
|
271
|
+
async #probeStore(name, probeFn) {
|
|
272
|
+
const start = performance.now();
|
|
273
|
+
try {
|
|
274
|
+
await probeFn();
|
|
275
|
+
return {
|
|
276
|
+
status: 'healthy',
|
|
277
|
+
latencyMs: Math.round(performance.now() - start),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
return {
|
|
282
|
+
status: 'unhealthy',
|
|
283
|
+
latencyMs: Math.round(performance.now() - start),
|
|
284
|
+
error: String(error),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Aggregated server statistics.
|
|
290
|
+
*/
|
|
291
|
+
async #handleStats(url) {
|
|
292
|
+
if (!this.#adminStore) {
|
|
293
|
+
return Response.json({ error: 'Admin store unavailable. Admin features require a SQL storage backend.' }, { status: 501 });
|
|
294
|
+
}
|
|
295
|
+
const refresh = url.searchParams.get('refresh') === 'true';
|
|
296
|
+
const globalStats = await this.#adminStore.getGlobalStats({ refresh });
|
|
297
|
+
const suspendedCount = await this.#adminStore.getSuspendedTenantCount();
|
|
298
|
+
const connectionCount = this.#getConnectionCount();
|
|
299
|
+
const subscriptionCount = this.#getSubscriptionCount();
|
|
300
|
+
const stats = {
|
|
301
|
+
tenants: {
|
|
302
|
+
total: globalStats.tenantCount,
|
|
303
|
+
suspended: suspendedCount,
|
|
304
|
+
},
|
|
305
|
+
storage: {
|
|
306
|
+
totalMessages: globalStats.totalMessages,
|
|
307
|
+
totalDataBytes: globalStats.totalDataBytes,
|
|
308
|
+
totalProtocols: globalStats.totalProtocols,
|
|
309
|
+
},
|
|
310
|
+
connections: {
|
|
311
|
+
websocket: {
|
|
312
|
+
active: connectionCount,
|
|
313
|
+
subscriptions: subscriptionCount,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
registration: {
|
|
317
|
+
proofOfWorkEnabled: this.#config.registrationProofOfWorkEnabled,
|
|
318
|
+
},
|
|
319
|
+
uptime: Math.floor((Date.now() - this.#startTime) / 1000),
|
|
320
|
+
};
|
|
321
|
+
return Response.json(stats);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Paginated tenant list with optional search, filter, and sort.
|
|
325
|
+
*
|
|
326
|
+
* @see https://github.com/enboxorg/enbox/issues/390
|
|
327
|
+
*/
|
|
328
|
+
async #handleTenantList(url) {
|
|
329
|
+
if (!this.#adminStore) {
|
|
330
|
+
return Response.json({ error: 'Admin store unavailable. Admin features require a SQL storage backend.' }, { status: 501 });
|
|
331
|
+
}
|
|
332
|
+
const listOptions = {
|
|
333
|
+
cursor: url.searchParams.get('cursor') ?? undefined,
|
|
334
|
+
limit: Math.min(parseIntOrDefault(url.searchParams.get('limit'), 20), 100),
|
|
335
|
+
search: url.searchParams.get('search') ?? undefined,
|
|
336
|
+
status: url.searchParams.get('status') ?? undefined,
|
|
337
|
+
sort: url.searchParams.get('sort') ?? undefined,
|
|
338
|
+
order: url.searchParams.get('order') ?? undefined,
|
|
339
|
+
};
|
|
340
|
+
// Validate enum-style params.
|
|
341
|
+
if (listOptions.status !== undefined && listOptions.status !== 'active' && listOptions.status !== 'suspended') {
|
|
342
|
+
return Response.json({ error: 'status must be "active" or "suspended"' }, { status: 400 });
|
|
343
|
+
}
|
|
344
|
+
if (listOptions.sort !== undefined && !['did', 'storage', 'messages'].includes(listOptions.sort)) {
|
|
345
|
+
return Response.json({ error: 'sort must be "did", "storage", or "messages"' }, { status: 400 });
|
|
346
|
+
}
|
|
347
|
+
if (listOptions.order !== undefined && listOptions.order !== 'asc' && listOptions.order !== 'desc') {
|
|
348
|
+
return Response.json({ error: 'order must be "asc" or "desc"' }, { status: 400 });
|
|
349
|
+
}
|
|
350
|
+
// Get tenant list from registration store if available, otherwise discover from messages.
|
|
351
|
+
let tenantDids;
|
|
352
|
+
let nextCursor;
|
|
353
|
+
if (this.#registrationStore) {
|
|
354
|
+
const result = await this.#registrationStore.listTenants({
|
|
355
|
+
cursor: listOptions.cursor,
|
|
356
|
+
limit: listOptions.limit,
|
|
357
|
+
search: listOptions.search,
|
|
358
|
+
status: listOptions.status,
|
|
359
|
+
});
|
|
360
|
+
tenantDids = result.tenants.map((t) => t.did);
|
|
361
|
+
nextCursor = result.cursor;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
const result = await this.#adminStore.getDistinctTenants({
|
|
365
|
+
cursor: listOptions.cursor,
|
|
366
|
+
limit: listOptions.limit,
|
|
367
|
+
search: listOptions.search,
|
|
368
|
+
});
|
|
369
|
+
tenantDids = result.tenants;
|
|
370
|
+
nextCursor = result.cursor;
|
|
371
|
+
}
|
|
372
|
+
// Enrich with stats.
|
|
373
|
+
const tenants = await Promise.all(tenantDids.map(async (did) => {
|
|
374
|
+
const stats = await this.#adminStore.getTenantStats(did);
|
|
375
|
+
return {
|
|
376
|
+
did,
|
|
377
|
+
messageCount: stats.messageCount,
|
|
378
|
+
dataStorageBytes: stats.dataStorageBytes,
|
|
379
|
+
};
|
|
380
|
+
}));
|
|
381
|
+
// Apply client-side sort when sorting by storage or messages (not supported in SQL cursor pagination).
|
|
382
|
+
if (listOptions.sort === 'storage') {
|
|
383
|
+
const dir = listOptions.order === 'desc' ? -1 : 1;
|
|
384
|
+
tenants.sort((a, b) => (a.dataStorageBytes - b.dataStorageBytes) * dir);
|
|
385
|
+
}
|
|
386
|
+
else if (listOptions.sort === 'messages') {
|
|
387
|
+
const dir = listOptions.order === 'desc' ? -1 : 1;
|
|
388
|
+
tenants.sort((a, b) => (a.messageCount - b.messageCount) * dir);
|
|
389
|
+
}
|
|
390
|
+
const totalCount = this.#registrationStore
|
|
391
|
+
? await this.#registrationStore.getTenantCount({ search: listOptions.search, status: listOptions.status })
|
|
392
|
+
: await this.#adminStore.getTenantCount();
|
|
393
|
+
const response = {
|
|
394
|
+
data: tenants,
|
|
395
|
+
cursor: nextCursor,
|
|
396
|
+
totalCount,
|
|
397
|
+
};
|
|
398
|
+
return Response.json(response);
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Detailed information for a single tenant.
|
|
402
|
+
*/
|
|
403
|
+
async #handleTenantDetail(did) {
|
|
404
|
+
if (!this.#adminStore) {
|
|
405
|
+
return Response.json({ error: 'Admin store unavailable. Admin features require a SQL storage backend.' }, { status: 501 });
|
|
406
|
+
}
|
|
407
|
+
const stats = await this.#adminStore.getTenantStats(did);
|
|
408
|
+
// If no messages at all, tenant may not exist.
|
|
409
|
+
if (stats.messageCount === 0) {
|
|
410
|
+
// Check registration store.
|
|
411
|
+
let registration;
|
|
412
|
+
if (this.#registrationStore) {
|
|
413
|
+
registration = await this.#registrationStore.getTenantRegistration(did);
|
|
414
|
+
}
|
|
415
|
+
if (!registration && stats.messageCount === 0) {
|
|
416
|
+
return Response.json({ error: 'Tenant not found' }, { status: 404 });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Check registration and active status.
|
|
420
|
+
let isActive = true;
|
|
421
|
+
let suspended = false;
|
|
422
|
+
let registration;
|
|
423
|
+
if (this.#registrationManager) {
|
|
424
|
+
const activeCheck = await this.#registrationManager.isActiveTenant(did);
|
|
425
|
+
isActive = activeCheck.isActiveTenant;
|
|
426
|
+
}
|
|
427
|
+
if (this.#registrationStore) {
|
|
428
|
+
const regData = await this.#registrationStore.getTenantRegistration(did);
|
|
429
|
+
if (regData) {
|
|
430
|
+
registration = {
|
|
431
|
+
termsOfServiceHash: regData.termsOfServiceHash,
|
|
432
|
+
};
|
|
433
|
+
suspended = Boolean(regData.suspended);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
// Resolve quota info.
|
|
437
|
+
let quotaSource = 'unlimited';
|
|
438
|
+
let maxMessages = this.#config.quotaMaxMessages ?? 0;
|
|
439
|
+
let maxStorageBytes = this.#config.quotaMaxStorageBytes ?? 0;
|
|
440
|
+
if (maxMessages > 0 || maxStorageBytes > 0) {
|
|
441
|
+
quotaSource = 'global';
|
|
442
|
+
}
|
|
443
|
+
if (this.#registrationStore) {
|
|
444
|
+
const tenantQuota = await this.#registrationStore.getQuota(did);
|
|
445
|
+
if (tenantQuota !== undefined) {
|
|
446
|
+
maxMessages = tenantQuota.maxMessages ?? maxMessages;
|
|
447
|
+
maxStorageBytes = tenantQuota.maxStorageBytes ?? maxStorageBytes;
|
|
448
|
+
quotaSource = 'tenant';
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const detail = {
|
|
452
|
+
did,
|
|
453
|
+
isActive,
|
|
454
|
+
suspended,
|
|
455
|
+
registration,
|
|
456
|
+
storage: {
|
|
457
|
+
messageCount: stats.messageCount,
|
|
458
|
+
dataStorageBytes: stats.dataStorageBytes,
|
|
459
|
+
protocolCount: stats.protocolCount,
|
|
460
|
+
},
|
|
461
|
+
protocols: stats.protocols,
|
|
462
|
+
quota: {
|
|
463
|
+
maxMessages,
|
|
464
|
+
maxStorageBytes,
|
|
465
|
+
source: quotaSource,
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
return Response.json(detail);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Delete a tenant and optionally purge their DWN data.
|
|
472
|
+
*/
|
|
473
|
+
async #handleTenantDelete(did, purge) {
|
|
474
|
+
let deleted = false;
|
|
475
|
+
if (this.#registrationStore) {
|
|
476
|
+
deleted = await this.#registrationStore.deleteTenant(did);
|
|
477
|
+
}
|
|
478
|
+
let purged = false;
|
|
479
|
+
if (purge && this.#adminStore) {
|
|
480
|
+
const deletedCount = await this.#adminStore.purgeTenantData(did);
|
|
481
|
+
purged = deletedCount > 0 || deleted;
|
|
482
|
+
}
|
|
483
|
+
if (!deleted && !purged) {
|
|
484
|
+
return Response.json({ error: 'Tenant not found' }, { status: 404 });
|
|
485
|
+
}
|
|
486
|
+
await this.#audit('tenant.delete', did, JSON.stringify({ purged }));
|
|
487
|
+
return Response.json({ success: true, did, purged });
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Suspend a tenant.
|
|
491
|
+
*/
|
|
492
|
+
async #handleTenantSuspend(did) {
|
|
493
|
+
if (!this.#registrationStore) {
|
|
494
|
+
return Response.json({ error: 'Tenant suspension requires registration to be enabled.' }, { status: 501 });
|
|
495
|
+
}
|
|
496
|
+
const success = await this.#registrationStore.suspendTenant(did);
|
|
497
|
+
if (!success) {
|
|
498
|
+
return Response.json({ error: 'Tenant not found' }, { status: 404 });
|
|
499
|
+
}
|
|
500
|
+
await this.#audit('tenant.suspend', did);
|
|
501
|
+
return Response.json({ success: true, did });
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Unsuspend a tenant.
|
|
505
|
+
*/
|
|
506
|
+
async #handleTenantUnsuspend(did) {
|
|
507
|
+
if (!this.#registrationStore) {
|
|
508
|
+
return Response.json({ error: 'Tenant unsuspension requires registration to be enabled.' }, { status: 501 });
|
|
509
|
+
}
|
|
510
|
+
const success = await this.#registrationStore.unsuspendTenant(did);
|
|
511
|
+
if (!success) {
|
|
512
|
+
return Response.json({ error: 'Tenant not found' }, { status: 404 });
|
|
513
|
+
}
|
|
514
|
+
await this.#audit('tenant.unsuspend', did);
|
|
515
|
+
return Response.json({ success: true, did });
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Pre-registers a tenant DID via the admin API. Optionally sets a quota.
|
|
519
|
+
*
|
|
520
|
+
* @see https://github.com/enboxorg/enbox/issues/393
|
|
521
|
+
*/
|
|
522
|
+
async #handleTenantCreate(req) {
|
|
523
|
+
if (!this.#registrationStore) {
|
|
524
|
+
return Response.json({ error: 'Tenant creation requires registration to be enabled.' }, { status: 501 });
|
|
525
|
+
}
|
|
526
|
+
let body;
|
|
527
|
+
try {
|
|
528
|
+
body = await req.json();
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
532
|
+
}
|
|
533
|
+
if (!body.did || typeof body.did !== 'string') {
|
|
534
|
+
return Response.json({ error: 'did is required and must be a string' }, { status: 400 });
|
|
535
|
+
}
|
|
536
|
+
const created = await this.#registrationStore.createTenant(body.did);
|
|
537
|
+
if (!created) {
|
|
538
|
+
return Response.json({ error: 'Tenant already exists' }, { status: 409 });
|
|
539
|
+
}
|
|
540
|
+
// Set quota if provided.
|
|
541
|
+
if (body.maxMessages !== undefined || body.maxStorageBytes !== undefined) {
|
|
542
|
+
await this.#registrationStore.setQuota({
|
|
543
|
+
did: body.did,
|
|
544
|
+
maxMessages: body.maxMessages ?? 0,
|
|
545
|
+
maxStorageBytes: body.maxStorageBytes ?? 0,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
await this.#audit('tenant.create', body.did, JSON.stringify({
|
|
549
|
+
maxMessages: body.maxMessages,
|
|
550
|
+
maxStorageBytes: body.maxStorageBytes,
|
|
551
|
+
}));
|
|
552
|
+
return Response.json({ success: true, did: body.did }, { status: 201 });
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Returns recent DWN activity events from the in-memory ring buffer.
|
|
556
|
+
*/
|
|
557
|
+
#handleEvents(url) {
|
|
558
|
+
if (!this.#activityLog) {
|
|
559
|
+
return Response.json({ error: 'Activity log is not enabled.' }, { status: 501 });
|
|
560
|
+
}
|
|
561
|
+
const since = parseIntOrDefault(url.searchParams.get('since'), 0);
|
|
562
|
+
const limit = Math.min(parseIntOrDefault(url.searchParams.get('limit'), 50), 1000);
|
|
563
|
+
const { events, cursor } = this.#activityLog.getEvents({ since, limit });
|
|
564
|
+
return Response.json({ events, cursor });
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Returns snapshots of active WebSocket connections and their subscriptions.
|
|
568
|
+
*/
|
|
569
|
+
#handleConnections() {
|
|
570
|
+
const connections = this.#connectionManager
|
|
571
|
+
? this.#connectionManager.getConnectionSnapshots()
|
|
572
|
+
: [];
|
|
573
|
+
return Response.json({ connections });
|
|
574
|
+
}
|
|
575
|
+
// ---------------------------------------------------------------------------
|
|
576
|
+
// Quota handlers
|
|
577
|
+
// ---------------------------------------------------------------------------
|
|
578
|
+
/**
|
|
579
|
+
* Returns the quota status for a tenant, including effective limits and current usage.
|
|
580
|
+
*/
|
|
581
|
+
async #handleQuotaGet(did) {
|
|
582
|
+
if (!this.#adminStore) {
|
|
583
|
+
return Response.json({ error: 'Admin store unavailable. Quotas require a SQL storage backend.' }, { status: 501 });
|
|
584
|
+
}
|
|
585
|
+
// Resolve effective quota.
|
|
586
|
+
let maxMessages = this.#config.quotaMaxMessages ?? 0;
|
|
587
|
+
let maxStorageBytes = this.#config.quotaMaxStorageBytes ?? 0;
|
|
588
|
+
let source = maxMessages > 0 || maxStorageBytes > 0 ? 'global' : 'unlimited';
|
|
589
|
+
if (this.#registrationStore) {
|
|
590
|
+
const tenantQuota = await this.#registrationStore.getQuota(did);
|
|
591
|
+
if (tenantQuota !== undefined) {
|
|
592
|
+
maxMessages = tenantQuota.maxMessages ?? maxMessages;
|
|
593
|
+
maxStorageBytes = tenantQuota.maxStorageBytes ?? maxStorageBytes;
|
|
594
|
+
source = 'tenant';
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const [messageCount, storageBytes] = await Promise.all([
|
|
598
|
+
this.#adminStore.getTenantMessageCount(did),
|
|
599
|
+
this.#adminStore.getTenantStorageSize(did),
|
|
600
|
+
]);
|
|
601
|
+
const status = {
|
|
602
|
+
quota: {
|
|
603
|
+
maxMessages,
|
|
604
|
+
maxStorageBytes,
|
|
605
|
+
source,
|
|
606
|
+
},
|
|
607
|
+
usage: {
|
|
608
|
+
messageCount,
|
|
609
|
+
storageBytes,
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
return Response.json(status);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Sets the per-tenant quota.
|
|
616
|
+
*/
|
|
617
|
+
async #handleQuotaSet(did, req) {
|
|
618
|
+
if (!this.#registrationStore) {
|
|
619
|
+
return Response.json({ error: 'Quotas require registration to be enabled.' }, { status: 501 });
|
|
620
|
+
}
|
|
621
|
+
let body;
|
|
622
|
+
try {
|
|
623
|
+
body = await req.json();
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
627
|
+
}
|
|
628
|
+
if (body.maxMessages === undefined && body.maxStorageBytes === undefined) {
|
|
629
|
+
return Response.json({ error: 'At least one of maxMessages or maxStorageBytes must be provided.' }, { status: 400 });
|
|
630
|
+
}
|
|
631
|
+
// Merge with existing quota if only one field is provided.
|
|
632
|
+
const existing = await this.#registrationStore.getQuota(did);
|
|
633
|
+
const newQuota = {
|
|
634
|
+
did,
|
|
635
|
+
maxMessages: body.maxMessages ?? existing?.maxMessages ?? 0,
|
|
636
|
+
maxStorageBytes: body.maxStorageBytes ?? existing?.maxStorageBytes ?? 0,
|
|
637
|
+
};
|
|
638
|
+
await this.#registrationStore.setQuota(newQuota);
|
|
639
|
+
await this.#audit('quota.update', did, JSON.stringify({
|
|
640
|
+
maxMessages: newQuota.maxMessages,
|
|
641
|
+
maxStorageBytes: newQuota.maxStorageBytes,
|
|
642
|
+
}));
|
|
643
|
+
return Response.json({ success: true, did });
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Deletes the per-tenant quota, reverting to global defaults.
|
|
647
|
+
*/
|
|
648
|
+
async #handleQuotaDelete(did) {
|
|
649
|
+
if (!this.#registrationStore) {
|
|
650
|
+
return Response.json({ error: 'Quotas require registration to be enabled.' }, { status: 501 });
|
|
651
|
+
}
|
|
652
|
+
const deleted = await this.#registrationStore.deleteQuota(did);
|
|
653
|
+
if (!deleted) {
|
|
654
|
+
return Response.json({ error: 'No per-tenant quota found' }, { status: 404 });
|
|
655
|
+
}
|
|
656
|
+
await this.#audit('quota.delete', did);
|
|
657
|
+
return Response.json({ success: true, did });
|
|
658
|
+
}
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
// Audit log handler
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
/**
|
|
663
|
+
* Queries the persistent audit log with optional filtering and pagination.
|
|
664
|
+
*/
|
|
665
|
+
async #handleAudit(url) {
|
|
666
|
+
if (!this.#auditLog) {
|
|
667
|
+
return Response.json({ error: 'Audit log is not enabled. Requires a SQL storage backend.' }, { status: 501 });
|
|
668
|
+
}
|
|
669
|
+
const since = url.searchParams.get('since') ?? undefined;
|
|
670
|
+
const action = url.searchParams.get('action') ?? undefined;
|
|
671
|
+
const target = url.searchParams.get('target') ?? undefined;
|
|
672
|
+
const limit = Math.min(parseIntOrDefault(url.searchParams.get('limit'), 50), 1000);
|
|
673
|
+
const cursorParam = url.searchParams.get('cursor');
|
|
674
|
+
const cursor = cursorParam !== null ? parseIntOrDefault(cursorParam, 0) : undefined;
|
|
675
|
+
const result = await this.#auditLog.query({ since, action, target, limit, cursor });
|
|
676
|
+
return Response.json(result);
|
|
677
|
+
}
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
// Runtime configuration handlers
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
/**
|
|
682
|
+
* Returns the current runtime-changeable configuration (non-secret values only).
|
|
683
|
+
*/
|
|
684
|
+
#handleConfigGet() {
|
|
685
|
+
const runtimeConfig = {
|
|
686
|
+
logLevel: this.#config.logLevel,
|
|
687
|
+
maxRecordDataSize: this.#config.maxRecordDataSize,
|
|
688
|
+
maxInFlight: this.#config.maxInFlight,
|
|
689
|
+
quotaMaxMessages: this.#config.quotaMaxMessages,
|
|
690
|
+
quotaMaxStorageBytes: this.#config.quotaMaxStorageBytes,
|
|
691
|
+
rateLimitRequestsPerSecond: this.#config.rateLimitRequestsPerSecond,
|
|
692
|
+
rateLimitBurst: this.#config.rateLimitBurst,
|
|
693
|
+
rateLimitTenantRequestsPerSecond: this.#config.rateLimitTenantRequestsPerSecond,
|
|
694
|
+
rateLimitTenantBurst: this.#config.rateLimitTenantBurst,
|
|
695
|
+
};
|
|
696
|
+
return Response.json(runtimeConfig);
|
|
697
|
+
}
|
|
698
|
+
/**
|
|
699
|
+
* Patches runtime-changeable configuration values and applies them immediately.
|
|
700
|
+
*/
|
|
701
|
+
async #handleConfigPatch(req) {
|
|
702
|
+
let body;
|
|
703
|
+
try {
|
|
704
|
+
body = await req.json();
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
708
|
+
}
|
|
709
|
+
// Validate types before applying any changes.
|
|
710
|
+
const validLogLevels = ['trace', 'debug', 'info', 'warn', 'error', 'silent'];
|
|
711
|
+
if (body.logLevel !== undefined && (typeof body.logLevel !== 'string' || !validLogLevels.includes(body.logLevel.toLowerCase()))) {
|
|
712
|
+
return Response.json({ error: `logLevel must be one of: ${validLogLevels.join(', ')}` }, { status: 400 });
|
|
713
|
+
}
|
|
714
|
+
const numericFields = [
|
|
715
|
+
'maxRecordDataSize', 'maxInFlight', 'quotaMaxMessages', 'quotaMaxStorageBytes',
|
|
716
|
+
'rateLimitRequestsPerSecond', 'rateLimitBurst', 'rateLimitTenantRequestsPerSecond', 'rateLimitTenantBurst',
|
|
717
|
+
];
|
|
718
|
+
for (const field of numericFields) {
|
|
719
|
+
if (body[field] !== undefined && (typeof body[field] !== 'number' || !Number.isFinite(body[field]) || body[field] < 0)) {
|
|
720
|
+
return Response.json({ error: `${field} must be a non-negative number` }, { status: 400 });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
const changes = [];
|
|
724
|
+
if (body.logLevel !== undefined) {
|
|
725
|
+
this.#config.logLevel = body.logLevel;
|
|
726
|
+
log.setLevel(body.logLevel);
|
|
727
|
+
changes.push('logLevel');
|
|
728
|
+
}
|
|
729
|
+
if (body.maxRecordDataSize !== undefined) {
|
|
730
|
+
this.#config.maxRecordDataSize = body.maxRecordDataSize;
|
|
731
|
+
changes.push('maxRecordDataSize');
|
|
732
|
+
}
|
|
733
|
+
if (body.maxInFlight !== undefined) {
|
|
734
|
+
this.#config.maxInFlight = body.maxInFlight;
|
|
735
|
+
changes.push('maxInFlight');
|
|
736
|
+
}
|
|
737
|
+
if (body.quotaMaxMessages !== undefined) {
|
|
738
|
+
this.#config.quotaMaxMessages = body.quotaMaxMessages;
|
|
739
|
+
changes.push('quotaMaxMessages');
|
|
740
|
+
}
|
|
741
|
+
if (body.quotaMaxStorageBytes !== undefined) {
|
|
742
|
+
this.#config.quotaMaxStorageBytes = body.quotaMaxStorageBytes;
|
|
743
|
+
changes.push('quotaMaxStorageBytes');
|
|
744
|
+
}
|
|
745
|
+
if (body.rateLimitRequestsPerSecond !== undefined) {
|
|
746
|
+
this.#config.rateLimitRequestsPerSecond = body.rateLimitRequestsPerSecond;
|
|
747
|
+
changes.push('rateLimitRequestsPerSecond');
|
|
748
|
+
}
|
|
749
|
+
if (body.rateLimitBurst !== undefined) {
|
|
750
|
+
this.#config.rateLimitBurst = body.rateLimitBurst;
|
|
751
|
+
changes.push('rateLimitBurst');
|
|
752
|
+
}
|
|
753
|
+
if (body.rateLimitTenantRequestsPerSecond !== undefined) {
|
|
754
|
+
this.#config.rateLimitTenantRequestsPerSecond = body.rateLimitTenantRequestsPerSecond;
|
|
755
|
+
changes.push('rateLimitTenantRequestsPerSecond');
|
|
756
|
+
}
|
|
757
|
+
if (body.rateLimitTenantBurst !== undefined) {
|
|
758
|
+
this.#config.rateLimitTenantBurst = body.rateLimitTenantBurst;
|
|
759
|
+
changes.push('rateLimitTenantBurst');
|
|
760
|
+
}
|
|
761
|
+
if (changes.length === 0) {
|
|
762
|
+
return Response.json({ error: 'No valid configuration fields provided.' }, { status: 400 });
|
|
763
|
+
}
|
|
764
|
+
// Reconfigure rate limiters if rate limit settings changed.
|
|
765
|
+
// @see https://github.com/enboxorg/enbox/issues/389
|
|
766
|
+
if (this.#ipRateLimiter && (body.rateLimitRequestsPerSecond !== undefined || body.rateLimitBurst !== undefined)) {
|
|
767
|
+
this.#ipRateLimiter.reconfigure({
|
|
768
|
+
refillRate: this.#config.rateLimitRequestsPerSecond,
|
|
769
|
+
maxTokens: this.#config.rateLimitBurst,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
if (this.#tenantRateLimiter && (body.rateLimitTenantRequestsPerSecond !== undefined || body.rateLimitTenantBurst !== undefined)) {
|
|
773
|
+
this.#tenantRateLimiter.reconfigure({
|
|
774
|
+
refillRate: this.#config.rateLimitTenantRequestsPerSecond,
|
|
775
|
+
maxTokens: this.#config.rateLimitTenantBurst,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
await this.#audit('config.update', undefined, JSON.stringify({ changes, values: body }));
|
|
779
|
+
return Response.json({ success: true, updated: changes });
|
|
780
|
+
}
|
|
781
|
+
// ---------------------------------------------------------------------------
|
|
782
|
+
// Tenant data browser handlers
|
|
783
|
+
// ---------------------------------------------------------------------------
|
|
784
|
+
/**
|
|
785
|
+
* Returns paginated message metadata for a tenant (no content/encoded bytes).
|
|
786
|
+
*/
|
|
787
|
+
async #handleTenantMessages(did, url) {
|
|
788
|
+
if (!this.#adminStore) {
|
|
789
|
+
return Response.json({ error: 'Admin store unavailable. Requires a SQL storage backend.' }, { status: 501 });
|
|
790
|
+
}
|
|
791
|
+
const iface = url.searchParams.get('interface') ?? undefined;
|
|
792
|
+
const method = url.searchParams.get('method') ?? undefined;
|
|
793
|
+
const protocol = url.searchParams.get('protocol') ?? undefined;
|
|
794
|
+
const limit = Math.min(parseIntOrDefault(url.searchParams.get('limit'), 20), 100);
|
|
795
|
+
const cursorParam = url.searchParams.get('cursor');
|
|
796
|
+
const cursor = cursorParam !== null ? parseIntOrDefault(cursorParam, 0) : undefined;
|
|
797
|
+
const result = await this.#adminStore.getTenantMessages(did, {
|
|
798
|
+
interface: iface,
|
|
799
|
+
method,
|
|
800
|
+
protocol,
|
|
801
|
+
limit,
|
|
802
|
+
cursor,
|
|
803
|
+
});
|
|
804
|
+
return Response.json(result);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Returns per-protocol message counts for a tenant.
|
|
808
|
+
*/
|
|
809
|
+
async #handleTenantProtocols(did) {
|
|
810
|
+
if (!this.#adminStore) {
|
|
811
|
+
return Response.json({ error: 'Admin store unavailable. Requires a SQL storage backend.' }, { status: 501 });
|
|
812
|
+
}
|
|
813
|
+
const protocols = await this.#adminStore.getTenantProtocolCounts(did);
|
|
814
|
+
return Response.json({ protocols });
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Exports all message metadata and data records for a tenant as JSON.
|
|
818
|
+
*
|
|
819
|
+
* @see https://github.com/enboxorg/enbox/issues/391
|
|
820
|
+
*/
|
|
821
|
+
async #handleTenantExport(did) {
|
|
822
|
+
if (!this.#adminStore) {
|
|
823
|
+
return Response.json({ error: 'Admin store unavailable. Requires a SQL storage backend.' }, { status: 501 });
|
|
824
|
+
}
|
|
825
|
+
const exportData = await this.#adminStore.exportTenantData(did);
|
|
826
|
+
if (exportData.metadata.messageCount === 0 && exportData.metadata.dataRecordCount === 0) {
|
|
827
|
+
return Response.json({ error: 'Tenant not found or has no data' }, { status: 404 });
|
|
828
|
+
}
|
|
829
|
+
await this.#audit('tenant.export', did, JSON.stringify({
|
|
830
|
+
messageCount: exportData.metadata.messageCount,
|
|
831
|
+
dataRecordCount: exportData.metadata.dataRecordCount,
|
|
832
|
+
}));
|
|
833
|
+
return Response.json(exportData, {
|
|
834
|
+
headers: {
|
|
835
|
+
'content-disposition': `attachment; filename="${did}-export.json"`,
|
|
836
|
+
},
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
// ---------------------------------------------------------------------------
|
|
840
|
+
// Rate limit handler
|
|
841
|
+
// ---------------------------------------------------------------------------
|
|
842
|
+
/**
|
|
843
|
+
* Returns the current rate limiting configuration and active entry counts.
|
|
844
|
+
*/
|
|
845
|
+
#handleRateLimits() {
|
|
846
|
+
const status = {
|
|
847
|
+
config: {
|
|
848
|
+
perIp: {
|
|
849
|
+
requestsPerSecond: this.#config.rateLimitRequestsPerSecond ?? 0,
|
|
850
|
+
burst: this.#config.rateLimitBurst ?? 50,
|
|
851
|
+
enabled: (this.#config.rateLimitRequestsPerSecond ?? 0) > 0,
|
|
852
|
+
},
|
|
853
|
+
perTenant: {
|
|
854
|
+
requestsPerSecond: this.#config.rateLimitTenantRequestsPerSecond ?? 0,
|
|
855
|
+
burst: this.#config.rateLimitTenantBurst ?? 50,
|
|
856
|
+
enabled: (this.#config.rateLimitTenantRequestsPerSecond ?? 0) > 0,
|
|
857
|
+
},
|
|
858
|
+
},
|
|
859
|
+
activeEntries: {
|
|
860
|
+
ip: this.#ipRateLimiter?.size ?? 0,
|
|
861
|
+
tenant: this.#tenantRateLimiter?.size ?? 0,
|
|
862
|
+
},
|
|
863
|
+
};
|
|
864
|
+
return Response.json(status);
|
|
865
|
+
}
|
|
866
|
+
// ---------------------------------------------------------------------------
|
|
867
|
+
// Webhook handlers
|
|
868
|
+
// ---------------------------------------------------------------------------
|
|
869
|
+
/**
|
|
870
|
+
* Lists all registered webhooks.
|
|
871
|
+
*
|
|
872
|
+
* @see https://github.com/enboxorg/enbox/issues/395
|
|
873
|
+
*/
|
|
874
|
+
async #handleWebhookList() {
|
|
875
|
+
if (!this.#webhookManager) {
|
|
876
|
+
return Response.json({ error: 'Webhooks are not enabled. Requires a SQL storage backend.' }, { status: 501 });
|
|
877
|
+
}
|
|
878
|
+
const webhooks = await this.#webhookManager.list();
|
|
879
|
+
return Response.json({ webhooks });
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Registers a new webhook endpoint.
|
|
883
|
+
*
|
|
884
|
+
* @see https://github.com/enboxorg/enbox/issues/395
|
|
885
|
+
*/
|
|
886
|
+
async #handleWebhookCreate(req) {
|
|
887
|
+
if (!this.#webhookManager) {
|
|
888
|
+
return Response.json({ error: 'Webhooks are not enabled. Requires a SQL storage backend.' }, { status: 501 });
|
|
889
|
+
}
|
|
890
|
+
let body;
|
|
891
|
+
try {
|
|
892
|
+
body = await req.json();
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
|
|
896
|
+
}
|
|
897
|
+
if (!body.url || typeof body.url !== 'string') {
|
|
898
|
+
return Response.json({ error: 'url is required and must be a string' }, { status: 400 });
|
|
899
|
+
}
|
|
900
|
+
if (!body.events || !Array.isArray(body.events) || body.events.length === 0) {
|
|
901
|
+
return Response.json({ error: 'events is required and must be a non-empty array' }, { status: 400 });
|
|
902
|
+
}
|
|
903
|
+
// Validate URL format.
|
|
904
|
+
try {
|
|
905
|
+
new URL(body.url);
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
return Response.json({ error: 'url must be a valid URL' }, { status: 400 });
|
|
909
|
+
}
|
|
910
|
+
const webhook = await this.#webhookManager.register(body);
|
|
911
|
+
await this.#audit('webhook.create', webhook.id, JSON.stringify({ url: body.url, events: body.events }));
|
|
912
|
+
return Response.json(webhook, { status: 201 });
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Deletes a webhook registration by ID.
|
|
916
|
+
*
|
|
917
|
+
* @see https://github.com/enboxorg/enbox/issues/395
|
|
918
|
+
*/
|
|
919
|
+
async #handleWebhookDelete(id) {
|
|
920
|
+
if (!this.#webhookManager) {
|
|
921
|
+
return Response.json({ error: 'Webhooks are not enabled. Requires a SQL storage backend.' }, { status: 501 });
|
|
922
|
+
}
|
|
923
|
+
const deleted = await this.#webhookManager.delete(id);
|
|
924
|
+
if (!deleted) {
|
|
925
|
+
return Response.json({ error: 'Webhook not found' }, { status: 404 });
|
|
926
|
+
}
|
|
927
|
+
await this.#audit('webhook.delete', id);
|
|
928
|
+
return Response.json({ success: true, id });
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Public accessor to fire webhook events from other components.
|
|
932
|
+
*/
|
|
933
|
+
fireWebhook(event, target, data) {
|
|
934
|
+
if (this.#webhookManager) {
|
|
935
|
+
this.#webhookManager.fire(event, target, data);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// ---------------------------------------------------------------------------
|
|
939
|
+
// Periodic metrics updater
|
|
940
|
+
// ---------------------------------------------------------------------------
|
|
941
|
+
/**
|
|
942
|
+
* Starts a periodic timer that updates Prometheus gauge metrics from the
|
|
943
|
+
* admin store and connection manager. The interval is configured via
|
|
944
|
+
* `adminMetricsUpdateIntervalSeconds` (default 30s).
|
|
945
|
+
*/
|
|
946
|
+
startMetricsUpdater() {
|
|
947
|
+
if (this.#metricsInterval) {
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const intervalMs = (this.#config.adminMetricsUpdateIntervalSeconds ?? 30) * 1000;
|
|
951
|
+
// Run immediately, then on interval.
|
|
952
|
+
this.#updateMetrics();
|
|
953
|
+
this.#metricsInterval = setInterval(() => {
|
|
954
|
+
this.#updateMetrics();
|
|
955
|
+
}, intervalMs);
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Stops the periodic metrics updater.
|
|
959
|
+
*/
|
|
960
|
+
stopMetricsUpdater() {
|
|
961
|
+
if (this.#metricsInterval) {
|
|
962
|
+
clearInterval(this.#metricsInterval);
|
|
963
|
+
this.#metricsInterval = undefined;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Fetches stats from the admin store and connection manager, and sets
|
|
968
|
+
* Prometheus gauge values accordingly.
|
|
969
|
+
*/
|
|
970
|
+
#updateMetrics() {
|
|
971
|
+
// Connection gauges (synchronous).
|
|
972
|
+
websocketConnections.set(this.#getConnectionCount());
|
|
973
|
+
websocketSubscriptions.set(this.#getSubscriptionCount());
|
|
974
|
+
// Store-based gauges (async — fire and forget).
|
|
975
|
+
if (this.#adminStore) {
|
|
976
|
+
this.#adminStore.getGlobalStats({ refresh: true }).then((stats) => {
|
|
977
|
+
activeTenants.set(stats.tenantCount);
|
|
978
|
+
totalMessages.set(stats.totalMessages);
|
|
979
|
+
totalDataBytes.set(stats.totalDataBytes);
|
|
980
|
+
}).catch((err) => {
|
|
981
|
+
log.error('Failed to update Prometheus gauge metrics:', err);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// ---------------------------------------------------------------------------
|
|
986
|
+
// Helpers
|
|
987
|
+
// ---------------------------------------------------------------------------
|
|
988
|
+
/**
|
|
989
|
+
* Logs failed admin authentication attempts, rate-limited to one entry per IP
|
|
990
|
+
* per 60 seconds to prevent audit log flooding.
|
|
991
|
+
*
|
|
992
|
+
* @see https://github.com/enboxorg/enbox/issues/392
|
|
993
|
+
*/
|
|
994
|
+
#auditFailedAuth(req, path) {
|
|
995
|
+
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown';
|
|
996
|
+
const now = Date.now();
|
|
997
|
+
const lastLogged = this.#failedAuthLog.get(ip);
|
|
998
|
+
if (lastLogged !== undefined && (now - lastLogged) < AdminApi.#AUTH_AUDIT_INTERVAL_MS) {
|
|
999
|
+
return; // Rate-limited — skip.
|
|
1000
|
+
}
|
|
1001
|
+
this.#failedAuthLog.set(ip, now);
|
|
1002
|
+
// Fire-and-forget — never block the response for audit logging.
|
|
1003
|
+
this.#audit('admin.auth.failure', ip, JSON.stringify({ path }));
|
|
1004
|
+
// Periodically prune old entries to prevent unbounded Map growth.
|
|
1005
|
+
if (this.#failedAuthLog.size > 1000) {
|
|
1006
|
+
const threshold = now - AdminApi.#AUTH_AUDIT_INTERVAL_MS;
|
|
1007
|
+
for (const [key, ts] of this.#failedAuthLog) {
|
|
1008
|
+
if (ts < threshold) {
|
|
1009
|
+
this.#failedAuthLog.delete(key);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Records an audit event if the audit log is available.
|
|
1016
|
+
* Errors are logged but never propagated — audit logging must not break operations.
|
|
1017
|
+
*/
|
|
1018
|
+
async #audit(action, target, detail) {
|
|
1019
|
+
if (!this.#auditLog) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
await this.#auditLog.record({
|
|
1024
|
+
actor: 'admin',
|
|
1025
|
+
action,
|
|
1026
|
+
target,
|
|
1027
|
+
detail,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
catch (err) {
|
|
1031
|
+
log.error('Failed to record audit event:', err);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
#getConnectionCount() {
|
|
1035
|
+
if (this.#connectionManager) {
|
|
1036
|
+
return this.#connectionManager.getConnectionCount();
|
|
1037
|
+
}
|
|
1038
|
+
return 0;
|
|
1039
|
+
}
|
|
1040
|
+
#getSubscriptionCount() {
|
|
1041
|
+
if (this.#connectionManager) {
|
|
1042
|
+
return this.#connectionManager.getSubscriptionCount();
|
|
1043
|
+
}
|
|
1044
|
+
return 0;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
//# sourceMappingURL=admin-api.js.map
|