@lidofinance/next-ip-rate-limit 0.10.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 ADDED
@@ -0,0 +1,46 @@
1
+ # @lidofinance/next-ip-rate-limit
2
+
3
+ IP rate limit for next server api requests.
4
+
5
+ ## Installation
6
+
7
+ `yarn add @lidofinance/next-ip-rate-limit`.
8
+
9
+ ## Getting started
10
+
11
+ ### With Next.js API
12
+
13
+ ```ts
14
+ import { ipRateLimit } from '@lidofinance/next-ip-rate-limit'
15
+ import { NextApiRequest, NextApiResponse } from 'next'
16
+
17
+ // env vars
18
+ const RATE_LIMIT = 60
19
+ const RATE_LIMIT_TIME_FRAME = 60 // 1 min
20
+
21
+ const someRequest = async (req, res) => {
22
+ ipRateLimit({
23
+ req,
24
+ res,
25
+ limit: RATE_LIMIT,
26
+ timeFrame: RATE_LIMIT_TIME_FRAME,
27
+ })
28
+
29
+ await fetch()
30
+ }
31
+
32
+ export default someRequest
33
+ ```
34
+
35
+ ### With @lidofinance/next-api-wrapper
36
+
37
+ ```ts
38
+ import { wrapRequest, API } from '@lidofinance/next-api-wrapper'
39
+ import { ipRateLimit, rateLimitWrapper } from '@lidofinance/next-ip-rate-limit'
40
+
41
+ const someRequest: API = async (req, res) => await fetch()
42
+
43
+ export const rateLimit = rateLimitWrapper({ rateLimit: RATE_LIMIT, rateLimitTimeFrame: RATE_LIMIT_TIME_FRAME })
44
+
45
+ export default wrapRequest([rateLimit, someWrapper])(someRequest)
46
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,129 @@
1
+ function $parcel$exportWildcard(dest, source) {
2
+ Object.keys(source).forEach(function(key) {
3
+ if (key === 'default' || key === '__esModule' || dest.hasOwnProperty(key)) {
4
+ return;
5
+ }
6
+
7
+ Object.defineProperty(dest, key, {
8
+ enumerable: true,
9
+ get: function get() {
10
+ return source[key];
11
+ }
12
+ });
13
+ });
14
+
15
+ return dest;
16
+ }
17
+ function $parcel$export(e, n, v, s) {
18
+ Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
19
+ }
20
+ var $abe9c276dfd79488$exports = {};
21
+
22
+ $parcel$export($abe9c276dfd79488$exports, "ipRateLimit", () => $abe9c276dfd79488$export$122bebc9e141287c);
23
+ class $9e3ba9e2f0393d79$export$230ecc6ad0ef6558 {
24
+ timeoutIds = [];
25
+ constructor(){
26
+ this._storage = new Map();
27
+ }
28
+ set(key, value, durationSec) {
29
+ this._storage.set(key, {
30
+ value: value
31
+ });
32
+ if (this._storage.get(key)?.timeoutId) clearTimeout(this._storage.get(key)?.timeoutId);
33
+ // code based on the work with setTimeouts from the code from the nestjs throttling service
34
+ // https://github.com/nestjs/throttler/blob/master/src/throttler.service.ts#L25
35
+ const timeoutId = setTimeout(()=>{
36
+ clearTimeout(timeoutId);
37
+ this.timeoutIds = this.timeoutIds.filter((id)=>id != timeoutId);
38
+ this.delete(key);
39
+ }, durationSec * 1000);
40
+ this.timeoutIds.push(timeoutId);
41
+ }
42
+ get(key) {
43
+ return this._storage.get(key)?.value;
44
+ }
45
+ inc(key, durationSec) {
46
+ if (this._storage.has(key)) this.set(key, (this._storage.get(key)?.value || 0) + 1, durationSec);
47
+ else this.set(key, 1, durationSec);
48
+ return this.get(key);
49
+ }
50
+ delete(key) {
51
+ if (this._storage.has(key)) this._storage.delete(key);
52
+ }
53
+ }
54
+
55
+
56
+ var $54a02752d7f6015e$exports = {};
57
+
58
+ $parcel$export($54a02752d7f6015e$exports, "RATE_LIMIT_HEADERS", () => $54a02752d7f6015e$export$a063967512de3ebf);
59
+ $parcel$export($54a02752d7f6015e$exports, "ERROR_MESSAGE", () => $54a02752d7f6015e$export$3f27bc10068ef877);
60
+ const $54a02752d7f6015e$export$a063967512de3ebf = {
61
+ limit: `X-RateLimit-Limit`,
62
+ remaining: `X-RateLimit-Remaining`,
63
+ reset: `X-RateLimit-Reset`
64
+ };
65
+ const $54a02752d7f6015e$export$3f27bc10068ef877 = "API rate limit exceeded for";
66
+
67
+
68
+ const $c63185ef25ccfd28$export$67218453e01a0be8 = (req)=>{
69
+ const cfConnectionIp = req.headers["cf_connecting_ip"];
70
+ const xff = req.headers["x-forwarded-for"];
71
+ if (cfConnectionIp) return Array.isArray(cfConnectionIp) ? cfConnectionIp[0] : cfConnectionIp.split(",")[0];
72
+ return xff ? Array.isArray(xff) ? xff[0] : xff.split(",")[0] : "127.0.0.1";
73
+ };
74
+ const $c63185ef25ccfd28$export$d0a20d44a54350ab = ({ id: id , res: res , req: req })=>{
75
+ res.status(429);
76
+ throw new Error(`${(0, $54a02752d7f6015e$export$3f27bc10068ef877)} ${id} ${req.url || ""}`);
77
+ };
78
+
79
+
80
+
81
+ let $abe9c276dfd79488$var$rateLimitStorage;
82
+ const $abe9c276dfd79488$export$122bebc9e141287c = (data)=>{
83
+ if (!$abe9c276dfd79488$var$rateLimitStorage) $abe9c276dfd79488$var$rateLimitStorage = new (0, $9e3ba9e2f0393d79$export$230ecc6ad0ef6558)();
84
+ const { res: res , req: req , limit: limit , timeFrame: timeFrame } = data;
85
+ const headers = (0, $54a02752d7f6015e$export$a063967512de3ebf);
86
+ const id = `ip:${(0, $c63185ef25ccfd28$export$67218453e01a0be8)(req)}`;
87
+ const time = Math.floor(Date.now() / 1000 / timeFrame);
88
+ const key = `${id}:${time}`;
89
+ const callCount = $abe9c276dfd79488$var$rateLimitStorage.inc(key, data.timeFrame);
90
+ const remaining = limit - (callCount || 0);
91
+ const reset = (time + 1) * timeFrame;
92
+ res.setHeader(headers.limit, `${limit}`);
93
+ res.setHeader(headers.remaining, `${remaining < 0 ? 0 : remaining}`);
94
+ res.setHeader(headers.reset, `${reset}`);
95
+ if (remaining < 0) (0, $c63185ef25ccfd28$export$d0a20d44a54350ab)({
96
+ id: id,
97
+ res: res,
98
+ req: req
99
+ });
100
+ return res;
101
+ };
102
+
103
+
104
+ var $bad81bc593a4351d$exports = {};
105
+
106
+ $parcel$export($bad81bc593a4351d$exports, "rateLimitWrapper", () => $bad81bc593a4351d$export$1e3e762f475f853);
107
+
108
+ const $bad81bc593a4351d$export$1e3e762f475f853 = ({ rateLimit: rateLimit , rateLimitTimeFrame: rateLimitTimeFrame })=>async (req, res, next)=>{
109
+ (0, $abe9c276dfd79488$export$122bebc9e141287c)({
110
+ req: req,
111
+ res: res,
112
+ limit: rateLimit,
113
+ timeFrame: rateLimitTimeFrame
114
+ });
115
+ await next?.(req, res, next);
116
+ };
117
+
118
+
119
+
120
+ var $3cbdb2b44a8a698d$exports = {};
121
+
122
+
123
+ $parcel$exportWildcard(module.exports, $abe9c276dfd79488$exports);
124
+ $parcel$exportWildcard(module.exports, $bad81bc593a4351d$exports);
125
+ $parcel$exportWildcard(module.exports, $54a02752d7f6015e$exports);
126
+ $parcel$exportWildcard(module.exports, $3cbdb2b44a8a698d$exports);
127
+
128
+
129
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"mappings":"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","sources":["packages/next/ip-rate-limit/src/index.ts","packages/next/ip-rate-limit/src/ip-rate-limit.ts","packages/next/ip-rate-limit/src/storage.ts","packages/next/ip-rate-limit/src/utils.ts","packages/next/ip-rate-limit/src/constants.ts","packages/next/ip-rate-limit/src/wrapper.ts","packages/next/ip-rate-limit/src/types.ts"],"sourcesContent":["export * from './ip-rate-limit'\nexport * from './wrapper'\nexport * from './constants'\nexport * from './types'\n","import { RateLimitStorage } from './storage'\nimport { getIP, rateLimitedResponse } from './utils'\nimport { RATE_LIMIT_HEADERS } from './constants'\nimport { RateLimit } from './types'\n\nlet rateLimitStorage: RateLimitStorage\n\nexport const ipRateLimit: RateLimit = (data) => {\n if (!rateLimitStorage) rateLimitStorage = new RateLimitStorage()\n\n const { res, req, limit, timeFrame } = data\n const headers = RATE_LIMIT_HEADERS\n const id = `ip:${getIP(req)}`\n\n const time = Math.floor(Date.now() / 1000 / timeFrame)\n const key = `${id}:${time}`\n\n const callCount = rateLimitStorage.inc(key, data.timeFrame)\n const remaining = limit - (callCount || 0)\n const reset = (time + 1) * timeFrame\n\n res.setHeader(headers.limit, `${limit}`)\n res.setHeader(headers.remaining, `${remaining < 0 ? 0 : remaining}`)\n res.setHeader(headers.reset, `${reset}`)\n\n if (remaining < 0) rateLimitedResponse({ id, res, req })\n\n return res\n}\n","import { RateLimitStorageType } from './types'\n\nexport class RateLimitStorage {\n private _storage: RateLimitStorageType\n private timeoutIds: NodeJS.Timeout[] = []\n\n constructor() {\n this._storage = new Map()\n }\n\n set(key: string, value: number, durationSec: number) {\n this._storage.set(key, { value })\n\n if (this._storage.get(key)?.timeoutId) {\n clearTimeout(this._storage.get(key)?.timeoutId)\n }\n\n // code based on the work with setTimeouts from the code from the nestjs throttling service\n // https://github.com/nestjs/throttler/blob/master/src/throttler.service.ts#L25\n const timeoutId = setTimeout(() => {\n clearTimeout(timeoutId)\n\n this.timeoutIds = this.timeoutIds.filter((id) => id != timeoutId)\n this.delete(key)\n }, durationSec * 1000)\n this.timeoutIds.push(timeoutId)\n }\n\n get(key: string) {\n return this._storage.get(key)?.value\n }\n\n inc(key: string, durationSec: number) {\n if (this._storage.has(key)) {\n this.set(key, (this._storage.get(key)?.value || 0) + 1, durationSec)\n } else {\n this.set(key, 1, durationSec)\n }\n\n return this.get(key)\n }\n delete(key: string) {\n if (this._storage.has(key)) {\n this._storage.delete(key)\n }\n }\n}\n","import { NextApiRequest } from 'next'\nimport { ERROR_MESSAGE } from './constants'\nimport { RateLimitedResponse } from './types'\n\n// now we use headers from \"cloudflare\" and rely on their validity.\n// If there are no \"cloudflare\" headers, we use the \"x-forwarded-for\" header,\n// but keep in mind that these headers can be changed by anyone\n// https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/\nexport const getIP = (req: NextApiRequest) => {\n const cfConnectionIp = req.headers['cf_connecting_ip']\n const xff = req.headers['x-forwarded-for']\n\n if (cfConnectionIp) return Array.isArray(cfConnectionIp) ? cfConnectionIp[0] : cfConnectionIp.split(',')[0]\n\n return xff ? (Array.isArray(xff) ? xff[0] : xff.split(',')[0]) : '127.0.0.1'\n}\n\nexport const rateLimitedResponse: RateLimitedResponse = ({ id, res, req }) => {\n res.status(429)\n\n throw new Error(`${ERROR_MESSAGE} ${id} ${req.url || ''}`)\n}\n","export const RATE_LIMIT_HEADERS = {\n limit: `X-RateLimit-Limit`,\n remaining: `X-RateLimit-Remaining`,\n reset: `X-RateLimit-Reset`,\n}\n\nexport const ERROR_MESSAGE = 'API rate limit exceeded for'\n","import { RequestWrapper } from '@lidofinance/next-api-wrapper'\n\nimport { ipRateLimit } from './ip-rate-limit'\nimport { RateLimitWrapperArgs } from './types'\n\nexport const rateLimitWrapper =\n ({ rateLimit, rateLimitTimeFrame }: RateLimitWrapperArgs): RequestWrapper =>\n async (req, res, next) => {\n ipRateLimit({\n req,\n res,\n limit: rateLimit,\n timeFrame: rateLimitTimeFrame,\n })\n\n await next?.(req, res, next)\n }\n","import { NextApiResponse, NextApiRequest } from 'next'\n\nexport type RateLimitStorageType = Map<string, { value: number; timeoutId?: NodeJS.Timeout }>\n\nexport type RateLimitedResponse = (data: { res: NextApiResponse; id: string; req: NextApiRequest }) => void\n\nexport type RateLimit = (data: {\n req: NextApiRequest\n res: NextApiResponse\n limit: number\n timeFrame: number\n}) => NextApiResponse\n\nexport type RateLimitWrapperArgs = {\n rateLimit: number\n rateLimitTimeFrame: number\n}\n"],"names":[],"version":3,"file":"index.cjs.map","sourceRoot":"../../../../"}
@@ -0,0 +1,31 @@
1
+ import { NextApiResponse, NextApiRequest } from "next";
2
+ import { RequestWrapper } from "@lidofinance/next-api-wrapper";
3
+ export type RateLimitStorageType = Map<string, {
4
+ value: number;
5
+ timeoutId?: NodeJS.Timeout;
6
+ }>;
7
+ export type RateLimitedResponse = (data: {
8
+ res: NextApiResponse;
9
+ id: string;
10
+ req: NextApiRequest;
11
+ }) => void;
12
+ export type RateLimit = (data: {
13
+ req: NextApiRequest;
14
+ res: NextApiResponse;
15
+ limit: number;
16
+ timeFrame: number;
17
+ }) => NextApiResponse;
18
+ export type RateLimitWrapperArgs = {
19
+ rateLimit: number;
20
+ rateLimitTimeFrame: number;
21
+ };
22
+ export const RATE_LIMIT_HEADERS: {
23
+ limit: string;
24
+ remaining: string;
25
+ reset: string;
26
+ };
27
+ export const ERROR_MESSAGE = "API rate limit exceeded for";
28
+ export const ipRateLimit: RateLimit;
29
+ export const rateLimitWrapper: ({ rateLimit, rateLimitTimeFrame }: RateLimitWrapperArgs) => RequestWrapper;
30
+
31
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"mappings":";;AAEA,mCAAmC,GAAG,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,OAAO,CAAA;CAAE,CAAC,CAAA;AAE7F,kCAAkC,CAAC,IAAI,EAAE;IAAE,GAAG,EAAE,eAAe,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,cAAc,CAAA;CAAE,KAAK,IAAI,CAAA;AAE3G,wBAAwB,CAAC,IAAI,EAAE;IAC7B,GAAG,EAAE,cAAc,CAAA;IACnB,GAAG,EAAE,eAAe,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB,KAAK,eAAe,CAAA;AAErB,mCAAmC;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,EAAE,MAAM,CAAA;CAC3B,CAAA;AEhBD,OAAO,MAAM;;;;CAIZ,CAAA;AAED,OAAO,MAAM,6CAA6C,CAAA;AEC1D,OAAO,MAAM,aAAa,SAqBzB,CAAA;ACvBD,OAAO,MAAM,sDACyB,oBAAoB,KAAG,cAU1D,CAAA","sources":["packages/next/ip-rate-limit/src/src/types.ts","packages/next/ip-rate-limit/src/src/storage.ts","packages/next/ip-rate-limit/src/src/constants.ts","packages/next/ip-rate-limit/src/src/utils.ts","packages/next/ip-rate-limit/src/src/ip-rate-limit.ts","packages/next/ip-rate-limit/src/src/wrapper.ts","packages/next/ip-rate-limit/src/src/index.ts","packages/next/ip-rate-limit/src/index.ts"],"sourcesContent":[null,null,null,null,null,null,null,"export * from './ip-rate-limit'\nexport * from './wrapper'\nexport * from './constants'\nexport * from './types'\n"],"names":[],"version":3,"file":"index.d.ts.map","sourceRoot":"../../../../"}
package/dist/index.mjs ADDED
@@ -0,0 +1,110 @@
1
+ function $parcel$export(e, n, v, s) {
2
+ Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
3
+ }
4
+ var $4a980b7982e8368e$exports = {};
5
+
6
+ $parcel$export($4a980b7982e8368e$exports, "ipRateLimit", () => $4a980b7982e8368e$export$122bebc9e141287c);
7
+ class $a5fb111f145df95d$export$230ecc6ad0ef6558 {
8
+ timeoutIds = [];
9
+ constructor(){
10
+ this._storage = new Map();
11
+ }
12
+ set(key, value, durationSec) {
13
+ this._storage.set(key, {
14
+ value: value
15
+ });
16
+ if (this._storage.get(key)?.timeoutId) clearTimeout(this._storage.get(key)?.timeoutId);
17
+ // code based on the work with setTimeouts from the code from the nestjs throttling service
18
+ // https://github.com/nestjs/throttler/blob/master/src/throttler.service.ts#L25
19
+ const timeoutId = setTimeout(()=>{
20
+ clearTimeout(timeoutId);
21
+ this.timeoutIds = this.timeoutIds.filter((id)=>id != timeoutId);
22
+ this.delete(key);
23
+ }, durationSec * 1000);
24
+ this.timeoutIds.push(timeoutId);
25
+ }
26
+ get(key) {
27
+ return this._storage.get(key)?.value;
28
+ }
29
+ inc(key, durationSec) {
30
+ if (this._storage.has(key)) this.set(key, (this._storage.get(key)?.value || 0) + 1, durationSec);
31
+ else this.set(key, 1, durationSec);
32
+ return this.get(key);
33
+ }
34
+ delete(key) {
35
+ if (this._storage.has(key)) this._storage.delete(key);
36
+ }
37
+ }
38
+
39
+
40
+ var $1022f9b28fec0213$exports = {};
41
+
42
+ $parcel$export($1022f9b28fec0213$exports, "RATE_LIMIT_HEADERS", () => $1022f9b28fec0213$export$a063967512de3ebf);
43
+ $parcel$export($1022f9b28fec0213$exports, "ERROR_MESSAGE", () => $1022f9b28fec0213$export$3f27bc10068ef877);
44
+ const $1022f9b28fec0213$export$a063967512de3ebf = {
45
+ limit: `X-RateLimit-Limit`,
46
+ remaining: `X-RateLimit-Remaining`,
47
+ reset: `X-RateLimit-Reset`
48
+ };
49
+ const $1022f9b28fec0213$export$3f27bc10068ef877 = "API rate limit exceeded for";
50
+
51
+
52
+ const $15f73ba5d70a67f1$export$67218453e01a0be8 = (req)=>{
53
+ const cfConnectionIp = req.headers["cf_connecting_ip"];
54
+ const xff = req.headers["x-forwarded-for"];
55
+ if (cfConnectionIp) return Array.isArray(cfConnectionIp) ? cfConnectionIp[0] : cfConnectionIp.split(",")[0];
56
+ return xff ? Array.isArray(xff) ? xff[0] : xff.split(",")[0] : "127.0.0.1";
57
+ };
58
+ const $15f73ba5d70a67f1$export$d0a20d44a54350ab = ({ id: id , res: res , req: req })=>{
59
+ res.status(429);
60
+ throw new Error(`${(0, $1022f9b28fec0213$export$3f27bc10068ef877)} ${id} ${req.url || ""}`);
61
+ };
62
+
63
+
64
+
65
+ let $4a980b7982e8368e$var$rateLimitStorage;
66
+ const $4a980b7982e8368e$export$122bebc9e141287c = (data)=>{
67
+ if (!$4a980b7982e8368e$var$rateLimitStorage) $4a980b7982e8368e$var$rateLimitStorage = new (0, $a5fb111f145df95d$export$230ecc6ad0ef6558)();
68
+ const { res: res , req: req , limit: limit , timeFrame: timeFrame } = data;
69
+ const headers = (0, $1022f9b28fec0213$export$a063967512de3ebf);
70
+ const id = `ip:${(0, $15f73ba5d70a67f1$export$67218453e01a0be8)(req)}`;
71
+ const time = Math.floor(Date.now() / 1000 / timeFrame);
72
+ const key = `${id}:${time}`;
73
+ const callCount = $4a980b7982e8368e$var$rateLimitStorage.inc(key, data.timeFrame);
74
+ const remaining = limit - (callCount || 0);
75
+ const reset = (time + 1) * timeFrame;
76
+ res.setHeader(headers.limit, `${limit}`);
77
+ res.setHeader(headers.remaining, `${remaining < 0 ? 0 : remaining}`);
78
+ res.setHeader(headers.reset, `${reset}`);
79
+ if (remaining < 0) (0, $15f73ba5d70a67f1$export$d0a20d44a54350ab)({
80
+ id: id,
81
+ res: res,
82
+ req: req
83
+ });
84
+ return res;
85
+ };
86
+
87
+
88
+ var $f6440f950b4feda9$exports = {};
89
+
90
+ $parcel$export($f6440f950b4feda9$exports, "rateLimitWrapper", () => $f6440f950b4feda9$export$1e3e762f475f853);
91
+
92
+ const $f6440f950b4feda9$export$1e3e762f475f853 = ({ rateLimit: rateLimit , rateLimitTimeFrame: rateLimitTimeFrame })=>async (req, res, next)=>{
93
+ (0, $4a980b7982e8368e$export$122bebc9e141287c)({
94
+ req: req,
95
+ res: res,
96
+ limit: rateLimit,
97
+ timeFrame: rateLimitTimeFrame
98
+ });
99
+ await next?.(req, res, next);
100
+ };
101
+
102
+
103
+
104
+ var $4f28aa0b2d7963de$exports = {};
105
+
106
+
107
+
108
+
109
+ export {$4a980b7982e8368e$export$122bebc9e141287c as ipRateLimit, $f6440f950b4feda9$export$1e3e762f475f853 as rateLimitWrapper, $1022f9b28fec0213$export$a063967512de3ebf as RATE_LIMIT_HEADERS, $1022f9b28fec0213$export$3f27bc10068ef877 as ERROR_MESSAGE};
110
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"mappings":"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","sources":["packages/next/ip-rate-limit/src/index.ts","packages/next/ip-rate-limit/src/ip-rate-limit.ts","packages/next/ip-rate-limit/src/storage.ts","packages/next/ip-rate-limit/src/utils.ts","packages/next/ip-rate-limit/src/constants.ts","packages/next/ip-rate-limit/src/wrapper.ts","packages/next/ip-rate-limit/src/types.ts"],"sourcesContent":["export * from './ip-rate-limit'\nexport * from './wrapper'\nexport * from './constants'\nexport * from './types'\n","import { RateLimitStorage } from './storage'\nimport { getIP, rateLimitedResponse } from './utils'\nimport { RATE_LIMIT_HEADERS } from './constants'\nimport { RateLimit } from './types'\n\nlet rateLimitStorage: RateLimitStorage\n\nexport const ipRateLimit: RateLimit = (data) => {\n if (!rateLimitStorage) rateLimitStorage = new RateLimitStorage()\n\n const { res, req, limit, timeFrame } = data\n const headers = RATE_LIMIT_HEADERS\n const id = `ip:${getIP(req)}`\n\n const time = Math.floor(Date.now() / 1000 / timeFrame)\n const key = `${id}:${time}`\n\n const callCount = rateLimitStorage.inc(key, data.timeFrame)\n const remaining = limit - (callCount || 0)\n const reset = (time + 1) * timeFrame\n\n res.setHeader(headers.limit, `${limit}`)\n res.setHeader(headers.remaining, `${remaining < 0 ? 0 : remaining}`)\n res.setHeader(headers.reset, `${reset}`)\n\n if (remaining < 0) rateLimitedResponse({ id, res, req })\n\n return res\n}\n","import { RateLimitStorageType } from './types'\n\nexport class RateLimitStorage {\n private _storage: RateLimitStorageType\n private timeoutIds: NodeJS.Timeout[] = []\n\n constructor() {\n this._storage = new Map()\n }\n\n set(key: string, value: number, durationSec: number) {\n this._storage.set(key, { value })\n\n if (this._storage.get(key)?.timeoutId) {\n clearTimeout(this._storage.get(key)?.timeoutId)\n }\n\n // code based on the work with setTimeouts from the code from the nestjs throttling service\n // https://github.com/nestjs/throttler/blob/master/src/throttler.service.ts#L25\n const timeoutId = setTimeout(() => {\n clearTimeout(timeoutId)\n\n this.timeoutIds = this.timeoutIds.filter((id) => id != timeoutId)\n this.delete(key)\n }, durationSec * 1000)\n this.timeoutIds.push(timeoutId)\n }\n\n get(key: string) {\n return this._storage.get(key)?.value\n }\n\n inc(key: string, durationSec: number) {\n if (this._storage.has(key)) {\n this.set(key, (this._storage.get(key)?.value || 0) + 1, durationSec)\n } else {\n this.set(key, 1, durationSec)\n }\n\n return this.get(key)\n }\n delete(key: string) {\n if (this._storage.has(key)) {\n this._storage.delete(key)\n }\n }\n}\n","import { NextApiRequest } from 'next'\nimport { ERROR_MESSAGE } from './constants'\nimport { RateLimitedResponse } from './types'\n\n// now we use headers from \"cloudflare\" and rely on their validity.\n// If there are no \"cloudflare\" headers, we use the \"x-forwarded-for\" header,\n// but keep in mind that these headers can be changed by anyone\n// https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/\nexport const getIP = (req: NextApiRequest) => {\n const cfConnectionIp = req.headers['cf_connecting_ip']\n const xff = req.headers['x-forwarded-for']\n\n if (cfConnectionIp) return Array.isArray(cfConnectionIp) ? cfConnectionIp[0] : cfConnectionIp.split(',')[0]\n\n return xff ? (Array.isArray(xff) ? xff[0] : xff.split(',')[0]) : '127.0.0.1'\n}\n\nexport const rateLimitedResponse: RateLimitedResponse = ({ id, res, req }) => {\n res.status(429)\n\n throw new Error(`${ERROR_MESSAGE} ${id} ${req.url || ''}`)\n}\n","export const RATE_LIMIT_HEADERS = {\n limit: `X-RateLimit-Limit`,\n remaining: `X-RateLimit-Remaining`,\n reset: `X-RateLimit-Reset`,\n}\n\nexport const ERROR_MESSAGE = 'API rate limit exceeded for'\n","import { RequestWrapper } from '@lidofinance/next-api-wrapper'\n\nimport { ipRateLimit } from './ip-rate-limit'\nimport { RateLimitWrapperArgs } from './types'\n\nexport const rateLimitWrapper =\n ({ rateLimit, rateLimitTimeFrame }: RateLimitWrapperArgs): RequestWrapper =>\n async (req, res, next) => {\n ipRateLimit({\n req,\n res,\n limit: rateLimit,\n timeFrame: rateLimitTimeFrame,\n })\n\n await next?.(req, res, next)\n }\n","import { NextApiResponse, NextApiRequest } from 'next'\n\nexport type RateLimitStorageType = Map<string, { value: number; timeoutId?: NodeJS.Timeout }>\n\nexport type RateLimitedResponse = (data: { res: NextApiResponse; id: string; req: NextApiRequest }) => void\n\nexport type RateLimit = (data: {\n req: NextApiRequest\n res: NextApiResponse\n limit: number\n timeFrame: number\n}) => NextApiResponse\n\nexport type RateLimitWrapperArgs = {\n rateLimit: number\n rateLimitTimeFrame: number\n}\n"],"names":[],"version":3,"file":"index.mjs.map","sourceRoot":"../../../../"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@lidofinance/next-ip-rate-limit",
3
+ "description": "IP rate limit for next server api requests",
4
+ "repository": "git@github.com:lidofinance/warehouse.git",
5
+ "license": "MIT",
6
+ "version": "0.10.0",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "engines": {
11
+ "node": ">= 16"
12
+ },
13
+ "source": "./src/index.ts",
14
+ "main": "dist/index.cjs",
15
+ "module": "dist/index.mjs",
16
+ "types": "dist/index.d.ts",
17
+ "scripts": {
18
+ "build": "parcel build",
19
+ "format": "prettier --check src",
20
+ "format:fix": "yarn format --write",
21
+ "types": "tsc --noEmit",
22
+ "test": "jest"
23
+ },
24
+ "peerDependencies": {
25
+ "next": "^12.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "@lidofinance/config-prettier": "~0.10.0",
29
+ "@types/jest": "^28.0.0",
30
+ "@types/node": "^18.11.2",
31
+ "jest": "^28.0.0",
32
+ "next": "^12.3.1",
33
+ "ts-jest": "^28.0.0"
34
+ },
35
+ "dependencies": {
36
+ "@lidofinance/next-api-wrapper": "~0.10.0"
37
+ }
38
+ }