@devbro/pashmak 0.1.36 → 0.1.38
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/DatabaseServiceProvider.mjs +0 -3
- package/dist/DatabaseServiceProvider.mjs.map +1 -1
- package/dist/bin/DatabaseServiceProvider.cjs +0 -3
- package/dist/bin/index.cjs +0 -3
- package/dist/bin/middlewares.cjs +61 -4
- package/dist/middlewares.d.mts +45 -2
- package/dist/middlewares.mjs +60 -1
- package/dist/middlewares.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/DatabaseServiceProvider.mts"],"sourcesContent":["import { Middleware } from \"@devbro/neko-router\";\nimport { Request, Response } from \"@devbro/neko-router\";\nimport { PostgresqlConnection } from \"@devbro/neko-sql\";\nimport { PoolConfig } from \"pg\";\nimport { Connection } from \"@devbro/neko-sql\";\nimport { BaseModel } from \"@devbro/neko-orm\";\nimport { ctx } from \"@devbro/neko-context\";\nimport { config } from \"@devbro/neko-config\";\nimport { Global } from \"./global.mjs\";\n\nexport class DatabaseServiceProvider extends Middleware {\n async call(\n req: Request,\n res: Response,\n next: () => Promise<void>,\n ): Promise<void> {\n const db_configs: Record<string, PoolConfig & { name: string }> =\n config.get(\"databases\");\n\n const conns = [];\n try {\n for (const [name, db_config] of Object.entries(db_configs)) {\n const conn = await this.getConnection(db_config);\n ctx().set([\"database\", name], conn);\n conns.push(conn);\n }\n BaseModel.setConnection(() => {\n const key = [\"database\", \"default\"];\n let rc: Connection | undefined;\n\n if (ctx.isActive()) {\n rc = ctx().get<Connection>(key);\n } else if (Global.has(key)) {\n rc = Global.get<Connection>(key);\n } else {\n rc = this.getConnection(db_configs[\"default\"]);\n Global.set(key, rc);\n }\n\n return rc!;\n });\n await next();\n } finally {\n for (const conn of conns) {\n await conn.disconnect();\n }\n }\n }\n\n private static instance: DatabaseServiceProvider;\n\n async register(): Promise<void> {}\n\n static getInstance(): DatabaseServiceProvider {\n if (!DatabaseServiceProvider.instance) {\n DatabaseServiceProvider.instance = new DatabaseServiceProvider();\n }\n return DatabaseServiceProvider.instance;\n }\n\n getConnection(db_config: PoolConfig): Connection {\n const conn = new PostgresqlConnection(db_config);\n
|
|
1
|
+
{"version":3,"sources":["../src/DatabaseServiceProvider.mts"],"sourcesContent":["import { Middleware } from \"@devbro/neko-router\";\nimport { Request, Response } from \"@devbro/neko-router\";\nimport { PostgresqlConnection } from \"@devbro/neko-sql\";\nimport { PoolConfig } from \"pg\";\nimport { Connection } from \"@devbro/neko-sql\";\nimport { BaseModel } from \"@devbro/neko-orm\";\nimport { ctx } from \"@devbro/neko-context\";\nimport { config } from \"@devbro/neko-config\";\nimport { Global } from \"./global.mjs\";\n\nexport class DatabaseServiceProvider extends Middleware {\n async call(\n req: Request,\n res: Response,\n next: () => Promise<void>,\n ): Promise<void> {\n const db_configs: Record<string, PoolConfig & { name: string }> =\n config.get(\"databases\");\n\n const conns = [];\n try {\n for (const [name, db_config] of Object.entries(db_configs)) {\n const conn = await this.getConnection(db_config);\n ctx().set([\"database\", name], conn);\n conns.push(conn);\n }\n BaseModel.setConnection(() => {\n const key = [\"database\", \"default\"];\n let rc: Connection | undefined;\n\n if (ctx.isActive()) {\n rc = ctx().get<Connection>(key);\n } else if (Global.has(key)) {\n rc = Global.get<Connection>(key);\n } else {\n rc = this.getConnection(db_configs[\"default\"]);\n Global.set(key, rc);\n }\n\n return rc!;\n });\n await next();\n } finally {\n for (const conn of conns) {\n await conn.disconnect();\n }\n }\n }\n\n private static instance: DatabaseServiceProvider;\n\n async register(): Promise<void> {}\n\n static getInstance(): DatabaseServiceProvider {\n if (!DatabaseServiceProvider.instance) {\n DatabaseServiceProvider.instance = new DatabaseServiceProvider();\n }\n return DatabaseServiceProvider.instance;\n }\n\n getConnection(db_config: PoolConfig): Connection {\n const conn = new PostgresqlConnection(db_config);\n return conn;\n }\n}\n"],"mappings":";;AAAA,SAAS,kBAAkB;AAE3B,SAAS,4BAA4B;AAGrC,SAAS,iBAAiB;AAC1B,SAAS,WAAW;AACpB,SAAS,cAAc;AACvB,SAAS,cAAc;AAEhB,MAAM,gCAAgC,WAAW;AAAA,EAVxD,OAUwD;AAAA;AAAA;AAAA,EACtD,MAAM,KACJ,KACA,KACA,MACe;AACf,UAAM,aACJ,OAAO,IAAI,WAAW;AAExB,UAAM,QAAQ,CAAC;AACf,QAAI;AACF,iBAAW,CAAC,MAAM,SAAS,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC1D,cAAM,OAAO,MAAM,KAAK,cAAc,SAAS;AAC/C,YAAI,EAAE,IAAI,CAAC,YAAY,IAAI,GAAG,IAAI;AAClC,cAAM,KAAK,IAAI;AAAA,MACjB;AACA,gBAAU,cAAc,MAAM;AAC5B,cAAM,MAAM,CAAC,YAAY,SAAS;AAClC,YAAI;AAEJ,YAAI,IAAI,SAAS,GAAG;AAClB,eAAK,IAAI,EAAE,IAAgB,GAAG;AAAA,QAChC,WAAW,OAAO,IAAI,GAAG,GAAG;AAC1B,eAAK,OAAO,IAAgB,GAAG;AAAA,QACjC,OAAO;AACL,eAAK,KAAK,cAAc,WAAW,SAAS,CAAC;AAC7C,iBAAO,IAAI,KAAK,EAAE;AAAA,QACpB;AAEA,eAAO;AAAA,MACT,CAAC;AACD,YAAM,KAAK;AAAA,IACb,UAAE;AACA,iBAAW,QAAQ,OAAO;AACxB,cAAM,KAAK,WAAW;AAAA,MACxB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,OAAe;AAAA,EAEf,MAAM,WAA0B;AAAA,EAAC;AAAA,EAEjC,OAAO,cAAuC;AAC5C,QAAI,CAAC,wBAAwB,UAAU;AACrC,8BAAwB,WAAW,IAAI,wBAAwB;AAAA,IACjE;AACA,WAAO,wBAAwB;AAAA,EACjC;AAAA,EAEA,cAAc,WAAmC;AAC/C,UAAM,OAAO,IAAI,qBAAqB,SAAS;AAC/C,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -129,9 +129,6 @@ var DatabaseServiceProvider = class _DatabaseServiceProvider extends Middleware
|
|
|
129
129
|
}
|
|
130
130
|
getConnection(db_config) {
|
|
131
131
|
const conn = new import_neko_sql.PostgresqlConnection(db_config);
|
|
132
|
-
if (!conn.isConnected()) {
|
|
133
|
-
conn.connect();
|
|
134
|
-
}
|
|
135
132
|
return conn;
|
|
136
133
|
}
|
|
137
134
|
};
|
package/dist/bin/index.cjs
CHANGED
|
@@ -2935,9 +2935,6 @@ var init_DatabaseServiceProvider = __esm({
|
|
|
2935
2935
|
}
|
|
2936
2936
|
getConnection(db_config) {
|
|
2937
2937
|
const conn = new import_neko_sql2.PostgresqlConnection(db_config);
|
|
2938
|
-
if (!conn.isConnected()) {
|
|
2939
|
-
conn.connect();
|
|
2940
|
-
}
|
|
2941
2938
|
return conn;
|
|
2942
2939
|
}
|
|
2943
2940
|
};
|
package/dist/bin/middlewares.cjs
CHANGED
|
@@ -32,15 +32,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
32
32
|
// src/middlewares.mts
|
|
33
33
|
var middlewares_exports = {};
|
|
34
34
|
__export(middlewares_exports, {
|
|
35
|
+
RateLimiterMiddleware: () => RateLimiterMiddleware,
|
|
35
36
|
cors: () => cors,
|
|
36
37
|
dbTransaction: () => dbTransaction
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(middlewares_exports);
|
|
39
40
|
|
|
40
|
-
// src/router.mts
|
|
41
|
-
var import_neko_context = require("@devbro/neko-context");
|
|
42
|
-
var import_errors = require("@devbro/neko-http/errors");
|
|
43
|
-
|
|
44
41
|
// ../neko-router/dist/CompiledRoute.mjs
|
|
45
42
|
var import_stream = require("stream");
|
|
46
43
|
|
|
@@ -457,6 +454,8 @@ var Router = class {
|
|
|
457
454
|
};
|
|
458
455
|
|
|
459
456
|
// src/router.mts
|
|
457
|
+
var import_neko_context = require("@devbro/neko-context");
|
|
458
|
+
var import_errors = require("@devbro/neko-http/errors");
|
|
460
459
|
var import_neko_config = require("@devbro/neko-config");
|
|
461
460
|
|
|
462
461
|
// src/facades.mts
|
|
@@ -756,6 +755,7 @@ var cache = (0, import_neko_helper2.createSingleton)((label) => {
|
|
|
756
755
|
});
|
|
757
756
|
|
|
758
757
|
// src/middlewares.mts
|
|
758
|
+
var import_neko_http = require("@devbro/neko-http");
|
|
759
759
|
function cors(options = {}) {
|
|
760
760
|
return async (req, res, next) => {
|
|
761
761
|
const allowedOrigins = options.allowedOrigins || ["*"];
|
|
@@ -787,8 +787,65 @@ async function dbTransaction(req, res, next) {
|
|
|
787
787
|
}
|
|
788
788
|
}
|
|
789
789
|
__name(dbTransaction, "dbTransaction");
|
|
790
|
+
var RateLimiterMiddleware = class _RateLimiterMiddleware extends Middleware {
|
|
791
|
+
constructor(params) {
|
|
792
|
+
super();
|
|
793
|
+
this.params = params;
|
|
794
|
+
}
|
|
795
|
+
static {
|
|
796
|
+
__name(this, "RateLimiterMiddleware");
|
|
797
|
+
}
|
|
798
|
+
static singletonInstance = void 0;
|
|
799
|
+
static getInstance(params = {}) {
|
|
800
|
+
if (_RateLimiterMiddleware.singletonInstance) {
|
|
801
|
+
return _RateLimiterMiddleware.singletonInstance;
|
|
802
|
+
}
|
|
803
|
+
let default_params = {
|
|
804
|
+
generateIdentifier: /* @__PURE__ */ __name((req) => req.socket.remoteAddress || "unknown", "generateIdentifier"),
|
|
805
|
+
maxRequests: 200,
|
|
806
|
+
windowTimeSize: 30,
|
|
807
|
+
windowCount: 4
|
|
808
|
+
};
|
|
809
|
+
const merged_params = { ...default_params, ...params };
|
|
810
|
+
return _RateLimiterMiddleware.singletonInstance = new _RateLimiterMiddleware(
|
|
811
|
+
merged_params
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
async call(req, res, next) {
|
|
815
|
+
let window = parseInt(
|
|
816
|
+
(Date.now() / 1e3 / this.params.windowTimeSize).toString()
|
|
817
|
+
);
|
|
818
|
+
let key = `rate_limiter:${this.params.generateIdentifier(req)}:${window}`;
|
|
819
|
+
let count = await cache().get(key) || 0;
|
|
820
|
+
if (!count) {
|
|
821
|
+
await cache().put(
|
|
822
|
+
key,
|
|
823
|
+
1,
|
|
824
|
+
this.params.windowTimeSize * (this.params.windowCount + 1)
|
|
825
|
+
);
|
|
826
|
+
count = 1;
|
|
827
|
+
} else {
|
|
828
|
+
count = await cache().increment(key, 1);
|
|
829
|
+
}
|
|
830
|
+
for (let i = 1; i < this.params.windowCount; i++) {
|
|
831
|
+
count += parseInt(
|
|
832
|
+
await cache().get(
|
|
833
|
+
`rate_limiter:${this.params.generateIdentifier(req)}:${window - i}`
|
|
834
|
+
) || "0"
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
if (count > this.params.maxRequests) {
|
|
838
|
+
throw new import_neko_http.HttpTooManyRequestsError(
|
|
839
|
+
"Too many requests. Please try again later."
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
await next();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
};
|
|
790
846
|
// Annotate the CommonJS export names for ESM import in node:
|
|
791
847
|
0 && (module.exports = {
|
|
848
|
+
RateLimiterMiddleware,
|
|
792
849
|
cors,
|
|
793
850
|
dbTransaction
|
|
794
851
|
});
|
package/dist/middlewares.d.mts
CHANGED
|
@@ -1,8 +1,51 @@
|
|
|
1
|
-
import { Request, Response } from '@devbro/neko-router';
|
|
1
|
+
import { Request, Response, Middleware } from '@devbro/neko-router';
|
|
2
2
|
|
|
3
3
|
declare function cors(options?: {
|
|
4
4
|
allowedOrigins?: (string | RegExp)[];
|
|
5
5
|
}): (req: Request, res: Response, next: () => Promise<void>) => Promise<void>;
|
|
6
6
|
declare function dbTransaction(req: Request, res: Response, next: () => Promise<void>): Promise<void>;
|
|
7
|
+
type RateLimiterMiddlewareParams = {
|
|
8
|
+
generateIdentifier: (req: Request) => string;
|
|
9
|
+
maxRequests: number;
|
|
10
|
+
windowTimeSize: number;
|
|
11
|
+
windowCount: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Rate limiter middleware using a sliding window algorithm.
|
|
15
|
+
*
|
|
16
|
+
* This middleware tracks request counts across multiple time windows to prevent abuse.
|
|
17
|
+
* It uses atomic cache increments to ensure accurate counting in concurrent environments.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // Basic usage with defaults (200 requests per 2 minutes)
|
|
22
|
+
* authnedRouter.addGlobalMiddleware(RateLimiterMiddleware);
|
|
23
|
+
*
|
|
24
|
+
* // Custom configuration
|
|
25
|
+
* authnedRouter.addGlobalMiddleware(RateLimiterMiddleware.getInstance({
|
|
26
|
+
* generateIdentifier: (req) => req.headers['x-api-key'] || req.socket.remoteAddress,
|
|
27
|
+
* maxRequests: 200, // Allow 200 requests
|
|
28
|
+
* windowTimeSize: 30, // in 30 second windows
|
|
29
|
+
* windowCount: 4 // looking back 4 windows (total: 120 seconds)
|
|
30
|
+
* }));
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @param params.generateIdentifier - Function to generate unique identifier for rate limiting (default: IP address)
|
|
34
|
+
* @param params.maxRequests - Maximum number of requests allowed across all windows (default: 200)
|
|
35
|
+
* @param params.windowTimeSize - Size of each time window in seconds (default: 30)
|
|
36
|
+
* @param params.windowCount - Number of previous windows to check (default: 4)
|
|
37
|
+
*
|
|
38
|
+
* The total time period checked is `windowTimeSize * windowCount` seconds.
|
|
39
|
+
* Default configuration: 200 requests per 120 seconds (30s × 4 windows).
|
|
40
|
+
*
|
|
41
|
+
* @throws {HttpTooManyRequestsError} When rate limit is exceeded
|
|
42
|
+
*/
|
|
43
|
+
declare class RateLimiterMiddleware extends Middleware {
|
|
44
|
+
private params;
|
|
45
|
+
static singletonInstance: Middleware | undefined;
|
|
46
|
+
static getInstance(params?: Partial<RateLimiterMiddlewareParams>): Middleware;
|
|
47
|
+
constructor(params: RateLimiterMiddlewareParams);
|
|
48
|
+
call(req: Request, res: Response, next: () => Promise<void>): Promise<void>;
|
|
49
|
+
}
|
|
7
50
|
|
|
8
|
-
export { cors, dbTransaction };
|
|
51
|
+
export { RateLimiterMiddleware, type RateLimiterMiddlewareParams, cors, dbTransaction };
|
package/dist/middlewares.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
-
import {
|
|
3
|
+
import { Middleware } from "@devbro/neko-router";
|
|
4
|
+
import { db, cache } from "./facades.mjs";
|
|
5
|
+
import { HttpTooManyRequestsError } from "@devbro/neko-http";
|
|
4
6
|
function cors(options = {}) {
|
|
5
7
|
return async (req, res, next) => {
|
|
6
8
|
const allowedOrigins = options.allowedOrigins || ["*"];
|
|
@@ -32,7 +34,64 @@ async function dbTransaction(req, res, next) {
|
|
|
32
34
|
}
|
|
33
35
|
}
|
|
34
36
|
__name(dbTransaction, "dbTransaction");
|
|
37
|
+
class RateLimiterMiddleware extends Middleware {
|
|
38
|
+
constructor(params) {
|
|
39
|
+
super();
|
|
40
|
+
this.params = params;
|
|
41
|
+
}
|
|
42
|
+
static {
|
|
43
|
+
__name(this, "RateLimiterMiddleware");
|
|
44
|
+
}
|
|
45
|
+
static singletonInstance = void 0;
|
|
46
|
+
static getInstance(params = {}) {
|
|
47
|
+
if (RateLimiterMiddleware.singletonInstance) {
|
|
48
|
+
return RateLimiterMiddleware.singletonInstance;
|
|
49
|
+
}
|
|
50
|
+
let default_params = {
|
|
51
|
+
generateIdentifier: /* @__PURE__ */ __name((req) => req.socket.remoteAddress || "unknown", "generateIdentifier"),
|
|
52
|
+
maxRequests: 200,
|
|
53
|
+
windowTimeSize: 30,
|
|
54
|
+
windowCount: 4
|
|
55
|
+
};
|
|
56
|
+
const merged_params = { ...default_params, ...params };
|
|
57
|
+
return RateLimiterMiddleware.singletonInstance = new RateLimiterMiddleware(
|
|
58
|
+
merged_params
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
async call(req, res, next) {
|
|
62
|
+
let window = parseInt(
|
|
63
|
+
(Date.now() / 1e3 / this.params.windowTimeSize).toString()
|
|
64
|
+
);
|
|
65
|
+
let key = `rate_limiter:${this.params.generateIdentifier(req)}:${window}`;
|
|
66
|
+
let count = await cache().get(key) || 0;
|
|
67
|
+
if (!count) {
|
|
68
|
+
await cache().put(
|
|
69
|
+
key,
|
|
70
|
+
1,
|
|
71
|
+
this.params.windowTimeSize * (this.params.windowCount + 1)
|
|
72
|
+
);
|
|
73
|
+
count = 1;
|
|
74
|
+
} else {
|
|
75
|
+
count = await cache().increment(key, 1);
|
|
76
|
+
}
|
|
77
|
+
for (let i = 1; i < this.params.windowCount; i++) {
|
|
78
|
+
count += parseInt(
|
|
79
|
+
await cache().get(
|
|
80
|
+
`rate_limiter:${this.params.generateIdentifier(req)}:${window - i}`
|
|
81
|
+
) || "0"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (count > this.params.maxRequests) {
|
|
85
|
+
throw new HttpTooManyRequestsError(
|
|
86
|
+
"Too many requests. Please try again later."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
await next();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
35
93
|
export {
|
|
94
|
+
RateLimiterMiddleware,
|
|
36
95
|
cors,
|
|
37
96
|
dbTransaction
|
|
38
97
|
};
|
package/dist/middlewares.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middlewares.mts"],"sourcesContent":["import { Request, Response } from \"@devbro/neko-router\";\nimport { logger, db } from \"./facades.mjs\";\n\nexport function cors(\n options: { allowedOrigins?: (string | RegExp)[] } = {},\n): (req: Request, res: Response, next: () => Promise<void>) => Promise<void> {\n return async (\n req: Request,\n res: Response,\n next: () => Promise<void>,\n ): Promise<void> => {\n const allowedOrigins = options.allowedOrigins || [\"*\"];\n const origin = req.headers.origin || \"*\";\n\n for (const allowedOrigin of allowedOrigins) {\n if (typeof allowedOrigin === \"string\" && allowedOrigin === origin) {\n res.setHeader(\"Access-Control-Allow-Origin\", allowedOrigin);\n break;\n } else if (\n allowedOrigin instanceof RegExp &&\n allowedOrigin.test(origin)\n ) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n break;\n } else if (allowedOrigin === \"*\") {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n break;\n }\n }\n\n res.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n await next();\n };\n}\n\nexport async function dbTransaction(\n req: Request,\n res: Response,\n next: () => Promise<void>,\n): Promise<void> {\n try {\n await db().beginTransaction();\n await next();\n await db().commit();\n } finally {\n await db().rollback();\n }\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"sources":["../src/middlewares.mts"],"sourcesContent":["import { Middleware, Request, Response } from \"@devbro/neko-router\";\nimport { logger, db, cache } from \"./facades.mjs\";\nimport { HttpTooManyRequestsError } from \"@devbro/neko-http\";\n\nexport function cors(\n options: { allowedOrigins?: (string | RegExp)[] } = {},\n): (req: Request, res: Response, next: () => Promise<void>) => Promise<void> {\n return async (\n req: Request,\n res: Response,\n next: () => Promise<void>,\n ): Promise<void> => {\n const allowedOrigins = options.allowedOrigins || [\"*\"];\n const origin = req.headers.origin || \"*\";\n\n for (const allowedOrigin of allowedOrigins) {\n if (typeof allowedOrigin === \"string\" && allowedOrigin === origin) {\n res.setHeader(\"Access-Control-Allow-Origin\", allowedOrigin);\n break;\n } else if (\n allowedOrigin instanceof RegExp &&\n allowedOrigin.test(origin)\n ) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n break;\n } else if (allowedOrigin === \"*\") {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n break;\n }\n }\n\n res.setHeader(\"Access-Control-Allow-Headers\", \"*\");\n await next();\n };\n}\n\nexport async function dbTransaction(\n req: Request,\n res: Response,\n next: () => Promise<void>,\n): Promise<void> {\n try {\n await db().beginTransaction();\n await next();\n await db().commit();\n } finally {\n await db().rollback();\n }\n}\n\nexport type RateLimiterMiddlewareParams = {\n generateIdentifier: (req: Request) => string;\n maxRequests: number;\n windowTimeSize: number;\n windowCount: number;\n};\n\n/**\n * Rate limiter middleware using a sliding window algorithm.\n *\n * This middleware tracks request counts across multiple time windows to prevent abuse.\n * It uses atomic cache increments to ensure accurate counting in concurrent environments.\n *\n * @example\n * ```typescript\n * // Basic usage with defaults (200 requests per 2 minutes)\n * authnedRouter.addGlobalMiddleware(RateLimiterMiddleware);\n *\n * // Custom configuration\n * authnedRouter.addGlobalMiddleware(RateLimiterMiddleware.getInstance({\n * generateIdentifier: (req) => req.headers['x-api-key'] || req.socket.remoteAddress,\n * maxRequests: 200, // Allow 200 requests\n * windowTimeSize: 30, // in 30 second windows\n * windowCount: 4 // looking back 4 windows (total: 120 seconds)\n * }));\n * ```\n *\n * @param params.generateIdentifier - Function to generate unique identifier for rate limiting (default: IP address)\n * @param params.maxRequests - Maximum number of requests allowed across all windows (default: 200)\n * @param params.windowTimeSize - Size of each time window in seconds (default: 30)\n * @param params.windowCount - Number of previous windows to check (default: 4)\n *\n * The total time period checked is `windowTimeSize * windowCount` seconds.\n * Default configuration: 200 requests per 120 seconds (30s × 4 windows).\n *\n * @throws {HttpTooManyRequestsError} When rate limit is exceeded\n */\nexport class RateLimiterMiddleware extends Middleware {\n static singletonInstance: Middleware | undefined = undefined;\n static getInstance(\n params: Partial<RateLimiterMiddlewareParams> = {},\n ): Middleware {\n if (RateLimiterMiddleware.singletonInstance) {\n return RateLimiterMiddleware.singletonInstance;\n }\n let default_params: RateLimiterMiddlewareParams = {\n generateIdentifier: (req: Request) =>\n req.socket.remoteAddress || \"unknown\",\n maxRequests: 200,\n windowTimeSize: 30,\n windowCount: 4,\n };\n const merged_params = { ...default_params, ...params };\n return (RateLimiterMiddleware.singletonInstance = new RateLimiterMiddleware(\n merged_params,\n ));\n }\n\n constructor(private params: RateLimiterMiddlewareParams) {\n super();\n }\n\n async call(\n req: Request,\n res: Response,\n next: () => Promise<void>,\n ): Promise<void> {\n let window = parseInt(\n (Date.now() / 1000 / this.params.windowTimeSize).toString(),\n );\n let key = `rate_limiter:${this.params.generateIdentifier(req)}:${window}`;\n let count: number = (await cache().get(key)) || 0;\n if (!count) {\n await cache().put(\n key,\n 1,\n this.params.windowTimeSize * (this.params.windowCount + 1),\n );\n count = 1;\n } else {\n count = await cache().increment(key, 1);\n }\n\n for (let i = 1; i < this.params.windowCount; i++) {\n count += parseInt(\n (await cache().get(\n `rate_limiter:${this.params.generateIdentifier(req)}:${window - i}`,\n )) || \"0\",\n );\n }\n\n if (count > this.params.maxRequests) {\n throw new HttpTooManyRequestsError(\n \"Too many requests. Please try again later.\",\n );\n }\n\n await next();\n return;\n }\n}\n"],"mappings":";;AAAA,SAAS,kBAAqC;AAC9C,SAAiB,IAAI,aAAa;AAClC,SAAS,gCAAgC;AAElC,SAAS,KACd,UAAoD,CAAC,GACsB;AAC3E,SAAO,OACL,KACA,KACA,SACkB;AAClB,UAAM,iBAAiB,QAAQ,kBAAkB,CAAC,GAAG;AACrD,UAAM,SAAS,IAAI,QAAQ,UAAU;AAErC,eAAW,iBAAiB,gBAAgB;AAC1C,UAAI,OAAO,kBAAkB,YAAY,kBAAkB,QAAQ;AACjE,YAAI,UAAU,+BAA+B,aAAa;AAC1D;AAAA,MACF,WACE,yBAAyB,UACzB,cAAc,KAAK,MAAM,GACzB;AACA,YAAI,UAAU,+BAA+B,MAAM;AACnD;AAAA,MACF,WAAW,kBAAkB,KAAK;AAChC,YAAI,UAAU,+BAA+B,GAAG;AAChD;AAAA,MACF;AAAA,IACF;AAEA,QAAI,UAAU,gCAAgC,GAAG;AACjD,UAAM,KAAK;AAAA,EACb;AACF;AA9BgB;AAgChB,eAAsB,cACpB,KACA,KACA,MACe;AACf,MAAI;AACF,UAAM,GAAG,EAAE,iBAAiB;AAC5B,UAAM,KAAK;AACX,UAAM,GAAG,EAAE,OAAO;AAAA,EACpB,UAAE;AACA,UAAM,GAAG,EAAE,SAAS;AAAA,EACtB;AACF;AAZsB;AAmDf,MAAM,8BAA8B,WAAW;AAAA,EAqBpD,YAAoB,QAAqC;AACvD,UAAM;AADY;AAAA,EAEpB;AAAA,EA9GF,OAuFsD;AAAA;AAAA;AAAA,EACpD,OAAO,oBAA4C;AAAA,EACnD,OAAO,YACL,SAA+C,CAAC,GACpC;AACZ,QAAI,sBAAsB,mBAAmB;AAC3C,aAAO,sBAAsB;AAAA,IAC/B;AACA,QAAI,iBAA8C;AAAA,MAChD,oBAAoB,wBAAC,QACnB,IAAI,OAAO,iBAAiB,WADV;AAAA,MAEpB,aAAa;AAAA,MACb,gBAAgB;AAAA,MAChB,aAAa;AAAA,IACf;AACA,UAAM,gBAAgB,EAAE,GAAG,gBAAgB,GAAG,OAAO;AACrD,WAAQ,sBAAsB,oBAAoB,IAAI;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAAA,EAMA,MAAM,KACJ,KACA,KACA,MACe;AACf,QAAI,SAAS;AAAA,OACV,KAAK,IAAI,IAAI,MAAO,KAAK,OAAO,gBAAgB,SAAS;AAAA,IAC5D;AACA,QAAI,MAAM,gBAAgB,KAAK,OAAO,mBAAmB,GAAG,CAAC,IAAI,MAAM;AACvE,QAAI,QAAiB,MAAM,MAAM,EAAE,IAAI,GAAG,KAAM;AAChD,QAAI,CAAC,OAAO;AACV,YAAM,MAAM,EAAE;AAAA,QACZ;AAAA,QACA;AAAA,QACA,KAAK,OAAO,kBAAkB,KAAK,OAAO,cAAc;AAAA,MAC1D;AACA,cAAQ;AAAA,IACV,OAAO;AACL,cAAQ,MAAM,MAAM,EAAE,UAAU,KAAK,CAAC;AAAA,IACxC;AAEA,aAAS,IAAI,GAAG,IAAI,KAAK,OAAO,aAAa,KAAK;AAChD,eAAS;AAAA,QACN,MAAM,MAAM,EAAE;AAAA,UACb,gBAAgB,KAAK,OAAO,mBAAmB,GAAG,CAAC,IAAI,SAAS,CAAC;AAAA,QACnE,KAAM;AAAA,MACR;AAAA,IACF;AAEA,QAAI,QAAQ,KAAK,OAAO,aAAa;AACnC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,KAAK;AACX;AAAA,EACF;AACF;","names":[]}
|