mindoodb 0.0.9 → 0.0.11
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/InMemoryContentAddressedStoreFactory.d.ts.map +1 -1
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js +3 -5
- package/dist/appendonlystores/InMemoryContentAddressedStoreFactory.js.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +7 -2
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +23 -3
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +5 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +66 -36
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +4 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +15 -3
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.d.ts.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js +7 -3
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStoreFactory.js.map +1 -1
- package/dist/browser/index.mjs +1 -0
- package/dist/core/BaseMindooDB.d.ts +44 -5
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +244 -53
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +2 -3
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +9 -18
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.d.ts +1 -0
- package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
- package/dist/core/BaseMindooTenantDirectory.js +24 -8
- package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
- package/dist/core/appendonlystores/AttachmentReadPlanner.d.ts.map +1 -1
- package/dist/core/appendonlystores/AttachmentReadPlanner.js +1 -1
- package/dist/core/appendonlystores/AttachmentReadPlanner.js.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts +4 -2
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.d.ts.map +1 -1
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js +13 -8
- package/dist/core/appendonlystores/InMemoryContentAddressedStore.js.map +1 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +5 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/appendonlystores/types.d.ts +14 -10
- package/dist/core/appendonlystores/types.d.ts.map +1 -1
- package/dist/core/appendonlystores/types.js +6 -0
- package/dist/core/appendonlystores/types.js.map +1 -1
- package/dist/core/types.d.ts +24 -12
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +3 -1
- package/dist/core/types.js.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts +5 -2
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.d.ts.map +1 -1
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js +10 -4
- package/dist/node/appendonlystores/BasicOnDiskContentAddressedStore.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts +105 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +240 -66
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/ServerSync.d.ts +1 -1
- package/dist/node/server/ServerSync.d.ts.map +1 -1
- package/dist/node/server/ServerSync.js +2 -1
- package/dist/node/server/ServerSync.js.map +1 -1
- package/dist/node/server/StoreFactory.d.ts +3 -3
- package/dist/node/server/StoreFactory.d.ts.map +1 -1
- package/dist/node/server/StoreFactory.js +8 -8
- package/dist/node/server/StoreFactory.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +5 -3
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +33 -20
- package/dist/node/server/TenantManager.js.map +1 -1
- package/dist/node/server/config.d.ts.map +1 -1
- package/dist/node/server/config.js +4 -0
- package/dist/node/server/config.js.map +1 -1
- package/dist/node/server/server.js +6 -5
- package/dist/node/server/server.js.map +1 -1
- package/dist/node/server/types.d.ts +1 -0
- package/dist/node/server/types.d.ts.map +1 -1
- package/dist/node/server/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -51,21 +51,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
51
51
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
52
52
|
exports.MindooDBServer = void 0;
|
|
53
53
|
const express_1 = __importStar(require("express"));
|
|
54
|
-
const
|
|
54
|
+
const http2_1 = __importDefault(require("http2"));
|
|
55
55
|
const path_1 = __importDefault(require("path"));
|
|
56
56
|
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 types_1 = require("../../core/appendonlystores/
|
|
60
|
+
const types_1 = require("../../core/appendonlystores/types");
|
|
61
|
+
const types_2 = require("../../core/appendonlystores/network/types");
|
|
61
62
|
const TenantManager_1 = require("./TenantManager");
|
|
62
63
|
const CapabilityMatcher_1 = require("./CapabilityMatcher");
|
|
63
64
|
const SystemAdminAuth_1 = require("./SystemAdminAuth");
|
|
64
65
|
const config_1 = require("./config");
|
|
65
66
|
const SystemIpAllowlist_1 = require("./SystemIpAllowlist");
|
|
66
67
|
const validation_1 = require("./validation");
|
|
68
|
+
/**
|
|
69
|
+
* Rate-limit defaults. Auth and sync have separate tiers because a single
|
|
70
|
+
* challenge/authenticate handshake is two requests; with multiple tenants,
|
|
71
|
+
* store kinds, and token renewals the auth budget exhausts quickly if set
|
|
72
|
+
* too low. These can be overridden in config.json under `rateLimits.auth`
|
|
73
|
+
* and `rateLimits.sync`.
|
|
74
|
+
*/
|
|
75
|
+
const DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS = 60000;
|
|
76
|
+
const DEFAULT_AUTH_RATE_LIMIT_MAX = 120;
|
|
67
77
|
const DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS = 60000;
|
|
68
78
|
const DEFAULT_SYNC_RATE_LIMIT_MAX = 1000;
|
|
79
|
+
const DEFAULT_SERVER_SOCKET_TIMEOUT_MS = 120000;
|
|
80
|
+
/** Parse a human-readable size string ("5mb", "1024", "100kb") into bytes. */
|
|
69
81
|
function parseBodySizeLimitToBytes(limit) {
|
|
70
82
|
const normalized = limit.trim().toLowerCase();
|
|
71
83
|
if (!normalized) {
|
|
@@ -90,6 +102,15 @@ function parseBodySizeLimitToBytes(limit) {
|
|
|
90
102
|
return Math.round(value * multiplier);
|
|
91
103
|
}
|
|
92
104
|
class MindooDBServer {
|
|
105
|
+
/**
|
|
106
|
+
* @param dataDir Root directory for on-disk tenant data and server identity
|
|
107
|
+
* @param serverPassword Password used to decrypt the server identity private keys
|
|
108
|
+
* and per-tenant key bags at startup
|
|
109
|
+
* @param staticDir Optional directory served at /statics/ (e.g. the Haven web UI)
|
|
110
|
+
* @param config Pre-loaded ServerConfig; when omitted, an empty config
|
|
111
|
+
* is used (all /system/* endpoints locked down)
|
|
112
|
+
* @param configPath Explicit path to config.json; defaults to `<dataDir>/config.json`
|
|
113
|
+
*/
|
|
93
114
|
constructor(dataDir, serverPassword, staticDir, config, configPath) {
|
|
94
115
|
this.app = (0, express_1.default)();
|
|
95
116
|
this.tenantManager = new TenantManager_1.TenantManager(dataDir, serverPassword);
|
|
@@ -99,6 +120,8 @@ class MindooDBServer {
|
|
|
99
120
|
this.jsonBodyLimitBytes = parseBodySizeLimitToBytes(this.jsonBodyLimit);
|
|
100
121
|
this.serverConfig = config ?? { capabilities: {} };
|
|
101
122
|
this.capabilityMatcher = new CapabilityMatcher_1.CapabilityMatcher(this.serverConfig);
|
|
123
|
+
// SystemAdminAuthService generates a random HMAC-SHA256 secret for JWTs
|
|
124
|
+
// on each server start — existing tokens become invalid after restart.
|
|
102
125
|
const cryptoAdapter = this.tenantManager.getCryptoAdapter();
|
|
103
126
|
this.systemAdminAuth = new SystemAdminAuth_1.SystemAdminAuthService(cryptoAdapter, this.serverConfig);
|
|
104
127
|
const principalCount = Object.values(this.serverConfig.capabilities).reduce((sum, arr) => sum + arr.length, 0);
|
|
@@ -126,6 +149,11 @@ class MindooDBServer {
|
|
|
126
149
|
getConfigPath() {
|
|
127
150
|
return this.configPath;
|
|
128
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Hot-swap the server config without restarting. In-flight JWTs remain
|
|
154
|
+
* valid (same HMAC secret), but removed principals will fail the
|
|
155
|
+
* CapabilityMatcher check on their next request.
|
|
156
|
+
*/
|
|
129
157
|
reloadConfig(newConfig) {
|
|
130
158
|
this.serverConfig = newConfig;
|
|
131
159
|
this.capabilityMatcher.reload(newConfig);
|
|
@@ -135,19 +163,37 @@ class MindooDBServer {
|
|
|
135
163
|
const server = this.app.listen(port, () => {
|
|
136
164
|
console.log(`[MindooDBServer] Listening on port ${port}`);
|
|
137
165
|
});
|
|
138
|
-
server.setTimeout(
|
|
166
|
+
server.setTimeout(DEFAULT_SERVER_SOCKET_TIMEOUT_MS);
|
|
139
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Start the server with TLS using HTTP/2 (with automatic HTTP/1.1 ALPN
|
|
170
|
+
* fallback for clients that don't support h2). Express is mounted as the
|
|
171
|
+
* raw handler because http2.createSecureServer doesn't natively integrate
|
|
172
|
+
* with Express; the `as unknown` casts bridge the type gap.
|
|
173
|
+
*/
|
|
140
174
|
listenTls(port, certPath, keyPath) {
|
|
141
175
|
const tlsOptions = {
|
|
176
|
+
allowHTTP1: true,
|
|
142
177
|
cert: (0, fs_1.readFileSync)(certPath),
|
|
143
178
|
key: (0, fs_1.readFileSync)(keyPath),
|
|
144
179
|
};
|
|
145
|
-
const server =
|
|
180
|
+
const server = http2_1.default.createSecureServer(tlsOptions, (req, res) => {
|
|
181
|
+
this.app(req, res);
|
|
182
|
+
});
|
|
146
183
|
server.listen(port, () => {
|
|
147
|
-
console.log(`[MindooDBServer] Listening on HTTPS port ${port}`);
|
|
184
|
+
console.log(`[MindooDBServer] Listening on HTTPS port ${port} with HTTP/2 enabled (HTTP/1 fallback active)`);
|
|
148
185
|
});
|
|
149
|
-
server.setTimeout(
|
|
186
|
+
server.setTimeout(DEFAULT_SERVER_SOCKET_TIMEOUT_MS);
|
|
150
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Global middleware stack applied to every request, in order:
|
|
190
|
+
* 1. helmet — security headers (CSP, X-Frame-Options, etc.)
|
|
191
|
+
* 2. cors — controlled cross-origin access for web clients (e.g. Haven)
|
|
192
|
+
* 3. global rate limit — coarse IP-based throttle as a safety net;
|
|
193
|
+
* individual route tiers (auth, sync, system) provide finer control
|
|
194
|
+
* 4. JSON body parser — captures raw byte size for diagnostics
|
|
195
|
+
* 5. request logger
|
|
196
|
+
*/
|
|
151
197
|
setupMiddleware() {
|
|
152
198
|
this.app.use((0, helmet_1.default)());
|
|
153
199
|
const corsOrigin = this.readCorsOriginsFromEnv();
|
|
@@ -172,10 +218,11 @@ class MindooDBServer {
|
|
|
172
218
|
this.app.use((req, res, next) => {
|
|
173
219
|
const contentType = req.headers["content-type"] ?? "-";
|
|
174
220
|
const contentLength = req.headers["content-length"] ?? "-";
|
|
175
|
-
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} content-type=${contentType} content-length=${contentLength}`);
|
|
221
|
+
console.log(`[${new Date().toISOString()}] HTTP/${req.httpVersion} ${req.method} ${req.path} content-type=${contentType} content-length=${contentLength}`);
|
|
176
222
|
next();
|
|
177
223
|
});
|
|
178
224
|
}
|
|
225
|
+
/** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
|
|
179
226
|
readCorsOriginsFromEnv() {
|
|
180
227
|
const raw = process.env.MINDOODB_CORS_ORIGIN?.trim();
|
|
181
228
|
if (!raw) {
|
|
@@ -190,8 +237,16 @@ class MindooDBServer {
|
|
|
190
237
|
}
|
|
191
238
|
return origins;
|
|
192
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Route hierarchy:
|
|
242
|
+
* / — redirect to static UI (if present)
|
|
243
|
+
* /health — unauthenticated health probe
|
|
244
|
+
* /.well-known/* — public discovery endpoints (server info, tenant fingerprints)
|
|
245
|
+
* /statics/* — optional static file serving (Haven web UI)
|
|
246
|
+
* /system/* — admin API: IP-allowlisted, JWT-authenticated, capability-checked
|
|
247
|
+
* /:tenantId/* — per-tenant sync & auth API
|
|
248
|
+
*/
|
|
193
249
|
setupRoutes() {
|
|
194
|
-
// Root redirect
|
|
195
250
|
this.app.get("/", (req, res) => {
|
|
196
251
|
if (this.staticDir && (0, fs_1.existsSync)(path_1.default.join(this.staticDir, "index.html"))) {
|
|
197
252
|
res.redirect(302, "/statics/index.html");
|
|
@@ -203,6 +258,8 @@ class MindooDBServer {
|
|
|
203
258
|
this.app.get("/health", (req, res) => {
|
|
204
259
|
res.json({ status: "ok", timestamp: Date.now() });
|
|
205
260
|
});
|
|
261
|
+
// .well-known endpoints are unauthenticated so clients can discover
|
|
262
|
+
// server identity and tenant availability before attempting to authenticate.
|
|
206
263
|
this.app.get("/.well-known/mindoodb-server-info", (req, res) => {
|
|
207
264
|
const info = this.tenantManager.getServerPublicInfo();
|
|
208
265
|
if (!info) {
|
|
@@ -255,7 +312,10 @@ class MindooDBServer {
|
|
|
255
312
|
}, express_1.default.static(resolvedStaticDir, { dotfiles: "deny", etag: true }));
|
|
256
313
|
console.log(`[MindooDBServer] Serving static files from ${resolvedStaticDir} at /statics/`);
|
|
257
314
|
}
|
|
258
|
-
//
|
|
315
|
+
// /system/* admin routes are protected by three layers (applied in order):
|
|
316
|
+
// 1. IP allowlist — optional whitelist via MINDOODB_ADMIN_ALLOWED_IPS env var
|
|
317
|
+
// 2. Rate limit — aggressive per-IP throttle (30 req/min)
|
|
318
|
+
// 3. JWT auth — applied per-route (auth endpoints exempt; they *produce* JWTs)
|
|
259
319
|
const systemIpRaw = (0, SystemIpAllowlist_1.readSystemIpAllowListFromEnv)();
|
|
260
320
|
if (!(0, SystemIpAllowlist_1.isSystemIpAllowListDisabled)(systemIpRaw)) {
|
|
261
321
|
console.log(`[MindooDBServer] MINDOODB_ADMIN_ALLOWED_IPS is set — /system/* limited to: ${systemIpRaw}`);
|
|
@@ -270,8 +330,10 @@ class MindooDBServer {
|
|
|
270
330
|
const systemRouter = (0, express_1.Router)();
|
|
271
331
|
this.setupSystemRoutes(systemRouter);
|
|
272
332
|
this.app.use("/system", SystemIpAllowlist_1.systemIpAllowlistMiddleware, systemRateLimit, systemRouter);
|
|
273
|
-
// Tenant-scoped routes
|
|
333
|
+
// Tenant-scoped routes: tenantMiddleware validates the :tenantId param
|
|
334
|
+
// and rejects unknown tenants before any handler runs.
|
|
274
335
|
this.app.use("/:tenantId", this.tenantMiddleware.bind(this), this.createTenantRouter());
|
|
336
|
+
// Express error handler — must be registered last and have the 4-arg signature
|
|
275
337
|
this.app.use(this.errorHandler.bind(this));
|
|
276
338
|
}
|
|
277
339
|
// ==================== System Admin Auth Middleware ====================
|
|
@@ -328,18 +390,17 @@ class MindooDBServer {
|
|
|
328
390
|
next();
|
|
329
391
|
}
|
|
330
392
|
// ==================== System Routes ====================
|
|
393
|
+
/**
|
|
394
|
+
* Wire up all /system/* routes. Auth endpoints sit outside the JWT
|
|
395
|
+
* middleware because they implement the challenge-response handshake
|
|
396
|
+
* that *produces* the JWT. Every other route requires a valid JWT and
|
|
397
|
+
* passes through the CapabilityMatcher for fine-grained authorization.
|
|
398
|
+
*/
|
|
331
399
|
setupSystemRoutes(router) {
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
standardHeaders: true,
|
|
337
|
-
legacyHeaders: false,
|
|
338
|
-
message: { error: "Too many authentication attempts, please try again later" },
|
|
339
|
-
});
|
|
340
|
-
router.post("/auth/challenge", authRateLimit, this.handleSystemChallenge.bind(this));
|
|
341
|
-
router.post("/auth/authenticate", authRateLimit, this.handleSystemAuthenticate.bind(this));
|
|
342
|
-
// All other /system/* routes require JWT + capability check
|
|
400
|
+
const challengeRateLimit = this.createAuthRateLimit("system", "challenge");
|
|
401
|
+
const authenticateRateLimit = this.createAuthRateLimit("system", "authenticate");
|
|
402
|
+
router.post("/auth/challenge", challengeRateLimit, this.handleSystemChallenge.bind(this));
|
|
403
|
+
router.post("/auth/authenticate", authenticateRateLimit, this.handleSystemAuthenticate.bind(this));
|
|
343
404
|
const authMiddleware = this.systemAdminMiddleware.bind(this);
|
|
344
405
|
// Tenant CRUD
|
|
345
406
|
router.get("/tenants", authMiddleware, (req, res) => {
|
|
@@ -353,6 +414,8 @@ class MindooDBServer {
|
|
|
353
414
|
res.status(500).json({ error: "Failed to list tenants" });
|
|
354
415
|
}
|
|
355
416
|
});
|
|
417
|
+
// Tenant registration is idempotent: re-POSTing with the same tenantId
|
|
418
|
+
// and matching $publicinfos key returns 200 instead of 409.
|
|
356
419
|
router.post("/tenants/:tenantId", authMiddleware, async (req, res) => {
|
|
357
420
|
try {
|
|
358
421
|
const tenantId = req.params.tenantId.toLowerCase();
|
|
@@ -724,7 +787,8 @@ class MindooDBServer {
|
|
|
724
787
|
});
|
|
725
788
|
return;
|
|
726
789
|
}
|
|
727
|
-
// Self-lockout protection:
|
|
790
|
+
// Self-lockout protection: simulate the proposed config against the calling
|
|
791
|
+
// admin's identity — reject if they would lose their own config access.
|
|
728
792
|
const callingAdmin = req.systemAdmin;
|
|
729
793
|
const proposedMatcher = new CapabilityMatcher_1.CapabilityMatcher(newConfig);
|
|
730
794
|
if (!proposedMatcher.isAuthorized("PUT", "/system/config", callingAdmin.username, callingAdmin.publicsignkey)) {
|
|
@@ -756,18 +820,20 @@ class MindooDBServer {
|
|
|
756
820
|
}
|
|
757
821
|
}
|
|
758
822
|
// ==================== Tenant Routes ====================
|
|
823
|
+
/**
|
|
824
|
+
* Build the per-tenant router. Rate limiters are scoped: auth endpoints
|
|
825
|
+
* use a per-user composite key (see createAuthRateLimit) while sync
|
|
826
|
+
* endpoints use a plain IP-based bucket with a higher ceiling.
|
|
827
|
+
*
|
|
828
|
+
* Both "docs" and "attachments" store kinds expose the same set of sync
|
|
829
|
+
* operations, mounted at /sync/docs/* and /sync/attachments/*.
|
|
830
|
+
*/
|
|
759
831
|
createTenantRouter() {
|
|
760
832
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
833
|
+
const authRateLimitConfig = this.getAuthRateLimitConfig();
|
|
761
834
|
const syncRateLimitConfig = this.getSyncRateLimitConfig();
|
|
762
|
-
|
|
763
|
-
const
|
|
764
|
-
windowMs: 60000,
|
|
765
|
-
max: 20,
|
|
766
|
-
standardHeaders: true,
|
|
767
|
-
legacyHeaders: false,
|
|
768
|
-
message: { error: "Too many authentication attempts, please try again later" },
|
|
769
|
-
});
|
|
770
|
-
// Sync endpoints get a higher limit
|
|
835
|
+
const challengeRateLimit = this.createAuthRateLimit("tenant", "challenge");
|
|
836
|
+
const authenticateRateLimit = this.createAuthRateLimit("tenant", "authenticate");
|
|
771
837
|
const syncRateLimit = (0, express_rate_limit_1.default)({
|
|
772
838
|
windowMs: syncRateLimitConfig.windowMs,
|
|
773
839
|
max: syncRateLimitConfig.max,
|
|
@@ -775,32 +841,107 @@ class MindooDBServer {
|
|
|
775
841
|
legacyHeaders: false,
|
|
776
842
|
message: { error: "Too many sync requests, please try again later" },
|
|
777
843
|
});
|
|
778
|
-
|
|
779
|
-
router.post("/auth/
|
|
780
|
-
router.post("/
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
844
|
+
console.log(`[MindooDBServer] Tenant auth rate limit configured windowMs=${authRateLimitConfig.windowMs} max=${authRateLimitConfig.max}`);
|
|
845
|
+
router.post("/auth/challenge", challengeRateLimit, this.handleChallenge.bind(this));
|
|
846
|
+
router.post("/auth/authenticate", authenticateRateLimit, this.handleAuthenticate.bind(this));
|
|
847
|
+
for (const storeKind of ["docs", "attachments"]) {
|
|
848
|
+
const syncBase = `/sync/${storeKind}`;
|
|
849
|
+
router.post(`${syncBase}/findNewEntries`, syncRateLimit, this.handleFindNewEntries.bind(this));
|
|
850
|
+
router.post(`${syncBase}/findNewEntriesForDoc`, syncRateLimit, this.handleFindNewEntriesForDoc.bind(this));
|
|
851
|
+
router.post(`${syncBase}/findEntries`, syncRateLimit, this.handleFindEntries.bind(this));
|
|
852
|
+
router.post(`${syncBase}/scanEntriesSince`, syncRateLimit, this.handleScanEntriesSince.bind(this));
|
|
853
|
+
router.post(`${syncBase}/getIdBloomSummary`, syncRateLimit, this.handleGetIdBloomSummary.bind(this));
|
|
854
|
+
router.post(`${syncBase}/getCompactionStatus`, syncRateLimit, this.handleGetCompactionStatus.bind(this));
|
|
855
|
+
router.get(`${syncBase}/capabilities`, syncRateLimit, this.handleGetCapabilities.bind(this));
|
|
856
|
+
router.post(`${syncBase}/getEntries`, syncRateLimit, this.handleGetEntries.bind(this));
|
|
857
|
+
router.post(`${syncBase}/getEntryMetadata`, syncRateLimit, this.handleGetEntryMetadata.bind(this));
|
|
858
|
+
router.post(`${syncBase}/putEntries`, syncRateLimit, this.handlePutEntries.bind(this));
|
|
859
|
+
router.post(`${syncBase}/hasEntries`, syncRateLimit, this.handleHasEntries.bind(this));
|
|
860
|
+
router.get(`${syncBase}/getAllIds`, syncRateLimit, this.handleGetAllIds.bind(this));
|
|
861
|
+
router.post(`${syncBase}/resolveDependencies`, syncRateLimit, this.handleResolveDependencies.bind(this));
|
|
862
|
+
router.post(`${syncBase}/planDocumentMaterialization`, syncRateLimit, this.handlePlanDocumentMaterialization.bind(this));
|
|
863
|
+
router.post(`${syncBase}/planDocumentMaterializationBatch`, syncRateLimit, this.handlePlanDocumentMaterializationBatch.bind(this));
|
|
864
|
+
router.post(`${syncBase}/planAttachmentReadByWalkingMetadata`, syncRateLimit, this.handlePlanAttachmentReadByWalkingMetadata.bind(this));
|
|
865
|
+
}
|
|
796
866
|
return router;
|
|
797
867
|
}
|
|
868
|
+
/** Resolved auth rate-limit config; falls back to defaults when not set in config.json. */
|
|
869
|
+
getAuthRateLimitConfig() {
|
|
870
|
+
return {
|
|
871
|
+
windowMs: this.serverConfig.rateLimits?.auth?.windowMs ?? DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS,
|
|
872
|
+
max: this.serverConfig.rateLimits?.auth?.max ?? DEFAULT_AUTH_RATE_LIMIT_MAX,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
/** Resolved sync rate-limit config; falls back to defaults when not set in config.json. */
|
|
798
876
|
getSyncRateLimitConfig() {
|
|
799
877
|
return {
|
|
800
878
|
windowMs: this.serverConfig.rateLimits?.sync?.windowMs ?? DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS,
|
|
801
879
|
max: this.serverConfig.rateLimits?.sync?.max ?? DEFAULT_SYNC_RATE_LIMIT_MAX,
|
|
802
880
|
};
|
|
803
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Create a rate-limiter for authentication endpoints. Challenge and
|
|
884
|
+
* authenticate get separate limiters so that one phase can't starve the
|
|
885
|
+
* other. The custom keyGenerator buckets by (scope, tenant, phase, IP,
|
|
886
|
+
* username) — this prevents users behind a shared IP (NAT / reverse
|
|
887
|
+
* proxy) from exhausting each other's budgets.
|
|
888
|
+
*/
|
|
889
|
+
createAuthRateLimit(scope, phase) {
|
|
890
|
+
const authRateLimitConfig = this.getAuthRateLimitConfig();
|
|
891
|
+
return (0, express_rate_limit_1.default)({
|
|
892
|
+
windowMs: authRateLimitConfig.windowMs,
|
|
893
|
+
max: authRateLimitConfig.max,
|
|
894
|
+
standardHeaders: true,
|
|
895
|
+
legacyHeaders: false,
|
|
896
|
+
keyGenerator: (req) => this.getAuthRateLimitKey(req, scope, phase),
|
|
897
|
+
handler: (req, res, _next, options) => {
|
|
898
|
+
this.logAuthRateLimitHit(req, scope, phase);
|
|
899
|
+
res.status(options.statusCode).json(options.message);
|
|
900
|
+
},
|
|
901
|
+
message: { error: "Too many authentication attempts, please try again later" },
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Composite rate-limit key. Challenge requests include the username so
|
|
906
|
+
* that brute-forcing one account doesn't block legitimate users on the
|
|
907
|
+
* same IP. Authenticate requests omit username because the challenge
|
|
908
|
+
* token is already scoped to a single principal.
|
|
909
|
+
*/
|
|
910
|
+
getAuthRateLimitKey(req, scope, phase) {
|
|
911
|
+
const tenantId = scope === "tenant"
|
|
912
|
+
? req.tenantId ?? req.params.tenantId?.toLowerCase() ?? "-"
|
|
913
|
+
: "system";
|
|
914
|
+
const clientIp = this.getClientIpForRateLimit(req);
|
|
915
|
+
if (phase === "challenge") {
|
|
916
|
+
const username = typeof req.body?.username === "string"
|
|
917
|
+
? req.body.username.trim().toLowerCase()
|
|
918
|
+
: "-";
|
|
919
|
+
return `${scope}:${tenantId}:${phase}:${clientIp}:${username}`;
|
|
920
|
+
}
|
|
921
|
+
return `${scope}:${tenantId}:${phase}:${clientIp}`;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Emit a structured warning when an auth rate limit is triggered.
|
|
925
|
+
* The forwarded-for header is logged to help diagnose situations where
|
|
926
|
+
* a reverse proxy collapses many clients onto a single visible IP.
|
|
927
|
+
*/
|
|
928
|
+
logAuthRateLimitHit(req, scope, phase) {
|
|
929
|
+
const forwardedForHeader = req.headers["x-forwarded-for"];
|
|
930
|
+
const forwardedFor = Array.isArray(forwardedForHeader)
|
|
931
|
+
? forwardedForHeader.join(", ")
|
|
932
|
+
: forwardedForHeader;
|
|
933
|
+
const username = typeof req.body?.username === "string" ? req.body.username : "-";
|
|
934
|
+
console.warn(`[MindooDBServer] Auth rate limit exceeded scope=${scope} phase=${phase} tenant=${req.tenantId ?? req.params.tenantId?.toLowerCase() ?? "-"} ip=${this.getClientIpForRateLimit(req)} forwarded-for=${forwardedFor ?? "-"} username=${username} path=${req.path}`);
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* Best-effort client IP. Express populates `req.ip` from the trust-proxy
|
|
938
|
+
* setting; fall back to the raw socket address when it's unavailable.
|
|
939
|
+
*/
|
|
940
|
+
getClientIpForRateLimit(req) {
|
|
941
|
+
return ((typeof req.ip === "string" && req.ip.length > 0
|
|
942
|
+
? req.ip
|
|
943
|
+
: req.socket?.remoteAddress) ?? "unknown");
|
|
944
|
+
}
|
|
804
945
|
// ==================== Validation Helpers ====================
|
|
805
946
|
validateDbId(dbId) {
|
|
806
947
|
return (0, validation_1.validateIdentifier)(dbId, "dbId");
|
|
@@ -810,6 +951,10 @@ class MindooDBServer {
|
|
|
810
951
|
return `Tenant ${tenantId} not found on server ${serverName}`;
|
|
811
952
|
}
|
|
812
953
|
// ==================== Tenant Auth Handlers ====================
|
|
954
|
+
// Two-step challenge-response flow:
|
|
955
|
+
// 1. POST /auth/challenge → client sends username, server returns a random nonce
|
|
956
|
+
// 2. POST /auth/authenticate → client signs the nonce with its Ed25519 private key,
|
|
957
|
+
// server verifies the signature and returns a JWT for subsequent sync calls
|
|
813
958
|
async handleChallenge(req, res) {
|
|
814
959
|
try {
|
|
815
960
|
const { username } = req.body;
|
|
@@ -847,7 +992,7 @@ class MindooDBServer {
|
|
|
847
992
|
const { dbId, haveIds } = req.body;
|
|
848
993
|
const validDbId = this.validateDbId(dbId);
|
|
849
994
|
(0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
|
|
850
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
995
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
851
996
|
const entries = await serverStore.handleFindNewEntries(token, haveIds || []);
|
|
852
997
|
res.json({
|
|
853
998
|
entries: entries.map((e) => this.serializeEntryMetadata(e)),
|
|
@@ -867,7 +1012,7 @@ class MindooDBServer {
|
|
|
867
1012
|
return;
|
|
868
1013
|
}
|
|
869
1014
|
(0, validation_1.validateArraySize)(haveIds, validation_1.MAX_HAVE_IDS, "haveIds");
|
|
870
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1015
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
871
1016
|
const entries = await serverStore.handleFindNewEntriesForDoc(token, haveIds || [], docId);
|
|
872
1017
|
res.json({
|
|
873
1018
|
entries: entries.map((e) => this.serializeEntryMetadata(e)),
|
|
@@ -882,7 +1027,7 @@ class MindooDBServer {
|
|
|
882
1027
|
const token = this.extractToken(req);
|
|
883
1028
|
const { dbId, type, creationDateFrom, creationDateUntil } = req.body;
|
|
884
1029
|
const validDbId = this.validateDbId(dbId);
|
|
885
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1030
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
886
1031
|
const entries = await serverStore.handleFindEntries(token, type, creationDateFrom ?? null, creationDateUntil ?? null);
|
|
887
1032
|
res.json({
|
|
888
1033
|
entries: entries.map((e) => this.serializeEntryMetadata(e)),
|
|
@@ -897,7 +1042,7 @@ class MindooDBServer {
|
|
|
897
1042
|
const token = this.extractToken(req);
|
|
898
1043
|
const { dbId, cursor, limit, filters } = req.body;
|
|
899
1044
|
const validDbId = this.validateDbId(dbId);
|
|
900
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1045
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
901
1046
|
const result = await serverStore.handleScanEntriesSince(token, cursor ?? null, limit, filters);
|
|
902
1047
|
res.json({
|
|
903
1048
|
entries: result.entries.map((e) => this.serializeEntryMetadata(e)),
|
|
@@ -914,7 +1059,7 @@ class MindooDBServer {
|
|
|
914
1059
|
const token = this.extractToken(req);
|
|
915
1060
|
const { dbId } = req.body;
|
|
916
1061
|
const validDbId = this.validateDbId(dbId);
|
|
917
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1062
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
918
1063
|
const summary = await serverStore.handleGetIdBloomSummary(token);
|
|
919
1064
|
res.json({ summary: summary });
|
|
920
1065
|
}
|
|
@@ -927,7 +1072,7 @@ class MindooDBServer {
|
|
|
927
1072
|
const token = this.extractToken(req);
|
|
928
1073
|
const rawDbId = req.query.dbId || "directory";
|
|
929
1074
|
const validDbId = this.validateDbId(rawDbId);
|
|
930
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1075
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
931
1076
|
const capabilities = await serverStore.handleGetCapabilities(token);
|
|
932
1077
|
res.json({ capabilities: capabilities });
|
|
933
1078
|
}
|
|
@@ -940,7 +1085,7 @@ class MindooDBServer {
|
|
|
940
1085
|
const token = this.extractToken(req);
|
|
941
1086
|
const { dbId } = req.body;
|
|
942
1087
|
const validDbId = this.validateDbId(dbId);
|
|
943
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1088
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
944
1089
|
const status = await serverStore.handleGetCompactionStatus(token);
|
|
945
1090
|
res.json({ status: status });
|
|
946
1091
|
}
|
|
@@ -954,7 +1099,7 @@ class MindooDBServer {
|
|
|
954
1099
|
const { dbId, ids } = req.body;
|
|
955
1100
|
const validDbId = this.validateDbId(dbId);
|
|
956
1101
|
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
957
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1102
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
958
1103
|
const entries = await serverStore.handleGetEntries(token, ids || []);
|
|
959
1104
|
res.json({
|
|
960
1105
|
entries: entries.map((e) => ({
|
|
@@ -976,7 +1121,7 @@ class MindooDBServer {
|
|
|
976
1121
|
res.status(400).json({ error: "id is required" });
|
|
977
1122
|
return;
|
|
978
1123
|
}
|
|
979
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1124
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
980
1125
|
const entry = await serverStore.handleGetEntryMetadata(token, id);
|
|
981
1126
|
res.json({
|
|
982
1127
|
entry: entry ? this.serializeEntryMetadata(entry) : null,
|
|
@@ -986,6 +1131,11 @@ class MindooDBServer {
|
|
|
986
1131
|
this.handleRequestError(error, res);
|
|
987
1132
|
}
|
|
988
1133
|
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Ingest encrypted entries from a client. The `stage` variable tracks
|
|
1136
|
+
* progress through the pipeline so that error logs pinpoint the failing
|
|
1137
|
+
* step (token extraction → body parsing → deserialization → storage).
|
|
1138
|
+
*/
|
|
989
1139
|
async handlePutEntries(req, res) {
|
|
990
1140
|
let stage = "extract-token";
|
|
991
1141
|
try {
|
|
@@ -1002,7 +1152,7 @@ class MindooDBServer {
|
|
|
1002
1152
|
}, {});
|
|
1003
1153
|
console.log(`[MindooDBServer] putEntries request tenant=${req.tenantId ?? "-"} db=${validDbId} entries=${serializedEntries.length} parsed-bytes=${req.jsonBodyBytes ?? "-"} content-length=${req.headers["content-length"] ?? "-"} types=${JSON.stringify(serializedEntryTypeCounts)}`);
|
|
1004
1154
|
stage = "load-server-store";
|
|
1005
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1155
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1006
1156
|
stage = "deserialize-entries";
|
|
1007
1157
|
const deserializedEntries = serializedEntries.map((entry, index) => {
|
|
1008
1158
|
try {
|
|
@@ -1050,7 +1200,7 @@ class MindooDBServer {
|
|
|
1050
1200
|
const { dbId, ids } = req.body;
|
|
1051
1201
|
const validDbId = this.validateDbId(dbId);
|
|
1052
1202
|
(0, validation_1.validateArraySize)(ids, validation_1.MAX_ENTRY_IDS, "ids");
|
|
1053
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1203
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1054
1204
|
const existingIds = await serverStore.handleHasEntries(token, ids || []);
|
|
1055
1205
|
res.json({ ids: existingIds });
|
|
1056
1206
|
}
|
|
@@ -1063,7 +1213,7 @@ class MindooDBServer {
|
|
|
1063
1213
|
const token = this.extractToken(req);
|
|
1064
1214
|
const rawDbId = req.query.dbId || "directory";
|
|
1065
1215
|
const validDbId = this.validateDbId(rawDbId);
|
|
1066
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1216
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1067
1217
|
const ids = await serverStore.handleGetAllIds(token);
|
|
1068
1218
|
res.json({ ids });
|
|
1069
1219
|
}
|
|
@@ -1080,7 +1230,7 @@ class MindooDBServer {
|
|
|
1080
1230
|
res.status(400).json({ error: "startId is required" });
|
|
1081
1231
|
return;
|
|
1082
1232
|
}
|
|
1083
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1233
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1084
1234
|
const ids = await serverStore.handleResolveDependencies(token, startId, options);
|
|
1085
1235
|
res.json({ ids });
|
|
1086
1236
|
}
|
|
@@ -1097,7 +1247,7 @@ class MindooDBServer {
|
|
|
1097
1247
|
res.status(400).json({ error: "docId is required" });
|
|
1098
1248
|
return;
|
|
1099
1249
|
}
|
|
1100
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1250
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1101
1251
|
const plan = await serverStore.handlePlanDocumentMaterialization(token, docId, options);
|
|
1102
1252
|
res.json({ plan: plan });
|
|
1103
1253
|
}
|
|
@@ -1111,7 +1261,7 @@ class MindooDBServer {
|
|
|
1111
1261
|
const { dbId, docIds, options } = req.body;
|
|
1112
1262
|
const validDbId = this.validateDbId(dbId);
|
|
1113
1263
|
(0, validation_1.validateArraySize)(docIds, validation_1.MAX_HAVE_IDS, "docIds");
|
|
1114
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1264
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1115
1265
|
const batchPlan = await serverStore.handlePlanDocumentMaterializationBatch(token, docIds || [], options);
|
|
1116
1266
|
res.json({ batchPlan: batchPlan });
|
|
1117
1267
|
}
|
|
@@ -1136,7 +1286,7 @@ class MindooDBServer {
|
|
|
1136
1286
|
res.status(400).json({ error: "options.startByte is required" });
|
|
1137
1287
|
return;
|
|
1138
1288
|
}
|
|
1139
|
-
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId);
|
|
1289
|
+
const serverStore = await this.tenantManager.getServerStore(req.tenantId, validDbId, this.getStoreKindFromRequest(req));
|
|
1140
1290
|
const plan = await serverStore.handlePlanAttachmentReadByWalkingMetadata(token, lastChunkId, attachmentSize, options);
|
|
1141
1291
|
res.json({ plan: plan });
|
|
1142
1292
|
}
|
|
@@ -1149,13 +1299,29 @@ class MindooDBServer {
|
|
|
1149
1299
|
res.json({ success: true, message: "Sync triggered (not yet implemented)" });
|
|
1150
1300
|
}
|
|
1151
1301
|
// ==================== Helper Methods ====================
|
|
1302
|
+
/**
|
|
1303
|
+
* Derive the StoreKind from the request path. Both "docs" and
|
|
1304
|
+
* "attachments" share the same handler implementations; the store kind
|
|
1305
|
+
* selects which on-disk append-only store to operate against.
|
|
1306
|
+
*/
|
|
1307
|
+
getStoreKindFromRequest(req) {
|
|
1308
|
+
const trimmedPath = req.path.replace(/\/$/, "");
|
|
1309
|
+
if (trimmedPath.startsWith("/sync/docs/") || trimmedPath === "/sync/docs/capabilities" || trimmedPath === "/sync/docs/getAllIds") {
|
|
1310
|
+
return types_1.StoreKind.docs;
|
|
1311
|
+
}
|
|
1312
|
+
if (trimmedPath.startsWith("/sync/attachments/") || trimmedPath === "/sync/attachments/capabilities" || trimmedPath === "/sync/attachments/getAllIds") {
|
|
1313
|
+
return types_1.StoreKind.attachments;
|
|
1314
|
+
}
|
|
1315
|
+
throw new Error(`Unsupported sync store kind for path: ${req.path}`);
|
|
1316
|
+
}
|
|
1152
1317
|
extractToken(req) {
|
|
1153
1318
|
const auth = req.headers.authorization;
|
|
1154
1319
|
if (!auth || !auth.startsWith("Bearer ")) {
|
|
1155
|
-
throw new
|
|
1320
|
+
throw new types_2.NetworkError("INVALID_TOKEN", "Missing or invalid Authorization header");
|
|
1156
1321
|
}
|
|
1157
1322
|
return auth.substring(7);
|
|
1158
1323
|
}
|
|
1324
|
+
/** Map domain errors to HTTP status codes; unknown errors become 500. */
|
|
1159
1325
|
handleRequestError(error, res) {
|
|
1160
1326
|
if (error instanceof validation_1.ValidationError) {
|
|
1161
1327
|
res.status(400).json({ error: error.message });
|
|
@@ -1186,6 +1352,11 @@ class MindooDBServer {
|
|
|
1186
1352
|
return 500;
|
|
1187
1353
|
}
|
|
1188
1354
|
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Express error handler (4-arg signature). Handles body-parser errors
|
|
1357
|
+
* (413 entity too large, 400 parse failed) with structured JSON responses;
|
|
1358
|
+
* everything else is logged and returned as a generic 500.
|
|
1359
|
+
*/
|
|
1189
1360
|
errorHandler(err, req, res, next) {
|
|
1190
1361
|
const parserError = err;
|
|
1191
1362
|
const status = parserError.statusCode ?? parserError.status;
|
|
@@ -1221,6 +1392,9 @@ class MindooDBServer {
|
|
|
1221
1392
|
res.status(500).json({ error: "Internal server error" });
|
|
1222
1393
|
}
|
|
1223
1394
|
// ==================== Serialization ====================
|
|
1395
|
+
// Entries are stored with binary fields (Uint8Array) internally but
|
|
1396
|
+
// transmitted as base64 strings over JSON. These methods handle the
|
|
1397
|
+
// conversion in both directions.
|
|
1224
1398
|
serializeEntryMetadata(metadata) {
|
|
1225
1399
|
return {
|
|
1226
1400
|
entryType: metadata.entryType,
|