mindoodb 0.0.85 → 0.0.88
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/README-server.md +6 -2
- package/README.md +6 -4
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +2 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +151 -12
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +340 -24
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +14 -0
- package/dist/cloudflare/DurableObjectSql.d.ts +56 -0
- package/dist/cloudflare/DurableObjectSql.d.ts.map +1 -0
- package/dist/cloudflare/DurableObjectSql.js +180 -0
- package/dist/cloudflare/DurableObjectSql.js.map +1 -0
- package/dist/cloudflare/MindooDatabaseObject.d.ts +86 -0
- package/dist/cloudflare/MindooDatabaseObject.d.ts.map +1 -0
- package/dist/cloudflare/MindooDatabaseObject.js +614 -0
- package/dist/cloudflare/MindooDatabaseObject.js.map +1 -0
- package/dist/cloudflare/MindooServerObject.d.ts +63 -0
- package/dist/cloudflare/MindooServerObject.d.ts.map +1 -0
- package/dist/cloudflare/MindooServerObject.js +521 -0
- package/dist/cloudflare/MindooServerObject.js.map +1 -0
- package/dist/cloudflare/WorkersCryptoAdapter.d.ts +7 -0
- package/dist/cloudflare/WorkersCryptoAdapter.d.ts.map +1 -0
- package/dist/cloudflare/WorkersCryptoAdapter.js +14 -0
- package/dist/cloudflare/WorkersCryptoAdapter.js.map +1 -0
- package/dist/cloudflare/host.d.ts +49 -0
- package/dist/cloudflare/host.d.ts.map +1 -0
- package/dist/cloudflare/host.js +104 -0
- package/dist/cloudflare/host.js.map +1 -0
- package/dist/cloudflare/http.d.ts +91 -0
- package/dist/cloudflare/http.d.ts.map +1 -0
- package/dist/cloudflare/http.js +266 -0
- package/dist/cloudflare/http.js.map +1 -0
- package/dist/cloudflare/index.d.ts +12 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +20 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/cloudflare/storeRpc.d.ts +22 -0
- package/dist/cloudflare/storeRpc.d.ts.map +1 -0
- package/dist/cloudflare/storeRpc.js +85 -0
- package/dist/cloudflare/storeRpc.js.map +1 -0
- package/dist/cloudflare/types.d.ts +101 -0
- package/dist/cloudflare/types.d.ts.map +1 -0
- package/dist/cloudflare/types.js +10 -0
- package/dist/cloudflare/types.js.map +1 -0
- package/dist/cloudflare/worker.d.ts +25 -0
- package/dist/cloudflare/worker.d.ts.map +1 -0
- package/dist/cloudflare/worker.js +170 -0
- package/dist/cloudflare/worker.js.map +1 -0
- package/dist/core/BaseMindooDB.d.ts +95 -10
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +441 -98
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +43 -0
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +157 -6
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +103 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +371 -8
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +2 -1
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/ChangeDocDraft.d.ts +133 -0
- package/dist/core/ChangeDocDraft.d.ts.map +1 -0
- package/dist/core/ChangeDocDraft.js +492 -0
- package/dist/core/ChangeDocDraft.js.map +1 -0
- package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
- package/dist/core/MindooDBServerAdmin.js +2 -1
- package/dist/core/MindooDBServerAdmin.js.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.d.ts.map +1 -1
- package/dist/core/accesscontrol/directoryProjection.js +19 -0
- package/dist/core/accesscontrol/directoryProjection.js.map +1 -1
- package/dist/core/accesscontrol/evaluate.d.ts +14 -0
- package/dist/core/accesscontrol/evaluate.d.ts.map +1 -1
- package/dist/core/accesscontrol/evaluate.js +42 -1
- package/dist/core/accesscontrol/evaluate.js.map +1 -1
- package/dist/core/accesscontrol/types.d.ts +88 -4
- package/dist/core/accesscontrol/types.d.ts.map +1 -1
- package/dist/core/accesscontrol/types.js +114 -1
- package/dist/core/accesscontrol/types.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +14 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +27 -7
- package/dist/core/appendonlystores/network/AuthenticationService.js.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js +12 -0
- package/dist/core/appendonlystores/network/IrohNetworkTransport.js.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/IrohStreamIO.js +3 -23
- package/dist/core/appendonlystores/network/IrohStreamIO.js.map +1 -1
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts +6 -0
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js +20 -1
- package/dist/core/appendonlystores/rpc/RpcContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/rpc/storeRpcCodec.d.ts.map +1 -1
- package/dist/core/appendonlystores/rpc/storeRpcCodec.js +3 -23
- package/dist/core/appendonlystores/rpc/storeRpcCodec.js.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts +7 -0
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +39 -30
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteDriver.d.ts +37 -1
- package/dist/core/appendonlystores/sqlite/SqliteDriver.d.ts.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteDriver.js +99 -40
- package/dist/core/appendonlystores/sqlite/SqliteDriver.js.map +1 -1
- package/dist/core/appendonlystores/syncStores.d.ts.map +1 -1
- package/dist/core/appendonlystores/syncStores.js +25 -5
- package/dist/core/appendonlystores/syncStores.js.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +7 -1
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/types.js +1 -0
- package/dist/core/appendonlystores/types.js.map +1 -1
- package/dist/core/builtinDbInvariants.d.ts +49 -0
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +86 -1
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/crypto/AuthChallenge.d.ts +68 -0
- package/dist/core/crypto/AuthChallenge.d.ts.map +1 -0
- package/dist/core/crypto/AuthChallenge.js +118 -0
- package/dist/core/crypto/AuthChallenge.js.map +1 -0
- package/dist/core/crypto/DomainStatement.d.ts +5 -3
- package/dist/core/crypto/DomainStatement.d.ts.map +1 -1
- package/dist/core/crypto/DomainStatement.js +5 -3
- package/dist/core/crypto/DomainStatement.js.map +1 -1
- package/dist/core/crypto/encryptedFields.d.ts +11 -0
- package/dist/core/crypto/encryptedFields.d.ts.map +1 -1
- package/dist/core/crypto/encryptedFields.js +18 -0
- package/dist/core/crypto/encryptedFields.js.map +1 -1
- package/dist/core/expressions/evaluateExpression.d.ts +8 -0
- package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
- package/dist/core/expressions/evaluateExpression.js +29 -4
- package/dist/core/expressions/evaluateExpression.js.map +1 -1
- package/dist/core/expressions/metadata.js +4 -4
- package/dist/core/expressions/metadata.js.map +1 -1
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +17 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/query/executeQuery.d.ts.map +1 -1
- package/dist/core/query/executeQuery.js +5 -1
- package/dist/core/query/executeQuery.js.map +1 -1
- package/dist/core/query/executeQueryInclude.d.ts.map +1 -1
- package/dist/core/query/executeQueryInclude.js +2 -0
- package/dist/core/query/executeQueryInclude.js.map +1 -1
- package/dist/core/query/queryView.d.ts.map +1 -1
- package/dist/core/query/queryView.js +5 -1
- package/dist/core/query/queryView.js.map +1 -1
- package/dist/core/types.d.ts +204 -10
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/uri/MindooURI.d.ts.map +1 -1
- package/dist/core/uri/MindooURI.js +3 -24
- package/dist/core/uri/MindooURI.js.map +1 -1
- package/dist/core/userkeys/index.d.ts +1 -0
- package/dist/core/userkeys/index.d.ts.map +1 -1
- package/dist/core/userkeys/index.js +5 -1
- package/dist/core/userkeys/index.js.map +1 -1
- package/dist/core/userkeys/sealedValues.d.ts +54 -0
- package/dist/core/userkeys/sealedValues.d.ts.map +1 -0
- package/dist/core/userkeys/sealedValues.js +67 -0
- package/dist/core/userkeys/sealedValues.js.map +1 -0
- package/dist/core/utils/base64.d.ts +17 -0
- package/dist/core/utils/base64.d.ts.map +1 -0
- package/dist/core/utils/base64.js +54 -0
- package/dist/core/utils/base64.js.map +1 -0
- package/dist/core/utils/dynamicImportInterop.d.ts +10 -0
- package/dist/core/utils/dynamicImportInterop.d.ts.map +1 -0
- package/dist/core/utils/dynamicImportInterop.js +22 -0
- package/dist/core/utils/dynamicImportInterop.js.map +1 -0
- package/dist/core/utils/idGeneration.d.ts.map +1 -1
- package/dist/core/utils/idGeneration.js +14 -3
- package/dist/core/utils/idGeneration.js.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +7 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/FsServerDataStore.d.ts +23 -0
- package/dist/node/server/FsServerDataStore.d.ts.map +1 -0
- package/dist/node/server/FsServerDataStore.js +59 -0
- package/dist/node/server/FsServerDataStore.js.map +1 -0
- package/dist/node/server/IrohServerEndpoint.d.ts +1 -1
- package/dist/node/server/IrohServerEndpoint.d.ts.map +1 -1
- package/dist/node/server/IrohServerRpc.d.ts +5 -5
- package/dist/node/server/IrohServerRpc.d.ts.map +1 -1
- package/dist/node/server/IrohServerRpc.js +1 -1
- package/dist/node/server/IrohServerRpc.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts +29 -150
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +160 -1573
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/PurgedDocRegistry.d.ts +4 -27
- package/dist/node/server/PurgedDocRegistry.d.ts.map +1 -1
- package/dist/node/server/PurgedDocRegistry.js +6 -83
- package/dist/node/server/PurgedDocRegistry.js.map +1 -1
- package/dist/node/server/StoreFactory.d.ts +1 -1
- package/dist/node/server/StoreFactory.d.ts.map +1 -1
- package/dist/node/server/TenantManager.d.ts +16 -284
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +35 -1168
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/add-to-network.js +1 -1
- package/dist/node/server/add-to-network.js.map +1 -1
- package/dist/node/server/config.d.ts +11 -15
- package/dist/node/server/config.d.ts.map +1 -1
- package/dist/node/server/config.js +30 -245
- package/dist/node/server/config.js.map +1 -1
- package/dist/node/server/expressAdapter.d.ts +23 -0
- package/dist/node/server/expressAdapter.d.ts.map +1 -0
- package/dist/node/server/expressAdapter.js +108 -0
- package/dist/node/server/expressAdapter.js.map +1 -0
- package/dist/node/server/nativeIrohStreamIO.d.ts +1 -1
- package/dist/node/server/nativeIrohStreamIO.d.ts.map +1 -1
- package/dist/node/server/peer/FileClusterAuditStorage.d.ts +16 -0
- package/dist/node/server/peer/FileClusterAuditStorage.d.ts.map +1 -0
- package/dist/node/server/peer/FileClusterAuditStorage.js +39 -0
- package/dist/node/server/peer/FileClusterAuditStorage.js.map +1 -0
- package/dist/node/server/peer/IrohPeerLink.d.ts +1 -1
- package/dist/node/server/peer/IrohPeerLink.d.ts.map +1 -1
- package/dist/node/server/peer/routes.d.ts +10 -44
- package/dist/node/server/peer/routes.d.ts.map +1 -1
- package/dist/node/server/peer/routes.js +33 -275
- package/dist/node/server/peer/routes.js.map +1 -1
- package/dist/node/server/resolveServerPassword.js +1 -1
- package/dist/node/server/resolveServerPassword.js.map +1 -1
- package/dist/node/server/server.js +1 -1
- package/dist/node/server/server.js.map +1 -1
- package/dist/node/server/serverinit.js +1 -1
- package/dist/node/server/serverinit.js.map +1 -1
- package/dist/server/CapabilityMatcher.d.ts +60 -0
- package/dist/server/CapabilityMatcher.d.ts.map +1 -0
- package/dist/server/CapabilityMatcher.js +138 -0
- package/dist/server/CapabilityMatcher.js.map +1 -0
- package/dist/server/DeviceDiscoveryService.d.ts +36 -0
- package/dist/server/DeviceDiscoveryService.d.ts.map +1 -0
- package/dist/server/DeviceDiscoveryService.js +164 -0
- package/dist/server/DeviceDiscoveryService.js.map +1 -0
- package/dist/server/PurgedDocRegistry.d.ts +35 -0
- package/dist/server/PurgedDocRegistry.d.ts.map +1 -0
- package/dist/server/PurgedDocRegistry.js +90 -0
- package/dist/server/PurgedDocRegistry.js.map +1 -0
- package/dist/server/ServerDataStore.d.ts +67 -0
- package/dist/server/ServerDataStore.d.ts.map +1 -0
- package/dist/server/ServerDataStore.js +88 -0
- package/dist/server/ServerDataStore.js.map +1 -0
- package/dist/server/SyncEventBus.d.ts +39 -0
- package/dist/server/SyncEventBus.d.ts.map +1 -0
- package/dist/server/SyncEventBus.js +41 -0
- package/dist/server/SyncEventBus.js.map +1 -0
- package/dist/server/SystemAdminAuth.d.ts +82 -0
- package/dist/server/SystemAdminAuth.d.ts.map +1 -0
- package/dist/server/SystemAdminAuth.js +227 -0
- package/dist/server/SystemAdminAuth.js.map +1 -0
- package/dist/server/TenantManager.d.ts +366 -0
- package/dist/server/TenantManager.d.ts.map +1 -0
- package/dist/server/TenantManager.js +1416 -0
- package/dist/server/TenantManager.js.map +1 -0
- package/dist/server/http/MindooDBServerCore.d.ts +313 -0
- package/dist/server/http/MindooDBServerCore.d.ts.map +1 -0
- package/dist/server/http/MindooDBServerCore.js +1410 -0
- package/dist/server/http/MindooDBServerCore.js.map +1 -0
- package/dist/server/http/types.d.ts +89 -0
- package/dist/server/http/types.d.ts.map +1 -0
- package/dist/server/http/types.js +29 -0
- package/dist/server/http/types.js.map +1 -0
- package/dist/server/index.d.ts +32 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +76 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/peer/ClusterAuditLog.d.ts +71 -0
- package/dist/server/peer/ClusterAuditLog.d.ts.map +1 -0
- package/dist/server/peer/ClusterAuditLog.js +81 -0
- package/dist/server/peer/ClusterAuditLog.js.map +1 -0
- package/dist/server/peer/ClusterJobRunner.d.ts +43 -0
- package/dist/server/peer/ClusterJobRunner.d.ts.map +1 -0
- package/dist/server/peer/ClusterJobRunner.js +119 -0
- package/dist/server/peer/ClusterJobRunner.js.map +1 -0
- package/dist/server/peer/ClusterManager.d.ts +149 -0
- package/dist/server/peer/ClusterManager.d.ts.map +1 -0
- package/dist/server/peer/ClusterManager.js +292 -0
- package/dist/server/peer/ClusterManager.js.map +1 -0
- package/dist/server/peer/PeerAuthService.d.ts +69 -0
- package/dist/server/peer/PeerAuthService.d.ts.map +1 -0
- package/dist/server/peer/PeerAuthService.js +207 -0
- package/dist/server/peer/PeerAuthService.js.map +1 -0
- package/dist/server/peer/PeerReplicator.d.ts +303 -0
- package/dist/server/peer/PeerReplicator.d.ts.map +1 -0
- package/dist/server/peer/PeerReplicator.js +1072 -0
- package/dist/server/peer/PeerReplicator.js.map +1 -0
- package/dist/server/peer/clusterHttp.d.ts +113 -0
- package/dist/server/peer/clusterHttp.d.ts.map +1 -0
- package/dist/server/peer/clusterHttp.js +290 -0
- package/dist/server/peer/clusterHttp.js.map +1 -0
- package/dist/server/peer/types.d.ts +244 -0
- package/dist/server/peer/types.d.ts.map +1 -0
- package/dist/server/peer/types.js +189 -0
- package/dist/server/peer/types.js.map +1 -0
- package/dist/server/rateLimits.d.ts +119 -0
- package/dist/server/rateLimits.d.ts.map +1 -0
- package/dist/server/rateLimits.js +159 -0
- package/dist/server/rateLimits.js.map +1 -0
- package/dist/server/serverConfig.d.ts +37 -0
- package/dist/server/serverConfig.d.ts.map +1 -0
- package/dist/server/serverConfig.js +251 -0
- package/dist/server/serverConfig.js.map +1 -0
- package/dist/server/timestampProxy.d.ts +129 -0
- package/dist/server/timestampProxy.d.ts.map +1 -0
- package/dist/server/timestampProxy.js +361 -0
- package/dist/server/timestampProxy.js.map +1 -0
- package/dist/server/types.d.ts +267 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +56 -0
- package/dist/server/types.js.map +1 -0
- package/dist/server/validation.d.ts +43 -0
- package/dist/server/validation.d.ts.map +1 -0
- package/dist/server/validation.js +104 -0
- package/dist/server/validation.js.map +1 -0
- package/package.json +9 -1
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MindooDB Server - Express HTTP server implementing the sync API.
|
|
4
4
|
*
|
|
5
|
+
* The route handlers themselves are platform-neutral and live in
|
|
6
|
+
* {@link MindooDBServerCore} (`src/server/http`); this class is the Node host:
|
|
7
|
+
* it wires the server's components, mounts the handlers on Express and adds
|
|
8
|
+
* what only a Node process provides — HTTP/2 + TLS, static files, Iroh, the
|
|
9
|
+
* peer and cluster routers, and the middleware stack below.
|
|
10
|
+
*
|
|
5
11
|
* Security features:
|
|
6
12
|
* - Capabilities-based system admin authorization via config.json
|
|
7
13
|
* - Challenge/response (Ed25519) authentication for /system/* endpoints
|
|
@@ -55,104 +61,29 @@ const http2_1 = __importDefault(require("http2"));
|
|
|
55
61
|
const path_1 = __importDefault(require("path"));
|
|
56
62
|
const fs_1 = require("fs");
|
|
57
63
|
const helmet_1 = __importDefault(require("helmet"));
|
|
58
|
-
const errors_1 = require("../../core/errors");
|
|
59
64
|
const cors_1 = __importDefault(require("cors"));
|
|
60
65
|
const express_rate_limit_1 = __importStar(require("express-rate-limit"));
|
|
61
66
|
const compressionMiddleware_1 = require("./compressionMiddleware");
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
67
|
+
const SyncEventBus_1 = require("../../server/SyncEventBus");
|
|
68
|
+
const ClusterManager_1 = require("../../server/peer/ClusterManager");
|
|
69
|
+
const FileClusterAuditStorage_1 = require("./peer/FileClusterAuditStorage");
|
|
70
|
+
const IrohPeerLink_1 = require("./peer/IrohPeerLink");
|
|
66
71
|
const routes_1 = require("./peer/routes");
|
|
67
|
-
const types_1 = require("./peer/types");
|
|
68
|
-
const types_2 = require("../../core/appendonlystores/types");
|
|
69
|
-
const types_3 = require("../../core/appendonlystores/network/types");
|
|
70
72
|
const TenantManager_1 = require("./TenantManager");
|
|
71
|
-
const CapabilityMatcher_1 = require("
|
|
72
|
-
const SystemAdminAuth_1 = require("
|
|
73
|
-
const DeviceDiscoveryService_1 = require("
|
|
73
|
+
const CapabilityMatcher_1 = require("../../server/CapabilityMatcher");
|
|
74
|
+
const SystemAdminAuth_1 = require("../../server/SystemAdminAuth");
|
|
75
|
+
const DeviceDiscoveryService_1 = require("../../server/DeviceDiscoveryService");
|
|
74
76
|
const config_1 = require("./config");
|
|
75
77
|
const SystemIpAllowlist_1 = require("./SystemIpAllowlist");
|
|
76
78
|
const IrohServerRpc_1 = require("./IrohServerRpc");
|
|
77
79
|
const IrohServerEndpoint_1 = require("./IrohServerEndpoint");
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
* Wire-format types for the sync HTTP API live in
|
|
85
|
-
* {@link ../../core/appendonlystores/network/entryWireCodec}: binary fields
|
|
86
|
-
* (signature, encryptedData, rsaEncryptedPayload) are base64 strings in
|
|
87
|
-
* transit. Client and server share that codec so signed trailing blocks
|
|
88
|
-
* cannot drift off the wire.
|
|
89
|
-
*/
|
|
90
|
-
/**
|
|
91
|
-
* Rate-limit defaults. Auth and sync have separate tiers because a single
|
|
92
|
-
* challenge/authenticate handshake is two requests; with multiple tenants,
|
|
93
|
-
* store kinds, and token renewals the auth budget exhausts quickly if set
|
|
94
|
-
* too low. These can be overridden in config.json under `rateLimits.auth`
|
|
95
|
-
* and `rateLimits.sync`.
|
|
96
|
-
*/
|
|
97
|
-
const DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS = 60000;
|
|
98
|
-
const DEFAULT_AUTH_RATE_LIMIT_MAX = 120;
|
|
99
|
-
const DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS = 60000;
|
|
100
|
-
/**
|
|
101
|
-
* Sync is keyed per authenticated principal, and a client legitimately opens
|
|
102
|
-
* several databases at once: a host like Teacher's Desk reads six, and a cold
|
|
103
|
-
* pass over one of them costs a few hundred batched requests. A budget sized
|
|
104
|
-
* for a single database turns that normal open into a throttled one.
|
|
105
|
-
*/
|
|
106
|
-
const DEFAULT_SYNC_RATE_LIMIT_MAX = 3000;
|
|
107
|
-
/**
|
|
108
|
-
* The global limiter is a coarse net for unauthenticated floods and unknown
|
|
109
|
-
* routes; the per-tier limiters are the real budgets. It runs first, so a
|
|
110
|
-
* global max below a tier max silently replaces that tier — the sync budget
|
|
111
|
-
* was unreachable while this was a hardcoded 500. The default is therefore
|
|
112
|
-
* derived from the tiers, and an override that would shadow one is honoured
|
|
113
|
-
* but flagged at startup rather than quietly applied.
|
|
114
|
-
*/
|
|
115
|
-
const DEFAULT_GLOBAL_RATE_LIMIT_WINDOW_MS = 60000;
|
|
116
|
-
const GLOBAL_RATE_LIMIT_TIER_HEADROOM = 500;
|
|
117
|
-
/**
|
|
118
|
-
* Timestamping gets its own budget rather than sharing the sync one. Sync is
|
|
119
|
-
* chatty and high-volume; sealing a document is a deliberate, rare user action.
|
|
120
|
-
* Sharing a bucket would let ordinary sync traffic starve the seal endpoint, and
|
|
121
|
-
* a runaway seal loop degrade sync.
|
|
122
|
-
*
|
|
123
|
-
* The daily cap matters more than the burst cap: proxying collapses every user
|
|
124
|
-
* onto the server's IP, so one tenant can exhaust an upstream's per-IP allowance
|
|
125
|
-
* (open-tsa.eu publishes 60/minute) and lock out everyone else on this server.
|
|
126
|
-
*/
|
|
127
|
-
const DEFAULT_TIMESTAMP_RATE_LIMIT_WINDOW_MS = 60000;
|
|
128
|
-
const DEFAULT_TIMESTAMP_RATE_LIMIT_MAX = 10;
|
|
129
|
-
const DEFAULT_TIMESTAMP_DAILY_LIMIT_MAX = 500;
|
|
130
|
-
const TIMESTAMP_DAILY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
80
|
+
const types_1 = require("../../server/types");
|
|
81
|
+
const timestampProxy_1 = require("../../server/timestampProxy");
|
|
82
|
+
const rateLimits_1 = require("../../server/rateLimits");
|
|
83
|
+
const MindooDBServerCore_1 = require("../../server/http/MindooDBServerCore");
|
|
84
|
+
const expressAdapter_1 = require("./expressAdapter");
|
|
85
|
+
const types_2 = require("../../server/http/types");
|
|
131
86
|
const DEFAULT_SERVER_SOCKET_TIMEOUT_MS = 120000;
|
|
132
|
-
/** Parse a human-readable size string ("5mb", "1024", "100kb") into bytes. */
|
|
133
|
-
function parseBodySizeLimitToBytes(limit) {
|
|
134
|
-
const normalized = limit.trim().toLowerCase();
|
|
135
|
-
if (!normalized) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
if (/^\d+$/.test(normalized)) {
|
|
139
|
-
return Number(normalized);
|
|
140
|
-
}
|
|
141
|
-
const match = normalized.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
|
|
142
|
-
if (!match) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
const value = Number(match[1]);
|
|
146
|
-
const unit = match[2];
|
|
147
|
-
const multiplier = unit === "b"
|
|
148
|
-
? 1
|
|
149
|
-
: unit === "kb"
|
|
150
|
-
? 1024
|
|
151
|
-
: unit === "mb"
|
|
152
|
-
? 1024 * 1024
|
|
153
|
-
: 1024 * 1024 * 1024;
|
|
154
|
-
return Math.round(value * multiplier);
|
|
155
|
-
}
|
|
156
87
|
class MindooDBServer {
|
|
157
88
|
/**
|
|
158
89
|
* @param dataDir Root directory for on-disk tenant data and server identity
|
|
@@ -171,9 +102,9 @@ class MindooDBServer {
|
|
|
171
102
|
this.staticDir = staticDir;
|
|
172
103
|
this.configPath = configPath ?? path_1.default.join(dataDir, "config.json");
|
|
173
104
|
this.jsonBodyLimit = process.env.MINDOODB_JSON_BODY_LIMIT?.trim() || "5mb";
|
|
174
|
-
|
|
175
|
-
this.tsaProviders = (0, timestampProxy_1.parseTsaProviders)(process.env[
|
|
176
|
-
allowInsecureUrls:
|
|
105
|
+
const allowInsecureSyncUrls = /^(1|true)$/i.test(process.env[types_1.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? "");
|
|
106
|
+
this.tsaProviders = (0, timestampProxy_1.parseTsaProviders)(process.env[types_1.ENV_VARS.TSA_PROVIDERS], {
|
|
107
|
+
allowInsecureUrls: allowInsecureSyncUrls,
|
|
177
108
|
});
|
|
178
109
|
if (this.tsaProviders.length > 0) {
|
|
179
110
|
console.log(`[MindooDBServer] RFC 3161 timestamp proxy enabled with providers: ${this.tsaProviders
|
|
@@ -191,7 +122,7 @@ class MindooDBServer {
|
|
|
191
122
|
this.deviceDiscovery = new DeviceDiscoveryService_1.DeviceDiscoveryService(this.tenantManager, cryptoAdapter);
|
|
192
123
|
this.cluster = new ClusterManager_1.ClusterManager({
|
|
193
124
|
cryptoAdapter,
|
|
194
|
-
dataDir,
|
|
125
|
+
auditStorage: new FileClusterAuditStorage_1.FileClusterAuditStorage(dataDir),
|
|
195
126
|
eventBus: this.syncEventBus,
|
|
196
127
|
listTrustedServers: () => this.tenantManager.listTrustedServers(),
|
|
197
128
|
localServerName: () => this.tenantManager.getServerPublicInfo()?.name ?? null,
|
|
@@ -205,6 +136,7 @@ class MindooDBServer {
|
|
|
205
136
|
onDirectoryPulled: (tenantId) => this.tenantManager.executePendingPurges(tenantId),
|
|
206
137
|
localRole: this.serverConfig.cluster?.role,
|
|
207
138
|
getIrohStreamIO: () => this.irohEndpoint.getStreamIO(),
|
|
139
|
+
openIrohLink: (io, ticket) => new IrohPeerLink_1.IrohPeerLink(io, ticket),
|
|
208
140
|
});
|
|
209
141
|
// Peer tokens are accepted by the tenant sync routes so a peer can mirror
|
|
210
142
|
// the encrypted stores through the ordinary protocol; the store bypasses
|
|
@@ -225,6 +157,24 @@ class MindooDBServer {
|
|
|
225
157
|
},
|
|
226
158
|
trustedWitnessResolver: (receivedByPublicKey) => this.cluster.auth.findTrustedServerByKey(receivedByPublicKey) !== null,
|
|
227
159
|
});
|
|
160
|
+
this.core = new MindooDBServerCore_1.MindooDBServerCore({
|
|
161
|
+
tenantManager: this.tenantManager,
|
|
162
|
+
systemAdminAuth: this.systemAdminAuth,
|
|
163
|
+
capabilityMatcher: this.capabilityMatcher,
|
|
164
|
+
deviceDiscovery: this.deviceDiscovery,
|
|
165
|
+
syncEvents: this.syncEventBus,
|
|
166
|
+
configRepository: new config_1.FileServerConfigRepository(this.configPath),
|
|
167
|
+
getServerConfig: () => this.serverConfig,
|
|
168
|
+
reloadConfig: (newConfig) => this.reloadConfig(newConfig),
|
|
169
|
+
tsaProviders: this.tsaProviders,
|
|
170
|
+
jsonBodyLimit: this.jsonBodyLimit,
|
|
171
|
+
allowInsecureSyncUrls,
|
|
172
|
+
cluster: {
|
|
173
|
+
mirrorsForTenant: (tenantId) => this.cluster.mirrorsForTenant(tenantId),
|
|
174
|
+
syncReplicators: () => this.cluster.syncReplicators(),
|
|
175
|
+
resolvePeerName: (token) => this.cluster.auth.validateToken(token).then((payload) => payload?.sub),
|
|
176
|
+
},
|
|
177
|
+
});
|
|
228
178
|
const principalCount = Object.values(this.serverConfig.capabilities).reduce((sum, arr) => sum + arr.length, 0);
|
|
229
179
|
if (principalCount === 0) {
|
|
230
180
|
console.log(`[MindooDBServer] WARNING: No system admin principals configured. All /system/* endpoints are locked down.`);
|
|
@@ -328,10 +278,7 @@ class MindooDBServer {
|
|
|
328
278
|
config: irohConfig,
|
|
329
279
|
handler: (0, IrohServerRpc_1.createMindooDBServerIrohHandler)({
|
|
330
280
|
getServerPublicInfo: () => this.tenantManager.getServerPublicInfo(),
|
|
331
|
-
getJsonBodyLimit: () => (
|
|
332
|
-
limit: this.jsonBodyLimit,
|
|
333
|
-
bytes: this.jsonBodyLimitBytes,
|
|
334
|
-
}),
|
|
281
|
+
getJsonBodyLimit: () => this.core.getJsonBodyLimit(),
|
|
335
282
|
getClusterRole: () => this.serverConfig.cluster?.role ?? "peer",
|
|
336
283
|
listTenantPublicInfosFingerprints: (tenantId) => this.tenantManager.listTenantPublicInfosFingerprints(tenantId),
|
|
337
284
|
getAuthService: (tenantId) => this.tenantManager.getAuthService(tenantId),
|
|
@@ -369,6 +316,10 @@ class MindooDBServer {
|
|
|
369
316
|
console.error("[MindooDBServer] Failed to start Iroh listener:", error);
|
|
370
317
|
}
|
|
371
318
|
}
|
|
319
|
+
/** Every limiter tier, resolved against the current config.json. */
|
|
320
|
+
rateLimitPolicy() {
|
|
321
|
+
return (0, rateLimits_1.resolveServerRateLimits)(this.serverConfig.rateLimits);
|
|
322
|
+
}
|
|
372
323
|
/**
|
|
373
324
|
* Global middleware stack applied to every request, in order:
|
|
374
325
|
* 1. helmet — security headers (CSP, X-Frame-Options, etc.)
|
|
@@ -412,7 +363,6 @@ class MindooDBServer {
|
|
|
412
363
|
next();
|
|
413
364
|
});
|
|
414
365
|
}
|
|
415
|
-
/** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
|
|
416
366
|
/**
|
|
417
367
|
* Teach Express which upstream hops to trust for `X-Forwarded-For`, so
|
|
418
368
|
* `req.ip` is the client rather than the proxy. Every rate limiter keys on
|
|
@@ -427,7 +377,7 @@ class MindooDBServer {
|
|
|
427
377
|
* list over `true`, which trusts the whole chain.
|
|
428
378
|
*/
|
|
429
379
|
applyTrustProxySetting() {
|
|
430
|
-
const raw = process.env[
|
|
380
|
+
const raw = process.env[types_1.ENV_VARS.TRUST_PROXY]?.trim();
|
|
431
381
|
if (!raw || /^(0|false)$/i.test(raw)) {
|
|
432
382
|
return;
|
|
433
383
|
}
|
|
@@ -447,6 +397,7 @@ class MindooDBServer {
|
|
|
447
397
|
console.log(`[MindooDBServer] trust proxy set to ${JSON.stringify(value)}`);
|
|
448
398
|
}
|
|
449
399
|
}
|
|
400
|
+
/** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
|
|
450
401
|
readCorsOriginsFromEnv() {
|
|
451
402
|
const raw = process.env.MINDOODB_CORS_ORIGIN?.trim();
|
|
452
403
|
if (!raw) {
|
|
@@ -471,6 +422,7 @@ class MindooDBServer {
|
|
|
471
422
|
* /:tenantId/* — per-tenant sync & auth API
|
|
472
423
|
*/
|
|
473
424
|
setupRoutes() {
|
|
425
|
+
const core = this.core;
|
|
474
426
|
this.app.get("/", (req, res) => {
|
|
475
427
|
if (this.staticDir && (0, fs_1.existsSync)(path_1.default.join(this.staticDir, "index.html"))) {
|
|
476
428
|
res.redirect(302, "/statics/index.html");
|
|
@@ -479,99 +431,24 @@ class MindooDBServer {
|
|
|
479
431
|
res.status(404).json({ error: "Not found" });
|
|
480
432
|
}
|
|
481
433
|
});
|
|
482
|
-
this.app.get("/health", (
|
|
483
|
-
res.json({ status: "ok", timestamp: Date.now() });
|
|
484
|
-
});
|
|
434
|
+
this.app.get("/health", (0, expressAdapter_1.route)(() => core.health()));
|
|
485
435
|
// Server-wide device discovery (before /:tenantId so "device" is not a tenant id).
|
|
486
436
|
// Aggressive rate limit: challenge+discover scans every local tenant.
|
|
437
|
+
const deviceLimit = this.rateLimitPolicy().device;
|
|
487
438
|
const deviceDiscoveryRateLimit = (0, express_rate_limit_1.default)({
|
|
488
|
-
windowMs:
|
|
489
|
-
max:
|
|
439
|
+
windowMs: deviceLimit.windowMs,
|
|
440
|
+
max: deviceLimit.max,
|
|
490
441
|
standardHeaders: true,
|
|
491
442
|
legacyHeaders: false,
|
|
492
443
|
message: { error: "Too many device discovery requests, please try again later" },
|
|
493
444
|
});
|
|
494
|
-
this.app.post("/device/challenge", deviceDiscoveryRateLimit,
|
|
495
|
-
this.app.post("/device/discover", deviceDiscoveryRateLimit,
|
|
445
|
+
this.app.post("/device/challenge", deviceDiscoveryRateLimit, (0, expressAdapter_1.route)((req) => core.deviceChallenge(req)));
|
|
446
|
+
this.app.post("/device/discover", deviceDiscoveryRateLimit, (0, expressAdapter_1.route)((req) => core.deviceDiscover(req)));
|
|
496
447
|
// .well-known endpoints are unauthenticated so clients can discover
|
|
497
448
|
// server identity and tenant availability before attempting to authenticate.
|
|
498
|
-
this.app.get("/.well-known/mindoodb-server-info", (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
res.status(503).json({ error: "Server identity not initialized" });
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
res.json({
|
|
505
|
-
...info,
|
|
506
|
-
// Its own mesh role, so `add-to-network` can propagate it into the
|
|
507
|
-
// other side's trusted-servers entry instead of asking the operator to
|
|
508
|
-
// repeat it on every node.
|
|
509
|
-
clusterRole: this.serverConfig.cluster?.role ?? "peer",
|
|
510
|
-
maxJsonRequestBodyLimit: this.jsonBodyLimit,
|
|
511
|
-
maxJsonRequestBodyBytes: this.jsonBodyLimitBytes ?? undefined,
|
|
512
|
-
// Lets a client decide between calling a browser-reachable TSA directly
|
|
513
|
-
// and routing through this server, per tenant and without a probe.
|
|
514
|
-
timestamping: {
|
|
515
|
-
enabled: this.tsaProviders.length > 0,
|
|
516
|
-
providers: (0, timestampProxy_1.toPublicTsaProviders)(this.tsaProviders),
|
|
517
|
-
},
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
|
-
/**
|
|
521
|
-
* Mirrors of one tenant, for client failover.
|
|
522
|
-
*
|
|
523
|
-
* Scoped to the tenant in the query on purpose: a client asking about
|
|
524
|
-
* tenant A must not learn which other tenants this server replicates, or
|
|
525
|
-
* to whom. Unauthenticated like the other discovery endpoints — a client
|
|
526
|
-
* needs it precisely when it cannot reach a server to authenticate with.
|
|
527
|
-
*/
|
|
528
|
-
this.app.get("/.well-known/mindoodb-cluster", (req, res) => {
|
|
529
|
-
const tenantId = typeof req.query.tenantId === "string" ? req.query.tenantId.toLowerCase() : "";
|
|
530
|
-
if (!tenantId) {
|
|
531
|
-
res.status(400).json({ error: "tenantId query parameter is required" });
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
try {
|
|
535
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
536
|
-
}
|
|
537
|
-
catch {
|
|
538
|
-
res.status(400).json({ error: "Invalid tenantId format" });
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
542
|
-
res.status(404).json({ error: "Tenant not found on server" });
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
res.json({ tenantId, mirrors: this.cluster.mirrorsForTenant(tenantId) });
|
|
546
|
-
});
|
|
547
|
-
this.app.get("/.well-known/mindoodb-tenants/:tenantId/publicinfos-fingerprints", async (req, res) => {
|
|
548
|
-
const tenantId = req.params.tenantId.toLowerCase();
|
|
549
|
-
try {
|
|
550
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
551
|
-
const fingerprints = await this.tenantManager.listTenantPublicInfosFingerprints(tenantId);
|
|
552
|
-
if (fingerprints.length === 0) {
|
|
553
|
-
res.status(404).json({ error: "Tenant not found on server" });
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
const response = {
|
|
557
|
-
tenantId,
|
|
558
|
-
fingerprints,
|
|
559
|
-
};
|
|
560
|
-
res.json(response);
|
|
561
|
-
}
|
|
562
|
-
catch (error) {
|
|
563
|
-
if (error instanceof validation_1.ValidationError) {
|
|
564
|
-
res.status(400).json({ error: error.message });
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
if (error instanceof Error && error.message.includes("not found")) {
|
|
568
|
-
res.status(404).json({ error: "Tenant not found on server" });
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
console.error("[MindooDBServer] Error reading tenant publicInfos fingerprints:", error);
|
|
572
|
-
res.status(500).json({ error: "Failed to read tenant publicInfos fingerprints" });
|
|
573
|
-
}
|
|
574
|
-
});
|
|
449
|
+
this.app.get("/.well-known/mindoodb-server-info", (0, expressAdapter_1.route)(() => core.serverInfo()));
|
|
450
|
+
this.app.get("/.well-known/mindoodb-cluster", (0, expressAdapter_1.route)((req) => core.clusterMirrors(req)));
|
|
451
|
+
this.app.get("/.well-known/mindoodb-tenants/:tenantId/publicinfos-fingerprints", (0, expressAdapter_1.route)((req) => core.publicInfosFingerprints(req)));
|
|
575
452
|
// Static file serving
|
|
576
453
|
if (this.staticDir) {
|
|
577
454
|
const resolvedStaticDir = path_1.default.resolve(this.staticDir);
|
|
@@ -592,9 +469,10 @@ class MindooDBServer {
|
|
|
592
469
|
if (!(0, SystemIpAllowlist_1.isSystemIpAllowListDisabled)(systemIpRaw)) {
|
|
593
470
|
console.log(`[MindooDBServer] MINDOODB_ADMIN_ALLOWED_IPS is set — /system/* limited to: ${systemIpRaw}`);
|
|
594
471
|
}
|
|
472
|
+
const systemLimit = this.rateLimitPolicy().system;
|
|
595
473
|
const systemRateLimit = (0, express_rate_limit_1.default)({
|
|
596
|
-
windowMs:
|
|
597
|
-
max:
|
|
474
|
+
windowMs: systemLimit.windowMs,
|
|
475
|
+
max: systemLimit.max,
|
|
598
476
|
standardHeaders: true,
|
|
599
477
|
legacyHeaders: false,
|
|
600
478
|
message: { error: "Too many system requests, please try again later" },
|
|
@@ -604,9 +482,10 @@ class MindooDBServer {
|
|
|
604
482
|
// would lock out the operator's own cluster, and 30 req/min would throttle
|
|
605
483
|
// replication to a crawl. They are protected by the peer JWT, which is only
|
|
606
484
|
// issued to a key in trusted-servers.json.
|
|
485
|
+
const peerLimit = this.rateLimitPolicy().peer;
|
|
607
486
|
this.app.use("/system/peer", (0, express_rate_limit_1.default)({
|
|
608
|
-
windowMs:
|
|
609
|
-
max:
|
|
487
|
+
windowMs: peerLimit.windowMs,
|
|
488
|
+
max: peerLimit.max,
|
|
610
489
|
standardHeaders: true,
|
|
611
490
|
legacyHeaders: false,
|
|
612
491
|
message: { error: "Too many peer requests, please try again later" },
|
|
@@ -619,93 +498,12 @@ class MindooDBServer {
|
|
|
619
498
|
const systemRouter = (0, express_1.Router)();
|
|
620
499
|
this.setupSystemRoutes(systemRouter);
|
|
621
500
|
this.app.use("/system", SystemIpAllowlist_1.systemIpAllowlistMiddleware, systemRateLimit, systemRouter);
|
|
622
|
-
// Tenant-scoped routes:
|
|
501
|
+
// Tenant-scoped routes: the tenant guard validates the :tenantId param
|
|
623
502
|
// and rejects unknown tenants before any handler runs.
|
|
624
|
-
this.app.use("/:tenantId",
|
|
503
|
+
this.app.use("/:tenantId", (0, expressAdapter_1.guard)((req) => core.tenantGuard(req)), this.createTenantRouter());
|
|
625
504
|
// Express error handler — must be registered last and have the 4-arg signature
|
|
626
505
|
this.app.use(this.errorHandler.bind(this));
|
|
627
506
|
}
|
|
628
|
-
// ==================== System Admin Auth Middleware ====================
|
|
629
|
-
/**
|
|
630
|
-
* JWT-based middleware for /system/* routes (excluding /system/auth/*).
|
|
631
|
-
* Extracts and validates the bearer token, then checks capabilities
|
|
632
|
-
* for the current (method, path, username, publicsignkey).
|
|
633
|
-
*
|
|
634
|
-
* `DELETE /system/tenants/:tenantId` is also allowed when the JWT signing
|
|
635
|
-
* key is that tenant's registered `adminSigningPublicKey` (own-tenant
|
|
636
|
-
* delete). Sub-resources and other tenants stay capability-gated.
|
|
637
|
-
*/
|
|
638
|
-
async systemAdminMiddleware(req, res, next) {
|
|
639
|
-
const auth = req.headers.authorization;
|
|
640
|
-
if (!auth || !auth.startsWith("Bearer ")) {
|
|
641
|
-
res.status(401).json({ error: "Missing or invalid Authorization header" });
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
const token = auth.substring(7);
|
|
645
|
-
const payload = await this.systemAdminAuth.validateToken(token);
|
|
646
|
-
if (!payload) {
|
|
647
|
-
res.status(401).json({ error: "Invalid or expired token" });
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
// The original request path relative to the /system mount is in req.path,
|
|
651
|
-
// but we need the full path for capability matching.
|
|
652
|
-
const fullPath = `/system${req.path}`;
|
|
653
|
-
const method = req.method.toUpperCase();
|
|
654
|
-
const capabilityAuthorized = this.capabilityMatcher.isAuthorized(method, fullPath, payload.sub, payload.publicsignkey);
|
|
655
|
-
const ownerDeleteAuthorized = !capabilityAuthorized &&
|
|
656
|
-
this.isOwnTenantDeleteAuthorized(method, fullPath, payload.publicsignkey);
|
|
657
|
-
if (!capabilityAuthorized && !ownerDeleteAuthorized) {
|
|
658
|
-
res.status(403).json({ error: "Forbidden: insufficient capabilities" });
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
req.systemAdmin = {
|
|
662
|
-
username: payload.sub,
|
|
663
|
-
publicsignkey: payload.publicsignkey,
|
|
664
|
-
};
|
|
665
|
-
next();
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* Owner bypass for deleting the caller's own tenant. Matches only
|
|
669
|
-
* `DELETE /system/tenants/:tenantId` (no sub-resource) and requires the
|
|
670
|
-
* JWT public key to equal that tenant's stored admin signing key.
|
|
671
|
-
*/
|
|
672
|
-
isOwnTenantDeleteAuthorized(method, fullPath, publicsignkey) {
|
|
673
|
-
if (method !== "DELETE") {
|
|
674
|
-
return false;
|
|
675
|
-
}
|
|
676
|
-
const tenantId = (0, CapabilityMatcher_1.parseSystemTenantItemPath)(fullPath);
|
|
677
|
-
if (!tenantId) {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
try {
|
|
681
|
-
(0, validation_1.validateTenantIdFormat)(tenantId);
|
|
682
|
-
}
|
|
683
|
-
catch {
|
|
684
|
-
return false;
|
|
685
|
-
}
|
|
686
|
-
return this.tenantManager.isTenantAdminSigningKeyFor(tenantId, publicsignkey);
|
|
687
|
-
}
|
|
688
|
-
// ==================== Tenant Middleware ====================
|
|
689
|
-
tenantMiddleware(req, res, next) {
|
|
690
|
-
const rawTenantId = req.params.tenantId?.toLowerCase();
|
|
691
|
-
if (!rawTenantId) {
|
|
692
|
-
res.status(400).json({ error: "Tenant ID required" });
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
try {
|
|
696
|
-
(0, validation_1.validateTenantIdFormat)(rawTenantId);
|
|
697
|
-
}
|
|
698
|
-
catch {
|
|
699
|
-
res.status(400).json({ error: "Invalid tenant ID format" });
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
if (!this.tenantManager.tenantExists(rawTenantId)) {
|
|
703
|
-
res.status(404).json({ error: this.formatTenantNotFoundOnServerError(rawTenantId) });
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
req.tenantId = rawTenantId;
|
|
707
|
-
next();
|
|
708
|
-
}
|
|
709
507
|
// ==================== System Routes ====================
|
|
710
508
|
/**
|
|
711
509
|
* Wire up all /system/* routes. Auth endpoints sit outside the JWT
|
|
@@ -714,379 +512,39 @@ class MindooDBServer {
|
|
|
714
512
|
* passes through the CapabilityMatcher for fine-grained authorization.
|
|
715
513
|
*/
|
|
716
514
|
setupSystemRoutes(router) {
|
|
515
|
+
const core = this.core;
|
|
717
516
|
const challengeRateLimit = this.createAuthRateLimit("system", "challenge");
|
|
718
517
|
const authenticateRateLimit = this.createAuthRateLimit("system", "authenticate");
|
|
719
|
-
router.post("/auth/challenge", challengeRateLimit,
|
|
720
|
-
router.post("/auth/authenticate", authenticateRateLimit,
|
|
721
|
-
|
|
518
|
+
router.post("/auth/challenge", challengeRateLimit, (0, expressAdapter_1.route)((req) => core.systemChallenge(req)));
|
|
519
|
+
router.post("/auth/authenticate", authenticateRateLimit, (0, expressAdapter_1.route)((req) => core.systemAuthenticate(req)));
|
|
520
|
+
// Capabilities are matched against the full path below /system. req.path
|
|
521
|
+
// is relative to the mount the middleware runs under, so a middleware
|
|
522
|
+
// mounted below /system names its own prefix.
|
|
523
|
+
const authMiddlewareFor = (prefix) => (req, res, next) => (0, expressAdapter_1.guard)((serverRequest) => core.systemAdminGuard(serverRequest, `${prefix}${req.path}`))(req, res, next);
|
|
524
|
+
const authMiddleware = authMiddlewareFor("/system");
|
|
722
525
|
// Cluster administration. Behind the same JWT + capability middleware as
|
|
723
526
|
// the rest of /system, so config.json can grant an auditor principal the
|
|
724
527
|
// GET routes without granting the POST/PATCH/DELETE ones.
|
|
725
|
-
router.use("/cluster",
|
|
528
|
+
router.use("/cluster", authMiddlewareFor("/system/cluster"), (0, routes_1.createClusterRouter)({
|
|
726
529
|
cluster: this.cluster,
|
|
727
530
|
listTrustedServers: () => this.tenantManager.listTrustedServers(),
|
|
728
531
|
saveTrustedServer: (peer) => this.tenantManager.saveTrustedServer(peer),
|
|
729
532
|
removeTrustedServer: (name) => this.tenantManager.removeTrustedServer(name),
|
|
730
533
|
}));
|
|
731
534
|
// Tenant CRUD
|
|
732
|
-
router.get("/tenants", authMiddleware, (
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
res.json(response);
|
|
737
|
-
}
|
|
738
|
-
catch (error) {
|
|
739
|
-
console.error("[MindooDBServer] Error listing tenants:", error);
|
|
740
|
-
res.status(500).json({ error: "Failed to list tenants" });
|
|
741
|
-
}
|
|
742
|
-
});
|
|
743
|
-
// Tenant registration is idempotent: re-POSTing with the same tenantId
|
|
744
|
-
// and matching $publicinfos key returns 200 instead of 409.
|
|
745
|
-
router.post("/tenants/:tenantId", authMiddleware, async (req, res) => {
|
|
746
|
-
try {
|
|
747
|
-
const tenantId = req.params.tenantId.toLowerCase();
|
|
748
|
-
try {
|
|
749
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
750
|
-
}
|
|
751
|
-
catch (validationError) {
|
|
752
|
-
res.status(400).json({
|
|
753
|
-
error: validationError instanceof validation_1.ValidationError
|
|
754
|
-
? validationError.message
|
|
755
|
-
: "Invalid tenantId format",
|
|
756
|
-
});
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
const request = {
|
|
760
|
-
...req.body,
|
|
761
|
-
tenantId,
|
|
762
|
-
};
|
|
763
|
-
if (!request.adminSigningPublicKey) {
|
|
764
|
-
res.status(400).json({ error: "adminSigningPublicKey is required" });
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
if (!request.adminEncryptionPublicKey) {
|
|
768
|
-
res.status(400).json({ error: "adminEncryptionPublicKey is required" });
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
if (request.adminUsername !== undefined) {
|
|
772
|
-
(0, validation_1.validateUsername)(request.adminUsername);
|
|
773
|
-
}
|
|
774
|
-
(0, validation_1.validateStringLength)(request.adminSigningPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "adminSigningPublicKey");
|
|
775
|
-
(0, validation_1.validateStringLength)(request.adminEncryptionPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "adminEncryptionPublicKey");
|
|
776
|
-
if (!request.encryptedPublicInfosKey && !request.publicInfosKey) {
|
|
777
|
-
res.status(400).json({ error: "encryptedPublicInfosKey or publicInfosKey is required" });
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (request.publicInfosKey !== undefined) {
|
|
781
|
-
(0, validation_1.validateStringLength)(request.publicInfosKey, validation_1.MAX_PEM_KEY_LENGTH, "publicInfosKey");
|
|
782
|
-
}
|
|
783
|
-
if (request.encryptedPublicInfosKey !== undefined) {
|
|
784
|
-
(0, validation_1.validateStringLength)(request.encryptedPublicInfosKey, validation_1.MAX_PEM_KEY_LENGTH, "encryptedPublicInfosKey");
|
|
785
|
-
}
|
|
786
|
-
const result = await this.tenantManager.registerTenant(request);
|
|
787
|
-
const response = {
|
|
788
|
-
success: true,
|
|
789
|
-
tenantId: result.context.tenantId,
|
|
790
|
-
created: result.created,
|
|
791
|
-
message: result.created
|
|
792
|
-
? `Tenant ${result.context.tenantId} registered successfully`
|
|
793
|
-
: `Tenant ${result.context.tenantId} already exists with matching $publicinfos key`,
|
|
794
|
-
};
|
|
795
|
-
res.status(result.created ? 201 : 200).json(response);
|
|
796
|
-
}
|
|
797
|
-
catch (error) {
|
|
798
|
-
if (error instanceof validation_1.ValidationError) {
|
|
799
|
-
res.status(400).json({ error: error.message });
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
if (error instanceof Error && error.message.includes("different $publicinfos key")) {
|
|
803
|
-
res.status(409).json({ error: error.message });
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
|
-
console.error("[MindooDBServer] Error registering tenant:", error);
|
|
807
|
-
res.status(500).json({ error: "Failed to register tenant" });
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
router.put("/tenants/:tenantId", authMiddleware, (req, res) => {
|
|
811
|
-
try {
|
|
812
|
-
const tenantId = req.params.tenantId.toLowerCase();
|
|
813
|
-
try {
|
|
814
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
815
|
-
}
|
|
816
|
-
catch {
|
|
817
|
-
res.status(400).json({ error: "Invalid tenantId format" });
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
821
|
-
res.status(404).json({ error: "Tenant not found" });
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
this.tenantManager.updateTenantConfig(tenantId, req.body);
|
|
825
|
-
res.json({ success: true, message: `Tenant ${tenantId} updated` });
|
|
826
|
-
}
|
|
827
|
-
catch (error) {
|
|
828
|
-
if (error instanceof validation_1.ValidationError) {
|
|
829
|
-
res.status(400).json({ error: error.message });
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
console.error("[MindooDBServer] Error updating tenant:", error);
|
|
833
|
-
res.status(500).json({ error: "Failed to update tenant" });
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
router.delete("/tenants/:tenantId", authMiddleware, async (req, res) => {
|
|
837
|
-
try {
|
|
838
|
-
const tenantId = req.params.tenantId.toLowerCase();
|
|
839
|
-
try {
|
|
840
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
841
|
-
}
|
|
842
|
-
catch {
|
|
843
|
-
res.status(400).json({ error: "Invalid tenantId format" });
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
847
|
-
res.status(404).json({ error: "Tenant not found" });
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
await this.tenantManager.removeTenant(tenantId);
|
|
851
|
-
res.json({ success: true, message: `Tenant ${tenantId} removed` });
|
|
852
|
-
}
|
|
853
|
-
catch (error) {
|
|
854
|
-
console.error("[MindooDBServer] Error removing tenant:", error);
|
|
855
|
-
res.status(500).json({ error: "Failed to remove tenant" });
|
|
856
|
-
}
|
|
857
|
-
});
|
|
535
|
+
router.get("/tenants", authMiddleware, (0, expressAdapter_1.route)(() => core.listTenants()));
|
|
536
|
+
router.post("/tenants/:tenantId", authMiddleware, (0, expressAdapter_1.route)((req) => core.registerTenant(req)));
|
|
537
|
+
router.put("/tenants/:tenantId", authMiddleware, (0, expressAdapter_1.route)((req) => core.updateTenant(req)));
|
|
538
|
+
router.delete("/tenants/:tenantId", authMiddleware, (0, expressAdapter_1.route)((req) => core.deleteTenant(req)));
|
|
858
539
|
// Trusted server management
|
|
859
|
-
router.get("/trusted-servers", authMiddleware, (
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
}
|
|
863
|
-
catch (error) {
|
|
864
|
-
console.error("[MindooDBServer] Error listing trusted servers:", error);
|
|
865
|
-
res.status(500).json({ error: "Failed to list trusted servers" });
|
|
866
|
-
}
|
|
867
|
-
});
|
|
868
|
-
router.post("/trusted-servers", authMiddleware, (req, res) => {
|
|
869
|
-
try {
|
|
870
|
-
const { name, signingPublicKey, encryptionPublicKey, url, direction, role, attachments } = req.body;
|
|
871
|
-
if (!name || !signingPublicKey || !encryptionPublicKey) {
|
|
872
|
-
res.status(400).json({ error: "name, signingPublicKey, and encryptionPublicKey are required" });
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
(0, validation_1.validateStringLength)(name, 256, "name");
|
|
876
|
-
(0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "signingPublicKey");
|
|
877
|
-
(0, validation_1.validateStringLength)(encryptionPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "encryptionPublicKey");
|
|
878
|
-
const server = { name, signingPublicKey, encryptionPublicKey };
|
|
879
|
-
// Replication settings are optional: an entry without a url is a trust
|
|
880
|
-
// relationship only (inbound peers may authenticate, we never dial).
|
|
881
|
-
if (url !== undefined) {
|
|
882
|
-
(0, validation_1.validateStringLength)(url, 2048, "url");
|
|
883
|
-
const allowInsecure = /^(1|true)$/i.test(process.env[types_4.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? "");
|
|
884
|
-
try {
|
|
885
|
-
(0, urlSafety_1.assertSafeSyncUrl)(url, {
|
|
886
|
-
requireHttps: !allowInsecure,
|
|
887
|
-
allowPrivate: allowInsecure,
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
catch (e) {
|
|
891
|
-
if (e instanceof urlSafety_1.UnsafeUrlError) {
|
|
892
|
-
res.status(400).json({ error: e.message });
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
throw e;
|
|
896
|
-
}
|
|
897
|
-
server.url = url;
|
|
898
|
-
}
|
|
899
|
-
for (const [field, value, guard] of [
|
|
900
|
-
["direction", direction, types_1.isPeerSyncDirection],
|
|
901
|
-
["role", role, types_1.isPeerRole],
|
|
902
|
-
["attachments", attachments, types_1.isPeerAttachmentMode],
|
|
903
|
-
]) {
|
|
904
|
-
if (value === undefined)
|
|
905
|
-
continue;
|
|
906
|
-
if (!guard(value)) {
|
|
907
|
-
res.status(400).json({ error: `Invalid ${field}: ${String(value)}` });
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
Object.assign(server, { [field]: value });
|
|
911
|
-
}
|
|
912
|
-
this.tenantManager.addTrustedServer(server);
|
|
913
|
-
this.cluster.syncReplicators();
|
|
914
|
-
res.status(201).json({ success: true, message: `Trusted server "${name}" added` });
|
|
915
|
-
}
|
|
916
|
-
catch (error) {
|
|
917
|
-
if (error instanceof validation_1.ValidationError) {
|
|
918
|
-
res.status(400).json({ error: error.message });
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
if (error instanceof Error && error.message.includes("already exists")) {
|
|
922
|
-
res.status(409).json({ error: error.message });
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
console.error("[MindooDBServer] Error adding trusted server:", error);
|
|
926
|
-
res.status(500).json({ error: "Failed to add trusted server" });
|
|
927
|
-
}
|
|
928
|
-
});
|
|
929
|
-
router.delete("/trusted-servers/:serverName", authMiddleware, (req, res) => {
|
|
930
|
-
try {
|
|
931
|
-
const serverName = req.params.serverName;
|
|
932
|
-
(0, validation_1.validateStringLength)(serverName, 256, "serverName");
|
|
933
|
-
const removed = this.tenantManager.removeTrustedServer(serverName);
|
|
934
|
-
if (!removed) {
|
|
935
|
-
res.status(404).json({ error: "Trusted server not found" });
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
// Revoking trust must also stop replicating with that server.
|
|
939
|
-
this.cluster.syncReplicators();
|
|
940
|
-
res.json({ success: true, message: `Trusted server "${serverName}" removed` });
|
|
941
|
-
}
|
|
942
|
-
catch (error) {
|
|
943
|
-
if (error instanceof validation_1.ValidationError) {
|
|
944
|
-
res.status(400).json({ error: error.message });
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
console.error("[MindooDBServer] Error removing trusted server:", error);
|
|
948
|
-
res.status(500).json({ error: "Failed to remove trusted server" });
|
|
949
|
-
}
|
|
950
|
-
});
|
|
540
|
+
router.get("/trusted-servers", authMiddleware, (0, expressAdapter_1.route)(() => core.listTrustedServers()));
|
|
541
|
+
router.post("/trusted-servers", authMiddleware, (0, expressAdapter_1.route)((req) => core.addTrustedServer(req)));
|
|
542
|
+
router.delete("/trusted-servers/:serverName", authMiddleware, (0, expressAdapter_1.route)((req) => core.removeTrustedServer(req)));
|
|
951
543
|
// Runtime config management
|
|
952
|
-
router.get("/config/backups", authMiddleware, (
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
catch (error) {
|
|
957
|
-
console.error("[MindooDBServer] Error listing config backups:", error);
|
|
958
|
-
res.status(500).json({ error: "Failed to list config backups" });
|
|
959
|
-
}
|
|
960
|
-
});
|
|
961
|
-
router.get("/config/backups/:backupFile", authMiddleware, (req, res) => {
|
|
962
|
-
try {
|
|
963
|
-
const backupFile = decodeURIComponent(req.params.backupFile);
|
|
964
|
-
if (!(0, config_1.isConfigBackupFilename)(backupFile)) {
|
|
965
|
-
res.status(400).json({ error: "Invalid config backup filename" });
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
const config = (0, config_1.loadConfigBackup)(this.configPath, backupFile);
|
|
969
|
-
res.json({ file: backupFile, config });
|
|
970
|
-
}
|
|
971
|
-
catch (error) {
|
|
972
|
-
if (error instanceof Error && error.message.startsWith("Config backup not found:")) {
|
|
973
|
-
res.status(404).json({ error: "Config backup not found" });
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
if (error instanceof Error && error.message.startsWith("Invalid config backup filename:")) {
|
|
977
|
-
res.status(400).json({ error: "Invalid config backup filename" });
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
console.error("[MindooDBServer] Error reading config backup:", error);
|
|
981
|
-
res.status(500).json({ error: "Failed to read config backup" });
|
|
982
|
-
}
|
|
983
|
-
});
|
|
984
|
-
router.get("/config", authMiddleware, (req, res) => {
|
|
985
|
-
res.json(this.serverConfig);
|
|
986
|
-
});
|
|
987
|
-
router.put("/config", authMiddleware, this.handleUpdateConfig.bind(this));
|
|
988
|
-
}
|
|
989
|
-
// ==================== System Auth Handlers ====================
|
|
990
|
-
async handleSystemChallenge(req, res) {
|
|
991
|
-
try {
|
|
992
|
-
const { username, publicsignkey } = req.body;
|
|
993
|
-
if (!username || typeof username !== "string") {
|
|
994
|
-
res.status(400).json({ error: "username is required" });
|
|
995
|
-
return;
|
|
996
|
-
}
|
|
997
|
-
if (!publicsignkey || typeof publicsignkey !== "string") {
|
|
998
|
-
res.status(400).json({ error: "publicsignkey is required" });
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
(0, validation_1.validateUsername)(username);
|
|
1002
|
-
(0, validation_1.validateStringLength)(publicsignkey, validation_1.MAX_PEM_KEY_LENGTH, "publicsignkey");
|
|
1003
|
-
const challenge = await this.systemAdminAuth.generateChallenge(username, publicsignkey);
|
|
1004
|
-
res.json({ challenge });
|
|
1005
|
-
}
|
|
1006
|
-
catch (error) {
|
|
1007
|
-
if (error instanceof Error && error.message === "Unknown system admin principal") {
|
|
1008
|
-
res.status(404).json({ error: "System admin principal not found" });
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
if (error instanceof validation_1.ValidationError) {
|
|
1012
|
-
res.status(400).json({ error: error.message });
|
|
1013
|
-
return;
|
|
1014
|
-
}
|
|
1015
|
-
console.error("[MindooDBServer] System challenge error:", error);
|
|
1016
|
-
res.status(500).json({ error: "Internal server error" });
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
async handleSystemAuthenticate(req, res) {
|
|
1020
|
-
try {
|
|
1021
|
-
const { challenge, signature } = req.body;
|
|
1022
|
-
if (!challenge || !signature) {
|
|
1023
|
-
res.status(400).json({ error: "challenge and signature are required" });
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
(0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
|
|
1027
|
-
(0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
|
|
1028
|
-
const signatureBytes = this.base64ToUint8Array(signature);
|
|
1029
|
-
const result = await this.systemAdminAuth.authenticate(challenge, signatureBytes);
|
|
1030
|
-
// Return 401 on a failed authentication instead of 200 (audit, Low).
|
|
1031
|
-
if (!result.success) {
|
|
1032
|
-
res.status(401).json(result);
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
res.json(result);
|
|
1036
|
-
}
|
|
1037
|
-
catch (error) {
|
|
1038
|
-
if (error instanceof validation_1.ValidationError) {
|
|
1039
|
-
res.status(400).json({ error: error.message });
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
console.error("[MindooDBServer] System authenticate error:", error);
|
|
1043
|
-
res.status(500).json({ error: "Internal server error" });
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
// ==================== Config Update Handler ====================
|
|
1047
|
-
async handleUpdateConfig(req, res) {
|
|
1048
|
-
try {
|
|
1049
|
-
let newConfig;
|
|
1050
|
-
try {
|
|
1051
|
-
newConfig = (0, config_1.validateServerConfig)(req.body, "request body");
|
|
1052
|
-
}
|
|
1053
|
-
catch (err) {
|
|
1054
|
-
res.status(400).json({
|
|
1055
|
-
error: err instanceof Error ? err.message : "Invalid config format",
|
|
1056
|
-
});
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
// Self-lockout protection: simulate the proposed config against the calling
|
|
1060
|
-
// admin's identity — reject if they would lose their own config access.
|
|
1061
|
-
const callingAdmin = req.systemAdmin;
|
|
1062
|
-
const proposedMatcher = new CapabilityMatcher_1.CapabilityMatcher(newConfig);
|
|
1063
|
-
if (!proposedMatcher.isAuthorized("PUT", "/system/config", callingAdmin.username, callingAdmin.publicsignkey)) {
|
|
1064
|
-
res.status(400).json({
|
|
1065
|
-
error: "This change would remove your own access to PUT /system/config",
|
|
1066
|
-
});
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
// Backup the current config before overwriting
|
|
1070
|
-
const { existsSync: fsExists } = await import("fs");
|
|
1071
|
-
let backupFile = null;
|
|
1072
|
-
if (fsExists(this.configPath)) {
|
|
1073
|
-
backupFile = (0, config_1.backupConfig)(this.configPath);
|
|
1074
|
-
}
|
|
1075
|
-
// Persist new config to disk
|
|
1076
|
-
(0, config_1.writeConfig)(this.configPath, newConfig);
|
|
1077
|
-
// Hot-swap in-memory state
|
|
1078
|
-
this.reloadConfig(newConfig);
|
|
1079
|
-
console.log(`[MindooDBServer] Config updated by ${callingAdmin.username}` +
|
|
1080
|
-
(backupFile ? ` (backup: ${backupFile})` : ""));
|
|
1081
|
-
res.json({
|
|
1082
|
-
success: true,
|
|
1083
|
-
...(backupFile ? { backupFile } : {}),
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
catch (error) {
|
|
1087
|
-
console.error("[MindooDBServer] Error updating config:", error);
|
|
1088
|
-
res.status(500).json({ error: "Failed to update config" });
|
|
1089
|
-
}
|
|
544
|
+
router.get("/config/backups", authMiddleware, (0, expressAdapter_1.route)(() => core.listConfigBackups()));
|
|
545
|
+
router.get("/config/backups/:backupFile", authMiddleware, (0, expressAdapter_1.route)((req) => core.getConfigBackup(req)));
|
|
546
|
+
router.get("/config", authMiddleware, (0, expressAdapter_1.route)(() => core.getConfig()));
|
|
547
|
+
router.put("/config", authMiddleware, (0, expressAdapter_1.route)((req) => core.updateConfig(req)));
|
|
1090
548
|
}
|
|
1091
549
|
// ==================== Tenant Routes ====================
|
|
1092
550
|
/**
|
|
@@ -1095,52 +553,47 @@ class MindooDBServer {
|
|
|
1095
553
|
* createSyncRateLimit), sync with a much higher ceiling.
|
|
1096
554
|
*
|
|
1097
555
|
* Both "docs" and "attachments" store kinds expose the same set of sync
|
|
1098
|
-
* operations, mounted at /sync/docs/* and /sync/attachments/*.
|
|
556
|
+
* operations (SYNC_ROUTES), mounted at /sync/docs/* and /sync/attachments/*.
|
|
1099
557
|
*/
|
|
1100
558
|
createTenantRouter() {
|
|
559
|
+
const core = this.core;
|
|
1101
560
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
1102
561
|
const authRateLimitConfig = this.getAuthRateLimitConfig();
|
|
1103
562
|
const challengeRateLimit = this.createAuthRateLimit("tenant", "challenge");
|
|
1104
563
|
const authenticateRateLimit = this.createAuthRateLimit("tenant", "authenticate");
|
|
1105
564
|
// Identity resolution precedes the limiter so it can key on the caller.
|
|
1106
565
|
const syncGuards = [
|
|
1107
|
-
|
|
566
|
+
(0, expressAdapter_1.guard)(async (req) => {
|
|
567
|
+
await core.resolveSyncPrincipal(req);
|
|
568
|
+
return null;
|
|
569
|
+
}),
|
|
1108
570
|
this.createSyncRateLimit(),
|
|
1109
571
|
];
|
|
1110
572
|
console.log(`[MindooDBServer] Tenant auth rate limit configured windowMs=${authRateLimitConfig.windowMs} max=${authRateLimitConfig.max}`);
|
|
1111
|
-
router.post("/auth/challenge", challengeRateLimit,
|
|
1112
|
-
router.post("/auth/authenticate", authenticateRateLimit,
|
|
573
|
+
router.post("/auth/challenge", challengeRateLimit, (0, expressAdapter_1.route)((req) => core.challenge(req)));
|
|
574
|
+
router.post("/auth/authenticate", authenticateRateLimit, (0, expressAdapter_1.route)((req) => core.authenticate(req)));
|
|
1113
575
|
// RFC 3161 proxy. Authentication runs *before* the limiters so they can key
|
|
1114
576
|
// on the identity rather than only the IP — otherwise everyone behind one
|
|
1115
577
|
// NAT shares a budget, and a single tenant can lock out the rest.
|
|
1116
|
-
router.post("/timestamps/rfc3161",
|
|
1117
|
-
for (const storeKind of
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
router.post(`${syncBase}/putEntriesBinary`, syncGuards, express_1.default.raw({ type: binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE, limit: this.jsonBodyLimit }), this.handlePutEntriesBinary.bind(this));
|
|
1136
|
-
router.post(`${syncBase}/getEntryMetadata`, syncGuards, this.handleGetEntryMetadata.bind(this));
|
|
1137
|
-
router.post(`${syncBase}/putEntries`, syncGuards, this.handlePutEntries.bind(this));
|
|
1138
|
-
router.post(`${syncBase}/hasEntries`, syncGuards, this.handleHasEntries.bind(this));
|
|
1139
|
-
router.get(`${syncBase}/getAllIds`, syncGuards, this.handleGetAllIds.bind(this));
|
|
1140
|
-
router.post(`${syncBase}/resolveDependencies`, syncGuards, this.handleResolveDependencies.bind(this));
|
|
1141
|
-
router.post(`${syncBase}/planDocumentMaterialization`, syncGuards, this.handlePlanDocumentMaterialization.bind(this));
|
|
1142
|
-
router.post(`${syncBase}/planDocumentMaterializationBatch`, syncGuards, this.handlePlanDocumentMaterializationBatch.bind(this));
|
|
1143
|
-
router.post(`${syncBase}/planAttachmentReadByWalkingMetadata`, syncGuards, this.handlePlanAttachmentReadByWalkingMetadata.bind(this));
|
|
578
|
+
router.post("/timestamps/rfc3161", (0, expressAdapter_1.guard)((req) => core.timestampAuthGuard(req)), this.createTimestampRateLimit("burst"), this.createTimestampRateLimit("daily"), express_1.default.raw({ type: timestampProxy_1.TIMESTAMP_QUERY_CONTENT_TYPE, limit: timestampProxy_1.MAX_TIMESTAMP_REQUEST_BYTES }), (0, expressAdapter_1.route)((req) => core.timestampRequest(req)));
|
|
579
|
+
for (const storeKind of MindooDBServerCore_1.SYNC_STORE_KINDS) {
|
|
580
|
+
for (const syncRoute of MindooDBServerCore_1.SYNC_ROUTES) {
|
|
581
|
+
const routePath = `/sync/${storeKind}/${syncRoute.name}`;
|
|
582
|
+
const handlers = [...syncGuards];
|
|
583
|
+
if ("rawBodyType" in syncRoute) {
|
|
584
|
+
// The global JSON parser only handles application/json.
|
|
585
|
+
handlers.push(express_1.default.raw({ type: syncRoute.rawBodyType, limit: this.jsonBodyLimit }));
|
|
586
|
+
}
|
|
587
|
+
handlers.push(syncRoute.name === "events"
|
|
588
|
+
? (req, res, next) => this.handleSyncEvents(storeKind, req, res).catch(next)
|
|
589
|
+
: (0, expressAdapter_1.route)((req) => core.sync(syncRoute.name, req, storeKind)));
|
|
590
|
+
if (syncRoute.method === "GET") {
|
|
591
|
+
router.get(routePath, handlers);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
router.post(routePath, handlers);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
1144
597
|
}
|
|
1145
598
|
return router;
|
|
1146
599
|
}
|
|
@@ -1150,12 +603,7 @@ class MindooDBServer {
|
|
|
1150
603
|
* the net catches only traffic no tier claims.
|
|
1151
604
|
*/
|
|
1152
605
|
getGlobalRateLimitConfig() {
|
|
1153
|
-
const resolved = (
|
|
1154
|
-
sync: this.getSyncRateLimitConfig(),
|
|
1155
|
-
auth: this.getAuthRateLimitConfig(),
|
|
1156
|
-
defaultWindowMs: DEFAULT_GLOBAL_RATE_LIMIT_WINDOW_MS,
|
|
1157
|
-
headroom: GLOBAL_RATE_LIMIT_TIER_HEADROOM,
|
|
1158
|
-
});
|
|
606
|
+
const resolved = this.rateLimitPolicy().global;
|
|
1159
607
|
if (resolved.shadowsTiers) {
|
|
1160
608
|
console.warn(`[MindooDBServer] rateLimits.global.max=${resolved.max} is below the ${resolved.tierFloor} ` +
|
|
1161
609
|
"requests the sync and auth tiers may spend in the same window; the global limiter " +
|
|
@@ -1165,49 +613,26 @@ class MindooDBServer {
|
|
|
1165
613
|
}
|
|
1166
614
|
/** Resolved auth rate-limit config; falls back to defaults when not set in config.json. */
|
|
1167
615
|
getAuthRateLimitConfig() {
|
|
1168
|
-
return
|
|
1169
|
-
windowMs: this.serverConfig.rateLimits?.auth?.windowMs ?? DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS,
|
|
1170
|
-
max: this.serverConfig.rateLimits?.auth?.max ?? DEFAULT_AUTH_RATE_LIMIT_MAX,
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
/** Resolved sync rate-limit config; falls back to defaults when not set in config.json. */
|
|
1174
|
-
getSyncRateLimitConfig() {
|
|
1175
|
-
return {
|
|
1176
|
-
windowMs: this.serverConfig.rateLimits?.sync?.windowMs ?? DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS,
|
|
1177
|
-
max: this.serverConfig.rateLimits?.sync?.max ?? DEFAULT_SYNC_RATE_LIMIT_MAX,
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
/** Resolved timestamp rate-limit config; falls back to defaults when not set in config.json. */
|
|
1181
|
-
getTimestampRateLimitConfig() {
|
|
1182
|
-
const configured = this.serverConfig.rateLimits?.timestamps;
|
|
1183
|
-
return {
|
|
1184
|
-
windowMs: configured?.windowMs ?? DEFAULT_TIMESTAMP_RATE_LIMIT_WINDOW_MS,
|
|
1185
|
-
max: configured?.max ?? DEFAULT_TIMESTAMP_RATE_LIMIT_MAX,
|
|
1186
|
-
dailyMax: configured?.dailyMax ?? DEFAULT_TIMESTAMP_DAILY_LIMIT_MAX,
|
|
1187
|
-
};
|
|
616
|
+
return this.rateLimitPolicy().auth;
|
|
1188
617
|
}
|
|
1189
618
|
/**
|
|
1190
619
|
* Two limiters guard the timestamp proxy: a short burst window that keeps a
|
|
1191
620
|
* misbehaving client from hammering an upstream, and a daily cap that bounds
|
|
1192
621
|
* total consumption of a shared (often free) upstream allowance.
|
|
1193
622
|
*
|
|
1194
|
-
* Both key on tenant + principal + IP. The principal is present because
|
|
1195
|
-
*
|
|
623
|
+
* Both key on tenant + principal + IP. The principal is present because the
|
|
624
|
+
* timestamp auth guard runs first.
|
|
1196
625
|
*/
|
|
1197
626
|
createTimestampRateLimit(tier) {
|
|
1198
|
-
const config = this.
|
|
1199
|
-
const windowMs = tier === "burst" ? config.windowMs : TIMESTAMP_DAILY_WINDOW_MS;
|
|
627
|
+
const config = this.rateLimitPolicy().timestamp;
|
|
628
|
+
const windowMs = tier === "burst" ? config.windowMs : rateLimits_1.TIMESTAMP_DAILY_WINDOW_MS;
|
|
1200
629
|
const max = tier === "burst" ? config.max : config.dailyMax;
|
|
1201
630
|
return (0, express_rate_limit_1.default)({
|
|
1202
631
|
windowMs,
|
|
1203
632
|
max,
|
|
1204
633
|
standardHeaders: true,
|
|
1205
634
|
legacyHeaders: false,
|
|
1206
|
-
keyGenerator: (req) =>
|
|
1207
|
-
const tenantId = req.tenantId ?? "unknown";
|
|
1208
|
-
const principal = req.timestampPrincipal ?? "anonymous";
|
|
1209
|
-
return `timestamp:${tier}:${tenantId}:${principal}:${this.getClientIpForRateLimit(req)}`;
|
|
1210
|
-
},
|
|
635
|
+
keyGenerator: (req) => (0, rateLimits_1.buildTimestampRateLimitKey)(tier, req.tenantId, req.timestampPrincipal, this.getClientIpForRateLimit(req)),
|
|
1211
636
|
handler: (req, res, _next, options) => {
|
|
1212
637
|
console.warn(`[MindooDBServer] Timestamp rate limit hit (${tier}) tenant=${req.tenantId} ip=${req.ip}`);
|
|
1213
638
|
res.status(options.statusCode).json(options.message);
|
|
@@ -1219,30 +644,6 @@ class MindooDBServer {
|
|
|
1219
644
|
},
|
|
1220
645
|
});
|
|
1221
646
|
}
|
|
1222
|
-
/**
|
|
1223
|
-
* Resolve the caller of a sync request so {@link createSyncRateLimit} can
|
|
1224
|
-
* bucket per identity instead of per address.
|
|
1225
|
-
*
|
|
1226
|
-
* Unlike {@link timestampAuthMiddleware} this never rejects: the route
|
|
1227
|
-
* handlers do their own token validation and own the 401, and this pass only
|
|
1228
|
-
* needs to know whose budget to spend. An unverifiable token simply falls
|
|
1229
|
-
* back to address-only bucketing — a forged one must not be able to conjure
|
|
1230
|
-
* a fresh budget, which is why the token is verified rather than decoded.
|
|
1231
|
-
*/
|
|
1232
|
-
async syncPrincipalMiddleware(req, _res, next) {
|
|
1233
|
-
try {
|
|
1234
|
-
const token = this.extractToken(req);
|
|
1235
|
-
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
1236
|
-
const payload = await authService.validateToken(token);
|
|
1237
|
-
if (payload && payload.tenantId === req.tenantId) {
|
|
1238
|
-
req.syncPrincipal = payload.deviceSigningKey ?? payload.sub;
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
catch {
|
|
1242
|
-
// No usable token; fall through to address-only bucketing.
|
|
1243
|
-
}
|
|
1244
|
-
next();
|
|
1245
|
-
}
|
|
1246
647
|
/**
|
|
1247
648
|
* Sync budget, keyed on tenant + principal + IP like the auth and timestamp
|
|
1248
649
|
* tiers. Address-only keying would put every user behind one NAT or reverse
|
|
@@ -1250,7 +651,7 @@ class MindooDBServer {
|
|
|
1250
651
|
* throttle everyone else on that address.
|
|
1251
652
|
*/
|
|
1252
653
|
createSyncRateLimit() {
|
|
1253
|
-
const config = this.
|
|
654
|
+
const config = this.rateLimitPolicy().sync;
|
|
1254
655
|
console.log(`[MindooDBServer] Tenant sync rate limit configured windowMs=${config.windowMs} max=${config.max}`);
|
|
1255
656
|
return (0, express_rate_limit_1.default)({
|
|
1256
657
|
windowMs: config.windowMs,
|
|
@@ -1266,91 +667,12 @@ class MindooDBServer {
|
|
|
1266
667
|
message: { error: "Too many sync requests, please try again later" },
|
|
1267
668
|
});
|
|
1268
669
|
}
|
|
1269
|
-
/**
|
|
1270
|
-
* Authenticate a timestamp request with the tenant's existing bearer token and
|
|
1271
|
-
* record the principal for rate-limit bucketing.
|
|
1272
|
-
*
|
|
1273
|
-
* The proxy makes outbound requests on the caller's behalf, so it must never
|
|
1274
|
-
* be open: unauthenticated it would be a free relay to third-party services.
|
|
1275
|
-
*/
|
|
1276
|
-
async timestampAuthMiddleware(req, res, next) {
|
|
1277
|
-
try {
|
|
1278
|
-
const token = this.extractToken(req);
|
|
1279
|
-
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
1280
|
-
const payload = await authService.validateToken(token);
|
|
1281
|
-
if (!payload || payload.tenantId !== req.tenantId) {
|
|
1282
|
-
res.status(401).json({ error: "Invalid or expired token" });
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
req.timestampPrincipal = payload.deviceSigningKey ?? payload.sub;
|
|
1286
|
-
next();
|
|
1287
|
-
}
|
|
1288
|
-
catch (error) {
|
|
1289
|
-
this.handleRequestError(error, res);
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
/**
|
|
1293
|
-
* Forward a DER TimeStampReq to a configured TSA and return the DER
|
|
1294
|
-
* TimeStampResp verbatim.
|
|
1295
|
-
*
|
|
1296
|
-
* The server neither parses nor validates the ASN.1: the client checks that
|
|
1297
|
-
* the returned token's `messageImprint` matches the digest it sent and that
|
|
1298
|
-
* the token chains to a trusted root. That is what reduces a hostile proxy —
|
|
1299
|
-
* ours or anyone's — from a forgery risk to a denial-of-service risk.
|
|
1300
|
-
*/
|
|
1301
|
-
async handleTimestampRequest(req, res) {
|
|
1302
|
-
if (this.tsaProviders.length === 0) {
|
|
1303
|
-
res.status(503).json({ error: "Timestamping is not enabled on this server" });
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const body = req.body;
|
|
1307
|
-
if (!Buffer.isBuffer(body)) {
|
|
1308
|
-
res.status(415).json({
|
|
1309
|
-
error: `Timestamp requests must be sent as ${timestampProxy_1.TIMESTAMP_QUERY_CONTENT_TYPE}`,
|
|
1310
|
-
});
|
|
1311
|
-
return;
|
|
1312
|
-
}
|
|
1313
|
-
// The provider is named by id and resolved against the env allowlist; a
|
|
1314
|
-
// client-supplied URL would make this an SSRF gadget.
|
|
1315
|
-
const requestedProvider = req.query.provider;
|
|
1316
|
-
let preferredProviderId;
|
|
1317
|
-
if (typeof requestedProvider === "string" && requestedProvider.length > 0) {
|
|
1318
|
-
preferredProviderId = requestedProvider.toLowerCase();
|
|
1319
|
-
}
|
|
1320
|
-
else if (requestedProvider !== undefined) {
|
|
1321
|
-
res.status(400).json({ error: "provider must be a single string value" });
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
try {
|
|
1325
|
-
const result = await (0, timestampProxy_1.requestTimestampToken)({
|
|
1326
|
-
providers: this.tsaProviders,
|
|
1327
|
-
request: new Uint8Array(body),
|
|
1328
|
-
preferredProviderId,
|
|
1329
|
-
});
|
|
1330
|
-
res.setHeader("content-type", timestampProxy_1.TIMESTAMP_REPLY_CONTENT_TYPE);
|
|
1331
|
-
res.setHeader("x-mindoodb-tsa-provider", result.providerId);
|
|
1332
|
-
res.status(200).send(Buffer.from(result.response));
|
|
1333
|
-
}
|
|
1334
|
-
catch (error) {
|
|
1335
|
-
if (error instanceof timestampProxy_1.TimestampProxyError) {
|
|
1336
|
-
if (error.attempts.length > 0) {
|
|
1337
|
-
console.warn(`[MindooDBServer] Timestamp request failed for tenant ${req.tenantId}: ${error.attempts
|
|
1338
|
-
.map((attempt) => `${attempt.providerId}=${attempt.error ?? "ok"}`)
|
|
1339
|
-
.join("; ")}`);
|
|
1340
|
-
}
|
|
1341
|
-
res.status(error.status).json({ error: error.message });
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
console.error("[MindooDBServer] Unexpected timestamp proxy error:", error);
|
|
1345
|
-
res.status(500).json({ error: "Failed to obtain a timestamp" });
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
670
|
/**
|
|
1349
671
|
* Create a rate-limiter for authentication endpoints. Challenge and
|
|
1350
672
|
* authenticate get separate limiters so that one phase can't starve the
|
|
1351
|
-
* other. The
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
673
|
+
* other. The key buckets by (scope, tenant, phase, IP, username) — this
|
|
674
|
+
* prevents users behind a shared IP (NAT / reverse proxy) from exhausting
|
|
675
|
+
* each other's budgets.
|
|
1354
676
|
*/
|
|
1355
677
|
createAuthRateLimit(scope, phase) {
|
|
1356
678
|
const authRateLimitConfig = this.getAuthRateLimitConfig();
|
|
@@ -1359,7 +681,7 @@ class MindooDBServer {
|
|
|
1359
681
|
max: authRateLimitConfig.max,
|
|
1360
682
|
standardHeaders: true,
|
|
1361
683
|
legacyHeaders: false,
|
|
1362
|
-
keyGenerator: (req) =>
|
|
684
|
+
keyGenerator: (req) => (0, rateLimits_1.buildAuthRateLimitKey)(scope, phase, req.tenantId ?? req.params.tenantId?.toLowerCase(), this.getClientIpForRateLimit(req), req.body),
|
|
1363
685
|
handler: (req, res, _next, options) => {
|
|
1364
686
|
this.logAuthRateLimitHit(req, scope, phase);
|
|
1365
687
|
res.status(options.statusCode).json(options.message);
|
|
@@ -1367,30 +689,6 @@ class MindooDBServer {
|
|
|
1367
689
|
message: { error: "Too many authentication attempts, please try again later" },
|
|
1368
690
|
});
|
|
1369
691
|
}
|
|
1370
|
-
/**
|
|
1371
|
-
* Composite rate-limit key. Challenge requests include the username so
|
|
1372
|
-
* that brute-forcing one account doesn't block legitimate users on the
|
|
1373
|
-
* same IP. Authenticate requests omit username because the challenge
|
|
1374
|
-
* token is already scoped to a single principal.
|
|
1375
|
-
*/
|
|
1376
|
-
getAuthRateLimitKey(req, scope, phase) {
|
|
1377
|
-
const tenantId = scope === "tenant"
|
|
1378
|
-
? req.tenantId ?? req.params.tenantId?.toLowerCase() ?? "-"
|
|
1379
|
-
: "system";
|
|
1380
|
-
const clientIp = this.getClientIpForRateLimit(req);
|
|
1381
|
-
if (phase === "challenge") {
|
|
1382
|
-
// Bucket by username when supplied, otherwise by the signing public key a
|
|
1383
|
-
// key-based client identifies with, so key-based clients still get a
|
|
1384
|
-
// stable per-principal bucket instead of all sharing "-".
|
|
1385
|
-
const principal = typeof req.body?.username === "string" && req.body.username.trim()
|
|
1386
|
-
? req.body.username.trim().toLowerCase()
|
|
1387
|
-
: typeof req.body?.signingPublicKey === "string" && req.body.signingPublicKey.trim()
|
|
1388
|
-
? req.body.signingPublicKey.trim()
|
|
1389
|
-
: "-";
|
|
1390
|
-
return `${scope}:${tenantId}:${phase}:${clientIp}:${principal}`;
|
|
1391
|
-
}
|
|
1392
|
-
return `${scope}:${tenantId}:${phase}:${clientIp}`;
|
|
1393
|
-
}
|
|
1394
692
|
/**
|
|
1395
693
|
* Emit a structured warning when an auth rate limit is triggered.
|
|
1396
694
|
* The forwarded-for header is logged to help diagnose situations where
|
|
@@ -1420,739 +718,47 @@ class MindooDBServer {
|
|
|
1420
718
|
: req.socket?.remoteAddress) ?? "unknown";
|
|
1421
719
|
return (0, express_rate_limit_1.ipKeyGenerator)(raw);
|
|
1422
720
|
}
|
|
1423
|
-
// ====================
|
|
1424
|
-
validateDbId(dbId) {
|
|
1425
|
-
return (0, validation_1.validateIdentifier)(dbId, "dbId");
|
|
1426
|
-
}
|
|
1427
|
-
formatTenantNotFoundOnServerError(tenantId) {
|
|
1428
|
-
const serverName = this.tenantManager.getServerPublicInfo()?.name ?? "unknown";
|
|
1429
|
-
return `Tenant ${tenantId} not found on server ${serverName}`;
|
|
1430
|
-
}
|
|
1431
|
-
// ==================== Device Discovery (server-wide) ====================
|
|
1432
|
-
async handleDeviceChallenge(req, res) {
|
|
1433
|
-
try {
|
|
1434
|
-
const { signingPublicKey } = req.body ?? {};
|
|
1435
|
-
if (typeof signingPublicKey !== "string" || !signingPublicKey.trim()) {
|
|
1436
|
-
res.status(400).json({ error: "signingPublicKey is required" });
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
(0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "signingPublicKey");
|
|
1440
|
-
const challenge = this.deviceDiscovery.createChallenge(signingPublicKey);
|
|
1441
|
-
res.json({ challenge });
|
|
1442
|
-
}
|
|
1443
|
-
catch (error) {
|
|
1444
|
-
this.handleRequestError(error, res);
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
async handleDeviceDiscover(req, res) {
|
|
1448
|
-
try {
|
|
1449
|
-
const { challenge, signature } = req.body ?? {};
|
|
1450
|
-
if (typeof challenge !== "string" || !challenge.trim()) {
|
|
1451
|
-
res.status(400).json({ error: "challenge is required" });
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
if (typeof signature !== "string" || !signature.trim()) {
|
|
1455
|
-
res.status(400).json({ error: "signature is required" });
|
|
1456
|
-
return;
|
|
1457
|
-
}
|
|
1458
|
-
(0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
|
|
1459
|
-
(0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
|
|
1460
|
-
const signatureBytes = this.base64ToUint8Array(signature);
|
|
1461
|
-
const result = await this.deviceDiscovery.discover(challenge, signatureBytes);
|
|
1462
|
-
res.json(result);
|
|
1463
|
-
}
|
|
1464
|
-
catch (error) {
|
|
1465
|
-
if (error instanceof DeviceDiscoveryService_1.DeviceDiscoveryAuthError) {
|
|
1466
|
-
res.status(401).json({ error: error.message });
|
|
1467
|
-
return;
|
|
1468
|
-
}
|
|
1469
|
-
this.handleRequestError(error, res);
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
// ==================== Tenant Auth Handlers ====================
|
|
1473
|
-
// Two-step challenge-response flow:
|
|
1474
|
-
// 1. POST /auth/challenge → client sends username, server returns a random nonce
|
|
1475
|
-
// 2. POST /auth/authenticate → client signs the nonce with its Ed25519 private key,
|
|
1476
|
-
// server verifies the signature and returns a JWT for subsequent sync calls
|
|
1477
|
-
async handleChallenge(req, res) {
|
|
1478
|
-
try {
|
|
1479
|
-
const { username, signingPublicKey } = req.body;
|
|
1480
|
-
// The username is optional: a client may identify itself by its device
|
|
1481
|
-
// signing public key instead, so the server never needs the cleartext
|
|
1482
|
-
// name. Validate whichever was supplied.
|
|
1483
|
-
if (username !== undefined && username !== null) {
|
|
1484
|
-
(0, validation_1.validateUsername)(username);
|
|
1485
|
-
}
|
|
1486
|
-
if (signingPublicKey !== undefined && signingPublicKey !== null) {
|
|
1487
|
-
(0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_SIGNATURE_LENGTH, "signingPublicKey");
|
|
1488
|
-
}
|
|
1489
|
-
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
1490
|
-
const challenge = await authService.generateChallenge(typeof username === "string" ? username : undefined, typeof signingPublicKey === "string" ? { signingPublicKey } : undefined);
|
|
1491
|
-
res.json({ challenge });
|
|
1492
|
-
}
|
|
1493
|
-
catch (error) {
|
|
1494
|
-
this.handleRequestError(error, res);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
async handleAuthenticate(req, res) {
|
|
1498
|
-
try {
|
|
1499
|
-
const { challenge, signature } = req.body;
|
|
1500
|
-
if (!challenge || !signature) {
|
|
1501
|
-
res.status(400).json({ error: "challenge and signature are required" });
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
(0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
|
|
1505
|
-
(0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
|
|
1506
|
-
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
1507
|
-
const signatureBytes = this.base64ToUint8Array(signature);
|
|
1508
|
-
const result = await authService.authenticate(challenge, signatureBytes);
|
|
1509
|
-
// Return 401 on a failed authentication instead of 200 (audit, Low): a
|
|
1510
|
-
// failed credential check must not look like a success at the HTTP layer.
|
|
1511
|
-
if (!result.success) {
|
|
1512
|
-
res.status(401).json(result);
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
res.json(result);
|
|
1516
|
-
}
|
|
1517
|
-
catch (error) {
|
|
1518
|
-
this.handleRequestError(error, res);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
// ==================== Sync Handlers ====================
|
|
1522
|
-
async handleFindNewEntries(req, res) {
|
|
1523
|
-
try {
|
|
1524
|
-
const token = this.extractToken(req);
|
|
1525
|
-
const { dbId, haveIds } = req.body;
|
|
1526
|
-
const validDbId = this.validateDbId(dbId);
|
|
1527
|
-
(0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
|
|
1528
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1529
|
-
const entries = await serverStore.handleFindNewEntries(token, haveIds || []);
|
|
1530
|
-
res.json({
|
|
1531
|
-
entries: entries.map((e) => this.serializeEntryMetadata(e)),
|
|
1532
|
-
});
|
|
1533
|
-
}
|
|
1534
|
-
catch (error) {
|
|
1535
|
-
this.handleRequestError(error, res);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
async handleFindNewEntriesForDoc(req, res) {
|
|
1539
|
-
try {
|
|
1540
|
-
const token = this.extractToken(req);
|
|
1541
|
-
const { dbId, haveIds, docId } = req.body;
|
|
1542
|
-
const validDbId = this.validateDbId(dbId);
|
|
1543
|
-
if (!docId) {
|
|
1544
|
-
res.status(400).json({ error: "docId is required" });
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
|
-
(0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
|
|
1548
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1549
|
-
const entries = await serverStore.handleFindNewEntriesForDoc(token, haveIds || [], docId);
|
|
1550
|
-
res.json({
|
|
1551
|
-
entries: entries.map((e) => this.serializeEntryMetadata(e)),
|
|
1552
|
-
});
|
|
1553
|
-
}
|
|
1554
|
-
catch (error) {
|
|
1555
|
-
this.handleRequestError(error, res);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
async handleFindEntries(req, res) {
|
|
1559
|
-
try {
|
|
1560
|
-
const token = this.extractToken(req);
|
|
1561
|
-
const { dbId, type, creationDateFrom, creationDateUntil } = req.body;
|
|
1562
|
-
const validDbId = this.validateDbId(dbId);
|
|
1563
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1564
|
-
const entries = await serverStore.handleFindEntries(token, type, creationDateFrom ?? null, creationDateUntil ?? null);
|
|
1565
|
-
res.json({
|
|
1566
|
-
entries: entries.map((e) => this.serializeEntryMetadata(e)),
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
catch (error) {
|
|
1570
|
-
this.handleRequestError(error, res);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
async handleScanEntriesSince(req, res) {
|
|
1574
|
-
try {
|
|
1575
|
-
const token = this.extractToken(req);
|
|
1576
|
-
const { dbId, cursor, limit, filters } = req.body;
|
|
1577
|
-
const validDbId = this.validateDbId(dbId);
|
|
1578
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1579
|
-
const result = await serverStore.handleScanEntriesSince(token, cursor ?? null, limit, filters);
|
|
1580
|
-
res.json({
|
|
1581
|
-
entries: result.entries.map((e) => this.serializeEntryMetadata(e)),
|
|
1582
|
-
nextCursor: result.nextCursor,
|
|
1583
|
-
hasMore: result.hasMore,
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
catch (error) {
|
|
1587
|
-
this.handleRequestError(error, res);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
async handleGetStoreHead(req, res) {
|
|
1591
|
-
try {
|
|
1592
|
-
const token = this.extractToken(req);
|
|
1593
|
-
const { dbId } = req.body;
|
|
1594
|
-
const validDbId = this.validateDbId(dbId);
|
|
1595
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1596
|
-
const head = await serverStore.handleGetStoreHead(token);
|
|
1597
|
-
res.json({ head });
|
|
1598
|
-
}
|
|
1599
|
-
catch (error) {
|
|
1600
|
-
this.handleRequestError(error, res);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
async handleGetIdBloomSummary(req, res) {
|
|
1604
|
-
try {
|
|
1605
|
-
const token = this.extractToken(req);
|
|
1606
|
-
const { dbId } = req.body;
|
|
1607
|
-
const validDbId = this.validateDbId(dbId);
|
|
1608
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1609
|
-
const summary = await serverStore.handleGetIdBloomSummary(token);
|
|
1610
|
-
res.json({ summary: summary });
|
|
1611
|
-
}
|
|
1612
|
-
catch (error) {
|
|
1613
|
-
this.handleRequestError(error, res);
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
async handleGetCapabilities(req, res) {
|
|
1617
|
-
try {
|
|
1618
|
-
const token = this.extractToken(req);
|
|
1619
|
-
const validDbId = this.validateDbId(req.query.dbId);
|
|
1620
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1621
|
-
const capabilities = await serverStore.handleGetCapabilities(token);
|
|
1622
|
-
// HTTP-layer features on top of the transport-agnostic capability set:
|
|
1623
|
-
// binary octet-stream framing routes (phase 3) and the SSE change feed
|
|
1624
|
-
// (phase 5) exist only in this Express host.
|
|
1625
|
-
res.json({
|
|
1626
|
-
capabilities: {
|
|
1627
|
-
...capabilities,
|
|
1628
|
-
supportsBinaryEntries: true,
|
|
1629
|
-
supportsPayloadOnlyEntries: true,
|
|
1630
|
-
supportsChangeEvents: true,
|
|
1631
|
-
},
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
catch (error) {
|
|
1635
|
-
this.handleRequestError(error, res);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
/**
|
|
1639
|
-
* SSE live change feed (sync-v5, phase 5).
|
|
1640
|
-
*
|
|
1641
|
-
* `GET /:tenantId/sync/:storeKind/events` — Bearer-authenticated like every
|
|
1642
|
-
* other sync route (fetch-streaming clients can set headers; native
|
|
1643
|
-
* EventSource cannot, which is why the SDK uses a fetch reader).
|
|
1644
|
-
*
|
|
1645
|
-
* The subscribe-time read gate runs via `handleGetCapabilities` against the
|
|
1646
|
-
* database given in `?dbId=` (default `directory`, which every tenant member
|
|
1647
|
-
* can read): it validates the token and runs the database-open gate. Events
|
|
1648
|
-
* carry only `{dbId, storeKind, epoch, maxReceiptOrder}` — metadata, never
|
|
1649
|
-
* content. A heartbeat comment every 30 s keeps proxies from closing the
|
|
1650
|
-
* stream. Token expiry does not tear down an open stream; the events are
|
|
1651
|
-
* non-sensitive change hints and a reconnect re-authenticates.
|
|
1652
|
-
*/
|
|
1653
|
-
async handleSyncEvents(req, res) {
|
|
1654
|
-
try {
|
|
1655
|
-
const token = this.extractToken(req);
|
|
1656
|
-
const tenantId = req.tenantId;
|
|
1657
|
-
const storeKind = this.getStoreKindFromRequest(req);
|
|
1658
|
-
const gateDbId = this.validateDbId(typeof req.query.dbId === "string" && req.query.dbId ? req.query.dbId : "directory");
|
|
1659
|
-
// Validates the token and runs the database-open/read gate before any
|
|
1660
|
-
// event is delivered.
|
|
1661
|
-
const serverStore = await this.tenantManager.getServerStore(tenantId, gateDbId, storeKind);
|
|
1662
|
-
await serverStore.handleGetCapabilities(token);
|
|
1663
|
-
res.status(200);
|
|
1664
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
1665
|
-
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
1666
|
-
res.setHeader("X-Accel-Buffering", "no");
|
|
1667
|
-
if (!req.httpVersion.startsWith("2")) {
|
|
1668
|
-
// Forbidden (and unnecessary) on HTTP/2 streams.
|
|
1669
|
-
res.setHeader("Connection", "keep-alive");
|
|
1670
|
-
}
|
|
1671
|
-
res.flushHeaders();
|
|
1672
|
-
res.write(`event: hello\ndata: ${JSON.stringify({ protocolVersion: "sync-v5" })}\n\n`);
|
|
1673
|
-
const unsubscribe = this.syncEventBus.subscribe((event) => {
|
|
1674
|
-
if (event.tenantId !== tenantId || event.storeKind !== storeKind) {
|
|
1675
|
-
return;
|
|
1676
|
-
}
|
|
1677
|
-
res.write(`event: change\ndata: ${JSON.stringify({
|
|
1678
|
-
dbId: event.dbId,
|
|
1679
|
-
storeKind: event.storeKind,
|
|
1680
|
-
epoch: event.epoch,
|
|
1681
|
-
maxReceiptOrder: event.maxReceiptOrder,
|
|
1682
|
-
})}\n\n`);
|
|
1683
|
-
});
|
|
1684
|
-
const heartbeat = setInterval(() => {
|
|
1685
|
-
res.write(`: heartbeat ${Date.now()}\n\n`);
|
|
1686
|
-
}, 30000);
|
|
1687
|
-
const cleanup = () => {
|
|
1688
|
-
clearInterval(heartbeat);
|
|
1689
|
-
unsubscribe();
|
|
1690
|
-
};
|
|
1691
|
-
req.on("close", cleanup);
|
|
1692
|
-
res.on("close", cleanup);
|
|
1693
|
-
console.log(`[MindooDBServer] SSE subscriber connected tenant=${tenantId} storeKind=${storeKind} subscribers=${this.syncEventBus.listenerCount}`);
|
|
1694
|
-
}
|
|
1695
|
-
catch (error) {
|
|
1696
|
-
this.handleRequestError(error, res);
|
|
1697
|
-
}
|
|
1698
|
-
}
|
|
721
|
+
// ==================== Sync Event Stream ====================
|
|
1699
722
|
/**
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
723
|
+
* SSE transport for the change feed: the core runs the subscribe-time read
|
|
724
|
+
* gate, this keeps the stream open on the in-process event bus with a
|
|
725
|
+
* heartbeat every 30 s.
|
|
1703
726
|
*/
|
|
1704
|
-
|
|
1705
|
-
|
|
727
|
+
async handleSyncEvents(storeKind, req, res) {
|
|
728
|
+
const response = await this.core.sync("events", (0, expressAdapter_1.toServerRequest)(req), storeKind);
|
|
729
|
+
if (!(0, types_2.isEventStreamResponse)(response)) {
|
|
730
|
+
await (0, expressAdapter_1.sendServerResponse)(req, res, response);
|
|
1706
731
|
return;
|
|
1707
732
|
}
|
|
1708
|
-
|
|
1709
|
-
// peer has to be labelled: without the label it would be pushed straight
|
|
1710
|
-
// back to its origin, and two bidirectional peers would ping-pong.
|
|
1711
|
-
const originPeer = this.cluster?.auth
|
|
1712
|
-
? this.cluster.auth.validateToken(token).then((payload) => payload?.sub)
|
|
1713
|
-
: Promise.resolve(undefined);
|
|
1714
|
-
void Promise.all([serverStore.handleGetStoreHead(token).catch(() => null), originPeer]).then(([head, peerName]) => {
|
|
1715
|
-
this.syncEventBus.publish({
|
|
1716
|
-
tenantId,
|
|
1717
|
-
dbId,
|
|
1718
|
-
storeKind,
|
|
1719
|
-
epoch: head?.epoch,
|
|
1720
|
-
maxReceiptOrder: head?.maxReceiptOrder,
|
|
1721
|
-
originPeer: peerName,
|
|
1722
|
-
});
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
async handleGetCompactionStatus(req, res) {
|
|
1726
|
-
try {
|
|
1727
|
-
const token = this.extractToken(req);
|
|
1728
|
-
const { dbId } = req.body;
|
|
1729
|
-
const validDbId = this.validateDbId(dbId);
|
|
1730
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1731
|
-
const status = await serverStore.handleGetCompactionStatus(token);
|
|
1732
|
-
res.json({ status: status });
|
|
1733
|
-
}
|
|
1734
|
-
catch (error) {
|
|
1735
|
-
this.handleRequestError(error, res);
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1738
|
-
async handleGetEntries(req, res) {
|
|
1739
|
-
try {
|
|
1740
|
-
const token = this.extractToken(req);
|
|
1741
|
-
const { dbId, ids, sessionKeyWrap } = req.body;
|
|
1742
|
-
const validDbId = this.validateDbId(dbId);
|
|
1743
|
-
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
1744
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1745
|
-
// Session-key transport format (sync-v5, phase 2): one RSA-wrapped
|
|
1746
|
-
// AES-256-GCM key per response instead of per-entry RSA. Negotiated via
|
|
1747
|
-
// the `sessionKeyWrap` request flag; legacy clients get the old format.
|
|
1748
|
-
if (sessionKeyWrap === true) {
|
|
1749
|
-
const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
|
|
1750
|
-
res.json({
|
|
1751
|
-
wrappedSessionKey: this.uint8ArrayToBase64(batch.wrappedSessionKey),
|
|
1752
|
-
entries: batch.entries.map((e) => ({
|
|
1753
|
-
...this.serializeEntryMetadata(e),
|
|
1754
|
-
iv: this.uint8ArrayToBase64(e.iv),
|
|
1755
|
-
sessionEncryptedPayload: this.uint8ArrayToBase64(e.sessionEncryptedPayload),
|
|
1756
|
-
})),
|
|
1757
|
-
});
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
const entries = await serverStore.handleGetEntries(token, ids || []);
|
|
1761
|
-
res.json({
|
|
1762
|
-
entries: entries.map((e) => ({
|
|
1763
|
-
...this.serializeEntryMetadata(e),
|
|
1764
|
-
rsaEncryptedPayload: this.uint8ArrayToBase64(e.rsaEncryptedPayload),
|
|
1765
|
-
})),
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
catch (error) {
|
|
1769
|
-
this.handleRequestError(error, res);
|
|
1770
|
-
}
|
|
1771
|
-
}
|
|
1772
|
-
/**
|
|
1773
|
-
* Binary variant of getEntries (sync-v5, phase 3): always session-key
|
|
1774
|
-
* encrypted, response framed as `application/octet-stream` (see
|
|
1775
|
-
* binaryEntryFraming.ts). The request stays JSON (small).
|
|
1776
|
-
*/
|
|
1777
|
-
async handleGetEntriesBinary(req, res) {
|
|
1778
|
-
try {
|
|
1779
|
-
const token = this.extractToken(req);
|
|
1780
|
-
const { dbId, ids, payloadOnly } = req.body;
|
|
1781
|
-
const validDbId = this.validateDbId(dbId);
|
|
1782
|
-
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
1783
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1784
|
-
const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
|
|
1785
|
-
// `payloadOnly` (capability supportsPayloadOnlyEntries): the client
|
|
1786
|
-
// already holds the metadata from its scan, so each frame carries only
|
|
1787
|
-
// what it cannot have yet. Same entries, same session-key encryption.
|
|
1788
|
-
const body = (0, binaryEntryFraming_1.encodeBinaryEntryMessage)({
|
|
1789
|
-
header: {
|
|
1790
|
-
format: binaryEntryFraming_1.BINARY_GET_ENTRIES_FORMAT,
|
|
1791
|
-
wrappedSessionKey: this.uint8ArrayToBase64(batch.wrappedSessionKey),
|
|
1792
|
-
...(payloadOnly === true ? { payloadOnly: true } : {}),
|
|
1793
|
-
},
|
|
1794
|
-
entries: batch.entries.map((e) => ({
|
|
1795
|
-
meta: payloadOnly === true
|
|
1796
|
-
? { id: e.id, iv: this.uint8ArrayToBase64(e.iv) }
|
|
1797
|
-
: { ...this.serializeEntryMetadata(e), iv: this.uint8ArrayToBase64(e.iv) },
|
|
1798
|
-
payload: e.sessionEncryptedPayload,
|
|
1799
|
-
})),
|
|
1800
|
-
});
|
|
1801
|
-
const payloadBytes = batch.entries.reduce((sum, e) => sum + e.sessionEncryptedPayload.byteLength, 0);
|
|
1802
|
-
res.setHeader("Content-Type", binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE);
|
|
1803
|
-
await (0, compressionMiddleware_1.sendBinaryEntryBody)(req, res, body, body.byteLength - payloadBytes);
|
|
1804
|
-
}
|
|
1805
|
-
catch (error) {
|
|
1806
|
-
this.handleRequestError(error, res);
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
async handleGetEntryMetadata(req, res) {
|
|
1810
|
-
try {
|
|
1811
|
-
const token = this.extractToken(req);
|
|
1812
|
-
const { dbId, id } = req.body;
|
|
1813
|
-
const validDbId = this.validateDbId(dbId);
|
|
1814
|
-
if (!id) {
|
|
1815
|
-
res.status(400).json({ error: "id is required" });
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1819
|
-
const entry = await serverStore.handleGetEntryMetadata(token, id);
|
|
1820
|
-
res.json({
|
|
1821
|
-
entry: entry ? this.serializeEntryMetadata(entry) : null,
|
|
1822
|
-
});
|
|
1823
|
-
}
|
|
1824
|
-
catch (error) {
|
|
1825
|
-
this.handleRequestError(error, res);
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
/**
|
|
1829
|
-
* Ingest encrypted entries from a client. The `stage` variable tracks
|
|
1830
|
-
* progress through the pipeline so that error logs pinpoint the failing
|
|
1831
|
-
* step (token extraction → body parsing → deserialization → storage).
|
|
1832
|
-
*/
|
|
1833
|
-
async handlePutEntries(req, res) {
|
|
1834
|
-
let stage = "extract-token";
|
|
1835
|
-
try {
|
|
1836
|
-
const token = this.extractToken(req);
|
|
1837
|
-
stage = "read-body";
|
|
1838
|
-
const { dbId, entries } = req.body;
|
|
1839
|
-
const validDbId = this.validateDbId(dbId);
|
|
1840
|
-
(0, validation_1.validateArraySize)(entries, validation_1.MAX_PUT_ENTRIES, "entries");
|
|
1841
|
-
const serializedEntries = Array.isArray(entries) ? entries : [];
|
|
1842
|
-
const serializedEntryTypeCounts = serializedEntries.reduce((acc, entry) => {
|
|
1843
|
-
const key = entry?.entryType ?? "unknown";
|
|
1844
|
-
acc[key] = (acc[key] ?? 0) + 1;
|
|
1845
|
-
return acc;
|
|
1846
|
-
}, {});
|
|
1847
|
-
console.log(`[MindooDBServer] putEntries request tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${serializedEntries.length} parsed-bytes=${req.jsonBodyBytes ?? "-"} content-length=${req.headers["content-length"] ?? "-"} types=${JSON.stringify(serializedEntryTypeCounts)}`);
|
|
1848
|
-
stage = "load-server-store";
|
|
1849
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1850
|
-
stage = "deserialize-entries";
|
|
1851
|
-
const deserializedEntries = serializedEntries.map((entry, index) => {
|
|
1852
|
-
try {
|
|
1853
|
-
return this.deserializeEntry(entry);
|
|
1854
|
-
}
|
|
1855
|
-
catch (error) {
|
|
1856
|
-
console.error("[MindooDBServer] putEntries failed while deserializing entry", {
|
|
1857
|
-
index,
|
|
1858
|
-
id: entry?.id ?? "-",
|
|
1859
|
-
type: entry?.entryType ?? "-",
|
|
1860
|
-
docId: entry?.docId ?? "-",
|
|
1861
|
-
encryptedSize: entry?.encryptedSize ?? "-",
|
|
1862
|
-
originalSize: entry?.originalSize ?? "-",
|
|
1863
|
-
signatureLength: entry?.signature?.length ?? "-",
|
|
1864
|
-
encryptedDataLength: entry?.encryptedData?.length ?? "-",
|
|
1865
|
-
error,
|
|
1866
|
-
});
|
|
1867
|
-
throw error;
|
|
1868
|
-
}
|
|
1869
|
-
});
|
|
1870
|
-
const totalEncryptedBytes = deserializedEntries.reduce((sum, entry) => sum + entry.encryptedData.byteLength, 0);
|
|
1871
|
-
const totalOriginalBytes = deserializedEntries.reduce((sum, entry) => sum + entry.originalSize, 0);
|
|
1872
|
-
console.log(`[MindooDBServer] putEntries deserialized tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${deserializedEntries.length} totalEncryptedBytes=${totalEncryptedBytes} totalOriginalBytes=${totalOriginalBytes}`);
|
|
1873
|
-
stage = "store-entries";
|
|
1874
|
-
const ack = await serverStore.handlePutEntries(token, deserializedEntries);
|
|
1875
|
-
stage = "respond-success";
|
|
1876
|
-
// Return witness receipts for accepted entries so the pushing client (and
|
|
1877
|
-
// every replica it later syncs with) can persist the attestation
|
|
1878
|
-
// (docs/accesscontrol.md §5.3), plus per-entry rejections for
|
|
1879
|
-
// signature-class failures the server skipped. Older clients ignore the
|
|
1880
|
-
// extra fields.
|
|
1881
|
-
res.json({
|
|
1882
|
-
success: true,
|
|
1883
|
-
receipts: ack.receipts.map((e) => this.serializeEntryMetadata(e)),
|
|
1884
|
-
rejected: ack.rejected,
|
|
1885
|
-
});
|
|
1886
|
-
// Announce the accepted write to SSE subscribers (sync-v5, phase 5).
|
|
1887
|
-
if (ack.receipts.length > 0) {
|
|
1888
|
-
this.publishSyncChangeEvent(req.tenantId, validDbId, this.getStoreKindFromRequest(req), serverStore, token);
|
|
1889
|
-
}
|
|
1890
|
-
// A directory push may carry new admin-signed purge requests
|
|
1891
|
-
// (docs/accesscontrol.md §13). Execute any pending purges after the
|
|
1892
|
-
// response so the server physically removes purged history from its own
|
|
1893
|
-
// stores and arms the re-push denylist. Fire-and-forget: failures are
|
|
1894
|
-
// logged and retried on the next directory push.
|
|
1895
|
-
if (validDbId === "directory") {
|
|
1896
|
-
const tenantId = req.tenantId;
|
|
1897
|
-
void this.tenantManager.executePendingPurges(tenantId).catch((error) => {
|
|
1898
|
-
console.error(`[MindooDBServer] executePendingPurges failed for ${tenantId}:`, error);
|
|
1899
|
-
});
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
catch (error) {
|
|
1903
|
-
console.error("[MindooDBServer] putEntries failed", {
|
|
1904
|
-
stage,
|
|
1905
|
-
tenantId: req.tenantId ?? "-",
|
|
1906
|
-
dbId: req.body?.dbId ?? "-",
|
|
1907
|
-
parsedBytes: req.jsonBodyBytes ?? "-",
|
|
1908
|
-
contentLength: req.headers["content-length"] ?? "-",
|
|
1909
|
-
entryCount: Array.isArray(req.body?.entries) ? req.body.entries.length : "-",
|
|
1910
|
-
error,
|
|
1911
|
-
});
|
|
1912
|
-
this.handleRequestError(error, res);
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
/**
|
|
1916
|
-
* Binary variant of putEntries (sync-v5, phase 3). The request body is
|
|
1917
|
-
* octet-stream framing; `dbId` travels in the message header JSON because
|
|
1918
|
-
* there is no JSON body. Receipts are returned as JSON like putEntries.
|
|
1919
|
-
*/
|
|
1920
|
-
async handlePutEntriesBinary(req, res) {
|
|
1921
|
-
let stage = "extract-token";
|
|
1922
|
-
try {
|
|
1923
|
-
const token = this.extractToken(req);
|
|
1924
|
-
stage = "decode-body";
|
|
1925
|
-
if (!Buffer.isBuffer(req.body)) {
|
|
1926
|
-
res.status(400).json({ error: `putEntriesBinary requires a ${binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE} body` });
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
const message = (0, binaryEntryFraming_1.decodeBinaryEntryMessage)(new Uint8Array(req.body.buffer, req.body.byteOffset, req.body.byteLength));
|
|
1930
|
-
if (message.header.format !== binaryEntryFraming_1.BINARY_PUT_ENTRIES_FORMAT) {
|
|
1931
|
-
res.status(400).json({ error: `Unsupported binary entry format: ${String(message.header.format)}` });
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
|
-
const validDbId = this.validateDbId(message.header.dbId);
|
|
1935
|
-
(0, validation_1.validateArraySize)(message.entries, validation_1.MAX_PUT_ENTRIES, "entries");
|
|
1936
|
-
console.log(`[MindooDBServer] putEntriesBinary request tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${message.entries.length} content-length=${req.headers["content-length"] ?? "-"}`);
|
|
1937
|
-
stage = "load-server-store";
|
|
1938
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1939
|
-
stage = "deserialize-entries";
|
|
1940
|
-
const deserializedEntries = message.entries.map((frame, index) => {
|
|
1941
|
-
const meta = frame.meta;
|
|
1942
|
-
try {
|
|
1943
|
-
return {
|
|
1944
|
-
...this.deserializeEntryMetadata(meta),
|
|
1945
|
-
encryptedData: frame.payload,
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
catch (error) {
|
|
1949
|
-
console.error("[MindooDBServer] putEntriesBinary failed while deserializing entry", {
|
|
1950
|
-
index,
|
|
1951
|
-
id: meta?.id ?? "-",
|
|
1952
|
-
type: meta?.entryType ?? "-",
|
|
1953
|
-
docId: meta?.docId ?? "-",
|
|
1954
|
-
payloadLength: frame.payload?.byteLength ?? "-",
|
|
1955
|
-
error,
|
|
1956
|
-
});
|
|
1957
|
-
throw error;
|
|
1958
|
-
}
|
|
1959
|
-
});
|
|
1960
|
-
stage = "store-entries";
|
|
1961
|
-
const ack = await serverStore.handlePutEntries(token, deserializedEntries);
|
|
1962
|
-
stage = "respond-success";
|
|
1963
|
-
res.json({
|
|
1964
|
-
success: true,
|
|
1965
|
-
receipts: ack.receipts.map((e) => this.serializeEntryMetadata(e)),
|
|
1966
|
-
rejected: ack.rejected,
|
|
1967
|
-
});
|
|
1968
|
-
// Announce the accepted write to SSE subscribers (sync-v5, phase 5).
|
|
1969
|
-
if (ack.receipts.length > 0) {
|
|
1970
|
-
this.publishSyncChangeEvent(req.tenantId, validDbId, this.getStoreKindFromRequest(req), serverStore, token);
|
|
1971
|
-
}
|
|
1972
|
-
// Same post-response purge hook as the JSON putEntries path.
|
|
1973
|
-
if (validDbId === "directory") {
|
|
1974
|
-
const tenantId = req.tenantId;
|
|
1975
|
-
void this.tenantManager.executePendingPurges(tenantId).catch((error) => {
|
|
1976
|
-
console.error(`[MindooDBServer] executePendingPurges failed for ${tenantId}:`, error);
|
|
1977
|
-
});
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
catch (error) {
|
|
1981
|
-
console.error("[MindooDBServer] putEntriesBinary failed", {
|
|
1982
|
-
stage,
|
|
1983
|
-
tenantId: req.tenantId ?? "-",
|
|
1984
|
-
contentLength: req.headers["content-length"] ?? "-",
|
|
1985
|
-
error,
|
|
1986
|
-
});
|
|
1987
|
-
this.handleRequestError(error, res);
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
async handleHasEntries(req, res) {
|
|
1991
|
-
try {
|
|
1992
|
-
const token = this.extractToken(req);
|
|
1993
|
-
const { dbId, ids } = req.body;
|
|
1994
|
-
const validDbId = this.validateDbId(dbId);
|
|
1995
|
-
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
1996
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1997
|
-
const existingIds = await serverStore.handleHasEntries(token, ids || []);
|
|
1998
|
-
res.json({ ids: existingIds });
|
|
1999
|
-
}
|
|
2000
|
-
catch (error) {
|
|
2001
|
-
this.handleRequestError(error, res);
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
async handleGetAllIds(req, res) {
|
|
2005
|
-
try {
|
|
2006
|
-
const token = this.extractToken(req);
|
|
2007
|
-
const validDbId = this.validateDbId(req.query.dbId);
|
|
2008
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
2009
|
-
const ids = await serverStore.handleGetAllIds(token);
|
|
2010
|
-
res.json({ ids });
|
|
2011
|
-
}
|
|
2012
|
-
catch (error) {
|
|
2013
|
-
this.handleRequestError(error, res);
|
|
2014
|
-
}
|
|
733
|
+
this.openSyncEventStream(req, res, response.events);
|
|
2015
734
|
}
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
}
|
|
2025
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
2026
|
-
const ids = await serverStore.handleResolveDependencies(token, startId, options);
|
|
2027
|
-
res.json({ ids });
|
|
2028
|
-
}
|
|
2029
|
-
catch (error) {
|
|
2030
|
-
this.handleRequestError(error, res);
|
|
735
|
+
openSyncEventStream(req, res, subscription) {
|
|
736
|
+
res.status(200);
|
|
737
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
738
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
739
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
740
|
+
if (!req.httpVersion.startsWith("2")) {
|
|
741
|
+
// Forbidden (and unnecessary) on HTTP/2 streams.
|
|
742
|
+
res.setHeader("Connection", "keep-alive");
|
|
2031
743
|
}
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
const
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
if (!docId) {
|
|
2039
|
-
res.status(400).json({ error: "docId is required" });
|
|
2040
|
-
return;
|
|
744
|
+
res.flushHeaders();
|
|
745
|
+
res.write((0, MindooDBServerCore_1.formatSyncHelloEvent)());
|
|
746
|
+
const unsubscribe = this.syncEventBus.subscribe((event) => {
|
|
747
|
+
const frame = (0, MindooDBServerCore_1.formatSyncChangeEvent)(subscription, event);
|
|
748
|
+
if (frame) {
|
|
749
|
+
res.write(frame);
|
|
2041
750
|
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
res.
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const { dbId, docIds, options } = req.body;
|
|
2054
|
-
const validDbId = this.validateDbId(dbId);
|
|
2055
|
-
(0, validation_1.validateArraySize)(docIds, validation_1.MAX_HAVE_IDS, "docIds");
|
|
2056
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
2057
|
-
const batchPlan = await serverStore.handlePlanDocumentMaterializationBatch(token, docIds || [], options);
|
|
2058
|
-
res.json({ batchPlan: batchPlan });
|
|
2059
|
-
}
|
|
2060
|
-
catch (error) {
|
|
2061
|
-
this.handleRequestError(error, res);
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
async handlePlanAttachmentReadByWalkingMetadata(req, res) {
|
|
2065
|
-
try {
|
|
2066
|
-
const token = this.extractToken(req);
|
|
2067
|
-
const { dbId, lastChunkId, attachmentSize, options } = req.body;
|
|
2068
|
-
const validDbId = this.validateDbId(dbId);
|
|
2069
|
-
if (!lastChunkId) {
|
|
2070
|
-
res.status(400).json({ error: "lastChunkId is required" });
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
if (typeof attachmentSize !== "number" || !Number.isFinite(attachmentSize)) {
|
|
2074
|
-
res.status(400).json({ error: "attachmentSize is required" });
|
|
2075
|
-
return;
|
|
2076
|
-
}
|
|
2077
|
-
if (!options || typeof options.startByte !== "number") {
|
|
2078
|
-
res.status(400).json({ error: "options.startByte is required" });
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
2082
|
-
const plan = await serverStore.handlePlanAttachmentReadByWalkingMetadata(token, lastChunkId, attachmentSize, options);
|
|
2083
|
-
res.json({ plan: plan });
|
|
2084
|
-
}
|
|
2085
|
-
catch (error) {
|
|
2086
|
-
this.handleRequestError(error, res);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
// ==================== Management Handlers ====================
|
|
2090
|
-
// ==================== Helper Methods ====================
|
|
2091
|
-
/**
|
|
2092
|
-
* Derive the StoreKind from the request path. Both "docs" and
|
|
2093
|
-
* "attachments" share the same handler implementations; the store kind
|
|
2094
|
-
* selects which on-disk append-only store to operate against.
|
|
2095
|
-
*/
|
|
2096
|
-
getStoreKindFromRequest(req) {
|
|
2097
|
-
const trimmedPath = req.path.replace(/\/$/, "");
|
|
2098
|
-
if (trimmedPath.startsWith("/sync/docs/") || trimmedPath === "/sync/docs/capabilities" || trimmedPath === "/sync/docs/getAllIds") {
|
|
2099
|
-
return types_2.StoreKind.docs;
|
|
2100
|
-
}
|
|
2101
|
-
if (trimmedPath.startsWith("/sync/attachments/") || trimmedPath === "/sync/attachments/capabilities" || trimmedPath === "/sync/attachments/getAllIds") {
|
|
2102
|
-
return types_2.StoreKind.attachments;
|
|
2103
|
-
}
|
|
2104
|
-
throw new Error(`Unsupported sync store kind for path: ${req.path}`);
|
|
2105
|
-
}
|
|
2106
|
-
extractToken(req) {
|
|
2107
|
-
const auth = req.headers.authorization;
|
|
2108
|
-
if (!auth || !auth.startsWith("Bearer ")) {
|
|
2109
|
-
throw new types_3.NetworkError("INVALID_TOKEN", "Missing or invalid Authorization header");
|
|
2110
|
-
}
|
|
2111
|
-
return auth.substring(7);
|
|
2112
|
-
}
|
|
2113
|
-
/** Map domain errors to HTTP status codes; unknown errors become 500. */
|
|
2114
|
-
handleRequestError(error, res) {
|
|
2115
|
-
if (error instanceof validation_1.ValidationError) {
|
|
2116
|
-
res.status(400).json({ error: error.message });
|
|
2117
|
-
return;
|
|
2118
|
-
}
|
|
2119
|
-
if (error instanceof Error && error.name === "NetworkError") {
|
|
2120
|
-
const networkError = error;
|
|
2121
|
-
const status = this.getStatusForErrorType(networkError.type);
|
|
2122
|
-
// The status alone loses the distinction the client needs: several
|
|
2123
|
-
// NetworkErrorTypes map onto the same code (USER_REVOKED and
|
|
2124
|
-
// ACCESS_DENIED both become 403), and a client that has to guess falls
|
|
2125
|
-
// back to matching on the message text. Carry the type so it does not
|
|
2126
|
-
// have to.
|
|
2127
|
-
res.status(status).json({ error: networkError.message, type: networkError.type });
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
if ((0, errors_1.isMissingDependencyError)(error)) {
|
|
2131
|
-
// Not a server fault: the requested chain is incomplete in this store.
|
|
2132
|
-
// Pass the details on so the caller can tell what is missing.
|
|
2133
|
-
res.status(409).json({ error: error.message, type: "MISSING_DEPENDENCY" });
|
|
2134
|
-
return;
|
|
2135
|
-
}
|
|
2136
|
-
console.error("[MindooDBServer] Request error:", error);
|
|
2137
|
-
res.status(500).json({ error: "Internal server error" });
|
|
2138
|
-
}
|
|
2139
|
-
getStatusForErrorType(type) {
|
|
2140
|
-
switch (type) {
|
|
2141
|
-
case "INVALID_TOKEN":
|
|
2142
|
-
return 401;
|
|
2143
|
-
case "USER_REVOKED":
|
|
2144
|
-
return 403;
|
|
2145
|
-
case "USER_NOT_FOUND":
|
|
2146
|
-
return 404;
|
|
2147
|
-
case "INVALID_SIGNATURE":
|
|
2148
|
-
return 403;
|
|
2149
|
-
case "CHALLENGE_EXPIRED":
|
|
2150
|
-
return 401;
|
|
2151
|
-
case "ACCESS_DENIED":
|
|
2152
|
-
return 403;
|
|
2153
|
-
default:
|
|
2154
|
-
return 500;
|
|
2155
|
-
}
|
|
751
|
+
});
|
|
752
|
+
const heartbeat = setInterval(() => {
|
|
753
|
+
res.write((0, MindooDBServerCore_1.formatSyncHeartbeat)(Date.now()));
|
|
754
|
+
}, MindooDBServerCore_1.SYNC_EVENT_HEARTBEAT_MS);
|
|
755
|
+
const cleanup = () => {
|
|
756
|
+
clearInterval(heartbeat);
|
|
757
|
+
unsubscribe();
|
|
758
|
+
};
|
|
759
|
+
req.on("close", cleanup);
|
|
760
|
+
res.on("close", cleanup);
|
|
761
|
+
console.log(`[MindooDBServer] SSE subscriber connected tenant=${subscription.tenantId} storeKind=${subscription.storeKind} subscribers=${this.syncEventBus.listenerCount}`);
|
|
2156
762
|
}
|
|
2157
763
|
/**
|
|
2158
764
|
* Express error handler (4-arg signature). Handles body-parser errors
|
|
@@ -2193,25 +799,6 @@ class MindooDBServer {
|
|
|
2193
799
|
});
|
|
2194
800
|
res.status(500).json({ error: "Internal server error" });
|
|
2195
801
|
}
|
|
2196
|
-
// ==================== Serialization ====================
|
|
2197
|
-
// Entries are stored with binary fields (Uint8Array) internally but
|
|
2198
|
-
// transmitted as base64 strings over JSON. Shared with the client
|
|
2199
|
-
// (entryWireCodec) so signed trailing blocks cannot drift off the wire.
|
|
2200
|
-
serializeEntryMetadata(metadata) {
|
|
2201
|
-
return (0, entryWireCodec_1.serializeEntryMetadata)(metadata);
|
|
2202
|
-
}
|
|
2203
|
-
deserializeEntryMetadata(serialized) {
|
|
2204
|
-
return (0, entryWireCodec_1.deserializeEntryMetadata)(serialized);
|
|
2205
|
-
}
|
|
2206
|
-
deserializeEntry(serialized) {
|
|
2207
|
-
return (0, entryWireCodec_1.deserializeStoreEntry)(serialized);
|
|
2208
|
-
}
|
|
2209
|
-
uint8ArrayToBase64(bytes) {
|
|
2210
|
-
return Buffer.from(bytes).toString("base64");
|
|
2211
|
-
}
|
|
2212
|
-
base64ToUint8Array(base64) {
|
|
2213
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
2214
|
-
}
|
|
2215
802
|
}
|
|
2216
803
|
exports.MindooDBServer = MindooDBServer;
|
|
2217
804
|
//# sourceMappingURL=MindooDBServer.js.map
|