mindoodb 0.0.34 → 0.0.35

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 (91) hide show
  1. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +37 -3
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +158 -4
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  5. package/dist/appendonlystores/network/HttpTransport.d.ts +54 -3
  6. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  7. package/dist/appendonlystores/network/HttpTransport.js +344 -17
  8. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  9. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +39 -2
  10. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
  11. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +93 -11
  12. package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
  13. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +9 -1
  14. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
  15. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +27 -0
  16. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
  17. package/dist/browser/index.mjs +7 -0
  18. package/dist/core/BaseMindooDB.d.ts +82 -9
  19. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  20. package/dist/core/BaseMindooDB.js +556 -150
  21. package/dist/core/BaseMindooDB.js.map +1 -1
  22. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  23. package/dist/core/BaseMindooTenant.js +2 -1
  24. package/dist/core/BaseMindooTenant.js.map +1 -1
  25. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  26. package/dist/core/BaseMindooTenantDirectory.js +4 -3
  27. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  28. package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
  29. package/dist/core/BaseMindooTenantFactory.js +2 -1
  30. package/dist/core/BaseMindooTenantFactory.js.map +1 -1
  31. package/dist/core/DocumentDagAnalysis.d.ts +39 -0
  32. package/dist/core/DocumentDagAnalysis.d.ts.map +1 -1
  33. package/dist/core/DocumentDagAnalysis.js +147 -3
  34. package/dist/core/DocumentDagAnalysis.js.map +1 -1
  35. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +7 -1
  36. package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
  37. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +15 -0
  38. package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
  39. package/dist/core/appendonlystores/network/NetworkTransport.d.ts +55 -6
  40. package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
  41. package/dist/core/appendonlystores/network/binaryEntryFraming.d.ts +58 -0
  42. package/dist/core/appendonlystores/network/binaryEntryFraming.d.ts.map +1 -0
  43. package/dist/core/appendonlystores/network/binaryEntryFraming.js +121 -0
  44. package/dist/core/appendonlystores/network/binaryEntryFraming.js.map +1 -0
  45. package/dist/core/appendonlystores/network/types.d.ts +68 -0
  46. package/dist/core/appendonlystores/network/types.d.ts.map +1 -1
  47. package/dist/core/appendonlystores/network/types.js.map +1 -1
  48. package/dist/core/appendonlystores/types.d.ts +58 -1
  49. package/dist/core/appendonlystores/types.d.ts.map +1 -1
  50. package/dist/core/appendonlystores/types.js.map +1 -1
  51. package/dist/core/crypto/RSAEncryption.d.ts +11 -0
  52. package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
  53. package/dist/core/crypto/RSAEncryption.js +25 -0
  54. package/dist/core/crypto/RSAEncryption.js.map +1 -1
  55. package/dist/core/index.d.ts +2 -2
  56. package/dist/core/index.d.ts.map +1 -1
  57. package/dist/core/index.js +8 -2
  58. package/dist/core/index.js.map +1 -1
  59. package/dist/core/types.d.ts +100 -8
  60. package/dist/core/types.d.ts.map +1 -1
  61. package/dist/core/types.js +9 -1
  62. package/dist/core/types.js.map +1 -1
  63. package/dist/core/utils/idGeneration.d.ts +52 -0
  64. package/dist/core/utils/idGeneration.d.ts.map +1 -1
  65. package/dist/core/utils/idGeneration.js +78 -1
  66. package/dist/core/utils/idGeneration.js.map +1 -1
  67. package/dist/core/utils/index.d.ts +2 -1
  68. package/dist/core/utils/index.d.ts.map +1 -1
  69. package/dist/core/utils/index.js +8 -1
  70. package/dist/core/utils/index.js.map +1 -1
  71. package/dist/core/utils/timeSource.d.ts +34 -0
  72. package/dist/core/utils/timeSource.d.ts.map +1 -0
  73. package/dist/core/utils/timeSource.js +49 -0
  74. package/dist/core/utils/timeSource.js.map +1 -0
  75. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +17 -1
  76. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
  77. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +43 -0
  78. package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
  79. package/dist/node/server/MindooDBServer.d.ts +38 -0
  80. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  81. package/dist/node/server/MindooDBServer.js +262 -6
  82. package/dist/node/server/MindooDBServer.js.map +1 -1
  83. package/dist/node/server/SyncEventBus.d.ts +29 -0
  84. package/dist/node/server/SyncEventBus.d.ts.map +1 -0
  85. package/dist/node/server/SyncEventBus.js +41 -0
  86. package/dist/node/server/SyncEventBus.js.map +1 -0
  87. package/dist/node/server/compressionMiddleware.d.ts +25 -0
  88. package/dist/node/server/compressionMiddleware.d.ts.map +1 -0
  89. package/dist/node/server/compressionMiddleware.js +140 -0
  90. package/dist/node/server/compressionMiddleware.js.map +1 -0
  91. package/package.json +1 -1
