tezx 4.0.2 → 4.0.3
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 +16 -52
- package/cjs/core/request.js +0 -1
- package/cjs/core/server.js +26 -21
- package/cjs/helper/index.js +4 -0
- package/cjs/index.js +1 -1
- package/cjs/middleware/detect-bot.js +2 -1
- package/cjs/middleware/index.js +0 -1
- package/cjs/middleware/pagination.js +3 -2
- package/cjs/middleware/rate-limiter.js +5 -5
- package/cjs/utils/mimeTypes.js +17 -5
- package/core/context.d.ts +30 -32
- package/core/context.js +16 -52
- package/core/request.d.ts +1 -13
- package/core/request.js +0 -1
- package/core/server.d.ts +1 -1
- package/core/server.js +26 -21
- package/helper/index.d.ts +15 -0
- package/helper/index.js +3 -0
- package/index.js +1 -1
- package/middleware/detect-bot.d.ts +24 -42
- package/middleware/detect-bot.js +2 -1
- package/middleware/index.d.ts +0 -1
- package/middleware/index.js +0 -1
- package/middleware/pagination.js +3 -2
- package/middleware/rate-limiter.d.ts +27 -29
- package/middleware/rate-limiter.js +4 -4
- package/package.json +1 -1
- package/types/index.d.ts +5 -1
- package/utils/mimeTypes.d.ts +114 -1
- package/utils/mimeTypes.js +17 -5
- package/cjs/middleware/getConnInfo.js +0 -13
- package/middleware/getConnInfo.d.ts +0 -22
- package/middleware/getConnInfo.js +0 -10
package/cjs/core/context.js
CHANGED
|
@@ -31,7 +31,7 @@ class Context {
|
|
|
31
31
|
return this;
|
|
32
32
|
const _key = key.toLowerCase();
|
|
33
33
|
const append = options?.append || _key === "set-cookie";
|
|
34
|
-
const target = this.
|
|
34
|
+
const target = this.headers;
|
|
35
35
|
if (append) {
|
|
36
36
|
target.append(_key, value);
|
|
37
37
|
}
|
|
@@ -47,10 +47,11 @@ class Context {
|
|
|
47
47
|
this.#status = status;
|
|
48
48
|
return this;
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
50
|
+
createResponse(body, type, init = {}) {
|
|
51
|
+
const headers = (0, response_js_1.mergeHeaders)(this.#headers, init.headers);
|
|
52
|
+
if (type) {
|
|
53
53
|
headers.set("Content-Type", type);
|
|
54
|
+
}
|
|
54
55
|
return new Response(body, {
|
|
55
56
|
status: init.status ?? this.#status,
|
|
56
57
|
statusText: init.statusText,
|
|
@@ -58,50 +59,16 @@ class Context {
|
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
newResponse(body, init = {}) {
|
|
61
|
-
|
|
62
|
-
return new Response(body, {
|
|
63
|
-
status: init.status ?? this.#status,
|
|
64
|
-
statusText: init.statusText,
|
|
65
|
-
headers,
|
|
66
|
-
});
|
|
62
|
+
return this.createResponse(body, null, init);
|
|
67
63
|
}
|
|
68
64
|
text(content, init) {
|
|
69
|
-
return this
|
|
65
|
+
return this.createResponse(content, "text/plain; charset=utf-8", init);
|
|
70
66
|
}
|
|
71
67
|
html(strings, init) {
|
|
72
|
-
return this
|
|
68
|
+
return this.createResponse(strings, "text/html; charset=utf-8", init);
|
|
73
69
|
}
|
|
74
70
|
json(json, init) {
|
|
75
|
-
return this
|
|
76
|
-
}
|
|
77
|
-
send(body, init) {
|
|
78
|
-
let _body;
|
|
79
|
-
let type;
|
|
80
|
-
if (body === null || body === undefined) {
|
|
81
|
-
_body = "";
|
|
82
|
-
type = "text/plain";
|
|
83
|
-
}
|
|
84
|
-
else if (typeof body === "string" || typeof body === "number") {
|
|
85
|
-
_body = body;
|
|
86
|
-
type = "text/plain";
|
|
87
|
-
}
|
|
88
|
-
else if (body instanceof Uint8Array || body instanceof ArrayBuffer || (typeof ReadableStream !== "undefined" && body instanceof ReadableStream)) {
|
|
89
|
-
_body = body;
|
|
90
|
-
type = "application/octet-stream";
|
|
91
|
-
}
|
|
92
|
-
else if (body instanceof Blob || (typeof File !== "undefined" && body instanceof File)) {
|
|
93
|
-
_body = body;
|
|
94
|
-
type = body.type || "application/octet-stream";
|
|
95
|
-
}
|
|
96
|
-
else if (typeof body === "object") {
|
|
97
|
-
_body = JSON.stringify(body);
|
|
98
|
-
type = "application/json";
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
_body = String(body);
|
|
102
|
-
type = "text/plain";
|
|
103
|
-
}
|
|
104
|
-
return this.#newResponse(_body, type, init);
|
|
71
|
+
return this.createResponse(JSON.stringify(json), "application/json; charset=utf-8", init);
|
|
105
72
|
}
|
|
106
73
|
redirect(url, status = 302) {
|
|
107
74
|
const headers = new Headers(this.#headers);
|
|
@@ -122,18 +89,15 @@ class Context {
|
|
|
122
89
|
if (init?.filename) {
|
|
123
90
|
headers["Content-Disposition"] = `attachment; filename="${init?.filename}"`;
|
|
124
91
|
}
|
|
92
|
+
let contentType = null;
|
|
125
93
|
if (init?.download || init?.filename) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
94
|
+
contentType = "application/octet-stream";
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const ext = (0, utils_js_1.extensionExtract)(filePath);
|
|
98
|
+
contentType = mimeTypes_js_1.mimeTypes[ext] ?? mimeTypes_js_1.defaultMimeType;
|
|
132
99
|
}
|
|
133
|
-
|
|
134
|
-
const mimeType = mimeTypes_js_1.mimeTypes[ext] ?? mimeTypes_js_1.defaultMimeType;
|
|
135
|
-
headers["Content-Type"] = mimeType;
|
|
136
|
-
return this.newResponse(stream, {
|
|
100
|
+
return this.createResponse(stream, contentType, {
|
|
137
101
|
status: init?.status ?? this.#status,
|
|
138
102
|
statusText: init?.statusText,
|
|
139
103
|
headers,
|
package/cjs/core/request.js
CHANGED
package/cjs/core/server.js
CHANGED
|
@@ -7,10 +7,9 @@ const url_js_1 = require("../utils/url.js");
|
|
|
7
7
|
const context_js_1 = require("./context.js");
|
|
8
8
|
const router_js_1 = require("./router.js");
|
|
9
9
|
class TezX extends router_js_1.Router {
|
|
10
|
-
#pathResolver;
|
|
11
10
|
#notFound = response_js_1.notFoundResponse;
|
|
12
11
|
#errorHandler = response_js_1.handleErrorResponse;
|
|
13
|
-
constructor({ basePath = "/", routeRegistry = new RadixRouter_js_1.RadixRouter()
|
|
12
|
+
constructor({ basePath = "/", routeRegistry = new RadixRouter_js_1.RadixRouter() } = {}) {
|
|
14
13
|
super({ basePath });
|
|
15
14
|
if (!routeRegistry) {
|
|
16
15
|
throw new Error("routeRegistry is required for TezX initialization");
|
|
@@ -30,24 +29,27 @@ class TezX extends router_js_1.Router {
|
|
|
30
29
|
let index = -1;
|
|
31
30
|
const dispatch = (i) => {
|
|
32
31
|
if (i <= index)
|
|
33
|
-
|
|
32
|
+
return Promise.reject(new Error("next() called multiple times"));
|
|
34
33
|
index = i;
|
|
35
34
|
const fn = stack[i];
|
|
36
35
|
if (!fn)
|
|
37
36
|
return ctx.res;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (result instanceof
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
try {
|
|
38
|
+
const result = fn(ctx, () => dispatch(i + 1));
|
|
39
|
+
if (!(result instanceof Promise)) {
|
|
40
|
+
if (result instanceof Response)
|
|
41
|
+
ctx.res = result;
|
|
42
|
+
return result ?? ctx.res;
|
|
43
|
+
}
|
|
44
|
+
return result.then((res) => {
|
|
45
|
+
if (res instanceof Response)
|
|
46
|
+
ctx.res = res;
|
|
47
|
+
return res ?? ctx.res;
|
|
48
|
+
}, (err) => Promise.reject(err));
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return Promise.reject(err);
|
|
43
52
|
}
|
|
44
|
-
return result.then((res) => {
|
|
45
|
-
if (res instanceof Response)
|
|
46
|
-
ctx.res = res;
|
|
47
|
-
return ctx.res;
|
|
48
|
-
}, (err) => {
|
|
49
|
-
throw err;
|
|
50
|
-
});
|
|
51
53
|
};
|
|
52
54
|
return dispatch(0);
|
|
53
55
|
}
|
|
@@ -66,9 +68,13 @@ class TezX extends router_js_1.Router {
|
|
|
66
68
|
if (mLen === 0)
|
|
67
69
|
return this.#notFound(ctx);
|
|
68
70
|
ctx.params = params;
|
|
69
|
-
if (mLen === 1)
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
if (mLen === 1) {
|
|
72
|
+
ctx.res = await middlewares[0](ctx);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
ctx.res = await this.#composeChain(ctx, middlewares);
|
|
76
|
+
}
|
|
77
|
+
return ctx.res ?? this.#notFound(ctx);
|
|
72
78
|
}
|
|
73
79
|
catch (err) {
|
|
74
80
|
let error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -76,10 +82,9 @@ class TezX extends router_js_1.Router {
|
|
|
76
82
|
}
|
|
77
83
|
}
|
|
78
84
|
async serve(req, server) {
|
|
79
|
-
const method = req.method
|
|
85
|
+
const method = req.method;
|
|
80
86
|
if (method === "HEAD") {
|
|
81
|
-
const
|
|
82
|
-
const res = await this.#handleRequest(headReq, "GET", server);
|
|
87
|
+
const res = await this.#handleRequest(req, "GET", server);
|
|
83
88
|
return new Response(null, {
|
|
84
89
|
status: res.status,
|
|
85
90
|
statusText: res.statusText,
|
package/cjs/helper/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateUUID = exports.generateRandomBase64 = exports.generateID = exports.useFormData = void 0;
|
|
4
|
+
exports.getConnInfo = getConnInfo;
|
|
4
5
|
const formData_js_1 = require("./formData.js");
|
|
5
6
|
Object.defineProperty(exports, "useFormData", { enumerable: true, get: function () { return formData_js_1.useFormData; } });
|
|
6
7
|
const generateID_js_1 = require("./generateID.js");
|
|
@@ -11,3 +12,6 @@ exports.default = {
|
|
|
11
12
|
useFormData: formData_js_1.useFormData,
|
|
12
13
|
generateID: generateID_js_1.generateID, generateRandomBase64: generateID_js_1.generateRandomBase64, generateUUID: generateID_js_1.generateUUID
|
|
13
14
|
};
|
|
15
|
+
function getConnInfo(ctx) {
|
|
16
|
+
return ctx?.server?.requestIP?.(ctx.rawRequest);
|
|
17
|
+
}
|
package/cjs/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const router_js_1 = require("./core/router.js");
|
|
|
5
5
|
Object.defineProperty(exports, "Router", { enumerable: true, get: function () { return router_js_1.Router; } });
|
|
6
6
|
const server_js_1 = require("./core/server.js");
|
|
7
7
|
Object.defineProperty(exports, "TezX", { enumerable: true, get: function () { return server_js_1.TezX; } });
|
|
8
|
-
exports.version = "4.0.
|
|
8
|
+
exports.version = "4.0.3";
|
|
9
9
|
exports.default = {
|
|
10
10
|
Router: router_js_1.Router,
|
|
11
11
|
TezX: server_js_1.TezX,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.detectBot = void 0;
|
|
4
|
+
const index_js_1 = require("../helper/index.js");
|
|
4
5
|
const rateLimit_js_1 = require("../utils/rateLimit.js");
|
|
5
6
|
const detectBot = (opts = {}) => {
|
|
6
7
|
const botUAs = opts.botUserAgents || ["bot", "spider", "crawl", "slurp"];
|
|
@@ -8,7 +9,7 @@ const detectBot = (opts = {}) => {
|
|
|
8
9
|
const botRegex = new RegExp(botUAs.join("|"), "i");
|
|
9
10
|
checkBot = (ua) => botRegex.test(ua);
|
|
10
11
|
const keyGenerator = opts.keyGenerator ?? ((ctx) => {
|
|
11
|
-
const addr = ctx
|
|
12
|
+
const addr = (0, index_js_1.getConnInfo)(ctx);
|
|
12
13
|
return addr ? `${addr.address}:${addr.port}` : "unknown";
|
|
13
14
|
});
|
|
14
15
|
const maxReq = opts.maxRequests || 30;
|
package/cjs/middleware/index.js
CHANGED
|
@@ -20,7 +20,6 @@ __exportStar(require("./cache-control.js"), exports);
|
|
|
20
20
|
__exportStar(require("./cors.js"), exports);
|
|
21
21
|
__exportStar(require("./detect-bot.js"), exports);
|
|
22
22
|
__exportStar(require("./etag.js"), exports);
|
|
23
|
-
__exportStar(require("./getConnInfo.js"), exports);
|
|
24
23
|
__exportStar(require("./i18n.js"), exports);
|
|
25
24
|
__exportStar(require("./logger.js"), exports);
|
|
26
25
|
__exportStar(require("./pagination.js"), exports);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.rateLimiter = exports.default = void 0;
|
|
4
|
+
const index_js_1 = require("../helper/index.js");
|
|
4
5
|
const rateLimit_js_1 = require("../utils/rateLimit.js");
|
|
5
6
|
const rateLimiter = (options) => {
|
|
6
7
|
const { maxRequests, windowMs, keyGenerator = (ctx) => {
|
|
@@ -12,9 +13,8 @@ const rateLimiter = (options) => {
|
|
|
12
13
|
const clientIp = ctx.req.header("client-ip");
|
|
13
14
|
if (clientIp)
|
|
14
15
|
return clientIp;
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
return `${addr}:${port}`;
|
|
16
|
+
const { port, address } = (0, index_js_1.getConnInfo)(ctx) ?? {};
|
|
17
|
+
return `${address}:${port}`;
|
|
18
18
|
}, storage = (0, rateLimit_js_1.createRateLimitDefaultStorage)(), onError = (ctx, retryAfter, error) => {
|
|
19
19
|
ctx.status(429);
|
|
20
20
|
throw new Error(`Rate limit exceeded. Try again in ${retryAfter} seconds.`);
|
|
@@ -33,5 +33,5 @@ const rateLimiter = (options) => {
|
|
|
33
33
|
return await next();
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
|
-
exports.rateLimiter = rateLimiter;
|
|
37
36
|
exports.default = rateLimiter;
|
|
37
|
+
exports.rateLimiter = rateLimiter;
|
package/cjs/utils/mimeTypes.js
CHANGED
|
@@ -15,6 +15,10 @@ exports.mimeTypes = {
|
|
|
15
15
|
tsv: "text/tab-separated-values",
|
|
16
16
|
rtf: "application/rtf",
|
|
17
17
|
markdown: "text/markdown",
|
|
18
|
+
jsx: "text/javascript",
|
|
19
|
+
ts: "text/typescript",
|
|
20
|
+
tsx: "text/typescript",
|
|
21
|
+
jsonld: "application/ld+json",
|
|
18
22
|
png: "image/png",
|
|
19
23
|
jpg: "image/jpeg",
|
|
20
24
|
jpeg: "image/jpeg",
|
|
@@ -25,6 +29,13 @@ exports.mimeTypes = {
|
|
|
25
29
|
bmp: "image/bmp",
|
|
26
30
|
tiff: "image/tiff",
|
|
27
31
|
psd: "image/vnd.adobe.photoshop",
|
|
32
|
+
tif: "image/tiff",
|
|
33
|
+
avif: "image/avif",
|
|
34
|
+
woff: "font/woff",
|
|
35
|
+
woff2: "font/woff2",
|
|
36
|
+
ttf: "font/ttf",
|
|
37
|
+
otf: "font/otf",
|
|
38
|
+
eot: "application/vnd.ms-fontobject",
|
|
28
39
|
mp4: "video/mp4",
|
|
29
40
|
webm: "video/webm",
|
|
30
41
|
ogg: "video/ogg",
|
|
@@ -33,6 +44,9 @@ exports.mimeTypes = {
|
|
|
33
44
|
wmv: "video/x-ms-wmv",
|
|
34
45
|
flv: "video/x-flv",
|
|
35
46
|
"3gp": "video/3gpp",
|
|
47
|
+
mkv: "video/x-matroska",
|
|
48
|
+
mpeg: "video/mpeg",
|
|
49
|
+
mpg: "video/mpeg",
|
|
36
50
|
mp3: "audio/mpeg",
|
|
37
51
|
wav: "audio/wav",
|
|
38
52
|
aac: "audio/aac",
|
|
@@ -40,11 +54,7 @@ exports.mimeTypes = {
|
|
|
40
54
|
m4a: "audio/mp4",
|
|
41
55
|
mid: "audio/midi",
|
|
42
56
|
midi: "audio/midi",
|
|
43
|
-
|
|
44
|
-
woff2: "font/woff2",
|
|
45
|
-
ttf: "font/ttf",
|
|
46
|
-
otf: "font/otf",
|
|
47
|
-
eot: "application/vnd.ms-fontobject",
|
|
57
|
+
weba: "audio/webm",
|
|
48
58
|
pdf: "application/pdf",
|
|
49
59
|
odp: "application/vnd.oasis.opendocument.presentation",
|
|
50
60
|
zip: "application/zip",
|
|
@@ -83,6 +93,8 @@ exports.mimeTypes = {
|
|
|
83
93
|
deb: "application/x-debian-package",
|
|
84
94
|
rpm: "application/x-redhat-package-manager",
|
|
85
95
|
apk: "application/vnd.android.package-archive",
|
|
96
|
+
bin: "application/octet-stream",
|
|
97
|
+
iso: "application/octet-stream",
|
|
86
98
|
webmanifest: "application/manifest+json",
|
|
87
99
|
ics: "text/calendar",
|
|
88
100
|
vcf: "text/vcard",
|
package/core/context.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ExtractParamsFromPath, HttpBaseResponse, ResHeaderKey, ResponseInit, WebSocketEvent } from "../types/index.js";
|
|
2
|
+
import { ContentType } from "../utils/mimeTypes.js";
|
|
2
3
|
import { TezXRequest } from "./request.js";
|
|
3
4
|
export declare class Context<TPath extends string = any> {
|
|
4
5
|
#private;
|
|
@@ -112,6 +113,35 @@ export declare class Context<TPath extends string = any> {
|
|
|
112
113
|
* ctx.status(404).text("Not found");
|
|
113
114
|
*/
|
|
114
115
|
status(status: number): this;
|
|
116
|
+
/**
|
|
117
|
+
* Creates a native Response object with optional body, Content-Type, and headers.
|
|
118
|
+
*
|
|
119
|
+
* This function always overwrites the `Content-Type` header if `type` is provided.
|
|
120
|
+
* It merges any existing headers from the context (`this.#headers`) with headers
|
|
121
|
+
* provided in `init.headers`.
|
|
122
|
+
*
|
|
123
|
+
* @param {BodyInit | null} body - The response body. Can be string, Blob, ArrayBuffer, Uint8Array, or null.
|
|
124
|
+
* @param {string | null} type - The MIME type to set in the `Content-Type` header. If null, no Content-Type is set.
|
|
125
|
+
* @param {ResponseInit} [init={}] - Optional response initialization object (status, statusText, headers).
|
|
126
|
+
* @param {number} [init.status] - HTTP status code (default is `this.#status`).
|
|
127
|
+
* @param {string} [init.statusText] - Optional status text for the response.
|
|
128
|
+
* @param {HeadersInit} [init.headers] - Additional headers to merge with context headers.
|
|
129
|
+
*
|
|
130
|
+
* @returns {Response} A native Fetch API Response object.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* // Simple text response
|
|
134
|
+
* const res = createResponse("Hello World", "text/plain");
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* // JSON response
|
|
138
|
+
* const res = createResponse(JSON.stringify({ ok: true }), "application/json", { status: 200 });
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* // Custom headers
|
|
142
|
+
* const res = createResponse("Hello", "text/plain", { headers: { "X-Custom": "test" } });
|
|
143
|
+
*/
|
|
144
|
+
createResponse(body: BodyInit | null, type: ContentType | null, init?: ResponseInit): Response;
|
|
115
145
|
/**
|
|
116
146
|
* Protected helper method to create a Response or PlainResponse
|
|
117
147
|
* based on runtime environment (Node.js or Web).
|
|
@@ -157,38 +187,6 @@ export declare class Context<TPath extends string = any> {
|
|
|
157
187
|
* ctx.json({ success: true });
|
|
158
188
|
*/
|
|
159
189
|
json(json: object, init?: ResponseInit): HttpBaseResponse;
|
|
160
|
-
/**
|
|
161
|
-
* Send a response with automatic content type detection.
|
|
162
|
-
*
|
|
163
|
-
* This method determines the proper `Content-Type` header based on the type of `body`.
|
|
164
|
-
* If a `Content-Type` is provided in `init.headers`, it will be used instead.
|
|
165
|
-
*
|
|
166
|
-
* Supported types:
|
|
167
|
-
* - `string` / `number` → "text/plain"
|
|
168
|
-
* - `object` → JSON serialized, "application/json"
|
|
169
|
-
* - `Uint8Array` / `ArrayBuffer` / `ReadableStream` → "application/octet-stream"
|
|
170
|
-
* - `Blob` / `File` → uses `body.type` or falls back to "application/octet-stream"
|
|
171
|
-
* - `null` / `undefined` → empty string, "text/plain"
|
|
172
|
-
* - any other → `String(body)`, "text/plain"
|
|
173
|
-
*
|
|
174
|
-
* @param {any} body - Response body of any type.
|
|
175
|
-
* @param {ResponseInit} [init] - Optional response init object, headers, status, etc.
|
|
176
|
-
* @returns {HttpBaseResponse} - A Bun-compatible response object.
|
|
177
|
-
*
|
|
178
|
-
* @example
|
|
179
|
-
* // Send a string
|
|
180
|
-
* ctx.send("Hello World");
|
|
181
|
-
*
|
|
182
|
-
* // Send JSON
|
|
183
|
-
* ctx.send({ user: "Alice", id: 123 });
|
|
184
|
-
*
|
|
185
|
-
* // Send binary
|
|
186
|
-
* ctx.send(new Uint8Array([1,2,3]));
|
|
187
|
-
*
|
|
188
|
-
* // Custom content-type
|
|
189
|
-
* ctx.send("Hello", { headers: { "Content-Type": "text/html" } });
|
|
190
|
-
*/
|
|
191
|
-
send(body: any, init?: ResponseInit): HttpBaseResponse;
|
|
192
190
|
/**
|
|
193
191
|
* Sends an HTTP redirect response to the specified URL.
|
|
194
192
|
*
|
package/core/context.js
CHANGED
|
@@ -28,7 +28,7 @@ export class Context {
|
|
|
28
28
|
return this;
|
|
29
29
|
const _key = key.toLowerCase();
|
|
30
30
|
const append = options?.append || _key === "set-cookie";
|
|
31
|
-
const target = this.
|
|
31
|
+
const target = this.headers;
|
|
32
32
|
if (append) {
|
|
33
33
|
target.append(_key, value);
|
|
34
34
|
}
|
|
@@ -44,10 +44,11 @@ export class Context {
|
|
|
44
44
|
this.#status = status;
|
|
45
45
|
return this;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
47
|
+
createResponse(body, type, init = {}) {
|
|
48
|
+
const headers = mergeHeaders(this.#headers, init.headers);
|
|
49
|
+
if (type) {
|
|
50
50
|
headers.set("Content-Type", type);
|
|
51
|
+
}
|
|
51
52
|
return new Response(body, {
|
|
52
53
|
status: init.status ?? this.#status,
|
|
53
54
|
statusText: init.statusText,
|
|
@@ -55,50 +56,16 @@ export class Context {
|
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
newResponse(body, init = {}) {
|
|
58
|
-
|
|
59
|
-
return new Response(body, {
|
|
60
|
-
status: init.status ?? this.#status,
|
|
61
|
-
statusText: init.statusText,
|
|
62
|
-
headers,
|
|
63
|
-
});
|
|
59
|
+
return this.createResponse(body, null, init);
|
|
64
60
|
}
|
|
65
61
|
text(content, init) {
|
|
66
|
-
return this
|
|
62
|
+
return this.createResponse(content, "text/plain; charset=utf-8", init);
|
|
67
63
|
}
|
|
68
64
|
html(strings, init) {
|
|
69
|
-
return this
|
|
65
|
+
return this.createResponse(strings, "text/html; charset=utf-8", init);
|
|
70
66
|
}
|
|
71
67
|
json(json, init) {
|
|
72
|
-
return this
|
|
73
|
-
}
|
|
74
|
-
send(body, init) {
|
|
75
|
-
let _body;
|
|
76
|
-
let type;
|
|
77
|
-
if (body === null || body === undefined) {
|
|
78
|
-
_body = "";
|
|
79
|
-
type = "text/plain";
|
|
80
|
-
}
|
|
81
|
-
else if (typeof body === "string" || typeof body === "number") {
|
|
82
|
-
_body = body;
|
|
83
|
-
type = "text/plain";
|
|
84
|
-
}
|
|
85
|
-
else if (body instanceof Uint8Array || body instanceof ArrayBuffer || (typeof ReadableStream !== "undefined" && body instanceof ReadableStream)) {
|
|
86
|
-
_body = body;
|
|
87
|
-
type = "application/octet-stream";
|
|
88
|
-
}
|
|
89
|
-
else if (body instanceof Blob || (typeof File !== "undefined" && body instanceof File)) {
|
|
90
|
-
_body = body;
|
|
91
|
-
type = body.type || "application/octet-stream";
|
|
92
|
-
}
|
|
93
|
-
else if (typeof body === "object") {
|
|
94
|
-
_body = JSON.stringify(body);
|
|
95
|
-
type = "application/json";
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
_body = String(body);
|
|
99
|
-
type = "text/plain";
|
|
100
|
-
}
|
|
101
|
-
return this.#newResponse(_body, type, init);
|
|
68
|
+
return this.createResponse(JSON.stringify(json), "application/json; charset=utf-8", init);
|
|
102
69
|
}
|
|
103
70
|
redirect(url, status = 302) {
|
|
104
71
|
const headers = new Headers(this.#headers);
|
|
@@ -119,18 +86,15 @@ export class Context {
|
|
|
119
86
|
if (init?.filename) {
|
|
120
87
|
headers["Content-Disposition"] = `attachment; filename="${init?.filename}"`;
|
|
121
88
|
}
|
|
89
|
+
let contentType = null;
|
|
122
90
|
if (init?.download || init?.filename) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
91
|
+
contentType = "application/octet-stream";
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const ext = extensionExtract(filePath);
|
|
95
|
+
contentType = mimeTypes[ext] ?? defaultMimeType;
|
|
129
96
|
}
|
|
130
|
-
|
|
131
|
-
const mimeType = mimeTypes[ext] ?? defaultMimeType;
|
|
132
|
-
headers["Content-Type"] = mimeType;
|
|
133
|
-
return this.newResponse(stream, {
|
|
97
|
+
return this.createResponse(stream, contentType, {
|
|
134
98
|
status: init?.status ?? this.#status,
|
|
135
99
|
statusText: init?.statusText,
|
|
136
100
|
headers,
|
package/core/request.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ExtractParamsFromPath, HTTPMethod,
|
|
1
|
+
import { ExtractParamsFromPath, HTTPMethod, ReqHeaderKey, RequestHeaders } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* A wrapper around the raw HTTP request that provides convenient access to URL, headers, body parsing, and route parameters.
|
|
4
4
|
*
|
|
@@ -26,18 +26,6 @@ export declare class TezXRequest<Path extends string = any> {
|
|
|
26
26
|
* @type {ExtractParamsFromPath<Path>}
|
|
27
27
|
*/
|
|
28
28
|
readonly params: ExtractParamsFromPath<Path>;
|
|
29
|
-
/**
|
|
30
|
-
* Remote address details of the connected client.
|
|
31
|
-
* @requires injectRemoteAddress middleware.
|
|
32
|
-
* @typedef {Object} NetAddr
|
|
33
|
-
* @property {string} [transport] - Transport protocol (e.g., "tcp", "udp").
|
|
34
|
-
* @property {"IPv4" | "IPv6" | "Unix"} [family] - Address family.
|
|
35
|
-
* @property {string} [hostname] - Hostname or IP address.
|
|
36
|
-
* @property {number} [port] - Port number.
|
|
37
|
-
* @type {NetAddr}
|
|
38
|
-
* @default {}
|
|
39
|
-
*/
|
|
40
|
-
remoteAddress: NetAddr;
|
|
41
29
|
/**
|
|
42
30
|
* Creates an instance of TezXRequest.
|
|
43
31
|
*
|
package/core/request.js
CHANGED
package/core/server.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare class TezX<T extends Record<string, any> = {}> extends Router<T>
|
|
|
25
25
|
#private;
|
|
26
26
|
/** Internal route registry to hold all routes */
|
|
27
27
|
protected router?: RouteRegistry;
|
|
28
|
-
constructor({ basePath, routeRegistry
|
|
28
|
+
constructor({ basePath, routeRegistry }?: TezXConfig);
|
|
29
29
|
/**
|
|
30
30
|
* Register a custom 404 (not found) handler.
|
|
31
31
|
*
|
package/core/server.js
CHANGED
|
@@ -4,10 +4,9 @@ import { getPathname } from "../utils/url.js";
|
|
|
4
4
|
import { Context } from "./context.js";
|
|
5
5
|
import { Router } from "./router.js";
|
|
6
6
|
export class TezX extends Router {
|
|
7
|
-
#pathResolver;
|
|
8
7
|
#notFound = notFoundResponse;
|
|
9
8
|
#errorHandler = handleErrorResponse;
|
|
10
|
-
constructor({ basePath = "/", routeRegistry = new RadixRouter()
|
|
9
|
+
constructor({ basePath = "/", routeRegistry = new RadixRouter() } = {}) {
|
|
11
10
|
super({ basePath });
|
|
12
11
|
if (!routeRegistry) {
|
|
13
12
|
throw new Error("routeRegistry is required for TezX initialization");
|
|
@@ -27,24 +26,27 @@ export class TezX extends Router {
|
|
|
27
26
|
let index = -1;
|
|
28
27
|
const dispatch = (i) => {
|
|
29
28
|
if (i <= index)
|
|
30
|
-
|
|
29
|
+
return Promise.reject(new Error("next() called multiple times"));
|
|
31
30
|
index = i;
|
|
32
31
|
const fn = stack[i];
|
|
33
32
|
if (!fn)
|
|
34
33
|
return ctx.res;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (result instanceof
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
try {
|
|
35
|
+
const result = fn(ctx, () => dispatch(i + 1));
|
|
36
|
+
if (!(result instanceof Promise)) {
|
|
37
|
+
if (result instanceof Response)
|
|
38
|
+
ctx.res = result;
|
|
39
|
+
return result ?? ctx.res;
|
|
40
|
+
}
|
|
41
|
+
return result.then((res) => {
|
|
42
|
+
if (res instanceof Response)
|
|
43
|
+
ctx.res = res;
|
|
44
|
+
return res ?? ctx.res;
|
|
45
|
+
}, (err) => Promise.reject(err));
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return Promise.reject(err);
|
|
40
49
|
}
|
|
41
|
-
return result.then((res) => {
|
|
42
|
-
if (res instanceof Response)
|
|
43
|
-
ctx.res = res;
|
|
44
|
-
return ctx.res;
|
|
45
|
-
}, (err) => {
|
|
46
|
-
throw err;
|
|
47
|
-
});
|
|
48
50
|
};
|
|
49
51
|
return dispatch(0);
|
|
50
52
|
}
|
|
@@ -63,9 +65,13 @@ export class TezX extends Router {
|
|
|
63
65
|
if (mLen === 0)
|
|
64
66
|
return this.#notFound(ctx);
|
|
65
67
|
ctx.params = params;
|
|
66
|
-
if (mLen === 1)
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
if (mLen === 1) {
|
|
69
|
+
ctx.res = await middlewares[0](ctx);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
ctx.res = await this.#composeChain(ctx, middlewares);
|
|
73
|
+
}
|
|
74
|
+
return ctx.res ?? this.#notFound(ctx);
|
|
69
75
|
}
|
|
70
76
|
catch (err) {
|
|
71
77
|
let error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -73,10 +79,9 @@ export class TezX extends Router {
|
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
async serve(req, server) {
|
|
76
|
-
const method = req.method
|
|
82
|
+
const method = req.method;
|
|
77
83
|
if (method === "HEAD") {
|
|
78
|
-
const
|
|
79
|
-
const res = await this.#handleRequest(headReq, "GET", server);
|
|
84
|
+
const res = await this.#handleRequest(req, "GET", server);
|
|
80
85
|
return new Response(null, {
|
|
81
86
|
status: res.status,
|
|
82
87
|
statusText: res.statusText,
|
package/helper/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Ctx, NetAddr } from "../types/index.js";
|
|
1
2
|
import { useFormData } from "./formData.js";
|
|
2
3
|
import { generateID, generateRandomBase64, generateUUID } from "./generateID.js";
|
|
3
4
|
export { useFormData, generateID, generateRandomBase64, generateUUID };
|
|
@@ -8,3 +9,17 @@ declare const _default: {
|
|
|
8
9
|
generateUUID: typeof generateUUID;
|
|
9
10
|
};
|
|
10
11
|
export default _default;
|
|
12
|
+
/**
|
|
13
|
+
* Retrieves remote connection details of the client from the Bun server context.
|
|
14
|
+
*
|
|
15
|
+
* This function returns network address information such as transport protocol,
|
|
16
|
+
* address family, IP/hostname, and port.
|
|
17
|
+
*
|
|
18
|
+
* @param {Ctx} ctx - The request context containing the Bun server and raw request.
|
|
19
|
+
* @returns {NetAddr} Object containing client connection details.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* const conn = getConnInfo(ctx);
|
|
23
|
+
* console.log(conn.address, conn.port);
|
|
24
|
+
*/
|
|
25
|
+
export declare function getConnInfo(ctx: Ctx): NetAddr;
|
package/helper/index.js
CHANGED
package/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { HttpBaseResponse } from "../types/index.js";
|
|
|
6
6
|
export type DetectBotOptions = {
|
|
7
7
|
/**
|
|
8
8
|
* 🤖 List of known bot-like User-Agent patterns.
|
|
9
|
+
* Middleware will block requests whose User-Agent matches any of these patterns.
|
|
9
10
|
* @default ["bot", "spider", "crawl", "slurp"]
|
|
10
11
|
* @example
|
|
11
12
|
* botUserAgents: ["bot", "crawler", "indexer"]
|
|
@@ -13,40 +14,31 @@ export type DetectBotOptions = {
|
|
|
13
14
|
botUserAgents?: string[];
|
|
14
15
|
/**
|
|
15
16
|
* ⚖️ Enable rate-limiting based bot detection.
|
|
16
|
-
*
|
|
17
|
+
* Middleware will track requests per client and block if exceeding limits.
|
|
17
18
|
* @default false
|
|
18
|
-
* @example
|
|
19
|
-
* enableRateLimiting: true
|
|
20
19
|
*/
|
|
21
20
|
enableRateLimiting?: boolean;
|
|
22
21
|
/**
|
|
23
|
-
* 🔑
|
|
24
|
-
*
|
|
22
|
+
* 🔑 Function to generate a client identifier for rate-limiting.
|
|
23
|
+
* By default, it uses the client's IP and port via `getConnInfo`.
|
|
25
24
|
* @example
|
|
26
|
-
* keyGenerator: (ctx) => ctx.user?.id || ctx.ip
|
|
25
|
+
* keyGenerator: (ctx) => ctx.user?.id || ctx.ip
|
|
27
26
|
*/
|
|
28
27
|
keyGenerator?: (ctx: Context) => string;
|
|
29
28
|
/**
|
|
30
29
|
* ⚠️ Maximum allowed requests in the rate-limit window.
|
|
31
|
-
* Only used
|
|
30
|
+
* Only used if `enableRateLimiting` is true.
|
|
32
31
|
* @default 30
|
|
33
32
|
*/
|
|
34
33
|
maxRequests?: number;
|
|
35
34
|
/**
|
|
36
|
-
* ⏱️ Time window for rate-limiting
|
|
35
|
+
* ⏱️ Time window for rate-limiting in milliseconds.
|
|
37
36
|
* @default 60000 (1 minute)
|
|
38
37
|
*/
|
|
39
38
|
windowMs?: number;
|
|
40
39
|
/**
|
|
41
40
|
* 🔄 Custom rate-limit storage implementation.
|
|
42
|
-
*
|
|
43
|
-
* @default In-memory Map
|
|
44
|
-
* @example
|
|
45
|
-
* storage: {
|
|
46
|
-
* get: (key) => myRedisClient.get(key),
|
|
47
|
-
* set: (key, value) => myRedisClient.set(key, value),
|
|
48
|
-
* clearExpired: () => {}
|
|
49
|
-
* }
|
|
41
|
+
* Can integrate with Redis, Memcached, or an in-memory store.
|
|
50
42
|
*/
|
|
51
43
|
storage?: {
|
|
52
44
|
get: (key: string) => {
|
|
@@ -61,54 +53,44 @@ export type DetectBotOptions = {
|
|
|
61
53
|
};
|
|
62
54
|
/**
|
|
63
55
|
* 🚫 Optional IP blacklist checker.
|
|
64
|
-
*
|
|
56
|
+
* Return true to block a specific IP.
|
|
65
57
|
* @default () => false
|
|
66
|
-
* @example
|
|
67
|
-
* isBlacklisted: (ctx) => ctx.req.remoteAddress.address === "192.168.0.10"
|
|
68
58
|
*/
|
|
69
59
|
isBlacklisted?: (ctx: Context) => boolean | Promise<boolean>;
|
|
70
60
|
/**
|
|
71
61
|
* 🧠 Custom bot detector function.
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* customBotDetector: (ctx) => ctx.query?.token === "weird"
|
|
62
|
+
* Return true to block the request based on custom logic.
|
|
75
63
|
*/
|
|
76
64
|
customBotDetector?: (ctx: Context) => boolean | Promise<boolean>;
|
|
77
65
|
/**
|
|
78
|
-
* 🛡️
|
|
79
|
-
* Can return a custom HTTP response (
|
|
66
|
+
* 🛡️ Callback executed when a bot is detected.
|
|
67
|
+
* Can return a custom HTTP response (JSON, HTML, redirect, etc.).
|
|
80
68
|
* @default Responds with 403 and JSON error.
|
|
81
|
-
* @example
|
|
82
|
-
* onBotDetected: (ctx, reason) => ctx.status(403).json({ error: `Blocked: ${reason}` })
|
|
83
69
|
*/
|
|
84
70
|
onBotDetected?: (ctx: Context, reason: string) => HttpBaseResponse;
|
|
85
71
|
};
|
|
86
72
|
/**
|
|
87
73
|
* 🤖 Smart Bot Detection Middleware
|
|
88
74
|
*
|
|
89
|
-
* Detects automated or malicious requests using
|
|
75
|
+
* Detects automated or malicious requests using multiple strategies:
|
|
90
76
|
* - User-Agent analysis
|
|
91
|
-
* - IP
|
|
92
|
-
* - Rate
|
|
93
|
-
* - Custom detection
|
|
94
|
-
*
|
|
95
|
-
* 🧩 Supports:
|
|
96
|
-
* - Bun (uses precompiled RegExp for speed)
|
|
97
|
-
* - Node.js / Deno (optimized `includes()` loop)
|
|
77
|
+
* - IP blacklist
|
|
78
|
+
* - Rate-limiting per client
|
|
79
|
+
* - Custom bot detection
|
|
98
80
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
81
|
+
* The client identity for rate-limiting is determined via `getConnInfo(ctx)`,
|
|
82
|
+
* which returns the connection info:
|
|
101
83
|
* ```ts
|
|
102
|
-
*
|
|
103
|
-
* //
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
84
|
+
* {
|
|
85
|
+
* address: string; // IP address of the client
|
|
86
|
+
* port?: number; // Port number of the client
|
|
87
|
+
* transport?: string;// Transport protocol (tcp, udp)
|
|
88
|
+
* }
|
|
107
89
|
* ```
|
|
108
90
|
*
|
|
109
91
|
* 📦 Example Usage:
|
|
110
92
|
* ```ts
|
|
111
|
-
* import { detectBot } from "tezx/middleware
|
|
93
|
+
* import { detectBot } from "tezx/middleware";
|
|
112
94
|
*
|
|
113
95
|
* app.use(detectBot({
|
|
114
96
|
* enableRateLimiting: true,
|
package/middleware/detect-bot.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getConnInfo } from "../helper/index.js";
|
|
1
2
|
import { createRateLimitDefaultStorage, isRateLimit } from "../utils/rateLimit.js";
|
|
2
3
|
export const detectBot = (opts = {}) => {
|
|
3
4
|
const botUAs = opts.botUserAgents || ["bot", "spider", "crawl", "slurp"];
|
|
@@ -5,7 +6,7 @@ export const detectBot = (opts = {}) => {
|
|
|
5
6
|
const botRegex = new RegExp(botUAs.join("|"), "i");
|
|
6
7
|
checkBot = (ua) => botRegex.test(ua);
|
|
7
8
|
const keyGenerator = opts.keyGenerator ?? ((ctx) => {
|
|
8
|
-
const addr = ctx
|
|
9
|
+
const addr = getConnInfo(ctx);
|
|
9
10
|
return addr ? `${addr.address}:${addr.port}` : "unknown";
|
|
10
11
|
});
|
|
11
12
|
const maxReq = opts.maxRequests || 30;
|
package/middleware/index.d.ts
CHANGED
package/middleware/index.js
CHANGED
package/middleware/pagination.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context } from "../
|
|
1
|
+
import { Context } from "../index.js";
|
|
2
2
|
import { HttpBaseResponse, Middleware } from "../types/index.js";
|
|
3
3
|
export type RateLimiterOptions = {
|
|
4
4
|
/**
|
|
@@ -14,15 +14,16 @@ export type RateLimiterOptions = {
|
|
|
14
14
|
*/
|
|
15
15
|
windowMs: number;
|
|
16
16
|
/**
|
|
17
|
-
* 🔑
|
|
18
|
-
*
|
|
17
|
+
* 🔑 Function to generate a client identifier for rate-limiting.
|
|
18
|
+
* By default, it uses `X-Forwarded-For`, `Client-IP`, or `getConnInfo(ctx)`.
|
|
19
19
|
* @example
|
|
20
|
-
* keyGenerator: (ctx) => ctx.user?.id || ctx.ip
|
|
20
|
+
* keyGenerator: (ctx) => ctx.user?.id || ctx.ip
|
|
21
21
|
*/
|
|
22
22
|
keyGenerator?: (ctx: Context) => string;
|
|
23
23
|
/**
|
|
24
|
-
* 🔄 Custom cache
|
|
25
|
-
*
|
|
24
|
+
* 🔄 Custom cache/storage implementation.
|
|
25
|
+
* Must provide `get`, `set`, and `clearExpired` methods.
|
|
26
|
+
* @default In-memory Map via `createRateLimitDefaultStorage()`
|
|
26
27
|
*/
|
|
27
28
|
storage?: {
|
|
28
29
|
get: (key: string) => {
|
|
@@ -36,42 +37,39 @@ export type RateLimiterOptions = {
|
|
|
36
37
|
clearExpired: () => void;
|
|
37
38
|
};
|
|
38
39
|
/**
|
|
39
|
-
* 🛑 Custom rate limit exceeded
|
|
40
|
-
* @default
|
|
41
|
-
* @example
|
|
42
|
-
* onError: (ctx, retryAfter) => {
|
|
43
|
-
* ctx.status = 429;
|
|
44
|
-
* throw new Error( `Rate limit exceeded. Try again in ${retryAfter} seconds.`);
|
|
45
|
-
* }
|
|
40
|
+
* 🛑 Custom handler when rate limit is exceeded.
|
|
41
|
+
* @default Throws 429 status with Retry-After header
|
|
46
42
|
*/
|
|
47
43
|
onError?: (ctx: Context, retryAfter: number, error: Error) => HttpBaseResponse;
|
|
48
44
|
};
|
|
49
45
|
/**
|
|
50
|
-
* 🚦 Rate
|
|
46
|
+
* 🚦 Rate Limiter Middleware
|
|
47
|
+
*
|
|
48
|
+
* Throttles requests per client based on a sliding window.
|
|
49
|
+
* Supports custom client identification and storage backends.
|
|
50
|
+
*
|
|
51
|
+
* Client IP detection uses (in order):
|
|
52
|
+
* 1. `X-Forwarded-For` header
|
|
53
|
+
* 2. `Client-IP` header
|
|
54
|
+
* 3. `getConnInfo(ctx)` fallback
|
|
51
55
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
* @param {RateLimiterOptions} options - Configuration
|
|
60
|
-
* @returns {Middleware} Middleware function
|
|
56
|
+
* ```ts
|
|
57
|
+
* import { getConnInfo } from "tezx/helper";
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* @param options RateLimiterOptions
|
|
61
|
+
* @returns Middleware function
|
|
61
62
|
*
|
|
62
63
|
* @example
|
|
63
64
|
* // Basic rate limiting (100 requests/minute)
|
|
64
|
-
* app.use(rateLimiter({
|
|
65
|
-
* maxRequests: 100,
|
|
66
|
-
* windowMs: 60_000
|
|
67
|
-
* }));
|
|
65
|
+
* app.use(rateLimiter({ maxRequests: 100, windowMs: 60_000 }));
|
|
68
66
|
*
|
|
69
67
|
* // Custom client identification
|
|
70
68
|
* app.use(rateLimiter({
|
|
71
69
|
* maxRequests: 10,
|
|
72
70
|
* windowMs: 10_000,
|
|
73
|
-
* keyGenerator: (ctx) => ctx.user?.id
|
|
71
|
+
* keyGenerator: (ctx) => ctx.user?.id
|
|
74
72
|
* }));
|
|
75
73
|
*/
|
|
76
74
|
declare const rateLimiter: <T extends Record<string, any> = {}, Path extends string = any>(options: RateLimiterOptions) => Middleware<T, Path>;
|
|
77
|
-
export { rateLimiter
|
|
75
|
+
export { rateLimiter as default, rateLimiter };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getConnInfo } from "../helper/index.js";
|
|
1
2
|
import { createRateLimitDefaultStorage, isRateLimit } from "../utils/rateLimit.js";
|
|
2
3
|
const rateLimiter = (options) => {
|
|
3
4
|
const { maxRequests, windowMs, keyGenerator = (ctx) => {
|
|
@@ -9,9 +10,8 @@ const rateLimiter = (options) => {
|
|
|
9
10
|
const clientIp = ctx.req.header("client-ip");
|
|
10
11
|
if (clientIp)
|
|
11
12
|
return clientIp;
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
return `${addr}:${port}`;
|
|
13
|
+
const { port, address } = getConnInfo(ctx) ?? {};
|
|
14
|
+
return `${address}:${port}`;
|
|
15
15
|
}, storage = createRateLimitDefaultStorage(), onError = (ctx, retryAfter, error) => {
|
|
16
16
|
ctx.status(429);
|
|
17
17
|
throw new Error(`Rate limit exceeded. Try again in ${retryAfter} seconds.`);
|
|
@@ -30,4 +30,4 @@ const rateLimiter = (options) => {
|
|
|
30
30
|
return await next();
|
|
31
31
|
};
|
|
32
32
|
};
|
|
33
|
-
export { rateLimiter
|
|
33
|
+
export { rateLimiter as default, rateLimiter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tezx",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.3",
|
|
4
4
|
"description": "TezX is a modern, ultra-lightweight, and high-performance JavaScript framework built specifically for Bun. It provides a minimal yet powerful API, seamless environment management, and a high-concurrency HTTP engine for building fast, scalable web applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "cjs/index.js",
|
package/types/index.d.ts
CHANGED
|
@@ -311,7 +311,11 @@ export interface FormDataOptions {
|
|
|
311
311
|
*/
|
|
312
312
|
type TransportType = "tcp" | "udp" | "unix" | "pipe" | "unixpacket";
|
|
313
313
|
/**
|
|
314
|
-
*
|
|
314
|
+
* Remote address details of the connected client.
|
|
315
|
+
* @property {string} [transport] - Transport protocol (e.g., "tcp", "udp").
|
|
316
|
+
* @property {"IPv4" | "IPv6" | "Unix"} [family] - Address family.
|
|
317
|
+
* @property {string} [hostname] - Hostname or IP address.
|
|
318
|
+
* @property {number} [port] - Port number.
|
|
315
319
|
*/
|
|
316
320
|
export type NetAddr = {
|
|
317
321
|
/** Transport protocol used by the connection. */
|
package/utils/mimeTypes.d.ts
CHANGED
|
@@ -1,4 +1,117 @@
|
|
|
1
1
|
export declare const mimeTypes: {
|
|
2
|
-
|
|
2
|
+
readonly html: "text/html";
|
|
3
|
+
readonly htm: "text/html";
|
|
4
|
+
readonly css: "text/css";
|
|
5
|
+
readonly js: "text/javascript";
|
|
6
|
+
readonly mjs: "text/javascript";
|
|
7
|
+
readonly json: "application/json";
|
|
8
|
+
readonly xml: "application/xml";
|
|
9
|
+
readonly txt: "text/plain";
|
|
10
|
+
readonly md: "text/markdown";
|
|
11
|
+
readonly csv: "text/csv";
|
|
12
|
+
readonly tsv: "text/tab-separated-values";
|
|
13
|
+
readonly rtf: "application/rtf";
|
|
14
|
+
readonly markdown: "text/markdown";
|
|
15
|
+
readonly jsx: "text/javascript";
|
|
16
|
+
readonly ts: "text/typescript";
|
|
17
|
+
readonly tsx: "text/typescript";
|
|
18
|
+
readonly jsonld: "application/ld+json";
|
|
19
|
+
readonly png: "image/png";
|
|
20
|
+
readonly jpg: "image/jpeg";
|
|
21
|
+
readonly jpeg: "image/jpeg";
|
|
22
|
+
readonly gif: "image/gif";
|
|
23
|
+
readonly svg: "image/svg+xml";
|
|
24
|
+
readonly webp: "image/webp";
|
|
25
|
+
readonly ico: "image/x-icon";
|
|
26
|
+
readonly bmp: "image/bmp";
|
|
27
|
+
readonly tiff: "image/tiff";
|
|
28
|
+
readonly psd: "image/vnd.adobe.photoshop";
|
|
29
|
+
readonly tif: "image/tiff";
|
|
30
|
+
readonly avif: "image/avif";
|
|
31
|
+
readonly woff: "font/woff";
|
|
32
|
+
readonly woff2: "font/woff2";
|
|
33
|
+
readonly ttf: "font/ttf";
|
|
34
|
+
readonly otf: "font/otf";
|
|
35
|
+
readonly eot: "application/vnd.ms-fontobject";
|
|
36
|
+
readonly mp4: "video/mp4";
|
|
37
|
+
readonly webm: "video/webm";
|
|
38
|
+
readonly ogg: "video/ogg";
|
|
39
|
+
readonly mov: "video/quicktime";
|
|
40
|
+
readonly avi: "video/x-msvideo";
|
|
41
|
+
readonly wmv: "video/x-ms-wmv";
|
|
42
|
+
readonly flv: "video/x-flv";
|
|
43
|
+
readonly "3gp": "video/3gpp";
|
|
44
|
+
readonly mkv: "video/x-matroska";
|
|
45
|
+
readonly mpeg: "video/mpeg";
|
|
46
|
+
readonly mpg: "video/mpeg";
|
|
47
|
+
readonly mp3: "audio/mpeg";
|
|
48
|
+
readonly wav: "audio/wav";
|
|
49
|
+
readonly aac: "audio/aac";
|
|
50
|
+
readonly flac: "audio/flac";
|
|
51
|
+
readonly m4a: "audio/mp4";
|
|
52
|
+
readonly mid: "audio/midi";
|
|
53
|
+
readonly midi: "audio/midi";
|
|
54
|
+
readonly weba: "audio/webm";
|
|
55
|
+
readonly pdf: "application/pdf";
|
|
56
|
+
readonly odp: "application/vnd.oasis.opendocument.presentation";
|
|
57
|
+
readonly zip: "application/zip";
|
|
58
|
+
readonly gz: "application/gzip";
|
|
59
|
+
readonly tar: "application/x-tar";
|
|
60
|
+
readonly rar: "application/x-rar-compressed";
|
|
61
|
+
readonly _7z: "application/x-7z-compressed";
|
|
62
|
+
readonly bz2: "application/x-bzip2";
|
|
63
|
+
readonly "7z": "application/x-7z-compressed";
|
|
64
|
+
readonly doc: "application/msword";
|
|
65
|
+
readonly docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
66
|
+
readonly xls: "application/vnd.ms-excel";
|
|
67
|
+
readonly xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
68
|
+
readonly ppt: "application/vnd.ms-powerpoint";
|
|
69
|
+
readonly pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
70
|
+
readonly odt: "application/vnd.oasis.opendocument.text";
|
|
71
|
+
readonly ods: "application/vnd.oasis.opendocument.spreadsheet";
|
|
72
|
+
readonly wasm: "application/wasm";
|
|
73
|
+
readonly map: "application/json";
|
|
74
|
+
readonly yaml: "application/yaml";
|
|
75
|
+
readonly yml: "application/yaml";
|
|
76
|
+
readonly proto: "text/plain";
|
|
77
|
+
readonly graphql: "application/graphql";
|
|
78
|
+
readonly pem: "application/x-pem-file";
|
|
79
|
+
readonly cer: "application/pkix-cert";
|
|
80
|
+
readonly crt: "application/x-x509-ca-cert";
|
|
81
|
+
readonly key: "application/x-pem-file";
|
|
82
|
+
readonly pfx: "application/x-pkcs12";
|
|
83
|
+
readonly glb: "model/gltf-binary";
|
|
84
|
+
readonly gltf: "model/gltf+json";
|
|
85
|
+
readonly obj: "model/obj";
|
|
86
|
+
readonly stl: "model/stl";
|
|
87
|
+
readonly usdz: "model/vnd.usdz+zip";
|
|
88
|
+
readonly exe: "application/x-msdownload";
|
|
89
|
+
readonly dmg: "application/x-apple-diskimage";
|
|
90
|
+
readonly deb: "application/x-debian-package";
|
|
91
|
+
readonly rpm: "application/x-redhat-package-manager";
|
|
92
|
+
readonly apk: "application/vnd.android.package-archive";
|
|
93
|
+
readonly bin: "application/octet-stream";
|
|
94
|
+
readonly iso: "application/octet-stream";
|
|
95
|
+
readonly webmanifest: "application/manifest+json";
|
|
96
|
+
readonly ics: "text/calendar";
|
|
97
|
+
readonly vcf: "text/vcard";
|
|
98
|
+
readonly warc: "application/warc";
|
|
99
|
+
readonly atom: "application/atom+xml";
|
|
100
|
+
readonly rss: "application/rss+xml";
|
|
101
|
+
readonly dll: "application/x-msdownload";
|
|
102
|
+
readonly sh: "application/x-sh";
|
|
103
|
+
readonly py: "text/x-python";
|
|
104
|
+
readonly rb: "text/x-ruby";
|
|
105
|
+
readonly pl: "text/x-perl";
|
|
106
|
+
readonly php: "application/x-httpd-php";
|
|
107
|
+
readonly torrent: "application/x-bittorrent";
|
|
108
|
+
readonly ipa: "application/vnd.iphone";
|
|
109
|
+
readonly eps: "application/postscript";
|
|
110
|
+
readonly ps: "application/postscript";
|
|
111
|
+
readonly ai: "application/postscript";
|
|
112
|
+
readonly swf: "application/x-shockwave-flash";
|
|
113
|
+
readonly jar: "application/java-archive";
|
|
114
|
+
readonly gcode: "text/x.gcode";
|
|
3
115
|
};
|
|
116
|
+
export type ContentType = (typeof mimeTypes[keyof typeof mimeTypes]) | "application/octet-stream" | (string & {});
|
|
4
117
|
export declare const defaultMimeType = "application/octet-stream";
|
package/utils/mimeTypes.js
CHANGED
|
@@ -12,6 +12,10 @@ export const mimeTypes = {
|
|
|
12
12
|
tsv: "text/tab-separated-values",
|
|
13
13
|
rtf: "application/rtf",
|
|
14
14
|
markdown: "text/markdown",
|
|
15
|
+
jsx: "text/javascript",
|
|
16
|
+
ts: "text/typescript",
|
|
17
|
+
tsx: "text/typescript",
|
|
18
|
+
jsonld: "application/ld+json",
|
|
15
19
|
png: "image/png",
|
|
16
20
|
jpg: "image/jpeg",
|
|
17
21
|
jpeg: "image/jpeg",
|
|
@@ -22,6 +26,13 @@ export const mimeTypes = {
|
|
|
22
26
|
bmp: "image/bmp",
|
|
23
27
|
tiff: "image/tiff",
|
|
24
28
|
psd: "image/vnd.adobe.photoshop",
|
|
29
|
+
tif: "image/tiff",
|
|
30
|
+
avif: "image/avif",
|
|
31
|
+
woff: "font/woff",
|
|
32
|
+
woff2: "font/woff2",
|
|
33
|
+
ttf: "font/ttf",
|
|
34
|
+
otf: "font/otf",
|
|
35
|
+
eot: "application/vnd.ms-fontobject",
|
|
25
36
|
mp4: "video/mp4",
|
|
26
37
|
webm: "video/webm",
|
|
27
38
|
ogg: "video/ogg",
|
|
@@ -30,6 +41,9 @@ export const mimeTypes = {
|
|
|
30
41
|
wmv: "video/x-ms-wmv",
|
|
31
42
|
flv: "video/x-flv",
|
|
32
43
|
"3gp": "video/3gpp",
|
|
44
|
+
mkv: "video/x-matroska",
|
|
45
|
+
mpeg: "video/mpeg",
|
|
46
|
+
mpg: "video/mpeg",
|
|
33
47
|
mp3: "audio/mpeg",
|
|
34
48
|
wav: "audio/wav",
|
|
35
49
|
aac: "audio/aac",
|
|
@@ -37,11 +51,7 @@ export const mimeTypes = {
|
|
|
37
51
|
m4a: "audio/mp4",
|
|
38
52
|
mid: "audio/midi",
|
|
39
53
|
midi: "audio/midi",
|
|
40
|
-
|
|
41
|
-
woff2: "font/woff2",
|
|
42
|
-
ttf: "font/ttf",
|
|
43
|
-
otf: "font/otf",
|
|
44
|
-
eot: "application/vnd.ms-fontobject",
|
|
54
|
+
weba: "audio/webm",
|
|
45
55
|
pdf: "application/pdf",
|
|
46
56
|
odp: "application/vnd.oasis.opendocument.presentation",
|
|
47
57
|
zip: "application/zip",
|
|
@@ -80,6 +90,8 @@ export const mimeTypes = {
|
|
|
80
90
|
deb: "application/x-debian-package",
|
|
81
91
|
rpm: "application/x-redhat-package-manager",
|
|
82
92
|
apk: "application/vnd.android.package-archive",
|
|
93
|
+
bin: "application/octet-stream",
|
|
94
|
+
iso: "application/octet-stream",
|
|
83
95
|
webmanifest: "application/manifest+json",
|
|
84
96
|
ics: "text/calendar",
|
|
85
97
|
vcf: "text/vcard",
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getConnInfo = getConnInfo;
|
|
4
|
-
function getConnInfo() {
|
|
5
|
-
return (ctx, next) => {
|
|
6
|
-
let server = ctx.server;
|
|
7
|
-
if (server && server.requestIP) {
|
|
8
|
-
ctx.req.remoteAddress = server.requestIP(ctx.rawRequest);
|
|
9
|
-
}
|
|
10
|
-
return next();
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
exports.default = getConnInfo;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { Middleware } from "../types/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* Middleware to extract and inject connection information into the request context.
|
|
4
|
-
*
|
|
5
|
-
* This middleware reads the socket's remote address information (like IP, port, and family)
|
|
6
|
-
* from the request object and attaches it to `ctx.req.remoteAddress`.
|
|
7
|
-
*
|
|
8
|
-
* @returns {Middleware<any>} The middleware function that sets `ctx.req.remoteAddress`.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* import { getConnInfo } from "tezx/middleware";
|
|
12
|
-
*
|
|
13
|
-
* app.use(getConnInfo());
|
|
14
|
-
*
|
|
15
|
-
* // Access later in route handler:
|
|
16
|
-
* router.get("/", (ctx) => {
|
|
17
|
-
* const ip = ctx.req.remoteAddress?.address;
|
|
18
|
-
* return new Response(`Your IP: ${ip}`);
|
|
19
|
-
* });
|
|
20
|
-
*/
|
|
21
|
-
export declare function getConnInfo<T extends Record<string, any> = {}, Path extends string = any>(): Middleware<T, Path>;
|
|
22
|
-
export default getConnInfo;
|