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/chunk-NLSZKAPA.mjs
DELETED
|
@@ -1,1044 +0,0 @@
|
|
|
1
|
-
import { createHmac, createHash, randomUUID, randomBytes } from 'crypto';
|
|
2
|
-
import { createGzip, createDeflate } from 'zlib';
|
|
3
|
-
import { existsSync, statSync, createReadStream, mkdirSync, writeFileSync } from 'fs';
|
|
4
|
-
import { resolve, join, extname } from 'path';
|
|
5
|
-
import { request as request$1 } from 'http';
|
|
6
|
-
import { request } from 'https';
|
|
7
|
-
import { URL } from 'url';
|
|
8
|
-
|
|
9
|
-
// src/plugins/CORSPlugin.ts
|
|
10
|
-
var DEFAULT_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
|
|
11
|
-
var DEFAULT_HEADERS = ["Content-Type", "Authorization", "Accept", "X-Requested-With"];
|
|
12
|
-
function CORSPlugin(options = {}) {
|
|
13
|
-
const {
|
|
14
|
-
origins = "*",
|
|
15
|
-
methods = DEFAULT_METHODS,
|
|
16
|
-
allowedHeaders = DEFAULT_HEADERS,
|
|
17
|
-
exposedHeaders = [],
|
|
18
|
-
credentials = false,
|
|
19
|
-
maxAge = 86400,
|
|
20
|
-
preflightContinue = false
|
|
21
|
-
} = options;
|
|
22
|
-
const methodsStr = methods.join(", ");
|
|
23
|
-
const headersStr = allowedHeaders.join(", ");
|
|
24
|
-
const exposedStr = exposedHeaders.length > 0 ? exposedHeaders.join(", ") : "";
|
|
25
|
-
const isAllowedOrigin = (origin) => {
|
|
26
|
-
if (origins === "*") return credentials ? origin : "*";
|
|
27
|
-
if (typeof origins === "function") return origins(origin) ? origin : false;
|
|
28
|
-
if (typeof origins === "string") return origin === origins ? origin : false;
|
|
29
|
-
if (Array.isArray(origins)) return origins.includes(origin) ? origin : false;
|
|
30
|
-
return false;
|
|
31
|
-
};
|
|
32
|
-
return (ctx, next) => {
|
|
33
|
-
const { req, res } = ctx;
|
|
34
|
-
const origin = req.headers.origin ?? "";
|
|
35
|
-
const allowedOrigin = isAllowedOrigin(origin);
|
|
36
|
-
if (allowedOrigin) {
|
|
37
|
-
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
38
|
-
if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
39
|
-
if (exposedStr) res.setHeader("Access-Control-Expose-Headers", exposedStr);
|
|
40
|
-
if (allowedOrigin !== "*") res.setHeader("Vary", "Origin");
|
|
41
|
-
}
|
|
42
|
-
if (req.method === "OPTIONS") {
|
|
43
|
-
res.setHeader("Access-Control-Allow-Methods", methodsStr);
|
|
44
|
-
res.setHeader("Access-Control-Allow-Headers", headersStr);
|
|
45
|
-
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
46
|
-
if (!preflightContinue) {
|
|
47
|
-
res.statusCode = 204;
|
|
48
|
-
res.end();
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
next();
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// src/plugins/RateLimitPlugin.ts
|
|
57
|
-
var MemoryStore = class {
|
|
58
|
-
store = /* @__PURE__ */ new Map();
|
|
59
|
-
cleanupInterval;
|
|
60
|
-
constructor(windowMs) {
|
|
61
|
-
this.cleanupInterval = setInterval(() => {
|
|
62
|
-
const now = Date.now();
|
|
63
|
-
for (const [key, entry] of this.store) {
|
|
64
|
-
if (now >= entry.resetTime) this.store.delete(key);
|
|
65
|
-
}
|
|
66
|
-
}, windowMs);
|
|
67
|
-
if (this.cleanupInterval.unref) this.cleanupInterval.unref();
|
|
68
|
-
}
|
|
69
|
-
increment(key, windowMs) {
|
|
70
|
-
const now = Date.now();
|
|
71
|
-
const entry = this.store.get(key);
|
|
72
|
-
if (!entry || now >= entry.resetTime) {
|
|
73
|
-
const resetTime = now + windowMs;
|
|
74
|
-
this.store.set(key, { count: 1, resetTime });
|
|
75
|
-
return { totalHits: 1, resetTime: new Date(resetTime) };
|
|
76
|
-
}
|
|
77
|
-
entry.count++;
|
|
78
|
-
return { totalHits: entry.count, resetTime: new Date(entry.resetTime) };
|
|
79
|
-
}
|
|
80
|
-
decrement(key) {
|
|
81
|
-
const entry = this.store.get(key);
|
|
82
|
-
if (entry && entry.count > 0) entry.count--;
|
|
83
|
-
}
|
|
84
|
-
resetKey(key) {
|
|
85
|
-
this.store.delete(key);
|
|
86
|
-
}
|
|
87
|
-
destroy() {
|
|
88
|
-
clearInterval(this.cleanupInterval);
|
|
89
|
-
this.store.clear();
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
function RateLimitPlugin(options = {}) {
|
|
93
|
-
const {
|
|
94
|
-
windowMs = 6e4,
|
|
95
|
-
max = 100,
|
|
96
|
-
message = { error: { statusCode: 429, message: "Too many requests, please try again later" } },
|
|
97
|
-
statusCode = 429,
|
|
98
|
-
keyGenerator = (req) => req.ip,
|
|
99
|
-
skipSuccessfulRequests = false,
|
|
100
|
-
skipFailedRequests = false
|
|
101
|
-
} = options;
|
|
102
|
-
const memStore = new MemoryStore(windowMs);
|
|
103
|
-
return (ctx, next) => {
|
|
104
|
-
const { req, res } = ctx;
|
|
105
|
-
const key = keyGenerator(req);
|
|
106
|
-
const { totalHits, resetTime } = memStore.increment(key, windowMs);
|
|
107
|
-
res.setHeader("X-RateLimit-Limit", String(max));
|
|
108
|
-
res.setHeader("X-RateLimit-Remaining", String(Math.max(0, max - totalHits)));
|
|
109
|
-
res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetTime.getTime() / 1e3)));
|
|
110
|
-
if (totalHits > max) {
|
|
111
|
-
res.setHeader("Retry-After", String(Math.ceil(windowMs / 1e3)));
|
|
112
|
-
res.statusCode = statusCode;
|
|
113
|
-
const body = typeof message === "string" ? message : JSON.stringify(message);
|
|
114
|
-
res.setHeader("Content-Type", typeof message === "string" ? "text/plain" : "application/json");
|
|
115
|
-
res.end(body);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const origEnd = res.end;
|
|
119
|
-
res.end = function(...args) {
|
|
120
|
-
if (skipSuccessfulRequests && res.statusCode < 400) {
|
|
121
|
-
memStore.decrement(key);
|
|
122
|
-
}
|
|
123
|
-
if (skipFailedRequests && res.statusCode >= 400) {
|
|
124
|
-
memStore.decrement(key);
|
|
125
|
-
}
|
|
126
|
-
return origEnd.apply(this, args);
|
|
127
|
-
};
|
|
128
|
-
next();
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/plugins/HelmetPlugin.ts
|
|
133
|
-
function HelmetPlugin(options = {}) {
|
|
134
|
-
const headers = [];
|
|
135
|
-
if (options.noSniff !== false) {
|
|
136
|
-
headers.push(["X-Content-Type-Options", "nosniff"]);
|
|
137
|
-
}
|
|
138
|
-
if (options.xssFilter !== false) {
|
|
139
|
-
headers.push(["X-XSS-Protection", "0"]);
|
|
140
|
-
}
|
|
141
|
-
if (options.ieNoOpen !== false) {
|
|
142
|
-
headers.push(["X-Download-Options", "noopen"]);
|
|
143
|
-
}
|
|
144
|
-
const frameguard = options.frameguard ?? true;
|
|
145
|
-
if (frameguard !== false) {
|
|
146
|
-
const action = typeof frameguard === "object" ? frameguard.action : "sameorigin";
|
|
147
|
-
headers.push(["X-Frame-Options", action.toUpperCase()]);
|
|
148
|
-
}
|
|
149
|
-
if (options.dnsPrefetchControl !== false) {
|
|
150
|
-
const allow = typeof options.dnsPrefetchControl === "object" ? options.dnsPrefetchControl.allow : false;
|
|
151
|
-
headers.push(["X-DNS-Prefetch-Control", allow ? "on" : "off"]);
|
|
152
|
-
}
|
|
153
|
-
const hsts = options.hsts ?? true;
|
|
154
|
-
if (hsts !== false) {
|
|
155
|
-
const maxAge = typeof hsts === "object" ? hsts.maxAge : 15552e3;
|
|
156
|
-
const includeSubDomains = typeof hsts === "object" ? hsts.includeSubDomains !== false : true;
|
|
157
|
-
const preload = typeof hsts === "object" ? hsts.preload : false;
|
|
158
|
-
let value = `max-age=${maxAge}`;
|
|
159
|
-
if (includeSubDomains) value += "; includeSubDomains";
|
|
160
|
-
if (preload) value += "; preload";
|
|
161
|
-
headers.push(["Strict-Transport-Security", value]);
|
|
162
|
-
}
|
|
163
|
-
if (options.crossOriginEmbedderPolicy !== false) {
|
|
164
|
-
headers.push(["Cross-Origin-Embedder-Policy", "require-corp"]);
|
|
165
|
-
}
|
|
166
|
-
if (options.crossOriginOpenerPolicy !== false) {
|
|
167
|
-
const policy = typeof options.crossOriginOpenerPolicy === "object" ? options.crossOriginOpenerPolicy.policy : "same-origin";
|
|
168
|
-
headers.push(["Cross-Origin-Opener-Policy", policy]);
|
|
169
|
-
}
|
|
170
|
-
if (options.crossOriginResourcePolicy !== false) {
|
|
171
|
-
const policy = typeof options.crossOriginResourcePolicy === "object" ? options.crossOriginResourcePolicy.policy : "same-origin";
|
|
172
|
-
headers.push(["Cross-Origin-Resource-Policy", policy]);
|
|
173
|
-
}
|
|
174
|
-
const referrer = options.referrerPolicy ?? true;
|
|
175
|
-
if (referrer !== false) {
|
|
176
|
-
const policy = typeof referrer === "object" ? Array.isArray(referrer.policy) ? referrer.policy.join(", ") : referrer.policy : "no-referrer";
|
|
177
|
-
headers.push(["Referrer-Policy", policy]);
|
|
178
|
-
}
|
|
179
|
-
if (options.contentSecurityPolicy !== false && options.contentSecurityPolicy) {
|
|
180
|
-
const csp = options.contentSecurityPolicy;
|
|
181
|
-
const directives = Object.entries(csp).map(([key, values]) => `${key} ${values.join(" ")}`).join("; ");
|
|
182
|
-
headers.push(["Content-Security-Policy", directives]);
|
|
183
|
-
}
|
|
184
|
-
if (options.permissionsPolicy) {
|
|
185
|
-
const pp = Object.entries(options.permissionsPolicy).map(([key, values]) => `${key}=(${values.join(" ")})`).join(", ");
|
|
186
|
-
headers.push(["Permissions-Policy", pp]);
|
|
187
|
-
}
|
|
188
|
-
return (ctx, next) => {
|
|
189
|
-
for (const [name, value] of headers) {
|
|
190
|
-
ctx.res.setHeader(name, value);
|
|
191
|
-
}
|
|
192
|
-
next();
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
function base64urlEncode(data) {
|
|
196
|
-
const buf = typeof data === "string" ? Buffer.from(data) : data;
|
|
197
|
-
return buf.toString("base64url");
|
|
198
|
-
}
|
|
199
|
-
function base64urlDecode(str) {
|
|
200
|
-
return Buffer.from(str, "base64url").toString("utf-8");
|
|
201
|
-
}
|
|
202
|
-
var ALGO_MAP = {
|
|
203
|
-
HS256: "sha256",
|
|
204
|
-
HS384: "sha384",
|
|
205
|
-
HS512: "sha512"
|
|
206
|
-
};
|
|
207
|
-
function signJWT(payload, secret, options = {}) {
|
|
208
|
-
const algorithm = options.algorithm ?? "HS256";
|
|
209
|
-
const header = base64urlEncode(JSON.stringify({ alg: algorithm, typ: "JWT" }));
|
|
210
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
211
|
-
const body = { ...payload, iat: now };
|
|
212
|
-
if (options.expiresIn) {
|
|
213
|
-
body.exp = now + parseExpiration(options.expiresIn);
|
|
214
|
-
}
|
|
215
|
-
const encodedPayload = base64urlEncode(JSON.stringify(body));
|
|
216
|
-
const signature = createHmac(ALGO_MAP[algorithm] ?? "sha256", secret).update(`${header}.${encodedPayload}`).digest("base64url");
|
|
217
|
-
return `${header}.${encodedPayload}.${signature}`;
|
|
218
|
-
}
|
|
219
|
-
function verifyJWT(token, secret, options = {}) {
|
|
220
|
-
const parts = token.split(".");
|
|
221
|
-
if (parts.length !== 3) throw new Error("Invalid token format");
|
|
222
|
-
const [header, payload, signature] = parts;
|
|
223
|
-
const algorithm = options.algorithm ?? "HS256";
|
|
224
|
-
const expectedSig = createHmac(ALGO_MAP[algorithm] ?? "sha256", secret).update(`${header}.${payload}`).digest("base64url");
|
|
225
|
-
if (signature !== expectedSig) throw new Error("Invalid token signature");
|
|
226
|
-
const decoded = JSON.parse(base64urlDecode(payload));
|
|
227
|
-
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1e3)) {
|
|
228
|
-
throw new Error("Token expired");
|
|
229
|
-
}
|
|
230
|
-
return decoded;
|
|
231
|
-
}
|
|
232
|
-
function parseExpiration(exp) {
|
|
233
|
-
if (typeof exp === "number") return exp;
|
|
234
|
-
const match = exp.match(/^(\d+)(s|m|h|d)$/);
|
|
235
|
-
if (!match) return 3600;
|
|
236
|
-
const val = parseInt(match[1], 10);
|
|
237
|
-
switch (match[2]) {
|
|
238
|
-
case "s":
|
|
239
|
-
return val;
|
|
240
|
-
case "m":
|
|
241
|
-
return val * 60;
|
|
242
|
-
case "h":
|
|
243
|
-
return val * 3600;
|
|
244
|
-
case "d":
|
|
245
|
-
return val * 86400;
|
|
246
|
-
default:
|
|
247
|
-
return 3600;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
function extractToken(req) {
|
|
251
|
-
const auth = req.headers.authorization;
|
|
252
|
-
if (auth?.startsWith("Bearer ")) return auth.slice(7);
|
|
253
|
-
return req.query.token ?? req.cookies?.token ?? null;
|
|
254
|
-
}
|
|
255
|
-
function JWTPlugin(options) {
|
|
256
|
-
const {
|
|
257
|
-
secret,
|
|
258
|
-
algorithm = "HS256",
|
|
259
|
-
paths = [],
|
|
260
|
-
exclude = [],
|
|
261
|
-
getToken
|
|
262
|
-
} = options;
|
|
263
|
-
return (ctx, next) => {
|
|
264
|
-
const { req, res } = ctx;
|
|
265
|
-
const path = req.pathname;
|
|
266
|
-
if (exclude.some((p) => path.startsWith(p))) {
|
|
267
|
-
next();
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (paths.length > 0 && !paths.some((p) => path.startsWith(p))) {
|
|
271
|
-
next();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
const token = getToken ? getToken(req) : extractToken(req);
|
|
275
|
-
if (!token) {
|
|
276
|
-
res.statusCode = 401;
|
|
277
|
-
res.setHeader("Content-Type", "application/json");
|
|
278
|
-
res.end(JSON.stringify({ error: { statusCode: 401, message: "No token provided" } }));
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
try {
|
|
282
|
-
const payload = verifyJWT(token, secret, { algorithm });
|
|
283
|
-
req.user = payload;
|
|
284
|
-
next();
|
|
285
|
-
} catch (err) {
|
|
286
|
-
res.statusCode = 401;
|
|
287
|
-
res.setHeader("Content-Type", "application/json");
|
|
288
|
-
res.end(JSON.stringify({ error: { statusCode: 401, message: err.message ?? "Invalid token" } }));
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
var MemorySessionStore = class {
|
|
293
|
-
sessions = /* @__PURE__ */ new Map();
|
|
294
|
-
cleanupTimer;
|
|
295
|
-
constructor() {
|
|
296
|
-
this.cleanupTimer = setInterval(() => {
|
|
297
|
-
const now = Date.now();
|
|
298
|
-
for (const [id, session] of this.sessions) {
|
|
299
|
-
if (now >= session.expires) this.sessions.delete(id);
|
|
300
|
-
}
|
|
301
|
-
}, 6e4);
|
|
302
|
-
if (this.cleanupTimer.unref) this.cleanupTimer.unref();
|
|
303
|
-
}
|
|
304
|
-
async get(id) {
|
|
305
|
-
const session = this.sessions.get(id);
|
|
306
|
-
if (!session) return null;
|
|
307
|
-
if (Date.now() >= session.expires) {
|
|
308
|
-
this.sessions.delete(id);
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
return session.data;
|
|
312
|
-
}
|
|
313
|
-
async set(id, data, maxAge) {
|
|
314
|
-
this.sessions.set(id, { data, expires: Date.now() + maxAge });
|
|
315
|
-
}
|
|
316
|
-
async destroy(id) {
|
|
317
|
-
this.sessions.delete(id);
|
|
318
|
-
}
|
|
319
|
-
async touch(id, maxAge) {
|
|
320
|
-
const session = this.sessions.get(id);
|
|
321
|
-
if (session) session.expires = Date.now() + maxAge;
|
|
322
|
-
}
|
|
323
|
-
};
|
|
324
|
-
function generateSessionId() {
|
|
325
|
-
return randomBytes(24).toString("hex");
|
|
326
|
-
}
|
|
327
|
-
function SessionPlugin(options) {
|
|
328
|
-
const {
|
|
329
|
-
secret: _secret,
|
|
330
|
-
name = "azura.sid",
|
|
331
|
-
maxAge = 864e5,
|
|
332
|
-
secure = false,
|
|
333
|
-
httpOnly = true,
|
|
334
|
-
sameSite = "Lax",
|
|
335
|
-
store = new MemorySessionStore()
|
|
336
|
-
} = options;
|
|
337
|
-
return async (ctx, next) => {
|
|
338
|
-
const { req, res } = ctx;
|
|
339
|
-
let sessionId = req.cookies[name];
|
|
340
|
-
let sessionData = null;
|
|
341
|
-
if (sessionId) {
|
|
342
|
-
sessionData = await store.get(sessionId);
|
|
343
|
-
}
|
|
344
|
-
if (!sessionData) {
|
|
345
|
-
sessionId = generateSessionId();
|
|
346
|
-
sessionData = {};
|
|
347
|
-
}
|
|
348
|
-
req.session = sessionData;
|
|
349
|
-
const originalEnd = res.end;
|
|
350
|
-
res.end = async function(...args) {
|
|
351
|
-
await store.set(sessionId, req.session ?? {}, maxAge);
|
|
352
|
-
const cookieParts = [
|
|
353
|
-
`${name}=${sessionId}`,
|
|
354
|
-
`Path=/`,
|
|
355
|
-
`Max-Age=${Math.floor(maxAge / 1e3)}`,
|
|
356
|
-
`SameSite=${sameSite}`
|
|
357
|
-
];
|
|
358
|
-
if (httpOnly) cookieParts.push("HttpOnly");
|
|
359
|
-
if (secure) cookieParts.push("Secure");
|
|
360
|
-
const existing = res.getHeader("Set-Cookie");
|
|
361
|
-
const cookies = existing ? [...Array.isArray(existing) ? existing : [String(existing)], cookieParts.join("; ")] : [cookieParts.join("; ")];
|
|
362
|
-
res.setHeader("Set-Cookie", cookies);
|
|
363
|
-
return originalEnd.apply(this, args);
|
|
364
|
-
};
|
|
365
|
-
next();
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
var COMPRESSIBLE_TYPES = /^text\/|application\/json|application\/javascript|application\/xml|image\/svg\+xml/;
|
|
369
|
-
function CompressionPlugin(options = {}) {
|
|
370
|
-
const {
|
|
371
|
-
threshold = 1024,
|
|
372
|
-
level = 6,
|
|
373
|
-
algorithms = ["gzip", "deflate"],
|
|
374
|
-
filter
|
|
375
|
-
} = options;
|
|
376
|
-
return (ctx, next) => {
|
|
377
|
-
const { req, res } = ctx;
|
|
378
|
-
if (filter && !filter(req, res)) {
|
|
379
|
-
next();
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
const acceptEncoding = req.headers["accept-encoding"] ?? "";
|
|
383
|
-
let encoding = null;
|
|
384
|
-
for (const algo of algorithms) {
|
|
385
|
-
if (typeof acceptEncoding === "string" && acceptEncoding.includes(algo)) {
|
|
386
|
-
encoding = algo;
|
|
387
|
-
break;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
if (!encoding) {
|
|
391
|
-
next();
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
const originalEnd = res.end;
|
|
395
|
-
res.write;
|
|
396
|
-
res.end = function(chunk, encodingArg, callback) {
|
|
397
|
-
if (!chunk || typeof chunk !== "string" && !Buffer.isBuffer(chunk)) {
|
|
398
|
-
return originalEnd.call(this, chunk, encodingArg, callback);
|
|
399
|
-
}
|
|
400
|
-
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
401
|
-
if (buf.length < threshold) {
|
|
402
|
-
return originalEnd.call(this, chunk, encodingArg, callback);
|
|
403
|
-
}
|
|
404
|
-
const contentType = String(res.getHeader("Content-Type") ?? "");
|
|
405
|
-
if (!COMPRESSIBLE_TYPES.test(contentType)) {
|
|
406
|
-
return originalEnd.call(this, chunk, encodingArg, callback);
|
|
407
|
-
}
|
|
408
|
-
res.removeHeader("Content-Length");
|
|
409
|
-
res.setHeader("Content-Encoding", encoding);
|
|
410
|
-
res.setHeader("Vary", "Accept-Encoding");
|
|
411
|
-
let stream;
|
|
412
|
-
if (encoding === "gzip") {
|
|
413
|
-
stream = createGzip({ level });
|
|
414
|
-
} else {
|
|
415
|
-
stream = createDeflate({ level });
|
|
416
|
-
}
|
|
417
|
-
const chunks = [];
|
|
418
|
-
stream.on("data", (c) => chunks.push(c));
|
|
419
|
-
stream.on("end", () => {
|
|
420
|
-
const compressed = Buffer.concat(chunks);
|
|
421
|
-
res.setHeader("Content-Length", compressed.length);
|
|
422
|
-
originalEnd.call(res, compressed);
|
|
423
|
-
});
|
|
424
|
-
stream.end(buf);
|
|
425
|
-
};
|
|
426
|
-
next();
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
var MIME_TYPES = {
|
|
430
|
-
".html": "text/html; charset=utf-8",
|
|
431
|
-
".htm": "text/html; charset=utf-8",
|
|
432
|
-
".css": "text/css; charset=utf-8",
|
|
433
|
-
".js": "application/javascript; charset=utf-8",
|
|
434
|
-
".mjs": "application/javascript; charset=utf-8",
|
|
435
|
-
".json": "application/json; charset=utf-8",
|
|
436
|
-
".xml": "application/xml; charset=utf-8",
|
|
437
|
-
".txt": "text/plain; charset=utf-8",
|
|
438
|
-
".csv": "text/csv; charset=utf-8",
|
|
439
|
-
".png": "image/png",
|
|
440
|
-
".jpg": "image/jpeg",
|
|
441
|
-
".jpeg": "image/jpeg",
|
|
442
|
-
".gif": "image/gif",
|
|
443
|
-
".svg": "image/svg+xml",
|
|
444
|
-
".ico": "image/x-icon",
|
|
445
|
-
".webp": "image/webp",
|
|
446
|
-
".avif": "image/avif",
|
|
447
|
-
".woff": "font/woff",
|
|
448
|
-
".woff2": "font/woff2",
|
|
449
|
-
".ttf": "font/ttf",
|
|
450
|
-
".otf": "font/otf",
|
|
451
|
-
".eot": "application/vnd.ms-fontobject",
|
|
452
|
-
".mp4": "video/mp4",
|
|
453
|
-
".webm": "video/webm",
|
|
454
|
-
".mp3": "audio/mpeg",
|
|
455
|
-
".ogg": "audio/ogg",
|
|
456
|
-
".wav": "audio/wav",
|
|
457
|
-
".pdf": "application/pdf",
|
|
458
|
-
".zip": "application/zip",
|
|
459
|
-
".gz": "application/gzip",
|
|
460
|
-
".wasm": "application/wasm",
|
|
461
|
-
".map": "application/json"
|
|
462
|
-
};
|
|
463
|
-
function StaticPlugin(options) {
|
|
464
|
-
const {
|
|
465
|
-
root,
|
|
466
|
-
prefix = "/",
|
|
467
|
-
index = ["index.html"],
|
|
468
|
-
dotfiles = "ignore",
|
|
469
|
-
maxAge = 0,
|
|
470
|
-
etag = true,
|
|
471
|
-
lastModified = true,
|
|
472
|
-
fallthrough = true,
|
|
473
|
-
immutable = false
|
|
474
|
-
} = options;
|
|
475
|
-
const resolvedRoot = resolve(root);
|
|
476
|
-
const indexFiles = Array.isArray(index) ? index : [index];
|
|
477
|
-
const normalizedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
478
|
-
return (ctx, next) => {
|
|
479
|
-
const { req, res } = ctx;
|
|
480
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
481
|
-
next();
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
const path = req.pathname;
|
|
485
|
-
if (!path.startsWith(normalizedPrefix)) {
|
|
486
|
-
next();
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
const relativePath = path.slice(normalizedPrefix.length) || "/";
|
|
490
|
-
if (relativePath.includes("..")) {
|
|
491
|
-
next();
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
if (dotfiles !== "allow" && relativePath.split("/").some((s) => s.startsWith("."))) {
|
|
495
|
-
if (dotfiles === "deny") {
|
|
496
|
-
res.statusCode = 403;
|
|
497
|
-
res.end("Forbidden");
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
next();
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
let filePath = join(resolvedRoot, relativePath);
|
|
504
|
-
if (!existsSync(filePath)) {
|
|
505
|
-
if (fallthrough) {
|
|
506
|
-
next();
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
res.statusCode = 404;
|
|
510
|
-
res.end("Not Found");
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
let stat = statSync(filePath);
|
|
514
|
-
if (stat.isDirectory()) {
|
|
515
|
-
let found = false;
|
|
516
|
-
for (const idx of indexFiles) {
|
|
517
|
-
const candidate = join(filePath, idx);
|
|
518
|
-
if (existsSync(candidate)) {
|
|
519
|
-
filePath = candidate;
|
|
520
|
-
stat = statSync(filePath);
|
|
521
|
-
found = true;
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (!found) {
|
|
526
|
-
if (fallthrough) {
|
|
527
|
-
next();
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
res.statusCode = 404;
|
|
531
|
-
res.end("Not Found");
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
const ext = extname(filePath).toLowerCase();
|
|
536
|
-
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
537
|
-
res.setHeader("Content-Type", contentType);
|
|
538
|
-
res.setHeader("Content-Length", stat.size);
|
|
539
|
-
if (lastModified) {
|
|
540
|
-
res.setHeader("Last-Modified", stat.mtime.toUTCString());
|
|
541
|
-
}
|
|
542
|
-
if (etag) {
|
|
543
|
-
const etagValue = `W/"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
|
|
544
|
-
res.setHeader("ETag", etagValue);
|
|
545
|
-
const ifNoneMatch = req.headers["if-none-match"];
|
|
546
|
-
if (ifNoneMatch === etagValue) {
|
|
547
|
-
res.statusCode = 304;
|
|
548
|
-
res.end();
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
let cacheControl = `public, max-age=${maxAge}`;
|
|
553
|
-
if (immutable) cacheControl += ", immutable";
|
|
554
|
-
res.setHeader("Cache-Control", cacheControl);
|
|
555
|
-
if (req.method === "HEAD") {
|
|
556
|
-
res.end();
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
const stream = createReadStream(filePath);
|
|
560
|
-
stream.pipe(res);
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
function ETagPlugin(options = {}) {
|
|
564
|
-
const { weak = true } = options;
|
|
565
|
-
return (ctx, next) => {
|
|
566
|
-
const { req, res } = ctx;
|
|
567
|
-
const originalEnd = res.end;
|
|
568
|
-
res.end = function(chunk, encoding, callback) {
|
|
569
|
-
if (chunk && (typeof chunk === "string" || Buffer.isBuffer(chunk)) && res.statusCode === 200) {
|
|
570
|
-
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
571
|
-
const hash = createHash("md5").update(buf).digest("hex").slice(0, 16);
|
|
572
|
-
const etag = weak ? `W/"${hash}"` : `"${hash}"`;
|
|
573
|
-
res.setHeader("ETag", etag);
|
|
574
|
-
const ifNoneMatch = req.headers["if-none-match"];
|
|
575
|
-
if (ifNoneMatch === etag) {
|
|
576
|
-
res.statusCode = 304;
|
|
577
|
-
return originalEnd.call(this);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return originalEnd.call(this, chunk, encoding, callback);
|
|
581
|
-
};
|
|
582
|
-
next();
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
function RequestIdPlugin(options = {}) {
|
|
586
|
-
const {
|
|
587
|
-
header = "X-Request-Id",
|
|
588
|
-
generator = randomUUID
|
|
589
|
-
} = options;
|
|
590
|
-
return (ctx, next) => {
|
|
591
|
-
const { req, res } = ctx;
|
|
592
|
-
const existingId = req.headers[header.toLowerCase()];
|
|
593
|
-
const requestId = existingId ?? generator();
|
|
594
|
-
req.requestId = requestId;
|
|
595
|
-
res.setHeader(header, requestId);
|
|
596
|
-
next();
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// src/plugins/TimeoutPlugin.ts
|
|
601
|
-
function TimeoutPlugin(options) {
|
|
602
|
-
const {
|
|
603
|
-
timeout,
|
|
604
|
-
message = { error: { statusCode: 408, message: "Request Timeout" } },
|
|
605
|
-
statusCode = 408
|
|
606
|
-
} = options;
|
|
607
|
-
return (ctx, next) => {
|
|
608
|
-
const { res } = ctx;
|
|
609
|
-
let timedOut = false;
|
|
610
|
-
const timer = setTimeout(() => {
|
|
611
|
-
timedOut = true;
|
|
612
|
-
if (!res.headersSent) {
|
|
613
|
-
res.statusCode = statusCode;
|
|
614
|
-
res.setHeader("Content-Type", typeof message === "string" ? "text/plain" : "application/json");
|
|
615
|
-
res.end(typeof message === "string" ? message : JSON.stringify(message));
|
|
616
|
-
}
|
|
617
|
-
}, timeout);
|
|
618
|
-
const originalEnd = res.end;
|
|
619
|
-
res.end = function(...args) {
|
|
620
|
-
clearTimeout(timer);
|
|
621
|
-
if (!timedOut) {
|
|
622
|
-
return originalEnd.apply(this, args);
|
|
623
|
-
}
|
|
624
|
-
};
|
|
625
|
-
next();
|
|
626
|
-
};
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// src/plugins/HealthCheckPlugin.ts
|
|
630
|
-
function HealthCheckPlugin(options = {}) {
|
|
631
|
-
const {
|
|
632
|
-
path = "/health",
|
|
633
|
-
checks = {},
|
|
634
|
-
timeout: checkTimeout = 5e3
|
|
635
|
-
} = options;
|
|
636
|
-
return async (ctx, next) => {
|
|
637
|
-
const { req, res } = ctx;
|
|
638
|
-
if (req.pathname !== path) {
|
|
639
|
-
next();
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
const results = {};
|
|
643
|
-
let allHealthy = true;
|
|
644
|
-
for (const [name, check] of Object.entries(checks)) {
|
|
645
|
-
const start = Date.now();
|
|
646
|
-
try {
|
|
647
|
-
const result = await Promise.race([
|
|
648
|
-
Promise.resolve(check()),
|
|
649
|
-
new Promise(
|
|
650
|
-
(_, reject) => setTimeout(() => reject(new Error("timeout")), checkTimeout)
|
|
651
|
-
)
|
|
652
|
-
]);
|
|
653
|
-
results[name] = {
|
|
654
|
-
status: result ? "healthy" : "unhealthy",
|
|
655
|
-
duration: Date.now() - start
|
|
656
|
-
};
|
|
657
|
-
if (!result) allHealthy = false;
|
|
658
|
-
} catch {
|
|
659
|
-
results[name] = {
|
|
660
|
-
status: "unhealthy",
|
|
661
|
-
duration: Date.now() - start
|
|
662
|
-
};
|
|
663
|
-
allHealthy = false;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
const body = JSON.stringify({
|
|
667
|
-
status: allHealthy ? "healthy" : "unhealthy",
|
|
668
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
669
|
-
uptime: process.uptime(),
|
|
670
|
-
memory: process.memoryUsage(),
|
|
671
|
-
checks: results
|
|
672
|
-
});
|
|
673
|
-
res.statusCode = allHealthy ? 200 : 503;
|
|
674
|
-
res.setHeader("Content-Type", "application/json");
|
|
675
|
-
res.setHeader("Cache-Control", "no-cache, no-store");
|
|
676
|
-
res.end(body);
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
// src/plugins/CircuitBreakerPlugin.ts
|
|
681
|
-
function CircuitBreakerPlugin(options = {}) {
|
|
682
|
-
const {
|
|
683
|
-
threshold = 5,
|
|
684
|
-
timeout: breakerTimeout = 3e4,
|
|
685
|
-
resetTimeout = 6e4,
|
|
686
|
-
halfOpenRequests = 1,
|
|
687
|
-
monitor
|
|
688
|
-
} = options;
|
|
689
|
-
let state = "CLOSED";
|
|
690
|
-
let failures = 0;
|
|
691
|
-
let successes = 0;
|
|
692
|
-
let lastFailureTime = 0;
|
|
693
|
-
let halfOpenCount = 0;
|
|
694
|
-
const transition = (newState) => {
|
|
695
|
-
if (state !== newState) {
|
|
696
|
-
state = newState;
|
|
697
|
-
monitor?.(state);
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
return (ctx, next) => {
|
|
701
|
-
const { res } = ctx;
|
|
702
|
-
if (state === "OPEN") {
|
|
703
|
-
if (Date.now() - lastFailureTime >= resetTimeout) {
|
|
704
|
-
transition("HALF_OPEN");
|
|
705
|
-
halfOpenCount = 0;
|
|
706
|
-
} else {
|
|
707
|
-
res.statusCode = 503;
|
|
708
|
-
res.setHeader("Content-Type", "application/json");
|
|
709
|
-
res.setHeader("Retry-After", String(Math.ceil(resetTimeout / 1e3)));
|
|
710
|
-
res.end(
|
|
711
|
-
JSON.stringify({
|
|
712
|
-
error: { statusCode: 503, message: "Service temporarily unavailable (circuit open)" }
|
|
713
|
-
})
|
|
714
|
-
);
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (state === "HALF_OPEN" && halfOpenCount >= halfOpenRequests) {
|
|
719
|
-
res.statusCode = 503;
|
|
720
|
-
res.setHeader("Content-Type", "application/json");
|
|
721
|
-
res.end(
|
|
722
|
-
JSON.stringify({
|
|
723
|
-
error: { statusCode: 503, message: "Service temporarily unavailable (circuit half-open)" }
|
|
724
|
-
})
|
|
725
|
-
);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
if (state === "HALF_OPEN") halfOpenCount++;
|
|
729
|
-
const originalEnd = res.end;
|
|
730
|
-
res.end = function(...args) {
|
|
731
|
-
if (res.statusCode >= 500) {
|
|
732
|
-
failures++;
|
|
733
|
-
lastFailureTime = Date.now();
|
|
734
|
-
if (state === "HALF_OPEN" || failures >= threshold) {
|
|
735
|
-
transition("OPEN");
|
|
736
|
-
}
|
|
737
|
-
} else {
|
|
738
|
-
if (state === "HALF_OPEN") {
|
|
739
|
-
successes++;
|
|
740
|
-
if (successes >= halfOpenRequests) {
|
|
741
|
-
failures = 0;
|
|
742
|
-
successes = 0;
|
|
743
|
-
transition("CLOSED");
|
|
744
|
-
}
|
|
745
|
-
} else {
|
|
746
|
-
failures = Math.max(0, failures - 1);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
return originalEnd.apply(this, args);
|
|
750
|
-
};
|
|
751
|
-
next();
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// src/plugins/SSEPlugin.ts
|
|
756
|
-
var SSEManager = class {
|
|
757
|
-
clients = /* @__PURE__ */ new Map();
|
|
758
|
-
clientCounter = 0;
|
|
759
|
-
addClient(res) {
|
|
760
|
-
const id = `sse-${++this.clientCounter}-${Date.now()}`;
|
|
761
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
762
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
763
|
-
res.setHeader("Connection", "keep-alive");
|
|
764
|
-
res.setHeader("X-Accel-Buffering", "no");
|
|
765
|
-
res.statusCode = 200;
|
|
766
|
-
res.flushHeaders();
|
|
767
|
-
const client = {
|
|
768
|
-
id,
|
|
769
|
-
res,
|
|
770
|
-
send(event, data, eventId) {
|
|
771
|
-
const serialized = typeof data === "string" ? data : JSON.stringify(data);
|
|
772
|
-
let message = "";
|
|
773
|
-
if (eventId) message += `id: ${eventId}
|
|
774
|
-
`;
|
|
775
|
-
message += `event: ${event}
|
|
776
|
-
`;
|
|
777
|
-
message += `data: ${serialized}
|
|
778
|
-
|
|
779
|
-
`;
|
|
780
|
-
res.write(message);
|
|
781
|
-
},
|
|
782
|
-
close() {
|
|
783
|
-
res.end();
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
this.clients.set(id, client);
|
|
787
|
-
res.on("close", () => {
|
|
788
|
-
this.clients.delete(id);
|
|
789
|
-
});
|
|
790
|
-
return client;
|
|
791
|
-
}
|
|
792
|
-
broadcast(event, data, id) {
|
|
793
|
-
for (const client of this.clients.values()) {
|
|
794
|
-
client.send(event, data, id);
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
getClient(id) {
|
|
798
|
-
return this.clients.get(id);
|
|
799
|
-
}
|
|
800
|
-
getClientCount() {
|
|
801
|
-
return this.clients.size;
|
|
802
|
-
}
|
|
803
|
-
closeAll() {
|
|
804
|
-
for (const client of this.clients.values()) {
|
|
805
|
-
client.close();
|
|
806
|
-
}
|
|
807
|
-
this.clients.clear();
|
|
808
|
-
}
|
|
809
|
-
};
|
|
810
|
-
function SSEPlugin(options) {
|
|
811
|
-
const {
|
|
812
|
-
path,
|
|
813
|
-
heartbeatInterval = 3e4,
|
|
814
|
-
retry = 3e3,
|
|
815
|
-
maxClients = 1e3
|
|
816
|
-
} = options;
|
|
817
|
-
const manager = new SSEManager();
|
|
818
|
-
let heartbeat = null;
|
|
819
|
-
if (heartbeatInterval > 0) {
|
|
820
|
-
heartbeat = setInterval(() => {
|
|
821
|
-
manager.broadcast("heartbeat", { time: Date.now() });
|
|
822
|
-
}, heartbeatInterval);
|
|
823
|
-
if (heartbeat.unref) heartbeat.unref();
|
|
824
|
-
}
|
|
825
|
-
const plugin = Object.assign(
|
|
826
|
-
(ctx, next) => {
|
|
827
|
-
const { req, res } = ctx;
|
|
828
|
-
if (req.pathname !== path || req.method !== "GET") {
|
|
829
|
-
next();
|
|
830
|
-
return;
|
|
831
|
-
}
|
|
832
|
-
if (manager.getClientCount() >= maxClients) {
|
|
833
|
-
res.statusCode = 503;
|
|
834
|
-
res.setHeader("Content-Type", "application/json");
|
|
835
|
-
res.end(JSON.stringify({ error: { statusCode: 503, message: "Too many SSE connections" } }));
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
const client = manager.addClient(res);
|
|
839
|
-
res.write(`retry: ${retry}
|
|
840
|
-
|
|
841
|
-
`);
|
|
842
|
-
client.send("connected", { clientId: client.id });
|
|
843
|
-
},
|
|
844
|
-
{ manager }
|
|
845
|
-
);
|
|
846
|
-
return plugin;
|
|
847
|
-
}
|
|
848
|
-
function ProxyPlugin(options) {
|
|
849
|
-
const {
|
|
850
|
-
target,
|
|
851
|
-
pathRewrite = {},
|
|
852
|
-
changeOrigin = true,
|
|
853
|
-
timeout = 3e4,
|
|
854
|
-
headers: extraHeaders = {},
|
|
855
|
-
onProxyReq,
|
|
856
|
-
onProxyRes
|
|
857
|
-
} = options;
|
|
858
|
-
const targetUrl = new URL(target);
|
|
859
|
-
const isHttps = targetUrl.protocol === "https:";
|
|
860
|
-
const requestFn = isHttps ? request : request$1;
|
|
861
|
-
return (ctx, next) => {
|
|
862
|
-
const { req, res } = ctx;
|
|
863
|
-
let targetPath = req.pathname;
|
|
864
|
-
for (const [from, to] of Object.entries(pathRewrite)) {
|
|
865
|
-
targetPath = targetPath.replace(new RegExp(from), to);
|
|
866
|
-
}
|
|
867
|
-
const search = req.url?.includes("?") ? req.url.slice(req.url.indexOf("?")) : "";
|
|
868
|
-
const fullPath = targetPath + search;
|
|
869
|
-
const proxyHeaders = {};
|
|
870
|
-
for (const [key, val] of Object.entries(req.headers)) {
|
|
871
|
-
if (val) proxyHeaders[key] = Array.isArray(val) ? val.join(", ") : val;
|
|
872
|
-
}
|
|
873
|
-
if (changeOrigin) {
|
|
874
|
-
proxyHeaders.host = targetUrl.host;
|
|
875
|
-
}
|
|
876
|
-
Object.assign(proxyHeaders, extraHeaders);
|
|
877
|
-
const proxyReq = requestFn(
|
|
878
|
-
{
|
|
879
|
-
hostname: targetUrl.hostname,
|
|
880
|
-
port: targetUrl.port || (isHttps ? 443 : 80),
|
|
881
|
-
path: fullPath,
|
|
882
|
-
method: req.method,
|
|
883
|
-
headers: proxyHeaders,
|
|
884
|
-
timeout
|
|
885
|
-
},
|
|
886
|
-
(proxyRes) => {
|
|
887
|
-
onProxyRes?.(proxyRes, res);
|
|
888
|
-
res.statusCode = proxyRes.statusCode ?? 502;
|
|
889
|
-
for (const [key, val] of Object.entries(proxyRes.headers)) {
|
|
890
|
-
if (val) res.setHeader(key, val);
|
|
891
|
-
}
|
|
892
|
-
proxyRes.pipe(res);
|
|
893
|
-
}
|
|
894
|
-
);
|
|
895
|
-
proxyReq.on("error", (err) => {
|
|
896
|
-
if (!res.headersSent) {
|
|
897
|
-
res.statusCode = 502;
|
|
898
|
-
res.setHeader("Content-Type", "application/json");
|
|
899
|
-
res.end(JSON.stringify({ error: { statusCode: 502, message: "Bad Gateway", details: err.message } }));
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
proxyReq.on("timeout", () => {
|
|
903
|
-
proxyReq.destroy();
|
|
904
|
-
if (!res.headersSent) {
|
|
905
|
-
res.statusCode = 504;
|
|
906
|
-
res.setHeader("Content-Type", "application/json");
|
|
907
|
-
res.end(JSON.stringify({ error: { statusCode: 504, message: "Gateway Timeout" } }));
|
|
908
|
-
}
|
|
909
|
-
});
|
|
910
|
-
onProxyReq?.(proxyReq, req);
|
|
911
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
912
|
-
req.pipe(proxyReq);
|
|
913
|
-
} else {
|
|
914
|
-
proxyReq.end();
|
|
915
|
-
}
|
|
916
|
-
};
|
|
917
|
-
}
|
|
918
|
-
function MultipartPlugin(options = {}) {
|
|
919
|
-
const {
|
|
920
|
-
maxFileSize = 10 * 1024 * 1024,
|
|
921
|
-
maxFiles = 10,
|
|
922
|
-
maxFieldSize = 1024 * 1024,
|
|
923
|
-
maxFields = 50,
|
|
924
|
-
allowedMimeTypes,
|
|
925
|
-
uploadDir
|
|
926
|
-
} = options;
|
|
927
|
-
if (uploadDir && !existsSync(uploadDir)) {
|
|
928
|
-
mkdirSync(uploadDir, { recursive: true });
|
|
929
|
-
}
|
|
930
|
-
return async (ctx, next) => {
|
|
931
|
-
const { req } = ctx;
|
|
932
|
-
const contentType = req.headers["content-type"] ?? "";
|
|
933
|
-
if (!contentType.startsWith("multipart/form-data")) {
|
|
934
|
-
next();
|
|
935
|
-
return;
|
|
936
|
-
}
|
|
937
|
-
const boundaryMatch = contentType.match(/boundary=([^\s;]+)/);
|
|
938
|
-
if (!boundaryMatch) {
|
|
939
|
-
next();
|
|
940
|
-
return;
|
|
941
|
-
}
|
|
942
|
-
const boundary = boundaryMatch[1];
|
|
943
|
-
try {
|
|
944
|
-
const { fields, files } = await parseMultipart(req, boundary, {
|
|
945
|
-
maxFileSize,
|
|
946
|
-
maxFiles,
|
|
947
|
-
maxFieldSize,
|
|
948
|
-
maxFields,
|
|
949
|
-
allowedMimeTypes
|
|
950
|
-
});
|
|
951
|
-
if (uploadDir) {
|
|
952
|
-
for (const file of files) {
|
|
953
|
-
const ext = file.filename.includes(".") ? file.filename.slice(file.filename.lastIndexOf(".")) : "";
|
|
954
|
-
const savedName = `${randomBytes(16).toString("hex")}${ext}`;
|
|
955
|
-
const savePath = join(uploadDir, savedName);
|
|
956
|
-
writeFileSync(savePath, file.buffer);
|
|
957
|
-
file.path = savePath;
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
req.files = files;
|
|
961
|
-
req.body = { ...req.body ?? {}, ...fields };
|
|
962
|
-
next();
|
|
963
|
-
} catch (err) {
|
|
964
|
-
const { res } = ctx;
|
|
965
|
-
res.statusCode = 400;
|
|
966
|
-
res.setHeader("Content-Type", "application/json");
|
|
967
|
-
res.end(JSON.stringify({ error: { statusCode: 400, message: err.message } }));
|
|
968
|
-
}
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
async function parseMultipart(req, boundary, opts) {
|
|
972
|
-
const raw = await collectBody(req);
|
|
973
|
-
const delimiter = Buffer.from(`--${boundary}`);
|
|
974
|
-
const endDelimiter = Buffer.from(`--${boundary}--`);
|
|
975
|
-
const fields = {};
|
|
976
|
-
const files = [];
|
|
977
|
-
let fieldCount = 0;
|
|
978
|
-
let start = bufferIndexOf(raw, delimiter, 0);
|
|
979
|
-
if (start === -1) return { fields, files };
|
|
980
|
-
start += delimiter.length + 2;
|
|
981
|
-
while (start < raw.length) {
|
|
982
|
-
const end = bufferIndexOf(raw, delimiter, start);
|
|
983
|
-
if (end === -1) break;
|
|
984
|
-
const part = raw.subarray(start, end - 2);
|
|
985
|
-
const headerEnd = bufferIndexOf(part, Buffer.from("\r\n\r\n"), 0);
|
|
986
|
-
if (headerEnd === -1) {
|
|
987
|
-
start = end + delimiter.length + 2;
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
const headerStr = part.subarray(0, headerEnd).toString("utf-8");
|
|
991
|
-
const body = part.subarray(headerEnd + 4);
|
|
992
|
-
const nameMatch = headerStr.match(/name="([^"]+)"/);
|
|
993
|
-
const filenameMatch = headerStr.match(/filename="([^"]*)"/);
|
|
994
|
-
const typeMatch = headerStr.match(/Content-Type:\s*(.+)/i);
|
|
995
|
-
if (filenameMatch && nameMatch) {
|
|
996
|
-
if (files.length >= opts.maxFiles) throw new Error("Too many files");
|
|
997
|
-
if (body.length > opts.maxFileSize) throw new Error(`File ${filenameMatch[1]} exceeds max size`);
|
|
998
|
-
const mimetype = typeMatch?.[1]?.trim() ?? "application/octet-stream";
|
|
999
|
-
if (opts.allowedMimeTypes && !opts.allowedMimeTypes.includes(mimetype)) {
|
|
1000
|
-
throw new Error(`File type ${mimetype} not allowed`);
|
|
1001
|
-
}
|
|
1002
|
-
files.push({
|
|
1003
|
-
fieldname: nameMatch[1],
|
|
1004
|
-
filename: filenameMatch[1],
|
|
1005
|
-
mimetype,
|
|
1006
|
-
size: body.length,
|
|
1007
|
-
buffer: Buffer.from(body)
|
|
1008
|
-
});
|
|
1009
|
-
} else if (nameMatch) {
|
|
1010
|
-
if (++fieldCount > opts.maxFields) throw new Error("Too many fields");
|
|
1011
|
-
if (body.length > opts.maxFieldSize) throw new Error(`Field ${nameMatch[1]} exceeds max size`);
|
|
1012
|
-
fields[nameMatch[1]] = body.toString("utf-8");
|
|
1013
|
-
}
|
|
1014
|
-
start = end + delimiter.length;
|
|
1015
|
-
if (bufferIndexOf(raw, endDelimiter, end) === end) break;
|
|
1016
|
-
start += 2;
|
|
1017
|
-
}
|
|
1018
|
-
return { fields, files };
|
|
1019
|
-
}
|
|
1020
|
-
function collectBody(stream) {
|
|
1021
|
-
return new Promise((resolve2, reject) => {
|
|
1022
|
-
const chunks = [];
|
|
1023
|
-
stream.on("data", (chunk) => chunks.push(chunk));
|
|
1024
|
-
stream.on("end", () => resolve2(Buffer.concat(chunks)));
|
|
1025
|
-
stream.on("error", reject);
|
|
1026
|
-
});
|
|
1027
|
-
}
|
|
1028
|
-
function bufferIndexOf(buf, search, offset) {
|
|
1029
|
-
for (let i = offset; i <= buf.length - search.length; i++) {
|
|
1030
|
-
let found = true;
|
|
1031
|
-
for (let j = 0; j < search.length; j++) {
|
|
1032
|
-
if (buf[i + j] !== search[j]) {
|
|
1033
|
-
found = false;
|
|
1034
|
-
break;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
if (found) return i;
|
|
1038
|
-
}
|
|
1039
|
-
return -1;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
export { CORSPlugin, CircuitBreakerPlugin, CompressionPlugin, ETagPlugin, HealthCheckPlugin, HelmetPlugin, JWTPlugin, MultipartPlugin, ProxyPlugin, RateLimitPlugin, RequestIdPlugin, SSEManager, SSEPlugin, SessionPlugin, StaticPlugin, TimeoutPlugin, signJWT, verifyJWT };
|
|
1043
|
-
//# sourceMappingURL=chunk-NLSZKAPA.mjs.map
|
|
1044
|
-
//# sourceMappingURL=chunk-NLSZKAPA.mjs.map
|