mindoodb 0.0.84 → 0.0.86
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 +1 -0
- package/README.md +6 -4
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +15 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +75 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +9 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +38 -10
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- 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 +43 -1
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +209 -87
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +14 -0
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +46 -0
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +10 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +79 -8
- package/dist/core/BaseMindooTenantDirectory.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/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +1 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts +8 -0
- package/dist/core/appendonlystores/network/AuthenticationService.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/AuthenticationService.js +19 -0
- package/dist/core/appendonlystores/network/AuthenticationService.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/network/NetworkTransport.d.ts +7 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.d.ts +23 -0
- package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/types.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.map +1 -1
- package/dist/core/appendonlystores/sqlite/SqliteContentAddressedStore.js +21 -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 +253 -168
- package/dist/core/appendonlystores/syncStores.js.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +8 -0
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/types.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/types.d.ts +61 -2
- 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/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/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 +1 -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 -1568
- 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/compressionMiddleware.d.ts +14 -0
- package/dist/node/server/compressionMiddleware.d.ts.map +1 -1
- package/dist/node/server/compressionMiddleware.js +46 -3
- package/dist/node/server/compressionMiddleware.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 +165 -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 +228 -0
- package/dist/server/SystemAdminAuth.js.map +1 -0
- package/dist/server/TenantManager.d.ts +330 -0
- package/dist/server/TenantManager.d.ts.map +1 -0
- package/dist/server/TenantManager.js +1186 -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 +1403 -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 +208 -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 +1071 -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
|
@@ -0,0 +1,1403 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-neutral route handlers of the MindooDB server.
|
|
4
|
+
*
|
|
5
|
+
* Every endpoint of the HTTP API that does not depend on the host runtime —
|
|
6
|
+
* discovery, device discovery, system administration, tenant auth, the sync
|
|
7
|
+
* protocol and the RFC 3161 proxy — is implemented here against
|
|
8
|
+
* {@link ServerRequest}/{@link ServerResponse}. Hosts own the transport: the
|
|
9
|
+
* Node server mounts these handlers on Express with its middleware stack, a
|
|
10
|
+
* serverless host calls them from its `fetch` handler.
|
|
11
|
+
*
|
|
12
|
+
* This module must stay free of Node built-ins; a test bundles it for a
|
|
13
|
+
* neutral platform to keep it that way.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.MindooDBServerCore = exports.SYNC_STORE_KINDS = exports.SYNC_ROUTES = exports.SYNC_EVENT_HEARTBEAT_MS = void 0;
|
|
17
|
+
exports.parseBodySizeLimitToBytes = parseBodySizeLimitToBytes;
|
|
18
|
+
exports.errorResponse = errorResponse;
|
|
19
|
+
exports.formatSyncHelloEvent = formatSyncHelloEvent;
|
|
20
|
+
exports.formatSyncChangeEvent = formatSyncChangeEvent;
|
|
21
|
+
exports.formatSyncHeartbeat = formatSyncHeartbeat;
|
|
22
|
+
const errors_1 = require("../../core/errors");
|
|
23
|
+
const binaryEntryFraming_1 = require("../../core/appendonlystores/network/binaryEntryFraming");
|
|
24
|
+
const entryWireCodec_1 = require("../../core/appendonlystores/network/entryWireCodec");
|
|
25
|
+
const types_1 = require("../../core/appendonlystores/types");
|
|
26
|
+
const types_2 = require("../../core/appendonlystores/network/types");
|
|
27
|
+
const urlSafety_1 = require("../../core/utils/urlSafety");
|
|
28
|
+
const base64_1 = require("../../core/utils/base64");
|
|
29
|
+
const CapabilityMatcher_1 = require("../CapabilityMatcher");
|
|
30
|
+
const DeviceDiscoveryService_1 = require("../DeviceDiscoveryService");
|
|
31
|
+
const serverConfig_1 = require("../serverConfig");
|
|
32
|
+
const types_3 = require("../peer/types");
|
|
33
|
+
const validation_1 = require("../validation");
|
|
34
|
+
const timestampProxy_1 = require("../timestampProxy");
|
|
35
|
+
const types_4 = require("./types");
|
|
36
|
+
/** Parse a human-readable size string ("5mb", "1024", "100kb") into bytes. */
|
|
37
|
+
function parseBodySizeLimitToBytes(limit) {
|
|
38
|
+
const normalized = limit.trim().toLowerCase();
|
|
39
|
+
if (!normalized) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (/^\d+$/.test(normalized)) {
|
|
43
|
+
return Number(normalized);
|
|
44
|
+
}
|
|
45
|
+
const match = normalized.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)$/);
|
|
46
|
+
if (!match) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const value = Number(match[1]);
|
|
50
|
+
const unit = match[2];
|
|
51
|
+
const multiplier = unit === "b"
|
|
52
|
+
? 1
|
|
53
|
+
: unit === "kb"
|
|
54
|
+
? 1024
|
|
55
|
+
: unit === "mb"
|
|
56
|
+
? 1024 * 1024
|
|
57
|
+
: 1024 * 1024 * 1024;
|
|
58
|
+
return Math.round(value * multiplier);
|
|
59
|
+
}
|
|
60
|
+
/** HTTP status of each {@link NetworkError} type. */
|
|
61
|
+
function getStatusForErrorType(type) {
|
|
62
|
+
switch (type) {
|
|
63
|
+
case "INVALID_TOKEN":
|
|
64
|
+
return 401;
|
|
65
|
+
case "USER_REVOKED":
|
|
66
|
+
return 403;
|
|
67
|
+
case "USER_NOT_FOUND":
|
|
68
|
+
return 404;
|
|
69
|
+
case "INVALID_SIGNATURE":
|
|
70
|
+
return 403;
|
|
71
|
+
case "CHALLENGE_EXPIRED":
|
|
72
|
+
return 401;
|
|
73
|
+
case "ACCESS_DENIED":
|
|
74
|
+
return 403;
|
|
75
|
+
default:
|
|
76
|
+
return 500;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Map domain errors to HTTP responses; unknown errors become 500. */
|
|
80
|
+
function errorResponse(error) {
|
|
81
|
+
if (error instanceof validation_1.ValidationError) {
|
|
82
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
83
|
+
}
|
|
84
|
+
if (error instanceof Error && error.name === "NetworkError") {
|
|
85
|
+
const networkError = error;
|
|
86
|
+
const status = getStatusForErrorType(networkError.type);
|
|
87
|
+
// The status alone loses the distinction the client needs: several
|
|
88
|
+
// NetworkErrorTypes map onto the same code (USER_REVOKED and
|
|
89
|
+
// ACCESS_DENIED both become 403), and a client that has to guess falls
|
|
90
|
+
// back to matching on the message text. Carry the type so it does not
|
|
91
|
+
// have to.
|
|
92
|
+
return (0, types_4.jsonResponse)(status, { error: networkError.message, type: networkError.type });
|
|
93
|
+
}
|
|
94
|
+
if ((0, errors_1.isMissingDependencyError)(error)) {
|
|
95
|
+
// Not a server fault: the requested chain is incomplete in this store.
|
|
96
|
+
// Pass the details on so the caller can tell what is missing.
|
|
97
|
+
return (0, types_4.jsonResponse)(409, { error: error.message, type: "MISSING_DEPENDENCY" });
|
|
98
|
+
}
|
|
99
|
+
console.error("[MindooDBServer] Request error:", error);
|
|
100
|
+
return (0, types_4.jsonResponse)(500, { error: "Internal server error" });
|
|
101
|
+
}
|
|
102
|
+
function extractToken(req) {
|
|
103
|
+
const auth = req.headers.authorization;
|
|
104
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
105
|
+
throw new types_2.NetworkError("INVALID_TOKEN", "Missing or invalid Authorization header");
|
|
106
|
+
}
|
|
107
|
+
return auth.substring(7);
|
|
108
|
+
}
|
|
109
|
+
function validateDbId(dbId) {
|
|
110
|
+
return (0, validation_1.validateIdentifier)(dbId, "dbId");
|
|
111
|
+
}
|
|
112
|
+
/** `event: hello` frame that opens every sync event stream. */
|
|
113
|
+
function formatSyncHelloEvent() {
|
|
114
|
+
return `event: hello\ndata: ${JSON.stringify({ protocolVersion: "sync-v5" })}\n\n`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* `event: change` frame for a subscriber of `tenantId`/`storeKind`, or null
|
|
118
|
+
* when the event belongs to someone else. Events carry only metadata.
|
|
119
|
+
*/
|
|
120
|
+
function formatSyncChangeEvent(subscription, event) {
|
|
121
|
+
if (event.tenantId !== subscription.tenantId || event.storeKind !== subscription.storeKind) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return `event: change\ndata: ${JSON.stringify({
|
|
125
|
+
dbId: event.dbId,
|
|
126
|
+
storeKind: event.storeKind,
|
|
127
|
+
epoch: event.epoch,
|
|
128
|
+
maxReceiptOrder: event.maxReceiptOrder,
|
|
129
|
+
})}\n\n`;
|
|
130
|
+
}
|
|
131
|
+
/** Comment frame that keeps proxies from closing an idle stream. */
|
|
132
|
+
function formatSyncHeartbeat(now) {
|
|
133
|
+
return `: heartbeat ${now}\n\n`;
|
|
134
|
+
}
|
|
135
|
+
/** Interval of {@link formatSyncHeartbeat} frames. */
|
|
136
|
+
exports.SYNC_EVENT_HEARTBEAT_MS = 30000;
|
|
137
|
+
/** Sync endpoints mounted under `/:tenantId/sync/{docs,attachments}/`. */
|
|
138
|
+
exports.SYNC_ROUTES = [
|
|
139
|
+
{ method: "POST", name: "findNewEntries" },
|
|
140
|
+
{ method: "POST", name: "findNewEntriesForDoc" },
|
|
141
|
+
{ method: "POST", name: "findEntries" },
|
|
142
|
+
{ method: "POST", name: "scanEntriesSince" },
|
|
143
|
+
{ method: "POST", name: "getStoreHead" },
|
|
144
|
+
{ method: "POST", name: "getIdBloomSummary" },
|
|
145
|
+
{ method: "POST", name: "getCompactionStatus" },
|
|
146
|
+
{ method: "GET", name: "capabilities" },
|
|
147
|
+
// SSE live change feed (sync-v5, phase 5): one long-lived GET per
|
|
148
|
+
// subscriber; putEntries publishes into the event bus.
|
|
149
|
+
{ method: "GET", name: "events" },
|
|
150
|
+
{ method: "POST", name: "getEntries" },
|
|
151
|
+
// Binary wire format v2 (sync-v5, phase 3): octet-stream framing for the
|
|
152
|
+
// two payload-heavy endpoints. putEntriesBinary takes a raw body.
|
|
153
|
+
{ method: "POST", name: "getEntriesBinary" },
|
|
154
|
+
{ method: "POST", name: "putEntriesBinary", rawBodyType: binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE },
|
|
155
|
+
{ method: "POST", name: "getEntryMetadata" },
|
|
156
|
+
{ method: "POST", name: "putEntries" },
|
|
157
|
+
{ method: "POST", name: "hasEntries" },
|
|
158
|
+
{ method: "GET", name: "getAllIds" },
|
|
159
|
+
{ method: "POST", name: "resolveDependencies" },
|
|
160
|
+
{ method: "POST", name: "planDocumentMaterialization" },
|
|
161
|
+
{ method: "POST", name: "planDocumentMaterializationBatch" },
|
|
162
|
+
{ method: "POST", name: "planAttachmentReadByWalkingMetadata" },
|
|
163
|
+
];
|
|
164
|
+
exports.SYNC_STORE_KINDS = [types_1.StoreKind.docs, types_1.StoreKind.attachments];
|
|
165
|
+
class MindooDBServerCore {
|
|
166
|
+
constructor(options) {
|
|
167
|
+
this.options = options;
|
|
168
|
+
this.jsonBodyLimitBytes = parseBodySizeLimitToBytes(options.jsonBodyLimit);
|
|
169
|
+
}
|
|
170
|
+
get tenantManager() {
|
|
171
|
+
return this.options.tenantManager;
|
|
172
|
+
}
|
|
173
|
+
runInBackground(task) {
|
|
174
|
+
if (this.options.runInBackground) {
|
|
175
|
+
this.options.runInBackground(task);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
void task;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
getJsonBodyLimit() {
|
|
182
|
+
return { limit: this.options.jsonBodyLimit, bytes: this.jsonBodyLimitBytes };
|
|
183
|
+
}
|
|
184
|
+
// ==================== Discovery ====================
|
|
185
|
+
health() {
|
|
186
|
+
return (0, types_4.jsonResponse)(200, { status: "ok", timestamp: Date.now() });
|
|
187
|
+
}
|
|
188
|
+
serverInfo() {
|
|
189
|
+
const info = this.tenantManager.getServerPublicInfo();
|
|
190
|
+
if (!info) {
|
|
191
|
+
return (0, types_4.jsonResponse)(503, { error: "Server identity not initialized" });
|
|
192
|
+
}
|
|
193
|
+
return (0, types_4.jsonResponse)(200, {
|
|
194
|
+
...info,
|
|
195
|
+
// Its own mesh role, so `add-to-network` can propagate it into the
|
|
196
|
+
// other side's trusted-servers entry instead of asking the operator to
|
|
197
|
+
// repeat it on every node.
|
|
198
|
+
clusterRole: this.options.getServerConfig().cluster?.role ?? "peer",
|
|
199
|
+
maxJsonRequestBodyLimit: this.options.jsonBodyLimit,
|
|
200
|
+
maxJsonRequestBodyBytes: this.jsonBodyLimitBytes ?? undefined,
|
|
201
|
+
// Lets a client decide between calling a browser-reachable TSA directly
|
|
202
|
+
// and routing through this server, per tenant and without a probe.
|
|
203
|
+
timestamping: {
|
|
204
|
+
enabled: this.options.tsaProviders.length > 0,
|
|
205
|
+
providers: (0, timestampProxy_1.toPublicTsaProviders)(this.options.tsaProviders),
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Mirrors of one tenant, for client failover.
|
|
211
|
+
*
|
|
212
|
+
* Scoped to the tenant in the query on purpose: a client asking about
|
|
213
|
+
* tenant A must not learn which other tenants this server replicates, or
|
|
214
|
+
* to whom. Unauthenticated like the other discovery endpoints — a client
|
|
215
|
+
* needs it precisely when it cannot reach a server to authenticate with.
|
|
216
|
+
*/
|
|
217
|
+
clusterMirrors(req) {
|
|
218
|
+
const tenantId = typeof req.query.tenantId === "string" ? req.query.tenantId.toLowerCase() : "";
|
|
219
|
+
if (!tenantId) {
|
|
220
|
+
return (0, types_4.jsonResponse)(400, { error: "tenantId query parameter is required" });
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
(0, validation_1.validateTenantId)(tenantId);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return (0, types_4.jsonResponse)(400, { error: "Invalid tenantId format" });
|
|
227
|
+
}
|
|
228
|
+
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
229
|
+
return (0, types_4.jsonResponse)(404, { error: "Tenant not found on server" });
|
|
230
|
+
}
|
|
231
|
+
return (0, types_4.jsonResponse)(200, {
|
|
232
|
+
tenantId,
|
|
233
|
+
mirrors: this.options.cluster?.mirrorsForTenant(tenantId) ?? [],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async publicInfosFingerprints(req) {
|
|
237
|
+
const tenantId = (req.params.tenantId ?? "").toLowerCase();
|
|
238
|
+
try {
|
|
239
|
+
(0, validation_1.validateTenantId)(tenantId);
|
|
240
|
+
const fingerprints = await this.tenantManager.listTenantPublicInfosFingerprints(tenantId);
|
|
241
|
+
if (fingerprints.length === 0) {
|
|
242
|
+
return (0, types_4.jsonResponse)(404, { error: "Tenant not found on server" });
|
|
243
|
+
}
|
|
244
|
+
const response = {
|
|
245
|
+
tenantId,
|
|
246
|
+
fingerprints,
|
|
247
|
+
};
|
|
248
|
+
return (0, types_4.jsonResponse)(200, response);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error instanceof validation_1.ValidationError) {
|
|
252
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
253
|
+
}
|
|
254
|
+
if (error instanceof Error && error.message.includes("not found")) {
|
|
255
|
+
return (0, types_4.jsonResponse)(404, { error: "Tenant not found on server" });
|
|
256
|
+
}
|
|
257
|
+
console.error("[MindooDBServer] Error reading tenant publicInfos fingerprints:", error);
|
|
258
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to read tenant publicInfos fingerprints" });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
// ==================== Device Discovery (server-wide) ====================
|
|
262
|
+
async deviceChallenge(req) {
|
|
263
|
+
try {
|
|
264
|
+
const { signingPublicKey } = req.body ?? {};
|
|
265
|
+
if (typeof signingPublicKey !== "string" || !signingPublicKey.trim()) {
|
|
266
|
+
return (0, types_4.jsonResponse)(400, { error: "signingPublicKey is required" });
|
|
267
|
+
}
|
|
268
|
+
(0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "signingPublicKey");
|
|
269
|
+
const challenge = this.options.deviceDiscovery.createChallenge(signingPublicKey);
|
|
270
|
+
return (0, types_4.jsonResponse)(200, { challenge });
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
return errorResponse(error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async deviceDiscover(req) {
|
|
277
|
+
try {
|
|
278
|
+
const { challenge, signature } = req.body ?? {};
|
|
279
|
+
if (typeof challenge !== "string" || !challenge.trim()) {
|
|
280
|
+
return (0, types_4.jsonResponse)(400, { error: "challenge is required" });
|
|
281
|
+
}
|
|
282
|
+
if (typeof signature !== "string" || !signature.trim()) {
|
|
283
|
+
return (0, types_4.jsonResponse)(400, { error: "signature is required" });
|
|
284
|
+
}
|
|
285
|
+
(0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
|
|
286
|
+
(0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
|
|
287
|
+
const result = await this.options.deviceDiscovery.discover(challenge, (0, base64_1.base64ToBytes)(signature));
|
|
288
|
+
return (0, types_4.jsonResponse)(200, result);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
if (error instanceof DeviceDiscoveryService_1.DeviceDiscoveryAuthError) {
|
|
292
|
+
return (0, types_4.jsonResponse)(401, { error: error.message });
|
|
293
|
+
}
|
|
294
|
+
return errorResponse(error);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// ==================== System Admin Auth ====================
|
|
298
|
+
/**
|
|
299
|
+
* JWT check for /system/* routes (excluding /system/auth/*). Validates the
|
|
300
|
+
* bearer token, then checks capabilities for (method, fullPath, username,
|
|
301
|
+
* publicsignkey). Returns the rejection, or null and sets `req.systemAdmin`.
|
|
302
|
+
*
|
|
303
|
+
* `DELETE /system/tenants/:tenantId` is also allowed when the JWT signing
|
|
304
|
+
* key is that tenant's registered `adminSigningPublicKey` (own-tenant
|
|
305
|
+
* delete). Sub-resources and other tenants stay capability-gated.
|
|
306
|
+
*/
|
|
307
|
+
async systemAdminGuard(req, fullPath) {
|
|
308
|
+
const auth = req.headers.authorization;
|
|
309
|
+
if (!auth || !auth.startsWith("Bearer ")) {
|
|
310
|
+
return (0, types_4.jsonResponse)(401, { error: "Missing or invalid Authorization header" });
|
|
311
|
+
}
|
|
312
|
+
const token = auth.substring(7);
|
|
313
|
+
const payload = await this.options.systemAdminAuth.validateToken(token);
|
|
314
|
+
if (!payload) {
|
|
315
|
+
return (0, types_4.jsonResponse)(401, { error: "Invalid or expired token" });
|
|
316
|
+
}
|
|
317
|
+
const method = req.method.toUpperCase();
|
|
318
|
+
const capabilityAuthorized = this.options.capabilityMatcher.isAuthorized(method, fullPath, payload.sub, payload.publicsignkey);
|
|
319
|
+
const ownerDeleteAuthorized = !capabilityAuthorized &&
|
|
320
|
+
this.isOwnTenantDeleteAuthorized(method, fullPath, payload.publicsignkey);
|
|
321
|
+
if (!capabilityAuthorized && !ownerDeleteAuthorized) {
|
|
322
|
+
return (0, types_4.jsonResponse)(403, { error: "Forbidden: insufficient capabilities" });
|
|
323
|
+
}
|
|
324
|
+
req.systemAdmin = {
|
|
325
|
+
username: payload.sub,
|
|
326
|
+
publicsignkey: payload.publicsignkey,
|
|
327
|
+
};
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Owner bypass for deleting the caller's own tenant. Matches only
|
|
332
|
+
* `DELETE /system/tenants/:tenantId` (no sub-resource) and requires the
|
|
333
|
+
* JWT public key to equal that tenant's stored admin signing key.
|
|
334
|
+
*/
|
|
335
|
+
isOwnTenantDeleteAuthorized(method, fullPath, publicsignkey) {
|
|
336
|
+
if (method !== "DELETE") {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const tenantId = (0, CapabilityMatcher_1.parseSystemTenantItemPath)(fullPath);
|
|
340
|
+
if (!tenantId) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
try {
|
|
344
|
+
(0, validation_1.validateTenantIdFormat)(tenantId);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
return this.tenantManager.isTenantAdminSigningKeyFor(tenantId, publicsignkey);
|
|
350
|
+
}
|
|
351
|
+
async systemChallenge(req) {
|
|
352
|
+
try {
|
|
353
|
+
const { username, publicsignkey } = req.body;
|
|
354
|
+
if (!username || typeof username !== "string") {
|
|
355
|
+
return (0, types_4.jsonResponse)(400, { error: "username is required" });
|
|
356
|
+
}
|
|
357
|
+
if (!publicsignkey || typeof publicsignkey !== "string") {
|
|
358
|
+
return (0, types_4.jsonResponse)(400, { error: "publicsignkey is required" });
|
|
359
|
+
}
|
|
360
|
+
(0, validation_1.validateUsername)(username);
|
|
361
|
+
(0, validation_1.validateStringLength)(publicsignkey, validation_1.MAX_PEM_KEY_LENGTH, "publicsignkey");
|
|
362
|
+
const challenge = await this.options.systemAdminAuth.generateChallenge(username, publicsignkey);
|
|
363
|
+
return (0, types_4.jsonResponse)(200, { challenge });
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
if (error instanceof Error && error.message === "Unknown system admin principal") {
|
|
367
|
+
return (0, types_4.jsonResponse)(404, { error: "System admin principal not found" });
|
|
368
|
+
}
|
|
369
|
+
if (error instanceof validation_1.ValidationError) {
|
|
370
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
371
|
+
}
|
|
372
|
+
console.error("[MindooDBServer] System challenge error:", error);
|
|
373
|
+
return (0, types_4.jsonResponse)(500, { error: "Internal server error" });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
async systemAuthenticate(req) {
|
|
377
|
+
try {
|
|
378
|
+
const { challenge, signature } = req.body;
|
|
379
|
+
if (!challenge || !signature) {
|
|
380
|
+
return (0, types_4.jsonResponse)(400, { error: "challenge and signature are required" });
|
|
381
|
+
}
|
|
382
|
+
(0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
|
|
383
|
+
(0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
|
|
384
|
+
const result = await this.options.systemAdminAuth.authenticate(challenge, (0, base64_1.base64ToBytes)(signature));
|
|
385
|
+
// Return 401 on a failed authentication instead of 200 (audit, Low).
|
|
386
|
+
return (0, types_4.jsonResponse)(result.success ? 200 : 401, result);
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
if (error instanceof validation_1.ValidationError) {
|
|
390
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
391
|
+
}
|
|
392
|
+
console.error("[MindooDBServer] System authenticate error:", error);
|
|
393
|
+
return (0, types_4.jsonResponse)(500, { error: "Internal server error" });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// ==================== System: Tenants ====================
|
|
397
|
+
listTenants() {
|
|
398
|
+
try {
|
|
399
|
+
const tenants = this.tenantManager.listTenants();
|
|
400
|
+
const response = { tenants };
|
|
401
|
+
return (0, types_4.jsonResponse)(200, response);
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
console.error("[MindooDBServer] Error listing tenants:", error);
|
|
405
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to list tenants" });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Tenant registration is idempotent: re-POSTing with the same tenantId
|
|
410
|
+
* and matching $publicinfos key returns 200 instead of 409.
|
|
411
|
+
*/
|
|
412
|
+
async registerTenant(req) {
|
|
413
|
+
try {
|
|
414
|
+
const tenantId = (req.params.tenantId ?? "").toLowerCase();
|
|
415
|
+
try {
|
|
416
|
+
(0, validation_1.validateTenantId)(tenantId);
|
|
417
|
+
}
|
|
418
|
+
catch (validationError) {
|
|
419
|
+
return (0, types_4.jsonResponse)(400, {
|
|
420
|
+
error: validationError instanceof validation_1.ValidationError
|
|
421
|
+
? validationError.message
|
|
422
|
+
: "Invalid tenantId format",
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const request = {
|
|
426
|
+
...req.body,
|
|
427
|
+
tenantId,
|
|
428
|
+
};
|
|
429
|
+
if (!request.adminSigningPublicKey) {
|
|
430
|
+
return (0, types_4.jsonResponse)(400, { error: "adminSigningPublicKey is required" });
|
|
431
|
+
}
|
|
432
|
+
if (!request.adminEncryptionPublicKey) {
|
|
433
|
+
return (0, types_4.jsonResponse)(400, { error: "adminEncryptionPublicKey is required" });
|
|
434
|
+
}
|
|
435
|
+
if (request.adminUsername !== undefined) {
|
|
436
|
+
(0, validation_1.validateUsername)(request.adminUsername);
|
|
437
|
+
}
|
|
438
|
+
(0, validation_1.validateStringLength)(request.adminSigningPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "adminSigningPublicKey");
|
|
439
|
+
(0, validation_1.validateStringLength)(request.adminEncryptionPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "adminEncryptionPublicKey");
|
|
440
|
+
if (!request.encryptedPublicInfosKey && !request.publicInfosKey) {
|
|
441
|
+
return (0, types_4.jsonResponse)(400, { error: "encryptedPublicInfosKey or publicInfosKey is required" });
|
|
442
|
+
}
|
|
443
|
+
if (request.publicInfosKey !== undefined) {
|
|
444
|
+
(0, validation_1.validateStringLength)(request.publicInfosKey, validation_1.MAX_PEM_KEY_LENGTH, "publicInfosKey");
|
|
445
|
+
}
|
|
446
|
+
if (request.encryptedPublicInfosKey !== undefined) {
|
|
447
|
+
(0, validation_1.validateStringLength)(request.encryptedPublicInfosKey, validation_1.MAX_PEM_KEY_LENGTH, "encryptedPublicInfosKey");
|
|
448
|
+
}
|
|
449
|
+
const result = await this.tenantManager.registerTenant(request);
|
|
450
|
+
const response = {
|
|
451
|
+
success: true,
|
|
452
|
+
tenantId: result.context.tenantId,
|
|
453
|
+
created: result.created,
|
|
454
|
+
message: result.created
|
|
455
|
+
? `Tenant ${result.context.tenantId} registered successfully`
|
|
456
|
+
: `Tenant ${result.context.tenantId} already exists with matching $publicinfos key`,
|
|
457
|
+
};
|
|
458
|
+
return (0, types_4.jsonResponse)(result.created ? 201 : 200, response);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
if (error instanceof validation_1.ValidationError) {
|
|
462
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
463
|
+
}
|
|
464
|
+
if (error instanceof Error && error.message.includes("different $publicinfos key")) {
|
|
465
|
+
return (0, types_4.jsonResponse)(409, { error: error.message });
|
|
466
|
+
}
|
|
467
|
+
console.error("[MindooDBServer] Error registering tenant:", error);
|
|
468
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to register tenant" });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
updateTenant(req) {
|
|
472
|
+
try {
|
|
473
|
+
const tenantId = (req.params.tenantId ?? "").toLowerCase();
|
|
474
|
+
try {
|
|
475
|
+
(0, validation_1.validateTenantId)(tenantId);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return (0, types_4.jsonResponse)(400, { error: "Invalid tenantId format" });
|
|
479
|
+
}
|
|
480
|
+
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
481
|
+
return (0, types_4.jsonResponse)(404, { error: "Tenant not found" });
|
|
482
|
+
}
|
|
483
|
+
this.tenantManager.updateTenantConfig(tenantId, req.body);
|
|
484
|
+
return (0, types_4.jsonResponse)(200, { success: true, message: `Tenant ${tenantId} updated` });
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
if (error instanceof validation_1.ValidationError) {
|
|
488
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
489
|
+
}
|
|
490
|
+
console.error("[MindooDBServer] Error updating tenant:", error);
|
|
491
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to update tenant" });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async deleteTenant(req) {
|
|
495
|
+
try {
|
|
496
|
+
const tenantId = (req.params.tenantId ?? "").toLowerCase();
|
|
497
|
+
try {
|
|
498
|
+
(0, validation_1.validateTenantId)(tenantId);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return (0, types_4.jsonResponse)(400, { error: "Invalid tenantId format" });
|
|
502
|
+
}
|
|
503
|
+
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
504
|
+
return (0, types_4.jsonResponse)(404, { error: "Tenant not found" });
|
|
505
|
+
}
|
|
506
|
+
await this.tenantManager.removeTenant(tenantId);
|
|
507
|
+
return (0, types_4.jsonResponse)(200, { success: true, message: `Tenant ${tenantId} removed` });
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
console.error("[MindooDBServer] Error removing tenant:", error);
|
|
511
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to remove tenant" });
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ==================== System: Trusted Servers ====================
|
|
515
|
+
listTrustedServers() {
|
|
516
|
+
try {
|
|
517
|
+
return (0, types_4.jsonResponse)(200, { servers: this.tenantManager.listTrustedServers() });
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
console.error("[MindooDBServer] Error listing trusted servers:", error);
|
|
521
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to list trusted servers" });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
addTrustedServer(req) {
|
|
525
|
+
try {
|
|
526
|
+
const { name, signingPublicKey, encryptionPublicKey, url, direction, role, attachments } = req.body;
|
|
527
|
+
if (!name || !signingPublicKey || !encryptionPublicKey) {
|
|
528
|
+
return (0, types_4.jsonResponse)(400, { error: "name, signingPublicKey, and encryptionPublicKey are required" });
|
|
529
|
+
}
|
|
530
|
+
(0, validation_1.validateStringLength)(name, 256, "name");
|
|
531
|
+
(0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "signingPublicKey");
|
|
532
|
+
(0, validation_1.validateStringLength)(encryptionPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "encryptionPublicKey");
|
|
533
|
+
const server = { name, signingPublicKey, encryptionPublicKey };
|
|
534
|
+
// Replication settings are optional: an entry without a url is a trust
|
|
535
|
+
// relationship only (inbound peers may authenticate, we never dial).
|
|
536
|
+
if (url !== undefined) {
|
|
537
|
+
(0, validation_1.validateStringLength)(url, 2048, "url");
|
|
538
|
+
const allowInsecure = this.options.allowInsecureSyncUrls;
|
|
539
|
+
try {
|
|
540
|
+
(0, urlSafety_1.assertSafeSyncUrl)(url, {
|
|
541
|
+
requireHttps: !allowInsecure,
|
|
542
|
+
allowPrivate: allowInsecure,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
catch (e) {
|
|
546
|
+
if (e instanceof urlSafety_1.UnsafeUrlError) {
|
|
547
|
+
return (0, types_4.jsonResponse)(400, { error: e.message });
|
|
548
|
+
}
|
|
549
|
+
throw e;
|
|
550
|
+
}
|
|
551
|
+
server.url = url;
|
|
552
|
+
}
|
|
553
|
+
for (const [field, value, guard] of [
|
|
554
|
+
["direction", direction, types_3.isPeerSyncDirection],
|
|
555
|
+
["role", role, types_3.isPeerRole],
|
|
556
|
+
["attachments", attachments, types_3.isPeerAttachmentMode],
|
|
557
|
+
]) {
|
|
558
|
+
if (value === undefined)
|
|
559
|
+
continue;
|
|
560
|
+
if (!guard(value)) {
|
|
561
|
+
return (0, types_4.jsonResponse)(400, { error: `Invalid ${field}: ${String(value)}` });
|
|
562
|
+
}
|
|
563
|
+
Object.assign(server, { [field]: value });
|
|
564
|
+
}
|
|
565
|
+
this.tenantManager.addTrustedServer(server);
|
|
566
|
+
this.options.cluster?.syncReplicators();
|
|
567
|
+
return (0, types_4.jsonResponse)(201, { success: true, message: `Trusted server "${name}" added` });
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
if (error instanceof validation_1.ValidationError) {
|
|
571
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
572
|
+
}
|
|
573
|
+
if (error instanceof Error && error.message.includes("already exists")) {
|
|
574
|
+
return (0, types_4.jsonResponse)(409, { error: error.message });
|
|
575
|
+
}
|
|
576
|
+
console.error("[MindooDBServer] Error adding trusted server:", error);
|
|
577
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to add trusted server" });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
removeTrustedServer(req) {
|
|
581
|
+
try {
|
|
582
|
+
const serverName = req.params.serverName;
|
|
583
|
+
(0, validation_1.validateStringLength)(serverName, 256, "serverName");
|
|
584
|
+
const removed = this.tenantManager.removeTrustedServer(serverName);
|
|
585
|
+
if (!removed) {
|
|
586
|
+
return (0, types_4.jsonResponse)(404, { error: "Trusted server not found" });
|
|
587
|
+
}
|
|
588
|
+
// Revoking trust must also stop replicating with that server.
|
|
589
|
+
this.options.cluster?.syncReplicators();
|
|
590
|
+
return (0, types_4.jsonResponse)(200, { success: true, message: `Trusted server "${serverName}" removed` });
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
if (error instanceof validation_1.ValidationError) {
|
|
594
|
+
return (0, types_4.jsonResponse)(400, { error: error.message });
|
|
595
|
+
}
|
|
596
|
+
console.error("[MindooDBServer] Error removing trusted server:", error);
|
|
597
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to remove trusted server" });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// ==================== System: Runtime Config ====================
|
|
601
|
+
listConfigBackups() {
|
|
602
|
+
try {
|
|
603
|
+
return (0, types_4.jsonResponse)(200, { backups: this.options.configRepository.listBackups() });
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
console.error("[MindooDBServer] Error listing config backups:", error);
|
|
607
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to list config backups" });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* `req.params.backupFile` as the host's router decoded it; it is decoded
|
|
612
|
+
* once more here, as the Express server always did.
|
|
613
|
+
*/
|
|
614
|
+
getConfigBackup(req) {
|
|
615
|
+
try {
|
|
616
|
+
const backupFile = decodeURIComponent(req.params.backupFile ?? "");
|
|
617
|
+
if (!(0, serverConfig_1.isConfigBackupFilename)(backupFile)) {
|
|
618
|
+
return (0, types_4.jsonResponse)(400, { error: "Invalid config backup filename" });
|
|
619
|
+
}
|
|
620
|
+
const config = this.options.configRepository.loadBackup(backupFile);
|
|
621
|
+
return (0, types_4.jsonResponse)(200, { file: backupFile, config });
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
if (error instanceof Error && error.message.startsWith("Config backup not found:")) {
|
|
625
|
+
return (0, types_4.jsonResponse)(404, { error: "Config backup not found" });
|
|
626
|
+
}
|
|
627
|
+
if (error instanceof Error && error.message.startsWith("Invalid config backup filename:")) {
|
|
628
|
+
return (0, types_4.jsonResponse)(400, { error: "Invalid config backup filename" });
|
|
629
|
+
}
|
|
630
|
+
console.error("[MindooDBServer] Error reading config backup:", error);
|
|
631
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to read config backup" });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
getConfig() {
|
|
635
|
+
return (0, types_4.jsonResponse)(200, this.options.getServerConfig());
|
|
636
|
+
}
|
|
637
|
+
async updateConfig(req) {
|
|
638
|
+
try {
|
|
639
|
+
let newConfig;
|
|
640
|
+
try {
|
|
641
|
+
newConfig = (0, serverConfig_1.validateServerConfig)(req.body, "request body");
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
return (0, types_4.jsonResponse)(400, {
|
|
645
|
+
error: err instanceof Error ? err.message : "Invalid config format",
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
// Self-lockout protection: simulate the proposed config against the calling
|
|
649
|
+
// admin's identity — reject if they would lose their own config access.
|
|
650
|
+
const callingAdmin = req.systemAdmin;
|
|
651
|
+
const proposedMatcher = new CapabilityMatcher_1.CapabilityMatcher(newConfig);
|
|
652
|
+
if (!proposedMatcher.isAuthorized("PUT", "/system/config", callingAdmin.username, callingAdmin.publicsignkey)) {
|
|
653
|
+
return (0, types_4.jsonResponse)(400, {
|
|
654
|
+
error: "This change would remove your own access to PUT /system/config",
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
// Back up the current config, then persist the new one.
|
|
658
|
+
const backupFile = this.options.configRepository.replace(newConfig);
|
|
659
|
+
// Hot-swap in-memory state
|
|
660
|
+
this.options.reloadConfig(newConfig);
|
|
661
|
+
console.log(`[MindooDBServer] Config updated by ${callingAdmin.username}` +
|
|
662
|
+
(backupFile ? ` (backup: ${backupFile})` : ""));
|
|
663
|
+
return (0, types_4.jsonResponse)(200, {
|
|
664
|
+
success: true,
|
|
665
|
+
...(backupFile ? { backupFile } : {}),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
console.error("[MindooDBServer] Error updating config:", error);
|
|
670
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to update config" });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// ==================== Tenant Guards ====================
|
|
674
|
+
/** Validate the `:tenantId` parameter and reject unknown tenants; sets `req.tenantId`. */
|
|
675
|
+
tenantGuard(req) {
|
|
676
|
+
const rawTenantId = req.params.tenantId?.toLowerCase();
|
|
677
|
+
if (!rawTenantId) {
|
|
678
|
+
return (0, types_4.jsonResponse)(400, { error: "Tenant ID required" });
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
(0, validation_1.validateTenantIdFormat)(rawTenantId);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
return (0, types_4.jsonResponse)(400, { error: "Invalid tenant ID format" });
|
|
685
|
+
}
|
|
686
|
+
if (!this.tenantManager.tenantExists(rawTenantId)) {
|
|
687
|
+
return (0, types_4.jsonResponse)(404, { error: this.formatTenantNotFoundOnServerError(rawTenantId) });
|
|
688
|
+
}
|
|
689
|
+
req.tenantId = rawTenantId;
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
formatTenantNotFoundOnServerError(tenantId) {
|
|
693
|
+
const serverName = this.tenantManager.getServerPublicInfo()?.name ?? "unknown";
|
|
694
|
+
return `Tenant ${tenantId} not found on server ${serverName}`;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Resolve the caller of a sync request so the sync limiter can bucket per
|
|
698
|
+
* identity instead of per address.
|
|
699
|
+
*
|
|
700
|
+
* Unlike {@link timestampAuthGuard} this never rejects: the route handlers
|
|
701
|
+
* do their own token validation and own the 401, and this pass only needs
|
|
702
|
+
* to know whose budget to spend. An unverifiable token simply falls back to
|
|
703
|
+
* address-only bucketing — a forged one must not be able to conjure a fresh
|
|
704
|
+
* budget, which is why the token is verified rather than decoded.
|
|
705
|
+
*/
|
|
706
|
+
async resolveSyncPrincipal(req) {
|
|
707
|
+
try {
|
|
708
|
+
const token = extractToken(req);
|
|
709
|
+
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
710
|
+
// Signature and expiry are enough to name the caller; the handler runs
|
|
711
|
+
// the full check (revocation, wipe) right after, so the directory is
|
|
712
|
+
// evaluated once per request rather than twice.
|
|
713
|
+
const payload = await authService.readIssuedToken(token);
|
|
714
|
+
if (payload && payload.tenantId === req.tenantId) {
|
|
715
|
+
req.syncPrincipal = payload.deviceSigningKey ?? payload.sub;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// No usable token; fall through to address-only bucketing.
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Authenticate a timestamp request with the tenant's existing bearer token
|
|
724
|
+
* and record the principal for rate-limit bucketing.
|
|
725
|
+
*
|
|
726
|
+
* The proxy makes outbound requests on the caller's behalf, so it must never
|
|
727
|
+
* be open: unauthenticated it would be a free relay to third-party services.
|
|
728
|
+
*/
|
|
729
|
+
async timestampAuthGuard(req) {
|
|
730
|
+
try {
|
|
731
|
+
const token = extractToken(req);
|
|
732
|
+
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
733
|
+
const payload = await authService.validateToken(token);
|
|
734
|
+
if (!payload || payload.tenantId !== req.tenantId) {
|
|
735
|
+
return (0, types_4.jsonResponse)(401, { error: "Invalid or expired token" });
|
|
736
|
+
}
|
|
737
|
+
req.timestampPrincipal = payload.deviceSigningKey ?? payload.sub;
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
return errorResponse(error);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Forward a DER TimeStampReq to a configured TSA and return the DER
|
|
746
|
+
* TimeStampResp verbatim.
|
|
747
|
+
*
|
|
748
|
+
* The server neither parses nor validates the ASN.1: the client checks that
|
|
749
|
+
* the returned token's `messageImprint` matches the digest it sent and that
|
|
750
|
+
* the token chains to a trusted root. That is what reduces a hostile proxy —
|
|
751
|
+
* ours or anyone's — from a forgery risk to a denial-of-service risk.
|
|
752
|
+
*/
|
|
753
|
+
async timestampRequest(req) {
|
|
754
|
+
const providers = this.options.tsaProviders;
|
|
755
|
+
if (providers.length === 0) {
|
|
756
|
+
return (0, types_4.jsonResponse)(503, { error: "Timestamping is not enabled on this server" });
|
|
757
|
+
}
|
|
758
|
+
const body = req.rawBody;
|
|
759
|
+
if (!body) {
|
|
760
|
+
return (0, types_4.jsonResponse)(415, {
|
|
761
|
+
error: `Timestamp requests must be sent as ${timestampProxy_1.TIMESTAMP_QUERY_CONTENT_TYPE}`,
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
// The provider is named by id and resolved against the env allowlist; a
|
|
765
|
+
// client-supplied URL would make this an SSRF gadget.
|
|
766
|
+
const requestedProvider = req.query.provider;
|
|
767
|
+
let preferredProviderId;
|
|
768
|
+
if (typeof requestedProvider === "string" && requestedProvider.length > 0) {
|
|
769
|
+
preferredProviderId = requestedProvider.toLowerCase();
|
|
770
|
+
}
|
|
771
|
+
else if (requestedProvider !== undefined) {
|
|
772
|
+
return (0, types_4.jsonResponse)(400, { error: "provider must be a single string value" });
|
|
773
|
+
}
|
|
774
|
+
try {
|
|
775
|
+
const result = await (0, timestampProxy_1.requestTimestampToken)({
|
|
776
|
+
providers,
|
|
777
|
+
request: body,
|
|
778
|
+
preferredProviderId,
|
|
779
|
+
});
|
|
780
|
+
return {
|
|
781
|
+
status: 200,
|
|
782
|
+
bytes: result.response,
|
|
783
|
+
headers: {
|
|
784
|
+
"content-type": timestampProxy_1.TIMESTAMP_REPLY_CONTENT_TYPE,
|
|
785
|
+
"x-mindoodb-tsa-provider": result.providerId,
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
catch (error) {
|
|
790
|
+
if (error instanceof timestampProxy_1.TimestampProxyError) {
|
|
791
|
+
if (error.attempts.length > 0) {
|
|
792
|
+
console.warn(`[MindooDBServer] Timestamp request failed for tenant ${req.tenantId}: ${error.attempts
|
|
793
|
+
.map((attempt) => `${attempt.providerId}=${attempt.error ?? "ok"}`)
|
|
794
|
+
.join("; ")}`);
|
|
795
|
+
}
|
|
796
|
+
return (0, types_4.jsonResponse)(error.status, { error: error.message });
|
|
797
|
+
}
|
|
798
|
+
console.error("[MindooDBServer] Unexpected timestamp proxy error:", error);
|
|
799
|
+
return (0, types_4.jsonResponse)(500, { error: "Failed to obtain a timestamp" });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// ==================== Tenant Auth ====================
|
|
803
|
+
// Two-step challenge-response flow:
|
|
804
|
+
// 1. POST /auth/challenge → client sends username, server returns a random nonce
|
|
805
|
+
// 2. POST /auth/authenticate → client signs the nonce with its Ed25519 private key,
|
|
806
|
+
// server verifies the signature and returns a JWT for subsequent sync calls
|
|
807
|
+
async challenge(req) {
|
|
808
|
+
try {
|
|
809
|
+
const { username, signingPublicKey } = req.body;
|
|
810
|
+
// The username is optional: a client may identify itself by its device
|
|
811
|
+
// signing public key instead, so the server never needs the cleartext
|
|
812
|
+
// name. Validate whichever was supplied.
|
|
813
|
+
if (username !== undefined && username !== null) {
|
|
814
|
+
(0, validation_1.validateUsername)(username);
|
|
815
|
+
}
|
|
816
|
+
if (signingPublicKey !== undefined && signingPublicKey !== null) {
|
|
817
|
+
(0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_SIGNATURE_LENGTH, "signingPublicKey");
|
|
818
|
+
}
|
|
819
|
+
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
820
|
+
const challenge = await authService.generateChallenge(typeof username === "string" ? username : undefined, typeof signingPublicKey === "string" ? { signingPublicKey } : undefined);
|
|
821
|
+
return (0, types_4.jsonResponse)(200, { challenge });
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
return errorResponse(error);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async authenticate(req) {
|
|
828
|
+
try {
|
|
829
|
+
const { challenge, signature } = req.body;
|
|
830
|
+
if (!challenge || !signature) {
|
|
831
|
+
return (0, types_4.jsonResponse)(400, { error: "challenge and signature are required" });
|
|
832
|
+
}
|
|
833
|
+
(0, validation_1.validateStringLength)(challenge, validation_1.MAX_CHALLENGE_LENGTH, "challenge");
|
|
834
|
+
(0, validation_1.validateStringLength)(signature, validation_1.MAX_SIGNATURE_LENGTH, "signature");
|
|
835
|
+
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
836
|
+
const result = await authService.authenticate(challenge, (0, base64_1.base64ToBytes)(signature));
|
|
837
|
+
// Return 401 on a failed authentication instead of 200 (audit, Low): a
|
|
838
|
+
// failed credential check must not look like a success at the HTTP layer.
|
|
839
|
+
return (0, types_4.jsonResponse)(result.success ? 200 : 401, result);
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
return errorResponse(error);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// ==================== Sync ====================
|
|
846
|
+
/** Dispatch one of {@link SYNC_ROUTES} for a store kind. */
|
|
847
|
+
sync(name, req, storeKind) {
|
|
848
|
+
switch (name) {
|
|
849
|
+
case "findNewEntries": return this.findNewEntries(req, storeKind);
|
|
850
|
+
case "findNewEntriesForDoc": return this.findNewEntriesForDoc(req, storeKind);
|
|
851
|
+
case "findEntries": return this.findEntries(req, storeKind);
|
|
852
|
+
case "scanEntriesSince": return this.scanEntriesSince(req, storeKind);
|
|
853
|
+
case "getStoreHead": return this.getStoreHead(req, storeKind);
|
|
854
|
+
case "getIdBloomSummary": return this.getIdBloomSummary(req, storeKind);
|
|
855
|
+
case "getCompactionStatus": return this.getCompactionStatus(req, storeKind);
|
|
856
|
+
case "capabilities": return this.getCapabilities(req, storeKind);
|
|
857
|
+
case "events": return this.syncEvents(req, storeKind);
|
|
858
|
+
case "getEntries": return this.getEntries(req, storeKind);
|
|
859
|
+
case "getEntriesBinary": return this.getEntriesBinary(req, storeKind);
|
|
860
|
+
case "putEntriesBinary": return this.putEntriesBinary(req, storeKind);
|
|
861
|
+
case "getEntryMetadata": return this.getEntryMetadata(req, storeKind);
|
|
862
|
+
case "putEntries": return this.putEntries(req, storeKind);
|
|
863
|
+
case "hasEntries": return this.hasEntries(req, storeKind);
|
|
864
|
+
case "getAllIds": return this.getAllIds(req, storeKind);
|
|
865
|
+
case "resolveDependencies": return this.resolveDependencies(req, storeKind);
|
|
866
|
+
case "planDocumentMaterialization": return this.planDocumentMaterialization(req, storeKind);
|
|
867
|
+
case "planDocumentMaterializationBatch": return this.planDocumentMaterializationBatch(req, storeKind);
|
|
868
|
+
case "planAttachmentReadByWalkingMetadata": return this.planAttachmentReadByWalkingMetadata(req, storeKind);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
serverStore(req, dbId, storeKind) {
|
|
872
|
+
return this.tenantManager.getServerStore(req.tenantId, dbId, storeKind);
|
|
873
|
+
}
|
|
874
|
+
async findNewEntries(req, storeKind) {
|
|
875
|
+
try {
|
|
876
|
+
const token = extractToken(req);
|
|
877
|
+
const { dbId, haveIds } = req.body;
|
|
878
|
+
const validDbId = validateDbId(dbId);
|
|
879
|
+
(0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
|
|
880
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
881
|
+
const entries = await serverStore.handleFindNewEntries(token, haveIds || []);
|
|
882
|
+
return (0, types_4.jsonResponse)(200, {
|
|
883
|
+
entries: entries.map((e) => (0, entryWireCodec_1.serializeEntryMetadata)(e)),
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
return errorResponse(error);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
async findNewEntriesForDoc(req, storeKind) {
|
|
891
|
+
try {
|
|
892
|
+
const token = extractToken(req);
|
|
893
|
+
const { dbId, haveIds, docId } = req.body;
|
|
894
|
+
const validDbId = validateDbId(dbId);
|
|
895
|
+
if (!docId) {
|
|
896
|
+
return (0, types_4.jsonResponse)(400, { error: "docId is required" });
|
|
897
|
+
}
|
|
898
|
+
(0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
|
|
899
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
900
|
+
const entries = await serverStore.handleFindNewEntriesForDoc(token, haveIds || [], docId);
|
|
901
|
+
return (0, types_4.jsonResponse)(200, {
|
|
902
|
+
entries: entries.map((e) => (0, entryWireCodec_1.serializeEntryMetadata)(e)),
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
return errorResponse(error);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
async findEntries(req, storeKind) {
|
|
910
|
+
try {
|
|
911
|
+
const token = extractToken(req);
|
|
912
|
+
const { dbId, type, creationDateFrom, creationDateUntil } = req.body;
|
|
913
|
+
const validDbId = validateDbId(dbId);
|
|
914
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
915
|
+
const entries = await serverStore.handleFindEntries(token, type, creationDateFrom ?? null, creationDateUntil ?? null);
|
|
916
|
+
return (0, types_4.jsonResponse)(200, {
|
|
917
|
+
entries: entries.map((e) => (0, entryWireCodec_1.serializeEntryMetadata)(e)),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
return errorResponse(error);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async scanEntriesSince(req, storeKind) {
|
|
925
|
+
try {
|
|
926
|
+
const token = extractToken(req);
|
|
927
|
+
const { dbId, cursor, limit, filters } = req.body;
|
|
928
|
+
const validDbId = validateDbId(dbId);
|
|
929
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
930
|
+
const result = await serverStore.handleScanEntriesSince(token, cursor ?? null, limit, filters);
|
|
931
|
+
return (0, types_4.jsonResponse)(200, {
|
|
932
|
+
entries: result.entries.map((e) => (0, entryWireCodec_1.serializeEntryMetadata)(e)),
|
|
933
|
+
nextCursor: result.nextCursor,
|
|
934
|
+
hasMore: result.hasMore,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
catch (error) {
|
|
938
|
+
return errorResponse(error);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
async getStoreHead(req, storeKind) {
|
|
942
|
+
try {
|
|
943
|
+
const token = extractToken(req);
|
|
944
|
+
const { dbId } = req.body;
|
|
945
|
+
const validDbId = validateDbId(dbId);
|
|
946
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
947
|
+
const head = await serverStore.handleGetStoreHead(token);
|
|
948
|
+
return (0, types_4.jsonResponse)(200, { head });
|
|
949
|
+
}
|
|
950
|
+
catch (error) {
|
|
951
|
+
return errorResponse(error);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async getIdBloomSummary(req, storeKind) {
|
|
955
|
+
try {
|
|
956
|
+
const token = extractToken(req);
|
|
957
|
+
const { dbId } = req.body;
|
|
958
|
+
const validDbId = validateDbId(dbId);
|
|
959
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
960
|
+
const summary = await serverStore.handleGetIdBloomSummary(token);
|
|
961
|
+
return (0, types_4.jsonResponse)(200, { summary: summary });
|
|
962
|
+
}
|
|
963
|
+
catch (error) {
|
|
964
|
+
return errorResponse(error);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
async getCapabilities(req, storeKind) {
|
|
968
|
+
try {
|
|
969
|
+
const token = extractToken(req);
|
|
970
|
+
const validDbId = validateDbId(req.query.dbId);
|
|
971
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
972
|
+
const capabilities = await serverStore.handleGetCapabilities(token);
|
|
973
|
+
// HTTP-layer features on top of the transport-agnostic capability set:
|
|
974
|
+
// binary octet-stream framing routes (phase 3) and the SSE change feed
|
|
975
|
+
// (phase 5) exist only over HTTP.
|
|
976
|
+
return (0, types_4.jsonResponse)(200, {
|
|
977
|
+
capabilities: {
|
|
978
|
+
...capabilities,
|
|
979
|
+
supportsBinaryEntries: true,
|
|
980
|
+
supportsPayloadOnlyEntries: true,
|
|
981
|
+
supportsChangeEvents: true,
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
return errorResponse(error);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* SSE live change feed (sync-v5, phase 5).
|
|
991
|
+
*
|
|
992
|
+
* `GET /:tenantId/sync/:storeKind/events` — Bearer-authenticated like every
|
|
993
|
+
* other sync route (fetch-streaming clients can set headers; native
|
|
994
|
+
* EventSource cannot, which is why the SDK uses a fetch reader).
|
|
995
|
+
*
|
|
996
|
+
* The subscribe-time read gate runs via `handleGetCapabilities` against the
|
|
997
|
+
* database given in `?dbId=` (default `directory`, which every tenant member
|
|
998
|
+
* can read): it validates the token and runs the database-open gate. Events
|
|
999
|
+
* carry only `{dbId, storeKind, epoch, maxReceiptOrder}` — metadata, never
|
|
1000
|
+
* content. The host keeps the stream open with a heartbeat every 30 s. Token
|
|
1001
|
+
* expiry does not tear down an open stream; the events are non-sensitive
|
|
1002
|
+
* change hints and a reconnect re-authenticates.
|
|
1003
|
+
*/
|
|
1004
|
+
async syncEvents(req, storeKind) {
|
|
1005
|
+
try {
|
|
1006
|
+
const token = extractToken(req);
|
|
1007
|
+
const tenantId = req.tenantId;
|
|
1008
|
+
const gateDbId = validateDbId(typeof req.query.dbId === "string" && req.query.dbId ? req.query.dbId : "directory");
|
|
1009
|
+
// Validates the token and runs the database-open/read gate before any
|
|
1010
|
+
// event is delivered.
|
|
1011
|
+
const serverStore = await this.serverStore(req, gateDbId, storeKind);
|
|
1012
|
+
await serverStore.handleGetCapabilities(token);
|
|
1013
|
+
return { status: 200, events: { tenantId, storeKind } };
|
|
1014
|
+
}
|
|
1015
|
+
catch (error) {
|
|
1016
|
+
return errorResponse(error);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Publish a change event after an accepted putEntries write (sync-v5,
|
|
1021
|
+
* phase 5). Fire-and-forget: head lookup failures must never affect the
|
|
1022
|
+
* write path.
|
|
1023
|
+
*/
|
|
1024
|
+
publishSyncChangeEvent(tenantId, dbId, storeKind, serverStore, token) {
|
|
1025
|
+
const events = this.options.syncEvents;
|
|
1026
|
+
if (events.listenerCount === 0) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
// Peer replicators subscribe to this bus, so a write that arrived from a
|
|
1030
|
+
// peer has to be labelled: without the label it would be pushed straight
|
|
1031
|
+
// back to its origin, and two bidirectional peers would ping-pong.
|
|
1032
|
+
const originPeer = this.options.cluster
|
|
1033
|
+
? this.options.cluster.resolvePeerName(token)
|
|
1034
|
+
: Promise.resolve(undefined);
|
|
1035
|
+
this.runInBackground(Promise.all([serverStore.handleGetStoreHead(token).catch(() => null), originPeer]).then(([head, peerName]) => {
|
|
1036
|
+
events.publish({
|
|
1037
|
+
tenantId,
|
|
1038
|
+
dbId,
|
|
1039
|
+
storeKind,
|
|
1040
|
+
epoch: head?.epoch,
|
|
1041
|
+
maxReceiptOrder: head?.maxReceiptOrder,
|
|
1042
|
+
originPeer: peerName,
|
|
1043
|
+
});
|
|
1044
|
+
}));
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Work that follows an accepted write: the change event, and for a
|
|
1048
|
+
* directory push the purge requests it may carry (docs/accesscontrol.md
|
|
1049
|
+
* §13), which physically remove purged history from this server's stores
|
|
1050
|
+
* and arm the re-push denylist. Both are fire-and-forget; purge failures
|
|
1051
|
+
* are logged and retried on the next directory push.
|
|
1052
|
+
*/
|
|
1053
|
+
afterAcceptedWrite(tenantId, dbId, storeKind, serverStore, token, receiptCount) {
|
|
1054
|
+
// Announce the accepted write to SSE subscribers (sync-v5, phase 5).
|
|
1055
|
+
if (receiptCount > 0) {
|
|
1056
|
+
this.publishSyncChangeEvent(tenantId, dbId, storeKind, serverStore, token);
|
|
1057
|
+
}
|
|
1058
|
+
if (dbId === "directory") {
|
|
1059
|
+
const onDirectoryWrite = this.options.onDirectoryWrite ?? ((id) => this.tenantManager.executePendingPurges(id));
|
|
1060
|
+
this.runInBackground(onDirectoryWrite(tenantId).catch((error) => {
|
|
1061
|
+
console.error(`[MindooDBServer] executePendingPurges failed for ${tenantId}:`, error);
|
|
1062
|
+
}));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async getCompactionStatus(req, storeKind) {
|
|
1066
|
+
try {
|
|
1067
|
+
const token = extractToken(req);
|
|
1068
|
+
const { dbId } = req.body;
|
|
1069
|
+
const validDbId = validateDbId(dbId);
|
|
1070
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1071
|
+
const status = await serverStore.handleGetCompactionStatus(token);
|
|
1072
|
+
return (0, types_4.jsonResponse)(200, { status: status });
|
|
1073
|
+
}
|
|
1074
|
+
catch (error) {
|
|
1075
|
+
return errorResponse(error);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
async getEntries(req, storeKind) {
|
|
1079
|
+
try {
|
|
1080
|
+
const token = extractToken(req);
|
|
1081
|
+
const { dbId, ids, sessionKeyWrap } = req.body;
|
|
1082
|
+
const validDbId = validateDbId(dbId);
|
|
1083
|
+
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
1084
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1085
|
+
// Session-key transport format (sync-v5, phase 2): one RSA-wrapped
|
|
1086
|
+
// AES-256-GCM key per response instead of per-entry RSA. Negotiated via
|
|
1087
|
+
// the `sessionKeyWrap` request flag; legacy clients get the old format.
|
|
1088
|
+
if (sessionKeyWrap === true) {
|
|
1089
|
+
const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
|
|
1090
|
+
return (0, types_4.jsonResponse)(200, {
|
|
1091
|
+
wrappedSessionKey: (0, base64_1.bytesToBase64)(batch.wrappedSessionKey),
|
|
1092
|
+
entries: batch.entries.map((e) => ({
|
|
1093
|
+
...(0, entryWireCodec_1.serializeEntryMetadata)(e),
|
|
1094
|
+
iv: (0, base64_1.bytesToBase64)(e.iv),
|
|
1095
|
+
sessionEncryptedPayload: (0, base64_1.bytesToBase64)(e.sessionEncryptedPayload),
|
|
1096
|
+
})),
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
const entries = await serverStore.handleGetEntries(token, ids || []);
|
|
1100
|
+
return (0, types_4.jsonResponse)(200, {
|
|
1101
|
+
entries: entries.map((e) => ({
|
|
1102
|
+
...(0, entryWireCodec_1.serializeEntryMetadata)(e),
|
|
1103
|
+
rsaEncryptedPayload: (0, base64_1.bytesToBase64)(e.rsaEncryptedPayload),
|
|
1104
|
+
})),
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
catch (error) {
|
|
1108
|
+
return errorResponse(error);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Binary variant of getEntries (sync-v5, phase 3): always session-key
|
|
1113
|
+
* encrypted, response framed as `application/octet-stream` (see
|
|
1114
|
+
* binaryEntryFraming.ts). The request stays JSON (small).
|
|
1115
|
+
*/
|
|
1116
|
+
async getEntriesBinary(req, storeKind) {
|
|
1117
|
+
try {
|
|
1118
|
+
const token = extractToken(req);
|
|
1119
|
+
const { dbId, ids, payloadOnly } = req.body;
|
|
1120
|
+
const validDbId = validateDbId(dbId);
|
|
1121
|
+
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
1122
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1123
|
+
const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
|
|
1124
|
+
// `payloadOnly` (capability supportsPayloadOnlyEntries): the client
|
|
1125
|
+
// already holds the metadata from its scan, so each frame carries only
|
|
1126
|
+
// what it cannot have yet. Same entries, same session-key encryption.
|
|
1127
|
+
const body = (0, binaryEntryFraming_1.encodeBinaryEntryMessage)({
|
|
1128
|
+
header: {
|
|
1129
|
+
format: binaryEntryFraming_1.BINARY_GET_ENTRIES_FORMAT,
|
|
1130
|
+
wrappedSessionKey: (0, base64_1.bytesToBase64)(batch.wrappedSessionKey),
|
|
1131
|
+
...(payloadOnly === true ? { payloadOnly: true } : {}),
|
|
1132
|
+
},
|
|
1133
|
+
entries: batch.entries.map((e) => ({
|
|
1134
|
+
meta: payloadOnly === true
|
|
1135
|
+
? { id: e.id, iv: (0, base64_1.bytesToBase64)(e.iv) }
|
|
1136
|
+
: { ...(0, entryWireCodec_1.serializeEntryMetadata)(e), iv: (0, base64_1.bytesToBase64)(e.iv) },
|
|
1137
|
+
payload: e.sessionEncryptedPayload,
|
|
1138
|
+
})),
|
|
1139
|
+
});
|
|
1140
|
+
const payloadBytes = batch.entries.reduce((sum, e) => sum + e.sessionEncryptedPayload.byteLength, 0);
|
|
1141
|
+
return {
|
|
1142
|
+
status: 200,
|
|
1143
|
+
bytes: body,
|
|
1144
|
+
headers: { "content-type": binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE },
|
|
1145
|
+
binaryEntryFrameBytes: body.byteLength - payloadBytes,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
catch (error) {
|
|
1149
|
+
return errorResponse(error);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
async getEntryMetadata(req, storeKind) {
|
|
1153
|
+
try {
|
|
1154
|
+
const token = extractToken(req);
|
|
1155
|
+
const { dbId, id } = req.body;
|
|
1156
|
+
const validDbId = validateDbId(dbId);
|
|
1157
|
+
if (!id) {
|
|
1158
|
+
return (0, types_4.jsonResponse)(400, { error: "id is required" });
|
|
1159
|
+
}
|
|
1160
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1161
|
+
const entry = await serverStore.handleGetEntryMetadata(token, id);
|
|
1162
|
+
return (0, types_4.jsonResponse)(200, {
|
|
1163
|
+
entry: entry ? (0, entryWireCodec_1.serializeEntryMetadata)(entry) : null,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
return errorResponse(error);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Ingest encrypted entries from a client. The `stage` variable tracks
|
|
1172
|
+
* progress through the pipeline so that error logs pinpoint the failing
|
|
1173
|
+
* step (token extraction → body parsing → deserialization → storage).
|
|
1174
|
+
*/
|
|
1175
|
+
async putEntries(req, storeKind) {
|
|
1176
|
+
let stage = "extract-token";
|
|
1177
|
+
try {
|
|
1178
|
+
const token = extractToken(req);
|
|
1179
|
+
stage = "read-body";
|
|
1180
|
+
const { dbId, entries } = req.body;
|
|
1181
|
+
const validDbId = validateDbId(dbId);
|
|
1182
|
+
(0, validation_1.validateArraySize)(entries, validation_1.MAX_PUT_ENTRIES, "entries");
|
|
1183
|
+
const serializedEntries = Array.isArray(entries) ? entries : [];
|
|
1184
|
+
const serializedEntryTypeCounts = serializedEntries.reduce((acc, entry) => {
|
|
1185
|
+
const key = entry?.entryType ?? "unknown";
|
|
1186
|
+
acc[key] = (acc[key] ?? 0) + 1;
|
|
1187
|
+
return acc;
|
|
1188
|
+
}, {});
|
|
1189
|
+
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)}`);
|
|
1190
|
+
stage = "load-server-store";
|
|
1191
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1192
|
+
stage = "deserialize-entries";
|
|
1193
|
+
const deserializedEntries = serializedEntries.map((entry, index) => {
|
|
1194
|
+
try {
|
|
1195
|
+
return (0, entryWireCodec_1.deserializeStoreEntry)(entry);
|
|
1196
|
+
}
|
|
1197
|
+
catch (error) {
|
|
1198
|
+
console.error("[MindooDBServer] putEntries failed while deserializing entry", {
|
|
1199
|
+
index,
|
|
1200
|
+
id: entry?.id ?? "-",
|
|
1201
|
+
type: entry?.entryType ?? "-",
|
|
1202
|
+
docId: entry?.docId ?? "-",
|
|
1203
|
+
encryptedSize: entry?.encryptedSize ?? "-",
|
|
1204
|
+
originalSize: entry?.originalSize ?? "-",
|
|
1205
|
+
signatureLength: entry?.signature?.length ?? "-",
|
|
1206
|
+
encryptedDataLength: entry?.encryptedData?.length ?? "-",
|
|
1207
|
+
error,
|
|
1208
|
+
});
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
const totalEncryptedBytes = deserializedEntries.reduce((sum, entry) => sum + entry.encryptedData.byteLength, 0);
|
|
1213
|
+
const totalOriginalBytes = deserializedEntries.reduce((sum, entry) => sum + entry.originalSize, 0);
|
|
1214
|
+
console.log(`[MindooDBServer] putEntries deserialized tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${deserializedEntries.length} totalEncryptedBytes=${totalEncryptedBytes} totalOriginalBytes=${totalOriginalBytes}`);
|
|
1215
|
+
stage = "store-entries";
|
|
1216
|
+
const ack = await serverStore.handlePutEntries(token, deserializedEntries);
|
|
1217
|
+
stage = "respond-success";
|
|
1218
|
+
this.afterAcceptedWrite(req.tenantId, validDbId, storeKind, serverStore, token, ack.receipts.length);
|
|
1219
|
+
// Return witness receipts for accepted entries so the pushing client (and
|
|
1220
|
+
// every replica it later syncs with) can persist the attestation
|
|
1221
|
+
// (docs/accesscontrol.md §5.3), plus per-entry rejections for
|
|
1222
|
+
// signature-class failures the server skipped. Older clients ignore the
|
|
1223
|
+
// extra fields.
|
|
1224
|
+
return (0, types_4.jsonResponse)(200, {
|
|
1225
|
+
success: true,
|
|
1226
|
+
receipts: ack.receipts.map((e) => (0, entryWireCodec_1.serializeEntryMetadata)(e)),
|
|
1227
|
+
rejected: ack.rejected,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
catch (error) {
|
|
1231
|
+
console.error("[MindooDBServer] putEntries failed", {
|
|
1232
|
+
stage,
|
|
1233
|
+
tenantId: req.tenantId ?? "-",
|
|
1234
|
+
dbId: req.body?.dbId ?? "-",
|
|
1235
|
+
parsedBytes: req.jsonBodyBytes ?? "-",
|
|
1236
|
+
contentLength: req.headers["content-length"] ?? "-",
|
|
1237
|
+
entryCount: Array.isArray(req.body?.entries) ? req.body.entries.length : "-",
|
|
1238
|
+
error,
|
|
1239
|
+
});
|
|
1240
|
+
return errorResponse(error);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
/**
|
|
1244
|
+
* Binary variant of putEntries (sync-v5, phase 3). The request body is
|
|
1245
|
+
* octet-stream framing; `dbId` travels in the message header JSON because
|
|
1246
|
+
* there is no JSON body. Receipts are returned as JSON like putEntries.
|
|
1247
|
+
*/
|
|
1248
|
+
async putEntriesBinary(req, storeKind) {
|
|
1249
|
+
let stage = "extract-token";
|
|
1250
|
+
try {
|
|
1251
|
+
const token = extractToken(req);
|
|
1252
|
+
stage = "decode-body";
|
|
1253
|
+
if (!req.rawBody) {
|
|
1254
|
+
return (0, types_4.jsonResponse)(400, { error: `putEntriesBinary requires a ${binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE} body` });
|
|
1255
|
+
}
|
|
1256
|
+
const message = (0, binaryEntryFraming_1.decodeBinaryEntryMessage)(req.rawBody);
|
|
1257
|
+
if (message.header.format !== binaryEntryFraming_1.BINARY_PUT_ENTRIES_FORMAT) {
|
|
1258
|
+
return (0, types_4.jsonResponse)(400, { error: `Unsupported binary entry format: ${String(message.header.format)}` });
|
|
1259
|
+
}
|
|
1260
|
+
const validDbId = validateDbId(message.header.dbId);
|
|
1261
|
+
(0, validation_1.validateArraySize)(message.entries, validation_1.MAX_PUT_ENTRIES, "entries");
|
|
1262
|
+
console.log(`[MindooDBServer] putEntriesBinary request tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${message.entries.length} content-length=${req.headers["content-length"] ?? "-"}`);
|
|
1263
|
+
stage = "load-server-store";
|
|
1264
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1265
|
+
stage = "deserialize-entries";
|
|
1266
|
+
const deserializedEntries = message.entries.map((frame, index) => {
|
|
1267
|
+
const meta = frame.meta;
|
|
1268
|
+
try {
|
|
1269
|
+
return {
|
|
1270
|
+
...(0, entryWireCodec_1.deserializeEntryMetadata)(meta),
|
|
1271
|
+
encryptedData: frame.payload,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
catch (error) {
|
|
1275
|
+
console.error("[MindooDBServer] putEntriesBinary failed while deserializing entry", {
|
|
1276
|
+
index,
|
|
1277
|
+
id: meta?.id ?? "-",
|
|
1278
|
+
type: meta?.entryType ?? "-",
|
|
1279
|
+
docId: meta?.docId ?? "-",
|
|
1280
|
+
payloadLength: frame.payload?.byteLength ?? "-",
|
|
1281
|
+
error,
|
|
1282
|
+
});
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
});
|
|
1286
|
+
stage = "store-entries";
|
|
1287
|
+
const ack = await serverStore.handlePutEntries(token, deserializedEntries);
|
|
1288
|
+
stage = "respond-success";
|
|
1289
|
+
// Same post-response hooks as the JSON putEntries path.
|
|
1290
|
+
this.afterAcceptedWrite(req.tenantId, validDbId, storeKind, serverStore, token, ack.receipts.length);
|
|
1291
|
+
return (0, types_4.jsonResponse)(200, {
|
|
1292
|
+
success: true,
|
|
1293
|
+
receipts: ack.receipts.map((e) => (0, entryWireCodec_1.serializeEntryMetadata)(e)),
|
|
1294
|
+
rejected: ack.rejected,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
catch (error) {
|
|
1298
|
+
console.error("[MindooDBServer] putEntriesBinary failed", {
|
|
1299
|
+
stage,
|
|
1300
|
+
tenantId: req.tenantId ?? "-",
|
|
1301
|
+
contentLength: req.headers["content-length"] ?? "-",
|
|
1302
|
+
error,
|
|
1303
|
+
});
|
|
1304
|
+
return errorResponse(error);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
async hasEntries(req, storeKind) {
|
|
1308
|
+
try {
|
|
1309
|
+
const token = extractToken(req);
|
|
1310
|
+
const { dbId, ids } = req.body;
|
|
1311
|
+
const validDbId = validateDbId(dbId);
|
|
1312
|
+
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
1313
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1314
|
+
const existingIds = await serverStore.handleHasEntries(token, ids || []);
|
|
1315
|
+
return (0, types_4.jsonResponse)(200, { ids: existingIds });
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
return errorResponse(error);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
async getAllIds(req, storeKind) {
|
|
1322
|
+
try {
|
|
1323
|
+
const token = extractToken(req);
|
|
1324
|
+
const validDbId = validateDbId(req.query.dbId);
|
|
1325
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1326
|
+
const ids = await serverStore.handleGetAllIds(token);
|
|
1327
|
+
return (0, types_4.jsonResponse)(200, { ids });
|
|
1328
|
+
}
|
|
1329
|
+
catch (error) {
|
|
1330
|
+
return errorResponse(error);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async resolveDependencies(req, storeKind) {
|
|
1334
|
+
try {
|
|
1335
|
+
const token = extractToken(req);
|
|
1336
|
+
const { dbId, startId, options } = req.body;
|
|
1337
|
+
const validDbId = validateDbId(dbId);
|
|
1338
|
+
if (!startId) {
|
|
1339
|
+
return (0, types_4.jsonResponse)(400, { error: "startId is required" });
|
|
1340
|
+
}
|
|
1341
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1342
|
+
const ids = await serverStore.handleResolveDependencies(token, startId, options);
|
|
1343
|
+
return (0, types_4.jsonResponse)(200, { ids });
|
|
1344
|
+
}
|
|
1345
|
+
catch (error) {
|
|
1346
|
+
return errorResponse(error);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async planDocumentMaterialization(req, storeKind) {
|
|
1350
|
+
try {
|
|
1351
|
+
const token = extractToken(req);
|
|
1352
|
+
const { dbId, docId, options } = req.body;
|
|
1353
|
+
const validDbId = validateDbId(dbId);
|
|
1354
|
+
if (!docId) {
|
|
1355
|
+
return (0, types_4.jsonResponse)(400, { error: "docId is required" });
|
|
1356
|
+
}
|
|
1357
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1358
|
+
const plan = await serverStore.handlePlanDocumentMaterialization(token, docId, options);
|
|
1359
|
+
return (0, types_4.jsonResponse)(200, { plan: plan });
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
return errorResponse(error);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
async planDocumentMaterializationBatch(req, storeKind) {
|
|
1366
|
+
try {
|
|
1367
|
+
const token = extractToken(req);
|
|
1368
|
+
const { dbId, docIds, options } = req.body;
|
|
1369
|
+
const validDbId = validateDbId(dbId);
|
|
1370
|
+
(0, validation_1.validateArraySize)(docIds, validation_1.MAX_HAVE_IDS, "docIds");
|
|
1371
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1372
|
+
const batchPlan = await serverStore.handlePlanDocumentMaterializationBatch(token, docIds || [], options);
|
|
1373
|
+
return (0, types_4.jsonResponse)(200, { batchPlan: batchPlan });
|
|
1374
|
+
}
|
|
1375
|
+
catch (error) {
|
|
1376
|
+
return errorResponse(error);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async planAttachmentReadByWalkingMetadata(req, storeKind) {
|
|
1380
|
+
try {
|
|
1381
|
+
const token = extractToken(req);
|
|
1382
|
+
const { dbId, lastChunkId, attachmentSize, options } = req.body;
|
|
1383
|
+
const validDbId = validateDbId(dbId);
|
|
1384
|
+
if (!lastChunkId) {
|
|
1385
|
+
return (0, types_4.jsonResponse)(400, { error: "lastChunkId is required" });
|
|
1386
|
+
}
|
|
1387
|
+
if (typeof attachmentSize !== "number" || !Number.isFinite(attachmentSize)) {
|
|
1388
|
+
return (0, types_4.jsonResponse)(400, { error: "attachmentSize is required" });
|
|
1389
|
+
}
|
|
1390
|
+
if (!options || typeof options.startByte !== "number") {
|
|
1391
|
+
return (0, types_4.jsonResponse)(400, { error: "options.startByte is required" });
|
|
1392
|
+
}
|
|
1393
|
+
const serverStore = await this.serverStore(req, validDbId, storeKind);
|
|
1394
|
+
const plan = await serverStore.handlePlanAttachmentReadByWalkingMetadata(token, lastChunkId, attachmentSize, options);
|
|
1395
|
+
return (0, types_4.jsonResponse)(200, { plan: plan });
|
|
1396
|
+
}
|
|
1397
|
+
catch (error) {
|
|
1398
|
+
return errorResponse(error);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
exports.MindooDBServerCore = MindooDBServerCore;
|
|
1403
|
+
//# sourceMappingURL=MindooDBServerCore.js.map
|