tezx 1.0.36 → 1.0.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/cjs/core/context.js +3 -3
- package/cjs/core/request.js +36 -7
- package/cjs/core/router.js +0 -1
- package/cjs/core/server.js +1 -1
- package/cjs/index.js +1 -1
- package/cjs/middleware/detectBot.js +104 -0
- package/cjs/middleware/i18nMiddleware.js +13 -13
- package/cjs/middleware/index.js +8 -6
- package/cjs/middleware/lazyLoadModules.js +12 -7
- package/cjs/middleware/pagination.js +5 -1
- package/cjs/middleware/rateLimiter.js +7 -22
- package/cjs/middleware/xssProtection.js +1 -1
- package/core/context.js +3 -3
- package/core/request.d.ts +76 -2
- package/core/request.js +36 -7
- package/core/router.js +0 -1
- package/core/server.js +1 -1
- package/index.js +1 -1
- package/middleware/detectBot.d.ts +123 -0
- package/middleware/detectBot.js +98 -0
- package/middleware/i18nMiddleware.d.ts +4 -4
- package/middleware/i18nMiddleware.js +13 -13
- package/middleware/index.d.ts +7 -5
- package/middleware/index.js +6 -5
- package/middleware/lazyLoadModules.js +12 -7
- package/middleware/pagination.js +5 -1
- package/middleware/rateLimiter.d.ts +5 -8
- package/middleware/rateLimiter.js +7 -22
- package/middleware/xssProtection.js +1 -1
- package/package.json +1 -1
package/cjs/core/context.js
CHANGED
|
@@ -98,7 +98,7 @@ class Context {
|
|
|
98
98
|
return this;
|
|
99
99
|
}
|
|
100
100
|
get cookies() {
|
|
101
|
-
const c = this.headers.getAll("cookie");
|
|
101
|
+
const c = this.req.headers.getAll("cookie");
|
|
102
102
|
let cookies = {};
|
|
103
103
|
if (Array.isArray(c) && c.length != 0) {
|
|
104
104
|
const cookieHeader = c.join("; ").split(";");
|
|
@@ -167,8 +167,8 @@ class Context {
|
|
|
167
167
|
else if (typeof args[0] === "object") {
|
|
168
168
|
headers = args[0];
|
|
169
169
|
}
|
|
170
|
-
if (
|
|
171
|
-
if (typeof body === "string" || typeof body ==
|
|
170
|
+
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
171
|
+
if (typeof body === "string" || typeof body == "number") {
|
|
172
172
|
headers["Content-Type"] = "text/plain;";
|
|
173
173
|
}
|
|
174
174
|
else if (typeof body === "object" && body !== null) {
|
package/cjs/core/request.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Request = void 0;
|
|
4
|
-
const environment_1 = require("./environment");
|
|
5
|
-
const header_1 = require("./header");
|
|
6
4
|
const formData_1 = require("../utils/formData");
|
|
7
5
|
const url_1 = require("../utils/url");
|
|
6
|
+
const environment_1 = require("./environment");
|
|
7
|
+
const header_1 = require("./header");
|
|
8
8
|
class Request {
|
|
9
|
-
headers = new header_1.HeadersParser();
|
|
9
|
+
#headers = new header_1.HeadersParser();
|
|
10
10
|
url;
|
|
11
11
|
method;
|
|
12
12
|
urlRef = {
|
|
@@ -27,13 +27,13 @@ class Request {
|
|
|
27
27
|
remoteAddress = {};
|
|
28
28
|
constructor(req, params, remoteAddress) {
|
|
29
29
|
this.remoteAddress = remoteAddress;
|
|
30
|
-
this
|
|
30
|
+
this.#headers = new header_1.HeadersParser(req?.headers);
|
|
31
31
|
this.method = req?.method?.toUpperCase();
|
|
32
32
|
this.params = params;
|
|
33
33
|
this.rawRequest = req;
|
|
34
34
|
if (environment_1.EnvironmentDetector.getEnvironment == "node") {
|
|
35
35
|
const protocol = environment_1.EnvironmentDetector.detectProtocol(req);
|
|
36
|
-
const host = environment_1.EnvironmentDetector.getHost(this
|
|
36
|
+
const host = environment_1.EnvironmentDetector.getHost(this.#headers);
|
|
37
37
|
this.url = `${protocol}://${host}${req.url}`;
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
@@ -42,11 +42,40 @@ class Request {
|
|
|
42
42
|
this.urlRef = (0, url_1.urlParse)(this.url);
|
|
43
43
|
this.query = this.urlRef.query;
|
|
44
44
|
}
|
|
45
|
+
get headers() {
|
|
46
|
+
let requestHeaders = this.#headers;
|
|
47
|
+
return {
|
|
48
|
+
get: function get(key) {
|
|
49
|
+
return requestHeaders.get(key.toLowerCase());
|
|
50
|
+
},
|
|
51
|
+
getAll: function getAll(key) {
|
|
52
|
+
return requestHeaders.get(key.toLowerCase()) || [];
|
|
53
|
+
},
|
|
54
|
+
has: function has(key) {
|
|
55
|
+
return requestHeaders.has(key.toLowerCase());
|
|
56
|
+
},
|
|
57
|
+
entries: function entries() {
|
|
58
|
+
return requestHeaders.entries();
|
|
59
|
+
},
|
|
60
|
+
keys: function keys() {
|
|
61
|
+
return requestHeaders.keys();
|
|
62
|
+
},
|
|
63
|
+
values: function values() {
|
|
64
|
+
return requestHeaders.values();
|
|
65
|
+
},
|
|
66
|
+
forEach: function forEach(callback) {
|
|
67
|
+
return requestHeaders.forEach(callback);
|
|
68
|
+
},
|
|
69
|
+
toObject: function toObject() {
|
|
70
|
+
return requestHeaders.toObject();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
45
74
|
async text() {
|
|
46
75
|
return await (0, formData_1.parseTextBody)(this.rawRequest);
|
|
47
76
|
}
|
|
48
77
|
async json() {
|
|
49
|
-
const contentType = this
|
|
78
|
+
const contentType = this.#headers.get("content-type") || "";
|
|
50
79
|
if (contentType.includes("application/json")) {
|
|
51
80
|
return await (0, formData_1.parseJsonBody)(this.rawRequest);
|
|
52
81
|
}
|
|
@@ -55,7 +84,7 @@ class Request {
|
|
|
55
84
|
}
|
|
56
85
|
}
|
|
57
86
|
async formData(options) {
|
|
58
|
-
const contentType = this
|
|
87
|
+
const contentType = this.#headers.get("content-type") || "";
|
|
59
88
|
if (!contentType) {
|
|
60
89
|
throw Error("Invalid Content-Type");
|
|
61
90
|
}
|
package/cjs/core/router.js
CHANGED
|
@@ -5,7 +5,6 @@ const staticFile_1 = require("../utils/staticFile");
|
|
|
5
5
|
const url_1 = require("../utils/url");
|
|
6
6
|
const config_1 = require("./config");
|
|
7
7
|
const MiddlewareConfigure_1 = require("./MiddlewareConfigure");
|
|
8
|
-
;
|
|
9
8
|
class TrieRouter {
|
|
10
9
|
children = new Map();
|
|
11
10
|
handlers = new Map();
|
package/cjs/core/server.js
CHANGED
|
@@ -133,7 +133,7 @@ class TezX extends router_1.Router {
|
|
|
133
133
|
return (await this.#createHandler(middlewares, callback)(ctx));
|
|
134
134
|
}
|
|
135
135
|
else {
|
|
136
|
-
let res = await config_1.GlobalConfig.notFound(ctx);
|
|
136
|
+
let res = (await config_1.GlobalConfig.notFound(ctx));
|
|
137
137
|
ctx.setStatus = res.status;
|
|
138
138
|
return res;
|
|
139
139
|
}
|
package/cjs/index.js
CHANGED
|
@@ -7,4 +7,4 @@ var server_1 = require("./core/server");
|
|
|
7
7
|
Object.defineProperty(exports, "TezX", { enumerable: true, get: function () { return server_1.TezX; } });
|
|
8
8
|
var params_1 = require("./utils/params");
|
|
9
9
|
Object.defineProperty(exports, "useParams", { enumerable: true, get: function () { return params_1.useParams; } });
|
|
10
|
-
exports.version = "1.0.
|
|
10
|
+
exports.version = "1.0.38";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.detectBot = void 0;
|
|
4
|
+
exports.createRateLimitDefaultStorage = createRateLimitDefaultStorage;
|
|
5
|
+
exports.isRateLimit = isRateLimit;
|
|
6
|
+
const config_1 = require("../core/config");
|
|
7
|
+
const detectBot = (options = {}) => {
|
|
8
|
+
const { botUserAgents = ["bot", "spider", "crawl", "slurp"], maxRequests = 30, windowMs = 60000, isBlacklistedIP = async () => false, queryKeyBot = "bot", onBotDetected = "block", enableRateLimiting = false, customBotDetector = async () => false, customBlockedResponse = (ctx, { reason }) => {
|
|
9
|
+
ctx.setStatus = 403;
|
|
10
|
+
ctx.body = { error: `Bot detected: ${reason}` };
|
|
11
|
+
}, storage, confidenceThreshold = 0.5, } = options;
|
|
12
|
+
let store = storage;
|
|
13
|
+
if (enableRateLimiting) {
|
|
14
|
+
store = createRateLimitDefaultStorage();
|
|
15
|
+
}
|
|
16
|
+
return async (ctx, next) => {
|
|
17
|
+
const detectionResult = {
|
|
18
|
+
isBot: false,
|
|
19
|
+
indicators: [],
|
|
20
|
+
};
|
|
21
|
+
const userAgent = ctx.headers.get("user-agent")?.toLowerCase() || "";
|
|
22
|
+
const ip = ctx.req.remoteAddress?.address || "unknown";
|
|
23
|
+
const isBotQuery = ctx.req.query[queryKeyBot] === "true";
|
|
24
|
+
if (botUserAgents.some((agent) => userAgent.includes(agent))) {
|
|
25
|
+
detectionResult.indicators.push("User-Agent");
|
|
26
|
+
}
|
|
27
|
+
if (await isBlacklistedIP(ip)) {
|
|
28
|
+
detectionResult.indicators.push("Blacklisted IP");
|
|
29
|
+
}
|
|
30
|
+
if (isBotQuery) {
|
|
31
|
+
detectionResult.indicators.push("Query Parameter");
|
|
32
|
+
}
|
|
33
|
+
const key = `${ctx.req.remoteAddress.address}:${ctx.req.remoteAddress.port}`;
|
|
34
|
+
if (enableRateLimiting &&
|
|
35
|
+
isRateLimit(ctx, key, store, maxRequests, windowMs).check) {
|
|
36
|
+
detectionResult.indicators.push("Rate Limiting");
|
|
37
|
+
}
|
|
38
|
+
if (await customBotDetector(ctx)) {
|
|
39
|
+
detectionResult.indicators.push("Custom Detector");
|
|
40
|
+
}
|
|
41
|
+
detectionResult.isBot = detectionResult.indicators.length > 0;
|
|
42
|
+
if (detectionResult.indicators.length > 1) {
|
|
43
|
+
detectionResult.reason = "Multiple Indicators";
|
|
44
|
+
const confidence = Math.min(0.3 * detectionResult.indicators.length, 1);
|
|
45
|
+
detectionResult.isBot = confidence >= confidenceThreshold;
|
|
46
|
+
}
|
|
47
|
+
else if (detectionResult.indicators.length === 1) {
|
|
48
|
+
detectionResult.reason = detectionResult.indicators[0];
|
|
49
|
+
}
|
|
50
|
+
if (detectionResult.isBot) {
|
|
51
|
+
config_1.GlobalConfig.debugging.warn(`Bot detected: ${detectionResult.reason}`, {
|
|
52
|
+
ip,
|
|
53
|
+
userAgent,
|
|
54
|
+
indicators: detectionResult.indicators,
|
|
55
|
+
});
|
|
56
|
+
if (onBotDetected === "block") {
|
|
57
|
+
return customBlockedResponse(ctx, detectionResult);
|
|
58
|
+
}
|
|
59
|
+
else if (typeof onBotDetected === "function") {
|
|
60
|
+
return onBotDetected(ctx, detectionResult);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return await next();
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
exports.detectBot = detectBot;
|
|
67
|
+
function createRateLimitDefaultStorage() {
|
|
68
|
+
const store = new Map();
|
|
69
|
+
return {
|
|
70
|
+
get: (key) => store.get(key),
|
|
71
|
+
set: (key, value) => store.set(key, value),
|
|
72
|
+
delete: (key) => store.delete(key),
|
|
73
|
+
clearExpired: () => {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const [key, entry] of store.entries()) {
|
|
76
|
+
if (now >= entry.resetTime) {
|
|
77
|
+
store.delete(key);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function isRateLimit(ctx, key, store, maxRequests, windowMs) {
|
|
84
|
+
store.clearExpired();
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
let entry = store.get(key) || { count: 0, resetTime: now + windowMs };
|
|
87
|
+
if (now < entry.resetTime) {
|
|
88
|
+
entry.count++;
|
|
89
|
+
if (entry.count > maxRequests) {
|
|
90
|
+
return {
|
|
91
|
+
check: true,
|
|
92
|
+
entry: entry,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
entry = { count: 1, resetTime: now + windowMs };
|
|
98
|
+
}
|
|
99
|
+
store.set(key, entry);
|
|
100
|
+
return {
|
|
101
|
+
check: false,
|
|
102
|
+
entry: entry,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -4,12 +4,12 @@ exports.i18nMiddleware = void 0;
|
|
|
4
4
|
const config_1 = require("../core/config");
|
|
5
5
|
const i18nMiddleware = (options) => {
|
|
6
6
|
const { loadTranslations, defaultCacheDuration = 3600000, isCacheValid = (cached) => cached.expiresAt > Date.now(), detectLanguage = (ctx) => ctx.req.query.lang ||
|
|
7
|
-
ctx.cookies?.get(
|
|
8
|
-
ctx.headers.get(
|
|
7
|
+
ctx.cookies?.get("lang") ||
|
|
8
|
+
ctx.req.headers.get("accept-language")?.split(",")[0] ||
|
|
9
9
|
options.defaultLanguage ||
|
|
10
|
-
|
|
11
|
-
return Object.entries(options).reduce((msg, [key, value]) => msg.replace(new RegExp(`{{${key}}}`,
|
|
12
|
-
}, cacheTranslations = true } = options;
|
|
10
|
+
"en", defaultLanguage = "en", fallbackChain = [], translationFunctionKey = "t", formatMessage = (message, options = {}) => {
|
|
11
|
+
return Object.entries(options).reduce((msg, [key, value]) => msg.replace(new RegExp(`{{${key}}}`, "g"), String(value)), message);
|
|
12
|
+
}, cacheTranslations = true, } = options;
|
|
13
13
|
const translationCache = {};
|
|
14
14
|
return async (ctx, next) => {
|
|
15
15
|
try {
|
|
@@ -17,12 +17,12 @@ const i18nMiddleware = (options) => {
|
|
|
17
17
|
const languageChain = [
|
|
18
18
|
detectedLanguage,
|
|
19
19
|
...fallbackChain,
|
|
20
|
-
defaultLanguage
|
|
20
|
+
defaultLanguage,
|
|
21
21
|
].filter(Boolean);
|
|
22
22
|
let translations = null;
|
|
23
23
|
let selectedLanguage = defaultLanguage;
|
|
24
24
|
for (const lang of languageChain) {
|
|
25
|
-
const normalizedLang = lang.split(
|
|
25
|
+
const normalizedLang = lang.split("-")[0].toLowerCase();
|
|
26
26
|
if (cacheTranslations && translationCache[normalizedLang]) {
|
|
27
27
|
const cached = translationCache[normalizedLang];
|
|
28
28
|
if (isCacheValid(cached, normalizedLang)) {
|
|
@@ -38,7 +38,7 @@ const i18nMiddleware = (options) => {
|
|
|
38
38
|
if (expiresAt instanceof Date) {
|
|
39
39
|
expirationTime = expiresAt.getTime();
|
|
40
40
|
}
|
|
41
|
-
else if (typeof expiresAt ===
|
|
41
|
+
else if (typeof expiresAt === "number") {
|
|
42
42
|
expirationTime = expiresAt;
|
|
43
43
|
}
|
|
44
44
|
translations = loadedTranslations;
|
|
@@ -56,13 +56,13 @@ const i18nMiddleware = (options) => {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
if (!translations) {
|
|
59
|
-
throw new Error(
|
|
59
|
+
throw new Error("No translations available");
|
|
60
60
|
}
|
|
61
61
|
ctx[translationFunctionKey] = (key, options) => {
|
|
62
|
-
const value = key.split(
|
|
63
|
-
return
|
|
62
|
+
const value = key.split(".").reduce((acc, k) => {
|
|
63
|
+
return acc && typeof acc === "object" ? acc[k] : undefined;
|
|
64
64
|
}, translations);
|
|
65
|
-
const message = typeof value ===
|
|
65
|
+
const message = typeof value === "string" ? value : key;
|
|
66
66
|
return formatMessage(message, options);
|
|
67
67
|
};
|
|
68
68
|
ctx.language = selectedLanguage;
|
|
@@ -70,7 +70,7 @@ const i18nMiddleware = (options) => {
|
|
|
70
70
|
return await next();
|
|
71
71
|
}
|
|
72
72
|
catch (error) {
|
|
73
|
-
config_1.GlobalConfig.debugging.error(
|
|
73
|
+
config_1.GlobalConfig.debugging.error("i18n processing error:", error);
|
|
74
74
|
ctx.setStatus = 500;
|
|
75
75
|
throw error;
|
|
76
76
|
}
|
package/cjs/middleware/index.js
CHANGED
|
@@ -14,16 +14,18 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.cors = void 0;
|
|
17
|
+
exports.detectBot = exports.cors = void 0;
|
|
18
18
|
var cors_1 = require("./cors");
|
|
19
19
|
Object.defineProperty(exports, "cors", { enumerable: true, get: function () { return cors_1.cors; } });
|
|
20
|
+
var detectBot_1 = require("./detectBot");
|
|
21
|
+
Object.defineProperty(exports, "detectBot", { enumerable: true, get: function () { return detectBot_1.detectBot; } });
|
|
22
|
+
__exportStar(require("./i18nMiddleware"), exports);
|
|
23
|
+
__exportStar(require("./lazyLoadModules"), exports);
|
|
20
24
|
__exportStar(require("./logger"), exports);
|
|
25
|
+
__exportStar(require("./pagination"), exports);
|
|
21
26
|
__exportStar(require("./powered-by"), exports);
|
|
27
|
+
__exportStar(require("./rateLimiter"), exports);
|
|
22
28
|
__exportStar(require("./request-id"), exports);
|
|
29
|
+
__exportStar(require("./sanitizeHeader"), exports);
|
|
23
30
|
__exportStar(require("./secureHeaders"), exports);
|
|
24
31
|
__exportStar(require("./xssProtection"), exports);
|
|
25
|
-
__exportStar(require("./sanitizeHeader"), exports);
|
|
26
|
-
__exportStar(require("./rateLimiter"), exports);
|
|
27
|
-
__exportStar(require("./pagination"), exports);
|
|
28
|
-
__exportStar(require("./i18nMiddleware"), exports);
|
|
29
|
-
__exportStar(require("./lazyLoadModules"), exports);
|
|
@@ -2,21 +2,26 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.lazyLoadModules = void 0;
|
|
4
4
|
const config_1 = require("../core/config");
|
|
5
|
-
;
|
|
6
5
|
const lazyLoadModules = (options) => {
|
|
7
|
-
const { moduleKey = (ctx) => ctx.req.params[queryKeyModule] || ctx.req.query[queryKeyModule], getModuleLoader, queryKeyModule = "module", moduleContextKey = "module", cacheTTL = 3600000, dependencies = {}, enableCache = true, cacheStorage = new Map(), lifecycleHooks = {}, validateModule } = options;
|
|
6
|
+
const { moduleKey = (ctx) => ctx.req.params[queryKeyModule] || ctx.req.query[queryKeyModule], getModuleLoader, queryKeyModule = "module", moduleContextKey = "module", cacheTTL = 3600000, dependencies = {}, enableCache = true, cacheStorage = new Map(), lifecycleHooks = {}, validateModule, } = options;
|
|
8
7
|
return async (ctx, next) => {
|
|
9
|
-
let moduleName = moduleKey(ctx) ||
|
|
8
|
+
let moduleName = moduleKey(ctx) ||
|
|
9
|
+
ctx.req.params[queryKeyModule] ||
|
|
10
|
+
ctx.req.query[queryKeyModule];
|
|
10
11
|
if (!moduleName) {
|
|
11
12
|
config_1.GlobalConfig.debugging.warn("No module specified for lazy loading.");
|
|
12
13
|
return await next();
|
|
13
14
|
}
|
|
15
|
+
let storage = cacheStorage;
|
|
16
|
+
if (enableCache && !cacheStorage) {
|
|
17
|
+
storage = new Map();
|
|
18
|
+
}
|
|
14
19
|
try {
|
|
15
20
|
if (enableCache) {
|
|
16
|
-
const cached =
|
|
21
|
+
const cached = storage.get(moduleName);
|
|
17
22
|
if (cached) {
|
|
18
23
|
if (cached.expiresAt > Date.now()) {
|
|
19
|
-
|
|
24
|
+
storage.delete(moduleName);
|
|
20
25
|
}
|
|
21
26
|
else {
|
|
22
27
|
config_1.GlobalConfig.debugging.info(`Using cached module: ${moduleName}`);
|
|
@@ -41,9 +46,9 @@ const lazyLoadModules = (options) => {
|
|
|
41
46
|
}
|
|
42
47
|
ctx.dependencies = dependencies;
|
|
43
48
|
if (enableCache) {
|
|
44
|
-
|
|
49
|
+
storage.set(moduleName, {
|
|
45
50
|
module,
|
|
46
|
-
expiresAt: Date.now() + cacheTTL
|
|
51
|
+
expiresAt: Date.now() + cacheTTL,
|
|
47
52
|
});
|
|
48
53
|
lifecycleHooks.onCacheSet?.(moduleName, module, ctx);
|
|
49
54
|
}
|
|
@@ -17,7 +17,11 @@ const paginationHandler = (options = {}) => {
|
|
|
17
17
|
queryKeyLimit,
|
|
18
18
|
};
|
|
19
19
|
if (getDataSource) {
|
|
20
|
-
const dataSourceResponse = await getDataSource(ctx, {
|
|
20
|
+
const dataSourceResponse = await getDataSource(ctx, {
|
|
21
|
+
page,
|
|
22
|
+
limit,
|
|
23
|
+
offset,
|
|
24
|
+
});
|
|
21
25
|
const total = dataSourceResponse?.[countKey];
|
|
22
26
|
const data = dataSourceResponse?.[dataKey];
|
|
23
27
|
const pagination = {
|
|
@@ -1,38 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.rateLimiter = void 0;
|
|
4
|
+
const detectBot_1 = require("./detectBot");
|
|
4
5
|
const rateLimiter = (options) => {
|
|
5
|
-
const { maxRequests, windowMs, keyGenerator = (ctx) => `${ctx.req.remoteAddress.address}:${ctx.req.remoteAddress.port}`,
|
|
6
|
+
const { maxRequests, windowMs, keyGenerator = (ctx) => `${ctx.req.remoteAddress.address}:${ctx.req.remoteAddress.port}`, storage = (0, detectBot_1.createRateLimitDefaultStorage)(), onError = (ctx, retryAfter, error) => {
|
|
6
7
|
ctx.setStatus = 429;
|
|
7
8
|
throw new Error(`Rate limit exceeded. Try again in ${retryAfter} seconds.`);
|
|
8
9
|
}, } = options;
|
|
9
10
|
return async (ctx, next) => {
|
|
10
11
|
const key = keyGenerator(ctx);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (Date.now() >= entry.resetTime) {
|
|
15
|
-
cacheStorage.delete(key);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const entry = cacheStorage.get(key);
|
|
19
|
-
if (entry && Date.now() < entry.resetTime) {
|
|
20
|
-
requestCount = entry.count + 1;
|
|
21
|
-
resetTime = entry.resetTime;
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
requestCount = 1;
|
|
25
|
-
resetTime = Date.now() + windowMs;
|
|
26
|
-
cacheStorage.set(key, { count: requestCount, resetTime });
|
|
27
|
-
}
|
|
28
|
-
if (requestCount > maxRequests) {
|
|
29
|
-
const retryAfter = Math.ceil((resetTime - Date.now()) / 1000);
|
|
12
|
+
const { check, entry } = (0, detectBot_1.isRateLimit)(ctx, key, storage, maxRequests, windowMs);
|
|
13
|
+
if (check) {
|
|
14
|
+
const retryAfter = Math.ceil((entry.resetTime - Date.now()) / 1000);
|
|
30
15
|
ctx.headers.set("Retry-After", retryAfter.toString());
|
|
31
16
|
return onError(ctx, retryAfter, new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`));
|
|
32
17
|
}
|
|
33
18
|
ctx.headers.set("X-RateLimit-Limit", maxRequests.toString());
|
|
34
|
-
ctx.headers.set("X-RateLimit-Remaining", (maxRequests -
|
|
35
|
-
ctx.headers.set("X-RateLimit-Reset", resetTime.toString());
|
|
19
|
+
ctx.headers.set("X-RateLimit-Remaining", (maxRequests - entry.count).toString());
|
|
20
|
+
ctx.headers.set("X-RateLimit-Reset", entry.resetTime.toString());
|
|
36
21
|
return await next();
|
|
37
22
|
};
|
|
38
23
|
};
|
|
@@ -14,7 +14,7 @@ const xssProtection = (options = {}) => {
|
|
|
14
14
|
ctx.headers.set("X-XSS-Protection", xssHeaderValue);
|
|
15
15
|
config_1.GlobalConfig.debugging.warn(`🟢 X-XSS-Protection set to: ${xssHeaderValue}`);
|
|
16
16
|
if (fallbackCSP) {
|
|
17
|
-
const existingCSP = ctx.headers.get("Content-Security-Policy");
|
|
17
|
+
const existingCSP = ctx.req.headers.get("Content-Security-Policy");
|
|
18
18
|
if (!existingCSP) {
|
|
19
19
|
ctx.headers.set("Content-Security-Policy", fallbackCSP);
|
|
20
20
|
config_1.GlobalConfig.debugging.warn(`🟣 Fallback CSP set to: ${fallbackCSP}`);
|
package/core/context.js
CHANGED
|
@@ -95,7 +95,7 @@ export class Context {
|
|
|
95
95
|
return this;
|
|
96
96
|
}
|
|
97
97
|
get cookies() {
|
|
98
|
-
const c = this.headers.getAll("cookie");
|
|
98
|
+
const c = this.req.headers.getAll("cookie");
|
|
99
99
|
let cookies = {};
|
|
100
100
|
if (Array.isArray(c) && c.length != 0) {
|
|
101
101
|
const cookieHeader = c.join("; ").split(";");
|
|
@@ -164,8 +164,8 @@ export class Context {
|
|
|
164
164
|
else if (typeof args[0] === "object") {
|
|
165
165
|
headers = args[0];
|
|
166
166
|
}
|
|
167
|
-
if (
|
|
168
|
-
if (typeof body === "string" || typeof body ==
|
|
167
|
+
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
168
|
+
if (typeof body === "string" || typeof body == "number") {
|
|
169
169
|
headers["Content-Type"] = "text/plain;";
|
|
170
170
|
}
|
|
171
171
|
else if (typeof body === "object" && body !== null) {
|
package/core/request.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { HeadersParser } from "./header";
|
|
2
1
|
import { UrlRef } from "../utils/url";
|
|
3
2
|
export type FormDataOptions = {
|
|
4
3
|
maxSize?: number;
|
|
@@ -19,7 +18,7 @@ export type ConnAddress = {
|
|
|
19
18
|
};
|
|
20
19
|
export type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE" | "OPTIONS" | "PATCH" | "HEAD" | "ALL" | "TRACE" | "CONNECT" | string;
|
|
21
20
|
export declare class Request {
|
|
22
|
-
|
|
21
|
+
#private;
|
|
23
22
|
/**
|
|
24
23
|
* Full request URL including protocol and query string
|
|
25
24
|
* @type {string}
|
|
@@ -57,6 +56,81 @@ export declare class Request {
|
|
|
57
56
|
*/
|
|
58
57
|
remoteAddress: AddressType;
|
|
59
58
|
constructor(req: any, params: Record<string, any>, remoteAddress: AddressType);
|
|
59
|
+
get headers(): {
|
|
60
|
+
/**
|
|
61
|
+
* Retrieves the first value of a specific header.
|
|
62
|
+
* @param key - Header name to search for.
|
|
63
|
+
* @returns The first header value or undefined if not found.
|
|
64
|
+
* @example
|
|
65
|
+
* get('content-type') // returns 'application/json'
|
|
66
|
+
*/
|
|
67
|
+
get: (key: string) => string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Retrieves all values of a specific header.
|
|
70
|
+
* If multiple values exist for a header, all will be returned as an array.
|
|
71
|
+
* @param key - Header name to search for.
|
|
72
|
+
* @returns An array of all header values associated with the key.
|
|
73
|
+
* @example
|
|
74
|
+
* getAll('accept-language') // returns ['en-US', 'fr-CA']
|
|
75
|
+
*/
|
|
76
|
+
getAll: (key: string) => string | never[];
|
|
77
|
+
/**
|
|
78
|
+
* Checks if a header exists in the request.
|
|
79
|
+
* @param key - Header name to check for existence.
|
|
80
|
+
* @returns True if the header exists, false otherwise.
|
|
81
|
+
* @example
|
|
82
|
+
* has('Authorization') // returns true if 'Authorization' header exists
|
|
83
|
+
*/
|
|
84
|
+
has: (key: string) => boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Returns an iterator over all header entries.
|
|
87
|
+
* Each entry is a [key, value] pair where the value can be an array of strings.
|
|
88
|
+
* @returns IterableIterator for iterating over header key-value pairs.
|
|
89
|
+
* @example
|
|
90
|
+
* for (let [key, value] of headers.entries()) {
|
|
91
|
+
* console.log(key, value);
|
|
92
|
+
* }
|
|
93
|
+
*/
|
|
94
|
+
entries: () => IterableIterator<[string, string[]]>;
|
|
95
|
+
/**
|
|
96
|
+
* Returns an iterator over all header keys.
|
|
97
|
+
* This allows iteration over the names of all headers in the request.
|
|
98
|
+
* @returns IterableIterator of header names.
|
|
99
|
+
* @example
|
|
100
|
+
* for (let key of headers.keys()) {
|
|
101
|
+
* console.log(key);
|
|
102
|
+
* }
|
|
103
|
+
*/
|
|
104
|
+
keys: () => IterableIterator<string>;
|
|
105
|
+
/**
|
|
106
|
+
* Returns an iterator over all header values.
|
|
107
|
+
* This allows iteration over the values of all headers, with each value being an array of strings.
|
|
108
|
+
* @returns IterableIterator of header values.
|
|
109
|
+
* @example
|
|
110
|
+
* for (let value of headers.values()) {
|
|
111
|
+
* console.log(value);
|
|
112
|
+
* }
|
|
113
|
+
*/
|
|
114
|
+
values: () => IterableIterator<string[]>;
|
|
115
|
+
/**
|
|
116
|
+
* Iterates over each header and executes a callback for every header found.
|
|
117
|
+
* @param callback - Function to execute for each header. Receives the value array and key.
|
|
118
|
+
* @example
|
|
119
|
+
* headers.forEach((value, key) => {
|
|
120
|
+
* console.log(key, value);
|
|
121
|
+
* });
|
|
122
|
+
*/
|
|
123
|
+
forEach: (callback: (value: string[], key: string) => void) => void;
|
|
124
|
+
/**
|
|
125
|
+
* Converts all headers into a plain JavaScript object.
|
|
126
|
+
* Single-value headers are represented as a string, and multi-value headers as an array.
|
|
127
|
+
* @returns A plain object with header names as keys and their values as strings or arrays.
|
|
128
|
+
* @example
|
|
129
|
+
* const headersObject = headers.toObject();
|
|
130
|
+
* console.log(headersObject);
|
|
131
|
+
*/
|
|
132
|
+
toObject: () => Record<string, string | string[]>;
|
|
133
|
+
};
|
|
60
134
|
/**
|
|
61
135
|
* Parses the request body as plain text.
|
|
62
136
|
* @returns {Promise<string>} The text content of the request body.
|
package/core/request.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { EnvironmentDetector } from "./environment";
|
|
2
|
-
import { HeadersParser } from "./header";
|
|
3
1
|
import { parseJsonBody, parseMultipartBody, parseTextBody, parseUrlEncodedBody, } from "../utils/formData";
|
|
4
2
|
import { urlParse } from "../utils/url";
|
|
3
|
+
import { EnvironmentDetector } from "./environment";
|
|
4
|
+
import { HeadersParser } from "./header";
|
|
5
5
|
export class Request {
|
|
6
|
-
headers = new HeadersParser();
|
|
6
|
+
#headers = new HeadersParser();
|
|
7
7
|
url;
|
|
8
8
|
method;
|
|
9
9
|
urlRef = {
|
|
@@ -24,13 +24,13 @@ export class Request {
|
|
|
24
24
|
remoteAddress = {};
|
|
25
25
|
constructor(req, params, remoteAddress) {
|
|
26
26
|
this.remoteAddress = remoteAddress;
|
|
27
|
-
this
|
|
27
|
+
this.#headers = new HeadersParser(req?.headers);
|
|
28
28
|
this.method = req?.method?.toUpperCase();
|
|
29
29
|
this.params = params;
|
|
30
30
|
this.rawRequest = req;
|
|
31
31
|
if (EnvironmentDetector.getEnvironment == "node") {
|
|
32
32
|
const protocol = EnvironmentDetector.detectProtocol(req);
|
|
33
|
-
const host = EnvironmentDetector.getHost(this
|
|
33
|
+
const host = EnvironmentDetector.getHost(this.#headers);
|
|
34
34
|
this.url = `${protocol}://${host}${req.url}`;
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
@@ -39,11 +39,40 @@ export class Request {
|
|
|
39
39
|
this.urlRef = urlParse(this.url);
|
|
40
40
|
this.query = this.urlRef.query;
|
|
41
41
|
}
|
|
42
|
+
get headers() {
|
|
43
|
+
let requestHeaders = this.#headers;
|
|
44
|
+
return {
|
|
45
|
+
get: function get(key) {
|
|
46
|
+
return requestHeaders.get(key.toLowerCase());
|
|
47
|
+
},
|
|
48
|
+
getAll: function getAll(key) {
|
|
49
|
+
return requestHeaders.get(key.toLowerCase()) || [];
|
|
50
|
+
},
|
|
51
|
+
has: function has(key) {
|
|
52
|
+
return requestHeaders.has(key.toLowerCase());
|
|
53
|
+
},
|
|
54
|
+
entries: function entries() {
|
|
55
|
+
return requestHeaders.entries();
|
|
56
|
+
},
|
|
57
|
+
keys: function keys() {
|
|
58
|
+
return requestHeaders.keys();
|
|
59
|
+
},
|
|
60
|
+
values: function values() {
|
|
61
|
+
return requestHeaders.values();
|
|
62
|
+
},
|
|
63
|
+
forEach: function forEach(callback) {
|
|
64
|
+
return requestHeaders.forEach(callback);
|
|
65
|
+
},
|
|
66
|
+
toObject: function toObject() {
|
|
67
|
+
return requestHeaders.toObject();
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
42
71
|
async text() {
|
|
43
72
|
return await parseTextBody(this.rawRequest);
|
|
44
73
|
}
|
|
45
74
|
async json() {
|
|
46
|
-
const contentType = this
|
|
75
|
+
const contentType = this.#headers.get("content-type") || "";
|
|
47
76
|
if (contentType.includes("application/json")) {
|
|
48
77
|
return await parseJsonBody(this.rawRequest);
|
|
49
78
|
}
|
|
@@ -52,7 +81,7 @@ export class Request {
|
|
|
52
81
|
}
|
|
53
82
|
}
|
|
54
83
|
async formData(options) {
|
|
55
|
-
const contentType = this
|
|
84
|
+
const contentType = this.#headers.get("content-type") || "";
|
|
56
85
|
if (!contentType) {
|
|
57
86
|
throw Error("Invalid Content-Type");
|
|
58
87
|
}
|
package/core/router.js
CHANGED
|
@@ -2,7 +2,6 @@ import { getFiles } from "../utils/staticFile";
|
|
|
2
2
|
import { sanitizePathSplit } from "../utils/url";
|
|
3
3
|
import { GlobalConfig } from "./config";
|
|
4
4
|
import MiddlewareConfigure, { TriMiddleware, } from "./MiddlewareConfigure";
|
|
5
|
-
;
|
|
6
5
|
class TrieRouter {
|
|
7
6
|
children = new Map();
|
|
8
7
|
handlers = new Map();
|
package/core/server.js
CHANGED
|
@@ -130,7 +130,7 @@ export class TezX extends Router {
|
|
|
130
130
|
return (await this.#createHandler(middlewares, callback)(ctx));
|
|
131
131
|
}
|
|
132
132
|
else {
|
|
133
|
-
let res = await GlobalConfig.notFound(ctx);
|
|
133
|
+
let res = (await GlobalConfig.notFound(ctx));
|
|
134
134
|
ctx.setStatus = res.status;
|
|
135
135
|
return res;
|
|
136
136
|
}
|
package/index.js
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Context, Middleware } from "..";
|
|
2
|
+
export type DetectBotReason = "User-Agent" | "Blacklisted IP" | "Query Parameter" | "Rate Limiting" | "Custom Detector" | "Multiple Indicators";
|
|
3
|
+
type BotDetectionResult = {
|
|
4
|
+
isBot: boolean;
|
|
5
|
+
reason?: DetectBotReason;
|
|
6
|
+
indicators: string[];
|
|
7
|
+
};
|
|
8
|
+
type DetectBotOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* 🤖 List of bot-like user-agent substrings to detect
|
|
11
|
+
* @default ["bot", "spider", "crawl", "slurp"]
|
|
12
|
+
*/
|
|
13
|
+
botUserAgents?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* ⚠️ Maximum allowed requests in the time window
|
|
16
|
+
* @default 30 requests
|
|
17
|
+
*/
|
|
18
|
+
maxRequests?: number;
|
|
19
|
+
/**
|
|
20
|
+
* ⏱️ Time window in milliseconds for rate limiting
|
|
21
|
+
* @default 60000 (1 minute)
|
|
22
|
+
*/
|
|
23
|
+
windowMs?: number;
|
|
24
|
+
/**
|
|
25
|
+
* 🚫 IP blacklist checker
|
|
26
|
+
* @default () => false
|
|
27
|
+
*/
|
|
28
|
+
isBlacklistedIP?: (ip: string) => boolean | Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* 🔍 Query parameter name for bot identification
|
|
31
|
+
* @default "bot"
|
|
32
|
+
*/
|
|
33
|
+
queryKeyBot?: string;
|
|
34
|
+
/**
|
|
35
|
+
* 🛡️ Action to take when bot is detected
|
|
36
|
+
* @default "block"
|
|
37
|
+
*/
|
|
38
|
+
onBotDetected?: "block" | ((ctx: Context, result: BotDetectionResult) => void);
|
|
39
|
+
/**
|
|
40
|
+
* ⚖️ Enable rate-limiting based detection
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
enableRateLimiting?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* 🔎 Custom bot detection logic
|
|
46
|
+
* @default () => false
|
|
47
|
+
*/
|
|
48
|
+
customBotDetector?: (ctx: Context) => boolean | Promise<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* ✉️ Custom response for blocked requests
|
|
51
|
+
*/
|
|
52
|
+
customBlockedResponse?: (ctx: Context, result: BotDetectionResult) => void;
|
|
53
|
+
/**
|
|
54
|
+
* 🔄 Custom cache storage implementation (e.g., using `Map`, `Redis`, etc.).
|
|
55
|
+
* By default, it uses a `Map<string, { count: number; resetTime: number }>`.
|
|
56
|
+
*/
|
|
57
|
+
storage?: {
|
|
58
|
+
get: (key: string) => {
|
|
59
|
+
count: number;
|
|
60
|
+
resetTime: number;
|
|
61
|
+
} | undefined;
|
|
62
|
+
set: (key: string, value: {
|
|
63
|
+
count: number;
|
|
64
|
+
resetTime: number;
|
|
65
|
+
}) => void;
|
|
66
|
+
delete: (key: string) => void;
|
|
67
|
+
clearExpired: () => void;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* 📊 Minimum confidence score to consider as bot (0-1)
|
|
71
|
+
* @default 0.5
|
|
72
|
+
*/
|
|
73
|
+
confidenceThreshold?: number;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* 🤖 Advanced bot detection middleware with multiple detection methods
|
|
77
|
+
*
|
|
78
|
+
* Features:
|
|
79
|
+
* - User-Agent analysis
|
|
80
|
+
* - IP blacklisting
|
|
81
|
+
* - Query parameter detection
|
|
82
|
+
* - Rate limiting
|
|
83
|
+
* - Custom detection logic
|
|
84
|
+
* - Confidence-based scoring
|
|
85
|
+
*
|
|
86
|
+
* @param {DetectBotOptions} options - Configuration options
|
|
87
|
+
* @returns {Middleware} Configured middleware
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* // Basic usage
|
|
91
|
+
* app.use(detectBot());
|
|
92
|
+
*
|
|
93
|
+
* // Custom configuration
|
|
94
|
+
* app.use(detectBot({
|
|
95
|
+
* botUserAgents: ["bot", "crawler"],
|
|
96
|
+
* isBlacklistedIP: async (ip) => await checkIPReputation(ip),
|
|
97
|
+
* onBotDetected: (ctx, { reason }) => {
|
|
98
|
+
* ctx.status = 403;
|
|
99
|
+
* ctx.body = { error: `Bot detected (${reason})` };
|
|
100
|
+
* }
|
|
101
|
+
* }));
|
|
102
|
+
*/
|
|
103
|
+
export declare const detectBot: (options?: DetectBotOptions) => Middleware;
|
|
104
|
+
export declare function createRateLimitDefaultStorage(): {
|
|
105
|
+
get: (key: string) => {
|
|
106
|
+
count: number;
|
|
107
|
+
resetTime: number;
|
|
108
|
+
} | undefined;
|
|
109
|
+
set: (key: string, value: {
|
|
110
|
+
count: number;
|
|
111
|
+
resetTime: number;
|
|
112
|
+
}) => Map<string, {
|
|
113
|
+
count: number;
|
|
114
|
+
resetTime: number;
|
|
115
|
+
}>;
|
|
116
|
+
delete: (key: string) => boolean;
|
|
117
|
+
clearExpired: () => void;
|
|
118
|
+
};
|
|
119
|
+
export declare function isRateLimit(ctx: Context, key: string, store: any, maxRequests: number, windowMs: number): {
|
|
120
|
+
check: boolean;
|
|
121
|
+
entry: any;
|
|
122
|
+
};
|
|
123
|
+
export {};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { GlobalConfig } from "../core/config";
|
|
2
|
+
export const detectBot = (options = {}) => {
|
|
3
|
+
const { botUserAgents = ["bot", "spider", "crawl", "slurp"], maxRequests = 30, windowMs = 60000, isBlacklistedIP = async () => false, queryKeyBot = "bot", onBotDetected = "block", enableRateLimiting = false, customBotDetector = async () => false, customBlockedResponse = (ctx, { reason }) => {
|
|
4
|
+
ctx.setStatus = 403;
|
|
5
|
+
ctx.body = { error: `Bot detected: ${reason}` };
|
|
6
|
+
}, storage, confidenceThreshold = 0.5, } = options;
|
|
7
|
+
let store = storage;
|
|
8
|
+
if (enableRateLimiting) {
|
|
9
|
+
store = createRateLimitDefaultStorage();
|
|
10
|
+
}
|
|
11
|
+
return async (ctx, next) => {
|
|
12
|
+
const detectionResult = {
|
|
13
|
+
isBot: false,
|
|
14
|
+
indicators: [],
|
|
15
|
+
};
|
|
16
|
+
const userAgent = ctx.headers.get("user-agent")?.toLowerCase() || "";
|
|
17
|
+
const ip = ctx.req.remoteAddress?.address || "unknown";
|
|
18
|
+
const isBotQuery = ctx.req.query[queryKeyBot] === "true";
|
|
19
|
+
if (botUserAgents.some((agent) => userAgent.includes(agent))) {
|
|
20
|
+
detectionResult.indicators.push("User-Agent");
|
|
21
|
+
}
|
|
22
|
+
if (await isBlacklistedIP(ip)) {
|
|
23
|
+
detectionResult.indicators.push("Blacklisted IP");
|
|
24
|
+
}
|
|
25
|
+
if (isBotQuery) {
|
|
26
|
+
detectionResult.indicators.push("Query Parameter");
|
|
27
|
+
}
|
|
28
|
+
const key = `${ctx.req.remoteAddress.address}:${ctx.req.remoteAddress.port}`;
|
|
29
|
+
if (enableRateLimiting &&
|
|
30
|
+
isRateLimit(ctx, key, store, maxRequests, windowMs).check) {
|
|
31
|
+
detectionResult.indicators.push("Rate Limiting");
|
|
32
|
+
}
|
|
33
|
+
if (await customBotDetector(ctx)) {
|
|
34
|
+
detectionResult.indicators.push("Custom Detector");
|
|
35
|
+
}
|
|
36
|
+
detectionResult.isBot = detectionResult.indicators.length > 0;
|
|
37
|
+
if (detectionResult.indicators.length > 1) {
|
|
38
|
+
detectionResult.reason = "Multiple Indicators";
|
|
39
|
+
const confidence = Math.min(0.3 * detectionResult.indicators.length, 1);
|
|
40
|
+
detectionResult.isBot = confidence >= confidenceThreshold;
|
|
41
|
+
}
|
|
42
|
+
else if (detectionResult.indicators.length === 1) {
|
|
43
|
+
detectionResult.reason = detectionResult.indicators[0];
|
|
44
|
+
}
|
|
45
|
+
if (detectionResult.isBot) {
|
|
46
|
+
GlobalConfig.debugging.warn(`Bot detected: ${detectionResult.reason}`, {
|
|
47
|
+
ip,
|
|
48
|
+
userAgent,
|
|
49
|
+
indicators: detectionResult.indicators,
|
|
50
|
+
});
|
|
51
|
+
if (onBotDetected === "block") {
|
|
52
|
+
return customBlockedResponse(ctx, detectionResult);
|
|
53
|
+
}
|
|
54
|
+
else if (typeof onBotDetected === "function") {
|
|
55
|
+
return onBotDetected(ctx, detectionResult);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return await next();
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
export function createRateLimitDefaultStorage() {
|
|
62
|
+
const store = new Map();
|
|
63
|
+
return {
|
|
64
|
+
get: (key) => store.get(key),
|
|
65
|
+
set: (key, value) => store.set(key, value),
|
|
66
|
+
delete: (key) => store.delete(key),
|
|
67
|
+
clearExpired: () => {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
for (const [key, entry] of store.entries()) {
|
|
70
|
+
if (now >= entry.resetTime) {
|
|
71
|
+
store.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function isRateLimit(ctx, key, store, maxRequests, windowMs) {
|
|
78
|
+
store.clearExpired();
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
let entry = store.get(key) || { count: 0, resetTime: now + windowMs };
|
|
81
|
+
if (now < entry.resetTime) {
|
|
82
|
+
entry.count++;
|
|
83
|
+
if (entry.count > maxRequests) {
|
|
84
|
+
return {
|
|
85
|
+
check: true,
|
|
86
|
+
entry: entry,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
entry = { count: 1, resetTime: now + windowMs };
|
|
92
|
+
}
|
|
93
|
+
store.set(key, entry);
|
|
94
|
+
return {
|
|
95
|
+
check: false,
|
|
96
|
+
entry: entry,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -8,10 +8,10 @@ export type loadTranslations = (language: string) => Promise<{
|
|
|
8
8
|
}>;
|
|
9
9
|
export type I18nOptions = {
|
|
10
10
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
* 🌐 Function to load translations dynamically
|
|
12
|
+
* @param language - Language code to load (e.g., "en-US")
|
|
13
|
+
* @returns Promise with translations map and optional expiration
|
|
14
|
+
*/
|
|
15
15
|
loadTranslations: (language: string) => Promise<{
|
|
16
16
|
translations: TranslationMap;
|
|
17
17
|
expiresAt?: Date | number;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { GlobalConfig } from "../core/config";
|
|
2
2
|
export const i18nMiddleware = (options) => {
|
|
3
3
|
const { loadTranslations, defaultCacheDuration = 3600000, isCacheValid = (cached) => cached.expiresAt > Date.now(), detectLanguage = (ctx) => ctx.req.query.lang ||
|
|
4
|
-
ctx.cookies?.get(
|
|
5
|
-
ctx.headers.get(
|
|
4
|
+
ctx.cookies?.get("lang") ||
|
|
5
|
+
ctx.req.headers.get("accept-language")?.split(",")[0] ||
|
|
6
6
|
options.defaultLanguage ||
|
|
7
|
-
|
|
8
|
-
return Object.entries(options).reduce((msg, [key, value]) => msg.replace(new RegExp(`{{${key}}}`,
|
|
9
|
-
}, cacheTranslations = true } = options;
|
|
7
|
+
"en", defaultLanguage = "en", fallbackChain = [], translationFunctionKey = "t", formatMessage = (message, options = {}) => {
|
|
8
|
+
return Object.entries(options).reduce((msg, [key, value]) => msg.replace(new RegExp(`{{${key}}}`, "g"), String(value)), message);
|
|
9
|
+
}, cacheTranslations = true, } = options;
|
|
10
10
|
const translationCache = {};
|
|
11
11
|
return async (ctx, next) => {
|
|
12
12
|
try {
|
|
@@ -14,12 +14,12 @@ export const i18nMiddleware = (options) => {
|
|
|
14
14
|
const languageChain = [
|
|
15
15
|
detectedLanguage,
|
|
16
16
|
...fallbackChain,
|
|
17
|
-
defaultLanguage
|
|
17
|
+
defaultLanguage,
|
|
18
18
|
].filter(Boolean);
|
|
19
19
|
let translations = null;
|
|
20
20
|
let selectedLanguage = defaultLanguage;
|
|
21
21
|
for (const lang of languageChain) {
|
|
22
|
-
const normalizedLang = lang.split(
|
|
22
|
+
const normalizedLang = lang.split("-")[0].toLowerCase();
|
|
23
23
|
if (cacheTranslations && translationCache[normalizedLang]) {
|
|
24
24
|
const cached = translationCache[normalizedLang];
|
|
25
25
|
if (isCacheValid(cached, normalizedLang)) {
|
|
@@ -35,7 +35,7 @@ export const i18nMiddleware = (options) => {
|
|
|
35
35
|
if (expiresAt instanceof Date) {
|
|
36
36
|
expirationTime = expiresAt.getTime();
|
|
37
37
|
}
|
|
38
|
-
else if (typeof expiresAt ===
|
|
38
|
+
else if (typeof expiresAt === "number") {
|
|
39
39
|
expirationTime = expiresAt;
|
|
40
40
|
}
|
|
41
41
|
translations = loadedTranslations;
|
|
@@ -53,13 +53,13 @@ export const i18nMiddleware = (options) => {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
if (!translations) {
|
|
56
|
-
throw new Error(
|
|
56
|
+
throw new Error("No translations available");
|
|
57
57
|
}
|
|
58
58
|
ctx[translationFunctionKey] = (key, options) => {
|
|
59
|
-
const value = key.split(
|
|
60
|
-
return
|
|
59
|
+
const value = key.split(".").reduce((acc, k) => {
|
|
60
|
+
return acc && typeof acc === "object" ? acc[k] : undefined;
|
|
61
61
|
}, translations);
|
|
62
|
-
const message = typeof value ===
|
|
62
|
+
const message = typeof value === "string" ? value : key;
|
|
63
63
|
return formatMessage(message, options);
|
|
64
64
|
};
|
|
65
65
|
ctx.language = selectedLanguage;
|
|
@@ -67,7 +67,7 @@ export const i18nMiddleware = (options) => {
|
|
|
67
67
|
return await next();
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
|
-
GlobalConfig.debugging.error(
|
|
70
|
+
GlobalConfig.debugging.error("i18n processing error:", error);
|
|
71
71
|
ctx.setStatus = 500;
|
|
72
72
|
throw error;
|
|
73
73
|
}
|
package/middleware/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { cors } from "./cors";
|
|
2
2
|
export type { CorsOptions } from "./cors";
|
|
3
|
+
export { detectBot } from "./detectBot";
|
|
4
|
+
export type { DetectBotReason } from "./detectBot";
|
|
5
|
+
export * from "./i18nMiddleware";
|
|
6
|
+
export * from "./lazyLoadModules";
|
|
3
7
|
export * from "./logger";
|
|
8
|
+
export * from "./pagination";
|
|
4
9
|
export * from "./powered-by";
|
|
10
|
+
export * from "./rateLimiter";
|
|
5
11
|
export * from "./request-id";
|
|
12
|
+
export * from "./sanitizeHeader";
|
|
6
13
|
export * from "./secureHeaders";
|
|
7
14
|
export * from "./xssProtection";
|
|
8
|
-
export * from "./sanitizeHeader";
|
|
9
|
-
export * from "./rateLimiter";
|
|
10
|
-
export * from "./pagination";
|
|
11
|
-
export * from "./i18nMiddleware";
|
|
12
|
-
export * from './lazyLoadModules';
|
package/middleware/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
export { cors } from "./cors";
|
|
2
|
+
export { detectBot } from "./detectBot";
|
|
3
|
+
export * from "./i18nMiddleware";
|
|
4
|
+
export * from "./lazyLoadModules";
|
|
2
5
|
export * from "./logger";
|
|
6
|
+
export * from "./pagination";
|
|
3
7
|
export * from "./powered-by";
|
|
8
|
+
export * from "./rateLimiter";
|
|
4
9
|
export * from "./request-id";
|
|
10
|
+
export * from "./sanitizeHeader";
|
|
5
11
|
export * from "./secureHeaders";
|
|
6
12
|
export * from "./xssProtection";
|
|
7
|
-
export * from "./sanitizeHeader";
|
|
8
|
-
export * from "./rateLimiter";
|
|
9
|
-
export * from "./pagination";
|
|
10
|
-
export * from "./i18nMiddleware";
|
|
11
|
-
export * from './lazyLoadModules';
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { GlobalConfig } from "../core/config";
|
|
2
|
-
;
|
|
3
2
|
export const lazyLoadModules = (options) => {
|
|
4
|
-
const { moduleKey = (ctx) => ctx.req.params[queryKeyModule] || ctx.req.query[queryKeyModule], getModuleLoader, queryKeyModule = "module", moduleContextKey = "module", cacheTTL = 3600000, dependencies = {}, enableCache = true, cacheStorage = new Map(), lifecycleHooks = {}, validateModule } = options;
|
|
3
|
+
const { moduleKey = (ctx) => ctx.req.params[queryKeyModule] || ctx.req.query[queryKeyModule], getModuleLoader, queryKeyModule = "module", moduleContextKey = "module", cacheTTL = 3600000, dependencies = {}, enableCache = true, cacheStorage = new Map(), lifecycleHooks = {}, validateModule, } = options;
|
|
5
4
|
return async (ctx, next) => {
|
|
6
|
-
let moduleName = moduleKey(ctx) ||
|
|
5
|
+
let moduleName = moduleKey(ctx) ||
|
|
6
|
+
ctx.req.params[queryKeyModule] ||
|
|
7
|
+
ctx.req.query[queryKeyModule];
|
|
7
8
|
if (!moduleName) {
|
|
8
9
|
GlobalConfig.debugging.warn("No module specified for lazy loading.");
|
|
9
10
|
return await next();
|
|
10
11
|
}
|
|
12
|
+
let storage = cacheStorage;
|
|
13
|
+
if (enableCache && !cacheStorage) {
|
|
14
|
+
storage = new Map();
|
|
15
|
+
}
|
|
11
16
|
try {
|
|
12
17
|
if (enableCache) {
|
|
13
|
-
const cached =
|
|
18
|
+
const cached = storage.get(moduleName);
|
|
14
19
|
if (cached) {
|
|
15
20
|
if (cached.expiresAt > Date.now()) {
|
|
16
|
-
|
|
21
|
+
storage.delete(moduleName);
|
|
17
22
|
}
|
|
18
23
|
else {
|
|
19
24
|
GlobalConfig.debugging.info(`Using cached module: ${moduleName}`);
|
|
@@ -38,9 +43,9 @@ export const lazyLoadModules = (options) => {
|
|
|
38
43
|
}
|
|
39
44
|
ctx.dependencies = dependencies;
|
|
40
45
|
if (enableCache) {
|
|
41
|
-
|
|
46
|
+
storage.set(moduleName, {
|
|
42
47
|
module,
|
|
43
|
-
expiresAt: Date.now() + cacheTTL
|
|
48
|
+
expiresAt: Date.now() + cacheTTL,
|
|
44
49
|
});
|
|
45
50
|
lifecycleHooks.onCacheSet?.(moduleName, module, ctx);
|
|
46
51
|
}
|
package/middleware/pagination.js
CHANGED
|
@@ -14,7 +14,11 @@ export const paginationHandler = (options = {}) => {
|
|
|
14
14
|
queryKeyLimit,
|
|
15
15
|
};
|
|
16
16
|
if (getDataSource) {
|
|
17
|
-
const dataSourceResponse = await getDataSource(ctx, {
|
|
17
|
+
const dataSourceResponse = await getDataSource(ctx, {
|
|
18
|
+
page,
|
|
19
|
+
limit,
|
|
20
|
+
offset,
|
|
21
|
+
});
|
|
18
22
|
const total = dataSourceResponse?.[countKey];
|
|
19
23
|
const data = dataSourceResponse?.[dataKey];
|
|
20
24
|
const pagination = {
|
|
@@ -25,10 +25,10 @@ export type RateLimiterOptions = {
|
|
|
25
25
|
// * @todo Implement Redis storage
|
|
26
26
|
// */
|
|
27
27
|
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
* 🔄 Custom cache storage implementation (e.g., using `Map`, `Redis`, etc.).
|
|
29
|
+
* By default, it uses a `Map<string, { count: number; resetTime: number }>`.
|
|
30
|
+
*/
|
|
31
|
+
storage?: {
|
|
32
32
|
get: (key: string) => {
|
|
33
33
|
count: number;
|
|
34
34
|
resetTime: number;
|
|
@@ -38,10 +38,7 @@ export type RateLimiterOptions = {
|
|
|
38
38
|
resetTime: number;
|
|
39
39
|
}) => void;
|
|
40
40
|
delete: (key: string) => void;
|
|
41
|
-
|
|
42
|
-
count: number;
|
|
43
|
-
resetTime: number;
|
|
44
|
-
}]>;
|
|
41
|
+
clearExpired: () => void;
|
|
45
42
|
};
|
|
46
43
|
/**
|
|
47
44
|
* 🛑 Custom rate limit exceeded handler
|
|
@@ -1,35 +1,20 @@
|
|
|
1
|
+
import { createRateLimitDefaultStorage, isRateLimit } from "./detectBot";
|
|
1
2
|
export const rateLimiter = (options) => {
|
|
2
|
-
const { maxRequests, windowMs, keyGenerator = (ctx) => `${ctx.req.remoteAddress.address}:${ctx.req.remoteAddress.port}`,
|
|
3
|
+
const { maxRequests, windowMs, keyGenerator = (ctx) => `${ctx.req.remoteAddress.address}:${ctx.req.remoteAddress.port}`, storage = createRateLimitDefaultStorage(), onError = (ctx, retryAfter, error) => {
|
|
3
4
|
ctx.setStatus = 429;
|
|
4
5
|
throw new Error(`Rate limit exceeded. Try again in ${retryAfter} seconds.`);
|
|
5
6
|
}, } = options;
|
|
6
7
|
return async (ctx, next) => {
|
|
7
8
|
const key = keyGenerator(ctx);
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (Date.now() >= entry.resetTime) {
|
|
12
|
-
cacheStorage.delete(key);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
const entry = cacheStorage.get(key);
|
|
16
|
-
if (entry && Date.now() < entry.resetTime) {
|
|
17
|
-
requestCount = entry.count + 1;
|
|
18
|
-
resetTime = entry.resetTime;
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
requestCount = 1;
|
|
22
|
-
resetTime = Date.now() + windowMs;
|
|
23
|
-
cacheStorage.set(key, { count: requestCount, resetTime });
|
|
24
|
-
}
|
|
25
|
-
if (requestCount > maxRequests) {
|
|
26
|
-
const retryAfter = Math.ceil((resetTime - Date.now()) / 1000);
|
|
9
|
+
const { check, entry } = isRateLimit(ctx, key, storage, maxRequests, windowMs);
|
|
10
|
+
if (check) {
|
|
11
|
+
const retryAfter = Math.ceil((entry.resetTime - Date.now()) / 1000);
|
|
27
12
|
ctx.headers.set("Retry-After", retryAfter.toString());
|
|
28
13
|
return onError(ctx, retryAfter, new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds.`));
|
|
29
14
|
}
|
|
30
15
|
ctx.headers.set("X-RateLimit-Limit", maxRequests.toString());
|
|
31
|
-
ctx.headers.set("X-RateLimit-Remaining", (maxRequests -
|
|
32
|
-
ctx.headers.set("X-RateLimit-Reset", resetTime.toString());
|
|
16
|
+
ctx.headers.set("X-RateLimit-Remaining", (maxRequests - entry.count).toString());
|
|
17
|
+
ctx.headers.set("X-RateLimit-Reset", entry.resetTime.toString());
|
|
33
18
|
return await next();
|
|
34
19
|
};
|
|
35
20
|
};
|
|
@@ -11,7 +11,7 @@ export const xssProtection = (options = {}) => {
|
|
|
11
11
|
ctx.headers.set("X-XSS-Protection", xssHeaderValue);
|
|
12
12
|
GlobalConfig.debugging.warn(`🟢 X-XSS-Protection set to: ${xssHeaderValue}`);
|
|
13
13
|
if (fallbackCSP) {
|
|
14
|
-
const existingCSP = ctx.headers.get("Content-Security-Policy");
|
|
14
|
+
const existingCSP = ctx.req.headers.get("Content-Security-Policy");
|
|
15
15
|
if (!existingCSP) {
|
|
16
16
|
ctx.headers.set("Content-Security-Policy", fallbackCSP);
|
|
17
17
|
GlobalConfig.debugging.warn(`🟣 Fallback CSP set to: ${fallbackCSP}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tezx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "TezX is a high-performance, lightweight JavaScript framework designed for speed, scalability, and flexibility. It enables efficient routing, middleware management, and static file serving with minimal configuration. Fully compatible with Node.js, Deno, and Bun.",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "index.js",
|