azurajs 3.0.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/index.js +128 -6
- package/dist/config/index.js.map +1 -1
- package/dist/config/index.mjs +130 -1
- package/dist/config/index.mjs.map +1 -1
- package/dist/core/index.js +1100 -11
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +1102 -3
- package/dist/core/index.mjs.map +1 -1
- package/dist/decorators/index.js +117 -87
- package/dist/decorators/index.js.map +1 -1
- package/dist/decorators/index.mjs +98 -1
- package/dist/decorators/index.mjs.map +1 -1
- package/dist/index.js +2592 -236
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2537 -9
- package/dist/index.mjs.map +1 -1
- package/dist/middleware/index.js +16 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +17 -1
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/plugins/index.js +1056 -73
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/index.mjs +1042 -1
- package/dist/plugins/index.mjs.map +1 -1
- package/dist/types/index.js +49 -12
- package/dist/types/index.js.map +1 -1
- package/dist/types/index.mjs +49 -2
- package/dist/types/index.mjs.map +1 -1
- package/dist/utils/index.js +551 -50
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +541 -3
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +35 -17
- package/{dist/chunk-DR254CWJ.mjs → src/config/ConfigModule.ts} +169 -132
- package/src/config/index.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/router.ts +284 -0
- package/{dist/chunk-EYAHUNC7.mjs → src/core/server.ts} +590 -699
- package/src/decorators/Route.ts +110 -0
- package/src/decorators/index.ts +23 -0
- package/src/index.ts +12 -0
- package/src/middleware/LoggingMiddleware.ts +20 -0
- package/src/middleware/index.ts +1 -0
- package/src/plugins/CORSPlugin.ts +56 -0
- package/src/plugins/CircuitBreakerPlugin.ts +84 -0
- package/src/plugins/CompressionPlugin.ts +80 -0
- package/src/plugins/ETagPlugin.ts +31 -0
- package/src/plugins/HealthCheckPlugin.ts +57 -0
- package/src/plugins/HelmetPlugin.ts +89 -0
- package/src/plugins/JWTPlugin.ts +132 -0
- package/src/plugins/MultipartPlugin.ts +168 -0
- package/src/plugins/ProxyPlugin.ts +89 -0
- package/src/plugins/RateLimitPlugin.ts +96 -0
- package/src/plugins/RequestIdPlugin.ts +21 -0
- package/src/plugins/SSEPlugin.ts +114 -0
- package/src/plugins/SessionPlugin.ts +98 -0
- package/src/plugins/StaticPlugin.ts +152 -0
- package/src/plugins/TimeoutPlugin.ts +33 -0
- package/src/plugins/index.ts +18 -0
- package/src/types/common.type.ts +82 -0
- package/src/types/config.type.ts +57 -0
- package/{dist/chunk-OWUGAI5V.mjs → src/types/http/status.ts} +49 -51
- package/src/types/index.ts +55 -0
- package/src/types/plugins/plugin.type.ts +170 -0
- package/src/types/reflect.d.ts +14 -0
- package/src/types/routes.type.ts +70 -0
- package/src/utils/HttpError.ts +62 -0
- package/src/utils/IpResolver.ts +30 -0
- package/src/utils/Logger.ts +144 -0
- package/src/utils/Parser.ts +182 -0
- package/src/utils/cookies/CookieManager.ts +48 -0
- package/src/utils/index.ts +9 -0
- package/{dist/chunk-UWIFSGSQ.mjs → src/utils/validators/DTOValidator.ts} +145 -141
- package/src/utils/validators/SchemaValidator.ts +45 -0
- package/dist/chunk-3UFAWS2V.js +0 -392
- package/dist/chunk-3UFAWS2V.js.map +0 -1
- package/dist/chunk-4LSFAAZW.js +0 -4
- package/dist/chunk-4LSFAAZW.js.map +0 -1
- package/dist/chunk-7NSRIVZM.js +0 -54
- package/dist/chunk-7NSRIVZM.js.map +0 -1
- package/dist/chunk-AOG6NYAM.js +0 -144
- package/dist/chunk-AOG6NYAM.js.map +0 -1
- package/dist/chunk-DR254CWJ.mjs.map +0 -1
- package/dist/chunk-EYAHUNC7.mjs.map +0 -1
- package/dist/chunk-HHDQPIJN.mjs +0 -19
- package/dist/chunk-HHDQPIJN.mjs.map +0 -1
- package/dist/chunk-HHZNAGGI.js +0 -702
- package/dist/chunk-HHZNAGGI.js.map +0 -1
- package/dist/chunk-KJM5XCAY.js +0 -21
- package/dist/chunk-KJM5XCAY.js.map +0 -1
- package/dist/chunk-NLSZKAPA.mjs +0 -1044
- package/dist/chunk-NLSZKAPA.mjs.map +0 -1
- package/dist/chunk-OWUGAI5V.mjs.map +0 -1
- package/dist/chunk-POPNQEOK.js +0 -1063
- package/dist/chunk-POPNQEOK.js.map +0 -1
- package/dist/chunk-QPRW4YU4.js +0 -134
- package/dist/chunk-QPRW4YU4.js.map +0 -1
- package/dist/chunk-REJDZUZ5.mjs +0 -382
- package/dist/chunk-REJDZUZ5.mjs.map +0 -1
- package/dist/chunk-TC6N6TJZ.mjs +0 -100
- package/dist/chunk-TC6N6TJZ.mjs.map +0 -1
- package/dist/chunk-TEUXKMXP.js +0 -122
- package/dist/chunk-TEUXKMXP.js.map +0 -1
- package/dist/chunk-UWIFSGSQ.mjs.map +0 -1
- package/dist/chunk-YPBKY4KY.mjs +0 -3
- package/dist/chunk-YPBKY4KY.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,2539 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import '
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { createServer as createServer$1, request as request$1 } from 'http';
|
|
2
|
+
import { createServer, request } from 'https';
|
|
3
|
+
import { readFileSync, existsSync, statSync, createReadStream, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
+
import { createHmac, createHash, randomUUID, randomBytes } from 'crypto';
|
|
5
|
+
import { createGzip, createDeflate } from 'zlib';
|
|
6
|
+
import { resolve, join, extname } from 'path';
|
|
7
|
+
import { URL } from 'url';
|
|
8
|
+
|
|
9
|
+
// src/core/server.ts
|
|
10
|
+
|
|
11
|
+
// src/core/router.ts
|
|
12
|
+
var PARAM_PREFIX = 58;
|
|
13
|
+
function createNode(segment = "") {
|
|
14
|
+
return {
|
|
15
|
+
segment,
|
|
16
|
+
children: /* @__PURE__ */ new Map(),
|
|
17
|
+
paramChild: null,
|
|
18
|
+
paramName: "",
|
|
19
|
+
handlers: /* @__PURE__ */ new Map(),
|
|
20
|
+
wildcardHandler: null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
var LRUCache = class {
|
|
24
|
+
capacity;
|
|
25
|
+
map;
|
|
26
|
+
head;
|
|
27
|
+
tail;
|
|
28
|
+
constructor(capacity) {
|
|
29
|
+
this.capacity = capacity;
|
|
30
|
+
this.map = /* @__PURE__ */ new Map();
|
|
31
|
+
this.head = { prev: null, next: null };
|
|
32
|
+
this.tail = { prev: null, next: null };
|
|
33
|
+
this.head.next = this.tail;
|
|
34
|
+
this.tail.prev = this.head;
|
|
35
|
+
}
|
|
36
|
+
get(key) {
|
|
37
|
+
const node = this.map.get(key);
|
|
38
|
+
if (!node) return void 0;
|
|
39
|
+
this.moveToHead(node);
|
|
40
|
+
return node.value;
|
|
41
|
+
}
|
|
42
|
+
set(key, value) {
|
|
43
|
+
const existing = this.map.get(key);
|
|
44
|
+
if (existing) {
|
|
45
|
+
existing.value = value;
|
|
46
|
+
this.moveToHead(existing);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const node = { value, key, prev: null, next: null };
|
|
50
|
+
this.map.set(key, node);
|
|
51
|
+
this.addToHead(node);
|
|
52
|
+
if (this.map.size > this.capacity) {
|
|
53
|
+
const removed = this.removeTail();
|
|
54
|
+
if (removed) this.map.delete(removed.key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
addToHead(node) {
|
|
58
|
+
node.prev = this.head;
|
|
59
|
+
node.next = this.head.next;
|
|
60
|
+
this.head.next.prev = node;
|
|
61
|
+
this.head.next = node;
|
|
62
|
+
}
|
|
63
|
+
removeNode(node) {
|
|
64
|
+
node.prev.next = node.next;
|
|
65
|
+
node.next.prev = node.prev;
|
|
66
|
+
}
|
|
67
|
+
moveToHead(node) {
|
|
68
|
+
this.removeNode(node);
|
|
69
|
+
this.addToHead(node);
|
|
70
|
+
}
|
|
71
|
+
removeTail() {
|
|
72
|
+
const node = this.tail.prev;
|
|
73
|
+
if (node === this.head) return null;
|
|
74
|
+
this.removeNode(node);
|
|
75
|
+
return node;
|
|
76
|
+
}
|
|
77
|
+
clear() {
|
|
78
|
+
this.map.clear();
|
|
79
|
+
this.head.next = this.tail;
|
|
80
|
+
this.tail.prev = this.head;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var Router = class {
|
|
84
|
+
root = createNode();
|
|
85
|
+
cache;
|
|
86
|
+
staticRoutes = /* @__PURE__ */ new Map();
|
|
87
|
+
constructor(cacheSize = 1024) {
|
|
88
|
+
this.cache = new LRUCache(cacheSize);
|
|
89
|
+
}
|
|
90
|
+
add(method, path, handler, middlewares = [], meta) {
|
|
91
|
+
this.cache.clear();
|
|
92
|
+
const normalizedPath = this.normalizePath(path);
|
|
93
|
+
const stored = { handler, middlewares, meta };
|
|
94
|
+
if (!normalizedPath.includes(":") && !normalizedPath.includes("*")) {
|
|
95
|
+
const key = `${method}:${normalizedPath}`;
|
|
96
|
+
this.staticRoutes.set(key, stored);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const segments = normalizedPath.split("/").filter(Boolean);
|
|
100
|
+
let node = this.root;
|
|
101
|
+
for (let i = 0; i < segments.length; i++) {
|
|
102
|
+
const seg = segments[i];
|
|
103
|
+
if (seg === "*") {
|
|
104
|
+
if (!node.wildcardHandler) {
|
|
105
|
+
node.wildcardHandler = /* @__PURE__ */ new Map();
|
|
106
|
+
}
|
|
107
|
+
node.wildcardHandler.set(method, stored);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (seg.charCodeAt(0) === PARAM_PREFIX) {
|
|
111
|
+
if (!node.paramChild) {
|
|
112
|
+
node.paramChild = createNode(seg);
|
|
113
|
+
node.paramChild.paramName = seg.slice(1);
|
|
114
|
+
}
|
|
115
|
+
node = node.paramChild;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let child = node.children.get(seg);
|
|
119
|
+
if (!child) {
|
|
120
|
+
child = createNode(seg);
|
|
121
|
+
node.children.set(seg, child);
|
|
122
|
+
}
|
|
123
|
+
node = child;
|
|
124
|
+
}
|
|
125
|
+
node.handlers.set(method, stored);
|
|
126
|
+
}
|
|
127
|
+
find(method, path) {
|
|
128
|
+
const normalizedPath = this.normalizePath(path);
|
|
129
|
+
const staticKey = `${method}:${normalizedPath}`;
|
|
130
|
+
const staticRoute = this.staticRoutes.get(staticKey);
|
|
131
|
+
if (staticRoute) {
|
|
132
|
+
return {
|
|
133
|
+
handler: staticRoute.handler,
|
|
134
|
+
params: {},
|
|
135
|
+
middlewares: staticRoute.middlewares,
|
|
136
|
+
meta: staticRoute.meta
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const cacheKey = staticKey;
|
|
140
|
+
const cached = this.cache.get(cacheKey);
|
|
141
|
+
if (cached !== void 0) {
|
|
142
|
+
return cached ? { ...cached, params: { ...cached.params } } : null;
|
|
143
|
+
}
|
|
144
|
+
const segments = normalizedPath.split("/").filter(Boolean);
|
|
145
|
+
const params = {};
|
|
146
|
+
const result = this.matchNode(this.root, segments, 0, params, method);
|
|
147
|
+
if (result) {
|
|
148
|
+
const match = {
|
|
149
|
+
handler: result.handler,
|
|
150
|
+
params,
|
|
151
|
+
middlewares: result.middlewares,
|
|
152
|
+
meta: result.meta
|
|
153
|
+
};
|
|
154
|
+
this.cache.set(cacheKey, match);
|
|
155
|
+
return { ...match, params: { ...params } };
|
|
156
|
+
}
|
|
157
|
+
this.cache.set(cacheKey, null);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
matchNode(node, segments, idx, params, method) {
|
|
161
|
+
if (idx === segments.length) {
|
|
162
|
+
return node.handlers.get(method) ?? null;
|
|
163
|
+
}
|
|
164
|
+
const seg = segments[idx];
|
|
165
|
+
const child = node.children.get(seg);
|
|
166
|
+
if (child) {
|
|
167
|
+
const result = this.matchNode(child, segments, idx + 1, params, method);
|
|
168
|
+
if (result) return result;
|
|
169
|
+
}
|
|
170
|
+
if (node.paramChild) {
|
|
171
|
+
params[node.paramChild.paramName] = seg;
|
|
172
|
+
const result = this.matchNode(node.paramChild, segments, idx + 1, params, method);
|
|
173
|
+
if (result) return result;
|
|
174
|
+
delete params[node.paramChild.paramName];
|
|
175
|
+
}
|
|
176
|
+
if (node.wildcardHandler) {
|
|
177
|
+
const route = node.wildcardHandler.get(method);
|
|
178
|
+
if (route) return route;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
normalizePath(path) {
|
|
183
|
+
if (path === "/") return "/";
|
|
184
|
+
if (path.endsWith("/") && path.length > 1) return path.slice(0, -1);
|
|
185
|
+
return path;
|
|
186
|
+
}
|
|
187
|
+
getAllRoutes() {
|
|
188
|
+
const routes = [];
|
|
189
|
+
for (const [key] of this.staticRoutes) {
|
|
190
|
+
const [method, path] = key.split(":", 2);
|
|
191
|
+
routes.push({ method, path });
|
|
192
|
+
}
|
|
193
|
+
this.collectRoutes(this.root, "", routes);
|
|
194
|
+
return routes;
|
|
195
|
+
}
|
|
196
|
+
collectRoutes(node, prefix, routes) {
|
|
197
|
+
for (const [method] of node.handlers) {
|
|
198
|
+
routes.push({ method, path: prefix || "/" });
|
|
199
|
+
}
|
|
200
|
+
for (const [seg, child] of node.children) {
|
|
201
|
+
this.collectRoutes(child, `${prefix}/${seg}`, routes);
|
|
202
|
+
}
|
|
203
|
+
if (node.paramChild) {
|
|
204
|
+
this.collectRoutes(
|
|
205
|
+
node.paramChild,
|
|
206
|
+
`${prefix}/:${node.paramChild.paramName}`,
|
|
207
|
+
routes
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (node.wildcardHandler) {
|
|
211
|
+
for (const [method] of node.wildcardHandler) {
|
|
212
|
+
routes.push({ method, path: `${prefix}/*` });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/utils/Logger.ts
|
|
219
|
+
var LEVEL_PRIORITY = {
|
|
220
|
+
debug: 0,
|
|
221
|
+
info: 1,
|
|
222
|
+
warn: 2,
|
|
223
|
+
error: 3,
|
|
224
|
+
silent: 4
|
|
225
|
+
};
|
|
226
|
+
var COLORS = {
|
|
227
|
+
reset: "\x1B[0m",
|
|
228
|
+
bold: "\x1B[1m",
|
|
229
|
+
dim: "\x1B[2m",
|
|
230
|
+
red: "\x1B[31m",
|
|
231
|
+
green: "\x1B[32m",
|
|
232
|
+
yellow: "\x1B[33m",
|
|
233
|
+
blue: "\x1B[34m",
|
|
234
|
+
magenta: "\x1B[35m",
|
|
235
|
+
cyan: "\x1B[36m",
|
|
236
|
+
white: "\x1B[37m",
|
|
237
|
+
gray: "\x1B[90m"};
|
|
238
|
+
var METHOD_COLORS = {
|
|
239
|
+
GET: COLORS.green,
|
|
240
|
+
POST: COLORS.blue,
|
|
241
|
+
PUT: COLORS.yellow,
|
|
242
|
+
DELETE: COLORS.red,
|
|
243
|
+
PATCH: COLORS.magenta,
|
|
244
|
+
HEAD: COLORS.cyan,
|
|
245
|
+
OPTIONS: COLORS.gray
|
|
246
|
+
};
|
|
247
|
+
function statusColor(code) {
|
|
248
|
+
if (code < 200) return COLORS.gray;
|
|
249
|
+
if (code < 300) return COLORS.green;
|
|
250
|
+
if (code < 400) return COLORS.cyan;
|
|
251
|
+
if (code < 500) return COLORS.yellow;
|
|
252
|
+
return COLORS.red;
|
|
253
|
+
}
|
|
254
|
+
function formatDuration(ns) {
|
|
255
|
+
const us = Number(ns) / 1e3;
|
|
256
|
+
if (us < 1e3) return `${us.toFixed(0)}\xB5s`;
|
|
257
|
+
const ms = us / 1e3;
|
|
258
|
+
if (ms < 1e3) return `${ms.toFixed(1)}ms`;
|
|
259
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
260
|
+
}
|
|
261
|
+
var Logger = class {
|
|
262
|
+
level;
|
|
263
|
+
useColors;
|
|
264
|
+
showTimestamp;
|
|
265
|
+
prefix;
|
|
266
|
+
constructor(options = {}) {
|
|
267
|
+
this.level = options.level ?? "info";
|
|
268
|
+
this.useColors = options.colors ?? process.stdout.isTTY !== false;
|
|
269
|
+
this.showTimestamp = options.timestamp ?? true;
|
|
270
|
+
this.prefix = options.prefix ?? "azura";
|
|
271
|
+
}
|
|
272
|
+
shouldLog(level) {
|
|
273
|
+
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.level];
|
|
274
|
+
}
|
|
275
|
+
timestamp() {
|
|
276
|
+
if (!this.showTimestamp) return "";
|
|
277
|
+
const now = /* @__PURE__ */ new Date();
|
|
278
|
+
return `${COLORS.gray}${now.toISOString().slice(11, 23)}${COLORS.reset} `;
|
|
279
|
+
}
|
|
280
|
+
tag(level) {
|
|
281
|
+
if (!this.useColors) return `[${level.toUpperCase()}]`;
|
|
282
|
+
const colorMap = {
|
|
283
|
+
debug: COLORS.gray,
|
|
284
|
+
info: COLORS.blue,
|
|
285
|
+
warn: COLORS.yellow,
|
|
286
|
+
error: COLORS.red
|
|
287
|
+
};
|
|
288
|
+
return `${colorMap[level] ?? ""}[${level.toUpperCase()}]${COLORS.reset}`;
|
|
289
|
+
}
|
|
290
|
+
debug(message, ...args) {
|
|
291
|
+
if (!this.shouldLog("debug")) return;
|
|
292
|
+
console.debug(`${this.timestamp()}${this.tag("debug")} ${message}`, ...args);
|
|
293
|
+
}
|
|
294
|
+
info(message, ...args) {
|
|
295
|
+
if (!this.shouldLog("info")) return;
|
|
296
|
+
console.info(`${this.timestamp()}${this.tag("info")} ${message}`, ...args);
|
|
297
|
+
}
|
|
298
|
+
warn(message, ...args) {
|
|
299
|
+
if (!this.shouldLog("warn")) return;
|
|
300
|
+
console.warn(`${this.timestamp()}${this.tag("warn")} ${message}`, ...args);
|
|
301
|
+
}
|
|
302
|
+
error(message, ...args) {
|
|
303
|
+
if (!this.shouldLog("error")) return;
|
|
304
|
+
console.error(`${this.timestamp()}${this.tag("error")} ${message}`, ...args);
|
|
305
|
+
}
|
|
306
|
+
request(method, path, statusCode, duration) {
|
|
307
|
+
if (!this.shouldLog("info")) return;
|
|
308
|
+
const mc = this.useColors ? METHOD_COLORS[method] ?? COLORS.white : "";
|
|
309
|
+
const sc = this.useColors ? statusColor(statusCode) : "";
|
|
310
|
+
const r = this.useColors ? COLORS.reset : "";
|
|
311
|
+
const dur = formatDuration(duration);
|
|
312
|
+
console.info(
|
|
313
|
+
`${this.timestamp()}${mc}${method.padEnd(7)}${r} ${path} ${sc}${statusCode}${r} ${COLORS.dim}${dur}${r}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
banner(port, host) {
|
|
317
|
+
if (!this.shouldLog("info")) return;
|
|
318
|
+
const c = this.useColors;
|
|
319
|
+
const lines = [
|
|
320
|
+
"",
|
|
321
|
+
`${c ? COLORS.bold + COLORS.cyan : ""} \u26A1 AzuraJS v3.0.0${c ? COLORS.reset : ""}`,
|
|
322
|
+
"",
|
|
323
|
+
`${c ? COLORS.green : ""} \u279C Local: ${c ? COLORS.bold : ""}http://${host}:${port}/${c ? COLORS.reset : ""}`,
|
|
324
|
+
`${c ? COLORS.dim : ""} \u279C Press Ctrl+C to stop${c ? COLORS.reset : ""}`,
|
|
325
|
+
""
|
|
326
|
+
];
|
|
327
|
+
console.info(lines.join("\n"));
|
|
328
|
+
}
|
|
329
|
+
setLevel(level) {
|
|
330
|
+
this.level = level;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
new Logger();
|
|
334
|
+
|
|
335
|
+
// src/types/http/status.ts
|
|
336
|
+
var HttpStatus = {
|
|
337
|
+
OK: 200,
|
|
338
|
+
CREATED: 201,
|
|
339
|
+
ACCEPTED: 202,
|
|
340
|
+
NO_CONTENT: 204,
|
|
341
|
+
MOVED_PERMANENTLY: 301,
|
|
342
|
+
FOUND: 302,
|
|
343
|
+
NOT_MODIFIED: 304,
|
|
344
|
+
BAD_REQUEST: 400,
|
|
345
|
+
UNAUTHORIZED: 401,
|
|
346
|
+
FORBIDDEN: 403,
|
|
347
|
+
NOT_FOUND: 404,
|
|
348
|
+
METHOD_NOT_ALLOWED: 405,
|
|
349
|
+
CONFLICT: 409,
|
|
350
|
+
GONE: 410,
|
|
351
|
+
UNPROCESSABLE_ENTITY: 422,
|
|
352
|
+
TOO_MANY_REQUESTS: 429,
|
|
353
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
354
|
+
NOT_IMPLEMENTED: 501,
|
|
355
|
+
BAD_GATEWAY: 502,
|
|
356
|
+
SERVICE_UNAVAILABLE: 503,
|
|
357
|
+
GATEWAY_TIMEOUT: 504
|
|
358
|
+
};
|
|
359
|
+
var HttpStatusText = {
|
|
360
|
+
200: "OK",
|
|
361
|
+
201: "Created",
|
|
362
|
+
202: "Accepted",
|
|
363
|
+
204: "No Content",
|
|
364
|
+
301: "Moved Permanently",
|
|
365
|
+
302: "Found",
|
|
366
|
+
304: "Not Modified",
|
|
367
|
+
400: "Bad Request",
|
|
368
|
+
401: "Unauthorized",
|
|
369
|
+
403: "Forbidden",
|
|
370
|
+
404: "Not Found",
|
|
371
|
+
405: "Method Not Allowed",
|
|
372
|
+
409: "Conflict",
|
|
373
|
+
410: "Gone",
|
|
374
|
+
422: "Unprocessable Entity",
|
|
375
|
+
429: "Too Many Requests",
|
|
376
|
+
500: "Internal Server Error",
|
|
377
|
+
501: "Not Implemented",
|
|
378
|
+
502: "Bad Gateway",
|
|
379
|
+
503: "Service Unavailable",
|
|
380
|
+
504: "Gateway Timeout"
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/utils/HttpError.ts
|
|
384
|
+
var HttpError = class _HttpError extends Error {
|
|
385
|
+
statusCode;
|
|
386
|
+
details;
|
|
387
|
+
isOperational;
|
|
388
|
+
constructor(statusCode, message, details) {
|
|
389
|
+
super(message ?? HttpStatusText[statusCode] ?? "Unknown Error");
|
|
390
|
+
this.statusCode = statusCode;
|
|
391
|
+
this.details = details;
|
|
392
|
+
this.isOperational = true;
|
|
393
|
+
Object.setPrototypeOf(this, _HttpError.prototype);
|
|
394
|
+
}
|
|
395
|
+
toJSON() {
|
|
396
|
+
const obj = {
|
|
397
|
+
error: {
|
|
398
|
+
statusCode: this.statusCode,
|
|
399
|
+
message: this.message
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
if (this.details) obj.error.details = this.details;
|
|
403
|
+
return obj;
|
|
404
|
+
}
|
|
405
|
+
static badRequest(message, details) {
|
|
406
|
+
return new _HttpError(400, message ?? "Bad Request", details);
|
|
407
|
+
}
|
|
408
|
+
static unauthorized(message) {
|
|
409
|
+
return new _HttpError(401, message ?? "Unauthorized");
|
|
410
|
+
}
|
|
411
|
+
static forbidden(message) {
|
|
412
|
+
return new _HttpError(403, message ?? "Forbidden");
|
|
413
|
+
}
|
|
414
|
+
static notFound(message) {
|
|
415
|
+
return new _HttpError(404, message ?? "Not Found");
|
|
416
|
+
}
|
|
417
|
+
static methodNotAllowed(message) {
|
|
418
|
+
return new _HttpError(405, message ?? "Method Not Allowed");
|
|
419
|
+
}
|
|
420
|
+
static conflict(message, details) {
|
|
421
|
+
return new _HttpError(409, message ?? "Conflict", details);
|
|
422
|
+
}
|
|
423
|
+
static unprocessableEntity(message, details) {
|
|
424
|
+
return new _HttpError(422, message ?? "Unprocessable Entity", details);
|
|
425
|
+
}
|
|
426
|
+
static tooManyRequests(message) {
|
|
427
|
+
return new _HttpError(429, message ?? "Too Many Requests");
|
|
428
|
+
}
|
|
429
|
+
static internal(message) {
|
|
430
|
+
return new _HttpError(500, message ?? "Internal Server Error");
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// src/utils/Parser.ts
|
|
435
|
+
function parseQueryString(qs) {
|
|
436
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
437
|
+
if (!qs || qs.length === 0) return result;
|
|
438
|
+
let key = "";
|
|
439
|
+
let value = "";
|
|
440
|
+
let startingKey = true;
|
|
441
|
+
let i = qs.charCodeAt(0) === 63 ? 1 : 0;
|
|
442
|
+
for (; i < qs.length; i++) {
|
|
443
|
+
const ch = qs.charCodeAt(i);
|
|
444
|
+
if (ch === 61 && startingKey) {
|
|
445
|
+
startingKey = false;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (ch === 38) {
|
|
449
|
+
if (key.length > 0) {
|
|
450
|
+
const decodedKey = fastDecode(key);
|
|
451
|
+
const decodedVal = fastDecode(value);
|
|
452
|
+
const existing = result[decodedKey];
|
|
453
|
+
if (existing === void 0) {
|
|
454
|
+
result[decodedKey] = decodedVal;
|
|
455
|
+
} else if (typeof existing === "string") {
|
|
456
|
+
result[decodedKey] = [existing, decodedVal];
|
|
457
|
+
} else {
|
|
458
|
+
existing.push(decodedVal);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
key = "";
|
|
462
|
+
value = "";
|
|
463
|
+
startingKey = true;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (startingKey) {
|
|
467
|
+
key += qs[i];
|
|
468
|
+
} else {
|
|
469
|
+
value += qs[i];
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (key.length > 0) {
|
|
473
|
+
const decodedKey = fastDecode(key);
|
|
474
|
+
const decodedVal = fastDecode(value);
|
|
475
|
+
const existing = result[decodedKey];
|
|
476
|
+
if (existing === void 0) {
|
|
477
|
+
result[decodedKey] = decodedVal;
|
|
478
|
+
} else if (typeof existing === "string") {
|
|
479
|
+
result[decodedKey] = [existing, decodedVal];
|
|
480
|
+
} else {
|
|
481
|
+
existing.push(decodedVal);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
function fastDecode(str) {
|
|
487
|
+
if (str.indexOf("%") === -1 && str.indexOf("+") === -1) return str;
|
|
488
|
+
try {
|
|
489
|
+
return decodeURIComponent(str.replace(/\+/g, " "));
|
|
490
|
+
} catch {
|
|
491
|
+
return str;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function parseCookies(header) {
|
|
495
|
+
const cookies = /* @__PURE__ */ Object.create(null);
|
|
496
|
+
if (!header) return cookies;
|
|
497
|
+
let i = 0;
|
|
498
|
+
const len = header.length;
|
|
499
|
+
while (i < len) {
|
|
500
|
+
while (i < len && header.charCodeAt(i) === 32) i++;
|
|
501
|
+
let eqIdx = -1;
|
|
502
|
+
let semiIdx = -1;
|
|
503
|
+
for (let j = i; j < len; j++) {
|
|
504
|
+
const ch = header.charCodeAt(j);
|
|
505
|
+
if (ch === 61 && eqIdx === -1) eqIdx = j;
|
|
506
|
+
if (ch === 59) {
|
|
507
|
+
semiIdx = j;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (eqIdx === -1) {
|
|
512
|
+
i = semiIdx === -1 ? len : semiIdx + 1;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const end = semiIdx === -1 ? len : semiIdx;
|
|
516
|
+
const name = header.slice(i, eqIdx).trim();
|
|
517
|
+
let val = header.slice(eqIdx + 1, end).trim();
|
|
518
|
+
if (val.charCodeAt(0) === 34 && val.charCodeAt(val.length - 1) === 34) {
|
|
519
|
+
val = val.slice(1, -1);
|
|
520
|
+
}
|
|
521
|
+
if (cookies[name] === void 0) {
|
|
522
|
+
cookies[name] = fastDecode(val);
|
|
523
|
+
}
|
|
524
|
+
i = end + 1;
|
|
525
|
+
}
|
|
526
|
+
return cookies;
|
|
527
|
+
}
|
|
528
|
+
var CONTENT_TYPE_JSON = "application/json";
|
|
529
|
+
var CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
|
|
530
|
+
async function parseBody(req) {
|
|
531
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
532
|
+
const contentLength = req.headers["content-length"];
|
|
533
|
+
if (req.method === "GET" || req.method === "HEAD" || req.method === "OPTIONS" || contentLength !== void 0 && contentLength === "0") {
|
|
534
|
+
return void 0;
|
|
535
|
+
}
|
|
536
|
+
const chunks = [];
|
|
537
|
+
let totalSize = 0;
|
|
538
|
+
const maxSize = 10 * 1024 * 1024;
|
|
539
|
+
return new Promise((resolve3, reject) => {
|
|
540
|
+
req.on("data", (chunk) => {
|
|
541
|
+
totalSize += chunk.length;
|
|
542
|
+
if (totalSize > maxSize) {
|
|
543
|
+
req.destroy();
|
|
544
|
+
reject(new Error("Request body too large"));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
chunks.push(chunk);
|
|
548
|
+
});
|
|
549
|
+
req.on("end", () => {
|
|
550
|
+
if (chunks.length === 0) {
|
|
551
|
+
resolve3(void 0);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
555
|
+
if (contentType.startsWith(CONTENT_TYPE_JSON)) {
|
|
556
|
+
try {
|
|
557
|
+
resolve3(JSON.parse(raw));
|
|
558
|
+
} catch {
|
|
559
|
+
resolve3(raw);
|
|
560
|
+
}
|
|
561
|
+
} else if (contentType.startsWith(CONTENT_TYPE_FORM)) {
|
|
562
|
+
resolve3(parseQueryString(raw));
|
|
563
|
+
} else {
|
|
564
|
+
resolve3(raw);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
req.on("error", reject);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
function parseUrl(url) {
|
|
571
|
+
let pathname = "";
|
|
572
|
+
let search = "";
|
|
573
|
+
let i = 0;
|
|
574
|
+
for (; i < url.length; i++) {
|
|
575
|
+
if (url.charCodeAt(i) === 63) {
|
|
576
|
+
pathname = url.slice(0, i);
|
|
577
|
+
search = url.slice(i);
|
|
578
|
+
return { pathname, search };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return { pathname: url, search: "" };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/utils/IpResolver.ts
|
|
585
|
+
var PROXY_HEADERS = [
|
|
586
|
+
"x-forwarded-for",
|
|
587
|
+
"x-real-ip",
|
|
588
|
+
"cf-connecting-ip",
|
|
589
|
+
"x-client-ip",
|
|
590
|
+
"x-cluster-client-ip",
|
|
591
|
+
"fastly-client-ip",
|
|
592
|
+
"true-client-ip"
|
|
593
|
+
];
|
|
594
|
+
function resolveIp(req) {
|
|
595
|
+
for (const header of PROXY_HEADERS) {
|
|
596
|
+
const val = req.headers[header];
|
|
597
|
+
if (val) {
|
|
598
|
+
const ip = typeof val === "string" ? val.split(",")[0].trim() : val[0];
|
|
599
|
+
if (ip && ip.length > 0) return normalizeIp(ip);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const remoteAddr = req.socket?.remoteAddress ?? "127.0.0.1";
|
|
603
|
+
return normalizeIp(remoteAddr);
|
|
604
|
+
}
|
|
605
|
+
function normalizeIp(ip) {
|
|
606
|
+
if (ip === "::1" || ip === "::ffff:127.0.0.1") return "127.0.0.1";
|
|
607
|
+
if (ip.startsWith("::ffff:")) return ip.slice(7);
|
|
608
|
+
return ip;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// src/utils/cookies/CookieManager.ts
|
|
612
|
+
function serializeCookie(name, value, options = {}) {
|
|
613
|
+
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
|
|
614
|
+
if (options.maxAge != null) {
|
|
615
|
+
cookie += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
616
|
+
}
|
|
617
|
+
if (options.expires) {
|
|
618
|
+
cookie += `; Expires=${options.expires.toUTCString()}`;
|
|
619
|
+
}
|
|
620
|
+
if (options.domain) {
|
|
621
|
+
cookie += `; Domain=${options.domain}`;
|
|
622
|
+
}
|
|
623
|
+
cookie += `; Path=${options.path ?? "/"}`;
|
|
624
|
+
if (options.secure) {
|
|
625
|
+
cookie += "; Secure";
|
|
626
|
+
}
|
|
627
|
+
if (options.httpOnly !== false) {
|
|
628
|
+
cookie += "; HttpOnly";
|
|
629
|
+
}
|
|
630
|
+
if (options.sameSite) {
|
|
631
|
+
cookie += `; SameSite=${options.sameSite}`;
|
|
632
|
+
}
|
|
633
|
+
return cookie;
|
|
634
|
+
}
|
|
635
|
+
function clearCookieHeader(name, options = {}) {
|
|
636
|
+
return serializeCookie(name, "", {
|
|
637
|
+
...options,
|
|
638
|
+
maxAge: 0,
|
|
639
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/core/server.ts
|
|
644
|
+
var CONTROLLER_META_KEY = "azura:controller";
|
|
645
|
+
var ROUTES_META_KEY = "azura:routes";
|
|
646
|
+
var PARAMS_META_KEY = "azura:params";
|
|
647
|
+
var AzuraServer = class {
|
|
648
|
+
server = null;
|
|
649
|
+
router;
|
|
650
|
+
logger;
|
|
651
|
+
middlewares = [];
|
|
652
|
+
plugins = [];
|
|
653
|
+
errorHandler = null;
|
|
654
|
+
config;
|
|
655
|
+
compiledChain = null;
|
|
656
|
+
constructor(config = {}) {
|
|
657
|
+
this.config = {
|
|
658
|
+
environment: config.environment ?? process.env.NODE_ENV ?? "development",
|
|
659
|
+
server: {
|
|
660
|
+
port: config.server?.port ?? 3e3,
|
|
661
|
+
host: config.server?.host ?? "0.0.0.0",
|
|
662
|
+
keepAliveTimeout: config.server?.keepAliveTimeout ?? 72e3,
|
|
663
|
+
headersTimeout: config.server?.headersTimeout ?? 75e3,
|
|
664
|
+
requestTimeout: config.server?.requestTimeout ?? 3e4,
|
|
665
|
+
...config.server
|
|
666
|
+
},
|
|
667
|
+
logging: {
|
|
668
|
+
enabled: config.logging?.enabled ?? true,
|
|
669
|
+
level: config.logging?.level ?? "info",
|
|
670
|
+
timestamp: config.logging?.timestamp ?? true,
|
|
671
|
+
colors: config.logging?.colors ?? true,
|
|
672
|
+
requestLogging: config.logging?.requestLogging ?? true,
|
|
673
|
+
...config.logging
|
|
674
|
+
},
|
|
675
|
+
plugins: config.plugins ?? {},
|
|
676
|
+
debug: config.debug ?? false
|
|
677
|
+
};
|
|
678
|
+
this.router = new Router();
|
|
679
|
+
this.logger = new Logger({
|
|
680
|
+
level: this.config.logging?.enabled === false ? "silent" : this.config.logging?.level,
|
|
681
|
+
colors: this.config.logging?.colors,
|
|
682
|
+
timestamp: this.config.logging?.timestamp
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
use(handler) {
|
|
686
|
+
this.compiledChain = null;
|
|
687
|
+
if (handler.length <= 2) {
|
|
688
|
+
this.plugins.push(handler);
|
|
689
|
+
} else {
|
|
690
|
+
this.middlewares.push(handler);
|
|
691
|
+
}
|
|
692
|
+
return this;
|
|
693
|
+
}
|
|
694
|
+
plugin(handler) {
|
|
695
|
+
this.compiledChain = null;
|
|
696
|
+
this.plugins.push(handler);
|
|
697
|
+
return this;
|
|
698
|
+
}
|
|
699
|
+
onError(handler) {
|
|
700
|
+
this.errorHandler = handler;
|
|
701
|
+
return this;
|
|
702
|
+
}
|
|
703
|
+
get(path, ...handlers) {
|
|
704
|
+
return this.route("GET", path, handlers);
|
|
705
|
+
}
|
|
706
|
+
post(path, ...handlers) {
|
|
707
|
+
return this.route("POST", path, handlers);
|
|
708
|
+
}
|
|
709
|
+
put(path, ...handlers) {
|
|
710
|
+
return this.route("PUT", path, handlers);
|
|
711
|
+
}
|
|
712
|
+
delete(path, ...handlers) {
|
|
713
|
+
return this.route("DELETE", path, handlers);
|
|
714
|
+
}
|
|
715
|
+
patch(path, ...handlers) {
|
|
716
|
+
return this.route("PATCH", path, handlers);
|
|
717
|
+
}
|
|
718
|
+
head(path, ...handlers) {
|
|
719
|
+
return this.route("HEAD", path, handlers);
|
|
720
|
+
}
|
|
721
|
+
options(path, ...handlers) {
|
|
722
|
+
return this.route("OPTIONS", path, handlers);
|
|
723
|
+
}
|
|
724
|
+
all(path, ...handlers) {
|
|
725
|
+
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
726
|
+
for (const method of methods) {
|
|
727
|
+
this.route(method, path, handlers);
|
|
728
|
+
}
|
|
729
|
+
return this;
|
|
730
|
+
}
|
|
731
|
+
route(method, path, handlers) {
|
|
732
|
+
const routeHandler = handlers[handlers.length - 1];
|
|
733
|
+
const middlewares = handlers.slice(0, -1);
|
|
734
|
+
this.router.add(method, path, routeHandler, middlewares);
|
|
735
|
+
return this;
|
|
736
|
+
}
|
|
737
|
+
register(...controllers) {
|
|
738
|
+
for (const Controller2 of controllers) {
|
|
739
|
+
this.registerController(Controller2);
|
|
740
|
+
}
|
|
741
|
+
return this;
|
|
742
|
+
}
|
|
743
|
+
registerController(Controller2) {
|
|
744
|
+
const instance = new Controller2();
|
|
745
|
+
const prefix = Reflect.getMetadata?.(CONTROLLER_META_KEY, Controller2) ?? "";
|
|
746
|
+
const controllerMiddlewares = Reflect.getMetadata?.("azura:middlewares", Controller2) ?? [];
|
|
747
|
+
const prototype = Controller2.prototype;
|
|
748
|
+
const propertyNames = Object.getOwnPropertyNames(prototype).filter(
|
|
749
|
+
(p) => p !== "constructor"
|
|
750
|
+
);
|
|
751
|
+
for (const propertyKey of propertyNames) {
|
|
752
|
+
const routeMeta = Reflect.getMetadata?.(
|
|
753
|
+
ROUTES_META_KEY,
|
|
754
|
+
prototype,
|
|
755
|
+
propertyKey
|
|
756
|
+
);
|
|
757
|
+
if (!routeMeta) continue;
|
|
758
|
+
const fullPath = this.joinPaths(prefix, routeMeta.path);
|
|
759
|
+
const paramsMeta = Reflect.getMetadata?.(PARAMS_META_KEY, prototype, propertyKey) ?? [];
|
|
760
|
+
const routeMiddlewares = Reflect.getMetadata?.("azura:middlewares", prototype, propertyKey) ?? [];
|
|
761
|
+
const allMiddlewares = [...controllerMiddlewares, ...routeMiddlewares];
|
|
762
|
+
const handler = async (req, res) => {
|
|
763
|
+
const args = this.resolveParams(paramsMeta, req, res);
|
|
764
|
+
const result = await instance[propertyKey](...args);
|
|
765
|
+
if (result !== void 0 && !res.sent) {
|
|
766
|
+
if (typeof result === "object") {
|
|
767
|
+
res.json(result);
|
|
768
|
+
} else {
|
|
769
|
+
res.send(String(result));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
this.router.add(routeMeta.method, fullPath, handler, allMiddlewares, routeMeta.meta);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
resolveParams(paramsMeta, req, res) {
|
|
777
|
+
if (!paramsMeta || paramsMeta.length === 0) return [req, res];
|
|
778
|
+
const sorted = [...paramsMeta].sort((a, b) => a.index - b.index);
|
|
779
|
+
const args = new Array(sorted.length);
|
|
780
|
+
for (const meta of sorted) {
|
|
781
|
+
let value;
|
|
782
|
+
switch (meta.type) {
|
|
783
|
+
case "body":
|
|
784
|
+
value = meta.name ? req.body?.[meta.name] : req.body;
|
|
785
|
+
break;
|
|
786
|
+
case "query":
|
|
787
|
+
value = meta.name ? req.query[meta.name] : req.query;
|
|
788
|
+
break;
|
|
789
|
+
case "param":
|
|
790
|
+
value = meta.name ? req.params[meta.name] : req.params;
|
|
791
|
+
break;
|
|
792
|
+
case "header":
|
|
793
|
+
value = meta.name ? req.headers[meta.name.toLowerCase()] : req.headers;
|
|
794
|
+
break;
|
|
795
|
+
case "cookie":
|
|
796
|
+
value = meta.name ? req.cookies[meta.name] : req.cookies;
|
|
797
|
+
break;
|
|
798
|
+
case "req":
|
|
799
|
+
value = req;
|
|
800
|
+
break;
|
|
801
|
+
case "res":
|
|
802
|
+
value = res;
|
|
803
|
+
break;
|
|
804
|
+
case "next":
|
|
805
|
+
value = () => {
|
|
806
|
+
};
|
|
807
|
+
break;
|
|
808
|
+
case "ip":
|
|
809
|
+
value = req.ip;
|
|
810
|
+
break;
|
|
811
|
+
case "session":
|
|
812
|
+
value = req.session;
|
|
813
|
+
break;
|
|
814
|
+
default:
|
|
815
|
+
value = void 0;
|
|
816
|
+
}
|
|
817
|
+
if (meta.pipes) {
|
|
818
|
+
for (const pipe of meta.pipes) {
|
|
819
|
+
value = pipe(value);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
args[meta.index] = value;
|
|
823
|
+
}
|
|
824
|
+
return args;
|
|
825
|
+
}
|
|
826
|
+
joinPaths(prefix, path) {
|
|
827
|
+
const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
828
|
+
const s = path.startsWith("/") ? path : `/${path}`;
|
|
829
|
+
return `${p}${s}`;
|
|
830
|
+
}
|
|
831
|
+
extendRequest(req, pathname, query) {
|
|
832
|
+
const azReq = req;
|
|
833
|
+
azReq.pathname = pathname;
|
|
834
|
+
azReq.query = query;
|
|
835
|
+
azReq.params = {};
|
|
836
|
+
azReq.cookies = parseCookies(req.headers.cookie);
|
|
837
|
+
azReq.ip = resolveIp(req);
|
|
838
|
+
azReq.protocol = req.socket.encrypted ? "https" : "http";
|
|
839
|
+
azReq.secure = azReq.protocol === "https";
|
|
840
|
+
azReq.hostname = (req.headers.host ?? "localhost").split(":")[0];
|
|
841
|
+
azReq.startTime = process.hrtime.bigint();
|
|
842
|
+
azReq.raw = req;
|
|
843
|
+
return azReq;
|
|
844
|
+
}
|
|
845
|
+
extendResponse(res) {
|
|
846
|
+
const azRes = res;
|
|
847
|
+
azRes.locals = {};
|
|
848
|
+
azRes.sent = false;
|
|
849
|
+
azRes.status = function(code) {
|
|
850
|
+
this.statusCode = code;
|
|
851
|
+
return this;
|
|
852
|
+
};
|
|
853
|
+
azRes.json = function(data) {
|
|
854
|
+
if (this.sent) return;
|
|
855
|
+
this.sent = true;
|
|
856
|
+
const body = JSON.stringify(data);
|
|
857
|
+
this.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
858
|
+
this.setHeader("Content-Length", Buffer.byteLength(body));
|
|
859
|
+
this.end(body);
|
|
860
|
+
};
|
|
861
|
+
azRes.send = function(body) {
|
|
862
|
+
if (this.sent) return;
|
|
863
|
+
this.sent = true;
|
|
864
|
+
if (!this.getHeader("Content-Type")) {
|
|
865
|
+
this.setHeader("Content-Type", typeof body === "string" ? "text/plain; charset=utf-8" : "application/octet-stream");
|
|
866
|
+
}
|
|
867
|
+
const buf = typeof body === "string" ? Buffer.from(body) : body;
|
|
868
|
+
this.setHeader("Content-Length", buf.length);
|
|
869
|
+
this.end(buf);
|
|
870
|
+
};
|
|
871
|
+
azRes.html = function(body) {
|
|
872
|
+
if (this.sent) return;
|
|
873
|
+
this.sent = true;
|
|
874
|
+
this.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
875
|
+
this.setHeader("Content-Length", Buffer.byteLength(body));
|
|
876
|
+
this.end(body);
|
|
877
|
+
};
|
|
878
|
+
azRes.redirect = function(url, code = 302) {
|
|
879
|
+
if (this.sent) return;
|
|
880
|
+
this.sent = true;
|
|
881
|
+
this.statusCode = code;
|
|
882
|
+
this.setHeader("Location", url);
|
|
883
|
+
this.end();
|
|
884
|
+
};
|
|
885
|
+
azRes.cookie = function(name, value, options) {
|
|
886
|
+
const header = serializeCookie(name, value, options);
|
|
887
|
+
const existing = this.getHeader("Set-Cookie");
|
|
888
|
+
if (existing) {
|
|
889
|
+
const arr = Array.isArray(existing) ? existing : [String(existing)];
|
|
890
|
+
arr.push(header);
|
|
891
|
+
this.setHeader("Set-Cookie", arr);
|
|
892
|
+
} else {
|
|
893
|
+
this.setHeader("Set-Cookie", header);
|
|
894
|
+
}
|
|
895
|
+
return this;
|
|
896
|
+
};
|
|
897
|
+
azRes.clearCookie = function(name, options) {
|
|
898
|
+
const header = clearCookieHeader(name, options);
|
|
899
|
+
const existing = this.getHeader("Set-Cookie");
|
|
900
|
+
if (existing) {
|
|
901
|
+
const arr = Array.isArray(existing) ? existing : [String(existing)];
|
|
902
|
+
arr.push(header);
|
|
903
|
+
this.setHeader("Set-Cookie", arr);
|
|
904
|
+
} else {
|
|
905
|
+
this.setHeader("Set-Cookie", header);
|
|
906
|
+
}
|
|
907
|
+
return this;
|
|
908
|
+
};
|
|
909
|
+
azRes.header = function(name, value) {
|
|
910
|
+
this.setHeader(name, value);
|
|
911
|
+
return this;
|
|
912
|
+
};
|
|
913
|
+
azRes.type = function(contentType) {
|
|
914
|
+
this.setHeader("Content-Type", contentType);
|
|
915
|
+
return this;
|
|
916
|
+
};
|
|
917
|
+
azRes.attachment = function(filename) {
|
|
918
|
+
if (filename) {
|
|
919
|
+
this.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
|
920
|
+
} else {
|
|
921
|
+
this.setHeader("Content-Disposition", "attachment");
|
|
922
|
+
}
|
|
923
|
+
return this;
|
|
924
|
+
};
|
|
925
|
+
azRes.download = function(_path, _filename) {
|
|
926
|
+
};
|
|
927
|
+
return azRes;
|
|
928
|
+
}
|
|
929
|
+
compileMiddlewareChain() {
|
|
930
|
+
if (this.compiledChain) return this.compiledChain;
|
|
931
|
+
const self = this;
|
|
932
|
+
const globalMiddlewares = [...this.middlewares];
|
|
933
|
+
this.compiledChain = async (req, res) => {
|
|
934
|
+
for (const plugin of self.plugins) {
|
|
935
|
+
if (res.sent) return;
|
|
936
|
+
await new Promise((resolve3, reject) => {
|
|
937
|
+
try {
|
|
938
|
+
const result = plugin({ req, res }, (err) => {
|
|
939
|
+
if (err) reject(err);
|
|
940
|
+
else resolve3();
|
|
941
|
+
});
|
|
942
|
+
if (result && typeof result.then === "function") {
|
|
943
|
+
result.then(resolve3, reject);
|
|
944
|
+
}
|
|
945
|
+
} catch (e) {
|
|
946
|
+
reject(e);
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
if (res.sent) return;
|
|
951
|
+
let idx = 0;
|
|
952
|
+
const runMiddleware = async () => {
|
|
953
|
+
if (idx >= globalMiddlewares.length || res.sent) return;
|
|
954
|
+
const mw = globalMiddlewares[idx++];
|
|
955
|
+
await new Promise((resolve3, reject) => {
|
|
956
|
+
try {
|
|
957
|
+
const result = mw(req, res, (err) => {
|
|
958
|
+
if (err) reject(err);
|
|
959
|
+
else resolve3();
|
|
960
|
+
});
|
|
961
|
+
if (result && typeof result.then === "function") {
|
|
962
|
+
result.then(resolve3, reject);
|
|
963
|
+
}
|
|
964
|
+
} catch (e) {
|
|
965
|
+
reject(e);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
await runMiddleware();
|
|
969
|
+
};
|
|
970
|
+
await runMiddleware();
|
|
971
|
+
};
|
|
972
|
+
return this.compiledChain;
|
|
973
|
+
}
|
|
974
|
+
async handleRequest(req, res) {
|
|
975
|
+
const { pathname, search } = parseUrl(req.url ?? "/");
|
|
976
|
+
const query = parseQueryString(search);
|
|
977
|
+
const azReq = this.extendRequest(req, pathname, query);
|
|
978
|
+
const azRes = this.extendResponse(res);
|
|
979
|
+
try {
|
|
980
|
+
const chain = this.compileMiddlewareChain();
|
|
981
|
+
await chain(azReq, azRes);
|
|
982
|
+
if (azRes.sent) return;
|
|
983
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
984
|
+
const match = this.router.find(method, pathname);
|
|
985
|
+
if (!match) {
|
|
986
|
+
if (pathname === "/favicon.ico") {
|
|
987
|
+
azRes.status(204).send("");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
throw HttpError.notFound(`Route ${method} ${pathname} not found`);
|
|
991
|
+
}
|
|
992
|
+
azReq.params = match.params;
|
|
993
|
+
if (method !== "GET" && method !== "HEAD" && method !== "OPTIONS") {
|
|
994
|
+
azReq.body = await parseBody(req);
|
|
995
|
+
}
|
|
996
|
+
for (const mw of match.middlewares) {
|
|
997
|
+
if (azRes.sent) return;
|
|
998
|
+
await new Promise((resolve3, reject) => {
|
|
999
|
+
try {
|
|
1000
|
+
const result2 = mw(azReq, azRes, (err) => {
|
|
1001
|
+
if (err) reject(err);
|
|
1002
|
+
else resolve3();
|
|
1003
|
+
});
|
|
1004
|
+
if (result2 && typeof result2.then === "function") {
|
|
1005
|
+
result2.then(resolve3, reject);
|
|
1006
|
+
}
|
|
1007
|
+
} catch (e) {
|
|
1008
|
+
reject(e);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
if (azRes.sent) return;
|
|
1013
|
+
const result = await match.handler(azReq, azRes);
|
|
1014
|
+
if (result !== void 0 && !azRes.sent) {
|
|
1015
|
+
if (typeof result === "object" && result !== null) {
|
|
1016
|
+
azRes.json(result);
|
|
1017
|
+
} else {
|
|
1018
|
+
azRes.send(String(result));
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} catch (err) {
|
|
1022
|
+
this.handleError(err, azReq, azRes);
|
|
1023
|
+
} finally {
|
|
1024
|
+
if (this.config.logging?.requestLogging) {
|
|
1025
|
+
const duration = process.hrtime.bigint() - azReq.startTime;
|
|
1026
|
+
this.logger.request(
|
|
1027
|
+
req.method ?? "GET",
|
|
1028
|
+
pathname,
|
|
1029
|
+
res.statusCode,
|
|
1030
|
+
duration
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
handleError(err, req, res) {
|
|
1036
|
+
if (res.sent) return;
|
|
1037
|
+
if (this.errorHandler) {
|
|
1038
|
+
try {
|
|
1039
|
+
this.errorHandler(err, req, res, () => {
|
|
1040
|
+
});
|
|
1041
|
+
return;
|
|
1042
|
+
} catch (handlerErr) {
|
|
1043
|
+
this.logger.error("Error handler threw:", handlerErr);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
if (err instanceof HttpError) {
|
|
1047
|
+
res.status(err.statusCode).json(err.toJSON());
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const statusCode = err?.statusCode ?? err?.status ?? 500;
|
|
1051
|
+
const message = this.config.environment === "production" ? "Internal Server Error" : err?.message ?? "Internal Server Error";
|
|
1052
|
+
this.logger.error(`Unhandled error: ${err?.message ?? err}`);
|
|
1053
|
+
if (this.config.debug && err?.stack) {
|
|
1054
|
+
this.logger.error(err.stack);
|
|
1055
|
+
}
|
|
1056
|
+
res.status(statusCode).json({
|
|
1057
|
+
error: {
|
|
1058
|
+
statusCode,
|
|
1059
|
+
message,
|
|
1060
|
+
...this.config.debug ? { stack: err?.stack } : {}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
async listen(port, callback) {
|
|
1065
|
+
const serverPort = port ?? this.config.server?.port ?? 3e3;
|
|
1066
|
+
const host = this.config.server?.host ?? "0.0.0.0";
|
|
1067
|
+
const handler = (req, res) => {
|
|
1068
|
+
this.handleRequest(req, res).catch((err) => {
|
|
1069
|
+
this.logger.error("Fatal request error:", err);
|
|
1070
|
+
if (!res.headersSent) {
|
|
1071
|
+
res.statusCode = 500;
|
|
1072
|
+
res.end('{"error":{"statusCode":500,"message":"Internal Server Error"}}');
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
};
|
|
1076
|
+
if (this.config.server?.https) {
|
|
1077
|
+
const httpsConfig = this.config.server.https;
|
|
1078
|
+
this.server = createServer(
|
|
1079
|
+
{
|
|
1080
|
+
key: readFileSync(httpsConfig.key),
|
|
1081
|
+
cert: readFileSync(httpsConfig.cert),
|
|
1082
|
+
...httpsConfig.ca ? { ca: readFileSync(httpsConfig.ca) } : {}
|
|
1083
|
+
},
|
|
1084
|
+
handler
|
|
1085
|
+
);
|
|
1086
|
+
} else {
|
|
1087
|
+
this.server = createServer$1(handler);
|
|
1088
|
+
}
|
|
1089
|
+
if (this.config.server?.keepAliveTimeout) {
|
|
1090
|
+
this.server.keepAliveTimeout = this.config.server.keepAliveTimeout;
|
|
1091
|
+
}
|
|
1092
|
+
if (this.config.server?.headersTimeout) {
|
|
1093
|
+
this.server.headersTimeout = this.config.server.headersTimeout;
|
|
1094
|
+
}
|
|
1095
|
+
return new Promise((resolve3) => {
|
|
1096
|
+
this.server.listen(serverPort, host, () => {
|
|
1097
|
+
this.logger.banner(serverPort, host);
|
|
1098
|
+
callback?.();
|
|
1099
|
+
resolve3(this.server);
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
async close() {
|
|
1104
|
+
return new Promise((resolve3, reject) => {
|
|
1105
|
+
if (!this.server) {
|
|
1106
|
+
resolve3();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
this.server.close((err) => {
|
|
1110
|
+
if (err) reject(err);
|
|
1111
|
+
else resolve3();
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
getRouter() {
|
|
1116
|
+
return this.router;
|
|
1117
|
+
}
|
|
1118
|
+
getLogger() {
|
|
1119
|
+
return this.logger;
|
|
1120
|
+
}
|
|
1121
|
+
getConfig() {
|
|
1122
|
+
return this.config;
|
|
1123
|
+
}
|
|
1124
|
+
getRoutes() {
|
|
1125
|
+
return this.router.getAllRoutes();
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// src/decorators/Route.ts
|
|
1130
|
+
var CONTROLLER_META_KEY2 = "azura:controller";
|
|
1131
|
+
var ROUTES_META_KEY2 = "azura:routes";
|
|
1132
|
+
var PARAMS_META_KEY2 = "azura:params";
|
|
1133
|
+
var MIDDLEWARES_META_KEY = "azura:middlewares";
|
|
1134
|
+
function ensureReflect() {
|
|
1135
|
+
if (typeof Reflect === "undefined" || !Reflect.getMetadata) {
|
|
1136
|
+
const store = /* @__PURE__ */ new Map();
|
|
1137
|
+
Reflect.defineMetadata = (key, value, target, prop) => {
|
|
1138
|
+
const k = prop ? `${key}::${prop}` : key;
|
|
1139
|
+
if (!store.has(k)) store.set(k, /* @__PURE__ */ new Map());
|
|
1140
|
+
store.get(k).set(target, value);
|
|
1141
|
+
};
|
|
1142
|
+
Reflect.getMetadata = (key, target, prop) => {
|
|
1143
|
+
const k = prop ? `${key}::${prop}` : key;
|
|
1144
|
+
return store.get(k)?.get(target);
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
ensureReflect();
|
|
1149
|
+
function Controller(prefix = "") {
|
|
1150
|
+
return (target) => {
|
|
1151
|
+
Reflect.defineMetadata(CONTROLLER_META_KEY2, prefix, target);
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
function createMethodDecorator(method) {
|
|
1155
|
+
return (path = "") => {
|
|
1156
|
+
return (target, propertyKey, _descriptor) => {
|
|
1157
|
+
Reflect.defineMetadata(
|
|
1158
|
+
ROUTES_META_KEY2,
|
|
1159
|
+
{ method, path, propertyKey: String(propertyKey), middlewares: [], params: [], meta: void 0 },
|
|
1160
|
+
target,
|
|
1161
|
+
String(propertyKey)
|
|
1162
|
+
);
|
|
1163
|
+
};
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
var Get = createMethodDecorator("GET");
|
|
1167
|
+
var Post = createMethodDecorator("POST");
|
|
1168
|
+
var Put = createMethodDecorator("PUT");
|
|
1169
|
+
var Delete = createMethodDecorator("DELETE");
|
|
1170
|
+
var Patch = createMethodDecorator("PATCH");
|
|
1171
|
+
var Head = createMethodDecorator("HEAD");
|
|
1172
|
+
var Options = createMethodDecorator("OPTIONS");
|
|
1173
|
+
function createParamDecorator(type) {
|
|
1174
|
+
return (name) => {
|
|
1175
|
+
return (target, propertyKey, parameterIndex) => {
|
|
1176
|
+
const key = String(propertyKey);
|
|
1177
|
+
const existing = Reflect.getMetadata(PARAMS_META_KEY2, target, key) ?? [];
|
|
1178
|
+
existing.push({ index: parameterIndex, type, name });
|
|
1179
|
+
Reflect.defineMetadata(PARAMS_META_KEY2, existing, target, key);
|
|
1180
|
+
};
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
var Body = createParamDecorator("body");
|
|
1184
|
+
var Query = createParamDecorator("query");
|
|
1185
|
+
var Param = createParamDecorator("param");
|
|
1186
|
+
var Headers = createParamDecorator("header");
|
|
1187
|
+
var Cookies = createParamDecorator("cookie");
|
|
1188
|
+
var Req = createParamDecorator("req");
|
|
1189
|
+
var Res = createParamDecorator("res");
|
|
1190
|
+
var NextFunc = createParamDecorator("next");
|
|
1191
|
+
var Ip = createParamDecorator("ip");
|
|
1192
|
+
var Session = createParamDecorator("session");
|
|
1193
|
+
function UseMiddleware(...middlewares) {
|
|
1194
|
+
return (target, propertyKey, _descriptor) => {
|
|
1195
|
+
if (propertyKey) {
|
|
1196
|
+
const existing = Reflect.getMetadata(MIDDLEWARES_META_KEY, target, String(propertyKey)) ?? [];
|
|
1197
|
+
Reflect.defineMetadata(MIDDLEWARES_META_KEY, [...existing, ...middlewares], target, String(propertyKey));
|
|
1198
|
+
} else {
|
|
1199
|
+
const existing = Reflect.getMetadata(MIDDLEWARES_META_KEY, target) ?? [];
|
|
1200
|
+
Reflect.defineMetadata(MIDDLEWARES_META_KEY, [...existing, ...middlewares], target);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function applyDecorators(...decorators) {
|
|
1205
|
+
return (target, propertyKey, descriptor) => {
|
|
1206
|
+
for (const decorator of decorators) {
|
|
1207
|
+
if (propertyKey !== void 0) {
|
|
1208
|
+
decorator(target, propertyKey, descriptor);
|
|
1209
|
+
} else {
|
|
1210
|
+
decorator(target);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
function Meta(key, value) {
|
|
1216
|
+
return (target, propertyKey, _descriptor) => {
|
|
1217
|
+
const routeMeta = Reflect.getMetadata(ROUTES_META_KEY2, target, String(propertyKey));
|
|
1218
|
+
if (routeMeta) {
|
|
1219
|
+
if (!routeMeta.meta) routeMeta.meta = {};
|
|
1220
|
+
routeMeta.meta[key] = value;
|
|
1221
|
+
Reflect.defineMetadata(ROUTES_META_KEY2, routeMeta, target, String(propertyKey));
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// src/plugins/CORSPlugin.ts
|
|
1227
|
+
var DEFAULT_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
1228
|
+
var DEFAULT_HEADERS = ["Content-Type", "Authorization", "Accept", "X-Requested-With"];
|
|
1229
|
+
function CORSPlugin(options = {}) {
|
|
1230
|
+
const {
|
|
1231
|
+
origins = "*",
|
|
1232
|
+
methods = DEFAULT_METHODS,
|
|
1233
|
+
allowedHeaders = DEFAULT_HEADERS,
|
|
1234
|
+
exposedHeaders = [],
|
|
1235
|
+
credentials = false,
|
|
1236
|
+
maxAge = 86400,
|
|
1237
|
+
preflightContinue = false
|
|
1238
|
+
} = options;
|
|
1239
|
+
const methodsStr = methods.join(", ");
|
|
1240
|
+
const headersStr = allowedHeaders.join(", ");
|
|
1241
|
+
const exposedStr = exposedHeaders.length > 0 ? exposedHeaders.join(", ") : "";
|
|
1242
|
+
const isAllowedOrigin = (origin) => {
|
|
1243
|
+
if (origins === "*") return credentials ? origin : "*";
|
|
1244
|
+
if (typeof origins === "function") return origins(origin) ? origin : false;
|
|
1245
|
+
if (typeof origins === "string") return origin === origins ? origin : false;
|
|
1246
|
+
if (Array.isArray(origins)) return origins.includes(origin) ? origin : false;
|
|
1247
|
+
return false;
|
|
1248
|
+
};
|
|
1249
|
+
return (ctx, next) => {
|
|
1250
|
+
const { req, res } = ctx;
|
|
1251
|
+
const origin = req.headers.origin ?? "";
|
|
1252
|
+
const allowedOrigin = isAllowedOrigin(origin);
|
|
1253
|
+
if (allowedOrigin) {
|
|
1254
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
1255
|
+
if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
1256
|
+
if (exposedStr) res.setHeader("Access-Control-Expose-Headers", exposedStr);
|
|
1257
|
+
if (allowedOrigin !== "*") res.setHeader("Vary", "Origin");
|
|
1258
|
+
}
|
|
1259
|
+
if (req.method === "OPTIONS") {
|
|
1260
|
+
res.setHeader("Access-Control-Allow-Methods", methodsStr);
|
|
1261
|
+
res.setHeader("Access-Control-Allow-Headers", headersStr);
|
|
1262
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
1263
|
+
if (!preflightContinue) {
|
|
1264
|
+
res.statusCode = 204;
|
|
1265
|
+
res.end();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
next();
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/plugins/RateLimitPlugin.ts
|
|
1274
|
+
var MemoryStore = class {
|
|
1275
|
+
store = /* @__PURE__ */ new Map();
|
|
1276
|
+
cleanupInterval;
|
|
1277
|
+
constructor(windowMs) {
|
|
1278
|
+
this.cleanupInterval = setInterval(() => {
|
|
1279
|
+
const now = Date.now();
|
|
1280
|
+
for (const [key, entry] of this.store) {
|
|
1281
|
+
if (now >= entry.resetTime) this.store.delete(key);
|
|
1282
|
+
}
|
|
1283
|
+
}, windowMs);
|
|
1284
|
+
if (this.cleanupInterval.unref) this.cleanupInterval.unref();
|
|
1285
|
+
}
|
|
1286
|
+
increment(key, windowMs) {
|
|
1287
|
+
const now = Date.now();
|
|
1288
|
+
const entry = this.store.get(key);
|
|
1289
|
+
if (!entry || now >= entry.resetTime) {
|
|
1290
|
+
const resetTime = now + windowMs;
|
|
1291
|
+
this.store.set(key, { count: 1, resetTime });
|
|
1292
|
+
return { totalHits: 1, resetTime: new Date(resetTime) };
|
|
1293
|
+
}
|
|
1294
|
+
entry.count++;
|
|
1295
|
+
return { totalHits: entry.count, resetTime: new Date(entry.resetTime) };
|
|
1296
|
+
}
|
|
1297
|
+
decrement(key) {
|
|
1298
|
+
const entry = this.store.get(key);
|
|
1299
|
+
if (entry && entry.count > 0) entry.count--;
|
|
1300
|
+
}
|
|
1301
|
+
resetKey(key) {
|
|
1302
|
+
this.store.delete(key);
|
|
1303
|
+
}
|
|
1304
|
+
destroy() {
|
|
1305
|
+
clearInterval(this.cleanupInterval);
|
|
1306
|
+
this.store.clear();
|
|
1307
|
+
}
|
|
1308
|
+
};
|
|
1309
|
+
function RateLimitPlugin(options = {}) {
|
|
1310
|
+
const {
|
|
1311
|
+
windowMs = 6e4,
|
|
1312
|
+
max = 100,
|
|
1313
|
+
message = { error: { statusCode: 429, message: "Too many requests, please try again later" } },
|
|
1314
|
+
statusCode = 429,
|
|
1315
|
+
keyGenerator = (req) => req.ip,
|
|
1316
|
+
skipSuccessfulRequests = false,
|
|
1317
|
+
skipFailedRequests = false
|
|
1318
|
+
} = options;
|
|
1319
|
+
const memStore = new MemoryStore(windowMs);
|
|
1320
|
+
return (ctx, next) => {
|
|
1321
|
+
const { req, res } = ctx;
|
|
1322
|
+
const key = keyGenerator(req);
|
|
1323
|
+
const { totalHits, resetTime } = memStore.increment(key, windowMs);
|
|
1324
|
+
res.setHeader("X-RateLimit-Limit", String(max));
|
|
1325
|
+
res.setHeader("X-RateLimit-Remaining", String(Math.max(0, max - totalHits)));
|
|
1326
|
+
res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetTime.getTime() / 1e3)));
|
|
1327
|
+
if (totalHits > max) {
|
|
1328
|
+
res.setHeader("Retry-After", String(Math.ceil(windowMs / 1e3)));
|
|
1329
|
+
res.statusCode = statusCode;
|
|
1330
|
+
const body = typeof message === "string" ? message : JSON.stringify(message);
|
|
1331
|
+
res.setHeader("Content-Type", typeof message === "string" ? "text/plain" : "application/json");
|
|
1332
|
+
res.end(body);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const origEnd = res.end;
|
|
1336
|
+
res.end = function(...args) {
|
|
1337
|
+
if (skipSuccessfulRequests && res.statusCode < 400) {
|
|
1338
|
+
memStore.decrement(key);
|
|
1339
|
+
}
|
|
1340
|
+
if (skipFailedRequests && res.statusCode >= 400) {
|
|
1341
|
+
memStore.decrement(key);
|
|
1342
|
+
}
|
|
1343
|
+
return origEnd.apply(this, args);
|
|
1344
|
+
};
|
|
1345
|
+
next();
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/plugins/HelmetPlugin.ts
|
|
1350
|
+
function HelmetPlugin(options = {}) {
|
|
1351
|
+
const headers = [];
|
|
1352
|
+
if (options.noSniff !== false) {
|
|
1353
|
+
headers.push(["X-Content-Type-Options", "nosniff"]);
|
|
1354
|
+
}
|
|
1355
|
+
if (options.xssFilter !== false) {
|
|
1356
|
+
headers.push(["X-XSS-Protection", "0"]);
|
|
1357
|
+
}
|
|
1358
|
+
if (options.ieNoOpen !== false) {
|
|
1359
|
+
headers.push(["X-Download-Options", "noopen"]);
|
|
1360
|
+
}
|
|
1361
|
+
const frameguard = options.frameguard ?? true;
|
|
1362
|
+
if (frameguard !== false) {
|
|
1363
|
+
const action = typeof frameguard === "object" ? frameguard.action : "sameorigin";
|
|
1364
|
+
headers.push(["X-Frame-Options", action.toUpperCase()]);
|
|
1365
|
+
}
|
|
1366
|
+
if (options.dnsPrefetchControl !== false) {
|
|
1367
|
+
const allow = typeof options.dnsPrefetchControl === "object" ? options.dnsPrefetchControl.allow : false;
|
|
1368
|
+
headers.push(["X-DNS-Prefetch-Control", allow ? "on" : "off"]);
|
|
1369
|
+
}
|
|
1370
|
+
const hsts = options.hsts ?? true;
|
|
1371
|
+
if (hsts !== false) {
|
|
1372
|
+
const maxAge = typeof hsts === "object" ? hsts.maxAge : 15552e3;
|
|
1373
|
+
const includeSubDomains = typeof hsts === "object" ? hsts.includeSubDomains !== false : true;
|
|
1374
|
+
const preload = typeof hsts === "object" ? hsts.preload : false;
|
|
1375
|
+
let value = `max-age=${maxAge}`;
|
|
1376
|
+
if (includeSubDomains) value += "; includeSubDomains";
|
|
1377
|
+
if (preload) value += "; preload";
|
|
1378
|
+
headers.push(["Strict-Transport-Security", value]);
|
|
1379
|
+
}
|
|
1380
|
+
if (options.crossOriginEmbedderPolicy !== false) {
|
|
1381
|
+
headers.push(["Cross-Origin-Embedder-Policy", "require-corp"]);
|
|
1382
|
+
}
|
|
1383
|
+
if (options.crossOriginOpenerPolicy !== false) {
|
|
1384
|
+
const policy = typeof options.crossOriginOpenerPolicy === "object" ? options.crossOriginOpenerPolicy.policy : "same-origin";
|
|
1385
|
+
headers.push(["Cross-Origin-Opener-Policy", policy]);
|
|
1386
|
+
}
|
|
1387
|
+
if (options.crossOriginResourcePolicy !== false) {
|
|
1388
|
+
const policy = typeof options.crossOriginResourcePolicy === "object" ? options.crossOriginResourcePolicy.policy : "same-origin";
|
|
1389
|
+
headers.push(["Cross-Origin-Resource-Policy", policy]);
|
|
1390
|
+
}
|
|
1391
|
+
const referrer = options.referrerPolicy ?? true;
|
|
1392
|
+
if (referrer !== false) {
|
|
1393
|
+
const policy = typeof referrer === "object" ? Array.isArray(referrer.policy) ? referrer.policy.join(", ") : referrer.policy : "no-referrer";
|
|
1394
|
+
headers.push(["Referrer-Policy", policy]);
|
|
1395
|
+
}
|
|
1396
|
+
if (options.contentSecurityPolicy !== false && options.contentSecurityPolicy) {
|
|
1397
|
+
const csp = options.contentSecurityPolicy;
|
|
1398
|
+
const directives = Object.entries(csp).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
|
|
1399
|
+
headers.push(["Content-Security-Policy", directives]);
|
|
1400
|
+
}
|
|
1401
|
+
if (options.permissionsPolicy) {
|
|
1402
|
+
const pp = Object.entries(options.permissionsPolicy).map(([key, values]) => `${key}=(${values.join(" ")})`).join(", ");
|
|
1403
|
+
headers.push(["Permissions-Policy", pp]);
|
|
1404
|
+
}
|
|
1405
|
+
return (ctx, next) => {
|
|
1406
|
+
for (const [name, value] of headers) {
|
|
1407
|
+
ctx.res.setHeader(name, value);
|
|
1408
|
+
}
|
|
1409
|
+
next();
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function base64urlEncode(data) {
|
|
1413
|
+
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
1414
|
+
return buf.toString("base64url");
|
|
1415
|
+
}
|
|
1416
|
+
function base64urlDecode(str) {
|
|
1417
|
+
return Buffer.from(str, "base64url").toString("utf-8");
|
|
1418
|
+
}
|
|
1419
|
+
var ALGO_MAP = {
|
|
1420
|
+
HS256: "sha256",
|
|
1421
|
+
HS384: "sha384",
|
|
1422
|
+
HS512: "sha512"
|
|
1423
|
+
};
|
|
1424
|
+
function signJWT(payload, secret, options = {}) {
|
|
1425
|
+
const algorithm = options.algorithm ?? "HS256";
|
|
1426
|
+
const header = base64urlEncode(JSON.stringify({ alg: algorithm, typ: "JWT" }));
|
|
1427
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1428
|
+
const body = { ...payload, iat: now };
|
|
1429
|
+
if (options.expiresIn) {
|
|
1430
|
+
body.exp = now + parseExpiration(options.expiresIn);
|
|
1431
|
+
}
|
|
1432
|
+
const encodedPayload = base64urlEncode(JSON.stringify(body));
|
|
1433
|
+
const signature = createHmac(ALGO_MAP[algorithm] ?? "sha256", secret).update(`${header}.${encodedPayload}`).digest("base64url");
|
|
1434
|
+
return `${header}.${encodedPayload}.${signature}`;
|
|
1435
|
+
}
|
|
1436
|
+
function verifyJWT(token, secret, options = {}) {
|
|
1437
|
+
const parts = token.split(".");
|
|
1438
|
+
if (parts.length !== 3) throw new Error("Invalid token format");
|
|
1439
|
+
const [header, payload, signature] = parts;
|
|
1440
|
+
const algorithm = options.algorithm ?? "HS256";
|
|
1441
|
+
const expectedSig = createHmac(ALGO_MAP[algorithm] ?? "sha256", secret).update(`${header}.${payload}`).digest("base64url");
|
|
1442
|
+
if (signature !== expectedSig) throw new Error("Invalid token signature");
|
|
1443
|
+
const decoded = JSON.parse(base64urlDecode(payload));
|
|
1444
|
+
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1e3)) {
|
|
1445
|
+
throw new Error("Token expired");
|
|
1446
|
+
}
|
|
1447
|
+
return decoded;
|
|
1448
|
+
}
|
|
1449
|
+
function parseExpiration(exp) {
|
|
1450
|
+
if (typeof exp === "number") return exp;
|
|
1451
|
+
const match = exp.match(/^(\d+)(s|m|h|d)$/);
|
|
1452
|
+
if (!match) return 3600;
|
|
1453
|
+
const val = parseInt(match[1], 10);
|
|
1454
|
+
switch (match[2]) {
|
|
1455
|
+
case "s":
|
|
1456
|
+
return val;
|
|
1457
|
+
case "m":
|
|
1458
|
+
return val * 60;
|
|
1459
|
+
case "h":
|
|
1460
|
+
return val * 3600;
|
|
1461
|
+
case "d":
|
|
1462
|
+
return val * 86400;
|
|
1463
|
+
default:
|
|
1464
|
+
return 3600;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
function extractToken(req) {
|
|
1468
|
+
const auth = req.headers.authorization;
|
|
1469
|
+
if (auth?.startsWith("Bearer ")) return auth.slice(7);
|
|
1470
|
+
return req.query.token ?? req.cookies?.token ?? null;
|
|
1471
|
+
}
|
|
1472
|
+
function JWTPlugin(options) {
|
|
1473
|
+
const {
|
|
1474
|
+
secret,
|
|
1475
|
+
algorithm = "HS256",
|
|
1476
|
+
paths = [],
|
|
1477
|
+
exclude = [],
|
|
1478
|
+
getToken
|
|
1479
|
+
} = options;
|
|
1480
|
+
return (ctx, next) => {
|
|
1481
|
+
const { req, res } = ctx;
|
|
1482
|
+
const path = req.pathname;
|
|
1483
|
+
if (exclude.some((p) => path.startsWith(p))) {
|
|
1484
|
+
next();
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1487
|
+
if (paths.length > 0 && !paths.some((p) => path.startsWith(p))) {
|
|
1488
|
+
next();
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const token = getToken ? getToken(req) : extractToken(req);
|
|
1492
|
+
if (!token) {
|
|
1493
|
+
res.statusCode = 401;
|
|
1494
|
+
res.setHeader("Content-Type", "application/json");
|
|
1495
|
+
res.end(JSON.stringify({ error: { statusCode: 401, message: "No token provided" } }));
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
try {
|
|
1499
|
+
const payload = verifyJWT(token, secret, { algorithm });
|
|
1500
|
+
req.user = payload;
|
|
1501
|
+
next();
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
res.statusCode = 401;
|
|
1504
|
+
res.setHeader("Content-Type", "application/json");
|
|
1505
|
+
res.end(JSON.stringify({ error: { statusCode: 401, message: err.message ?? "Invalid token" } }));
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
var MemorySessionStore = class {
|
|
1510
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1511
|
+
cleanupTimer;
|
|
1512
|
+
constructor() {
|
|
1513
|
+
this.cleanupTimer = setInterval(() => {
|
|
1514
|
+
const now = Date.now();
|
|
1515
|
+
for (const [id, session] of this.sessions) {
|
|
1516
|
+
if (now >= session.expires) this.sessions.delete(id);
|
|
1517
|
+
}
|
|
1518
|
+
}, 6e4);
|
|
1519
|
+
if (this.cleanupTimer.unref) this.cleanupTimer.unref();
|
|
1520
|
+
}
|
|
1521
|
+
async get(id) {
|
|
1522
|
+
const session = this.sessions.get(id);
|
|
1523
|
+
if (!session) return null;
|
|
1524
|
+
if (Date.now() >= session.expires) {
|
|
1525
|
+
this.sessions.delete(id);
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
return session.data;
|
|
1529
|
+
}
|
|
1530
|
+
async set(id, data, maxAge) {
|
|
1531
|
+
this.sessions.set(id, { data, expires: Date.now() + maxAge });
|
|
1532
|
+
}
|
|
1533
|
+
async destroy(id) {
|
|
1534
|
+
this.sessions.delete(id);
|
|
1535
|
+
}
|
|
1536
|
+
async touch(id, maxAge) {
|
|
1537
|
+
const session = this.sessions.get(id);
|
|
1538
|
+
if (session) session.expires = Date.now() + maxAge;
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
function generateSessionId() {
|
|
1542
|
+
return randomBytes(24).toString("hex");
|
|
1543
|
+
}
|
|
1544
|
+
function SessionPlugin(options) {
|
|
1545
|
+
const {
|
|
1546
|
+
secret: _secret,
|
|
1547
|
+
name = "azura.sid",
|
|
1548
|
+
maxAge = 864e5,
|
|
1549
|
+
secure = false,
|
|
1550
|
+
httpOnly = true,
|
|
1551
|
+
sameSite = "Lax",
|
|
1552
|
+
store = new MemorySessionStore()
|
|
1553
|
+
} = options;
|
|
1554
|
+
return async (ctx, next) => {
|
|
1555
|
+
const { req, res } = ctx;
|
|
1556
|
+
let sessionId = req.cookies[name];
|
|
1557
|
+
let sessionData = null;
|
|
1558
|
+
if (sessionId) {
|
|
1559
|
+
sessionData = await store.get(sessionId);
|
|
1560
|
+
}
|
|
1561
|
+
if (!sessionData) {
|
|
1562
|
+
sessionId = generateSessionId();
|
|
1563
|
+
sessionData = {};
|
|
1564
|
+
}
|
|
1565
|
+
req.session = sessionData;
|
|
1566
|
+
const originalEnd = res.end;
|
|
1567
|
+
res.end = async function(...args) {
|
|
1568
|
+
await store.set(sessionId, req.session ?? {}, maxAge);
|
|
1569
|
+
const cookieParts = [
|
|
1570
|
+
`${name}=${sessionId}`,
|
|
1571
|
+
`Path=/`,
|
|
1572
|
+
`Max-Age=${Math.floor(maxAge / 1e3)}`,
|
|
1573
|
+
`SameSite=${sameSite}`
|
|
1574
|
+
];
|
|
1575
|
+
if (httpOnly) cookieParts.push("HttpOnly");
|
|
1576
|
+
if (secure) cookieParts.push("Secure");
|
|
1577
|
+
const existing = res.getHeader("Set-Cookie");
|
|
1578
|
+
const cookies = existing ? [...Array.isArray(existing) ? existing : [String(existing)], cookieParts.join("; ")] : [cookieParts.join("; ")];
|
|
1579
|
+
res.setHeader("Set-Cookie", cookies);
|
|
1580
|
+
return originalEnd.apply(this, args);
|
|
1581
|
+
};
|
|
1582
|
+
next();
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
var COMPRESSIBLE_TYPES = /^text\/|application\/json|application\/javascript|application\/xml|image\/svg\+xml/;
|
|
1586
|
+
function CompressionPlugin(options = {}) {
|
|
1587
|
+
const {
|
|
1588
|
+
threshold = 1024,
|
|
1589
|
+
level = 6,
|
|
1590
|
+
algorithms = ["gzip", "deflate"],
|
|
1591
|
+
filter
|
|
1592
|
+
} = options;
|
|
1593
|
+
return (ctx, next) => {
|
|
1594
|
+
const { req, res } = ctx;
|
|
1595
|
+
if (filter && !filter(req, res)) {
|
|
1596
|
+
next();
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const acceptEncoding = req.headers["accept-encoding"] ?? "";
|
|
1600
|
+
let encoding = null;
|
|
1601
|
+
for (const algo of algorithms) {
|
|
1602
|
+
if (typeof acceptEncoding === "string" && acceptEncoding.includes(algo)) {
|
|
1603
|
+
encoding = algo;
|
|
1604
|
+
break;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
if (!encoding) {
|
|
1608
|
+
next();
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
const originalEnd = res.end;
|
|
1612
|
+
res.write;
|
|
1613
|
+
res.end = function(chunk, encodingArg, callback) {
|
|
1614
|
+
if (!chunk || typeof chunk !== "string" && !Buffer.isBuffer(chunk)) {
|
|
1615
|
+
return originalEnd.call(this, chunk, encodingArg, callback);
|
|
1616
|
+
}
|
|
1617
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
1618
|
+
if (buf.length < threshold) {
|
|
1619
|
+
return originalEnd.call(this, chunk, encodingArg, callback);
|
|
1620
|
+
}
|
|
1621
|
+
const contentType = String(res.getHeader("Content-Type") ?? "");
|
|
1622
|
+
if (!COMPRESSIBLE_TYPES.test(contentType)) {
|
|
1623
|
+
return originalEnd.call(this, chunk, encodingArg, callback);
|
|
1624
|
+
}
|
|
1625
|
+
res.removeHeader("Content-Length");
|
|
1626
|
+
res.setHeader("Content-Encoding", encoding);
|
|
1627
|
+
res.setHeader("Vary", "Accept-Encoding");
|
|
1628
|
+
let stream;
|
|
1629
|
+
if (encoding === "gzip") {
|
|
1630
|
+
stream = createGzip({ level });
|
|
1631
|
+
} else {
|
|
1632
|
+
stream = createDeflate({ level });
|
|
1633
|
+
}
|
|
1634
|
+
const chunks = [];
|
|
1635
|
+
stream.on("data", (c) => chunks.push(c));
|
|
1636
|
+
stream.on("end", () => {
|
|
1637
|
+
const compressed = Buffer.concat(chunks);
|
|
1638
|
+
res.setHeader("Content-Length", compressed.length);
|
|
1639
|
+
originalEnd.call(res, compressed);
|
|
1640
|
+
});
|
|
1641
|
+
stream.end(buf);
|
|
1642
|
+
};
|
|
1643
|
+
next();
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
var MIME_TYPES = {
|
|
1647
|
+
".html": "text/html; charset=utf-8",
|
|
1648
|
+
".htm": "text/html; charset=utf-8",
|
|
1649
|
+
".css": "text/css; charset=utf-8",
|
|
1650
|
+
".js": "application/javascript; charset=utf-8",
|
|
1651
|
+
".mjs": "application/javascript; charset=utf-8",
|
|
1652
|
+
".json": "application/json; charset=utf-8",
|
|
1653
|
+
".xml": "application/xml; charset=utf-8",
|
|
1654
|
+
".txt": "text/plain; charset=utf-8",
|
|
1655
|
+
".csv": "text/csv; charset=utf-8",
|
|
1656
|
+
".png": "image/png",
|
|
1657
|
+
".jpg": "image/jpeg",
|
|
1658
|
+
".jpeg": "image/jpeg",
|
|
1659
|
+
".gif": "image/gif",
|
|
1660
|
+
".svg": "image/svg+xml",
|
|
1661
|
+
".ico": "image/x-icon",
|
|
1662
|
+
".webp": "image/webp",
|
|
1663
|
+
".avif": "image/avif",
|
|
1664
|
+
".woff": "font/woff",
|
|
1665
|
+
".woff2": "font/woff2",
|
|
1666
|
+
".ttf": "font/ttf",
|
|
1667
|
+
".otf": "font/otf",
|
|
1668
|
+
".eot": "application/vnd.ms-fontobject",
|
|
1669
|
+
".mp4": "video/mp4",
|
|
1670
|
+
".webm": "video/webm",
|
|
1671
|
+
".mp3": "audio/mpeg",
|
|
1672
|
+
".ogg": "audio/ogg",
|
|
1673
|
+
".wav": "audio/wav",
|
|
1674
|
+
".pdf": "application/pdf",
|
|
1675
|
+
".zip": "application/zip",
|
|
1676
|
+
".gz": "application/gzip",
|
|
1677
|
+
".wasm": "application/wasm",
|
|
1678
|
+
".map": "application/json"
|
|
1679
|
+
};
|
|
1680
|
+
function StaticPlugin(options) {
|
|
1681
|
+
const {
|
|
1682
|
+
root,
|
|
1683
|
+
prefix = "/",
|
|
1684
|
+
index = ["index.html"],
|
|
1685
|
+
dotfiles = "ignore",
|
|
1686
|
+
maxAge = 0,
|
|
1687
|
+
etag = true,
|
|
1688
|
+
lastModified = true,
|
|
1689
|
+
fallthrough = true,
|
|
1690
|
+
immutable = false
|
|
1691
|
+
} = options;
|
|
1692
|
+
const resolvedRoot = resolve(root);
|
|
1693
|
+
const indexFiles = Array.isArray(index) ? index : [index];
|
|
1694
|
+
const normalizedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
1695
|
+
return (ctx, next) => {
|
|
1696
|
+
const { req, res } = ctx;
|
|
1697
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1698
|
+
next();
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const path = req.pathname;
|
|
1702
|
+
if (!path.startsWith(normalizedPrefix)) {
|
|
1703
|
+
next();
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
const relativePath = path.slice(normalizedPrefix.length) || "/";
|
|
1707
|
+
if (relativePath.includes("..")) {
|
|
1708
|
+
next();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (dotfiles !== "allow" && relativePath.split("/").some((s) => s.startsWith("."))) {
|
|
1712
|
+
if (dotfiles === "deny") {
|
|
1713
|
+
res.statusCode = 403;
|
|
1714
|
+
res.end("Forbidden");
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
next();
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
let filePath = join(resolvedRoot, relativePath);
|
|
1721
|
+
if (!existsSync(filePath)) {
|
|
1722
|
+
if (fallthrough) {
|
|
1723
|
+
next();
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
res.statusCode = 404;
|
|
1727
|
+
res.end("Not Found");
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
let stat = statSync(filePath);
|
|
1731
|
+
if (stat.isDirectory()) {
|
|
1732
|
+
let found = false;
|
|
1733
|
+
for (const idx of indexFiles) {
|
|
1734
|
+
const candidate = join(filePath, idx);
|
|
1735
|
+
if (existsSync(candidate)) {
|
|
1736
|
+
filePath = candidate;
|
|
1737
|
+
stat = statSync(filePath);
|
|
1738
|
+
found = true;
|
|
1739
|
+
break;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
if (!found) {
|
|
1743
|
+
if (fallthrough) {
|
|
1744
|
+
next();
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
res.statusCode = 404;
|
|
1748
|
+
res.end("Not Found");
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
const ext = extname(filePath).toLowerCase();
|
|
1753
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1754
|
+
res.setHeader("Content-Type", contentType);
|
|
1755
|
+
res.setHeader("Content-Length", stat.size);
|
|
1756
|
+
if (lastModified) {
|
|
1757
|
+
res.setHeader("Last-Modified", stat.mtime.toUTCString());
|
|
1758
|
+
}
|
|
1759
|
+
if (etag) {
|
|
1760
|
+
const etagValue = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
|
|
1761
|
+
res.setHeader("ETag", etagValue);
|
|
1762
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
1763
|
+
if (ifNoneMatch === etagValue) {
|
|
1764
|
+
res.statusCode = 304;
|
|
1765
|
+
res.end();
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
let cacheControl = `public, max-age=${maxAge}`;
|
|
1770
|
+
if (immutable) cacheControl += ", immutable";
|
|
1771
|
+
res.setHeader("Cache-Control", cacheControl);
|
|
1772
|
+
if (req.method === "HEAD") {
|
|
1773
|
+
res.end();
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
const stream = createReadStream(filePath);
|
|
1777
|
+
stream.pipe(res);
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function ETagPlugin(options = {}) {
|
|
1781
|
+
const { weak = true } = options;
|
|
1782
|
+
return (ctx, next) => {
|
|
1783
|
+
const { req, res } = ctx;
|
|
1784
|
+
const originalEnd = res.end;
|
|
1785
|
+
res.end = function(chunk, encoding, callback) {
|
|
1786
|
+
if (chunk && (typeof chunk === "string" || Buffer.isBuffer(chunk)) && res.statusCode === 200) {
|
|
1787
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
1788
|
+
const hash = createHash("md5").update(buf).digest("hex").slice(0, 16);
|
|
1789
|
+
const etag = weak ? `W/"${hash}"` : `"${hash}"`;
|
|
1790
|
+
res.setHeader("ETag", etag);
|
|
1791
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
1792
|
+
if (ifNoneMatch === etag) {
|
|
1793
|
+
res.statusCode = 304;
|
|
1794
|
+
return originalEnd.call(this);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return originalEnd.call(this, chunk, encoding, callback);
|
|
1798
|
+
};
|
|
1799
|
+
next();
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
function RequestIdPlugin(options = {}) {
|
|
1803
|
+
const {
|
|
1804
|
+
header = "X-Request-Id",
|
|
1805
|
+
generator = randomUUID
|
|
1806
|
+
} = options;
|
|
1807
|
+
return (ctx, next) => {
|
|
1808
|
+
const { req, res } = ctx;
|
|
1809
|
+
const existingId = req.headers[header.toLowerCase()];
|
|
1810
|
+
const requestId = existingId ?? generator();
|
|
1811
|
+
req.requestId = requestId;
|
|
1812
|
+
res.setHeader(header, requestId);
|
|
1813
|
+
next();
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// src/plugins/TimeoutPlugin.ts
|
|
1818
|
+
function TimeoutPlugin(options) {
|
|
1819
|
+
const {
|
|
1820
|
+
timeout,
|
|
1821
|
+
message = { error: { statusCode: 408, message: "Request Timeout" } },
|
|
1822
|
+
statusCode = 408
|
|
1823
|
+
} = options;
|
|
1824
|
+
return (ctx, next) => {
|
|
1825
|
+
const { res } = ctx;
|
|
1826
|
+
let timedOut = false;
|
|
1827
|
+
const timer = setTimeout(() => {
|
|
1828
|
+
timedOut = true;
|
|
1829
|
+
if (!res.headersSent) {
|
|
1830
|
+
res.statusCode = statusCode;
|
|
1831
|
+
res.setHeader("Content-Type", typeof message === "string" ? "text/plain" : "application/json");
|
|
1832
|
+
res.end(typeof message === "string" ? message : JSON.stringify(message));
|
|
1833
|
+
}
|
|
1834
|
+
}, timeout);
|
|
1835
|
+
const originalEnd = res.end;
|
|
1836
|
+
res.end = function(...args) {
|
|
1837
|
+
clearTimeout(timer);
|
|
1838
|
+
if (!timedOut) {
|
|
1839
|
+
return originalEnd.apply(this, args);
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
next();
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// src/plugins/HealthCheckPlugin.ts
|
|
1847
|
+
function HealthCheckPlugin(options = {}) {
|
|
1848
|
+
const {
|
|
1849
|
+
path = "/health",
|
|
1850
|
+
checks = {},
|
|
1851
|
+
timeout: checkTimeout = 5e3
|
|
1852
|
+
} = options;
|
|
1853
|
+
return async (ctx, next) => {
|
|
1854
|
+
const { req, res } = ctx;
|
|
1855
|
+
if (req.pathname !== path) {
|
|
1856
|
+
next();
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const results = {};
|
|
1860
|
+
let allHealthy = true;
|
|
1861
|
+
for (const [name, check] of Object.entries(checks)) {
|
|
1862
|
+
const start = Date.now();
|
|
1863
|
+
try {
|
|
1864
|
+
const result = await Promise.race([
|
|
1865
|
+
Promise.resolve(check()),
|
|
1866
|
+
new Promise(
|
|
1867
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), checkTimeout)
|
|
1868
|
+
)
|
|
1869
|
+
]);
|
|
1870
|
+
results[name] = {
|
|
1871
|
+
status: result ? "healthy" : "unhealthy",
|
|
1872
|
+
duration: Date.now() - start
|
|
1873
|
+
};
|
|
1874
|
+
if (!result) allHealthy = false;
|
|
1875
|
+
} catch {
|
|
1876
|
+
results[name] = {
|
|
1877
|
+
status: "unhealthy",
|
|
1878
|
+
duration: Date.now() - start
|
|
1879
|
+
};
|
|
1880
|
+
allHealthy = false;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
const body = JSON.stringify({
|
|
1884
|
+
status: allHealthy ? "healthy" : "unhealthy",
|
|
1885
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1886
|
+
uptime: process.uptime(),
|
|
1887
|
+
memory: process.memoryUsage(),
|
|
1888
|
+
checks: results
|
|
1889
|
+
});
|
|
1890
|
+
res.statusCode = allHealthy ? 200 : 503;
|
|
1891
|
+
res.setHeader("Content-Type", "application/json");
|
|
1892
|
+
res.setHeader("Cache-Control", "no-cache, no-store");
|
|
1893
|
+
res.end(body);
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// src/plugins/CircuitBreakerPlugin.ts
|
|
1898
|
+
function CircuitBreakerPlugin(options = {}) {
|
|
1899
|
+
const {
|
|
1900
|
+
threshold = 5,
|
|
1901
|
+
timeout: breakerTimeout = 3e4,
|
|
1902
|
+
resetTimeout = 6e4,
|
|
1903
|
+
halfOpenRequests = 1,
|
|
1904
|
+
monitor
|
|
1905
|
+
} = options;
|
|
1906
|
+
let state = "CLOSED";
|
|
1907
|
+
let failures = 0;
|
|
1908
|
+
let successes = 0;
|
|
1909
|
+
let lastFailureTime = 0;
|
|
1910
|
+
let halfOpenCount = 0;
|
|
1911
|
+
const transition = (newState) => {
|
|
1912
|
+
if (state !== newState) {
|
|
1913
|
+
state = newState;
|
|
1914
|
+
monitor?.(state);
|
|
1915
|
+
}
|
|
1916
|
+
};
|
|
1917
|
+
return (ctx, next) => {
|
|
1918
|
+
const { res } = ctx;
|
|
1919
|
+
if (state === "OPEN") {
|
|
1920
|
+
if (Date.now() - lastFailureTime >= resetTimeout) {
|
|
1921
|
+
transition("HALF_OPEN");
|
|
1922
|
+
halfOpenCount = 0;
|
|
1923
|
+
} else {
|
|
1924
|
+
res.statusCode = 503;
|
|
1925
|
+
res.setHeader("Content-Type", "application/json");
|
|
1926
|
+
res.setHeader("Retry-After", String(Math.ceil(resetTimeout / 1e3)));
|
|
1927
|
+
res.end(
|
|
1928
|
+
JSON.stringify({
|
|
1929
|
+
error: { statusCode: 503, message: "Service temporarily unavailable (circuit open)" }
|
|
1930
|
+
})
|
|
1931
|
+
);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
if (state === "HALF_OPEN" && halfOpenCount >= halfOpenRequests) {
|
|
1936
|
+
res.statusCode = 503;
|
|
1937
|
+
res.setHeader("Content-Type", "application/json");
|
|
1938
|
+
res.end(
|
|
1939
|
+
JSON.stringify({
|
|
1940
|
+
error: { statusCode: 503, message: "Service temporarily unavailable (circuit half-open)" }
|
|
1941
|
+
})
|
|
1942
|
+
);
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (state === "HALF_OPEN") halfOpenCount++;
|
|
1946
|
+
const originalEnd = res.end;
|
|
1947
|
+
res.end = function(...args) {
|
|
1948
|
+
if (res.statusCode >= 500) {
|
|
1949
|
+
failures++;
|
|
1950
|
+
lastFailureTime = Date.now();
|
|
1951
|
+
if (state === "HALF_OPEN" || failures >= threshold) {
|
|
1952
|
+
transition("OPEN");
|
|
1953
|
+
}
|
|
1954
|
+
} else {
|
|
1955
|
+
if (state === "HALF_OPEN") {
|
|
1956
|
+
successes++;
|
|
1957
|
+
if (successes >= halfOpenRequests) {
|
|
1958
|
+
failures = 0;
|
|
1959
|
+
successes = 0;
|
|
1960
|
+
transition("CLOSED");
|
|
1961
|
+
}
|
|
1962
|
+
} else {
|
|
1963
|
+
failures = Math.max(0, failures - 1);
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
return originalEnd.apply(this, args);
|
|
1967
|
+
};
|
|
1968
|
+
next();
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/plugins/SSEPlugin.ts
|
|
1973
|
+
var SSEManager = class {
|
|
1974
|
+
clients = /* @__PURE__ */ new Map();
|
|
1975
|
+
clientCounter = 0;
|
|
1976
|
+
addClient(res) {
|
|
1977
|
+
const id = `sse-${++this.clientCounter}-${Date.now()}`;
|
|
1978
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
1979
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
1980
|
+
res.setHeader("Connection", "keep-alive");
|
|
1981
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
1982
|
+
res.statusCode = 200;
|
|
1983
|
+
res.flushHeaders();
|
|
1984
|
+
const client = {
|
|
1985
|
+
id,
|
|
1986
|
+
res,
|
|
1987
|
+
send(event, data, eventId) {
|
|
1988
|
+
const serialized = typeof data === "string" ? data : JSON.stringify(data);
|
|
1989
|
+
let message = "";
|
|
1990
|
+
if (eventId) message += `id: ${eventId}
|
|
1991
|
+
`;
|
|
1992
|
+
message += `event: ${event}
|
|
1993
|
+
`;
|
|
1994
|
+
message += `data: ${serialized}
|
|
1995
|
+
|
|
1996
|
+
`;
|
|
1997
|
+
res.write(message);
|
|
1998
|
+
},
|
|
1999
|
+
close() {
|
|
2000
|
+
res.end();
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
this.clients.set(id, client);
|
|
2004
|
+
res.on("close", () => {
|
|
2005
|
+
this.clients.delete(id);
|
|
2006
|
+
});
|
|
2007
|
+
return client;
|
|
2008
|
+
}
|
|
2009
|
+
broadcast(event, data, id) {
|
|
2010
|
+
for (const client of this.clients.values()) {
|
|
2011
|
+
client.send(event, data, id);
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
getClient(id) {
|
|
2015
|
+
return this.clients.get(id);
|
|
2016
|
+
}
|
|
2017
|
+
getClientCount() {
|
|
2018
|
+
return this.clients.size;
|
|
2019
|
+
}
|
|
2020
|
+
closeAll() {
|
|
2021
|
+
for (const client of this.clients.values()) {
|
|
2022
|
+
client.close();
|
|
2023
|
+
}
|
|
2024
|
+
this.clients.clear();
|
|
2025
|
+
}
|
|
2026
|
+
};
|
|
2027
|
+
function SSEPlugin(options) {
|
|
2028
|
+
const {
|
|
2029
|
+
path,
|
|
2030
|
+
heartbeatInterval = 3e4,
|
|
2031
|
+
retry = 3e3,
|
|
2032
|
+
maxClients = 1e3
|
|
2033
|
+
} = options;
|
|
2034
|
+
const manager = new SSEManager();
|
|
2035
|
+
let heartbeat = null;
|
|
2036
|
+
if (heartbeatInterval > 0) {
|
|
2037
|
+
heartbeat = setInterval(() => {
|
|
2038
|
+
manager.broadcast("heartbeat", { time: Date.now() });
|
|
2039
|
+
}, heartbeatInterval);
|
|
2040
|
+
if (heartbeat.unref) heartbeat.unref();
|
|
2041
|
+
}
|
|
2042
|
+
const plugin = Object.assign(
|
|
2043
|
+
(ctx, next) => {
|
|
2044
|
+
const { req, res } = ctx;
|
|
2045
|
+
if (req.pathname !== path || req.method !== "GET") {
|
|
2046
|
+
next();
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
if (manager.getClientCount() >= maxClients) {
|
|
2050
|
+
res.statusCode = 503;
|
|
2051
|
+
res.setHeader("Content-Type", "application/json");
|
|
2052
|
+
res.end(JSON.stringify({ error: { statusCode: 503, message: "Too many SSE connections" } }));
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
const client = manager.addClient(res);
|
|
2056
|
+
res.write(`retry: ${retry}
|
|
2057
|
+
|
|
2058
|
+
`);
|
|
2059
|
+
client.send("connected", { clientId: client.id });
|
|
2060
|
+
},
|
|
2061
|
+
{ manager }
|
|
2062
|
+
);
|
|
2063
|
+
return plugin;
|
|
2064
|
+
}
|
|
2065
|
+
function ProxyPlugin(options) {
|
|
2066
|
+
const {
|
|
2067
|
+
target,
|
|
2068
|
+
pathRewrite = {},
|
|
2069
|
+
changeOrigin = true,
|
|
2070
|
+
timeout = 3e4,
|
|
2071
|
+
headers: extraHeaders = {},
|
|
2072
|
+
onProxyReq,
|
|
2073
|
+
onProxyRes
|
|
2074
|
+
} = options;
|
|
2075
|
+
const targetUrl = new URL(target);
|
|
2076
|
+
const isHttps = targetUrl.protocol === "https:";
|
|
2077
|
+
const requestFn = isHttps ? request : request$1;
|
|
2078
|
+
return (ctx, next) => {
|
|
2079
|
+
const { req, res } = ctx;
|
|
2080
|
+
let targetPath = req.pathname;
|
|
2081
|
+
for (const [from, to] of Object.entries(pathRewrite)) {
|
|
2082
|
+
targetPath = targetPath.replace(new RegExp(from), to);
|
|
2083
|
+
}
|
|
2084
|
+
const search = req.url?.includes("?") ? req.url.slice(req.url.indexOf("?")) : "";
|
|
2085
|
+
const fullPath = targetPath + search;
|
|
2086
|
+
const proxyHeaders = {};
|
|
2087
|
+
for (const [key, val] of Object.entries(req.headers)) {
|
|
2088
|
+
if (val) proxyHeaders[key] = Array.isArray(val) ? val.join(", ") : val;
|
|
2089
|
+
}
|
|
2090
|
+
if (changeOrigin) {
|
|
2091
|
+
proxyHeaders.host = targetUrl.host;
|
|
2092
|
+
}
|
|
2093
|
+
Object.assign(proxyHeaders, extraHeaders);
|
|
2094
|
+
const proxyReq = requestFn(
|
|
2095
|
+
{
|
|
2096
|
+
hostname: targetUrl.hostname,
|
|
2097
|
+
port: targetUrl.port || (isHttps ? 443 : 80),
|
|
2098
|
+
path: fullPath,
|
|
2099
|
+
method: req.method,
|
|
2100
|
+
headers: proxyHeaders,
|
|
2101
|
+
timeout
|
|
2102
|
+
},
|
|
2103
|
+
(proxyRes) => {
|
|
2104
|
+
onProxyRes?.(proxyRes, res);
|
|
2105
|
+
res.statusCode = proxyRes.statusCode ?? 502;
|
|
2106
|
+
for (const [key, val] of Object.entries(proxyRes.headers)) {
|
|
2107
|
+
if (val) res.setHeader(key, val);
|
|
2108
|
+
}
|
|
2109
|
+
proxyRes.pipe(res);
|
|
2110
|
+
}
|
|
2111
|
+
);
|
|
2112
|
+
proxyReq.on("error", (err) => {
|
|
2113
|
+
if (!res.headersSent) {
|
|
2114
|
+
res.statusCode = 502;
|
|
2115
|
+
res.setHeader("Content-Type", "application/json");
|
|
2116
|
+
res.end(JSON.stringify({ error: { statusCode: 502, message: "Bad Gateway", details: err.message } }));
|
|
2117
|
+
}
|
|
2118
|
+
});
|
|
2119
|
+
proxyReq.on("timeout", () => {
|
|
2120
|
+
proxyReq.destroy();
|
|
2121
|
+
if (!res.headersSent) {
|
|
2122
|
+
res.statusCode = 504;
|
|
2123
|
+
res.setHeader("Content-Type", "application/json");
|
|
2124
|
+
res.end(JSON.stringify({ error: { statusCode: 504, message: "Gateway Timeout" } }));
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
onProxyReq?.(proxyReq, req);
|
|
2128
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2129
|
+
req.pipe(proxyReq);
|
|
2130
|
+
} else {
|
|
2131
|
+
proxyReq.end();
|
|
2132
|
+
}
|
|
2133
|
+
};
|
|
2134
|
+
}
|
|
2135
|
+
function MultipartPlugin(options = {}) {
|
|
2136
|
+
const {
|
|
2137
|
+
maxFileSize = 10 * 1024 * 1024,
|
|
2138
|
+
maxFiles = 10,
|
|
2139
|
+
maxFieldSize = 1024 * 1024,
|
|
2140
|
+
maxFields = 50,
|
|
2141
|
+
allowedMimeTypes,
|
|
2142
|
+
uploadDir
|
|
2143
|
+
} = options;
|
|
2144
|
+
if (uploadDir && !existsSync(uploadDir)) {
|
|
2145
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
2146
|
+
}
|
|
2147
|
+
return async (ctx, next) => {
|
|
2148
|
+
const { req } = ctx;
|
|
2149
|
+
const contentType = req.headers["content-type"] ?? "";
|
|
2150
|
+
if (!contentType.startsWith("multipart/form-data")) {
|
|
2151
|
+
next();
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
2155
|
+
if (!boundaryMatch) {
|
|
2156
|
+
next();
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
const boundary = boundaryMatch[1];
|
|
2160
|
+
try {
|
|
2161
|
+
const { fields, files } = await parseMultipart(req, boundary, {
|
|
2162
|
+
maxFileSize,
|
|
2163
|
+
maxFiles,
|
|
2164
|
+
maxFieldSize,
|
|
2165
|
+
maxFields,
|
|
2166
|
+
allowedMimeTypes
|
|
2167
|
+
});
|
|
2168
|
+
if (uploadDir) {
|
|
2169
|
+
for (const file of files) {
|
|
2170
|
+
const ext = file.filename.includes(".") ? file.filename.slice(file.filename.lastIndexOf(".")) : "";
|
|
2171
|
+
const savedName = `${randomBytes(16).toString("hex")}${ext}`;
|
|
2172
|
+
const savePath = join(uploadDir, savedName);
|
|
2173
|
+
writeFileSync(savePath, file.buffer);
|
|
2174
|
+
file.path = savePath;
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
req.files = files;
|
|
2178
|
+
req.body = { ...req.body ?? {}, ...fields };
|
|
2179
|
+
next();
|
|
2180
|
+
} catch (err) {
|
|
2181
|
+
const { res } = ctx;
|
|
2182
|
+
res.statusCode = 400;
|
|
2183
|
+
res.setHeader("Content-Type", "application/json");
|
|
2184
|
+
res.end(JSON.stringify({ error: { statusCode: 400, message: err.message } }));
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
async function parseMultipart(req, boundary, opts) {
|
|
2189
|
+
const raw = await collectBody(req);
|
|
2190
|
+
const delimiter = Buffer.from(`--${boundary}`);
|
|
2191
|
+
const endDelimiter = Buffer.from(`--${boundary}--`);
|
|
2192
|
+
const fields = {};
|
|
2193
|
+
const files = [];
|
|
2194
|
+
let fieldCount = 0;
|
|
2195
|
+
let start = bufferIndexOf(raw, delimiter, 0);
|
|
2196
|
+
if (start === -1) return { fields, files };
|
|
2197
|
+
start += delimiter.length + 2;
|
|
2198
|
+
while (start < raw.length) {
|
|
2199
|
+
const end = bufferIndexOf(raw, delimiter, start);
|
|
2200
|
+
if (end === -1) break;
|
|
2201
|
+
const part = raw.subarray(start, end - 2);
|
|
2202
|
+
const headerEnd = bufferIndexOf(part, Buffer.from("\r\n\r\n"), 0);
|
|
2203
|
+
if (headerEnd === -1) {
|
|
2204
|
+
start = end + delimiter.length + 2;
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
2207
|
+
const headerStr = part.subarray(0, headerEnd).toString("utf-8");
|
|
2208
|
+
const body = part.subarray(headerEnd + 4);
|
|
2209
|
+
const nameMatch = headerStr.match(/name="([^"]+)"/);
|
|
2210
|
+
const filenameMatch = headerStr.match(/filename="([^"]*)"/);
|
|
2211
|
+
const typeMatch = headerStr.match(/Content-Type:\s*(.+)/i);
|
|
2212
|
+
if (filenameMatch && nameMatch) {
|
|
2213
|
+
if (files.length >= opts.maxFiles) throw new Error("Too many files");
|
|
2214
|
+
if (body.length > opts.maxFileSize) throw new Error(`File ${filenameMatch[1]} exceeds max size`);
|
|
2215
|
+
const mimetype = typeMatch?.[1]?.trim() ?? "application/octet-stream";
|
|
2216
|
+
if (opts.allowedMimeTypes && !opts.allowedMimeTypes.includes(mimetype)) {
|
|
2217
|
+
throw new Error(`File type ${mimetype} not allowed`);
|
|
2218
|
+
}
|
|
2219
|
+
files.push({
|
|
2220
|
+
fieldname: nameMatch[1],
|
|
2221
|
+
filename: filenameMatch[1],
|
|
2222
|
+
mimetype,
|
|
2223
|
+
size: body.length,
|
|
2224
|
+
buffer: Buffer.from(body)
|
|
2225
|
+
});
|
|
2226
|
+
} else if (nameMatch) {
|
|
2227
|
+
if (++fieldCount > opts.maxFields) throw new Error("Too many fields");
|
|
2228
|
+
if (body.length > opts.maxFieldSize) throw new Error(`Field ${nameMatch[1]} exceeds max size`);
|
|
2229
|
+
fields[nameMatch[1]] = body.toString("utf-8");
|
|
2230
|
+
}
|
|
2231
|
+
start = end + delimiter.length;
|
|
2232
|
+
if (bufferIndexOf(raw, endDelimiter, end) === end) break;
|
|
2233
|
+
start += 2;
|
|
2234
|
+
}
|
|
2235
|
+
return { fields, files };
|
|
2236
|
+
}
|
|
2237
|
+
function collectBody(stream) {
|
|
2238
|
+
return new Promise((resolve3, reject) => {
|
|
2239
|
+
const chunks = [];
|
|
2240
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
2241
|
+
stream.on("end", () => resolve3(Buffer.concat(chunks)));
|
|
2242
|
+
stream.on("error", reject);
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
function bufferIndexOf(buf, search, offset) {
|
|
2246
|
+
for (let i = offset; i <= buf.length - search.length; i++) {
|
|
2247
|
+
let found = true;
|
|
2248
|
+
for (let j = 0; j < search.length; j++) {
|
|
2249
|
+
if (buf[i + j] !== search[j]) {
|
|
2250
|
+
found = false;
|
|
2251
|
+
break;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
if (found) return i;
|
|
2255
|
+
}
|
|
2256
|
+
return -1;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/utils/validators/DTOValidator.ts
|
|
2260
|
+
var DTOValidator = class {
|
|
2261
|
+
static validate(data, schema) {
|
|
2262
|
+
const errors = [];
|
|
2263
|
+
if (data == null || typeof data !== "object") {
|
|
2264
|
+
return {
|
|
2265
|
+
valid: false,
|
|
2266
|
+
errors: [{ field: "_root", message: "Request body must be an object" }]
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
for (const [field, rule] of Object.entries(schema)) {
|
|
2270
|
+
const value = data[field];
|
|
2271
|
+
if (rule.required && (value === void 0 || value === null || value === "")) {
|
|
2272
|
+
errors.push({
|
|
2273
|
+
field,
|
|
2274
|
+
message: rule.message ?? `${field} is required`,
|
|
2275
|
+
value
|
|
2276
|
+
});
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
if (value === void 0 || value === null) continue;
|
|
2280
|
+
if (rule.type) {
|
|
2281
|
+
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
2282
|
+
if (actualType !== rule.type) {
|
|
2283
|
+
errors.push({
|
|
2284
|
+
field,
|
|
2285
|
+
message: rule.message ?? `${field} must be of type ${rule.type}`,
|
|
2286
|
+
value
|
|
2287
|
+
});
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (rule.type === "string" || typeof value === "string") {
|
|
2292
|
+
if (rule.min !== void 0 && value.length < rule.min) {
|
|
2293
|
+
errors.push({
|
|
2294
|
+
field,
|
|
2295
|
+
message: rule.message ?? `${field} must be at least ${rule.min} characters`,
|
|
2296
|
+
value
|
|
2297
|
+
});
|
|
2298
|
+
}
|
|
2299
|
+
if (rule.max !== void 0 && value.length > rule.max) {
|
|
2300
|
+
errors.push({
|
|
2301
|
+
field,
|
|
2302
|
+
message: rule.message ?? `${field} must be at most ${rule.max} characters`,
|
|
2303
|
+
value
|
|
2304
|
+
});
|
|
2305
|
+
}
|
|
2306
|
+
if (rule.pattern && !rule.pattern.test(value)) {
|
|
2307
|
+
errors.push({
|
|
2308
|
+
field,
|
|
2309
|
+
message: rule.message ?? `${field} has invalid format`,
|
|
2310
|
+
value
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
if (rule.type === "number" || typeof value === "number") {
|
|
2315
|
+
if (rule.min !== void 0 && value < rule.min) {
|
|
2316
|
+
errors.push({
|
|
2317
|
+
field,
|
|
2318
|
+
message: rule.message ?? `${field} must be at least ${rule.min}`,
|
|
2319
|
+
value
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
if (rule.max !== void 0 && value > rule.max) {
|
|
2323
|
+
errors.push({
|
|
2324
|
+
field,
|
|
2325
|
+
message: rule.message ?? `${field} must be at most ${rule.max}`,
|
|
2326
|
+
value
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
if (rule.type === "array" && Array.isArray(value)) {
|
|
2331
|
+
if (rule.min !== void 0 && value.length < rule.min) {
|
|
2332
|
+
errors.push({
|
|
2333
|
+
field,
|
|
2334
|
+
message: rule.message ?? `${field} must have at least ${rule.min} items`,
|
|
2335
|
+
value
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
if (rule.max !== void 0 && value.length > rule.max) {
|
|
2339
|
+
errors.push({
|
|
2340
|
+
field,
|
|
2341
|
+
message: rule.message ?? `${field} must have at most ${rule.max} items`,
|
|
2342
|
+
value
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
if (rule.enum && !rule.enum.includes(value)) {
|
|
2347
|
+
errors.push({
|
|
2348
|
+
field,
|
|
2349
|
+
message: rule.message ?? `${field} must be one of: ${rule.enum.join(", ")}`,
|
|
2350
|
+
value
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
if (rule.custom) {
|
|
2354
|
+
const customError = rule.custom(value);
|
|
2355
|
+
if (customError) {
|
|
2356
|
+
errors.push({ field, message: customError, value });
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
return {
|
|
2361
|
+
valid: errors.length === 0,
|
|
2362
|
+
errors,
|
|
2363
|
+
data: errors.length === 0 ? data : void 0
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
// src/utils/validators/SchemaValidator.ts
|
|
2369
|
+
var SchemaValidator = class {
|
|
2370
|
+
static validate(data, schema) {
|
|
2371
|
+
if (typeof schema.safeParse === "function") {
|
|
2372
|
+
const result = schema.safeParse(data);
|
|
2373
|
+
if (result.success) {
|
|
2374
|
+
return { valid: true, data: result.data };
|
|
2375
|
+
}
|
|
2376
|
+
return { valid: false, errors: result.error };
|
|
2377
|
+
}
|
|
2378
|
+
if (typeof schema.parse === "function") {
|
|
2379
|
+
try {
|
|
2380
|
+
const parsed = schema.parse(data);
|
|
2381
|
+
return { valid: true, data: parsed };
|
|
2382
|
+
} catch (err) {
|
|
2383
|
+
return { valid: false, errors: err };
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (typeof schema.validate === "function") {
|
|
2387
|
+
const result = schema.validate(data);
|
|
2388
|
+
if (result.error) {
|
|
2389
|
+
return { valid: false, errors: result.error };
|
|
2390
|
+
}
|
|
2391
|
+
return { valid: true, data: result.value };
|
|
2392
|
+
}
|
|
2393
|
+
return { valid: true, data };
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
var CONFIG_FILES = [
|
|
2397
|
+
"azura.config.ts",
|
|
2398
|
+
"azura.config.js",
|
|
2399
|
+
"azura.config.mjs",
|
|
2400
|
+
"azura.config.json",
|
|
2401
|
+
"azura.config.yaml",
|
|
2402
|
+
"azura.config.yml"
|
|
2403
|
+
];
|
|
2404
|
+
var ConfigModule = class _ConfigModule {
|
|
2405
|
+
static instance = null;
|
|
2406
|
+
config = {};
|
|
2407
|
+
constructor() {
|
|
2408
|
+
}
|
|
2409
|
+
static getInstance() {
|
|
2410
|
+
if (!_ConfigModule.instance) {
|
|
2411
|
+
_ConfigModule.instance = new _ConfigModule();
|
|
2412
|
+
}
|
|
2413
|
+
return _ConfigModule.instance;
|
|
2414
|
+
}
|
|
2415
|
+
static async load(cwd) {
|
|
2416
|
+
const instance = _ConfigModule.getInstance();
|
|
2417
|
+
const baseDir = cwd ?? process.cwd();
|
|
2418
|
+
for (const file of CONFIG_FILES) {
|
|
2419
|
+
const fullPath = resolve(baseDir, file);
|
|
2420
|
+
if (!existsSync(fullPath)) continue;
|
|
2421
|
+
if (file.endsWith(".json")) {
|
|
2422
|
+
instance.config = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
2423
|
+
return instance.config;
|
|
2424
|
+
}
|
|
2425
|
+
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
|
2426
|
+
instance.config = parseSimpleYaml(readFileSync(fullPath, "utf-8"));
|
|
2427
|
+
return instance.config;
|
|
2428
|
+
}
|
|
2429
|
+
if (file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".mjs")) {
|
|
2430
|
+
try {
|
|
2431
|
+
const mod = await import(`file://${fullPath}`);
|
|
2432
|
+
instance.config = mod.default ?? mod;
|
|
2433
|
+
return instance.config;
|
|
2434
|
+
} catch {
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
return instance.config;
|
|
2439
|
+
}
|
|
2440
|
+
static loadSync(cwd) {
|
|
2441
|
+
const instance = _ConfigModule.getInstance();
|
|
2442
|
+
const baseDir = cwd ?? process.cwd();
|
|
2443
|
+
for (const file of CONFIG_FILES) {
|
|
2444
|
+
const fullPath = resolve(baseDir, file);
|
|
2445
|
+
if (!existsSync(fullPath)) continue;
|
|
2446
|
+
if (file.endsWith(".json")) {
|
|
2447
|
+
instance.config = JSON.parse(readFileSync(fullPath, "utf-8"));
|
|
2448
|
+
return instance.config;
|
|
2449
|
+
}
|
|
2450
|
+
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
|
2451
|
+
instance.config = parseSimpleYaml(readFileSync(fullPath, "utf-8"));
|
|
2452
|
+
return instance.config;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return instance.config;
|
|
2456
|
+
}
|
|
2457
|
+
static get() {
|
|
2458
|
+
return _ConfigModule.getInstance().config;
|
|
2459
|
+
}
|
|
2460
|
+
static set(config) {
|
|
2461
|
+
_ConfigModule.getInstance().config = config;
|
|
2462
|
+
}
|
|
2463
|
+
static merge(partial) {
|
|
2464
|
+
const instance = _ConfigModule.getInstance();
|
|
2465
|
+
instance.config = deepMerge(instance.config, partial);
|
|
2466
|
+
return instance.config;
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
function deepMerge(target, source) {
|
|
2470
|
+
const output = { ...target };
|
|
2471
|
+
for (const key of Object.keys(source)) {
|
|
2472
|
+
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key]) && target[key] && typeof target[key] === "object") {
|
|
2473
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
2474
|
+
} else {
|
|
2475
|
+
output[key] = source[key];
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
return output;
|
|
2479
|
+
}
|
|
2480
|
+
function parseSimpleYaml(content) {
|
|
2481
|
+
const result = {};
|
|
2482
|
+
const lines = content.split("\n");
|
|
2483
|
+
const stack = [{ indent: -1, obj: result }];
|
|
2484
|
+
for (const line of lines) {
|
|
2485
|
+
const trimmed = line.trim();
|
|
2486
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2487
|
+
const indent = line.search(/\S/);
|
|
2488
|
+
const colonIdx = trimmed.indexOf(":");
|
|
2489
|
+
if (colonIdx === -1) continue;
|
|
2490
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
2491
|
+
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
2492
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
2493
|
+
stack.pop();
|
|
2494
|
+
}
|
|
2495
|
+
const parent = stack[stack.length - 1].obj;
|
|
2496
|
+
if (!rawValue) {
|
|
2497
|
+
const newObj = {};
|
|
2498
|
+
parent[key] = newObj;
|
|
2499
|
+
stack.push({ indent, obj: newObj });
|
|
2500
|
+
} else {
|
|
2501
|
+
parent[key] = parseYamlValue(rawValue);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
return result;
|
|
2505
|
+
}
|
|
2506
|
+
function parseYamlValue(val) {
|
|
2507
|
+
if (val === "true") return true;
|
|
2508
|
+
if (val === "false") return false;
|
|
2509
|
+
if (val === "null") return null;
|
|
2510
|
+
if (/^-?\d+$/.test(val)) return parseInt(val, 10);
|
|
2511
|
+
if (/^-?\d+\.\d+$/.test(val)) return parseFloat(val);
|
|
2512
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
2513
|
+
return val.slice(1, -1);
|
|
2514
|
+
}
|
|
2515
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
2516
|
+
return val.slice(1, -1).split(",").map((v) => parseYamlValue(v.trim()));
|
|
2517
|
+
}
|
|
2518
|
+
return val;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
// src/middleware/LoggingMiddleware.ts
|
|
2522
|
+
function LoggingMiddleware() {
|
|
2523
|
+
return (ctx, next) => {
|
|
2524
|
+
const { req, res } = ctx;
|
|
2525
|
+
const start = process.hrtime.bigint();
|
|
2526
|
+
const originalEnd = res.end;
|
|
2527
|
+
res.end = function(...args) {
|
|
2528
|
+
const duration = process.hrtime.bigint() - start;
|
|
2529
|
+
const ms = Number(duration) / 1e6;
|
|
2530
|
+
res.setHeader("X-Response-Time", `${ms.toFixed(2)}ms`);
|
|
2531
|
+
return originalEnd.apply(this, args);
|
|
2532
|
+
};
|
|
2533
|
+
next();
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
export { AzuraServer, Body, CORSPlugin, CircuitBreakerPlugin, CompressionPlugin, ConfigModule, Controller, Cookies, DTOValidator, Delete, ETagPlugin, Get, Head, Headers, HealthCheckPlugin, HelmetPlugin, HttpError, HttpStatus, HttpStatusText, Ip, JWTPlugin, Logger, LoggingMiddleware, Meta, MultipartPlugin, NextFunc, Options, Param, Patch, Post, ProxyPlugin, Put, Query, RateLimitPlugin, Req, RequestIdPlugin, Res, Router, SSEManager, SSEPlugin, SchemaValidator, Session, SessionPlugin, StaticPlugin, TimeoutPlugin, UseMiddleware, applyDecorators, clearCookieHeader, parseBody, parseCookies, parseQueryString, parseUrl, resolveIp, serializeCookie, signJWT, verifyJWT };
|
|
10
2538
|
//# sourceMappingURL=index.mjs.map
|
|
11
2539
|
//# sourceMappingURL=index.mjs.map
|