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.
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +37 -3
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +158 -4
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +54 -3
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +344 -17
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts +39 -2
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js +93 -11
- package/dist/appendonlystores/network/ServerNetworkContentAddressedStore.js.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +9 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +27 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
- package/dist/browser/index.mjs +7 -0
- package/dist/core/BaseMindooDB.d.ts +82 -9
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +556 -150
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +2 -1
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +4 -3
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/BaseMindooTenantFactory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantFactory.js +2 -1
- package/dist/core/BaseMindooTenantFactory.js.map +1 -1
- package/dist/core/DocumentDagAnalysis.d.ts +39 -0
- package/dist/core/DocumentDagAnalysis.d.ts.map +1 -1
- package/dist/core/DocumentDagAnalysis.js +147 -3
- package/dist/core/DocumentDagAnalysis.js.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +7 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +15 -0
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +55 -6
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/network/binaryEntryFraming.d.ts +58 -0
- package/dist/core/appendonlystores/network/binaryEntryFraming.d.ts.map +1 -0
- package/dist/core/appendonlystores/network/binaryEntryFraming.js +121 -0
- package/dist/core/appendonlystores/network/binaryEntryFraming.js.map +1 -0
- package/dist/core/appendonlystores/network/types.d.ts +68 -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/types.d.ts +58 -1
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/types.js.map +1 -1
- package/dist/core/crypto/RSAEncryption.d.ts +11 -0
- package/dist/core/crypto/RSAEncryption.d.ts.map +1 -1
- package/dist/core/crypto/RSAEncryption.js +25 -0
- package/dist/core/crypto/RSAEncryption.js.map +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +8 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/types.d.ts +100 -8
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +9 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils/idGeneration.d.ts +52 -0
- package/dist/core/utils/idGeneration.d.ts.map +1 -1
- package/dist/core/utils/idGeneration.js +78 -1
- package/dist/core/utils/idGeneration.js.map +1 -1
- package/dist/core/utils/index.d.ts +2 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +8 -1
- package/dist/core/utils/index.js.map +1 -1
- package/dist/core/utils/timeSource.d.ts +34 -0
- package/dist/core/utils/timeSource.d.ts.map +1 -0
- package/dist/core/utils/timeSource.js +49 -0
- package/dist/core/utils/timeSource.js.map +1 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +17 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +43 -0
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts +38 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +262 -6
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/SyncEventBus.d.ts +29 -0
- package/dist/node/server/SyncEventBus.d.ts.map +1 -0
- package/dist/node/server/SyncEventBus.js +41 -0
- package/dist/node/server/SyncEventBus.js.map +1 -0
- package/dist/node/server/compressionMiddleware.d.ts +25 -0
- package/dist/node/server/compressionMiddleware.d.ts.map +1 -0
- package/dist/node/server/compressionMiddleware.js +140 -0
- package/dist/node/server/compressionMiddleware.js.map +1 -0
- 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
|
-
|
|
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
|
|
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)
|
|
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:
|
|
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
|
-
|
|
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
|
}
|