@6thbridge/utils 1.10.3 → 1.10.5
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/lib/middlewares/index.d.ts +1 -0
- package/lib/middlewares/index.js +3 -1
- package/lib/middlewares/index.js.map +1 -1
- package/lib/middlewares/rate-limiter.d.ts +18 -0
- package/lib/middlewares/rate-limiter.js +43 -0
- package/lib/middlewares/rate-limiter.js.map +1 -0
- package/lib/middlewares/rate-limiter.test.d.ts +1 -0
- package/lib/middlewares/rate-limiter.test.js +208 -0
- package/lib/middlewares/rate-limiter.test.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/readme.md +50 -0
package/lib/middlewares/index.js
CHANGED
|
@@ -33,9 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.unless = exports.express = void 0;
|
|
36
|
+
exports.unless = exports.express = exports.Ratelimiter = void 0;
|
|
37
37
|
const express = __importStar(require("./express"));
|
|
38
38
|
exports.express = express;
|
|
39
39
|
const common_1 = require("./common");
|
|
40
40
|
Object.defineProperty(exports, "unless", { enumerable: true, get: function () { return common_1.unless; } });
|
|
41
|
+
var rate_limiter_1 = require("./rate-limiter");
|
|
42
|
+
Object.defineProperty(exports, "Ratelimiter", { enumerable: true, get: function () { return rate_limiter_1.Ratelimiter; } });
|
|
41
43
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middlewares/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,mDAAqC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/middlewares/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,mDAAqC;AAG5B,0BAAO;AAFhB,qCAAkC;AAEhB,uFAFT,eAAM,OAES;AADxB,+CAAmG;AAA1F,2GAAA,WAAW,OAAA"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Request, Response } from 'express';
|
|
2
|
+
import { type RateLimitRequestHandler } from 'express-rate-limit';
|
|
3
|
+
import type Redis from 'ioredis';
|
|
4
|
+
export type RateLimiterConfiguration = {
|
|
5
|
+
redisClient: Redis;
|
|
6
|
+
serviceName?: string;
|
|
7
|
+
};
|
|
8
|
+
export type RateLimiterProps = {
|
|
9
|
+
keyPrefix?: string;
|
|
10
|
+
skipSuccessfulRequests?: boolean;
|
|
11
|
+
numberOfTries?: number;
|
|
12
|
+
keyGenerator?: (req: Request, res: Response) => string;
|
|
13
|
+
};
|
|
14
|
+
export declare class Ratelimiter {
|
|
15
|
+
private static configuration;
|
|
16
|
+
static configure(config: RateLimiterConfiguration): void;
|
|
17
|
+
static limit(config?: RateLimiterProps): RateLimitRequestHandler;
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Ratelimiter = void 0;
|
|
4
|
+
const express_rate_limit_1 = require("express-rate-limit");
|
|
5
|
+
const rate_limit_redis_1 = require("rate-limit-redis");
|
|
6
|
+
class Ratelimiter {
|
|
7
|
+
static configure(config) {
|
|
8
|
+
var _a;
|
|
9
|
+
const serviceName = (_a = config.serviceName) !== null && _a !== void 0 ? _a : process.env.APP_NAME;
|
|
10
|
+
if (!serviceName)
|
|
11
|
+
throw new Error('Ratelimiter requires serviceName or APP_NAME');
|
|
12
|
+
this.configuration = { redisClient: config.redisClient, serviceName };
|
|
13
|
+
}
|
|
14
|
+
static limit(config = {}) {
|
|
15
|
+
var _a;
|
|
16
|
+
if (!this.configuration)
|
|
17
|
+
throw new Error('Ratelimiter has not been configured');
|
|
18
|
+
const { redisClient, serviceName } = this.configuration;
|
|
19
|
+
return (0, express_rate_limit_1.rateLimit)({
|
|
20
|
+
legacyHeaders: false,
|
|
21
|
+
windowMs: 60 * 30 * 1000,
|
|
22
|
+
store: new rate_limit_redis_1.RedisStore({
|
|
23
|
+
prefix: `rl:${serviceName}:`,
|
|
24
|
+
sendCommand: (command, ...args) => redisClient.call(command, ...args),
|
|
25
|
+
}),
|
|
26
|
+
limit: config.numberOfTries || 5,
|
|
27
|
+
skipSuccessfulRequests: config.skipSuccessfulRequests || false,
|
|
28
|
+
message: {
|
|
29
|
+
error: 'Resource has been accessed too many times. Please try again after 30 minutes.',
|
|
30
|
+
statusCode: 429,
|
|
31
|
+
errorCode: 'RateLimitExceeded',
|
|
32
|
+
},
|
|
33
|
+
keyGenerator: (_a = config.keyGenerator) !== null && _a !== void 0 ? _a : function (req, res) {
|
|
34
|
+
var _a, _b, _c, _d;
|
|
35
|
+
const ip = ((_d = (_c = (_b = (_a = res.locals.ip) !== null && _a !== void 0 ? _a : req.headers['cf-connecting-ip']) !== null && _b !== void 0 ? _b : req.headers['x-forwarded-for']) !== null && _c !== void 0 ? _c : req.headers['ip']) !== null && _d !== void 0 ? _d : req.ip);
|
|
36
|
+
return `${config.keyPrefix || 'rate-limit-key'}:${(0, express_rate_limit_1.ipKeyGenerator)(ip)}`;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
exports.Ratelimiter = Ratelimiter;
|
|
42
|
+
Ratelimiter.configuration = null;
|
|
43
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/middlewares/rate-limiter.ts"],"names":[],"mappings":";;;AACA,2DAA6F;AAE7F,uDAA+D;AAc/D,MAAa,WAAW;IAGtB,MAAM,CAAC,SAAS,CAAC,MAAgC;;QAC/C,MAAM,WAAW,GAAG,MAAA,MAAM,CAAC,WAAW,mCAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC/D,IAAI,CAAC,WAAW;YAAE,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;QAElF,IAAI,CAAC,aAAa,GAAG,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,WAAW,EAAE,CAAC;IACxE,CAAC;IAED,MAAM,CAAC,KAAK,CAAC,SAA2B,EAAE;;QACxC,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;QAEhF,MAAM,EAAE,WAAW,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC;QACxD,OAAO,IAAA,8BAAS,EAAC;YACf,aAAa,EAAE,KAAK;YACpB,QAAQ,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;YACxB,KAAK,EAAE,IAAI,6BAAU,CAAC;gBACpB,MAAM,EAAE,MAAM,WAAW,GAAG;gBAC5B,WAAW,EAAE,CAAC,OAAe,EAAE,GAAG,IAAc,EAAE,EAAE,CAClD,WAAW,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAwB;aAC5D,CAAC;YACF,KAAK,EAAE,MAAM,CAAC,aAAa,IAAI,CAAC;YAChC,sBAAsB,EAAE,MAAM,CAAC,sBAAsB,IAAI,KAAK;YAC9D,OAAO,EAAE;gBACP,KAAK,EAAE,+EAA+E;gBACtF,UAAU,EAAE,GAAG;gBACf,SAAS,EAAE,mBAAmB;aAC/B;YACD,YAAY,EACV,MAAA,MAAM,CAAC,YAAY,mCACnB,UAAU,GAAG,EAAE,GAAG;;gBAChB,MAAM,EAAE,GAAG,CAAC,MAAA,MAAA,MAAA,MAAA,GAAG,CAAC,MAAM,CAAC,EAAE,mCACvB,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAC,mCAC/B,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,mCAC9B,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,mCACjB,GAAG,CAAC,EAAE,CAAW,CAAC;gBACpB,OAAO,GAAG,MAAM,CAAC,SAAS,IAAI,gBAAgB,IAAI,IAAA,mCAAc,EAAC,EAAE,CAAC,EAAE,CAAC;YACzE,CAAC;SACJ,CAAC,CAAC;IACL,CAAC;;AAxCH,kCAyCC;AAxCgB,yBAAa,GAA8C,IAAI,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const node_events_1 = require("node:events");
|
|
16
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
17
|
+
const rate_limiter_1 = require("./rate-limiter");
|
|
18
|
+
describe('Ratelimiter.limit', () => {
|
|
19
|
+
const originalAppName = process.env.APP_NAME;
|
|
20
|
+
let redisClient;
|
|
21
|
+
let call;
|
|
22
|
+
let counters;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.useFakeTimers();
|
|
25
|
+
redisClient = new ioredis_1.default({ lazyConnect: true });
|
|
26
|
+
counters = new Map();
|
|
27
|
+
// Keep the real middleware and Redis store; replace only Redis I/O.
|
|
28
|
+
call = jest.spyOn(redisClient, 'call').mockImplementation((command, ...args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
+
if (command === 'SCRIPT' && args[0] === 'LOAD') {
|
|
30
|
+
return args[1].includes('"INCR"') ? 'increment-sha' : 'get-sha';
|
|
31
|
+
}
|
|
32
|
+
if (command === 'EVALSHA') {
|
|
33
|
+
const [script, , key, windowMs] = args;
|
|
34
|
+
let counter = counters.get(key);
|
|
35
|
+
if (script === 'increment-sha') {
|
|
36
|
+
if (!counter || counter.expiresAt <= Date.now()) {
|
|
37
|
+
counter = { hits: 0, expiresAt: Date.now() + Number(windowMs) };
|
|
38
|
+
counters.set(key, counter);
|
|
39
|
+
}
|
|
40
|
+
counter.hits++;
|
|
41
|
+
}
|
|
42
|
+
return counter ? [counter.hits, counter.expiresAt - Date.now()] : [false, -2];
|
|
43
|
+
}
|
|
44
|
+
if (command === 'DECR') {
|
|
45
|
+
const counter = counters.get(args[0]);
|
|
46
|
+
if (counter)
|
|
47
|
+
return --counter.hits;
|
|
48
|
+
return -1;
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Unexpected Redis command: ${command}`);
|
|
51
|
+
}));
|
|
52
|
+
rate_limiter_1.Ratelimiter.configure({ redisClient, serviceName: 'test-service' });
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
if (originalAppName === undefined)
|
|
56
|
+
delete process.env.APP_NAME;
|
|
57
|
+
else
|
|
58
|
+
process.env.APP_NAME = originalAppName;
|
|
59
|
+
redisClient.disconnect();
|
|
60
|
+
jest.restoreAllMocks();
|
|
61
|
+
jest.useRealTimers();
|
|
62
|
+
});
|
|
63
|
+
function request(overrides = {}) {
|
|
64
|
+
return Object.assign({ ip: '203.0.113.10', headers: {} }, overrides);
|
|
65
|
+
}
|
|
66
|
+
function run(limiter_1) {
|
|
67
|
+
return __awaiter(this, arguments, void 0, function* (limiter, req = request(), locals = {}) {
|
|
68
|
+
const res = Object.assign(new node_events_1.EventEmitter(), {
|
|
69
|
+
locals,
|
|
70
|
+
statusCode: 200,
|
|
71
|
+
headersSent: false,
|
|
72
|
+
setHeader: jest.fn(),
|
|
73
|
+
status: jest.fn().mockReturnThis(),
|
|
74
|
+
send: jest.fn().mockReturnThis(),
|
|
75
|
+
});
|
|
76
|
+
const next = jest.fn();
|
|
77
|
+
yield limiter(req, res, next);
|
|
78
|
+
return { res, next };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function limiter(options = {}) {
|
|
82
|
+
return rate_limiter_1.Ratelimiter.limit(options);
|
|
83
|
+
}
|
|
84
|
+
it('requires configuration before creating a limiter', () => {
|
|
85
|
+
jest.isolateModules(() => {
|
|
86
|
+
const { Ratelimiter: UnconfiguredRatelimiter } = require('./rate-limiter');
|
|
87
|
+
expect(() => UnconfiguredRatelimiter.limit({})).toThrow('Ratelimiter has not been configured');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it('uses APP_NAME as the service prefix when serviceName is omitted', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
process.env.APP_NAME = 'env-service';
|
|
92
|
+
rate_limiter_1.Ratelimiter.configure({ redisClient });
|
|
93
|
+
yield run(limiter());
|
|
94
|
+
expect(call).toHaveBeenCalledWith('EVALSHA', 'increment-sha', '1', 'rl:env-service:rate-limit-key:203.0.113.10', '1800000');
|
|
95
|
+
}));
|
|
96
|
+
it('prefers an explicit serviceName over APP_NAME', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
97
|
+
process.env.APP_NAME = 'env-service';
|
|
98
|
+
rate_limiter_1.Ratelimiter.configure({ redisClient, serviceName: 'explicit-service' });
|
|
99
|
+
yield run(limiter());
|
|
100
|
+
expect(call).toHaveBeenCalledWith('EVALSHA', 'increment-sha', '1', 'rl:explicit-service:rate-limit-key:203.0.113.10', '1800000');
|
|
101
|
+
}));
|
|
102
|
+
it('requires a serviceName or APP_NAME to avoid sharing an unnamed service prefix', () => {
|
|
103
|
+
delete process.env.APP_NAME;
|
|
104
|
+
expect(() => rate_limiter_1.Ratelimiter.configure({ redisClient })).toThrow('Ratelimiter requires serviceName or APP_NAME');
|
|
105
|
+
});
|
|
106
|
+
it('isolates services using the same Redis client, including custom keys', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
107
|
+
const options = { numberOfTries: 1, keyGenerator: () => 'same-user' };
|
|
108
|
+
const firstService = limiter(options);
|
|
109
|
+
rate_limiter_1.Ratelimiter.configure({ redisClient, serviceName: 'another-service' });
|
|
110
|
+
const secondService = limiter(options);
|
|
111
|
+
expect((yield run(firstService)).next).toHaveBeenCalledWith();
|
|
112
|
+
expect((yield run(secondService)).next).toHaveBeenCalledWith();
|
|
113
|
+
expect((yield run(firstService)).res.status).toHaveBeenCalledWith(429);
|
|
114
|
+
expect((yield run(secondService)).res.status).toHaveBeenCalledWith(429);
|
|
115
|
+
}));
|
|
116
|
+
it('allows five requests by default and blocks the sixth with the existing error response', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
const middleware = rate_limiter_1.Ratelimiter.limit();
|
|
118
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
119
|
+
const { res, next } = yield run(middleware);
|
|
120
|
+
expect(next).toHaveBeenCalledWith();
|
|
121
|
+
expect(res.send).not.toHaveBeenCalled();
|
|
122
|
+
expect(res.setHeader).not.toHaveBeenCalled();
|
|
123
|
+
}
|
|
124
|
+
const { res, next } = yield run(middleware);
|
|
125
|
+
expect(next).not.toHaveBeenCalled();
|
|
126
|
+
expect(res.status).toHaveBeenCalledWith(429);
|
|
127
|
+
expect(res.send).toHaveBeenCalledWith({
|
|
128
|
+
error: 'Resource has been accessed too many times. Please try again after 30 minutes.',
|
|
129
|
+
statusCode: 429,
|
|
130
|
+
errorCode: 'RateLimitExceeded',
|
|
131
|
+
});
|
|
132
|
+
}));
|
|
133
|
+
it('uses the configured attempt limit and resets after 30 minutes', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
134
|
+
const middleware = limiter({ numberOfTries: 1 });
|
|
135
|
+
expect((yield run(middleware)).next).toHaveBeenCalledWith();
|
|
136
|
+
jest.advanceTimersByTime(30 * 60 * 1000 - 1);
|
|
137
|
+
expect((yield run(middleware)).res.status).toHaveBeenCalledWith(429);
|
|
138
|
+
jest.advanceTimersByTime(1);
|
|
139
|
+
expect((yield run(middleware)).next).toHaveBeenCalledWith();
|
|
140
|
+
}));
|
|
141
|
+
it.each([
|
|
142
|
+
{ skipSuccessfulRequests: false, statusCode: 200, blocked: true },
|
|
143
|
+
{ skipSuccessfulRequests: true, statusCode: 200, blocked: false },
|
|
144
|
+
{ skipSuccessfulRequests: true, statusCode: 400, blocked: true },
|
|
145
|
+
])('counts finished requests with $skipSuccessfulRequests and status $statusCode', (_a) => __awaiter(void 0, [_a], void 0, function* ({ skipSuccessfulRequests, statusCode, blocked }) {
|
|
146
|
+
const middleware = limiter({ numberOfTries: 1, skipSuccessfulRequests });
|
|
147
|
+
const { res } = yield run(middleware);
|
|
148
|
+
res.statusCode = statusCode;
|
|
149
|
+
res.emit('finish');
|
|
150
|
+
yield jest.advanceTimersByTimeAsync(0);
|
|
151
|
+
const result = yield run(middleware);
|
|
152
|
+
if (blocked) {
|
|
153
|
+
expect(result.res.status).toHaveBeenCalledWith(429);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
expect(result.next).toHaveBeenCalledWith();
|
|
157
|
+
}
|
|
158
|
+
}));
|
|
159
|
+
it.each([
|
|
160
|
+
{ locals: { ip: '203.0.113.11' }, headers: { 'cf-connecting-ip': '203.0.113.12', 'x-forwarded-for': '203.0.113.13', ip: '203.0.113.14' }, expectedIp: '203.0.113.11' },
|
|
161
|
+
{ locals: {}, headers: { 'cf-connecting-ip': '203.0.113.12', 'x-forwarded-for': '203.0.113.13', ip: '203.0.113.14' }, expectedIp: '203.0.113.12' },
|
|
162
|
+
{ locals: {}, headers: { 'x-forwarded-for': '203.0.113.13', ip: '203.0.113.14' }, expectedIp: '203.0.113.13' },
|
|
163
|
+
{ locals: {}, headers: { ip: '203.0.113.14' }, expectedIp: '203.0.113.14' },
|
|
164
|
+
{ locals: {}, headers: {}, expectedIp: '203.0.113.10' },
|
|
165
|
+
])('selects $expectedIp using IP precedence', (_a) => __awaiter(void 0, [_a], void 0, function* ({ locals, headers, expectedIp }) {
|
|
166
|
+
const middleware = limiter({ numberOfTries: 1 });
|
|
167
|
+
yield run(middleware, request({ headers }), locals);
|
|
168
|
+
const { res } = yield run(middleware, request({ ip: expectedIp }));
|
|
169
|
+
expect(res.status).toHaveBeenCalledWith(429);
|
|
170
|
+
}));
|
|
171
|
+
it('normalizes IPv6 addresses to the same subnet key', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
172
|
+
const middleware = limiter({ numberOfTries: 1 });
|
|
173
|
+
yield run(middleware, request({ ip: '2001:db8:abcd:1200::1' }));
|
|
174
|
+
const { res } = yield run(middleware, request({ ip: '2001:db8:abcd:12ff::2' }));
|
|
175
|
+
expect(res.status).toHaveBeenCalledWith(429);
|
|
176
|
+
expect(call).toHaveBeenCalledWith('EVALSHA', 'increment-sha', '1', 'rl:test-service:rate-limit-key:2001:db8:abcd:1200::/56', '1800000');
|
|
177
|
+
}));
|
|
178
|
+
it('isolates default and custom prefixes on a shared client', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
179
|
+
const defaultLimiter = limiter({ numberOfTries: 1 });
|
|
180
|
+
const loginLimiter = limiter({ numberOfTries: 1, keyPrefix: 'login' });
|
|
181
|
+
yield run(defaultLimiter);
|
|
182
|
+
expect((yield run(loginLimiter)).next).toHaveBeenCalledWith();
|
|
183
|
+
expect((yield run(defaultLimiter)).res.status).toHaveBeenCalledWith(429);
|
|
184
|
+
expect((yield run(loginLimiter)).res.status).toHaveBeenCalledWith(429);
|
|
185
|
+
}));
|
|
186
|
+
it('lets custom keys replace the prefix and IP key generation', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
187
|
+
const middleware = limiter({
|
|
188
|
+
numberOfTries: 1,
|
|
189
|
+
keyPrefix: 'ignored',
|
|
190
|
+
keyGenerator: (req, res) => `${res.locals.clientId}:${req.body.email}`,
|
|
191
|
+
});
|
|
192
|
+
yield run(middleware, request({ body: { email: 'user@example.com' } }), { clientId: 'client-1' });
|
|
193
|
+
const { res } = yield run(middleware, request({ ip: '203.0.113.99', body: { email: 'user@example.com' } }), { clientId: 'client-1' });
|
|
194
|
+
expect(res.status).toHaveBeenCalledWith(429);
|
|
195
|
+
expect(call).toHaveBeenCalledWith('EVALSHA', 'increment-sha', '1', 'rl:test-service:client-1:user@example.com', '1800000');
|
|
196
|
+
}));
|
|
197
|
+
it('forwards Redis failures to next without allowing the request', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
198
|
+
const middleware = limiter();
|
|
199
|
+
yield run(middleware);
|
|
200
|
+
const error = new Error('Redis unavailable');
|
|
201
|
+
call.mockRejectedValueOnce(error);
|
|
202
|
+
const { res, next } = yield run(middleware);
|
|
203
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(next).toHaveBeenCalledWith(error);
|
|
205
|
+
expect(res.send).not.toHaveBeenCalled();
|
|
206
|
+
}));
|
|
207
|
+
});
|
|
208
|
+
//# sourceMappingURL=rate-limiter.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.test.js","sourceRoot":"","sources":["../../src/middlewares/rate-limiter.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;AAAA,6CAA2C;AAE3C,sDAA4B;AAC5B,iDAAoE;AAEpE,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC7C,IAAI,WAAkB,CAAC;IACvB,IAAI,IAAsB,CAAC;IAC3B,IAAI,QAA0D,CAAC;IAE/D,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,WAAW,GAAG,IAAI,iBAAK,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC;QACrB,oEAAoE;QACpE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,kBAAkB,CAAC,CAAO,OAAO,EAAE,GAAG,IAAI,EAAE,EAAE;YACnF,IAAI,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;gBAC/C,OAAQ,IAAI,CAAC,CAAC,CAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS,CAAC;YAC9E,CAAC;YACD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,MAAM,CAAC,MAAM,EAAE,AAAD,EAAG,GAAG,EAAE,QAAQ,CAAC,GAAG,IAAgB,CAAC;gBACnD,IAAI,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAChC,IAAI,MAAM,KAAK,eAAe,EAAE,CAAC;oBAC/B,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;wBAChD,OAAO,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;wBAChE,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;oBAC7B,CAAC;oBACD,OAAO,CAAC,IAAI,EAAE,CAAC;gBACjB,CAAC;gBACD,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YAChF,CAAC;YACD,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;gBACvB,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAW,CAAC,CAAC;gBAChD,IAAI,OAAO;oBAAE,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC;gBACnC,OAAO,CAAC,CAAC,CAAC;YACZ,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,6BAA6B,OAAO,EAAE,CAAC,CAAC;QAC1D,CAAC,CAAA,CAAC,CAAC;QACH,0BAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,eAAe,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;;YAC1D,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,eAAe,CAAC;QAC5C,WAAW,CAAC,UAAU,EAAE,CAAC;QACzB,IAAI,CAAC,eAAe,EAAE,CAAC;QACvB,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,SAAS,OAAO,CAAC,YAA8B,EAAE;QAC/C,OAAO,gBAAE,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,IAAK,SAAS,CAAa,CAAC;IACtE,CAAC;IAED,SAAe,GAAG;6DAChB,OAA6C,EAC7C,GAAG,GAAG,OAAO,EAAE,EACf,SAA6B,EAAE;YAE/B,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,0BAAY,EAAE,EAAE;gBAC5C,MAAM;gBACN,UAAU,EAAE,GAAG;gBACf,WAAW,EAAE,KAAK;gBAClB,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE;gBACpB,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,cAAc,EAAE;gBAClC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,cAAc,EAAE;aACjC,CAAwB,CAAC;YAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,EAAE,EAAE,CAAC;YACvB,MAAM,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;YAC9B,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;QACvB,CAAC;KAAA;IAED,SAAS,OAAO,CAAC,UAA4B,EAAE;QAC7C,OAAO,0BAAW,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IAED,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE;YACvB,MAAM,EAAE,WAAW,EAAE,uBAAuB,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;YAC3E,MAAM,CAAC,GAAG,EAAE,CAAC,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,qCAAqC,CAAC,CAAC;QACjG,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAS,EAAE;QAC/E,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,aAAa,CAAC;QACrC,0BAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC;QACvC,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAErB,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,EAAE,4CAA4C,EAAE,SAAS,CAAC,CAAC;IAC9H,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAS,EAAE;QAC7D,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,aAAa,CAAC;QACrC,0BAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,kBAAkB,EAAE,CAAC,CAAC;QACxE,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;QAErB,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,EAAE,iDAAiD,EAAE,SAAS,CAAC,CAAC;IACnI,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;QAC5B,MAAM,CAAC,GAAG,EAAE,CAAC,0BAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,8CAA8C,CAAC,CAAC;IAC/G,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,GAAS,EAAE;QACpF,MAAM,OAAO,GAAqB,EAAE,aAAa,EAAE,CAAC,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;QACxF,MAAM,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QACtC,0BAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACvE,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QAEvC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;QAC9D,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;QAC/D,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QACvE,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;IAC1E,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,uFAAuF,EAAE,GAAS,EAAE;QACrG,MAAM,UAAU,GAAG,0BAAW,CAAC,KAAK,EAAE,CAAC;QACvC,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,EAAE,EAAE,CAAC;YAC7C,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC;YAC5C,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;YACpC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;YACxC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QAC/C,CAAC;QAED,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC;YACpC,KAAK,EAAE,+EAA+E;YACtF,UAAU,EAAE,GAAG;YACf,SAAS,EAAE,mBAAmB;SAC/B,CAAC,CAAC;IACL,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAS,EAAE;QAC7E,MAAM,UAAU,GAAG,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC;QACjD,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;QAC5D,IAAI,CAAC,mBAAmB,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC;QAC7C,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QACrE,IAAI,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;QAC5B,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;IAC9D,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC;QACN,EAAE,sBAAsB,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;QACjE,EAAE,sBAAsB,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE;QACjE,EAAE,sBAAsB,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;KACjE,CAAC,CAAC,8EAA8E,EAAE,KAAwD,EAAE,4CAAnD,EAAE,sBAAsB,EAAE,UAAU,EAAE,OAAO,EAAE;QACvI,MAAM,UAAU,GAAG,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,sBAAsB,EAAE,CAAC,CAAC;QACzE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC;QACtC,GAAG,CAAC,UAAU,GAAG,UAAU,CAAC;QAC5B,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnB,MAAM,IAAI,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC;QAEvC,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC;QACrC,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QACtD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;QAC7C,CAAC;IACH,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,IAAI,CAAC;QACN,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE;QACtK,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,kBAAkB,EAAE,cAAc,EAAE,iBAAiB,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE;QAClJ,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,iBAAiB,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE;QAC9G,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,cAAc,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE;QAC3E,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,cAAc,EAAE;KACxD,CAAC,CAAC,yCAAyC,EAAE,KAAwC,EAAE,4CAAnC,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE;QAClF,MAAM,UAAU,GAAG,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC;QACjD,MAAM,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;QACpD,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;QACnE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;IAC/C,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAS,EAAE;QAChE,MAAM,UAAU,GAAG,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC;QACjD,MAAM,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC;QAChE,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC;QAChF,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,EAAE,wDAAwD,EAAE,SAAS,CAAC,CAAC;IAC1I,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAS,EAAE;QACvE,MAAM,cAAc,GAAG,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC,CAAC;QACrD,MAAM,YAAY,GAAG,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;QACvE,MAAM,GAAG,CAAC,cAAc,CAAC,CAAC;QAC1B,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;QAC9D,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QACzE,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACzE,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAS,EAAE;QACzE,MAAM,UAAU,GAAG,OAAO,CAAC;YACzB,aAAa,EAAE,CAAC;YAChB,SAAS,EAAE,SAAS;YACpB,YAAY,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE;SAC1F,CAAC,CAAC;QACH,MAAM,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;QAClG,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAC;QACtI,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,oBAAoB,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,EAAE,2CAA2C,EAAE,SAAS,CAAC,CAAC;IAC7H,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,GAAS,EAAE;QAC5E,MAAM,UAAU,GAAG,OAAO,EAAE,CAAC;QAC7B,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC;QACtB,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC7C,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC;QAElC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC;QAC5C,MAAM,CAAC,IAAI,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,CAAC,oBAAoB,CAAC,KAAK,CAAC,CAAC;QACzC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC1C,CAAC,CAAA,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|