mindoodb 0.0.10 → 0.0.12
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 +1 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +10 -0
- package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.d.ts +1 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +4 -3
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +34 -0
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +194 -9
- package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
- package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts +22 -0
- package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts.map +1 -1
- package/dist/browser/cache/IndexedDBLocalCacheStore.js +185 -24
- package/dist/browser/cache/IndexedDBLocalCacheStore.js.map +1 -1
- package/dist/browser/index.d.ts +2 -2
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +3 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/index.mjs +2 -0
- package/dist/core/BaseMindooDB.d.ts +30 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +155 -38
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts +0 -1
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +3 -13
- 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/types.d.ts +17 -8
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts +104 -0
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +192 -32
- package/dist/node/server/MindooDBServer.js.map +1 -1
- package/dist/node/server/TenantManager.d.ts +2 -0
- package/dist/node/server/TenantManager.d.ts.map +1 -1
- package/dist/node/server/TenantManager.js +21 -12
- 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 +2 -2
- 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 +2 -1
|
@@ -51,7 +51,7 @@ 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"));
|
|
@@ -65,8 +65,19 @@ const SystemAdminAuth_1 = require("./SystemAdminAuth");
|
|
|
65
65
|
const config_1 = require("./config");
|
|
66
66
|
const SystemIpAllowlist_1 = require("./SystemIpAllowlist");
|
|
67
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;
|
|
68
77
|
const DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS = 60000;
|
|
69
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. */
|
|
70
81
|
function parseBodySizeLimitToBytes(limit) {
|
|
71
82
|
const normalized = limit.trim().toLowerCase();
|
|
72
83
|
if (!normalized) {
|
|
@@ -91,6 +102,15 @@ function parseBodySizeLimitToBytes(limit) {
|
|
|
91
102
|
return Math.round(value * multiplier);
|
|
92
103
|
}
|
|
93
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
|
+
*/
|
|
94
114
|
constructor(dataDir, serverPassword, staticDir, config, configPath) {
|
|
95
115
|
this.app = (0, express_1.default)();
|
|
96
116
|
this.tenantManager = new TenantManager_1.TenantManager(dataDir, serverPassword);
|
|
@@ -100,6 +120,8 @@ class MindooDBServer {
|
|
|
100
120
|
this.jsonBodyLimitBytes = parseBodySizeLimitToBytes(this.jsonBodyLimit);
|
|
101
121
|
this.serverConfig = config ?? { capabilities: {} };
|
|
102
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.
|
|
103
125
|
const cryptoAdapter = this.tenantManager.getCryptoAdapter();
|
|
104
126
|
this.systemAdminAuth = new SystemAdminAuth_1.SystemAdminAuthService(cryptoAdapter, this.serverConfig);
|
|
105
127
|
const principalCount = Object.values(this.serverConfig.capabilities).reduce((sum, arr) => sum + arr.length, 0);
|
|
@@ -127,6 +149,11 @@ class MindooDBServer {
|
|
|
127
149
|
getConfigPath() {
|
|
128
150
|
return this.configPath;
|
|
129
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
|
+
*/
|
|
130
157
|
reloadConfig(newConfig) {
|
|
131
158
|
this.serverConfig = newConfig;
|
|
132
159
|
this.capabilityMatcher.reload(newConfig);
|
|
@@ -136,19 +163,37 @@ class MindooDBServer {
|
|
|
136
163
|
const server = this.app.listen(port, () => {
|
|
137
164
|
console.log(`[MindooDBServer] Listening on port ${port}`);
|
|
138
165
|
});
|
|
139
|
-
server.setTimeout(
|
|
166
|
+
server.setTimeout(DEFAULT_SERVER_SOCKET_TIMEOUT_MS);
|
|
140
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
|
+
*/
|
|
141
174
|
listenTls(port, certPath, keyPath) {
|
|
142
175
|
const tlsOptions = {
|
|
176
|
+
allowHTTP1: true,
|
|
143
177
|
cert: (0, fs_1.readFileSync)(certPath),
|
|
144
178
|
key: (0, fs_1.readFileSync)(keyPath),
|
|
145
179
|
};
|
|
146
|
-
const server =
|
|
180
|
+
const server = http2_1.default.createSecureServer(tlsOptions, (req, res) => {
|
|
181
|
+
this.app(req, res);
|
|
182
|
+
});
|
|
147
183
|
server.listen(port, () => {
|
|
148
|
-
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)`);
|
|
149
185
|
});
|
|
150
|
-
server.setTimeout(
|
|
186
|
+
server.setTimeout(DEFAULT_SERVER_SOCKET_TIMEOUT_MS);
|
|
151
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
|
+
*/
|
|
152
197
|
setupMiddleware() {
|
|
153
198
|
this.app.use((0, helmet_1.default)());
|
|
154
199
|
const corsOrigin = this.readCorsOriginsFromEnv();
|
|
@@ -173,10 +218,11 @@ class MindooDBServer {
|
|
|
173
218
|
this.app.use((req, res, next) => {
|
|
174
219
|
const contentType = req.headers["content-type"] ?? "-";
|
|
175
220
|
const contentLength = req.headers["content-length"] ?? "-";
|
|
176
|
-
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}`);
|
|
177
222
|
next();
|
|
178
223
|
});
|
|
179
224
|
}
|
|
225
|
+
/** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
|
|
180
226
|
readCorsOriginsFromEnv() {
|
|
181
227
|
const raw = process.env.MINDOODB_CORS_ORIGIN?.trim();
|
|
182
228
|
if (!raw) {
|
|
@@ -191,8 +237,16 @@ class MindooDBServer {
|
|
|
191
237
|
}
|
|
192
238
|
return origins;
|
|
193
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
|
+
*/
|
|
194
249
|
setupRoutes() {
|
|
195
|
-
// Root redirect
|
|
196
250
|
this.app.get("/", (req, res) => {
|
|
197
251
|
if (this.staticDir && (0, fs_1.existsSync)(path_1.default.join(this.staticDir, "index.html"))) {
|
|
198
252
|
res.redirect(302, "/statics/index.html");
|
|
@@ -204,6 +258,8 @@ class MindooDBServer {
|
|
|
204
258
|
this.app.get("/health", (req, res) => {
|
|
205
259
|
res.json({ status: "ok", timestamp: Date.now() });
|
|
206
260
|
});
|
|
261
|
+
// .well-known endpoints are unauthenticated so clients can discover
|
|
262
|
+
// server identity and tenant availability before attempting to authenticate.
|
|
207
263
|
this.app.get("/.well-known/mindoodb-server-info", (req, res) => {
|
|
208
264
|
const info = this.tenantManager.getServerPublicInfo();
|
|
209
265
|
if (!info) {
|
|
@@ -256,7 +312,10 @@ class MindooDBServer {
|
|
|
256
312
|
}, express_1.default.static(resolvedStaticDir, { dotfiles: "deny", etag: true }));
|
|
257
313
|
console.log(`[MindooDBServer] Serving static files from ${resolvedStaticDir} at /statics/`);
|
|
258
314
|
}
|
|
259
|
-
//
|
|
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)
|
|
260
319
|
const systemIpRaw = (0, SystemIpAllowlist_1.readSystemIpAllowListFromEnv)();
|
|
261
320
|
if (!(0, SystemIpAllowlist_1.isSystemIpAllowListDisabled)(systemIpRaw)) {
|
|
262
321
|
console.log(`[MindooDBServer] MINDOODB_ADMIN_ALLOWED_IPS is set — /system/* limited to: ${systemIpRaw}`);
|
|
@@ -271,8 +330,10 @@ class MindooDBServer {
|
|
|
271
330
|
const systemRouter = (0, express_1.Router)();
|
|
272
331
|
this.setupSystemRoutes(systemRouter);
|
|
273
332
|
this.app.use("/system", SystemIpAllowlist_1.systemIpAllowlistMiddleware, systemRateLimit, systemRouter);
|
|
274
|
-
// Tenant-scoped routes
|
|
333
|
+
// Tenant-scoped routes: tenantMiddleware validates the :tenantId param
|
|
334
|
+
// and rejects unknown tenants before any handler runs.
|
|
275
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
|
|
276
337
|
this.app.use(this.errorHandler.bind(this));
|
|
277
338
|
}
|
|
278
339
|
// ==================== System Admin Auth Middleware ====================
|
|
@@ -329,18 +390,17 @@ class MindooDBServer {
|
|
|
329
390
|
next();
|
|
330
391
|
}
|
|
331
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
|
+
*/
|
|
332
399
|
setupSystemRoutes(router) {
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
standardHeaders: true,
|
|
338
|
-
legacyHeaders: false,
|
|
339
|
-
message: { error: "Too many authentication attempts, please try again later" },
|
|
340
|
-
});
|
|
341
|
-
router.post("/auth/challenge", authRateLimit, this.handleSystemChallenge.bind(this));
|
|
342
|
-
router.post("/auth/authenticate", authRateLimit, this.handleSystemAuthenticate.bind(this));
|
|
343
|
-
// 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));
|
|
344
404
|
const authMiddleware = this.systemAdminMiddleware.bind(this);
|
|
345
405
|
// Tenant CRUD
|
|
346
406
|
router.get("/tenants", authMiddleware, (req, res) => {
|
|
@@ -354,6 +414,8 @@ class MindooDBServer {
|
|
|
354
414
|
res.status(500).json({ error: "Failed to list tenants" });
|
|
355
415
|
}
|
|
356
416
|
});
|
|
417
|
+
// Tenant registration is idempotent: re-POSTing with the same tenantId
|
|
418
|
+
// and matching $publicinfos key returns 200 instead of 409.
|
|
357
419
|
router.post("/tenants/:tenantId", authMiddleware, async (req, res) => {
|
|
358
420
|
try {
|
|
359
421
|
const tenantId = req.params.tenantId.toLowerCase();
|
|
@@ -725,7 +787,8 @@ class MindooDBServer {
|
|
|
725
787
|
});
|
|
726
788
|
return;
|
|
727
789
|
}
|
|
728
|
-
// 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.
|
|
729
792
|
const callingAdmin = req.systemAdmin;
|
|
730
793
|
const proposedMatcher = new CapabilityMatcher_1.CapabilityMatcher(newConfig);
|
|
731
794
|
if (!proposedMatcher.isAuthorized("PUT", "/system/config", callingAdmin.username, callingAdmin.publicsignkey)) {
|
|
@@ -757,18 +820,20 @@ class MindooDBServer {
|
|
|
757
820
|
}
|
|
758
821
|
}
|
|
759
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
|
+
*/
|
|
760
831
|
createTenantRouter() {
|
|
761
832
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
833
|
+
const authRateLimitConfig = this.getAuthRateLimitConfig();
|
|
762
834
|
const syncRateLimitConfig = this.getSyncRateLimitConfig();
|
|
763
|
-
|
|
764
|
-
const
|
|
765
|
-
windowMs: 60000,
|
|
766
|
-
max: 20,
|
|
767
|
-
standardHeaders: true,
|
|
768
|
-
legacyHeaders: false,
|
|
769
|
-
message: { error: "Too many authentication attempts, please try again later" },
|
|
770
|
-
});
|
|
771
|
-
// Sync endpoints get a higher limit
|
|
835
|
+
const challengeRateLimit = this.createAuthRateLimit("tenant", "challenge");
|
|
836
|
+
const authenticateRateLimit = this.createAuthRateLimit("tenant", "authenticate");
|
|
772
837
|
const syncRateLimit = (0, express_rate_limit_1.default)({
|
|
773
838
|
windowMs: syncRateLimitConfig.windowMs,
|
|
774
839
|
max: syncRateLimitConfig.max,
|
|
@@ -776,8 +841,9 @@ class MindooDBServer {
|
|
|
776
841
|
legacyHeaders: false,
|
|
777
842
|
message: { error: "Too many sync requests, please try again later" },
|
|
778
843
|
});
|
|
779
|
-
|
|
780
|
-
router.post("/auth/
|
|
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));
|
|
781
847
|
for (const storeKind of ["docs", "attachments"]) {
|
|
782
848
|
const syncBase = `/sync/${storeKind}`;
|
|
783
849
|
router.post(`${syncBase}/findNewEntries`, syncRateLimit, this.handleFindNewEntries.bind(this));
|
|
@@ -799,12 +865,83 @@ class MindooDBServer {
|
|
|
799
865
|
}
|
|
800
866
|
return router;
|
|
801
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. */
|
|
802
876
|
getSyncRateLimitConfig() {
|
|
803
877
|
return {
|
|
804
878
|
windowMs: this.serverConfig.rateLimits?.sync?.windowMs ?? DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS,
|
|
805
879
|
max: this.serverConfig.rateLimits?.sync?.max ?? DEFAULT_SYNC_RATE_LIMIT_MAX,
|
|
806
880
|
};
|
|
807
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
|
+
}
|
|
808
945
|
// ==================== Validation Helpers ====================
|
|
809
946
|
validateDbId(dbId) {
|
|
810
947
|
return (0, validation_1.validateIdentifier)(dbId, "dbId");
|
|
@@ -814,6 +951,10 @@ class MindooDBServer {
|
|
|
814
951
|
return `Tenant ${tenantId} not found on server ${serverName}`;
|
|
815
952
|
}
|
|
816
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
|
|
817
958
|
async handleChallenge(req, res) {
|
|
818
959
|
try {
|
|
819
960
|
const { username } = req.body;
|
|
@@ -990,6 +1131,11 @@ class MindooDBServer {
|
|
|
990
1131
|
this.handleRequestError(error, res);
|
|
991
1132
|
}
|
|
992
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
|
+
*/
|
|
993
1139
|
async handlePutEntries(req, res) {
|
|
994
1140
|
let stage = "extract-token";
|
|
995
1141
|
try {
|
|
@@ -1153,6 +1299,11 @@ class MindooDBServer {
|
|
|
1153
1299
|
res.json({ success: true, message: "Sync triggered (not yet implemented)" });
|
|
1154
1300
|
}
|
|
1155
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
|
+
*/
|
|
1156
1307
|
getStoreKindFromRequest(req) {
|
|
1157
1308
|
const trimmedPath = req.path.replace(/\/$/, "");
|
|
1158
1309
|
if (trimmedPath.startsWith("/sync/docs/") || trimmedPath === "/sync/docs/capabilities" || trimmedPath === "/sync/docs/getAllIds") {
|
|
@@ -1170,6 +1321,7 @@ class MindooDBServer {
|
|
|
1170
1321
|
}
|
|
1171
1322
|
return auth.substring(7);
|
|
1172
1323
|
}
|
|
1324
|
+
/** Map domain errors to HTTP status codes; unknown errors become 500. */
|
|
1173
1325
|
handleRequestError(error, res) {
|
|
1174
1326
|
if (error instanceof validation_1.ValidationError) {
|
|
1175
1327
|
res.status(400).json({ error: error.message });
|
|
@@ -1200,6 +1352,11 @@ class MindooDBServer {
|
|
|
1200
1352
|
return 500;
|
|
1201
1353
|
}
|
|
1202
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
|
+
*/
|
|
1203
1360
|
errorHandler(err, req, res, next) {
|
|
1204
1361
|
const parserError = err;
|
|
1205
1362
|
const status = parserError.statusCode ?? parserError.status;
|
|
@@ -1235,6 +1392,9 @@ class MindooDBServer {
|
|
|
1235
1392
|
res.status(500).json({ error: "Internal server error" });
|
|
1236
1393
|
}
|
|
1237
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.
|
|
1238
1398
|
serializeEntryMetadata(metadata) {
|
|
1239
1399
|
return {
|
|
1240
1400
|
entryType: metadata.entryType,
|