@harshror77/rate-limiter-sdk 1.0.0 → 1.0.2
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/package.json +1 -1
- package/src/RateLimiterClient.js +27 -10
- package/src/RateLimiterMiddleware.js +43 -6
package/package.json
CHANGED
package/src/RateLimiterClient.js
CHANGED
|
@@ -1,23 +1,40 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
let autoProjectName = 'Unknown Project';
|
|
6
|
+
try {
|
|
7
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
8
|
+
if (fs.existsSync(pkgPath)) {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
10
|
+
if (pkg.name) {
|
|
11
|
+
autoProjectName = pkg.name;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class RateLimiterClient {
|
|
18
|
+
constructor({ serviceUrl, apiKey, timeout = 3000 }) {
|
|
5
19
|
this.serviceUrl = serviceUrl;
|
|
6
20
|
this.apiKey = apiKey;
|
|
7
21
|
this.http = axios.create({
|
|
8
|
-
baseURL:serviceUrl,
|
|
22
|
+
baseURL: serviceUrl,
|
|
9
23
|
timeout
|
|
10
24
|
});
|
|
11
25
|
}
|
|
12
26
|
|
|
13
|
-
async check(request={}){
|
|
14
|
-
try{
|
|
15
|
-
const response = await this.http.post('/api/check', request,{
|
|
16
|
-
headers:{
|
|
17
|
-
|
|
27
|
+
async check(request = {}) {
|
|
28
|
+
try {
|
|
29
|
+
const response = await this.http.post('/api/check', request, {
|
|
30
|
+
headers: {
|
|
31
|
+
'x-api-key': this.apiKey,
|
|
32
|
+
'x-client-name': autoProjectName
|
|
33
|
+
},
|
|
34
|
+
});
|
|
18
35
|
return response.data;
|
|
19
|
-
}catch(err){
|
|
20
|
-
if(err.response){
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err.response) {
|
|
21
38
|
return err.response.data;
|
|
22
39
|
}
|
|
23
40
|
throw err;
|
|
@@ -1,29 +1,66 @@
|
|
|
1
1
|
import { RateLimiterClient } from './RateLimiterClient.js';
|
|
2
2
|
|
|
3
|
-
export function RateLimiterMiddleware({ serviceUrl, apiKey, timeout }) {
|
|
3
|
+
export function RateLimiterMiddleware({ serviceUrl, apiKey, timeout, onLimitExceeded } = {}) {
|
|
4
4
|
const client = new RateLimiterClient({ serviceUrl, apiKey, timeout });
|
|
5
5
|
|
|
6
6
|
return async function (req, res, next) {
|
|
7
7
|
try {
|
|
8
8
|
const result = await client.check({
|
|
9
9
|
ip: req.ip,
|
|
10
|
-
userId: req.userId,
|
|
10
|
+
userId: req.user?.id || req.userId,
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
+
res.setHeader('X-RateLimit-Remaining', result.remaining ?? 0);
|
|
14
|
+
res.setHeader('X-RateLimit-Reset', result.resetAt ?? 0);
|
|
15
|
+
res.setHeader('X-RateLimit-Denied-By', result.deniedBy || '');
|
|
16
|
+
|
|
13
17
|
if (!result.allowed) {
|
|
14
|
-
|
|
18
|
+
const resetTime = result.resetAt
|
|
19
|
+
? new Date(result.resetAt).toISOString()
|
|
20
|
+
: null;
|
|
21
|
+
|
|
22
|
+
const retryAfterSeconds = result.resetAt
|
|
23
|
+
? Math.ceil((result.resetAt - Date.now()) / 1000)
|
|
24
|
+
: 60;
|
|
25
|
+
|
|
26
|
+
res.setHeader('Retry-After', retryAfterSeconds);
|
|
27
|
+
|
|
28
|
+
if (onLimitExceeded) {
|
|
29
|
+
return onLimitExceeded(req, res, result);
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
return res.status(429).json({
|
|
16
|
-
error: '
|
|
33
|
+
error: 'Too Many Requests',
|
|
34
|
+
message: buildMessage(result.deniedBy, retryAfterSeconds),
|
|
17
35
|
deniedBy: result.deniedBy,
|
|
18
|
-
remaining: result.remaining,
|
|
36
|
+
remaining: result.remaining ?? 0,
|
|
19
37
|
resetAt: result.resetAt,
|
|
38
|
+
retryAfter: `${retryAfterSeconds} seconds`,
|
|
39
|
+
retryAfterISO: resetTime,
|
|
20
40
|
});
|
|
21
41
|
}
|
|
22
42
|
|
|
23
|
-
res.setHeader('X-RateLimit-Remaining', result.remaining ?? 0);
|
|
24
43
|
next();
|
|
25
44
|
} catch (err) {
|
|
45
|
+
console.warn('[RateShield] Service unreachable, failing open:', err.message);
|
|
26
46
|
next();
|
|
27
47
|
}
|
|
28
48
|
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildMessage(deniedBy, retryAfterSeconds) {
|
|
52
|
+
const retry = retryAfterSeconds > 60
|
|
53
|
+
? `${Math.ceil(retryAfterSeconds / 60)} minute(s)`
|
|
54
|
+
: `${retryAfterSeconds} second(s)`;
|
|
55
|
+
|
|
56
|
+
switch (deniedBy) {
|
|
57
|
+
case 'plan':
|
|
58
|
+
return `You have exceeded your plan's request limit. Please try again in ${retry}.`;
|
|
59
|
+
case 'ip':
|
|
60
|
+
return `Too many requests from your IP address. Please try again in ${retry}.`;
|
|
61
|
+
case 'user':
|
|
62
|
+
return `You are sending requests too quickly. Please slow down and try again in ${retry}.`;
|
|
63
|
+
default:
|
|
64
|
+
return `Rate limit exceeded. Please try again in ${retry}.`;
|
|
65
|
+
}
|
|
29
66
|
}
|