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.
@@ -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 ((!headers["Content-Type"] && !headers['content-type'])) {
171
- if (typeof body === "string" || typeof body == 'number') {
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) {
@@ -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.headers = new header_1.HeadersParser(req?.headers);
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.headers);
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.headers.get("content-type") || "";
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.headers.get("content-type") || "";
87
+ const contentType = this.#headers.get("content-type") || "";
59
88
  if (!contentType) {
60
89
  throw Error("Invalid Content-Type");
61
90
  }
@@ -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();
@@ -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.36";
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('lang') ||
8
- ctx.headers.get('accept-language')?.split(',')[0] ||
7
+ ctx.cookies?.get("lang") ||
8
+ ctx.req.headers.get("accept-language")?.split(",")[0] ||
9
9
  options.defaultLanguage ||
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;
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('-')[0].toLowerCase();
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 === 'number') {
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('No translations available');
59
+ throw new Error("No translations available");
60
60
  }
61
61
  ctx[translationFunctionKey] = (key, options) => {
62
- const value = key.split('.').reduce((acc, k) => {
63
- return (acc && typeof acc === 'object') ? acc[k] : undefined;
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 === 'string' ? value : key;
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('i18n processing error:', error);
73
+ config_1.GlobalConfig.debugging.error("i18n processing error:", error);
74
74
  ctx.setStatus = 500;
75
75
  throw error;
76
76
  }
@@ -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) || ctx.req.params[queryKeyModule] || ctx.req.query[queryKeyModule];
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 = cacheStorage.get(moduleName);
21
+ const cached = storage.get(moduleName);
17
22
  if (cached) {
18
23
  if (cached.expiresAt > Date.now()) {
19
- cacheStorage.delete(moduleName);
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
- cacheStorage.set(moduleName, {
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, { page, limit, offset });
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}`, cacheStorage = new Map(), onError = (ctx, retryAfter, error) => {
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
- let requestCount;
12
- let resetTime;
13
- for (const [key, entry] of cacheStorage.entries()) {
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 - requestCount).toString());
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 ((!headers["Content-Type"] && !headers['content-type'])) {
168
- if (typeof body === "string" || typeof body == 'number') {
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
- headers: HeadersParser;
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.headers = new HeadersParser(req?.headers);
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.headers);
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.headers.get("content-type") || "";
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.headers.get("content-type") || "";
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
@@ -1,4 +1,4 @@
1
1
  export { Router } from "./core/router";
2
2
  export { TezX } from "./core/server";
3
3
  export { useParams } from "./utils/params";
4
- export let version = "1.0.36";
4
+ export let version = "1.0.38";
@@ -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
- * 🌐 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
- */
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('lang') ||
5
- ctx.headers.get('accept-language')?.split(',')[0] ||
4
+ ctx.cookies?.get("lang") ||
5
+ ctx.req.headers.get("accept-language")?.split(",")[0] ||
6
6
  options.defaultLanguage ||
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;
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('-')[0].toLowerCase();
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 === 'number') {
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('No translations available');
56
+ throw new Error("No translations available");
57
57
  }
58
58
  ctx[translationFunctionKey] = (key, options) => {
59
- const value = key.split('.').reduce((acc, k) => {
60
- return (acc && typeof acc === 'object') ? acc[k] : undefined;
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 === 'string' ? value : key;
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('i18n processing error:', error);
70
+ GlobalConfig.debugging.error("i18n processing error:", error);
71
71
  ctx.setStatus = 500;
72
72
  throw error;
73
73
  }
@@ -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';
@@ -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) || ctx.req.params[queryKeyModule] || ctx.req.query[queryKeyModule];
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 = cacheStorage.get(moduleName);
18
+ const cached = storage.get(moduleName);
14
19
  if (cached) {
15
20
  if (cached.expiresAt > Date.now()) {
16
- cacheStorage.delete(moduleName);
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
- cacheStorage.set(moduleName, {
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
  }
@@ -14,7 +14,11 @@ export const paginationHandler = (options = {}) => {
14
14
  queryKeyLimit,
15
15
  };
16
16
  if (getDataSource) {
17
- const dataSourceResponse = await getDataSource(ctx, { page, limit, offset });
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
- * 🔄 Custom cache storage implementation (e.g., using `Map`, `Redis`, etc.).
29
- * By default, it uses a `Map<string, { count: number; resetTime: number }>`.
30
- */
31
- cacheStorage?: {
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
- entries: () => IterableIterator<[string, {
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}`, cacheStorage = new Map(), onError = (ctx, retryAfter, error) => {
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
- let requestCount;
9
- let resetTime;
10
- for (const [key, entry] of cacheStorage.entries()) {
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 - requestCount).toString());
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.36",
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",