mindoodb 0.0.47 → 0.0.49
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/README-server.md +39 -3
- package/dist/appendonlystores/network/HttpTransport.d.ts +14 -0
- package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
- package/dist/appendonlystores/network/HttpTransport.js +58 -39
- package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
- package/dist/appendonlystores/network/RequestScheduler.d.ts +65 -0
- package/dist/appendonlystores/network/RequestScheduler.d.ts.map +1 -0
- package/dist/appendonlystores/network/RequestScheduler.js +147 -0
- package/dist/appendonlystores/network/RequestScheduler.js.map +1 -0
- package/dist/appendonlystores/network/index.d.ts +1 -0
- package/dist/appendonlystores/network/index.d.ts.map +1 -1
- package/dist/appendonlystores/network/index.js +6 -1
- package/dist/appendonlystores/network/index.js.map +1 -1
- package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts +15 -6
- package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts.map +1 -1
- package/dist/browser/cache/IndexedDBLocalCacheStore.js +85 -26
- package/dist/browser/cache/IndexedDBLocalCacheStore.js.map +1 -1
- package/dist/browser/index.d.ts +1 -1
- 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 +127 -0
- package/dist/core/BaseMindooDB.d.ts.map +1 -1
- package/dist/core/BaseMindooDB.js +514 -51
- package/dist/core/BaseMindooDB.js.map +1 -1
- package/dist/core/BaseMindooTenant.d.ts.map +1 -1
- package/dist/core/BaseMindooTenant.js +8 -1
- package/dist/core/BaseMindooTenant.js.map +1 -1
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts +10 -0
- package/dist/core/appendonlystores/network/NetworkTransport.d.ts.map +1 -1
- package/dist/core/expressions/builder.d.ts +1 -0
- package/dist/core/expressions/builder.d.ts.map +1 -1
- package/dist/core/expressions/builder.js +3 -0
- package/dist/core/expressions/builder.js.map +1 -1
- package/dist/core/expressions/evaluateExpression.d.ts +2 -0
- package/dist/core/expressions/evaluateExpression.d.ts.map +1 -1
- package/dist/core/expressions/evaluateExpression.js +2 -0
- package/dist/core/expressions/evaluateExpression.js.map +1 -1
- package/dist/core/expressions/formulaSource.d.ts.map +1 -1
- package/dist/core/expressions/formulaSource.js +1 -0
- package/dist/core/expressions/formulaSource.js.map +1 -1
- package/dist/core/expressions/metadata.d.ts +1 -1
- package/dist/core/expressions/metadata.d.ts.map +1 -1
- package/dist/core/expressions/metadata.js +1 -0
- package/dist/core/expressions/metadata.js.map +1 -1
- package/dist/core/expressions/types.d.ts +1 -1
- package/dist/core/expressions/types.d.ts.map +1 -1
- package/dist/core/indexing/createViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/createViewDataProvider.js +1 -0
- package/dist/core/indexing/createViewDataProvider.js.map +1 -1
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js +1 -0
- package/dist/core/indexing/summary/SummaryVirtualViewDataProvider.js.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.d.ts.map +1 -1
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js +1 -0
- package/dist/core/indexing/virtualviews/MindooDBVirtualViewDataProvider.js.map +1 -1
- package/dist/core/query/executeQuery.d.ts.map +1 -1
- package/dist/core/query/executeQuery.js +2 -0
- package/dist/core/query/executeQuery.js.map +1 -1
- package/dist/core/types.d.ts +19 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/node/server/MindooDBServer.d.ts +51 -5
- package/dist/node/server/MindooDBServer.d.ts.map +1 -1
- package/dist/node/server/MindooDBServer.js +164 -40
- package/dist/node/server/MindooDBServer.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/rateLimits.d.ts +46 -0
- package/dist/node/server/rateLimits.d.ts.map +1 -0
- package/dist/node/server/rateLimits.js +50 -0
- package/dist/node/server/rateLimits.js.map +1 -0
- package/dist/node/server/types.d.ts +15 -0
- package/dist/node/server/types.d.ts.map +1 -1
- package/dist/node/server/types.js +9 -0
- package/dist/node/server/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -56,7 +56,7 @@ 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
|
-
const express_rate_limit_1 =
|
|
59
|
+
const express_rate_limit_1 = __importStar(require("express-rate-limit"));
|
|
60
60
|
const compressionMiddleware_1 = require("./compressionMiddleware");
|
|
61
61
|
const binaryEntryFraming_1 = require("../../core/appendonlystores/network/binaryEntryFraming");
|
|
62
62
|
const SyncEventBus_1 = require("./SyncEventBus");
|
|
@@ -71,6 +71,7 @@ const validation_1 = require("./validation");
|
|
|
71
71
|
const urlSafety_1 = require("../../core/utils/urlSafety");
|
|
72
72
|
const types_3 = require("./types");
|
|
73
73
|
const timestampProxy_1 = require("./timestampProxy");
|
|
74
|
+
const rateLimits_1 = require("./rateLimits");
|
|
74
75
|
/**
|
|
75
76
|
* Rate-limit defaults. Auth and sync have separate tiers because a single
|
|
76
77
|
* challenge/authenticate handshake is two requests; with multiple tenants,
|
|
@@ -81,7 +82,23 @@ const timestampProxy_1 = require("./timestampProxy");
|
|
|
81
82
|
const DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS = 60000;
|
|
82
83
|
const DEFAULT_AUTH_RATE_LIMIT_MAX = 120;
|
|
83
84
|
const DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS = 60000;
|
|
84
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Sync is keyed per authenticated principal, and a client legitimately opens
|
|
87
|
+
* several databases at once: a host like Teacher's Desk reads six, and a cold
|
|
88
|
+
* pass over one of them costs a few hundred batched requests. A budget sized
|
|
89
|
+
* for a single database turns that normal open into a throttled one.
|
|
90
|
+
*/
|
|
91
|
+
const DEFAULT_SYNC_RATE_LIMIT_MAX = 3000;
|
|
92
|
+
/**
|
|
93
|
+
* The global limiter is a coarse net for unauthenticated floods and unknown
|
|
94
|
+
* routes; the per-tier limiters are the real budgets. It runs first, so a
|
|
95
|
+
* global max below a tier max silently replaces that tier — the sync budget
|
|
96
|
+
* was unreachable while this was a hardcoded 500. The default is therefore
|
|
97
|
+
* derived from the tiers, and an override that would shadow one is honoured
|
|
98
|
+
* but flagged at startup rather than quietly applied.
|
|
99
|
+
*/
|
|
100
|
+
const DEFAULT_GLOBAL_RATE_LIMIT_WINDOW_MS = 60000;
|
|
101
|
+
const GLOBAL_RATE_LIMIT_TIER_HEADROOM = 500;
|
|
85
102
|
/**
|
|
86
103
|
* Timestamping gets its own budget rather than sharing the sync one. Sync is
|
|
87
104
|
* chatty and high-volume; sealing a document is a deliberate, rare user action.
|
|
@@ -242,6 +259,7 @@ class MindooDBServer {
|
|
|
242
259
|
* 5. request logger
|
|
243
260
|
*/
|
|
244
261
|
setupMiddleware() {
|
|
262
|
+
this.applyTrustProxySetting();
|
|
245
263
|
this.app.use((0, helmet_1.default)());
|
|
246
264
|
const corsOrigin = this.readCorsOriginsFromEnv();
|
|
247
265
|
this.app.use((0, cors_1.default)({
|
|
@@ -249,9 +267,11 @@ class MindooDBServer {
|
|
|
249
267
|
methods: ["GET", "POST", "PUT", "DELETE"],
|
|
250
268
|
allowedHeaders: ["Content-Type", "Authorization"],
|
|
251
269
|
}));
|
|
270
|
+
const globalRateLimitConfig = this.getGlobalRateLimitConfig();
|
|
271
|
+
console.log(`[MindooDBServer] Global rate limit configured windowMs=${globalRateLimitConfig.windowMs} max=${globalRateLimitConfig.max}`);
|
|
252
272
|
this.app.use((0, express_rate_limit_1.default)({
|
|
253
|
-
windowMs:
|
|
254
|
-
max:
|
|
273
|
+
windowMs: globalRateLimitConfig.windowMs,
|
|
274
|
+
max: globalRateLimitConfig.max,
|
|
255
275
|
standardHeaders: true,
|
|
256
276
|
legacyHeaders: false,
|
|
257
277
|
message: { error: "Too many requests, please try again later" },
|
|
@@ -273,6 +293,40 @@ class MindooDBServer {
|
|
|
273
293
|
});
|
|
274
294
|
}
|
|
275
295
|
/** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
|
|
296
|
+
/**
|
|
297
|
+
* Teach Express which upstream hops to trust for `X-Forwarded-For`, so
|
|
298
|
+
* `req.ip` is the client rather than the proxy. Every rate limiter keys on
|
|
299
|
+
* `req.ip`; without this, a deployment behind Cloudflare or nginx puts all
|
|
300
|
+
* of its users in one bucket and the first busy client locks out the rest.
|
|
301
|
+
*
|
|
302
|
+
* `MINDOODB_TRUST_PROXY` takes the Express `trust proxy` values: a hop count
|
|
303
|
+
* (`1` for Cloudflare only, `2` when nginx also fronts the origin), a
|
|
304
|
+
* comma-separated list of trusted addresses/CIDRs, or `loopback`. Off by
|
|
305
|
+
* default, because trusting a header nobody strips lets a client spoof its
|
|
306
|
+
* own address and mint unlimited budgets. Prefer a hop count or an address
|
|
307
|
+
* list over `true`, which trusts the whole chain.
|
|
308
|
+
*/
|
|
309
|
+
applyTrustProxySetting() {
|
|
310
|
+
const raw = process.env[types_3.ENV_VARS.TRUST_PROXY]?.trim();
|
|
311
|
+
if (!raw || /^(0|false)$/i.test(raw)) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const hops = Number(raw);
|
|
315
|
+
const value = Number.isInteger(hops) && hops >= 0
|
|
316
|
+
? hops
|
|
317
|
+
: /^true$/i.test(raw)
|
|
318
|
+
? true
|
|
319
|
+
: raw.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
320
|
+
this.app.set("trust proxy", value);
|
|
321
|
+
if (value === true) {
|
|
322
|
+
console.warn("[MindooDBServer] MINDOODB_TRUST_PROXY=true trusts the entire forwarding chain; " +
|
|
323
|
+
"a client can then spoof X-Forwarded-For and bypass per-IP rate limits. " +
|
|
324
|
+
"Set a hop count (e.g. 1 behind Cloudflare) or a trusted address list instead.");
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
console.log(`[MindooDBServer] trust proxy set to ${JSON.stringify(value)}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
276
330
|
readCorsOriginsFromEnv() {
|
|
277
331
|
const raw = process.env.MINDOODB_CORS_ORIGIN?.trim();
|
|
278
332
|
if (!raw) {
|
|
@@ -906,9 +960,9 @@ class MindooDBServer {
|
|
|
906
960
|
}
|
|
907
961
|
// ==================== Tenant Routes ====================
|
|
908
962
|
/**
|
|
909
|
-
* Build the per-tenant router. Rate limiters are scoped: auth
|
|
910
|
-
* use a per-user composite key (see createAuthRateLimit
|
|
911
|
-
*
|
|
963
|
+
* Build the per-tenant router. Rate limiters are scoped: auth and sync
|
|
964
|
+
* endpoints both use a per-user composite key (see createAuthRateLimit and
|
|
965
|
+
* createSyncRateLimit), sync with a much higher ceiling.
|
|
912
966
|
*
|
|
913
967
|
* Both "docs" and "attachments" store kinds expose the same set of sync
|
|
914
968
|
* operations, mounted at /sync/docs/* and /sync/attachments/*.
|
|
@@ -916,16 +970,13 @@ class MindooDBServer {
|
|
|
916
970
|
createTenantRouter() {
|
|
917
971
|
const router = (0, express_1.Router)({ mergeParams: true });
|
|
918
972
|
const authRateLimitConfig = this.getAuthRateLimitConfig();
|
|
919
|
-
const syncRateLimitConfig = this.getSyncRateLimitConfig();
|
|
920
973
|
const challengeRateLimit = this.createAuthRateLimit("tenant", "challenge");
|
|
921
974
|
const authenticateRateLimit = this.createAuthRateLimit("tenant", "authenticate");
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
message: { error: "Too many sync requests, please try again later" },
|
|
928
|
-
});
|
|
975
|
+
// Identity resolution precedes the limiter so it can key on the caller.
|
|
976
|
+
const syncGuards = [
|
|
977
|
+
this.syncPrincipalMiddleware.bind(this),
|
|
978
|
+
this.createSyncRateLimit(),
|
|
979
|
+
];
|
|
929
980
|
console.log(`[MindooDBServer] Tenant auth rate limit configured windowMs=${authRateLimitConfig.windowMs} max=${authRateLimitConfig.max}`);
|
|
930
981
|
router.post("/auth/challenge", challengeRateLimit, this.handleChallenge.bind(this));
|
|
931
982
|
router.post("/auth/authenticate", authenticateRateLimit, this.handleAuthenticate.bind(this));
|
|
@@ -935,34 +986,53 @@ class MindooDBServer {
|
|
|
935
986
|
router.post("/timestamps/rfc3161", this.timestampAuthMiddleware.bind(this), this.createTimestampRateLimit("burst"), this.createTimestampRateLimit("daily"), express_1.default.raw({ type: timestampProxy_1.TIMESTAMP_QUERY_CONTENT_TYPE, limit: timestampProxy_1.MAX_TIMESTAMP_REQUEST_BYTES }), this.handleTimestampRequest.bind(this));
|
|
936
987
|
for (const storeKind of ["docs", "attachments"]) {
|
|
937
988
|
const syncBase = `/sync/${storeKind}`;
|
|
938
|
-
router.post(`${syncBase}/findNewEntries`,
|
|
939
|
-
router.post(`${syncBase}/findNewEntriesForDoc`,
|
|
940
|
-
router.post(`${syncBase}/findEntries`,
|
|
941
|
-
router.post(`${syncBase}/scanEntriesSince`,
|
|
942
|
-
router.post(`${syncBase}/getStoreHead`,
|
|
943
|
-
router.post(`${syncBase}/getIdBloomSummary`,
|
|
944
|
-
router.post(`${syncBase}/getCompactionStatus`,
|
|
945
|
-
router.get(`${syncBase}/capabilities`,
|
|
989
|
+
router.post(`${syncBase}/findNewEntries`, syncGuards, this.handleFindNewEntries.bind(this));
|
|
990
|
+
router.post(`${syncBase}/findNewEntriesForDoc`, syncGuards, this.handleFindNewEntriesForDoc.bind(this));
|
|
991
|
+
router.post(`${syncBase}/findEntries`, syncGuards, this.handleFindEntries.bind(this));
|
|
992
|
+
router.post(`${syncBase}/scanEntriesSince`, syncGuards, this.handleScanEntriesSince.bind(this));
|
|
993
|
+
router.post(`${syncBase}/getStoreHead`, syncGuards, this.handleGetStoreHead.bind(this));
|
|
994
|
+
router.post(`${syncBase}/getIdBloomSummary`, syncGuards, this.handleGetIdBloomSummary.bind(this));
|
|
995
|
+
router.post(`${syncBase}/getCompactionStatus`, syncGuards, this.handleGetCompactionStatus.bind(this));
|
|
996
|
+
router.get(`${syncBase}/capabilities`, syncGuards, this.handleGetCapabilities.bind(this));
|
|
946
997
|
// SSE live change feed (sync-v5, phase 5): one long-lived GET per
|
|
947
998
|
// subscriber; putEntries publishes into the in-process event bus.
|
|
948
|
-
router.get(`${syncBase}/events`,
|
|
949
|
-
router.post(`${syncBase}/getEntries`,
|
|
999
|
+
router.get(`${syncBase}/events`, syncGuards, this.handleSyncEvents.bind(this));
|
|
1000
|
+
router.post(`${syncBase}/getEntries`, syncGuards, this.handleGetEntries.bind(this));
|
|
950
1001
|
// Binary wire format v2 (sync-v5, phase 3): octet-stream framing for the
|
|
951
1002
|
// two payload-heavy endpoints. putEntriesBinary needs its own raw body
|
|
952
1003
|
// parser (the global JSON parser only handles application/json).
|
|
953
|
-
router.post(`${syncBase}/getEntriesBinary`,
|
|
954
|
-
router.post(`${syncBase}/putEntriesBinary`,
|
|
955
|
-
router.post(`${syncBase}/getEntryMetadata`,
|
|
956
|
-
router.post(`${syncBase}/putEntries`,
|
|
957
|
-
router.post(`${syncBase}/hasEntries`,
|
|
958
|
-
router.get(`${syncBase}/getAllIds`,
|
|
959
|
-
router.post(`${syncBase}/resolveDependencies`,
|
|
960
|
-
router.post(`${syncBase}/planDocumentMaterialization`,
|
|
961
|
-
router.post(`${syncBase}/planDocumentMaterializationBatch`,
|
|
962
|
-
router.post(`${syncBase}/planAttachmentReadByWalkingMetadata`,
|
|
1004
|
+
router.post(`${syncBase}/getEntriesBinary`, syncGuards, this.handleGetEntriesBinary.bind(this));
|
|
1005
|
+
router.post(`${syncBase}/putEntriesBinary`, syncGuards, express_1.default.raw({ type: binaryEntryFraming_1.BINARY_ENTRIES_CONTENT_TYPE, limit: this.jsonBodyLimit }), this.handlePutEntriesBinary.bind(this));
|
|
1006
|
+
router.post(`${syncBase}/getEntryMetadata`, syncGuards, this.handleGetEntryMetadata.bind(this));
|
|
1007
|
+
router.post(`${syncBase}/putEntries`, syncGuards, this.handlePutEntries.bind(this));
|
|
1008
|
+
router.post(`${syncBase}/hasEntries`, syncGuards, this.handleHasEntries.bind(this));
|
|
1009
|
+
router.get(`${syncBase}/getAllIds`, syncGuards, this.handleGetAllIds.bind(this));
|
|
1010
|
+
router.post(`${syncBase}/resolveDependencies`, syncGuards, this.handleResolveDependencies.bind(this));
|
|
1011
|
+
router.post(`${syncBase}/planDocumentMaterialization`, syncGuards, this.handlePlanDocumentMaterialization.bind(this));
|
|
1012
|
+
router.post(`${syncBase}/planDocumentMaterializationBatch`, syncGuards, this.handlePlanDocumentMaterializationBatch.bind(this));
|
|
1013
|
+
router.post(`${syncBase}/planAttachmentReadByWalkingMetadata`, syncGuards, this.handlePlanAttachmentReadByWalkingMetadata.bind(this));
|
|
963
1014
|
}
|
|
964
1015
|
return router;
|
|
965
1016
|
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Resolved global rate-limit config. The default leaves room for the sync
|
|
1019
|
+
* and auth tiers to spend their full budgets inside the global window, so
|
|
1020
|
+
* the net catches only traffic no tier claims.
|
|
1021
|
+
*/
|
|
1022
|
+
getGlobalRateLimitConfig() {
|
|
1023
|
+
const resolved = (0, rateLimits_1.resolveGlobalRateLimit)(this.serverConfig.rateLimits?.global, {
|
|
1024
|
+
sync: this.getSyncRateLimitConfig(),
|
|
1025
|
+
auth: this.getAuthRateLimitConfig(),
|
|
1026
|
+
defaultWindowMs: DEFAULT_GLOBAL_RATE_LIMIT_WINDOW_MS,
|
|
1027
|
+
headroom: GLOBAL_RATE_LIMIT_TIER_HEADROOM,
|
|
1028
|
+
});
|
|
1029
|
+
if (resolved.shadowsTiers) {
|
|
1030
|
+
console.warn(`[MindooDBServer] rateLimits.global.max=${resolved.max} is below the ${resolved.tierFloor} ` +
|
|
1031
|
+
"requests the sync and auth tiers may spend in the same window; the global limiter " +
|
|
1032
|
+
"will reject requests those tiers still allow.");
|
|
1033
|
+
}
|
|
1034
|
+
return { windowMs: resolved.windowMs, max: resolved.max };
|
|
1035
|
+
}
|
|
966
1036
|
/** Resolved auth rate-limit config; falls back to defaults when not set in config.json. */
|
|
967
1037
|
getAuthRateLimitConfig() {
|
|
968
1038
|
return {
|
|
@@ -1006,7 +1076,7 @@ class MindooDBServer {
|
|
|
1006
1076
|
keyGenerator: (req) => {
|
|
1007
1077
|
const tenantId = req.tenantId ?? "unknown";
|
|
1008
1078
|
const principal = req.timestampPrincipal ?? "anonymous";
|
|
1009
|
-
return `timestamp:${tier}:${tenantId}:${principal}:${req
|
|
1079
|
+
return `timestamp:${tier}:${tenantId}:${principal}:${this.getClientIpForRateLimit(req)}`;
|
|
1010
1080
|
},
|
|
1011
1081
|
handler: (req, res, _next, options) => {
|
|
1012
1082
|
console.warn(`[MindooDBServer] Timestamp rate limit hit (${tier}) tenant=${req.tenantId} ip=${req.ip}`);
|
|
@@ -1019,6 +1089,53 @@ class MindooDBServer {
|
|
|
1019
1089
|
},
|
|
1020
1090
|
});
|
|
1021
1091
|
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Resolve the caller of a sync request so {@link createSyncRateLimit} can
|
|
1094
|
+
* bucket per identity instead of per address.
|
|
1095
|
+
*
|
|
1096
|
+
* Unlike {@link timestampAuthMiddleware} this never rejects: the route
|
|
1097
|
+
* handlers do their own token validation and own the 401, and this pass only
|
|
1098
|
+
* needs to know whose budget to spend. An unverifiable token simply falls
|
|
1099
|
+
* back to address-only bucketing — a forged one must not be able to conjure
|
|
1100
|
+
* a fresh budget, which is why the token is verified rather than decoded.
|
|
1101
|
+
*/
|
|
1102
|
+
async syncPrincipalMiddleware(req, _res, next) {
|
|
1103
|
+
try {
|
|
1104
|
+
const token = this.extractToken(req);
|
|
1105
|
+
const authService = await this.tenantManager.getAuthService(req.tenantId);
|
|
1106
|
+
const payload = await authService.validateToken(token);
|
|
1107
|
+
if (payload && payload.tenantId === req.tenantId) {
|
|
1108
|
+
req.syncPrincipal = payload.deviceSigningKey ?? payload.sub;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
// No usable token; fall through to address-only bucketing.
|
|
1113
|
+
}
|
|
1114
|
+
next();
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Sync budget, keyed on tenant + principal + IP like the auth and timestamp
|
|
1118
|
+
* tiers. Address-only keying would put every user behind one NAT or reverse
|
|
1119
|
+
* proxy in a single bucket, so one client opening several databases would
|
|
1120
|
+
* throttle everyone else on that address.
|
|
1121
|
+
*/
|
|
1122
|
+
createSyncRateLimit() {
|
|
1123
|
+
const config = this.getSyncRateLimitConfig();
|
|
1124
|
+
console.log(`[MindooDBServer] Tenant sync rate limit configured windowMs=${config.windowMs} max=${config.max}`);
|
|
1125
|
+
return (0, express_rate_limit_1.default)({
|
|
1126
|
+
windowMs: config.windowMs,
|
|
1127
|
+
max: config.max,
|
|
1128
|
+
standardHeaders: true,
|
|
1129
|
+
legacyHeaders: false,
|
|
1130
|
+
keyGenerator: (req) => (0, rateLimits_1.buildSyncRateLimitKey)(req.tenantId, req.syncPrincipal, this.getClientIpForRateLimit(req)),
|
|
1131
|
+
handler: (req, res, _next, options) => {
|
|
1132
|
+
console.warn(`[MindooDBServer] Sync rate limit hit tenant=${req.tenantId} ` +
|
|
1133
|
+
`principal=${req.syncPrincipal ?? "anonymous"} ip=${req.ip} path=${req.path}`);
|
|
1134
|
+
res.status(options.statusCode).json(options.message);
|
|
1135
|
+
},
|
|
1136
|
+
message: { error: "Too many sync requests, please try again later" },
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1022
1139
|
/**
|
|
1023
1140
|
* Authenticate a timestamp request with the tenant's existing bearer token and
|
|
1024
1141
|
* record the principal for rate-limit bucketing.
|
|
@@ -1158,13 +1275,20 @@ class MindooDBServer {
|
|
|
1158
1275
|
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}`);
|
|
1159
1276
|
}
|
|
1160
1277
|
/**
|
|
1161
|
-
* Best-effort client IP. Express populates `req.ip` from the
|
|
1162
|
-
* setting; fall back to the raw socket address when it's
|
|
1278
|
+
* Best-effort client IP for bucketing. Express populates `req.ip` from the
|
|
1279
|
+
* trust-proxy setting; fall back to the raw socket address when it's
|
|
1280
|
+
* unavailable.
|
|
1281
|
+
*
|
|
1282
|
+
* IPv6 addresses collapse to their /56 prefix. A single IPv6 client is
|
|
1283
|
+
* routinely handed far more than one address, so keying on the full address
|
|
1284
|
+
* would let it mint a fresh budget per request. Non-IPv6 values, including
|
|
1285
|
+
* the "unknown" fallback, pass through unchanged.
|
|
1163
1286
|
*/
|
|
1164
1287
|
getClientIpForRateLimit(req) {
|
|
1165
|
-
|
|
1288
|
+
const raw = (typeof req.ip === "string" && req.ip.length > 0
|
|
1166
1289
|
? req.ip
|
|
1167
|
-
: req.socket?.remoteAddress) ?? "unknown"
|
|
1290
|
+
: req.socket?.remoteAddress) ?? "unknown";
|
|
1291
|
+
return (0, express_rate_limit_1.ipKeyGenerator)(raw);
|
|
1168
1292
|
}
|
|
1169
1293
|
// ==================== Validation Helpers ====================
|
|
1170
1294
|
validateDbId(dbId) {
|