@@ -57,6 +57,9 @@ const fs_1 = require("fs");
57
57
  const helmet_1 = __importDefault(require("helmet"));
58
58
  const cors_1 = __importDefault(require("cors"));
59
59
  const express_rate_limit_1 = __importDefault(require("express-rate-limit"));
60
+ const compressionMiddleware_1 = require("./compressionMiddleware");
61
+ const binaryEntryFraming_1 = require("../../core/appendonlystores/network/binaryEntryFraming");
62
+ const SyncEventBus_1 = require("./SyncEventBus");
60
63
  const types_1 = require("../../core/appendonlystores/types");
61
64
  const types_2 = require("../../core/appendonlystores/network/types");
62
65
  const TenantManager_1 = require("./TenantManager");
@@ -133,6 +136,7 @@ class MindooDBServer {
133
136
  */
134
137
  constructor(dataDir, serverPassword, staticDir, config, configPath) {
135
138
  this.app = (0, express_1.default)();
139
+ this.syncEventBus = new SyncEventBus_1.SyncEventBus();
136
140
  this.tenantManager = new TenantManager_1.TenantManager(dataDir, serverPassword);
137
141
  this.staticDir = staticDir;
138
142
  this.configPath = configPath ?? path_1.default.join(dataDir, "config.json");
@@ -229,6 +233,9 @@ class MindooDBServer {
229
233
  legacyHeaders: false,
230
234
  message: { error: "Too many requests, please try again later" },
231
235
  }));
