tezx 3.0.14-beta → 3.0.14-beta2
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/bun/env.d.ts +5 -0
- package/bun/env.js +44 -0
- package/bun/index.d.ts +1 -1
- package/bun/index.js +1 -1
- package/bun/ws.js +4 -3
- package/cjs/bun/env.js +47 -0
- package/cjs/bun/index.js +1 -1
- package/cjs/bun/ws.js +4 -3
- package/cjs/core/context.js +4 -3
- package/cjs/core/error.js +8 -0
- package/cjs/core/request.js +3 -2
- package/cjs/core/router.js +11 -17
- package/cjs/core/server.js +14 -9
- package/cjs/deno/env.js +2 -1
- package/cjs/deno/serveStatic.js +2 -1
- package/cjs/deno/ws.js +4 -3
- package/cjs/helper/index.js +12 -10
- package/cjs/index.js +1 -1
- package/cjs/jwt/node.js +94 -0
- package/cjs/jwt/web.js +178 -0
- package/cjs/middleware/basic-auth.js +9 -14
- package/cjs/middleware/bearer-auth.js +5 -5
- package/cjs/middleware/cache-control.js +44 -0
- package/cjs/middleware/cors.js +1 -1
- package/cjs/middleware/detect-bot.js +57 -0
- package/cjs/middleware/i18n.js +92 -0
- package/cjs/middleware/index.js +5 -0
- package/cjs/middleware/logger.js +3 -2
- package/cjs/middleware/rate-limiter.js +1 -1
- package/cjs/middleware/sanitize-headers.js +1 -1
- package/cjs/middleware/secure-headers copy.js +143 -0
- package/cjs/middleware/secure-headers.js +157 -0
- package/cjs/node/env.js +4 -3
- package/cjs/node/serveStatic.js +2 -1
- package/cjs/node/ws.js +3 -2
- package/cjs/registry/RadixRouter.js +2 -33
- package/cjs/utils/buffer.js +17 -0
- package/cjs/utils/file.js +28 -6
- package/cjs/utils/generateID.js +10 -0
- package/cjs/utils/response.js +3 -1
- package/core/context.d.ts +3 -3
- package/core/context.js +4 -3
- package/core/error.d.ts +1 -0
- package/core/error.js +7 -0
- package/core/request.js +3 -2
- package/core/router.d.ts +3 -8
- package/core/router.js +11 -17
- package/core/server.d.ts +10 -23
- package/core/server.js +15 -10
- package/deno/env.js +2 -1
- package/deno/index.d.ts +1 -1
- package/deno/serveStatic.js +2 -1
- package/deno/ws.d.ts +1 -1
- package/deno/ws.js +4 -3
- package/helper/index.d.ts +7 -6
- package/helper/index.js +7 -6
- package/index.d.ts +5 -2
- package/index.js +1 -1
- package/jwt/node.d.ts +39 -0
- package/jwt/node.js +87 -0
- package/jwt/web.d.ts +14 -0
- package/jwt/web.js +174 -0
- package/middleware/basic-auth.d.ts +2 -1
- package/middleware/basic-auth.js +9 -14
- package/middleware/bearer-auth.d.ts +2 -1
- package/middleware/bearer-auth.js +5 -5
- package/middleware/cache-control.d.ts +30 -0
- package/middleware/cache-control.js +40 -0
- package/middleware/cors.js +1 -1
- package/middleware/detect-bot.d.ts +113 -0
- package/middleware/detect-bot.js +53 -0
- package/middleware/i18n.d.ts +194 -0
- package/middleware/i18n.js +88 -0
- package/middleware/index.d.ts +5 -0
- package/middleware/index.js +5 -0
- package/middleware/logger.d.ts +1 -1
- package/middleware/logger.js +3 -2
- package/middleware/rate-limiter.d.ts +3 -2
- package/middleware/rate-limiter.js +1 -1
- package/middleware/sanitize-headers.js +1 -1
- package/middleware/secure-headers copy.d.ts +15 -0
- package/middleware/secure-headers copy.js +136 -0
- package/middleware/secure-headers.d.ts +132 -0
- package/middleware/secure-headers.js +153 -0
- package/node/env.js +4 -3
- package/node/serveStatic.d.ts +8 -0
- package/node/serveStatic.js +2 -1
- package/node/ws.d.ts +1 -1
- package/node/ws.js +3 -2
- package/package.json +12 -1
- package/registry/RadixRouter.js +2 -33
- package/types/index.d.ts +1 -1
- package/utils/buffer.d.ts +1 -0
- package/utils/buffer.js +14 -0
- package/utils/file.d.ts +6 -2
- package/utils/file.js +27 -6
- package/utils/generateID.d.ts +9 -0
- package/utils/generateID.js +9 -0
- package/utils/response.js +3 -1
- package/cjs/utils/regexRouter.js +0 -57
- package/utils/regexRouter.d.ts +0 -66
- package/utils/regexRouter.js +0 -52
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TezXError, TezXErrorParse } from "../core/error.js";
|
|
1
2
|
export const bearerAuth = (options) => {
|
|
2
3
|
const { validate, realm = "API", onUnauthorized = (ctx, error) => {
|
|
3
4
|
ctx.setStatus = 401;
|
|
@@ -7,23 +8,22 @@ export const bearerAuth = (options) => {
|
|
|
7
8
|
return async (ctx, next) => {
|
|
8
9
|
const auth = ctx.req.header("authorization");
|
|
9
10
|
if (!auth || !auth.startsWith("Bearer ")) {
|
|
10
|
-
return onUnauthorized(ctx, new
|
|
11
|
+
return onUnauthorized(ctx, new TezXError("Bearer token required"));
|
|
11
12
|
}
|
|
12
13
|
const token = auth.slice(7).trim();
|
|
13
14
|
if (!token) {
|
|
14
|
-
return onUnauthorized(ctx, new
|
|
15
|
+
return onUnauthorized(ctx, new TezXError("Empty token"));
|
|
15
16
|
}
|
|
16
17
|
try {
|
|
17
18
|
const valid = await validate(token, ctx);
|
|
18
19
|
if (!valid) {
|
|
19
|
-
return onUnauthorized(ctx, new
|
|
20
|
+
return onUnauthorized(ctx, new TezXError("Invalid or expired token"));
|
|
20
21
|
}
|
|
21
22
|
ctx.token = token;
|
|
22
23
|
await next();
|
|
23
24
|
}
|
|
24
25
|
catch (err) {
|
|
25
|
-
|
|
26
|
-
return onUnauthorized(ctx, error);
|
|
26
|
+
return onUnauthorized(ctx, TezXErrorParse(err));
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
29
|
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Context } from "../core/context.js";
|
|
2
|
+
import { TezXError } from "../core/error.js";
|
|
3
|
+
import { HttpBaseResponse, Middleware } from "../types/index.js";
|
|
4
|
+
export interface CacheRule {
|
|
5
|
+
/** 🎯 Condition to determine if this rule applies */
|
|
6
|
+
condition: (ctx: Context) => boolean;
|
|
7
|
+
/** ⏳ Max age (seconds) */
|
|
8
|
+
maxAge: number;
|
|
9
|
+
/** 🌐 Cache scope */
|
|
10
|
+
scope: "public" | "private";
|
|
11
|
+
/** 🏷️ Vary header list */
|
|
12
|
+
vary?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface CacheSettings extends Pick<CacheRule, "maxAge" | "scope" | "vary"> {
|
|
15
|
+
}
|
|
16
|
+
export interface CacheOptions {
|
|
17
|
+
/** 🧱 Default cache behavior */
|
|
18
|
+
defaultSettings: CacheSettings;
|
|
19
|
+
/** 🔧 Optional rules for dynamic caching */
|
|
20
|
+
rules?: readonly CacheRule[];
|
|
21
|
+
/** 📝 Logging hook */
|
|
22
|
+
/** 🚨 Error handler */
|
|
23
|
+
onError?: (error: TezXError, ctx: Context) => HttpBaseResponse;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* ⚡ Ultra-lightweight, low-level cache control middleware.
|
|
27
|
+
* Adds 'Cache-Control', 'Expires', and optional 'Vary' headers.
|
|
28
|
+
*/
|
|
29
|
+
export declare const cacheControl: (opts: CacheOptions) => Middleware;
|
|
30
|
+
export default cacheControl;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { TezXErrorParse } from "../core/error.js";
|
|
2
|
+
export const cacheControl = (opts) => {
|
|
3
|
+
const { defaultSettings, rules = [], onError = (err, ctx) => {
|
|
4
|
+
ctx.setStatus = 500;
|
|
5
|
+
ctx.body = { error: err.message ?? "Cache middleware failed" };
|
|
6
|
+
}, } = opts;
|
|
7
|
+
const len = rules.length | 0;
|
|
8
|
+
return async function cacheControlMiddleware(ctx, next) {
|
|
9
|
+
const method = ctx.method;
|
|
10
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
11
|
+
return await next();
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
await next();
|
|
15
|
+
let matched;
|
|
16
|
+
for (let i = 0; i < len; i++) {
|
|
17
|
+
const rule = rules[i];
|
|
18
|
+
if (rule.condition(ctx)) {
|
|
19
|
+
matched = rule;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const settings = matched ?? defaultSettings;
|
|
24
|
+
const maxAge = settings.maxAge;
|
|
25
|
+
const scope = settings.scope;
|
|
26
|
+
const vary = settings.vary;
|
|
27
|
+
const cacheValue = `${scope}, max-age=${maxAge}`;
|
|
28
|
+
const expiresValue = new Date(Date.now() + maxAge * 1000).toUTCString();
|
|
29
|
+
const headers = ctx.headers;
|
|
30
|
+
headers.set("Cache-Control", cacheValue);
|
|
31
|
+
headers.set("Expires", expiresValue);
|
|
32
|
+
if (vary && vary.length > 0)
|
|
33
|
+
headers.set("Vary", vary.join(", "));
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
return onError(TezXErrorParse(err), ctx);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export default cacheControl;
|
package/middleware/cors.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
function cors(option = {}) {
|
|
2
|
-
const { credentials, maxAge, origin
|
|
2
|
+
const { credentials, maxAge, origin } = option;
|
|
3
3
|
let methods = (option.methods || ["GET", "POST", "PUT", "DELETE"]).join(", ");
|
|
4
4
|
let allowedHeaders = (option.allowedHeaders || ["Content-Type", "Authorization"]).join(", ");
|
|
5
5
|
let exposedHeaders = option?.exposedHeaders?.join(", ");
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Context, Middleware } from "../index.js";
|
|
2
|
+
import { HttpBaseResponse } from "../types/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* ⚙️ Configuration options for the `detectBot` middleware.
|
|
5
|
+
*/
|
|
6
|
+
export type DetectBotOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* 🤖 List of known bot-like User-Agent patterns.
|
|
9
|
+
* @default ["bot", "spider", "crawl", "slurp"]
|
|
10
|
+
* @example
|
|
11
|
+
* botUserAgents: ["bot", "crawler", "indexer"]
|
|
12
|
+
*/
|
|
13
|
+
botUserAgents?: string[];
|
|
14
|
+
/**
|
|
15
|
+
* ⚖️ Enable rate-limiting based bot detection.
|
|
16
|
+
* Requires `getConnInfo()` import from TezX runtime.
|
|
17
|
+
* @default false
|
|
18
|
+
* @example
|
|
19
|
+
* enableRateLimiting: true
|
|
20
|
+
*/
|
|
21
|
+
enableRateLimiting?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* ⚠️ Maximum allowed requests in the rate-limit window.
|
|
24
|
+
* Only used when `enableRateLimiting` is true.
|
|
25
|
+
* @default 30
|
|
26
|
+
*/
|
|
27
|
+
maxRequests?: number;
|
|
28
|
+
/**
|
|
29
|
+
* ⏱️ Time window for rate-limiting (in milliseconds).
|
|
30
|
+
* @default 60000 (1 minute)
|
|
31
|
+
*/
|
|
32
|
+
windowMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* 🔄 Custom rate-limit storage implementation.
|
|
35
|
+
* Allows integration with Redis, Memcached, or in-memory stores.
|
|
36
|
+
* @default In-memory Map
|
|
37
|
+
* @example
|
|
38
|
+
* storage: {
|
|
39
|
+
* get: (key) => myRedisClient.get(key),
|
|
40
|
+
* set: (key, value) => myRedisClient.set(key, value),
|
|
41
|
+
* clearExpired: () => {}
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
storage?: {
|
|
45
|
+
get: (key: string) => {
|
|
46
|
+
count: number;
|
|
47
|
+
resetTime: number;
|
|
48
|
+
} | undefined;
|
|
49
|
+
set: (key: string, value: {
|
|
50
|
+
count: number;
|
|
51
|
+
resetTime: number;
|
|
52
|
+
}) => void;
|
|
53
|
+
clearExpired: () => void;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* 🚫 Optional IP blacklist checker.
|
|
57
|
+
* Should return true if the current request’s IP is banned or suspicious.
|
|
58
|
+
* @default () => false
|
|
59
|
+
* @example
|
|
60
|
+
* isBlacklisted: (ctx) => ctx.req.remoteAddress.address === "192.168.0.10"
|
|
61
|
+
*/
|
|
62
|
+
isBlacklisted?: (ctx: Context) => boolean | Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* 🧠 Custom bot detector function.
|
|
65
|
+
* Use this for advanced heuristics (e.g., suspicious query params or headers).
|
|
66
|
+
* @example
|
|
67
|
+
* customBotDetector: (ctx) => ctx.query?.token === "weird"
|
|
68
|
+
*/
|
|
69
|
+
customBotDetector?: (ctx: Context) => boolean | Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* 🛡️ Action executed when a bot is detected.
|
|
72
|
+
* Can return a custom HTTP response (e.g., JSON, HTML, or redirect).
|
|
73
|
+
* @default Responds with 403 and JSON error.
|
|
74
|
+
* @example
|
|
75
|
+
* onBotDetected: (ctx, reason) => ctx.status(403).json({ error: `Blocked: ${reason}` })
|
|
76
|
+
*/
|
|
77
|
+
onBotDetected?: (ctx: Context, reason: string) => HttpBaseResponse;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* 🤖 Smart Bot Detection Middleware
|
|
81
|
+
*
|
|
82
|
+
* Detects automated or malicious requests using a combination of:
|
|
83
|
+
* - User-Agent analysis
|
|
84
|
+
* - IP blacklisting
|
|
85
|
+
* - Rate limiting
|
|
86
|
+
* - Custom detection logic
|
|
87
|
+
*
|
|
88
|
+
* 🧩 Supports:
|
|
89
|
+
* - Bun (uses precompiled RegExp for speed)
|
|
90
|
+
* - Node.js / Deno (optimized `includes()` loop)
|
|
91
|
+
*
|
|
92
|
+
* ⚙️ Requirements (for rate limiting):
|
|
93
|
+
* You must import `getConnInfo` from your runtime adapter:
|
|
94
|
+
* ```ts
|
|
95
|
+
* import { getConnInfo } from "tezx/bun";
|
|
96
|
+
* // or
|
|
97
|
+
* import { getConnInfo } from "tezx/node";
|
|
98
|
+
* // or
|
|
99
|
+
* import { getConnInfo } from "tezx/deno";
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* 📦 Example Usage:
|
|
103
|
+
* ```ts
|
|
104
|
+
* import { detectBot } from "tezx/middleware/detectBot";
|
|
105
|
+
*
|
|
106
|
+
* app.use(detectBot({
|
|
107
|
+
* enableRateLimiting: true,
|
|
108
|
+
* botUserAgents: ["bot", "crawler"],
|
|
109
|
+
* onBotDetected: (ctx, reason) => ctx.status(403).json({ error: `Bot detected: ${reason}` })
|
|
110
|
+
* }));
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export declare const detectBot: (opts?: DetectBotOptions) => Middleware;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { GlobalConfig } from "../core/config.js";
|
|
2
|
+
import { createRateLimitDefaultStorage, isRateLimit, } from "../utils/rateLimit.js";
|
|
3
|
+
import { runtime } from "../utils/runtime.js";
|
|
4
|
+
export const detectBot = (opts = {}) => {
|
|
5
|
+
const botUAs = opts.botUserAgents || ["bot", "spider", "crawl", "slurp"];
|
|
6
|
+
let checkBot;
|
|
7
|
+
if (runtime === "bun") {
|
|
8
|
+
const botRegex = new RegExp(botUAs.join("|"), "i");
|
|
9
|
+
checkBot = (ua) => botRegex.test(ua);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
checkBot = (ua) => {
|
|
13
|
+
for (const b of botUAs)
|
|
14
|
+
if (ua.includes(b))
|
|
15
|
+
return true;
|
|
16
|
+
return false;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const maxReq = opts.maxRequests || 30;
|
|
20
|
+
const winMs = opts.windowMs || 60000;
|
|
21
|
+
const enableRL = !!opts.enableRateLimiting;
|
|
22
|
+
const onBot = opts.onBotDetected ??
|
|
23
|
+
((ctx, reason) => ctx.status(403).json({ error: `Bot detected: ${reason}` }));
|
|
24
|
+
let store = opts.storage;
|
|
25
|
+
if (enableRL && !store)
|
|
26
|
+
store = createRateLimitDefaultStorage();
|
|
27
|
+
return async (ctx, next) => {
|
|
28
|
+
const ua = ctx.headers.get("user-agent") || "";
|
|
29
|
+
if (checkBot(ua)) {
|
|
30
|
+
return onBot?.(ctx, "User-Agent");
|
|
31
|
+
}
|
|
32
|
+
if (await opts?.isBlacklisted?.(ctx)) {
|
|
33
|
+
return onBot?.(ctx, "Blacklisted IP");
|
|
34
|
+
}
|
|
35
|
+
if (enableRL) {
|
|
36
|
+
const addr = ctx.req.remoteAddress;
|
|
37
|
+
if (!addr) {
|
|
38
|
+
GlobalConfig.debugging.warn("[TezX detectBot] Missing remoteAddress. Use `getConnInfo(ctx)` to enable rate limiting.");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const key = `${addr.address}:${addr.port || 0}`;
|
|
42
|
+
const { check, entry } = isRateLimit(key, store, maxReq, winMs);
|
|
43
|
+
if (check && addr.address) {
|
|
44
|
+
return onBot?.(ctx, `Rate limit exceeded. Retry after ${Math.ceil((entry.resetTime - Date.now()) / 1000)} seconds.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (await opts.customBotDetector?.(ctx)) {
|
|
49
|
+
return onBot?.(ctx, "Custom Detector");
|
|
50
|
+
}
|
|
51
|
+
return await next();
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Context, Middleware } from "../index.js";
|
|
2
|
+
export type TranslationMap = {
|
|
3
|
+
[key: string]: string | TranslationMap;
|
|
4
|
+
};
|
|
5
|
+
export type loadTranslations = (language: string) => Promise<TranslationMap>;
|
|
6
|
+
/**
|
|
7
|
+
* 📦 Interface defining a pluggable cache adapter for i18n translations.
|
|
8
|
+
*
|
|
9
|
+
* You can implement this interface to provide custom caching logic —
|
|
10
|
+
* for example, in-memory, Redis, file-based, or distributed cache.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const redisCache: I18nCacheAdapter = {
|
|
15
|
+
* async get(lang) {
|
|
16
|
+
* const data = await redis.get(`i18n:${lang}`);
|
|
17
|
+
* return data ? JSON.parse(data) : null;
|
|
18
|
+
* },
|
|
19
|
+
* async set(lang, data) {
|
|
20
|
+
* await redis.set(`i18n:${lang}`, JSON.stringify(data), 'PX', data.expiresAt - Date.now());
|
|
21
|
+
* },
|
|
22
|
+
* async delete(lang) {
|
|
23
|
+
* await redis.del(`i18n:${lang}`);
|
|
24
|
+
* }
|
|
25
|
+
* };
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export interface I18nCacheAdapter {
|
|
29
|
+
/**
|
|
30
|
+
* Retrieve cached translations for a specific language.
|
|
31
|
+
*
|
|
32
|
+
* @param lang - The language code (e.g., `"en"`, `"bn"`, `"fr"`)
|
|
33
|
+
* @returns A Promise resolving to the cached translations object
|
|
34
|
+
* or `null` if not available or expired.
|
|
35
|
+
*/
|
|
36
|
+
get(lang: string): Promise<{
|
|
37
|
+
translations: TranslationMap;
|
|
38
|
+
expiresAt: number;
|
|
39
|
+
} | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Store translations in cache for a specific language.
|
|
42
|
+
*
|
|
43
|
+
* @param lang - The language code (e.g., `"en"`, `"es"`)
|
|
44
|
+
* @param data - An object containing the translation map and expiration timestamp.
|
|
45
|
+
* @returns A Promise that resolves when caching is complete.
|
|
46
|
+
*/
|
|
47
|
+
set(lang: string, data: {
|
|
48
|
+
translations: TranslationMap;
|
|
49
|
+
expiresAt: number;
|
|
50
|
+
}): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Remove cached translations for a specific language.
|
|
53
|
+
*
|
|
54
|
+
* @param lang - The language code to delete.
|
|
55
|
+
* @returns A Promise that resolves when deletion is complete.
|
|
56
|
+
*/
|
|
57
|
+
delete(lang: string): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* ⚙️ Configuration options for the TezX i18n middleware.
|
|
61
|
+
*
|
|
62
|
+
* This configuration provides flexibility for:
|
|
63
|
+
* - dynamic translation loading
|
|
64
|
+
* - custom caching strategies
|
|
65
|
+
* - language detection logic
|
|
66
|
+
* - message formatting and interpolation
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* app.use(i18n({
|
|
71
|
+
* loadTranslations: lang => import(`./locales/${lang}.json`),
|
|
72
|
+
* detectLanguage: ctx => ctx.req.query.lang || 'en',
|
|
73
|
+
* cacheTranslations: true,
|
|
74
|
+
* cacheStorage: redisCache,
|
|
75
|
+
* defaultLanguage: 'en',
|
|
76
|
+
* formatMessage: (msg, vars) => msg.replace(/\{\{(\w+)\}\}/g, (_, k) => vars[k])
|
|
77
|
+
* }));
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export type I18nOptions = {
|
|
81
|
+
/**
|
|
82
|
+
* 🌐 Function responsible for loading translation data dynamically.
|
|
83
|
+
*
|
|
84
|
+
* This can load translations from local JSON, database, API, etc.
|
|
85
|
+
*
|
|
86
|
+
* @param language - Language code to load (e.g., `"en"`, `"bn-BD"`)
|
|
87
|
+
* @returns Promise resolving to translation map for that language.
|
|
88
|
+
*/
|
|
89
|
+
loadTranslations: loadTranslations;
|
|
90
|
+
/**
|
|
91
|
+
* ⏱️ Default cache duration in milliseconds.
|
|
92
|
+
*
|
|
93
|
+
* Defines how long translations remain valid before reloading.
|
|
94
|
+
*
|
|
95
|
+
* @default 3600000 (1 hour)
|
|
96
|
+
*/
|
|
97
|
+
defaultCacheDuration?: number;
|
|
98
|
+
/**
|
|
99
|
+
* 🔍 Function to detect the preferred user language.
|
|
100
|
+
*
|
|
101
|
+
* Determines language priority from request context.
|
|
102
|
+
* Common strategies include:
|
|
103
|
+
* - URL query parameter
|
|
104
|
+
* - Cookie
|
|
105
|
+
* - HTTP `Accept-Language` header
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* detectLanguage: (ctx) => ctx.req.query.lang || ctx.cookies.lang || 'en'
|
|
109
|
+
*/
|
|
110
|
+
detectLanguage: (ctx: Context) => string;
|
|
111
|
+
/**
|
|
112
|
+
* 🏠 Default fallback language.
|
|
113
|
+
*
|
|
114
|
+
* Used when detected language translations are unavailable.
|
|
115
|
+
*
|
|
116
|
+
* @default "en"
|
|
117
|
+
*/
|
|
118
|
+
defaultLanguage?: string;
|
|
119
|
+
/**
|
|
120
|
+
* 🗝️ Context property key where the translation function (`t`) is attached.
|
|
121
|
+
*
|
|
122
|
+
* @default "t"
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* ctx.t("greeting.hello");
|
|
126
|
+
* ctx.t("user.welcome", { name: "Rakibul" });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
translationFunctionKey?: string;
|
|
130
|
+
/**
|
|
131
|
+
* 💬 Custom message formatting function.
|
|
132
|
+
*
|
|
133
|
+
* Handles variable interpolation (e.g., replacing `{{name}}` with actual value).
|
|
134
|
+
* You can override this for advanced templating logic.
|
|
135
|
+
*
|
|
136
|
+
* @param message - The base translation string.
|
|
137
|
+
* @param vars - Optional variable map for placeholders.
|
|
138
|
+
* @returns Formatted string with variables replaced.
|
|
139
|
+
*
|
|
140
|
+
* @default
|
|
141
|
+
* ```ts
|
|
142
|
+
* (msg, vars = {}) =>
|
|
143
|
+
* Object.entries(vars).reduce(
|
|
144
|
+
* (m, [k, v]) => m.replace(new RegExp(`{{${k}}}`, "g"), String(v)),
|
|
145
|
+
* msg
|
|
146
|
+
* );
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
formatMessage?: (message: string, vars?: Record<string, any>) => string;
|
|
150
|
+
/**
|
|
151
|
+
* 🔒 Custom cache validation logic.
|
|
152
|
+
*
|
|
153
|
+
* Checks whether cached translations are still valid.
|
|
154
|
+
*
|
|
155
|
+
* @param cached - Cached object with translations and expiry timestamp.
|
|
156
|
+
* @param lang - The language being validated.
|
|
157
|
+
* @returns `true` if cache is valid, otherwise `false`.
|
|
158
|
+
*
|
|
159
|
+
* @default
|
|
160
|
+
* ```ts
|
|
161
|
+
* (cached) => cached.expiresAt > Date.now()
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
isCacheValid?: (cached: {
|
|
165
|
+
translations: TranslationMap;
|
|
166
|
+
expiresAt: number;
|
|
167
|
+
}, lang: string) => boolean;
|
|
168
|
+
/**
|
|
169
|
+
* 🗃️ Whether to enable caching for translations.
|
|
170
|
+
*
|
|
171
|
+
* If disabled, translations are always loaded fresh.
|
|
172
|
+
*
|
|
173
|
+
* @default false
|
|
174
|
+
*/
|
|
175
|
+
cacheTranslations?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* 💾 Optional external cache adapter for distributed or persistent caching.
|
|
178
|
+
*
|
|
179
|
+
* Supports any system implementing `I18nCacheAdapter` interface —
|
|
180
|
+
* such as Redis, filesystem, in-memory store, or custom backends.
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* app.use(i18n({
|
|
185
|
+
* loadTranslations,
|
|
186
|
+
* cacheTranslations: true,
|
|
187
|
+
* cacheStorage: redisCache
|
|
188
|
+
* }));
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
cacheStorage?: I18nCacheAdapter;
|
|
192
|
+
};
|
|
193
|
+
declare const i18n: (options: I18nOptions) => Middleware;
|
|
194
|
+
export { i18n as default, i18n };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { TezXError } from "../index.js";
|
|
2
|
+
const i18n = (options) => {
|
|
3
|
+
const { loadTranslations, defaultCacheDuration = 3600000, detectLanguage, defaultLanguage = "en", translationFunctionKey = "t", formatMessage = (msg, vars = {}) => {
|
|
4
|
+
if (vars && msg.indexOf("{{") !== -1) {
|
|
5
|
+
let out = "";
|
|
6
|
+
let i = 0;
|
|
7
|
+
while (i < msg.length) {
|
|
8
|
+
const startVar = msg.indexOf("{{", i);
|
|
9
|
+
if (startVar === -1) {
|
|
10
|
+
out += msg.slice(i);
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
const endVar = msg.indexOf("}}", startVar + 2);
|
|
14
|
+
if (endVar === -1) {
|
|
15
|
+
out += msg.slice(i);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
out += msg.slice(i, startVar);
|
|
19
|
+
const keyVar = msg.slice(startVar + 2, endVar).trim();
|
|
20
|
+
out += vars[keyVar] !== undefined ? String(vars[keyVar]) : "";
|
|
21
|
+
i = endVar + 2;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
return msg;
|
|
26
|
+
}, isCacheValid = (cached) => cached.expiresAt > Date.now(), cacheTranslations = false, cacheStorage: externalCache = null, } = options;
|
|
27
|
+
let localCache = null;
|
|
28
|
+
if (cacheTranslations && !externalCache)
|
|
29
|
+
localCache = new Map();
|
|
30
|
+
return async function i18n(ctx, next) {
|
|
31
|
+
try {
|
|
32
|
+
const lang = detectLanguage(ctx) ?? defaultLanguage;
|
|
33
|
+
let translations = undefined;
|
|
34
|
+
if (cacheTranslations) {
|
|
35
|
+
if (externalCache) {
|
|
36
|
+
const cached = await externalCache.get(lang);
|
|
37
|
+
if (cached && isCacheValid(cached, lang))
|
|
38
|
+
translations = cached.translations;
|
|
39
|
+
}
|
|
40
|
+
else if (localCache?.get(lang)) {
|
|
41
|
+
const cached = localCache.get(lang);
|
|
42
|
+
if (isCacheValid(cached, lang))
|
|
43
|
+
translations = cached.translations;
|
|
44
|
+
else
|
|
45
|
+
localCache.delete(lang);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!translations) {
|
|
49
|
+
translations = await loadTranslations(lang);
|
|
50
|
+
const expiresAt = Date.now() + defaultCacheDuration;
|
|
51
|
+
if (cacheTranslations) {
|
|
52
|
+
if (externalCache) {
|
|
53
|
+
await externalCache.set(lang, { translations, expiresAt });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
localCache?.set(lang, { translations, expiresAt });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
ctx[translationFunctionKey] = function translate(key, vars) {
|
|
61
|
+
let acc = translations;
|
|
62
|
+
let start = 0;
|
|
63
|
+
for (let i = 0; i <= key.length; i++) {
|
|
64
|
+
const c = key.charCodeAt(i);
|
|
65
|
+
if (c === 46 || i === key.length) {
|
|
66
|
+
const segment = key.slice(start, i);
|
|
67
|
+
if (acc && typeof acc === "object") {
|
|
68
|
+
acc = acc[segment];
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
acc = undefined;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
start = i + 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let msg = typeof acc === "string" ? acc : key;
|
|
78
|
+
return formatMessage(msg, vars);
|
|
79
|
+
};
|
|
80
|
+
ctx.language = lang;
|
|
81
|
+
return await next();
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
throw new TezXError(error?.message ?? "i18n failed", 500, error?.stack);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export { i18n as default, i18n };
|
package/middleware/index.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export * from "./basic-auth.js";
|
|
2
2
|
export * from "./bearer-auth.js";
|
|
3
|
+
export * from "./cache-control.js";
|
|
3
4
|
export * from "./cors.js";
|
|
5
|
+
export * from "./detect-bot.js";
|
|
6
|
+
export * from "./i18n.js";
|
|
4
7
|
export * from "./logger.js";
|
|
5
8
|
export * from "./pagination.js";
|
|
6
9
|
export * from "./powered-by.js";
|
|
10
|
+
export * from "./rate-limiter.js";
|
|
7
11
|
export * from "./request-id.js";
|
|
8
12
|
export * from "./sanitize-headers.js";
|
|
13
|
+
export * from "./secure-headers.js";
|
|
9
14
|
export * from "./xss-protection.js";
|
package/middleware/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export * from "./basic-auth.js";
|
|
2
2
|
export * from "./bearer-auth.js";
|
|
3
|
+
export * from "./cache-control.js";
|
|
3
4
|
export * from "./cors.js";
|
|
5
|
+
export * from "./detect-bot.js";
|
|
6
|
+
export * from "./i18n.js";
|
|
4
7
|
export * from "./logger.js";
|
|
5
8
|
export * from "./pagination.js";
|
|
6
9
|
export * from "./powered-by.js";
|
|
10
|
+
export * from "./rate-limiter.js";
|
|
7
11
|
export * from "./request-id.js";
|
|
8
12
|
export * from "./sanitize-headers.js";
|
|
13
|
+
export * from "./secure-headers.js";
|
|
9
14
|
export * from "./xss-protection.js";
|
package/middleware/logger.d.ts
CHANGED
package/middleware/logger.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TezXError } from "../core/error.js";
|
|
1
2
|
import { colorText } from "../utils/colors.js";
|
|
2
3
|
function logger(options = { enabled: true }) {
|
|
3
4
|
return async function logger(ctx, next) {
|
|
@@ -15,8 +16,8 @@ function logger(options = { enabled: true }) {
|
|
|
15
16
|
}
|
|
16
17
|
catch (err) {
|
|
17
18
|
console.error(`${colorText("Error:", "red")}`, err.stack);
|
|
18
|
-
throw new
|
|
19
|
+
throw new TezXError(err.stack);
|
|
19
20
|
}
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
|
-
export { logger
|
|
23
|
+
export { logger as default, logger };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Context } from "../core/context.js";
|
|
2
|
+
import { TezXError } from "../core/error.js";
|
|
2
3
|
import { HttpBaseResponse, Middleware } from "../types/index.js";
|
|
3
4
|
export type RateLimiterOptions = {
|
|
4
5
|
/**
|
|
@@ -41,10 +42,10 @@ export type RateLimiterOptions = {
|
|
|
41
42
|
* @example
|
|
42
43
|
* onError: (ctx, retryAfter) => {
|
|
43
44
|
* ctx.status = 429;
|
|
44
|
-
* throw new
|
|
45
|
+
* throw new TezXError( `Rate limit exceeded. Try again in ${retryAfter} seconds.`);
|
|
45
46
|
* }
|
|
46
47
|
*/
|
|
47
|
-
onError?: (ctx: Context, retryAfter: number, error:
|
|
48
|
+
onError?: (ctx: Context, retryAfter: number, error: TezXError) => HttpBaseResponse;
|
|
48
49
|
};
|
|
49
50
|
/**
|
|
50
51
|
* 🚦 Rate limiting middleware for request throttling
|
|
@@ -23,7 +23,7 @@ const rateLimiter = (options) => {
|
|
|
23
23
|
if (check) {
|
|
24
24
|
const retryAfter = Math.ceil((entry.resetTime - Date.now()) / 1000);
|
|
25
25
|
ctx.headers.set("Retry-After", retryAfter.toString());
|
|
26
|
-
return onError(ctx, retryAfter, new
|
|
26
|
+
return onError(ctx, retryAfter, new TezXError(`Rate limit exceeded. Retry after ${retryAfter} seconds.`));
|
|
27
27
|
}
|
|
28
28
|
ctx.headers.set("X-RateLimit-Limit", maxRequests.toString());
|
|
29
29
|
ctx.headers.set("X-RateLimit-Remaining", (maxRequests - entry.count).toString());
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const sanitizeHeaders = (options = {}) => {
|
|
2
|
-
const { whitelist = [], blacklist = []
|
|
2
|
+
const { whitelist = [], blacklist = [] } = options;
|
|
3
3
|
const normalizedWhitelist = whitelist.map((h) => h.toLowerCase());
|
|
4
4
|
const normalizedBlacklist = blacklist.map((h) => h.toLowerCase());
|
|
5
5
|
let lWhite = normalizedWhitelist.length;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SecureHeadersOptions = {
|
|
2
|
+
preset?: "strict" | "balanced" | "dev";
|
|
3
|
+
hsts?: boolean;
|
|
4
|
+
hstsMaxAge?: number;
|
|
5
|
+
frameGuard?: "DENY" | "SAMEORIGIN" | string;
|
|
6
|
+
noSniff?: boolean;
|
|
7
|
+
xssProtection?: boolean;
|
|
8
|
+
referrerPolicy?: string;
|
|
9
|
+
permissionsPolicy?: string;
|
|
10
|
+
csp?: string | Record<string, string | string[]>;
|
|
11
|
+
cspReportOnly?: boolean;
|
|
12
|
+
cspUseNonce?: boolean;
|
|
13
|
+
ultraFastMode?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare const secureHeaders: <T extends Record<string, any> = {}, Path extends string = any>(userOpts?: SecureHeadersOptions) => (ctx: any, next: () => Promise<any>) => Promise<any>;
|