@lidofinance/next-ip-rate-limit 0.59.0 → 0.61.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/dist/index.cjs CHANGED
@@ -1,3 +1,5 @@
1
+ var $2Dv6i$net = require("net");
2
+
1
3
  function $parcel$exportWildcard(dest, source) {
2
4
  Object.keys(source).forEach(function(key) {
3
5
  if (key === 'default' || key === '__esModule' || dest.hasOwnProperty(key)) {
@@ -53,6 +55,7 @@ class $9e3ba9e2f0393d79$export$230ecc6ad0ef6558 {
53
55
  }
54
56
 
55
57
 
58
+
56
59
  var $54a02752d7f6015e$exports = {};
57
60
 
58
61
  $parcel$export($54a02752d7f6015e$exports, "RATE_LIMIT_HEADERS", () => $54a02752d7f6015e$export$a063967512de3ebf);
@@ -65,11 +68,21 @@ const $54a02752d7f6015e$export$a063967512de3ebf = {
65
68
  const $54a02752d7f6015e$export$3f27bc10068ef877 = "API rate limit exceeded for";
66
69
 
67
70
 
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";
71
+ const $c63185ef25ccfd28$export$73f7dfe1bf8dc8ad = (ip)=>ip.trim().replace(/^::ffff:/i, "");
72
+ // Get first IP address from a header value or return undefined if invalid
73
+ const $c63185ef25ccfd28$var$firstHeaderValue = (header)=>{
74
+ const raw = Array.isArray(header) ? header[0] : header;
75
+ const value = raw ? $c63185ef25ccfd28$export$73f7dfe1bf8dc8ad(raw.split(",")[0]) : undefined;
76
+ return value && (0, $2Dv6i$net.isIP)(value) ? value : undefined;
77
+ };
78
+ const $c63185ef25ccfd28$export$67218453e01a0be8 = (req, customIpHeader)=>{
79
+ // For custom header setup
80
+ const customIpValue = customIpHeader ? $c63185ef25ccfd28$var$firstHeaderValue(req.headers[customIpHeader.toLowerCase()]) : undefined;
81
+ // Default CF header
82
+ const cfConnectionIp = $c63185ef25ccfd28$var$firstHeaderValue(req.headers["cf-connecting-ip"]);
83
+ // Fallback to remote address
84
+ const remoteIp = $c63185ef25ccfd28$export$73f7dfe1bf8dc8ad(req.socket?.remoteAddress || "0.0.0.0");
85
+ return customIpValue || cfConnectionIp || remoteIp;
73
86
  };
74
87
  const $c63185ef25ccfd28$export$d0a20d44a54350ab = ({ id: id, res: res, req: req, retryAfter: retryAfter })=>{
75
88
  res.status(429);
@@ -83,9 +96,9 @@ let $abe9c276dfd79488$var$rateLimitStorage;
83
96
  const $abe9c276dfd79488$export$122bebc9e141287c = (data)=>{
84
97
  if (!$abe9c276dfd79488$var$rateLimitStorage) $abe9c276dfd79488$var$rateLimitStorage = new (0, $9e3ba9e2f0393d79$export$230ecc6ad0ef6558)();
85
98
  const now = Date.now() / 1000;
86
- const { res: res, req: req, limit: limit, timeFrame: timeFrame } = data;
99
+ const { res: res, req: req, limit: limit, timeFrame: timeFrame, customIpHeader: customIpHeader } = data;
87
100
  const headers = (0, $54a02752d7f6015e$export$a063967512de3ebf);
88
- const id = `ip:${(0, $c63185ef25ccfd28$export$67218453e01a0be8)(req)}`;
101
+ const id = `ip:${(0, $c63185ef25ccfd28$export$67218453e01a0be8)(req, customIpHeader)}`;
89
102
  const time = Math.floor(now / timeFrame);
90
103
  const key = `${id}:${time}`;
91
104
  const callCount = $abe9c276dfd79488$var$rateLimitStorage.inc(key, data.timeFrame);
@@ -111,12 +124,13 @@ var $bad81bc593a4351d$exports = {};
111
124
 
112
125
  $parcel$export($bad81bc593a4351d$exports, "rateLimitWrapper", () => $bad81bc593a4351d$export$1e3e762f475f853);
113
126
 
114
- const $bad81bc593a4351d$export$1e3e762f475f853 = ({ rateLimit: rateLimit, rateLimitTimeFrame: rateLimitTimeFrame })=>async (req, res, next)=>{
127
+ const $bad81bc593a4351d$export$1e3e762f475f853 = ({ rateLimit: rateLimit, rateLimitTimeFrame: rateLimitTimeFrame, customIpHeader: customIpHeader })=>async (req, res, next)=>{
115
128
  (0, $abe9c276dfd79488$export$122bebc9e141287c)({
116
129
  req: req,
117
130
  res: res,
118
131
  limit: rateLimit,
119
- timeFrame: rateLimitTimeFrame
132
+ timeFrame: rateLimitTimeFrame,
133
+ customIpHeader: customIpHeader
120
134
  });
121
135
  await next?.(req, res, next);
122
136
  };
@@ -1 +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 now = Date.now() / 1000\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(now / 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) {\n const retryAfter = Math.ceil(Math.max(reset - now, 0))\n rateLimitedResponse({ id, res, req, retryAfter })\n }\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, retryAfter }) => {\n res.status(429)\n if (retryAfter) res.setHeader('Retry-After', retryAfter.toString())\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: {\n res: NextApiResponse\n id: string\n req: NextApiRequest\n retryAfter?: number\n}) => 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":"../../../../"}
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 now = Date.now() / 1000\n\n const { res, req, limit, timeFrame, customIpHeader } = data\n const headers = RATE_LIMIT_HEADERS\n const id = `ip:${getIP(req, customIpHeader)}`\n\n const time = Math.floor(now / 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) {\n const retryAfter = Math.ceil(Math.max(reset - now, 0))\n rateLimitedResponse({ id, res, req, retryAfter })\n }\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 { isIP } from 'net'\nimport { NextApiRequest } from 'next'\nimport { ERROR_MESSAGE } from './constants'\nimport { RateLimitedResponse } from './types'\n\n// Normalize IP address by trimming whitespace and removing IPv6 prefix if present\nexport const normalizeIP = (ip: string): string => ip.trim().replace(/^::ffff:/i, '')\n\n// Get first IP address from a header value or return undefined if invalid\nconst firstHeaderValue = (header?: string | string[]): string | undefined => {\n const raw = Array.isArray(header) ? header[0] : header\n const value = raw ? normalizeIP(raw.split(',')[0]) : undefined\n\n return value && isIP(value) ? value : undefined\n}\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, customIpHeader?: string): string => {\n // For custom header setup\n const customIpValue = customIpHeader ? firstHeaderValue(req.headers[customIpHeader.toLowerCase()]) : undefined\n\n // Default CF header\n const cfConnectionIp = firstHeaderValue(req.headers['cf-connecting-ip'])\n\n // Fallback to remote address\n const remoteIp = normalizeIP(req.socket?.remoteAddress || '0.0.0.0')\n\n return customIpValue || cfConnectionIp || remoteIp\n}\n\nexport const rateLimitedResponse: RateLimitedResponse = ({ id, res, req, retryAfter }) => {\n res.status(429)\n if (retryAfter) res.setHeader('Retry-After', retryAfter.toString())\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, customIpHeader }: RateLimitWrapperArgs): RequestWrapper =>\n async (req, res, next) => {\n ipRateLimit({\n req,\n res,\n limit: rateLimit,\n timeFrame: rateLimitTimeFrame,\n customIpHeader,\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: {\n res: NextApiResponse\n id: string\n req: NextApiRequest\n retryAfter?: number\n}) => void\n\nexport type RateLimit = (data: {\n req: NextApiRequest\n res: NextApiResponse\n limit: number\n timeFrame: number\n customIpHeader?: string\n}) => NextApiResponse\n\nexport type RateLimitWrapperArgs = {\n rateLimit: number\n rateLimitTimeFrame: number\n customIpHeader?: string\n}\n"],"names":[],"version":3,"file":"index.cjs.map","sourceRoot":"../../../../"}
package/dist/index.d.ts CHANGED
@@ -15,10 +15,12 @@ export type RateLimit = (data: {
15
15
  res: NextApiResponse;
16
16
  limit: number;
17
17
  timeFrame: number;
18
+ customIpHeader?: string;
18
19
  }) => NextApiResponse;
19
20
  export type RateLimitWrapperArgs = {
20
21
  rateLimit: number;
21
22
  rateLimitTimeFrame: number;
23
+ customIpHeader?: string;
22
24
  };
23
25
  export const RATE_LIMIT_HEADERS: {
24
26
  limit: string;
@@ -27,6 +29,6 @@ export const RATE_LIMIT_HEADERS: {
27
29
  };
28
30
  export const ERROR_MESSAGE = "API rate limit exceeded for";
29
31
  export const ipRateLimit: RateLimit;
30
- export const rateLimitWrapper: ({ rateLimit, rateLimitTimeFrame }: RateLimitWrapperArgs) => RequestWrapper;
32
+ export const rateLimitWrapper: ({ rateLimit, rateLimitTimeFrame, customIpHeader }: RateLimitWrapperArgs) => RequestWrapper;
31
33
 
32
34
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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;IACvC,GAAG,EAAE,eAAe,CAAA;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,cAAc,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,KAAK,IAAI,CAAA;AAEV,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;AErBD,OAAO,MAAM;;;;CAIZ,CAAA;AAED,OAAO,MAAM,6CAA6C,CAAA;AEC1D,OAAO,MAAM,aAAa,SA0BzB,CAAA;AC5BD,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":"../../../../"}
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;IACvC,GAAG,EAAE,eAAe,CAAA;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,cAAc,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,KAAK,IAAI,CAAA;AAEV,wBAAwB,CAAC,IAAI,EAAE;IAC7B,GAAG,EAAE,cAAc,CAAA;IACnB,GAAG,EAAE,eAAe,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,KAAK,eAAe,CAAA;AAErB,mCAAmC;IACjC,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB,CAAA;AEvBD,OAAO,MAAM;;;;CAIZ,CAAA;AAED,OAAO,MAAM,6CAA6C,CAAA;AEC1D,OAAO,MAAM,aAAa,SA0BzB,CAAA;AC5BD,OAAO,MAAM,sEACyC,oBAAoB,KAAG,cAW1E,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 CHANGED
@@ -1,3 +1,5 @@
1
+ import {isIP as $iZoYP$isIP} from "net";
2
+
1
3
  function $parcel$export(e, n, v, s) {
2
4
  Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
3
5
  }
@@ -37,6 +39,7 @@ class $a5fb111f145df95d$export$230ecc6ad0ef6558 {
37
39
  }
38
40
 
39
41
 
42
+
40
43
  var $1022f9b28fec0213$exports = {};
41
44
 
42
45
  $parcel$export($1022f9b28fec0213$exports, "RATE_LIMIT_HEADERS", () => $1022f9b28fec0213$export$a063967512de3ebf);
@@ -49,11 +52,21 @@ const $1022f9b28fec0213$export$a063967512de3ebf = {
49
52
  const $1022f9b28fec0213$export$3f27bc10068ef877 = "API rate limit exceeded for";
50
53
 
51
54
 
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";
55
+ const $15f73ba5d70a67f1$export$73f7dfe1bf8dc8ad = (ip)=>ip.trim().replace(/^::ffff:/i, "");
56
+ // Get first IP address from a header value or return undefined if invalid
57
+ const $15f73ba5d70a67f1$var$firstHeaderValue = (header)=>{
58
+ const raw = Array.isArray(header) ? header[0] : header;
59
+ const value = raw ? $15f73ba5d70a67f1$export$73f7dfe1bf8dc8ad(raw.split(",")[0]) : undefined;
60
+ return value && (0, $iZoYP$isIP)(value) ? value : undefined;
61
+ };
62
+ const $15f73ba5d70a67f1$export$67218453e01a0be8 = (req, customIpHeader)=>{
63
+ // For custom header setup
64
+ const customIpValue = customIpHeader ? $15f73ba5d70a67f1$var$firstHeaderValue(req.headers[customIpHeader.toLowerCase()]) : undefined;
65
+ // Default CF header
66
+ const cfConnectionIp = $15f73ba5d70a67f1$var$firstHeaderValue(req.headers["cf-connecting-ip"]);
67
+ // Fallback to remote address
68
+ const remoteIp = $15f73ba5d70a67f1$export$73f7dfe1bf8dc8ad(req.socket?.remoteAddress || "0.0.0.0");
69
+ return customIpValue || cfConnectionIp || remoteIp;
57
70
  };
58
71
  const $15f73ba5d70a67f1$export$d0a20d44a54350ab = ({ id: id, res: res, req: req, retryAfter: retryAfter })=>{
59
72
  res.status(429);
@@ -67,9 +80,9 @@ let $4a980b7982e8368e$var$rateLimitStorage;
67
80
  const $4a980b7982e8368e$export$122bebc9e141287c = (data)=>{
68
81
  if (!$4a980b7982e8368e$var$rateLimitStorage) $4a980b7982e8368e$var$rateLimitStorage = new (0, $a5fb111f145df95d$export$230ecc6ad0ef6558)();
69
82
  const now = Date.now() / 1000;
70
- const { res: res, req: req, limit: limit, timeFrame: timeFrame } = data;
83
+ const { res: res, req: req, limit: limit, timeFrame: timeFrame, customIpHeader: customIpHeader } = data;
71
84
  const headers = (0, $1022f9b28fec0213$export$a063967512de3ebf);
72
- const id = `ip:${(0, $15f73ba5d70a67f1$export$67218453e01a0be8)(req)}`;
85
+ const id = `ip:${(0, $15f73ba5d70a67f1$export$67218453e01a0be8)(req, customIpHeader)}`;
73
86
  const time = Math.floor(now / timeFrame);
74
87
  const key = `${id}:${time}`;
75
88
  const callCount = $4a980b7982e8368e$var$rateLimitStorage.inc(key, data.timeFrame);
@@ -95,12 +108,13 @@ var $f6440f950b4feda9$exports = {};
95
108
 
96
109
  $parcel$export($f6440f950b4feda9$exports, "rateLimitWrapper", () => $f6440f950b4feda9$export$1e3e762f475f853);
97
110
 
98
- const $f6440f950b4feda9$export$1e3e762f475f853 = ({ rateLimit: rateLimit, rateLimitTimeFrame: rateLimitTimeFrame })=>async (req, res, next)=>{
111
+ const $f6440f950b4feda9$export$1e3e762f475f853 = ({ rateLimit: rateLimit, rateLimitTimeFrame: rateLimitTimeFrame, customIpHeader: customIpHeader })=>async (req, res, next)=>{
99
112
  (0, $4a980b7982e8368e$export$122bebc9e141287c)({
100
113
  req: req,
101
114
  res: res,
102
115
  limit: rateLimit,
103
- timeFrame: rateLimitTimeFrame
116
+ timeFrame: rateLimitTimeFrame,
117
+ customIpHeader: customIpHeader
104
118
  });
105
119
  await next?.(req, res, next);
106
120
  };
@@ -1 +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 now = Date.now() / 1000\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(now / 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) {\n const retryAfter = Math.ceil(Math.max(reset - now, 0))\n rateLimitedResponse({ id, res, req, retryAfter })\n }\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, retryAfter }) => {\n res.status(429)\n if (retryAfter) res.setHeader('Retry-After', retryAfter.toString())\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: {\n res: NextApiResponse\n id: string\n req: NextApiRequest\n retryAfter?: number\n}) => 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":"../../../../"}
1
+ {"mappings":";;;;;;;;AEEO,MAAM;IAIX,aAAc;aAFN,aAA+B,EAAE;QAGvC,IAAI,CAAC,WAAW,IAAI;IACtB;IAEA,IAAI,GAAW,EAAE,KAAa,EAAE,WAAmB,EAAE;QACnD,IAAI,CAAC,SAAS,IAAI,KAAK;mBAAE;QAAM;QAE/B,IAAI,IAAI,CAAC,SAAS,IAAI,MAAM,WAC1B,aAAa,IAAI,CAAC,SAAS,IAAI,MAAM;QAGvC,2FAA2F;QAC3F,+EAA+E;QAC/E,MAAM,YAAY,WAAW;YAC3B,aAAa;YAEb,IAAI,CAAC,aAAa,IAAI,CAAC,WAAW,OAAO,CAAC,KAAO,MAAM;YACvD,IAAI,CAAC,OAAO;QACd,GAAG,cAAc;QACjB,IAAI,CAAC,WAAW,KAAK;IACvB;IAEA,IAAI,GAAW,EAAE;QACf,OAAO,IAAI,CAAC,SAAS,IAAI,MAAM;IACjC;IAEA,IAAI,GAAW,EAAE,WAAmB,EAAE;QACpC,IAAI,IAAI,CAAC,SAAS,IAAI,MACpB,IAAI,CAAC,IAAI,KAAK,AAAC,CAAA,IAAI,CAAC,SAAS,IAAI,MAAM,SAAS,CAAA,IAAK,GAAG;aAExD,IAAI,CAAC,IAAI,KAAK,GAAG;QAGnB,OAAO,IAAI,CAAC,IAAI;IAClB;IACA,OAAO,GAAW,EAAE;QAClB,IAAI,IAAI,CAAC,SAAS,IAAI,MACpB,IAAI,CAAC,SAAS,OAAO;IAEzB;AACF;;;;;;;;AE9CO,MAAM,4CAAqB;IAChC,OAAO,CAAC,iBAAiB,CAAC;IAC1B,WAAW,CAAC,qBAAqB,CAAC;IAClC,OAAO,CAAC,iBAAiB,CAAC;AAC5B;AAEO,MAAM,4CAAgB;;;ADAtB,MAAM,4CAAc,CAAC,KAAuB,GAAG,OAAO,QAAQ,aAAa;AAElF,0EAA0E;AAC1E,MAAM,yCAAmB,CAAC;IACxB,MAAM,MAAM,MAAM,QAAQ,UAAU,MAAM,CAAC,EAAE,GAAG;IAChD,MAAM,QAAQ,MAAM,0CAAY,IAAI,MAAM,IAAI,CAAC,EAAE,IAAI;IAErD,OAAO,SAAS,CAAA,GAAA,WAAG,EAAE,SAAS,QAAQ;AACxC;AAMO,MAAM,4CAAQ,CAAC,KAAqB;IACzC,0BAA0B;IAC1B,MAAM,gBAAgB,iBAAiB,uCAAiB,IAAI,OAAO,CAAC,eAAe,cAAc,IAAI;IAErG,oBAAoB;IACpB,MAAM,iBAAiB,uCAAiB,IAAI,OAAO,CAAC,mBAAmB;IAEvE,6BAA6B;IAC7B,MAAM,WAAW,0CAAY,IAAI,QAAQ,iBAAiB;IAE1D,OAAO,iBAAiB,kBAAkB;AAC5C;AAEO,MAAM,4CAA2C,CAAC,MAAE,EAAE,OAAE,GAAG,OAAE,GAAG,cAAE,UAAU,EAAE;IACnF,IAAI,OAAO;IACX,IAAI,YAAY,IAAI,UAAU,eAAe,WAAW;IACxD,MAAM,IAAI,MAAM,CAAC,EAAE,CAAA,GAAA,yCAAY,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,OAAO,GAAG,CAAC;AAC3D;;;;AFhCA,IAAI;AAEG,MAAM,4CAAyB,CAAC;IACrC,IAAI,CAAC,wCAAkB,yCAAmB,IAAI,CAAA,GAAA,yCAAe;IAE7D,MAAM,MAAM,KAAK,QAAQ;IAEzB,MAAM,OAAE,GAAG,OAAE,GAAG,SAAE,KAAK,aAAE,SAAS,kBAAE,cAAc,EAAE,GAAG;IACvD,MAAM,UAAU,CAAA,GAAA,yCAAiB;IACjC,MAAM,KAAK,CAAC,GAAG,EAAE,CAAA,GAAA,yCAAI,EAAE,KAAK,gBAAgB,CAAC;IAE7C,MAAM,OAAO,KAAK,MAAM,MAAM;IAC9B,MAAM,MAAM,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC;IAE3B,MAAM,YAAY,uCAAiB,IAAI,KAAK,KAAK;IACjD,MAAM,YAAY,QAAS,CAAA,aAAa,CAAA;IACxC,MAAM,QAAQ,AAAC,CAAA,OAAO,CAAA,IAAK;IAE3B,IAAI,UAAU,QAAQ,OAAO,CAAC,EAAE,MAAM,CAAC;IACvC,IAAI,UAAU,QAAQ,WAAW,CAAC,EAAE,YAAY,IAAI,IAAI,UAAU,CAAC;IACnE,IAAI,UAAU,QAAQ,OAAO,CAAC,EAAE,MAAM,CAAC;IAEvC,IAAI,YAAY,GAAG;QACjB,MAAM,aAAa,KAAK,KAAK,KAAK,IAAI,QAAQ,KAAK;QACnD,CAAA,GAAA,yCAAkB,EAAE;gBAAE;iBAAI;iBAAK;wBAAK;QAAW;IACjD;IAEA,OAAO;AACT;;;;;;;AI5BO,MAAM,2CACX,CAAC,aAAE,SAAS,sBAAE,kBAAkB,kBAAE,cAAc,EAAwB,GACxE,OAAO,KAAK,KAAK;QACf,CAAA,GAAA,yCAAU,EAAE;iBACV;iBACA;YACA,OAAO;YACP,WAAW;4BACX;QACF;QAEA,MAAM,OAAO,KAAK,KAAK;IACzB;;;;;;","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 now = Date.now() / 1000\n\n const { res, req, limit, timeFrame, customIpHeader } = data\n const headers = RATE_LIMIT_HEADERS\n const id = `ip:${getIP(req, customIpHeader)}`\n\n const time = Math.floor(now / 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) {\n const retryAfter = Math.ceil(Math.max(reset - now, 0))\n rateLimitedResponse({ id, res, req, retryAfter })\n }\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 { isIP } from 'net'\nimport { NextApiRequest } from 'next'\nimport { ERROR_MESSAGE } from './constants'\nimport { RateLimitedResponse } from './types'\n\n// Normalize IP address by trimming whitespace and removing IPv6 prefix if present\nexport const normalizeIP = (ip: string): string => ip.trim().replace(/^::ffff:/i, '')\n\n// Get first IP address from a header value or return undefined if invalid\nconst firstHeaderValue = (header?: string | string[]): string | undefined => {\n const raw = Array.isArray(header) ? header[0] : header\n const value = raw ? normalizeIP(raw.split(',')[0]) : undefined\n\n return value && isIP(value) ? value : undefined\n}\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, customIpHeader?: string): string => {\n // For custom header setup\n const customIpValue = customIpHeader ? firstHeaderValue(req.headers[customIpHeader.toLowerCase()]) : undefined\n\n // Default CF header\n const cfConnectionIp = firstHeaderValue(req.headers['cf-connecting-ip'])\n\n // Fallback to remote address\n const remoteIp = normalizeIP(req.socket?.remoteAddress || '0.0.0.0')\n\n return customIpValue || cfConnectionIp || remoteIp\n}\n\nexport const rateLimitedResponse: RateLimitedResponse = ({ id, res, req, retryAfter }) => {\n res.status(429)\n if (retryAfter) res.setHeader('Retry-After', retryAfter.toString())\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, customIpHeader }: RateLimitWrapperArgs): RequestWrapper =>\n async (req, res, next) => {\n ipRateLimit({\n req,\n res,\n limit: rateLimit,\n timeFrame: rateLimitTimeFrame,\n customIpHeader,\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: {\n res: NextApiResponse\n id: string\n req: NextApiRequest\n retryAfter?: number\n}) => void\n\nexport type RateLimit = (data: {\n req: NextApiRequest\n res: NextApiResponse\n limit: number\n timeFrame: number\n customIpHeader?: string\n}) => NextApiResponse\n\nexport type RateLimitWrapperArgs = {\n rateLimit: number\n rateLimitTimeFrame: number\n customIpHeader?: string\n}\n"],"names":[],"version":3,"file":"index.mjs.map","sourceRoot":"../../../../"}
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/lidofinance/warehouse/issues"
8
8
  },
9
9
  "license": "MIT",
10
- "version": "0.59.0",
10
+ "version": "0.61.0",
11
11
  "files": [
12
12
  "dist"
13
13
  ],
@@ -35,7 +35,7 @@
35
35
  "next": "^12.2.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@lidofinance/config-prettier": "~0.59.0",
38
+ "@lidofinance/config-prettier": "~0.61.0",
39
39
  "@types/jest": "^29.0.0",
40
40
  "@types/node": "^20.0.0",
41
41
  "jest": "^29.0.0",
@@ -43,7 +43,7 @@
43
43
  "ts-jest": "^29.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@lidofinance/next-api-wrapper": "~0.59.0"
46
+ "@lidofinance/next-api-wrapper": "~0.61.0"
47
47
  },
48
48
  "publishConfig": {
49
49
  "provenance": true,