@rapidrest/service-core 1.5.0 → 1.7.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/README.md +2 -0
- package/dist/lib/RateLimiter.js +174 -0
- package/dist/lib/RateLimiter.js.map +1 -0
- package/dist/lib/decorators/RouteDecorators.js +21 -0
- package/dist/lib/decorators/RouteDecorators.js.map +1 -1
- package/dist/lib/http/bun/BunRouter.js +14 -3
- package/dist/lib/http/bun/BunRouter.js.map +1 -1
- package/dist/lib/http/uWS/Router.js +13 -2
- package/dist/lib/http/uWS/Router.js.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/models/ModelUtils.js +12 -3
- package/dist/lib/models/ModelUtils.js.map +1 -1
- package/dist/lib/models/RepoUtils.js +20 -4
- package/dist/lib/models/RepoUtils.js.map +1 -1
- package/dist/lib/routes/RouteUtils.js +28 -9
- package/dist/lib/routes/RouteUtils.js.map +1 -1
- package/dist/lib/security/ACLUtils.js +51 -12
- package/dist/lib/security/ACLUtils.js.map +1 -1
- package/dist/types/RateLimiter.d.ts +87 -0
- package/dist/types/decorators/RouteDecorators.d.ts +10 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/routes/RouteUtils.d.ts +7 -0
- package/dist/types/security/ACLUtils.d.ts +29 -5
- package/package.json +24 -22
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { HttpRequest } from "./http/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The event type recorded via `@rapidrest/core`'s `EventUtils.record()` when a rate limit is exceeded (either
|
|
4
|
+
* the per-identifier or the per-source-IP layer - see `layer` on the event). A brute-force/abuse signal
|
|
5
|
+
* covering every rate-limited route from one call site, rather than instrumenting each caller's individual
|
|
6
|
+
* failure branches.
|
|
7
|
+
*/
|
|
8
|
+
export declare const RATELIMIT_EXCEEDED_EVENT = "ratelimit.exceeded";
|
|
9
|
+
/**
|
|
10
|
+
* Configuration options for the source-IP counter layered alongside the primary, identifier-keyed one. An
|
|
11
|
+
* identifier (username/email/etc.) alone can't catch an attacker who rotates identifiers against a single
|
|
12
|
+
* source, and is itself something an attacker and a victim can share (e.g. a common username) - keying a
|
|
13
|
+
* second, independent counter on the caller's IP address closes that gap without weakening the existing
|
|
14
|
+
* per-identifier throttle. Deliberately more permissive by default than the per-identifier limit, since a
|
|
15
|
+
* single IP can legitimately represent many users behind NAT/a corporate proxy.
|
|
16
|
+
*/
|
|
17
|
+
export interface IPRateLimiterConfig {
|
|
18
|
+
/** Set to `false` to disable the per-IP counter. Default is `true`. */
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
/** The maximum number of attempts allowed from a single IP within `windowSeconds`. Default is `100`. */
|
|
21
|
+
maxAttempts?: number;
|
|
22
|
+
/** The length of the window, in seconds, that `maxAttempts` applies to. Default is `300` (5 minutes). */
|
|
23
|
+
windowSeconds?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Configuration options for `RateLimiter`, read from the `rateLimit` path of the application configuration.
|
|
27
|
+
*/
|
|
28
|
+
export interface RateLimiterConfig {
|
|
29
|
+
/** Set to `false` to disable rate limiting entirely. Default is `true`. */
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
/** The maximum number of attempts allowed within `windowSeconds` before being rejected. Default is `5`. */
|
|
32
|
+
maxAttempts?: number;
|
|
33
|
+
/** The length of the sliding window, in seconds, that `maxAttempts` applies to. Default is `300` (5 minutes). */
|
|
34
|
+
windowSeconds?: number;
|
|
35
|
+
/** Configuration for the additional, independent per-source-IP counter. */
|
|
36
|
+
ip?: IPRateLimiterConfig;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A simple attempt-count rate limiter used to defend expensive or guessable endpoints (credential
|
|
40
|
+
* verification, one-time-code redemption, enumeration-prone lookups, etc.) against brute-force/abuse
|
|
41
|
+
* attacks. Backed by Redis when a `cache` connection is configured (so the count is shared across server
|
|
42
|
+
* instances), and falls back to an in-process in-memory counter otherwise.
|
|
43
|
+
*
|
|
44
|
+
* @author Jean-Philippe Steinmetz
|
|
45
|
+
*/
|
|
46
|
+
export declare class RateLimiter {
|
|
47
|
+
protected config: RateLimiterConfig;
|
|
48
|
+
protected trustedProxies: string[];
|
|
49
|
+
private connMgr?;
|
|
50
|
+
/** In-memory fallback store, used both when no `cache` connection is configured and when the connected
|
|
51
|
+
* Redis server is too old to support `INCREX` (see `redisIncrexUnsupported` below). */
|
|
52
|
+
private readonly memoryStore;
|
|
53
|
+
protected logger: any;
|
|
54
|
+
/** Set once `incrementRedis()` learns the connected server rejects `INCREX` as unknown (`INCREX` is a
|
|
55
|
+
* Redis 8.8+ command - not yet supported by Redis Software, Redis Cloud, or any Redis-protocol-compatible
|
|
56
|
+
* server that hasn't caught up, e.g. Memurai on Windows) - subsequent calls skip straight to the in-memory
|
|
57
|
+
* fallback instead of paying for (and logging) a failed round trip on every single request. This does mean
|
|
58
|
+
* the per-identifier/per-IP counters stop being atomic across multiple server instances sharing one Redis
|
|
59
|
+
* for as long as this process runs, which is a real, deliberate degradation - see the class doc comment -
|
|
60
|
+
* but a working, non-atomic rate limiter is better than every rate-limited request 500ing.
|
|
61
|
+
*/
|
|
62
|
+
private redisIncrexUnsupported;
|
|
63
|
+
private get cacheClient();
|
|
64
|
+
/**
|
|
65
|
+
* Records an attempt for the given identifier and throws once it has exceeded the configured
|
|
66
|
+
* `maxAttempts` within `windowSeconds`. A no-op when rate limiting is disabled via config.
|
|
67
|
+
*
|
|
68
|
+
* When `req` is supplied, an independent, more permissive counter keyed on the caller's source IP is
|
|
69
|
+
* also checked and incremented (see `IPRateLimiterConfig`) - this catches an attacker who rotates
|
|
70
|
+
* identifiers against a single source, which the identifier-keyed counter alone cannot.
|
|
71
|
+
* @param identifier A value that scopes the counter to a particular caller/target (e.g. a claimed username
|
|
72
|
+
* or email). Callers should be aware that an identifier alone can be shared by an attacker and a victim
|
|
73
|
+
* (e.g. a username), so this limits attempts against that identifier globally rather than per-source.
|
|
74
|
+
* @param req The source HTTP request, used to derive the caller's IP for the additional per-IP counter.
|
|
75
|
+
* Omit to check only the identifier-keyed counter (e.g. when no request is available).
|
|
76
|
+
*/
|
|
77
|
+
checkAndIncrement(identifier: string, req?: HttpRequest): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Increments the counter for `key` and throws once it exceeds `maxAttempts` within `windowSeconds`.
|
|
80
|
+
* Shared by the identifier-keyed and IP-keyed counters in `checkAndIncrement()` - the two are otherwise
|
|
81
|
+
* entirely independent (different keys, different limits), this just avoids duplicating the
|
|
82
|
+
* increment-then-compare logic between them.
|
|
83
|
+
*/
|
|
84
|
+
private enforceLimit;
|
|
85
|
+
private incrementRedis;
|
|
86
|
+
private incrementMemory;
|
|
87
|
+
}
|
|
@@ -135,6 +135,16 @@ export declare function Protect(acl?: PartialACL): (target: any, propertyKey?: s
|
|
|
135
135
|
* @param name THe name of the query parameter whose value will be injected.
|
|
136
136
|
*/
|
|
137
137
|
export declare function Query(name?: string | undefined): (target: any, propertyKey: string, index: number) => void;
|
|
138
|
+
/**
|
|
139
|
+
* Indicates that the endpoint should have rate limiting applied for all incoming requests. This can be applied
|
|
140
|
+
* at the endpoint function level or the route class level. When applying at the class level, all defined
|
|
141
|
+
* endpoints in the class will have rate limiting applied.
|
|
142
|
+
*
|
|
143
|
+
* When performing rate limiting, the method and path of the request (without the query portion) is used as the identifier
|
|
144
|
+
* when calling `RateLimiter.checkAndIncrement()` such that a `GET /path/to/my/route` request has the effect of calling
|
|
145
|
+
* `RateLimiter.checkAndIncrement('GET /path/to/my/route', req)` directly.
|
|
146
|
+
*/
|
|
147
|
+
export declare function RateLimit(): (target: any, propertyKey?: string, descriptor?: PropertyDescriptor) => void;
|
|
138
148
|
/**
|
|
139
149
|
* Injects the HTTP request object as the value of the decorated argument.
|
|
140
150
|
*/
|
package/dist/types/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from "./NetUtils.js";
|
|
|
7
7
|
export * from "./NotificationUtils.js";
|
|
8
8
|
export * from "./ObjectFactory.js";
|
|
9
9
|
export * from "./OpenApiSpec.js";
|
|
10
|
+
export * from "./RateLimiter.js";
|
|
10
11
|
export * from "./Server.js";
|
|
11
12
|
export * from "./Types.js";
|
|
12
13
|
export * from "./auth/index.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RequestHandler } from "../http/types.js";
|
|
2
|
+
import { RateLimiter } from "../RateLimiter.js";
|
|
2
3
|
/**
|
|
3
4
|
* Provides a set of utilities for converting Route classes to HTTP middleware.
|
|
4
5
|
*
|
|
@@ -9,12 +10,18 @@ export declare class RouteUtils {
|
|
|
9
10
|
private apiSpec;
|
|
10
11
|
private authMiddleware?;
|
|
11
12
|
private logger?;
|
|
13
|
+
protected rateLimiter?: RateLimiter;
|
|
12
14
|
protected trustedRoles: string[];
|
|
13
15
|
/**
|
|
14
16
|
* Creates a middleware function that checks if the user has elevated privileges and if not
|
|
15
17
|
* throws the `AUTH_REQUIRES_ELEVATION` error.
|
|
16
18
|
*/
|
|
17
19
|
checkElevation(lastStart?: number): RequestHandler;
|
|
20
|
+
/**
|
|
21
|
+
* Creates a middleware function that performs rate limiting on the request where the identifier
|
|
22
|
+
* used to identify the resource is the request `<method> <path>`.
|
|
23
|
+
*/
|
|
24
|
+
checkRateLimiter(): RequestHandler;
|
|
18
25
|
/**
|
|
19
26
|
* Creates a middleware function that verifies the incoming request is from a valid user with at least
|
|
20
27
|
* one of the specified roles.
|
|
@@ -153,17 +153,41 @@ export declare class ACLUtils {
|
|
|
153
153
|
*/
|
|
154
154
|
saveACLs(acls: AccessControlList[]): Promise<void>;
|
|
155
155
|
/**
|
|
156
|
-
* Retrieves the most specific record in the provided ACL associated with the provided user
|
|
157
|
-
* uid/anonymous match beats a role match, which beats a wildcard (`.*`/`*`) match
|
|
156
|
+
* Retrieves the most specific record in the provided ACL associated with the provided user. An exact
|
|
157
|
+
* uid/anonymous match beats a role match, which beats a wildcard (`.*`/`*`) match, regardless of where
|
|
158
158
|
* each record sits in `acl.records`. Without this, a wildcard grant authored before a specific record
|
|
159
|
-
* (a natural authoring order) would silently shadow that more specific record.
|
|
160
|
-
*
|
|
159
|
+
* (a natural authoring order) would silently shadow that more specific record. If no record can be found
|
|
160
|
+
* in the ACL provided, searches each parent for a matching record.
|
|
161
|
+
*
|
|
162
|
+
* The depth of the search can be limited by setting the `options.maxDepth` option to a non-negative value. A value
|
|
163
|
+
* of `0` only searches the provided ACL, a value of `1` searches the provided ACL and its direct parent, a value of
|
|
164
|
+
* `2` searches the ACL, it's parent and grand-parent, and so on. A value of `-1` (the default) means no limit;
|
|
165
|
+
* the entire parent chain is searched.
|
|
166
|
+
*
|
|
167
|
+
* Since records support regex patterns, it is also possible to cap the match quality (**specificity**) via the
|
|
168
|
+
* `options.specificity` option. This is a ceiling, not an exact requirement: setting `options.specificity` to
|
|
169
|
+
* `exact` only matches the user's UID exactly. A value of `role` matches the UID or a user's role (preferring
|
|
170
|
+
* the UID match). A value of `wildcard` matches the UID, role, or any regex pattern (e.g. `.*` or `user*`),
|
|
171
|
+
* preferring the most specific of those that match.
|
|
172
|
+
*
|
|
173
|
+
* The default options are:
|
|
174
|
+
* ```ts
|
|
175
|
+
* {
|
|
176
|
+
* maxDepth: -1 // no limit,
|
|
177
|
+
* specificity: "wildcard"
|
|
178
|
+
* }
|
|
179
|
+
* ```
|
|
161
180
|
*
|
|
162
181
|
* @param acl The access control list that will be searched.
|
|
163
182
|
* @param user The user to find a record for.
|
|
183
|
+
* @param options The set of options to consider
|
|
164
184
|
* @returns The ACL record associated with the given user if found, otherwise `undefined`.
|
|
165
185
|
*/
|
|
166
|
-
getRecord(acl: AccessControlList, user: JWTUser | undefined
|
|
186
|
+
getRecord(acl: AccessControlList, user: JWTUser | undefined, options?: {
|
|
187
|
+
specificity?: "exact" | "role" | "wildcard";
|
|
188
|
+
maxDepth?: number;
|
|
189
|
+
curDepth?: number;
|
|
190
|
+
}): ACLRecord | null;
|
|
167
191
|
/**
|
|
168
192
|
* Attempts to retrieve the parent access control list for the given ACL object.
|
|
169
193
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rapidrest/service-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Provides all core functionality for RapidREST based backend services.",
|
|
5
5
|
"repository": "https://github.com/rapidrest/service-core.git",
|
|
6
6
|
"author": "RapidREST <rapidrests@gmail.com>",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:prod": "yarn lint && vitest run --coverage",
|
|
23
23
|
"test:watch": "vitest",
|
|
24
|
-
"release": "
|
|
24
|
+
"release": "rapidrest release"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"axios": "^1.
|
|
27
|
+
"axios": "^1.20.0",
|
|
28
28
|
"dayjs": "^1.11.23",
|
|
29
29
|
"deepmerge": "^4.3.1",
|
|
30
30
|
"node-schedule": "^2.1.1",
|
|
@@ -58,30 +58,31 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@rapidrest/
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
61
|
+
"@rapidrest/cli": "^1.0.1",
|
|
62
|
+
"@rapidrest/core": "^5.2.0",
|
|
63
|
+
"@simplewebauthn/server": "^14.0.1",
|
|
64
|
+
"@swc/core": "^1.16.2",
|
|
64
65
|
"@types/better-sqlite3": "^9.6.0",
|
|
65
|
-
"@types/bun": "^1.4.
|
|
66
|
-
"@types/node": "^26.
|
|
66
|
+
"@types/bun": "^1.4.2",
|
|
67
|
+
"@types/node": "^26.5.0",
|
|
67
68
|
"@types/node-schedule": "^2.1.8",
|
|
68
69
|
"@types/rimraf": "^4.0.5",
|
|
69
|
-
"@types/semver": "^7",
|
|
70
|
+
"@types/semver": "^7.8.0",
|
|
70
71
|
"@types/speakeasy": "^2.0.10",
|
|
71
72
|
"@types/uuid": "^11.0.0",
|
|
72
73
|
"@types/ws": "^8.18.1",
|
|
73
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
74
|
-
"@typescript-eslint/parser": "^8.
|
|
75
|
-
"@vitest/coverage-v8": "^
|
|
74
|
+
"@typescript-eslint/eslint-plugin": "^8.70.0",
|
|
75
|
+
"@typescript-eslint/parser": "^8.70.0",
|
|
76
|
+
"@vitest/coverage-v8": "^5.0.0",
|
|
76
77
|
"autocannon": "^8.0.0",
|
|
77
|
-
"better-sqlite3": "^
|
|
78
|
-
"eslint": "^10.
|
|
78
|
+
"better-sqlite3": "^13.0.3",
|
|
79
|
+
"eslint": "^10.10.0",
|
|
79
80
|
"eslint-config-prettier": "^10.1.8",
|
|
80
81
|
"eslint-plugin-import": "^2.32.0",
|
|
81
|
-
"eslint-plugin-jsdoc": "^64.
|
|
82
|
-
"globals": "^17.
|
|
82
|
+
"eslint-plugin-jsdoc": "^64.3.6",
|
|
83
|
+
"globals": "^17.12.0",
|
|
83
84
|
"lodash-es": "^4.18.1",
|
|
84
|
-
"mongodb": "^7.
|
|
85
|
+
"mongodb": "^7.6.0",
|
|
85
86
|
"mongodb-memory-server": "^11.2.0",
|
|
86
87
|
"nconf": "^0.13.0",
|
|
87
88
|
"nock": "^14.0.17",
|
|
@@ -93,14 +94,15 @@
|
|
|
93
94
|
"rimraf": "^6.1.3",
|
|
94
95
|
"semver": "^7.8.5",
|
|
95
96
|
"ts-node": "^10.9.2",
|
|
96
|
-
"tsx": "^4.23.
|
|
97
|
+
"tsx": "^4.23.13",
|
|
97
98
|
"typedoc": "^0.28.20",
|
|
98
|
-
"typedoc-plugin-markdown": "^4.
|
|
99
|
-
"typeorm": "^1.1.
|
|
99
|
+
"typedoc-plugin-markdown": "^4.13.0",
|
|
100
|
+
"typeorm": "^1.1.1",
|
|
100
101
|
"typescript": "^6.0.3",
|
|
101
|
-
"unplugin-swc": "^1.
|
|
102
|
+
"unplugin-swc": "^1.6.0",
|
|
102
103
|
"uuid": "^14.0.2",
|
|
103
|
-
"
|
|
104
|
+
"vite": "^8.2.2",
|
|
105
|
+
"vitest": "^5.0.0",
|
|
104
106
|
"winston": "^3.19.0",
|
|
105
107
|
"ws": "^8.21.3"
|
|
106
108
|
},
|