@90soft/parse-server-kit 2.10.1 → 2.11.0
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/CHANGELOG.md +84 -0
- package/CLAUDE.md +44 -8
- package/dist/decorators/types/cloudTypes.d.ts +12 -0
- package/dist/decorators/types/cloudTypes.d.ts.map +1 -1
- package/dist/middleware/rateLimit.d.ts +25 -6
- package/dist/middleware/rateLimit.d.ts.map +1 -1
- package/dist/middleware/rateLimit.js +59 -13
- package/dist/middleware/rateLimit.js.map +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,90 @@ All notable changes to `@90soft/parse-server-kit`.
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## [2.11.0]
|
|
12
|
+
|
|
13
|
+
### Security
|
|
14
|
+
|
|
15
|
+
- **`rateLimit` could be bypassed completely, by anyone, without an account.**
|
|
16
|
+
The bucket key was `req.headers['x-parse-session-token'] || req.ip`. That
|
|
17
|
+
header is supplied by the caller and is **not validated at this point in the
|
|
18
|
+
pipeline** — the entity-route middleware runs before Parse Server resolves the
|
|
19
|
+
session — so sending a fresh random token on every request minted a fresh
|
|
20
|
+
bucket each time.
|
|
21
|
+
|
|
22
|
+
Measured against a limit of `max: 3`: ten requests with ten different random
|
|
23
|
+
tokens all succeeded. The limiter was decorative.
|
|
24
|
+
|
|
25
|
+
The key is now the **client address** (`req.ip`, falling back to the socket
|
|
26
|
+
address). An optional `keyBy` is available for apps that can derive a
|
|
27
|
+
trustworthy identity themselves.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
@CloudFunction({ rateLimit: { windowMs: 60_000, max: 5 } }) // keyed on address
|
|
31
|
+
@CloudFunction({ rateLimit: { windowMs: 60_000, max: 5, keyBy: r => r.apiKeyId } })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Two things to know when you adopt this:**
|
|
35
|
+
|
|
36
|
+
1. **Configure Express `trust proxy` precisely** — a hop count, or the proxy's
|
|
37
|
+
address — if you run behind nginx or a load balancer. `req.ip` honours that
|
|
38
|
+
setting, so a permissive `true` lets a caller spoof the key with
|
|
39
|
+
`X-Forwarded-For`. This is the same bypass Parse Server fixed for its own
|
|
40
|
+
limiter in 9.10.1. Unset, Express ignores the header, which is safe.
|
|
41
|
+
2. **Limits were previously per session token and are now per address**, so
|
|
42
|
+
several users behind one NAT now share a bucket. Raise `max` where that
|
|
43
|
+
matters, or supply `keyBy`.
|
|
44
|
+
|
|
45
|
+
- **The bucket store is now bounded** (10,000 clients per function, oldest
|
|
46
|
+
evicted). A caller rotating addresses could previously grow the map without
|
|
47
|
+
limit, turning the rate limiter into a memory-exhaustion vector.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- `resetRateLimits()`, exported for tests.
|
|
52
|
+
- `test/rateLimit.test.ts` — eight tests, including the bypass above as a
|
|
53
|
+
regression. Rate limiting previously had **no test coverage at all**; the
|
|
54
|
+
regression test was verified to fail against the old implementation.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **`engines.node` is now `>=20.19.0`** (was `>=18.0.0`). Parse Server 9
|
|
59
|
+
requires it, so the old floor advertised a combination that cannot work.
|
|
60
|
+
- **Peer floors raised** to `parse >= 8.0.0` and `parse-server >= 9.0.0` (were
|
|
61
|
+
`>= 5.0.0` and `>= 8.3.0`). Development and tests now run against
|
|
62
|
+
`parse@8.6.2` / `parse-server@9.10.1`; previously the kit was tested against
|
|
63
|
+
Parse JS SDK **5** while every consuming app ran 8.x. That gap matters:
|
|
64
|
+
SDK 6 made `Parse.Object.set`/`setACL`/`unset` and `Role.setName` **throw**
|
|
65
|
+
instead of taking an error callback, so such a failure escapes the
|
|
66
|
+
`catchError` convention entirely.
|
|
67
|
+
|
|
68
|
+
The floors stay permissive deliberately — nothing in a consuming app breaks on
|
|
69
|
+
install. **Parse Server ≥ 9.10.1 is strongly recommended**: the 9.5→9.10 line
|
|
70
|
+
is almost entirely security fixes, including account takeover through
|
|
71
|
+
auth-adapter identity spoofing, protected-field bypasses via logical
|
|
72
|
+
operators, dot-notation, relation counts and LiveQuery, and several stored-XSS
|
|
73
|
+
routes through file upload.
|
|
74
|
+
|
|
75
|
+
### Notes
|
|
76
|
+
|
|
77
|
+
- Rate limiting remains **per process**: with N app instances the effective
|
|
78
|
+
limit is N × `max`. For cross-instance enforcement use Parse Server's own
|
|
79
|
+
`rateLimit` server option with `redisUrl`.
|
|
80
|
+
- A unique index with a **value** predicate (e.g. "one active row per pair")
|
|
81
|
+
needs `partialFilterExpression`, not `partialFilterNulls` — the latter only
|
|
82
|
+
emits `$exists: true`. Use the `options` escape hatch, which is passed
|
|
83
|
+
straight through to MongoDB:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
compoundIndexes: [{
|
|
87
|
+
fields: ['trainee', 'sport'],
|
|
88
|
+
unique: true,
|
|
89
|
+
options: { partialFilterExpression: { status: 'active' } },
|
|
90
|
+
}]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
11
95
|
## [2.10.1]
|
|
12
96
|
|
|
13
97
|
Twelve fixes found by a full read of the source, each verified against a running
|
package/CLAUDE.md
CHANGED
|
@@ -187,7 +187,7 @@ Static-method decorator. Registers the function and wraps it with role checking.
|
|
|
187
187
|
interface RouteConfig {
|
|
188
188
|
methods: HttpMethod[]; // 'GET'|'POST'|'PUT'|'DELETE'|'PATCH'
|
|
189
189
|
requiresAuth?: boolean;
|
|
190
|
-
rateLimit?: {windowMs: number; max: number};
|
|
190
|
+
rateLimit?: {windowMs: number; max: number; keyBy?: (req) => string}; // keyed on client ADDRESS
|
|
191
191
|
validation?: Parse.Cloud.Validator;
|
|
192
192
|
description?: string;
|
|
193
193
|
requireRoles?: string[];
|
|
@@ -454,11 +454,35 @@ parent's final ACL and **before** saving. For status-derived visibility set in a
|
|
|
454
454
|
| `removeResultMiddleware` | Unwraps Parse's `{result: …}` response |
|
|
455
455
|
| `conditionalJsonMiddleware` | `express.json({limit:'10mb', type:['text/plain']})`, skipping `{mountPath}/files` |
|
|
456
456
|
| `validateFunctionRoutes` | Legacy `/functions/*` validation |
|
|
457
|
-
| `checkRateLimit(req,res,name,cfg)` | In-process token bucket; returns `false` after sending 429 |
|
|
457
|
+
| `checkRateLimit(req,res,name,cfg)` | In-process token bucket keyed on client address; returns `false` after sending 429 |
|
|
458
458
|
|
|
459
459
|
**Master key** is read from the `X-Parse-Master-Key` header (preferred) or the
|
|
460
|
-
request body (deprecated, warns once).
|
|
461
|
-
|
|
460
|
+
request body (deprecated, warns once).
|
|
461
|
+
|
|
462
|
+
### Rate limiting — read before relying on it
|
|
463
|
+
|
|
464
|
+
The bucket key is the **client address** (`req.ip`, falling back to the socket
|
|
465
|
+
address). Pass `keyBy` only if the app can derive an identity it actually trusts.
|
|
466
|
+
|
|
467
|
+
> Before 2.11.0 the key was `x-parse-session-token || req.ip`. That header is
|
|
468
|
+
> caller-supplied and unvalidated here — this middleware runs **before** Parse
|
|
469
|
+
> resolves the session — so a rotating random token minted a fresh bucket per
|
|
470
|
+
> request and the limit did nothing. Ten requests passed a limit of three.
|
|
471
|
+
|
|
472
|
+
Three constraints:
|
|
473
|
+
|
|
474
|
+
1. **`trust proxy` must be exact** — a hop count or the proxy's address — when
|
|
475
|
+
behind nginx or a load balancer. `req.ip` honours it, so a permissive `true`
|
|
476
|
+
lets a caller spoof the key via `X-Forwarded-For` (the bypass Parse Server
|
|
477
|
+
fixed for its own limiter in 9.10.1). Unset is safe: Express ignores the
|
|
478
|
+
header.
|
|
479
|
+
2. **Per process.** With N instances the effective limit is N × `max`. For
|
|
480
|
+
cross-instance enforcement use Parse Server's `rateLimit` option with
|
|
481
|
+
`redisUrl`.
|
|
482
|
+
3. **Per address, not per user.** Users behind one NAT share a bucket.
|
|
483
|
+
|
|
484
|
+
The store is bounded at 10,000 clients per function, oldest evicted, so a caller
|
|
485
|
+
rotating addresses cannot grow it without limit.
|
|
462
486
|
|
|
463
487
|
---
|
|
464
488
|
|
|
@@ -488,7 +512,9 @@ interface CompoundIndex {
|
|
|
488
512
|
unique?: boolean;
|
|
489
513
|
name?: string;
|
|
490
514
|
sparse?: boolean;
|
|
491
|
-
partialFilterNulls?: boolean; // only index docs where all fields
|
|
515
|
+
partialFilterNulls?: boolean; // only index docs where all fields EXIST
|
|
516
|
+
// For a VALUE predicate (e.g. one active row per pair) use the options escape hatch:
|
|
517
|
+
// options: {partialFilterExpression: {status: 'active'}} -> passed straight to MongoDB
|
|
492
518
|
fieldTypes?: Record<string, 1|-1|'text'|'2dsphere'|'hashed'>;
|
|
493
519
|
options?: Record<string, unknown>; // e.g. default_language
|
|
494
520
|
}
|
|
@@ -688,11 +714,21 @@ output for `[Versioning]`.
|
|
|
688
714
|
|
|
689
715
|
| | Requirement |
|
|
690
716
|
|---|---|
|
|
691
|
-
| Node |
|
|
692
|
-
| parse-server | ≥
|
|
717
|
+
| Node | **≥ 20.19** |
|
|
718
|
+
| parse-server | ≥ 9.0 (optional peer); **≥ 9.10.1 strongly recommended** — see below. ≥ 9.10.0 is required on MongoDB 8.3+ (GeoPoint distance queries 500 otherwise) |
|
|
693
719
|
| MongoDB | ≥ 7.0.16 for parse-server 9; **replica set** for transactions |
|
|
694
|
-
| Parse SDK | ≥
|
|
720
|
+
| Parse SDK | ≥ 8; developed and tested on 8.6.2 |
|
|
695
721
|
| TypeScript | any, with `experimentalDecorators: true` |
|
|
696
722
|
|
|
723
|
+
**Why ≥ 9.10.1 rather than merely 9.x:** the 9.5→9.10 releases are almost
|
|
724
|
+
entirely security fixes — account takeover through auth-adapter identity
|
|
725
|
+
spoofing, protected-field bypasses via logical operators, dot-notation, relation
|
|
726
|
+
counts and LiveQuery, multiple rate-limit bypasses, and several stored-XSS routes
|
|
727
|
+
through file upload. An older 9.x is not "stable", it is unpatched.
|
|
728
|
+
|
|
729
|
+
**`@types/parse` is far behind the runtime** (3.0.x against an 8.x SDK), which is
|
|
730
|
+
why reaching newer Parse APIs sometimes needs an `as unknown as` cast. That is
|
|
731
|
+
the types package, not this kit.
|
|
732
|
+
|
|
697
733
|
**Transactions and `@ParseVersionField` are MongoDB-only.** On Postgres the
|
|
698
734
|
version field is declared but never enforced.
|
|
@@ -12,9 +12,21 @@ export interface SwaggerDocConfig {
|
|
|
12
12
|
export interface RouteConfig {
|
|
13
13
|
methods: HttpMethod[];
|
|
14
14
|
requiresAuth?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Per-function rate limit, keyed on the CLIENT ADDRESS.
|
|
17
|
+
*
|
|
18
|
+
* Enforced per process: with N app instances the effective limit is N x `max`.
|
|
19
|
+
* For cross-instance enforcement use Parse Server's own `rateLimit` server
|
|
20
|
+
* option with `redisUrl`.
|
|
21
|
+
*
|
|
22
|
+
* Requires Express `trust proxy` to be configured precisely (a hop count or
|
|
23
|
+
* the proxy's address) when running behind a reverse proxy. A permissive
|
|
24
|
+
* value lets a caller spoof the key with `X-Forwarded-For`.
|
|
25
|
+
*/
|
|
15
26
|
rateLimit?: {
|
|
16
27
|
windowMs: number;
|
|
17
28
|
max: number;
|
|
29
|
+
keyBy?: (req: any) => string;
|
|
18
30
|
};
|
|
19
31
|
validation?: Parse.Cloud.Validator;
|
|
20
32
|
description?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudTypes.d.ts","sourceRoot":"","sources":["../../../src/decorators/types/cloudTypes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"cloudTypes.d.ts","sourceRoot":"","sources":["../../../src/decorators/types/cloudTypes.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,GAAG,CAAA;KAAE,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,MAAM,CAAA;KAAE,CAAC;IAC5E,UAAU,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,KAAK,CAAC,eAAe,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACtE,2EAA2E;IAC3E,KAAK,CAAC,EAAE,GAAG,CAAC;IACZ,oCAAoC;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB"}
|
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
import { Request, Response } from 'express';
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
* Returns null if allowed, or an error response object if exceeded.
|
|
5
|
-
*/
|
|
6
|
-
export declare function checkRateLimit(req: Request, res: Response, functionName: string, config: {
|
|
2
|
+
/** Per-function configuration, as declared on `@CloudFunction({rateLimit})`. */
|
|
3
|
+
export interface RateLimitConfig {
|
|
7
4
|
windowMs: number;
|
|
8
5
|
max: number;
|
|
9
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Optional custom bucket key.
|
|
8
|
+
*
|
|
9
|
+
* This middleware runs BEFORE Parse Server validates the session, so there is
|
|
10
|
+
* no authenticated user here. Supply this only if the app can derive a
|
|
11
|
+
* trustworthy identity on its own (e.g. a validated API key). Returning
|
|
12
|
+
* anything the caller controls freely re-opens the bypass described below.
|
|
13
|
+
*/
|
|
14
|
+
keyBy?: (req: Request) => string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check the rate limit for a function + client.
|
|
18
|
+
*
|
|
19
|
+
* Returns true when the request may proceed. Returns false after sending a 429,
|
|
20
|
+
* in which case the caller must not continue the chain.
|
|
21
|
+
*
|
|
22
|
+
* NOTE — the store is in-memory and therefore PER PROCESS. With N app instances
|
|
23
|
+
* the effective limit is N x `max`. For cross-instance enforcement use Parse
|
|
24
|
+
* Server's own `rateLimit` server option with `redisUrl`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkRateLimit(req: Request, res: Response, functionName: string, config: RateLimitConfig): boolean;
|
|
27
|
+
/** Clear all buckets. Intended for tests. */
|
|
28
|
+
export declare function resetRateLimits(): void;
|
|
10
29
|
//# sourceMappingURL=rateLimit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"rateLimit.d.ts","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,SAAS,CAAC;AAO1C,gFAAgF;AAChF,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA8DD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,eAAe,GACtB,OAAO,CAwCT;AAED,6CAA6C;AAC7C,wBAAgB,eAAe,IAAI,IAAI,CAEtC"}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.checkRateLimit = checkRateLimit;
|
|
4
|
+
exports.resetRateLimits = resetRateLimits;
|
|
4
5
|
// Map<functionName, Map<clientKey, RateLimitEntry>>
|
|
5
6
|
const store = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Hard ceiling on tracked clients per function. A caller rotating addresses
|
|
9
|
+
* would otherwise grow this map without bound — turning the rate limiter into
|
|
10
|
+
* a memory-exhaustion vector. When the ceiling is hit we drop the oldest
|
|
11
|
+
* entries rather than stop limiting.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_CLIENTS_PER_FUNCTION = 10000;
|
|
6
14
|
// Cleanup old entries every 5 minutes
|
|
7
15
|
const cleanupTimer = setInterval(() => {
|
|
8
16
|
const now = Date.now();
|
|
@@ -24,34 +32,68 @@ const cleanupTimer = setInterval(() => {
|
|
|
24
32
|
// which is the only time it has anything to do.
|
|
25
33
|
cleanupTimer.unref();
|
|
26
34
|
/**
|
|
27
|
-
*
|
|
35
|
+
* Resolve the bucket key for a request.
|
|
36
|
+
*
|
|
37
|
+
* SECURITY — why this is the client address and NOT the session token:
|
|
38
|
+
*
|
|
39
|
+
* Until v2.11.0 the key was `req.headers['x-parse-session-token'] || req.ip`.
|
|
40
|
+
* That header is supplied by the caller and is NOT validated at this point in
|
|
41
|
+
* the pipeline, so sending a fresh random token on every request minted a fresh
|
|
42
|
+
* bucket each time and the limit did nothing at all. No account was needed.
|
|
43
|
+
*
|
|
44
|
+
* The address is the only identifier available here that the caller cannot
|
|
45
|
+
* multiply at will.
|
|
46
|
+
*
|
|
47
|
+
* `req.ip` honours Express's `trust proxy` setting. The app MUST configure that
|
|
48
|
+
* precisely — a hop count, or the proxy's address. With a permissive value
|
|
49
|
+
* (`true`) a caller sets `X-Forwarded-For: 127.0.0.1` and spoofs the key, which
|
|
50
|
+
* is the same bypass Parse Server fixed in 9.10.1 for its own limiter. When
|
|
51
|
+
* `trust proxy` is unset, Express already ignores the header and `req.ip` is
|
|
52
|
+
* the socket address, which is what we want.
|
|
28
53
|
*/
|
|
29
|
-
function getClientKey(req) {
|
|
30
|
-
|
|
54
|
+
function getClientKey(req, config) {
|
|
55
|
+
if (config.keyBy) {
|
|
56
|
+
return config.keyBy(req);
|
|
57
|
+
}
|
|
58
|
+
return req.ip || req.socket?.remoteAddress || 'unknown';
|
|
31
59
|
}
|
|
32
60
|
/**
|
|
33
|
-
* Check rate limit for a function + client.
|
|
34
|
-
*
|
|
61
|
+
* Check the rate limit for a function + client.
|
|
62
|
+
*
|
|
63
|
+
* Returns true when the request may proceed. Returns false after sending a 429,
|
|
64
|
+
* in which case the caller must not continue the chain.
|
|
65
|
+
*
|
|
66
|
+
* NOTE — the store is in-memory and therefore PER PROCESS. With N app instances
|
|
67
|
+
* the effective limit is N x `max`. For cross-instance enforcement use Parse
|
|
68
|
+
* Server's own `rateLimit` server option with `redisUrl`.
|
|
35
69
|
*/
|
|
36
70
|
function checkRateLimit(req, res, functionName, config) {
|
|
37
|
-
const clientKey = getClientKey(req);
|
|
71
|
+
const clientKey = getClientKey(req, config);
|
|
38
72
|
const now = Date.now();
|
|
39
|
-
// Get or create function map
|
|
40
73
|
if (!store.has(functionName)) {
|
|
41
74
|
store.set(functionName, new Map());
|
|
42
75
|
}
|
|
43
76
|
const clients = store.get(functionName);
|
|
44
|
-
// Get or create client entry
|
|
45
77
|
const entry = clients.get(clientKey);
|
|
46
78
|
if (!entry || now > entry.resetAt) {
|
|
47
|
-
// First request
|
|
79
|
+
// First request in this window — start fresh.
|
|
80
|
+
if (!entry && clients.size >= MAX_CLIENTS_PER_FUNCTION) {
|
|
81
|
+
// Evict the oldest-expiring entries to make room. Map preserves insertion
|
|
82
|
+
// order, so the head is the least recently added.
|
|
83
|
+
const toEvict = Math.ceil(MAX_CLIENTS_PER_FUNCTION * 0.1);
|
|
84
|
+
let evicted = 0;
|
|
85
|
+
for (const key of clients.keys()) {
|
|
86
|
+
clients.delete(key);
|
|
87
|
+
if (++evicted >= toEvict)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
48
91
|
clients.set(clientKey, { count: 1, resetAt: now + config.windowMs });
|
|
49
|
-
return true;
|
|
92
|
+
return true;
|
|
50
93
|
}
|
|
51
94
|
if (entry.count < config.max) {
|
|
52
|
-
// Under limit
|
|
53
95
|
entry.count++;
|
|
54
|
-
return true;
|
|
96
|
+
return true;
|
|
55
97
|
}
|
|
56
98
|
// Exceeded — send 429
|
|
57
99
|
const retryAfter = Math.ceil((entry.resetAt - now) / 1000);
|
|
@@ -60,6 +102,10 @@ function checkRateLimit(req, res, functionName, config) {
|
|
|
60
102
|
message: `Too many requests. Try again in ${retryAfter} seconds.`,
|
|
61
103
|
retryAfter,
|
|
62
104
|
});
|
|
63
|
-
return false;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
/** Clear all buckets. Intended for tests. */
|
|
108
|
+
function resetRateLimits() {
|
|
109
|
+
store.clear();
|
|
64
110
|
}
|
|
65
111
|
//# sourceMappingURL=rateLimit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"rateLimit.js","sourceRoot":"","sources":["../../src/middleware/rateLimit.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@90soft/parse-server-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Complete decorator-driven development kit for Parse Server — models, cloud functions, triggers, cron, routing, ACL, Swagger, and more.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"url": "https://git.90-soft.com/90_soft/parse-server-kit.git"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
37
|
+
"node": ">=20.19.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@types/parse": ">=3.0.0",
|
|
41
41
|
"express": ">=4.0.0",
|
|
42
42
|
"node-cron": ">=3.0.0",
|
|
43
|
-
"parse": ">=
|
|
44
|
-
"parse-server": ">=
|
|
43
|
+
"parse": ">=8.0.0",
|
|
44
|
+
"parse-server": ">=9.0.0",
|
|
45
45
|
"reflect-metadata": ">=0.1.13",
|
|
46
46
|
"swagger-ui-express": ">=4.0.0"
|
|
47
47
|
},
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"jest": "^29.7.0",
|
|
68
68
|
"mongodb-memory-server": "^11.2.0",
|
|
69
69
|
"node-cron": "^4.0.0",
|
|
70
|
-
"parse": "^
|
|
71
|
-
"parse-server": "^9.10.
|
|
70
|
+
"parse": "^8.6.2",
|
|
71
|
+
"parse-server": "^9.10.1",
|
|
72
72
|
"reflect-metadata": "^0.2.2",
|
|
73
73
|
"swagger-ui-express": "^5.0.1",
|
|
74
74
|
"ts-jest": "^29.2.0",
|