236
+ // Response compression (sync-v5, phase 3): brotli/gzip for JSON bodies
237
+ // >= 1 KB. Metadata-heavy sync responses shrink 5-10x.
238
+ this.app.use(compressionMiddleware_1.jsonCompressionMiddleware);
232
239
  this.app.use(express_1.default.json({
233
240
  limit: this.jsonBodyLimit,
234
241
  verify: (req, _res, buf) => {
@@ -899,10 +906,19 @@ class MindooDBServer {
899
906
  router.post(`${syncBase}/findNewEntriesForDoc`, syncRateLimit, this.handleFindNewEntriesForDoc.bind(this));
900
907
  router.post(`${syncBase}/findEntries`, syncRateLimit, this.handleFindEntries.bind(this));
901
908
  router.post(`${syncBase}/scanEntriesSince`, syncRateLimit, this.handleScanEntriesSince.bind(this));
909
+ router.post(`${syncBase}/getStoreHead`, syncRateLimit, this.handleGetStoreHead.bind(this));
902
910
  router.post(`${syncBase}/getIdBloomSummary`, syncRateLimit, this.handleGetIdBloomSummary.bind(this));
903
911
  router.post(`${syncBase}/getCompactionStatus`, syncRateLimit, this.handleGetCompactionStatus.bind(this));
904
912
  router.get(`${syncBase}/capabilities`, syncRateLimit, this.handleGetCapabilities.bind(this));
913
+ // SSE live change feed (sync-v5, phase 5): one long-lived GET per
914
+ // subscriber; putEntries publishes into the in-process event bus.
915
+ router.get(`${syncBase}/events`, syncRateLimit, this.handleSyncEvents.bind(this));
905
916
  router.post(`${syncBase}/getEntries`, syncRateLimit, this.handleGetEntries.bind(this));
917
+ // Binary wire format v2 (sync-v5, phase 3): octet-stream framing for the
918
+ // two payload-heavy endpoints. putEntriesBinary needs its own raw body
919
+ // parser (the global JSON parser only handles application/json).
920
+ router.post(`${syncBase}/getEntriesBinary`, syncRateLimit, this.handleGetEntriesBinary.bind(this));
921
+ router.post(`${syncBase}/putEntriesBinary`, syncRateLimit, express_1.default.raw({ type: binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE, limit: this.jsonBodyLimit }), this.handlePutEntriesBinary.bind(this));
906
922
  router.post(`${syncBase}/getEntryMetadata`, syncRateLimit, this.handleGetEntryMetadata.bind(this));
907
923
  router.post(`${syncBase}/putEntries`, syncRateLimit, this.handlePutEntries.bind(this));
908
924
  router.post(`${syncBase}/hasEntries`, syncRateLimit, this.handleHasEntries.bind(this));
@@ -1122,6 +1138,19 @@ class MindooDBServer {
1122
1138
  this.handleRequestError(error, res);
1123
1139
  }
1124
1140
  }
1141
+ async handleGetStoreHead(req, res) {
1142
+ try {
1143
+ const token = this.extractToken(req);
1144
+ const { dbId } = req.body;
1145
+ const validDbId = this.validateDbId(dbId);
1146
+ const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1147
+ const head = await serverStore.handleGetStoreHead(token);
1148
+ res.json({ head });
1149
+ }
1150
+ catch (error) {
1151
+ this.handleRequestError(error, res);
1152
+ }
1153
+ }
1125
1154
  async handleGetIdBloomSummary(req, res) {
1126
1155
  try {
1127
1156
  const token = this.extractToken(req);
@@ -1141,12 +1170,104 @@ class MindooDBServer {
1141
1170
  const validDbId = this.validateDbId(req.query.dbId);
1142
1171
  const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1143
1172
  const capabilities = await serverStore.handleGetCapabilities(token);
1144
- res.json({ capabilities: capabilities });
1173
+ // HTTP-layer features on top of the transport-agnostic capability set:
1174
+ // binary octet-stream framing routes (phase 3) and the SSE change feed
1175
+ // (phase 5) exist only in this Express host.
1176
+ res.json({
1177
+ capabilities: {
1178
+ ...capabilities,
1179
+ supportsBinaryEntries: true,
1180
+ supportsChangeEvents: true,
1181
+ },
1182
+ });
1145
1183
  }
1146
1184
  catch (error) {
1147
1185
  this.handleRequestError(error, res);
1148
1186
  }
1149
1187
  }
1188
+ /**
1189
+ * SSE live change feed (sync-v5, phase 5).
1190
+ *
1191
+ * `GET /:tenantId/sync/:storeKind/events` — Bearer-authenticated like every
1192
+ * other sync route (fetch-streaming clients can set headers; native
1193
+ * EventSource cannot, which is why the SDK uses a fetch reader).
1194
+ *
1195
+ * The subscribe-time read gate runs via `handleGetCapabilities` against the
1196
+ * database given in `?dbId=` (default `directory`, which every tenant member
1197
+ * can read): it validates the token and runs the database-open gate. Events
1198
+ * carry only `{dbId, storeKind, epoch, maxReceiptOrder}` — metadata, never
1199
+ * content. A heartbeat comment every 30 s keeps proxies from closing the
1200
+ * stream. Token expiry does not tear down an open stream; the events are
1201
+ * non-sensitive change hints and a reconnect re-authenticates.
1202
+ */
1203
+ async handleSyncEvents(req, res) {
1204
+ try {
1205
+ const token = this.extractToken(req);
1206
+ const tenantId = req.tenantId;
1207
+ const storeKind = this.getStoreKindFromRequest(req);
1208
+ const gateDbId = this.validateDbId(typeof req.query.dbId === "string" && req.query.dbId ? req.query.dbId : "directory");
1209
+ // Validates the token and runs the database-open/read gate before any
1210
+ // event is delivered.
1211
+ const serverStore = await this.tenantManager.getServerStore(tenantId, gateDbId, storeKind);
1212
+ await serverStore.handleGetCapabilities(token);
1213
+ res.status(200);
1214
+ res.setHeader("Content-Type", "text/event-stream");
1215
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1216
+ res.setHeader("X-Accel-Buffering", "no");
1217
+ if (!req.httpVersion.startsWith("2")) {
1218
+ // Forbidden (and unnecessary) on HTTP/2 streams.
1219
+ res.setHeader("Connection", "keep-alive");
1220
+ }
1221
+ res.flushHeaders();
1222
+ res.write(`event: hello\ndata: ${JSON.stringify({ protocolVersion: "sync-v5" })}\n\n`);
1223
+ const unsubscribe = this.syncEventBus.subscribe((event) => {
1224
+ if (event.tenantId !== tenantId || event.storeKind !== storeKind) {
1225
+ return;
1226
+ }
1227
+ res.write(`event: change\ndata: ${JSON.stringify({
1228
+ dbId: event.dbId,
1229
+ storeKind: event.storeKind,
1230
+ epoch: event.epoch,
1231
+ maxReceiptOrder: event.maxReceiptOrder,
1232
+ })}\n\n`);
1233
+ });
1234
+ const heartbeat = setInterval(() => {
1235
+ res.write(`: heartbeat ${Date.now()}\n\n`);
1236
+ }, 30000);
1237
+ const cleanup = () => {
1238
+ clearInterval(heartbeat);
1239
+ unsubscribe();
1240
+ };
1241
+ req.on("close", cleanup);
1242
+ res.on("close", cleanup);
1243
+ console.log(`[MindooDBServer] SSE subscriber connected tenant=${tenantId} storeKind=${storeKind} subscribers=${this.syncEventBus.listenerCount}`);
1244
+ }
1245
+ catch (error) {
1246
+ this.handleRequestError(error, res);
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Publish a change event after an accepted putEntries write (sync-v5,
1251
+ * phase 5). Fire-and-forget: head lookup failures must never affect the
1252
+ * write path.
1253
+ */
1254
+ publishSyncChangeEvent(tenantId, dbId, storeKind, serverStore, token) {
1255
+ if (this.syncEventBus.listenerCount === 0) {
1256
+ return;
1257
+ }
1258
+ void serverStore
1259
+ .handleGetStoreHead(token)
1260
+ .catch(() => null)
1261
+ .then((head) => {
1262
+ this.syncEventBus.publish({
1263
+ tenantId,
1264
+ dbId,
1265
+ storeKind,
1266
+ epoch: head?.epoch,
1267
+ maxReceiptOrder: head?.maxReceiptOrder,
1268
+ });
1269
+ });
1270
+ }
1150
1271
  async handleGetCompactionStatus(req, res) {
1151
1272
  try {
1152
1273
  const token = this.extractToken(req);
@@ -1163,10 +1284,25 @@ class MindooDBServer {
1163
1284
  async handleGetEntries(req, res) {
1164
1285
  try {
1165
1286
  const token = this.extractToken(req);
1166
- const { dbId, ids } = req.body;
1287
+ const { dbId, ids, sessionKeyWrap } = req.body;
1167
1288
  const validDbId = this.validateDbId(dbId);
1168
1289
  (0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
1169
1290
  const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1291
+ // Session-key transport format (sync-v5, phase 2): one RSA-wrapped
1292
+ // AES-256-GCM key per response instead of per-entry RSA. Negotiated via
1293
+ // the `sessionKeyWrap` request flag; legacy clients get the old format.
1294
+ if (sessionKeyWrap === true) {
1295
+ const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
1296
+ res.json({
1297
+ wrappedSessionKey: this.uint8ArrayToBase64(batch.wrappedSessionKey),
1298
+ entries: batch.entries.map((e) => ({
1299
+ ...this.serializeEntryMetadata(e),
1300
+ iv: this.uint8ArrayToBase64(e.iv),
1301
+ sessionEncryptedPayload: this.uint8ArrayToBase64(e.sessionEncryptedPayload),
1302
+ })),
1303
+ });
1304
+ return;
1305
+ }
1170
1306
  const entries = await serverStore.handleGetEntries(token, ids || []);
1171
1307
  res.json({
1172
1308
  entries: entries.map((e) => ({
@@ -1179,6 +1315,39 @@ class MindooDBServer {
1179
1315
  this.handleRequestError(error, res);
1180
1316
  }
1181
1317
  }
1318
+ /**
1319
+ * Binary variant of getEntries (sync-v5, phase 3): always session-key
1320
+ * encrypted, response framed as `application/octet-stream` (see
1321
+ * binaryEntryFraming.ts). The request stays JSON (small).
1322
+ */
1323
+ async handleGetEntriesBinary(req, res) {
1324
+ try {
1325
+ const token = this.extractToken(req);
1326
+ const { dbId, ids } = req.body;
1327
+ const validDbId = this.validateDbId(dbId);
1328
+ (0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
1329
+ const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1330
+ const batch = await serverStore.handleGetEntriesSessionWrapped(token, ids || []);
1331
+ const body = (0, binaryEntryFraming_1.encodeBinaryEntryMessage)({
1332
+ header: {
1333
+ format: binaryEntryFraming_1.BINARY_GET_ENTRIES_FORMAT,
1334
+ wrappedSessionKey: this.uint8ArrayToBase64(batch.wrappedSessionKey),
1335
+ },
1336
+ entries: batch.entries.map((e) => ({
1337
+ meta: {
1338
+ ...this.serializeEntryMetadata(e),
1339
+ iv: this.uint8ArrayToBase64(e.iv),
1340
+ },
1341
+ payload: e.sessionEncryptedPayload,
1342
+ })),
1343
+ });
1344
+ res.setHeader("Content-Type", binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE);
1345
+ res.send(Buffer.from(body.buffer, body.byteOffset, body.byteLength));
1346
+ }
1347
+ catch (error) {
1348
+ this.handleRequestError(error, res);
1349
+ }
1350
+ }
1182
1351
  async handleGetEntryMetadata(req, res) {
1183
1352
  try {
1184
1353
  const token = this.extractToken(req);
@@ -1244,15 +1413,22 @@ class MindooDBServer {
1244
1413
  const totalOriginalBytes = deserializedEntries.reduce((sum, entry) => sum + entry.originalSize, 0);
1245
1414
  console.log(`[MindooDBServer] putEntries deserialized tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${deserializedEntries.length} totalEncryptedBytes=${totalEncryptedBytes} totalOriginalBytes=${totalOriginalBytes}`);
1246
1415
  stage = "store-entries";
1247
- const stampedMetadata = await serverStore.handlePutEntries(token, deserializedEntries);
1416
+ const ack = await serverStore.handlePutEntries(token, deserializedEntries);
1248
1417
  stage = "respond-success";
1249
1418
  // Return witness receipts for accepted entries so the pushing client (and
1250
1419
  // every replica it later syncs with) can persist the attestation
1251
- // (docs/accesscontrol.md §5.3). Older clients ignore the extra field.
1420
+ // (docs/accesscontrol.md §5.3), plus per-entry rejections for
1421
+ // signature-class failures the server skipped. Older clients ignore the
1422
+ // extra fields.
1252
1423
  res.json({
1253
1424
  success: true,
1254
- receipts: stampedMetadata.map((e) => this.serializeEntryMetadata(e)),
1425
+ receipts: ack.receipts.map((e) => this.serializeEntryMetadata(e)),
1426
+ rejected: ack.rejected,
1255
1427
  });
1428
+ // Announce the accepted write to SSE subscribers (sync-v5, phase 5).
1429
+ if (ack.receipts.length > 0) {
1430
+ this.publishSyncChangeEvent(req.tenantId, validDbId, this.getStoreKindFromRequest(req), serverStore, token);
1431
+ }
1256
1432
  // A directory push may carry new admin-signed purge requests
1257
1433
  // (docs/accesscontrol.md §13). Execute any pending purges after the
1258
1434
  // response so the server physically removes purged history from its own
@@ -1278,6 +1454,81 @@ class MindooDBServer {
1278
1454
  this.handleRequestError(error, res);
1279
1455
  }
1280
1456
  }
1457
+ /**
1458
+ * Binary variant of putEntries (sync-v5, phase 3). The request body is
1459
+ * octet-stream framing; `dbId` travels in the message header JSON because
1460
+ * there is no JSON body. Receipts are returned as JSON like putEntries.
1461
+ */
1462
+ async handlePutEntriesBinary(req, res) {
1463
+ let stage = "extract-token";
1464
+ try {
1465
+ const token = this.extractToken(req);
1466
+ stage = "decode-body";
1467
+ if (!Buffer.isBuffer(req.body)) {
1468
+ res.status(400).json({ error: `putEntriesBinary requires a ${binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE} body` });
1469
+ return;
1470
+ }
1471
+ const message = (0, binaryEntryFraming_1.decodeBinaryEntryMessage)(new Uint8Array(req.body.buffer, req.body.byteOffset, req.body.byteLength));
1472
+ if (message.header.format !== binaryEntryFraming_1.BINARY_PUT_ENTRIES_FORMAT) {
1473
+ res.status(400).json({ error: `Unsupported binary entry format: ${String(message.header.format)}` });
1474
+ return;
1475
+ }
1476
+ const validDbId = this.validateDbId(message.header.dbId);
1477
+ (0, validation_1.validateArraySize)(message.entries, validation_1.MAX_PUT_ENTRIES, "entries");
1478
+ console.log(`[MindooDBServer] putEntriesBinary request tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${message.entries.length} content-length=${req.headers["content-length"] ?? "-"}`);
1479
+ stage = "load-server-store";
1480
+ const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
1481
+ stage = "deserialize-entries";
1482
+ const deserializedEntries = message.entries.map((frame, index) => {
1483
+ const meta = frame.meta;
1484
+ try {
1485
+ return {
1486
+ ...this.deserializeEntryMetadata(meta),
1487
+ encryptedData: frame.payload,
1488
+ };
1489
+ }
1490
+ catch (error) {
1491
+ console.error("[MindooDBServer] putEntriesBinary failed while deserializing entry", {
1492
+ index,
1493
+ id: meta?.id ?? "-",
1494
+ type: meta?.entryType ?? "-",
1495
+ docId: meta?.docId ?? "-",
1496
+ payloadLength: frame.payload?.byteLength ?? "-",
1497
+ error,
1498
+ });
1499
+ throw error;
1500
+ }
1501
+ });
1502
+ stage = "store-entries";
1503
+ const ack = await serverStore.handlePutEntries(token, deserializedEntries);
1504
+ stage = "respond-success";
1505
+ res.json({
1506
+ success: true,
1507
+ receipts: ack.receipts.map((e) => this.serializeEntryMetadata(e)),
1508
+ rejected: ack.rejected,
1509
+ });
1510
+ // Announce the accepted write to SSE subscribers (sync-v5, phase 5).
1511
+ if (ack.receipts.length > 0) {
1512
+ this.publishSyncChangeEvent(req.tenantId, validDbId, this.getStoreKindFromRequest(req), serverStore, token);
1513
+ }
1514
+ // Same post-response purge hook as the JSON putEntries path.
1515
+ if (validDbId === "directory") {
1516
+ const tenantId = req.tenantId;
1517
+ void this.tenantManager.executePendingPurges(tenantId).catch((error) => {
1518
+ console.error(`[MindooDBServer] executePendingPurges failed for ${tenantId}:`, error);
1519
+ });
1520
+ }
1521
+ }
1522
+ catch (error) {
1523
+ console.error("[MindooDBServer] putEntriesBinary failed", {
1524
+ stage,
1525
+ tenantId: req.tenantId ?? "-",
1526
+ contentLength: req.headers["content-length"] ?? "-",
1527
+ error,
1528
+ });
1529
+ this.handleRequestError(error, res);
1530
+ }
1531
+ }
1281
1532
  async handleHasEntries(req, res) {
1282
1533
  try {
1283
1534
  const token = this.extractToken(req);
@@ -1506,7 +1757,7 @@ class MindooDBServer {
1506
1757
  entryVersion: metadata.entryVersion,
1507
1758
  };
1508
1759
  }
1509
- deserializeEntry(serialized) {
1760
+ deserializeEntryMetadata(serialized) {
1510
1761
  return {
1511
1762
  entryType: serialized.entryType,
1512
1763
  id: serialized.id,
@@ -1532,6 +1783,11 @@ class MindooDBServer {
1532
1783
  : undefined,
1533
1784
  attachmentRefs: serialized.attachmentRefs,
1534
1785
  entryVersion: serialized.entryVersion,
1786
+ };
1787
+ }
1788
+ deserializeEntry(serialized) {
1789
+ return {
1790
+ ...this.deserializeEntryMetadata(serialized),
1535
1791
  encryptedData: this.base64ToUint8Array(serialized.encryptedData),
1536
1792
  };
1537
1793
  }