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.
Files changed (99) hide show
  1. package/README-server.md +125 -69
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +17 -0
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +22 -0
  5. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  6. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +58 -0
  7. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  8. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +156 -3
  9. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  10. package/dist/browser/index.mjs +2 -0
  11. package/dist/core/BaseMindooDB.d.ts +73 -74
  12. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  13. package/dist/core/BaseMindooDB.js +263 -509
  14. package/dist/core/BaseMindooDB.js.map +1 -1
  15. package/dist/core/MindooDBServerAdmin.d.ts +44 -22
  16. package/dist/core/MindooDBServerAdmin.d.ts.map +1 -1
  17. package/dist/core/MindooDBServerAdmin.js +45 -21
  18. package/dist/core/MindooDBServerAdmin.js.map +1 -1
  19. package/dist/core/appendonlystores/syncStores.d.ts +147 -0
  20. package/dist/core/appendonlystores/syncStores.d.ts.map +1 -0
  21. package/dist/core/appendonlystores/syncStores.js +612 -0
  22. package/dist/core/appendonlystores/syncStores.js.map +1 -0
  23. package/dist/core/builtinDbInvariants.d.ts +30 -0
  24. package/dist/core/builtinDbInvariants.d.ts.map +1 -1
  25. package/dist/core/builtinDbInvariants.js +97 -0
  26. package/dist/core/builtinDbInvariants.js.map +1 -1
  27. package/dist/core/index.d.ts +1 -1
  28. package/dist/core/index.d.ts.map +1 -1
  29. package/dist/core/index.js +7 -4
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/core/types.d.ts +42 -0
  32. package/dist/core/types.d.ts.map +1 -1
  33. package/dist/node/server/CapabilityMatcher.d.ts +2 -2
  34. package/dist/node/server/CapabilityMatcher.js +2 -2
  35. package/dist/node/server/MindooDBServer.d.ts +14 -1
  36. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  37. package/dist/node/server/MindooDBServer.js +166 -159
  38. package/dist/node/server/MindooDBServer.js.map +1 -1
  39. package/dist/node/server/SyncEventBus.d.ts +10 -0
  40. package/dist/node/server/SyncEventBus.d.ts.map +1 -1
  41. package/dist/node/server/SyncEventBus.js.map +1 -1
  42. package/dist/node/server/TenantManager.d.ts +58 -6
  43. package/dist/node/server/TenantManager.d.ts.map +1 -1
  44. package/dist/node/server/TenantManager.js +90 -65
  45. package/dist/node/server/TenantManager.js.map +1 -1
  46. package/dist/node/server/add-to-network.d.ts.map +1 -1
  47. package/dist/node/server/add-to-network.js +33 -15
  48. package/dist/node/server/add-to-network.js.map +1 -1
  49. package/dist/node/server/canonicalSetupName.d.ts +22 -0
  50. package/dist/node/server/canonicalSetupName.d.ts.map +1 -0
  51. package/dist/node/server/canonicalSetupName.js +38 -0
  52. package/dist/node/server/canonicalSetupName.js.map +1 -0
  53. package/dist/node/server/config.d.ts.map +1 -1
  54. package/dist/node/server/config.js +33 -1
  55. package/dist/node/server/config.js.map +1 -1
  56. package/dist/node/server/peer/ClusterAuditLog.d.ts +64 -0
  57. package/dist/node/server/peer/ClusterAuditLog.d.ts.map +1 -0
  58. package/dist/node/server/peer/ClusterAuditLog.js +99 -0
  59. package/dist/node/server/peer/ClusterAuditLog.js.map +1 -0
  60. package/dist/node/server/peer/ClusterJobRunner.d.ts +43 -0
  61. package/dist/node/server/peer/ClusterJobRunner.d.ts.map +1 -0
  62. package/dist/node/server/peer/ClusterJobRunner.js +119 -0
  63. package/dist/node/server/peer/ClusterJobRunner.js.map +1 -0
  64. package/dist/node/server/peer/ClusterManager.d.ts +116 -0
  65. package/dist/node/server/peer/ClusterManager.d.ts.map +1 -0
  66. package/dist/node/server/peer/ClusterManager.js +274 -0
  67. package/dist/node/server/peer/ClusterManager.js.map +1 -0
  68. package/dist/node/server/peer/PeerAuthService.d.ts +69 -0
  69. package/dist/node/server/peer/PeerAuthService.d.ts.map +1 -0
  70. package/dist/node/server/peer/PeerAuthService.js +208 -0
  71. package/dist/node/server/peer/PeerAuthService.js.map +1 -0
  72. package/dist/node/server/peer/PeerReplicator.d.ts +232 -0
  73. package/dist/node/server/peer/PeerReplicator.d.ts.map +1 -0
  74. package/dist/node/server/peer/PeerReplicator.js +879 -0
  75. package/dist/node/server/peer/PeerReplicator.js.map +1 -0
  76. package/dist/node/server/peer/routes.d.ts +66 -0
  77. package/dist/node/server/peer/routes.d.ts.map +1 -0
  78. package/dist/node/server/peer/routes.js +322 -0
  79. package/dist/node/server/peer/routes.js.map +1 -0
  80. package/dist/node/server/peer/types.d.ts +244 -0
  81. package/dist/node/server/peer/types.d.ts.map +1 -0
  82. package/dist/node/server/peer/types.js +189 -0
  83. package/dist/node/server/peer/types.js.map +1 -0
  84. package/dist/node/server/server.d.ts +1 -1
  85. package/dist/node/server/server.js +18 -37
  86. package/dist/node/server/server.js.map +1 -1
  87. package/dist/node/server/serverinit.d.ts +2 -1
  88. package/dist/node/server/serverinit.d.ts.map +1 -1
  89. package/dist/node/server/serverinit.js +34 -12
  90. package/dist/node/server/serverinit.js.map +1 -1
  91. package/dist/node/server/types.d.ts +42 -30
  92. package/dist/node/server/types.d.ts.map +1 -1
  93. package/dist/node/server/types.js +1 -7
  94. package/dist/node/server/types.js.map +1 -1
  95. package/dist/node/server/validation.d.ts +0 -6
  96. package/dist/node/server/validation.d.ts.map +1 -1
  97. package/dist/node/server/validation.js +1 -7
  98. package/dist/node/server/validation.js.map +1 -1
  99. 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 types_1 = require("../../core/appendonlystores/types");
65
- const types_2 = require("../../core/appendonlystores/network/types");
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 types_3 = require("./types");
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[types_3.ENV_VARS.TSA_PROVIDERS], {
187
- allowInsecureUrls: /^(1|true)$/i.test(process.env[types_3.ENV_VARS.ALLOW_INSECURE_SYNC_URLS] ?? ""),
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[types_3.ENV_VARS.TRUST_PROXY]?.trim();
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
- void serverStore
1629
- .handleGetStoreHead(token)
1630
- .catch(() => null)
1631
- .then((head) => {
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 types_1.StoreKind.docs;
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 types_1.StoreKind.attachments;
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 types_2.NetworkError("INVALID_TOKEN", "Missing or invalid Authorization header");
2031
+ throw new types_3.NetworkError("INVALID_TOKEN", "Missing or invalid Authorization header");
2025
2032
  }
2026
2033
  return auth.substring(7);
2027
2034
  }