mindoodb 0.0.53 → 0.0.55
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 +125 -69
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +17 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +22 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +58 -0
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +156 -3
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +2 -0
- package/dist/core/BaseMindooDB.d.ts +73 -74
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +263 -509
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/MindooDBServerAdmin.d.ts +44 -22
- package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
- package/dist/core/MindooDBServerAdmin.js +45 -21
- package/dist/core/MindooDBServerAdmin.js.map +1 -1
- package/dist/core/appendonlystores/syncStores.d.ts +147 -0
- package/dist/core/appendonlystores/syncStores.d.ts.map +1 -0
- package/dist/core/appendonlystores/syncStores.js +612 -0
- package/dist/core/appendonlystores/syncStores.js.map +1 -0
- package/dist/core/builtinDbInvariants.d.ts +30 -0
- package/dist/core/builtinDbInvariants.d.ts.map +1 -1
- package/dist/core/builtinDbInvariants.js +97 -0
- package/dist/core/builtinDbInvariants.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +42 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/node/server/CapabilityMatcher.d.ts +2 -2
- package/dist/node/server/CapabilityMatcher.js +2 -2
- package/dist/node/server/MindooDBServer.d.ts +14 -1
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +166 -159
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/SyncEventBus.d.ts +10 -0
- package/dist/node/server/SyncEventBus.d.ts.map +1 -1
- package/dist/node/server/SyncEventBus.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +58 -6
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +90 -65
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/add-to-network.d.ts.map +1 -1
- package/dist/node/server/add-to-network.js +33 -15
- package/dist/node/server/add-to-network.js.map +1 -1
- package/dist/node/server/canonicalSetupName.d.ts +22 -0
- package/dist/node/server/canonicalSetupName.d.ts.map +1 -0
- package/dist/node/server/canonicalSetupName.js +38 -0
- package/dist/node/server/canonicalSetupName.js.map +1 -0
- package/dist/node/server/config.d.ts.map +1 -1
- package/dist/node/server/config.js +33 -1
- package/dist/node/server/config.js.map +1 -1
- package/dist/node/server/peer/ClusterAuditLog.d.ts +64 -0
- package/dist/node/server/peer/ClusterAuditLog.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterAuditLog.js +99 -0
- package/dist/node/server/peer/ClusterAuditLog.js.map +1 -0
- package/dist/node/server/peer/ClusterJobRunner.d.ts +43 -0
- package/dist/node/server/peer/ClusterJobRunner.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterJobRunner.js +119 -0
- package/dist/node/server/peer/ClusterJobRunner.js.map +1 -0
- package/dist/node/server/peer/ClusterManager.d.ts +116 -0
- package/dist/node/server/peer/ClusterManager.d.ts.map +1 -0
- package/dist/node/server/peer/ClusterManager.js +274 -0
- package/dist/node/server/peer/ClusterManager.js.map +1 -0
- package/dist/node/server/peer/PeerAuthService.d.ts +69 -0
- package/dist/node/server/peer/PeerAuthService.d.ts.map +1 -0
- package/dist/node/server/peer/PeerAuthService.js +208 -0
- package/dist/node/server/peer/PeerAuthService.js.map +1 -0
- package/dist/node/server/peer/PeerReplicator.d.ts +232 -0
- package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -0
- package/dist/node/server/peer/PeerReplicator.js +879 -0
- package/dist/node/server/peer/PeerReplicator.js.map +1 -0
- package/dist/node/server/peer/routes.d.ts +66 -0
- package/dist/node/server/peer/routes.d.ts.map +1 -0
- package/dist/node/server/peer/routes.js +322 -0
- package/dist/node/server/peer/routes.js.map +1 -0
- package/dist/node/server/peer/types.d.ts +244 -0
- package/dist/node/server/peer/types.d.ts.map +1 -0
- package/dist/node/server/peer/types.js +189 -0
- package/dist/node/server/peer/types.js.map +1 -0
- package/dist/node/server/server.d.ts +1 -1
- package/dist/node/server/server.js +18 -37
- package/dist/node/server/server.js.map +1 -1
- package/dist/node/server/serverinit.d.ts +2 -1
- package/dist/node/server/serverinit.d.ts.map +1 -1
- package/dist/node/server/serverinit.js +34 -12
- package/dist/node/server/serverinit.js.map +1 -1
- package/dist/node/server/types.d.ts +42 -30
- package/dist/node/server/types.d.ts.map +1 -1
- package/dist/node/server/types.js +1 -7
- package/dist/node/server/types.js.map +1 -1
- package/dist/node/server/validation.d.ts +0 -6
- package/dist/node/server/validation.d.ts.map +1 -1
- package/dist/node/server/validation.js +1 -7
- package/dist/node/server/validation.js.map +1 -1
- package/package.json +1 -1
|
@@ -61,8 +61,11 @@ const compressionMiddleware_1 = require("./compressionMiddleware");
|
|
|
61
61
|
const binaryEntryFraming_1 = require("../../core/appendonlystores/network/binaryEntryFraming");
|
|
62
62
|
const entryWireCodec_1 = require("../../core/appendonlystores/network/entryWireCodec");
|
|
63
63
|
const SyncEventBus_1 = require("./SyncEventBus");
|
|
64
|
-
const
|
|
65
|
-
const
|
|
64
|
+
const ClusterManager_1 = require("./peer/ClusterManager");
|
|
65
|
+
const routes_1 = require("./peer/routes");
|
|
66
|
+
const types_1 = require("./peer/types");
|
|
67
|
+
const types_2 = require("../../core/appendonlystores/types");
|
|
68
|
+
const types_3 = require("../../core/appendonlystores/network/types");
|
|
66
69
|
const TenantManager_1 = require("./TenantManager");
|
|
67
70
|
const CapabilityMatcher_1 = require("./CapabilityMatcher");
|
|
68
71
|
const SystemAdminAuth_1 = require("./SystemAdminAuth");
|
|
@@ -71,7 +74,7 @@ const config_1 = require("./config");
|
|
|
71
74
|
const SystemIpAllowlist_1 = require("./SystemIpAllowlist");
|
|
72
75
|
const validation_1 = require("./validation");
|
|
73
76
|
const urlSafety_1 = require("../../core/utils/urlSafety");
|
|
74
|
-
const
|
|
77
|
+
const types_4 = require("./types");
|
|
75
78
|
const timestampProxy_1 = require("./timestampProxy");
|
|
76
79
|
const rateLimits_1 = require("./rateLimits");
|
|
77
80
|
/**
|
|
@@ -123,24 +126,6 @@ const DEFAULT_TIMESTAMP_RATE_LIMIT_MAX = 10;
|
|
|
123
126
|
const DEFAULT_TIMESTAMP_DAILY_LIMIT_MAX = 500;
|
|
124
127
|
const TIMESTAMP_DAILY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
125
128
|
const DEFAULT_SERVER_SOCKET_TIMEOUT_MS = 120000;
|
|
126
|
-
/**
|
|
127
|
-
* The effective max number of database ids per sync-server registration. Reads
|
|
128
|
-
* the `MINDOODB_MAX_SYNC_SERVER_DATABASES` env override and falls back to
|
|
129
|
-
* {@link DEFAULT_MAX_SYNC_SERVER_DATABASES} when unset or invalid (non-integer
|
|
130
|
-
* or non-positive), so a typo can never silently disable the bound.
|
|
131
|
-
*/
|
|
132
|
-
function resolveMaxSyncServerDatabases() {
|
|
133
|
-
const raw = process.env[types_3.ENV_VARS.MAX_SYNC_SERVER_DATABASES]?.trim();
|
|
134
|
-
if (!raw)
|
|
135
|
-
return validation_1.DEFAULT_MAX_SYNC_SERVER_DATABASES;
|
|
136
|
-
const parsed = Number(raw);
|
|
137
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
138
|
-
console.warn(`[MindooDBServer] Ignoring invalid ${types_3.ENV_VARS.MAX_SYNC_SERVER_DATABASES}="${raw}" ` +
|
|
139
|
-
`(expected a positive integer); using default ${validation_1.DEFAULT_MAX_SYNC_SERVER_DATABASES}`);
|
|
140
|
-
return validation_1.DEFAULT_MAX_SYNC_SERVER_DATABASES;
|
|
141
|
-
}
|
|
142
|
-
return parsed;
|
|
143
|
-
}
|
|
144
129
|
/** Parse a human-readable size string ("5mb", "1024", "100kb") into bytes. */
|
|
145
130
|
function parseBodySizeLimitToBytes(limit) {
|
|
146
131
|
const normalized = limit.trim().toLowerCase();
|
|
@@ -183,8 +168,8 @@ class MindooDBServer {
|
|
|
183
168
|
this.configPath = configPath ?? path_1.default.join(dataDir, "config.json");
|
|
184
169
|
this.jsonBodyLimit = process.env.MINDOODB_JSON_BODY_LIMIT?.trim() || "5mb";
|
|
185
170
|
this.jsonBodyLimitBytes = parseBodySizeLimitToBytes(this.jsonBodyLimit);
|
|
186
|
-
this.tsaProviders = (0, timestampProxy_1.parseTsaProviders)(process.env[
|
|
187
|
-
allowInsecureUrls: /^(1|true)$/i.test(process.env[
|
|
171
|
+
this.tsaProviders = (0, timestampProxy_1.parseTsaProviders)(process.env[types_4.ENV_VARS.TSA_PROVIDERS], {
|
|
172
|
+
allowInsecureUrls: /^(1|true)$/i.test(process.env[types_4.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? ""),
|
|
188
173
|
});
|
|
189
174
|
if (this.tsaProviders.length > 0) {
|
|
190
175
|
console.log(`[MindooDBServer] RFC 3161 timestamp proxy enabled with providers: ${this.tsaProviders
|
|
@@ -200,6 +185,39 @@ class MindooDBServer {
|
|
|
200
185
|
isTenantAdminSigningKey: (publicsignkey) => this.tenantManager.isRegisteredTenantAdminSigningKey(publicsignkey),
|
|
201
186
|
});
|
|
202
187
|
this.deviceDiscovery = new DeviceDiscoveryService_1.DeviceDiscoveryService(this.tenantManager, cryptoAdapter);
|
|
188
|
+
this.cluster = new ClusterManager_1.ClusterManager({
|
|
189
|
+
cryptoAdapter,
|
|
190
|
+
dataDir,
|
|
191
|
+
eventBus: this.syncEventBus,
|
|
192
|
+
listTrustedServers: () => this.tenantManager.listTrustedServers(),
|
|
193
|
+
localServerName: () => this.tenantManager.getServerPublicInfo()?.name ?? null,
|
|
194
|
+
localSigningPublicKey: () => this.tenantManager.getServerPublicInfo()?.signingPublicKey ?? null,
|
|
195
|
+
getSigningKey: async () => (await this.tenantManager.getWitnessSigner())?.signingPrivateKey,
|
|
196
|
+
getEncryptionKey: () => this.tenantManager.getServerEncryptionPrivateKey(),
|
|
197
|
+
listTenants: () => this.tenantManager.listTenants(),
|
|
198
|
+
listDatabases: (tenantId) => this.tenantManager.listDatabases(tenantId),
|
|
199
|
+
getLocalStore: (tenantId, dbId, storeKind) => this.tenantManager.getStore(tenantId, dbId, storeKind),
|
|
200
|
+
localRole: this.serverConfig.cluster?.role,
|
|
201
|
+
});
|
|
202
|
+
// Peer tokens are accepted by the tenant sync routes so a peer can mirror
|
|
203
|
+
// the encrypted stores through the ordinary protocol; the store bypasses
|
|
204
|
+
// per-user access control for them (see ServerNetworkContentAddressedStore).
|
|
205
|
+
this.tenantManager.setPeerAccessControl({
|
|
206
|
+
peerTokenResolver: async (token) => {
|
|
207
|
+
const payload = await this.cluster.auth.validateToken(token);
|
|
208
|
+
if (!payload)
|
|
209
|
+
return null;
|
|
210
|
+
const trusted = this.cluster.auth.findTrustedServerByKey(payload.publicsignkey);
|
|
211
|
+
if (!trusted)
|
|
212
|
+
return null;
|
|
213
|
+
return {
|
|
214
|
+
name: trusted.name,
|
|
215
|
+
signingPublicKey: trusted.signingPublicKey,
|
|
216
|
+
encryptionPublicKey: trusted.encryptionPublicKey,
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
trustedWitnessResolver: (receivedByPublicKey) => this.cluster.auth.findTrustedServerByKey(receivedByPublicKey) !== null,
|
|
220
|
+
});
|
|
203
221
|
const principalCount = Object.values(this.serverConfig.capabilities).reduce((sum, arr) => sum + arr.length, 0);
|
|
204
222
|
if (principalCount === 0) {
|
|
205
223
|
console.log(`[MindooDBServer] WARNING: No system admin principals configured. All /system/* endpoints are locked down.`);
|
|
@@ -219,6 +237,23 @@ class MindooDBServer {
|
|
|
219
237
|
getSystemAdminAuth() {
|
|
220
238
|
return this.systemAdminAuth;
|
|
221
239
|
}
|
|
240
|
+
getClusterManager() {
|
|
241
|
+
return this.cluster;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Begin replicating with every trusted server that has a `url`.
|
|
245
|
+
*
|
|
246
|
+
* Separate from the constructor so a server can serve the peer and cluster
|
|
247
|
+
* endpoints — answering handshakes, status and admin actions — without
|
|
248
|
+
* dialling out itself. That is what `--auto-sync` toggles, and it is what
|
|
249
|
+
* makes a passive mirror or a test harness possible.
|
|
250
|
+
*/
|
|
251
|
+
startCluster() {
|
|
252
|
+
this.cluster.start();
|
|
253
|
+
}
|
|
254
|
+
async stopCluster() {
|
|
255
|
+
await this.cluster.stop();
|
|
256
|
+
}
|
|
222
257
|
getServerConfig() {
|
|
223
258
|
return this.serverConfig;
|
|
224
259
|
}
|
|
@@ -319,7 +354,7 @@ class MindooDBServer {
|
|
|
319
354
|
* list over `true`, which trusts the whole chain.
|
|
320
355
|
*/
|
|
321
356
|
applyTrustProxySetting() {
|
|
322
|
-
const raw = process.env[
|
|
357
|
+
const raw = process.env[types_4.ENV_VARS.TRUST_PROXY]?.trim();
|
|
323
358
|
if (!raw || /^(0|false)$/i.test(raw)) {
|
|
324
359
|
return;
|
|
325
360
|
}
|
|
@@ -395,6 +430,10 @@ class MindooDBServer {
|
|
|
395
430
|
}
|
|
396
431
|
res.json({
|
|
397
432
|
...info,
|
|
433
|
+
// Its own mesh role, so `add-to-network` can propagate it into the
|
|
434
|
+
// other side's trusted-servers entry instead of asking the operator to
|
|
435
|
+
// repeat it on every node.
|
|
436
|
+
clusterRole: this.serverConfig.cluster?.role ?? "peer",
|
|
398
437
|
maxJsonRequestBodyLimit: this.jsonBodyLimit,
|
|
399
438
|
maxJsonRequestBodyBytes: this.jsonBodyLimitBytes ?? undefined,
|
|
400
439
|
// Lets a client decide between calling a browser-reachable TSA directly
|
|
@@ -405,6 +444,33 @@ class MindooDBServer {
|
|
|
405
444
|
},
|
|
406
445
|
});
|
|
407
446
|
});
|
|
447
|
+
/**
|
|
448
|
+
* Mirrors of one tenant, for client failover.
|
|
449
|
+
*
|
|
450
|
+
* Scoped to the tenant in the query on purpose: a client asking about
|
|
451
|
+
* tenant A must not learn which other tenants this server replicates, or
|
|
452
|
+
* to whom. Unauthenticated like the other discovery endpoints — a client
|
|
453
|
+
* needs it precisely when it cannot reach a server to authenticate with.
|
|
454
|
+
*/
|
|
455
|
+
this.app.get("/.well-known/mindoodb-cluster", (req, res) => {
|
|
456
|
+
const tenantId = typeof req.query.tenantId === "string" ? req.query.tenantId.toLowerCase() : "";
|
|
457
|
+
if (!tenantId) {
|
|
458
|
+
res.status(400).json({ error: "tenantId query parameter is required" });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
(0, validation_1.validateTenantId)(tenantId);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
res.status(400).json({ error: "Invalid tenantId format" });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
469
|
+
res.status(404).json({ error: "Tenant not found on server" });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
res.json({ tenantId, mirrors: this.cluster.mirrorsForTenant(tenantId) });
|
|
473
|
+
});
|
|
408
474
|
this.app.get("/.well-known/mindoodb-tenants/:tenantId/publicinfos-fingerprints", async (req, res) => {
|
|
409
475
|
const tenantId = req.params.tenantId.toLowerCase();
|
|
410
476
|
try {
|
|
@@ -454,12 +520,29 @@ class MindooDBServer {
|
|
|
454
520
|
console.log(`[MindooDBServer] MINDOODB_ADMIN_ALLOWED_IPS is set — /system/* limited to: ${systemIpRaw}`);
|
|
455
521
|
}
|
|
456
522
|
const systemRateLimit = (0, express_rate_limit_1.default)({
|
|
457
|
-
windowMs: 60000,
|
|
458
|
-
max: 30,
|
|
523
|
+
windowMs: this.serverConfig.rateLimits?.system?.windowMs ?? 60000,
|
|
524
|
+
max: this.serverConfig.rateLimits?.system?.max ?? 30,
|
|
459
525
|
standardHeaders: true,
|
|
460
526
|
legacyHeaders: false,
|
|
461
527
|
message: { error: "Too many system requests, please try again later" },
|
|
462
528
|
});
|
|
529
|
+
// Peer routes are mounted before the /system chain on purpose. They are
|
|
530
|
+
// server-to-server traffic, not administration: the admin IP allowlist
|
|
531
|
+
// would lock out the operator's own cluster, and 30 req/min would throttle
|
|
532
|
+
// replication to a crawl. They are protected by the peer JWT, which is only
|
|
533
|
+
// issued to a key in trusted-servers.json.
|
|
534
|
+
this.app.use("/system/peer", (0, express_rate_limit_1.default)({
|
|
535
|
+
windowMs: 60000,
|
|
536
|
+
max: 600,
|
|
537
|
+
standardHeaders: true,
|
|
538
|
+
legacyHeaders: false,
|
|
539
|
+
message: { error: "Too many peer requests, please try again later" },
|
|
540
|
+
}), (0, routes_1.createPeerRouter)({
|
|
541
|
+
cluster: this.cluster,
|
|
542
|
+
eventBus: this.syncEventBus,
|
|
543
|
+
listTenants: () => this.tenantManager.listTenants(),
|
|
544
|
+
listDatabases: (tenantId) => this.tenantManager.listDatabases(tenantId),
|
|
545
|
+
}));
|
|
463
546
|
const systemRouter = (0, express_1.Router)();
|
|
464
547
|
this.setupSystemRoutes(systemRouter);
|
|
465
548
|
this.app.use("/system", SystemIpAllowlist_1.systemIpAllowlistMiddleware, systemRateLimit, systemRouter);
|
|
@@ -563,6 +646,15 @@ class MindooDBServer {
|
|
|
563
646
|
router.post("/auth/challenge", challengeRateLimit, this.handleSystemChallenge.bind(this));
|
|
564
647
|
router.post("/auth/authenticate", authenticateRateLimit, this.handleSystemAuthenticate.bind(this));
|
|
565
648
|
const authMiddleware = this.systemAdminMiddleware.bind(this);
|
|
649
|
+
// Cluster administration. Behind the same JWT + capability middleware as
|
|
650
|
+
// the rest of /system, so config.json can grant an auditor principal the
|
|
651
|
+
// GET routes without granting the POST/PATCH/DELETE ones.
|
|
652
|
+
router.use("/cluster", authMiddleware, (0, routes_1.createClusterRouter)({
|
|
653
|
+
cluster: this.cluster,
|
|
654
|
+
listTrustedServers: () => this.tenantManager.listTrustedServers(),
|
|
655
|
+
saveTrustedServer: (peer) => this.tenantManager.saveTrustedServer(peer),
|
|
656
|
+
removeTrustedServer: (name) => this.tenantManager.removeTrustedServer(name),
|
|
657
|
+
}));
|
|
566
658
|
// Tenant CRUD
|
|
567
659
|
router.get("/tenants", authMiddleware, (req, res) => {
|
|
568
660
|
try {
|
|
@@ -702,7 +794,7 @@ class MindooDBServer {
|
|
|
702
794
|
});
|
|
703
795
|
router.post("/trusted-servers", authMiddleware, (req, res) => {
|
|
704
796
|
try {
|
|
705
|
-
const { name, signingPublicKey, encryptionPublicKey } = req.body;
|
|
797
|
+
const { name, signingPublicKey, encryptionPublicKey, url, direction, role, attachments } = req.body;
|
|
706
798
|
if (!name || !signingPublicKey || !encryptionPublicKey) {
|
|
707
799
|
res.status(400).json({ error: "name, signingPublicKey, and encryptionPublicKey are required" });
|
|
708
800
|
return;
|
|
@@ -711,7 +803,41 @@ class MindooDBServer {
|
|
|
711
803
|
(0, validation_1.validateStringLength)(signingPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "signingPublicKey");
|
|
712
804
|
(0, validation_1.validateStringLength)(encryptionPublicKey, validation_1.MAX_PEM_KEY_LENGTH, "encryptionPublicKey");
|
|
713
805
|
const server = { name, signingPublicKey, encryptionPublicKey };
|
|
806
|
+
// Replication settings are optional: an entry without a url is a trust
|
|
807
|
+
// relationship only (inbound peers may authenticate, we never dial).
|
|
808
|
+
if (url !== undefined) {
|
|
809
|
+
(0, validation_1.validateStringLength)(url, 2048, "url");
|
|
810
|
+
const allowInsecure = /^(1|true)$/i.test(process.env[types_4.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? "");
|
|
811
|
+
try {
|
|
812
|
+
(0, urlSafety_1.assertSafeSyncUrl)(url, {
|
|
813
|
+
requireHttps: !allowInsecure,
|
|
814
|
+
allowPrivate: allowInsecure,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
catch (e) {
|
|
818
|
+
if (e instanceof urlSafety_1.UnsafeUrlError) {
|
|
819
|
+
res.status(400).json({ error: e.message });
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
throw e;
|
|
823
|
+
}
|
|
824
|
+
server.url = url;
|
|
825
|
+
}
|
|
826
|
+
for (const [field, value, guard] of [
|
|
827
|
+
["direction", direction, types_1.isPeerSyncDirection],
|
|
828
|
+
["role", role, types_1.isPeerRole],
|
|
829
|
+
["attachments", attachments, types_1.isPeerAttachmentMode],
|
|
830
|
+
]) {
|
|
831
|
+
if (value === undefined)
|
|
832
|
+
continue;
|
|
833
|
+
if (!guard(value)) {
|
|
834
|
+
res.status(400).json({ error: `Invalid ${field}: ${String(value)}` });
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
Object.assign(server, { [field]: value });
|
|
838
|
+
}
|
|
714
839
|
this.tenantManager.addTrustedServer(server);
|
|
840
|
+
this.cluster.syncReplicators();
|
|
715
841
|
res.status(201).json({ success: true, message: `Trusted server "${name}" added` });
|
|
716
842
|
}
|
|
717
843
|
catch (error) {
|
|
@@ -736,6 +862,8 @@ class MindooDBServer {
|
|
|
736
862
|
res.status(404).json({ error: "Trusted server not found" });
|
|
737
863
|
return;
|
|
738
864
|
}
|
|
865
|
+
// Revoking trust must also stop replicating with that server.
|
|
866
|
+
this.cluster.syncReplicators();
|
|
739
867
|
res.json({ success: true, message: `Trusted server "${serverName}" removed` });
|
|
740
868
|
}
|
|
741
869
|
catch (error) {
|
|
@@ -747,128 +875,6 @@ class MindooDBServer {
|
|
|
747
875
|
res.status(500).json({ error: "Failed to remove trusted server" });
|
|
748
876
|
}
|
|
749
877
|
});
|
|
750
|
-
// Per-tenant sync server management
|
|
751
|
-
router.get("/tenants/:tenantId/sync-servers", authMiddleware, (req, res) => {
|
|
752
|
-
try {
|
|
753
|
-
const tenantId = req.params.tenantId.toLowerCase();
|
|
754
|
-
try {
|
|
755
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
756
|
-
}
|
|
757
|
-
catch {
|
|
758
|
-
res.status(400).json({ error: "Invalid tenantId format" });
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
762
|
-
res.status(404).json({ error: "Tenant not found" });
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
const servers = this.tenantManager.getTenantSyncServers(tenantId);
|
|
766
|
-
res.json({ servers });
|
|
767
|
-
}
|
|
768
|
-
catch (error) {
|
|
769
|
-
console.error("[MindooDBServer] Error listing sync servers:", error);
|
|
770
|
-
res.status(500).json({ error: "Failed to list sync servers" });
|
|
771
|
-
}
|
|
772
|
-
});
|
|
773
|
-
router.post("/tenants/:tenantId/sync-servers", authMiddleware, (req, res) => {
|
|
774
|
-
try {
|
|
775
|
-
const tenantId = req.params.tenantId.toLowerCase();
|
|
776
|
-
try {
|
|
777
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
778
|
-
}
|
|
779
|
-
catch {
|
|
780
|
-
res.status(400).json({ error: "Invalid tenantId format" });
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
784
|
-
res.status(404).json({ error: "Tenant not found" });
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
const { name, url, syncIntervalMs, databases } = req.body;
|
|
788
|
-
if (!name || !url) {
|
|
789
|
-
res.status(400).json({ error: "name and url are required" });
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
if (!databases || !Array.isArray(databases) || databases.length === 0) {
|
|
793
|
-
res.status(400).json({ error: "databases array is required and must not be empty" });
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
// Bound and format-validate each database id (audit, Low): these are
|
|
797
|
-
// used to scope sync and must be safe identifiers, not arbitrary input.
|
|
798
|
-
// The bound is overridable per deployment via
|
|
799
|
-
// MINDOODB_MAX_SYNC_SERVER_DATABASES.
|
|
800
|
-
(0, validation_1.validateArraySize)(databases, resolveMaxSyncServerDatabases(), "databases");
|
|
801
|
-
for (const db of databases) {
|
|
802
|
-
(0, validation_1.validateIdentifier)(db, "databases[]");
|
|
803
|
-
}
|
|
804
|
-
(0, validation_1.validateStringLength)(name, 256, "name");
|
|
805
|
-
(0, validation_1.validateStringLength)(url, 2048, "url");
|
|
806
|
-
// SSRF guard: an admin-supplied sync URL is fetched server-side, so
|
|
807
|
-
// reject plaintext/internal targets unless explicitly allowed for dev.
|
|
808
|
-
const allowInsecure = /^(1|true)$/i.test(process.env[types_3.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? "");
|
|
809
|
-
try {
|
|
810
|
-
(0, urlSafety_1.assertSafeSyncUrl)(url, {
|
|
811
|
-
requireHttps: !allowInsecure,
|
|
812
|
-
allowPrivate: allowInsecure,
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
catch (e) {
|
|
816
|
-
if (e instanceof urlSafety_1.UnsafeUrlError) {
|
|
817
|
-
res.status(400).json({ error: e.message });
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
throw e;
|
|
821
|
-
}
|
|
822
|
-
const config = { name, url, databases };
|
|
823
|
-
if (syncIntervalMs !== undefined) {
|
|
824
|
-
config.syncIntervalMs = syncIntervalMs;
|
|
825
|
-
}
|
|
826
|
-
this.tenantManager.addTenantSyncServer(tenantId, config);
|
|
827
|
-
res.status(201).json({ success: true, message: `Sync server "${name}" configured for tenant ${tenantId}` });
|
|
828
|
-
}
|
|
829
|
-
catch (error) {
|
|
830
|
-
if (error instanceof validation_1.ValidationError) {
|
|
831
|
-
res.status(400).json({ error: error.message });
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
console.error("[MindooDBServer] Error adding sync server:", error);
|
|
835
|
-
res.status(500).json({ error: "Failed to add sync server" });
|
|
836
|
-
}
|
|
837
|
-
});
|
|
838
|
-
router.delete("/tenants/:tenantId/sync-servers/:serverName", authMiddleware, (req, res) => {
|
|
839
|
-
try {
|
|
840
|
-
const tenantId = req.params.tenantId.toLowerCase();
|
|
841
|
-
try {
|
|
842
|
-
(0, validation_1.validateTenantId)(tenantId);
|
|
843
|
-
}
|
|
844
|
-
catch {
|
|
845
|
-
res.status(400).json({ error: "Invalid tenantId format" });
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
if (!this.tenantManager.tenantExists(tenantId)) {
|
|
849
|
-
res.status(404).json({ error: "Tenant not found" });
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
const serverName = decodeURIComponent(req.params.serverName);
|
|
853
|
-
(0, validation_1.validateStringLength)(serverName, 256, "serverName");
|
|
854
|
-
const removed = this.tenantManager.removeTenantSyncServer(tenantId, serverName);
|
|
855
|
-
if (!removed) {
|
|
856
|
-
res.status(404).json({ error: "Sync server not found" });
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
res.json({ success: true, message: `Sync server "${serverName}" removed from tenant ${tenantId}` });
|
|
860
|
-
}
|
|
861
|
-
catch (error) {
|
|
862
|
-
if (error instanceof validation_1.ValidationError) {
|
|
863
|
-
res.status(400).json({ error: error.message });
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
console.error("[MindooDBServer] Error removing sync server:", error);
|
|
867
|
-
res.status(500).json({ error: "Failed to remove sync server" });
|
|
868
|
-
}
|
|
869
|
-
});
|
|
870
|
-
// Per-tenant trigger-sync (moved from /:tenantId/admin/trigger-sync)
|
|
871
|
-
router.post("/tenants/:tenantId/trigger-sync", authMiddleware, this.handleTriggerSync.bind(this));
|
|
872
878
|
// Runtime config management
|
|
873
879
|
router.get("/config/backups", authMiddleware, (req, res) => {
|
|
874
880
|
try {
|
|
@@ -1625,16 +1631,20 @@ class MindooDBServer {
|
|
|
1625
1631
|
if (this.syncEventBus.listenerCount === 0) {
|
|
1626
1632
|
return;
|
|
1627
1633
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1634
|
+
// Peer replicators subscribe to this bus, so a write that arrived from a
|
|
1635
|
+
// peer has to be labelled: without the label it would be pushed straight
|
|
1636
|
+
// back to its origin, and two bidirectional peers would ping-pong.
|
|
1637
|
+
const originPeer = this.cluster?.auth
|
|
1638
|
+
? this.cluster.auth.validateToken(token).then((payload) => payload?.sub)
|
|
1639
|
+
: Promise.resolve(undefined);
|
|
1640
|
+
void Promise.all([serverStore.handleGetStoreHead(token).catch(() => null), originPeer]).then(([head, peerName]) => {
|
|
1632
1641
|
this.syncEventBus.publish({
|
|
1633
1642
|
tenantId,
|
|
1634
1643
|
dbId,
|
|
1635
1644
|
storeKind,
|
|
1636
1645
|
epoch: head?.epoch,
|
|
1637
1646
|
maxReceiptOrder: head?.maxReceiptOrder,
|
|
1647
|
+
originPeer: peerName,
|
|
1638
1648
|
});
|
|
1639
1649
|
});
|
|
1640
1650
|
}
|
|
@@ -1999,9 +2009,6 @@ class MindooDBServer {
|
|
|
1999
2009
|
}
|
|
2000
2010
|
}
|
|
2001
2011
|
// ==================== Management Handlers ====================
|
|
2002
|
-
async handleTriggerSync(req, res) {
|
|
2003
|
-
res.json({ success: true, message: "Sync triggered (not yet implemented)" });
|
|
2004
|
-
}
|
|
2005
2012
|
// ==================== Helper Methods ====================
|
|
2006
2013
|
/**
|
|
2007
2014
|
* Derive the StoreKind from the request path. Both "docs" and
|
|
@@ -2011,17 +2018,17 @@ class MindooDBServer {
|
|
|
2011
2018
|
getStoreKindFromRequest(req) {
|
|
2012
2019
|
const trimmedPath = req.path.replace(/\/$/, "");
|
|
2013
2020
|
if (trimmedPath.startsWith("/sync/docs/") || trimmedPath === "/sync/docs/capabilities" || trimmedPath === "/sync/docs/getAllIds") {
|
|
2014
|
-
return
|
|
2021
|
+
return types_2.StoreKind.docs;
|
|
2015
2022
|
}
|
|
2016
2023
|
if (trimmedPath.startsWith("/sync/attachments/") || trimmedPath === "/sync/attachments/capabilities" || trimmedPath === "/sync/attachments/getAllIds") {
|
|
2017
|
-
return
|
|
2024
|
+
return types_2.StoreKind.attachments;
|
|
2018
2025
|
}
|
|
2019
2026
|
throw new Error(`Unsupported sync store kind for path: ${req.path}`);
|
|
2020
2027
|
}
|
|
2021
2028
|
extractToken(req) {
|
|
2022
2029
|
const auth = req.headers.authorization;
|
|
2023
2030
|
if (!auth || !auth.startsWith("Bearer ")) {
|
|
2024
|
-
throw new
|
|
2031
|
+
throw new types_3.NetworkError("INVALID_TOKEN", "Missing or invalid Authorization header");
|
|
2025
2032
|
}
|
|
2026
2033
|
return auth.substring(7);
|
|
2027
2034
|
}
|